Refs #17922 · sibling of #17924
Context
#17922 AC-4 asks the guard to "close the class, not just the instance". This is that leaf: #17924 restored the 21 specs that came back verbatim, and this makes the deletion that lost them mechanically observable next time.
buildScripts/util/check-spec-retirement.mjs already existed when c623b2f63c deleted 796 unit specs, and it passed correctly. Its documented contract — settled deliberately when the guard was authored — is that an account must EXIST, never that it is TRUE:
The guard checks that an account exists, never that it is true — a guard that graded the prose would be a rule nobody could satisfy mechanically.
That reasoning is sound and this ticket does not challenge it. But it means the guard's protection is exactly as strong as the account's honesty, and one sentence was allowed to account for 796 deletions.
The Problem
One half of "true" is not prose at all.
Nothing can grade "folded into X". But "the subject of this deleted spec is still in this repository" is a fact about the tree, not a claim about intent — and it is precisely the half c623b2f63c got wrong.
Measured against the tree that commit itself produced, not against dev today:
| population |
count |
unit specs deleted by c623b2f63c |
796 |
| ...whose subject still resolved in that same tree |
34 |
| ...of those 34, named anywhere in the account |
0 |
The account described all 796 as Brain-owned coverage leaving with Agent OS. For 34 of them the implementation never left.
Two of those 34 rows are new information, not a restatement of #17922. That ticket censused test/playwright/unit/ai/buildScripts/** by hand and found 27 surviving subjects. This instrument sees 22 of those 27 (bound stated below) — and 12 more that nothing had censused, all under src/ai/**, all with implementations still present at HEAD today and no unit coverage:
src/ai/LockRegistry.mjs src/ai/client/ComponentService.mjs
src/ai/TransactionService.mjs src/ai/client/DockService.mjs
src/ai/WriteGuard.mjs src/ai/client/InstanceService.mjs
src/ai/admitWrite.mjs src/ai/client/RuntimeService.mjs
src/ai/deriveSubtreePath.mjs src/ai/client/TourRunner.mjs
src/ai/parseAgentEnvelope.mjs src/ai/resolveWriteLock.mjs
#17922 explicitly scoped those out ("the other 774 deletions... this ticket makes no claim about them"). They are recorded here as a finding, not as scope: restoring them is neither this ticket nor #17922, and wants its own leaf.
The Architectural Reality
buildScripts/util/check-spec-retirement.mjs — the guard. Pure exported functions plus a git-reading collectViolations; the spec imports the pure half and needs no repository.
test/playwright/unit/buildScripts/checkSpecRetirement.spec.mjs — its spec, one of the survivors of the split (it sat outside the ai/ subtree that was swept).
.husky/pre-push runs the guard; .github/workflows/spec-retirement-lint.yml runs it on dev PRs. Neither changes here.
- The guard reads
git ls-tree -r --name-only <sha> — the tree the deleting commit produced. Not HEAD: the question is whether the subject survived that deletion, which keeps the rule deterministic and replayable over history.
The Fix
A second, narrower violation class beside the existing one. A commit that (a) has an account, (b) deletes a unit spec, and (c) does not mention the implementation that stayed, is reported — naming any one candidate path discharges the row.
deriveSubjectSuffix(specPath) → <parentDir>/<name>.mjs
findSurvivingSubjects(specPath, treePaths) → surviving subject paths, test/ excluded
unaccountedSurvivors(specs, treePaths, message) → the unnamed rows
formatSurvivingSubjectFailure(violations) → the report
The parent-directory affinity is measured, not stylistic, and it is the load-bearing design choice. A bare basename rule was the first draft; replaying it over c623b2f63c produced ten false positives — four per-server logger.spec.mjs files resolving to src/util/Logger.mjs, two Server.spec.mjs files to examples/form/field/fileupload/server.mjs, scheduling/picker.spec.mjs to src/form/field/Picker.mjs. None is the deleted spec's subject. Demanding an account for a file the author never touched is what the guard's own header warns against: it trains people to route around the check.
So the rule under-reports rather than over-reports, deliberately. For an account-demanding guard a false positive is the expensive error and a missed row is merely the status quo.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Behavior |
Fallback |
Docs |
Evidence |
unaccountedSurvivors violation class |
this ticket |
a deleting commit that HAS an account must name each deleted spec's surviving subject; any one candidate path discharges the row |
the existing no-account class is untouched and reported independently |
function JSDoc |
replay of c623b2f63c reds with 34 rows; the same commit with paths appended greens |
deriveSubjectSuffix affinity rule |
this ticket |
subject resolves as <parentDir>/<name>.mjs, case-insensitive, test/ excluded |
under-reports where a subject's filename differs from its spec's; never over-reports |
function JSDoc naming the ten measured collisions |
all ten basename collisions dropped; 22 in-scope rows retained |
| account contract for genuinely departed subjects |
the guard's existing contract |
unchanged — free prose, no path required |
n/a |
function JSDoc |
the three departed subjects stay green |
Decision Record impact
none — this narrows a guard's own contract; no ADR governs the spec-retirement account shape.
Acceptance Criteria
Out of Scope
- Restoring the 12
src/ai/** specs this surfaced. A finding here, not scope. It now has its own leaf and its own census: #17968, filed 2026-08-31 per RA-4 of @neo-gpt's review on PR #17965, so the finding stays in the live queue rather than living in this body.
- The six residual specs of #17922. Still owed on the parent, each with a named cause.
- Grading the account's prose. Rejected deliberately when the guard was authored, and still right — the mechanical half is subject resolvability, which is what this tests.
- Making
spec-retirement-lint.yml a required context. #17171 owns required-context binding for all 19 *-lint.yml workflows.
- Guards with no caller. #17783 owns the enforcement axis.
Avoided Traps
- A repo-wide basename match. Measured: ten false positives on the one commit this exists to catch. A guard that fires on files the author never touched gets routed around, and then protects nothing.
- Reading
HEAD instead of the deleting commit's own tree. It would make the verdict drift with unrelated later commits and make history un-replayable.
- Claiming this reproduces #17922's 27. It sees 22. The five it cannot see have subjects whose filename differs from the spec's (
TicketIndex.spec.mjs → tickets.mjs, WorkflowConcurrency.spec.mjs → a .yml file), which no mechanical name rule recovers. Stating the bound is the point; a guard that claimed the full 27 would be the same over-claim the ticket is about.
- Requiring every candidate path. The author is being asked to acknowledge the implementation stayed, not to enumerate every file sharing a name.
Related
- #17922 — parent; this is its AC-4
- #17924 / PR #17925 — sibling leaf: the 21 verbatim restorations
- #17151 — the guard's origin, whose account-exists contract this narrows
- #17171 — required-context binding for the
*-lint.yml family
- #17783 — guards with no caller (enforcement axis)
- #17791 /
c623b2f63c — the split commit this is measured against
Handoff Retrieval Hints
Retrieval Hint: "spec-retirement surviving subject account names no path"
Retrieval Hint: Commit c623b2f63c — 796 unit spec deletions, 34 with a surviving subject, 0 named
- Reproduce: for each
D row under test/playwright/unit/** in git show c623b2f63c --name-status -M, derive <parentDir>/<name>.mjs and match case-insensitively against git ls-tree -r --name-only c623b2f63c excluding test/.
Live latest-open sweep: checked the latest 20 open neomjs/neo issues at 2026-08-31T17:49:36Z plus a spec-retirement/spec-retired subject search across all states (#17151 and #17924 are the closed guard-origin and verbatim-restoration tickets; #17783 and #17171 own the enforcement axis) and the A2A claim window (last 30 messages, all read-states) — no equivalent ticket and no competing claim. Structure-map gate: N/A — no new or relocated .mjs.
Note on ordering: the implementation for this leaf was authored under #17922's lane authority (self-assigned, [lane-claim] broadcast naming AC-4 at 2026-08-31T17:36:28Z) before this leaf existed. This ticket is filed to give that work an honest delivered close target rather than a bare Refs edge on the parent, which still owes six residual specs and cannot close.
Origin Session ID: cb4705c8-385e-4b5e-9d41-5d6a9d45577c
Authored by Grace (Anthropic Claude Opus 5, Claude Code).
Refs #17922 · sibling of #17924
Context
#17922 AC-4 asks the guard to "close the class, not just the instance". This is that leaf: #17924 restored the 21 specs that came back verbatim, and this makes the deletion that lost them mechanically observable next time.
buildScripts/util/check-spec-retirement.mjsalready existed whenc623b2f63cdeleted 796 unit specs, and it passed correctly. Its documented contract — settled deliberately when the guard was authored — is that an account must EXIST, never that it is TRUE:That reasoning is sound and this ticket does not challenge it. But it means the guard's protection is exactly as strong as the account's honesty, and one sentence was allowed to account for 796 deletions.
The Problem
One half of "true" is not prose at all.
Nothing can grade "folded into
X". But "the subject of this deleted spec is still in this repository" is a fact about the tree, not a claim about intent — and it is precisely the halfc623b2f63cgot wrong.Measured against the tree that commit itself produced, not against
devtoday:c623b2f63cThe account described all 796 as Brain-owned coverage leaving with Agent OS. For 34 of them the implementation never left.
Two of those 34 rows are new information, not a restatement of #17922. That ticket censused
test/playwright/unit/ai/buildScripts/**by hand and found 27 surviving subjects. This instrument sees 22 of those 27 (bound stated below) — and 12 more that nothing had censused, all undersrc/ai/**, all with implementations still present atHEADtoday and no unit coverage:#17922 explicitly scoped those out ("the other 774 deletions... this ticket makes no claim about them"). They are recorded here as a finding, not as scope: restoring them is neither this ticket nor #17922, and wants its own leaf.
The Architectural Reality
buildScripts/util/check-spec-retirement.mjs— the guard. Pure exported functions plus a git-readingcollectViolations; the spec imports the pure half and needs no repository.test/playwright/unit/buildScripts/checkSpecRetirement.spec.mjs— its spec, one of the survivors of the split (it sat outside theai/subtree that was swept)..husky/pre-pushruns the guard;.github/workflows/spec-retirement-lint.ymlruns it ondevPRs. Neither changes here.git ls-tree -r --name-only <sha>— the tree the deleting commit produced. NotHEAD: the question is whether the subject survived that deletion, which keeps the rule deterministic and replayable over history.The Fix
A second, narrower violation class beside the existing one. A commit that (a) has an account, (b) deletes a unit spec, and (c) does not mention the implementation that stayed, is reported — naming any one candidate path discharges the row.
deriveSubjectSuffix(specPath)→<parentDir>/<name>.mjsfindSurvivingSubjects(specPath, treePaths)→ surviving subject paths,test/excludedunaccountedSurvivors(specs, treePaths, message)→ the unnamed rowsformatSurvivingSubjectFailure(violations)→ the reportThe parent-directory affinity is measured, not stylistic, and it is the load-bearing design choice. A bare basename rule was the first draft; replaying it over
c623b2f63cproduced ten false positives — four per-serverlogger.spec.mjsfiles resolving tosrc/util/Logger.mjs, twoServer.spec.mjsfiles toexamples/form/field/fileupload/server.mjs,scheduling/picker.spec.mjstosrc/form/field/Picker.mjs. None is the deleted spec's subject. Demanding an account for a file the author never touched is what the guard's own header warns against: it trains people to route around the check.So the rule under-reports rather than over-reports, deliberately. For an account-demanding guard a false positive is the expensive error and a missed row is merely the status quo.
Contract Ledger Matrix
unaccountedSurvivorsviolation classc623b2f63creds with 34 rows; the same commit with paths appended greensderiveSubjectSuffixaffinity rule<parentDir>/<name>.mjs, case-insensitive,test/excludedDecision Record impact
none— this narrows a guard's own contract; no ADR governs the spec-retirement account shape.Acceptance Criteria
c623b2f63cwith its exact account reds, naming the deleted spec and the surviving subject per row. Shown green against the pre-change guard first.c623b2f63care not reported.Out of Scope
src/ai/**specs this surfaced. A finding here, not scope. It now has its own leaf and its own census: #17968, filed 2026-08-31 per RA-4 of @neo-gpt's review on PR #17965, so the finding stays in the live queue rather than living in this body.spec-retirement-lint.ymla required context. #17171 owns required-context binding for all 19*-lint.ymlworkflows.Avoided Traps
HEADinstead of the deleting commit's own tree. It would make the verdict drift with unrelated later commits and make history un-replayable.TicketIndex.spec.mjs→tickets.mjs,WorkflowConcurrency.spec.mjs→ a.ymlfile), which no mechanical name rule recovers. Stating the bound is the point; a guard that claimed the full 27 would be the same over-claim the ticket is about.Related
*-lint.ymlfamilyc623b2f63c— the split commit this is measured againstHandoff Retrieval Hints
Retrieval Hint: "spec-retirement surviving subject account names no path"Retrieval Hint: Commit c623b2f63c — 796 unit spec deletions, 34 with a surviving subject, 0 namedDrow undertest/playwright/unit/**ingit show c623b2f63c --name-status -M, derive<parentDir>/<name>.mjsand match case-insensitively againstgit ls-tree -r --name-only c623b2f63cexcludingtest/.Live latest-open sweep: checked the latest 20 open
neomjs/neoissues at 2026-08-31T17:49:36Z plus aspec-retirement/spec-retired subjectsearch across all states (#17151 and #17924 are the closed guard-origin and verbatim-restoration tickets; #17783 and #17171 own the enforcement axis) and the A2A claim window (last 30 messages, all read-states) — no equivalent ticket and no competing claim. Structure-map gate: N/A — no new or relocated.mjs.Note on ordering: the implementation for this leaf was authored under #17922's lane authority (self-assigned,
[lane-claim]broadcast naming AC-4 at 2026-08-31T17:36:28Z) before this leaf existed. This ticket is filed to give that work an honest delivered close target rather than a bareRefsedge on the parent, which still owes six residual specs and cannot close.Origin Session ID: cb4705c8-385e-4b5e-9d41-5d6a9d45577c
Authored by Grace (Anthropic Claude Opus 5, Claude Code).