opencode-swarm 7.140.3 → 7.140.5
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/.opencode/skills/brainstorm/SKILL.md +1 -1
- package/.opencode/skills/ci-failure-batching/SKILL.md +11 -1
- package/.opencode/skills/ci-fix-monitor/SKILL.md +6 -4
- package/.opencode/skills/discover/SKILL.md +1 -1
- package/.opencode/skills/durable-session-state/SKILL.md +82 -0
- package/.opencode/skills/engineering-conventions/SKILL.md +1 -1
- package/.opencode/skills/issue-ingest/SKILL.md +13 -7
- package/.opencode/skills/issue-tracer/SKILL.md +1 -1
- package/.opencode/skills/orchestrating-subagents/SKILL.md +113 -0
- package/.opencode/skills/parallel-work-check/SKILL.md +1 -1
- package/.opencode/skills/plan/SKILL.md +40 -12
- package/.opencode/skills/swarm-ci-monitor/SKILL.md +5 -3
- package/.opencode/skills/swarm-pr-feedback/SKILL.md +30 -4
- package/.opencode/skills/writing-tests/SKILL.md +1 -1
- package/dist/agents/architect.d.ts +2 -1
- package/dist/agents/reviewer-directive-compliance.d.ts +19 -5
- package/dist/background/pending-delegations.d.ts +5 -0
- package/dist/cli/{config-doctor-fnmfd35x.js → config-doctor-5vzpq4qa.js} +3 -3
- package/dist/cli/{core-p5cyej3r.js → core-xjzdc56e.js} +2 -2
- package/dist/cli/{curation-policy-d52h4g8t.js → curation-policy-avvb56x2.js} +6 -8
- package/dist/cli/{curator-3zpdqt9r.js → curator-5nytvg7p.js} +38 -38
- package/dist/cli/curator-llm-factory-rds75zhj.js +61 -0
- package/dist/cli/{evidence-summary-service-nfdc1ht9.js → evidence-summary-service-zq4gyx6g.js} +15 -12
- package/dist/cli/gate-evidence-smn2wqft.js +51 -0
- package/dist/cli/guardrail-explain-an7jvam1.js +62 -0
- package/dist/cli/{guardrail-log-82da8y5a.js → guardrail-log-0hnd2k9v.js} +4 -4
- package/dist/cli/{hive-promoter-af5yaxvt.js → hive-promoter-qe8pqg79.js} +38 -38
- package/dist/cli/{index-qksjaxpd.js → index-01ggt2mj.js} +49 -18
- package/dist/cli/{index-j1xmren9.js → index-0r2m0vbq.js} +12 -10
- package/dist/cli/{index-pg7n141e.js → index-0rx9t9w8.js} +1 -1
- package/dist/cli/{index-brh827g0.js → index-2bg901qt.js} +73 -32
- package/dist/cli/{index-8n4c3vec.js → index-386ts4tm.js} +4 -4
- package/dist/cli/{index-898f16j9.js → index-8rp1y4ab.js} +2 -2
- package/dist/cli/{index-hfvxhk4z.js → index-912qz4y0.js} +88 -88
- package/dist/cli/index-affh1gtg.js +2554 -0
- package/dist/cli/{index-1m1ksyqt.js → index-c432knmy.js} +1 -1
- package/dist/cli/{index-jvk119ck.js → index-e0mpz2j3.js} +54 -19
- package/dist/cli/{index-ry0hgg3m.js → index-epvhf50h.js} +37 -3
- package/dist/cli/{index-95bj4kaw.js → index-ex7r16gc.js} +2 -2
- package/dist/cli/{index-7d5jseea.js → index-jm4cpp4n.js} +5 -2
- package/dist/cli/{index-n408rw7n.js → index-jqsfsqbx.js} +27 -2
- package/dist/cli/{index-7kdd75se.js → index-kb5q9v34.js} +11 -9
- package/dist/cli/{index-jfjnn408.js → index-m9xrdjn0.js} +1 -1
- package/dist/cli/{index-pj9h75cc.js → index-mm2egvdt.js} +40 -40
- package/dist/cli/{index-8bnzzv3b.js → index-nwrwy8eb.js} +70 -34
- package/dist/cli/index-nyjtzmsj.js +43 -0
- package/dist/cli/{index-h2waehqq.js → index-p8kk34am.js} +3 -3
- package/dist/cli/{index-wf35ka4z.js → index-ptn13xjn.js} +3 -3
- package/dist/cli/{index-1kyex7z2.js → index-qwm6vdyv.js} +32 -1
- package/dist/cli/{index-p9dsb0tw.js → index-qy1nq52t.js} +2 -19
- package/dist/cli/{index-b9rd29ge.js → index-s1v0wza5.js} +6 -2
- package/dist/cli/{index-yyyk63kk.js → index-t5pytnca.js} +2 -2
- package/dist/cli/index-tjqzr3jc.js +623 -0
- package/dist/cli/{index-rs1f1bh0.js → index-vh05t3w5.js} +1 -1
- package/dist/cli/{index-adyxbmt5.js → index-wkt9fj06.js} +4 -4
- package/dist/cli/{index-kbfsp07d.js → index-wrb0gykj.js} +217 -15
- package/dist/cli/{index-xz8f1n1a.js → index-z7h37d63.js} +2 -2
- package/dist/cli/{index-q1q8b85k.js → index-z90nvqj3.js} +49315 -47331
- package/dist/cli/{index-b8zgtz81.js → index-zsrtt42s.js} +34 -1
- package/dist/cli/{index-gtbmabws.js → index-ztgn70jz.js} +2 -2
- package/dist/cli/index.js +37 -37
- package/dist/cli/{knowledge-escalator-7q59skqg.js → knowledge-escalator-4pfrc902.js} +20 -8
- package/dist/cli/{knowledge-events-3nwfdpq7.js → knowledge-events-3zkfnksk.js} +19 -6
- package/dist/cli/{knowledge-link-q46k9adx.js → knowledge-link-m9p80f4r.js} +5 -5
- package/dist/cli/{knowledge-store-0d83rhh3.js → knowledge-store-dpqgvvjt.js} +6 -6
- package/dist/cli/{knowledge-validator-6mfx91a2.js → knowledge-validator-2c7w87dk.js} +8 -9
- package/dist/cli/{pending-delegations-qzrx38y9.js → pending-delegations-cxx9tezp.js} +4 -4
- package/dist/cli/{pr-subscriptions-5s59h4dd.js → pr-subscriptions-2sfnb599.js} +4 -4
- package/dist/cli/{runner-y78d7ryb.js → runner-mky3vvht.js} +6 -6
- package/dist/cli/{scan-cursor-rc8q12ss.js → scan-cursor-1z60gwrw.js} +7 -7
- package/dist/cli/{schema-n9eaxp3a.js → schema-vpnkwvez.js} +2 -2
- package/dist/cli/{scope-persistence-dr1msqxw.js → scope-persistence-p9dmprry.js} +10 -5
- package/dist/cli/{skill-generator-q7c6vadg.js → skill-generator-yx7ka1cj.js} +19 -10
- package/dist/cli/{telemetry-sc57xtb0.js → telemetry-0x6kszvk.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-7a8e6gr9.js → worktree-collision-ownership-wm5pse49.js} +8 -8
- package/dist/cli/worktree-isolation-bwc6ra8e.js +102 -0
- package/dist/cli/worktree-provisioning-owner-sjh7te1h.js +22 -0
- package/dist/commands/acknowledge-spec-drift.d.ts +4 -5
- package/dist/commands/ci-simulate.d.ts +7 -3
- package/dist/commands/close.d.ts +8 -0
- package/dist/commands/registry.d.ts +2 -2
- package/dist/commands/sdd.d.ts +15 -0
- package/dist/config/bundled-skills.d.ts +1 -1
- package/dist/config/evidence-schema.d.ts +3 -3
- package/dist/config/index.d.ts +2 -2
- package/dist/config/plan-schema.d.ts +8 -0
- package/dist/config/schema.d.ts +4 -2
- package/dist/council/council-evidence-writer.d.ts +1 -1
- package/dist/gate-evidence.d.ts +117 -4
- package/dist/git/branch.d.ts +1 -1
- package/dist/hooks/delegate-ack-collector.d.ts +5 -0
- package/dist/hooks/delegation-gate.d.ts +21 -0
- package/dist/hooks/hive-promoter.d.ts +4 -4
- package/dist/hooks/issue-trace-reducer.d.ts +7 -1
- package/dist/hooks/issue-trace-state.d.ts +18 -0
- package/dist/hooks/issue-trace.d.ts +3 -1
- package/dist/hooks/knowledge-application-gate.d.ts +8 -3
- package/dist/hooks/knowledge-application.d.ts +8 -5
- package/dist/hooks/knowledge-escalator.d.ts +9 -5
- package/dist/hooks/knowledge-events.d.ts +21 -8
- package/dist/hooks/knowledge-injector.d.ts +9 -0
- package/dist/hooks/knowledge-receipt-ledger-storage.d.ts +49 -0
- package/dist/hooks/knowledge-receipt-ledger.d.ts +305 -0
- package/dist/hooks/knowledge-receipt-observability.d.ts +28 -0
- package/dist/hooks/knowledge-receipt-validator.d.ts +13 -50
- package/dist/hooks/knowledge-store.d.ts +6 -1
- package/dist/hooks/knowledge-types.d.ts +12 -0
- package/dist/hooks/phase-complete-directive-gate.d.ts +3 -1
- package/dist/hooks/phase-directives.d.ts +4 -4
- package/dist/hooks/pr-workflow-gate.d.ts +34 -1
- package/dist/hooks/promotion-evidence-store.d.ts +14 -8
- package/dist/hooks/reviewer-verdict-parser.d.ts +34 -10
- package/dist/index.js +823 -779
- package/dist/knowledge/curation-policy.d.ts +0 -2
- package/dist/observability/catalog.d.ts +5 -3
- package/dist/observability/legacy.d.ts +4 -3
- package/dist/plan/ledger.d.ts +55 -0
- package/dist/plan/manager.d.ts +9 -0
- package/dist/plan/planning-profile.d.ts +27 -0
- package/dist/services/spec-drift-recovery.d.ts +84 -0
- package/dist/session/snapshot-writer.d.ts +3 -1
- package/dist/state.d.ts +19 -17
- package/dist/summaries/schema.d.ts +2 -2
- package/dist/telemetry.d.ts +1 -1
- package/dist/tools/convene-council.d.ts +6 -6
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/manifest.d.ts +3 -0
- package/dist/tools/phase-complete.d.ts +9 -0
- package/dist/tools/rebind-pr-feedback-head.d.ts +17 -0
- package/dist/tools/record-implementation-review.d.ts +19 -0
- package/dist/tools/record-recurrence-sweep.d.ts +17 -0
- package/dist/tools/run-pr-feedback-stage-a.d.ts +8 -0
- package/dist/tools/save-plan.d.ts +8 -0
- package/dist/tools/submit-phase-council-verdicts.d.ts +6 -6
- package/dist/tools/tool-metadata.d.ts +40 -28
- package/dist/tools/update-task-status.d.ts +13 -0
- package/dist/tools/write-final-council-evidence.d.ts +6 -6
- package/dist/types/events.d.ts +29 -1
- package/dist/workflow/coder-settlement.d.ts +62 -0
- package/dist/workflow/task-repair.d.ts +30 -0
- package/dist/workflow/task-terminal.d.ts +52 -0
- package/package.json +3 -1
- package/dist/cli/curator-llm-factory-2h0xb62t.js +0 -61
- package/dist/cli/gate-evidence-46qeyfc1.js +0 -32
- package/dist/cli/guardrail-explain-yktan15c.js +0 -62
- package/dist/cli/index-d181x9zr.js +0 -71
- package/dist/cli/index-n0pja0qs.js +0 -239
- package/dist/cli/index-n8jncheh.js +0 -666
|
@@ -0,0 +1,2554 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
validateSwarmPath
|
|
4
|
+
} from "./index-k46me75h.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveHiveEventsPath
|
|
7
|
+
} from "./index-ae75rja9.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveKnowledgeStoreDir
|
|
10
|
+
} from "./index-vh05t3w5.js";
|
|
11
|
+
import {
|
|
12
|
+
atomicWriteFile
|
|
13
|
+
} from "./index-p8kk34am.js";
|
|
14
|
+
import {
|
|
15
|
+
assertProjectRoot
|
|
16
|
+
} from "./index-912qz4y0.js";
|
|
17
|
+
import {
|
|
18
|
+
require_proper_lockfile
|
|
19
|
+
} from "./index-7g4c7s5r.js";
|
|
20
|
+
import {
|
|
21
|
+
loadPluginConfigWithMeta
|
|
22
|
+
} from "./index-nwrwy8eb.js";
|
|
23
|
+
import {
|
|
24
|
+
init_logger,
|
|
25
|
+
warn
|
|
26
|
+
} from "./index-zgwm4ryv.js";
|
|
27
|
+
import {
|
|
28
|
+
KnowledgeConfigSchema,
|
|
29
|
+
init_schema
|
|
30
|
+
} from "./index-jm4cpp4n.js";
|
|
31
|
+
import {
|
|
32
|
+
emit
|
|
33
|
+
} from "./index-epvhf50h.js";
|
|
34
|
+
import {
|
|
35
|
+
__require,
|
|
36
|
+
__toESM
|
|
37
|
+
} from "./index-a76rekgs.js";
|
|
38
|
+
|
|
39
|
+
// src/hooks/knowledge-events.ts
|
|
40
|
+
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
41
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
42
|
+
import { existsSync as existsSync3 } from "fs";
|
|
43
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
44
|
+
import * as path2 from "path";
|
|
45
|
+
init_logger();
|
|
46
|
+
|
|
47
|
+
// src/hooks/knowledge-receipt-ledger.ts
|
|
48
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
49
|
+
import { existsSync as existsSync2 } from "fs";
|
|
50
|
+
init_schema();
|
|
51
|
+
|
|
52
|
+
// src/hooks/knowledge-receipt-ledger-storage.ts
|
|
53
|
+
import { createHash, randomUUID } from "crypto";
|
|
54
|
+
import * as fs from "fs";
|
|
55
|
+
import {
|
|
56
|
+
lstat,
|
|
57
|
+
mkdir,
|
|
58
|
+
open,
|
|
59
|
+
readFile,
|
|
60
|
+
realpath,
|
|
61
|
+
rename,
|
|
62
|
+
rm,
|
|
63
|
+
stat
|
|
64
|
+
} from "fs/promises";
|
|
65
|
+
import * as path from "path";
|
|
66
|
+
var RECEIPT_SCHEMA_VERSION = 2;
|
|
67
|
+
var RECEIPT_CUTOVER_VERSION = 1;
|
|
68
|
+
var LOCK_RETRY_MS = 25;
|
|
69
|
+
var LOCK_TIMEOUT_MS = 500;
|
|
70
|
+
var UNINITIALIZED_LOCK_STALE_MS = 30000;
|
|
71
|
+
var PROC_STATE_READ_TIMEOUT_MS = 25;
|
|
72
|
+
|
|
73
|
+
class ReceiptStoreError extends Error {
|
|
74
|
+
code;
|
|
75
|
+
constructor(code, message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.code = code;
|
|
78
|
+
this.name = "ReceiptStoreError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function resolveReceiptLedgerPaths(directory) {
|
|
82
|
+
assertProjectRoot(directory, undefined, "knowledge receipt state");
|
|
83
|
+
let root;
|
|
84
|
+
try {
|
|
85
|
+
root = fs.realpathSync(directory);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new ReceiptStoreError("store_unavailable", "cannot canonicalize project root");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
root,
|
|
91
|
+
swarmDir: path.join(root, ".swarm"),
|
|
92
|
+
journal: validateSwarmPath(root, "knowledge-receipts-v2.jsonl"),
|
|
93
|
+
snapshot: validateSwarmPath(root, "knowledge-receipts-v2.snapshot.json"),
|
|
94
|
+
archive: validateSwarmPath(root, "knowledge-receipts-v2-archive.jsonl"),
|
|
95
|
+
quarantine: validateSwarmPath(root, "knowledge-receipts-v2-quarantine.json"),
|
|
96
|
+
lockTarget: validateSwarmPath(root, "knowledge-receipts-v2.lock"),
|
|
97
|
+
legacyEvents: validateSwarmPath(root, "knowledge-events.jsonl"),
|
|
98
|
+
legacyBaseline: validateSwarmPath(root, "knowledge-counter-baseline.json"),
|
|
99
|
+
linkPointer: validateSwarmPath(root, "link.json")
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function validateExistingRegularFile(filePath) {
|
|
103
|
+
if (!fs.existsSync(filePath))
|
|
104
|
+
return;
|
|
105
|
+
const info = fs.lstatSync(filePath);
|
|
106
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
107
|
+
throw new ReceiptStoreError("store_unavailable", `receipt artifact is not a regular file: ${path.basename(filePath)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function preparePaths(paths) {
|
|
111
|
+
await mkdir(paths.swarmDir, { recursive: true });
|
|
112
|
+
const swarmInfo = fs.lstatSync(paths.swarmDir);
|
|
113
|
+
if (swarmInfo.isSymbolicLink() || !swarmInfo.isDirectory()) {
|
|
114
|
+
throw new ReceiptStoreError("store_unavailable", ".swarm is not a real project-local directory");
|
|
115
|
+
}
|
|
116
|
+
if (fs.realpathSync(path.dirname(paths.journal)) !== fs.realpathSync(paths.swarmDir)) {
|
|
117
|
+
throw new ReceiptStoreError("store_unavailable", "receipt artifact parent escaped the project .swarm directory");
|
|
118
|
+
}
|
|
119
|
+
for (const artifact of [
|
|
120
|
+
paths.journal,
|
|
121
|
+
paths.snapshot,
|
|
122
|
+
paths.archive,
|
|
123
|
+
paths.quarantine,
|
|
124
|
+
paths.lockTarget
|
|
125
|
+
]) {
|
|
126
|
+
validateExistingRegularFile(artifact);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function samePath(left, right) {
|
|
130
|
+
const normalize = (value) => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
131
|
+
return normalize(left) === normalize(right);
|
|
132
|
+
}
|
|
133
|
+
async function validateMutationParent(filePath, dependencies = { lstat, realpath }) {
|
|
134
|
+
const parent = path.dirname(path.resolve(filePath));
|
|
135
|
+
const info = await dependencies.lstat(parent);
|
|
136
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
137
|
+
throw new ReceiptStoreError("store_unavailable", "receipt artifact parent is not a real directory");
|
|
138
|
+
}
|
|
139
|
+
const canonical = await dependencies.realpath(parent);
|
|
140
|
+
const canonicalInfo = await dependencies.lstat(canonical);
|
|
141
|
+
if (canonicalInfo.isSymbolicLink() || !canonicalInfo.isDirectory() || !samePath(canonical, parent) && !sameIdentity({ dev: info.dev, ino: info.ino }, { dev: canonicalInfo.dev, ino: canonicalInfo.ino })) {
|
|
142
|
+
throw new ReceiptStoreError("store_unavailable", "receipt artifact parent identity changed before mutation");
|
|
143
|
+
}
|
|
144
|
+
return canonical;
|
|
145
|
+
}
|
|
146
|
+
async function existingFileIdentity(filePath) {
|
|
147
|
+
try {
|
|
148
|
+
const info = await lstat(filePath);
|
|
149
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
150
|
+
throw new ReceiptStoreError("store_unavailable", `receipt artifact is not a regular file: ${path.basename(filePath)}`);
|
|
151
|
+
}
|
|
152
|
+
return { dev: info.dev, ino: info.ino };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code === "ENOENT")
|
|
155
|
+
return null;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function sameIdentity(left, right) {
|
|
160
|
+
return left !== null && right !== null && left.dev === right.dev && left.ino === right.ino;
|
|
161
|
+
}
|
|
162
|
+
async function syncParentDirectory(parent) {
|
|
163
|
+
let handle;
|
|
164
|
+
try {
|
|
165
|
+
handle = await open(parent, "r");
|
|
166
|
+
await handle.sync();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const code = error.code;
|
|
169
|
+
if (process.platform !== "win32" || code !== "EISDIR" && code !== "EPERM" && code !== "EACCES") {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
await handle?.close().catch(() => {
|
|
174
|
+
return;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function writeAll(handle, content) {
|
|
179
|
+
const bytes = Buffer.from(content, "utf8");
|
|
180
|
+
let offset = 0;
|
|
181
|
+
while (offset < bytes.length) {
|
|
182
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, null);
|
|
183
|
+
if (bytesWritten <= 0)
|
|
184
|
+
throw new Error("receipt write made no progress");
|
|
185
|
+
offset += bytesWritten;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function readLinuxProcStat(pid, signal) {
|
|
189
|
+
return await readFile(`/proc/${pid}/stat`, { encoding: "utf8", signal });
|
|
190
|
+
}
|
|
191
|
+
async function isProcessAlive(pid) {
|
|
192
|
+
try {
|
|
193
|
+
_internals.killProcess(pid);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return error.code !== "ESRCH";
|
|
196
|
+
}
|
|
197
|
+
if (_internals.platform() !== "linux")
|
|
198
|
+
return true;
|
|
199
|
+
const controller = new AbortController;
|
|
200
|
+
let timer;
|
|
201
|
+
try {
|
|
202
|
+
const procRead = _internals.readLinuxProcStat(pid, controller.signal).catch(() => null);
|
|
203
|
+
const timedOut = new Promise((resolve2) => {
|
|
204
|
+
timer = setTimeout(() => {
|
|
205
|
+
controller.abort();
|
|
206
|
+
resolve2(null);
|
|
207
|
+
}, _internals.procStateReadTimeoutMs);
|
|
208
|
+
});
|
|
209
|
+
const statLine = await Promise.race([procRead, timedOut]);
|
|
210
|
+
if (statLine === null)
|
|
211
|
+
return true;
|
|
212
|
+
const commandEnd = statLine.lastIndexOf(")");
|
|
213
|
+
if (commandEnd < 0)
|
|
214
|
+
return true;
|
|
215
|
+
const state = statLine.slice(commandEnd + 1).trimStart().charAt(0);
|
|
216
|
+
return state !== "Z";
|
|
217
|
+
} finally {
|
|
218
|
+
if (timer !== undefined)
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function readLockOwner(lockPath, expectedRoot) {
|
|
223
|
+
try {
|
|
224
|
+
validateExistingRegularFile(lockPath);
|
|
225
|
+
const parsed = JSON.parse(await readFile(lockPath, "utf8"));
|
|
226
|
+
if (typeof parsed.owner_token !== "string" || !parsed.owner_token || !Number.isInteger(parsed.pid) || (parsed.pid ?? 0) <= 0 || !Number.isFinite(parsed.created_at_ms) || typeof parsed.root_identity !== "string" || !parsed.root_identity || expectedRoot !== undefined && !samePath(parsed.root_identity, expectedRoot)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return parsed;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error.code === "ENOENT")
|
|
232
|
+
return null;
|
|
233
|
+
if (error instanceof SyntaxError)
|
|
234
|
+
return null;
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function recoverLockIfSafe(paths) {
|
|
239
|
+
const lockPath = paths.lockTarget;
|
|
240
|
+
const expectedRoot = paths.root;
|
|
241
|
+
const recoveryOwner = {
|
|
242
|
+
owner_token: randomUUID(),
|
|
243
|
+
pid: process.pid,
|
|
244
|
+
created_at_ms: Date.now(),
|
|
245
|
+
root_identity: expectedRoot
|
|
246
|
+
};
|
|
247
|
+
const parent = await validateMutationParent(lockPath);
|
|
248
|
+
let claimedPath;
|
|
249
|
+
let info;
|
|
250
|
+
try {
|
|
251
|
+
info = await stat(lockPath);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.code === "ENOENT")
|
|
254
|
+
return;
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
const owner = await readLockOwner(lockPath, expectedRoot);
|
|
258
|
+
if (owner && await _internals.isProcessAlive(owner.pid))
|
|
259
|
+
return;
|
|
260
|
+
if (!owner && Date.now() - info.mtimeMs < UNINITIALIZED_LOCK_STALE_MS)
|
|
261
|
+
return;
|
|
262
|
+
claimedPath = `${lockPath}.${recoveryOwner.owner_token}.recovering`;
|
|
263
|
+
try {
|
|
264
|
+
await _internals.rename(lockPath, claimedPath);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error.code === "ENOENT")
|
|
267
|
+
return;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
await _internals.afterRecoveryRename(paths, claimedPath);
|
|
271
|
+
const claimedInfo = await stat(claimedPath);
|
|
272
|
+
const claimedOwner = await readLockOwner(claimedPath, expectedRoot);
|
|
273
|
+
const sameObservedOwner = owner ? claimedOwner?.owner_token === owner.owner_token : claimedOwner === null;
|
|
274
|
+
const stillRecoverable = owner ? sameObservedOwner && !await _internals.isProcessAlive(owner.pid) : sameObservedOwner && Date.now() - claimedInfo.mtimeMs >= UNINITIALIZED_LOCK_STALE_MS;
|
|
275
|
+
if (stillRecoverable) {
|
|
276
|
+
await _internals.rm(claimedPath, { force: true });
|
|
277
|
+
claimedPath = undefined;
|
|
278
|
+
await syncParentDirectory(parent);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!fs.existsSync(lockPath)) {
|
|
282
|
+
await _internals.rename(claimedPath, lockPath);
|
|
283
|
+
claimedPath = undefined;
|
|
284
|
+
await syncParentDirectory(parent);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
throw new ReceiptStoreError("store_unavailable", "receipt recovery claim changed while a successor lock exists");
|
|
288
|
+
}
|
|
289
|
+
async function acquireReceiptLock(paths) {
|
|
290
|
+
const lockPath = paths.lockTarget;
|
|
291
|
+
const owner = {
|
|
292
|
+
owner_token: randomUUID(),
|
|
293
|
+
pid: process.pid,
|
|
294
|
+
created_at_ms: Date.now(),
|
|
295
|
+
root_identity: paths.root
|
|
296
|
+
};
|
|
297
|
+
const deadline = performance.now() + LOCK_TIMEOUT_MS;
|
|
298
|
+
for (;; ) {
|
|
299
|
+
let handle;
|
|
300
|
+
try {
|
|
301
|
+
const parent = await validateMutationParent(lockPath);
|
|
302
|
+
handle = await open(lockPath, "wx");
|
|
303
|
+
await writeAll(handle, `${JSON.stringify(owner)}
|
|
304
|
+
`);
|
|
305
|
+
await handle.sync();
|
|
306
|
+
const info = await handle.stat();
|
|
307
|
+
if (!info.isFile())
|
|
308
|
+
throw new Error("receipt lock is not a regular file");
|
|
309
|
+
await handle.close();
|
|
310
|
+
handle = undefined;
|
|
311
|
+
validateExistingRegularFile(lockPath);
|
|
312
|
+
await syncParentDirectory(parent);
|
|
313
|
+
return async () => {
|
|
314
|
+
const current = await readLockOwner(lockPath, paths.root);
|
|
315
|
+
if (current?.owner_token === owner.owner_token) {
|
|
316
|
+
await rm(lockPath, { force: true });
|
|
317
|
+
await syncParentDirectory(parent);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
} catch (error) {
|
|
321
|
+
await handle?.close().catch(() => {
|
|
322
|
+
return;
|
|
323
|
+
});
|
|
324
|
+
if (error.code !== "EEXIST")
|
|
325
|
+
throw error;
|
|
326
|
+
await _internals.recoverLockIfSafe(paths);
|
|
327
|
+
const remainingMs = deadline - performance.now();
|
|
328
|
+
if (remainingMs <= 0)
|
|
329
|
+
break;
|
|
330
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(LOCK_RETRY_MS, remainingMs)));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw new ReceiptStoreError("lock_timeout", "receipt correctness lock unavailable before bounded timeout");
|
|
334
|
+
}
|
|
335
|
+
async function withReceiptLedgerLock(directory, action) {
|
|
336
|
+
const paths = resolveReceiptLedgerPaths(directory);
|
|
337
|
+
try {
|
|
338
|
+
await preparePaths(paths);
|
|
339
|
+
const release = await acquireReceiptLock(paths);
|
|
340
|
+
try {
|
|
341
|
+
resolveReceiptLedgerPaths(paths.root);
|
|
342
|
+
await preparePaths(paths);
|
|
343
|
+
return await action(paths);
|
|
344
|
+
} finally {
|
|
345
|
+
await release().catch(() => {
|
|
346
|
+
return;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (error instanceof ReceiptStoreError)
|
|
351
|
+
throw error;
|
|
352
|
+
throw new ReceiptStoreError("store_unavailable", error instanceof Error ? error.message : String(error));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function receiptRecordHash(value) {
|
|
356
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
357
|
+
}
|
|
358
|
+
async function readUtf8IfPresent(filePath, maxBytes = 32 * 1024 * 1024) {
|
|
359
|
+
try {
|
|
360
|
+
validateExistingRegularFile(filePath);
|
|
361
|
+
const info = await stat(filePath);
|
|
362
|
+
if (info.size > maxBytes) {
|
|
363
|
+
throw new ReceiptStoreError("store_corrupt", `receipt artifact exceeds ${maxBytes} bytes: ${path.basename(filePath)}`);
|
|
364
|
+
}
|
|
365
|
+
return await readFile(filePath, "utf8");
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error.code === "ENOENT")
|
|
368
|
+
return null;
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function appendFsynced(filePath, line) {
|
|
373
|
+
const parent = await validateMutationParent(filePath);
|
|
374
|
+
const before = await existingFileIdentity(filePath);
|
|
375
|
+
const handle = await open(filePath, "a");
|
|
376
|
+
try {
|
|
377
|
+
const opened = await handle.stat();
|
|
378
|
+
if (!opened.isFile())
|
|
379
|
+
throw new Error("receipt journal is not a regular file");
|
|
380
|
+
if (before && (opened.dev !== before.dev || opened.ino !== before.ino)) {
|
|
381
|
+
throw new ReceiptStoreError("store_unavailable", "receipt journal identity changed before append");
|
|
382
|
+
}
|
|
383
|
+
await writeAll(handle, line);
|
|
384
|
+
await handle.sync();
|
|
385
|
+
const after = await existingFileIdentity(filePath);
|
|
386
|
+
if (!after || after.dev !== opened.dev || after.ino !== opened.ino || !samePath(await realpath(path.dirname(filePath)), parent)) {
|
|
387
|
+
throw new ReceiptStoreError("store_unavailable", "receipt journal identity changed during append");
|
|
388
|
+
}
|
|
389
|
+
} finally {
|
|
390
|
+
await handle.close();
|
|
391
|
+
}
|
|
392
|
+
if (!before)
|
|
393
|
+
await syncParentDirectory(parent);
|
|
394
|
+
}
|
|
395
|
+
async function renameWithRetry(tempPath, targetPath) {
|
|
396
|
+
let lastError;
|
|
397
|
+
for (let attempt = 0;attempt < 6; attempt++) {
|
|
398
|
+
try {
|
|
399
|
+
await rename(tempPath, targetPath);
|
|
400
|
+
return;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
lastError = error;
|
|
403
|
+
const code = error.code;
|
|
404
|
+
if (code !== "EPERM" && code !== "EBUSY" && code !== "EEXIST")
|
|
405
|
+
throw error;
|
|
406
|
+
await new Promise((resolve2) => setTimeout(resolve2, 10 * (attempt + 1)));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
throw lastError;
|
|
410
|
+
}
|
|
411
|
+
async function atomicWriteFsynced(targetPath, content) {
|
|
412
|
+
const parent = await validateMutationParent(targetPath);
|
|
413
|
+
await existingFileIdentity(targetPath);
|
|
414
|
+
const tempPath = `${targetPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
415
|
+
const handle = await open(tempPath, "wx");
|
|
416
|
+
let tempIdentity;
|
|
417
|
+
try {
|
|
418
|
+
await writeAll(handle, content);
|
|
419
|
+
await handle.sync();
|
|
420
|
+
const info = await handle.stat();
|
|
421
|
+
if (!info.isFile())
|
|
422
|
+
throw new Error("receipt temp is not a regular file");
|
|
423
|
+
tempIdentity = { dev: info.dev, ino: info.ino };
|
|
424
|
+
} finally {
|
|
425
|
+
await handle.close();
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const onDiskTemp = await existingFileIdentity(tempPath);
|
|
429
|
+
if (!sameIdentity(onDiskTemp, tempIdentity)) {
|
|
430
|
+
throw new ReceiptStoreError("store_unavailable", "receipt temp identity changed before rename");
|
|
431
|
+
}
|
|
432
|
+
if (!samePath(await realpath(path.dirname(targetPath)), parent)) {
|
|
433
|
+
throw new ReceiptStoreError("store_unavailable", "receipt artifact parent identity changed before rename");
|
|
434
|
+
}
|
|
435
|
+
await existingFileIdentity(targetPath);
|
|
436
|
+
await renameWithRetry(tempPath, targetPath);
|
|
437
|
+
const targetIdentity = await existingFileIdentity(targetPath);
|
|
438
|
+
if (!sameIdentity(targetIdentity, tempIdentity)) {
|
|
439
|
+
throw new ReceiptStoreError("store_unavailable", "receipt target identity changed during rename");
|
|
440
|
+
}
|
|
441
|
+
if (!samePath(await realpath(path.dirname(targetPath)), parent)) {
|
|
442
|
+
throw new ReceiptStoreError("store_unavailable", "receipt artifact parent identity changed after rename");
|
|
443
|
+
}
|
|
444
|
+
await syncParentDirectory(parent);
|
|
445
|
+
} finally {
|
|
446
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
447
|
+
return;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
var _internals = {
|
|
452
|
+
platform: () => process.platform,
|
|
453
|
+
killProcess: (pid) => {
|
|
454
|
+
process.kill(pid, 0);
|
|
455
|
+
},
|
|
456
|
+
readLinuxProcStat,
|
|
457
|
+
procStateReadTimeoutMs: PROC_STATE_READ_TIMEOUT_MS,
|
|
458
|
+
isProcessAlive,
|
|
459
|
+
rename,
|
|
460
|
+
rm,
|
|
461
|
+
afterRecoveryRename: async (_paths, _claimedPath) => {},
|
|
462
|
+
recoverLockIfSafe,
|
|
463
|
+
validateMutationParent
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/hooks/knowledge-receipt-observability.ts
|
|
467
|
+
var TRANSITIONS = new Set([
|
|
468
|
+
"membership_committed",
|
|
469
|
+
"empty_retrieval_committed",
|
|
470
|
+
"terminal_committed",
|
|
471
|
+
"terminal_attempt_rejected",
|
|
472
|
+
"terminal_attempt_idempotent",
|
|
473
|
+
"authorized_transition_committed",
|
|
474
|
+
"application_marker_committed",
|
|
475
|
+
"phase_close_intent",
|
|
476
|
+
"phase_closed",
|
|
477
|
+
"legacy_imported",
|
|
478
|
+
"legacy_unverifiable",
|
|
479
|
+
"cutover_completed",
|
|
480
|
+
"checkpoint"
|
|
481
|
+
]);
|
|
482
|
+
var REASON_CODES = new Set([
|
|
483
|
+
"committed",
|
|
484
|
+
"idempotent",
|
|
485
|
+
"trace_not_found",
|
|
486
|
+
"id_not_in_trace",
|
|
487
|
+
"wrong_session",
|
|
488
|
+
"wrong_phase",
|
|
489
|
+
"wrong_task",
|
|
490
|
+
"duplicate_conflicting_terminal",
|
|
491
|
+
"event_id_conflict",
|
|
492
|
+
"invalid_outcome",
|
|
493
|
+
"empty_receipt",
|
|
494
|
+
"lock_timeout",
|
|
495
|
+
"store_unavailable",
|
|
496
|
+
"store_corrupt",
|
|
497
|
+
"legacy_unverifiable",
|
|
498
|
+
"unauthorized_transition"
|
|
499
|
+
]);
|
|
500
|
+
var RECEIPT_OUTCOMES = new Set([
|
|
501
|
+
"applied",
|
|
502
|
+
"ignored",
|
|
503
|
+
"contradicted",
|
|
504
|
+
"violated",
|
|
505
|
+
"n_a",
|
|
506
|
+
"no_relevant"
|
|
507
|
+
]);
|
|
508
|
+
var BOUNDED_CODE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
509
|
+
function boundedCode(value) {
|
|
510
|
+
return typeof value === "string" && BOUNDED_CODE.test(value) ? value : undefined;
|
|
511
|
+
}
|
|
512
|
+
function boundedPhase(value) {
|
|
513
|
+
const code = boundedCode(value);
|
|
514
|
+
return code && /^phase(?:[_-][a-z0-9_-]+)?$/.test(code) ? code : undefined;
|
|
515
|
+
}
|
|
516
|
+
function addIdentifier(payload, key, value) {
|
|
517
|
+
if (typeof value === "string" && value.length > 0)
|
|
518
|
+
payload[key] = value;
|
|
519
|
+
}
|
|
520
|
+
function emitKnowledgeReceiptTransition(observation) {
|
|
521
|
+
try {
|
|
522
|
+
if (!TRANSITIONS.has(observation.transition) || !REASON_CODES.has(observation.reasonCode) || !Number.isSafeInteger(observation.schemaVersion) || observation.schemaVersion < 1) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const payload = {
|
|
526
|
+
transition: observation.transition,
|
|
527
|
+
reasonCode: observation.reasonCode,
|
|
528
|
+
schemaVersion: observation.schemaVersion
|
|
529
|
+
};
|
|
530
|
+
addIdentifier(payload, "knowledgeTraceId", observation.knowledgeTraceId);
|
|
531
|
+
addIdentifier(payload, "knowledgeEntryId", observation.knowledgeEntryId);
|
|
532
|
+
addIdentifier(payload, "sessionId", observation.sessionId);
|
|
533
|
+
addIdentifier(payload, "taskId", observation.taskId);
|
|
534
|
+
addIdentifier(payload, "phase", boundedPhase(observation.phase));
|
|
535
|
+
if (observation.receiptOutcome !== undefined && RECEIPT_OUTCOMES.has(observation.receiptOutcome)) {
|
|
536
|
+
payload.receiptOutcome = observation.receiptOutcome;
|
|
537
|
+
}
|
|
538
|
+
if (typeof observation.receiptSource === "string" && BOUNDED_CODE.test(observation.receiptSource)) {
|
|
539
|
+
payload.receiptSource = observation.receiptSource;
|
|
540
|
+
}
|
|
541
|
+
_internals2.emit("knowledge_receipt_transition", payload);
|
|
542
|
+
} catch {}
|
|
543
|
+
}
|
|
544
|
+
var _internals2 = { emit };
|
|
545
|
+
|
|
546
|
+
// src/hooks/knowledge-receipt-ledger.ts
|
|
547
|
+
var GENESIS_HASH = "GENESIS";
|
|
548
|
+
var MAX_JOURNAL_RECORDS = 2000;
|
|
549
|
+
var MAX_AUDIT_TAIL_RECORDS = 256;
|
|
550
|
+
var MAX_JOURNAL_BYTES = 32 * 1024 * 1024;
|
|
551
|
+
var MAX_ARCHIVE_RECORDS = 1e4;
|
|
552
|
+
var MAX_ARCHIVE_BYTES = 16 * 1024 * 1024;
|
|
553
|
+
var MAX_LEGACY_BYTES = 32 * 1024 * 1024;
|
|
554
|
+
var DEFAULT_RECEIPT_GRACE_DAYS = 7;
|
|
555
|
+
var keyOf = (traceId, entryId) => `${traceId.length}:${traceId}${entryId}`;
|
|
556
|
+
var emptyKeyOf = (traceId, sessionId) => `${traceId.length}:${traceId}${sessionId}`;
|
|
557
|
+
var phaseLifecycleKey = (phase, sessionId, taskId) => `${sessionId.length}:${sessionId}${phase.length}:${phase}${taskId === undefined ? "u:" : `s:${taskId.length}:${taskId}`}`;
|
|
558
|
+
function nowIso() {
|
|
559
|
+
return new Date(_internals3.nowMs()).toISOString();
|
|
560
|
+
}
|
|
561
|
+
function configuredGraceDays(directory) {
|
|
562
|
+
try {
|
|
563
|
+
const { config } = loadPluginConfigWithMeta(directory);
|
|
564
|
+
return KnowledgeConfigSchema.parse(config.knowledge ?? {}).receipt_close_grace_days;
|
|
565
|
+
} catch {
|
|
566
|
+
return DEFAULT_RECEIPT_GRACE_DAYS;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function emptyState(observations = []) {
|
|
570
|
+
return {
|
|
571
|
+
memberships: new Map,
|
|
572
|
+
traceIds: new Set,
|
|
573
|
+
emptyTraces: new Map,
|
|
574
|
+
cutoverCompleted: false,
|
|
575
|
+
legacyUnverifiable: false,
|
|
576
|
+
legacyTraceRegistry: new Set,
|
|
577
|
+
legacyUnverifiableTraceIds: new Set,
|
|
578
|
+
phaseLifecycle: new Map,
|
|
579
|
+
auditTail: [],
|
|
580
|
+
lastSeq: 0,
|
|
581
|
+
lastHash: GENESIS_HASH,
|
|
582
|
+
recordCount: 0,
|
|
583
|
+
observations
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function isPlainRecord(value) {
|
|
587
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
588
|
+
}
|
|
589
|
+
function hasOnlyKeys(value, allowed) {
|
|
590
|
+
const allow = new Set(allowed);
|
|
591
|
+
return Object.keys(value).every((key) => allow.has(key));
|
|
592
|
+
}
|
|
593
|
+
function isOptionalString(value) {
|
|
594
|
+
return value === undefined || typeof value === "string";
|
|
595
|
+
}
|
|
596
|
+
function isValidTimestamp(value) {
|
|
597
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
598
|
+
}
|
|
599
|
+
function parsePredicateCheck(value) {
|
|
600
|
+
if (!isPlainRecord(value))
|
|
601
|
+
return null;
|
|
602
|
+
if (!hasOnlyKeys(value, ["predicate", "result", "detail"]) || typeof value.predicate !== "string" || value.result !== "pass" && value.result !== "fail" && value.result !== "error" || typeof value.detail !== "string") {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
predicate: value.predicate,
|
|
607
|
+
result: value.result,
|
|
608
|
+
detail: value.detail
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function parseAuthorizedTransition(value) {
|
|
612
|
+
if (!isPlainRecord(value))
|
|
613
|
+
return null;
|
|
614
|
+
if (!hasOnlyKeys(value, [
|
|
615
|
+
"actor",
|
|
616
|
+
"reason",
|
|
617
|
+
"previous_event_id",
|
|
618
|
+
"previous_outcome"
|
|
619
|
+
]) || value.actor !== "manual-override" && value.actor !== "reviewer-remediation" && value.actor !== "phase-override" || typeof value.reason !== "string" || typeof value.previous_event_id !== "string" || value.previous_outcome !== undefined && !isReceiptOutcome(value.previous_outcome)) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
actor: value.actor,
|
|
624
|
+
reason: value.reason,
|
|
625
|
+
previous_event_id: value.previous_event_id,
|
|
626
|
+
previous_outcome: value.previous_outcome
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function parseTerminal(value) {
|
|
630
|
+
if (!isPlainRecord(value))
|
|
631
|
+
return null;
|
|
632
|
+
if (!hasOnlyKeys(value, [
|
|
633
|
+
"outcome",
|
|
634
|
+
"source",
|
|
635
|
+
"reason",
|
|
636
|
+
"predicate_check",
|
|
637
|
+
"event_id",
|
|
638
|
+
"committed_at",
|
|
639
|
+
"authorized_transition"
|
|
640
|
+
]) || !isReceiptOutcome(value.outcome) || typeof value.source !== "string" || !isOptionalString(value.reason) || typeof value.event_id !== "string" || !value.event_id || !isValidTimestamp(value.committed_at)) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const predicate = value.predicate_check === undefined ? undefined : parsePredicateCheck(value.predicate_check);
|
|
644
|
+
if (predicate === null)
|
|
645
|
+
return null;
|
|
646
|
+
const authorization = value.authorized_transition === undefined ? undefined : parseAuthorizedTransition(value.authorized_transition);
|
|
647
|
+
if (authorization === null)
|
|
648
|
+
return null;
|
|
649
|
+
return {
|
|
650
|
+
outcome: value.outcome,
|
|
651
|
+
source: value.source,
|
|
652
|
+
reason: value.reason,
|
|
653
|
+
predicate_check: predicate,
|
|
654
|
+
event_id: value.event_id,
|
|
655
|
+
committed_at: value.committed_at,
|
|
656
|
+
authorized_transition: authorization
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function parseApplicationMarker(value) {
|
|
660
|
+
if (!isPlainRecord(value))
|
|
661
|
+
return null;
|
|
662
|
+
if (!hasOnlyKeys(value, [
|
|
663
|
+
"outcome",
|
|
664
|
+
"source",
|
|
665
|
+
"reason",
|
|
666
|
+
"event_id",
|
|
667
|
+
"committed_at"
|
|
668
|
+
]) || !isReceiptOutcome(value.outcome) || typeof value.source !== "string" || !isOptionalString(value.reason) || typeof value.event_id !== "string" || !value.event_id || !isValidTimestamp(value.committed_at)) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
outcome: value.outcome,
|
|
673
|
+
source: value.source,
|
|
674
|
+
reason: value.reason,
|
|
675
|
+
event_id: value.event_id,
|
|
676
|
+
committed_at: value.committed_at
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function parseMembership(value) {
|
|
680
|
+
if (!isPlainRecord(value))
|
|
681
|
+
return null;
|
|
682
|
+
if (!hasOnlyKeys(value, [
|
|
683
|
+
"trace_id",
|
|
684
|
+
"entry_id",
|
|
685
|
+
"session_id",
|
|
686
|
+
"phase",
|
|
687
|
+
"task_id",
|
|
688
|
+
"agent",
|
|
689
|
+
"critical",
|
|
690
|
+
"rank",
|
|
691
|
+
"score",
|
|
692
|
+
"committed_at",
|
|
693
|
+
"membership_event_id",
|
|
694
|
+
"grace_days",
|
|
695
|
+
"phase_close_intent_at",
|
|
696
|
+
"phase_closed_at",
|
|
697
|
+
"terminal",
|
|
698
|
+
"terminal_history",
|
|
699
|
+
"application_marker",
|
|
700
|
+
"cohort_id",
|
|
701
|
+
"source_link_id",
|
|
702
|
+
"exposure_kind",
|
|
703
|
+
"origin"
|
|
704
|
+
]) || typeof value.trace_id !== "string" || !value.trace_id || typeof value.entry_id !== "string" || !value.entry_id || typeof value.session_id !== "string" || !value.session_id || !isOptionalString(value.phase) || !isOptionalString(value.task_id) || !isOptionalString(value.agent) || typeof value.critical !== "boolean" || value.rank !== undefined && !Number.isFinite(value.rank) || value.score !== undefined && !Number.isFinite(value.score) || !isValidTimestamp(value.committed_at) || typeof value.membership_event_id !== "string" || !value.membership_event_id || !Number.isFinite(value.grace_days) || value.grace_days < 0 || value.phase_close_intent_at !== undefined && !isValidTimestamp(value.phase_close_intent_at) || value.phase_closed_at !== undefined && !isValidTimestamp(value.phase_closed_at) || !isOptionalString(value.cohort_id) || !isOptionalString(value.source_link_id) || !isReceiptExposureKind(value.exposure_kind) || value.origin !== "v2" && value.origin !== "legacy") {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const terminal = value.terminal === undefined ? undefined : parseTerminal(value.terminal);
|
|
708
|
+
if (terminal === null)
|
|
709
|
+
return null;
|
|
710
|
+
let terminalHistory;
|
|
711
|
+
if (value.terminal_history !== undefined) {
|
|
712
|
+
if (!Array.isArray(value.terminal_history))
|
|
713
|
+
return null;
|
|
714
|
+
terminalHistory = [];
|
|
715
|
+
for (const item of value.terminal_history) {
|
|
716
|
+
const parsed = parseTerminal(item);
|
|
717
|
+
if (!parsed)
|
|
718
|
+
return null;
|
|
719
|
+
terminalHistory.push(parsed);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const marker = value.application_marker === undefined ? undefined : parseApplicationMarker(value.application_marker);
|
|
723
|
+
if (marker === null)
|
|
724
|
+
return null;
|
|
725
|
+
return {
|
|
726
|
+
trace_id: value.trace_id,
|
|
727
|
+
entry_id: value.entry_id,
|
|
728
|
+
session_id: value.session_id,
|
|
729
|
+
phase: value.phase,
|
|
730
|
+
task_id: value.task_id,
|
|
731
|
+
agent: value.agent,
|
|
732
|
+
critical: value.critical,
|
|
733
|
+
rank: value.rank,
|
|
734
|
+
score: value.score,
|
|
735
|
+
committed_at: value.committed_at,
|
|
736
|
+
membership_event_id: value.membership_event_id,
|
|
737
|
+
grace_days: value.grace_days,
|
|
738
|
+
phase_close_intent_at: value.phase_close_intent_at,
|
|
739
|
+
phase_closed_at: value.phase_closed_at,
|
|
740
|
+
terminal,
|
|
741
|
+
terminal_history: terminalHistory,
|
|
742
|
+
application_marker: marker,
|
|
743
|
+
cohort_id: value.cohort_id,
|
|
744
|
+
source_link_id: value.source_link_id,
|
|
745
|
+
exposure_kind: value.exposure_kind,
|
|
746
|
+
origin: value.origin
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function isReceiptExposureKind(value) {
|
|
750
|
+
return value === "architect_directive" || value === "delegate_directive" || value === "manual_recall" || value === "legacy_unknown";
|
|
751
|
+
}
|
|
752
|
+
function legacyExposureKind(event) {
|
|
753
|
+
if (event.retrieval_mode === "auto_injection")
|
|
754
|
+
return "architect_directive";
|
|
755
|
+
if (event.retrieval_mode === "delegate_inject")
|
|
756
|
+
return "delegate_directive";
|
|
757
|
+
if (event.retrieval_mode === "manual")
|
|
758
|
+
return "manual_recall";
|
|
759
|
+
return "legacy_unknown";
|
|
760
|
+
}
|
|
761
|
+
function parseEmptyTrace(value, requireSummaryKind = false) {
|
|
762
|
+
if (!isPlainRecord(value))
|
|
763
|
+
return null;
|
|
764
|
+
if (!hasOnlyKeys(value, [
|
|
765
|
+
"trace_id",
|
|
766
|
+
"session_id",
|
|
767
|
+
"phase",
|
|
768
|
+
"task_id",
|
|
769
|
+
"committed_at",
|
|
770
|
+
"empty_event_id",
|
|
771
|
+
"grace_days",
|
|
772
|
+
"phase_close_intent_at",
|
|
773
|
+
"phase_closed_at",
|
|
774
|
+
"terminal_event_id",
|
|
775
|
+
"summary_kind"
|
|
776
|
+
]) || requireSummaryKind && value.summary_kind !== "empty_trace" || !requireSummaryKind && value.summary_kind !== undefined || typeof value.trace_id !== "string" || !value.trace_id || typeof value.session_id !== "string" || !value.session_id || !isOptionalString(value.phase) || !isOptionalString(value.task_id) || !isValidTimestamp(value.committed_at) || typeof value.empty_event_id !== "string" || !value.empty_event_id || !Number.isFinite(value.grace_days) || value.grace_days < 0 || value.phase_close_intent_at !== undefined && !isValidTimestamp(value.phase_close_intent_at) || value.phase_closed_at !== undefined && !isValidTimestamp(value.phase_closed_at) || !isOptionalString(value.terminal_event_id)) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const trace = {
|
|
780
|
+
trace_id: value.trace_id,
|
|
781
|
+
session_id: value.session_id,
|
|
782
|
+
phase: value.phase,
|
|
783
|
+
task_id: value.task_id,
|
|
784
|
+
committed_at: value.committed_at,
|
|
785
|
+
empty_event_id: value.empty_event_id,
|
|
786
|
+
grace_days: value.grace_days,
|
|
787
|
+
phase_close_intent_at: value.phase_close_intent_at,
|
|
788
|
+
phase_closed_at: value.phase_closed_at,
|
|
789
|
+
terminal_event_id: value.terminal_event_id
|
|
790
|
+
};
|
|
791
|
+
return requireSummaryKind ? { ...trace, summary_kind: "empty_trace" } : trace;
|
|
792
|
+
}
|
|
793
|
+
function parsePhaseLifecycle(value) {
|
|
794
|
+
if (!isPlainRecord(value))
|
|
795
|
+
return null;
|
|
796
|
+
if (!hasOnlyKeys(value, [
|
|
797
|
+
"phase",
|
|
798
|
+
"session_id",
|
|
799
|
+
"task_id",
|
|
800
|
+
"intent_event_id",
|
|
801
|
+
"intent_at",
|
|
802
|
+
"closed_event_id",
|
|
803
|
+
"closed_at"
|
|
804
|
+
]) || typeof value.phase !== "string" || !value.phase || typeof value.session_id !== "string" || !value.session_id || !isOptionalString(value.task_id) || !isOptionalString(value.intent_event_id) || value.intent_at !== undefined && !isValidTimestamp(value.intent_at) || !isOptionalString(value.closed_event_id) || value.closed_at !== undefined && !isValidTimestamp(value.closed_at)) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
phase: value.phase,
|
|
809
|
+
session_id: value.session_id,
|
|
810
|
+
task_id: value.task_id,
|
|
811
|
+
intent_event_id: value.intent_event_id,
|
|
812
|
+
intent_at: value.intent_at,
|
|
813
|
+
closed_event_id: value.closed_event_id,
|
|
814
|
+
closed_at: value.closed_at
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function parseAuditSummary(value) {
|
|
818
|
+
if (!isPlainRecord(value))
|
|
819
|
+
return null;
|
|
820
|
+
if (!hasOnlyKeys(value, [
|
|
821
|
+
"original_seq",
|
|
822
|
+
"original_hash",
|
|
823
|
+
"event_id",
|
|
824
|
+
"timestamp",
|
|
825
|
+
"kind",
|
|
826
|
+
"trace_id",
|
|
827
|
+
"phase",
|
|
828
|
+
"entry_ids",
|
|
829
|
+
"reason_codes"
|
|
830
|
+
]) || !Number.isInteger(value.original_seq) || value.original_seq <= 0 || typeof value.original_hash !== "string" || !value.original_hash || typeof value.event_id !== "string" || !value.event_id || !isValidTimestamp(value.timestamp) || typeof value.kind !== "string" || !isOptionalString(value.trace_id) || !isOptionalString(value.phase) || value.entry_ids !== undefined && (!Array.isArray(value.entry_ids) || !value.entry_ids.every((entryId) => typeof entryId === "string" && !!entryId)) || value.reason_codes !== undefined && (!Array.isArray(value.reason_codes) || !value.reason_codes.every((reason) => typeof reason === "string" && !!reason))) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
const kind = value.kind;
|
|
834
|
+
if (![
|
|
835
|
+
"membership_committed",
|
|
836
|
+
"empty_retrieval_committed",
|
|
837
|
+
"terminal_committed",
|
|
838
|
+
"terminal_attempt_rejected",
|
|
839
|
+
"terminal_attempt_idempotent",
|
|
840
|
+
"authorized_transition_committed",
|
|
841
|
+
"application_marker_committed",
|
|
842
|
+
"phase_close_intent",
|
|
843
|
+
"phase_closed",
|
|
844
|
+
"legacy_imported",
|
|
845
|
+
"legacy_unverifiable",
|
|
846
|
+
"cutover_completed",
|
|
847
|
+
"checkpoint"
|
|
848
|
+
].includes(kind))
|
|
849
|
+
return null;
|
|
850
|
+
return {
|
|
851
|
+
original_seq: value.original_seq,
|
|
852
|
+
original_hash: value.original_hash,
|
|
853
|
+
event_id: value.event_id,
|
|
854
|
+
timestamp: value.timestamp,
|
|
855
|
+
kind,
|
|
856
|
+
trace_id: value.trace_id,
|
|
857
|
+
phase: value.phase,
|
|
858
|
+
entry_ids: value.entry_ids,
|
|
859
|
+
reason_codes: value.reason_codes
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function summarizeRecord(record) {
|
|
863
|
+
const payload = record.payload;
|
|
864
|
+
const entryIds = new Set;
|
|
865
|
+
for (const collection of [
|
|
866
|
+
payload.memberships,
|
|
867
|
+
payload.transitions,
|
|
868
|
+
payload.markers,
|
|
869
|
+
payload.rejected
|
|
870
|
+
]) {
|
|
871
|
+
if (!Array.isArray(collection))
|
|
872
|
+
continue;
|
|
873
|
+
for (const item of collection) {
|
|
874
|
+
if (isPlainRecord(item) && typeof item.entry_id === "string") {
|
|
875
|
+
entryIds.add(item.entry_id);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const entryId of Array.isArray(payload.entry_ids) ? payload.entry_ids : []) {
|
|
880
|
+
if (typeof entryId === "string")
|
|
881
|
+
entryIds.add(entryId);
|
|
882
|
+
}
|
|
883
|
+
const reasonCodes = new Set;
|
|
884
|
+
for (const reason of Array.isArray(payload.reason_codes) ? payload.reason_codes : []) {
|
|
885
|
+
if (typeof reason === "string")
|
|
886
|
+
reasonCodes.add(reason);
|
|
887
|
+
}
|
|
888
|
+
for (const rejected of Array.isArray(payload.rejected) ? payload.rejected : []) {
|
|
889
|
+
if (isPlainRecord(rejected) && typeof rejected.reason === "string") {
|
|
890
|
+
reasonCodes.add(rejected.reason);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
original_seq: record.seq,
|
|
895
|
+
original_hash: record.hash,
|
|
896
|
+
event_id: record.event_id,
|
|
897
|
+
timestamp: record.timestamp,
|
|
898
|
+
kind: record.kind,
|
|
899
|
+
trace_id: typeof payload.trace_id === "string" ? payload.trace_id : typeof payload.empty_trace_id === "string" ? payload.empty_trace_id : undefined,
|
|
900
|
+
phase: typeof payload.phase === "string" ? payload.phase : undefined,
|
|
901
|
+
entry_ids: entryIds.size ? [...entryIds].sort() : undefined,
|
|
902
|
+
reason_codes: reasonCodes.size ? [...reasonCodes].sort() : undefined
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function parseArchiveSummary(value) {
|
|
906
|
+
const membership = parseMembership(value);
|
|
907
|
+
if (membership)
|
|
908
|
+
return membership;
|
|
909
|
+
return parseEmptyTrace(value, true);
|
|
910
|
+
}
|
|
911
|
+
function archiveKey(summary) {
|
|
912
|
+
return "entry_id" in summary ? `membership:${keyOf(summary.trace_id, summary.entry_id)}` : `empty:${emptyKeyOf(summary.trace_id, summary.session_id)}`;
|
|
913
|
+
}
|
|
914
|
+
function applyRecord(state, record) {
|
|
915
|
+
const memberships = record.payload.memberships;
|
|
916
|
+
if (record.kind === "checkpoint") {
|
|
917
|
+
state.memberships.clear();
|
|
918
|
+
state.traceIds.clear();
|
|
919
|
+
state.emptyTraces.clear();
|
|
920
|
+
state.legacyTraceRegistry.clear();
|
|
921
|
+
state.legacyUnverifiableTraceIds.clear();
|
|
922
|
+
state.phaseLifecycle.clear();
|
|
923
|
+
state.auditTail = [];
|
|
924
|
+
if (Array.isArray(memberships)) {
|
|
925
|
+
for (const value of memberships) {
|
|
926
|
+
const membership = parseMembership(value);
|
|
927
|
+
if (membership) {
|
|
928
|
+
state.traceIds.add(membership.trace_id);
|
|
929
|
+
state.memberships.set(keyOf(membership.trace_id, membership.entry_id), membership);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
for (const value of Array.isArray(record.payload.empty_traces) ? record.payload.empty_traces : []) {
|
|
934
|
+
const trace = parseEmptyTrace(value);
|
|
935
|
+
if (trace && !("summary_kind" in trace)) {
|
|
936
|
+
state.emptyTraces.set(emptyKeyOf(trace.trace_id, trace.session_id), {
|
|
937
|
+
...trace
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
for (const traceId of Array.isArray(record.payload.legacy_trace_registry) ? record.payload.legacy_trace_registry : []) {
|
|
942
|
+
if (typeof traceId === "string" && traceId) {
|
|
943
|
+
state.legacyTraceRegistry.add(traceId);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const traceId of Array.isArray(record.payload.legacy_unverifiable_trace_ids) ? record.payload.legacy_unverifiable_trace_ids : []) {
|
|
947
|
+
if (typeof traceId === "string" && traceId) {
|
|
948
|
+
state.legacyUnverifiableTraceIds.add(traceId);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
for (const value of Array.isArray(record.payload.phase_lifecycle) ? record.payload.phase_lifecycle : []) {
|
|
952
|
+
const lifecycle = parsePhaseLifecycle(value);
|
|
953
|
+
if (lifecycle)
|
|
954
|
+
state.phaseLifecycle.set(phaseLifecycleKey(lifecycle.phase, lifecycle.session_id, lifecycle.task_id), lifecycle);
|
|
955
|
+
}
|
|
956
|
+
for (const value of Array.isArray(record.payload.audit_tail) ? record.payload.audit_tail : []) {
|
|
957
|
+
const summary = parseAuditSummary(value);
|
|
958
|
+
if (summary)
|
|
959
|
+
state.auditTail.push(summary);
|
|
960
|
+
}
|
|
961
|
+
state.cutoverCompleted = record.payload.cutover_completed === true;
|
|
962
|
+
state.legacyUnverifiable = record.payload.legacy_unverifiable === true;
|
|
963
|
+
} else if (record.kind === "membership_committed" || record.kind === "legacy_imported") {
|
|
964
|
+
if (Array.isArray(memberships)) {
|
|
965
|
+
for (const value of memberships) {
|
|
966
|
+
const membership = parseMembership(value);
|
|
967
|
+
if (membership) {
|
|
968
|
+
state.traceIds.add(membership.trace_id);
|
|
969
|
+
state.memberships.set(keyOf(membership.trace_id, membership.entry_id), membership);
|
|
970
|
+
if (record.kind === "legacy_imported") {
|
|
971
|
+
state.legacyTraceRegistry.add(membership.trace_id);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} else if (record.kind === "empty_retrieval_committed") {
|
|
977
|
+
const trace = parseEmptyTrace(record.payload.trace);
|
|
978
|
+
if (trace && !("summary_kind" in trace)) {
|
|
979
|
+
state.emptyTraces.set(emptyKeyOf(trace.trace_id, trace.session_id), {
|
|
980
|
+
...trace
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
} else if (record.kind === "terminal_committed" || record.kind === "authorized_transition_committed") {
|
|
984
|
+
for (const value of Array.isArray(record.payload.transitions) ? record.payload.transitions : []) {
|
|
985
|
+
const transition = value;
|
|
986
|
+
const membership = state.memberships.get(keyOf(transition.trace_id, transition.entry_id));
|
|
987
|
+
const terminal = parseTerminal(transition.terminal);
|
|
988
|
+
if (membership && terminal) {
|
|
989
|
+
const history = membership.terminal_history ?? [];
|
|
990
|
+
if (!history.some((item) => item.event_id === terminal.event_id)) {
|
|
991
|
+
history.push({ ...terminal });
|
|
992
|
+
}
|
|
993
|
+
membership.terminal_history = history;
|
|
994
|
+
membership.terminal = { ...terminal };
|
|
995
|
+
if (transition.cohort_id !== undefined) {
|
|
996
|
+
membership.cohort_id = transition.cohort_id;
|
|
997
|
+
}
|
|
998
|
+
if (transition.source_link_id !== undefined) {
|
|
999
|
+
membership.source_link_id = transition.source_link_id;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const emptyTraceId = record.payload.empty_trace_id;
|
|
1004
|
+
if (typeof emptyTraceId === "string") {
|
|
1005
|
+
const emptySessionId = record.payload.empty_trace_session_id;
|
|
1006
|
+
const trace = typeof emptySessionId === "string" ? state.emptyTraces.get(emptyKeyOf(emptyTraceId, emptySessionId)) : [...state.emptyTraces.values()].find((candidate) => candidate.trace_id === emptyTraceId);
|
|
1007
|
+
if (trace)
|
|
1008
|
+
trace.terminal_event_id = record.event_id;
|
|
1009
|
+
}
|
|
1010
|
+
} else if (record.kind === "application_marker_committed") {
|
|
1011
|
+
for (const value of Array.isArray(record.payload.transitions) ? record.payload.transitions : []) {
|
|
1012
|
+
const transition = value;
|
|
1013
|
+
const membership = state.memberships.get(keyOf(transition.trace_id, transition.entry_id));
|
|
1014
|
+
const terminal = parseTerminal(transition.terminal);
|
|
1015
|
+
if (membership && terminal) {
|
|
1016
|
+
const history = membership.terminal_history ?? [];
|
|
1017
|
+
if (!history.some((item) => item.event_id === terminal.event_id)) {
|
|
1018
|
+
history.push({ ...terminal });
|
|
1019
|
+
}
|
|
1020
|
+
membership.terminal_history = history;
|
|
1021
|
+
membership.terminal = { ...terminal };
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
for (const value of Array.isArray(record.payload.markers) ? record.payload.markers : []) {
|
|
1025
|
+
const transition = value;
|
|
1026
|
+
const membership = state.memberships.get(keyOf(transition.trace_id, transition.entry_id));
|
|
1027
|
+
const marker = parseApplicationMarker(transition.marker);
|
|
1028
|
+
if (membership && marker) {
|
|
1029
|
+
membership.application_marker = { ...marker };
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} else if (record.kind === "phase_close_intent" || record.kind === "phase_closed") {
|
|
1033
|
+
const phase = record.payload.phase;
|
|
1034
|
+
const sessionId = record.payload.session_id;
|
|
1035
|
+
const taskId = record.payload.task_id;
|
|
1036
|
+
if (typeof phase === "string" && typeof sessionId === "string" && isOptionalString(taskId)) {
|
|
1037
|
+
const lifecycleKey = phaseLifecycleKey(phase, sessionId, taskId);
|
|
1038
|
+
const lifecycle = state.phaseLifecycle.get(lifecycleKey) ?? {
|
|
1039
|
+
phase,
|
|
1040
|
+
session_id: sessionId,
|
|
1041
|
+
task_id: taskId
|
|
1042
|
+
};
|
|
1043
|
+
if (record.kind === "phase_close_intent" && !lifecycle.intent_event_id) {
|
|
1044
|
+
lifecycle.intent_event_id = record.event_id;
|
|
1045
|
+
lifecycle.intent_at = record.timestamp;
|
|
1046
|
+
} else if (record.kind === "phase_closed" && !lifecycle.closed_event_id) {
|
|
1047
|
+
lifecycle.closed_event_id = record.event_id;
|
|
1048
|
+
lifecycle.closed_at = record.timestamp;
|
|
1049
|
+
}
|
|
1050
|
+
state.phaseLifecycle.set(lifecycleKey, lifecycle);
|
|
1051
|
+
for (const membership of state.memberships.values()) {
|
|
1052
|
+
if (membership.phase !== phase || membership.session_id !== sessionId || membership.task_id !== taskId)
|
|
1053
|
+
continue;
|
|
1054
|
+
if (record.kind === "phase_close_intent" && !membership.phase_close_intent_at)
|
|
1055
|
+
membership.phase_close_intent_at = record.timestamp;
|
|
1056
|
+
else if (record.kind === "phase_closed" && !membership.phase_closed_at)
|
|
1057
|
+
membership.phase_closed_at = record.timestamp;
|
|
1058
|
+
}
|
|
1059
|
+
for (const trace of state.emptyTraces.values()) {
|
|
1060
|
+
if (trace.phase !== phase || trace.session_id !== sessionId || trace.task_id !== taskId)
|
|
1061
|
+
continue;
|
|
1062
|
+
if (record.kind === "phase_close_intent" && !trace.phase_close_intent_at) {
|
|
1063
|
+
trace.phase_close_intent_at = record.timestamp;
|
|
1064
|
+
} else if (record.kind === "phase_closed" && !trace.phase_closed_at) {
|
|
1065
|
+
trace.phase_closed_at = record.timestamp;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
} else if (record.kind === "cutover_completed") {
|
|
1070
|
+
state.cutoverCompleted = true;
|
|
1071
|
+
for (const traceId of Array.isArray(record.payload.imported_trace_ids) ? record.payload.imported_trace_ids : []) {
|
|
1072
|
+
if (typeof traceId === "string" && traceId)
|
|
1073
|
+
state.legacyTraceRegistry.add(traceId);
|
|
1074
|
+
}
|
|
1075
|
+
} else if (record.kind === "legacy_unverifiable") {
|
|
1076
|
+
state.legacyUnverifiable = true;
|
|
1077
|
+
for (const traceId of Array.isArray(record.payload.trace_ids) ? record.payload.trace_ids : []) {
|
|
1078
|
+
if (typeof traceId === "string" && traceId) {
|
|
1079
|
+
state.legacyUnverifiableTraceIds.add(traceId);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (record.kind !== "checkpoint") {
|
|
1084
|
+
state.auditTail.push(summarizeRecord(record));
|
|
1085
|
+
if (state.auditTail.length > MAX_AUDIT_TAIL_RECORDS) {
|
|
1086
|
+
state.auditTail.splice(0, state.auditTail.length - MAX_AUDIT_TAIL_RECORDS);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
state.lastSeq = record.seq;
|
|
1090
|
+
state.lastHash = record.hash;
|
|
1091
|
+
state.recordCount++;
|
|
1092
|
+
}
|
|
1093
|
+
function isTerminalTransition(value) {
|
|
1094
|
+
if (!isPlainRecord(value))
|
|
1095
|
+
return false;
|
|
1096
|
+
return hasOnlyKeys(value, [
|
|
1097
|
+
"trace_id",
|
|
1098
|
+
"entry_id",
|
|
1099
|
+
"terminal",
|
|
1100
|
+
"cohort_id",
|
|
1101
|
+
"source_link_id"
|
|
1102
|
+
]) && typeof value.trace_id === "string" && !!value.trace_id && typeof value.entry_id === "string" && !!value.entry_id && parseTerminal(value.terminal) !== null && isOptionalString(value.cohort_id) && isOptionalString(value.source_link_id);
|
|
1103
|
+
}
|
|
1104
|
+
function isMarkerTransition(value) {
|
|
1105
|
+
if (!isPlainRecord(value))
|
|
1106
|
+
return false;
|
|
1107
|
+
return hasOnlyKeys(value, ["trace_id", "entry_id", "marker"]) && typeof value.trace_id === "string" && !!value.trace_id && typeof value.entry_id === "string" && !!value.entry_id && parseApplicationMarker(value.marker) !== null;
|
|
1108
|
+
}
|
|
1109
|
+
function isAuthorizationPayload(value) {
|
|
1110
|
+
if (!isPlainRecord(value))
|
|
1111
|
+
return false;
|
|
1112
|
+
return hasOnlyKeys(value, [
|
|
1113
|
+
"actor",
|
|
1114
|
+
"reason",
|
|
1115
|
+
"expected_event_id",
|
|
1116
|
+
"expected_outcome"
|
|
1117
|
+
]) && (value.actor === "manual-override" || value.actor === "reviewer-remediation" || value.actor === "phase-override") && typeof value.reason === "string" && typeof value.expected_event_id === "string" && (value.expected_outcome === undefined || isReceiptOutcome(value.expected_outcome));
|
|
1118
|
+
}
|
|
1119
|
+
function validateRecordPayload(row) {
|
|
1120
|
+
const payload = row.payload;
|
|
1121
|
+
if (!isPlainRecord(payload))
|
|
1122
|
+
return false;
|
|
1123
|
+
switch (row.kind) {
|
|
1124
|
+
case "membership_committed":
|
|
1125
|
+
case "legacy_imported":
|
|
1126
|
+
return hasOnlyKeys(payload, ["memberships"]) && Array.isArray(payload.memberships) && payload.memberships.length > 0 && payload.memberships.every((item) => parseMembership(item) !== null);
|
|
1127
|
+
case "empty_retrieval_committed":
|
|
1128
|
+
return hasOnlyKeys(payload, ["trace"]) && parseEmptyTrace(payload.trace) !== null;
|
|
1129
|
+
case "terminal_committed":
|
|
1130
|
+
case "authorized_transition_committed": {
|
|
1131
|
+
if (!hasOnlyKeys(payload, [
|
|
1132
|
+
"transitions",
|
|
1133
|
+
"authorization",
|
|
1134
|
+
"empty_trace_id",
|
|
1135
|
+
"empty_trace_session_id"
|
|
1136
|
+
]))
|
|
1137
|
+
return false;
|
|
1138
|
+
const transitionsValid = payload.transitions === undefined || Array.isArray(payload.transitions) && payload.transitions.every(isTerminalTransition);
|
|
1139
|
+
const emptyValid = payload.empty_trace_id === undefined || typeof payload.empty_trace_id === "string" && !!payload.empty_trace_id && typeof payload.empty_trace_session_id === "string" && !!payload.empty_trace_session_id;
|
|
1140
|
+
const authorizationValid = payload.authorization === undefined || isAuthorizationPayload(payload.authorization);
|
|
1141
|
+
return transitionsValid && emptyValid && authorizationValid && (payload.transitions !== undefined || payload.empty_trace_id !== undefined);
|
|
1142
|
+
}
|
|
1143
|
+
case "application_marker_committed":
|
|
1144
|
+
return hasOnlyKeys(payload, ["markers", "transitions"]) && (payload.markers === undefined || Array.isArray(payload.markers) && payload.markers.every(isMarkerTransition)) && (payload.transitions === undefined || Array.isArray(payload.transitions) && payload.transitions.every(isTerminalTransition)) && (payload.markers !== undefined || payload.transitions !== undefined);
|
|
1145
|
+
case "terminal_attempt_idempotent":
|
|
1146
|
+
return hasOnlyKeys(payload, ["trace_id", "entry_ids"]) && typeof payload.trace_id === "string" && Array.isArray(payload.entry_ids) && payload.entry_ids.every((entryId) => typeof entryId === "string" && !!entryId);
|
|
1147
|
+
case "terminal_attempt_rejected":
|
|
1148
|
+
return hasOnlyKeys(payload, ["trace_id", "rejected"]) && typeof payload.trace_id === "string" && Array.isArray(payload.rejected) && payload.rejected.every((item) => isPlainRecord(item) && hasOnlyKeys(item, ["entry_id", "reason"]) && typeof item.entry_id === "string" && typeof item.reason === "string");
|
|
1149
|
+
case "phase_close_intent":
|
|
1150
|
+
case "phase_closed":
|
|
1151
|
+
return hasOnlyKeys(payload, ["phase", "session_id", "task_id"]) && typeof payload.phase === "string" && !!payload.phase && typeof payload.session_id === "string" && !!payload.session_id && isOptionalString(payload.task_id);
|
|
1152
|
+
case "legacy_unverifiable":
|
|
1153
|
+
return hasOnlyKeys(payload, ["reason_codes", "trace_ids"]) && Array.isArray(payload.reason_codes) && payload.reason_codes.every((reason) => typeof reason === "string" && !!reason) && (payload.trace_ids === undefined || Array.isArray(payload.trace_ids) && payload.trace_ids.every((traceId) => typeof traceId === "string" && !!traceId));
|
|
1154
|
+
case "cutover_completed":
|
|
1155
|
+
return hasOnlyKeys(payload, ["imported_count", "imported_trace_ids"]) && Number.isInteger(payload.imported_count) && payload.imported_count >= 0 && Array.isArray(payload.imported_trace_ids) && payload.imported_trace_ids.every((traceId) => typeof traceId === "string" && !!traceId);
|
|
1156
|
+
case "checkpoint":
|
|
1157
|
+
return hasOnlyKeys(payload, [
|
|
1158
|
+
"memberships",
|
|
1159
|
+
"empty_traces",
|
|
1160
|
+
"cutover_completed",
|
|
1161
|
+
"legacy_unverifiable",
|
|
1162
|
+
"legacy_trace_registry",
|
|
1163
|
+
"legacy_unverifiable_trace_ids",
|
|
1164
|
+
"phase_lifecycle",
|
|
1165
|
+
"audit_tail"
|
|
1166
|
+
]) && Array.isArray(payload.memberships) && payload.memberships.every((item) => parseMembership(item) !== null) && Array.isArray(payload.empty_traces) && payload.empty_traces.every((item) => parseEmptyTrace(item) !== null) && typeof payload.cutover_completed === "boolean" && typeof payload.legacy_unverifiable === "boolean" && Array.isArray(payload.legacy_trace_registry) && payload.legacy_trace_registry.every((traceId) => typeof traceId === "string" && !!traceId) && Array.isArray(payload.legacy_unverifiable_trace_ids) && payload.legacy_unverifiable_trace_ids.every((traceId) => typeof traceId === "string" && !!traceId) && Array.isArray(payload.phase_lifecycle) && payload.phase_lifecycle.every((item) => parsePhaseLifecycle(item) !== null) && Array.isArray(payload.audit_tail) && payload.audit_tail.length <= MAX_AUDIT_TAIL_RECORDS && payload.audit_tail.every((item) => parseAuditSummary(item) !== null);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function validateRecord(value, state) {
|
|
1170
|
+
if (!value || typeof value !== "object")
|
|
1171
|
+
throw new Error("record is not an object");
|
|
1172
|
+
const row = value;
|
|
1173
|
+
if (row.schema_version !== RECEIPT_SCHEMA_VERSION || row.cutover_version !== RECEIPT_CUTOVER_VERSION || row.seq !== state.lastSeq + 1 || row.prev_hash !== state.lastHash || !Number.isInteger(row.seq) || typeof row.event_id !== "string" || !row.event_id || !isValidTimestamp(row.timestamp) || ![
|
|
1174
|
+
"membership_committed",
|
|
1175
|
+
"empty_retrieval_committed",
|
|
1176
|
+
"terminal_committed",
|
|
1177
|
+
"terminal_attempt_rejected",
|
|
1178
|
+
"terminal_attempt_idempotent",
|
|
1179
|
+
"authorized_transition_committed",
|
|
1180
|
+
"application_marker_committed",
|
|
1181
|
+
"phase_close_intent",
|
|
1182
|
+
"phase_closed",
|
|
1183
|
+
"legacy_imported",
|
|
1184
|
+
"legacy_unverifiable",
|
|
1185
|
+
"cutover_completed",
|
|
1186
|
+
"checkpoint"
|
|
1187
|
+
].includes(row.kind) || typeof row.hash !== "string")
|
|
1188
|
+
throw new Error("record contract or chain mismatch");
|
|
1189
|
+
const { hash, ...withoutHash } = row;
|
|
1190
|
+
if (receiptRecordHash(withoutHash) !== hash)
|
|
1191
|
+
throw new Error("record hash mismatch");
|
|
1192
|
+
if (!validateRecordPayload(row))
|
|
1193
|
+
throw new Error("record payload mismatch");
|
|
1194
|
+
return row;
|
|
1195
|
+
}
|
|
1196
|
+
async function loadState(paths, observations = []) {
|
|
1197
|
+
const raw = await readUtf8IfPresent(paths.journal, MAX_JOURNAL_BYTES);
|
|
1198
|
+
const state = emptyState(observations);
|
|
1199
|
+
if (!raw)
|
|
1200
|
+
return state;
|
|
1201
|
+
const lines = raw.split(`
|
|
1202
|
+
`);
|
|
1203
|
+
const endedWithNewline = raw.endsWith(`
|
|
1204
|
+
`);
|
|
1205
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1206
|
+
const line = lines[i];
|
|
1207
|
+
if (!line)
|
|
1208
|
+
continue;
|
|
1209
|
+
try {
|
|
1210
|
+
applyRecord(state, validateRecord(JSON.parse(line), state));
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
const isPartialTail = i === lines.length - 1 && !endedWithNewline;
|
|
1213
|
+
if (isPartialTail) {
|
|
1214
|
+
await atomicWriteFsynced(paths.quarantine, `${JSON.stringify({
|
|
1215
|
+
recovered_at: nowIso(),
|
|
1216
|
+
partial_tail: line.slice(0, 1024 * 1024)
|
|
1217
|
+
})}
|
|
1218
|
+
`);
|
|
1219
|
+
await atomicWriteFsynced(paths.journal, `${lines.slice(0, i).filter(Boolean).join(`
|
|
1220
|
+
`)}
|
|
1221
|
+
`);
|
|
1222
|
+
return state;
|
|
1223
|
+
}
|
|
1224
|
+
throw new ReceiptStoreError("store_corrupt", `receipt journal corruption at line ${i + 1}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return state;
|
|
1228
|
+
}
|
|
1229
|
+
function makeRecord(state, kind, payload, eventId = randomUUID2()) {
|
|
1230
|
+
const withoutHash = {
|
|
1231
|
+
schema_version: RECEIPT_SCHEMA_VERSION,
|
|
1232
|
+
cutover_version: RECEIPT_CUTOVER_VERSION,
|
|
1233
|
+
seq: state.lastSeq + 1,
|
|
1234
|
+
prev_hash: state.lastHash,
|
|
1235
|
+
event_id: eventId,
|
|
1236
|
+
timestamp: nowIso(),
|
|
1237
|
+
kind,
|
|
1238
|
+
payload
|
|
1239
|
+
};
|
|
1240
|
+
return { ...withoutHash, hash: receiptRecordHash(withoutHash) };
|
|
1241
|
+
}
|
|
1242
|
+
async function appendRecord(paths, state, row) {
|
|
1243
|
+
await appendFsynced(paths.journal, `${JSON.stringify(row)}
|
|
1244
|
+
`);
|
|
1245
|
+
applyRecord(state, row);
|
|
1246
|
+
queueRecordObservations(state, row);
|
|
1247
|
+
}
|
|
1248
|
+
function rejectionReasonCode(value) {
|
|
1249
|
+
switch (value) {
|
|
1250
|
+
case "trace_not_found":
|
|
1251
|
+
case "id_not_in_trace":
|
|
1252
|
+
case "wrong_session":
|
|
1253
|
+
case "wrong_phase":
|
|
1254
|
+
case "wrong_task":
|
|
1255
|
+
case "duplicate_conflicting_terminal":
|
|
1256
|
+
case "event_id_conflict":
|
|
1257
|
+
case "invalid_outcome":
|
|
1258
|
+
case "empty_receipt":
|
|
1259
|
+
case "unauthorized_transition":
|
|
1260
|
+
case "legacy_unverifiable":
|
|
1261
|
+
return value;
|
|
1262
|
+
default:
|
|
1263
|
+
return "store_unavailable";
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function queueRecordObservations(state, row) {
|
|
1267
|
+
const base = {
|
|
1268
|
+
transition: row.kind,
|
|
1269
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION
|
|
1270
|
+
};
|
|
1271
|
+
if (row.kind === "membership_committed" || row.kind === "legacy_imported") {
|
|
1272
|
+
for (const value of Array.isArray(row.payload.memberships) ? row.payload.memberships : []) {
|
|
1273
|
+
const membership = parseMembership(value);
|
|
1274
|
+
if (!membership)
|
|
1275
|
+
continue;
|
|
1276
|
+
state.observations.push({
|
|
1277
|
+
...base,
|
|
1278
|
+
reasonCode: "committed",
|
|
1279
|
+
knowledgeTraceId: membership.trace_id,
|
|
1280
|
+
knowledgeEntryId: membership.entry_id,
|
|
1281
|
+
sessionId: membership.session_id,
|
|
1282
|
+
taskId: membership.task_id,
|
|
1283
|
+
phase: membership.phase,
|
|
1284
|
+
receiptOutcome: membership.terminal?.outcome,
|
|
1285
|
+
receiptSource: membership.terminal?.source
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (row.kind === "empty_retrieval_committed") {
|
|
1291
|
+
const trace = row.payload.trace;
|
|
1292
|
+
if (trace?.trace_id) {
|
|
1293
|
+
state.observations.push({
|
|
1294
|
+
...base,
|
|
1295
|
+
reasonCode: "committed",
|
|
1296
|
+
knowledgeTraceId: trace.trace_id,
|
|
1297
|
+
sessionId: trace.session_id,
|
|
1298
|
+
taskId: trace.task_id,
|
|
1299
|
+
phase: trace.phase,
|
|
1300
|
+
receiptOutcome: trace.terminal_event_id ? "no_relevant" : undefined
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (row.kind === "terminal_committed" || row.kind === "authorized_transition_committed" || row.kind === "application_marker_committed") {
|
|
1306
|
+
const values = row.kind === "application_marker_committed" ? row.payload.markers : row.payload.transitions;
|
|
1307
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
1308
|
+
const transition = value;
|
|
1309
|
+
const outcome = transition.terminal?.outcome ?? transition.marker?.outcome;
|
|
1310
|
+
const source = transition.terminal?.source ?? transition.marker?.source;
|
|
1311
|
+
state.observations.push({
|
|
1312
|
+
...base,
|
|
1313
|
+
reasonCode: "committed",
|
|
1314
|
+
knowledgeTraceId: transition.trace_id,
|
|
1315
|
+
knowledgeEntryId: transition.entry_id,
|
|
1316
|
+
receiptOutcome: outcome,
|
|
1317
|
+
receiptSource: source
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
if (typeof row.payload.empty_trace_id === "string") {
|
|
1321
|
+
state.observations.push({
|
|
1322
|
+
...base,
|
|
1323
|
+
reasonCode: "committed",
|
|
1324
|
+
knowledgeTraceId: row.payload.empty_trace_id,
|
|
1325
|
+
receiptOutcome: "no_relevant"
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (row.kind === "terminal_attempt_rejected") {
|
|
1331
|
+
for (const value of Array.isArray(row.payload.rejected) ? row.payload.rejected : []) {
|
|
1332
|
+
const rejected = value;
|
|
1333
|
+
state.observations.push({
|
|
1334
|
+
...base,
|
|
1335
|
+
reasonCode: rejectionReasonCode(rejected.reason),
|
|
1336
|
+
knowledgeTraceId: typeof row.payload.trace_id === "string" ? row.payload.trace_id : undefined,
|
|
1337
|
+
knowledgeEntryId: rejected.entry_id
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (row.kind === "terminal_attempt_idempotent") {
|
|
1343
|
+
for (const entryId of Array.isArray(row.payload.entry_ids) ? row.payload.entry_ids : []) {
|
|
1344
|
+
state.observations.push({
|
|
1345
|
+
...base,
|
|
1346
|
+
reasonCode: "idempotent",
|
|
1347
|
+
knowledgeTraceId: typeof row.payload.trace_id === "string" ? row.payload.trace_id : undefined,
|
|
1348
|
+
knowledgeEntryId: typeof entryId === "string" ? entryId : undefined
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
state.observations.push({
|
|
1354
|
+
...base,
|
|
1355
|
+
reasonCode: row.kind === "legacy_unverifiable" ? "legacy_unverifiable" : "committed",
|
|
1356
|
+
knowledgeTraceId: typeof row.payload.trace_id === "string" ? row.payload.trace_id : undefined,
|
|
1357
|
+
phase: typeof row.payload.phase === "string" ? row.payload.phase : undefined
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
async function writeSnapshot(paths, state) {
|
|
1361
|
+
const snapshot = {
|
|
1362
|
+
schema_version: RECEIPT_SCHEMA_VERSION,
|
|
1363
|
+
cutover_version: RECEIPT_CUTOVER_VERSION,
|
|
1364
|
+
authoritative: false,
|
|
1365
|
+
rebuildable: true,
|
|
1366
|
+
through_seq: state.lastSeq,
|
|
1367
|
+
through_hash: state.lastHash,
|
|
1368
|
+
memberships: [...state.memberships.values()],
|
|
1369
|
+
empty_traces: [...state.emptyTraces.values()],
|
|
1370
|
+
cutover_completed: state.cutoverCompleted,
|
|
1371
|
+
legacy_unverifiable: state.legacyUnverifiable,
|
|
1372
|
+
legacy_trace_registry: [...state.legacyTraceRegistry].sort(),
|
|
1373
|
+
legacy_unverifiable_trace_ids: [...state.legacyUnverifiableTraceIds].sort(),
|
|
1374
|
+
phase_lifecycle: [...state.phaseLifecycle.values()],
|
|
1375
|
+
audit_tail: state.auditTail
|
|
1376
|
+
};
|
|
1377
|
+
await atomicWriteFsynced(paths.snapshot, `${JSON.stringify(snapshot)}
|
|
1378
|
+
`);
|
|
1379
|
+
}
|
|
1380
|
+
function unavailable(error) {
|
|
1381
|
+
if (error instanceof ReceiptStoreError)
|
|
1382
|
+
return {
|
|
1383
|
+
ok: false,
|
|
1384
|
+
code: error.code,
|
|
1385
|
+
detail: error.message,
|
|
1386
|
+
uncertainty: error.message
|
|
1387
|
+
};
|
|
1388
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1389
|
+
return { ok: false, code: "store_unavailable", detail, uncertainty: detail };
|
|
1390
|
+
}
|
|
1391
|
+
async function ensureCutoverLocked(paths, state, graceDays) {
|
|
1392
|
+
if (state.cutoverCompleted)
|
|
1393
|
+
return;
|
|
1394
|
+
const imported = [];
|
|
1395
|
+
const unverifiable = new Set;
|
|
1396
|
+
const unverifiableTraceIds = new Set;
|
|
1397
|
+
if (existsSync2(paths.linkPointer)) {
|
|
1398
|
+
unverifiable.add("linked_legacy_store");
|
|
1399
|
+
} else {
|
|
1400
|
+
let raw = null;
|
|
1401
|
+
try {
|
|
1402
|
+
raw = await readUtf8IfPresent(paths.legacyEvents, MAX_LEGACY_BYTES);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
if (error instanceof ReceiptStoreError && error.code === "store_corrupt") {
|
|
1405
|
+
unverifiable.add("oversized_legacy_store");
|
|
1406
|
+
} else {
|
|
1407
|
+
throw error;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (raw) {
|
|
1411
|
+
const events = [];
|
|
1412
|
+
for (const line of raw.split(`
|
|
1413
|
+
`).filter(Boolean)) {
|
|
1414
|
+
try {
|
|
1415
|
+
events.push(JSON.parse(line));
|
|
1416
|
+
} catch {
|
|
1417
|
+
unverifiable.add("malformed_legacy_line");
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const retrievals = new Map;
|
|
1421
|
+
const observedTraceIds = new Set;
|
|
1422
|
+
for (const event of events) {
|
|
1423
|
+
if (typeof event.trace_id === "string" && event.trace_id) {
|
|
1424
|
+
observedTraceIds.add(event.trace_id);
|
|
1425
|
+
}
|
|
1426
|
+
if (event.type !== "retrieved" || typeof event.trace_id !== "string") {
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
const rows = retrievals.get(event.trace_id) ?? [];
|
|
1430
|
+
rows.push(event);
|
|
1431
|
+
retrievals.set(event.trace_id, rows);
|
|
1432
|
+
}
|
|
1433
|
+
for (const [traceId, rows] of retrievals) {
|
|
1434
|
+
if (rows.length !== 1) {
|
|
1435
|
+
unverifiable.add("duplicate_legacy_trace");
|
|
1436
|
+
unverifiableTraceIds.add(traceId);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const event = rows[0];
|
|
1440
|
+
if (typeof event.session_id !== "string" || !event.session_id || !Array.isArray(event.result_ids)) {
|
|
1441
|
+
unverifiable.add("malformed_legacy_membership");
|
|
1442
|
+
unverifiableTraceIds.add(traceId);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
const timestamp = typeof event.timestamp === "string" ? event.timestamp : "";
|
|
1446
|
+
if (!Number.isFinite(Date.parse(timestamp))) {
|
|
1447
|
+
unverifiable.add("malformed_legacy_timestamp");
|
|
1448
|
+
unverifiableTraceIds.add(traceId);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const entryIds = event.result_ids;
|
|
1452
|
+
if (entryIds.length === 0 || entryIds.some((entryId) => typeof entryId !== "string" || !entryId) || new Set(entryIds).size !== entryIds.length) {
|
|
1453
|
+
unverifiable.add("malformed_legacy_membership");
|
|
1454
|
+
unverifiableTraceIds.add(traceId);
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const traceMemberships = [];
|
|
1458
|
+
let traceUnverifiable = false;
|
|
1459
|
+
for (const entryId of entryIds) {
|
|
1460
|
+
const terminals = events.filter((candidate) => isReceiptOutcome(candidate.type) && candidate.trace_id === traceId && candidate.knowledge_id === entryId && candidate.session_id === event.session_id);
|
|
1461
|
+
const distinctOutcomes = new Set(terminals.map((terminal2) => terminal2.type));
|
|
1462
|
+
if (distinctOutcomes.size > 1) {
|
|
1463
|
+
unverifiable.add("conflicting_legacy_terminal");
|
|
1464
|
+
unverifiableTraceIds.add(traceId);
|
|
1465
|
+
traceUnverifiable = true;
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
const terminalEvent = terminals.at(-1);
|
|
1469
|
+
let terminal;
|
|
1470
|
+
if (terminalEvent) {
|
|
1471
|
+
if (typeof terminalEvent.event_id !== "string" || !terminalEvent.event_id || typeof terminalEvent.timestamp !== "string" || !Number.isFinite(Date.parse(terminalEvent.timestamp))) {
|
|
1472
|
+
unverifiable.add("malformed_legacy_terminal");
|
|
1473
|
+
unverifiableTraceIds.add(traceId);
|
|
1474
|
+
traceUnverifiable = true;
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1477
|
+
terminal = {
|
|
1478
|
+
outcome: terminalEvent.type,
|
|
1479
|
+
source: typeof terminalEvent.source === "string" ? terminalEvent.source : "unknown",
|
|
1480
|
+
reason: typeof terminalEvent.reason === "string" ? terminalEvent.reason : undefined,
|
|
1481
|
+
event_id: terminalEvent.event_id,
|
|
1482
|
+
committed_at: terminalEvent.timestamp
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
traceMemberships.push({
|
|
1486
|
+
trace_id: traceId,
|
|
1487
|
+
entry_id: entryId,
|
|
1488
|
+
session_id: event.session_id,
|
|
1489
|
+
phase: typeof event.phase === "string" ? event.phase : undefined,
|
|
1490
|
+
task_id: typeof event.task_id === "string" ? event.task_id : undefined,
|
|
1491
|
+
agent: typeof event.agent === "string" ? event.agent : undefined,
|
|
1492
|
+
critical: true,
|
|
1493
|
+
committed_at: timestamp,
|
|
1494
|
+
membership_event_id: typeof event.event_id === "string" && event.event_id ? event.event_id : randomUUID2(),
|
|
1495
|
+
grace_days: graceDays,
|
|
1496
|
+
exposure_kind: legacyExposureKind(event),
|
|
1497
|
+
origin: "legacy",
|
|
1498
|
+
terminal,
|
|
1499
|
+
terminal_history: terminal ? [{ ...terminal }] : undefined
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
if (!traceUnverifiable) {
|
|
1503
|
+
imported.push(...traceMemberships);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
for (const traceId of observedTraceIds) {
|
|
1507
|
+
if (!retrievals.has(traceId)) {
|
|
1508
|
+
unverifiable.add("evicted_legacy_membership");
|
|
1509
|
+
unverifiableTraceIds.add(traceId);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
} else {
|
|
1513
|
+
unverifiable.add(existsSync2(paths.legacyEvents) ? "empty_legacy_store" : "missing_legacy_store");
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (imported.length)
|
|
1517
|
+
await appendRecord(paths, state, makeRecord(state, "legacy_imported", { memberships: imported }));
|
|
1518
|
+
if (unverifiable.size) {
|
|
1519
|
+
await appendRecord(paths, state, makeRecord(state, "legacy_unverifiable", {
|
|
1520
|
+
reason_codes: [...unverifiable].sort(),
|
|
1521
|
+
trace_ids: [...unverifiableTraceIds].sort()
|
|
1522
|
+
}));
|
|
1523
|
+
}
|
|
1524
|
+
await appendRecord(paths, state, makeRecord(state, "cutover_completed", {
|
|
1525
|
+
imported_count: imported.length,
|
|
1526
|
+
imported_trace_ids: [
|
|
1527
|
+
...new Set(imported.map((membership) => membership.trace_id))
|
|
1528
|
+
].sort()
|
|
1529
|
+
}));
|
|
1530
|
+
}
|
|
1531
|
+
function isReceiptOutcome(value) {
|
|
1532
|
+
return value === "applied" || value === "ignored" || value === "contradicted" || value === "violated" || value === "n_a";
|
|
1533
|
+
}
|
|
1534
|
+
function isLegacyUnverifiableTrace(state, traceId) {
|
|
1535
|
+
if (state.legacyTraceRegistry.has(traceId))
|
|
1536
|
+
return true;
|
|
1537
|
+
if (state.legacyUnverifiableTraceIds.has(traceId))
|
|
1538
|
+
return true;
|
|
1539
|
+
if (!state.legacyUnverifiable)
|
|
1540
|
+
return false;
|
|
1541
|
+
return ![...state.memberships.values()].some((membership) => membership.origin === "v2");
|
|
1542
|
+
}
|
|
1543
|
+
async function runLocked(directory, graceDays, action, options = {}) {
|
|
1544
|
+
const observations = [];
|
|
1545
|
+
try {
|
|
1546
|
+
const result = await withReceiptLedgerLock(directory, async (paths) => {
|
|
1547
|
+
const state = await loadState(paths, observations);
|
|
1548
|
+
const resolvedGraceDays = graceDays ?? (state.cutoverCompleted ? DEFAULT_RECEIPT_GRACE_DAYS : configuredGraceDays(directory));
|
|
1549
|
+
await ensureCutoverLocked(paths, state, resolvedGraceDays);
|
|
1550
|
+
if (options.compact !== false)
|
|
1551
|
+
await compactIfNeeded(paths, state);
|
|
1552
|
+
const result2 = await action(paths, state, resolvedGraceDays);
|
|
1553
|
+
if (options.writeSnapshot !== false) {
|
|
1554
|
+
await _internals3.writeSnapshot(paths, state).catch(() => {
|
|
1555
|
+
return;
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return { ok: true, ...result2 };
|
|
1559
|
+
});
|
|
1560
|
+
for (const observation of observations) {
|
|
1561
|
+
emitKnowledgeReceiptTransition(observation);
|
|
1562
|
+
}
|
|
1563
|
+
return result;
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
for (const observation of observations) {
|
|
1566
|
+
emitKnowledgeReceiptTransition(observation);
|
|
1567
|
+
}
|
|
1568
|
+
return unavailable(error);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
async function commitApplicationMarkerBatch(directory, input) {
|
|
1572
|
+
return runLocked(directory, undefined, async (paths, state) => {
|
|
1573
|
+
const items = input.items ?? input.markers ?? [];
|
|
1574
|
+
const committed = [];
|
|
1575
|
+
const idempotent = [];
|
|
1576
|
+
const rejected = [];
|
|
1577
|
+
const transitions = [];
|
|
1578
|
+
const traceExists = state.traceIds.has(input.trace_id);
|
|
1579
|
+
const staged = new Map;
|
|
1580
|
+
for (const item of items) {
|
|
1581
|
+
const membership = state.memberships.get(keyOf(input.trace_id, item.entry_id));
|
|
1582
|
+
if (!membership) {
|
|
1583
|
+
rejected.push({
|
|
1584
|
+
entry_id: item.entry_id,
|
|
1585
|
+
reason: traceExists ? "id_not_in_trace" : isLegacyUnverifiableTrace(state, input.trace_id) ? "legacy_unverifiable" : "trace_not_found"
|
|
1586
|
+
});
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (membership.session_id !== input.session_id) {
|
|
1590
|
+
rejected.push({ entry_id: item.entry_id, reason: "wrong_session" });
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
const stagedOutcome = staged.get(item.entry_id);
|
|
1594
|
+
if (stagedOutcome !== undefined) {
|
|
1595
|
+
if (stagedOutcome === item.outcome)
|
|
1596
|
+
idempotent.push(item.entry_id);
|
|
1597
|
+
else {
|
|
1598
|
+
rejected.push({
|
|
1599
|
+
entry_id: item.entry_id,
|
|
1600
|
+
reason: "duplicate_conflicting_terminal"
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
staged.set(item.entry_id, item.outcome);
|
|
1606
|
+
if (membership.application_marker?.outcome === item.outcome) {
|
|
1607
|
+
idempotent.push(item.entry_id);
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (membership.application_marker) {
|
|
1611
|
+
rejected.push({
|
|
1612
|
+
entry_id: item.entry_id,
|
|
1613
|
+
reason: "duplicate_conflicting_terminal"
|
|
1614
|
+
});
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const eventId = randomUUID2();
|
|
1618
|
+
const marker = {
|
|
1619
|
+
outcome: item.outcome,
|
|
1620
|
+
source: item.source ?? "unknown",
|
|
1621
|
+
reason: item.reason,
|
|
1622
|
+
event_id: eventId,
|
|
1623
|
+
committed_at: nowIso()
|
|
1624
|
+
};
|
|
1625
|
+
transitions.push({
|
|
1626
|
+
trace_id: input.trace_id,
|
|
1627
|
+
entry_id: item.entry_id,
|
|
1628
|
+
marker
|
|
1629
|
+
});
|
|
1630
|
+
committed.push({
|
|
1631
|
+
entry_id: item.entry_id,
|
|
1632
|
+
outcome: item.outcome,
|
|
1633
|
+
event_id: eventId
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
if (transitions.length) {
|
|
1637
|
+
await appendRecord(paths, state, makeRecord(state, "application_marker_committed", {
|
|
1638
|
+
markers: transitions
|
|
1639
|
+
}));
|
|
1640
|
+
}
|
|
1641
|
+
return { committed, idempotent, rejected };
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
async function queryHistoricalOutcomes(directory, entryIds, graceDays) {
|
|
1645
|
+
return runLocked(directory, graceDays, async (paths, state) => {
|
|
1646
|
+
const wanted = entryIds ? new Set(entryIds) : null;
|
|
1647
|
+
const memberships = new Map;
|
|
1648
|
+
for (const membership of state.memberships.values()) {
|
|
1649
|
+
if (!wanted || wanted.has(membership.entry_id)) {
|
|
1650
|
+
memberships.set(archiveKey(membership), membership);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const raw = await readUtf8IfPresent(paths.archive, MAX_ARCHIVE_BYTES);
|
|
1654
|
+
if (raw) {
|
|
1655
|
+
for (const line of raw.split(`
|
|
1656
|
+
`).filter(Boolean)) {
|
|
1657
|
+
let summary = null;
|
|
1658
|
+
try {
|
|
1659
|
+
summary = parseArchiveSummary(JSON.parse(line));
|
|
1660
|
+
} catch {}
|
|
1661
|
+
if (!summary) {
|
|
1662
|
+
throw new ReceiptStoreError("store_corrupt", "receipt archive contains an invalid authoritative summary");
|
|
1663
|
+
}
|
|
1664
|
+
if ("entry_id" in summary && (!wanted || wanted.has(summary.entry_id))) {
|
|
1665
|
+
const key = archiveKey(summary);
|
|
1666
|
+
if (!memberships.has(key))
|
|
1667
|
+
memberships.set(key, summary);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return { memberships: [...memberships.values()] };
|
|
1672
|
+
}, { compact: false, writeSnapshot: false });
|
|
1673
|
+
}
|
|
1674
|
+
async function phaseTransition(directory, phase, kind, sessionId, taskId, graceDays) {
|
|
1675
|
+
return runLocked(directory, graceDays, async (paths, state) => {
|
|
1676
|
+
if (!phase.trim()) {
|
|
1677
|
+
throw new ReceiptStoreError("store_unavailable", "phase is required");
|
|
1678
|
+
}
|
|
1679
|
+
const scopeKeys = new Map;
|
|
1680
|
+
const addScope = (scopeSessionId, scopeTaskId) => {
|
|
1681
|
+
if (sessionId !== undefined && scopeSessionId !== sessionId || taskId !== undefined && scopeTaskId !== taskId)
|
|
1682
|
+
return;
|
|
1683
|
+
scopeKeys.set(phaseLifecycleKey(phase, scopeSessionId, scopeTaskId), {
|
|
1684
|
+
session_id: scopeSessionId,
|
|
1685
|
+
task_id: scopeTaskId
|
|
1686
|
+
});
|
|
1687
|
+
};
|
|
1688
|
+
for (const membership of state.memberships.values()) {
|
|
1689
|
+
if (membership.phase === phase)
|
|
1690
|
+
addScope(membership.session_id, membership.task_id);
|
|
1691
|
+
}
|
|
1692
|
+
for (const trace of state.emptyTraces.values()) {
|
|
1693
|
+
if (trace.phase === phase)
|
|
1694
|
+
addScope(trace.session_id, trace.task_id);
|
|
1695
|
+
}
|
|
1696
|
+
for (const lifecycle of state.phaseLifecycle.values()) {
|
|
1697
|
+
if (lifecycle.phase === phase)
|
|
1698
|
+
addScope(lifecycle.session_id, lifecycle.task_id);
|
|
1699
|
+
}
|
|
1700
|
+
if (scopeKeys.size === 0 && sessionId !== undefined) {
|
|
1701
|
+
addScope(sessionId, taskId);
|
|
1702
|
+
}
|
|
1703
|
+
if (scopeKeys.size === 0) {
|
|
1704
|
+
throw new ReceiptStoreError("store_unavailable", "no exact receipt lifecycle scope exists for phase closure");
|
|
1705
|
+
}
|
|
1706
|
+
if (sessionId === undefined && scopeKeys.size !== 1) {
|
|
1707
|
+
throw new ReceiptStoreError("store_unavailable", "receipt lifecycle scope is ambiguous without an exact session identity");
|
|
1708
|
+
}
|
|
1709
|
+
let eventId = "";
|
|
1710
|
+
for (const [key, scope] of scopeKeys) {
|
|
1711
|
+
const lifecycle = state.phaseLifecycle.get(key);
|
|
1712
|
+
const existingEventId = kind === "phase_close_intent" ? lifecycle?.intent_event_id : lifecycle?.closed_event_id;
|
|
1713
|
+
if (existingEventId) {
|
|
1714
|
+
eventId ||= existingEventId;
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const row = makeRecord(state, kind, { phase, ...scope });
|
|
1718
|
+
await appendRecord(paths, state, row);
|
|
1719
|
+
eventId ||= row.event_id;
|
|
1720
|
+
}
|
|
1721
|
+
if (kind === "phase_closed")
|
|
1722
|
+
await compactIfNeeded(paths, state);
|
|
1723
|
+
return { event_id: eventId };
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
var recordPhaseCloseIntent = (directory, phase, sessionId, taskId) => phaseTransition(directory, phase, "phase_close_intent", sessionId, taskId);
|
|
1727
|
+
var commitPhaseClosed = (directory, phase, sessionId, taskId) => phaseTransition(directory, phase, "phase_closed", sessionId, taskId);
|
|
1728
|
+
async function reconcilePhaseClose(directory, phase, durablePlanClosed, sessionId, taskId) {
|
|
1729
|
+
if (!durablePlanClosed)
|
|
1730
|
+
return { ok: true, reconciled: false };
|
|
1731
|
+
const result = await commitPhaseClosed(directory, phase, sessionId, taskId);
|
|
1732
|
+
return result.ok ? { ok: true, reconciled: true } : result;
|
|
1733
|
+
}
|
|
1734
|
+
async function compactIfNeeded(paths, state) {
|
|
1735
|
+
const now = _internals3.nowMs();
|
|
1736
|
+
const eligible = [...state.memberships.values()].filter((m) => m.terminal && m.phase_closed_at && now >= Date.parse(m.phase_closed_at) + m.grace_days * 86400000);
|
|
1737
|
+
const eligibleEmpty = [...state.emptyTraces.values()].filter((trace) => trace.terminal_event_id && trace.phase_closed_at && now >= Date.parse(trace.phase_closed_at) + trace.grace_days * 86400000);
|
|
1738
|
+
if (!eligible.length && !eligibleEmpty.length && state.recordCount <= _internals3.maxJournalRecords)
|
|
1739
|
+
return;
|
|
1740
|
+
const archiveById = new Map;
|
|
1741
|
+
const existing = await readUtf8IfPresent(paths.archive, MAX_ARCHIVE_BYTES);
|
|
1742
|
+
if (existing) {
|
|
1743
|
+
for (const line of existing.split(`
|
|
1744
|
+
`).filter(Boolean)) {
|
|
1745
|
+
let summary = null;
|
|
1746
|
+
try {
|
|
1747
|
+
summary = parseArchiveSummary(JSON.parse(line));
|
|
1748
|
+
} catch {}
|
|
1749
|
+
if (!summary) {
|
|
1750
|
+
throw new ReceiptStoreError("store_corrupt", "receipt archive contains an invalid summary");
|
|
1751
|
+
}
|
|
1752
|
+
archiveById.set(archiveKey(summary), summary);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const retainedArchive = [...archiveById.values()];
|
|
1756
|
+
let archiveContent = serializeArchive(retainedArchive);
|
|
1757
|
+
let archiveBytes = Buffer.byteLength(archiveContent, "utf8");
|
|
1758
|
+
let archiveChanged = false;
|
|
1759
|
+
const retainIfCapacity = (summary) => {
|
|
1760
|
+
const key = archiveKey(summary);
|
|
1761
|
+
if (archiveById.has(key))
|
|
1762
|
+
return;
|
|
1763
|
+
const line = `${JSON.stringify(summary)}
|
|
1764
|
+
`;
|
|
1765
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
1766
|
+
if (retainedArchive.length + 1 > _internals3.maxArchiveRecords || archiveBytes + lineBytes > _internals3.maxArchiveBytes) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
archiveById.set(key, summary);
|
|
1770
|
+
retainedArchive.push(summary);
|
|
1771
|
+
archiveContent += line;
|
|
1772
|
+
archiveBytes += lineBytes;
|
|
1773
|
+
archiveChanged = true;
|
|
1774
|
+
};
|
|
1775
|
+
for (const membership of eligible)
|
|
1776
|
+
retainIfCapacity(membership);
|
|
1777
|
+
for (const trace of eligibleEmpty) {
|
|
1778
|
+
retainIfCapacity({ ...trace, summary_kind: "empty_trace" });
|
|
1779
|
+
}
|
|
1780
|
+
if (archiveChanged) {
|
|
1781
|
+
await _internals3.atomicWriteFsynced(paths.archive, archiveContent);
|
|
1782
|
+
}
|
|
1783
|
+
const retainedIds = new Set(retainedArchive.map(archiveKey));
|
|
1784
|
+
for (const membership of eligible) {
|
|
1785
|
+
if (retainedIds.has(archiveKey(membership))) {
|
|
1786
|
+
state.memberships.delete(keyOf(membership.trace_id, membership.entry_id));
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
for (const trace of eligibleEmpty) {
|
|
1790
|
+
const summary = {
|
|
1791
|
+
...trace,
|
|
1792
|
+
summary_kind: "empty_trace"
|
|
1793
|
+
};
|
|
1794
|
+
if (retainedIds.has(archiveKey(summary))) {
|
|
1795
|
+
state.emptyTraces.delete(emptyKeyOf(trace.trace_id, trace.session_id));
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
for (const traceId of [...state.legacyTraceRegistry]) {
|
|
1799
|
+
const stillLive = [...state.memberships.values()].some((membership) => membership.origin === "legacy" && membership.trace_id === traceId);
|
|
1800
|
+
if (!stillLive)
|
|
1801
|
+
state.legacyTraceRegistry.delete(traceId);
|
|
1802
|
+
}
|
|
1803
|
+
state.traceIds = new Set([...state.memberships.values()].map((membership) => membership.trace_id));
|
|
1804
|
+
const fresh = emptyState(state.observations);
|
|
1805
|
+
fresh.cutoverCompleted = state.cutoverCompleted;
|
|
1806
|
+
fresh.legacyUnverifiable = state.legacyUnverifiable;
|
|
1807
|
+
fresh.legacyTraceRegistry = new Set(state.legacyTraceRegistry);
|
|
1808
|
+
fresh.legacyUnverifiableTraceIds = new Set(state.legacyUnverifiableTraceIds);
|
|
1809
|
+
fresh.phaseLifecycle = new Map(state.phaseLifecycle);
|
|
1810
|
+
fresh.auditTail = [...state.auditTail];
|
|
1811
|
+
const checkpoint = makeRecord(fresh, "checkpoint", {
|
|
1812
|
+
memberships: [...state.memberships.values()],
|
|
1813
|
+
empty_traces: [...state.emptyTraces.values()],
|
|
1814
|
+
cutover_completed: state.cutoverCompleted,
|
|
1815
|
+
legacy_unverifiable: state.legacyUnverifiable,
|
|
1816
|
+
legacy_trace_registry: [...state.legacyTraceRegistry].sort(),
|
|
1817
|
+
legacy_unverifiable_trace_ids: [...state.legacyUnverifiableTraceIds].sort(),
|
|
1818
|
+
phase_lifecycle: [...state.phaseLifecycle.values()],
|
|
1819
|
+
audit_tail: state.auditTail
|
|
1820
|
+
});
|
|
1821
|
+
await _internals3.atomicWriteFsynced(paths.journal, `${JSON.stringify(checkpoint)}
|
|
1822
|
+
`);
|
|
1823
|
+
state.lastSeq = checkpoint.seq;
|
|
1824
|
+
state.lastHash = checkpoint.hash;
|
|
1825
|
+
state.recordCount = 1;
|
|
1826
|
+
queueRecordObservations(state, checkpoint);
|
|
1827
|
+
}
|
|
1828
|
+
function serializeArchive(summaries) {
|
|
1829
|
+
return summaries.length ? `${summaries.map((summary) => JSON.stringify(summary)).join(`
|
|
1830
|
+
`)}
|
|
1831
|
+
` : "";
|
|
1832
|
+
}
|
|
1833
|
+
var _internals3 = {
|
|
1834
|
+
nowMs: () => Date.now(),
|
|
1835
|
+
writeSnapshot,
|
|
1836
|
+
atomicWriteFsynced,
|
|
1837
|
+
commitApplicationMarkerBatch,
|
|
1838
|
+
maxJournalRecords: MAX_JOURNAL_RECORDS,
|
|
1839
|
+
maxArchiveRecords: MAX_ARCHIVE_RECORDS,
|
|
1840
|
+
maxArchiveBytes: MAX_ARCHIVE_BYTES
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
// src/hooks/knowledge-events.ts
|
|
1844
|
+
var KNOWLEDGE_EVENT_SCHEMA_VERSION = 1;
|
|
1845
|
+
var MAX_EVENT_LOG_ENTRIES = 5000;
|
|
1846
|
+
var counterRollupCache = new Map;
|
|
1847
|
+
var MAX_COUNTER_ROLLUP_CACHE_DIRS = 32;
|
|
1848
|
+
var RECEIPT_EVENT_TYPES = new Set([
|
|
1849
|
+
"acknowledged",
|
|
1850
|
+
"applied",
|
|
1851
|
+
"ignored",
|
|
1852
|
+
"contradicted",
|
|
1853
|
+
"violated",
|
|
1854
|
+
"n_a",
|
|
1855
|
+
"override",
|
|
1856
|
+
"no_relevant"
|
|
1857
|
+
]);
|
|
1858
|
+
function resolveKnowledgeEventsPath(directory) {
|
|
1859
|
+
return path2.join(resolveKnowledgeStoreDir(directory), "knowledge-events.jsonl");
|
|
1860
|
+
}
|
|
1861
|
+
function resolveKnowledgeCounterBaselinePath(directory) {
|
|
1862
|
+
return path2.join(resolveKnowledgeStoreDir(directory), "knowledge-counter-baseline.json");
|
|
1863
|
+
}
|
|
1864
|
+
var resolveHiveEventsPath2 = resolveHiveEventsPath;
|
|
1865
|
+
function resolveLegacyApplicationLogPath(directory) {
|
|
1866
|
+
return path2.join(resolveKnowledgeStoreDir(directory), "knowledge-application.jsonl");
|
|
1867
|
+
}
|
|
1868
|
+
function newTraceId() {
|
|
1869
|
+
return randomUUID3();
|
|
1870
|
+
}
|
|
1871
|
+
function newEventId() {
|
|
1872
|
+
return randomUUID3();
|
|
1873
|
+
}
|
|
1874
|
+
function withDefaults(event) {
|
|
1875
|
+
return {
|
|
1876
|
+
schema_version: KNOWLEDGE_EVENT_SCHEMA_VERSION,
|
|
1877
|
+
...event,
|
|
1878
|
+
event_id: event.event_id || newEventId(),
|
|
1879
|
+
timestamp: event.timestamp || new Date().toISOString()
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
async function appendKnowledgeEvent(directory, event) {
|
|
1883
|
+
const [populated] = await appendKnowledgeEventsBatch(directory, [event]);
|
|
1884
|
+
return populated;
|
|
1885
|
+
}
|
|
1886
|
+
async function appendKnowledgeEventsBatch(directory, events) {
|
|
1887
|
+
if (events.length === 0)
|
|
1888
|
+
return [];
|
|
1889
|
+
const populated = events.map(withDefaults);
|
|
1890
|
+
const filePath = resolveKnowledgeEventsPath(directory);
|
|
1891
|
+
const dirPath = path2.dirname(filePath);
|
|
1892
|
+
await mkdir2(dirPath, { recursive: true });
|
|
1893
|
+
let release;
|
|
1894
|
+
try {
|
|
1895
|
+
release = await import_proper_lockfile.default.lock(dirPath, {
|
|
1896
|
+
retries: { retries: 200, minTimeout: 10, maxTimeout: 100 }
|
|
1897
|
+
});
|
|
1898
|
+
await appendFile(filePath, `${populated.map((e) => JSON.stringify(e)).join(`
|
|
1899
|
+
`)}
|
|
1900
|
+
`, "utf-8");
|
|
1901
|
+
try {
|
|
1902
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1903
|
+
const lines = content.split(`
|
|
1904
|
+
`).filter((line) => line.trim().length > 0);
|
|
1905
|
+
if (lines.length > MAX_EVENT_LOG_ENTRIES) {
|
|
1906
|
+
const evicted = lines.slice(0, lines.length - MAX_EVENT_LOG_ENTRIES);
|
|
1907
|
+
const trimmed = lines.slice(lines.length - MAX_EVENT_LOG_ENTRIES);
|
|
1908
|
+
await foldEvictedEventsIntoBaseline(directory, evicted, filePath);
|
|
1909
|
+
await atomicWriteFile(filePath, `${trimmed.join(`
|
|
1910
|
+
`)}
|
|
1911
|
+
`);
|
|
1912
|
+
}
|
|
1913
|
+
} catch (err) {
|
|
1914
|
+
warn(`[knowledge-events] local cap trim failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1915
|
+
}
|
|
1916
|
+
} finally {
|
|
1917
|
+
if (release)
|
|
1918
|
+
await release().catch(() => {});
|
|
1919
|
+
}
|
|
1920
|
+
return populated;
|
|
1921
|
+
}
|
|
1922
|
+
async function recordKnowledgeEvent(directory, event) {
|
|
1923
|
+
try {
|
|
1924
|
+
return await appendKnowledgeEvent(directory, event);
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
warn(`[knowledge-events] recordKnowledgeEvent failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
async function recordKnowledgeEventsBatch(directory, events) {
|
|
1931
|
+
try {
|
|
1932
|
+
return await appendKnowledgeEventsBatch(directory, events);
|
|
1933
|
+
} catch (err) {
|
|
1934
|
+
warn(`[knowledge-events] recordKnowledgeEventsBatch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
async function appendHiveKnowledgeEvent(event) {
|
|
1939
|
+
const populated = withDefaults(event);
|
|
1940
|
+
const filePath = resolveHiveEventsPath2();
|
|
1941
|
+
const dirPath = path2.dirname(filePath);
|
|
1942
|
+
await mkdir2(dirPath, { recursive: true });
|
|
1943
|
+
let release;
|
|
1944
|
+
try {
|
|
1945
|
+
release = await import_proper_lockfile.default.lock(dirPath, {
|
|
1946
|
+
retries: { retries: 200, minTimeout: 10, maxTimeout: 100 },
|
|
1947
|
+
stale: 5000
|
|
1948
|
+
});
|
|
1949
|
+
await appendFile(filePath, `${JSON.stringify(populated)}
|
|
1950
|
+
`, "utf-8");
|
|
1951
|
+
try {
|
|
1952
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1953
|
+
const lines = content.split(`
|
|
1954
|
+
`).filter((line) => line.trim().length > 0);
|
|
1955
|
+
if (lines.length > MAX_EVENT_LOG_ENTRIES) {
|
|
1956
|
+
const trimmed = lines.slice(lines.length - MAX_EVENT_LOG_ENTRIES);
|
|
1957
|
+
await atomicWriteFile(filePath, `${trimmed.join(`
|
|
1958
|
+
`)}
|
|
1959
|
+
`);
|
|
1960
|
+
}
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
warn(`[knowledge-events] hive cap trim failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1963
|
+
}
|
|
1964
|
+
} finally {
|
|
1965
|
+
if (release)
|
|
1966
|
+
await release().catch(() => {});
|
|
1967
|
+
}
|
|
1968
|
+
return populated;
|
|
1969
|
+
}
|
|
1970
|
+
async function recordHiveKnowledgeEvent(event) {
|
|
1971
|
+
try {
|
|
1972
|
+
return await appendHiveKnowledgeEvent(event);
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
warn(`[knowledge-events] recordHiveKnowledgeEvent failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
async function readKnowledgeEvents(directory, maxEvents) {
|
|
1979
|
+
const filePath = resolveKnowledgeEventsPath(directory);
|
|
1980
|
+
if (!existsSync3(filePath))
|
|
1981
|
+
return [];
|
|
1982
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1983
|
+
const out = [];
|
|
1984
|
+
const max = maxEvents !== undefined && maxEvents > 0 ? maxEvents : Infinity;
|
|
1985
|
+
for (const line of content.split(`
|
|
1986
|
+
`)) {
|
|
1987
|
+
if (out.length >= max)
|
|
1988
|
+
break;
|
|
1989
|
+
const trimmed = line.trim();
|
|
1990
|
+
if (!trimmed)
|
|
1991
|
+
continue;
|
|
1992
|
+
try {
|
|
1993
|
+
out.push(JSON.parse(trimmed));
|
|
1994
|
+
} catch {
|
|
1995
|
+
warn(`[knowledge-events] Skipping corrupted JSONL line in ${filePath}: ${trimmed.slice(0, 80)}`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
return out;
|
|
1999
|
+
}
|
|
2000
|
+
async function readHiveKnowledgeEvents() {
|
|
2001
|
+
const filePath = resolveHiveEventsPath2();
|
|
2002
|
+
if (!existsSync3(filePath))
|
|
2003
|
+
return [];
|
|
2004
|
+
const content = await readFile2(filePath, "utf-8");
|
|
2005
|
+
return parseEventLines(content.split(`
|
|
2006
|
+
`), filePath);
|
|
2007
|
+
}
|
|
2008
|
+
async function readLegacyApplicationRecords(directory) {
|
|
2009
|
+
const filePath = resolveLegacyApplicationLogPath(directory);
|
|
2010
|
+
if (!existsSync3(filePath))
|
|
2011
|
+
return [];
|
|
2012
|
+
const content = await readFile2(filePath, "utf-8");
|
|
2013
|
+
const out = [];
|
|
2014
|
+
for (const line of content.split(`
|
|
2015
|
+
`)) {
|
|
2016
|
+
const trimmed = line.trim();
|
|
2017
|
+
if (!trimmed)
|
|
2018
|
+
continue;
|
|
2019
|
+
try {
|
|
2020
|
+
out.push(JSON.parse(trimmed));
|
|
2021
|
+
} catch {
|
|
2022
|
+
warn(`[knowledge-events] Skipping corrupted JSONL line in ${filePath}: ${trimmed.slice(0, 80)}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return out;
|
|
2026
|
+
}
|
|
2027
|
+
var MAX_VIOLATION_TIMESTAMPS = 10;
|
|
2028
|
+
function emptyRollup() {
|
|
2029
|
+
return {
|
|
2030
|
+
shown_count: 0,
|
|
2031
|
+
acknowledged_count: 0,
|
|
2032
|
+
applied_explicit_count: 0,
|
|
2033
|
+
ignored_count: 0,
|
|
2034
|
+
violated_count: 0,
|
|
2035
|
+
contradicted_count: 0,
|
|
2036
|
+
n_a_count: 0,
|
|
2037
|
+
succeeded_after_shown_count: 0,
|
|
2038
|
+
failed_after_shown_count: 0,
|
|
2039
|
+
partial_after_shown_count: 0,
|
|
2040
|
+
violation_timestamps: []
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function cloneRollup(input) {
|
|
2044
|
+
return {
|
|
2045
|
+
...emptyRollup(),
|
|
2046
|
+
...input,
|
|
2047
|
+
violation_timestamps: [...input.violation_timestamps ?? []]
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
function cloneRollupMap(input) {
|
|
2051
|
+
const out = new Map;
|
|
2052
|
+
for (const [id, rollup] of input) {
|
|
2053
|
+
out.set(id, cloneRollup(rollup));
|
|
2054
|
+
}
|
|
2055
|
+
return out;
|
|
2056
|
+
}
|
|
2057
|
+
function normalizeRollupTimestamps(rollup) {
|
|
2058
|
+
if (rollup.violation_timestamps.length > 1) {
|
|
2059
|
+
rollup.violation_timestamps.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
|
|
2060
|
+
}
|
|
2061
|
+
if (rollup.violation_timestamps.length > MAX_VIOLATION_TIMESTAMPS) {
|
|
2062
|
+
rollup.violation_timestamps = rollup.violation_timestamps.slice(0, MAX_VIOLATION_TIMESTAMPS);
|
|
2063
|
+
}
|
|
2064
|
+
return rollup;
|
|
2065
|
+
}
|
|
2066
|
+
function mergeRollupInto(target, source) {
|
|
2067
|
+
target.shown_count += source.shown_count ?? 0;
|
|
2068
|
+
target.acknowledged_count += source.acknowledged_count ?? 0;
|
|
2069
|
+
target.applied_explicit_count += source.applied_explicit_count ?? 0;
|
|
2070
|
+
target.ignored_count += source.ignored_count ?? 0;
|
|
2071
|
+
target.violated_count += source.violated_count ?? 0;
|
|
2072
|
+
target.contradicted_count += source.contradicted_count ?? 0;
|
|
2073
|
+
target.n_a_count += source.n_a_count ?? 0;
|
|
2074
|
+
target.succeeded_after_shown_count += source.succeeded_after_shown_count ?? 0;
|
|
2075
|
+
target.failed_after_shown_count += source.failed_after_shown_count ?? 0;
|
|
2076
|
+
target.partial_after_shown_count += source.partial_after_shown_count ?? 0;
|
|
2077
|
+
if (source.last_applied_at) {
|
|
2078
|
+
target.last_applied_at = maxIso(target.last_applied_at, source.last_applied_at);
|
|
2079
|
+
}
|
|
2080
|
+
if (source.last_acknowledged_at) {
|
|
2081
|
+
target.last_acknowledged_at = maxIso(target.last_acknowledged_at, source.last_acknowledged_at);
|
|
2082
|
+
}
|
|
2083
|
+
target.violation_timestamps.push(...source.violation_timestamps ?? []);
|
|
2084
|
+
normalizeRollupTimestamps(target);
|
|
2085
|
+
}
|
|
2086
|
+
function get(map, id) {
|
|
2087
|
+
let r = map.get(id);
|
|
2088
|
+
if (!r) {
|
|
2089
|
+
r = emptyRollup();
|
|
2090
|
+
map.set(id, r);
|
|
2091
|
+
}
|
|
2092
|
+
return r;
|
|
2093
|
+
}
|
|
2094
|
+
function maxIso(current, candidate) {
|
|
2095
|
+
if (!current)
|
|
2096
|
+
return candidate;
|
|
2097
|
+
return candidate > current ? candidate : current;
|
|
2098
|
+
}
|
|
2099
|
+
async function readCounterBaseline(directory) {
|
|
2100
|
+
const filePath = resolveKnowledgeCounterBaselinePath(directory);
|
|
2101
|
+
if (!existsSync3(filePath))
|
|
2102
|
+
return new Map;
|
|
2103
|
+
let raw;
|
|
2104
|
+
try {
|
|
2105
|
+
raw = JSON.parse(await readFile2(filePath, "utf-8"));
|
|
2106
|
+
} catch (err) {
|
|
2107
|
+
warn(`[knowledge-events] corrupted counter baseline at ${filePath}; ignoring folded baseline and replaying live events only: ${err instanceof Error ? err.message : String(err)}`);
|
|
2108
|
+
return new Map;
|
|
2109
|
+
}
|
|
2110
|
+
const map = new Map;
|
|
2111
|
+
for (const [id, rollup] of Object.entries(raw)) {
|
|
2112
|
+
map.set(id, normalizeRollupTimestamps(cloneRollup(rollup)));
|
|
2113
|
+
}
|
|
2114
|
+
return map;
|
|
2115
|
+
}
|
|
2116
|
+
async function writeCounterBaseline(directory, baseline) {
|
|
2117
|
+
const filePath = resolveKnowledgeCounterBaselinePath(directory);
|
|
2118
|
+
const out = {};
|
|
2119
|
+
for (const [id, rollup] of [...baseline.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
2120
|
+
out[id] = normalizeRollupTimestamps(cloneRollup(rollup));
|
|
2121
|
+
}
|
|
2122
|
+
await atomicWriteFile(filePath, `${JSON.stringify(out, null, 2)}
|
|
2123
|
+
`);
|
|
2124
|
+
}
|
|
2125
|
+
async function statCacheKey(filePath) {
|
|
2126
|
+
try {
|
|
2127
|
+
const fileStat = await stat2(filePath);
|
|
2128
|
+
return `${fileStat.mtimeMs}:${fileStat.ctimeMs}:${fileStat.size}`;
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
if (err?.code === "ENOENT")
|
|
2131
|
+
return "missing";
|
|
2132
|
+
throw err;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async function buildCounterRollupCacheKey(directory) {
|
|
2136
|
+
const [eventsKey, legacyKey, baselineKey] = await Promise.all([
|
|
2137
|
+
statCacheKey(resolveKnowledgeEventsPath(directory)),
|
|
2138
|
+
statCacheKey(resolveLegacyApplicationLogPath(directory)),
|
|
2139
|
+
statCacheKey(resolveKnowledgeCounterBaselinePath(directory))
|
|
2140
|
+
]);
|
|
2141
|
+
return `${eventsKey}|${legacyKey}|${baselineKey}`;
|
|
2142
|
+
}
|
|
2143
|
+
function setCounterRollupCache(directory, key, rollups) {
|
|
2144
|
+
if (counterRollupCache.has(directory)) {
|
|
2145
|
+
counterRollupCache.delete(directory);
|
|
2146
|
+
}
|
|
2147
|
+
counterRollupCache.set(directory, {
|
|
2148
|
+
key,
|
|
2149
|
+
rollups: cloneRollupMap(rollups)
|
|
2150
|
+
});
|
|
2151
|
+
while (counterRollupCache.size > MAX_COUNTER_ROLLUP_CACHE_DIRS) {
|
|
2152
|
+
const oldestKey = counterRollupCache.keys().next().value;
|
|
2153
|
+
if (oldestKey === undefined)
|
|
2154
|
+
break;
|
|
2155
|
+
counterRollupCache.delete(oldestKey);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
function parseEventLines(lines, filePath) {
|
|
2159
|
+
const out = [];
|
|
2160
|
+
for (const line of lines) {
|
|
2161
|
+
const trimmed = line.trim();
|
|
2162
|
+
if (!trimmed)
|
|
2163
|
+
continue;
|
|
2164
|
+
try {
|
|
2165
|
+
out.push(JSON.parse(trimmed));
|
|
2166
|
+
} catch {
|
|
2167
|
+
warn(`[knowledge-events] Skipping corrupted JSONL line in ${filePath}: ${trimmed.slice(0, 80)}`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
return out;
|
|
2171
|
+
}
|
|
2172
|
+
async function foldEvictedEventsIntoBaseline(directory, evictedLines, filePath) {
|
|
2173
|
+
const evictedEvents = parseEventLines(evictedLines, filePath);
|
|
2174
|
+
if (evictedEvents.length === 0)
|
|
2175
|
+
return;
|
|
2176
|
+
const baseline = await readCounterBaseline(directory);
|
|
2177
|
+
for (const [id, rollup] of recomputeCounters(evictedEvents)) {
|
|
2178
|
+
mergeRollupInto(get(baseline, id), rollup);
|
|
2179
|
+
}
|
|
2180
|
+
await writeCounterBaseline(directory, baseline);
|
|
2181
|
+
}
|
|
2182
|
+
function recomputeCounters(events, legacyRecords = [], baseline = new Map) {
|
|
2183
|
+
const map = new Map;
|
|
2184
|
+
const retrievedIds = new Set;
|
|
2185
|
+
for (const [id, rollup] of baseline) {
|
|
2186
|
+
map.set(id, cloneRollup(rollup));
|
|
2187
|
+
if ((rollup.shown_count ?? 0) > 0)
|
|
2188
|
+
retrievedIds.add(id);
|
|
2189
|
+
}
|
|
2190
|
+
for (const e of events) {
|
|
2191
|
+
switch (e.type) {
|
|
2192
|
+
case "retrieved": {
|
|
2193
|
+
for (const id of e.result_ids) {
|
|
2194
|
+
retrievedIds.add(id);
|
|
2195
|
+
get(map, id).shown_count += 1;
|
|
2196
|
+
}
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2199
|
+
case "acknowledged": {
|
|
2200
|
+
if (!e.knowledge_id)
|
|
2201
|
+
break;
|
|
2202
|
+
const r = get(map, e.knowledge_id);
|
|
2203
|
+
r.acknowledged_count += 1;
|
|
2204
|
+
r.last_acknowledged_at = maxIso(r.last_acknowledged_at, e.timestamp);
|
|
2205
|
+
break;
|
|
2206
|
+
}
|
|
2207
|
+
case "applied": {
|
|
2208
|
+
if (!e.knowledge_id)
|
|
2209
|
+
break;
|
|
2210
|
+
const r = get(map, e.knowledge_id);
|
|
2211
|
+
r.applied_explicit_count += 1;
|
|
2212
|
+
r.last_applied_at = maxIso(r.last_applied_at, e.timestamp);
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
case "ignored":
|
|
2216
|
+
if (!e.knowledge_id)
|
|
2217
|
+
break;
|
|
2218
|
+
get(map, e.knowledge_id).ignored_count += 1;
|
|
2219
|
+
break;
|
|
2220
|
+
case "violated": {
|
|
2221
|
+
if (!e.knowledge_id)
|
|
2222
|
+
break;
|
|
2223
|
+
const r = get(map, e.knowledge_id);
|
|
2224
|
+
r.violated_count += 1;
|
|
2225
|
+
r.violation_timestamps.push(e.timestamp);
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
case "contradicted":
|
|
2229
|
+
if (!e.knowledge_id)
|
|
2230
|
+
break;
|
|
2231
|
+
get(map, e.knowledge_id).contradicted_count += 1;
|
|
2232
|
+
break;
|
|
2233
|
+
case "n_a":
|
|
2234
|
+
if (!e.knowledge_id)
|
|
2235
|
+
break;
|
|
2236
|
+
get(map, e.knowledge_id).n_a_count += 1;
|
|
2237
|
+
break;
|
|
2238
|
+
case "no_relevant":
|
|
2239
|
+
break;
|
|
2240
|
+
case "unacknowledged":
|
|
2241
|
+
break;
|
|
2242
|
+
case "outcome": {
|
|
2243
|
+
if (!e.knowledge_id)
|
|
2244
|
+
break;
|
|
2245
|
+
const r = get(map, e.knowledge_id);
|
|
2246
|
+
if (e.outcome === "success")
|
|
2247
|
+
r.succeeded_after_shown_count += 1;
|
|
2248
|
+
else if (e.outcome === "failure")
|
|
2249
|
+
r.failed_after_shown_count += 1;
|
|
2250
|
+
else if (e.outcome === "partial")
|
|
2251
|
+
r.partial_after_shown_count += 1;
|
|
2252
|
+
break;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
for (const rec of legacyRecords) {
|
|
2257
|
+
const r = get(map, rec.knowledgeId);
|
|
2258
|
+
switch (rec.result) {
|
|
2259
|
+
case "shown":
|
|
2260
|
+
if (!retrievedIds.has(rec.knowledgeId))
|
|
2261
|
+
r.shown_count += 1;
|
|
2262
|
+
break;
|
|
2263
|
+
case "acknowledged":
|
|
2264
|
+
r.acknowledged_count += 1;
|
|
2265
|
+
r.last_acknowledged_at = maxIso(r.last_acknowledged_at, rec.timestamp);
|
|
2266
|
+
break;
|
|
2267
|
+
case "applied":
|
|
2268
|
+
r.applied_explicit_count += 1;
|
|
2269
|
+
r.last_applied_at = maxIso(r.last_applied_at, rec.timestamp);
|
|
2270
|
+
break;
|
|
2271
|
+
case "ignored":
|
|
2272
|
+
r.ignored_count += 1;
|
|
2273
|
+
break;
|
|
2274
|
+
case "contradicted":
|
|
2275
|
+
r.contradicted_count += 1;
|
|
2276
|
+
break;
|
|
2277
|
+
case "violated":
|
|
2278
|
+
r.violated_count += 1;
|
|
2279
|
+
r.violation_timestamps.push(rec.timestamp);
|
|
2280
|
+
break;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
for (const r of map.values()) {
|
|
2284
|
+
normalizeRollupTimestamps(r);
|
|
2285
|
+
}
|
|
2286
|
+
return map;
|
|
2287
|
+
}
|
|
2288
|
+
function countViolationsInWindow(timestamps, windowDays, now = new Date) {
|
|
2289
|
+
const cutoff = now.getTime() - windowDays * 24 * 60 * 60 * 1000;
|
|
2290
|
+
let count = 0;
|
|
2291
|
+
for (const ts of timestamps) {
|
|
2292
|
+
const t = Date.parse(ts);
|
|
2293
|
+
if (!Number.isNaN(t) && t >= cutoff)
|
|
2294
|
+
count += 1;
|
|
2295
|
+
}
|
|
2296
|
+
return count;
|
|
2297
|
+
}
|
|
2298
|
+
function countEmptyTraceTerminals(events) {
|
|
2299
|
+
let n = 0;
|
|
2300
|
+
for (const e of events) {
|
|
2301
|
+
if (e.type === "no_relevant")
|
|
2302
|
+
n += 1;
|
|
2303
|
+
}
|
|
2304
|
+
return n;
|
|
2305
|
+
}
|
|
2306
|
+
async function countEntryViolationsInWindow(directory, entryId, windowDays, now = new Date) {
|
|
2307
|
+
try {
|
|
2308
|
+
const cutoff = now.getTime() - windowDays * 24 * 60 * 60 * 1000;
|
|
2309
|
+
const [events, legacyRecords] = await Promise.all([
|
|
2310
|
+
readKnowledgeEvents(directory),
|
|
2311
|
+
readLegacyApplicationRecords(directory)
|
|
2312
|
+
]);
|
|
2313
|
+
let count = 0;
|
|
2314
|
+
for (const e of events) {
|
|
2315
|
+
if (e.type !== "violated" || e.knowledge_id !== entryId)
|
|
2316
|
+
continue;
|
|
2317
|
+
const t = Date.parse(e.timestamp);
|
|
2318
|
+
if (!Number.isNaN(t) && t >= cutoff)
|
|
2319
|
+
count += 1;
|
|
2320
|
+
}
|
|
2321
|
+
for (const rec of legacyRecords) {
|
|
2322
|
+
if (rec.result !== "violated" || rec.knowledgeId !== entryId)
|
|
2323
|
+
continue;
|
|
2324
|
+
const t = Date.parse(rec.timestamp);
|
|
2325
|
+
if (!Number.isNaN(t) && t >= cutoff)
|
|
2326
|
+
count += 1;
|
|
2327
|
+
}
|
|
2328
|
+
return count;
|
|
2329
|
+
} catch {
|
|
2330
|
+
return 0;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
async function countEntryContradictionsInWindow(directory, entryId, windowDays, now = new Date) {
|
|
2334
|
+
try {
|
|
2335
|
+
const cutoff = now.getTime() - windowDays * 24 * 60 * 60 * 1000;
|
|
2336
|
+
const events = await readKnowledgeEvents(directory);
|
|
2337
|
+
let count = 0;
|
|
2338
|
+
for (const e of events) {
|
|
2339
|
+
if (e.type !== "contradicted" || e.knowledge_id !== entryId)
|
|
2340
|
+
continue;
|
|
2341
|
+
const t = Date.parse(e.timestamp);
|
|
2342
|
+
if (!Number.isNaN(t) && t >= cutoff)
|
|
2343
|
+
count += 1;
|
|
2344
|
+
}
|
|
2345
|
+
return count;
|
|
2346
|
+
} catch {
|
|
2347
|
+
return 0;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
async function readKnowledgeCounterRollups(directory) {
|
|
2351
|
+
try {
|
|
2352
|
+
const cacheKey = await buildCounterRollupCacheKey(directory);
|
|
2353
|
+
const cached = counterRollupCache.get(directory);
|
|
2354
|
+
if (cached?.key === cacheKey) {
|
|
2355
|
+
counterRollupCache.delete(directory);
|
|
2356
|
+
counterRollupCache.set(directory, cached);
|
|
2357
|
+
return cloneRollupMap(cached.rollups);
|
|
2358
|
+
}
|
|
2359
|
+
const [events, legacyRecords, baseline] = await Promise.all([
|
|
2360
|
+
readKnowledgeEvents(directory),
|
|
2361
|
+
readLegacyApplicationRecords(directory),
|
|
2362
|
+
readCounterBaseline(directory)
|
|
2363
|
+
]);
|
|
2364
|
+
const rollups = recomputeCounters(events, legacyRecords, baseline);
|
|
2365
|
+
setCounterRollupCache(directory, cacheKey, rollups);
|
|
2366
|
+
return cloneRollupMap(rollups);
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
warn(`[knowledge-events] readKnowledgeCounterRollups failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2369
|
+
return new Map;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async function readAuthoritativeKnowledgeCounterRollups(directory) {
|
|
2373
|
+
const history = await queryHistoricalOutcomes(directory);
|
|
2374
|
+
if (!history.ok)
|
|
2375
|
+
return new Map;
|
|
2376
|
+
const rollups = new Map;
|
|
2377
|
+
for (const membership of history.memberships) {
|
|
2378
|
+
const rollup = get(rollups, membership.entry_id);
|
|
2379
|
+
rollup.shown_count += 1;
|
|
2380
|
+
if (membership.application_marker) {
|
|
2381
|
+
rollup.acknowledged_count += 1;
|
|
2382
|
+
rollup.last_acknowledged_at = maxIso(rollup.last_acknowledged_at, membership.application_marker.committed_at);
|
|
2383
|
+
}
|
|
2384
|
+
const compatible = membership;
|
|
2385
|
+
const seen = new Set;
|
|
2386
|
+
for (const terminal of [
|
|
2387
|
+
...compatible.terminal_history ?? [],
|
|
2388
|
+
...compatible.historical_terminals ?? [],
|
|
2389
|
+
...membership.terminal ? [membership.terminal] : []
|
|
2390
|
+
]) {
|
|
2391
|
+
if (seen.has(terminal.event_id))
|
|
2392
|
+
continue;
|
|
2393
|
+
seen.add(terminal.event_id);
|
|
2394
|
+
switch (terminal.outcome) {
|
|
2395
|
+
case "applied":
|
|
2396
|
+
rollup.applied_explicit_count += 1;
|
|
2397
|
+
rollup.last_applied_at = maxIso(rollup.last_applied_at, terminal.committed_at);
|
|
2398
|
+
break;
|
|
2399
|
+
case "ignored":
|
|
2400
|
+
rollup.ignored_count += 1;
|
|
2401
|
+
break;
|
|
2402
|
+
case "violated":
|
|
2403
|
+
rollup.violated_count += 1;
|
|
2404
|
+
rollup.violation_timestamps.push(terminal.committed_at);
|
|
2405
|
+
break;
|
|
2406
|
+
case "contradicted":
|
|
2407
|
+
rollup.contradicted_count += 1;
|
|
2408
|
+
break;
|
|
2409
|
+
case "n_a":
|
|
2410
|
+
rollup.n_a_count += 1;
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
normalizeRollupTimestamps(rollup);
|
|
2415
|
+
}
|
|
2416
|
+
return rollups;
|
|
2417
|
+
}
|
|
2418
|
+
function effectiveRetrievalOutcomes(stored, rollup) {
|
|
2419
|
+
const base = stored ?? {
|
|
2420
|
+
applied_count: 0,
|
|
2421
|
+
succeeded_after_count: 0,
|
|
2422
|
+
failed_after_count: 0
|
|
2423
|
+
};
|
|
2424
|
+
if (!rollup)
|
|
2425
|
+
return base;
|
|
2426
|
+
const { n_a_count: _na, ...rollupWithoutNa } = rollup;
|
|
2427
|
+
return {
|
|
2428
|
+
...base,
|
|
2429
|
+
...rollupWithoutNa,
|
|
2430
|
+
succeeded_after_shown_count: (base.succeeded_after_shown_count ?? 0) + (rollup.succeeded_after_shown_count ?? 0),
|
|
2431
|
+
failed_after_shown_count: (base.failed_after_shown_count ?? 0) + (rollup.failed_after_shown_count ?? 0)
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
var VERDICT_CONFIDENCE_BOOST = 0.03;
|
|
2435
|
+
var VERDICT_CONFIDENCE_DECAY = 0.05;
|
|
2436
|
+
async function applyKnowledgeVerdictFeedback(directory, options) {
|
|
2437
|
+
try {
|
|
2438
|
+
const history = await _internals4.queryHistoricalOutcomes(directory);
|
|
2439
|
+
if (!history.ok)
|
|
2440
|
+
return { processed: 0, bumps: 0 };
|
|
2441
|
+
const seenEventIds = new Set;
|
|
2442
|
+
const events = history.memberships.flatMap((membership) => {
|
|
2443
|
+
const compatible = membership;
|
|
2444
|
+
const terminals = [
|
|
2445
|
+
...compatible.terminal_history ?? [],
|
|
2446
|
+
...compatible.historical_terminals ?? [],
|
|
2447
|
+
...membership.terminal ? [membership.terminal] : []
|
|
2448
|
+
];
|
|
2449
|
+
return terminals.flatMap((terminal) => {
|
|
2450
|
+
if (terminal.outcome !== "applied" && terminal.outcome !== "violated" && terminal.outcome !== "ignored" || seenEventIds.has(terminal.event_id)) {
|
|
2451
|
+
return [];
|
|
2452
|
+
}
|
|
2453
|
+
seenEventIds.add(terminal.event_id);
|
|
2454
|
+
return [
|
|
2455
|
+
{
|
|
2456
|
+
type: terminal.outcome,
|
|
2457
|
+
event_id: terminal.event_id,
|
|
2458
|
+
knowledge_id: membership.entry_id,
|
|
2459
|
+
timestamp: terminal.committed_at
|
|
2460
|
+
}
|
|
2461
|
+
];
|
|
2462
|
+
});
|
|
2463
|
+
}).sort((a, b) => a.timestamp === b.timestamp ? a.event_id.localeCompare(b.event_id) : a.timestamp.localeCompare(b.timestamp));
|
|
2464
|
+
const markerIndex = options?.sinceTimestamp && options.sinceEventId ? events.findIndex((e) => e.timestamp === options.sinceTimestamp && e.event_id === options.sinceEventId) : -1;
|
|
2465
|
+
const actionable = events.filter((e, index) => {
|
|
2466
|
+
if (options?.sinceTimestamp && e.timestamp < options.sinceTimestamp) {
|
|
2467
|
+
return false;
|
|
2468
|
+
}
|
|
2469
|
+
if (options?.sinceTimestamp && e.timestamp === options.sinceTimestamp) {
|
|
2470
|
+
if (!options.sinceEventId) {
|
|
2471
|
+
return false;
|
|
2472
|
+
}
|
|
2473
|
+
if (markerIndex < 0) {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
return index > markerIndex;
|
|
2477
|
+
}
|
|
2478
|
+
return true;
|
|
2479
|
+
});
|
|
2480
|
+
if (actionable.length === 0) {
|
|
2481
|
+
return { processed: 0, bumps: 0 };
|
|
2482
|
+
}
|
|
2483
|
+
const groups = new Map;
|
|
2484
|
+
let lastProcessedTimestamp;
|
|
2485
|
+
let lastProcessedEventId;
|
|
2486
|
+
for (const event of actionable) {
|
|
2487
|
+
if (typeof event.timestamp === "string" && (!lastProcessedTimestamp || event.timestamp >= lastProcessedTimestamp)) {
|
|
2488
|
+
lastProcessedTimestamp = event.timestamp;
|
|
2489
|
+
lastProcessedEventId = event.event_id;
|
|
2490
|
+
}
|
|
2491
|
+
const kid = event.knowledge_id;
|
|
2492
|
+
if (!kid)
|
|
2493
|
+
continue;
|
|
2494
|
+
const group = groups.get(kid) ?? [];
|
|
2495
|
+
group.push(event);
|
|
2496
|
+
groups.set(kid, group);
|
|
2497
|
+
}
|
|
2498
|
+
const deltas = [];
|
|
2499
|
+
for (const [id, group] of groups) {
|
|
2500
|
+
const positives = group.filter((event) => event.type === "applied").length;
|
|
2501
|
+
const negatives = group.length - positives;
|
|
2502
|
+
if (positives === 0 && negatives === 0)
|
|
2503
|
+
continue;
|
|
2504
|
+
const delta = positives > negatives ? VERDICT_CONFIDENCE_BOOST : -VERDICT_CONFIDENCE_DECAY;
|
|
2505
|
+
deltas.push({
|
|
2506
|
+
id,
|
|
2507
|
+
delta,
|
|
2508
|
+
receipt_events: group.map((event) => ({
|
|
2509
|
+
event_id: event.event_id,
|
|
2510
|
+
timestamp: event.timestamp,
|
|
2511
|
+
outcome: event.type
|
|
2512
|
+
}))
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
let bumps = 0;
|
|
2516
|
+
if (deltas.length > 0) {
|
|
2517
|
+
const { bumpKnowledgeConfidenceBatch } = await import("./knowledge-store-dpqgvvjt.js");
|
|
2518
|
+
bumps = await bumpKnowledgeConfidenceBatch(directory, deltas, options?.floorOptions);
|
|
2519
|
+
}
|
|
2520
|
+
return {
|
|
2521
|
+
processed: groups.size,
|
|
2522
|
+
bumps,
|
|
2523
|
+
lastProcessedTimestamp,
|
|
2524
|
+
lastProcessedEventId
|
|
2525
|
+
};
|
|
2526
|
+
} catch (err) {
|
|
2527
|
+
warn(`[knowledge-events] applyKnowledgeVerdictFeedback failed (fail-open): ${err instanceof Error ? err.message : String(err)}`);
|
|
2528
|
+
return { processed: 0, bumps: 0 };
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
var _internals4 = {
|
|
2532
|
+
resolveKnowledgeEventsPath,
|
|
2533
|
+
resolveKnowledgeCounterBaselinePath,
|
|
2534
|
+
appendKnowledgeEvent,
|
|
2535
|
+
recordKnowledgeEvent,
|
|
2536
|
+
readKnowledgeEvents,
|
|
2537
|
+
queryHistoricalOutcomes,
|
|
2538
|
+
readCounterBaseline,
|
|
2539
|
+
readLegacyApplicationRecords,
|
|
2540
|
+
readKnowledgeCounterRollups,
|
|
2541
|
+
readAuthoritativeKnowledgeCounterRollups,
|
|
2542
|
+
effectiveRetrievalOutcomes,
|
|
2543
|
+
recomputeCounters,
|
|
2544
|
+
applyKnowledgeVerdictFeedback,
|
|
2545
|
+
newTraceId,
|
|
2546
|
+
newEventId,
|
|
2547
|
+
resolveHiveEventsPath: resolveHiveEventsPath2,
|
|
2548
|
+
appendHiveKnowledgeEvent,
|
|
2549
|
+
recordHiveKnowledgeEvent,
|
|
2550
|
+
readHiveKnowledgeEvents,
|
|
2551
|
+
mergeRollupInto
|
|
2552
|
+
};
|
|
2553
|
+
|
|
2554
|
+
export { queryHistoricalOutcomes, recordPhaseCloseIntent, reconcilePhaseClose, KNOWLEDGE_EVENT_SCHEMA_VERSION, MAX_EVENT_LOG_ENTRIES, RECEIPT_EVENT_TYPES, resolveKnowledgeEventsPath, resolveKnowledgeCounterBaselinePath, resolveHiveEventsPath2 as resolveHiveEventsPath, resolveLegacyApplicationLogPath, newTraceId, newEventId, appendKnowledgeEvent, appendKnowledgeEventsBatch, recordKnowledgeEvent, recordKnowledgeEventsBatch, appendHiveKnowledgeEvent, recordHiveKnowledgeEvent, readKnowledgeEvents, readHiveKnowledgeEvents, readLegacyApplicationRecords, MAX_VIOLATION_TIMESTAMPS, recomputeCounters, countViolationsInWindow, countEmptyTraceTerminals, countEntryViolationsInWindow, countEntryContradictionsInWindow, readKnowledgeCounterRollups, readAuthoritativeKnowledgeCounterRollups, effectiveRetrievalOutcomes, applyKnowledgeVerdictFeedback, _internals4 as _internals };
|