@smartmemory/compose 0.2.47-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.
- package/.claude/skills/compose/SKILL.md +2 -0
- package/.compose-deps.json +13 -0
- package/bin/compose.js +5 -2
- package/dist/assets/{App-D7E7S49Q.js → App-DOerhoDp.js} +160 -160
- package/dist/assets/{arc-LOW60tiI.js → arc-B1TqAC_R.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-BDY00hRy.js → architectureDiagram-3BPJPVTR-C4hfxNOw.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-DiC_6ktq.js → blockDiagram-GPEHLZMM-DRqkGncy.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-CXZC8woU.js → c4Diagram-AAUBKEIU-Dc6swoQo.js} +1 -1
- package/dist/assets/channel-KsY1x-7s.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-b8Rdk4S-.js → chunk-2J33WTMH-6HKDSnOw.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-BSxn03f-.js → chunk-4BX2VUAB-CdqXgd68.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-uUk1WzZ1.js → chunk-55IACEB6-D7VauOmU.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-zvXhT6iZ.js → chunk-727SXJPM-CeKpXDif.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-B6GCyrIp.js → chunk-AQP2D5EJ-y2XL-YHV.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DlhrqkBp.js → chunk-FMBD7UC4-C1Z5m8DP.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-lo_8kNzw.js → chunk-ND2GUHAM-DmqZMEQe.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-on-yD-C0.js → chunk-QZHKN3VN-DY9GU2uD.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DxV7Y2bU.js → cose-bilkent-S5V4N54A-B34g9xzV.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-BHf9sURy.js → dagre-BM42HDAG-klZ_GOcw.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-R9Puxzlh.js → diagram-2AECGRRQ-D0FmM6Wf.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-BbjlClKx.js → diagram-5GNKFQAL-DYuhby6q.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-r89Mx4m1.js → diagram-KO2AKTUF-CCtQZgal.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-CstVDJZ3.js → diagram-LMA3HP47-Cd1OqEln.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-xmX57FKh.js → diagram-OG6HWLK6-CiJrhoxo.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-DhLPxvrl.js → erDiagram-TEJ5UH35-C05_Iqmb.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-BExoUaNm.js → flowDiagram-I6XJVG4X-lXpnb-Ch.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-BEXa5a22.js → ganttDiagram-6RSMTGT7-EPbL_2Hr.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-Cbes9api.js → gitGraphDiagram-PVQCEYII-COCoF_vW.js} +1 -1
- package/dist/assets/{index-DNuHtZwR.js → index-DlgxA8dX.js} +2 -2
- package/dist/assets/{infoDiagram-5YYISTIA-UBm4ssfm.js → infoDiagram-5YYISTIA-B1sJw6j5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CksBYHCF.js → ishikawaDiagram-YF4QCWOH-CgdsWdgu.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-CKHkIHky.js → journeyDiagram-JHISSGLW-CyvHmE3z.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-CEahMFzE.js → kanban-definition-UN3LZRKU-Bqjdyv0T.js} +1 -1
- package/dist/assets/{linear-CqLVtYRk.js → linear-DecnRXf0.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-C61zWt4M.js → mindmap-definition-RKZ34NQL-DM1x9vNY.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-BIpZHyJW.js → pieDiagram-4H26LBE5-CRtmCvwV.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-Db7nRbZm.js → quadrantDiagram-W4KKPZXB-CsBLWg27.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-BQqFCAr0.js → requirementDiagram-4Y6WPE33-DD-Gg_y5.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-D-oAAMci.js → sankeyDiagram-5OEKKPKP-BPdMN8UU.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-Dc4Cp0Om.js → sequenceDiagram-3UESZ5HK-D4AENdze.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-B5DkQ8Pr.js → stateDiagram-AJRCARHV-DDplpCf_.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-DU9Fl-FW.js → timeline-definition-PNZ67QCA-BPm2AEQj.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-Bthz7siw.js → vennDiagram-CIIHVFJN-DQCDWwlj.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-nqdlFSIA.js → wardley-L42UT6IY-WERqOmJt.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-BJ1BC5zc.js → wardleyDiagram-YWT4CUSO-5teUSOiI.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-Bib5JrbR.js → xychartDiagram-2RQKCTM6-Ba05SXXF.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build.js +5 -1
- package/lib/deps.js +98 -1
- package/lib/rtk.js +66 -0
- package/lib/state-migrations.js +210 -41
- package/lib/vision-writer.js +6 -13
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +9 -1
- package/server/project-root.js +14 -0
- package/server/vision-store.js +6 -11
- 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/stateDiagram-v2-BHNVJYJU-BM1Uf6a_.js +0 -1
package/lib/state-migrations.js
CHANGED
|
@@ -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 = 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
|
-
|
|
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
|
}
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
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);
|
package/server/project-root.js
CHANGED
|
@@ -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
|
+
}
|
package/server/vision-store.js
CHANGED
|
@@ -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.
|
|
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
|
|
116
|
-
if (
|
|
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};
|