fix(#3255): updateTableCell scans for the table carrying the requested column (#3377)

* test(#3255): updateTableCell reaches a later table when the first lacks the column

A ## Traceability section holding a summary table (no Status) above the
requirement table (with Status) made updateTableCell bind to the first table
and return 'unknown column: Status', so requirements.mark-complete left the
row at Pending. Add a failing-first regression at the primitive.

RED — fails on next; fix follows.

* fix(#3255): updateTableCell scans for the table carrying the requested column

updateTableCell bound to the FIRST GFM table in the scoped text and returned
'unknown column' if that table lacked the column — so a ## Traceability section
holding a phase-summary table above the requirement table never reached the
requirement table, and requirements.mark-complete left the row at Pending while
reporting table_unmatched (the #2140 silent-divergence class one level deeper,
flagged but not closed by the #2245 cross-section scoping fix).

Scan candidate table headers and pick the first VALID table (delimiter +
matching column count) whose columns include the requested column. Single-table
behaviour is byte-identical (the one table carries the column). Error semantics
preserved: no table -> 'no table found'; valid table without the column ->
'unknown column'; lone malformed table keeps its specific reason. Also fixes the
derived hasRow/doneTable reasoning, which probes via the same call.

* chore(#3255): add changeset fragment

* chore(#3255): backfill changeset PR number (#3377)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-11 23:52:46 -04:00
committed by GitHub
parent ad07f76a31
commit 864f76b46f
3 changed files with 64 additions and 22 deletions

View File

@@ -326,36 +326,50 @@ export function updateTableCell(
): Result<string> {
const lines = splitLinesWithOffsets(tableText);
// #3255: pick the first VALID table whose columns include `column`. The prior
// code bound to the FIRST table of any shape and returned 'unknown column' if
// that one lacked the column — so a section holding a summary table above the
// target (e.g. ## Traceability: a phase-summary table, then the requirement
// rows) never reached the target table. Scan for the first valid table that
// carries the column; if none does but a valid table exists, still return
// 'unknown column' (single-table behaviour unchanged). Track the first
// malformation reason so a lone malformed table keeps its specific error.
let headerIdx = -1;
let columns: string[] = [];
let firstValidIdx = -1;
let firstMalformedReason: string | null = null;
const recordMalformed = (reason: string): void => {
if (firstMalformedReason === null && firstValidIdx === -1) firstMalformedReason = reason;
};
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].line.trim();
if (trimmed.startsWith('|') && trimmed.indexOf('|', 1) !== -1) {
if (!trimmed.startsWith('|') || trimmed.indexOf('|', 1) === -1) continue;
const delimiterLine = lines[i + 1]?.line;
if (delimiterLine === undefined || !delimiterLine.trim().startsWith('|')) {
recordMalformed('missing delimiter row');
continue;
}
const candidateRanges = splitTableRowRanges(lines[i].line, lines[i].start);
const candidateColumns = candidateRanges.map((r) => unescapeCellText(tableText.slice(r.start, r.end)));
const delimiterCells = splitTableRow(delimiterLine);
if (!isDelimiterRow(delimiterCells)) {
recordMalformed('missing delimiter row');
continue;
}
if (delimiterCells.length !== candidateColumns.length) {
recordMalformed('delimiter/header column count mismatch');
continue;
}
if (firstValidIdx === -1) firstValidIdx = i;
if (candidateColumns.includes(column)) {
headerIdx = i;
columns = candidateColumns;
break;
}
}
if (headerIdx === -1) {
return { ok: false, reason: 'no table found' };
}
const delimiterLine = lines[headerIdx + 1]?.line;
if (delimiterLine === undefined || !delimiterLine.trim().startsWith('|')) {
return { ok: false, reason: 'missing delimiter row' };
}
const headerRanges = splitTableRowRanges(lines[headerIdx].line, lines[headerIdx].start);
const columns = headerRanges.map((r) => unescapeCellText(tableText.slice(r.start, r.end)));
const delimiterCells = splitTableRow(delimiterLine);
if (!isDelimiterRow(delimiterCells)) {
return { ok: false, reason: 'missing delimiter row' };
}
if (delimiterCells.length !== columns.length) {
return { ok: false, reason: 'delimiter/header column count mismatch' };
}
if (!columns.includes(column)) {
return { ok: false, reason: `unknown column: ${column}` };
if (firstValidIdx !== -1) return { ok: false, reason: `unknown column: ${column}` };
return { ok: false, reason: firstMalformedReason ?? 'no table found' };
}
const targetColIdx = columns.indexOf(column);