@opengsd/gsd-core 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +85 -5
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +5 -3
- package/agents/gsd-code-reviewer.md +8 -6
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +3 -3
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -12
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +2 -2
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +15 -9
- package/agents/gsd-planner.md +15 -11
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +21 -30
- package/agents/gsd-ui-auditor.md +166 -37
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +37 -14
- package/bin/install.js +764 -287
- package/commands/gsd/add-tests.md +6 -1
- package/commands/gsd/ai-integration-phase.md +6 -1
- package/commands/gsd/audit-fix.md +5 -0
- package/commands/gsd/audit-milestone.md +6 -1
- package/commands/gsd/autonomous.md +7 -2
- package/commands/gsd/capture.md +9 -5
- package/commands/gsd/code-review.md +7 -2
- package/commands/gsd/complete-milestone.md +4 -0
- package/commands/gsd/config.md +7 -3
- package/commands/gsd/debug.md +11 -7
- package/commands/gsd/discuss-phase.md +7 -3
- package/commands/gsd/docs-update.md +12 -7
- package/commands/gsd/eval-review.md +6 -1
- package/commands/gsd/execute-phase.md +12 -7
- package/commands/gsd/extract-learnings.md +5 -0
- package/commands/gsd/fast.md +4 -0
- package/commands/gsd/forensics.md +5 -1
- package/commands/gsd/graphify.md +10 -6
- package/commands/gsd/health.md +5 -0
- package/commands/gsd/help.md +7 -2
- package/commands/gsd/import.md +7 -3
- package/commands/gsd/inbox.md +5 -0
- package/commands/gsd/ingest-docs.md +5 -1
- package/commands/gsd/manager.md +6 -1
- package/commands/gsd/map-codebase.md +7 -3
- package/commands/gsd/mempalace-capture.md +12 -4
- package/commands/gsd/mempalace-recall.md +5 -1
- package/commands/gsd/milestone-summary.md +5 -1
- package/commands/gsd/mvp-phase.md +8 -3
- package/commands/gsd/new-milestone.md +6 -1
- package/commands/gsd/new-project.md +5 -0
- package/commands/gsd/next.md +6 -1
- package/commands/gsd/ns-context.md +4 -0
- package/commands/gsd/ns-ideate.md +4 -0
- package/commands/gsd/ns-manage.md +4 -0
- package/commands/gsd/ns-project.md +4 -0
- package/commands/gsd/ns-review.md +4 -0
- package/commands/gsd/ns-workflow.md +4 -0
- package/commands/gsd/onboard.md +6 -1
- package/commands/gsd/pause-work.md +5 -1
- package/commands/gsd/phase.md +8 -4
- package/commands/gsd/plan-phase.md +6 -1
- package/commands/gsd/plan-review-convergence.md +11 -7
- package/commands/gsd/pr-branch.md +4 -0
- package/commands/gsd/profile-user.md +5 -1
- package/commands/gsd/progress.md +7 -2
- package/commands/gsd/quick-batch.md +21 -9
- package/commands/gsd/quick.md +12 -7
- package/commands/gsd/review.md +7 -4
- package/commands/gsd/secure-phase.md +6 -1
- package/commands/gsd/ship.md +5 -0
- package/commands/gsd/sketch.md +7 -2
- package/commands/gsd/spec-phase.md +5 -1
- package/commands/gsd/spike.md +8 -3
- package/commands/gsd/surface.md +5 -1
- package/commands/gsd/thread.md +4 -0
- package/commands/gsd/ui-phase.md +6 -1
- package/commands/gsd/ui-review.md +6 -1
- package/commands/gsd/ultraplan-phase.md +5 -1
- package/commands/gsd/undo.md +5 -1
- package/commands/gsd/update.md +6 -2
- package/commands/gsd/validate-phase.md +6 -1
- package/commands/gsd/verify-work.md +6 -1
- package/commands/gsd/workspace.md +7 -3
- package/gsd-core/bin/gsd-tools.cjs +477 -78
- package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
- package/gsd-core/bin/lib/audit.cjs +144 -42
- package/gsd-core/bin/lib/broken-windows.cjs +13 -13
- package/gsd-core/bin/lib/capability-activation.cjs +9 -4
- package/gsd-core/bin/lib/capability-registry.cjs +197 -222
- package/gsd-core/bin/lib/capability-validator.cjs +16 -1
- package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
- package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
- package/gsd-core/bin/lib/cli-exit.cjs +12 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
- package/gsd-core/bin/lib/command-aliases.cjs +7 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
- package/gsd-core/bin/lib/commands.cjs +343 -207
- package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +76 -19
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/coverage.cjs +4 -8
- package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/drift.cjs +177 -42
- package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
- package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
- package/gsd-core/bin/lib/frontmatter.cjs +426 -234
- package/gsd-core/bin/lib/gap-checker.cjs +72 -29
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
- package/gsd-core/bin/lib/gate-args.cjs +53 -0
- package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
- package/gsd-core/bin/lib/gate-config.cjs +46 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
- package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
- package/gsd-core/bin/lib/gate-exit.cjs +27 -0
- package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
- package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
- package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
- package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
- package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
- package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
- package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
- package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
- package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
- package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
- package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
- package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +364 -132
- package/gsd-core/bin/lib/install-engine.cjs +30 -33
- package/gsd-core/bin/lib/install-profiles.cjs +7 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/io.cjs +122 -3
- package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
- package/gsd-core/bin/lib/milestone.cjs +37 -6
- package/gsd-core/bin/lib/model-resolver.cjs +171 -62
- package/gsd-core/bin/lib/observability/event.cjs +1 -1
- package/gsd-core/bin/lib/observability/logger.cjs +46 -1
- package/gsd-core/bin/lib/pattern.cjs +10 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
- package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +110 -8
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase-status.cjs +360 -0
- package/gsd-core/bin/lib/phase.cjs +489 -88
- package/gsd-core/bin/lib/plan-document.cjs +142 -20
- package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
- package/gsd-core/bin/lib/planning-document.cjs +692 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/profile-output.cjs +6 -3
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
- package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
- package/gsd-core/bin/lib/report-parser.cjs +269 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
- package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
- package/gsd-core/bin/lib/roadmap.cjs +405 -88
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
- package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
- package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
- package/gsd-core/bin/lib/state-contract.cjs +15 -18
- package/gsd-core/bin/lib/state-document.cjs +100 -22
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +256 -105
- package/gsd-core/bin/lib/surface.cjs +19 -2
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
- package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
- package/gsd-core/bin/lib/uat.cjs +432 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/vendor/README.md +31 -9
- package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
- package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +1378 -274
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
- package/gsd-core/bin/lib/verify.cjs +357 -531
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
- package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
- package/gsd-core/bin/lib/workstream.cjs +11 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/references/autonomous-smart-discuss.md +2 -1
- package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-mvp-tdd.md +5 -10
- package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +7 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/offer-next.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +12 -3
- package/gsd-core/references/spidr-splitting.md +1 -1
- package/gsd-core/references/tdd.md +37 -8
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verifier-phase-gates.md +5 -2
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -3
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/UAT.md +17 -1
- package/gsd-core/templates/config.json +2 -11
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +8 -7
- package/gsd-core/workflows/add-tests.md +4 -3
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +13 -4
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +4 -3
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +43 -23
- package/gsd-core/workflows/check-todos.md +7 -6
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
- package/gsd-core/workflows/code-review-fix.md +112 -25
- package/gsd-core/workflows/code-review.md +146 -135
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
- package/gsd-core/workflows/complete-milestone.md +13 -8
- package/gsd-core/workflows/debug.md +32 -7
- package/gsd-core/workflows/diagnose-issues.md +3 -2
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +4 -3
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +4 -3
- package/gsd-core/workflows/eval-review.md +14 -5
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +96 -178
- package/gsd-core/workflows/execute-plan.md +18 -23
- package/gsd-core/workflows/explore.md +4 -4
- package/gsd-core/workflows/extract-learnings.md +4 -2
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +3 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +4 -3
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +4 -3
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +6 -4
- package/gsd-core/workflows/map-codebase.md +5 -4
- package/gsd-core/workflows/milestone-summary.md +3 -2
- package/gsd-core/workflows/mvp-phase.md +14 -14
- package/gsd-core/workflows/new-milestone.md +11 -11
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +2 -2
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +50 -24
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +19 -49
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +15 -10
- package/gsd-core/workflows/quick.md +62 -39
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +3 -2
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +3 -2
- package/gsd-core/workflows/review.md +33 -17
- package/gsd-core/workflows/scan.md +3 -2
- package/gsd-core/workflows/secure-phase.md +13 -13
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +14 -13
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +2 -2
- package/gsd-core/workflows/transition.md +13 -23
- package/gsd-core/workflows/ui-phase.md +48 -11
- package/gsd-core/workflows/ui-review.md +21 -6
- package/gsd-core/workflows/ultraplan-phase.md +3 -2
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +12 -13
- package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
- package/gsd-core/workflows/verify-work.md +129 -55
- package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
- package/hooks/dist/gsd-check-update-worker.js +8 -0
- package/hooks/dist/gsd-check-update.js +8 -0
- package/hooks/dist/gsd-context-monitor.js +31 -8
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
- package/hooks/dist/gsd-secret-read-guard.js +161 -4
- package/hooks/dist/gsd-statusline.js +85 -20
- package/hooks/dist/gsd-update-banner.js +8 -0
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
- package/hooks/dist/gsd-workflow-guard.js +5 -4
- package/hooks/dist/gsd-worktree-path-guard.js +6 -2
- package/hooks/dist/lib/cli-exit.js +12 -0
- package/hooks/dist/lib/git-probe.js +17 -1
- package/hooks/dist/lib/isolation-sentinel.js +2 -2
- package/hooks/gsd-agent-isolation-guard.js +32 -0
- package/hooks/gsd-check-update-worker.js +8 -0
- package/hooks/gsd-check-update.js +8 -0
- package/hooks/gsd-context-monitor.js +31 -8
- package/hooks/gsd-cursor-subagent-start.js +8 -0
- package/hooks/gsd-secret-read-guard.js +161 -4
- package/hooks/gsd-statusline.js +85 -20
- package/hooks/gsd-update-banner.js +8 -0
- package/hooks/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-windsurf-pre-write.js +11 -2
- package/hooks/gsd-workflow-guard.js +5 -4
- package/hooks/gsd-worktree-path-guard.js +6 -2
- package/hooks/hooks.json +5 -5
- package/hooks/lib/cli-exit.js +12 -0
- package/hooks/lib/git-probe.js +17 -1
- package/hooks/lib/isolation-sentinel.js +2 -2
- package/package.json +22 -4
- package/scripts/build-hooks.js +15 -6
- package/scripts/changeset/parse.cjs +52 -4
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/ci-timeout-report.cjs +770 -4
- package/scripts/command-contract-helpers.cjs +15 -8
- package/scripts/docs-guard-registry.cjs +34 -0
- package/scripts/gen-features.cjs +13 -8
- package/scripts/gen-hooks-cli-exit.cjs +12 -28
- package/scripts/gen-loop-host-contract.cjs +79 -1
- package/scripts/gen-platform-conformance-tier.cjs +187 -1
- package/scripts/gen-plugin-skills.cjs +87 -1
- package/scripts/gen-research-agents.cjs +24 -31
- package/scripts/gen-scripts-cli-exit.cjs +30 -3
- package/scripts/gen-test-timings.cjs +32 -7
- package/scripts/lib/cli-exit.cjs +12 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
- package/scripts/lib/ndjson-reporter.cjs +31 -5
- package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
- package/scripts/lib/registration-ledger-preload.cjs +155 -0
- package/scripts/lib/vendor-bundle.cjs +59 -0
- package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-completion-predicate-drift.cjs +18 -19
- package/scripts/lint-descriptions.cjs +7 -3
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
- package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
- package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-enumeration-drift.cjs +12 -8
- package/scripts/lint-phase-id-drift.cjs +319 -5
- package/scripts/lint-planning-document-positive-control.cjs +329 -0
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +3 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-skill-deps.cjs +7 -3
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +38 -9
- package/scripts/lint-test-file-count.cjs +34 -1
- package/scripts/lint-vendored-deps.cjs +41 -5
- package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
- package/scripts/mutation-matrix.cjs +50 -5
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/require-issue-link-policy.cjs +6 -2
- package/scripts/sync-runtime-launcher.cjs +184 -2
- package/scripts/verify-npm-publish.cjs +76 -20
- package/skills/gsd-add-tests/SKILL.md +6 -1
- package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
- package/skills/gsd-audit-fix/SKILL.md +5 -0
- package/skills/gsd-audit-milestone/SKILL.md +6 -1
- package/skills/gsd-autonomous/SKILL.md +7 -2
- package/skills/gsd-capture/SKILL.md +9 -5
- package/skills/gsd-code-review/SKILL.md +7 -2
- package/skills/gsd-complete-milestone/SKILL.md +4 -0
- package/skills/gsd-config/SKILL.md +8 -4
- package/skills/gsd-debug/SKILL.md +11 -7
- package/skills/gsd-discuss-phase/SKILL.md +7 -3
- package/skills/gsd-docs-update/SKILL.md +12 -7
- package/skills/gsd-eval-review/SKILL.md +6 -1
- package/skills/gsd-execute-phase/SKILL.md +12 -7
- package/skills/gsd-extract-learnings/SKILL.md +5 -0
- package/skills/gsd-fast/SKILL.md +4 -0
- package/skills/gsd-forensics/SKILL.md +5 -1
- package/skills/gsd-graphify/SKILL.md +10 -6
- package/skills/gsd-health/SKILL.md +5 -0
- package/skills/gsd-help/SKILL.md +7 -2
- package/skills/gsd-import/SKILL.md +7 -3
- package/skills/gsd-inbox/SKILL.md +5 -0
- package/skills/gsd-ingest-docs/SKILL.md +5 -1
- package/skills/gsd-manager/SKILL.md +6 -1
- package/skills/gsd-map-codebase/SKILL.md +7 -3
- package/skills/gsd-mempalace-capture/SKILL.md +12 -4
- package/skills/gsd-mempalace-recall/SKILL.md +5 -1
- package/skills/gsd-milestone-summary/SKILL.md +5 -1
- package/skills/gsd-mvp-phase/SKILL.md +8 -3
- package/skills/gsd-new-milestone/SKILL.md +6 -1
- package/skills/gsd-new-project/SKILL.md +5 -0
- package/skills/gsd-next/SKILL.md +6 -1
- package/skills/gsd-ns-context/SKILL.md +4 -0
- package/skills/gsd-ns-ideate/SKILL.md +4 -0
- package/skills/gsd-ns-manage/SKILL.md +4 -0
- package/skills/gsd-ns-project/SKILL.md +4 -0
- package/skills/gsd-ns-review/SKILL.md +4 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -0
- package/skills/gsd-onboard/SKILL.md +6 -1
- package/skills/gsd-pause-work/SKILL.md +5 -1
- package/skills/gsd-phase/SKILL.md +8 -4
- package/skills/gsd-plan-phase/SKILL.md +6 -1
- package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
- package/skills/gsd-pr-branch/SKILL.md +4 -0
- package/skills/gsd-profile-user/SKILL.md +5 -1
- package/skills/gsd-progress/SKILL.md +7 -2
- package/skills/gsd-quick/SKILL.md +16 -10
- package/skills/gsd-quick-batch/SKILL.md +21 -9
- package/skills/gsd-review/SKILL.md +7 -4
- package/skills/gsd-review-backlog/SKILL.md +3 -2
- package/skills/gsd-secure-phase/SKILL.md +6 -1
- package/skills/gsd-ship/SKILL.md +5 -0
- package/skills/gsd-sketch/SKILL.md +7 -2
- package/skills/gsd-spec-phase/SKILL.md +5 -1
- package/skills/gsd-spike/SKILL.md +8 -3
- package/skills/gsd-surface/SKILL.md +5 -1
- package/skills/gsd-thread/SKILL.md +4 -0
- package/skills/gsd-ui-phase/SKILL.md +6 -1
- package/skills/gsd-ui-review/SKILL.md +6 -1
- package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
- package/skills/gsd-undo/SKILL.md +5 -1
- package/skills/gsd-update/SKILL.md +6 -2
- package/skills/gsd-validate-phase/SKILL.md +6 -1
- package/skills/gsd-verify-work/SKILL.md +6 -1
- package/skills/gsd-workspace/SKILL.md +7 -3
- package/skills/gsd-workstreams/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Planning Document — the parse -> mutate -> serialize seam for a `.planning/`
|
|
4
|
+
* root artifact BODY (ADR-4910, epic #4906 Phase 1, #4917).
|
|
5
|
+
*
|
|
6
|
+
* Composes the existing structural seams — never reimplements them:
|
|
7
|
+
* - `markdown-sectionizer.cjs` (`tokenizeHeadings`, `collectSections`,
|
|
8
|
+
* `scanFencedBlocks`, `scanInlineCodeSpans`) for headings/sections and
|
|
9
|
+
* fence/inline-code awareness.
|
|
10
|
+
* - `markdown-table.cjs` (`splitTableRow`, `isDelimiterRow`,
|
|
11
|
+
* `parseMarkdownTable`) for GFM table detection and validation.
|
|
12
|
+
* - `artifacts.cjs` (`isCanonicalPlanningFile`) for the artifact-kind gate.
|
|
13
|
+
*
|
|
14
|
+
* This phase migrates NO call site — it is purely additive (ADR-4910 §7).
|
|
15
|
+
* Only `boldField` nodes are writable; `table`/`checklist` nodes parse and
|
|
16
|
+
* read only. Phase 3 (#4958) checked its own evidence (#4736, #4793) and
|
|
17
|
+
* found neither needed a table/checklist writer here — see ADR-4910's
|
|
18
|
+
* 2026-09-24 amendment. A writer for either kind is unclaimed until a real
|
|
19
|
+
* call site names it.
|
|
20
|
+
*
|
|
21
|
+
* Hyrum's Law commitment (row 3 of the design's behaviour table): `serialize`
|
|
22
|
+
* with zero staged edits returns `doc.source` BYTE-IDENTICAL — never a
|
|
23
|
+
* re-render (#4499's root cause). Every byte outside an edited `valueSpan` is
|
|
24
|
+
* the ORIGINAL source, spliced, never regenerated.
|
|
25
|
+
*
|
|
26
|
+
* ADR-457 build-at-publish: source in src/planning-document.cts, compiled to
|
|
27
|
+
* gsd-core/bin/lib/planning-document.cjs (gitignored).
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.PLANNING_ARTIFACTS = void 0;
|
|
34
|
+
exports.parsePlanningDoc = parsePlanningDoc;
|
|
35
|
+
exports.findField = findField;
|
|
36
|
+
exports.readNode = readNode;
|
|
37
|
+
exports.readFrontmatterField = readFrontmatterField;
|
|
38
|
+
exports.readFrontmatterFieldFromSource = readFrontmatterFieldFromSource;
|
|
39
|
+
exports.readFrontmatterFieldsFromSource = readFrontmatterFieldsFromSource;
|
|
40
|
+
exports.setFieldValue = setFieldValue;
|
|
41
|
+
exports.hasUnreadableNodes = hasUnreadableNodes;
|
|
42
|
+
exports.serialize = serialize;
|
|
43
|
+
exports.replaceProse = replaceProse;
|
|
44
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
45
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
46
|
+
const artifacts_cjs_1 = require("./artifacts.cjs");
|
|
47
|
+
// `frontmatter.cts` uses `export =` (CJS-style single export object), so it
|
|
48
|
+
// is imported as a default import (esModuleInterop), not a named import.
|
|
49
|
+
const frontmatter_cjs_1 = __importDefault(require("./frontmatter.cjs"));
|
|
50
|
+
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
51
|
+
const { extractFrontmatter, FRONTMATTER_UNPARSEABLE } = frontmatter_cjs_1.default;
|
|
52
|
+
/**
|
|
53
|
+
* Canonical `.planning/` root artifact basenames this seam recognises,
|
|
54
|
+
* derived from the SAME registry `isCanonicalPlanningFile` consults
|
|
55
|
+
* (`artifacts.cts`'s `CANONICAL_EXACT`) — never a second, independently
|
|
56
|
+
* maintained list.
|
|
57
|
+
*
|
|
58
|
+
* Filtered to `.md` names only: `CANONICAL_EXACT` also carries non-markdown
|
|
59
|
+
* artifacts (`config.json`, `state.json`, `milestone.lock`, …) that this
|
|
60
|
+
* parser has no grammar for. Handing that JSON/lock content to the markdown
|
|
61
|
+
* parser below returns a successful EMPTY document (`nodes: []`), which reads
|
|
62
|
+
* as "this document records nothing" when the truth is "wrong kind entirely"
|
|
63
|
+
* — the empty-vs-error confusion #4917 / ADR-4910 §5 exists to eliminate. Do
|
|
64
|
+
* NOT remove this filter to "restore" the full registry.
|
|
65
|
+
*/
|
|
66
|
+
exports.PLANNING_ARTIFACTS = Object.freeze(Array.from(artifacts_cjs_1.CANONICAL_EXACT).filter((name) => name.endsWith('.md')));
|
|
67
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
68
|
+
let nodeCounter = 0;
|
|
69
|
+
function mintId(kind) {
|
|
70
|
+
nodeCounter += 1;
|
|
71
|
+
return `${kind}-${nodeCounter}-${Math.random().toString(36).slice(2, 8)}`;
|
|
72
|
+
}
|
|
73
|
+
function splitLinesInfo(source) {
|
|
74
|
+
const out = [];
|
|
75
|
+
let offset = 0;
|
|
76
|
+
const rawLines = source.split('\n');
|
|
77
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
78
|
+
const raw = rawLines[i];
|
|
79
|
+
const hasCR = raw.endsWith('\r');
|
|
80
|
+
const text = hasCR ? raw.slice(0, -1) : raw;
|
|
81
|
+
out.push({ text, start: offset, end: offset + text.length });
|
|
82
|
+
offset += raw.length + 1; // +1 for the '\n' split on ('\r' already counted in raw.length)
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Locate the frontmatter block, if any, from `locateFrontmatterFence` — the
|
|
88
|
+
* one owner of the fence grammar (byte-0 rule, BOM tolerance, whole-line
|
|
89
|
+
* closing fence, an adjacent empty block), which every frontmatter reader and
|
|
90
|
+
* writer also reads; this seam never re-derives it (ADR-4910 Decision 1).
|
|
91
|
+
*
|
|
92
|
+
* The span is an absolute range into `source`, from the opening fence (after
|
|
93
|
+
* any BOM) through the closing fence line's text, plus that line's CR when it
|
|
94
|
+
* ends in CRLF. Found while implementing #5105: the previous adapter re-derived
|
|
95
|
+
* the closing fence's end from the YAML region's length and was one character
|
|
96
|
+
* long on an adjacent empty block (`---\n---\n`), which has no line of its own
|
|
97
|
+
* before the closer.
|
|
98
|
+
*/
|
|
99
|
+
function findFrontmatterSpan(source) {
|
|
100
|
+
const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(source);
|
|
101
|
+
if (!fence)
|
|
102
|
+
return null;
|
|
103
|
+
const start = fence.bom.length;
|
|
104
|
+
if (!fence.closed) {
|
|
105
|
+
return { span: { start, end: source.length }, terminated: false };
|
|
106
|
+
}
|
|
107
|
+
const end = fence.closingFenceEnd + (source[fence.closingFenceEnd] === '\r' ? 1 : 0);
|
|
108
|
+
return { span: { start, end }, terminated: true };
|
|
109
|
+
}
|
|
110
|
+
/** Build the set of 0-based line indices that fall inside a fenced code
|
|
111
|
+
* block (opening/closing delimiter lines included), so `**Label:**`/table/
|
|
112
|
+
* checklist scanning never treats fenced content as a node (rows 9/14). */
|
|
113
|
+
function fencedLineIndices(lines) {
|
|
114
|
+
const raw = lines.map((l) => l.text);
|
|
115
|
+
const blocks = (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(raw);
|
|
116
|
+
const set = new Set();
|
|
117
|
+
for (const b of blocks) {
|
|
118
|
+
const end = b.closeLineIdx === -1 ? raw.length - 1 : b.closeLineIdx;
|
|
119
|
+
for (let i = b.openLineIdx; i <= end; i++)
|
|
120
|
+
set.add(i);
|
|
121
|
+
}
|
|
122
|
+
return set;
|
|
123
|
+
}
|
|
124
|
+
/** Matches both shipped bold-field spellings: colon-inside (`**Label:**`,
|
|
125
|
+
* the original grammar) and colon-outside (`**Label**:`, the canonical form
|
|
126
|
+
* used throughout `templates/roadmap.md`). Each alternative's trailing
|
|
127
|
+
* marker is exactly 3 characters (`:**` or `**:`), so `token.slice(2, -3)`
|
|
128
|
+
* in `parseBoldFieldLine` strips the leading `**` and the spelling-specific
|
|
129
|
+
* trailing marker identically for both, yielding the same `label` either
|
|
130
|
+
* way. Deliberately excludes a bare unbolded `Label:` form — see Phase 1's
|
|
131
|
+
* prose-vs-field disambiguation design. */
|
|
132
|
+
const BOLD_FIELD_RE = /^(\s*)(\*\*[^*\r\n]+(?::\*\*|\*\*:))([ \t]*)([^\r\n]*)$/;
|
|
133
|
+
/** Boundary marking a hand-written trailing annotation on a field line —
|
|
134
|
+
* the token owner must never destroy prose past this separator. */
|
|
135
|
+
const TRAILING_SEPARATOR_RE = / — /;
|
|
136
|
+
function parseBoldFieldLine(line) {
|
|
137
|
+
const m = BOLD_FIELD_RE.exec(line.text);
|
|
138
|
+
if (!m)
|
|
139
|
+
return null;
|
|
140
|
+
const [, leading, token, spacing, rest] = m;
|
|
141
|
+
const labelStart = line.start + leading.length;
|
|
142
|
+
const labelSpan = { start: labelStart, end: labelStart + token.length };
|
|
143
|
+
const label = token.slice(2, -3);
|
|
144
|
+
const restStart = labelSpan.end + spacing.length;
|
|
145
|
+
const sepMatch = TRAILING_SEPARATOR_RE.exec(rest);
|
|
146
|
+
const valueRaw = sepMatch ? rest.slice(0, sepMatch.index) : rest;
|
|
147
|
+
const trimmedValue = valueRaw.replace(/\s+$/, '');
|
|
148
|
+
const valueSpan = { start: restStart, end: restStart + trimmedValue.length };
|
|
149
|
+
const trailingSpan = { start: valueSpan.end, end: line.end };
|
|
150
|
+
return {
|
|
151
|
+
kind: 'boldField',
|
|
152
|
+
id: mintId('boldField'),
|
|
153
|
+
span: { start: labelSpan.start, end: line.end },
|
|
154
|
+
error: null,
|
|
155
|
+
label,
|
|
156
|
+
labelSpan,
|
|
157
|
+
valueSpan,
|
|
158
|
+
trailingSpan,
|
|
159
|
+
value: trimmedValue,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** A checklist line is one whose SOLE bullet, per `iterateBullets` (the same
|
|
163
|
+
* grammar the repo's other bullet consumers use), is a checkbox marker, OR
|
|
164
|
+
* whose bullet TEXT begins with a task-list marker.
|
|
165
|
+
*
|
|
166
|
+
* `iterateBullets` owns bullet *structure* — is this a bullet, where does its
|
|
167
|
+
* text start — and continues to own that here unchanged. It only classifies
|
|
168
|
+
* `-`-prefixed bullets as `checkbox-checked`/`checkbox-unchecked`; GFM also
|
|
169
|
+
* permits `*` and `+` as bullet markers, and `* [ ] x` / `+ [x] y` are valid
|
|
170
|
+
* GFM task-list items that `iterateBullets` reports as plain `dash`-family
|
|
171
|
+
* bullets with the `[ ]`/`[x]` left in the bullet's own text. Widening
|
|
172
|
+
* `iterateBullets` itself is forbidden by ADR-2143 §2's extend-never-mutate
|
|
173
|
+
* lock (inherited by this epic), so the task-list-marker interpretation is
|
|
174
|
+
* layered on here, over the bullet's already-extracted text — never by
|
|
175
|
+
* re-scanning the raw line with a new hand-rolled regex.
|
|
176
|
+
*
|
|
177
|
+
* Known limit inherited from `iterateBullets`, not introduced here:
|
|
178
|
+
* `-\t[ ] text` (a tab between the marker and the text) is not recognised as
|
|
179
|
+
* a bullet at all, so it can never become a checklist line. That is a
|
|
180
|
+
* pre-existing `markdown-sectionizer` boundary affecting every consumer of
|
|
181
|
+
* `iterateBullets`, and fixing it would mean altering the locked seam. */
|
|
182
|
+
function isChecklistLine(text) {
|
|
183
|
+
const items = (0, markdown_sectionizer_cjs_1.iterateBullets)(text);
|
|
184
|
+
if (items.length !== 1)
|
|
185
|
+
return false;
|
|
186
|
+
const item = items[0];
|
|
187
|
+
if (item.marker === 'checkbox-checked' || item.marker === 'checkbox-unchecked')
|
|
188
|
+
return true;
|
|
189
|
+
return /^\[[ xX]\] /.test(item.text);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Scan the document body (everything outside the frontmatter block and
|
|
193
|
+
* outside fenced code) for `boldField`, `table`, and `checklist` nodes, in
|
|
194
|
+
* document order.
|
|
195
|
+
*/
|
|
196
|
+
function scanBodyNodes(source, lines, frontmatterEnd) {
|
|
197
|
+
const fenced = fencedLineIndices(lines);
|
|
198
|
+
const nodes = [];
|
|
199
|
+
let i = 0;
|
|
200
|
+
while (i < lines.length) {
|
|
201
|
+
const line = lines[i];
|
|
202
|
+
if (fenced.has(i) || line.start < frontmatterEnd) {
|
|
203
|
+
i += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const trimmed = line.text.trim();
|
|
207
|
+
// Table: a pipe-shaped header line followed by a valid delimiter row.
|
|
208
|
+
if (trimmed.startsWith('|') && trimmed.indexOf('|', 1) !== -1 && i + 1 < lines.length) {
|
|
209
|
+
const delimiterLine = lines[i + 1];
|
|
210
|
+
const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(delimiterLine.text);
|
|
211
|
+
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(line.text);
|
|
212
|
+
if (delimiterLine.text.trim().startsWith('|')
|
|
213
|
+
&& (0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells)
|
|
214
|
+
&& delimiterCells.length === headerCells.length
|
|
215
|
+
&& !fenced.has(i + 1)) {
|
|
216
|
+
let last = i + 1;
|
|
217
|
+
while (last + 1 < lines.length && lines[last + 1].text.trim().startsWith('|') && !fenced.has(last + 1)) {
|
|
218
|
+
last += 1;
|
|
219
|
+
}
|
|
220
|
+
const span = { start: line.start, end: lines[last].end };
|
|
221
|
+
const tableText = source.slice(span.start, span.end);
|
|
222
|
+
const parsed = (0, markdown_table_cjs_1.parseMarkdownTable)(tableText);
|
|
223
|
+
nodes.push(parsed.ok
|
|
224
|
+
? {
|
|
225
|
+
kind: 'table',
|
|
226
|
+
id: mintId('table'),
|
|
227
|
+
span,
|
|
228
|
+
error: null,
|
|
229
|
+
columns: parsed.value.columns,
|
|
230
|
+
}
|
|
231
|
+
: {
|
|
232
|
+
kind: 'table',
|
|
233
|
+
id: mintId('table'),
|
|
234
|
+
span,
|
|
235
|
+
error: { reason: parsed.reason, span },
|
|
236
|
+
columns: null,
|
|
237
|
+
});
|
|
238
|
+
i = last + 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Checklist: a contiguous run of checkbox-bullet lines.
|
|
243
|
+
if (isChecklistLine(line.text)) {
|
|
244
|
+
let last = i;
|
|
245
|
+
let count = 0;
|
|
246
|
+
while (last < lines.length && !fenced.has(last) && isChecklistLine(lines[last].text)) {
|
|
247
|
+
count += 1;
|
|
248
|
+
last += 1;
|
|
249
|
+
}
|
|
250
|
+
last -= 1;
|
|
251
|
+
const span = { start: line.start, end: lines[last].end };
|
|
252
|
+
nodes.push({ kind: 'checklist', id: mintId('checklist'), span, error: null, items: count });
|
|
253
|
+
i = last + 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// Bold field.
|
|
257
|
+
const field = parseBoldFieldLine(line);
|
|
258
|
+
if (field) {
|
|
259
|
+
nodes.push(field);
|
|
260
|
+
i += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
i += 1;
|
|
264
|
+
}
|
|
265
|
+
return nodes;
|
|
266
|
+
}
|
|
267
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
268
|
+
/**
|
|
269
|
+
* Parse `source` (the raw text of a `.planning/` root artifact) into a
|
|
270
|
+
* `PlanningDoc`. Document-level `Result` failure is reserved for: `artifact`
|
|
271
|
+
* not a recognised planning artifact kind, `source` not a readable string, or
|
|
272
|
+
* an opened-but-never-closed frontmatter fence (ADR-4910 §5's reservation).
|
|
273
|
+
* A malformed SUB-structure (a ragged table, say) never fails the whole
|
|
274
|
+
* document — it is recorded as that one node's `error`, and every sibling
|
|
275
|
+
* node stays readable (row 7). `nodes: []` on a genuinely empty document is
|
|
276
|
+
* success, not an error (row 15).
|
|
277
|
+
*/
|
|
278
|
+
function parsePlanningDoc(source, artifact) {
|
|
279
|
+
if (typeof source !== 'string') {
|
|
280
|
+
return { ok: false, reason: 'unreadable: source is not a string' };
|
|
281
|
+
}
|
|
282
|
+
if (typeof artifact !== 'string' ||
|
|
283
|
+
!(0, artifacts_cjs_1.isCanonicalPlanningFile)(artifact) ||
|
|
284
|
+
!exports.PLANNING_ARTIFACTS.includes(artifact)) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
reason: `not a markdown planning document (artifact: ${String(artifact)})`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const nodes = [];
|
|
291
|
+
let frontmatterEnd = 0;
|
|
292
|
+
const fm = findFrontmatterSpan(source);
|
|
293
|
+
if (fm) {
|
|
294
|
+
if (!fm.terminated) {
|
|
295
|
+
return { ok: false, reason: 'no frontmatter terminator' };
|
|
296
|
+
}
|
|
297
|
+
nodes.push({ kind: 'frontmatter', id: mintId('frontmatter'), span: fm.span, error: null });
|
|
298
|
+
frontmatterEnd = fm.span.end;
|
|
299
|
+
}
|
|
300
|
+
if (source.length === 0) {
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
value: { source, artifact, nodes: [], staged: new Map() },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const lines = splitLinesInfo(source);
|
|
307
|
+
// Sections: one per heading, in document order — every heading is its own
|
|
308
|
+
// boundary (`collectSections(source, () => true)`), so a nested `####`
|
|
309
|
+
// still gets its own SectionNode rather than being folded into its parent.
|
|
310
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(source);
|
|
311
|
+
if (headings.length > 0) {
|
|
312
|
+
const sections = (0, markdown_sectionizer_cjs_1.collectSections)(source, () => true);
|
|
313
|
+
for (const s of sections) {
|
|
314
|
+
nodes.push({
|
|
315
|
+
kind: 'section',
|
|
316
|
+
id: mintId('section'),
|
|
317
|
+
span: { start: s.heading.offset, end: s.bodyEnd },
|
|
318
|
+
error: null,
|
|
319
|
+
heading: s.heading.text,
|
|
320
|
+
level: s.heading.level,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
nodes.push(...scanBodyNodes(source, lines, frontmatterEnd));
|
|
325
|
+
nodes.sort((a, b) => a.span.start - b.span.start);
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
value: { source, artifact, nodes, staged: new Map() },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** Find the id of the (first, document-order) `boldField` node whose label
|
|
332
|
+
* exactly matches `label`, or `null` when none does. */
|
|
333
|
+
function findField(doc, label) {
|
|
334
|
+
for (const n of doc.nodes) {
|
|
335
|
+
if (n.kind === 'boldField' && n.label === label)
|
|
336
|
+
return n.id;
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
/** Read a node by id. Node-scoped failure only — an unknown id or a node
|
|
341
|
+
* that failed to parse never throws. */
|
|
342
|
+
function readNode(doc, id) {
|
|
343
|
+
const node = doc.nodes.find((n) => n.id === id);
|
|
344
|
+
if (!node) {
|
|
345
|
+
return { ok: false, reason: 'unknown node id', span: { start: 0, end: 0 } };
|
|
346
|
+
}
|
|
347
|
+
if (node.error) {
|
|
348
|
+
return { ok: false, reason: node.error.reason, span: node.error.span };
|
|
349
|
+
}
|
|
350
|
+
if (node.kind === 'boldField') {
|
|
351
|
+
return { ok: true, value: doc.staged.get(id) ?? node.value };
|
|
352
|
+
}
|
|
353
|
+
return { ok: true, value: doc.source.slice(node.span.start, node.span.end) };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Parse a frontmatter block's own span TEXT (fences included) via
|
|
357
|
+
* `frontmatter.cts`'s `extractFrontmatter`, exactly ONCE, returning either the
|
|
358
|
+
* parsed object or a `{ ok: false }` marker for the `FRONTMATTER_UNPARSEABLE`
|
|
359
|
+
* case. Split out of the single-key lookup below (#5026 follow-up) so a
|
|
360
|
+
* caller reading MULTIPLE keys off the SAME region
|
|
361
|
+
* (`readFrontmatterFieldsFromSource`) detects the span and parses its YAML
|
|
362
|
+
* once, not once per key — this function is the one and only place that
|
|
363
|
+
* detect-then-parse step happens; every reader (single-key or bulk) composes
|
|
364
|
+
* it rather than re-deriving it.
|
|
365
|
+
*/
|
|
366
|
+
function parseFrontmatterRegion(regionText) {
|
|
367
|
+
// A CRLF block's span ends on its closing fence line's CR (`findFrontmatterSpan`). That CR is
|
|
368
|
+
// half a line ending, not part of the fence: a `---` line ended by a lone CR does not close a
|
|
369
|
+
// block, so the text is parsed without it.
|
|
370
|
+
const fm = extractFrontmatter(regionText.endsWith('\r') ? regionText.slice(0, -1) : regionText);
|
|
371
|
+
if (fm[FRONTMATTER_UNPARSEABLE] === true) {
|
|
372
|
+
return { ok: false };
|
|
373
|
+
}
|
|
374
|
+
return { ok: true, value: fm };
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Shared core of `readFrontmatterField` / `readFrontmatterFieldFromSource` /
|
|
378
|
+
* `readFrontmatterFieldsFromSource`: given an ALREADY-PARSED frontmatter
|
|
379
|
+
* result (`parseFrontmatterRegion`'s output) and the `Span` to report on
|
|
380
|
+
* failure, shape one `key`'s lookup as a `NodeRead`. Every reader composes
|
|
381
|
+
* this SAME shaping step — neither may duplicate it (the exact
|
|
382
|
+
* `DEFECT.GENERATIVE-FIX` class this epic exists to close).
|
|
383
|
+
*
|
|
384
|
+
* A region that failed to parse as YAML (`extractFrontmatter` reports this by
|
|
385
|
+
* returning `{}` carrying the `FRONTMATTER_UNPARSEABLE` Symbol, per that
|
|
386
|
+
* module's own documented contract) → `{ ok: false, reason:
|
|
387
|
+
* 'unparseable-frontmatter', span }`. A parseable region whose key is simply
|
|
388
|
+
* absent → `{ ok: false, reason: 'field-not-found', span }` —
|
|
389
|
+
* `extractFrontmatter` itself has no notion of "field not found" (a missing
|
|
390
|
+
* key just reads `undefined` off its returned object), so this reason string
|
|
391
|
+
* is this seam's own, not a passthrough of an upstream contract.
|
|
392
|
+
*/
|
|
393
|
+
function shapeFrontmatterField(parsed, span, key) {
|
|
394
|
+
if (!parsed.ok) {
|
|
395
|
+
return { ok: false, reason: 'unparseable-frontmatter', span };
|
|
396
|
+
}
|
|
397
|
+
const value = parsed.value[key];
|
|
398
|
+
if (value === undefined) {
|
|
399
|
+
return { ok: false, reason: 'field-not-found', span };
|
|
400
|
+
}
|
|
401
|
+
return { ok: true, value };
|
|
402
|
+
}
|
|
403
|
+
/** Single-key lookup: parse `regionText` once (`parseFrontmatterRegion`) and
|
|
404
|
+
* shape `key`'s result (`shapeFrontmatterField`). Used by `readFrontmatterField`
|
|
405
|
+
* and `readFrontmatterFieldFromSource`, which each already have the region's
|
|
406
|
+
* own text and span in hand; a caller reading several keys off one region
|
|
407
|
+
* should use `readFrontmatterFieldsFromSource` instead, to avoid re-parsing
|
|
408
|
+
* once per key. */
|
|
409
|
+
function lookupFrontmatterField(regionText, span, key) {
|
|
410
|
+
return shapeFrontmatterField(parseFrontmatterRegion(regionText), span, key);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Read one top-level frontmatter key off an already-parsed `PlanningDoc`,
|
|
414
|
+
* composing `frontmatter.cts`'s `extractFrontmatter` rather than
|
|
415
|
+
* reimplementing YAML parsing (ADR-4910 Decision 1's precedent — the same
|
|
416
|
+
* composition `frontmatterRegion` already uses).
|
|
417
|
+
*
|
|
418
|
+
* No `FrontmatterNode` in `doc.nodes` (there is at most one per document) →
|
|
419
|
+
* `{ ok: false, reason: 'no-frontmatter', span: {0,0} }`, mirroring
|
|
420
|
+
* `findField`'s own not-found span convention (`readNode`'s unknown-id case).
|
|
421
|
+
*
|
|
422
|
+
* A `FrontmatterNode` is only ever pushed onto `doc.nodes` when its fence was
|
|
423
|
+
* terminated (`parsePlanningDoc` fails the whole document, before any node
|
|
424
|
+
* exists, on an unterminated fence) — so the "opened but never closed" case
|
|
425
|
+
* `extractFrontmatter` also handles is never reachable from an already-parsed
|
|
426
|
+
* `PlanningDoc` through this function. See `lookupFrontmatterField` for the
|
|
427
|
+
* unparseable-frontmatter / field-not-found / present result shapes.
|
|
428
|
+
*/
|
|
429
|
+
function readFrontmatterField(doc, key) {
|
|
430
|
+
const node = doc.nodes.find((n) => n.kind === 'frontmatter');
|
|
431
|
+
if (!node) {
|
|
432
|
+
return { ok: false, reason: 'no-frontmatter', span: { start: 0, end: 0 } };
|
|
433
|
+
}
|
|
434
|
+
const raw = doc.source.slice(node.span.start, node.span.end);
|
|
435
|
+
return lookupFrontmatterField(raw, node.span, key);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Read one top-level frontmatter key directly off raw `source` text, with no
|
|
439
|
+
* `PlanningDoc`/artifact-kind gate involved (#5026). `parsePlanningDoc`'s
|
|
440
|
+
* artifact-kind gate exists to distinguish "this document records nothing"
|
|
441
|
+
* from "wrong kind entirely" for a caller that might hand it any
|
|
442
|
+
* `.planning/`-root file, including a non-markdown one (config.json,
|
|
443
|
+
* state.json, …) — a risk that does not exist for a caller (`plan-document.
|
|
444
|
+
* cts`) that is ONLY ever invoked on real `*-PLAN.md` content, never on
|
|
445
|
+
* anything else, and is never given a canonical root-artifact basename to
|
|
446
|
+
* gate on in the first place (`*-PLAN.md` lives nested under
|
|
447
|
+
* `.planning/phase/*\/plans/`, never at the `.planning/` root
|
|
448
|
+
* `PLANNING_ARTIFACTS` enumerates — confirmed by execution:
|
|
449
|
+
* `isCanonicalPlanningFile('01-PLAN.md')` is `false`). This entry point
|
|
450
|
+
* routes around that gate rather than through it, for exactly that caller
|
|
451
|
+
* shape: content in hand, no filename to check, no need for any other
|
|
452
|
+
* `PlanningDoc` capability (sections/tables/checklists) this seam offers.
|
|
453
|
+
*
|
|
454
|
+
* Locates the frontmatter span via `findFrontmatterSpan` — the SAME
|
|
455
|
+
* `locateFrontmatterFence`-reading helper `parsePlanningDoc` itself uses to
|
|
456
|
+
* build a `FrontmatterNode` — so this is not a second detection mechanism,
|
|
457
|
+
* only a bypass of the node-parsing pipeline neither this caller nor its
|
|
458
|
+
* content needs.
|
|
459
|
+
*
|
|
460
|
+
* No frontmatter fence at byte 0 at all → `{ ok: false, reason:
|
|
461
|
+
* 'no-frontmatter', span: {0,0} }`, the same shape `readFrontmatterField`
|
|
462
|
+
* returns for its own not-found case. An OPENED-but-never-closed fence is
|
|
463
|
+
* reachable here (unlike `readFrontmatterField`, which can only ever see an
|
|
464
|
+
* already-terminated frontmatter node): `extractFrontmatter` treats that
|
|
465
|
+
* region as if it had none (`{}`, no `FRONTMATTER_UNPARSEABLE` marker), so
|
|
466
|
+
* every key on it comes back `field-not-found` — matching exactly what a
|
|
467
|
+
* direct `extractFrontmatter(source)[key] === undefined` check already
|
|
468
|
+
* produces for that same input today. See `lookupFrontmatterField` for the
|
|
469
|
+
* unparseable-frontmatter / field-not-found / present result shapes.
|
|
470
|
+
*/
|
|
471
|
+
function readFrontmatterFieldFromSource(source, key) {
|
|
472
|
+
const found = findFrontmatterSpan(source);
|
|
473
|
+
if (!found) {
|
|
474
|
+
return { ok: false, reason: 'no-frontmatter', span: { start: 0, end: 0 } };
|
|
475
|
+
}
|
|
476
|
+
const raw = source.slice(found.span.start, found.span.end);
|
|
477
|
+
return lookupFrontmatterField(raw, found.span, key);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Read MULTIPLE top-level frontmatter keys off raw `source` text in ONE pass
|
|
481
|
+
* — the bulk sibling of `readFrontmatterFieldFromSource`, for a caller (added
|
|
482
|
+
* for `plan-document.cts`'s `parsePlanDocument`, #5026 follow-up) that needs
|
|
483
|
+
* several keys off the SAME document. Calling `readFrontmatterFieldFromSource`
|
|
484
|
+
* once per key each independently re-detects the frontmatter span AND
|
|
485
|
+
* re-parses the full frontmatter YAML from scratch (`findFrontmatterSpan` +
|
|
486
|
+
* `parseFrontmatterRegion`, both non-trivial scans) — this function detects
|
|
487
|
+
* the span and parses the YAML exactly ONCE, then shapes every requested key
|
|
488
|
+
* off that SAME parsed result.
|
|
489
|
+
*
|
|
490
|
+
* This is a second ENTRY POINT into the one shared detect-span +
|
|
491
|
+
* parse-YAML + shape-a-key pipeline (`findFrontmatterSpan` /
|
|
492
|
+
* `parseFrontmatterRegion` / `shapeFrontmatterField`), never a second parser:
|
|
493
|
+
* per-key result shapes are byte-identical to calling
|
|
494
|
+
* `readFrontmatterFieldFromSource` once per key (same `no-frontmatter` /
|
|
495
|
+
* `unparseable-frontmatter` / `field-not-found` / present shapes, same span).
|
|
496
|
+
*/
|
|
497
|
+
function readFrontmatterFieldsFromSource(source, keys) {
|
|
498
|
+
const found = findFrontmatterSpan(source);
|
|
499
|
+
const out = {};
|
|
500
|
+
if (!found) {
|
|
501
|
+
const span = { start: 0, end: 0 };
|
|
502
|
+
for (const key of keys)
|
|
503
|
+
out[key] = { ok: false, reason: 'no-frontmatter', span };
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
const raw = source.slice(found.span.start, found.span.end);
|
|
507
|
+
const parsed = parseFrontmatterRegion(raw);
|
|
508
|
+
for (const key of keys)
|
|
509
|
+
out[key] = shapeFrontmatterField(parsed, found.span, key);
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Stage a new value for a `boldField` node, returning a NEW `PlanningDoc`
|
|
514
|
+
* (immutable — `doc` itself is never mutated). Refuses an id this doc did
|
|
515
|
+
* not mint, and refuses any node kind other than `boldField` — only
|
|
516
|
+
* `valueSpan` is ever writable this phase (ADR-4910 §1).
|
|
517
|
+
*
|
|
518
|
+
* #5007 / ADR-4910 Phase 6: a prior amendment here added a `{ allowSeparator:
|
|
519
|
+
* true }` escape hatch that spliced the caller's value across the FULL
|
|
520
|
+
* rest-of-line span (`valueSpan.start`..`trailingSpan.end`) to let a value
|
|
521
|
+
* legitimately containing the grammar's ` — ` trailing-separator token
|
|
522
|
+
* (`TRAILING_SEPARATOR_RE`) be written without triggering the round-trip
|
|
523
|
+
* refusal below. That option was REMOVED (still #5007, same phase) after a
|
|
524
|
+
* failing-first reproduction proved it only avoided the refusal AT WRITE
|
|
525
|
+
* TIME: the written bytes are correct, but `parseBoldFieldLine` splits on
|
|
526
|
+
* ` — ` unconditionally and without any escaping/metadata to distinguish
|
|
527
|
+
* "atomic value containing the token" from "value plus hand-annotation" —
|
|
528
|
+
* the same input shape either way. So the NEXT fresh `parsePlanningDoc` of
|
|
529
|
+
* that exact text (not the in-memory `doc` the option's own tests checked)
|
|
530
|
+
* silently re-truncates the value and demotes the rest to `trailingSpan`,
|
|
531
|
+
* with `findField`/`readNode` reporting a confident, wrong `ok: true`
|
|
532
|
+
* result and no error — reproduced live: staging `"Phase — COMPLETE"` this
|
|
533
|
+
* way, serializing, and re-parsing the output through a fresh
|
|
534
|
+
* `parsePlanningDoc` read back `"Phase"` via `readNode`, silently losing
|
|
535
|
+
* ` — COMPLETE`. This is exactly the #4917 finding-2 corruption this
|
|
536
|
+
* module's round-trip check exists to prevent, just moved one parse cycle
|
|
537
|
+
* downstream of where the check could still catch it. There is no escaping
|
|
538
|
+
* convention anywhere in this grammar (`BOLD_FIELD_RE`/`TRAILING_SEPARATOR_
|
|
539
|
+
* RE` are unconditional, unversioned regexes with no metadata channel), and
|
|
540
|
+
* `TRAILING_SEPARATOR_RE`'s split is relied on by every other reader of this
|
|
541
|
+
* seam (`findField`/`readNode`, used today for ROADMAP.md's `Plans`/
|
|
542
|
+
* `Depends on` fields) — narrowing or version-gating it here would be a
|
|
543
|
+
* grammar change with its own blast radius, not a local bug fix. Widening
|
|
544
|
+
* `setFieldValue`'s PUBLIC, shared contract to include a write path that is
|
|
545
|
+
* only safe for a caller who never reads the field back through this same
|
|
546
|
+
* module is an attractive nuisance: nothing stops a future `findField`/
|
|
547
|
+
* `readNode` caller from reaching for it and hitting this exact corruption.
|
|
548
|
+
* The one real caller (`stateReplaceField`, src/state-document.cts) never
|
|
549
|
+
* reads STATE.md fields back through `parsePlanningDoc`/`findField` (it uses
|
|
550
|
+
* `stateExtractField`'s own non-splitting regex instead), so it does its own
|
|
551
|
+
* local full-rest-of-line splice directly against `content`, using this
|
|
552
|
+
* module only to LOCATE the field's spans — keeping the dangerous affordance
|
|
553
|
+
* out of this shared seam's public surface entirely, rather than fixing it
|
|
554
|
+
* with a narrower version of the same false-safety option.
|
|
555
|
+
*/
|
|
556
|
+
function setFieldValue(doc, id, value) {
|
|
557
|
+
const node = doc.nodes.find((n) => n.id === id);
|
|
558
|
+
if (!node) {
|
|
559
|
+
return { ok: false, reason: 'unknown node id' };
|
|
560
|
+
}
|
|
561
|
+
if (node.kind !== 'boldField') {
|
|
562
|
+
return { ok: false, reason: `node kind '${node.kind}' is not writable this phase` };
|
|
563
|
+
}
|
|
564
|
+
// #4917 / ADR-4910 Decision 2 & 4: a boldField's token boundary is a LINE
|
|
565
|
+
// boundary, not just an offset range — a value containing \n or \r escapes
|
|
566
|
+
// the field's own span and reparses as sibling structure (a forged field)
|
|
567
|
+
// once spliced back into the source. Decision 4 licenses refusal for any
|
|
568
|
+
// value the grammar cannot represent; Phase 3 may widen this to escaping,
|
|
569
|
+
// but Phase 1 refuses outright. Do not remove this as an over-restriction.
|
|
570
|
+
if (/[\r\n]/.test(value)) {
|
|
571
|
+
return { ok: false, reason: 'field value must not contain a line break (\\r or \\n)' };
|
|
572
|
+
}
|
|
573
|
+
// #4917 / ADR-4910 Decision 4: "a value that cannot be represented in the
|
|
574
|
+
// grammar is refused by the writer, with a report." This is a GENERAL
|
|
575
|
+
// round-trip representability check, not a blacklist of forbidden
|
|
576
|
+
// substrings — the `\r`/`\n` guard above is a narrower special case kept
|
|
577
|
+
// for its clearer message, but THIS check is the backstop. It rebuilds the
|
|
578
|
+
// line exactly as it would be written (existing leading/label/spacing +
|
|
579
|
+
// the new value + the existing trailing text) and re-parses that line
|
|
580
|
+
// through the SAME `parseBoldFieldLine` grammar the reader uses. If the
|
|
581
|
+
// value the grammar reads back is not byte-identical to what the caller
|
|
582
|
+
// staged, the grammar cannot represent this value (e.g. it contains the
|
|
583
|
+
// ` — ` trailing-separator token, which would silently reclassify the
|
|
584
|
+
// rest of the value as trailing prose) and the write is refused. Do NOT
|
|
585
|
+
// replace this with a list of forbidden characters/substrings — the next
|
|
586
|
+
// separator the grammar grows would silently slip past a blacklist.
|
|
587
|
+
const leadingText = doc.source.slice(node.span.start, node.labelSpan.start);
|
|
588
|
+
const tokenText = doc.source.slice(node.labelSpan.start, node.labelSpan.end);
|
|
589
|
+
const spacingText = doc.source.slice(node.labelSpan.end, node.valueSpan.start);
|
|
590
|
+
const trailingText = doc.source.slice(node.trailingSpan.start, node.trailingSpan.end);
|
|
591
|
+
const candidateLine = `${leadingText}${tokenText}${spacingText}${value}${trailingText}`;
|
|
592
|
+
const candidateInfo = { text: candidateLine, start: 0, end: candidateLine.length };
|
|
593
|
+
const reparsed = parseBoldFieldLine(candidateInfo);
|
|
594
|
+
if (!reparsed || reparsed.value !== value) {
|
|
595
|
+
return {
|
|
596
|
+
ok: false,
|
|
597
|
+
reason: 'field value is not representable in the boldField grammar (would not round-trip)',
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const staged = new Map(doc.staged);
|
|
601
|
+
staged.set(id, value);
|
|
602
|
+
return {
|
|
603
|
+
ok: true,
|
|
604
|
+
value: { source: doc.source, artifact: doc.artifact, nodes: doc.nodes, staged },
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/** True when any node in `doc` failed to parse. */
|
|
608
|
+
function hasUnreadableNodes(doc) {
|
|
609
|
+
return doc.nodes.some((n) => n.error !== null);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Splice every staged edit into `doc.source` and return the resulting text.
|
|
613
|
+
* With zero staged edits, returns `doc.source` BYTE-IDENTICAL — never a
|
|
614
|
+
* re-render (row 3). Refuses outright — even with zero staged edits — when
|
|
615
|
+
* `hasUnreadableNodes(doc)` is true (the ADR-4910 amendment): `serialize`
|
|
616
|
+
* re-emits the WHOLE document, so the refusal is document-scoped, not
|
|
617
|
+
* mutation-scoped.
|
|
618
|
+
*/
|
|
619
|
+
function serialize(doc) {
|
|
620
|
+
if (hasUnreadableNodes(doc)) {
|
|
621
|
+
return {
|
|
622
|
+
ok: false,
|
|
623
|
+
reason: 'unreadable-nodes',
|
|
624
|
+
nodes: doc.nodes
|
|
625
|
+
.filter((n) => n.error !== null)
|
|
626
|
+
.map((n) => ({ id: n.id, kind: n.kind, span: n.error.span, reason: n.error.reason })),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (doc.staged.size === 0) {
|
|
630
|
+
return { ok: true, value: doc.source };
|
|
631
|
+
}
|
|
632
|
+
const edits = [];
|
|
633
|
+
for (const [id, value] of doc.staged) {
|
|
634
|
+
const node = doc.nodes.find((n) => n.id === id);
|
|
635
|
+
if (!node || node.kind !== 'boldField')
|
|
636
|
+
continue; // unreachable: setFieldValue already gated this
|
|
637
|
+
edits.push({ start: node.valueSpan.start, end: node.valueSpan.end, value });
|
|
638
|
+
}
|
|
639
|
+
edits.sort((a, b) => a.start - b.start);
|
|
640
|
+
let out = '';
|
|
641
|
+
let cursor = 0;
|
|
642
|
+
for (const e of edits) {
|
|
643
|
+
out += doc.source.slice(cursor, e.start) + e.value;
|
|
644
|
+
cursor = e.end;
|
|
645
|
+
}
|
|
646
|
+
out += doc.source.slice(cursor);
|
|
647
|
+
return { ok: true, value: out };
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Replace every occurrence of the single-line literal `from` with `to` in the
|
|
651
|
+
* document's prose — the seam's answer to a verbatim cross-reference rewrite
|
|
652
|
+
* (ADR-5057 §6, Phase 13, #5217: the migration's "Phase 1:" -> "Phase 1-01:"
|
|
653
|
+
* substitution in PROJECT.md / STATE.md).
|
|
654
|
+
*
|
|
655
|
+
* Fenced code blocks are never rewritten (rows 9/14: fenced content is not a
|
|
656
|
+
* node), every other byte — each line's own terminator included — is copied
|
|
657
|
+
* from `doc.source`, and a substitution that changes nothing returns a doc
|
|
658
|
+
* whose `source` is byte-identical. The result is re-parsed so its node spans
|
|
659
|
+
* describe the new text. Refuses (Result failure, nothing written) when `from`
|
|
660
|
+
* is empty or spans a line break, or when `doc` already carries staged
|
|
661
|
+
* field edits (their spans address the pre-substitution text).
|
|
662
|
+
*/
|
|
663
|
+
function replaceProse(doc, from, to) {
|
|
664
|
+
if (typeof from !== 'string' || from.length === 0) {
|
|
665
|
+
return { ok: false, reason: 'replaceProse: `from` must be a non-empty string' };
|
|
666
|
+
}
|
|
667
|
+
if (typeof to !== 'string' || /[\r\n]/.test(from) || /[\r\n]/.test(to)) {
|
|
668
|
+
return { ok: false, reason: 'replaceProse: `from` and `to` must be single-line strings' };
|
|
669
|
+
}
|
|
670
|
+
if (doc.staged.size > 0) {
|
|
671
|
+
return { ok: false, reason: 'replaceProse: refused while field edits are staged' };
|
|
672
|
+
}
|
|
673
|
+
const lines = splitLinesInfo(doc.source);
|
|
674
|
+
const fenced = fencedLineIndices(lines);
|
|
675
|
+
let out = '';
|
|
676
|
+
let cursor = 0;
|
|
677
|
+
let changed = false;
|
|
678
|
+
for (let i = 0; i < lines.length; i++) {
|
|
679
|
+
const line = lines[i];
|
|
680
|
+
if (fenced.has(i) || !line.text.includes(from))
|
|
681
|
+
continue;
|
|
682
|
+
out += doc.source.slice(cursor, line.start) + line.text.split(from).join(to);
|
|
683
|
+
cursor = line.end;
|
|
684
|
+
changed = true;
|
|
685
|
+
}
|
|
686
|
+
if (!changed)
|
|
687
|
+
return { ok: true, value: doc };
|
|
688
|
+
out += doc.source.slice(cursor);
|
|
689
|
+
return parsePlanningDoc(out, doc.artifact);
|
|
690
|
+
}
|
|
691
|
+
// Consumers: require('../gsd-core/bin/lib/planning-document.cjs')
|
|
692
|
+
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|