* fix(#1162): support table-format STATE.md in state field read/replace Extend stateExtractField and stateReplaceField in state-document.cts to detect and operate on pipe-table rows (| Field | value |). The separator row | --- | --- | is excluded from matching. updateCurrentPositionFields in state.cts now falls through to stateReplaceField for table-format Current Position sections when the inline Status:/Last activity: patterns do not match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1163): insert missing plan checklist rows in roadmap update-plan-progress cmdRoadmapUpdatePlanProgress now inserts `- [ ] NN-XX-PLAN.md` checkbox rows under the phase Plans: line when no per-plan checkbox rows exist yet (fresh template). Rows are sorted ascending and any already-summarised plans are immediately marked [x]. The planCountPattern is extended to also match plain `Plans:` (in addition to bold `**Plans:**`) so plan counts are updated in both template variants. The existing-rows check covers both top-level and indented checkbox forms to preserve idempotency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1163): fill partial plan-row gaps and scope insertion to active milestone - Finding 1 (HIGH): replace all-or-nothing rowsAlreadyPresent guard with per-file set-difference so missing rows are inserted even when SOME plan rows already exist - Finding 2 (MEDIUM): extend planCountPattern to recognise **Plans**: (canonical template form — bold word + outer colon) alongside **Plans:** and plain Plans:; use two-pattern fallback for row insertion to anchor under Plans: checklist header rather than the **Plans**: summary line - Finding 3 (MEDIUM): scope row insertion to the active (post-</details>) milestone region so duplicate phase headings in archived sections never receive new rows - Finding 4 (LOW): rename misleading "pipe-like content" test to honestly describe what it tests (multi-row table isolation); add out-of-scope escaped-pipe comment - Remove now-unused anyCheckboxMatched variable (lint clean) - Add 5 adversarial regression tests (pre-fix failures confirmed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1163): scope missing-plan detection to active milestone; preserve authored state fields in table format - roadmap.cts: compute activeRegion (post-</details> slice) once and use it for BOTH missingPlans detection and row insertion, so archived <details> rows no longer suppress active-section inserts (Finding 1 code-review) - roadmap.cts: change (Plans:) inner capture to non-capturing (?:Plans:) in insertRowsPatternA to prevent group-numbering shift (Finding 3) - state.cts: mirror inline-branch preserve-authored guards onto table-format branches in updateCurrentPositionFields — Status table branch checks isInList/matchesPattern before replacing; Last Activity table branch checks isDateShape/inList, preserving executor-authored narrative prose (Finding 2 code-review) - tests: add three failing-first regression tests confirming each finding Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1162,#1163): fold table-format + plan-row regressions into owning module tests Move bug-1162 cases into tests/state.test.cjs and bug-1163 cases into tests/roadmap.test.cjs under named regression describe blocks; delete the standalone bug-NNNN files and prune their allowlist entries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1156): add changeset fragment for table-format state + roadmap insert fix 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:
@@ -32,27 +32,79 @@ function existingProgressExceedsDerived(existingProgress: ProgressRecord, derive
|
||||
return existing !== null && derived !== null && existing > derived;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a pipe-table row's first cell is a separator cell (`---`
|
||||
* variants) rather than a field name. Prevents the separator row
|
||||
* `| --- | --- |` from being treated as a field named "---".
|
||||
*/
|
||||
function isTableSeparatorRow(firstCell: string): boolean {
|
||||
// A separator cell contains only dashes, colons (alignment hints), and whitespace.
|
||||
return /^[\s\-:]+$/.test(firstCell.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a regex that matches a pipe-table row `| FieldName | value |` for the
|
||||
* given (already-escaped) field name. The match is case-insensitive and
|
||||
* tolerates variable amounts of whitespace around the cell contents.
|
||||
*
|
||||
* Capture group 1: leading pipe + whitespace before the field cell
|
||||
* Capture group 2: the field name cell text (trimmed)
|
||||
* Capture group 3: whitespace between field cell and separator pipe
|
||||
* Capture group 4: the value cell text (trimmed)
|
||||
* Capture group 5: trailing whitespace + closing pipe(s)
|
||||
*
|
||||
* We use a single-line match (`m` flag so ^ anchors work on each line) to
|
||||
* avoid cross-row replacement.
|
||||
*/
|
||||
function tableRowPattern(escapedFieldName: string): RegExp {
|
||||
return new RegExp(
|
||||
`^(\\|[ \\t]*)(${escapedFieldName})([ \\t]*\\|[ \\t]*)([^|\\n]*?)([ \\t]*\\|[ \\t]*)$`,
|
||||
'im',
|
||||
);
|
||||
}
|
||||
|
||||
export function stateExtractField(content: string, fieldName: string): string | null {
|
||||
const escaped = escapeRegex(fieldName);
|
||||
// Bold inline format: **FieldName:** value
|
||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch)
|
||||
return boldMatch[1].trim();
|
||||
// Plain line-start format: FieldName: value
|
||||
const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
if (plainMatch)
|
||||
return plainMatch[1].trim();
|
||||
// Pipe-table format: | FieldName | value |
|
||||
// (Separator rows such as `| --- | --- |` are excluded.)
|
||||
const tableMatch = content.match(tableRowPattern(escaped));
|
||||
if (tableMatch && !isTableSeparatorRow(tableMatch[2]))
|
||||
return tableMatch[4].trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stateReplaceField(content: string, fieldName: string, newValue: string): string | null {
|
||||
const escaped = escapeRegex(fieldName);
|
||||
// Bold inline format: **FieldName:** value
|
||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (boldPattern.test(content)) {
|
||||
return content.replace(boldPattern, (_match, prefix: string) => `${prefix}${newValue}`);
|
||||
}
|
||||
// Plain line-start format: FieldName: value
|
||||
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
|
||||
if (plainPattern.test(content)) {
|
||||
return content.replace(plainPattern, (_match, prefix: string) => `${prefix}${newValue}`);
|
||||
}
|
||||
// Pipe-table format: | FieldName | value |
|
||||
// Preserve the surrounding pipe/whitespace structure; only swap the value cell.
|
||||
const tblPat = tableRowPattern(escaped);
|
||||
const tblMatch = content.match(tblPat);
|
||||
if (tblMatch && !isTableSeparatorRow(tblMatch[2])) {
|
||||
// Reconstruct the row, preserving the original surrounding whitespace/pipes.
|
||||
return content.replace(tblPat, (_m, leadPipe: string, fieldCell: string, midPipe: string, _oldVal: string, trailPipe: string) =>
|
||||
`${leadPipe}${fieldCell}${midPipe}${newValue}${trailPipe}`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user