Development record
2026-08-26 — bughunt round 7 (state #368): hardened the new issue-resolution
2026-08-26 — bughunt round 7 (state #368): hardened the new issue-resolution gate scripts. check-issue-resolution.sh now cd's to the repo root (RS003 no longer passes vacuously from a subdirectory), extracts iss-ids non-fatally (a non-record file last in the diff no longer silently aborts RS001/RS002 under set -e), and RS002 reads each changed record's frontmatter via a helper shared with RS003 instead of the raw diff (a body-level commit: example is no longer reachability-checked); check-issue-resolution-cases.sh gained the check-attribution-cases hermetic git-env scrub (iss-28/iss-313 sibling authored four days after that fix). iss-2608261040378346, iss-2608261041020040. Doc corrections: the 05-internals and 04-surfaces indexes no longer call the shipped site check gates / explorer / deploy design targets (10-site.md is the correct side; adr-48), brief/README nine→ten internals chapters, capture.md --shipped-in named as derivation-time not resolve-time validation, CONTEXT surface_coverage bullet names the sub-verb CLI-tree pass. iss-2608261041027043, iss-2608261041029596, iss-2608261041027268, iss-2608261041020419. Recorded not fixed (autonomous-scope limits): release.yml feeds an unvalidated tag into make build's -ldflags shell recipe — a shape-check-parity gap vs site.yml, insider-bounded, deferred because CI cannot exercise the release pipeline (iss-2608261041218890); the principle spec-moves-with-the-surface sits unpromoted while its named surface_coverage gate is armed at blocker — promotion is a governance act for the maintainer (iss-2608261041210476). Refuted: a resolved_by bare-null "regression" (the shape is unwritable and the new rejection removes a phantom empty-pointer); the release.yml verify job "not mirroring ci" (a deliberate, gate_lockstep-checked subset).
Frontmatter
| id | dcn-2608260000000001 |
|---|---|
| type | decision |
| date | 2026-08-26 |
.abcd/work/decisions/dcn-2608260000000001-bughunt-round-7-state-368-hardened-the.md
open on GitHub ↗ · commit history ↗
mentions11
- adr-48The website deploys on release, not on merge
- iss-28scaffold a hermetic git environment for tests that shell out to git: any test invoking git as a subprocess can read the developer's real ~/.gitconfig (identity, aliases, includeIf, hooks path) — non-determinism and identity leakage into fixtures — or, via one un-scoped call (missing -C/cwd) or corrupted repo state, mutate the ambient repo's config, refs, or history. Per-call temp-repo scoping is defence-in-breadth only; the robust fix also isolates the git environment. Ship a shared hermetic-git helper in the generated test scaffolding, per target language (Go gitIsolatedEnv(), pytest fixture, shell shim), that pins HOME and XDG_CONFIG_HOME to a per-test temp dir, sets GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM to /dev/null (or GIT_CONFIG_NOSYSTEM=1), supplies identity via GIT_AUTHOR_*/GIT_COMMITTER_* env rather than git config, sets GIT_TERMINAL_PROMPT=0 and core.hooksPath=/dev/null, and is always combined with scoping to the temp repo — documented in the scaffolded repo's testing conventions/AGENTS.md so it is the path of least resistance. Optional enforcement: a lint/pre-commit check flagging git subprocesses in test code that bypass the helper, turning convention into guarantee. Exactly the cross-repo hygiene the scaffolder exists to standardise; removes a whole class of flaky-test and repo-pollution bugs.
- iss-313check-attribution-cases.sh scratch repos are not hermetic so an inherited GIT_DIR makes the corpus report green while rewriting the ambient repository history
- iss-2608261040378346check-issue-resolution.sh scans cwd-relative and dies silently on a non-iss path, and RS002 reachability-checks a commit: line in a record body. Three sibling holes in the new issue-resolution gate: (1) it never cd's to the repo toplevel like check-reviews.sh does, so a git pathspec run from a subdirectory matches nothing and the gate reports a clean pass having scanned zero records — RS003, the drift detector with no other check, passes vacuously; (2) under set -euo pipefail the id extractors end a matched case with 'basename | grep ^iss-', which exits 1 on a non-iss filename, so when that is the last diff entry the while-loop and the closed= assignment inherit the failure and the script dies exit 1 with no diagnostic, RS001/RS002 never running; (3) RS002 greps the raw range diff for a '+ commit:' line with no frontmatter boundary, so a commit: example in a record's prose body is reachability-checked and raises a false violation — the exact boundary RS003 already draws. Fix: cd to toplevel; make the id extraction non-fatal; read the frontmatter per changed record via a helper RS003 shares.
- iss-2608261041020040check-issue-resolution-cases.sh runs its scratch-repo git commands under the inherited environment, so an ambient GIT_DIR or global core.hooksPath corrupts the caller's repo or breaks the gate. The harness builds ~60 'git -C $d' fixtures but omits the hermetic-env scrub its sibling check-attribution-cases.sh carries (iss-28, iss-313, resolved four days before this script was authored): an inherited absolute GIT_DIR overrides -C and redirects the fixture commits and config onto the ambient repository while every case still prints ok (green-while-corrupting, reachable on a direct make preflight under a git alias / rebase -x / bisect run); and an inherited GIT_CONFIG_GLOBAL / core.hooksPath — which this repo's .githooks/pre-push documents the maintainer uses — fires the developer's global pre-commit hook inside the scratch repos and turns make lint-issues red with no gate diagnostic. Fix: hoist the same unset GIT_DIR family + export GIT_CONFIG_GLOBAL=/dev/null scrub before the first git call.
- iss-2608261041020419CONTEXT.md's surface_coverage sharp-edge says it reads only the command and skill surfaces; its sub-verb pass also reads the CLI command tree. The bullet says the agent, hook, and CLI-verb surfaces are not covered, but subverbs.go reads the committed surface.json command-tree snapshot and files findings under surface_coverage for surface-bearing verbs, including a reverse sweep. The staleness errs in the safe direction (understating enforcement) so it is low-risk, but CLI-verb surfaces are now partly covered; genuinely uncovered are the operator-internal verbs, bare CLI-only verbs, and all of Direction A. Fix: reword to name the sub-verb pass and the truly-uncovered set.
- iss-2608261041027043The 05-internals index and the 04-surfaces row still call shipped site capabilities design targets, contradicting 10-site.md. 05-internals/README.md row 10 says the check gates, the explorer pages and the deploy workflow are design targets, and 04-surfaces/README.md row 22 says the deploy workflow remains a design target — but 10-site.md marks only the abcdev.app MkDocs root as the design target, abcd site check is registered shipped in surface.json, the explorer shipped (itd-136), and the deploy workflow ships (adr-48 accepted, release.yml calls site.yml, CHANGELOG 0.6.6). Both index cells are stale surviving copies of the earlier all-staged claim; outside the release-gate crosscheck's pinned briefDocs corpus, so no gate catches the record-vs-record contradiction. Fix: align both cells with 10-site.md.
- iss-2608261041027268commands/capture.md implies --shipped-in is validated for existence at resolve time; the code shape-checks only, and existence/ancestry run at release derivation. The flag doc says the value must name a tag that exists and that the release can reach, a shape-valid but wrong version is refused — read as a resolve-time precondition. In fact resolve only matches vMAJOR.MINOR.PATCH (workflow.go reShippedIn); a nonexistent shape-valid tag (v9.9.9) is accepted and written, and existence/ancestry are enforced only at derivation (changelog/shipped.go), which keeps a wrong-version record IN the cut with a stated reason rather than dropping it. The system never silently drops the record, but the doc gives false confidence at the point of use that resolve validated the tag. Fix: move the guarantee to derivation-time wording.
- iss-2608261041029596brief/README.md still says the internals README indexes nine chapters and its tree roster omits site, but there are ten. brief/README.md line 52 reads 'indexes the nine chapters' and the tree line 40 lists nine internals topics without site, while 05-internals/README.md indexes ten rows (10-site.md exists on disk). iss-356 designated the tree line and the internals index as the single rosters to keep in sync; a tenth chapter was added and the index updated but the count and tree line were not. No lint rule gates this roster. Fix: nine to ten, add site to the tree line.
- iss-2608261041210476The principle spec-moves-with-the-surface names a promotion trigger that is already armed, yet sits unpromoted in the not-yet-enforced layer. The principle says a record-lint cross-check (every entry under commands/ and skills/ resolves to a brief surface row) WOULD promote it to a discipline; that is exactly surface_coverage Direction B, armed at blocker (lint.go, record-lint.json), documented live. principles/README.md makes promotion unconditional once any mechanical gate lands (the file is then retired to a discipline-kind intent, as itd-79 personas did), but no discipline intent exists for this principle and the file was never retired. The conditional 'would promote' predates the gate and was left stale — the inverse of the phantom-gate family (it understates its own enforcement). RECORDED NOT FIXED this round: the correct remedy is a governance act (mint a discipline-kind intent and retire the principle file, per the itd-79 precedent), which needs the maintainer rather than an autonomous edit. Proposed: promote per principles/README, or if promotion is deliberately deferred, record why on the principle.
- iss-2608261041218890release.yml feeds an unvalidated tag name into 'make build VERSION=$TAG', splicing it textually into the -ldflags shell recipe. TAG is inputs.tag or github.ref_name with no shape-check; the release job runs 'make build VERSION="${TAG}"' and Make substitutes $(VERSION) textually into 'go build -ldflags "...Version=$(VERSION)"', so a tag containing a double-quote closes the quoting and splices a shell command (reproduced: VERSION='v9.9.9";echo INJECTED;x="'). Such a tag is a legal git ref matching the v* trigger. site.yml shape-checks the identical value with a [[ =~ ]] vX.Y.Z pattern and documents why; release.yml shape-checks the sha but not the tag. Bounded: the injectable step is behind the release environment's required reviewer and github.ref_name is settable only by an insider who can already run code in CI, so it grants no new capability — defense-in-depth, but a real shape-check-parity gap that also propagates to the scaffold template (release.yml.tmpl). RECORDED NOT FIXED this round: release.yml cannot be exercised by CI, so an autonomous edit to the release pipeline is deferred to the maintainer. Proposed fix: add a tag=$TAG shape-check step (the site.yml regex, [[ =~ ]] not grep) before make build, in both release.yml and the scaffold template.