@jungjaehoon/mama-core 1.4.2 → 1.6.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 (181) hide show
  1. package/README.md +48 -10
  2. package/db/migrations/026-create-canonical-entity-tables.sql +99 -0
  3. package/db/migrations/027-create-entity-review-support.sql +44 -0
  4. package/db/migrations/028-create-entity-audit-tables.sql +45 -0
  5. package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
  6. package/db/migrations/030-case-first-memory-substrate.sql +901 -0
  7. package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
  8. package/db/migrations/032-add-memory-provenance-columns.sql +21 -0
  9. package/db/migrations/033-create-model-runs-and-tool-traces.sql +53 -0
  10. package/db/migrations/034-add-connector-event-scope-columns.sql +14 -0
  11. package/db/migrations/035-create-twin-edges.sql +63 -0
  12. package/db/migrations/036-create-agent-situation-packets.sql +61 -0
  13. package/dist/agent-graph/alias-write.d.ts +3 -0
  14. package/dist/agent-graph/alias-write.js +231 -0
  15. package/dist/agent-graph/entity-resolve.d.ts +3 -0
  16. package/dist/agent-graph/entity-resolve.js +275 -0
  17. package/dist/agent-graph/errors.d.ts +4 -0
  18. package/dist/agent-graph/errors.js +11 -0
  19. package/dist/agent-graph/graph-query.d.ts +5 -0
  20. package/dist/agent-graph/graph-query.js +359 -0
  21. package/dist/agent-graph/index.d.ts +6 -0
  22. package/dist/agent-graph/index.js +22 -0
  23. package/dist/agent-graph/types.d.ts +162 -0
  24. package/dist/agent-graph/types.js +3 -0
  25. package/dist/agent-situation/builder.d.ts +8 -0
  26. package/dist/agent-situation/builder.js +239 -0
  27. package/dist/agent-situation/cache-key.d.ts +4 -0
  28. package/dist/agent-situation/cache-key.js +64 -0
  29. package/dist/agent-situation/index.d.ts +7 -0
  30. package/dist/agent-situation/index.js +28 -0
  31. package/dist/agent-situation/packet-store.d.ts +27 -0
  32. package/dist/agent-situation/packet-store.js +228 -0
  33. package/dist/agent-situation/ranking-policy.d.ts +18 -0
  34. package/dist/agent-situation/ranking-policy.js +75 -0
  35. package/dist/agent-situation/source-readers.d.ts +52 -0
  36. package/dist/agent-situation/source-readers.js +300 -0
  37. package/dist/agent-situation/types.d.ts +195 -0
  38. package/dist/agent-situation/types.js +3 -0
  39. package/dist/canonicalize.d.ts +38 -0
  40. package/dist/canonicalize.js +106 -0
  41. package/dist/cases/case-links.d.ts +116 -0
  42. package/dist/cases/case-links.js +794 -0
  43. package/dist/cases/composition-overrides.d.ts +90 -0
  44. package/dist/cases/composition-overrides.js +724 -0
  45. package/dist/cases/corrections.d.ts +156 -0
  46. package/dist/cases/corrections.js +1045 -0
  47. package/dist/cases/freshness.d.ts +57 -0
  48. package/dist/cases/freshness.js +283 -0
  49. package/dist/cases/live-state.d.ts +48 -0
  50. package/dist/cases/live-state.js +256 -0
  51. package/dist/cases/membership-explain.d.ts +56 -0
  52. package/dist/cases/membership-explain.js +140 -0
  53. package/dist/cases/membership-matcher.d.ts +23 -0
  54. package/dist/cases/membership-matcher.js +193 -0
  55. package/dist/cases/merge-split.d.ts +61 -0
  56. package/dist/cases/merge-split.js +368 -0
  57. package/dist/cases/role-inference.d.ts +10 -0
  58. package/dist/cases/role-inference.js +85 -0
  59. package/dist/cases/search-rollup.d.ts +27 -0
  60. package/dist/cases/search-rollup.js +309 -0
  61. package/dist/cases/sqlite-transaction.d.ts +10 -0
  62. package/dist/cases/sqlite-transaction.js +62 -0
  63. package/dist/cases/store.d.ts +74 -0
  64. package/dist/cases/store.js +816 -0
  65. package/dist/cases/target-ref.d.ts +25 -0
  66. package/dist/cases/target-ref.js +22 -0
  67. package/dist/cases/timeline-range.d.ts +6 -0
  68. package/dist/cases/timeline-range.js +322 -0
  69. package/dist/cases/tombstone-sweeper.d.ts +13 -0
  70. package/dist/cases/tombstone-sweeper.js +128 -0
  71. package/dist/cases/types.d.ts +244 -0
  72. package/dist/cases/types.js +47 -0
  73. package/dist/cases/wiki-page-index.d.ts +50 -0
  74. package/dist/cases/wiki-page-index.js +350 -0
  75. package/dist/connectors/event-index.d.ts +45 -0
  76. package/dist/connectors/event-index.js +381 -0
  77. package/dist/connectors/raw-query.d.ts +18 -0
  78. package/dist/connectors/raw-query.js +305 -0
  79. package/dist/connectors/types.d.ts +109 -0
  80. package/dist/connectors/types.js +3 -0
  81. package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -0
  82. package/dist/db-adapter/node-sqlite-adapter.js +131 -0
  83. package/dist/db-manager.d.ts +12 -1
  84. package/dist/db-manager.js +60 -5
  85. package/dist/decision-formatter.d.ts +2 -0
  86. package/dist/decision-formatter.js +11 -8
  87. package/dist/edges/ref-validation.d.ts +8 -0
  88. package/dist/edges/ref-validation.js +262 -0
  89. package/dist/edges/store.d.ts +10 -0
  90. package/dist/edges/store.js +343 -0
  91. package/dist/edges/types.d.ts +86 -0
  92. package/dist/edges/types.js +18 -0
  93. package/dist/embedding-server/mobile/daemon.d.ts +1 -0
  94. package/dist/embedding-server/mobile/daemon.js +11 -1
  95. package/dist/embeddings.js +13 -0
  96. package/dist/entities/audit-metrics.d.ts +73 -0
  97. package/dist/entities/audit-metrics.js +128 -0
  98. package/dist/entities/candidate-generator.d.ts +9 -0
  99. package/dist/entities/candidate-generator.js +436 -0
  100. package/dist/entities/entity-impact.d.ts +41 -0
  101. package/dist/entities/entity-impact.js +140 -0
  102. package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
  103. package/dist/entities/entity-linked-decision-counts.js +29 -0
  104. package/dist/entities/entity-list.d.ts +23 -0
  105. package/dist/entities/entity-list.js +99 -0
  106. package/dist/entities/entity-orphan-list.d.ts +26 -0
  107. package/dist/entities/entity-orphan-list.js +91 -0
  108. package/dist/entities/entity-search.d.ts +21 -0
  109. package/dist/entities/entity-search.js +150 -0
  110. package/dist/entities/errors.d.ts +65 -0
  111. package/dist/entities/errors.js +124 -0
  112. package/dist/entities/exact-merge-backfill.d.ts +11 -0
  113. package/dist/entities/exact-merge-backfill.js +161 -0
  114. package/dist/entities/lineage-backfill.d.ts +12 -0
  115. package/dist/entities/lineage-backfill.js +101 -0
  116. package/dist/entities/lineage-store.d.ts +51 -0
  117. package/dist/entities/lineage-store.js +308 -0
  118. package/dist/entities/normalization.d.ts +14 -0
  119. package/dist/entities/normalization.js +63 -0
  120. package/dist/entities/policy-store.d.ts +17 -0
  121. package/dist/entities/policy-store.js +263 -0
  122. package/dist/entities/policy-types.d.ts +72 -0
  123. package/dist/entities/policy-types.js +59 -0
  124. package/dist/entities/projection.d.ts +8 -0
  125. package/dist/entities/projection.js +82 -0
  126. package/dist/entities/provenance-query.d.ts +33 -0
  127. package/dist/entities/provenance-query.js +167 -0
  128. package/dist/entities/read-identity.d.ts +31 -0
  129. package/dist/entities/read-identity.js +264 -0
  130. package/dist/entities/recall-bridge.d.ts +5 -0
  131. package/dist/entities/recall-bridge.js +171 -0
  132. package/dist/entities/resolution-engine.d.ts +15 -0
  133. package/dist/entities/resolution-engine.js +69 -0
  134. package/dist/entities/rollback-preview.d.ts +39 -0
  135. package/dist/entities/rollback-preview.js +235 -0
  136. package/dist/entities/source-locator.d.ts +3 -0
  137. package/dist/entities/source-locator.js +32 -0
  138. package/dist/entities/store.d.ts +75 -0
  139. package/dist/entities/store.js +465 -0
  140. package/dist/entities/types.d.ts +162 -0
  141. package/dist/entities/types.js +30 -0
  142. package/dist/index.d.ts +62 -3
  143. package/dist/index.js +116 -26
  144. package/dist/mama-api.d.ts +53 -35
  145. package/dist/mama-api.js +437 -78
  146. package/dist/memory/api.d.ts +26 -38
  147. package/dist/memory/api.js +772 -68
  148. package/dist/memory/event-store.d.ts +3 -0
  149. package/dist/memory/event-store.js +21 -2
  150. package/dist/memory/provenance-audit.d.ts +22 -0
  151. package/dist/memory/provenance-audit.js +112 -0
  152. package/dist/memory/provenance-query.d.ts +11 -0
  153. package/dist/memory/provenance-query.js +193 -0
  154. package/dist/memory/provenance.d.ts +29 -0
  155. package/dist/memory/provenance.js +136 -0
  156. package/dist/memory/scope-backfill.d.ts +17 -0
  157. package/dist/memory/scope-backfill.js +77 -0
  158. package/dist/memory/types.d.ts +97 -16
  159. package/dist/model-runs/store.d.ts +13 -0
  160. package/dist/model-runs/store.js +457 -0
  161. package/dist/model-runs/tool-trace-store.d.ts +4 -0
  162. package/dist/model-runs/tool-trace-store.js +114 -0
  163. package/dist/model-runs/types.d.ts +69 -0
  164. package/dist/model-runs/types.js +5 -0
  165. package/dist/runtime/trusted-provenance.d.ts +3 -0
  166. package/dist/runtime/trusted-provenance.js +6 -0
  167. package/dist/search/feedback-store.d.ts +59 -0
  168. package/dist/search/feedback-store.js +211 -0
  169. package/dist/search/question-type.d.ts +5 -0
  170. package/dist/search/question-type.js +41 -0
  171. package/dist/search/ranker-features.d.ts +23 -0
  172. package/dist/search/ranker-features.js +104 -0
  173. package/dist/search/ranker-rescore.d.ts +31 -0
  174. package/dist/search/ranker-rescore.js +226 -0
  175. package/dist/search/ranker-trainer.d.ts +94 -0
  176. package/dist/search/ranker-trainer.js +860 -0
  177. package/dist/search/search-quality.d.ts +36 -0
  178. package/dist/search/search-quality.js +29 -0
  179. package/dist/test-utils.d.ts +14 -0
  180. package/dist/test-utils.js +52 -0
  181. package/package.json +28 -3
@@ -0,0 +1,816 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCanonicalCaseChain = resolveCanonicalCaseChain;
4
+ exports.expandCaseChainForAssembly = expandCaseChainForAssembly;
5
+ exports.upsertCaseTruthSlowFields = upsertCaseTruthSlowFields;
6
+ exports.upsertExplicitCaseMemberships = upsertExplicitCaseMemberships;
7
+ exports.listActiveMembershipsForCaseChain = listActiveMembershipsForCaseChain;
8
+ exports.listActiveCorrectionsForCase = listActiveCorrectionsForCase;
9
+ exports.listActiveCorrectionsForCaseChain = listActiveCorrectionsForCaseChain;
10
+ exports.enqueueCaseProposal = enqueueCaseProposal;
11
+ exports.listUnresolvedCaseProposals = listUnresolvedCaseProposals;
12
+ exports.assembleCase = assembleCase;
13
+ const node_crypto_1 = require("node:crypto");
14
+ const canonicalize_js_1 = require("../canonicalize.js");
15
+ const db_manager_js_1 = require("../db-manager.js");
16
+ const errors_js_1 = require("../entities/errors.js");
17
+ function isAdapter(value) {
18
+ return (typeof value === 'object' &&
19
+ value !== null &&
20
+ typeof value.prepare === 'function' &&
21
+ typeof value.transaction === 'function');
22
+ }
23
+ function resolveWriteArgs(adapterOrInput, input) {
24
+ if (input !== undefined) {
25
+ if (!isAdapter(adapterOrInput)) {
26
+ throw new Error('First argument must be a database adapter when input is provided.');
27
+ }
28
+ return { adapter: adapterOrInput, input };
29
+ }
30
+ if (isAdapter(adapterOrInput)) {
31
+ throw new Error('Input is required when first argument is a database adapter.');
32
+ }
33
+ return { adapter: (0, db_manager_js_1.getAdapter)(), input: adapterOrInput };
34
+ }
35
+ function resolveAssembleArgs(adapterOrCaseId, caseIdOrOptions, options) {
36
+ if (isAdapter(adapterOrCaseId)) {
37
+ if (typeof caseIdOrOptions !== 'string') {
38
+ throw new Error('caseId is required when first argument is a database adapter.');
39
+ }
40
+ return { adapter: adapterOrCaseId, caseId: caseIdOrOptions, options };
41
+ }
42
+ return {
43
+ adapter: (0, db_manager_js_1.getAdapter)(),
44
+ caseId: adapterOrCaseId,
45
+ options: typeof caseIdOrOptions === 'object' ? caseIdOrOptions : undefined,
46
+ };
47
+ }
48
+ function nowIso() {
49
+ return new Date().toISOString();
50
+ }
51
+ function nullableString(value) {
52
+ if (value === null || value === undefined) {
53
+ return null;
54
+ }
55
+ return String(value);
56
+ }
57
+ function nullableNumber(value) {
58
+ if (value === null || value === undefined) {
59
+ return null;
60
+ }
61
+ const n = Number(value);
62
+ return Number.isFinite(n) ? n : null;
63
+ }
64
+ function flag01(value) {
65
+ return Number(value) === 1 ? 1 : 0;
66
+ }
67
+ function toJsonText(value) {
68
+ if (value === null || value === undefined) {
69
+ return null;
70
+ }
71
+ if (typeof value === 'string') {
72
+ return value;
73
+ }
74
+ return JSON.stringify(value);
75
+ }
76
+ function parseJsonArray(value) {
77
+ if (value === null || value === '') {
78
+ return [];
79
+ }
80
+ const parsed = JSON.parse(value);
81
+ if (!Array.isArray(parsed)) {
82
+ throw new Error('Expected JSON array in case_truth JSON field.');
83
+ }
84
+ return parsed;
85
+ }
86
+ function parseTargetRef(value) {
87
+ const parsed = JSON.parse(value);
88
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
89
+ throw new Error('case_corrections.target_ref_json must decode to a JSON object.');
90
+ }
91
+ return parsed;
92
+ }
93
+ function toBuffer(value) {
94
+ if (Buffer.isBuffer(value)) {
95
+ return value;
96
+ }
97
+ if (value instanceof Uint8Array) {
98
+ return Buffer.from(value);
99
+ }
100
+ throw new Error('Expected SQLite BLOB value to be a Buffer.');
101
+ }
102
+ function placeholders(values) {
103
+ if (values.length === 0) {
104
+ throw new Error('Cannot build SQL IN clause for an empty value list.');
105
+ }
106
+ return values.map(() => '?').join(', ');
107
+ }
108
+ function sourceKey(sourceType, sourceId) {
109
+ return `${sourceType}:${sourceId}`;
110
+ }
111
+ function timestampMs(value) {
112
+ if (value === null || value === undefined) {
113
+ return null;
114
+ }
115
+ if (typeof value === 'number') {
116
+ return Number.isFinite(value) ? value : null;
117
+ }
118
+ const text = String(value);
119
+ if (text.length === 0) {
120
+ return null;
121
+ }
122
+ const numeric = Number(text);
123
+ if (Number.isFinite(numeric) && /^\d+$/.test(text)) {
124
+ return numeric;
125
+ }
126
+ const parsed = Date.parse(text);
127
+ return Number.isNaN(parsed) ? null : parsed;
128
+ }
129
+ function formatTimestamp(value) {
130
+ const ms = timestampMs(value);
131
+ if (ms !== null) {
132
+ return new Date(ms).toISOString();
133
+ }
134
+ return value === null || value === undefined ? '' : String(value);
135
+ }
136
+ function compareByUpdatedAtDesc(left, right) {
137
+ const leftMs = timestampMs(left) ?? 0;
138
+ const rightMs = timestampMs(right) ?? 0;
139
+ return rightMs - leftMs;
140
+ }
141
+ function compareBySortTimeDesc(left, right) {
142
+ return (right.sort_time ?? 0) - (left.sort_time ?? 0);
143
+ }
144
+ function isMissingSchemaError(error) {
145
+ return error instanceof Error && /no such (table|column)/i.test(error.message);
146
+ }
147
+ function rethrowUnlessMissingSchema(error) {
148
+ if (isMissingSchemaError(error)) {
149
+ return;
150
+ }
151
+ throw error instanceof Error ? error : new Error(String(error));
152
+ }
153
+ function withinSince(sortTime, sinceMs) {
154
+ return sinceMs === null || sortTime === null || sortTime >= sinceMs;
155
+ }
156
+ function resultLimit(options) {
157
+ const limit = options?.limit ?? 30;
158
+ return Math.max(0, Math.floor(limit));
159
+ }
160
+ function mapCaseTruthRow(row) {
161
+ return {
162
+ case_id: String(row.case_id),
163
+ current_wiki_path: nullableString(row.current_wiki_path),
164
+ title: String(row.title),
165
+ status: row.status,
166
+ status_reason: nullableString(row.status_reason),
167
+ primary_actors: nullableString(row.primary_actors),
168
+ blockers: nullableString(row.blockers),
169
+ last_activity_at: nullableString(row.last_activity_at),
170
+ canonical_case_id: nullableString(row.canonical_case_id),
171
+ split_from_case_id: nullableString(row.split_from_case_id),
172
+ wiki_path_history: nullableString(row.wiki_path_history),
173
+ scope_refs: nullableString(row.scope_refs),
174
+ confidence: nullableString(row.confidence),
175
+ compiled_at: nullableString(row.compiled_at),
176
+ state_updated_at: nullableString(row.state_updated_at),
177
+ created_at: String(row.created_at),
178
+ updated_at: String(row.updated_at),
179
+ };
180
+ }
181
+ function mapCaseMembershipRow(row) {
182
+ return {
183
+ case_id: String(row.case_id),
184
+ source_type: row.source_type,
185
+ source_id: String(row.source_id),
186
+ role: nullableString(row.role),
187
+ confidence: nullableNumber(row.confidence),
188
+ reason: nullableString(row.reason),
189
+ status: row.status,
190
+ added_by: row.added_by,
191
+ added_at: String(row.added_at),
192
+ updated_at: String(row.updated_at),
193
+ user_locked: flag01(row.user_locked),
194
+ };
195
+ }
196
+ function mapCaseCorrectionRow(row) {
197
+ return {
198
+ correction_id: String(row.correction_id),
199
+ case_id: String(row.case_id),
200
+ target_kind: row.target_kind,
201
+ target_ref_json: String(row.target_ref_json),
202
+ target_ref_hash: toBuffer(row.target_ref_hash),
203
+ field_name: nullableString(row.field_name),
204
+ old_value_json: nullableString(row.old_value_json),
205
+ new_value_json: String(row.new_value_json),
206
+ reason: String(row.reason),
207
+ is_lock_active: flag01(row.is_lock_active),
208
+ superseded_by: nullableString(row.superseded_by),
209
+ reverted_at: nullableString(row.reverted_at),
210
+ applied_by: String(row.applied_by),
211
+ applied_at: String(row.applied_at),
212
+ };
213
+ }
214
+ function mapCaseProposalRow(row) {
215
+ return {
216
+ proposal_id: String(row.proposal_id),
217
+ project: String(row.project),
218
+ proposal_kind: row.proposal_kind,
219
+ proposed_payload: String(row.proposed_payload),
220
+ payload_fingerprint: toBuffer(row.payload_fingerprint),
221
+ conflicting_case_id: nullableString(row.conflicting_case_id),
222
+ detected_at: String(row.detected_at),
223
+ resolved_at: nullableString(row.resolved_at),
224
+ resolution: nullableString(row.resolution),
225
+ resolution_note: nullableString(row.resolution_note),
226
+ };
227
+ }
228
+ function loadCaseTruth(adapter, caseId) {
229
+ const row = adapter.prepare('SELECT * FROM case_truth WHERE case_id = ?').get(caseId);
230
+ return row ? mapCaseTruthRow(row) : null;
231
+ }
232
+ function dedupeMembershipRecords(rows) {
233
+ const sorted = [...rows].sort((left, right) => {
234
+ if (left.user_locked !== right.user_locked) {
235
+ return right.user_locked - left.user_locked;
236
+ }
237
+ return compareByUpdatedAtDesc(left.updated_at, right.updated_at);
238
+ });
239
+ const seen = new Set();
240
+ const deduped = [];
241
+ for (const row of sorted) {
242
+ const key = sourceKey(row.source_type, row.source_id);
243
+ if (seen.has(key)) {
244
+ continue;
245
+ }
246
+ seen.add(key);
247
+ deduped.push(row);
248
+ }
249
+ return deduped;
250
+ }
251
+ function listActiveMembershipRecordsForCaseChain(adapter, caseIds) {
252
+ if (caseIds.length === 0) {
253
+ return [];
254
+ }
255
+ const rows = adapter
256
+ .prepare(`
257
+ SELECT *
258
+ FROM case_memberships
259
+ WHERE case_id IN (${placeholders(caseIds)})
260
+ AND status = 'active'
261
+ `)
262
+ .all(...caseIds);
263
+ return dedupeMembershipRecords(rows.map(mapCaseMembershipRow));
264
+ }
265
+ function toAssemblyMembership(row) {
266
+ return {
267
+ source_type: row.source_type,
268
+ source_id: row.source_id,
269
+ role: row.role,
270
+ confidence: row.confidence,
271
+ reason: row.reason,
272
+ user_locked: row.user_locked === 1,
273
+ };
274
+ }
275
+ function resolveCanonicalCaseChain(adapter, caseId, maxDepth = 64) {
276
+ const visited = new Set();
277
+ const chain = [];
278
+ let current = caseId;
279
+ for (;;) {
280
+ const detectedAtDepth = chain.length + 1;
281
+ const cycleChain = [...chain, current];
282
+ if (visited.has(current)) {
283
+ throw new errors_js_1.CaseMergeChainCycleError({
284
+ case_id: caseId,
285
+ chain: cycleChain,
286
+ detected_at_depth: detectedAtDepth,
287
+ });
288
+ }
289
+ if (detectedAtDepth > maxDepth) {
290
+ throw new errors_js_1.CaseMergeChainCycleError({
291
+ case_id: caseId,
292
+ chain: cycleChain,
293
+ detected_at_depth: detectedAtDepth,
294
+ });
295
+ }
296
+ visited.add(current);
297
+ chain.push(current);
298
+ const row = adapter
299
+ .prepare('SELECT canonical_case_id FROM case_truth WHERE case_id = ?')
300
+ .get(current);
301
+ if (!row) {
302
+ throw new Error(`Case ${current} not found while resolving canonical case_id for ${caseId}.`);
303
+ }
304
+ if (!row.canonical_case_id) {
305
+ return {
306
+ terminal_case_id: current,
307
+ chain,
308
+ resolved_via_case_id: current === caseId ? null : caseId,
309
+ };
310
+ }
311
+ current = row.canonical_case_id;
312
+ }
313
+ }
314
+ function expandCaseChainForAssembly(adapter, terminalCaseId, resolvedChain) {
315
+ const rows = adapter
316
+ .prepare(`
317
+ WITH RECURSIVE merged_chain(case_id, depth) AS (
318
+ SELECT case_id, 0
319
+ FROM case_truth
320
+ WHERE case_id = ?
321
+
322
+ UNION
323
+
324
+ SELECT ct.case_id, merged_chain.depth + 1
325
+ FROM case_truth ct
326
+ JOIN merged_chain ON ct.canonical_case_id = merged_chain.case_id
327
+ WHERE merged_chain.depth < 64
328
+ )
329
+ SELECT case_id
330
+ FROM merged_chain
331
+ ORDER BY depth ASC, case_id ASC
332
+ `)
333
+ .all(terminalCaseId);
334
+ const chain = [];
335
+ for (const caseId of [...resolvedChain, ...rows.map((row) => row.case_id)]) {
336
+ if (!chain.includes(caseId)) {
337
+ chain.push(caseId);
338
+ }
339
+ }
340
+ return chain;
341
+ }
342
+ function upsertCaseTruthSlowFields(adapterOrInput, maybeInput) {
343
+ const { adapter, input } = resolveWriteArgs(adapterOrInput, maybeInput);
344
+ const createdAt = nowIso();
345
+ const updatedAt = createdAt;
346
+ return adapter.transaction(() => {
347
+ adapter
348
+ .prepare(`
349
+ INSERT INTO case_truth (
350
+ case_id, current_wiki_path, title, status_reason, primary_actors, blockers,
351
+ wiki_path_history, scope_refs, confidence, compiled_at, created_at, updated_at
352
+ )
353
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
354
+ ON CONFLICT(case_id) DO UPDATE SET
355
+ current_wiki_path = excluded.current_wiki_path,
356
+ title = excluded.title,
357
+ status_reason = excluded.status_reason,
358
+ primary_actors = excluded.primary_actors,
359
+ blockers = excluded.blockers,
360
+ wiki_path_history = excluded.wiki_path_history,
361
+ scope_refs = excluded.scope_refs,
362
+ confidence = excluded.confidence,
363
+ compiled_at = excluded.compiled_at,
364
+ updated_at = excluded.updated_at
365
+ `)
366
+ .run(input.case_id, input.current_wiki_path ?? null, input.title, input.status_reason ?? null, toJsonText(input.primary_actors), toJsonText(input.blockers), toJsonText(input.wiki_path_history), toJsonText(input.scope_refs), input.confidence ?? null, input.compiled_at, createdAt, updatedAt);
367
+ const saved = loadCaseTruth(adapter, input.case_id);
368
+ if (!saved) {
369
+ throw new Error(`Failed to load case_truth row after upsert for case_id=${input.case_id}.`);
370
+ }
371
+ return saved;
372
+ });
373
+ }
374
+ function upsertExplicitCaseMemberships(adapterOrInput, maybeInput) {
375
+ const { adapter, input } = resolveWriteArgs(adapterOrInput, maybeInput);
376
+ const updatedAt = nowIso();
377
+ return adapter.transaction(() => {
378
+ // wiki-compiler memberships are hard-coded to status='active' and
379
+ // added_by='wiki-compiler' per spec §5.3 L251-253. Callers cannot
380
+ // override status — non-active statuses are Phase 2 memory-agent /
381
+ // user-correction / sweeper territory.
382
+ const stmt = adapter.prepare(`
383
+ INSERT INTO case_memberships (
384
+ case_id, source_type, source_id, role, confidence, reason, status,
385
+ added_by, added_at, updated_at, user_locked
386
+ )
387
+ VALUES (?, ?, ?, ?, ?, ?, 'active', 'wiki-compiler', ?, ?, 0)
388
+ ON CONFLICT(case_id, source_type, source_id) DO UPDATE SET
389
+ role = excluded.role,
390
+ confidence = excluded.confidence,
391
+ reason = excluded.reason,
392
+ status = 'active',
393
+ added_by = 'wiki-compiler',
394
+ updated_at = excluded.updated_at
395
+ WHERE case_memberships.user_locked = 0
396
+ `);
397
+ for (const row of input.rows) {
398
+ stmt.run(input.case_id, row.source_type, row.source_id, row.role ?? null, row.confidence ?? null, row.reason ?? null, updatedAt, updatedAt);
399
+ }
400
+ const rows = adapter
401
+ .prepare(`
402
+ SELECT *
403
+ FROM case_memberships
404
+ WHERE case_id = ?
405
+ ORDER BY updated_at DESC, source_type ASC, source_id ASC
406
+ `)
407
+ .all(input.case_id);
408
+ return rows.map(mapCaseMembershipRow);
409
+ });
410
+ }
411
+ function listActiveMembershipsForCaseChain(adapter, caseIds, limit) {
412
+ const deduped = listActiveMembershipRecordsForCaseChain(adapter, caseIds);
413
+ const limited = limit === undefined ? deduped : deduped.slice(0, Math.max(0, limit));
414
+ return limited.map(toAssemblyMembership);
415
+ }
416
+ /**
417
+ * Resolves the canonical merge chain for caseId (both up to terminal and
418
+ * down to merged losers) and returns every active, unreverted correction
419
+ * across the entire chain. HITL surfaces must use this rather than a
420
+ * direct `WHERE case_id = ?` query so post-merge corrections on loser
421
+ * cases remain visible and actionable from the survivor's view.
422
+ */
423
+ function listActiveCorrectionsForCase(adapter, caseId) {
424
+ const resolution = resolveCanonicalCaseChain(adapter, caseId);
425
+ const chain = expandCaseChainForAssembly(adapter, resolution.terminal_case_id, resolution.chain);
426
+ const corrections = listActiveCorrectionsForCaseChain(adapter, chain);
427
+ return {
428
+ terminal_case_id: resolution.terminal_case_id,
429
+ resolved_via_case_id: resolution.resolved_via_case_id,
430
+ chain,
431
+ corrections,
432
+ };
433
+ }
434
+ function listActiveCorrectionsForCaseChain(adapter, caseIds) {
435
+ if (caseIds.length === 0) {
436
+ return [];
437
+ }
438
+ const rows = adapter
439
+ .prepare(`
440
+ SELECT *
441
+ FROM case_corrections
442
+ WHERE case_id IN (${placeholders(caseIds)})
443
+ AND is_lock_active = 1
444
+ AND reverted_at IS NULL
445
+ ORDER BY applied_at DESC, correction_id ASC
446
+ `)
447
+ .all(...caseIds);
448
+ return rows.map(mapCaseCorrectionRow);
449
+ }
450
+ function isUniqueConstraintError(error) {
451
+ if (!(error instanceof Error)) {
452
+ return false;
453
+ }
454
+ return /UNIQUE constraint|SQLITE_CONSTRAINT_UNIQUE/i.test(error.message);
455
+ }
456
+ function findExistingProposalId(adapter, input, payloadFingerprint) {
457
+ const row = adapter
458
+ .prepare(`
459
+ SELECT proposal_id
460
+ FROM case_proposal_queue
461
+ WHERE project = ?
462
+ AND proposal_kind = ?
463
+ AND COALESCE(conflicting_case_id, '') = ?
464
+ AND payload_fingerprint = ?
465
+ AND resolved_at IS NULL
466
+ LIMIT 1
467
+ `)
468
+ .get(input.project, input.proposal_kind, input.conflicting_case_id ?? '', payloadFingerprint);
469
+ return row?.proposal_id ?? null;
470
+ }
471
+ function enqueueCaseProposal(adapter, input) {
472
+ const canonicalFingerprintInput = (0, canonicalize_js_1.canonicalizeJSON)(input.stable_fingerprint_input);
473
+ const payloadFingerprint = (0, canonicalize_js_1.targetRefHashCanonicalJSON)(canonicalFingerprintInput);
474
+ const proposalId = (0, node_crypto_1.randomUUID)();
475
+ const detectedAt = nowIso();
476
+ return adapter.transaction(() => {
477
+ try {
478
+ adapter
479
+ .prepare(`
480
+ INSERT INTO case_proposal_queue (
481
+ proposal_id, project, proposal_kind, proposed_payload, payload_fingerprint,
482
+ conflicting_case_id, detected_at, resolved_at, resolution, resolution_note
483
+ )
484
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL)
485
+ `)
486
+ .run(proposalId, input.project, input.proposal_kind, input.proposed_payload, payloadFingerprint, input.conflicting_case_id ?? null, detectedAt);
487
+ return { proposal_id: proposalId, inserted: true };
488
+ }
489
+ catch (error) {
490
+ if (!isUniqueConstraintError(error)) {
491
+ throw error;
492
+ }
493
+ const existingId = findExistingProposalId(adapter, input, payloadFingerprint);
494
+ if (!existingId) {
495
+ throw error;
496
+ }
497
+ // Spec §5.7 L355: re-detection updates detected_at without duplicating.
498
+ // Keeps listUnresolvedCaseProposals ordering (ASC detected_at) honest —
499
+ // recently re-detected rows move to the tail instead of sitting at
500
+ // stale timestamps.
501
+ adapter
502
+ .prepare(`
503
+ UPDATE case_proposal_queue
504
+ SET detected_at = ?
505
+ WHERE proposal_id = ?
506
+ AND resolved_at IS NULL
507
+ `)
508
+ .run(detectedAt, existingId);
509
+ return { proposal_id: existingId, inserted: false };
510
+ }
511
+ });
512
+ }
513
+ function listUnresolvedCaseProposals(adapter, project) {
514
+ const rows = adapter
515
+ .prepare(`
516
+ SELECT *
517
+ FROM case_proposal_queue
518
+ WHERE project = ?
519
+ AND resolved_at IS NULL
520
+ ORDER BY detected_at ASC
521
+ `)
522
+ .all(project);
523
+ return rows.map(mapCaseProposalRow);
524
+ }
525
+ function resolveDecisionRows(adapter, memberships, options) {
526
+ const ids = memberships
527
+ .filter((membership) => membership.source_type === 'decision')
528
+ .map((membership) => membership.source_id);
529
+ if (ids.length === 0) {
530
+ return [];
531
+ }
532
+ const sinceMs = options?.since ? timestampMs(options.since) : null;
533
+ const rows = adapter
534
+ .prepare(`
535
+ SELECT id, topic, decision, reasoning, confidence, event_date, event_datetime, created_at
536
+ FROM decisions
537
+ WHERE id IN (${placeholders(ids)})
538
+ `)
539
+ .all(...ids);
540
+ return rows
541
+ .map((row) => {
542
+ const sortTime = timestampMs(row.event_datetime) ??
543
+ timestampMs(row.event_date) ??
544
+ timestampMs(row.created_at);
545
+ return {
546
+ id: String(row.id),
547
+ topic: String(row.topic),
548
+ decision: String(row.decision),
549
+ reasoning: nullableString(row.reasoning),
550
+ confidence: nullableNumber(row.confidence),
551
+ event_date: nullableString(row.event_date),
552
+ sort_time: sortTime,
553
+ };
554
+ })
555
+ .filter((row) => withinSince(row.sort_time, sinceMs))
556
+ .sort(compareBySortTimeDesc)
557
+ .slice(0, resultLimit(options))
558
+ .map(({ sort_time: _sortTime, ...row }) => row);
559
+ }
560
+ function resolveTimelineEventRows(adapter, memberships, options) {
561
+ const eventMemberships = memberships.filter((membership) => membership.source_type === 'event');
562
+ const ids = eventMemberships.map((membership) => membership.source_id);
563
+ if (ids.length === 0) {
564
+ return [];
565
+ }
566
+ const membershipById = new Map(eventMemberships.map((membership) => [membership.source_id, membership]));
567
+ const sinceMs = options?.since ? timestampMs(options.since) : null;
568
+ const rows = adapter
569
+ .prepare(`
570
+ SELECT id, event_type, entity_id, role, observed_at, summary, details
571
+ FROM entity_timeline_events
572
+ WHERE id IN (${placeholders(ids)})
573
+ `)
574
+ .all(...ids);
575
+ return rows
576
+ .map((row) => {
577
+ const eventId = String(row.id);
578
+ const sortTime = timestampMs(row.observed_at);
579
+ return {
580
+ event_id: eventId,
581
+ event_type: String(row.event_type),
582
+ entity_id: String(row.entity_id),
583
+ role: membershipById.get(eventId)?.role ?? nullableString(row.role),
584
+ observed_at: formatTimestamp(row.observed_at),
585
+ summary: String(row.summary),
586
+ details: nullableString(row.details),
587
+ sort_time: sortTime,
588
+ };
589
+ })
590
+ .filter((row) => withinSince(row.sort_time, sinceMs))
591
+ .sort(compareBySortTimeDesc)
592
+ .slice(0, resultLimit(options))
593
+ .map(({ sort_time: _sortTime, ...row }) => row);
594
+ }
595
+ function resolveObservationRows(adapter, memberships, options) {
596
+ const ids = memberships
597
+ .filter((membership) => membership.source_type === 'observation')
598
+ .map((membership) => membership.source_id);
599
+ if (ids.length === 0) {
600
+ return [];
601
+ }
602
+ const sinceMs = options?.since ? timestampMs(options.since) : null;
603
+ const rows = adapter
604
+ .prepare(`
605
+ SELECT id, surface_form, source_locator, timestamp_observed
606
+ FROM entity_observations
607
+ WHERE id IN (${placeholders(ids)})
608
+ `)
609
+ .all(...ids);
610
+ return rows
611
+ .map((row) => {
612
+ const sortTime = timestampMs(row.timestamp_observed);
613
+ return {
614
+ observation_id: String(row.id),
615
+ surface_form: String(row.surface_form),
616
+ source_locator: nullableString(row.source_locator) ?? '',
617
+ timestamp_observed: formatTimestamp(row.timestamp_observed),
618
+ sort_time: sortTime,
619
+ };
620
+ })
621
+ .filter((row) => withinSince(row.sort_time, sinceMs))
622
+ .sort(compareBySortTimeDesc)
623
+ .slice(0, resultLimit(options))
624
+ .map(({ sort_time: _sortTime, ...row }) => row);
625
+ }
626
+ function buildLinkedCases(adapter, chain, terminalCaseId) {
627
+ const linked = chain
628
+ .filter((caseId) => caseId !== terminalCaseId)
629
+ .map((caseId) => ({ case_id: caseId, relation: 'merged_from' }));
630
+ if (chain.length === 0) {
631
+ return linked;
632
+ }
633
+ const rows = adapter
634
+ .prepare(`
635
+ SELECT case_id
636
+ FROM case_truth
637
+ WHERE split_from_case_id IN (${placeholders(chain)})
638
+ ORDER BY updated_at DESC
639
+ `)
640
+ .all(...chain);
641
+ for (const row of rows) {
642
+ linked.push({ case_id: String(row.case_id), relation: 'split_into' });
643
+ }
644
+ return linked;
645
+ }
646
+ function assembleCase(adapterOrCaseId, caseIdOrOptions, maybeOptions) {
647
+ const { adapter, caseId, options } = resolveAssembleArgs(adapterOrCaseId, caseIdOrOptions, maybeOptions);
648
+ const resolution = resolveCanonicalCaseChain(adapter, caseId);
649
+ const assemblyChain = expandCaseChainForAssembly(adapter, resolution.terminal_case_id, resolution.chain);
650
+ const caseTruth = loadCaseTruth(adapter, resolution.terminal_case_id);
651
+ const membershipRecords = listActiveMembershipRecordsForCaseChain(adapter, assemblyChain);
652
+ const memberships = membershipRecords.map(toAssemblyMembership);
653
+ const activeCorrections = listActiveCorrectionsForCaseChain(adapter, assemblyChain);
654
+ // Phase 3 additive fields: case_links + promoted_sources + freshness +
655
+ // membership_explanations. Read inline to avoid cross-file import cycles.
656
+ // Each block is try/catch-wrapped so older DBs without migrations
657
+ // 047/048/049 applied still work (returns empty/null).
658
+ let phase3CaseLinks = [];
659
+ try {
660
+ if (assemblyChain.length > 0) {
661
+ const rows = adapter
662
+ .prepare(`
663
+ SELECT link_id, case_id_from, case_id_to, link_type, created_at, created_by,
664
+ confidence, reason_json, source_kind, source_ref
665
+ FROM case_links
666
+ WHERE case_id_from IN (${placeholders(assemblyChain)})
667
+ AND revoked_at IS NULL
668
+ ORDER BY created_at DESC, link_id ASC
669
+ `)
670
+ .all(...assemblyChain);
671
+ phase3CaseLinks = rows.map((row) => ({
672
+ link_id: String(row.link_id),
673
+ case_id_from: String(row.case_id_from),
674
+ case_id_to: String(row.case_id_to),
675
+ link_type: String(row.link_type),
676
+ created_at: String(row.created_at),
677
+ created_by: String(row.created_by),
678
+ confidence: nullableNumber(row.confidence),
679
+ reason_json: nullableString(row.reason_json),
680
+ source_kind: String(row.source_kind),
681
+ source_ref: nullableString(row.source_ref),
682
+ }));
683
+ }
684
+ }
685
+ catch (error) {
686
+ rethrowUnlessMissingSchema(error);
687
+ phase3CaseLinks = [];
688
+ }
689
+ let phase3PromotedSources = null;
690
+ let phase3Freshness = null;
691
+ try {
692
+ const row = adapter
693
+ .prepare(`
694
+ SELECT canonical_decision_id, canonical_event_id, promoted_at, promoted_by,
695
+ promotion_reason, freshness_score, freshness_state,
696
+ freshness_score_is_drifted, freshness_drift_threshold,
697
+ freshness_checked_at, freshness_reason_json
698
+ FROM case_truth
699
+ WHERE case_id = ?
700
+ `)
701
+ .get(resolution.terminal_case_id);
702
+ if (row) {
703
+ phase3PromotedSources = {
704
+ canonical_decision_id: nullableString(row.canonical_decision_id),
705
+ canonical_event_id: nullableString(row.canonical_event_id),
706
+ promoted_at: nullableString(row.promoted_at),
707
+ promoted_by: nullableString(row.promoted_by),
708
+ promotion_reason: nullableString(row.promotion_reason),
709
+ };
710
+ phase3Freshness = {
711
+ freshness_score: nullableNumber(row.freshness_score),
712
+ freshness_state: nullableString(row.freshness_state),
713
+ freshness_score_is_drifted: flag01(row.freshness_score_is_drifted),
714
+ freshness_drift_threshold: nullableNumber(row.freshness_drift_threshold),
715
+ freshness_checked_at: nullableString(row.freshness_checked_at),
716
+ freshness_reason_json: nullableString(row.freshness_reason_json),
717
+ };
718
+ }
719
+ }
720
+ catch (error) {
721
+ rethrowUnlessMissingSchema(error);
722
+ phase3PromotedSources = null;
723
+ phase3Freshness = null;
724
+ }
725
+ const phase3MembershipExplanations = {};
726
+ try {
727
+ if (memberships.length > 0) {
728
+ const explanationRows = adapter
729
+ .prepare(`
730
+ SELECT source_type, source_id, user_locked, updated_at, score_breakdown_json,
731
+ source_locator, assignment_strategy, explanation_updated_at
732
+ FROM case_memberships
733
+ WHERE case_id IN (${placeholders(assemblyChain)})
734
+ AND status = 'active'
735
+ `)
736
+ .all(...assemblyChain);
737
+ const selectedMemberships = new Map(membershipRecords.map((membership) => [
738
+ sourceKey(membership.source_type, membership.source_id),
739
+ membership,
740
+ ]));
741
+ const sortedExplanationRows = [...explanationRows].sort((left, right) => {
742
+ const lockDelta = flag01(right.user_locked) - flag01(left.user_locked);
743
+ return lockDelta !== 0
744
+ ? lockDelta
745
+ : compareByUpdatedAtDesc(String(left.updated_at), String(right.updated_at));
746
+ });
747
+ for (const row of sortedExplanationRows) {
748
+ const key = `${String(row.source_type)}:${String(row.source_id)}`;
749
+ const selected = selectedMemberships.get(key);
750
+ if (!selected) {
751
+ continue;
752
+ }
753
+ if (flag01(row.user_locked) !== selected.user_locked ||
754
+ String(row.updated_at) !== selected.updated_at) {
755
+ continue;
756
+ }
757
+ phase3MembershipExplanations[key] = {
758
+ source_type: String(row.source_type),
759
+ source_id: String(row.source_id),
760
+ score_breakdown: row.score_breakdown_json
761
+ ? JSON.parse(String(row.score_breakdown_json))
762
+ : null,
763
+ source_locator: nullableString(row.source_locator),
764
+ assignment_strategy: nullableString(row.assignment_strategy),
765
+ explanation_updated_at: nullableString(row.explanation_updated_at),
766
+ };
767
+ }
768
+ }
769
+ }
770
+ catch (error) {
771
+ rethrowUnlessMissingSchema(error);
772
+ // Old DB without case_memberships explanation columns — leave empty.
773
+ }
774
+ const assembly = {
775
+ case_id: resolution.terminal_case_id,
776
+ current_wiki_path: caseTruth?.current_wiki_path ?? null,
777
+ wiki_page: null,
778
+ case_truth: caseTruth
779
+ ? {
780
+ title: caseTruth.title,
781
+ status: caseTruth.status,
782
+ status_reason: caseTruth.status_reason,
783
+ primary_actors: parseJsonArray(caseTruth.primary_actors),
784
+ blockers: parseJsonArray(caseTruth.blockers),
785
+ last_activity_at: caseTruth.last_activity_at,
786
+ confidence: caseTruth.confidence,
787
+ canonical_case_id: caseTruth.canonical_case_id ?? null,
788
+ split_from_case_id: caseTruth.split_from_case_id ?? null,
789
+ }
790
+ : null,
791
+ memberships,
792
+ timeline_events: resolveTimelineEventRows(adapter, memberships, options),
793
+ decisions: resolveDecisionRows(adapter, memberships, options),
794
+ recent_evidence: resolveObservationRows(adapter, memberships, options),
795
+ active_corrections: activeCorrections.map((correction) => ({
796
+ correction_id: correction.correction_id,
797
+ target_kind: correction.target_kind,
798
+ target_ref: parseTargetRef(correction.target_ref_json),
799
+ new_value_json: correction.new_value_json,
800
+ reason: correction.reason,
801
+ applied_at: correction.applied_at,
802
+ })),
803
+ linked_cases: buildLinkedCases(adapter, assemblyChain, resolution.terminal_case_id),
804
+ // Phase 3 additive (Task 30) — cast through unknown since inline
805
+ // query rows are structurally shaped but TS sees Record<string,unknown>.
806
+ case_links: phase3CaseLinks,
807
+ promoted_sources: phase3PromotedSources,
808
+ freshness: phase3Freshness,
809
+ membership_explanations: phase3MembershipExplanations,
810
+ };
811
+ if (resolution.resolved_via_case_id) {
812
+ assembly.resolved_via_case_id = resolution.resolved_via_case_id;
813
+ }
814
+ return assembly;
815
+ }
816
+ //# sourceMappingURL=store.js.map