@smartmemory/compose 0.3.7 → 0.3.8

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 (215) hide show
  1. package/.compose-deps.json +1 -13
  2. package/README.md +72 -5
  3. package/bin/compose.js +470 -351
  4. package/bin/judgment-migrate.js +387 -0
  5. package/contracts/comp-obs-contract.schema.json +9 -3
  6. package/contracts/fluid-record.schema.json +209 -0
  7. package/contracts/lifecycle-backfill.schema.json +322 -0
  8. package/dist/assets/App-Z4MU-H_F.js +916 -0
  9. package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
  10. package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
  13. package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
  14. package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  15. package/dist/assets/channel-SnZzzh7k.js +1 -0
  16. package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  17. package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
  18. package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  19. package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  20. package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  21. package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
  22. package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  23. package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  24. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  25. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  26. package/dist/assets/clone-DgklGjHm.js +1 -0
  27. package/dist/assets/{cose-bilkent-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  28. package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
  29. package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
  30. package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
  31. package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
  32. package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
  33. package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
  34. package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
  35. package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
  36. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
  37. package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
  38. package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
  39. package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
  40. package/dist/assets/index-yyrA5OZd.css +1 -0
  41. package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
  42. package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
  43. package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  44. package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
  45. package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
  46. package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
  47. package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
  48. package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
  49. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  50. package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
  51. package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
  52. package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
  53. package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
  54. package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
  55. package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
  56. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  57. package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
  58. package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
  59. package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
  60. package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
  61. package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
  62. package/dist/index.html +3 -3
  63. package/lib/agent-string.js +7 -5
  64. package/lib/append-integrity.js +81 -0
  65. package/lib/backfill-evidence.js +109 -0
  66. package/lib/bug-escalation.js +9 -0
  67. package/lib/build-stream-schema.js +3 -1
  68. package/lib/build-stream-writer.js +25 -0
  69. package/lib/build.js +874 -170
  70. package/lib/canon-guard.js +28 -6
  71. package/lib/canon-override.js +196 -0
  72. package/lib/canon-registry.js +104 -0
  73. package/lib/cli-commands.js +144 -0
  74. package/lib/codex-preflight.js +26 -13
  75. package/lib/colleague/context.js +215 -0
  76. package/lib/colleague/writeback.js +95 -0
  77. package/lib/completion-gate.js +1421 -0
  78. package/lib/completion-writer.js +47 -47
  79. package/lib/consumer-fanout.js +105 -11
  80. package/lib/coverage-gate.js +200 -0
  81. package/lib/dir-lock.js +170 -0
  82. package/lib/dispatch-ledger.js +3 -3
  83. package/lib/feature-json.js +1 -1
  84. package/lib/feature-reconciler.js +8 -0
  85. package/lib/feature-validator.js +64 -1
  86. package/lib/feature-writer.js +57 -2
  87. package/lib/fluid/factory.js +167 -0
  88. package/lib/fluid/ideabox-dates.js +73 -0
  89. package/lib/fluid/ideabox-migrate.js +154 -0
  90. package/lib/fluid/ideabox-ops.js +585 -0
  91. package/lib/fluid/ideabox-view.js +146 -0
  92. package/lib/fluid/import-ideabox.js +186 -0
  93. package/lib/fluid/local-provider.js +606 -0
  94. package/lib/fluid/provider.js +684 -0
  95. package/lib/fluid/record-shape.js +214 -0
  96. package/lib/fluid/record-store.js +328 -0
  97. package/lib/fluid/render-ideabox.js +261 -0
  98. package/lib/fluid/schema.js +40 -0
  99. package/lib/fluid/smartmemory-provider.js +1695 -0
  100. package/lib/gsd.js +63 -23
  101. package/lib/guard-cli.js +175 -0
  102. package/lib/guard-custody.js +141 -0
  103. package/lib/guard-descriptors.js +530 -0
  104. package/lib/guard-enrol.js +254 -0
  105. package/lib/health-score.js +1 -1
  106. package/lib/ideabox-cli.js +315 -0
  107. package/lib/ideabox.js +121 -21
  108. package/lib/judgment/store/index.js +9 -1
  109. package/lib/judgment/store/records.js +1 -1
  110. package/lib/judgment/trace.js +380 -0
  111. package/lib/judgment-decision-write.js +277 -0
  112. package/lib/judgment-decisions.js +466 -0
  113. package/lib/judgment-gen.js +5 -1
  114. package/lib/judgment-writer.js +56 -2
  115. package/lib/lifecycle-modes.js +4 -4
  116. package/lib/lineage.js +400 -0
  117. package/lib/local-claude-connector.js +52 -1
  118. package/lib/maya-client.js +302 -0
  119. package/lib/maya-config.js +53 -0
  120. package/lib/maya-identity.js +283 -0
  121. package/lib/migrate-anon.js +5 -0
  122. package/lib/migrate-roadmap.js +15 -0
  123. package/lib/new.js +13 -1
  124. package/lib/pipeline-compat.js +104 -0
  125. package/lib/policy-catalog.js +295 -0
  126. package/lib/policy-check.js +0 -0
  127. package/lib/process-termination.js +98 -0
  128. package/lib/resolve-workspace.js +5 -1
  129. package/lib/result-normalizer.js +396 -199
  130. package/lib/roadmap-errors.js +65 -0
  131. package/lib/roadmap-preservers.js +24 -4
  132. package/lib/roadmap-residue.js +299 -0
  133. package/lib/smartmemory-client.js +614 -78
  134. package/lib/smartmemory-config.js +54 -0
  135. package/lib/smartmemory-ingest.js +19 -2
  136. package/lib/step-prompt.js +7 -6
  137. package/lib/stratum-engine.js +53 -4
  138. package/lib/stratum-mcp-client.js +271 -36
  139. package/lib/test-bootstrap.js +31 -0
  140. package/lib/tool-inventory.js +122 -0
  141. package/lib/version-check.js +91 -19
  142. package/lib/vision-writer.js +88 -1
  143. package/package.json +7 -6
  144. package/pipelines/bug-fix.stratum.yaml +205 -211
  145. package/pipelines/build-quick.profiles.json +12 -0
  146. package/pipelines/build-quick.stratum.yaml +263 -350
  147. package/pipelines/content.stratum.yaml +81 -77
  148. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  149. package/pipelines/plan.stratum.yaml +76 -86
  150. package/pipelines/refactor.stratum.yaml +125 -125
  151. package/pipelines/research.stratum.yaml +56 -58
  152. package/pipelines/review-fix.profiles.json +6 -0
  153. package/pipelines/review-fix.stratum.yaml +110 -83
  154. package/presets/team-feature.profiles.json +6 -0
  155. package/presets/team-feature.stratum.yaml +93 -66
  156. package/presets/team-research.profiles.json +6 -0
  157. package/presets/team-research.stratum.yaml +89 -80
  158. package/presets/team-review.profiles.json +8 -0
  159. package/presets/team-review.stratum.yaml +98 -80
  160. package/scripts/cost-census.mjs +70 -0
  161. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  162. package/server/agent-health.js +22 -0
  163. package/server/agent-hooks.js +14 -1
  164. package/server/agent-server.js +5 -248
  165. package/server/agent-spawn.js +3 -4
  166. package/server/agent-workspace.js +294 -0
  167. package/server/build-routes.js +6 -5
  168. package/server/build-stream-bridge.js +53 -0
  169. package/server/cc-session-watcher.js +4 -1
  170. package/server/coalescing-buffer.js +7 -1
  171. package/server/completion-projection.js +228 -0
  172. package/server/compose-mcp-tools.js +109 -23
  173. package/server/compose-mcp.js +88 -882
  174. package/server/decision-event-emit.js +41 -2
  175. package/server/decision-event-id.js +17 -0
  176. package/server/decision-events-snapshot.js +3 -0
  177. package/server/design-routes.js +14 -8
  178. package/server/feature-scan.js +76 -2
  179. package/server/file-watcher.js +170 -21
  180. package/server/ideabox-routes.js +166 -224
  181. package/server/index.js +70 -100
  182. package/server/lifecycle-guard.js +240 -10
  183. package/server/lifecycle-phase-history.js +276 -0
  184. package/server/maya-routes.js +507 -0
  185. package/server/mcp-tool-defs.js +940 -0
  186. package/server/mcp-tool-policy.js +34 -2
  187. package/server/model-tiers.js +22 -5
  188. package/server/pipeline-routes.js +21 -11
  189. package/server/project-root.js +58 -19
  190. package/server/remote-utils.js +3 -1
  191. package/server/schema-validator.js +7 -1
  192. package/server/session-manager.js +5 -6
  193. package/server/session-routes.js +3 -1
  194. package/server/stratum-client.js +57 -10
  195. package/server/stratum-sync.js +6 -3
  196. package/server/summarizer.js +3 -4
  197. package/server/supervisor.js +0 -1
  198. package/server/vision-routes.js +208 -98
  199. package/server/vision-server.js +86 -23
  200. package/server/vision-store.js +60 -6
  201. package/server/vision-utils.js +3 -4
  202. package/server/workspace-activity.js +18 -0
  203. package/server/workspace-middleware.js +2 -2
  204. package/server/workspace-runtime.js +243 -0
  205. package/server/worktree-gc.js +1 -0
  206. package/dist/assets/App-PkZzHeMj.js +0 -894
  207. package/dist/assets/channel-qVK_qn4E.js +0 -1
  208. package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
  209. package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
  210. package/dist/assets/clone-Pu3RyLUh.js +0 -1
  211. package/dist/assets/index-LIwREYgH.css +0 -1
  212. package/dist/assets/mobile-BnXEOE3U.js +0 -17
  213. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
  214. package/lib/staleness.js +0 -87
  215. package/server/ideabox-cache.js +0 -77
@@ -0,0 +1,122 @@
1
+ /**
2
+ * tool-inventory.js — COMP-COVERAGE-GATE slice 1.
3
+ *
4
+ * The single derived answer to "which MCP tools change state, and what canon do
5
+ * they write". Pure, no I/O (shape template: server/mcp-tool-policy.js).
6
+ *
7
+ * WHY THIS EXISTS. Compose kept three hand-maintained lists that each partially
8
+ * described the mutating surface — canon-registry's TOOLS_FOR_* /
9
+ * JUDGMENT_WRITE_TOOLS, and mcp-tool-policy's IMPLEMENTER_DENY / REVIEWER_ALLOW
10
+ * — and NONE of them was the inventory. Nothing enumerated every mutating tool,
11
+ * and nothing cross-checked the three against the tool definitions themselves.
12
+ * A tool added to server/compose-mcp.js was enforced only where an author
13
+ * remembered to name it. Same shape as COMP-COMPLETION-GATE, where guard
14
+ * coverage turned out to be 0/321 — discovered by audit, not by a check.
15
+ *
16
+ * The fix is to put the declaration ON the tool definition (`effect`, `writes`)
17
+ * and DERIVE the sets from it. A side list is a fourth thing to forget; a
18
+ * required field on the definition cannot be added without the author seeing it.
19
+ *
20
+ * FAIL-CLOSED HERE, FAIL-OPEN AT RUNTIME. A tool with no `effect` is reported as
21
+ * `undeclared` and fails validate_project. That is safe because this module runs
22
+ * at validate time and never in a request path — unlike canon-guard.js, which
23
+ * fails open so a malformed input can never wedge a session.
24
+ */
25
+
26
+ /** Valid `effect` values on a tool definition. */
27
+ export const EFFECTS = /** @type {const} */ (['read', 'mutating', 'setup']);
28
+
29
+ /**
30
+ * Canon path ids a `writes` entry may name — the `id` field of each
31
+ * lib/canon-registry.js REGISTRY entry. Kept as a literal rather than imported
32
+ * so this module stays dependency-free and pure; `checkAuthorizationCoverage`
33
+ * (slice 2) validates `writes` against the live registry, which is where a
34
+ * drifted id must be caught. This set is only for the shape check below.
35
+ */
36
+ export const CANON_IDS = new Set([
37
+ 'roadmap', 'changelog', 'feature-json', 'judgment',
38
+ 'override-ledger', 'override-attest', 'override-grants',
39
+ ]);
40
+
41
+ /**
42
+ * Partition tool definitions by declared effect.
43
+ *
44
+ * @param {Array<{name?: string, effect?: string, writes?: string[]}>} toolDefs
45
+ * @returns {{
46
+ * read: string[], mutating: string[], setup: string[],
47
+ * undeclared: Array<{ name: string, reason: string }>,
48
+ * writesByTool: Record<string, string[]>,
49
+ * }}
50
+ */
51
+ export function loadToolInventory(toolDefs) {
52
+ const read = [];
53
+ const mutating = [];
54
+ const setup = [];
55
+ const undeclared = [];
56
+ /** @type {Record<string, string[]>} */
57
+ const writesByTool = {};
58
+
59
+ if (!Array.isArray(toolDefs)) return { read, mutating, setup, undeclared, writesByTool };
60
+
61
+ for (const def of toolDefs) {
62
+ const name = def && typeof def.name === 'string' ? def.name : null;
63
+ if (!name) {
64
+ undeclared.push({ name: '<unnamed>', reason: 'tool definition has no name' });
65
+ continue;
66
+ }
67
+
68
+ const effect = def.effect;
69
+ if (!EFFECTS.includes(effect)) {
70
+ undeclared.push({
71
+ name,
72
+ reason: effect === undefined
73
+ ? 'no `effect` field'
74
+ : `unknown effect '${effect}' (expected ${EFFECTS.join('|')})`,
75
+ });
76
+ continue;
77
+ }
78
+
79
+ if (effect === 'read') { read.push(name); continue; }
80
+ if (effect === 'setup') { setup.push(name); continue; }
81
+
82
+ // effect === 'mutating' — `writes` is required, `[]` being a real answer
83
+ // ("mutates state, but no CANON path"). Distinguishing "declared empty"
84
+ // from "forgot to declare" is the entire point of requiring the field.
85
+ if (!Array.isArray(def.writes)) {
86
+ undeclared.push({ name, reason: 'mutating tool has no `writes` array (use [] for non-canon writes)' });
87
+ continue;
88
+ }
89
+ const bad = def.writes.filter((w) => !CANON_IDS.has(w));
90
+ if (bad.length > 0) {
91
+ undeclared.push({ name, reason: `\`writes\` names unknown canon id(s): ${bad.join(', ')}` });
92
+ continue;
93
+ }
94
+ mutating.push(name);
95
+ writesByTool[name] = [...def.writes];
96
+ }
97
+
98
+ return { read, mutating, setup, undeclared, writesByTool };
99
+ }
100
+
101
+ /**
102
+ * The derived mutating set. Deliberately a function of the definitions rather
103
+ * than a literal — a literal is exactly the drift this feature exists to stop.
104
+ *
105
+ * @param {Array<{name?: string, effect?: string, writes?: string[]}>} toolDefs
106
+ * @returns {Set<string>}
107
+ */
108
+ export function mutatingTools(toolDefs) {
109
+ return new Set(loadToolInventory(toolDefs).mutating);
110
+ }
111
+
112
+ /**
113
+ * Tools that write a given canon path id, derived from `writes`.
114
+ *
115
+ * @param {Array<{name?: string, effect?: string, writes?: string[]}>} toolDefs
116
+ * @param {string} canonId
117
+ * @returns {string[]}
118
+ */
119
+ export function toolsWritingCanon(toolDefs, canonId) {
120
+ const { writesByTool } = loadToolInventory(toolDefs);
121
+ return Object.keys(writesByTool).filter((t) => writesByTool[t].includes(canonId)).sort();
122
+ }
@@ -6,7 +6,7 @@
6
6
  * on any failure (network, parse, etc.) so it never breaks `compose doctor`.
7
7
  */
8
8
 
9
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
9
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs'
10
10
  import { join, dirname } from 'path'
11
11
  import { homedir } from 'os'
12
12
 
@@ -14,38 +14,48 @@ const PACKAGE_NAME = '@smartmemory/compose'
14
14
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24h
15
15
  const FETCH_TIMEOUT_MS = 3000
16
16
 
17
- function cachePath() {
18
- return join(homedir(), '.compose', 'version-cache.json')
17
+ export function cachePath() {
18
+ return process.env.COMPOSE_VERSION_CACHE || join(homedir(), '.compose', 'version-cache.json')
19
19
  }
20
20
 
21
- function readCache() {
21
+ function readCacheDoc(file) {
22
22
  try {
23
- const raw = readFileSync(cachePath(), 'utf-8')
24
- const parsed = JSON.parse(raw)
25
- if (typeof parsed?.fetchedAt !== 'number' || typeof parsed?.latest !== 'string') return null
26
- if (Date.now() - parsed.fetchedAt > CACHE_TTL_MS) return null
23
+ const parsed = JSON.parse(readFileSync(file, 'utf-8'))
24
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
25
+ // Legacy cache is exactly { fetchedAt: <number>, latest: <string> }.
26
+ // A package named `latest` is valid and would otherwise be mistaken for that shape.
27
+ if (typeof parsed.fetchedAt === 'number' && typeof parsed.latest === 'string') return {}
27
28
  return parsed
28
29
  } catch {
29
- return null
30
+ return {}
30
31
  }
31
32
  }
32
33
 
33
- function writeCache(latest) {
34
+ function readCache(pkg, file) {
35
+ const entry = readCacheDoc(file)[pkg]
36
+ if (typeof entry?.fetchedAt !== 'number' || typeof entry?.latest !== 'string') return null
37
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) return null
38
+ return entry
39
+ }
40
+
41
+ function writeCache(pkg, latest, file) {
34
42
  try {
35
- const path = cachePath()
36
- mkdirSync(dirname(path), { recursive: true })
37
- writeFileSync(path, JSON.stringify({ fetchedAt: Date.now(), latest }, null, 2))
43
+ mkdirSync(dirname(file), { recursive: true })
44
+ const doc = readCacheDoc(file)
45
+ doc[pkg] = { fetchedAt: Date.now(), latest }
46
+ writeFileSync(file, JSON.stringify(doc, null, 2))
38
47
  } catch {
39
48
  // best-effort cache; ignore failures
40
49
  }
41
50
  }
42
51
 
43
- async function fetchLatest() {
52
+ async function fetchLatest(pkg) {
53
+ if (process.env.COMPOSE_VERSION_CHECK_OFFLINE === '1') return null
44
54
  if (typeof fetch !== 'function') return null
45
55
  const controller = new AbortController()
46
56
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
47
57
  try {
48
- const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
58
+ const url = `https://registry.npmjs.org/${pkg}/latest`
49
59
  const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } })
50
60
  if (!res.ok) return null
51
61
  const data = await res.json()
@@ -89,11 +99,12 @@ export function compareVersions(a, b) {
89
99
  * behind: true if current < latest, false otherwise.
90
100
  * source: 'cache' | 'network'
91
101
  */
92
- export async function checkLatestVersion(currentVersion, { force = false } = {}) {
102
+ export async function checkPackageVersion(pkg, currentVersion, opts) {
103
+ const { force = false, fetchImpl = fetchLatest, cacheFile = cachePath(), cacheOnly = false } = opts ?? {}
93
104
  if (!currentVersion) return null
94
105
 
95
106
  if (!force) {
96
- const cached = readCache()
107
+ const cached = readCache(pkg, cacheFile)
97
108
  if (cached) {
98
109
  const cmp = compareVersions(currentVersion, cached.latest)
99
110
  if (cmp === null) return null
@@ -101,10 +112,71 @@ export async function checkLatestVersion(currentVersion, { force = false } = {})
101
112
  }
102
113
  }
103
114
 
104
- const latest = await fetchLatest()
115
+ // cacheOnly callers accept a miss rather than pay for the network. Used by the
116
+ // CLI nudge for non-interactive callers, where a courtesy line is not worth a
117
+ // request — and where concurrent spawns once left the fetch unsettled.
118
+ if (cacheOnly) return null
119
+
120
+ let latest = null
121
+ try {
122
+ latest = await fetchImpl(pkg)
123
+ } catch {
124
+ return null
125
+ }
105
126
  if (!latest) return null
106
- writeCache(latest)
127
+ writeCache(pkg, latest, cacheFile)
107
128
  const cmp = compareVersions(currentVersion, latest)
108
129
  if (cmp === null) return null
109
130
  return { current: currentVersion, latest, behind: cmp < 0, source: 'network' }
110
131
  }
132
+
133
+ /** Compose's own check. Shape and behavior unchanged — `compose doctor` and its
134
+ * --json consumers depend on this exact object. */
135
+ export async function checkLatestVersion(currentVersion, opts = {}) {
136
+ return checkPackageVersion(PACKAGE_NAME, currentVersion, opts ?? {})
137
+ }
138
+
139
+ /** The version stratum ACTUALLY resolved to, read from node_modules.
140
+ *
141
+ * Deliberately not the range declared in compose's package.json. Compose 0.3.7
142
+ * moved that declaration from an exact pin to `^0.3.3`, and npm does not
143
+ * re-resolve a caret unless something triggers an install — so the declared
144
+ * range and the installed version drift apart, and the installed one is the
145
+ * only one that tells the user anything true.
146
+ *
147
+ * Absent stratum is a normal state (not installed, capabilities.stratum off),
148
+ * not an error. */
149
+ export function resolveStratumVersion(packageRoot) {
150
+ try {
151
+ const manifest = join(packageRoot, 'node_modules', '@smartmemory', 'stratum', 'package.json')
152
+ const version = JSON.parse(readFileSync(manifest, 'utf-8')).version
153
+ return typeof version === 'string' ? version : null
154
+ } catch {
155
+ return null
156
+ }
157
+ }
158
+
159
+ /** Zero or one lines. Pure: no network, no filesystem, no clock — so every
160
+ * branch is decidable in a unit test.
161
+ *
162
+ * Silence is the default. A nudge that prints on every run gets filtered out by
163
+ * the reader, at which point it has negative value. */
164
+ export function formatDriftNudge(arg = {}) {
165
+ if (!arg || typeof arg !== 'object') return []
166
+ const { compose = null, stratum = null } = arg
167
+ const composeRecord = compose && typeof compose === 'object' ? compose : null
168
+ const stratumRecord = stratum && typeof stratum === 'object' ? stratum : null
169
+ const valid = (record) =>
170
+ record &&
171
+ record.behind === true &&
172
+ typeof record.current === 'string' &&
173
+ record.current !== '' &&
174
+ typeof record.latest === 'string' &&
175
+ record.latest !== ''
176
+
177
+ const parts = []
178
+ if (valid(composeRecord)) parts.push(`compose ${compose.current} → ${compose.latest}`)
179
+ if (valid(stratumRecord)) parts.push(`stratum ${stratum.current} → ${stratum.latest}`)
180
+ if (parts.length === 0) return []
181
+ return [`⚠ update available: ${parts.join(', ')} — run: compose update`]
182
+ }
@@ -401,12 +401,99 @@ export class VisionWriter {
401
401
  }
402
402
 
403
403
  async updateItemStatus(itemId, status) {
404
- if (await this._serverAvailable()) {
404
+ const up = await this._serverAvailable();
405
+ // COMP-COMPLETION-GATE slice 3 (AC-16, path 14): `complete` is not a status
406
+ // this general writer sets on a managed build-mode feature item, in EITHER
407
+ // transport. The legitimate write is `completeItem` — the self-verifying
408
+ // seam of §2.3b, which re-reads canonical state before writing. Fix/plan
409
+ // items, UI items with no lifecycle, and the `compose new` kickoff item
410
+ // (build mode, no feature.json) are not feature completions and keep this
411
+ // path (§2.3d). In REST mode the server's PATCH refuses too (AC-10); the
412
+ // pre-check here makes the refusal the writer's own, with the writer's message.
413
+ if (status === 'complete') {
414
+ const item = up ? await this._restGetItem(itemId) : this._load().items.find(i => i.id === itemId);
415
+ const { isManagedBuildItem } = await import('../server/completion-projection.js');
416
+ if (item && isManagedBuildItem(item, this._workspaceRoot())) {
417
+ const e = new Error(
418
+ `VisionWriter.updateItemStatus: refusing to mark ${itemId} (${item.lifecycle.featureCode}) ` +
419
+ `complete — a managed build-mode feature completes through the completion gate, which ` +
420
+ `projects it via completeItem() after verifying the commit, tests and guard ledger.`,
421
+ );
422
+ e.code = 'COMPLETE_VIA_GATE_ONLY';
423
+ throw e;
424
+ }
425
+ }
426
+ if (up) {
405
427
  return this._restUpdateItemStatus(itemId, status);
406
428
  }
407
429
  return this._directUpdateItemStatus(itemId, status);
408
430
  }
409
431
 
432
+ /**
433
+ * COMP-COMPLETION-GATE slice 3 (§2.3b, AC-4b): the ONLY path that writes
434
+ * `status: 'complete'` on a managed build-mode feature item. It carries no
435
+ * authority: in REST mode the server re-reads feature.json + the guard ledger
436
+ * before writing (`POST …/completion-projection`); in direct mode the same
437
+ * predicate runs here. Either way a completion that canonical state does not
438
+ * already record is refused.
439
+ *
440
+ * @param {string} itemId
441
+ * @param {object} evidence
442
+ * @param {string} evidence.featureCode
443
+ * @param {string} [evidence.cwd] workspace root; derived from dataDir when omitted
444
+ * @param {string} [evidence.commitSha]
445
+ * @param {string} [evidence.ledgerRef]
446
+ * @returns {Promise<{ok:true, itemId:string, verified_by:string}>}
447
+ */
448
+ async completeItem(itemId, evidence = {}) {
449
+ if (!evidence.featureCode) throw new Error('VisionWriter.completeItem: evidence.featureCode is required');
450
+ if (await this._serverAvailable()) {
451
+ const r = await this._fetch(`/api/vision/items/${itemId}/completion-projection`, {
452
+ method: 'POST',
453
+ body: JSON.stringify({
454
+ featureCode: evidence.featureCode,
455
+ commitSha: evidence.commitSha ?? null,
456
+ ledgerRef: evidence.ledgerRef ?? null,
457
+ }),
458
+ });
459
+ return { ok: true, itemId, verified_by: r.verified_by, transport: 'rest' };
460
+ }
461
+ return this._directCompleteItem(itemId, { ...evidence, cwd: evidence.cwd || this._workspaceRoot() });
462
+ }
463
+
464
+ /** The workspace root this writer's dataDir belongs to (`<root>/.compose/data`). */
465
+ _workspaceRoot() {
466
+ return path.resolve(this._dataDir, '..', '..');
467
+ }
468
+
469
+ async _restGetItem(itemId) {
470
+ try {
471
+ return await this._fetch(`/api/vision/items/${itemId}`);
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+
477
+ async _directCompleteItem(itemId, evidence) {
478
+ const state = this._load();
479
+ const item = state.items.find(i => i.id === itemId);
480
+ if (!item) throw new Error(`VisionWriter.completeItem: item not found: ${itemId}`);
481
+ const { verifiedCompleteProjection, projectionStamp } = await import('../server/completion-projection.js');
482
+ const v = await verifiedCompleteProjection({
483
+ item, featureCode: evidence.featureCode, cwd: evidence.cwd, consultGuard: true,
484
+ });
485
+ if (!v.ok) {
486
+ const e = new Error(`VisionWriter.completeItem: projection refused — ${v.reasons.join('; ')}`);
487
+ e.code = 'PROJECTION_REFUSED';
488
+ e.reasons = v.reasons;
489
+ throw e;
490
+ }
491
+ item.status = 'complete';
492
+ item.completion_projection = projectionStamp(v, { ...evidence, source: 'direct' });
493
+ this._atomicWrite(state);
494
+ return { ok: true, itemId, verified_by: v.verified_by, transport: 'direct' };
495
+ }
496
+
410
497
  async updateItemPhase(itemId, stepId) {
411
498
  if (await this._serverAvailable()) {
412
499
  return this._restUpdateItemPhase(itemId, stepId);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.3.7",
4
- "description": "Structured AI dev pipeline goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
3
+ "version": "0.3.8",
4
+ "description": "Structured AI dev pipeline: your agent writes the code, Compose makes it prove it. Gated design decisions, enforced postconditions, and independent review from goal to shipped code.",
5
5
  "author": "SmartMemory",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -20,11 +20,11 @@
20
20
  "dev:client": "vite",
21
21
  "build": "vite build",
22
22
  "preview": "vite preview",
23
- "test": "node --import ./test/suppress-expected-drift.js --test --test-timeout=120000 test/*.test.js test/comp-obs-branch/*.test.js test/integration/*.test.js && npm run test:ui && npm run test:tracker",
23
+ "test": "node --import ./test/suppress-expected-drift.js --test --test-timeout=300000 test/*.test.js test/comp-obs-branch/*.test.js test/integration/*.test.js test/golden/*.test.js && npm run test:ui && npm run test:tracker",
24
24
  "test:ui": "vitest run",
25
25
  "test:tracker": "vitest run --config vitest.tracker.config.js",
26
- "test:integration": "node --test --test-timeout=120000 test/integration/*.test.js",
27
- "test:wave-6": "node --test --test-timeout=120000 test/wave-6-integration.test.js test/wave-6-contract-compliance.test.js",
26
+ "test:integration": "node --test --test-timeout=300000 test/integration/*.test.js",
27
+ "test:wave-6": "node --test --test-timeout=300000 test/wave-6-integration.test.js test/wave-6-contract-compliance.test.js",
28
28
  "prepublishOnly": "npm run build"
29
29
  },
30
30
  "keywords": [
@@ -85,7 +85,8 @@
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.3",
88
+ "@smartmemory/sdk-js": "^1.4.60",
89
+ "@smartmemory/stratum": "^0.4.5",
89
90
  "@tanstack/react-virtual": "^3.13.23",
90
91
  "ajv": "^8.18.0",
91
92
  "ajv-formats": "^3.0.1",