{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/routing/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EACX,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,MAAM,YAAY,CAAC;AAOpB,0BAA0B;AAC1B,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD;AA0DD,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,IAAI,mBAAmB,GAAG,SAAS,CAEzE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAE1E;AAeD,wBAAgB,WAAW,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAMlE;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS,CAI/E;AAED,wBAAgB,YAAY,IAAI,sBAAsB,EAAE,CASvD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGnD;AAMD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAIrE;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAElF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,EAAE,CASpE;AAMD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAIjE;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAS/E;AAMD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAIlE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CASnE;AAMD,mFAAmF;AACnF,wBAAgB,WAAW,CAC1B,KAAK,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC,GACpE,kBAAkB,CAiBpB;AAeD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAkB9D;AAMD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIxE;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,CAmB9E;AAMD,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1E,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,2GAA2G;AAC3G,wBAAgB,oBAAoB,IAAI,gBAAgB,CA0BvD","sourcesContent":["/**\n * Durable, content-addressed storage for routing policies, evidence, decisions,\n * shadow decisions, events and drift samples.\n *\n * Storage lives under `<agentDir>/routing/`. All writes are atomic\n * (write-temp-then-rename), read snapshots are cached but never authoritative,\n * and the active-policy pointer is swapped atomically with an immutable old\n * policy retained for rollback.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { getCanonicalSubagentRegistry } from \"../subagent-registry.js\";\nimport type {\n\tCandidateEvidence,\n\tOrchestrationDecision,\n\tOrchestrationEvent,\n\tRoutingPolicyCandidate,\n\tShadowDecision,\n} from \"./types.js\";\n\n/** Directory holding the canonical subagent registry (for candidate generation). */\nfunction registryDir(): string {\n\treturn path.join(getAgentDir(), \"subagents\");\n}\n\n/** SHA-256 hex digest. */\nexport function sha256(input: string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/** Deterministic stable JSON serialization (sorted keys). */\nexport function stableStringify(value: unknown): string {\n\treturn JSON.stringify(value, (_, v) => (v === undefined ? undefined : v), 2);\n}\n\nfunction routingRoot(): string {\n\t// Test/embedder override keeps routing state isolated and never in the way\n\t// of a real user's durable evidence.\n\tconst override = process.env.JENSEN_ROUTING_ROOT;\n\tif (override) return override;\n\treturn path.join(getAgentDir(), \"routing\");\n}\n\nfunction ensureDir(dir: string): void {\n\tfs.mkdirSync(dir, { recursive: true });\n}\n\nfunction readJson<T>(file: string): T | undefined {\n\ttry {\n\t\tconst raw = fs.readFileSync(file, \"utf-8\");\n\t\treturn JSON.parse(raw) as T;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Atomic write: write to temp file in the same dir then rename. */\nfunction atomicWriteJson(file: string, value: unknown): void {\n\tensureDir(path.dirname(file));\n\tconst tmp = `${file}.tmp-${process.pid}-${randomUUID()}`;\n\tfs.writeFileSync(tmp, stableStringify(value), \"utf-8\");\n\tfs.renameSync(tmp, file);\n}\n\nfunction decisionsDir(): string {\n\treturn path.join(routingRoot(), \"decisions\");\n}\nfunction evidenceDir(): string {\n\treturn path.join(routingRoot(), \"evidence\");\n}\nfunction policiesDir(): string {\n\treturn path.join(routingRoot(), \"policies\");\n}\nfunction shadowDir(): string {\n\treturn path.join(routingRoot(), \"shadow\");\n}\nfunction eventsDir(): string {\n\treturn path.join(routingRoot(), \"events\");\n}\nfunction driftDir(): string {\n\treturn path.join(routingRoot(), \"drift\");\n}\n\nfunction activePolicyFile(): string {\n\treturn path.join(routingRoot(), \"active-policy.json\");\n}\n\n// =============================================================================\n// Active policy pointer\n// =============================================================================\n\nexport interface ActivePolicyPointer {\n\tpolicyId: string;\n\tpolicyVersion: number;\n\thash: string;\n\tactivatedAt: string;\n\tpreviousPolicyId?: string;\n}\n\nexport function loadActivePolicyPointer(): ActivePolicyPointer | undefined {\n\treturn readJson<ActivePolicyPointer>(activePolicyFile());\n}\n\nexport function saveActivePolicyPointer(pointer: ActivePolicyPointer): void {\n\tatomicWriteJson(activePolicyFile(), pointer);\n}\n\n// =============================================================================\n// Policies\n// =============================================================================\n\n/** Sanitize a policy id: only allow safe, path-bounded identifiers (no separators, no traversal). */\nfunction safePolicyId(policyId: string): string | undefined {\n\tif (!policyId) return undefined;\n\t// Allow UUIDs and simple dotted/slug identifiers only.\n\tif (!/^[a-zA-Z0-9._-]+$/.test(policyId)) return undefined;\n\tif (policyId.includes(\"..\")) return undefined;\n\treturn policyId;\n}\n\nexport function writePolicy(policy: RoutingPolicyCandidate): string {\n\tconst id = safePolicyId(policy.policyId);\n\tif (!id) throw new Error(\"INVALID_POLICY_ID: policy id must be safe and bounded\");\n\tconst file = path.join(policiesDir(), `${id}.json`);\n\tatomicWriteJson(file, policy);\n\treturn file;\n}\n\nexport function readPolicy(policyId: string): RoutingPolicyCandidate | undefined {\n\tconst id = safePolicyId(policyId);\n\tif (!id) return undefined;\n\treturn readJson<RoutingPolicyCandidate>(path.join(policiesDir(), `${id}.json`));\n}\n\nexport function listPolicies(): RoutingPolicyCandidate[] {\n\tensureDir(policiesDir());\n\tconst out: RoutingPolicyCandidate[] = [];\n\tfor (const f of fs.readdirSync(policiesDir())) {\n\t\tif (!f.endsWith(\".json\")) continue;\n\t\tconst p = readJson<RoutingPolicyCandidate>(path.join(policiesDir(), f));\n\t\tif (p) out.push(p);\n\t}\n\treturn out.sort((a, b) => b.policyVersion - a.policyVersion);\n}\n\nexport function deletePolicy(policyId: string): void {\n\tconst file = path.join(policiesDir(), `${policyId}.json`);\n\tif (fs.existsSync(file)) fs.unlinkSync(file);\n}\n\n// =============================================================================\n// Decisions\n// =============================================================================\n\nexport function writeDecision(decision: OrchestrationDecision): string {\n\tconst file = path.join(decisionsDir(), `${decision.decisionId}.json`);\n\tatomicWriteJson(file, decision);\n\treturn file;\n}\n\nexport function readDecision(decisionId: string): OrchestrationDecision | undefined {\n\treturn readJson<OrchestrationDecision>(path.join(decisionsDir(), `${decisionId}.json`));\n}\n\nexport function listDecisions(limit: number): OrchestrationDecision[] {\n\tensureDir(decisionsDir());\n\tconst out: OrchestrationDecision[] = [];\n\tfor (const f of fs.readdirSync(decisionsDir())) {\n\t\tif (!f.endsWith(\".json\")) continue;\n\t\tconst d = readJson<OrchestrationDecision>(path.join(decisionsDir(), f));\n\t\tif (d) out.push(d);\n\t}\n\treturn out.sort((a, b) => (a.selectedAt < b.selectedAt ? 1 : a.selectedAt > b.selectedAt ? -1 : 0)).slice(0, limit);\n}\n\n// =============================================================================\n// Evidence\n// =============================================================================\n\nexport function writeEvidence(evidence: CandidateEvidence): string {\n\tconst file = path.join(evidenceDir(), `${evidence.candidateId}@${evidence.evidenceHash}.json`);\n\tatomicWriteJson(file, evidence);\n\treturn file;\n}\n\nexport function readEvidence(candidateId: string): CandidateEvidence | undefined {\n\tensureDir(evidenceDir());\n\tlet best: CandidateEvidence | undefined;\n\tfor (const f of fs.readdirSync(evidenceDir())) {\n\t\tif (!f.startsWith(`${candidateId}@`)) continue;\n\t\tconst e = readJson<CandidateEvidence>(path.join(evidenceDir(), f));\n\t\tif (e && (!best || e.version > best.version)) best = e;\n\t}\n\treturn best;\n}\n\n// =============================================================================\n// Shadow decisions\n// =============================================================================\n\nexport function writeShadowDecision(shadow: ShadowDecision): string {\n\tconst file = path.join(shadowDir(), `${shadow.shadowId}.json`);\n\tatomicWriteJson(file, shadow);\n\treturn file;\n}\n\nexport function listShadowDecisions(limit: number): ShadowDecision[] {\n\tensureDir(shadowDir());\n\tconst out: ShadowDecision[] = [];\n\tfor (const f of fs.readdirSync(shadowDir())) {\n\t\tif (!f.endsWith(\".json\")) continue;\n\t\tconst s = readJson<ShadowDecision>(path.join(shadowDir(), f));\n\t\tif (s) out.push(s);\n\t}\n\treturn out.sort((a, b) => (a.recordedAt < b.recordedAt ? 1 : a.recordedAt > b.recordedAt ? -1 : 0)).slice(0, limit);\n}\n\n// =============================================================================\n// Events\n// =============================================================================\n\n/** Append an event to the current run's event log (bounded). Returns the event. */\nexport function appendEvent(\n\tevent: Omit<OrchestrationEvent, \"eventId\" | \"sequence\" | \"occurredAt\">,\n): OrchestrationEvent {\n\tensureDir(eventsDir());\n\tconst seq = fs.existsSync(path.join(eventsDir(), \"seq.txt\"))\n\t\t? Number(fs.readFileSync(path.join(eventsDir(), \"seq.txt\"), \"utf-8\")) + 1\n\t\t: 1;\n\tfs.writeFileSync(path.join(eventsDir(), \"seq.txt\"), String(seq), \"utf-8\");\n\tconst full: OrchestrationEvent = {\n\t\t...event,\n\t\teventId: randomUUID(),\n\t\tsequence: seq,\n\t\toccurredAt: new Date().toISOString(),\n\t};\n\t// Append-only log, kept bounded (trim oldest beyond 5000).\n\tconst logFile = path.join(eventsDir(), \"events.jsonl\");\n\tfs.appendFileSync(logFile, `${stableStringify(full)}\\n`, \"utf-8\");\n\ttrimEventLog(logFile);\n\treturn full;\n}\n\nfunction trimEventLog(logFile: string): void {\n\tconst MAX = 5000;\n\tlet lines: string[];\n\ttry {\n\t\tlines = fs.readFileSync(logFile, \"utf-8\").split(\"\\n\").filter(Boolean);\n\t} catch {\n\t\treturn;\n\t}\n\tif (lines.length <= MAX) return;\n\tconst trimmed = lines.slice(lines.length - MAX);\n\tfs.writeFileSync(logFile, `${trimmed.join(\"\\n\")}\\n`, \"utf-8\");\n}\n\nexport function listEvents(limit: number): OrchestrationEvent[] {\n\tensureDir(eventsDir());\n\tconst logFile = path.join(eventsDir(), \"events.jsonl\");\n\ttry {\n\t\tconst lines = fs.readFileSync(logFile, \"utf-8\").split(\"\\n\").filter(Boolean);\n\t\treturn lines\n\t\t\t.slice(-limit)\n\t\t\t.map((l) => {\n\t\t\t\ttry {\n\t\t\t\t\treturn JSON.parse(l) as OrchestrationEvent;\n\t\t\t\t} catch {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.filter((e): e is OrchestrationEvent => Boolean(e));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n// =============================================================================\n// Drift samples\n// =============================================================================\n\nexport function appendDriftSample(dimension: string, value: number): void {\n\tensureDir(driftDir());\n\tconst file = path.join(driftDir(), `${dimension}.jsonl`);\n\tfs.appendFileSync(file, `${JSON.stringify({ t: Date.now(), v: value })}\\n`, \"utf-8\");\n}\n\nexport function readDriftSamples(dimension: string): { t: number; v: number }[] {\n\tensureDir(driftDir());\n\tconst file = path.join(driftDir(), `${dimension}.jsonl`);\n\ttry {\n\t\treturn fs\n\t\t\t.readFileSync(file, \"utf-8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.filter(Boolean)\n\t\t\t.map((l) => {\n\t\t\t\ttry {\n\t\t\t\t\treturn JSON.parse(l) as { t: number; v: number };\n\t\t\t\t} catch {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.filter((s): s is { t: number; v: number } => Boolean(s));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n// =============================================================================\n// Registry scan (canonical subagent registry for candidate generation)\n// =============================================================================\n\nexport interface RegistrySnapshot {\n\tagents: { name: string; role: string; model: string; provider: string }[];\n\tretrievalPolicies: string[];\n\tbudgetClasses: string[];\n}\n\n/** Scan the canonical subagent registry files. Never authoritative; used only for candidate generation. */\nexport function scanSubagentRegistry(): RegistrySnapshot {\n\tconst agents: RegistrySnapshot[\"agents\"] = [];\n\ttry {\n\t\tconsole.debug(`scanning subagent registry at ${registryDir()}`);\n\t} catch {\n\t\t/* noop */\n\t}\n\t// Use the in-memory canonical registry store when available.\n\ttry {\n\t\tconst registry = getCanonicalSubagentRegistry();\n\t\tfor (const def of registry.list()) {\n\t\t\tagents.push({\n\t\t\t\tname: def.name,\n\t\t\t\trole: def.role,\n\t\t\t\tmodel: def.model,\n\t\t\t\tprovider: def.provider,\n\t\t\t});\n\t\t}\n\t} catch {\n\t\t// fall through to filesystem scan\n\t}\n\treturn {\n\t\tagents,\n\t\tretrievalPolicies: [\"none\", \"lexical\", \"symbolic\", \"semantic\", \"hybrid\", \"hybrid_reranked\"],\n\t\tbudgetClasses: [\"tiny\", \"small\", \"standard\", \"large\", \"high_assurance\", \"release\"],\n\t};\n}\n"]}