@opengsd/gsd-core 1.19.0 → 1.20.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/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/bin/install.js +36 -40
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +177 -93
- 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 +46 -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/gsd-tool-dir.cjs +134 -0
- 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 +181 -68
- package/gsd-core/bin/lib/install-engine.cjs +9 -9
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- 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 +627 -36
- 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 +3039 -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 +759 -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/runtime-artifact-conversion.cjs +310 -130
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- 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 +51 -15
- package/gsd-core/bin/lib/state.cjs +499 -180
- 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 +27 -21
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- 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-agent-isolation-guard.js +1 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
- 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/dist/lib/isolation-sentinel.js +77 -2
- package/hooks/gsd-agent-isolation-guard.js +1 -1
- package/hooks/gsd-cursor-subagent-start.js +1 -1
- 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/hooks/lib/isolation-sentinel.js +77 -2
- 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-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/changeset/lint.cjs +28 -3
- 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 +10 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +15 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +48 -7
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -21,84 +21,25 @@
|
|
|
21
21
|
* - Issue #4 (open-gsd/gsd-core)
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.locateProgressTable = locateProgressTable;
|
|
25
|
-
exports.normalizeMilestoneToken = normalizeMilestoneToken;
|
|
26
|
-
exports.milestoneVersionOf = milestoneVersionOf;
|
|
27
24
|
exports.deriveProgressFromRoadmap = deriveProgressFromRoadmap;
|
|
28
25
|
exports.clampPercentFromFraction = clampPercentFromFraction;
|
|
29
26
|
exports.clampPercent = clampPercent;
|
|
30
27
|
exports.progressBarFilledCells = progressBarFilledCells;
|
|
31
28
|
exports.renderProgressBar = renderProgressBar;
|
|
32
|
-
|
|
29
|
+
// #5761: the Progress table and its milestone slice have one owner.
|
|
30
|
+
const roadmap_progress_cjs_1 = require("./roadmap-progress.cjs");
|
|
33
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
34
32
|
const phaseIdMod = require("./phase-id.cjs");
|
|
35
33
|
const { isSentinelPhaseId } = phaseIdMod;
|
|
36
34
|
// #5060: `phase-status.cjs` is a load-time leaf (no top-level requires), safe
|
|
37
35
|
// to import here for the ROADMAP Status-cell reader.
|
|
38
36
|
const phase_status_cjs_1 = require("./phase-status.cjs");
|
|
39
|
-
/**
|
|
40
|
-
* #3227: the single owner of "where is this ROADMAP's Progress table".
|
|
41
|
-
* Lifted verbatim out of deriveProgressFromRoadmap so `state-contract.cts`
|
|
42
|
-
* enumerates phases from THE SAME table this module derives its counts from.
|
|
43
|
-
* A second copy of this locator is the DEFECT.GENERATIVE-FIX shape.
|
|
44
|
-
*
|
|
45
|
-
* ADR-2143 §3 ("addressed by NAME, never ordinal"): the Progress table is
|
|
46
|
-
* located via the markdown-table seam's `findTableWithColumns`, which is
|
|
47
|
-
* column-NAME/order/count-invariant — it matches the first table whose header
|
|
48
|
-
* is a SUPERSET of the canonical `Phase` / `Plans Complete` / `Status` /
|
|
49
|
-
* `Completed` names, in any order, tolerating extra/injected unrelated
|
|
50
|
-
* columns (#2137's fast-check property test shuffles headers and injects
|
|
51
|
-
* columns and asserts the derived counts never change). This supersedes the
|
|
52
|
-
* earlier `findTableBySchema` exact-schema lookup, which required an exact
|
|
53
|
-
* canonical column SET+ORDER and returned all-null on any reordering or
|
|
54
|
-
* injection.
|
|
55
|
-
*
|
|
56
|
-
* Scoped to the `## Progress` section when the document has one (#2012 decoy
|
|
57
|
-
* avoidance — a differently-headed table sharing the same column names must
|
|
58
|
-
* not be picked up instead); a headingless milestone slice (#1445) falls back
|
|
59
|
-
* to scanning the whole input, preserving the "Progress table not under a
|
|
60
|
-
* `## Progress` heading, or not the first table in the document, still
|
|
61
|
-
* resolves" behaviour.
|
|
62
|
-
*/
|
|
63
|
-
function locateProgressTable(roadmapContent) {
|
|
64
|
-
const progressMatch = roadmapContent.match(/^##[ \t]+Progress\b/im);
|
|
65
|
-
let scoped = roadmapContent;
|
|
66
|
-
if (progressMatch && progressMatch.index !== undefined) {
|
|
67
|
-
const afterHeading = roadmapContent.slice(progressMatch.index);
|
|
68
|
-
const nextHeading = afterHeading.search(/\n#{1,2}[ \t]/);
|
|
69
|
-
scoped = nextHeading >= 0 ? afterHeading.slice(0, nextHeading) : afterHeading;
|
|
70
|
-
}
|
|
71
|
-
return (0, markdown_table_cjs_1.findTableWithColumns)(scoped, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* #5165: a milestone token compared by identity, not spelling — `v1.1`, `V1.1`
|
|
75
|
-
* and ` 1.1 ` name one milestone. A hand-decorated Progress cell
|
|
76
|
-
* (`**v1.1**`, `` `v1.1` ``, `v1.1 (current)`) still names its first
|
|
77
|
-
* whitespace-separated token. Absent/blank tokens normalize to `''`.
|
|
78
|
-
*/
|
|
79
|
-
function normalizeMilestoneToken(value) {
|
|
80
|
-
// `[v1.1](ROADMAP.md#v11)` names its link text.
|
|
81
|
-
const unlinked = (milestoneVersionOf(value) ?? '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1');
|
|
82
|
-
const first = unlinked.split(/\s+/)[0] ?? '';
|
|
83
|
-
return first.replace(/^[*_`]+|[*_`:,;]+$/g, '').replace(/^v/i, '').toLowerCase();
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* #5165: a STATE.md `milestone:` frontmatter value as a trimmed version string,
|
|
87
|
-
* or null when it is absent, blank or not scalar text.
|
|
88
|
-
*/
|
|
89
|
-
function milestoneVersionOf(value) {
|
|
90
|
-
if (typeof value !== 'string' && typeof value !== 'number')
|
|
91
|
-
return null;
|
|
92
|
-
const text = String(value).trim();
|
|
93
|
-
return text === '' ? null : text;
|
|
94
|
-
}
|
|
95
37
|
/**
|
|
96
38
|
* Derive completed_phases, total_phases, and total_plans from ROADMAP content.
|
|
97
39
|
* Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
|
|
98
40
|
*
|
|
99
|
-
* The Progress table itself is located by `
|
|
100
|
-
*
|
|
101
|
-
* that table.
|
|
41
|
+
* The Progress table itself is located by `locateRoadmapProgress` (the Roadmap
|
|
42
|
+
* Progress Module, ADR-2143 §3) — this function consumes that table's slice.
|
|
102
43
|
*
|
|
103
44
|
* Cells are read by column NAME (`r['Status']`, `r['Plans Complete']`,
|
|
104
45
|
* `r['Phase']`), fixing #2137 (the old position-based regex assumed "Status"
|
|
@@ -117,18 +58,12 @@ function deriveProgressFromRoadmap(roadmapContent, milestoneFilter) {
|
|
|
117
58
|
// `{ ok: false, reason }`, not an exception — so the catch was masking
|
|
118
59
|
// nothing but dead code paths. Removed per ADR-2143 §5; the public
|
|
119
60
|
// `RoadmapProgress` contract (nulls = absent) is unchanged.
|
|
120
|
-
|
|
61
|
+
// #5165: the milestone-grouped Progress table keeps every milestone's rows,
|
|
62
|
+
// shipped ones included; the owner returns only the named milestone's slice.
|
|
63
|
+
// A named milestone that matches no row yields nulls (nothing countable)
|
|
64
|
+
// rather than a foreign count.
|
|
65
|
+
const { table, rows: scopedRows } = (0, roadmap_progress_cjs_1.locateRoadmapProgress)(roadmapContent, { milestone: milestoneFilter });
|
|
121
66
|
if (table) {
|
|
122
|
-
// #5165: the milestone-grouped Progress table (templates/roadmap.md) keeps
|
|
123
|
-
// every milestone's rows, shipped ones included. When the caller names the
|
|
124
|
-
// milestone and the table carries a `Milestone` column, count only that
|
|
125
|
-
// milestone's rows. A table without the column (or a caller without a
|
|
126
|
-
// milestone) keeps the whole-table behavior; a named milestone that matches
|
|
127
|
-
// no row yields nulls (nothing countable) rather than a foreign count.
|
|
128
|
-
const wantMilestone = normalizeMilestoneToken(milestoneFilter);
|
|
129
|
-
const scopedRows = wantMilestone !== '' && table.columns.includes('Milestone')
|
|
130
|
-
? table.rows.filter((r) => normalizeMilestoneToken(r['Milestone']) === wantMilestone)
|
|
131
|
-
: table.rows;
|
|
132
67
|
// Data rows only (exclude sentinel phases 0 and 999.x).
|
|
133
68
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
134
69
|
const dataRows = scopedRows.filter((r) => {
|
|
@@ -6,15 +6,25 @@
|
|
|
6
6
|
* Owns active-phase discovery against the `.planning/phases/` tree
|
|
7
7
|
* (`searchPhaseInDir`, `findPhaseInternal`) and archived-phase-dir
|
|
8
8
|
* enumeration (`getArchivedPhaseDirs`), matching phase ids/tokens against
|
|
9
|
-
* the filesystem.
|
|
10
|
-
*
|
|
9
|
+
* the filesystem. Also owns the silent naming projection and shared writer
|
|
10
|
+
* tail (preflight, containment, directory creation, identity markers).
|
|
11
|
+
* The core.cjs re-export spine
|
|
11
12
|
* was retired in epic #1267; callers import phase-locator helpers directly.
|
|
12
13
|
*
|
|
13
|
-
* Dependencies
|
|
14
|
+
* Dependencies:
|
|
14
15
|
* - node:fs / node:path (stdlib)
|
|
15
|
-
* - ./phase-id.cjs (
|
|
16
|
+
* - ./phase-id.cjs (phaseDirectoryContext, customPhaseQuery, customPhaseDirectoryParts,
|
|
17
|
+
* normalizePhaseName, matchPhaseDirs, phaseNumberForMatch)
|
|
18
|
+
* - ./phase-id-display.cjs (milestone numeric canonicalization)
|
|
16
19
|
* - ./core-utils.cjs (readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath)
|
|
17
|
-
* - ./planning-workspace.cjs (planningDir)
|
|
20
|
+
* - ./planning-workspace.cjs (planningDir, resolvePhaseIdConvention)
|
|
21
|
+
* - ./config-loader.cjs (CONFIG_DEFAULTS, _deepMergeConfig, _getConfigValue;
|
|
22
|
+
* silent file-selection projection; loadConfig for the
|
|
23
|
+
* active bracket project code)
|
|
24
|
+
* - ./runtime-homes.cjs (resolveGsdHome)
|
|
25
|
+
* - ./frontmatter.cjs / ./plan-dependency-graph.cjs (plan dependency evidence)
|
|
26
|
+
* - ./roadmap-parser.cjs / ./planning-scope.cjs (milestone membership/scope)
|
|
27
|
+
* - ./io.cjs / ./shell-command-projection.cjs (typed write errors and files)
|
|
18
28
|
*/
|
|
19
29
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -22,14 +32,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
22
32
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
33
|
const node_path_1 = __importDefault(require("node:path"));
|
|
24
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const ioMod = require("./io.cjs");
|
|
36
|
+
const { error, ERROR_REASON } = ioMod;
|
|
37
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
39
|
const phaseIdModule = require("./phase-id.cjs");
|
|
26
|
-
const {
|
|
40
|
+
const { BRACKET_DIR_PREFIX_SRC, CUSTOM_PHASE_ID_FILE, comparePhaseNum, customPhaseDirectoryParts, customPhaseQuery, foldBracketId, isSentinelPhaseId, matchPhaseDirs, normalizePhaseName, parsePhaseId, phaseDirectoryContext, phaseNumberForMatch, } = phaseIdModule;
|
|
27
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
42
|
const coreUtilsModule = require("./core-utils.cjs");
|
|
29
43
|
const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath, findUnsummarizedPlans } = coreUtilsModule;
|
|
30
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
45
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
32
|
-
const { planningDir, planningRoot } = planningWorkspace;
|
|
46
|
+
const { planningDir, planningRoot, resolveEnvWorkstream, resolvePhaseIdConvention } = planningWorkspace;
|
|
47
|
+
// Naming readers and writers share the loader's scoped-key precedence and defaults.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
|
+
const configLoaderModule = require("./config-loader.cjs");
|
|
50
|
+
const { CONFIG_DEFAULTS, _deepMergeConfig, _getConfigValue, loadConfig } = configLoaderModule;
|
|
51
|
+
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
33
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
53
|
const frontmatterModule = require("./frontmatter.cjs");
|
|
35
54
|
const { extractFrontmatter } = frontmatterModule;
|
|
@@ -38,10 +57,17 @@ const planDependencyGraphModule = require("./plan-dependency-graph.cjs");
|
|
|
38
57
|
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphModule;
|
|
39
58
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
59
|
const roadmapParserModule = require("./roadmap-parser.cjs");
|
|
41
|
-
const { getMilestonePhaseFilter } = roadmapParserModule;
|
|
60
|
+
const { getMilestoneInfo, getMilestonePhaseFilter } = roadmapParserModule;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
|
+
const phaseIdDisplayModule = require("./phase-id-display.cjs");
|
|
63
|
+
const { milestoneToken, phaseToken } = phaseIdDisplayModule;
|
|
42
64
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
65
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
44
66
|
const { SCOPE } = planningScopeMod;
|
|
67
|
+
// #4304: result shaping must distinguish a canonical bracket directory from a
|
|
68
|
+
// legacy directory that matchPhaseDirs deliberately still admits during a
|
|
69
|
+
// bracket migration. Derive that distinction from the exported owner grammar.
|
|
70
|
+
const BRACKET_DIRECTORY_PREFIX_RE = new RegExp(`^${BRACKET_DIR_PREFIX_SRC}`);
|
|
45
71
|
/**
|
|
46
72
|
* #2830: parse a plan file's `depends_on` frontmatter. Returns [] — never
|
|
47
73
|
* throws — on a missing/unreadable/malformed plan or absent field, matching
|
|
@@ -64,6 +90,75 @@ function parsePlanDependsOn(phaseDir, planFile) {
|
|
|
64
90
|
return [];
|
|
65
91
|
}
|
|
66
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the convention and active bracket identity once for every current-
|
|
95
|
+
* checkout phase-directory lookup. A caller-provided convention takes priority
|
|
96
|
+
* over config resolution. Legacy callers receive the exact historical
|
|
97
|
+
* normalization and no context. A partially configured bracket repo degrades
|
|
98
|
+
* to the ambiguity-safe unqualified matcher rather than inventing an identity.
|
|
99
|
+
*/
|
|
100
|
+
function resolvePhaseDirectoryLookup(cwd, phase, conventionOverride, directoryContext) {
|
|
101
|
+
const legacyNormalized = normalizePhaseName(phase);
|
|
102
|
+
const resolvedConvention = conventionOverride ?? resolvePhaseIdConvention(cwd);
|
|
103
|
+
// Custom ids have no padding form: the id is looked up as written.
|
|
104
|
+
if (directoryContext?.phaseNaming === 'custom' && resolvedConvention !== 'bracket') {
|
|
105
|
+
return {
|
|
106
|
+
normalized: typeof phase === 'string' ? phase : legacyNormalized,
|
|
107
|
+
legacyNormalized: undefined,
|
|
108
|
+
convention: undefined,
|
|
109
|
+
bracketContext: undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (resolvedConvention !== 'bracket') {
|
|
113
|
+
return {
|
|
114
|
+
normalized: legacyNormalized,
|
|
115
|
+
legacyNormalized: undefined,
|
|
116
|
+
convention: undefined,
|
|
117
|
+
bracketContext: undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// The bracket writers canonicalize both numeric segments through phaseToken
|
|
121
|
+
// (`1.1` -> `01.01`). Finish the convention-only query normalization at
|
|
122
|
+
// this shared locator boundary while retaining normalizePhaseName's legacy
|
|
123
|
+
// decimal spelling (`01.1`) as the second-pass migration fallback. Every
|
|
124
|
+
// other convention and already-canonical bracket form remains single-pass.
|
|
125
|
+
const normalized = phaseToken(legacyNormalized) ?? legacyNormalized;
|
|
126
|
+
let bracketContext;
|
|
127
|
+
try {
|
|
128
|
+
const config = loadConfig(cwd);
|
|
129
|
+
const project = typeof config['project_code'] === 'string' ? config['project_code'].trim() : '';
|
|
130
|
+
const version = getMilestoneInfo(cwd).value?.version ?? '';
|
|
131
|
+
const milestone = milestoneToken(version);
|
|
132
|
+
if (project && milestone !== null)
|
|
133
|
+
bracketContext = { project, milestone };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Read-side lookup remains non-throwing when config/milestone state is
|
|
137
|
+
// incomplete; matchPhaseDirs will surface ambiguity rather than guess.
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
normalized,
|
|
141
|
+
legacyNormalized: normalized === legacyNormalized ? undefined : legacyNormalized,
|
|
142
|
+
convention: 'bracket',
|
|
143
|
+
bracketContext,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Apply one resolved lookup to a directory set. Bracket queries try the writer-
|
|
148
|
+
* canonical spelling first, then the pre-bracket `normalizePhaseName` spelling
|
|
149
|
+
* only when the canonical pass finds nothing. `matchPhaseDirs` remains the
|
|
150
|
+
* single selection owner for both passes, including its migration-window legacy
|
|
151
|
+
* fallback and ambiguity behavior. Non-bracket lookups have no second spelling.
|
|
152
|
+
*/
|
|
153
|
+
function matchPhaseDirsForLookup(dirs, lookup, bracketContextOverride, directoryContext) {
|
|
154
|
+
const bracketContext = bracketContextOverride === undefined
|
|
155
|
+
? lookup.bracketContext
|
|
156
|
+
: bracketContextOverride;
|
|
157
|
+
const primary = matchPhaseDirs(dirs, lookup.normalized, lookup.convention, bracketContext, directoryContext);
|
|
158
|
+
if (primary.matches.length > 0 || lookup.legacyNormalized === undefined)
|
|
159
|
+
return primary;
|
|
160
|
+
return matchPhaseDirs(dirs, lookup.legacyNormalized, lookup.convention, bracketContext, directoryContext);
|
|
161
|
+
}
|
|
67
162
|
// ─── Phase search helpers ─────────────────────────────────────────────────────
|
|
68
163
|
/**
|
|
69
164
|
* #2855: single source of truth for resolving and enumerating a project's
|
|
@@ -146,7 +241,190 @@ function listArchiveVersionDirs(cwd, wsOverride) {
|
|
|
146
241
|
}
|
|
147
242
|
return out;
|
|
148
243
|
}
|
|
149
|
-
|
|
244
|
+
/** Effective naming is a silent projection; roadmap and marker reads are bounded. */
|
|
245
|
+
function phaseDirectoryOptions(cwd, config, roadmap, ws) {
|
|
246
|
+
// These two flat keys have no legacy aliases and are not inherited from
|
|
247
|
+
// global defaults by loadConfig. Mirror only its file-selection branches,
|
|
248
|
+
// reusing its merge, value and default owners without warnings or write-back.
|
|
249
|
+
// Replace this projection if the loader gains a quiet option; parity tests
|
|
250
|
+
// guard workstream, project, corrupt-file and global-default precedence.
|
|
251
|
+
let naming = config;
|
|
252
|
+
let configError;
|
|
253
|
+
if (naming === undefined) {
|
|
254
|
+
const readConfig = (file) => {
|
|
255
|
+
try {
|
|
256
|
+
const value = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
257
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
258
|
+
return value;
|
|
259
|
+
configError = { path: file, message: 'configuration must be a JSON object' };
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
catch (cause) {
|
|
263
|
+
if (cause.code !== 'ENOENT')
|
|
264
|
+
configError = { path: file, message: cause.message };
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const activeWs = ws === undefined ? resolveEnvWorkstream() : ws?.trim() || null;
|
|
269
|
+
const root = activeWs ? readConfig(node_path_1.default.join(planningRoot(cwd), 'config.json')) : null;
|
|
270
|
+
const scoped = readConfig(node_path_1.default.join(planningDir(cwd, activeWs), 'config.json'));
|
|
271
|
+
// A missing/broken workstream with a usable root recurses with ws:null in
|
|
272
|
+
// loadConfig, including GSD_PROJECT's own config rather than the flat root.
|
|
273
|
+
const parsed = scoped ? (root ? _deepMergeConfig(root, scoped) : scoped)
|
|
274
|
+
: root ? readConfig(node_path_1.default.join(planningDir(cwd, null), 'config.json')) : null;
|
|
275
|
+
naming = {
|
|
276
|
+
phase_naming: _getConfigValue(parsed ?? {}, 'phase_naming') ?? CONFIG_DEFAULTS.phase_naming,
|
|
277
|
+
project_code: _getConfigValue(parsed ?? {}, 'project_code') ?? CONFIG_DEFAULTS.project_code,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const context = phaseDirectoryContext(naming, roadmap ?? '');
|
|
281
|
+
if (configError)
|
|
282
|
+
context.configError = configError;
|
|
283
|
+
if (context.phaseNaming !== 'custom')
|
|
284
|
+
return context;
|
|
285
|
+
// Load directory-owned IDs once for this scope, including archive fallback.
|
|
286
|
+
// Old directories retain known heading hints. Mutations accept a unique
|
|
287
|
+
// owner and refuse ambiguous candidates before any deletion or rename.
|
|
288
|
+
context.directoryIdsByRoot = Object.create(null);
|
|
289
|
+
const unmarkedDirectories = [];
|
|
290
|
+
context.unmarkedDirectories = unmarkedDirectories;
|
|
291
|
+
const roots = [node_path_1.default.join(planningDir(cwd, ws), 'phases'), ...listArchiveVersionDirs(cwd, ws).map(entry => entry.archivePath)];
|
|
292
|
+
for (const root of roots) {
|
|
293
|
+
const ids = Object.create(null);
|
|
294
|
+
context.directoryIdsByRoot[root] = ids;
|
|
295
|
+
const directories = readSubdirectories(root, true);
|
|
296
|
+
for (const dir of directories) {
|
|
297
|
+
let identityFd;
|
|
298
|
+
try {
|
|
299
|
+
const markerPath = node_path_1.default.join(root, dir, CUSTOM_PHASE_ID_FILE);
|
|
300
|
+
const marker = node_fs_1.default.lstatSync(markerPath);
|
|
301
|
+
if (!marker.isFile() || marker.size > 1024)
|
|
302
|
+
continue;
|
|
303
|
+
// Nonblocking/no-follow open also bounds a replacement between lstat and
|
|
304
|
+
// open; fstat validates the actual descriptor before reading at most 1 KiB.
|
|
305
|
+
// Windows may omit these flags: lstat/fstat still require a regular file,
|
|
306
|
+
// but the open is not atomically no-follow there. Adopt Windows no-follow
|
|
307
|
+
// flags here if Node exposes them; no caller should weaken these checks.
|
|
308
|
+
identityFd = node_fs_1.default.openSync(markerPath, node_fs_1.default.constants.O_RDONLY | node_fs_1.default.constants.O_NONBLOCK | node_fs_1.default.constants.O_NOFOLLOW);
|
|
309
|
+
const opened = node_fs_1.default.fstatSync(identityFd);
|
|
310
|
+
if (!opened.isFile() || opened.size > 1024)
|
|
311
|
+
continue;
|
|
312
|
+
const buffer = Buffer.alloc(opened.size);
|
|
313
|
+
const count = node_fs_1.default.readSync(identityFd, buffer, 0, buffer.length, 0);
|
|
314
|
+
const id = buffer.toString('utf8', 0, count).trim();
|
|
315
|
+
if (id) {
|
|
316
|
+
phaseIdModule.phaseDirectoryName(id, '');
|
|
317
|
+
ids[dir] = id;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch { /* An absent or unusable marker supplies no ownership proof. */ }
|
|
321
|
+
finally {
|
|
322
|
+
if (identityFd !== undefined)
|
|
323
|
+
node_fs_1.default.closeSync(identityFd);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
unmarkedDirectories.push(...directories.filter(dir => ids[dir] === undefined));
|
|
327
|
+
}
|
|
328
|
+
// Basenames can repeat across milestones with different ID/slug boundaries.
|
|
329
|
+
// Identity hints span the scope; authoritative records belong to one pool.
|
|
330
|
+
context.directoryIds = context.directoryIdsByRoot[roots[0]];
|
|
331
|
+
context.knownIds = [...new Set(Object.values(context.directoryIdsByRoot).flatMap(ids => Object.values(ids)))];
|
|
332
|
+
if (roadmap !== undefined)
|
|
333
|
+
return context;
|
|
334
|
+
let fd;
|
|
335
|
+
try {
|
|
336
|
+
fd = node_fs_1.default.openSync(node_path_1.default.join(planningDir(cwd, ws), 'ROADMAP.md'), 'r');
|
|
337
|
+
const buffer = Buffer.alloc(Math.min(node_fs_1.default.fstatSync(fd).size, 1024 * 1024));
|
|
338
|
+
const count = node_fs_1.default.readSync(fd, buffer, 0, buffer.length, 0);
|
|
339
|
+
return { ...context, roadmap: buffer.toString('utf8', 0, count) };
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return context;
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
if (fd !== undefined)
|
|
346
|
+
node_fs_1.default.closeSync(fd);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const directoryPoolContexts = new WeakMap();
|
|
350
|
+
/** Select records for this physical pool, reusing its context across queries. */
|
|
351
|
+
function phaseDirectoryContextForRoot(context, root) {
|
|
352
|
+
const ids = context.directoryIdsByRoot?.[root];
|
|
353
|
+
if (!ids || ids === context.directoryIds)
|
|
354
|
+
return context;
|
|
355
|
+
let pools = directoryPoolContexts.get(context);
|
|
356
|
+
if (!pools) {
|
|
357
|
+
pools = new Map();
|
|
358
|
+
directoryPoolContexts.set(context, pools);
|
|
359
|
+
}
|
|
360
|
+
let selected = pools.get(root);
|
|
361
|
+
if (!selected) {
|
|
362
|
+
selected = { ...context, directoryIds: ids };
|
|
363
|
+
pools.set(root, selected);
|
|
364
|
+
}
|
|
365
|
+
return selected;
|
|
366
|
+
}
|
|
367
|
+
/** Usage guidance for every custom-ID writer, using the resolved naming policy. */
|
|
368
|
+
function customPhaseNamingUsage(cwd, context) {
|
|
369
|
+
if (context.phaseNaming === 'custom')
|
|
370
|
+
return null;
|
|
371
|
+
if (context.configError)
|
|
372
|
+
return `Cannot read phase naming from ${context.configError.path}: ${context.configError.message}. Repair the project config before using a custom ID`;
|
|
373
|
+
let message = 'phase_naming is not custom for this project; configure phase_naming as "custom" before using a custom ID';
|
|
374
|
+
try {
|
|
375
|
+
const defaultsPath = node_path_1.default.join((0, runtime_homes_cjs_1.resolveGsdHome)(), '.gsd', 'defaults.json');
|
|
376
|
+
const defaults = JSON.parse(node_fs_1.default.readFileSync(defaultsPath, 'utf8'));
|
|
377
|
+
if (typeof defaults.phase_naming === 'string' && defaults.phase_naming.toLowerCase() === 'custom' && node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'config.json'))) {
|
|
378
|
+
message += `; project config shadows phase_naming from ${defaultsPath}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch { /* Missing/unusable defaults do not change effective naming. */ }
|
|
382
|
+
return message;
|
|
383
|
+
}
|
|
384
|
+
/** Preflight the complete write set, then create directories and identity records. */
|
|
385
|
+
function createPhaseDirectories(cwd, entries, context, gitkeep = true) {
|
|
386
|
+
const phasesRoot = node_path_1.default.resolve(planningDir(cwd), 'phases');
|
|
387
|
+
let prepared;
|
|
388
|
+
try {
|
|
389
|
+
const recorded = entries.filter(entry => entry.recordIdentity ?? context.phaseNaming === 'custom');
|
|
390
|
+
if (recorded.length > 0)
|
|
391
|
+
phaseIdModule.validateCustomPhaseIds(recorded.map(entry => entry.id), context);
|
|
392
|
+
const pendingPaths = new Set();
|
|
393
|
+
prepared = entries.map(entry => {
|
|
394
|
+
// Validate the raw ID before legacy scaffold normalization can discard it.
|
|
395
|
+
phaseIdModule.phaseDirectoryName(entry.id, '', context.projectCode);
|
|
396
|
+
const dirName = phaseIdModule.phaseDirectoryName(entry.directoryId ?? entry.id, entry.slug, context.projectCode);
|
|
397
|
+
const dirPath = node_path_1.default.resolve(phasesRoot, dirName);
|
|
398
|
+
if (node_path_1.default.dirname(dirPath) !== phasesRoot)
|
|
399
|
+
error('Phase directory escapes the phases directory', ERROR_REASON.USAGE);
|
|
400
|
+
const recordIdentity = entry.recordIdentity ?? context.phaseNaming === 'custom';
|
|
401
|
+
// An occupied target is a collision only where a custom writer would
|
|
402
|
+
// record an identity into it. Numeric writers keep the idempotent
|
|
403
|
+
// ensure-directory they always had.
|
|
404
|
+
if (recordIdentity && (pendingPaths.has(dirPath) || node_fs_1.default.lstatSync(dirPath, { throwIfNoEntry: false }) !== undefined)) {
|
|
405
|
+
throw new phaseIdModule.PhaseDirectoryNameError(`Phase directory ${dirName} already exists or is claimed by another pending entry; choose a distinct ID or title`, 'collision');
|
|
406
|
+
}
|
|
407
|
+
pendingPaths.add(dirPath);
|
|
408
|
+
return { ...entry, dirName, dirPath, recordIdentity };
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (cause) {
|
|
412
|
+
if (cause instanceof phaseIdModule.PhaseDirectoryNameError) {
|
|
413
|
+
error(cause.message, cause.kind === 'collision' ? ERROR_REASON.PHASE_ID_COLLISION : ERROR_REASON.USAGE);
|
|
414
|
+
}
|
|
415
|
+
throw cause;
|
|
416
|
+
}
|
|
417
|
+
for (const entry of prepared) {
|
|
418
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(entry.dirPath);
|
|
419
|
+
if (gitkeep)
|
|
420
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(entry.dirPath, '.gitkeep'), '');
|
|
421
|
+
if (entry.recordIdentity)
|
|
422
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(entry.dirPath, CUSTOM_PHASE_ID_FILE), entry.id);
|
|
423
|
+
}
|
|
424
|
+
return prepared;
|
|
425
|
+
}
|
|
426
|
+
function searchPhaseInDir(baseDir, relBase, normalized, convention, bracketContext, legacyNormalized, context = phaseDirectoryContext({})) {
|
|
427
|
+
context = phaseDirectoryContextForRoot(context, baseDir);
|
|
150
428
|
try {
|
|
151
429
|
const dirs = readSubdirectories(baseDir, true);
|
|
152
430
|
// #2528: canonical two-pass selection (exact token match, then the
|
|
@@ -154,7 +432,12 @@ function searchPhaseInDir(baseDir, relBase, normalized, convention) {
|
|
|
154
432
|
// phase-plan-index scans — see phase-id.cts::matchPhaseDirs.
|
|
155
433
|
// #4801: the convention is threaded (optional) so bracket-convention
|
|
156
434
|
// consumers get exact token matching here too.
|
|
157
|
-
const { matches, usedBareFallback } =
|
|
435
|
+
const { matches, usedBareFallback } = matchPhaseDirsForLookup(dirs, {
|
|
436
|
+
normalized,
|
|
437
|
+
legacyNormalized,
|
|
438
|
+
convention: convention === 'bracket' ? 'bracket' : undefined,
|
|
439
|
+
bracketContext: bracketContext ?? undefined,
|
|
440
|
+
}, bracketContext, context);
|
|
158
441
|
if (matches.length === 0)
|
|
159
442
|
return null;
|
|
160
443
|
// #2237: fail loud when multiple directories match the same bare phase
|
|
@@ -182,10 +465,26 @@ function searchPhaseInDir(baseDir, relBase, normalized, convention) {
|
|
|
182
465
|
};
|
|
183
466
|
}
|
|
184
467
|
const match = matches[0];
|
|
185
|
-
const
|
|
468
|
+
const isBracketDirectory = convention === 'bracket'
|
|
469
|
+
&& BRACKET_DIRECTORY_PREFIX_RE.test(foldBracketId(match));
|
|
470
|
+
const resultConvention = isBracketDirectory ? 'bracket' : undefined;
|
|
471
|
+
const custom = convention === 'bracket' ? null : customPhaseQuery(normalized, context);
|
|
472
|
+
const parts = custom ? customPhaseDirectoryParts(match, custom) : null;
|
|
473
|
+
const phaseToken = parts?.id || phaseNumberForMatch(match, usedBareFallback, resultConvention);
|
|
186
474
|
const phaseNumber = phaseToken || normalized;
|
|
187
|
-
|
|
188
|
-
|
|
475
|
+
let afterToken;
|
|
476
|
+
if (isBracketDirectory) {
|
|
477
|
+
const id = parsePhaseId(match);
|
|
478
|
+
const idToken = `${id.phase}${id.subphase ? `.${id.subphase}` : ''}`;
|
|
479
|
+
const bracketPrefix = `${id.project}.${id.milestone}-${idToken}`;
|
|
480
|
+
afterToken = match.slice(bracketPrefix.length).replace(/^-/, '');
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// Exact pre-#4304 legacy shaping, including project-prefixed and bare
|
|
484
|
+
// fallback matches. Bracket mode changes selection, not these bytes.
|
|
485
|
+
afterToken = match.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
|
|
486
|
+
}
|
|
487
|
+
const phaseName = parts?.slug || afterToken || null;
|
|
189
488
|
const phaseDir = node_path_1.default.join(baseDir, match);
|
|
190
489
|
const { plans: unsortedPlans, summaries: unsortedSummaries, hasResearch, hasContext, hasVerification, hasReviews } = getPhaseFileStats(phaseDir);
|
|
191
490
|
const plans = unsortedPlans.sort();
|
|
@@ -283,14 +582,16 @@ function searchPhaseInDir(baseDir, relBase, normalized, convention) {
|
|
|
283
582
|
return null;
|
|
284
583
|
}
|
|
285
584
|
}
|
|
286
|
-
function findPhaseInternal(cwd, phase, convention) {
|
|
585
|
+
function findPhaseInternal(cwd, phase, convention, context) {
|
|
287
586
|
if (!phase)
|
|
288
587
|
return null;
|
|
588
|
+
// Existing callers may omit context; multi-phase readers supply one reusable context.
|
|
589
|
+
context ??= phaseDirectoryOptions(cwd);
|
|
289
590
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
290
|
-
const normalized =
|
|
591
|
+
const { normalized, legacyNormalized, convention: lookupConvention, bracketContext, } = resolvePhaseDirectoryLookup(cwd, phase, convention, context);
|
|
291
592
|
const relPhasesDir = toPosixPath(node_path_1.default.relative(cwd, phasesDir));
|
|
292
593
|
// #4801: convention threaded through to the matcher (see searchPhaseInDir).
|
|
293
|
-
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized,
|
|
594
|
+
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized, lookupConvention, bracketContext, legacyNormalized, context);
|
|
294
595
|
if (current)
|
|
295
596
|
return current;
|
|
296
597
|
// #2855: scope the archived-milestone fallback to the SAME workstream as the
|
|
@@ -304,7 +605,7 @@ function findPhaseInternal(cwd, phase, convention) {
|
|
|
304
605
|
// getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
|
|
305
606
|
for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
|
|
306
607
|
const relBase = toPosixPath(node_path_1.default.relative(cwd, archivePath));
|
|
307
|
-
const result = searchPhaseInDir(archivePath, relBase, normalized,
|
|
608
|
+
const result = searchPhaseInDir(archivePath, relBase, normalized, lookupConvention, null, legacyNormalized, context);
|
|
308
609
|
if (result) {
|
|
309
610
|
result.archived = version;
|
|
310
611
|
return result;
|
|
@@ -366,10 +667,12 @@ function listMilestonePhaseDirs(phasesDir, opts = {}) {
|
|
|
366
667
|
// Without a cwd there is nothing to scope AGAINST — the caller asked for an
|
|
367
668
|
// unscoped read, which is a real answer (mirrors extractCurrentMilestoneScoped's
|
|
368
669
|
// row 1). Sentinels are still refused: they are never milestone phases.
|
|
670
|
+
let directoryContext;
|
|
369
671
|
let inWindow = () => true;
|
|
370
672
|
let scope = SCOPE.COMPLETE;
|
|
371
673
|
if (cwd) {
|
|
372
|
-
|
|
674
|
+
directoryContext = phaseDirectoryOptions(cwd, undefined, undefined, ws);
|
|
675
|
+
const filter = getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws, directoryContext);
|
|
373
676
|
inWindow = filter;
|
|
374
677
|
scope = filter.scope;
|
|
375
678
|
}
|
|
@@ -392,7 +695,7 @@ function listMilestonePhaseDirs(phasesDir, opts = {}) {
|
|
|
392
695
|
}
|
|
393
696
|
const value = names
|
|
394
697
|
.filter((name) => inWindow(name) && !isSentinelPhaseId(name, phaseIdConvention ?? undefined))
|
|
395
|
-
.sort((a, b) => comparePhaseNum(a, b));
|
|
698
|
+
.sort((a, b) => comparePhaseNum(a, b, directoryContext));
|
|
396
699
|
return { value, scope };
|
|
397
700
|
}
|
|
398
701
|
/**
|
|
@@ -497,7 +800,13 @@ function getAllArchivedPhaseDirs(cwd) {
|
|
|
497
800
|
return out;
|
|
498
801
|
}
|
|
499
802
|
module.exports = {
|
|
803
|
+
createPhaseDirectories,
|
|
804
|
+
phaseDirectoryOptions,
|
|
805
|
+
phaseDirectoryContextForRoot,
|
|
806
|
+
customPhaseNamingUsage,
|
|
500
807
|
searchPhaseInDir,
|
|
808
|
+
resolvePhaseDirectoryLookup,
|
|
809
|
+
matchPhaseDirsForLookup,
|
|
501
810
|
findPhaseInternal,
|
|
502
811
|
getArchivedPhaseDirs,
|
|
503
812
|
getAllArchivedPhaseDirs,
|