@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
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* anchors/alias refusal, the #3257 comment channel, the #1882 truncation
|
|
16
16
|
* probe, null-byte preservation and object-list flattening for the existing
|
|
17
17
|
* string-shaped value contract — is layered on top, in one place, below.
|
|
18
|
+
*
|
|
19
|
+
* #5105: the WRITER — `spliceFrontmatter` and its layout, comment-classification, parse-budget
|
|
20
|
+
* and read-back machinery — lives in `frontmatter-splice.cts`, split out by module ownership.
|
|
21
|
+
* This module re-exports its public names unchanged and requires it lazily (`spliceModule`).
|
|
18
22
|
*/
|
|
19
23
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
24
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -27,6 +31,8 @@ const { output, error } = ioMod;
|
|
|
27
31
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
28
32
|
const validate_cjs_1 = require("./validate.cjs");
|
|
29
33
|
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
34
|
+
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
35
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
30
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
37
|
const unusableInputMod = require("./unusable-input.cjs");
|
|
32
38
|
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
@@ -40,7 +46,7 @@ const js_yaml_cjs_1 = require("./vendor/js-yaml.cjs");
|
|
|
40
46
|
* which is the documented behavior `tests/fixtures/adversarial/frontmatter/
|
|
41
47
|
* duplicate-keys.md` pins.
|
|
42
48
|
*/
|
|
43
|
-
const YAML_LOAD_OPTS = { schema: js_yaml_cjs_1.FAILSAFE_SCHEMA, json: true };
|
|
49
|
+
const YAML_LOAD_OPTS = Object.freeze({ schema: js_yaml_cjs_1.FAILSAFE_SCHEMA, json: true });
|
|
44
50
|
/**
|
|
45
51
|
* How many parsed keys an unterminated region must yield before it is reported as a
|
|
46
52
|
* truncated frontmatter rather than left alone as ordinary Markdown. See the rationale on
|
|
@@ -88,8 +94,36 @@ function isFrontmatterShaped(region) {
|
|
|
88
94
|
* are attached to the top-level key that follows them; comments after the last
|
|
89
95
|
* key go to `trailing`. Only set when a comment is actually seen, so comment-less
|
|
90
96
|
* frontmatter parses byte-identically to before.
|
|
97
|
+
*
|
|
98
|
+
* `leading` (and `inline`) are keyed by `commentPathKey` of the key path the comment belongs
|
|
99
|
+
* to — `["status"]` for a top-level key, `["progress","total"]` for a nested one — so a
|
|
100
|
+
* top-level key literally named `a.b` never shares an entry with sub-key `b` of map `a`.
|
|
101
|
+
* `inline` holds a comment that sits on its key's own line (`a: 1 # note`), verbatim from
|
|
102
|
+
* the whitespace before its `#`; only `spliceFrontmatter` sets it, for the one key it
|
|
103
|
+
* regenerates (see `segmentComments`).
|
|
91
104
|
*/
|
|
92
105
|
const FULL_LINE_COMMENTS = Symbol('fullLineComments');
|
|
106
|
+
/**
|
|
107
|
+
* The comment-channel entry name of a key path: the JSON array of its key segments, which no
|
|
108
|
+
* two different paths share whatever characters a key holds (found while implementing #5105 —
|
|
109
|
+
* a dot-joined path read a top-level key named `a.b` as sub-key `b` of map `a`).
|
|
110
|
+
*/
|
|
111
|
+
function commentPathKey(segments) {
|
|
112
|
+
return JSON.stringify(segments);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The mapping key a line opens and the line's indentation, or null for a line that opens no
|
|
116
|
+
* key (a list item, a flow or scalar continuation, a comment). The key is read exactly as
|
|
117
|
+
* `segmentKeyOf` reads a top-level one — a quoted key unescaped, a plain key ending at the
|
|
118
|
+
* first `:` followed by whitespace — at any indentation. At indent > 0 the no-space bare-key
|
|
119
|
+
* fallback is disabled (see `segmentKeyOf`'s docblock): a nested continuation line like
|
|
120
|
+
* ` https://x` is not misread as opening key `https`.
|
|
121
|
+
*/
|
|
122
|
+
function channelKeyLine(line) {
|
|
123
|
+
const indent = /^\s*/.exec(line)?.[0].length ?? 0;
|
|
124
|
+
const k = segmentKeyOf(line.slice(indent), indent);
|
|
125
|
+
return k ? { indent, key: k.key } : null;
|
|
126
|
+
}
|
|
93
127
|
/**
|
|
94
128
|
* ADR-3473 §8.1 §0.3 (#3881, consequence 2): a Symbol-keyed marker carried on the `{}`
|
|
95
129
|
* `extractFrontmatter` returns when the region failed to parse (malformed YAML, or a refused
|
|
@@ -273,7 +307,7 @@ function restoreNullBytesDeep(value) {
|
|
|
273
307
|
* produced four different strings for those four spellings (ADR-3473 40-design.md §0.1). No
|
|
274
308
|
* adapter over a tree can recover a distinction the tree does not carry, so this renders a single
|
|
275
309
|
* canonical string per object-list item instead, keeping the existing value SHAPE (an array of
|
|
276
|
-
* strings) that `
|
|
310
|
+
* strings) that `sliceFrontmatterLayout`, the `[object Object]` guard and
|
|
277
311
|
* `noOpObjectListSetError` all depend on. Choosing structured (non-string) values is fork (b) —
|
|
278
312
|
* out of scope for this phase.
|
|
279
313
|
*/
|
|
@@ -343,25 +377,22 @@ function extractCommentChannel(yaml, orderedKeys) {
|
|
|
343
377
|
const lines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
344
378
|
// #3742: pending full-line comments carry their indentation so an INDENTED
|
|
345
379
|
// comment (` # note` above a nested key) can attach to the nested key that
|
|
346
|
-
// follows it — recorded under
|
|
347
|
-
// that reconstructFrontmatter re-emits at the same
|
|
348
|
-
// comments keep the exact pre-#3742 behavior
|
|
380
|
+
// follows it — recorded under its key path (`["progress","total_phases"]`,
|
|
381
|
+
// see `commentPathKey`) that reconstructFrontmatter re-emits at the same
|
|
382
|
+
// nesting depth. Column-0 comments keep the exact pre-#3742 behavior
|
|
383
|
+
// (top-level key attachment).
|
|
349
384
|
let pending = [];
|
|
350
385
|
let channel;
|
|
351
386
|
let keyIdx = 0;
|
|
352
|
-
// Stack of enclosing mapping keys with their indentation, for
|
|
387
|
+
// Stack of enclosing mapping keys with their indentation, for key-path
|
|
353
388
|
// construction on nested key lines. Only indented keys push here.
|
|
354
389
|
const pathStack = [];
|
|
355
|
-
const attach = (
|
|
390
|
+
const attach = (segments, comments) => {
|
|
356
391
|
if (!channel)
|
|
357
392
|
channel = { leading: Object.create(null), trailing: [] };
|
|
358
|
-
// Null-prototype `leading` (post-#3881-review, finding 3):
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
// through here. On an ordinary `{}` those resolve to inherited
|
|
362
|
-
// Object.prototype members; the null prototype makes every lookup an
|
|
363
|
-
// own-property-or-undefined read.
|
|
364
|
-
channel.leading[pathKey] = comments.map((c) => c.line);
|
|
393
|
+
// Null-prototype `leading` (post-#3881-review, finding 3): every lookup is an
|
|
394
|
+
// own-property-or-undefined read, whatever key text a user-authored path holds.
|
|
395
|
+
channel.leading[commentPathKey(segments)] = comments.map((c) => c.line);
|
|
365
396
|
};
|
|
366
397
|
for (const line of lines) {
|
|
367
398
|
if (line.trim() === '')
|
|
@@ -371,23 +402,18 @@ function extractCommentChannel(yaml, orderedKeys) {
|
|
|
371
402
|
pending.push({ indent: commentMatch[1].length, line });
|
|
372
403
|
continue;
|
|
373
404
|
}
|
|
374
|
-
// A list item (`- foo: bar`) is not a mapping key
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
? null
|
|
380
|
-
: /^(\s*)(?:"([^"]+)"|'([^']+)'|([^:\s][^:]*)):(?:\s|$)/.exec(line);
|
|
381
|
-
if (keyLineMatch) {
|
|
382
|
-
const indent = keyLineMatch[1].length;
|
|
383
|
-
const key = keyLineMatch[2] ?? keyLineMatch[3] ?? keyLineMatch[4];
|
|
405
|
+
// A list item (`- foo: bar`) is not a mapping key (#3742 review): `channelKeyLine`
|
|
406
|
+
// reads no key from it, so it falls through to the pending-drop below.
|
|
407
|
+
const keyLine = channelKeyLine(line);
|
|
408
|
+
if (keyLine) {
|
|
409
|
+
const { indent, key } = keyLine;
|
|
384
410
|
if (indent === 0) {
|
|
385
411
|
// Top-level: keep the pre-#3742 orderedKeys walk — the comment
|
|
386
412
|
// attaches only to the next EXPECTED top-level key.
|
|
387
413
|
if (keyIdx < orderedKeys.length && key === orderedKeys[keyIdx]) {
|
|
388
414
|
const col0 = pending.filter((c) => c.indent === 0);
|
|
389
415
|
if (col0.length)
|
|
390
|
-
attach(key, col0);
|
|
416
|
+
attach([key], col0);
|
|
391
417
|
keyIdx++;
|
|
392
418
|
// A top-level mapping key opens a nesting context for the indented
|
|
393
419
|
// keys that follow it (#3742 dotted-path attachment).
|
|
@@ -399,14 +425,14 @@ function extractCommentChannel(yaml, orderedKeys) {
|
|
|
399
425
|
}
|
|
400
426
|
else {
|
|
401
427
|
// Nested key line: a pending comment at the SAME indentation attaches
|
|
402
|
-
// to this key under its
|
|
428
|
+
// to this key under its key path. Deeper/misaligned pending
|
|
403
429
|
// comments were not leading this key — drop them, matching the
|
|
404
430
|
// top-level rule's "attach only when a key follows" discipline.
|
|
405
431
|
while (pathStack.length > 0 && pathStack[pathStack.length - 1].indent >= indent)
|
|
406
432
|
pathStack.pop();
|
|
407
433
|
const sameIndent = pending.filter((c) => c.indent === indent);
|
|
408
434
|
if (sameIndent.length && key.length > 0) {
|
|
409
|
-
attach([...pathStack.map((e) => e.key), key]
|
|
435
|
+
attach([...pathStack.map((e) => e.key), key], sameIndent);
|
|
410
436
|
}
|
|
411
437
|
pathStack.push({ indent, key });
|
|
412
438
|
pending = [];
|
|
@@ -675,28 +701,79 @@ function countTopLevelKeyShapedLines(region) {
|
|
|
675
701
|
* the same bytes the offsets were computed against.
|
|
676
702
|
*/
|
|
677
703
|
function frontmatterRegion(content) {
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// at all. The BOM is a single codepoint; stripping it here restores byte-0
|
|
684
|
-
// alignment. Scope: BOM only. Arbitrary non-BOM content before the fence
|
|
685
|
-
// (leading whitespace/blank line/comment) is a separate product-intent
|
|
686
|
-
// decision (tolerate vs diagnose) left to a future change.
|
|
687
|
-
if (content.charCodeAt(0) === 0xFEFF)
|
|
688
|
-
content = content.slice(1);
|
|
689
|
-
// Match frontmatter only at byte 0 — a `---` block later in the document body
|
|
690
|
-
// (YAML examples, horizontal rules) must never be treated as frontmatter.
|
|
691
|
-
const headerEnd = content.startsWith('---\r\n') ? 5 : content.startsWith('---\n') ? 4 : -1;
|
|
692
|
-
if (headerEnd === -1)
|
|
704
|
+
// The fence rules — BOM tolerance (#2977), the byte-0 opening fence, the whole-line closing
|
|
705
|
+
// fence, an adjacent empty block — live in `locateFrontmatterFence`, the one owner every
|
|
706
|
+
// fence consumer reads (found while implementing #5105: four copies of this answer disagreed).
|
|
707
|
+
const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
|
|
708
|
+
if (!fence)
|
|
693
709
|
return null;
|
|
694
|
-
const
|
|
695
|
-
if (
|
|
696
|
-
return { region: content.slice(
|
|
710
|
+
const stripped = content.slice(fence.bom.length);
|
|
711
|
+
if (!fence.closed) {
|
|
712
|
+
return { region: content.slice(fence.openEnd), terminated: false, content: stripped };
|
|
697
713
|
}
|
|
698
|
-
|
|
699
|
-
|
|
714
|
+
return { region: content.slice(fence.openEnd, fence.bodyEnd), terminated: true, content: stripped };
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* The closed frontmatter BLOCK of a document for a writer: `bom` (the leading BOM, or ''),
|
|
718
|
+
* `block` (from the opening `---` through the closing `---`, both fences included, no line
|
|
719
|
+
* ending after the closing fence) and `rest` (everything after it), so
|
|
720
|
+
* `bom + block + rest === content`. Read from `locateFrontmatterFence`, so a writer and every
|
|
721
|
+
* reader agree on where the block is — BOM, CRLF and an empty block included (found while
|
|
722
|
+
* implementing #5105: two private fence regexes disagreed with the reader on a BOM document
|
|
723
|
+
* and on a block holding only a blank line). Null when there is no block or it is unterminated.
|
|
724
|
+
*/
|
|
725
|
+
function frontmatterBlock(content) {
|
|
726
|
+
const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
|
|
727
|
+
if (!fence || !fence.closed)
|
|
728
|
+
return null;
|
|
729
|
+
return {
|
|
730
|
+
bom: fence.bom,
|
|
731
|
+
block: content.slice(fence.bom.length, fence.closingFenceEnd),
|
|
732
|
+
rest: content.slice(fence.closingFenceEnd),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* One top-level frontmatter key's block as RAW TEXT: the rest of the key's line plus every
|
|
737
|
+
* following blank or indented line (its nested value), or '' when the closed frontmatter block
|
|
738
|
+
* has no such key. The block is the one `frontmatterRegion` finds — the same fence every reader
|
|
739
|
+
* agrees on — and, unlike `rawFrontmatterField`, the text is returned WITHOUT parsing the YAML, so
|
|
740
|
+
* it survives frontmatter the parser refuses (a `--- x` line inside the block, say). For a caller
|
|
741
|
+
* that scans a block's text for a citation rather than reading its value — the decision-coverage
|
|
742
|
+
* gate's `must_haves` / `truths` / `objective` scan (#5139, moved here from the router).
|
|
743
|
+
* `key` is matched LITERALLY (regex-escaped), so no caller-supplied key can change the pattern.
|
|
744
|
+
*/
|
|
745
|
+
function frontmatterKeyBlockText(content, key) {
|
|
746
|
+
const found = frontmatterRegion(content);
|
|
747
|
+
if (!found || !found.terminated)
|
|
748
|
+
return '';
|
|
749
|
+
const match = found.region.match(new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}\\s*:(.*)$`, 'm'));
|
|
750
|
+
if (!match)
|
|
751
|
+
return '';
|
|
752
|
+
const startIdx = (match.index || 0) + match[0].length;
|
|
753
|
+
const rest = found.region.slice(startIdx + 1).split(/\r?\n/);
|
|
754
|
+
const block = [match[1] || ''];
|
|
755
|
+
for (const line of rest) {
|
|
756
|
+
if (line === '' || /^\s/.test(line))
|
|
757
|
+
block.push(line);
|
|
758
|
+
else
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
return block.join('\n');
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* True when the closed frontmatter block has a line `<key>:<spaces><value><spaces>` — the
|
|
765
|
+
* multiline test `^<key>:\s*<value>\s*$` over the fence owner's region, key and value matched
|
|
766
|
+
* LITERALLY (regex-escaped). The pattern is deliberately the one the `check tdd-review-checkpoint`
|
|
767
|
+
* gate always ran (`^type:\s*tdd\s*$`), so its detection is byte-equivalent to the old regex
|
|
768
|
+
* whatever the block holds (a duplicate key, a value on the next line, CRLF): the block is raw
|
|
769
|
+
* text, never parsed, so it classifies even when the YAML parser refuses the frontmatter. An
|
|
770
|
+
* unterminated or absent block is false. #5139.
|
|
771
|
+
*/
|
|
772
|
+
function frontmatterKeyHasValue(content, key, value) {
|
|
773
|
+
const found = frontmatterRegion(content);
|
|
774
|
+
if (!found || !found.terminated)
|
|
775
|
+
return false;
|
|
776
|
+
return new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:\\s*${(0, pattern_cjs_1.escapeRegex)(value)}\\s*$`, 'm').test(found.region);
|
|
700
777
|
}
|
|
701
778
|
function extractFrontmatter(content, sourcePath) {
|
|
702
779
|
// Fence location (BOM strip, byte-0 rule, CR handling) lives in
|
|
@@ -758,6 +835,22 @@ function extractFrontmatter(content, sourcePath) {
|
|
|
758
835
|
* "nothing to iterate" cases the caller should not have to tell apart.
|
|
759
836
|
*/
|
|
760
837
|
function frontmatterListEntries(content, key) {
|
|
838
|
+
const field = rawFrontmatterField(content, key);
|
|
839
|
+
// `Array.isArray` narrows an `unknown` to `any[]`, and returning that
|
|
840
|
+
// unchecked is how `any` escapes a guarded parser into every caller. The
|
|
841
|
+
// element type genuinely IS unknown here — that is the point of this
|
|
842
|
+
// function — so say so.
|
|
843
|
+
if (!field || !Array.isArray(field.value))
|
|
844
|
+
return null;
|
|
845
|
+
return field.value;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* One top-level frontmatter key's value VERBATIM — before the display flattening
|
|
849
|
+
* `extractFrontmatter` applies — off the same guarded parse path (`frontmatterRegion`, the
|
|
850
|
+
* anchor/alias and sentinel guards, the ambiguous-colon repair). Null when the document has
|
|
851
|
+
* no closed, parseable frontmatter mapping or the key is not an own key of it.
|
|
852
|
+
*/
|
|
853
|
+
function rawFrontmatterField(content, key) {
|
|
761
854
|
const found = frontmatterRegion(content);
|
|
762
855
|
if (!found || !found.terminated)
|
|
763
856
|
return null;
|
|
@@ -774,14 +867,9 @@ function frontmatterListEntries(content, key) {
|
|
|
774
867
|
}
|
|
775
868
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
776
869
|
return null;
|
|
777
|
-
|
|
778
|
-
// `Array.isArray` narrows an `unknown` to `any[]`, and returning that
|
|
779
|
-
// unchecked is how `any` escapes a guarded parser into every caller. The
|
|
780
|
-
// element type genuinely IS unknown here — that is the point of this
|
|
781
|
-
// function — so say so.
|
|
782
|
-
if (!Array.isArray(value))
|
|
870
|
+
if (!Object.prototype.hasOwnProperty.call(raw, key))
|
|
783
871
|
return null;
|
|
784
|
-
return value;
|
|
872
|
+
return { value: raw[key] };
|
|
785
873
|
}
|
|
786
874
|
/**
|
|
787
875
|
* Escape a string for emission inside a YAML double-quoted scalar (#1779). ADR-3473 §8.1
|
|
@@ -923,100 +1011,128 @@ function agentScalarNeedsDoubleQuoting(s) {
|
|
|
923
1011
|
function generalScalarNeedsNumericQuoting(s) {
|
|
924
1012
|
return YAML_NUMERIC_RE.test(s) && !/^\d+$/.test(s);
|
|
925
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* A list item `reconstructFrontmatter` may write inside an inline `[a, b]` list: a string
|
|
1016
|
+
* that reads back as itself there. A flow indicator (`,[]{}`), a `: ` or ` #` (a flow
|
|
1017
|
+
* mapping pair, a comment) or anything that needs quoting as a plain scalar would split,
|
|
1018
|
+
* truncate or re-type the item, so such a list is written in block form instead, where
|
|
1019
|
+
* `blockSequenceItem` quotes the item (found while implementing #5105).
|
|
1020
|
+
*/
|
|
1021
|
+
function isPlainFlowSequenceItem(item) {
|
|
1022
|
+
return typeof item === 'string' && !/[,[\]{}]|:(?:\s|$)|\s#/.test(item) && !scalarNeedsDoubleQuoting(item);
|
|
1023
|
+
}
|
|
1024
|
+
/** One block-sequence item as `reconstructFrontmatter` writes it: quoted whenever bare would misread. */
|
|
1025
|
+
function blockSequenceItem(item) {
|
|
1026
|
+
// A null item reads back as '' (see `readBackProjection`), so it is written as one.
|
|
1027
|
+
if (item === null || item === undefined)
|
|
1028
|
+
return '""';
|
|
1029
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
1030
|
+
if (typeof item !== 'string')
|
|
1031
|
+
return String(item);
|
|
1032
|
+
return item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item) ? `"${escapeDoubleQuotedScalar(item)}"` : item;
|
|
1033
|
+
}
|
|
1034
|
+
/** A nested mapping's scalar value as `reconstructFrontmatter` writes it: quoted whenever bare would misread. */
|
|
1035
|
+
function nestedScalar(value) {
|
|
1036
|
+
const sv = String(value);
|
|
1037
|
+
return sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) || generalScalarNeedsNumericQuoting(sv) ? `"${escapeDoubleQuotedScalar(sv)}"` : sv;
|
|
1038
|
+
}
|
|
926
1039
|
function reconstructFrontmatter(obj) {
|
|
927
1040
|
const lines = [];
|
|
928
1041
|
// #3257: read the full-line-comment channel (set by parseGuardedYamlRegion when comments
|
|
929
1042
|
// were present). Object.entries skips the Symbol key, so the data loop is unchanged.
|
|
930
1043
|
const commentChannel = obj[FULL_LINE_COMMENTS];
|
|
1044
|
+
// A key's leading full-line comments and its inline comment, by exact key path.
|
|
1045
|
+
const leadingOf = (segments) => commentChannel?.leading[commentPathKey(segments)];
|
|
1046
|
+
const inlineOf = (segments) => commentChannel?.inline?.[commentPathKey(segments)] ?? '';
|
|
931
1047
|
for (const [key, value] of Object.entries(obj)) {
|
|
932
1048
|
if (value === null || value === undefined)
|
|
933
1049
|
continue;
|
|
934
1050
|
// #3257: re-emit this key's leading full-line comments before the key itself.
|
|
935
|
-
const leading =
|
|
1051
|
+
const leading = leadingOf([key]);
|
|
936
1052
|
if (leading)
|
|
937
1053
|
for (const c of leading)
|
|
938
1054
|
lines.push(c);
|
|
1055
|
+
const inline = inlineOf([key]);
|
|
939
1056
|
if (Array.isArray(value)) {
|
|
940
1057
|
if (value.length === 0) {
|
|
941
|
-
lines.push(`${key}: []`);
|
|
1058
|
+
lines.push(`${key}: []${inline}`);
|
|
942
1059
|
}
|
|
943
|
-
else if (value.every(
|
|
944
|
-
lines.push(`${key}: [${(value).join(', ')}]`);
|
|
1060
|
+
else if (value.every(isPlainFlowSequenceItem) && value.length <= 3 && (value).join(', ').length < 60) {
|
|
1061
|
+
lines.push(`${key}: [${(value).join(', ')}]${inline}`);
|
|
945
1062
|
}
|
|
946
1063
|
else {
|
|
947
|
-
lines.push(`${key}
|
|
1064
|
+
lines.push(`${key}:${inline}`);
|
|
948
1065
|
for (const item of value) {
|
|
949
|
-
lines.push(` - ${
|
|
1066
|
+
lines.push(` - ${blockSequenceItem(item)}`);
|
|
950
1067
|
}
|
|
951
1068
|
}
|
|
952
1069
|
}
|
|
953
1070
|
else if (typeof value === 'object') {
|
|
954
|
-
lines.push(`${key}
|
|
1071
|
+
lines.push(`${key}:${inline}`);
|
|
955
1072
|
for (const [subkey, subval] of Object.entries(value)) {
|
|
956
1073
|
if (subval === null || subval === undefined)
|
|
957
1074
|
continue;
|
|
958
1075
|
// #3742: re-emit a nested key's leading full-line comments (channel
|
|
959
|
-
// path
|
|
960
|
-
const nestedLeading =
|
|
1076
|
+
// path `[parent, subkey]`) at the subkey's own indentation.
|
|
1077
|
+
const nestedLeading = leadingOf([key, subkey]);
|
|
961
1078
|
if (nestedLeading)
|
|
962
1079
|
for (const c of nestedLeading)
|
|
963
1080
|
lines.push(` ${c.trimStart()}`);
|
|
1081
|
+
const subInline = inlineOf([key, subkey]);
|
|
964
1082
|
if (Array.isArray(subval)) {
|
|
965
1083
|
if (subval.length === 0) {
|
|
966
|
-
lines.push(` ${subkey}: []`);
|
|
1084
|
+
lines.push(` ${subkey}: []${subInline}`);
|
|
967
1085
|
}
|
|
968
|
-
else if (subval.every(
|
|
969
|
-
lines.push(` ${subkey}: [${(subval).join(', ')}]`);
|
|
1086
|
+
else if (subval.every(isPlainFlowSequenceItem) && subval.length <= 3 && (subval).join(', ').length < 60) {
|
|
1087
|
+
lines.push(` ${subkey}: [${(subval).join(', ')}]${subInline}`);
|
|
970
1088
|
}
|
|
971
1089
|
else {
|
|
972
|
-
lines.push(` ${subkey}
|
|
1090
|
+
lines.push(` ${subkey}:${subInline}`);
|
|
973
1091
|
for (const item of subval) {
|
|
974
|
-
lines.push(` - ${
|
|
1092
|
+
lines.push(` - ${blockSequenceItem(item)}`);
|
|
975
1093
|
}
|
|
976
1094
|
}
|
|
977
1095
|
}
|
|
978
1096
|
else if (typeof subval === 'object') {
|
|
979
|
-
lines.push(` ${subkey}
|
|
1097
|
+
lines.push(` ${subkey}:${subInline}`);
|
|
980
1098
|
for (const [subsubkey, subsubval] of Object.entries(subval)) {
|
|
981
1099
|
if (subsubval === null || subsubval === undefined)
|
|
982
1100
|
continue;
|
|
983
1101
|
// #3742: same nested-comment re-emission one level deeper
|
|
984
|
-
// (`parent
|
|
985
|
-
const deepLeading =
|
|
1102
|
+
// (`[parent, sub, subsub]`).
|
|
1103
|
+
const deepLeading = leadingOf([key, subkey, subsubkey]);
|
|
986
1104
|
if (deepLeading)
|
|
987
1105
|
for (const c of deepLeading)
|
|
988
1106
|
lines.push(` ${c.trimStart()}`);
|
|
1107
|
+
const deepInline = inlineOf([key, subkey, subsubkey]);
|
|
989
1108
|
if (Array.isArray(subsubval)) {
|
|
990
1109
|
if (subsubval.length === 0) {
|
|
991
|
-
lines.push(` ${subsubkey}: []`);
|
|
1110
|
+
lines.push(` ${subsubkey}: []${deepInline}`);
|
|
992
1111
|
}
|
|
993
1112
|
else {
|
|
994
|
-
lines.push(` ${subsubkey}
|
|
1113
|
+
lines.push(` ${subsubkey}:${deepInline}`);
|
|
995
1114
|
for (const item of subsubval) {
|
|
996
|
-
lines.push(` - ${item}`);
|
|
1115
|
+
lines.push(` - ${blockSequenceItem(item)}`);
|
|
997
1116
|
}
|
|
998
1117
|
}
|
|
999
1118
|
}
|
|
1000
1119
|
else {
|
|
1001
|
-
|
|
1002
|
-
lines.push(` ${subsubkey}: ${subsubval}`);
|
|
1120
|
+
lines.push(` ${subsubkey}: ${nestedScalar(subsubval)}${deepInline}`);
|
|
1003
1121
|
}
|
|
1004
1122
|
}
|
|
1005
1123
|
}
|
|
1006
1124
|
else {
|
|
1007
|
-
|
|
1008
|
-
const sv = String(subval);
|
|
1009
|
-
lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) || generalScalarNeedsNumericQuoting(sv) ? `"${escapeDoubleQuotedScalar(sv)}"` : sv}`);
|
|
1125
|
+
lines.push(` ${subkey}: ${nestedScalar(subval)}${subInline}`);
|
|
1010
1126
|
}
|
|
1011
1127
|
}
|
|
1012
1128
|
}
|
|
1013
1129
|
else {
|
|
1014
1130
|
const sv = String(value);
|
|
1015
1131
|
if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{') || scalarNeedsDoubleQuoting(sv) || generalScalarNeedsNumericQuoting(sv)) {
|
|
1016
|
-
lines.push(`${key}: "${escapeDoubleQuotedScalar(sv)}"`);
|
|
1132
|
+
lines.push(`${key}: "${escapeDoubleQuotedScalar(sv)}"${inline}`);
|
|
1017
1133
|
}
|
|
1018
1134
|
else {
|
|
1019
|
-
lines.push(`${key}: ${sv}`);
|
|
1135
|
+
lines.push(`${key}: ${sv}${inline}`);
|
|
1020
1136
|
}
|
|
1021
1137
|
}
|
|
1022
1138
|
}
|
|
@@ -1042,8 +1158,8 @@ function propagateCommentChannel(source, target) {
|
|
|
1042
1158
|
return;
|
|
1043
1159
|
// #3742: two changes, both about a target that is a PARTIAL rebuild.
|
|
1044
1160
|
//
|
|
1045
|
-
// (a) Root-segment membership: a comment keyed by a
|
|
1046
|
-
// (`progress
|
|
1161
|
+
// (a) Root-segment membership: a comment keyed by a nested path
|
|
1162
|
+
// (`["progress","total_plans"]`) survives while its root section survives —
|
|
1047
1163
|
// requiring the full path to resolve inside `target` would drop every
|
|
1048
1164
|
// nested comment the moment the rebuild reconstructed the section
|
|
1049
1165
|
// object (a fresh object with the same leaf keys still matches at
|
|
@@ -1054,8 +1170,10 @@ function propagateCommentChannel(source, target) {
|
|
|
1054
1170
|
// concatenate (source first, mirroring document order when the source
|
|
1055
1171
|
// is the earlier snapshot).
|
|
1056
1172
|
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
1173
|
+
// The root is the path's first segment (`commentPathKey`), never text before a `.` — a
|
|
1174
|
+
// top-level key named `a.b` is its own root (found while implementing #5105).
|
|
1057
1175
|
const rootAlive = (k) => {
|
|
1058
|
-
const root =
|
|
1176
|
+
const root = JSON.parse(k)[0];
|
|
1059
1177
|
return hasOwn(target, root);
|
|
1060
1178
|
};
|
|
1061
1179
|
// Null-prototype `leading` (post-#3881-review, finding 3) — same rationale as
|
|
@@ -1086,130 +1204,64 @@ function propagateCommentChannel(source, target) {
|
|
|
1086
1204
|
target[FULL_LINE_COMMENTS] = merged;
|
|
1087
1205
|
}
|
|
1088
1206
|
}
|
|
1207
|
+
/** Column-0 quoted key: `"…":` (JSON-style escapes) or `'…':` (`''` escapes a quote). */
|
|
1208
|
+
const QUOTED_SEGMENT_KEY_RE = /^(?:"((?:[^"\\]|\\.)*)"|'((?:[^']|'')*)')[ \t]*:/;
|
|
1089
1209
|
/**
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* not modify (e.g. must_haves.artifacts / .prohibitions).
|
|
1210
|
+
* First character of a plain (unquoted) top-level key. A comment, an indented line, a
|
|
1211
|
+
* quote and a YAML indicator (`-`/`?`/`:` followed by whitespace or end of line, or any
|
|
1212
|
+
* of `,[]{}&*!|>%@` and backtick) never start one — those lines are never a key line, so
|
|
1213
|
+
* a key the parser reads from them (an explicit `? k` key, a flow mapping) has no segment
|
|
1214
|
+
* and `spliceFrontmatter` refuses rather than duplicating it.
|
|
1096
1215
|
*/
|
|
1097
|
-
|
|
1098
|
-
const lines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
1099
|
-
const segments = [];
|
|
1100
|
-
let current = null;
|
|
1101
|
-
for (const line of lines) {
|
|
1102
|
-
// A column-0 `key:` (no leading whitespace) starts a new top-level segment.
|
|
1103
|
-
if (/^[A-Za-z0-9_-]+:/.test(line)) {
|
|
1104
|
-
if (current)
|
|
1105
|
-
segments.push({ key: current.key, raw: current.raw.join('\n') });
|
|
1106
|
-
const keyName = line.match(/^([A-Za-z0-9_-]+):/)[1];
|
|
1107
|
-
current = { key: keyName, raw: [line] };
|
|
1108
|
-
}
|
|
1109
|
-
else if (current) {
|
|
1110
|
-
current.raw.push(line);
|
|
1111
|
-
}
|
|
1112
|
-
// Stray lines before the first top-level key (rare in frontmatter) are dropped.
|
|
1113
|
-
}
|
|
1114
|
-
if (current)
|
|
1115
|
-
segments.push({ key: current.key, raw: current.raw.join('\n') });
|
|
1116
|
-
return segments;
|
|
1117
|
-
}
|
|
1216
|
+
const PLAIN_KEY_START_RE = /^(?:[^\s#,[\]{}&*!|>'"%@`\-?:]|[-?:](?=[^\s]))/;
|
|
1118
1217
|
/**
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1218
|
+
* Where the key of a column-0 line ends, agreeing with the YAML parser: the key is
|
|
1219
|
+
* everything before the FIRST `:` followed by whitespace or end of line (so `a:b: 1` is
|
|
1220
|
+
* key `a:b` and `http://x: 1` is key `http://x`, exactly as js-yaml reads them). Only when
|
|
1221
|
+
* no such colon exists does the no-space `key:value` spelling (`updated:2026-01-01`) fall
|
|
1222
|
+
* back to the bare-ASCII key before the first `:`. Returns null for a line that is not a
|
|
1223
|
+
* top-level key line.
|
|
1224
|
+
*
|
|
1225
|
+
* `indent` is the caller's indentation of `line` (0 for the genuinely column-0 callers —
|
|
1226
|
+
* `sliceFrontmatterLayout` — which never see a nonzero value since a real indented line
|
|
1227
|
+
* already fails `PLAIN_KEY_START_RE` there). `channelKeyLine` (#5105 follow-up) calls this
|
|
1228
|
+
* on an INDENT-STRIPPED nested line instead, to read nested keys like `total_phases: 5` —
|
|
1229
|
+
* but the no-space bare-ASCII fallback exists only for the top-level `key:value` shorthand a
|
|
1230
|
+
* document author actually writes; on a nested continuation line (a URL, a path, or any
|
|
1231
|
+
* other value line of a multi-line scalar that happens to contain an ASCII word immediately
|
|
1232
|
+
* followed by `:`, e.g. `https://x`) it misreads the line as opening a key. Restricting the
|
|
1233
|
+
* fallback to `indent === 0` keeps the top-level shorthand working while a nested line only
|
|
1234
|
+
* ever opens a key when the colon is unambiguously followed by whitespace or end of line.
|
|
1126
1235
|
*/
|
|
1127
|
-
function
|
|
1128
|
-
const
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return rendered;
|
|
1135
|
-
}
|
|
1136
|
-
function spliceFrontmatter(content, newObj) {
|
|
1137
|
-
const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
|
|
1138
|
-
if (match) {
|
|
1139
|
-
const fmBlock = match[0];
|
|
1140
|
-
// Whole-document no-op guard: a true no-op returns content verbatim (byte-exact,
|
|
1141
|
-
// including any formatting the lossy serializer would normalize).
|
|
1142
|
-
try {
|
|
1143
|
-
if (frontmatterDeepEqual(extractFrontmatter(content), newObj)) {
|
|
1144
|
-
return content;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
catch {
|
|
1148
|
-
/* fall through to regeneration on any comparison hiccup */
|
|
1149
|
-
}
|
|
1150
|
-
// Per-key identity preservation (#1572). `reconstructFrontmatter` is a deliberately
|
|
1151
|
-
// lossy serializer — it cannot faithfully re-emit nested object-list items (e.g.
|
|
1152
|
-
// must_haves.artifacts / .prohibitions, whose items are `{ path, provides }` /
|
|
1153
|
-
// `{ statement, status }` maps; `extractFrontmatter` flattens those to scalar
|
|
1154
|
-
// strings, so a round-trip drops `provides:` and collapses the list to a malformed
|
|
1155
|
-
// inline array). For any top-level key whose value is STRUCTURALLY UNCHANGED between
|
|
1156
|
-
// the original parse and `newObj`, preserve that key's ORIGINAL raw text verbatim;
|
|
1157
|
-
// regenerate only keys that actually changed. This generalizes the whole-document
|
|
1158
|
-
// no-op guard above to per-key fidelity, so mutating `wave` no longer destroys an
|
|
1159
|
-
// unrelated `must_haves` block. Keys absent from the original (genuinely new) are
|
|
1160
|
-
// regenerated and appended; keys absent from `newObj` are preserved (never silently
|
|
1161
|
-
// deleted by a set/merge).
|
|
1162
|
-
const fmLines = (0, text_lines_cjs_1.splitLines)(fmBlock);
|
|
1163
|
-
const inner = fmLines.slice(1, -1).join('\n'); // drop the opening `---` and closing `---`
|
|
1164
|
-
let originalParsed;
|
|
1165
|
-
try {
|
|
1166
|
-
originalParsed = extractFrontmatter(fmBlock);
|
|
1167
|
-
}
|
|
1168
|
-
catch {
|
|
1169
|
-
originalParsed = {};
|
|
1170
|
-
}
|
|
1171
|
-
const segments = sliceTopLevelFrontmatterSegments(inner);
|
|
1172
|
-
const emitted = [];
|
|
1173
|
-
const seen = new Set();
|
|
1174
|
-
for (const seg of segments) {
|
|
1175
|
-
seen.add(seg.key);
|
|
1176
|
-
if (Object.prototype.hasOwnProperty.call(newObj, seg.key)) {
|
|
1177
|
-
// Key is in newObj: preserve original raw text if structurally unchanged,
|
|
1178
|
-
// otherwise regenerate. The key SET is defined by newObj — keys that were in
|
|
1179
|
-
// the original but are absent from newObj are intentionally dropped (the real
|
|
1180
|
-
// cmdSet/cmdMerge flow always passes the full merged object, so this only
|
|
1181
|
-
// matters for direct unit callers and matches spliceFrontmatter's contract:
|
|
1182
|
-
// the result frontmatter IS newObj).
|
|
1183
|
-
if (frontmatterDeepEqual(newObj[seg.key], originalParsed[seg.key])) {
|
|
1184
|
-
emitted.push(seg.raw); // unchanged → preserve original raw text verbatim
|
|
1185
|
-
}
|
|
1186
|
-
else {
|
|
1187
|
-
emitted.push(regenerateFrontmatterKey(seg.key, newObj[seg.key])); // changed → regenerate (fail-closed on object-lists)
|
|
1188
|
-
}
|
|
1236
|
+
function segmentKeyOf(line, indent = 0) {
|
|
1237
|
+
const q = QUOTED_SEGMENT_KEY_RE.exec(line);
|
|
1238
|
+
if (q) {
|
|
1239
|
+
const valueStart = q[0].length;
|
|
1240
|
+
if (q[1] !== undefined) {
|
|
1241
|
+
try {
|
|
1242
|
+
return { key: JSON.parse(`"${q[1]}"`), valueStart };
|
|
1189
1243
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
// Append genuinely-new keys not present in the original frontmatter.
|
|
1193
|
-
for (const k of Object.keys(newObj)) {
|
|
1194
|
-
if (!seen.has(k)) {
|
|
1195
|
-
emitted.push(regenerateFrontmatterKey(k, newObj[k]));
|
|
1244
|
+
catch {
|
|
1245
|
+
return { key: q[1], valueStart };
|
|
1196
1246
|
}
|
|
1197
1247
|
}
|
|
1198
|
-
|
|
1199
|
-
return `---\n${yamlStr}\n---` + content.slice(fmBlock.length);
|
|
1200
|
-
}
|
|
1201
|
-
// No existing frontmatter — generate from scratch, fail-closed on unrepresentable values.
|
|
1202
|
-
const yamlStr = reconstructFrontmatter(newObj);
|
|
1203
|
-
if (/\[object Object\]/.test(yamlStr)) {
|
|
1204
|
-
throw new Error('frontmatter: cannot faithfully serialize the requested frontmatter — it contains a nested ' +
|
|
1205
|
-
'object-list (e.g. must_haves.artifacts) the writer cannot represent. Edit the file directly.');
|
|
1248
|
+
return { key: q[2].replace(/''/g, "'"), valueStart };
|
|
1206
1249
|
}
|
|
1207
|
-
|
|
1250
|
+
if (!PLAIN_KEY_START_RE.test(line))
|
|
1251
|
+
return null;
|
|
1252
|
+
const spaced = /:(?:[ \t]|$)/.exec(line);
|
|
1253
|
+
if (spaced)
|
|
1254
|
+
return { key: line.slice(0, spaced.index).trimEnd(), valueStart: spaced.index + 1 };
|
|
1255
|
+
if (indent > 0)
|
|
1256
|
+
return null;
|
|
1257
|
+
const bare = /^([A-Za-z0-9_-]+):/.exec(line);
|
|
1258
|
+
return bare ? { key: bare[1], valueStart: bare[0].length } : null;
|
|
1208
1259
|
}
|
|
1209
1260
|
/**
|
|
1210
1261
|
* Structural deep-equality for two parsed frontmatter objects. Order-sensitive for arrays
|
|
1211
|
-
* (YAML lists are ordered), key-order-insensitive for objects. Used
|
|
1212
|
-
* to recognize a no-op write-back
|
|
1262
|
+
* (YAML lists are ordered), key-order-insensitive for objects. Used by the writer
|
|
1263
|
+
* (`frontmatter-splice.cts`, through `spliceSeam`) to recognize a no-op write-back, and by
|
|
1264
|
+
* `objectListFieldWouldLoseData` below; intentionally narrow (handles the string / string[] /
|
|
1213
1265
|
* nested-object shapes `extractFrontmatter` produces).
|
|
1214
1266
|
*/
|
|
1215
1267
|
function frontmatterDeepEqual(a, b) {
|
|
@@ -1285,10 +1337,12 @@ function normalizeMustHavesItem(item) {
|
|
|
1285
1337
|
* `.planning/` must_haves blocks are untrusted input exactly like the rest of frontmatter.
|
|
1286
1338
|
*/
|
|
1287
1339
|
function parseMustHavesBlock(content, blockName) {
|
|
1288
|
-
|
|
1289
|
-
|
|
1340
|
+
// Located through the one fence owner, so a BOM document's must_haves are read like an
|
|
1341
|
+
// LF or CRLF one's (found while implementing #5105).
|
|
1342
|
+
const found = frontmatterRegion(content);
|
|
1343
|
+
if (!found || !found.terminated)
|
|
1290
1344
|
return [];
|
|
1291
|
-
const yaml =
|
|
1345
|
+
const yaml = found.region;
|
|
1292
1346
|
let parsed;
|
|
1293
1347
|
try {
|
|
1294
1348
|
refuseAnchorsAndAliases(yaml);
|
|
@@ -1315,6 +1369,16 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
1315
1369
|
}
|
|
1316
1370
|
return block.map(normalizeMustHavesItem);
|
|
1317
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* The frontmatter WRITER (`frontmatter-splice.cts`, split out of this module by #5105), required
|
|
1374
|
+
* LAZILY: that module requires this one at load time for the parser it builds on, so requiring
|
|
1375
|
+
* it here at load time would hand one of the two a half-built export object. Called only from
|
|
1376
|
+
* the re-export getters and the set/merge commands below — never while this module loads.
|
|
1377
|
+
*/
|
|
1378
|
+
function spliceModule() {
|
|
1379
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1380
|
+
return require('./frontmatter-splice.cjs');
|
|
1381
|
+
}
|
|
1318
1382
|
// ─── Frontmatter CRUD commands ────────────────────────────────────────────────
|
|
1319
1383
|
// Shared base for 'plan' and 'plan-gap-closure' below — a plain array reference (not
|
|
1320
1384
|
// FRONTMATTER_SCHEMAS.plan.required) because the object literal that defines
|
|
@@ -1364,14 +1428,29 @@ const FRONTMATTER_SCHEMAS = {
|
|
|
1364
1428
|
*
|
|
1365
1429
|
* Canonical home for this primitive (#2143 audit dedup): previously
|
|
1366
1430
|
* duplicated byte-identically in both `state.cts` and `state-transition.cts`.
|
|
1431
|
+
*
|
|
1432
|
+
* Each block is the one `locateFrontmatterFence` finds — the same block every reader and
|
|
1433
|
+
* writer sees — and the whitespace after its closing fence goes with it. Found while
|
|
1434
|
+
* implementing #5105: the previous regex needed a line ending before the closing `---` that
|
|
1435
|
+
* the opening fence's own line ending could not supply, so it could not see an adjacent empty
|
|
1436
|
+
* block (`---\n---\n`) and stripped through the first `---` in the BODY instead; it also
|
|
1437
|
+
* closed on any line merely starting with `---`, which no reader of the block agreed with.
|
|
1438
|
+
*
|
|
1439
|
+
* Whitespace BEFORE the opening fence is skipped here, and only here: readers see no block in
|
|
1440
|
+
* such a document, but this is the writer's strip step (`state update` strips the old block and
|
|
1441
|
+
* writes a new one), and not skipping it would stack a second block above the stale one. The
|
|
1442
|
+
* whitespace goes only when a closed block follows it; otherwise `content` is returned as is.
|
|
1367
1443
|
*/
|
|
1368
1444
|
function stripFrontmatter(content, opts = {}) {
|
|
1369
1445
|
let result = content;
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1446
|
+
const unindented = content.replace(/^\s+/, '');
|
|
1447
|
+
if (unindented !== content && (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(unindented)?.closed)
|
|
1448
|
+
result = unindented;
|
|
1449
|
+
for (;;) {
|
|
1450
|
+
const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(result);
|
|
1451
|
+
if (!fence || !fence.closed)
|
|
1373
1452
|
break;
|
|
1374
|
-
result =
|
|
1453
|
+
result = result.slice(fence.closingFenceEnd).replace(/^\s*/, '');
|
|
1375
1454
|
if (opts.once)
|
|
1376
1455
|
break;
|
|
1377
1456
|
}
|
|
@@ -1394,6 +1473,14 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
|
|
|
1394
1473
|
// Pass the resolved path so a truncated file is named in the diagnostic and deduplicated
|
|
1395
1474
|
// per file rather than per content digest (#1882, ADR-1411 wiring clause).
|
|
1396
1475
|
const fm = extractFrontmatter(content, fullPath);
|
|
1476
|
+
// #4806: an unparseable frontmatter block is a distinct outcome — the file
|
|
1477
|
+
// HAS a frontmatter block but its YAML failed to parse. Reporting
|
|
1478
|
+
// "Field not found" tells the caller the key is absent, which is
|
|
1479
|
+
// indistinguishable from a file that genuinely lacks it.
|
|
1480
|
+
if (fm[FRONTMATTER_UNPARSEABLE] === true) {
|
|
1481
|
+
output({ error: 'Frontmatter is not parseable YAML — fix the syntax error in the frontmatter block', path: filePath }, raw, undefined);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1397
1484
|
if (field) {
|
|
1398
1485
|
const value = fm[field];
|
|
1399
1486
|
if (value === undefined) {
|
|
@@ -1406,6 +1493,43 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
|
|
|
1406
1493
|
output(fm, raw, undefined);
|
|
1407
1494
|
}
|
|
1408
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* A field name is one line of a YAML key: a line break, NUL or other control character in it
|
|
1498
|
+
* is never an intended key name. The one check `frontmatter set` and `frontmatter merge`
|
|
1499
|
+
* share (found while implementing #5105).
|
|
1500
|
+
*/
|
|
1501
|
+
function rejectControlCharacterFieldName(field) {
|
|
1502
|
+
if (/[\u0000-\u001f\u007f]/.test(field)) {
|
|
1503
|
+
error('field name contains a control character (a line break, tab, NUL or other C0/DEL character) — use a plain key name');
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Write one field as an own data property. `fm[field] =` and `Object.assign` treat a field
|
|
1508
|
+
* named `__proto__` as the prototype setter, so the key was never written while the command
|
|
1509
|
+
* reported success (found while implementing #5105).
|
|
1510
|
+
*/
|
|
1511
|
+
function setOwnField(fm, field, value) {
|
|
1512
|
+
Object.defineProperty(fm, field, { value, writable: true, enumerable: true, configurable: true });
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* `spliceFrontmatter` for the set/merge commands: a write refusal (`FrontmatterWriteRefusedError`
|
|
1516
|
+
* — unparseable block, unreconcilable keys, a block that would not read back, a comment that
|
|
1517
|
+
* would be lost, a block too complex to classify within the parse budget) is reported as `{ error, code, path }` and nothing is written — the same
|
|
1518
|
+
* shape `cmdFrontmatterGet` uses for an unparseable block. Returns null when refused; any
|
|
1519
|
+
* other error propagates as before.
|
|
1520
|
+
*/
|
|
1521
|
+
function spliceOrReportRefusal(content, fm, filePath, raw) {
|
|
1522
|
+
const { spliceFrontmatter, isFrontmatterWriteRefusal } = spliceModule();
|
|
1523
|
+
try {
|
|
1524
|
+
return spliceFrontmatter(content, fm);
|
|
1525
|
+
}
|
|
1526
|
+
catch (err) {
|
|
1527
|
+
if (!isFrontmatterWriteRefusal(err))
|
|
1528
|
+
throw err;
|
|
1529
|
+
output({ error: err.message, code: err.code, path: filePath }, raw, undefined);
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1409
1533
|
function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
1410
1534
|
if (!filePath || !field || value === undefined) {
|
|
1411
1535
|
error('file, field, and value required');
|
|
@@ -1414,6 +1538,7 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
|
1414
1538
|
if (filePath.includes('\0')) {
|
|
1415
1539
|
error('file path contains null bytes');
|
|
1416
1540
|
}
|
|
1541
|
+
rejectControlCharacterFieldName(field);
|
|
1417
1542
|
const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
|
|
1418
1543
|
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
1419
1544
|
output({ error: 'File not found', path: filePath }, raw, undefined);
|
|
@@ -1438,8 +1563,10 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
|
1438
1563
|
output({ error: lossyErr, field }, raw, undefined);
|
|
1439
1564
|
return;
|
|
1440
1565
|
}
|
|
1441
|
-
fm
|
|
1442
|
-
const newContent =
|
|
1566
|
+
setOwnField(fm, field, parsedValue);
|
|
1567
|
+
const newContent = spliceOrReportRefusal(content, fm, filePath, raw);
|
|
1568
|
+
if (newContent === null)
|
|
1569
|
+
return;
|
|
1443
1570
|
// #1660: a no-op set (newContent unchanged) with a dict-valued field means the lossy
|
|
1444
1571
|
// frontmatter parser made the new value's projection equal the original's — the change
|
|
1445
1572
|
// did not apply (bites object-list fields like must_haves). Detection lives in the pure
|
|
@@ -1480,15 +1607,31 @@ function noOpObjectListSetError(originalContent, newContent, parsedValue) {
|
|
|
1480
1607
|
* no-op guard, sails through `regenerateFrontmatterKey` (which only refuses when the NEW value
|
|
1481
1608
|
* itself contains a live JS object), and silently writes a version with `provides` gone.
|
|
1482
1609
|
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
* field (not merely re-supplying an equal value,
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1610
|
+
* A field is lossy exactly when the parse a caller sees FLATTENED it: its verbatim YAML value
|
|
1611
|
+
* (`rawFrontmatterField`) holds a list item that is itself a mapping or a list, which
|
|
1612
|
+
* `extractFrontmatter` hands back as one display string, so any replacement a caller builds
|
|
1613
|
+
* from that view silently drops the item's structure. When that is true AND the caller is
|
|
1614
|
+
* genuinely changing the field (not merely re-supplying an equal value), the set is refused —
|
|
1615
|
+
* matching `regenerateFrontmatterKey`'s own fail-closed philosophy for the mirror-image case
|
|
1616
|
+
* (new value carries a nested object outright).
|
|
1617
|
+
*
|
|
1618
|
+
* Found while implementing #5105: this used to compare the field's regenerated text against
|
|
1619
|
+
* its source text, which refused every field merely written in another style — a quoted
|
|
1620
|
+
* scalar (`title: 'x'`), a trailing comment, a block list, a multi-line value — although the
|
|
1621
|
+
* parse had lost nothing. What the caller replaces is its own business; only data the caller
|
|
1622
|
+
* could not see is protected here, and whether the NEW value reads back is `spliceFrontmatter`'s
|
|
1623
|
+
* read-back check.
|
|
1490
1624
|
*/
|
|
1625
|
+
function holdsFlattenedListItem(value) {
|
|
1626
|
+
if (Array.isArray(value))
|
|
1627
|
+
return value.some((item) => (item !== null && typeof item === 'object') || holdsFlattenedListItem(item));
|
|
1628
|
+
if (value !== null && typeof value === 'object')
|
|
1629
|
+
return Object.values(value).some(holdsFlattenedListItem);
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
/** See `holdsFlattenedListItem`: the #1660 lossy object-list refusal for one changed field. */
|
|
1491
1633
|
function objectListFieldWouldLoseData(content, field, newValue) {
|
|
1634
|
+
const { regenerateFrontmatterKey, readBackProjection } = spliceModule();
|
|
1492
1635
|
// A NEW value that itself carries a live nested object (rather than an already-flattened
|
|
1493
1636
|
// string) is the mirror-image case `regenerateFrontmatterKey` already refuses on its own
|
|
1494
1637
|
// (the "[object Object]" guard, via spliceFrontmatter) — leave that path's existing throw
|
|
@@ -1508,25 +1651,13 @@ function objectListFieldWouldLoseData(content, field, newValue) {
|
|
|
1508
1651
|
}
|
|
1509
1652
|
if (!Object.prototype.hasOwnProperty.call(originalParsed, field))
|
|
1510
1653
|
return null;
|
|
1511
|
-
|
|
1512
|
-
if (frontmatterDeepEqual(newValue, originalValue))
|
|
1513
|
-
return null;
|
|
1514
|
-
const fmMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
1515
|
-
if (!fmMatch)
|
|
1516
|
-
return null;
|
|
1517
|
-
const original = sliceTopLevelFrontmatterSegments(fmMatch[1]).find((s) => s.key === field);
|
|
1518
|
-
if (!original)
|
|
1654
|
+
if (frontmatterDeepEqual(readBackProjection(newValue), originalParsed[field]))
|
|
1519
1655
|
return null;
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
return `frontmatter set refused — the existing "${field}" field contains a nested object-list ` +
|
|
1526
|
-
`(e.g. must_haves.artifacts) the frontmatter writer cannot faithfully represent, and this change ` +
|
|
1527
|
-
`would silently discard data. Edit the file directly instead of using frontmatter set/merge.`;
|
|
1528
|
-
}
|
|
1529
|
-
if (regeneratedOriginal.trim() === original.raw.trim())
|
|
1656
|
+
// The verbatim value is read through the one fence owner (`frontmatterRegion`), so a BOM
|
|
1657
|
+
// document is refused exactly like an LF or CRLF one, and any key spelling (bare, quoted,
|
|
1658
|
+
// Unicode) is found by its parsed key (found while implementing #5105).
|
|
1659
|
+
const original = rawFrontmatterField(content, field);
|
|
1660
|
+
if (!original || !holdsFlattenedListItem(original.value))
|
|
1530
1661
|
return null;
|
|
1531
1662
|
return `frontmatter set refused — the existing "${field}" field cannot be faithfully round-tripped by ` +
|
|
1532
1663
|
`the frontmatter writer (its structure would be flattened and data, such as a nested object-list ` +
|
|
@@ -1553,8 +1684,29 @@ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
|
|
|
1553
1684
|
error('Invalid JSON for --data');
|
|
1554
1685
|
return;
|
|
1555
1686
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1687
|
+
// Only a JSON object names fields: an array or a string would spread into index-named
|
|
1688
|
+
// keys (`0: q`) and `null` crashed (found while implementing #5105).
|
|
1689
|
+
if (mergeData === null || typeof mergeData !== 'object' || Array.isArray(mergeData)) {
|
|
1690
|
+
error('--data must be a JSON object of field names to values');
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
for (const key of Object.keys(mergeData))
|
|
1694
|
+
rejectControlCharacterFieldName(key);
|
|
1695
|
+
// #1660 parity with `cmdFrontmatterSet`: a merged key that would flatten a lossy object-list
|
|
1696
|
+
// field is refused before anything is written, reported in set's `{ error, field }` shape
|
|
1697
|
+
// (found while implementing #5105).
|
|
1698
|
+
for (const [key, value] of Object.entries(mergeData)) {
|
|
1699
|
+
const lossyErr = objectListFieldWouldLoseData(content, key, value);
|
|
1700
|
+
if (lossyErr) {
|
|
1701
|
+
output({ error: lossyErr, field: key }, raw, undefined);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
for (const [key, value] of Object.entries(mergeData))
|
|
1706
|
+
setOwnField(fm, key, value);
|
|
1707
|
+
const newContent = spliceOrReportRefusal(content, fm, filePath, raw);
|
|
1708
|
+
if (newContent === null)
|
|
1709
|
+
return;
|
|
1558
1710
|
(0, shell_command_projection_cjs_1.platformWriteSync)(fullPath, newContent);
|
|
1559
1711
|
output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
|
|
1560
1712
|
}
|
|
@@ -1632,7 +1784,17 @@ module.exports = {
|
|
|
1632
1784
|
// the alias so the prohibition schema round-trip and any future caller can use the canonical name.
|
|
1633
1785
|
parseFrontmatter: extractFrontmatter,
|
|
1634
1786
|
reconstructFrontmatter,
|
|
1635
|
-
|
|
1787
|
+
// The writer lives in `frontmatter-splice.cts` (#5105); its public names are re-exported
|
|
1788
|
+
// here, unchanged, so no caller's import moves. Getters, because that module requires this
|
|
1789
|
+
// one at load time — see `spliceModule`.
|
|
1790
|
+
get spliceFrontmatter() { return spliceModule().spliceFrontmatter; },
|
|
1791
|
+
// The one owner of "may a writer splice this frontmatter block?" — callers catch
|
|
1792
|
+
// `isFrontmatterWriteRefusal(err)` and surface `err.message`/`err.code`.
|
|
1793
|
+
get FrontmatterWriteRefusedError() { return spliceModule().FrontmatterWriteRefusedError; },
|
|
1794
|
+
get isFrontmatterWriteRefusal() { return spliceModule().isFrontmatterWriteRefusal; },
|
|
1795
|
+
// The parse allowance past which `spliceFrontmatter` refuses with FRONTMATTER_TOO_COMPLEX —
|
|
1796
|
+
// exported so its boundary can be exercised exactly.
|
|
1797
|
+
get SPLICE_PARSE_BUDGET_CHARS() { return spliceModule().SPLICE_PARSE_BUDGET_CHARS; },
|
|
1636
1798
|
stripFrontmatter,
|
|
1637
1799
|
noOpObjectListSetError,
|
|
1638
1800
|
parseMustHavesBlock,
|
|
@@ -1640,6 +1802,16 @@ module.exports = {
|
|
|
1640
1802
|
// branch on an entry's `status:`/`resolution:` rather than print it. Off the
|
|
1641
1803
|
// same parse path as `extractFrontmatter`, minus only the display flattening.
|
|
1642
1804
|
frontmatterListEntries,
|
|
1805
|
+
// #5139: one top-level key's value VERBATIM (before display flattening), off the same guarded
|
|
1806
|
+
// parse path — the decision-coverage gates read `must_haves`/`truths`/`objective` and the
|
|
1807
|
+
// SUMMARY `files_modified` list through it instead of a hand-rolled `^key:` scan / regex.
|
|
1808
|
+
rawFrontmatterField,
|
|
1809
|
+
// #5139: one key's block as unparsed text, for a citation scan that must survive frontmatter
|
|
1810
|
+
// the YAML parser refuses.
|
|
1811
|
+
frontmatterKeyBlockText,
|
|
1812
|
+
// #5139: `^<key>:\s*<value>\s*$` over the fence owner's region, both escaped — the tdd gate's
|
|
1813
|
+
// `type: tdd` test, equivalent to the regex it replaced.
|
|
1814
|
+
frontmatterKeyHasValue,
|
|
1643
1815
|
// #3850: the display rendering itself, so a caller deriving a name from those
|
|
1644
1816
|
// objects produces the byte-identical string `extractFrontmatter` would have.
|
|
1645
1817
|
flattenObjectListItem,
|
|
@@ -1649,4 +1821,24 @@ module.exports = {
|
|
|
1649
1821
|
cmdFrontmatterMerge,
|
|
1650
1822
|
cmdFrontmatterValidate,
|
|
1651
1823
|
propagateCommentChannel,
|
|
1824
|
+
// #4917 / ADR-4910 Decision 1: additive-only export so `planning-document.cts`
|
|
1825
|
+
// can COMPOSE this seam's fence-detection grammar (byte-0 rule, BOM strip,
|
|
1826
|
+
// CR handling) instead of reimplementing it. No behavior change — same
|
|
1827
|
+
// function `extractFrontmatter`/`frontmatterListEntries` already call.
|
|
1828
|
+
frontmatterRegion,
|
|
1829
|
+
// The closed block for a writer (`bom + block + rest === content`), composing
|
|
1830
|
+
// `frontmatterRegion` — `uat.cts` locates its `updated:` edits through it.
|
|
1831
|
+
frontmatterBlock,
|
|
1832
|
+
// #5105: the reader internals the writer (`frontmatter-splice.cts`) builds on. Not public
|
|
1833
|
+
// API — one bag, so the reader's public surface does not grow by eight names.
|
|
1834
|
+
spliceSeam: Object.freeze({
|
|
1835
|
+
FULL_LINE_COMMENTS,
|
|
1836
|
+
YAML_LOAD_OPTS,
|
|
1837
|
+
commentPathKey,
|
|
1838
|
+
channelKeyLine,
|
|
1839
|
+
segmentKeyOf,
|
|
1840
|
+
escapeNullBytesForParse,
|
|
1841
|
+
unparseableResult,
|
|
1842
|
+
frontmatterDeepEqual,
|
|
1843
|
+
}),
|
|
1652
1844
|
};
|