@opengsd/gsd-core 1.10.0 → 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 +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- 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 +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.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 +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -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 +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- 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 +575 -101
- 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 +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- 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 +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- 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-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- 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 +134 -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-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- 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 +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- 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 +56 -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/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- 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/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -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 +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- 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 +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- 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 +24 -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 +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- 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/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- 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/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- 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.md +2 -1
- 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 +31 -6
- 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 +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- 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/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- 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/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- 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 +77 -2
- 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 +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- 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 +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- 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 +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- 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/changeset/lint.cjs +60 -5
- 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-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- 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 +1 -26
- 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-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-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -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 +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- 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 +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- 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 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -24,9 +24,6 @@ const { collectSection, tokenizeHeadings } = markdownSectionizer;
|
|
|
24
24
|
const markdownTable = require("./markdown-table.cjs");
|
|
25
25
|
const { splitTableRow, isDelimiterRow } = markdownTable;
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
-
const roadmapParser = require("./roadmap-parser.cjs");
|
|
28
|
-
const { getMilestonePhaseFilter } = roadmapParser;
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
27
|
const coreUtils = require("./core-utils.cjs");
|
|
31
28
|
const { toPosixPath } = coreUtils;
|
|
32
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -37,10 +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;
|
|
41
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
39
|
const phaseLocator = require("./phase-locator.cjs");
|
|
43
|
-
const { getArchivedPhaseDirs } = phaseLocator;
|
|
40
|
+
const { getArchivedPhaseDirs, listMilestonePhaseDirs } = phaseLocator;
|
|
44
41
|
const security_cjs_1 = require("./security.cjs");
|
|
45
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
46
43
|
const configLoader = require("./config-loader.cjs");
|
|
@@ -65,19 +62,18 @@ function cmdAuditUat(cwd, raw) {
|
|
|
65
62
|
if (!hasActivePhases && archivedDirs.length === 0) {
|
|
66
63
|
error('No phases directory found in planning directory');
|
|
67
64
|
}
|
|
68
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
69
65
|
const results = [];
|
|
70
66
|
// Active dirs are milestone-filtered; archived dirs deliberately are NOT.
|
|
71
|
-
//
|
|
72
|
-
// ROADMAP.md, and archived phases belong
|
|
73
|
-
// applying it to them discards every
|
|
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.
|
|
74
71
|
const scanTargets = [];
|
|
75
72
|
if (hasActivePhases) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.sort();
|
|
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;
|
|
81
77
|
for (const dir of dirs) {
|
|
82
78
|
scanTargets.push({ dir, phaseDir: node_path_1.default.join(phasesDir, dir) });
|
|
83
79
|
}
|
|
@@ -93,8 +89,12 @@ function cmdAuditUat(cwd, raw) {
|
|
|
93
89
|
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
94
90
|
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
95
91
|
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
96
|
-
// Process UAT files
|
|
97
|
-
|
|
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)) {
|
|
98
98
|
const uatFilePath = node_path_1.default.join(phaseDir, file);
|
|
99
99
|
const content = node_fs_1.default.readFileSync(uatFilePath, 'utf-8');
|
|
100
100
|
const items = parseUatItems(content);
|
|
@@ -111,8 +111,9 @@ function cmdAuditUat(cwd, raw) {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
// Process VERIFICATION files
|
|
115
|
-
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)) {
|
|
116
117
|
const verificationFilePath = node_path_1.default.join(phaseDir, file);
|
|
117
118
|
const content = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
|
|
118
119
|
const status = extractFrontmatter(content, verificationFilePath).status || 'unknown';
|
|
@@ -766,31 +767,308 @@ function parseGapsTableItems(sectionBody) {
|
|
|
766
767
|
* `.planning/todos/pending/*.md` entry required). Every other entry —
|
|
767
768
|
* including one with no `status:` field at all — is UNRESOLVED and is
|
|
768
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.
|
|
769
777
|
*/
|
|
770
|
-
|
|
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) {
|
|
771
790
|
const deferredSection = collectSection(content, (h) => /^deferred\s+items$/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
772
791
|
const sectionBody = deferredSection ? deferredSection.body : content;
|
|
773
792
|
const items = [];
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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) {
|
|
779
809
|
const text = rawGapEntryText(entryLines);
|
|
780
810
|
if (!text)
|
|
781
811
|
continue;
|
|
782
|
-
items.push({
|
|
783
|
-
name: text,
|
|
784
|
-
result: 'unresolved',
|
|
785
|
-
category: 'deferred',
|
|
786
|
-
});
|
|
812
|
+
items.push({ name: text, status: fields.status || '' });
|
|
787
813
|
}
|
|
788
814
|
// #2766: union with the table form — see parseDeferredTableItems. Executors
|
|
789
815
|
// write this file by hand with no mandated shape, and a GFM table is a natural
|
|
790
816
|
// choice for the common "test → failing seeds" case, which produced ZERO items.
|
|
791
|
-
|
|
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: '' })));
|
|
792
826
|
return items;
|
|
793
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
|
+
}
|
|
837
|
+
/**
|
|
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.
|
|
850
|
+
*
|
|
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.
|
|
859
|
+
*
|
|
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.
|
|
896
|
+
*/
|
|
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;
|
|
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
|
+
}
|
|
794
1072
|
/**
|
|
795
1073
|
* Extract deferred entries from GFM pipe tables in a deferred-items.md body
|
|
796
1074
|
* (#2766) — a UNION with the bullet scan in `parseDeferredItems`.
|
|
@@ -831,28 +1109,37 @@ function parseDeferredTableItems(sectionBody) {
|
|
|
831
1109
|
return items;
|
|
832
1110
|
}
|
|
833
1111
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
* the CURRENT entry. This keeps a `artifacts:`/`missing:` sub-list's `- `
|
|
843
|
-
* items from being mis-split into spurious standalone entries (#2286 review
|
|
844
|
-
* LOW finding).
|
|
845
|
-
*
|
|
846
|
-
* Lines before the first bullet (e.g. the `<!-- YAML format ... -->` comment
|
|
847
|
-
* the template emits) are discarded. An empty/whitespace-only section body
|
|
848
|
-
* (heading present, no bullets) returns `[]`.
|
|
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.
|
|
849
1120
|
*/
|
|
850
|
-
function
|
|
851
|
-
const
|
|
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
|
+
}
|
|
852
1132
|
const entries = [];
|
|
853
1133
|
let current = null;
|
|
1134
|
+
let currentStartLine = -1;
|
|
1135
|
+
let currentEndLine = -1;
|
|
854
1136
|
let baseIndent = null;
|
|
855
|
-
|
|
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) => {
|
|
856
1143
|
const line = rawLine.replace(/\r$/, '');
|
|
857
1144
|
const bulletMatch = line.match(/^(\s*)-\s/);
|
|
858
1145
|
if (bulletMatch) {
|
|
@@ -860,20 +1147,61 @@ function splitGapsEntries(sectionBody) {
|
|
|
860
1147
|
if (baseIndent === null)
|
|
861
1148
|
baseIndent = indent;
|
|
862
1149
|
if (indent <= baseIndent) {
|
|
863
|
-
|
|
864
|
-
entries.push(current);
|
|
1150
|
+
flush();
|
|
865
1151
|
current = [line];
|
|
866
|
-
|
|
1152
|
+
currentStartLine = idx;
|
|
1153
|
+
currentEndLine = idx;
|
|
1154
|
+
return;
|
|
867
1155
|
}
|
|
868
1156
|
}
|
|
869
|
-
if (current)
|
|
1157
|
+
if (current !== null) {
|
|
870
1158
|
current.push(line);
|
|
1159
|
+
currentEndLine = idx;
|
|
1160
|
+
}
|
|
871
1161
|
// else: pre-first-bullet content (e.g. the template's HTML comment) — discarded.
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
entries.push(current);
|
|
1162
|
+
});
|
|
1163
|
+
flush();
|
|
875
1164
|
return entries;
|
|
876
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
|
+
}
|
|
877
1205
|
/**
|
|
878
1206
|
* Extract `key: value` fields from one Gaps entry's lines, anchored to the
|
|
879
1207
|
* START of each (bullet-marker-stripped, trimmed) line — never scanning the
|
|
@@ -888,10 +1216,22 @@ function splitGapsEntries(sectionBody) {
|
|
|
888
1216
|
* any nested sub-list content in the template's field ordering); later
|
|
889
1217
|
* `key:`-shaped nested-list content is captured, if it parses as one, but
|
|
890
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.
|
|
891
1230
|
*/
|
|
892
1231
|
function extractGapEntryFields(entryLines) {
|
|
893
1232
|
const fields = {};
|
|
894
1233
|
const fieldLineRe = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/;
|
|
1234
|
+
const boldedKeyRe = /^\*+([A-Za-z_][A-Za-z0-9_-]*):\*+/;
|
|
895
1235
|
entryLines.forEach((rawLine, idx) => {
|
|
896
1236
|
const line = rawLine.replace(/\r$/, '');
|
|
897
1237
|
// Strip ONLY the entry-opening bullet marker (idx 0); a bullet marker on
|
|
@@ -899,7 +1239,8 @@ function extractGapEntryFields(entryLines) {
|
|
|
899
1239
|
// `splitGapsEntries` already folding it in — it is not itself a field
|
|
900
1240
|
// line unless it independently matches `key: value` after stripping.
|
|
901
1241
|
const bulletStripped = line.match(/^(\s*)-\s+(.*)$/);
|
|
902
|
-
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()}:`);
|
|
903
1244
|
const m = fieldLineRe.exec(content);
|
|
904
1245
|
if (!m)
|
|
905
1246
|
return;
|
|
@@ -1134,4 +1475,6 @@ module.exports = {
|
|
|
1134
1475
|
resolveCheckpointFrame,
|
|
1135
1476
|
checkpointBoxLine,
|
|
1136
1477
|
parseDeferredItems,
|
|
1478
|
+
parseDeferredItemsWithStatus,
|
|
1479
|
+
acknowledgeDeferredItem,
|
|
1137
1480
|
};
|