Skip to content

refactor(#5170): typed gate evidence and a verdict-driven exit status (Phase 8 of #5056) - #5171

Merged
trek-e merged 57 commits into
nextfrom
chore/5170-gate-evidence
Oct 3, 2026
Merged

trek-e merged 57 commits into
nextfrom
chore/5170-gate-evidence

Conversation

@trek-e

@trek-e trek-e commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Fix PR

Linked Issue

Closes #5170

Refs #5056 (ADR-5057 §4, third and fourth bullets — Phase 8). Evidence, all closed: Refs #4562, Refs #4541, Refs #4259, Refs #4686, Refs #4031, Refs #4176.

What was broken

A gate that could not read its evidence treated it as empty and passed: five empty catch blocks and a tolerant readIfExists in the gate modules turned an unreadable plan, directory, COVERAGE.md or ROADMAP into "nothing there". A failing gate verb printed its negative verdict in JSON and exited 0 (phase uat-passed, verify artifacts, and the other verify verbs), so a shell caller could not tell a failed check from a passed one.

What this fix does

Adds Evidence<T> (found / none / unreadable, src/gate-evidence.cts) and a verdict-driven exit (src/gate-exit.cts, declared through cli-exit). An unreadable gate verdict exits 69 (UNAVAILABLE) and can never be a passing outcome; a negative status-mode verdict exits 1; an empty scope exits 66. The check verbs and the three drift verbs stay payload-mode: a delivered blocking verdict is still exit 0, because the workflow gate-dispatch contract treats non-zero as a command failure routed by onError. Delivered-verdict JSON is unchanged. Every shell caller of an affected verb is exit-status-aware under set -e.

Census (AST-measured, re-run on the final tree): empty or pass-shaped catch in gate hosts 5 → 0; readIfExists references 4 → 0; gate verbs that choose their own exit status: all → 0; "could not look" verdicts that exit 0: 3 + 1 (evaluation-scope) → 0. scripts/lint-gate-evidence-drift.cjs fails CI on each shape, with positive-control fixtures and an allowlist of exactly one named entry.

Also fixed here (evidence for the epic's per-phase criteria): #4562 (verify schema-drift read files_modified with an inline-array-only regex; it now reads through the Frontmatter Module), #4541 (verify plan-structure flags ! grep negative gates), #4686 (the exit statuses above), #4031 (run-tests fails a chunk whose registered tests never reached the report), #4176 (gsd-ui-auditor screenshot block follows redirects, probes the documented ports and reports failed captures). #4259 needed no change: its fix is already on next (tests/helpers/shell-doc-scan.cjs, 1f84f45ed5).

Root cause

No owner of what a gate found when it looked: each gate swallowed read failures in its own way, and the exit status was whatever output() left behind (always 0). ADR-5057 §4 assigns the owners.

Testing

How I verified the fix

gsd-test on the exact pushed sha d9e3d9b562: 50577 passed, 0 failed (linux-node24); npm run lint:ci and npm run build:lib exit 0. Three earlier runs failed and were root-caused and fixed in this branch (agent size caps, golden install trees, run-tests ledger keying, jq-dependent shell tests, ui-interaction-capture fence).

Regression test added?

Platforms tested

  • Linux
  • macOS
  • Windows (including backslash path handling)

Runtimes tested

  • N/A (not runtime-specific)

Checklist

  • Issue linked above with Closes #5170
  • Linked issue has the confirmed-bug label
  • Fix is scoped to the issue — the epic's Phase 8 deliverables and their evidence issues
  • Regression tests added
  • All tests pass via gsd-test
  • .changeset/ fragment added
  • No dependencies added

Breaking changes

Gate verbs that could not read their evidence, or that returned a negative status-mode verdict, now exit non-zero (69, 1 or 66) where they exited 0; their stdout JSON is unchanged. Shell callers outside this repository that ignore the exit status are unaffected; callers that rely on set -e or $? now see the verdict. Declared payload deltas: verify schema-drift now sees a block-sequence files_modified, verify plan-structure flags ! grep negative gates, an empty plan file exits 66 for verify artifacts and verify key-links, and verify commits outside a git work tree returns {error: 'Not a git repository'} with exit 69.

trek-e pushed a commit that referenced this pull request Oct 2, 2026
const script = [
"const fs = require('node:fs');",
'const real = fs.statSync;',
`fs.statSync = function (p, ...rest) { if (String(p) === ${JSON.stringify(phasesDir)}) { throw Object.assign(new Error('EACCES: injected'), { code: 'EACCES' }); } return real.call(fs, p, ...rest); };`,
@trek-e
trek-e force-pushed the chore/5170-gate-evidence branch from 2a3ce07 to 5a238b5 Compare October 2, 2026 12:47
trek-e pushed a commit that referenced this pull request Oct 2, 2026
sim and others added 23 commits October 3, 2026 11:25
… (T1)

Add src/gate-evidence.cts: Evidence<T> (found / none / unreadable), readTextEvidence and
readDirEvidence (ENOENT and ENOTDIR are none; every other failure is unreadable carrying the
errno), and verdictFromEvidence whose unreadable arm must return a branded UnreadableVerdict.
GateOutcome gains 'unreadable' and gate-verdict gains gateUnreadable.

Add src/gate-exit.cts: gateExitOutcome is a total mapping from a verdict and a mode to PASS,
FAIL or UNAVAILABLE, and declareGateExit declares it through the new cli-exit declareOutcome
after output() has run. emitGateResult declares in payload mode, so a delivered verdict keeps
exit 0 and an unreadable one exits UNAVAILABLE.

phase uat-passed and verify artifacts derive their exit status from their verdict (status
mode): a negative verdict exits 1, a plan with nothing to evaluate exits UNAVAILABLE. Their
stdout JSON is unchanged. verify-work.md and gsd-verifier.md branch on the exit status in the
same change.

Refs #5170, #5056

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
phase uat-passed with passed:false and verify artifacts with all_passed:false now exit 1, and a
plan with no artifacts to evaluate exits UNAVAILABLE. These assertions required result.success
on a negative verdict, which encoded the old exit-0-on-failure behavior; they now pin the exit
status that follows the verdict, and the surfaces that only need an answer accept exit 0 or 1.

Refs #5170, #5056

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…atus

Cover readTextEvidence and readDirEvidence (missing, directory, injected EACCES and EIO, empty
file, ENOTDIR), the compile-time guarantee that the unreadable arm cannot return a passing
verdict, the total gateExitOutcome mapping as a seeded fast-check property with a positive
control, declareOutcome, and child-process CLI tests for phase uat-passed and verify artifacts.

Refs #5170, #5056

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s outcome unreadable (T2)

Migrate every gate module read onto the Evidence seam and delete the tolerant
readers (ADR-5057 section 4, third bullet):

- gate-phase-context: readIfExists is deleted. resolvePhaseDirOrEmpty becomes
  resolvePhaseDir (Evidence), findUiSpecInDir and lookupRoadmapPhase answer
  none vs unreadable, locateUiSpec composes the two, and the two empty catch
  clauses are gone.
- gate-evidence: evidenceFromError, readDirEntriesEvidence, statEvidence and the
  Observed type, so a gate never needs a bare catch around a read.
- ui-plan-gate / ui-safety-gate: an unreadable ROADMAP or phase directory, and
  ui-safety's unresolvable scope, are outcome unreadable (block stays the gate's
  own policy). The verify-command-paths / verify-failure-directions probes that
  cannot look are outcome unreadable with the payload unchanged.
- api-coverage verify-pre: the empty readdir catch at the COVERAGE.md lookup is
  unreadable and never falls through to the detector; an unreadable COVERAGE.md
  or plan scope stays block:true with outcome unreadable.
- tdd-review-checkpoint: an unreadable plan or phase directory is unreadable,
  not "not a TDD plan"; an absent phase directory stays none and the existing skip.
- predicate: findPhaseArtifact throws on an unreadable lookup (the existing usage
  failure) instead of reading it as "artifact not found".
- tdd-red-evidence, decision-coverage plan/verify and their support module read
  CONTEXT.md, plans, summaries and listed files as evidence; an unreadable one is
  outcome unreadable instead of an empty string that certifies "no decisions".

Delivered pass/skip/advisory/block payloads for readable evidence are unchanged
(the cutover goldens still match byte for byte).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…p / exit 0

A verdict whose evidence could not be read is outcome unreadable and exits 69
(UNAVAILABLE); it is never a skip or exit 0. Update the tests that pinned the
old outcome:

- cutover goldens: the nine arms that cannot look (ui-safety unresolvable scope,
  both verify probes, unreadable COVERAGE.md / plan scope) now record exitCode
  69; their stdout is unchanged byte for byte.
- unit tests for ui-safety, verify-command-paths, verify-failure-directions and
  the evaluation-scope consumers expect outcome unreadable.
- the execute-wave pipeline fixture wrote files_modified outside the plan's
  frontmatter; it now carries the field in the frontmatter block, the shape
  production plans have, because schema-drift reads it through the Frontmatter
  Module.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… Module; negated grep gates are scanned (#4562, #4541)

verify schema-drift (#4562): the plan's files_modified came from a
`files_modified:\s*\[...\]` regex that knew only the inline array, so a YAML
block sequence yielded no files and a schema change reported "no drift". The
list is now read the way phase-plan-index reads it (RawPlan.filesModified), so
block sequences (one or many items), inline arrays and CRLF all yield the files.
A plan or summary that cannot be read is reported as unreadable (the existing
payload plus unreadable / unreadable_file / read_error, exit 69) and is never
"no files, no drift"; --skip still bypasses the gate.

verify plan-structure (#4541): the negative-grep comment-echo scan fired only
for `grep -c 'LIT' f == 0`. `! grep -q 'LIT' f`, `if ! grep ...` and
`grep ...; test $? -ne 0` are the same gate and now fire too, and the error names
both forms; a plain positive `grep -q 'LIT' f` gate does not fire, and an
inverted grep is still a positive assertion. A negated command pasted into an
<action> does not flag itself.

Tests: block-sequence (1 and N items), inline, CRLF, unreadable plan / summary,
--skip control; negated spellings, positive controls, allowlist, bareword, and an
end-to-end plan-structure case.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…AILABLE in every gate (matrix rows 16-23)

Behavioral coverage, each with a control that reads cleanly:

- cutover suite: exit status follows the verdict, driven through the real CLI
  (ui-safety unresolvable scope, both probes, unreadable api-coverage evidence
  exit 69 with the payload unchanged; a delivered block stays exit 0).
- api-coverage: the phase directory that cannot be listed (the former empty
  catch) is unreadable and never the detector's pass; an unreadable COVERAGE.md
  and plan scope keep block:true with outcome unreadable.
- tdd-review-checkpoint: an unreadable plan is unreadable, an absent phase
  directory is none and the existing skip.
- predicate: an unlistable phase directory is the usage failure, an absent one
  is artifactNotFound.
- ui-plan: unreadable phase directory and ROADMAP; decision-coverage plan and
  verify: unreadable CONTEXT.md, plan and listed file; tdd-red-evidence:
  unreadable record. Read and readdir failures are injected by replacing the fs
  method and restoring it in finally.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
check verify-command-paths and check verify-failure-directions exit 0 whenever
they could look (a blocker finding is still exit 0) and exit 69 (UNAVAILABLE)
with the same JSON when they could not.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… documented ports, and reports failed captures as not captured (#4176)

The static capture block misread a redirecting dev server as no server
(no -L, equality against 200), printed "Screenshots captured" when every
capture failed (stderr discarded, no exit or file check), and never tried
the 5173/8080 fallback the next line documents. It now probes 3000, 5173,
8080 with -L and a bounded --max-time, judges each capture by exit status
and a non-empty file, and ends on captured / PARTIAL / NOT captured.
Both agents/gsd-ui-auditor.md and the compact variant carry the fix; the
test executes the extracted block against a local http server.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…l reach the report (#4031)

--test-force-exit (the Windows post-test hang backstop, #1051/#869) can end
the node --test parent while part of a test file's results is still unread
on the child's pipe (nodejs/node#64833). The runner then printed a smaller
count and exited 0, so every reported count was a lower bound.

The count of registered tests now comes from the child over a channel that
does not share that pipe: scripts/lib/test-registration-ledger.cjs is loaded
into each test-file child with --require, counts the test()/it()
registrations made through node:test and appends one line per file at exit.
The ndjson reporter records the kind (suite or test) of every pass/fail, and
analyzeChunkAccounting compares the two per file. A chunk that exited 0 with
registered > reported now fails, naming the chunk and the counts, and the
remaining chunks still run. The check is one-sided (run-time subtests are
reported but not registered), so it can miss a loss but cannot invent one.
--test-force-exit stays the default, so the Windows hang protection is intact.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…t-* pattern and regenerate the conformance tiers

A scripts/lib file whose basename matches Node's test-collection pattern
would be collected and executed as a test by the remote runner, so the
preload is registration-ledger-preload.cjs. The Windows and macOS
conformance-tier lists are regenerated for the two new test files.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…at exit 69 as "could not look"

The verify-command-paths / verify-failure-directions probes and the
ui-plan-gate now exit 69 (UNAVAILABLE) when they could not look, still
printing their JSON. plan-phase.md, the quick plan-checker loop and the
ui-plan-gate capture branch on the captured exit status instead of reading
the JSON blind: 0 is a normal look, 69 hands the unresolvable payload to
the checker unchanged, any other non-zero is a command failure. verify-work.md's
gate dispatch captures CHECK_EXIT like the other dispatch hosts, and the
shared loop-hook-dispatch contract states once that exit 69 is a step-1
command failure routed by onError, never a verdict.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nd aborts on exit 69 (matrix row 30)

Extracts both documented captures from verify-work.md and runs them under
bash with set -e and a stub gsd_run: exit 0 and exit 1 with JSON are
verdicts and are read, exit 1 with nothing on stdout, exit 69 and exit 2
abort with "could not run" and read nothing.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d exits 69; the workflow captures keep its JSON

`check evaluation-scope` mapped scope.status 'unresolvable' (a missing git, a timeout, an unknown
phase) to outcome 'skip' and exit 0: "could not look" reported as a pass-shaped exit. It is now
outcome 'unreadable' (block stays false: the resolver is not a policy), so the router exits
UNAVAILABLE (69). The JSON payload is unchanged and still printed.

Consumers: the code-review and quick-task scope captures no longer discard the JSON that carries the
degradation reason (`|| SCOPE_JSON=""`) nor abort under `set -e`; exit 69 keeps the JSON, any other
non-zero status drops stdout. The gsd-code-reviewer agent (full and compact) notes exit 69.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…; workflow captures survive set -e

Corrects the resolver test that pinned 'skip' for an unresolvable scope, and adds the CLI case (exit 69
with the JSON and reason on stdout, payload equal to the in-process verdict), the degraded control
(exit 0), and a bash set -e harness over the code-review and quick-task captures (exit 0 / 69 / 68 /
70 / 1-with-empty-stdout).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ed evidence; schema-drift exceptions exit UNAVAILABLE

src/gap-checker.cts (the engine behind `check gap-analysis-plan-post`) swallowed every read: an
unreadable config, REQUIREMENTS.md, CONTEXT.md or plan became '' (so every ID read "Not covered", or
"No requirements or decisions to check"), a phase directory read failure never reached the verdict,
and `existsSync` answered "absent" for an EACCES on a parent. Every read is now `readTextEvidence`;
anything that exists but cannot be read is listed in an additive `unreadable` field (present only when
non-empty, so the delivered wire shape is unchanged) and the gate returns outcome `unreadable`
(block:false, advisory policy unchanged) which exits 69. An unreadable REQUIREMENTS.md no longer makes
every cited ID "Missing from REQUIREMENTS.md".

`fs.existsSync` probes whose answer decides a verdict are replaced by `statEvidence`: api-coverage
(the phases tree and the phase directory) and decision-coverage-plan (CONTEXT.md); the verify gate's
redundant probe is dropped (its one read already yields none/unreadable). An EACCES is `unreadable`,
never "not a GSD project" / "CONTEXT.md missing".

`verify schema-drift`: the catch around the gate printed a non-blocking "exception:" payload and exited
0, so a gate that threw looked like "no drift"; it now declares UNAVAILABLE after printing the same
payload, and an unexaminable phases directory is `unreadable` instead of "No phases directory".
`tdd-red-evidence` parses the record through typed evidence instead of a catch that returned null.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…overage, decision-coverage)

Real EISDIR fixtures where a real failure can be built; fs.statSync/readFileSync monkeypatched to throw
EACCES (restored in finally, never chmod) where it cannot. Absent controls stay `none` (documented
policy); the gap-analysis CLI case asserts exit 69 with the table on stdout and exit 0 for the control.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…int:ci

scripts/lint-gate-evidence-drift.cjs parses the gate modules, check-auto-mode, gap-checker,
decision-coverage-support and every gate-exit importer (router, phase, verify) with
@typescript-eslint/parser and fails on: an empty or literal-only catch, any `readIfExists`
reference, a gate verb entry owning its exit (process.exitCode / process.exit / numeric return /
direct declareOutcome), a passing verdict in an `unreadable` arm, and a verb catch that prints a
payload without declaring an exit. The allowlist is empty. Run over the pre-Phase-8 sources it
flags the original census (the empty catches and readIfExists).

Positive controls: one fixture per shape under tests/fixtures/gate-evidence-drift/ (stored as
.cts.txt so no compiler or linter walks a deliberately violating file) plus a clean fixture; the
test runs the guard against them, boundaries (0/1/2 catch statements), a seeded fast-check property,
fail-closed cases, and publishes the census over the real tree as a function asserted zero.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… unexaminable phases directory

Real CLI: no phases directory stays exit 0 (control); a file where the phases directory belongs throws
inside the gate and exits 69 with the unchanged non-blocking payload; an EACCES on the phases stat
(preload patch, never chmod) is `unreadable` with exit 69.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e is NO_INPUT

verify plan-structure/phase-completeness/references/commits/key-links build real
verdicts and exit 1 (negative), 66 (read, genuinely empty scope), 69 (could not
look; an empty plan file is found '', not absent). schema-drift, codebase-drift,
context-drift and prohibition-enforcement route through gate-exit in payload mode
(the gate dispatch reads .block and routes a non-zero exit by onError). Duplicated
CONTEXT.md-missing and schema-drift payload builders collapse; hand-built found
literals use evidenceFound; gap-checker indentation fixed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
safe_resume_gate and the TDD gate fail closed on an unresolvable evaluation scope,
completion reconciliation keeps the scope status, the autonomous UI gate and the
decision-coverage and drift pre-checks no longer read exit 69 as "nothing there".
Test helpers (fs failure injection, tool probe, CLI runner, doc bash spans) move
to tests/helpers and the duplicated copies are removed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…stence collapses

Entries come from the verify/phase/check routers, not a name list; new rules
verb-no-gate-exit, verdict-owns-exit and exists-collapse (one named allowlist
site, stale entries are problems), with positive-control fixtures and a census.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
sim and others added 8 commits October 3, 2026 11:27
…nts x 2 filters, its divergences are named; the io census explains why verify.cts left it

The earlier claim of equality with real jq on 26 pairs was true when measured but
only 14 cases were pinned. The test now pins all 13 documents against both
filters with expected values measured from real jq, and pins the known
divergences (empty stdin: jq exits 4, the stub 2; a string/array `.data` or a
top-level array: jq errors with exit 5, the stub reads null; an object printed by
-r: jq pretty-prints, the stub prints compact). The stub header lists them; none
is reachable from the gate handler's JSON.

tests/io.test.cjs: the stale "verify 8" breakdown is marked as measured then, and
the census entry now states the real reason verify.cts left it. Read from
src/verify.cts and src/gate-exit.cts: emitVerbVerdict prints the verdict payload
through output() with a non-literal argument (so the literal-shape census no
longer sees the sites), output() still records DEGRADED for a payload carrying an
`error` key, and declareGateExit then overrides it for a non-PASS verdict
(unreadable 69, empty 66, block in status mode 1) while a PASS leaves
output()'s outcome untouched.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A misspelled method would inject nothing, and the test that relied on it would pass without exercising a failure. The Memtrace PR review flagged the unchecked fs[method] lookup.
The other three review findings are not defects: labels[index] and FORMS[form] index collections with keys drawn from the same collection, and the setTimeout in the ui-auditor harness is a watchdog kill bound for a spawned child, cleared on close.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ut, so --test-force-exit no longer drops them (#4031)

CI (docs-lint, plugin-validate) failed with "N tests registered, M reported" while
the TAP summary itself said M, so the accounting was right and results really did
not reach the report. Root cause, from node's v24 runner source and measured on a
Linux node:24 container: the runner forwards --test-force-exit to every test-file
child (getRunArgs); each forced child calls process.exit() the moment its root test
ends, and its stdout, the pipe its V8-framed results travel on, is non-blocking on
POSIX, so frames still queued in the Socket are discarded (nodejs/node#64833, fix
proposed in nodejs/node#64875). It needs no concurrency: plugin-validate lost 23 of
54 in a single file.

Measurement (Node 24.18.0, Linux, 20 files x 300 tests, concurrency 8, the runner's
own events file and ledger, 4 runs each): count-only preload 5049-5074 reported of
6000 (ledger 6000: the child had emitted them all, so the loss is child to parent);
preload that makes the child's stdout blocking 6000/6000 on every run; without
force-exit 6000/6000.

registration-ledger-preload.cjs now sets the child's stdout handle blocking at load
(blockChildStdout, best-effort, test-file children only), the same operation the
upstream fix performs at force-exit, but done up front so nothing is ever queued.
--test-force-exit stays on every platform: it is the Windows post-test hang guard
(#1051) and nothing about that changes, whereas dropping it on POSIX would trade a
loud accounting failure for silent hangs on leaked handles. The accounting stays as
the backstop for any other loss. Unit rows pin blockChildStdout; the behavioral proof
is a real shard's own accounting.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s; acknowledge the code-reviewer agent growth

Read from the failed CI run of the previous head, each with its platform:

- ubuntu shards 1-3, windows 2/3: "the human_needed UAT precheck" and "the
  phase-completion predicate" read PASSED=false for {"passed":true} at exit 0
  (tests/verify-work-auto-transition.test.cjs:465). runBash defined `jq` as the
  subset stub unconditionally, shadowing the REAL jq that GitHub's runners have,
  and the stub knows only the two filters of the plan-phase block, so verify-work's
  `jq -r '.passed // false'` failed and fell back to "false". It passed on the
  bench because those rows skip there (no jq). The stub is now defined only when
  `command -v jq` finds none.
- windows 2/3: every run-tests-accounting row that spawns the ndjson reporter died
  with ERR_UNSUPPORTED_ESM_URL_SCHEME ("Received protocol 'd:'"): --test-reporter
  takes an import specifier, and a bare Windows path is not one (run-tests.cjs itself
  passes pathToFileURL for this reason). The tests do too.
- macOS: the ledger key is node's own realpath (/private/var/...) while the rows
  expected path.resolve of the symlinked tmp (/var/...); they compare against the
  realpath, as the runner's matching does.
- ubuntu 3/3, ubuntu, macOS: emitted-attribution against the CI base 2cc432e
  names gsd-code-reviewer.md (+210) and its compact variant (+30): the exit-69
  wording the resolver-driven file scope now carries. Acknowledged below; every
  other grown emitted file already carries its trailer, and none is stale.

Emitted-Drift-Ack-Growth: gsd-code-reviewer.md — states that the evaluation-scope resolver exits 69 for status unresolvable while still printing its JSON, so an empty files list is read as could-not-look and fails closed instead of as a clean review
Emitted-Drift-Ack-Growth: gsd-code-reviewer.compact.md — the same exit-69 wording for the compact variant of the reviewer's scope resolution
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…w node:test's report frames

The accounting still failed on CI after the blocking-stdout fix, on exactly two
files: ci-pr-mergeability.test.cjs (81 registered, 38 reported) and
ci-next-health.test.cjs (37 registered, 3 reported). The results were genuinely
dropped child to parent, by the tests themselves, not over-counted: their
callMain() does t.mock.method(process.stdout, 'write', ...) and
t.mock.method(process.stderr, 'write', ...) to capture the CLI's output. In a
test-file child node:test writes its V8-framed results as Buffers through
process.stdout.write, looked up at write time, so while the mock is active the
frames are captured into the test's array and never reach the parent: no error,
exit 0. Measured on Node 24 with a real framed test-file child: 22 tests of which
two mock process.stdout.write reported 11; the same shape with the fixed helper
reported 22; 22 plain tests reported 22.

tests/helpers/stdio-capture.cjs captureStringWrites() records the strings the code
under test writes (the scripts write strings) and forwards everything else to the
real write. Both files use it. A unit row pins it. Any other test that mocks the
stdio writes the same way will now be named by the accounting, which is its job.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ro-result one

macOS CI failed one row: `--test-only` expected the ledger file as
/private/var/... and got /var/.... The ledger has two sources for a file name:
node's own result events (`data.file`, already resolved through symlinks) and,
for a child that reports no result, process.argv[1] as given. With the tmp dir
symlinked (macOS /var -> /private/var) the two kinds of line disagreed about the
same file. The preload now writes canonicalPath() (path.resolve + best-effort
fs.realpathSync.native, falling back to the absolute spelling) on every line.
analyzeChunkAccounting normalizes both sides regardless; a shim with a symlinked
tmp dir confirms it matches both spellings. The accounting tests compare with
fs.realpathSync.native throughout; the remaining path.resolve rows feed synthetic
paths straight to analyzeChunkAccounting, which normalizes them itself.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…latest next

Upstream added the Test-Report Adapter Module entry next to this branch's Gate Evidence and Gate Exit entries in CONTEXT.md; both generated indexes are rebuilt from the merged glossary (301 predicates).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@trek-e
trek-e force-pushed the chore/5170-gate-evidence branch from 3141c26 to 71e6bbd Compare October 3, 2026 15:50
@trek-e
trek-e merged commit 84b4dfe into next Oct 3, 2026
48 checks passed
@trek-e
trek-e deleted the chore/5170-gate-evidence branch October 3, 2026 17:38
drakeo338 added a commit to drakeo338/gsd-core that referenced this pull request Oct 3, 2026
…hat-d

Resolve the decision-coverage gate conflicts against open-gsd#5171's typed gate
evidence and exit-status model, keeping open-gsd#4939's caller errors for a phase
dir that names no directory (null totals, ENOENT/ENOTDIR -> not found).
Regenerate compact-content-benchmark-baseline.json with the script.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 3, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 5, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 7, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 9, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 9, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 11, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 16, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 18, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 21, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 22, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 23, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
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.

chore(#5056): gate evidence is typed and the exit code follows the verdict — Phase 8

2 participants