@opengsd/gsd-core 1.4.2 → 1.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +11 -2
- package/agents/gsd-planner.md +11 -7
- package/agents/gsd-verifier.md +3 -3
- package/bin/install.js +947 -149
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +6 -5
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +417 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +57 -1817
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/installer-migrations/004-prune-stale-pristine-snapshots.cjs +123 -0
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/io.cjs +167 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +4 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +96 -9
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +2 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +40 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/verify.cjs +1 -1
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +3 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +57 -7
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +86 -15
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +3 -2
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +27 -2
- package/gsd-core/workflows/plan-review-convergence.md +76 -63
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/reapply-patches.md +18 -1
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +1 -1
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +20 -15
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +7 -2
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +9 -2
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +1808 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +12 -4
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/run-tests.cjs +79 -16
- package/scripts/secret-scan.sh +3 -3
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Configuration Module —
|
|
4
|
-
*
|
|
3
|
+
* Configuration Module — legacy-key normalization, defaults merge, and explicit
|
|
4
|
+
* on-disk migration. Pure normalization primitives consumed by config-loader.cjs
|
|
5
|
+
* and config-schema.cjs. `loadConfig` was extracted to config-loader.cjs per
|
|
6
|
+
* ADR-857 phase 2e (#885) and removed from this module per #893.
|
|
5
7
|
*
|
|
6
8
|
* ADR-457 build-at-publish: the hand-written bin/lib/configuration.cjs collapsed
|
|
7
9
|
* to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
|
@@ -9,7 +11,6 @@
|
|
|
9
11
|
*/
|
|
10
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
13
|
exports.DYNAMIC_KEY_PATTERNS = exports.RUNTIME_STATE_KEYS = exports.VALID_CONFIG_KEYS = exports.CONFIG_DEFAULTS = void 0;
|
|
12
|
-
exports.loadConfig = loadConfig;
|
|
13
14
|
exports.normalizeLegacyKeys = normalizeLegacyKeys;
|
|
14
15
|
exports.mergeDefaults = mergeDefaults;
|
|
15
16
|
exports.migrateOnDisk = migrateOnDisk;
|
|
@@ -160,37 +161,6 @@ function mergeDefaults(parsed) {
|
|
|
160
161
|
const defaults = structuredClone(CONFIG_DEFAULTS);
|
|
161
162
|
return deepMergeConfig(defaults, parsed);
|
|
162
163
|
}
|
|
163
|
-
function loadConfig(cwd, options) {
|
|
164
|
-
const configPath = (0, node_path_1.join)(planningDir(cwd, options?.workstream), 'config.json');
|
|
165
|
-
let raw;
|
|
166
|
-
try {
|
|
167
|
-
raw = (0, node_fs_1.readFileSync)(configPath, 'utf-8');
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// File missing — return defaults
|
|
171
|
-
return mergeDefaults({});
|
|
172
|
-
}
|
|
173
|
-
const trimmed = raw.trim();
|
|
174
|
-
if (trimmed === '') {
|
|
175
|
-
return mergeDefaults({});
|
|
176
|
-
}
|
|
177
|
-
let parsed;
|
|
178
|
-
try {
|
|
179
|
-
parsed = JSON.parse(trimmed);
|
|
180
|
-
}
|
|
181
|
-
catch (err) {
|
|
182
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
-
throw new Error(`Failed to parse config at ${configPath}: ${msg}`);
|
|
184
|
-
}
|
|
185
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
186
|
-
throw new Error(`Config at ${configPath} must be a JSON object`);
|
|
187
|
-
}
|
|
188
|
-
const { parsed: normalized, normalizations } = normalizeLegacyKeys(parsed);
|
|
189
|
-
if (options?.onNormalizations && normalizations.length > 0) {
|
|
190
|
-
options.onNormalizations(normalizations);
|
|
191
|
-
}
|
|
192
|
-
return mergeDefaults(normalized);
|
|
193
|
-
}
|
|
194
164
|
function migrateOnDisk(cwd, workstream) {
|
|
195
165
|
const configPath = (0, node_path_1.join)(planningDir(cwd, workstream), 'config.json');
|
|
196
166
|
let raw;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core Utilities — Shared low-level utility primitives
|
|
4
|
+
*
|
|
5
|
+
* ADR-857 rollout phase 2c: extracted from core.cts (issue #877).
|
|
6
|
+
* Owns POSIX path normalization, sub-repo/subdirectory scanning,
|
|
7
|
+
* phase file stats, slug/one-liner/plan-id helpers, and time-ago.
|
|
8
|
+
* Behaviour is preserved byte-for-behaviour from the prior location;
|
|
9
|
+
* only the module boundary moved. core.cjs re-exports every public symbol
|
|
10
|
+
* here under its own `export =` object so existing consumers are unaffected.
|
|
11
|
+
*
|
|
12
|
+
* New imports should pull core-utils helpers from core-utils.cjs directly.
|
|
13
|
+
*
|
|
14
|
+
* Dependencies (leaf modules only — no core.cjs, no loadConfig):
|
|
15
|
+
* - node:fs / node:path (stdlib)
|
|
16
|
+
* - ./phase-id.cjs (comparePhaseNum, used by readSubdirectories)
|
|
17
|
+
* - ./planning-workspace.cjs (findContextMdIn, used by getPhaseFileStats)
|
|
18
|
+
*/
|
|
19
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
21
|
+
};
|
|
22
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
+
const phaseIdModule = require("./phase-id.cjs");
|
|
26
|
+
const { comparePhaseNum } = phaseIdModule;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
29
|
+
const { findContextMdIn } = planningWorkspace;
|
|
30
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
31
|
+
/** Normalize a relative path to always use forward slashes (cross-platform). */
|
|
32
|
+
function toPosixPath(p) {
|
|
33
|
+
return p.split(node_path_1.default.sep).join('/');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Scan immediate child directories for separate git repos.
|
|
37
|
+
* Returns a sorted array of directory names that have their own `.git`.
|
|
38
|
+
* Excludes hidden directories and node_modules.
|
|
39
|
+
*/
|
|
40
|
+
function detectSubRepos(cwd) {
|
|
41
|
+
const results = [];
|
|
42
|
+
try {
|
|
43
|
+
const entries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry.isDirectory())
|
|
46
|
+
continue;
|
|
47
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
48
|
+
continue;
|
|
49
|
+
const gitPath = node_path_1.default.join(cwd, entry.name, '.git');
|
|
50
|
+
try {
|
|
51
|
+
if (node_fs_1.default.existsSync(gitPath)) {
|
|
52
|
+
results.push(entry.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { /* ignore */ }
|
|
59
|
+
return results.sort();
|
|
60
|
+
}
|
|
61
|
+
// ─── Summary body helpers ─────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Extract a one-liner from the summary body when it's not in frontmatter.
|
|
64
|
+
*/
|
|
65
|
+
function extractOneLinerFromBody(content) {
|
|
66
|
+
if (!content)
|
|
67
|
+
return null;
|
|
68
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
69
|
+
const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
|
70
|
+
const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m);
|
|
71
|
+
if (!match)
|
|
72
|
+
return null;
|
|
73
|
+
const boldInner = match[1].trim();
|
|
74
|
+
const afterBold = match[2];
|
|
75
|
+
if (/:\s*$/.test(boldInner)) {
|
|
76
|
+
const prose = afterBold.trim();
|
|
77
|
+
return prose.length > 0 ? prose : null;
|
|
78
|
+
}
|
|
79
|
+
return boldInner.length > 0 ? boldInner : null;
|
|
80
|
+
}
|
|
81
|
+
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
|
82
|
+
function pathExistsInternal(cwd, targetPath) {
|
|
83
|
+
const fullPath = node_path_1.default.isAbsolute(targetPath) ? targetPath : node_path_1.default.join(cwd, targetPath);
|
|
84
|
+
try {
|
|
85
|
+
node_fs_1.default.statSync(fullPath);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function generateSlugInternal(text) {
|
|
93
|
+
if (!text)
|
|
94
|
+
return null;
|
|
95
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 60);
|
|
96
|
+
}
|
|
97
|
+
// ─── Phase file helpers ──────────────────────────────────────────────────────
|
|
98
|
+
/** Filter a file list to just PLAN.md / *-PLAN.md entries. */
|
|
99
|
+
function filterPlanFiles(files) {
|
|
100
|
+
return files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
101
|
+
}
|
|
102
|
+
/** Filter a file list to just SUMMARY.md / *-SUMMARY.md entries. */
|
|
103
|
+
function filterSummaryFiles(files) {
|
|
104
|
+
return files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Read a phase directory and return counts/flags for common file types.
|
|
108
|
+
*/
|
|
109
|
+
function getPhaseFileStats(phaseDir) {
|
|
110
|
+
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
111
|
+
return {
|
|
112
|
+
plans: filterPlanFiles(files),
|
|
113
|
+
summaries: filterSummaryFiles(files),
|
|
114
|
+
hasResearch: files.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
|
|
115
|
+
hasContext: findContextMdIn(files) !== null,
|
|
116
|
+
hasVerification: files.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
|
|
117
|
+
hasReviews: files.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read immediate child directories from a path.
|
|
122
|
+
* Returns [] if the path doesn't exist or can't be read.
|
|
123
|
+
* Pass sort=true to apply comparePhaseNum ordering.
|
|
124
|
+
*/
|
|
125
|
+
function readSubdirectories(dirPath, sort = false) {
|
|
126
|
+
try {
|
|
127
|
+
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
128
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
129
|
+
return sort ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
|
|
137
|
+
*/
|
|
138
|
+
function timeAgo(date) {
|
|
139
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
140
|
+
if (seconds < 5)
|
|
141
|
+
return 'just now';
|
|
142
|
+
if (seconds < 60)
|
|
143
|
+
return `${seconds} seconds ago`;
|
|
144
|
+
const minutes = Math.floor(seconds / 60);
|
|
145
|
+
if (minutes === 1)
|
|
146
|
+
return '1 minute ago';
|
|
147
|
+
if (minutes < 60)
|
|
148
|
+
return `${minutes} minutes ago`;
|
|
149
|
+
const hours = Math.floor(minutes / 60);
|
|
150
|
+
if (hours === 1)
|
|
151
|
+
return '1 hour ago';
|
|
152
|
+
if (hours < 24)
|
|
153
|
+
return `${hours} hours ago`;
|
|
154
|
+
const days = Math.floor(hours / 24);
|
|
155
|
+
if (days === 1)
|
|
156
|
+
return '1 day ago';
|
|
157
|
+
if (days < 30)
|
|
158
|
+
return `${days} days ago`;
|
|
159
|
+
const months = Math.floor(days / 30);
|
|
160
|
+
if (months === 1)
|
|
161
|
+
return '1 month ago';
|
|
162
|
+
if (months < 12)
|
|
163
|
+
return `${months} months ago`;
|
|
164
|
+
const years = Math.floor(days / 365);
|
|
165
|
+
if (years === 1)
|
|
166
|
+
return '1 year ago';
|
|
167
|
+
return `${years} years ago`;
|
|
168
|
+
}
|
|
169
|
+
// ─── Plan ID helpers ─────────────────────────────────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* Extract the canonical plan ID from a filename.
|
|
172
|
+
* Private to the core cluster — exported so core.cjs:searchPhaseInDir can
|
|
173
|
+
* import it from this leaf without circular dependency, but NOT re-exported
|
|
174
|
+
* from core.cjs's public `export =` block.
|
|
175
|
+
*/
|
|
176
|
+
function extractCanonicalPlanId(filename) {
|
|
177
|
+
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
|
178
|
+
const parts = base.split('-').filter(Boolean);
|
|
179
|
+
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
|
180
|
+
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
|
181
|
+
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
|
182
|
+
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
|
183
|
+
}
|
|
184
|
+
return base;
|
|
185
|
+
}
|
|
186
|
+
module.exports = {
|
|
187
|
+
toPosixPath,
|
|
188
|
+
detectSubRepos,
|
|
189
|
+
extractOneLinerFromBody,
|
|
190
|
+
pathExistsInternal,
|
|
191
|
+
generateSlugInternal,
|
|
192
|
+
filterPlanFiles,
|
|
193
|
+
filterSummaryFiles,
|
|
194
|
+
getPhaseFileStats,
|
|
195
|
+
readSubdirectories,
|
|
196
|
+
timeAgo,
|
|
197
|
+
extractCanonicalPlanId,
|
|
198
|
+
};
|