@kanadego/dsh-heartbeat 1.9.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +344 -396
- package/client.js +108 -20
- package/config/policy.json +74 -73
- package/config/profile-schema.json +306 -306
- package/dist/{bindings-225SI7KC.js → bindings-U2M3CO2D.js} +3 -3
- package/dist/{chunk-TY6UP74X.js → chunk-3DNGY4DS.js} +2 -2
- package/dist/{chunk-CUOSICYJ.js → chunk-544QOJN5.js} +397 -23
- package/dist/chunk-544QOJN5.js.map +1 -0
- package/dist/{chunk-AJ5HLAL7.js → chunk-6MVKWICX.js} +708 -154
- package/dist/chunk-6MVKWICX.js.map +1 -0
- package/dist/{chunk-IG6YMUHW.js → chunk-6XM6KY7M.js} +43 -12
- package/dist/chunk-6XM6KY7M.js.map +1 -0
- package/dist/chunk-AKIJ4LS2.js +680 -0
- package/dist/chunk-AKIJ4LS2.js.map +1 -0
- package/dist/{chunk-KB5SMG3F.js → chunk-DC5HMCJC.js} +3 -3
- package/dist/{chunk-3MDBU6WY.js → chunk-DZ7YCFGX.js} +37 -7
- package/dist/chunk-DZ7YCFGX.js.map +1 -0
- package/dist/{chunk-WRUTATW4.js → chunk-IBBAISSW.js} +13 -2
- package/dist/chunk-IBBAISSW.js.map +1 -0
- package/dist/{chunk-ZLV4NMFX.js → chunk-IFRMKVIC.js} +85 -21
- package/dist/chunk-IFRMKVIC.js.map +1 -0
- package/dist/{chunk-XFNCTOW7.js → chunk-NCPKOWS7.js} +52 -4
- package/dist/chunk-NCPKOWS7.js.map +1 -0
- package/dist/{chunk-JDH2LDFS.js → chunk-TVX4HV4Y.js} +4 -6
- package/dist/{chunk-JDH2LDFS.js.map → chunk-TVX4HV4Y.js.map} +1 -1
- package/dist/cli/index.js +149 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-P26DDMWC.js → inbox-OWBK6PE2.js} +8 -7
- package/dist/index.d.ts +50 -315
- package/dist/index.js +288 -920
- package/dist/index.js.map +1 -1
- package/dist/migrate-TQ5PB2DI.js +27 -0
- package/dist/{orchestrator-EUTZ3KG6.js → orchestrator-P7AEWVYR.js} +27 -12
- package/dist/{pool-UTXQJJ5C.js → pool-2T6TVS2L.js} +4 -5
- package/dist/{snapshot-K5VHETTK.js → snapshot-F5B2DD56.js} +5 -5
- package/package.json +84 -83
- package/dist/chunk-3MDBU6WY.js.map +0 -1
- package/dist/chunk-AJ5HLAL7.js.map +0 -1
- package/dist/chunk-CUOSICYJ.js.map +0 -1
- package/dist/chunk-IG6YMUHW.js.map +0 -1
- package/dist/chunk-IV2ZWQA3.js +0 -56
- package/dist/chunk-IV2ZWQA3.js.map +0 -1
- package/dist/chunk-K5Y6JP2B.js +0 -399
- package/dist/chunk-K5Y6JP2B.js.map +0 -1
- package/dist/chunk-WRUTATW4.js.map +0 -1
- package/dist/chunk-XFNCTOW7.js.map +0 -1
- package/dist/chunk-ZLV4NMFX.js.map +0 -1
- package/dist/migrate-D4VKAKYT.js +0 -21
- /package/dist/{bindings-225SI7KC.js.map → bindings-U2M3CO2D.js.map} +0 -0
- /package/dist/{chunk-TY6UP74X.js.map → chunk-3DNGY4DS.js.map} +0 -0
- /package/dist/{chunk-KB5SMG3F.js.map → chunk-DC5HMCJC.js.map} +0 -0
- /package/dist/{inbox-P26DDMWC.js.map → inbox-OWBK6PE2.js.map} +0 -0
- /package/dist/{migrate-D4VKAKYT.js.map → migrate-TQ5PB2DI.js.map} +0 -0
- /package/dist/{orchestrator-EUTZ3KG6.js.map → orchestrator-P7AEWVYR.js.map} +0 -0
- /package/dist/{pool-UTXQJJ5C.js.map → pool-2T6TVS2L.js.map} +0 -0
- /package/dist/{snapshot-K5VHETTK.js.map → snapshot-F5B2DD56.js.map} +0 -0
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
isEncrypted,
|
|
3
3
|
loadEncryptedText,
|
|
4
4
|
saveEncryptedText
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-AKIJ4LS2.js";
|
|
6
6
|
|
|
7
7
|
// src/vault/migrate.ts
|
|
8
8
|
import fs from "fs";
|
|
@@ -35,6 +35,16 @@ var MIGRATE_FILES = [
|
|
|
35
35
|
"seed_report.jsonl",
|
|
36
36
|
"preference.json"
|
|
37
37
|
];
|
|
38
|
+
var MIGRATE_ARCHIVE_RE = /^profile_journal\.archive-\d+\.jsonl$/;
|
|
39
|
+
var MIGRATE_WEEKLY_RE = /^weekly\/[^/\\]+\.json$/;
|
|
40
|
+
function isMigratablePath(rel) {
|
|
41
|
+
if (MIGRATE_FILES.includes(rel)) return true;
|
|
42
|
+
if (MIGRATE_ARCHIVE_RE.test(rel)) return true;
|
|
43
|
+
if (MIGRATE_WEEKLY_RE.test(rel)) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
var MIGRATE_MAX_FILES = 5e3;
|
|
47
|
+
var MIGRATE_MAX_BYTES = 64 * 1024 * 1024;
|
|
38
48
|
function collectWeeklyFiles(guard, paths) {
|
|
39
49
|
const dir = path.join(paths.dataDir, "weekly");
|
|
40
50
|
try {
|
|
@@ -90,6 +100,22 @@ function assertKdfN(v) {
|
|
|
90
100
|
}
|
|
91
101
|
return n;
|
|
92
102
|
}
|
|
103
|
+
function assertMigrateBudget(files) {
|
|
104
|
+
if (files.length > MIGRATE_MAX_FILES) {
|
|
105
|
+
throw new Error(`migrate: container lists ${files.length} files, over the ${MIGRATE_MAX_FILES} limit`);
|
|
106
|
+
}
|
|
107
|
+
let bytes = 0;
|
|
108
|
+
for (const f of files) {
|
|
109
|
+
const entry = f;
|
|
110
|
+
if (!entry || typeof entry.path !== "string" || typeof entry.content !== "string") {
|
|
111
|
+
throw new Error("migrate: container file entry is malformed");
|
|
112
|
+
}
|
|
113
|
+
bytes += Buffer.byteLength(entry.content, "utf8");
|
|
114
|
+
if (bytes > MIGRATE_MAX_BYTES) {
|
|
115
|
+
throw new Error(`migrate: container payload exceeds ${MIGRATE_MAX_BYTES} bytes`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
93
119
|
function encryptContainer(entries, passphrase, now = /* @__PURE__ */ new Date()) {
|
|
94
120
|
if (!passphrase) throw new Error("migrate: passphrase must not be empty");
|
|
95
121
|
const salt = randomBytes(16);
|
|
@@ -119,7 +145,11 @@ function decryptContainer(text, passphrase) {
|
|
|
119
145
|
const salt = Buffer.from(String(outer.kdf?.salt ?? ""), "hex");
|
|
120
146
|
const iv = Buffer.from(String(outer.iv ?? ""), "hex");
|
|
121
147
|
const tag = Buffer.from(String(outer.tag ?? ""), "hex");
|
|
122
|
-
const
|
|
148
|
+
const dataB64 = String(outer.data ?? "");
|
|
149
|
+
if (dataB64.length > MIGRATE_MAX_BYTES * 2) {
|
|
150
|
+
throw new Error(`migrate: container payload is too large (${dataB64.length} base64 chars)`);
|
|
151
|
+
}
|
|
152
|
+
const data = Buffer.from(dataB64, "base64");
|
|
123
153
|
const key = deriveKey(passphrase, salt, n);
|
|
124
154
|
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
125
155
|
decipher.setAuthTag(tag);
|
|
@@ -132,6 +162,7 @@ function decryptContainer(text, passphrase) {
|
|
|
132
162
|
const inner = JSON.parse(plain.toString("utf8"));
|
|
133
163
|
if (inner.version !== MIGRATE_VERSION) throw new Error(`migrate: unsupported container version ${inner.version}`);
|
|
134
164
|
if (!Array.isArray(inner.files)) throw new Error("migrate: container has no file list");
|
|
165
|
+
assertMigrateBudget(inner.files);
|
|
135
166
|
return { createdAt: inner.createdAt, files: inner.files };
|
|
136
167
|
}
|
|
137
168
|
function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
@@ -142,7 +173,7 @@ function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
|
142
173
|
const staged = [];
|
|
143
174
|
for (const f of files) {
|
|
144
175
|
const rel = String(f.path ?? "");
|
|
145
|
-
if (!rel || rel.includes("..") || path.isAbsolute(rel)) {
|
|
176
|
+
if (!rel || rel.includes("..") || path.isAbsolute(rel) || !isMigratablePath(rel)) {
|
|
146
177
|
skipped.push(rel);
|
|
147
178
|
continue;
|
|
148
179
|
}
|
|
@@ -168,6 +199,20 @@ function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
|
168
199
|
}
|
|
169
200
|
}
|
|
170
201
|
}
|
|
202
|
+
if (skipped.length > 0) {
|
|
203
|
+
for (const s of staged) {
|
|
204
|
+
try {
|
|
205
|
+
fs.rmSync(s.incoming, { force: true });
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
restored: [],
|
|
211
|
+
backedUp: [],
|
|
212
|
+
skipped,
|
|
213
|
+
error: `staging failed for ${skipped.length} file(s): ${skipped.join(", ")}; nothing was imported`
|
|
214
|
+
};
|
|
215
|
+
}
|
|
171
216
|
const committed = [];
|
|
172
217
|
try {
|
|
173
218
|
for (const s of staged) {
|
|
@@ -207,9 +252,12 @@ export {
|
|
|
207
252
|
MIGRATE_MAGIC,
|
|
208
253
|
MIGRATE_VERSION,
|
|
209
254
|
MIGRATE_FILES,
|
|
255
|
+
isMigratablePath,
|
|
256
|
+
MIGRATE_MAX_FILES,
|
|
257
|
+
MIGRATE_MAX_BYTES,
|
|
210
258
|
collectMigrationEntries,
|
|
211
259
|
encryptContainer,
|
|
212
260
|
decryptContainer,
|
|
213
261
|
applyMigrationEntries
|
|
214
262
|
};
|
|
215
|
-
//# sourceMappingURL=chunk-
|
|
263
|
+
//# sourceMappingURL=chunk-NCPKOWS7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vault/migrate.ts"],"sourcesContent":["// Cross-machine migration (v1.8.0). DPAPI binds the sensitive stores to one\n// machine + account, so a new machine cannot read them — this module packs\n// the memory files into ONE passphrase-encrypted container (AES-256-GCM,\n// scrypt-derived key, node:crypto only) that can travel safely, and restores\n// them on the other side re-encrypted with the LOCAL DPAPI.\n//\n// Restore is byte-identical: for each file we record whether it was DPAPI\n// encrypted on disk (isEncrypted magic check) and write it back through the\n// same path — no per-module write semantics to keep in sync. Existing files\n// are backed up before overwrite (rule 4).\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n createCipheriv,\n createDecipheriv,\n randomBytes,\n scryptSync,\n} from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport type { WorkspacePaths } from '../core/paths.js';\nimport { isEncrypted, loadEncryptedText, saveEncryptedText } from './vault.js';\n\nexport const MIGRATE_MAGIC = 'HBMIG1';\nexport const MIGRATE_VERSION = 1;\n/** scrypt cost for NEW containers (2^14 = Node's default, 16 MiB of work).\n * Reading is parameterised by the container's own kdf.N, so raising this\n * never orphans an existing export. */\nconst KDF_N = 163_84;\n/** Hard bounds for a container-supplied N: a hostile/broken container must not\n * be able to make the import allocate unbounded memory. */\nconst KDF_N_MIN = 1 << 12; // 2^12 = 4 MiB\nconst KDF_N_MAX = 1 << 16; // 2^16 = 64 MiB\n/** Node's scrypt maxmem defaults to 32 MiB, which N=2^15 already exceeds — a\n * legal container with a larger N would fail without an explicit cap. */\nconst SCRYPT_MAXMEM = 128 * 1024 * 1024;\nconst KEY_LEN = 32;\n\n/** Memory-relevant runtime files (dataDir-relative). Deliberately NOT here:\n * gate.json (machine-local counters + home session id — the new machine\n * builds its own) and screen snapshots (per-beat transients, not memory). */\nexport const MIGRATE_FILES = [\n 'profile.json',\n 'profile_snapshot.json',\n 'profile_inbox.jsonl',\n 'profile_journal.jsonl',\n 'profile_rhythm.json',\n 'sent.json',\n 'browse.json',\n 'seeds.jsonl',\n 'ledger.md',\n 'cursors.json',\n // v1.9.0: persona report log + topic preference stats\n 'seed_report.jsonl',\n 'preference.json',\n];\n\n/** HB-18: an import writes with the user's privileges inside data/, so the set\n * of names a container may touch is closed, not merely sanitised. Anything an\n * export can produce is here; everything else (gate.json, settings/, vault.key,\n * other modules' files) is refused. */\nconst MIGRATE_ARCHIVE_RE = /^profile_journal\\.archive-\\d+\\.jsonl$/;\nconst MIGRATE_WEEKLY_RE = /^weekly\\/[^/\\\\]+\\.json$/;\n\nexport function isMigratablePath(rel: string): boolean {\n if (MIGRATE_FILES.includes(rel)) return true;\n if (MIGRATE_ARCHIVE_RE.test(rel)) return true;\n if (MIGRATE_WEEKLY_RE.test(rel)) return true;\n return false;\n}\n\n/** HB-18: budget for a container-supplied payload. Generous next to a real\n * memory (a few MB), small next to what a hostile container would like. */\nexport const MIGRATE_MAX_FILES = 5_000;\nexport const MIGRATE_MAX_BYTES = 64 * 1024 * 1024;\n\nexport interface MigrateEntry {\n path: string; // dataDir-relative\n encrypted: boolean; // was DPAPI-encrypted on the source machine\n content: string;\n}\n\nexport interface MigrateProgress {\n packed: string[];\n missing: string[];\n}\n\nfunction collectWeeklyFiles(guard: PathGuard, paths: WorkspacePaths): string[] {\n const dir = path.join(paths.dataDir, 'weekly');\n try {\n return fs.readdirSync(guard.assert(dir))\n .filter((f) => f.endsWith('.json'))\n .map((f) => path.join('weekly', f).replace(/\\\\/g, '/'));\n } catch {\n return [];\n }\n}\n\n/** Journal archive shards (snapshot rotation output) travel with the memory. */\nfunction collectJournalArchives(guard: PathGuard, paths: WorkspacePaths): string[] {\n try {\n return fs.readdirSync(guard.assert(paths.dataDir))\n .filter((f) => f.startsWith('profile_journal.archive-') && f.endsWith('.jsonl'));\n } catch {\n return [];\n }\n}\n\n/** Read every migration file. A DPAPI file that fails to decrypt aborts the\n * export — a silent half-memory container would be worse than a loud error. */\nexport function collectMigrationEntries(guard: PathGuard, paths: WorkspacePaths): { entries: MigrateEntry[]; progress: MigrateProgress } {\n const entries: MigrateEntry[] = [];\n const packed: string[] = [];\n const missing: string[] = [];\n const rels = [...MIGRATE_FILES, ...collectJournalArchives(guard, paths), ...collectWeeklyFiles(guard, paths)];\n for (const rel of rels) {\n const abs = path.join(paths.dataDir, rel);\n let absCanon: string;\n try {\n absCanon = guard.assert(abs);\n } catch {\n missing.push(rel);\n continue;\n }\n if (!fs.existsSync(absCanon)) {\n missing.push(rel);\n continue;\n }\n const encrypted = isEncrypted(absCanon);\n const content = encrypted\n ? loadEncryptedText(guard, absCanon) ?? (() => { throw new Error(`cannot decrypt ${rel} on this machine`); })()\n : fs.readFileSync(absCanon, 'utf8');\n entries.push({ path: rel.replace(/\\\\/g, '/'), encrypted, content });\n packed.push(rel);\n }\n return { entries, progress: { packed, missing } };\n}\n\n/** Derive the key with the container's OWN cost parameter. `n` must already\n * have passed assertKdfN (export uses the current default). */\nfunction deriveKey(passphrase: string, salt: Buffer, n: number): Buffer {\n return scryptSync(passphrase, salt, KEY_LEN, { N: n, r: 8, p: 1, maxmem: SCRYPT_MAXMEM });\n}\n\n/** Validate the scrypt cost carried by a container. A missing/absurd N is a\n * BROKEN container — reported as such, never as \"wrong passphrase\" (before\n * 2026-10-06 the N in the container was ignored and the two were the same\n * error, so raising the cost would have orphaned every old export). */\nfunction assertKdfN(v: unknown): number {\n const n = Number(v);\n if (!Number.isInteger(n) || n <= 0 || (n & (n - 1)) !== 0) {\n throw new Error('migrate: container has invalid kdf parameters (N must be a power of two)');\n }\n if (n < KDF_N_MIN || n > KDF_N_MAX) {\n throw new Error(`migrate: container kdf N ${n} is outside the supported range [${KDF_N_MIN}, ${KDF_N_MAX}]`);\n }\n return n;\n}\n\n/** HB-18: a container does not get to decide how much work the import does. */\nfunction assertMigrateBudget(files: unknown[]): asserts files is MigrateEntry[] {\n if (files.length > MIGRATE_MAX_FILES) {\n throw new Error(`migrate: container lists ${files.length} files, over the ${MIGRATE_MAX_FILES} limit`);\n }\n let bytes = 0;\n for (const f of files) {\n const entry = f as { path?: unknown; content?: unknown } | null;\n if (!entry || typeof entry.path !== 'string' || typeof entry.content !== 'string') {\n throw new Error('migrate: container file entry is malformed');\n }\n bytes += Buffer.byteLength(entry.content, 'utf8');\n if (bytes > MIGRATE_MAX_BYTES) {\n throw new Error(`migrate: container payload exceeds ${MIGRATE_MAX_BYTES} bytes`);\n }\n }\n}\n\n/** Build the container file content. Everything sensitive lives inside the\n * ciphertext; the outer JSON carries only crypto parameters. */\nexport function encryptContainer(entries: MigrateEntry[], passphrase: string, now = new Date()): string {\n if (!passphrase) throw new Error('migrate: passphrase must not be empty');\n const salt = randomBytes(16);\n const iv = randomBytes(12);\n const key = deriveKey(passphrase, salt, KDF_N);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const plain = Buffer.from(JSON.stringify({ version: MIGRATE_VERSION, createdAt: now.toISOString(), files: entries }), 'utf8');\n const data = Buffer.concat([cipher.update(plain), cipher.final()]);\n const tag = cipher.getAuthTag();\n return JSON.stringify({\n magic: MIGRATE_MAGIC,\n kdf: { name: 'scrypt', salt: salt.toString('hex'), N: KDF_N },\n iv: iv.toString('hex'),\n tag: tag.toString('hex'),\n data: data.toString('base64'),\n }, null, 1) + '\\n';\n}\n\n/** Parse + decrypt a container. Throws (loudly) on a wrong passphrase or a\n * corrupted file — never returns partial data. */\nexport function decryptContainer(text: string, passphrase: string): { createdAt: string; files: MigrateEntry[] } {\n let outer: { magic?: string; kdf?: { salt?: string; N?: unknown }; iv?: string; tag?: string; data?: string };\n try {\n outer = JSON.parse(text);\n } catch {\n throw new Error('migrate: not a migration container (bad JSON)');\n }\n if (outer.magic !== MIGRATE_MAGIC) throw new Error('migrate: not a migration container (bad magic)');\n const n = assertKdfN(outer.kdf?.N);\n const salt = Buffer.from(String(outer.kdf?.salt ?? ''), 'hex');\n const iv = Buffer.from(String(outer.iv ?? ''), 'hex');\n const tag = Buffer.from(String(outer.tag ?? ''), 'hex');\n // HB-18: refuse an oversized payload before decoding it. base64 runs ~4/3 of\n // the ciphertext, so this cheap length check bounds the allocation below.\n const dataB64 = String(outer.data ?? '');\n if (dataB64.length > MIGRATE_MAX_BYTES * 2) {\n throw new Error(`migrate: container payload is too large (${dataB64.length} base64 chars)`);\n }\n const data = Buffer.from(dataB64, 'base64');\n const key = deriveKey(passphrase, salt, n);\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n let plain: Buffer;\n try {\n plain = Buffer.concat([decipher.update(data), decipher.final()]);\n } catch {\n throw new Error('migrate: wrong passphrase or corrupted container');\n }\n const inner = JSON.parse(plain.toString('utf8')) as { version: number; createdAt: string; files: MigrateEntry[] };\n if (inner.version !== MIGRATE_VERSION) throw new Error(`migrate: unsupported container version ${inner.version}`);\n if (!Array.isArray(inner.files)) throw new Error('migrate: container has no file list');\n assertMigrateBudget(inner.files);\n return { createdAt: inner.createdAt, files: inner.files };\n}\n\nexport interface MigrateImportResult {\n restored: string[];\n backedUp: string[];\n skipped: string[];\n /** H-21: files put back from their backups after a failed commit phase. */\n rolledBack?: string[];\n /** H-21: set when the commit phase failed and the import was rolled back. */\n error?: string;\n}\n\n/**\n * Restore entries into the local dataDir, re-encrypting DPAPI files with the\n * LOCAL machine key.\n *\n * H-21: this used to be a per-file \"back up, then overwrite\" loop with no\n * pre-flight and no rollback — a failure halfway through left a mixed memory\n * (a new `profile.json` next to a stale `profile_journal.jsonl`). Now it runs\n * in two phases: stage every entry next to its target (nothing visible changes\n * yet), then commit each one by rename. A commit failure rolls the\n * already-committed files back from their backups.\n *\n * HB-06 (2026-10-17): the phases are all-or-nothing at BOTH ends. Earlier a\n * staging failure only skipped that entry and the rest were still committed,\n * so one bad path was enough to leave a half-migrated memory. Now any skipped\n * entry aborts the whole import with nothing written; the reason is returned\n * in `error` alongside the offending names.\n */\nexport function applyMigrationEntries(guard: PathGuard, paths: WorkspacePaths, files: MigrateEntry[], now = Date.now()): MigrateImportResult {\n const restored: string[] = [];\n const backedUp: string[] = [];\n const skipped: string[] = [];\n const stamp = new Date(now).toISOString().replace(/[-:T]/g, '').slice(0, 14);\n const staged: Array<{ rel: string; abs: string; incoming: string }> = [];\n for (const f of files) {\n const rel = String(f.path ?? '');\n // String-level rejection BEFORE the path is resolved: the guard's realpath\n // check already refuses escapes, but an import must not depend on another\n // module staying correct. HB-18: and the name must be one an export can\n // produce - a container may not reach settings/, gate.json or vault.key.\n if (!rel || rel.includes('..') || path.isAbsolute(rel) || !isMigratablePath(rel)) {\n skipped.push(rel);\n continue;\n }\n const abs = path.join(paths.dataDir, rel);\n let absCanon: string;\n try {\n absCanon = guard.assert(abs);\n } catch {\n skipped.push(rel); // path refused by the guard — never write it\n continue;\n }\n const incoming = `${absCanon}.incoming-${stamp}`;\n try {\n fs.mkdirSync(path.dirname(absCanon), { recursive: true });\n if (f.encrypted) saveEncryptedText(guard, incoming, f.content);\n else fs.writeFileSync(incoming, f.content, 'utf8');\n staged.push({ rel, abs: absCanon, incoming });\n } catch {\n skipped.push(rel);\n try {\n fs.rmSync(incoming, { force: true });\n } catch {\n /* nothing staged */\n }\n }\n }\n // HB-06: staging is the point of no return. A single entry that could not be\n // staged used to be skipped while the rest were committed, which left a\n // half-migrated memory (a new profile.json next to a stale journal). Any skip\n // now aborts the import with nothing written; the reason travels in `error`.\n if (skipped.length > 0) {\n for (const s of staged) {\n try {\n fs.rmSync(s.incoming, { force: true });\n } catch {\n /* nothing staged */\n }\n }\n return {\n restored: [],\n backedUp: [],\n skipped,\n error: `staging failed for ${skipped.length} file(s): ${skipped.join(', ')}; nothing was imported`,\n };\n }\n const committed: Array<{ rel: string; abs: string; backup: string | null }> = [];\n try {\n for (const s of staged) {\n let backup: string | null = null;\n if (fs.existsSync(s.abs)) {\n backup = `${s.abs}.bak-migrate-${stamp}`;\n fs.copyFileSync(s.abs, backup);\n backedUp.push(s.rel);\n }\n fs.renameSync(s.incoming, s.abs);\n committed.push({ rel: s.rel, abs: s.abs, backup });\n restored.push(s.rel);\n }\n return { restored, backedUp, skipped };\n } catch (e) {\n // Roll back so the dataDir never holds a half-migrated mix; staged files\n // that were never committed are simply dropped.\n const rolledBack: string[] = [];\n for (const c of committed.reverse()) {\n try {\n if (c.backup) fs.renameSync(c.backup, c.abs);\n else fs.rmSync(c.abs, { force: true });\n rolledBack.push(c.rel);\n } catch {\n /* best effort — the backup file itself is still on disk */\n }\n }\n for (const s of staged) {\n if (committed.some((c) => c.abs === s.abs)) continue;\n try {\n fs.rmSync(s.incoming, { force: true });\n } catch {\n /* already gone */\n }\n }\n return { restored: [], backedUp, skipped, rolledBack, error: String(e) };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
withFileLock
|
|
3
|
-
} from "./chunk-IV2ZWQA3.js";
|
|
4
1
|
import {
|
|
5
2
|
loadEncryptedText,
|
|
6
|
-
saveEncryptedText
|
|
7
|
-
|
|
3
|
+
saveEncryptedText,
|
|
4
|
+
withFileLock
|
|
5
|
+
} from "./chunk-AKIJ4LS2.js";
|
|
8
6
|
|
|
9
7
|
// src/seeds/pool.ts
|
|
10
8
|
import path from "path";
|
|
@@ -314,4 +312,4 @@ export {
|
|
|
314
312
|
restoreSeed,
|
|
315
313
|
deleteSeed
|
|
316
314
|
};
|
|
317
|
-
//# sourceMappingURL=chunk-
|
|
315
|
+
//# sourceMappingURL=chunk-TVX4HV4Y.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/seeds/pool.ts","../src/seeds/types.ts"],"sourcesContent":["// Material pool engine (design doc §4). All four eviction rules are\n// deterministic code - no LLM involvement (§4.2). Storage: data/seeds.jsonl,\n// one seed per JSON line, DPAPI-encrypted at rest (D14).\n\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\nimport { withFileLock } from '../core/file-lock.js';\nimport type { Policy } from '../config/schema.js';\nimport {\n SEED_SOURCE_DEFAULT_CONFIDENCE,\n emptySeedDb,\n type Seed,\n type SeedCategory,\n type SeedDb,\n type SeedRetireReason,\n type SeedSource,\n type SeedTag,\n} from './types.js';\n\nconst DAY_MS = 86_400_000;\n\n// ── lifecycle audit (2026-10-06 / v1.9.0) ───────────────────────────────\n// The pool used to mutate in silence: a seed could appear, be evicted for\n// capacity, or be dropped from the archive with nothing in the log. Every\n// mutator now takes an optional sink and reports what it did. IDs and\n// metadata ONLY — heartbeat.jsonl is plaintext by charter and must never\n// carry conversation text; the seed text itself stays in the DPAPI pool.\nexport type SeedAuditFn = (entry: Record<string, unknown>) => void;\n\nconst NO_AUDIT: SeedAuditFn = () => {};\n\n/** Audit must never break the pool (or the beat that called it). */\nfunction emit(audit: SeedAuditFn, entry: Record<string, unknown>): void {\n try {\n audit(entry);\n } catch {\n /* audit is diagnostic; a failed append is not a pool failure */\n }\n}\n\n// Per-category pool caps (2026-09-18 spec ⑤): topic stock vs conversation-grown\n// chat material, decoupled from `source`; sum == the global maxActive default.\nexport const SEED_CATEGORY_CAPS: Record<SeedCategory, number> = { topic: 16, chat: 14 };\n\nconst CATEGORY_VALUES: readonly string[] = ['topic', 'chat'];\n\n/** Lenient category coercion for on-disk rows: anything missing/unknown (all\n * pre-v1.5 seeds) counts as topic stock. */\nexport function normalizeCategory(v: unknown): SeedCategory {\n return typeof v === 'string' && (CATEGORY_VALUES as readonly string[]).includes(v)\n ? (v as SeedCategory)\n : 'topic';\n}\n\nexport interface AddSeedInput {\n text: string;\n topic?: string;\n tag?: SeedTag;\n source?: SeedSource;\n /** Loop role (spec ⑤); defaults from source: chat->chat, everything else->topic. */\n category?: SeedCategory;\n confidence?: number;\n}\n\nexport interface AddSeedResult {\n kind: 'added' | 'merged' | 'duplicate';\n seed: Seed;\n evicted?: Seed;\n}\n\nexport interface GcReport {\n consumed: number;\n expired: number;\n coldBench: number;\n activeAfter: number;\n /** v1.9.0: archived rows dropped to honor seeds.archiveCap (oldest first). */\n archiveTrimmed: number;\n}\n\nexport const TTL_KEYS: readonly SeedTag[] = ['news', 'fandom', 'scene', 'promise'];\n\nexport function normalizeTag(tag: string | undefined): SeedTag {\n if (tag && (TTL_KEYS as readonly string[]).includes(tag)) return tag as SeedTag;\n return 'scene';\n}\n\nfunction parseIso(v: string): number {\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : 0;\n}\n\n// ── storage ─────────────────────────────────────────────────────────────\n\nexport function seedsFilePath(dataDir: string): string {\n return path.join(dataDir, 'seeds.jsonl');\n}\n\nexport function loadPool(guard: PathGuard, file: string): SeedDb {\n const raw = loadEncryptedText(guard, file);\n if (raw === null) return emptySeedDb();\n const db = emptySeedDb();\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const obj = JSON.parse(trimmed) as Seed;\n if (typeof obj.id === 'string' && obj.id.startsWith('s')) {\n // spec ⑤ loadPool tolerance: only the id is load-bearing; a missing or\n // unknown category (every pre-v1.5 row) falls back to topic stock.\n obj.category = normalizeCategory(obj.category);\n db.seeds.push(obj);\n const n = Number(obj.id.slice(1));\n if (Number.isFinite(n) && n > db.seq) db.seq = n;\n }\n } catch {\n // corrupt line: skip (pool is a cache; journal-level audit lives elsewhere)\n }\n }\n return db;\n}\n\nexport function savePool(guard: PathGuard, file: string, db: SeedDb): void {\n const body = db.seeds.map((s) => JSON.stringify(s)).join('\\n');\n saveEncryptedText(guard, file, body ? body + '\\n' : '');\n}\n\n/**\n * H-20 (minimal tier): every pool writer goes through here, so the load happens\n * INSIDE the cross-process lock and cannot be based on a read that went stale\n * while it was being mutated — the CLI is a second process sharing this file,\n * and an interleaved write used to be silently overwritten. The ciphertext is a\n * whole-file rewrite, so append/merge does not apply here; the append-only pool\n * with compaction is v2.0 work (ledger #8649f9).\n */\nfunction update<T>(guard: PathGuard, file: string, fn: (db: SeedDb) => T): T {\n return withFileLock(file, () => fn(loadPool(guard, file)));\n}\n\n// ── queries ─────────────────────────────────────────────────────────────\n\nexport const activeSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'active');\nexport const archivedSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'archived');\n\n// ── eviction scoring (rule 4) ───────────────────────────────────────────\n\n/**\n * Comprehensive score (§4.2): freshness x0.4 + unused x0.3 + confidence x0.3.\n * Higher = keep. Lowest-scoring active is evicted when the pool is full.\n */\nexport function evictionScore(seed: Seed, policy: Policy, now: number): number {\n const ttlDays = policy.seeds.ttlDays[seed.tag] || 14;\n const daysStale = Math.max(0, (now - parseIso(seed.lastEvidenceAt)) / DAY_MS);\n const freshness = Math.max(0, 1 - daysStale / ttlDays);\n const unused = seed.used === 0 ? 1 : 1 / seed.used;\n const w = policy.seeds.scoreWeights;\n return freshness * w.freshness + unused * w.unused + seed.confidence * w.confidence;\n}\n\n/** Pick the eviction victim: unprotected actives first (§4.2 加权保护); with\n * `category`, only that category's actives compete (spec ⑤ per-category caps). */\nexport function pickEvictionVictim(db: SeedDb, policy: Policy, now: number, category?: SeedCategory): Seed | null {\n const actives = activeSeeds(db).filter((s) => category === undefined || normalizeCategory(s.category) === category);\n if (actives.length === 0) return null;\n const unprotected = actives.filter((s) => !s.protected);\n const candidates = unprotected.length > 0 ? unprotected : actives;\n let worst: Seed | null = null;\n let worstScore = Number.POSITIVE_INFINITY;\n for (const s of candidates) {\n const score = evictionScore(s, policy, now);\n if (score < worstScore) {\n worstScore = score;\n worst = s;\n }\n }\n return worst;\n}\n\nfunction archiveSeed(seed: Seed, reason: SeedRetireReason, now: number): void {\n seed.status = 'archived';\n seed.retireReason = reason;\n seed.retiredAt = new Date(now).toISOString();\n}\n\n/** Spec ④: chat-grown material is consumed ONCE (conversation freshness);\n * every other category follows the policy-wide retireAfterUsed. */\nfunction retireLimit(s: Seed, policy: Policy): number {\n return normalizeCategory(s.category) === 'chat' ? 1 : policy.seeds.retireAfterUsed;\n}\n\n/**\n * Rule 1 (§4.2): used up to its limit AND no evidence newer than the PREVIOUS\n * use. `prevUsedAt` MUST be read before bumping `lastUsedAt` — comparing the\n * just-written value against `lastEvidenceAt` is vacuously true (H-22), which\n * retired seeds that had received fresh evidence in the same beat. Both\n * callers (gc and surface) share this, so the rule cannot drift again.\n *\n * `prevUsedAt === 0` means \"never used before\": there is no earlier use for\n * the evidence to be newer THAN, so the exemption cannot apply — a one-shot\n * (chat) seed still retires on its first surfacing (spec ④), which is the\n * contract, not a regression.\n */\nfunction isConsumed(s: Seed, policy: Policy, prevUsedAt: number, sinceEvidence: number): boolean {\n if (s.used < retireLimit(s, policy)) return false;\n return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);\n}\n\n// ── operations ──────────────────────────────────────────────────────────\n\nexport function addSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): AddSeedResult {\n // H-20: the read-modify-write below runs under the cross-process pool lock\n // (same guard as `update`, kept as a named body to avoid re-indenting it).\n return withFileLock(file, () => addSeedLocked(guard, file, policy, input, now, audit));\n}\n\nfunction addSeedLocked(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now: number,\n audit: SeedAuditFn,\n): AddSeedResult {\n const db = loadPool(guard, file);\n const text = input.text.trim();\n if (!text) throw new Error('seed text must not be empty');\n const tag = normalizeTag(input.tag);\n const source: SeedSource = input.source ?? 'chat';\n const category = normalizeCategory(input.category ?? (source === 'chat' ? 'chat' : 'topic'));\n const confidence = input.confidence ?? SEED_SOURCE_DEFAULT_CONFIDENCE[source];\n\n // Exact-text dedup against actives.\n const dup = activeSeeds(db).find((s) => s.text === text);\n if (dup) return { kind: 'duplicate', seed: dup };\n\n const nowIso = new Date(now).toISOString();\n const ttlMs = (policy.seeds.ttlDays[tag] || 14) * DAY_MS;\n\n // Same-topic merge (§4.3): collapse all same-topic actives + the new item.\n const sameTopic = activeSeeds(db).filter((s) => s.topic === (input.topic ?? text.slice(0, 24)));\n let seed: Seed;\n if (sameTopic.length > 0) {\n const kept = sameTopic[0]!;\n kept.text = text; // brief takes the latest\n kept.tag = tag;\n kept.source = source;\n kept.category = normalizeCategory(input.category ?? kept.category ?? category);\n kept.confidence = Math.max(kept.confidence, confidence);\n kept.used = sameTopic.reduce((acc, s) => acc + s.used, 0); // used accumulates\n kept.lastEvidenceAt = [kept.lastEvidenceAt, nowIso, ...sameTopic.map((s) => s.lastEvidenceAt)]\n .reduce((a, b) => (parseIso(b) > parseIso(a) ? b : a));\n kept.expiresAt = new Date(Math.max(parseIso(kept.expiresAt), now + ttlMs)).toISOString();\n kept.protected = kept.protected || source === 'hand' || (source === 'profile' && confidence >= 0.7);\n // remove the other same-topic actives (merged into kept)\n for (const extra of sameTopic.slice(1)) {\n extra.status = 'archived';\n extra.retireReason = 'completed';\n extra.retiredAt = nowIso;\n emit(audit, { event: 'seed_retired', id: extra.id, reason: 'completed', via: 'merge', into: kept.id });\n }\n seed = kept;\n // capacity still applies after growth check below if kept is somehow over cap\n if (activeSeeds(db).length > policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, normalizeCategory(kept.category));\n if (victim && victim.id !== kept.id) {\n archiveSeed(victim, 'pool_cap', now);\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n emit(audit, { event: 'seed_retired', id: victim.id, reason: 'pool_cap', via: 'merge', into: kept.id });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept, evicted: victim };\n }\n }\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept };\n }\n\n // Capacity first (rules 4 + spec ⑤): per-category cap evicts within its own\n // pool; the global cap evicts in the incoming seed's category (a full topic\n // stock must not push chat material out and vice versa).\n let evicted: Seed | undefined;\n const catCount = activeSeeds(db).filter((s) => normalizeCategory(s.category) === category).length;\n if (catCount >= SEED_CATEGORY_CAPS[category]) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = victim;\n }\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = evicted ?? victim;\n }\n }\n\n db.seq += 1;\n seed = {\n id: `s${db.seq}`,\n text,\n topic: input.topic ?? text.slice(0, 24),\n tag,\n source,\n category,\n confidence,\n protected: source === 'hand' || (source === 'profile' && confidence >= 0.7),\n used: 0,\n bornAt: nowIso,\n expiresAt: new Date(now + ttlMs).toISOString(),\n lastUsedAt: null,\n lastEvidenceAt: nowIso,\n status: 'active',\n };\n db.seeds.push(seed);\n emit(audit, { event: 'seed_added', id: seed.id, kind: 'added', tag: seed.tag, source: seed.source, category: seed.category, confidence: seed.confidence });\n if (evicted) emit(audit, { event: 'seed_retired', id: evicted.id, reason: 'pool_cap', via: 'add', into: seed.id });\n savePool(guard, file, db);\n return { kind: 'added', seed, evicted };\n}\n\n/**\n * Deterministic gc (rules 1-3 of §4.2). Rule 4 (pool cap) fires on add only.\n * v1.9.0: the archive area is ALSO capped (seeds.archiveCap) — the archive is\n * a forensic trail, not a hoard; trim oldest-retired first.\n */\nexport function gcPool(guard: PathGuard, file: string, policy: Policy, now = Date.now(), audit: SeedAuditFn = NO_AUDIT): GcReport {\n return update(guard, file, (db) => {\n const report: GcReport = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };\n for (const s of activeSeeds(db)) {\n const ageMs = now - parseIso(s.bornAt);\n const sinceEvidence = parseIso(s.lastEvidenceAt);\n const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n if (isConsumed(s, policy, sinceUsed, sinceEvidence)) {\n archiveSeed(s, 'consumed', now);\n report.consumed += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'gc', used: s.used });\n } else if (now > parseIso(s.expiresAt)) {\n archiveSeed(s, 'expired', now);\n report.expired += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'expired', via: 'gc', used: s.used });\n } else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {\n archiveSeed(s, 'cold_bench', now);\n report.coldBench += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'cold_bench', via: 'gc', used: s.used });\n }\n }\n // archive cap: drop the oldest-retired rows beyond the cap (retiredAt may be\n // missing on pre-v1.5 rows — parseIso('') = 0 sorts them oldest).\n const archived = db.seeds.filter((s) => s.status === 'archived');\n if (archived.length > policy.seeds.archiveCap) {\n const drop = [...archived]\n .sort((a, b) => parseIso(a.retiredAt ?? '') - parseIso(b.retiredAt ?? ''))\n .slice(0, archived.length - policy.seeds.archiveCap);\n const dropped = new Set(drop.map((s) => s.id));\n db.seeds = db.seeds.filter((s) => !dropped.has(s.id));\n report.archiveTrimmed = drop.length;\n // These rows are GONE (not archived — deleted): the log is the only trace\n // left, so name them. IDs only, and cap the list in case a first GC trims\n // a legacy archive wholesale.\n emit(audit, { event: 'seed_archive_trimmed', count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });\n }\n report.activeAfter = activeSeeds(db).length;\n savePool(guard, file, db);\n return report;\n });\n}\n\n/** The seed surfaced in a real expression: count + maybe retire (rule 1). */\nexport function surfaceSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n // H-22: snapshot the PREVIOUS use time before bumping it — the retirement\n // check must compare against the last use, not against this one.\n const prevUsedAt = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n s.used += 1;\n s.lastUsedAt = new Date(now).toISOString();\n if (isConsumed(s, policy, prevUsedAt, parseIso(s.lastEvidenceAt))) {\n archiveSeed(s, 'consumed', now);\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'surface', used: s.used });\n }\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Deliberate retirement (item completed / no longer relevant). */\nexport function archiveSeedById(\n guard: PathGuard,\n file: string,\n id: string,\n reason: SeedRetireReason = 'completed',\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n archiveSeed(s, reason, now);\n emit(audit, { event: 'seed_retired', id: s.id, reason, via: 'manual' });\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Restore an archived seed to the active pool (UI operation; resets TTL). */\nexport function restoreSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): { ok: true; seed: Seed } | { ok: false; reason: string } {\n return update<{ ok: true; seed: Seed } | { ok: false; reason: string }>(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'archived');\n if (!s) return { ok: false, reason: 'archived seed not found' };\n // H-23: a restore must satisfy the same two caps an add does — the global\n // one AND the per-category one. The add path evicts a neighbour instead, but\n // restoring a memory is a user action: refuse it rather than silently\n // pushing someone else's material out.\n const category = normalizeCategory(s.category);\n const catCap = SEED_CATEGORY_CAPS[category];\n const inCategory = activeSeeds(db).filter((x) => normalizeCategory(x.category) === category).length;\n if (inCategory >= catCap) {\n return { ok: false, reason: `category full (${category}: ${catCap}); archive something in it first` };\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };\n }\n s.status = 'active';\n s.retireReason = undefined;\n s.retiredAt = undefined;\n // H-23: reset bornAt as well — cold-bench eviction (used === 0 && age >=\n // coldBenchDays) keys off bornAt, so a restored seed used to be re-archived\n // by the very next maintenance beat (\"restored, then gone one beat later\").\n s.bornAt = new Date(now).toISOString();\n s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();\n s.lastEvidenceAt = new Date(now).toISOString();\n emit(audit, { event: 'seed_restored', id: s.id });\n savePool(guard, file, db);\n return { ok: true, seed: s };\n });\n}\n\n/** Hard delete (UI explicit action with confirm; the caller audits it). */\nexport function deleteSeed(guard: PathGuard, file: string, id: string, audit: SeedAuditFn = NO_AUDIT): boolean {\n return update(guard, file, (db) => {\n const before = db.seeds.length;\n db.seeds = db.seeds.filter((x) => x.id !== id);\n if (db.seeds.length === before) return false;\n emit(audit, { event: 'seed_deleted', id });\n savePool(guard, file, db);\n return true;\n });\n}\n","// Material pool (\"素材池\") types. Charter (axiom 3): this pool is a CACHE,\n// not memory - volatile, evictable, burnable; long-term memory only absorbs\n// conversation-validated content. Everything here may be destroyed.\n\nexport type SeedTag = 'news' | 'fandom' | 'scene' | 'promise';\nexport type SeedSource = 'chat' | 'screen' | 'browse' | 'hand' | 'profile';\nexport type SeedCategory = 'topic' | 'chat';\nexport type SeedRetireReason = 'consumed' | 'expired' | 'cold_bench' | 'pool_cap' | 'completed';\n\nexport interface Seed {\n id: string;\n /** Short brief shown to the reflection prompt. */\n text: string;\n /** Merge key (§4.3): same-topic actives collapse into one. */\n topic: string;\n tag: SeedTag;\n source: SeedSource;\n /** Loop role, decoupled from source (2026-09-18 spec ⑤): 'topic' = wander/\n * hand/profile stock (random pick), 'chat' = conversation-grown, consumed\n * once. Absent on pre-v1.5 rows; loadPool defaults them to 'topic'. */\n category?: SeedCategory;\n /** 0..1 trust level; handwriting and high-confidence profile sources gain eviction protection. */\n confidence: number;\n /** Eviction protection (§4.2 pool-cap rule): handwritten / high-confidence profile sources. */\n protected: boolean;\n /** Times this seed surfaced in an actual expression. */\n used: number;\n bornAt: string;\n /** bornAt + ttlDays[tag]; archived when passed (rule 2). */\n expiresAt: string;\n lastUsedAt: string | null;\n /** Fresh-evidence watermark: rule 1 spares seeds with evidence newer than last use. */\n lastEvidenceAt: string;\n status: 'active' | 'archived';\n retireReason?: SeedRetireReason;\n retiredAt?: string;\n}\n\nexport interface SeedDb {\n seq: number;\n seeds: Seed[];\n}\n\nexport function emptySeedDb(): SeedDb {\n return { seq: 0, seeds: [] };\n}\n\nexport const SEED_SOURCE_DEFAULT_CONFIDENCE: Record<SeedSource, number> = {\n hand: 1.0,\n profile: 0.7,\n chat: 0.6,\n screen: 0.4,\n browse: 0.4,\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/seeds/pool.ts","../src/seeds/types.ts"],"sourcesContent":["// Material pool engine (design doc §4). All four eviction rules are\n// deterministic code - no LLM involvement (§4.2). Storage: data/seeds.jsonl,\n// one seed per JSON line, DPAPI-encrypted at rest (D14).\n\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\nimport { withFileLock } from '../core/file-lock.js';\nimport type { Policy } from '../config/schema.js';\nimport {\n SEED_SOURCE_DEFAULT_CONFIDENCE,\n emptySeedDb,\n type Seed,\n type SeedCategory,\n type SeedDb,\n type SeedRetireReason,\n type SeedSource,\n type SeedTag,\n} from './types.js';\n\nconst DAY_MS = 86_400_000;\n\n// ── lifecycle audit (2026-10-06 / v1.9.0) ───────────────────────────────\n// The pool used to mutate in silence: a seed could appear, be evicted for\n// capacity, or be dropped from the archive with nothing in the log. Every\n// mutator now takes an optional sink and reports what it did. IDs and\n// metadata ONLY — heartbeat.jsonl is plaintext by charter and must never\n// carry conversation text; the seed text itself stays in the DPAPI pool.\nexport type SeedAuditFn = (entry: Record<string, unknown>) => void;\n\nconst NO_AUDIT: SeedAuditFn = () => {};\n\n/** Audit must never break the pool (or the beat that called it). */\nfunction emit(audit: SeedAuditFn, entry: Record<string, unknown>): void {\n try {\n audit(entry);\n } catch {\n /* audit is diagnostic; a failed append is not a pool failure */\n }\n}\n\n// Per-category pool caps (2026-09-18 spec ⑤): topic stock vs conversation-grown\n// chat material, decoupled from `source`; sum == the global maxActive default.\nexport const SEED_CATEGORY_CAPS: Record<SeedCategory, number> = { topic: 16, chat: 14 };\n\nconst CATEGORY_VALUES: readonly string[] = ['topic', 'chat'];\n\n/** Lenient category coercion for on-disk rows: anything missing/unknown (all\n * pre-v1.5 seeds) counts as topic stock. */\nexport function normalizeCategory(v: unknown): SeedCategory {\n return typeof v === 'string' && (CATEGORY_VALUES as readonly string[]).includes(v)\n ? (v as SeedCategory)\n : 'topic';\n}\n\nexport interface AddSeedInput {\n text: string;\n topic?: string;\n tag?: SeedTag;\n source?: SeedSource;\n /** Loop role (spec ⑤); defaults from source: chat->chat, everything else->topic. */\n category?: SeedCategory;\n confidence?: number;\n}\n\nexport interface AddSeedResult {\n kind: 'added' | 'merged' | 'duplicate';\n seed: Seed;\n evicted?: Seed;\n}\n\nexport interface GcReport {\n consumed: number;\n expired: number;\n coldBench: number;\n activeAfter: number;\n /** v1.9.0: archived rows dropped to honor seeds.archiveCap (oldest first). */\n archiveTrimmed: number;\n}\n\nexport const TTL_KEYS: readonly SeedTag[] = ['news', 'fandom', 'scene', 'promise'];\n\nexport function normalizeTag(tag: string | undefined): SeedTag {\n if (tag && (TTL_KEYS as readonly string[]).includes(tag)) return tag as SeedTag;\n return 'scene';\n}\n\nfunction parseIso(v: string): number {\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : 0;\n}\n\n// ── storage ─────────────────────────────────────────────────────────────\n\nexport function seedsFilePath(dataDir: string): string {\n return path.join(dataDir, 'seeds.jsonl');\n}\n\nexport function loadPool(guard: PathGuard, file: string): SeedDb {\n const raw = loadEncryptedText(guard, file);\n if (raw === null) return emptySeedDb();\n const db = emptySeedDb();\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const obj = JSON.parse(trimmed) as Seed;\n if (typeof obj.id === 'string' && obj.id.startsWith('s')) {\n // spec ⑤ loadPool tolerance: only the id is load-bearing; a missing or\n // unknown category (every pre-v1.5 row) falls back to topic stock.\n obj.category = normalizeCategory(obj.category);\n db.seeds.push(obj);\n const n = Number(obj.id.slice(1));\n if (Number.isFinite(n) && n > db.seq) db.seq = n;\n }\n } catch {\n // corrupt line: skip (pool is a cache; journal-level audit lives elsewhere)\n }\n }\n return db;\n}\n\nexport function savePool(guard: PathGuard, file: string, db: SeedDb): void {\n const body = db.seeds.map((s) => JSON.stringify(s)).join('\\n');\n saveEncryptedText(guard, file, body ? body + '\\n' : '');\n}\n\n/**\n * H-20 (minimal tier): every pool writer goes through here, so the load happens\n * INSIDE the cross-process lock and cannot be based on a read that went stale\n * while it was being mutated — the CLI is a second process sharing this file,\n * and an interleaved write used to be silently overwritten. The ciphertext is a\n * whole-file rewrite, so append/merge does not apply here; the append-only pool\n * with compaction is v2.0 work (ledger #8649f9).\n */\nfunction update<T>(guard: PathGuard, file: string, fn: (db: SeedDb) => T): T {\n return withFileLock(file, () => fn(loadPool(guard, file)));\n}\n\n// ── queries ─────────────────────────────────────────────────────────────\n\nexport const activeSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'active');\nexport const archivedSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'archived');\n\n// ── eviction scoring (rule 4) ───────────────────────────────────────────\n\n/**\n * Comprehensive score (§4.2): freshness x0.4 + unused x0.3 + confidence x0.3.\n * Higher = keep. Lowest-scoring active is evicted when the pool is full.\n */\nexport function evictionScore(seed: Seed, policy: Policy, now: number): number {\n const ttlDays = policy.seeds.ttlDays[seed.tag] || 14;\n const daysStale = Math.max(0, (now - parseIso(seed.lastEvidenceAt)) / DAY_MS);\n const freshness = Math.max(0, 1 - daysStale / ttlDays);\n const unused = seed.used === 0 ? 1 : 1 / seed.used;\n const w = policy.seeds.scoreWeights;\n return freshness * w.freshness + unused * w.unused + seed.confidence * w.confidence;\n}\n\n/** Pick the eviction victim: unprotected actives first (§4.2 加权保护); with\n * `category`, only that category's actives compete (spec ⑤ per-category caps). */\nexport function pickEvictionVictim(db: SeedDb, policy: Policy, now: number, category?: SeedCategory): Seed | null {\n const actives = activeSeeds(db).filter((s) => category === undefined || normalizeCategory(s.category) === category);\n if (actives.length === 0) return null;\n const unprotected = actives.filter((s) => !s.protected);\n const candidates = unprotected.length > 0 ? unprotected : actives;\n let worst: Seed | null = null;\n let worstScore = Number.POSITIVE_INFINITY;\n for (const s of candidates) {\n const score = evictionScore(s, policy, now);\n if (score < worstScore) {\n worstScore = score;\n worst = s;\n }\n }\n return worst;\n}\n\nfunction archiveSeed(seed: Seed, reason: SeedRetireReason, now: number): void {\n seed.status = 'archived';\n seed.retireReason = reason;\n seed.retiredAt = new Date(now).toISOString();\n}\n\n/** Spec ④: chat-grown material is consumed ONCE (conversation freshness);\n * every other category follows the policy-wide retireAfterUsed. */\nfunction retireLimit(s: Seed, policy: Policy): number {\n return normalizeCategory(s.category) === 'chat' ? 1 : policy.seeds.retireAfterUsed;\n}\n\n/**\n * Rule 1 (§4.2): used up to its limit AND no evidence newer than the PREVIOUS\n * use. `prevUsedAt` MUST be read before bumping `lastUsedAt` — comparing the\n * just-written value against `lastEvidenceAt` is vacuously true (H-22), which\n * retired seeds that had received fresh evidence in the same beat. Both\n * callers (gc and surface) share this, so the rule cannot drift again.\n *\n * `prevUsedAt === 0` means \"never used before\": there is no earlier use for\n * the evidence to be newer THAN, so the exemption cannot apply — a one-shot\n * (chat) seed still retires on its first surfacing (spec ④), which is the\n * contract, not a regression.\n */\nfunction isConsumed(s: Seed, policy: Policy, prevUsedAt: number, sinceEvidence: number): boolean {\n if (s.used < retireLimit(s, policy)) return false;\n return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);\n}\n\n// ── operations ──────────────────────────────────────────────────────────\n\nexport function addSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): AddSeedResult {\n // H-20: the read-modify-write below runs under the cross-process pool lock\n // (same guard as `update`, kept as a named body to avoid re-indenting it).\n return withFileLock(file, () => addSeedLocked(guard, file, policy, input, now, audit));\n}\n\nfunction addSeedLocked(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now: number,\n audit: SeedAuditFn,\n): AddSeedResult {\n const db = loadPool(guard, file);\n const text = input.text.trim();\n if (!text) throw new Error('seed text must not be empty');\n const tag = normalizeTag(input.tag);\n const source: SeedSource = input.source ?? 'chat';\n const category = normalizeCategory(input.category ?? (source === 'chat' ? 'chat' : 'topic'));\n const confidence = input.confidence ?? SEED_SOURCE_DEFAULT_CONFIDENCE[source];\n\n // Exact-text dedup against actives.\n const dup = activeSeeds(db).find((s) => s.text === text);\n if (dup) return { kind: 'duplicate', seed: dup };\n\n const nowIso = new Date(now).toISOString();\n const ttlMs = (policy.seeds.ttlDays[tag] || 14) * DAY_MS;\n\n // Same-topic merge (§4.3): collapse all same-topic actives + the new item.\n const sameTopic = activeSeeds(db).filter((s) => s.topic === (input.topic ?? text.slice(0, 24)));\n let seed: Seed;\n if (sameTopic.length > 0) {\n const kept = sameTopic[0]!;\n kept.text = text; // brief takes the latest\n kept.tag = tag;\n kept.source = source;\n kept.category = normalizeCategory(input.category ?? kept.category ?? category);\n kept.confidence = Math.max(kept.confidence, confidence);\n kept.used = sameTopic.reduce((acc, s) => acc + s.used, 0); // used accumulates\n kept.lastEvidenceAt = [kept.lastEvidenceAt, nowIso, ...sameTopic.map((s) => s.lastEvidenceAt)]\n .reduce((a, b) => (parseIso(b) > parseIso(a) ? b : a));\n kept.expiresAt = new Date(Math.max(parseIso(kept.expiresAt), now + ttlMs)).toISOString();\n kept.protected = kept.protected || source === 'hand' || (source === 'profile' && confidence >= 0.7);\n // remove the other same-topic actives (merged into kept)\n for (const extra of sameTopic.slice(1)) {\n extra.status = 'archived';\n extra.retireReason = 'completed';\n extra.retiredAt = nowIso;\n emit(audit, { event: 'seed_retired', id: extra.id, reason: 'completed', via: 'merge', into: kept.id });\n }\n seed = kept;\n // capacity still applies after growth check below if kept is somehow over cap\n if (activeSeeds(db).length > policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, normalizeCategory(kept.category));\n if (victim && victim.id !== kept.id) {\n archiveSeed(victim, 'pool_cap', now);\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n emit(audit, { event: 'seed_retired', id: victim.id, reason: 'pool_cap', via: 'merge', into: kept.id });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept, evicted: victim };\n }\n }\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept };\n }\n\n // Capacity first (rules 4 + spec ⑤): per-category cap evicts within its own\n // pool; the global cap evicts in the incoming seed's category (a full topic\n // stock must not push chat material out and vice versa).\n let evicted: Seed | undefined;\n const catCount = activeSeeds(db).filter((s) => normalizeCategory(s.category) === category).length;\n if (catCount >= SEED_CATEGORY_CAPS[category]) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = victim;\n }\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = evicted ?? victim;\n }\n }\n\n db.seq += 1;\n seed = {\n id: `s${db.seq}`,\n text,\n topic: input.topic ?? text.slice(0, 24),\n tag,\n source,\n category,\n confidence,\n protected: source === 'hand' || (source === 'profile' && confidence >= 0.7),\n used: 0,\n bornAt: nowIso,\n expiresAt: new Date(now + ttlMs).toISOString(),\n lastUsedAt: null,\n lastEvidenceAt: nowIso,\n status: 'active',\n };\n db.seeds.push(seed);\n emit(audit, { event: 'seed_added', id: seed.id, kind: 'added', tag: seed.tag, source: seed.source, category: seed.category, confidence: seed.confidence });\n if (evicted) emit(audit, { event: 'seed_retired', id: evicted.id, reason: 'pool_cap', via: 'add', into: seed.id });\n savePool(guard, file, db);\n return { kind: 'added', seed, evicted };\n}\n\n/**\n * Deterministic gc (rules 1-3 of §4.2). Rule 4 (pool cap) fires on add only.\n * v1.9.0: the archive area is ALSO capped (seeds.archiveCap) — the archive is\n * a forensic trail, not a hoard; trim oldest-retired first.\n */\nexport function gcPool(guard: PathGuard, file: string, policy: Policy, now = Date.now(), audit: SeedAuditFn = NO_AUDIT): GcReport {\n return update(guard, file, (db) => {\n const report: GcReport = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };\n for (const s of activeSeeds(db)) {\n const ageMs = now - parseIso(s.bornAt);\n const sinceEvidence = parseIso(s.lastEvidenceAt);\n const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n if (isConsumed(s, policy, sinceUsed, sinceEvidence)) {\n archiveSeed(s, 'consumed', now);\n report.consumed += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'gc', used: s.used });\n } else if (now > parseIso(s.expiresAt)) {\n archiveSeed(s, 'expired', now);\n report.expired += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'expired', via: 'gc', used: s.used });\n } else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {\n archiveSeed(s, 'cold_bench', now);\n report.coldBench += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'cold_bench', via: 'gc', used: s.used });\n }\n }\n // archive cap: drop the oldest-retired rows beyond the cap (retiredAt may be\n // missing on pre-v1.5 rows — parseIso('') = 0 sorts them oldest).\n const archived = db.seeds.filter((s) => s.status === 'archived');\n if (archived.length > policy.seeds.archiveCap) {\n const drop = [...archived]\n .sort((a, b) => parseIso(a.retiredAt ?? '') - parseIso(b.retiredAt ?? ''))\n .slice(0, archived.length - policy.seeds.archiveCap);\n const dropped = new Set(drop.map((s) => s.id));\n db.seeds = db.seeds.filter((s) => !dropped.has(s.id));\n report.archiveTrimmed = drop.length;\n // These rows are GONE (not archived — deleted): the log is the only trace\n // left, so name them. IDs only, and cap the list in case a first GC trims\n // a legacy archive wholesale.\n emit(audit, { event: 'seed_archive_trimmed', count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });\n }\n report.activeAfter = activeSeeds(db).length;\n savePool(guard, file, db);\n return report;\n });\n}\n\n/** The seed surfaced in a real expression: count + maybe retire (rule 1). */\nexport function surfaceSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n // H-22: snapshot the PREVIOUS use time before bumping it — the retirement\n // check must compare against the last use, not against this one.\n const prevUsedAt = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n s.used += 1;\n s.lastUsedAt = new Date(now).toISOString();\n if (isConsumed(s, policy, prevUsedAt, parseIso(s.lastEvidenceAt))) {\n archiveSeed(s, 'consumed', now);\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'surface', used: s.used });\n }\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Deliberate retirement (item completed / no longer relevant). */\nexport function archiveSeedById(\n guard: PathGuard,\n file: string,\n id: string,\n reason: SeedRetireReason = 'completed',\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n archiveSeed(s, reason, now);\n emit(audit, { event: 'seed_retired', id: s.id, reason, via: 'manual' });\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Restore an archived seed to the active pool (UI operation; resets TTL). */\nexport function restoreSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): { ok: true; seed: Seed } | { ok: false; reason: string } {\n return update<{ ok: true; seed: Seed } | { ok: false; reason: string }>(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'archived');\n if (!s) return { ok: false, reason: 'archived seed not found' };\n // H-23: a restore must satisfy the same two caps an add does — the global\n // one AND the per-category one. The add path evicts a neighbour instead, but\n // restoring a memory is a user action: refuse it rather than silently\n // pushing someone else's material out.\n const category = normalizeCategory(s.category);\n const catCap = SEED_CATEGORY_CAPS[category];\n const inCategory = activeSeeds(db).filter((x) => normalizeCategory(x.category) === category).length;\n if (inCategory >= catCap) {\n return { ok: false, reason: `category full (${category}: ${catCap}); archive something in it first` };\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };\n }\n s.status = 'active';\n s.retireReason = undefined;\n s.retiredAt = undefined;\n // H-23: reset bornAt as well — cold-bench eviction (used === 0 && age >=\n // coldBenchDays) keys off bornAt, so a restored seed used to be re-archived\n // by the very next maintenance beat (\"restored, then gone one beat later\").\n s.bornAt = new Date(now).toISOString();\n s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();\n s.lastEvidenceAt = new Date(now).toISOString();\n emit(audit, { event: 'seed_restored', id: s.id });\n savePool(guard, file, db);\n return { ok: true, seed: s };\n });\n}\n\n/** Hard delete (UI explicit action with confirm; the caller audits it). */\nexport function deleteSeed(guard: PathGuard, file: string, id: string, audit: SeedAuditFn = NO_AUDIT): boolean {\n return update(guard, file, (db) => {\n const before = db.seeds.length;\n db.seeds = db.seeds.filter((x) => x.id !== id);\n if (db.seeds.length === before) return false;\n emit(audit, { event: 'seed_deleted', id });\n savePool(guard, file, db);\n return true;\n });\n}\n","// Material pool (\"素材池\") types. Charter (axiom 3): this pool is a CACHE,\n// not memory - volatile, evictable, burnable; long-term memory only absorbs\n// conversation-validated content. Everything here may be destroyed.\n\nexport type SeedTag = 'news' | 'fandom' | 'scene' | 'promise';\nexport type SeedSource = 'chat' | 'screen' | 'browse' | 'hand' | 'profile';\nexport type SeedCategory = 'topic' | 'chat';\nexport type SeedRetireReason = 'consumed' | 'expired' | 'cold_bench' | 'pool_cap' | 'completed';\n\nexport interface Seed {\n id: string;\n /** Short brief shown to the reflection prompt. */\n text: string;\n /** Merge key (§4.3): same-topic actives collapse into one. */\n topic: string;\n tag: SeedTag;\n source: SeedSource;\n /** Loop role, decoupled from source (2026-09-18 spec ⑤): 'topic' = wander/\n * hand/profile stock (random pick), 'chat' = conversation-grown, consumed\n * once. Absent on pre-v1.5 rows; loadPool defaults them to 'topic'. */\n category?: SeedCategory;\n /** 0..1 trust level; handwriting and high-confidence profile sources gain eviction protection. */\n confidence: number;\n /** Eviction protection (§4.2 pool-cap rule): handwritten / high-confidence profile sources. */\n protected: boolean;\n /** Times this seed surfaced in an actual expression. */\n used: number;\n bornAt: string;\n /** bornAt + ttlDays[tag]; archived when passed (rule 2). */\n expiresAt: string;\n lastUsedAt: string | null;\n /** Fresh-evidence watermark: rule 1 spares seeds with evidence newer than last use. */\n lastEvidenceAt: string;\n status: 'active' | 'archived';\n retireReason?: SeedRetireReason;\n retiredAt?: string;\n}\n\nexport interface SeedDb {\n seq: number;\n seeds: Seed[];\n}\n\nexport function emptySeedDb(): SeedDb {\n return { seq: 0, seeds: [] };\n}\n\nexport const SEED_SOURCE_DEFAULT_CONFIDENCE: Record<SeedSource, number> = {\n hand: 1.0,\n profile: 0.7,\n chat: 0.6,\n screen: 0.4,\n browse: 0.4,\n};\n"],"mappings":"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","names":[]}
|