* 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:
5
.changeset/silly-pandas-gather.md
Normal file
5
.changeset/silly-pandas-gather.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Fixed
|
||||||
|
pr: 2248
|
||||||
|
---
|
||||||
|
**`phase complete` now reads milestone-grouped ROADMAP progress tables** — progress reported 0% on projects whose Progress table carries a Milestone column, because the reader assumed a fixed column position; it now resolves progress columns by name so both flat and milestone-grouped tables work (#2137). Quick Tasks logging via `/gsd:fast` also appends schema-correct, lock-safe rows instead of guessing the column count in shell (#2133). (#2248)
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -91,6 +91,7 @@ build/
|
|||||||
/gsd-core/bin/lib/capability-consent.cjs
|
/gsd-core/bin/lib/capability-consent.cjs
|
||||||
/gsd-core/bin/lib/capability-lock.cjs
|
/gsd-core/bin/lib/capability-lock.cjs
|
||||||
/gsd-core/bin/lib/markdown-sectionizer.cjs
|
/gsd-core/bin/lib/markdown-sectionizer.cjs
|
||||||
|
/gsd-core/bin/lib/markdown-table.cjs
|
||||||
/gsd-core/bin/lib/resolution.cjs
|
/gsd-core/bin/lib/resolution.cjs
|
||||||
/gsd-core/bin/lib/research-store.cjs
|
/gsd-core/bin/lib/research-store.cjs
|
||||||
/gsd-core/bin/lib/research-provider.cjs
|
/gsd-core/bin/lib/research-provider.cjs
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ Module owning the tool's CLI I/O primitives: `output()` result emission (with la
|
|||||||
### Markdown Sectionizer
|
### Markdown Sectionizer
|
||||||
Canonical markdown-structure parsing seam (`gsd-core/bin/lib/markdown-sectionizer.cjs`, generated from `src/markdown-sectionizer.cts`). Pure functions, Node built-ins only. Exports: `stripFencedCode(content) → { text, unterminatedFence }` (CommonMark-correct state machine, CRLF-safe, signals unterminated fences); `tokenizeHeadings(content) → HeadingToken[]` (ATX headings outside fenced blocks, `{ level, text, line, offset }`); `collectSections(content, stopPredicate) → Section[]` (line-by-line section collection driven by a heading predicate); `collectSection(content, headingPredicate, { levelBounded, stripFences }) → Section | null` (single named section with level-bounded stop); `iterateBullets(sectionText) → BulletItem[]` (dash/checkbox/numbered markers with indented continuation); `extractTaggedBlocks(content, tagName) → string[]` (inner text of every `<tagName>…</tagName>` block in document order, tagName regex-escaped, caller decides fence-stripping — generalises `decisions.cts`'s bespoke extractor for T1); `replaceSection(content, section, newBody) → string` (pure character-offset splice using `Section.bodyStart`/`bodyEnd` for read-modify-write callers — eliminates T6 `state.cts`'s 7× inline `content.replace` pattern). `Section` carries `bodyStart`/`bodyEnd` offsets for `replaceSection`. ADR-1372 (epic #1372) establishes this seam and a tiered migration plan (T0–T7) to retire the 8+ ad-hoc markdown parsers and ~20 inline section-collects across `src/*.cts`. New `src/*.cts` modules must import this seam instead of hand-rolling fence strippers or heading-regex section walks (enforced by the `no-adhoc-markdown-parsing` ESLint rule landing in tier T7).
|
Canonical markdown-structure parsing seam (`gsd-core/bin/lib/markdown-sectionizer.cjs`, generated from `src/markdown-sectionizer.cts`). Pure functions, Node built-ins only. Exports: `stripFencedCode(content) → { text, unterminatedFence }` (CommonMark-correct state machine, CRLF-safe, signals unterminated fences); `tokenizeHeadings(content) → HeadingToken[]` (ATX headings outside fenced blocks, `{ level, text, line, offset }`); `collectSections(content, stopPredicate) → Section[]` (line-by-line section collection driven by a heading predicate); `collectSection(content, headingPredicate, { levelBounded, stripFences }) → Section | null` (single named section with level-bounded stop); `iterateBullets(sectionText) → BulletItem[]` (dash/checkbox/numbered markers with indented continuation); `extractTaggedBlocks(content, tagName) → string[]` (inner text of every `<tagName>…</tagName>` block in document order, tagName regex-escaped, caller decides fence-stripping — generalises `decisions.cts`'s bespoke extractor for T1); `replaceSection(content, section, newBody) → string` (pure character-offset splice using `Section.bodyStart`/`bodyEnd` for read-modify-write callers — eliminates T6 `state.cts`'s 7× inline `content.replace` pattern). `Section` carries `bodyStart`/`bodyEnd` offsets for `replaceSection`. ADR-1372 (epic #1372) establishes this seam and a tiered migration plan (T0–T7) to retire the 8+ ad-hoc markdown parsers and ~20 inline section-collects across `src/*.cts`. New `src/*.cts` modules must import this seam instead of hand-rolling fence strippers or heading-regex section walks (enforced by the `no-adhoc-markdown-parsing` ESLint rule landing in tier T7).
|
||||||
|
|
||||||
|
### Markdown Table Model
|
||||||
|
Canonical GFM table parsing + schema registry seam (`gsd-core/bin/lib/markdown-table.cjs`, generated from `src/markdown-table.cts`; ADR-2143, epic #2143). Pure functions, Node built-ins only, string-in/value-out, no I/O. Exports: `parseMarkdownTable(sectionText) → Result<MarkdownTable>` (parses the first GFM pipe table found; typed `{ok:false,reason}` parse errors for no-table, missing/misaligned delimiter row, and ragged data rows — never silently drops or coerces a malformed row); `MarkdownTable` (`{columns: string[], rows: Record<string,string>[]}`, rows addressed by column name, not position); `Result<T>` (`{ok:true,value}\|{ok:false,reason}` — deliberately distinct from command-routing-hub's dispatch `Result` `{ok,data\|kind}`; the two never mix); `TABLE_SCHEMAS` (`Record<string, CanonicalTableVariant[]>` — the canonical column-header variants for every GFM table GSD parses or generates: `RoadmapProgress` flat/milestone-grouped, `RequirementsTraceability`, `QuickTasks` no-status/with-status, `Security` trust-boundaries/threat-register/accepted-risks/audit-trail); `matchTableSchema(columns) → {id,label}\|null` (resolves a parsed header back to its canonical schema by exact column-name/order match). This registry is the single source of truth for ROADMAP/STATE/SECURITY canonical tables — a parity test (`tests/markdown-table.test.cjs`) asserts every variant's header appears verbatim in the template/workflow file that generates it, so the registry and templates can never silently drift (ADR-2143 §3 Generative-Fix-Divergence guard). `phase-lifecycle.cts`'s `deriveProgressFromRoadmap` is the first consumer: it locates the Progress section via the Markdown Sectionizer's `collectSection` and reads cells by column NAME through this seam, fixing #2137 (the prior position-anchored regex assumed `Status` was always the 3rd cell, which broke for the 5-column milestone-grouped `Milestone` variant).
|
||||||
|
|
||||||
### Roadmap Parser Module
|
### Roadmap Parser Module
|
||||||
Module owning ROADMAP.md parsing: shipped-milestone slicing, current-milestone extraction, milestone/phase lookups, and milestone-phase filtering (`stripShippedMilestones`, `extractCurrentMilestone`, `replaceInCurrentMilestone`, `getRoadmapPhaseInternal`, `getMilestoneInfo`, `getMilestonePhaseFilter`). Depends only on leaf modules (`phase-id`, `planning-workspace`, `shell-command-projection`) — no `loadConfig`, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2b (#870), resolving the ROADMAP.md parse/write straddle so the Roadmap module (`roadmap.cjs`, which owns ROADMAP.md mutation) imports parsing directly instead of through Core; the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/roadmap-parser.cjs` (generated from `src/roadmap-parser.cts`).
|
Module owning ROADMAP.md parsing: shipped-milestone slicing, current-milestone extraction, milestone/phase lookups, and milestone-phase filtering (`stripShippedMilestones`, `extractCurrentMilestone`, `replaceInCurrentMilestone`, `getRoadmapPhaseInternal`, `getMilestoneInfo`, `getMilestonePhaseFilter`). Depends only on leaf modules (`phase-id`, `planning-workspace`, `shell-command-projection`) — no `loadConfig`, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2b (#870), resolving the ROADMAP.md parse/write straddle so the Roadmap module (`roadmap.cjs`, which owns ROADMAP.md mutation) imports parsing directly instead of through Core; the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/roadmap-parser.cjs` (generated from `src/roadmap-parser.cts`).
|
||||||
|
|
||||||
|
|||||||
@@ -531,6 +531,9 @@ node gsd-tools.cjs list-seeds [status]
|
|||||||
# Check file/directory existence
|
# Check file/directory existence
|
||||||
node gsd-tools.cjs verify-path-exists <path>
|
node gsd-tools.cjs verify-path-exists <path>
|
||||||
|
|
||||||
|
# Append a row to STATE.md's "Quick Tasks Completed" table (schema-backed; #2133)
|
||||||
|
node gsd-tools.cjs quick-tasks-append --task "<description>"
|
||||||
|
|
||||||
# Aggregate all SUMMARY.md data
|
# Aggregate all SUMMARY.md data
|
||||||
node gsd-tools.cjs history-digest
|
node gsd-tools.cjs history-digest
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"loop-host-contract.cjs",
|
"loop-host-contract.cjs",
|
||||||
"loop-resolver.cjs",
|
"loop-resolver.cjs",
|
||||||
"markdown-sectionizer.cjs",
|
"markdown-sectionizer.cjs",
|
||||||
|
"markdown-table.cjs",
|
||||||
"mcp-server.cjs",
|
"mcp-server.cjs",
|
||||||
"milestone.cjs",
|
"milestone.cjs",
|
||||||
"model-adapter.cjs",
|
"model-adapter.cjs",
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
|||||||
| `loop-host-contract.cjs` | Generated Loop Host Contract — 12 loop points, per-step agent roles, and core artifacts for the five-step pipeline (discuss/plan/execute/verify/ship); emitted by `scripts/gen-loop-host-contract.cjs --write` (ADR-894 §3); consumed by `gen-capability-registry.cjs` |
|
| `loop-host-contract.cjs` | Generated Loop Host Contract — 12 loop points, per-step agent roles, and core artifacts for the five-step pipeline (discuss/plan/execute/verify/ship); emitted by `scripts/gen-loop-host-contract.cjs --write` (ADR-894 §3); consumed by `gen-capability-registry.cjs` |
|
||||||
| `loop-resolver.cjs` | Loop Extension Point resolver — ADR-857 phase 3c/6 registry-consuming query; given a canonical loop point, filters `byLoopPoint` by resolved Capability State plus config activation (`when` key traversal with prototype-pollution guard), returns `{ point, activeHooks, rendered }` envelope; `resolveLoopHooks` and `renderLoopHooks` are pure (no I/O); command surface: `gsd-tools loop render-hooks <point> [--config-dir <path>]` |
|
| `loop-resolver.cjs` | Loop Extension Point resolver — ADR-857 phase 3c/6 registry-consuming query; given a canonical loop point, filters `byLoopPoint` by resolved Capability State plus config activation (`when` key traversal with prototype-pollution guard), returns `{ point, activeHooks, rendered }` envelope; `resolveLoopHooks` and `renderLoopHooks` are pure (no I/O); command surface: `gsd-tools loop render-hooks <point> [--config-dir <path>]` |
|
||||||
| `markdown-sectionizer.cjs` | Canonical markdown-structure parsing seam (ADR-1372, epic #1372) — pure, Node built-ins only; exports `stripFencedCode` (CommonMark-correct fence stripper, CRLF-safe), `tokenizeHeadings` (ATX headings outside fenced blocks), `collectSections`/`collectSection` (line-by-line section collection with `bodyStart`/`bodyEnd` offsets), `iterateBullets` (dash/checkbox/numbered markers), `extractTaggedBlocks` (inner text of `<tag>…</tag>` blocks, caller decides fence-stripping), and `replaceSection` (pure character-offset body splice for read-modify-write callers); foundation for T0–T7 migration tiers retiring 8+ ad-hoc parsers |
|
| `markdown-sectionizer.cjs` | Canonical markdown-structure parsing seam (ADR-1372, epic #1372) — pure, Node built-ins only; exports `stripFencedCode` (CommonMark-correct fence stripper, CRLF-safe), `tokenizeHeadings` (ATX headings outside fenced blocks), `collectSections`/`collectSection` (line-by-line section collection with `bodyStart`/`bodyEnd` offsets), `iterateBullets` (dash/checkbox/numbered markers), `extractTaggedBlocks` (inner text of `<tag>…</tag>` blocks, caller decides fence-stripping), and `replaceSection` (pure character-offset body splice for read-modify-write callers); foundation for T0–T7 migration tiers retiring 8+ ad-hoc parsers |
|
||||||
|
| `markdown-table.cjs` | Canonical GFM table model + `TABLE_SCHEMAS` registry seam (ADR-2143, epic #2143) — pure, Node built-ins only; exports `parseMarkdownTable(sectionText) → Result<MarkdownTable>` (parses the first GFM pipe table, typed parse errors for ragged/malformed rows rather than silent coercion), `MarkdownTable` (`{columns, rows}`, rows addressed by column name), `Result<T>` (`{ok:true,value}\|{ok:false,reason}` — distinct from command-routing-hub's dispatch `Result`), `TABLE_SCHEMAS` (canonical column-header variants for `RoadmapProgress`/`RequirementsTraceability`/`QuickTasks`/`Security` tables), and `matchTableSchema(columns) → {id,label}\|null` (resolves parsed headers back to a canonical schema); consumed by `phase-lifecycle.cts`'s `deriveProgressFromRoadmap` (fixes #2137, the 5-column milestone-grouped Progress table) |
|
||||||
| `milestone.cjs` | Milestone archival, requirements marking |
|
| `milestone.cjs` | Milestone archival, requirements marking |
|
||||||
| `model-catalog.cjs` | CJS adapter over the shared model catalog JSON; exports canonical runtime tier defaults, agent profile maps, alias maps, and routing metadata for all CLI consumers |
|
| `model-catalog.cjs` | CJS adapter over the shared model catalog JSON; exports canonical runtime tier defaults, agent profile maps, alias maps, and routing metadata for all CLI consumers |
|
||||||
| `model-profiles.cjs` | Backward-compatible profile helpers derived from `model-catalog.cjs`; no longer owns its own model table |
|
| `model-profiles.cjs` | Backward-compatible profile helpers derived from `model-catalog.cjs`; no longer owns its own model table |
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ export default tseslint.config(
|
|||||||
'gsd-core/bin/lib/teams-status.cjs',
|
'gsd-core/bin/lib/teams-status.cjs',
|
||||||
// ADR-1372: tsc-generated runtime artifact — lint the src/markdown-sectionizer.cts source.
|
// ADR-1372: tsc-generated runtime artifact — lint the src/markdown-sectionizer.cts source.
|
||||||
'gsd-core/bin/lib/markdown-sectionizer.cjs',
|
'gsd-core/bin/lib/markdown-sectionizer.cjs',
|
||||||
|
// ADR-2143: tsc-generated runtime artifact — lint the src/markdown-table.cts source.
|
||||||
|
'gsd-core/bin/lib/markdown-table.cjs',
|
||||||
// ADR-1239 Phase C-1 (#1680): tsc-generated — lint src/embedding-adapter.cts + src/adapter-declarative.cts.
|
// ADR-1239 Phase C-1 (#1680): tsc-generated — lint src/embedding-adapter.cts + src/adapter-declarative.cts.
|
||||||
'gsd-core/bin/lib/embedding-adapter.cjs',
|
'gsd-core/bin/lib/embedding-adapter.cjs',
|
||||||
'gsd-core/bin/lib/adapter-declarative.cjs',
|
'gsd-core/bin/lib/adapter-declarative.cjs',
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
* list-todos [area] Count and enumerate pending todos
|
* list-todos [area] Count and enumerate pending todos
|
||||||
* list-seeds [status] List captured seeds (optional status filter)
|
* list-seeds [status] List captured seeds (optional status filter)
|
||||||
* verify-path-exists <path> Check file/directory existence
|
* verify-path-exists <path> Check file/directory existence
|
||||||
|
* quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
|
||||||
|
* Completed" table (schema-backed via
|
||||||
|
* markdown-table.cjs; #2133/ADR-2143).
|
||||||
|
* Fails loud (non-zero exit) on a missing
|
||||||
|
* or unrecognized table instead of the old
|
||||||
|
* awk NF-2 silent-skip guess.
|
||||||
* config-ensure-section Initialize .planning/config.json
|
* config-ensure-section Initialize .planning/config.json
|
||||||
* history-digest Aggregate all SUMMARY.md data
|
* history-digest Aggregate all SUMMARY.md data
|
||||||
* summary-extract <path> [--fields] Extract structured data from SUMMARY.md
|
* summary-extract <path> [--fields] Extract structured data from SUMMARY.md
|
||||||
@@ -693,7 +699,7 @@ async function main() {
|
|||||||
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
||||||
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
||||||
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
||||||
'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
|
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
|
||||||
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
||||||
'Global flags:\n' +
|
'Global flags:\n' +
|
||||||
' --raw Emit raw output without post-processing\n' +
|
' --raw Emit raw output without post-processing\n' +
|
||||||
@@ -1213,6 +1219,59 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'quick-tasks-append': {
|
||||||
|
// #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
|
||||||
|
// `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
|
||||||
|
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
||||||
|
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
||||||
|
const qtaArgs = args.slice(1);
|
||||||
|
const qtaTask = parseNamedArgs(qtaArgs, ['task']).task || args[1];
|
||||||
|
if (!qtaTask) {
|
||||||
|
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||||
|
if (!fs.existsSync(statePath)) {
|
||||||
|
error(`quick-tasks-append: STATE.md not found at ${statePath}`, ERROR_REASON.USAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
const { execGit } = require('./lib/shell-command-projection.cjs');
|
||||||
|
const hashResult = execGit(['rev-parse', '--short', 'HEAD'], { cwd });
|
||||||
|
const commit = hashResult.exitCode === 0 && hashResult.stdout ? hashResult.stdout : '—';
|
||||||
|
|
||||||
|
const { appendQuickTaskRow } = require('./lib/markdown-table.cjs');
|
||||||
|
|
||||||
|
// #2242 review fix: route the read -> mutate -> write cycle through
|
||||||
|
// state.readModifyWriteStateMd (lib/state.cjs) instead of a raw
|
||||||
|
// fs.readFileSync + fs.writeFileSync pair, so the whole read-modify-write
|
||||||
|
// is atomic under STATE.md's lockfile — closing the lost-update race a
|
||||||
|
// raw read/write pair left open (cf. #500/#905/#1230). This mirrors the
|
||||||
|
// pattern every other STATE.md-mutating case in state.cts uses (e.g.
|
||||||
|
// cmdStateAddBlocker, cmdStateAddDecision): a mutable outer variable
|
||||||
|
// captures the pure helper's side output, and a fail-loud reason throws
|
||||||
|
// ExitError from INSIDE the transform (readModifyWriteStateMd's finally
|
||||||
|
// still releases the lock before the throw propagates; the transform
|
||||||
|
// throws before returning new content, so nothing is ever written).
|
||||||
|
let mutation;
|
||||||
|
state.readModifyWriteStateMd(statePath, (content) => {
|
||||||
|
const result = appendQuickTaskRow(content, { description: qtaTask, date, commit });
|
||||||
|
if (!result.ok) {
|
||||||
|
// Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
|
||||||
|
// is an expected, recoverable condition (no table / unrecognized
|
||||||
|
// schema), not a hard crash. ExitError sets a non-zero exit code (so
|
||||||
|
// fast.md's `|| echo ...` fallback fires) without calling
|
||||||
|
// process.exit() directly — stdout stays flushed and untouched.
|
||||||
|
throw new ExitError(1, `⚠ quick-tasks-append: ${result.reason}`);
|
||||||
|
}
|
||||||
|
mutation = result.value;
|
||||||
|
return result.value.content;
|
||||||
|
}, cwd);
|
||||||
|
|
||||||
|
output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'config-ensure-section': {
|
case 'config-ensure-section': {
|
||||||
// Phase 6 (#3575): dispatch via SDK executeForCjs. The catalog rebinds
|
// Phase 6 (#3575): dispatch via SDK executeForCjs. The catalog rebinds
|
||||||
// 'config-ensure-section' to configNewProject in
|
// 'config-ensure-section' to configNewProject in
|
||||||
|
|||||||
@@ -64,40 +64,18 @@ Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:`
|
|||||||
|
|
||||||
<step name="log_to_state">
|
<step name="log_to_state">
|
||||||
If `.planning/STATE.md` exists and has a "Quick Tasks Completed" table, append a row
|
If `.planning/STATE.md` exists and has a "Quick Tasks Completed" table, append a row
|
||||||
that matches the existing table's schema. If no table exists, skip silently.
|
that matches the existing table's schema via the schema-backed `gsd-tools
|
||||||
If the table's schema is unrecognized, skip with a brief log rather than append a
|
quick-tasks-append` helper (`markdown-table.cjs`'s `appendQuickTaskRow`; #2133,
|
||||||
malformed row.
|
ADR-2143 §3/§7). If no table exists, skip silently. If the table's schema is
|
||||||
|
unrecognized, the helper fails loud (non-zero exit) instead of silently guessing
|
||||||
|
a column count — this replaces the prior inline `awk NF-2` arithmetic that was
|
||||||
|
the root cause of #2133.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
||||||
# Detect whether STATE.md has a Quick Tasks Completed table
|
# Detect whether STATE.md has a Quick Tasks Completed table
|
||||||
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
|
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
|
||||||
# Read the table header line to determine the column schema.
|
gsd_run quick-tasks-append --task "$TASK" || echo "⚠ fast.md log_to_state: could not append Quick Tasks row (see message above); continuing."
|
||||||
# quick.md Step 7b writes two shapes:
|
|
||||||
# 5-column (non-validate): | # | Description | Date | Commit | Directory |
|
|
||||||
# 6-column (validate): | # | Description | Date | Commit | Status | Directory |
|
|
||||||
HEADER_LINE=$(grep -A2 "Quick Tasks Completed" .planning/STATE.md 2>/dev/null | grep "^|" | head -1)
|
|
||||||
# Count REAL columns: a markdown header has a leading and a trailing pipe, so
|
|
||||||
# awk's NF counts (real columns + 2). NF-2 yields the real column count.
|
|
||||||
# (NF-1 was the off-by-one root cause of #2133: it returned the pipe count,
|
|
||||||
# making the `-eq 5` test unsatisfiable for the very header quick.md writes.)
|
|
||||||
COL_COUNT=$(echo "$HEADER_LINE" | awk -F'|' '{print NF-2}')
|
|
||||||
|
|
||||||
# Next row number + latest commit hash are schema-independent.
|
|
||||||
NEXT_NUM=$(awk '/Quick Tasks Completed/{found=1} found && /^\|/ && !/^[|][-: |]*[|]$/ && !/Description/{count++} END{print count+1}' .planning/STATE.md 2>/dev/null || echo "1")
|
|
||||||
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "—")
|
|
||||||
|
|
||||||
# Select the appended row's template by the detected column count so its cell
|
|
||||||
# count always matches the header (prevents the malformed-row symptom of #27).
|
|
||||||
if [ "$COL_COUNT" -eq 5 ] && echo "$HEADER_LINE" | grep -qi "Description" && echo "$HEADER_LINE" | grep -qi "Commit" && echo "$HEADER_LINE" | grep -qi "Directory"; then
|
|
||||||
# 5-column schema from quick.md Step 7b (non-validate).
|
|
||||||
echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — |" >> .planning/STATE.md
|
|
||||||
elif [ "$COL_COUNT" -eq 6 ] && echo "$HEADER_LINE" | grep -qi "Status" && echo "$HEADER_LINE" | grep -qi "Directory"; then
|
|
||||||
# 6-column schema from quick.md Step 7b (validate, with Status).
|
|
||||||
echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — | — |" >> .planning/STATE.md
|
|
||||||
else
|
|
||||||
# Unrecognized table schema — skip to avoid appending a malformed row (#27).
|
|
||||||
echo "⚠ fast.md log_to_state: Quick Tasks Completed table has unrecognized schema (${COL_COUNT} columns); skipping STATE.md update."
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
</step>
|
</step>
|
||||||
|
|||||||
@@ -971,6 +971,8 @@ Use `date` from init:
|
|||||||
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
|
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a schema-safe append outside this workflow (e.g. from fast.md), `gsd-tools quick-tasks-append --task <text>` performs the equivalent write via the shared, schema-backed `appendQuickTaskRow` helper (#2133, ADR-2143 §3/§7).
|
||||||
|
|
||||||
**7d. Update "Last activity" line:**
|
**7d. Update "Last activity" line:**
|
||||||
|
|
||||||
Use `date` from init:
|
Use `date` from init:
|
||||||
|
|||||||
@@ -70,16 +70,12 @@ function cleanup(root) {
|
|||||||
const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
|
const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
|
||||||
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
||||||
|
|
||||||
// Track the claude root so we can reuse it for the local layout fixture below.
|
|
||||||
let claudeRoot = null;
|
|
||||||
|
|
||||||
for (const runtime of targets) {
|
for (const runtime of targets) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
|
if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
|
||||||
process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
|
process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
|
const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
|
||||||
if (runtime === 'claude') claudeRoot = root;
|
|
||||||
let actual;
|
let actual;
|
||||||
try {
|
try {
|
||||||
actual = buildParityManifest(configDir, root);
|
actual = buildParityManifest(configDir, root);
|
||||||
|
|||||||
367
src/markdown-table.cts
Normal file
367
src/markdown-table.cts
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
/**
|
||||||
|
* Markdown Table Model — canonical GFM table parsing + schema registry seam
|
||||||
|
* (ADR-2143, epic #2143). Pure functions, Node built-ins only, string-in/value-out,
|
||||||
|
* no I/O. Compiled by tsc to gsd-core/bin/lib/markdown-table.cjs.
|
||||||
|
*
|
||||||
|
* NOTE: the `Result<T>` here is the ADR-2143 parse-result shape {ok,value|reason} —
|
||||||
|
* deliberately distinct from command-routing-hub's dispatch `Result` {ok,data|kind};
|
||||||
|
* the two never mix (different modules).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { collectSection, replaceSection } from './markdown-sectionizer.cjs';
|
||||||
|
|
||||||
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type Result<T> = { ok: true; value: T } | { ok: false; reason: string };
|
||||||
|
|
||||||
|
/** A parsed GFM pipe table: header column names + rows addressed by column name. */
|
||||||
|
export interface MarkdownTable {
|
||||||
|
columns: string[];
|
||||||
|
rows: Record<string, string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One recognised header-shape variant of a canonical table kind. */
|
||||||
|
export interface CanonicalTableVariant {
|
||||||
|
label: string;
|
||||||
|
columns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Schema registry ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical column-header shapes for every GFM table GSD parses or generates.
|
||||||
|
* Each entry in `TABLE_SCHEMAS[id]` is one accepted variant (exact column names,
|
||||||
|
* in order); `matchTableSchema` resolves a parsed header back to `{id, label}`.
|
||||||
|
*
|
||||||
|
* This registry is the single source of truth — a parity test
|
||||||
|
* (tests/markdown-table.test.cjs) asserts every variant's header appears
|
||||||
|
* verbatim in the template/workflow file that generates it, so the registry
|
||||||
|
* and the templates can never silently drift (ADR-2143 §3 Generative-Fix-
|
||||||
|
* Divergence guard).
|
||||||
|
*/
|
||||||
|
export const TABLE_SCHEMAS: Record<string, CanonicalTableVariant[]> = {
|
||||||
|
RoadmapProgress: [
|
||||||
|
{ label: 'flat', columns: ['Phase', 'Plans Complete', 'Status', 'Completed'] },
|
||||||
|
{
|
||||||
|
label: 'milestone-grouped',
|
||||||
|
columns: ['Phase', 'Milestone', 'Plans Complete', 'Status', 'Completed'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
RequirementsTraceability: [
|
||||||
|
{ label: 'default', columns: ['Requirement', 'Phase', 'Status'] },
|
||||||
|
],
|
||||||
|
QuickTasks: [
|
||||||
|
{ label: 'no-status', columns: ['#', 'Description', 'Date', 'Commit', 'Directory'] },
|
||||||
|
{
|
||||||
|
label: 'with-status',
|
||||||
|
columns: ['#', 'Description', 'Date', 'Commit', 'Status', 'Directory'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Security: [
|
||||||
|
{ label: 'trust-boundaries', columns: ['Boundary', 'Description', 'Data Crossing'] },
|
||||||
|
{
|
||||||
|
label: 'threat-register',
|
||||||
|
columns: [
|
||||||
|
'Threat ID',
|
||||||
|
'Category',
|
||||||
|
'Component',
|
||||||
|
'Severity',
|
||||||
|
'Disposition',
|
||||||
|
'Mitigation',
|
||||||
|
'Status',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'accepted-risks',
|
||||||
|
columns: ['Risk ID', 'Threat Ref', 'Rationale', 'Accepted By', 'Date'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'audit-trail',
|
||||||
|
columns: ['Audit Date', 'Threats Total', 'Closed', 'Open', 'Run By'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a parsed table's header columns to the canonical schema it matches
|
||||||
|
* (exact column names, same length, same order), else `null`.
|
||||||
|
*/
|
||||||
|
export function matchTableSchema(columns: string[]): { id: string; label: string } | null {
|
||||||
|
for (const [id, variants] of Object.entries(TABLE_SCHEMAS)) {
|
||||||
|
for (const variant of variants) {
|
||||||
|
if (
|
||||||
|
variant.columns.length === columns.length
|
||||||
|
&& variant.columns.every((col, idx) => col === columns[idx])
|
||||||
|
) {
|
||||||
|
return { id, label: variant.label };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Parsing ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split one GFM table row line into trimmed cell strings.
|
||||||
|
* Strips one leading and one trailing `|`, splits on unescaped `|`, trims
|
||||||
|
* each cell, and unescapes `\\` back to `\` and `\|` back to `|` (the exact
|
||||||
|
* reverse of `escapeCell`'s `\`->`\\` then `|`->`\|` order below), so cell
|
||||||
|
* values round-trip exactly — including literal backslashes.
|
||||||
|
*/
|
||||||
|
function splitTableRow(line: string): string[] {
|
||||||
|
let stripped = line.trim();
|
||||||
|
if (stripped.startsWith('|')) stripped = stripped.slice(1);
|
||||||
|
if (stripped.endsWith('|')) stripped = stripped.slice(0, -1);
|
||||||
|
return stripped.split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\([\\|])/g, '$1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when every delimiter cell matches GFM's `:?-{1,}:?` shape (spaces removed). */
|
||||||
|
function isDelimiterRow(cells: string[]): boolean {
|
||||||
|
return cells.every((cell) => /^:?-{1,}:?$/.test(cell.replace(/\s+/g, '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the FIRST GFM pipe table found in `sectionText`.
|
||||||
|
*
|
||||||
|
* Defensive by design: never throws — every malformed shape (no table,
|
||||||
|
* missing/misaligned delimiter row, ragged data row) returns a typed
|
||||||
|
* `{ok:false, reason}` instead of silently coercing or dropping data
|
||||||
|
* (ADR-2143 §3 — ragged rows are errors, not silent).
|
||||||
|
*
|
||||||
|
* Scope note: GSD planning tables (STATE.md/ROADMAP.md/requirements.md/
|
||||||
|
* SECURITY.md) are always fully-piped (leading + trailing `|` on every row)
|
||||||
|
* and non-indented — this parser targets THAT shape, not arbitrary
|
||||||
|
* CommonMark (which also allows non-piped rows and up to 3 leading spaces).
|
||||||
|
*/
|
||||||
|
export function parseMarkdownTable(sectionText: string): Result<MarkdownTable> {
|
||||||
|
if (typeof sectionText !== 'string' || sectionText.trim() === '') {
|
||||||
|
return { ok: false, reason: 'empty or non-string input' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = sectionText.split(/\r?\n/);
|
||||||
|
|
||||||
|
let headerIdx = -1;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const trimmed = lines[i].trim();
|
||||||
|
if (trimmed.startsWith('|') && trimmed.indexOf('|', 1) !== -1) {
|
||||||
|
headerIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (headerIdx === -1) {
|
||||||
|
return { ok: false, reason: 'no table found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = splitTableRow(lines[headerIdx]);
|
||||||
|
|
||||||
|
const delimiterLine = lines[headerIdx + 1];
|
||||||
|
if (delimiterLine === undefined || !delimiterLine.trim().startsWith('|')) {
|
||||||
|
return { ok: false, reason: 'missing delimiter row' };
|
||||||
|
}
|
||||||
|
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' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: Record<string, string>[] = [];
|
||||||
|
let rowNum = 0;
|
||||||
|
for (let i = headerIdx + 2; i < lines.length; i++) {
|
||||||
|
const trimmed = lines[i].trim();
|
||||||
|
if (!trimmed.startsWith('|')) break;
|
||||||
|
|
||||||
|
rowNum += 1;
|
||||||
|
const cells = splitTableRow(lines[i]);
|
||||||
|
if (cells.length !== columns.length) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: `row ${rowNum} has ${cells.length} cells, expected ${columns.length}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const row: Record<string, string> = {};
|
||||||
|
columns.forEach((col, idx) => {
|
||||||
|
row[col] = cells[idx];
|
||||||
|
});
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, value: { columns, rows } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first table in `text` whose header matches `TABLE_SCHEMAS[schemaId]`,
|
||||||
|
* scanning the WHOLE document (not just a named section). Returns `null` when
|
||||||
|
* no table with that schema is found.
|
||||||
|
*
|
||||||
|
* Fixes the regression where callers first located a named heading (e.g.
|
||||||
|
* `## Progress`) via `collectSection` and only then parsed a table inside it —
|
||||||
|
* a schema-matching table that lives under a differently-named heading (or no
|
||||||
|
* heading at all), or that isn't the first table in the document, was
|
||||||
|
* invisible to that approach. Scanning the whole document by schema restores
|
||||||
|
* the old "find the progress table anywhere" behaviour while staying
|
||||||
|
* seam-based (ADR-2143).
|
||||||
|
*/
|
||||||
|
export function findTableBySchema(text: string, schemaId: string): MarkdownTable | null {
|
||||||
|
if (typeof text !== 'string') return null;
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const t = lines[i].trim();
|
||||||
|
if (!t.startsWith('|') || t.indexOf('|', 1) === -1) continue;
|
||||||
|
const cols = splitTableRow(lines[i]);
|
||||||
|
const m = matchTableSchema(cols);
|
||||||
|
if (m && m.id === schemaId) {
|
||||||
|
const parsed = parseMarkdownTable(lines.slice(i).join('\n'));
|
||||||
|
if (parsed.ok) return parsed.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first GFM table in `text` whose header contains ALL of `required`
|
||||||
|
* column names (order-independent; extra/injected columns allowed). Returns
|
||||||
|
* the parsed `MarkdownTable`, or `null` when no table's header is a superset
|
||||||
|
* of `required`.
|
||||||
|
*
|
||||||
|
* Column-NAME/order/count-invariant counterpart to `findTableBySchema` (ADR-2143
|
||||||
|
* §3 "addressed by NAME, never ordinal"): where `findTableBySchema` requires an
|
||||||
|
* EXACT canonical column set+order registered in `TABLE_SCHEMAS`, this scans
|
||||||
|
* for any header that names the required columns, in any order, tolerating
|
||||||
|
* extra/unrelated injected columns. Cells remain addressable by column NAME
|
||||||
|
* via the returned `MarkdownTable`.
|
||||||
|
*/
|
||||||
|
export function findTableWithColumns(text: string, required: string[]): MarkdownTable | null {
|
||||||
|
if (typeof text !== 'string') return null;
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const t = lines[i].trim();
|
||||||
|
if (!t.startsWith('|') || t.indexOf('|', 1) === -1) continue;
|
||||||
|
const cols = splitTableRow(lines[i]);
|
||||||
|
if (required.every((rq) => cols.includes(rq))) {
|
||||||
|
const parsed = parseMarkdownTable(lines.slice(i).join('\n'));
|
||||||
|
if (parsed.ok) return parsed.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Quick Tasks row append (#2133) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape one dynamic cell value for insertion into a GFM pipe-table row.
|
||||||
|
*
|
||||||
|
* Escapes `\` -> `\\` FIRST, then `|` -> `\|` (in that order, so a literal
|
||||||
|
* backslash already in the value is never mistaken for part of an escape
|
||||||
|
* sequence introduced by this function — CodeQL js/incomplete-sanitization).
|
||||||
|
* `splitTableRow` reverses both in the opposite order (`\\` -> `\` then
|
||||||
|
* `\|` -> `|`, see line ~114 above), so escaping/unescaping round-trips
|
||||||
|
* exactly, including literal backslashes. Newlines are collapsed to a
|
||||||
|
* single space — a raw `|` or embedded newline in a cell value (e.g. a task
|
||||||
|
* `description`) would otherwise corrupt the table (extra column / a fake
|
||||||
|
* extra row) and get rejected by the now-fail-loud `parseMarkdownTable` as a
|
||||||
|
* ragged row.
|
||||||
|
*/
|
||||||
|
function escapeCell(value: string): string {
|
||||||
|
return String(value)
|
||||||
|
.replace(/\r?\n+/g, ' ')
|
||||||
|
.replace(/\\/g, '\\\\') // escape the escape char FIRST (CodeQL js/incomplete-sanitization)
|
||||||
|
.replace(/\|/g, '\\|')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fields needed to render one "Quick Tasks Completed" row (schema-driven). */
|
||||||
|
export interface QuickTaskFields {
|
||||||
|
description: string;
|
||||||
|
date: string;
|
||||||
|
commit: string;
|
||||||
|
status?: string;
|
||||||
|
directory?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append one row to STATE.md's "Quick Tasks Completed" table.
|
||||||
|
*
|
||||||
|
* Pure, schema-driven replacement for fast.md's inline `awk NF-2` column-count
|
||||||
|
* guess (#2133, ADR-2143 §3 schema registry / §7 fail-loud unrecognized-schema
|
||||||
|
* guard). Never touches disk, git, or the clock — callers (the `gsd-tools
|
||||||
|
* quick-tasks-append` subcommand) compute `date`/`commit` and pass them in.
|
||||||
|
*
|
||||||
|
* Fails loud (`{ok:false, reason}`, never a silent skip) when:
|
||||||
|
* - no "Quick Tasks Completed" heading exists in `stateContent`
|
||||||
|
* - the section's body doesn't parse as a GFM table (parseMarkdownTable failure)
|
||||||
|
* - the table's header doesn't match a known `TABLE_SCHEMAS.QuickTasks` variant
|
||||||
|
* (the old awk arithmetic silently skipped here instead — that silent-skip
|
||||||
|
* branch is the bug this replaces).
|
||||||
|
*
|
||||||
|
* The new row is inserted immediately after the LAST existing table row line
|
||||||
|
* (or immediately after the header/delimiter when the table has zero data
|
||||||
|
* rows), preserving any surrounding blank lines/trailing content in the section.
|
||||||
|
*/
|
||||||
|
export function appendQuickTaskRow(
|
||||||
|
stateContent: string,
|
||||||
|
fields: QuickTaskFields,
|
||||||
|
): Result<{ content: string; row: string; variant: string }> {
|
||||||
|
const section = collectSection(stateContent, (h) => /^quick tasks completed$/i.test(h.text.trim()));
|
||||||
|
if (!section) {
|
||||||
|
return { ok: false, reason: 'no Quick Tasks Completed section' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseMarkdownTable(section.body);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
return { ok: false, reason: `quick-tasks table: ${parsed.reason}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = matchTableSchema(parsed.value.columns);
|
||||||
|
if (!match || match.id !== 'QuickTasks') {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: `unrecognized Quick Tasks schema (columns: ${parsed.value.columns.join(' | ')})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant = TABLE_SCHEMAS.QuickTasks.find((v) => v.label === match.label);
|
||||||
|
const columns = variant ? variant.columns : parsed.value.columns;
|
||||||
|
|
||||||
|
const rowNumber = parsed.value.rows.length + 1;
|
||||||
|
const cellFor = (col: string): string => {
|
||||||
|
switch (col) {
|
||||||
|
case '#': return escapeCell(String(rowNumber));
|
||||||
|
case 'Description': return escapeCell(fields.description);
|
||||||
|
case 'Date': return escapeCell(fields.date);
|
||||||
|
case 'Commit': return escapeCell(fields.commit);
|
||||||
|
case 'Status': return escapeCell(fields.status ?? '—');
|
||||||
|
case 'Directory': return escapeCell(fields.directory ?? '—');
|
||||||
|
default: return '—';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const row = `| ${columns.map(cellFor).join(' | ')} |`;
|
||||||
|
|
||||||
|
// Detect the section's EOL BEFORE splitting on /\r?\n/ (which discards it) so
|
||||||
|
// the rejoin below preserves CRLF instead of downgrading a CRLF section to
|
||||||
|
// mixed EOL (the inserted `row` itself never contains a newline).
|
||||||
|
const eol = /\r\n/.test(section.body) ? '\r\n' : '\n';
|
||||||
|
const lines = section.body.split(/\r?\n/);
|
||||||
|
let lastTableLineIdx = -1;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].trim().startsWith('|')) lastTableLineIdx = i;
|
||||||
|
}
|
||||||
|
// lastTableLineIdx is always >= 0 here — parseMarkdownTable already
|
||||||
|
// confirmed a header + delimiter row exist in this same `section.body`.
|
||||||
|
const newLines = [
|
||||||
|
...lines.slice(0, lastTableLineIdx + 1),
|
||||||
|
row,
|
||||||
|
...lines.slice(lastTableLineIdx + 1),
|
||||||
|
];
|
||||||
|
const newBody = newLines.join(eol);
|
||||||
|
|
||||||
|
const content = replaceSection(stateContent, section, newBody);
|
||||||
|
|
||||||
|
return { ok: true, value: { content, row, variant: match.label } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
|
||||||
|
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
* - Issue #4 (open-gsd/gsd-core)
|
* - Issue #4 (open-gsd/gsd-core)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { findTableWithColumns } from './markdown-table.cjs';
|
||||||
|
|
||||||
/** Result of deriveProgressFromRoadmap. */
|
/** Result of deriveProgressFromRoadmap. */
|
||||||
export interface RoadmapProgress {
|
export interface RoadmapProgress {
|
||||||
completedPhases: number | null;
|
completedPhases: number | null;
|
||||||
@@ -30,6 +32,30 @@ export interface RoadmapProgress {
|
|||||||
/**
|
/**
|
||||||
* Derive completed_phases, total_phases, and total_plans from ROADMAP content.
|
* Derive completed_phases, total_phases, and total_plans from ROADMAP content.
|
||||||
* Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
|
* Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
|
||||||
|
*
|
||||||
|
* ADR-2143 §3 ("addressed by NAME, never ordinal"): the Progress table is
|
||||||
|
* located via the markdown-table seam's `findTableWithColumns`, which is
|
||||||
|
* column-NAME/order/count-invariant — it matches the first table whose header
|
||||||
|
* is a SUPERSET of the canonical `Phase` / `Plans Complete` / `Status` /
|
||||||
|
* `Completed` names, in any order, tolerating extra/injected unrelated
|
||||||
|
* columns (#2137's fast-check property test shuffles headers and injects
|
||||||
|
* columns and asserts the derived counts never change). This supersedes the
|
||||||
|
* earlier `findTableBySchema` exact-schema lookup, which required an exact
|
||||||
|
* canonical column SET+ORDER and returned all-null on any reordering or
|
||||||
|
* injection.
|
||||||
|
*
|
||||||
|
* Scoped to the `## Progress` section when the document has one (#2012 decoy
|
||||||
|
* avoidance — a differently-headed table sharing the same column names must
|
||||||
|
* not be picked up instead); a headingless milestone slice (#1445) falls back
|
||||||
|
* to scanning the whole input, preserving the "Progress table not under a
|
||||||
|
* `## Progress` heading, or not the first table in the document, still
|
||||||
|
* resolves" behaviour.
|
||||||
|
*
|
||||||
|
* Cells are read by column NAME (`r['Status']`, `r['Plans Complete']`,
|
||||||
|
* `r['Phase']`), fixing #2137 (the old position-based regex assumed "Status"
|
||||||
|
* was always the 3rd cell and "Plans Complete" the 2nd, which broke for the
|
||||||
|
* 5-column milestone-grouped variant that inserts a `Milestone` column ahead
|
||||||
|
* of them).
|
||||||
*/
|
*/
|
||||||
export function deriveProgressFromRoadmap(roadmapContent: string): RoadmapProgress {
|
export function deriveProgressFromRoadmap(roadmapContent: string): RoadmapProgress {
|
||||||
let completedPhases: number | null = null;
|
let completedPhases: number | null = null;
|
||||||
@@ -37,97 +63,40 @@ export function deriveProgressFromRoadmap(roadmapContent: string): RoadmapProgre
|
|||||||
let totalPlans: number | null = null;
|
let totalPlans: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse the Progress table by HEADER, not by a fixed column count. The
|
// ADR-2143 §3: read the Progress table by column NAME (order/injection-invariant),
|
||||||
// writer (cmdPhaseComplete) already branches on `cells.length === 5`, so it
|
// via the markdown-table seam. Scope to the `## Progress` section when present
|
||||||
// understands both the 4-column greenfield table
|
// (#2012 decoy avoidance); a headingless milestone slice (#1445) falls back to the
|
||||||
// | Phase | Plans Complete | Status | Completed |
|
// whole input. Requires the canonical Phase/Plans Complete/Status/Completed columns
|
||||||
// and the 5-column milestone-grouped table the same template ships
|
// in any order (extra columns ignored) — supersedes findTableBySchema's exact-schema lookup.
|
||||||
// | Phase | Milestone | Plans Complete | Status | Completed |
|
|
||||||
// The reader used two 4-column-only regexes, so every project past its v1.0
|
|
||||||
// milestone (5-column shape) parsed to all-null and phase.complete silently
|
|
||||||
// skipped the STATE progress write. Reading column indices by NAME keeps the
|
|
||||||
// reader and writer in agreement across both shapes and any future column
|
|
||||||
// (#2137). The table is located by its header row rather than a `## Progress`
|
|
||||||
// heading because some callers pass a milestone slice with no heading (#1445).
|
|
||||||
//
|
|
||||||
// When a `## Progress` heading IS present, scope the search to that section
|
|
||||||
// (mirroring the writer's #2012 scoping in cmdPhaseComplete) so the reader
|
|
||||||
// cannot bind to an earlier Phase/Status/Completed-shaped table elsewhere in
|
|
||||||
// the roadmap. Callers that pass a headingless milestone slice fall back to
|
|
||||||
// scanning the whole input.
|
|
||||||
// Line-anchored h2 match — `indexOf('## Progress')` would also match inside
|
|
||||||
// an h3 `### Progress` (the `## Progress` substring starts at the 2nd hash),
|
|
||||||
// letting a decoy subheading hijack the slice.
|
|
||||||
// Case-insensitive to match the case-insensitive header-cell comparison below.
|
|
||||||
//
|
|
||||||
// allow-adhoc-markdown: line-based Progress-table scan (header lookup +
|
|
||||||
// positional cell indexing); table parsing is out of the markdown-sectionizer
|
|
||||||
// seam's scope. Superseded by the ADR-2143 parseMarkdownTable/TABLE_SCHEMAS
|
|
||||||
// seam; pending #2143.
|
|
||||||
const progressMatch = roadmapContent.match(/^##[ \t]+Progress\b/im);
|
const progressMatch = roadmapContent.match(/^##[ \t]+Progress\b/im);
|
||||||
let scoped = roadmapContent;
|
let scoped = roadmapContent;
|
||||||
if (progressMatch && progressMatch.index !== undefined) {
|
if (progressMatch && progressMatch.index !== undefined) {
|
||||||
// Slice from `## Progress` to the next h1/h2 heading (or end); h3+ headings
|
|
||||||
// inside the section do not terminate it. The heading sits at index 0 of
|
|
||||||
// this slice with no leading newline, so the `\n#` search cannot match it.
|
|
||||||
const afterHeading = roadmapContent.slice(progressMatch.index);
|
const afterHeading = roadmapContent.slice(progressMatch.index);
|
||||||
const nextHeading = afterHeading.search(/\n#{1,2}[ \t]/);
|
const nextHeading = afterHeading.search(/\n#{1,2}[ \t]/);
|
||||||
scoped = nextHeading >= 0 ? afterHeading.slice(0, nextHeading) : afterHeading;
|
scoped = nextHeading >= 0 ? afterHeading.slice(0, nextHeading) : afterHeading;
|
||||||
}
|
}
|
||||||
const lines = scoped.split('\n');
|
const table = findTableWithColumns(scoped, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
||||||
|
|
||||||
// Split a markdown table row into trimmed cells — the same
|
if (table) {
|
||||||
// `split('|').slice(1, -1)` boundary the writer uses (phase.cts).
|
const allRows = table.rows;
|
||||||
const rowCells = (line: string): string[] =>
|
|
||||||
line.split('|').slice(1, -1).map((c) => c.trim());
|
|
||||||
const isTableRow = (line: string): boolean => line.trim().startsWith('|');
|
|
||||||
const isSeparatorRow = (cells: string[]): boolean =>
|
|
||||||
cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
|
|
||||||
|
|
||||||
let headerLine = -1;
|
const completed = allRows.filter((r) => /^complete$/i.test((r['Status'] ?? '').trim())).length;
|
||||||
let phaseIdx = -1;
|
completedPhases = completed > 0 ? completed : null;
|
||||||
let statusIdx = -1;
|
|
||||||
let plansIdx = -1;
|
// Data rows only (exclude 999.x backlog phases). Mirrors init.cts /^999(?:\.|$)/ filter.
|
||||||
for (let i = 0; i < lines.length; i++) {
|
const dataRows = allRows.filter((r) => {
|
||||||
if (!isTableRow(lines[i])) continue;
|
const phase = (r['Phase'] ?? '').trim();
|
||||||
const lc = rowCells(lines[i]).map((c) => c.toLowerCase());
|
return /^\d/.test(phase) && !/^999\b/.test(phase);
|
||||||
const p = lc.indexOf('phase');
|
});
|
||||||
const s = lc.indexOf('status');
|
totalPhases = dataRows.length > 0 ? dataRows.length : null;
|
||||||
const c = lc.indexOf('completed');
|
|
||||||
if (p >= 0 && s >= 0 && c >= 0) {
|
let totalPlansSum = 0;
|
||||||
headerLine = i;
|
for (const r of allRows) {
|
||||||
phaseIdx = p;
|
const cell = (r['Plans Complete'] ?? '').trim();
|
||||||
statusIdx = s;
|
const m = /(\d+)\s*\/\s*(\d+)/.exec(cell);
|
||||||
plansIdx = lc.findIndex((h) => h.includes('plans'));
|
if (m) totalPlansSum += parseInt(m[2], 10);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
totalPlans = totalPlansSum > 0 ? totalPlansSum : null;
|
||||||
|
|
||||||
if (headerLine >= 0) {
|
|
||||||
let phaseCount = 0;
|
|
||||||
let completedCount = 0;
|
|
||||||
let plansSum = 0;
|
|
||||||
// Walk the contiguous rows after the header; a markdown table ends at the
|
|
||||||
// first non-`|` line.
|
|
||||||
for (let i = headerLine + 1; i < lines.length; i++) {
|
|
||||||
if (!isTableRow(lines[i])) break;
|
|
||||||
const cells = rowCells(lines[i]);
|
|
||||||
if (isSeparatorRow(cells)) continue;
|
|
||||||
const phaseToken = (cells[phaseIdx] ?? '').trim();
|
|
||||||
if (!/^\d/.test(phaseToken)) continue; // not a data row
|
|
||||||
if (/^999\b/.test(phaseToken)) continue; // 999.x backlog sentinel (#1445)
|
|
||||||
phaseCount++;
|
|
||||||
if ((cells[statusIdx] ?? '').toLowerCase() === 'complete') completedCount++;
|
|
||||||
if (plansIdx >= 0) {
|
|
||||||
const mn = (cells[plansIdx] ?? '').match(/^(\d+)\/(\d+)$/);
|
|
||||||
if (mn) plansSum += parseInt(mn[2], 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Preserve the prior contract: a count of 0 is reported as null (absent),
|
|
||||||
// so the consumer leaves the existing STATE value untouched.
|
|
||||||
completedPhases = completedCount > 0 ? completedCount : null;
|
|
||||||
totalPhases = phaseCount > 0 ? phaseCount : null;
|
|
||||||
totalPlans = plansSum > 0 ? plansSum : null;
|
|
||||||
}
|
}
|
||||||
} catch { /* intentionally empty — fall through to existing values */ }
|
} catch { /* intentionally empty — fall through to existing values */ }
|
||||||
|
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
/**
|
|
||||||
* #2133 — fast.md log_to_state schema gate is unreachable.
|
|
||||||
*
|
|
||||||
* PR #85 added a column-count guard to fast.md's log_to_state step that used
|
|
||||||
* `awk -F'|' '{print NF-1}'`. A markdown table header has both a leading and a
|
|
||||||
* trailing pipe, so NF counts (real columns + 2) and NF-1 is always one too
|
|
||||||
* high. The `-eq 5` test could therefore never hold for the 5-column header
|
|
||||||
* quick.md writes, so /gsd-fast has never appended a Quick Task row since #85.
|
|
||||||
*
|
|
||||||
* This test does what the removed prose-regex test (bug-3805-*) did not: it
|
|
||||||
* EXTRACTS the actual bash block deployed in fast.md and EXECUTES it against
|
|
||||||
* real STATE.md fixtures, asserting on the filesystem result (row appended,
|
|
||||||
* cell count aligned with header). It fails on the NF-1 bug and passes once
|
|
||||||
* the count uses NF-2 and both 5/6-column schemas are accepted.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { test, describe } = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
const fs = require('node:fs');
|
|
||||||
const path = require('node:path');
|
|
||||||
const { execFileSync } = require('node:child_process');
|
|
||||||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
|
||||||
|
|
||||||
const FAST_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'fast.md');
|
|
||||||
|
|
||||||
const HEADER_5COL = '| # | Description | Date | Commit | Directory |';
|
|
||||||
const SEP_5COL = '|---|-------------|------|--------|-----------|';
|
|
||||||
const HEADER_6COL = '| # | Description | Date | Commit | Status | Directory |';
|
|
||||||
const SEP_6COL = '|---|-------------|------|--------|--------|-----------|';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the ```bash block embedded in fast.md's <step name="log_to_state">.
|
|
||||||
* This is the exact program the workflow runs — executing it is a behavioral
|
|
||||||
* test of the deployed product, not a source-grep over its prose.
|
|
||||||
*/
|
|
||||||
function extractLogToStateBash() {
|
|
||||||
const content = fs.readFileSync(FAST_MD, 'utf8');
|
|
||||||
const stepTag = '<step name="log_to_state">';
|
|
||||||
const stepStart = content.indexOf(stepTag);
|
|
||||||
assert.notEqual(stepStart, -1, 'fast.md must contain a log_to_state step');
|
|
||||||
const stepEnd = content.indexOf('</step>', stepStart);
|
|
||||||
assert.notEqual(stepEnd, -1, 'log_to_state step must close');
|
|
||||||
const step = content.slice(stepStart, stepEnd);
|
|
||||||
const fenceStart = step.indexOf('```bash');
|
|
||||||
assert.notEqual(fenceStart, -1, 'log_to_state step must contain a bash block');
|
|
||||||
const codeStart = step.indexOf('\n', fenceStart) + 1;
|
|
||||||
const fenceEnd = step.indexOf('\n```', codeStart);
|
|
||||||
assert.notEqual(fenceEnd, -1, 'log_to_state bash block must close');
|
|
||||||
return step.slice(codeStart, fenceEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeStateMd(headerLine, separatorLine, existingRows) {
|
|
||||||
return [
|
|
||||||
'# Project State',
|
|
||||||
'',
|
|
||||||
'### Blockers/Concerns',
|
|
||||||
'',
|
|
||||||
'None.',
|
|
||||||
'',
|
|
||||||
'### Quick Tasks Completed',
|
|
||||||
'',
|
|
||||||
headerLine,
|
|
||||||
separatorLine,
|
|
||||||
...existingRows,
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Count `|` chars on a line — the invariant cell-count signal. */
|
|
||||||
function pipeCount(line) {
|
|
||||||
return (line.match(/\|/g) || []).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Data rows = lines starting with `|` that are not the separator or header. */
|
|
||||||
function dataRows(content) {
|
|
||||||
return content.split(/\r?\n/).filter((l) => {
|
|
||||||
if (!l.startsWith('|')) return false;
|
|
||||||
if (/^[|][-: |]*[|]$/.test(l)) return false; // separator
|
|
||||||
if (/Description/.test(l)) return false; // header
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('#2133 fast.md log_to_state schema gate', () => {
|
|
||||||
const bashBlock = extractLogToStateBash();
|
|
||||||
const TASK_DESC = 'sample inline fix';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the extracted log_to_state bash against a temp project's STATE.md and
|
|
||||||
* return the post-run file content + captured stdout.
|
|
||||||
*/
|
|
||||||
function runAgainst(headerLine, separatorLine, existingRows) {
|
|
||||||
const tmpDir = createTempProject('fix-2133-');
|
|
||||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
|
||||||
fs.writeFileSync(statePath, makeStateMd(headerLine, separatorLine, existingRows));
|
|
||||||
// Cleanup is bound by each caller via t.after(tmpDir).
|
|
||||||
const stdout = execFileSync('bash', ['-c', bashBlock], {
|
|
||||||
cwd: tmpDir,
|
|
||||||
env: { ...process.env, TASK: TASK_DESC },
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
const after = fs.readFileSync(statePath, 'utf8');
|
|
||||||
return { tmpDir, stdout, after };
|
|
||||||
}
|
|
||||||
|
|
||||||
test('appends a 5-cell row to the 5-column (non-validate) schema (#27 stays fixed)', (t) => {
|
|
||||||
const before = makeStateMd(HEADER_5COL, SEP_5COL, ['| 1 | earlier task | 2026-07-01 | deadbee | — |']);
|
|
||||||
const { tmpDir, after } = runAgainst(HEADER_5COL, SEP_5COL, ['| 1 | earlier task | 2026-07-01 | deadbee | — |']);
|
|
||||||
t.after(() => cleanup(tmpDir));
|
|
||||||
|
|
||||||
const rowsBefore = dataRows(before).length;
|
|
||||||
const rowsAfter = dataRows(after).length;
|
|
||||||
assert.equal(rowsAfter - rowsBefore, 1, 'exactly one row must be appended');
|
|
||||||
|
|
||||||
const appended = dataRows(after).slice(-1)[0];
|
|
||||||
assert.equal(pipeCount(appended), pipeCount(HEADER_5COL),
|
|
||||||
'appended row pipe-count must match the 5-column header (no malformed row)');
|
|
||||||
assert.ok(appended.includes(TASK_DESC), 'appended row must carry the task description');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('appends a 6-cell row to the 6-column (validate, with Status) schema', (t) => {
|
|
||||||
const before = makeStateMd(HEADER_6COL, SEP_6COL, []);
|
|
||||||
const { tmpDir, after } = runAgainst(HEADER_6COL, SEP_6COL, []);
|
|
||||||
t.after(() => cleanup(tmpDir));
|
|
||||||
|
|
||||||
const rowsBefore = dataRows(before).length;
|
|
||||||
const rowsAfter = dataRows(after).length;
|
|
||||||
assert.equal(rowsAfter - rowsBefore, 1, 'exactly one row must be appended to the 6-column table');
|
|
||||||
|
|
||||||
const appended = dataRows(after).slice(-1)[0];
|
|
||||||
assert.equal(pipeCount(appended), pipeCount(HEADER_6COL),
|
|
||||||
'appended row pipe-count must match the 6-column header (cell count aligned with header)');
|
|
||||||
assert.ok(appended.includes(TASK_DESC), 'appended row must carry the task description');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('column count awk uses NF-2 (real columns — the off-by-one root cause)', () => {
|
|
||||||
// The deployed bash must compute the real column count. A 5-column header
|
|
||||||
// split on '|' yields NF=7; the correct real-column formula is NF-2=5.
|
|
||||||
// (NF-1 was the off-by-one bug: it returned 6, making `-eq 5` unsatisfiable.)
|
|
||||||
// Match the executable COL_COUNT assignment specifically — the explanatory
|
|
||||||
// comment may still reference "NF-1" to document the history.
|
|
||||||
assert.match(
|
|
||||||
bashBlock,
|
|
||||||
/COL_COUNT=\$\(.+awk -F'\|' '\{print NF-2\}'\)/,
|
|
||||||
'COL_COUNT must be derived via awk NF-2 (NF-1 was the off-by-one bug)'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('unrecognized schema still skips with a warning (safety guard intact)', (t) => {
|
|
||||||
// A 3-column table quick.md never writes must NOT receive a row.
|
|
||||||
const weirdHeader = '| Alpha | Beta | Gamma |';
|
|
||||||
const weirdSep = '|-------|------|-------|';
|
|
||||||
const before = makeStateMd(weirdHeader, weirdSep, []);
|
|
||||||
const { tmpDir, stdout, after } = runAgainst(weirdHeader, weirdSep, []);
|
|
||||||
t.after(() => cleanup(tmpDir));
|
|
||||||
|
|
||||||
assert.equal(dataRows(after).length, dataRows(before).length,
|
|
||||||
'no row may be appended for an unrecognized schema');
|
|
||||||
assert.ok(/unrecognized schema/i.test(stdout),
|
|
||||||
'the unrecognized-schema warning must be emitted');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('no Quick Tasks table → silent no-op', (t) => {
|
|
||||||
const tmpDir = createTempProject('fix-2133-noop-');
|
|
||||||
t.after(() => cleanup(tmpDir));
|
|
||||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
|
||||||
const before = '# Project State\n\n### Blockers/Concerns\n\nNone.\n';
|
|
||||||
fs.writeFileSync(statePath, before);
|
|
||||||
const stdout = execFileSync('bash', ['-c', bashBlock], {
|
|
||||||
cwd: tmpDir,
|
|
||||||
env: { ...process.env, TASK: TASK_DESC },
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
const after = fs.readFileSync(statePath, 'utf8');
|
|
||||||
assert.equal(after, before, 'STATE.md must be untouched when no Quick Tasks table exists');
|
|
||||||
assert.equal(stdout, '', 'no output when there is no table to update');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
|
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb",
|
"gsd-core/bin/gsd-tools.cjs": "efc88e7691c6c3e6",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "907af77eafc3d97b",
|
"gsd-core/workflows/execute-plan.md": "907af77eafc3d97b",
|
||||||
"gsd-core/workflows/explore.md": "934c00f9f216dbdb",
|
"gsd-core/workflows/explore.md": "934c00f9f216dbdb",
|
||||||
"gsd-core/workflows/extract-learnings.md": "167ea7f0e23bf496",
|
"gsd-core/workflows/extract-learnings.md": "167ea7f0e23bf496",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "568e6c3ec00b6e00",
|
||||||
"gsd-core/workflows/forensics.md": "b64f0309b8c3fde1",
|
"gsd-core/workflows/forensics.md": "b64f0309b8c3fde1",
|
||||||
"gsd-core/workflows/graduation.md": "a9d8f15ba81a993f",
|
"gsd-core/workflows/graduation.md": "a9d8f15ba81a993f",
|
||||||
"gsd-core/workflows/health.md": "1ac4d567ee95acfb",
|
"gsd-core/workflows/health.md": "1ac4d567ee95acfb",
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
|
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
|
||||||
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
|
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
|
||||||
"gsd-core/workflows/progress.md": "79a11ce798082054",
|
"gsd-core/workflows/progress.md": "79a11ce798082054",
|
||||||
"gsd-core/workflows/quick.md": "7108075f69e88e7c",
|
"gsd-core/workflows/quick.md": "424c0eb61769e747",
|
||||||
"gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca",
|
"gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca",
|
||||||
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
|
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
|
||||||
"gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798",
|
"gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "2c412310dce31a0b",
|
"gsd-core/workflows/execute-plan.md": "2c412310dce31a0b",
|
||||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
"gsd-core/workflows/fast.md": "11f5cd10ae5cc7d3",
|
||||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||||
"gsd-core/workflows/profile-user.md": "14263db831230142",
|
"gsd-core/workflows/profile-user.md": "14263db831230142",
|
||||||
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
||||||
"gsd-core/workflows/quick.md": "069fe37d083a94a3",
|
"gsd-core/workflows/quick.md": "25f0cc40bc22ed3e",
|
||||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "f17623fd47e795dd",
|
"gsd-core/workflows/execute-plan.md": "f17623fd47e795dd",
|
||||||
"gsd-core/workflows/explore.md": "95e463d4bdd6dadd",
|
"gsd-core/workflows/explore.md": "95e463d4bdd6dadd",
|
||||||
"gsd-core/workflows/extract-learnings.md": "fd75072c339b58bd",
|
"gsd-core/workflows/extract-learnings.md": "fd75072c339b58bd",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "7f7687b920d79b29",
|
||||||
"gsd-core/workflows/forensics.md": "857d7b064f4cca21",
|
"gsd-core/workflows/forensics.md": "857d7b064f4cca21",
|
||||||
"gsd-core/workflows/graduation.md": "ecf8da93e094fd2e",
|
"gsd-core/workflows/graduation.md": "ecf8da93e094fd2e",
|
||||||
"gsd-core/workflows/health.md": "551e63aa6f3df711",
|
"gsd-core/workflows/health.md": "551e63aa6f3df711",
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
|
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
|
||||||
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",
|
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",
|
||||||
"gsd-core/workflows/progress.md": "2be3a57916eccf87",
|
"gsd-core/workflows/progress.md": "2be3a57916eccf87",
|
||||||
"gsd-core/workflows/quick.md": "20f9dbfcd20b4b4b",
|
"gsd-core/workflows/quick.md": "f1b474b46327034f",
|
||||||
"gsd-core/workflows/reapply-patches.md": "44a96b52b975e9bb",
|
"gsd-core/workflows/reapply-patches.md": "44a96b52b975e9bb",
|
||||||
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
|
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
|
||||||
"gsd-core/workflows/remove-workspace.md": "10882656198d9075",
|
"gsd-core/workflows/remove-workspace.md": "10882656198d9075",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "cce1a33fe9a0a32d",
|
"gsd-core/workflows/execute-plan.md": "cce1a33fe9a0a32d",
|
||||||
"gsd-core/workflows/explore.md": "b9eea1bac358c9ce",
|
"gsd-core/workflows/explore.md": "b9eea1bac358c9ce",
|
||||||
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "41a6568b873aef99",
|
||||||
"gsd-core/workflows/forensics.md": "c01da0178fb97b21",
|
"gsd-core/workflows/forensics.md": "c01da0178fb97b21",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "f934b1f1e9f3ae72",
|
"gsd-core/workflows/health.md": "f934b1f1e9f3ae72",
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
||||||
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
|
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
|
||||||
"gsd-core/workflows/progress.md": "bd1ecf9207331bda",
|
"gsd-core/workflows/progress.md": "bd1ecf9207331bda",
|
||||||
"gsd-core/workflows/quick.md": "3363bdfefd403686",
|
"gsd-core/workflows/quick.md": "b4237633eaf5a246",
|
||||||
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
||||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||||
"gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed",
|
"gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf",
|
"gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "a84914f7cab74332",
|
"gsd-core/bin/gsd-tools.cjs": "49dfaa890fdd5627",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "0c5551f99ee0a017",
|
"gsd-core/workflows/execute-plan.md": "0c5551f99ee0a017",
|
||||||
"gsd-core/workflows/explore.md": "e83af8ceae314cf9",
|
"gsd-core/workflows/explore.md": "e83af8ceae314cf9",
|
||||||
"gsd-core/workflows/extract-learnings.md": "6f39375b7dc775f9",
|
"gsd-core/workflows/extract-learnings.md": "6f39375b7dc775f9",
|
||||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
"gsd-core/workflows/fast.md": "e4f74a454b6ca5e8",
|
||||||
"gsd-core/workflows/forensics.md": "9fc65a8eed5d8bfc",
|
"gsd-core/workflows/forensics.md": "9fc65a8eed5d8bfc",
|
||||||
"gsd-core/workflows/graduation.md": "16fedecda36769eb",
|
"gsd-core/workflows/graduation.md": "16fedecda36769eb",
|
||||||
"gsd-core/workflows/health.md": "788fad84fa42eb9f",
|
"gsd-core/workflows/health.md": "788fad84fa42eb9f",
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
|
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
|
||||||
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
|
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
|
||||||
"gsd-core/workflows/progress.md": "9f326d63afb4b76b",
|
"gsd-core/workflows/progress.md": "9f326d63afb4b76b",
|
||||||
"gsd-core/workflows/quick.md": "2afc046e94daad0a",
|
"gsd-core/workflows/quick.md": "797b1c8d62c3d33d",
|
||||||
"gsd-core/workflows/reapply-patches.md": "eb4272145a117904",
|
"gsd-core/workflows/reapply-patches.md": "eb4272145a117904",
|
||||||
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
|
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
|
||||||
"gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90",
|
"gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "933d10547116794a",
|
"gsd-core/workflows/execute-plan.md": "933d10547116794a",
|
||||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
"gsd-core/workflows/fast.md": "11f5cd10ae5cc7d3",
|
||||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||||
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
|
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
|
||||||
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
||||||
"gsd-core/workflows/quick.md": "cc530299ba461539",
|
"gsd-core/workflows/quick.md": "0bd83bd88c5e63d1",
|
||||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -348,7 +348,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "ac1f1d9ada00a91e",
|
"gsd-core/workflows/execute-plan.md": "ac1f1d9ada00a91e",
|
||||||
"gsd-core/workflows/explore.md": "2ef10d17c8864a04",
|
"gsd-core/workflows/explore.md": "2ef10d17c8864a04",
|
||||||
"gsd-core/workflows/extract-learnings.md": "f716aa03fcb5f8da",
|
"gsd-core/workflows/extract-learnings.md": "f716aa03fcb5f8da",
|
||||||
"gsd-core/workflows/fast.md": "bbf1f8e219ec9031",
|
"gsd-core/workflows/fast.md": "e4ed60f96a7b3ac8",
|
||||||
"gsd-core/workflows/forensics.md": "2e8a01b5b44e65f3",
|
"gsd-core/workflows/forensics.md": "2e8a01b5b44e65f3",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "5b835fc606fd3e9b",
|
"gsd-core/workflows/health.md": "5b835fc606fd3e9b",
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
|
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
|
||||||
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",
|
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",
|
||||||
"gsd-core/workflows/progress.md": "7bedc431bb55edb8",
|
"gsd-core/workflows/progress.md": "7bedc431bb55edb8",
|
||||||
"gsd-core/workflows/quick.md": "62b9e138dc70b479",
|
"gsd-core/workflows/quick.md": "bf70b7beb314aa50",
|
||||||
"gsd-core/workflows/reapply-patches.md": "26297b84736e66a4",
|
"gsd-core/workflows/reapply-patches.md": "26297b84736e66a4",
|
||||||
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
|
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
|
||||||
"gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62",
|
"gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
|
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb",
|
"gsd-core/bin/gsd-tools.cjs": "efc88e7691c6c3e6",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "c5e9dae726db15cc",
|
"gsd-core/workflows/execute-plan.md": "c5e9dae726db15cc",
|
||||||
"gsd-core/workflows/explore.md": "5fd91a8510e1114b",
|
"gsd-core/workflows/explore.md": "5fd91a8510e1114b",
|
||||||
"gsd-core/workflows/extract-learnings.md": "f34d0b1927545b18",
|
"gsd-core/workflows/extract-learnings.md": "f34d0b1927545b18",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "66821090b6b8ed3b",
|
||||||
"gsd-core/workflows/forensics.md": "459644dce26ee2ef",
|
"gsd-core/workflows/forensics.md": "459644dce26ee2ef",
|
||||||
"gsd-core/workflows/graduation.md": "f013efc29096faf0",
|
"gsd-core/workflows/graduation.md": "f013efc29096faf0",
|
||||||
"gsd-core/workflows/health.md": "f343fd32a0e398e4",
|
"gsd-core/workflows/health.md": "f343fd32a0e398e4",
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
|
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
|
||||||
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
|
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
|
||||||
"gsd-core/workflows/progress.md": "6b9a84a43dc55af5",
|
"gsd-core/workflows/progress.md": "6b9a84a43dc55af5",
|
||||||
"gsd-core/workflows/quick.md": "eb68ea8748546331",
|
"gsd-core/workflows/quick.md": "4a53899b69b8c801",
|
||||||
"gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180",
|
"gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180",
|
||||||
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
|
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
|
||||||
"gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754",
|
"gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828",
|
"gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "db16ded31dd6eecf",
|
"gsd-core/bin/gsd-tools.cjs": "50658d517405cd63",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "83dc1bf7f73735c0",
|
"gsd-core/workflows/execute-plan.md": "83dc1bf7f73735c0",
|
||||||
"gsd-core/workflows/explore.md": "b9eea1bac358c9ce",
|
"gsd-core/workflows/explore.md": "b9eea1bac358c9ce",
|
||||||
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
||||||
"gsd-core/workflows/fast.md": "91f126c4ef24217e",
|
"gsd-core/workflows/fast.md": "77b49793e26b3323",
|
||||||
"gsd-core/workflows/forensics.md": "a65f817d4a515291",
|
"gsd-core/workflows/forensics.md": "a65f817d4a515291",
|
||||||
"gsd-core/workflows/graduation.md": "53a4a6fa3b4e6613",
|
"gsd-core/workflows/graduation.md": "53a4a6fa3b4e6613",
|
||||||
"gsd-core/workflows/health.md": "e4d770484b5e7496",
|
"gsd-core/workflows/health.md": "e4d770484b5e7496",
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
|
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
|
||||||
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
|
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
|
||||||
"gsd-core/workflows/progress.md": "65aabee5e8a6dd82",
|
"gsd-core/workflows/progress.md": "65aabee5e8a6dd82",
|
||||||
"gsd-core/workflows/quick.md": "32a71f62041c8510",
|
"gsd-core/workflows/quick.md": "3b37350964cfe391",
|
||||||
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
||||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||||
"gsd-core/workflows/remove-workspace.md": "433affcd1a200826",
|
"gsd-core/workflows/remove-workspace.md": "433affcd1a200826",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f",
|
"gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "b3f927ee4b4c4711",
|
"gsd-core/bin/gsd-tools.cjs": "12ee14a48b678d2b",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "4dbe9b6f0c976245",
|
"gsd-core/workflows/execute-plan.md": "4dbe9b6f0c976245",
|
||||||
"gsd-core/workflows/explore.md": "48770d68e8b9c132",
|
"gsd-core/workflows/explore.md": "48770d68e8b9c132",
|
||||||
"gsd-core/workflows/extract-learnings.md": "e9e167c718949c0b",
|
"gsd-core/workflows/extract-learnings.md": "e9e167c718949c0b",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "c801145115755524",
|
||||||
"gsd-core/workflows/forensics.md": "91961b811917c5c4",
|
"gsd-core/workflows/forensics.md": "91961b811917c5c4",
|
||||||
"gsd-core/workflows/graduation.md": "d1fd52bbe41dcf34",
|
"gsd-core/workflows/graduation.md": "d1fd52bbe41dcf34",
|
||||||
"gsd-core/workflows/health.md": "c622a5ad0d347d30",
|
"gsd-core/workflows/health.md": "c622a5ad0d347d30",
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
|
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
|
||||||
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
|
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
|
||||||
"gsd-core/workflows/progress.md": "f18db000584d9cb1",
|
"gsd-core/workflows/progress.md": "f18db000584d9cb1",
|
||||||
"gsd-core/workflows/quick.md": "e7a395b9e7786b56",
|
"gsd-core/workflows/quick.md": "731c20b5605bd319",
|
||||||
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
|
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
|
||||||
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
|
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
|
||||||
"gsd-core/workflows/remove-workspace.md": "8facde381657dd71",
|
"gsd-core/workflows/remove-workspace.md": "8facde381657dd71",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "8dc89b35582407f7",
|
"gsd-core/workflows/execute-plan.md": "8dc89b35582407f7",
|
||||||
"gsd-core/workflows/explore.md": "14242d36d4822df6",
|
"gsd-core/workflows/explore.md": "14242d36d4822df6",
|
||||||
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "41a6568b873aef99",
|
||||||
"gsd-core/workflows/forensics.md": "c01da0178fb97b21",
|
"gsd-core/workflows/forensics.md": "c01da0178fb97b21",
|
||||||
"gsd-core/workflows/graduation.md": "5cc8638dabcbfd40",
|
"gsd-core/workflows/graduation.md": "5cc8638dabcbfd40",
|
||||||
"gsd-core/workflows/health.md": "064b3668d5d9569a",
|
"gsd-core/workflows/health.md": "064b3668d5d9569a",
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
||||||
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
|
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
|
||||||
"gsd-core/workflows/progress.md": "9381c59676ccb937",
|
"gsd-core/workflows/progress.md": "9381c59676ccb937",
|
||||||
"gsd-core/workflows/quick.md": "e71c92cf0463a461",
|
"gsd-core/workflows/quick.md": "4ddd6d3214f57f57",
|
||||||
"gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4",
|
"gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4",
|
||||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||||
"gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7",
|
"gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -306,7 +306,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "c8502b7475d797a7",
|
"gsd-core/workflows/execute-plan.md": "c8502b7475d797a7",
|
||||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
"gsd-core/workflows/fast.md": "11f5cd10ae5cc7d3",
|
||||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||||
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
|
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
|
||||||
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
||||||
"gsd-core/workflows/quick.md": "09d88dd1c0b3e03a",
|
"gsd-core/workflows/quick.md": "8dcda37c46954ebe",
|
||||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "e8de8ea661c1fe81",
|
"gsd-core/workflows/execute-plan.md": "e8de8ea661c1fe81",
|
||||||
"gsd-core/workflows/explore.md": "7f5f9231cfd3089b",
|
"gsd-core/workflows/explore.md": "7f5f9231cfd3089b",
|
||||||
"gsd-core/workflows/extract-learnings.md": "92b3c0979604b7d0",
|
"gsd-core/workflows/extract-learnings.md": "92b3c0979604b7d0",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "12187b242e6af970",
|
||||||
"gsd-core/workflows/forensics.md": "9354cb830152fd28",
|
"gsd-core/workflows/forensics.md": "9354cb830152fd28",
|
||||||
"gsd-core/workflows/graduation.md": "13080b5c24eec27d",
|
"gsd-core/workflows/graduation.md": "13080b5c24eec27d",
|
||||||
"gsd-core/workflows/health.md": "a91e0a8e5d20b2e9",
|
"gsd-core/workflows/health.md": "a91e0a8e5d20b2e9",
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
|
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
|
||||||
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
|
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
|
||||||
"gsd-core/workflows/progress.md": "8fc3404087f50b95",
|
"gsd-core/workflows/progress.md": "8fc3404087f50b95",
|
||||||
"gsd-core/workflows/quick.md": "9bdccb7c6a5530c9",
|
"gsd-core/workflows/quick.md": "774d646560a74c93",
|
||||||
"gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2",
|
"gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2",
|
||||||
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
|
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
|
||||||
"gsd-core/workflows/remove-workspace.md": "446847e71aa52504",
|
"gsd-core/workflows/remove-workspace.md": "446847e71aa52504",
|
||||||
|
|||||||
6
tests/fixtures/golden-install-parity/pi.json
vendored
6
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -5,7 +5,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "ff172c3540b52e9d",
|
"gsd-core/workflows/execute-plan.md": "ff172c3540b52e9d",
|
||||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
"gsd-core/workflows/fast.md": "11f5cd10ae5cc7d3",
|
||||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||||
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",
|
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",
|
||||||
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
||||||
"gsd-core/workflows/quick.md": "4471707540842a41",
|
"gsd-core/workflows/quick.md": "31787c1e8a867e3f",
|
||||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2",
|
"gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "6454021dec4d9563",
|
"gsd-core/bin/gsd-tools.cjs": "205830afac36f33a",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "503b0ced0731dc38",
|
"gsd-core/workflows/execute-plan.md": "503b0ced0731dc38",
|
||||||
"gsd-core/workflows/explore.md": "e1a83a8982532e5b",
|
"gsd-core/workflows/explore.md": "e1a83a8982532e5b",
|
||||||
"gsd-core/workflows/extract-learnings.md": "dd4fdb88605de49a",
|
"gsd-core/workflows/extract-learnings.md": "dd4fdb88605de49a",
|
||||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
"gsd-core/workflows/fast.md": "93ed453edddd8c0c",
|
||||||
"gsd-core/workflows/forensics.md": "82800a3138ac1da9",
|
"gsd-core/workflows/forensics.md": "82800a3138ac1da9",
|
||||||
"gsd-core/workflows/graduation.md": "40401655435beee3",
|
"gsd-core/workflows/graduation.md": "40401655435beee3",
|
||||||
"gsd-core/workflows/health.md": "043de14edb6a9723",
|
"gsd-core/workflows/health.md": "043de14edb6a9723",
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
|
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
|
||||||
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
|
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
|
||||||
"gsd-core/workflows/progress.md": "3b1b2142a74af85c",
|
"gsd-core/workflows/progress.md": "3b1b2142a74af85c",
|
||||||
"gsd-core/workflows/quick.md": "bb9a4a145a7edc36",
|
"gsd-core/workflows/quick.md": "d0fffe516abaf47a",
|
||||||
"gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2",
|
"gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2",
|
||||||
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
|
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
|
||||||
"gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e",
|
"gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527",
|
"gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "c28c9076058c5e73",
|
"gsd-core/bin/gsd-tools.cjs": "c8283c0c8888e357",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "4910f75bab2040ab",
|
"gsd-core/workflows/execute-plan.md": "4910f75bab2040ab",
|
||||||
"gsd-core/workflows/explore.md": "8a5437aa0c239c38",
|
"gsd-core/workflows/explore.md": "8a5437aa0c239c38",
|
||||||
"gsd-core/workflows/extract-learnings.md": "3fcc858b20d0d0e6",
|
"gsd-core/workflows/extract-learnings.md": "3fcc858b20d0d0e6",
|
||||||
"gsd-core/workflows/fast.md": "91f126c4ef24217e",
|
"gsd-core/workflows/fast.md": "0f05b1e008ac2fc2",
|
||||||
"gsd-core/workflows/forensics.md": "665546666547875d",
|
"gsd-core/workflows/forensics.md": "665546666547875d",
|
||||||
"gsd-core/workflows/graduation.md": "1ca877cda258a5de",
|
"gsd-core/workflows/graduation.md": "1ca877cda258a5de",
|
||||||
"gsd-core/workflows/health.md": "1f9fd2deea45896f",
|
"gsd-core/workflows/health.md": "1f9fd2deea45896f",
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
|
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
|
||||||
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
|
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
|
||||||
"gsd-core/workflows/progress.md": "94768f835b0b8908",
|
"gsd-core/workflows/progress.md": "94768f835b0b8908",
|
||||||
"gsd-core/workflows/quick.md": "a568015cccb53615",
|
"gsd-core/workflows/quick.md": "0529abcf9913ec04",
|
||||||
"gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78",
|
"gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78",
|
||||||
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
|
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
|
||||||
"gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663",
|
"gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2",
|
"gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "568e7c6bdd415c1a",
|
"gsd-core/bin/gsd-tools.cjs": "f21bb9ba5e55f642",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "c8567fb4438b4404",
|
"gsd-core/workflows/execute-plan.md": "c8567fb4438b4404",
|
||||||
"gsd-core/workflows/explore.md": "04e461ff8159a24e",
|
"gsd-core/workflows/explore.md": "04e461ff8159a24e",
|
||||||
"gsd-core/workflows/extract-learnings.md": "af793bdf4ffd1c8a",
|
"gsd-core/workflows/extract-learnings.md": "af793bdf4ffd1c8a",
|
||||||
"gsd-core/workflows/fast.md": "91f126c4ef24217e",
|
"gsd-core/workflows/fast.md": "b03b9f595892479a",
|
||||||
"gsd-core/workflows/forensics.md": "3d1ce16b5f605592",
|
"gsd-core/workflows/forensics.md": "3d1ce16b5f605592",
|
||||||
"gsd-core/workflows/graduation.md": "a766039ff6ca653f",
|
"gsd-core/workflows/graduation.md": "a766039ff6ca653f",
|
||||||
"gsd-core/workflows/health.md": "7b19d6e2c0357c3e",
|
"gsd-core/workflows/health.md": "7b19d6e2c0357c3e",
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
|
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
|
||||||
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",
|
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",
|
||||||
"gsd-core/workflows/progress.md": "18813a345bd2343a",
|
"gsd-core/workflows/progress.md": "18813a345bd2343a",
|
||||||
"gsd-core/workflows/quick.md": "b7813e1810c683e1",
|
"gsd-core/workflows/quick.md": "71fbcf38e2305de0",
|
||||||
"gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379",
|
"gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379",
|
||||||
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
|
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
|
||||||
"gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a",
|
"gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a",
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||||
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
|
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"gsd-core/workflows/execute-plan.md": "ed874410972d32b7",
|
"gsd-core/workflows/execute-plan.md": "ed874410972d32b7",
|
||||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
"gsd-core/workflows/fast.md": "11f5cd10ae5cc7d3",
|
||||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||||
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
|
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
|
||||||
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
||||||
"gsd-core/workflows/quick.md": "12917fdc9a624a0c",
|
"gsd-core/workflows/quick.md": "7ad8d250c536c4dd",
|
||||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||||
|
|||||||
613
tests/markdown-table.test.cjs
Normal file
613
tests/markdown-table.test.cjs
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavioral tests for markdown-table.cjs
|
||||||
|
*
|
||||||
|
* Module: gsd-core/bin/lib/markdown-table.cjs
|
||||||
|
* Exports: parseMarkdownTable, matchTableSchema, TABLE_SCHEMAS
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - parseMarkdownTable happy path (4-col + 5-col headers, cells addressed by name)
|
||||||
|
* - BOUNDARY coverage: ragged data rows at limit-1 / limit / limit+1 cell counts
|
||||||
|
* - malformed-input error paths (empty, non-string, no table, missing delimiter row)
|
||||||
|
* - matchTableSchema resolving every canonical variant + null for unknown headers
|
||||||
|
* - fast-check round-trip property test
|
||||||
|
* - registry <-> template/workflow parity guard (ADR-2143 §3 Generative-Fix-
|
||||||
|
* Divergence guard) — every TABLE_SCHEMAS header must appear verbatim in the
|
||||||
|
* source file that generates it
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, describe } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const fc = require('./helpers/fast-check-setup.cjs');
|
||||||
|
|
||||||
|
const { parseMarkdownTable, matchTableSchema, TABLE_SCHEMAS, appendQuickTaskRow, findTableBySchema, findTableWithColumns } = require('../gsd-core/bin/lib/markdown-table.cjs');
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// ─── parseMarkdownTable: happy path ───────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('parseMarkdownTable: happy path', () => {
|
||||||
|
test('parses a 4-column flat RoadmapProgress table, cells addressed by name', () => {
|
||||||
|
const src = [
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | 1/2 | In Progress | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.deepEqual(result.value.columns, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
||||||
|
assert.equal(result.value.rows.length, 2);
|
||||||
|
assert.equal(result.value.rows[0]['Phase'], '1. Alpha');
|
||||||
|
assert.equal(result.value.rows[0]['Plans Complete'], '2/2');
|
||||||
|
assert.equal(result.value.rows[0]['Status'], 'Complete');
|
||||||
|
assert.equal(result.value.rows[1]['Status'], 'In Progress');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses a 5-column milestone-grouped RoadmapProgress table, cells addressed by name', () => {
|
||||||
|
const src = [
|
||||||
|
'| Phase | Milestone | Plans Complete | Status | Completed |',
|
||||||
|
'|---|---|---|---|---|',
|
||||||
|
'| 1. Alpha | v1.0 | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | v1.1 | 0/3 | Planned | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.deepEqual(result.value.columns, ['Phase', 'Milestone', 'Plans Complete', 'Status', 'Completed']);
|
||||||
|
assert.equal(result.value.rows[0]['Milestone'], 'v1.0');
|
||||||
|
assert.equal(result.value.rows[1]['Milestone'], 'v1.1');
|
||||||
|
assert.equal(result.value.rows[1]['Status'], 'Planned');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finds the FIRST table when the section has leading prose', () => {
|
||||||
|
const src = [
|
||||||
|
'Some intro prose before the table.',
|
||||||
|
'',
|
||||||
|
'| Requirement | Phase | Status |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
'| R1 | 1 | Done |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.deepEqual(result.value.columns, ['Requirement', 'Phase', 'Status']);
|
||||||
|
assert.equal(result.value.rows[0]['Requirement'], 'R1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── BOUNDARY coverage: ragged data rows ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe('parseMarkdownTable: boundary coverage (ragged rows)', () => {
|
||||||
|
const header = '| Phase | Plans Complete | Status | Completed |';
|
||||||
|
const delimiter = '| --- | --- | --- | --- |';
|
||||||
|
|
||||||
|
test('limit-1: a 3-cell data row (one short of the 4-column header) is a typed error', () => {
|
||||||
|
const src = [header, delimiter, '| 1. Alpha | 2/2 | Complete |'].join('\n');
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.reason, /row 1 has 3 cells, expected 4/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('limit: a 4-cell data row (exactly matching the 4-column header) parses ok', () => {
|
||||||
|
const src = [header, delimiter, '| 1. Alpha | 2/2 | Complete | ✅ |'].join('\n');
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.value.rows.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('limit+1: a 5-cell data row (one over the 4-column header) is a typed error', () => {
|
||||||
|
const src = [header, delimiter, '| 1. Alpha | 2/2 | Complete | ✅ | extra |'].join('\n');
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.reason, /row 1 has 5 cells, expected 4/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Malformed input ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('parseMarkdownTable: malformed input', () => {
|
||||||
|
test('empty string returns a typed error', () => {
|
||||||
|
const result = parseMarkdownTable('');
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.reason, 'empty or non-string input');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('whitespace-only string returns a typed error', () => {
|
||||||
|
const result = parseMarkdownTable(' \n \n ');
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.reason, 'empty or non-string input');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-string input returns a typed error, does not throw', () => {
|
||||||
|
let result;
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
result = parseMarkdownTable(42);
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.reason, 'empty or non-string input');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('content with no pipe table returns a typed error', () => {
|
||||||
|
const result = parseMarkdownTable('# Heading\n\nJust some prose, no table here.\n');
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.reason, 'no table found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing delimiter row returns a typed error', () => {
|
||||||
|
const src = [
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
].join('\n');
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.reason, 'missing delimiter row');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delimiter row present but column count mismatch returns a typed error', () => {
|
||||||
|
const src = [
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
].join('\n');
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.reason, 'delimiter/header column count mismatch');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── matchTableSchema ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('matchTableSchema', () => {
|
||||||
|
test('resolves every canonical variant header to its {id,label}', () => {
|
||||||
|
for (const [id, variants] of Object.entries(TABLE_SCHEMAS)) {
|
||||||
|
for (const variant of variants) {
|
||||||
|
const match = matchTableSchema(variant.columns);
|
||||||
|
assert.deepEqual(
|
||||||
|
match,
|
||||||
|
{ id, label: variant.label },
|
||||||
|
`expected ${id}/${variant.label} to resolve for columns ${JSON.stringify(variant.columns)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for an unknown header', () => {
|
||||||
|
const match = matchTableSchema(['Foo', 'Bar', 'Baz']);
|
||||||
|
assert.equal(match, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when column order differs from every variant', () => {
|
||||||
|
const match = matchTableSchema(['Status', 'Phase', 'Plans Complete', 'Completed']);
|
||||||
|
assert.equal(match, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when column count differs from every variant', () => {
|
||||||
|
const match = matchTableSchema(['Phase', 'Plans Complete', 'Status']);
|
||||||
|
assert.equal(match, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Property test: round-trip render -> parse ────────────────────────────────
|
||||||
|
|
||||||
|
describe('parseMarkdownTable: property-based round-trip', () => {
|
||||||
|
// Safe cell text: no '|' or newline, non-empty, bounded length.
|
||||||
|
const safeCell = fc
|
||||||
|
.string({ minLength: 1, maxLength: 8 })
|
||||||
|
.filter((s) => !s.includes('|') && !s.includes('\n') && !s.includes('\r') && s.trim().length > 0)
|
||||||
|
.map((s) => s.trim());
|
||||||
|
|
||||||
|
const safeColumnName = fc
|
||||||
|
.string({ minLength: 1, maxLength: 6 })
|
||||||
|
.filter((s) => !s.includes('|') && !s.includes('\n') && !s.includes('\r') && s.trim().length > 0)
|
||||||
|
.map((s) => s.trim());
|
||||||
|
|
||||||
|
function renderTable(columns, rows) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`| ${columns.join(' | ')} |`);
|
||||||
|
lines.push(`| ${columns.map(() => '---').join(' | ')} |`);
|
||||||
|
for (const row of rows) {
|
||||||
|
lines.push(`| ${row.join(' | ')} |`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// A table's rows depend on its column count, so derive the rows arbitrary
|
||||||
|
// from the generated columns via .chain() (dependent arbitrary generation) —
|
||||||
|
// never fc.sample() inside a property, which breaks shrinking/reproducibility.
|
||||||
|
const tableArb = fc.uniqueArray(safeColumnName, { minLength: 1, maxLength: 4 }).chain((columns) =>
|
||||||
|
fc.tuple(
|
||||||
|
fc.constant(columns),
|
||||||
|
fc.array(fc.array(safeCell, { minLength: columns.length, maxLength: columns.length }), { maxLength: 4 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
test('property: rendering a table then parsing it round-trips columns and row values', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(tableArb, ([columns, rows]) => {
|
||||||
|
const src = renderTable(columns, rows);
|
||||||
|
|
||||||
|
const result = parseMarkdownTable(src);
|
||||||
|
assert.equal(result.ok, true, `expected ok:true, got ${JSON.stringify(result)}`);
|
||||||
|
assert.deepEqual(result.value.columns, columns);
|
||||||
|
assert.equal(result.value.rows.length, rows.length);
|
||||||
|
rows.forEach((cells, i) => {
|
||||||
|
columns.forEach((col, j) => {
|
||||||
|
assert.equal(result.value.rows[i][col], cells[j]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── appendQuickTaskRow (#2133) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('appendQuickTaskRow (#2133)', () => {
|
||||||
|
const noStatusState = [
|
||||||
|
'# STATE',
|
||||||
|
'',
|
||||||
|
'### Quick Tasks Completed',
|
||||||
|
'',
|
||||||
|
'| # | Description | Date | Commit | Directory |',
|
||||||
|
'|---|-------------|------|--------|-----------|',
|
||||||
|
'| 1 | fix typo | 2026-01-01 | abc1234 | — |',
|
||||||
|
'',
|
||||||
|
'### Blockers/Concerns',
|
||||||
|
'None',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const withStatusState = [
|
||||||
|
'# STATE',
|
||||||
|
'',
|
||||||
|
'### Quick Tasks Completed',
|
||||||
|
'',
|
||||||
|
'| # | Description | Date | Commit | Status | Directory |',
|
||||||
|
'|---|-------------|------|--------|--------|-----------|',
|
||||||
|
'| 1 | fix typo | 2026-01-01 | abc1234 | Pass | — |',
|
||||||
|
'',
|
||||||
|
'### Blockers/Concerns',
|
||||||
|
'None',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
test('5-col no-status table: appends a 5-cell row, content contains it, variant is no-status', () => {
|
||||||
|
const result = appendQuickTaskRow(noStatusState, {
|
||||||
|
description: 'add missing import',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'a574966',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.value.variant, 'no-status');
|
||||||
|
assert.equal(result.value.row, '| 2 | add missing import | 2026-07-13 | a574966 | — |');
|
||||||
|
assert.ok(result.value.content.includes(result.value.row));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('6-col with-status table: appends a 6-cell row, variant is with-status', () => {
|
||||||
|
const result = appendQuickTaskRow(withStatusState, {
|
||||||
|
description: 'bump version',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'b6fc5f6',
|
||||||
|
status: 'Needs Review',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.value.variant, 'with-status');
|
||||||
|
assert.equal(result.value.row, '| 2 | bump version | 2026-07-13 | b6fc5f6 | Needs Review | — |');
|
||||||
|
assert.ok(result.value.content.includes(result.value.row));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown/garbled header (4-col table): fails loud with a reason instead of silently skipping', () => {
|
||||||
|
const garbled = [
|
||||||
|
'# STATE',
|
||||||
|
'',
|
||||||
|
'### Quick Tasks Completed',
|
||||||
|
'',
|
||||||
|
'| Foo | Bar | Baz | Qux |',
|
||||||
|
'|---|---|---|---|',
|
||||||
|
'| 1 | 2 | 3 | 4 |',
|
||||||
|
].join('\n');
|
||||||
|
const result = appendQuickTaskRow(garbled, { description: 'x', date: '2026-07-13', commit: 'abc' });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.reason, /unrecognized Quick Tasks schema/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no "Quick Tasks Completed" section: fails loud with a reason', () => {
|
||||||
|
const noSection = '# STATE\n\n### Blockers/Concerns\nNone\n';
|
||||||
|
const result = appendQuickTaskRow(noSection, { description: 'x', date: '2026-07-13', commit: 'abc' });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.reason, /no Quick Tasks Completed section/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('boundary: next row number is 1 with zero data rows, 3 with two data rows', () => {
|
||||||
|
const zeroRows = [
|
||||||
|
'# STATE',
|
||||||
|
'',
|
||||||
|
'### Quick Tasks Completed',
|
||||||
|
'',
|
||||||
|
'| # | Description | Date | Commit | Directory |',
|
||||||
|
'|---|-------------|------|--------|-----------|',
|
||||||
|
].join('\n');
|
||||||
|
const zeroResult = appendQuickTaskRow(zeroRows, { description: 'x', date: '2026-07-13', commit: 'abc' });
|
||||||
|
assert.equal(zeroResult.ok, true);
|
||||||
|
assert.match(zeroResult.value.row, /^\| 1 \|/);
|
||||||
|
|
||||||
|
const twoRows = [
|
||||||
|
'# STATE',
|
||||||
|
'',
|
||||||
|
'### Quick Tasks Completed',
|
||||||
|
'',
|
||||||
|
'| # | Description | Date | Commit | Directory |',
|
||||||
|
'|---|-------------|------|--------|-----------|',
|
||||||
|
'| 1 | first | 2026-01-01 | aaa1111 | — |',
|
||||||
|
'| 2 | second | 2026-01-02 | bbb2222 | — |',
|
||||||
|
].join('\n');
|
||||||
|
const twoResult = appendQuickTaskRow(twoRows, { description: 'x', date: '2026-07-13', commit: 'abc' });
|
||||||
|
assert.equal(twoResult.ok, true);
|
||||||
|
assert.match(twoResult.value.row, /^\| 3 \|/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('appended row cell count equals the header column count (round-trips via parseMarkdownTable)', () => {
|
||||||
|
const result = appendQuickTaskRow(noStatusState, {
|
||||||
|
description: 'round trip check',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'ccc3333',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
const section = result.value.content.split('### Blockers/Concerns')[0];
|
||||||
|
const reparsed = parseMarkdownTable(section);
|
||||||
|
assert.equal(reparsed.ok, true);
|
||||||
|
assert.equal(reparsed.value.rows.length, 2);
|
||||||
|
for (const row of reparsed.value.rows) {
|
||||||
|
assert.equal(Object.keys(row).length, reparsed.value.columns.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Regression: cell-value escaping (#2242 review Fix 1) ──────────────────
|
||||||
|
// A raw `|` or newline in `description` used to be inserted verbatim,
|
||||||
|
// corrupting the table (extra column / a fake extra row) — the now-fail-loud
|
||||||
|
// parseMarkdownTable rejects the resulting ragged row.
|
||||||
|
|
||||||
|
test('description containing "|" round-trips: ok:true, no ragged row, cell value preserved', () => {
|
||||||
|
const result = appendQuickTaskRow(noStatusState, {
|
||||||
|
description: 'fix a | b bug',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'a574966',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
|
||||||
|
const section = result.value.content.split('### Blockers/Concerns')[0];
|
||||||
|
const reparsed = parseMarkdownTable(section);
|
||||||
|
assert.equal(reparsed.ok, true, `expected ok:true (not a ragged-row error), got ${JSON.stringify(reparsed)}`);
|
||||||
|
assert.equal(reparsed.value.rows.length, 2);
|
||||||
|
assert.equal(reparsed.value.rows[1]['Description'], 'fix a | b bug');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Regression: backslash escaping (CodeQL js/incomplete-sanitization) ────
|
||||||
|
// escapeCell used to escape `|` -> `\|` without first escaping a literal `\`,
|
||||||
|
// so a description with a raw backslash (e.g. a Windows path) could produce
|
||||||
|
// an escape sequence that splitTableRow misreads on unescape. escapeCell now
|
||||||
|
// escapes `\` -> `\\` before `|` -> `\|`, and splitTableRow reverses both.
|
||||||
|
|
||||||
|
test('description containing a literal backslash round-trips byte-for-byte', () => {
|
||||||
|
const result = appendQuickTaskRow(noStatusState, {
|
||||||
|
description: 'fix C:\\path bug',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'a574966',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
|
||||||
|
const section = result.value.content.split('### Blockers/Concerns')[0];
|
||||||
|
const reparsed = parseMarkdownTable(section);
|
||||||
|
assert.equal(reparsed.ok, true, `expected ok:true (not a ragged-row error), got ${JSON.stringify(reparsed)}`);
|
||||||
|
assert.equal(reparsed.value.rows.length, 2);
|
||||||
|
assert.equal(reparsed.value.rows[1]['Description'], 'fix C:\\path bug');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('description containing backslash-pipe ("a\\|b") round-trips to exactly "a\\|b"', () => {
|
||||||
|
const result = appendQuickTaskRow(noStatusState, {
|
||||||
|
description: 'a\\|b',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'a574966',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
|
||||||
|
const section = result.value.content.split('### Blockers/Concerns')[0];
|
||||||
|
const reparsed = parseMarkdownTable(section);
|
||||||
|
assert.equal(reparsed.ok, true, `expected ok:true (not a ragged-row error), got ${JSON.stringify(reparsed)}`);
|
||||||
|
assert.equal(reparsed.value.rows.length, 2);
|
||||||
|
assert.equal(reparsed.value.rows[1]['Description'], 'a\\|b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('description containing a newline collapses to a single-line cell and round-trips', () => {
|
||||||
|
const result = appendQuickTaskRow(noStatusState, {
|
||||||
|
description: 'line one\nline two',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'a574966',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
// Collapsed to a single line: the row itself must not contain a newline.
|
||||||
|
assert.ok(!result.value.row.includes('\n'));
|
||||||
|
|
||||||
|
const section = result.value.content.split('### Blockers/Concerns')[0];
|
||||||
|
const reparsed = parseMarkdownTable(section);
|
||||||
|
assert.equal(reparsed.ok, true, `expected ok:true (not a ragged-row error), got ${JSON.stringify(reparsed)}`);
|
||||||
|
assert.equal(reparsed.value.rows.length, 2);
|
||||||
|
assert.equal(reparsed.value.rows[1]['Description'], 'line one line two');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Regression: CRLF preservation (#2242 review Fix 3) ─────────────────────
|
||||||
|
// section.body used to be split on /\r?\n/ and rejoined with '\n', downgrading
|
||||||
|
// a CRLF section to mixed EOL.
|
||||||
|
|
||||||
|
test('CRLF-input STATE.md keeps \\r\\n in the touched section (no mixed EOL)', () => {
|
||||||
|
const crlfState = noStatusState.replace(/\n/g, '\r\n');
|
||||||
|
const result = appendQuickTaskRow(crlfState, {
|
||||||
|
description: 'crlf check',
|
||||||
|
date: '2026-07-13',
|
||||||
|
commit: 'a574966',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
|
||||||
|
const section = result.value.content.split('### Blockers/Concerns')[0];
|
||||||
|
// No mixed EOL: every line break in the touched section is \r\n, and there
|
||||||
|
// must be no bare \n (i.e. no \n NOT preceded by \r).
|
||||||
|
assert.ok(!/(?<!\r)\n/.test(section), 'expected no bare \\n (mixed EOL) in the touched section');
|
||||||
|
assert.ok(section.includes('\r\n'), 'expected \\r\\n to be preserved in the touched section');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── findTableBySchema (#2242 review Fix 4) ────────────────────────────────────
|
||||||
|
|
||||||
|
describe('findTableBySchema', () => {
|
||||||
|
test('finds a RoadmapProgress table that appears after other content, not under a "## Progress" heading', () => {
|
||||||
|
const doc = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'## Overview',
|
||||||
|
'',
|
||||||
|
'Some prose describing the roadmap. No table here.',
|
||||||
|
'',
|
||||||
|
'## Milestone v1.0: Test',
|
||||||
|
'',
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | 1/2 | In Progress | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const table = findTableBySchema(doc, 'RoadmapProgress');
|
||||||
|
assert.notEqual(table, null);
|
||||||
|
assert.deepEqual(table.columns, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
||||||
|
assert.equal(table.rows.length, 2);
|
||||||
|
assert.equal(table.rows[0]['Phase'], '1. Alpha');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finds a RoadmapProgress table that is not the first table in the document', () => {
|
||||||
|
const doc = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'## Legend',
|
||||||
|
'',
|
||||||
|
'| Symbol | Meaning |',
|
||||||
|
'| --- | --- |',
|
||||||
|
'| ✅ | Done |',
|
||||||
|
'',
|
||||||
|
'## Progress',
|
||||||
|
'',
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const table = findTableBySchema(doc, 'RoadmapProgress');
|
||||||
|
assert.notEqual(table, null);
|
||||||
|
assert.equal(table.rows.length, 1);
|
||||||
|
assert.equal(table.rows[0]['Phase'], '1. Alpha');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when no table matches the given schema', () => {
|
||||||
|
const doc = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'| Symbol | Meaning |',
|
||||||
|
'| --- | --- |',
|
||||||
|
'| ✅ | Done |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
assert.equal(findTableBySchema(doc, 'RoadmapProgress'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for non-string input', () => {
|
||||||
|
assert.equal(findTableBySchema(undefined, 'RoadmapProgress'), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── findTableWithColumns (#2242: column-order/count-invariant reader seam) ──
|
||||||
|
|
||||||
|
describe('findTableWithColumns', () => {
|
||||||
|
test('finds a table whose header has the required columns in shuffled order, plus extra/injected columns', () => {
|
||||||
|
const doc = [
|
||||||
|
'## Progress',
|
||||||
|
'',
|
||||||
|
'| Status | Foo | Phase | Plans Complete | Completed |',
|
||||||
|
'| --- | --- | --- | --- | --- |',
|
||||||
|
'| Complete | x | 1. Alpha | 2/2 | ✅ |',
|
||||||
|
'| In Progress | x | 2. Beta | 1/2 | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const table = findTableWithColumns(doc, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
||||||
|
assert.notEqual(table, null);
|
||||||
|
assert.deepEqual(table.columns, ['Status', 'Foo', 'Phase', 'Plans Complete', 'Completed']);
|
||||||
|
assert.equal(table.rows.length, 2);
|
||||||
|
assert.equal(table.rows[0]['Phase'], '1. Alpha');
|
||||||
|
assert.equal(table.rows[0]['Status'], 'Complete');
|
||||||
|
assert.equal(table.rows[1]['Plans Complete'], '1/2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when a required column is absent from every table header', () => {
|
||||||
|
const doc = [
|
||||||
|
'## Progress',
|
||||||
|
'',
|
||||||
|
'| Phase | Owner | Completed |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
'| 1. Alpha | jo | ✅ |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
assert.equal(findTableWithColumns(doc, ['Phase', 'Plans Complete', 'Status', 'Completed']), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for non-string input', () => {
|
||||||
|
assert.equal(findTableWithColumns(undefined, ['Phase']), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── PARITY / DRIFT guard: registry <-> template/workflow source files ───────
|
||||||
|
|
||||||
|
describe('TABLE_SCHEMAS parity: registry headers must appear verbatim in their source templates', () => {
|
||||||
|
/**
|
||||||
|
* Build the `| a | b | c |` header line for a variant and assert the given
|
||||||
|
* source file contains it verbatim (whitespace around pipes normalized so
|
||||||
|
* template formatting quirks don't cause false failures).
|
||||||
|
*/
|
||||||
|
function assertHeaderInFile(relPath, variant) {
|
||||||
|
const fullPath = path.join(ROOT, relPath);
|
||||||
|
const content = fs.readFileSync(fullPath, 'utf8'); // allow-test-rule: runtime-contract-is-the-product — template/registry parity (#2242)
|
||||||
|
const expectedHeader = `| ${variant.columns.join(' | ')} |`;
|
||||||
|
const normalize = (s) => s.replace(/[ \t]*\|[ \t]*/g, '|').trim();
|
||||||
|
const normalizedExpected = normalize(expectedHeader);
|
||||||
|
const found = content
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.some((line) => normalize(line) === normalizedExpected);
|
||||||
|
assert.ok(
|
||||||
|
found,
|
||||||
|
`expected header ${JSON.stringify(expectedHeader)} to appear verbatim in ${relPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('RoadmapProgress variants appear in gsd-core/templates/roadmap.md', () => {
|
||||||
|
for (const variant of TABLE_SCHEMAS.RoadmapProgress) {
|
||||||
|
assertHeaderInFile('gsd-core/templates/roadmap.md', variant);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RequirementsTraceability variant appears in gsd-core/templates/requirements.md', () => {
|
||||||
|
for (const variant of TABLE_SCHEMAS.RequirementsTraceability) {
|
||||||
|
assertHeaderInFile('gsd-core/templates/requirements.md', variant);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('QuickTasks variants appear in gsd-core/workflows/quick.md', () => {
|
||||||
|
for (const variant of TABLE_SCHEMAS.QuickTasks) {
|
||||||
|
assertHeaderInFile('gsd-core/workflows/quick.md', variant);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Security variants appear in gsd-core/templates/SECURITY.md', () => {
|
||||||
|
for (const variant of TABLE_SCHEMAS.Security) {
|
||||||
|
assertHeaderInFile('gsd-core/templates/SECURITY.md', variant);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
184
tests/phase-lifecycle.test.cjs
Normal file
184
tests/phase-lifecycle.test.cjs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavioral tests for phase-lifecycle.cjs
|
||||||
|
*
|
||||||
|
* Module: gsd-core/bin/lib/phase-lifecycle.cjs
|
||||||
|
* Exports: deriveProgressFromRoadmap, clampPercent
|
||||||
|
*
|
||||||
|
* ADR-2143 (epic #2143) migrated deriveProgressFromRoadmap from position-based
|
||||||
|
* regexes to the markdown-table schema registry (collectSection + parseMarkdownTable
|
||||||
|
* + matchTableSchema against TABLE_SCHEMAS.RoadmapProgress). This suite pins:
|
||||||
|
* - the pre-existing 4-column flat Progress table behaviour (unchanged)
|
||||||
|
* - #2137: the 5-column milestone-grouped Progress table (Phase | Milestone |
|
||||||
|
* Plans Complete | Status | Completed) — the OLD position-anchored regex
|
||||||
|
* assumed "Status" was always the 3rd cell, so it silently returned all-null
|
||||||
|
* for this variant; the schema-registry rewrite reads cells by column NAME
|
||||||
|
* and fixes this.
|
||||||
|
* - the pre-existing 999.x backlog exclusion for totalPhases
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, describe } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const { deriveProgressFromRoadmap, clampPercent } = require('../gsd-core/bin/lib/phase-lifecycle.cjs');
|
||||||
|
|
||||||
|
describe('deriveProgressFromRoadmap', () => {
|
||||||
|
test('parses the 4-column flat Progress table (behaviour preserved)', () => {
|
||||||
|
const roadmap = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'## Progress',
|
||||||
|
'',
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | 1/2 | In Progress | |',
|
||||||
|
'| 3. Gamma | 0/1 | Planned | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = deriveProgressFromRoadmap(roadmap);
|
||||||
|
assert.equal(result.completedPhases, 1, `expected 1 completed phase, got ${result.completedPhases}`);
|
||||||
|
assert.equal(result.totalPhases, 3, `expected 3 total phases, got ${result.totalPhases}`);
|
||||||
|
assert.equal(result.totalPlans, 5, `expected totalPlans 5 (2+2+1), got ${result.totalPlans}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#2137: deriveProgressFromRoadmap parses the 5-column milestone-grouped Progress table', () => {
|
||||||
|
// Before ADR-2143: the old position-anchored regex assumed the 3rd cell was
|
||||||
|
// Status; here Status is the 4th cell (Milestone inserted at position 2), so
|
||||||
|
// the old code silently returned { completedPhases: null, totalPhases: null }.
|
||||||
|
const roadmap = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'## Progress',
|
||||||
|
'',
|
||||||
|
'| Phase | Milestone | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | v1.0 | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | v1.0 | 1/2 | In Progress | |',
|
||||||
|
'| 3. Gamma | v1.1 | 0/1 | Planned | |',
|
||||||
|
'| 4. Delta | v1.1 | 3/3 | Complete | ✅ |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = deriveProgressFromRoadmap(roadmap);
|
||||||
|
assert.equal(result.completedPhases, 2, `expected 2 completed phases (non-null), got ${result.completedPhases}`);
|
||||||
|
assert.equal(result.totalPhases, 4, `expected 4 total phases (non-null), got ${result.totalPhases}`);
|
||||||
|
assert.equal(result.totalPlans, 8, `expected totalPlans 8 (2+2+1+3), got ${result.totalPlans}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('excludes 999.x backlog rows from totalPhases (4-column table)', () => {
|
||||||
|
const roadmap = [
|
||||||
|
'## Milestone v1.0: Test',
|
||||||
|
'',
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | 1/2 | In Progress | |',
|
||||||
|
'| 3. Gamma | 0/1 | Planned | |',
|
||||||
|
'| 999.1 Backlog: Future Idea | 0/0 | Backlog | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = deriveProgressFromRoadmap(roadmap);
|
||||||
|
assert.equal(
|
||||||
|
result.totalPhases,
|
||||||
|
3,
|
||||||
|
`total_phases must be 3 (not 4) — 999.1 backlog row must be excluded. Got ${result.totalPhases}`,
|
||||||
|
);
|
||||||
|
assert.equal(result.completedPhases, 1, `completed_phases must be 1. Got ${result.completedPhases}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('excludes 999.x backlog rows from totalPhases (5-column milestone-grouped table)', () => {
|
||||||
|
const roadmap = [
|
||||||
|
'## Progress',
|
||||||
|
'',
|
||||||
|
'| Phase | Milestone | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | v1.0 | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | v1.0 | 1/2 | In Progress | |',
|
||||||
|
'| 999.1 Backlog | v1.0 | 0/0 | Backlog | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = deriveProgressFromRoadmap(roadmap);
|
||||||
|
assert.equal(
|
||||||
|
result.totalPhases,
|
||||||
|
2,
|
||||||
|
`total_phases must be 2 (not 3) — 999.1 backlog row must be excluded. Got ${result.totalPhases}`,
|
||||||
|
);
|
||||||
|
assert.equal(result.completedPhases, 1, `completed_phases must be 1. Got ${result.completedPhases}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-table content returns all-null (no throw)', () => {
|
||||||
|
const result = deriveProgressFromRoadmap('# Roadmap\n\nNo table here.\n');
|
||||||
|
assert.equal(result.completedPhases, null);
|
||||||
|
assert.equal(result.totalPhases, null);
|
||||||
|
assert.equal(result.totalPlans, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Regression (#2242 review Fix 4): heading-dependency removed ───────────
|
||||||
|
// The Progress table used to be located via collectSection(h => /^progress$/i)
|
||||||
|
// first, THEN parsed — so a schema-matching table under a non-"Progress"
|
||||||
|
// heading (or one that isn't the first table in the document) returned
|
||||||
|
// all-null. deriveProgressFromRoadmap now delegates to findTableBySchema,
|
||||||
|
// which scans the whole document for the schema, independent of heading name.
|
||||||
|
|
||||||
|
test('Progress table under a NON-"Progress" heading, not the first table in the doc, still resolves', () => {
|
||||||
|
const roadmap = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'## Legend',
|
||||||
|
'',
|
||||||
|
'| Symbol | Meaning |',
|
||||||
|
'| --- | --- |',
|
||||||
|
'| ✅ | Done |',
|
||||||
|
'',
|
||||||
|
'## Milestone v1.0: Alpha Release',
|
||||||
|
'',
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
'| 2. Beta | 1/2 | In Progress | |',
|
||||||
|
'| 3. Gamma | 0/1 | Planned | |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = deriveProgressFromRoadmap(roadmap);
|
||||||
|
assert.equal(result.completedPhases, 1, `expected 1 completed phase (non-null), got ${result.completedPhases}`);
|
||||||
|
assert.equal(result.totalPhases, 3, `expected 3 total phases (non-null), got ${result.totalPhases}`);
|
||||||
|
assert.equal(result.totalPlans, 5, `expected totalPlans 5 (2+2+1), got ${result.totalPlans}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Progress table with NO heading at all, not the first table in the doc, still resolves', () => {
|
||||||
|
const roadmap = [
|
||||||
|
'# Roadmap',
|
||||||
|
'',
|
||||||
|
'Some intro prose describing conventions used below.',
|
||||||
|
'',
|
||||||
|
'| Symbol | Meaning |',
|
||||||
|
'| --- | --- |',
|
||||||
|
'| ✅ | Done |',
|
||||||
|
'',
|
||||||
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
|
'| --- | --- | --- | --- |',
|
||||||
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = deriveProgressFromRoadmap(roadmap);
|
||||||
|
assert.equal(result.completedPhases, 1, `expected 1 completed phase (non-null), got ${result.completedPhases}`);
|
||||||
|
assert.equal(result.totalPhases, 1, `expected 1 total phase (non-null), got ${result.totalPhases}`);
|
||||||
|
assert.equal(result.totalPlans, 2, `expected totalPlans 2, got ${result.totalPlans}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clampPercent', () => {
|
||||||
|
test('computes a normal percentage', () => {
|
||||||
|
assert.equal(clampPercent(1, 2), 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clamps to 100 when completed exceeds total', () => {
|
||||||
|
assert.equal(clampPercent(5, 2), 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 0 when total is 0 or negative', () => {
|
||||||
|
assert.equal(clampPercent(0, 0), 0);
|
||||||
|
assert.equal(clampPercent(3, -1), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -441,25 +441,27 @@ describe('bug #3805: fast.md log_to_state must be schema-aware', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('fast.md log_to_state step references the 5-column quick.md schema', () => {
|
test('fast.md log_to_state step references the 5-column quick.md schema', () => {
|
||||||
// The fix must handle the 5-col schema: | # | Description | Date | Commit | Directory |
|
// Schema-awareness no longer lives inline in fast.md as hardcoded column
|
||||||
// Test that all 5 column names appear in the log_to_state step's context.
|
// names — it was moved to the schema-backed `quick-tasks-append` helper
|
||||||
// Extract the log_to_state step content to scope the check.
|
// (`appendQuickTaskRow` in markdown-table.cjs; #2133, ADR-2143 §3/§7),
|
||||||
|
// which introspects the existing table's schema (5-col or 6-col) itself.
|
||||||
|
// Assert the step delegates to that helper instead of requiring the old
|
||||||
|
// literal column names.
|
||||||
const logToStateMatch = fastMdContent.match(/<step name="log_to_state">([\s\S]*?)<\/step>/);
|
const logToStateMatch = fastMdContent.match(/<step name="log_to_state">([\s\S]*?)<\/step>/);
|
||||||
assert.ok(logToStateMatch, 'fast.md must contain a <step name="log_to_state"> element');
|
assert.ok(logToStateMatch, 'fast.md must contain a <step name="log_to_state"> element');
|
||||||
|
|
||||||
const stepContent = logToStateMatch[1];
|
const stepContent = logToStateMatch[1];
|
||||||
|
|
||||||
// All 5 column names from quick.md Step 7 must be referenced in the step.
|
assert.ok(
|
||||||
for (const col of QUICK_MD_STEP7_COLUMNS) {
|
stepContent.includes('quick-tasks-append'),
|
||||||
assert.ok(
|
[
|
||||||
stepContent.toLowerCase().includes(col.toLowerCase()),
|
'fast.md log_to_state step does not invoke the schema-aware quick-tasks-append helper.',
|
||||||
[
|
'Schema-awareness now lives in the gsd-tools quick-tasks-append subcommand',
|
||||||
`fast.md log_to_state step does not reference column "${col}"`,
|
`(appendQuickTaskRow, handling both the ${QUICK_MD_STEP7_COL_COUNT}-column quick.md Step 7 schema`,
|
||||||
`Expected all 5 columns from quick.md Step 7: ${QUICK_MD_STEP7_COLUMNS.join(', ')}`,
|
`${QUICK_MD_STEP7_COLUMNS.join(', ')} and the 6-column with-status variant) —`,
|
||||||
`(${QUICK_MD_STEP7_COL_COUNT}-column schema)`,
|
'the step must delegate to it rather than hardcoding column names inline.',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fast.md log_to_state step skips STATE.md write on unrecognized schema', () => {
|
test('fast.md log_to_state step skips STATE.md write on unrecognized schema', () => {
|
||||||
|
|||||||
@@ -541,10 +541,15 @@ function completePhaseBody() {
|
|||||||
|
|
||||||
// A roadmap with a progress table: 3 of 5 phases Complete → deriveProgressFromRoadmap
|
// A roadmap with a progress table: 3 of 5 phases Complete → deriveProgressFromRoadmap
|
||||||
// returns { completedPhases: 3, totalPhases: 5 }.
|
// returns { completedPhases: 3, totalPhases: 5 }.
|
||||||
|
// ADR-2143 (epic #2143): deriveProgressFromRoadmap now resolves this table via the
|
||||||
|
// markdown-table schema registry (TABLE_SCHEMAS.RoadmapProgress), which requires the
|
||||||
|
// exact canonical header (gsd-core/templates/roadmap.md); the 2nd column is named
|
||||||
|
// "Plans Complete" to match (its cell values here are unused free text, not M/N
|
||||||
|
// counts — no test in this file asserts totalPlans).
|
||||||
const ROADMAP_3_OF_5 = [
|
const ROADMAP_3_OF_5 = [
|
||||||
'## Roadmap',
|
'## Roadmap',
|
||||||
'',
|
'',
|
||||||
'| Phase | Title | Status | Completed |',
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
'| --- | --- | --- | --- |',
|
'| --- | --- | --- | --- |',
|
||||||
'| 1 | A | Complete | 2026-01-01 |',
|
'| 1 | A | Complete | 2026-01-01 |',
|
||||||
'| 2 | B | Complete | 2026-02-01 |',
|
'| 2 | B | Complete | 2026-02-01 |',
|
||||||
|
|||||||
@@ -9487,13 +9487,20 @@ const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|||||||
const { deriveProgressFromRoadmap } = require('../gsd-core/bin/lib/phase-lifecycle.cjs');
|
const { deriveProgressFromRoadmap } = require('../gsd-core/bin/lib/phase-lifecycle.cjs');
|
||||||
|
|
||||||
// ─── Scenario A: deriveProgressFromRoadmap unit test ────────────────────────
|
// ─── 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', () => {
|
describe('bug #1445 — deriveProgressFromRoadmap excludes 999.x rows', () => {
|
||||||
test('3 real phases + 1 999.x backlog row → total_phases: 3, not 4', () => {
|
test('3 real phases + 1 999.x backlog row → total_phases: 3, not 4', () => {
|
||||||
const roadmap = [
|
const roadmap = [
|
||||||
'## Milestone v1.0: Test',
|
'## Milestone v1.0: Test',
|
||||||
'',
|
'',
|
||||||
'| Phase | Plans | Status | Completed |',
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
'| --- | --- | --- | --- |',
|
'| --- | --- | --- | --- |',
|
||||||
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
'| 1. Alpha | 2/2 | Complete | ✅ |',
|
||||||
'| 2. Beta | 1/2 | In Progress | |',
|
'| 2. Beta | 1/2 | In Progress | |',
|
||||||
@@ -9518,7 +9525,7 @@ describe('bug #1445 — deriveProgressFromRoadmap excludes 999.x rows', () => {
|
|||||||
const roadmap = [
|
const roadmap = [
|
||||||
'## Milestone v1.0: Test',
|
'## Milestone v1.0: Test',
|
||||||
'',
|
'',
|
||||||
'| Phase | Plans | Status | Completed |',
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
'| --- | --- | --- | --- |',
|
'| --- | --- | --- | --- |',
|
||||||
'| 1. Alpha | 1/1 | Complete | ✅ |',
|
'| 1. Alpha | 1/1 | Complete | ✅ |',
|
||||||
'| 2. Beta | 1/1 | Complete | ✅ |',
|
'| 2. Beta | 1/1 | Complete | ✅ |',
|
||||||
@@ -9542,7 +9549,7 @@ describe('bug #1445 — deriveProgressFromRoadmap excludes 999.x rows', () => {
|
|||||||
const roadmap = [
|
const roadmap = [
|
||||||
'## Milestone v1.0: Test',
|
'## Milestone v1.0: Test',
|
||||||
'',
|
'',
|
||||||
'| Phase | Plans | Status | Completed |',
|
'| Phase | Plans Complete | Status | Completed |',
|
||||||
'| --- | --- | --- | --- |',
|
'| --- | --- | --- | --- |',
|
||||||
'| 999.1 Future A | 0/0 | Backlog | |',
|
'| 999.1 Future A | 0/0 | Backlog | |',
|
||||||
'| 999.2 Future B | 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', () => {
|
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
|
// (#2242 review Fix 5 / ADR-2143 §3): deriveProgressFromRoadmap now resolves
|
||||||
// column is ignored and a SHORT row simply has absent Status/Plans cells
|
// the Progress table via the markdown-table seam's parseMarkdownTable, which
|
||||||
// (`cells[idx] ?? ''`) — neither should throw or corrupt the well-formed row.
|
// 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 = [
|
const roadmap = [
|
||||||
'## Progress',
|
'## Progress',
|
||||||
'',
|
'',
|
||||||
@@ -9737,13 +9748,10 @@ describe('#2137 regression: deriveProgressFromRoadmap parses the milestone-group
|
|||||||
assert.doesNotThrow(() => {
|
assert.doesNotThrow(() => {
|
||||||
result = deriveProgressFromRoadmap(roadmap);
|
result = deriveProgressFromRoadmap(roadmap);
|
||||||
}, 'ragged rows must not throw');
|
}, '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(
|
assert.deepEqual(
|
||||||
result,
|
result,
|
||||||
{ completedPhases: 1, totalPhases: 2, totalPlans: 2 },
|
{ completedPhases: null, totalPhases: null, totalPlans: null },
|
||||||
`ragged rows must degrade gracefully to {1,2,2}, got ${JSON.stringify(result)}`,
|
`a ragged-row table must fail loud to all-null (no throw), got ${JSON.stringify(result)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"execute-plan.md": 32655,
|
"execute-plan.md": 32655,
|
||||||
"explore.md": 10541,
|
"explore.md": 10541,
|
||||||
"extract-learnings.md": 12893,
|
"extract-learnings.md": 12893,
|
||||||
"fast.md": 4790,
|
"fast.md": 7613,
|
||||||
"forensics.md": 12531,
|
"forensics.md": 12531,
|
||||||
"graduation.md": 11622,
|
"graduation.md": 11622,
|
||||||
"health.md": 11868,
|
"health.md": 11868,
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"pr-branch.md": 15963,
|
"pr-branch.md": 15963,
|
||||||
"profile-user.md": 21246,
|
"profile-user.md": 21246,
|
||||||
"progress.md": 30599,
|
"progress.md": 30599,
|
||||||
"quick.md": 50470,
|
"quick.md": 50699,
|
||||||
"reapply-patches.md": 20312,
|
"reapply-patches.md": 20312,
|
||||||
"remove-phase.md": 8513,
|
"remove-phase.md": 8513,
|
||||||
"remove-workspace.md": 7551,
|
"remove-workspace.md": 7551,
|
||||||
|
|||||||
Reference in New Issue
Block a user