@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,684 @@
1
+ /**
2
+ * lib/fluid/provider.js — the fluid-store provider seam.
3
+ *
4
+ * Owner ruling: `PROVIDER-SEAM` (docs/product/2026-07-20-what-to-build-vision.md
5
+ * §8k, owner-locked 2026-07-21). Canonical fluid records (ideas, positions,
6
+ * joints, decisions, ledger entries) live behind THIS interface. The floor is a
7
+ * zero-install local provider; SmartMemory is the reference, capability-rich
8
+ * provider.
9
+ *
10
+ * The interface is drawn at exactly three things:
11
+ * 1. typed record CRUD
12
+ * 2. lifecycle events
13
+ * 3. capability discovery
14
+ *
15
+ * and nothing else. Semantic machinery — recall, challenge, conviction/decay,
16
+ * calibration, contradiction — is deliberately NOT part of the interface. Those
17
+ * are capabilities that light up when the configured provider declares them.
18
+ *
19
+ * WHY THIS IS THE WHOLE POINT: abstracting semantics into the seam is the
20
+ * lowest-common-denominator failure, and the ruling prohibits it. A provider
21
+ * without a capability must LACK it visibly; nothing may fake it. Concretely,
22
+ * `recall()` on a provider that never declared RECALL must throw — it must not
23
+ * return `[]`. An empty array is indistinguishable from a real "nothing matched"
24
+ * answer, so returning one silently converts a missing capability into a wrong
25
+ * answer, which is the precise failure this seam exists to prevent.
26
+ *
27
+ * Contract: contracts/fluid-record.schema.json
28
+ * Pattern precedent: lib/tracker/provider.js (the tracker seam).
29
+ * Pattern REJECTED: lib/tracker/factory.js `withFallback` — see lib/fluid/factory.js.
30
+ */
31
+
32
+ /**
33
+ * Capabilities every provider must implement. These ARE the seam's contract.
34
+ */
35
+ export const STORAGE_CAP = Object.freeze({
36
+ RECORDS: 'RECORDS',
37
+ EVENTS: 'EVENTS',
38
+ LINKS: 'LINKS',
39
+ });
40
+
41
+ /**
42
+ * Capabilities a provider may declare. These are the reason the seam exists.
43
+ * Declared, never abstracted — the floor has none of them, and that is correct.
44
+ */
45
+ export const SEMANTIC_CAP = Object.freeze({
46
+ RECALL: 'RECALL',
47
+ CHALLENGE: 'CHALLENGE',
48
+ CONVICTION: 'CONVICTION',
49
+ CALIBRATION: 'CALIBRATION',
50
+ CONTRADICTION: 'CONTRADICTION',
51
+ });
52
+
53
+ export const CAP = Object.freeze({ ...STORAGE_CAP, ...SEMANTIC_CAP });
54
+
55
+ /**
56
+ * Bounds for `recall(query, {limit})`. On the seam, not on a provider, so two
57
+ * providers cannot disagree about what `limit: 0` or `limit: 5000` means.
58
+ *
59
+ * The clamp is load-bearing rather than decorative: a provider backed by an HTTP
60
+ * search API typically forwards this as a result count, and an unbounded value
61
+ * becomes an unbounded fetch plus a proportional ranking cost on the server.
62
+ */
63
+ export const RECALL_LIMIT_DEFAULT = 10;
64
+ export const RECALL_LIMIT_MIN = 1;
65
+ export const RECALL_LIMIT_MAX = 100;
66
+
67
+ /**
68
+ * Normalize a caller's `limit` to the seam's bounds. Never throws — recall is a
69
+ * discovery call, and answering sanely beats failing on a sloppy argument.
70
+ *
71
+ * The two ends are deliberately NOT symmetric:
72
+ *
73
+ * - **Too large is clamped.** `limit: 5000` is a legible intent — "give me
74
+ * lots" — so it is honoured up to the ceiling.
75
+ * - **Zero, negative and unusable take the DEFAULT, not the floor.** These carry
76
+ * no intent at all; they are almost always an uninitialised variable or a
77
+ * failed parse. Clamping `0` to `1` would answer a question nobody asked, and
78
+ * would do it silently — a single near-useless result looks like a real answer.
79
+ *
80
+ * @param {unknown} limit
81
+ * @returns {number}
82
+ */
83
+ export function normalizeRecallLimit(limit) {
84
+ const n = typeof limit === 'number' && Number.isFinite(limit) ? Math.floor(limit) : NaN;
85
+ if (Number.isNaN(n) || n < RECALL_LIMIT_MIN) return RECALL_LIMIT_DEFAULT;
86
+ return Math.min(n, RECALL_LIMIT_MAX);
87
+ }
88
+
89
+ /**
90
+ * One recall result.
91
+ *
92
+ * @typedef {object} RecallHit
93
+ * @property {string} handle
94
+ * The record's citation — and **the authority**. `record` below is a
95
+ * convenience payload; anything about to ACT on a result must re-read it with
96
+ * `getRecord(handle)`.
97
+ * @property {number|null} score
98
+ * The provider's relevance score, passed through untouched. `null` when the
99
+ * provider did not supply one — deliberately not `0`, which would sort as a
100
+ * real and terrible score rather than as "unknown".
101
+ * @property {object} record
102
+ * A hit-time snapshot of the record, valid against
103
+ * `#/definitions/record`. It is a SIBLING of `score`, never a carrier for it:
104
+ * the record contract is `additionalProperties: false`, so a score attached to
105
+ * the record object would produce something the contract rejects.
106
+ *
107
+ * **Current, not "as indexed".** A provider whose index lags its store may
108
+ * match on stale text while returning the current record. What can be stale is
109
+ * *why a hit surfaced and where it ranked* — never what you receive.
110
+ */
111
+
112
+ const SEMANTIC_CAP_VALUES = Object.freeze(new Set(Object.values(SEMANTIC_CAP)));
113
+ const STORAGE_CAP_VALUES = Object.freeze(new Set(Object.values(STORAGE_CAP)));
114
+
115
+ export function isSemanticCapability(cap) {
116
+ return SEMANTIC_CAP_VALUES.has(cap);
117
+ }
118
+
119
+ export function isStorageCapability(cap) {
120
+ return STORAGE_CAP_VALUES.has(cap);
121
+ }
122
+
123
+ /**
124
+ * How far a provider's mutation serialization actually reaches.
125
+ *
126
+ * Declared rather than assumed, because "is this store safe for two writers" has
127
+ * no single answer — it depends on the mechanism AND on how far the store is
128
+ * reachable. A filesystem mutex genuinely serializes every process on one
129
+ * machine and cannot serialize two machines; saying "locked" would be true and
130
+ * misleading in the same breath.
131
+ *
132
+ * Ordered weakest to strongest, and comparable via {@link mutationScopeAtLeast}.
133
+ */
134
+ export const MUTATION_SCOPE = Object.freeze({
135
+ /** Nothing is serialized. Concurrent writers can destroy each other's records. */
136
+ NONE: 'none',
137
+ /** Serialized within one process only — no help against a second CLI or server. */
138
+ PROCESS: 'process',
139
+ /** Serialized across every process on one machine. The floor (`lib/dir-lock.js`). */
140
+ MACHINE: 'machine',
141
+ /** Serialized across every machine reaching the store. Required of a shared store. */
142
+ CLUSTER: 'cluster',
143
+ });
144
+
145
+ const MUTATION_SCOPE_RANK = Object.freeze({
146
+ [MUTATION_SCOPE.NONE]: 0,
147
+ [MUTATION_SCOPE.PROCESS]: 1,
148
+ [MUTATION_SCOPE.MACHINE]: 2,
149
+ [MUTATION_SCOPE.CLUSTER]: 3,
150
+ });
151
+
152
+ /** True when `scope` is at least as strong as `required`. */
153
+ export function mutationScopeAtLeast(scope, required) {
154
+ return (MUTATION_SCOPE_RANK[scope] ?? -1) >= (MUTATION_SCOPE_RANK[required] ?? Infinity);
155
+ }
156
+
157
+ /** Record kinds the contract defines. A provider implements a subset. */
158
+ export const KIND = Object.freeze({
159
+ IDEA: 'idea',
160
+ POSITION: 'position',
161
+ JOINT: 'joint',
162
+ DECISION: 'decision',
163
+ THREAD: 'thread',
164
+ QUESTION: 'question',
165
+ // A grouping is a record, not a label on its members: an umbrella owns a
166
+ // hand-authored Theme paragraph, and the only place that could live on a
167
+ // member is duplicated across all of them.
168
+ CLUSTER: 'cluster',
169
+ });
170
+
171
+ /**
172
+ * The kinds `challenge()` accepts (COMP-FOH FOH-3). A seam-level contract, not a
173
+ * provider detail: only assertion-shaped kinds (a decision, an idea) can
174
+ * meaningfully contradict stored memory. The challenge endpoint runs its
175
+ * detection cascade directly with no `should_challenge` gate, so a non-assertional
176
+ * kind (thread/question/cluster) must be refused here rather than fed to it.
177
+ */
178
+ export const CHALLENGEABLE_KINDS = Object.freeze(new Set([KIND.DECISION, KIND.IDEA]));
179
+
180
+ /**
181
+ * The resolution strategies `resolveConflict()` accepts (COMP-FOH FOH-4). A
182
+ * seam-level contract like {@link CHALLENGEABLE_KINDS}: v1 allows ONLY
183
+ * `accept_new` — the one strategy with an observable effect at this seam (it
184
+ * decays the target's confidence). `keep_existing` is expressed by not calling
185
+ * resolve at all; `keep_both`/`defer` write server-side markers nothing in
186
+ * Compose reads (a dead capability, refused rather than shipped); `merge` has
187
+ * no deterministic server effect. The parameter stays explicit so the
188
+ * allowlist can widen later without a signature change.
189
+ *
190
+ * A frozen ARRAY, not a frozen Set: `Object.freeze` on a Set freezes its
191
+ * properties but not its entries — `.add()` would still succeed, and this is
192
+ * the autonomy gate on a destructive write, so it must be genuinely immutable.
193
+ */
194
+ export const CONVICTION_STRATEGIES = Object.freeze(['accept_new']);
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Errors
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /** Configuration is present but wrong. Never swallowed — see factory.js. */
201
+ export class FluidConfigError extends Error {
202
+ constructor(message, detail = {}) {
203
+ super(message);
204
+ this.name = 'FluidConfigError';
205
+ this.detail = detail;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * The configured provider does not offer the requested capability.
211
+ *
212
+ * This is the "visibly unavailable, never faked" mechanism in code form. It
213
+ * names both the capability and the provider so the surface above can render a
214
+ * funnel ("challenge: connect SmartMemory") rather than an empty state — the
215
+ * distinction `COLLEAGUE-ALL-IN` turns on.
216
+ */
217
+ export class FluidCapabilityUnavailable extends Error {
218
+ constructor(capability, providerName, detail = {}) {
219
+ super(
220
+ `fluid: capability ${capability} is not available on provider "${providerName}". ` +
221
+ `This capability is not emulated by design — connect a provider that declares it.`
222
+ );
223
+ this.name = 'FluidCapabilityUnavailable';
224
+ this.capability = capability;
225
+ this.provider = providerName;
226
+ this.detail = detail;
227
+ }
228
+ }
229
+
230
+ /** The provider does not implement this record kind. */
231
+ export class FluidKindUnsupported extends Error {
232
+ constructor(kind, providerName, supported = []) {
233
+ super(
234
+ `fluid: record kind "${kind}" is not supported by provider "${providerName}" ` +
235
+ `(supported: ${supported.length ? supported.join(', ') : 'none'})`
236
+ );
237
+ this.name = 'FluidKindUnsupported';
238
+ this.kind = kind;
239
+ this.provider = providerName;
240
+ this.supported = supported;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * A find-or-create matched more than one record.
246
+ *
247
+ * Distinct from "not found" because the caller's options are opposite: a miss is
248
+ * resolved by creating, an ambiguity can only be resolved by naming a handle.
249
+ * Auto-picking one would silently attach work to whichever duplicate sorted
250
+ * first.
251
+ */
252
+ export class FluidAmbiguousMatch extends Error {
253
+ constructor(kind, title, handles) {
254
+ super(
255
+ `fluid: "${title}" matches ${handles.length} ${kind} records ` +
256
+ `(${handles.join(', ')}). Name the handle instead.`
257
+ );
258
+ this.name = 'FluidAmbiguousMatch';
259
+ this.kind = kind;
260
+ this.title = title;
261
+ this.handles = handles;
262
+ }
263
+ }
264
+
265
+ /** A record was addressed that does not exist. */
266
+ export class FluidRecordNotFound extends Error {
267
+ constructor(handle, providerName) {
268
+ super(`fluid: no record with handle "${handle}" on provider "${providerName}"`);
269
+ this.name = 'FluidRecordNotFound';
270
+ this.handle = handle;
271
+ this.provider = providerName;
272
+ }
273
+ }
274
+
275
+ // -- resolution failures (COMP-FOH FOH-4) -----------------------------------
276
+ //
277
+ // Four distinct outcomes of the destructive resolve path, typed because the
278
+ // caller's next move is different for each — and because exactly ONE of them
279
+ // is safe to retry. Collapsing them into a generic error would force callers
280
+ // back to message-matching to decide whether retrying can double-decay.
281
+
282
+ /** The caller named a resolution strategy outside the v1 allowlist. Refused
283
+ * before any request is made. */
284
+ export class FluidInvalidStrategy extends Error {
285
+ constructor(strategy, allowed = []) {
286
+ super(
287
+ `fluid: resolution strategy "${strategy}" is not allowed ` +
288
+ `(v1 allows: ${allowed.join(', ') || 'none'})`
289
+ );
290
+ this.name = 'FluidInvalidStrategy';
291
+ this.strategy = strategy;
292
+ this.allowed = allowed;
293
+ }
294
+ }
295
+
296
+ /** The resolution's record pairing is invalid (e.g. target === source).
297
+ * A pre-mutation validation refusal — distinct from the post-mutation outcome
298
+ * classes below, which describe what a real attempt did. */
299
+ export class FluidInvalidTarget extends Error {
300
+ constructor(handle, reason) {
301
+ super(`fluid: invalid resolution target "${handle}" (${reason})`);
302
+ this.name = 'FluidInvalidTarget';
303
+ this.handle = handle;
304
+ this.reason = reason;
305
+ }
306
+ }
307
+
308
+ /** The resolve endpoint answered cleanly but persisted nothing. The ONLY
309
+ * retryable resolution failure: a clean response proves the server finished
310
+ * and did nothing, so trying again cannot double-apply. */
311
+ export class FluidResolutionNoOp extends Error {
312
+ constructor(handle) {
313
+ super(`fluid: resolution of "${handle}" persisted nothing (clean no-op; safe to retry)`);
314
+ this.name = 'FluidResolutionNoOp';
315
+ this.handle = handle;
316
+ this.retryable = true;
317
+ }
318
+ }
319
+
320
+ /** Another caller moved the target concurrently (the workspace lease covers
321
+ * Compose callers, not direct SmartMemory ones). NOT retryable — re-read the
322
+ * conviction and re-decide; the contradiction may already be resolved. */
323
+ export class FluidResolutionConflict extends Error {
324
+ constructor(handle) {
325
+ super(
326
+ `fluid: resolution of "${handle}" collided with a concurrent change — ` +
327
+ 're-read its conviction and re-decide before resolving again'
328
+ );
329
+ this.name = 'FluidResolutionConflict';
330
+ this.handle = handle;
331
+ this.retryable = false;
332
+ }
333
+ }
334
+
335
+ /** The outcome is unknowable: the transport failed in a way that does not
336
+ * prove the server's synchronous mutation stopped (timeout, proxy error,
337
+ * gateway 5xx), and reconciliation could not positively confirm the decay.
338
+ * Explicitly NOT retryable — the original mutation may still land, and a
339
+ * retry on top of it double-decays a near-irreversible value. */
340
+ export class FluidResolutionIndeterminate extends Error {
341
+ constructor(handle) {
342
+ super(
343
+ `fluid: resolution of "${handle}" has an unknowable outcome (ambiguous transport; ` +
344
+ 'the decay may still land). Do NOT retry; re-read its conviction later.'
345
+ );
346
+ this.name = 'FluidResolutionIndeterminate';
347
+ this.handle = handle;
348
+ this.retryable = false;
349
+ }
350
+ }
351
+
352
+ const NI = (m) => { throw new Error(`FluidProvider.${m}: not implemented`); };
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // The seam
356
+ // ---------------------------------------------------------------------------
357
+
358
+ export class FluidProvider {
359
+ /** Stable provider name, used in errors and diagnostics. */
360
+ name() { return NI('name'); }
361
+
362
+ /**
363
+ * The capabilities this provider actually offers.
364
+ * A provider MUST NOT declare a semantic capability it emulates.
365
+ * @returns {Set<string>}
366
+ */
367
+ capabilities() { return new Set(); }
368
+
369
+ /**
370
+ * Record kinds this provider implements.
371
+ * @returns {Set<string>}
372
+ */
373
+ supportedKinds() { return new Set(); }
374
+
375
+ /**
376
+ * How far this provider's mutation serialization reaches.
377
+ *
378
+ * **Defaults to `NONE`, deliberately.** A provider that has not thought about
379
+ * concurrency has not solved it, and the default that assumes the best is the
380
+ * one that produced this whole feature: S3b-1's second implementation looked
381
+ * complete precisely because nothing ever asked it this question.
382
+ *
383
+ * @returns {string} one of {@link MUTATION_SCOPE}
384
+ */
385
+ mutationScope() { return MUTATION_SCOPE.NONE; }
386
+
387
+ /**
388
+ * True when this provider's store can be reached from more than one machine.
389
+ *
390
+ * Paired with `mutationScope()` because neither answers the safety question
391
+ * alone: `MACHINE` scope is complete for a local directory and useless for a
392
+ * shared workspace. A provider that is `shared` and below `CLUSTER` scope has
393
+ * a real, silent data-loss hole, and that is the pair the factory warns on.
394
+ */
395
+ isShared() { return false; }
396
+
397
+ async init(_cwd, _config) { return this; }
398
+
399
+ async health() {
400
+ return {
401
+ ok: true,
402
+ provider: this.name?.() ?? 'base',
403
+ capabilities: [...this.capabilities()],
404
+ kinds: [...this.supportedKinds()],
405
+ };
406
+ }
407
+
408
+ // -- capability + kind guards ---------------------------------------------
409
+
410
+ /** True when the capability is declared. Callers use this to decide whether
411
+ * to OFFER a semantic affordance at all. */
412
+ has(cap) { return this.capabilities().has(cap); }
413
+
414
+ /**
415
+ * Assert a capability, or throw. Every semantic entry point calls this FIRST,
416
+ * before any work, so that a missing capability surfaces identically whether
417
+ * or not the underlying store happens to hold matching data.
418
+ */
419
+ require(cap) {
420
+ if (!this.has(cap)) throw new FluidCapabilityUnavailable(cap, this.name());
421
+ return true;
422
+ }
423
+
424
+ /** Assert this provider implements a record kind, or throw. */
425
+ requireKind(kind) {
426
+ if (!this.supportedKinds().has(kind)) {
427
+ throw new FluidKindUnsupported(kind, this.name(), [...this.supportedKinds()]);
428
+ }
429
+ return true;
430
+ }
431
+
432
+ // -- records (STORAGE_CAP.RECORDS) ----------------------------------------
433
+ //
434
+ // TWO OBLIGATIONS EVERY PROVIDER OWES, STATED HERE BECAUSE THEY ARE THE SEAM'S
435
+ // PROMISE AND NOT A PROVIDER'S CHOICE (COMP-FLUID-SEAM-GUARANTEES).
436
+ //
437
+ // Both were built in S3b-1 and both were built in ONE provider. The second
438
+ // implementation then satisfied this interface completely while having
439
+ // neither, and nothing failed — which is the whole argument for writing them
440
+ // down where a third implementation must read them. `record-shape.js` holds
441
+ // the same line for `normalizeRecord` and `UNPATCHABLE`.
442
+ //
443
+ // 1. **SERIALIZED MUTATION.** Every mutating method is atomic with respect to
444
+ // concurrent callers, INCLUDING callers in other processes and — for a
445
+ // store reachable from more than one machine — on other machines. Two
446
+ // failure modes ride on this, and the first is the loud one:
447
+ //
448
+ // - Handle allocation reads the maximum issued handle and adds one.
449
+ // Unserialized, N concurrent creates all allocate the same handle.
450
+ // Measured, not theorised: 3/3 rounds collided (2026-08-05). What the
451
+ // collision COSTS is the store's to decide, and the two providers
452
+ // diverge — the floor keys its file BY HANDLE, so last-writer-wins
453
+ // destroys N-1 records; SmartMemory writes by `item_id`, so all N
454
+ // persist and N-1 are silently ORPHANED behind a handle that resolves
455
+ // to one of them. Collision is the invariant here; loss is not.
456
+ // - `updateRecord`, `appendDiscussion`, `addLink` and `removeLink` are
457
+ // read-modify-write against one record. Unserialized, the later write
458
+ // erases the earlier one.
459
+ //
460
+ // The mechanism is the provider's to choose and MUST match its reach: the
461
+ // floor uses a filesystem mutex (`lib/dir-lock.js`), which is correct for
462
+ // a local directory and definitionally wrong for a store shared across
463
+ // machines — being shared is the entire reason to use such a store.
464
+ //
465
+ // A provider that cannot serialize must SAY SO rather than approximate
466
+ // it — see `mutationScope()`. Declared, never faked, exactly as with the
467
+ // semantic capabilities: a silent approximation of atomicity is worse
468
+ // than its absence, because the caller believes it is safe.
469
+ //
470
+ // 2. **`reclaimAborted` ON `createRecord`.** Creation burns a handle before
471
+ // the record exists (the tombstone ordering, which is the only safe
472
+ // direction — see the floor's `_createRecordLocked`). A crash between the
473
+ // two steps therefore leaves a handle that is issued, has no record, and
474
+ // is permanently un-creatable.
475
+ //
476
+ // `createRecord({ handle, reclaimAborted: true })` MUST reclaim exactly
477
+ // that state: a handle that was issued, never became live, and was never
478
+ // `deleted`. It MUST NOT reclaim a handle whose record exists or was
479
+ // deleted — a retired handle stays retired, because handles are external
480
+ // citations and reissuing one repoints them at a different record.
481
+ //
482
+ // This exists so the one-time import is RESTARTABLE. A migration of a
483
+ // project's entire corpus that cannot be re-run after a partial failure
484
+ // is a migration that fails permanently on its most likely failure.
485
+ //
486
+ // `test/fluid-provider-conformance.test.js` asserts both against every
487
+ // provider, so a new implementation cannot satisfy this interface while
488
+ // missing them the way the second one did.
489
+
490
+ async getRecord(_handle) { return NI('getRecord'); }
491
+ async listRecords(_filter) { return NI('listRecords'); }
492
+ async createRecord(_input) { return NI('createRecord'); }
493
+ async updateRecord(_handle, _patch) { return NI('updateRecord'); }
494
+ async deleteRecord(_handle) { return NI('deleteRecord'); }
495
+
496
+ /**
497
+ * Find a record of `kind` whose title matches (case-insensitively), or create
498
+ * it. **One operation, not two calls a caller sequences.**
499
+ *
500
+ * COMP-FLUID-SEAM-GUARANTEES F6-1. `compose ideabox add --cluster "Umbrella A"`
501
+ * looked the cluster up and then created it, and the per-mutation lock covers
502
+ * each of those but not the pair — so two concurrent adds both miss and both
503
+ * create, leaving two clusters with the same name and the ideas split across
504
+ * them. The lookup is the half that is not a mutation, which is exactly why
505
+ * per-mutation locking could never cover it.
506
+ *
507
+ * On the seam rather than in the caller because the fix is a critical section
508
+ * only the provider can open, and because every future caller of "get me the
509
+ * thing called X" has the same race. Its atomicity is the provider's
510
+ * `mutationScope()` — genuinely atomic on the floor, and on a provider
511
+ * declaring `NONE` no better than the two calls it replaces, which is what
512
+ * that declaration is for.
513
+ *
514
+ * @returns {Promise<{record: object, created: boolean}>}
515
+ * @throws {FluidAmbiguousMatch} when more than one record already matches.
516
+ */
517
+ async findOrCreateRecord({ kind, title }, input = {}) {
518
+ // Base implementation: correct, and only as atomic as the provider is. A
519
+ // provider that can do better overrides this; the floor does.
520
+ const matches = (await this.listRecords({ kind }))
521
+ .filter((r) => r.title.toLowerCase() === String(title).toLowerCase());
522
+ if (matches.length > 1) {
523
+ throw new FluidAmbiguousMatch(kind, title, matches.map((m) => m.handle));
524
+ }
525
+ if (matches.length === 1) return { record: matches[0], created: false };
526
+ return { record: await this.createRecord({ ...input, kind, title }), created: true };
527
+ }
528
+
529
+ // -- links (STORAGE_CAP.LINKS) --------------------------------------------
530
+
531
+ async addLink(_handle, _link) { return NI('addLink'); }
532
+ async removeLink(_handle, _link) { return NI('removeLink'); }
533
+
534
+ // -- lifecycle events (STORAGE_CAP.EVENTS) --------------------------------
535
+
536
+ async appendEvent(_event) { return NI('appendEvent'); }
537
+ async readEvents(_handle) { return NI('readEvents'); }
538
+
539
+ // -- semantic capabilities ------------------------------------------------
540
+ //
541
+ // These are declared here ONLY so that the failure mode is uniform and
542
+ // typed: the base implementation's entire job is to refuse. It is deliberate
543
+ // that there is no fallback, no heuristic, and no degraded path. A provider
544
+ // that offers one of these overrides the method AND declares the capability;
545
+ // the two must move together, which is what makes `has()` trustworthy.
546
+ //
547
+ // See {@link RecallHit} above `recall()` for the one return shape that IS
548
+ // specified here rather than left to a provider.
549
+
550
+ /**
551
+ * Semantic recall over records. Declared here, implemented by any provider
552
+ * that has the machinery for it.
553
+ *
554
+ * THE SHAPE IS PART OF THE SEAM, not of whichever provider gets there first.
555
+ * A second provider that returned bare records, or glued a score onto one,
556
+ * would be a different contract wearing the same method name — the exact
557
+ * drift that put `normalizeRecord` and `UNPATCHABLE` in `record-shape.js`.
558
+ *
559
+ * @param {string} _query free text
560
+ * @param {{limit?: number}} [_opts] `limit` defaults to {@link RECALL_LIMIT_DEFAULT}
561
+ * and is clamped to [{@link RECALL_LIMIT_MIN}, {@link RECALL_LIMIT_MAX}]. A
562
+ * missing or unusable value takes the default rather than throwing: recall
563
+ * is a discovery call, usually driven by a UI or an agent, and failing hard
564
+ * on a sloppy limit is worse than answering sanely.
565
+ * @returns {Promise<RecallHit[]>} ranked best-first
566
+ * @throws {FluidCapabilityUnavailable} unless the provider declares RECALL.
567
+ */
568
+ async recall(_query, _opts) { this.require(CAP.RECALL); return NI('recall'); }
569
+
570
+ /**
571
+ * Contradiction detection: surface stored records that contradict the record
572
+ * at `handle`. THE SHAPE IS PART OF THE SEAM (like {@link RecallHit}), not of
573
+ * whichever provider implements it first.
574
+ *
575
+ * @typedef {object} Conflict
576
+ * @property {string} handle a fluid record that contradicts the challenged one
577
+ * @property {string} existingText the contradicting record's asserted text
578
+ * @property {string} conflictType e.g. direct_contradiction, temporal_conflict
579
+ * @property {number} confidence 0..1
580
+ * @property {string} explanation why it conflicts
581
+ * @property {string} suggestedResolution e.g. keep_existing, accept_new, merge
582
+ *
583
+ * @typedef {object} ChallengeResult
584
+ * @property {string} assertion the challenged record's text
585
+ * @property {boolean} hasConflicts DERIVED from the returned conflicts, never a
586
+ * provider's upstream pre-filter value
587
+ * @property {number} confidence DERIVED from the returned conflicts (1.0 when none)
588
+ * @property {Conflict[]} conflicts best-first
589
+ *
590
+ * @param {string} _handle
591
+ * @param {{useLlm?: boolean, timeoutMs?: number}} [_opts]
592
+ * @returns {Promise<ChallengeResult>}
593
+ * @throws {FluidCapabilityUnavailable} unless the provider declares CHALLENGE.
594
+ */
595
+ async challenge(_handle, _opts) { this.require(CAP.CHALLENGE); return NI('challenge'); }
596
+
597
+ /**
598
+ * Belief-strength read: a record's current confidence and its decay history
599
+ * (COMP-FOH FOH-4). THE SHAPE IS PART OF THE SEAM (like {@link RecallHit}).
600
+ *
601
+ * @typedef {object} ConvictionEvent
602
+ * @property {string} timestamp ISO
603
+ * @property {number} oldConfidence
604
+ * @property {number} newConfidence
605
+ * @property {number} decayFactor
606
+ * @property {string} reason e.g. manual_resolution:accept_new
607
+ * @property {string} [conflictingFact] the contradicting text, truncated server-side
608
+ *
609
+ * @typedef {object} ConvictionResult
610
+ * @property {string} handle the record whose conviction this is
611
+ * @property {number} confidence current belief-strength 0..1 (1.0 if never touched)
612
+ * @property {boolean} challenged DERIVED: challengeCount > 0
613
+ * @property {number} challengeCount
614
+ * @property {string|null} lastChallengedAt DERIVED: newest history event's timestamp
615
+ * @property {ConvictionEvent[]} history oldest-first, capped server-side (20)
616
+ *
617
+ * @param {string} _handle
618
+ * @returns {Promise<ConvictionResult>}
619
+ * @throws {FluidCapabilityUnavailable} unless the provider declares CONVICTION.
620
+ */
621
+ async conviction(_handle) { this.require(CAP.CONVICTION); return NI('conviction'); }
622
+
623
+ /**
624
+ * Gated resolution of a contradiction (COMP-FOH FOH-4): decay the TARGET
625
+ * record's confidence because the SOURCE record supersedes it.
626
+ *
627
+ * The first fluid method that MUTATES stored memory, so the contract is
628
+ * spelled out here where every provider must read it:
629
+ * - The strategy is always caller-supplied ({@link CONVICTION_STRATEGIES};
630
+ * v1: `accept_new` only). Never automatic, no default.
631
+ * - A 0.5 decay is near-irreversible (no fluid reinforce path exists).
632
+ * - Intentional repeat calls decay again (1.0 → 0.5 → 0.0) — dedupe is the
633
+ * caller's responsibility.
634
+ * - Of the typed failures, ONLY {@link FluidResolutionNoOp} is safe to
635
+ * retry. {@link FluidResolutionIndeterminate} means the decay may still
636
+ * land; retrying it can double-decay.
637
+ * - The provider verifies the target really is a same-kind, non-self
638
+ * record, but NOT that it was genuinely challenged against the source —
639
+ * the caller is trusted to pass a pairing obtained from a real
640
+ * `challenge()` result (v1 trust boundary, disclosed).
641
+ *
642
+ * @param {string} _sourceHandle the surviving record (its text becomes the contradicting fact)
643
+ * @param {string} _targetHandle the contradicted record whose confidence decays
644
+ * @param {{strategy: string, timeoutMs?: number}} [_opts]
645
+ * @returns {Promise<ConvictionResult>} the target's post-decay conviction
646
+ * @throws {FluidCapabilityUnavailable} unless the provider declares CONVICTION.
647
+ */
648
+ async resolveConflict(_sourceHandle, _targetHandle, _opts) {
649
+ this.require(CAP.CONVICTION);
650
+ return NI('resolveConflict');
651
+ }
652
+
653
+ /** @throws {FluidCapabilityUnavailable} unless the provider declares CALIBRATION. */
654
+ async calibration(_scope) { this.require(CAP.CALIBRATION); return NI('calibration'); }
655
+
656
+ /**
657
+ * The records that CONTRADICT `handle` (COMP-FOH FOH-5): the itemized,
658
+ * resolvable form of what {@link conviction} shows only as truncated history
659
+ * text. Each hit is a record whose assertion was used to decay `handle`'s
660
+ * confidence — an INCOMING contradiction edge from that source into `handle`.
661
+ *
662
+ * **A best-effort LOWER BOUND, not a complete set.** The edge that backs a hit
663
+ * is written best-effort after a resolution decays confidence; if that write
664
+ * was abandoned (network), the decay still happened but the link may be
665
+ * missing. This call can therefore under-report. The authoritative, complete
666
+ * record of decays is `conviction(handle).history` (as text). Repeating a
667
+ * resolution to "repair" a missing edge is forbidden — it decays again.
668
+ *
669
+ * `handle` on each hit is the authority: re-read with `getRecord(handle)`
670
+ * before acting. Hits are canonicalized — a duplicate source handle yields at
671
+ * most one hit, the one whose item is `getRecord`'s canonical item.
672
+ *
673
+ * @typedef {object} ContradictionHit
674
+ * @property {string} handle the contradicting record's citation and authority
675
+ * @property {string} kind the contradicting record's kind
676
+ * @property {object} record a current snapshot, valid against `#/definitions/record`
677
+ *
678
+ * @param {string} _handle the record whose contradictions to list
679
+ * @returns {Promise<ContradictionHit[]>} contradicting records, canonicalized; `[]` if none
680
+ * @throws {FluidRecordNotFound} if `_handle` is unknown, or the target vanished mid-read.
681
+ * @throws {FluidCapabilityUnavailable} unless the provider declares CONTRADICTION.
682
+ */
683
+ async contradictions(_handle) { this.require(CAP.CONTRADICTION); return NI('contradictions'); }
684
+ }