@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,1045 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.insertCaseCorrectionLock = insertCaseCorrectionLock;
4
+ exports.applyCorrection = applyCorrection;
5
+ exports.revertCorrection = revertCorrection;
6
+ exports.supersedeCorrection = supersedeCorrection;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const canonicalize_js_1 = require("../canonicalize.js");
9
+ const sqlite_transaction_js_1 = require("./sqlite-transaction.js");
10
+ const target_ref_js_1 = require("./target-ref.js");
11
+ const RECONFIRM_TOKEN_TTL_MS = 10 * 60_000;
12
+ const TERMINAL_CASE_STATUSES = new Set(['merged', 'archived', 'split']);
13
+ const CORRECTION_FIELD_NAMES = new Set([
14
+ 'status',
15
+ 'status_reason',
16
+ 'primary_actors',
17
+ 'blockers',
18
+ 'confidence',
19
+ ]);
20
+ function normalizeNow(value) {
21
+ return value ?? new Date().toISOString();
22
+ }
23
+ function timestampMs(value) {
24
+ const parsed = Date.parse(value);
25
+ return Number.isFinite(parsed) ? parsed : Date.now();
26
+ }
27
+ function sha256Hex(value) {
28
+ return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
29
+ }
30
+ function hmacHex(secret, value) {
31
+ return (0, node_crypto_1.createHmac)('sha256', secret).update(value, 'utf8').digest('hex');
32
+ }
33
+ function deterministicCorrectionId(nonce) {
34
+ return `corr_${sha256Hex(`case-reconfirm:v1:${nonce}`).slice(0, 32)}`;
35
+ }
36
+ function randomCorrectionId() {
37
+ return `corr_${(0, node_crypto_1.randomUUID)().replace(/-/g, '')}`;
38
+ }
39
+ function isRecord(value) {
40
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
41
+ }
42
+ function parseJsonValue(value) {
43
+ return JSON.parse(value);
44
+ }
45
+ function canonicalJsonOrNull(value) {
46
+ if (typeof value !== 'string') {
47
+ return null;
48
+ }
49
+ return (0, canonicalize_js_1.canonicalizeJSON)(parseJsonValue(value));
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
+ if (!Number.isFinite(n)) {
63
+ throw new Error('Membership confidence must be a finite number.');
64
+ }
65
+ return n;
66
+ }
67
+ function canonicalStoredJson(value) {
68
+ if (value === null || value.trim().length === 0) {
69
+ return (0, canonicalize_js_1.canonicalizeJSON)(null);
70
+ }
71
+ return (0, canonicalize_js_1.canonicalizeJSON)(parseJsonValue(value));
72
+ }
73
+ function decodeSecretWithEncoding(value, encoding) {
74
+ if (value.length === 0) {
75
+ return null;
76
+ }
77
+ if (encoding === 'hex') {
78
+ if (!/^[0-9a-f]+$/i.test(value) || value.length % 2 !== 0) {
79
+ return null;
80
+ }
81
+ const hex = Buffer.from(value, 'hex');
82
+ return hex.length >= 32 ? hex : null;
83
+ }
84
+ const decoded = Buffer.from(value, encoding);
85
+ return decoded.length >= 32 ? decoded : null;
86
+ }
87
+ function decodeSecret(raw) {
88
+ const trimmed = raw.trim();
89
+ if (trimmed.length === 0) {
90
+ return null;
91
+ }
92
+ const explicitEncoding = trimmed.match(/^(hex|base64|utf8):(.*)$/is);
93
+ if (explicitEncoding) {
94
+ return decodeSecretWithEncoding(explicitEncoding[2]?.trim() ?? '', explicitEncoding[1].toLowerCase());
95
+ }
96
+ const hex = decodeSecretWithEncoding(trimmed, 'hex');
97
+ if (hex) {
98
+ return hex;
99
+ }
100
+ const base64 = decodeSecretWithEncoding(trimmed, 'base64');
101
+ if (base64) {
102
+ return base64;
103
+ }
104
+ return decodeSecretWithEncoding(trimmed, 'utf8');
105
+ }
106
+ function secretKid(secret) {
107
+ return (0, node_crypto_1.createHash)('sha256').update(secret).digest('hex').slice(0, 16);
108
+ }
109
+ function currentSecret() {
110
+ const raw = process.env.MAMA_RECONFIRM_TOKEN_SECRET;
111
+ const secret = raw ? decodeSecret(raw) : null;
112
+ if (!secret) {
113
+ throw new Error('MAMA_RECONFIRM_TOKEN_SECRET must be a 32+ byte secret (optionally prefixed with hex:, base64:, or utf8:).');
114
+ }
115
+ return {
116
+ kid: secretKid(secret),
117
+ secret,
118
+ };
119
+ }
120
+ function verificationSecrets() {
121
+ const secrets = new Map();
122
+ const current = currentSecret();
123
+ secrets.set(current.kid, current.secret);
124
+ for (const raw of (process.env.MAMA_RECONFIRM_TOKEN_OLD_SECRETS ?? '').split(',')) {
125
+ const secret = decodeSecret(raw);
126
+ if (secret) {
127
+ secrets.set(secretKid(secret), secret);
128
+ }
129
+ }
130
+ return secrets;
131
+ }
132
+ function encodeReconfirmToken(payload, secret) {
133
+ const payloadJson = (0, canonicalize_js_1.canonicalizeJSON)(payload);
134
+ const signatureHex = hmacHex(secret, payloadJson);
135
+ const envelope = {
136
+ kid: payload.kid,
137
+ payload,
138
+ signature_hex: signatureHex,
139
+ };
140
+ return Buffer.from((0, canonicalize_js_1.canonicalizeJSON)(envelope), 'utf8').toString('base64url');
141
+ }
142
+ function readReconfirmEnvelope(token) {
143
+ try {
144
+ const parsed = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
145
+ if (!isRecord(parsed) || !isRecord(parsed.payload)) {
146
+ return null;
147
+ }
148
+ const payload = parsed.payload;
149
+ if (parsed.kid !== payload.kid ||
150
+ payload.v !== 1 ||
151
+ typeof parsed.kid !== 'string' ||
152
+ typeof parsed.signature_hex !== 'string' ||
153
+ typeof payload.kid !== 'string' ||
154
+ typeof payload.nonce !== 'string' ||
155
+ typeof payload.case_id !== 'string' ||
156
+ typeof payload.target_kind !== 'string' ||
157
+ typeof payload.target_ref_hash_hex !== 'string' ||
158
+ typeof payload.current_value_hash_hex !== 'string' ||
159
+ typeof payload.proposed_value_hash_hex !== 'string' ||
160
+ typeof payload.confirmed_by !== 'string' ||
161
+ !(payload.session_id === null || typeof payload.session_id === 'string') ||
162
+ typeof payload.expires_at !== 'string') {
163
+ return null;
164
+ }
165
+ if (payload.target_kind !== 'case_field' &&
166
+ payload.target_kind !== 'membership' &&
167
+ payload.target_kind !== 'wiki_section') {
168
+ return null;
169
+ }
170
+ return {
171
+ kid: parsed.kid,
172
+ payload: payload,
173
+ signature_hex: parsed.signature_hex,
174
+ };
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
180
+ function verifyReconfirmToken(input) {
181
+ const envelope = readReconfirmEnvelope(input.token);
182
+ if (!envelope) {
183
+ return null;
184
+ }
185
+ const secret = verificationSecrets().get(envelope.kid);
186
+ if (!secret) {
187
+ return null;
188
+ }
189
+ const expected = Buffer.from(hmacHex(secret, (0, canonicalize_js_1.canonicalizeJSON)(envelope.payload)), 'hex');
190
+ const actual = Buffer.from(envelope.signature_hex, 'hex');
191
+ if (actual.length !== expected.length || !(0, node_crypto_1.timingSafeEqual)(actual, expected)) {
192
+ return null;
193
+ }
194
+ const expiresAtMs = Date.parse(envelope.payload.expires_at);
195
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= timestampMs(input.now)) {
196
+ return null;
197
+ }
198
+ if (envelope.payload.case_id !== input.case_id ||
199
+ envelope.payload.target_kind !== input.target_kind ||
200
+ envelope.payload.target_ref_hash_hex !== input.target_ref_hash_hex ||
201
+ envelope.payload.proposed_value_hash_hex !== input.proposed_value_hash_hex ||
202
+ envelope.payload.confirmed_by !== input.confirmed_by ||
203
+ envelope.payload.session_id !== input.session_id) {
204
+ return null;
205
+ }
206
+ return envelope.payload;
207
+ }
208
+ function buildRequiresReconfirm(input) {
209
+ const secret = currentSecret();
210
+ const expiresAt = new Date(timestampMs(input.now) + RECONFIRM_TOKEN_TTL_MS).toISOString();
211
+ const payload = {
212
+ v: 1,
213
+ kid: secret.kid,
214
+ nonce: (0, node_crypto_1.randomUUID)(),
215
+ case_id: input.case_id,
216
+ target_kind: input.target_kind,
217
+ target_ref_hash_hex: input.target_ref_hash_hex,
218
+ current_value_hash_hex: sha256Hex(input.current_value_json),
219
+ proposed_value_hash_hex: sha256Hex(input.proposed_new_value_json),
220
+ confirmed_by: input.confirmed_by,
221
+ session_id: input.session_id,
222
+ expires_at: expiresAt,
223
+ };
224
+ return {
225
+ kind: 'requires_reconfirm',
226
+ code: 'case.correction_requires_reconfirm',
227
+ case_id: input.case_id,
228
+ target_kind: input.target_kind,
229
+ target_ref_json: input.target_ref_json,
230
+ current_value_json: input.current_value_json,
231
+ old_value_json: input.old_value_json,
232
+ proposed_new_value_json: input.proposed_new_value_json,
233
+ reconfirm_token: encodeReconfirmToken(payload, secret.secret),
234
+ reconfirm_token_expires_at: expiresAt,
235
+ message: 'The target changed since the viewed snapshot. Reconfirm against the current value.',
236
+ };
237
+ }
238
+ function requireMatchingTargetKind(input) {
239
+ if (input.target_ref.kind !== input.target_kind) {
240
+ throw new Error(`Correction target_kind ${input.target_kind} does not match target_ref kind ${input.target_ref.kind}.`);
241
+ }
242
+ }
243
+ function correctionFieldName(input) {
244
+ const fieldFromRef = input.target_ref.kind === 'case_field' ? input.target_ref.field : null;
245
+ const field = input.field_name ?? fieldFromRef;
246
+ if (!field || !CORRECTION_FIELD_NAMES.has(field)) {
247
+ throw new Error(`Unsupported case correction field: ${String(field)}`);
248
+ }
249
+ if (input.field_name && fieldFromRef && input.field_name !== fieldFromRef) {
250
+ throw new Error('field_name must match target_ref.field.');
251
+ }
252
+ return field;
253
+ }
254
+ function membershipTarget(input) {
255
+ if (input.target_ref.kind !== 'membership') {
256
+ throw new Error('Membership correction requires membership target_ref.');
257
+ }
258
+ return input.target_ref;
259
+ }
260
+ function readCaseFieldCurrentValueJson(adapter, caseId, field) {
261
+ const row = adapter
262
+ .prepare(`
263
+ SELECT status, status_reason, primary_actors, blockers, confidence
264
+ FROM case_truth
265
+ WHERE case_id = ?
266
+ `)
267
+ .get(caseId);
268
+ if (!row) {
269
+ throw new Error(`case_truth row disappeared while applying correction: ${caseId}`);
270
+ }
271
+ if (field === 'primary_actors' || field === 'blockers') {
272
+ return canonicalStoredJson(row[field]);
273
+ }
274
+ if (field === 'confidence') {
275
+ if (row.confidence === null || row.confidence === undefined) {
276
+ return (0, canonicalize_js_1.canonicalizeJSON)(null);
277
+ }
278
+ const confidence = Number(row.confidence);
279
+ if (!Number.isFinite(confidence)) {
280
+ throw new Error(`case_truth confidence is not a finite number for case_id=${caseId}`);
281
+ }
282
+ return (0, canonicalize_js_1.canonicalizeJSON)(confidence);
283
+ }
284
+ return (0, canonicalize_js_1.canonicalizeJSON)(row[field]);
285
+ }
286
+ function readMembershipCurrentValueJson(adapter, caseId, sourceType, sourceId) {
287
+ const row = adapter
288
+ .prepare(`
289
+ SELECT status, role, confidence, reason, user_locked
290
+ FROM case_memberships
291
+ WHERE case_id = ?
292
+ AND source_type = ?
293
+ AND source_id = ?
294
+ `)
295
+ .get(caseId, sourceType, sourceId);
296
+ if (!row) {
297
+ return (0, canonicalize_js_1.canonicalizeJSON)(null);
298
+ }
299
+ return (0, canonicalize_js_1.canonicalizeJSON)({
300
+ status: row.status,
301
+ role: row.role,
302
+ confidence: row.confidence,
303
+ reason: row.reason,
304
+ user_locked: Number(row.user_locked) === 1 ? 1 : 0,
305
+ });
306
+ }
307
+ function readCurrentValueJson(adapter, input) {
308
+ if (input.target_kind === 'case_field') {
309
+ return readCaseFieldCurrentValueJson(adapter, input.case_id, correctionFieldName(input));
310
+ }
311
+ if (input.target_kind === 'membership') {
312
+ const target = membershipTarget(input);
313
+ return readMembershipCurrentValueJson(adapter, input.case_id, target.source_type, target.source_id);
314
+ }
315
+ return (0, canonicalize_js_1.canonicalizeJSON)(null);
316
+ }
317
+ function storageValueForCaseField(field, newValueJson) {
318
+ const value = parseJsonValue(newValueJson);
319
+ if (value === null) {
320
+ return null;
321
+ }
322
+ if (field === 'primary_actors' || field === 'blockers') {
323
+ return (0, canonicalize_js_1.canonicalizeJSON)(value);
324
+ }
325
+ if (field === 'confidence') {
326
+ const confidence = Number(value);
327
+ if (!Number.isFinite(confidence)) {
328
+ throw new Error('Case field confidence correction value must be a finite number or null.');
329
+ }
330
+ return String(confidence);
331
+ }
332
+ if (typeof value !== 'string') {
333
+ throw new Error(`Case field ${field} correction value must be a JSON string or null.`);
334
+ }
335
+ return value;
336
+ }
337
+ function applyCaseFieldMutation(input) {
338
+ const storageValue = storageValueForCaseField(input.field, input.new_value_json);
339
+ const result = input.adapter
340
+ .prepare(`
341
+ UPDATE case_truth
342
+ SET ${input.field} = ?,
343
+ updated_at = ?
344
+ WHERE case_id = ?
345
+ AND status NOT IN ('merged','archived','split')
346
+ `)
347
+ .run(storageValue, input.now, input.case_id);
348
+ if (result.changes !== 1) {
349
+ throw new Error(`Failed to apply case field correction for case_id=${input.case_id}.`);
350
+ }
351
+ const actual = readCaseFieldCurrentValueJson(input.adapter, input.case_id, input.field);
352
+ const expected = (0, canonicalize_js_1.canonicalizeJSON)(parseJsonValue(input.new_value_json));
353
+ if (actual !== expected) {
354
+ throw new Error(`Case field correction verification failed for ${input.field}.`);
355
+ }
356
+ }
357
+ function membershipStatus(value) {
358
+ if (value === 'active' || value === 'excluded' || value === 'removed') {
359
+ return value;
360
+ }
361
+ throw new Error('Membership correction status must be active, excluded, or removed.');
362
+ }
363
+ function verifyMembershipMutation(input) {
364
+ const row = input.adapter
365
+ .prepare(`
366
+ SELECT status, role, confidence, reason, user_locked, added_by
367
+ FROM case_memberships
368
+ WHERE case_id = ?
369
+ AND source_type = ?
370
+ AND source_id = ?
371
+ `)
372
+ .get(input.case_id, input.source_type, input.source_id);
373
+ if (!row) {
374
+ throw new Error('Membership correction verification failed: row missing.');
375
+ }
376
+ const expectedStatus = String(input.expected.status);
377
+ if (row.status !== expectedStatus ||
378
+ Number(row.user_locked) !== 1 ||
379
+ row.added_by !== 'user-correction') {
380
+ throw new Error('Membership correction verification failed.');
381
+ }
382
+ if (expectedStatus === 'active') {
383
+ const expectedRole = nullableString(input.expected.role);
384
+ const expectedConfidence = nullableNumber(input.expected.confidence);
385
+ const expectedReason = nullableString(input.expected.reason);
386
+ if (row.role !== expectedRole ||
387
+ row.confidence !== expectedConfidence ||
388
+ row.reason !== expectedReason) {
389
+ throw new Error('Active membership correction verification failed.');
390
+ }
391
+ }
392
+ }
393
+ function applyMembershipMutation(input) {
394
+ const value = parseJsonValue(input.new_value_json);
395
+ if (!isRecord(value)) {
396
+ throw new Error('Membership correction value must be a JSON object.');
397
+ }
398
+ const status = membershipStatus(value.status);
399
+ if (status === 'removed') {
400
+ input.adapter
401
+ .prepare(`
402
+ UPDATE case_memberships
403
+ SET status = 'removed',
404
+ user_locked = 1,
405
+ added_by = 'user-correction',
406
+ updated_at = ?
407
+ WHERE case_id = ?
408
+ AND source_type = ?
409
+ AND source_id = ?
410
+ `)
411
+ .run(input.now, input.case_id, input.target.source_type, input.target.source_id);
412
+ }
413
+ else if (status === 'excluded') {
414
+ const confidence = nullableNumber(value.confidence);
415
+ const reason = nullableString(value.reason) ?? input.reason;
416
+ input.adapter
417
+ .prepare(`
418
+ INSERT INTO case_memberships (
419
+ case_id, source_type, source_id, role, confidence, reason, status,
420
+ added_by, added_at, user_locked, updated_at
421
+ )
422
+ VALUES (?, ?, ?, NULL, ?, ?, 'excluded', 'user-correction', ?, 1, ?)
423
+ ON CONFLICT(case_id, source_type, source_id) DO UPDATE SET
424
+ role = excluded.role,
425
+ confidence = excluded.confidence,
426
+ reason = excluded.reason,
427
+ status = excluded.status,
428
+ added_by = 'user-correction',
429
+ user_locked = 1,
430
+ updated_at = excluded.updated_at
431
+ `)
432
+ .run(input.case_id, input.target.source_type, input.target.source_id, confidence, reason, input.now, input.now);
433
+ }
434
+ else {
435
+ const role = nullableString(value.role);
436
+ const confidence = nullableNumber(value.confidence);
437
+ const reason = nullableString(value.reason);
438
+ input.adapter
439
+ .prepare(`
440
+ INSERT INTO case_memberships (
441
+ case_id, source_type, source_id, role, confidence, reason, status,
442
+ added_by, added_at, user_locked, updated_at
443
+ )
444
+ VALUES (?, ?, ?, ?, ?, ?, 'active', 'user-correction', ?, 1, ?)
445
+ ON CONFLICT(case_id, source_type, source_id) DO UPDATE SET
446
+ role = excluded.role,
447
+ confidence = excluded.confidence,
448
+ reason = excluded.reason,
449
+ status = excluded.status,
450
+ added_by = 'user-correction',
451
+ user_locked = 1,
452
+ updated_at = excluded.updated_at
453
+ `)
454
+ .run(input.case_id, input.target.source_type, input.target.source_id, role, confidence, reason, input.now, input.now);
455
+ }
456
+ verifyMembershipMutation({
457
+ adapter: input.adapter,
458
+ case_id: input.case_id,
459
+ source_type: input.target.source_type,
460
+ source_id: input.target.source_id,
461
+ expected: value,
462
+ });
463
+ }
464
+ function applyTargetMutation(input) {
465
+ if (input.correction.target_kind === 'case_field') {
466
+ applyCaseFieldMutation({
467
+ adapter: input.adapter,
468
+ case_id: input.correction.case_id,
469
+ field: correctionFieldName(input.correction),
470
+ new_value_json: input.correction.new_value_json,
471
+ now: input.now,
472
+ });
473
+ return;
474
+ }
475
+ if (input.correction.target_kind === 'membership') {
476
+ applyMembershipMutation({
477
+ adapter: input.adapter,
478
+ case_id: input.correction.case_id,
479
+ target: membershipTarget(input.correction),
480
+ new_value_json: input.correction.new_value_json,
481
+ reason: input.correction.reason,
482
+ now: input.now,
483
+ });
484
+ }
485
+ }
486
+ function revertMembershipValueJson(value) {
487
+ const canonicalValue = canonicalJsonOrNull(value);
488
+ if (canonicalValue === null || canonicalValue === (0, canonicalize_js_1.canonicalizeJSON)(null)) {
489
+ return (0, canonicalize_js_1.canonicalizeJSON)({ status: 'removed' });
490
+ }
491
+ return canonicalValue;
492
+ }
493
+ function insertCorrectionMemoryEvent(input) {
494
+ const evidenceRefs = [input.correction_id, ...(input.extra_refs ?? [])];
495
+ input.adapter
496
+ .prepare(`
497
+ INSERT INTO memory_events (
498
+ event_id, event_type, actor, source_turn_id, memory_id, topic,
499
+ scope_refs, evidence_refs, reason, created_at
500
+ )
501
+ VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
502
+ `)
503
+ .run(`evt_${(0, node_crypto_1.randomUUID)().replace(/-/g, '')}`, input.event_type, input.actor, input.source_turn_id ?? null, `case:${input.case_id}`, (0, canonicalize_js_1.canonicalizeJSON)([{ type: 'case', id: input.case_id }]), (0, canonicalize_js_1.canonicalizeJSON)(evidenceRefs), (0, canonicalize_js_1.canonicalizeJSON)({
504
+ correction_id: input.correction_id,
505
+ confirmation_summary: input.confirmation_summary,
506
+ ...(input.extra_refs ? { predecessor_correction_ids: input.extra_refs } : {}),
507
+ }), timestampMs(input.now));
508
+ }
509
+ function activeCorrectionForTarget(adapter, caseId, targetRefHash) {
510
+ const row = adapter
511
+ .prepare(`
512
+ SELECT correction_id
513
+ FROM case_corrections
514
+ WHERE case_id = ?
515
+ AND target_ref_hash = ?
516
+ AND is_lock_active = 1
517
+ AND reverted_at IS NULL
518
+ LIMIT 1
519
+ `)
520
+ .get(caseId, targetRefHash);
521
+ return row ?? null;
522
+ }
523
+ function correctionExists(adapter, correctionId) {
524
+ const row = adapter
525
+ .prepare('SELECT correction_id FROM case_corrections WHERE correction_id = ?')
526
+ .get(correctionId);
527
+ return row !== undefined;
528
+ }
529
+ function isUniqueConstraintError(error) {
530
+ return (error instanceof Error && /UNIQUE constraint|SQLITE_CONSTRAINT_UNIQUE/i.test(error.message));
531
+ }
532
+ function insertCaseCorrectionLock(adapter, input) {
533
+ const correctionAdapter = adapter;
534
+ const canonical = (0, target_ref_js_1.canonicalTargetRef)(input.target_ref);
535
+ const correctionId = input.correction_id ?? randomCorrectionId();
536
+ const isLockActive = input.is_lock_active ?? 1;
537
+ correctionAdapter
538
+ .prepare(`
539
+ INSERT INTO case_corrections (
540
+ correction_id, case_id, target_kind, target_ref_json, target_ref_hash,
541
+ field_name, old_value_json, new_value_json, reason,
542
+ applied_by, applied_at, is_lock_active, reverted_at, superseded_by
543
+ )
544
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?);
545
+ `)
546
+ .run(correctionId, input.case_id, input.target_kind, canonical.json, canonical.hash, input.field_name ?? null, input.old_value_json, input.new_value_json, input.reason, input.applied_by, input.applied_at, isLockActive, input.superseded_by ?? null);
547
+ return {
548
+ correction_id: correctionId,
549
+ case_id: input.case_id,
550
+ target_kind: input.target_kind,
551
+ target_ref_json: canonical.json,
552
+ target_ref_hash: canonical.hash,
553
+ canonical_hash_hex: canonical.hash.toString('hex'),
554
+ applied_at: input.applied_at,
555
+ is_lock_active: isLockActive,
556
+ };
557
+ }
558
+ function applyCorrection(adapter, input) {
559
+ const correctionAdapter = adapter;
560
+ return (0, sqlite_transaction_js_1.runImmediateTransaction)(correctionAdapter, () => {
561
+ requireMatchingTargetKind(input);
562
+ const now = normalizeNow(input.now);
563
+ const caseRow = correctionAdapter
564
+ .prepare(`
565
+ SELECT case_id, status, canonical_case_id
566
+ FROM case_truth
567
+ WHERE case_id = ?;
568
+ `)
569
+ .get(input.case_id);
570
+ if (!caseRow) {
571
+ return { kind: 'precompile_gap', code: 'case.precompile_gap', case_id: input.case_id };
572
+ }
573
+ if (TERMINAL_CASE_STATUSES.has(caseRow.status)) {
574
+ return {
575
+ kind: 'rejected',
576
+ code: 'case.terminal_status',
577
+ message: `Active corrections cannot target terminal case status ${caseRow.status}.`,
578
+ case_id: input.case_id,
579
+ };
580
+ }
581
+ const canonical = (0, target_ref_js_1.canonicalTargetRef)(input.target_ref);
582
+ const targetRefHashHex = canonical.hash.toString('hex');
583
+ const currentValueJson = readCurrentValueJson(correctionAdapter, input);
584
+ const expectedOldValueJson = canonicalJsonOrNull(input.old_value_json);
585
+ let correctionId = randomCorrectionId();
586
+ if (expectedOldValueJson !== null && expectedOldValueJson !== currentValueJson) {
587
+ const token = input.reconfirm_token ?? null;
588
+ if (!token) {
589
+ return buildRequiresReconfirm({
590
+ case_id: input.case_id,
591
+ target_kind: input.target_kind,
592
+ target_ref_json: canonical.json,
593
+ target_ref_hash_hex: targetRefHashHex,
594
+ current_value_json: currentValueJson,
595
+ old_value_json: expectedOldValueJson,
596
+ proposed_new_value_json: input.new_value_json,
597
+ confirmed_by: input.confirmed_by,
598
+ session_id: input.session_id ?? null,
599
+ now,
600
+ });
601
+ }
602
+ const payload = verifyReconfirmToken({
603
+ token,
604
+ case_id: input.case_id,
605
+ target_kind: input.target_kind,
606
+ target_ref_hash_hex: targetRefHashHex,
607
+ proposed_value_hash_hex: sha256Hex(input.new_value_json),
608
+ confirmed_by: input.confirmed_by,
609
+ session_id: input.session_id ?? null,
610
+ now,
611
+ });
612
+ if (!payload) {
613
+ return buildRequiresReconfirm({
614
+ case_id: input.case_id,
615
+ target_kind: input.target_kind,
616
+ target_ref_json: canonical.json,
617
+ target_ref_hash_hex: targetRefHashHex,
618
+ current_value_json: currentValueJson,
619
+ old_value_json: expectedOldValueJson,
620
+ proposed_new_value_json: input.new_value_json,
621
+ confirmed_by: input.confirmed_by,
622
+ session_id: input.session_id ?? null,
623
+ now,
624
+ });
625
+ }
626
+ correctionId = deterministicCorrectionId(payload.nonce);
627
+ if (correctionExists(correctionAdapter, correctionId)) {
628
+ return {
629
+ kind: 'rejected',
630
+ code: 'case.reconfirm_token_replayed',
631
+ message: 'Reconfirm token has already been consumed.',
632
+ case_id: input.case_id,
633
+ };
634
+ }
635
+ if (payload.current_value_hash_hex !== sha256Hex(currentValueJson)) {
636
+ return buildRequiresReconfirm({
637
+ case_id: input.case_id,
638
+ target_kind: input.target_kind,
639
+ target_ref_json: canonical.json,
640
+ target_ref_hash_hex: targetRefHashHex,
641
+ current_value_json: currentValueJson,
642
+ old_value_json: expectedOldValueJson,
643
+ proposed_new_value_json: input.new_value_json,
644
+ confirmed_by: input.confirmed_by,
645
+ session_id: input.session_id ?? null,
646
+ now,
647
+ });
648
+ }
649
+ }
650
+ if (activeCorrectionForTarget(correctionAdapter, input.case_id, canonical.hash)) {
651
+ return {
652
+ kind: 'rejected',
653
+ code: 'case.correction_active_conflict',
654
+ message: 'An active correction already locks this target.',
655
+ case_id: input.case_id,
656
+ };
657
+ }
658
+ let inserted;
659
+ try {
660
+ inserted = insertCaseCorrectionLock(adapter, {
661
+ correction_id: correctionId,
662
+ case_id: input.case_id,
663
+ target_kind: input.target_kind,
664
+ target_ref: input.target_ref,
665
+ field_name: input.target_kind === 'case_field' ? correctionFieldName(input) : null,
666
+ old_value_json: currentValueJson,
667
+ new_value_json: input.new_value_json,
668
+ reason: input.reason,
669
+ applied_by: input.confirmed_by,
670
+ applied_at: now,
671
+ is_lock_active: 1,
672
+ superseded_by: null,
673
+ });
674
+ }
675
+ catch (error) {
676
+ if (isUniqueConstraintError(error)) {
677
+ return {
678
+ kind: 'rejected',
679
+ code: 'case.correction_active_conflict',
680
+ message: 'An active correction already locks this target.',
681
+ case_id: input.case_id,
682
+ };
683
+ }
684
+ throw error;
685
+ }
686
+ applyTargetMutation({ adapter: correctionAdapter, correction: input, now });
687
+ insertCorrectionMemoryEvent({
688
+ adapter: correctionAdapter,
689
+ event_type: 'case.correction_applied',
690
+ correction_id: inserted.correction_id,
691
+ case_id: input.case_id,
692
+ actor: input.confirmed_by,
693
+ source_turn_id: input.session_id ?? null,
694
+ confirmation_summary: input.confirmation_summary,
695
+ now,
696
+ });
697
+ return {
698
+ kind: 'applied',
699
+ correction_id: inserted.correction_id,
700
+ case_id: input.case_id,
701
+ target_ref_json: inserted.target_ref_json,
702
+ canonical_hash_hex: inserted.canonical_hash_hex,
703
+ };
704
+ });
705
+ }
706
+ function revertCorrection(adapter, input) {
707
+ const correctionAdapter = adapter;
708
+ return (0, sqlite_transaction_js_1.runImmediateTransaction)(correctionAdapter, () => {
709
+ const now = normalizeNow(input.now);
710
+ const row = correctionAdapter
711
+ .prepare(`
712
+ SELECT correction_id, case_id, target_kind, target_ref_json, field_name,
713
+ old_value_json, is_lock_active, reverted_at
714
+ , superseded_by
715
+ FROM case_corrections
716
+ WHERE correction_id = ?
717
+ `)
718
+ .get(input.correction_id);
719
+ if (!row) {
720
+ return {
721
+ kind: 'rejected',
722
+ code: 'case.correction_not_found',
723
+ message: 'Correction not found.',
724
+ correction_id: input.correction_id,
725
+ };
726
+ }
727
+ if (row.reverted_at !== null) {
728
+ return {
729
+ kind: 'rejected',
730
+ code: 'case.correction_reverted',
731
+ message: 'Correction has already been reverted.',
732
+ correction_id: input.correction_id,
733
+ };
734
+ }
735
+ if (Number(row.is_lock_active) === 0 && row.superseded_by) {
736
+ return {
737
+ kind: 'rejected',
738
+ code: 'case.correction_already_superseded',
739
+ message: 'Correction has already been superseded.',
740
+ correction_id: input.correction_id,
741
+ };
742
+ }
743
+ if (Number(row.is_lock_active) === 0) {
744
+ return {
745
+ kind: 'rejected',
746
+ code: 'case.correction_inactive',
747
+ message: 'Correction is inactive.',
748
+ correction_id: input.correction_id,
749
+ };
750
+ }
751
+ const caseRow = correctionAdapter
752
+ .prepare('SELECT status, canonical_case_id FROM case_truth WHERE case_id = ?')
753
+ .get(row.case_id);
754
+ if (!caseRow) {
755
+ return {
756
+ kind: 'precompile_gap',
757
+ code: 'case.precompile_gap',
758
+ case_id: row.case_id,
759
+ };
760
+ }
761
+ if (TERMINAL_CASE_STATUSES.has(caseRow.status)) {
762
+ return {
763
+ kind: 'rejected',
764
+ code: 'case.terminal_status',
765
+ message: 'Case is in a terminal status.',
766
+ case_id: row.case_id,
767
+ };
768
+ }
769
+ applyTargetMutation({
770
+ adapter: correctionAdapter,
771
+ correction: {
772
+ case_id: row.case_id,
773
+ target_kind: row.target_kind,
774
+ target_ref: parseJsonValue(row.target_ref_json),
775
+ field_name: row.field_name ?? undefined,
776
+ old_value_json: null,
777
+ new_value_json: row.target_kind === 'membership'
778
+ ? revertMembershipValueJson(row.old_value_json)
779
+ : (row.old_value_json ?? (0, canonicalize_js_1.canonicalizeJSON)(null)),
780
+ reason: 'Revert correction',
781
+ confirmed: true,
782
+ confirmed_by: input.confirmed_by,
783
+ confirmation_summary: input.confirmation_summary,
784
+ },
785
+ now,
786
+ });
787
+ correctionAdapter
788
+ .prepare(`
789
+ UPDATE case_corrections
790
+ SET reverted_at = ?,
791
+ is_lock_active = 0
792
+ WHERE correction_id = ?
793
+ `)
794
+ .run(now, input.correction_id);
795
+ insertCorrectionMemoryEvent({
796
+ adapter: correctionAdapter,
797
+ event_type: 'case.correction_reverted',
798
+ correction_id: input.correction_id,
799
+ case_id: row.case_id,
800
+ actor: input.confirmed_by,
801
+ confirmation_summary: input.confirmation_summary,
802
+ now,
803
+ });
804
+ return { kind: 'reverted', correction_id: input.correction_id, case_id: row.case_id };
805
+ });
806
+ }
807
+ /**
808
+ * supersedeCorrection — Phase 2b. Unblocked by spec amendment-8 (2026-04-18)
809
+ * which added `superseded_by` to the mutable-column set in §5.4.
810
+ *
811
+ * Semantics: user changes their mind on an active correction. Inside a single
812
+ * transaction:
813
+ * 1. Read OLD row; reject if not found / reverted / already superseded / inactive.
814
+ * 2. INSERT new correction row referencing the same (case_id, target_kind,
815
+ * target_ref_hash) with new_value_json and is_lock_active=1.
816
+ * 3. UPDATE OLD row: superseded_by = new.correction_id, is_lock_active = 0.
817
+ * 4. Emit case.correction_superseded memory_event.
818
+ *
819
+ * Per spec §5.4 invariant (amendment-8), the three mutable columns are
820
+ * is_lock_active + reverted_at + superseded_by. superseded_by is written
821
+ * ONLY inside this transition.
822
+ */
823
+ function supersedeCorrection(adapter, input) {
824
+ const correctionAdapter = adapter;
825
+ return (0, sqlite_transaction_js_1.runImmediateTransaction)(correctionAdapter, () => {
826
+ const now = normalizeNow(input.now);
827
+ const old = correctionAdapter
828
+ .prepare(`
829
+ SELECT correction_id, case_id, target_kind, target_ref_json, target_ref_hash,
830
+ field_name, is_lock_active, reverted_at, superseded_by
831
+ FROM case_corrections
832
+ WHERE correction_id = ?
833
+ `)
834
+ .get(input.old_correction_id);
835
+ if (!old) {
836
+ return {
837
+ kind: 'rejected',
838
+ code: 'case.correction_not_found',
839
+ message: 'Old correction not found.',
840
+ correction_id: input.old_correction_id,
841
+ };
842
+ }
843
+ if (old.reverted_at !== null) {
844
+ return {
845
+ kind: 'rejected',
846
+ code: 'case.correction_reverted',
847
+ message: 'Cannot supersede a reverted correction.',
848
+ correction_id: input.old_correction_id,
849
+ };
850
+ }
851
+ if (old.superseded_by !== null) {
852
+ return {
853
+ kind: 'rejected',
854
+ code: 'case.correction_already_superseded',
855
+ message: 'Correction has already been superseded.',
856
+ correction_id: input.old_correction_id,
857
+ };
858
+ }
859
+ if (Number(old.is_lock_active) === 0) {
860
+ return {
861
+ kind: 'rejected',
862
+ code: 'case.correction_inactive',
863
+ message: 'Correction is inactive.',
864
+ correction_id: input.old_correction_id,
865
+ };
866
+ }
867
+ // Terminal-case gate — mirror applyCorrection. Supersede must not
868
+ // rotate locks or mutate live state on merged/archived/split cases.
869
+ // Without this guard, case_field supersede throws from
870
+ // applyCaseFieldMutation (transaction rolls back but handler leaks
871
+ // 500) and membership supersede silently writes to a dead case.
872
+ const caseRow = correctionAdapter
873
+ .prepare('SELECT status, canonical_case_id FROM case_truth WHERE case_id = ?')
874
+ .get(old.case_id);
875
+ if (!caseRow) {
876
+ return {
877
+ kind: 'precompile_gap',
878
+ code: 'case.precompile_gap',
879
+ case_id: old.case_id,
880
+ };
881
+ }
882
+ if (TERMINAL_CASE_STATUSES.has(caseRow.status)) {
883
+ return {
884
+ kind: 'rejected',
885
+ code: 'case.terminal_status',
886
+ message: `Supersede rejected: case is in terminal status ${caseRow.status}.`,
887
+ case_id: old.case_id,
888
+ };
889
+ }
890
+ let newCorrectionId = randomCorrectionId();
891
+ // Reconstruct the ApplyCorrection-shaped input from OLD's canonical
892
+ // target_ref_json so we can reuse readCurrentValueJson + applyTargetMutation.
893
+ // The JSON round-trips through canonicalizeJSON on insert so it is safe to
894
+ // parse here without re-validating shape.
895
+ const syntheticTargetRef = parseJsonValue(old.target_ref_json);
896
+ const syntheticInput = {
897
+ case_id: old.case_id,
898
+ target_kind: old.target_kind,
899
+ target_ref: syntheticTargetRef,
900
+ field_name: old.field_name ?? undefined,
901
+ new_value_json: input.new_value_json,
902
+ reason: input.reason,
903
+ confirmed: true,
904
+ confirmed_by: input.confirmed_by,
905
+ confirmation_summary: input.confirmation_summary,
906
+ };
907
+ // Snapshot the current live value BEFORE mutating so the NEW row's
908
+ // old_value_json reflects the state the user actually superseded
909
+ // (not NULL, which would erase the audit trail).
910
+ const currentValueJson = readCurrentValueJson(correctionAdapter, syntheticInput);
911
+ const targetRefHashHex = Buffer.from(old.target_ref_hash).toString('hex');
912
+ // CAS / reconfirm dance — mirrors applyCorrection. Now that supersede
913
+ // actually writes live state (P1-1 fix), a stale-view supersede could
914
+ // silently overwrite a value the user never saw. Opt-in via
915
+ // expected_current_value_json (undefined/null = skip CAS).
916
+ //
917
+ // Canonicalize the caller's expected value before comparison so
918
+ // key-ordering or whitespace differences do not cause false 409s.
919
+ // Invalid JSON (including empty string "") throws at the handler
920
+ // boundary — the caller sees a validation_error rather than a bogus
921
+ // silent 409.
922
+ let canonicalExpectedCurrent = null;
923
+ if (typeof input.expected_current_value_json === 'string') {
924
+ canonicalExpectedCurrent = (0, canonicalize_js_1.canonicalizeJSON)(parseJsonValue(input.expected_current_value_json));
925
+ }
926
+ if (canonicalExpectedCurrent !== null && canonicalExpectedCurrent !== currentValueJson) {
927
+ const token = input.reconfirm_token ?? null;
928
+ if (!token) {
929
+ return buildRequiresReconfirm({
930
+ case_id: old.case_id,
931
+ target_kind: old.target_kind,
932
+ target_ref_json: old.target_ref_json,
933
+ target_ref_hash_hex: targetRefHashHex,
934
+ current_value_json: currentValueJson,
935
+ old_value_json: input.expected_current_value_json ?? null,
936
+ proposed_new_value_json: input.new_value_json,
937
+ confirmed_by: input.confirmed_by,
938
+ session_id: input.session_id ?? null,
939
+ now,
940
+ });
941
+ }
942
+ const payload = verifyReconfirmToken({
943
+ token,
944
+ case_id: old.case_id,
945
+ target_kind: old.target_kind,
946
+ target_ref_hash_hex: targetRefHashHex,
947
+ proposed_value_hash_hex: sha256Hex(input.new_value_json),
948
+ confirmed_by: input.confirmed_by,
949
+ session_id: input.session_id ?? null,
950
+ now,
951
+ });
952
+ if (!payload) {
953
+ return buildRequiresReconfirm({
954
+ case_id: old.case_id,
955
+ target_kind: old.target_kind,
956
+ target_ref_json: old.target_ref_json,
957
+ target_ref_hash_hex: targetRefHashHex,
958
+ current_value_json: currentValueJson,
959
+ old_value_json: input.expected_current_value_json ?? null,
960
+ proposed_new_value_json: input.new_value_json,
961
+ confirmed_by: input.confirmed_by,
962
+ session_id: input.session_id ?? null,
963
+ now,
964
+ });
965
+ }
966
+ newCorrectionId = deterministicCorrectionId(payload.nonce);
967
+ if (correctionExists(correctionAdapter, newCorrectionId)) {
968
+ return {
969
+ kind: 'rejected',
970
+ code: 'case.reconfirm_token_replayed',
971
+ message: 'Reconfirm token has already been consumed.',
972
+ case_id: old.case_id,
973
+ };
974
+ }
975
+ if (payload.current_value_hash_hex !== sha256Hex(currentValueJson)) {
976
+ return buildRequiresReconfirm({
977
+ case_id: old.case_id,
978
+ target_kind: old.target_kind,
979
+ target_ref_json: old.target_ref_json,
980
+ target_ref_hash_hex: targetRefHashHex,
981
+ current_value_json: currentValueJson,
982
+ old_value_json: input.expected_current_value_json ?? null,
983
+ proposed_new_value_json: input.new_value_json,
984
+ confirmed_by: input.confirmed_by,
985
+ session_id: input.session_id ?? null,
986
+ now,
987
+ });
988
+ }
989
+ }
990
+ // Drop OLD lock FIRST so the partial-unique (case_id, target_ref_hash)
991
+ // slot is free before INSERTing the NEW active row.
992
+ correctionAdapter
993
+ .prepare(`
994
+ UPDATE case_corrections
995
+ SET is_lock_active = 0
996
+ WHERE correction_id = ?
997
+ `)
998
+ .run(input.old_correction_id);
999
+ insertCaseCorrectionLock(adapter, {
1000
+ correction_id: newCorrectionId,
1001
+ case_id: old.case_id,
1002
+ target_kind: old.target_kind,
1003
+ target_ref: syntheticTargetRef,
1004
+ field_name: old.field_name ?? undefined,
1005
+ old_value_json: currentValueJson,
1006
+ new_value_json: input.new_value_json,
1007
+ reason: input.reason,
1008
+ applied_by: input.confirmed_by,
1009
+ applied_at: now,
1010
+ is_lock_active: 1,
1011
+ });
1012
+ // Populate linkage last — sole mutation of superseded_by, permitted by
1013
+ // spec §5.4 amendment-8 (2026-04-18).
1014
+ correctionAdapter
1015
+ .prepare(`
1016
+ UPDATE case_corrections
1017
+ SET superseded_by = ?
1018
+ WHERE correction_id = ?
1019
+ `)
1020
+ .run(newCorrectionId, input.old_correction_id);
1021
+ // Write the revised value to the live target (case_truth / case_memberships)
1022
+ // so the correction drawer and the case board stay consistent. wiki_section
1023
+ // targets are overlay-only and intentionally no-op here.
1024
+ applyTargetMutation({ adapter: correctionAdapter, correction: syntheticInput, now });
1025
+ insertCorrectionMemoryEvent({
1026
+ adapter: correctionAdapter,
1027
+ event_type: 'case.correction_superseded',
1028
+ correction_id: newCorrectionId,
1029
+ case_id: old.case_id,
1030
+ actor: input.confirmed_by,
1031
+ confirmation_summary: input.confirmation_summary,
1032
+ now,
1033
+ extra_refs: [old.correction_id],
1034
+ });
1035
+ return {
1036
+ kind: 'superseded',
1037
+ old_correction_id: input.old_correction_id,
1038
+ new_correction_id: newCorrectionId,
1039
+ case_id: old.case_id,
1040
+ target_ref_json: old.target_ref_json,
1041
+ canonical_hash_hex: Buffer.from(old.target_ref_hash).toString('hex'),
1042
+ };
1043
+ });
1044
+ }
1045
+ //# sourceMappingURL=corrections.js.map