Third sibling of #726 and #727, same helper. The three are one question, asked at three points: #726 is what gate-46 does not read, #727 is where it does not look, and this is when it stops resolving.
The gap
resolve() in scripts/lib/check_spec_anchors.py consults the capability index only after every literal spelling has already failed:
# LAST RESORT ONLY — see build_capability_index.
if not cands and capability_index:
That is what makes the "an anchor may name a canonical path whose spec is still in an open change" convention work at all: resolution falls through to the index and finds the requirement in the change's delta.
It stops working the moment the canonical file exists. A capability file that is present for other reasons makes cands non-empty, the index is never reached, and the delta sitting beside it in an open change is never opened. The anchor then reports anchor not found, and the requirement it names is written, reviewed and merged.
So a requirement that is early inside a capability that is not cannot be cited at all. The only spellings that resolve are the change path, which will move on archive, or an exclusion, which claims no coverage.
Reproduced
Two spec files, one anchor, nothing else:
openspec/specs/case-management/spec.md (exists, older scenarios)
openspec/changes/case-delete-guard/specs/case-management/spec.md (the new scenario)
lib/Thing.php: @spec openspec/specs/case-management/spec.md#a-case-delete-is-refused-when-the-schema-is-archival
Run through the real helper:
lib/Thing.php: @spec anchor not found in openspec/specs/case-management/spec.md
→ #a-case-delete-is-refused-when-the-schema-is-archival
Repoint the same tag at the change path and the finding disappears. The requirement never moved; only the spelling did.
Measured in the field: eight anchors in dossiq, in an open change with 11 of 13 tasks done, all naming scenarios that are written. They now carry explicit exclusions naming the governing scenario and its change, which claims no coverage and makes the post-archive sweep mechanical. That is a workaround for a resolver gap, not an end state.
What a fix has to weigh, and why it is not in either open PR
The obvious repair is to consult the capability index alongside the literal candidates rather than after them. That only ever adds candidates, so it can only ever remove findings, and a change that can only remove findings from a gate is exactly the kind that needs its own review rather than a ride inside a widening. Both PRs against #726 and #727 deliberately leave it alone.
The reason to be careful is on the same page as the reason to fix it.
A citation can resolve to the wrong thing, and no count catches that
From the same measurement: one anchor resolves cleanly to a scenario that says the opposite of what its test asserts. The scenario says deleting a case succeeds. The test asserts it is refused with a 403, because the schema is archival. The anchor resolves, so it passes gate-46 and reads as verified coverage.
Every fix in this family counts resolution, and resolution is not agreement. Widening the index makes more anchors resolve, which is the point, and it also makes this failure mode more available. Worth stating plainly here so that whoever loosens the resolver decides it on purpose:
an anchor that resolves to the wrong requirement is worse than one that dangles, because the dangling one gets fixed on the day it is typed.
Third sibling of #726 and #727, same helper. The three are one question, asked at three points: #726 is what gate-46 does not read, #727 is where it does not look, and this is when it stops resolving.
The gap
resolve()inscripts/lib/check_spec_anchors.pyconsults the capability index only after every literal spelling has already failed:That is what makes the "an anchor may name a canonical path whose spec is still in an open change" convention work at all: resolution falls through to the index and finds the requirement in the change's delta.
It stops working the moment the canonical file exists. A capability file that is present for other reasons makes
candsnon-empty, the index is never reached, and the delta sitting beside it in an open change is never opened. The anchor then reportsanchor not found, and the requirement it names is written, reviewed and merged.So a requirement that is early inside a capability that is not cannot be cited at all. The only spellings that resolve are the change path, which will move on archive, or an exclusion, which claims no coverage.
Reproduced
Two spec files, one anchor, nothing else:
Run through the real helper:
Repoint the same tag at the change path and the finding disappears. The requirement never moved; only the spelling did.
Measured in the field: eight anchors in dossiq, in an open change with 11 of 13 tasks done, all naming scenarios that are written. They now carry explicit exclusions naming the governing scenario and its change, which claims no coverage and makes the post-archive sweep mechanical. That is a workaround for a resolver gap, not an end state.
What a fix has to weigh, and why it is not in either open PR
The obvious repair is to consult the capability index alongside the literal candidates rather than after them. That only ever adds candidates, so it can only ever remove findings, and a change that can only remove findings from a gate is exactly the kind that needs its own review rather than a ride inside a widening. Both PRs against #726 and #727 deliberately leave it alone.
The reason to be careful is on the same page as the reason to fix it.
A citation can resolve to the wrong thing, and no count catches that
From the same measurement: one anchor resolves cleanly to a scenario that says the opposite of what its test asserts. The scenario says deleting a case succeeds. The test asserts it is refused with a 403, because the schema is archival. The anchor resolves, so it passes gate-46 and reads as verified coverage.
Every fix in this family counts resolution, and resolution is not agreement. Widening the index makes more anchors resolve, which is the point, and it also makes this failure mode more available. Worth stating plainly here so that whoever loosens the resolver decides it on purpose:
an anchor that resolves to the wrong requirement is worse than one that dangles, because the dangling one gets fixed on the day it is typed.