fix(#2788): recover Gaps Found rows; mark-complete no longer false-succeeds on a rejected row (#2902)
* fix(#2788): recover Gaps Found rows; mark-complete no longer false-succeeds on a rejected row
Two coupled defects in the requirement traceability state machine:
Defect 1 (terminal state): requirements revert-phase (the gaps_found response)
left a row at 'Gaps Found' with no inverse — neither mark-complete's /^pending$/i
guard nor the phase-complete reconcile's /^(?:pending|in progress)$/i accepted it,
so a single failed verification stranded every requirement permanently and blocked
the milestone. Widen both guards to accept 'gaps found' so a genuinely-satisfied
stranded row reaches Complete again.
Defect 2 (false success): mark-complete ORed checkboxHit || tableHit for 'updated',
so on a Gaps Found row it flipped the checkbox but could not move the row, yet
reported updated:true. When a traceability table has a row for an ID, gate 'updated'
on the row moving (tableHit) — a checkbox-only flip on a table-bearing file no
longer lies. The #2140 table_unmatched path (no row for the ID) is preserved.
* chore(#2788): backfill changeset PR 2902
---------
Co-authored-by: Test <test@example.com>
(cherry picked from commit 9f567a1627)
This commit is contained in:
@@ -156,10 +156,15 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
|
||||
// Surface 1 — the checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
||||
// Use replace() + compare to avoid the test()+replace() global regex
|
||||
// lastIndex bug where test() advances state and replace() misses matches.
|
||||
// (#2788 defect 2: the flip is CONDITIONAL — when a traceability row EXISTS
|
||||
// for this ID but its Status write is rejected, the checkbox must NOT flip,
|
||||
// so the two surfaces cannot silently diverge. The row-write outcome below
|
||||
// gates whether the flip is kept.)
|
||||
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
|
||||
const beforeCheckbox = reqContent;
|
||||
const afterCheckbox = reqContent.replace(checkboxPattern, '$1x$2');
|
||||
const checkboxHit = afterCheckbox !== reqContent;
|
||||
if (checkboxHit) reqContent = afterCheckbox;
|
||||
const checkboxFlipped = afterCheckbox !== beforeCheckbox;
|
||||
if (checkboxFlipped) reqContent = afterCheckbox;
|
||||
|
||||
// Surface 2 — the traceability row: | <REQ-ID> | Phase N | Pending | → ... Complete |
|
||||
// via the markdown-table seam (ADR-2143 §7) — supersedes the prior ordinal
|
||||
@@ -178,7 +183,11 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
|
||||
// updateTableCell call both probes the current value and writes.
|
||||
let tableHit = false;
|
||||
const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
|
||||
if (/^pending$/i.test(current.trim())) {
|
||||
// #2788: accept `Gaps Found` as a forward input too — `revert-phase` (the
|
||||
// documented gaps_found response) leaves a row stranded at Gaps Found with
|
||||
// no inverse; a genuinely-satisfied requirement must be able to reach
|
||||
// Complete again via mark-complete, or the milestone is blocked forever.
|
||||
if (/^(pending|gaps found)$/i.test(current.trim())) {
|
||||
tableHit = true;
|
||||
return ' Complete ';
|
||||
}
|
||||
@@ -188,6 +197,18 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
|
||||
reqContent = tableUpdate.value;
|
||||
}
|
||||
|
||||
// #2788 defect 2: if a row EXISTS for this ID but its Status write was
|
||||
// rejected (e.g. the row reads `Blocked`, which mark-complete does not
|
||||
// accept), roll the checkbox back so the checkbox and the row cannot
|
||||
// silently diverge. The checkbox and the row are two representations of the
|
||||
// same fact; flipping one while the other rejects the write is the lie.
|
||||
let checkboxHit = checkboxFlipped;
|
||||
const rowExistsProbe = tableUpdate; // ok === a row matched (probes existence)
|
||||
if (checkboxFlipped && rowExistsProbe.ok && !tableHit) {
|
||||
reqContent = beforeCheckbox;
|
||||
checkboxHit = false;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -215,7 +236,14 @@ function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: bool
|
||||
const doneCheckbox = new RegExp(`-\\s*\\[x\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'i').test(reqContent);
|
||||
const doneTable = Boolean(hasRow && /^complete$/i.test(currentStatusCell.trim()));
|
||||
|
||||
if (checkboxHit || tableHit) {
|
||||
// #2788 defect 2: when a traceability table exists AND this ID has a row in
|
||||
// it, `updated`/`marked_complete` must reflect the ROW moving, not a
|
||||
// checkbox-only flip. Otherwise (`table_unmatched` — no row for this ID, or
|
||||
// no table at all) the checkbox flip is a legitimate partial reconcile / the
|
||||
// sole completion surface, so the #2140 OR semantics are preserved.
|
||||
const rowExists = hasTable && hasRow;
|
||||
const idUpdated = rowExists ? tableHit : (checkboxHit || tableHit);
|
||||
if (idUpdated) {
|
||||
updated.push(reqId);
|
||||
} else if (doneTable || (doneCheckbox && !hasTable)) {
|
||||
// Fully reconciled: the table row is Complete, OR the checkbox is done and
|
||||
|
||||
@@ -2078,7 +2078,9 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
// Complete" gate is folded into the newValue callback so one
|
||||
// updateTableCell call both probes and writes.
|
||||
const reqUpdate = updateTraceabilityCell(reqContent, reqRowMatch, 'Status', (current) =>
|
||||
/^(?:pending|in progress)$/i.test(current.trim()) ? ' Complete ' : current);
|
||||
// #2788: accept `Gaps Found` too so a phase stranded by revert-phase (the
|
||||
// gaps_found response) can complete without hand-editing the table.
|
||||
/^(?:pending|in progress|gaps found)$/i.test(current.trim()) ? ' Complete ' : current);
|
||||
if (reqUpdate.ok) {
|
||||
reqContent = reqUpdate.value;
|
||||
} else if (!isPlaceholderReqId(reqId)) {
|
||||
|
||||
Reference in New Issue
Block a user