diff --git a/.mintignore b/.mintignore index 2da84ff..4ec7e17 100644 --- a/.mintignore +++ b/.mintignore @@ -3,8 +3,39 @@ # — but in practice the dev server still tries to MDX-parse files inside # .github (e.g. pull_request_template.md, which uses HTML +# markers into it — and so `make check`, and the pre-push hook that runs +# it, were red on develop from the day they were added. +# +# Measured, not assumed — each candidate was probed by appending an HTML +# comment and re-running `mint broken-links` (2026-08-14, mint 4.2.802): +# +# CLAUDE.md PARSED -> listed below (this is the one that broke) +# AGENTS.md PARSED -> listed below +# TERMINOLOGY.md PARSED -> listed below +# .cursor/BUGBOT.md PARSED -> listed below (via .cursor/) +# README.md skipped -> auto-ignore genuinely holds here +# CONTRIBUTING.md skipped -> auto-ignore genuinely holds here +# .claude/** skipped -> auto-ignore genuinely holds here +# +# None of these are pages: no root markdown file appears in docs.json +# navigation, so ignoring them checks nothing less. They are listed +# pre-emptively rather than reactively — any of them can grow an HTML +# comment (the org-standards sync is automated and will keep writing +# them), and the failure mode is a red run for every contributor, not a +# local nuisance. .github/ .github/** +.cursor/ +.cursor/** +CLAUDE.md +AGENTS.md +TERMINOLOGY.md # Draft content drafts/ diff --git a/Makefile b/Makefile index 6cc4fbf..164d4e9 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,23 @@ help: @echo " check mint broken-links (~2 s) — run this before every push" @echo " check-all check + the SDK install-extras gate (needs network)" @echo " setup npm i -g mint" + @echo " install-hooks (re)install the git pre-push hook that runs 'make check'" @echo @echo " dev mint dev — local preview on http://localhost:3000" # ---- check ------------------------------------------------------- # -# MEASURED at 1.8 s on the current tree, and green. +# MEASURED at 1.8 s warm (2.9 s cold) on the current tree, and green. +# +# The "and green" half of that claim was false from the day it was +# written, and is only true again as of this commit. `mint broken-links` +# MDX-parses the repo-meta markdown at the root, and the org-standards +# sync had already written HTML comments — which MDX cannot represent — +# into CLAUDE.md on 2026-08-10, the day before this target landed. So +# `make check` was red on develop for its entire life up to here, and the +# pre-push hook that runs it would have blocked every push. Fixed by +# listing those files in .mintignore, which carries the reasoning and the +# per-file measurements. # # This repo has no test suite. The automated checks on a docs PR are the # org-shared ones (gitleaks + house-rules, which need the shared checker @@ -42,8 +53,12 @@ help: # `mint broken-links` is what catches a bad link before pushing. It is # also the check that would have caught the class of problem # preview-page-coverage.yml exists to catch, one step earlier. +# +# The prerequisite is spelled `guard-toolchain` — the uniform name the pre-push +# hook asks for before it runs `make check` (backend#1995) — and in this repo +# that is guard-mint. .PHONY: check -check: guard-mint +check: guard-toolchain $(MINT) broken-links # check-all adds the one gate that needs network: every documented @@ -65,6 +80,7 @@ check-sdk-extras: setup: npm i -g mint @echo "==> setup: mint installed; run 'make check'" + @$(MAKE) --no-print-directory install-hooks # ---- individual targets ------------------------------------------ @@ -75,7 +91,150 @@ guard-mint: echo " npm i -g mint (or: make setup)"; \ exit 1; } +# guard-toolchain: is the toolchain `check` needs on PATH at all? +# +# The uniform name every repo exposes, so the pre-push hook can ask one +# question — "can this shell run check at all?" — and skip itself on failure +# rather than hard-failing the push on "mint: command not found" +# (backend#1995). GUI/IDE git clients launch hooks with a minimal PATH, where +# /usr/bin/make is present but `npm i -g mint` output is not. +# +# Reuses guard-mint rather than making a second copy of that check: `check` +# depends on this target, so the mint probe lives in exactly one place and +# cannot drift as `check` changes. (`check-all` additionally needs python3, +# which is a base-system tool and is not gated here; the hook only runs +# `check`.) +# +# node is checked too, because mint IS a node program — its bin is a +# `#!/usr/bin/env node` script. mint on PATH without node on PATH is a REAL +# combination: `npm i -g mint` can land the shim in a prefix a GUI hook shell +# has while nvm/fnm/volta put node somewhere it does not. Then mint is found and +# dies with "env: node: No such file or directory", exit 127 — the same hard +# failure backend#1995 is about. +# +# TOOLS, NOT DEPENDENCIES: it asks whether mint and node are on PATH, never +# whether the docs tree is in a state mint would accept. A broken link is a real +# failure and must NOT be skipped just because "cannot run" and "runs and fails" +# look similar from the outside. +.PHONY: guard-toolchain +guard-toolchain: guard-mint + @command -v node >/dev/null 2>&1 || { \ + echo "node is not on PATH — the Mintlify CLI is a Node program; install Node, then:"; \ + echo " make setup"; \ + exit 1; } + # dev: local preview, per README.md. .PHONY: dev dev: guard-mint $(MINT) dev + +# install-hooks: put a pre-push hook in place that runs `make check`, so the +# canon's "run the tests before you push" is carried by the tooling rather than +# by memory. Factored out of `setup` so it is independently runnable and +# testable, and so a contributor who only wants the hook need not rerun the +# full `make setup`. +# +# Honest by design: the hook catches FORGETTING, not defiance — `git push +# --no-verify` skips it and always will. And it refuses to clobber a pre-push +# hook that is already there and not ours (e.g. one the pre-commit framework +# manages), rather than silently stomping a contributor's setup. +# +# `git rev-parse --git-path hooks` (not a hard-coded `.git/hooks`) so it lands +# in the right place inside a linked worktree or a submodule, where the git dir +# is not `.git`. +# +# The core.hooksPath guard below resolves the HOOKS DIRECTORY ITSELF, not its +# parent (frontend-app#809). Two cases the parent-based version got wrong: +# +# symlink core.hooksPath=.githooks where .githooks is a symlink to a shared +# dir. `dirname` is the checkout root, which resolves in-repo, so it +# installed — and the write went THROUGH the symlink into the shared +# dir. The one path element that can point elsewhere was the only +# one never resolved. +# worktree a linked worktree whose core.hooksPath is the main repo's +# .git/hooks. The parent is outside the worktree's toplevel, so it +# skipped — even though that directory belongs to the SAME +# repository, and the worktree silently got no hook. +# +# So: resolve the hooks dir itself when it exists, else the deepest existing +# ancestor (there is no symlink left to resolve below that), and count it as +# in-repo if it is under EITHER the worktree toplevel OR the repo's common git +# dir. The common-git-dir arm is what fixes the worktree case without +# re-opening the shared-dir case the guard legitimately exists to catch. An +# unresolvable path still skips: "cannot tell" is not evidence that it is ours. +.PHONY: install-hooks +install-hooks: + @if ! git rev-parse --git-dir >/dev/null 2>&1; then \ + echo "note: not a git checkout — skipping pre-push hook install"; \ + elif hp="$$(git config --get core.hooksPath 2>/dev/null || true)"; [ -n "$$hp" ] && { \ + hd="$$(git rev-parse --git-path hooks)"; \ + case "$$hd" in /*) hdd="$$hd";; *) hdd="$$PWD/$$hd";; esac; \ + hdx="$$hdd"; \ + while [ ! -d "$$hdx" ] && [ "$$hdx" != "$$(dirname "$$hdx")" ]; do \ + hdx="$$(dirname "$$hdx")"; \ + done; \ + chd="$$(cd "$$hdx" 2>/dev/null && pwd -P || true)"; \ + ctop="$$(cd "$$(git rev-parse --show-toplevel)" && pwd -P)"; \ + cgd="$$(cd "$$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P || true)"; \ + inr=0; \ + case "$$chd/" in "$$ctop/"*) inr=1;; esac; \ + if [ -n "$$cgd" ]; then case "$$chd/" in "$$cgd/"*) inr=1;; esac; fi; \ + [ -z "$$chd" ] || [ "$$inr" = 0 ]; \ + }; then \ + echo "note: core.hooksPath is set to '$$hp' (resolves to '$$chd'), outside this repo — skipping."; \ + echo " That is a shared hooks dir; installing here would run 'make check' from every repo you push."; \ + echo " Add 'make check' to that hook by hand if you want it everywhere."; \ + else \ + hook="$$(git rev-parse --git-path hooks)/pre-push"; \ + if [ -e "$$hook" ] && ! grep -q 'tracebloc pre-push hook' "$$hook" 2>/dev/null; then \ + echo "note: $$hook already exists and is not ours — leaving it untouched."; \ + echo " add 'make check' to it, or remove it and re-run 'make install-hooks'."; \ + else \ + mkdir -p "$$(dirname "$$hook")" && \ + printf '%s\n' \ + '#!/bin/sh' \ + '# tracebloc pre-push hook installed by make setup (backend#1606).' \ + '# Runs make check so a push that would be red in CI is caught locally first.' \ + '# It catches forgetting, not defiance: git push --no-verify skips it.' \ + '#' \ + '# Nothing to check on a delete/no-op push: a branch delete streams a' \ + '# local sha of all-zeros on stdin (no new commits). Skip so a red tree' \ + '# cannot block "git push --delete", and cleanup pushes stay free.' \ + 'z=0000000000000000000000000000000000000000' \ + 'had_update=0' \ + 'while read -r _ local_sha _ _; do' \ + ' [ "$$local_sha" != "$$z" ] && had_update=1' \ + 'done' \ + '[ "$$had_update" = 0 ] && exit 0' \ + '#' \ + '# Degrade gracefully when the toolchain is absent: GUI/IDE git clients' \ + '# (Tower, GitKraken, VS Code) launch hooks with a minimal PATH, so make' \ + '# may be missing — and several do not expose --no-verify. Skipping beats' \ + '# hard-blocking every push with "make: command not found".' \ + 'command -v make >/dev/null 2>&1 || exit 0' \ + '#' \ + '# Git exports GIT_DIR/GIT_WORK_TREE/etc into hook processes; a nested git' \ + '# invocation (from a test, tool, or setuptools-scm) then fails in a linked' \ + '# worktree with exit status 128. Clear them so the make runs below behave' \ + '# as they do from an ordinary shell.' \ + 'unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY' \ + '#' \ + '# Guarding on make alone was not enough (backend#1995): /usr/bin/make ships' \ + '# with the Xcode CLT and sits on the default launchd PATH, while the tools' \ + '# make check actually runs — yarn/node via nvm/fnm/volta, mint via npm -g —' \ + '# are put on PATH by shell rc files this hook shell never sources. So the' \ + '# skip above passed and the push then hard-failed on "command not found":' \ + '# exactly the outcome the skip exists to prevent, and VS Code offers no' \ + '# --no-verify on push.' \ + '#' \ + '# Ask the Makefile rather than restating the tool list here: guard-toolchain' \ + '# is a prerequisite of check itself, so it cannot drift when check gains a' \ + '# dependency. It guards on the TOOLS, not on installed dependencies — a' \ + '# missing node_modules is a real failure and must not be skipped.' \ + 'make guard-toolchain >/dev/null 2>&1 || exit 0' \ + 'exec make check' > "$$hook" && \ + chmod +x "$$hook" && \ + echo "==> pre-push hook installed at $$hook" && \ + echo " 'make check' now runs before each push (skip once with: git push --no-verify)"; \ + fi; \ + fi