@opengsd/gsd-core 1.18.0 → 1.20.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 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +7 -7
- package/agents/gsd-code-fixer.md +13 -7
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +179 -30
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +376 -103
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +32 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +45 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +245 -68
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +61 -51
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +629 -38
- package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +3066 -653
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
- package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
- package/gsd-core/bin/lib/roadmap.cjs +383 -123
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
- package/gsd-core/bin/lib/smart-entry.cjs +35 -11
- package/gsd-core/bin/lib/state-contract.cjs +39 -15
- package/gsd-core/bin/lib/state-document.cjs +344 -27
- package/gsd-core/bin/lib/state-transition.cjs +156 -32
- package/gsd-core/bin/lib/state.cjs +527 -186
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +4 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +64 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +10 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +14 -14
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +70 -154
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +15 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +37 -3
- package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
- package/hooks/dist/gsd-workflow-guard.js +3 -2
- package/hooks/dist/gsd-worktree-path-guard.js +29 -2
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +37 -3
- package/hooks/gsd-windsurf-pre-write.js +29 -2
- package/hooks/gsd-workflow-guard.js +3 -2
- package/hooks/gsd-worktree-path-guard.js +29 -2
- package/hooks/hooks.json +1 -0
- package/hooks/lib/git-probe.js +16 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +15 -8
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +774 -0
- package/pi/package-agents/gsd-code-reviewer.md +441 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +898 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +54 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-features.cjs +14 -2
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
22
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
23
|
};
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
24
25
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
26
27
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
28
|
+
const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
|
|
27
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
30
|
const phaseIdModule = require("./phase-id.cjs");
|
|
29
31
|
const { phaseMarkdownRegexSource, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE,
|
|
@@ -53,10 +55,14 @@ const planningWorkspace = require("./planning-workspace.cjs");
|
|
|
53
55
|
const { planningDir, resolvePhaseIdConvention } = planningWorkspace;
|
|
54
56
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
55
57
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
59
|
+
const { rawFrontmatterMapping } = frontmatterMod;
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
61
|
const unusableInputMod = require("./unusable-input.cjs");
|
|
57
62
|
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
58
63
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
59
64
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
65
|
+
const roadmap_progress_cjs_1 = require("./roadmap-progress.cjs");
|
|
60
66
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
61
67
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
62
68
|
const { SCOPE } = planningScopeMod;
|
|
@@ -72,12 +78,94 @@ const MILESTONE_ACTIVE_MARKER_PATTERN = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progres
|
|
|
72
78
|
function isClosedMilestoneHeading(headingText) {
|
|
73
79
|
return MILESTONE_CLOSED_MARKER_PATTERN.test(headingText) && !MILESTONE_ACTIVE_MARKER_PATTERN.test(headingText);
|
|
74
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Is this `<details>` block a CLOSED milestone? One test for the read-side strip
|
|
83
|
+
* and the write-side mask. The `<summary>` is read with inline tags removed
|
|
84
|
+
* (`<b>v1.0</b> ✅`); a milestone-shaped summary (version token or "milestone")
|
|
85
|
+
* that says "Complete(d)" counts too. No `<summary>` means not closed.
|
|
86
|
+
*/
|
|
87
|
+
function isClosedMilestoneBlock(block) {
|
|
88
|
+
const summary = block.match(/<summary[^>]*>([\s\S]{0,1000}?)<\/summary>/i);
|
|
89
|
+
if (!summary)
|
|
90
|
+
return false;
|
|
91
|
+
const text = summary[1].replace(/<[^>]{0,200}>/g, '');
|
|
92
|
+
return isClosedMilestoneHeading(text)
|
|
93
|
+
|| (/\bcomplete(?:d)?\b/i.test(text) && /\bv\d+(?:\.\d+)*\b|\bmilestone\b/i.test(text)
|
|
94
|
+
&& !MILESTONE_ACTIVE_MARKER_PATTERN.test(text));
|
|
95
|
+
}
|
|
75
96
|
/**
|
|
76
97
|
* Strip shipped milestone content wrapped in <details> blocks.
|
|
77
98
|
*/
|
|
78
99
|
function stripShippedMilestones(content) {
|
|
79
100
|
return (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content, 'details');
|
|
80
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* #5543: hide every CLOSED-milestone `<details>` block (`<details>` opener, a
|
|
104
|
+
* `<summary>` that `isClosedMilestoneHeading` reads as shipped/archived, the
|
|
105
|
+
* #3982 test) behind a placeholder comment; `restore` puts them back
|
|
106
|
+
* byte-for-byte, or returns null when `edited` lost or duplicated a placeholder.
|
|
107
|
+
* For writers whose first `### Phase N:` in document order must never sit in a
|
|
108
|
+
* shipped block. Other `<details>` (notes, a phase wrapper, an active
|
|
109
|
+
* milestone's collapsed block) stay visible, so a roadmap with no archived
|
|
110
|
+
* milestone is edited as before. Blocks are matched by depth, so a notes
|
|
111
|
+
* `<details>` nested inside a shipped one stays inside it (the flat
|
|
112
|
+
* `taggedBlockPattern` closes at the inner `</details>`). An unmatched
|
|
113
|
+
* opener (prose, a fence) wraps nothing, so a later shipped block is still
|
|
114
|
+
* masked. Fences are not parsed, as in every ROADMAP `<details>` reader here.
|
|
115
|
+
*/
|
|
116
|
+
function maskShippedBlocks(content) {
|
|
117
|
+
const blocks = [];
|
|
118
|
+
let nonce = 0;
|
|
119
|
+
while (content.includes(`<!-- gsd-masked-shipped-${nonce}-`))
|
|
120
|
+
nonce += 1;
|
|
121
|
+
const placeholder = (i) => `<!-- gsd-masked-shipped-${nonce}-${i} -->`;
|
|
122
|
+
const tags = /<details(?:\s[^>]{0,1000})?>|<\/details>/gi;
|
|
123
|
+
// Matched pairs by stack; an unmatched opener (prose, a fence) never closes, so it
|
|
124
|
+
// wraps nothing and a later block is still found. `outer` keeps only the pairs not
|
|
125
|
+
// nested in another matched pair.
|
|
126
|
+
const open = [];
|
|
127
|
+
const outer = [];
|
|
128
|
+
for (let m = tags.exec(content); m !== null; m = tags.exec(content)) {
|
|
129
|
+
if (m[0][1] !== '/')
|
|
130
|
+
open.push(m.index);
|
|
131
|
+
else if (open.length > 0) {
|
|
132
|
+
const start = open.pop();
|
|
133
|
+
while (outer.length > 0 && outer[outer.length - 1][0] >= start)
|
|
134
|
+
outer.pop();
|
|
135
|
+
outer.push([start, m.index + m[0].length]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let masked = '';
|
|
139
|
+
let copied = 0;
|
|
140
|
+
for (const [start, end] of outer) {
|
|
141
|
+
const block = content.slice(start, end);
|
|
142
|
+
if (block.startsWith('<details>') && isClosedMilestoneBlock(block)) {
|
|
143
|
+
masked += content.slice(copied, start) + placeholder(blocks.length);
|
|
144
|
+
blocks.push(block);
|
|
145
|
+
copied = end;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
masked += content.slice(copied);
|
|
149
|
+
return {
|
|
150
|
+
masked,
|
|
151
|
+
restore(edited) {
|
|
152
|
+
let restored = edited;
|
|
153
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
154
|
+
const marker = placeholder(i);
|
|
155
|
+
const at = restored.indexOf(marker);
|
|
156
|
+
if (at === -1 || restored.indexOf(marker, at + marker.length) !== -1)
|
|
157
|
+
return null;
|
|
158
|
+
restored = restored.slice(0, at) + blocks[i] + restored.slice(at + marker.length);
|
|
159
|
+
}
|
|
160
|
+
return restored;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function editOutsideShippedBlocks(content, edit) {
|
|
165
|
+
const { masked, restore } = maskShippedBlocks(content);
|
|
166
|
+
const edited = edit(masked);
|
|
167
|
+
return (typeof edited === 'string' ? restore(edited) : null) ?? content;
|
|
168
|
+
}
|
|
81
169
|
/**
|
|
82
170
|
* #3982: strip only <details> blocks whose <summary> marks a CLOSED milestone
|
|
83
171
|
* (ARCHIVED/SHIPPED/✅/… without an active marker) — the narrow form of
|
|
@@ -86,12 +174,15 @@ function stripShippedMilestones(content) {
|
|
|
86
174
|
* reproduce the phase_count: 0 class of #557/#2947.
|
|
87
175
|
*/
|
|
88
176
|
function stripClosedMilestoneDetails(content) {
|
|
89
|
-
return content.replace(/<details\b[^>]*>[\s\S]*?<\/details>/gi, (block) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
177
|
+
return content.replace(/<details\b[^>]*>[\s\S]*?<\/details>/gi, (block) => (isClosedMilestoneBlock(block) ? '' : block));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Classify a complete `<details>` block through isClosedMilestoneBlock, the one rule shared with
|
|
181
|
+
* stripClosedMilestoneDetails and the write-side shipped-block mask. Destructive consumers share this predicate so
|
|
182
|
+
* an active collapsed phase list is never reclassified as shipped history.
|
|
183
|
+
*/
|
|
184
|
+
function isClosedMilestoneDetails(block) {
|
|
185
|
+
return isClosedMilestoneBlock(block);
|
|
95
186
|
}
|
|
96
187
|
/**
|
|
97
188
|
* #2562: is the milestone `version` marked SHIPPED by the ROADMAP itself?
|
|
@@ -347,6 +438,25 @@ function listMilestoneHeadings(content) {
|
|
|
347
438
|
// rather than typing `^#{1,3}\s+(?!Phase\s+\S)` a second time — the exact
|
|
348
439
|
// duplication class ADR-3180 §7.2's widened guard exists to catch.
|
|
349
440
|
const MILESTONE_HEADING_LINE_SOURCE = '^#{1,3}\\s+(?!Phase\\s+\\S)[^\\n]*';
|
|
441
|
+
/**
|
|
442
|
+
* #4304: classify one level-1..3 heading through the same milestone grammar
|
|
443
|
+
* the window locator owns. Phase headings are excluded first through the
|
|
444
|
+
* reader's own legacy/bracket discriminators, regardless of words or markers
|
|
445
|
+
* in their titles; `Failed Payment Recovery` and `Done ✅` remain phase
|
|
446
|
+
* titles, never milestone section boundaries. Version-bearing headings route
|
|
447
|
+
* through `listMilestoneHeadings`; version-less bracket headings route through
|
|
448
|
+
* `isBracketMilestoneBoundary`, the discriminator also used by the bracket
|
|
449
|
+
* fallback locator.
|
|
450
|
+
*/
|
|
451
|
+
function isRecognizedMilestoneHeading(headingText, level) {
|
|
452
|
+
if (level < 1 || level > 3)
|
|
453
|
+
return false;
|
|
454
|
+
if (/^Phase\s+\S/i.test(headingText) || BRACKET_PHASE_TAIL_RE.test(headingText))
|
|
455
|
+
return false;
|
|
456
|
+
const line = `${'#'.repeat(level)} ${headingText}`;
|
|
457
|
+
return listMilestoneHeadings(line).length > 0
|
|
458
|
+
|| isBracketMilestoneBoundary(headingText, level, null);
|
|
459
|
+
}
|
|
350
460
|
/**
|
|
351
461
|
* #3184: the sole milestone-heading locator. Boundary-matched on the version
|
|
352
462
|
* token with `\b`, NOT the stricter `(?![\w.-])`: this function keeps `\b`
|
|
@@ -665,23 +775,32 @@ const PHASE_HEADING_TEXT_RE = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:
|
|
|
665
775
|
* leading `#{1,6}` markers and surrounding whitespace already stripped
|
|
666
776
|
* (`tokenizeHeadings`'s own `h.text`, or the equivalent for a line a caller
|
|
667
777
|
* already knows is a `#{2,4}` heading) — is a phase heading by the readers'
|
|
668
|
-
* own grammar. The single owner of this test
|
|
669
|
-
*
|
|
778
|
+
* own grammar. The single owner of this test: `hasPhaseEntries` reaches it
|
|
779
|
+
* through `isPhaseEntryHeading` below, and the bracket roadmap migrator
|
|
780
|
+
* calls it directly.
|
|
670
781
|
*/
|
|
671
782
|
function isPhaseHeadingText(headingText) {
|
|
672
783
|
return PHASE_HEADING_TEXT_RE.test(headingText);
|
|
673
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Does one fence-excluded heading token use the reader's phase-entry grammar?
|
|
787
|
+
* Kept separate from heading depth so destructive consumers can apply the
|
|
788
|
+
* same bracket-plus-legacy recognition to tokens from `tokenizeHeadings`.
|
|
789
|
+
* The legacy arm is `isPhaseHeadingText`, that grammar's single owner.
|
|
790
|
+
*/
|
|
791
|
+
function isPhaseEntryHeading(headingText, phaseIdConvention) {
|
|
792
|
+
if (isPhaseHeadingText(headingText))
|
|
793
|
+
return true;
|
|
794
|
+
return phaseIdConvention === 'bracket' && BRACKET_PHASE_ENTRY_HEADING_RE.test(headingText);
|
|
795
|
+
}
|
|
674
796
|
function hasPhaseEntries(markdown, phaseIdConvention) {
|
|
675
797
|
// #3641: the widened grammar engages ONLY when the resolved convention is
|
|
676
798
|
// 'bracket' — a project that has not opted in runs the legacy pattern
|
|
677
799
|
// alone, byte-identically.
|
|
678
|
-
const bracketMode = phaseIdConvention === 'bracket';
|
|
679
800
|
for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(markdown)) {
|
|
680
801
|
if (h.level < 2 || h.level > 4)
|
|
681
802
|
continue;
|
|
682
|
-
if (
|
|
683
|
-
return true;
|
|
684
|
-
if (bracketMode && BRACKET_PHASE_ENTRY_HEADING_RE.test(h.text))
|
|
803
|
+
if (isPhaseEntryHeading(h.text, phaseIdConvention))
|
|
685
804
|
return true;
|
|
686
805
|
}
|
|
687
806
|
// #3184 review finding: the bullet fallback must be fence-aware too, or a
|
|
@@ -775,7 +894,7 @@ function hasPhaseListingTableHeader(content) {
|
|
|
775
894
|
* narrower question — "which phase ids does this window declare" — where only
|
|
776
895
|
* the 999 icebox range is excluded.
|
|
777
896
|
*/
|
|
778
|
-
function scanMilestonePhaseIdSets(window, convention) {
|
|
897
|
+
function scanMilestonePhaseIdSets(window, convention, context) {
|
|
779
898
|
const ids = new Set();
|
|
780
899
|
const qualifiedIds = new Set();
|
|
781
900
|
// Use tokenizeHeadings (fence-aware) instead of stripFencedLines + regex.
|
|
@@ -786,7 +905,7 @@ function scanMilestonePhaseIdSets(window, convention) {
|
|
|
786
905
|
// belonging to different milestones.
|
|
787
906
|
const capturing = convention === 'bracket';
|
|
788
907
|
const bracketGroups = capturing ? 1 : 0;
|
|
789
|
-
const phaseHeadingPattern = new RegExp(`^${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, convention, capturing)}([\\w][\\w.-]*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
|
|
908
|
+
const phaseHeadingPattern = new RegExp(`^${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, convention, capturing)}(${context?.phaseNaming === 'custom' && convention !== 'bracket' ? phaseIdModule.phaseScanTokenSource(context) : '[\\w][\\w.-]*'})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
|
|
790
909
|
for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(window)) {
|
|
791
910
|
if (h.level < 2 || h.level > 4)
|
|
792
911
|
continue;
|
|
@@ -944,6 +1063,87 @@ function classifyMilestoneWindow(input) {
|
|
|
944
1063
|
const PHASE_HEADING_BLOCK_STRIP_RE = new RegExp(
|
|
945
1064
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` (OPTIONAL_PHASE_TAG_SOURCE) tolerates a pre-colon ( ) tag.
|
|
946
1065
|
`^#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY)}${PHASE_NUMBER_TOKEN_SOURCE}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:[^\\n]*(?:\\n(?!#{1,6}\\s)[^\\n]*)*\\n?`, 'gim');
|
|
1066
|
+
/**
|
|
1067
|
+
* #612/#4304: the bracket-fallback candidate search for a milestone heading
|
|
1068
|
+
* carrying no vX.Y version token at all — the ADR-612 canonical shape
|
|
1069
|
+
* (`## [GSD.09] Hidden`). Tried by a caller only when the legacy
|
|
1070
|
+
* version-string search (`locateMilestoneHeadings`) finds nothing and the
|
|
1071
|
+
* project is on the bracket convention. Extracted (#4304 PR-4) from
|
|
1072
|
+
* `extractCurrentMilestoneScoped`'s own inline block so it and
|
|
1073
|
+
* `currentMilestoneRawRanges` share one heading grammar instead of each
|
|
1074
|
+
* re-deriving the bracket intro pattern independently, the Generative Fix
|
|
1075
|
+
* Divergence class #2761 documents. Byte-identical logic to the block it replaces; moving it
|
|
1076
|
+
* changes no behavior.
|
|
1077
|
+
*/
|
|
1078
|
+
function bracketFallbackHeadingMatches(content, version, bracketScopeConvention) {
|
|
1079
|
+
if (bracketScopeConvention !== 'bracket')
|
|
1080
|
+
return [];
|
|
1081
|
+
const vMatch = version.match(/^v(\d+)/i);
|
|
1082
|
+
const milestoneInt = vMatch ? parseInt(vMatch[1], 10) : NaN;
|
|
1083
|
+
if (!Number.isSafeInteger(milestoneInt))
|
|
1084
|
+
return [];
|
|
1085
|
+
// #2761 M3: the phase-id owner supplies both the bracket intro grammar
|
|
1086
|
+
// and canonical pad2 spelling; `[CODE.2]` therefore cannot bound a
|
|
1087
|
+
// section whose phase headings the bracket grammar rejects.
|
|
1088
|
+
// #612: HeadingToken.offset is the line start, so require the
|
|
1089
|
+
// `#` there to preserve the old line-start anchor's indentation parity.
|
|
1090
|
+
// For every survivor, rebuilding [fullLine, fullLine] plus `.index`
|
|
1091
|
+
// retains the match shape and first-match order expected downstream.
|
|
1092
|
+
const bracketMilestoneHeadingRe = new RegExp(`^${bracketMilestoneIntroSrcFor(milestoneInt)}`, 'i');
|
|
1093
|
+
return (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content)
|
|
1094
|
+
.filter((h) => h.level <= 3 && content[h.offset] === '#' && bracketMilestoneHeadingRe.test(h.text))
|
|
1095
|
+
.map((h) => {
|
|
1096
|
+
const lineEnd = content.indexOf('\n', h.offset);
|
|
1097
|
+
const fullLine = content.slice(h.offset, lineEnd === -1 ? content.length : lineEnd);
|
|
1098
|
+
return Object.assign([fullLine, fullLine], { index: h.offset });
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* #612/#4304: the milestone-heading SELECTION fallback chain — prefers the
|
|
1103
|
+
* canonical legacy selector (`selectMilestoneHeading`), then falls back to
|
|
1104
|
+
* the gated bracket candidates (`headingMatches`, already populated by
|
|
1105
|
+
* `bracketFallbackHeadingMatches` when the legacy selector's own internal
|
|
1106
|
+
* `locateMilestoneHeadings` call finds nothing) when a name-only bracket
|
|
1107
|
+
* milestone carries no version token for that selector to find. Shared
|
|
1108
|
+
* (#4304 PR-4) by `extractCurrentMilestoneScoped` and
|
|
1109
|
+
* `currentMilestoneRawRanges` so both agree on which heading is "the active
|
|
1110
|
+
* one" for a version-less bracket milestone. `headingMatches` must be
|
|
1111
|
+
* non-empty (every caller checks this first).
|
|
1112
|
+
*/
|
|
1113
|
+
function selectActiveMilestoneHeading(content, version, headingMatches) {
|
|
1114
|
+
return selectMilestoneHeading(content, version)
|
|
1115
|
+
?? headingMatches.find((m) => !isClosedMilestoneHeading(m[1]))
|
|
1116
|
+
?? headingMatches[0];
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* #612/#4304: resolves the bracket-aware milestone SECTION END, the
|
|
1120
|
+
* SELECTED heading's own bracket id, and the `bracketBoundary` predicate
|
|
1121
|
+
* itself — the caller reuses all three afterward (`extractCurrentMilestoneScoped`'s
|
|
1122
|
+
* own "earliest milestone heading" preamble scan reuses `selectedBracketId`;
|
|
1123
|
+
* its "(Phase Details)" section-end lookup reuses `bracketBoundary`).
|
|
1124
|
+
* `bracketBoundaryActive` and `currentMilestoneHeadings` are supplied by the
|
|
1125
|
+
* caller (not recomputed here) because both callers need them again
|
|
1126
|
+
* afterward for their own, divergent purposes. Shared (#4304 PR-4) by
|
|
1127
|
+
* `extractCurrentMilestoneScoped` and `currentMilestoneRawRanges` so a
|
|
1128
|
+
* version-less bracket milestone heading (`## [GSD.09] Hidden`) bounds its
|
|
1129
|
+
* section identically for both, instead of one of them silently running the
|
|
1130
|
+
* LEGACY-only (vX.Y/emoji-only) boundary rule. Byte-identical logic to the
|
|
1131
|
+
* block it replaces; moving it changes no behavior.
|
|
1132
|
+
*/
|
|
1133
|
+
function bracketAwareMilestoneSection(content, selected, sectionStart, bracketBoundaryActive, currentMilestoneHeadings) {
|
|
1134
|
+
// #2761 B1: a bracket heading bearing the selected milestone's own id is
|
|
1135
|
+
// a continuation, not a boundary. Derive the selected id once and compose
|
|
1136
|
+
// the shared discriminator with upstream's centralized section-end walk.
|
|
1137
|
+
const selectedBracketMatch = bracketBoundaryActive
|
|
1138
|
+
? selected[0].match(new RegExp(`^#{1,3}\\s+\\[(${BRACKET_ID_SRC})\\]`, 'i'))
|
|
1139
|
+
: null;
|
|
1140
|
+
const selectedBracketId = selectedBracketMatch ? foldBracketId(selectedBracketMatch[1]) : null;
|
|
1141
|
+
const bracketBoundary = bracketBoundaryActive
|
|
1142
|
+
? (heading) => isBracketMilestoneBoundary(heading.text, heading.level, selectedBracketId)
|
|
1143
|
+
: undefined;
|
|
1144
|
+
const sectionEnd = computeMilestoneSectionEnd(content, selected[0], sectionStart, bracketBoundary, currentMilestoneHeadings);
|
|
1145
|
+
return { sectionEnd, selectedBracketId, bracketBoundary };
|
|
1146
|
+
}
|
|
947
1147
|
/**
|
|
948
1148
|
* #5038: single owner for classifying a `milestone:` scalar as STATE.md spells
|
|
949
1149
|
* it. The frontmatter parser (FAILSAFE_SCHEMA) and the raw-regex readers both
|
|
@@ -964,6 +1164,28 @@ function classifyMilestoneScalar(raw) {
|
|
|
964
1164
|
return { explicitNull: true, version: null };
|
|
965
1165
|
return { explicitNull: false, version: trimmed };
|
|
966
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* #4998: STATE.md's `milestone:` scalar as the frontmatter parser DECODES it.
|
|
1169
|
+
* The raw `^milestone:` line read captured the on-disk YAML text — quotes and
|
|
1170
|
+
* escapes included — and that text was re-serialized as the value, adding one
|
|
1171
|
+
* escape layer per `state.*` write. Falls back to the legacy line read only
|
|
1172
|
+
* when there is no parseable frontmatter, or its `milestone` is not text. One
|
|
1173
|
+
* that parses without a `milestone` key names none: a `milestone:` line further
|
|
1174
|
+
* down (a fenced example) is not one (#4998 review, Minor 5).
|
|
1175
|
+
*/
|
|
1176
|
+
function readStateMilestoneScalar(stateRaw) {
|
|
1177
|
+
const fm = rawFrontmatterMapping(stateRaw);
|
|
1178
|
+
if (fm) {
|
|
1179
|
+
if (!Object.prototype.hasOwnProperty.call(fm, 'milestone'))
|
|
1180
|
+
return null;
|
|
1181
|
+
if (typeof fm['milestone'] === 'string')
|
|
1182
|
+
return fm['milestone'];
|
|
1183
|
+
// A non-string value keeps the pre-#4998 line read, which finds this
|
|
1184
|
+
// frontmatter line first.
|
|
1185
|
+
}
|
|
1186
|
+
const m = stateRaw.match(/^milestone:\s*(.+)/m);
|
|
1187
|
+
return m ? m[1] : null;
|
|
1188
|
+
}
|
|
967
1189
|
/**
|
|
968
1190
|
* Extract the current milestone section from ROADMAP.md by positive lookup,
|
|
969
1191
|
* carrying a `scope` discriminator (ADR-3180 Decision 2) alongside the value.
|
|
@@ -1003,9 +1225,9 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1003
1225
|
const statePath = node_path_1.default.join(planningDir(cwd, ws), 'STATE.md');
|
|
1004
1226
|
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
1005
1227
|
if (stateRaw !== null) {
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1008
|
-
const classified = classifyMilestoneScalar(
|
|
1228
|
+
const milestoneScalar = readStateMilestoneScalar(stateRaw);
|
|
1229
|
+
if (milestoneScalar !== null) {
|
|
1230
|
+
const classified = classifyMilestoneScalar(milestoneScalar);
|
|
1009
1231
|
version = classified.version;
|
|
1010
1232
|
stateAssertsNoMilestone = classified.explicitNull;
|
|
1011
1233
|
}
|
|
@@ -1056,26 +1278,8 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1056
1278
|
}
|
|
1057
1279
|
catch { /* unresolvable convention → preserve the legacy fallback */ }
|
|
1058
1280
|
}
|
|
1059
|
-
if (headingMatches.length === 0
|
|
1060
|
-
|
|
1061
|
-
const milestoneInt = vMatch ? parseInt(vMatch[1], 10) : NaN;
|
|
1062
|
-
if (Number.isSafeInteger(milestoneInt)) {
|
|
1063
|
-
// #2761 M3: the phase-id owner supplies both the bracket intro grammar
|
|
1064
|
-
// and canonical pad2 spelling; `[CODE.2]` therefore cannot bound a
|
|
1065
|
-
// section whose phase headings the bracket grammar rejects.
|
|
1066
|
-
// #612 round-5: HeadingToken.offset is the line start, so require the
|
|
1067
|
-
// `#` there to preserve the old line-start anchor's indentation parity.
|
|
1068
|
-
// For every survivor, rebuilding [fullLine, fullLine] plus `.index`
|
|
1069
|
-
// retains the match shape and first-match order expected downstream.
|
|
1070
|
-
const bracketMilestoneHeadingRe = new RegExp(`^${bracketMilestoneIntroSrcFor(milestoneInt)}`, 'i');
|
|
1071
|
-
headingMatches = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content)
|
|
1072
|
-
.filter((h) => h.level <= 3 && content[h.offset] === '#' && bracketMilestoneHeadingRe.test(h.text))
|
|
1073
|
-
.map((h) => {
|
|
1074
|
-
const lineEnd = content.indexOf('\n', h.offset);
|
|
1075
|
-
const fullLine = content.slice(h.offset, lineEnd === -1 ? content.length : lineEnd);
|
|
1076
|
-
return Object.assign([fullLine, fullLine], { index: h.offset });
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1281
|
+
if (headingMatches.length === 0) {
|
|
1282
|
+
headingMatches = bracketFallbackHeadingMatches(content, version, bracketScopeConvention);
|
|
1079
1283
|
}
|
|
1080
1284
|
if (headingMatches.length === 0) {
|
|
1081
1285
|
const summaryMatch = content.match(summaryPattern);
|
|
@@ -1130,29 +1334,14 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1130
1334
|
// #3184: selection collapses to the sole owner; `allMatches` is still needed
|
|
1131
1335
|
// below (offsets, detailsMatch search), so only the selection itself routes
|
|
1132
1336
|
// through `selectMilestoneHeading` rather than the whole block.
|
|
1133
|
-
|
|
1134
|
-
// bracket candidates above when a name-only bracket milestone carries no
|
|
1135
|
-
// version token for that selector to find.
|
|
1136
|
-
const selected = selectMilestoneHeading(content, version)
|
|
1137
|
-
?? allMatches.find((m) => !isClosed(m[1]))
|
|
1138
|
-
?? firstMatch;
|
|
1337
|
+
const selected = selectActiveMilestoneHeading(content, version, allMatches);
|
|
1139
1338
|
const sectionStart = selected.index;
|
|
1140
|
-
// #2761 B1: a bracket heading bearing the selected milestone's own id is
|
|
1141
|
-
// a continuation, not a boundary. Derive the selected id once and compose
|
|
1142
|
-
// the shared discriminator with upstream's centralized section-end walk.
|
|
1143
1339
|
const bracketBoundaryActive = bracketScopeConvention === 'bracket';
|
|
1144
|
-
const selectedBracketMatch = bracketBoundaryActive
|
|
1145
|
-
? selected[0].match(new RegExp(`^#{1,3}\\s+\\[(${BRACKET_ID_SRC})\\]`, 'i'))
|
|
1146
|
-
: null;
|
|
1147
|
-
const selectedBracketId = selectedBracketMatch ? foldBracketId(selectedBracketMatch[1]) : null;
|
|
1148
1340
|
// #2761 B3: tokenize once for both the centralized section-end owner and
|
|
1149
1341
|
// the bracket preamble scan below. This keeps both boundary decisions
|
|
1150
1342
|
// fence-aware without restoring the local section walker retired by #3184.
|
|
1151
1343
|
const currentMilestoneHeadings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
1152
|
-
const bracketBoundary = bracketBoundaryActive
|
|
1153
|
-
? (heading) => isBracketMilestoneBoundary(heading.text, heading.level, selectedBracketId)
|
|
1154
|
-
: undefined;
|
|
1155
|
-
const sectionEnd = computeMilestoneSectionEnd(content, selected[0], sectionStart, bracketBoundary, currentMilestoneHeadings);
|
|
1344
|
+
const { sectionEnd, selectedBracketId, bracketBoundary } = bracketAwareMilestoneSection(content, selected, sectionStart, bracketBoundaryActive, currentMilestoneHeadings);
|
|
1156
1345
|
const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧)/im;
|
|
1157
1346
|
let earliestMilestoneIndex;
|
|
1158
1347
|
if (!bracketBoundaryActive) {
|
|
@@ -1375,18 +1564,261 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1375
1564
|
function extractCurrentMilestone(content, cwd, ws) {
|
|
1376
1565
|
return extractCurrentMilestoneScoped(content, cwd, ws).value;
|
|
1377
1566
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1567
|
+
/**
|
|
1568
|
+
* Same-length copy of `s` with HTML comments blanked. An unterminated `<!--`, or
|
|
1569
|
+
* one whose `-->` lies beyond the next heading, is bounded at the end of its own
|
|
1570
|
+
* line so a prose mention never hides later archives. Linear: `-->` and heading
|
|
1571
|
+
* lookups are cached while they stay ahead of the cursor.
|
|
1572
|
+
*/
|
|
1573
|
+
function blankComments(s) {
|
|
1574
|
+
const heading = /\n#{1,6} /g;
|
|
1575
|
+
let out = '';
|
|
1576
|
+
let i = 0;
|
|
1577
|
+
let close = 0;
|
|
1578
|
+
let head = -2; // -2: heading not looked up yet (-1 means none ahead); offset 0 must still trigger the first lookup
|
|
1579
|
+
for (let o = s.indexOf('<!--'); o !== -1; o = s.indexOf('<!--', i)) {
|
|
1580
|
+
if (close !== -1 && close < o + 4)
|
|
1581
|
+
close = s.indexOf('-->', o + 4);
|
|
1582
|
+
if (head !== -1 && head < o) {
|
|
1583
|
+
heading.lastIndex = o;
|
|
1584
|
+
const m = heading.exec(s);
|
|
1585
|
+
head = m ? m.index : -1;
|
|
1586
|
+
}
|
|
1587
|
+
let end;
|
|
1588
|
+
if (close !== -1 && (head === -1 || close < head))
|
|
1589
|
+
end = close + 3;
|
|
1590
|
+
else {
|
|
1591
|
+
end = s.indexOf('\n', o);
|
|
1592
|
+
if (end === -1)
|
|
1593
|
+
end = s.length;
|
|
1594
|
+
}
|
|
1595
|
+
out += s.slice(i, o) + s.slice(o, end).replace(/[^\r\n]/g, ' ');
|
|
1596
|
+
i = end;
|
|
1597
|
+
}
|
|
1598
|
+
return out + s.slice(i);
|
|
1599
|
+
}
|
|
1600
|
+
const VERSION_TOKEN = /v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i;
|
|
1601
|
+
/** Same-length copy of `content` with fenced blocks, inline code spans and HTML comments blanked. */
|
|
1602
|
+
function blankCodeAndComments(content) {
|
|
1603
|
+
// Same-length copy with fenced blocks, inline code spans and HTML comments
|
|
1604
|
+
// blanked (code first: a backticked `<!--` must not open a comment), so a `<details>` mentioned in prose or an example never opens or
|
|
1605
|
+
// closes a block.
|
|
1606
|
+
const lines = content.split('\n');
|
|
1607
|
+
const blank = (t) => t.replace(/[^\r\n]/g, ' ');
|
|
1608
|
+
for (const f of (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines)) {
|
|
1609
|
+
// An unclosed fence is bounded at the next heading, not EOF, so later archives stay visible.
|
|
1610
|
+
let last = f.closeLineIdx;
|
|
1611
|
+
if (last === -1) {
|
|
1612
|
+
last = lines.length - 1;
|
|
1613
|
+
for (let i = f.openLineIdx + 1; i < lines.length; i++)
|
|
1614
|
+
if (/^#{1,6} /.test(lines[i])) {
|
|
1615
|
+
last = i - 1;
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
for (let i = f.openLineIdx; i <= last; i++)
|
|
1620
|
+
lines[i] = blank(lines[i]);
|
|
1621
|
+
}
|
|
1622
|
+
const joined = lines.join('\n');
|
|
1623
|
+
let codeBlanked = '';
|
|
1624
|
+
let at = 0;
|
|
1625
|
+
for (const sp of (0, markdown_sectionizer_cjs_1.scanInlineCodeSpans)(joined)) {
|
|
1626
|
+
codeBlanked += joined.slice(at, sp.start) + blank(joined.slice(sp.start, sp.end));
|
|
1627
|
+
at = sp.end;
|
|
1385
1628
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1629
|
+
return blankComments(codeBlanked + joined.slice(at));
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* A closed milestone written as a plain heading section (no `<details>`) is archive too; the
|
|
1633
|
+
* active milestone's own `## v2.0 Part 2 ✅` sections are not.
|
|
1634
|
+
*/
|
|
1635
|
+
function plainClosedSections(content, overlapsActive, activeVersion) {
|
|
1636
|
+
// A closed milestone written as a plain heading section (no `<details>`) is
|
|
1637
|
+
// archive too. It ends at the next heading of the same or shallower level, so
|
|
1638
|
+
// a following `## Progress` table stays visible.
|
|
1639
|
+
const headingTokens = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
1640
|
+
const spans = [];
|
|
1641
|
+
for (let i = 0; i < headingTokens.length; i++) {
|
|
1642
|
+
const h = headingTokens[i];
|
|
1643
|
+
if (/^Phase\s+\S/i.test(h.text))
|
|
1644
|
+
continue;
|
|
1645
|
+
if (!isClosedMilestoneHeading(h.text) || extractMilestoneHeadingName(h.text) === null)
|
|
1646
|
+
continue;
|
|
1647
|
+
let next;
|
|
1648
|
+
for (let j = i + 1; j < headingTokens.length && !next; j++) {
|
|
1649
|
+
const t = headingTokens[j];
|
|
1650
|
+
if (t.level <= h.level && !/^Phase\s+\S/i.test(t.text))
|
|
1651
|
+
next = t;
|
|
1652
|
+
}
|
|
1653
|
+
// End before the newline that precedes `next`, so that heading stays at line start.
|
|
1654
|
+
const end = next ? next.offset - (content[next.offset - 2] === '\r' ? 2 : 1) : content.length;
|
|
1655
|
+
if (!overlapsActive(h.offset, end) && !(activeVersion && h.text.match(VERSION_TOKEN)?.[0].toLowerCase() === activeVersion))
|
|
1656
|
+
spans.push({ start: h.offset, end });
|
|
1657
|
+
}
|
|
1658
|
+
return spans;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* #5553: locate every archive span in `content` (`<details>` blocks and plain closed-milestone
|
|
1662
|
+
* sections) that does not wrap the active milestone; `inActive` is the subset overlapping it.
|
|
1663
|
+
*/
|
|
1664
|
+
function scanArchiveSpans(content, active) {
|
|
1665
|
+
const activeVersion = active.length > 0 ? content.slice(active[0].start, active[0].start + 300).split('\n', 1)[0].match(VERSION_TOKEN)?.[0].toLowerCase() : undefined;
|
|
1666
|
+
// A block whose own <summary> marks a closed milestone is an archive wherever it sits.
|
|
1667
|
+
const closedSummary = (at) => {
|
|
1668
|
+
const m = /^<details\b[^>]{0,200}>[\s>]*<summary[^>]{0,200}>([^<]{0,300})<\/summary>/i.exec(content.slice(at, at + 800));
|
|
1669
|
+
return m !== null && isClosedMilestoneHeading(m[1]);
|
|
1670
|
+
};
|
|
1671
|
+
const scan = blankCodeAndComments(content);
|
|
1672
|
+
// Depth-aware: an outer block closes at its own `</details>`, so a nested
|
|
1673
|
+
// collapsed section inside an archive is masked with it.
|
|
1674
|
+
const spans = [];
|
|
1675
|
+
const inActive = [];
|
|
1676
|
+
const overlapsActive = (start, end) => active.some((r) => start < r.end && end > r.start);
|
|
1677
|
+
// Block-level tags only: a tag at line start (<=3 spaces, optionally inside a
|
|
1678
|
+
// blockquote `>` prefix) starts the scan of its line, and further tags on that
|
|
1679
|
+
// same line (a one-line `<details>...</details>`) count with it. Inline or
|
|
1680
|
+
// indented mentions elsewhere never count, and the bounded attribute run keeps
|
|
1681
|
+
// a hostile run of `<details ` linear.
|
|
1682
|
+
const lineStart = /^[ \t]{0,3}(?:>[ \t]{0,3})*(?=<\/?details\b)/gim;
|
|
1683
|
+
const anyTag = /<details\b[^>]{0,200}>|<\/details[ \t]*>/gi;
|
|
1684
|
+
const tags = [];
|
|
1685
|
+
for (let l = lineStart.exec(scan); l; l = lineStart.exec(scan)) {
|
|
1686
|
+
const from = l.index + l[0].length;
|
|
1687
|
+
const nl = scan.indexOf('\n', from);
|
|
1688
|
+
const lineEnd = nl === -1 ? scan.length : nl;
|
|
1689
|
+
anyTag.lastIndex = from;
|
|
1690
|
+
for (let t = anyTag.exec(scan); t && t.index < lineEnd; t = anyTag.exec(scan)) {
|
|
1691
|
+
tags.push({ at: t.index, len: t[0].length, closer: t[0][1] === '/' });
|
|
1692
|
+
}
|
|
1693
|
+
lineStart.lastIndex = Math.max(lineStart.lastIndex, lineEnd);
|
|
1694
|
+
}
|
|
1695
|
+
// Stack pairing: every closer matches its nearest opener, so each closed pair is
|
|
1696
|
+
// evaluated on its own (a nested archive inside an active wrapper is masked) and
|
|
1697
|
+
// an unmatched opener or closer is ignored instead of disabling later masking.
|
|
1698
|
+
const openers = [];
|
|
1699
|
+
const kept = [];
|
|
1700
|
+
let paired = false;
|
|
1701
|
+
let strayCloser = false;
|
|
1702
|
+
for (const tag of tags) {
|
|
1703
|
+
if (!tag.closer) {
|
|
1704
|
+
openers.push(tag.at);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
const start = openers.pop();
|
|
1708
|
+
if (start === undefined) {
|
|
1709
|
+
// A closer with nothing to pair after an earlier pair closed: that pair may have swallowed this block's own opener.
|
|
1710
|
+
if (paired)
|
|
1711
|
+
strayCloser = true;
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
paired = true;
|
|
1715
|
+
const end = tag.at + tag.len;
|
|
1716
|
+
// Only a block wrapping an active heading stays editable; any other block is archive, with or without a closed <summary>.
|
|
1717
|
+
if (active.some((r) => start <= r.start && r.start < end))
|
|
1718
|
+
continue;
|
|
1719
|
+
const inRange = overlapsActive(start, end) && !closedSummary(start);
|
|
1720
|
+
// A collapsed block of the active range that holds the Progress table stays editable, so the row moves with the plan ticks.
|
|
1721
|
+
// A level-1 `# Progress` has always counted here (#5692: no behavior change), hence `levels`.
|
|
1722
|
+
if (inRange && (0, roadmap_progress_cjs_1.findProgressSection)(scan.slice(start, end), { levels: [1, 2] }) !== null) {
|
|
1723
|
+
kept.push({ start, end });
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
spans.push({ start, end });
|
|
1727
|
+
if (inRange)
|
|
1728
|
+
inActive.push({ start, end });
|
|
1729
|
+
}
|
|
1730
|
+
// An opener left unpaired makes the structure ambiguous when it either opens a
|
|
1731
|
+
// closed-summary archive that does not close on its own line (its extent is unknown) or precedes a masked
|
|
1732
|
+
// block in the active range (it may be that block's real wrapper, with the
|
|
1733
|
+
// stray tag inside). A harmless stray opener is ignored.
|
|
1734
|
+
const eol = (o) => { const e = content.indexOf('\n', o); return e === -1 ? content.length : e; };
|
|
1735
|
+
const ambiguous = strayCloser || openers.some((o) => closedSummary(o) && !/<\/details/i.test(content.slice(o, eol(o)))) || (openers.length > 0 && [...inActive, ...kept].some((r) => r.start > openers[0]));
|
|
1736
|
+
spans.push(...plainClosedSections(content, overlapsActive, activeVersion));
|
|
1737
|
+
return { spans, inActive, ambiguous };
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* #5553: hide every archived `<details>` block, and every closed milestone
|
|
1741
|
+
* written as a plain heading section, behind a placeholder; `restore` puts them
|
|
1742
|
+
* back unchanged. Tag case, attributes and archive position
|
|
1743
|
+
* (before OR after the active milestone) do not matter, unlike a "region after
|
|
1744
|
+
* the last `</details>`" cut. A block stays visible only when it wraps the
|
|
1745
|
+
* start of a resolved active milestone range (`<details open>` around the
|
|
1746
|
+
* active milestone); every other block is hidden, whatever its `<summary>`
|
|
1747
|
+
* (none, non-closed, closed), including a collapsed block inside the active
|
|
1748
|
+
* range. That is deliberate for THIS write path only; the read-side
|
|
1749
|
+
* `stripClosedMilestoneDetails` window (#3982/#1341) is unchanged. With no
|
|
1750
|
+
* resolvable active milestone every block is hidden, as the old
|
|
1751
|
+
* last-`</details>` cut did. `ambiguous` flags unpaired tags that hide where an
|
|
1752
|
+
* archive ends (the caller declines); `activeRange` is the active milestone's
|
|
1753
|
+
* range in `visible` coordinates.
|
|
1754
|
+
*/
|
|
1755
|
+
function maskArchivedDetails(content, cwd) {
|
|
1756
|
+
if (!/<details\b/i.test(content) && !MILESTONE_CLOSED_MARKER_PATTERN.test(content)) {
|
|
1757
|
+
return { visible: content, restore: (edited) => edited, hiddenInActive: '', ambiguous: false, activeRange: null };
|
|
1758
|
+
}
|
|
1759
|
+
const resolved = currentMilestoneRawRanges(content, cwd);
|
|
1760
|
+
// A stale STATE can name a milestone whose own heading is closed (all headings closed): nothing is active, so everything shipped stays masked.
|
|
1761
|
+
const ranges = resolved && !isClosedMilestoneHeading(content.slice(resolved.primary.start).split('\n', 1)[0]) ? resolved : null;
|
|
1762
|
+
const active = ranges ? [ranges.primary, ...(ranges.details ? [ranges.details] : [])] : [];
|
|
1763
|
+
const { spans, inActive, ambiguous } = scanArchiveSpans(content, active);
|
|
1764
|
+
const blocks = [];
|
|
1765
|
+
// Unguessable per call: a plan filename or any other edited-in text cannot forge a marker.
|
|
1766
|
+
const nonce = (0, node_crypto_1.randomBytes)(8).toString('hex');
|
|
1767
|
+
const marker = (i) => `<!-- gsd-archived-details-${nonce}-${i} -->`;
|
|
1768
|
+
spans.sort((x, y) => x.start - y.start || y.end - x.end);
|
|
1769
|
+
let visible = '';
|
|
1770
|
+
let last = 0;
|
|
1771
|
+
const hidden = [];
|
|
1772
|
+
for (const span of spans) {
|
|
1773
|
+
if (span.start < last) {
|
|
1774
|
+
// Nested in an already-masked span: only a partial overlap extends it.
|
|
1775
|
+
if (span.end <= last)
|
|
1776
|
+
continue;
|
|
1777
|
+
span.start = last;
|
|
1778
|
+
}
|
|
1779
|
+
blocks.push(content.slice(span.start, span.end));
|
|
1780
|
+
visible += content.slice(last, span.start);
|
|
1781
|
+
const m = marker(blocks.length - 1);
|
|
1782
|
+
hidden.push({ start: span.start, end: span.end, vis: visible.length, len: m.length });
|
|
1783
|
+
visible += m;
|
|
1784
|
+
last = span.end;
|
|
1785
|
+
}
|
|
1786
|
+
visible += content.slice(last);
|
|
1787
|
+
// One pass, so a hostile ROADMAP with thousands of blocks stays linear.
|
|
1788
|
+
const markerRe = new RegExp(`<!-- gsd-archived-details-${nonce}-(\\d+) -->`, 'g');
|
|
1789
|
+
const restore = (edited) => {
|
|
1790
|
+
const seen = new Array(blocks.length).fill(0);
|
|
1791
|
+
let bad = false;
|
|
1792
|
+
const restored = edited.replace(markerRe, (m, i) => {
|
|
1793
|
+
const n = Number(i);
|
|
1794
|
+
if (n >= blocks.length || seen[n]++ > 0) {
|
|
1795
|
+
bad = true;
|
|
1796
|
+
return m;
|
|
1797
|
+
}
|
|
1798
|
+
return blocks[n];
|
|
1799
|
+
});
|
|
1800
|
+
// edit dropped or duplicated a placeholder: caller must refuse rather than lose or misplace an archive
|
|
1801
|
+
return bad || seen.includes(0) ? null : restored;
|
|
1802
|
+
};
|
|
1803
|
+
// Blocks hidden inside the active range stay unticked, but their rows still count as present.
|
|
1804
|
+
const hiddenInActive = inActive.map((r) => content.slice(r.start, r.end)).join('\n');
|
|
1805
|
+
// Original offset -> offset in `visible`.
|
|
1806
|
+
const toVisible = (p) => {
|
|
1807
|
+
let h;
|
|
1808
|
+
for (const x of hidden) {
|
|
1809
|
+
if (x.start < p)
|
|
1810
|
+
h = x;
|
|
1811
|
+
else
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
if (!h)
|
|
1815
|
+
return p;
|
|
1816
|
+
return p >= h.end ? h.vis + h.len + (p - h.end) : h.vis;
|
|
1817
|
+
};
|
|
1818
|
+
const activeRange = active.length > 0
|
|
1819
|
+
? { start: toVisible(Math.min(...active.map((r) => r.start))), end: toVisible(Math.max(...active.map((r) => r.end))) }
|
|
1820
|
+
: null;
|
|
1821
|
+
return { visible, restore, hiddenInActive, ambiguous, activeRange };
|
|
1390
1822
|
}
|
|
1391
1823
|
/**
|
|
1392
1824
|
* Resolve a single phase's detail-section heading (`### Phase N: …`, any level
|
|
@@ -1420,11 +1852,21 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
|
1420
1852
|
* phase-heading level, so a level-bounded stop could fold a deeper sibling
|
|
1421
1853
|
* heading (e.g. a `####` phase following a `###` phase) into this phase's
|
|
1422
1854
|
* body and let `edit` reach into it.
|
|
1855
|
+
*
|
|
1856
|
+
* `levelBounded: true` instead keeps the phase's own non-phase `####` sub-sections
|
|
1857
|
+
* (a plan list under a sub-heading) in the body, still ending at the next heading
|
|
1858
|
+
* that opens another phase, of any level. Serves the presence check and the
|
|
1859
|
+
* plan-row tick writer.
|
|
1423
1860
|
*/
|
|
1424
|
-
function withPhaseSection(content, phaseId, edit) {
|
|
1861
|
+
function withPhaseSection(content, phaseId, edit, levelBounded = false) {
|
|
1425
1862
|
const src = phaseMarkdownRegexSource(phaseId);
|
|
1426
1863
|
const headingRe = new RegExp(`^\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${src}(?=[\\s:(]|$)`, 'i');
|
|
1427
|
-
|
|
1864
|
+
// levelBounded runs to the next same-or-higher heading, so cut at a deeper phase heading too.
|
|
1865
|
+
const ownBody = !levelBounded ? edit : (body) => {
|
|
1866
|
+
const cut = body.search(/^#{1,6}[ \t]+(?:\[[^\]\n]{1,200}\][ \t]*)?Phase\s+\S/m);
|
|
1867
|
+
return cut === -1 ? edit(body) : edit(body.slice(0, cut)) + body.slice(cut);
|
|
1868
|
+
};
|
|
1869
|
+
return (0, markdown_sectionizer_cjs_1.withSection)(content, (h) => headingRe.test(h.text), ownBody, { levelBounded });
|
|
1428
1870
|
}
|
|
1429
1871
|
// ─── Roadmap phase lookup ─────────────────────────────────────────────────────
|
|
1430
1872
|
// #2199: a bullet/checkbox phase entry, e.g. `- [ ] **Phase 36 — Authentication**`
|
|
@@ -1432,11 +1874,11 @@ function withPhaseSection(content, phaseId, edit) {
|
|
|
1432
1874
|
// is captured in group 1, the name in group 2; the separator may be an em-dash,
|
|
1433
1875
|
// en-dash, hyphen, or colon. Used as a fallback when no ATX heading matches, and
|
|
1434
1876
|
// to count phases in a milestone that uses the bullet form.
|
|
1435
|
-
const BULLET_PHASE_LINE_PATTERN =
|
|
1877
|
+
const BULLET_PHASE_LINE_PATTERN = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+([\\w][\\w.-]*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*(.+?)${phase_entry_grammar_cjs_1.PHASE_ENTRY_BOLD_CLOSE_SRC}`, 'im', { box: 'optional', bold: 'required' });
|
|
1436
1878
|
/** Build a bullet-phase-line regex pinned to a specific phase number (#2199). */
|
|
1437
1879
|
function bulletPhaseLineFor(phaseNum, phaseSource) {
|
|
1438
1880
|
const num = phaseSource ?? phaseMarkdownRegexSource(phaseNum);
|
|
1439
|
-
return
|
|
1881
|
+
return (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase\\s+(${num})${OPTIONAL_PHASE_TAG_SOURCE}\\s*[—–:\\-]\\s*(.+?)${phase_entry_grammar_cjs_1.PHASE_ENTRY_BOLD_CLOSE_SRC}`, 'im', { box: 'optional', bold: 'required' });
|
|
1440
1882
|
}
|
|
1441
1883
|
function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
1442
1884
|
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
@@ -1577,39 +2019,6 @@ function reportUnreadableRoadmap(err, roadmapPath) {
|
|
|
1577
2019
|
return;
|
|
1578
2020
|
warnUnusableInput({ reason: UNUSABLE_REASON.ROADMAP_UNREADABLE, source: roadmapPath });
|
|
1579
2021
|
}
|
|
1580
|
-
// ─── Roadmap progress table (#1956/#2012 decoy avoidance) ─────────────────────
|
|
1581
|
-
/**
|
|
1582
|
-
* Locate ROADMAP.md's "Progress" table — the sole owner of the #2012
|
|
1583
|
-
* decoy-avoidance scope for the `drift-guard phase-status` CLI seam (#1956).
|
|
1584
|
-
*
|
|
1585
|
-
* Scopes to the `## Progress` heading first (level-2, exact case-insensitive
|
|
1586
|
-
* text `'progress'`, `{ levelBounded: true }`) via `collectSection` — the
|
|
1587
|
-
* same CRLF-safe seam `stateCurrentPositionSlice` (state-document.cts) uses
|
|
1588
|
-
* to scope STATE.md's `## Current Position` — so a differently-headed table
|
|
1589
|
-
* that happens to share the same column names (e.g. an "Archive Notes"
|
|
1590
|
-
* table) is never picked up instead of the real one (#2012). Falls back to
|
|
1591
|
-
* scanning the WHOLE document when no `## Progress` heading exists, so a
|
|
1592
|
-
* headingless milestone slice (#1445) still resolves rather than going
|
|
1593
|
-
* uncheckable — the same fallback `deriveProgressFromRoadmap`
|
|
1594
|
-
* (phase-lifecycle.cts) deliberately preserves.
|
|
1595
|
-
*
|
|
1596
|
-
* `deriveProgressFromRoadmap` independently expresses this same "scope to
|
|
1597
|
-
* `## Progress`, else whole document" rule via its own regex-based scope
|
|
1598
|
-
* (kept there deliberately rather than refactored onto this function — its
|
|
1599
|
-
* blast radius is large). The two locators are therefore separate
|
|
1600
|
-
* implementations of the same scoping rule and must agree about WHICH table
|
|
1601
|
-
* is the Progress table; a parity test in
|
|
1602
|
-
* tests/adr-22-plan-drift-guard.test.cjs asserts they do, per the repo's
|
|
1603
|
-
* generative-fix-divergence guard.
|
|
1604
|
-
*
|
|
1605
|
-
* Returns the same shape `findTableWithColumns` returns (or `null`).
|
|
1606
|
-
*/
|
|
1607
|
-
function findRoadmapProgressTable(roadmapContent) {
|
|
1608
|
-
const isProgressHeading = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'progress';
|
|
1609
|
-
const section = (0, markdown_sectionizer_cjs_1.collectSection)(roadmapContent, isProgressHeading, { levelBounded: true });
|
|
1610
|
-
const scoped = section ? section.body : roadmapContent;
|
|
1611
|
-
return (0, markdown_table_cjs_1.findTableWithColumns)(scoped, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
|
1612
|
-
}
|
|
1613
2022
|
/**
|
|
1614
2023
|
* Strip a leading delimiter run (whitespace, em/en-dash, colon, hyphen) from a
|
|
1615
2024
|
* milestone-name capture. Markdown headings commonly take the shape
|
|
@@ -1766,9 +2175,9 @@ function getMilestoneInfo(cwd) {
|
|
|
1766
2175
|
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1767
2176
|
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
1768
2177
|
if (stateRaw !== null) {
|
|
1769
|
-
const
|
|
1770
|
-
if (
|
|
1771
|
-
const classified = classifyMilestoneScalar(
|
|
2178
|
+
const milestoneScalar = readStateMilestoneScalar(stateRaw);
|
|
2179
|
+
if (milestoneScalar !== null) {
|
|
2180
|
+
const classified = classifyMilestoneScalar(milestoneScalar);
|
|
1772
2181
|
explicitlyNoMilestone = classified.explicitNull;
|
|
1773
2182
|
stateVersion = classified.version;
|
|
1774
2183
|
}
|
|
@@ -1902,7 +2311,7 @@ function getMilestoneInfo(cwd) {
|
|
|
1902
2311
|
* resolution exactly, including its `GSD_WORKSTREAM` env fallback — every
|
|
1903
2312
|
* pre-#2562 call site is unaffected.
|
|
1904
2313
|
*/
|
|
1905
|
-
function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
|
|
2314
|
+
function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws, context) {
|
|
1906
2315
|
const milestonePhaseNums = new Set();
|
|
1907
2316
|
// #612: the milestone-QUALIFIED form (`{CODE}.{MM}-{PP}`) of each in-scope
|
|
1908
2317
|
// bracket heading, kept in its OWN set — deliberately not in
|
|
@@ -1999,7 +2408,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
|
|
|
1999
2408
|
// #3262 remains the single owner of the phase-set scan. #612 extends its
|
|
2000
2409
|
// return with qualified bracket ids rather than restoring the superseded
|
|
2001
2410
|
// inline duplicate this commit was originally written against.
|
|
2002
|
-
const scanned = scanMilestonePhaseIdSets(roadmap, headingConvention);
|
|
2411
|
+
const scanned = scanMilestonePhaseIdSets(roadmap, headingConvention, context);
|
|
2003
2412
|
for (const id of scanned.ids)
|
|
2004
2413
|
milestonePhaseNums.add(id);
|
|
2005
2414
|
for (const qualified of scanned.qualifiedIds)
|
|
@@ -2053,7 +2462,11 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
|
|
|
2053
2462
|
? new RegExp(`^0*(\\d+[A-Za-z]?(?:-${phaseIdModule.PHASE_CONTINUATION_SEGMENT_SOURCE}[A-Z]?)*(?:\\.\\d+)*)(?=-|$)`)
|
|
2054
2463
|
// phase-id-owner: the [A-Za-z] letter class does real case handling here — this regex carries NO /i flag; kept literal, not source-byte-equal to the canonical PHASE_NUMBER_TOKEN_SOURCE.
|
|
2055
2464
|
: /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
|
|
2465
|
+
const customKeys = context?.phaseNaming === 'custom' && headingConvention !== 'bracket'
|
|
2466
|
+
? new Set([...milestonePhaseNums].map(id => phaseIdModule.phaseKeyFromToken(id, context))) : null;
|
|
2056
2467
|
function isDirInMilestone(dirName) {
|
|
2468
|
+
if (customKeys)
|
|
2469
|
+
return customKeys.has(phaseIdModule.phaseKeyFromDir(dirName, headingConvention, context));
|
|
2057
2470
|
// #612: the DIR side of this filter, selected by the same convention the
|
|
2058
2471
|
// heading side selected. Without it the heading scan's new bracket reach was
|
|
2059
2472
|
// half a fix: `milestonePhaseNums` became non-empty, so the pass-all degrade
|
|
@@ -2142,27 +2555,31 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
|
|
|
2142
2555
|
* Details section) — both consume the same `locateMilestoneHeadings` /
|
|
2143
2556
|
* `computeMilestoneSectionEnd` owner (#3184), so there is no separate copy to
|
|
2144
2557
|
* keep in sync. Returns null when there is no versioned active milestone;
|
|
2145
|
-
* callers then fall back to
|
|
2558
|
+
* callers then fall back to the shipped-block-masked whole content
|
|
2559
|
+
* (`editOutsideShippedBlocks`) via `editCurrentMilestone`, or to unscoped
|
|
2560
|
+
* whole-content handling where they call this directly (`phase.cts`
|
|
2561
|
+
* `phaseEntryInsertOffset`).
|
|
2146
2562
|
*
|
|
2147
|
-
* #2761
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
2152
|
-
*
|
|
2153
|
-
*
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2159
|
-
*
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2563
|
+
* #2761 / #4304: bracket-widened
|
|
2564
|
+
* in lockstep with the write path landing, per this doc's own prior note.
|
|
2565
|
+
* `phaseIdConvention` is the same additive, optional trailing parameter
|
|
2566
|
+
* `extractCurrentMilestoneScoped` accepts — omitted (every pre-#4304 2-arg
|
|
2567
|
+
* call site), it compiles byte-identically to before: `bracketFallbackHeadingMatches`
|
|
2568
|
+
* and `bracketAwareMilestoneSection`'s bracket branch are both no-ops unless
|
|
2569
|
+
* explicitly told `'bracket'`, so a version-string match still short-circuits
|
|
2570
|
+
* both, and a non-bracket/unresolved convention still returns null exactly as
|
|
2571
|
+
* it always has. Now shares its heading-selection and section-end grammar
|
|
2572
|
+
* with `extractCurrentMilestoneScoped` (`bracketFallbackHeadingMatches`,
|
|
2573
|
+
* `selectActiveMilestoneHeading`, `bracketAwareMilestoneSection`) instead of
|
|
2574
|
+
* carrying its own unwidened copy, so a version-less bracket milestone
|
|
2575
|
+
* heading (`## [GSD.09] Hidden`) bounds a section for this function's callers
|
|
2576
|
+
* too, not only for the read path.
|
|
2577
|
+
* Consumers: `editCurrentMilestone` (`phase complete`'s `mutateMilestonePhase`,
|
|
2578
|
+
* `roadmap update-plan-progress`) and `phase.cts` `phaseEntryInsertOffset`. On the null
|
|
2579
|
+
* path `editCurrentMilestone` edits the ROADMAP with closed-milestone `<details>`
|
|
2580
|
+
* blocks masked, so no writer reaches into a shipped block.
|
|
2164
2581
|
*/
|
|
2165
|
-
function currentMilestoneRawRanges(content, cwd) {
|
|
2582
|
+
function currentMilestoneRawRanges(content, cwd, phaseIdConvention) {
|
|
2166
2583
|
if (!cwd)
|
|
2167
2584
|
return null;
|
|
2168
2585
|
let version = null;
|
|
@@ -2171,9 +2588,9 @@ function currentMilestoneRawRanges(content, cwd) {
|
|
|
2171
2588
|
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
2172
2589
|
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
2173
2590
|
if (stateRaw !== null) {
|
|
2174
|
-
const
|
|
2175
|
-
if (
|
|
2176
|
-
const classified = classifyMilestoneScalar(
|
|
2591
|
+
const milestoneScalar = readStateMilestoneScalar(stateRaw);
|
|
2592
|
+
if (milestoneScalar !== null) {
|
|
2593
|
+
const classified = classifyMilestoneScalar(milestoneScalar);
|
|
2177
2594
|
version = classified.version;
|
|
2178
2595
|
stateAssertsNoMilestone = classified.explicitNull;
|
|
2179
2596
|
}
|
|
@@ -2187,15 +2604,21 @@ function currentMilestoneRawRanges(content, cwd) {
|
|
|
2187
2604
|
}
|
|
2188
2605
|
if (!version)
|
|
2189
2606
|
return null;
|
|
2190
|
-
const
|
|
2607
|
+
const bracketScopeConvention = phaseIdConvention ?? null;
|
|
2608
|
+
let headingMatches = locateMilestoneHeadings(content, version);
|
|
2609
|
+
if (headingMatches.length === 0) {
|
|
2610
|
+
headingMatches = bracketFallbackHeadingMatches(content, version, bracketScopeConvention);
|
|
2611
|
+
}
|
|
2191
2612
|
if (headingMatches.length === 0)
|
|
2192
2613
|
return null;
|
|
2193
2614
|
const isClosed = isClosedMilestoneHeading;
|
|
2194
2615
|
// #3184: selection collapses to the sole owner; `headingMatches` is still
|
|
2195
2616
|
// needed below for the detailsMatch search over all headings.
|
|
2196
|
-
const selected =
|
|
2617
|
+
const selected = selectActiveMilestoneHeading(content, version, headingMatches);
|
|
2197
2618
|
const sectionStart = selected.index ?? 0;
|
|
2198
|
-
const
|
|
2619
|
+
const bracketBoundaryActive = bracketScopeConvention === 'bracket';
|
|
2620
|
+
const currentMilestoneHeadings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
2621
|
+
const { sectionEnd, bracketBoundary } = bracketAwareMilestoneSection(content, selected, sectionStart, bracketBoundaryActive, currentMilestoneHeadings);
|
|
2199
2622
|
const selectedVersionToken = selected[1].match(/v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i)?.[0];
|
|
2200
2623
|
const detailsVersionBoundary = selectedVersionToken
|
|
2201
2624
|
? new RegExp(`${(0, pattern_cjs_1.escapeRegex)(selectedVersionToken)}(?![\\w.-])`, 'i')
|
|
@@ -2207,10 +2630,160 @@ function currentMilestoneRawRanges(content, cwd) {
|
|
|
2207
2630
|
let details = null;
|
|
2208
2631
|
if (detailsMatch) {
|
|
2209
2632
|
const detailsStart = detailsMatch.index ?? 0;
|
|
2210
|
-
|
|
2633
|
+
// #4304: pass the SAME bracketBoundary (and heading tokens)
|
|
2634
|
+
// bracketAwareMilestoneSection already derived for the primary range —
|
|
2635
|
+
// extractCurrentMilestoneScoped's own details lookup (line ~1312) has
|
|
2636
|
+
// always done this; this copy omitted both trailing arguments, so a
|
|
2637
|
+
// version-less bracket milestone heading (no v\d+.\d+/emoji marker) never
|
|
2638
|
+
// stopped this details window at the NEXT milestone's own "(Phase
|
|
2639
|
+
// Details)" section, and the two copies silently disagreed on the active
|
|
2640
|
+
// details window.
|
|
2641
|
+
details = {
|
|
2642
|
+
start: detailsStart,
|
|
2643
|
+
end: computeMilestoneSectionEnd(content, detailsMatch[0], detailsStart, bracketBoundary, currentMilestoneHeadings),
|
|
2644
|
+
};
|
|
2211
2645
|
}
|
|
2212
2646
|
return { primary: { start: sectionStart, end: sectionEnd }, details };
|
|
2213
2647
|
}
|
|
2648
|
+
/** `<details>` / `</details>` tags of `content` in document order (bare opener only: `<details open>` wraps the active milestone, it is not an archive). */
|
|
2649
|
+
function bareDetailsTags(content) {
|
|
2650
|
+
return [...content.matchAll(/<(\/?)details>/gi)].map((m) => ({ at: m.index, close: m[1] === '/' }));
|
|
2651
|
+
}
|
|
2652
|
+
/** Whether offset `idx` sits inside a bare `<details>` block that has not closed yet (`tags` from `bareDetailsTags`). */
|
|
2653
|
+
function insideBareDetails(tags, idx) {
|
|
2654
|
+
let lo = 0;
|
|
2655
|
+
let hi = tags.length;
|
|
2656
|
+
while (lo < hi) {
|
|
2657
|
+
const mid = (lo + hi) >> 1;
|
|
2658
|
+
if (tags[mid].at < idx)
|
|
2659
|
+
lo = mid + 1;
|
|
2660
|
+
else
|
|
2661
|
+
hi = mid;
|
|
2662
|
+
}
|
|
2663
|
+
return lo > 0 && !tags[lo - 1].close;
|
|
2664
|
+
}
|
|
2665
|
+
/** A phase heading or a bold phase-list entry: what makes a block a shipped/backlog milestone. */
|
|
2666
|
+
const ACTIVE_SCOPE_PHASE_ENTRY_SRC = `(?:^#{2,4}[ \\t]+Phase\\b|${(0, phase_entry_grammar_cjs_1.phaseEntryPrefixSrc)({ bold: 'required' })}Phase\\b)`;
|
|
2667
|
+
/**
|
|
2668
|
+
* The active milestone's writable scope as offsets into `content`.
|
|
2669
|
+
*
|
|
2670
|
+
* `start` is just past the last `</details>` that closes BEFORE the selected
|
|
2671
|
+
* milestone heading, so a backlog `</details>` AFTER it no longer hides it
|
|
2672
|
+
* (#5221) while phase/Progress content above the heading (#2947 preamble layout)
|
|
2673
|
+
* stays in scope. No resolved heading, or one that is itself closed (stale
|
|
2674
|
+
* STATE), uses the last `</details>` before the first phase entry outside any
|
|
2675
|
+
* open <details>, so a backlog `</details>` below the active phases does not
|
|
2676
|
+
* hide them.
|
|
2677
|
+
*
|
|
2678
|
+
* `end` is the first line-leading bare `<details>` AFTER that heading (or first
|
|
2679
|
+
* phase entry): a shipped block placed below the active milestone is never
|
|
2680
|
+
* writable, so a second run cannot tick the archived phase once the active one
|
|
2681
|
+
* is done. Writers that must reach a bottom `## Progress` past a backlog block
|
|
2682
|
+
* (`editProgressTableSlice`) use `start` alone. Tags match case-insensitively.
|
|
2683
|
+
* An archive whose `</details>` is missing cannot be told from a wrapper, so
|
|
2684
|
+
* `start` stays 0 for it (documented fail-open).
|
|
2685
|
+
*/
|
|
2686
|
+
function activeScope(content, cwd) {
|
|
2687
|
+
const tags = bareDetailsTags(content);
|
|
2688
|
+
const headingAt = currentMilestoneRawRanges(content, cwd)?.primary.start;
|
|
2689
|
+
const open = headingAt !== undefined && !isClosedMilestoneHeading(content.slice(headingAt).split('\n', 1)[0]);
|
|
2690
|
+
let anchor;
|
|
2691
|
+
if (open) {
|
|
2692
|
+
anchor = headingAt;
|
|
2693
|
+
}
|
|
2694
|
+
else {
|
|
2695
|
+
const firstActive = [...content.matchAll(new RegExp(ACTIVE_SCOPE_PHASE_ENTRY_SRC, 'gim'))]
|
|
2696
|
+
.find((m) => !insideBareDetails(tags, m.index));
|
|
2697
|
+
anchor = firstActive?.index ?? content.length;
|
|
2698
|
+
}
|
|
2699
|
+
let start = 0;
|
|
2700
|
+
for (const t of tags) {
|
|
2701
|
+
if (t.at >= anchor)
|
|
2702
|
+
break;
|
|
2703
|
+
if (t.close)
|
|
2704
|
+
start = t.at + '</details>'.length;
|
|
2705
|
+
}
|
|
2706
|
+
// Only a block that carries phase entries is a shipped/backlog milestone; a collapsible note inside the active milestone stays writable.
|
|
2707
|
+
const phaseEntry = new RegExp(ACTIVE_SCOPE_PHASE_ENTRY_SRC, 'im');
|
|
2708
|
+
const below = tags.find((t, i) => {
|
|
2709
|
+
if (t.close || t.at <= anchor || !/^[ \t]*$/.test(content.slice(content.lastIndexOf('\n', t.at - 1) + 1, t.at)))
|
|
2710
|
+
return false;
|
|
2711
|
+
const close = tags.slice(i + 1).find((c) => c.close);
|
|
2712
|
+
return phaseEntry.test(content.slice(t.at, close ? close.at : content.length));
|
|
2713
|
+
});
|
|
2714
|
+
return { start, end: below ? below.at : content.length };
|
|
2715
|
+
}
|
|
2716
|
+
function activeScopeStart(content, cwd) {
|
|
2717
|
+
return activeScope(content, cwd).start;
|
|
2718
|
+
}
|
|
2719
|
+
/** [start,end) windows of the current milestone, document order; the whole ROADMAP when none resolves. */
|
|
2720
|
+
function currentMilestoneWindows(content, cwd) {
|
|
2721
|
+
const ranges = currentMilestoneRawRanges(content, cwd);
|
|
2722
|
+
if (!ranges)
|
|
2723
|
+
return [{ start: 0, end: content.length }];
|
|
2724
|
+
// `details` always starts at or after `primary.end` (see `currentMilestoneRawRanges`),
|
|
2725
|
+
// so this is document order: a "first window wins" caller means the primary one.
|
|
2726
|
+
return [ranges.primary, ranges.details].filter((w) => w !== null);
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Read-only counterpart of `editCurrentMilestone`: the text of each window,
|
|
2730
|
+
* with closed-milestone `<details>` blocks hidden exactly as the writers see it.
|
|
2731
|
+
*/
|
|
2732
|
+
function readCurrentMilestone(content, cwd) {
|
|
2733
|
+
return currentMilestoneWindows(content, cwd).map((w) => maskShippedBlocks(content.slice(w.start, w.end)).masked);
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Run `edit` over the current milestone's raw window(s) from
|
|
2737
|
+
* `currentMilestoneRawRanges`, or, when no milestone resolves, over the whole
|
|
2738
|
+
* ROADMAP, each with closed-milestone `<details>` blocks hidden (#5543;
|
|
2739
|
+
* `editOutsideShippedBlocks`: a notes or phase-wrapper block stays editable).
|
|
2740
|
+
* `edit` is called once per window, in document order. The one milestone
|
|
2741
|
+
* scoping for per-phase writers (`phase complete`, `roadmap
|
|
2742
|
+
* update-plan-progress` #5483), so neither cuts at the last `</details>`.
|
|
2743
|
+
*/
|
|
2744
|
+
function editCurrentMilestone(content, cwd, edit) {
|
|
2745
|
+
const windows = currentMilestoneWindows(content, cwd);
|
|
2746
|
+
const edited = windows.map((w) => editOutsideShippedBlocks(content.slice(w.start, w.end), edit));
|
|
2747
|
+
// Splice last-first so an earlier window's offsets are not shifted by a
|
|
2748
|
+
// length-changing mutation in a later window.
|
|
2749
|
+
return windows.reduceRight((acc, w, i) => acc.slice(0, w.start) + edited[i] + acc.slice(w.end), content);
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* The checklist bullet of the phase whose number matches `phasePattern`
|
|
2753
|
+
* (`- [ ] **Phase N: …**`), as the one grammar shared by the Plans-count /
|
|
2754
|
+
* completion stamp and the plan-row ticks. `anyState` also matches a checked box.
|
|
2755
|
+
* Group 1 is everything through the opening `[` (indent and marker included),
|
|
2756
|
+
* group 2 the rest of the line from the closing `]`. The prefix (marker, box, bold token) is the phase-entry grammar's (#5764); the
|
|
2757
|
+
* tail stops before `\r` so a stamp appended to group 2 lands on the entry's
|
|
2758
|
+
* line, before its CRLF.
|
|
2759
|
+
*/
|
|
2760
|
+
function phaseChecklistBullet(phasePattern, anyState) {
|
|
2761
|
+
return (0, phase_entry_grammar_cjs_1.phaseEntrySplitRegExp)(`Phase[ \\t]+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s]|$)[^\\r\\n]*`, 'i', { state: anyState ? 'any' : 'unchecked' });
|
|
2762
|
+
}
|
|
2763
|
+
/** A checklist bullet of ANY phase: ends the rows nested under the previous one. */
|
|
2764
|
+
const ANY_PHASE_BULLET = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)('Phase[ \\t]', 'i');
|
|
2765
|
+
/**
|
|
2766
|
+
* [start,end) of the rows nested under a phase's checklist bullet: from the
|
|
2767
|
+
* line after `bullet` (a match of `phaseChecklistBullet` in `text`) up to a
|
|
2768
|
+
* blank line, a shallower line, or the next phase's bullet. Empty when the
|
|
2769
|
+
* bullet is the last line with no trailing newline.
|
|
2770
|
+
*/
|
|
2771
|
+
function checklistBulletExtent(text, bullet) {
|
|
2772
|
+
// The bullet's own indent, read from its line: a caller's capture group 1 may
|
|
2773
|
+
// be the indent alone (which has no non-space and used to give -1, so a
|
|
2774
|
+
// shallower line never ended the extent) or everything through the `[`.
|
|
2775
|
+
const depth = text.slice(bullet.index).search(/\S/);
|
|
2776
|
+
const start = text.indexOf('\n', bullet.index) + 1;
|
|
2777
|
+
let end = start;
|
|
2778
|
+
while (start > 0 && end < text.length) {
|
|
2779
|
+
const next = text.indexOf('\n', end);
|
|
2780
|
+
const line = text.slice(end, next === -1 ? text.length : next);
|
|
2781
|
+
if (line.trim() === '' || line.search(/\S/) < depth || ANY_PHASE_BULLET.test(line))
|
|
2782
|
+
break;
|
|
2783
|
+
end = next === -1 ? text.length : next + 1;
|
|
2784
|
+
}
|
|
2785
|
+
return { start, end };
|
|
2786
|
+
}
|
|
2214
2787
|
/**
|
|
2215
2788
|
* #4731 — extract a bold-labeled phase field (Goal, Requirements, ...) reading
|
|
2216
2789
|
* past hard-wrapped continuation lines. The roadmapper soft-wraps long fields
|
|
@@ -2275,21 +2848,32 @@ function extractPhaseFieldMultiline(section, label) {
|
|
|
2275
2848
|
return [firstLine, ...contLines].join(' ').trim() || null;
|
|
2276
2849
|
}
|
|
2277
2850
|
module.exports = {
|
|
2851
|
+
activeScope,
|
|
2852
|
+
activeScopeStart,
|
|
2278
2853
|
stripShippedMilestones,
|
|
2279
2854
|
extractCurrentMilestone,
|
|
2280
2855
|
extractCurrentMilestoneScoped,
|
|
2281
2856
|
isMilestoneShippedInRoadmap,
|
|
2282
2857
|
isMilestoneBoundedInRoadmap,
|
|
2283
|
-
|
|
2858
|
+
maskArchivedDetails,
|
|
2284
2859
|
getRoadmapPhaseInternal,
|
|
2285
2860
|
getMilestoneInfo,
|
|
2286
2861
|
classifyMilestoneScalar,
|
|
2862
|
+
readStateMilestoneScalar,
|
|
2287
2863
|
getMilestonePhaseFilter,
|
|
2288
2864
|
currentMilestoneRawRanges,
|
|
2289
2865
|
withPhaseSection,
|
|
2866
|
+
editOutsideShippedBlocks,
|
|
2867
|
+
editCurrentMilestone,
|
|
2868
|
+
readCurrentMilestone,
|
|
2869
|
+
phaseChecklistBullet,
|
|
2870
|
+
checklistBulletExtent,
|
|
2290
2871
|
computeMilestoneSectionEnd,
|
|
2291
2872
|
locateMilestoneHeadings,
|
|
2292
2873
|
listMilestoneHeadings,
|
|
2874
|
+
// #4304: historical mutation guards consume the window locator's
|
|
2875
|
+
// milestone-vs-phase decision before applying CLOSED markers.
|
|
2876
|
+
isRecognizedMilestoneHeading,
|
|
2293
2877
|
selectMilestoneHeading,
|
|
2294
2878
|
classifyMilestoneWindow,
|
|
2295
2879
|
// #3184: the sole "give me this version's window" composition — see its
|
|
@@ -2301,9 +2885,6 @@ module.exports = {
|
|
|
2301
2885
|
hasMilestoneSectioning,
|
|
2302
2886
|
// #3642: the >=1 sibling buildStateFrontmatter's unbounded branch consumes.
|
|
2303
2887
|
hasAnyMilestoneSection,
|
|
2304
|
-
// #1956: sole owner of the #2012 decoy-avoidance scope for the
|
|
2305
|
-
// `drift-guard phase-status` CLI seam.
|
|
2306
|
-
findRoadmapProgressTable,
|
|
2307
2888
|
// #3262 (write-time milestone-scope guard): the window phase-id scan owner
|
|
2308
2889
|
// (consumed by getMilestonePhaseFilter above and the roadmap milestone-scope
|
|
2309
2890
|
// CLI probe) and the free-text predicate the phase add/add-batch/insert
|
|
@@ -2317,6 +2898,23 @@ module.exports = {
|
|
|
2317
2898
|
// owner (and its convention gate) instead of a private inline copy.
|
|
2318
2899
|
extractPhaseFieldMultiline,
|
|
2319
2900
|
hasPhaseEntries,
|
|
2901
|
+
isPhaseEntryHeading,
|
|
2902
|
+
// #4304: exported so `phase.cts`'s milestone-marker
|
|
2903
|
+
// enumeration (bracketRecognizedMilestoneMarkers) can recognize a
|
|
2904
|
+
// version-less bracket milestone heading through the SAME grammar the
|
|
2905
|
+
// window locator (bracketAwareMilestoneSection) already uses to decide
|
|
2906
|
+
// section boundaries, instead of a narrower, independently-typed copy.
|
|
2907
|
+
isBracketMilestoneBoundary,
|
|
2908
|
+
bracketFallbackHeadingMatches,
|
|
2909
|
+
// #4304: exported so `phase.cts`'s pre-mutation window guard
|
|
2910
|
+
// (bracketOwnedLineOutsideActiveWindow) can recognize a CLOSED/shipped
|
|
2911
|
+
// milestone heading through the SAME predicate `currentMilestoneRawRanges`
|
|
2912
|
+
// itself already uses to skip a closed heading when selecting the active
|
|
2913
|
+
// one, instead of a re-typed copy of MILESTONE_CLOSED_MARKER_PATTERN.
|
|
2914
|
+
isClosedMilestoneHeading,
|
|
2915
|
+
// #4304: destructive ROADMAP writers classify details blocks through
|
|
2916
|
+
// the same closed-summary rule as stripClosedMilestoneDetails.
|
|
2917
|
+
isClosedMilestoneDetails,
|
|
2320
2918
|
// #4144 round 5 Blocker 1: the readers' own phase-heading grammar, so the
|
|
2321
2919
|
// bracket roadmap migrator's "phase-like but unparsed" refusal
|
|
2322
2920
|
// (src/roadmap-upgrade.cts) can gate on it instead of a private, looser
|