@jungjaehoon/mama-core 2.4.1 → 4.0.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 (371) hide show
  1. package/README.md +76 -239
  2. package/db/migrations/030-case-first-memory-substrate.sql +13 -98
  3. package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
  4. package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
  5. package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
  6. package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
  7. package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
  8. package/db/migrations/069-create-registry-nodes.sql +52 -0
  9. package/db/migrations/070-record-identity.sql +35 -0
  10. package/db/migrations/071-service-operation-origins.sql +187 -0
  11. package/db/migrations/072-observation-versions.sql +29 -0
  12. package/db/migrations/073-registry-corrections.sql +45 -0
  13. package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
  14. package/db/migrations/075-agent-judgments.sql +58 -0
  15. package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
  16. package/db/migrations/077-legacy-record-kind.sql +6 -0
  17. package/db/migrations/078-source-ingest-commands.sql +18 -0
  18. package/db/migrations/079-twin-edge-relations.sql +43 -0
  19. package/db/migrations/080-restore-duration-days.sql +7 -0
  20. package/db/migrations/081-mailbox.sql +61 -0
  21. package/db/migrations/082-commitments-row-id.sql +12 -0
  22. package/db/migrations/083-observation-is-the-evidence.sql +49 -0
  23. package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
  24. package/db/migrations/085-a-migration-has-a-source.sql +37 -0
  25. package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
  26. package/db/migrations/087-mailbox-payload.sql +5 -0
  27. package/db/migrations/088-native-input-journal.sql +14 -0
  28. package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
  29. package/db/migrations/090-model-run-native-inputs.sql +29 -0
  30. package/db/migrations/091-native-turn-results.sql +11 -0
  31. package/db/migrations/092-restore-decision-fts.sql +34 -0
  32. package/db/migrations/093-native-turn-primary-kind.sql +14 -0
  33. package/db/migrations/094-commitment-run-provenance.sql +9 -0
  34. package/db/migrations/095-model-run-native-input-identity.sql +39 -0
  35. package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
  36. package/db/migrations/097-native-input-json-validity.sql +37 -0
  37. package/db/migrations/098-workflow-memory-kind.sql +105 -0
  38. package/dist/action-contracts.d.ts +212 -0
  39. package/dist/action-contracts.js +10 -0
  40. package/dist/api/catalog.d.ts +118 -0
  41. package/dist/api/catalog.js +1756 -0
  42. package/dist/api/dispatch.d.ts +76 -0
  43. package/dist/api/dispatch.js +372 -0
  44. package/dist/canonicalize.d.ts +2 -2
  45. package/dist/canonicalize.js +3 -3
  46. package/dist/client/client.d.ts +33 -0
  47. package/dist/client/client.js +123 -0
  48. package/dist/client/ipc.d.ts +81 -0
  49. package/dist/client/ipc.js +314 -0
  50. package/dist/db-adapter/base-adapter.d.ts +6 -66
  51. package/dist/db-adapter/base-adapter.js +6 -6
  52. package/dist/db-adapter/index.d.ts +5 -5
  53. package/dist/db-adapter/index.js +2 -5
  54. package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
  55. package/dist/db-adapter/node-sqlite-adapter.js +1968 -570
  56. package/dist/db-manager.d.ts +53 -97
  57. package/dist/db-manager.js +121 -603
  58. package/dist/embedding/embedder.d.ts +132 -0
  59. package/dist/embedding/embedder.js +299 -0
  60. package/dist/identity/principal-repository.d.ts +2 -2
  61. package/dist/index.d.ts +43 -58
  62. package/dist/index.js +97 -145
  63. package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
  64. package/dist/knowledge/access.js +663 -0
  65. package/dist/knowledge/case-errors.d.ts +31 -0
  66. package/dist/knowledge/case-errors.js +40 -0
  67. package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
  68. package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
  69. package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
  70. package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
  71. package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
  72. package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
  73. package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
  74. package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
  75. package/dist/knowledge/commitments.d.ts +188 -0
  76. package/dist/knowledge/commitments.js +370 -0
  77. package/dist/knowledge/decision-edges.d.ts +70 -0
  78. package/dist/knowledge/decision-edges.js +123 -0
  79. package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
  80. package/dist/knowledge/graph-query.js +1585 -0
  81. package/dist/knowledge/identity.d.ts +16 -0
  82. package/dist/knowledge/identity.js +22 -0
  83. package/dist/knowledge/index.d.ts +47 -0
  84. package/dist/knowledge/index.js +90 -0
  85. package/dist/knowledge/judgments.d.ts +118 -0
  86. package/dist/knowledge/judgments.js +808 -0
  87. package/dist/knowledge/observations.d.ts +160 -0
  88. package/dist/knowledge/observations.js +449 -0
  89. package/dist/knowledge/question-type.d.ts +34 -0
  90. package/dist/knowledge/question-type.js +70 -0
  91. package/dist/{search → knowledge}/ranker-features.js +1 -1
  92. package/dist/{search → knowledge}/search-quality.d.ts +0 -1
  93. package/dist/knowledge/search.d.ts +38 -0
  94. package/dist/knowledge/search.js +91 -0
  95. package/dist/knowledge/source-ingest.d.ts +65 -0
  96. package/dist/knowledge/source-ingest.js +174 -0
  97. package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
  98. package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
  99. package/dist/knowledge/work-dates.d.ts +49 -0
  100. package/dist/knowledge/work-dates.js +120 -0
  101. package/dist/mama-api.d.ts +114 -778
  102. package/dist/mama-api.js +313 -2461
  103. package/dist/memory/api.d.ts +264 -24
  104. package/dist/memory/api.js +1633 -843
  105. package/dist/memory/bootstrap-builder.d.ts +2 -1
  106. package/dist/memory/bootstrap-builder.js +5 -5
  107. package/dist/memory/channel-summary-state-store.d.ts +3 -2
  108. package/dist/memory/channel-summary-state-store.js +53 -51
  109. package/dist/memory/channel-summary-store.d.ts +5 -3
  110. package/dist/memory/channel-summary-store.js +3 -8
  111. package/dist/memory/dashboard-read.d.ts +46 -0
  112. package/dist/memory/dashboard-read.js +94 -0
  113. package/dist/memory/event-store.d.ts +3 -3
  114. package/dist/memory/event-store.js +3 -10
  115. package/dist/memory/finding-store.d.ts +3 -2
  116. package/dist/memory/finding-store.js +2 -7
  117. package/dist/memory/graph-read.d.ts +52 -0
  118. package/dist/memory/graph-read.js +130 -0
  119. package/dist/memory/judgment-types.d.ts +394 -0
  120. package/dist/memory/judgment-types.js +16 -0
  121. package/dist/memory/provenance-audit.d.ts +5 -2
  122. package/dist/memory/provenance-audit.js +8 -14
  123. package/dist/memory/provenance-live.d.ts +141 -0
  124. package/dist/memory/provenance-live.js +409 -0
  125. package/dist/memory/provenance-query.d.ts +5 -4
  126. package/dist/memory/provenance-query.js +21 -29
  127. package/dist/memory/provenance-resolver.d.ts +191 -0
  128. package/dist/memory/provenance-resolver.js +155 -0
  129. package/dist/memory/provenance.d.ts +1 -11
  130. package/dist/memory/provenance.js +2 -33
  131. package/dist/memory/recall-sanitize.d.ts +55 -0
  132. package/dist/memory/recall-sanitize.js +144 -0
  133. package/dist/memory/secret-filter.d.ts +32 -0
  134. package/dist/memory/secret-filter.js +126 -0
  135. package/dist/memory/truth-store.d.ts +2 -1
  136. package/dist/memory/truth-store.js +1 -4
  137. package/dist/memory/types.d.ts +74 -24
  138. package/dist/memory/types.js +75 -5
  139. package/dist/memory/write-adapters.d.ts +98 -0
  140. package/dist/memory/write-adapters.js +242 -0
  141. package/dist/operations/owner-action-effects.d.ts +53 -0
  142. package/dist/operations/owner-action-effects.js +96 -0
  143. package/dist/provenance/source-ref.d.ts +1 -1
  144. package/dist/provenance/source-ref.js +0 -2
  145. package/dist/registry/corrections.d.ts +31 -0
  146. package/dist/registry/corrections.js +332 -0
  147. package/dist/registry/record-identity.d.ts +49 -0
  148. package/dist/registry/record-identity.js +133 -0
  149. package/dist/registry/store.d.ts +101 -0
  150. package/dist/registry/store.js +395 -0
  151. package/dist/registry/types.d.ts +28 -0
  152. package/dist/registry/types.js +5 -0
  153. package/dist/relevance-scorer.js +2 -2
  154. package/dist/runtime/agent-event-bus.d.ts +50 -0
  155. package/dist/runtime/agent-event-bus.js +101 -0
  156. package/dist/runtime/drivers/claude-cli-wrapper.d.ts +155 -0
  157. package/dist/runtime/drivers/claude-cli-wrapper.js +392 -0
  158. package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
  159. package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
  160. package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
  161. package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
  162. package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
  163. package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
  164. package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
  165. package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
  166. package/dist/runtime/drivers/codex-home.d.ts +20 -0
  167. package/dist/runtime/drivers/codex-home.js +674 -0
  168. package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
  169. package/dist/runtime/drivers/codex-thread-registry.js +433 -0
  170. package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
  171. package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
  172. package/dist/runtime/drivers/persistent-cli-process.d.ts +424 -0
  173. package/dist/runtime/drivers/persistent-cli-process.js +1543 -0
  174. package/dist/runtime/drivers/types.d.ts +456 -0
  175. package/dist/runtime/drivers/types.js +110 -0
  176. package/dist/runtime/mailbox.d.ts +223 -0
  177. package/dist/runtime/mailbox.js +582 -0
  178. package/dist/runtime/model-run-store.d.ts +28 -0
  179. package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
  180. package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
  181. package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
  182. package/dist/runtime/native-effect-observer.d.ts +22 -0
  183. package/dist/runtime/native-effect-observer.js +103 -0
  184. package/dist/runtime/native-input-journal.d.ts +46 -0
  185. package/dist/runtime/native-input-journal.js +308 -0
  186. package/dist/runtime/native-prompt.d.ts +118 -0
  187. package/dist/runtime/native-prompt.js +496 -0
  188. package/dist/runtime/native-session.d.ts +10 -0
  189. package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
  190. package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
  191. package/dist/runtime/native-tool-trace-observer.js +65 -0
  192. package/dist/runtime/native-turn.d.ts +272 -0
  193. package/dist/runtime/native-turn.js +1041 -0
  194. package/dist/runtime/operations.d.ts +236 -0
  195. package/dist/runtime/operations.js +565 -0
  196. package/dist/runtime/post-tool-handler.d.ts +74 -0
  197. package/dist/runtime/post-tool-handler.js +137 -0
  198. package/dist/runtime/prompt-layers.d.ts +126 -0
  199. package/dist/runtime/prompt-layers.js +239 -0
  200. package/dist/runtime/runtime-process.d.ts +102 -0
  201. package/dist/runtime/runtime-process.js +206 -0
  202. package/dist/runtime/runtime.d.ts +278 -0
  203. package/dist/runtime/runtime.js +563 -0
  204. package/dist/runtime/session-pool.d.ts +176 -0
  205. package/dist/runtime/session-pool.js +369 -0
  206. package/dist/runtime/stimulus-payload.d.ts +5 -0
  207. package/dist/runtime/stimulus-payload.js +50 -0
  208. package/dist/runtime/subagent-bridge.d.ts +24 -0
  209. package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
  210. package/dist/runtime/text-completion.d.ts +22 -0
  211. package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
  212. package/dist/runtime/token-estimator.d.ts +25 -0
  213. package/dist/runtime/token-estimator.js +75 -0
  214. package/dist/runtime/tool-trace-store.d.ts +22 -0
  215. package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
  216. package/dist/runtime/trace-summary.d.ts +5 -0
  217. package/dist/runtime/trace-summary.js +74 -0
  218. package/dist/runtime/turn-text.d.ts +23 -0
  219. package/dist/runtime/turn-text.js +41 -0
  220. package/dist/storage/database.d.ts +67 -0
  221. package/dist/storage/database.js +158 -0
  222. package/package.json +20 -34
  223. package/dist/agent-graph/alias-write.d.ts +0 -3
  224. package/dist/agent-graph/alias-write.js +0 -234
  225. package/dist/agent-graph/entity-resolve.d.ts +0 -3
  226. package/dist/agent-graph/entity-resolve.js +0 -271
  227. package/dist/agent-graph/errors.d.ts +0 -4
  228. package/dist/agent-graph/errors.js +0 -11
  229. package/dist/agent-graph/graph-query.d.ts +0 -5
  230. package/dist/agent-graph/graph-query.js +0 -360
  231. package/dist/agent-graph/index.d.ts +0 -6
  232. package/dist/agent-situation/builder.d.ts +0 -8
  233. package/dist/agent-situation/builder.js +0 -239
  234. package/dist/agent-situation/cache-key.d.ts +0 -4
  235. package/dist/agent-situation/cache-key.js +0 -64
  236. package/dist/agent-situation/index.d.ts +0 -7
  237. package/dist/agent-situation/index.js +0 -28
  238. package/dist/agent-situation/packet-store.d.ts +0 -27
  239. package/dist/agent-situation/packet-store.js +0 -228
  240. package/dist/agent-situation/ranking-policy.d.ts +0 -18
  241. package/dist/agent-situation/ranking-policy.js +0 -75
  242. package/dist/agent-situation/source-readers.d.ts +0 -52
  243. package/dist/agent-situation/source-readers.js +0 -300
  244. package/dist/agent-situation/types.d.ts +0 -195
  245. package/dist/cases/wiki-page-index.d.ts +0 -37
  246. package/dist/cases/wiki-page-index.js +0 -201
  247. package/dist/config-loader.d.ts +0 -68
  248. package/dist/config-loader.js +0 -239
  249. package/dist/connectors/event-index.d.ts +0 -24
  250. package/dist/connectors/event-index.js +0 -245
  251. package/dist/connectors/raw-query.d.ts +0 -40
  252. package/dist/connectors/raw-query.js +0 -412
  253. package/dist/connectors/types.d.ts +0 -117
  254. package/dist/connectors/types.js +0 -3
  255. package/dist/context-compile/boundary-defaults.d.ts +0 -12
  256. package/dist/context-compile/boundary-defaults.js +0 -105
  257. package/dist/context-compile/channel-grant.js +0 -66
  258. package/dist/context-compile/compiler-policy.d.ts +0 -39
  259. package/dist/context-compile/compiler-policy.js +0 -147
  260. package/dist/context-compile/compiler.d.ts +0 -18
  261. package/dist/context-compile/compiler.js +0 -404
  262. package/dist/context-compile/index.d.ts +0 -10
  263. package/dist/context-compile/index.js +0 -26
  264. package/dist/context-compile/packet-store.d.ts +0 -8
  265. package/dist/context-compile/packet-store.js +0 -239
  266. package/dist/context-compile/ref.d.ts +0 -7
  267. package/dist/context-compile/ref.js +0 -102
  268. package/dist/context-compile/source-readers.d.ts +0 -70
  269. package/dist/context-compile/source-readers.js +0 -840
  270. package/dist/context-compile/types.d.ts +0 -155
  271. package/dist/context-compile/types.js +0 -11
  272. package/dist/context-compile/visibility.d.ts +0 -21
  273. package/dist/context-compile/visibility.js +0 -224
  274. package/dist/decision-tracker.d.ts +0 -206
  275. package/dist/decision-tracker.js +0 -481
  276. package/dist/edges/ref-validation.d.ts +0 -7
  277. package/dist/edges/ref-validation.js +0 -321
  278. package/dist/edges/store.d.ts +0 -8
  279. package/dist/edges/store.js +0 -129
  280. package/dist/embedding-client.d.ts +0 -28
  281. package/dist/embedding-client.js +0 -53
  282. package/dist/embedding-server/index.d.ts +0 -67
  283. package/dist/embedding-server/index.js +0 -405
  284. package/dist/embedding-server/mobile/auth.d.ts +0 -44
  285. package/dist/embedding-server/mobile/auth.js +0 -150
  286. package/dist/embedding-server/mobile/daemon.d.ts +0 -129
  287. package/dist/embedding-server/mobile/daemon.js +0 -313
  288. package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
  289. package/dist/embedding-server/mobile/output-parser.js +0 -241
  290. package/dist/embedding-server/mobile/session-api.d.ts +0 -57
  291. package/dist/embedding-server/mobile/session-api.js +0 -261
  292. package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
  293. package/dist/embedding-server/mobile/session-manager.js +0 -378
  294. package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
  295. package/dist/embedding-server/mobile/websocket-handler.js +0 -486
  296. package/dist/embeddings.d.ts +0 -68
  297. package/dist/embeddings.js +0 -244
  298. package/dist/entities/audit-metrics.d.ts +0 -68
  299. package/dist/entities/audit-metrics.js +0 -124
  300. package/dist/entities/candidate-generator.d.ts +0 -8
  301. package/dist/entities/candidate-generator.js +0 -24
  302. package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
  303. package/dist/entities/entity-linked-decision-counts.js +0 -29
  304. package/dist/entities/entity-list.d.ts +0 -22
  305. package/dist/entities/entity-list.js +0 -3
  306. package/dist/entities/entity-orphan-list.d.ts +0 -17
  307. package/dist/entities/entity-orphan-list.js +0 -3
  308. package/dist/entities/entity-search.d.ts +0 -20
  309. package/dist/entities/entity-search.js +0 -3
  310. package/dist/entities/errors.d.ts +0 -55
  311. package/dist/entities/errors.js +0 -100
  312. package/dist/entities/lineage-store.d.ts +0 -28
  313. package/dist/entities/lineage-store.js +0 -204
  314. package/dist/entities/normalization.d.ts +0 -14
  315. package/dist/entities/normalization.js +0 -63
  316. package/dist/entities/policy-store.d.ts +0 -8
  317. package/dist/entities/policy-store.js +0 -75
  318. package/dist/entities/policy-types.d.ts +0 -72
  319. package/dist/entities/policy-types.js +0 -59
  320. package/dist/entities/projection.d.ts +0 -8
  321. package/dist/entities/projection.js +0 -82
  322. package/dist/entities/provenance-query.d.ts +0 -32
  323. package/dist/entities/provenance-query.js +0 -3
  324. package/dist/entities/read-identity.d.ts +0 -31
  325. package/dist/entities/read-identity.js +0 -264
  326. package/dist/entities/recall-bridge.d.ts +0 -5
  327. package/dist/entities/recall-bridge.js +0 -171
  328. package/dist/entities/resolution-engine.d.ts +0 -8
  329. package/dist/entities/resolution-engine.js +0 -3
  330. package/dist/entities/rollback-preview.d.ts +0 -35
  331. package/dist/entities/rollback-preview.js +0 -3
  332. package/dist/entities/source-locator.d.ts +0 -3
  333. package/dist/entities/source-locator.js +0 -32
  334. package/dist/entities/store.d.ts +0 -69
  335. package/dist/entities/store.js +0 -544
  336. package/dist/entities/types.d.ts +0 -166
  337. package/dist/entities/types.js +0 -30
  338. package/dist/memory/extraction-prompt.d.ts +0 -4
  339. package/dist/memory/extraction-prompt.js +0 -89
  340. package/dist/memory/scope-backfill.d.ts +0 -14
  341. package/dist/memory/scope-backfill.js +0 -3
  342. package/dist/memory/scope-store.d.ts +0 -4
  343. package/dist/memory/scope-store.js +0 -9
  344. package/dist/memory-store.d.ts +0 -87
  345. package/dist/memory-store.js +0 -78
  346. package/dist/model-runs/store.d.ts +0 -13
  347. package/dist/model-runs/tool-trace-store.d.ts +0 -8
  348. package/dist/notification-manager.d.ts +0 -7
  349. package/dist/notification-manager.js +0 -12
  350. package/dist/outcome-tracker.d.ts +0 -151
  351. package/dist/outcome-tracker.js +0 -271
  352. package/dist/query-intent.d.ts +0 -23
  353. package/dist/query-intent.js +0 -114
  354. package/dist/runtime/trusted-provenance.d.ts +0 -3
  355. package/dist/runtime/trusted-provenance.js +0 -6
  356. package/dist/search/question-type.d.ts +0 -5
  357. package/dist/search/question-type.js +0 -41
  358. package/dist/test-utils.d.ts +0 -80
  359. package/dist/test-utils.js +0 -218
  360. package/dist/tier-validator.d.ts +0 -43
  361. package/dist/tier-validator.js +0 -159
  362. package/dist/time-formatter.d.ts +0 -25
  363. package/dist/time-formatter.js +0 -93
  364. /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
  365. /package/dist/{search → knowledge}/feedback-store.js +0 -0
  366. /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
  367. /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
  368. /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
  369. /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
  370. /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
  371. /package/dist/{search → knowledge}/search-quality.js +0 -0
@@ -0,0 +1,1585 @@
1
+ "use strict";
2
+ /**
3
+ * Knowledge graph queries: how a topic got to where it is, and what else it
4
+ * argues with. Two surfaces live here: the decision-chain graph (supersedes /
5
+ * decision_edges) and the twin-edge graph (neighborhood, paths, timeline over
6
+ * `twin_edges`).
7
+ *
8
+ * Every function takes the adapter it reads through, so a caller cannot walk a
9
+ * graph in a database it did not open.
10
+ *
11
+ * @module knowledge/graph-query
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.AgentGraphValidationError = void 0;
15
+ exports.queryDecisionGraph = queryDecisionGraph;
16
+ exports.querySemanticEdges = querySemanticEdges;
17
+ exports.getGraphNeighborhood = getGraphNeighborhood;
18
+ exports.getGraphPaths = getGraphPaths;
19
+ exports.getGraphTimeline = getGraphTimeline;
20
+ exports.queryGraph = queryGraph;
21
+ const access_js_1 = require("../knowledge/access.js");
22
+ const judgments_js_1 = require("./judgments.js");
23
+ const observations_js_1 = require("./observations.js");
24
+ const case_store_js_1 = require("./case-store.js");
25
+ const store_js_1 = require("../registry/store.js");
26
+ const twin_edge_types_js_1 = require("../knowledge/twin-edge-types.js");
27
+ /**
28
+ * `refined_from` is stored as a JSON array of decision ids.
29
+ *
30
+ * Both branches of queryDecisionGraph used to answer a parse failure with an
31
+ * empty array, which reads downstream as "this decision refines nothing" - the
32
+ * same sentence a decision with no ancestry produces. Corruption is not
33
+ * ancestry, so it is reported instead. The stored value is left alone.
34
+ */
35
+ function parseRefinedFrom(decision) {
36
+ if (!decision.refined_from || typeof decision.refined_from !== 'string') {
37
+ return decision.refined_from;
38
+ }
39
+ try {
40
+ return JSON.parse(decision.refined_from);
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ throw new Error(`decision ${decision.id} has unreadable refined_from: ${message}`);
45
+ }
46
+ }
47
+ /**
48
+ * Walk a topic's supersedes chain with a recursive CTE.
49
+ *
50
+ * @param adapter - Database to read through
51
+ * @param topic - Decision topic to query
52
+ * @param anchorId - Start from this decision instead of the topic's current head
53
+ * @returns Decisions ordered by recency, each carrying its approved edges
54
+ */
55
+ async function queryDecisionGraph(adapter, topic, anchorId) {
56
+ try {
57
+ if (!anchorId) {
58
+ const decisions = adapter
59
+ .prepare(`
60
+ WITH RECURSIVE decision_chain AS (
61
+ SELECT * FROM decisions WHERE topic = ? AND superseded_by IS NULL
62
+ UNION
63
+ SELECT d.* FROM decisions d
64
+ JOIN decision_chain dc ON d.id = dc.supersedes
65
+ )
66
+ SELECT * FROM decision_chain ORDER BY created_at DESC, id DESC
67
+ `)
68
+ .all(topic);
69
+ const edgesStmt = adapter.prepare(`
70
+ SELECT * FROM decision_edges
71
+ WHERE from_id = ?
72
+ AND (approved_by_user = 1 OR approved_by_user IS NULL)
73
+ `);
74
+ for (const decision of decisions) {
75
+ decision.edges = edgesStmt.all(decision.id);
76
+ decision.refined_from = parseRefinedFrom(decision);
77
+ }
78
+ return decisions;
79
+ }
80
+ const stmt = adapter.prepare(`
81
+ WITH RECURSIVE decision_chain AS (
82
+ SELECT * FROM (
83
+ SELECT * FROM decisions
84
+ WHERE id = ?
85
+ ORDER BY created_at DESC, id DESC
86
+ LIMIT 1
87
+ )
88
+
89
+ UNION ALL
90
+
91
+ -- Recursive case: Get previous decisions
92
+ SELECT d.* FROM decisions d
93
+ JOIN decision_chain dc ON d.id = dc.supersedes
94
+ )
95
+ SELECT * FROM decision_chain
96
+ ORDER BY created_at DESC
97
+ `);
98
+ const decisions = stmt.all(anchorId);
99
+ // Join with decision_edges to include relationships
100
+ // Prepare statement once outside loop for performance
101
+ const edgesStmt = adapter.prepare(`
102
+ SELECT * FROM decision_edges
103
+ WHERE from_id = ?
104
+ AND (approved_by_user = 1 OR approved_by_user IS NULL)
105
+ `);
106
+ for (const decision of decisions) {
107
+ decision.edges = edgesStmt.all(decision.id);
108
+ decision.refined_from = parseRefinedFrom(decision);
109
+ }
110
+ return decisions;
111
+ }
112
+ catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ throw new Error(`Decision graph query failed: ${message}`);
115
+ }
116
+ }
117
+ /**
118
+ * Outgoing and incoming semantic edges for a set of decisions.
119
+ *
120
+ * @param adapter - Database to read through
121
+ * @param decisionIds - Decision IDs to query edges for
122
+ * @returns Edges categorized by relationship and direction
123
+ */
124
+ async function querySemanticEdges(adapter, decisionIds) {
125
+ if (!decisionIds || decisionIds.length === 0) {
126
+ return {
127
+ refines: [],
128
+ refined_by: [],
129
+ contradicts: [],
130
+ contradicted_by: [],
131
+ // Story 2.1: Extended edge types
132
+ builds_on: [],
133
+ built_on_by: [],
134
+ debates: [],
135
+ debated_by: [],
136
+ synthesizes: [],
137
+ synthesized_by: [],
138
+ };
139
+ }
140
+ try {
141
+ // Build placeholders for IN clause
142
+ const placeholders = decisionIds.map(() => '?').join(',');
143
+ // Story 2.1: Include new edge types in query
144
+ const edgeTypes = ['refines', 'contradicts', 'builds_on', 'debates', 'synthesizes'];
145
+ const edgeTypePlaceholders = edgeTypes.map(() => '?').join(',');
146
+ // Query outgoing edges (from_id = decision)
147
+ const outgoingStmt = adapter.prepare(`
148
+ SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
149
+ FROM decision_edges e
150
+ JOIN decisions d ON e.to_id = d.id
151
+ WHERE e.from_id IN (${placeholders})
152
+ AND e.relationship IN (${edgeTypePlaceholders})
153
+ AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
154
+ ORDER BY e.created_at DESC
155
+ `);
156
+ const outgoingEdges = outgoingStmt.all(...decisionIds, ...edgeTypes);
157
+ // Query incoming edges (to_id = decision)
158
+ const incomingStmt = adapter.prepare(`
159
+ SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
160
+ FROM decision_edges e
161
+ JOIN decisions d ON e.from_id = d.id
162
+ WHERE e.to_id IN (${placeholders})
163
+ AND e.relationship IN (${edgeTypePlaceholders})
164
+ AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
165
+ ORDER BY e.created_at DESC
166
+ `);
167
+ const incomingEdges = incomingStmt.all(...decisionIds, ...edgeTypes);
168
+ // Categorize edges (original + v1.3 extended)
169
+ const refines = outgoingEdges.filter((e) => e.relationship === 'refines');
170
+ const refined_by = incomingEdges.filter((e) => e.relationship === 'refines');
171
+ const contradicts = outgoingEdges.filter((e) => e.relationship === 'contradicts');
172
+ const contradicted_by = incomingEdges.filter((e) => e.relationship === 'contradicts');
173
+ // Story 2.1: New edge type categories
174
+ const builds_on = outgoingEdges.filter((e) => e.relationship === 'builds_on');
175
+ const built_on_by = incomingEdges.filter((e) => e.relationship === 'builds_on');
176
+ const debates = outgoingEdges.filter((e) => e.relationship === 'debates');
177
+ const debated_by = incomingEdges.filter((e) => e.relationship === 'debates');
178
+ const synthesizes = outgoingEdges.filter((e) => e.relationship === 'synthesizes');
179
+ const synthesized_by = incomingEdges.filter((e) => e.relationship === 'synthesizes');
180
+ return {
181
+ refines,
182
+ refined_by,
183
+ contradicts,
184
+ contradicted_by,
185
+ builds_on,
186
+ built_on_by,
187
+ debates,
188
+ debated_by,
189
+ synthesizes,
190
+ synthesized_by,
191
+ };
192
+ }
193
+ catch (error) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ throw new Error(`Semantic edges query failed: ${message}`);
196
+ }
197
+ }
198
+ // ── Twin-edge graph ────────────────────────────────────────────────────────────
199
+ class AgentGraphValidationError extends Error {
200
+ constructor(message) {
201
+ super(message);
202
+ this.name = 'AgentGraphValidationError';
203
+ }
204
+ }
205
+ exports.AgentGraphValidationError = AgentGraphValidationError;
206
+ const DEFAULT_GRAPH_LIMIT = 100;
207
+ const DEFAULT_PATH_LIMIT = 10;
208
+ const MAX_DEPTH = 5;
209
+ // A path count cannot bound a dense graph with an unreachable target.
210
+ const MAX_PATH_FRONTIER = 1000;
211
+ const MAX_PATH_EDGE_WORK = 10000;
212
+ function refKey(ref) {
213
+ return `${ref.kind}:${ref.id}`;
214
+ }
215
+ function edgeKey(edge) {
216
+ return edge.edge_id;
217
+ }
218
+ function eventKey(event) {
219
+ if (event.kind === 'edge') {
220
+ return event.edge.edge_id;
221
+ }
222
+ return `${event.ref.kind}:${event.ref.id}`;
223
+ }
224
+ function eventKindRank(kind) {
225
+ if (kind === 'memory') {
226
+ return 1;
227
+ }
228
+ if (kind === 'raw') {
229
+ return 2;
230
+ }
231
+ if (kind === 'case') {
232
+ return 3;
233
+ }
234
+ return 4;
235
+ }
236
+ function normalizeDepth(value, fallback) {
237
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
238
+ return fallback;
239
+ }
240
+ return Math.max(0, Math.min(MAX_DEPTH, Math.floor(value)));
241
+ }
242
+ function normalizeLimit(value, fallback) {
243
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
244
+ return fallback;
245
+ }
246
+ return Math.max(1, Math.floor(value));
247
+ }
248
+ function addNode(nodes, seen, ref) {
249
+ const key = refKey(ref);
250
+ if (!seen.has(key)) {
251
+ seen.add(key);
252
+ nodes.push(ref);
253
+ }
254
+ }
255
+ function addRef(refs, seen, ref) {
256
+ if (ref.kind === 'edge' || ref.kind === 'report') {
257
+ return;
258
+ }
259
+ const key = refKey(ref);
260
+ if (!seen.has(key)) {
261
+ seen.add(key);
262
+ refs.push(ref);
263
+ }
264
+ }
265
+ function oppositeRef(edge, current) {
266
+ const currentKey = refKey(current);
267
+ const refs = [];
268
+ if (refKey(edge.subject_ref) !== currentKey) {
269
+ refs.push(edge.subject_ref);
270
+ }
271
+ if (refKey(edge.object_ref) !== currentKey) {
272
+ refs.push(edge.object_ref);
273
+ }
274
+ if (refs.length === 0) {
275
+ refs.push(edge.object_ref);
276
+ }
277
+ return refs;
278
+ }
279
+ function listFilteredEdges(adapter, refs, input) {
280
+ // Spread, not a field-by-field re-listing. Re-listing is how the channel grant was
281
+ // silently dropped here while every other path enforced it: a new field on the
282
+ // visibility type simply never arrived, and nothing failed to say so.
283
+ return (0, access_js_1.listVisibleTwinEdgesForRefs)(adapter, refs, {
284
+ ...input.visibility,
285
+ edgeTypes: input.edge_filters?.edge_types,
286
+ asOfMs: input.as_of_ms,
287
+ limit: input.limit,
288
+ });
289
+ }
290
+ function assertRefsVisible(adapter, refs, visibility, asOfMs) {
291
+ try {
292
+ (0, access_js_1.assertTwinRefsVisible)(adapter, refs, { ...visibility, asOfMs });
293
+ }
294
+ catch (error) {
295
+ // A visibility refusal is a denial, not a malformed query — keep its type
296
+ // so callers can answer denied instead of invalid.
297
+ if (error instanceof access_js_1.TwinRefNotVisibleError) {
298
+ throw error;
299
+ }
300
+ throw new AgentGraphValidationError(error instanceof Error ? error.message : String(error));
301
+ }
302
+ }
303
+ function graphVisibility(input) {
304
+ return {
305
+ scopes: input.scopes,
306
+ connectors: input.connectors,
307
+ connectorWideRead: input.connector_wide_read,
308
+ projectRefs: input.project_refs,
309
+ tenantId: input.tenant_id,
310
+ principalId: input.principal_id,
311
+ agentId: input.agent_id,
312
+ maxSourceMs: input.max_source_ms,
313
+ includeReplaced: input.history === 'all',
314
+ ...(input.channels ? { channels: input.channels } : {}),
315
+ };
316
+ }
317
+ function projectCurrentEdges(adapter, edges, visibility) {
318
+ if (edges.length === 0) {
319
+ return [];
320
+ }
321
+ const edgeIds = [...new Set(edges.map((edge) => edge.edge_id))];
322
+ const placeholders = edgeIds.map(() => '?').join(', ');
323
+ const assignmentRows = adapter
324
+ .prepare(`SELECT edge_id, endpoint, resolved_node_id
325
+ FROM registry_ref_assignments assignment
326
+ WHERE edge_id IN (${placeholders})
327
+ AND committed_revision = (
328
+ SELECT MAX(current_assignment.committed_revision)
329
+ FROM registry_ref_assignments current_assignment
330
+ WHERE current_assignment.edge_id = assignment.edge_id
331
+ AND current_assignment.endpoint = assignment.endpoint
332
+ )`)
333
+ .all(...edgeIds);
334
+ const assignments = new Map(assignmentRows.map((row) => [
335
+ `${row.edge_id}\0${row.endpoint}`,
336
+ row.resolved_node_id === null
337
+ ? null
338
+ : { kind: 'registry', id: row.resolved_node_id },
339
+ ]));
340
+ const projections = edges.flatMap((edge) => ['from', 'to'].map((endpoint) => {
341
+ const original = endpoint === 'from' ? edge.subject_ref : edge.object_ref;
342
+ const key = `${edge.edge_id}\0${endpoint}`;
343
+ return {
344
+ edge_id: edge.edge_id,
345
+ endpoint,
346
+ original_ref: original,
347
+ current_ref: assignments.has(key) ? (assignments.get(key) ?? null) : original,
348
+ };
349
+ }));
350
+ const registryIds = [
351
+ ...new Set(projections
352
+ .map((projection) => projection.current_ref)
353
+ .filter((ref) => ref?.kind === 'registry')
354
+ .map((ref) => ref.id)),
355
+ ];
356
+ const resolvedRegistry = new Map();
357
+ if (registryIds.length > 0) {
358
+ const registryPlaceholders = registryIds.map(() => '?').join(', ');
359
+ const rows = adapter
360
+ .prepare(`WITH RECURSIVE registry_chain(origin_id, id, merged_into, depth) AS (
361
+ SELECT id, id, merged_into, 0 FROM registry_nodes WHERE id IN (${registryPlaceholders})
362
+ UNION ALL
363
+ SELECT chain.origin_id, node.id, node.merged_into, chain.depth + 1
364
+ FROM registry_chain chain
365
+ JOIN registry_nodes node ON node.id = chain.merged_into
366
+ WHERE chain.depth < 100
367
+ )
368
+ SELECT origin_id, id, merged_into, depth FROM registry_chain ORDER BY origin_id, depth`)
369
+ .all(...registryIds);
370
+ // The rows are ordered by origin and depth. Group their terminal row once:
371
+ // filtering the whole result for every origin made a wide graph O(N²).
372
+ const lastByOrigin = new Map();
373
+ for (const row of rows) {
374
+ lastByOrigin.set(row.origin_id, row);
375
+ }
376
+ for (const id of registryIds) {
377
+ const last = lastByOrigin.get(id);
378
+ resolvedRegistry.set(id, last && last.merged_into === null ? last.id : null);
379
+ }
380
+ const currentIds = [
381
+ ...new Set([...resolvedRegistry.values()].filter((id) => !!id)),
382
+ ];
383
+ const visibleIds = new Set();
384
+ if (!visibility.scopes || visibility.scopes.length === 0) {
385
+ currentIds.forEach((id) => visibleIds.add(id));
386
+ }
387
+ else if (currentIds.length > 0) {
388
+ const currentPlaceholders = currentIds.map(() => '?').join(', ');
389
+ const scopeClauses = visibility.scopes
390
+ .map(() => '(scope_kind = ? AND scope_id = ?)')
391
+ .join(' OR ');
392
+ const bindings = adapter
393
+ .prepare(`SELECT DISTINCT node_id FROM registry_scope_bindings
394
+ WHERE node_id IN (${currentPlaceholders}) AND (${scopeClauses})`)
395
+ .all(...currentIds, ...visibility.scopes.flatMap((scope) => [scope.kind, scope.id]));
396
+ bindings.forEach((row) => visibleIds.add(row.node_id));
397
+ }
398
+ for (const projection of projections) {
399
+ if (projection.current_ref?.kind === 'registry') {
400
+ const resolved = resolvedRegistry.get(projection.current_ref.id) ?? null;
401
+ projection.current_ref =
402
+ resolved && visibleIds.has(resolved) ? { kind: 'registry', id: resolved } : null;
403
+ }
404
+ }
405
+ }
406
+ const nonRegistryRefs = projections
407
+ .map((projection) => projection.current_ref)
408
+ .filter((ref) => ref !== null && ref.kind !== 'registry');
409
+ const visibleNonRegistry = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, nonRegistryRefs, visibility);
410
+ for (const projection of projections) {
411
+ const current = projection.current_ref;
412
+ if (!current || current.kind === 'registry') {
413
+ continue;
414
+ }
415
+ if (!visibleNonRegistry.has(`${current.kind}\0${current.id}`)) {
416
+ projection.current_ref = null;
417
+ }
418
+ }
419
+ return projections;
420
+ }
421
+ function numberMs(value, field, refId) {
422
+ if (typeof value === 'number' && Number.isFinite(value)) {
423
+ return Math.floor(value);
424
+ }
425
+ throw new Error(`Invalid ${field} timestamp for graph timeline ref ${refId}.`);
426
+ }
427
+ function nullableNumberMs(value, field, refId) {
428
+ if (value === null || value === undefined) {
429
+ return null;
430
+ }
431
+ return numberMs(value, field, refId);
432
+ }
433
+ function parseTimestampMs(value, field, refId) {
434
+ if (typeof value === 'number') {
435
+ return Math.floor(value);
436
+ }
437
+ if (typeof value === 'string') {
438
+ const parsed = Date.parse(value);
439
+ if (Number.isFinite(parsed)) {
440
+ return parsed;
441
+ }
442
+ }
443
+ throw new Error(`Invalid ${field} timestamp for graph timeline ref ${refId}.`);
444
+ }
445
+ function timelineWindow(input) {
446
+ const upperBounds = [input.to_ms, input.as_of_ms].filter((value) => typeof value === 'number');
447
+ return {
448
+ fromMs: input.from_ms,
449
+ toMs: upperBounds.length > 0 ? Math.min(...upperBounds) : undefined,
450
+ };
451
+ }
452
+ function isInTimelineWindow(atMs, window) {
453
+ return ((window.fromMs === undefined || atMs >= window.fromMs) &&
454
+ (window.toMs === undefined || atMs <= window.toMs));
455
+ }
456
+ function loadTimelineRecordEvent(adapter, ref) {
457
+ if (ref.kind === 'memory') {
458
+ const row = adapter
459
+ .prepare(`
460
+ SELECT id, topic, decision, created_at, event_datetime
461
+ FROM decisions
462
+ WHERE id = ?
463
+ LIMIT 1
464
+ `)
465
+ .get(ref.id);
466
+ if (!row) {
467
+ return null;
468
+ }
469
+ const eventDatetime = nullableNumberMs(row.event_datetime, 'decisions.event_datetime', ref.id);
470
+ const createdAt = numberMs(row.created_at, 'decisions.created_at', ref.id);
471
+ return {
472
+ kind: 'memory',
473
+ at_ms: eventDatetime ?? createdAt,
474
+ ref,
475
+ memory: {
476
+ id: row.id,
477
+ topic: row.topic,
478
+ decision: row.decision,
479
+ created_at: createdAt,
480
+ event_datetime: eventDatetime,
481
+ },
482
+ };
483
+ }
484
+ if (ref.kind === 'raw') {
485
+ const row = adapter
486
+ .prepare(
487
+ // A raw ref resolves against observations: one evidence space, and the
488
+ // connector index belongs to the package that has connectors. The aliases
489
+ // keep the wire shape callers already read.
490
+ `
491
+ SELECT
492
+ observation_id AS event_index_id, source AS source_connector, source_type,
493
+ source_id, source_locator, title, source_at AS event_datetime,
494
+ observed_at AS source_timestamp_ms, observation_id AS current_observation_id
495
+ FROM observation_versions
496
+ WHERE observation_id = ?
497
+ LIMIT 1
498
+ `)
499
+ .get(ref.id);
500
+ if (!row) {
501
+ return null;
502
+ }
503
+ const eventDatetime = nullableNumberMs(row.event_datetime, 'observation_versions.source_at', ref.id);
504
+ const sourceTimestampMs = numberMs(row.source_timestamp_ms, 'observation_versions.observed_at', ref.id);
505
+ return {
506
+ kind: 'raw',
507
+ at_ms: eventDatetime ?? sourceTimestampMs,
508
+ ref,
509
+ raw: {
510
+ event_index_id: row.event_index_id,
511
+ source_connector: row.source_connector,
512
+ source_type: row.source_type,
513
+ source_id: row.source_id,
514
+ source_locator: row.source_locator,
515
+ title: row.title,
516
+ event_datetime: eventDatetime,
517
+ source_timestamp_ms: sourceTimestampMs,
518
+ observation_ref: row.current_observation_id,
519
+ },
520
+ };
521
+ }
522
+ if (ref.kind === 'case') {
523
+ const row = adapter
524
+ .prepare(`
525
+ SELECT case_id, title, status, last_activity_at, created_at, updated_at
526
+ FROM case_truth
527
+ WHERE case_id = ?
528
+ LIMIT 1
529
+ `)
530
+ .get(ref.id);
531
+ if (!row) {
532
+ return null;
533
+ }
534
+ return {
535
+ kind: 'case',
536
+ at_ms: parseTimestampMs(row.last_activity_at ?? row.updated_at, 'case_truth.updated_at', ref.id),
537
+ ref,
538
+ case: {
539
+ case_id: row.case_id,
540
+ title: row.title,
541
+ status: row.status,
542
+ created_at: row.created_at,
543
+ updated_at: row.updated_at,
544
+ last_activity_at: row.last_activity_at,
545
+ },
546
+ };
547
+ }
548
+ return null;
549
+ }
550
+ function getGraphNeighborhood(adapter, input) {
551
+ const depth = normalizeDepth(input.depth, 1);
552
+ const limit = normalizeLimit(input.limit, DEFAULT_GRAPH_LIMIT);
553
+ const visibility = graphVisibility(input);
554
+ assertRefsVisible(adapter, [input.ref], visibility, input.as_of_ms);
555
+ const nodes = [];
556
+ const edges = [];
557
+ const seenNodes = new Set();
558
+ const seenEdges = new Set();
559
+ addNode(nodes, seenNodes, input.ref);
560
+ let frontier = [input.ref];
561
+ for (let currentDepth = 0; currentDepth < depth && frontier.length > 0; currentDepth++) {
562
+ const found = listFilteredEdges(adapter, frontier, {
563
+ visibility,
564
+ edge_filters: input.edge_filters,
565
+ as_of_ms: input.as_of_ms,
566
+ limit,
567
+ });
568
+ const direction = input.direction ?? 'both';
569
+ const frontierKeys = new Set(frontier.map(refKey));
570
+ const nextFrontier = [];
571
+ const nextKeys = new Set();
572
+ for (const edge of found) {
573
+ if (seenEdges.size >= limit) {
574
+ break;
575
+ }
576
+ const fromHere = frontierKeys.has(refKey(edge.subject_ref));
577
+ const toHere = frontierKeys.has(refKey(edge.object_ref));
578
+ if (direction === 'out' ? !fromHere : direction === 'in' ? !toHere : !fromHere && !toHere) {
579
+ continue;
580
+ }
581
+ if (!seenEdges.has(edgeKey(edge))) {
582
+ seenEdges.add(edgeKey(edge));
583
+ edges.push(edge);
584
+ }
585
+ const expand = direction === 'out'
586
+ ? [edge.object_ref]
587
+ : direction === 'in'
588
+ ? [edge.subject_ref]
589
+ : [edge.subject_ref, edge.object_ref];
590
+ for (const endpoint of expand) {
591
+ const before = seenNodes.size;
592
+ addNode(nodes, seenNodes, endpoint);
593
+ if (seenNodes.size > before && !nextKeys.has(refKey(endpoint))) {
594
+ nextKeys.add(refKey(endpoint));
595
+ nextFrontier.push(endpoint);
596
+ }
597
+ }
598
+ }
599
+ frontier = nextFrontier;
600
+ }
601
+ return { nodes, edges, current_projection: projectCurrentEdges(adapter, edges, visibility) };
602
+ }
603
+ function getGraphPaths(adapter, input) {
604
+ const maxDepth = normalizeDepth(input.max_depth, 3);
605
+ const limit = normalizeLimit(input.limit, DEFAULT_PATH_LIMIT);
606
+ const visibility = { ...graphVisibility(input), asOfMs: input.as_of_ms };
607
+ assertRefsVisible(adapter, [input.from_ref, input.to_ref], visibility, input.as_of_ms);
608
+ const targetKey = refKey(input.to_ref);
609
+ const queue = [{ refs: [input.from_ref], edges: [] }];
610
+ const paths = [];
611
+ let edgeWork = 0;
612
+ let limitReached = false;
613
+ while (queue.length > 0 && paths.length < limit) {
614
+ const path = queue.shift();
615
+ if (!path) {
616
+ break;
617
+ }
618
+ const current = path.refs[path.refs.length - 1];
619
+ if (!current || path.edges.length >= maxDepth) {
620
+ continue;
621
+ }
622
+ const remaining = MAX_PATH_EDGE_WORK - edgeWork;
623
+ if (remaining === 0) {
624
+ limitReached = true;
625
+ break;
626
+ }
627
+ // Bound raw candidates before visibility filtering: hidden edges also cost work.
628
+ const candidates = (0, judgments_js_1.listTwinEdgesForRefs)(adapter, [current], {
629
+ newest: true,
630
+ limit: remaining + 1,
631
+ edgeTypes: input.edge_filters?.edge_types,
632
+ asOfMs: input.as_of_ms,
633
+ });
634
+ if (candidates.length > remaining)
635
+ limitReached = true;
636
+ const bounded = candidates.slice(0, remaining);
637
+ edgeWork += bounded.length;
638
+ const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, bounded.flatMap((edge) => [edge.subject_ref, edge.object_ref]), visibility);
639
+ const edges = bounded.filter((edge) => visible.has(`${edge.subject_ref.kind}\0${edge.subject_ref.id}`) &&
640
+ visible.has(`${edge.object_ref.kind}\0${edge.object_ref.id}`));
641
+ for (const edge of edges) {
642
+ for (const next of oppositeRef(edge, current)) {
643
+ if (path.refs.some((ref) => refKey(ref) === refKey(next))) {
644
+ continue;
645
+ }
646
+ const nextPath = { refs: [...path.refs, next], edges: [...path.edges, edge] };
647
+ if (refKey(next) === targetKey) {
648
+ paths.push(nextPath);
649
+ if (paths.length >= limit) {
650
+ break;
651
+ }
652
+ }
653
+ else if (nextPath.edges.length < maxDepth) {
654
+ if (queue.length < MAX_PATH_FRONTIER)
655
+ queue.push(nextPath);
656
+ else
657
+ limitReached = true;
658
+ }
659
+ }
660
+ if (paths.length >= limit) {
661
+ break;
662
+ }
663
+ }
664
+ }
665
+ const pathEdges = [
666
+ ...new Map(paths.flatMap((path) => path.edges).map((edge) => [edge.edge_id, edge])).values(),
667
+ ];
668
+ return {
669
+ paths,
670
+ limit_reached: limitReached || paths.length >= limit,
671
+ current_projection: projectCurrentEdges(adapter, pathEdges, visibility),
672
+ };
673
+ }
674
+ /**
675
+ * A case seed's timeline is its canonical merge cluster's timeline: cases
676
+ * that merged into the terminal still own their edges, and the members
677
+ * themselves appear as case events. Members the caller cannot see do not
678
+ * expand — a merge is never a scope bypass.
679
+ */
680
+ function timelineSeeds(adapter, ref, visibility) {
681
+ if (ref.kind !== 'case') {
682
+ return [ref];
683
+ }
684
+ const resolution = (0, case_store_js_1.resolveCanonicalCaseChain)(adapter, ref.id);
685
+ const cluster = (0, case_store_js_1.expandCaseChainForAssembly)(adapter, resolution.terminal_case_id, resolution.chain);
686
+ const members = cluster
687
+ .filter((id) => id !== ref.id)
688
+ .map((id) => ({ kind: 'case', id }));
689
+ if (members.length === 0) {
690
+ return [ref];
691
+ }
692
+ const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, members, visibility);
693
+ return [ref, ...members.filter((member) => visible.has(`${member.kind}\0${member.id}`))];
694
+ }
695
+ function getGraphTimeline(adapter, input) {
696
+ const visibility = graphVisibility(input);
697
+ assertRefsVisible(adapter, [input.ref], visibility, input.as_of_ms);
698
+ const limit = normalizeLimit(input.limit, DEFAULT_GRAPH_LIMIT);
699
+ const window = timelineWindow(input);
700
+ const seeds = timelineSeeds(adapter, input.ref, visibility);
701
+ const edges = listFilteredEdges(adapter, seeds, {
702
+ visibility,
703
+ edge_filters: input.edge_filters,
704
+ as_of_ms: input.as_of_ms,
705
+ }).filter((edge) => isInTimelineWindow(edge.created_at, window));
706
+ const refs = [];
707
+ const seenRefs = new Set();
708
+ for (const seed of seeds) {
709
+ addRef(refs, seenRefs, seed);
710
+ }
711
+ for (const edge of edges) {
712
+ addRef(refs, seenRefs, edge.subject_ref);
713
+ addRef(refs, seenRefs, edge.object_ref);
714
+ }
715
+ const events = [];
716
+ for (const ref of refs) {
717
+ const event = loadTimelineRecordEvent(adapter, ref);
718
+ if (event && isInTimelineWindow(event.at_ms, window)) {
719
+ events.push(event);
720
+ }
721
+ }
722
+ events.push(...edges.map((edge) => ({ kind: 'edge', at_ms: edge.created_at, edge })));
723
+ const ordered = events
724
+ .filter((event) => {
725
+ const recorded = eventRecordedAt(event);
726
+ return ((input.recorded_from_ms === undefined || recorded >= input.recorded_from_ms) &&
727
+ (input.recorded_to_ms === undefined || recorded <= input.recorded_to_ms));
728
+ })
729
+ .sort((left, right) => left.at_ms - right.at_ms ||
730
+ eventKindRank(left.kind) - eventKindRank(right.kind) ||
731
+ eventKey(left).localeCompare(eventKey(right)));
732
+ return {
733
+ ref: input.ref,
734
+ events: ordered.slice(0, limit),
735
+ has_more: ordered.length > limit,
736
+ current_projection: projectCurrentEdges(adapter, edges, visibility),
737
+ };
738
+ }
739
+ /* --------------------------------------------------------------------------
740
+ * queryGraph — the §4.1 knowledge read surface over the twin-edge graph.
741
+ *
742
+ * All five views share one boundary: seeds resolve through explicit refs or the
743
+ * three search paths the design names (registry alias, decisions FTS,
744
+ * observation metadata); every ref is checked against the caller's visibility
745
+ * before it is read; and `history` decides whether the page shows refs as
746
+ * written ('all') or folded through merges and supersession to what is current
747
+ * ('current'). Node resolution follows identity chains (registry merges,
748
+ * decision supersession); edge endpoints resolve through T4 slot assignments —
749
+ * the same original ref can point at different current nodes on different
750
+ * edges, which is what a split means.
751
+ * --------------------------------------------------------------------------
752
+ */
753
+ const SEARCH_SEED_LIMIT = 25;
754
+ const MEMORY_HISTORY_FOLLOW_LIMIT = 100;
755
+ function graphAccessInput(access) {
756
+ return {
757
+ scopes: access.scopes.map((scope) => ({ kind: scope.kind, id: scope.id })),
758
+ connectors: access.connectors ? [...access.connectors] : undefined,
759
+ connector_wide_read: access.connectorWideRead ? [...access.connectorWideRead] : undefined,
760
+ project_refs: access.projectRefs ? [...access.projectRefs] : undefined,
761
+ tenant_id: access.tenantId ?? null,
762
+ channels: access.channels ? { ...access.channels } : undefined,
763
+ principal_id: access.principalId,
764
+ agent_id: access.agentId,
765
+ max_source_ms: access.maxSourceMs,
766
+ };
767
+ }
768
+ /**
769
+ * Name-free entry: registry alias, decisions FTS, observation metadata. Search
770
+ * seeds are candidates, not citations — refs the caller cannot see are dropped
771
+ * here rather than asserted, because a search must not fail on a hit the
772
+ * caller was never allowed to read.
773
+ */
774
+ function resolveSearchSeeds(adapter, search, access, visibility) {
775
+ const text = search.text.trim();
776
+ if (!text) {
777
+ return [];
778
+ }
779
+ const wants = (kind) => !search.kinds || search.kinds.includes(kind);
780
+ const seeds = [];
781
+ if (wants('registry')) {
782
+ for (const node of (0, store_js_1.resolveAliasCandidates)(adapter, text, { scopes: access.scopes })) {
783
+ seeds.push({ kind: 'registry', id: node.id });
784
+ }
785
+ }
786
+ if (wants('memory')) {
787
+ // An absent FTS table is a real empty answer, matching knowledge/search.ts.
788
+ const ftsTable = adapter
789
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'decisions_fts'")
790
+ .get();
791
+ const terms = text.match(/[\p{L}\p{N}_]+/gu) ?? [];
792
+ if (ftsTable && terms.length > 0) {
793
+ // Preserve FTS's AND-of-terms recall without accepting user text as
794
+ // operators. A hyphenated name otherwise becomes subtraction and raises
795
+ // "no such column"; quoting the whole input would lose nonadjacent hits.
796
+ const literalTerms = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(' AND ');
797
+ const rows = adapter
798
+ .prepare(`SELECT d.id AS id
799
+ FROM decisions_fts JOIN decisions d ON decisions_fts.rowid = d.rowid
800
+ WHERE decisions_fts MATCH ?
801
+ ORDER BY rank
802
+ LIMIT ?`)
803
+ .all(literalTerms, SEARCH_SEED_LIMIT);
804
+ for (const row of rows) {
805
+ seeds.push({ kind: 'memory', id: row.id });
806
+ }
807
+ }
808
+ }
809
+ if (wants('observation')) {
810
+ const like = `%${text.replace(/[%_\\]/g, '\\$&')}%`;
811
+ const rows = adapter
812
+ .prepare(`SELECT observation_id
813
+ FROM observation_versions
814
+ WHERE source_id LIKE ? ESCAPE '\\' OR author LIKE ? ESCAPE '\\' OR metadata_json LIKE ? ESCAPE '\\'
815
+ LIMIT ?`)
816
+ .all(like, like, like, SEARCH_SEED_LIMIT);
817
+ for (const row of rows) {
818
+ seeds.push({ kind: 'observation', id: row.observation_id });
819
+ }
820
+ }
821
+ const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, seeds, visibility);
822
+ return seeds.filter((ref) => visible.has(`${ref.kind}\0${ref.id}`));
823
+ }
824
+ function resolveSeeds(adapter, query, access, visibility) {
825
+ const explicit = query.seeds ?? [];
826
+ const resolved = query.search
827
+ ? resolveSearchSeeds(adapter, query.search, access, visibility)
828
+ : [];
829
+ return { explicit, resolved };
830
+ }
831
+ /** Follow `superseded_by` to the record that currently speaks for this one. */
832
+ function currentMemoryRef(adapter, ref) {
833
+ let current = ref;
834
+ for (let hops = 0; hops < MEMORY_HISTORY_FOLLOW_LIMIT; hops += 1) {
835
+ const row = adapter
836
+ .prepare('SELECT superseded_by FROM decisions WHERE id = ?')
837
+ .get(current.id);
838
+ if (!row?.superseded_by) {
839
+ return current;
840
+ }
841
+ current = { kind: 'memory', id: row.superseded_by };
842
+ }
843
+ throw new AgentGraphValidationError(`Supersession chain from ${ref.id} does not settle within ${MEMORY_HISTORY_FOLLOW_LIMIT} hops.`);
844
+ }
845
+ /**
846
+ * The node a ref resolves to under identity corrections — registry merges for
847
+ * nodes, supersession heads for records. Returns null when the identity has no
848
+ * current node (a merge chain that settled on nothing).
849
+ */
850
+ function resolveNodeIdentity(adapter, ref) {
851
+ if (ref.kind === 'registry') {
852
+ const node = (0, store_js_1.resolveNodeById)(adapter, ref.id);
853
+ return node ? { kind: 'registry', id: node.id } : null;
854
+ }
855
+ if (ref.kind === 'memory') {
856
+ return currentMemoryRef(adapter, ref);
857
+ }
858
+ if (ref.kind === 'case') {
859
+ // A merged case folds to its canonical terminal — the same identity
860
+ // contract registry merges and memory supersession already honor. A
861
+ // missing row resolves away like any other absent record.
862
+ const exists = adapter
863
+ .prepare('SELECT 1 FROM case_truth WHERE case_id = ? LIMIT 1')
864
+ .get(ref.id);
865
+ if (!exists) {
866
+ return null;
867
+ }
868
+ const resolution = (0, case_store_js_1.resolveCanonicalCaseChain)(adapter, ref.id);
869
+ return { kind: 'case', id: resolution.terminal_case_id };
870
+ }
871
+ return ref;
872
+ }
873
+ function readRegistryRow(adapter, id) {
874
+ const row = adapter.prepare('SELECT * FROM registry_nodes WHERE id = ?').get(id);
875
+ if (!row) {
876
+ return null;
877
+ }
878
+ return {
879
+ id: row.id,
880
+ kind: row.kind,
881
+ name: row.name,
882
+ parentId: row.parent_id,
883
+ mergedInto: row.merged_into,
884
+ note: row.note,
885
+ createdAt: row.created_at,
886
+ updatedAt: row.updated_at,
887
+ };
888
+ }
889
+ function windowSectionText(ctx, ref, sections) {
890
+ const section = ctx.query.section ?? 'summary';
891
+ const full = sections[section];
892
+ const offset = ctx.query.textOffset ?? 0;
893
+ const limit = ctx.query.textLimit;
894
+ const text = limit === undefined ? full.slice(offset) : full.slice(offset, offset + limit);
895
+ const complete = offset + text.length >= full.length;
896
+ return {
897
+ text,
898
+ content: {
899
+ complete,
900
+ nextRead: complete
901
+ ? null
902
+ : {
903
+ seeds: [ref],
904
+ view: 'detail',
905
+ section,
906
+ textOffset: offset + text.length,
907
+ textLimit: limit,
908
+ },
909
+ },
910
+ };
911
+ }
912
+ function hydrateMemoryNode(adapter, ref, ctx) {
913
+ const row = adapter
914
+ .prepare(`SELECT id, topic, decision, reasoning, outcome, failure_reason, limitation, confidence,
915
+ event_date, event_datetime, created_at, updated_at, status, superseded_by,
916
+ refined_from, record_kind, kind
917
+ FROM decisions
918
+ WHERE id = ?
919
+ LIMIT 1`)
920
+ .get(ref.id);
921
+ if (!row) {
922
+ return null;
923
+ }
924
+ const recordedAt = numberMs(row.created_at, 'decisions.created_at', ref.id);
925
+ const appliesFrom = nullableNumberMs(row.event_datetime, 'decisions.event_datetime', ref.id);
926
+ let appliesUntil = null;
927
+ if (row.superseded_by) {
928
+ const superseder = adapter
929
+ .prepare('SELECT created_at FROM decisions WHERE id = ?')
930
+ .get(row.superseded_by);
931
+ if (superseder) {
932
+ appliesUntil = numberMs(superseder.created_at, 'decisions.created_at', row.superseded_by);
933
+ }
934
+ }
935
+ const workLink = adapter
936
+ .prepare(`SELECT a.commitment_id, a.revision, c.row_id, c.head_record_id, c.withdrawn
937
+ FROM commitment_assignments a
938
+ JOIN commitments c ON c.commitment_id = a.commitment_id
939
+ WHERE a.record_id = ?
940
+ ORDER BY a.revision DESC
941
+ LIMIT 1`)
942
+ .get(ref.id);
943
+ const work = workLink
944
+ ? {
945
+ commitmentId: workLink.commitment_id,
946
+ rowId: workLink.row_id,
947
+ revision: workLink.revision,
948
+ latestJudgmentRef: { kind: 'memory', id: workLink.head_record_id },
949
+ }
950
+ : null;
951
+ const stateAtSnapshot = workLink?.withdrawn === 1
952
+ ? 'withdrawn'
953
+ : row.status === 'superseded' || row.superseded_by
954
+ ? 'replaced'
955
+ : appliesFrom !== null && appliesFrom > ctx.asOfMs
956
+ ? 'not_yet_effective'
957
+ : 'current';
958
+ const refined = parseRefinedFrom({ id: ref.id, refined_from: row.refined_from });
959
+ const replaces = (Array.isArray(refined) ? refined : refined ? [refined] : []).map(String);
960
+ const payload = {};
961
+ if (row.reasoning !== null)
962
+ payload.reasoning = row.reasoning;
963
+ if (row.outcome !== null)
964
+ payload.outcome = row.outcome;
965
+ if (row.failure_reason !== null)
966
+ payload.failure_reason = row.failure_reason;
967
+ if (row.limitation !== null)
968
+ payload.limitation = row.limitation;
969
+ if (row.confidence !== null)
970
+ payload.confidence = row.confidence;
971
+ if (row.event_date !== null)
972
+ payload.event_date = row.event_date;
973
+ if (row.updated_at !== null)
974
+ payload.updated_at = row.updated_at;
975
+ const { text, content } = windowSectionText(ctx, ref, {
976
+ summary: row.decision ?? '',
977
+ reasoning: row.reasoning ?? '',
978
+ payload: JSON.stringify(payload),
979
+ });
980
+ const recordKind = row.record_kind === 'judgment' || row.record_kind === 'commitment'
981
+ ? row.record_kind
982
+ : 'legacy';
983
+ return {
984
+ label: row.topic ?? ((row.decision ?? '').slice(0, 80) || ref.id),
985
+ data: {
986
+ kind: 'memory',
987
+ recordKind,
988
+ memoryKind: row.kind,
989
+ topic: row.topic ?? '',
990
+ summary: text,
991
+ recordedAt,
992
+ appliesFrom,
993
+ appliesUntil,
994
+ stateAtSnapshot,
995
+ replaces,
996
+ payload,
997
+ work,
998
+ content,
999
+ },
1000
+ };
1001
+ }
1002
+ function hydrateRegistryNode(adapter, ref, ctx, access) {
1003
+ const resolved = (0, store_js_1.resolveNodeById)(adapter, ref.id);
1004
+ const node = ctx.history === 'all' ? (readRegistryRow(adapter, ref.id) ?? resolved) : resolved;
1005
+ if (!node) {
1006
+ return null;
1007
+ }
1008
+ const admitted = access.scopes.length > 0 ? access.scopes : [{ kind: 'global', id: '*' }];
1009
+ const aliasRows = adapter
1010
+ .prepare(`SELECT DISTINCT alias_display FROM registry_aliases
1011
+ WHERE node_id = ?
1012
+ AND ((scope_kind = 'global' AND scope_id = '*')
1013
+ OR (scope_kind || ':' || scope_id) IN (${admitted.map(() => '?').join(', ')}))
1014
+ ORDER BY created_at, rowid`)
1015
+ .all(node.id, ...admitted.map((scope) => `${scope.kind}:${scope.id}`));
1016
+ const children = (0, store_js_1.listNodes)(adapter, { parentId: node.id, scopes: access.scopes });
1017
+ const touching = listFilteredEdges(adapter, [ref], { visibility: ctx.visibility });
1018
+ const unresolved = [];
1019
+ for (const projection of projectCurrentEdges(adapter, touching, ctx.visibility)) {
1020
+ if (projection.current_ref === null && refKey(projection.original_ref) === refKey(ref)) {
1021
+ unresolved.push({ edgeId: projection.edge_id, endpoint: projection.endpoint });
1022
+ }
1023
+ }
1024
+ return {
1025
+ label: node.name,
1026
+ data: {
1027
+ kind: 'registry',
1028
+ nodeKind: node.kind,
1029
+ name: node.name,
1030
+ parentId: node.parentId,
1031
+ visibleAliases: aliasRows.map((row) => row.alias_display),
1032
+ identityRevision: (0, store_js_1.currentIdentityRevision)(adapter),
1033
+ visibleChildren: children.map((child) => ({
1034
+ kind: 'registry',
1035
+ id: child.id,
1036
+ name: child.name,
1037
+ })),
1038
+ unresolvedSlots: unresolved,
1039
+ },
1040
+ };
1041
+ }
1042
+ function hydrateObservationNode(adapter, ref) {
1043
+ const record = (0, observations_js_1.getObservationVersion)(adapter, ref.id);
1044
+ if (!record) {
1045
+ return null;
1046
+ }
1047
+ return {
1048
+ label: `${record.source}:${record.sourceId}`,
1049
+ data: {
1050
+ kind: 'observation',
1051
+ connector: record.source,
1052
+ sourceId: record.sourceId,
1053
+ sourceAt: record.sourceAt,
1054
+ observedAt: record.observedAt,
1055
+ contentHash: record.contentHash,
1056
+ },
1057
+ };
1058
+ }
1059
+ function hydrateRecordNode(adapter, ref) {
1060
+ if (ref.kind === 'case') {
1061
+ const row = adapter
1062
+ .prepare(`SELECT case_id, title, status, last_activity_at, created_at, updated_at
1063
+ FROM case_truth WHERE case_id = ? LIMIT 1`)
1064
+ .get(ref.id);
1065
+ if (!row) {
1066
+ return null;
1067
+ }
1068
+ return {
1069
+ label: String(row.title ?? ref.id),
1070
+ data: { kind: 'case', data: row },
1071
+ };
1072
+ }
1073
+ if (ref.kind === 'raw') {
1074
+ const row = adapter
1075
+ .prepare(`SELECT observation_id AS event_index_id, source AS source_connector, source_type,
1076
+ source_id, source_locator, title, source_at AS event_datetime,
1077
+ observed_at AS source_timestamp_ms, observation_id AS current_observation_id
1078
+ FROM observation_versions WHERE observation_id = ? LIMIT 1`)
1079
+ .get(ref.id);
1080
+ if (!row) {
1081
+ return null;
1082
+ }
1083
+ return {
1084
+ label: String(row.title ?? `${row.source_connector}:${row.source_id}`),
1085
+ data: { kind: 'raw', data: row },
1086
+ };
1087
+ }
1088
+ if (ref.kind === 'edge') {
1089
+ const edge = (0, judgments_js_1.getTwinEdge)(adapter, ref.id);
1090
+ if (!edge) {
1091
+ return null;
1092
+ }
1093
+ return {
1094
+ label: `${edge.edge_type}:${edge.edge_id}`,
1095
+ data: {
1096
+ kind: 'edge',
1097
+ data: {
1098
+ edge_id: edge.edge_id,
1099
+ edge_type: edge.edge_type,
1100
+ subject_ref: edge.subject_ref,
1101
+ object_ref: edge.object_ref,
1102
+ relation_attrs: (edge.relation_attrs ?? null),
1103
+ confidence: edge.confidence,
1104
+ source: edge.source,
1105
+ agent_id: edge.agent_id,
1106
+ model_run_id: edge.model_run_id,
1107
+ reason_classification: edge.reason_classification,
1108
+ reason_text: edge.reason_text,
1109
+ created_at: edge.created_at,
1110
+ },
1111
+ },
1112
+ };
1113
+ }
1114
+ // 'report' has no backing store; the ref id is the whole honest answer.
1115
+ return { label: ref.id, data: { kind: 'report', data: { id: ref.id } } };
1116
+ }
1117
+ function hydrateNode(adapter, ref, ctx, access) {
1118
+ if (ref.kind === 'memory') {
1119
+ return hydrateMemoryNode(adapter, ref, ctx);
1120
+ }
1121
+ if (ref.kind === 'registry') {
1122
+ const hydrated = hydrateRegistryNode(adapter, ref, ctx, access);
1123
+ return hydrated ? { label: hydrated.label, data: hydrated.data } : null;
1124
+ }
1125
+ if (ref.kind === 'observation') {
1126
+ return hydrateObservationNode(adapter, ref);
1127
+ }
1128
+ return hydrateRecordNode(adapter, ref);
1129
+ }
1130
+ function pageEdgeAttrs(edge) {
1131
+ return {
1132
+ edge_type: edge.edge_type,
1133
+ relation_attrs: (edge.relation_attrs ?? null),
1134
+ confidence: edge.confidence,
1135
+ source: edge.source,
1136
+ agent_id: edge.agent_id,
1137
+ model_run_id: edge.model_run_id,
1138
+ reason_classification: edge.reason_classification,
1139
+ reason_text: edge.reason_text,
1140
+ created_at: edge.created_at,
1141
+ content_hash: edge.content_hash.toString('hex'),
1142
+ };
1143
+ }
1144
+ function projectionKey(edgeId, endpoint) {
1145
+ return `${edgeId}${endpoint}`;
1146
+ }
1147
+ function mergeProjections(target, projections) {
1148
+ for (const projection of projections) {
1149
+ target.set(projectionKey(projection.edge_id, projection.endpoint), projection);
1150
+ }
1151
+ }
1152
+ function eventRecordedAt(event) {
1153
+ if (event.kind === 'edge') {
1154
+ return event.edge.created_at;
1155
+ }
1156
+ if (event.kind === 'memory') {
1157
+ return event.memory.created_at;
1158
+ }
1159
+ if (event.kind === 'case') {
1160
+ return parseTimestampMs(event.case.created_at, 'case_truth.created_at', event.ref.id);
1161
+ }
1162
+ return event.raw.source_timestamp_ms;
1163
+ }
1164
+ function validateRelations(relations) {
1165
+ if (!relations) {
1166
+ return undefined;
1167
+ }
1168
+ const known = new Set(twin_edge_types_js_1.TWIN_EDGE_TYPES);
1169
+ const unknown = relations.filter((relation) => !known.has(relation));
1170
+ if (unknown.length > 0) {
1171
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', `Unsupported graph relation(s): ${unknown.join(', ')}. Known: ${twin_edge_types_js_1.TWIN_EDGE_TYPES.join(', ')}`);
1172
+ }
1173
+ return [...new Set(relations)];
1174
+ }
1175
+ function requireSeeds(view, seeds, searchProvided) {
1176
+ // A search that resolves to nothing is a legitimate answer — the lookup's
1177
+ // found:false — not a malformed query. Only a query with no anchor at all
1178
+ // (no explicit seeds, no search) is an error.
1179
+ if (seeds.length === 0 && !searchProvided) {
1180
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', `graph ${view} requires at least one seed (seeds or search).`);
1181
+ }
1182
+ }
1183
+ function graphSnapshot(adapter, asOf) {
1184
+ const commandsTable = adapter
1185
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'judgment_commands'")
1186
+ .get();
1187
+ const judgmentWatermark = commandsTable
1188
+ ? (adapter
1189
+ .prepare('SELECT COALESCE(MAX(committed_watermark), 0) AS w FROM judgment_commands')
1190
+ .get().w ?? 0)
1191
+ : 0;
1192
+ return { judgmentWatermark, identityRevision: (0, store_js_1.currentIdentityRevision)(adapter), asOf };
1193
+ }
1194
+ function decodeBrowseCursor(value, history, relations) {
1195
+ let parsed;
1196
+ try {
1197
+ parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
1198
+ }
1199
+ catch {
1200
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor is malformed');
1201
+ }
1202
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1203
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor is malformed');
1204
+ }
1205
+ const cursor = parsed;
1206
+ if (cursor.v !== 1 ||
1207
+ typeof cursor.asOf !== 'number' ||
1208
+ !Number.isFinite(cursor.asOf) ||
1209
+ typeof cursor.afterAt !== 'number' ||
1210
+ !Number.isFinite(cursor.afterAt) ||
1211
+ typeof cursor.afterId !== 'string' ||
1212
+ cursor.afterId.length === 0 ||
1213
+ cursor.history !== history ||
1214
+ cursor.relations !== relations) {
1215
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor does not match this read');
1216
+ }
1217
+ return cursor;
1218
+ }
1219
+ function browseVisibleEdges(adapter, options) {
1220
+ const batchSize = 250;
1221
+ const maxScan = Math.max(batchSize, options.limit * 4);
1222
+ const admitted = [];
1223
+ let afterAt = options.cursor?.afterAt;
1224
+ let afterId = options.cursor?.afterId;
1225
+ let scanned = 0;
1226
+ let lastProcessed = null;
1227
+ let exhausted = false;
1228
+ const relations = options.relations ? new Set(options.relations) : null;
1229
+ while (scanned < maxScan && admitted.length < options.limit) {
1230
+ const rowLimit = Math.min(batchSize, maxScan - scanned);
1231
+ const rows = (afterAt === undefined
1232
+ ? adapter
1233
+ .prepare('SELECT edge_id, created_at FROM twin_edges WHERE created_at <= ? ORDER BY created_at DESC, edge_id ASC LIMIT ?')
1234
+ .all(options.asOf, rowLimit)
1235
+ : adapter
1236
+ .prepare('SELECT edge_id, created_at FROM twin_edges WHERE created_at <= ? AND (created_at < ? OR (created_at = ? AND edge_id > ?)) ORDER BY created_at DESC, edge_id ASC LIMIT ?')
1237
+ .all(options.asOf, afterAt, afterAt, afterId, rowLimit));
1238
+ if (rows.length === 0) {
1239
+ exhausted = true;
1240
+ break;
1241
+ }
1242
+ const candidates = rows.map((row) => (0, judgments_js_1.getTwinEdge)(adapter, row.edge_id));
1243
+ const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, candidates.flatMap((edge) => (edge ? [edge.subject_ref, edge.object_ref] : [])), { ...options.visibility, asOfMs: options.asOf });
1244
+ for (let i = 0; i < rows.length; i += 1) {
1245
+ const row = rows[i];
1246
+ const edge = candidates[i];
1247
+ lastProcessed = row;
1248
+ scanned += 1;
1249
+ if (edge &&
1250
+ (!relations || relations.has(edge.edge_type)) &&
1251
+ visible.has(`${edge.subject_ref.kind}\0${edge.subject_ref.id}`) &&
1252
+ visible.has(`${edge.object_ref.kind}\0${edge.object_ref.id}`)) {
1253
+ admitted.push(edge);
1254
+ }
1255
+ if (admitted.length >= options.limit)
1256
+ break;
1257
+ }
1258
+ if (lastProcessed) {
1259
+ afterAt = lastProcessed.created_at;
1260
+ afterId = lastProcessed.edge_id;
1261
+ }
1262
+ if (rows.length < rowLimit && admitted.length < options.limit) {
1263
+ exhausted = true;
1264
+ break;
1265
+ }
1266
+ }
1267
+ if (!lastProcessed || exhausted)
1268
+ return { edges: admitted, nextCursor: null };
1269
+ const more = adapter
1270
+ .prepare('SELECT 1 FROM twin_edges WHERE created_at <= ? AND (created_at < ? OR (created_at = ? AND edge_id > ?)) LIMIT 1')
1271
+ .get(options.asOf, lastProcessed.created_at, lastProcessed.created_at, lastProcessed.edge_id);
1272
+ const nextCursor = more
1273
+ ? Buffer.from(JSON.stringify({
1274
+ v: 1,
1275
+ asOf: options.asOf,
1276
+ afterAt: lastProcessed.created_at,
1277
+ afterId: lastProcessed.edge_id,
1278
+ history: options.history,
1279
+ relations: [...(options.relations ?? [])].sort().join(','),
1280
+ })).toString('base64url')
1281
+ : null;
1282
+ return { edges: admitted, nextCursor };
1283
+ }
1284
+ function queryGraph(adapter, query, access) {
1285
+ if (query.cursor !== undefined && query.view !== 'browse') {
1286
+ throw new judgments_js_1.JudgmentError('CURSOR_UNSUPPORTED', 'graph cursors are not issued yet; restart the query without cursor.');
1287
+ }
1288
+ const history = query.history ?? 'current';
1289
+ if (history !== 'current' && history !== 'all') {
1290
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', "history must be 'current' or 'all'");
1291
+ }
1292
+ const direction = query.direction ?? 'both';
1293
+ if (direction !== 'in' && direction !== 'out' && direction !== 'both') {
1294
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', "direction must be 'in', 'out', or 'both'");
1295
+ }
1296
+ const relationTypes = validateRelations(query.relations);
1297
+ const relationKey = [...(relationTypes ?? [])].sort().join(',');
1298
+ const browseCursor = query.view === 'browse' && query.cursor
1299
+ ? decodeBrowseCursor(query.cursor, history, relationKey)
1300
+ : null;
1301
+ if (browseCursor && query.asOf !== undefined && query.asOf !== browseCursor.asOf) {
1302
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor snapshot changed');
1303
+ }
1304
+ const accessInput = graphAccessInput(access);
1305
+ const visibility = graphVisibility({ ...accessInput, history });
1306
+ const asOf = browseCursor?.asOf ?? query.asOf ?? Date.now();
1307
+ const limit = normalizeLimit(query.limit, DEFAULT_GRAPH_LIMIT);
1308
+ const edgeFilters = relationTypes ? { edge_types: relationTypes } : undefined;
1309
+ const ctx = { visibility, history, query, asOfMs: asOf };
1310
+ const { explicit, resolved } = resolveSeeds(adapter, query, access, visibility);
1311
+ const seedRefs = [];
1312
+ const seenSeeds = new Set();
1313
+ for (const ref of [...explicit, ...resolved]) {
1314
+ if (!seenSeeds.has(refKey(ref))) {
1315
+ seenSeeds.add(refKey(ref));
1316
+ seedRefs.push(ref);
1317
+ }
1318
+ }
1319
+ const nodeRefs = [];
1320
+ const seenNodes = new Set();
1321
+ const collectedEdges = [];
1322
+ const seenEdges = new Set();
1323
+ const projections = new Map();
1324
+ const reasons = new Set();
1325
+ const nodeOrder = new Map();
1326
+ let nextCursor = null;
1327
+ let sequence = 0;
1328
+ const addRefNode = (ref, order) => {
1329
+ if (!seenNodes.has(refKey(ref))) {
1330
+ seenNodes.add(refKey(ref));
1331
+ nodeRefs.push(ref);
1332
+ }
1333
+ if (order !== undefined && !nodeOrder.has(refKey(ref))) {
1334
+ nodeOrder.set(refKey(ref), order);
1335
+ }
1336
+ };
1337
+ const addEdge = (edge) => {
1338
+ if (!seenEdges.has(edgeKey(edge))) {
1339
+ seenEdges.add(edgeKey(edge));
1340
+ collectedEdges.push(edge);
1341
+ }
1342
+ };
1343
+ switch (query.view) {
1344
+ case 'browse': {
1345
+ if (query.seeds ||
1346
+ query.search ||
1347
+ query.from ||
1348
+ query.to ||
1349
+ query.maxDepth !== undefined ||
1350
+ query.direction !== undefined ||
1351
+ query.section !== undefined ||
1352
+ query.textOffset !== undefined ||
1353
+ query.textLimit !== undefined ||
1354
+ query.eventRange !== undefined ||
1355
+ query.recordedRange !== undefined) {
1356
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse accepts only history, relations, asOf, limit and cursor');
1357
+ }
1358
+ const page = browseVisibleEdges(adapter, {
1359
+ asOf,
1360
+ limit: Math.min(limit, 2000),
1361
+ cursor: browseCursor,
1362
+ visibility,
1363
+ relations: relationTypes,
1364
+ history,
1365
+ });
1366
+ for (const edge of page.edges) {
1367
+ addRefNode(edge.subject_ref, sequence++);
1368
+ addRefNode(edge.object_ref, sequence++);
1369
+ addEdge(edge);
1370
+ }
1371
+ mergeProjections(projections, projectCurrentEdges(adapter, page.edges, visibility));
1372
+ nextCursor = page.nextCursor;
1373
+ if (nextCursor)
1374
+ reasons.add('limit_reached');
1375
+ break;
1376
+ }
1377
+ case 'neighbors': {
1378
+ requireSeeds(query.view, seedRefs, query.search !== undefined);
1379
+ if (seedRefs.length === 0) {
1380
+ reasons.add('search_no_match');
1381
+ break;
1382
+ }
1383
+ for (const seed of seedRefs) {
1384
+ const result = getGraphNeighborhood(adapter, {
1385
+ ref: seed,
1386
+ depth: query.maxDepth,
1387
+ direction,
1388
+ ...accessInput,
1389
+ history,
1390
+ edge_filters: edgeFilters,
1391
+ as_of_ms: query.asOf ?? null,
1392
+ limit,
1393
+ });
1394
+ for (const ref of result.nodes)
1395
+ addRefNode(ref, sequence++);
1396
+ for (const edge of result.edges)
1397
+ addEdge(edge);
1398
+ mergeProjections(projections, result.current_projection);
1399
+ if (result.edges.length >= limit)
1400
+ reasons.add('limit_reached');
1401
+ }
1402
+ break;
1403
+ }
1404
+ case 'paths': {
1405
+ if (!query.from || !query.to) {
1406
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph paths requires from and to refs.');
1407
+ }
1408
+ const result = getGraphPaths(adapter, {
1409
+ from_ref: query.from,
1410
+ to_ref: query.to,
1411
+ max_depth: query.maxDepth,
1412
+ ...accessInput,
1413
+ history,
1414
+ edge_filters: edgeFilters,
1415
+ as_of_ms: query.asOf ?? null,
1416
+ limit,
1417
+ });
1418
+ if (result.limit_reached)
1419
+ reasons.add('limit_reached');
1420
+ for (const path of result.paths) {
1421
+ for (const ref of path.refs)
1422
+ addRefNode(ref, sequence++);
1423
+ for (const edge of path.edges)
1424
+ addEdge(edge);
1425
+ }
1426
+ mergeProjections(projections, result.current_projection);
1427
+ break;
1428
+ }
1429
+ case 'timeline': {
1430
+ requireSeeds(query.view, seedRefs, query.search !== undefined);
1431
+ if (seedRefs.length === 0) {
1432
+ reasons.add('search_no_match');
1433
+ break;
1434
+ }
1435
+ const merged = new Map();
1436
+ for (const seed of seedRefs) {
1437
+ const result = getGraphTimeline(adapter, {
1438
+ ref: seed,
1439
+ ...accessInput,
1440
+ history,
1441
+ edge_filters: edgeFilters,
1442
+ from_ms: query.eventRange?.start,
1443
+ to_ms: query.eventRange?.end,
1444
+ recorded_from_ms: query.recordedRange?.start,
1445
+ recorded_to_ms: query.recordedRange?.end,
1446
+ as_of_ms: query.asOf ?? null,
1447
+ limit,
1448
+ });
1449
+ if (result.has_more)
1450
+ reasons.add('limit_reached');
1451
+ for (const event of result.events) {
1452
+ merged.set(`${event.kind}${eventKey(event)}`, event);
1453
+ }
1454
+ mergeProjections(projections, result.current_projection);
1455
+ }
1456
+ let events = [...merged.values()];
1457
+ events.sort((left, right) => left.at_ms - right.at_ms ||
1458
+ eventKindRank(left.kind) - eventKindRank(right.kind) ||
1459
+ eventKey(left).localeCompare(eventKey(right)));
1460
+ if (events.length > limit)
1461
+ reasons.add('limit_reached');
1462
+ events = events.slice(0, limit);
1463
+ for (const seed of seedRefs)
1464
+ addRefNode(seed, -1);
1465
+ for (const event of events) {
1466
+ if (event.kind === 'edge') {
1467
+ addEdge(event.edge);
1468
+ }
1469
+ else {
1470
+ addRefNode(event.ref, event.at_ms);
1471
+ }
1472
+ }
1473
+ break;
1474
+ }
1475
+ case 'detail': {
1476
+ requireSeeds(query.view, seedRefs, query.search !== undefined);
1477
+ if (seedRefs.length > 0) {
1478
+ assertRefsVisible(adapter, seedRefs, visibility, query.asOf ?? null);
1479
+ for (const seed of seedRefs)
1480
+ addRefNode(seed, sequence++);
1481
+ }
1482
+ else {
1483
+ reasons.add('search_no_match');
1484
+ }
1485
+ break;
1486
+ }
1487
+ case 'overview': {
1488
+ // The name-free entry point: the registry's visible roots are the named
1489
+ // anchors a caller picks before narrowing into neighbors/detail.
1490
+ const roots = (0, store_js_1.listNodes)(adapter, { parentId: null, scopes: access.scopes });
1491
+ if (roots.length > limit)
1492
+ reasons.add('limit_reached');
1493
+ for (const node of roots.slice(0, limit)) {
1494
+ addRefNode({ kind: 'registry', id: node.id }, sequence++);
1495
+ }
1496
+ break;
1497
+ }
1498
+ default: {
1499
+ throw new judgments_js_1.JudgmentError('INVALID_QUERY', `graph view must be one of overview, browse, neighbors, timeline, paths, detail; got ${String(query.view)}.`);
1500
+ }
1501
+ }
1502
+ const nodes = [];
1503
+ const emitted = new Set();
1504
+ for (const original of nodeRefs) {
1505
+ const resolved = resolveNodeIdentity(adapter, original);
1506
+ if (history === 'current') {
1507
+ if (resolved === null) {
1508
+ reasons.add('resolved_away');
1509
+ continue;
1510
+ }
1511
+ const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, [resolved], {
1512
+ ...visibility,
1513
+ asOfMs: query.asOf ?? null,
1514
+ });
1515
+ if (!visible.has(`${resolved.kind}\0${resolved.id}`)) {
1516
+ reasons.add('resolved_not_visible');
1517
+ continue;
1518
+ }
1519
+ if (emitted.has(refKey(resolved))) {
1520
+ continue;
1521
+ }
1522
+ emitted.add(refKey(resolved));
1523
+ const hydrated = hydrateNode(adapter, resolved, ctx, access);
1524
+ if (!hydrated) {
1525
+ reasons.add('records_missing');
1526
+ continue;
1527
+ }
1528
+ if (hydrated.data.kind === 'memory' &&
1529
+ (hydrated.data.stateAtSnapshot === 'replaced' ||
1530
+ hydrated.data.stateAtSnapshot === 'withdrawn')) {
1531
+ reasons.add('non_current_omitted');
1532
+ continue;
1533
+ }
1534
+ nodes.push({
1535
+ ref: resolved,
1536
+ resolvedRef: resolved,
1537
+ label: hydrated.label,
1538
+ data: hydrated.data,
1539
+ });
1540
+ }
1541
+ else {
1542
+ const hydrated = hydrateNode(adapter, original, ctx, access);
1543
+ if (!hydrated) {
1544
+ reasons.add('records_missing');
1545
+ continue;
1546
+ }
1547
+ nodes.push({
1548
+ ref: original,
1549
+ resolvedRef: resolved ?? original,
1550
+ label: hydrated.label,
1551
+ data: hydrated.data,
1552
+ });
1553
+ }
1554
+ }
1555
+ if (query.view === 'timeline' || nodeOrder.size > 0) {
1556
+ nodes.sort((left, right) => (nodeOrder.get(`${left.ref.kind}:${left.ref.id}`) ?? Number.MAX_SAFE_INTEGER) -
1557
+ (nodeOrder.get(`${right.ref.kind}:${right.ref.id}`) ?? Number.MAX_SAFE_INTEGER));
1558
+ }
1559
+ const pageEdges = collectedEdges.map((edge) => {
1560
+ const fromProjection = projections.get(projectionKey(edge.edge_id, 'from'));
1561
+ const toProjection = projections.get(projectionKey(edge.edge_id, 'to'));
1562
+ return {
1563
+ id: edge.edge_id,
1564
+ relation: edge.edge_type,
1565
+ from: edge.subject_ref,
1566
+ to: edge.object_ref,
1567
+ resolvedFrom: fromProjection?.current_ref ?? edge.subject_ref,
1568
+ resolvedTo: toProjection?.current_ref ?? edge.object_ref,
1569
+ attrs: pageEdgeAttrs(edge),
1570
+ };
1571
+ });
1572
+ return {
1573
+ nodes,
1574
+ edges: pageEdges,
1575
+ coverage: {
1576
+ returned: nodes.length,
1577
+ total: null,
1578
+ complete: reasons.size === 0,
1579
+ reasons: [...reasons],
1580
+ },
1581
+ snapshot: graphSnapshot(adapter, asOf),
1582
+ nextCursor,
1583
+ };
1584
+ }
1585
+ //# sourceMappingURL=graph-query.js.map