@jbarket/loomai 0.5.0 → 0.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/CHANGELOG.md +114 -1
- package/README.md +97 -0
- package/dist/backends/dedup.d.ts +23 -0
- package/dist/backends/dedup.js +100 -0
- package/dist/backends/embedding-runtime.js +38 -9
- package/dist/backends/episodes.js +1 -1
- package/dist/backends/fastembed.js +30 -4
- package/dist/backends/migrations.js +200 -0
- package/dist/backends/mmr.d.ts +8 -0
- package/dist/backends/mmr.js +18 -4
- package/dist/backends/people.d.ts +192 -0
- package/dist/backends/people.js +824 -0
- package/dist/backends/proposals.js +1 -1
- package/dist/backends/recorder-health.d.ts +34 -0
- package/dist/backends/recorder-health.js +117 -0
- package/dist/backends/salience.d.ts +39 -0
- package/dist/backends/salience.js +52 -4
- package/dist/backends/scope.d.ts +56 -0
- package/dist/backends/scope.js +75 -0
- package/dist/backends/sqlite-knowledge.d.ts +24 -4
- package/dist/backends/sqlite-knowledge.js +253 -91
- package/dist/backends/sqlite-vec.d.ts +32 -1
- package/dist/backends/sqlite-vec.js +394 -75
- package/dist/backends/types.d.ts +203 -3
- package/dist/categories.d.ts +2 -0
- package/dist/categories.js +2 -0
- package/dist/cli/args.d.ts +16 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +57 -3
- package/dist/cli/knowledge.js +4 -1
- package/dist/cli/memory.js +2 -2
- package/dist/cli/person.d.ts +2 -0
- package/dist/cli/person.js +116 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +10 -0
- package/dist/cli/remote.d.ts +36 -0
- package/dist/cli/remote.js +173 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +94 -5
- package/dist/cli/subcommands.d.ts +1 -1
- package/dist/cli/subcommands.js +1 -1
- package/dist/cli/update.js +30 -11
- package/dist/clients.js +7 -4
- package/dist/config.d.ts +14 -0
- package/dist/config.js +25 -2
- package/dist/index.js +22 -7
- package/dist/injection/harnesses.js +9 -3
- package/dist/install/harnesses.js +7 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.js +461 -374
- package/dist/tools/dossier.js +5 -1
- package/dist/tools/eurorack-import.js +3 -0
- package/dist/tools/identity.d.ts +2 -0
- package/dist/tools/identity.js +35 -2
- package/dist/tools/knowledge-history.d.ts +6 -1
- package/dist/tools/knowledge-history.js +26 -8
- package/dist/tools/knowledge-maintain.d.ts +5 -0
- package/dist/tools/knowledge-maintain.js +92 -5
- package/dist/tools/knowledge-recall.d.ts +7 -1
- package/dist/tools/knowledge-recall.js +53 -3
- package/dist/tools/knowledge-verify.d.ts +11 -1
- package/dist/tools/knowledge-verify.js +17 -4
- package/dist/tools/knowledge-write.d.ts +8 -1
- package/dist/tools/knowledge-write.js +27 -9
- package/dist/tools/memory-history.d.ts +5 -0
- package/dist/tools/memory-history.js +52 -0
- package/dist/tools/memory-list.js +2 -1
- package/dist/tools/memory-merge.d.ts +2 -0
- package/dist/tools/memory-merge.js +40 -0
- package/dist/tools/memory-revision-restore.d.ts +5 -0
- package/dist/tools/memory-revision-restore.js +33 -0
- package/dist/tools/person.d.ts +32 -0
- package/dist/tools/person.js +111 -0
- package/dist/tools/prune.js +1 -1
- package/dist/tools/recall.js +7 -1
- package/dist/tools/remember.js +18 -2
- package/dist/tools/update.js +5 -1
- package/dist/transport/cli-endpoint.d.ts +13 -0
- package/dist/transport/cli-endpoint.js +97 -0
- package/dist/transport/guards.d.ts +14 -0
- package/dist/transport/guards.js +26 -0
- package/dist/transport/http-server.d.ts +28 -1
- package/dist/transport/http-server.js +156 -18
- package/dist/transport/knowledge-server.d.ts +11 -0
- package/dist/transport/knowledge-server.js +30 -0
- package/dist/transport/knowledge-tools.d.ts +50 -0
- package/dist/transport/knowledge-tools.js +290 -0
- package/dist/transport/mcp-app-hello.d.ts +1 -1
- package/dist/widgets/display-modes.html +62 -56
- package/dist/widgets/hello.html +62 -56
- package/dist/widgets/proposals.html +62 -56
- package/dist/widgets/top-of-mind.html +60 -54
- package/package.json +7 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rebuildUniqKeys } from './dedup.js';
|
|
1
2
|
function hasColumn(db, table, column) {
|
|
2
3
|
const info = db.pragma(`table_info(${table})`);
|
|
3
4
|
return info.some((c) => c.name === column);
|
|
@@ -96,6 +97,24 @@ export const MIGRATIONS = [
|
|
|
96
97
|
db.prepare('CREATE INDEX idx_memory_revisions_memory ON memory_revisions(memory_id)').run();
|
|
97
98
|
},
|
|
98
99
|
},
|
|
100
|
+
{
|
|
101
|
+
id: 'add_sourcing',
|
|
102
|
+
description: 'Add sourcing column — provenance/trust tier: observed | relayed | inferred | system (t-328)',
|
|
103
|
+
pending: (db) => !hasColumn(db, 'memories', 'sourcing'),
|
|
104
|
+
run: (db) => {
|
|
105
|
+
// NULL on legacy records: "unknown". New writes should supply a value;
|
|
106
|
+
// the lint-on-write validator will warn (not reject) when omitted.
|
|
107
|
+
db.prepare('ALTER TABLE memories ADD COLUMN sourcing TEXT').run();
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'add_provenance',
|
|
112
|
+
description: 'Add provenance column — free-form origin (surface / person / session) for trust layer (t-328)',
|
|
113
|
+
pending: (db) => !hasColumn(db, 'memories', 'provenance'),
|
|
114
|
+
run: (db) => {
|
|
115
|
+
db.prepare('ALTER TABLE memories ADD COLUMN provenance TEXT').run();
|
|
116
|
+
},
|
|
117
|
+
},
|
|
99
118
|
{
|
|
100
119
|
id: 'add_memory_supersessions',
|
|
101
120
|
description: 'Add memory_supersessions table — records that new_ref replaced old_ref, ' +
|
|
@@ -119,6 +138,187 @@ export const MIGRATIONS = [
|
|
|
119
138
|
db.prepare('CREATE INDEX idx_memory_supersessions_new ON memory_supersessions(new_ref)').run();
|
|
120
139
|
},
|
|
121
140
|
},
|
|
141
|
+
{
|
|
142
|
+
id: 'add_confidence',
|
|
143
|
+
description: 'Add confidence column — evidence-backed trust score for feedback memories, ' +
|
|
144
|
+
'computed in code (t-338)',
|
|
145
|
+
pending: (db) => !hasColumn(db, 'memories', 'confidence'),
|
|
146
|
+
run: (db) => {
|
|
147
|
+
// NULL = not applicable (non-feedback memories never get a confidence
|
|
148
|
+
// score). remember() seeds it for category='feedback'; update() adjusts
|
|
149
|
+
// it on a confirm/contradict observation.
|
|
150
|
+
db.prepare('ALTER TABLE memories ADD COLUMN confidence REAL').run();
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'add_evidence_count',
|
|
155
|
+
description: 'Add evidence_count column — number of observations backing a feedback ' +
|
|
156
|
+
'memory\'s confidence (t-338)',
|
|
157
|
+
pending: (db) => !hasColumn(db, 'memories', 'evidence_count'),
|
|
158
|
+
run: (db) => {
|
|
159
|
+
db.prepare('ALTER TABLE memories ADD COLUMN evidence_count INTEGER NOT NULL DEFAULT 0').run();
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'add_observed_at',
|
|
164
|
+
description: 'Add observed_at column — when a feedback memory\'s confidence last took an ' +
|
|
165
|
+
'observation; the decay clock, separate from `updated` (t-338)',
|
|
166
|
+
pending: (db) => !hasColumn(db, 'memories', 'observed_at'),
|
|
167
|
+
run: (db) => {
|
|
168
|
+
// NULL falls back to `created`. Rows that already carry a confidence
|
|
169
|
+
// got it from an observation no later than their last update, so
|
|
170
|
+
// that's the best clock available for them.
|
|
171
|
+
db.prepare('ALTER TABLE memories ADD COLUMN observed_at TEXT').run();
|
|
172
|
+
db.prepare('UPDATE memories SET observed_at = COALESCE(updated, created) WHERE confidence IS NOT NULL').run();
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'add_memory_deletions',
|
|
177
|
+
description: 'Add memory_deletions table — an append-only journal of every hard delete ' +
|
|
178
|
+
'(deleteById), so forget()/prune() leave a trace of what was removed and ' +
|
|
179
|
+
'why (t-436). Standalone, keyed by ref — deliberately NOT a child table of ' +
|
|
180
|
+
'memories, so it survives the delete it records (that FK-cascade blind spot ' +
|
|
181
|
+
'is the bug this fixes).',
|
|
182
|
+
pending: (db) => !hasTable(db, 'memory_deletions'),
|
|
183
|
+
run: (db) => {
|
|
184
|
+
db.prepare(`
|
|
185
|
+
CREATE TABLE memory_deletions (
|
|
186
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
187
|
+
ref TEXT NOT NULL,
|
|
188
|
+
category TEXT NOT NULL,
|
|
189
|
+
title TEXT NOT NULL,
|
|
190
|
+
project TEXT,
|
|
191
|
+
content TEXT NOT NULL,
|
|
192
|
+
created TEXT NOT NULL,
|
|
193
|
+
deleted_at TEXT NOT NULL,
|
|
194
|
+
op TEXT NOT NULL
|
|
195
|
+
)
|
|
196
|
+
`).run();
|
|
197
|
+
db.prepare('CREATE INDEX idx_memory_deletions_ref ON memory_deletions(ref)').run();
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'add_scope',
|
|
202
|
+
description: 'Add scope column — explicit project|global visibility override, enforced ' +
|
|
203
|
+
'at recall against the category default (backends/scope.ts, t-337)',
|
|
204
|
+
pending: (db) => !hasColumn(db, 'memories', 'scope'),
|
|
205
|
+
run: (db) => {
|
|
206
|
+
// NULL on legacy records and any write that doesn't set it explicitly:
|
|
207
|
+
// effectiveScope() falls back to the category default in that case.
|
|
208
|
+
db.prepare('ALTER TABLE memories ADD COLUMN scope TEXT').run();
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'add_times_seen',
|
|
213
|
+
description: 'Add times_seen counter — bumped when remember() reheats an exact restatement instead of inserting (t-335)',
|
|
214
|
+
pending: (db) => !hasColumn(db, 'memories', 'times_seen'),
|
|
215
|
+
run: (db) => {
|
|
216
|
+
// DEFAULT 1 backfills existing rows too: each was "seen" at least once
|
|
217
|
+
// (the write that created it), even though restatements before t-335
|
|
218
|
+
// weren't counted.
|
|
219
|
+
db.prepare('ALTER TABLE memories ADD COLUMN times_seen INTEGER NOT NULL DEFAULT 1').run();
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'add_uniq',
|
|
224
|
+
description: 'Add uniq dedup key column + index, backfilled from existing rows, so write-time ' +
|
|
225
|
+
'remember() can reheat an exact restatement instead of inserting a duplicate (t-335)',
|
|
226
|
+
pending: (db) => !hasColumn(db, 'memories', 'uniq'),
|
|
227
|
+
run: (db) => {
|
|
228
|
+
db.prepare('ALTER TABLE memories ADD COLUMN uniq TEXT').run();
|
|
229
|
+
db.prepare('CREATE INDEX idx_memories_uniq ON memories(uniq)').run();
|
|
230
|
+
// Backfill: compute uniq for every existing ACTIVE row so future
|
|
231
|
+
// restatements of pre-t-335 content also reheat. Archived rows stay
|
|
232
|
+
// NULL and, within a pre-existing duplicate cluster, only the oldest
|
|
233
|
+
// active row gets the key — see rebuildUniqKeys().
|
|
234
|
+
rebuildUniqKeys(db);
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'rekey_uniq_unicode',
|
|
239
|
+
description: 'Recompute every uniq key with the Unicode-preserving normalizer — the first ' +
|
|
240
|
+
'normalizer stripped all non-ASCII text, so distinct non-Latin memories shared ' +
|
|
241
|
+
'one key (PR120 review)',
|
|
242
|
+
// Marker: the index this migration creates. Databases that ran add_uniq
|
|
243
|
+
// under the old normalizer have only idx_memories_uniq.
|
|
244
|
+
// uniq_active_unique (below) re-keys too and replaces this index.
|
|
245
|
+
pending: (db) => !hasIndex(db, 'memories', 'idx_memories_uniq_v2') &&
|
|
246
|
+
!hasIndex(db, 'memories', 'idx_memories_uniq_active') &&
|
|
247
|
+
!hasIndex(db, 'memories', 'idx_memories_uniq_exact'),
|
|
248
|
+
run: (db) => {
|
|
249
|
+
rebuildUniqKeys(db);
|
|
250
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_memories_uniq_v2 ON memories(uniq)').run();
|
|
251
|
+
db.prepare('DROP INDEX IF EXISTS idx_memories_uniq').run();
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'uniq_active_unique',
|
|
256
|
+
description: 'Make uniq UNIQUE among active rows (partial index), so two concurrent ' +
|
|
257
|
+
'remember() calls of the same content cannot both insert (PR120 review)',
|
|
258
|
+
// rekey_uniq_exact (below) replaces this index with idx_memories_uniq_exact.
|
|
259
|
+
pending: (db) => !hasIndex(db, 'memories', 'idx_memories_uniq_active') &&
|
|
260
|
+
!hasIndex(db, 'memories', 'idx_memories_uniq_exact'),
|
|
261
|
+
run: (db) => {
|
|
262
|
+
db.transaction(() => {
|
|
263
|
+
// Re-key first: any active duplicates written before this index
|
|
264
|
+
// existed (the race it closes, or stale keys from before update()
|
|
265
|
+
// refreshed them) would otherwise make CREATE UNIQUE INDEX fail.
|
|
266
|
+
rebuildUniqKeys(db);
|
|
267
|
+
db.prepare('CREATE UNIQUE INDEX idx_memories_uniq_active ON memories(uniq) WHERE archived = 0').run();
|
|
268
|
+
db.prepare('DROP INDEX IF EXISTS idx_memories_uniq_v2').run();
|
|
269
|
+
db.prepare('DROP INDEX IF EXISTS idx_memories_uniq').run();
|
|
270
|
+
}).immediate();
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'rekey_uniq_exact',
|
|
275
|
+
description: 'Recompute every uniq key with the exact normalizer — the previous one folded ' +
|
|
276
|
+
'case, punctuation and whitespace, and compared project ids loosely, so ' +
|
|
277
|
+
'"Offset -5"/"Offset 5", "1.5"/"15" and projects foo-bar/foobar shared a key ' +
|
|
278
|
+
'(PR120 review, round 2)',
|
|
279
|
+
// Marker: the unique index this migration creates, under a new name so
|
|
280
|
+
// databases keyed by the folding normalizer are detectable.
|
|
281
|
+
pending: (db) => !hasIndex(db, 'memories', 'idx_memories_uniq_exact'),
|
|
282
|
+
run: (db) => {
|
|
283
|
+
db.transaction(() => {
|
|
284
|
+
// Drop the old indexes first so the new unique index is built once,
|
|
285
|
+
// over the final keys only.
|
|
286
|
+
db.prepare('DROP INDEX IF EXISTS idx_memories_uniq_active').run();
|
|
287
|
+
db.prepare('DROP INDEX IF EXISTS idx_memories_uniq_v2').run();
|
|
288
|
+
db.prepare('DROP INDEX IF EXISTS idx_memories_uniq').run();
|
|
289
|
+
rebuildUniqKeys(db);
|
|
290
|
+
db.prepare('CREATE UNIQUE INDEX idx_memories_uniq_exact ON memories(uniq) WHERE archived = 0').run();
|
|
291
|
+
}).immediate();
|
|
292
|
+
},
|
|
293
|
+
}, {
|
|
294
|
+
id: 'people_cards',
|
|
295
|
+
description: 'Add people + person_revisions: person cards, the condensed current state of a ' +
|
|
296
|
+
'human the agent works with (docs/specs/person-cards.md, t-1376). Additive — ' +
|
|
297
|
+
'dropping both tables is the whole backout',
|
|
298
|
+
pending: (db) => !hasTable(db, 'people') || !hasTable(db, 'person_revisions'),
|
|
299
|
+
run: (db) => {
|
|
300
|
+
db.transaction(() => {
|
|
301
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS people (
|
|
302
|
+
slug TEXT PRIMARY KEY,
|
|
303
|
+
name TEXT NOT NULL,
|
|
304
|
+
card_json TEXT NOT NULL,
|
|
305
|
+
archived_at TEXT,
|
|
306
|
+
archived_by TEXT,
|
|
307
|
+
archive_reason TEXT,
|
|
308
|
+
updated TEXT NOT NULL,
|
|
309
|
+
synthesized_at TEXT
|
|
310
|
+
)`).run();
|
|
311
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS person_revisions (
|
|
312
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
313
|
+
slug TEXT NOT NULL,
|
|
314
|
+
snapshot_json TEXT NOT NULL,
|
|
315
|
+
op TEXT NOT NULL,
|
|
316
|
+
replaced_at TEXT NOT NULL
|
|
317
|
+
)`).run();
|
|
318
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_person_revisions_slug ON person_revisions(slug, id)').run();
|
|
319
|
+
}).immediate();
|
|
320
|
+
},
|
|
321
|
+
},
|
|
122
322
|
];
|
|
123
323
|
/**
|
|
124
324
|
* Apply pending migrations to a database.
|
package/dist/backends/mmr.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export interface MmrCandidate {
|
|
|
25
25
|
export interface MmrResult<T> {
|
|
26
26
|
/** Chosen candidates in selection order (first pick = most relevant). */
|
|
27
27
|
selected: T[];
|
|
28
|
+
/**
|
|
29
|
+
* The score that actually drove each pick, same order/index as `selected`:
|
|
30
|
+
* score(c) = λ·relevance(c) − (1−λ)·max_{s ∈ selected} sim(c, s). Equals
|
|
31
|
+
* the candidate's own `relevance` when MMR made no change (diversity 0 or
|
|
32
|
+
* pool <= limit) — "blended" only means something once MMR has traded a
|
|
33
|
+
* pick off against the ones already chosen.
|
|
34
|
+
*/
|
|
35
|
+
scores: number[];
|
|
28
36
|
/**
|
|
29
37
|
* How many of the top-`limit` candidates by relevance were displaced by
|
|
30
38
|
* a less-relevant-but-different one. 0 means MMR changed nothing.
|
package/dist/backends/mmr.js
CHANGED
|
@@ -19,7 +19,18 @@
|
|
|
19
19
|
/** Default `diversity` (1 − λ) for recall: λ = 0.7. */
|
|
20
20
|
export const DEFAULT_DIVERSITY = 0.3;
|
|
21
21
|
export function cosineSimilarity(a, b) {
|
|
22
|
-
|
|
22
|
+
// A zero-length vector is the "no embedding available" sentinel (e.g. a
|
|
23
|
+
// lookup miss in the caller) — treat it as no similarity, same as before.
|
|
24
|
+
// A mismatch between two REAL vectors means the embedding dimension
|
|
25
|
+
// changed between model versions somewhere upstream; silently comparing
|
|
26
|
+
// the overlapping prefix (the old Math.min(a.length, b.length) behavior)
|
|
27
|
+
// hid that corruption instead of catching it. Raise instead (t-334).
|
|
28
|
+
if (a.length === 0 || b.length === 0)
|
|
29
|
+
return 0;
|
|
30
|
+
if (a.length !== b.length) {
|
|
31
|
+
throw new Error(`cosineSimilarity: dimension mismatch (${a.length} vs ${b.length}) — embeddings from different model versions can't be compared`);
|
|
32
|
+
}
|
|
33
|
+
const n = a.length;
|
|
23
34
|
let dot = 0;
|
|
24
35
|
let na = 0;
|
|
25
36
|
let nb = 0;
|
|
@@ -42,13 +53,15 @@ export function cosineSimilarity(a, b) {
|
|
|
42
53
|
*/
|
|
43
54
|
export function mmrSelect(candidates, limit, diversity = DEFAULT_DIVERSITY) {
|
|
44
55
|
if (!(limit > 0))
|
|
45
|
-
return { selected: [], diversityDrops: 0 };
|
|
56
|
+
return { selected: [], scores: [], diversityDrops: 0 };
|
|
46
57
|
if (!(diversity > 0) || candidates.length <= limit) {
|
|
47
|
-
|
|
58
|
+
const picked = candidates.slice(0, limit);
|
|
59
|
+
return { selected: picked, scores: picked.map((c) => c.relevance), diversityDrops: 0 };
|
|
48
60
|
}
|
|
49
61
|
const lambda = 1 - Math.min(diversity, 1);
|
|
50
62
|
const remaining = candidates.map((_, i) => i);
|
|
51
63
|
const chosen = [];
|
|
64
|
+
const chosenScores = [];
|
|
52
65
|
// maxSim[i]: similarity of candidate i to the closest already-selected item.
|
|
53
66
|
const maxSim = new Float64Array(candidates.length);
|
|
54
67
|
while (chosen.length < limit && remaining.length > 0) {
|
|
@@ -64,6 +77,7 @@ export function mmrSelect(candidates, limit, diversity = DEFAULT_DIVERSITY) {
|
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
chosen.push(best);
|
|
80
|
+
chosenScores.push(bestScore);
|
|
67
81
|
remaining.splice(remaining.indexOf(best), 1);
|
|
68
82
|
const pickedVec = candidates[best].vector;
|
|
69
83
|
for (const i of remaining) {
|
|
@@ -78,5 +92,5 @@ export function mmrSelect(candidates, limit, diversity = DEFAULT_DIVERSITY) {
|
|
|
78
92
|
.slice(0, limit);
|
|
79
93
|
const chosenSet = new Set(chosen);
|
|
80
94
|
const diversityDrops = topByRelevance.filter((i) => !chosenSet.has(i)).length;
|
|
81
|
-
return { selected: chosen.map((i) => candidates[i]), diversityDrops };
|
|
95
|
+
return { selected: chosen.map((i) => candidates[i]), scores: chosenScores, diversityDrops };
|
|
82
96
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export declare const SLUG_RE: RegExp;
|
|
2
|
+
export declare const MAX_SLUG = 64;
|
|
3
|
+
export declare const MAX_RENDER = 2000;
|
|
4
|
+
/**
|
|
5
|
+
* The harness's gating names. Fixed here, not configured: a card that carried
|
|
6
|
+
* one would look like a gate, and the agent writes its own cards.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RESERVED_KEYS: readonly ['tier', 'field', 'projects', 'scope', 'trust'];
|
|
9
|
+
export type CardValue = string | number | boolean | string[];
|
|
10
|
+
export interface WhereEnvelope {
|
|
11
|
+
value: CardValue;
|
|
12
|
+
verified_at?: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
stale_after_days?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface MayEntry {
|
|
17
|
+
text: string;
|
|
18
|
+
source: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PersonCard {
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
summary?: string;
|
|
24
|
+
where: Record<string, WhereEnvelope>;
|
|
25
|
+
how: Record<string, CardValue>;
|
|
26
|
+
may: MayEntry[];
|
|
27
|
+
}
|
|
28
|
+
export interface PersonWriteInput {
|
|
29
|
+
slug: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
summary?: string;
|
|
32
|
+
where?: Record<string, WhereEnvelope | null>;
|
|
33
|
+
how?: Record<string, CardValue | null>;
|
|
34
|
+
may?: MayEntry[];
|
|
35
|
+
merge?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface PersonRow {
|
|
38
|
+
slug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
card: PersonCard;
|
|
41
|
+
archived_at: string | null;
|
|
42
|
+
archived_by: string | null;
|
|
43
|
+
archive_reason: string | null;
|
|
44
|
+
updated: string;
|
|
45
|
+
synthesized_at: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface MutationResult {
|
|
48
|
+
card: PersonCard;
|
|
49
|
+
unchanged: boolean;
|
|
50
|
+
revision_id: number | null;
|
|
51
|
+
archived: boolean;
|
|
52
|
+
updated: string;
|
|
53
|
+
synthesized_at: string | null;
|
|
54
|
+
}
|
|
55
|
+
export type PersonReadResult = {
|
|
56
|
+
found: false;
|
|
57
|
+
} | {
|
|
58
|
+
found: true;
|
|
59
|
+
archived: true;
|
|
60
|
+
} | {
|
|
61
|
+
found: true;
|
|
62
|
+
rendered: string;
|
|
63
|
+
card: PersonCard;
|
|
64
|
+
archived: boolean;
|
|
65
|
+
updated: string;
|
|
66
|
+
synthesized_at: string | null;
|
|
67
|
+
};
|
|
68
|
+
export interface PersonListEntry {
|
|
69
|
+
slug: string;
|
|
70
|
+
name: string;
|
|
71
|
+
archived: boolean;
|
|
72
|
+
updated: string;
|
|
73
|
+
synthesized_at: string | null;
|
|
74
|
+
stale: number;
|
|
75
|
+
unverified: number;
|
|
76
|
+
memories_since_synthesis: number;
|
|
77
|
+
}
|
|
78
|
+
export interface PersonRevisionMeta {
|
|
79
|
+
id: number;
|
|
80
|
+
op: RevisionOp;
|
|
81
|
+
replaced_at: string;
|
|
82
|
+
}
|
|
83
|
+
export type RevisionOp = 'write' | 'verify' | 'archive' | 'restore' | 'revision_restore';
|
|
84
|
+
/** Every rejection the people surface makes. The message names the path. */
|
|
85
|
+
export declare class PersonCardError extends Error {
|
|
86
|
+
constructor(message: string);
|
|
87
|
+
}
|
|
88
|
+
/** Null when `slug` is a valid person slug, else the reason. Never echoes an over-long slug. */
|
|
89
|
+
export declare function personSlugError(slug: unknown): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Validate a complete card: keys, types, lengths and the worst-case render
|
|
92
|
+
* cap. `nowMs` adds the time checks (no future verified_at); pass null to skip
|
|
93
|
+
* them, which revision restore does — the snapshot was valid when written.
|
|
94
|
+
*/
|
|
95
|
+
export declare function validateCard(card: unknown, nowMs: number | null): asserts card is PersonCard;
|
|
96
|
+
export type FieldState = {
|
|
97
|
+
kind: 'ok';
|
|
98
|
+
} | {
|
|
99
|
+
kind: 'unverified';
|
|
100
|
+
} | {
|
|
101
|
+
kind: 'stale';
|
|
102
|
+
days: number;
|
|
103
|
+
};
|
|
104
|
+
/** Stale when now − verified_at > stale_after_days × 86,400 s (strictly greater). */
|
|
105
|
+
export declare function fieldState(env: WhereEnvelope, nowMs: number): FieldState;
|
|
106
|
+
/**
|
|
107
|
+
* The rendered card — the text identity and `person` return. With
|
|
108
|
+
* `worstCase`, every where marker is counted at its widest ([STALE 999+d]):
|
|
109
|
+
* that is the length the write-time cap checks, so time passing can never
|
|
110
|
+
* push a stored card over it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function renderCard(card: PersonCard, nowMs: number, opts?: {
|
|
113
|
+
worstCase?: boolean;
|
|
114
|
+
}): string;
|
|
115
|
+
export interface PeopleStoreConfig {
|
|
116
|
+
dbPath: string;
|
|
117
|
+
/** Open read-only and never migrate. Reads on an unmigrated store see no cards. */
|
|
118
|
+
readonly?: boolean;
|
|
119
|
+
busyTimeoutMs?: number;
|
|
120
|
+
/** Clock, for tests. */
|
|
121
|
+
now?: () => number;
|
|
122
|
+
}
|
|
123
|
+
/** Thrown when a stored row cannot be turned back into a card. */
|
|
124
|
+
export declare class UnreadableCardError extends Error {
|
|
125
|
+
constructor(slug: string, cause: unknown);
|
|
126
|
+
}
|
|
127
|
+
export declare class PeopleStore {
|
|
128
|
+
private db;
|
|
129
|
+
private readonly now;
|
|
130
|
+
/** False on a read-only store whose tables do not exist yet. */
|
|
131
|
+
readonly migrated: boolean;
|
|
132
|
+
constructor(config: PeopleStoreConfig);
|
|
133
|
+
close(): void;
|
|
134
|
+
private nowIso;
|
|
135
|
+
private readRow;
|
|
136
|
+
private snapshot;
|
|
137
|
+
private store;
|
|
138
|
+
/** One BEGIN IMMEDIATE transaction around `fn`, retried as a whole on SQLITE_BUSY. */
|
|
139
|
+
private mutate;
|
|
140
|
+
private result;
|
|
141
|
+
private requireActive;
|
|
142
|
+
write(input: PersonWriteInput): MutationResult;
|
|
143
|
+
verify(input: {
|
|
144
|
+
slug: string;
|
|
145
|
+
fields: string[];
|
|
146
|
+
verified_at?: string;
|
|
147
|
+
source?: string;
|
|
148
|
+
}): MutationResult;
|
|
149
|
+
archive(input: {
|
|
150
|
+
slug: string;
|
|
151
|
+
reason: string;
|
|
152
|
+
actor: string;
|
|
153
|
+
}): MutationResult;
|
|
154
|
+
restore(input: {
|
|
155
|
+
slug: string;
|
|
156
|
+
}): MutationResult;
|
|
157
|
+
revisionRestore(input: {
|
|
158
|
+
slug: string;
|
|
159
|
+
revision_id: number;
|
|
160
|
+
}): MutationResult;
|
|
161
|
+
/** The stored row, or null. Throws UnreadableCardError for a row that does not parse or validate. */
|
|
162
|
+
getRow(slug: string): PersonRow | null;
|
|
163
|
+
read(slug: string, includeArchived?: boolean): PersonReadResult;
|
|
164
|
+
list(includeArchived?: boolean): PersonListEntry[];
|
|
165
|
+
history(slug: string): PersonRevisionMeta[];
|
|
166
|
+
/** A revision's snapshot (the whole previous row), for tests and inspection. */
|
|
167
|
+
revisionSnapshot(id: number): {
|
|
168
|
+
slug: string;
|
|
169
|
+
card: PersonCard;
|
|
170
|
+
synthesized_at: string | null;
|
|
171
|
+
archived_at: string | null;
|
|
172
|
+
} | null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Open the people store on a context's memory database. A read-only open
|
|
176
|
+
* returns null when there is no database file yet; a writable one creates it.
|
|
177
|
+
*/
|
|
178
|
+
export declare function openPeopleStore(contextDir: string, opts?: {
|
|
179
|
+
readonly?: boolean;
|
|
180
|
+
now?: () => number;
|
|
181
|
+
}): PeopleStore | null;
|
|
182
|
+
export interface RoomOptions {
|
|
183
|
+
nowMs?: number;
|
|
184
|
+
/** Called once per failure (store open or one unreadable row). Never throws through. */
|
|
185
|
+
onFailure?: (err: unknown) => void;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The `# In the room` block for identity, or null for no block. Best-effort
|
|
189
|
+
* like the tape: a store that cannot be opened or is not migrated gives no
|
|
190
|
+
* block; one bad row gives that slug the unreadable line and the rest render.
|
|
191
|
+
*/
|
|
192
|
+
export declare function roomBlock(contextDir: string, people: readonly string[] | undefined, opts?: RoomOptions): string | null;
|