@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,277 @@
1
+ /**
2
+ * lib/judgment-decision-write.js — the judgment write path into SmartMemory
3
+ * (GOV-COMPOSE-SEAM-1 step 1 `canon-on-decisions`, phase P2).
4
+ *
5
+ * FAIL-CLOSED, unlike `smartmemory-ingest.js`.
6
+ *
7
+ * Ingest is fire-and-forget on purpose: a dropped feature event costs an
8
+ * analytics row, and the local durable write already happened. A dropped
9
+ * DECISION is different — under D1 the SmartMemory record is the canon, so a
10
+ * silently dropped write leaves a build governed by a rule set that is missing
11
+ * the thing just decided. Nobody sees a warning in a log. So a failed decision
12
+ * write throws, and the caller's tool call fails with it.
13
+ *
14
+ * Three guarantees, in the order they are enforced:
15
+ *
16
+ * 1. GATED — does nothing at all unless `smartmemory.enabled === true`.
17
+ * 2. IDEMPOTENT — a stable key per ledger entry, recorded in ONE local
18
+ * ledger shared by the live path and the backfill, and verified against
19
+ * the service before a skip. **There must be exactly one such ledger.**
20
+ * Two of them briefly existed (this sidecar and the backfill's own
21
+ * `.compose/data/judgment-migration-state.json`), keyed identically but
22
+ * read separately, so a decision written live and then backfilled would
23
+ * be written twice — the create endpoint has no server-side idempotency
24
+ * to catch it. Merged here 2026-08-22; the old file is adopted on first
25
+ * read and then unused.
26
+ * 3. VERIFIED — reads the decision back and checks the provenance survived,
27
+ * rather than trusting a 200. Against a service predating the 2026-08-22
28
+ * field addition, FastAPI silently ignores `source_type` /
29
+ * `context_snapshot`, so a 200 proves nothing about the thing D4 needs.
30
+ */
31
+
32
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
33
+ import { dirname, join, resolve } from 'node:path';
34
+
35
+ import { getSmartmemoryConfig } from './smartmemory-config.js';
36
+ import { createSmartmemoryClient } from './smartmemory-client.js';
37
+ import { syncManifest } from './judgment-attest.js';
38
+ import { atomicWrite } from './judgment/store/records.js';
39
+
40
+ /**
41
+ * Local idempotency ledger: `idempotency_key -> decision_id`.
42
+ *
43
+ * Lives beside the judgment records because it IS judgment provenance — which
44
+ * ledger entry became which decision. Also serves as the backfill's resume
45
+ * file (P3), which is why it is a file and not process memory.
46
+ */
47
+ export function sidecarPath(cwd) {
48
+ return join(cwd, 'docs', 'judgment', 'records', 'decision-ids.json');
49
+ }
50
+
51
+ /** Decisions created but never verified. Sits beside the sidecar; hand-cleared. */
52
+ export function orphanPath(cwd) {
53
+ return join(cwd, 'docs', 'judgment', 'records', 'decision-orphans.json');
54
+ }
55
+
56
+ /**
57
+ * Where the backfill's own resume file used to live before the two idempotency
58
+ * ledgers were merged (2026-08-22). Read once, so a migration already run
59
+ * against the old file is not repeated against the new one.
60
+ */
61
+ export function legacyStatePath(cwd) {
62
+ return join(cwd, '.compose', 'data', 'judgment-migration-state.json');
63
+ }
64
+
65
+ export function readSidecar(cwd) {
66
+ const path = sidecarPath(cwd);
67
+ if (!existsSync(path)) {
68
+ const legacy = legacyStatePath(cwd);
69
+ if (existsSync(legacy)) {
70
+ try {
71
+ const parsed = JSON.parse(readFileSync(legacy, 'utf8'));
72
+ // Old shape was { version, written: {key: id} }.
73
+ const written = parsed?.written;
74
+ if (written && typeof written === 'object') return { ...written };
75
+ } catch {
76
+ throw new Error(
77
+ `judgment-decision-write: the legacy resume file ${legacy} is unreadable. `
78
+ + 'Repair or delete it deliberately; ignoring it would re-write every decision '
79
+ + 'the earlier backfill already stored.',
80
+ );
81
+ }
82
+ }
83
+ return {};
84
+ }
85
+ try {
86
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
87
+ return parsed && typeof parsed === 'object' ? parsed : {};
88
+ } catch {
89
+ // A corrupt sidecar must not silently become "nothing was ever written" —
90
+ // that would re-write every decision on the next run.
91
+ throw new Error(
92
+ `judgment-decision-write: ${path} is unreadable. Repair or delete it deliberately; `
93
+ + 'treating it as empty would duplicate every decision already written.',
94
+ );
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Both ledgers live UNDER `docs/judgment/records/`, so every byte written here
100
+ * is attested: `recordFileSet` walks that tree wholesale and the drift detector
101
+ * reports anything the manifest does not remember. Writing without stamping
102
+ * leaves the canon in permanent `[added]` drift and blocks every later commit
103
+ * and push — which is exactly what the first real backfill did (2026-08-23).
104
+ *
105
+ * Stamping is the sanctioned writer path (`lib/judgment-writer.js` does the same
106
+ * after every publication), not a way around the guard: it records what THIS
107
+ * writer wrote, so a later hand-edit of the ledger still surfaces as drift.
108
+ */
109
+ function stampLedger(cwd, path) {
110
+ syncManifest(resolve(cwd), [path]);
111
+ }
112
+
113
+ function writeSidecar(cwd, map) {
114
+ const path = sidecarPath(cwd);
115
+ mkdirSync(dirname(path), { recursive: true });
116
+ atomicWrite(path, `${JSON.stringify(map, null, 2)}\n`);
117
+ stampLedger(cwd, path);
118
+ }
119
+
120
+ /**
121
+ * Did the provenance actually land?
122
+ *
123
+ * Checks the two fields the feature turns on, not the whole payload. A service
124
+ * that ignored them answers 200 and stores a decision whose conviction
125
+ * provenance is simply absent — indistinguishable, from the caller's side, from
126
+ * a successful write, which is the exact failure D4 exists to prevent.
127
+ */
128
+ /**
129
+ * Note a decision that was created but could not be verified.
130
+ *
131
+ * Kept beside the sidecar and NEVER auto-deleted. A create that we then failed
132
+ * to verify is exactly the case where we do not know what the service stored,
133
+ * and an automated delete there can destroy a good record on a bad diagnosis.
134
+ * Recording it means the next run can report the duplicate rather than the
135
+ * duplicate going unnoticed forever.
136
+ */
137
+ export function recordOrphan(cwd, entry) {
138
+ const p = orphanPath(cwd);
139
+ const existing = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : { version: 1, orphans: [] };
140
+ existing.orphans.push(entry);
141
+ mkdirSync(dirname(p), { recursive: true });
142
+ writeFileSync(p, `${JSON.stringify(existing, null, 2)}\n`);
143
+ stampLedger(cwd, p);
144
+ }
145
+
146
+ export function provenanceLanded(stored, sent) {
147
+ if (!stored) return false;
148
+ if (sent.source_type !== undefined && stored.source_type !== sent.source_type) return false;
149
+ if (sent.context_snapshot !== undefined) {
150
+ const got = stored.context_snapshot;
151
+ if (!got || typeof got !== 'object') return false;
152
+ // Key-wise, not deep-equal: the lifecycle owns three reserved slots inside
153
+ // context_snapshot and may add them (CORE-SUPERSEDE-NOTE-1), so a strict
154
+ // equality check would fail on a correct write.
155
+ for (const [k, v] of Object.entries(sent.context_snapshot)) {
156
+ // A sent `null` comes back ABSENT: the store drops null-valued keys from
157
+ // context_snapshot (measured 2026-08-22 — every key of a 13-key snapshot
158
+ // round-tripped except `ledger_anchor`, the only null). The store cannot
159
+ // represent the difference, so "sent null" and "stored absent" are the
160
+ // same fact and must not read as a lost write. Anything else still has
161
+ // to match exactly.
162
+ if (v === null && got[k] === undefined) continue;
163
+ if (JSON.stringify(got[k]) !== JSON.stringify(v)) return false;
164
+ }
165
+ }
166
+ return true;
167
+ }
168
+
169
+ /**
170
+ * Write one mapped decision, once.
171
+ *
172
+ * @param {string} cwd
173
+ * @param {object} decision payload from `ledgerEntryToDecision`
174
+ * @param {{client?: object, config?: object}} [deps] injection seam for tests
175
+ * @returns {Promise<{decision_id: string, skipped: boolean}|null>} null when the
176
+ * coupling is off — the ONLY silent no-op, and it is a configuration state,
177
+ * not a failure.
178
+ */
179
+ export async function writeJudgmentDecision(cwd, decision, deps = {}) {
180
+ const cfg = deps.config ?? getSmartmemoryConfig(cwd);
181
+ if (cfg.enabled !== true) return null;
182
+
183
+ const key = decision.idempotency_key;
184
+ if (!key) {
185
+ throw new Error('judgment-decision-write: decision has no idempotency_key; refusing to write');
186
+ }
187
+
188
+ const sidecar = readSidecar(cwd);
189
+ const client = deps.client ?? createSmartmemoryClient(cfg);
190
+
191
+ const known = sidecar[key];
192
+ if (known) {
193
+ // Idempotency is VERIFIED, not assumed. A key in the local ledger only
194
+ // counts once the decision is confirmed still present server-side —
195
+ // otherwise a ledger that has drifted from the service (restored backup,
196
+ // deleted decision, wrong workspace) silently skips a write that never
197
+ // landed, and the gap is invisible forever after.
198
+ const live = await client.getDecision(known);
199
+ if (live) return { decision_id: known, skipped: true };
200
+ process.stderr.write(
201
+ `[judgment-decision-write] ledger claimed ${key} -> ${known} but the service has no `
202
+ + 'such decision; rewriting.\n',
203
+ );
204
+ }
205
+
206
+ // `idempotency_key` and `supersedes_slug` are mapper-internal and are not
207
+ // fields on the create contract. The key travels inside context_snapshot so
208
+ // the remote record can be reconciled against the sidecar if they diverge.
209
+ const { idempotency_key: _k, supersedes_slug: _s, status: intendedStatus, ...rest } = decision;
210
+
211
+ // `status` is NOT a field on the create contract, and there is no route that
212
+ // can write a decision with both a non-active lifecycle state and its
213
+ // provenance: `/decisions/create` has no `status`, and `/decisions/pending/create`
214
+ // accepts no `source_type`, `context_snapshot`, `confidence` or `rationale`.
215
+ // A `pending` decision therefore lands `active` whichever route is used, and
216
+ // the choice is between a wrong status and lost provenance.
217
+ //
218
+ // Provenance wins, and the divergence is made LOUD rather than dropped: the
219
+ // intent is recorded on the record itself so it is visible to anyone reading
220
+ // the decision, and warned once per write so it is visible to whoever ran the
221
+ // migration. Closing it needs a service change (a `status` on create, or the
222
+ // pending route accepting provenance) — see the P3 migration report.
223
+ const statusDiverged = intendedStatus !== undefined && intendedStatus !== 'active';
224
+ if (statusDiverged) {
225
+ process.stderr.write(
226
+ `[judgment-decision-write] ${key}: intended status "${intendedStatus}" cannot be written — `
227
+ + 'no route accepts a lifecycle state together with provenance. Landing as "active" with '
228
+ + 'intended_status recorded on the record.\n',
229
+ );
230
+ }
231
+
232
+ const payload = {
233
+ ...rest,
234
+ context_snapshot: {
235
+ ...(decision.context_snapshot ?? {}),
236
+ idempotency_key: key,
237
+ ...(statusDiverged ? { intended_status: intendedStatus, status_diverged: true } : {}),
238
+ },
239
+ };
240
+
241
+ const created = await client.createDecision(payload);
242
+ const stored = await client.getDecision(created.decision_id);
243
+
244
+ if (!provenanceLanded(stored, payload)) {
245
+ // The decision EXISTS server-side at this point and is not going to be
246
+ // recorded in the sidecar, so a later re-run would write a second copy of
247
+ // the same ledger entry. Record it as an orphan so the next run reports it
248
+ // instead of silently duplicating. Not auto-deleted: deleting is
249
+ // irreversible and this path fires precisely when we do not understand
250
+ // what the service did with the write.
251
+ recordOrphan(cwd, {
252
+ key,
253
+ decision_id: created.decision_id,
254
+ at: stored ? 'provenance-mismatch' : 'read-back-empty',
255
+ });
256
+ throw new Error(
257
+ `judgment-decision-write: ${created.decision_id} was created but its provenance did not land. `
258
+ + 'The service is probably older than the 2026-08-22 source_type/context_snapshot change, '
259
+ + 'which FastAPI ignores silently. Failing rather than recording a decision whose '
260
+ + 'conviction provenance is absent. The created decision is recorded as an orphan; '
261
+ + 'delete it by hand once the cause is understood.',
262
+ );
263
+ }
264
+
265
+ // Remote first, sidecar second. The reverse would leave a phantom entry when
266
+ // the write fails, permanently skipping a decision that was never stored.
267
+ //
268
+ // KNOWN WINDOW: a crash between the two duplicates this one decision on the
269
+ // next run. Stated rather than hidden — closing it needs a server-side
270
+ // idempotency key, which the create contract does not have. The remote record
271
+ // carries `context_snapshot.idempotency_key`, so a duplicate is detectable
272
+ // and repairable after the fact.
273
+ sidecar[key] = created.decision_id;
274
+ writeSidecar(cwd, sidecar);
275
+
276
+ return { decision_id: created.decision_id, skipped: false };
277
+ }