@markstanden/opencode-test-container 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +263 -2
- package/container/Containerfile +24 -0
- package/container/README.md +29 -0
- package/dist/adapters/opencode/events.d.ts +90 -0
- package/dist/adapters/opencode/events.js +213 -0
- package/dist/adapters/opencode/runtime.d.ts +87 -0
- package/dist/adapters/opencode/runtime.js +136 -0
- package/dist/adapters/podman/container.d.ts +62 -0
- package/dist/adapters/podman/container.js +119 -0
- package/dist/adapters/podman/image.d.ts +50 -0
- package/dist/adapters/podman/image.js +124 -0
- package/dist/adapters/process.d.ts +38 -0
- package/dist/adapters/process.js +71 -0
- package/dist/campaign/assess.d.ts +45 -0
- package/dist/campaign/assess.js +208 -0
- package/dist/campaign/close.d.ts +17 -0
- package/dist/campaign/close.js +112 -0
- package/dist/campaign/evidence-guard.d.ts +24 -0
- package/dist/campaign/evidence-guard.js +49 -0
- package/dist/campaign/freeze.d.ts +94 -0
- package/dist/campaign/freeze.js +297 -0
- package/dist/campaign/inventory.d.ts +62 -0
- package/dist/campaign/inventory.js +192 -0
- package/dist/campaign/judge.d.ts +209 -0
- package/dist/campaign/judge.js +1285 -0
- package/dist/campaign/ledger.d.ts +64 -0
- package/dist/campaign/ledger.js +149 -0
- package/dist/campaign/prepare.d.ts +129 -0
- package/dist/campaign/prepare.js +483 -0
- package/dist/campaign/primary.d.ts +96 -0
- package/dist/campaign/primary.js +189 -0
- package/dist/campaign/run.d.ts +151 -0
- package/dist/campaign/run.js +562 -0
- package/dist/campaign/status.d.ts +76 -0
- package/dist/campaign/status.js +237 -0
- package/dist/cli/otc.d.ts +51 -0
- package/dist/cli/otc.js +435 -0
- package/dist/contracts/canonical.d.ts +21 -0
- package/dist/contracts/canonical.js +44 -0
- package/dist/contracts/types.d.ts +261 -0
- package/dist/contracts/types.js +9 -0
- package/dist/contracts/validate.d.ts +72 -0
- package/dist/contracts/validate.js +1021 -0
- package/dist/contracts/version.d.ts +44 -0
- package/dist/contracts/version.js +147 -0
- package/dist/evaluators/docs/analysis/accounting.d.ts +140 -0
- package/dist/evaluators/docs/analysis/accounting.js +237 -0
- package/dist/evaluators/docs/analysis/blinding.d.ts +40 -0
- package/dist/evaluators/docs/analysis/blinding.js +80 -0
- package/dist/evaluators/docs/analysis/bounds.d.ts +40 -0
- package/dist/evaluators/docs/analysis/bounds.js +31 -0
- package/dist/evaluators/docs/analysis/budget.d.ts +137 -0
- package/dist/evaluators/docs/analysis/budget.js +305 -0
- package/dist/evaluators/docs/analysis/closure.d.ts +175 -0
- package/dist/evaluators/docs/analysis/closure.js +251 -0
- package/dist/evaluators/docs/analysis/decision.d.ts +59 -0
- package/dist/evaluators/docs/analysis/decision.js +161 -0
- package/dist/evaluators/docs/analysis/freeze.d.ts +201 -0
- package/dist/evaluators/docs/analysis/freeze.js +395 -0
- package/dist/evaluators/docs/analysis/guardrails.d.ts +51 -0
- package/dist/evaluators/docs/analysis/guardrails.js +194 -0
- package/dist/evaluators/docs/analysis/journal.d.ts +150 -0
- package/dist/evaluators/docs/analysis/journal.js +475 -0
- package/dist/evaluators/docs/analysis/ledger.d.ts +78 -0
- package/dist/evaluators/docs/analysis/ledger.js +145 -0
- package/dist/evaluators/docs/analysis/measures.d.ts +302 -0
- package/dist/evaluators/docs/analysis/measures.js +1069 -0
- package/dist/evaluators/docs/analysis/model.d.ts +236 -0
- package/dist/evaluators/docs/analysis/model.js +740 -0
- package/dist/evaluators/docs/analysis/pairing.d.ts +68 -0
- package/dist/evaluators/docs/analysis/pairing.js +95 -0
- package/dist/evaluators/docs/analysis/records.d.ts +229 -0
- package/dist/evaluators/docs/analysis/records.js +935 -0
- package/dist/evaluators/docs/analysis/recovery.d.ts +225 -0
- package/dist/evaluators/docs/analysis/recovery.js +507 -0
- package/dist/evaluators/docs/analysis/render.d.ts +235 -0
- package/dist/evaluators/docs/analysis/render.js +566 -0
- package/dist/evaluators/docs/analysis/replay.d.ts +15 -0
- package/dist/evaluators/docs/analysis/replay.js +1272 -0
- package/dist/evaluators/docs/analysis/screening/inventory.d.ts +136 -0
- package/dist/evaluators/docs/analysis/screening/inventory.js +307 -0
- package/dist/evaluators/docs/analysis/screening/noteVersion.d.ts +15 -0
- package/dist/evaluators/docs/analysis/screening/noteVersion.js +46 -0
- package/dist/evaluators/docs/analysis/screening/reconcile.d.ts +38 -0
- package/dist/evaluators/docs/analysis/screening/reconcile.js +40 -0
- package/dist/evaluators/docs/analysis/screening/rules.d.ts +163 -0
- package/dist/evaluators/docs/analysis/screening/rules.js +330 -0
- package/dist/evaluators/docs/analysis/screening/scan.d.ts +72 -0
- package/dist/evaluators/docs/analysis/screening/scan.js +362 -0
- package/dist/evaluators/docs/analysis/selector.d.ts +68 -0
- package/dist/evaluators/docs/analysis/selector.js +123 -0
- package/dist/evaluators/docs/assess.d.ts +129 -0
- package/dist/evaluators/docs/assess.js +433 -0
- package/dist/evaluators/docs/campaign.d.ts +133 -0
- package/dist/evaluators/docs/campaign.js +222 -0
- package/dist/evaluators/docs/index.d.ts +25 -0
- package/dist/evaluators/docs/index.js +25 -0
- package/dist/evaluators/docs/journal.d.ts +150 -0
- package/dist/evaluators/docs/journal.js +475 -0
- package/dist/evaluators/docs/scoring.d.ts +367 -0
- package/dist/evaluators/docs/scoring.js +662 -0
- package/dist/evaluators/skills/assess.d.ts +94 -0
- package/dist/evaluators/skills/assess.js +207 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +29 -0
- package/dist/legacy/prod014.d.ts +86 -0
- package/dist/legacy/prod014.js +209 -0
- package/dist/reporting/report.d.ts +79 -0
- package/dist/reporting/report.js +242 -0
- package/docs/architecture.md +106 -0
- package/docs/capability-matrix.md +84 -0
- package/docs/offline-rehearsal.md +75 -0
- package/docs/parity-record.md +81 -0
- package/docs/pr-description.md +103 -0
- package/docs/schema.md +97 -0
- package/examples/skills/README.md +13 -0
- package/examples/skills/campaign.json +73 -0
- package/examples/skills/skills/otel-helper/SKILL.md +10 -0
- package/examples/skills/suite.json +29 -0
- package/package.json +45 -5
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence loading for the trial analysis framework.
|
|
3
|
+
*
|
|
4
|
+
* Every record is read through an injected {@link EvidenceReader} and lands
|
|
5
|
+
* in one of four explicit states: available, missing, malformed or
|
|
6
|
+
* unsupported-loaded. Absence is never conflated with emptiness — a missing
|
|
7
|
+
* snapshot is not an empty lake, and an absent judgement is not "none". The
|
|
8
|
+
* ac-v1 adapter reads the layout the 2026-11-10 campaigns recorded (per-arm
|
|
9
|
+
* result roots holding run metadata, gate and primary-check records, judge
|
|
10
|
+
* attempt directories and trial artefacts). A legacy record without the
|
|
11
|
+
* fields it needs stays limited or unknown; it is never silently upgraded.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here touches the filesystem or calls a model: the CLI supplies the
|
|
14
|
+
* reader, and tests supply an in-memory one.
|
|
15
|
+
*/
|
|
16
|
+
import { DOCUMENTATION_OUTCOMES, } from "../scoring.js";
|
|
17
|
+
import { parseCampaignLedger } from "./ledger.js";
|
|
18
|
+
import { selectionProblems, } from "./recovery.js";
|
|
19
|
+
import { answerIdentityProblem, hostDispatchesFromTrail, journalIdentityFromTrail, verifyOperationsJournal, } from "./journal.js";
|
|
20
|
+
/** Wrap a value as available. */
|
|
21
|
+
function loaded(value, source) {
|
|
22
|
+
return { state: "available", value, source };
|
|
23
|
+
}
|
|
24
|
+
/** Join two path segments with a slash. */
|
|
25
|
+
function joinPath(left, right) {
|
|
26
|
+
return `${left}/${right}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read and parse one JSON file, keeping missing and malformed explicit.
|
|
30
|
+
* @param reader The evidence reader.
|
|
31
|
+
* @param path The file path relative to the evidence root.
|
|
32
|
+
* @returns The parsed value, or the explicit failure state.
|
|
33
|
+
*/
|
|
34
|
+
function loadJson(reader, path) {
|
|
35
|
+
const text = reader.readTextFile(path);
|
|
36
|
+
if (text.state !== "available")
|
|
37
|
+
return text;
|
|
38
|
+
try {
|
|
39
|
+
return loaded(JSON.parse(text.value), path);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
state: "malformed",
|
|
44
|
+
source: path,
|
|
45
|
+
problems: [`invalid JSON (${String(error)})`],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read one text file as its nonblank lines.
|
|
51
|
+
* @param reader The evidence reader.
|
|
52
|
+
* @param path The file path relative to the evidence root.
|
|
53
|
+
* @returns The lines, or the explicit failure state.
|
|
54
|
+
*/
|
|
55
|
+
function loadLines(reader, path) {
|
|
56
|
+
const text = reader.readTextFile(path);
|
|
57
|
+
if (text.state !== "available")
|
|
58
|
+
return text;
|
|
59
|
+
return loaded(text.value.split("\n").filter((line) => line.trim() !== ""), path);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Walk a directory tree collecting Markdown documents, path-sorted. A
|
|
63
|
+
* non-Markdown name is inspected as a possible directory; a plain file such as
|
|
64
|
+
* `README.txt` answers `not-a-directory` and is skipped, never treated as an
|
|
65
|
+
* empty directory.
|
|
66
|
+
* @param reader The evidence reader.
|
|
67
|
+
* @param dir The directory to walk, relative to the evidence root.
|
|
68
|
+
* @param prefix The display prefix already accumulated.
|
|
69
|
+
* @param docs The accumulated documents.
|
|
70
|
+
* @param problems Accumulated problems for entries that cannot be read.
|
|
71
|
+
*/
|
|
72
|
+
function walkMarkdown(reader, dir, prefix, docs, problems) {
|
|
73
|
+
const entries = reader.readDirectoryEntries(dir);
|
|
74
|
+
// A file is not a lake subtree: skip it. Any other failure stays a problem.
|
|
75
|
+
if (entries.state === "not-a-directory")
|
|
76
|
+
return;
|
|
77
|
+
if (entries.state !== "available") {
|
|
78
|
+
problems.push(`${dir}: directory unreadable (${entries.state})`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const name of entries.value) {
|
|
82
|
+
if (name.startsWith("."))
|
|
83
|
+
continue;
|
|
84
|
+
const child = joinPath(dir, name);
|
|
85
|
+
if (!name.endsWith(".md")) {
|
|
86
|
+
walkMarkdown(reader, child, `${prefix}${name}/`, docs, problems);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const file = reader.readTextFile(child);
|
|
90
|
+
if (file.state === "available") {
|
|
91
|
+
docs.push({ path: `${prefix}${name}`, contents: file.value });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
problems.push(`${prefix}${name}: unreadable (${file.state})`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read the Markdown documents under one directory, recursively and sorted.
|
|
100
|
+
* A missing directory stays missing; an existing directory with no Markdown
|
|
101
|
+
* documents is available and empty — absence is never conflated with
|
|
102
|
+
* emptiness.
|
|
103
|
+
* @param reader The evidence reader.
|
|
104
|
+
* @param dir The directory, relative to the evidence root.
|
|
105
|
+
* @returns The documents sorted by path, or the explicit state.
|
|
106
|
+
*/
|
|
107
|
+
export function readMarkdownTree(reader, dir) {
|
|
108
|
+
const entries = reader.readDirectoryEntries(dir);
|
|
109
|
+
if (entries.state !== "available")
|
|
110
|
+
return entries;
|
|
111
|
+
const docs = [];
|
|
112
|
+
const problems = [];
|
|
113
|
+
walkMarkdown(reader, dir, "", docs, problems);
|
|
114
|
+
if (problems.length > 0) {
|
|
115
|
+
return { state: "malformed", source: dir, problems };
|
|
116
|
+
}
|
|
117
|
+
docs.sort((left, right) => left.path.localeCompare(right.path));
|
|
118
|
+
return loaded(docs, dir);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Derive what one trial directory proves about the run: a recorded run needs
|
|
122
|
+
* a run record; a bare reservation and an unrecorded directory prove nothing.
|
|
123
|
+
* @param reader The evidence reader.
|
|
124
|
+
* @param dir The trial directory, relative to the evidence root.
|
|
125
|
+
* @returns The attempt state and any problems.
|
|
126
|
+
*/
|
|
127
|
+
function attemptOf(reader, dir) {
|
|
128
|
+
const entries = reader.readDirectoryEntries(dir);
|
|
129
|
+
if (entries.state === "missing")
|
|
130
|
+
return { state: "absent", problems: [] };
|
|
131
|
+
if (entries.state !== "available") {
|
|
132
|
+
return {
|
|
133
|
+
state: "unknown",
|
|
134
|
+
problems: [`directory entries unreadable (${entries.state})`],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const recorded = entries.value.filter((name) => name !== ".reservation");
|
|
138
|
+
const hasReservation = entries.value.includes(".reservation");
|
|
139
|
+
if (recorded.length === 0 && hasReservation) {
|
|
140
|
+
return { state: "reservation-only", problems: [] };
|
|
141
|
+
}
|
|
142
|
+
if (recorded.length === 0)
|
|
143
|
+
return { state: "unknown", problems: [] };
|
|
144
|
+
if (!recorded.includes("meta.json")) {
|
|
145
|
+
return {
|
|
146
|
+
state: "unknown",
|
|
147
|
+
problems: ["no run record (meta.json) in the trial directory"],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { state: "recorded", problems: [] };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Derive usability from the recorded run metadata, keeping every failing
|
|
154
|
+
* condition visible. A record without an answer-validity field stays limited
|
|
155
|
+
* rather than being upgraded.
|
|
156
|
+
* @param run The loaded run metadata.
|
|
157
|
+
* @returns Usability and the visible reasons.
|
|
158
|
+
*/
|
|
159
|
+
function usableOf(run) {
|
|
160
|
+
if (run.state === "missing") {
|
|
161
|
+
return {
|
|
162
|
+
usable: "unknown",
|
|
163
|
+
problems: ["no run record to assess usability"],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (run.state !== "available") {
|
|
167
|
+
return { usable: false, problems: ["run record unreadable"] };
|
|
168
|
+
}
|
|
169
|
+
const problems = [];
|
|
170
|
+
const meta = run.value;
|
|
171
|
+
if (meta.exit !== 0)
|
|
172
|
+
problems.push(`exit is ${String(meta.exit)}`);
|
|
173
|
+
if (meta.signal !== null && meta.signal !== undefined) {
|
|
174
|
+
problems.push(`signal is ${String(meta.signal)}`);
|
|
175
|
+
}
|
|
176
|
+
if (meta.timedOut === true)
|
|
177
|
+
problems.push("timedOut");
|
|
178
|
+
if (meta.outputOverflow === true)
|
|
179
|
+
problems.push("outputOverflow");
|
|
180
|
+
if (meta.answerValid !== true) {
|
|
181
|
+
problems.push("no answerValid record (limited evidence, never upgraded)");
|
|
182
|
+
}
|
|
183
|
+
return { usable: problems.length === 0, problems };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Load the run metadata record.
|
|
187
|
+
* @param reader The evidence reader.
|
|
188
|
+
* @param dir The trial directory, relative to the evidence root.
|
|
189
|
+
* @returns The metadata, or the explicit state.
|
|
190
|
+
*/
|
|
191
|
+
function loadRun(reader, dir) {
|
|
192
|
+
return loadJsonObject(reader, joinPath(dir, "meta.json"), "run record");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Require a loaded JSON value to be an object, remapping other states.
|
|
196
|
+
* @param reader The evidence reader.
|
|
197
|
+
* @param path The file path relative to the evidence root.
|
|
198
|
+
* @param label The display label for shape problems.
|
|
199
|
+
* @returns The object, or the explicit failure state.
|
|
200
|
+
*/
|
|
201
|
+
function loadJsonObject(reader, path, label) {
|
|
202
|
+
const parsed = loadJson(reader, path);
|
|
203
|
+
if (parsed.state === "available") {
|
|
204
|
+
return isRecord(parsed.value)
|
|
205
|
+
? loaded(parsed.value, parsed.source)
|
|
206
|
+
: {
|
|
207
|
+
state: "malformed",
|
|
208
|
+
source: parsed.source,
|
|
209
|
+
problems: [`${label} is not a JSON object`],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (parsed.state === "malformed") {
|
|
213
|
+
return {
|
|
214
|
+
state: "malformed",
|
|
215
|
+
source: parsed.source,
|
|
216
|
+
problems: parsed.problems,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (parsed.state === "unsupported-loaded") {
|
|
220
|
+
return {
|
|
221
|
+
state: "unsupported-loaded",
|
|
222
|
+
source: parsed.source,
|
|
223
|
+
reason: parsed.reason,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { state: "missing", source: parsed.source };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Load the recorded gate verdict.
|
|
230
|
+
* @param reader The evidence reader.
|
|
231
|
+
* @param dir The trial directory, relative to the evidence root.
|
|
232
|
+
* @returns The gate record, or the explicit state.
|
|
233
|
+
*/
|
|
234
|
+
function loadGate(reader, dir) {
|
|
235
|
+
const parsed = loadJsonObject(reader, joinPath(dir, "gate.json"), "gate record");
|
|
236
|
+
if (parsed.state === "missing")
|
|
237
|
+
return parsed;
|
|
238
|
+
if (parsed.state !== "available")
|
|
239
|
+
return parsed;
|
|
240
|
+
if (typeof parsed.value.status !== "string") {
|
|
241
|
+
return {
|
|
242
|
+
state: "malformed",
|
|
243
|
+
source: parsed.source,
|
|
244
|
+
problems: ["gate record has no status string"],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const reason = parsed.value.reason;
|
|
248
|
+
return loaded({
|
|
249
|
+
status: parsed.value.status,
|
|
250
|
+
reason: typeof reason === "string" ? reason : null,
|
|
251
|
+
}, parsed.source);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Categorise the primary-task record: only an explicit pass or fail counts;
|
|
255
|
+
* missing, malformed and other statuses stay unknown.
|
|
256
|
+
* @param parsed The loaded primary-check record.
|
|
257
|
+
* @returns The primary category and any loading problems.
|
|
258
|
+
*/
|
|
259
|
+
function primaryOf(parsed) {
|
|
260
|
+
if (parsed.state === "missing")
|
|
261
|
+
return { status: "unknown", problems: [] };
|
|
262
|
+
if (parsed.state === "not-a-directory") {
|
|
263
|
+
return {
|
|
264
|
+
status: "unknown",
|
|
265
|
+
problems: ["primary record is a directory, not a file"],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (parsed.state !== "available") {
|
|
269
|
+
return {
|
|
270
|
+
status: "unknown",
|
|
271
|
+
problems: parsed.state === "malformed"
|
|
272
|
+
? parsed.problems
|
|
273
|
+
: [parsed.reason],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (!isRecord(parsed.value)) {
|
|
277
|
+
return {
|
|
278
|
+
status: "unknown",
|
|
279
|
+
problems: ["primary record is not an object"],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const status = parsed.value.status;
|
|
283
|
+
if (status === "pass")
|
|
284
|
+
return { status: "pass", problems: [] };
|
|
285
|
+
if (status === "fail")
|
|
286
|
+
return { status: "fail", problems: [] };
|
|
287
|
+
return {
|
|
288
|
+
status: "unknown",
|
|
289
|
+
problems: [`unsupported primary status ${JSON.stringify(status)}`],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Look for a judged deterministic resolution beside the raw judgement.
|
|
294
|
+
* @param reader The evidence reader.
|
|
295
|
+
* @param attemptDir The attempt directory, relative to the evidence root.
|
|
296
|
+
* @returns The lookup outcome.
|
|
297
|
+
*/
|
|
298
|
+
function judgedResolutionOf(reader, attemptDir) {
|
|
299
|
+
const resolution = loadJson(reader, joinPath(attemptDir, "resolution.json"));
|
|
300
|
+
if (resolution.state === "missing")
|
|
301
|
+
return { state: "absent" };
|
|
302
|
+
if (resolution.state !== "available")
|
|
303
|
+
return { state: "malformed" };
|
|
304
|
+
if (!isRecord(resolution.value))
|
|
305
|
+
return { state: "malformed" };
|
|
306
|
+
if (resolution.value.status === "judged" &&
|
|
307
|
+
isRecord(resolution.value.parsed)) {
|
|
308
|
+
return {
|
|
309
|
+
state: "selected",
|
|
310
|
+
value: {
|
|
311
|
+
value: resolution.value.parsed,
|
|
312
|
+
layer: "deterministic-resolution",
|
|
313
|
+
status: "judged",
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return { state: "unresolved" };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Read the raw judgement as the fallback layer.
|
|
321
|
+
* @param reader The evidence reader.
|
|
322
|
+
* @param attemptDir The attempt directory, relative to the evidence root.
|
|
323
|
+
* @param problems Problem collector.
|
|
324
|
+
* @returns The judgement value, or undefined.
|
|
325
|
+
*/
|
|
326
|
+
function rawJudgement(reader, attemptDir, problems) {
|
|
327
|
+
const judgement = loadJson(reader, joinPath(attemptDir, "judgement.json"));
|
|
328
|
+
if (judgement.state !== "available") {
|
|
329
|
+
if (judgement.state === "malformed")
|
|
330
|
+
problems.push(...judgement.problems);
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
if (!isRecord(judgement.value)) {
|
|
334
|
+
problems.push("judgement is not a JSON object");
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
if (typeof judgement.value.status !== "string") {
|
|
338
|
+
problems.push("judgement has no status string");
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
value: judgement.value,
|
|
343
|
+
layer: "judge",
|
|
344
|
+
status: judgement.value.status,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Select the judgement layer per the recorded policy. A judged deterministic
|
|
349
|
+
* resolution wins only under resolution-then-judgement; the raw judgement
|
|
350
|
+
* governs under judgement-only, whatever the resolution says.
|
|
351
|
+
* @param reader The evidence reader.
|
|
352
|
+
* @param attemptDir The attempt directory, relative to the evidence root.
|
|
353
|
+
* @param selectionPolicy The recorded selection policy.
|
|
354
|
+
* @param problems Problem collector.
|
|
355
|
+
* @returns The selected judgement value, or undefined.
|
|
356
|
+
*/
|
|
357
|
+
function selectJudgement(reader, attemptDir, selectionPolicy, problems) {
|
|
358
|
+
if (selectionPolicy !== "resolution-then-judgement") {
|
|
359
|
+
return rawJudgement(reader, attemptDir, problems);
|
|
360
|
+
}
|
|
361
|
+
const resolution = judgedResolutionOf(reader, attemptDir);
|
|
362
|
+
if (resolution.state === "selected")
|
|
363
|
+
return resolution.value;
|
|
364
|
+
if (resolution.state === "unresolved") {
|
|
365
|
+
problems.push("the recorded resolution stayed unresolved; the raw judgement governs");
|
|
366
|
+
}
|
|
367
|
+
return rawJudgement(reader, attemptDir, problems);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Extract the documented outcome from a judgement value, reporting an
|
|
371
|
+
* unsupported value instead of silently dropping it.
|
|
372
|
+
* @param value The judgement object.
|
|
373
|
+
* @param problems Problem collector.
|
|
374
|
+
* @returns The outcome, or undefined.
|
|
375
|
+
*/
|
|
376
|
+
function outcomeOf(value, problems) {
|
|
377
|
+
const outcome = value.documentationOutcome;
|
|
378
|
+
if (outcome === undefined)
|
|
379
|
+
return undefined;
|
|
380
|
+
if (typeof outcome !== "string") {
|
|
381
|
+
problems.push(`unsupported documentationOutcome ${JSON.stringify(outcome)}; treated as unassessed, never as none`);
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const known = DOCUMENTATION_OUTCOMES.find((known) => known === outcome);
|
|
385
|
+
if (known !== undefined)
|
|
386
|
+
return known;
|
|
387
|
+
problems.push(`unsupported documentationOutcome ${JSON.stringify(outcome)}; treated as unassessed, never as none`);
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Load per-fact preservation observations for the scenario's inventory.
|
|
392
|
+
* @param reader The evidence reader.
|
|
393
|
+
* @param attemptDir The attempt directory, relative to the evidence root.
|
|
394
|
+
* @param factIds The inventory fact ids to load.
|
|
395
|
+
* @returns The per-fact observations, or the explicit state.
|
|
396
|
+
*/
|
|
397
|
+
function loadPreservation(reader, attemptDir, factIds) {
|
|
398
|
+
const judgement = loadJson(reader, joinPath(attemptDir, "judgement.json"));
|
|
399
|
+
const resolution = loadJson(reader, joinPath(attemptDir, "resolution.json"));
|
|
400
|
+
const value = resolution.state === "available" && isRecord(resolution.value)
|
|
401
|
+
? resolution.value.parsed
|
|
402
|
+
: judgement.state === "available"
|
|
403
|
+
? judgement.value
|
|
404
|
+
: undefined;
|
|
405
|
+
if (!isRecord(value) || !isRecord(value.preservation)) {
|
|
406
|
+
return { state: "missing", source: attemptDir };
|
|
407
|
+
}
|
|
408
|
+
const byFact = {};
|
|
409
|
+
for (const factId of factIds) {
|
|
410
|
+
byFact[factId] = preservationFactOf(joinPath(attemptDir, `resolution.json#${factId}`), value.preservation[factId]);
|
|
411
|
+
}
|
|
412
|
+
return loaded(byFact, attemptDir);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Load one inventory fact's observation, keeping shape problems explicit.
|
|
416
|
+
* @param source The display source for the observation.
|
|
417
|
+
* @param entry The raw fact entry.
|
|
418
|
+
* @returns The observation, or the explicit state.
|
|
419
|
+
*/
|
|
420
|
+
function preservationFactOf(source, entry) {
|
|
421
|
+
if (entry === undefined)
|
|
422
|
+
return { state: "missing", source };
|
|
423
|
+
if (!isRecord(entry)) {
|
|
424
|
+
return {
|
|
425
|
+
state: "malformed",
|
|
426
|
+
source,
|
|
427
|
+
problems: ["preservation entry is not an object"],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const problems = [];
|
|
431
|
+
if (typeof entry.state !== "string" || entry.state.trim() === "") {
|
|
432
|
+
problems.push("preservation state must be a nonblank string");
|
|
433
|
+
}
|
|
434
|
+
if (typeof entry.quote !== "string" || entry.quote.trim() === "") {
|
|
435
|
+
problems.push("preservation quote must be a nonblank string");
|
|
436
|
+
}
|
|
437
|
+
if (problems.length > 0)
|
|
438
|
+
return { state: "malformed", source, problems };
|
|
439
|
+
return loaded({ state: entry.state, quote: entry.quote }, source);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Build an assessment for a trial with no usable judgement layer.
|
|
443
|
+
* @param status The assessment status to record.
|
|
444
|
+
* @param source The display source for the missing preservation record.
|
|
445
|
+
* @param problems Problems found while getting here.
|
|
446
|
+
* @returns The assessment.
|
|
447
|
+
*/
|
|
448
|
+
function unassessedResult(status, source, problems) {
|
|
449
|
+
return {
|
|
450
|
+
status,
|
|
451
|
+
layer: "none",
|
|
452
|
+
outcome: undefined,
|
|
453
|
+
reason: undefined,
|
|
454
|
+
evidence: [],
|
|
455
|
+
preservation: { state: "missing", source },
|
|
456
|
+
retrieval: { state: "missing", source },
|
|
457
|
+
problems,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Report unselected judge attempt directories, so extra judgement layers
|
|
462
|
+
* cannot silently disappear from the record.
|
|
463
|
+
* @param reader The evidence reader.
|
|
464
|
+
* @param acDir The trial's ac directory, relative to the evidence root.
|
|
465
|
+
* @returns One problem per unselected attempt directory.
|
|
466
|
+
*/
|
|
467
|
+
function unselectedAttemptProblems(reader, acDir, selectedAttemptId) {
|
|
468
|
+
const entries = reader.readDirectoryEntries(acDir);
|
|
469
|
+
if (entries.state !== "available")
|
|
470
|
+
return [];
|
|
471
|
+
const problems = [];
|
|
472
|
+
for (const name of entries.value) {
|
|
473
|
+
if (!/^attempt-\d+$/.test(name) || name === selectedAttemptId)
|
|
474
|
+
continue;
|
|
475
|
+
const judgement = loadJson(reader, joinPath(joinPath(acDir, name), "judgement.json"));
|
|
476
|
+
const judged = judgement.state === "available" &&
|
|
477
|
+
isRecord(judgement.value) &&
|
|
478
|
+
judgement.value.status === "judged";
|
|
479
|
+
if (judged) {
|
|
480
|
+
problems.push(`unselected judge attempt present: ${name}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return problems;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Load the selected judgement layer's assessment.
|
|
487
|
+
* @param reader The evidence reader.
|
|
488
|
+
* @param attemptDir The selected attempt directory.
|
|
489
|
+
* @param options The load options.
|
|
490
|
+
* @param problems Problem collector.
|
|
491
|
+
* @returns The resolved assessment.
|
|
492
|
+
*/
|
|
493
|
+
function selectedAssessment(reader, attemptDir, options, problems) {
|
|
494
|
+
const selectedValue = selectJudgement(reader, attemptDir, options.selectionPolicy, problems);
|
|
495
|
+
if (selectedValue === undefined) {
|
|
496
|
+
return unassessedResult("unassessed", attemptDir, problems);
|
|
497
|
+
}
|
|
498
|
+
const { status, value, layer } = selectedValue;
|
|
499
|
+
const recorded = status === "judged" ||
|
|
500
|
+
status === "invalid-verdict" ||
|
|
501
|
+
status === "judge-error";
|
|
502
|
+
if (!recorded) {
|
|
503
|
+
problems.push(`unsupported judgement status ${JSON.stringify(status)}`);
|
|
504
|
+
return unassessedResult("unassessed", attemptDir, problems);
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
status,
|
|
508
|
+
layer,
|
|
509
|
+
outcome: status === "judged" ? outcomeOf(value, problems) : undefined,
|
|
510
|
+
reason: typeof value.documentationReason === "string"
|
|
511
|
+
? value.documentationReason
|
|
512
|
+
: undefined,
|
|
513
|
+
evidence: evidenceOf(value, problems),
|
|
514
|
+
preservation: loadPreservation(reader, attemptDir, options.inventoryFactIds),
|
|
515
|
+
retrieval: status === "judged"
|
|
516
|
+
? retrievalOf(value, attemptDir)
|
|
517
|
+
: { state: "missing", source: attemptDir },
|
|
518
|
+
problems,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Extract the judge's retrieval-application finding, keeping a missing or
|
|
523
|
+
* malformed block explicit rather than treating it as a miss.
|
|
524
|
+
* @param value The judgement object.
|
|
525
|
+
* @param source The display source.
|
|
526
|
+
* @returns The observation, or the explicit state.
|
|
527
|
+
*/
|
|
528
|
+
function retrievalOf(value, source) {
|
|
529
|
+
const entry = value.retrievalApplication;
|
|
530
|
+
if (entry === undefined)
|
|
531
|
+
return { state: "missing", source };
|
|
532
|
+
if (!isRecord(entry)) {
|
|
533
|
+
return {
|
|
534
|
+
state: "malformed",
|
|
535
|
+
source,
|
|
536
|
+
problems: ["retrievalApplication is not an object"],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (typeof entry.state !== "string" || typeof entry.quote !== "string") {
|
|
540
|
+
return {
|
|
541
|
+
state: "malformed",
|
|
542
|
+
source,
|
|
543
|
+
problems: ["retrievalApplication needs string state and quote"],
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
state: "available",
|
|
548
|
+
value: { state: entry.state, quote: entry.quote },
|
|
549
|
+
source,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Re-derive one attempt's recorded facts from the evidence.
|
|
554
|
+
* @param reader The evidence reader.
|
|
555
|
+
* @param acDir The trial's ac directory.
|
|
556
|
+
* @param attemptId The attempt directory name.
|
|
557
|
+
* @returns The attempt facts the selection check compares against.
|
|
558
|
+
*/
|
|
559
|
+
function attemptFactOf(reader, acDir, attemptId) {
|
|
560
|
+
const base = joinPath(acDir, attemptId);
|
|
561
|
+
const judge = loadedRecordOf(loadJson(reader, joinPath(base, "judgement.json")));
|
|
562
|
+
const resolved = loadedRecordOf(loadJson(reader, joinPath(base, "resolution.json")));
|
|
563
|
+
const metadata = loadedRecordOf(loadJson(reader, joinPath(base, "meta.json")));
|
|
564
|
+
const completion = metadata !== null && isRecord(metadata.completion)
|
|
565
|
+
? metadata.completion
|
|
566
|
+
: null;
|
|
567
|
+
return {
|
|
568
|
+
attemptId,
|
|
569
|
+
judgementStatus: attemptStatusOf(judge, resolved),
|
|
570
|
+
packetSha256: typeof metadata?.packetSha256 === "string"
|
|
571
|
+
? metadata.packetSha256
|
|
572
|
+
: null,
|
|
573
|
+
failureClass: typeof judge?.failureClass === "string" ? judge.failureClass : null,
|
|
574
|
+
complete: typeof completion?.complete === "boolean"
|
|
575
|
+
? completion.complete
|
|
576
|
+
: null,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/** The recorded object of one loaded JSON file, or null. */
|
|
580
|
+
function loadedRecordOf(loaded) {
|
|
581
|
+
return loaded.state === "available" && isRecord(loaded.value)
|
|
582
|
+
? loaded.value
|
|
583
|
+
: null;
|
|
584
|
+
}
|
|
585
|
+
/** One attempt's effective status: a resolved verdict is usable. */
|
|
586
|
+
function attemptStatusOf(judge, resolved) {
|
|
587
|
+
if (resolved?.status === "judged")
|
|
588
|
+
return "judged";
|
|
589
|
+
return typeof judge?.status === "string" ? judge.status : "missing";
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Read and check one slot's published selection against the evidence.
|
|
593
|
+
*
|
|
594
|
+
* Absence keeps the historical behaviour (the manifest selector). A present
|
|
595
|
+
* record is checked evidence: a tampered or ineligible selection is refused,
|
|
596
|
+
* never silently replaced by the manifest selector.
|
|
597
|
+
* @param reader The evidence reader.
|
|
598
|
+
* @param acDir The trial's ac directory.
|
|
599
|
+
* @param options The load options.
|
|
600
|
+
* @returns The selection outcome.
|
|
601
|
+
*/
|
|
602
|
+
function loadSelection(reader, acDir, options) {
|
|
603
|
+
const loaded = loadJson(reader, joinPath(acDir, "selection.json"));
|
|
604
|
+
if (loaded.state !== "available")
|
|
605
|
+
return unavailableSelection(loaded);
|
|
606
|
+
const dirs = reader.readDirectoryEntries(acDir);
|
|
607
|
+
if (dirs.state !== "available") {
|
|
608
|
+
return invalidSelection(`selection record cannot be checked: ${acDir} is ${dirs.state}`);
|
|
609
|
+
}
|
|
610
|
+
const attemptIds = dirs.value.filter((name) => /^attempt-\d{3}$/.test(name));
|
|
611
|
+
const attemptFacts = {};
|
|
612
|
+
for (const attemptId of attemptIds) {
|
|
613
|
+
attemptFacts[attemptId] = attemptFactOf(reader, acDir, attemptId);
|
|
614
|
+
}
|
|
615
|
+
return selectionOutcome(selectionProblems(loaded.value, selectionFactsOf(options, attemptIds, attemptFacts)), selectedIdOf(loaded.value));
|
|
616
|
+
}
|
|
617
|
+
/** A missing or unreadable selection record. */
|
|
618
|
+
function unavailableSelection(loaded) {
|
|
619
|
+
if (loaded.state === "missing") {
|
|
620
|
+
return {
|
|
621
|
+
attemptId: null,
|
|
622
|
+
present: false,
|
|
623
|
+
invalid: false,
|
|
624
|
+
problems: [],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return invalidSelection(`selection record is ${loaded.state}: ${loaded.source}`);
|
|
628
|
+
}
|
|
629
|
+
/** An untrustworthy selection record. */
|
|
630
|
+
function invalidSelection(...problems) {
|
|
631
|
+
return { attemptId: null, present: true, invalid: true, problems };
|
|
632
|
+
}
|
|
633
|
+
/** The selected attempt, or null when the record names none. */
|
|
634
|
+
function selectedIdOf(value) {
|
|
635
|
+
return isRecord(value) && typeof value.selectedAttemptId === "string"
|
|
636
|
+
? value.selectedAttemptId
|
|
637
|
+
: null;
|
|
638
|
+
}
|
|
639
|
+
/** The final selection outcome. */
|
|
640
|
+
function selectionOutcome(problems, selected) {
|
|
641
|
+
return problems.length === 0 && selected !== null
|
|
642
|
+
? { attemptId: selected, present: true, invalid: false, problems: [] }
|
|
643
|
+
: invalidSelection(...(problems.length > 0
|
|
644
|
+
? problems
|
|
645
|
+
: ["selection record names no selected attempt"]));
|
|
646
|
+
}
|
|
647
|
+
/** The facts a recorded selection is checked against here. */
|
|
648
|
+
function selectionFactsOf(options, attemptIds, attemptFacts) {
|
|
649
|
+
return {
|
|
650
|
+
...(options.slot === undefined ? {} : { slot: options.slot }),
|
|
651
|
+
...(options.pairId === undefined ? {} : { pairId: options.pairId }),
|
|
652
|
+
stamp: options.stamp,
|
|
653
|
+
...(options.arm === undefined ? {} : { arm: options.arm }),
|
|
654
|
+
scenario: options.scenario,
|
|
655
|
+
attemptIds,
|
|
656
|
+
manifestAttemptId: options.attemptId ?? "attempt-001",
|
|
657
|
+
policy: options.recoveryPolicy ?? null,
|
|
658
|
+
attemptFacts,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Load one trial's assessment through the recorded selection policy.
|
|
663
|
+
* @param reader The evidence reader.
|
|
664
|
+
* @param dir The trial directory, relative to the evidence root.
|
|
665
|
+
* @param attempt The attempt state of the trial directory.
|
|
666
|
+
* @param options The load options.
|
|
667
|
+
* @returns The resolved assessment.
|
|
668
|
+
*/
|
|
669
|
+
function loadAssessment(reader, dir, attempt, options) {
|
|
670
|
+
const acDir = joinPath(dir, "ac");
|
|
671
|
+
const selection = loadSelection(reader, acDir, options);
|
|
672
|
+
if (selection.invalid) {
|
|
673
|
+
return unassessedResult("unassessed", acDir, selection.problems);
|
|
674
|
+
}
|
|
675
|
+
const attemptId = selection.attemptId ?? options.attemptId ?? "attempt-001";
|
|
676
|
+
const problems = unselectedAttemptProblems(reader, acDir, attemptId);
|
|
677
|
+
const assessment = attemptAssessmentOf(reader, acDir, attempt, attemptId, options, problems);
|
|
678
|
+
return selection.present && selection.attemptId !== null
|
|
679
|
+
? { ...assessment, selectedAttemptId: selection.attemptId }
|
|
680
|
+
: assessment;
|
|
681
|
+
}
|
|
682
|
+
/** The assessment of one selected attempt directory. */
|
|
683
|
+
function attemptAssessmentOf(reader, acDir, attempt, attemptId, options, problems) {
|
|
684
|
+
if (attempt === "absent") {
|
|
685
|
+
return unassessedResult("not-run", acDir, problems);
|
|
686
|
+
}
|
|
687
|
+
if (attempt !== "recorded") {
|
|
688
|
+
return unassessedResult("unknown", acDir, problems);
|
|
689
|
+
}
|
|
690
|
+
const selected = joinPath(acDir, attemptId);
|
|
691
|
+
const attemptDirs = reader.readDirectoryEntries(acDir);
|
|
692
|
+
if (attemptDirs.state !== "available" ||
|
|
693
|
+
!attemptDirs.value.includes(attemptId)) {
|
|
694
|
+
return unassessedResult("unassessed", selected, problems);
|
|
695
|
+
}
|
|
696
|
+
return selectedAssessment(reader, selected, options, problems);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Extract the judgement's evidence quotes, keeping shape problems visible.
|
|
700
|
+
* @param value The judgement object.
|
|
701
|
+
* @param problems Problem collector.
|
|
702
|
+
* @returns The nonblank string quotes; empty when absent or malformed.
|
|
703
|
+
*/
|
|
704
|
+
function evidenceOf(value, problems) {
|
|
705
|
+
const evidence = value.evidence;
|
|
706
|
+
if (evidence === undefined)
|
|
707
|
+
return [];
|
|
708
|
+
if (!Array.isArray(evidence)) {
|
|
709
|
+
problems.push("judgement evidence is not an array");
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
712
|
+
const quotes = evidence.filter((quote) => typeof quote === "string" && quote.trim() !== "");
|
|
713
|
+
if (quotes.length !== evidence.length) {
|
|
714
|
+
problems.push("judgement evidence holds non-string or blank entries");
|
|
715
|
+
}
|
|
716
|
+
return quotes;
|
|
717
|
+
}
|
|
718
|
+
/** Whether a value is a plain JSON object. */
|
|
719
|
+
function isRecord(value) {
|
|
720
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
721
|
+
}
|
|
722
|
+
/** An empty per-field token total. */
|
|
723
|
+
function emptyField() {
|
|
724
|
+
return { total: 0, measuredSteps: 0, unknownSteps: 0 };
|
|
725
|
+
}
|
|
726
|
+
/** An empty token total. */
|
|
727
|
+
function emptyUsage() {
|
|
728
|
+
return {
|
|
729
|
+
steps: 0,
|
|
730
|
+
input: emptyField(),
|
|
731
|
+
output: emptyField(),
|
|
732
|
+
reasoning: emptyField(),
|
|
733
|
+
cacheRead: emptyField(),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Parse one trail line into a JSON object, if it is one.
|
|
738
|
+
* @param line The raw trail line.
|
|
739
|
+
* @returns The object, or undefined when the line is not a JSON object.
|
|
740
|
+
*/
|
|
741
|
+
function parseTrailLine(line) {
|
|
742
|
+
try {
|
|
743
|
+
const value = JSON.parse(line);
|
|
744
|
+
return isRecord(value) ? value : undefined;
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Add one reported token field. A numeric value is measured, including an
|
|
752
|
+
* explicit zero; an absent or non-numeric value is unknown and contributes no
|
|
753
|
+
* subtotal.
|
|
754
|
+
* @param field The accumulated field usage.
|
|
755
|
+
* @param value The raw recorded value.
|
|
756
|
+
*/
|
|
757
|
+
function addTokenField(field, value) {
|
|
758
|
+
if (typeof value === "number") {
|
|
759
|
+
field.total += value;
|
|
760
|
+
field.measuredSteps += 1;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
field.unknownSteps += 1;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Add one trail event's token usage. Every step_finish event counts as an
|
|
768
|
+
* observed step; each token field keeps its own measured/unknown coverage, so
|
|
769
|
+
* a step reporting only `input` never implies a measured zero `output`.
|
|
770
|
+
* @param usage The accumulated usage.
|
|
771
|
+
* @param event The parsed trail event.
|
|
772
|
+
*/
|
|
773
|
+
function addTokenUsage(usage, event) {
|
|
774
|
+
if (event.type !== "step_finish")
|
|
775
|
+
return;
|
|
776
|
+
usage.steps += 1;
|
|
777
|
+
const part = isRecord(event.part) ? event.part : {};
|
|
778
|
+
const tokens = isRecord(part.tokens) ? part.tokens : undefined;
|
|
779
|
+
const cache = tokens !== undefined && isRecord(tokens.cache)
|
|
780
|
+
? tokens.cache
|
|
781
|
+
: undefined;
|
|
782
|
+
addTokenField(usage.input, tokens?.input);
|
|
783
|
+
addTokenField(usage.output, tokens?.output);
|
|
784
|
+
addTokenField(usage.reasoning, tokens?.reasoning);
|
|
785
|
+
addTokenField(usage.cacheRead, cache?.read);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Sum token accounting from a recorded session trail. An unparsable trail is
|
|
789
|
+
* malformed rather than partially summed, so a corrupt line cannot deflate a
|
|
790
|
+
* cost silently.
|
|
791
|
+
* @param reader The evidence reader.
|
|
792
|
+
* @param path The trail path, relative to the evidence root.
|
|
793
|
+
* @returns The summed usage, or the explicit state.
|
|
794
|
+
*/
|
|
795
|
+
function loadTokens(reader, path) {
|
|
796
|
+
const lines = loadLines(reader, path);
|
|
797
|
+
if (lines.state !== "available")
|
|
798
|
+
return lines;
|
|
799
|
+
const usage = emptyUsage();
|
|
800
|
+
const problems = [];
|
|
801
|
+
for (const [index, line] of lines.value.entries()) {
|
|
802
|
+
const event = parseTrailLine(line);
|
|
803
|
+
if (event === undefined) {
|
|
804
|
+
problems.push(`unparsable trail line ${index + 1}: ${line.slice(0, 80)}`);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
addTokenUsage(usage, event);
|
|
808
|
+
}
|
|
809
|
+
if (problems.length > 0) {
|
|
810
|
+
return { state: "malformed", source: path, problems };
|
|
811
|
+
}
|
|
812
|
+
return loaded(usage, path);
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Load and verify the plugin's operation journal for one trial.
|
|
816
|
+
*
|
|
817
|
+
* Verification inputs come from the same trial directory: the saved journal,
|
|
818
|
+
* its per-run signing key, the session/message ids in `stdout.jsonl`, and the
|
|
819
|
+
* host dispatches in `tools.jsonl`. A missing journal is absent evidence, not
|
|
820
|
+
* an empty one; an unreadable journal credits nothing.
|
|
821
|
+
* @param reader The evidence reader.
|
|
822
|
+
* @param dir The trial directory, relative to the evidence root.
|
|
823
|
+
* @returns The verification result.
|
|
824
|
+
*/
|
|
825
|
+
function loadJournalVerification(reader, dir) {
|
|
826
|
+
const journal = reader.readTextFile(joinPath(dir, "docs-operations.jsonl"));
|
|
827
|
+
if (journal.state === "missing") {
|
|
828
|
+
return {
|
|
829
|
+
state: "absent",
|
|
830
|
+
credited: [],
|
|
831
|
+
rejected: [],
|
|
832
|
+
problems: [],
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
const key = reader.readTextFile(joinPath(dir, "docs-operations.key"));
|
|
836
|
+
const stdout = reader.readTextFile(joinPath(dir, "stdout.jsonl"));
|
|
837
|
+
const tools = reader.readTextFile(joinPath(dir, "tools.jsonl"));
|
|
838
|
+
const host = tools.state === "available"
|
|
839
|
+
? hostDispatchesFromTrail(tools.value)
|
|
840
|
+
: { callIds: new Set(), nativeCalls: [], dispatches: [] };
|
|
841
|
+
return verifyOperationsJournal({
|
|
842
|
+
lines: journal.state === "available" ? journal.value.split("\n") : [],
|
|
843
|
+
key: key.state === "available" ? key.value : null,
|
|
844
|
+
identity: stdout.state === "available"
|
|
845
|
+
? journalIdentityFromTrail(stdout.value)
|
|
846
|
+
: { sessions: new Set(), messages: new Set() },
|
|
847
|
+
callIds: host.callIds,
|
|
848
|
+
nativeCalls: host.nativeCalls,
|
|
849
|
+
dispatches: host.dispatches,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Load one trial's evidence under the ac-v1 layout.
|
|
854
|
+
* @param reader The evidence reader rooted at the evidence root.
|
|
855
|
+
* @param options Which trial to load and how to select judgement layers.
|
|
856
|
+
* @returns The trial record.
|
|
857
|
+
*/
|
|
858
|
+
export function loadTrialRecord(reader, options) {
|
|
859
|
+
const dir = joinPath(joinPath(options.resultRoot, options.stamp), options.scenario);
|
|
860
|
+
const attempt = attemptOf(reader, dir);
|
|
861
|
+
const run = loadRun(reader, dir);
|
|
862
|
+
const usability = usableOf(run);
|
|
863
|
+
const finalAnswer = reader.readTextFile(joinPath(dir, "final.md"));
|
|
864
|
+
const answerProblem = finalAnswer.state === "available" && run.state === "available"
|
|
865
|
+
? answerIdentityProblem(finalAnswer.value, run.value.answerHash, {
|
|
866
|
+
allowUnbound: options.allowUnboundAnswer === true,
|
|
867
|
+
})
|
|
868
|
+
: null;
|
|
869
|
+
const problems = [...attempt.problems, ...usability.problems];
|
|
870
|
+
if (answerProblem !== null)
|
|
871
|
+
problems.push(answerProblem);
|
|
872
|
+
const assessment = loadAssessment(reader, dir, attempt.state, options);
|
|
873
|
+
return {
|
|
874
|
+
identity: {
|
|
875
|
+
resultRoot: options.resultRoot,
|
|
876
|
+
stamp: options.stamp,
|
|
877
|
+
scenario: options.scenario,
|
|
878
|
+
},
|
|
879
|
+
attempt: {
|
|
880
|
+
state: attempt.state,
|
|
881
|
+
usable: answerProblem === null ? usability.usable : false,
|
|
882
|
+
problems,
|
|
883
|
+
},
|
|
884
|
+
run,
|
|
885
|
+
gate: loadGate(reader, dir),
|
|
886
|
+
primary: primaryOf(loadJson(reader, joinPath(dir, "primary-check.json"))),
|
|
887
|
+
assessment,
|
|
888
|
+
judgeModel: loadJudgeModel(reader, dir, assessment.selectedAttemptId ?? options.attemptId ?? "attempt-001"),
|
|
889
|
+
tokens: loadTokens(reader, joinPath(dir, "stdout.jsonl")),
|
|
890
|
+
artefacts: {
|
|
891
|
+
finalAnswer,
|
|
892
|
+
writesText: reader.readTextFile(joinPath(dir, "writes.txt")),
|
|
893
|
+
docsOperations: loadLines(reader, joinPath(dir, "docs-operations.jsonl")),
|
|
894
|
+
promptText: reader.readTextFile(joinPath(dir, "prompt.md")),
|
|
895
|
+
},
|
|
896
|
+
journal: loadJournalVerification(reader, dir),
|
|
897
|
+
answerProblem,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Load the judge model identity from the attempt metadata.
|
|
902
|
+
* @param reader The evidence reader.
|
|
903
|
+
* @param dir The trial directory, relative to the evidence root.
|
|
904
|
+
* @returns The judge model, or the explicit state.
|
|
905
|
+
*/
|
|
906
|
+
function loadJudgeModel(reader, dir, attemptId) {
|
|
907
|
+
const meta = loadJson(reader, joinPath(dir, joinPath("ac", joinPath(attemptId, "meta.json"))));
|
|
908
|
+
if (meta.state === "available" && isRecord(meta.value)) {
|
|
909
|
+
const model = meta.value.judgeModel;
|
|
910
|
+
if (typeof model === "string" && model.trim() !== "") {
|
|
911
|
+
return loaded(model, meta.source);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return { state: "missing", source: meta.source };
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Load the campaign attempt ledger (one JSON event per line).
|
|
918
|
+
* @param reader The evidence reader.
|
|
919
|
+
* @param path The ledger path, relative to the evidence root.
|
|
920
|
+
* @returns The events, or the explicit state.
|
|
921
|
+
*/
|
|
922
|
+
export function loadLaunchLedger(reader, path) {
|
|
923
|
+
const text = reader.readTextFile(path);
|
|
924
|
+
if (text.state !== "available")
|
|
925
|
+
return text;
|
|
926
|
+
const parsed = parseCampaignLedger(text.value);
|
|
927
|
+
if (parsed.state === "malformed") {
|
|
928
|
+
return { state: "malformed", source: path, problems: parsed.problems };
|
|
929
|
+
}
|
|
930
|
+
return loaded(parsed.events.map((event) => ({
|
|
931
|
+
at: event.at,
|
|
932
|
+
event: event.event,
|
|
933
|
+
stamp: event.stamp,
|
|
934
|
+
})), path);
|
|
935
|
+
}
|