@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.
- package/README.md +48 -10
- package/db/migrations/026-create-canonical-entity-tables.sql +99 -0
- package/db/migrations/027-create-entity-review-support.sql +44 -0
- package/db/migrations/028-create-entity-audit-tables.sql +45 -0
- package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
- package/db/migrations/030-case-first-memory-substrate.sql +901 -0
- package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
- package/db/migrations/032-add-memory-provenance-columns.sql +21 -0
- package/db/migrations/033-create-model-runs-and-tool-traces.sql +53 -0
- package/db/migrations/034-add-connector-event-scope-columns.sql +14 -0
- package/db/migrations/035-create-twin-edges.sql +63 -0
- package/db/migrations/036-create-agent-situation-packets.sql +61 -0
- package/dist/agent-graph/alias-write.d.ts +3 -0
- package/dist/agent-graph/alias-write.js +231 -0
- package/dist/agent-graph/entity-resolve.d.ts +3 -0
- package/dist/agent-graph/entity-resolve.js +275 -0
- package/dist/agent-graph/errors.d.ts +4 -0
- package/dist/agent-graph/errors.js +11 -0
- package/dist/agent-graph/graph-query.d.ts +5 -0
- package/dist/agent-graph/graph-query.js +359 -0
- package/dist/agent-graph/index.d.ts +6 -0
- package/dist/agent-graph/index.js +22 -0
- package/dist/agent-graph/types.d.ts +162 -0
- package/dist/agent-graph/types.js +3 -0
- package/dist/agent-situation/builder.d.ts +8 -0
- package/dist/agent-situation/builder.js +239 -0
- package/dist/agent-situation/cache-key.d.ts +4 -0
- package/dist/agent-situation/cache-key.js +64 -0
- package/dist/agent-situation/index.d.ts +7 -0
- package/dist/agent-situation/index.js +28 -0
- package/dist/agent-situation/packet-store.d.ts +27 -0
- package/dist/agent-situation/packet-store.js +228 -0
- package/dist/agent-situation/ranking-policy.d.ts +18 -0
- package/dist/agent-situation/ranking-policy.js +75 -0
- package/dist/agent-situation/source-readers.d.ts +52 -0
- package/dist/agent-situation/source-readers.js +300 -0
- package/dist/agent-situation/types.d.ts +195 -0
- package/dist/agent-situation/types.js +3 -0
- package/dist/canonicalize.d.ts +38 -0
- package/dist/canonicalize.js +106 -0
- package/dist/cases/case-links.d.ts +116 -0
- package/dist/cases/case-links.js +794 -0
- package/dist/cases/composition-overrides.d.ts +90 -0
- package/dist/cases/composition-overrides.js +724 -0
- package/dist/cases/corrections.d.ts +156 -0
- package/dist/cases/corrections.js +1045 -0
- package/dist/cases/freshness.d.ts +57 -0
- package/dist/cases/freshness.js +283 -0
- package/dist/cases/live-state.d.ts +48 -0
- package/dist/cases/live-state.js +256 -0
- package/dist/cases/membership-explain.d.ts +56 -0
- package/dist/cases/membership-explain.js +140 -0
- package/dist/cases/membership-matcher.d.ts +23 -0
- package/dist/cases/membership-matcher.js +193 -0
- package/dist/cases/merge-split.d.ts +61 -0
- package/dist/cases/merge-split.js +368 -0
- package/dist/cases/role-inference.d.ts +10 -0
- package/dist/cases/role-inference.js +85 -0
- package/dist/cases/search-rollup.d.ts +27 -0
- package/dist/cases/search-rollup.js +309 -0
- package/dist/cases/sqlite-transaction.d.ts +10 -0
- package/dist/cases/sqlite-transaction.js +62 -0
- package/dist/cases/store.d.ts +74 -0
- package/dist/cases/store.js +816 -0
- package/dist/cases/target-ref.d.ts +25 -0
- package/dist/cases/target-ref.js +22 -0
- package/dist/cases/timeline-range.d.ts +6 -0
- package/dist/cases/timeline-range.js +322 -0
- package/dist/cases/tombstone-sweeper.d.ts +13 -0
- package/dist/cases/tombstone-sweeper.js +128 -0
- package/dist/cases/types.d.ts +244 -0
- package/dist/cases/types.js +47 -0
- package/dist/cases/wiki-page-index.d.ts +50 -0
- package/dist/cases/wiki-page-index.js +350 -0
- package/dist/connectors/event-index.d.ts +45 -0
- package/dist/connectors/event-index.js +381 -0
- package/dist/connectors/raw-query.d.ts +18 -0
- package/dist/connectors/raw-query.js +305 -0
- package/dist/connectors/types.d.ts +109 -0
- package/dist/connectors/types.js +3 -0
- package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -0
- package/dist/db-adapter/node-sqlite-adapter.js +131 -0
- package/dist/db-manager.d.ts +12 -1
- package/dist/db-manager.js +60 -5
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/decision-formatter.js +11 -8
- package/dist/edges/ref-validation.d.ts +8 -0
- package/dist/edges/ref-validation.js +262 -0
- package/dist/edges/store.d.ts +10 -0
- package/dist/edges/store.js +343 -0
- package/dist/edges/types.d.ts +86 -0
- package/dist/edges/types.js +18 -0
- package/dist/embedding-server/mobile/daemon.d.ts +1 -0
- package/dist/embedding-server/mobile/daemon.js +11 -1
- package/dist/embeddings.js +13 -0
- package/dist/entities/audit-metrics.d.ts +73 -0
- package/dist/entities/audit-metrics.js +128 -0
- package/dist/entities/candidate-generator.d.ts +9 -0
- package/dist/entities/candidate-generator.js +436 -0
- package/dist/entities/entity-impact.d.ts +41 -0
- package/dist/entities/entity-impact.js +140 -0
- package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
- package/dist/entities/entity-linked-decision-counts.js +29 -0
- package/dist/entities/entity-list.d.ts +23 -0
- package/dist/entities/entity-list.js +99 -0
- package/dist/entities/entity-orphan-list.d.ts +26 -0
- package/dist/entities/entity-orphan-list.js +91 -0
- package/dist/entities/entity-search.d.ts +21 -0
- package/dist/entities/entity-search.js +150 -0
- package/dist/entities/errors.d.ts +65 -0
- package/dist/entities/errors.js +124 -0
- package/dist/entities/exact-merge-backfill.d.ts +11 -0
- package/dist/entities/exact-merge-backfill.js +161 -0
- package/dist/entities/lineage-backfill.d.ts +12 -0
- package/dist/entities/lineage-backfill.js +101 -0
- package/dist/entities/lineage-store.d.ts +51 -0
- package/dist/entities/lineage-store.js +308 -0
- package/dist/entities/normalization.d.ts +14 -0
- package/dist/entities/normalization.js +63 -0
- package/dist/entities/policy-store.d.ts +17 -0
- package/dist/entities/policy-store.js +263 -0
- package/dist/entities/policy-types.d.ts +72 -0
- package/dist/entities/policy-types.js +59 -0
- package/dist/entities/projection.d.ts +8 -0
- package/dist/entities/projection.js +82 -0
- package/dist/entities/provenance-query.d.ts +33 -0
- package/dist/entities/provenance-query.js +167 -0
- package/dist/entities/read-identity.d.ts +31 -0
- package/dist/entities/read-identity.js +264 -0
- package/dist/entities/recall-bridge.d.ts +5 -0
- package/dist/entities/recall-bridge.js +171 -0
- package/dist/entities/resolution-engine.d.ts +15 -0
- package/dist/entities/resolution-engine.js +69 -0
- package/dist/entities/rollback-preview.d.ts +39 -0
- package/dist/entities/rollback-preview.js +235 -0
- package/dist/entities/source-locator.d.ts +3 -0
- package/dist/entities/source-locator.js +32 -0
- package/dist/entities/store.d.ts +75 -0
- package/dist/entities/store.js +465 -0
- package/dist/entities/types.d.ts +162 -0
- package/dist/entities/types.js +30 -0
- package/dist/index.d.ts +62 -3
- package/dist/index.js +116 -26
- package/dist/mama-api.d.ts +53 -35
- package/dist/mama-api.js +437 -78
- package/dist/memory/api.d.ts +26 -38
- package/dist/memory/api.js +772 -68
- package/dist/memory/event-store.d.ts +3 -0
- package/dist/memory/event-store.js +21 -2
- package/dist/memory/provenance-audit.d.ts +22 -0
- package/dist/memory/provenance-audit.js +112 -0
- package/dist/memory/provenance-query.d.ts +11 -0
- package/dist/memory/provenance-query.js +193 -0
- package/dist/memory/provenance.d.ts +29 -0
- package/dist/memory/provenance.js +136 -0
- package/dist/memory/scope-backfill.d.ts +17 -0
- package/dist/memory/scope-backfill.js +77 -0
- package/dist/memory/types.d.ts +97 -16
- package/dist/model-runs/store.d.ts +13 -0
- package/dist/model-runs/store.js +457 -0
- package/dist/model-runs/tool-trace-store.d.ts +4 -0
- package/dist/model-runs/tool-trace-store.js +114 -0
- package/dist/model-runs/types.d.ts +69 -0
- package/dist/model-runs/types.js +5 -0
- package/dist/runtime/trusted-provenance.d.ts +3 -0
- package/dist/runtime/trusted-provenance.js +6 -0
- package/dist/search/feedback-store.d.ts +59 -0
- package/dist/search/feedback-store.js +211 -0
- package/dist/search/question-type.d.ts +5 -0
- package/dist/search/question-type.js +41 -0
- package/dist/search/ranker-features.d.ts +23 -0
- package/dist/search/ranker-features.js +104 -0
- package/dist/search/ranker-rescore.d.ts +31 -0
- package/dist/search/ranker-rescore.js +226 -0
- package/dist/search/ranker-trainer.d.ts +94 -0
- package/dist/search/ranker-trainer.js +860 -0
- package/dist/search/search-quality.d.ts +36 -0
- package/dist/search/search-quality.js +29 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +52 -0
- package/package.json +28 -3
|
@@ -0,0 +1,794 @@
|
|
|
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
|