@titan-design/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/dist/index.js ADDED
@@ -0,0 +1,661 @@
1
+ // src/types.ts
2
+ import { z } from "zod";
3
+ var BULLET_SCOPES = ["global", "workspace", "language", "framework", "task"];
4
+ var BULLET_TYPES = ["rule", "anti-pattern"];
5
+ var BULLET_KINDS = ["project_convention", "stack_pattern", "workflow_rule", "anti_pattern"];
6
+ var BULLET_SOURCES = ["learned", "community", "manual", "custom"];
7
+ var BULLET_STATES = ["draft", "active", "retired"];
8
+ var MATURITIES = ["candidate", "established", "proven", "deprecated"];
9
+ var addDelta = z.object({
10
+ type: z.literal("add"),
11
+ content: z.string().min(1),
12
+ category: z.string().min(1).default("general"),
13
+ tags: z.array(z.string()).default([]),
14
+ scope: z.enum(BULLET_SCOPES).default("workspace"),
15
+ kind: z.enum(BULLET_KINDS).default("workflow_rule"),
16
+ isNegative: z.boolean().default(false),
17
+ reasoning: z.string().optional()
18
+ });
19
+ var feedbackDelta = (type) => z.object({ type: z.literal(type), bulletId: z.string().min(1), reason: z.string().optional() });
20
+ var replaceDelta = z.object({ type: z.literal("replace"), bulletId: z.string().min(1), content: z.string().min(1), reasoning: z.string().optional() });
21
+ var deprecateDelta = z.object({ type: z.literal("deprecate"), bulletId: z.string().min(1), reason: z.string().min(1) });
22
+ var mergeDelta = z.object({ type: z.literal("merge"), bulletIds: z.array(z.string().min(1)).min(2), content: z.string().min(1), reasoning: z.string().optional() });
23
+ var PlaybookDeltaSchema = z.discriminatedUnion("type", [
24
+ addDelta,
25
+ feedbackDelta("helpful"),
26
+ feedbackDelta("harmful"),
27
+ replaceDelta,
28
+ deprecateDelta,
29
+ mergeDelta
30
+ ]);
31
+
32
+ // src/schema.ts
33
+ import { SQL_NOW, kitDdl, quoteIdent } from "@titan-design/store-sqlite";
34
+ var DEFAULT_MEMORY_TABLES = {
35
+ entity: "memory_entity",
36
+ edge: "memory_edge",
37
+ cacheBlob: "memory_cache",
38
+ watermark: "memory_watermark",
39
+ feedback: "memory_feedback"
40
+ };
41
+ var BULLET_KIND = "bullet";
42
+ var BLOCKED_KIND = "blocked";
43
+ var SUPERSEDES = "supersedes";
44
+ function feedbackTableDdl(name = DEFAULT_MEMORY_TABLES.feedback) {
45
+ const t = quoteIdent(name);
46
+ return `
47
+ CREATE TABLE IF NOT EXISTS ${t} (
48
+ id INTEGER PRIMARY KEY,
49
+ bullet_ref TEXT NOT NULL,
50
+ type TEXT NOT NULL,
51
+ at TEXT NOT NULL DEFAULT (${SQL_NOW}),
52
+ session_ref TEXT,
53
+ reason TEXT
54
+ );
55
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_bullet`)} ON ${t}(bullet_ref, at);
56
+ `;
57
+ }
58
+ function memoryDdl(tables = DEFAULT_MEMORY_TABLES) {
59
+ const kit = kitDdl({ entity: tables.entity, edge: tables.edge, cacheBlob: tables.cacheBlob, watermark: tables.watermark });
60
+ return `${kit}
61
+ ${feedbackTableDdl(tables.feedback)}`;
62
+ }
63
+ function memoryMigration(version, tables = DEFAULT_MEMORY_TABLES) {
64
+ return { version, name: `memory:${tables.entity}`, up: (db) => db.exec(memoryDdl(tables)) };
65
+ }
66
+
67
+ // src/scoring.ts
68
+ var DEFAULT_HALF_LIFE_DAYS = 90;
69
+ var HARMFUL_WEIGHT = 4;
70
+ var HARD_DEPRECATE_SCORE = -3;
71
+ var MATURITY_MULTIPLIER = { candidate: 0.5, established: 1, proven: 1.5, deprecated: 0 };
72
+ var RANK = { deprecated: 0, candidate: 1, established: 2, proven: 3 };
73
+ var BY_RANK = ["deprecated", "candidate", "established", "proven"];
74
+ var DAY_MS = 864e5;
75
+ function decayedValue(at, now, halfLifeDays = DEFAULT_HALF_LIFE_DAYS) {
76
+ const ageDays = Math.max(0, now.getTime() - Date.parse(at)) / DAY_MS;
77
+ return 0.5 ** (ageDays / halfLifeDays);
78
+ }
79
+ function decayedCounts(events, now, halfLifeDays = DEFAULT_HALF_LIFE_DAYS) {
80
+ const counts = { helpful: 0, harmful: 0 };
81
+ for (const event of events) counts[event.type] += decayedValue(event.at, now, halfLifeDays);
82
+ return counts;
83
+ }
84
+ function effectiveScore(counts, maturity) {
85
+ return (counts.helpful - HARMFUL_WEIGHT * counts.harmful) * MATURITY_MULTIPLIER[maturity];
86
+ }
87
+ function maturityFor(counts) {
88
+ const total = counts.helpful + counts.harmful;
89
+ const harmfulRatio = total === 0 ? 0 : counts.harmful / total;
90
+ if (total < 3) return "candidate";
91
+ if (harmfulRatio > 0.3) return "deprecated";
92
+ if (counts.helpful >= 10 && harmfulRatio < 0.1) return "proven";
93
+ return "established";
94
+ }
95
+ function nextMaturity(current, counts, pinned) {
96
+ if (pinned || current === "deprecated") return current;
97
+ if (effectiveScore(counts, current) < HARD_DEPRECATE_SCORE) return "deprecated";
98
+ const target = maturityFor(counts);
99
+ if (RANK[target] > RANK[current]) return target;
100
+ if (RANK[target] < RANK[current]) return BY_RANK[RANK[current] - 1];
101
+ return current;
102
+ }
103
+ function isStale(lastEvidenceAt, now, { staleAfterDays = 180 } = {}) {
104
+ return now.getTime() - Date.parse(lastEvidenceAt) > staleAfterDays * DAY_MS;
105
+ }
106
+
107
+ // src/text.ts
108
+ import { contentHashOf } from "@titan-design/store-sqlite";
109
+ var TOKEN = /[a-z0-9_]+/g;
110
+ var NEGATION = /\b(never|don'?t|do not|avoid|not|no|stop|without)\b/i;
111
+ function tokenize(text) {
112
+ const out = /* @__PURE__ */ new Set();
113
+ for (const match of text.toLowerCase().matchAll(TOKEN)) if (match[0].length >= 2) out.add(match[0]);
114
+ return out;
115
+ }
116
+ function normalizeContent(text) {
117
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
118
+ }
119
+ function contentKey(text) {
120
+ return contentHashOf(normalizeContent(text));
121
+ }
122
+ function jaccard(a, b) {
123
+ const ta = tokenize(a);
124
+ const tb = tokenize(b);
125
+ if (ta.size === 0 && tb.size === 0) return 1;
126
+ let shared = 0;
127
+ for (const token of ta) if (tb.has(token)) shared++;
128
+ return shared / (ta.size + tb.size - shared);
129
+ }
130
+ function isNegated(text) {
131
+ return NEGATION.test(text);
132
+ }
133
+
134
+ // src/store.ts
135
+ import { EdgeTable, EntityTable, quoteIdent as quoteIdent2, refKind } from "@titan-design/store-sqlite";
136
+ import { randomUUID } from "crypto";
137
+ var bulletRef = refKind("memory");
138
+ var blockedRef = refKind("memory-blocked");
139
+ var BulletNotFound = class extends Error {
140
+ constructor(id) {
141
+ super(`bullet not found: ${id}`);
142
+ this.name = "BulletNotFound";
143
+ }
144
+ };
145
+ var PlaybookStore = class {
146
+ tables;
147
+ entities;
148
+ edges;
149
+ now;
150
+ insertFeedback;
151
+ feedbackByBullet;
152
+ allFeedback;
153
+ constructor(db, options = {}) {
154
+ this.tables = options.tables ?? DEFAULT_MEMORY_TABLES;
155
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
156
+ this.entities = new EntityTable(db, { name: this.tables.entity });
157
+ this.edges = new EdgeTable(db, { name: this.tables.edge });
158
+ const f = quoteIdent2(this.tables.feedback);
159
+ this.insertFeedback = db.prepare(`INSERT INTO ${f} (bullet_ref, type, at, session_ref, reason) VALUES (?, ?, ?, ?, ?)`);
160
+ this.feedbackByBullet = db.prepare(`SELECT * FROM ${f} WHERE bullet_ref = ? ORDER BY at, id`);
161
+ this.allFeedback = db.prepare(`SELECT * FROM ${f} ORDER BY at, id`);
162
+ }
163
+ add(input) {
164
+ const at = this.now().toISOString();
165
+ const bullet = materialize(input, at);
166
+ this.write(bullet);
167
+ return bullet;
168
+ }
169
+ get(id) {
170
+ const row = this.entities.get(bulletRef(id));
171
+ return row === void 0 || row.kind !== BULLET_KIND ? void 0 : toBullet(row);
172
+ }
173
+ require(id) {
174
+ const bullet = this.get(id);
175
+ if (bullet === void 0) throw new BulletNotFound(id);
176
+ return bullet;
177
+ }
178
+ /** Live bullets only, unless `includeRetired`; ordered by id for determinism. */
179
+ list({ includeRetired = false } = {}) {
180
+ const all = this.entities.listByKind(BULLET_KIND).map(toBullet);
181
+ return includeRetired ? all : all.filter((b) => b.state !== "retired");
182
+ }
183
+ update(id, patch) {
184
+ const next = { ...this.require(id), ...patch, updatedAt: this.now().toISOString() };
185
+ this.write(next);
186
+ return next;
187
+ }
188
+ /** Retire a bullet. With `replacedBy`, also assert `replacedBy -supersedes-> id`. */
189
+ deprecate(id, reason, replacedBy = null) {
190
+ const at = this.now().toISOString();
191
+ const bullet = this.update(id, { state: "retired", maturity: "deprecated", replacedBy, deprecatedAt: at, deprecationReason: reason });
192
+ if (replacedBy !== null) this.edges.assert({ sourceRef: bulletRef(replacedBy), relation: SUPERSEDES, targetRef: bulletRef(id), tValid: at });
193
+ return bullet;
194
+ }
195
+ /** Ids of the bullets this one replaced, following live `supersedes` edges. */
196
+ supersededBy(id) {
197
+ return this.edges.from(bulletRef(id)).filter((e) => e.relation === SUPERSEDES).map((e) => e.targetRef.slice("memory:".length));
198
+ }
199
+ recordFeedback(id, type, input = {}) {
200
+ this.require(id);
201
+ const at = input.at ?? this.now().toISOString();
202
+ const info = this.insertFeedback.run(bulletRef(id), type, at, input.sessionRef ?? null, input.reason ?? null);
203
+ return { id: Number(info.lastInsertRowid), bulletId: id, type, at, sessionRef: input.sessionRef ?? null, reason: input.reason ?? null };
204
+ }
205
+ feedbackFor(id) {
206
+ return this.feedbackByBullet.all(bulletRef(id)).map(toEvent);
207
+ }
208
+ /** Every event grouped by bullet id, one query, for scoring the whole playbook. */
209
+ feedbackByBulletId() {
210
+ const grouped = /* @__PURE__ */ new Map();
211
+ for (const event of this.allFeedback.all().map(toEvent)) {
212
+ const list = grouped.get(event.bulletId) ?? [];
213
+ list.push(event);
214
+ grouped.set(event.bulletId, list);
215
+ }
216
+ return grouped;
217
+ }
218
+ /** A content pattern a human has banned; the curator refuses to re-learn anything near it. */
219
+ block(pattern, reason) {
220
+ const blockedAt = this.now().toISOString();
221
+ this.entities.upsert({ ref: blockedRef(randomUUID()), kind: BLOCKED_KIND, name: pattern, attrs: { reason, blockedAt }, tValid: blockedAt });
222
+ return { pattern, reason, blockedAt };
223
+ }
224
+ blockedPatterns() {
225
+ return this.entities.listByKind(BLOCKED_KIND).map((row) => ({
226
+ pattern: row.name ?? "",
227
+ reason: String(row.attrs?.reason ?? ""),
228
+ blockedAt: String(row.attrs?.blockedAt ?? row.tValid)
229
+ }));
230
+ }
231
+ write(bullet) {
232
+ const { id, content, category, ...attrs } = bullet;
233
+ this.entities.upsert({ ref: bulletRef(id), kind: BULLET_KIND, name: category, attrs: { ...attrs, content }, tValid: bullet.createdAt });
234
+ }
235
+ };
236
+ function newBulletId(now = /* @__PURE__ */ new Date()) {
237
+ return `b-${now.getTime().toString(36)}-${randomUUID().slice(0, 6)}`;
238
+ }
239
+ function materialize(input, at) {
240
+ return {
241
+ id: input.id ?? newBulletId(new Date(at)),
242
+ content: input.content,
243
+ category: input.category ?? "general",
244
+ tags: [...input.tags ?? []],
245
+ scope: input.scope ?? "workspace",
246
+ type: input.type ?? (input.isNegative ? "anti-pattern" : "rule"),
247
+ kind: input.kind ?? (input.isNegative ? "anti_pattern" : "workflow_rule"),
248
+ isNegative: input.isNegative ?? false,
249
+ source: input.source ?? "learned",
250
+ state: input.state ?? "active",
251
+ maturity: "candidate",
252
+ pinned: input.pinned ?? false,
253
+ pinnedReason: input.pinnedReason ?? null,
254
+ replacedBy: null,
255
+ deprecatedAt: null,
256
+ deprecationReason: null,
257
+ halfLifeDays: input.halfLifeDays ?? DEFAULT_HALF_LIFE_DAYS,
258
+ sourceSessions: [...input.sourceSessions ?? []],
259
+ reasoning: input.reasoning ?? null,
260
+ createdAt: at,
261
+ updatedAt: at
262
+ };
263
+ }
264
+ function toBullet(row) {
265
+ const attrs = row.attrs ?? {};
266
+ const { content, ...rest } = attrs;
267
+ return { id: row.ref.slice("memory:".length), content, category: row.name ?? "general", ...rest };
268
+ }
269
+ function toEvent(raw) {
270
+ return { id: raw.id, bulletId: raw.bullet_ref.slice("memory:".length), type: raw.type, at: raw.at, sessionRef: raw.session_ref, reason: raw.reason };
271
+ }
272
+
273
+ // src/playbook.ts
274
+ function scoreBullet(bullet, events, now) {
275
+ const counts = decayedCounts(events, now, bullet.halfLifeDays);
276
+ const last = events[events.length - 1];
277
+ return {
278
+ ...bullet,
279
+ helpfulCount: events.filter((e) => e.type === "helpful").length,
280
+ harmfulCount: events.filter((e) => e.type === "harmful").length,
281
+ decayedHelpful: counts.helpful,
282
+ decayedHarmful: counts.harmful,
283
+ effectiveScore: effectiveScore(counts, bullet.maturity),
284
+ lastEvidenceAt: last?.at ?? bullet.createdAt
285
+ };
286
+ }
287
+ function scorePlaybook(store, now, options = {}) {
288
+ const feedback = store.feedbackByBulletId();
289
+ return store.list(options).map((bullet) => scoreBullet(bullet, feedback.get(bullet.id) ?? [], now));
290
+ }
291
+ function applyMaturity(store, now) {
292
+ const changes = [];
293
+ for (const bullet of scorePlaybook(store, now)) {
294
+ const to = nextMaturity(bullet.maturity, { helpful: bullet.decayedHelpful, harmful: bullet.decayedHarmful }, bullet.pinned);
295
+ if (to === bullet.maturity) continue;
296
+ if (to === "deprecated") store.deprecate(bullet.id, "auto-demoted below the confidence floor");
297
+ else store.update(bullet.id, { maturity: to });
298
+ changes.push({ bulletId: bullet.id, from: bullet.maturity, to });
299
+ }
300
+ return changes;
301
+ }
302
+ function staleBullets(store, now, options = {}) {
303
+ return scorePlaybook(store, now).filter((b) => !b.pinned && isStale(b.lastEvidenceAt, now, options));
304
+ }
305
+
306
+ // src/curate.ts
307
+ function curate(store, deltas, options = {}) {
308
+ const ctx = makeContext(store, options);
309
+ const parsed = deltas.map((delta) => PlaybookDeltaSchema.parse(delta));
310
+ for (const delta of dedupeDeltas(parsed, ctx.report)) applyDelta(ctx, delta);
311
+ invertHarmfulRules(ctx);
312
+ ctx.report.maturityChanges = applyMaturity(store, ctx.now());
313
+ return ctx.report;
314
+ }
315
+ function makeContext(store, options) {
316
+ return {
317
+ store,
318
+ report: { added: [], reinforced: [], penalized: [], deprecated: [], inverted: [], blocked: [], skipped: [], conflicts: [], maturityChanges: [] },
319
+ now: options.now ?? (() => /* @__PURE__ */ new Date()),
320
+ provenance: options.provenance ? [options.provenance] : [],
321
+ nearDup: options.nearDupThreshold ?? 0.85,
322
+ conflict: options.conflictThreshold ?? 0.5,
323
+ inversion: { minHarmful: options.inversion?.minHarmful ?? 3, harmfulToHelpfulRatio: options.inversion?.harmfulToHelpfulRatio ?? 2 },
324
+ blocked: store.blockedPatterns()
325
+ };
326
+ }
327
+ function dedupeDeltas(deltas, report) {
328
+ const seen = /* @__PURE__ */ new Set();
329
+ const kept = [];
330
+ for (const delta of deltas) {
331
+ const key = delta.type === "add" ? contentKey(delta.content) : JSON.stringify(delta);
332
+ if (seen.has(key)) report.skipped.push({ delta, reason: "duplicate delta" });
333
+ else {
334
+ seen.add(key);
335
+ kept.push(delta);
336
+ }
337
+ }
338
+ return kept;
339
+ }
340
+ function applyDelta(ctx, delta) {
341
+ switch (delta.type) {
342
+ case "add":
343
+ return applyAdd(ctx, delta);
344
+ case "helpful":
345
+ case "harmful":
346
+ return applyFeedback(ctx, delta);
347
+ case "replace":
348
+ return applyReplace(ctx, delta);
349
+ case "deprecate":
350
+ return applyDeprecate(ctx, delta);
351
+ case "merge":
352
+ return applyMerge(ctx, delta);
353
+ }
354
+ }
355
+ function applyAdd(ctx, delta) {
356
+ if (isBlocked(ctx, delta.content)) {
357
+ ctx.report.blocked.push(delta.content);
358
+ return;
359
+ }
360
+ const live = ctx.store.list();
361
+ const duplicate = findDuplicate(live, delta.content, ctx.nearDup);
362
+ if (duplicate) {
363
+ reinforce(ctx, duplicate.id, "restated by a new add");
364
+ return;
365
+ }
366
+ const bullet = ctx.store.add({ ...addFields(delta), sourceSessions: ctx.provenance });
367
+ ctx.report.added.push(bullet.id);
368
+ detectConflicts(ctx, bullet, live);
369
+ }
370
+ function applyFeedback(ctx, delta) {
371
+ if (!ctx.store.get(delta.bulletId)) {
372
+ ctx.report.skipped.push({ delta, reason: "unknown bullet" });
373
+ return;
374
+ }
375
+ ctx.store.recordFeedback(delta.bulletId, delta.type, { sessionRef: ctx.provenance[0]?.sessionRef, reason: delta.reason });
376
+ (delta.type === "helpful" ? ctx.report.reinforced : ctx.report.penalized).push(delta.bulletId);
377
+ }
378
+ function applyReplace(ctx, delta) {
379
+ const old = ctx.store.get(delta.bulletId);
380
+ if (!old) {
381
+ ctx.report.skipped.push({ delta, reason: "unknown bullet" });
382
+ return;
383
+ }
384
+ const next = ctx.store.add({ ...inherit(old), content: delta.content, reasoning: delta.reasoning ?? old.reasoning, sourceSessions: [...old.sourceSessions, ...ctx.provenance] });
385
+ ctx.store.deprecate(old.id, "replaced", next.id);
386
+ ctx.report.added.push(next.id);
387
+ ctx.report.deprecated.push(old.id);
388
+ }
389
+ function applyDeprecate(ctx, delta) {
390
+ if (!ctx.store.get(delta.bulletId)) {
391
+ ctx.report.skipped.push({ delta, reason: "unknown bullet" });
392
+ return;
393
+ }
394
+ ctx.store.deprecate(delta.bulletId, delta.reason);
395
+ ctx.report.deprecated.push(delta.bulletId);
396
+ }
397
+ function applyMerge(ctx, delta) {
398
+ const olds = delta.bulletIds.map((id) => ctx.store.get(id));
399
+ const first = olds[0];
400
+ if (!first || olds.some((b) => b === void 0)) {
401
+ ctx.report.skipped.push({ delta, reason: "unknown bullet" });
402
+ return;
403
+ }
404
+ const present = olds;
405
+ const next = ctx.store.add({
406
+ ...inherit(first),
407
+ content: delta.content,
408
+ tags: [...new Set(present.flatMap((b) => b.tags))],
409
+ reasoning: delta.reasoning ?? first.reasoning,
410
+ sourceSessions: [...present.flatMap((b) => b.sourceSessions), ...ctx.provenance]
411
+ });
412
+ ctx.report.added.push(next.id);
413
+ for (const old of present) {
414
+ ctx.store.deprecate(old.id, "merged", next.id);
415
+ ctx.report.deprecated.push(old.id);
416
+ }
417
+ }
418
+ function addFields(delta) {
419
+ const { content, category, tags, scope, kind, isNegative, reasoning } = delta;
420
+ return { content, category, tags, scope, kind, isNegative, reasoning };
421
+ }
422
+ function inherit(bullet) {
423
+ const { category, tags, scope, type, kind, isNegative, source, halfLifeDays } = bullet;
424
+ return { category, tags, scope, type, kind, isNegative, source, halfLifeDays };
425
+ }
426
+ function reinforce(ctx, id, reason) {
427
+ ctx.store.recordFeedback(id, "helpful", { sessionRef: ctx.provenance[0]?.sessionRef, reason });
428
+ ctx.report.reinforced.push(id);
429
+ }
430
+ function isBlocked(ctx, content) {
431
+ const normalized = normalizeContent(content);
432
+ return ctx.blocked.some((b) => normalizeContent(b.pattern) === normalized || jaccard(b.pattern, content) >= ctx.nearDup);
433
+ }
434
+ function findDuplicate(live, content, threshold) {
435
+ const key = contentKey(content);
436
+ const negated = isNegated(content);
437
+ return live.find((b) => contentKey(b.content) === key) ?? live.find((b) => isNegated(b.content) === negated && jaccard(b.content, content) >= threshold);
438
+ }
439
+ function detectConflicts(ctx, bullet, others) {
440
+ const negated = isNegated(bullet.content);
441
+ for (const other of others) {
442
+ if (isNegated(other.content) === negated || jaccard(bullet.content, other.content) < ctx.conflict) continue;
443
+ ctx.report.conflicts.push({ bulletId: bullet.id, otherId: other.id, reason: "similar wording with opposite polarity" });
444
+ }
445
+ }
446
+ function shouldInvert(ctx, bullet) {
447
+ if (bullet.type !== "rule" || bullet.pinned || bullet.state !== "active") return false;
448
+ return bullet.decayedHarmful >= ctx.inversion.minHarmful && bullet.decayedHarmful > ctx.inversion.harmfulToHelpfulRatio * bullet.decayedHelpful;
449
+ }
450
+ function invertHarmfulRules(ctx) {
451
+ for (const bullet of scorePlaybook(ctx.store, ctx.now())) {
452
+ if (!shouldInvert(ctx, bullet)) continue;
453
+ const avoid = ctx.store.add({
454
+ ...inherit(bullet),
455
+ content: `AVOID: ${bullet.content}`,
456
+ type: "anti-pattern",
457
+ kind: "anti_pattern",
458
+ isNegative: true,
459
+ reasoning: "inverted after repeated harmful feedback",
460
+ sourceSessions: [...bullet.sourceSessions, ...ctx.provenance]
461
+ });
462
+ ctx.store.deprecate(bullet.id, "inverted to an anti-pattern", avoid.id);
463
+ ctx.report.inverted.push({ from: bullet.id, to: avoid.id });
464
+ }
465
+ }
466
+
467
+ // src/recall.ts
468
+ import { gatherFailOpen, vectorRetriever } from "@titan-design/retrieval";
469
+ var EXACT_TOKEN = 3;
470
+ var SUBSTRING = 1;
471
+ var TAG_MATCH = 5;
472
+ var SCORE_FLOOR = 0.1;
473
+ async function recall(store, query, options = {}) {
474
+ const now = options.now ?? /* @__PURE__ */ new Date();
475
+ const limit = options.limit ?? 10;
476
+ const minRelevance = options.minRelevance ?? 0.1;
477
+ const candidates = scorePlaybook(store, now, { includeRetired: true }).filter((b) => inScope(b, options.scope));
478
+ const semantic = await semanticScores(query, candidates.length, options.semantic);
479
+ const ranked = candidates.map((b) => rank(b, query, options, semantic)).filter((b) => b.relevanceScore >= minRelevance).sort(byScore);
480
+ const live = ranked.filter((b) => b.state !== "retired");
481
+ return {
482
+ bullets: live.filter((b) => !b.isNegative).slice(0, limit),
483
+ antiPatterns: live.filter((b) => b.isNegative).slice(0, limit),
484
+ deprecatedWarnings: ranked.filter((b) => b.state === "retired").slice(0, limit),
485
+ degraded: semantic.degraded
486
+ };
487
+ }
488
+ function inScope(bullet, scope) {
489
+ return scope === void 0 || bullet.scope === "global" || bullet.scope === scope;
490
+ }
491
+ function keywordScore(content, tags, query, extraTags = []) {
492
+ const contentTokens = tokenize(content);
493
+ const lower = content.toLowerCase();
494
+ const tagSet = new Set(tags.map((t) => t.toLowerCase()));
495
+ let score = 0;
496
+ for (const token of tokenize(query)) {
497
+ if (contentTokens.has(token)) score += EXACT_TOKEN;
498
+ else if (lower.includes(token)) score += SUBSTRING;
499
+ if (tagSet.has(token)) score += TAG_MATCH;
500
+ }
501
+ for (const tag of extraTags) if (tagSet.has(tag.toLowerCase())) score += TAG_MATCH;
502
+ return score;
503
+ }
504
+ async function semanticScores(query, count, options) {
505
+ if (!options || count === 0) return { similarity: /* @__PURE__ */ new Map(), weight: 0, degraded: [] };
506
+ const retriever = vectorRetriever(options.embedder, options.index, { name: "memory-vector" });
507
+ const { lists, degraded } = await gatherFailOpen([retriever], query, { limit: count, timeoutMs: options.timeoutMs });
508
+ const similarity = /* @__PURE__ */ new Map();
509
+ for (const hit of lists[0]?.hits ?? []) similarity.set(hit.id, hit.score ?? 0);
510
+ return { similarity, weight: degraded.length === 0 ? options.weight ?? 0.6 : 0, degraded };
511
+ }
512
+ function rank(bullet, query, options, semantic) {
513
+ const keyword = keywordScore(bullet.content, bullet.tags, query, options.tags);
514
+ const similarity = semantic.similarity.get(bullet.id);
515
+ const semanticScore = semantic.weight === 0 ? null : similarity ?? 0;
516
+ const relevanceScore = keyword * (1 - semantic.weight) + (semanticScore ?? 0) * semantic.weight;
517
+ return { ...bullet, keywordScore: keyword, semanticScore, relevanceScore, finalScore: relevanceScore * Math.max(SCORE_FLOOR, bullet.effectiveScore) };
518
+ }
519
+ function byScore(a, b) {
520
+ return b.finalScore - a.finalScore || b.relevanceScore - a.relevanceScore || a.id.localeCompare(b.id);
521
+ }
522
+
523
+ // src/vectors.ts
524
+ import { fromFloat32Buffer, toFloat32Buffer } from "@titan-design/embed";
525
+ import { BruteForceVectorIndex } from "@titan-design/retrieval";
526
+ import { CacheBlobTable } from "@titan-design/store-sqlite";
527
+ var MemoryVectors = class {
528
+ constructor(db, embedder, options = {}) {
529
+ this.embedder = embedder;
530
+ this.cache = new CacheBlobTable(db, { name: (options.tables ?? DEFAULT_MEMORY_TABLES).cacheBlob });
531
+ this.namespace = options.namespace ?? "memory:bullet";
532
+ }
533
+ embedder;
534
+ cache;
535
+ namespace;
536
+ /** Embed every bullet the cache does not have, in one call. Returns how many were computed. */
537
+ async ensure(bullets) {
538
+ const missing = bullets.filter((b) => this.cache.get(this.keyFor(b)) === void 0);
539
+ if (missing.length === 0) return 0;
540
+ const vectors = await this.embedder.embed(missing.map((b) => normalizeContent(b.content)));
541
+ missing.forEach((bullet, i) => {
542
+ const vector = vectors[i];
543
+ if (vector) this.cache.put(this.keyFor(bullet), toFloat32Buffer(vector), { bulletId: bullet.id });
544
+ });
545
+ return missing.length;
546
+ }
547
+ /** A searchable index over these bullets, embedding any misses first. */
548
+ async index(bullets) {
549
+ await this.ensure(bullets);
550
+ const index = new BruteForceVectorIndex();
551
+ for (const bullet of bullets) {
552
+ const hit = this.cache.get(this.keyFor(bullet));
553
+ if (hit) index.add(bullet.id, fromFloat32Buffer(hit.value));
554
+ }
555
+ return index;
556
+ }
557
+ keyFor(bullet) {
558
+ return { namespace: this.namespace, model: this.embedder.model, text: normalizeContent(bullet.content) };
559
+ }
560
+ };
561
+
562
+ // src/reflect.ts
563
+ async function reflectSession(deps, input, reflector, options = {}) {
564
+ const maxIterations = options.maxIterations ?? 3;
565
+ const maxDeltas = options.maxDeltas ?? 50;
566
+ const deltas = [];
567
+ const rejected = [];
568
+ const seen = /* @__PURE__ */ new Set();
569
+ let iterations = 0;
570
+ while (iterations < maxIterations && deltas.length < maxDeltas) {
571
+ iterations++;
572
+ const raw = await reflector({ sessionRef: input.sessionRef, diary: input.diary, iteration: iterations, bullets: deps.store.list(), priorDeltas: [...deltas] });
573
+ const fresh = parseDeltas(raw, seen, iterations, rejected);
574
+ if (fresh.length === 0) break;
575
+ deltas.push(...fresh.slice(0, maxDeltas - deltas.length));
576
+ }
577
+ const report = curate(deps.store, deltas, { ...curateOptions(options), provenance: { sessionRef: input.sessionRef, byteOffset: input.byteOffset } });
578
+ advanceWatermark(deps.watermark, input);
579
+ return { deltas, rejected, iterations, report };
580
+ }
581
+ function curateOptions(options) {
582
+ const { now, nearDupThreshold, conflictThreshold, inversion } = options;
583
+ return { now, nearDupThreshold, conflictThreshold, inversion };
584
+ }
585
+ function parseDeltas(raw, seen, iteration, rejected) {
586
+ if (!Array.isArray(raw)) {
587
+ rejected.push({ iteration, index: -1, reason: "reflector did not return an array" });
588
+ return [];
589
+ }
590
+ const fresh = [];
591
+ raw.forEach((item, index) => {
592
+ const parsed = PlaybookDeltaSchema.safeParse(item);
593
+ if (!parsed.success) {
594
+ rejected.push({ iteration, index, reason: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") });
595
+ return;
596
+ }
597
+ const key = deltaKey(parsed.data);
598
+ if (seen.has(key)) return;
599
+ seen.add(key);
600
+ fresh.push(parsed.data);
601
+ });
602
+ return fresh;
603
+ }
604
+ function deltaKey(delta) {
605
+ switch (delta.type) {
606
+ case "add":
607
+ case "replace":
608
+ case "merge":
609
+ return `${delta.type}:${normalizeContent(delta.content)}`;
610
+ default:
611
+ return `${delta.type}:${delta.bulletId}`;
612
+ }
613
+ }
614
+ function advanceWatermark(watermark, input) {
615
+ if (!watermark) return;
616
+ watermark.ensure(input.sessionRef);
617
+ watermark.advance(input.sessionRef, { lastOffset: input.byteOffset ?? 0 });
618
+ }
619
+ export {
620
+ BULLET_KINDS,
621
+ BULLET_SCOPES,
622
+ BULLET_SOURCES,
623
+ BULLET_STATES,
624
+ BULLET_TYPES,
625
+ BulletNotFound,
626
+ DEFAULT_HALF_LIFE_DAYS,
627
+ DEFAULT_MEMORY_TABLES,
628
+ HARD_DEPRECATE_SCORE,
629
+ HARMFUL_WEIGHT,
630
+ MATURITIES,
631
+ MATURITY_MULTIPLIER,
632
+ MemoryVectors,
633
+ PlaybookDeltaSchema,
634
+ PlaybookStore,
635
+ SUPERSEDES,
636
+ applyMaturity,
637
+ bulletRef,
638
+ contentKey,
639
+ curate,
640
+ decayedCounts,
641
+ decayedValue,
642
+ effectiveScore,
643
+ feedbackTableDdl,
644
+ isStale,
645
+ jaccard,
646
+ keywordScore,
647
+ maturityFor,
648
+ memoryDdl,
649
+ memoryMigration,
650
+ newBulletId,
651
+ nextMaturity,
652
+ normalizeContent,
653
+ parseDeltas,
654
+ recall,
655
+ reflectSession,
656
+ scoreBullet,
657
+ scorePlaybook,
658
+ staleBullets,
659
+ tokenize
660
+ };
661
+ //# sourceMappingURL=index.js.map