@smartmemory/compose 0.2.50-beta → 0.2.51

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 (118) hide show
  1. package/.claude/skills/compose/SKILL.md +24 -1
  2. package/.claude/skills/compose/references/hermes-tools.md +80 -0
  3. package/README.md +1 -1
  4. package/bin/compose.js +310 -52
  5. package/dist/assets/App-CT0vXPgd.js +724 -0
  6. package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
  7. package/dist/assets/{arc-DeHak63Z.js → arc-BXht3LyE.js} +1 -1
  8. package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
  9. package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
  10. package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
  11. package/dist/assets/{c4Diagram-AAUBKEIU-BsFJmPuS.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
  12. package/dist/assets/channel-Dd4XaiYv.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-XO4S2_89.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
  14. package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
  15. package/dist/assets/{chunk-55IACEB6-CtFSLInj.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
  16. package/dist/assets/{chunk-2J33WTMH-AEu6HIoY.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
  17. package/dist/assets/{chunk-FMBD7UC4-BMGd_Y9a.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
  18. package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
  19. package/dist/assets/{chunk-QZHKN3VN-DHhUJ8RK.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
  20. package/dist/assets/{chunk-ND2GUHAM-BlrOECEr.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
  21. package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
  22. package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
  23. package/dist/assets/clone-CBsbNGAa.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-C3M0mZqk.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
  25. package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
  26. package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
  27. package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
  28. package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
  29. package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
  30. package/dist/assets/{erDiagram-TEJ5UH35-AMDitV0b.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
  31. package/dist/assets/{flowDiagram-I6XJVG4X-C0t2ZGQv.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
  32. package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
  33. package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
  34. package/dist/assets/graph-DZe55uk8.js +331 -0
  35. package/dist/assets/graph-Tq_bs_r0.js +1 -0
  36. package/dist/assets/index-8UhRLbGq.js +123 -0
  37. package/dist/assets/index-CRqB9els.css +1 -0
  38. package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
  39. package/dist/assets/{ishikawaDiagram-YF4QCWOH-CCBwgjFs.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
  40. package/dist/assets/{journeyDiagram-JHISSGLW-zUJN38EU.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
  41. package/dist/assets/{kanban-definition-UN3LZRKU-BNxWi-8J.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
  42. package/dist/assets/katex-DkKDou_j.js +257 -0
  43. package/dist/assets/layout-qtUgN9BC.js +1 -0
  44. package/dist/assets/{linear-CRH9b4g7.js → linear-D158F7OT.js} +1 -1
  45. package/dist/assets/min-hESxN-c0.js +1 -0
  46. package/dist/assets/{mindmap-definition-RKZ34NQL-DolYwq7Y.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
  47. package/dist/assets/mobile-1gVCT0OK.css +1 -0
  48. package/dist/assets/mobile-Chw8RWyH.js +17 -0
  49. package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
  50. package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
  51. package/dist/assets/{requirementDiagram-4Y6WPE33-DsXZ05jo.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
  52. package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
  53. package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
  54. package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
  55. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
  56. package/dist/assets/{timeline-definition-PNZ67QCA-Bvnhw58e.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
  57. package/dist/assets/{vennDiagram-CIIHVFJN-5WnyjZqf.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
  58. package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
  59. package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
  60. package/dist/assets/{xychartDiagram-2RQKCTM6-BCShes6B.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
  61. package/dist/index.html +4 -4
  62. package/lib/build.js +272 -33
  63. package/lib/codex-preflight.js +151 -0
  64. package/lib/completion-writer.js +21 -1
  65. package/lib/feature-validator.js +5 -1
  66. package/lib/hooks-status.js +135 -0
  67. package/lib/install-agent-defs.js +22 -0
  68. package/lib/migrate-anon.js +277 -0
  69. package/lib/roadmap-parser.js +1 -1
  70. package/lib/test-bootstrap.js +165 -2
  71. package/lib/vocabulary-inject.js +103 -0
  72. package/package.json +2 -1
  73. package/pipelines/build-quick.stratum.yaml +433 -0
  74. package/pipelines/build.stratum.yaml +60 -3
  75. package/scripts/watch-server.sh +32 -0
  76. package/server/build-routes.js +89 -9
  77. package/server/build-stream-bridge.js +12 -0
  78. package/server/compose-mcp-tools.js +73 -1
  79. package/server/compose-mcp.js +27 -0
  80. package/server/feature-scaffold-routes.js +82 -0
  81. package/server/health-routes.js +261 -0
  82. package/server/qa-scope-routes.js +96 -0
  83. package/server/validate-routes.js +70 -0
  84. package/server/vision-routes.js +29 -0
  85. package/server/vision-server.js +21 -0
  86. package/dist/assets/App-Bxyif1yv.js +0 -706
  87. package/dist/assets/architectureDiagram-3BPJPVTR-C5Nkyl9y.js +0 -36
  88. package/dist/assets/blockDiagram-GPEHLZMM-BilnUBG2.js +0 -132
  89. package/dist/assets/channel-ZgbQ1k0u.js +0 -1
  90. package/dist/assets/chunk-727SXJPM-CZeBRpM9.js +0 -206
  91. package/dist/assets/chunk-AQP2D5EJ-_vmxcBc4.js +0 -231
  92. package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +0 -1
  93. package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +0 -1
  94. package/dist/assets/dagre-BM42HDAG-DJlm4fEk.js +0 -4
  95. package/dist/assets/diagram-2AECGRRQ-BmcONQG5.js +0 -43
  96. package/dist/assets/diagram-5GNKFQAL-5JbWGIwd.js +0 -10
  97. package/dist/assets/diagram-KO2AKTUF-DAnIvyI0.js +0 -3
  98. package/dist/assets/diagram-LMA3HP47-Bmh7tvmI.js +0 -24
  99. package/dist/assets/diagram-OG6HWLK6-hnHVMNnk.js +0 -24
  100. package/dist/assets/ganttDiagram-6RSMTGT7-BPlM-BEN.js +0 -292
  101. package/dist/assets/gitGraphDiagram-PVQCEYII-Lv_YTxnb.js +0 -106
  102. package/dist/assets/graph-BBXaecIU.js +0 -331
  103. package/dist/assets/graph-CAnANduQ.js +0 -1
  104. package/dist/assets/index-BwLfbbOu.css +0 -1
  105. package/dist/assets/index-COq21Zym.js +0 -119
  106. package/dist/assets/infoDiagram-5YYISTIA-Dul1vdUm.js +0 -2
  107. package/dist/assets/katex-C5jXJg4s.js +0 -257
  108. package/dist/assets/layout-DGIYPm2g.js +0 -1
  109. package/dist/assets/mobile-Cag5dHlF.css +0 -1
  110. package/dist/assets/mobile-DwmxS_O4.js +0 -17
  111. package/dist/assets/pieDiagram-4H26LBE5-oxNfr2TX.js +0 -30
  112. package/dist/assets/quadrantDiagram-W4KKPZXB-BmZCvD-z.js +0 -7
  113. package/dist/assets/sankeyDiagram-5OEKKPKP-DsnYaawP.js +0 -40
  114. package/dist/assets/sequenceDiagram-3UESZ5HK-Dt5mu3g8.js +0 -162
  115. package/dist/assets/stateDiagram-AJRCARHV-BFR5ZINQ.js +0 -1
  116. package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +0 -1
  117. package/dist/assets/wardley-L42UT6IY-CTxW4xow.js +0 -173
  118. package/dist/assets/wardleyDiagram-YWT4CUSO-K8Y1EOvb.js +0 -78
@@ -119,7 +119,10 @@ export function detectTestFramework(cwd) {
119
119
  framework: 'go-test',
120
120
  runner: 'go',
121
121
  configFile: 'go.mod',
122
- command: 'go test ./...',
122
+ // -v emits per-test "--- PASS:/FAIL:" verdicts so parseTestSummary can
123
+ // count tests (COMP-TEST-BOOTSTRAP-4); plain `go test` prints only a
124
+ // package-level ok/FAIL with no counts.
125
+ command: 'go test -v ./...',
123
126
  };
124
127
  }
125
128
  }
@@ -298,7 +301,8 @@ export function scaffoldTestFramework(cwd, language) {
298
301
  framework: 'go-test',
299
302
  configFile: 'golden_test.go',
300
303
  testDir: '.',
301
- command: 'go test ./...',
304
+ // -v so parseTestSummary can count per-test verdicts (COMP-TEST-BOOTSTRAP-4).
305
+ command: 'go test -v ./...',
302
306
  };
303
307
  }
304
308
 
@@ -399,3 +403,162 @@ export function scaffoldTestFramework(cwd, language) {
399
403
  }
400
404
  }
401
405
  }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // COMP-TEST-BOOTSTRAP-4-1: isTestFile
409
+ // ---------------------------------------------------------------------------
410
+
411
+ /**
412
+ * Whether a repo-relative path looks like a test file. Used to scope the
413
+ * post-coverage test-review pass to the tests the coverage step produced.
414
+ * Conservative: matches conventional test locations/suffixes across the
415
+ * frameworks detectTestFramework knows, and avoids substring false positives
416
+ * (e.g. "contest", "greatest").
417
+ *
418
+ * @param {string} p - repo-relative file path
419
+ * @returns {boolean}
420
+ */
421
+ export function isTestFile(p) {
422
+ if (typeof p !== 'string' || p.length === 0) return false;
423
+ return (
424
+ /(^|\/)(tests?|__tests__|spec)\//.test(p) || // test/ tests/ __tests__/ spec/ dirs
425
+ /\.(test|spec)\.[mc]?[jt]sx?$/.test(p) || // *.test.* / *.spec.* (js/ts/jsx/tsx/mjs/cjs)
426
+ /_test\.go$/.test(p) || // go
427
+ /(^|\/)test_[^/]*\.py$/.test(p) || // pytest test_*.py
428
+ /_test\.py$/.test(p) // pytest *_test.py
429
+ );
430
+ }
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // COMP-TEST-BOOTSTRAP-4: parseTestSummary
434
+ // ---------------------------------------------------------------------------
435
+
436
+ const _UNPARSED = Object.freeze({ test_count: 0, pass_rate: 0, parsed: false });
437
+
438
+ /** Strip ANSI color/escape codes so regexes match raw text. */
439
+ function _stripAnsi(s) {
440
+ // eslint-disable-next-line no-control-regex
441
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
442
+ }
443
+
444
+ /** First capture group as an int, or null if no match. */
445
+ function _int(m) {
446
+ return m ? parseInt(m[1], 10) : null;
447
+ }
448
+
449
+ /**
450
+ * Build a result from pass/fail counts. The denominator is passed+failed only —
451
+ * skipped/pending/ignored tests are excluded from the rate (a fully-skipped run
452
+ * is not "100% passing"; a run with zero verdicts degrades to parsed:false).
453
+ */
454
+ function _fromCounts(passed, failed) {
455
+ const total = passed + failed;
456
+ if (total <= 0) return _UNPARSED;
457
+ // Round to 2 decimals; an all-pass run yields exactly 100.
458
+ const pass_rate = Math.round((passed / total) * 10000) / 100;
459
+ return { test_count: total, pass_rate, parsed: true };
460
+ }
461
+
462
+ function _parseVitest(out) {
463
+ // " Tests 2 failed | 10 passed (12)" / " Tests 12 passed (12)"
464
+ // Match the "Tests" line specifically, not the "Test Files" line.
465
+ const line = out.match(/^[^\S\n]*Tests[^\S\n]+(.+)$/m);
466
+ if (!line) return _UNPARSED;
467
+ const passed = _int(line[1].match(/(\d+)\s+passed/));
468
+ const failed = _int(line[1].match(/(\d+)\s+failed/));
469
+ if (passed === null && failed === null) return _UNPARSED;
470
+ return _fromCounts(passed ?? 0, failed ?? 0);
471
+ }
472
+
473
+ function _parseJest(out) {
474
+ // "Tests: 2 failed, 10 passed, 12 total"
475
+ const line = out.match(/^[^\S\n]*Tests:[^\S\n]+(.+)$/m);
476
+ if (!line) return _UNPARSED;
477
+ const passed = _int(line[1].match(/(\d+)\s+passed/));
478
+ const failed = _int(line[1].match(/(\d+)\s+failed/));
479
+ if (passed === null && failed === null) return _UNPARSED;
480
+ return _fromCounts(passed ?? 0, failed ?? 0);
481
+ }
482
+
483
+ function _parseMocha(out) {
484
+ // " 10 passing (24ms)" + optional " 2 failing"; pending excluded.
485
+ const passed = _int(out.match(/(\d+)\s+passing/));
486
+ const failed = _int(out.match(/(\d+)\s+failing/));
487
+ if (passed === null && failed === null) return _UNPARSED;
488
+ return _fromCounts(passed ?? 0, failed ?? 0);
489
+ }
490
+
491
+ function _parsePytest(out) {
492
+ // "===== 2 failed, 9 passed, 1 error, 1 skipped in 0.50s =====" / "12 passed in 0.34s"
493
+ const passed = _int(out.match(/(\d+)\s+passed/));
494
+ const failed = _int(out.match(/(\d+)\s+failed/));
495
+ const errors = _int(out.match(/(\d+)\s+error/)); // "error"/"errors"
496
+ if (passed === null && failed === null && errors === null) return _UNPARSED;
497
+ return _fromCounts(passed ?? 0, (failed ?? 0) + (errors ?? 0));
498
+ }
499
+
500
+ function _parseGoTest(out) {
501
+ // Requires verbose output (`go test -v`): per-test "--- PASS:" / "--- FAIL:" lines.
502
+ // Without -v there are no per-test verdicts to count → degrade.
503
+ const passed = (out.match(/^[^\S\n]*--- PASS:/gm) || []).length;
504
+ const failed = (out.match(/^[^\S\n]*--- FAIL:/gm) || []).length;
505
+ if (passed + failed === 0) return _UNPARSED;
506
+ return _fromCounts(passed, failed);
507
+ }
508
+
509
+ function _parseCargo(out) {
510
+ // Sum every "test result: <ok|FAILED>. N passed; M failed; ..." line
511
+ // (a crate emits one per unit/integration/doc target).
512
+ const re = /test result:\s+\w+\.\s+(\d+)\s+passed;\s+(\d+)\s+failed/g;
513
+ let passed = 0, failed = 0, found = false, m;
514
+ while ((m = re.exec(out)) !== null) {
515
+ passed += parseInt(m[1], 10);
516
+ failed += parseInt(m[2], 10);
517
+ found = true;
518
+ }
519
+ if (!found) return _UNPARSED;
520
+ return _fromCounts(passed, failed);
521
+ }
522
+
523
+ /**
524
+ * Extract a structured test-result summary from raw test-runner stdout.
525
+ *
526
+ * Pure and total: any framework it can't read — or output it can't recognize —
527
+ * degrades to `{ test_count: 0, pass_rate: 0, parsed: false }` rather than
528
+ * guessing. Callers MUST treat `parsed:false` as "no signal" and never as a
529
+ * failing gate (see lib/build.js ship path).
530
+ *
531
+ * @param {string|null} framework - value from detectTestFramework().framework
532
+ * (vitest | jest | mocha | pytest | go-test | cargo-test; others degrade)
533
+ * @param {string|null} stdout - combined stdout+stderr of the test run
534
+ * @returns {{ test_count: number, pass_rate: number, parsed: boolean }}
535
+ */
536
+ export function parseTestSummary(framework, stdout) {
537
+ if (!framework || typeof stdout !== 'string' || stdout.length === 0) return _UNPARSED;
538
+ const out = _stripAnsi(stdout);
539
+ switch (framework) {
540
+ case 'vitest': return _parseVitest(out);
541
+ case 'jest': return _parseJest(out);
542
+ case 'mocha': return _parseMocha(out);
543
+ case 'pytest': return _parsePytest(out);
544
+ case 'go-test': return _parseGoTest(out);
545
+ case 'cargo-test': return _parseCargo(out);
546
+ default: return _UNPARSED; // ava, tap, unknown
547
+ }
548
+ }
549
+
550
+ /**
551
+ * The ship gate: derive a `tests_pass` boolean from a parsed summary.
552
+ *
553
+ * - parsed: pass only when at least one test ran AND every test passed.
554
+ * - unparsed (or absent): degrade to `true` — an unreadable summary is "no
555
+ * signal", never a failing gate. This is the safety valve that keeps the
556
+ * gate from misfiring on frameworks/output the parser can't read.
557
+ *
558
+ * @param {{ test_count: number, pass_rate: number, parsed: boolean }|null} summary
559
+ * @returns {boolean}
560
+ */
561
+ export function deriveTestsPass(summary) {
562
+ if (!summary || !summary.parsed) return true;
563
+ return summary.test_count >= 1 && summary.pass_rate === 100;
564
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * STRAT-VOCAB-3 — Compose integration for vocabulary enforcement.
3
+ *
4
+ * Wires the already-shipped Stratum `vocabulary_compliance` ensure builtin
5
+ * (stratum-mcp/src/stratum_mcp/spec.py) into the compose lifecycle:
6
+ * - VOCABULARY_TEMPLATE : starter contracts/vocabulary.yaml (compose init)
7
+ * - vocabularyEnabled() : gate — capability not disabled AND a vocab file exists
8
+ * - injectVocabularyEnsure() : append the vocab ensure to the build flow's `review` step
9
+ * - tagVocabularyViolations() : mark vocab violation strings as must-fix for display
10
+ *
11
+ * Design: docs/features/STRAT-VOCAB-3/design.md
12
+ */
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ /** Project-relative location of the vocabulary file (also the path baked into the ensure). */
17
+ export const VOCABULARY_FILE = 'contracts/vocabulary.yaml';
18
+
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
+ /** Starter vocabulary file: all comments, so `_load_vocabulary` returns {} (inert) until edited. */
29
+ export const VOCABULARY_TEMPLATE = `# contracts/vocabulary.yaml — project naming vocabulary (STRAT-VOCAB)
30
+ #
31
+ # Declare canonical names and the aliases that must NOT appear in code. During
32
+ # \`compose build\`, the review step scans the files changed by the build for any
33
+ # rejected alias (whole-word, case-sensitive) and fails until each is replaced
34
+ # with its canonical name. An empty / comments-only file is a no-op.
35
+ #
36
+ # Format — a flat map of canonical_name -> { reject: [...], reason: "..." }:
37
+ #
38
+ # auth_token:
39
+ # reject: [jwt, accessToken, JwtToken, authToken]
40
+ # reason: "use auth_token everywhere for the session credential"
41
+ #
42
+ # user_id:
43
+ # reject: [uid, userId, UserId]
44
+ #
45
+ # Rules: canonical names and aliases must be identifiers; an alias may not also
46
+ # be a canonical name or be repeated across entries; \`reason\` is optional.
47
+ # Uncomment and edit the examples above (or add your own) to enable enforcement.
48
+ `;
49
+
50
+ /**
51
+ * Is vocabulary enforcement active for this project?
52
+ * Default-ON (honoring the roadmap's "by default") but gated on the file existing,
53
+ * so the generated spec is byte-identical for any project without a vocab file.
54
+ * Opt out with capabilities.vocabularyCompliance === false.
55
+ */
56
+ export function vocabularyEnabled(cwd, composeConfig) {
57
+ if (composeConfig?.capabilities?.vocabularyCompliance === false) return false;
58
+ return existsSync(join(cwd, VOCABULARY_FILE));
59
+ }
60
+
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
+ /**
86
+ * Tag vocabulary failure strings as must-fix for the findings display.
87
+ * The cli-progress parser classifies a string by keyword (defaults to `nit`);
88
+ * vocab failures carry no marker, so prefix them with `must-fix:` — the parser
89
+ * then classifies must-fix AND still extracts the file:line, and strips the
90
+ * prefix for a clean description. Non-string / non-vocab items pass through
91
+ * unchanged. Returns a new array (does not mutate input or stored violations).
92
+ *
93
+ * Matches every string the builtin emits — both alias hits
94
+ * ("vocabulary violation: …") and a broken vocab file
95
+ * ("vocabulary.yaml malformed: …" / "vocabulary.yaml schema error: …") — all of
96
+ * which block the step and so deserve must-fix, not nit.
97
+ */
98
+ export function tagVocabularyViolations(violations) {
99
+ if (!Array.isArray(violations)) return violations;
100
+ return violations.map((v) =>
101
+ typeof v === 'string' && /^vocabulary[ .]/i.test(v) ? `must-fix: ${v}` : v
102
+ );
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.2.50-beta",
3
+ "version": "0.2.51",
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",
@@ -16,6 +16,7 @@
16
16
  "dev": "node server/supervisor.js 2>&1 | tee /tmp/compose-server.log",
17
17
  "dev:server": "node server/supervisor.js",
18
18
  "dev:server:bare": "node server/index.js",
19
+ "dev:watch": "bash scripts/watch-server.sh",
19
20
  "dev:client": "vite",
20
21
  "build": "vite build",
21
22
  "preview": "vite preview",