dsh-daoing-memory 0.1.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 -0
- package/README.md +91 -0
- package/README.zh-CN.md +91 -0
- package/cordis.patch.yml +22 -0
- package/lib/client.js +10339 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3138 -0
- package/lib/invariant.js +33 -0
- package/lib/tools.js +1472 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +4058 -0
- package/lib/typert.remote-client.d.ts +149 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +3167 -0
- package/lib/types/client/ExperiencePage.d.ts +15 -0
- package/lib/types/client/ExperiencePage.d.ts.map +1 -0
- package/lib/types/client/ExperiencePage.js +117 -0
- package/lib/types/client/ExperiencePage.js.map +1 -0
- package/lib/types/client/FactDiaryPage.d.ts +17 -0
- package/lib/types/client/FactDiaryPage.d.ts.map +1 -0
- package/lib/types/client/FactDiaryPage.js +155 -0
- package/lib/types/client/FactDiaryPage.js.map +1 -0
- package/lib/types/client/HumanOpsPage.d.ts +16 -0
- package/lib/types/client/HumanOpsPage.d.ts.map +1 -0
- package/lib/types/client/HumanOpsPage.js +208 -0
- package/lib/types/client/HumanOpsPage.js.map +1 -0
- package/lib/types/client/LedgerPage.d.ts +13 -0
- package/lib/types/client/LedgerPage.d.ts.map +1 -0
- package/lib/types/client/LedgerPage.js +113 -0
- package/lib/types/client/LedgerPage.js.map +1 -0
- package/lib/types/client/MemoryNavSection.d.ts +20 -0
- package/lib/types/client/MemoryNavSection.d.ts.map +1 -0
- package/lib/types/client/MemoryNavSection.js +23 -0
- package/lib/types/client/MemoryNavSection.js.map +1 -0
- package/lib/types/client/Workbench.d.ts +23 -0
- package/lib/types/client/Workbench.d.ts.map +1 -0
- package/lib/types/client/Workbench.js +140 -0
- package/lib/types/client/Workbench.js.map +1 -0
- package/lib/types/client/actions.d.ts +165 -0
- package/lib/types/client/actions.d.ts.map +1 -0
- package/lib/types/client/actions.js +51 -0
- package/lib/types/client/actions.js.map +1 -0
- package/lib/types/client/index.d.ts +24 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +103 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/navStore.d.ts +45 -0
- package/lib/types/client/navStore.d.ts.map +1 -0
- package/lib/types/client/navStore.js +36 -0
- package/lib/types/client/navStore.js.map +1 -0
- package/lib/types/core.d.ts +195 -0
- package/lib/types/core.d.ts.map +1 -0
- package/lib/types/core.js +1304 -0
- package/lib/types/core.js.map +1 -0
- package/lib/types/index.d.ts +71 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +97 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/invariant.d.ts +21 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/invariant.js.map +1 -0
- package/lib/types/service.d.ts +130 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/service.js +417 -0
- package/lib/types/service.js.map +1 -0
- package/lib/types/store.d.ts +244 -0
- package/lib/types/store.d.ts.map +1 -0
- package/lib/types/store.js +864 -0
- package/lib/types/store.js.map +1 -0
- package/lib/types/tools.d.ts +25 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/tools.js +1144 -0
- package/lib/types/tools.js.map +1 -0
- package/lib/types/types.d.ts +836 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +8 -0
- package/lib/types/types.js.map +1 -0
- package/package.json +106 -0
- package/skill/memory-extraction.md +62 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory store: durable SQLite persistence for the experience lifecycle,
|
|
3
|
+
* use reports, diary, facts, extractions, recall telemetry, and the
|
|
4
|
+
* append-only hash-chained ledger. Uses node:sqlite (DatabaseSync) so the
|
|
5
|
+
* library runs with zero external services.
|
|
6
|
+
* @module dsh-daoing-memory/store
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
/** Monotone store schema version. v1→v2 is an additive ALTER migration (see open()). */
|
|
10
|
+
export const MEMORY_SCHEMA_VERSION = 5;
|
|
11
|
+
/** Half-life (ms) of the recency weighting applied to verification samples. */
|
|
12
|
+
export const TRUST_HALF_LIFE_MS = 180 * 24 * 60 * 60 * 1000;
|
|
13
|
+
/** Hash one ledger block's content, chained to the previous hash. */
|
|
14
|
+
export function blockHash(ts, op, objectType, objectId, actor, payload, prevHash) {
|
|
15
|
+
const body = JSON.stringify({ ts, op, objectType, objectId, actor, payload, prevHash });
|
|
16
|
+
return createHash('sha256').update(body).digest('hex').slice(0, 24);
|
|
17
|
+
}
|
|
18
|
+
/** Tokenize into latin words + CJK bigrams so Chinese situations match. */
|
|
19
|
+
export function tokenize(text) {
|
|
20
|
+
const lower = text.toLowerCase();
|
|
21
|
+
const latin = lower.match(/[a-z0-9_]+/g) ?? [];
|
|
22
|
+
const cjk = lower.match(/[\u4e00-\u9fff]+/g) ?? [];
|
|
23
|
+
const bigrams = [];
|
|
24
|
+
for (const seg of cjk) {
|
|
25
|
+
if (seg.length === 1)
|
|
26
|
+
bigrams.push(seg);
|
|
27
|
+
for (let i = 0; i < seg.length - 1; i++)
|
|
28
|
+
bigrams.push(seg.slice(i, i + 2));
|
|
29
|
+
}
|
|
30
|
+
return [...latin, ...bigrams];
|
|
31
|
+
}
|
|
32
|
+
/** Rough token estimate for injection budgeting (CJK-heavy text). */
|
|
33
|
+
export function estimateTokens(text) {
|
|
34
|
+
const cjk = (text.match(/[\u4e00-\u9fff]/g) ?? []).length;
|
|
35
|
+
const rest = text.length - cjk;
|
|
36
|
+
return Math.ceil(cjk * 0.6 + rest / 4);
|
|
37
|
+
}
|
|
38
|
+
/** The durable store behind the memory core. */
|
|
39
|
+
export class MemoryStore {
|
|
40
|
+
db;
|
|
41
|
+
/** @param db - an open node:sqlite DatabaseSync handle. */
|
|
42
|
+
constructor(db) {
|
|
43
|
+
this.db = db;
|
|
44
|
+
db.exec(`
|
|
45
|
+
PRAGMA journal_mode = WAL;
|
|
46
|
+
CREATE TABLE IF NOT EXISTS memory_meta (
|
|
47
|
+
key TEXT PRIMARY KEY,
|
|
48
|
+
value TEXT NOT NULL
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS experiences (
|
|
51
|
+
family_id TEXT NOT NULL,
|
|
52
|
+
revision INTEGER NOT NULL,
|
|
53
|
+
kind TEXT NOT NULL,
|
|
54
|
+
source TEXT NOT NULL,
|
|
55
|
+
family TEXT NOT NULL,
|
|
56
|
+
gist TEXT NOT NULL,
|
|
57
|
+
situation TEXT NOT NULL,
|
|
58
|
+
path TEXT NOT NULL,
|
|
59
|
+
reasoning TEXT NOT NULL,
|
|
60
|
+
limits TEXT NOT NULL,
|
|
61
|
+
status TEXT NOT NULL,
|
|
62
|
+
alpha REAL NOT NULL,
|
|
63
|
+
beta REAL NOT NULL,
|
|
64
|
+
last_verified_at INTEGER,
|
|
65
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
tokens_saved REAL NOT NULL DEFAULT 0,
|
|
67
|
+
tokens_spent REAL NOT NULL DEFAULT 0,
|
|
68
|
+
parent_revision INTEGER,
|
|
69
|
+
failure_reason TEXT,
|
|
70
|
+
evidence TEXT,
|
|
71
|
+
challenge_reason TEXT,
|
|
72
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
context TEXT NOT NULL DEFAULT '',
|
|
74
|
+
verified_count INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
reject_count INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
global_flag INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
created_at INTEGER NOT NULL,
|
|
78
|
+
updated_at INTEGER NOT NULL,
|
|
79
|
+
PRIMARY KEY (family_id, revision)
|
|
80
|
+
);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_exp_status ON experiences (status, deleted);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_exp_family ON experiences (family);
|
|
83
|
+
CREATE TABLE IF NOT EXISTS use_reports (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
experience_id TEXT NOT NULL,
|
|
86
|
+
revision INTEGER NOT NULL,
|
|
87
|
+
outcome TEXT NOT NULL,
|
|
88
|
+
attribution TEXT NOT NULL,
|
|
89
|
+
counted TEXT NOT NULL,
|
|
90
|
+
evidence TEXT,
|
|
91
|
+
dedupe_key TEXT,
|
|
92
|
+
ts INTEGER NOT NULL
|
|
93
|
+
);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_reports_exp ON use_reports (experience_id, revision, ts);
|
|
95
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_reports_dedupe
|
|
96
|
+
ON use_reports (experience_id, revision, dedupe_key)
|
|
97
|
+
WHERE dedupe_key IS NOT NULL;
|
|
98
|
+
CREATE TABLE IF NOT EXISTS ledger (
|
|
99
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
ts INTEGER NOT NULL,
|
|
101
|
+
op TEXT NOT NULL,
|
|
102
|
+
object_type TEXT NOT NULL,
|
|
103
|
+
object_id TEXT NOT NULL,
|
|
104
|
+
actor TEXT NOT NULL,
|
|
105
|
+
reason TEXT,
|
|
106
|
+
payload TEXT NOT NULL,
|
|
107
|
+
prev_hash TEXT NOT NULL,
|
|
108
|
+
hash TEXT NOT NULL
|
|
109
|
+
);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_ledger_object ON ledger (object_type, object_id, seq DESC);
|
|
111
|
+
CREATE TABLE IF NOT EXISTS diary (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
ts INTEGER NOT NULL,
|
|
114
|
+
kind TEXT NOT NULL,
|
|
115
|
+
content TEXT NOT NULL,
|
|
116
|
+
session_ref TEXT,
|
|
117
|
+
tags TEXT NOT NULL,
|
|
118
|
+
extracted INTEGER NOT NULL DEFAULT 0
|
|
119
|
+
);
|
|
120
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
category TEXT NOT NULL,
|
|
123
|
+
fact_key TEXT NOT NULL,
|
|
124
|
+
value TEXT NOT NULL,
|
|
125
|
+
origin TEXT NOT NULL,
|
|
126
|
+
source_diary_ids TEXT NOT NULL,
|
|
127
|
+
corroboration INTEGER NOT NULL DEFAULT 1,
|
|
128
|
+
valid_from INTEGER NOT NULL,
|
|
129
|
+
valid_to INTEGER,
|
|
130
|
+
recorded_at INTEGER NOT NULL,
|
|
131
|
+
superseded_by TEXT,
|
|
132
|
+
locked INTEGER NOT NULL DEFAULT 0,
|
|
133
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
conflict_pending INTEGER NOT NULL DEFAULT 0
|
|
135
|
+
);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_facts_slot ON facts (category, fact_key, valid_to);
|
|
137
|
+
CREATE TABLE IF NOT EXISTS extractions (
|
|
138
|
+
id TEXT PRIMARY KEY,
|
|
139
|
+
ts INTEGER NOT NULL,
|
|
140
|
+
trigger TEXT NOT NULL,
|
|
141
|
+
summary TEXT NOT NULL,
|
|
142
|
+
produced_fact_ids TEXT NOT NULL,
|
|
143
|
+
diary_count INTEGER NOT NULL
|
|
144
|
+
);
|
|
145
|
+
CREATE TABLE IF NOT EXISTS recall_events (
|
|
146
|
+
id TEXT PRIMARY KEY,
|
|
147
|
+
ts INTEGER NOT NULL,
|
|
148
|
+
situation TEXT NOT NULL,
|
|
149
|
+
injected_ids TEXT NOT NULL,
|
|
150
|
+
none INTEGER NOT NULL DEFAULT 0,
|
|
151
|
+
context TEXT NOT NULL DEFAULT ''
|
|
152
|
+
);
|
|
153
|
+
CREATE TABLE IF NOT EXISTS concerns (
|
|
154
|
+
id TEXT PRIMARY KEY,
|
|
155
|
+
parent_id TEXT,
|
|
156
|
+
title TEXT NOT NULL,
|
|
157
|
+
background TEXT NOT NULL DEFAULT '',
|
|
158
|
+
kind TEXT,
|
|
159
|
+
status TEXT,
|
|
160
|
+
ts INTEGER NOT NULL,
|
|
161
|
+
source_diary_ids TEXT NOT NULL,
|
|
162
|
+
context TEXT NOT NULL DEFAULT '',
|
|
163
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
164
|
+
);
|
|
165
|
+
CREATE TABLE IF NOT EXISTS consolidations (
|
|
166
|
+
id TEXT PRIMARY KEY,
|
|
167
|
+
ts INTEGER NOT NULL,
|
|
168
|
+
merged_ids TEXT NOT NULL,
|
|
169
|
+
produced_id TEXT NOT NULL,
|
|
170
|
+
note TEXT NOT NULL DEFAULT ''
|
|
171
|
+
);
|
|
172
|
+
`);
|
|
173
|
+
const row = db.prepare('SELECT value FROM memory_meta WHERE key = ?').get('schema_version');
|
|
174
|
+
if (row === undefined) {
|
|
175
|
+
db.prepare('INSERT INTO memory_meta (key, value) VALUES (?, ?)').run('schema_version', String(MEMORY_SCHEMA_VERSION));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Sequential additive migrations; the concerns table is created by the
|
|
179
|
+
// bootstrap exec above for every older version (IF NOT EXISTS).
|
|
180
|
+
let v = Number(row.value);
|
|
181
|
+
if (v === 1) {
|
|
182
|
+
// v1 → v2 (006): context scope, dual counters, global flag.
|
|
183
|
+
db.exec(`
|
|
184
|
+
ALTER TABLE experiences ADD COLUMN context TEXT NOT NULL DEFAULT '';
|
|
185
|
+
ALTER TABLE experiences ADD COLUMN verified_count INTEGER NOT NULL DEFAULT 0;
|
|
186
|
+
ALTER TABLE experiences ADD COLUMN reject_count INTEGER NOT NULL DEFAULT 0;
|
|
187
|
+
ALTER TABLE experiences ADD COLUMN global_flag INTEGER NOT NULL DEFAULT 0;
|
|
188
|
+
ALTER TABLE recall_events ADD COLUMN context TEXT NOT NULL DEFAULT '';
|
|
189
|
+
`);
|
|
190
|
+
v = 2;
|
|
191
|
+
}
|
|
192
|
+
if (v === 2 && MEMORY_SCHEMA_VERSION >= 3) {
|
|
193
|
+
// v2 → v3 (007): concerns layer (table already created by bootstrap).
|
|
194
|
+
v = 3;
|
|
195
|
+
}
|
|
196
|
+
if (v === 3 && MEMORY_SCHEMA_VERSION >= 4) {
|
|
197
|
+
// v3 → v4 (008): consolidation log (table already created by bootstrap).
|
|
198
|
+
v = 4;
|
|
199
|
+
}
|
|
200
|
+
if (v === 4 && MEMORY_SCHEMA_VERSION >= 5) {
|
|
201
|
+
// v4 → v5 (010): concerns gain a background scene column.
|
|
202
|
+
db.exec(`ALTER TABLE concerns ADD COLUMN background TEXT NOT NULL DEFAULT '';`);
|
|
203
|
+
v = 5;
|
|
204
|
+
}
|
|
205
|
+
if (v !== MEMORY_SCHEMA_VERSION) {
|
|
206
|
+
throw new Error(`memory store schema version ${row.value} does not match ${MEMORY_SCHEMA_VERSION}; refusing to open`);
|
|
207
|
+
}
|
|
208
|
+
db.prepare('UPDATE memory_meta SET value = ? WHERE key = ?').run(String(MEMORY_SCHEMA_VERSION), 'schema_version');
|
|
209
|
+
}
|
|
210
|
+
// Context index lives outside the bootstrap exec so a v1 table (no `context`
|
|
211
|
+
// column yet) never breaks index creation; safe once the columns exist.
|
|
212
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_exp_context ON experiences (context);');
|
|
213
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_concern_parent ON concerns (parent_id);');
|
|
214
|
+
}
|
|
215
|
+
// ── experiences ───────────────────────────────────────────────────────────
|
|
216
|
+
/** Insert or update one experience revision row. */
|
|
217
|
+
upsertExperience(s) {
|
|
218
|
+
this.db.prepare(`
|
|
219
|
+
INSERT INTO experiences (
|
|
220
|
+
family_id, revision, kind, source, family, gist, situation, path, reasoning, limits,
|
|
221
|
+
status, alpha, beta, last_verified_at, pinned, tokens_saved, tokens_spent,
|
|
222
|
+
parent_revision, failure_reason, evidence, challenge_reason, deleted,
|
|
223
|
+
context, verified_count, reject_count, global_flag, created_at, updated_at
|
|
224
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?,?,?,?,?,?)
|
|
225
|
+
ON CONFLICT (family_id, revision) DO UPDATE SET
|
|
226
|
+
kind = excluded.kind,
|
|
227
|
+
source = excluded.source,
|
|
228
|
+
family = excluded.family,
|
|
229
|
+
gist = excluded.gist,
|
|
230
|
+
situation = excluded.situation,
|
|
231
|
+
path = excluded.path,
|
|
232
|
+
reasoning = excluded.reasoning,
|
|
233
|
+
limits = excluded.limits,
|
|
234
|
+
status = excluded.status,
|
|
235
|
+
alpha = excluded.alpha,
|
|
236
|
+
beta = excluded.beta,
|
|
237
|
+
last_verified_at = excluded.last_verified_at,
|
|
238
|
+
pinned = excluded.pinned,
|
|
239
|
+
tokens_saved = excluded.tokens_saved,
|
|
240
|
+
tokens_spent = excluded.tokens_spent,
|
|
241
|
+
parent_revision = excluded.parent_revision,
|
|
242
|
+
failure_reason = excluded.failure_reason,
|
|
243
|
+
evidence = excluded.evidence,
|
|
244
|
+
challenge_reason = excluded.challenge_reason,
|
|
245
|
+
context = excluded.context,
|
|
246
|
+
verified_count = excluded.verified_count,
|
|
247
|
+
reject_count = excluded.reject_count,
|
|
248
|
+
global_flag = excluded.global_flag,
|
|
249
|
+
updated_at = excluded.updated_at
|
|
250
|
+
`).run(s.id, s.revision, s.kind, s.source, s.family, s.gist, JSON.stringify(s.situation), JSON.stringify(s.path), s.reasoning, JSON.stringify(s.limits), s.status, s.alpha, s.beta, s.lastVerifiedAt ?? null, s.pinned ? 1 : 0, s.tokensSaved, s.tokensSpent, s.parentRevision ?? null, s.failureReason ?? null, s.evidence === undefined ? null : JSON.stringify(s.evidence), s.challengeReason ?? null, s.context ?? '', s.verifiedCount ?? 0, s.rejectCount ?? 0, s.globalFlag === true ? 1 : 0, s.createdAt, s.updatedAt);
|
|
251
|
+
}
|
|
252
|
+
/** Read one experience revision. */
|
|
253
|
+
getExperience(familyId, revision) {
|
|
254
|
+
const row = this.db.prepare('SELECT * FROM experiences WHERE family_id = ? AND revision = ? AND deleted = 0').get(familyId, revision);
|
|
255
|
+
return row === undefined ? undefined : this.rowToExperience(row);
|
|
256
|
+
}
|
|
257
|
+
/** The active (non-superseded, non-deleted) revision of a family, if any. */
|
|
258
|
+
getActiveRevision(familyId) {
|
|
259
|
+
const row = this.db.prepare(`
|
|
260
|
+
SELECT * FROM experiences
|
|
261
|
+
WHERE family_id = ? AND deleted = 0 AND status IN ('candidate', 'live', 'challenged')
|
|
262
|
+
ORDER BY revision DESC LIMIT 1
|
|
263
|
+
`).get(familyId);
|
|
264
|
+
return row === undefined ? undefined : this.rowToExperience(row);
|
|
265
|
+
}
|
|
266
|
+
/** All revisions of one family, oldest first. */
|
|
267
|
+
getFamily(familyId) {
|
|
268
|
+
const rows = this.db.prepare('SELECT * FROM experiences WHERE family_id = ? AND deleted = 0 ORDER BY revision ASC').all(familyId);
|
|
269
|
+
return rows.map(row => this.rowToExperience(row));
|
|
270
|
+
}
|
|
271
|
+
/** List experience revisions by filter; live-ish first, best trust first. */
|
|
272
|
+
listExperiences(filter) {
|
|
273
|
+
const clauses = ['deleted = 0'];
|
|
274
|
+
const params = [];
|
|
275
|
+
if (filter.status !== undefined) {
|
|
276
|
+
clauses.push('status = ?');
|
|
277
|
+
params.push(filter.status);
|
|
278
|
+
}
|
|
279
|
+
if (filter.kind !== undefined) {
|
|
280
|
+
clauses.push('kind = ?');
|
|
281
|
+
params.push(filter.kind);
|
|
282
|
+
}
|
|
283
|
+
if (filter.family !== undefined) {
|
|
284
|
+
clauses.push('family = ?');
|
|
285
|
+
params.push(filter.family);
|
|
286
|
+
}
|
|
287
|
+
if (filter.context !== undefined) {
|
|
288
|
+
clauses.push('context = ?');
|
|
289
|
+
params.push(filter.context);
|
|
290
|
+
}
|
|
291
|
+
const rows = this.db.prepare(`
|
|
292
|
+
SELECT * FROM experiences WHERE ${clauses.join(' AND ')}
|
|
293
|
+
ORDER BY CASE status WHEN 'live' THEN 0 WHEN 'candidate' THEN 1 WHEN 'challenged' THEN 2
|
|
294
|
+
WHEN 'cold' THEN 3 WHEN 'archived' THEN 4 ELSE 5 END,
|
|
295
|
+
(alpha + 1.0) / (alpha + beta + 2.0) DESC,
|
|
296
|
+
updated_at DESC
|
|
297
|
+
`).all(...params);
|
|
298
|
+
return rows.map(row => this.rowToExperience(row));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Recall candidates: token overlap against situation+gist+limits of the given
|
|
302
|
+
* statuses, best overlap first. An optional context scopes the pool: only
|
|
303
|
+
* same-context or globally-shared (global_flag) revisions qualify (006 §2).
|
|
304
|
+
*/
|
|
305
|
+
recallCandidates(queryTokens, topK, opts = {}) {
|
|
306
|
+
const statuses = opts.statuses ?? ['live'];
|
|
307
|
+
let sql = `
|
|
308
|
+
SELECT * FROM experiences
|
|
309
|
+
WHERE deleted = 0 AND status IN (${statuses.map(() => '?').join(', ')})
|
|
310
|
+
`;
|
|
311
|
+
const params = [...statuses];
|
|
312
|
+
if (opts.context !== undefined && opts.context !== '') {
|
|
313
|
+
sql += ' AND (context = ? OR global_flag = 1)';
|
|
314
|
+
params.push(opts.context);
|
|
315
|
+
}
|
|
316
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
317
|
+
const scored = [];
|
|
318
|
+
for (const row of rows) {
|
|
319
|
+
const snapshot = this.rowToExperience(row);
|
|
320
|
+
const hay = new Set(tokenize([snapshot.gist, ...snapshot.situation, ...snapshot.limits].join(' ')));
|
|
321
|
+
if (hay.size === 0)
|
|
322
|
+
continue;
|
|
323
|
+
let hits = 0;
|
|
324
|
+
for (const token of queryTokens)
|
|
325
|
+
if (hay.has(token))
|
|
326
|
+
hits += 1;
|
|
327
|
+
const score = hits / Math.sqrt(queryTokens.size * hay.size);
|
|
328
|
+
if (score > 0)
|
|
329
|
+
scored.push({ snapshot, score });
|
|
330
|
+
}
|
|
331
|
+
scored.sort((a, b) => b.score - a.score);
|
|
332
|
+
return scored.slice(0, topK);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Near-duplicate detection for the information-gain gate (007 flow-log fix).
|
|
336
|
+
* Unlike recallCandidates it scores on gist+situation only (the lesson's
|
|
337
|
+
* identity), so identical lessons with divergent limits/paths still match.
|
|
338
|
+
* Returns the single best match across the requested statuses, or undefined.
|
|
339
|
+
*/
|
|
340
|
+
findNearDuplicate(queryTokens, statuses) {
|
|
341
|
+
if (queryTokens.size === 0)
|
|
342
|
+
return undefined;
|
|
343
|
+
const sql = `
|
|
344
|
+
SELECT * FROM experiences
|
|
345
|
+
WHERE deleted = 0 AND status IN (${statuses.map(() => '?').join(', ')})
|
|
346
|
+
`;
|
|
347
|
+
const rows = this.db.prepare(sql).all(...statuses);
|
|
348
|
+
let best;
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
const snapshot = this.rowToExperience(row);
|
|
351
|
+
const hay = new Set(tokenize([snapshot.gist, ...snapshot.situation].join(' ')));
|
|
352
|
+
if (hay.size === 0)
|
|
353
|
+
continue;
|
|
354
|
+
let hits = 0;
|
|
355
|
+
for (const token of queryTokens)
|
|
356
|
+
if (hay.has(token))
|
|
357
|
+
hits += 1;
|
|
358
|
+
const score = hits / Math.sqrt(queryTokens.size * hay.size);
|
|
359
|
+
if (best === undefined || score > best.score)
|
|
360
|
+
best = { snapshot, score };
|
|
361
|
+
}
|
|
362
|
+
return best;
|
|
363
|
+
}
|
|
364
|
+
/** Count live revisions in one family (capacity budget). */
|
|
365
|
+
countFamilyActive(familyTag) {
|
|
366
|
+
const row = this.db.prepare("SELECT COUNT(*) AS n FROM experiences WHERE family = ? AND status = 'live' AND deleted = 0").get(familyTag);
|
|
367
|
+
return row.n;
|
|
368
|
+
}
|
|
369
|
+
/** Mark one revision deleted (human delete tombstone). */
|
|
370
|
+
deleteFamily(familyId) {
|
|
371
|
+
this.db.prepare('UPDATE experiences SET deleted = 1, updated_at = ? WHERE family_id = ?')
|
|
372
|
+
.run(Date.now(), familyId);
|
|
373
|
+
}
|
|
374
|
+
// ── use reports ───────────────────────────────────────────────────────────
|
|
375
|
+
/**
|
|
376
|
+
* Insert one counted report; a repeated (family, revision, dedupeKey)
|
|
377
|
+
* returns false instead of double-counting (idempotent use).
|
|
378
|
+
*/
|
|
379
|
+
insertReport(report) {
|
|
380
|
+
try {
|
|
381
|
+
this.db.prepare(`
|
|
382
|
+
INSERT INTO use_reports (id, experience_id, revision, outcome, attribution, counted, evidence, dedupe_key, ts)
|
|
383
|
+
VALUES (?,?,?,?,?,?,?,?,?)
|
|
384
|
+
`).run(report.id, report.experienceId, report.revision, report.outcome, report.attribution, report.counted, report.evidence === undefined ? null : JSON.stringify(report.evidence), report.dedupeKey ?? null, report.ts);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (error instanceof Error && error.message.includes('UNIQUE'))
|
|
389
|
+
return false;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** Recent counted reports of one revision, newest first. */
|
|
394
|
+
reportsFor(familyId, revision, limit) {
|
|
395
|
+
const rows = this.db.prepare(`
|
|
396
|
+
SELECT * FROM use_reports WHERE experience_id = ? AND revision = ? AND counted != 'none'
|
|
397
|
+
ORDER BY ts DESC LIMIT ?
|
|
398
|
+
`).all(familyId, revision, limit);
|
|
399
|
+
return rows.map(row => this.rowToReport(row));
|
|
400
|
+
}
|
|
401
|
+
/** Every report row (export). */
|
|
402
|
+
allReports() {
|
|
403
|
+
const rows = this.db.prepare('SELECT * FROM use_reports ORDER BY ts ASC').all();
|
|
404
|
+
return rows.map(row => this.rowToReport(row));
|
|
405
|
+
}
|
|
406
|
+
/** Recency-weighted alpha/beta over counted reports. */
|
|
407
|
+
weightedTrust(familyId, revision, now) {
|
|
408
|
+
const rows = this.db.prepare(`
|
|
409
|
+
SELECT ts, counted FROM use_reports
|
|
410
|
+
WHERE experience_id = ? AND revision = ? AND counted IN ('alpha', 'beta')
|
|
411
|
+
`).all(familyId, revision);
|
|
412
|
+
let alpha = 0;
|
|
413
|
+
let beta = 0;
|
|
414
|
+
for (const row of rows) {
|
|
415
|
+
const weight = Math.pow(0.5, (now - row.ts) / TRUST_HALF_LIFE_MS);
|
|
416
|
+
if (row.counted === 'alpha')
|
|
417
|
+
alpha += weight;
|
|
418
|
+
else
|
|
419
|
+
beta += weight;
|
|
420
|
+
}
|
|
421
|
+
return (alpha + 1) / (alpha + beta + 2);
|
|
422
|
+
}
|
|
423
|
+
// ── ledger ────────────────────────────────────────────────────────────────
|
|
424
|
+
/** Append one block, chaining the hash; returns the stored block. */
|
|
425
|
+
appendLedger(block) {
|
|
426
|
+
const hash = blockHash(block.ts, block.op, block.objectType, block.objectId, block.actor, block.payload, block.prevHash);
|
|
427
|
+
const info = this.db.prepare(`
|
|
428
|
+
INSERT INTO ledger (ts, op, object_type, object_id, actor, reason, payload, prev_hash, hash)
|
|
429
|
+
VALUES (?,?,?,?,?,?,?,?,?)
|
|
430
|
+
`).run(block.ts, block.op, block.objectType, block.objectId, block.actor, block.reason ?? null, block.payload, block.prevHash, hash);
|
|
431
|
+
return { ...block, seq: Number(info.lastInsertRowid), hash };
|
|
432
|
+
}
|
|
433
|
+
/** The newest block's hash ('' when the ledger is empty). */
|
|
434
|
+
ledgerHead() {
|
|
435
|
+
const row = this.db.prepare('SELECT hash FROM ledger ORDER BY seq DESC LIMIT 1').get();
|
|
436
|
+
return row?.hash ?? '';
|
|
437
|
+
}
|
|
438
|
+
/** Ledger blocks, newest first, optionally filtered. */
|
|
439
|
+
ledgerQuery(filter) {
|
|
440
|
+
const { clauses, params } = this.ledgerFilterClauses(filter);
|
|
441
|
+
params.push(filter.limit);
|
|
442
|
+
let sql = `SELECT * FROM ledger WHERE ${clauses.join(' AND ')} ORDER BY seq DESC LIMIT ?`;
|
|
443
|
+
if (filter.offset !== undefined && filter.offset > 0) {
|
|
444
|
+
sql += ' OFFSET ?';
|
|
445
|
+
params.push(filter.offset);
|
|
446
|
+
}
|
|
447
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
448
|
+
return rows.map(row => this.rowToLedger(row));
|
|
449
|
+
}
|
|
450
|
+
/** Shared WHERE-builder for the ledger query and its filtered count. */
|
|
451
|
+
ledgerFilterClauses(filter) {
|
|
452
|
+
const clauses = ['1 = 1'];
|
|
453
|
+
const params = [];
|
|
454
|
+
if (filter.objectType !== undefined) {
|
|
455
|
+
clauses.push('object_type = ?');
|
|
456
|
+
params.push(filter.objectType);
|
|
457
|
+
}
|
|
458
|
+
if (filter.objectId !== undefined) {
|
|
459
|
+
clauses.push('object_id = ?');
|
|
460
|
+
params.push(filter.objectId);
|
|
461
|
+
}
|
|
462
|
+
if (filter.op !== undefined) {
|
|
463
|
+
clauses.push('op = ?');
|
|
464
|
+
params.push(filter.op);
|
|
465
|
+
}
|
|
466
|
+
if (filter.seqFrom !== undefined) {
|
|
467
|
+
clauses.push('seq >= ?');
|
|
468
|
+
params.push(filter.seqFrom);
|
|
469
|
+
}
|
|
470
|
+
if (filter.seqTo !== undefined) {
|
|
471
|
+
clauses.push('seq <= ?');
|
|
472
|
+
params.push(filter.seqTo);
|
|
473
|
+
}
|
|
474
|
+
return { clauses, params };
|
|
475
|
+
}
|
|
476
|
+
/** Count ledger blocks matching a filter (for pagination, 007 §2). */
|
|
477
|
+
ledgerQueryCount(filter) {
|
|
478
|
+
const { clauses, params } = this.ledgerFilterClauses(filter);
|
|
479
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS n FROM ledger WHERE ${clauses.join(' AND ')}`).get(...params);
|
|
480
|
+
return row.n;
|
|
481
|
+
}
|
|
482
|
+
/** The complete ledger, oldest first (integrity checks + export). */
|
|
483
|
+
ledgerAll() {
|
|
484
|
+
const rows = this.db.prepare('SELECT * FROM ledger ORDER BY seq ASC').all();
|
|
485
|
+
return rows.map(row => this.rowToLedger(row));
|
|
486
|
+
}
|
|
487
|
+
/** Total ledger block count. */
|
|
488
|
+
ledgerCount() {
|
|
489
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM ledger').get();
|
|
490
|
+
return row.n;
|
|
491
|
+
}
|
|
492
|
+
// ── diary ─────────────────────────────────────────────────────────────────
|
|
493
|
+
/** Append one diary entry (append-only layer). */
|
|
494
|
+
insertDiary(entry) {
|
|
495
|
+
this.db.prepare(`
|
|
496
|
+
INSERT INTO diary (id, ts, kind, content, session_ref, tags, extracted)
|
|
497
|
+
VALUES (?,?,?,?,?,?,?)
|
|
498
|
+
`).run(entry.id, entry.ts, entry.kind, entry.content, entry.sessionRef ?? null, JSON.stringify(entry.tags), entry.extracted ? 1 : 0);
|
|
499
|
+
}
|
|
500
|
+
/** Diary entries, newest first. */
|
|
501
|
+
listDiary(limit, offset, onlyUnextracted) {
|
|
502
|
+
const where = onlyUnextracted ? 'WHERE extracted = 0' : '';
|
|
503
|
+
const rows = this.db.prepare(`SELECT * FROM diary ${where} ORDER BY ts DESC, rowid DESC LIMIT ? OFFSET ?`).all(limit, offset);
|
|
504
|
+
return rows.map(row => this.rowToDiary(row));
|
|
505
|
+
}
|
|
506
|
+
/** Unextracted entries, oldest first (the extraction window). */
|
|
507
|
+
unextractedDiary() {
|
|
508
|
+
const rows = this.db.prepare('SELECT * FROM diary WHERE extracted = 0 ORDER BY ts ASC').all();
|
|
509
|
+
return rows.map(row => this.rowToDiary(row));
|
|
510
|
+
}
|
|
511
|
+
/** Mark diary entries extracted. */
|
|
512
|
+
markDiaryExtracted(ids) {
|
|
513
|
+
const stmt = this.db.prepare('UPDATE diary SET extracted = 1 WHERE id = ?');
|
|
514
|
+
for (const id of ids)
|
|
515
|
+
stmt.run(id);
|
|
516
|
+
}
|
|
517
|
+
/** One diary entry by id. */
|
|
518
|
+
getDiary(id) {
|
|
519
|
+
const row = this.db.prepare('SELECT * FROM diary WHERE id = ?').get(id);
|
|
520
|
+
return row === undefined ? undefined : this.rowToDiary(row);
|
|
521
|
+
}
|
|
522
|
+
/** Several diary entries by id, preserving the requested order (008 Path A: fact→diary provenance). */
|
|
523
|
+
getDiaryByIds(ids) {
|
|
524
|
+
const out = [];
|
|
525
|
+
const stmt = this.db.prepare('SELECT * FROM diary WHERE id = ?');
|
|
526
|
+
for (const id of ids) {
|
|
527
|
+
const row = stmt.get(id);
|
|
528
|
+
if (row !== undefined)
|
|
529
|
+
out.push(this.rowToDiary(row));
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
/** Diary counters. */
|
|
534
|
+
diaryCounts() {
|
|
535
|
+
const total = this.db.prepare('SELECT COUNT(*) AS n FROM diary').get();
|
|
536
|
+
const unextracted = this.db.prepare('SELECT COUNT(*) AS n FROM diary WHERE extracted = 0').get();
|
|
537
|
+
return { total: total.n, unextracted: unextracted.n };
|
|
538
|
+
}
|
|
539
|
+
// ── facts ─────────────────────────────────────────────────────────────────
|
|
540
|
+
/** Insert one fact version. */
|
|
541
|
+
insertFact(fact) {
|
|
542
|
+
this.db.prepare(`
|
|
543
|
+
INSERT INTO facts (
|
|
544
|
+
id, category, fact_key, value, origin, source_diary_ids, corroboration,
|
|
545
|
+
valid_from, valid_to, recorded_at, superseded_by, locked, deleted, conflict_pending
|
|
546
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
547
|
+
`).run(fact.id, fact.category, fact.factKey, fact.value, fact.origin, JSON.stringify(fact.sourceDiaryIds), fact.corroboration, fact.validFrom, fact.validTo ?? null, fact.recordedAt, fact.supersededBy ?? null, fact.locked ? 1 : 0, fact.deleted ? 1 : 0, fact.conflictPending ? 1 : 0);
|
|
548
|
+
}
|
|
549
|
+
/** Update one fact version in place (locking, tombstones, supersede links). */
|
|
550
|
+
updateFact(fact) {
|
|
551
|
+
this.db.prepare(`
|
|
552
|
+
UPDATE facts SET value = ?, valid_to = ?, superseded_by = ?, locked = ?, deleted = ?, conflict_pending = ?
|
|
553
|
+
WHERE id = ?
|
|
554
|
+
`).run(fact.value, fact.validTo ?? null, fact.supersededBy ?? null, fact.locked ? 1 : 0, fact.deleted ? 1 : 0, fact.conflictPending ? 1 : 0, fact.id);
|
|
555
|
+
}
|
|
556
|
+
/** The current (open valid-time window, not deleted) version of one slot. */
|
|
557
|
+
currentFact(category, factKey) {
|
|
558
|
+
const row = this.db.prepare('SELECT * FROM facts WHERE category = ? AND fact_key = ? AND valid_to IS NULL AND deleted = 0 ORDER BY recorded_at DESC LIMIT 1').get(category, factKey);
|
|
559
|
+
return row === undefined ? undefined : this.rowToFact(row);
|
|
560
|
+
}
|
|
561
|
+
/** One fact version by id. */
|
|
562
|
+
getFact(id) {
|
|
563
|
+
const row = this.db.prepare('SELECT * FROM facts WHERE id = ?').get(id);
|
|
564
|
+
return row === undefined ? undefined : this.rowToFact(row);
|
|
565
|
+
}
|
|
566
|
+
/** Shared WHERE for fact filters (008 §3: reused by list + count). */
|
|
567
|
+
factFilterClauses(filter) {
|
|
568
|
+
const clauses = ['deleted = 0'];
|
|
569
|
+
const params = [];
|
|
570
|
+
if (!filter.includeHistory)
|
|
571
|
+
clauses.push('valid_to IS NULL');
|
|
572
|
+
if (filter.category !== undefined) {
|
|
573
|
+
clauses.push('category = ?');
|
|
574
|
+
params.push(filter.category);
|
|
575
|
+
}
|
|
576
|
+
return { where: clauses.join(' AND '), params };
|
|
577
|
+
}
|
|
578
|
+
/** Fact versions by filter (008 §3: server-side pagination). */
|
|
579
|
+
listFacts(filter, limit, offset) {
|
|
580
|
+
const { where, params } = this.factFilterClauses(filter);
|
|
581
|
+
const rows = this.db.prepare(`SELECT * FROM facts WHERE ${where} ORDER BY category, fact_key, recorded_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
582
|
+
return rows.map(row => this.rowToFact(row));
|
|
583
|
+
}
|
|
584
|
+
/** Count of facts matching the filter (008 §3: pagination total). */
|
|
585
|
+
factFilteredCount(filter) {
|
|
586
|
+
const { where, params } = this.factFilterClauses(filter);
|
|
587
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS n FROM facts WHERE ${where}`).get(...params);
|
|
588
|
+
return row.n;
|
|
589
|
+
}
|
|
590
|
+
/** All fact versions including tombstones (export). */
|
|
591
|
+
allFacts() {
|
|
592
|
+
const rows = this.db.prepare('SELECT * FROM facts ORDER BY recorded_at ASC').all();
|
|
593
|
+
return rows.map(row => this.rowToFact(row));
|
|
594
|
+
}
|
|
595
|
+
/** Fact counters. */
|
|
596
|
+
factCounts() {
|
|
597
|
+
const q = (where) => this.db.prepare(`SELECT COUNT(*) AS n FROM facts WHERE ${where}`).get().n;
|
|
598
|
+
return {
|
|
599
|
+
total: q('deleted = 0'),
|
|
600
|
+
current: q('valid_to IS NULL AND deleted = 0'),
|
|
601
|
+
locked: q('valid_to IS NULL AND deleted = 0 AND locked = 1'),
|
|
602
|
+
conflictPending: q('valid_to IS NULL AND deleted = 0 AND conflict_pending = 1'),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
// ── concerns (007 §2, evolving user concerns with parent/child loop) ──────
|
|
606
|
+
rowToConcern(row) {
|
|
607
|
+
return {
|
|
608
|
+
id: row.id,
|
|
609
|
+
...(row.parent_id === null ? {} : { parentId: row.parent_id }),
|
|
610
|
+
title: row.title,
|
|
611
|
+
...(row.background === '' ? {} : { background: row.background }),
|
|
612
|
+
...(row.kind === null ? {} : { kind: row.kind }),
|
|
613
|
+
...(row.status === null ? {} : { status: row.status }),
|
|
614
|
+
ts: row.ts,
|
|
615
|
+
sourceDiaryIds: JSON.parse(row.source_diary_ids),
|
|
616
|
+
...(row.context === '' ? {} : { context: row.context }),
|
|
617
|
+
deleted: row.deleted === 1,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/** Insert one concerns row (top-level or a discussion mention). */
|
|
621
|
+
insertConcern(c) {
|
|
622
|
+
this.db.prepare(`
|
|
623
|
+
INSERT INTO concerns (id, parent_id, title, background, kind, status, ts, source_diary_ids, context, deleted)
|
|
624
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
625
|
+
`).run(c.id, c.parentId ?? null, c.title, c.background ?? '', c.kind ?? null, c.status ?? null, c.ts, JSON.stringify(c.sourceDiaryIds), c.context ?? '', c.deleted ? 1 : 0);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Top-level concerns (newest first) each with its discussion loop (oldest
|
|
629
|
+
* first). Optional kind/status filter + limit/offset pagination over the
|
|
630
|
+
* top-level rows; a mention set is attached for every returned top-level.
|
|
631
|
+
*/
|
|
632
|
+
listConcernTrees(filter, limit, offset) {
|
|
633
|
+
const where = ['parent_id IS NULL', 'deleted = 0'];
|
|
634
|
+
const params = [];
|
|
635
|
+
if (filter.kind !== undefined && filter.kind !== '') {
|
|
636
|
+
where.push('kind = ?');
|
|
637
|
+
params.push(filter.kind);
|
|
638
|
+
}
|
|
639
|
+
if (filter.status !== undefined && filter.status !== '') {
|
|
640
|
+
where.push('status = ?');
|
|
641
|
+
params.push(filter.status);
|
|
642
|
+
}
|
|
643
|
+
let sql = `SELECT * FROM concerns WHERE ${where.join(' AND ')} ORDER BY ts DESC`;
|
|
644
|
+
if (limit !== undefined) {
|
|
645
|
+
sql += ' LIMIT ? OFFSET ?';
|
|
646
|
+
params.push(limit, offset ?? 0);
|
|
647
|
+
}
|
|
648
|
+
const tops = this.db.prepare(sql).all(...params);
|
|
649
|
+
const mentions = this.db.prepare('SELECT * FROM concerns WHERE parent_id IS NOT NULL AND deleted = 0 ORDER BY ts ASC').all();
|
|
650
|
+
const byParent = new Map();
|
|
651
|
+
for (const m of mentions) {
|
|
652
|
+
const list = byParent.get(m.parent_id) ?? [];
|
|
653
|
+
list.push(this.rowToConcern(m));
|
|
654
|
+
byParent.set(m.parent_id, list);
|
|
655
|
+
}
|
|
656
|
+
return tops.map(t => ({ concern: this.rowToConcern(t), mentions: byParent.get(t.id) ?? [] }));
|
|
657
|
+
}
|
|
658
|
+
/** Count top-level (non-deleted) concerns matching the optional kind/status filter. */
|
|
659
|
+
listConcernsCount(filter) {
|
|
660
|
+
const where = ['parent_id IS NULL', 'deleted = 0'];
|
|
661
|
+
const params = [];
|
|
662
|
+
if (filter.kind !== undefined && filter.kind !== '') {
|
|
663
|
+
where.push('kind = ?');
|
|
664
|
+
params.push(filter.kind);
|
|
665
|
+
}
|
|
666
|
+
if (filter.status !== undefined && filter.status !== '') {
|
|
667
|
+
where.push('status = ?');
|
|
668
|
+
params.push(filter.status);
|
|
669
|
+
}
|
|
670
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS n FROM concerns WHERE ${where.join(' AND ')}`).get(...params);
|
|
671
|
+
return row.n;
|
|
672
|
+
}
|
|
673
|
+
/** Update a top-level concern's lifecycle status. */
|
|
674
|
+
setConcernStatus(id, status) {
|
|
675
|
+
this.db.prepare('UPDATE concerns SET status = ? WHERE id = ? AND parent_id IS NULL').run(status, id);
|
|
676
|
+
}
|
|
677
|
+
/** Tombstone a concern and its whole discussion loop (human-only cleanup). */
|
|
678
|
+
deleteConcernSubtree(id) {
|
|
679
|
+
this.db.prepare('UPDATE concerns SET deleted = 1 WHERE id = ? OR parent_id = ?').run(id, id);
|
|
680
|
+
}
|
|
681
|
+
// ── extractions + recall telemetry ────────────────────────────────────────
|
|
682
|
+
/** Record one extraction run. */
|
|
683
|
+
insertExtraction(record) {
|
|
684
|
+
this.db.prepare(`
|
|
685
|
+
INSERT INTO extractions (id, ts, trigger, summary, produced_fact_ids, diary_count)
|
|
686
|
+
VALUES (?,?,?,?,?,?)
|
|
687
|
+
`).run(record.id, record.ts, record.trigger, record.summary, JSON.stringify(record.producedFactIds), record.diaryCount);
|
|
688
|
+
}
|
|
689
|
+
/** Extraction runs, newest first (008 §3: server-side pagination). */
|
|
690
|
+
listExtractions(limit, offset) {
|
|
691
|
+
const rows = this.db.prepare('SELECT * FROM extractions ORDER BY ts DESC, rowid DESC LIMIT ? OFFSET ?').all(limit, offset);
|
|
692
|
+
return rows.map(row => ({
|
|
693
|
+
id: row.id,
|
|
694
|
+
ts: row.ts,
|
|
695
|
+
trigger: row.trigger,
|
|
696
|
+
summary: row.summary,
|
|
697
|
+
producedFactIds: JSON.parse(row.produced_fact_ids),
|
|
698
|
+
diaryCount: row.diary_count,
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
/** Total extraction runs (008 §3: pagination count). */
|
|
702
|
+
extractionCount() {
|
|
703
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM extractions').get();
|
|
704
|
+
return row.n;
|
|
705
|
+
}
|
|
706
|
+
/** Last extraction ts (cadence gate). */
|
|
707
|
+
lastExtractionTs() {
|
|
708
|
+
const row = this.db.prepare('SELECT MAX(ts) AS ts FROM extractions').get();
|
|
709
|
+
return row.ts ?? 0;
|
|
710
|
+
}
|
|
711
|
+
// ── consolidation log (008 §1) ────────────────────────────────────────────
|
|
712
|
+
/** Record one consolidation run (which experiences merged into which). */
|
|
713
|
+
recordConsolidation(record) {
|
|
714
|
+
this.db.prepare('INSERT INTO consolidations (id, ts, merged_ids, produced_id, note) VALUES (?,?,?,?,?)')
|
|
715
|
+
.run(record.id, record.ts, JSON.stringify(record.mergedIds), record.producedId, record.note);
|
|
716
|
+
}
|
|
717
|
+
/** Last consolidation ts (interval cadence gate; 0 = never consolidated). */
|
|
718
|
+
lastConsolidationTs() {
|
|
719
|
+
const row = this.db.prepare('SELECT MAX(ts) AS ts FROM consolidations').get();
|
|
720
|
+
return row.ts ?? 0;
|
|
721
|
+
}
|
|
722
|
+
/** Non-deleted experiences created strictly after `ts` (new material since last consolidation). */
|
|
723
|
+
countExperiencesSince(ts) {
|
|
724
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM experiences WHERE deleted = 0 AND created_at > ?').get(ts);
|
|
725
|
+
return row.n;
|
|
726
|
+
}
|
|
727
|
+
/** Archive a set of experiences by family_id (leave recall; recoverable — never hard-deleted autonomously). */
|
|
728
|
+
archiveExperienceIds(ids, ts) {
|
|
729
|
+
const stmt = this.db.prepare(`UPDATE experiences SET status = ?, updated_at = ? WHERE family_id = ? AND deleted = 0 AND status IN ('candidate','live','challenged')`);
|
|
730
|
+
for (const id of ids)
|
|
731
|
+
stmt.run('archived', ts, id);
|
|
732
|
+
}
|
|
733
|
+
/** Record one recall event. */
|
|
734
|
+
insertRecallEvent(event) {
|
|
735
|
+
this.db.prepare('INSERT INTO recall_events (id, ts, situation, injected_ids, none, context) VALUES (?,?,?,?,?,?)')
|
|
736
|
+
.run(event.id, event.ts, event.situation, JSON.stringify(event.injectedIds), event.none ? 1 : 0, event.context ?? '');
|
|
737
|
+
}
|
|
738
|
+
/** Recall telemetry counters. */
|
|
739
|
+
recallCounts() {
|
|
740
|
+
const events = this.db.prepare('SELECT COUNT(*) AS n FROM recall_events').get();
|
|
741
|
+
const negative = this.db.prepare('SELECT COUNT(*) AS n FROM recall_events WHERE none = 1').get();
|
|
742
|
+
return { events: events.n, negative: negative.n };
|
|
743
|
+
}
|
|
744
|
+
/** All recall events (export). */
|
|
745
|
+
allRecallEvents() {
|
|
746
|
+
const rows = this.db.prepare('SELECT * FROM recall_events ORDER BY ts ASC').all();
|
|
747
|
+
return rows.map(row => ({
|
|
748
|
+
id: row.id,
|
|
749
|
+
ts: row.ts,
|
|
750
|
+
situation: row.situation,
|
|
751
|
+
injectedIds: JSON.parse(row.injected_ids),
|
|
752
|
+
none: row.none === 1,
|
|
753
|
+
context: row.context ?? '',
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
// ── row mapping ───────────────────────────────────────────────────────────
|
|
757
|
+
rowToExperience(row) {
|
|
758
|
+
const alpha = row.alpha;
|
|
759
|
+
const beta = row.beta;
|
|
760
|
+
const snapshot = {
|
|
761
|
+
id: row.family_id,
|
|
762
|
+
revision: row.revision,
|
|
763
|
+
kind: row.kind,
|
|
764
|
+
source: row.source,
|
|
765
|
+
family: row.family,
|
|
766
|
+
gist: row.gist,
|
|
767
|
+
situation: JSON.parse(row.situation),
|
|
768
|
+
path: JSON.parse(row.path),
|
|
769
|
+
reasoning: row.reasoning,
|
|
770
|
+
limits: JSON.parse(row.limits),
|
|
771
|
+
status: row.status,
|
|
772
|
+
alpha,
|
|
773
|
+
beta,
|
|
774
|
+
samples: alpha + beta,
|
|
775
|
+
trust: (alpha + 1) / (alpha + beta + 2),
|
|
776
|
+
weightedTrust: this.weightedTrust(row.family_id, row.revision, Date.now()),
|
|
777
|
+
pinned: row.pinned === 1,
|
|
778
|
+
tokensSaved: row.tokens_saved,
|
|
779
|
+
tokensSpent: row.tokens_spent,
|
|
780
|
+
context: row.context,
|
|
781
|
+
verifiedCount: row.verified_count,
|
|
782
|
+
rejectCount: row.reject_count,
|
|
783
|
+
globalFlag: row.global_flag === 1,
|
|
784
|
+
createdAt: row.created_at,
|
|
785
|
+
updatedAt: row.updated_at,
|
|
786
|
+
};
|
|
787
|
+
if (row.last_verified_at !== null)
|
|
788
|
+
snapshot.lastVerifiedAt = row.last_verified_at;
|
|
789
|
+
if (row.parent_revision !== null)
|
|
790
|
+
snapshot.parentRevision = row.parent_revision;
|
|
791
|
+
if (row.failure_reason !== null)
|
|
792
|
+
snapshot.failureReason = row.failure_reason;
|
|
793
|
+
if (row.evidence !== null)
|
|
794
|
+
snapshot.evidence = JSON.parse(row.evidence);
|
|
795
|
+
if (row.challenge_reason !== null)
|
|
796
|
+
snapshot.challengeReason = row.challenge_reason;
|
|
797
|
+
return snapshot;
|
|
798
|
+
}
|
|
799
|
+
rowToReport(row) {
|
|
800
|
+
const report = {
|
|
801
|
+
id: row.id,
|
|
802
|
+
experienceId: row.experience_id,
|
|
803
|
+
revision: row.revision,
|
|
804
|
+
outcome: row.outcome,
|
|
805
|
+
attribution: row.attribution,
|
|
806
|
+
counted: row.counted,
|
|
807
|
+
evidence: row.evidence === null ? undefined : JSON.parse(row.evidence),
|
|
808
|
+
dedupeKey: row.dedupe_key === null ? undefined : row.dedupe_key,
|
|
809
|
+
ts: row.ts,
|
|
810
|
+
};
|
|
811
|
+
return report;
|
|
812
|
+
}
|
|
813
|
+
rowToLedger(row) {
|
|
814
|
+
const block = {
|
|
815
|
+
seq: row.seq,
|
|
816
|
+
ts: row.ts,
|
|
817
|
+
op: row.op,
|
|
818
|
+
objectType: row.object_type,
|
|
819
|
+
objectId: row.object_id,
|
|
820
|
+
actor: row.actor,
|
|
821
|
+
payload: row.payload,
|
|
822
|
+
prevHash: row.prev_hash,
|
|
823
|
+
hash: row.hash,
|
|
824
|
+
};
|
|
825
|
+
if (row.reason !== null)
|
|
826
|
+
block.reason = row.reason;
|
|
827
|
+
return block;
|
|
828
|
+
}
|
|
829
|
+
rowToDiary(row) {
|
|
830
|
+
const entry = {
|
|
831
|
+
id: row.id,
|
|
832
|
+
ts: row.ts,
|
|
833
|
+
kind: row.kind,
|
|
834
|
+
content: row.content,
|
|
835
|
+
tags: JSON.parse(row.tags),
|
|
836
|
+
extracted: row.extracted === 1,
|
|
837
|
+
};
|
|
838
|
+
if (row.session_ref !== null)
|
|
839
|
+
entry.sessionRef = row.session_ref;
|
|
840
|
+
return entry;
|
|
841
|
+
}
|
|
842
|
+
rowToFact(row) {
|
|
843
|
+
const fact = {
|
|
844
|
+
id: row.id,
|
|
845
|
+
category: row.category,
|
|
846
|
+
factKey: row.fact_key,
|
|
847
|
+
value: row.value,
|
|
848
|
+
origin: row.origin,
|
|
849
|
+
sourceDiaryIds: JSON.parse(row.source_diary_ids),
|
|
850
|
+
corroboration: row.corroboration,
|
|
851
|
+
validFrom: row.valid_from,
|
|
852
|
+
recordedAt: row.recorded_at,
|
|
853
|
+
locked: row.locked === 1,
|
|
854
|
+
deleted: row.deleted === 1,
|
|
855
|
+
conflictPending: row.conflict_pending === 1,
|
|
856
|
+
};
|
|
857
|
+
if (row.valid_to !== null)
|
|
858
|
+
fact.validTo = row.valid_to;
|
|
859
|
+
if (row.superseded_by !== null)
|
|
860
|
+
fact.supersededBy = row.superseded_by;
|
|
861
|
+
return fact;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
//# sourceMappingURL=store.js.map
|