linksee-memory 0.7.2 → 0.10.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/LICENSE +21 -21
- package/README.md +839 -678
- package/dist/bin/declare-anchor.d.ts +2 -0
- package/dist/bin/declare-anchor.js +146 -0
- package/dist/bin/detect-drift.d.ts +2 -0
- package/dist/bin/detect-drift.js +91 -0
- package/dist/bin/guard-hook.d.ts +2 -0
- package/dist/bin/guard-hook.js +96 -0
- package/dist/bin/import-sessions.js +44 -11
- package/dist/bin/install-skill.js +14 -14
- package/dist/bin/setup.js +107 -18
- package/dist/bin/stats.js +20 -20
- package/dist/db/migrate.js +52 -28
- package/dist/db/schema.sql +371 -232
- package/dist/lib/consolidate.js +19 -19
- package/dist/lib/drift-anchors.d.ts +78 -0
- package/dist/lib/drift-anchors.js +224 -0
- package/dist/lib/drift-detection.d.ts +62 -0
- package/dist/lib/drift-detection.js +416 -0
- package/dist/lib/drift-view.d.ts +62 -0
- package/dist/lib/drift-view.js +120 -0
- package/dist/lib/edge-detection.js +8 -8
- package/dist/lib/guard.d.ts +81 -0
- package/dist/lib/guard.js +321 -0
- package/dist/lib/lexical-match.d.ts +6 -0
- package/dist/lib/lexical-match.js +87 -0
- package/dist/lib/momentum.js +7 -7
- package/dist/lib/session-extractor.d.ts +1 -0
- package/dist/lib/session-extractor.js +62 -11
- package/dist/lib/truth-engine.d.ts +84 -0
- package/dist/lib/truth-engine.js +417 -0
- package/dist/mcp/read-smart.js +8 -8
- package/dist/mcp/server.js +573 -3
- package/dist/skill/SKILL.md +734 -631
- package/package.json +10 -5
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// Truth Engine — drift state derivation (engine-side, P0).
|
|
2
|
+
//
|
|
3
|
+
// Migrated from linksee-dashboard/lib/truth.ts into the MCP engine so that
|
|
4
|
+
// agents can query drift status via MCP tools. The dashboard can later import
|
|
5
|
+
// from here instead of duplicating the logic.
|
|
6
|
+
//
|
|
7
|
+
// Make-or-break rule: a divergence accounted for by a recorded resolution
|
|
8
|
+
// (supersede/fix/acknowledge) is NOT drift. Only unaccounted gaps are flagged.
|
|
9
|
+
//
|
|
10
|
+
// 4-species taxonomy (display-layer classification):
|
|
11
|
+
// hypothesis → Decision Cards (decision journal format)
|
|
12
|
+
// constraint → Rules (compact pass/fail checklist)
|
|
13
|
+
// commitment → Heartbeats (cadence monitoring, alive/dead)
|
|
14
|
+
// source_of_truth → Reference (quiet, rarely changes)
|
|
15
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
16
|
+
const DOMAIN_ORDER = [
|
|
17
|
+
'strategy', 'monetization', 'product', 'engineering',
|
|
18
|
+
'growth', 'operations', 'security', 'roadmap', 'memory', 'other',
|
|
19
|
+
];
|
|
20
|
+
const STATE_RANK = {
|
|
21
|
+
drift: 0, review: 1, held: 2, aligned: 3,
|
|
22
|
+
};
|
|
23
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
+
function safeJsonParse(s, fallback) {
|
|
25
|
+
if (s == null)
|
|
26
|
+
return fallback;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(String(s));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function formatDate(ts) {
|
|
35
|
+
if (ts == null)
|
|
36
|
+
return null;
|
|
37
|
+
return new Date(ts * 1000).toISOString().slice(0, 10);
|
|
38
|
+
}
|
|
39
|
+
function parseJsonArray(s) {
|
|
40
|
+
if (!s)
|
|
41
|
+
return [];
|
|
42
|
+
try {
|
|
43
|
+
const a = JSON.parse(s);
|
|
44
|
+
return Array.isArray(a) ? a.map(String) : [];
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Classify a node into one of 4 species based on decision_mode */
|
|
51
|
+
function classifySpecies(decision_mode) {
|
|
52
|
+
switch (decision_mode) {
|
|
53
|
+
case 'hypothesis': return 'hypothesis';
|
|
54
|
+
case 'constraint': return 'constraint';
|
|
55
|
+
case 'commitment': return 'commitment';
|
|
56
|
+
case 'source_of_truth': return 'source_of_truth';
|
|
57
|
+
// Fallback: constraints and prohibitions map to 'constraint',
|
|
58
|
+
// metrics to 'hypothesis', anything else to 'hypothesis' (safest default)
|
|
59
|
+
default: return 'hypothesis';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function buildResolutionLookup(db) {
|
|
63
|
+
const t3res = safeJsonParse(db.prepare("SELECT value FROM meta WHERE key='t3_resolutions'").get()?.value, {});
|
|
64
|
+
const t2res = safeJsonParse(db.prepare("SELECT value FROM meta WHERE key='t2_resolutions'").get()?.value, {});
|
|
65
|
+
// Collect ALL matching resolutions, then pick the most recent one.
|
|
66
|
+
// Without this, an older acknowledge can shadow a newer fix.
|
|
67
|
+
return function resolutionFor(id) {
|
|
68
|
+
const matches = [];
|
|
69
|
+
for (const r of Object.values(t3res)) {
|
|
70
|
+
if (r && (r.superseded_node === id || r.superseded_by === id ||
|
|
71
|
+
r.node === id || r.direction_node === id || r.constraint_node === id)) {
|
|
72
|
+
matches.push(r);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const r of Object.values(t2res)) {
|
|
76
|
+
if (r && (r.node === id || r.constraint_node === id)) {
|
|
77
|
+
matches.push(r);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (matches.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
if (matches.length === 1)
|
|
83
|
+
return { action: matches[0].action };
|
|
84
|
+
// Multiple matches → prefer most recent (by resolved_at, falling back to last found)
|
|
85
|
+
matches.sort((a, b) => {
|
|
86
|
+
const ta = a.resolved_at ? new Date(a.resolved_at).getTime() : 0;
|
|
87
|
+
const tb = b.resolved_at ? new Date(b.resolved_at).getTime() : 0;
|
|
88
|
+
return tb - ta; // newest first
|
|
89
|
+
});
|
|
90
|
+
return { action: matches[0].action };
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ── Core: getTruthView ───────────────────────────────────────────────────────
|
|
94
|
+
export function getTruthView(db, opts = {}) {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const resolutionFor = buildResolutionLookup(db);
|
|
97
|
+
// ── Candidates (indexed by target node) ──
|
|
98
|
+
const candRows = db
|
|
99
|
+
.prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status, proposed_node
|
|
100
|
+
FROM memory_write_candidates ORDER BY id DESC`)
|
|
101
|
+
.all();
|
|
102
|
+
const pendingByNode = new Map();
|
|
103
|
+
const cardByNode = new Map();
|
|
104
|
+
for (const c of candRows) {
|
|
105
|
+
if (c.target_node_id == null)
|
|
106
|
+
continue;
|
|
107
|
+
if (c.status === 'pending_review' && !pendingByNode.has(c.target_node_id)) {
|
|
108
|
+
pendingByNode.set(c.target_node_id, c);
|
|
109
|
+
}
|
|
110
|
+
const pn = String(c.proposed_node || '');
|
|
111
|
+
if ((pn.includes('"src":"t3"') || pn.includes('"src":"t2"')) && !cardByNode.has(c.target_node_id)) {
|
|
112
|
+
cardByNode.set(c.target_node_id, c);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ── Active nodes + state derivation ──
|
|
116
|
+
let sql = `SELECT id, node_type, domain, decision_mode, statement, rationale, confidence, lifecycle,
|
|
117
|
+
card_policy, review_after
|
|
118
|
+
FROM drift_anchors WHERE status = 'active'`;
|
|
119
|
+
const params = [];
|
|
120
|
+
if (opts.domain) {
|
|
121
|
+
sql += ' AND domain = ?';
|
|
122
|
+
params.push(opts.domain);
|
|
123
|
+
}
|
|
124
|
+
if (opts.decision_mode) {
|
|
125
|
+
sql += ' AND decision_mode = ?';
|
|
126
|
+
params.push(opts.decision_mode);
|
|
127
|
+
}
|
|
128
|
+
sql += ' ORDER BY domain, id';
|
|
129
|
+
const rows = db.prepare(sql).all(...params);
|
|
130
|
+
const nodes = rows.map((r) => {
|
|
131
|
+
let cadence = null;
|
|
132
|
+
try {
|
|
133
|
+
cadence = JSON.parse(r.card_policy || '{}').cadence_days ?? null;
|
|
134
|
+
}
|
|
135
|
+
catch { /* ignore */ }
|
|
136
|
+
const res = resolutionFor(r.id);
|
|
137
|
+
const pending = pendingByNode.get(r.id);
|
|
138
|
+
const card = cardByNode.get(r.id);
|
|
139
|
+
const overdue = r.review_after != null && r.review_after * 1000 < now;
|
|
140
|
+
// ── State derivation (the make-or-break logic) ──
|
|
141
|
+
let state;
|
|
142
|
+
let accounted;
|
|
143
|
+
let accountedBy;
|
|
144
|
+
if (res?.action === 'acknowledge_validate' || res?.action === 'acknowledge') {
|
|
145
|
+
// ⚪ held — but if time-box expired, escalate to 🔴
|
|
146
|
+
state = overdue ? 'drift' : 'held';
|
|
147
|
+
accounted = !overdue;
|
|
148
|
+
accountedBy = overdue
|
|
149
|
+
? 'acknowledge (expired → reopened)'
|
|
150
|
+
: 'acknowledge (held, time-boxed)';
|
|
151
|
+
}
|
|
152
|
+
else if (res?.action === 'fix' || res?.action === 'fix_implemented') {
|
|
153
|
+
state = 'aligned';
|
|
154
|
+
accounted = true;
|
|
155
|
+
accountedBy = 'fix (resolved)';
|
|
156
|
+
}
|
|
157
|
+
else if (res?.action === 'supersede') {
|
|
158
|
+
state = 'aligned';
|
|
159
|
+
accounted = true;
|
|
160
|
+
accountedBy = 'supersede (intentional evolution)';
|
|
161
|
+
}
|
|
162
|
+
else if (pending) {
|
|
163
|
+
// 🟡 Soft signal awaiting human decision
|
|
164
|
+
state = 'review';
|
|
165
|
+
accounted = false;
|
|
166
|
+
accountedBy = null;
|
|
167
|
+
}
|
|
168
|
+
else if (r.lifecycle === 'at_risk') {
|
|
169
|
+
// 🔴 declared-core but unproven / unaccounted
|
|
170
|
+
state = 'drift';
|
|
171
|
+
accounted = false;
|
|
172
|
+
accountedBy = null;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// 🔵 convergent — reality matches intent (or no signal)
|
|
176
|
+
state = 'aligned';
|
|
177
|
+
accounted = true;
|
|
178
|
+
accountedBy = null;
|
|
179
|
+
}
|
|
180
|
+
const reality = card?.rationale
|
|
181
|
+
?? pending?.rationale
|
|
182
|
+
?? (state === 'aligned' ? 'Committed reality matches intent (convergent)' : null);
|
|
183
|
+
return {
|
|
184
|
+
id: r.id,
|
|
185
|
+
node_type: r.node_type,
|
|
186
|
+
domain: r.domain,
|
|
187
|
+
decision_mode: r.decision_mode,
|
|
188
|
+
species: classifySpecies(r.decision_mode),
|
|
189
|
+
statement: r.statement,
|
|
190
|
+
rationale: r.rationale ?? null,
|
|
191
|
+
confidence: r.confidence,
|
|
192
|
+
lifecycle: r.lifecycle ?? 'active',
|
|
193
|
+
cadence_days: cadence,
|
|
194
|
+
review_after: r.review_after ?? null,
|
|
195
|
+
state,
|
|
196
|
+
accounted,
|
|
197
|
+
accountedBy,
|
|
198
|
+
reality,
|
|
199
|
+
reviewDate: formatDate(r.review_after),
|
|
200
|
+
overdue,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
// ── Partition: attention (loud) vs aligned (quiet) ──
|
|
204
|
+
const attention = nodes
|
|
205
|
+
.filter((n) => n.state !== 'aligned')
|
|
206
|
+
.sort((a, b) => STATE_RANK[a.state] - STATE_RANK[b.state] || b.confidence - a.confidence);
|
|
207
|
+
const alignedGroups = new Map();
|
|
208
|
+
for (const n of nodes.filter((n) => n.state === 'aligned')) {
|
|
209
|
+
const d = n.domain ?? 'other';
|
|
210
|
+
if (!alignedGroups.has(d))
|
|
211
|
+
alignedGroups.set(d, []);
|
|
212
|
+
alignedGroups.get(d).push(n);
|
|
213
|
+
}
|
|
214
|
+
const alignedByDomain = [...alignedGroups.entries()]
|
|
215
|
+
.sort((a, b) => DOMAIN_ORDER.indexOf(a[0]) - DOMAIN_ORDER.indexOf(b[0]))
|
|
216
|
+
.map(([domain, ns]) => ({ domain, nodes: ns }));
|
|
217
|
+
// ── Counts + next reopen ──
|
|
218
|
+
const by_mode = {};
|
|
219
|
+
for (const n of nodes) {
|
|
220
|
+
const m = n.decision_mode ?? 'unset';
|
|
221
|
+
by_mode[m] = (by_mode[m] || 0) + 1;
|
|
222
|
+
}
|
|
223
|
+
const by_species = {
|
|
224
|
+
hypothesis: 0, constraint: 0, commitment: 0, source_of_truth: 0,
|
|
225
|
+
};
|
|
226
|
+
for (const n of nodes)
|
|
227
|
+
by_species[n.species]++;
|
|
228
|
+
const by_state = { drift: 0, review: 0, held: 0, aligned: 0 };
|
|
229
|
+
for (const n of nodes)
|
|
230
|
+
by_state[n.state]++;
|
|
231
|
+
const reopenDates = nodes
|
|
232
|
+
.filter((n) => n.state === 'held' && n.reviewDate)
|
|
233
|
+
.map((n) => n.reviewDate)
|
|
234
|
+
.sort();
|
|
235
|
+
const nextReopen = reopenDates[0] ?? null;
|
|
236
|
+
// ── Candidate mapping ──
|
|
237
|
+
const stmtById = new Map(nodes.map((n) => [n.id, n.statement]));
|
|
238
|
+
const mapCand = (c) => ({
|
|
239
|
+
id: c.id,
|
|
240
|
+
candidate_type: c.candidate_type,
|
|
241
|
+
target_node_id: c.target_node_id,
|
|
242
|
+
target_statement: c.target_node_id != null ? stmtById.get(c.target_node_id) ?? null : null,
|
|
243
|
+
rationale: c.rationale,
|
|
244
|
+
confidence: c.confidence,
|
|
245
|
+
status: c.status,
|
|
246
|
+
});
|
|
247
|
+
const auto = candRows.filter((c) => c.status === 'auto_accepted').map(mapCand);
|
|
248
|
+
const suppressed = candRows.filter((c) => c.status === 'rejected').map(mapCand);
|
|
249
|
+
return {
|
|
250
|
+
attention,
|
|
251
|
+
alignedByDomain,
|
|
252
|
+
candidates: { auto, suppressed },
|
|
253
|
+
counts: {
|
|
254
|
+
nodes: nodes.length,
|
|
255
|
+
by_mode,
|
|
256
|
+
by_species,
|
|
257
|
+
by_state,
|
|
258
|
+
auto: auto.length,
|
|
259
|
+
suppressed: suppressed.length,
|
|
260
|
+
},
|
|
261
|
+
nextReopen,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// ── check_decision: single-node deep view ────────────────────────────────────
|
|
265
|
+
export function getDecisionDetail(db, anchorId) {
|
|
266
|
+
const row = db
|
|
267
|
+
.prepare(`SELECT id, kind, node_type, domain, decision_mode, statement, rationale, confidence,
|
|
268
|
+
lifecycle, card_policy, review_after, affects, detect_terms, violation_signal, tier
|
|
269
|
+
FROM drift_anchors WHERE id = ? AND status = 'active'`)
|
|
270
|
+
.get(anchorId);
|
|
271
|
+
if (!row)
|
|
272
|
+
return null;
|
|
273
|
+
// Get full truth view for this single node (reuse state derivation)
|
|
274
|
+
const resolutionFor = buildResolutionLookup(db);
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
let cadence = null;
|
|
277
|
+
try {
|
|
278
|
+
cadence = JSON.parse(row.card_policy || '{}').cadence_days ?? null;
|
|
279
|
+
}
|
|
280
|
+
catch { /* */ }
|
|
281
|
+
const res = resolutionFor(row.id);
|
|
282
|
+
const overdue = row.review_after != null && row.review_after * 1000 < now;
|
|
283
|
+
// State derivation (same logic)
|
|
284
|
+
let state, accounted, accountedBy;
|
|
285
|
+
const pendingCand = db
|
|
286
|
+
.prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status
|
|
287
|
+
FROM memory_write_candidates
|
|
288
|
+
WHERE target_node_id = ? AND status = 'pending_review'
|
|
289
|
+
ORDER BY id DESC`)
|
|
290
|
+
.all(anchorId);
|
|
291
|
+
const cardCand = db
|
|
292
|
+
.prepare(`SELECT rationale FROM memory_write_candidates
|
|
293
|
+
WHERE target_node_id = ? AND proposed_node LIKE '%"src":"t%'
|
|
294
|
+
ORDER BY id DESC LIMIT 1`)
|
|
295
|
+
.get(anchorId);
|
|
296
|
+
const hasPending = pendingCand.length > 0;
|
|
297
|
+
if (res?.action === 'acknowledge_validate' || res?.action === 'acknowledge') {
|
|
298
|
+
state = overdue ? 'drift' : 'held';
|
|
299
|
+
accounted = !overdue;
|
|
300
|
+
accountedBy = overdue ? 'acknowledge (expired → reopened)' : 'acknowledge (held, time-boxed)';
|
|
301
|
+
}
|
|
302
|
+
else if (res?.action === 'fix' || res?.action === 'fix_implemented') {
|
|
303
|
+
state = 'aligned';
|
|
304
|
+
accounted = true;
|
|
305
|
+
accountedBy = 'fix (resolved)';
|
|
306
|
+
}
|
|
307
|
+
else if (res?.action === 'supersede') {
|
|
308
|
+
state = 'aligned';
|
|
309
|
+
accounted = true;
|
|
310
|
+
accountedBy = 'supersede (intentional evolution)';
|
|
311
|
+
}
|
|
312
|
+
else if (hasPending) {
|
|
313
|
+
state = 'review';
|
|
314
|
+
accounted = false;
|
|
315
|
+
accountedBy = null;
|
|
316
|
+
}
|
|
317
|
+
else if (row.lifecycle === 'at_risk') {
|
|
318
|
+
state = 'drift';
|
|
319
|
+
accounted = false;
|
|
320
|
+
accountedBy = null;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
state = 'aligned';
|
|
324
|
+
accounted = true;
|
|
325
|
+
accountedBy = null;
|
|
326
|
+
}
|
|
327
|
+
const reality = cardCand?.rationale ?? (hasPending ? pendingCand[0].rationale : null)
|
|
328
|
+
?? (state === 'aligned' ? 'Committed reality matches intent (convergent)' : null);
|
|
329
|
+
// Drift edges for this anchor
|
|
330
|
+
const edges = db
|
|
331
|
+
.prepare(`SELECT id AS edge_id, verdict, confidence, status, detected_at
|
|
332
|
+
FROM drift_edges WHERE anchor_id = ? ORDER BY detected_at DESC`)
|
|
333
|
+
.all(anchorId);
|
|
334
|
+
return {
|
|
335
|
+
id: row.id,
|
|
336
|
+
kind: row.kind,
|
|
337
|
+
node_type: row.node_type,
|
|
338
|
+
domain: row.domain,
|
|
339
|
+
decision_mode: row.decision_mode,
|
|
340
|
+
species: classifySpecies(row.decision_mode),
|
|
341
|
+
statement: row.statement,
|
|
342
|
+
rationale: row.rationale ?? null,
|
|
343
|
+
confidence: row.confidence,
|
|
344
|
+
lifecycle: row.lifecycle ?? 'active',
|
|
345
|
+
cadence_days: cadence,
|
|
346
|
+
review_after: row.review_after ?? null,
|
|
347
|
+
state,
|
|
348
|
+
accounted,
|
|
349
|
+
accountedBy,
|
|
350
|
+
reality,
|
|
351
|
+
reviewDate: formatDate(row.review_after),
|
|
352
|
+
overdue,
|
|
353
|
+
// Deep fields
|
|
354
|
+
affects: parseJsonArray(row.affects),
|
|
355
|
+
detect_terms: parseJsonArray(row.detect_terms),
|
|
356
|
+
violation_signal: parseJsonArray(row.violation_signal),
|
|
357
|
+
tier: row.tier,
|
|
358
|
+
pendingCandidates: pendingCand.map((c) => ({
|
|
359
|
+
id: c.id,
|
|
360
|
+
candidate_type: c.candidate_type,
|
|
361
|
+
target_node_id: c.target_node_id,
|
|
362
|
+
target_statement: row.statement,
|
|
363
|
+
rationale: c.rationale,
|
|
364
|
+
confidence: c.confidence,
|
|
365
|
+
status: c.status,
|
|
366
|
+
})),
|
|
367
|
+
driftEdges: edges,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
export function resolveDrift(db, input) {
|
|
371
|
+
const ACTIONS = new Set(['fix', 'supersede', 'acknowledge', 'dismiss']);
|
|
372
|
+
if (!ACTIONS.has(input.action)) {
|
|
373
|
+
throw new Error(`invalid action "${input.action}" — one of: fix, supersede, acknowledge, dismiss`);
|
|
374
|
+
}
|
|
375
|
+
// Verify anchor exists and is active
|
|
376
|
+
const anchor = db
|
|
377
|
+
.prepare("SELECT id, statement FROM drift_anchors WHERE id = ? AND status = 'active'")
|
|
378
|
+
.get(input.anchor_id);
|
|
379
|
+
if (!anchor) {
|
|
380
|
+
throw new Error(`anchor ${input.anchor_id} not found or not active`);
|
|
381
|
+
}
|
|
382
|
+
// Build resolution record
|
|
383
|
+
const resolution = {
|
|
384
|
+
action: input.action,
|
|
385
|
+
node: input.anchor_id,
|
|
386
|
+
rationale: input.rationale ?? null,
|
|
387
|
+
resolved_at: new Date().toISOString(),
|
|
388
|
+
};
|
|
389
|
+
if (input.action === 'acknowledge' && input.review_after) {
|
|
390
|
+
resolution.action = 'acknowledge_validate';
|
|
391
|
+
const ts = Math.floor(new Date(input.review_after).getTime() / 1000);
|
|
392
|
+
resolution.review_after = ts;
|
|
393
|
+
// Also set review_after on the anchor itself
|
|
394
|
+
db.prepare('UPDATE drift_anchors SET review_after = ?, updated_at = unixepoch() WHERE id = ?')
|
|
395
|
+
.run(ts, input.anchor_id);
|
|
396
|
+
}
|
|
397
|
+
if (input.action === 'supersede' && input.superseded_by != null) {
|
|
398
|
+
resolution.superseded_node = input.anchor_id;
|
|
399
|
+
resolution.superseded_by = input.superseded_by;
|
|
400
|
+
}
|
|
401
|
+
// Write to meta (t3_resolutions) — additive, keyed by anchor id
|
|
402
|
+
const key = 't3_resolutions';
|
|
403
|
+
const existing = safeJsonParse(db.prepare("SELECT value FROM meta WHERE key = ?").get(key)?.value, {});
|
|
404
|
+
existing[`A${input.anchor_id}`] = resolution;
|
|
405
|
+
db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?")
|
|
406
|
+
.run(key, JSON.stringify(existing), JSON.stringify(existing));
|
|
407
|
+
// If action is 'dismiss', also mark all open drift_edges for this anchor as dismissed
|
|
408
|
+
if (input.action === 'dismiss') {
|
|
409
|
+
db.prepare("UPDATE drift_edges SET status = 'dismissed' WHERE anchor_id = ? AND status = 'open'").run(input.anchor_id);
|
|
410
|
+
}
|
|
411
|
+
// If action is 'fix', mark open edges as resolved
|
|
412
|
+
if (input.action === 'fix') {
|
|
413
|
+
db.prepare("UPDATE drift_edges SET status = 'resolved' WHERE anchor_id = ? AND status = 'open'").run(input.anchor_id);
|
|
414
|
+
}
|
|
415
|
+
return { ok: true, resolution };
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=truth-engine.js.map
|
package/dist/mcp/read-smart.js
CHANGED
|
@@ -30,14 +30,14 @@ export function handleReadSmart(db, args) {
|
|
|
30
30
|
const fileHash = hashFile(content);
|
|
31
31
|
const chunks = chunkFile(path, content);
|
|
32
32
|
const chunkMeta = chunks.map(toMeta);
|
|
33
|
-
db.prepare(`INSERT INTO file_snapshots (path, content_hash, mtime, size_bytes, chunks, last_read_at, read_count)
|
|
34
|
-
VALUES (?, ?, ?, ?, ?, unixepoch(), 1)
|
|
35
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
36
|
-
content_hash = excluded.content_hash,
|
|
37
|
-
mtime = excluded.mtime,
|
|
38
|
-
size_bytes = excluded.size_bytes,
|
|
39
|
-
chunks = excluded.chunks,
|
|
40
|
-
last_read_at = unixepoch(),
|
|
33
|
+
db.prepare(`INSERT INTO file_snapshots (path, content_hash, mtime, size_bytes, chunks, last_read_at, read_count)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, unixepoch(), 1)
|
|
35
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
36
|
+
content_hash = excluded.content_hash,
|
|
37
|
+
mtime = excluded.mtime,
|
|
38
|
+
size_bytes = excluded.size_bytes,
|
|
39
|
+
chunks = excluded.chunks,
|
|
40
|
+
last_read_at = unixepoch(),
|
|
41
41
|
read_count = read_count + 1`).run(path, fileHash, mtime, size, JSON.stringify(chunkMeta));
|
|
42
42
|
return JSON.stringify({
|
|
43
43
|
ok: true,
|