@smartmemory/compose 0.2.48-beta → 0.2.49-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.
Files changed (63) hide show
  1. package/.claude/skills/compose/SKILL.md +2 -0
  2. package/.compose-deps.json +13 -0
  3. package/bin/compose.js +5 -2
  4. package/dist/assets/{App-D7E7S49Q.js → App-DOerhoDp.js} +160 -160
  5. package/dist/assets/{arc-LOW60tiI.js → arc-B1TqAC_R.js} +1 -1
  6. package/dist/assets/{architectureDiagram-3BPJPVTR-BDY00hRy.js → architectureDiagram-3BPJPVTR-C4hfxNOw.js} +1 -1
  7. package/dist/assets/{blockDiagram-GPEHLZMM-DiC_6ktq.js → blockDiagram-GPEHLZMM-DRqkGncy.js} +1 -1
  8. package/dist/assets/{c4Diagram-AAUBKEIU-CXZC8woU.js → c4Diagram-AAUBKEIU-Dc6swoQo.js} +1 -1
  9. package/dist/assets/channel-KsY1x-7s.js +1 -0
  10. package/dist/assets/{chunk-2J33WTMH-b8Rdk4S-.js → chunk-2J33WTMH-6HKDSnOw.js} +1 -1
  11. package/dist/assets/{chunk-4BX2VUAB-BSxn03f-.js → chunk-4BX2VUAB-CdqXgd68.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-uUk1WzZ1.js → chunk-55IACEB6-D7VauOmU.js} +1 -1
  13. package/dist/assets/{chunk-727SXJPM-zvXhT6iZ.js → chunk-727SXJPM-CeKpXDif.js} +1 -1
  14. package/dist/assets/{chunk-AQP2D5EJ-B6GCyrIp.js → chunk-AQP2D5EJ-y2XL-YHV.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-DlhrqkBp.js → chunk-FMBD7UC4-C1Z5m8DP.js} +1 -1
  16. package/dist/assets/{chunk-ND2GUHAM-lo_8kNzw.js → chunk-ND2GUHAM-DmqZMEQe.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-on-yD-C0.js → chunk-QZHKN3VN-DY9GU2uD.js} +1 -1
  18. package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +1 -0
  19. package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +1 -0
  20. package/dist/assets/{cose-bilkent-S5V4N54A-DxV7Y2bU.js → cose-bilkent-S5V4N54A-B34g9xzV.js} +1 -1
  21. package/dist/assets/{dagre-BM42HDAG-BHf9sURy.js → dagre-BM42HDAG-klZ_GOcw.js} +1 -1
  22. package/dist/assets/{diagram-2AECGRRQ-R9Puxzlh.js → diagram-2AECGRRQ-D0FmM6Wf.js} +1 -1
  23. package/dist/assets/{diagram-5GNKFQAL-BbjlClKx.js → diagram-5GNKFQAL-DYuhby6q.js} +1 -1
  24. package/dist/assets/{diagram-KO2AKTUF-r89Mx4m1.js → diagram-KO2AKTUF-CCtQZgal.js} +1 -1
  25. package/dist/assets/{diagram-LMA3HP47-CstVDJZ3.js → diagram-LMA3HP47-Cd1OqEln.js} +1 -1
  26. package/dist/assets/{diagram-OG6HWLK6-xmX57FKh.js → diagram-OG6HWLK6-CiJrhoxo.js} +1 -1
  27. package/dist/assets/{erDiagram-TEJ5UH35-DhLPxvrl.js → erDiagram-TEJ5UH35-C05_Iqmb.js} +1 -1
  28. package/dist/assets/{flowDiagram-I6XJVG4X-BExoUaNm.js → flowDiagram-I6XJVG4X-lXpnb-Ch.js} +1 -1
  29. package/dist/assets/{ganttDiagram-6RSMTGT7-BEXa5a22.js → ganttDiagram-6RSMTGT7-EPbL_2Hr.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-PVQCEYII-Cbes9api.js → gitGraphDiagram-PVQCEYII-COCoF_vW.js} +1 -1
  31. package/dist/assets/{index-DNuHtZwR.js → index-DlgxA8dX.js} +2 -2
  32. package/dist/assets/{infoDiagram-5YYISTIA-UBm4ssfm.js → infoDiagram-5YYISTIA-B1sJw6j5.js} +1 -1
  33. package/dist/assets/{ishikawaDiagram-YF4QCWOH-CksBYHCF.js → ishikawaDiagram-YF4QCWOH-CgdsWdgu.js} +1 -1
  34. package/dist/assets/{journeyDiagram-JHISSGLW-CKHkIHky.js → journeyDiagram-JHISSGLW-CyvHmE3z.js} +1 -1
  35. package/dist/assets/{kanban-definition-UN3LZRKU-CEahMFzE.js → kanban-definition-UN3LZRKU-Bqjdyv0T.js} +1 -1
  36. package/dist/assets/{linear-CqLVtYRk.js → linear-DecnRXf0.js} +1 -1
  37. package/dist/assets/{mindmap-definition-RKZ34NQL-C61zWt4M.js → mindmap-definition-RKZ34NQL-DM1x9vNY.js} +1 -1
  38. package/dist/assets/{pieDiagram-4H26LBE5-BIpZHyJW.js → pieDiagram-4H26LBE5-CRtmCvwV.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-W4KKPZXB-Db7nRbZm.js → quadrantDiagram-W4KKPZXB-CsBLWg27.js} +1 -1
  40. package/dist/assets/{requirementDiagram-4Y6WPE33-BQqFCAr0.js → requirementDiagram-4Y6WPE33-DD-Gg_y5.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-5OEKKPKP-D-oAAMci.js → sankeyDiagram-5OEKKPKP-BPdMN8UU.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-3UESZ5HK-Dc4Cp0Om.js → sequenceDiagram-3UESZ5HK-D4AENdze.js} +1 -1
  43. package/dist/assets/{stateDiagram-AJRCARHV-B5DkQ8Pr.js → stateDiagram-AJRCARHV-DDplpCf_.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +1 -0
  45. package/dist/assets/{timeline-definition-PNZ67QCA-DU9Fl-FW.js → timeline-definition-PNZ67QCA-BPm2AEQj.js} +1 -1
  46. package/dist/assets/{vennDiagram-CIIHVFJN-Bthz7siw.js → vennDiagram-CIIHVFJN-DQCDWwlj.js} +1 -1
  47. package/dist/assets/{wardley-L42UT6IY-nqdlFSIA.js → wardley-L42UT6IY-WERqOmJt.js} +1 -1
  48. package/dist/assets/{wardleyDiagram-YWT4CUSO-BJ1BC5zc.js → wardleyDiagram-YWT4CUSO-5teUSOiI.js} +1 -1
  49. package/dist/assets/{xychartDiagram-2RQKCTM6-Bib5JrbR.js → xychartDiagram-2RQKCTM6-Ba05SXXF.js} +1 -1
  50. package/dist/index.html +1 -1
  51. package/lib/build.js +5 -1
  52. package/lib/deps.js +98 -1
  53. package/lib/rtk.js +66 -0
  54. package/lib/state-migrations.js +210 -41
  55. package/lib/vision-writer.js +6 -13
  56. package/package.json +1 -1
  57. package/server/compose-mcp-tools.js +9 -1
  58. package/server/project-root.js +14 -0
  59. package/server/vision-store.js +6 -11
  60. package/dist/assets/channel-Dh96pu1k.js +0 -1
  61. package/dist/assets/classDiagram-4FO5ZUOK-BCqpDcaS.js +0 -1
  62. package/dist/assets/classDiagram-v2-Q7XG4LA2-BCqpDcaS.js +0 -1
  63. package/dist/assets/stateDiagram-v2-BHNVJYJU-BM1Uf6a_.js +0 -1
@@ -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 feature.json state migrations for the workspace at `cwd`.
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 migrateFeature transform throws (migration-code bug) — aborts
136
- * WITHOUT advancing the stamp.
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
- const featuresDir = loadFeaturesDir(cwd); // honors paths.features override
159
- const featuresRoot = join(cwd, featuresDir);
160
- const perMigration = pending.map((m) => ({ id: m.id, version: m.version, touched: [] }));
161
- const parseErrors = [];
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
- // Own directory walk — do NOT use listFeatures(): it silently skips unreadable
164
- // files, which would hide exactly the cold-data corruption we must surface.
165
- let dirs = [];
166
- try {
167
- dirs = readdirSync(featuresRoot, { withFileTypes: true })
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
- for (const code of dirs) {
175
- const fpath = join(featuresRoot, code, 'feature.json');
176
- if (!existsSync(fpath)) continue;
177
- let feature;
284
+ // --- feature.json walk -----------------------------------------------------
285
+ if (featurePending.length > 0) {
286
+ const featuresDir = loadFeaturesDir(cwd); // honors paths.features override
287
+ const featuresRoot = join(cwd, 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
- feature = JSON.parse(readFileSync(fpath, 'utf-8'));
180
- } catch (err) {
181
- parseErrors.push({ path: fpath, message: err.message });
182
- continue;
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
- let changedAny = false;
185
- pending.forEach((m, i) => {
186
- const res = m.migrateFeature(feature); // pure/total; a throw = migration-code bug → fail-fast
187
- if (res.changed) {
188
- feature = res.feature;
189
- changedAny = true;
190
- perMigration[i].touched.push(feature.code || code);
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
- return `migrated ${touched} feature.json across ${report.perMigration.length} migration(s) `
216
- + `→ stateVersion ${report.to}${errs ? `, ${errs} unparseable (reported)` : ''}${dry}`;
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
  }
@@ -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
- function normalizeOutcome(outcome) {
39
- const map = { approved: 'approve', killed: 'kill', revised: 'revise' };
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.featureCode && item.featureCode.startsWith('feature:') && !item.lifecycle?.featureCode) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.2.48-beta",
3
+ "version": "0.2.49-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);
@@ -124,3 +124,17 @@ export function resolveProjectPath(key) {
124
124
  if (!rel) return path.join(getTargetRoot(), DEFAULT_CONFIG.paths[key] || key);
125
125
  return path.join(getTargetRoot(), rel);
126
126
  }
127
+
128
+ /**
129
+ * Whether the bound workspace runs the lifecycle/vision store.
130
+ *
131
+ * FORGE-ROADMAP-RETIRE-STORE: a workspace sets `capabilities.lifecycle: false`
132
+ * to RETIRE its vision store — used by narrative-owned workspaces (e.g. forge-top)
133
+ * where the prose ROADMAP is the single source of truth and a second, frozen
134
+ * `vision-state.json` would only be a drift-prone parallel answer. Default true
135
+ * (absent ⇒ enabled); only an explicit `false` disables it.
136
+ * @returns {boolean}
137
+ */
138
+ export function isLifecycleEnabled() {
139
+ return loadProjectConfig().capabilities?.lifecycle !== false;
140
+ }
@@ -13,6 +13,7 @@ export const VALID_CONNECTION_TYPES = ['informs', 'blocks', 'supports', 'contrad
13
13
  export const VALID_PHASES = ['vision', 'specification', 'planning', 'implementation', 'verification', 'release'];
14
14
 
15
15
  import { getDataDir as getDefaultDataDir } from './project-root.js';
16
+ import { migrateVisionItemFeatureCode, normalizeGateOutcome } from '../lib/state-migrations.js';
16
17
 
17
18
  const DATA_FILE = path.join(getDefaultDataDir(), 'vision-state.json');
18
19
 
@@ -67,14 +68,8 @@ export class VisionStore {
67
68
  let migrated = false;
68
69
  if (Array.isArray(data.items)) {
69
70
  for (const item of data.items) {
70
- // Migration: legacy featureCode → lifecycle.featureCode
71
- if (item.featureCode && item.featureCode.startsWith('feature:') && !item.lifecycle?.featureCode) {
72
- const bare = item.featureCode.replace(/^feature:/, '');
73
- item.lifecycle = item.lifecycle || {};
74
- item.lifecycle.featureCode = bare;
75
- delete item.featureCode;
76
- migrated = true;
77
- }
71
+ // Migration: legacy featureCode → lifecycle.featureCode (shared impl)
72
+ if (migrateVisionItemFeatureCode(item).changed) migrated = true;
78
73
  if (!item.slug && item.title) item.slug = slugify(item.title);
79
74
  if (!item.files) item.files = [];
80
75
  // Group precedence (most-authoritative first):
@@ -110,10 +105,10 @@ export class VisionStore {
110
105
  if (seenPendingKeys.has(key)) { migrated = true; continue; }
111
106
  seenPendingKeys.add(key);
112
107
  }
113
- // Migration: normalize legacy gate outcomes
108
+ // Migration: normalize legacy gate outcomes (shared impl)
114
109
  if (gate.outcome) {
115
- const map = { approved: 'approve', killed: 'kill', revised: 'revise' };
116
- if (map[gate.outcome]) { gate.outcome = map[gate.outcome]; migrated = true; }
110
+ const normalized = normalizeGateOutcome(gate.outcome);
111
+ if (normalized !== gate.outcome) { gate.outcome = normalized; migrated = true; }
117
112
  }
118
113
  this.gates.set(gate.id, gate);
119
114
  }
@@ -1 +0,0 @@
1
- import{ai as o,aj as n}from"./App-D7E7S49Q.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-zvXhT6iZ.js";import{_ as i}from"./App-D7E7S49Q.js";import"./chunk-FMBD7UC4-DlhrqkBp.js";import"./chunk-ND2GUHAM-lo_8kNzw.js";import"./chunk-55IACEB6-uUk1WzZ1.js";import"./chunk-2J33WTMH-b8Rdk4S-.js";import"./mobile-DwmxS_O4.js";import"./index-DNuHtZwR.js";import"./graph-BBXaecIU.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-zvXhT6iZ.js";import{_ as i}from"./App-D7E7S49Q.js";import"./chunk-FMBD7UC4-DlhrqkBp.js";import"./chunk-ND2GUHAM-lo_8kNzw.js";import"./chunk-55IACEB6-uUk1WzZ1.js";import"./chunk-2J33WTMH-b8Rdk4S-.js";import"./mobile-DwmxS_O4.js";import"./index-DNuHtZwR.js";import"./graph-BBXaecIU.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as r,b as e,a,S as s}from"./chunk-AQP2D5EJ-B6GCyrIp.js";import{_ as i}from"./App-D7E7S49Q.js";import"./chunk-55IACEB6-uUk1WzZ1.js";import"./chunk-2J33WTMH-b8Rdk4S-.js";import"./mobile-DwmxS_O4.js";import"./index-DNuHtZwR.js";import"./graph-BBXaecIU.js";var n={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{n as diagram};