Skip to content

scan-annotations: fix five silent link losses, and let @tested read in both directions - #1

Merged
Freshair129 merged 3 commits into
mainfrom
fix/namespaced-requirement-ids
Sep 6, 2026
Merged

Freshair129 merged 3 commits into
mainfrom
fix/namespaced-requirement-ids

Conversation

@Freshair129

@Freshair129 Freshair129 commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

Two commits on one branch, because they are the same surface and the second depends on the first's parity test.

scan-annotations is the only thing that turns a comment into a graph edge, so everything it drops is — downstream — a link that never existed.


1. fix: five failures that reported nothing

Found while adopting rwang:doc-graph v2 in a repo that uses namespaced requirement ids (ZPP-*, RAG-*, TAX-*).

The main one: namespaced ids

The two scanners failed in opposite directions on the same input.

input .sh reported .ps1 reported correct
@req ZPP-FR-009 FR-009 (nothing) ZPP-FR-009

A project prefixes its ids precisely because its FR-009 is not the flat FR-009. So the .sh behaviour is not a lost prefix — it is a different requirement asserted as fact. Both now accept NS-KIND-nnn; AI-AGT-001 / AI-ETH-001 fall out of the same branch instead of needing their own enumeration.

Four more, found while verifying that one

  • .ps1 skip list matched substrings of the whole path — build swallowed builder.ts, dist swallowed distance.ts. Those files reported nothing and were never listed as skipped. Now compared against path segments.
  • .ps1 dropped a whole annotation when one id was unrecognised — @spec FR-093, ADR-058 lost FR-093 too, because ADR is not an enumerated kind and the pattern had to consume the line.
  • .sh could not see past a UTF-8 BOM — Windows editors write one, so every ^-anchored pattern failed on line 1, which is where a file-level annotation goes. Get-Content strips it, so .ps1 never saw the problem and neither scanner looked wrong on its own machine.
  • .sh crashed on a tree with no annotations — under set -euo pipefail, the grep that matched nothing ended the script before it printed anything. The count it would have printed was a doubled zero, since grep -c . both prints 0 and exits non-zero.

Two divergences also closed, moving .sh to .ps1's stricter reading: structured annotations require a comment prefix (it had been counting const s = "@req FR-999"), and unstructured references require the ids-only form.


2. feat: @tested reads in both directions

// src/generation.ts
// @tested __tests__/generation.test.ts::creates_generation   // already supported

// __tests__/generation.test.ts
// @tested FR-001, SDD-004                                    // new

Both assert the same verified_by relation and differ only in which end the annotated file is. A project maintains its traceability from one side or the other, and understanding only one form meant a repository that annotates its tests scanned as having no annotations at all — not a warning, a zero. On the repository this came from, tests/ read as 0 while holding 69.

Annotating from the test side is often the more durable choice: the assertion and the claim that it verifies a requirement live in one file, so deleting the test takes its claim with it.

Structured annotations now carry form — test-ref, requirement, or section. It says what the payload is rather than which keyword introduced it, which is the distinction a consumer needs: @designs already took either a section or an id.


Verification

Every new test fails against the previous scanners. The addition worth keeping is the cross-scanner parity check — two halves of one tool that disagree mean a graph built on Windows differs from the same graph built on Linux, and neither is wrong locally.

Measured on a real repository, both scanners now identical:

tree structured requirement ids before
src/ 80 76 .ps1 75/73, .sh 80/76
tests/ 69 70 0 / 0 in both

scan-annotations 5/5 · validate-graph 23/23 · validate-plan 13/13 — existing suites unchanged.

Version 1.4.0 (the marketplace update signal).

🤖 Generated with Claude Code

Freshair129 and others added 2 commits September 6, 2026 10:58
scan-annotations is the only thing that turns a comment into a graph edge.
Everything it drops is, downstream, a link that never existed — and none of
these five failures reported anything. Each one produced a smaller report,
stated with full confidence.

Namespaced ids were the worst of them, because the two scanners failed in
opposite directions. Given ZPP-FR-009, the .sh scanner matched the flat id
inside it and reported FR-009; the .ps1 scanner matched nothing and reported
none. A project prefixes its ids precisely because its FR-009 is not the flat
FR-009, so the .sh behaviour is not a lost prefix, it is a different requirement
asserted as fact. Both now accept NS-KIND-nnn, and AI-AGT-001 / AI-ETH-001 fall
out of the same branch rather than needing their own.

Four more, found while verifying that one:

- The .ps1 skip list was compared against the whole path, so "build" swallowed
  builder.ts and "dist" swallowed distance.ts. A skip list names directories, so
  it now matches path segments.
- .ps1 dropped a whole annotation when one id in the list was unrecognised:
  `@spec FR-093, ADR-058` lost FR-093 as well, silently, because the pattern had
  to consume the line. It now keeps what it recognises.
- .sh could not see past a UTF-8 BOM. Windows editors write one, so every
  ^-anchored pattern failed on line 1 — which is where a file-level annotation
  goes. Get-Content strips the BOM, so .ps1 never saw the problem, and neither
  scanner looked wrong on its own machine.
- .sh crashed on a tree with no annotations: under `set -euo pipefail` the grep
  that matched nothing ended the script before it printed anything. The count it
  would have printed was a doubled zero, because `grep -c .` both prints 0 and
  exits non-zero.

Two divergences are closed rather than fixed, moving .sh to .ps1's stricter
reading: structured annotations now require a comment prefix (it had been
counting `const s = "@Req FR-999"`), unstructured references require the ids-only
form, and the @tested payload is validated as a test-file reference.

The tests cover each failure and add a cross-scanner parity check. That check is
the one worth keeping: two halves of one tool that disagree mean a graph built on
Windows differs from the same graph built on Linux, and neither is wrong locally.
Verified equal on a real 101-file repository — 80 structured annotations, 76
requirement ids, identical sets.

Every new test fails against the previous scanners. Existing suites unchanged:
scan-annotations, validate-graph 23/23, validate-plan 13/13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`@tested` understood one form: a test reference on a source file, meaning "this
code is verified by that test". It now also accepts requirement ids on a test
file, meaning "this test verifies these requirements".

    // src/generation.ts
    // @tested __tests__/generation.test.ts::creates_generation

    // __tests__/generation.test.ts
    // @tested FR-001, SDD-004

Both assert the same verified_by relation. They differ only in which end the
annotated file is, and a project maintains its traceability from one side or the
other. Understanding only one of them meant a repository that annotates its tests
scanned as having no annotations at all — not a warning, a zero. On the
repository this came from, tests/ read as 0 annotations while holding 69.

Annotating from the test side is often the more durable of the two: the assertion
and the claim that it verifies a requirement live in one file, so deleting the
test takes its claim with it. Nothing about the old form changes.

Structured annotations now carry `form` — test-ref, requirement, or section. It
describes what the payload IS rather than which keyword introduced it, which is
the distinction a consumer actually needs: @Designs already took either a section
or an id, and @tested now takes either a path or ids, so switching on the keyword
alone meant re-parsing the value to discover what you were holding.

Both scanners accept both payloads, and a payload that is neither a path nor an
id is still not an annotation. Parity on a real repository: src/ 80 annotations
and 76 ids, tests/ 69 and 70, identical from either scanner.

Docs updated where the grammar is stated: the annotation table in skills/doc-graph,
the README examples, the @tested check in skills/doc-preflight, and the @tested
description in references/doc-graph-schema.json.

scan-annotations 5/5, validate-graph 23/23, validate-plan 13/13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Freshair129 Freshair129 changed the title fix(scan-annotations): stop losing links the scanner never said it lost scan-annotations: fix five silent link losses, and let @tested read in both directions Sep 6, 2026
CI checks the two harness manifests agree, and the previous commit moved only
the Claude one. scripts/bump-version.ps1 exists for exactly this reason; I
edited the manifest by hand instead and the check caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Freshair129
Freshair129 merged commit 42ef41f into main Sep 6, 2026
1 check passed
@Freshair129
Freshair129 deleted the fix/namespaced-requirement-ids branch September 6, 2026 04:08
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