@smartmemory/compose 0.2.58-beta → 0.3.1-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/README.md +2 -2
- package/bin/compose.js +55 -54
- package/dist/assets/{App-s4CulNkO.js → App-DR9pCiNE.js} +290 -290
- package/dist/assets/{abnfDiagram-VRR7QNED-CDwzQ5wr.js → abnfDiagram-VRR7QNED-CZ6p2bRe.js} +1 -1
- package/dist/assets/{arc-kIU8DDth.js → arc-6j04LBqn.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-CleorO_0.js → architectureDiagram-ZJ3FMSHR-00QM3pmc.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-NvEFfoMk.js → blockDiagram-677ZJIJ3-BUb9Kjfd.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-BcDQdTVB.js → c4Diagram-LMCZKHZV-D1PkhmSm.js} +1 -1
- package/dist/assets/channel-BcWZM2KA.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BseRkpm4.js → chunk-2Q5K7J3B-CduDdMf4.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-DCHhvgFV.js → chunk-32BRIVSS-CH8Kj5rh.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DcTpMdJ4.js → chunk-5VM5RSS4-Cfts6Lix.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BdN8fvWs.js → chunk-EX3LRPZG-Dguk4yKZ.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmZJP-8E.js → chunk-JWPE2WC7-DbCJp6t9.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-V74uYM17.js → chunk-MOJQB5TN-CNjM_9_U.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CoXUXbn_.js → chunk-RYQCIY6F-D4k-BUbh.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DUscIydR.js → chunk-V7JOEXUC-O1sFoHf1.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C4UDHq_a.js → chunk-VR4S4FIN-CGbSXVPL.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DIH9i8mZ.js → chunk-XXDRQBXY-iIelV7SO.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Ce7YjlIK.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-Ce7YjlIK.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-Dn6Gy6l1.js → cose-bilkent-JH36ORCC-BKmPLgw2.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C2LW5fzB.js → cynefin-VYW2F7L2-CmT6ieC5.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-CGoAq7C9.js → cynefinDiagram-TSTJHNR4-Hn2vSVOw.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-BaOKtuBE.js → dagre-VKFMJZFB-B0ifIwlU.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BdQwqt4x.js → diagram-FQU43EPY-BuCDB-JG.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-Dkcf0o5z.js → diagram-G47NLZAW-D5avTZAQ.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-C_fDi59r.js → diagram-NH7WQ7WH-24QCIgpj.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-jaTQcOxt.js → diagram-OA4YK3LP-A-bbUTK3.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-dEtqVj3s.js → diagram-WEI45ONY-BIMDm-Kj.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-s8ImqsSs.js → ebnfDiagram-CCIWWBDH-DCXMfG-M.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-5Zy29-SA.js → erDiagram-Q63AITRT-BlDrG0Ev.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-DV74V9he.js → flowDiagram-23GEKE2U-aTyazY6P.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-BND-CiLO.js → ganttDiagram-NO4QXBWP-Bxo-a80f.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js → gitGraphDiagram-IHSO6WYX-COhKb9wR.js} +1 -1
- package/dist/assets/index-BcmsQZX-.js +119 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-Duyz8rZM.js → infoDiagram-FWYZ7A6U-DLCbpinV.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-D2Qft0e5.js → ishikawaDiagram-FXEZZL3T-eyHF7GIj.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-q6rG4Rig.js → journeyDiagram-5HDEW3XC-BsVocmlw.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CsPX7gBL.js → kanban-definition-HUTT4EX6-CaFPSZxY.js} +1 -1
- package/dist/assets/{linear-CiccpAAd.js → linear-cqnI_wgW.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-DRW2njpA.js → mindmap-definition-LN4V7U3C-B66BGqKn.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-DoDgm2S_.js → pegDiagram-2B236MQR-F6IN0_L5.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-CNSYLVAF.js → pieDiagram-ENE6RG2P-D5nOQxpo.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-8-Nd6ony.js → quadrantDiagram-ABIIQ3AL-DwUAYbMO.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-bfdg6Bo4.js → railroadDiagram-RFXS5EU6-Cvmlah5P.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-slDkxCQy.js → requirementDiagram-TGXJPOKE-DoUtsFnL.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-CR3CboiK.js → sankeyDiagram-HTMAVEWB-D4XTwiKp.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BIXw1JS1.js → sequenceDiagram-DBY2YBRQ-CiG9yRIR.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-BdKI7JyF.js → sizeCapture-X5ZJPWSS-zd7ydQ_Y.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-ktVgpBZS.js → stateDiagram-2N3HPSRC-BZesBHQ4.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BnlLOJzB.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CaXpC3FM.js → swimlanes-5IMT3BWC-Bg8exSE7.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-DZMVxqbs.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CKWSlRUl.js → timeline-definition-FHXFAJF6-TKj_HY9k.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-CwOhxDyv.js → vennDiagram-L72KCM5P-CJf-bqn0.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DGavMpEB.js → wardleyDiagram-EHGQE667-CqizSfCL.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CWmnmLZz.js → xychartDiagram-FW5EYKEG-CK-OErMM.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +0 -5
- package/lib/build-stream-schema.js +1 -1
- package/lib/build.js +1762 -2614
- package/lib/consumer-fanout.js +1317 -0
- package/lib/feature-validator.js +6 -4
- package/lib/flow-state.js +15 -14
- package/lib/gsd-budget.js +48 -10
- package/lib/gsd-prompt.js +3 -4
- package/lib/gsd-stuck.js +1 -1
- package/lib/gsd.js +303 -149
- package/lib/local-claude-connector.js +149 -0
- package/lib/new.js +162 -307
- package/lib/result-normalizer.js +233 -18
- package/lib/review-lenses.js +1 -1
- package/lib/step-prompt.js +41 -119
- package/lib/stratum-engine.js +297 -0
- package/lib/stratum-mcp-client.js +224 -213
- package/lib/vocabulary-compliance.js +268 -0
- package/lib/vocabulary-inject.js +1 -36
- package/package.json +6 -2
- package/pipelines/build-quick.stratum.yaml +5 -17
- package/pipelines/build.profiles.json +11 -0
- package/pipelines/build.stratum.yaml +288 -467
- package/pipelines/gsd.stratum.yaml +73 -125
- package/pipelines/new.stratum.yaml +68 -149
- package/server/build-routes.js +4 -1
- package/server/design-routes.js +37 -21
- package/server/index.js +13 -21
- package/server/lifecycle-guard.js +1 -1
- package/server/pipeline-routes.js +113 -31
- package/server/stratum-client.js +33 -47
- package/server/stratum-sync.js +3 -4
- package/server/vision-server.js +1 -1
- package/dist/assets/channel-DiJkE9og.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DS76VBEL.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DS76VBEL.js +0 -1
- package/dist/assets/index-CBWbmG7W.js +0 -119
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +0 -8
- package/lib/connector-factory-shim.js +0 -167
- package/server/agent-mcp.js +0 -10
|
@@ -312,7 +312,7 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode
|
|
|
312
312
|
workspaceRoot,
|
|
313
313
|
});
|
|
314
314
|
} catch (e) {
|
|
315
|
-
// A thrown
|
|
315
|
+
// A thrown Stratum CLI failure must NOT escape as
|
|
316
316
|
// a generic 500/400 — normalise to a fail-closed error result.
|
|
317
317
|
return { error: { code: 'GUARD_UNREACHABLE', message: e.message } };
|
|
318
318
|
}
|
|
@@ -184,14 +184,23 @@ function flowNamesOf(parsed) {
|
|
|
184
184
|
* src/lib/pipeline-model.js `denormalizeStep` (kept in sync; server cannot import
|
|
185
185
|
* the frontend module). `id` leads; surfaced fields override `_extra` on clash.
|
|
186
186
|
*/
|
|
187
|
-
|
|
187
|
+
// D2/D3: the physical instruction key is VERSION-derived — `do` for TS v1,
|
|
188
|
+
// `intent` for v0.3 — never presence-derived. Mirrors src/lib/pipeline-model.js.
|
|
189
|
+
function intentKeyForVersion(version) {
|
|
190
|
+
return version === 1 ? 'do' : 'intent';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function denormalizeModelStep(step, version) {
|
|
188
194
|
const out = { id: step.id };
|
|
189
195
|
if (step._extra && typeof step._extra === 'object') {
|
|
190
196
|
for (const key of Object.keys(step._extra)) out[key] = step._extra[key];
|
|
191
197
|
}
|
|
192
198
|
if (step.agent !== undefined) out.agent = step.agent;
|
|
193
199
|
if (step.function !== undefined) out.function = step.function;
|
|
194
|
-
|
|
200
|
+
// C3/D2/D3: write the instruction to its physical field so a serialized v1 step
|
|
201
|
+
// never carries an undeclared `intent`. A NEW step (no `_intentKey`) defaults to
|
|
202
|
+
// the spec version's key, not `intent`.
|
|
203
|
+
if (step.intent !== undefined) out[step._intentKey || intentKeyForVersion(version)] = step.intent;
|
|
195
204
|
if (step.inputs && Object.keys(step.inputs).length > 0) out.inputs = { ...step.inputs };
|
|
196
205
|
if (step.output_contract !== undefined) out.output_contract = step.output_contract;
|
|
197
206
|
if (Array.isArray(step.ensure) && step.ensure.length > 0) out.ensure = [...step.ensure];
|
|
@@ -210,6 +219,33 @@ function modelFlow(model, flowName) {
|
|
|
210
219
|
return model.flows.find(f => f && f.name === flowName) || null;
|
|
211
220
|
}
|
|
212
221
|
|
|
222
|
+
/**
|
|
223
|
+
* F2: is this a GENUINE full-document (spec-wide) save? An omitted `flowName` is
|
|
224
|
+
* NOT sufficient — a partial model (e.g. `{flows:[oneFlow]}`) with no flowName would
|
|
225
|
+
* otherwise perform a one-flow write through the hash-OPTIONAL spec-wide branch,
|
|
226
|
+
* bypassing E1's staleness guard. A true spec-wide save carries the authoritative
|
|
227
|
+
* full document (`model._doc`) AND covers EVERY flow present on disk (flows with a
|
|
228
|
+
* steps[] array, plus a workflow.steps single flow). Anything missing a disk flow
|
|
229
|
+
* is a flow-scoped write in disguise → classified flow-scoped (baseHash required).
|
|
230
|
+
*/
|
|
231
|
+
function coversFullDocument(model, diskParsed) {
|
|
232
|
+
if (!model || !model._doc || typeof model._doc !== 'object') return false;
|
|
233
|
+
const diskFlowNames = new Set();
|
|
234
|
+
if (diskParsed && diskParsed.flows && typeof diskParsed.flows === 'object') {
|
|
235
|
+
for (const n of Object.keys(diskParsed.flows)) {
|
|
236
|
+
if (Array.isArray(diskParsed.flows[n]?.steps)) diskFlowNames.add(n);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(diskParsed?.workflow?.steps)) {
|
|
240
|
+
diskFlowNames.add(diskParsed.workflow.name || 'workflow');
|
|
241
|
+
}
|
|
242
|
+
const modelFlowNames = new Set((model.flows || []).map(f => f && f.name).filter(Boolean));
|
|
243
|
+
for (const n of diskFlowNames) {
|
|
244
|
+
if (!modelFlowNames.has(n)) return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
// The reserved built-in contract — never written as a user contract (mirrors
|
|
214
250
|
// src/lib/pipeline-model.js serializeContracts; server can't import the FE module).
|
|
215
251
|
const RESERVED_CONTRACT = 'TaskGraph';
|
|
@@ -470,14 +506,32 @@ function surfacedValue(step, key) {
|
|
|
470
506
|
*/
|
|
471
507
|
function mergeStepNode(doc, mapNode, step, specWide = false) {
|
|
472
508
|
const extra = (step._extra && typeof step._extra === 'object') ? step._extra : {};
|
|
509
|
+
// C3/D2: the surfaced `intent` is written to its PHYSICAL field — `do` for TS v1,
|
|
510
|
+
// `intent` for v0.3. Prefer the model's remembered key; fall back to the spec
|
|
511
|
+
// VERSION (never the disk node's presence — a v1 step carrying both fields must
|
|
512
|
+
// resolve to `do`, and the stray `intent` is cleaned up below).
|
|
513
|
+
const intentKey = step._intentKey || (intentKeyForVersion(doc.get('version')));
|
|
473
514
|
if (!specWide) {
|
|
474
|
-
//
|
|
515
|
+
// D4: DIFF-DRIVEN, and NEVER delete. A key absent from an incomplete client
|
|
516
|
+
// _extra is preserved (finding 2); a key PRESENT and CHANGED is written back —
|
|
517
|
+
// whatever it is. This persists every field a renameStep rewrote (C4: after[],
|
|
518
|
+
// nested gate routes, AND `when`/`set`/fanout.over/with templates) without an
|
|
519
|
+
// allowlist, so new template-bearing fields are future-proofed. Unchanged keys
|
|
520
|
+
// are left untouched, preserving their comments.
|
|
475
521
|
for (const k of Object.keys(extra)) {
|
|
476
|
-
if (!mapNode.has(k))
|
|
522
|
+
if (!mapNode.has(k)) {
|
|
523
|
+
mapNode.set(k, doc.createNode(extra[k]));
|
|
524
|
+
} else {
|
|
525
|
+
const diskNode = mapNode.get(k, true);
|
|
526
|
+
const diskJs = diskNode && typeof diskNode.toJSON === 'function' ? diskNode.toJSON() : mapNode.get(k);
|
|
527
|
+
if (!deepEqual(diskJs, extra[k])) mapNode.set(k, doc.createNode(extra[k]));
|
|
528
|
+
}
|
|
477
529
|
}
|
|
478
530
|
} else {
|
|
479
|
-
// Full reconcile of unsurfaced fields.
|
|
480
|
-
|
|
531
|
+
// Full reconcile of unsurfaced fields. The physical intent key is surfaced
|
|
532
|
+
// (below), so it must be treated as surfaced here too — never deleted as an
|
|
533
|
+
// unknown extra.
|
|
534
|
+
const surfacedSet = new Set(['id', ...SURFACED_STEP_KEYS, intentKey]);
|
|
481
535
|
// Add / update changed extra keys (skip unchanged to preserve their comments).
|
|
482
536
|
for (const k of Object.keys(extra)) {
|
|
483
537
|
if (!mapNode.has(k)) {
|
|
@@ -500,8 +554,15 @@ function mergeStepNode(doc, mapNode, step, specWide = false) {
|
|
|
500
554
|
if (mapNode.get('id') !== step.id) mapNode.set('id', step.id);
|
|
501
555
|
for (const key of SURFACED_STEP_KEYS) {
|
|
502
556
|
const { present, value } = surfacedValue(step, key);
|
|
503
|
-
|
|
504
|
-
|
|
557
|
+
// C3: the `intent` surfaced key maps to its physical field (`do`/`intent`).
|
|
558
|
+
const diskKey = key === 'intent' ? intentKey : key;
|
|
559
|
+
if (present) mapNode.set(diskKey, doc.createNode(value));
|
|
560
|
+
else if (mapNode.has(diskKey)) mapNode.delete(diskKey);
|
|
561
|
+
// Never leave the stale sibling — e.g. a python-era `intent` on a v1 step.
|
|
562
|
+
if (key === 'intent') {
|
|
563
|
+
const stray = intentKey === 'do' ? 'intent' : 'do';
|
|
564
|
+
if (mapNode.has(stray)) mapNode.delete(stray);
|
|
565
|
+
}
|
|
505
566
|
}
|
|
506
567
|
return mapNode;
|
|
507
568
|
}
|
|
@@ -804,18 +865,6 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
|
|
|
804
865
|
return res.status(400).json({ error: 'model with flows[] is required' });
|
|
805
866
|
}
|
|
806
867
|
|
|
807
|
-
// Determine which flows to write back. If flowName is given, write only that
|
|
808
|
-
// flow (the editor edits one flow at a time); otherwise write every flow the
|
|
809
|
-
// model carries that exists in the document.
|
|
810
|
-
const flowsToWrite = flowName
|
|
811
|
-
? [flowName]
|
|
812
|
-
: model.flows.map(f => f && f.name).filter(Boolean);
|
|
813
|
-
|
|
814
|
-
// A SPEC-WIDE save (flowName omitted) treats model._doc as the authoritative
|
|
815
|
-
// full document: reconcilers update AND delete, not just add. The flow-scoped
|
|
816
|
-
// path (flowName given) stays additive and never touches other flows/blocks.
|
|
817
|
-
const specWide = !flowName;
|
|
818
|
-
|
|
819
868
|
let doc;
|
|
820
869
|
let diskParsed;
|
|
821
870
|
let diskText;
|
|
@@ -825,10 +874,24 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
|
|
|
825
874
|
return res.status(400).json({ error: `Cannot read current spec: ${e.message}` });
|
|
826
875
|
}
|
|
827
876
|
|
|
828
|
-
// COMP-PIPE-EDIT-6
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
|
|
877
|
+
// Conflict detection (COMP-PIPE-EDIT-6 + E1/G1). ANY save of an existing file
|
|
878
|
+
// resubmits a model derived from a snapshot of disk; a stale editor (loaded old,
|
|
879
|
+
// another writer saved new, this client resubmits its stale value) would SILENTLY
|
|
880
|
+
// resurrect the old value on merge. G1: proving "full-document-ness" from request
|
|
881
|
+
// shape is spoofable (an empty `_doc` + stub flow bodies covering every disk flow
|
|
882
|
+
// NAME would slip through a hash-optional spec-wide branch and destructively
|
|
883
|
+
// delete real steps), so the guard NO LONGER keys on classification: a baseHash
|
|
884
|
+
// is REQUIRED on EVERY save of an existing file — spec-wide and flow-scoped alike.
|
|
885
|
+
// Missing → 400, stale → 409. F1: ONLY strict `force === true` bypasses — a
|
|
886
|
+
// truthy-but-non-boolean `force` ("false", 1, {}) must NOT authorize the bypass.
|
|
887
|
+
// G3: this check runs on the RAW disk bytes BEFORE parsing, so a stale baseHash
|
|
888
|
+
// is a 409 even when the on-disk YAML is malformed (parse-independent semantics).
|
|
889
|
+
if (force !== true) {
|
|
890
|
+
if (typeof baseHash !== 'string' || baseHash.length === 0) {
|
|
891
|
+
return res.status(400).json({
|
|
892
|
+
error: 'baseHash is required to save an existing spec (reload the spec, or pass force:true to override)',
|
|
893
|
+
});
|
|
894
|
+
}
|
|
832
895
|
const currentHash = sha256(diskText);
|
|
833
896
|
if (currentHash !== baseHash) {
|
|
834
897
|
return res.status(409).json({
|
|
@@ -846,6 +909,22 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
|
|
|
846
909
|
return res.status(400).json({ error: `Failed to parse current spec: ${e.message}` });
|
|
847
910
|
}
|
|
848
911
|
|
|
912
|
+
// A SPEC-WIDE save treats model._doc as the authoritative full document:
|
|
913
|
+
// reconcilers update AND delete, not just add. The flow-scoped path stays
|
|
914
|
+
// additive and never touches other flows/blocks. This classification now drives
|
|
915
|
+
// MERGE BEHAVIOR ONLY (full replace vs additive) — it never relaxes the baseHash
|
|
916
|
+
// guard above (G1). An omitted `flowName` alone does NOT make a save spec-wide:
|
|
917
|
+
// the model must genuinely cover the full document (model._doc + every disk flow),
|
|
918
|
+
// else a partial model writes additively as a flow-scoped save.
|
|
919
|
+
const specWide = !flowName && coversFullDocument(model, diskParsed);
|
|
920
|
+
|
|
921
|
+
// Determine which flows to write back. If flowName is given, write only that
|
|
922
|
+
// flow (the editor edits one flow at a time); otherwise write every flow the
|
|
923
|
+
// model carries that exists in the document.
|
|
924
|
+
const flowsToWrite = flowName
|
|
925
|
+
? [flowName]
|
|
926
|
+
: model.flows.map(f => f && f.name).filter(Boolean);
|
|
927
|
+
|
|
849
928
|
for (const name of flowsToWrite) {
|
|
850
929
|
const flow = modelFlow(model, name);
|
|
851
930
|
if (!flow) {
|
|
@@ -868,10 +947,13 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
|
|
|
868
947
|
// COMP-PIPE-EDIT-5: the flow is not on disk yet (e.g. a just-collapsed
|
|
869
948
|
// sub-flow that exists only in the model). CREATE the flows.<name> node —
|
|
870
949
|
// steps + input + output from model._doc.flows — rather than erroring.
|
|
871
|
-
// ONLY on a SPEC-WIDE save
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
950
|
+
// ONLY on a genuine SPEC-WIDE save: G2 — gate on the computed `specWide`
|
|
951
|
+
// classification, not a bare `!flowName`. A flow-scoped-classified request
|
|
952
|
+
// (no flowName but a partial model that does NOT cover the full document)
|
|
953
|
+
// must NOT perform this documented spec-wide-only flow creation; an explicit
|
|
954
|
+
// flowName that doesn't map to a steps location is likewise a 400 (never
|
|
955
|
+
// auto-create a flow the caller named by mistake / clobber a workflow.steps).
|
|
956
|
+
const created = specWide && createNewFlowNode(doc, model, name);
|
|
875
957
|
if (!created) {
|
|
876
958
|
return res.status(400).json({ error: `Flow "${name}" has no steps location in the spec` });
|
|
877
959
|
}
|
|
@@ -902,7 +984,7 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
|
|
|
902
984
|
}
|
|
903
985
|
return existing
|
|
904
986
|
? mergeStepNode(doc, existing, step, specWide)
|
|
905
|
-
: doc.createNode(denormalizeModelStep(step));
|
|
987
|
+
: doc.createNode(denormalizeModelStep(step, doc.get('version')));
|
|
906
988
|
});
|
|
907
989
|
}
|
|
908
990
|
|
|
@@ -1041,7 +1123,7 @@ function buildSpecObjectFromModel(model) {
|
|
|
1041
1123
|
if (Array.isArray(doc?.workflow?.steps)) {
|
|
1042
1124
|
const wfName = doc.workflow.name || 'workflow';
|
|
1043
1125
|
const flow = byName.get(wfName);
|
|
1044
|
-
if (flow) out.workflow = { ...doc.workflow, steps: (flow.steps || []).map(denormalizeModelStep) };
|
|
1126
|
+
if (flow) out.workflow = { ...doc.workflow, steps: (flow.steps || []).map(s => denormalizeModelStep(s, doc.version)) };
|
|
1045
1127
|
}
|
|
1046
1128
|
|
|
1047
1129
|
if (doc?.flows && typeof doc.flows === 'object') {
|
|
@@ -1049,7 +1131,7 @@ function buildSpecObjectFromModel(model) {
|
|
|
1049
1131
|
for (const name of Object.keys(doc.flows)) {
|
|
1050
1132
|
const flow = byName.get(name);
|
|
1051
1133
|
out.flows[name] = flow
|
|
1052
|
-
? { ...doc.flows[name], steps: (flow.steps || []).map(denormalizeModelStep) }
|
|
1134
|
+
? { ...doc.flows[name], steps: (flow.steps || []).map(s => denormalizeModelStep(s, doc.version)) }
|
|
1053
1135
|
: doc.flows[name];
|
|
1054
1136
|
}
|
|
1055
1137
|
}
|
package/server/stratum-client.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stratum-client.js — Single adapter for all
|
|
2
|
+
* stratum-client.js — Single adapter for all Stratum TS CLI subprocess calls.
|
|
3
3
|
*
|
|
4
|
-
* This is the ONLY module in compose that spawns
|
|
4
|
+
* This is the ONLY module in compose that spawns Stratum CLI processes.
|
|
5
5
|
* All query and mutation calls go through the exported functions below.
|
|
6
|
-
* No other file may
|
|
6
|
+
* No other file may spawn the Stratum CLI directly.
|
|
7
7
|
*
|
|
8
8
|
* Contract:
|
|
9
9
|
* - Query calls: 5s timeout, 1 retry on timeout, no retry on error
|
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { execFile as _execFileDefault } from 'node:child_process';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
const STRATUM_BIN = 'stratum-mcp';
|
|
18
|
+
import { getTargetRoot } from './project-root.js';
|
|
19
|
+
import { resolveStratumBin, resolveStratumEngine as resolveEngine } from '../lib/stratum-engine.js';
|
|
21
20
|
|
|
22
21
|
// Injected executor — replaced by tests only. Production code never calls this setter.
|
|
23
22
|
let _execFile = _execFileDefault;
|
|
@@ -28,31 +27,21 @@ const MUTATION_TIMEOUT_MS = 10_000;
|
|
|
28
27
|
// ---------------------------------------------------------------------------
|
|
29
28
|
// Engine selection (COMP-STRATUM-TS)
|
|
30
29
|
//
|
|
31
|
-
//
|
|
32
|
-
// the
|
|
33
|
-
//
|
|
34
|
-
// the TS port. Resolution order: COMPOSE_STRATUM_ENGINE env override, then
|
|
35
|
-
// capabilities.stratumEngine in .compose/compose.json, then "python".
|
|
36
|
-
// Unknown values fail loudly; never a silent fallback.
|
|
30
|
+
// All flow, gate, and guard calls use the TS CLI. The shared resolver still
|
|
31
|
+
// reads the environment/project capability so a retired Python selection fails
|
|
32
|
+
// loudly before any subprocess is spawned.
|
|
37
33
|
// ---------------------------------------------------------------------------
|
|
38
34
|
|
|
39
|
-
/** @returns {'
|
|
35
|
+
/** @returns {'ts'} */
|
|
40
36
|
export function resolveStratumEngine() {
|
|
41
|
-
|
|
42
|
-
if (value === undefined || value === '') {
|
|
43
|
-
try { value = loadProjectConfig()?.capabilities?.stratumEngine; } catch { value = undefined; }
|
|
44
|
-
}
|
|
45
|
-
if (value === undefined || value === '') return 'python';
|
|
46
|
-
if (value !== 'python' && value !== 'ts') {
|
|
47
|
-
throw new Error(`stratumEngine must be "python" or "ts", got ${JSON.stringify(value)}`);
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
37
|
+
return resolveEngine(getTargetRoot());
|
|
50
38
|
}
|
|
51
39
|
|
|
52
40
|
/** Binary for flow/gate query+mutation calls under the selected engine. */
|
|
53
41
|
function flowGateBin() {
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
const cwd = getTargetRoot();
|
|
43
|
+
resolveEngine(cwd);
|
|
44
|
+
return resolveStratumBin('cli', cwd);
|
|
56
45
|
}
|
|
57
46
|
|
|
58
47
|
// ---------------------------------------------------------------------------
|
|
@@ -65,10 +54,10 @@ function flowGateBin() {
|
|
|
65
54
|
*
|
|
66
55
|
* @param {string[]} args
|
|
67
56
|
* @param {number} timeoutMs
|
|
68
|
-
* @param {string} [bin] —
|
|
57
|
+
* @param {string} [bin] — explicit binary selected by the calling seam
|
|
69
58
|
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
70
59
|
*/
|
|
71
|
-
function spawnStratum(args, timeoutMs, bin
|
|
60
|
+
function spawnStratum(args, timeoutMs, bin) {
|
|
72
61
|
return new Promise((resolve) => {
|
|
73
62
|
const proc = _execFile(bin, args, { timeout: timeoutMs }, (err, out, err2) => {
|
|
74
63
|
resolve(_spawnResult(bin, err, out, err2));
|
|
@@ -110,9 +99,7 @@ function _spawnResult(bin, err, out, err2) {
|
|
|
110
99
|
|
|
111
100
|
/** Binary-specific spawn-failure message with the install/path remedy. */
|
|
112
101
|
function _spawnRemedy(bin, code) {
|
|
113
|
-
return bin
|
|
114
|
-
? `stratum-mcp failed to spawn (${code}). Install with: pip install stratum-mcp`
|
|
115
|
-
: `${bin} (TS stratum engine) failed to spawn (${code}). Install @smartmemory/stratum or set COMPOSE_STRATUM_TS_BIN`;
|
|
102
|
+
return `${bin} (TS stratum engine) failed to spawn (${code}). Install @smartmemory/stratum or set COMPOSE_STRATUM_TS_CLI_BIN`;
|
|
116
103
|
}
|
|
117
104
|
|
|
118
105
|
/**
|
|
@@ -128,7 +115,7 @@ async function runQuery(args) {
|
|
|
128
115
|
// Retry once on timeout
|
|
129
116
|
result = await spawnStratum(args, QUERY_TIMEOUT_MS, bin);
|
|
130
117
|
if (result.code === -1) {
|
|
131
|
-
return { error: { code: 'TIMEOUT', message: '
|
|
118
|
+
return { error: { code: 'TIMEOUT', message: 'Stratum query timed out', detail: '' } };
|
|
132
119
|
}
|
|
133
120
|
}
|
|
134
121
|
|
|
@@ -142,14 +129,14 @@ async function runQuery(args) {
|
|
|
142
129
|
try {
|
|
143
130
|
return JSON.parse(result.stdout);
|
|
144
131
|
} catch {
|
|
145
|
-
return { error: { code: 'UNKNOWN', message: '
|
|
132
|
+
return { error: { code: 'UNKNOWN', message: 'Stratum query failed', detail: '' } };
|
|
146
133
|
}
|
|
147
134
|
}
|
|
148
135
|
|
|
149
136
|
try {
|
|
150
137
|
return JSON.parse(result.stdout);
|
|
151
138
|
} catch {
|
|
152
|
-
return { error: { code: 'PARSE_ERROR', message: '
|
|
139
|
+
return { error: { code: 'PARSE_ERROR', message: 'Stratum returned invalid JSON', detail: '' } };
|
|
153
140
|
}
|
|
154
141
|
}
|
|
155
142
|
|
|
@@ -162,7 +149,7 @@ async function runMutation(args) {
|
|
|
162
149
|
const result = await spawnStratum(args, MUTATION_TIMEOUT_MS, flowGateBin());
|
|
163
150
|
|
|
164
151
|
if (result.code === -1) {
|
|
165
|
-
return { error: { code: 'TIMEOUT', message: '
|
|
152
|
+
return { error: { code: 'TIMEOUT', message: 'Stratum gate timed out', detail: '' } };
|
|
166
153
|
}
|
|
167
154
|
|
|
168
155
|
if (result.code === -2) {
|
|
@@ -183,36 +170,35 @@ async function runMutation(args) {
|
|
|
183
170
|
try {
|
|
184
171
|
return JSON.parse(result.stdout);
|
|
185
172
|
} catch {
|
|
186
|
-
return { error: { code: 'UNKNOWN', message: '
|
|
173
|
+
return { error: { code: 'UNKNOWN', message: 'Stratum gate failed', detail: '' } };
|
|
187
174
|
}
|
|
188
175
|
}
|
|
189
176
|
|
|
190
177
|
try {
|
|
191
178
|
return JSON.parse(result.stdout);
|
|
192
179
|
} catch {
|
|
193
|
-
return { error: { code: 'PARSE_ERROR', message: '
|
|
180
|
+
return { error: { code: 'PARSE_ERROR', message: 'Stratum returned invalid JSON', detail: '' } };
|
|
194
181
|
}
|
|
195
182
|
}
|
|
196
183
|
|
|
197
184
|
/**
|
|
198
|
-
* Spawn
|
|
185
|
+
* Spawn the TS Stratum CLI with args and pipe `inputJson` on stdin.
|
|
199
186
|
* Used by the STRAT-GUARD adapter, whose CLI reads one JSON kwargs object from
|
|
200
|
-
* stdin. Same resolve contract as spawnStratum.
|
|
201
|
-
* STRAT-GUARD is not part of the TS port and ignores the engine flag.
|
|
187
|
+
* stdin. Same resolve contract as spawnStratum.
|
|
202
188
|
*
|
|
203
189
|
* @param {string[]} args
|
|
204
190
|
* @param {string} inputJson
|
|
205
191
|
* @param {number} timeoutMs
|
|
206
192
|
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
207
193
|
*/
|
|
208
|
-
function spawnStratumStdin(args, inputJson, timeoutMs) {
|
|
194
|
+
function spawnStratumStdin(args, inputJson, timeoutMs, bin) {
|
|
209
195
|
return new Promise((resolve) => {
|
|
210
|
-
const proc = _execFile(
|
|
211
|
-
resolve(_spawnResult(
|
|
196
|
+
const proc = _execFile(bin, args, { timeout: timeoutMs }, (err, out, err2) => {
|
|
197
|
+
resolve(_spawnResult(bin, err, out, err2));
|
|
212
198
|
});
|
|
213
199
|
// Same both-paths-settle-identically contract as spawnStratum.
|
|
214
200
|
proc.on('error', (err) => {
|
|
215
|
-
resolve(_spawnResult(
|
|
201
|
+
resolve(_spawnResult(bin, err ?? new Error('child process error'), '', ''));
|
|
216
202
|
});
|
|
217
203
|
|
|
218
204
|
// Feed the JSON kwargs on stdin. The test mock supplies a fake stdin; a
|
|
@@ -234,10 +220,10 @@ function spawnStratumStdin(args, inputJson, timeoutMs) {
|
|
|
234
220
|
* @returns {Promise<any>} parsed JSON result or { error }
|
|
235
221
|
*/
|
|
236
222
|
async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS) {
|
|
237
|
-
const result = await spawnStratumStdin(['guard', action], JSON.stringify(kwargs), timeoutMs);
|
|
223
|
+
const result = await spawnStratumStdin(['guard', action], JSON.stringify(kwargs), timeoutMs, flowGateBin());
|
|
238
224
|
|
|
239
225
|
if (result.code === -1) {
|
|
240
|
-
return { error: { code: 'TIMEOUT', message: '
|
|
226
|
+
return { error: { code: 'TIMEOUT', message: 'Stratum guard timed out', detail: '' } };
|
|
241
227
|
}
|
|
242
228
|
if (result.code === -2) {
|
|
243
229
|
console.error('[stratum-client] guard spawn failure:', result.stderr);
|
|
@@ -248,13 +234,13 @@ async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS) {
|
|
|
248
234
|
try {
|
|
249
235
|
return JSON.parse(result.stdout); // canonical { status:"error", ... }
|
|
250
236
|
} catch {
|
|
251
|
-
return { error: { code: 'UNKNOWN', message: '
|
|
237
|
+
return { error: { code: 'UNKNOWN', message: 'Stratum guard failed', detail: '' } };
|
|
252
238
|
}
|
|
253
239
|
}
|
|
254
240
|
try {
|
|
255
241
|
return JSON.parse(result.stdout);
|
|
256
242
|
} catch {
|
|
257
|
-
return { error: { code: 'PARSE_ERROR', message: '
|
|
243
|
+
return { error: { code: 'PARSE_ERROR', message: 'Stratum returned invalid JSON', detail: '' } };
|
|
258
244
|
}
|
|
259
245
|
}
|
|
260
246
|
|
|
@@ -334,7 +320,7 @@ export async function gateRevise(flowId, stepId, note = '', resolvedBy = 'human'
|
|
|
334
320
|
//
|
|
335
321
|
// Reaches stratum's guarded-transition primitive over the same CLI-subprocess
|
|
336
322
|
// seam. Each function translates camelCase params into the snake_case JSON
|
|
337
|
-
// kwargs the `stratum
|
|
323
|
+
// kwargs the `stratum guard <action>` CLI forwards verbatim to the guard
|
|
338
324
|
// library, and pipes them on stdin.
|
|
339
325
|
// ---------------------------------------------------------------------------
|
|
340
326
|
|
package/server/stratum-sync.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Polls stratum flow state via stratum-client (not direct file reads) and syncs
|
|
5
5
|
* into the vision store on a self-scheduling, sleep-aware loop.
|
|
6
6
|
*
|
|
7
|
-
* Sleep behavior: each poll cold-starts
|
|
8
|
-
* interpreter spin-up — the expensive part, not the query itself). Doing that
|
|
7
|
+
* Sleep behavior: each poll cold-starts the Stratum TS CLI. Doing that
|
|
9
8
|
* every few seconds generated enough periodic activity to keep the host from
|
|
10
9
|
* sleeping. Two mitigations:
|
|
11
10
|
* 1. Cadence widened to 60s (override with COMPOSE_STRATUM_POLL_MS).
|
|
@@ -37,7 +36,7 @@ const WAKE_GAP_FACTOR = 1.5;
|
|
|
37
36
|
/**
|
|
38
37
|
* Sleep-aware poll gate: true only when some bound vision item could still
|
|
39
38
|
* change state (i.e. is bound to a flow and not settled-`complete`). When this
|
|
40
|
-
* is false the poller skips the
|
|
39
|
+
* is false the poller skips the CLI spawn so an idle host can sleep.
|
|
41
40
|
*
|
|
42
41
|
* A bound `complete` item is steady-state (status only advances to `complete`
|
|
43
42
|
* via the audit route, never back), so polling it changes nothing. Anything
|
|
@@ -97,7 +96,7 @@ export class StratumSync {
|
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
/**
|
|
100
|
-
* One poll tick. Skips the
|
|
99
|
+
* One poll tick. Skips the Stratum CLI spawn when nothing is live (lets the
|
|
101
100
|
* host sleep); on a tick that fired far later than scheduled, notes the
|
|
102
101
|
* sleep gap before resyncing.
|
|
103
102
|
*/
|
package/server/vision-server.js
CHANGED
|
@@ -275,7 +275,7 @@ export class VisionServer {
|
|
|
275
275
|
console.log('[vision] Stratum sync enabled');
|
|
276
276
|
} else {
|
|
277
277
|
app.use('/api/stratum', (_req, res) => {
|
|
278
|
-
res.status(503).json({ error: 'Stratum not enabled', hint: '
|
|
278
|
+
res.status(503).json({ error: 'Stratum not enabled', hint: 'compose init (TS engine; python retired — see python-legacy branch)' });
|
|
279
279
|
});
|
|
280
280
|
}
|
|
281
281
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ag as o,ah as n}from"./App-s4CulNkO.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-V7JOEXUC-DUscIydR.js";import{_ as i}from"./App-s4CulNkO.js";import"./chunk-5VM5RSS4-DcTpMdJ4.js";import"./chunk-XXDRQBXY-DIH9i8mZ.js";import"./chunk-VR4S4FIN-C4UDHq_a.js";import"./chunk-32BRIVSS-DCHhvgFV.js";import"./mobile-PV1zQVwv.js";import"./index-CBWbmG7W.js";import"./graph--wGAvskB.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-V7JOEXUC-DUscIydR.js";import{_ as i}from"./App-s4CulNkO.js";import"./chunk-5VM5RSS4-DcTpMdJ4.js";import"./chunk-XXDRQBXY-DIH9i8mZ.js";import"./chunk-VR4S4FIN-C4UDHq_a.js";import"./chunk-32BRIVSS-DCHhvgFV.js";import"./mobile-PV1zQVwv.js";import"./index-CBWbmG7W.js";import"./graph--wGAvskB.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};
|