LearnNewsExamplesServices
Frontmatter
id18209
titleThe release path's orphan and broad-stage guards have no spec
stateClosed
labels
bugaitestingregressionbuild
assigneesneo-opus-grace
createdAtSep 3, 2026, 4:00 PM
updatedAtSep 3, 2026, 4:53 PM
githubUrlhttps://github.com/neomjs/neo/issues/18209
authorneo-opus-grace
commentsCount0
parentIssue17922
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 3, 2026, 4:53 PM

The release path's orphan and broad-stage guards have no spec

Closed Backlog/active-chunk-20 bugaitestingregressionbuild
neo-opus-grace
neo-opus-grace commented on Sep 3, 2026, 4:00 PM

Sub of #17922 · sibling of #17924, #17964, #18052 · the third of #17922's owed residual rows

Live latest-open sweep: checked the latest 20 open issues at 2026-09-03T13:59:34Z; no equivalent found. A2A claim sweep over the last 30 messages (all read-states) at 13:45Z, plus my own [lane-claim] on this exact row at 13:48Z: no competing claim. Memory Core rationale sweep on the problem's nouns (deleted release-note orphan guard, broad-staging witness) returned no prior decision on this row beyond #17922's own ruling. Own-assignment sweep: 13 open tickets assigned to me, none covering the release path's coverage.

Context

c623b2f63c (#17791 via #17806) deleted 804 unit spec files on the test/playwright/unit/ai/** path prefix. #17922 censused the 30 under ai/buildScripts/ and found 27 whose subject never left this repository. 21 recovered verbatim in #17924 / PR #17925; DataSyncPipeline came back via #17920 / #17927; WorkflowScopeClassifier via #18052 / #18053; check-theme-surfaces via #18162 / #18163.

Re-measured at dev@f6c7a0c360, #17922's owed set is 3, not the 4 its body still states — the check-theme-surfaces row reads as "in flight — PR #18163, awaiting review" and that PR merged at 09:29Z today. This ticket takes one of the three: release/PublishReleaseNoteOrphan.spec.mjs, 171 lines, 6 arms.

The Problem

The subject is buildScripts/release/publish.mjs — 281 lines at HEAD, the operator-invoked release entrypoint, protected by §critical_gates 8. It never left. Four of its six deleted arms guard properties that are live today with no witness at all:

unguarded property consequence if it silently breaks
the staging note is removed only AFTER gh release create reads it the release is cut from a file the script already deleted, or the staging copy lingers
no top-level release-notes/v*.md survives beside its chunk-N mirror the version double-lists in the release index and the sitemap
the orphan predicate distinguishes a pre-publish staging note from a post-publish duplicate either the designed staging state reads as a defect, or the defect reads as staging
each release leaf appears once in apps/portal/resources/data/releases.json the reader sees the duplicate the orphan class produces

Two more arms asserted through ai/scripts/lifecycle/postReleaseSync.mjs, which correctly departed. #17922's disposition ruling (2026-09-02) decided both per arm rather than per file: the handoff arm re-aims onto the ordering invariant that survives, and the broad-stage arm keeps its Engine half, with its Brain half named as a custody transfer owed to neomjs/neo-agent-brain rather than dropped.

The broad-stage half is the sharpest of the four live properties. publish.mjs:155 stages with git add . and :154 is assertNoArchiveLogicalIdentityCollisions(...). Every commit on the release path runs --no-verify by design, so husky and the lint-staged copy of that guard are structurally blind to it, and proving the ordering behaviourally would require cutting a release. A source-ordering assertion is the only available instrument, and there is none.

The Architectural Reality

  • Placement is the surviving sibling tree test/playwright/unit/buildScripts/** (17 specs at HEAD), not the deleted unit/ai/buildScripts/** path. The ai/ prefix is what classified the file as extracted substrate and would re-arm the same deletion at the next boundary change. This contradicts .agents/skills/unit-test/references/unit-test.md §7, which still routes buildScripts specs to unit/ai/buildScripts/; that is stale substrate and is out of scope here.
  • The arms read sources rather than importing them: publish.mjs is a top-level release script whose module scope runs main(), so a source-text assertion is the honest instrument, matching the restored WorkflowScopeClassifier.spec.mjs precedent.
  • The release-index arm's subject is generated: buildScripts/docs/index/release.mjs writes releases.json, and its line 138 deliberately omits isLeaf on release nodes.

The Fix

  1. Restore the spec to test/playwright/unit/buildScripts/release/.
  2. Re-aim the handoff arm: drop the ai:post-release-sync script resolution (that script is absent from package.json), keep the ordering half, and assert the print names the Brain-side checkout — a beat that does not say where the next step runs is not a handoff.
  3. Reduce the broad-stage arm to its Engine half, keeping the per-file pinning (never a total) so a second unguarded broad stage cannot arrive and still pass, and name the Brain-side custody transfer in the file.
  4. Repair the release-index arm, which cannot fail as written. It filters node.isLeaf; the generator omits that field on release leaves, so the filter matches 0 of 174 nodes — and 0 at c623b2f63c~1 as well, so this is not split drift: the arm was already vacuous when it was deleted. Select a leaf by the property that distinguishes one, assert the population is non-empty so it cannot silently re-vacuate, and check path alongside id.

Contract Ledger Matrix

Target Surface Source of Authority Behavior Fallback Docs Evidence
test/playwright/unit/buildScripts/release/PublishReleaseNoteOrphan.spec.mjs #17922 AC-1; sibling precedent test/playwright/unit/buildScripts/util/WorkflowScopeClassifier.spec.mjs 6 arms over publish.mjs, the release-notes corpus and releases.json none — a release path with no witness is the defect spec header each arm red against a seeded mutation of its own subject
the release-index leaf predicate buildScripts/docs/index/release.mjs:131,138 a leaf is a node pointing at a release-note file; the population is asserted non-empty n/a spec JSDoc red when the index is stripped of leaves, which the pre-repair arm could not see

Decision Record impact

none — restoring Engine-owned coverage to the Engine is what ADR 0040 §2.3 already prescribes (the ADR lives in neo-agent-brain, not here; its absence from learn/agentos/decisions/ is not evidence).

Acceptance Criteria

  • PublishReleaseNoteOrphan.spec.mjs exists under test/playwright/unit/buildScripts/release/ and passes against its HEAD subjects.
  • Every arm is shown red against a seeded mutation of its own subject, and each mutation reddens only its own arm — a restored spec that cannot fail is not restored coverage. Mutations must cover, at minimum: the staging-note removal moved before the release, a lingering post-publish duplicate, a neutered orphan predicate, a twice-listed release leaf, an index with no leaves at all, a deleted handoff print, a handoff that names no destination, and a broad stage moved before its guard.
  • The file names both re-aimed arms and the reason, so the difference from the deleted original is auditable rather than invisible.
  • The release-index arm's vacuity is stated with the measurement from both trees, so it cannot read as split drift.
  • The Brain-side broad-stage half is named in the spec as a custody transfer, not silently absent.
  • The unit project's collected spec-file count rises by exactly 1, measured by count and not by a green run.
  • #17922's body is corrected so its check-theme-surfaces row no longer reads as in flight.

Out of Scope

  • The other two owed rows — RebuildContentIndexesAndSeo and WorkflowConcurrency stay with #17922, unclaimed. WorkflowConcurrency is the sharpest of them: its subject runs on every PR.
  • Filing the Brain-side witness. The ordering invariant in ai/scripts/lifecycle/postReleaseSync.mjs has no spec in the repository that owns it. That is a neomjs/neo-agent-brain filing with its own sweep, and #17922's ruling deliberately declined to file it from inside this arc.
  • Changing what publish.mjs does. Restoration is not the moment to fix the subject; both call sites are correctly ordered at HEAD and this ticket reports no live defect in them.
  • unit-test.md §7's stale placement rule. Real friction, wrong repository — the skill corpus is neomjs/neo-agent-skills substrate.

Related

  • #17922 — the parent census; this is one of its owed residual rows
  • #17924 / PR #17925 — the 21-spec verbatim restoration
  • #18052 / PR #18053 — the sibling residual, same shape (restore + re-aim + name the drops)
  • #17920 / PR #17927 — the DataSyncPipeline residual
  • #17151 / efba884dcb — the spec-retirement account guard whose contract #17922 narrowed

Handoff Retrieval Hints

  • Retrieval Hint: "the release path's broad git add . stage has no ordering witness since c623b2f63c"
  • Retrieval Hint: releases.json carries no isLeaf:true node, so a node.isLeaf filter is vacuous in both trees