@opengsd/gsd-core 1.9.1 → 1.11.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -22,10 +22,7 @@ const markdownSectionizer = require("./markdown-sectionizer.cjs");
|
|
|
22
22
|
const { collectSection, tokenizeHeadings } = markdownSectionizer;
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const markdownTable = require("./markdown-table.cjs");
|
|
25
|
-
const { splitTableRow } = markdownTable;
|
|
26
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
-
const roadmapParser = require("./roadmap-parser.cjs");
|
|
28
|
-
const { getMilestonePhaseFilter } = roadmapParser;
|
|
25
|
+
const { splitTableRow, isDelimiterRow } = markdownTable;
|
|
29
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
27
|
const coreUtils = require("./core-utils.cjs");
|
|
31
28
|
const { toPosixPath } = coreUtils;
|
|
@@ -37,7 +34,10 @@ const frontmatter = require("./frontmatter.cjs");
|
|
|
37
34
|
const { extractFrontmatter } = frontmatter;
|
|
38
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
36
|
const phaseIdMod = require("./phase-id.cjs");
|
|
40
|
-
const { PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
37
|
+
const { PHASE_NUMBER_TOKEN_SOURCE, scopeToPhase } = phaseIdMod;
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
+
const phaseLocator = require("./phase-locator.cjs");
|
|
40
|
+
const { getArchivedPhaseDirs, listMilestonePhaseDirs } = phaseLocator;
|
|
41
41
|
const security_cjs_1 = require("./security.cjs");
|
|
42
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
43
43
|
const configLoader = require("./config-loader.cjs");
|
|
@@ -45,24 +45,56 @@ const { loadConfig } = configLoader;
|
|
|
45
45
|
// ─── cmdAuditUat ─────────────────────────────────────────────────────────────
|
|
46
46
|
function cmdAuditUat(cwd, raw) {
|
|
47
47
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
48
|
-
|
|
48
|
+
const hasActivePhases = node_fs_1.default.existsSync(phasesDir);
|
|
49
|
+
// #2766: on milestone completion `milestone.cts` MOVES each phase dir into
|
|
50
|
+
// `.planning/milestones/<version>-phases/` (archive-by-default since #1871),
|
|
51
|
+
// leaving `.planning/phases/` empty or absent. Scanning only the active tree
|
|
52
|
+
// meant a partly-archived project silently omitted the archived phases, and a
|
|
53
|
+
// fully-archived one hard-errored with "No phases directory found" —
|
|
54
|
+
// indistinguishable from a broken install. Outstanding UAT items do not stop
|
|
55
|
+
// mattering when a milestone closes: a deferred human-UAT scenario or a
|
|
56
|
+
// `skipped` live-stack test is exactly what gets archived still-open.
|
|
57
|
+
//
|
|
58
|
+
// Reuses the canonical `getArchivedPhaseDirs` seam (phase-locator.cts), which
|
|
59
|
+
// `findPhaseInternal` already uses for this same fallback, so the archive
|
|
60
|
+
// layout convention stays owned by one module.
|
|
61
|
+
const archivedDirs = getArchivedPhaseDirs(cwd);
|
|
62
|
+
if (!hasActivePhases && archivedDirs.length === 0) {
|
|
49
63
|
error('No phases directory found in planning directory');
|
|
50
64
|
}
|
|
51
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
52
65
|
const results = [];
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
// Active dirs are milestone-filtered; archived dirs deliberately are NOT.
|
|
67
|
+
// listMilestonePhaseDirs derives the CURRENT milestone's phase directories
|
|
68
|
+
// (window + sentinel filtered) from ROADMAP.md, and archived phases belong
|
|
69
|
+
// to past milestones by definition — so applying it to them discards every
|
|
70
|
+
// one and silently reinstates the bug.
|
|
71
|
+
const scanTargets = [];
|
|
72
|
+
if (hasActivePhases) {
|
|
73
|
+
// #3185 (ADR-3180 Decision 1): routed through the canonical owner
|
|
74
|
+
// instead of a hand-rolled readdirSync + isDirInMilestone filter, which
|
|
75
|
+
// also never excluded sentinels, unlike the owner.
|
|
76
|
+
const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
77
|
+
for (const dir of dirs) {
|
|
78
|
+
scanTargets.push({ dir, phaseDir: node_path_1.default.join(phasesDir, dir) });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const archived of archivedDirs) {
|
|
82
|
+
scanTargets.push({
|
|
83
|
+
dir: archived.name,
|
|
84
|
+
phaseDir: archived.fullPath,
|
|
85
|
+
milestone: archived.milestone,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
for (const { dir, phaseDir, milestone } of scanTargets) {
|
|
60
89
|
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
61
90
|
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
62
|
-
const phaseDir = node_path_1.default.join(phasesDir, dir);
|
|
63
91
|
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
64
|
-
// Process UAT files
|
|
65
|
-
|
|
92
|
+
// Process UAT files — scoped to THIS phase's own token (#3511) via
|
|
93
|
+
// scopeToPhase, so a stray, cross-phase, or ad-hoc file cannot be reported
|
|
94
|
+
// under this phase's audit-uat entry. A phase whose own UAT file is
|
|
95
|
+
// genuinely absent scopes to empty and contributes nothing — correct, and
|
|
96
|
+
// the reason scopeToPhase has no unfiltered fallback.
|
|
97
|
+
for (const file of scopeToPhase(files.filter(f => f.includes('-UAT') && f.endsWith('.md')), dir)) {
|
|
66
98
|
const uatFilePath = node_path_1.default.join(phaseDir, file);
|
|
67
99
|
const content = node_fs_1.default.readFileSync(uatFilePath, 'utf-8');
|
|
68
100
|
const items = parseUatItems(content);
|
|
@@ -74,12 +106,14 @@ function cmdAuditUat(cwd, raw) {
|
|
|
74
106
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
|
|
75
107
|
type: 'uat',
|
|
76
108
|
status: (extractFrontmatter(content, uatFilePath).status || 'unknown'),
|
|
109
|
+
archived_milestone: milestone,
|
|
77
110
|
items,
|
|
78
111
|
});
|
|
79
112
|
}
|
|
80
113
|
}
|
|
81
|
-
// Process VERIFICATION files
|
|
82
|
-
for
|
|
114
|
+
// Process VERIFICATION files — scoped to THIS phase's own token (#3511)
|
|
115
|
+
// for the same reason as the UAT loop above.
|
|
116
|
+
for (const file of scopeToPhase(files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md')), dir)) {
|
|
83
117
|
const verificationFilePath = node_path_1.default.join(phaseDir, file);
|
|
84
118
|
const content = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
|
|
85
119
|
const status = extractFrontmatter(content, verificationFilePath).status || 'unknown';
|
|
@@ -93,6 +127,7 @@ function cmdAuditUat(cwd, raw) {
|
|
|
93
127
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
|
|
94
128
|
type: 'verification',
|
|
95
129
|
status,
|
|
130
|
+
archived_milestone: milestone,
|
|
96
131
|
items,
|
|
97
132
|
});
|
|
98
133
|
}
|
|
@@ -117,6 +152,7 @@ function cmdAuditUat(cwd, raw) {
|
|
|
117
152
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, deferredFile))),
|
|
118
153
|
type: 'deferred',
|
|
119
154
|
status: 'unresolved',
|
|
155
|
+
archived_milestone: milestone,
|
|
120
156
|
items,
|
|
121
157
|
});
|
|
122
158
|
}
|
|
@@ -308,6 +344,43 @@ const CHECKPOINT_FRAMES = {
|
|
|
308
344
|
banner: 'PUNTO DI CONTROLLO: Verifica richiesta',
|
|
309
345
|
instruction: 'Digita `pass` o descrivi cosa non va.',
|
|
310
346
|
},
|
|
347
|
+
dutch: {
|
|
348
|
+
banner: 'CONTROLEPUNT: Verificatie vereist',
|
|
349
|
+
instruction: 'Typ `pass` of beschrijf wat er mis is.',
|
|
350
|
+
},
|
|
351
|
+
polish: {
|
|
352
|
+
banner: 'PUNKT KONTROLNY: Wymagana weryfikacja',
|
|
353
|
+
instruction: 'Wpisz `pass` lub opisz, co jest nie tak.',
|
|
354
|
+
},
|
|
355
|
+
russian: {
|
|
356
|
+
banner: 'КОНТРОЛЬНАЯ ТОЧКА: требуется проверка',
|
|
357
|
+
instruction: 'Введите `pass` или опишите, что не так.',
|
|
358
|
+
},
|
|
359
|
+
ukrainian: {
|
|
360
|
+
banner: 'КОНТРОЛЬНА ТОЧКА: потрібна перевірка',
|
|
361
|
+
instruction: 'Введіть `pass` або опишіть, що не так.',
|
|
362
|
+
},
|
|
363
|
+
turkish: {
|
|
364
|
+
banner: 'KONTROL NOKTASI: Doğrulama gerekli',
|
|
365
|
+
instruction: '`pass` yazın veya sorunu açıklayın.',
|
|
366
|
+
},
|
|
367
|
+
hindi: {
|
|
368
|
+
banner: 'चेकपॉइंट: सत्यापन आवश्यक',
|
|
369
|
+
instruction: '`pass` लिखें या बताएं कि क्या गलत है।',
|
|
370
|
+
},
|
|
371
|
+
arabic: {
|
|
372
|
+
banner: 'نقطة تحقق: المراجعة مطلوبة',
|
|
373
|
+
instruction: 'اكتب `pass` أو صف المشكلة.',
|
|
374
|
+
direction: 'rtl',
|
|
375
|
+
},
|
|
376
|
+
vietnamese: {
|
|
377
|
+
banner: 'ĐIỂM KIỂM TRA: Cần xác minh',
|
|
378
|
+
instruction: 'Nhập `pass` hoặc mô tả vấn đề.',
|
|
379
|
+
},
|
|
380
|
+
indonesian: {
|
|
381
|
+
banner: 'TITIK PEMERIKSAAN: Verifikasi diperlukan',
|
|
382
|
+
instruction: 'Ketik `pass` atau jelaskan apa yang salah.',
|
|
383
|
+
},
|
|
311
384
|
};
|
|
312
385
|
// Free-form response_language aliases → canonical CHECKPOINT_FRAMES key.
|
|
313
386
|
const CHECKPOINT_LANGUAGE_ALIASES = {
|
|
@@ -320,21 +393,27 @@ const CHECKPOINT_LANGUAGE_ALIASES = {
|
|
|
320
393
|
chinese: 'chinese', zh: 'chinese', 'zh-cn': 'chinese', 'zh-tw': 'chinese', mandarin: 'chinese', 'simplified chinese': 'chinese', 'traditional chinese': 'chinese', '中文': 'chinese',
|
|
321
394
|
korean: 'korean', ko: 'korean', '한국어': 'korean',
|
|
322
395
|
italian: 'italian', it: 'italian', italiano: 'italian',
|
|
396
|
+
dutch: 'dutch', nl: 'dutch', nederlands: 'dutch', flemish: 'dutch', vlaams: 'dutch',
|
|
397
|
+
polish: 'polish', pl: 'polish', polski: 'polish',
|
|
398
|
+
russian: 'russian', ru: 'russian', 'ru-ru': 'russian', 'русский': 'russian',
|
|
399
|
+
ukrainian: 'ukrainian', uk: 'ukrainian', ua: 'ukrainian', 'українська': 'ukrainian',
|
|
400
|
+
turkish: 'turkish', tr: 'turkish', 'türkçe': 'turkish', turkce: 'turkish',
|
|
401
|
+
hindi: 'hindi', hi: 'hindi', 'हिन्दी': 'hindi', 'हिंदी': 'hindi',
|
|
402
|
+
arabic: 'arabic', ar: 'arabic', 'العربية': 'arabic',
|
|
403
|
+
vietnamese: 'vietnamese', vi: 'vietnamese', 'tiếng việt': 'vietnamese', 'tieng viet': 'vietnamese',
|
|
404
|
+
indonesian: 'indonesian', id: 'indonesian', 'bahasa indonesia': 'indonesian',
|
|
323
405
|
};
|
|
324
406
|
function resolveCheckpointFrame(responseLanguage) {
|
|
325
407
|
if (!responseLanguage)
|
|
326
408
|
return CHECKPOINT_FRAMES.english;
|
|
327
|
-
const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().toLowerCase()];
|
|
409
|
+
const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().normalize('NFC').toLowerCase()];
|
|
328
410
|
return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english;
|
|
329
411
|
}
|
|
330
|
-
// Approximate East Asian Width
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// medium finding). Latin-script frames (English/Spanish/French/German/
|
|
336
|
-
// Portuguese/Italian) contain no wide code points, so displayWidth === length
|
|
337
|
-
// for them — no behavior change there.
|
|
412
|
+
// Approximate terminal-cell width. East Asian Width W/F code points occupy two
|
|
413
|
+
// cells, while Unicode combining marks occupy no additional cell beyond their
|
|
414
|
+
// base character. Counting only W/F ranges is insufficient for scripts such as
|
|
415
|
+
// Devanagari: Hindi vowel signs and viramas are combining marks, and treating
|
|
416
|
+
// each as a full cell visibly shifts the checkpoint box's right border.
|
|
338
417
|
function isWideCodePoint(codePoint) {
|
|
339
418
|
return ((codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
|
340
419
|
codePoint === 0x2329 || codePoint === 0x232a ||
|
|
@@ -351,11 +430,17 @@ function isWideCodePoint(codePoint) {
|
|
|
351
430
|
(codePoint >= 0x20000 && codePoint <= 0x3fffd) // CJK Unified Ideographs Extension B+ / supplementary
|
|
352
431
|
);
|
|
353
432
|
}
|
|
433
|
+
// Non-spacing/enclosing marks and format controls occupy zero terminal cells.
|
|
434
|
+
// Spacing combining marks (General_Category=Mc), such as Devanagari vowel
|
|
435
|
+
// signs, still advance the cursor and must contribute one column.
|
|
436
|
+
const ZERO_WIDTH_MARK_RE = /\p{gc=Mn}|\p{gc=Me}|\p{gc=Cf}/u;
|
|
354
437
|
// Iterates by Unicode code point (not UTF-16 code unit) so astral characters
|
|
355
438
|
// are measured once, not as two surrogate units.
|
|
356
439
|
function displayWidth(text) {
|
|
357
440
|
let width = 0;
|
|
358
441
|
for (const ch of text) {
|
|
442
|
+
if (ZERO_WIDTH_MARK_RE.test(ch))
|
|
443
|
+
continue;
|
|
359
444
|
width += isWideCodePoint(ch.codePointAt(0)) ? 2 : 1;
|
|
360
445
|
}
|
|
361
446
|
return width;
|
|
@@ -370,11 +455,20 @@ function checkpointBoxLine(text) {
|
|
|
370
455
|
const padded = padLength > 0 ? content + ' '.repeat(padLength) : content;
|
|
371
456
|
return `║${padded}║`;
|
|
372
457
|
}
|
|
458
|
+
const RTL_ISOLATE = '\u2067';
|
|
459
|
+
const POP_DIRECTIONAL_ISOLATE = '\u2069';
|
|
460
|
+
function isolateCheckpointFrameText(text, frame) {
|
|
461
|
+
return frame.direction === 'rtl'
|
|
462
|
+
? `${RTL_ISOLATE}${text}${POP_DIRECTIONAL_ISOLATE}`
|
|
463
|
+
: text;
|
|
464
|
+
}
|
|
373
465
|
function buildCheckpoint(currentTest, responseLanguage) {
|
|
374
466
|
const frame = resolveCheckpointFrame(responseLanguage);
|
|
467
|
+
const banner = isolateCheckpointFrameText(frame.banner, frame);
|
|
468
|
+
const instruction = isolateCheckpointFrameText(frame.instruction, frame);
|
|
375
469
|
return [
|
|
376
470
|
'╔══════════════════════════════════════════════════════════════╗',
|
|
377
|
-
checkpointBoxLine(
|
|
471
|
+
checkpointBoxLine(banner),
|
|
378
472
|
'╚══════════════════════════════════════════════════════════════╝',
|
|
379
473
|
'',
|
|
380
474
|
`**Test ${currentTest.number}: ${currentTest.name}**`,
|
|
@@ -382,7 +476,7 @@ function buildCheckpoint(currentTest, responseLanguage) {
|
|
|
382
476
|
currentTest.expected,
|
|
383
477
|
'',
|
|
384
478
|
'──────────────────────────────────────────────────────────────',
|
|
385
|
-
|
|
479
|
+
instruction,
|
|
386
480
|
'──────────────────────────────────────────────────────────────',
|
|
387
481
|
].join('\n');
|
|
388
482
|
}
|
|
@@ -486,6 +580,165 @@ function parseGapsItems(content) {
|
|
|
486
580
|
item.reason = reason;
|
|
487
581
|
items.push(item);
|
|
488
582
|
}
|
|
583
|
+
// #2766: union with the table form. A `|`-leading line is never a `- ` bullet
|
|
584
|
+
// opener, so a section mixing bullet entries and a table surfaces both with no
|
|
585
|
+
// double-counting.
|
|
586
|
+
items.push(...parseGapsTableItems(gapsSection.body));
|
|
587
|
+
return items;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Split a section body into its GFM pipe tables, one entry per table (#2766).
|
|
591
|
+
*
|
|
592
|
+
* Shared by `parseGapsTableItems` and `parseDeferredTableItems` so the
|
|
593
|
+
* header/delimiter/table-boundary handling — the fiddly part — lives in exactly
|
|
594
|
+
* one place, and the two consumers only decide what a data row MEANS.
|
|
595
|
+
*
|
|
596
|
+
* Header detection is lookahead-free: the last data-shaped row is held in
|
|
597
|
+
* `pending` until the NEXT line decides its fate — a delimiter row
|
|
598
|
+
* (`|---|---|`) proves the held row was a header, anything else promotes it to a
|
|
599
|
+
* data row. So a conventional table drops exactly its header, a HEADERLESS table
|
|
600
|
+
* keeps every row (hand-authored planning tables often omit the delimiter), and
|
|
601
|
+
* a header with no data rows yields nothing. A prose or blank line ends the
|
|
602
|
+
* current table, so two tables separated by text are read independently and each
|
|
603
|
+
* drops its own header.
|
|
604
|
+
*
|
|
605
|
+
* Reuses the canonical `isDelimiterRow` shape check from markdown-table.cts
|
|
606
|
+
* rather than re-deriving it. Deliberately NOT routed through
|
|
607
|
+
* `parseMarkdownTable`, which reads only the FIRST table in a body and treats
|
|
608
|
+
* ragged/headerless shapes as errors (ADR-2143 §3) — correct for the mandated
|
|
609
|
+
* tables in STATE.md/ROADMAP.md, but the wrong contract here, where a malformed
|
|
610
|
+
* hand-written table must still surface its rows rather than be dropped.
|
|
611
|
+
*/
|
|
612
|
+
function collectTableRows(sectionBody) {
|
|
613
|
+
const tables = [];
|
|
614
|
+
let current = null;
|
|
615
|
+
let pending = null;
|
|
616
|
+
const ensure = () => {
|
|
617
|
+
if (!current)
|
|
618
|
+
current = { header: null, rows: [] };
|
|
619
|
+
};
|
|
620
|
+
const flushPending = () => {
|
|
621
|
+
if (pending) {
|
|
622
|
+
ensure();
|
|
623
|
+
current.rows.push(pending);
|
|
624
|
+
pending = null;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const endTable = () => {
|
|
628
|
+
flushPending();
|
|
629
|
+
if (current) {
|
|
630
|
+
tables.push(current);
|
|
631
|
+
current = null;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
for (const rawLine of sectionBody.split('\n')) {
|
|
635
|
+
const line = rawLine.replace(/\r$/, '').trim();
|
|
636
|
+
if (!line.startsWith('|')) {
|
|
637
|
+
endTable();
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const cells = splitTableRow(line);
|
|
641
|
+
if (cells.length === 0)
|
|
642
|
+
continue;
|
|
643
|
+
if (isDelimiterRow(cells)) {
|
|
644
|
+
ensure();
|
|
645
|
+
current.header = pending; // may be null for a delimiter-first table
|
|
646
|
+
pending = null;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
flushPending();
|
|
650
|
+
pending = cells;
|
|
651
|
+
}
|
|
652
|
+
endTable();
|
|
653
|
+
return tables;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Header-name → canonical Gaps field (#2766).
|
|
657
|
+
*
|
|
658
|
+
* Anchored on the `## Gaps` field vocabulary `templates/UAT.md` mandates for the
|
|
659
|
+
* YAML-lite bullet form (truth/status/reason/severity/test), plus the obvious
|
|
660
|
+
* synonyms a human writing the same information as a table reaches for instead.
|
|
661
|
+
*/
|
|
662
|
+
const GAPS_COLUMN_ALIASES = {
|
|
663
|
+
truth: 'truth', gap: 'truth', finding: 'truth', item: 'truth',
|
|
664
|
+
description: 'truth', issue: 'truth', name: 'truth',
|
|
665
|
+
status: 'status', result: 'status', state: 'status',
|
|
666
|
+
reason: 'reason', note: 'reason', notes: 'reason',
|
|
667
|
+
detail: 'reason', details: 'reason', evidence: 'reason',
|
|
668
|
+
severity: 'severity',
|
|
669
|
+
test: 'test', '#': 'test', 'test #': 'test', 'test number': 'test',
|
|
670
|
+
};
|
|
671
|
+
function mapGapsHeader(header) {
|
|
672
|
+
if (!header)
|
|
673
|
+
return null;
|
|
674
|
+
const columns = {};
|
|
675
|
+
header.forEach((cell, idx) => {
|
|
676
|
+
const key = GAPS_COLUMN_ALIASES[cell.trim().toLowerCase().replace(/\*+/g, '')];
|
|
677
|
+
if (key && !(key in columns))
|
|
678
|
+
columns[key] = idx;
|
|
679
|
+
});
|
|
680
|
+
return Object.keys(columns).length > 0 ? columns : null;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Extract gap entries from GFM pipe tables in a `## Gaps` section (#2766) — a
|
|
684
|
+
* UNION with the YAML-lite bullet scan in `parseGapsItems`, for the same reason
|
|
685
|
+
* `parseDeferredTableItems` exists: `splitGapsEntries` keys entirely on `- `
|
|
686
|
+
* bullet openers, so a table-shaped `## Gaps` section yielded ZERO items and
|
|
687
|
+
* every finding in it was silently invisible.
|
|
688
|
+
*
|
|
689
|
+
* Neither `templates/UAT.md` nor `templates/verification-report.md` documents a
|
|
690
|
+
* table for this section (both mandate the bullet/numbered form), so a table
|
|
691
|
+
* here is off-template hand-authoring — which is precisely why it must not fail
|
|
692
|
+
* silently. Note `parseVerificationItems` in this same file already reads table
|
|
693
|
+
* rows AND numbered AND bullet items as a union because the live sections mix
|
|
694
|
+
* shapes; the Gaps and deferred parsers never got the same treatment.
|
|
695
|
+
*
|
|
696
|
+
* When a header row is present its columns are mapped by name against the
|
|
697
|
+
* template's own field vocabulary (see GAPS_COLUMN_ALIASES) so a tabled gap
|
|
698
|
+
* carries the same status/reason/test fields as its bullet equivalent and
|
|
699
|
+
* `categorizeItem` classifies it identically. With no recognizable header, the
|
|
700
|
+
* row degrades to a joined-cells name with status `unknown` — surfaced, not
|
|
701
|
+
* dropped, matching this module's established fail-safe stance.
|
|
702
|
+
*
|
|
703
|
+
* Resolution follows the bullet path exactly: an entry is skipped ONLY on an
|
|
704
|
+
* explicit resolved marker — the mapped `status` column reading `resolved`, or,
|
|
705
|
+
* absent a status column, any cell reading exactly `resolved`. A gap with no
|
|
706
|
+
* parseable status is NEVER treated as resolved.
|
|
707
|
+
*/
|
|
708
|
+
function parseGapsTableItems(sectionBody) {
|
|
709
|
+
const items = [];
|
|
710
|
+
for (const { header, rows } of collectTableRows(sectionBody)) {
|
|
711
|
+
const columns = mapGapsHeader(header);
|
|
712
|
+
for (const cells of rows) {
|
|
713
|
+
const at = (key) => (columns && key in columns ? (cells[columns[key]] ?? '').trim() : '');
|
|
714
|
+
const rawStatus = at('status');
|
|
715
|
+
if (rawStatus && rawStatus.toLowerCase() === 'resolved')
|
|
716
|
+
continue;
|
|
717
|
+
// No status column: fall back to an explicit resolved marker in any cell
|
|
718
|
+
// (the headerless-table equivalent of `status: resolved`).
|
|
719
|
+
if (!columns || !('status' in columns)) {
|
|
720
|
+
if (cells.some(c => /^resolved$/i.test(c.trim())))
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const truth = at('truth');
|
|
724
|
+
const reason = at('reason');
|
|
725
|
+
const testNum = at('test');
|
|
726
|
+
const name = truth || cells.filter(c => c !== '').join(' — ');
|
|
727
|
+
if (!name)
|
|
728
|
+
continue;
|
|
729
|
+
const status = rawStatus || 'unknown';
|
|
730
|
+
const item = {
|
|
731
|
+
name,
|
|
732
|
+
result: status,
|
|
733
|
+
category: categorizeItem(status, reason || undefined, undefined),
|
|
734
|
+
};
|
|
735
|
+
if (testNum && /^\d+$/.test(testNum))
|
|
736
|
+
item.test = parseInt(testNum, 10);
|
|
737
|
+
if (reason)
|
|
738
|
+
item.reason = reason;
|
|
739
|
+
items.push(item);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
489
742
|
return items;
|
|
490
743
|
}
|
|
491
744
|
// ─── parseDeferredItems ────────────────────────────────────────────────────────
|
|
@@ -514,50 +767,379 @@ function parseGapsItems(content) {
|
|
|
514
767
|
* `.planning/todos/pending/*.md` entry required). Every other entry —
|
|
515
768
|
* including one with no `status:` field at all — is UNRESOLVED and is
|
|
516
769
|
* surfaced.
|
|
770
|
+
*
|
|
771
|
+
* #3457: when the section body contains headings, entries are delimited by
|
|
772
|
+
* LEAF headings (see `splitDeferredHeadingEntries`) rather than by bullets —
|
|
773
|
+
* the executor convention writes one deferred item as a heading followed by
|
|
774
|
+
* sibling `- **Field:** …` bullets, which the bullet-only split mis-counted as
|
|
775
|
+
* one item PER BULLET. A body with no headings keeps the original
|
|
776
|
+
* one-bullet-per-item split unchanged.
|
|
517
777
|
*/
|
|
518
|
-
|
|
778
|
+
/**
|
|
779
|
+
* One `deferred-items.md` entry with its RAW (un-lowercased) `status:` field
|
|
780
|
+
* value (`''` when the entry carries no parseable status). #3458 follow-up:
|
|
781
|
+
* `parseDeferredItems` (below) is now DEFINED IN TERMS OF this — it filters
|
|
782
|
+
* to `status !== 'resolved'` — and `audit.cts`'s `scanDeferredItems` also
|
|
783
|
+
* consumes this directly so it can tell `resolved` (fixed for real, never
|
|
784
|
+
* counted), the newer `acknowledged` (suppressed-but-tallied, #3458
|
|
785
|
+
* follow-up), and everything else (open) apart WITHOUT a second,
|
|
786
|
+
* independent entry-boundary/field-extraction pass that could drift from
|
|
787
|
+
* this one.
|
|
788
|
+
*/
|
|
789
|
+
function parseDeferredItemsWithStatus(content) {
|
|
519
790
|
const deferredSection = collectSection(content, (h) => /^deferred\s+items$/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
520
791
|
const sectionBody = deferredSection ? deferredSection.body : content;
|
|
521
792
|
const items = [];
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
793
|
+
// #3457: heading-delimited shape — an entry's fields live in sibling bullets
|
|
794
|
+
// (`- **Status:** resolved`), so the bullet marker is stripped on EVERY line
|
|
795
|
+
// before field extraction, not just line 0 (which `extractGapEntryFields`
|
|
796
|
+
// does for the headless/Gaps shape, where a later `- ` line is a nested
|
|
797
|
+
// sub-list, not a field).
|
|
798
|
+
const headingEntries = splitDeferredHeadingEntries(sectionBody);
|
|
799
|
+
const entries = headingEntries !== null
|
|
800
|
+
? headingEntries.map((entryLines) => ({
|
|
801
|
+
lines: entryLines,
|
|
802
|
+
fields: extractGapEntryFields(entryLines.map(stripLeadingBulletMarker)),
|
|
803
|
+
}))
|
|
804
|
+
: splitGapsEntries(sectionBody).map((entryLines) => ({
|
|
805
|
+
lines: entryLines,
|
|
806
|
+
fields: extractGapEntryFields(entryLines),
|
|
807
|
+
}));
|
|
808
|
+
for (const { lines: entryLines, fields } of entries) {
|
|
527
809
|
const text = rawGapEntryText(entryLines);
|
|
528
810
|
if (!text)
|
|
529
811
|
continue;
|
|
530
|
-
items.push({
|
|
531
|
-
name: text,
|
|
532
|
-
result: 'unresolved',
|
|
533
|
-
category: 'deferred',
|
|
534
|
-
});
|
|
812
|
+
items.push({ name: text, status: fields.status || '' });
|
|
535
813
|
}
|
|
814
|
+
// #2766: union with the table form — see parseDeferredTableItems. Executors
|
|
815
|
+
// write this file by hand with no mandated shape, and a GFM table is a natural
|
|
816
|
+
// choice for the common "test → failing seeds" case, which produced ZERO items.
|
|
817
|
+
// Table rows carry no independently-parseable status column in general —
|
|
818
|
+
// `parseDeferredTableItems` already excludes resolved/done/pass rows at its
|
|
819
|
+
// own layer (any cell reading exactly one of those three) — so anything it
|
|
820
|
+
// returns here is inherently open; `acknowledge` (#3458 follow-up) has no
|
|
821
|
+
// representable field to write for a table row, so those are reported with
|
|
822
|
+
// status `''` (never `resolved`/`acknowledged`) and remain permanently
|
|
823
|
+
// un-acknowledgeable via the CLI writer — a known, deliberate limitation
|
|
824
|
+
// (see `acknowledgeDeferredItem`'s doc comment).
|
|
825
|
+
items.push(...parseDeferredTableItems(sectionBody).map((item) => ({ name: item.name, status: '' })));
|
|
536
826
|
return items;
|
|
537
827
|
}
|
|
828
|
+
function parseDeferredItems(content) {
|
|
829
|
+
return parseDeferredItemsWithStatus(content)
|
|
830
|
+
.filter((entry) => !(entry.status && entry.status.toLowerCase() === 'resolved'))
|
|
831
|
+
.map((entry) => ({
|
|
832
|
+
name: entry.name,
|
|
833
|
+
result: 'unresolved',
|
|
834
|
+
category: 'deferred',
|
|
835
|
+
}));
|
|
836
|
+
}
|
|
538
837
|
/**
|
|
539
|
-
*
|
|
540
|
-
*
|
|
838
|
+
* CLI-writer half of the #3458 follow-up deferred_items suppression seam.
|
|
839
|
+
* Sets the ONE deferred entry whose rendered text (`rawGapEntryText`, the
|
|
840
|
+
* same value `parseDeferredItemsWithStatus`/the audit's JSON output surface
|
|
841
|
+
* as `name`/`text`) exactly equals `targetText` to `status: acknowledged` —
|
|
842
|
+
* a NEW terminal value, distinct from the existing `resolved` (which keeps
|
|
843
|
+
* meaning "actually fixed"). This is the marker for this category: unlike
|
|
844
|
+
* every other audit category (a sibling `audit_acknowledged` frontmatter map
|
|
845
|
+
* that never touches the artifact's own `status:`), a deferred-items.md
|
|
846
|
+
* entry's `status:` field carries no OTHER meaning, so the field itself
|
|
847
|
+
* doubles as the marker — self-invalidating for free: edit the entry's
|
|
848
|
+
* `status:` away from `acknowledged` (or delete the field) and it resurfaces
|
|
849
|
+
* with no separate cleanup step, exactly like every other category's marker.
|
|
541
850
|
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
851
|
+
* Deliberately refuses (`unsupported_heading_shape`) rather than guess when
|
|
852
|
+
* the section uses the heading-delimited (#3457) entry shape: reliably
|
|
853
|
+
* mapping a `splitDeferredHeadingEntries` entry back to its EXACT source line
|
|
854
|
+
* span is not safely derivable without re-deriving that function's
|
|
855
|
+
* leaf/container walk against a document that may also mix in headless
|
|
856
|
+
* (`splitGapsEntries`-derived) entries between headings — attempting it risks
|
|
857
|
+
* writing into the WRONG entry. The bullet-only (headless) shape below is the
|
|
858
|
+
* primary, documented SCOPE BOUNDARY convention and is handled precisely.
|
|
550
859
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* (
|
|
860
|
+
* Also refuses `ambiguous` (2+ entries share the exact same text — status must
|
|
861
|
+
* be unique to identify one) and `not_found`, and is a no-op
|
|
862
|
+
* (`already_resolved`) on an entry already carrying `status: resolved` — the
|
|
863
|
+
* verdict-preserving direction: acknowledging a genuinely-fixed item would
|
|
864
|
+
* silently downgrade its terminal state.
|
|
865
|
+
*
|
|
866
|
+
* SPAN-CARRIED, not re-searched (F1, #3458 follow-up review — see
|
|
867
|
+
* `splitGapsEntriesWithSpans`'s doc comment): the target entry's location
|
|
868
|
+
* within `sectionBody` is the (start, end) character span recorded by
|
|
869
|
+
* `splitGapsEntriesWithSpans` in the SAME pass that produced `entryLines` /
|
|
870
|
+
* `targetText` above — never re-derived afterwards by searching. The
|
|
871
|
+
* previous implementation re-found the entry with a regex anchored on its
|
|
872
|
+
* own (escaped) exact text; that regex necessarily matches the FIRST
|
|
873
|
+
* occurrence of that text within `sectionBody`, which is not always the
|
|
874
|
+
* entry that was actually selected (a continuation/quoted line inside an
|
|
875
|
+
* EARLIER or LATER entry can carry byte-identical text) — and because the
|
|
876
|
+
* mis-targeted span is byte-identical to `targetText`, no downstream check
|
|
877
|
+
* on the WRITTEN text could ever distinguish a wrong-entry write from a
|
|
878
|
+
* correct one. Carrying the span removes the re-derivation step entirely:
|
|
879
|
+
* there is no second search to mis-target.
|
|
880
|
+
*
|
|
881
|
+
* Section-anchored (BLOCKER 1, #3458 follow-up review): the span is
|
|
882
|
+
* `sectionBody`-relative — the SAME string `matches`/the `ambiguous` guard
|
|
883
|
+
* were computed over — not `content`-relative, so an identical bullet living
|
|
884
|
+
* outside `## Deferred Items` (e.g. in an unrelated `# Notes` or a
|
|
885
|
+
* UAT/VERIFICATION body) can never steal the write. The span is translated
|
|
886
|
+
* into `content`-relative offsets via `deferredSection.bodyStart` (the
|
|
887
|
+
* section's own start offset, an invariant `collectSection` guarantees:
|
|
888
|
+
* `content.slice(bodyStart, bodyEnd) === body`). Before writing, the
|
|
889
|
+
* spanned text's own raw entry is re-derived and compared against
|
|
890
|
+
* `targetText` one more time — this is now a GENUINE invariant check (the
|
|
891
|
+
* span was computed by `splitGapsEntriesCore`'s independent offset
|
|
892
|
+
* bookkeeping, a different code path than the `entryLines`/`targetText`
|
|
893
|
+
* comparison above), not a no-op — if it does not match, the write is
|
|
894
|
+
* refused with `match_verification_failed` rather than risk touching the
|
|
895
|
+
* wrong span.
|
|
554
896
|
*/
|
|
555
|
-
function
|
|
897
|
+
function acknowledgeDeferredItem(content, targetText) {
|
|
898
|
+
const deferredSection = collectSection(content, (h) => /^deferred\s+items$/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
899
|
+
const sectionBody = deferredSection ? deferredSection.body : content;
|
|
900
|
+
if (splitDeferredHeadingEntries(sectionBody) !== null) {
|
|
901
|
+
return { content, status: 'unsupported_heading_shape' };
|
|
902
|
+
}
|
|
903
|
+
const entries = splitGapsEntriesWithSpans(sectionBody);
|
|
904
|
+
const matches = entries
|
|
905
|
+
.map((entry) => ({ entry, text: rawGapEntryText(entry.lines) }))
|
|
906
|
+
.filter((e) => e.text === targetText);
|
|
907
|
+
if (matches.length === 0)
|
|
908
|
+
return { content, status: 'not_found' };
|
|
909
|
+
if (matches.length > 1)
|
|
910
|
+
return { content, status: 'ambiguous' };
|
|
911
|
+
const { entry } = matches[0];
|
|
912
|
+
const { lines: entryLines, start, end } = entry;
|
|
913
|
+
const fields = extractGapEntryFields(entryLines);
|
|
914
|
+
if (fields.status && fields.status.toLowerCase() === 'resolved') {
|
|
915
|
+
return { content, status: 'already_resolved' };
|
|
916
|
+
}
|
|
917
|
+
// Anchor to the SAME section body `matches`/the `ambiguous` guard above
|
|
918
|
+
// were computed over (BLOCKER 1) — never the whole `content`, which could
|
|
919
|
+
// contain an identical bullet elsewhere. `start`/`end` are the entry's own
|
|
920
|
+
// span, carried directly from `splitGapsEntriesWithSpans` — no re-search.
|
|
921
|
+
const sectionOffset = deferredSection ? deferredSection.bodyStart : 0;
|
|
922
|
+
const matchedLines = sectionBody.slice(start, end).split('\n');
|
|
923
|
+
// Genuine invariant re-verification (see doc comment above): the span was
|
|
924
|
+
// computed by a code path independent of the `entryLines`/`targetText`
|
|
925
|
+
// comparison that selected this entry — this catches real drift between
|
|
926
|
+
// the two rather than a regex trivially guaranteed to agree with itself.
|
|
927
|
+
const strippedForVerify = matchedLines.map((l) => l.replace(/\r$/, ''));
|
|
928
|
+
if (rawGapEntryText(strippedForVerify) !== targetText) {
|
|
929
|
+
return { content, status: 'match_verification_failed' };
|
|
930
|
+
}
|
|
931
|
+
const matchIndexInContent = sectionOffset + start;
|
|
932
|
+
const statusFieldRe = /^\s*(?:-\s+)?(\*+status:\*+|status:)/i;
|
|
933
|
+
const statusLineIdx = matchedLines.findIndex((rawLine) => statusFieldRe.test(rawLine.replace(/\r$/, '')));
|
|
934
|
+
// No CRLF-preservation branch here (WARNING 1, #3458 follow-up review):
|
|
935
|
+
// every write goes through `platformWriteSync` → `normalizeContent`, which
|
|
936
|
+
// for a `.md` path unconditionally runs `_normalizeMd` — whole-file
|
|
937
|
+
// `\r\n` → `\n`, plus blank-line normalization around headings/lists — on
|
|
938
|
+
// EVERY write, not just this one. That is this codebase's single,
|
|
939
|
+
// deliberate OS-facing I/O seam (`shell-command-projection.cts`), applied
|
|
940
|
+
// uniformly to every `.md` writer; carving out one exception here would
|
|
941
|
+
// fight it rather than follow it, for a guarantee (byte-identical CRLF on
|
|
942
|
+
// disk) the seam already makes impossible. A marker write on a CRLF
|
|
943
|
+
// `deferred-items.md` normalizes the WHOLE file to LF, same as any other
|
|
944
|
+
// `.md` write in this codebase — expected, not a regression to guard
|
|
945
|
+
// against. Where a source line still carries a trailing `\r` (read from an
|
|
946
|
+
// on-disk CRLF document before normalization), `String.prototype.replace`
|
|
947
|
+
// consumes it as part of `.*$` and the replacement text does not
|
|
948
|
+
// reproduce it, so it is dropped here too — consistent with the eventual
|
|
949
|
+
// whole-file normalization rather than duplicating it.
|
|
950
|
+
let newMatchedLines;
|
|
951
|
+
if (statusLineIdx === -1) {
|
|
952
|
+
const bulletIndentMatch = matchedLines[0].match(/^(\s*)-\s+/);
|
|
953
|
+
const continuationIndent = ' '.repeat((bulletIndentMatch ? bulletIndentMatch[1].length : 0) + 2);
|
|
954
|
+
newMatchedLines = [
|
|
955
|
+
matchedLines[0],
|
|
956
|
+
`${continuationIndent}status: acknowledged`,
|
|
957
|
+
...matchedLines.slice(1),
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
const original = matchedLines[statusLineIdx];
|
|
962
|
+
const replaced = original.replace(/^(\s*(?:-\s+)?)(\*+status:\*+|status:)(\s*).*$/i, (_m, indent, key, ws) => `${indent}${key}${ws}acknowledged`);
|
|
963
|
+
newMatchedLines = matchedLines.slice();
|
|
964
|
+
newMatchedLines[statusLineIdx] = replaced;
|
|
965
|
+
}
|
|
966
|
+
const newContent = content.slice(0, matchIndexInContent) + newMatchedLines.join('\n') + content.slice(matchIndexInContent + (end - start));
|
|
967
|
+
return { content: newContent, status: 'ok' };
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Strip one leading `- ` bullet marker (#3457). Heading-delimited deferred
|
|
971
|
+
* entries carry their fields as sibling bullets; `extractGapEntryFields` only
|
|
972
|
+
* de-bullets line 0 (Gaps-protective — there, a later `- ` line is a nested
|
|
973
|
+
* sub-list), so the deferred heading path de-bullets every line itself before
|
|
974
|
+
* field extraction. Non-bullet lines pass through untouched.
|
|
975
|
+
*/
|
|
976
|
+
function stripLeadingBulletMarker(line) {
|
|
977
|
+
return line.replace(/^(\s*)-\s+/, '');
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Split a deferred-items section body into entries delimited by LEAF headings
|
|
981
|
+
* (#3457). Returns `null` when the body contains no heading at all — the
|
|
982
|
+
* caller then falls back to `splitGapsEntries`, keeping headless
|
|
983
|
+
* one-bullet-per-item files byte-for-byte on the pre-#3457 path.
|
|
984
|
+
*
|
|
985
|
+
* A heading is a CONTAINER (group/provenance/title label, contributes no
|
|
986
|
+
* entry) iff the NEXT heading is deeper — a deeper heading lives inside its
|
|
987
|
+
* span. Otherwise it is a LEAF: an entry boundary. This handles all three
|
|
988
|
+
* corpus shapes without hardcoding a depth: flat `#` title + `##` entries
|
|
989
|
+
* (title's next heading is deeper → container; each `##` followed by a
|
|
990
|
+
* same-or-shallower heading → leaf), a `##` container with `###` entries
|
|
991
|
+
* (container's next heading is deeper), and mixed-depth files where a
|
|
992
|
+
* childless `##` entry sits alongside a `##` group with `###` children — every
|
|
993
|
+
* childless heading is a leaf at whatever depth it is written. The shallower
|
|
994
|
+
* rules the issue reports as already tried (split on every heading; shallowest
|
|
995
|
+
* level; deepest level) each mis-count one of these shapes.
|
|
996
|
+
*
|
|
997
|
+
* A leaf entry is [heading text, ...body lines up to the next heading] and is
|
|
998
|
+
* kept only when its body (minus table lines) contains at least one `- `
|
|
999
|
+
* bullet:
|
|
1000
|
+
* - a prose-only or bare heading contributes nothing — "prose is not an item"
|
|
1001
|
+
* is this parser's pre-existing contract (see the `# Notes` case);
|
|
1002
|
+
* - a table-only body is left entirely to `parseDeferredTableItems`, which
|
|
1003
|
+
* unions over the same section body, so the heading cannot double-count the
|
|
1004
|
+
* table's rows.
|
|
1005
|
+
*
|
|
1006
|
+
* Lines before the first heading, and lines directly under a container heading
|
|
1007
|
+
* (before its first child), are split one-bullet-per-item by the unchanged
|
|
1008
|
+
* `splitGapsEntries` — headless parity, so loose bullets before a later
|
|
1009
|
+
* heading group (the mixed shape) stay one item each.
|
|
1010
|
+
*/
|
|
1011
|
+
function splitDeferredHeadingEntries(sectionBody) {
|
|
1012
|
+
const headings = tokenizeHeadings(sectionBody);
|
|
1013
|
+
if (headings.length === 0)
|
|
1014
|
+
return null;
|
|
556
1015
|
const lines = sectionBody.split('\n');
|
|
1016
|
+
const headingByLine = new Map();
|
|
1017
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1018
|
+
// Container iff the next heading is deeper (see doc comment). An empty
|
|
1019
|
+
// heading text (`##` alone) does not itself mean container — the flag is
|
|
1020
|
+
// carried explicitly so a bare LEAF heading still opens an entry.
|
|
1021
|
+
const isContainer = i + 1 < headings.length && headings[i + 1].level > headings[i].level;
|
|
1022
|
+
headingByLine.set(headings[i].line, { text: headings[i].text, isContainer });
|
|
1023
|
+
}
|
|
1024
|
+
const entries = [];
|
|
1025
|
+
let current = null; // accumulating a leaf heading's entry
|
|
1026
|
+
let pending = []; // preamble / container-heading body lines
|
|
1027
|
+
let currentHasBullet = false;
|
|
1028
|
+
const flushCurrent = () => {
|
|
1029
|
+
// Keep the leaf entry only when its body carries a bullet; the heading
|
|
1030
|
+
// text line itself (element 0) never counts as one.
|
|
1031
|
+
if (current !== null && currentHasBullet)
|
|
1032
|
+
entries.push(current);
|
|
1033
|
+
current = null;
|
|
1034
|
+
currentHasBullet = false;
|
|
1035
|
+
};
|
|
1036
|
+
const flushPending = () => {
|
|
1037
|
+
entries.push(...splitGapsEntries(pending.join('\n')));
|
|
1038
|
+
pending = [];
|
|
1039
|
+
};
|
|
1040
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1041
|
+
const lineNo = i + 1;
|
|
1042
|
+
const heading = headingByLine.get(lineNo);
|
|
1043
|
+
if (heading !== undefined) {
|
|
1044
|
+
flushCurrent();
|
|
1045
|
+
// Headless-shaped region (preamble / container-direct bullets) ends at
|
|
1046
|
+
// ANY heading; flushing here keeps entries in document order even when
|
|
1047
|
+
// a container's direct bullets precede its first child entry.
|
|
1048
|
+
flushPending();
|
|
1049
|
+
if (!heading.isContainer) {
|
|
1050
|
+
// Leaf heading: open an entry with the heading text as line 0.
|
|
1051
|
+
current = [heading.text];
|
|
1052
|
+
currentHasBullet = false;
|
|
1053
|
+
}
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
// Table lines belong to parseDeferredTableItems, never to a heading entry.
|
|
1057
|
+
if (/^\s*\|/.test(lines[i].replace(/\r$/, '')))
|
|
1058
|
+
continue;
|
|
1059
|
+
if (current !== null) {
|
|
1060
|
+
current.push(lines[i]);
|
|
1061
|
+
if (/^\s*-\s/.test(lines[i].replace(/\r$/, '')))
|
|
1062
|
+
currentHasBullet = true;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
pending.push(lines[i]);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
flushCurrent();
|
|
1069
|
+
flushPending();
|
|
1070
|
+
return entries;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Extract deferred entries from GFM pipe tables in a deferred-items.md body
|
|
1074
|
+
* (#2766) — a UNION with the bullet scan in `parseDeferredItems`.
|
|
1075
|
+
*
|
|
1076
|
+
* Cells are joined with ` — ` rather than taking only the first: these tables
|
|
1077
|
+
* carry the useful detail in the later columns (the failing seeds, the reason,
|
|
1078
|
+
* the owner), and dropping them would surface a name with no context.
|
|
1079
|
+
*
|
|
1080
|
+
* A row is skipped when any cell reads exactly `resolved`/`done`/`pass`
|
|
1081
|
+
* (case-insensitive), mirroring the "explicit resolution only" convention
|
|
1082
|
+
* `parseGapsItems` uses for `status: resolved` and `parseVerificationItems` uses
|
|
1083
|
+
* for its `hasPassResult` cell scan — so a human can close a tabled deferred
|
|
1084
|
+
* item in place and keep deferred-items.md the single source of truth.
|
|
1085
|
+
*
|
|
1086
|
+
* Deliberately permissive: an unrelated table in a deferred-items.md (say a
|
|
1087
|
+
* table of environment notes) will surface as deferred entries. That is the
|
|
1088
|
+
* correct fail-safe direction for a false-NEGATIVE bug — the whole file exists to
|
|
1089
|
+
* record outstanding work, and this module's established stance (see
|
|
1090
|
+
* parseGapsItems' 'unknown'-status fallback) is to surface a questionable entry
|
|
1091
|
+
* rather than silently drop a real one.
|
|
1092
|
+
*/
|
|
1093
|
+
function parseDeferredTableItems(sectionBody) {
|
|
1094
|
+
const items = [];
|
|
1095
|
+
for (const { rows } of collectTableRows(sectionBody)) {
|
|
1096
|
+
for (const cells of rows) {
|
|
1097
|
+
if (cells.some(c => /^(resolved|done|pass)$/i.test(c)))
|
|
1098
|
+
continue;
|
|
1099
|
+
const name = cells.filter(c => c !== '').join(' — ');
|
|
1100
|
+
if (!name)
|
|
1101
|
+
continue;
|
|
1102
|
+
items.push({
|
|
1103
|
+
name,
|
|
1104
|
+
result: 'unresolved',
|
|
1105
|
+
category: 'deferred',
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return items;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Shared walk behind `splitGapsEntries` and `splitGapsEntriesWithSpans` — ONE
|
|
1113
|
+
* pass over `sectionBody` that both groups its lines into entries (see
|
|
1114
|
+
* `splitGapsEntries`'s doc comment for the grouping rule) AND records each
|
|
1115
|
+
* entry's (start, end) character offset within `sectionBody`. Extracted so
|
|
1116
|
+
* the two public shapes can never drift apart on what counts as an entry
|
|
1117
|
+
* boundary — a second, independently-written grouping pass is exactly how a
|
|
1118
|
+
* span-carrying sibling could disagree with the plain-lines version it is
|
|
1119
|
+
* supposed to be span-annotating.
|
|
1120
|
+
*/
|
|
1121
|
+
function splitGapsEntriesCore(sectionBody) {
|
|
1122
|
+
const rawLines = sectionBody.split('\n');
|
|
1123
|
+
const lineStarts = [];
|
|
1124
|
+
const lineEnds = [];
|
|
1125
|
+
let cursor = 0;
|
|
1126
|
+
for (const rawLine of rawLines) {
|
|
1127
|
+
lineStarts.push(cursor);
|
|
1128
|
+
cursor += rawLine.length;
|
|
1129
|
+
lineEnds.push(cursor);
|
|
1130
|
+
cursor += 1; // the '\n' separator — absent after the final line, but nothing reads past it
|
|
1131
|
+
}
|
|
557
1132
|
const entries = [];
|
|
558
1133
|
let current = null;
|
|
1134
|
+
let currentStartLine = -1;
|
|
1135
|
+
let currentEndLine = -1;
|
|
559
1136
|
let baseIndent = null;
|
|
560
|
-
|
|
1137
|
+
const flush = () => {
|
|
1138
|
+
if (current !== null) {
|
|
1139
|
+
entries.push({ lines: current, start: lineStarts[currentStartLine], end: lineEnds[currentEndLine] });
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
rawLines.forEach((rawLine, idx) => {
|
|
561
1143
|
const line = rawLine.replace(/\r$/, '');
|
|
562
1144
|
const bulletMatch = line.match(/^(\s*)-\s/);
|
|
563
1145
|
if (bulletMatch) {
|
|
@@ -565,20 +1147,61 @@ function splitGapsEntries(sectionBody) {
|
|
|
565
1147
|
if (baseIndent === null)
|
|
566
1148
|
baseIndent = indent;
|
|
567
1149
|
if (indent <= baseIndent) {
|
|
568
|
-
|
|
569
|
-
entries.push(current);
|
|
1150
|
+
flush();
|
|
570
1151
|
current = [line];
|
|
571
|
-
|
|
1152
|
+
currentStartLine = idx;
|
|
1153
|
+
currentEndLine = idx;
|
|
1154
|
+
return;
|
|
572
1155
|
}
|
|
573
1156
|
}
|
|
574
|
-
if (current)
|
|
1157
|
+
if (current !== null) {
|
|
575
1158
|
current.push(line);
|
|
1159
|
+
currentEndLine = idx;
|
|
1160
|
+
}
|
|
576
1161
|
// else: pre-first-bullet content (e.g. the template's HTML comment) — discarded.
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
entries.push(current);
|
|
1162
|
+
});
|
|
1163
|
+
flush();
|
|
580
1164
|
return entries;
|
|
581
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Split a `## Gaps` section body into per-entry line groups on TOP-LEVEL
|
|
1168
|
+
* `- ` bullet openers.
|
|
1169
|
+
*
|
|
1170
|
+
* The indentation of the FIRST bullet line encountered establishes the
|
|
1171
|
+
* "top-level" indent for the whole section; any subsequent `- `-opening line
|
|
1172
|
+
* at that same indent (or shallower) starts a NEW entry, while everything
|
|
1173
|
+
* more deeply indented — field continuation lines (` status: ...`) AND
|
|
1174
|
+
* nested sub-lists (` - src/foo.ts` under ` artifacts:`) — is folded into
|
|
1175
|
+
* the CURRENT entry. This keeps a `artifacts:`/`missing:` sub-list's `- `
|
|
1176
|
+
* items from being mis-split into spurious standalone entries (#2286 review
|
|
1177
|
+
* LOW finding).
|
|
1178
|
+
*
|
|
1179
|
+
* Lines before the first bullet (e.g. the `<!-- YAML format ... -->` comment
|
|
1180
|
+
* the template emits) are discarded. An empty/whitespace-only section body
|
|
1181
|
+
* (heading present, no bullets) returns `[]`.
|
|
1182
|
+
*/
|
|
1183
|
+
function splitGapsEntries(sectionBody) {
|
|
1184
|
+
return splitGapsEntriesCore(sectionBody).map((entry) => entry.lines);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Sibling of `splitGapsEntries` (F1, #3458 follow-up review) that ADDITIVELY
|
|
1188
|
+
* carries each entry's character span — every existing `splitGapsEntries`
|
|
1189
|
+
* caller (`parseGapsItems`, `parseDeferredItemsWithStatus`,
|
|
1190
|
+
* `splitDeferredHeadingEntries`'s `flushPending`) is unaffected and keeps
|
|
1191
|
+
* using the plain `lines`-only shape. `acknowledgeDeferredItem` is the one
|
|
1192
|
+
* caller that needs a span: it used to select an entry via `splitGapsEntries`
|
|
1193
|
+
* and then RE-FIND that entry's location with a fresh regex search over
|
|
1194
|
+
* `sectionBody` — matching the FIRST occurrence of the entry's exact text,
|
|
1195
|
+
* not necessarily the entry actually selected (a continuation/quoted line
|
|
1196
|
+
* inside a DIFFERENT entry can carry byte-identical text). Because the
|
|
1197
|
+
* mis-targeted span is byte-identical to the target text, no check on the
|
|
1198
|
+
* WRITTEN result could ever tell a wrong-entry write apart from a correct
|
|
1199
|
+
* one. Carrying the span out of THIS same pass — the one that already knows
|
|
1200
|
+
* exactly where the entry lives — removes the re-derivation step entirely.
|
|
1201
|
+
*/
|
|
1202
|
+
function splitGapsEntriesWithSpans(sectionBody) {
|
|
1203
|
+
return splitGapsEntriesCore(sectionBody);
|
|
1204
|
+
}
|
|
582
1205
|
/**
|
|
583
1206
|
* Extract `key: value` fields from one Gaps entry's lines, anchored to the
|
|
584
1207
|
* START of each (bullet-marker-stripped, trimmed) line — never scanning the
|
|
@@ -593,10 +1216,22 @@ function splitGapsEntries(sectionBody) {
|
|
|
593
1216
|
* any nested sub-list content in the template's field ordering); later
|
|
594
1217
|
* `key:`-shaped nested-list content is captured, if it parses as one, but
|
|
595
1218
|
* never overrides an already-seen top-level field.
|
|
1219
|
+
*
|
|
1220
|
+
* #3457: markdown emphasis around the KEY (`**Status:** resolved` — the
|
|
1221
|
+
* deferred-items convention bolds every field, and a bolded resolution marker
|
|
1222
|
+
* previously failed this regex outright and surfaced as its own bogus
|
|
1223
|
+
* unresolved entry) is unwrapped before the match, still anchored at the
|
|
1224
|
+
* start of the line. The unwrapped key is lower-cased, because the bolded
|
|
1225
|
+
* convention form is Title-cased (`**Status:**`) while the field vocabulary
|
|
1226
|
+
* this module reads is lowercase (`status`) — the same normalization
|
|
1227
|
+
* `mapGapsHeader` already applies to table header cells. Bare (unbolded) keys
|
|
1228
|
+
* keep their literal case, and mid-line emphasis is untouched, preserving the
|
|
1229
|
+
* start-anchored decoy invariant above.
|
|
596
1230
|
*/
|
|
597
1231
|
function extractGapEntryFields(entryLines) {
|
|
598
1232
|
const fields = {};
|
|
599
1233
|
const fieldLineRe = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/;
|
|
1234
|
+
const boldedKeyRe = /^\*+([A-Za-z_][A-Za-z0-9_-]*):\*+/;
|
|
600
1235
|
entryLines.forEach((rawLine, idx) => {
|
|
601
1236
|
const line = rawLine.replace(/\r$/, '');
|
|
602
1237
|
// Strip ONLY the entry-opening bullet marker (idx 0); a bullet marker on
|
|
@@ -604,7 +1239,8 @@ function extractGapEntryFields(entryLines) {
|
|
|
604
1239
|
// `splitGapsEntries` already folding it in — it is not itself a field
|
|
605
1240
|
// line unless it independently matches `key: value` after stripping.
|
|
606
1241
|
const bulletStripped = line.match(/^(\s*)-\s+(.*)$/);
|
|
607
|
-
const content = idx === 0 && bulletStripped ? bulletStripped[2] : line.trim()
|
|
1242
|
+
const content = (idx === 0 && bulletStripped ? bulletStripped[2] : line.trim())
|
|
1243
|
+
.replace(boldedKeyRe, (_m, key) => `${key.toLowerCase()}:`);
|
|
608
1244
|
const m = fieldLineRe.exec(content);
|
|
609
1245
|
if (!m)
|
|
610
1246
|
return;
|
|
@@ -834,5 +1470,11 @@ module.exports = {
|
|
|
834
1470
|
cmdRenderCheckpoint,
|
|
835
1471
|
parseCurrentTest,
|
|
836
1472
|
buildCheckpoint,
|
|
1473
|
+
CHECKPOINT_FRAMES,
|
|
1474
|
+
CHECKPOINT_LANGUAGE_ALIASES,
|
|
1475
|
+
resolveCheckpointFrame,
|
|
1476
|
+
checkpointBoxLine,
|
|
837
1477
|
parseDeferredItems,
|
|
1478
|
+
parseDeferredItemsWithStatus,
|
|
1479
|
+
acknowledgeDeferredItem,
|
|
838
1480
|
};
|