@smartmemory/compose 0.2.58-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 (99) hide show
  1. package/README.md +2 -2
  2. package/bin/compose.js +55 -54
  3. package/dist/assets/{App-s4CulNkO.js → App-DJ5xk_Wx.js} +218 -218
  4. package/dist/assets/{abnfDiagram-VRR7QNED-CDwzQ5wr.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
  5. package/dist/assets/{arc-kIU8DDth.js → arc-DX5jqmcO.js} +1 -1
  6. package/dist/assets/{architectureDiagram-ZJ3FMSHR-CleorO_0.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
  7. package/dist/assets/{blockDiagram-677ZJIJ3-NvEFfoMk.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
  8. package/dist/assets/{c4Diagram-LMCZKHZV-BcDQdTVB.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
  9. package/dist/assets/channel-vsDnTvkh.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-BseRkpm4.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
  11. package/dist/assets/{chunk-32BRIVSS-DCHhvgFV.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
  12. package/dist/assets/{chunk-5VM5RSS4-DcTpMdJ4.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
  13. package/dist/assets/{chunk-EX3LRPZG-BdN8fvWs.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-DmZJP-8E.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
  15. package/dist/assets/{chunk-MOJQB5TN-V74uYM17.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-CoXUXbn_.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
  17. package/dist/assets/{chunk-V7JOEXUC-DUscIydR.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-C4UDHq_a.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
  19. package/dist/assets/{chunk-XXDRQBXY-DIH9i8mZ.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
  20. package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
  21. package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
  22. package/dist/assets/{cose-bilkent-JH36ORCC-Dn6Gy6l1.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
  23. package/dist/assets/{cynefin-VYW2F7L2-C2LW5fzB.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
  24. package/dist/assets/{cynefinDiagram-TSTJHNR4-CGoAq7C9.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
  25. package/dist/assets/{dagre-VKFMJZFB-BaOKtuBE.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
  26. package/dist/assets/{diagram-FQU43EPY-BdQwqt4x.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
  27. package/dist/assets/{diagram-G47NLZAW-Dkcf0o5z.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
  28. package/dist/assets/{diagram-NH7WQ7WH-C_fDi59r.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
  29. package/dist/assets/{diagram-OA4YK3LP-jaTQcOxt.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
  30. package/dist/assets/{diagram-WEI45ONY-dEtqVj3s.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
  31. package/dist/assets/{ebnfDiagram-CCIWWBDH-s8ImqsSs.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
  32. package/dist/assets/{erDiagram-Q63AITRT-5Zy29-SA.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
  33. package/dist/assets/{flowDiagram-23GEKE2U-DV74V9he.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
  34. package/dist/assets/{ganttDiagram-NO4QXBWP-BND-CiLO.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
  36. package/dist/assets/{index-CBWbmG7W.js → index-DZTJEk-y.js} +2 -2
  37. package/dist/assets/{infoDiagram-FWYZ7A6U-Duyz8rZM.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-FXEZZL3T-D2Qft0e5.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-5HDEW3XC-q6rG4Rig.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
  40. package/dist/assets/{kanban-definition-HUTT4EX6-CsPX7gBL.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
  41. package/dist/assets/{linear-CiccpAAd.js → linear-C1paCqE7.js} +1 -1
  42. package/dist/assets/{mindmap-definition-LN4V7U3C-DRW2njpA.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
  43. package/dist/assets/{pegDiagram-2B236MQR-DoDgm2S_.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
  44. package/dist/assets/{pieDiagram-ENE6RG2P-CNSYLVAF.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-ABIIQ3AL-8-Nd6ony.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
  46. package/dist/assets/{railroadDiagram-RFXS5EU6-bfdg6Bo4.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
  47. package/dist/assets/{requirementDiagram-TGXJPOKE-slDkxCQy.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-HTMAVEWB-CR3CboiK.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BIXw1JS1.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-BdKI7JyF.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
  51. package/dist/assets/{stateDiagram-2N3HPSRC-ktVgpBZS.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
  53. package/dist/assets/{swimlanes-5IMT3BWC-CaXpC3FM.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
  55. package/dist/assets/{timeline-definition-FHXFAJF6-CKWSlRUl.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
  56. package/dist/assets/{vennDiagram-L72KCM5P-CwOhxDyv.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-EHGQE667-DGavMpEB.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
  58. package/dist/assets/{xychartDiagram-FW5EYKEG-CWmnmLZz.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
  59. package/dist/index.html +1 -1
  60. package/lib/build-all.js +0 -5
  61. package/lib/build-stream-schema.js +1 -1
  62. package/lib/build.js +1762 -2614
  63. package/lib/consumer-fanout.js +1317 -0
  64. package/lib/feature-validator.js +6 -4
  65. package/lib/flow-state.js +15 -14
  66. package/lib/gsd-budget.js +48 -10
  67. package/lib/gsd-prompt.js +3 -4
  68. package/lib/gsd-stuck.js +1 -1
  69. package/lib/gsd.js +303 -149
  70. package/lib/local-claude-connector.js +149 -0
  71. package/lib/new.js +162 -307
  72. package/lib/result-normalizer.js +233 -18
  73. package/lib/review-lenses.js +1 -1
  74. package/lib/step-prompt.js +41 -119
  75. package/lib/stratum-engine.js +297 -0
  76. package/lib/stratum-mcp-client.js +224 -213
  77. package/lib/vocabulary-compliance.js +268 -0
  78. package/lib/vocabulary-inject.js +1 -36
  79. package/package.json +2 -1
  80. package/pipelines/build-quick.stratum.yaml +5 -17
  81. package/pipelines/build.profiles.json +11 -0
  82. package/pipelines/build.stratum.yaml +288 -467
  83. package/pipelines/gsd.stratum.yaml +73 -125
  84. package/pipelines/new.stratum.yaml +68 -149
  85. package/server/build-routes.js +4 -1
  86. package/server/design-routes.js +37 -21
  87. package/server/index.js +13 -21
  88. package/server/lifecycle-guard.js +1 -1
  89. package/server/pipeline-routes.js +113 -31
  90. package/server/stratum-client.js +33 -47
  91. package/server/stratum-sync.js +3 -4
  92. package/server/vision-server.js +1 -1
  93. package/dist/assets/channel-DiJkE9og.js +0 -1
  94. package/dist/assets/classDiagram-OUVF2IWQ-DS76VBEL.js +0 -1
  95. package/dist/assets/classDiagram-v2-EOCWNBFH-DS76VBEL.js +0 -1
  96. package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +0 -1
  97. package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +0 -8
  98. package/lib/connector-factory-shim.js +0 -167
  99. package/server/agent-mcp.js +0 -10
@@ -0,0 +1,268 @@
1
+ /**
2
+ * vocabulary-compliance.js — deterministic Compose-side vocabulary checks.
3
+ *
4
+ * E3/F5: the TS engine's `judged:` ensure cannot see the changed files or the
5
+ * vocabulary (the judge receives only {result, input} and fails when evidence is
6
+ * missing), so it is unevaluable as a merge guard. v1 vocabulary enforcement is
7
+ * therefore moved here: compose evaluates it over the ACTUAL changed files at the
8
+ * review_merge step and, on violation, sends a FAILURE step_done envelope so the
9
+ * engine's attempts/retry lifecycle governs (never a throw past the step handler).
10
+ *
11
+ * Inert when contracts/vocabulary.yaml is missing / empty / comments-only. Message
12
+ * Stable message formats let tagVocabularyViolations classify them.
13
+ */
14
+ import { existsSync, statSync, readFileSync } from 'node:fs';
15
+ import { normalize, isAbsolute, join } from 'node:path';
16
+ import { execFileSync } from 'node:child_process';
17
+ import YAML from 'yaml';
18
+
19
+ const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
20
+ const VOCAB_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB
21
+
22
+ /** Raised by loadVocabulary; `.violations` mirrors the Python ValueError([...]). */
23
+ export class VocabularyError extends Error {
24
+ constructor(violations) {
25
+ super(Array.isArray(violations) ? violations.join('; ') : String(violations));
26
+ this.name = 'VocabularyError';
27
+ this.violations = Array.isArray(violations) ? violations : [String(violations)];
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Load and validate vocabulary.yaml. Returns {} for missing/empty/comments-only
33
+ * files (the no-op case). Throws VocabularyError([messages]) on malformed YAML or
34
+ * schema errors.
35
+ * @returns {Record<string, {reject: string[], reason: string}>}
36
+ */
37
+ export function loadVocabulary(path) {
38
+ if (!existsSync(path) || !statSync(path).isFile()) return {};
39
+
40
+ let raw;
41
+ try {
42
+ raw = readFileSync(path, 'utf-8');
43
+ } catch (exc) {
44
+ throw new VocabularyError([`vocabulary.yaml malformed: cannot read ${path}: ${exc.message}`]);
45
+ }
46
+
47
+ let parsed;
48
+ try {
49
+ parsed = YAML.parse(raw);
50
+ } catch (exc) {
51
+ throw new VocabularyError([`vocabulary.yaml malformed: ${exc.message}`]);
52
+ }
53
+
54
+ // Empty file or comments-only → null/undefined → treat as empty.
55
+ if (parsed === null || parsed === undefined) return {};
56
+
57
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
58
+ throw new VocabularyError(['vocabulary.yaml schema error: top-level must be a mapping']);
59
+ }
60
+
61
+ const validated = {};
62
+ for (const [canonical, entry] of Object.entries(parsed)) {
63
+ if (typeof canonical !== 'string' || !IDENTIFIER_RE.test(canonical)) {
64
+ throw new VocabularyError([
65
+ `vocabulary.yaml schema error: canonical name ${JSON.stringify(canonical)} `
66
+ + `must match identifier syntax (letters, digits, underscore; starts with letter or underscore)`,
67
+ ]);
68
+ }
69
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
70
+ throw new VocabularyError([
71
+ `vocabulary.yaml schema error: entry for ${JSON.stringify(canonical)} `
72
+ + `must be a mapping, got ${Array.isArray(entry) ? 'array' : typeof entry}`,
73
+ ]);
74
+ }
75
+
76
+ const allowedFields = new Set(['reject', 'reason']);
77
+ const unknown = Object.keys(entry).filter((k) => !allowedFields.has(k)).sort();
78
+ if (unknown.length > 0) {
79
+ throw new VocabularyError([
80
+ `vocabulary.yaml schema error: entry for ${JSON.stringify(canonical)} `
81
+ + `has unknown fields: ${JSON.stringify(unknown)}. Allowed: ["reject","reason"]`,
82
+ ]);
83
+ }
84
+
85
+ const reject = entry.reject;
86
+ if (!Array.isArray(reject) || reject.length === 0) {
87
+ throw new VocabularyError([
88
+ `vocabulary.yaml schema error: entry for ${JSON.stringify(canonical)} must have non-empty 'reject' list`,
89
+ ]);
90
+ }
91
+
92
+ for (const alias of reject) {
93
+ if (typeof alias !== 'string' || !IDENTIFIER_RE.test(alias)) {
94
+ throw new VocabularyError([
95
+ `vocabulary.yaml schema error: alias ${JSON.stringify(alias)} in ${JSON.stringify(canonical)} `
96
+ + `must match identifier syntax`,
97
+ ]);
98
+ }
99
+ if (alias === canonical) {
100
+ throw new VocabularyError([
101
+ `vocabulary.yaml schema error: canonical ${JSON.stringify(canonical)} cannot reject itself`,
102
+ ]);
103
+ }
104
+ }
105
+
106
+ const reason = entry.reason ?? '';
107
+ if (reason !== null && typeof reason !== 'string') {
108
+ throw new VocabularyError([
109
+ `vocabulary.yaml schema error: reason for ${JSON.stringify(canonical)} must be a string`,
110
+ ]);
111
+ }
112
+
113
+ validated[canonical] = { reject: [...reject], reason: reason || '' };
114
+ }
115
+
116
+ // Cross-entry validation: no duplicate aliases, no canonical-as-alias.
117
+ const canonicals = new Set(Object.keys(validated));
118
+ const aliasToCanonical = new Map();
119
+ for (const [canonical, entry] of Object.entries(validated)) {
120
+ for (const alias of entry.reject) {
121
+ if (canonicals.has(alias)) {
122
+ throw new VocabularyError([
123
+ `vocabulary.yaml schema error: ${JSON.stringify(alias)} is both a canonical `
124
+ + `and a rejected alias for ${JSON.stringify(canonical)}`,
125
+ ]);
126
+ }
127
+ if (aliasToCanonical.has(alias)) {
128
+ throw new VocabularyError([
129
+ `vocabulary.yaml schema error: alias ${JSON.stringify(alias)} is rejected by `
130
+ + `multiple canonicals (${aliasToCanonical.get(alias)}, ${canonical})`,
131
+ ]);
132
+ }
133
+ aliasToCanonical.set(alias, canonical);
134
+ }
135
+ }
136
+
137
+ return validated;
138
+ }
139
+
140
+ /** git diff --name-only <base> plus untracked files. Returns null on any failure. */
141
+ function gitChangedFiles(base, cwd) {
142
+ try {
143
+ const diff = execFileSync('git', ['diff', '--name-only', base], {
144
+ cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
145
+ });
146
+ const tracked = diff.split('\n').filter(Boolean);
147
+ let untracked = [];
148
+ try {
149
+ const others = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
150
+ cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
151
+ });
152
+ untracked = others.split('\n').filter(Boolean);
153
+ } catch { /* untracked listing best-effort */ }
154
+ const seen = new Set();
155
+ const combined = [];
156
+ for (const f of [...tracked, ...untracked]) {
157
+ if (!seen.has(f)) { seen.add(f); combined.push(f); }
158
+ }
159
+ return combined;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ function scanFileForAliases(displayPath, absPath, aliasToCanonical, canonicalToReason, matchers) {
166
+ const violations = [];
167
+ let size;
168
+ try {
169
+ size = statSync(absPath).size;
170
+ } catch {
171
+ return violations; // disappeared between checks
172
+ }
173
+ if (size > VOCAB_SIZE_LIMIT) return violations; // skip large files silently
174
+
175
+ let lines;
176
+ try {
177
+ lines = readFileSync(absPath, 'utf-8').split(/\r?\n/);
178
+ } catch {
179
+ return violations;
180
+ }
181
+
182
+ for (let i = 0; i < lines.length; i += 1) {
183
+ const lineNo = i + 1;
184
+ for (const [alias, pattern] of matchers) {
185
+ // G5: emit one violation per OCCURRENCE (python parity — the builtin uses
186
+ // finditer), not one per line. matchAll requires the global flag on pattern.
187
+ for (const _match of lines[i].matchAll(pattern)) {
188
+ void _match;
189
+ const canonical = aliasToCanonical.get(alias);
190
+ const reason = canonicalToReason.get(canonical) ?? '';
191
+ let msg = `vocabulary violation: ${displayPath}:${lineNo} uses '${alias}' — canonical is '${canonical}'`;
192
+ if (reason) msg += ` (reason: ${reason})`;
193
+ violations.push(msg);
194
+ }
195
+ }
196
+ }
197
+ return violations;
198
+ }
199
+
200
+ /**
201
+ * Scan `filesChanged` for rejected vocabulary aliases. Returns an array of
202
+ * violation strings — EMPTY means compliant or inert (no vocabulary, nothing to
203
+ * scan). Never throws for violations: schema/malformed vocab errors are returned
204
+ * as (blocking) violation strings too, so the caller converts them into a failure
205
+ * envelope rather than crashing the step handler.
206
+ *
207
+ * @param {string} vocabPath path to vocabulary.yaml (resolved against cwd)
208
+ * @param {string[]} filesChanged authoritative list of touched files
209
+ * @param {object} [opts]
210
+ * @param {boolean} [opts.gitFallback=false] scan `git diff <base>` when filesChanged is empty
211
+ * @param {string} [opts.base='HEAD']
212
+ * @param {string} [opts.cwd=process.cwd()]
213
+ * @returns {string[]}
214
+ */
215
+ export function vocabularyCompliance(vocabPath, filesChanged, { gitFallback = false, base = 'HEAD', cwd = process.cwd() } = {}) {
216
+ let vocab;
217
+ try {
218
+ vocab = loadVocabulary(vocabPath);
219
+ } catch (err) {
220
+ // A malformed / invalid vocabulary blocks the step (matches the Python builtin
221
+ // raising) — surface the schema errors as violations.
222
+ return err instanceof VocabularyError ? err.violations : [String(err?.message ?? err)];
223
+ }
224
+ if (!vocab || Object.keys(vocab).length === 0) return []; // no vocabulary → nothing to check
225
+
226
+ const aliasToCanonical = new Map();
227
+ const canonicalToReason = new Map();
228
+ for (const [canonical, entry] of Object.entries(vocab)) {
229
+ canonicalToReason.set(canonical, entry.reason);
230
+ for (const alias of entry.reject) aliasToCanonical.set(alias, canonical);
231
+ }
232
+
233
+ let fileList;
234
+ if (Array.isArray(filesChanged) && filesChanged.length > 0) {
235
+ fileList = filesChanged;
236
+ } else if (gitFallback) {
237
+ const fallback = gitChangedFiles(base, cwd);
238
+ if (fallback === null) return []; // git failed; nothing we can do
239
+ fileList = fallback;
240
+ } else {
241
+ return []; // no files to scan
242
+ }
243
+
244
+ // Normalize + dedupe; keep only existing regular files.
245
+ const seen = new Set();
246
+ const files = []; // [displayPath, absPath]
247
+ for (const f of fileList) {
248
+ const disp = normalize(f);
249
+ if (seen.has(disp)) continue;
250
+ seen.add(disp);
251
+ const abs = isAbsolute(disp) ? disp : join(cwd, disp);
252
+ try {
253
+ if (statSync(abs).isFile()) files.push([disp, abs]);
254
+ } catch { /* missing/deleted — skip */ }
255
+ }
256
+ if (files.length === 0) return [];
257
+
258
+ const matchers = new Map();
259
+ for (const alias of aliasToCanonical.keys()) {
260
+ matchers.set(alias, new RegExp(`\\b${alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'));
261
+ }
262
+
263
+ const violations = [];
264
+ for (const [disp, abs] of files) {
265
+ violations.push(...scanFileForAliases(disp, abs, aliasToCanonical, canonicalToReason, matchers));
266
+ }
267
+ return violations;
268
+ }
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * STRAT-VOCAB-3 — Compose integration for vocabulary enforcement.
3
3
  *
4
- * Wires the already-shipped Stratum `vocabulary_compliance` ensure builtin
5
- * (stratum-mcp/src/stratum_mcp/spec.py) into the compose lifecycle:
4
+ * Wires Compose's deterministic vocabulary compliance check into the lifecycle:
6
5
  * - VOCABULARY_TEMPLATE : starter contracts/vocabulary.yaml (compose init)
7
6
  * - vocabularyEnabled() : gate — capability not disabled AND a vocab file exists
8
- * - injectVocabularyEnsure() : append the vocab ensure to the build flow's `review` step
9
7
  * - tagVocabularyViolations() : mark vocab violation strings as must-fix for display
10
8
  *
11
9
  * Design: docs/features/STRAT-VOCAB-3/design.md
@@ -16,15 +14,6 @@ import { join } from 'node:path';
16
14
  /** Project-relative location of the vocabulary file (also the path baked into the ensure). */
17
15
  export const VOCABULARY_FILE = 'contracts/vocabulary.yaml';
18
16
 
19
- /**
20
- * The ensure expression injected into the build flow's `review` step.
21
- * Empty-list literal + git_fallback=True: never references a possibly-missing
22
- * `result.*` attribute; scans the uncommitted working-tree diff vs HEAD (the
23
- * implementation changes are merged but not yet committed at review time).
24
- * Python-evaluated by the Stratum executor — hence `[]` and `True`.
25
- */
26
- export const VOCABULARY_ENSURE = `vocabulary_compliance('${VOCABULARY_FILE}', [], True)`;
27
-
28
17
  /** Starter vocabulary file: all comments, so `_load_vocabulary` returns {} (inert) until edited. */
29
18
  export const VOCABULARY_TEMPLATE = `# contracts/vocabulary.yaml — project naming vocabulary (STRAT-VOCAB)
30
19
  #
@@ -58,30 +47,6 @@ export function vocabularyEnabled(cwd, composeConfig) {
58
47
  return existsSync(join(cwd, VOCABULARY_FILE));
59
48
  }
60
49
 
61
- /**
62
- * Append the vocabulary ensure to the EXECUTED flow's `review` step (idempotent).
63
- * Mutates and returns specObj. No-op when that flow has no `review` step.
64
- *
65
- * `flowName` must be the flow Stratum will actually run (build.js resolves it via
66
- * extractFlowName). Targeting it precisely matters because a sub-flow
67
- * (`review_check`) ALSO has a step id'd `review`; injecting there would be wrong.
68
- * When `flowName` is omitted/unknown, fall back to the `build` flow (or the first),
69
- * matching the shipped templates.
70
- */
71
- export function injectVocabularyEnsure(specObj, flowName) {
72
- const flows = specObj?.flows ?? {};
73
- const keys = Object.keys(flows);
74
- const flowKey = flowName && keys.includes(flowName)
75
- ? flowName
76
- : (keys.includes('build') ? 'build' : keys[0]);
77
- const steps = flows[flowKey]?.steps ?? [];
78
- const review = steps.find((s) => s?.id === 'review');
79
- if (!review) return specObj;
80
- if (!Array.isArray(review.ensure)) review.ensure = [];
81
- if (!review.ensure.includes(VOCABULARY_ENSURE)) review.ensure.push(VOCABULARY_ENSURE);
82
- return specObj;
83
- }
84
-
85
50
  /**
86
51
  * Tag vocabulary failure strings as must-fix for the findings display.
87
52
  * The cli-progress parser classifies a string by keyword (defaults to `nit`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.2.58-beta",
3
+ "version": "0.3.0",
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",
@@ -85,6 +85,7 @@
85
85
  "@radix-ui/react-toggle": "^1.1.10",
86
86
  "@radix-ui/react-toggle-group": "^1.1.11",
87
87
  "@radix-ui/react-tooltip": "^1.2.8",
88
+ "@smartmemory/stratum": "0.3.2",
88
89
  "@tanstack/react-virtual": "^3.13.23",
89
90
  "ajv": "^8.18.0",
90
91
  "ajv-formats": "^3.0.1",
@@ -112,7 +112,6 @@ flows:
112
112
  task: {type: string}
113
113
  blueprint: {type: string}
114
114
  diff: {type: string}
115
- prior_dirty_lenses: {type: array, optional: true}
116
115
  output: ReviewResult
117
116
  steps:
118
117
  - id: triage
@@ -120,19 +119,9 @@ flows:
120
119
  intent: >
121
120
  Decide which review lenses to activate.
122
121
 
123
- RETRY PATH If the file .compose/prior_dirty_lenses.json exists, read it.
124
- It contains a JSON array of lens IDs that had actionable findings in the prior
125
- review run. In this case:
126
- - Activate all lenses listed in that array.
127
- - Always also include diff-quality and contract-compliance (baseline lenses —
128
- they re-run on every retry to catch regressions introduced by the fix, even
129
- if they passed clean last time).
130
- - Skip all other lenses — they already passed clean.
131
-
132
- FIRST RUN PATH — If .compose/prior_dirty_lenses.json does not exist:
133
- - Always include diff-quality and contract-compliance.
134
- - Add security if files touch auth, crypto, SQL, HTTP handlers.
135
- - Add framework if detected framework files (React, Express, Next.js, etc).
122
+ Always include diff-quality and contract-compliance.
123
+ Add security if files touch auth, crypto, SQL, HTTP handlers.
124
+ Add framework if detected framework files (React, Express, Next.js, etc).
136
125
 
137
126
  Return JSON: { "tasks": LensTask[] } where each task has id, lens_name,
138
127
  lens_focus, confidence_gate, exclusions.
@@ -314,8 +303,7 @@ flows:
314
303
  depends_on: [decompose]
315
304
 
316
305
  # Sub-flow: Review (parallel multi-lens review; falls back to review_check)
317
- # Parent-step ensure drives the fix loop: ensure_failed -> build.js fix ->
318
- # retry the whole parallel_review sub-flow, which re-runs fresh triage/lenses/merge.
306
+ # Parent-step ensure retries the whole review flow after a failed result.
319
307
  # Quick path: review reference is the design doc (no blueprint step exists).
320
308
  - id: review
321
309
  flow: parallel_review
@@ -337,7 +325,7 @@ flows:
337
325
  - "result.clean == True"
338
326
  depends_on: [review]
339
327
 
340
- # Sub-flow: Coverage (claude runs tests; build.js dispatches fix on ensure_failed)
328
+ # Sub-flow: Coverage (claude runs tests)
341
329
  - id: coverage
342
330
  flow: coverage_check
343
331
  inputs:
@@ -0,0 +1,11 @@
1
+ {
2
+ "_comment": "D6 (STRAT-TS-FANOUT-CONSUMER): compose-owned agent profiles for build.stratum.yaml. The TS engine accepts only the literal claude|codex agent, so the full profile strings (tool restrictions + model tier) that the v0.3->v1 conversion stripped live here, keyed by step id, and are applied compose-side at invocation (lib/build.js loadPipelineProfiles). The isolation:none review_lenses fanout MUST bind the read-only-reviewer restriction.",
3
+ "_reduceSteps": ["review_merge"],
4
+ "_reduceSteps_comment": "H1: ReviewResult-out steps that MERGE/deduplicate rather than review. They get review normalization (confidence handling) but NOT the reviewer scaffold. This is python's stripped reduce_mode input, restored compose-side.",
5
+ "blueprint": "claude::critical",
6
+ "review_triage": "claude:orchestrator",
7
+ "review_lenses": "claude:read-only-reviewer",
8
+ "review_merge": "claude:orchestrator",
9
+ "run_tests": "claude::fast",
10
+ "ship": "claude::critical"
11
+ }