@kanadego/dsh-heartbeat 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -4
- package/config/policy.json +73 -72
- package/dist/{chunk-SQJLKP7L.js → chunk-2SYXKKZF.js} +145 -37
- package/dist/chunk-2SYXKKZF.js.map +1 -0
- package/dist/{chunk-UCJR6TC7.js → chunk-IPOZFZPY.js} +28 -14
- package/dist/chunk-IPOZFZPY.js.map +1 -0
- package/dist/{chunk-MXJPX3FM.js → chunk-PQTSXW4F.js} +37 -8
- package/dist/chunk-PQTSXW4F.js.map +1 -0
- package/dist/cli/index.js +14 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +387 -174
- package/dist/index.js.map +1 -1
- package/dist/{migrate-43PUK25G.js → migrate-N6NXMZ3G.js} +2 -2
- package/dist/{pool-JQC5JWIZ.js → pool-7I4VACG2.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-MXJPX3FM.js.map +0 -1
- package/dist/chunk-SQJLKP7L.js.map +0 -1
- package/dist/chunk-UCJR6TC7.js.map +0 -1
- /package/dist/{migrate-43PUK25G.js.map → migrate-N6NXMZ3G.js.map} +0 -0
- /package/dist/{pool-JQC5JWIZ.js.map → pool-7I4VACG2.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/path-guard.ts","../src/config/schema.ts","../src/config/load.ts","../src/ledger/ledger.ts","../src/browse/browse.ts","../src/browse/preference.ts","../src/notify/notify.ts","../src/weekly/report.ts","../src/core/preset-install.ts"],"sourcesContent":["// Path whitelist guard (requirement 8 / design doc §10.2).\n//\n// Every fs write/delete must have its target canonicalized first, then be\n// checked against the canonical workspace prefix with a separator boundary.\n// A raw startsWith check is bypassable via \"..\", symlinks/junctions, and\n// case variants; canonical realpath closes all of those on Windows.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nexport class PathOutsideWorkspaceError extends Error {\n constructor(target: string, workspace: string) {\n super(`path outside workspace: \"${target}\" (workspace: \"${workspace}\")`);\n this.name = 'PathOutsideWorkspaceError';\n }\n}\n\n/**\n * Resolve to an absolute canonical path. For targets that do not exist yet,\n * realpath the deepest existing ancestor and re-append the virtual tail, so\n * planned files under the workspace validate while `..` escapes still resolve\n * through the real filesystem.\n */\nexport function canonicalize(target: string): string {\n const abs = path.resolve(target);\n try {\n return fs.realpathSync(abs);\n } catch {\n // Walk up from the missing leaf, recording each missing component, until\n // an existing ancestor is found; re-append the missing tail afterwards.\n const tail: string[] = [];\n let dir = abs;\n for (;;) {\n const base = path.basename(dir);\n const parent = path.dirname(dir);\n if (parent === dir) {\n throw new Error(`cannot canonicalize \"${target}\": no existing ancestor`);\n }\n tail.push(base);\n dir = parent;\n try {\n const realDir = fs.realpathSync(dir);\n return path.join(realDir, ...tail.reverse());\n } catch {\n continue;\n }\n }\n }\n}\n\n/**\n * True when `targetCanon` equals the workspace dir or lies under it.\n * Comparison is case-insensitive (Windows filesystems) and requires a\n * separator boundary so `D:\\ws-data-evil` does not match workspace `D:\\ws-data`.\n */\nexport function isInsideWorkspace(workspaceCanon: string, targetCanon: string): boolean {\n const norm = (p: string) => {\n let n = path.normalize(p).toLowerCase();\n if (!n.endsWith(path.sep)) n += path.sep;\n return n;\n };\n const w = norm(workspaceCanon);\n const t = norm(targetCanon);\n return t === w || t.startsWith(w);\n}\n\nexport interface PathGuard {\n /** Canonical workspace boundary. */\n readonly workspace: string;\n /** Canonicalize then validate; returns the canonical path or throws. */\n assert(target: string): string;\n /** Canonicalize then validate; returns null instead of throwing. */\n check(target: string): string | null;\n}\n\nexport function createPathGuard(workspaceDir: string): PathGuard {\n const workspace = canonicalize(workspaceDir);\n const guard: PathGuard = {\n workspace,\n check(target: string): string | null {\n const canon = canonicalize(target);\n return isInsideWorkspace(workspace, canon) ? canon : null;\n },\n assert(target: string): string {\n const canon = guard.check(target);\n if (canon === null) throw new PathOutsideWorkspaceError(target, workspace);\n return canon;\n },\n };\n return guard;\n}\n","// Policy shape + runtime validation. Factory defaults live in config/policy.json\r\n// (read-only); the user layer in data/settings/policy.json overrides via deep\r\n// merge (design doc §13, D3).\r\n\r\nexport interface QuietHours {\r\n start: string; // \"HH:MM\"\r\n end: string; // \"HH:MM\"\r\n}\r\n\r\nexport interface BrowseWindow {\r\n start: string;\r\n end: string;\r\n}\r\n\r\nexport interface Policy {\r\n heartbeat: { intervalMin: number; idleMode: boolean; archiveRotatedHome: boolean };\r\n gate: {\r\n maxDailySend: number;\r\n cooldownMinutes: number;\r\n quietHours: QuietHours;\r\n };\r\n browse: {\r\n windows: BrowseWindow[];\r\n minIntervalHours: number;\r\n maxSeedsPerVisit: number;\r\n };\r\n seeds: {\r\n maxActive: number;\r\n ttlDays: { news: number; fandom: number; scene: number; promise: number };\r\n coldBenchDays: number;\r\n /** Archive-area cap (v1.9.0): gc trims archived seeds beyond this, oldest first. */\r\n archiveCap: number;\r\n retireAfterUsed: number;\r\n scoreWeights: { freshness: number; unused: number; confidence: number };\r\n };\r\n profile: {\r\n consolidation: { minIntervalHours: number; inboxBacklog: number };\r\n partitionCap: number;\r\n maxOpsPerRun: number;\r\n confidenceCap: { chat: number; screen: number; browse: number };\r\n volatileDays: number;\r\n stableLowActivityDays: number;\r\n psyEnabled: boolean;\r\n };\r\n /** Observation depth (v1.8.0): how much of each user message enters the inbox. */\r\n observe: { maxChars: number; perBeat: number };\r\n weekly: { enabled: boolean };\r\n retention: { envPulseHours: number; decisionLogDays: number };\r\n}\r\n\r\nconst HHMM = /^([01]\\d|2[0-3]):[0-5]\\d$/;\r\n\r\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\r\n return typeof v === 'object' && v !== null && !Array.isArray(v);\r\n}\r\n\r\nfunction fail(msg: string): never {\r\n throw new Error(`policy: ${msg}`);\r\n}\r\n\r\nexport function assertPolicy(input: unknown): asserts input is Policy {\r\n if (!isPlainObject(input)) fail('root must be an object');\r\n const p = input;\r\n const hb = p.heartbeat;\r\n if (!isPlainObject(hb)) fail('heartbeat missing');\r\n if (typeof hb.intervalMin !== 'number' || hb.intervalMin < 1 || hb.intervalMin > 1440) {\r\n fail('heartbeat.intervalMin must be a number in [1, 1440]');\r\n }\r\n if (typeof hb.idleMode !== 'boolean') fail('heartbeat.idleMode must be boolean');\r\n if (typeof hb.archiveRotatedHome !== 'boolean') fail('heartbeat.archiveRotatedHome must be boolean');\r\n const g = p.gate;\r\n if (!isPlainObject(g)) fail('gate missing');\r\n if (typeof g.maxDailySend !== 'number' || g.maxDailySend < 0) fail('gate.maxDailySend must be >= 0');\r\n if (typeof g.cooldownMinutes !== 'number' || g.cooldownMinutes < 0) fail('gate.cooldownMinutes must be >= 0');\r\n const qh = g.quietHours;\r\n if (!isPlainObject(qh)) fail('gate.quietHours missing');\r\n if (typeof qh.start !== 'string' || !HHMM.test(qh.start) || typeof qh.end !== 'string' || !HHMM.test(qh.end)) {\r\n fail('gate.quietHours must be {start:\"HH:MM\", end:\"HH:MM\"}');\r\n }\r\n const b = p.browse;\r\n if (!isPlainObject(b)) fail('browse missing');\r\n if (!Array.isArray(b.windows) || b.windows.length === 0) fail('browse.windows must be a non-empty array');\r\n for (const w of b.windows) {\r\n if (!isPlainObject(w)) fail('browse.windows entries must be objects');\r\n if (typeof w.start !== 'string' || !HHMM.test(w.start) || typeof w.end !== 'string' || !HHMM.test(w.end)) {\r\n fail('browse.windows entries must be {start:\"HH:MM\", end:\"HH:MM\"}');\r\n }\r\n }\r\n if (typeof b.minIntervalHours !== 'number' || b.minIntervalHours <= 0) fail('browse.minIntervalHours must be > 0');\r\n if (typeof b.maxSeedsPerVisit !== 'number' || b.maxSeedsPerVisit < 1) fail('browse.maxSeedsPerVisit must be >= 1');\r\n const s = p.seeds;\r\n if (!isPlainObject(s)) fail('seeds missing');\r\n if (typeof s.maxActive !== 'number' || s.maxActive < 1) fail('seeds.maxActive must be >= 1');\r\n if (!isPlainObject(s.ttlDays)) fail('seeds.ttlDays missing');\r\n for (const k of ['news', 'fandom', 'scene', 'promise'] as const) {\r\n if (typeof s.ttlDays[k] !== 'number') fail(`seeds.ttlDays.${k} missing`);\r\n }\r\n if (typeof s.coldBenchDays !== 'number') fail('seeds.coldBenchDays missing');\r\n if (typeof s.archiveCap !== 'number' || s.archiveCap < 1) fail('seeds.archiveCap must be >= 1');\r\n if (typeof s.retireAfterUsed !== 'number' || s.retireAfterUsed < 1) fail('seeds.retireAfterUsed must be >= 1');\r\n if (!isPlainObject(s.scoreWeights)) fail('seeds.scoreWeights missing');\r\n const pr = p.profile;\r\n if (!isPlainObject(pr)) fail('profile missing');\r\n const c = pr.consolidation;\r\n if (!isPlainObject(c)) fail('profile.consolidation missing');\r\n if (typeof c.minIntervalHours !== 'number' || typeof c.inboxBacklog !== 'number') fail('profile.consolidation fields missing');\r\n if (typeof pr.partitionCap !== 'number' || pr.partitionCap < 1) fail('profile.partitionCap must be >= 1');\r\n if (typeof pr.maxOpsPerRun !== 'number' || pr.maxOpsPerRun < 1) fail('profile.maxOpsPerRun must be >= 1');\r\n const cc = pr.confidenceCap;\r\n if (!isPlainObject(cc)) fail('profile.confidenceCap missing');\r\n if (typeof cc.chat !== 'number' || typeof cc.screen !== 'number' || typeof cc.browse !== 'number') {\r\n fail('profile.confidenceCap fields missing');\r\n }\r\n if (typeof pr.volatileDays !== 'number' || pr.volatileDays < 1) fail('profile.volatileDays must be >= 1');\r\n if (typeof pr.stableLowActivityDays !== 'number' || pr.stableLowActivityDays < 1) fail('profile.stableLowActivityDays must be >= 1');\r\n if (typeof pr.psyEnabled !== 'boolean') fail('profile.psyEnabled must be boolean');\r\n const ob = p.observe;\r\n if (!isPlainObject(ob)) fail('observe missing');\r\n if (typeof ob.maxChars !== 'number' || ob.maxChars < 20 || ob.maxChars > 2000) fail('observe.maxChars must be in [20, 2000]');\r\n if (typeof ob.perBeat !== 'number' || ob.perBeat < 1 || ob.perBeat > 100) fail('observe.perBeat must be in [1, 100]');\r\n if (!isPlainObject(p.weekly)) fail('weekly missing');\r\n if (typeof p.weekly.enabled !== 'boolean') fail('weekly.enabled must be boolean');\r\n const r = p.retention;\r\n if (!isPlainObject(r)) fail('retention missing');\r\n if (typeof r.envPulseHours !== 'number' || typeof r.decisionLogDays !== 'number') fail('retention fields missing');\r\n}\r\n\r\n/** Recursive merge: user values win; objects merge, arrays and scalars replace. */\r\nexport function deepMerge<T>(base: T, override: unknown): T {\r\n if (!isPlainObject(base) || !isPlainObject(override)) {\r\n return (override === undefined ? base : (override as T));\r\n }\r\n const out: Record<string, unknown> = { ...base };\r\n for (const [k, v] of Object.entries(override)) {\r\n out[k] = v === undefined ? (base as Record<string, unknown>)[k] : deepMerge((base as Record<string, unknown>)[k], v);\r\n }\r\n return out as T;\r\n}","// Two-layer policy loading (D3): factory defaults (config/policy.json, read-only)\n// + user layer (data/settings/policy.json). Missing user layer is normal; an\n// invalid factory file or user layer is fail-closed (throw at startup).\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { assertPolicy, deepMerge, type Policy } from './schema.js';\n\nexport const USER_POLICY_FILE = 'policy.json';\n\nexport function loadPolicy(guard: PathGuard, configDir: string, settingsDir: string): Policy {\n const factoryPath = path.join(configDir, 'policy.json');\n let factoryRaw: unknown;\n try {\n factoryRaw = JSON.parse(fs.readFileSync(factoryPath, 'utf8'));\n } catch (e) {\n throw new Error(`factory policy unreadable at ${factoryPath}: ${String(e)}`);\n }\n assertPolicy(factoryRaw);\n\n const userPath = guard.assert(path.join(settingsDir, USER_POLICY_FILE));\n let merged: Policy = factoryRaw;\n if (fs.existsSync(userPath)) {\n try {\n const userRaw: unknown = JSON.parse(fs.readFileSync(userPath, 'utf8'));\n merged = deepMerge(factoryRaw, userRaw);\n } catch (e) {\n throw new Error(`user policy layer unparseable at ${userPath}: ${String(e)}`);\n }\n }\n assertPolicy(merged);\n return merged;\n}\n\n/**\n * Merge `patch` into the user policy layer (data/settings/policy.json) without\n * touching the factory file. Used by the settings card for policy-backed\n * toggles (psyEnabled, v1.7.0). No full-Policy validation here: the layer is\n * merged over the factory and validated together at the next loadPolicy.\n */\nexport function updateUserPolicy(guard: PathGuard, settingsDir: string, patch: Record<string, unknown>): Record<string, unknown> {\n const userPath = guard.assert(path.join(settingsDir, USER_POLICY_FILE));\n let user: Record<string, unknown> = {};\n try {\n user = JSON.parse(fs.readFileSync(userPath, 'utf8')) as Record<string, unknown>;\n if (!user || typeof user !== 'object' || Array.isArray(user)) user = {};\n } catch {\n user = {}; // fresh layer\n }\n const merged = deepMerge(user, patch) as Record<string, unknown>;\n fs.mkdirSync(path.dirname(userPath), { recursive: true });\n fs.writeFileSync(userPath, JSON.stringify(merged, null, 2) + '\\n', 'utf8');\n return merged;\n}\n","// Ledger (\"账本\") - the ONE shared ledger (design doc §5, requirement 5).\n// Human-readable Markdown by charter; the user may edit it by hand, so the\n// parser is tolerant: unknown lines are preserved verbatim on rewrite.\n\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { readText, writeText } from '../vault/vault.js';\n\nconst DAY_MS = 86_400_000;\n\nexport interface LedgerEntry {\n id: string;\n date: string; // \"YYYY-MM-DD\"\n time: string; // \"HH:MM\"\n status: 'open' | 'done';\n text: string;\n}\n\nexport function ledgerFilePath(dataDir: string): string {\n return path.join(dataDir, 'ledger.md');\n}\n\nconst LINE_RE = /^- \\[(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2})\\]\\[(open|done)\\]\\[#([0-9a-f]{6})\\] (.*)$/;\n\nfunction renderEntry(e: LedgerEntry): string {\n return `- [${e.date} ${e.time}][${e.status}][#${e.id}] ${e.text}`;\n}\n\nexport function readLedger(guard: PathGuard, file: string): { header: string; entries: LedgerEntry[]; rawLines: string[] } {\n const raw = readText(guard, file, '# 账本\\n');\n const lines = raw.split('\\n');\n const entries: LedgerEntry[] = [];\n const rawLines: string[] = [];\n for (const line of lines) {\n const m = LINE_RE.exec(line);\n if (m) {\n entries.push({ date: m[1]!, time: m[2]!, status: m[3] as 'open' | 'done', id: m[4]!, text: m[5]! });\n }\n rawLines.push(line);\n }\n return { header: lines[0] ?? '# 账本', entries, rawLines };\n}\n\nexport function appendEntry(guard: PathGuard, file: string, text: string, now = Date.now()): LedgerEntry {\n const textTrimmed = text.trim();\n if (!textTrimmed) throw new Error('ledger entry must not be empty');\n const d = new Date(now);\n const pad = (n: number) => String(n).padStart(2, '0');\n const entry: LedgerEntry = {\n id: randomUUID().slice(0, 6),\n date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,\n time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,\n status: 'open',\n text: textTrimmed.replace(/\\r?\\n/g, ' '),\n };\n const { rawLines } = readLedger(guard, file);\n rawLines.push(renderEntry(entry));\n writeText(guard, file, rawLines.join('\\n').replace(/\\n*$/, '\\n'));\n return entry;\n}\n\n/** Mark an entry done by id (preferred) or unique text substring. The date is\n * restamped to the completion day (time was already) so weekly reporting can\n * count \"resolved this week\" — the creation timestamp lives on in nothing,\n * but done entries are rarely aged against. */\nexport function markDone(guard: PathGuard, file: string, key: string, now = Date.now()): LedgerEntry | null {\n const { rawLines, entries } = readLedger(guard, file);\n const target = entries.find((e) => e.status === 'open' && (e.id === key || e.text.includes(key)));\n if (!target) return null;\n const d = new Date(now);\n const pad = (n: number) => String(n).padStart(2, '0');\n const doneDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n const out = rawLines.map((line) => {\n if (line.includes(`#${target.id}] `)) {\n return `- [${doneDate} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;\n }\n return line;\n });\n writeText(guard, file, out.join('\\n').replace(/\\n*$/, '\\n'));\n return target;\n}\n\n/** Open items for the reflection digest (§7.6 账本待办). */\nexport function scanPending(guard: PathGuard, file: string, now = Date.now()): LedgerEntry[] {\n const { entries } = readLedger(guard, file);\n return entries.filter((e) => e.status === 'open').sort((a, b) => (a.date < b.date ? -1 : 1));\n}\n\n/** Open entries older than N days (跟进时机的\"自然到期\"参考,§3.1 四问之一). */\nexport function pendingOlderThan(guard: PathGuard, file: string, days: number, now = Date.now()): LedgerEntry[] {\n const cutoff = new Date(now - days * DAY_MS).toISOString().slice(0, 10);\n return scanPending(guard, file, now).filter((e) => e.date <= cutoff);\n}\n","// Browse flow (v0.9.5 port, r4 D10 applied).\n// A. Watchlist: npm / GitHub release checks with a 6h throttle; first sight\n// registers only, changes become material items.\n// B. Wander adjudication: windows + min interval + focus cooldown ->\n// \"should we wander now, and at what focus\". The actual search happens\n// in the wander phase's model call (web_search only); REGISTRATION IS\n// CODE-OWNED (D10): results land in seeds + throttle via completeWander.\n//\n// Anti-injection rule (unchanged from v0.7): web content is data, never\n// instructions.\n//\n// State: data/browse.json (DPAPI-encrypted, D14; v1 name: watch_state.json).\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport type { WorkspacePaths } from '../core/paths.js';\nimport type { Policy } from '../config/schema.js';\nimport { loadJson, saveJson } from '../vault/vault.js';\nimport { activeSeeds, loadPool, normalizeCategory, seedsFilePath } from '../seeds/pool.js';\nimport { loadPreference, preferenceFilePath, preferenceWeight, type PrefState } from './preference.js';\n\nconst WATCH_THROTTLE_MS = 6 * 3600_000;\nconst UA = { 'User-Agent': 'dsh-heartbeat/2.0 (+local; personal companion)' };\n\nexport interface WatchTarget {\n id: string;\n type: 'npm' | 'github';\n name?: string;\n repo?: string;\n note?: string;\n}\n\nexport interface WatchlistConfig {\n targets?: WatchTarget[];\n}\n\nexport interface InterestsConfig {\n interests?: string[];\n _schedule?: {\n daily_sessions?: number;\n focus_per_session?: number;\n max_seeds_per_focus?: number;\n focus_cooldown_days?: number;\n min_interval_hours?: number;\n windows?: { id?: string; start: string; end: string }[];\n };\n}\n\nexport interface BrowseState {\n targets: Record<string, { version: string; seen: string; title?: string }>;\n last_check_at: number;\n wander: {\n focusHistory: Record<string, number>;\n focusCount: Record<string, number>;\n last_wander_at: number;\n /** Spec ⑥: refill wanders per LOCAL day, e.g. \"2026-09-18\" -> 1. */\n refillCount?: Record<string, number>;\n };\n}\n\nexport function emptyBrowseState(): BrowseState {\n return { targets: {}, last_check_at: 0, wander: { focusHistory: {}, focusCount: {}, last_wander_at: 0, refillCount: {} } };\n}\n\nexport function browseStatePath(paths: WorkspacePaths): string {\n return path.join(paths.dataDir, 'browse.json');\n}\n\nfunction readJsonFile<T>(file: string, fallback: T): T {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8')) as T;\n } catch {\n return fallback;\n }\n}\n\n/** Factory configs are read-only; a user layer with the same filename replaces them. */\nexport function loadInterests(paths: WorkspacePaths): InterestsConfig {\n const userPath = path.join(paths.settingsDir, 'interests.json');\n if (fs.existsSync(userPath)) return readJsonFile<InterestsConfig>(userPath, { interests: [], _schedule: {} });\n return readJsonFile<InterestsConfig>(path.join(paths.configDir, 'interests.json'), { interests: [], _schedule: {} });\n}\n\nexport function loadWatchlist(paths: WorkspacePaths): WatchlistConfig {\n const userPath = path.join(paths.settingsDir, 'watchlist.json');\n if (fs.existsSync(userPath)) return readJsonFile<WatchlistConfig>(userPath, { targets: [] });\n return readJsonFile<WatchlistConfig>(path.join(paths.configDir, 'watchlist.json'), { targets: [] });\n}\n\nfunction loadState(guard: PathGuard, paths: WorkspacePaths): BrowseState {\n return loadJson<BrowseState>(guard, browseStatePath(paths)) ?? emptyBrowseState();\n}\n\n// ── A. watchlist checks (fetcher injectable for tests) ──────────────────\n\nexport type FetchLike = (url: string, init?: { headers?: Record<string, string> }) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;\n\nasync function checkNpm(fetcher: FetchLike, name: string): Promise<{ version: string; seen: string; title?: string } | null> {\n const r = await fetcher(`https://registry.npmjs.org/${name}/latest`, { headers: UA });\n if (!r.ok) throw new Error(`npm ${r.status}`);\n const j = await r.json() as { version?: string };\n if (!j.version) throw new Error('npm: no version');\n return { version: j.version, seen: `npm:${j.version}` };\n}\n\nasync function checkGithub(fetcher: FetchLike, repo: string): Promise<{ version: string; seen: string; title?: string } | null> {\n const r = await fetcher(`https://api.github.com/repos/${repo}/releases/latest`, {\n headers: { ...UA, Accept: 'application/vnd.github+json' },\n });\n if (r.status === 404) return null; // repo has no releases yet\n if (!r.ok) throw new Error(`gh ${r.status}`);\n const j = await r.json() as { tag_name?: string; name?: string };\n if (!j.tag_name) throw new Error('gh: no tag');\n return { version: j.tag_name, seen: `gh:${j.tag_name}`, title: j.name || '' };\n}\n\nexport interface WatchReport {\n /** Material texts for the pool (one per changed target). */\n items: { text: string; topic: string; tag: 'news'; source: 'browse'; confidence: number }[];\n errors: string[];\n checked: number;\n}\n\nexport async function checkWatchlist(\n guard: PathGuard,\n paths: WorkspacePaths,\n opts: { fetcher?: FetchLike; throttleOk?: boolean } = {},\n now = Date.now(),\n): Promise<WatchReport> {\n const fetcher = opts.fetcher ?? (globalThis.fetch as unknown as FetchLike);\n const state = loadState(guard, paths);\n if (opts.throttleOk !== true && now - state.last_check_at < WATCH_THROTTLE_MS) {\n return { items: [], errors: [], checked: 0 };\n }\n const watchlist = loadWatchlist(paths);\n const report: WatchReport = { items: [], errors: [], checked: 0 };\n for (const t of watchlist.targets ?? []) {\n report.checked += 1;\n try {\n const info = t.type === 'npm' && t.name ? await checkNpm(fetcher, t.name)\n : t.type === 'github' && t.repo ? await checkGithub(fetcher, t.repo)\n : null;\n if (!info) continue;\n const prev = state.targets[t.id];\n if (prev && prev.seen !== info.seen) {\n const title = info.title ? `(${info.title.slice(0, 60)})` : '';\n report.items.push({\n text: `${t.note || t.id} 有更新:${prev.version} -> ${info.version}${title}`,\n topic: `watch:${t.id}`,\n tag: 'news',\n source: 'browse',\n confidence: 0.4,\n });\n }\n state.targets[t.id] = info; // first sight registers silently (首见不产素材)\n } catch (e) {\n report.errors.push(`${t.id}: ${String(e)}`);\n }\n }\n state.last_check_at = now;\n saveJson(guard, browseStatePath(paths), state);\n return report;\n}\n\n// ── B. wander adjudication (pure-ish, state injected) ───────────────────\n\nexport interface WanderAdvice {\n focus: string | null;\n query: string | null;\n skipped: string | null;\n}\n\nexport function inWanderWindow(now: Date, windows: { start: string; end: string }[]): string | null {\n const hm = now.getHours() * 60 + now.getMinutes();\n for (const w of windows) {\n const [sh, sm] = w.start.split(':').map(Number);\n const [eh, em] = w.end.split(':').map(Number);\n if (hm >= sh! * 60 + sm! && hm <= eh! * 60 + em!) return `${w.start}-${w.end}`;\n }\n return null;\n}\n\nfunction onCooldown(state: BrowseState, focus: string, cooldownDays: number, now: number): boolean {\n const last = state.wander.focusHistory[focus] ?? 0;\n return last > now - cooldownDays * 86_400_000;\n}\n\n/**\n * Focus pick (2026-10-06): round-robin recency as the base, preference rate as\n * a multiplier. Ordering key = effective age = realAgeSinceLastUse × weight —\n * a favorite's age \"grows\" faster, so it reaches the pick threshold sooner and\n * is wandered up to W_MAX times as often. Lockdown-proof by construction:\n * - cooldown (above) keeps every topic rotating no matter the weight;\n * - weight lives in [W_MIN, W_MAX] (preference.ts);\n * - hard starvation floor: any topic untouched ≥ STARVATION_DAYS ignores\n * weighting entirely and the oldest real age wins.\n * Without a preference state the behavior is exactly the old pure LRU.\n */\nexport const STARVATION_DAYS = 14;\n\nexport function pickFocus(\n state: BrowseState,\n interests: InterestsConfig,\n now: number,\n pref?: PrefState,\n): string | null {\n const sc = interests._schedule ?? {};\n const cooldown = sc.focus_cooldown_days ?? 3;\n const pool = (interests.interests ?? []).filter((t) => !onCooldown(state, t, cooldown, now));\n if (pool.length === 0) return null;\n if (!pref) {\n pool.sort((a, b) => (state.wander.focusHistory[a] ?? 0) - (state.wander.focusHistory[b] ?? 0));\n return pool[0]!;\n }\n const age = (t: string): number => now - (state.wander.focusHistory[t] ?? 0);\n const starved = pool.filter((t) => age(t) >= STARVATION_DAYS * 86_400_000);\n if (starved.length > 0) {\n starved.sort((a, b) => age(b) - age(a)); // oldest real age wins\n return starved[0]!;\n }\n const effAge = (t: string): number => age(t) * preferenceWeight(pref, t, now);\n pool.sort((a, b) => effAge(b) - effAge(a));\n return pool[0]!;\n}\n\n/** Adjudicate: windows + min interval + focus cooldown -> advice | skipped. */\nexport function adviseWander(\n guard: PathGuard,\n paths: WorkspacePaths,\n policy: Policy,\n now = new Date(),\n): WanderAdvice {\n const state = loadState(guard, paths);\n const interests = loadInterests(paths);\n const windows = interests._schedule?.windows?.length\n ? interests._schedule.windows\n : (policy.browse.windows as { start: string; end: string }[]);\n const win = inWanderWindow(now, windows);\n if (!win) {\n const hh = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;\n return { focus: null, query: null, skipped: `window(now=${hh})` };\n }\n const minGap = policy.browse.minIntervalHours * 3600_000;\n if (now.getTime() - state.wander.last_wander_at < minGap) {\n return { focus: null, query: null, skipped: 'min-interval' };\n }\n const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir)));\n if (!focus) return { focus: null, query: null, skipped: 'no-focus' };\n return { focus, query: `${focus} 2026 最新`, skipped: null };\n}\n\n/**\n * Registration is CODE-OWNED (D10): called by the orchestrator after the\n * wander phase's model call returned selections. Records cooldown/count/\n * throttle timestamps for the focus. `refill` additionally bumps the daily\n * refill counter (spec ⑥).\n */\nexport function completeWander(\n guard: PathGuard,\n paths: WorkspacePaths,\n focus: string,\n now = Date.now(),\n opts: { refill?: boolean } = {},\n): { focus: string; count: number; refillsToday?: number } {\n const state = loadState(guard, paths);\n state.wander.focusHistory[focus] = now;\n state.wander.focusCount[focus] = (state.wander.focusCount[focus] ?? 0) + 1;\n state.wander.last_wander_at = now;\n let refillsToday: number | undefined;\n if (opts.refill) {\n const today = localDayKey(new Date(now));\n state.wander.refillCount = state.wander.refillCount ?? {};\n state.wander.refillCount[today] = (state.wander.refillCount[today] ?? 0) + 1;\n refillsToday = state.wander.refillCount[today];\n }\n saveJson(guard, browseStatePath(paths), state);\n return { focus, count: state.wander.focusCount[focus]!, ...(refillsToday === undefined ? {} : { refillsToday }) };\n}\n\n/** LOCAL day key (refill quota is a daily human-day budget, not a UTC day). */\nfunction localDayKey(d: Date): string {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n}\n\n// ── C. refill wander (spec ⑥, 2026-09-18): top up topic stock when it runs\n// dry. Independent of the window/minInterval gates (user decision: may stack\n// with a normal wander in the same beat) but still respects the 3-day focus\n// cooldown, and is capped at REFILL_MAX_PER_DAY per local day.\n\nexport const REFILL_TOPIC_THRESHOLD = 4;\nexport const REFILL_MAX_PER_DAY = 2;\n\nexport interface RefillAdvice {\n focus: string | null;\n query: string | null;\n skipped: string | null;\n topicCount: number;\n refillsToday: number;\n}\n\nexport function adviseRefillWander(\n guard: PathGuard,\n paths: WorkspacePaths,\n policy: Policy,\n now = new Date(),\n): RefillAdvice {\n const state = loadState(guard, paths);\n const today = localDayKey(now);\n const refillsToday = state.wander.refillCount?.[today] ?? 0;\n const topicCount = activeSeeds(loadPool(guard, seedsFilePath(paths.dataDir)))\n .filter((s) => normalizeCategory(s.category) === 'topic').length;\n if (refillsToday >= REFILL_MAX_PER_DAY) {\n return { focus: null, query: null, skipped: `refill-daily-cap(${refillsToday})`, topicCount, refillsToday };\n }\n if (topicCount > REFILL_TOPIC_THRESHOLD) {\n return { focus: null, query: null, skipped: `topic-stock-ok(${topicCount})`, topicCount, refillsToday };\n }\n const interests = loadInterests(paths);\n const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir))); // 3-day cooldown still applies\n if (!focus) {\n return { focus: null, query: null, skipped: 'no-focus', topicCount, refillsToday };\n }\n return { focus, query: `${focus} 2026 最新`, skipped: null, topicCount, refillsToday };\n}\n\nexport function browseStatus(guard: PathGuard, paths: WorkspacePaths): BrowseState {\n return loadState(guard, paths);\n}\n","// Topic preference (2026-10-06): consumption-driven bias for wander focus\n// selection. The persona's seed_report attributions (and the engine-room\n// fallback) fold into per-topic delivered/adopted counters; pickFocus reads\n// the smoothed rate as a weight on top of round-robin recency.\n//\n// Anti-lockdown guarantees (user-reviewed design):\n// - weight clamped to [W_MIN, W_MAX] — a favorite doubles at most, a cold\n// topic never drops below half;\n// - Laplace smoothing — a topic with no data sits near neutral, not zero;\n// - exponential decay (30-day half-life) — old glory fades;\n// - pickFocus additionally enforces a hard starvation floor (14 days).\n//\n// Storage is one JSON file through the vault (DPAPI): topic keys derive from\n// the user's profile/interests, so they are user-adjacent data.\n\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadJson, saveJson } from '../vault/vault.js';\n\nconst DAY_MS = 86_400_000;\n\nexport const W_MIN = 0.5;\nexport const W_MAX = 2;\n/** Laplace prior: rate = (adopted + A) / (delivered + D). Neutral ≈ A/D. */\nexport const SMOOTH_ADOPTED = 1;\nexport const SMOOTH_DELIVERED = 4;\n/** Counts decay with this half-life (days) since the topic's last activity. */\nexport const DECAY_HALF_LIFE_DAYS = 30;\n\nexport interface TopicPref {\n delivered: number;\n adopted: number;\n lastDeliveredAt: number;\n lastAdoptedAt: number;\n}\n\nexport interface PrefState {\n version: 1;\n topics: Record<string, TopicPref>;\n updatedAt: number;\n}\n\nexport function preferenceFilePath(dataDir: string): string {\n return path.join(dataDir, 'preference.json');\n}\n\n/** Non-negative finite number or 0 (v1.9.0 guard against hand-edited files). */\nfunction num(v: unknown): number {\n const n = Number(v);\n return Number.isFinite(n) && n >= 0 ? n : 0;\n}\n\n/** One stored entry → a trustworthy counter. Returns null for a garbled entry:\n * a NaN delivered/adopted used to survive the load and poison the whole\n * ordering (NaN makes every comparison false, so the topic neither aged nor\n * sorted). Dropping the entry is the safe read — the loop refills it. */\nfunction sanitizeTopicPref(raw: unknown): TopicPref | null {\n if (!raw || typeof raw !== 'object') return null;\n const t = raw as Partial<TopicPref>;\n if (!Number.isFinite(Number(t.delivered)) || !Number.isFinite(Number(t.adopted))) return null;\n const delivered = Math.floor(num(t.delivered));\n return {\n delivered,\n adopted: Math.min(delivered, Math.floor(num(t.adopted))),\n lastDeliveredAt: num(t.lastDeliveredAt),\n lastAdoptedAt: num(t.lastAdoptedAt),\n };\n}\n\nexport function loadPreference(guard: PathGuard, file: string): PrefState {\n const doc = loadJson<Partial<PrefState>>(guard, file);\n if (!doc || typeof doc !== 'object' || !doc.topics || typeof doc.topics !== 'object') {\n return { version: 1, topics: {}, updatedAt: 0 };\n }\n const topics: Record<string, TopicPref> = {};\n for (const [key, raw] of Object.entries(doc.topics)) {\n const t = sanitizeTopicPref(raw);\n if (key && t) topics[key] = t;\n }\n const updatedAt = Number(doc.updatedAt);\n return { version: 1, topics, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };\n}\n\nexport function savePreference(guard: PathGuard, file: string, state: PrefState): void {\n saveJson(guard, file, state);\n}\n\n/** Fold one delivery into the counters. offeredTopics = every topic in the\n * package; adoptedTopics = the credited ones (subset by construction — unknown\n * keys are still tolerated so the two lists can drift safely). */\nexport function recordDelivery(\n guard: PathGuard,\n file: string,\n opts: { offeredTopics: string[]; adoptedTopics: string[]; now: number },\n): PrefState {\n const state = loadPreference(guard, file);\n const adopted = new Set(opts.adoptedTopics);\n for (const topic of opts.offeredTopics) {\n if (!topic) continue;\n const t = state.topics[topic] ?? { delivered: 0, adopted: 0, lastDeliveredAt: 0, lastAdoptedAt: 0 };\n t.delivered += 1;\n t.lastDeliveredAt = opts.now;\n if (adopted.has(topic)) {\n t.adopted += 1;\n t.lastAdoptedAt = opts.now;\n }\n state.topics[topic] = t;\n }\n state.updatedAt = opts.now;\n savePreference(guard, file, state);\n return state;\n}\n\n/** Time-decayed counts (30-day half-life since last activity of each kind). */\nexport function decayed(t: TopicPref, now: number): { delivered: number; adopted: number } {\n const dAgeDays = Math.max(0, now - t.lastDeliveredAt) / DAY_MS;\n const aAgeDays = Math.max(0, now - t.lastAdoptedAt) / DAY_MS;\n const dHalf = Math.pow(0.5, dAgeDays / DECAY_HALF_LIFE_DAYS);\n const aHalf = Math.pow(0.5, aAgeDays / DECAY_HALF_LIFE_DAYS);\n return { delivered: t.delivered * dHalf, adopted: t.adopted * aHalf };\n}\n\n/**\n * Preference weight for a topic, clamped to [W_MIN, W_MAX]:\n * rate = (decayed adopted + 1) / (decayed delivered + 4) (neutral = 0.25)\n * w = clamp(rate / 0.25, W_MIN, W_MAX)\n * No data at all → rate 1/4 → w 1.0: a brand-new topic starts exactly neutral\n * (per design: \"0.2 附近的中间值,不是 0 分垫底\"); repeated \"delivered but\n * never adopted\" drags it toward W_MIN, consistent adoption lifts it to W_MAX.\n */\nexport function preferenceWeight(state: PrefState, topic: string, now: number): number {\n const t = state.topics[topic];\n const rate = t\n ? (() => {\n const d = decayed(t, now);\n return (d.adopted + SMOOTH_ADOPTED) / (d.delivered + SMOOTH_DELIVERED);\n })()\n : SMOOTH_ADOPTED / SMOOTH_DELIVERED;\n return Math.min(W_MAX, Math.max(W_MIN, rate / (SMOOTH_ADOPTED / SMOOTH_DELIVERED)));\n}\n","// Windows toast channel (D12): attention hint ONLY - never carries the\n// expression body. Wraps assets/notify.ps1 (self-registering AUMID).\n\nimport { spawnSync } from 'node:child_process';\nimport path from 'node:path';\nimport type { WorkspacePaths } from '../core/paths.js';\n\nfunction runNotify(paths: WorkspacePaths, args: string[]): { status: number; out: string } {\n const r = spawnSync('powershell.exe',\n ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', path.join(paths.assetsDir, 'notify.ps1'), ...args],\n { timeout: 20_000, encoding: 'utf8' });\n return { status: r.status ?? -1, out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };\n}\n\n/** Register the toast identity if missing (idempotent, per machine/user). */\nexport function ensureRegistered(paths: WorkspacePaths): boolean {\n const check = runNotify(paths, ['-Check']);\n if (/REGISTERED: yes/.test(check.out)) return true;\n const reg = runNotify(paths, ['-RegisterOnly']);\n return reg.status === 0;\n}\n\n/**\n * Fire the \"new message\" hint. Fixed neutral text per D12 - the actual\n * expression lives in the dedicated heartbeat session, never in the toast.\n */\nexport function sendNewMessageHint(paths: WorkspacePaths): boolean {\n const r = runNotify(paths, ['-Title', 'Heartbeat', '-Message', '有新消息']);\n return r.status === 0 && /TOAST_SENT/.test(r.out);\n}\n\n/** Weekly report ready hint (v1.8.0): same D12 discipline — announces that a\n * report exists, never carries its content. */\nexport function sendWeeklyReadyHint(paths: WorkspacePaths): boolean {\n const r = runNotify(paths, ['-Title', '心跳周报', '-Message', '本期周报已生成']);\n return r.status === 0 && /TOAST_SENT/.test(r.out);\n}\n","// Weekly report store + writer helpers (v1.8.0). Deliberately host-free:\r\n// orchestration (when to run, which agent, toast) lives in the orchestrator —\r\n// same split as profile/consolidate.ts. Reports are DPAPI-encrypted because a\r\n// report contains user-identifying content (profile adds, ledger texts) —\r\n// same划线标准 as profile/seeds (design doc §10.5).\r\n\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { atomicWriteJsonSync } from '../core/atomic-fs.js';\r\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\r\nimport type { PathGuard } from '../core/path-guard.js';\r\nimport type { WeeklyFacts } from './collect.js';\r\n\r\nexport const WEEKLY_INTERVAL_MS = 7 * 86_400_000;\r\n\r\nexport interface WeeklyReport {\r\n start: string;\r\n end: string;\r\n generatedAt: string;\r\n /** 'llm' = engine-room prose; 'template' = deterministic fallback. */\r\n source: 'llm' | 'template';\r\n text: string;\r\n}\r\n\r\nexport function weeklyDirPath(dataDir: string): string {\r\n return path.join(dataDir, 'weekly');\r\n}\r\n\r\nfunction stateFilePath(dataDir: string): string {\r\n return path.join(weeklyDirPath(dataDir), 'state.json');\r\n}\r\n\r\nfunction reportFileName(endIso: string): string {\r\n return `report-${endIso.slice(0, 10)}.json`;\r\n}\r\n\r\n/** Last generation time from the weekly state file; 0 = never. */\r\nexport function lastWeeklyGeneratedAt(guard: PathGuard, dataDir: string): number {\r\n try {\r\n const raw = JSON.parse(fs.readFileSync(guard.assert(stateFilePath(dataDir)), 'utf8')) as { lastGeneratedAt?: number };\r\n return Number(raw.lastGeneratedAt) || 0;\r\n } catch {\r\n return 0;\r\n }\r\n}\r\n\r\n/** Due when the last report is ≥7 days old. A never-anchored store lands on\r\n * the install-time anchor (see ensureWeeklyAnchor), so a fresh install is NOT\r\n * due — the first report arrives one full interval after installation. */\r\nexport function weeklyDue(guard: PathGuard, dataDir: string, now = Date.now()): boolean {\r\n const last = lastWeeklyGeneratedAt(guard, dataDir);\r\n return now - last >= WEEKLY_INTERVAL_MS;\r\n}\r\n\r\n/** Stamp the install-time anchor for a store that has never generated a report\r\n * (v1.9.0). Returns true when it wrote — the caller audits that. Without this,\r\n * `lastGeneratedAt = 0` made the very first maintenance beat due, so a fresh\r\n * install produced an \"onboarding\" report immediately, contradicting the\r\n * release notes (\"first report after a week\"). */\r\nexport function ensureWeeklyAnchor(guard: PathGuard, dataDir: string, now = Date.now()): boolean {\r\n if (lastWeeklyGeneratedAt(guard, dataDir) > 0) return false;\r\n fs.mkdirSync(weeklyDirPath(dataDir), { recursive: true });\r\n atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: now });\r\n return true;\r\n}\r\n\r\n/** Persist a report (DPAPI) and stamp the state file. */\r\nexport function saveWeeklyReport(guard: PathGuard, dataDir: string, report: WeeklyReport): string {\r\n const dir = weeklyDirPath(dataDir);\r\n fs.mkdirSync(dir, { recursive: true });\r\n const file = path.join(dir, reportFileName(report.end));\r\n saveEncryptedText(guard, file, JSON.stringify(report, null, 1));\r\n atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: Date.parse(report.generatedAt) });\r\n return file;\r\n}\r\n\r\nexport interface WeeklyReportMeta {\r\n file: string;\r\n start: string;\r\n end: string;\r\n generatedAt: string;\r\n source: string;\r\n}\r\n\r\n/** Newest-first listing for the card; unparsable entries are skipped. */\r\nexport function listWeeklyReports(guard: PathGuard, dataDir: string): WeeklyReportMeta[] {\r\n const out: WeeklyReportMeta[] = [];\r\n try {\r\n for (const file of fs.readdirSync(weeklyDirPath(dataDir))) {\r\n if (!/^report-\\d{4}-\\d{2}-\\d{2}\\.json$/.test(file)) continue;\r\n const rep = readWeeklyReport(guard, dataDir, file);\r\n if (!rep) continue;\r\n out.push({ file, start: rep.start, end: rep.end, generatedAt: rep.generatedAt, source: rep.source });\r\n }\r\n } catch { /* no weekly dir yet */ }\r\n return out.sort((a, b) => (a.end < b.end ? 1 : -1));\r\n}\r\n\r\nexport function readWeeklyReport(guard: PathGuard, dataDir: string, file: string): WeeklyReport | null {\r\n if (!/^report-\\d{4}-\\d{2}-\\d{2}\\.json$/.test(file)) return null; // guard.assert would reject ../ anyway\r\n try {\r\n const raw = loadEncryptedText(guard, path.join(weeklyDirPath(dataDir), file));\r\n if (!raw) return null;\r\n const rep = JSON.parse(raw) as WeeklyReport;\r\n if (typeof rep.text !== 'string') return null;\r\n return rep;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** The engine-room prompt: neutral system narrator, fact-bound, ≤250 chars. */\r\nexport function buildWeeklyPrompt(facts: WeeklyFacts): string {\r\n return [\r\n '你是心跳插件的后台报告器。根据下面的本周事实清单,用中文写一份周报正文。',\r\n '规则:',\r\n '- 叙述者自称「心跳」——这是插件后台,不是任何会话里的 agent;绝不能以会话 agent 的身份或人格自称。',\r\n '- 只报告事实清单里有的内容;某一节没有数据就整节跳过,不要编造、不要凑数。',\r\n '- 语气平实中性,不用感叹号;篇幅不超过 250 字。',\r\n '- 待办积压那一节用商量的语气提一句,不催促。',\r\n '- 直接输出周报正文,不要输出解释、标题或 JSON。',\r\n '',\r\n '本周事实(JSON):',\r\n JSON.stringify(facts, null, 1),\r\n ].join('\\n');\r\n}\r\n\r\n/** Deterministic fallback: the facts, plainly stated. Used when the LLM turn\r\n * fails or comes back empty — a plain report beats no report. */\r\nexport function renderTemplateReport(facts: WeeklyFacts): string {\r\n const d = (iso: string): string => iso.slice(0, 10);\r\n const lines: string[] = [`本周(${d(facts.windowStart)} ~ ${d(facts.windowEnd)})`];\r\n lines.push(`表达 ${facts.spoken} 次,静默 ${facts.silent} 次。`);\r\n if (facts.silentTopReasons.length > 0) {\r\n lines.push(`静默主因:${facts.silentTopReasons.map((r) => `${r.reason}(${r.count})`).join('、')}。`);\r\n }\r\n if (facts.profileAdds.length > 0) {\r\n lines.push(`新认识 ${facts.profileAdds.length} 条:${facts.profileAdds.map((a) => a.content).join(';')}。`);\r\n }\r\n if (facts.ledger.added.length > 0 || facts.ledger.done.length > 0) {\r\n const parts: string[] = [];\r\n if (facts.ledger.added.length > 0) parts.push(`新增待办:${facts.ledger.added.join('、')}`);\r\n if (facts.ledger.done.length > 0) parts.push(`已解决:${facts.ledger.done.join('、')}`);\r\n lines.push(`${parts.join(';')}。`);\r\n }\r\n if (facts.ledger.stale.length > 0) {\r\n lines.push(`挂了很久:${facts.ledger.stale.map((s) => `${s.text}(${s.days} 天)`).join('、')}——要删掉还是继续挂着?`);\r\n }\r\n if (facts.seeds.added.length > 0) {\r\n lines.push(`素材池新增 ${facts.seeds.added.length} 条,已消费 ${facts.seeds.consumed.length} 条。`);\r\n }\r\n if (facts.seeds.waiting.length > 0) {\r\n lines.push(`还没聊过的素材:${facts.seeds.waiting.join('、')}。`);\r\n }\r\n if (facts.reports.total > 0) {\r\n const reasonPart = facts.reports.reasons.length > 0\r\n ? `(${facts.reports.reasons.map((r) => `${r.reason} ${r.count} 次`).join('、')})`\r\n : '';\r\n lines.push(`投递报账 ${facts.reports.total} 次:用素材 ${facts.reports.material}、说真心话 ${facts.reports.heartfelt}、没说话 ${facts.reports.silent}${reasonPart}。`);\r\n }\r\n // v1.9.0: unaccounted deliveries are reported even when no report landed at\r\n // all — a silent rise in \"no report\" is exactly what would otherwise hide.\r\n if (facts.reports.missing > 0) {\r\n lines.push(`另有 ${facts.reports.missing} 次投递没等到报账(这部分没算进偏好统计)。`);\r\n }\r\n if (facts.peakHours.length > 0) {\r\n lines.push(`活跃高峰:${facts.peakHours.join('、')}。`);\r\n }\r\n return lines.join('\\n');\r\n}\r\n","// Bundled agent-preset installation (contract C13).\n//\n// Why this exists: the heartbeat agent is created by the host `agents` service,\n// which does NOT go through the session-start preset picker. A bare agent joins\n// no preset, and dsh-agent-presets states the consequence verbatim — \"its tools,\n// prompt sections, and skill catalog resolve against the empty global layer\" —\n// so it cannot even see `web_search`. The preset is therefore not optional, and\n// asking the operator to hand-copy two YAML files was the most error-prone step\n// of setup. The plugin now materialises its own bundled template in the roster's\n// USER root on first run.\n//\n// Two properties keep that safe:\n// 1. An existing preset is NEVER overwritten — the composition file belongs to\n// whoever edited it. Only a missing (or ghost) directory is filled in.\n// 2. The target comes from the roster's OWN roots (`agentPresets.roots`,\n// trust === \"user\") rather than a guessed `~/.dsh`, so `$DSH_HOME` and a\n// configured home are honoured without re-deriving them here.\n//\n// Timing: dsh-agent-presets re-scans the filesystem on every read\n// (`list()` -> `discoverPresets` -> `scanRoot`, no cache), so a directory\n// created here is visible to the very next `mount()` — no restart in between.\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const COMPOSITION_FILE = 'agent.cordis.yml';\nexport const METADATA_FILE = 'preset.yml';\n/** The template this package ships, in `assets/presets/<id>/`. */\nexport const BUNDLED_PRESET_ID = 'heartbeat';\n\n/** One entry of the roster's root list (`AgentPresets.roots`). */\nexport interface PresetRootLike {\n path?: string;\n trust?: string;\n}\n\nexport type PresetInstallAction =\n | 'exists'\n | 'created'\n | 'repaired'\n | 'restored'\n | 'skipped-disabled'\n | 'skipped-custom-id'\n | 'skipped-no-root'\n | 'error';\n\nexport interface PresetInstallResult {\n action: PresetInstallAction;\n id: string;\n /** Where the preset lives (or would live). */\n dir?: string;\n /** Where the bundled template was read from. */\n bundledDir?: string;\n detail?: string;\n}\n\n/**\n * Locate the bundled template directory by walking up from a module URL.\n * Works from both entry points (`dist/index.js` and `dist/cli/index.js`) and\n * from the pnpm copy of an installed package.\n */\nexport function bundledPresetDir(\n moduleUrl: string,\n id: string = BUNDLED_PRESET_ID,\n): string | undefined {\n let dir: string;\n try {\n dir = path.dirname(fileURLToPath(moduleUrl));\n } catch {\n return undefined;\n }\n for (let depth = 0; depth < 5; depth += 1) {\n const candidate = path.join(dir, 'assets', 'presets', id);\n if (fs.existsSync(path.join(candidate, COMPOSITION_FILE))) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return undefined;\n}\n\n/** The roster's user-trust root, which is where locally authored presets live. */\nexport function userPresetRoot(roots?: readonly PresetRootLike[]): string | undefined {\n const found = roots?.find(\n (root) => root?.trust === 'user' && typeof root.path === 'string' && root.path.length > 0,\n );\n return found?.path === undefined ? undefined : path.resolve(found.path);\n}\n\n/**\n * `<dshHome>/.agent-presets` derived from the documented precedence\n * (`$DSH_HOME`, then `~/.dsh`). Only used where no roster is available — the\n * plugin itself prefers {@link userPresetRoot}.\n */\nexport function conventionalUserPresetRoot(\n env: NodeJS.ProcessEnv = process.env,\n home: string = os.homedir(),\n): string {\n const override = env.DSH_HOME?.trim();\n const root = override && override.length > 0 ? override : path.join(home, '.dsh');\n return path.join(root, '.agent-presets');\n}\n\nexport interface InstallPresetOptions {\n /** `import.meta.url` of the calling entry point. */\n moduleUrl: string;\n /** Preset id the heartbeat agent will join (config `agentPreset`). */\n id?: string;\n /** User-trust root reported by the roster. */\n root?: string;\n /** True when the roster answered — then an absent user root is a fact, not a guess. */\n rosterKnown?: boolean;\n /** `false` disables the install (config `installPreset`). */\n enabled?: boolean;\n /** Overwrite an existing composition file with the bundled one. */\n force?: boolean;\n}\n\n/**\n * Materialise the bundled preset in the roster's user root when it is absent.\n * Never destructive: an existing composition file is kept unless `force`.\n */\nexport function installBundledPreset(options: InstallPresetOptions): PresetInstallResult {\n const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;\n if (options.enabled === false) {\n return { action: 'skipped-disabled', id, detail: 'installPreset=false' };\n }\n const bundledDir = bundledPresetDir(options.moduleUrl, BUNDLED_PRESET_ID);\n if (id !== BUNDLED_PRESET_ID) {\n return {\n action: 'skipped-custom-id',\n id,\n ...(bundledDir === undefined ? {} : { bundledDir }),\n detail: `only \"${BUNDLED_PRESET_ID}\" ships with the plugin; \"${id}\" is yours to provide`,\n };\n }\n if (bundledDir === undefined) {\n return {\n action: 'error',\n id,\n detail: 'bundled template not found next to the plugin (assets/presets/heartbeat)',\n };\n }\n const root =\n options.root ?? (options.rosterKnown ? undefined : conventionalUserPresetRoot());\n if (root === undefined) {\n return {\n action: 'skipped-no-root',\n id,\n bundledDir,\n detail: 'the roster mounts no user preset root (includeUserRoot=false)',\n };\n }\n const dir = path.join(root, id);\n const composition = path.join(dir, COMPOSITION_FILE);\n try {\n if (fs.existsSync(composition)) {\n if (options.force !== true) {\n const drifted = !sameBytes(composition, path.join(bundledDir, COMPOSITION_FILE));\n return {\n action: 'exists',\n id,\n dir,\n bundledDir,\n detail: drifted ? 'kept as-is (differs from the bundled template)' : 'kept as-is',\n };\n }\n fs.copyFileSync(path.join(bundledDir, COMPOSITION_FILE), composition);\n return {\n action: 'restored',\n id,\n dir,\n bundledDir,\n detail: 'composition replaced from the bundled template',\n };\n }\n const existed = fs.existsSync(dir);\n fs.mkdirSync(dir, { recursive: true });\n fs.copyFileSync(path.join(bundledDir, COMPOSITION_FILE), composition);\n const metadata = path.join(dir, METADATA_FILE);\n // A directory with a composition but no metadata is legal; only fill a void.\n if (!fs.existsSync(metadata)) fs.copyFileSync(path.join(bundledDir, METADATA_FILE), metadata);\n return {\n action: existed ? 'repaired' : 'created',\n id,\n dir,\n bundledDir,\n detail: existed\n ? 'directory existed without a composition file (it occupied the id as a broken row)'\n : undefined,\n };\n } catch (error) {\n return { action: 'error', id, dir, bundledDir, detail: String(error).slice(0, 200) };\n }\n}\n\n/** One line suitable for the audit log and `ctx.logger`. */\nexport function describeInstall(result: PresetInstallResult): string {\n const where = result.dir === undefined ? '' : ` (${result.dir})`;\n const why = result.detail === undefined ? '' : ` — ${result.detail}`;\n return `preset ${result.id} ${result.action}${where}${why}`;\n}\n\nexport interface PresetStatus {\n id: string;\n dir: string;\n bundledDir?: string;\n installed: boolean;\n compositionMatches: boolean;\n metadataMatches: boolean;\n}\n\n/** Read-only inspection for the CLI (`preset status`). */\nexport function presetStatus(\n moduleUrl: string,\n id: string = BUNDLED_PRESET_ID,\n root: string = conventionalUserPresetRoot(),\n): PresetStatus {\n const dir = path.join(root, id);\n const bundledDir = bundledPresetDir(moduleUrl, id);\n const installed = fs.existsSync(path.join(dir, COMPOSITION_FILE));\n return {\n id,\n dir,\n ...(bundledDir === undefined ? {} : { bundledDir }),\n installed,\n compositionMatches:\n installed && bundledDir !== undefined && sameBytes(path.join(dir, COMPOSITION_FILE), path.join(bundledDir, COMPOSITION_FILE)),\n metadataMatches:\n bundledDir !== undefined &&\n fs.existsSync(path.join(dir, METADATA_FILE)) &&\n sameBytes(path.join(dir, METADATA_FILE), path.join(bundledDir, METADATA_FILE)),\n };\n}\n\nfunction sameBytes(left: string, right: string): boolean {\n try {\n return fs.readFileSync(left).equals(fs.readFileSync(right));\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAOA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEV,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACnD,YAAY,QAAgB,WAAmB;AAC7C,UAAM,4BAA4B,MAAM,kBAAkB,SAAS,IAAI;AACvE,SAAK,OAAO;AAAA,EACd;AACF;AAQO,SAAS,aAAa,QAAwB;AACnD,QAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,MAAI;AACF,WAAO,GAAG,aAAa,GAAG;AAAA,EAC5B,QAAQ;AAGN,UAAM,OAAiB,CAAC;AACxB,QAAI,MAAM;AACV,eAAS;AACP,YAAM,OAAO,KAAK,SAAS,GAAG;AAC9B,YAAM,SAAS,KAAK,QAAQ,GAAG;AAC/B,UAAI,WAAW,KAAK;AAClB,cAAM,IAAI,MAAM,wBAAwB,MAAM,yBAAyB;AAAA,MACzE;AACA,WAAK,KAAK,IAAI;AACd,YAAM;AACN,UAAI;AACF,cAAM,UAAU,GAAG,aAAa,GAAG;AACnC,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,QAAQ,CAAC;AAAA,MAC7C,QAAQ;AACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAOO,SAAS,kBAAkB,gBAAwB,aAA8B;AACtF,QAAM,OAAO,CAAC,MAAc;AAC1B,QAAI,IAAI,KAAK,UAAU,CAAC,EAAE,YAAY;AACtC,QAAI,CAAC,EAAE,SAAS,KAAK,GAAG,EAAG,MAAK,KAAK;AACrC,WAAO;AAAA,EACT;AACA,QAAM,IAAI,KAAK,cAAc;AAC7B,QAAM,IAAI,KAAK,WAAW;AAC1B,SAAO,MAAM,KAAK,EAAE,WAAW,CAAC;AAClC;AAWO,SAAS,gBAAgB,cAAiC;AAC/D,QAAM,YAAY,aAAa,YAAY;AAC3C,QAAM,QAAmB;AAAA,IACvB;AAAA,IACA,MAAM,QAA+B;AACnC,YAAM,QAAQ,aAAa,MAAM;AACjC,aAAO,kBAAkB,WAAW,KAAK,IAAI,QAAQ;AAAA,IACvD;AAAA,IACA,OAAO,QAAwB;AAC7B,YAAM,QAAQ,MAAM,MAAM,MAAM;AAChC,UAAI,UAAU,KAAM,OAAM,IAAI,0BAA0B,QAAQ,SAAS;AACzE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;;;ACxCA,IAAM,OAAO;AAEb,SAAS,cAAc,GAA0C;AAC/D,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAChE;AAEA,SAAS,KAAK,KAAoB;AAChC,QAAM,IAAI,MAAM,WAAW,GAAG,EAAE;AAClC;AAEO,SAAS,aAAa,OAAyC;AACpE,MAAI,CAAC,cAAc,KAAK,EAAG,MAAK,wBAAwB;AACxD,QAAM,IAAI;AACV,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,mBAAmB;AAChD,MAAI,OAAO,GAAG,gBAAgB,YAAY,GAAG,cAAc,KAAK,GAAG,cAAc,MAAM;AACrF,SAAK,qDAAqD;AAAA,EAC5D;AACA,MAAI,OAAO,GAAG,aAAa,UAAW,MAAK,oCAAoC;AAC/E,MAAI,OAAO,GAAG,uBAAuB,UAAW,MAAK,8CAA8C;AACnG,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,cAAc;AAC1C,MAAI,OAAO,EAAE,iBAAiB,YAAY,EAAE,eAAe,EAAG,MAAK,gCAAgC;AACnG,MAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,kBAAkB,EAAG,MAAK,mCAAmC;AAC5G,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,yBAAyB;AACtD,MAAI,OAAO,GAAG,UAAU,YAAY,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,OAAO,GAAG,QAAQ,YAAY,CAAC,KAAK,KAAK,GAAG,GAAG,GAAG;AAC5G,SAAK,sDAAsD;AAAA,EAC7D;AACA,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,gBAAgB;AAC5C,MAAI,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK,EAAE,QAAQ,WAAW,EAAG,MAAK,0CAA0C;AACxG,aAAW,KAAK,EAAE,SAAS;AACzB,QAAI,CAAC,cAAc,CAAC,EAAG,MAAK,wCAAwC;AACpE,QAAI,OAAO,EAAE,UAAU,YAAY,CAAC,KAAK,KAAK,EAAE,KAAK,KAAK,OAAO,EAAE,QAAQ,YAAY,CAAC,KAAK,KAAK,EAAE,GAAG,GAAG;AACxG,WAAK,6DAA6D;AAAA,IACpE;AAAA,EACF;AACA,MAAI,OAAO,EAAE,qBAAqB,YAAY,EAAE,oBAAoB,EAAG,MAAK,qCAAqC;AACjH,MAAI,OAAO,EAAE,qBAAqB,YAAY,EAAE,mBAAmB,EAAG,MAAK,sCAAsC;AACjH,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,eAAe;AAC3C,MAAI,OAAO,EAAE,cAAc,YAAY,EAAE,YAAY,EAAG,MAAK,8BAA8B;AAC3F,MAAI,CAAC,cAAc,EAAE,OAAO,EAAG,MAAK,uBAAuB;AAC3D,aAAW,KAAK,CAAC,QAAQ,UAAU,SAAS,SAAS,GAAY;AAC/D,QAAI,OAAO,EAAE,QAAQ,CAAC,MAAM,SAAU,MAAK,iBAAiB,CAAC,UAAU;AAAA,EACzE;AACA,MAAI,OAAO,EAAE,kBAAkB,SAAU,MAAK,6BAA6B;AAC3E,MAAI,OAAO,EAAE,eAAe,YAAY,EAAE,aAAa,EAAG,MAAK,+BAA+B;AAC9F,MAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,kBAAkB,EAAG,MAAK,oCAAoC;AAC7G,MAAI,CAAC,cAAc,EAAE,YAAY,EAAG,MAAK,4BAA4B;AACrE,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,iBAAiB;AAC9C,QAAM,IAAI,GAAG;AACb,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,+BAA+B;AAC3D,MAAI,OAAO,EAAE,qBAAqB,YAAY,OAAO,EAAE,iBAAiB,SAAU,MAAK,sCAAsC;AAC7H,MAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,eAAe,EAAG,MAAK,mCAAmC;AACxG,MAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,eAAe,EAAG,MAAK,mCAAmC;AACxG,QAAM,KAAK,GAAG;AACd,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,+BAA+B;AAC5D,MAAI,OAAO,GAAG,SAAS,YAAY,OAAO,GAAG,WAAW,YAAY,OAAO,GAAG,WAAW,UAAU;AACjG,SAAK,sCAAsC;AAAA,EAC7C;AACA,MAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,eAAe,EAAG,MAAK,mCAAmC;AACxG,MAAI,OAAO,GAAG,0BAA0B,YAAY,GAAG,wBAAwB,EAAG,MAAK,4CAA4C;AACnI,MAAI,OAAO,GAAG,eAAe,UAAW,MAAK,oCAAoC;AACjF,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,iBAAiB;AAC9C,MAAI,OAAO,GAAG,aAAa,YAAY,GAAG,WAAW,MAAM,GAAG,WAAW,IAAM,MAAK,wCAAwC;AAC5H,MAAI,OAAO,GAAG,YAAY,YAAY,GAAG,UAAU,KAAK,GAAG,UAAU,IAAK,MAAK,qCAAqC;AACpH,MAAI,CAAC,cAAc,EAAE,MAAM,EAAG,MAAK,gBAAgB;AACnD,MAAI,OAAO,EAAE,OAAO,YAAY,UAAW,MAAK,gCAAgC;AAChF,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,mBAAmB;AAC/C,MAAI,OAAO,EAAE,kBAAkB,YAAY,OAAO,EAAE,oBAAoB,SAAU,MAAK,0BAA0B;AACnH;AAGO,SAAS,UAAa,MAAS,UAAsB;AAC1D,MAAI,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,QAAQ,GAAG;AACpD,WAAQ,aAAa,SAAY,OAAQ;AAAA,EAC3C;AACA,QAAM,MAA+B,EAAE,GAAG,KAAK;AAC/C,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAC7C,QAAI,CAAC,IAAI,MAAM,SAAa,KAAiC,CAAC,IAAI,UAAW,KAAiC,CAAC,GAAG,CAAC;AAAA,EACrH;AACA,SAAO;AACT;;;ACrIA,OAAOA,SAAQ;AACf,OAAOC,WAAU;AAIV,IAAM,mBAAmB;AAEzB,SAAS,WAAW,OAAkB,WAAmB,aAA6B;AAC3F,QAAM,cAAcC,MAAK,KAAK,WAAW,aAAa;AACtD,MAAI;AACJ,MAAI;AACF,iBAAa,KAAK,MAAMC,IAAG,aAAa,aAAa,MAAM,CAAC;AAAA,EAC9D,SAAS,GAAG;AACV,UAAM,IAAI,MAAM,gCAAgC,WAAW,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,EAC7E;AACA,eAAa,UAAU;AAEvB,QAAM,WAAW,MAAM,OAAOD,MAAK,KAAK,aAAa,gBAAgB,CAAC;AACtE,MAAI,SAAiB;AACrB,MAAIC,IAAG,WAAW,QAAQ,GAAG;AAC3B,QAAI;AACF,YAAM,UAAmB,KAAK,MAAMA,IAAG,aAAa,UAAU,MAAM,CAAC;AACrE,eAAS,UAAU,YAAY,OAAO;AAAA,IACxC,SAAS,GAAG;AACV,YAAM,IAAI,MAAM,oCAAoC,QAAQ,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,IAC9E;AAAA,EACF;AACA,eAAa,MAAM;AACnB,SAAO;AACT;AAQO,SAAS,iBAAiB,OAAkB,aAAqB,OAAyD;AAC/H,QAAM,WAAW,MAAM,OAAOD,MAAK,KAAK,aAAa,gBAAgB,CAAC;AACtE,MAAI,OAAgC,CAAC;AACrC,MAAI;AACF,WAAO,KAAK,MAAMC,IAAG,aAAa,UAAU,MAAM,CAAC;AACnD,QAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,EAAG,QAAO,CAAC;AAAA,EACxE,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,UAAU,MAAM,KAAK;AACpC,EAAAA,IAAG,UAAUD,MAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,EAAAC,IAAG,cAAc,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,MAAM,MAAM;AACzE,SAAO;AACT;;;AClDA,OAAOC,WAAU;AACjB,SAAS,kBAAkB;AAI3B,IAAM,SAAS;AAUR,SAAS,eAAe,SAAyB;AACtD,SAAOC,MAAK,KAAK,SAAS,WAAW;AACvC;AAEA,IAAM,UAAU;AAEhB,SAAS,YAAY,GAAwB;AAC3C,SAAO,MAAM,EAAE,IAAI,IAAI,EAAE,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI;AACjE;AAEO,SAAS,WAAW,OAAkB,MAA8E;AACzH,QAAM,MAAM,SAAS,OAAO,MAAM,kBAAQ;AAC1C,QAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,QAAM,UAAyB,CAAC;AAChC,QAAM,WAAqB,CAAC;AAC5B,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,QAAQ,KAAK,IAAI;AAC3B,QAAI,GAAG;AACL,cAAQ,KAAK,EAAE,MAAM,EAAE,CAAC,GAAI,MAAM,EAAE,CAAC,GAAI,QAAQ,EAAE,CAAC,GAAsB,IAAI,EAAE,CAAC,GAAI,MAAM,EAAE,CAAC,EAAG,CAAC;AAAA,IACpG;AACA,aAAS,KAAK,IAAI;AAAA,EACpB;AACA,SAAO,EAAE,QAAQ,MAAM,CAAC,KAAK,kBAAQ,SAAS,SAAS;AACzD;AAEO,SAAS,YAAY,OAAkB,MAAc,MAAc,MAAM,KAAK,IAAI,GAAgB;AACvG,QAAM,cAAc,KAAK,KAAK;AAC9B,MAAI,CAAC,YAAa,OAAM,IAAI,MAAM,gCAAgC;AAClE,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,QAAM,MAAM,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AACpD,QAAM,QAAqB;AAAA,IACzB,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC;AAAA,IAC3B,MAAM,GAAG,EAAE,YAAY,CAAC,IAAI,IAAI,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,IACrE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,IAAI,IAAI,EAAE,WAAW,CAAC,CAAC;AAAA,IACjD,QAAQ;AAAA,IACR,MAAM,YAAY,QAAQ,UAAU,GAAG;AAAA,EACzC;AACA,QAAM,EAAE,SAAS,IAAI,WAAW,OAAO,IAAI;AAC3C,WAAS,KAAK,YAAY,KAAK,CAAC;AAChC,YAAU,OAAO,MAAM,SAAS,KAAK,IAAI,EAAE,QAAQ,QAAQ,IAAI,CAAC;AAChE,SAAO;AACT;AAMO,SAAS,SAAS,OAAkB,MAAc,KAAa,MAAM,KAAK,IAAI,GAAuB;AAC1G,QAAM,EAAE,UAAU,QAAQ,IAAI,WAAW,OAAO,IAAI;AACpD,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,SAAS,GAAG,EAAE;AAChG,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,QAAM,MAAM,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AACpD,QAAM,WAAW,GAAG,EAAE,YAAY,CAAC,IAAI,IAAI,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,QAAQ,CAAC,CAAC;AAChF,QAAM,MAAM,SAAS,IAAI,CAAC,SAAS;AACjC,QAAI,KAAK,SAAS,IAAI,OAAO,EAAE,IAAI,GAAG;AACpC,aAAO,MAAM,QAAQ,IAAI,IAAI,EAAE,SAAS,CAAC,CAAC,IAAI,IAAI,EAAE,WAAW,CAAC,CAAC,YAAY,OAAO,EAAE,KAAK,OAAO,IAAI;AAAA,IACxG;AACA,WAAO;AAAA,EACT,CAAC;AACD,YAAU,OAAO,MAAM,IAAI,KAAK,IAAI,EAAE,QAAQ,QAAQ,IAAI,CAAC;AAC3D,SAAO;AACT;AAGO,SAAS,YAAY,OAAkB,MAAc,MAAM,KAAK,IAAI,GAAkB;AAC3F,QAAM,EAAE,QAAQ,IAAI,WAAW,OAAO,IAAI;AAC1C,SAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE,KAAK,CAAC,GAAG,MAAO,EAAE,OAAO,EAAE,OAAO,KAAK,CAAE;AAC7F;AAGO,SAAS,iBAAiB,OAAkB,MAAc,MAAc,MAAM,KAAK,IAAI,GAAkB;AAC9G,QAAM,SAAS,IAAI,KAAK,MAAM,OAAO,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACtE,SAAO,YAAY,OAAO,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,MAAM;AACrE;;;AChFA,OAAOC,SAAQ;AACf,OAAOC,WAAU;;;ACCjB,OAAOC,WAAU;AAIjB,IAAMC,UAAS;AAER,IAAM,QAAQ;AACd,IAAM,QAAQ;AAEd,IAAM,iBAAiB;AACvB,IAAM,mBAAmB;AAEzB,IAAM,uBAAuB;AAe7B,SAAS,mBAAmB,SAAyB;AAC1D,SAAOC,MAAK,KAAK,SAAS,iBAAiB;AAC7C;AAGA,SAAS,IAAI,GAAoB;AAC/B,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC5C;AAMA,SAAS,kBAAkB,KAAgC;AACzD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,IAAI;AACV,MAAI,CAAC,OAAO,SAAS,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,OAAO,SAAS,OAAO,EAAE,OAAO,CAAC,EAAG,QAAO;AACzF,QAAM,YAAY,KAAK,MAAM,IAAI,EAAE,SAAS,CAAC;AAC7C,SAAO;AAAA,IACL;AAAA,IACA,SAAS,KAAK,IAAI,WAAW,KAAK,MAAM,IAAI,EAAE,OAAO,CAAC,CAAC;AAAA,IACvD,iBAAiB,IAAI,EAAE,eAAe;AAAA,IACtC,eAAe,IAAI,EAAE,aAAa;AAAA,EACpC;AACF;AAEO,SAAS,eAAe,OAAkB,MAAyB;AACxE,QAAM,MAAM,SAA6B,OAAO,IAAI;AACpD,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,CAAC,IAAI,UAAU,OAAO,IAAI,WAAW,UAAU;AACpF,WAAO,EAAE,SAAS,GAAG,QAAQ,CAAC,GAAG,WAAW,EAAE;AAAA,EAChD;AACA,QAAM,SAAoC,CAAC;AAC3C,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AACnD,UAAM,IAAI,kBAAkB,GAAG;AAC/B,QAAI,OAAO,EAAG,QAAO,GAAG,IAAI;AAAA,EAC9B;AACA,QAAM,YAAY,OAAO,IAAI,SAAS;AACtC,SAAO,EAAE,SAAS,GAAG,QAAQ,WAAW,OAAO,SAAS,SAAS,IAAI,YAAY,EAAE;AACrF;AAEO,SAAS,eAAe,OAAkB,MAAc,OAAwB;AACrF,WAAS,OAAO,MAAM,KAAK;AAC7B;AAKO,SAAS,eACd,OACA,MACA,MACW;AACX,QAAM,QAAQ,eAAe,OAAO,IAAI;AACxC,QAAM,UAAU,IAAI,IAAI,KAAK,aAAa;AAC1C,aAAW,SAAS,KAAK,eAAe;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,IAAI,MAAM,OAAO,KAAK,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,iBAAiB,GAAG,eAAe,EAAE;AAClG,MAAE,aAAa;AACf,MAAE,kBAAkB,KAAK;AACzB,QAAI,QAAQ,IAAI,KAAK,GAAG;AACtB,QAAE,WAAW;AACb,QAAE,gBAAgB,KAAK;AAAA,IACzB;AACA,UAAM,OAAO,KAAK,IAAI;AAAA,EACxB;AACA,QAAM,YAAY,KAAK;AACvB,iBAAe,OAAO,MAAM,KAAK;AACjC,SAAO;AACT;AAGO,SAAS,QAAQ,GAAc,KAAqD;AACzF,QAAM,WAAW,KAAK,IAAI,GAAG,MAAM,EAAE,eAAe,IAAID;AACxD,QAAM,WAAW,KAAK,IAAI,GAAG,MAAM,EAAE,aAAa,IAAIA;AACtD,QAAM,QAAQ,KAAK,IAAI,KAAK,WAAW,oBAAoB;AAC3D,QAAM,QAAQ,KAAK,IAAI,KAAK,WAAW,oBAAoB;AAC3D,SAAO,EAAE,WAAW,EAAE,YAAY,OAAO,SAAS,EAAE,UAAU,MAAM;AACtE;AAUO,SAAS,iBAAiB,OAAkB,OAAe,KAAqB;AACrF,QAAM,IAAI,MAAM,OAAO,KAAK;AAC5B,QAAM,OAAO,KACR,MAAM;AACL,UAAM,IAAI,QAAQ,GAAG,GAAG;AACxB,YAAQ,EAAE,UAAU,mBAAmB,EAAE,YAAY;AAAA,EACvD,GAAG,IACH,iBAAiB;AACrB,SAAO,KAAK,IAAI,OAAO,KAAK,IAAI,OAAO,QAAQ,iBAAiB,iBAAiB,CAAC;AACpF;;;ADrHA,IAAM,oBAAoB,IAAI;AAC9B,IAAM,KAAK,EAAE,cAAc,iDAAiD;AAsCrE,SAAS,mBAAgC;AAC9C,SAAO,EAAE,SAAS,CAAC,GAAG,eAAe,GAAG,QAAQ,EAAE,cAAc,CAAC,GAAG,YAAY,CAAC,GAAG,gBAAgB,GAAG,aAAa,CAAC,EAAE,EAAE;AAC3H;AAEO,SAAS,gBAAgB,OAA+B;AAC7D,SAAOE,MAAK,KAAK,MAAM,SAAS,aAAa;AAC/C;AAEA,SAAS,aAAgB,MAAc,UAAgB;AACrD,MAAI;AACF,WAAO,KAAK,MAAMC,IAAG,aAAa,MAAM,MAAM,CAAC;AAAA,EACjD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,cAAc,OAAwC;AACpE,QAAM,WAAWD,MAAK,KAAK,MAAM,aAAa,gBAAgB;AAC9D,MAAIC,IAAG,WAAW,QAAQ,EAAG,QAAO,aAA8B,UAAU,EAAE,WAAW,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC;AAC5G,SAAO,aAA8BD,MAAK,KAAK,MAAM,WAAW,gBAAgB,GAAG,EAAE,WAAW,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC;AACrH;AAEO,SAAS,cAAc,OAAwC;AACpE,QAAM,WAAWA,MAAK,KAAK,MAAM,aAAa,gBAAgB;AAC9D,MAAIC,IAAG,WAAW,QAAQ,EAAG,QAAO,aAA8B,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC;AAC3F,SAAO,aAA8BD,MAAK,KAAK,MAAM,WAAW,gBAAgB,GAAG,EAAE,SAAS,CAAC,EAAE,CAAC;AACpG;AAEA,SAAS,UAAU,OAAkB,OAAoC;AACvE,SAAO,SAAsB,OAAO,gBAAgB,KAAK,CAAC,KAAK,iBAAiB;AAClF;AAMA,eAAe,SAAS,SAAoB,MAAiF;AAC3H,QAAM,IAAI,MAAM,QAAQ,8BAA8B,IAAI,WAAW,EAAE,SAAS,GAAG,CAAC;AACpF,MAAI,CAAC,EAAE,GAAI,OAAM,IAAI,MAAM,OAAO,EAAE,MAAM,EAAE;AAC5C,QAAM,IAAI,MAAM,EAAE,KAAK;AACvB,MAAI,CAAC,EAAE,QAAS,OAAM,IAAI,MAAM,iBAAiB;AACjD,SAAO,EAAE,SAAS,EAAE,SAAS,MAAM,OAAO,EAAE,OAAO,GAAG;AACxD;AAEA,eAAe,YAAY,SAAoB,MAAiF;AAC9H,QAAM,IAAI,MAAM,QAAQ,gCAAgC,IAAI,oBAAoB;AAAA,IAC9E,SAAS,EAAE,GAAG,IAAI,QAAQ,8BAA8B;AAAA,EAC1D,CAAC;AACD,MAAI,EAAE,WAAW,IAAK,QAAO;AAC7B,MAAI,CAAC,EAAE,GAAI,OAAM,IAAI,MAAM,MAAM,EAAE,MAAM,EAAE;AAC3C,QAAM,IAAI,MAAM,EAAE,KAAK;AACvB,MAAI,CAAC,EAAE,SAAU,OAAM,IAAI,MAAM,YAAY;AAC7C,SAAO,EAAE,SAAS,EAAE,UAAU,MAAM,MAAM,EAAE,QAAQ,IAAI,OAAO,EAAE,QAAQ,GAAG;AAC9E;AASA,eAAsB,eACpB,OACA,OACA,OAAsD,CAAC,GACvD,MAAM,KAAK,IAAI,GACO;AACtB,QAAM,UAAU,KAAK,WAAY,WAAW;AAC5C,QAAM,QAAQ,UAAU,OAAO,KAAK;AACpC,MAAI,KAAK,eAAe,QAAQ,MAAM,MAAM,gBAAgB,mBAAmB;AAC7E,WAAO,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,GAAG,SAAS,EAAE;AAAA,EAC7C;AACA,QAAM,YAAY,cAAc,KAAK;AACrC,QAAM,SAAsB,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,GAAG,SAAS,EAAE;AAChE,aAAW,KAAK,UAAU,WAAW,CAAC,GAAG;AACvC,WAAO,WAAW;AAClB,QAAI;AACF,YAAM,OAAO,EAAE,SAAS,SAAS,EAAE,OAAO,MAAM,SAAS,SAAS,EAAE,IAAI,IACpE,EAAE,SAAS,YAAY,EAAE,OAAO,MAAM,YAAY,SAAS,EAAE,IAAI,IACjE;AACJ,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,MAAM,QAAQ,EAAE,EAAE;AAC/B,UAAI,QAAQ,KAAK,SAAS,KAAK,MAAM;AACnC,cAAM,QAAQ,KAAK,QAAQ,SAAI,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,WAAM;AAC5D,eAAO,MAAM,KAAK;AAAA,UAChB,MAAM,GAAG,EAAE,QAAQ,EAAE,EAAE,4BAAQ,KAAK,OAAO,OAAO,KAAK,OAAO,GAAG,KAAK;AAAA,UACtE,OAAO,SAAS,EAAE,EAAE;AAAA,UACpB,KAAK;AAAA,UACL,QAAQ;AAAA,UACR,YAAY;AAAA,QACd,CAAC;AAAA,MACH;AACA,YAAM,QAAQ,EAAE,EAAE,IAAI;AAAA,IACxB,SAAS,GAAG;AACV,aAAO,OAAO,KAAK,GAAG,EAAE,EAAE,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,IAC5C;AAAA,EACF;AACA,QAAM,gBAAgB;AACtB,WAAS,OAAO,gBAAgB,KAAK,GAAG,KAAK;AAC7C,SAAO;AACT;AAUO,SAAS,eAAe,KAAW,SAA0D;AAClG,QAAM,KAAK,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW;AAChD,aAAW,KAAK,SAAS;AACvB,UAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AAC9C,UAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AAC5C,QAAI,MAAM,KAAM,KAAK,MAAO,MAAM,KAAM,KAAK,GAAK,QAAO,GAAG,EAAE,KAAK,IAAI,EAAE,GAAG;AAAA,EAC9E;AACA,SAAO;AACT;AAEA,SAAS,WAAW,OAAoB,OAAe,cAAsB,KAAsB;AACjG,QAAM,OAAO,MAAM,OAAO,aAAa,KAAK,KAAK;AACjD,SAAO,OAAO,MAAM,eAAe;AACrC;AAaO,IAAM,kBAAkB;AAExB,SAAS,UACd,OACA,WACA,KACA,MACe;AACf,QAAM,KAAK,UAAU,aAAa,CAAC;AACnC,QAAM,WAAW,GAAG,uBAAuB;AAC3C,QAAM,QAAQ,UAAU,aAAa,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,OAAO,GAAG,UAAU,GAAG,CAAC;AAC3F,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,CAAC,MAAM;AACT,SAAK,KAAK,CAAC,GAAG,OAAO,MAAM,OAAO,aAAa,CAAC,KAAK,MAAM,MAAM,OAAO,aAAa,CAAC,KAAK,EAAE;AAC7F,WAAO,KAAK,CAAC;AAAA,EACf;AACA,QAAM,MAAM,CAAC,MAAsB,OAAO,MAAM,OAAO,aAAa,CAAC,KAAK;AAC1E,QAAM,UAAU,KAAK,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,kBAAkB,KAAU;AACzE,MAAI,QAAQ,SAAS,GAAG;AACtB,YAAQ,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;AACtC,WAAO,QAAQ,CAAC;AAAA,EAClB;AACA,QAAM,SAAS,CAAC,MAAsB,IAAI,CAAC,IAAI,iBAAiB,MAAM,GAAG,GAAG;AAC5E,OAAK,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC;AACzC,SAAO,KAAK,CAAC;AACf;AAGO,SAAS,aACd,OACA,OACA,QACA,MAAM,oBAAI,KAAK,GACD;AACd,QAAM,QAAQ,UAAU,OAAO,KAAK;AACpC,QAAM,YAAY,cAAc,KAAK;AACrC,QAAM,UAAU,UAAU,WAAW,SAAS,SAC1C,UAAU,UAAU,UACnB,OAAO,OAAO;AACnB,QAAM,MAAM,eAAe,KAAK,OAAO;AACvC,MAAI,CAAC,KAAK;AACR,UAAM,KAAK,GAAG,OAAO,IAAI,SAAS,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,IAAI,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAClG,WAAO,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,cAAc,EAAE,IAAI;AAAA,EAClE;AACA,QAAM,SAAS,OAAO,OAAO,mBAAmB;AAChD,MAAI,IAAI,QAAQ,IAAI,MAAM,OAAO,iBAAiB,QAAQ;AACxD,WAAO,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,eAAe;AAAA,EAC7D;AACA,QAAM,QAAQ,UAAU,OAAO,WAAW,IAAI,QAAQ,GAAG,eAAe,OAAO,mBAAmB,MAAM,OAAO,CAAC,CAAC;AACjH,MAAI,CAAC,MAAO,QAAO,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,WAAW;AACnE,SAAO,EAAE,OAAO,OAAO,GAAG,KAAK,sBAAY,SAAS,KAAK;AAC3D;AAQO,SAAS,eACd,OACA,OACA,OACA,MAAM,KAAK,IAAI,GACf,OAA6B,CAAC,GAC2B;AACzD,QAAM,QAAQ,UAAU,OAAO,KAAK;AACpC,QAAM,OAAO,aAAa,KAAK,IAAI;AACnC,QAAM,OAAO,WAAW,KAAK,KAAK,MAAM,OAAO,WAAW,KAAK,KAAK,KAAK;AACzE,QAAM,OAAO,iBAAiB;AAC9B,MAAI;AACJ,MAAI,KAAK,QAAQ;AACf,UAAM,QAAQ,YAAY,IAAI,KAAK,GAAG,CAAC;AACvC,UAAM,OAAO,cAAc,MAAM,OAAO,eAAe,CAAC;AACxD,UAAM,OAAO,YAAY,KAAK,KAAK,MAAM,OAAO,YAAY,KAAK,KAAK,KAAK;AAC3E,mBAAe,MAAM,OAAO,YAAY,KAAK;AAAA,EAC/C;AACA,WAAS,OAAO,gBAAgB,KAAK,GAAG,KAAK;AAC7C,SAAO,EAAE,OAAO,OAAO,MAAM,OAAO,WAAW,KAAK,GAAI,GAAI,iBAAiB,SAAY,CAAC,IAAI,EAAE,aAAa,EAAG;AAClH;AAGA,SAAS,YAAY,GAAiB;AACpC,QAAM,IAAI,EAAE,YAAY;AACxB,QAAM,IAAI,OAAO,EAAE,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AAClD,QAAM,MAAM,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AAC/C,SAAO,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG;AACzB;AAOO,IAAM,yBAAyB;AAC/B,IAAM,qBAAqB;AAU3B,SAAS,mBACd,OACA,OACA,QACA,MAAM,oBAAI,KAAK,GACD;AACd,QAAM,QAAQ,UAAU,OAAO,KAAK;AACpC,QAAM,QAAQ,YAAY,GAAG;AAC7B,QAAM,eAAe,MAAM,OAAO,cAAc,KAAK,KAAK;AAC1D,QAAM,aAAa,YAAY,SAAS,OAAO,cAAc,MAAM,OAAO,CAAC,CAAC,EACzE,OAAO,CAAC,MAAM,kBAAkB,EAAE,QAAQ,MAAM,OAAO,EAAE;AAC5D,MAAI,gBAAgB,oBAAoB;AACtC,WAAO,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,oBAAoB,YAAY,KAAK,YAAY,aAAa;AAAA,EAC5G;AACA,MAAI,aAAa,wBAAwB;AACvC,WAAO,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,kBAAkB,UAAU,KAAK,YAAY,aAAa;AAAA,EACxG;AACA,QAAM,YAAY,cAAc,KAAK;AACrC,QAAM,QAAQ,UAAU,OAAO,WAAW,IAAI,QAAQ,GAAG,eAAe,OAAO,mBAAmB,MAAM,OAAO,CAAC,CAAC;AACjH,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,YAAY,YAAY,aAAa;AAAA,EACnF;AACA,SAAO,EAAE,OAAO,OAAO,GAAG,KAAK,sBAAY,SAAS,MAAM,YAAY,aAAa;AACrF;AAEO,SAAS,aAAa,OAAkB,OAAoC;AACjF,SAAO,UAAU,OAAO,KAAK;AAC/B;;;AExUA,SAAS,iBAAiB;AAC1B,OAAOE,WAAU;AAGjB,SAAS,UAAU,OAAuB,MAAiD;AACzF,QAAM,IAAI;AAAA,IAAU;AAAA,IAClB,CAAC,cAAc,oBAAoB,UAAU,SAASA,MAAK,KAAK,MAAM,WAAW,YAAY,GAAG,GAAG,IAAI;AAAA,IACvG,EAAE,SAAS,KAAQ,UAAU,OAAO;AAAA,EAAC;AACvC,SAAO,EAAE,QAAQ,EAAE,UAAU,IAAI,KAAK,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE;AACpF;AAGO,SAAS,iBAAiB,OAAgC;AAC/D,QAAM,QAAQ,UAAU,OAAO,CAAC,QAAQ,CAAC;AACzC,MAAI,kBAAkB,KAAK,MAAM,GAAG,EAAG,QAAO;AAC9C,QAAM,MAAM,UAAU,OAAO,CAAC,eAAe,CAAC;AAC9C,SAAO,IAAI,WAAW;AACxB;AAMO,SAAS,mBAAmB,OAAgC;AACjE,QAAM,IAAI,UAAU,OAAO,CAAC,UAAU,aAAa,YAAY,0BAAM,CAAC;AACtE,SAAO,EAAE,WAAW,KAAK,aAAa,KAAK,EAAE,GAAG;AAClD;AAIO,SAAS,oBAAoB,OAAgC;AAClE,QAAM,IAAI,UAAU,OAAO,CAAC,UAAU,4BAAQ,YAAY,4CAAS,CAAC;AACpE,SAAO,EAAE,WAAW,KAAK,aAAa,KAAK,EAAE,GAAG;AAClD;;;AC9BA,OAAOC,SAAQ;AACf,OAAOC,WAAU;AAMV,IAAM,qBAAqB,IAAI;AAW/B,SAAS,cAAc,SAAyB;AACrD,SAAOC,MAAK,KAAK,SAAS,QAAQ;AACpC;AAEA,SAAS,cAAc,SAAyB;AAC9C,SAAOA,MAAK,KAAK,cAAc,OAAO,GAAG,YAAY;AACvD;AAEA,SAAS,eAAe,QAAwB;AAC9C,SAAO,UAAU,OAAO,MAAM,GAAG,EAAE,CAAC;AACtC;AAGO,SAAS,sBAAsB,OAAkB,SAAyB;AAC/E,MAAI;AACF,UAAM,MAAM,KAAK,MAAMC,IAAG,aAAa,MAAM,OAAO,cAAc,OAAO,CAAC,GAAG,MAAM,CAAC;AACpF,WAAO,OAAO,IAAI,eAAe,KAAK;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,UAAU,OAAkB,SAAiB,MAAM,KAAK,IAAI,GAAY;AACtF,QAAM,OAAO,sBAAsB,OAAO,OAAO;AACjD,SAAO,MAAM,QAAQ;AACvB;AAOO,SAAS,mBAAmB,OAAkB,SAAiB,MAAM,KAAK,IAAI,GAAY;AAC/F,MAAI,sBAAsB,OAAO,OAAO,IAAI,EAAG,QAAO;AACtD,EAAAA,IAAG,UAAU,cAAc,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,sBAAoB,cAAc,OAAO,GAAG,EAAE,iBAAiB,IAAI,CAAC;AACpE,SAAO;AACT;AAGO,SAAS,iBAAiB,OAAkB,SAAiB,QAA8B;AAChG,QAAM,MAAM,cAAc,OAAO;AACjC,EAAAA,IAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,OAAOD,MAAK,KAAK,KAAK,eAAe,OAAO,GAAG,CAAC;AACtD,oBAAkB,OAAO,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC9D,sBAAoB,cAAc,OAAO,GAAG,EAAE,iBAAiB,KAAK,MAAM,OAAO,WAAW,EAAE,CAAC;AAC/F,SAAO;AACT;AAWO,SAAS,kBAAkB,OAAkB,SAAqC;AACvF,QAAM,MAA0B,CAAC;AACjC,MAAI;AACF,eAAW,QAAQC,IAAG,YAAY,cAAc,OAAO,CAAC,GAAG;AACzD,UAAI,CAAC,mCAAmC,KAAK,IAAI,EAAG;AACpD,YAAM,MAAM,iBAAiB,OAAO,SAAS,IAAI;AACjD,UAAI,CAAC,IAAK;AACV,UAAI,KAAK,EAAE,MAAM,OAAO,IAAI,OAAO,KAAK,IAAI,KAAK,aAAa,IAAI,aAAa,QAAQ,IAAI,OAAO,CAAC;AAAA,IACrG;AAAA,EACF,QAAQ;AAAA,EAA0B;AAClC,SAAO,IAAI,KAAK,CAAC,GAAG,MAAO,EAAE,MAAM,EAAE,MAAM,IAAI,EAAG;AACpD;AAEO,SAAS,iBAAiB,OAAkB,SAAiB,MAAmC;AACrG,MAAI,CAAC,mCAAmC,KAAK,IAAI,EAAG,QAAO;AAC3D,MAAI;AACF,UAAM,MAAM,kBAAkB,OAAOD,MAAK,KAAK,cAAc,OAAO,GAAG,IAAI,CAAC;AAC5E,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,QAAI,OAAO,IAAI,SAAS,SAAU,QAAO;AACzC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,kBAAkB,OAA4B;AAC5D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,EAC/B,EAAE,KAAK,IAAI;AACb;AAIO,SAAS,qBAAqB,OAA4B;AAC/D,QAAM,IAAI,CAAC,QAAwB,IAAI,MAAM,GAAG,EAAE;AAClD,QAAM,QAAkB,CAAC,qBAAM,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC,QAAG;AAC9E,QAAM,KAAK,gBAAM,MAAM,MAAM,6BAAS,MAAM,MAAM,eAAK;AACvD,MAAI,MAAM,iBAAiB,SAAS,GAAG;AACrC,UAAM,KAAK,iCAAQ,MAAM,iBAAiB,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,SAAI,EAAE,KAAK,QAAG,EAAE,KAAK,QAAG,CAAC,QAAG;AAAA,EAC9F;AACA,MAAI,MAAM,YAAY,SAAS,GAAG;AAChC,UAAM,KAAK,sBAAO,MAAM,YAAY,MAAM,gBAAM,MAAM,YAAY,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,QAAG,CAAC,QAAG;AAAA,EACtG;AACA,MAAI,MAAM,OAAO,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,SAAS,GAAG;AACjE,UAAM,QAAkB,CAAC;AACzB,QAAI,MAAM,OAAO,MAAM,SAAS,EAAG,OAAM,KAAK,iCAAQ,MAAM,OAAO,MAAM,KAAK,QAAG,CAAC,EAAE;AACpF,QAAI,MAAM,OAAO,KAAK,SAAS,EAAG,OAAM,KAAK,2BAAO,MAAM,OAAO,KAAK,KAAK,QAAG,CAAC,EAAE;AACjF,UAAM,KAAK,GAAG,MAAM,KAAK,QAAG,CAAC,QAAG;AAAA,EAClC;AACA,MAAI,MAAM,OAAO,MAAM,SAAS,GAAG;AACjC,UAAM,KAAK,iCAAQ,MAAM,OAAO,MAAM,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,SAAI,EAAE,IAAI,eAAK,EAAE,KAAK,QAAG,CAAC,0EAAc;AAAA,EACpG;AACA,MAAI,MAAM,MAAM,MAAM,SAAS,GAAG;AAChC,UAAM,KAAK,kCAAS,MAAM,MAAM,MAAM,MAAM,mCAAU,MAAM,MAAM,SAAS,MAAM,eAAK;AAAA,EACxF;AACA,MAAI,MAAM,MAAM,QAAQ,SAAS,GAAG;AAClC,UAAM,KAAK,mDAAW,MAAM,MAAM,QAAQ,KAAK,QAAG,CAAC,QAAG;AAAA,EACxD;AACA,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,UAAM,aAAa,MAAM,QAAQ,QAAQ,SAAS,IAC9C,SAAI,MAAM,QAAQ,QAAQ,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,KAAK,SAAI,EAAE,KAAK,QAAG,CAAC,WAC1E;AACJ,UAAM,KAAK,4BAAQ,MAAM,QAAQ,KAAK,mCAAU,MAAM,QAAQ,QAAQ,kCAAS,MAAM,QAAQ,SAAS,4BAAQ,MAAM,QAAQ,MAAM,GAAG,UAAU,QAAG;AAAA,EACpJ;AAGA,MAAI,MAAM,QAAQ,UAAU,GAAG;AAC7B,UAAM,KAAK,gBAAM,MAAM,QAAQ,OAAO,iIAAwB;AAAA,EAChE;AACA,MAAI,MAAM,UAAU,SAAS,GAAG;AAC9B,UAAM,KAAK,iCAAQ,MAAM,UAAU,KAAK,QAAG,CAAC,QAAG;AAAA,EACjD;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;;;ACnJA,OAAOE,SAAQ;AACf,OAAO,QAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,qBAAqB;AAEvB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AAEtB,IAAM,oBAAoB;AAiC1B,SAAS,iBACd,WACA,KAAa,mBACO;AACpB,MAAI;AACJ,MAAI;AACF,UAAMA,MAAK,QAAQ,cAAc,SAAS,CAAC;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG;AACzC,UAAM,YAAYA,MAAK,KAAK,KAAK,UAAU,WAAW,EAAE;AACxD,QAAID,IAAG,WAAWC,MAAK,KAAK,WAAW,gBAAgB,CAAC,EAAG,QAAO;AAClE,UAAM,SAASA,MAAK,QAAQ,GAAG;AAC/B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AACA,SAAO;AACT;AAGO,SAAS,eAAe,OAAuD;AACpF,QAAM,QAAQ,OAAO;AAAA,IACnB,CAAC,SAAS,MAAM,UAAU,UAAU,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS;AAAA,EAC1F;AACA,SAAO,OAAO,SAAS,SAAY,SAAYA,MAAK,QAAQ,MAAM,IAAI;AACxE;AAOO,SAAS,2BACd,MAAyB,QAAQ,KACjC,OAAe,GAAG,QAAQ,GAClB;AACR,QAAM,WAAW,IAAI,UAAU,KAAK;AACpC,QAAM,OAAO,YAAY,SAAS,SAAS,IAAI,WAAWA,MAAK,KAAK,MAAM,MAAM;AAChF,SAAOA,MAAK,KAAK,MAAM,gBAAgB;AACzC;AAqBO,SAAS,qBAAqB,SAAoD;AACvF,QAAM,KAAK,QAAQ,MAAM,QAAQ,GAAG,SAAS,IAAI,QAAQ,KAAK;AAC9D,MAAI,QAAQ,YAAY,OAAO;AAC7B,WAAO,EAAE,QAAQ,oBAAoB,IAAI,QAAQ,sBAAsB;AAAA,EACzE;AACA,QAAM,aAAa,iBAAiB,QAAQ,WAAW,iBAAiB;AACxE,MAAI,OAAO,mBAAmB;AAC5B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;AAAA,MACjD,QAAQ,SAAS,iBAAiB,6BAA6B,EAAE;AAAA,IACnE;AAAA,EACF;AACA,MAAI,eAAe,QAAW;AAC5B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,OACJ,QAAQ,SAAS,QAAQ,cAAc,SAAY,2BAA2B;AAChF,MAAI,SAAS,QAAW;AACtB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,MAAMA,MAAK,KAAK,MAAM,EAAE;AAC9B,QAAM,cAAcA,MAAK,KAAK,KAAK,gBAAgB;AACnD,MAAI;AACF,QAAID,IAAG,WAAW,WAAW,GAAG;AAC9B,UAAI,QAAQ,UAAU,MAAM;AAC1B,cAAM,UAAU,CAAC,UAAU,aAAaC,MAAK,KAAK,YAAY,gBAAgB,CAAC;AAC/E,eAAO;AAAA,UACL,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,UAAU,mDAAmD;AAAA,QACvE;AAAA,MACF;AACA,MAAAD,IAAG,aAAaC,MAAK,KAAK,YAAY,gBAAgB,GAAG,WAAW;AACpE,aAAO;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF;AACA,UAAM,UAAUD,IAAG,WAAW,GAAG;AACjC,IAAAA,IAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,IAAAA,IAAG,aAAaC,MAAK,KAAK,YAAY,gBAAgB,GAAG,WAAW;AACpE,UAAM,WAAWA,MAAK,KAAK,KAAK,aAAa;AAE7C,QAAI,CAACD,IAAG,WAAW,QAAQ,EAAG,CAAAA,IAAG,aAAaC,MAAK,KAAK,YAAY,aAAa,GAAG,QAAQ;AAC5F,WAAO;AAAA,MACL,QAAQ,UAAU,aAAa;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,UACJ,sFACA;AAAA,IACN;AAAA,EACF,SAAS,OAAO;AACd,WAAO,EAAE,QAAQ,SAAS,IAAI,KAAK,YAAY,QAAQ,OAAO,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;AAAA,EACrF;AACF;AAGO,SAAS,gBAAgB,QAAqC;AACnE,QAAM,QAAQ,OAAO,QAAQ,SAAY,KAAK,KAAK,OAAO,GAAG;AAC7D,QAAM,MAAM,OAAO,WAAW,SAAY,KAAK,WAAM,OAAO,MAAM;AAClE,SAAO,UAAU,OAAO,EAAE,IAAI,OAAO,MAAM,GAAG,KAAK,GAAG,GAAG;AAC3D;AAYO,SAAS,aACd,WACA,KAAa,mBACb,OAAe,2BAA2B,GAC5B;AACd,QAAM,MAAMA,MAAK,KAAK,MAAM,EAAE;AAC9B,QAAM,aAAa,iBAAiB,WAAW,EAAE;AACjD,QAAM,YAAYD,IAAG,WAAWC,MAAK,KAAK,KAAK,gBAAgB,CAAC;AAChE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;AAAA,IACjD;AAAA,IACA,oBACE,aAAa,eAAe,UAAa,UAAUA,MAAK,KAAK,KAAK,gBAAgB,GAAGA,MAAK,KAAK,YAAY,gBAAgB,CAAC;AAAA,IAC9H,iBACE,eAAe,UACfD,IAAG,WAAWC,MAAK,KAAK,KAAK,aAAa,CAAC,KAC3C,UAAUA,MAAK,KAAK,KAAK,aAAa,GAAGA,MAAK,KAAK,YAAY,aAAa,CAAC;AAAA,EACjF;AACF;AAEA,SAAS,UAAU,MAAc,OAAwB;AACvD,MAAI;AACF,WAAOD,IAAG,aAAa,IAAI,EAAE,OAAOA,IAAG,aAAa,KAAK,CAAC;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":["fs","path","path","fs","path","path","fs","path","path","DAY_MS","path","path","fs","path","fs","path","path","fs","fs","path"]}
|
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
createCipheriv,
|
|
12
12
|
createDecipheriv,
|
|
13
13
|
randomBytes,
|
|
14
|
-
scryptSync
|
|
15
|
-
timingSafeEqual
|
|
14
|
+
scryptSync
|
|
16
15
|
} from "crypto";
|
|
17
16
|
var MIGRATE_MAGIC = "HBMIG1";
|
|
18
17
|
var MIGRATE_VERSION = 1;
|
|
19
18
|
var KDF_N = 16384;
|
|
19
|
+
var KDF_N_MIN = 1 << 12;
|
|
20
|
+
var KDF_N_MAX = 1 << 16;
|
|
21
|
+
var SCRYPT_MAXMEM = 128 * 1024 * 1024;
|
|
20
22
|
var KEY_LEN = 32;
|
|
21
23
|
var MIGRATE_FILES = [
|
|
22
24
|
"profile.json",
|
|
@@ -28,7 +30,10 @@ var MIGRATE_FILES = [
|
|
|
28
30
|
"browse.json",
|
|
29
31
|
"seeds.jsonl",
|
|
30
32
|
"ledger.md",
|
|
31
|
-
"cursors.json"
|
|
33
|
+
"cursors.json",
|
|
34
|
+
// v1.9.0: persona report log + topic preference stats
|
|
35
|
+
"seed_report.jsonl",
|
|
36
|
+
"preference.json"
|
|
32
37
|
];
|
|
33
38
|
function collectWeeklyFiles(guard, paths) {
|
|
34
39
|
const dir = path.join(paths.dataDir, "weekly");
|
|
@@ -72,14 +77,24 @@ function collectMigrationEntries(guard, paths) {
|
|
|
72
77
|
}
|
|
73
78
|
return { entries, progress: { packed, missing } };
|
|
74
79
|
}
|
|
75
|
-
function deriveKey(passphrase, salt) {
|
|
76
|
-
return scryptSync(passphrase, salt, KEY_LEN, { N:
|
|
80
|
+
function deriveKey(passphrase, salt, n) {
|
|
81
|
+
return scryptSync(passphrase, salt, KEY_LEN, { N: n, r: 8, p: 1, maxmem: SCRYPT_MAXMEM });
|
|
82
|
+
}
|
|
83
|
+
function assertKdfN(v) {
|
|
84
|
+
const n = Number(v);
|
|
85
|
+
if (!Number.isInteger(n) || n <= 0 || (n & n - 1) !== 0) {
|
|
86
|
+
throw new Error("migrate: container has invalid kdf parameters (N must be a power of two)");
|
|
87
|
+
}
|
|
88
|
+
if (n < KDF_N_MIN || n > KDF_N_MAX) {
|
|
89
|
+
throw new Error(`migrate: container kdf N ${n} is outside the supported range [${KDF_N_MIN}, ${KDF_N_MAX}]`);
|
|
90
|
+
}
|
|
91
|
+
return n;
|
|
77
92
|
}
|
|
78
93
|
function encryptContainer(entries, passphrase, now = /* @__PURE__ */ new Date()) {
|
|
79
94
|
if (!passphrase) throw new Error("migrate: passphrase must not be empty");
|
|
80
95
|
const salt = randomBytes(16);
|
|
81
96
|
const iv = randomBytes(12);
|
|
82
|
-
const key = deriveKey(passphrase, salt);
|
|
97
|
+
const key = deriveKey(passphrase, salt, KDF_N);
|
|
83
98
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
84
99
|
const plain = Buffer.from(JSON.stringify({ version: MIGRATE_VERSION, createdAt: now.toISOString(), files: entries }), "utf8");
|
|
85
100
|
const data = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
@@ -100,11 +115,12 @@ function decryptContainer(text, passphrase) {
|
|
|
100
115
|
throw new Error("migrate: not a migration container (bad JSON)");
|
|
101
116
|
}
|
|
102
117
|
if (outer.magic !== MIGRATE_MAGIC) throw new Error("migrate: not a migration container (bad magic)");
|
|
118
|
+
const n = assertKdfN(outer.kdf?.N);
|
|
103
119
|
const salt = Buffer.from(String(outer.kdf?.salt ?? ""), "hex");
|
|
104
120
|
const iv = Buffer.from(String(outer.iv ?? ""), "hex");
|
|
105
121
|
const tag = Buffer.from(String(outer.tag ?? ""), "hex");
|
|
106
122
|
const data = Buffer.from(String(outer.data ?? ""), "base64");
|
|
107
|
-
const key = deriveKey(passphrase, salt);
|
|
123
|
+
const key = deriveKey(passphrase, salt, n);
|
|
108
124
|
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
109
125
|
decipher.setAuthTag(tag);
|
|
110
126
|
let plain;
|
|
@@ -116,8 +132,6 @@ function decryptContainer(text, passphrase) {
|
|
|
116
132
|
const inner = JSON.parse(plain.toString("utf8"));
|
|
117
133
|
if (inner.version !== MIGRATE_VERSION) throw new Error(`migrate: unsupported container version ${inner.version}`);
|
|
118
134
|
if (!Array.isArray(inner.files)) throw new Error("migrate: container has no file list");
|
|
119
|
-
const v = Buffer.from([inner.version]);
|
|
120
|
-
timingSafeEqual(v, Buffer.from([MIGRATE_VERSION]));
|
|
121
135
|
return { createdAt: inner.createdAt, files: inner.files };
|
|
122
136
|
}
|
|
123
137
|
function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
@@ -127,6 +141,10 @@ function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
|
127
141
|
const stamp = new Date(now).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
128
142
|
for (const f of files) {
|
|
129
143
|
const rel = String(f.path ?? "");
|
|
144
|
+
if (!rel || rel.includes("..") || path.isAbsolute(rel)) {
|
|
145
|
+
skipped.push(rel);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
130
148
|
const abs = path.join(paths.dataDir, rel);
|
|
131
149
|
let absCanon;
|
|
132
150
|
try {
|
|
@@ -135,10 +153,6 @@ function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
|
135
153
|
skipped.push(rel);
|
|
136
154
|
continue;
|
|
137
155
|
}
|
|
138
|
-
if (rel.includes("..") || path.isAbsolute(rel)) {
|
|
139
|
-
skipped.push(rel);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
156
|
fs.mkdirSync(path.dirname(absCanon), { recursive: true });
|
|
143
157
|
if (fs.existsSync(absCanon)) {
|
|
144
158
|
fs.copyFileSync(absCanon, `${absCanon}.bak-migrate-${stamp}`);
|
|
@@ -160,4 +174,4 @@ export {
|
|
|
160
174
|
decryptContainer,
|
|
161
175
|
applyMigrationEntries
|
|
162
176
|
};
|
|
163
|
-
//# sourceMappingURL=chunk-
|
|
177
|
+
//# sourceMappingURL=chunk-IPOZFZPY.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\r\n// machine + account, so a new machine cannot read them — this module packs\r\n// the memory files into ONE passphrase-encrypted container (AES-256-GCM,\r\n// scrypt-derived key, node:crypto only) that can travel safely, and restores\r\n// them on the other side re-encrypted with the LOCAL DPAPI.\r\n//\r\n// Restore is byte-identical: for each file we record whether it was DPAPI\r\n// encrypted on disk (isEncrypted magic check) and write it back through the\r\n// same path — no per-module write semantics to keep in sync. Existing files\r\n// are backed up before overwrite (rule 4).\r\n\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport {\r\n createCipheriv,\r\n createDecipheriv,\r\n randomBytes,\r\n scryptSync,\r\n} from 'node:crypto';\r\nimport type { PathGuard } from '../core/path-guard.js';\r\nimport type { WorkspacePaths } from '../core/paths.js';\r\nimport { isEncrypted, loadEncryptedText, saveEncryptedText } from './vault.js';\r\n\r\nexport const MIGRATE_MAGIC = 'HBMIG1';\r\nexport const MIGRATE_VERSION = 1;\r\n/** scrypt cost for NEW containers (2^14 = Node's default, 16 MiB of work).\r\n * Reading is parameterised by the container's own kdf.N, so raising this\r\n * never orphans an existing export. */\r\nconst KDF_N = 163_84;\r\n/** Hard bounds for a container-supplied N: a hostile/broken container must not\r\n * be able to make the import allocate unbounded memory. */\r\nconst KDF_N_MIN = 1 << 12; // 2^12 = 4 MiB\r\nconst KDF_N_MAX = 1 << 16; // 2^16 = 64 MiB\r\n/** Node's scrypt maxmem defaults to 32 MiB, which N=2^15 already exceeds — a\r\n * legal container with a larger N would fail without an explicit cap. */\r\nconst SCRYPT_MAXMEM = 128 * 1024 * 1024;\r\nconst KEY_LEN = 32;\r\n\r\n/** Memory-relevant runtime files (dataDir-relative). Deliberately NOT here:\r\n * gate.json (machine-local counters + home session id — the new machine\r\n * builds its own) and screen snapshots (per-beat transients, not memory). */\r\nexport const MIGRATE_FILES = [\r\n 'profile.json',\r\n 'profile_snapshot.json',\r\n 'profile_inbox.jsonl',\r\n 'profile_journal.jsonl',\r\n 'profile_rhythm.json',\r\n 'sent.json',\r\n 'browse.json',\r\n 'seeds.jsonl',\r\n 'ledger.md',\r\n 'cursors.json',\r\n // v1.9.0: persona report log + topic preference stats\r\n 'seed_report.jsonl',\r\n 'preference.json',\r\n];\r\n\r\nexport interface MigrateEntry {\r\n path: string; // dataDir-relative\r\n encrypted: boolean; // was DPAPI-encrypted on the source machine\r\n content: string;\r\n}\r\n\r\nexport interface MigrateProgress {\r\n packed: string[];\r\n missing: string[];\r\n}\r\n\r\nfunction collectWeeklyFiles(guard: PathGuard, paths: WorkspacePaths): string[] {\r\n const dir = path.join(paths.dataDir, 'weekly');\r\n try {\r\n return fs.readdirSync(guard.assert(dir))\r\n .filter((f) => f.endsWith('.json'))\r\n .map((f) => path.join('weekly', f).replace(/\\\\/g, '/'));\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n/** Journal archive shards (snapshot rotation output) travel with the memory. */\r\nfunction collectJournalArchives(guard: PathGuard, paths: WorkspacePaths): string[] {\r\n try {\r\n return fs.readdirSync(guard.assert(paths.dataDir))\r\n .filter((f) => f.startsWith('profile_journal.archive-') && f.endsWith('.jsonl'));\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n/** Read every migration file. A DPAPI file that fails to decrypt aborts the\r\n * export — a silent half-memory container would be worse than a loud error. */\r\nexport function collectMigrationEntries(guard: PathGuard, paths: WorkspacePaths): { entries: MigrateEntry[]; progress: MigrateProgress } {\r\n const entries: MigrateEntry[] = [];\r\n const packed: string[] = [];\r\n const missing: string[] = [];\r\n const rels = [...MIGRATE_FILES, ...collectJournalArchives(guard, paths), ...collectWeeklyFiles(guard, paths)];\r\n for (const rel of rels) {\r\n const abs = path.join(paths.dataDir, rel);\r\n let absCanon: string;\r\n try {\r\n absCanon = guard.assert(abs);\r\n } catch {\r\n missing.push(rel);\r\n continue;\r\n }\r\n if (!fs.existsSync(absCanon)) {\r\n missing.push(rel);\r\n continue;\r\n }\r\n const encrypted = isEncrypted(absCanon);\r\n const content = encrypted\r\n ? loadEncryptedText(guard, absCanon) ?? (() => { throw new Error(`cannot decrypt ${rel} on this machine`); })()\r\n : fs.readFileSync(absCanon, 'utf8');\r\n entries.push({ path: rel.replace(/\\\\/g, '/'), encrypted, content });\r\n packed.push(rel);\r\n }\r\n return { entries, progress: { packed, missing } };\r\n}\r\n\r\n/** Derive the key with the container's OWN cost parameter. `n` must already\r\n * have passed assertKdfN (export uses the current default). */\r\nfunction deriveKey(passphrase: string, salt: Buffer, n: number): Buffer {\r\n return scryptSync(passphrase, salt, KEY_LEN, { N: n, r: 8, p: 1, maxmem: SCRYPT_MAXMEM });\r\n}\r\n\r\n/** Validate the scrypt cost carried by a container. A missing/absurd N is a\r\n * BROKEN container — reported as such, never as \"wrong passphrase\" (before\r\n * 2026-10-06 the N in the container was ignored and the two were the same\r\n * error, so raising the cost would have orphaned every old export). */\r\nfunction assertKdfN(v: unknown): number {\r\n const n = Number(v);\r\n if (!Number.isInteger(n) || n <= 0 || (n & (n - 1)) !== 0) {\r\n throw new Error('migrate: container has invalid kdf parameters (N must be a power of two)');\r\n }\r\n if (n < KDF_N_MIN || n > KDF_N_MAX) {\r\n throw new Error(`migrate: container kdf N ${n} is outside the supported range [${KDF_N_MIN}, ${KDF_N_MAX}]`);\r\n }\r\n return n;\r\n}\r\n\r\n/** Build the container file content. Everything sensitive lives inside the\r\n * ciphertext; the outer JSON carries only crypto parameters. */\r\nexport function encryptContainer(entries: MigrateEntry[], passphrase: string, now = new Date()): string {\r\n if (!passphrase) throw new Error('migrate: passphrase must not be empty');\r\n const salt = randomBytes(16);\r\n const iv = randomBytes(12);\r\n const key = deriveKey(passphrase, salt, KDF_N);\r\n const cipher = createCipheriv('aes-256-gcm', key, iv);\r\n const plain = Buffer.from(JSON.stringify({ version: MIGRATE_VERSION, createdAt: now.toISOString(), files: entries }), 'utf8');\r\n const data = Buffer.concat([cipher.update(plain), cipher.final()]);\r\n const tag = cipher.getAuthTag();\r\n return JSON.stringify({\r\n magic: MIGRATE_MAGIC,\r\n kdf: { name: 'scrypt', salt: salt.toString('hex'), N: KDF_N },\r\n iv: iv.toString('hex'),\r\n tag: tag.toString('hex'),\r\n data: data.toString('base64'),\r\n }, null, 1) + '\\n';\r\n}\r\n\r\n/** Parse + decrypt a container. Throws (loudly) on a wrong passphrase or a\r\n * corrupted file — never returns partial data. */\r\nexport function decryptContainer(text: string, passphrase: string): { createdAt: string; files: MigrateEntry[] } {\r\n let outer: { magic?: string; kdf?: { salt?: string; N?: unknown }; iv?: string; tag?: string; data?: string };\r\n try {\r\n outer = JSON.parse(text);\r\n } catch {\r\n throw new Error('migrate: not a migration container (bad JSON)');\r\n }\r\n if (outer.magic !== MIGRATE_MAGIC) throw new Error('migrate: not a migration container (bad magic)');\r\n const n = assertKdfN(outer.kdf?.N);\r\n const salt = Buffer.from(String(outer.kdf?.salt ?? ''), 'hex');\r\n const iv = Buffer.from(String(outer.iv ?? ''), 'hex');\r\n const tag = Buffer.from(String(outer.tag ?? ''), 'hex');\r\n const data = Buffer.from(String(outer.data ?? ''), 'base64');\r\n const key = deriveKey(passphrase, salt, n);\r\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\r\n decipher.setAuthTag(tag);\r\n let plain: Buffer;\r\n try {\r\n plain = Buffer.concat([decipher.update(data), decipher.final()]);\r\n } catch {\r\n throw new Error('migrate: wrong passphrase or corrupted container');\r\n }\r\n const inner = JSON.parse(plain.toString('utf8')) as { version: number; createdAt: string; files: MigrateEntry[] };\r\n if (inner.version !== MIGRATE_VERSION) throw new Error(`migrate: unsupported container version ${inner.version}`);\r\n if (!Array.isArray(inner.files)) throw new Error('migrate: container has no file list');\r\n return { createdAt: inner.createdAt, files: inner.files };\r\n}\r\n\r\nexport interface MigrateImportResult {\r\n restored: string[];\r\n backedUp: string[];\r\n skipped: string[];\r\n}\r\n\r\n/** Restore entries into the local dataDir, re-encrypting DPAPI files with the\r\n * LOCAL machine key. Existing files are copied aside first. */\r\nexport function applyMigrationEntries(guard: PathGuard, paths: WorkspacePaths, files: MigrateEntry[], now = Date.now()): MigrateImportResult {\r\n const restored: string[] = [];\r\n const backedUp: string[] = [];\r\n const skipped: string[] = [];\r\n const stamp = new Date(now).toISOString().replace(/[-:T]/g, '').slice(0, 14);\r\n for (const f of files) {\r\n const rel = String(f.path ?? '');\r\n // String-level rejection BEFORE the path is resolved: the guard's realpath\r\n // check already refuses escapes, but an import must not depend on another\r\n // module staying correct.\r\n if (!rel || rel.includes('..') || path.isAbsolute(rel)) {\r\n skipped.push(rel);\r\n continue;\r\n }\r\n const abs = path.join(paths.dataDir, rel);\r\n let absCanon: string;\r\n try {\r\n absCanon = guard.assert(abs);\r\n } catch {\r\n skipped.push(rel); // path refused by the guard — never write it\r\n continue;\r\n }\r\n fs.mkdirSync(path.dirname(absCanon), { recursive: true });\r\n if (fs.existsSync(absCanon)) {\r\n fs.copyFileSync(absCanon, `${absCanon}.bak-migrate-${stamp}`);\r\n backedUp.push(rel);\r\n }\r\n if (f.encrypted) saveEncryptedText(guard, absCanon, f.content);\r\n else fs.writeFileSync(absCanon, f.content, 'utf8');\r\n restored.push(rel);\r\n }\r\n return { restored, backedUp, skipped };\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -20,6 +20,14 @@ var SEED_SOURCE_DEFAULT_CONFIDENCE = {
|
|
|
20
20
|
|
|
21
21
|
// src/seeds/pool.ts
|
|
22
22
|
var DAY_MS = 864e5;
|
|
23
|
+
var NO_AUDIT = () => {
|
|
24
|
+
};
|
|
25
|
+
function emit(audit, entry) {
|
|
26
|
+
try {
|
|
27
|
+
audit(entry);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
23
31
|
var SEED_CATEGORY_CAPS = { topic: 16, chat: 14 };
|
|
24
32
|
var CATEGORY_VALUES = ["topic", "chat"];
|
|
25
33
|
function normalizeCategory(v) {
|
|
@@ -95,7 +103,7 @@ function archiveSeed(seed, reason, now) {
|
|
|
95
103
|
function retireLimit(s, policy) {
|
|
96
104
|
return normalizeCategory(s.category) === "chat" ? 1 : policy.seeds.retireAfterUsed;
|
|
97
105
|
}
|
|
98
|
-
function addSeed(guard, file, policy, input, now = Date.now()) {
|
|
106
|
+
function addSeed(guard, file, policy, input, now = Date.now(), audit = NO_AUDIT) {
|
|
99
107
|
const db = loadPool(guard, file);
|
|
100
108
|
const text = input.text.trim();
|
|
101
109
|
if (!text) throw new Error("seed text must not be empty");
|
|
@@ -124,16 +132,20 @@ function addSeed(guard, file, policy, input, now = Date.now()) {
|
|
|
124
132
|
extra.status = "archived";
|
|
125
133
|
extra.retireReason = "completed";
|
|
126
134
|
extra.retiredAt = nowIso;
|
|
135
|
+
emit(audit, { event: "seed_retired", id: extra.id, reason: "completed", via: "merge", into: kept.id });
|
|
127
136
|
}
|
|
128
137
|
seed = kept;
|
|
129
138
|
if (activeSeeds(db).length > policy.seeds.maxActive) {
|
|
130
139
|
const victim = pickEvictionVictim(db, policy, now, normalizeCategory(kept.category));
|
|
131
140
|
if (victim && victim.id !== kept.id) {
|
|
132
141
|
archiveSeed(victim, "pool_cap", now);
|
|
142
|
+
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 });
|
|
143
|
+
emit(audit, { event: "seed_retired", id: victim.id, reason: "pool_cap", via: "merge", into: kept.id });
|
|
133
144
|
savePool(guard, file, db);
|
|
134
145
|
return { kind: "merged", seed: kept, evicted: victim };
|
|
135
146
|
}
|
|
136
147
|
}
|
|
148
|
+
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 });
|
|
137
149
|
savePool(guard, file, db);
|
|
138
150
|
return { kind: "merged", seed: kept };
|
|
139
151
|
}
|
|
@@ -171,12 +183,14 @@ function addSeed(guard, file, policy, input, now = Date.now()) {
|
|
|
171
183
|
status: "active"
|
|
172
184
|
};
|
|
173
185
|
db.seeds.push(seed);
|
|
186
|
+
emit(audit, { event: "seed_added", id: seed.id, kind: "added", tag: seed.tag, source: seed.source, category: seed.category, confidence: seed.confidence });
|
|
187
|
+
if (evicted) emit(audit, { event: "seed_retired", id: evicted.id, reason: "pool_cap", via: "add", into: seed.id });
|
|
174
188
|
savePool(guard, file, db);
|
|
175
189
|
return { kind: "added", seed, evicted };
|
|
176
190
|
}
|
|
177
|
-
function gcPool(guard, file, policy, now = Date.now()) {
|
|
191
|
+
function gcPool(guard, file, policy, now = Date.now(), audit = NO_AUDIT) {
|
|
178
192
|
const db = loadPool(guard, file);
|
|
179
|
-
const report = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0 };
|
|
193
|
+
const report = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };
|
|
180
194
|
for (const s of activeSeeds(db)) {
|
|
181
195
|
const ageMs = now - parseIso(s.bornAt);
|
|
182
196
|
const sinceEvidence = parseIso(s.lastEvidenceAt);
|
|
@@ -184,19 +198,30 @@ function gcPool(guard, file, policy, now = Date.now()) {
|
|
|
184
198
|
if (s.used >= retireLimit(s, policy) && sinceEvidence <= sinceUsed) {
|
|
185
199
|
archiveSeed(s, "consumed", now);
|
|
186
200
|
report.consumed += 1;
|
|
201
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "consumed", via: "gc", used: s.used });
|
|
187
202
|
} else if (now > parseIso(s.expiresAt)) {
|
|
188
203
|
archiveSeed(s, "expired", now);
|
|
189
204
|
report.expired += 1;
|
|
205
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "expired", via: "gc", used: s.used });
|
|
190
206
|
} else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {
|
|
191
207
|
archiveSeed(s, "cold_bench", now);
|
|
192
208
|
report.coldBench += 1;
|
|
209
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "cold_bench", via: "gc", used: s.used });
|
|
193
210
|
}
|
|
194
211
|
}
|
|
212
|
+
const archived = db.seeds.filter((s) => s.status === "archived");
|
|
213
|
+
if (archived.length > policy.seeds.archiveCap) {
|
|
214
|
+
const drop = [...archived].sort((a, b) => parseIso(a.retiredAt ?? "") - parseIso(b.retiredAt ?? "")).slice(0, archived.length - policy.seeds.archiveCap);
|
|
215
|
+
const dropped = new Set(drop.map((s) => s.id));
|
|
216
|
+
db.seeds = db.seeds.filter((s) => !dropped.has(s.id));
|
|
217
|
+
report.archiveTrimmed = drop.length;
|
|
218
|
+
emit(audit, { event: "seed_archive_trimmed", count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });
|
|
219
|
+
}
|
|
195
220
|
report.activeAfter = activeSeeds(db).length;
|
|
196
221
|
savePool(guard, file, db);
|
|
197
222
|
return report;
|
|
198
223
|
}
|
|
199
|
-
function surfaceSeed(guard, file, policy, id, now = Date.now()) {
|
|
224
|
+
function surfaceSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
200
225
|
const db = loadPool(guard, file);
|
|
201
226
|
const s = db.seeds.find((x) => x.id === id && x.status === "active");
|
|
202
227
|
if (!s) return null;
|
|
@@ -204,19 +229,21 @@ function surfaceSeed(guard, file, policy, id, now = Date.now()) {
|
|
|
204
229
|
s.lastUsedAt = new Date(now).toISOString();
|
|
205
230
|
if (s.used >= retireLimit(s, policy) && parseIso(s.lastEvidenceAt) <= parseIso(s.lastUsedAt)) {
|
|
206
231
|
archiveSeed(s, "consumed", now);
|
|
232
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "consumed", via: "surface", used: s.used });
|
|
207
233
|
}
|
|
208
234
|
savePool(guard, file, db);
|
|
209
235
|
return s;
|
|
210
236
|
}
|
|
211
|
-
function archiveSeedById(guard, file, id, reason = "completed", now = Date.now()) {
|
|
237
|
+
function archiveSeedById(guard, file, id, reason = "completed", now = Date.now(), audit = NO_AUDIT) {
|
|
212
238
|
const db = loadPool(guard, file);
|
|
213
239
|
const s = db.seeds.find((x) => x.id === id && x.status === "active");
|
|
214
240
|
if (!s) return null;
|
|
215
241
|
archiveSeed(s, reason, now);
|
|
242
|
+
emit(audit, { event: "seed_retired", id: s.id, reason, via: "manual" });
|
|
216
243
|
savePool(guard, file, db);
|
|
217
244
|
return s;
|
|
218
245
|
}
|
|
219
|
-
function restoreSeed(guard, file, policy, id, now = Date.now()) {
|
|
246
|
+
function restoreSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
220
247
|
const db = loadPool(guard, file);
|
|
221
248
|
const s = db.seeds.find((x) => x.id === id && x.status === "archived");
|
|
222
249
|
if (!s) return { ok: false, reason: "archived seed not found" };
|
|
@@ -228,14 +255,16 @@ function restoreSeed(guard, file, policy, id, now = Date.now()) {
|
|
|
228
255
|
s.retiredAt = void 0;
|
|
229
256
|
s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();
|
|
230
257
|
s.lastEvidenceAt = new Date(now).toISOString();
|
|
258
|
+
emit(audit, { event: "seed_restored", id: s.id });
|
|
231
259
|
savePool(guard, file, db);
|
|
232
260
|
return { ok: true, seed: s };
|
|
233
261
|
}
|
|
234
|
-
function deleteSeed(guard, file, id) {
|
|
262
|
+
function deleteSeed(guard, file, id, audit = NO_AUDIT) {
|
|
235
263
|
const db = loadPool(guard, file);
|
|
236
264
|
const before = db.seeds.length;
|
|
237
265
|
db.seeds = db.seeds.filter((x) => x.id !== id);
|
|
238
266
|
if (db.seeds.length === before) return false;
|
|
267
|
+
emit(audit, { event: "seed_deleted", id });
|
|
239
268
|
savePool(guard, file, db);
|
|
240
269
|
return true;
|
|
241
270
|
}
|
|
@@ -259,4 +288,4 @@ export {
|
|
|
259
288
|
restoreSeed,
|
|
260
289
|
deleteSeed
|
|
261
290
|
};
|
|
262
|
-
//# sourceMappingURL=chunk-
|
|
291
|
+
//# sourceMappingURL=chunk-PQTSXW4F.js.map
|
|
@@ -0,0 +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 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// ── 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// ── 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 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 const db = loadPool(guard, file);\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 (s.used >= retireLimit(s, policy) && sinceEvidence <= sinceUsed) {\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/** 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 const db = loadPool(guard, file);\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n s.used += 1;\n s.lastUsedAt = new Date(now).toISOString();\n if (s.used >= retireLimit(s, policy) && parseIso(s.lastEvidenceAt) <= parseIso(s.lastUsedAt)) {\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/** 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 const db = loadPool(guard, file);\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/** 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 const db = loadPool(guard, file);\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 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 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/** 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 const db = loadPool(guard, file);\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","// 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":[]}
|
package/dist/cli/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
scanPending,
|
|
23
23
|
sendNewMessageHint,
|
|
24
24
|
weeklyDirPath
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-2SYXKKZF.js";
|
|
26
26
|
import {
|
|
27
27
|
activeSeeds,
|
|
28
28
|
addSeed,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
loadPool,
|
|
33
33
|
seedsFilePath,
|
|
34
34
|
surfaceSeed
|
|
35
|
-
} from "../chunk-
|
|
35
|
+
} from "../chunk-PQTSXW4F.js";
|
|
36
36
|
import {
|
|
37
37
|
appendAuditLine,
|
|
38
38
|
loadProfile,
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
collectMigrationEntries,
|
|
48
48
|
decryptContainer,
|
|
49
49
|
encryptContainer
|
|
50
|
-
} from "../chunk-
|
|
50
|
+
} from "../chunk-IPOZFZPY.js";
|
|
51
51
|
import {
|
|
52
52
|
initWorkspace,
|
|
53
53
|
loadEncryptedText,
|
|
@@ -214,6 +214,12 @@ async function main(argv) {
|
|
|
214
214
|
const seedsFile = seedsFilePath(paths.dataDir);
|
|
215
215
|
const ledgerFile = ledgerFilePath(paths.dataDir);
|
|
216
216
|
const decisionLog = path2.join(paths.logsDir, "heartbeat.jsonl");
|
|
217
|
+
const seedAudit = (entry) => {
|
|
218
|
+
try {
|
|
219
|
+
appendAuditLine(decisionLog, entry);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
};
|
|
217
223
|
switch (cmd) {
|
|
218
224
|
case "status": {
|
|
219
225
|
console.log(JSON.stringify({
|
|
@@ -240,7 +246,7 @@ async function main(argv) {
|
|
|
240
246
|
source: flag(rest, "--source"),
|
|
241
247
|
topic: flag(rest, "--topic"),
|
|
242
248
|
confidence: flag(rest, "--confidence") ? Number(flag(rest, "--confidence")) : void 0
|
|
243
|
-
});
|
|
249
|
+
}, Date.now(), seedAudit);
|
|
244
250
|
if (result.kind === "duplicate") {
|
|
245
251
|
console.log(`DUPLICATE: active seed ${result.seed.id} has the same text`);
|
|
246
252
|
return 0;
|
|
@@ -259,7 +265,7 @@ async function main(argv) {
|
|
|
259
265
|
return 0;
|
|
260
266
|
}
|
|
261
267
|
case "surface": {
|
|
262
|
-
const s = surfaceSeed(guard, seedsFile, policy, rest[0] ?? "");
|
|
268
|
+
const s = surfaceSeed(guard, seedsFile, policy, rest[0] ?? "", Date.now(), seedAudit);
|
|
263
269
|
if (!s) {
|
|
264
270
|
console.log("NOT_FOUND");
|
|
265
271
|
return 1;
|
|
@@ -268,7 +274,7 @@ async function main(argv) {
|
|
|
268
274
|
return 0;
|
|
269
275
|
}
|
|
270
276
|
case "archive": {
|
|
271
|
-
const s = archiveSeedById(guard, seedsFile, rest[0] ?? "");
|
|
277
|
+
const s = archiveSeedById(guard, seedsFile, rest[0] ?? "", "completed", Date.now(), seedAudit);
|
|
272
278
|
if (!s) {
|
|
273
279
|
console.log("NOT_FOUND");
|
|
274
280
|
return 1;
|
|
@@ -277,8 +283,8 @@ async function main(argv) {
|
|
|
277
283
|
return 0;
|
|
278
284
|
}
|
|
279
285
|
case "gc": {
|
|
280
|
-
const report = gcPool(guard, seedsFile, policy);
|
|
281
|
-
console.log(`GC consumed:${report.consumed} expired:${report.expired} cold_bench:${report.coldBench} -> active ${report.activeAfter}/${policy.seeds.maxActive}`);
|
|
286
|
+
const report = gcPool(guard, seedsFile, policy, Date.now(), seedAudit);
|
|
287
|
+
console.log(`GC consumed:${report.consumed} expired:${report.expired} cold_bench:${report.coldBench} archive_trimmed:${report.archiveTrimmed} -> active ${report.activeAfter}/${policy.seeds.maxActive} archive ${archivedSeeds(loadPool(guard, seedsFile)).length}/${policy.seeds.archiveCap}`);
|
|
282
288
|
return 0;
|
|
283
289
|
}
|
|
284
290
|
case "stats": {
|