chore(#2143): markdown table model + schema registry + fail-loud pilot — Phase 1 (#2248)

* 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:
Tom Boucher
2026-07-13 20:36:14 -04:00
committed by GitHub
parent db725e49e0
commit d49ac81306
38 changed files with 1397 additions and 376 deletions

View 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
View File

@@ -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

View File

@@ -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`).

View File

@@ -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

View File

@@ -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",

View File

@@ -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 |

View File

@@ -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',

View File

@@ -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

View File

@@ -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>

View File

@@ -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:

View File

@@ -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
View 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).

View File

@@ -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 */ }

View File

@@ -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');
});
});

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View 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);
}
});
});

View 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);
});
});

View File

@@ -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', () => {

View File

@@ -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 |',

View File

@@ -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)}`,
); );
}); });
}); });

View File

@@ -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,