@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
@@ -0,0 +1,146 @@
1
+ /**
2
+ * lib/fluid/ideabox-view.js — fluid records → the ideabox's client-facing shape.
3
+ *
4
+ * COMP-PLAN-IDEA-UNIFY S3b-2 (D21, reversed after review).
5
+ *
6
+ * THE ONE MAPPING
7
+ * ---------------
8
+ * `/api/ideabox` and every mutation response are produced HERE, from records.
9
+ * The first draft of this slice derived them by parsing the markdown projection
10
+ * the write had just rendered — one shape, reusing a proven parser, no second
11
+ * adapter to drift. That was wrong for two reasons that outrank tidiness:
12
+ *
13
+ * 1. **The feature's acceptance criterion says otherwise** ("`useIdeaboxStore`
14
+ * / `/api/ideabox` serve from fluid records", design.md). Parsing the
15
+ * projection is serving from a rendering of the records, which is not the
16
+ * same claim.
17
+ * 2. **It is only correct on the local provider.** The projection is a LOCAL
18
+ * file. Configure the SmartMemory provider — a store shared across machines
19
+ * — and a write on machine A never regenerates machine B's markdown, so B's
20
+ * REST and UI serve an indefinitely stale view of a store that is perfectly
21
+ * up to date. Fidelity of the projection says nothing about its freshness.
22
+ *
23
+ * The cost of this direction is the one the first draft was avoiding: this file
24
+ * is a second place where a record's fields become a client's fields, and a
25
+ * field added to the record contract but not here is invisible to the cockpit.
26
+ * That is bounded by a test asserting this projection and the markdown one carry
27
+ * the same field set, so the two cannot silently disagree.
28
+ *
29
+ * WHY THE SHAPE IS THE LEGACY PARSER'S AND NOT THE RECORD'S
30
+ * --------------------------------------------------------
31
+ * `id` here is the record's HANDLE (`IDEA-42`), not its `id` (a provider UUID
32
+ * that changes on a provider swap). Every client keys on the handle and always
33
+ * has. `description` is the record's `body`, `status` is the uppercase display
34
+ * token, and an untriaged priority is an em dash rather than null. Returning raw
35
+ * records would have been cleaner and would have broken the cockpit, the mobile
36
+ * app and their tests in the same commit.
37
+ */
38
+
39
+ import { toMarkdownDate } from './ideabox-dates.js';
40
+ import { KIND } from './provider.js';
41
+
42
+ /** Canonical status → the display token the surfaces have always rendered. */
43
+ const STATUS_TOKEN = Object.freeze({
44
+ new: 'NEW',
45
+ discussing: 'DISCUSSING',
46
+ promoted: 'PROMOTED',
47
+ killed: 'KILLED',
48
+ });
49
+
50
+ /** The numeric suffix of a handle, which clients sort and display as `num`. */
51
+ export function handleNumber(handle) {
52
+ const m = /-([0-9]+)$/.exec(handle ?? '');
53
+ return m ? Number(m[1]) : 0;
54
+ }
55
+
56
+ /**
57
+ * One record → one client idea.
58
+ *
59
+ * @param {object} record a normalized fluid record
60
+ * @param {Map<string,string>} clusterTitles handle → display title. Clients show
61
+ * and filter on the cluster's NAME, which is what the markdown surface always
62
+ * gave them; the record stores a handle so that renaming an umbrella does not
63
+ * orphan its members. Resolution happens here rather than at the call site so
64
+ * no caller can forget it and leak `CLUS-3` into the UI.
65
+ */
66
+ export function toClientIdea(record, clusterTitles = new Map()) {
67
+ return {
68
+ id: record.handle,
69
+ num: handleNumber(record.handle),
70
+ title: record.title,
71
+ // `status_label` preserves what an author wrote when the canonical enum
72
+ // could not hold it (`RE-AIMED (2026-07-21)`); the markdown surface shows
73
+ // that token, so this one must too.
74
+ status: record.status_label || STATUS_TOKEN[record.status] || 'NEW',
75
+ priority: record.priority ?? '—',
76
+ tags: [...(record.tags ?? [])],
77
+ source: record.source ?? '',
78
+ description: record.body ?? '',
79
+ cluster: record.cluster ? clusterTitles.get(record.cluster) ?? record.cluster : null,
80
+ clusterHandle: record.cluster ?? null,
81
+ mapsTo: record.links?.find((l) => l.type === 'maps_to')?.target ?? '',
82
+ promotedTo: record.links?.find((l) => l.type === 'promoted_to')?.target ?? '',
83
+ effort: record.effort ?? null,
84
+ impact: record.impact ?? null,
85
+ killedReason: record.killed?.reason ?? '',
86
+ killedDate: record.killed ? toMarkdownDate(record.killed.at) : '',
87
+ discussion: (record.discussion ?? []).map((d) => ({
88
+ date: toMarkdownDate(d.at),
89
+ author: d.author ?? 'unknown',
90
+ text: d.text,
91
+ })),
92
+ };
93
+ }
94
+
95
+ /** Cluster handle → title, for `toClientIdea`. */
96
+ export function clusterTitleMap(clusters) {
97
+ return new Map((clusters ?? []).map((c) => [c.handle, c.title]));
98
+ }
99
+
100
+ /**
101
+ * The whole `/api/ideabox` payload, read from the provider.
102
+ *
103
+ * Live and killed ideas are separate arrays because that is the split every
104
+ * client already renders, and `nextId` is derived rather than stored — the
105
+ * markdown parser computed it the same way, and a stored counter would be a
106
+ * second allocator disagreeing with the provider's.
107
+ *
108
+ * @param {import('./provider.js').FluidProvider} provider
109
+ */
110
+ export async function ideaboxView(provider) {
111
+ const [ideas, clusters] = await Promise.all([
112
+ provider.listRecords({ kind: KIND.IDEA }),
113
+ provider.listRecords({ kind: KIND.CLUSTER }),
114
+ ]);
115
+
116
+ const titles = clusterTitleMap(clusters);
117
+ const byHandle = (a, b) => handleNumber(a.handle) - handleNumber(b.handle);
118
+ const ordered = [...ideas].sort(byHandle);
119
+
120
+ const maxNum = ordered.reduce((m, r) => Math.max(m, handleNumber(r.handle)), 0);
121
+
122
+ return {
123
+ ideas: ordered.filter((r) => r.status !== 'killed').map((r) => toClientIdea(r, titles)),
124
+ killed: ordered.filter((r) => r.status === 'killed').map((r) => toClientIdea(r, titles)),
125
+ nextId: maxNum + 1,
126
+ clusters: [...clusters]
127
+ .sort((a, b) => (a.cluster_order ?? Number.MAX_SAFE_INTEGER) - (b.cluster_order ?? Number.MAX_SAFE_INTEGER)
128
+ || handleNumber(a.handle) - handleNumber(b.handle))
129
+ .map((c) => ({ handle: c.handle, name: c.title, theme: c.body ?? '', order: c.cluster_order ?? null })),
130
+ };
131
+ }
132
+
133
+ /**
134
+ * One record → the client shape, resolving its cluster title from the provider.
135
+ *
136
+ * The convenience form for a mutation response, which has one record and no
137
+ * cluster list in hand. `listRecords` for clusters is a handful of small reads
138
+ * on the floor and one call on a remote provider, and getting the name right
139
+ * matters more: a response whose `cluster` is `CLUS-3` where the hydrate says
140
+ * `Umbrella A` makes an optimistic client redraw the idea into a group that does
141
+ * not exist.
142
+ */
143
+ export async function toClientIdeaWith(provider, record) {
144
+ if (!record?.cluster) return toClientIdea(record);
145
+ return toClientIdea(record, clusterTitleMap(await provider.listRecords({ kind: KIND.CLUSTER })));
146
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * lib/fluid/import-ideabox.js — one-time markdown → fluid records import.
3
+ *
4
+ * COMP-PLAN-IDEA-UNIFY S2. `ONE-WAY-WRITES`: this is an import, executed once,
5
+ * NOT a sync. There is no reverse path and there must never be one — the
6
+ * ideabox↔vision-store fragmentation this epic exists to end was created by
7
+ * exactly that kind of two-way bridge. After the import, `ideabox.md` is a
8
+ * projection (see render-ideabox.js) and the records are canon.
9
+ *
10
+ * The import is deliberately conservative about identity:
11
+ * - existing `IDEA-N` handles are carried over VERBATIM, because they are
12
+ * cited in docs, commits and conversation (the substrate ruling itself
13
+ * cites IDEA-20). A migration that renumbered would silently invalidate
14
+ * every one of those references.
15
+ * - every imported record is stamped `origin: import:ideabox`, so a migrated
16
+ * row stays distinguishable from a natively captured one for the rest of
17
+ * its life. Provenance is captured at write time and never retrofitted.
18
+ */
19
+
20
+ import { readFileSync } from 'node:fs';
21
+
22
+ import { parseIdeabox } from '../ideabox.js';
23
+ import { toRecordTimestamp } from './ideabox-dates.js';
24
+ import { KIND } from './provider.js';
25
+
26
+ /** Markdown status token → canonical fluid status. */
27
+ const STATUS_MAP = Object.freeze({
28
+ NEW: 'new',
29
+ DISCUSSING: 'discussing',
30
+ PROMOTED: 'promoted',
31
+ KILLED: 'killed',
32
+ });
33
+
34
+ /**
35
+ * The ideabox writes free-form status values (`RE-AIMED (2026-07-21)`,
36
+ * `PROMOTED (→ FEAT-1)`). Map on the leading token and keep the full original
37
+ * so nothing is silently normalized away.
38
+ */
39
+ function toStatus(raw) {
40
+ const token = String(raw ?? '').trim().split(/[\s(]/)[0].toUpperCase();
41
+ return STATUS_MAP[token] ?? 'new';
42
+ }
43
+
44
+ function toPriority(raw) {
45
+ const p = String(raw ?? '').trim();
46
+ return /^P[012]$/.test(p) ? p : null;
47
+ }
48
+
49
+ /**
50
+ * Import an ideabox markdown document into a fluid provider.
51
+ *
52
+ * @param {import('./provider.js').FluidProvider} provider
53
+ * @param {object} opts
54
+ * @param {string} [opts.markdown] document contents
55
+ * @param {string} [opts.path] read the document from here instead
56
+ * @param {boolean} [opts.dryRun] compute the plan without writing
57
+ * @returns {Promise<{imported: string[], skipped: string[], clusters: string[], alreadyImported: boolean}>}
58
+ */
59
+ export async function importIdeabox(provider, { markdown, path, dryRun = false } = {}) {
60
+ const source = markdown ?? readFileSync(path, 'utf8');
61
+ const parsed = parseIdeabox(source);
62
+
63
+ const provenance = { origin: 'import:ideabox' };
64
+
65
+ // Idempotence: a handle already present is left ALONE, not overwritten.
66
+ // Re-running must not clobber edits made through the tools after the first
67
+ // import, and must not throw on the handle-already-issued guard.
68
+ const existing = new Set((await provider.listRecords()).map((r) => r.handle));
69
+
70
+ const imported = [];
71
+ const skipped = [];
72
+ const clusterHandles = [];
73
+
74
+ // ---- clusters first: members reference them by handle --------------------
75
+ const clusterHandleByName = new Map();
76
+ for (const cluster of parsed.clusters ?? []) {
77
+ const known = (await provider.listRecords({ kind: KIND.CLUSTER }))
78
+ .find((r) => r.title === cluster.name);
79
+ if (known) {
80
+ clusterHandleByName.set(cluster.name, known.handle);
81
+ skipped.push(known.handle);
82
+ continue;
83
+ }
84
+ if (dryRun) {
85
+ clusterHandleByName.set(cluster.name, `(new cluster) ${cluster.name}`);
86
+ continue;
87
+ }
88
+ const rec = await provider.createRecord({
89
+ kind: KIND.CLUSTER,
90
+ title: cluster.name,
91
+ // The umbrella's hand-authored Theme paragraph. This is the field the
92
+ // whole cluster-as-record decision exists for.
93
+ body: cluster.theme ?? '',
94
+ cluster_order: cluster.order,
95
+ provenance,
96
+ // See below — the import is the one caller that must survive a rerun.
97
+ reclaimAborted: true,
98
+ });
99
+ clusterHandleByName.set(cluster.name, rec.handle);
100
+ clusterHandles.push(rec.handle);
101
+ imported.push(rec.handle);
102
+ }
103
+
104
+ // ---- ideas ---------------------------------------------------------------
105
+ const all = [
106
+ ...(parsed.ideas ?? []).map((i) => ({ idea: i, killed: false })),
107
+ ...(parsed.killed ?? []).map((i) => ({ idea: i, killed: true })),
108
+ ];
109
+
110
+ for (const { idea, killed } of all) {
111
+ if (existing.has(idea.id)) {
112
+ skipped.push(idea.id);
113
+ continue;
114
+ }
115
+ const clusterHandle = idea.cluster ? clusterHandleByName.get(idea.cluster) ?? null : null;
116
+ const clusterOrder = idea.cluster
117
+ ? (parsed.clusters ?? []).find((c) => c.name === idea.cluster)?.order ?? null
118
+ : null;
119
+
120
+ const record = {
121
+ kind: KIND.IDEA,
122
+ // Verbatim. The whole point of the caller-supplied handle path.
123
+ handle: idea.id,
124
+ title: idea.title,
125
+ body: idea.description ?? '',
126
+ status: killed ? 'killed' : toStatus(idea.status),
127
+ // Keep the author's token when the canonical enum cannot hold it, so a
128
+ // closed enum does not quietly flatten `RE-AIMED (2026-07-21)` to `NEW`.
129
+ status_label: STATUS_MAP[String(idea.status ?? '').trim().toUpperCase()]
130
+ ? null
131
+ : (idea.status || null),
132
+ priority: toPriority(idea.priority),
133
+ // Carried, not dropped. `parseIdeabox` already validates both against
134
+ // their enums and yields null otherwise (`lib/ideabox.js:304-311`), so
135
+ // there is nothing to re-check here — but omitting them is not a harmless
136
+ // gap. This function is the first-use migration gate every upgrading
137
+ // install runs, and the render that follows it rewrites the markdown from
138
+ // the records. A dropped field is therefore deleted from the user's file
139
+ // on upgrade, silently, with no way back. No idea in THIS repo carries
140
+ // either, which is exactly why it went unnoticed.
141
+ effort: idea.effort ?? null,
142
+ impact: idea.impact ?? null,
143
+ cluster: clusterHandle,
144
+ cluster_order: clusterOrder,
145
+ tags: idea.tags ?? [],
146
+ source: idea.source || null,
147
+ links: idea.mapsTo ? [{ type: 'maps_to', target: idea.mapsTo }] : [],
148
+ killed: (killed || idea.killedReason)
149
+ ? {
150
+ at: toRecordTimestamp(idea.killedDate),
151
+ reason: idea.killedReason || 'reason not recorded in markdown',
152
+ }
153
+ : null,
154
+ discussion: (idea.discussion ?? []).map((d) => ({
155
+ at: toRecordTimestamp(d.date),
156
+ text: d.text ?? '',
157
+ author: d.author ?? null,
158
+ })),
159
+ provenance,
160
+ };
161
+
162
+ if (dryRun) {
163
+ imported.push(idea.id);
164
+ continue;
165
+ }
166
+ // `reclaimAborted` makes the import RESTARTABLE. Creation burns the handle
167
+ // before writing the record, and this loop skips only handles with a live
168
+ // record — so a crash between those two steps leaves a handle that is
169
+ // issued, absent, and permanently un-creatable. Without this flag the
170
+ // one-time migration of a project's entire idea corpus cannot be rerun
171
+ // after a partial failure, which is the failure it is most likely to have.
172
+ // Narrow by construction: the provider reclaims ONLY a handle that was
173
+ // never live and never deleted, and no other caller passes this.
174
+ await provider.createRecord({ ...record, reclaimAborted: true });
175
+ imported.push(idea.id);
176
+ }
177
+
178
+ return {
179
+ imported,
180
+ skipped,
181
+ clusters: clusterHandles,
182
+ // True when the document had content and every handle in it was already
183
+ // present — i.e. this was a re-run, not a first import.
184
+ alreadyImported: imported.length === 0 && skipped.length > 0,
185
+ };
186
+ }