@smartmemory/compose 0.2.49-beta → 0.2.50-beta
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/bin/compose.js +30 -18
- package/dist/assets/{App-DOerhoDp.js → App-Bxyif1yv.js} +53 -53
- package/dist/assets/{arc-B1TqAC_R.js → arc-DeHak63Z.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-C4hfxNOw.js → architectureDiagram-3BPJPVTR-C5Nkyl9y.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-DRqkGncy.js → blockDiagram-GPEHLZMM-BilnUBG2.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-Dc6swoQo.js → c4Diagram-AAUBKEIU-BsFJmPuS.js} +1 -1
- package/dist/assets/channel-ZgbQ1k0u.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-6HKDSnOw.js → chunk-2J33WTMH-AEu6HIoY.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-CdqXgd68.js → chunk-4BX2VUAB-XO4S2_89.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-D7VauOmU.js → chunk-55IACEB6-CtFSLInj.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-CeKpXDif.js → chunk-727SXJPM-CZeBRpM9.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-y2XL-YHV.js → chunk-AQP2D5EJ-_vmxcBc4.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C1Z5m8DP.js → chunk-FMBD7UC4-BMGd_Y9a.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-DmqZMEQe.js → chunk-ND2GUHAM-BlrOECEr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DY9GU2uD.js → chunk-QZHKN3VN-DHhUJ8RK.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B34g9xzV.js → cose-bilkent-S5V4N54A-C3M0mZqk.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-klZ_GOcw.js → dagre-BM42HDAG-DJlm4fEk.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-D0FmM6Wf.js → diagram-2AECGRRQ-BmcONQG5.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-DYuhby6q.js → diagram-5GNKFQAL-5JbWGIwd.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-CCtQZgal.js → diagram-KO2AKTUF-DAnIvyI0.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-Cd1OqEln.js → diagram-LMA3HP47-Bmh7tvmI.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-CiJrhoxo.js → diagram-OG6HWLK6-hnHVMNnk.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-C05_Iqmb.js → erDiagram-TEJ5UH35-AMDitV0b.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-lXpnb-Ch.js → flowDiagram-I6XJVG4X-C0t2ZGQv.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-EPbL_2Hr.js → ganttDiagram-6RSMTGT7-BPlM-BEN.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-COCoF_vW.js → gitGraphDiagram-PVQCEYII-Lv_YTxnb.js} +1 -1
- package/dist/assets/index-COq21Zym.js +119 -0
- package/dist/assets/{infoDiagram-5YYISTIA-B1sJw6j5.js → infoDiagram-5YYISTIA-Dul1vdUm.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CgdsWdgu.js → ishikawaDiagram-YF4QCWOH-CCBwgjFs.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-CyvHmE3z.js → journeyDiagram-JHISSGLW-zUJN38EU.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-Bqjdyv0T.js → kanban-definition-UN3LZRKU-BNxWi-8J.js} +1 -1
- package/dist/assets/{linear-DecnRXf0.js → linear-CRH9b4g7.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-DM1x9vNY.js → mindmap-definition-RKZ34NQL-DolYwq7Y.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-CRtmCvwV.js → pieDiagram-4H26LBE5-oxNfr2TX.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-CsBLWg27.js → quadrantDiagram-W4KKPZXB-BmZCvD-z.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-DD-Gg_y5.js → requirementDiagram-4Y6WPE33-DsXZ05jo.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-BPdMN8UU.js → sankeyDiagram-5OEKKPKP-DsnYaawP.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-D4AENdze.js → sequenceDiagram-3UESZ5HK-Dt5mu3g8.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-DDplpCf_.js → stateDiagram-AJRCARHV-BFR5ZINQ.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-BPm2AEQj.js → timeline-definition-PNZ67QCA-Bvnhw58e.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-DQCDWwlj.js → vennDiagram-CIIHVFJN-5WnyjZqf.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-WERqOmJt.js → wardley-L42UT6IY-CTxW4xow.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-5teUSOiI.js → wardleyDiagram-YWT4CUSO-K8Y1EOvb.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-Ba05SXXF.js → xychartDiagram-2RQKCTM6-BCShes6B.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +2 -1
- package/lib/build.js +91 -5
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/completion-writer.js +38 -14
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +22 -9
- package/lib/feature-write-guard.js +5 -7
- package/lib/feature-writer.js +3 -3
- package/lib/followup-writer.js +2 -2
- package/lib/get-roadmap.js +2 -2
- package/lib/gsd.js +2 -1
- package/lib/ideabox.js +3 -2
- package/lib/journal-writer.js +3 -3
- package/lib/migrate-roadmap.js +2 -3
- package/lib/paths-core.js +47 -0
- package/lib/project-paths.js +46 -37
- package/lib/roadmap-gen.js +3 -4
- package/lib/roadmap-graph/collect.js +5 -5
- package/lib/roadmap-graph/index.js +2 -2
- package/lib/state-migrations.js +3 -3
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +1 -1
- package/server/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +7 -4
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/vision-routes.js +17 -2
- package/server/vision-server.js +4 -3
- package/server/vision-utils.js +23 -4
- package/dist/assets/channel-KsY1x-7s.js +0 -1
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +0 -1
- package/dist/assets/index-DlgxA8dX.js +0 -123
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +0 -1
package/lib/build.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
|
12
|
-
import { join, resolve, dirname } from 'node:path';
|
|
12
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import { execSync, execFileSync } from 'node:child_process';
|
|
@@ -35,7 +35,7 @@ import { rtkPrefix } from './rtk.js';
|
|
|
35
35
|
|
|
36
36
|
import YAML from 'yaml';
|
|
37
37
|
// feature-json direct imports removed — mutations now go through TrackerProvider (T9)
|
|
38
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
38
|
+
import { loadFeaturesDir, resolveContextPath, resolveRoadmapPath, resolveFeaturesPath } from './project-paths.js';
|
|
39
39
|
|
|
40
40
|
// Lazy provider accessor — avoids circular import risk (factory → local-provider
|
|
41
41
|
// does NOT import build.js, so a static import is safe, but lazy is used for
|
|
@@ -605,10 +605,19 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
605
605
|
// `docs/features/<featureCode>/`. Callers must use this (not inline
|
|
606
606
|
// string concatenation) so the bug-mode path stays in sync.
|
|
607
607
|
// COMP-MCP-MIGRATION-2: feature-mode honors paths.features override.
|
|
608
|
+
// COMP-PATHS-EXTERNAL D7: two representations on purpose.
|
|
609
|
+
// - `featuresDir` stays RELATIVE (loadFeaturesDir): it flows into
|
|
610
|
+
// context.featuresDir → the MCP-enforcement guard, which matches against
|
|
611
|
+
// repo-relative `git status` output. External artifacts live in another
|
|
612
|
+
// repo and are out of that guard's scope by construction; making this
|
|
613
|
+
// absolute would break the relative match. triage consumers are
|
|
614
|
+
// absolute-safe (resolvePathValue) so the relative value is fine there too.
|
|
615
|
+
// - `resolveItemDir` resolves the actual FILESYSTEM dir via
|
|
616
|
+
// resolveFeaturesPath (absolute — handles absolute/../-escaping config).
|
|
608
617
|
const featuresDir = loadFeaturesDir(cwd);
|
|
609
618
|
const resolveItemDir = (code) => isBugMode
|
|
610
619
|
? join(cwd, 'docs', 'bugs', code)
|
|
611
|
-
: join(cwd,
|
|
620
|
+
: join(resolveFeaturesPath(cwd), code);
|
|
612
621
|
|
|
613
622
|
// COMP-MCP-MIGRATION-1: per-build correlation ID stamped onto every audit
|
|
614
623
|
// row written during this run, so `executeShipStep`'s pre-stage scan can
|
|
@@ -663,7 +672,7 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
663
672
|
}
|
|
664
673
|
let composeConfig = {};
|
|
665
674
|
try { composeConfig = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* use defaults */ }
|
|
666
|
-
const contextDirPath =
|
|
675
|
+
const contextDirPath = resolveContextPath(cwd);
|
|
667
676
|
|
|
668
677
|
// COMP-PAR-MERGE-QUEUE-CONSUMER-RETRY (D5): per-task pre-merge gate is opt-in.
|
|
669
678
|
// Resolve ONCE and thread into startFresh's planInputs only when the capability
|
|
@@ -2219,6 +2228,43 @@ export function maybeEmitSectionsAfterPlanGate(stepId, featureDir, opts = {}) {
|
|
|
2219
2228
|
// Ship step — runs git commit in-process (not via agent)
|
|
2220
2229
|
// ---------------------------------------------------------------------------
|
|
2221
2230
|
|
|
2231
|
+
/**
|
|
2232
|
+
* The git repository toplevel that contains `dir`, or null if `dir` is not
|
|
2233
|
+
* inside any git work tree. Used so ship decides commit ownership per-file by
|
|
2234
|
+
* containing repo, not by assuming the workspace root == the repo
|
|
2235
|
+
* (COMP-PATHS-EXTERNAL D6a).
|
|
2236
|
+
*/
|
|
2237
|
+
function gitToplevel(dir) {
|
|
2238
|
+
try {
|
|
2239
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
2240
|
+
cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2241
|
+
}).trim() || null;
|
|
2242
|
+
} catch { return null; }
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/**
|
|
2246
|
+
* For each owned artifact (ROADMAP, the feature folder) that resolves into a
|
|
2247
|
+
* git repo OTHER than the build's repo, log a one-line "commit it there"
|
|
2248
|
+
* notice. v1 does not auto-commit other repos (that is COMP-PATHS-EXTERNAL-1).
|
|
2249
|
+
* Artifacts in the build's own repo, or in no repo at all, produce no notice.
|
|
2250
|
+
*/
|
|
2251
|
+
function noticeExternalArtifacts(cwd, featureCode, buildToplevel) {
|
|
2252
|
+
try {
|
|
2253
|
+
const candidates = [
|
|
2254
|
+
['ROADMAP.md', resolveRoadmapPath(cwd)],
|
|
2255
|
+
['feature folder', join(resolveFeaturesPath(cwd), featureCode)],
|
|
2256
|
+
];
|
|
2257
|
+
for (const [label, abs] of candidates) {
|
|
2258
|
+
if (!existsSync(abs)) continue;
|
|
2259
|
+
const top = gitToplevel(dirname(abs));
|
|
2260
|
+
if (top && top !== buildToplevel) {
|
|
2261
|
+
// eslint-disable-next-line no-console
|
|
2262
|
+
console.warn(`[build/ship] 📝 wrote ${label} in ${top} — commit it there (Compose does not auto-commit other repos in v1)`);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
} catch { /* notice is best-effort */ }
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2222
2268
|
/**
|
|
2223
2269
|
* Execute the ship step: run tests, stage feature files, commit.
|
|
2224
2270
|
* Returns a PhaseResult-shaped object.
|
|
@@ -2240,11 +2286,34 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2240
2286
|
} catch { /* not a git repo */ }
|
|
2241
2287
|
|
|
2242
2288
|
if (!isGitRepo) {
|
|
2289
|
+
// COMP-PATHS-EXTERNAL D6b: there is no repo to commit into (e.g. a
|
|
2290
|
+
// forge-top-shaped workspace), but the lifecycle must still advance —
|
|
2291
|
+
// record a commit-less completion (null-SHA) so status flips to COMPLETE.
|
|
2292
|
+
// Best-effort: a completion failure must not fail ship.
|
|
2293
|
+
let completionWarning = null;
|
|
2294
|
+
if (featureCode) {
|
|
2295
|
+
try {
|
|
2296
|
+
const { recordCompletion } = await import('./completion-writer.js');
|
|
2297
|
+
await recordCompletion(cwd, {
|
|
2298
|
+
feature_code: featureCode,
|
|
2299
|
+
// commit_sha omitted — non-git workspace, stamped with the null-SHA
|
|
2300
|
+
tests_pass: true,
|
|
2301
|
+
files_changed: [],
|
|
2302
|
+
notes: description.split('\n')[0].slice(0, 72),
|
|
2303
|
+
});
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
completionWarning = `completion record failed (${err.code || 'UNKNOWN'}): ${err.message}`;
|
|
2306
|
+
// eslint-disable-next-line no-console
|
|
2307
|
+
console.warn(`[build/ship] ${featureCode}: ${completionWarning}`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2243
2310
|
return {
|
|
2244
2311
|
phase: 'ship',
|
|
2245
2312
|
artifact: 'no-git',
|
|
2246
2313
|
outcome: 'complete',
|
|
2247
|
-
summary: 'No git repository — commit skipped
|
|
2314
|
+
summary: 'No git repository — wrote artifacts, recorded completion (commit skipped)',
|
|
2315
|
+
commit: null,
|
|
2316
|
+
...(completionWarning ? { completionWarning } : {}),
|
|
2248
2317
|
};
|
|
2249
2318
|
}
|
|
2250
2319
|
|
|
@@ -2316,6 +2385,18 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2316
2385
|
const dataDir = join(cwd, '.compose', 'data');
|
|
2317
2386
|
const mode = readEnforcementMode(dataDir);
|
|
2318
2387
|
if (mode !== 'off') {
|
|
2388
|
+
// COMP-PATHS-EXTERNAL: the pre-stage guard matches repo-relative
|
|
2389
|
+
// `git status`, so relocated canon (an external paths.roadmap /
|
|
2390
|
+
// paths.features) is OUT of its scope — unauthorized edits there are
|
|
2391
|
+
// NOT enforced. Surface that visibly instead of failing silently.
|
|
2392
|
+
// Full external-canon enforcement needs cross-repo dirty detection
|
|
2393
|
+
// (tracked with COMP-PATHS-EXTERNAL-1).
|
|
2394
|
+
for (const [label, abs] of [['paths.roadmap', resolveRoadmapPath(cwd)], ['paths.features', resolveFeaturesPath(cwd)]]) {
|
|
2395
|
+
if (relative(cwd, abs).startsWith('..')) {
|
|
2396
|
+
// eslint-disable-next-line no-console
|
|
2397
|
+
console.warn(`[ship] MCP enforcement (${mode}) does NOT cover external ${label} (${abs}) — edits there are unguarded in v1.`);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2319
2400
|
const events = readEvents(cwd, { since: context.buildStartedAt });
|
|
2320
2401
|
const { violations } = scanGuarded({
|
|
2321
2402
|
dirtyFiles: featureFiles,
|
|
@@ -2441,6 +2522,11 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2441
2522
|
}
|
|
2442
2523
|
}
|
|
2443
2524
|
|
|
2525
|
+
// COMP-PATHS-EXTERNAL D6a: if ROADMAP / the feature folder resolved into a
|
|
2526
|
+
// DIFFERENT git repo, they were written but not committed here — tell the
|
|
2527
|
+
// user to commit them there (v1 does not auto-commit other repos).
|
|
2528
|
+
noticeExternalArtifacts(cwd, featureCode, gitToplevel(agentCwd));
|
|
2529
|
+
|
|
2444
2530
|
return {
|
|
2445
2531
|
phase: 'ship',
|
|
2446
2532
|
artifact: sha ?? '',
|
|
@@ -16,6 +16,7 @@ import { captureFingerprint } from './fingerprint.js';
|
|
|
16
16
|
import { captureAnchor } from './anchor.js';
|
|
17
17
|
import { scribePrompt } from './prompts.js';
|
|
18
18
|
import { createCheckpointStore } from './store/index.js';
|
|
19
|
+
import { resolveFeaturesPath } from '../project-paths.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Resolve the checkpoint config block from .compose/compose.json with defaults
|
|
@@ -75,7 +76,7 @@ export function writeCheckpoint(targetRoot, {
|
|
|
75
76
|
const cfg = checkpointConfig(targetRoot);
|
|
76
77
|
const dataDir = path.join(targetRoot, '.compose', 'data');
|
|
77
78
|
const composeDir = path.join(targetRoot, '.compose');
|
|
78
|
-
const featureDir = path.join(targetRoot,
|
|
79
|
+
const featureDir = path.join(resolveFeaturesPath(targetRoot), featureCode);
|
|
79
80
|
|
|
80
81
|
const store = createCheckpointStore(cfg.backend, { dataDir });
|
|
81
82
|
// The prior checkpoint (read before writing) seeds the scribe prompt's context.
|
|
@@ -121,7 +122,7 @@ export function anchorBoundary(targetRoot, { item, trigger, flowId = null } = {}
|
|
|
121
122
|
if (!featureCode) return null;
|
|
122
123
|
const dataDir = path.join(targetRoot, '.compose', 'data');
|
|
123
124
|
const composeDir = path.join(targetRoot, '.compose');
|
|
124
|
-
const featureDir = path.join(targetRoot,
|
|
125
|
+
const featureDir = path.join(resolveFeaturesPath(targetRoot), featureCode);
|
|
125
126
|
const store = createCheckpointStore(cfg.backend, { dataDir });
|
|
126
127
|
return captureAnchor({ item, trigger, cwd: targetRoot, featureDir, composeDir, dataDir, store, flowId });
|
|
127
128
|
} catch (err) {
|
package/lib/completion-writer.js
CHANGED
|
@@ -42,6 +42,13 @@ async function getProvider(cwd) {
|
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
const SHA_RE = /^[0-9a-f]{40}$/i; // FULL SHA only (Decision 9). Case-insensitive input; normalize to lowercase.
|
|
44
44
|
const SHORT_LEN = 8; // Display only — never the dedup key.
|
|
45
|
+
// git's null-SHA. Stamped for a commit-less completion (non-git workspace —
|
|
46
|
+
// COMP-PATHS-EXTERNAL Decision 6b). Schema-valid (matches SHA_RE) and a
|
|
47
|
+
// well-known "no commit" marker, so no schema or downstream change is needed.
|
|
48
|
+
const NULL_SHA = '0'.repeat(40);
|
|
49
|
+
// Monotonic per-process counter so distinct commit-less completions get
|
|
50
|
+
// distinct completion_ids even within the same millisecond.
|
|
51
|
+
let _nocommitSeq = 0;
|
|
45
52
|
const DEFAULT_LIMIT = 50;
|
|
46
53
|
const MAX_LIMIT = 500;
|
|
47
54
|
|
|
@@ -124,6 +131,10 @@ function validateRepoRelativePath(p) {
|
|
|
124
131
|
if (p.includes('\0')) {
|
|
125
132
|
throw inputError(`files_changed: entry contains NUL byte: "${p}"`);
|
|
126
133
|
}
|
|
134
|
+
// files_changed stays repo-relative on purpose — absolute paths are a
|
|
135
|
+
// path-safety hazard (e.g. "/etc/passwd") and no caller needs them: the
|
|
136
|
+
// commit-less (external) ship path passes []. (COMP-PATHS-EXTERNAL: kept the
|
|
137
|
+
// guard rather than widen it for an unused capability.)
|
|
127
138
|
if (p.startsWith('/')) {
|
|
128
139
|
throw inputError(`files_changed: absolute paths not allowed: "${p}"`);
|
|
129
140
|
}
|
|
@@ -151,16 +162,21 @@ function validate(args) {
|
|
|
151
162
|
);
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
// commit_sha —
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
165
|
+
// commit_sha — OPTIONAL (COMP-PATHS-EXTERNAL Decision 6b): omitted/null means
|
|
166
|
+
// a commit-less completion (non-git workspace) and is stamped with NULL_SHA.
|
|
167
|
+
// When PRESENT it must still be a full 40-char hex SHA (Decision 9) — a
|
|
168
|
+
// malformed value (short, non-hex, empty string, non-string) is rejected.
|
|
169
|
+
if (args.commit_sha !== undefined && args.commit_sha !== null) {
|
|
170
|
+
if (typeof args.commit_sha !== 'string' || args.commit_sha.trim().length === 0) {
|
|
171
|
+
throw inputError('completion-writer: commit_sha must be a non-empty 40-char hex SHA when provided');
|
|
172
|
+
}
|
|
173
|
+
const trimmedSha = args.commit_sha.trim();
|
|
174
|
+
if (!SHA_RE.test(trimmedSha)) {
|
|
175
|
+
throw inputError(
|
|
176
|
+
`completion-writer: commit_sha must be a full 40-char hex SHA (Decision 9). ` +
|
|
177
|
+
`Got "${trimmedSha}" (length ${trimmedSha.length}). Short prefixes are rejected on write.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
// tests_pass — strict boolean
|
|
@@ -257,13 +273,21 @@ export async function recordCompletion(cwd, args) {
|
|
|
257
273
|
// 1. Validate
|
|
258
274
|
validate(args);
|
|
259
275
|
|
|
260
|
-
// 2. Normalize SHA
|
|
261
|
-
const commit_sha = args.commit_sha.
|
|
276
|
+
// 2. Normalize SHA (omitted/null ⇒ NULL_SHA sentinel — commit-less completion)
|
|
277
|
+
const commit_sha = (args.commit_sha === undefined || args.commit_sha === null)
|
|
278
|
+
? NULL_SHA
|
|
279
|
+
: args.commit_sha.trim().toLowerCase();
|
|
262
280
|
const commit_sha_short = commit_sha.slice(0, SHORT_LEN);
|
|
263
281
|
const feature_code = args.feature_code;
|
|
264
282
|
|
|
265
|
-
// 3. Compute completion_id
|
|
266
|
-
|
|
283
|
+
// 3. Compute completion_id. For a real commit the SHA makes it stable and
|
|
284
|
+
// dedup-able. For a commit-less completion (NULL_SHA) the SHA is constant,
|
|
285
|
+
// so id off the sha would alias every no-commit completion of a feature
|
|
286
|
+
// onto one record (a re-completion would idempotent-no-op). Derive a
|
|
287
|
+
// distinct id from a timestamp instead. (COMP-PATHS-EXTERNAL D6b)
|
|
288
|
+
const completion_id = (commit_sha === NULL_SHA)
|
|
289
|
+
? `${feature_code}:nocommit:${Date.now().toString(36)}-${(_nocommitSeq++).toString(36)}`
|
|
290
|
+
: `${feature_code}:${commit_sha}`;
|
|
267
291
|
|
|
268
292
|
// 4. Wrap in maybeIdempotent for caller-key path
|
|
269
293
|
return maybeIdempotent({ ...args, cwd }, async () => {
|
package/lib/feature-json.js
CHANGED
|
@@ -10,6 +10,17 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkS
|
|
|
10
10
|
import { join, basename } from 'path';
|
|
11
11
|
import { readdirSync } from 'fs';
|
|
12
12
|
import { assertValidLinkShape, assertLinkTargetsExist } from './feature-write-guard.js';
|
|
13
|
+
import { resolvePathValue } from './paths-core.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the features-dir base. `featuresDir` may be relative-to-cwd (legacy,
|
|
17
|
+
* incl. the default), ../-escaping, or absolute (COMP-PATHS-EXTERNAL D7).
|
|
18
|
+
* Identical to join(cwd, featuresDir) for plain relative dirs.
|
|
19
|
+
* @returns {string} absolute features dir
|
|
20
|
+
*/
|
|
21
|
+
function featuresBase(cwd, featuresDir) {
|
|
22
|
+
return resolvePathValue(cwd, featuresDir, 'features');
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
/**
|
|
15
26
|
* @typedef {object} FeatureJson
|
|
@@ -34,7 +45,7 @@ import { assertValidLinkShape, assertLinkTargetsExist } from './feature-write-gu
|
|
|
34
45
|
* @returns {FeatureJson|null}
|
|
35
46
|
*/
|
|
36
47
|
export function readFeature(cwd, code, featuresDir = 'docs/features') {
|
|
37
|
-
const path = join(cwd, featuresDir, code, 'feature.json');
|
|
48
|
+
const path = join(featuresBase(cwd, featuresDir), code, 'feature.json');
|
|
38
49
|
if (!existsSync(path)) return null;
|
|
39
50
|
try {
|
|
40
51
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -66,7 +77,7 @@ export function writeFeature(cwd, feature, featuresDir = 'docs/features', opts =
|
|
|
66
77
|
const priorLinks = hasTargets ? (readFeature(cwd, feature.code, featuresDir)?.links) : undefined;
|
|
67
78
|
assertLinkTargetsExist(cwd, feature, { allowForwardRefs: opts.allowForwardRefs, priorLinks });
|
|
68
79
|
}
|
|
69
|
-
const dir = join(cwd, featuresDir, feature.code);
|
|
80
|
+
const dir = join(featuresBase(cwd, featuresDir), feature.code);
|
|
70
81
|
mkdirSync(dir, { recursive: true });
|
|
71
82
|
const path = join(dir, 'feature.json');
|
|
72
83
|
feature.updated = new Date().toISOString().slice(0, 10);
|
|
@@ -115,7 +126,7 @@ export function positionSortKey(position) {
|
|
|
115
126
|
* @returns {FeatureJson[]}
|
|
116
127
|
*/
|
|
117
128
|
export function listFeatures(cwd, featuresDir = 'docs/features') {
|
|
118
|
-
const dir =
|
|
129
|
+
const dir = featuresBase(cwd, featuresDir);
|
|
119
130
|
if (!existsSync(dir)) return [];
|
|
120
131
|
|
|
121
132
|
const features = [];
|
package/lib/feature-validator.js
CHANGED
|
@@ -43,6 +43,9 @@ import { featureStatusToVisionStatus } from './status-projection.js';
|
|
|
43
43
|
import { GitHubApi } from './tracker/github-api.js';
|
|
44
44
|
import { ArtifactManager } from '../server/artifact-manager.js';
|
|
45
45
|
import { SchemaValidator } from '../server/schema-validator.js';
|
|
46
|
+
import {
|
|
47
|
+
resolveRoadmapPathFromConfig, resolveFeaturesPathFromConfig, resolveJournalPathFromConfig,
|
|
48
|
+
} from './project-paths.js';
|
|
46
49
|
|
|
47
50
|
const __filename = fileURLToPath(import.meta.url);
|
|
48
51
|
const __dirname = path.dirname(__filename);
|
|
@@ -51,7 +54,6 @@ const FEATURE_JSON_SCHEMA = path.resolve(__dirname, '../contracts/feature-json
|
|
|
51
54
|
const VISION_STATE_SCHEMA = path.resolve(__dirname, '../contracts/vision-state.schema.json');
|
|
52
55
|
const ROADMAP_ROW_SCHEMA = path.resolve(__dirname, '../contracts/roadmap-row.schema.json');
|
|
53
56
|
|
|
54
|
-
const DEFAULT_PATHS = { docs: 'docs', features: 'docs/features', journal: 'docs/journal' };
|
|
55
57
|
|
|
56
58
|
const TERMINAL_STATUSES = new Set(['KILLED', 'SUPERSEDED']);
|
|
57
59
|
const VALID_STATUSES = new Set(['PLANNED', 'IN_PROGRESS', 'PARTIAL', 'COMPLETE', 'SUPERSEDED', 'PARKED', 'BLOCKED', 'KILLED']);
|
|
@@ -109,13 +111,14 @@ function readProjectConfig(cwd) {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
function resolveProjectPaths(cwd) {
|
|
112
|
-
const cfg = readProjectConfig(cwd);
|
|
113
|
-
|
|
114
|
+
const cfg = readProjectConfig(cwd) || {};
|
|
115
|
+
// Artifact paths may be relocated outside cwd (COMP-PATHS-EXTERNAL); resolve
|
|
116
|
+
// through the shared resolver. vision-state + changelog stay at the root.
|
|
114
117
|
return {
|
|
115
|
-
roadmap:
|
|
118
|
+
roadmap: resolveRoadmapPathFromConfig(cwd, cfg),
|
|
116
119
|
visionState: path.join(cwd, '.compose', 'data', 'vision-state.json'),
|
|
117
|
-
features:
|
|
118
|
-
journal:
|
|
120
|
+
features: resolveFeaturesPathFromConfig(cwd, cfg),
|
|
121
|
+
journal: resolveJournalPathFromConfig(cwd, cfg),
|
|
119
122
|
changelog: path.join(cwd, 'CHANGELOG.md'),
|
|
120
123
|
};
|
|
121
124
|
}
|
|
@@ -1125,6 +1128,18 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1125
1128
|
const ctx = loadValidationContext(cwd, options);
|
|
1126
1129
|
const findings = [];
|
|
1127
1130
|
|
|
1131
|
+
// COMP-PATHS-EXTERNAL: a relocated artifact path may resolve into a parent
|
|
1132
|
+
// that does not exist (e.g. the external docs repo isn't checked out). A
|
|
1133
|
+
// not-yet-created leaf dir is fine — writers mkdir it — but an unreachable
|
|
1134
|
+
// PARENT is a configuration error worth surfacing with the resolved path.
|
|
1135
|
+
for (const [key, p] of [['features', ctx.paths.features], ['roadmap', ctx.paths.roadmap]]) {
|
|
1136
|
+
const isExternal = path.relative(cwd, p).startsWith('..');
|
|
1137
|
+
if (isExternal && !fs.existsSync(path.dirname(p))) {
|
|
1138
|
+
findings.push(finding('error', 'CONFIGURED_PATH_UNREACHABLE', null,
|
|
1139
|
+
`configured ${key} path parent does not exist: ${path.dirname(p)} (resolved ${key} = ${p})`));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1128
1143
|
const allCodes = new Set([
|
|
1129
1144
|
...ctx.roadmapByCode.keys(),
|
|
1130
1145
|
...ctx.visionByCode.keys(),
|
|
@@ -1146,9 +1161,7 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1146
1161
|
|
|
1147
1162
|
// --- COMP-ROADMAP-RT: roundtrip + hierarchy ---
|
|
1148
1163
|
if (ctx.featureJsonMode) {
|
|
1149
|
-
const
|
|
1150
|
-
const featuresDir = (cfg && cfg.paths && cfg.paths.features) || DEFAULT_PATHS.features;
|
|
1151
|
-
const features = listFeatures(cwd, featuresDir);
|
|
1164
|
+
const features = listFeatures(cwd, ctx.paths.features);
|
|
1152
1165
|
const roadmapText = fs.existsSync(ctx.paths.roadmap)
|
|
1153
1166
|
? fs.readFileSync(ctx.paths.roadmap, 'utf8')
|
|
1154
1167
|
: '';
|
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'url';
|
|
|
20
20
|
import { SchemaValidator } from '../server/schema-validator.js';
|
|
21
21
|
import { FEATURE_CODE_RE_STRICT } from './feature-code.js';
|
|
22
22
|
import { splitRoadmapCells } from './roadmap-parser.js';
|
|
23
|
+
import { resolveFeaturesPath, resolveRoadmapPath } from './project-paths.js';
|
|
23
24
|
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const SCHEMA_PATH = resolve(__dirname, '../contracts/feature-json.schema.json');
|
|
@@ -77,14 +78,11 @@ export function assertValidLinkShape(feature) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function resolvePaths(cwd) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const cfg = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf-8'));
|
|
83
|
-
if (cfg?.paths?.features) featuresRel = cfg.paths.features;
|
|
84
|
-
} catch { /* no config — use defaults (mirrors resolveProjectPaths) */ }
|
|
81
|
+
// Artifact paths may be relocated outside cwd (COMP-PATHS-EXTERNAL); resolve
|
|
82
|
+
// through the shared resolver. vision-state stays at the root (D4).
|
|
85
83
|
return {
|
|
86
|
-
features:
|
|
87
|
-
roadmap:
|
|
84
|
+
features: resolveFeaturesPath(cwd),
|
|
85
|
+
roadmap: resolveRoadmapPath(cwd),
|
|
88
86
|
visionState: join(cwd, '.compose', 'data', 'vision-state.json'),
|
|
89
87
|
};
|
|
90
88
|
}
|
package/lib/feature-writer.js
CHANGED
|
@@ -21,7 +21,7 @@ import { resolve, normalize, sep, basename, dirname, join } from 'path';
|
|
|
21
21
|
|
|
22
22
|
import { readEvents } from './feature-events.js';
|
|
23
23
|
import { checkOrInsert } from './idempotency.js';
|
|
24
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
24
|
+
import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
|
|
25
25
|
import { checkRoundtrip } from './roadmap-roundtrip.js';
|
|
26
26
|
import { isNarrativeOwned, narrativeOwnedMessage } from './roadmap-config.js';
|
|
27
27
|
import { knownFeatureCodes, FeatureWriteValidationError } from './feature-write-guard.js';
|
|
@@ -238,7 +238,7 @@ function readRoadmapBase(roadmapPath) {
|
|
|
238
238
|
async function roundtripGuard(cwd, provider, mutate, { force, label }) {
|
|
239
239
|
const current = await provider.listFeatures();
|
|
240
240
|
const projected = mutate(current.map(f => ({ ...f })));
|
|
241
|
-
const roadmapPath =
|
|
241
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
242
242
|
// Read the existing ROADMAP as the regen base, but only when it's a readable
|
|
243
243
|
// regular file. If the path is missing, a directory, or otherwise unreadable,
|
|
244
244
|
// treat the base as empty: the guard must never mask the downstream
|
|
@@ -728,7 +728,7 @@ export async function setRoadmapRowStatus(cwd, args) {
|
|
|
728
728
|
throw new Error(`feature-writer: invalid status "${args.status}"`);
|
|
729
729
|
}
|
|
730
730
|
return maybeIdempotent({ ...args, cwd }, async () => {
|
|
731
|
-
const roadmapPath =
|
|
731
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
732
732
|
let text;
|
|
733
733
|
try { text = readFileSync(roadmapPath, 'utf-8'); }
|
|
734
734
|
catch { return { code: args.code, changed: false }; }
|
package/lib/followup-writer.js
CHANGED
|
@@ -28,7 +28,7 @@ import { writeRoadmap } from './roadmap-gen.js';
|
|
|
28
28
|
import { appendEvent } from './feature-events.js';
|
|
29
29
|
import { checkOrInsert } from './idempotency.js';
|
|
30
30
|
import { FEATURE_CODE_RE_STRICT } from './feature-code.js';
|
|
31
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
31
|
+
import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
|
|
32
32
|
|
|
33
33
|
const TERMINAL_STATUSES = new Set(['KILLED', 'SUPERSEDED']);
|
|
34
34
|
const VALID_STATUSES = new Set([
|
|
@@ -509,7 +509,7 @@ async function orchestrate({ cwd, parent_code, parent, args, phase, status, requ
|
|
|
509
509
|
parent_code,
|
|
510
510
|
phase: created?.phase ?? phase,
|
|
511
511
|
position: created?.position,
|
|
512
|
-
roadmap_path:
|
|
512
|
+
roadmap_path: resolveRoadmapPath(cwd),
|
|
513
513
|
scaffolded: scaffolded ?? { created: [], skipped: ['design.md'] },
|
|
514
514
|
link: { kind: 'surfaced_by', from_code: allocated_code, to_code: parent_code },
|
|
515
515
|
};
|
package/lib/get-roadmap.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* - Row parsing reuses parseRoadmap — no second hand-rolled regex.
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
15
|
|
|
17
16
|
import { generateRoadmap } from './roadmap-gen.js';
|
|
18
17
|
import { isNarrativeOwned } from './roadmap-config.js';
|
|
19
18
|
import { parseRoadmap, parseStatusToken } from './roadmap-parser.js';
|
|
19
|
+
import { resolveRoadmapPath } from './project-paths.js';
|
|
20
20
|
|
|
21
21
|
// Whole `**Last updated:** <date>` line — date-only and per-day, so it is
|
|
22
22
|
// stripped (not literal-matched) before drift comparison.
|
|
@@ -56,7 +56,7 @@ function stripVolatile(text) {
|
|
|
56
56
|
export function getRoadmap(root, opts = {}) {
|
|
57
57
|
const { status, phase, format = 'summary', check_drift = true, limit } = opts ?? {};
|
|
58
58
|
|
|
59
|
-
const roadmapPath =
|
|
59
|
+
const roadmapPath = resolveRoadmapPath(root);
|
|
60
60
|
const onDisk = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
|
|
61
61
|
|
|
62
62
|
// Branch on narrative ownership ourselves: read the file directly for narrative
|
package/lib/gsd.js
CHANGED
|
@@ -33,6 +33,7 @@ import { writeGsdState, readGsdState, gsdStatePath, pidAlive, clearGsdHaltArtifa
|
|
|
33
33
|
import { generateGsdMilestoneReport } from './gsd-milestone-report.js';
|
|
34
34
|
import { readHeadlessConfig } from './gsd-headless-config.js';
|
|
35
35
|
import { appendGsdEvent, clearGsdEvents } from './gsd-events.js';
|
|
36
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
36
37
|
|
|
37
38
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
@@ -62,7 +63,7 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// 1. Validate preconditions: blueprint exists + Boundary Map ok
|
|
65
|
-
const blueprintPath = join(cwd,
|
|
66
|
+
const blueprintPath = join(resolveFeaturesPath(cwd), featureCode, 'blueprint.md');
|
|
66
67
|
if (!existsSync(blueprintPath)) {
|
|
67
68
|
throw new Error(
|
|
68
69
|
`runGsd: blueprint missing at ${blueprintPath}. ` +
|
package/lib/ideabox.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
18
18
|
import { join, dirname } from 'node:path'
|
|
19
|
+
import { resolvePathValue } from './paths-core.js'
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Default template
|
|
@@ -540,7 +541,7 @@ export function loadLens(cwd, lensName) {
|
|
|
540
541
|
* Read and parse the ideabox file from the project.
|
|
541
542
|
*/
|
|
542
543
|
export function readIdeabox(cwd, ideaboxPath) {
|
|
543
|
-
const fullPath =
|
|
544
|
+
const fullPath = resolvePathValue(cwd, ideaboxPath, 'ideabox')
|
|
544
545
|
if (!existsSync(fullPath)) {
|
|
545
546
|
// Return empty state
|
|
546
547
|
return { ideas: [], killed: [], nextId: 1 }
|
|
@@ -553,7 +554,7 @@ export function readIdeabox(cwd, ideaboxPath) {
|
|
|
553
554
|
* Write serialized ideabox back to disk.
|
|
554
555
|
*/
|
|
555
556
|
export function writeIdeabox(cwd, ideaboxPath, parsedData) {
|
|
556
|
-
const fullPath =
|
|
557
|
+
const fullPath = resolvePathValue(cwd, ideaboxPath, 'ideabox')
|
|
557
558
|
mkdirSync(dirname(fullPath), { recursive: true })
|
|
558
559
|
writeFileSync(fullPath, serializeIdeabox(parsedData))
|
|
559
560
|
}
|
package/lib/journal-writer.js
CHANGED
|
@@ -29,12 +29,12 @@ import { join, dirname } from 'node:path';
|
|
|
29
29
|
|
|
30
30
|
import { appendEvent, normalizeSince } from './feature-events.js';
|
|
31
31
|
import { checkOrInsert } from './idempotency.js';
|
|
32
|
+
import { resolveJournalPath } from './project-paths.js';
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Constants & regexes
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
|
|
37
|
-
const JOURNAL_DIR = 'docs/journal';
|
|
38
38
|
const INDEX_FILE = 'README.md';
|
|
39
39
|
const ENTRIES_HEADING = '## Entries';
|
|
40
40
|
|
|
@@ -649,7 +649,7 @@ export async function writeJournalEntry(cwd, args) {
|
|
|
649
649
|
validateArgs(args);
|
|
650
650
|
|
|
651
651
|
return maybeIdempotent({ ...args, cwd }, async () => {
|
|
652
|
-
const journalDir =
|
|
652
|
+
const journalDir = resolveJournalPath(cwd);
|
|
653
653
|
const indexPath = join(journalDir, INDEX_FILE);
|
|
654
654
|
|
|
655
655
|
// Pre-flight: read + parse index before any disk mutation.
|
|
@@ -857,7 +857,7 @@ export async function writeJournalEntry(cwd, args) {
|
|
|
857
857
|
* @returns {{ entries: Array, count: number }}
|
|
858
858
|
*/
|
|
859
859
|
export function getJournalEntries(cwd, opts = {}) {
|
|
860
|
-
const journalDir =
|
|
860
|
+
const journalDir = resolveJournalPath(cwd);
|
|
861
861
|
if (!existsSync(journalDir)) return { entries: [], count: 0 };
|
|
862
862
|
|
|
863
863
|
const sinceMs = opts.since !== undefined ? normalizeSince(opts.since) : null;
|
package/lib/migrate-roadmap.js
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFileSync, existsSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
9
|
import { parseRoadmap } from './roadmap-parser.js';
|
|
11
10
|
import { readFeature, writeFeature } from './feature-json.js';
|
|
12
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
11
|
+
import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Migrate ROADMAP.md entries to feature.json files.
|
|
@@ -31,7 +30,7 @@ export function migrateRoadmap(cwd, opts = {}) {
|
|
|
31
30
|
const featuresDir = opts.featuresDir ?? loadFeaturesDir(cwd);
|
|
32
31
|
const externalPrefixes = opts.externalPrefixes ?? [];
|
|
33
32
|
const isExternal = (code) => externalPrefixes.some((p) => code.startsWith(p));
|
|
34
|
-
const roadmapPath =
|
|
33
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
35
34
|
|
|
36
35
|
if (!existsSync(roadmapPath)) {
|
|
37
36
|
throw new Error(`No ROADMAP.md found at ${roadmapPath}`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths-core.js — PURE artifact-path resolution. No fs, no config reading.
|
|
3
|
+
* Single source of truth for default artifact locations (COMP-PATHS-EXTERNAL).
|
|
4
|
+
*
|
|
5
|
+
* `resolvePathValue` uses path.resolve (NOT path.join) so an absolute or
|
|
6
|
+
* ../-escaping `paths.*` override resolves correctly instead of being
|
|
7
|
+
* silently re-rooted under cwd.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_PATHS = Object.freeze({
|
|
12
|
+
docs: 'docs',
|
|
13
|
+
roadmap: 'ROADMAP.md',
|
|
14
|
+
features: 'docs/features',
|
|
15
|
+
journal: 'docs/journal',
|
|
16
|
+
context: 'docs/context',
|
|
17
|
+
ideabox: 'docs/product/ideabox.md',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a configured `paths[key]` value to an absolute, normalized path.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} root Absolute workspace root.
|
|
24
|
+
* @param {*} value The configured paths[key] value (any type).
|
|
25
|
+
* @param {string} key Fallback key into DEFAULT_PATHS when value is unusable.
|
|
26
|
+
* @returns {string} Absolute, normalized path.
|
|
27
|
+
*/
|
|
28
|
+
export function resolvePathValue(root, value, key) {
|
|
29
|
+
const v = (typeof value === 'string' && value.trim().length > 0)
|
|
30
|
+
? value
|
|
31
|
+
: DEFAULT_PATHS[key];
|
|
32
|
+
return path.isAbsolute(v) ? path.normalize(v) : path.resolve(root, v);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Display-safe relativization: a clean root-relative string when `abs` is
|
|
37
|
+
* inside `root`, else the absolute path (never a `../`-prefixed string).
|
|
38
|
+
*
|
|
39
|
+
* @param {string} root Absolute workspace root.
|
|
40
|
+
* @param {string} abs Absolute path to relativize for display.
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function relForDisplay(root, abs) {
|
|
44
|
+
const rel = path.relative(root, abs);
|
|
45
|
+
if (rel === '') return '.';
|
|
46
|
+
return rel.startsWith('..') || path.isAbsolute(rel) ? abs : rel;
|
|
47
|
+
}
|