* test(#2945): prove phase complete checkbox ignores traceability row rejection Failing-first regression for #2945. cmdPhaseComplete flips the REQUIREMENTS.md checkbox unconditionally and never rolls back when the traceability row exists but rejects the Status write (Deferred/Blocked), so a deferred requirement reads as shipped. Rows 1-2 assert the checkbox stays [ ] for Deferred/Blocked; Row 3 guards the forward-status flip; Row 4 covers the no-row boundary. * fix(#2945): roll back the REQUIREMENTS checkbox when the traceability row rejects the write cmdPhaseComplete's inline requirement-write loop flipped the REQUIREMENTS.md checkbox unconditionally and kept the flip when the traceability row existed but rejected the Status write (Out/Deferred/Blocked), so a deferred requirement read as shipped — the #2788 defect-2 fix was written into cmdRequirementsMarkComplete (milestone.cts) only, never the phase.cts inline copy. Port the rollback: capture beforeCheckbox, track tableHit in the updateTraceabilityCell callback, and when reqUpdate.ok && !tableHit (row existed but rejected the write), restore beforeCheckbox. The two surfaces can no longer silently diverge. Forward-status rows (Pending/In Progress/Gaps Found) still flip + advance; absent rows still flip (nothing to disagree with). * chore(#2945): add changeset fragment * chore(#2945): backfill changeset PR number 3073 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -2241,10 +2241,17 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
|
||||
for (const reqId of citedReqIds) {
|
||||
const reqEscaped = escapeRegex(reqId);
|
||||
reqContent = reqContent.replace(
|
||||
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
||||
'$1x$2',
|
||||
);
|
||||
// Surface 1 — the checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**.
|
||||
// #2945: the flip is CONDITIONAL (porting #2788 defect-2's rollback from
|
||||
// cmdRequirementsMarkComplete). Capture the pre-flip content; if a
|
||||
// traceability row EXISTS for this ID below but its Status write is rejected
|
||||
// (Out/Deferred/Blocked), the checkbox is rolled back so the two surfaces
|
||||
// cannot silently diverge. A requirement recorded as deferred must not read
|
||||
// as shipped.
|
||||
const checkboxRe = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
|
||||
const beforeCheckbox = reqContent;
|
||||
reqContent = reqContent.replace(checkboxRe, '$1x$2');
|
||||
const checkboxFlipped = reqContent !== beforeCheckbox;
|
||||
|
||||
// Traceability row: | <REQ-ID> | Phase N | Pending|In Progress | ->
|
||||
// ... Complete | via the markdown-table seam (ADR-2143 §7). Match the
|
||||
@@ -2262,15 +2269,33 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
// requirement's write. The "only flip Pending/In Progress ->
|
||||
// Complete" gate is folded into the newValue callback so one
|
||||
// updateTableCell call both probes and writes.
|
||||
const reqUpdate = updateTraceabilityCell(reqContent, reqRowMatch, 'Status', (current) =>
|
||||
// #2945: track tableHit (did the callback actually CHANGE the value?) so the
|
||||
// checkbox rollback below can distinguish "row existed and accepted" from
|
||||
// "row existed and rejected".
|
||||
let tableHit = false;
|
||||
const reqUpdate = updateTraceabilityCell(reqContent, reqRowMatch, 'Status', (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 (/^(?:pending|in progress|gaps found)$/i.test(current.trim())) {
|
||||
tableHit = true;
|
||||
return ' Complete ';
|
||||
}
|
||||
return current;
|
||||
});
|
||||
if (reqUpdate.ok) {
|
||||
reqContent = reqUpdate.value;
|
||||
} else if (!isPlaceholderReqId(reqId)) {
|
||||
traceabilityWriteMisses.push(reqId);
|
||||
}
|
||||
|
||||
// #2945 defect-2 (port of milestone.cts:200-210): if a row EXISTS for this
|
||||
// ID but its Status write was rejected (row reads Out/Deferred/Blocked,
|
||||
// which the callback returned unchanged), roll the checkbox back so the
|
||||
// checkbox and the row cannot silently diverge. reqUpdate.ok === a row
|
||||
// matched (existence probe); !tableHit === the callback did not advance it.
|
||||
if (checkboxFlipped && reqUpdate.ok && !tableHit) {
|
||||
reqContent = beforeCheckbox;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user