* chore(#2143): markdown table model + schema registry + fail-loud pilot — Phase 1 Phase 1 of epic #2143 (ADR-2143): consolidate markdown table parsing onto a canonical seam and migrate the pilot reader. - Add src/markdown-table.cts: parseMarkdownTable (GFM tables -> typed {columns, rows} addressed by column NAME; ragged rows are typed parse errors, not silent), a single-source TABLE_SCHEMAS registry (RoadmapProgress / RequirementsTraceability / QuickTasks / Security, with variants under one id), matchTableSchema, and findTableBySchema. Result<T> is scoped to this seam (distinct from the dispatch Result). - Migrate deriveProgressFromRoadmap (src/phase-lifecycle.cts) off the position-anchored regex to name-based resolution via the seam — fixes #2137 (the 5-column milestone-grouped Progress table previously returned all-null). - Add a schema-backed `gsd-tools quick-tasks-append` subcommand and route fast.md's log_to_state through it, retiring the inline `awk NF-2` column arithmetic — fixes #2133 (addresses #2012, #2119). Cell values are escaped (| and newlines) and the STATE.md read-modify-write is atomic under readModifyWriteStateMd (lost-update race, cf. #500/#905/#1230). - Writer/reader/template parity test guards TABLE_SCHEMAS against drift (ADR-2143 §3 Generative-Fix-Divergence). Registration: .gitignore, eslint.config.mjs, docs/INVENTORY.md + INVENTORY-MANIFEST.json, CONTEXT.md glossary, docs/CLI-TOOLS.md. Behaviour-preserving for the canonical 4-column Progress table; the named bugs are driven fail-first. Extend-never-mutate (ADR-2143 §2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#2242): backfill changeset PR number (#2248) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2242): escape backslash before pipe in markdown-table cell escaping CodeQL js/incomplete-sanitization (high): escapeCell escaped | -> \| but not the backslash itself. Now escapes \ -> \\ before | -> \|, and splitTableRow unescapes both \\ -> \ and \| -> | symmetrically so cell values (incl. literal backslashes) round-trip exactly. Added backslash round-trip tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2242): read ROADMAP Progress table by column name — supersede #2168 ad-hoc scan Rebase reconciliation with #2168 (the tactical #2137 fix that marked itself "pending #2143"). deriveProgressFromRoadmap now resolves the Progress table via a new seam helper findTableWithColumns (first table whose header is a superset of Phase/Plans Complete/Status/Completed, any order, extra columns ignored) and reads cells by NAME — order/injection-invariant per ADR-2143 §3 — instead of the exact TABLE_SCHEMAS match. This satisfies #2168's column-invariance property test while staying seam-based and preserving its `## Progress` scoping (#2012/#1445). Ragged Progress tables now resolve to null (ADR-2143 fail-loud); updated the stale state.test.cjs assertion that predated the Phase-1 migration. 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:
@@ -9487,13 +9487,20 @@ const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
const { deriveProgressFromRoadmap } = require('../gsd-core/bin/lib/phase-lifecycle.cjs');
|
||||
|
||||
// ─── Scenario A: deriveProgressFromRoadmap unit test ────────────────────────
|
||||
//
|
||||
// ADR-2143 (epic #2143) migrated deriveProgressFromRoadmap from position-based
|
||||
// regexes to the markdown-table schema registry (TABLE_SCHEMAS.RoadmapProgress),
|
||||
// which resolves the Progress table by exact column-name match. These fixtures'
|
||||
// second column is renamed "Plans" -> "Plans Complete" to match the canonical
|
||||
// header (gsd-core/templates/roadmap.md) the schema now requires; the assertions
|
||||
// (999.x exclusion, Complete-row counting) are unchanged.
|
||||
|
||||
describe('bug #1445 — deriveProgressFromRoadmap excludes 999.x rows', () => {
|
||||
test('3 real phases + 1 999.x backlog row → total_phases: 3, not 4', () => {
|
||||
const roadmap = [
|
||||
'## Milestone v1.0: Test',
|
||||
'',
|
||||
'| Phase | Plans | Status | Completed |',
|
||||
'| Phase | Plans Complete | Status | Completed |',
|
||||
'| --- | --- | --- | --- |',
|
||||
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||
'| 2. Beta | 1/2 | In Progress | |',
|
||||
@@ -9518,7 +9525,7 @@ describe('bug #1445 — deriveProgressFromRoadmap excludes 999.x rows', () => {
|
||||
const roadmap = [
|
||||
'## Milestone v1.0: Test',
|
||||
'',
|
||||
'| Phase | Plans | Status | Completed |',
|
||||
'| Phase | Plans Complete | Status | Completed |',
|
||||
'| --- | --- | --- | --- |',
|
||||
'| 1. Alpha | 1/1 | Complete | ✅ |',
|
||||
'| 2. Beta | 1/1 | Complete | ✅ |',
|
||||
@@ -9542,7 +9549,7 @@ describe('bug #1445 — deriveProgressFromRoadmap excludes 999.x rows', () => {
|
||||
const roadmap = [
|
||||
'## Milestone v1.0: Test',
|
||||
'',
|
||||
'| Phase | Plans | Status | Completed |',
|
||||
'| Phase | Plans Complete | Status | Completed |',
|
||||
'| --- | --- | --- | --- |',
|
||||
'| 999.1 Future A | 0/0 | Backlog | |',
|
||||
'| 999.2 Future B | 0/0 | Backlog | |',
|
||||
@@ -9721,9 +9728,13 @@ describe('#2137 regression: deriveProgressFromRoadmap parses the milestone-group
|
||||
});
|
||||
|
||||
test('ragged rows (more/fewer cells than the header) are handled without throwing', () => {
|
||||
// The reader indexes cells positionally by header name, so an EXTRA trailing
|
||||
// column is ignored and a SHORT row simply has absent Status/Plans cells
|
||||
// (`cells[idx] ?? ''`) — neither should throw or corrupt the well-formed row.
|
||||
// (#2242 review Fix 5 / ADR-2143 §3): deriveProgressFromRoadmap now resolves
|
||||
// the Progress table via the markdown-table seam's parseMarkdownTable, which
|
||||
// is fail-loud on ragged data rows by design — "ragged rows are errors, not
|
||||
// silent" (src/markdown-table.cts) — rather than the pre-ADR-2143 reader's
|
||||
// graceful cell-count degradation this test used to assert. A ragged row
|
||||
// anywhere in the table now makes the WHOLE table unparseable, so the reader
|
||||
// falls through to its existing (null) values instead of throwing.
|
||||
const roadmap = [
|
||||
'## Progress',
|
||||
'',
|
||||
@@ -9737,13 +9748,10 @@ describe('#2137 regression: deriveProgressFromRoadmap parses the milestone-group
|
||||
assert.doesNotThrow(() => {
|
||||
result = deriveProgressFromRoadmap(roadmap);
|
||||
}, 'ragged rows must not throw');
|
||||
// Row 1: extra column ignored → counted, Complete, +2 plans.
|
||||
// Row 2: short row → counted as a phase, but Status/Plans cells are absent so
|
||||
// it is neither Complete nor plan-bearing.
|
||||
assert.deepEqual(
|
||||
result,
|
||||
{ completedPhases: 1, totalPhases: 2, totalPlans: 2 },
|
||||
`ragged rows must degrade gracefully to {1,2,2}, got ${JSON.stringify(result)}`,
|
||||
{ completedPhases: null, totalPhases: null, totalPlans: null },
|
||||
`a ragged-row table must fail loud to all-null (no throw), got ${JSON.stringify(result)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user