chore(#2371): representative gate-fixture corpus + document-shaped property test (#2380)

* chore(#2371): representative gate-fixture corpus + document-shaped property test

Adds tests/fixtures/representative/ — a permanent corpus of verbatim,
incident-sourced fixtures (never author-invented) from #2286, #2347,
#2365, #2366, each labeled with its expected gate verdict in a
MANIFEST.json and driven through the real CLI gate entrypoint via
tests/representative-corpus.test.cjs.

Adds a document-shaped fast-check property test alongside the existing
writer-seeded bijection test in tests/api-coverage.test.cjs: the existing
generator produces rows and renders them through the writer, so the
document shape is a constant and it cannot fail against a decoy table;
the new one generates the document space instead.

Two gates (#2365, #2347) are still open, so their corpus/property
assertions are marked with node:test's official `todo` option — the test
executes and reports its failure without affecting the process exit code
(https://nodejs.org/api/test.html#test-options). The audit-uat corpus
(#2286, fixed by #2317) is a normal passing assertion, proving the
methodology works end to end and not just cataloguing gaps.

Records the fixture-provenance rule in CONTRIBUTING.md: a gate's fixtures
may not be derived from the gate's own writer, grammar, or docstring
examples; a negative fixture must come from a source that doesn't know
the gate exists.

No production src/*.cts changes — validation only, per #2371's scope.

Closes #2371

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2371): address orthogonal-review findings — dedup generators, fix field naming, wire dead fields

Standards-axis review findings, all fixed:

- Deduplicated the row-shape generators (capabilityGen/rowGen/validRowGen)
  that were copy-pasted between the parse/render bijection test and the
  new document-shaped property test in tests/api-coverage.test.cjs — a
  future edit to one could have silently desynced the two properties.
  Hoisted to a single module-scope declaration both tests reference.

- Renamed decision-coverage-guard/MANIFEST.json's expectedOutcome ->
  expectedReason. It asserted against the gate's `reason` field, but this
  codebase already has a real, different `outcome` field at parser
  altitude (extractDecisions' DecisionOutcome) — naming the manifest
  field after the wrong altitude's term was exactly the ambiguity the
  "Fixture provenance" rule this PR adds exists to eliminate.

- Removed the unused `role` field from three MANIFEST.json files (never
  read by any test) and wired the previously-dead per-fixture
  `expectedMinItems` in audit-uat/MANIFEST.json into a real per-file
  assertion in tests/representative-corpus.test.cjs, using cmdAuditUat's
  `results` array — catches a regression that moves items between the
  two fixture files while preserving the aggregate total, which the
  existing total_items check alone would miss.

No changes to test intent or coverage — same assertions, correctly named
and fully wired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: remove dead capabilityState/capabilityWriter requires from gsd-tools.cjs

Surfaced by the mandatory pre-PR lint gate (no-unused-vars) while
preparing this PR — unrelated to #2371's own changes, but a defect
found while working is fixed in place rather than deferred.

Leftover from #2368/#2370 (merged just before this branch rebased onto
it): the case 'capability' arm that needed these two requires was
relocated to bin/lib/capability-command-router.cjs, which already
requires both modules directly (lines 24-25) and is their only real
consumer (cmdCapabilityState, resolveCapabilityRuntimeState,
cmdCapabilitySet). The two requires left behind in gsd-tools.cjs had
zero other references in the file and were never re-exported —
confirmed via grep across the file and its module.exports.

Behavior-preserving: Node's require cache means the underlying modules
still load exactly once via capability-command-router.cjs's own
requires; gsd-tools.cjs never used its now-removed local bindings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2371): replace todo-marked assertions with characterization tests

gsd-test's own JSONL result parser (gsd-test-runner's
internal/pipeline/parse.go, verified directly against that repo's
source) has no concept of node:test's `todo` option — it only
recognizes kind:"pass"|"fail" and hard-errors on anything else. A
{ todo: true } test whose body throws is counted as a real failure in
gsd-test's own verdict, exactly as if it weren't marked todo — proven
by an actual gsd-test run against this branch, which reported
outcome:"failed" with all six todo-marked assertions (the property
test plus five representative-corpus fixtures) in the failure list,
each carrying the correct raw node:test `todo` field the tool's parser
simply doesn't read.

Replaces todo with characterization: MANIFEST.json now carries both
the correct target verdict (expected*) and the exact current observed
verdict (currentBuggyOutput, directly verified against live CLI
output for all five fixtures). Tests assert currentBuggyOutput — an
honest, non-vacuous pin of today's known-broken reality that passes
today and will fail loudly the moment the referenced fix changes the
observed output, at which point the assertion should be flipped to
expected* and currentBuggyOutput deleted.

The document-shaped property test switches from throwing fc.assert to
non-throwing fc.check (returns RunDetails per fast-check's own docs)
and asserts report.failed === true directly, for the same reason.

Updates all prose (CONTRIBUTING.md, the fixture READMEs) that
previously claimed todo would be respected — that claim was
factually wrong for this repo's actual tooling and must not ship.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: regenerate golden-install-parity fixtures after rebase onto next

Rebasing onto the current next (which now includes #2381's
todo-severity changes to gsd-core/bin/gsd-tools.cjs) produced real
conflicts in all 18 golden-install-parity fixtures — expected, since
both branches changed the same gsd-tools.cjs hash entry. Resolved by
taking one side to unblock the rebase, then regenerating fresh from
source via npm run gen:golden and verifying the result; every file's
diff is exactly the one hash line for gsd-core/bin/gsd-tools.cjs,
correcting a stale intermediate hash from the arbitrary conflict pick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-17 15:23:09 -04:00
committed by GitHub
parent 58028eaf56
commit 062f3fda90
39 changed files with 721 additions and 34 deletions

82
tests/fixtures/representative/README.md vendored Normal file
View File

@@ -0,0 +1,82 @@
# Representative Gate Fixtures (#2371)
Every fixture in this tree is **verbatim** (or, where noted, a minimal
faithful subset) of an artifact that a real user actually produced and
reported against a real GSD gate. None of it was written by a gate's own
author to exercise that gate.
## Why this directory exists, and why `tests/fixtures/adversarial/` isn't enough
`tests/fixtures/adversarial/` covers hostile input — unicode, CRLF, nested
fences, heredoc breakout. Nobody attacked the gates these fixtures target.
A developer wrote an ordinary, well-formed artifact — a plan, a coverage
matrix, a CONTEXT.md — that a gate misjudged anyway. That input is neither
synthetic-happy nor adversarial; it's simply *real*, and until #2371 no gate
had coverage for it.
The pattern this corpus exists to break: a gate's test fixtures were
authored by the same person (or model) who wrote the gate, from the same
mental model, so they can only confirm what the author already believed —
never surface what the author didn't anticipate. See #2371 for the full
diagnosis (four incidents across three gates in ten days, including a
fast-check property test whose generator was seeded from the parser's own
writer function and therefore could not fail).
## Rule
**A gate's fixtures may not be derived from the gate's own writer, grammar,
or docstring examples. A negative fixture must come from a source that
does not know the gate exists.** Recorded in `CONTRIBUTING.md` under
"Fixture provenance."
## Layout
Each subdirectory is one gate:
- `api-coverage-detector/` — `detectApiIntegration` (#2365)
- `api-coverage-matrix/` — `parseCoverageMatrix` (#2366)
- `audit-uat/` — `parseUatItems` / `parseVerificationItems` (#2286, fixed by #2317)
- `decision-coverage-guard/` — `extractDecisions`'s could-not-parse guard (#1365 gap, #2347)
Each carries its own `README.md` (what each fixture is and where it came
from) and `MANIFEST.json` (machine-readable: file → source issue → gate →
expected verdict). `tests/representative-corpus.test.cjs` loads every
manifest and drives each fixture through the gate's real CLI entrypoint —
never the parser function directly — so the assertion is at gate-verdict
altitude (the boolean/JSON a user actually sees), not parse-tree altitude.
## Why the still-broken fixtures assert `currentBuggyOutput`, not a red `todo`
Three of the four gates here are still open bugs (#2365, #2366, #2347) at
the time this corpus was added. Their `MANIFEST.json` entries carry BOTH
the correct target verdict (`expected*` — what the eventual fix must
produce) and the exact CURRENT observed verdict (`currentBuggyOutput` —
what today's code actually returns). The test asserts against
`currentBuggyOutput`: an honest, non-vacuous characterization of today's
known-broken reality, not a fake pass.
This is deliberately NOT node:test's `todo` option. `todo` looked like the
right tool — a todo test executes and reports its failure without
affecting Node's own process exit code
(https://nodejs.org/api/test.html#test-options) — but this repo's actual
test-runner (`gsd-test` / `gsd-test-runner` v1.6.2) has no concept of it:
its JSONL result parser (`internal/pipeline/parse.go`'s `parseJSONL`, in
the separate `gsd-test-runner` repo) only recognizes `kind: "pass" | "fail"`
and hard-errors on anything else — verified directly against that source,
not assumed. A `{ todo: true }` test whose body throws is still counted as
a real failure in `gsd-test`'s own verdict, which would block the push
gate exactly as if it weren't marked todo at all.
Asserting `currentBuggyOutput` sidesteps this because the test genuinely
passes today — no runner-level "expected failure" feature required. The
fixes belong to #2365 / #2366 / #2347, not to this corpus. When one of
those lands, the corresponding assertion will fail (the gate now returns
something other than the pinned buggy value) — at that point, flip the
test to assert `expected*` instead and delete the stale
`currentBuggyOutput`.
The `audit-uat/` corpus has no `todo`: #2286 was fixed by #2317 before this
corpus was written, so its assertions are ordinary, currently-passing
tests — the proof that a representative fixture, driven through the real
gate, is not automatically doomed to fail. It demonstrates the methodology
working, not just the gaps it finds.

View File

@@ -0,0 +1,29 @@
{
"gate": "api-coverage.verify-pre",
"sourceIssue": "#2365",
"fixtures": [
{
"file": "nextjs-route-path.txt",
"expectedDetected": false,
"currentBuggyOutput": { "detected": true, "signal": { "verb": "integration", "noun": "api" } },
"note": "First-party Next.js route path. The noun-boundary class [^a-zA-Z0-9] treats '/' as a word boundary, so 'api' inside the path matches as if it were prose. Highest blast radius: any Next.js project names a route file."
},
{
"file": "unrelated-verb-noun.txt",
"expectedDetected": false,
"currentBuggyOutput": { "detected": true, "signal": { "verb": "wiring", "noun": "endpoint" } },
"note": "'wiring' and 'endpoint' co-occur on one line in unrelated clauses, reverse semantic order, no integration described. The verb/noun regexes are independently exec'd over the whole line with no proximity or grammatical relation."
},
{
"file": "threat-model-prose.txt",
"expectedDetected": false,
"currentBuggyOutput": { "detected": true, "signal": { "verb": "(surface)", "noun": "api" } },
"note": "Threat-model table cell describing a LOCAL interface. SERVICE_SURFACE_API_RE matches any capitalized word before API/SDK/REST/GraphQL; the stopword denylist cannot enumerate every ordinary English word that precedes 'API' in a sentence."
},
{
"file": "non-integration-assertion.txt",
"expectedDetected": false,
"note": "This line explicitly ASSERTS non-integration ('no new command/dependency') and is still read as an integration signal in the real $gsd-verify-work occurrence this was drawn from — but in isolation it already returns detected:false today (the multi-signal real occurrence needed the OTHER lines' signals to trip the gate). No currentBuggyOutput: this fixture already passes for the right reason."
}
]
}

View File

@@ -0,0 +1,23 @@
# API-coverage detector fixtures (#2365)
Verbatim reproduction lines from #2365, each used as a `PLAN.md` body and
driven through `check api-coverage.verify-pre <phaseDir>` — the real
blocking gate, not `detectApiIntegration()` called in isolation.
- `nextjs-route-path.txt` — a first-party Next.js route path read as an
external API signal because `/` is a word boundary.
- `unrelated-verb-noun.txt` — a verb and a noun on the same line, unrelated
clauses, no compound relation.
- `threat-model-prose.txt` — a threat-model table cell describing a LOCAL
interface, misread as a third-party service name.
- `non-integration-assertion.txt` — a line that explicitly states no new
integration was added, misread as evidence of one.
The first three currently `detected: true`; the gate expects `false` for
all four (see `MANIFEST.json`'s `expectedDetected`/`currentBuggyOutput`
fields). `non-integration-assertion.txt` already returns `detected: false`
in isolation, so it's asserted directly. The other three are asserted
against their `currentBuggyOutput` in `tests/representative-corpus.test.cjs`
— a characterization of today's known-broken behavior, not a `todo` (see
`tests/fixtures/representative/README.md` for why `todo` doesn't work with
this repo's test-runner) — until #2365 lands.

View File

@@ -0,0 +1 @@
Run integration tests for src/app/api/profile/route.test.ts

View File

@@ -0,0 +1 @@
Reuse existing validator APIs; no new command/dependency.

View File

@@ -0,0 +1 @@
| Tampering | Resolver-only API rejects arbitrary caller URLs. |

View File

@@ -0,0 +1 @@
Render the page and prove label endpoint, filename, and CSV/XLSX wiring.

View File

@@ -0,0 +1,23 @@
{
"gate": "api-coverage.verify-pre",
"sourceIssue": "#2366",
"fixtures": [
{
"file": "multi-table-with-summary.md",
"pairedPlan": "Integrate the Stripe API for payment processing.",
"expectedCounts": { "surface": 3, "integrate": 1, "optout": 2 },
"expectedErrorCount": 0,
"expectedBlock": false,
"currentBuggyOutput": {
"block": true,
"error_count": 3,
"errors": [
"row: decision \"**OPT-OUT**\" not in {INTEGRATE, OPT-OUT}",
"row: decision \"DECISION\" not in {INTEGRATE, OPT-OUT}",
"row: decision \"12\" not in {INTEGRATE, OPT-OUT}"
]
},
"note": "Self-contained 15-line COVERAGE.md from #2366's own reproduction: one canonical matrix (search/skip), a second section-split canonical table (widget) for a transferred capability, and a decoy 3-column summary table whose 2nd cell reads 'INTEGRATE'. Today's parser invents a 'tier' capability from the summary table's header row (silent corruption, zero errors) and drops 'skip' (bolded decision rejected), while also spuriously erroring on the second header — the errors array above shows the rejected-DECISION-header and the rejected-bolded-OPT-OUT and rejected-numeric-'12'-cell paths, none of which mention the silently-invented 'tier' row (that corruption produces no error at all, which is the headline finding). Expected once fixed: exactly 3 rows (search, skip, widget), 0 errors — see expectedCounts/expectedErrorCount/expectedBlock."
}
]
}

View File

@@ -0,0 +1,23 @@
# API-coverage matrix fixture (#2366)
`multi-table-with-summary.md` is the verbatim 15-line `repro-coverage.md`
from #2366's own reproduction, used as a `COVERAGE.md` body and driven
through `check api-coverage.verify-pre <phaseDir>` (paired with a PLAN.md
that integrates an API, so a matrix is required) — the real blocking gate,
not `parseCoverageMatrix()` called in isolation.
Contains, in one file: the canonical `| capability | decision | reason |`
matrix, a second canonical-schema table under a "Transferred to a later
phase" heading (the section-split use case #2366 names as legitimate), and
a decoy 3-column "Coverage summary" table whose header row happens to read
`| tier | INTEGRATE | OPT-OUT |`.
Expected once fixed: 3 rows (`search`, `skip`, `widget`), 0 errors (see
`MANIFEST.json`'s `expectedCounts`/`expectedErrorCount`/`expectedBlock`).
Today's parser instead invents a `tier` capability from the summary table
(silent corruption — zero errors reported for that path) while rejecting
the bolded `skip` decision and two other cells, producing 3 errors and
`block: true` — pinned in `MANIFEST.json`'s `currentBuggyOutput` and
asserted directly in `tests/representative-corpus.test.cjs` (a
characterization of today's known-broken behavior, not a `todo` — see
`tests/fixtures/representative/README.md` for why) until #2366 lands.

View File

@@ -0,0 +1,18 @@
# API Coverage — demo
| capability | decision | reason |
|---|---|---|
| search | INTEGRATE | |
| skip | **OPT-OUT** | not needed yet |
## Transferred to a later phase
| capability | decision | reason |
|---|---|---|
| widget | OPT-OUT | deferred to 9 |
## Coverage summary
| tier | INTEGRATE | OPT-OUT |
|---|---|---|
| phase 8 | 12 | 6 |

View File

@@ -0,0 +1,20 @@
{
"gate": "audit-uat",
"sourceIssue": "#2286",
"fixedBy": "#2317",
"fixtures": [
{
"file": "gaps-section-uat.md",
"filenameSuffix": "-UAT.md",
"expectedMinItems": 1,
"note": "A '## Gaps' bullet entry with status: open. Before #2317, parseUatItems never scanned this section at all, so this file's real outstanding item was silently invisible."
},
{
"file": "human-verification-frontmatter.md",
"filenameSuffix": "-VERIFICATION.md",
"expectedMinItems": 1,
"note": "Frontmatter declares status: human_needed with a populated human_verification: array. Before #2317, parseVerificationItems never read the frontmatter's structured array and only recognized specific body shapes, so this file read as zero items."
}
],
"expectedTotalItems": 2
}

View File

@@ -0,0 +1,17 @@
# Audit-UAT fixtures (#2286, fixed by #2317)
Verbatim reproduction files from #2286, driven through `gsd-tools
audit-uat --raw` (the real CLI gate) rather than `parseUatItems` /
`parseVerificationItems` called in isolation.
- `gaps-section-uat.md` — a UAT file whose only outstanding finding lives
in a `## Gaps` bullet entry.
- `human-verification-frontmatter.md` — a VERIFICATION file whose
frontmatter declares a structured `human_verification:` array.
This is the one corpus in `tests/fixtures/representative/` with **no**
`todo` marker. #2286 was fixed by #2317 (merged) before this corpus was
written, so `total_items >= 2` is a normal, currently-passing assertion —
proof that a representative fixture, driven through the real gate, is not
automatically doomed to fail. It demonstrates the methodology working end
to end, not just the gaps it finds in the other three gates.

View File

@@ -0,0 +1,4 @@
## Gaps
- truth: "SC1: some success criterion"
status: open

View File

@@ -0,0 +1,11 @@
---
status: human_needed
human_verification:
- test: "Confirm the widget renders correctly"
---
## Human Verification Required
### 1. Widget render check
**Confirm the widget appears as expected on the dashboard.**

View File

@@ -0,0 +1,18 @@
{
"gate": "check.decision-coverage-plan",
"sourceIssue": "#2347",
"fixtures": [
{
"file": "d5-prefix-context.md",
"expectedReason": "could-not-parse",
"expectedPassed": false,
"currentBuggyOutput": {
"passed": true,
"skipped": true,
"reason": "no trackable decisions",
"total": 0
},
"note": "A <decisions> block using the D5-01 ID-prefix shape from #2347's own reproduction ('- **D5-01:** some decision'), repeated twice so 'populated but 0 extracted' is unambiguous. The original report used 23 real decisions under a project-specific D5- prefix convention; this fixture preserves the exact grammar mismatch, not the count. The #1365 guard's evidence test (/\\bD-[A-Za-z0-9]/) shares the parser's own D- grammar, so it is blind to exactly the input class it exists to catch: both see nothing, and the gate reports passed:true, skipped:true, reason:'no trackable decisions' instead of failing loud."
}
]
}

View File

@@ -0,0 +1,24 @@
# Decision-coverage guard fixture (#2347)
`d5-prefix-context.md` is the verbatim reproduction shape from #2347 — the
`- **D5-01:** some decision` bullet given in the issue's own "Steps to
reproduce" — used as a CONTEXT.md `<decisions>` block and driven through
`query check.decision-coverage-plan <phaseDir> <contextPath>` (the real
CLI gate; see `tests/decisions.test.cjs` for the established pattern this
follows), not `extractDecisions()` called in isolation.
The #1365 fail-loud guard's "is this decision-shaped?" evidence test
(`/\bD-[A-Za-z0-9]/`) reuses the same `D-` grammar as the parser it guards.
For any ID prefix the parser cannot read — `D5-01` here — the guard sees
no evidence either, so the two failure modes the guard exists to
distinguish (`none-present` vs `could-not-parse`) collapse into
`none-present`, and a populated, genuinely decision-shaped CONTEXT.md
passes silently.
Expected once fixed: `reason: 'could-not-parse'`, `passed: false` (see
`MANIFEST.json`'s `expectedReason`/`expectedPassed`). Today's gate instead
reports `passed: true, skipped: true, reason: 'no trackable decisions'` —
pinned in `MANIFEST.json`'s `currentBuggyOutput` and asserted directly in
`tests/representative-corpus.test.cjs` (a characterization of today's
known-broken behavior, not a `todo` — see
`tests/fixtures/representative/README.md` for why) until #2347 lands.

View File

@@ -0,0 +1,4 @@
<decisions>
- **D5-01:** some decision
- **D5-02:** some other decision
</decisions>