linksee-memory 0.7.1 → 0.8.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 +193 -34
- 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/db/migrate.js +52 -28
- package/dist/db/schema.sql +348 -232
- package/dist/lib/consolidate.d.ts +2 -0
- package/dist/lib/consolidate.js +8 -0
- 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.d.ts +22 -0
- package/dist/lib/edge-detection.js +178 -0
- package/dist/lib/session-extractor.js +49 -0
- package/dist/lib/truth-engine.d.ts +84 -0
- package/dist/lib/truth-engine.js +417 -0
- package/dist/mcp/server.js +292 -57
- package/package.json +5 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Memory→memory edge detection (precision-first, no LLM required).
|
|
2
|
+
//
|
|
3
|
+
// Populates the `memory_edges` table with supersedes/contradicts links between
|
|
4
|
+
// DECISION memories, so the dashboard can render Pivot Chains and recall can
|
|
5
|
+
// surface superseded decisions. Runs inside the sleep-mode consolidation sweep.
|
|
6
|
+
// Idempotent (UNIQUE constraint on memory_edges).
|
|
7
|
+
//
|
|
8
|
+
// Heuristic: within one entity, a later decision that shares strong topical terms
|
|
9
|
+
// with an earlier decision *supersedes* it (→ *contradicts* if it carries reversal
|
|
10
|
+
// markers like やめる / 撤回 / revert / instead of). The earlier decision's state
|
|
11
|
+
// is flipped to 'superseded'. We link to the MOST RECENT same-topic decision so
|
|
12
|
+
// chains form (A → B → C) rather than cliques.
|
|
13
|
+
import { isMetaOrNoise, isPastedExternalContent } from './session-parser.js';
|
|
14
|
+
// A later decision either REVERSES an earlier one (→ contradicts) or REPLACES it
|
|
15
|
+
// (→ supersedes). A same-topic decision with NEITHER marker only EXTENDS the earlier
|
|
16
|
+
// one and must NOT deactivate it — marking a still-valid decision 'superseded' is
|
|
17
|
+
// silent data loss (e.g. "v2 format" extends, it does not kill, the "2-layer arch" decision).
|
|
18
|
+
const CONTRADICT_MARKERS = /やめ|撤回|取り消|ではなく|じゃなく|見直|revert|rollback|instead of|no longer|abandon|\bdon'?t\b/i;
|
|
19
|
+
const SUPERSEDE_MARKERS = /の代わり|に変更|に切り替|乗り換|を置き換|に置換|廃止|replaces?\b|supersed|deprecat|switch(?:ing|ed)?\s+(?:to|from|away)/i;
|
|
20
|
+
const STOPWORDS = new Set([
|
|
21
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'your', 'our', 'their',
|
|
22
|
+
'about', 'what', 'when', 'will', 'have', 'has', 'was', 'were', 'are', 'not', 'use', 'using',
|
|
23
|
+
'memory', 'linksee', 'session', 'decision', 'project', 'やった', 'する', 'した', 'して',
|
|
24
|
+
'こと', 'ため', 'よう', 'という', 'です', 'ます', 'など', 'その', 'この', 'これ', 'それ',
|
|
25
|
+
// conversational fillers — must never count as a shared "topic" term
|
|
26
|
+
'そうだね', 'そうね', 'ありがとう', 'なるほど', 'やろう', 'いいね', 'おおいね', 'これでいい',
|
|
27
|
+
'これでいいと思う', 'これでいいかな', 'わかった', '了解', 'おはよう', 'おつかれ',
|
|
28
|
+
]);
|
|
29
|
+
// Both endpoints must have substantive core text — kills "そうだね"/"ありがとう"
|
|
30
|
+
// turns that the upstream classifier over-labels as decisions.
|
|
31
|
+
const MIN_CORE_LEN = 40;
|
|
32
|
+
// Decisions whose body opens with an acknowledgement are almost always chitchat
|
|
33
|
+
// the upstream classifier mis-typed — never use them as edge endpoints.
|
|
34
|
+
const CHITCHAT_OPENER = /^\s*(?:そう(?:だ?ね|だよね)?|うん|ありがと|なるほど|おお|へえ|了解|わかった|はい|おはよう|おつかれ|いいね|まあ|ええ|あー)/;
|
|
35
|
+
// Terminal output, git/npm logs, and pasted emails also get mis-typed as decisions.
|
|
36
|
+
// isPastedExternalContent misses these shapes, so guard them explicitly.
|
|
37
|
+
const LOOKS_LIKE_PASTE = /PS [A-Za-z]:\\|[A-Za-z]:\\Users\\|create mode \d{6}|\bgit (?:commit|push|add|status|log|diff|branch|checkout|merge)\b|commit -m|\bnpm (?:run|install|ci)\b|Upon further review|resubmission|Thank you for your/i;
|
|
38
|
+
function significantTerms(text) {
|
|
39
|
+
const out = new Set();
|
|
40
|
+
const lower = text.toLowerCase();
|
|
41
|
+
const asciiRe = /[a-z][a-z0-9_+.-]{2,}/g;
|
|
42
|
+
let m;
|
|
43
|
+
while ((m = asciiRe.exec(lower)) !== null) {
|
|
44
|
+
const w = m[0];
|
|
45
|
+
if (w.length >= 3 && !STOPWORDS.has(w))
|
|
46
|
+
out.add(w);
|
|
47
|
+
}
|
|
48
|
+
// CJK runs (hiragana / katakana / kanji / half-width kana), length 2..12.
|
|
49
|
+
const cjkRe = /[-ヿ㐀-鿿ヲ-゚]{2,12}/g;
|
|
50
|
+
while ((m = cjkRe.exec(text)) !== null) {
|
|
51
|
+
const w = m[0];
|
|
52
|
+
if (!STOPWORDS.has(w))
|
|
53
|
+
out.add(w);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function coreText(content) {
|
|
58
|
+
try {
|
|
59
|
+
const o = JSON.parse(content);
|
|
60
|
+
return {
|
|
61
|
+
title: String(o.title ?? ''),
|
|
62
|
+
body: String(o.what ?? o.decision ?? o.intent ?? o.learned ?? ''),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { title: '', body: content };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function detectMemoryEdges(db, opts = {}) {
|
|
70
|
+
const lookback = opts.lookback ?? 25;
|
|
71
|
+
const res = {
|
|
72
|
+
decisionsScanned: 0, edgesCreated: 0, supersedes: 0, contradicts: 0, extends: 0, supersededMarked: 0, samples: [],
|
|
73
|
+
};
|
|
74
|
+
const rows = db.prepare(`
|
|
75
|
+
SELECT id, entity_id, content, created_at
|
|
76
|
+
FROM memories
|
|
77
|
+
WHERE mem_type = 'decision' AND json_valid(content)
|
|
78
|
+
ORDER BY entity_id ASC, created_at ASC, id ASC
|
|
79
|
+
`).all();
|
|
80
|
+
res.decisionsScanned = rows.length;
|
|
81
|
+
if (rows.length < 2)
|
|
82
|
+
return res;
|
|
83
|
+
const byEntity = new Map();
|
|
84
|
+
for (const r of rows) {
|
|
85
|
+
const arr = byEntity.get(r.entity_id);
|
|
86
|
+
if (arr)
|
|
87
|
+
arr.push(r);
|
|
88
|
+
else
|
|
89
|
+
byEntity.set(r.entity_id, [r]);
|
|
90
|
+
}
|
|
91
|
+
// Prepare write statements only when actually writing — keeps dryRun safe on a
|
|
92
|
+
// readonly connection (preview / verification path).
|
|
93
|
+
const insEdge = opts.dryRun ? null : db.prepare(`INSERT OR IGNORE INTO memory_edges (from_memory_id, to_memory_id, relation) VALUES (?, ?, ?)`);
|
|
94
|
+
const markSuperseded = opts.dryRun ? null : db.prepare(`
|
|
95
|
+
UPDATE memories SET content = json_set(content, '$.state', 'superseded')
|
|
96
|
+
WHERE id = ? AND json_valid(content) AND json_extract(content, '$.state') <> 'superseded'
|
|
97
|
+
`);
|
|
98
|
+
const apply = () => {
|
|
99
|
+
for (const decisions of byEntity.values()) {
|
|
100
|
+
if (decisions.length < 2)
|
|
101
|
+
continue;
|
|
102
|
+
const meta = decisions.map((d) => {
|
|
103
|
+
const { title, body } = coreText(d.content);
|
|
104
|
+
const text = `${title} ${body}`;
|
|
105
|
+
// Defend against the polluted 'decision' input set: drop pasted external
|
|
106
|
+
// content (emails / terminal logs), meta-noise, chitchat, and too-short bodies.
|
|
107
|
+
const usable = text.trim().length >= MIN_CORE_LEN
|
|
108
|
+
&& !CHITCHAT_OPENER.test(body)
|
|
109
|
+
&& !isMetaOrNoise(body)
|
|
110
|
+
&& !isPastedExternalContent(body)
|
|
111
|
+
&& !LOOKS_LIKE_PASTE.test(text);
|
|
112
|
+
return { id: d.id, title: (title || body).slice(0, 60), text, len: text.trim().length, terms: significantTerms(text), usable };
|
|
113
|
+
}).filter((m) => m.usable && m.terms.size >= 3);
|
|
114
|
+
for (let i = 1; i < meta.length; i++) {
|
|
115
|
+
if (meta[i].terms.size < 3 || meta[i].len < MIN_CORE_LEN)
|
|
116
|
+
continue;
|
|
117
|
+
let linked = -1;
|
|
118
|
+
let linkedShared = [];
|
|
119
|
+
for (let j = i - 1; j >= 0 && i - j <= lookback; j--) {
|
|
120
|
+
if (meta[j].terms.size < 3 || meta[j].len < MIN_CORE_LEN)
|
|
121
|
+
continue;
|
|
122
|
+
const shared = [];
|
|
123
|
+
for (const t of meta[i].terms)
|
|
124
|
+
if (meta[j].terms.has(t))
|
|
125
|
+
shared.push(t);
|
|
126
|
+
if (shared.length < 3)
|
|
127
|
+
continue; // require >= 3 shared topic terms (precision-first)
|
|
128
|
+
const overlap = shared.length / Math.min(meta[i].terms.size, meta[j].terms.size);
|
|
129
|
+
if (overlap > 0.85)
|
|
130
|
+
continue; // near-identical memories = a duplicate, not a supersession
|
|
131
|
+
if (overlap >= 0.25) {
|
|
132
|
+
linked = j;
|
|
133
|
+
linkedShared = shared;
|
|
134
|
+
break;
|
|
135
|
+
} // most-recent same-topic → chain
|
|
136
|
+
}
|
|
137
|
+
if (linked < 0)
|
|
138
|
+
continue;
|
|
139
|
+
const reversing = CONTRADICT_MARKERS.test(meta[i].text);
|
|
140
|
+
const replacing = SUPERSEDE_MARKERS.test(meta[i].text);
|
|
141
|
+
const relation = reversing ? 'contradicts' : replacing ? 'supersedes' : 'extends';
|
|
142
|
+
const deactivatesOlder = reversing || replacing; // 'extends' leaves the older decision valid
|
|
143
|
+
let counted = true;
|
|
144
|
+
if (!opts.dryRun) {
|
|
145
|
+
const r = insEdge.run(meta[i].id, meta[linked].id, relation);
|
|
146
|
+
if (r.changes > 0) {
|
|
147
|
+
if (deactivatesOlder && markSuperseded.run(meta[linked].id).changes > 0)
|
|
148
|
+
res.supersededMarked++;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
counted = false; // edge already existed
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (counted) {
|
|
155
|
+
res.edgesCreated++;
|
|
156
|
+
if (relation === 'contradicts')
|
|
157
|
+
res.contradicts++;
|
|
158
|
+
else if (relation === 'supersedes')
|
|
159
|
+
res.supersedes++;
|
|
160
|
+
else
|
|
161
|
+
res.extends++;
|
|
162
|
+
if (res.samples.length < 25) {
|
|
163
|
+
res.samples.push({
|
|
164
|
+
from_id: meta[i].id, to_id: meta[linked].id, relation,
|
|
165
|
+
from_title: meta[i].title, to_title: meta[linked].title, shared: linkedShared.slice(0, 6),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
if (opts.dryRun)
|
|
173
|
+
apply();
|
|
174
|
+
else
|
|
175
|
+
db.transaction(apply)();
|
|
176
|
+
return res;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=edge-detection.js.map
|
|
@@ -25,7 +25,35 @@ const TYPE_PATTERNS = [
|
|
|
25
25
|
[/学び|教訓|分かった|判明|発見|learn|realize|discover|find\s+out|turns?\s+out|takeaway|insight/i, 'learning'],
|
|
26
26
|
[/結果|完了|成功|失敗|outcome|result|shipped|deployed|launched|finished|accomplished/i, 'outcome'],
|
|
27
27
|
];
|
|
28
|
+
// Pasted terminal/git/npm output and forwarded emails get mis-typed as decisions.
|
|
29
|
+
// isPastedExternalContent misses these shapes, so guard them explicitly.
|
|
30
|
+
const LOOKS_LIKE_PASTE = /PS [A-Za-z]:\\|[A-Za-z]:\\Users\\|create mode \d{6}|\bgit (?:commit|push|add|status|log|diff|branch|checkout|merge)\b|commit -m|\bnpm (?:run|install|ci)\b|Upon further review|resubmission|Thank you for your/i;
|
|
31
|
+
// A turn that is ENTIRELY an acknowledgement carries no cognitive content. This only
|
|
32
|
+
// fires on short, wholly-filler messages — anything with substance after the opener
|
|
33
|
+
// (e.g. "そうだね、Reactを採用しよう") still flows to the patterns below.
|
|
34
|
+
const PURE_ACK = /^(?:そう(?:だ?ね|だよね)?|うん+|ありがと[うー]?|なるほど|了解(?:です)?|わかった|おはよう|おつかれ(?:さま)?|はい+|おお+|へえ+|いいね|まあ|ええ|あー|ok|okay|thanks?|got it|sounds good)[。、,.!!??\s~〜ねよ]*$/i;
|
|
35
|
+
// Whether a memory's text carries enough signal to assign a meaningful cognitive type.
|
|
36
|
+
// Junk (pasted logs/emails, meta-noise, pure acknowledgements) must NOT become a
|
|
37
|
+
// 'decision'/'comparison' — that pollution caps memory_edges + dashboard quality.
|
|
38
|
+
function isClassifiableContent(text) {
|
|
39
|
+
const t = text.trim();
|
|
40
|
+
if (t.length === 0)
|
|
41
|
+
return false;
|
|
42
|
+
if (PURE_ACK.test(t))
|
|
43
|
+
return false;
|
|
44
|
+
if (LOOKS_LIKE_PASTE.test(text))
|
|
45
|
+
return false;
|
|
46
|
+
if (isPastedExternalContent(text))
|
|
47
|
+
return false;
|
|
48
|
+
if (isMetaOrNoise(text))
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
28
52
|
export function inferType(text, layer) {
|
|
53
|
+
// Precision guard: junk content has no meaningful type — fall back to 'note'
|
|
54
|
+
// BEFORE pattern matching / layer defaults so it can never become a 'decision'.
|
|
55
|
+
if (!isClassifiableContent(text))
|
|
56
|
+
return 'note';
|
|
29
57
|
for (const [pattern, type] of TYPE_PATTERNS) {
|
|
30
58
|
if (pattern.test(text))
|
|
31
59
|
return type;
|
|
@@ -54,6 +82,9 @@ const STATE_PATTERNS = [
|
|
|
54
82
|
[/取り替え|代わりに|置き換え|replaced|superseded|deprecated|obsolete|旧版|old\s+approach/i, 'superseded'],
|
|
55
83
|
];
|
|
56
84
|
export function inferState(text, layer) {
|
|
85
|
+
// Junk content gets a neutral 'open' state rather than a confident 'decided'/'done'.
|
|
86
|
+
if (!isClassifiableContent(text))
|
|
87
|
+
return 'open';
|
|
57
88
|
for (const [pattern, state] of STATE_PATTERNS) {
|
|
58
89
|
if (pattern.test(text))
|
|
59
90
|
return state;
|
|
@@ -205,6 +236,10 @@ function dedupeEdits(edits) {
|
|
|
205
236
|
export function extractSession(session, projectName) {
|
|
206
237
|
const memories = [];
|
|
207
238
|
const file_edits = [];
|
|
239
|
+
// Turn-level dedup: a single user turn should yield at most ONE memory.
|
|
240
|
+
// Priority: goal(first_intent) > caveat > decision > context. capturedTurns
|
|
241
|
+
// tracks caveat-claimed turns so the decision pass skips them.
|
|
242
|
+
const capturedTurns = new Set();
|
|
208
243
|
// Detect fully-automated sessions (e.g. scheduled cron tasks) — no user intent to extract
|
|
209
244
|
const firstRawUserText = session.turns.find((t) => t.role === 'user' && !t.tool_results)?.text ?? '';
|
|
210
245
|
const automated = isAutomatedSession(firstRawUserText);
|
|
@@ -265,6 +300,12 @@ export function extractSession(session, projectName) {
|
|
|
265
300
|
continue;
|
|
266
301
|
if (t.text.trim().length < 40)
|
|
267
302
|
continue;
|
|
303
|
+
// Defer to the caveat/decision passes when they WOULD capture this turn,
|
|
304
|
+
// so a clarification that is really a warning/decision isn't double-saved as context.
|
|
305
|
+
const wouldBeCaveat = matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20 && !isChitchatWithBuriedDecision(t.text, CAVEAT_PATTERNS);
|
|
306
|
+
const wouldBeDecision = matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15 && !isChitchatWithBuriedDecision(t.text, DECISION_PATTERNS);
|
|
307
|
+
if (wouldBeCaveat || wouldBeDecision)
|
|
308
|
+
continue;
|
|
268
309
|
clarifyCount++;
|
|
269
310
|
const msgText = t.text.slice(0, 600);
|
|
270
311
|
memories.push({
|
|
@@ -340,6 +381,8 @@ export function extractSession(session, projectName) {
|
|
|
340
381
|
for (const t of session.turns) {
|
|
341
382
|
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
342
383
|
continue;
|
|
384
|
+
if (t === firstIntent)
|
|
385
|
+
continue; // already captured as the goal/first-intent memory
|
|
343
386
|
if (t.tool_results && t.tool_results.length > 0)
|
|
344
387
|
continue;
|
|
345
388
|
if (isPastedExternalContent(t.text))
|
|
@@ -364,6 +407,8 @@ export function extractSession(session, projectName) {
|
|
|
364
407
|
thread_id: session.session_id,
|
|
365
408
|
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'caveat' },
|
|
366
409
|
});
|
|
410
|
+
if (t.uuid)
|
|
411
|
+
capturedTurns.add(t.uuid); // claim this turn so the decision pass skips it
|
|
367
412
|
}
|
|
368
413
|
}
|
|
369
414
|
// 5) Learning layer — messages matching decision patterns
|
|
@@ -374,6 +419,10 @@ export function extractSession(session, projectName) {
|
|
|
374
419
|
for (const t of session.turns) {
|
|
375
420
|
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
376
421
|
continue;
|
|
422
|
+
if (t === firstIntent)
|
|
423
|
+
continue; // already captured as the goal/first-intent memory
|
|
424
|
+
if (t.uuid && capturedTurns.has(t.uuid))
|
|
425
|
+
continue; // already captured as a caveat
|
|
377
426
|
if (t.tool_results && t.tool_results.length > 0)
|
|
378
427
|
continue;
|
|
379
428
|
if (isPastedExternalContent(t.text))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export type DriftState = 'drift' | 'review' | 'held' | 'aligned';
|
|
3
|
+
export type Species = 'hypothesis' | 'constraint' | 'commitment' | 'source_of_truth';
|
|
4
|
+
export interface TruthNode {
|
|
5
|
+
id: number;
|
|
6
|
+
node_type: string | null;
|
|
7
|
+
domain: string | null;
|
|
8
|
+
decision_mode: string | null;
|
|
9
|
+
species: Species;
|
|
10
|
+
statement: string;
|
|
11
|
+
rationale: string | null;
|
|
12
|
+
confidence: number;
|
|
13
|
+
lifecycle: string;
|
|
14
|
+
cadence_days: number | null;
|
|
15
|
+
review_after: number | null;
|
|
16
|
+
state: DriftState;
|
|
17
|
+
accounted: boolean;
|
|
18
|
+
accountedBy: string | null;
|
|
19
|
+
reality: string | null;
|
|
20
|
+
reviewDate: string | null;
|
|
21
|
+
overdue: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface TruthCandidate {
|
|
24
|
+
id: number;
|
|
25
|
+
candidate_type: string;
|
|
26
|
+
target_node_id: number | null;
|
|
27
|
+
target_statement: string | null;
|
|
28
|
+
rationale: string;
|
|
29
|
+
confidence: number;
|
|
30
|
+
status: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TruthCounts {
|
|
33
|
+
nodes: number;
|
|
34
|
+
by_mode: Record<string, number>;
|
|
35
|
+
by_species: Record<Species, number>;
|
|
36
|
+
by_state: Record<DriftState, number>;
|
|
37
|
+
auto: number;
|
|
38
|
+
suppressed: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TruthView {
|
|
41
|
+
attention: TruthNode[];
|
|
42
|
+
alignedByDomain: Array<{
|
|
43
|
+
domain: string;
|
|
44
|
+
nodes: TruthNode[];
|
|
45
|
+
}>;
|
|
46
|
+
candidates: {
|
|
47
|
+
auto: TruthCandidate[];
|
|
48
|
+
suppressed: TruthCandidate[];
|
|
49
|
+
};
|
|
50
|
+
counts: TruthCounts;
|
|
51
|
+
nextReopen: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface DecisionDetail extends TruthNode {
|
|
54
|
+
kind: string | null;
|
|
55
|
+
affects: string[];
|
|
56
|
+
detect_terms: string[];
|
|
57
|
+
violation_signal: string[];
|
|
58
|
+
tier: string;
|
|
59
|
+
pendingCandidates: TruthCandidate[];
|
|
60
|
+
driftEdges: Array<{
|
|
61
|
+
edge_id: number;
|
|
62
|
+
verdict: string;
|
|
63
|
+
confidence: number;
|
|
64
|
+
status: string;
|
|
65
|
+
detected_at: number;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
export declare function getTruthView(db: Database.Database, opts?: {
|
|
69
|
+
domain?: string;
|
|
70
|
+
decision_mode?: string;
|
|
71
|
+
}): TruthView;
|
|
72
|
+
export declare function getDecisionDetail(db: Database.Database, anchorId: number): DecisionDetail | null;
|
|
73
|
+
export type ResolutionAction = 'fix' | 'supersede' | 'acknowledge' | 'dismiss';
|
|
74
|
+
export interface ResolveInput {
|
|
75
|
+
anchor_id: number;
|
|
76
|
+
action: ResolutionAction;
|
|
77
|
+
rationale?: string;
|
|
78
|
+
review_after?: string;
|
|
79
|
+
superseded_by?: number;
|
|
80
|
+
}
|
|
81
|
+
export declare function resolveDrift(db: Database.Database, input: ResolveInput): {
|
|
82
|
+
ok: boolean;
|
|
83
|
+
resolution: any;
|
|
84
|
+
};
|