Skip to content

fix(gate-46): resolve a spec wherever archiving left it, and accept #T<n> (#228) - #246

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-46-capability-resolution
Aug 8, 2026
Merged

fix(gate-46): resolve a spec wherever archiving left it, and accept #T<n> (#228)#246
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-46-capability-resolution

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

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

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'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. 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. 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 #T3 against ### Task 3: Fail-closed trust re-checks…; procest writes #T02 against ## 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-N also means "the Nth checkbox", and giving T<n> that second reading would let #T99 resolve against any file with 99 checkboxes.

Measured — full-tree, repos at origin/development

repo before after
portaliq 100 24
procest 82 15
doriath 79 79
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. doriath does not even ship the sniff; its cause is different and its findings are real.

Every survivor was checked by hand:

  • procest #T05 — against a file whose sections stop at 4. A natural control for the new shorthand rule: #T02 now resolves, #T05 still fails.
  • procest #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.
  • portaliq ×24 — 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.
  • larpingapp ×1 — 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) and CapabilityResolutionSurvivesArchiving (8). The latter includes the acceptance proof for #228: the same tag resolves at all three lifecycle stages, including after archiving.

Mutation results:

mutation new tests that fail
shorthand disabled 8
capability index never consulted 4
index applied always instead of last-resort 1 (the redirect control)
has_anchor returns True unconditionally every true-positive control in the file

bash hydra-gates/tests/run-helper-suites.sh → 27 passed, 0 failed, 2 pre-existing quarantines.

…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
rubenvdlinde merged commit d53047b into main Aug 8, 2026
30 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/gate-46-capability-resolution branch August 8, 2026 13:35
rubenvdlinde pushed a commit that referenced this pull request Aug 8, 2026
main advanced by four hydra-gates commits (#217, #246, #249, #248) while this
branch was open. No file overlap: this branch touches quality.yml,
quality-resolve-probe.yml, a fixture workflow and two scripts/.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant