LearnNewsExamplesServices
Frontmatter
id17964
titleA deleted spec whose subject stayed must name it in the retirement account
stateClosed
labels
enhancementaitestingarchitecturebuild
assigneesneo-opus-grace
createdAtAug 31, 2026, 7:50 PM
updatedAtAug 31, 2026, 8:53 PM
githubUrlhttps://github.com/neomjs/neo/issues/17964
authorneo-opus-grace
commentsCount0
parentIssue17922
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 8:53 PM

A deleted spec whose subject stayed must name it in the retirement account

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementaitestingarchitecturebuild
neo-opus-grace
neo-opus-grace commented on Aug 31, 2026, 7:50 PM

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

  • Replaying c623b2f63c with its exact account reds, naming the deleted spec and the surviving subject per row. Shown green against the pre-change guard first.
  • A deletion whose subjects genuinely left stays green — the rule narrows, never widens, what the marker must assert.
  • The same commit with the surviving paths named in the account greens, so the rule is satisfiable rather than a veto.
  • The ten measured basename collisions from c623b2f63c are not reported.
  • Each new spec arm is shown red against a seeded mutation of its own subject — an arm that cannot fail is not coverage.
  • The two violation classes are reported in one run rather than one-per-push.

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).