@smartmemory/compose 0.2.48-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/.claude/skills/compose/SKILL.md +2 -0
- package/.compose-deps.json +13 -0
- package/bin/compose.js +35 -20
- package/dist/assets/{App-D7E7S49Q.js → App-Bxyif1yv.js} +160 -160
- package/dist/assets/{arc-LOW60tiI.js → arc-DeHak63Z.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-BDY00hRy.js → architectureDiagram-3BPJPVTR-C5Nkyl9y.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-DiC_6ktq.js → blockDiagram-GPEHLZMM-BilnUBG2.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-CXZC8woU.js → c4Diagram-AAUBKEIU-BsFJmPuS.js} +1 -1
- package/dist/assets/channel-ZgbQ1k0u.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-b8Rdk4S-.js → chunk-2J33WTMH-AEu6HIoY.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-BSxn03f-.js → chunk-4BX2VUAB-XO4S2_89.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-uUk1WzZ1.js → chunk-55IACEB6-CtFSLInj.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-zvXhT6iZ.js → chunk-727SXJPM-CZeBRpM9.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-B6GCyrIp.js → chunk-AQP2D5EJ-_vmxcBc4.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DlhrqkBp.js → chunk-FMBD7UC4-BMGd_Y9a.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-lo_8kNzw.js → chunk-ND2GUHAM-BlrOECEr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-on-yD-C0.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-DxV7Y2bU.js → cose-bilkent-S5V4N54A-C3M0mZqk.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-BHf9sURy.js → dagre-BM42HDAG-DJlm4fEk.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-R9Puxzlh.js → diagram-2AECGRRQ-BmcONQG5.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-BbjlClKx.js → diagram-5GNKFQAL-5JbWGIwd.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-r89Mx4m1.js → diagram-KO2AKTUF-DAnIvyI0.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-CstVDJZ3.js → diagram-LMA3HP47-Bmh7tvmI.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-xmX57FKh.js → diagram-OG6HWLK6-hnHVMNnk.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-DhLPxvrl.js → erDiagram-TEJ5UH35-AMDitV0b.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-BExoUaNm.js → flowDiagram-I6XJVG4X-C0t2ZGQv.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-BEXa5a22.js → ganttDiagram-6RSMTGT7-BPlM-BEN.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-Cbes9api.js → gitGraphDiagram-PVQCEYII-Lv_YTxnb.js} +1 -1
- package/dist/assets/index-COq21Zym.js +119 -0
- package/dist/assets/{infoDiagram-5YYISTIA-UBm4ssfm.js → infoDiagram-5YYISTIA-Dul1vdUm.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CksBYHCF.js → ishikawaDiagram-YF4QCWOH-CCBwgjFs.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-CKHkIHky.js → journeyDiagram-JHISSGLW-zUJN38EU.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-CEahMFzE.js → kanban-definition-UN3LZRKU-BNxWi-8J.js} +1 -1
- package/dist/assets/{linear-CqLVtYRk.js → linear-CRH9b4g7.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-C61zWt4M.js → mindmap-definition-RKZ34NQL-DolYwq7Y.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-BIpZHyJW.js → pieDiagram-4H26LBE5-oxNfr2TX.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-Db7nRbZm.js → quadrantDiagram-W4KKPZXB-BmZCvD-z.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-BQqFCAr0.js → requirementDiagram-4Y6WPE33-DsXZ05jo.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-D-oAAMci.js → sankeyDiagram-5OEKKPKP-DsnYaawP.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-Dc4Cp0Om.js → sequenceDiagram-3UESZ5HK-Dt5mu3g8.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-B5DkQ8Pr.js → stateDiagram-AJRCARHV-BFR5ZINQ.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-DU9Fl-FW.js → timeline-definition-PNZ67QCA-Bvnhw58e.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-Bthz7siw.js → vennDiagram-CIIHVFJN-5WnyjZqf.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-nqdlFSIA.js → wardley-L42UT6IY-CTxW4xow.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-BJ1BC5zc.js → wardleyDiagram-YWT4CUSO-K8Y1EOvb.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-Bib5JrbR.js → xychartDiagram-2RQKCTM6-BCShes6B.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +2 -1
- package/lib/build.js +96 -6
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/completion-writer.js +38 -14
- package/lib/deps.js +98 -1
- 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/rtk.js +66 -0
- package/lib/state-migrations.js +211 -42
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vision-writer.js +6 -13
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +9 -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 +21 -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-store.js +6 -11
- package/server/vision-utils.js +23 -4
- package/dist/assets/channel-Dh96pu1k.js +0 -1
- package/dist/assets/classDiagram-4FO5ZUOK-BCqpDcaS.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-BCqpDcaS.js +0 -1
- package/dist/assets/index-DNuHtZwR.js +0 -123
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BM1Uf6a_.js +0 -1
package/lib/state-migrations.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
renameSync, unlinkSync, readdirSync,
|
|
28
28
|
} from 'fs';
|
|
29
29
|
import { join } from 'path';
|
|
30
|
-
import {
|
|
30
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
31
31
|
import { writeFeature } from './feature-json.js';
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -48,6 +48,7 @@ export const MIGRATIONS = [
|
|
|
48
48
|
{
|
|
49
49
|
version: 1,
|
|
50
50
|
id: 'normalize-complexity',
|
|
51
|
+
target: 'feature',
|
|
51
52
|
describe: 'Normalize legacy free-text complexity to the S/M/L/XL enum; drop null/unmappable (fails the string|number oneOf)',
|
|
52
53
|
migrateFeature(f) {
|
|
53
54
|
// Total on any parseable JSON value: a non-object (scalar/array/null)
|
|
@@ -68,8 +69,100 @@ export const MIGRATIONS = [
|
|
|
68
69
|
return { changed: true, feature: rest };
|
|
69
70
|
},
|
|
70
71
|
},
|
|
72
|
+
{
|
|
73
|
+
version: 2,
|
|
74
|
+
id: 'normalize-vision-legacy',
|
|
75
|
+
target: 'vision',
|
|
76
|
+
describe: 'Vision-state: legacy `featureCode: "feature:X"` → `lifecycle.featureCode` + normalize legacy gate outcomes to the imperative enum',
|
|
77
|
+
migrateState(state) { return migrateVisionState(state); },
|
|
78
|
+
},
|
|
71
79
|
];
|
|
72
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// COMP-MIGRATE-UNIFY-VISION — shared vision-state transforms.
|
|
83
|
+
//
|
|
84
|
+
// These two transforms were historically inlined AND duplicated in
|
|
85
|
+
// server/vision-store.js and lib/vision-writer.js, run lazily on every
|
|
86
|
+
// vision-state load. They are folded here as the single implementation, reused
|
|
87
|
+
// by both:
|
|
88
|
+
// - the load-time paths (vision-store._load / vision-writer._load), and
|
|
89
|
+
// - the eager runner below — so cold vision-state the server never loads
|
|
90
|
+
// (e.g. a frozen forge-top store) is migrated by `compose migrate-state`.
|
|
91
|
+
//
|
|
92
|
+
// Scope is EXACTLY these two transforms. Each file's other load-time work —
|
|
93
|
+
// gate/pending dedup, slug/files/group derivation — is deliberately NOT folded
|
|
94
|
+
// here (it differs per call site and is out of this consolidation's scope).
|
|
95
|
+
//
|
|
96
|
+
// They MUTATE their argument in place (and report `changed`) by design: the
|
|
97
|
+
// on-disk byte image must stay identical to the previous inline behavior, which
|
|
98
|
+
// mutated in place, so key-insertion order is preserved exactly. They are
|
|
99
|
+
// otherwise pure (no I/O) and idempotent.
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
const GATE_OUTCOME_MAP = { approved: 'approve', killed: 'kill', revised: 'revise' };
|
|
103
|
+
|
|
104
|
+
/** Normalize a legacy past-tense gate outcome to the imperative enum. Pure/total/idempotent. */
|
|
105
|
+
export function normalizeGateOutcome(outcome) {
|
|
106
|
+
return GATE_OUTCOME_MAP[outcome] || outcome;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Migrate one vision item's legacy `featureCode: "feature:X"` to
|
|
111
|
+
* `lifecycle.featureCode: "X"`. Mutates `item` in place (for byte-identity with
|
|
112
|
+
* the prior inline code) and reports whether anything changed. No-op when the
|
|
113
|
+
* binding is already in lifecycle form or the field is absent.
|
|
114
|
+
*
|
|
115
|
+
* PURE + TOTAL (the registry invariant): never throws on parseable JSON. The
|
|
116
|
+
* `typeof === 'string'` guard is deliberate — the prior inline loaders called
|
|
117
|
+
* `.startsWith` unguarded and would THROW on a malformed truthy non-string
|
|
118
|
+
* `featureCode` (a load-path crash → fresh-state fallback). On real corpora every
|
|
119
|
+
* `featureCode` is a string-or-absent, so the on-disk output is byte-identical;
|
|
120
|
+
* the only divergence is that pathological non-string values now no-op instead of
|
|
121
|
+
* crashing, which is required so the eager runner (no per-item try/catch) stays
|
|
122
|
+
* total. Out of scope to "fix" such malformed data here — this only consolidates.
|
|
123
|
+
* @param {object} item
|
|
124
|
+
* @returns {{changed: boolean}}
|
|
125
|
+
*/
|
|
126
|
+
export function migrateVisionItemFeatureCode(item) {
|
|
127
|
+
if (!item || typeof item !== 'object') return { changed: false };
|
|
128
|
+
if (item.featureCode && typeof item.featureCode === 'string'
|
|
129
|
+
&& item.featureCode.startsWith('feature:') && !item.lifecycle?.featureCode) {
|
|
130
|
+
const bare = item.featureCode.replace(/^feature:/, '');
|
|
131
|
+
item.lifecycle = item.lifecycle || {};
|
|
132
|
+
item.lifecycle.featureCode = bare;
|
|
133
|
+
delete item.featureCode;
|
|
134
|
+
return { changed: true };
|
|
135
|
+
}
|
|
136
|
+
return { changed: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply the legacy vision-state transforms across a whole parsed vision-state
|
|
141
|
+
* object: `featureCode→lifecycle.featureCode` over `items[]` and gate-outcome
|
|
142
|
+
* normalization over `gates[]`. Mutates in place and also returns the object so
|
|
143
|
+
* either the return value or the original reference may be used. Total on any
|
|
144
|
+
* parseable shape (missing/non-array items/gates are skipped).
|
|
145
|
+
* @param {object} state
|
|
146
|
+
* @returns {{changed: boolean, state: object}}
|
|
147
|
+
*/
|
|
148
|
+
export function migrateVisionState(state) {
|
|
149
|
+
let changed = false;
|
|
150
|
+
if (state && Array.isArray(state.items)) {
|
|
151
|
+
for (const item of state.items) {
|
|
152
|
+
if (migrateVisionItemFeatureCode(item).changed) changed = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (state && Array.isArray(state.gates)) {
|
|
156
|
+
for (const gate of state.gates) {
|
|
157
|
+
if (gate && gate.outcome) {
|
|
158
|
+
const normalized = normalizeGateOutcome(gate.outcome);
|
|
159
|
+
if (normalized !== gate.outcome) { gate.outcome = normalized; changed = true; }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { changed, state };
|
|
164
|
+
}
|
|
165
|
+
|
|
73
166
|
function stateFilePath(cwd) {
|
|
74
167
|
return join(cwd, '.compose', 'data', 'migration-state.json');
|
|
75
168
|
}
|
|
@@ -104,6 +197,24 @@ function writeMigrationStateAtomic(cwd, state) {
|
|
|
104
197
|
}
|
|
105
198
|
}
|
|
106
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Atomically rewrite `.compose/data/vision-state.json`, byte-for-byte matching
|
|
202
|
+
* VisionWriter._atomicWrite (2-space JSON + trailing newline, temp + rename).
|
|
203
|
+
*/
|
|
204
|
+
function writeVisionStateAtomic(cwd, state) {
|
|
205
|
+
const dir = join(cwd, '.compose', 'data');
|
|
206
|
+
mkdirSync(dir, { recursive: true });
|
|
207
|
+
const p = join(dir, 'vision-state.json');
|
|
208
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n');
|
|
211
|
+
renameSync(tmp, p);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
try { unlinkSync(tmp); } catch { /* tmp may not exist */ }
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
107
218
|
/**
|
|
108
219
|
* Tracker classification from `.compose/compose.json`:
|
|
109
220
|
* 'local' — tracker absent/null or provider==='local'
|
|
@@ -124,16 +235,20 @@ function trackerKind(cwd) {
|
|
|
124
235
|
}
|
|
125
236
|
|
|
126
237
|
/**
|
|
127
|
-
* Run pending
|
|
238
|
+
* Run pending state migrations for the workspace at `cwd`. Feature-target
|
|
239
|
+
* migrations walk every feature.json; vision-target migrations walk the single
|
|
240
|
+
* `.compose/data/vision-state.json` (COMP-MIGRATE-UNIFY-VISION) using the same
|
|
241
|
+
* pure transforms the server's load-time path uses. One shared `stateVersion`
|
|
242
|
+
* stamp covers both.
|
|
128
243
|
*
|
|
129
244
|
* @param {string} cwd - workspace root
|
|
130
245
|
* @param {{dryRun?: boolean}} [opts]
|
|
131
246
|
* @returns {object} report — one of:
|
|
132
|
-
* {skipped:'no-workspace'|'non-local-tracker'}
|
|
247
|
+
* {skipped:'no-workspace'|'non-local-tracker'|'unreadable-config'}
|
|
133
248
|
* {from, to, dryRun, noop:true, perMigration:[], parseErrors:[]}
|
|
134
|
-
* {from, to, dryRun, perMigration:[{id,version,touched:[]}], parseErrors:[{path,message}]}
|
|
135
|
-
* @throws if a
|
|
136
|
-
*
|
|
249
|
+
* {from, to, dryRun, perMigration:[{id,version,target,touched:[]}], parseErrors:[{path,message}]}
|
|
250
|
+
* @throws if a migrate transform throws (migration-code bug) — aborts WITHOUT
|
|
251
|
+
* advancing the stamp.
|
|
137
252
|
*/
|
|
138
253
|
export function runStateMigrations(cwd, opts = {}) {
|
|
139
254
|
const dryRun = !!opts.dryRun;
|
|
@@ -155,43 +270,82 @@ export function runStateMigrations(cwd, opts = {}) {
|
|
|
155
270
|
return { from, to: from, dryRun, noop: true, perMigration: [], parseErrors: [] };
|
|
156
271
|
}
|
|
157
272
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
273
|
+
// Migrations dispatch by `target` (default 'feature'): feature-target ones
|
|
274
|
+
// walk every feature.json; vision-target ones walk the single vision-state.json.
|
|
275
|
+
const featurePending = pending.filter((m) => (m.target || 'feature') === 'feature');
|
|
276
|
+
const visionPending = pending.filter((m) => m.target === 'vision');
|
|
162
277
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.filter((e) => e.isDirectory())
|
|
169
|
-
.map((e) => e.name);
|
|
170
|
-
} catch {
|
|
171
|
-
dirs = []; // features dir may not exist yet
|
|
172
|
-
}
|
|
278
|
+
const perMigration = pending.map((m) => ({
|
|
279
|
+
id: m.id, version: m.version, target: m.target || 'feature', touched: [],
|
|
280
|
+
}));
|
|
281
|
+
const perMigrationByVersion = new Map(perMigration.map((p) => [p.version, p]));
|
|
282
|
+
const parseErrors = [];
|
|
173
283
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
284
|
+
// --- feature.json walk -----------------------------------------------------
|
|
285
|
+
if (featurePending.length > 0) {
|
|
286
|
+
const featuresDir = resolveFeaturesPath(cwd); // absolute; honors paths.features override
|
|
287
|
+
const featuresRoot = featuresDir;
|
|
288
|
+
// Own directory walk — do NOT use listFeatures(): it silently skips unreadable
|
|
289
|
+
// files, which would hide exactly the cold-data corruption we must surface.
|
|
290
|
+
let dirs = [];
|
|
178
291
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
292
|
+
dirs = readdirSync(featuresRoot, { withFileTypes: true })
|
|
293
|
+
.filter((e) => e.isDirectory())
|
|
294
|
+
.map((e) => e.name);
|
|
295
|
+
} catch {
|
|
296
|
+
dirs = []; // features dir may not exist yet
|
|
183
297
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
298
|
+
|
|
299
|
+
for (const code of dirs) {
|
|
300
|
+
const fpath = join(featuresRoot, code, 'feature.json');
|
|
301
|
+
if (!existsSync(fpath)) continue;
|
|
302
|
+
let feature;
|
|
303
|
+
try {
|
|
304
|
+
feature = JSON.parse(readFileSync(fpath, 'utf-8'));
|
|
305
|
+
} catch (err) {
|
|
306
|
+
parseErrors.push({ path: fpath, message: err.message });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
let changedAny = false;
|
|
310
|
+
for (const m of featurePending) {
|
|
311
|
+
const res = m.migrateFeature(feature); // pure/total; a throw = migration-code bug → fail-fast
|
|
312
|
+
if (res.changed) {
|
|
313
|
+
feature = res.feature;
|
|
314
|
+
changedAny = true;
|
|
315
|
+
perMigrationByVersion.get(m.version).touched.push(feature.code || code);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (changedAny && !dryRun) {
|
|
319
|
+
writeFeature(cwd, feature, featuresDir, { validate: false });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- vision-state.json walk (COMP-MIGRATE-UNIFY-VISION) ---------------------
|
|
325
|
+
// Eagerly migrate cold vision-state the running server never loaded. Same pure
|
|
326
|
+
// transforms the load-time paths use, so the result is byte-identical.
|
|
327
|
+
if (visionPending.length > 0) {
|
|
328
|
+
const vpath = join(cwd, '.compose', 'data', 'vision-state.json');
|
|
329
|
+
if (existsSync(vpath)) {
|
|
330
|
+
let state = null;
|
|
331
|
+
try {
|
|
332
|
+
state = JSON.parse(readFileSync(vpath, 'utf-8'));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
parseErrors.push({ path: vpath, message: err.message }); // reported, never blocks the stamp
|
|
335
|
+
}
|
|
336
|
+
if (state) {
|
|
337
|
+
let changedAny = false;
|
|
338
|
+
for (const m of visionPending) {
|
|
339
|
+
const res = m.migrateState(state); // mutates in place; throw = migration-code bug → fail-fast
|
|
340
|
+
if (res.changed) {
|
|
341
|
+
changedAny = true;
|
|
342
|
+
perMigrationByVersion.get(m.version).touched.push('vision-state.json');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (changedAny && !dryRun) {
|
|
346
|
+
writeVisionStateAtomic(cwd, state);
|
|
347
|
+
}
|
|
191
348
|
}
|
|
192
|
-
});
|
|
193
|
-
if (changedAny && !dryRun) {
|
|
194
|
-
writeFeature(cwd, feature, featuresDir, { validate: false });
|
|
195
349
|
}
|
|
196
350
|
}
|
|
197
351
|
|
|
@@ -209,9 +363,24 @@ export function runStateMigrations(cwd, opts = {}) {
|
|
|
209
363
|
export function summarizeMigrationReport(report) {
|
|
210
364
|
if (!report || report.skipped) return null;
|
|
211
365
|
if (report.noop) return `state up to date (v${report.to})`;
|
|
212
|
-
const touched = report.perMigration.reduce((n, m) => n + m.touched.length, 0);
|
|
213
|
-
const errs = report.parseErrors.length;
|
|
214
366
|
const dry = report.dryRun ? ' (dry-run)' : '';
|
|
215
|
-
|
|
216
|
-
|
|
367
|
+
const errs = report.parseErrors.length;
|
|
368
|
+
const errClause = errs ? `, ${errs} unparseable (reported)` : '';
|
|
369
|
+
|
|
370
|
+
// Feature migrations report touched feature.json files; vision migrations
|
|
371
|
+
// touch the single vision-state.json. Absent `target` ⇒ 'feature' (back-compat
|
|
372
|
+
// with the COMP-MIGRATE-ON-UPGRADE report shape).
|
|
373
|
+
const feature = report.perMigration.filter((m) => (m.target || 'feature') === 'feature');
|
|
374
|
+
const vision = report.perMigration.filter((m) => m.target === 'vision');
|
|
375
|
+
const parts = [];
|
|
376
|
+
if (feature.length) {
|
|
377
|
+
const touched = feature.reduce((n, m) => n + m.touched.length, 0);
|
|
378
|
+
parts.push(`migrated ${touched} feature.json across ${feature.length} migration(s)`);
|
|
379
|
+
}
|
|
380
|
+
if (vision.length) {
|
|
381
|
+
const touched = vision.reduce((n, m) => n + m.touched.length, 0);
|
|
382
|
+
parts.push(`vision-state ${touched ? 'migrated' : 'up to date'} across ${vision.length} migration(s)`);
|
|
383
|
+
}
|
|
384
|
+
if (parts.length === 0) parts.push('no migrations applied');
|
|
385
|
+
return `${parts.join('; ')} → stateVersion ${report.to}${errClause}${dry}`;
|
|
217
386
|
}
|
|
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameS
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
|
|
4
4
|
import { readFeature, listFeatures as listFeaturesRaw, writeFeature } from '../feature-json.js';
|
|
5
|
-
import {
|
|
5
|
+
import { resolveFeaturesPath } from '../project-paths.js';
|
|
6
6
|
import { TrackerProvider, CAP } from './provider.js';
|
|
7
7
|
|
|
8
8
|
import { setFeatureStatus, addRoadmapEntry as addRoadmapEntryRaw } from '../feature-writer.js';
|
|
@@ -38,7 +38,7 @@ export class LocalFileProvider extends TrackerProvider {
|
|
|
38
38
|
|
|
39
39
|
async init(cwd) {
|
|
40
40
|
this.cwd = cwd;
|
|
41
|
-
this.featuresDir =
|
|
41
|
+
this.featuresDir = resolveFeaturesPath(cwd);
|
|
42
42
|
return this;
|
|
43
43
|
}
|
|
44
44
|
|
package/lib/triage.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
15
|
+
import { resolvePathValue } from './paths-core.js';
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Tier definitions
|
|
@@ -185,8 +187,8 @@ function deriveProfile(signals) {
|
|
|
185
187
|
*/
|
|
186
188
|
export async function runTriage(featureCode, opts = {}) {
|
|
187
189
|
const cwd = opts.cwd ?? process.cwd();
|
|
188
|
-
const featuresDir = opts.featuresDir ??
|
|
189
|
-
const featureDir = join(cwd, featuresDir, featureCode);
|
|
190
|
+
const featuresDir = opts.featuresDir ?? resolveFeaturesPath(cwd);
|
|
191
|
+
const featureDir = join(resolvePathValue(cwd, featuresDir, 'features'), featureCode);
|
|
190
192
|
|
|
191
193
|
// Collect content from key files
|
|
192
194
|
const candidateFiles = ['plan.md', 'blueprint.md', 'design.md', 'prd.md', 'architecture.md'];
|
|
@@ -234,11 +236,11 @@ export async function runTriage(featureCode, opts = {}) {
|
|
|
234
236
|
*
|
|
235
237
|
* @param {string} cwd - Project root
|
|
236
238
|
* @param {string} featureCode - Feature code
|
|
237
|
-
* @param {string} [featuresDir] -
|
|
239
|
+
* @param {string} [featuresDir] - Path to features dir (absolute or relative; default: resolved features path)
|
|
238
240
|
* @returns {boolean}
|
|
239
241
|
*/
|
|
240
|
-
export function isTriageStale(cwd, featureCode, featuresDir =
|
|
241
|
-
const featureDir = join(cwd, featuresDir, featureCode);
|
|
242
|
+
export function isTriageStale(cwd, featureCode, featuresDir = resolveFeaturesPath(cwd)) {
|
|
243
|
+
const featureDir = join(resolvePathValue(cwd, featuresDir, 'features'), featureCode);
|
|
242
244
|
const featureJsonPath = join(featureDir, 'feature.json');
|
|
243
245
|
|
|
244
246
|
if (!existsSync(featureJsonPath)) return true;
|
package/lib/vision-writer.js
CHANGED
|
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import crypto from 'node:crypto';
|
|
14
14
|
import { resolvePort } from './resolve-port.js';
|
|
15
15
|
import { probeServer } from './server-probe.js';
|
|
16
|
+
import { migrateVisionItemFeatureCode, normalizeGateOutcome } from './state-migrations.js';
|
|
16
17
|
|
|
17
18
|
const EMPTY_STATE = () => ({ items: [], connections: [], gates: [] });
|
|
18
19
|
|
|
@@ -34,11 +35,9 @@ function matchFeatureItem(items, featureCode) {
|
|
|
34
35
|
|| null;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** Canonical outcome normalization — maps legacy past-tense to imperative
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return map[outcome] || outcome;
|
|
41
|
-
}
|
|
38
|
+
/** Canonical outcome normalization — maps legacy past-tense to imperative.
|
|
39
|
+
* Shared single implementation lives in state-migrations.js (COMP-MIGRATE-UNIFY-VISION). */
|
|
40
|
+
const normalizeOutcome = normalizeGateOutcome;
|
|
42
41
|
|
|
43
42
|
export class ServerUnreachableError extends Error {
|
|
44
43
|
constructor(message = 'Server is unreachable') {
|
|
@@ -77,16 +76,10 @@ export class VisionWriter {
|
|
|
77
76
|
parsed.connections = parsed.connections || [];
|
|
78
77
|
parsed.gates = parsed.gates || [];
|
|
79
78
|
|
|
80
|
-
// Migration: legacy featureCode → lifecycle.featureCode
|
|
79
|
+
// Migration: legacy featureCode → lifecycle.featureCode (shared impl)
|
|
81
80
|
let migrated = false;
|
|
82
81
|
for (const item of parsed.items) {
|
|
83
|
-
if (item.
|
|
84
|
-
const bare = item.featureCode.replace(/^feature:/, '');
|
|
85
|
-
item.lifecycle = item.lifecycle || {};
|
|
86
|
-
item.lifecycle.featureCode = bare;
|
|
87
|
-
delete item.featureCode;
|
|
88
|
-
migrated = true;
|
|
89
|
-
}
|
|
82
|
+
if (migrateVisionItemFeatureCode(item).changed) migrated = true;
|
|
90
83
|
}
|
|
91
84
|
|
|
92
85
|
// Dedup gates by ID (keep latest)
|
package/lib/xref-push.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
|
-
import {
|
|
20
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
21
21
|
import { resolveSiblingRoot } from './xref-local.js';
|
|
22
22
|
|
|
23
23
|
const GITHUB_STATES = new Set(['open', 'closed']);
|
|
@@ -145,7 +145,7 @@ function localResolve(link, cwd) {
|
|
|
145
145
|
const sib = resolveSiblingRoot(cwd, link.repo);
|
|
146
146
|
if (sib.skipped) return sib;
|
|
147
147
|
try {
|
|
148
|
-
const fjPath = join(
|
|
148
|
+
const fjPath = join(resolveFeaturesPath(sib.root), link.to_code, 'feature.json');
|
|
149
149
|
if (!existsSync(fjPath)) return { skipped: true, reason: `local target ${link.repo}/${link.to_code} not found` };
|
|
150
150
|
return { state: JSON.parse(readFileSync(fjPath, 'utf8')).status || null, root: sib.root };
|
|
151
151
|
} catch (e) { return { skipped: true, reason: `unreadable local target: ${e.message}` }; }
|
|
@@ -171,13 +171,13 @@ function localResolve(link, cwd) {
|
|
|
171
171
|
* @returns {Promise<{pushed: Array, skipped: Array, unchanged: number, scanned: number}>}
|
|
172
172
|
*/
|
|
173
173
|
export async function pushExternalRefs(cwd, opts = {}) {
|
|
174
|
-
const featuresDir = opts.featuresDir ??
|
|
174
|
+
const featuresDir = opts.featuresDir ?? resolveFeaturesPath(cwd);
|
|
175
175
|
const clientOpts = { transport: opts.githubTransport ?? null, auth: opts.githubAuth };
|
|
176
176
|
const ghResolve = opts.resolve ?? ((link) => defaultResolve(link, clientOpts));
|
|
177
177
|
const ghWrite = opts.write ?? ((link, patch) => defaultWrite(link, patch, clientOpts));
|
|
178
178
|
const setStatus = opts.setStatus ?? defaultSetStatus;
|
|
179
179
|
const apply = opts.apply === true;
|
|
180
|
-
const dir =
|
|
180
|
+
const dir = featuresDir;
|
|
181
181
|
|
|
182
182
|
const pushed = [];
|
|
183
183
|
const skipped = [];
|
package/lib/xref-sync.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { writeFeature } from './feature-json.js';
|
|
19
|
-
import {
|
|
19
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
20
20
|
import { resolveSiblingRoot } from './xref-local.js';
|
|
21
21
|
|
|
22
22
|
const RESOLVABLE = new Set(['github', 'local']);
|
|
@@ -80,7 +80,7 @@ async function defaultResolve(link, cwd, featuresDir) {
|
|
|
80
80
|
const citedRoot = sib.root;
|
|
81
81
|
// Resolve the SIBLING's own features dir (it may have its own paths.features).
|
|
82
82
|
try {
|
|
83
|
-
const fjPath = join(
|
|
83
|
+
const fjPath = join(resolveFeaturesPath(citedRoot), link.to_code, 'feature.json');
|
|
84
84
|
if (!existsSync(fjPath)) return { skipped: true, reason: `local target ${link.repo}/${link.to_code} not found` };
|
|
85
85
|
return { state: JSON.parse(readFileSync(fjPath, 'utf8')).status || null };
|
|
86
86
|
} catch (e) { return { skipped: true, reason: `unreadable local target: ${e.message}` }; }
|
|
@@ -100,9 +100,9 @@ async function defaultResolve(link, cwd, featuresDir) {
|
|
|
100
100
|
* @returns {Promise<{synced: Array, skipped: Array, unchanged: number, scanned: number}>}
|
|
101
101
|
*/
|
|
102
102
|
export async function syncExternalRefs(cwd, opts = {}) {
|
|
103
|
-
const featuresDir = opts.featuresDir ??
|
|
103
|
+
const featuresDir = opts.featuresDir ?? resolveFeaturesPath(cwd);
|
|
104
104
|
const resolve = opts.resolve ?? defaultResolve;
|
|
105
|
-
const dir =
|
|
105
|
+
const dir = featuresDir;
|
|
106
106
|
|
|
107
107
|
const synced = [];
|
|
108
108
|
const skipped = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.50-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
|
|
12
|
-
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig } from './project-root.js';
|
|
12
|
+
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig, isLifecycleEnabled } from './project-root.js';
|
|
13
13
|
import { resolveProfile, isToolAllowed } from './mcp-tool-policy.js';
|
|
14
14
|
import { getRoadmap } from '../lib/get-roadmap.js';
|
|
15
15
|
|
|
@@ -125,6 +125,14 @@ export function getSessionsFile() { return path.join(getDataDir(), 'sessions.jso
|
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
export function loadVisionState() {
|
|
128
|
+
// FORGE-ROADMAP-RETIRE-STORE: when the bound workspace disables the lifecycle
|
|
129
|
+
// capability (capabilities.lifecycle:false — e.g. a narrative-owned forge-top),
|
|
130
|
+
// the vision store is RETIRED. Every MCP vision read funnels through here, so
|
|
131
|
+
// returning the empty shape guarantees get_vision_items (and every dependent:
|
|
132
|
+
// phase summary, item detail, pending gates, lifecycle status) can't surface a
|
|
133
|
+
// second, drift-prone answer beside the prose ROADMAP. Reads stay inert even if
|
|
134
|
+
// a stray write later recreates the file. get_roadmap (narrative) is unaffected.
|
|
135
|
+
if (!isLifecycleEnabled()) return { items: [], connections: [], gates: [] };
|
|
128
136
|
try {
|
|
129
137
|
const raw = fs.readFileSync(getVisionFile(), 'utf-8');
|
|
130
138
|
const state = JSON.parse(raw);
|
package/server/design-routes.js
CHANGED
|
@@ -16,6 +16,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
16
16
|
import { parseDecisionBlocks } from '../src/components/vision/designSessionState.js';
|
|
17
17
|
import { StratumMcpClient } from '../lib/stratum-mcp-client.js';
|
|
18
18
|
import { KNOWN_VERSIONS } from '../lib/build-stream-schema.js';
|
|
19
|
+
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
20
|
+
import { relForDisplay } from '../lib/project-paths.js';
|
|
19
21
|
|
|
20
22
|
// Lazy singleton — design conversations share one stratum-mcp connection
|
|
21
23
|
// across the server process lifetime. Concurrent runs are correlation-id scoped.
|
|
@@ -444,16 +446,19 @@ Output ONLY the Markdown content, no code fences.`;
|
|
|
444
446
|
return;
|
|
445
447
|
}
|
|
446
448
|
|
|
447
|
-
// Determine the output path
|
|
448
|
-
|
|
449
|
+
// Determine the output path. Resolve the absolute write target first
|
|
450
|
+
// (features may be relocated outside the root — COMP-PATHS-EXTERNAL),
|
|
451
|
+
// then derive the display-relative string from it.
|
|
452
|
+
let absPath, designDocPath;
|
|
449
453
|
if (scope === 'feature' && featureCode) {
|
|
450
|
-
|
|
454
|
+
absPath = path.join(resolveProjectPath('features'), featureCode, 'design.md');
|
|
455
|
+
designDocPath = relForDisplay(getTargetRoot(), absPath);
|
|
451
456
|
} else {
|
|
452
457
|
designDocPath = path.join('docs', 'design.md');
|
|
458
|
+
absPath = path.join(projectRoot, designDocPath);
|
|
453
459
|
}
|
|
454
460
|
|
|
455
461
|
// Guard: never overwrite an existing doc with empty content
|
|
456
|
-
const absPath = path.join(projectRoot, designDocPath);
|
|
457
462
|
if (!docContent.trim()) {
|
|
458
463
|
console.error('[design] Generated doc is empty — refusing to overwrite');
|
|
459
464
|
res.status(500).json({ error: 'Doc generation produced empty content' });
|
package/server/drift-axes.js
CHANGED
|
@@ -19,6 +19,7 @@ import fs from 'node:fs';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { execSync } from 'node:child_process';
|
|
21
21
|
import { diffContracts } from './contract-diff.js';
|
|
22
|
+
import { resolveFeaturesPath } from '../lib/project-paths.js';
|
|
22
23
|
|
|
23
24
|
// ── Threshold constants (Decision 2) ─────────────────────────────────────────
|
|
24
25
|
|
|
@@ -353,9 +354,10 @@ export function computeDriftAxes(item, projectRoot, now) {
|
|
|
353
354
|
|
|
354
355
|
const ts = now || new Date().toISOString();
|
|
355
356
|
|
|
356
|
-
// Resolve the
|
|
357
|
-
//
|
|
358
|
-
|
|
357
|
+
// Resolve the features/<FC> directory against projectRoot's own config
|
|
358
|
+
// (relocatable artifact paths, COMP-PATHS-EXTERNAL). Byte-identical to
|
|
359
|
+
// projectRoot/docs/features/<FC> for the in-root default.
|
|
360
|
+
const featurePath = path.join(resolveFeaturesPath(projectRoot), featureCode);
|
|
359
361
|
|
|
360
362
|
const pathAxis = computePathDrift(item, projectRoot, featurePath, ts);
|
|
361
363
|
const contractAxis = computeContractDrift(item, projectRoot, featurePath, ts);
|
package/server/feature-scan.js
CHANGED
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
|
|
18
18
|
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
19
|
+
import { relForDisplay } from '../lib/project-paths.js';
|
|
19
20
|
import { assertValidLinkShape } from '../lib/feature-write-guard.js';
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -608,6 +609,11 @@ export function seedFromRoadmapGraph(store) {
|
|
|
608
609
|
export function seedFeatures(features, store) {
|
|
609
610
|
const seeded = { features: 0, updated: 0, connections: 0 };
|
|
610
611
|
const featureItemMap = new Map(); // featureCode → itemId
|
|
612
|
+
const root = getTargetRoot();
|
|
613
|
+
const featuresBase = resolveProjectPath('features');
|
|
614
|
+
// Root-relative for the in-root default (relForDisplay guarantees byte-identity
|
|
615
|
+
// there); absolute when the features dir is relocated outside the workspace root.
|
|
616
|
+
const artifactPath = (feature, a) => relForDisplay(root, path.join(featuresBase, feature.name, a));
|
|
611
617
|
|
|
612
618
|
// First pass: create/update items
|
|
613
619
|
for (const feature of features) {
|
|
@@ -623,7 +629,7 @@ export function seedFeatures(features, store) {
|
|
|
623
629
|
status: feature.status || 'planned',
|
|
624
630
|
phase: feature.phase || 'planning',
|
|
625
631
|
confidence: feature.confidence,
|
|
626
|
-
files: feature.artifacts.map(a =>
|
|
632
|
+
files: feature.artifacts.map(a => artifactPath(feature, a)),
|
|
627
633
|
...(feature.group ? { group: feature.group } : {}),
|
|
628
634
|
});
|
|
629
635
|
try {
|
|
@@ -643,7 +649,7 @@ export function seedFeatures(features, store) {
|
|
|
643
649
|
if (feature.confidence > (featureItem.confidence || 0)) {
|
|
644
650
|
updates.confidence = feature.confidence;
|
|
645
651
|
}
|
|
646
|
-
const newFiles = feature.artifacts.map(a =>
|
|
652
|
+
const newFiles = feature.artifacts.map(a => artifactPath(feature, a));
|
|
647
653
|
if (JSON.stringify(newFiles) !== JSON.stringify(featureItem.files || [])) {
|
|
648
654
|
updates.files = newFiles;
|
|
649
655
|
}
|