@smartmemory/compose 0.3.6-beta → 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 (269) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/.compose-deps.json +1 -13
  3. package/README.md +72 -5
  4. package/bin/compose.js +754 -347
  5. package/bin/git-hooks/pre-push.template +29 -0
  6. package/bin/judgment-import.js +7 -0
  7. package/bin/judgment-migrate.js +387 -0
  8. package/contracts/comp-obs-contract.schema.json +9 -3
  9. package/contracts/feature-json.schema.json +5 -0
  10. package/contracts/fluid-record.schema.json +209 -0
  11. package/contracts/judgment-record.schema.json +425 -4
  12. package/contracts/lifecycle-backfill.schema.json +322 -0
  13. package/dist/assets/App-Z4MU-H_F.js +916 -0
  14. package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
  15. package/dist/assets/arc-DY26UIVo.js +1 -0
  16. package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
  17. package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
  18. package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
  19. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  20. package/dist/assets/channel-SnZzzh7k.js +1 -0
  21. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  22. package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
  23. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  24. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  26. package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  28. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  29. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  30. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  31. package/dist/assets/clone-DgklGjHm.js +1 -0
  32. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  33. package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
  34. package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
  35. package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
  36. package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
  37. package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
  38. package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
  39. package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
  40. package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
  41. package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
  42. package/dist/assets/graph-BNzKGvoy.js +1 -0
  43. package/dist/assets/graph-CI_1htl0.js +331 -0
  44. package/dist/assets/index-BEfrNBp8.js +123 -0
  45. package/dist/assets/index-yyrA5OZd.css +1 -0
  46. package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
  47. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
  48. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  49. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
  50. package/dist/assets/katex-DkKDou_j.js +257 -0
  51. package/dist/assets/layout-BI8cXFPI.js +1 -0
  52. package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
  53. package/dist/assets/min-vPHfnXcC.js +1 -0
  54. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
  55. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  56. package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
  57. package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
  58. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
  59. package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
  60. package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
  61. package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
  62. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  63. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
  64. package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
  65. package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
  66. package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
  67. package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +7 -5
  70. package/lib/append-integrity.js +81 -0
  71. package/lib/backfill-evidence.js +109 -0
  72. package/lib/bug-escalation.js +39 -4
  73. package/lib/build-stream-schema.js +3 -1
  74. package/lib/build-stream-writer.js +25 -0
  75. package/lib/build.js +1624 -195
  76. package/lib/canon-guard.js +245 -0
  77. package/lib/canon-override.js +196 -0
  78. package/lib/canon-registry.js +291 -0
  79. package/lib/cli-commands.js +144 -0
  80. package/lib/codex-preflight.js +50 -15
  81. package/lib/colleague/context.js +215 -0
  82. package/lib/colleague/writeback.js +95 -0
  83. package/lib/completion-gate.js +1421 -0
  84. package/lib/completion-writer.js +47 -47
  85. package/lib/consumer-fanout.js +105 -11
  86. package/lib/coverage-gate.js +200 -0
  87. package/lib/dir-lock.js +170 -0
  88. package/lib/dispatch-ledger.js +301 -0
  89. package/lib/dispatch-metrics.js +236 -0
  90. package/lib/experiment-judge.js +6 -1
  91. package/lib/feature-json.js +1 -1
  92. package/lib/feature-reconciler.js +8 -0
  93. package/lib/feature-validator.js +64 -1
  94. package/lib/feature-writer.js +66 -2
  95. package/lib/fluid/factory.js +167 -0
  96. package/lib/fluid/ideabox-dates.js +73 -0
  97. package/lib/fluid/ideabox-migrate.js +154 -0
  98. package/lib/fluid/ideabox-ops.js +585 -0
  99. package/lib/fluid/ideabox-view.js +146 -0
  100. package/lib/fluid/import-ideabox.js +186 -0
  101. package/lib/fluid/local-provider.js +606 -0
  102. package/lib/fluid/provider.js +684 -0
  103. package/lib/fluid/record-shape.js +214 -0
  104. package/lib/fluid/record-store.js +328 -0
  105. package/lib/fluid/render-ideabox.js +261 -0
  106. package/lib/fluid/schema.js +40 -0
  107. package/lib/fluid/smartmemory-provider.js +1695 -0
  108. package/lib/gsd.js +63 -14
  109. package/lib/guard-cli.js +175 -0
  110. package/lib/guard-custody.js +141 -0
  111. package/lib/guard-descriptors.js +530 -0
  112. package/lib/guard-enrol.js +254 -0
  113. package/lib/health-score.js +1 -1
  114. package/lib/hooks-status.js +32 -3
  115. package/lib/ideabox-cli.js +315 -0
  116. package/lib/ideabox.js +121 -21
  117. package/lib/judgment/store/index.js +166 -0
  118. package/lib/judgment/store/records.js +184 -25
  119. package/lib/judgment/trace.js +380 -0
  120. package/lib/judgment-attest.js +259 -0
  121. package/lib/judgment-decision-write.js +277 -0
  122. package/lib/judgment-decisions.js +466 -0
  123. package/lib/judgment-gen.js +375 -22
  124. package/lib/judgment-verify.js +153 -0
  125. package/lib/judgment-writer.js +2842 -262
  126. package/lib/lane-gate.js +2 -0
  127. package/lib/lifecycle-modes.js +4 -4
  128. package/lib/lineage.js +400 -0
  129. package/lib/local-claude-connector.js +250 -54
  130. package/lib/maya-client.js +302 -0
  131. package/lib/maya-config.js +53 -0
  132. package/lib/maya-identity.js +283 -0
  133. package/lib/mcp-enforcement.js +21 -35
  134. package/lib/migrate-anon.js +5 -0
  135. package/lib/migrate-roadmap.js +15 -0
  136. package/lib/new.js +13 -1
  137. package/lib/pipeline-compat.js +104 -0
  138. package/lib/policy-catalog.js +295 -0
  139. package/lib/policy-check.js +0 -0
  140. package/lib/process-termination.js +98 -0
  141. package/lib/resolve-workspace.js +5 -1
  142. package/lib/result-normalizer.js +428 -153
  143. package/lib/review-normalize.js +4 -0
  144. package/lib/roadmap-errors.js +65 -0
  145. package/lib/roadmap-preservers.js +24 -4
  146. package/lib/roadmap-residue.js +299 -0
  147. package/lib/smartmemory-client.js +614 -78
  148. package/lib/smartmemory-config.js +54 -0
  149. package/lib/smartmemory-ingest.js +19 -2
  150. package/lib/step-prompt.js +7 -6
  151. package/lib/stratum-engine.js +53 -4
  152. package/lib/stratum-mcp-client.js +391 -31
  153. package/lib/test-bootstrap.js +31 -0
  154. package/lib/tool-inventory.js +122 -0
  155. package/lib/version-check.js +91 -19
  156. package/lib/vision-writer.js +88 -1
  157. package/package.json +7 -6
  158. package/pipelines/bug-fix.stratum.yaml +205 -211
  159. package/pipelines/build-quick.profiles.json +12 -0
  160. package/pipelines/build-quick.stratum.yaml +263 -350
  161. package/pipelines/content.stratum.yaml +81 -77
  162. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  163. package/pipelines/plan.stratum.yaml +76 -86
  164. package/pipelines/refactor.stratum.yaml +125 -125
  165. package/pipelines/research.stratum.yaml +56 -58
  166. package/pipelines/review-fix.profiles.json +6 -0
  167. package/pipelines/review-fix.stratum.yaml +110 -83
  168. package/presets/team-feature.profiles.json +6 -0
  169. package/presets/team-feature.stratum.yaml +93 -66
  170. package/presets/team-research.profiles.json +6 -0
  171. package/presets/team-research.stratum.yaml +89 -80
  172. package/presets/team-review.profiles.json +8 -0
  173. package/presets/team-review.stratum.yaml +98 -80
  174. package/scripts/cost-census.mjs +70 -0
  175. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  176. package/server/agent-health.js +22 -0
  177. package/server/agent-hooks.js +14 -1
  178. package/server/agent-server.js +5 -248
  179. package/server/agent-spawn.js +3 -4
  180. package/server/agent-workspace.js +294 -0
  181. package/server/build-routes.js +6 -5
  182. package/server/build-stream-bridge.js +53 -0
  183. package/server/cc-session-watcher.js +4 -1
  184. package/server/coalescing-buffer.js +7 -1
  185. package/server/completion-projection.js +228 -0
  186. package/server/compose-mcp-tools.js +124 -24
  187. package/server/compose-mcp.js +91 -790
  188. package/server/decision-event-emit.js +41 -2
  189. package/server/decision-event-id.js +17 -0
  190. package/server/decision-events-snapshot.js +3 -0
  191. package/server/design-routes.js +14 -8
  192. package/server/feature-scan.js +76 -2
  193. package/server/file-watcher.js +170 -21
  194. package/server/ideabox-routes.js +166 -224
  195. package/server/index.js +70 -100
  196. package/server/lifecycle-guard.js +240 -10
  197. package/server/lifecycle-phase-history.js +276 -0
  198. package/server/maya-routes.js +507 -0
  199. package/server/mcp-tool-defs.js +940 -0
  200. package/server/mcp-tool-policy.js +35 -3
  201. package/server/model-tiers.js +22 -5
  202. package/server/pipeline-routes.js +21 -11
  203. package/server/project-root.js +58 -19
  204. package/server/remote-utils.js +3 -1
  205. package/server/schema-validator.js +7 -1
  206. package/server/session-manager.js +5 -6
  207. package/server/session-routes.js +3 -1
  208. package/server/stratum-client.js +57 -10
  209. package/server/stratum-sync.js +6 -3
  210. package/server/summarizer.js +3 -4
  211. package/server/supervisor.js +0 -1
  212. package/server/vision-routes.js +208 -98
  213. package/server/vision-server.js +86 -23
  214. package/server/vision-store.js +60 -6
  215. package/server/vision-utils.js +3 -4
  216. package/server/workspace-activity.js +18 -0
  217. package/server/workspace-middleware.js +2 -2
  218. package/server/workspace-runtime.js +243 -0
  219. package/server/worktree-gc.js +1 -0
  220. package/dist/assets/App-BG3ngu8H.js +0 -896
  221. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  222. package/dist/assets/arc-_v4hR_uD.js +0 -1
  223. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  224. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  225. package/dist/assets/channel-B3U5wFAT.js +0 -1
  226. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  227. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  228. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  229. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  230. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  231. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  232. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  233. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  234. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  235. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  236. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  237. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  238. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  239. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  240. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  241. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  242. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  243. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  244. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  245. package/dist/assets/graph-BXPQrYYB.js +0 -331
  246. package/dist/assets/graph-C9eacEi8.js +0 -1
  247. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  248. package/dist/assets/index-LIwREYgH.css +0 -1
  249. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  250. package/dist/assets/katex-C5jXJg4s.js +0 -257
  251. package/dist/assets/layout-DEXfKzaS.js +0 -1
  252. package/dist/assets/map-Czzmt4hB.js +0 -1
  253. package/dist/assets/mobile-CaoXUwAr.js +0 -17
  254. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  255. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  256. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  257. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  258. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  259. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  260. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  261. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  262. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  263. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  264. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  265. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  266. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  267. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
  268. package/lib/staleness.js +0 -87
  269. package/server/ideabox-cache.js +0 -77
@@ -68,6 +68,7 @@ const IMPACTS = new Set(['low', 'medium', 'high']);
68
68
  // front estimator, the doc-reading refinement, or an escalation bump.
69
69
  const LANES = new Set(['trivial', 'standard', 'complex']);
70
70
  const ESTIMATE_SOURCES = new Set(['front', 'refined', 'escalated']);
71
+ const TRIAGE_CONFIDENCES = new Set(['high', 'medium', 'low']);
71
72
 
72
73
  // ---------------------------------------------------------------------------
73
74
  // validateFeatureFields — shared field-shape guard
@@ -90,6 +91,7 @@ const ESTIMATE_SOURCES = new Set(['front', 'refined', 'escalated']);
90
91
  * @param {number} [fields.triageTier] integer 0-4
91
92
  * @param {string} [fields.lane] one of trivial|standard|complex
92
93
  * @param {string} [fields.estimateSource] one of front|refined|escalated
94
+ * @param {string} [fields.triageConfidence] one of high|medium|low
93
95
  */
94
96
  export function validateFeatureFields(fields) {
95
97
  if (fields.complexity !== undefined && !COMPLEXITIES.has(fields.complexity)) {
@@ -107,6 +109,11 @@ export function validateFeatureFields(fields) {
107
109
  `feature-writer: invalid estimateSource "${fields.estimateSource}" — must be one of ${[...ESTIMATE_SOURCES].join(', ')}`
108
110
  );
109
111
  }
112
+ if (fields.triageConfidence !== undefined && !TRIAGE_CONFIDENCES.has(fields.triageConfidence)) {
113
+ throw new Error(
114
+ `feature-writer: invalid triageConfidence "${fields.triageConfidence}" — must be one of ${[...TRIAGE_CONFIDENCES].join(', ')}`
115
+ );
116
+ }
110
117
  }
111
118
 
112
119
  // ---------------------------------------------------------------------------
@@ -164,6 +171,7 @@ function maybeIdempotent(args, fn) {
164
171
  * @param {string} [args.lane] COMP-TRIAGE-5: trivial | standard | complex
165
172
  * @param {number} [args.triageTier] COMP-TRIAGE-5: integer 0-4
166
173
  * @param {string} [args.estimateSource] COMP-TRIAGE-5: front | refined | escalated
174
+ * @param {string} [args.triageConfidence] COMP-TRIAGE-6: high | medium | low
167
175
  * @param {boolean} [args.force]
168
176
  * @param {string} [args.idempotency_key]
169
177
  */
@@ -185,6 +193,38 @@ export async function addRoadmapEntry(cwd, args) {
185
193
  if (!STATUSES.has(status)) {
186
194
  throw new Error(`feature-writer: invalid status "${status}"`);
187
195
  }
196
+ // COMP-COMPLETION-GATE: a feature cannot be BORN complete. This closed the
197
+ // single worst bypass found in the coverage audit — `compose roadmap add
198
+ // --status COMPLETE` (and proposeFollowup, which forwards caller status here)
199
+ // minted an already-finished feature in one command: no evidence, no
200
+ // lifecycle, no ledger entry, nothing to audit. Completion is a transition
201
+ // with evidence, never an initial condition.
202
+ //
203
+ // MIGRATION EXEMPTION. A migration is not minting a completion — it is
204
+ // transcribing one that already happened, from a ROADMAP row or an older
205
+ // layout into feature.json. Refusing those would make the migration tools
206
+ // unable to represent history that predates the gate. The exemption is
207
+ // explicit (callers pass a reason), narrow (creation only), and logged, so an
208
+ // exempt write is visible rather than silent. It is NOT a general escape
209
+ // hatch: ordinary callers have no reason to pass it, and `compose roadmap add`
210
+ // does not.
211
+ if (status === 'COMPLETE') {
212
+ if (!args._migration?.reason) {
213
+ const e = new Error(
214
+ 'feature-writer: cannot create a feature with status COMPLETE. A completion must be ' +
215
+ 'recorded through the completion gate (record_completion / `compose record-completion`), ' +
216
+ 'which verifies the commit and test evidence and writes a guarded ledger entry. ' +
217
+ 'Create the feature first, then complete it.',
218
+ );
219
+ e.code = 'COMPLETE_ON_CREATE_REFUSED';
220
+ throw e;
221
+ }
222
+ // eslint-disable-next-line no-console
223
+ console.warn(
224
+ `[feature-writer] migration exemption: creating "${args.code}" as COMPLETE ` +
225
+ `without a completion record — ${args._migration.reason}`,
226
+ );
227
+ }
188
228
  // COMP-ROADMAP-PLAN: minimal type validation for the plan-handshake fields.
189
229
  if (args.profile !== undefined &&
190
230
  (typeof args.profile !== 'object' || args.profile === null || Array.isArray(args.profile))) {
@@ -235,6 +275,7 @@ export async function addRoadmapEntry(cwd, args) {
235
275
  if (args.lane !== undefined) feature.lane = args.lane;
236
276
  if (args.triageTier !== undefined) feature.triageTier = args.triageTier;
237
277
  if (args.estimateSource !== undefined) feature.estimateSource = args.estimateSource;
278
+ if (args.triageConfidence !== undefined) feature.triageConfidence = args.triageConfidence;
238
279
 
239
280
  let roundtrip = null;
240
281
  if (isLocalProvider(provider)) {
@@ -334,7 +375,7 @@ function readRoadmapBase(roadmapPath) {
334
375
  // losslessness is surfaced by the validator (Task 6 / validate_project), not
335
376
  // blocked here. Return only the small diagnostic fields; `canonical` is the full
336
377
  // regenerated ROADMAP and must not leak into MCP writer results.
337
- async function roundtripGuard(cwd, provider, mutate, { force, label }) {
378
+ export async function roundtripGuard(cwd, provider, mutate, { force, label }) {
338
379
  const current = await provider.listFeatures();
339
380
  const projected = mutate(current.map(f => ({ ...f })));
340
381
  const roadmapPath = resolveRoadmapPath(cwd);
@@ -370,7 +411,7 @@ async function roundtripGuard(cwd, provider, mutate, { force, label }) {
370
411
  // Routes through provider.appendEvent so GitHubProvider can post
371
412
  // <!--compose-event--> comments + mirror Projects v2. LocalFileProvider
372
413
  // delegates to feature-events.js#appendEvent producing byte-identical output.
373
- async function safeAppendEvent(cwd, event) {
414
+ export async function safeAppendEvent(cwd, event) {
374
415
  try {
375
416
  const provider = await getProvider(cwd);
376
417
  await provider.appendEvent(event.code, event);
@@ -415,6 +456,29 @@ export async function setFeatureStatus(cwd, args) {
415
456
  return { code: args.code, from, to, ts: new Date().toISOString(), noop: true };
416
457
  }
417
458
 
459
+ // COMP-COMPLETION-GATE slice 3 (AC-9, Decision 8): COMPLETE is not a status
460
+ // this writer can set. Not with `force`, not with `derived`, not with an
461
+ // override token — unconditionally. A completion is a transition WITH
462
+ // evidence (a real commit, an attested test run, a guarded ledger entry),
463
+ // and the one place that verifies those is the completion gate, which then
464
+ // performs the COMPLETE write itself through `persistFeatureRaw`. Every
465
+ // other caller that used to reach COMPLETE through here was a bypass:
466
+ // `set_feature_status` (path 5), the reconciler's derived projection (path
467
+ // 10), `projectFeatureStatus(phase:'complete')`, and the sibling-repo
468
+ // xref-push. There is no marker that lets a caller through, because any
469
+ // marker this module exported would be importable by the callers it exists
470
+ // to refuse.
471
+ if (to === 'COMPLETE') {
472
+ const e = new Error(
473
+ `feature-writer: refusing to set ${args.code} to COMPLETE — status flips to COMPLETE go ` +
474
+ `through the completion gate only (record_completion / \`compose record-completion\` / ` +
475
+ `the build runner at terminalization), which verifies commit + test evidence and takes ` +
476
+ `the guarded transition. \`force\` and \`derived\` do not apply.`,
477
+ );
478
+ e.code = 'COMPLETE_VIA_GATE_ONLY';
479
+ throw e;
480
+ }
481
+
418
482
  const allowed = TRANSITIONS[from] ?? [];
419
483
  // `derived: true` marks a lifecycle-authoritative projection (COMP-MCP-ENFORCE
420
484
  // Slice 2, lifecycle-as-truth): the roadmap transition table is not the
@@ -0,0 +1,167 @@
1
+ /**
2
+ * lib/fluid/factory.js — configured fluid-store provider selection.
3
+ *
4
+ * Mirrors `lib/tracker/factory.js` in config handling (absent means default,
5
+ * malformed means fail loud) and DELIBERATELY DIVERGES from it in one respect:
6
+ * there is no `withFallback` proxy here.
7
+ *
8
+ * The tracker seam wraps its active provider so that an entity the provider
9
+ * cannot store falls through to the local one. That is right for the tracker,
10
+ * where the fallback is STORAGE and the substituted answer is equally true.
11
+ * It is wrong here. `PROVIDER-SEAM` forbids it: "a provider without a
12
+ * capability lacks it visibly; nothing fakes it." A fallback that answered
13
+ * `recall()` or `challenge()` from the floor would return a real-looking result
14
+ * produced by machinery that does not exist, which is worse than an error —
15
+ * the caller cannot tell the difference, and neither can the user.
16
+ *
17
+ * So capability absence propagates as `FluidCapabilityUnavailable` from
18
+ * `FluidProvider.require()`, and surfaces render it as a funnel ("challenge:
19
+ * connect SmartMemory") rather than an empty state.
20
+ */
21
+
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+
25
+ import { getSmartmemoryConfig } from '../smartmemory-config.js';
26
+ import { FluidConfigError, MUTATION_SCOPE, mutationScopeAtLeast } from './provider.js';
27
+ import { LocalFluidProvider } from './local-provider.js';
28
+ import { SmartMemoryFluidProvider } from './smartmemory-provider.js';
29
+
30
+ /**
31
+ * Read `.compose/compose.json` → `fluid`.
32
+ * Absent file or absent key → local floor. That is a valid, supported
33
+ * configuration (the zero-install default), not a misconfiguration.
34
+ */
35
+ function loadFluidConfig(cwd) {
36
+ const p = join(cwd, '.compose/compose.json');
37
+ if (!existsSync(p)) return { provider: 'local' };
38
+
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(readFileSync(p, 'utf8'));
42
+ } catch (e) {
43
+ // The file EXISTS but is malformed. Falling back silently here would mask a
44
+ // typo in the user's config and quietly downgrade them to the floor —
45
+ // meaning their configured semantic capabilities would vanish with no
46
+ // signal. Fail loud.
47
+ throw new FluidConfigError(
48
+ `compose: fluid config at ${p} contains invalid JSON — ${e.message}`,
49
+ { path: p }
50
+ );
51
+ }
52
+
53
+ // Valid JSON is not a valid config. `[]`, `"smartmemory"` and `42` all parse,
54
+ // and reading `.fluid` off them yields undefined — which would silently select
55
+ // the floor, exactly the quiet downgrade this function exists to prevent.
56
+ // `null` would throw a bare TypeError instead of a config error.
57
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
58
+ throw new FluidConfigError(
59
+ `compose: fluid config at ${p} must be a JSON object ` +
60
+ `(got ${parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed})`,
61
+ { path: p }
62
+ );
63
+ }
64
+
65
+ const fluid = parsed.fluid;
66
+ if (fluid === undefined || fluid === null) return { provider: 'local' };
67
+ if (typeof fluid !== 'object' || Array.isArray(fluid)) {
68
+ throw new FluidConfigError(
69
+ `compose: fluid config at ${p} has a "fluid" key but it is not an object ` +
70
+ `(got ${Array.isArray(fluid) ? 'array' : typeof fluid})`,
71
+ { path: p }
72
+ );
73
+ }
74
+ return fluid;
75
+ }
76
+
77
+ /**
78
+ * Construct the configured provider.
79
+ *
80
+ * @param {string} cwd project root
81
+ * @param {object} [opts] passed through to the provider's init (the floor takes
82
+ * `recordsRoot` to relocate its tracked record tree)
83
+ * @returns {Promise<import('./provider.js').FluidProvider>}
84
+ */
85
+ export async function fluidProviderFor(cwd, opts = {}) {
86
+ return warnIfUnsafelyShared(await construct(cwd, opts));
87
+ }
88
+
89
+ /**
90
+ * Warn when a provider's store is reachable from more machines than its
91
+ * serialization is.
92
+ *
93
+ * DERIVED FROM THE PROVIDER'S OWN DECLARATIONS, never hardcoded per provider.
94
+ * A hardcoded warning has to be remembered by whoever adds the third provider —
95
+ * which is the same "the second implementation looked complete" failure this
96
+ * whole feature exists to close — and it has to be remembered AGAIN, in the
97
+ * other direction, on the day the gap is fixed, or it keeps crying wolf.
98
+ *
99
+ * Warns rather than refuses: storage and recall work, COMP-FOH shipped them
100
+ * deliberately, and refusing would break a configuration that is fine for
101
+ * everything except concurrent writes. Silence is the only wrong option, because
102
+ * the failure is invisible until it has cost an idea.
103
+ */
104
+ function warnIfUnsafelyShared(provider) {
105
+ if (provider.isShared() && !mutationScopeAtLeast(provider.mutationScope(), MUTATION_SCOPE.CLUSTER)) {
106
+ console.warn(
107
+ `compose: fluid provider "${provider.name()}" is shared across machines but serializes `
108
+ + `mutation only at "${provider.mutationScope()}" scope, so concurrent writes can allocate `
109
+ + `the same handle and lose records. Prefer the local provider for the ideabox until its `
110
+ + `store offers a cross-machine reservation primitive (SmartMemory SVC-LEASE-1).`
111
+ );
112
+ }
113
+ return provider;
114
+ }
115
+
116
+ async function construct(cwd, opts = {}) {
117
+ const cfg = loadFluidConfig(cwd);
118
+ const name = cfg.provider ?? 'local';
119
+
120
+ if (name === 'local') {
121
+ return new LocalFluidProvider().init(cwd, { ...cfg.local, ...opts });
122
+ }
123
+
124
+ if (name === 'smartmemory') {
125
+ // The warning that used to live here is gone, not deleted: it is now
126
+ // `warnIfUnsafelyShared`, derived from this provider's own
127
+ // `isShared()`/`mutationScope()` rather than hardcoded to its name
128
+ // (COMP-FLUID-SEAM-GUARANTEES). BOTH gaps it named are now closed — the
129
+ // restartable import first, then cross-machine serialization once
130
+ // SmartMemory shipped SVC-ALLOC-1 and SVC-LEASE-1 (COMP-FLUID-SEAM-GUARANTEES) — so this
131
+ // provider declares CLUSTER and the warning no longer fires for it. That is
132
+ // exactly the "remember it AGAIN, in the other direction" the check below
133
+ // was written to make automatic: nothing here had to be edited to stop the
134
+ // warning, the declaration moving was enough.
135
+
136
+ // The endpoint and credential come from the EXISTING top-level
137
+ // `smartmemory` block, shared with the shipped kitchen pipeline, so there
138
+ // is one source of truth for where SmartMemory lives. Only `workspaceId`
139
+ // is new, and it lives under `fluid.smartmemory` because it scopes fluid
140
+ // records specifically.
141
+ //
142
+ // A missing block is a config error, not a downgrade to the floor: a user
143
+ // who configured SmartMemory did so to get capabilities the floor does not
144
+ // have, and starting up on the floor instead would present an
145
+ // intelligence-free system as a working one.
146
+ const sm = getSmartmemoryConfig(cwd);
147
+ if (!sm || Object.keys(sm).length === 0) {
148
+ throw new FluidConfigError(
149
+ 'compose: fluid provider "smartmemory" requires a top-level "smartmemory" config ' +
150
+ 'block in .compose/compose.json (baseUrl + apiKeyEnv). It is shared with the ' +
151
+ 'SmartMemory ingest pipeline rather than duplicated under "fluid".',
152
+ { provider: name, setting: 'smartmemory' }
153
+ );
154
+ }
155
+ return new SmartMemoryFluidProvider().init(cwd, {
156
+ baseUrl: sm.baseUrl,
157
+ apiKeyEnv: sm.apiKeyEnv,
158
+ timeoutMs: sm.timeoutMs,
159
+ ...cfg.smartmemory,
160
+ ...opts,
161
+ });
162
+ }
163
+
164
+ throw new FluidConfigError(`compose: unknown fluid provider "${name}"`, { provider: name });
165
+ }
166
+
167
+ export { loadFluidConfig };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * lib/fluid/ideabox-dates.js — the date vocabulary of the markdown boundary.
3
+ *
4
+ * COMP-PLAN-IDEA-UNIFY S3b-1.
5
+ *
6
+ * The record contract types every event date as `format: "date-time"`
7
+ * (`contracts/fluid-record.schema.json` — `killed.at`, `discussion[].at`), but
8
+ * the ideabox markdown has only ever carried a bare `YYYY-MM-DD`: the
9
+ * discussion grammar is `- [YYYY-MM-DD] author: text`
10
+ * (`lib/ideabox.js:53`) and `killIdea` stamps `…toISOString().slice(0, 10)`
11
+ * (`lib/ideabox.js:546`).
12
+ *
13
+ * Two directions, and BOTH were wrong before this module existed:
14
+ *
15
+ * import markdown date → record date-time (widen)
16
+ * render record date-time → markdown date (narrow)
17
+ *
18
+ * The importer passed the bare date straight into the contract, so importing
19
+ * any idea carrying a discussion entry or a kill date threw
20
+ * `must match format "date-time"`. The renderer emitted the full ISO timestamp
21
+ * straight into the markdown, where `DISCUSSION_ENTRY_RE` cannot match it — so
22
+ * a provider-written discussion entry degraded to an unparsed extra line and
23
+ * was silently lost on the next read.
24
+ *
25
+ * Neither defect was caught, because no idea on disk had ever carried a
26
+ * discussion entry and the Killed Ideas section was empty. Both paths were
27
+ * dead code with passing tests over them.
28
+ *
29
+ * They live together in one module deliberately: the two conversions are a
30
+ * single round-trip contract, and their drifting apart is precisely the bug.
31
+ * Splitting them across the importer and the renderer is what let it happen.
32
+ *
33
+ * Precision is intentionally asymmetric. A record keeps the full timestamp; the
34
+ * projection is a view and shows the day. Rendering a date-time into a file
35
+ * whose grammar is a date does not preserve information, it corrupts the line.
36
+ */
37
+
38
+ /** A bare calendar date, the only date form the ideabox markdown can carry. */
39
+ const MARKDOWN_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
40
+
41
+ const EPOCH = new Date(0).toISOString();
42
+
43
+ /**
44
+ * Widen a markdown date to a contract-valid `date-time`.
45
+ *
46
+ * A value that already carries a time is passed through untouched, so this is
47
+ * safe to apply to input of mixed provenance (an imported entry and a
48
+ * provider-written one can sit in the same array).
49
+ *
50
+ * @param {string|null|undefined} value a `YYYY-MM-DD`, a full ISO timestamp, or nothing
51
+ * @returns {string} an ISO 8601 date-time
52
+ */
53
+ export function toRecordTimestamp(value) {
54
+ if (!value) return EPOCH;
55
+ if (MARKDOWN_DATE_RE.test(value)) return `${value}T00:00:00.000Z`;
56
+ return value;
57
+ }
58
+
59
+ /**
60
+ * Narrow a contract `date-time` to the markdown's `YYYY-MM-DD`.
61
+ *
62
+ * Anything unparseable is returned unchanged rather than coerced: emitting a
63
+ * wrong-but-well-formed date would be worse than emitting the raw value, which
64
+ * is at least visibly odd.
65
+ *
66
+ * @param {string|null|undefined} value an ISO 8601 date-time
67
+ * @returns {string} a `YYYY-MM-DD`
68
+ */
69
+ export function toMarkdownDate(value) {
70
+ if (typeof value !== 'string') return '';
71
+ const head = value.slice(0, 10);
72
+ return MARKDOWN_DATE_RE.test(head) ? head : value;
73
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * lib/fluid/ideabox-migrate.js — the first-use gate between a markdown ideabox
3
+ * and the record store.
4
+ *
5
+ * COMP-PLAN-IDEA-UNIFY S3b-1 (F2).
6
+ *
7
+ * THE FAILURE THIS EXISTS TO PREVENT
8
+ * ----------------------------------
9
+ * `@smartmemory/compose` is published. Other projects have their own populated
10
+ * `docs/product/ideabox.md` and no fluid records. The cutover makes records
11
+ * canon and the markdown a projection of them — so without this gate, the first
12
+ * `compose ideabox add` in an upgraded project allocates IDEA-1 against an empty
13
+ * store and the projection replaces that project's entire ideabox with the one
14
+ * idea they just typed. Their ideas would survive only in their git history.
15
+ *
16
+ * The blueprint missed this by treating the import as a one-time operation on
17
+ * THIS repository. It is not: it is an upgrade path, and it runs once per
18
+ * installation, on whatever that installation happens to have.
19
+ *
20
+ * WHY IT REFUSES RATHER THAN REPAIRS
21
+ * ----------------------------------
22
+ * The dangerous state is not "no records" — that one is unambiguous and is
23
+ * simply imported. It is a PARTIAL store: some records present, and markdown
24
+ * entries that have no record behind them. Any automatic reading of that state
25
+ * is a guess. Importing the strays assumes the markdown is authoritative, which
26
+ * it no longer is. Ignoring them assumes they were deliberately deleted, and
27
+ * projects over them. Both silently discard someone's work in one of the two
28
+ * cases. So it stops and names what it found.
29
+ *
30
+ * That check costs a parse of a small file per mutation, which is the same file
31
+ * the CLI already read on every mutation before the cutover.
32
+ */
33
+
34
+ import { existsSync, readFileSync } from 'node:fs';
35
+
36
+ import { parseIdeabox } from '../ideabox.js';
37
+ import { importIdeabox } from './import-ideabox.js';
38
+ import { KIND } from './provider.js';
39
+
40
+ export class IdeaboxMigrationConflict extends Error {
41
+ constructor(missing, ideaboxPath) {
42
+ super(
43
+ `compose: the ideabox at ${ideaboxPath} contains ${missing.length} idea(s) with no record ` +
44
+ `behind them: ${missing.join(', ')}. The record store is canon now, so this file is ` +
45
+ `generated output — which means these entries were either hand-added after the migration ` +
46
+ `or lost by a partial one, and guessing which would discard someone's work either way. ` +
47
+ `Re-add them with \`compose ideabox add\`, or delete them from the file if they are stale, ` +
48
+ `then run \`compose ideabox render\`.`
49
+ );
50
+ this.name = 'IdeaboxMigrationConflict';
51
+ this.code = 'IDEABOX_MIGRATION_CONFLICT';
52
+ this.missing = missing;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Ensure the record store reflects the markdown before any mutation touches it.
58
+ *
59
+ * Runs before every mutating ideabox command. Three states, one of which stops
60
+ * the command:
61
+ *
62
+ * - **no records, markdown has entries** → import it (the upgrade path)
63
+ * - **records exist, markdown adds nothing** → already migrated, proceed
64
+ * - **records exist, markdown has entries with no record** → refuse
65
+ *
66
+ * A fresh project with no markdown and no records is the trivial first case and
67
+ * simply proceeds.
68
+ *
69
+ * @param {import('./provider.js').FluidProvider} provider
70
+ * @param {string} ideaboxPath absolute path to the markdown ideabox
71
+ * @returns {Promise<{migrated: boolean, imported: string[]}>}
72
+ */
73
+ export async function ensureIdeaboxMigrated(provider, ideaboxPath) {
74
+ const records = await provider.listRecords({ kind: KIND.IDEA });
75
+ const markdown = existsSync(ideaboxPath) ? readFileSync(ideaboxPath, 'utf8') : null;
76
+
77
+ if (markdown === null) return { migrated: false, imported: [] };
78
+
79
+ const parsed = parseIdeabox(markdown);
80
+ const inMarkdown = [...(parsed.ideas ?? []), ...(parsed.killed ?? [])].map((i) => i.id);
81
+
82
+ const known = new Set(records.map((r) => r.handle));
83
+ const missing = inMarkdown.filter((id) => !known.has(id));
84
+
85
+ if (records.length === 0) {
86
+ if (inMarkdown.length === 0) return { migrated: false, imported: [] };
87
+ // The upgrade path.
88
+ const result = await importIdeabox(provider, { markdown, path: ideaboxPath });
89
+ return { migrated: true, imported: result.imported };
90
+ }
91
+
92
+ if (missing.length) {
93
+ // RESUME versus REFUSE, decided PER HANDLE from the events log.
94
+ //
95
+ // A crash partway through the first-use import leaves some records written
96
+ // and the rest missing, which lands here rather than in the empty-store
97
+ // branch above. Refusing that outright strands the installation: the error
98
+ // names `compose ideabox add`, `add` runs this same gate, so every command
99
+ // fails and there is no way out — and the reclaim path built for exactly
100
+ // this case is never reached.
101
+ //
102
+ // The log distinguishes the two populations. A handle the import already
103
+ // burned carries an event; `importIdeabox` skips live records and reclaims
104
+ // its own aborted allocations, so resuming is safe and lossless.
105
+ //
106
+ // The evidence has to be per-handle, not "did an import ever run". Once the
107
+ // first import succeeds the log carries `imported` events forever, so a
108
+ // global check would quietly import anything later hand-added to what is
109
+ // now generated output — losing the very protection this gate exists for.
110
+ // A handle with no event was never issued here: it was typed into the file
111
+ // by hand, and importing it would treat the markdown as authoritative when
112
+ // it no longer is.
113
+ // Three populations, and only one of them is resumable:
114
+ // - issued, no `deleted` event → a create that crashed. RESUME.
115
+ // - issued, `deleted` event → deliberately retired; this file is just
116
+ // stale output. REFUSE (a render fixes it,
117
+ // and importing would resurrect it).
118
+ // - never issued → hand-typed into generated output. REFUSE.
119
+ const { issued, deleted } = await handleHistory(provider);
120
+ const resumable = missing.filter((id) => issued.has(id) && !deleted.has(id));
121
+ const strays = missing.filter((id) => !resumable.includes(id));
122
+ if (strays.length) throw new IdeaboxMigrationConflict(strays, ideaboxPath);
123
+
124
+ const result = await importIdeabox(provider, { markdown, path: ideaboxPath });
125
+ return { migrated: true, imported: result.imported };
126
+ }
127
+
128
+ return { migrated: false, imported: [] };
129
+ }
130
+
131
+ /**
132
+ * Which handles this store's log has seen, and which of those were retired.
133
+ *
134
+ * Both sets are empty when the history cannot be read, which makes every
135
+ * missing handle a stray and every ambiguous state a refusal. That is the safe
136
+ * direction: resuming on a guess is the one thing this must not do.
137
+ */
138
+ async function handleHistory(provider) {
139
+ const empty = { issued: new Set(), deleted: new Set() };
140
+ if (typeof provider.readEvents !== 'function') return empty;
141
+ try {
142
+ const events = (await provider.readEvents()) ?? [];
143
+ const issued = new Set();
144
+ const deleted = new Set();
145
+ for (const event of events) {
146
+ if (!event?.handle) continue;
147
+ issued.add(event.handle);
148
+ if (event.type === 'deleted') deleted.add(event.handle);
149
+ }
150
+ return { issued, deleted };
151
+ } catch {
152
+ return empty;
153
+ }
154
+ }