@opengsd/gsd-core 1.19.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 +1 -1
- package/agents/gsd-code-fixer.compact.md +5 -5
- package/agents/gsd-code-fixer.md +9 -4
- 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/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +73 -23
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-registry.cjs +284 -93
- package/gsd-core/bin/lib/capability-validator.cjs +15 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/core-utils.cjs +11 -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 +1 -1
- 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/git-info-exclude.cjs +1 -6
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
- 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/init.cjs +180 -67
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -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.cjs +18 -50
- 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 +609 -35
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +2990 -666
- 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 +12 -7
- package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
- package/gsd-core/bin/lib/roadmap.cjs +358 -106
- package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
- package/gsd-core/bin/lib/smart-entry.cjs +22 -11
- package/gsd-core/bin/lib/state-contract.cjs +11 -7
- package/gsd-core/bin/lib/state-document.cjs +332 -23
- package/gsd-core/bin/lib/state-transition.cjs +39 -10
- package/gsd-core/bin/lib/state.cjs +498 -179
- 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/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +3 -0
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +54 -141
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/ship.md +12 -0
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +4 -4
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-windsurf-pre-write.js +4 -4
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +4 -4
- package/hooks/lib/git-probe.js +16 -0
- package/package.json +3 -3
- package/pi/package-agents/gsd-code-fixer.md +9 -4
- package/pi/package-agents/gsd-code-reviewer.md +53 -14
- package/pi/package-agents/gsd-executor.md +8 -4
- package/pi/package-agents/manifest.json +3 -3
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-features.cjs +14 -2
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -15,6 +15,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
16
|
const clock_cjs_1 = require("./clock.cjs");
|
|
17
17
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
18
|
+
const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
|
|
18
19
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
|
|
20
21
|
const io = require("./io.cjs");
|
|
@@ -33,6 +34,8 @@ const roadmapParser = require("./roadmap-parser.cjs");
|
|
|
33
34
|
const coreUtils = require("./core-utils.cjs");
|
|
34
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
35
36
|
const phaseId = require("./phase-id.cjs");
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id-display.cjs is an export= CommonJS module
|
|
38
|
+
const phaseIdDisplayMod = require("./phase-id-display.cjs");
|
|
36
39
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
|
|
37
40
|
const worktreeSafety = require("./worktree-safety.cjs");
|
|
38
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
@@ -93,10 +96,11 @@ const { harvestPriorVerifyCommands } = verifyCommandGrounding;
|
|
|
93
96
|
const { output, error, ERROR_REASON, formatDiagnosticToken } = io;
|
|
94
97
|
const { loadConfig, loadConfigResolved } = configLoader;
|
|
95
98
|
const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
|
|
96
|
-
const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
|
|
99
|
+
const { phaseDirectoryOptions, findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs, } = phaseLocator;
|
|
97
100
|
const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
|
|
98
101
|
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
|
99
|
-
const { comparePhaseNum, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
102
|
+
const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
103
|
+
const { phaseToken } = phaseIdDisplayMod;
|
|
100
104
|
const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
101
105
|
const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, findPhaseSpecIn, resolvePhaseIdConvention, worktreesOptedOut, } = planningWorkspace;
|
|
102
106
|
const { extractFrontmatter, frontmatterBlock } = frontmatterMod;
|
|
@@ -134,8 +138,8 @@ function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
|
|
|
134
138
|
// #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
|
|
135
139
|
// with the #2056 foreign-prefix guard, so every init command gets the same
|
|
136
140
|
// protection without duplicating the guard logic at each call site.
|
|
137
|
-
function guardedFindPhase(cwd, phase, projectCode) {
|
|
138
|
-
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
141
|
+
function guardedFindPhase(cwd, phase, projectCode, phaseNaming) {
|
|
142
|
+
let phaseInfo = findPhaseInternal(cwd, phase, undefined, phaseDirectoryOptions(cwd, { phase_naming: phaseNaming, project_code: projectCode }));
|
|
139
143
|
if (isForeignPrefixedPhaseQuery(phase, projectCode) && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
|
|
140
144
|
phaseInfo = null;
|
|
141
145
|
}
|
|
@@ -902,7 +906,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
902
906
|
error('phase required for init execute-phase');
|
|
903
907
|
}
|
|
904
908
|
const config = loadConfig(cwd);
|
|
905
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
909
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
906
910
|
// #3216: getMilestoneInfo now returns a ScopedResult — `.value` carries the
|
|
907
911
|
// MilestoneInfo (or null on any non-COMPLETE scope). NOT display-only: when
|
|
908
912
|
// `branching_strategy === 'milestone'`, `milestone['version']`/`['name']`
|
|
@@ -1077,7 +1081,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
1077
1081
|
}
|
|
1078
1082
|
const config = loadConfig(cwd);
|
|
1079
1083
|
// #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
|
|
1080
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1084
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
1081
1085
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1082
1086
|
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1083
1087
|
const phaseName = rp['phase_name'];
|
|
@@ -1111,12 +1115,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
1111
1115
|
const phaseNamePlan = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
|
|
1112
1116
|
const rawProjectCodePlan = config.project_code || '';
|
|
1113
1117
|
let expectedPhaseDirPlan = null;
|
|
1114
|
-
|
|
1118
|
+
const namingContextPlan = phaseDirectoryContext(config);
|
|
1119
|
+
const customNamingPlan = customPhaseQuery(phase, namingContextPlan) !== null;
|
|
1120
|
+
if ((!phaseDirPlan || customNamingPlan) && phaseNumberPlan && phaseNamePlan) {
|
|
1115
1121
|
const paddedNum = normalizePhaseName(phaseNumberPlan);
|
|
1116
|
-
const
|
|
1122
|
+
const expectedName = roadmapPhase?.['phase_name'] || phaseNamePlan;
|
|
1123
|
+
const slug = (generateSlugInternal(expectedName) || '').substring(0, 60);
|
|
1117
1124
|
if (slug) {
|
|
1118
|
-
const
|
|
1119
|
-
const dirName = `${prefix}${paddedNum}-${slug}`;
|
|
1125
|
+
const dirName = phaseDirectoryName(customPhaseQuery(phaseNumberPlan, namingContextPlan) ? phaseNumberPlan : paddedNum, slug, rawProjectCodePlan);
|
|
1120
1126
|
// #2376: absolute — see comment on phase_dir below.
|
|
1121
1127
|
expectedPhaseDirPlan = toPosixPath(node_path_1.default.join(planningPaths(cwd).phases, dirName));
|
|
1122
1128
|
}
|
|
@@ -1707,7 +1713,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
1707
1713
|
}
|
|
1708
1714
|
const config = loadConfig(cwd);
|
|
1709
1715
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1710
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1716
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
1711
1717
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1712
1718
|
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1713
1719
|
const phaseName = rp['phase_name'];
|
|
@@ -1814,7 +1820,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
1814
1820
|
*/
|
|
1815
1821
|
function cmdInitCodeReview(cwd, phase, raw, options = {}) {
|
|
1816
1822
|
const config = loadConfig(cwd);
|
|
1817
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1823
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
1818
1824
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1819
1825
|
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1820
1826
|
const rpName = rp['phase_name'];
|
|
@@ -1880,7 +1886,7 @@ function cmdInitCodeReview(cwd, phase, raw, options = {}) {
|
|
|
1880
1886
|
*/
|
|
1881
1887
|
function cmdInitReview(cwd, phase, raw, options = {}) {
|
|
1882
1888
|
const config = loadConfig(cwd);
|
|
1883
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1889
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
1884
1890
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1885
1891
|
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => ({
|
|
1886
1892
|
found: true,
|
|
@@ -1939,7 +1945,7 @@ function cmdInitReview(cwd, phase, raw, options = {}) {
|
|
|
1939
1945
|
*/
|
|
1940
1946
|
function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
|
|
1941
1947
|
const config = loadConfig(cwd);
|
|
1942
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1948
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
1943
1949
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1944
1950
|
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1945
1951
|
const rpName = rp['phase_name'];
|
|
@@ -1995,7 +2001,7 @@ function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
|
|
|
1995
2001
|
}
|
|
1996
2002
|
function cmdInitPhaseOp(cwd, phase, raw) {
|
|
1997
2003
|
const config = loadConfig(cwd);
|
|
1998
|
-
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
2004
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
|
|
1999
2005
|
// #2237: surface ambiguous phase-directory collisions instead of silently
|
|
2000
2006
|
// taking the first match when unrelated projects share a .planning/phases/ tree.
|
|
2001
2007
|
if (phaseInfo?.['ambiguous_matches']) {
|
|
@@ -2598,37 +2604,37 @@ function cmdInitManager(cwd, raw) {
|
|
|
2598
2604
|
}
|
|
2599
2605
|
const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
|
|
2600
2606
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
2607
|
+
const directoryContext = phaseDirectoryOptions(cwd, config, rawContent);
|
|
2601
2608
|
// #3185 (ADR-3180 Decision 1): "which phase directories belong to the
|
|
2602
2609
|
// CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
|
|
2603
2610
|
// routed through it instead of a hand-rolled readdirSync + a separate
|
|
2604
2611
|
// getMilestonePhaseFilter window check (which also never excluded
|
|
2605
2612
|
// sentinels, unlike the owner).
|
|
2606
2613
|
const _checkboxStates = new Map();
|
|
2607
|
-
// #4982:
|
|
2608
|
-
// after the checkbox (tolerating only an optional `**`
|
|
2609
|
-
//
|
|
2610
|
-
//
|
|
2611
|
-
// own phase. First-match-wins (`if (!_checkboxStates.has(...))` below)
|
|
2614
|
+
// #4982: parsePhaseChecklistLine is line-anchored, so the phase label must
|
|
2615
|
+
// be the FIRST thing after the checkbox (tolerating only an optional `**`
|
|
2616
|
+
// bold marker), and a later "Phase N" in the line's prose cannot bind the
|
|
2617
|
+
// checkbox. First-match-wins (`if (!_checkboxStates.has(...))` below)
|
|
2612
2618
|
// additionally guards against a later anchored line (e.g. a per-plan
|
|
2613
2619
|
// sub-entry self-titled "Phase N ...") overwriting an already-recorded
|
|
2614
|
-
// phase's own (first) checkbox
|
|
2615
|
-
//
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
const _cbKey = _cbMatch[phaseGroup];
|
|
2621
|
-
if (!_checkboxStates.has(_cbKey)) {
|
|
2622
|
-
_checkboxStates.set(_cbKey, _cbMatch[1].toLowerCase() === 'x');
|
|
2620
|
+
// phase's own (first) checkbox; anchoring alone does not fully close that
|
|
2621
|
+
// "last-match-wins" composition defect.
|
|
2622
|
+
for (const line of content.split(/\r?\n/)) {
|
|
2623
|
+
const checkbox = parsePhaseChecklistLine(line, phaseIdConvention, directoryContext);
|
|
2624
|
+
if (checkbox && !_checkboxStates.has(checkbox.phaseToken)) {
|
|
2625
|
+
_checkboxStates.set(checkbox.phaseToken, checkbox.checked);
|
|
2623
2626
|
}
|
|
2624
2627
|
}
|
|
2625
2628
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2626
|
-
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${
|
|
2629
|
+
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
2627
2630
|
const phases = [];
|
|
2631
|
+
const bracketIdsByPhaseNumber = new Map();
|
|
2628
2632
|
let match;
|
|
2629
2633
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
2630
2634
|
const bracketId = capturesBracketId ? match[1] : undefined;
|
|
2631
2635
|
const phaseNum = capturesBracketId ? match[2] : match[1];
|
|
2636
|
+
if (bracketId)
|
|
2637
|
+
bracketIdsByPhaseNumber.set(phaseNum, bracketId);
|
|
2632
2638
|
const phaseName = (capturesBracketId ? match[3] : match[2])
|
|
2633
2639
|
.replace(/\(INSERTED\)/i, '')
|
|
2634
2640
|
.trim();
|
|
@@ -2644,7 +2650,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2644
2650
|
}
|
|
2645
2651
|
const sectionStart = match.index;
|
|
2646
2652
|
const restOfContent = content.slice(sectionStart);
|
|
2647
|
-
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}
|
|
2653
|
+
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext)}`, 'i'));
|
|
2648
2654
|
const sectionEnd = nextHeader
|
|
2649
2655
|
? sectionStart + nextHeader.index
|
|
2650
2656
|
: content.length;
|
|
@@ -2674,7 +2680,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2674
2680
|
// of reporting no_directory/phase_complete:false. Five other init
|
|
2675
2681
|
// commands already route through this same primitive; this was the
|
|
2676
2682
|
// holdout private copy (#4793-family declared-owner drift).
|
|
2677
|
-
const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention);
|
|
2683
|
+
const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention, directoryContext);
|
|
2678
2684
|
const dirMatch = located && located['found']
|
|
2679
2685
|
? node_path_1.default.posix.basename(String(located['directory']))
|
|
2680
2686
|
: null;
|
|
@@ -2785,15 +2791,83 @@ function cmdInitManager(cwd, raw) {
|
|
|
2785
2791
|
.filter((p) => p['phase_complete'] === true)
|
|
2786
2792
|
.map((p) => normalizePhaseNumber(p['number'])));
|
|
2787
2793
|
const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
|
|
2788
|
-
const
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2794
|
+
const bracketIdentityKey = (value) => {
|
|
2795
|
+
try {
|
|
2796
|
+
const id = parsePhaseId(value);
|
|
2797
|
+
const subphase = id.subphase ? `.${id.subphase}` : '';
|
|
2798
|
+
return bracketQualifiedKey(`${id.project}.${id.milestone}-${id.phase}${subphase}`, 'bracket');
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
return null;
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
const phaseByBracketIdentity = new Map();
|
|
2805
|
+
const bracketIdentityByPhaseNumber = new Map();
|
|
2806
|
+
const completedBracketIdentities = new Set();
|
|
2807
|
+
if (phaseIdConvention === 'bracket') {
|
|
2808
|
+
for (const phase of phases) {
|
|
2809
|
+
const phaseNumber = phase['number'];
|
|
2810
|
+
const bracketId = bracketIdsByPhaseNumber.get(phaseNumber);
|
|
2811
|
+
const canonicalPhaseToken = phaseToken(phaseNumber);
|
|
2812
|
+
// #4304: key each heading on the reader's canonical
|
|
2813
|
+
// token, not on a strict parse of its rendered display. An accepted
|
|
2814
|
+
// unpadded heading (`### [CK.02] 1:`) gets no display_id, because
|
|
2815
|
+
// parsePhaseId rejects `CK.02-1`, so it had no identity at all and a
|
|
2816
|
+
// checked `[CK.02] 01` row completed it while phase_complete was false.
|
|
2817
|
+
// phaseToken pads the number to `01`, and `CK.02-01` is the form
|
|
2818
|
+
// parsePhaseId accepts; canonical headings key exactly as before.
|
|
2819
|
+
const key = bracketId && canonicalPhaseToken
|
|
2820
|
+
? bracketIdentityKey(`${bracketId}-${canonicalPhaseToken}`)
|
|
2821
|
+
: null;
|
|
2822
|
+
if (!key)
|
|
2823
|
+
continue;
|
|
2824
|
+
bracketIdentityByPhaseNumber.set(normalizePhaseNumber(phaseNumber), key);
|
|
2825
|
+
phaseByBracketIdentity.set(key, phase);
|
|
2826
|
+
if (phase['phase_complete'] === true)
|
|
2827
|
+
completedBracketIdentities.add(key);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
if (phaseIdConvention === 'bracket') {
|
|
2831
|
+
// A checked historical row satisfies only its own qualified identity. In
|
|
2832
|
+
// particular, `[CK.01] 01` must not complete bare/current `[CK.02] 01`.
|
|
2833
|
+
for (const line of rawContent.split(/\r?\n/)) {
|
|
2834
|
+
const checkbox = parsePhaseChecklistLine(line, phaseIdConvention);
|
|
2835
|
+
if (!checkbox?.checked || !checkbox.bracketId)
|
|
2836
|
+
continue;
|
|
2837
|
+
const identity = `[${checkbox.bracketId}] ${checkbox.phaseToken}`;
|
|
2838
|
+
const key = bracketIdentityKey(identity);
|
|
2839
|
+
if (!key)
|
|
2840
|
+
continue;
|
|
2841
|
+
const current = phaseMap.get(normalizePhaseNumber(checkbox.phaseToken));
|
|
2842
|
+
const currentKey = current
|
|
2843
|
+
? bracketIdentityByPhaseNumber.get(normalizePhaseNumber(current['number'])) ?? null
|
|
2844
|
+
: null;
|
|
2845
|
+
if (!current || currentKey !== key || current['phase_complete'] === true) {
|
|
2846
|
+
completedBracketIdentities.add(key);
|
|
2847
|
+
}
|
|
2795
2848
|
}
|
|
2796
2849
|
}
|
|
2850
|
+
else {
|
|
2851
|
+
// Preserve the historical greedy non-bracket fallback byte-for-byte.
|
|
2852
|
+
const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
|
|
2853
|
+
let _allMatch;
|
|
2854
|
+
while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
|
|
2855
|
+
if (!(0, phase_entry_grammar_cjs_1.isPhaseEntryChecked)(_allMatch[0]))
|
|
2856
|
+
continue;
|
|
2857
|
+
const phaseNum = normalizePhaseNumber(_allMatch[1]);
|
|
2858
|
+
const phase = phaseMap.get(phaseNum);
|
|
2859
|
+
if (!phase || phase['phase_complete'] === true) {
|
|
2860
|
+
completedNums.add(phaseNum);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
const currentDependencyNumber = (dependency) => {
|
|
2865
|
+
const identity = bracketIdentityKey(dependency);
|
|
2866
|
+
if (!identity)
|
|
2867
|
+
return normalizePhaseNumber(dependency);
|
|
2868
|
+
const current = phaseByBracketIdentity.get(identity);
|
|
2869
|
+
return current ? normalizePhaseNumber(current['number']) : null;
|
|
2870
|
+
};
|
|
2797
2871
|
function reaches(from, to, visited = new Set()) {
|
|
2798
2872
|
const normalizedFrom = normalizePhaseNumber(from);
|
|
2799
2873
|
const normalizedTo = normalizePhaseNumber(to);
|
|
@@ -2803,15 +2877,18 @@ function cmdInitManager(cwd, raw) {
|
|
|
2803
2877
|
const p = phaseMap.get(normalizedFrom);
|
|
2804
2878
|
if (!p || !p['dep_phases'] || p['dep_phases'].length === 0)
|
|
2805
2879
|
return false;
|
|
2806
|
-
if (p['dep_phases'].some((dep) =>
|
|
2880
|
+
if (p['dep_phases'].some((dep) => currentDependencyNumber(dep) === normalizedTo)) {
|
|
2807
2881
|
return true;
|
|
2808
2882
|
}
|
|
2809
|
-
return p['dep_phases'].some((dep) =>
|
|
2883
|
+
return p['dep_phases'].some((dep) => {
|
|
2884
|
+
const dependencyNumber = currentDependencyNumber(dep);
|
|
2885
|
+
return dependencyNumber === null ? false : reaches(dependencyNumber, to, visited);
|
|
2886
|
+
});
|
|
2810
2887
|
}
|
|
2811
2888
|
function hasDepRelationship(numA, numB) {
|
|
2812
2889
|
return reaches(numA, numB) || reaches(numB, numA);
|
|
2813
2890
|
}
|
|
2814
|
-
// #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
|
|
2891
|
+
// #4764/#4304: a phase reference in depends_on prose is a PHASE-SHAPED token in
|
|
2815
2892
|
// context — directly following "Phase"/"Phases" — never a bare digit run.
|
|
2816
2893
|
// The previous whole-field scrape matched the token grammar against every
|
|
2817
2894
|
// digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
|
|
@@ -2824,9 +2901,27 @@ function cmdInitManager(cwd, raw) {
|
|
|
2824
2901
|
// "Phase 1-3") — silently dropping a REAL dependency would clear
|
|
2825
2902
|
// deps_satisfied prematurely, the dangerous direction. Negation prose
|
|
2826
2903
|
// ("dropped the dependency on Phase 654") is NOT detected: the issue's own
|
|
2827
|
-
// minimum keeps such tokens.
|
|
2828
|
-
|
|
2829
|
-
|
|
2904
|
+
// minimum keeps such tokens. Bracket repositories route through the same
|
|
2905
|
+
// phase-id owner so `[CK.02] 01` retains its complete qualified identity;
|
|
2906
|
+
// bare `01` still resolves against the active milestone. This prevents a
|
|
2907
|
+
// checked historical `[CK.01] 01` from satisfying incomplete `[CK.02] 01`.
|
|
2908
|
+
// Non-bracket extraction remains this manager surface's exact #4764 code
|
|
2909
|
+
// path below, including its case-insensitive token regex. Planning-inspect's
|
|
2910
|
+
// old token regex was case-sensitive, so forcing both through one widened
|
|
2911
|
+
// helper changed its `Phase 1a` bytes from `1` to `1a`.
|
|
2912
|
+
const legacyManagerDependencyTokens = (prose) => {
|
|
2913
|
+
const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
|
|
2914
|
+
const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
|
|
2915
|
+
const tokens = [];
|
|
2916
|
+
let refMatch;
|
|
2917
|
+
while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
|
|
2918
|
+
let tokenMatch;
|
|
2919
|
+
depTokenRe.lastIndex = 0;
|
|
2920
|
+
while ((tokenMatch = depTokenRe.exec(refMatch[1])) !== null)
|
|
2921
|
+
tokens.push(tokenMatch[0]);
|
|
2922
|
+
}
|
|
2923
|
+
return tokens;
|
|
2924
|
+
};
|
|
2830
2925
|
for (const phase of phases) {
|
|
2831
2926
|
if (!phase['depends_on'] ||
|
|
2832
2927
|
/^none$/i.test(phase['depends_on'].trim())) {
|
|
@@ -2835,24 +2930,39 @@ function cmdInitManager(cwd, raw) {
|
|
|
2835
2930
|
else {
|
|
2836
2931
|
const prose = phase['depends_on'];
|
|
2837
2932
|
const ownNumber = normalizePhaseNumber(phase['number']);
|
|
2933
|
+
const ownIdentity = typeof phase['display_id'] === 'string'
|
|
2934
|
+
? bracketIdentityKey(phase['display_id'])
|
|
2935
|
+
: null;
|
|
2838
2936
|
const depNums = [];
|
|
2839
2937
|
const seen = new Set();
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
if (seen.has(normalized))
|
|
2938
|
+
const dependencyTokens = phaseIdConvention === 'bracket'
|
|
2939
|
+
? extractPhaseDependencyTokens(prose, phaseIdConvention)
|
|
2940
|
+
: legacyManagerDependencyTokens(prose);
|
|
2941
|
+
for (const token of dependencyTokens) {
|
|
2942
|
+
const identity = bracketIdentityKey(token);
|
|
2943
|
+
if (identity) {
|
|
2944
|
+
if (identity === ownIdentity)
|
|
2945
|
+
continue;
|
|
2946
|
+
if (seen.has(identity))
|
|
2850
2947
|
continue;
|
|
2851
|
-
seen.add(
|
|
2852
|
-
depNums.push(
|
|
2948
|
+
seen.add(identity);
|
|
2949
|
+
depNums.push(token);
|
|
2950
|
+
continue;
|
|
2853
2951
|
}
|
|
2952
|
+
const normalized = normalizePhaseNumber(token);
|
|
2953
|
+
if (normalized === ownNumber)
|
|
2954
|
+
continue; // #4764: never the row's own phase
|
|
2955
|
+
if (seen.has(normalized))
|
|
2956
|
+
continue;
|
|
2957
|
+
seen.add(normalized);
|
|
2958
|
+
depNums.push(token);
|
|
2854
2959
|
}
|
|
2855
|
-
phase['deps_satisfied'] = depNums.every((
|
|
2960
|
+
phase['deps_satisfied'] = depNums.every((dependency) => {
|
|
2961
|
+
const identity = bracketIdentityKey(dependency);
|
|
2962
|
+
return identity
|
|
2963
|
+
? completedBracketIdentities.has(identity)
|
|
2964
|
+
: completedNums.has(normalizePhaseNumber(dependency));
|
|
2965
|
+
});
|
|
2856
2966
|
phase['dep_phases'] = depNums;
|
|
2857
2967
|
}
|
|
2858
2968
|
}
|
|
@@ -3316,6 +3426,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3316
3426
|
`Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE);
|
|
3317
3427
|
}
|
|
3318
3428
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
3429
|
+
const directoryContext = phaseDirectoryOptions(cwd, config);
|
|
3319
3430
|
const phases = [];
|
|
3320
3431
|
let currentPhase = null;
|
|
3321
3432
|
let nextPhase = null;
|
|
@@ -3328,7 +3439,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3328
3439
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
|
|
3329
3440
|
// buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
|
|
3330
3441
|
// bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
|
|
3331
|
-
const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention);
|
|
3442
|
+
const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention, directoryContext);
|
|
3332
3443
|
let hm;
|
|
3333
3444
|
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
|
3334
3445
|
roadmapPhaseNums.add(hm[hNumGroup]);
|
|
@@ -3361,12 +3472,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3361
3472
|
// matched (by absorbing the bracket text). This does NOT migrate the site
|
|
3362
3473
|
// onto phaseHeadingPrefixSrcFor / the bracket convention — that stays
|
|
3363
3474
|
// out of scope per the comment above (#4984 revert, pinned census).
|
|
3364
|
-
const cbPattern =
|
|
3475
|
+
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext)})[:\\s]`, 'gim', { state: 'captured' });
|
|
3365
3476
|
let cbm;
|
|
3366
3477
|
while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
|
|
3367
3478
|
const cbKey = cbm[2];
|
|
3368
3479
|
if (!roadmapCheckboxStates.has(cbKey)) {
|
|
3369
|
-
roadmapCheckboxStates.set(cbKey, cbm[1]
|
|
3480
|
+
roadmapCheckboxStates.set(cbKey, (0, phase_entry_grammar_cjs_1.isBoxCharChecked)(cbm[1]));
|
|
3370
3481
|
}
|
|
3371
3482
|
}
|
|
3372
3483
|
}
|
|
@@ -3383,10 +3494,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3383
3494
|
// behavior-preserving).
|
|
3384
3495
|
const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
3385
3496
|
for (const dir of dirs) {
|
|
3386
|
-
const dirMatch = dir.match(new RegExp(`^(${
|
|
3387
|
-
const
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3497
|
+
const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext)})-?(.*)`, 'i'));
|
|
3498
|
+
const query = customPhaseQuery(dir, directoryContext);
|
|
3499
|
+
const owner = query && customDirectoryOwner(dir, query);
|
|
3500
|
+
const phaseNumber = owner ? owner.id : dirMatch ? dirMatch[1] : dir;
|
|
3501
|
+
const phaseName = owner ? owner.slug : dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
3502
|
+
seenPhaseNums.add((phaseKeyFromToken(phaseNumber, directoryContext) ?? phaseNumber).toLowerCase());
|
|
3390
3503
|
const phasePath = node_path_1.default.join(phasesDir, dir);
|
|
3391
3504
|
const phaseFiles = node_fs_1.default.readdirSync(phasePath);
|
|
3392
3505
|
// #5060: Phase Status Module — the single owner of "what state is
|
|
@@ -3431,7 +3544,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3431
3544
|
/* otherwise intentionally empty */
|
|
3432
3545
|
}
|
|
3433
3546
|
for (const [num, name] of roadmapPhaseNames) {
|
|
3434
|
-
const stripped = num
|
|
3547
|
+
const stripped = (phaseKeyFromToken(num, directoryContext) ?? num).toLowerCase();
|
|
3435
3548
|
if (!seenPhaseNums.has(stripped)) {
|
|
3436
3549
|
const checkboxComplete = roadmapCheckboxStates.get(num) === true ||
|
|
3437
3550
|
roadmapCheckboxStates.get(stripped) === true;
|
|
@@ -3461,7 +3574,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3461
3574
|
}
|
|
3462
3575
|
}
|
|
3463
3576
|
}
|
|
3464
|
-
phases.sort((a, b) => comparePhaseNum(a['number'], b['number']));
|
|
3577
|
+
phases.sort((a, b) => comparePhaseNum(a['number'], b['number'], directoryContext));
|
|
3465
3578
|
// #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
|
|
3466
3579
|
// above could claim nextPhase from a stray out-of-order artifact directory
|
|
3467
3580
|
// (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -339,6 +339,9 @@ const ERROR_REASON = Object.freeze({
|
|
|
339
339
|
SDK_MISSING_ARG: 'sdk_missing_arg',
|
|
340
340
|
// workflow / phase
|
|
341
341
|
PHASE_NOT_FOUND: 'phase_not_found',
|
|
342
|
+
PHASE_ID_COLLISION: 'phase_id_collision',
|
|
343
|
+
AMBIGUOUS_PHASE_OWNER: 'ambiguous_phase_owner',
|
|
344
|
+
PHASE_NAMING_NOT_CUSTOM: 'phase_naming_not_custom',
|
|
342
345
|
PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
|
|
343
346
|
PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
|
|
344
347
|
SUMMARY_NO_PLANNING: 'summary_no_planning',
|
|
@@ -471,6 +474,9 @@ const REASON_TO_OUTCOME = Object.freeze({
|
|
|
471
474
|
[ERROR_REASON.SDK_MISSING_ARG]: 'USAGE',
|
|
472
475
|
[ERROR_REASON.GRAPHIFY_INVALID_QUERY]: 'USAGE',
|
|
473
476
|
[ERROR_REASON.USAGE]: 'USAGE',
|
|
477
|
+
[ERROR_REASON.PHASE_ID_COLLISION]: 'USAGE',
|
|
478
|
+
[ERROR_REASON.AMBIGUOUS_PHASE_OWNER]: 'USAGE',
|
|
479
|
+
[ERROR_REASON.PHASE_NAMING_NOT_CUSTOM]: 'USAGE',
|
|
474
480
|
// A specific, named thing does not exist / nothing was there to find —
|
|
475
481
|
// genuine, known emptiness rather than a broken prerequisite.
|
|
476
482
|
[ERROR_REASON.CONFIG_KEY_NOT_FOUND]: 'NO_INPUT',
|
|
@@ -30,6 +30,8 @@ exports.maskFences = maskFences;
|
|
|
30
30
|
exports.editOutsideFences = editOutsideFences;
|
|
31
31
|
exports.replaceSection = replaceSection;
|
|
32
32
|
exports.withSection = withSection;
|
|
33
|
+
exports.headingSliceRange = headingSliceRange;
|
|
34
|
+
exports.editHeadingSlice = editHeadingSlice;
|
|
33
35
|
exports.deleteSection = deleteSection;
|
|
34
36
|
exports.extractXmlTagBodies = extractXmlTagBodies;
|
|
35
37
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
@@ -1035,6 +1037,29 @@ function withSection(content, target, edit, opts = {}) {
|
|
|
1035
1037
|
return content;
|
|
1036
1038
|
return replaceSection(content, section, newBody);
|
|
1037
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Range of the slice that starts at `headings[index]` and runs to the next
|
|
1042
|
+
* heading of level <= `maxLevel` (fence-aware: `headings` comes from
|
|
1043
|
+
* `tokenizeHeadings`). Unlike `collectSection` it keeps the heading line and
|
|
1044
|
+
* every trailing byte, so a caller that edits the slice can splice it back
|
|
1045
|
+
* byte-exact (#5762).
|
|
1046
|
+
*/
|
|
1047
|
+
function headingSliceRange(content, headings, index, maxLevel = 2) {
|
|
1048
|
+
const next = headings.slice(index + 1).find((h) => h.level <= maxLevel);
|
|
1049
|
+
return { start: headings[index].offset, end: next ? next.offset : content.length };
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Run `edit` on `content.slice(range.start, end)` and splice the result back,
|
|
1053
|
+
* where `end` excludes the single newline that precedes the next heading — the
|
|
1054
|
+
* slice the pre-owner writers cut with `search(/\n#{1,2}[ \t]/)` — so a row
|
|
1055
|
+
* inserted at the end of a table keeps the blank line before that heading.
|
|
1056
|
+
*/
|
|
1057
|
+
function editHeadingSlice(content, range, edit) {
|
|
1058
|
+
const end = range.end < content.length && range.end > range.start && content[range.end - 1] === '\n'
|
|
1059
|
+
? range.end - 1
|
|
1060
|
+
: range.end;
|
|
1061
|
+
return content.slice(0, range.start) + edit(content.slice(range.start, end)) + content.slice(end);
|
|
1062
|
+
}
|
|
1038
1063
|
// ─── deleteSection ────────────────────────────────────────────────────────────
|
|
1039
1064
|
/**
|
|
1040
1065
|
* Delete an entire section — the matching heading line ITSELF plus its body —
|
|
@@ -1065,6 +1090,8 @@ function withSection(content, target, edit, opts = {}) {
|
|
|
1065
1090
|
* double-blank gap accumulates where the section used to sit. Content
|
|
1066
1091
|
* elsewhere in the document is never touched.
|
|
1067
1092
|
*
|
|
1093
|
+
* `endOffset` adds an optional caller-owned maximum boundary. It never widens
|
|
1094
|
+
* the heading-level range, and callers that omit it retain the legacy bytes.
|
|
1068
1095
|
* Returns `content` unchanged when no heading matches `headingPredicate`
|
|
1069
1096
|
* (bounded no-op, mirroring `withSection`'s miss behaviour).
|
|
1070
1097
|
*/
|
|
@@ -1103,7 +1130,13 @@ function deleteSection(content, headingPredicate, opts = {}) {
|
|
|
1103
1130
|
}
|
|
1104
1131
|
const eofOffset = acc;
|
|
1105
1132
|
const sectionStart = lineOffsets[target.line - 1]; // start of the target heading LINE itself
|
|
1106
|
-
const
|
|
1133
|
+
const headingBoundedEnd = stopLine <= lines.length ? lineOffsets[stopLine - 1] : eofOffset;
|
|
1134
|
+
const explicitEnd = typeof opts.endOffset === 'number' && Number.isFinite(opts.endOffset)
|
|
1135
|
+
? Math.max(0, Math.min(content.length, opts.endOffset))
|
|
1136
|
+
: headingBoundedEnd;
|
|
1137
|
+
const sectionEnd = Math.min(headingBoundedEnd, explicitEnd);
|
|
1138
|
+
if (sectionEnd <= sectionStart)
|
|
1139
|
+
return content;
|
|
1107
1140
|
const before = content.slice(0, sectionStart);
|
|
1108
1141
|
const after = content.slice(sectionEnd);
|
|
1109
1142
|
// Collapse a resulting blank-line seam to at most one blank line (2 newlines).
|
|
@@ -25,6 +25,7 @@ exports.escapeCell = escapeCell;
|
|
|
25
25
|
exports.appendQuickTaskRow = appendQuickTaskRow;
|
|
26
26
|
exports.migrateQuickTasksTable = migrateQuickTasksTable;
|
|
27
27
|
exports.resetQuickTaskRows = resetQuickTaskRows;
|
|
28
|
+
exports.updateTraceabilityCell = updateTraceabilityCell;
|
|
28
29
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
29
30
|
// ─── Schema registry ──────────────────────────────────────────────────────────
|
|
30
31
|
/**
|
|
@@ -978,5 +979,27 @@ function resetQuickTaskRows(stateContent) {
|
|
|
978
979
|
const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
|
|
979
980
|
return { ok: true, value: { content, cleared, variant: match.label } };
|
|
980
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* `updateTableCell` scoped to REQUIREMENTS.md's `## Traceability` (or
|
|
984
|
+
* `## Traceability Status`) section (#2245 F1, #5762): `updateTableCell` binds
|
|
985
|
+
* to the first table it finds, and the shipped template puts an `## Out of
|
|
986
|
+
* Scope` table (`Feature | Reason`, no `Status` column) before Traceability, so
|
|
987
|
+
* an unscoped call never reaches the real rows. The ONE copy of this slice
|
|
988
|
+
* (it used to live in both milestone.cts and phase.cts); the heading rule is
|
|
989
|
+
* the Progress owner's — first level-2 heading outside fenced code, running to
|
|
990
|
+
* the next level-1/2 heading. No such heading: the whole text, as before.
|
|
991
|
+
*/
|
|
992
|
+
function updateTraceabilityCell(text, match, column, newValue) {
|
|
993
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
|
|
994
|
+
const index = headings.findIndex((h) => h.level === 2 && /^Traceability(?:[ \t]+Status)?\b/i.test(h.text));
|
|
995
|
+
if (index === -1)
|
|
996
|
+
return updateTableCell(text, match, column, newValue);
|
|
997
|
+
let result = { ok: false, reason: 'no matching row' };
|
|
998
|
+
const edited = (0, markdown_sectionizer_cjs_1.editHeadingSlice)(text, (0, markdown_sectionizer_cjs_1.headingSliceRange)(text, headings, index, 2), (scoped) => {
|
|
999
|
+
result = updateTableCell(scoped, match, column, newValue);
|
|
1000
|
+
return result.ok ? result.value : scoped;
|
|
1001
|
+
});
|
|
1002
|
+
return result.ok ? { ok: true, value: edited } : result;
|
|
1003
|
+
}
|
|
981
1004
|
// Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
|
|
982
1005
|
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|
|
@@ -109,10 +109,10 @@ const TOOLS = [
|
|
|
109
109
|
type: 'object',
|
|
110
110
|
properties: {
|
|
111
111
|
family: { type: 'string', description: 'Command family (e.g. "query", "state", "phase").' },
|
|
112
|
-
subcommand: { type: 'string', description: 'Subcommand name.' },
|
|
112
|
+
subcommand: { type: 'string', description: 'Subcommand name, if the command takes one. Omit or leave empty for family-only commands.' },
|
|
113
113
|
args: { type: 'array', items: {}, description: 'Positional args.' },
|
|
114
114
|
},
|
|
115
|
-
required: ['family'
|
|
115
|
+
required: ['family'],
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
118
|
{
|
|
@@ -182,10 +182,13 @@ function callTool(name, args, ctx) {
|
|
|
182
182
|
if (name === 'gsd_invoke_command') {
|
|
183
183
|
const family = asString(a.family);
|
|
184
184
|
const subcommand = asString(a.subcommand);
|
|
185
|
-
if (!family
|
|
186
|
-
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family"
|
|
185
|
+
if (!family) {
|
|
186
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires a non-empty string "family".' }] };
|
|
187
187
|
}
|
|
188
|
-
|
|
188
|
+
if (a.subcommand !== undefined && subcommand === null) {
|
|
189
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "subcommand" when provided.' }] };
|
|
190
|
+
}
|
|
191
|
+
const res = dispatchGsdCommand({ family, subcommand: subcommand ?? undefined, args: Array.isArray(a.args) ? a.args : [], cwd });
|
|
189
192
|
if (!res.ok) {
|
|
190
193
|
return { isError: true, content: [{ type: 'text', text: res.stderr || res.stdout || `dispatch failed (exit ${res.code})` }] };
|
|
191
194
|
}
|