fix(gate-46): resolve a spec wherever archiving left it, and accept #T<n> (#228) - #246
Merged
Merged
Conversation
…T<n> (#228) Gate-46 was a treadmill: the repo's own BLOCKING phpcs SpecTagSniff instructs `@spec openspec/changes/{name}/tasks.md#task-N`, and a change directory is by definition temporary. 260 of portaliq's 385 tags did as told and dangled the moment their change was archived. The sniff is being repointed separately; this is the gate half — and without it, following the CORRECTED advice dangles too. WHY POINTING AT openspec/specs/ WAS NOT ALREADY SAFE A capability's spec.md occupies three homes over its life: 1. in flight openspec/changes/<change>/specs/<cap>/spec.md 2. archived openspec/changes/archive/<date>-<change>/specs/<cap>/spec.md 3. canonical openspec/specs/<cap>/spec.md build_archive_index already carried a tag from (1) to (2), keyed on the CHANGE name. That is not enough, twice over: * archiving does NOT reliably promote the delta spec into openspec/specs/. procest carries 18 tags at openspec/specs/process-mining-bottlenecks/spec.md — written exactly as the corrected sniff instructs — whose spec only ever existed at (2). Correct advice, dangling reference. * the change name need not equal the capability name. procest's realtime-updates-ui capability lives under the change adopt-live-updates-ui, so no CHANGE-keyed index can reach it. 10 more findings. Keying on the CAPABILITY spans all three homes and is stable across both the archive move and the promotion that may follow, so a tag written once keeps resolving whatever stage the change has reached. This widens WHERE the gate looks, never WHAT counts as resolved. The index is consulted only after every literal spelling has failed, so a tag whose own path exists is still judged against THAT file — a stale archived copy cannot vouch for a requirement the canonical spec has dropped. The fragment must still name a heading that exists. #T<n>, THE SHORTHAND NOBODY ACCEPTED portaliq writes #T3 against `### Task 3: Fail-closed trust re-checks…`, procest writes #T02 against `## 2. Controller + routes`. The task is right there; only the reference spelling is unusual. 117 findings, latent from the day they were written. Deliberately NOT wired into the positional-checkbox rule: #task-N also means "the Nth checkbox", and giving T<n> that reading would let #T99 resolve against any file with 99 checkboxes. MEASURED, full-tree, repos at origin/development portaliq 100 -> 24 doriath 79 -> 79 procest 82 -> 15 larpingapp 1 -> 1 openconnector 0 -> 0 total 262 -> 119 doriath is the control and it did not move. Its 77 findings are tags naming requirements — "Listing and download", "Masked presentation", "Lease management API" — that appear NOWHERE in the repo, in any of the three homes. Those are true positives and this change rescues none of them. Every survivor was checked by hand: procest's #T05 against a file whose sections stop at 4, its #task-2-5 against a list that runs 2.1-2.4b, portaliq's 24 deleted change dirs, larpingapp's manifest-v2-vue-scaffold which exists in no home at all. A FINDING COUNT IS NOT A DEFECT COUNT portaliq's 100 findings were 29 distinct targets — one dangling target annotated on 15 methods emits 15 lines. The summary now prints both, so the job reads as "repoint 29 targets" rather than "grind 100 tags". 13 new tests (37 -> 50), each mutation-checked: disabling the shorthand fails 8, never consulting the capability index fails 4, making the index apply always instead of last-resort fails the redirect control, and a has_anchor that returns True unconditionally fails every true-positive control in the file.
rubenvdlinde
pushed a commit
that referenced
this pull request
Aug 8, 2026
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the gate half of #228. The sniff half is being fixed in the app repos separately.
Cause
Gate-46 was a treadmill. The repo's own blocking phpcs
SpecTagSniffinstructs@spec openspec/changes/{name}/tasks.md#task-N, and a change directory is by definition temporary. 260 of portaliq's 385 tags did as told and dangled the moment their change was archived.But repointing tags at
openspec/specs/— the project rule — was not safe either, and that is the part that made this a treadmill rather than a one-off cleanup. A capability'sspec.mdoccupies three homes over its life:openspec/changes/<change>/specs/<cap>/spec.mdopenspec/changes/archive/<date>-<change>/specs/<cap>/spec.mdopenspec/specs/<cap>/spec.mdbuild_archive_indexalready carried a tag from (1) to (2), keyed on the change name. Not enough, twice over:openspec/specs/. procest carries 18 tags atopenspec/specs/process-mining-bottlenecks/spec.md, written exactly as the corrected sniff instructs, whose spec only ever existed at (2). Correct advice, dangling reference.realtime-updates-uicapability lives under the changeadopt-live-updates-ui, so no change-keyed index can reach it. 10 more findings.Keying on the capability spans all three homes and is stable across both the archive move and the promotion that may follow. That is what "a rule that survives archiving" has to mean.
Separately,
#T3/#T02— the shorthand spelling of "Task N" — had never resolved. portaliq writes#T3against### Task 3: Fail-closed trust re-checks…; procest writes#T02against## 2. Controller + routes. 117 findings, latent from the day they were written.This widens WHERE the gate looks, never WHAT counts as resolved
The capability index is consulted only after every literal spelling has failed, so a tag whose own path exists is still judged against that file — a stale archived copy cannot vouch for a requirement the canonical spec has dropped. The fragment must still name a heading that exists.
T<n>is deliberately not wired into the positional-checkbox rule:#task-Nalso means "the Nth checkbox", and givingT<n>that second reading would let#T99resolve against any file with 99 checkboxes.Measured — full-tree, repos at
origin/developmentdoriath is the control and it did not move. Its 77 findings are tags naming requirements —
Listing and download,Masked presentation,Lease management API— that appear nowhere in the repo, in any of the three homes. Those are true positives and this change rescues none of them. doriath does not even ship the sniff; its cause is different and its findings are real.Every survivor was checked by hand:
#T05— against a file whose sections stop at 4. A natural control for the new shorthand rule:#T02now resolves,#T05still fails.#task-2-5×10 — against a list that runs 2.1, 2.1b, 2.2, 2.3, 2.3b, 2.4, 2.4b. No 2.5.example-change,scaffold-v2,template-manifest-v1,portal-file-upload,portal-schema-endpoint,retrofit-2026-05-26-preferences-api: deleted, not archived, present in no home.manifest-v2-vue-scaffold, in no home at all.A finding count is not a defect count
portaliq's 100 findings were 29 distinct targets — one dangling target annotated on 15 methods emits 15 lines. That arithmetic is what drove people to grind tags one file at a time when the actual work is one repoint per target. The summary line now prints both numbers.
Tests — 37 → 50, all mutation-checked
TaskShorthandRelaxed(5) andCapabilityResolutionSurvivesArchiving(8). The latter includes the acceptance proof for #228: the same tag resolves at all three lifecycle stages, including after archiving.Mutation results:
has_anchorreturns True unconditionallybash hydra-gates/tests/run-helper-suites.sh→ 27 passed, 0 failed, 2 pre-existing quarantines.