@smartmemory/compose 0.3.7 → 0.4.0

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 (217) hide show
  1. package/.claude/skills/compose/SKILL.md +12 -3
  2. package/.compose-deps.json +51 -25
  3. package/README.md +79 -7
  4. package/bin/compose.js +495 -360
  5. package/bin/judgment-migrate.js +387 -0
  6. package/contracts/comp-obs-contract.schema.json +9 -3
  7. package/contracts/fluid-record.schema.json +209 -0
  8. package/contracts/lifecycle-backfill.schema.json +322 -0
  9. package/dist/assets/App-Z4MU-H_F.js +916 -0
  10. package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
  11. package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
  12. package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
  13. package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
  14. package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
  15. package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  16. package/dist/assets/channel-SnZzzh7k.js +1 -0
  17. package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  18. package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
  19. package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  20. package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  21. package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  22. package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
  23. package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  24. package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  25. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  26. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  27. package/dist/assets/clone-DgklGjHm.js +1 -0
  28. package/dist/assets/{cose-bilkent-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  29. package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
  30. package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
  31. package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
  32. package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
  33. package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
  34. package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
  35. package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
  36. package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
  37. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
  38. package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
  39. package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
  40. package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
  41. package/dist/assets/index-yyrA5OZd.css +1 -0
  42. package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  45. package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
  46. package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
  47. package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
  48. package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
  49. package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
  50. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  51. package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
  52. package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
  53. package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
  54. package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
  55. package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
  56. package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
  57. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  58. package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
  59. package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
  60. package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
  61. package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
  62. package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
  63. package/dist/index.html +3 -3
  64. package/lib/agent-string.js +7 -5
  65. package/lib/append-integrity.js +81 -0
  66. package/lib/backfill-evidence.js +109 -0
  67. package/lib/bug-escalation.js +9 -0
  68. package/lib/build-stream-schema.js +3 -1
  69. package/lib/build-stream-writer.js +25 -0
  70. package/lib/build.js +874 -170
  71. package/lib/canon-guard.js +28 -6
  72. package/lib/canon-override.js +196 -0
  73. package/lib/canon-registry.js +104 -0
  74. package/lib/cli-commands.js +144 -0
  75. package/lib/codex-preflight.js +26 -13
  76. package/lib/colleague/context.js +215 -0
  77. package/lib/colleague/writeback.js +95 -0
  78. package/lib/completion-gate.js +1421 -0
  79. package/lib/completion-writer.js +47 -47
  80. package/lib/consumer-fanout.js +105 -11
  81. package/lib/coverage-gate.js +200 -0
  82. package/lib/deps.js +164 -7
  83. package/lib/dir-lock.js +170 -0
  84. package/lib/dispatch-ledger.js +3 -3
  85. package/lib/feature-json.js +1 -1
  86. package/lib/feature-reconciler.js +8 -0
  87. package/lib/feature-validator.js +64 -1
  88. package/lib/feature-writer.js +57 -2
  89. package/lib/fluid/factory.js +167 -0
  90. package/lib/fluid/ideabox-dates.js +73 -0
  91. package/lib/fluid/ideabox-migrate.js +154 -0
  92. package/lib/fluid/ideabox-ops.js +585 -0
  93. package/lib/fluid/ideabox-view.js +146 -0
  94. package/lib/fluid/import-ideabox.js +186 -0
  95. package/lib/fluid/local-provider.js +606 -0
  96. package/lib/fluid/provider.js +684 -0
  97. package/lib/fluid/record-shape.js +214 -0
  98. package/lib/fluid/record-store.js +328 -0
  99. package/lib/fluid/render-ideabox.js +261 -0
  100. package/lib/fluid/schema.js +40 -0
  101. package/lib/fluid/smartmemory-provider.js +1695 -0
  102. package/lib/gsd.js +63 -23
  103. package/lib/guard-cli.js +175 -0
  104. package/lib/guard-custody.js +141 -0
  105. package/lib/guard-descriptors.js +530 -0
  106. package/lib/guard-enrol.js +254 -0
  107. package/lib/health-score.js +1 -1
  108. package/lib/ideabox-cli.js +315 -0
  109. package/lib/ideabox.js +121 -21
  110. package/lib/judgment/store/index.js +9 -1
  111. package/lib/judgment/store/records.js +1 -1
  112. package/lib/judgment/trace.js +380 -0
  113. package/lib/judgment-decision-write.js +277 -0
  114. package/lib/judgment-decisions.js +466 -0
  115. package/lib/judgment-gen.js +5 -1
  116. package/lib/judgment-writer.js +56 -2
  117. package/lib/lifecycle-modes.js +4 -4
  118. package/lib/lineage.js +400 -0
  119. package/lib/local-claude-connector.js +52 -1
  120. package/lib/maya-client.js +302 -0
  121. package/lib/maya-config.js +53 -0
  122. package/lib/maya-identity.js +283 -0
  123. package/lib/migrate-anon.js +5 -0
  124. package/lib/migrate-roadmap.js +15 -0
  125. package/lib/new.js +13 -1
  126. package/lib/pipeline-compat.js +104 -0
  127. package/lib/policy-catalog.js +295 -0
  128. package/lib/policy-check.js +0 -0
  129. package/lib/process-termination.js +98 -0
  130. package/lib/resolve-workspace.js +5 -1
  131. package/lib/result-normalizer.js +396 -199
  132. package/lib/roadmap-errors.js +65 -0
  133. package/lib/roadmap-preservers.js +24 -4
  134. package/lib/roadmap-residue.js +299 -0
  135. package/lib/smartmemory-client.js +614 -78
  136. package/lib/smartmemory-config.js +54 -0
  137. package/lib/smartmemory-ingest.js +19 -2
  138. package/lib/step-prompt.js +7 -6
  139. package/lib/stratum-engine.js +53 -4
  140. package/lib/stratum-mcp-client.js +271 -36
  141. package/lib/test-bootstrap.js +31 -0
  142. package/lib/tool-inventory.js +122 -0
  143. package/lib/version-check.js +91 -19
  144. package/lib/vision-writer.js +88 -1
  145. package/package.json +7 -6
  146. package/pipelines/bug-fix.stratum.yaml +205 -211
  147. package/pipelines/build-quick.profiles.json +12 -0
  148. package/pipelines/build-quick.stratum.yaml +263 -350
  149. package/pipelines/content.stratum.yaml +81 -77
  150. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  151. package/pipelines/plan.stratum.yaml +76 -86
  152. package/pipelines/refactor.stratum.yaml +125 -125
  153. package/pipelines/research.stratum.yaml +56 -58
  154. package/pipelines/review-fix.profiles.json +6 -0
  155. package/pipelines/review-fix.stratum.yaml +110 -83
  156. package/presets/team-feature.profiles.json +6 -0
  157. package/presets/team-feature.stratum.yaml +93 -66
  158. package/presets/team-research.profiles.json +6 -0
  159. package/presets/team-research.stratum.yaml +89 -80
  160. package/presets/team-review.profiles.json +8 -0
  161. package/presets/team-review.stratum.yaml +98 -80
  162. package/scripts/cost-census.mjs +70 -0
  163. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  164. package/server/agent-health.js +22 -0
  165. package/server/agent-hooks.js +14 -1
  166. package/server/agent-server.js +5 -248
  167. package/server/agent-spawn.js +3 -4
  168. package/server/agent-workspace.js +294 -0
  169. package/server/build-routes.js +6 -5
  170. package/server/build-stream-bridge.js +53 -0
  171. package/server/cc-session-watcher.js +4 -1
  172. package/server/coalescing-buffer.js +7 -1
  173. package/server/completion-projection.js +228 -0
  174. package/server/compose-mcp-tools.js +109 -23
  175. package/server/compose-mcp.js +88 -882
  176. package/server/decision-event-emit.js +41 -2
  177. package/server/decision-event-id.js +17 -0
  178. package/server/decision-events-snapshot.js +3 -0
  179. package/server/design-routes.js +14 -8
  180. package/server/feature-scan.js +76 -2
  181. package/server/file-watcher.js +170 -21
  182. package/server/ideabox-routes.js +166 -224
  183. package/server/index.js +70 -100
  184. package/server/lifecycle-guard.js +240 -10
  185. package/server/lifecycle-phase-history.js +276 -0
  186. package/server/maya-routes.js +507 -0
  187. package/server/mcp-tool-defs.js +940 -0
  188. package/server/mcp-tool-policy.js +34 -2
  189. package/server/model-tiers.js +22 -5
  190. package/server/pipeline-routes.js +21 -11
  191. package/server/project-root.js +58 -19
  192. package/server/remote-utils.js +3 -1
  193. package/server/schema-validator.js +7 -1
  194. package/server/session-manager.js +5 -6
  195. package/server/session-routes.js +3 -1
  196. package/server/stratum-client.js +57 -10
  197. package/server/stratum-sync.js +6 -3
  198. package/server/summarizer.js +3 -4
  199. package/server/supervisor.js +0 -1
  200. package/server/vision-routes.js +208 -98
  201. package/server/vision-server.js +86 -23
  202. package/server/vision-store.js +60 -6
  203. package/server/vision-utils.js +3 -4
  204. package/server/workspace-activity.js +18 -0
  205. package/server/workspace-middleware.js +2 -2
  206. package/server/workspace-runtime.js +243 -0
  207. package/server/worktree-gc.js +1 -0
  208. package/dist/assets/App-PkZzHeMj.js +0 -894
  209. package/dist/assets/channel-qVK_qn4E.js +0 -1
  210. package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
  211. package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
  212. package/dist/assets/clone-Pu3RyLUh.js +0 -1
  213. package/dist/assets/index-LIwREYgH.css +0 -1
  214. package/dist/assets/mobile-BnXEOE3U.js +0 -17
  215. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
  216. package/lib/staleness.js +0 -87
  217. package/server/ideabox-cache.js +0 -77
@@ -0,0 +1,466 @@
1
+ /**
2
+ * lib/judgment-decisions.js — ledger event to SmartMemory decision mapping
3
+ * (GOV-COMPOSE-SEAM-1 step 1 `canon-on-decisions`, phase P1).
4
+ *
5
+ * Pure. No I/O, no clock, no SmartMemory client. The write path (P2) and the
6
+ * backfill (P3) both call these; the dry run (`bin/judgment-migrate.js`) calls
7
+ * them and writes nothing, which is what makes the value spike measurable
8
+ * before anything is committed.
9
+ *
10
+ * Design answers this file implements — see
11
+ * smart-memory-docs/docs/features/GOV-COMPOSE-SEAM-1/design.md:
12
+ * D1 SmartMemory owns; markdown is a projection.
13
+ * D3 kind mapping (below).
14
+ * D4 an inferred conviction may NOT produce the same confidence as a stated
15
+ * one, and the distinction survives onto `source_type`.
16
+ * D5 enforceable = names a step + names an observable + a build could
17
+ * violate it. Fixed in writing before the count was taken.
18
+ *
19
+ * P2.5 (D4 review gate) is applied by `applyConvictionReview`, from the owner
20
+ * verdicts in `docs/features/GOV-COMPOSE-SEAM-1/conviction-review.json`. The
21
+ * backfill must call it and must refuse any inferred conviction the file does
22
+ * not rule on.
23
+ */
24
+
25
+ import { createHash } from 'node:crypto';
26
+
27
+ /** Ledger kinds that become a SmartMemory decision at all (D3). */
28
+ export const DECISION_KINDS = new Set(['decide', 'kill', 'open', 'correct']);
29
+
30
+ /**
31
+ * Kinds that touch a decision without being one. `calibrate` amends an existing
32
+ * decision's confidence; the rest are annotation, routing or integrity events
33
+ * and must not inflate the decision store (D3).
34
+ */
35
+ export const NON_DECISION_KINDS = new Set(['note', 'escalate', 'override', 'attest', 'calibrate']);
36
+
37
+ /**
38
+ * Conviction to confidence.
39
+ *
40
+ * Two separate scales on purpose (D4). A conviction the owner stated is worth
41
+ * more than one an agent inferred from a transcript, and the gap has to be
42
+ * visible in the number as well as in `source_type` — otherwise a guess reads
43
+ * as an assertion the moment it lands in a bundle rule.
44
+ *
45
+ * An inferred `high` (0.55) sits BELOW a stated `medium` (0.6) deliberately: we
46
+ * would rather act on something the owner actually said with middling
47
+ * conviction than on something we decided he probably meant.
48
+ */
49
+ export const CONFIDENCE = {
50
+ stated: { high: 0.9, medium: 0.6, low: 0.35 },
51
+ inferred: { high: 0.55, medium: 0.4, low: 0.25 },
52
+ };
53
+
54
+ /** Confidence for a decision-shaped entry that carries no conviction at all. */
55
+ export const CONFIDENCE_UNSTATED = 0.5;
56
+
57
+ /**
58
+ * Stable identity for a ledger entry.
59
+ *
60
+ * The jsonl carries no id — identity is position plus content. Hashing
61
+ * (seq, kind, title, written_at) gives the backfill an idempotency key that
62
+ * survives a re-run without a schema change, and changes if the entry is
63
+ * rewritten, which is what we want: a rewritten entry is a different fact.
64
+ */
65
+ export function stableEntryKey(entry, seq) {
66
+ const material = [
67
+ String(seq),
68
+ entry.kind ?? '',
69
+ entry.title ?? '',
70
+ entry.provenance?.written_at ?? '',
71
+ ].join(' ');
72
+ return `compose-ledger-${createHash('sha256').update(material).digest('hex').slice(0, 16)}`;
73
+ }
74
+
75
+ /**
76
+ * Split a ledger title of the form `slug — prose` into its two halves.
77
+ * Both em dash and hyphen separators appear in the imported canon.
78
+ */
79
+ export function splitTitle(title = '') {
80
+ const m = title.match(/^([a-z0-9][a-z0-9-]*)\s+[—-]\s+([\s\S]*)$/);
81
+ if (!m) return { slug: null, statement: (title || '').trim() };
82
+ return { slug: m[1], statement: m[2].trim() };
83
+ }
84
+
85
+ /**
86
+ * `decide` to `policy` when the entry states a forward-going rule, else
87
+ * `choice`.
88
+ *
89
+ * This is the D3 split that feeds the spike, so it is deliberately narrow: a
90
+ * policy has to read as an instruction that outlives the moment. Anything
91
+ * ambiguous stays a `choice`, which counts for less. Erring the other way would
92
+ * be the motivated counting the plan warns about.
93
+ */
94
+ const POLICY_MARKERS = [
95
+ /\balways\b/i,
96
+ /\bnever\b/i,
97
+ /\bmust\b/i,
98
+ /\bfrom (?:here|now) on\b/i,
99
+ /\bgoing forward\b/i,
100
+ /\bby default\b/i,
101
+ /\bno new\b/i,
102
+ ];
103
+
104
+ export function decideSubtype(entry) {
105
+ const text = `${entry.title ?? ''}\n${entry.body ?? ''}`;
106
+ return POLICY_MARKERS.some((re) => re.test(text)) ? 'policy' : 'choice';
107
+ }
108
+
109
+ /**
110
+ * D5 — the enforceability test, applied as three independent signals.
111
+ *
112
+ * Returns the signals, not just a verdict, because the reported spike number is
113
+ * an ADJUDICATED count: this classifier proposes, a human confirms. A regex
114
+ * cannot tell "reviews run before merge" from "reviewing is how we work", and
115
+ * pretending otherwise would produce exactly the motivated number D5 exists to
116
+ * prevent. `verdict: 'candidate'` means "bring this one to the adjudication
117
+ * pass", never "counted".
118
+ */
119
+ const STEP_MARKERS = [
120
+ /\b(?:design|blueprint|implement|review|merge|commit|push|deploy|release|ingest|recall|migrate|build|test)\b/i,
121
+ /\bstep\b/i,
122
+ /\bgate\b/i,
123
+ /\bpipeline\b/i,
124
+ ];
125
+
126
+ const OBSERVABLE_MARKERS = [
127
+ /\bfiles?\b/i,
128
+ /\bstatus\b/i,
129
+ /\bcount\b/i,
130
+ /\bexit code\b/i,
131
+ /\btests?\b/i,
132
+ /\bimports?\b/i,
133
+ /\bcontract\b/i,
134
+ /\bschema\b/i,
135
+ /\bversion\b/i,
136
+ /\bcommit\b/i,
137
+ /\bfields?\b/i,
138
+ ];
139
+
140
+ /**
141
+ * A rule true by construction cannot be violated, so it is not enforceable —
142
+ * it is a description of how the system already works. These read as claims
143
+ * about identity or belief rather than constraints on a run.
144
+ */
145
+ const NON_VIOLABLE_MARKERS = [
146
+ /\bis what\b/i,
147
+ /\bthe product is\b/i,
148
+ /\bwe are\b/i,
149
+ /\bthe point is\b/i,
150
+ /\bframing\b/i,
151
+ ];
152
+
153
+ export function classifyEnforceable(entry) {
154
+ const text = `${entry.title ?? ''}\n${entry.body ?? ''}`;
155
+ const namesStep = STEP_MARKERS.some((re) => re.test(text));
156
+ const namesObservable = OBSERVABLE_MARKERS.some((re) => re.test(text));
157
+ const violable = !NON_VIOLABLE_MARKERS.some((re) => re.test(text));
158
+ const signals = { namesStep, namesObservable, violable };
159
+ const all = namesStep && namesObservable && violable;
160
+ return {
161
+ signals,
162
+ // Three-way on purpose. `historical` is a confident no; `candidate` is
163
+ // "all three signals fired, a human must confirm"; nothing here is ever a
164
+ // confident yes, because the yes is the judgement the spike is measuring.
165
+ verdict: all ? 'candidate' : 'historical',
166
+ adjudicated: null,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * One rejected alternative, as the single string the contract accepts.
172
+ *
173
+ * The ledger models a rejection as `{what, why}`; `rejected_alternatives` is
174
+ * `array<string>` end to end. The `why` is kept in the same string rather than
175
+ * dropped — a rejected option without its reason is the least useful half.
176
+ */
177
+ export function flattenRejected(r) {
178
+ if (typeof r === 'string') return r;
179
+ const what = (r?.what ?? '').trim();
180
+ const why = (r?.why ?? '').trim();
181
+ if (!what) return why;
182
+ return why ? `${what} — ${why}` : what;
183
+ }
184
+
185
+ /**
186
+ * Map one ledger event to a SmartMemory decision payload (D3).
187
+ *
188
+ * Returns `null` for kinds that are not decisions — callers filter, they do not
189
+ * branch on kind themselves, so the D3 table has exactly one implementation.
190
+ *
191
+ * `correct` maps to a supersede, which needs a target that only the backfill
192
+ * knows (the decision written for the entry it corrects). The payload therefore
193
+ * carries `supersedes_slug` and the caller resolves it; mapping it here would
194
+ * require I/O and this file stays pure.
195
+ */
196
+ export function ledgerEntryToDecision(entry, seq) {
197
+ if (!entry || typeof entry !== 'object') return null;
198
+ const kind = entry.kind;
199
+ if (!DECISION_KINDS.has(kind)) return null;
200
+
201
+ const { slug, statement } = splitTitle(entry.title ?? '');
202
+ const conviction = entry.conviction ?? null;
203
+ const source = conviction?.source === 'stated'
204
+ ? 'stated'
205
+ : conviction?.source === 'inferred' ? 'inferred' : null;
206
+ const level = conviction?.level ?? null;
207
+
208
+ const confidence = source && level && CONFIDENCE[source]?.[level] !== undefined
209
+ ? CONFIDENCE[source][level]
210
+ : CONFIDENCE_UNSTATED;
211
+
212
+ // D4: the guess/assertion distinction survives onto the decision itself, not
213
+ // only into the confidence number, so a bundle rule built on a guess is
214
+ // visibly built on a guess wherever it is read.
215
+ const sourceType = source === 'stated'
216
+ ? 'explicit'
217
+ : source === 'inferred' ? 'inferred' : 'imported';
218
+
219
+ const decision = {
220
+ idempotency_key: stableEntryKey(entry, seq),
221
+ content: statement,
222
+ decision_type: kind === 'decide' ? decideSubtype(entry) : 'choice',
223
+ confidence,
224
+ source_type: sourceType,
225
+ status: kind === 'open' ? 'pending' : 'active',
226
+ domain: 'compose',
227
+ tags: ['compose-judgment', `ledger:${kind}`, ...(slug ? [`slug:${slug}`] : [])],
228
+ rationale: entry.body ?? '',
229
+ rejected_alternatives: [],
230
+ context_snapshot: {
231
+ ledger_seq: seq,
232
+ ledger_kind: kind,
233
+ ledger_slug: slug,
234
+ ledger_anchor: entry.anchor ?? null,
235
+ // `refs` is a RENDERED field: judgment-gen's EVENT_DETAIL_KEYS prints it
236
+ // into LEDGER.md. Dropping it made the markdown unreproducible from the
237
+ // decision store — measured 2026-08-23, 14 of the 43 backfilled entries
238
+ // carry refs and every one of them lost its evidence pointers. It is the
239
+ // ONLY rendered detail key the mapper did not carry; `rejected` and
240
+ // `conviction` already travel, and no decision-shaped entry uses any of
241
+ // the other fourteen. Empty stays empty rather than `[]` so the snapshot
242
+ // does not grow a key for the 29 entries that have no refs.
243
+ ...(Array.isArray(entry.refs) && entry.refs.length ? { ledger_refs: entry.refs } : {}),
244
+ // The STRUCTURED rejected list, beside the flattened one.
245
+ //
246
+ // `rejected_alternatives` is `list[str]` on the wire, so `{what, why}` is
247
+ // joined with " — ". That join is not reversible: the separator also
248
+ // occurs INSIDE `what` (an em dash is ordinary prose), and splitting on
249
+ // the first or the last occurrence both guess wrong. Measured 2026-08-23:
250
+ // 8 of the 43 backfilled entries reconstruct to the wrong what/why split,
251
+ // which renders a different LEDGER.md line.
252
+ //
253
+ // `context_snapshot` has no such type constraint, so the original travels
254
+ // intact here while the flattened list stays where readers expect it.
255
+ // This is NOT the same data twice over: one is for a human reading a
256
+ // decision, the other is what the projection has to reproduce.
257
+ ...(Array.isArray(entry.rejected) && entry.rejected.length
258
+ ? { ledger_rejected: entry.rejected }
259
+ : {}),
260
+ written_at: entry.provenance?.written_at ?? null,
261
+ via: entry.provenance?.via ?? null,
262
+ actor: entry.provenance?.actor ?? null,
263
+ conviction,
264
+ // D4 review gate: an inferred conviction is not written at stated
265
+ // confidence until the owner has ruled on it. The backfill REFUSES on
266
+ // this flag rather than warning (no silent degradation).
267
+ conviction_review_required: source === 'inferred',
268
+ conviction_review: null,
269
+ },
270
+ };
271
+
272
+ // `rejected_alternatives` is `array<string>` on both the core manager and the
273
+ // HTTP contract — there is NO field for the reason. Flattening "what (why)"
274
+ // into one string is lossy-but-visible; dropping the why silently would be
275
+ // lossy-and-invisible, and the why is the half a reader actually needs.
276
+ // See docs/features/GOV-COMPOSE-SEAM-1/decision-create-contract.json.
277
+ if (Array.isArray(entry.rejected)) {
278
+ decision.rejected_alternatives = entry.rejected.map((r) => flattenRejected(r));
279
+ }
280
+
281
+ // A kill is a live decision NOT to do something, so it stays `active` with
282
+ // the killed option recorded as the rejected alternative (D3). Mapping it to
283
+ // `abandoned` would say the decision itself was dropped, which is the
284
+ // opposite of what a kill means.
285
+ if (kind === 'kill') {
286
+ decision.rejected_alternatives.unshift(
287
+ flattenRejected({ what: statement, why: entry.reason ?? entry.body ?? '' }),
288
+ );
289
+ decision.content = `Do not: ${statement}`;
290
+ }
291
+
292
+ if (kind === 'correct') {
293
+ decision.supersedes_slug = slug;
294
+ }
295
+
296
+ return decision;
297
+ }
298
+
299
+ /**
300
+ * The inverse of `ledgerEntryToDecision` (GOV-COMPOSE-SEAM-1 P4).
301
+ *
302
+ * D1 says SmartMemory owns the canon and the markdown is a generated
303
+ * projection. That is only true if a decision can be turned back into the
304
+ * ledger event `judgment-gen.js` renders — otherwise the markdown is still
305
+ * authoritative and D1 was never really settled.
306
+ *
307
+ * Everything this needs is read from `context_snapshot`, which is the only
308
+ * field on the create contract with no shape constraint. `content`, `rationale`
309
+ * and `rejected_alternatives` are for a human reading a decision; they are
310
+ * derived views (a kill's content is prefixed, its alternatives are flattened)
311
+ * and are deliberately NOT the reconstruction source.
312
+ *
313
+ * What this CANNOT recover, and why:
314
+ * - `provenance.session` is never sent forward, so it comes back `null`. It
315
+ * is `null` on every entry in the current ledger and does not render.
316
+ * - Non-decision kinds (`note`, `escalate`, `override`, `calibrate`,
317
+ * `attest`) were ruled out of the decision store by D3 and are not in
318
+ * SmartMemory at all. 73 of the 116 ledger entries are in that class, so
319
+ * LEDGER.md as a whole does NOT regenerate from decisions — only its
320
+ * decision-shaped events do. See the P4 section of the plan.
321
+ *
322
+ * @param {object} decision a decision as the service returns it
323
+ * @returns {{ seq: number|null, entry: object }|null}
324
+ */
325
+ export function decisionToLedgerEntry(decision) {
326
+ const cs = decision?.context_snapshot;
327
+ if (!cs || typeof cs !== 'object') return null;
328
+ const kind = cs.ledger_kind;
329
+ if (!DECISION_KINDS.has(kind)) return null;
330
+
331
+ const slug = cs.ledger_slug ?? null;
332
+ // A kill's content is stored prefixed so it reads as a decision NOT to do
333
+ // something; the ledger title carries the bare statement.
334
+ const statement = kind === 'kill'
335
+ ? String(decision.content ?? '').replace(/^Do not: /, '')
336
+ : String(decision.content ?? '');
337
+
338
+ const entry = {
339
+ provenance: {
340
+ actor: cs.actor ?? null,
341
+ session: null,
342
+ written_at: cs.written_at ?? null,
343
+ // `via` is omitted rather than null when absent: the store drops null
344
+ // values from context_snapshot, and the renderer prints a `· via x`
345
+ // suffix only for a truthy value, so a null key would be noise.
346
+ ...(cs.via ? { via: cs.via } : {}),
347
+ },
348
+ kind,
349
+ title: slug ? `${slug} — ${statement}` : statement,
350
+ body: decision.rationale ?? '',
351
+ };
352
+
353
+ if (cs.ledger_refs) entry.refs = cs.ledger_refs;
354
+ // The structured list, never the flattened one: `flattenRejected` is not
355
+ // reversible (the " — " join also occurs inside `what`).
356
+ if (cs.ledger_rejected) entry.rejected = cs.ledger_rejected;
357
+ if (cs.conviction) entry.conviction = cs.conviction;
358
+ if (cs.ledger_anchor) entry.anchor = cs.ledger_anchor;
359
+
360
+ return { seq: cs.ledger_seq ?? null, entry };
361
+ }
362
+
363
+ /** Read a whole ledger stream into mapped decisions plus the skipped tail. */
364
+ export function mapLedger(events) {
365
+ const decisions = [];
366
+ const skipped = [];
367
+ events.forEach((entry, i) => {
368
+ const seq = i + 1;
369
+ const mapped = ledgerEntryToDecision(entry, seq);
370
+ if (mapped) {
371
+ decisions.push({
372
+ seq,
373
+ entry,
374
+ decision: mapped,
375
+ enforceability: classifyEnforceable(entry),
376
+ });
377
+ } else {
378
+ skipped.push({ seq, kind: entry?.kind ?? null, title: entry?.title ?? '' });
379
+ }
380
+ });
381
+ return { decisions, skipped };
382
+ }
383
+
384
+ // ── P2.5: the owner's inferred-conviction verdicts (D4) ────────────────────
385
+
386
+ /**
387
+ * The three verdicts the owner ruled on 2026-08-22. The 15 inferred entries
388
+ * sorted cleanly by what evidence of the owner they actually contain, so they
389
+ * were ruled as three groups rather than one at a time.
390
+ *
391
+ * - `promoted` (group A) — the entry quotes the owner's own words. The
392
+ * `inferred` label was simply wrong: someone wrote down what he said and then
393
+ * filed the confidence as a guess. Migrates at the stated scale.
394
+ * - `choice_kept_strength_dropped` (group B) — the owner made the decision
395
+ * ("Decided (owner, elicited)") but the CONFIDENCE was read off behaviour:
396
+ * "without hedging", "quick, unhesitating yes", "no elaboration offered".
397
+ * The choice is kept and attributed; the agent's reading of its strength is
398
+ * discarded. A fast yes can mean confident or can mean bored, and treating
399
+ * the two as the same is the laundering D4 exists to stop.
400
+ * - `unrated` (group C) — no trace the owner was ever asked. Migrates so the
401
+ * history survives, but carrying an explicit "nobody has ruled on this"
402
+ * marker, and may not back an enforceable rule until someone does.
403
+ */
404
+ export const REVIEW_VERDICTS = new Set(['promoted', 'choice_kept_strength_dropped', 'unrated']);
405
+
406
+ /**
407
+ * `Decision.confidence` is a non-optional `float` upstream
408
+ * (smartmemory/managed/framework.py:377 — `default_confidence=0.8`), so an
409
+ * unrated conviction cannot be written as null. It takes the neutral value and
410
+ * carries `conviction_unrated: true` alongside, which is what downstream must
411
+ * branch on. Stated here because a 0.5 that means "nobody asked" and a 0.5 that
412
+ * means "middling" are different facts wearing the same number.
413
+ */
414
+ export function applyConvictionReview(decision, verdict) {
415
+ const snap = decision.context_snapshot;
416
+ if (!snap.conviction_review_required) return decision;
417
+
418
+ if (!verdict || !REVIEW_VERDICTS.has(verdict.verdict)) {
419
+ // Refuse, do not warn. An unreviewed inferred conviction written at stated
420
+ // confidence is exactly the failure the gate exists to prevent, and a
421
+ // warning in a backfill log is not a gate.
422
+ throw new Error(
423
+ `judgment-decisions: ledger seq ${snap.ledger_seq} carries an inferred conviction with no owner verdict. `
424
+ + 'Add one to docs/features/GOV-COMPOSE-SEAM-1/conviction-review.json before backfilling.',
425
+ );
426
+ }
427
+
428
+ const level = snap.conviction?.level ?? null;
429
+ snap.conviction_review = { ...verdict, applied: true };
430
+ snap.conviction_review_required = false;
431
+
432
+ if (verdict.verdict === 'promoted') {
433
+ decision.source_type = 'explicit';
434
+ decision.confidence = (level && CONFIDENCE.stated[level] !== undefined)
435
+ ? CONFIDENCE.stated[level]
436
+ : CONFIDENCE_UNSTATED;
437
+ return decision;
438
+ }
439
+
440
+ if (verdict.verdict === 'choice_kept_strength_dropped') {
441
+ // The DECISION was the owner's, so the source is explicit; only the
442
+ // strength was guessed, so the number goes back to neutral.
443
+ decision.source_type = 'explicit';
444
+ decision.confidence = CONFIDENCE_UNSTATED;
445
+ snap.conviction_strength_dropped = true;
446
+ return decision;
447
+ }
448
+
449
+ // unrated
450
+ decision.source_type = 'inferred';
451
+ decision.confidence = CONFIDENCE_UNSTATED;
452
+ snap.conviction_unrated = true;
453
+ snap.enforceable_eligible = false;
454
+ return decision;
455
+ }
456
+
457
+ /** Apply a whole verdict file to a mapped ledger. Throws on the first gap. */
458
+ export function applyReviewFile(mapped, reviewFile) {
459
+ const verdicts = reviewFile?.verdicts ?? {};
460
+ for (const d of mapped.decisions) {
461
+ if (d.decision.context_snapshot.conviction_review_required) {
462
+ applyConvictionReview(d.decision, verdicts[String(d.seq)]);
463
+ }
464
+ }
465
+ return mapped;
466
+ }
@@ -28,6 +28,7 @@ import {
28
28
  effectiveStore,
29
29
  goalCutoverComplete,
30
30
  } from './judgment/store/index.js';
31
+ import { buildSupersessionIndex } from './judgment/trace.js';
31
32
 
32
33
  function atomicWrite(path, content) {
33
34
  const tmp = `${path}.tmp.${process.pid}`;
@@ -58,10 +59,13 @@ function smartmemoryTeamId(cwd) {
58
59
  export function loadSnapshot(cwd) {
59
60
  const rawStore = createJudgmentStore(cwd);
60
61
  const store = effectiveStore(rawStore);
62
+ // Prebuilt index: derivePositionStatus otherwise rescans every other slug's
63
+ // chain per call, making this whole-store pass O(n^2) (COMP-JUDGMENT-PRECEDENT).
64
+ const supersession = buildSupersessionIndex(store);
61
65
  const positions = store.listPositionSlugs().map((slug) => ({
62
66
  slug,
63
67
  chain: store.readPositionChain(slug),
64
- status: store.derivePositionStatus(slug),
68
+ status: store.derivePositionStatus(slug, supersession),
65
69
  }));
66
70
  return {
67
71
  positions,
@@ -48,9 +48,12 @@ import {
48
48
  effectiveStore,
49
49
  goalCutoverComplete,
50
50
  } from './judgment/store/index.js';
51
+ import { buildSupersessionIndex, tracePosition } from './judgment/trace.js';
51
52
  import { syncManifest } from './judgment-attest.js';
52
53
  import { regenerateProjections } from './judgment-gen.js';
53
54
  import { getJudgmentValidator } from './judgment/schema.js';
55
+ import { ledgerEntryToDecision } from './judgment-decisions.js';
56
+ import { writeJudgmentDecision } from './judgment-decision-write.js';
54
57
  import {
55
58
  assertValidRecord,
56
59
  assertGrounding,
@@ -3263,7 +3266,7 @@ export async function judgmentLedgerAppend(cwd, args, internal = {}) {
3263
3266
  assertValidRecord('ledger_event', event);
3264
3267
  return event;
3265
3268
  },
3266
- execute: (event) => {
3269
+ execute: async (event) => {
3267
3270
  const store = createJudgmentStore(cwd);
3268
3271
 
3269
3272
  let spawned = null;
@@ -3313,6 +3316,32 @@ export async function judgmentLedgerAppend(cwd, args, internal = {}) {
3313
3316
  assertValidRecord('prediction', graded);
3314
3317
  }
3315
3318
 
3319
+ // GOV-COMPOSE-SEAM-1 `canon-on-decisions` P2 — the SmartMemory write.
3320
+ //
3321
+ // BEFORE the local commit, deliberately. Under D1 the SmartMemory
3322
+ // decision is the canon and the markdown is a projection, so the canon
3323
+ // is written first and the projection follows. It is also the only
3324
+ // ordering that fails safely: a remote failure throws and nothing local
3325
+ // is written, whereas committing locally first would leave a decision
3326
+ // that exists in the projection and nowhere else.
3327
+ //
3328
+ // The seq is computed here rather than taken from `appendLedgerEvent`
3329
+ // because the key needs it before the append. Safe: we hold the advisory
3330
+ // lock, so the length cannot move underneath us. If the local commit
3331
+ // below then fails, a retry recomputes the SAME seq, hits the sidecar,
3332
+ // skips the remote write and completes the local one.
3333
+ //
3334
+ // `writeJudgmentDecision` is FAIL-CLOSED and returns null only when the
3335
+ // coupling is disabled. A live entry carrying an inferred conviction is
3336
+ // written on the inferred confidence scale and flagged
3337
+ // `conviction_review_required` for a later ruling — the P2.5 gate blocks
3338
+ // the BACKFILL, not new entries, which would otherwise be unrecordable.
3339
+ const nextSeq = store.readLedgerEvents().length + 1;
3340
+ const mapped = ledgerEntryToDecision(event, nextSeq);
3341
+ if (mapped) {
3342
+ await writeJudgmentDecision(cwd, mapped);
3343
+ }
3344
+
3316
3345
  const undo = new UndoLog();
3317
3346
  let seq;
3318
3347
  commitWithProjections(cwd, undo, () => {
@@ -3345,18 +3374,43 @@ export async function judgmentLedgerAppend(cwd, args, internal = {}) {
3345
3374
  * recent ledger. Small result (AUDIT-19) — titles and refs, never full
3346
3375
  * document text. Replays pending intents first (reconciler-on-read).
3347
3376
  */
3377
+ /**
3378
+ * Causal ancestry for one position (COMP-JUDGMENT-PRECEDENT slice A).
3379
+ *
3380
+ * Read-only counterpart to getJudgmentState, which returns latest-only and so
3381
+ * cannot answer "what did this replace, and what did we believe then". Takes the
3382
+ * same lock + intent replay so the trace reflects committed state, not a
3383
+ * half-applied intent queue.
3384
+ */
3385
+ export async function getJudgmentTrace(cwd, slug) {
3386
+ if (typeof slug !== 'string' || slug.trim() === '') {
3387
+ throw typedError('JUDGMENT_INPUT', 'get_judgment_trace: slug is required');
3388
+ }
3389
+ const release = await acquireJudgmentLock(cwd);
3390
+ try {
3391
+ await replayIntentsLocked(cwd);
3392
+ const store = effectiveStore(createJudgmentStore(cwd));
3393
+ return tracePosition(store, slug.trim());
3394
+ } finally {
3395
+ release();
3396
+ }
3397
+ }
3398
+
3348
3399
  export async function getJudgmentState(cwd) {
3349
3400
  const release = await acquireJudgmentLock(cwd);
3350
3401
  try {
3351
3402
  const replay = await replayIntentsLocked(cwd);
3352
3403
  regenerateProjections(cwd);
3353
3404
  const store = effectiveStore(createJudgmentStore(cwd));
3405
+ // Prebuilt index — see COMP-JUDGMENT-PRECEDENT: without it this loop is
3406
+ // O(n^2) over the store.
3407
+ const supersession = buildSupersessionIndex(store);
3354
3408
  const positions = store.listPositionSlugs().map((slug) => {
3355
3409
  const latest = store.latestPositionRevision(slug);
3356
3410
  return {
3357
3411
  slug,
3358
3412
  ref: `${slug}#r${latest.rev}`,
3359
- status: store.derivePositionStatus(slug),
3413
+ status: store.derivePositionStatus(slug, supersession),
3360
3414
  conviction: latest.conviction?.level ?? null,
3361
3415
  };
3362
3416
  });
@@ -51,7 +51,7 @@ export const LIFECYCLE_MODES = {
51
51
  ship: [],
52
52
  },
53
53
  skippable: ['prd', 'architecture', 'report'],
54
- terminal: ['complete', 'killed'],
54
+ terminal: ['complete', 'killed', 'complete_backfilled'],
55
55
  genesis: 'explore_design',
56
56
  completablePhase: 'ship', // the phase that → complete (was hard-coded by name)
57
57
  phaseArtifacts: ['design.md', 'prd.md', 'architecture.md', 'blueprint.md', 'plan.md', 'report.md'],
@@ -92,7 +92,7 @@ export const LIFECYCLE_MODES = {
92
92
  ship: [],
93
93
  },
94
94
  skippable: [],
95
- terminal: ['complete', 'killed'],
95
+ terminal: ['complete', 'killed', 'complete_backfilled'],
96
96
  genesis: 'reproduce',
97
97
  completablePhase: 'ship',
98
98
  phaseArtifacts: [], // empty → assess uses the global default (preserves today's bug behavior)
@@ -121,7 +121,7 @@ export const LIFECYCLE_MODES = {
121
121
  ship: [],
122
122
  },
123
123
  skippable: [],
124
- terminal: ['complete', 'killed'],
124
+ terminal: ['complete', 'killed', 'complete_backfilled'],
125
125
  genesis: 'explore_design',
126
126
  completablePhase: 'ship',
127
127
  phaseArtifacts: ['design.md', 'plan.md'],
@@ -163,7 +163,7 @@ export const LIFECYCLE_MODES = {
163
163
  inconclusive: ['under_test', 'open'],
164
164
  },
165
165
  skippable: [],
166
- terminal: ['superseded', 'dissolved', 'complete', 'killed'],
166
+ terminal: ['superseded', 'dissolved', 'complete', 'killed', 'complete_backfilled'],
167
167
  genesis: 'open',
168
168
  completablePhase: 'resolved',
169
169
  phaseArtifacts: [],