@pareto4/pp-core 1.11.1 → 1.12.1
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/plugin.json +1 -1
- package/README.md +31 -0
- package/agents/pp-code-fixer.md +1 -1
- package/agents/pp-debug-session-manager.md +1 -1
- package/agents/pp-debugger.md +1 -1
- package/agents/pp-executor.md +20 -10
- package/agents/pp-intel-updater.md +1 -1
- package/agents/pp-phase-researcher.md +1 -1
- package/agents/pp-plan-checker.md +5 -5
- package/agents/pp-planner.md +10 -24
- package/agents/pp-project-researcher.md +1 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-researcher.md +1 -1
- package/agents/pp-verifier.md +8 -4
- package/bin/install.js +429 -82
- package/commands/pp-autonomous.md +8 -6
- package/commands/pp-discovery-to-build.md +1 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-discuss-phase.md +2 -2
- package/commands/pp-execute-phase.md +3 -1
- package/commands/pp-graphify.md +5 -5
- package/commands/pp-import.md +1 -1
- package/commands/pp-map-codebase.md +1 -1
- package/commands/pp-ns-project.md +2 -3
- package/commands/pp-ns-workflow.md +1 -1
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-verify-work.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-cursor-session-start.js +2 -2
- package/hooks/dist/pp-session-state.sh +1 -1
- package/hooks/dist/pp-statusline.js +21 -2
- package/hooks/pp-cursor-session-start.js +2 -2
- package/hooks/pp-session-state.sh +1 -1
- package/hooks/pp-statusline.js +21 -2
- package/package.json +18 -5
- package/pareto-skills/pp-project-setup/SKILL.md +5 -63
- package/pp-core/bin/detect-build-commands.sh +182 -0
- package/pp-core/bin/lib/capability-activation.cjs +19 -9
- package/pp-core/bin/lib/capability-registry.cjs +65 -7
- package/pp-core/bin/lib/check-command-router.cjs +79 -38
- package/pp-core/bin/lib/clusters.cjs +2 -3
- package/pp-core/bin/lib/command-aliases.cjs +36 -0
- package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
- package/pp-core/bin/lib/commands.cjs +21 -6
- package/pp-core/bin/lib/compose-bindings.cjs +70 -6
- package/pp-core/bin/lib/config-loader.cjs +151 -8
- package/pp-core/bin/lib/config-schema.cjs +16 -0
- package/pp-core/bin/lib/config.cjs +142 -27
- package/pp-core/bin/lib/core-utils.cjs +43 -7
- package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
- package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
- package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
- package/pp-core/bin/lib/discovery-table.cjs +98 -0
- package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
- package/pp-core/bin/lib/frontmatter.cjs +17 -2
- package/pp-core/bin/lib/init-command-router.cjs +8 -5
- package/pp-core/bin/lib/init-contract.cjs +93 -4
- package/pp-core/bin/lib/init.cjs +124 -134
- package/pp-core/bin/lib/install-profiles.cjs +5 -0
- package/pp-core/bin/lib/install-run.cjs +536 -0
- package/pp-core/bin/lib/io.cjs +15 -0
- package/pp-core/bin/lib/loop-resolver.cjs +40 -2
- package/pp-core/bin/lib/milestone.cjs +345 -48
- package/pp-core/bin/lib/phase-command-router.cjs +23 -1
- package/pp-core/bin/lib/phase-locator.cjs +15 -6
- package/pp-core/bin/lib/phase-scope.cjs +91 -0
- package/pp-core/bin/lib/phase-tier.cjs +467 -0
- package/pp-core/bin/lib/phase.cjs +247 -21
- package/pp-core/bin/lib/phases-command-router.cjs +2 -0
- package/pp-core/bin/lib/plan-graph.cjs +368 -0
- package/pp-core/bin/lib/planning-workspace.cjs +53 -0
- package/pp-core/bin/lib/platform-bindings.cjs +32 -2
- package/pp-core/bin/lib/profile-output.cjs +11 -3
- package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
- package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
- package/pp-core/bin/lib/roadmap.cjs +141 -39
- package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
- package/pp-core/bin/lib/state-command-router.cjs +82 -13
- package/pp-core/bin/lib/state-document.cjs +124 -7
- package/pp-core/bin/lib/state.cjs +784 -257
- package/pp-core/bin/lib/template.cjs +22 -6
- package/pp-core/bin/lib/vault-command-router.cjs +57 -6
- package/pp-core/bin/lib/vault-practices.cjs +6 -36
- package/pp-core/bin/lib/vault-write-back.cjs +20 -4
- package/pp-core/bin/lib/vault.cjs +592 -23
- package/pp-core/bin/lib/verify.cjs +53 -31
- package/pp-core/bin/lib/workstream.cjs +1 -1
- package/pp-core/bin/lib/worktree-safety.cjs +26 -15
- package/pp-core/bin/portable-timeout.sh +89 -0
- package/pp-core/bin/pp-tools.cjs +149 -9
- package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
- package/pp-core/bin/shared/config-schema.manifest.json +3 -0
- package/pp-core/references/autonomous-best-effort.md +461 -0
- package/pp-core/references/autonomous-lifecycle.md +146 -0
- package/pp-core/references/autonomous-smart-discuss.md +16 -12
- package/pp-core/references/checkpoints.md +1 -1
- package/pp-core/references/discovery-id-conventions.md +202 -0
- package/pp-core/references/discovery-rebaseline.md +254 -0
- package/pp-core/references/discovery-research-track.md +283 -0
- package/pp-core/references/discovery-traceability.md +400 -0
- package/pp-core/references/loop-hook-dispatch.md +14 -0
- package/pp-core/references/mandatory-initial-read.md +1 -1
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-human-verify-mode.md +1 -1
- package/pp-core/references/planner-load-graph-context.md +1 -1
- package/pp-core/references/planner-plan-graph.md +66 -0
- package/pp-core/references/planner-tdd.md +1 -0
- package/pp-core/references/planning-config.md +8 -0
- package/pp-core/references/sketch-tooling.md +4 -0
- package/pp-core/references/tdd.md +1 -0
- package/pp-core/templates/claude-md.md +1 -1
- package/pp-core/templates/phase-prompt.md +6 -2
- package/pp-core/templates/prototype-review/review.js +303 -0
- package/pp-core/templates/state.md +6 -0
- package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-phase.md +1 -1
- package/pp-core/workflows/add-tests.md +1 -1
- package/pp-core/workflows/add-todo.md +1 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -2
- package/pp-core/workflows/audit-fix.md +12 -16
- package/pp-core/workflows/audit-milestone.md +2 -2
- package/pp-core/workflows/audit-uat.md +1 -1
- package/pp-core/workflows/autonomous.md +68 -306
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/cleanup.md +1 -1
- package/pp-core/workflows/code-review-fix.md +2 -2
- package/pp-core/workflows/code-review.md +20 -8
- package/pp-core/workflows/complete-milestone.md +41 -60
- package/pp-core/workflows/debug.md +1 -1
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
- package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
- package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
- package/pp-core/workflows/discovery-sprint.md +79 -1714
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
- package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
- package/pp-core/workflows/discovery-to-build.md +21 -238
- package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
- package/pp-core/workflows/discuss-phase.md +11 -4
- package/pp-core/workflows/do.md +4 -4
- package/pp-core/workflows/docs-update.md +1 -1
- package/pp-core/workflows/edit-phase.md +1 -1
- package/pp-core/workflows/eval-review.md +1 -1
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
- package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
- package/pp-core/workflows/execute-phase.md +104 -218
- package/pp-core/workflows/execute-plan.md +6 -2
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/extract-learnings.md +1 -1
- package/pp-core/workflows/forensics.md +4 -4
- package/pp-core/workflows/graduation.md +1 -1
- package/pp-core/workflows/health.md +1 -1
- package/pp-core/workflows/help/modes/brief.md +1 -0
- package/pp-core/workflows/help/modes/default.md +2 -1
- package/pp-core/workflows/help/modes/full.md +12 -5
- package/pp-core/workflows/import.md +1 -1
- package/pp-core/workflows/ingest-docs.md +1 -1
- package/pp-core/workflows/init.md +18 -2
- package/pp-core/workflows/insert-phase.md +1 -1
- package/pp-core/workflows/list-workspaces.md +1 -1
- package/pp-core/workflows/manager.md +3 -3
- package/pp-core/workflows/map-codebase.md +2 -2
- package/pp-core/workflows/milestone-summary.md +4 -4
- package/pp-core/workflows/mvp-phase.md +1 -1
- package/pp-core/workflows/new-milestone.md +7 -8
- package/pp-core/workflows/new-project.md +6 -1
- package/pp-core/workflows/new-workspace.md +1 -1
- package/pp-core/workflows/next.md +2 -2
- package/pp-core/workflows/note.md +2 -2
- package/pp-core/workflows/pause-work.md +1 -1
- package/pp-core/workflows/plan-milestone-gaps.md +1 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
- package/pp-core/workflows/plan-phase.md +43 -167
- package/pp-core/workflows/plan-review-convergence.md +2 -2
- package/pp-core/workflows/plant-seed.md +1 -1
- package/pp-core/workflows/pr-branch.md +1 -1
- package/pp-core/workflows/profile-user.md +1 -1
- package/pp-core/workflows/progress.md +3 -3
- package/pp-core/workflows/quick.md +4 -2
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/remove-workspace.md +1 -1
- package/pp-core/workflows/resume-project.md +6 -12
- package/pp-core/workflows/review.md +1 -1
- package/pp-core/workflows/scan.md +1 -1
- package/pp-core/workflows/secure-phase.md +2 -2
- package/pp-core/workflows/settings-advanced.md +1 -1
- package/pp-core/workflows/settings-integrations.md +1 -1
- package/pp-core/workflows/settings.md +1 -1
- package/pp-core/workflows/ship.md +26 -9
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +2 -1
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +1 -1
- package/pp-core/workflows/stats.md +2 -2
- package/pp-core/workflows/thread.md +1 -1
- package/pp-core/workflows/transition.md +5 -9
- package/pp-core/workflows/ui-phase.md +23 -2
- package/pp-core/workflows/ui-review.md +1 -1
- package/pp-core/workflows/ultraplan-phase.md +2 -2
- package/pp-core/workflows/validate-phase.md +2 -2
- package/pp-core/workflows/vault-write-back.md +1 -1
- package/pp-core/workflows/verify-phase.md +29 -26
- package/pp-core/workflows/verify-work.md +14 -10
- package/scripts/base64-scan.sh +7 -41
- package/scripts/changeset/cli.cjs +25 -5
- package/scripts/dev-env.cjs +462 -0
- package/scripts/lib/allowlist-ratchet.cjs +103 -1
- package/scripts/lib/size-budget.cjs +311 -0
- package/scripts/lint-neutrality-names.cjs +694 -0
- package/scripts/lint-org-coordinates.allowlist.json +35 -0
- package/scripts/lint-org-coordinates.cjs +426 -0
- package/scripts/lint-skill-deps.cjs +48 -9
- package/scripts/lint-test-file-count.cjs +17 -2
- package/scripts/prompt-injection-scan.sh +41 -20
- package/scripts/sync-manifest-versions.cjs +41 -7
- package/scripts/sync-runtime-launcher.cjs +106 -1
- package/scripts/update-size-baseline.cjs +274 -30
- package/scripts/workflow-size.cjs +61 -9
- package/commands/pp-build-handoff.md +0 -46
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* Dependencies (leaf modules only — no loadConfig):
|
|
13
13
|
* - node:fs / node:path (stdlib)
|
|
14
14
|
* - ./phase-id.cjs (escapeRegex, phaseMarkdownRegexSource)
|
|
15
|
-
* - ./planning-workspace.cjs (planningDir)
|
|
15
|
+
* - ./planning-workspace.cjs (planningDir, milestonePathsFor)
|
|
16
16
|
* - ./shell-command-projection.cjs (platformReadSync)
|
|
17
|
+
* - ./core-utils.cjs (generateSlugInternal — the phase-dir slug owner)
|
|
17
18
|
*/
|
|
18
19
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
20
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -25,7 +26,10 @@ const phaseIdModule = require("./phase-id.cjs");
|
|
|
25
26
|
const { escapeRegex, phaseMarkdownRegexSource } = phaseIdModule;
|
|
26
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
28
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
|
-
const { planningDir } = planningWorkspace;
|
|
29
|
+
const { planningDir, milestonePathsFor } = planningWorkspace;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const coreUtilsModule = require("./core-utils.cjs");
|
|
32
|
+
const { generateSlugInternal } = coreUtilsModule;
|
|
29
33
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
30
34
|
// ─── Roadmap milestone scoping ───────────────────────────────────────────────
|
|
31
35
|
/**
|
|
@@ -37,6 +41,25 @@ function stripShippedMilestones(content) {
|
|
|
37
41
|
/**
|
|
38
42
|
* Extract the current milestone section from ROADMAP.md by positive lookup.
|
|
39
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* The `<details>` block whose `<summary>` names `version` — the collapsed shape
|
|
46
|
+
* templates/roadmap.md gives a shipped milestone — as [start, end) offsets, or null.
|
|
47
|
+
* One owner for both extractCurrentMilestone (version from STATE.md) and milestoneScope
|
|
48
|
+
* (version asked for), 261007-mmz follow-up 4.
|
|
49
|
+
*/
|
|
50
|
+
function summaryDetailsBlock(content, version) {
|
|
51
|
+
const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${escapeRegex(version)}[^<]*)<\\/summary>`, 'i');
|
|
52
|
+
const summaryMatch = content.match(summaryPattern);
|
|
53
|
+
if (!summaryMatch)
|
|
54
|
+
return null;
|
|
55
|
+
const summaryIdx = content.indexOf(summaryMatch[0]);
|
|
56
|
+
const detailsOpenIdx = content.slice(0, summaryIdx).lastIndexOf('<details');
|
|
57
|
+
if (detailsOpenIdx === -1)
|
|
58
|
+
return null;
|
|
59
|
+
const closingMatch = content.slice(detailsOpenIdx).match(/<\/details>/i);
|
|
60
|
+
const end = closingMatch ? detailsOpenIdx + (closingMatch.index ?? 0) + '</details>'.length : content.length;
|
|
61
|
+
return { start: detailsOpenIdx, end };
|
|
62
|
+
}
|
|
40
63
|
function extractCurrentMilestone(content, cwd) {
|
|
41
64
|
if (!cwd)
|
|
42
65
|
return stripShippedMilestones(content);
|
|
@@ -62,29 +85,18 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
62
85
|
return stripShippedMilestones(content);
|
|
63
86
|
const escapedVersion = escapeRegex(version);
|
|
64
87
|
const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}\\b[^\\n]*)`, 'gmi');
|
|
65
|
-
const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${escapedVersion}[^<]*)<\\/summary>`, 'i');
|
|
66
88
|
const headingMatches = [...content.matchAll(sectionPattern)];
|
|
67
89
|
if (headingMatches.length === 0) {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: content.length;
|
|
79
|
-
const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
|
|
80
|
-
const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
|
|
81
|
-
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
|
|
82
|
-
const preamble = content.slice(0, preambleCutoff)
|
|
83
|
-
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
84
|
-
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
85
|
-
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
86
|
-
return preamble + content.slice(detailsOpenIdx, detailsEnd);
|
|
87
|
-
}
|
|
90
|
+
const block = summaryDetailsBlock(content, version);
|
|
91
|
+
if (block) {
|
|
92
|
+
const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
|
|
93
|
+
const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
|
|
94
|
+
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : block.start;
|
|
95
|
+
const preamble = content.slice(0, preambleCutoff)
|
|
96
|
+
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
97
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
98
|
+
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
99
|
+
return preamble + content.slice(block.start, block.end);
|
|
88
100
|
}
|
|
89
101
|
return stripShippedMilestones(content);
|
|
90
102
|
}
|
|
@@ -178,6 +190,71 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
|
178
190
|
const after = content.slice(offset);
|
|
179
191
|
return before + after.replace(pattern, replacement);
|
|
180
192
|
}
|
|
193
|
+
// ─── The phase-section-bounded Plans line (D-08-33) ───────────────────────────
|
|
194
|
+
/**
|
|
195
|
+
* A lazy scan that CANNOT leave the phase section it started in.
|
|
196
|
+
*
|
|
197
|
+
* Each step is tempered by a negative lookahead for the next phase heading, so
|
|
198
|
+
* a pattern anchored on `### Phase N` reaches only text that still belongs to
|
|
199
|
+
* phase N. `[\s\S]*?` on its own does not stop at a heading, and both roadmap
|
|
200
|
+
* writers used exactly that: given a phase section with no Plans line, the scan
|
|
201
|
+
* walked into the NEXT phase and wrote there.
|
|
202
|
+
*
|
|
203
|
+
* Owned here, in one place, because that bound is the property both the summary
|
|
204
|
+
* line pattern and `cmdRoadmapUpdatePlanProgress`'s row-insertion anchors need,
|
|
205
|
+
* and three copies of it would be three chances to lose it again (D-08-33).
|
|
206
|
+
*
|
|
207
|
+
* ReDoS shape (T-08-52): one lazy loop over a single character class, guarded
|
|
208
|
+
* by a lookahead with no quantifier of its own beyond the bounded `#{2,4}\s*`.
|
|
209
|
+
* There is no nested quantifier for a hostile roadmap to exploit; the
|
|
210
|
+
* `bug-663-redos-roadmap-phase-parsing` and `feat-3594-parser-adversarial-roadmap`
|
|
211
|
+
* suites are the standing regression net (an elapsed-time assertion is banned by
|
|
212
|
+
* `local/no-elapsed-assertion`).
|
|
213
|
+
*/
|
|
214
|
+
const PHASE_SECTION_SCAN = '(?:(?!\\n#{2,4}\\s*Phase\\s)[\\s\\S])*?';
|
|
215
|
+
/**
|
|
216
|
+
* The three Plans-summary-line spellings a ROADMAP.md may use. All are
|
|
217
|
+
* tolerated; the canonical template (`pp-core/templates/roadmap.md`) writes the
|
|
218
|
+
* first:
|
|
219
|
+
* `**Plans**: N plans` — bold word, colon OUTSIDE the bold markers
|
|
220
|
+
* `**Plans:** N plans` — colon INSIDE the bold markers
|
|
221
|
+
* `Plans: N plans` — plain-text header at start of a line
|
|
222
|
+
*/
|
|
223
|
+
const PLANS_SUMMARY_SPELLINGS = '(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)';
|
|
224
|
+
/**
|
|
225
|
+
* Build the pattern that locates ONE phase's Plans summary line and captures
|
|
226
|
+
* everything up to (and including) its `Plans:` marker, leaving the line's value
|
|
227
|
+
* as the unmatched tail for the caller to replace.
|
|
228
|
+
*
|
|
229
|
+
* `pattern.replace(p, '$1<new value>')` therefore rewrites the value and nothing
|
|
230
|
+
* else. Group 1 spans from the phase heading to the marker.
|
|
231
|
+
*
|
|
232
|
+
* FOUR properties, each of which one of the two call sites was missing before
|
|
233
|
+
* plan 08-10 (D-08-33):
|
|
234
|
+
*
|
|
235
|
+
* 1. All three spellings. `cmdPhaseComplete` knew only `**Plans:**`, so on a
|
|
236
|
+
* CANONICAL roadmap — `**Plans**:`, colon outside — it did not find its own
|
|
237
|
+
* phase's line and kept scanning.
|
|
238
|
+
* 2. The `(?=[:\s])` phase-number boundary. Without it `Phase 1` matches the
|
|
239
|
+
* heading `Phase 10`; `cmdRoadmapUpdatePlanProgress` had this guard and
|
|
240
|
+
* `cmdPhaseComplete` did not.
|
|
241
|
+
* 3. The phase-section bound (`PHASE_SECTION_SCAN`). This is the F2 defect
|
|
242
|
+
* measured on the live UAT fixture: `phase complete 1` rewrote line 90 of
|
|
243
|
+
* ROADMAP.md — inside `### Phase 2` — to `2/2 plans complete`, asserting a
|
|
244
|
+
* build that had never happened for a phase with nothing executed. The
|
|
245
|
+
* same latent bug sat in `cmdRoadmapUpdatePlanProgress`.
|
|
246
|
+
* 4. A same-line tail (`[^\S\n]*`, horizontal whitespace only). Both sites
|
|
247
|
+
* used `\s*`, which crosses newlines: a bare `Plans:` checklist HEADER on
|
|
248
|
+
* its own line would have had the first checklist row below it consumed and
|
|
249
|
+
* rewritten into a count.
|
|
250
|
+
*
|
|
251
|
+
* Consumers: `cmdRoadmapUpdatePlanProgress` (src/roadmap.cts) and
|
|
252
|
+
* `cmdPhaseComplete` (src/phase.cts). Neither carries its own copy.
|
|
253
|
+
*/
|
|
254
|
+
function buildPlansSummaryLinePattern(phaseNum) {
|
|
255
|
+
const phaseSource = phaseMarkdownRegexSource(phaseNum);
|
|
256
|
+
return new RegExp(`(#{2,4}\\s*Phase\\s+${phaseSource}(?=[:\\s])${PHASE_SECTION_SCAN}${PLANS_SUMMARY_SPELLINGS}[^\\S\\n]*)[^\\n]+`, 'i');
|
|
257
|
+
}
|
|
181
258
|
function findRoadmapPhaseInContent(content, phaseNum) {
|
|
182
259
|
const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
|
|
183
260
|
const headerMatch = content.match(phasePattern);
|
|
@@ -219,7 +296,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
219
296
|
return null;
|
|
220
297
|
}
|
|
221
298
|
}
|
|
222
|
-
function
|
|
299
|
+
function resolveMilestoneInfo(cwd) {
|
|
223
300
|
try {
|
|
224
301
|
const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
|
|
225
302
|
if (roadmap === null)
|
|
@@ -239,45 +316,83 @@ function getMilestoneInfo(cwd) {
|
|
|
239
316
|
}
|
|
240
317
|
if (stateVersion) {
|
|
241
318
|
const escapedVer = escapeRegex(stateVersion);
|
|
242
|
-
|
|
319
|
+
// 261007-ixt W3c: a `### Phase N: vX.Y …` heading names a version without
|
|
320
|
+
// declaring it, so it must not supply the milestone's name.
|
|
321
|
+
const headingMatch = roadmap.match(new RegExp(`^#{2,}\\s+(?!Phase\\s+\\S)[^\\n]*${escapedVer}[:\\s]+([^\\n(]+)`, 'im'));
|
|
243
322
|
if (headingMatch) {
|
|
244
323
|
if (!headingMatch[0].includes('✅')) {
|
|
245
|
-
return { version: stateVersion, name: headingMatch[1].trim() };
|
|
324
|
+
return { info: { version: stateVersion, name: headingMatch[1].trim() }, declared: true };
|
|
246
325
|
}
|
|
247
326
|
}
|
|
248
327
|
else {
|
|
249
328
|
const listMatch = roadmap.match(new RegExp(`🚧\\s*\\*?\\*?${escapedVer}\\s+([^*\\n]+)`, 'i'));
|
|
250
329
|
if (listMatch) {
|
|
251
|
-
return { version: stateVersion, name: listMatch[1].trim() };
|
|
330
|
+
return { info: { version: stateVersion, name: listMatch[1].trim() }, declared: true };
|
|
331
|
+
}
|
|
332
|
+
const bodyMatch = roadmap.match(new RegExp(`^\\*\\*\\s*Milestone\\s+${escapedVer}\\s*[—–:-]+\\s*([^*\\n]+?)\\s*\\*\\*`, 'im'));
|
|
333
|
+
if (bodyMatch) {
|
|
334
|
+
return { info: { version: stateVersion, name: bodyMatch[1].trim() }, declared: true };
|
|
252
335
|
}
|
|
253
|
-
return { version: stateVersion, name: 'milestone' };
|
|
336
|
+
return { info: { version: stateVersion, name: 'milestone' }, declared: true };
|
|
254
337
|
}
|
|
255
338
|
}
|
|
256
339
|
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
|
|
257
340
|
if (inProgressMatch) {
|
|
258
|
-
return {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
341
|
+
return { info: {
|
|
342
|
+
version: 'v' + inProgressMatch[1],
|
|
343
|
+
name: inProgressMatch[2].trim(),
|
|
344
|
+
}, declared: true };
|
|
262
345
|
}
|
|
263
346
|
const cleaned = stripShippedMilestones(roadmap);
|
|
264
|
-
|
|
347
|
+
// 261007-ixt W3c: a PHASE heading that names a version (`### Phase 12: v1.0
|
|
348
|
+
// Carry-Forward Closure`) does not declare that milestone — the same rule
|
|
349
|
+
// getMilestonePhaseFilter's `(?!Phase\s+\S)` lookahead applies (CARRY-04). With no
|
|
350
|
+
// STATE.md to read (health --repair regenerating it), this branch used to name
|
|
351
|
+
// the regenerated v1.1 record `v1.0 Carry-Forward Closure`.
|
|
352
|
+
const headingMatch = cleaned.match(/^#{2,}\s+(?!Phase\s+\S)(?!.*✅).*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/m);
|
|
265
353
|
if (headingMatch) {
|
|
266
|
-
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
354
|
+
return { info: {
|
|
355
|
+
version: 'v' + headingMatch[1],
|
|
356
|
+
name: headingMatch[2].trim(),
|
|
357
|
+
}, declared: true };
|
|
358
|
+
}
|
|
359
|
+
// A milestone declared in bold body text — `**Milestone v1.1 — Vault Hardening**`,
|
|
360
|
+
// the form CARRY-04 taught getMilestonePhaseFilter to recognise.
|
|
361
|
+
const bodyDeclared = cleaned.match(/^\*\*\s*Milestone\s+v(\d+(?:\.\d+)+)\s*[—–:-]+\s*([^*\n]+?)\s*\*\*/m);
|
|
362
|
+
if (bodyDeclared) {
|
|
363
|
+
return { info: {
|
|
364
|
+
version: 'v' + bodyDeclared[1],
|
|
365
|
+
name: bodyDeclared[2].trim(),
|
|
366
|
+
}, declared: true };
|
|
270
367
|
}
|
|
368
|
+
// Last resort: nothing DECLARES a current milestone (no STATE.md version, no
|
|
369
|
+
// in-progress marker, no milestone heading, no bold-body declaration). The first
|
|
370
|
+
// version the text mentions is a guess — between milestones it is whichever closed
|
|
371
|
+
// milestone the completed list happens to name first (261007-ixt W4).
|
|
271
372
|
const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/);
|
|
272
373
|
return {
|
|
273
|
-
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
274
|
-
|
|
374
|
+
info: { version: versionMatch ? versionMatch[0] : 'v1.0', name: 'milestone' },
|
|
375
|
+
declared: false,
|
|
275
376
|
};
|
|
276
377
|
}
|
|
277
378
|
catch {
|
|
278
|
-
return { version: 'v1.0', name: 'milestone' };
|
|
379
|
+
return { info: { version: 'v1.0', name: 'milestone' }, declared: false };
|
|
279
380
|
}
|
|
280
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* The milestone the planning documents DECLARE as current, or null when nothing
|
|
384
|
+
* declares one (261007-ixt W4). getMilestoneInfo keeps its long-standing last-resort
|
|
385
|
+
* guess for display callers; a writer that stores the milestone in a record must not
|
|
386
|
+
* store a guess — between milestones the guess was whichever closed milestone the
|
|
387
|
+
* ROADMAP's completed list named first.
|
|
388
|
+
*/
|
|
389
|
+
function getDeclaredMilestoneInfo(cwd) {
|
|
390
|
+
const resolved = resolveMilestoneInfo(cwd);
|
|
391
|
+
return resolved.declared ? resolved.info : null;
|
|
392
|
+
}
|
|
393
|
+
function getMilestoneInfo(cwd) {
|
|
394
|
+
return resolveMilestoneInfo(cwd).info;
|
|
395
|
+
}
|
|
281
396
|
// ─── Fence-aware text helper ──────────────────────────────────────────────────
|
|
282
397
|
/**
|
|
283
398
|
* Return a copy of `text` with every line that lies inside a fenced code block
|
|
@@ -319,6 +434,90 @@ function stripFencedLines(text) {
|
|
|
319
434
|
}
|
|
320
435
|
return result.join('\n');
|
|
321
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* 261007-mmz follow-up 3: the ONE owner of "which part of a ROADMAP text belongs to
|
|
439
|
+
* milestone <version>". Extracted verbatim from getMilestonePhaseFilter (which now
|
|
440
|
+
* calls it), so `milestone complete` and the closed-milestone snapshot reader scope a
|
|
441
|
+
* text the same way. With no versionOverride the scope is the current milestone
|
|
442
|
+
* (STATE.md-keyed when cwd is given, otherwise the document minus shipped blocks).
|
|
443
|
+
*/
|
|
444
|
+
function milestoneScope(roadmapContent, versionOverride, cwd) {
|
|
445
|
+
// 261007-mmz follow-up 4: when a version is asked for, STATE.md is never consulted —
|
|
446
|
+
// the answer depends on the text and the version only, so `milestone complete vX`
|
|
447
|
+
// (cwd) and the archived-snapshot reader (no cwd) scope the same text the same way.
|
|
448
|
+
// STATE.md keys only the no-version "current milestone" question.
|
|
449
|
+
let roadmap = versionOverride ? extractCurrentMilestone(roadmapContent) : extractCurrentMilestone(roadmapContent, cwd);
|
|
450
|
+
let missingExplicitVersion = false;
|
|
451
|
+
if (versionOverride) {
|
|
452
|
+
const escapedVersion = escapeRegex(versionOverride);
|
|
453
|
+
const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}[^\\n]*)`, 'mi');
|
|
454
|
+
const sectionMatch = roadmapContent.match(sectionPattern);
|
|
455
|
+
// A milestone declared only in a <summary> (the collapsed templates/roadmap.md shape)
|
|
456
|
+
// is scoped to ITS <details> block. This used to fall through to the current-milestone
|
|
457
|
+
// read: with cwd, STATE.md's milestone (v1.0 → 1-4, but v1.1 → v1.1's phases); without
|
|
458
|
+
// cwd, the document minus shipped blocks — i.e. everything EXCEPT the requested
|
|
459
|
+
// milestone (measured: v1.0's snapshot owned {5}, not {1..4}).
|
|
460
|
+
const summaryBlock = sectionMatch ? null : summaryDetailsBlock(roadmapContent, versionOverride);
|
|
461
|
+
if (summaryBlock) {
|
|
462
|
+
roadmap = roadmapContent.slice(summaryBlock.start, summaryBlock.end);
|
|
463
|
+
}
|
|
464
|
+
else if (!sectionMatch) {
|
|
465
|
+
const hasVersionedMilestones = /^#{1,3}\s+(?!Phase\s+\S).*v\d+\.\d+/mi.test(roadmapContent);
|
|
466
|
+
// A milestone is not always declared in a HEADING. The vault-assurance
|
|
467
|
+
// workstream declares its milestone on line 3 of its ROADMAP in bold
|
|
468
|
+
// body text — `**Milestone v1.1 — Vault Hardening** · started …` — while
|
|
469
|
+
// its only version-bearing heading is `### Phase 12: v1.0 …`, which the
|
|
470
|
+
// `(?!Phase\s+\S)` lookahead excludes ON PURPOSE: a phase heading that
|
|
471
|
+
// merely NAMES a version does not declare that milestone. Before phase
|
|
472
|
+
// 12 that combination left hasVersionedMilestones false for every
|
|
473
|
+
// requested version, so the guard could never fire, `roadmap` stayed
|
|
474
|
+
// scoped to the CURRENT milestone, and `milestone complete v1.0`
|
|
475
|
+
// archived the current milestone's phases under a v1.0 label and
|
|
476
|
+
// exited 0. Recognising the bold-body form is what closes CARRY-04.
|
|
477
|
+
const declaresBodyMilestone = /^\*\*\s*Milestone\s+v\d+\.\d+/mi.test(roadmapContent);
|
|
478
|
+
const bodyDeclaresRequested = new RegExp(`^\\*\\*\\s*Milestone\\s+${escapedVersion}\\b`, 'mi').test(roadmapContent);
|
|
479
|
+
const versionInSummary = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i').test(roadmapContent);
|
|
480
|
+
if ((hasVersionedMilestones || declaresBodyMilestone) && !versionInSummary && !bodyDeclaresRequested) {
|
|
481
|
+
roadmap = '';
|
|
482
|
+
missingExplicitVersion = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const sectionStart = sectionMatch.index;
|
|
487
|
+
const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
|
|
488
|
+
const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
|
|
489
|
+
const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
|
490
|
+
let sectionEnd = roadmapContent.length;
|
|
491
|
+
let fenceChar = null;
|
|
492
|
+
let fenceLen = 0;
|
|
493
|
+
let charOffset = 0;
|
|
494
|
+
for (const line of restContent.split('\n')) {
|
|
495
|
+
const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
|
496
|
+
if (fenceMatch) {
|
|
497
|
+
const char = fenceMatch[1][0];
|
|
498
|
+
const len = fenceMatch[1].length;
|
|
499
|
+
const trailing = fenceMatch[2] || '';
|
|
500
|
+
if (!fenceChar) {
|
|
501
|
+
fenceChar = char;
|
|
502
|
+
fenceLen = len;
|
|
503
|
+
}
|
|
504
|
+
else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
|
|
505
|
+
fenceChar = null;
|
|
506
|
+
fenceLen = 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (!fenceChar && nextMilestonePattern.test(line)) {
|
|
510
|
+
sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
charOffset += line.length + 1;
|
|
514
|
+
}
|
|
515
|
+
const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
|
|
516
|
+
roadmap = currentSection;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { scope: roadmap, missingExplicitVersion };
|
|
520
|
+
}
|
|
322
521
|
/**
|
|
323
522
|
* Returns a filter function that checks whether a phase directory belongs
|
|
324
523
|
* to the current milestone based on ROADMAP.md phase headings.
|
|
@@ -335,12 +534,18 @@ function stripFencedLines(text) {
|
|
|
335
534
|
function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
336
535
|
const milestonePhaseNums = new Set();
|
|
337
536
|
let missingExplicitVersion = false;
|
|
537
|
+
let roadmapMissing = false;
|
|
338
538
|
try {
|
|
339
539
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
340
540
|
const roadmapContent = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
|
|
341
|
-
if (roadmapContent === null)
|
|
541
|
+
if (roadmapContent === null) {
|
|
542
|
+
// The catch at the end of this block swallows the throw and the tail
|
|
543
|
+
// returns a pass-all filter. That stays as it is — the FLAG, not the
|
|
544
|
+
// catch, is what now carries the information out, so every caller's
|
|
545
|
+
// control flow is byte-identical (phase 12, CARRY-04).
|
|
546
|
+
roadmapMissing = true;
|
|
342
547
|
throw new Error('missing');
|
|
343
|
-
|
|
548
|
+
}
|
|
344
549
|
const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
|
345
550
|
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
|
|
346
551
|
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
|
|
@@ -348,63 +553,9 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
348
553
|
'The project has phase_id_convention set to "milestone-prefixed" in config.json but the ' +
|
|
349
554
|
'ROADMAP does not use versioned milestone headings. Run `pp-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default).');
|
|
350
555
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let sectionMatch = roadmapContent.match(sectionPattern);
|
|
355
|
-
if (!sectionMatch) {
|
|
356
|
-
const summaryPat = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i');
|
|
357
|
-
const summaryHit = roadmapContent.match(summaryPat);
|
|
358
|
-
if (summaryHit) {
|
|
359
|
-
const beforeSummary = roadmapContent.slice(0, summaryHit.index);
|
|
360
|
-
const detailsIdx = beforeSummary.lastIndexOf('<details');
|
|
361
|
-
if (detailsIdx !== -1) {
|
|
362
|
-
sectionMatch = null;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (!sectionMatch) {
|
|
367
|
-
const hasVersionedMilestones = /^#{1,3}\s+(?!Phase\s+\S).*v\d+\.\d+/mi.test(roadmapContent);
|
|
368
|
-
const versionInSummary = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i').test(roadmapContent);
|
|
369
|
-
if (hasVersionedMilestones && !versionInSummary) {
|
|
370
|
-
roadmap = '';
|
|
371
|
-
missingExplicitVersion = true;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
const sectionStart = sectionMatch.index;
|
|
376
|
-
const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
|
|
377
|
-
const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
|
|
378
|
-
const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
|
379
|
-
let sectionEnd = roadmapContent.length;
|
|
380
|
-
let fenceChar = null;
|
|
381
|
-
let fenceLen = 0;
|
|
382
|
-
let charOffset = 0;
|
|
383
|
-
for (const line of restContent.split('\n')) {
|
|
384
|
-
const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
|
385
|
-
if (fenceMatch) {
|
|
386
|
-
const char = fenceMatch[1][0];
|
|
387
|
-
const len = fenceMatch[1].length;
|
|
388
|
-
const trailing = fenceMatch[2] || '';
|
|
389
|
-
if (!fenceChar) {
|
|
390
|
-
fenceChar = char;
|
|
391
|
-
fenceLen = len;
|
|
392
|
-
}
|
|
393
|
-
else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
|
|
394
|
-
fenceChar = null;
|
|
395
|
-
fenceLen = 0;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
else if (!fenceChar && nextMilestonePattern.test(line)) {
|
|
399
|
-
sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
charOffset += line.length + 1;
|
|
403
|
-
}
|
|
404
|
-
const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
|
|
405
|
-
roadmap = currentSection;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
556
|
+
const scoped = milestoneScope(roadmapContent, versionOverride, cwd);
|
|
557
|
+
const roadmap = scoped.scope;
|
|
558
|
+
missingExplicitVersion = scoped.missingExplicitVersion;
|
|
408
559
|
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
|
|
409
560
|
const roadmapUnfenced = stripFencedLines(roadmap);
|
|
410
561
|
let m;
|
|
@@ -417,6 +568,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
417
568
|
const passAll = (() => true);
|
|
418
569
|
passAll.phaseCount = 0;
|
|
419
570
|
passAll.missingExplicitVersion = missingExplicitVersion;
|
|
571
|
+
passAll.roadmapMissing = roadmapMissing;
|
|
420
572
|
return passAll;
|
|
421
573
|
}
|
|
422
574
|
const normalized = new Set([...milestonePhaseNums].map(n => n.split('-').map(seg => (seg.replace(/^0+(?=\d)/, '') || '0')).join('-').toLowerCase()));
|
|
@@ -444,13 +596,128 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
444
596
|
}
|
|
445
597
|
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
446
598
|
isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
|
|
599
|
+
isDirInMilestone.roadmapMissing = roadmapMissing;
|
|
447
600
|
return isDirInMilestone;
|
|
448
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* `<number>:<slug>` keys for every `Phase N: Name` heading in `text` — the identity a
|
|
604
|
+
* phase directory carries in its own name (`01-release-pipeline`), number normalised
|
|
605
|
+
* (leading zeros dropped) and name slugged by the same owner that names directories.
|
|
606
|
+
*/
|
|
607
|
+
function phaseHeadingKeys(text) {
|
|
608
|
+
const keys = new Set();
|
|
609
|
+
const pattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Za-z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
610
|
+
let m;
|
|
611
|
+
const unfenced = stripFencedLines(text);
|
|
612
|
+
while ((m = pattern.exec(unfenced)) !== null) {
|
|
613
|
+
const slug = generateSlugInternal(m[2]);
|
|
614
|
+
if (slug)
|
|
615
|
+
keys.add(`${m[1].replace(/^0+(?=\d)/, '').toLowerCase()}:${slug}`);
|
|
616
|
+
}
|
|
617
|
+
return keys;
|
|
618
|
+
}
|
|
619
|
+
function phaseDirKey(dirName) {
|
|
620
|
+
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)-(.+)$/);
|
|
621
|
+
return m ? `${(m[1] || '0').toLowerCase()}:${m[2].toLowerCase()}` : null;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* 261007-ixt D-9: which directories under `phases/` belong to a CLOSED milestone.
|
|
625
|
+
*
|
|
626
|
+
* A directory is closed when an archived `milestones/vX-ROADMAP.md` (the snapshot
|
|
627
|
+
* `milestone complete` writes) lists a phase with the same number AND the same name,
|
|
628
|
+
* and the current milestone's ROADMAP scope does not list it. Number alone is not
|
|
629
|
+
* enough — a later milestone may reuse numbers under new names — and a phase the
|
|
630
|
+
* current scope still names is still the record's milestone (milestone complete
|
|
631
|
+
* without --archive-phases leaves both the snapshot and the directories in place).
|
|
632
|
+
*
|
|
633
|
+
* The milestone filter cannot see this: when the current milestone names no phase
|
|
634
|
+
* (between milestones), it passes every directory, and the progress scan counted a
|
|
635
|
+
* closed milestone's leftover directories as the current milestone's (measured on the
|
|
636
|
+
* live vault-assurance tree: v1.0's 01–07 rewrote the v1.1 block 5/5/39/39 to
|
|
637
|
+
* 7/7/119/120). No archive on disk → nothing is closed, so a single-milestone
|
|
638
|
+
* project's checklist-only ROADMAP keeps counting every directory.
|
|
639
|
+
*/
|
|
640
|
+
function getClosedMilestonePhaseDirFilter(cwd) {
|
|
641
|
+
const owners = getClosedMilestonePhaseDirOwners(cwd);
|
|
642
|
+
return (dirName) => owners(dirName).length > 0;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* 261007-mmz: the same predicate as getClosedMilestonePhaseDirFilter, answering WHICH
|
|
646
|
+
* closed milestones name a directory — the versions whose archived snapshot lists the
|
|
647
|
+
* directory's number AND name, sorted, minus any phase the current milestone's scope
|
|
648
|
+
* still lists. The filter above is `owners(dir).length > 0`, so the two can never
|
|
649
|
+
* disagree; `phases archive-closed` reads the versions to move a leftover directory
|
|
650
|
+
* into the one milestone archive that names it, and leaves it when none or several do.
|
|
651
|
+
*/
|
|
652
|
+
/**
|
|
653
|
+
* 261007-mmz follow-up 3: the phases an archived `<version>-ROADMAP.md` snapshot names
|
|
654
|
+
* for ITS OWN milestone. A snapshot is a copy of the whole ROADMAP at close time, so it
|
|
655
|
+
* also lists the next milestone's phases; reading every heading made v1.0's snapshot
|
|
656
|
+
* claim v1.1's phase 5 (measured on the step-walked fixture: `phases archive-closed`
|
|
657
|
+
* moved 05-security-audit into v1.0-phases while `milestone complete v1.0` had scoped
|
|
658
|
+
* 1-4). The scope comes from milestoneScope — the same owner `milestone complete`
|
|
659
|
+
* uses — read without cwd, because STATE.md no longer describes a closed milestone.
|
|
660
|
+
*
|
|
661
|
+
* A snapshot in which that scope names no phase for its own version is one the fixed
|
|
662
|
+
* verb would have refused to write (D-1/D-3), so only a close made before 261007-mmz
|
|
663
|
+
* produces it — the live vault-assurance v1.0 snapshot is one: a `### … whole of v1.0`
|
|
664
|
+
* heading in its Open Work section captures the section match. For those the reading
|
|
665
|
+
* stays the pre-existing whole-snapshot one, which is what 261007-ixt D-9 measured on
|
|
666
|
+
* the live tree; number AND name must still match.
|
|
667
|
+
*/
|
|
668
|
+
function snapshotPhaseKeys(text, version) {
|
|
669
|
+
const scoped = phaseHeadingKeys(milestoneScope(text, version).scope);
|
|
670
|
+
return scoped.size > 0 ? scoped : phaseHeadingKeys(text);
|
|
671
|
+
}
|
|
672
|
+
function getClosedMilestonePhaseDirOwners(cwd) {
|
|
673
|
+
const byKey = new Map();
|
|
674
|
+
try {
|
|
675
|
+
const archiveDir = milestonePathsFor(planningDir(cwd)).milestonesArchive;
|
|
676
|
+
for (const file of node_fs_1.default.readdirSync(archiveDir).sort()) {
|
|
677
|
+
const vm = file.match(/^(v\d+(?:\.\d+)*)-ROADMAP\.md$/i);
|
|
678
|
+
if (!vm)
|
|
679
|
+
continue;
|
|
680
|
+
const text = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(archiveDir, file));
|
|
681
|
+
if (text === null)
|
|
682
|
+
continue;
|
|
683
|
+
for (const key of snapshotPhaseKeys(text, vm[1])) {
|
|
684
|
+
const versions = byKey.get(key) ?? [];
|
|
685
|
+
if (!versions.includes(vm[1]))
|
|
686
|
+
versions.push(vm[1]);
|
|
687
|
+
byKey.set(key, versions);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch { /* no archive directory: nothing is closed */ }
|
|
692
|
+
if (byKey.size === 0)
|
|
693
|
+
return () => [];
|
|
694
|
+
try {
|
|
695
|
+
const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
|
|
696
|
+
if (roadmapRaw !== null) {
|
|
697
|
+
for (const key of phaseHeadingKeys(extractCurrentMilestone(roadmapRaw, cwd)))
|
|
698
|
+
byKey.delete(key);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch { /* unreadable ROADMAP: the archive alone decides */ }
|
|
702
|
+
return (dirName) => {
|
|
703
|
+
const key = phaseDirKey(dirName);
|
|
704
|
+
return key !== null ? [...(byKey.get(key) ?? [])] : [];
|
|
705
|
+
};
|
|
706
|
+
}
|
|
449
707
|
module.exports = {
|
|
708
|
+
getClosedMilestonePhaseDirFilter,
|
|
709
|
+
getClosedMilestonePhaseDirOwners,
|
|
450
710
|
stripShippedMilestones,
|
|
451
711
|
extractCurrentMilestone,
|
|
452
712
|
replaceInCurrentMilestone,
|
|
713
|
+
buildPlansSummaryLinePattern,
|
|
714
|
+
// Exported for `cmdRoadmapUpdatePlanProgress`'s row-INSERTION anchors, which
|
|
715
|
+
// need the same "cannot cross a phase heading" bound but a different tail and
|
|
716
|
+
// a different alternation. One owner for the bound, two shapes built from it.
|
|
717
|
+
PHASE_SECTION_SCAN,
|
|
453
718
|
getRoadmapPhaseInternal,
|
|
454
719
|
getMilestoneInfo,
|
|
720
|
+
getDeclaredMilestoneInfo,
|
|
455
721
|
getMilestonePhaseFilter,
|
|
722
|
+
milestoneScope,
|
|
456
723
|
};
|