@smartmemory/compose 0.2.57-beta → 0.3.0

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 (104) hide show
  1. package/README.md +2 -2
  2. package/bin/compose.js +55 -54
  3. package/contracts/feature-json.schema.json +16 -0
  4. package/dist/assets/{App-BmhlHOXF.js → App-DJ5xk_Wx.js} +219 -219
  5. package/dist/assets/{abnfDiagram-VRR7QNED-CMOTYAnt.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
  6. package/dist/assets/{arc-BJAv_6dL.js → arc-DX5jqmcO.js} +1 -1
  7. package/dist/assets/{architectureDiagram-ZJ3FMSHR-xa4DLUcp.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
  8. package/dist/assets/{blockDiagram-677ZJIJ3-BdkDqeQF.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
  9. package/dist/assets/{c4Diagram-LMCZKHZV-DiNrLIuL.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
  10. package/dist/assets/channel-vsDnTvkh.js +1 -0
  11. package/dist/assets/{chunk-2Q5K7J3B-BsjtgrSL.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
  12. package/dist/assets/{chunk-32BRIVSS-DpImULWy.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
  13. package/dist/assets/{chunk-5VM5RSS4-DTdG5JnR.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
  14. package/dist/assets/{chunk-EX3LRPZG-Cj1jNuKY.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
  15. package/dist/assets/{chunk-JWPE2WC7-DwQ0wwWn.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
  16. package/dist/assets/{chunk-MOJQB5TN-DZZa7unc.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
  17. package/dist/assets/{chunk-RYQCIY6F-CgdjqYQj.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
  18. package/dist/assets/{chunk-V7JOEXUC-DrPIj6XI.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
  19. package/dist/assets/{chunk-VR4S4FIN-BBUOVC5W.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
  20. package/dist/assets/{chunk-XXDRQBXY-BEIiZ6gS.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
  21. package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
  22. package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
  23. package/dist/assets/{cose-bilkent-JH36ORCC-ClFTQEyD.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
  24. package/dist/assets/{cynefin-VYW2F7L2-5Bp7xFO0.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
  25. package/dist/assets/{cynefinDiagram-TSTJHNR4-C-0QlSda.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
  26. package/dist/assets/{dagre-VKFMJZFB-DLkqsfBy.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
  27. package/dist/assets/{diagram-FQU43EPY-Cwh_tJ_r.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
  28. package/dist/assets/{diagram-G47NLZAW-CvvAjulf.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
  29. package/dist/assets/{diagram-NH7WQ7WH-CCoyx8_e.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
  30. package/dist/assets/{diagram-OA4YK3LP-DfjhKYCp.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
  31. package/dist/assets/{diagram-WEI45ONY-Vjz0ygfB.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
  32. package/dist/assets/{ebnfDiagram-CCIWWBDH-GsA8c6g2.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
  33. package/dist/assets/{erDiagram-Q63AITRT-Cc0lJ9sQ.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
  34. package/dist/assets/{flowDiagram-23GEKE2U-C_sotK-k.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
  35. package/dist/assets/{ganttDiagram-NO4QXBWP-BBtu_WUe.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
  36. package/dist/assets/{gitGraphDiagram-IHSO6WYX-BV_aSB8-.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
  37. package/dist/assets/{index-CKfOVv2N.js → index-DZTJEk-y.js} +2 -2
  38. package/dist/assets/{infoDiagram-FWYZ7A6U-DMAvNGtX.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
  39. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DfQB91MZ.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
  40. package/dist/assets/{journeyDiagram-5HDEW3XC-Bq4Llm9o.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
  41. package/dist/assets/{kanban-definition-HUTT4EX6-DTXHiDiE.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
  42. package/dist/assets/{linear-BQSYZcVg.js → linear-C1paCqE7.js} +1 -1
  43. package/dist/assets/{mindmap-definition-LN4V7U3C-KuH2NIj0.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
  44. package/dist/assets/{pegDiagram-2B236MQR-BKoNGQFr.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
  45. package/dist/assets/{pieDiagram-ENE6RG2P-EpYv162Y.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-ABIIQ3AL-C9deUjQ2.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-RFXS5EU6-Csqw2hK9.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
  48. package/dist/assets/{requirementDiagram-TGXJPOKE-DCuz3emr.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-HTMAVEWB-BTeTL24f.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-DBY2YBRQ-Cf1ZkTwO.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-D3Lbewzg.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
  52. package/dist/assets/{stateDiagram-2N3HPSRC-O8l37yJI.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
  54. package/dist/assets/{swimlanes-5IMT3BWC-BikmTh8b.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
  56. package/dist/assets/{timeline-definition-FHXFAJF6-BOxSosG0.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
  57. package/dist/assets/{vennDiagram-L72KCM5P-BSClOErZ.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-EHGQE667-DHFRjOK0.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
  59. package/dist/assets/{xychartDiagram-FW5EYKEG-Dh1v4x-O.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
  60. package/dist/index.html +1 -1
  61. package/lib/build-all.js +0 -5
  62. package/lib/build-stream-schema.js +1 -1
  63. package/lib/build.js +1809 -2646
  64. package/lib/consumer-fanout.js +1317 -0
  65. package/lib/escalation.js +69 -0
  66. package/lib/feature-validator.js +6 -4
  67. package/lib/feature-writer.js +58 -3
  68. package/lib/flow-state.js +15 -14
  69. package/lib/gsd-budget.js +48 -10
  70. package/lib/gsd-prompt.js +3 -4
  71. package/lib/gsd-stuck.js +1 -1
  72. package/lib/gsd.js +303 -149
  73. package/lib/lane-gate.js +200 -0
  74. package/lib/local-claude-connector.js +149 -0
  75. package/lib/new.js +162 -307
  76. package/lib/result-normalizer.js +233 -18
  77. package/lib/review-lenses.js +1 -1
  78. package/lib/step-prompt.js +41 -119
  79. package/lib/stratum-engine.js +297 -0
  80. package/lib/stratum-mcp-client.js +224 -213
  81. package/lib/triage.js +144 -0
  82. package/lib/vocabulary-compliance.js +268 -0
  83. package/lib/vocabulary-inject.js +1 -36
  84. package/package.json +2 -1
  85. package/pipelines/build-quick.stratum.yaml +5 -17
  86. package/pipelines/build.profiles.json +11 -0
  87. package/pipelines/build.stratum.yaml +288 -467
  88. package/pipelines/gsd.stratum.yaml +73 -125
  89. package/pipelines/new.stratum.yaml +68 -149
  90. package/server/build-routes.js +4 -1
  91. package/server/design-routes.js +37 -21
  92. package/server/index.js +13 -21
  93. package/server/lifecycle-guard.js +1 -1
  94. package/server/pipeline-routes.js +113 -31
  95. package/server/stratum-client.js +33 -47
  96. package/server/stratum-sync.js +3 -4
  97. package/server/vision-server.js +1 -1
  98. package/dist/assets/channel-CYErfopw.js +0 -1
  99. package/dist/assets/classDiagram-OUVF2IWQ-qCC_hnXu.js +0 -1
  100. package/dist/assets/classDiagram-v2-EOCWNBFH-qCC_hnXu.js +0 -1
  101. package/dist/assets/stateDiagram-v2-6OUMAXLB-BWqk_9py.js +0 -1
  102. package/dist/assets/swimlanesDiagram-G3AALYLV-C60ICdks.js +0 -8
  103. package/lib/connector-factory-shim.js +0 -167
  104. package/server/agent-mcp.js +0 -10
@@ -0,0 +1,297 @@
1
+ import { existsSync, readFileSync, writeFileSync, statSync, accessSync, constants as fsConstants } from 'node:fs';
2
+ import { execFileSync as _execFileSyncDefault } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+
10
+ // Development/monorepo fallback when @smartmemory/stratum is not installed.
11
+ export const LIVE_STRATUM_TS_MCP_BIN = resolve(
12
+ __dirname, '..', '..', 'stratum', 'ts', 'src', 'mcp', 'bin.mjs',
13
+ );
14
+
15
+ // The flow/gate SEAM uses the TS `query`/`gate` CLI (not the MCP bin).
16
+ export const LIVE_STRATUM_TS_CLI_BIN = resolve(
17
+ __dirname, '..', '..', 'stratum', 'ts', 'src', 'cli', 'bin.mjs',
18
+ );
19
+
20
+ // D1: a valid-format run id (matches the engine's `/^[a-zA-Z0-9-]+$/`) that no
21
+ // store will ever hold, so `query flow <SENTINEL>` deterministically returns the
22
+ // stratum NOT_FOUND projection — a POSITIVE identity check that does not depend on
23
+ // store data (an empty `query flows` array from a fresh store is legitimate and
24
+ // indistinguishable from an adversarial `[]`).
25
+ const PROBE_SENTINEL_RUN_ID = 'stratum-probe-nonexistent-run-0';
26
+
27
+ /**
28
+ * D1/C1: POSITIVELY identify a flow/gate binary as one that speaks the stratum
29
+ * query contract, before the adapter trusts it. Existence/executability alone is
30
+ * not enough (a bare `stratum` on $PATH can exist yet answer with "Unknown
31
+ * command" and exit 0), and a shape-agnostic "returns an array" check is spoofed
32
+ * by an adversarial stub emitting `[]` or `[{...}]` — AND a fresh store legitimately
33
+ * returns `[]`. So the probe queries a nonexistent-but-valid run id: a compatible
34
+ * binary echoes it back in the exact NOT_FOUND projection
35
+ * (`{error:{code:"NOT_FOUND", message:"Flow '<id>' not found"}}`). Returns
36
+ * { ok, reason? }; never throws.
37
+ *
38
+ * @param {string} bin
39
+ * @param {{ execFileSync?: Function }} [deps] test seam
40
+ */
41
+ export function probeStratumBin(bin, deps = {}) {
42
+ const execFileSync = deps.execFileSync ?? _execFileSyncDefault;
43
+ // Existence / executability. A configured path must be an executable regular
44
+ // file; a bare command is looked up on $PATH via the query call itself.
45
+ if (typeof bin === 'string' && bin.includes('/')) {
46
+ try {
47
+ if (!statSync(bin).isFile()) return { ok: false, reason: `${bin} is not a regular file` };
48
+ accessSync(bin, fsConstants.X_OK);
49
+ } catch {
50
+ return { ok: false, reason: `${bin} is not an executable file` };
51
+ }
52
+ }
53
+ // The NOT_FOUND projection is written to STDOUT but the CLI exits NON-ZERO for
54
+ // it, so capture stdout whether the call returns (exit 0) or throws (exit != 0).
55
+ let out;
56
+ try {
57
+ out = execFileSync(bin, ['query', 'flow', PROBE_SENTINEL_RUN_ID], {
58
+ encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
59
+ });
60
+ } catch (err) {
61
+ // E3: a TIMEOUT or SIGNAL-terminated run must NEVER be treated as a legitimate
62
+ // non-zero exit — a hung binary can flush partial/garbage stdout that happens to
63
+ // parse. Detect termination BEFORE trusting any captured stdout.
64
+ if (err && (err.killed === true || err.signal != null || err.code === 'ETIMEDOUT')) {
65
+ return { ok: false, reason: `\`${bin} query flow\` timed out or was terminated (${err.signal ?? err.code ?? 'killed'})` };
66
+ }
67
+ const captured = err?.stdout;
68
+ if (typeof captured === 'string' && captured.length > 0) {
69
+ out = captured;
70
+ } else if (captured && typeof captured.toString === 'function' && captured.length) {
71
+ out = captured.toString('utf8');
72
+ } else {
73
+ return { ok: false, reason: `\`${bin} query flow\` failed to run: ${err?.message ?? err}` };
74
+ }
75
+ }
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(out);
79
+ } catch {
80
+ return {
81
+ ok: false,
82
+ reason: `${bin} does not speak the stratum query contract `
83
+ + `(\`query flow\` returned non-JSON: ${String(out).trim().slice(0, 80)})`,
84
+ };
85
+ }
86
+ // E2: EXACT structural + literal match — not a substring. A JSON argument-parrot
87
+ // that echoes the sentinel anywhere inside a NOT_FOUND envelope must NOT pass. The
88
+ // message must equal the exact projection format the TS CLI emits.
89
+ const code = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
90
+ ? parsed.error?.code : undefined;
91
+ const message = parsed && typeof parsed === 'object' ? parsed.error?.message : undefined;
92
+ const expectedMessage = `Flow '${PROBE_SENTINEL_RUN_ID}' not found`;
93
+ if (code !== 'NOT_FOUND' || message !== expectedMessage) {
94
+ return {
95
+ ok: false,
96
+ reason: `${bin} does not speak the stratum query contract `
97
+ + `(\`query flow <sentinel>\` did not return the exact NOT_FOUND projection; got: ${String(out).trim().slice(0, 120)})`,
98
+ };
99
+ }
100
+ return { ok: true };
101
+ }
102
+
103
+ function configuredEngine(cwd) {
104
+ if (typeof cwd !== 'string' || cwd.length === 0) return undefined;
105
+ const configPath = join(cwd, '.compose', 'compose.json');
106
+ if (!existsSync(configPath)) return undefined;
107
+ try {
108
+ return JSON.parse(readFileSync(configPath, 'utf8'))?.capabilities?.stratumEngine;
109
+ } catch {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ const PYTHON_LEGACY_ERROR =
115
+ 'The Python Stratum engine has been deleted. Use the python-legacy branch for the archived Python runtime.';
116
+
117
+ /** Resolve env -> project capability -> TS-only runtime. */
118
+ export function resolveStratumEngine(cwd = process.cwd()) {
119
+ let value = process.env.COMPOSE_STRATUM_ENGINE;
120
+ if (value === undefined || value === '') value = configuredEngine(cwd);
121
+ if (value === undefined || value === '') return 'ts';
122
+ if (value === 'python') throw new Error(PYTHON_LEGACY_ERROR);
123
+ if (value !== 'ts') throw new Error(`stratumEngine must be "ts", got ${JSON.stringify(value)}`);
124
+ return 'ts';
125
+ }
126
+
127
+ const BIN_CONFIG = {
128
+ mcp: {
129
+ env: 'COMPOSE_STRATUM_TS_MCP_BIN',
130
+ packageBin: 'stratum-mcp',
131
+ sibling: LIVE_STRATUM_TS_MCP_BIN,
132
+ },
133
+ cli: {
134
+ env: 'COMPOSE_STRATUM_TS_CLI_BIN',
135
+ packageBin: 'stratum',
136
+ sibling: LIVE_STRATUM_TS_CLI_BIN,
137
+ },
138
+ };
139
+
140
+ /** Resolve a bin from an installed @smartmemory/stratum package, if present. */
141
+ function resolveInstalledBin(packageBin, requireResolve) {
142
+ let packageJsonPath;
143
+ try {
144
+ // The dependency line is intentionally added only when Stratum is published.
145
+ packageJsonPath = requireResolve('@smartmemory/stratum/package.json');
146
+ } catch (error) {
147
+ if (error?.code === 'MODULE_NOT_FOUND' || error?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
148
+ return undefined;
149
+ }
150
+ throw error;
151
+ }
152
+
153
+ try {
154
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
155
+ const relativeBin = typeof packageJson.bin === 'object'
156
+ ? packageJson.bin?.[packageBin]
157
+ : undefined;
158
+ if (typeof relativeBin !== 'string') return undefined;
159
+ return resolve(dirname(packageJsonPath), relativeBin);
160
+ } catch {
161
+ return undefined;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Resolve env -> installed dependency -> sibling checkout, checking every
167
+ * candidate before returning it.
168
+ *
169
+ * @param {'mcp'|'cli'} kind
170
+ * @param {string} [cwd]
171
+ * @param {{env?: object, requireResolve?: Function, siblingBins?: object}} [deps]
172
+ */
173
+ export function resolveStratumBin(kind, cwd = process.cwd(), deps = {}) {
174
+ const config = BIN_CONFIG[kind];
175
+ if (!config) throw new Error(`Unknown Stratum bin kind: ${JSON.stringify(kind)}`);
176
+
177
+ const env = deps.env ?? process.env;
178
+ const envCandidate = env[config.env]
179
+ // Backwards compatibility for the pre-chain flow/gate override.
180
+ || (kind === 'cli' ? env.COMPOSE_STRATUM_TS_BIN : undefined);
181
+ if (envCandidate && existsSync(envCandidate)) return envCandidate;
182
+
183
+ const installedCandidate = resolveInstalledBin(
184
+ config.packageBin,
185
+ deps.requireResolve ?? require.resolve.bind(require),
186
+ );
187
+ if (installedCandidate && existsSync(installedCandidate)) return installedCandidate;
188
+
189
+ const siblingCandidate = deps.siblingBins?.[kind] ?? config.sibling;
190
+ if (siblingCandidate && existsSync(siblingCandidate)) return siblingCandidate;
191
+
192
+ throw new Error(
193
+ `Unable to resolve the Stratum ${kind} binary for ${cwd}. `
194
+ + 'Install @smartmemory/stratum or place a stratum checkout as a sibling of compose.',
195
+ );
196
+ }
197
+
198
+ /** Resolve the selected engine to StratumMcpClient.connect() options. */
199
+ export function resolveStratumMcpConnection(cwd, deps = {}) {
200
+ resolveStratumEngine(cwd);
201
+ return {
202
+ command: (deps.env ?? process.env).COMPOSE_STRATUM_TS_NODE || process.execPath,
203
+ args: [resolveStratumBin('mcp', cwd, deps)],
204
+ ...(cwd ? { cwd } : {}),
205
+ };
206
+ }
207
+
208
+ function readStratumMcpEntry(cwd) {
209
+ try {
210
+ return JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'))?.mcpServers?.stratum;
211
+ } catch {
212
+ return undefined;
213
+ }
214
+ }
215
+
216
+ function failureReason(error) {
217
+ const stderr = error?.stderr;
218
+ if (typeof stderr === 'string' && stderr.trim()) return stderr.trim();
219
+ if (stderr && typeof stderr.toString === 'function' && stderr.length) {
220
+ return stderr.toString('utf8').trim();
221
+ }
222
+ return error?.message ?? String(error);
223
+ }
224
+
225
+ /**
226
+ * The `.mcp.json` `stratum` entry Compose OWNS for a workspace: whatever the
227
+ * runtime resolver spawns (installed dependency -> sibling checkout). This is
228
+ * exactly what `compose init` writes, so heal/check stay consistent with it.
229
+ *
230
+ * NB: deliberately NOT the npx-pinned form that `stratum mcp install`/`doctor`
231
+ * write for external consumers — that form 404s until @smartmemory/stratum is
232
+ * published, so delegating Compose's heal to `stratum doctor --fix` would
233
+ * rewrite a working direct-bin entry into a broken one. Compose resolves the
234
+ * engine itself and heals to the path it can prove exists.
235
+ */
236
+ function desiredStratumEntry(cwd, deps = {}) {
237
+ const conn = resolveStratumMcpConnection(cwd, deps);
238
+ return { command: conn.command, args: conn.args };
239
+ }
240
+
241
+ function sameMcpEntry(a, b) {
242
+ if (!a || !b || a.command !== b.command) return false;
243
+ const aArgs = Array.isArray(a.args) ? a.args : [];
244
+ const bArgs = Array.isArray(b.args) ? b.args : [];
245
+ return aArgs.length === bArgs.length && aArgs.every((value, index) => value === bArgs[index]);
246
+ }
247
+
248
+ /**
249
+ * Read-only wiring check for `compose doctor`. Reports stale when Compose's
250
+ * resolved entry differs from what is on disk (i.e. `compose update` would
251
+ * rewrite it). Never throws.
252
+ */
253
+ export function checkStratumWiring(cwd, deps = {}) {
254
+ let desired;
255
+ try {
256
+ desired = desiredStratumEntry(cwd, deps);
257
+ } catch (error) {
258
+ return { ok: false, skipped: failureReason(error) };
259
+ }
260
+ const current = readStratumMcpEntry(cwd);
261
+ if (sameMcpEntry(current, desired)) return { ok: true, stale: false };
262
+ return { ok: false, stale: true, current, desired };
263
+ }
264
+
265
+ /**
266
+ * Best-effort repair of a project's Stratum MCP wiring to Compose's resolved
267
+ * entry, preserving sibling MCP servers. Never throws.
268
+ */
269
+ export function healStratumWiring(cwd, deps = {}) {
270
+ const before = readStratumMcpEntry(cwd);
271
+ let desired;
272
+ try {
273
+ desired = desiredStratumEntry(cwd, deps);
274
+ } catch (error) {
275
+ return { healed: false, before, after: before, skipped: failureReason(error) };
276
+ }
277
+ if (sameMcpEntry(before, desired)) return { healed: false, before, after: before };
278
+
279
+ const path = join(cwd, '.mcp.json');
280
+ let document = {};
281
+ try {
282
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
283
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) document = parsed;
284
+ } catch {
285
+ document = {}; // missing or malformed → rebuild a minimal valid document
286
+ }
287
+ if (!document.mcpServers || typeof document.mcpServers !== 'object' || Array.isArray(document.mcpServers)) {
288
+ document.mcpServers = {};
289
+ }
290
+ document.mcpServers.stratum = desired;
291
+ try {
292
+ writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
293
+ } catch (error) {
294
+ return { healed: false, before, after: before, skipped: failureReason(error) };
295
+ }
296
+ return { healed: true, before, after: desired };
297
+ }