@jungjaehoon/mama-core 1.9.0 → 2.1.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 (139) hide show
  1. package/db/migrations/043-add-tool-trace-failure-code.sql +11 -0
  2. package/dist/agent-graph/alias-write.js +3 -0
  3. package/dist/agent-graph/entity-resolve.js +3 -7
  4. package/dist/agent-graph/graph-query.js +5 -4
  5. package/dist/agent-graph/types.d.ts +30 -0
  6. package/dist/cases/store.d.ts +0 -4
  7. package/dist/cases/store.js +0 -66
  8. package/dist/cases/wiki-page-index.d.ts +0 -13
  9. package/dist/cases/wiki-page-index.js +0 -153
  10. package/dist/connectors/event-index.d.ts +1 -22
  11. package/dist/connectors/event-index.js +0 -157
  12. package/dist/context-compile/channel-grant.d.ts +46 -0
  13. package/dist/context-compile/channel-grant.js +66 -0
  14. package/dist/context-compile/compiler.js +5 -2
  15. package/dist/context-compile/index.d.ts +1 -0
  16. package/dist/context-compile/index.js +1 -0
  17. package/dist/context-compile/packet-store.d.ts +0 -1
  18. package/dist/context-compile/packet-store.js +0 -16
  19. package/dist/context-compile/source-readers.d.ts +1 -0
  20. package/dist/context-compile/source-readers.js +156 -23
  21. package/dist/context-compile/types.d.ts +17 -0
  22. package/dist/context-compile/visibility.js +28 -4
  23. package/dist/db-adapter/node-sqlite-adapter.js +4 -0
  24. package/dist/db-manager.d.ts +0 -13
  25. package/dist/db-manager.js +0 -66
  26. package/dist/decision-formatter.d.ts +0 -11
  27. package/dist/decision-formatter.js +0 -122
  28. package/dist/decision-tracker.d.ts +0 -12
  29. package/dist/decision-tracker.js +0 -50
  30. package/dist/edges/ref-validation.d.ts +1 -2
  31. package/dist/edges/ref-validation.js +33 -13
  32. package/dist/edges/store.d.ts +1 -3
  33. package/dist/edges/store.js +0 -214
  34. package/dist/edges/types.d.ts +10 -0
  35. package/dist/embedding-client.d.ts +0 -17
  36. package/dist/embedding-client.js +0 -80
  37. package/dist/embedding-server/mobile/auth.d.ts +1 -10
  38. package/dist/embedding-server/mobile/auth.js +0 -15
  39. package/dist/embeddings.d.ts +0 -12
  40. package/dist/embeddings.js +0 -68
  41. package/dist/entities/audit-metrics.d.ts +0 -5
  42. package/dist/entities/audit-metrics.js +0 -4
  43. package/dist/entities/candidate-generator.d.ts +1 -2
  44. package/dist/entities/candidate-generator.js +0 -412
  45. package/dist/entities/entity-list.d.ts +0 -1
  46. package/dist/entities/entity-list.js +0 -96
  47. package/dist/entities/entity-orphan-list.d.ts +0 -9
  48. package/dist/entities/entity-orphan-list.js +0 -88
  49. package/dist/entities/entity-search.d.ts +0 -1
  50. package/dist/entities/entity-search.js +0 -147
  51. package/dist/entities/errors.d.ts +0 -10
  52. package/dist/entities/errors.js +1 -25
  53. package/dist/entities/lineage-store.d.ts +0 -23
  54. package/dist/entities/lineage-store.js +0 -104
  55. package/dist/entities/policy-store.d.ts +0 -9
  56. package/dist/entities/policy-store.js +0 -188
  57. package/dist/entities/provenance-query.d.ts +0 -1
  58. package/dist/entities/provenance-query.js +0 -164
  59. package/dist/entities/resolution-engine.d.ts +1 -8
  60. package/dist/entities/resolution-engine.js +0 -66
  61. package/dist/entities/rollback-preview.d.ts +0 -4
  62. package/dist/entities/rollback-preview.js +0 -232
  63. package/dist/entities/store.d.ts +1 -10
  64. package/dist/entities/store.js +0 -49
  65. package/dist/index.d.ts +15 -31
  66. package/dist/index.js +4 -46
  67. package/dist/mama-api.d.ts +19 -1
  68. package/dist/mama-api.js +97 -2
  69. package/dist/memory/api.d.ts +8 -0
  70. package/dist/memory/api.js +55 -2
  71. package/dist/memory/event-store.d.ts +0 -1
  72. package/dist/memory/event-store.js +0 -15
  73. package/dist/memory/finding-store.d.ts +0 -1
  74. package/dist/memory/finding-store.js +0 -12
  75. package/dist/memory/scope-backfill.d.ts +0 -3
  76. package/dist/memory/scope-backfill.js +0 -74
  77. package/dist/memory/scope-store.d.ts +0 -1
  78. package/dist/memory/scope-store.js +0 -4
  79. package/dist/memory/truth-store.d.ts +0 -3
  80. package/dist/memory/truth-store.js +0 -22
  81. package/dist/memory/types.d.ts +0 -1
  82. package/dist/memory/types.js +0 -13
  83. package/dist/memory-store.d.ts +0 -16
  84. package/dist/memory-store.js +0 -51
  85. package/dist/model-runs/tool-trace-store.js +9 -5
  86. package/dist/model-runs/types.d.ts +3 -0
  87. package/dist/ollama-client.d.ts +0 -9
  88. package/dist/ollama-client.js +0 -125
  89. package/dist/outcome-tracker.d.ts +0 -14
  90. package/dist/outcome-tracker.js +0 -44
  91. package/dist/progress-indicator.d.ts +0 -4
  92. package/dist/progress-indicator.js +0 -7
  93. package/dist/provenance/source-ref.d.ts +0 -9
  94. package/dist/provenance/source-ref.js +0 -145
  95. package/dist/query-intent.d.ts +0 -4
  96. package/dist/query-intent.js +0 -30
  97. package/dist/relevance-scorer.d.ts +0 -9
  98. package/dist/relevance-scorer.js +0 -81
  99. package/dist/search/feedback-store.d.ts +0 -1
  100. package/dist/search/feedback-store.js +0 -4
  101. package/dist/search/ranker-rescore.d.ts +0 -3
  102. package/dist/search/ranker-rescore.js +0 -27
  103. package/dist/search/ranker-trainer.d.ts +0 -8
  104. package/dist/search/ranker-trainer.js +0 -376
  105. package/dist/tier-validator.d.ts +0 -12
  106. package/dist/tier-validator.js +0 -56
  107. package/package.json +1 -10
  108. package/dist/cases/case-links.d.ts +0 -116
  109. package/dist/cases/case-links.js +0 -794
  110. package/dist/cases/composition-overrides.d.ts +0 -90
  111. package/dist/cases/composition-overrides.js +0 -724
  112. package/dist/cases/corrections.d.ts +0 -156
  113. package/dist/cases/corrections.js +0 -1045
  114. package/dist/cases/freshness.d.ts +0 -57
  115. package/dist/cases/freshness.js +0 -283
  116. package/dist/cases/live-state.d.ts +0 -48
  117. package/dist/cases/live-state.js +0 -256
  118. package/dist/cases/membership-explain.d.ts +0 -56
  119. package/dist/cases/membership-explain.js +0 -140
  120. package/dist/cases/membership-matcher.d.ts +0 -23
  121. package/dist/cases/membership-matcher.js +0 -193
  122. package/dist/cases/merge-split.d.ts +0 -61
  123. package/dist/cases/merge-split.js +0 -368
  124. package/dist/cases/role-inference.d.ts +0 -10
  125. package/dist/cases/role-inference.js +0 -85
  126. package/dist/cases/sqlite-transaction.d.ts +0 -10
  127. package/dist/cases/sqlite-transaction.js +0 -62
  128. package/dist/cases/target-ref.d.ts +0 -25
  129. package/dist/cases/target-ref.js +0 -22
  130. package/dist/cases/tombstone-sweeper.d.ts +0 -13
  131. package/dist/cases/tombstone-sweeper.js +0 -128
  132. package/dist/entities/entity-impact.d.ts +0 -41
  133. package/dist/entities/entity-impact.js +0 -140
  134. package/dist/entities/exact-merge-backfill.d.ts +0 -11
  135. package/dist/entities/exact-merge-backfill.js +0 -161
  136. package/dist/entities/lineage-backfill.d.ts +0 -12
  137. package/dist/entities/lineage-backfill.js +0 -101
  138. package/dist/memory-inject.d.ts +0 -24
  139. package/dist/memory-inject.js +0 -116
@@ -1,794 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sha256TombstoneFingerprint = sha256TombstoneFingerprint;
4
- exports.createCaseLink = createCaseLink;
5
- exports.revokeCaseLink = revokeCaseLink;
6
- exports.listActiveCaseLinks = listActiveCaseLinks;
7
- exports.isWikiTombstoneActive = isWikiTombstoneActive;
8
- exports.unsuppressWikiTombstone = unsuppressWikiTombstone;
9
- exports.backfillStructuralLinks = backfillStructuralLinks;
10
- const node_crypto_1 = require("node:crypto");
11
- const canonicalize_js_1 = require("../canonicalize.js");
12
- const sqlite_transaction_js_1 = require("./sqlite-transaction.js");
13
- const store_js_1 = require("./store.js");
14
- const TERMINAL_CASE_STATUSES = new Set(['merged', 'archived', 'split']);
15
- const RECONFIRM_TOKEN_TTL_MS = 10 * 60_000;
16
- function normalizeNow(value) {
17
- return value ?? new Date().toISOString();
18
- }
19
- function timestampMs(value) {
20
- const parsed = Date.parse(value);
21
- return Number.isFinite(parsed) ? parsed : Date.now();
22
- }
23
- function createdAtMs(value) {
24
- return timestampMs(value);
25
- }
26
- function randomLinkId() {
27
- return `link_${(0, node_crypto_1.randomUUID)().replace(/-/g, '')}`;
28
- }
29
- function randomTombstoneId() {
30
- return `tomb_${(0, node_crypto_1.randomUUID)().replace(/-/g, '')}`;
31
- }
32
- function deterministicLinkId(nonce) {
33
- return `link_${sha256Hex(`case-link-reconfirm:v1:${nonce}`).slice(0, 32)}`;
34
- }
35
- function sha256Hex(value) {
36
- return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
37
- }
38
- function hmacHex(secret, value) {
39
- return (0, node_crypto_1.createHmac)('sha256', secret).update(value, 'utf8').digest('hex');
40
- }
41
- function decodeSecret(raw) {
42
- const trimmed = raw.trim();
43
- if (trimmed.length === 0) {
44
- return null;
45
- }
46
- if (/^[0-9a-f]+$/i.test(trimmed) && trimmed.length % 2 === 0) {
47
- const hex = Buffer.from(trimmed, 'hex');
48
- if (hex.length >= 32) {
49
- return hex;
50
- }
51
- }
52
- const base64 = Buffer.from(trimmed, 'base64');
53
- if (base64.length >= 32) {
54
- return base64;
55
- }
56
- const utf8 = Buffer.from(trimmed, 'utf8');
57
- if (utf8.length >= 32) {
58
- return utf8;
59
- }
60
- return null;
61
- }
62
- function secretKid(secret) {
63
- return (0, node_crypto_1.createHash)('sha256').update(secret).digest('hex').slice(0, 16);
64
- }
65
- function currentSecret() {
66
- const raw = process.env.MAMA_RECONFIRM_TOKEN_SECRET;
67
- const secret = raw ? decodeSecret(raw) : null;
68
- if (!secret) {
69
- throw new Error('MAMA_RECONFIRM_TOKEN_SECRET must be a 32+ byte base64 or hex secret.');
70
- }
71
- return { kid: secretKid(secret), secret };
72
- }
73
- function verificationSecrets() {
74
- const secrets = new Map();
75
- const current = currentSecret();
76
- secrets.set(current.kid, current.secret);
77
- for (const raw of (process.env.MAMA_RECONFIRM_TOKEN_OLD_SECRETS ?? '').split(',')) {
78
- const secret = decodeSecret(raw);
79
- if (secret) {
80
- secrets.set(secretKid(secret), secret);
81
- }
82
- }
83
- return secrets;
84
- }
85
- function encodeReconfirmToken(payload, secret) {
86
- const payloadJson = (0, canonicalize_js_1.canonicalizeJSON)(payload);
87
- const envelope = {
88
- kid: payload.kid,
89
- payload,
90
- signature_hex: hmacHex(secret, payloadJson),
91
- };
92
- return Buffer.from((0, canonicalize_js_1.canonicalizeJSON)(envelope), 'utf8').toString('base64url');
93
- }
94
- function isRecord(value) {
95
- return value !== null && typeof value === 'object' && !Array.isArray(value);
96
- }
97
- function readReconfirmEnvelope(token) {
98
- try {
99
- const parsed = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
100
- if (!isRecord(parsed) || !isRecord(parsed.payload)) {
101
- return null;
102
- }
103
- const payload = parsed.payload;
104
- if (parsed.kid !== payload.kid ||
105
- payload.v !== 1 ||
106
- typeof parsed.kid !== 'string' ||
107
- typeof parsed.signature_hex !== 'string' ||
108
- typeof payload.kid !== 'string' ||
109
- typeof payload.nonce !== 'string' ||
110
- typeof payload.case_id !== 'string' ||
111
- payload.target_kind !== 'case_link' ||
112
- typeof payload.target_ref_hash_hex !== 'string' ||
113
- typeof payload.current_value_hash_hex !== 'string' ||
114
- typeof payload.proposed_value_hash_hex !== 'string' ||
115
- typeof payload.confirmed_by !== 'string' ||
116
- !(payload.session_id === null || typeof payload.session_id === 'string') ||
117
- typeof payload.expires_at !== 'string') {
118
- return null;
119
- }
120
- return {
121
- kid: parsed.kid,
122
- payload: payload,
123
- signature_hex: parsed.signature_hex,
124
- };
125
- }
126
- catch {
127
- return null;
128
- }
129
- }
130
- function verifyReconfirmToken(input) {
131
- const envelope = readReconfirmEnvelope(input.token);
132
- if (!envelope) {
133
- return null;
134
- }
135
- const secret = verificationSecrets().get(envelope.kid);
136
- if (!secret) {
137
- return null;
138
- }
139
- const expected = Buffer.from(hmacHex(secret, (0, canonicalize_js_1.canonicalizeJSON)(envelope.payload)), 'hex');
140
- const actual = Buffer.from(envelope.signature_hex, 'hex');
141
- if (actual.length !== expected.length || !(0, node_crypto_1.timingSafeEqual)(actual, expected)) {
142
- return null;
143
- }
144
- const expiresAtMs = Date.parse(envelope.payload.expires_at);
145
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= timestampMs(input.now)) {
146
- return null;
147
- }
148
- if (envelope.payload.case_id !== input.case_id ||
149
- envelope.payload.target_ref_hash_hex !== input.target_ref_hash_hex ||
150
- envelope.payload.proposed_value_hash_hex !== input.proposed_value_hash_hex ||
151
- envelope.payload.confirmed_by !== input.confirmed_by ||
152
- envelope.payload.session_id !== input.session_id) {
153
- return null;
154
- }
155
- return envelope.payload;
156
- }
157
- function buildRequiresReconfirm(input) {
158
- const secret = currentSecret();
159
- const expiresAt = new Date(timestampMs(input.now) + RECONFIRM_TOKEN_TTL_MS).toISOString();
160
- const payload = {
161
- v: 1,
162
- kid: secret.kid,
163
- nonce: (0, node_crypto_1.randomUUID)(),
164
- case_id: input.case_id,
165
- target_kind: 'case_link',
166
- target_ref_hash_hex: input.target_ref_hash_hex,
167
- current_value_hash_hex: sha256Hex(input.current_value_json),
168
- proposed_value_hash_hex: sha256Hex(input.proposed_new_value_json),
169
- confirmed_by: input.confirmed_by,
170
- session_id: input.session_id,
171
- expires_at: expiresAt,
172
- };
173
- return {
174
- kind: 'requires_reconfirm',
175
- code: 'case.correction_requires_reconfirm',
176
- case_id: input.case_id,
177
- target_kind: 'case_link',
178
- target_ref_json: input.target_ref_json,
179
- current_value_json: input.current_value_json,
180
- old_value_json: input.old_value_json,
181
- proposed_new_value_json: input.proposed_new_value_json,
182
- reconfirm_token: encodeReconfirmToken(payload, secret.secret),
183
- reconfirm_token_expires_at: expiresAt,
184
- message: 'The target changed since the viewed snapshot. Reconfirm against the current value.',
185
- };
186
- }
187
- function parseJsonValue(value) {
188
- return JSON.parse(value);
189
- }
190
- function canonicalExpectedJson(value) {
191
- if (typeof value !== 'string') {
192
- return null;
193
- }
194
- return (0, canonicalize_js_1.canonicalizeJSON)(parseJsonValue(value));
195
- }
196
- function toBuffer(value) {
197
- if (value === null || value === undefined) {
198
- return null;
199
- }
200
- if (Buffer.isBuffer(value)) {
201
- return value;
202
- }
203
- if (value instanceof Uint8Array) {
204
- return Buffer.from(value);
205
- }
206
- throw new Error('Expected SQLite BLOB value to be a Buffer.');
207
- }
208
- function nullableString(value) {
209
- if (value === null || value === undefined) {
210
- return null;
211
- }
212
- return String(value);
213
- }
214
- function nullableNumber(value) {
215
- if (value === null || value === undefined) {
216
- return null;
217
- }
218
- const n = Number(value);
219
- return Number.isFinite(n) ? n : null;
220
- }
221
- function mapCaseLinkRow(row) {
222
- const fingerprint = toBuffer(row.source_ref_fingerprint);
223
- return {
224
- link_id: String(row.link_id),
225
- case_id_from: String(row.case_id_from),
226
- case_id_to: String(row.case_id_to),
227
- link_type: String(row.link_type),
228
- created_at: String(row.created_at),
229
- created_by: String(row.created_by),
230
- confidence: nullableNumber(row.confidence),
231
- reason_json: nullableString(row.reason_json),
232
- source_kind: String(row.source_kind),
233
- source_ref: nullableString(row.source_ref),
234
- source_ref_fingerprint: fingerprint,
235
- source_ref_fingerprint_hex: fingerprint ? fingerprint.toString('hex') : null,
236
- revoked_at: nullableString(row.revoked_at),
237
- revoked_by: nullableString(row.revoked_by),
238
- revoke_reason: nullableString(row.revoke_reason),
239
- };
240
- }
241
- function placeholders(values) {
242
- if (values.length === 0) {
243
- throw new Error('Cannot build SQL IN clause for an empty value list.');
244
- }
245
- return values.map(() => '?').join(', ');
246
- }
247
- function loadCase(adapter, caseId) {
248
- const row = adapter
249
- .prepare(`
250
- SELECT case_id, status, canonical_case_id, split_from_case_id, title,
251
- current_wiki_path, updated_at
252
- FROM case_truth
253
- WHERE case_id = ?
254
- `)
255
- .get(caseId);
256
- return row ?? null;
257
- }
258
- function caseSnapshot(row) {
259
- return {
260
- case_id: row.case_id,
261
- status: row.status,
262
- canonical_case_id: row.canonical_case_id,
263
- split_from_case_id: row.split_from_case_id,
264
- title: row.title,
265
- current_wiki_path: row.current_wiki_path,
266
- updated_at: row.updated_at,
267
- };
268
- }
269
- function reasonJson(value) {
270
- if (value === null || value === undefined) {
271
- return null;
272
- }
273
- if (typeof value === 'string') {
274
- return value;
275
- }
276
- return (0, canonicalize_js_1.canonicalizeJSON)(value);
277
- }
278
- function caseLinkTargetRefJson(input) {
279
- return (0, canonicalize_js_1.canonicalizeJSON)({
280
- kind: 'case_link',
281
- case_id_from: input.case_id_from,
282
- case_id_to: input.case_id_to,
283
- link_type: input.link_type,
284
- });
285
- }
286
- function activeLinkForTriple(adapter, input) {
287
- const row = adapter
288
- .prepare(`
289
- SELECT *
290
- FROM case_links
291
- WHERE case_id_from = ?
292
- AND case_id_to = ?
293
- AND link_type = ?
294
- AND revoked_at IS NULL
295
- LIMIT 1
296
- `)
297
- .get(input.case_id_from, input.case_id_to, input.link_type);
298
- return row ? mapCaseLinkRow(row) : null;
299
- }
300
- function linkExistsById(adapter, linkId) {
301
- const row = adapter.prepare('SELECT link_id FROM case_links WHERE link_id = ?').get(linkId);
302
- return Boolean(row);
303
- }
304
- function activeTombstoneRow(adapter, input, fingerprint = sha256TombstoneFingerprint(input)) {
305
- const row = adapter
306
- .prepare(`
307
- SELECT tombstone_id
308
- FROM case_links_revoked_wiki_tombstones
309
- WHERE case_id_from = ?
310
- AND case_id_to = ?
311
- AND link_type = ?
312
- AND source_ref_fingerprint = ?
313
- AND unsuppressed_at IS NULL
314
- LIMIT 1
315
- `)
316
- .get(input.source_case_id, input.target_case_id, input.link_type, fingerprint);
317
- return row ?? null;
318
- }
319
- function unsuppressWikiTombstoneInTransaction(adapter, input, fingerprint = sha256TombstoneFingerprint(input)) {
320
- const result = adapter
321
- .prepare(`
322
- UPDATE case_links_revoked_wiki_tombstones
323
- SET unsuppressed_at = ?, unsuppressed_by = ?
324
- WHERE case_id_from = ?
325
- AND case_id_to = ?
326
- AND link_type = ?
327
- AND source_ref_fingerprint = ?
328
- AND unsuppressed_at IS NULL
329
- `)
330
- .run(input.now, input.unsuppressed_by, input.source_case_id, input.target_case_id, input.link_type, fingerprint);
331
- return result.changes;
332
- }
333
- function currentCreateValueJson(adapter, input, sourceCase, targetCase) {
334
- return (0, canonicalize_js_1.canonicalizeJSON)({
335
- source_case: caseSnapshot(sourceCase),
336
- target_case: caseSnapshot(targetCase),
337
- target_ref: {
338
- kind: 'case_link',
339
- case_id_from: input.case_id_from,
340
- case_id_to: input.case_id_to,
341
- link_type: input.link_type,
342
- },
343
- active_link: activeLinkForTriple(adapter, input),
344
- wiki_tombstone_active: isWikiTombstoneActive(adapter, {
345
- source_case_id: input.case_id_from,
346
- target_case_id: input.case_id_to,
347
- link_type: input.link_type,
348
- }),
349
- });
350
- }
351
- function proposedCreateValueJson(input) {
352
- return (0, canonicalize_js_1.canonicalizeJSON)({
353
- case_id_from: input.case_id_from,
354
- case_id_to: input.case_id_to,
355
- link_type: input.link_type,
356
- confidence: input.confidence ?? null,
357
- reason_json: reasonJson(input.reason_json),
358
- source_kind: input.source_kind ?? 'manual',
359
- source_ref: input.source_ref ?? null,
360
- });
361
- }
362
- function currentRevokeValueJson(row) {
363
- return (0, canonicalize_js_1.canonicalizeJSON)({
364
- link_id: row.link_id,
365
- case_id_from: row.case_id_from,
366
- case_id_to: row.case_id_to,
367
- link_type: row.link_type,
368
- revoked_at: row.revoked_at,
369
- revoked_by: row.revoked_by,
370
- revoke_reason: row.revoke_reason,
371
- source_kind: row.source_kind,
372
- source_ref_fingerprint_hex: row.source_ref_fingerprint_hex,
373
- });
374
- }
375
- function insertMemoryEvent(input) {
376
- input.adapter
377
- .prepare(`
378
- INSERT INTO memory_events (
379
- event_id, event_type, actor, source_turn_id, memory_id, topic,
380
- scope_refs, evidence_refs, reason, created_at
381
- )
382
- VALUES (?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?)
383
- `)
384
- .run(`me_${(0, node_crypto_1.randomUUID)()}`, input.event_type, input.actor, `case:${input.case_id}`, (0, canonicalize_js_1.canonicalizeJSON)([{ type: 'case', id: input.case_id }]), (0, canonicalize_js_1.canonicalizeJSON)([input.link_id]), (0, canonicalize_js_1.canonicalizeJSON)(input.reason), createdAtMs(input.now));
385
- }
386
- function sha256TombstoneFingerprint(input) {
387
- return (0, node_crypto_1.createHash)('sha256')
388
- .update((0, canonicalize_js_1.canonicalizeJSON)({
389
- source_case_id: input.source_case_id,
390
- target_case_id: input.target_case_id,
391
- link_type: input.link_type,
392
- }), 'utf8')
393
- .digest();
394
- }
395
- function createCaseLink(adapter, input) {
396
- const linkAdapter = adapter;
397
- return (0, sqlite_transaction_js_1.runImmediateTransaction)(linkAdapter, () => {
398
- const now = normalizeNow(input.now);
399
- const sourceKind = input.source_kind ?? 'manual';
400
- if (input.case_id_from === input.case_id_to) {
401
- return {
402
- kind: 'rejected',
403
- code: 'case.self_link',
404
- message: 'Case links cannot point to the same case.',
405
- case_id: input.case_id_from,
406
- };
407
- }
408
- const sourceCase = loadCase(linkAdapter, input.case_id_from);
409
- if (!sourceCase) {
410
- return {
411
- kind: 'rejected',
412
- code: 'case.precompile_gap',
413
- message: `Source case ${input.case_id_from} does not exist.`,
414
- case_id: input.case_id_from,
415
- };
416
- }
417
- const targetCase = loadCase(linkAdapter, input.case_id_to);
418
- if (!targetCase) {
419
- return {
420
- kind: 'rejected',
421
- code: 'case.precompile_gap',
422
- message: `Target case ${input.case_id_to} does not exist.`,
423
- case_id: input.case_id_to,
424
- };
425
- }
426
- for (const row of [sourceCase, targetCase]) {
427
- if (TERMINAL_CASE_STATUSES.has(row.status)) {
428
- return {
429
- kind: 'rejected',
430
- code: 'case.terminal_status',
431
- message: `Case links cannot target terminal case status ${row.status}.`,
432
- case_id: row.case_id,
433
- };
434
- }
435
- }
436
- const targetRefJson = caseLinkTargetRefJson(input);
437
- const targetRefHashHex = sha256Hex(targetRefJson);
438
- const currentValueJson = currentCreateValueJson(linkAdapter, input, sourceCase, targetCase);
439
- const canonicalExpectedCurrent = canonicalExpectedJson(input.expected_current_value_json);
440
- const proposedNewValueJson = proposedCreateValueJson(input);
441
- let linkId = input.link_id ?? randomLinkId();
442
- if (canonicalExpectedCurrent !== null && canonicalExpectedCurrent !== currentValueJson) {
443
- const token = input.reconfirm_token ?? null;
444
- if (!token) {
445
- return buildRequiresReconfirm({
446
- case_id: input.case_id_from,
447
- target_ref_json: targetRefJson,
448
- target_ref_hash_hex: targetRefHashHex,
449
- current_value_json: currentValueJson,
450
- old_value_json: input.expected_current_value_json ?? null,
451
- proposed_new_value_json: proposedNewValueJson,
452
- confirmed_by: input.created_by,
453
- session_id: input.session_id ?? null,
454
- now,
455
- });
456
- }
457
- const payload = verifyReconfirmToken({
458
- token,
459
- case_id: input.case_id_from,
460
- target_ref_hash_hex: targetRefHashHex,
461
- proposed_value_hash_hex: sha256Hex(proposedNewValueJson),
462
- confirmed_by: input.created_by,
463
- session_id: input.session_id ?? null,
464
- now,
465
- });
466
- if (!payload || payload.current_value_hash_hex !== sha256Hex(currentValueJson)) {
467
- return buildRequiresReconfirm({
468
- case_id: input.case_id_from,
469
- target_ref_json: targetRefJson,
470
- target_ref_hash_hex: targetRefHashHex,
471
- current_value_json: currentValueJson,
472
- old_value_json: input.expected_current_value_json ?? null,
473
- proposed_new_value_json: proposedNewValueJson,
474
- confirmed_by: input.created_by,
475
- session_id: input.session_id ?? null,
476
- now,
477
- });
478
- }
479
- linkId = deterministicLinkId(payload.nonce);
480
- if (linkExistsById(linkAdapter, linkId)) {
481
- return {
482
- kind: 'rejected',
483
- code: 'case.reconfirm_token_replayed',
484
- message: 'Reconfirm token has already been consumed.',
485
- case_id: input.case_id_from,
486
- };
487
- }
488
- }
489
- const fingerprint = toBuffer(input.source_ref_fingerprint) ??
490
- sha256TombstoneFingerprint({
491
- source_case_id: input.case_id_from,
492
- target_case_id: input.case_id_to,
493
- link_type: input.link_type,
494
- });
495
- if (sourceKind === 'manual') {
496
- if (activeLinkForTriple(linkAdapter, input)) {
497
- return {
498
- kind: 'rejected',
499
- code: 'case.correction_active_conflict',
500
- message: 'An active case link already exists for this relationship.',
501
- case_id: input.case_id_from,
502
- };
503
- }
504
- const tombstone = activeTombstoneRow(linkAdapter, {
505
- source_case_id: input.case_id_from,
506
- target_case_id: input.case_id_to,
507
- link_type: input.link_type,
508
- }, fingerprint);
509
- if (tombstone && input.unsuppress_wiki_tombstone !== true) {
510
- return {
511
- kind: 'rejected',
512
- code: 'case.wiki_tombstone_conflict',
513
- message: 'A revoked wiki-emitted link tombstone exists for this relationship.',
514
- case_id: input.case_id_from,
515
- };
516
- }
517
- if (tombstone) {
518
- unsuppressWikiTombstoneInTransaction(linkAdapter, {
519
- source_case_id: input.case_id_from,
520
- target_case_id: input.case_id_to,
521
- link_type: input.link_type,
522
- unsuppressed_by: input.created_by,
523
- now,
524
- }, fingerprint);
525
- }
526
- }
527
- if (activeLinkForTriple(linkAdapter, input)) {
528
- return {
529
- kind: 'rejected',
530
- code: 'case.correction_active_conflict',
531
- message: 'An active case link already exists for this relationship.',
532
- case_id: input.case_id_from,
533
- };
534
- }
535
- linkAdapter
536
- .prepare(`
537
- INSERT INTO case_links (
538
- link_id, case_id_from, case_id_to, link_type, created_at, created_by,
539
- confidence, reason_json, source_kind, source_ref, source_ref_fingerprint,
540
- revoked_at, revoked_by, revoke_reason
541
- )
542
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL)
543
- `)
544
- .run(linkId, input.case_id_from, input.case_id_to, input.link_type, now, input.created_by, input.confidence ?? null, reasonJson(input.reason_json), sourceKind, input.source_ref ?? null, fingerprint);
545
- insertMemoryEvent({
546
- adapter: linkAdapter,
547
- event_type: 'case.link_created',
548
- actor: input.created_by,
549
- case_id: input.case_id_from,
550
- link_id: linkId,
551
- reason: {
552
- case_id_from: input.case_id_from,
553
- case_id_to: input.case_id_to,
554
- link_type: input.link_type,
555
- source_kind: sourceKind,
556
- },
557
- now,
558
- });
559
- return {
560
- kind: 'created',
561
- link_id: linkId,
562
- source_ref_fingerprint_hex: fingerprint.toString('hex'),
563
- };
564
- });
565
- }
566
- function revokeCaseLink(adapter, input) {
567
- const linkAdapter = adapter;
568
- return (0, sqlite_transaction_js_1.runImmediateTransaction)(linkAdapter, () => {
569
- const now = normalizeNow(input.now);
570
- const raw = linkAdapter
571
- .prepare('SELECT * FROM case_links WHERE link_id = ?')
572
- .get(input.link_id);
573
- if (!raw) {
574
- return {
575
- kind: 'rejected',
576
- code: 'case.case_link_not_found',
577
- message: `Case link ${input.link_id} was not found.`,
578
- link_id: input.link_id,
579
- };
580
- }
581
- const row = mapCaseLinkRow(raw);
582
- if (row.revoked_at !== null) {
583
- return {
584
- kind: 'rejected',
585
- code: 'case.case_link_not_found',
586
- message: `Case link ${input.link_id} is already revoked.`,
587
- link_id: input.link_id,
588
- };
589
- }
590
- if (row.source_kind === 'wiki_compiler' && !row.source_ref_fingerprint) {
591
- return {
592
- kind: 'rejected',
593
- code: 'case.wiki_tombstone_fingerprint_missing',
594
- message: 'Cannot revoke a wiki compiler link without its stored source_ref_fingerprint.',
595
- link_id: input.link_id,
596
- };
597
- }
598
- const targetRefJson = caseLinkTargetRefJson(row);
599
- const targetRefHashHex = sha256Hex(targetRefJson);
600
- const currentValueJson = currentRevokeValueJson(row);
601
- const canonicalExpectedCurrent = canonicalExpectedJson(input.expected_current_value_json);
602
- const proposedNewValueJson = (0, canonicalize_js_1.canonicalizeJSON)({
603
- link_id: input.link_id,
604
- revoked_at: now,
605
- revoked_by: input.revoked_by,
606
- revoke_reason: input.revoke_reason,
607
- });
608
- if (canonicalExpectedCurrent !== null && canonicalExpectedCurrent !== currentValueJson) {
609
- const token = input.reconfirm_token ?? null;
610
- if (!token) {
611
- return buildRequiresReconfirm({
612
- case_id: row.case_id_from,
613
- target_ref_json: targetRefJson,
614
- target_ref_hash_hex: targetRefHashHex,
615
- current_value_json: currentValueJson,
616
- old_value_json: input.expected_current_value_json ?? null,
617
- proposed_new_value_json: proposedNewValueJson,
618
- confirmed_by: input.revoked_by,
619
- session_id: input.session_id ?? null,
620
- now,
621
- });
622
- }
623
- const payload = verifyReconfirmToken({
624
- token,
625
- case_id: row.case_id_from,
626
- target_ref_hash_hex: targetRefHashHex,
627
- proposed_value_hash_hex: sha256Hex(proposedNewValueJson),
628
- confirmed_by: input.revoked_by,
629
- session_id: input.session_id ?? null,
630
- now,
631
- });
632
- if (!payload || payload.current_value_hash_hex !== sha256Hex(currentValueJson)) {
633
- return buildRequiresReconfirm({
634
- case_id: row.case_id_from,
635
- target_ref_json: targetRefJson,
636
- target_ref_hash_hex: targetRefHashHex,
637
- current_value_json: currentValueJson,
638
- old_value_json: input.expected_current_value_json ?? null,
639
- proposed_new_value_json: proposedNewValueJson,
640
- confirmed_by: input.revoked_by,
641
- session_id: input.session_id ?? null,
642
- now,
643
- });
644
- }
645
- }
646
- linkAdapter
647
- .prepare(`
648
- UPDATE case_links
649
- SET revoked_at = ?, revoked_by = ?, revoke_reason = ?
650
- WHERE link_id = ?
651
- AND revoked_at IS NULL
652
- `)
653
- .run(now, input.revoked_by, input.revoke_reason, input.link_id);
654
- if (row.source_kind === 'wiki_compiler' && row.source_ref_fingerprint) {
655
- linkAdapter
656
- .prepare(`
657
- INSERT OR IGNORE INTO case_links_revoked_wiki_tombstones (
658
- tombstone_id, case_id_from, case_id_to, link_type, source_ref_fingerprint,
659
- source_ref, created_at, created_by, revoke_reason, unsuppressed_at,
660
- unsuppressed_by
661
- )
662
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL)
663
- `)
664
- .run(randomTombstoneId(), row.case_id_from, row.case_id_to, row.link_type, row.source_ref_fingerprint, row.source_ref ?? `case_link:${row.link_id}`, now, input.revoked_by, input.revoke_reason);
665
- }
666
- insertMemoryEvent({
667
- adapter: linkAdapter,
668
- event_type: 'case.link_revoked',
669
- actor: input.revoked_by,
670
- case_id: row.case_id_from,
671
- link_id: input.link_id,
672
- reason: {
673
- case_id_from: row.case_id_from,
674
- case_id_to: row.case_id_to,
675
- link_type: row.link_type,
676
- revoke_reason: input.revoke_reason,
677
- tombstone_written: row.source_kind === 'wiki_compiler',
678
- },
679
- now,
680
- });
681
- return { kind: 'revoked', link_id: input.link_id };
682
- });
683
- }
684
- function listActiveCaseLinks(adapter, caseId) {
685
- const linkAdapter = adapter;
686
- const resolution = (0, store_js_1.resolveCanonicalCaseChain)(linkAdapter, caseId);
687
- const chain = (0, store_js_1.expandCaseChainForAssembly)(linkAdapter, resolution.terminal_case_id, resolution.chain);
688
- if (chain.length === 0) {
689
- return {
690
- terminal_case_id: resolution.terminal_case_id,
691
- resolved_via_case_id: resolution.resolved_via_case_id,
692
- chain,
693
- links: [],
694
- };
695
- }
696
- const rows = linkAdapter
697
- .prepare(`
698
- SELECT *
699
- FROM case_links
700
- WHERE revoked_at IS NULL
701
- AND (
702
- case_id_from IN (${placeholders(chain)})
703
- OR case_id_to IN (${placeholders(chain)})
704
- )
705
- ORDER BY created_at DESC, link_id ASC
706
- `)
707
- .all(...chain, ...chain);
708
- return {
709
- terminal_case_id: resolution.terminal_case_id,
710
- resolved_via_case_id: resolution.resolved_via_case_id,
711
- chain,
712
- links: rows.map(mapCaseLinkRow),
713
- };
714
- }
715
- function isWikiTombstoneActive(adapter, input) {
716
- return Boolean(activeTombstoneRow(adapter, input));
717
- }
718
- function unsuppressWikiTombstone(adapter, input) {
719
- const linkAdapter = adapter;
720
- return (0, sqlite_transaction_js_1.runImmediateTransaction)(linkAdapter, () => ({
721
- kind: 'unsuppressed',
722
- unsuppressed_count: unsuppressWikiTombstoneInTransaction(linkAdapter, {
723
- source_case_id: input.source_case_id,
724
- target_case_id: input.target_case_id,
725
- link_type: input.link_type,
726
- unsuppressed_by: input.unsuppressed_by,
727
- now: normalizeNow(input.now),
728
- }),
729
- }));
730
- }
731
- function backfillStructuralLinks(adapter) {
732
- const linkAdapter = adapter;
733
- return (0, sqlite_transaction_js_1.runImmediateTransaction)(linkAdapter, () => {
734
- const duplicateResult = linkAdapter
735
- .prepare(`
736
- INSERT OR IGNORE INTO case_links (
737
- link_id, case_id_from, case_id_to, link_type, created_at, created_by,
738
- confidence, reason_json, source_kind, source_ref, source_ref_fingerprint,
739
- revoked_at, revoked_by, revoke_reason
740
- )
741
- SELECT
742
- 'backfill:canonical:' || case_id,
743
- case_id,
744
- canonical_case_id,
745
- 'duplicate-of',
746
- updated_at,
747
- 'system',
748
- NULL,
749
- '{"authority":"case_truth.canonical_case_id"}',
750
- 'system_backfill',
751
- 'case_truth.canonical_case_id',
752
- NULL,
753
- NULL,
754
- NULL,
755
- NULL
756
- FROM case_truth
757
- WHERE canonical_case_id IS NOT NULL
758
- AND canonical_case_id <> case_id
759
- `)
760
- .run();
761
- const splitResult = linkAdapter
762
- .prepare(`
763
- INSERT OR IGNORE INTO case_links (
764
- link_id, case_id_from, case_id_to, link_type, created_at, created_by,
765
- confidence, reason_json, source_kind, source_ref, source_ref_fingerprint,
766
- revoked_at, revoked_by, revoke_reason
767
- )
768
- SELECT
769
- 'backfill:split:' || case_id,
770
- case_id,
771
- split_from_case_id,
772
- 'subcase-of',
773
- updated_at,
774
- 'system',
775
- NULL,
776
- '{"authority":"case_truth.split_from_case_id"}',
777
- 'system_backfill',
778
- 'case_truth.split_from_case_id',
779
- NULL,
780
- NULL,
781
- NULL,
782
- NULL
783
- FROM case_truth
784
- WHERE split_from_case_id IS NOT NULL
785
- AND split_from_case_id <> case_id
786
- `)
787
- .run();
788
- return {
789
- duplicate_of_inserted: duplicateResult.changes,
790
- subcase_of_inserted: splitResult.changes,
791
- };
792
- });
793
- }
794
- //# sourceMappingURL=case-links.js.map