@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +30 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +1568 -622
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +761 -84
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +45 -4
- package/gsd-core/bin/lib/init.cjs +76 -39
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +228 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/security.cjs +6 -36
- package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +40 -6
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +14 -9
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +37 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +12 -9
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +6 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +6 -0
- package/package.json +9 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +8 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/run-tests.cjs +21 -1
- package/scripts/validate-registry.cjs +117 -0
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -22,7 +22,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const phaseIdModule = require("./phase-id.cjs");
|
|
25
|
-
const { escapeRegex, phaseMarkdownRegexSource,
|
|
25
|
+
const { escapeRegex, phaseMarkdownRegexSource, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE,
|
|
26
|
+
// #2121: roadmapPhaseLookupSources now lives in phase-id.cjs (single owner of
|
|
27
|
+
// the lookup-source ordering); imported here rather than defined locally.
|
|
28
|
+
roadmapPhaseLookupSources, } = phaseIdModule;
|
|
26
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
30
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
31
|
const { planningDir } = planningWorkspace;
|
|
@@ -33,7 +36,7 @@ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
|
33
36
|
* Strip shipped milestone content wrapped in <details> blocks.
|
|
34
37
|
*/
|
|
35
38
|
function stripShippedMilestones(content) {
|
|
36
|
-
return
|
|
39
|
+
return (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content, 'details');
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* Extract the current milestone section from ROADMAP.md by positive lookup.
|
|
@@ -80,10 +83,9 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
80
83
|
const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
|
|
81
84
|
const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
|
|
82
85
|
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
|
|
83
|
-
const preamble = content.slice(0, preambleCutoff)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]*\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
86
|
+
const preamble = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content.slice(0, preambleCutoff), 'details')
|
|
87
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
88
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
87
89
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
88
90
|
return preamble + content.slice(detailsOpenIdx, detailsEnd);
|
|
89
91
|
}
|
|
@@ -150,10 +152,9 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
150
152
|
const detailsStart = detailsMatch.index ?? 0;
|
|
151
153
|
detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
|
|
152
154
|
}
|
|
153
|
-
const preamble = beforeMilestones
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]*\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
155
|
+
const preamble = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(beforeMilestones, 'details')
|
|
156
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
157
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
157
158
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
158
159
|
return detailsSection
|
|
159
160
|
? preamble + currentSection + '\n' + detailsSection
|
|
@@ -174,7 +175,7 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
|
174
175
|
}
|
|
175
176
|
function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
176
177
|
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
177
|
-
const headingPattern = new RegExp(`^(?:\\[[^\\]]
|
|
178
|
+
const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
178
179
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
179
180
|
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
180
181
|
if (headingIndex === -1)
|
|
@@ -197,21 +198,6 @@ function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
|
197
198
|
section,
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
|
-
function roadmapPhaseLookupSources(phaseNum) {
|
|
201
|
-
const sources = [];
|
|
202
|
-
const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
|
|
203
|
-
if (exactSource)
|
|
204
|
-
sources.push(exactSource);
|
|
205
|
-
const numericSource = phaseMarkdownRegexSource(phaseNum);
|
|
206
|
-
// Source order matters: the bare numeric source is tried before the
|
|
207
|
-
// prefix-tolerant form so that a canonical bare heading ("Phase 117:") is
|
|
208
|
-
// preferred over a drifted prefixed heading ("Phase MANIFOLD-117:") when
|
|
209
|
-
// both exist in the same ROADMAP. The prefix-tolerant form is the fallback
|
|
210
|
-
// that handles the drifted-only case.
|
|
211
|
-
sources.push(numericSource);
|
|
212
|
-
sources.push(`${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${numericSource}`);
|
|
213
|
-
return [...new Set(sources)];
|
|
214
|
-
}
|
|
215
201
|
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
216
202
|
if (!phaseNum)
|
|
217
203
|
return null;
|
|
@@ -323,7 +309,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
323
309
|
throw new Error('missing');
|
|
324
310
|
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
325
311
|
const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
|
326
|
-
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]
|
|
312
|
+
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
|
|
327
313
|
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
|
|
328
314
|
console.warn('[gsd] Deprecated: free-form ROADMAP.md detected (no versioned milestone headings). ' +
|
|
329
315
|
'The project has phase_id_convention set to "milestone-prefixed" in config.json but the ' +
|
|
@@ -378,8 +364,8 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
378
364
|
}
|
|
379
365
|
// Use tokenizeHeadings (fence-aware) instead of stripFencedLines + regex.
|
|
380
366
|
// T4 seam migration: phase headings inside fences are excluded automatically.
|
|
381
|
-
// #1729: `(?:\s*\([^)\n]
|
|
382
|
-
const phaseHeadingPattern = /^(?:\[[^\]]
|
|
367
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
368
|
+
const phaseHeadingPattern = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/i;
|
|
383
369
|
for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(roadmap)) {
|
|
384
370
|
if (h.level < 2 || h.level > 4)
|
|
385
371
|
continue;
|
|
@@ -401,8 +387,13 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
401
387
|
return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
|
|
402
388
|
}
|
|
403
389
|
const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
|
|
390
|
+
// #2043: milestone-prefixed sub-phase components must be zero-padded (≥2 digits)
|
|
391
|
+
// — "-\d{2,}" instead of "-0*\d+" — so a single-digit slug word after the phase
|
|
392
|
+
// number (e.g. dir "46-6-rs-…") captures "46" and is not silently excluded from
|
|
393
|
+
// the milestone as a bogus "46-6" id.
|
|
404
394
|
const numericRe = roadmapUsesHyphenedIds
|
|
405
|
-
? /^0*(\d+(
|
|
395
|
+
? /^0*(\d+(?:-\d{2,})*[A-Za-z]?(?:\.\d+)*)/
|
|
396
|
+
// 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.
|
|
406
397
|
: /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
|
|
407
398
|
function isDirInMilestone(dirName) {
|
|
408
399
|
const m2 = dirName.match(numericRe);
|
|
@@ -19,16 +19,16 @@ const planningWorkspace = require("./planning-workspace.cjs");
|
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
20
|
const phaseIdMod = require("./phase-id.cjs");
|
|
21
21
|
const { planningDir } = planningWorkspace;
|
|
22
|
-
const { stripProjectCodePrefix } = phaseIdMod;
|
|
22
|
+
const { stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
23
23
|
// ─── Regex helpers ────────────────────────────────────────────────────────────
|
|
24
24
|
// Matches legacy phase headings: ### Phase N: Name (also decimal: Phase 2.1:)
|
|
25
25
|
// Captures: (hashes)(spaces)(phase-number)(rest-of-line)
|
|
26
|
-
const LEGACY_PHASE_HEADING_RE =
|
|
26
|
+
const LEGACY_PHASE_HEADING_RE = new RegExp(`^(#{2,4})\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})\\s*:(.*)`, 'i');
|
|
27
27
|
// Matches already-migrated phase headings: ### Phase M-NN: Name
|
|
28
|
-
const MIGRATED_PHASE_HEADING_RE = /^#{2,4}\s*(?:\[[^\]]
|
|
28
|
+
const MIGRATED_PHASE_HEADING_RE = /^#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+\d+-\d{2}\s*:/i;
|
|
29
29
|
// Matches milestone section headings: ## v1.0, ## Roadmap v2.0, ## ✅ v1.0, ## [GSD] v1.0, etc.
|
|
30
30
|
// The optional bracket-token prefix (e.g., [GSD]) must be tested before the emoji group.
|
|
31
|
-
const MILESTONE_HEADING_RE = /^##\s+(?:\[[^\]]
|
|
31
|
+
const MILESTONE_HEADING_RE = /^##\s+(?:\[[^\]]{1,200}\]\s+|Roadmap\s+|[✅🚧]\s*)?v(\d+)\.(\d+)(?:\s|:)/iu;
|
|
32
32
|
// ─── Pure computation helpers ─────────────────────────────────────────────────
|
|
33
33
|
/**
|
|
34
34
|
* Parse the ROADMAP.md content and build a list of phase entries with their
|
|
@@ -106,7 +106,7 @@ function extractPhaseNumFromDir(dirName) {
|
|
|
106
106
|
const stripped = stripProjectCodePrefix(dirName);
|
|
107
107
|
// Matches: digits + optional letter + optional decimal suffix, followed by '-' or end.
|
|
108
108
|
// e.g. "02.1-hotfix" → "02.1", "01-setup" → "01"
|
|
109
|
-
const m = stripped.match(
|
|
109
|
+
const m = stripped.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})(?:-|$)`, 'i'));
|
|
110
110
|
return m ? m[1] : null;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
@@ -119,7 +119,7 @@ function buildNewDirName(oldDirName, newId, projectCode) {
|
|
|
119
119
|
// Strip existing project_code prefix
|
|
120
120
|
const stripped = stripProjectCodePrefix(oldDirName);
|
|
121
121
|
// Extract slug: everything after "NN-" (the old phase num, including decimal like 02.1)
|
|
122
|
-
const slugMatch = stripped.match(
|
|
122
|
+
const slugMatch = stripped.match(new RegExp(`^${PHASE_NUMBER_TOKEN_SOURCE}-(.*)`, 'i'));
|
|
123
123
|
const slug = slugMatch ? slugMatch[1] : stripped;
|
|
124
124
|
// Build M-NN prefix (zero-pad both parts)
|
|
125
125
|
const [milestoneStr, subStr] = newId.split('-');
|
|
@@ -256,7 +256,7 @@ function computeMigrationPlan(cwd, options = {}) {
|
|
|
256
256
|
continue;
|
|
257
257
|
// Rewrite heading line: "### Phase N: Name" → "### Phase M-NN: Name"
|
|
258
258
|
const oldLine = lines[entry.lineIndex];
|
|
259
|
-
const newLine = oldLine.replace(
|
|
259
|
+
const newLine = oldLine.replace(new RegExp(`^(#{2,4}\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+)${PHASE_NUMBER_TOKEN_SOURCE}(\\s*:)`, 'i'), `$1${mapping.newId}$2`);
|
|
260
260
|
if (newLine !== oldLine) {
|
|
261
261
|
roadmapEdits.push({ lineIndex: entry.lineIndex, from: oldLine, to: newLine });
|
|
262
262
|
}
|
|
@@ -274,7 +274,7 @@ function computeMigrationPlan(cwd, options = {}) {
|
|
|
274
274
|
if (roadmapEdits.some(e => e.lineIndex === i))
|
|
275
275
|
continue;
|
|
276
276
|
// Match checklist items: "- [ ] **Phase N:**" or "- [x] Phase N:" (also decimal)
|
|
277
|
-
const checklistMatch = line.match(
|
|
277
|
+
const checklistMatch = line.match(new RegExp(`^(\\s*-\\s*\\[[ x]\\]\\s*\\*{0,2}Phase\\s+)(${PHASE_NUMBER_TOKEN_SOURCE})(\\s*[:\\s*])`, 'i'));
|
|
278
278
|
if (checklistMatch) {
|
|
279
279
|
const legacyNum = checklistMatch[2];
|
|
280
280
|
const cIntPart = parseInt(legacyNum, 10);
|
|
@@ -300,7 +300,7 @@ function computeMigrationPlan(cwd, options = {}) {
|
|
|
300
300
|
newId = found.newId;
|
|
301
301
|
}
|
|
302
302
|
if (newId) {
|
|
303
|
-
const newLine = line.replace(
|
|
303
|
+
const newLine = line.replace(new RegExp(`^(\\s*-\\s*\\[[ x]\\]\\s*\\*{0,2}Phase\\s+)${PHASE_NUMBER_TOKEN_SOURCE}(\\s*[:\\s*])`, 'i'), `$1${newId}$2`);
|
|
304
304
|
if (newLine !== line) {
|
|
305
305
|
roadmapEdits.push({ lineIndex: i, from: line, to: newLine });
|
|
306
306
|
}
|
|
@@ -17,7 +17,7 @@ const ioMod = require("./io.cjs");
|
|
|
17
17
|
const { output, error } = ioMod;
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
19
|
const phaseIdMod = require("./phase-id.cjs");
|
|
20
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource,
|
|
20
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseTokenMatches, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE, roadmapPhaseLookupSources } = phaseIdMod;
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
22
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
23
23
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
@@ -102,7 +102,7 @@ function countPhasePlansAndSummaries(phaseDir) {
|
|
|
102
102
|
*/
|
|
103
103
|
function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
104
104
|
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
105
|
-
const headingPattern = new RegExp(`^(?:\\[[^\\]]
|
|
105
|
+
const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
106
106
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
107
107
|
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
108
108
|
const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
|
|
@@ -174,23 +174,18 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
|
174
174
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
175
175
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
176
176
|
const fullContent = stripShippedMilestones(rawContent);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
// #2121/#2114: iterate the shared lookup-source list (exact → numeric →
|
|
178
|
+
// prefix-tolerant) so this resolver matches getRoadmapPhaseInternal and a
|
|
179
|
+
// bare-number query resolves a drifted project-code-prefixed heading.
|
|
180
|
+
for (const source of roadmapPhaseLookupSources(phaseNum)) {
|
|
181
|
+
const milestoneResult = searchPhaseInContent(milestoneContent, source, phaseNum);
|
|
182
|
+
if (milestoneResult && !milestoneResult.error)
|
|
183
|
+
return milestoneResult.section ?? null;
|
|
184
|
+
const fullResult = searchPhaseInContent(fullContent, source, phaseNum);
|
|
185
|
+
if (fullResult && !fullResult.error)
|
|
186
|
+
return fullResult.section ?? null;
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
|
|
188
|
-
const result = (milestoneResult && !milestoneResult.error)
|
|
189
|
-
? milestoneResult
|
|
190
|
-
: searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
|
|
191
|
-
if (!result || result.error)
|
|
192
|
-
return null;
|
|
193
|
-
return result.section ?? null;
|
|
188
|
+
return null;
|
|
194
189
|
}
|
|
195
190
|
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
|
|
196
191
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
@@ -206,46 +201,35 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
206
201
|
try {
|
|
207
202
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
208
203
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
209
|
-
// #3599 two-pass: when the caller passes a project-code-prefixed ID like
|
|
210
|
-
// `PROJ-42`, try the exact-prefixed heading first (`### Phase PROJ-42:`).
|
|
211
|
-
// If no match, fall back to the #3537 padding-tolerant numeric form so
|
|
212
|
-
// a `CK-01` query still resolves to `### Phase 1:`. Doing this at the
|
|
213
|
-
// call site (instead of inside phaseMarkdownRegexSource) avoids the
|
|
214
|
-
// alternation-order ambiguity where a bare `### Phase 42:` heading in
|
|
215
|
-
// the same document would intercept the match for a `PROJ-42` query.
|
|
216
204
|
const fullContent = stripShippedMilestones(rawContent);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
205
|
+
// #2121/#2114: iterate the shared lookup-source list (exact → numeric →
|
|
206
|
+
// prefix-tolerant) so all three roadmap resolvers share one contract and a
|
|
207
|
+
// bare-number query resolves a drifted `### Phase AB-29:` heading. This
|
|
208
|
+
// preserves the #3599 exact-prefix-first and #3537 padding-tolerant behavior
|
|
209
|
+
// (both now encoded in roadmapPhaseLookupSources' ordering). A clean match
|
|
210
|
+
// (milestone or full, any source) wins immediately; a malformed_roadmap
|
|
211
|
+
// (checklist-only) candidate is surfaced only if no source finds a real
|
|
212
|
+
// heading — so a milestone checklist never blocks a full-roadmap header.
|
|
213
|
+
let malformed = null;
|
|
214
|
+
for (const source of roadmapPhaseLookupSources(phaseNum)) {
|
|
215
|
+
const milestoneResult = searchPhaseInContent(milestoneContent, source, phaseNum);
|
|
216
|
+
if (milestoneResult && !milestoneResult.error) {
|
|
217
|
+
output(milestoneResult, raw, milestoneResult.section);
|
|
222
218
|
return;
|
|
223
219
|
}
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
output(
|
|
220
|
+
const fullResult = searchPhaseInContent(fullContent, source, phaseNum);
|
|
221
|
+
if (fullResult && !fullResult.error) {
|
|
222
|
+
output(fullResult, raw, fullResult.section);
|
|
227
223
|
return;
|
|
228
224
|
}
|
|
225
|
+
if (!malformed)
|
|
226
|
+
malformed = (milestoneResult?.error ? milestoneResult : (fullResult?.error ? fullResult : null));
|
|
229
227
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
233
|
-
// Search the current milestone slice first, then fall back to full roadmap.
|
|
234
|
-
// A malformed_roadmap result (checklist-only) from the milestone should not
|
|
235
|
-
// block finding a full header match in the wider roadmap content.
|
|
236
|
-
const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
|
|
237
|
-
const result = (milestoneResult && !milestoneResult.error)
|
|
238
|
-
? milestoneResult
|
|
239
|
-
: searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
|
|
240
|
-
if (!result) {
|
|
241
|
-
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (result.error) {
|
|
245
|
-
output(result, raw, '');
|
|
228
|
+
if (malformed) {
|
|
229
|
+
output(malformed, raw, '');
|
|
246
230
|
return;
|
|
247
231
|
}
|
|
248
|
-
output(
|
|
232
|
+
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
249
233
|
}
|
|
250
234
|
catch (e) {
|
|
251
235
|
error('Failed to read ROADMAP.md: ' + e.message);
|
|
@@ -262,8 +246,9 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
262
246
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
263
247
|
const phasesDir = planningPaths(cwd).phases;
|
|
264
248
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
265
|
-
// #1729: `(?:\s*\([^)\n]
|
|
266
|
-
|
|
249
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
250
|
+
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
251
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
|
|
267
252
|
const phases = [];
|
|
268
253
|
let match;
|
|
269
254
|
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
@@ -296,7 +281,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
296
281
|
const restOfContent = content.slice(sectionStart);
|
|
297
282
|
// #3691: `\d` → `\d[\d.]*` so decimal phase headings (e.g. `### Phase 02.3:`) are
|
|
298
283
|
// recognised as section boundaries.
|
|
299
|
-
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]
|
|
284
|
+
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+\d[\d.-]*/i);
|
|
300
285
|
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
301
286
|
const section = content.slice(sectionStart, sectionEnd);
|
|
302
287
|
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
@@ -383,6 +368,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
383
368
|
// The char class must allow `-` (not just `.`) so dash-separated milestone-prefixed
|
|
384
369
|
// IDs (e.g. `1-01`) match the detail-heading scanner above; otherwise they truncate
|
|
385
370
|
// at the dash (`1-01` -> `1`) and every such phase reports a phantom missing detail.
|
|
371
|
+
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
386
372
|
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)/gi;
|
|
387
373
|
const checklistPhases = new Set();
|
|
388
374
|
let checklistMatch;
|
|
@@ -473,7 +459,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
473
459
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
474
460
|
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
475
461
|
// `Plans: N plans` — plain text header
|
|
476
|
-
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
462
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
477
463
|
const planCountText = isComplete
|
|
478
464
|
? `${summaryCount}/${planCount} plans complete`
|
|
479
465
|
: `${summaryCount}/${planCount} plans executed`;
|
|
@@ -530,8 +516,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
530
516
|
//
|
|
531
517
|
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
532
518
|
// Pattern B: fallback to bold summary when no bare header exists.
|
|
533
|
-
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
534
|
-
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
519
|
+
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
520
|
+
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
535
521
|
const sortedMissing = [...missingPlans].sort();
|
|
536
522
|
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
537
523
|
const inserter = (match) => `${match}\n${newRows}`;
|