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