chore(#2143): fail-loud Result + per-surface write-set contract — Phase 3 (#2251)

* chore(#2143): fail-loud Result + per-surface write-set contract — Phase 3

Phase 3 of epic #2143 (ADR-2143 §5/§6). The three target bugs (#2140, #2112,
#2118) were already fixed tactically on next; this introduces the reusable
structural contracts and rewires the primary #2140 site onto them.

- src/write-set.cts (new): the parse `Result<T> = {ok,value|reason}` (§5) and the
  per-surface write-set (`WriteOutcome {surface, applied, requirement?}`,
  `WriteSet`, `writeSetComplete`) (§6). markdown-table.cts now imports + re-exports
  `Result` from here (single source; distinct from command-routing-hub's Result).
- requirements mark-complete (src/milestone.cts): returns a PER-REQUIREMENT,
  per-surface write-set; `write_set_complete` is true only if every surface of
  every requirement applied — structurally forbidding the #2140 OR-into-one-flag
  masking, including across a multi-ID batch (adversarial-review regression).
  Pre-existing output fields unchanged (behaviour-preserving; #2140 already fixed).
- deriveProgressFromRoadmap (src/phase-lifecycle.cts): removed the vestigial
  null-swallowing try/catch (findTableWithColumns never throws) — ADR §5 no-swallow;
  RoadmapProgress return contract unchanged.
- commit --files (#2112) and milestone complete --dry-run (#2118) left as-is
  (single-surface commit / pre-mutation preview — not genuine multi-surface writes).

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

* chore(#2244): backfill changeset PR number (#2251)

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-13 20:39:58 -04:00
committed by GitHub
parent efd04716da
commit 2cbf186420
12 changed files with 381 additions and 43 deletions

View File

@@ -18,6 +18,8 @@ import { platformWriteSync, platformEnsureDir, execGit, retryRenameSync } from '
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { realClock } from './clock.cjs';
import { transitionCore } from './state-transition.cjs';
import { writeSetComplete } from './write-set.cjs';
import type { WriteSet } from './write-set.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import ioMod = require('./io.cjs');
const { output, error } = ioMod;
@@ -79,6 +81,18 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
// a missing row only counts as drift when a table actually exists.
const hasTable = /^\|\s*Requirement\s*\|/im.test(reqContent);
// ADR-2143 §6 per-surface write-set, tracked PER requirement ID: a
// multi-ID batch must not OR one ID's surface outcome into another's —
// that is the exact #2140 class one level up (an ID whose traceability
// row is absent/unmatched must not have its partial write masked by a
// different ID in the same invocation that fully reconciled). Reported
// additively as `write_set` below — it does not change the existing
// marked_complete/already_complete/not_found/table_unmatched/updated
// computation, which stays byte-for-behaviour identical (#2140's tactical
// fix already surfaces the checkbox-only-partial-write case via
// table_unmatched; this only adds the structured ADR-2143 shape on top).
const writeSet: WriteSet = [];
for (const reqId of reqIds) {
const reqEscaped = escapeRegex(reqId);
@@ -96,6 +110,16 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
const tableHit = afterTable !== reqContent;
if (tableHit) reqContent = afterTable;
// ADR-2143 §6 per-ID write-set entries: this ID's checkbox surface is
// always tracked; the traceability surface is tracked only when the file
// has a traceability table at all (same `hasTable` gate the existing
// required-surface logic below uses) — omitted entirely, not a false
// `applied:false`, when no table is required of this file.
writeSet.push({ requirement: reqId, surface: 'checkbox', applied: checkboxHit });
if (hasTable) {
writeSet.push({ requirement: reqId, surface: 'traceability', applied: tableHit });
}
// Coverage of the traceability surface for this ID (computed after any flip).
// hasRow keys on the ID + a second cell (`| ID | <phase> |`) so a bare mention
// of the ID in a non-traceability table does not masquerade as a real row.
@@ -129,6 +153,17 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
platformWriteSync(reqPath, reqContent);
}
// ADR-2143 §6: `writeSet` above already carries one WriteOutcome per
// (requirement, surface) this invocation could have written to — per ID,
// not ORed across the batch. `write_set` and `write_set_complete` are
// additive: they do not replace or gate `updated` / `marked_complete` /
// `already_complete` / `not_found` / `table_unmatched`, which remain
// computed exactly as before (see #2140 note above — that fix already
// surfaces a checkbox-only partial write via `table_unmatched`;
// `write_set_complete` is a structured, ADR-2143-shaped read of the SAME
// per-surface, per-ID facts, `false` if ANY id's ANY required surface did
// not apply, since `writeSetComplete` requires EVERY entry to have
// applied, never an OR across surfaces OR across IDs).
output(
{
updated: updated.length > 0,
@@ -137,6 +172,8 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
not_found: notFound,
table_unmatched: tableUnmatched,
total: reqIds.length,
write_set: writeSet,
write_set_complete: writeSetComplete(writeSet),
},
raw,
`${updated.length}/${reqIds.length} requirements marked complete`,