@markstanden/opencode-test-container 0.0.0-stage → 0.1.1

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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -2
  3. package/container/Containerfile +24 -0
  4. package/container/README.md +29 -0
  5. package/dist/adapters/opencode/events.d.ts +90 -0
  6. package/dist/adapters/opencode/events.js +213 -0
  7. package/dist/adapters/opencode/runtime.d.ts +87 -0
  8. package/dist/adapters/opencode/runtime.js +136 -0
  9. package/dist/adapters/podman/container.d.ts +62 -0
  10. package/dist/adapters/podman/container.js +119 -0
  11. package/dist/adapters/podman/image.d.ts +50 -0
  12. package/dist/adapters/podman/image.js +124 -0
  13. package/dist/adapters/process.d.ts +38 -0
  14. package/dist/adapters/process.js +71 -0
  15. package/dist/campaign/assess.d.ts +45 -0
  16. package/dist/campaign/assess.js +208 -0
  17. package/dist/campaign/close.d.ts +17 -0
  18. package/dist/campaign/close.js +112 -0
  19. package/dist/campaign/evidence-guard.d.ts +24 -0
  20. package/dist/campaign/evidence-guard.js +49 -0
  21. package/dist/campaign/freeze.d.ts +94 -0
  22. package/dist/campaign/freeze.js +297 -0
  23. package/dist/campaign/inventory.d.ts +62 -0
  24. package/dist/campaign/inventory.js +192 -0
  25. package/dist/campaign/judge.d.ts +209 -0
  26. package/dist/campaign/judge.js +1285 -0
  27. package/dist/campaign/ledger.d.ts +64 -0
  28. package/dist/campaign/ledger.js +149 -0
  29. package/dist/campaign/prepare.d.ts +129 -0
  30. package/dist/campaign/prepare.js +483 -0
  31. package/dist/campaign/primary.d.ts +96 -0
  32. package/dist/campaign/primary.js +189 -0
  33. package/dist/campaign/run.d.ts +151 -0
  34. package/dist/campaign/run.js +562 -0
  35. package/dist/campaign/status.d.ts +76 -0
  36. package/dist/campaign/status.js +237 -0
  37. package/dist/cli/otc.d.ts +51 -0
  38. package/dist/cli/otc.js +435 -0
  39. package/dist/contracts/canonical.d.ts +21 -0
  40. package/dist/contracts/canonical.js +44 -0
  41. package/dist/contracts/types.d.ts +261 -0
  42. package/dist/contracts/types.js +9 -0
  43. package/dist/contracts/validate.d.ts +72 -0
  44. package/dist/contracts/validate.js +1021 -0
  45. package/dist/contracts/version.d.ts +44 -0
  46. package/dist/contracts/version.js +147 -0
  47. package/dist/evaluators/docs/analysis/accounting.d.ts +140 -0
  48. package/dist/evaluators/docs/analysis/accounting.js +237 -0
  49. package/dist/evaluators/docs/analysis/blinding.d.ts +40 -0
  50. package/dist/evaluators/docs/analysis/blinding.js +80 -0
  51. package/dist/evaluators/docs/analysis/bounds.d.ts +40 -0
  52. package/dist/evaluators/docs/analysis/bounds.js +31 -0
  53. package/dist/evaluators/docs/analysis/budget.d.ts +137 -0
  54. package/dist/evaluators/docs/analysis/budget.js +305 -0
  55. package/dist/evaluators/docs/analysis/closure.d.ts +175 -0
  56. package/dist/evaluators/docs/analysis/closure.js +251 -0
  57. package/dist/evaluators/docs/analysis/decision.d.ts +59 -0
  58. package/dist/evaluators/docs/analysis/decision.js +161 -0
  59. package/dist/evaluators/docs/analysis/freeze.d.ts +201 -0
  60. package/dist/evaluators/docs/analysis/freeze.js +395 -0
  61. package/dist/evaluators/docs/analysis/guardrails.d.ts +51 -0
  62. package/dist/evaluators/docs/analysis/guardrails.js +194 -0
  63. package/dist/evaluators/docs/analysis/journal.d.ts +150 -0
  64. package/dist/evaluators/docs/analysis/journal.js +475 -0
  65. package/dist/evaluators/docs/analysis/ledger.d.ts +78 -0
  66. package/dist/evaluators/docs/analysis/ledger.js +145 -0
  67. package/dist/evaluators/docs/analysis/measures.d.ts +302 -0
  68. package/dist/evaluators/docs/analysis/measures.js +1069 -0
  69. package/dist/evaluators/docs/analysis/model.d.ts +236 -0
  70. package/dist/evaluators/docs/analysis/model.js +740 -0
  71. package/dist/evaluators/docs/analysis/pairing.d.ts +68 -0
  72. package/dist/evaluators/docs/analysis/pairing.js +95 -0
  73. package/dist/evaluators/docs/analysis/records.d.ts +229 -0
  74. package/dist/evaluators/docs/analysis/records.js +935 -0
  75. package/dist/evaluators/docs/analysis/recovery.d.ts +225 -0
  76. package/dist/evaluators/docs/analysis/recovery.js +507 -0
  77. package/dist/evaluators/docs/analysis/render.d.ts +235 -0
  78. package/dist/evaluators/docs/analysis/render.js +566 -0
  79. package/dist/evaluators/docs/analysis/replay.d.ts +15 -0
  80. package/dist/evaluators/docs/analysis/replay.js +1272 -0
  81. package/dist/evaluators/docs/analysis/screening/inventory.d.ts +136 -0
  82. package/dist/evaluators/docs/analysis/screening/inventory.js +307 -0
  83. package/dist/evaluators/docs/analysis/screening/noteVersion.d.ts +15 -0
  84. package/dist/evaluators/docs/analysis/screening/noteVersion.js +46 -0
  85. package/dist/evaluators/docs/analysis/screening/reconcile.d.ts +38 -0
  86. package/dist/evaluators/docs/analysis/screening/reconcile.js +40 -0
  87. package/dist/evaluators/docs/analysis/screening/rules.d.ts +163 -0
  88. package/dist/evaluators/docs/analysis/screening/rules.js +330 -0
  89. package/dist/evaluators/docs/analysis/screening/scan.d.ts +72 -0
  90. package/dist/evaluators/docs/analysis/screening/scan.js +362 -0
  91. package/dist/evaluators/docs/analysis/selector.d.ts +68 -0
  92. package/dist/evaluators/docs/analysis/selector.js +123 -0
  93. package/dist/evaluators/docs/assess.d.ts +129 -0
  94. package/dist/evaluators/docs/assess.js +433 -0
  95. package/dist/evaluators/docs/campaign.d.ts +133 -0
  96. package/dist/evaluators/docs/campaign.js +222 -0
  97. package/dist/evaluators/docs/index.d.ts +25 -0
  98. package/dist/evaluators/docs/index.js +25 -0
  99. package/dist/evaluators/docs/journal.d.ts +150 -0
  100. package/dist/evaluators/docs/journal.js +475 -0
  101. package/dist/evaluators/docs/scoring.d.ts +367 -0
  102. package/dist/evaluators/docs/scoring.js +662 -0
  103. package/dist/evaluators/skills/assess.d.ts +94 -0
  104. package/dist/evaluators/skills/assess.js +207 -0
  105. package/dist/index.d.ts +46 -0
  106. package/dist/index.js +29 -0
  107. package/dist/legacy/prod014.d.ts +86 -0
  108. package/dist/legacy/prod014.js +209 -0
  109. package/dist/reporting/report.d.ts +79 -0
  110. package/dist/reporting/report.js +242 -0
  111. package/docs/architecture.md +106 -0
  112. package/docs/capability-matrix.md +84 -0
  113. package/docs/offline-rehearsal.md +75 -0
  114. package/docs/parity-record.md +81 -0
  115. package/docs/pr-description.md +103 -0
  116. package/docs/release-0.1.0.md +52 -0
  117. package/docs/releasing.md +82 -0
  118. package/docs/schema.md +97 -0
  119. package/examples/skills/README.md +13 -0
  120. package/examples/skills/campaign.json +73 -0
  121. package/examples/skills/skills/otel-helper/SKILL.md +10 -0
  122. package/examples/skills/suite.json +29 -0
  123. package/package.json +45 -5
@@ -0,0 +1,433 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { sha256Bytes } from "../../contracts/canonical.js";
4
+ import { isRecord } from "../../contracts/validate.js";
5
+ import { decodeTrail } from "../../adapters/opencode/events.js";
6
+ import { hostDispatchesFromTrail, journalIdentityFromTrail, verifyOperationsJournal, } from "./journal.js";
7
+ import { readPrimaryCheck, } from "../../campaign/primary.js";
8
+ /** Determine which docs verb one host input names. */
9
+ export function docsVerbOf(input) {
10
+ if (!isRecord(input))
11
+ return "other";
12
+ if (input.add !== undefined)
13
+ return "add";
14
+ if (input.search !== undefined)
15
+ return "search";
16
+ if (input.read !== undefined)
17
+ return "read";
18
+ if (input.check !== undefined)
19
+ return "check";
20
+ if (input.help !== undefined)
21
+ return "help";
22
+ return "other";
23
+ }
24
+ /** The metadata a served docs result carried. */
25
+ function resultMetadata(record) {
26
+ const result = record.result;
27
+ return isRecord(result) && isRecord(result.metadata) ? result.metadata : {};
28
+ }
29
+ /** Whether a credited record proves a completed write and where. */
30
+ export function creditedWriteOf(record) {
31
+ if (resultMetadata(record).written !== true)
32
+ return null;
33
+ const metadata = resultMetadata(record);
34
+ const input = isRecord(record.input) ? record.input : {};
35
+ const add = input.add;
36
+ const mode = typeof add === "string"
37
+ ? "create"
38
+ : isRecord(add) && typeof add.mode === "string"
39
+ ? add.mode
40
+ : null;
41
+ return {
42
+ path: typeof metadata.path === "string" ? metadata.path : null,
43
+ mode,
44
+ };
45
+ }
46
+ /** Read meta without trusting it. */
47
+ function readMeta(path) {
48
+ if (!existsSync(path))
49
+ return null;
50
+ try {
51
+ const value = JSON.parse(readFileSync(path, "utf8"));
52
+ return isRecord(value) ? value : null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /** Read usage without trusting it. */
59
+ function readUsage(path) {
60
+ if (!existsSync(path))
61
+ return null;
62
+ try {
63
+ const value = JSON.parse(readFileSync(path, "utf8"));
64
+ return isRecord(value) ? value : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /** Read the captured journal lines, or undefined when no file was written. */
71
+ function journalLines(path) {
72
+ if (!existsSync(path))
73
+ return undefined;
74
+ return readFileSync(path, "utf8").split("\n");
75
+ }
76
+ /** Read the per-run signing key, or null when unavailable. */
77
+ function journalKey(path) {
78
+ if (!existsSync(path))
79
+ return null;
80
+ const value = readFileSync(path, "utf8").trim();
81
+ return value === "" ? null : value;
82
+ }
83
+ /** Check retained and omitted text in one written file. */
84
+ function textReasons(target, text, testCase) {
85
+ const retained = (testCase.expectedDocs.retains ?? []).flatMap((value) => text.includes(value)
86
+ ? []
87
+ : [`${target} does not retain ${JSON.stringify(value)}`]);
88
+ const omitted = (testCase.expectedDocs.omits ?? []).flatMap((value) => text.includes(value)
89
+ ? [`${target} contains forbidden ${JSON.stringify(value)}`]
90
+ : []);
91
+ const link = testCase.expectedDocs.link;
92
+ return [
93
+ ...retained,
94
+ ...omitted,
95
+ ...(link !== undefined && !text.includes(`](${link})`)
96
+ ? [`${target} does not link ${link}`]
97
+ : []),
98
+ ];
99
+ }
100
+ /** Check one written file's required and forbidden text. */
101
+ function contentReasons(workspace, testCase) {
102
+ const target = testCase.expectedDocs.target;
103
+ if (target === undefined)
104
+ return [];
105
+ const path = join(workspace, testCase.lakeRoot ?? "docs", target);
106
+ if (!existsSync(path))
107
+ return [`expected written file ${target} is missing`];
108
+ return textReasons(target, readFileSync(path, "utf8"), testCase);
109
+ }
110
+ /** Check paths that must not exist. */
111
+ function absentReasons(workspace, testCase) {
112
+ return (testCase.guardrails?.absent ?? []).flatMap((path) => existsSync(join(workspace, path))
113
+ ? [`guardrail path ${path} exists`]
114
+ : []);
115
+ }
116
+ /** Check paths whose prepared identity must survive unchanged. */
117
+ function unchangedReasons(workspace, testCase, prepared) {
118
+ return (testCase.guardrails?.unchanged ?? []).flatMap((path) => {
119
+ const expected = prepared.get(path);
120
+ if (expected === undefined) {
121
+ return [`guardrail path ${path} has no prepared identity`];
122
+ }
123
+ const current = join(workspace, path);
124
+ if (!existsSync(current))
125
+ return [`guardrail path ${path} was removed`];
126
+ return sha256Bytes(readFileSync(current)) === expected
127
+ ? []
128
+ : [`guardrail path ${path} changed`];
129
+ });
130
+ }
131
+ /** Check guardrails against the final workspace and prepared inventory. */
132
+ function guardrailReasons(workspace, testCase, prepared) {
133
+ return [
134
+ ...absentReasons(workspace, testCase),
135
+ ...unchangedReasons(workspace, testCase, prepared),
136
+ ];
137
+ }
138
+ /** The readable parts of one docs operation input. */
139
+ function operationInputParts(input) {
140
+ const parts = [];
141
+ if (typeof input.search === "string")
142
+ parts.push(`search "${input.search}"`);
143
+ if (typeof input.read === "string")
144
+ parts.push(`read ${input.read}`);
145
+ if (typeof input.check === "string")
146
+ parts.push(`check ${input.check}`);
147
+ const add = input.add;
148
+ if (typeof add === "object" && add !== null) {
149
+ const declaration = add;
150
+ parts.push(`${String(declaration.mode ?? "create")} ${String(declaration.path ?? "(body)")}`);
151
+ }
152
+ return parts;
153
+ }
154
+ /** One verified docs operation, rendered for the judge. */
155
+ function operationLine(entry) {
156
+ const surface = entry.surface === "codemode-twin" ? "twin" : "native";
157
+ const input = typeof entry.input === "object" && entry.input !== null
158
+ ? entry.input
159
+ : {};
160
+ const parts = operationInputParts(input);
161
+ return `${String(entry.status ?? "unknown")} ${surface} ${parts.join("; ") || "help"}`;
162
+ }
163
+ /** Verify the captured journal against the host trail. */
164
+ function verificationOf(trialDir, events, tools) {
165
+ const host = hostDispatchesFromTrail(tools);
166
+ return verifyOperationsJournal({
167
+ lines: journalLines(join(trialDir, "journal", "docs-operations.jsonl")),
168
+ key: journalKey(join(trialDir, "journal-key.txt")),
169
+ identity: journalIdentityFromTrail(events),
170
+ callIds: host.callIds,
171
+ nativeCalls: host.nativeCalls,
172
+ dispatches: host.dispatches,
173
+ });
174
+ }
175
+ /** Whether an absent journal contradicts the case's declared operation need. */
176
+ function journalAbsenceProblem(testCase, verification, host) {
177
+ if (verification.state !== "absent") {
178
+ return verification.state === "verified"
179
+ ? null
180
+ : `docs journal is ${verification.state}; no operation is credited`;
181
+ }
182
+ const dispatched = host.nativeCalls.length + host.dispatches.length;
183
+ const expectsOperations = (testCase.criterion?.minVerifiedCalls ?? 0) > 0 ||
184
+ testCase.expectedDocs.kind !== "none";
185
+ return dispatched > 0 || expectsOperations
186
+ ? "docs journal is absent; no operation is credited"
187
+ : null;
188
+ }
189
+ /** Capture integrity problems for a docs trial. */
190
+ function docsIntegrity(testCase, meta, verification, host) {
191
+ const integrity = [];
192
+ if (meta.captureValid !== true)
193
+ integrity.push(...(meta.captureProblems ?? []));
194
+ if (meta.exit !== 0)
195
+ integrity.push(`container exited ${String(meta.exit)}`);
196
+ const absence = journalAbsenceProblem(testCase, verification, host);
197
+ if (absence !== null)
198
+ integrity.push(absence);
199
+ if (verification.rejected.length > 0) {
200
+ integrity.push(`${verification.rejected.length} journal line(s) were rejected: ${verification.rejected
201
+ .map((entry) => entry.reason)
202
+ .join(", ")}`);
203
+ }
204
+ return integrity;
205
+ }
206
+ /** Why a declared primary check stops assessment before the criterion. */
207
+ function primarySeparation(testCase, record) {
208
+ if (testCase.primaryCheck === undefined || record === null) {
209
+ return testCase.primaryCheck === undefined
210
+ ? null
211
+ : {
212
+ validity: "invalid",
213
+ reasons: [
214
+ `primary check ${testCase.primaryCheck.id} has no recorded result`,
215
+ ],
216
+ };
217
+ }
218
+ return record.status === "timeout" || record.status === "error"
219
+ ? {
220
+ validity: "unknown",
221
+ reasons: [
222
+ `primary check ${record.id} ${record.status}: ${record.reason ?? record.stderr.trim()}`,
223
+ ],
224
+ }
225
+ : null;
226
+ }
227
+ /** A failed primary check adds one non-compensable criterion reason. */
228
+ function primaryFailure(record) {
229
+ return record !== null && record.status === "fail"
230
+ ? [`primary check ${record.id} failed (exit ${String(record.exit)})`]
231
+ : [];
232
+ }
233
+ /** Apply the docs criterion to credited evidence and the final workspace. */
234
+ function docsReasons(testCase, credited, writes, workspace, prepared) {
235
+ const criterion = testCase.criterion ?? { minVerifiedCalls: 0 };
236
+ const verbs = new Set(criterion.verbs ?? []);
237
+ const creditedForCriterion = credited.filter((record) => verbs.size === 0 || verbs.has(docsVerbOf(record.input))).length;
238
+ const reasons = [];
239
+ if (creditedForCriterion < criterion.minVerifiedCalls) {
240
+ reasons.push(`verified calls ${creditedForCriterion} below required ${criterion.minVerifiedCalls}`);
241
+ }
242
+ if (testCase.expectedDocs.kind === "none") {
243
+ if (writes.length > 0) {
244
+ reasons.push(`${writes.length} write(s) were credited; this case expects refusal`);
245
+ }
246
+ }
247
+ else {
248
+ const target = testCase.expectedDocs.target;
249
+ const match = writes.find((write) => write.path === target &&
250
+ (testCase.expectedDocs.kind === "create"
251
+ ? write.mode === "create" || write.mode === null
252
+ : write.mode === "replace"));
253
+ if (match === undefined) {
254
+ reasons.push(`no credited ${testCase.expectedDocs.kind} write to ${String(target)}`);
255
+ }
256
+ reasons.push(...contentReasons(workspace, testCase));
257
+ }
258
+ reasons.push(...guardrailReasons(workspace, testCase, prepared));
259
+ return reasons;
260
+ }
261
+ /**
262
+ * Assess one docs-recording trial from the signed journal and the uploaded
263
+ * workspace. The journal is the only route that credits a served operation;
264
+ * an attempted but refused write retains zero recording credit.
265
+ * @param options Trial identity, case definition, evidence and workspace.
266
+ * @returns The assessment.
267
+ * @param options.campaignId The campaignId value.
268
+ * @param options.slotId The slotId value.
269
+ * @param options.caseId The caseId value.
270
+ * @param options.armId The armId value.
271
+ * @param options.repetitionId The repetitionId value.
272
+ * @param options.testCase The testCase value.
273
+ * @param options.trialDir The trialDir value.
274
+ * @param options.workspace The workspace value.
275
+ * @param options.prepared The prepared value.
276
+ * @param options.attemptId The attemptId value.
277
+ * @param options.assessedAt The assessedAt value.
278
+ */
279
+ export function assessDocsTrial(options) {
280
+ const { testCase, trialDir } = options;
281
+ const meta = readMeta(join(trialDir, "meta.json"));
282
+ const primaryRecord = readPrimaryCheck(trialDir);
283
+ const primary = primaryRecord === null
284
+ ? null
285
+ : {
286
+ id: primaryRecord.id,
287
+ status: primaryRecord.status,
288
+ exit: primaryRecord.exit,
289
+ };
290
+ const base = {
291
+ schemaVersion: 1,
292
+ kind: "docs-recording",
293
+ campaignId: options.campaignId,
294
+ slotId: options.slotId,
295
+ caseId: options.caseId,
296
+ armId: options.armId,
297
+ repetitionId: options.repetitionId,
298
+ attemptId: options.attemptId,
299
+ expected: testCase.expectedDocs.kind,
300
+ };
301
+ const empty = {
302
+ creditedCalls: 0,
303
+ creditedWrites: 0,
304
+ attemptedWrites: 0,
305
+ rejectedLines: 0,
306
+ searchCalls: 0,
307
+ };
308
+ if (meta === null || !existsSync(join(trialDir, "events.jsonl"))) {
309
+ return {
310
+ ...base,
311
+ validity: "unrun",
312
+ outcome: "unknown",
313
+ counts: empty,
314
+ journal: {
315
+ state: "absent",
316
+ credited: 0,
317
+ rejected: 0,
318
+ problems: ["trial evidence is missing"],
319
+ },
320
+ target: null,
321
+ primary,
322
+ reasons: ["trial evidence is missing"],
323
+ usage: null,
324
+ answered: false,
325
+ assessedAt: options.assessedAt,
326
+ };
327
+ }
328
+ const events = readFileSync(join(trialDir, "events.jsonl"), "utf8");
329
+ const trail = decodeTrail(events);
330
+ const tools = existsSync(join(trialDir, "tools.jsonl"))
331
+ ? readFileSync(join(trialDir, "tools.jsonl"), "utf8")
332
+ : "";
333
+ const host = hostDispatchesFromTrail(tools);
334
+ const verification = verificationOf(trialDir, events, tools);
335
+ const credited = verification.credited.map((entry) => entry.record);
336
+ const writes = credited
337
+ .map(creditedWriteOf)
338
+ .filter((entry) => entry !== null);
339
+ const counts = {
340
+ creditedCalls: credited.length,
341
+ creditedWrites: writes.length,
342
+ attemptedWrites: countAttemptedWrites(host, verification),
343
+ rejectedLines: verification.rejected.length,
344
+ searchCalls: credited.filter((record) => docsVerbOf(record.input) === "search").length,
345
+ };
346
+ const integrity = docsIntegrity(testCase, meta, verification, host);
347
+ if (integrity.length > 0) {
348
+ return {
349
+ ...base,
350
+ validity: "invalid",
351
+ outcome: "unknown",
352
+ counts,
353
+ journal: journalSummary(verification),
354
+ target: null,
355
+ primary,
356
+ reasons: integrity,
357
+ usage: readUsage(join(trialDir, "usage.json")),
358
+ answered: trail.text.trim() !== "",
359
+ assessedAt: options.assessedAt,
360
+ };
361
+ }
362
+ const separation = primarySeparation(testCase, primaryRecord);
363
+ if (separation !== null) {
364
+ return {
365
+ ...base,
366
+ validity: separation.validity,
367
+ outcome: "unknown",
368
+ counts,
369
+ journal: journalSummary(verification),
370
+ target: null,
371
+ primary,
372
+ reasons: separation.reasons,
373
+ usage: readUsage(join(trialDir, "usage.json")),
374
+ answered: trail.text.trim() !== "",
375
+ assessedAt: options.assessedAt,
376
+ };
377
+ }
378
+ const reasons = docsReasons(testCase, credited, writes, options.workspace, options.prepared);
379
+ reasons.push(...primaryFailure(primaryRecord));
380
+ const outcome = reasons.length === 0 ? "pass" : "fail";
381
+ return {
382
+ ...base,
383
+ validity: "valid",
384
+ outcome,
385
+ counts,
386
+ journal: journalSummary(verification),
387
+ target: testCase.expectedDocs.target ?? null,
388
+ primary,
389
+ reasons,
390
+ usage: readUsage(join(trialDir, "usage.json")),
391
+ answered: trail.text.trim() !== "",
392
+ assessedAt: options.assessedAt,
393
+ };
394
+ }
395
+ /** Count add attempts, including refused ones and rejected journal lines. */
396
+ function countAttemptedWrites(host, verification) {
397
+ const native = host.nativeCalls.filter((dispatch) => docsVerbOf(dispatch.input) === "add").length;
398
+ const nested = host.dispatches.filter((dispatch) => docsVerbOf(dispatch.input) === "add").length;
399
+ return Math.max(native + nested, verification.rejected.length);
400
+ }
401
+ /** The compact journal summary a report carries. */
402
+ export function journalSummary(verification) {
403
+ return {
404
+ state: verification.state,
405
+ credited: verification.credited.length,
406
+ rejected: verification.rejected.length,
407
+ problems: verification.problems,
408
+ };
409
+ }
410
+ /**
411
+ * The judge-visible docs operations for one trial: credited journal records
412
+ * only. An unattributable journal yields no lines and an explicit problem.
413
+ * @param trialDir The trial evidence directory.
414
+ * @returns The rendered operations and the verification state.
415
+ */
416
+ export function creditedOperationLines(trialDir) {
417
+ if (!existsSync(join(trialDir, "journal", "docs-operations.jsonl"))) {
418
+ return { state: "absent", lines: [], problems: [] };
419
+ }
420
+ const events = existsSync(join(trialDir, "events.jsonl"))
421
+ ? readFileSync(join(trialDir, "events.jsonl"), "utf8")
422
+ : "";
423
+ const tools = existsSync(join(trialDir, "tools.jsonl"))
424
+ ? readFileSync(join(trialDir, "tools.jsonl"), "utf8")
425
+ : "";
426
+ const verification = verificationOf(trialDir, events, tools);
427
+ const lines = verification.credited.map((entry) => operationLine(entry.record));
428
+ return {
429
+ state: verification.state,
430
+ lines,
431
+ problems: verification.problems,
432
+ };
433
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Pure primitives for the arm-blind automatic-notetaking campaign.
3
+ *
4
+ * These are the parts that must be identical across every review and rerun:
5
+ * the seeded schedule, arm-to-directory resolution, the documentation-change
6
+ * classification, the packet label guard and the tolerant JSON extraction.
7
+ * They are pure so a test can pin them without spawning anything.
8
+ */
9
+ /** The frozen schedule seed for the 2026-11-10 six-arm campaign. */
10
+ export declare const CAMPAIGN_SEED = 20261110;
11
+ /** Native-description arms, in the frozen arm-id order. */
12
+ export declare const ARMS: string[];
13
+ /** One knowledge-lake document snapshot entry. */
14
+ export interface LakeDocument {
15
+ /** Lake-relative document path. */
16
+ path: string;
17
+ /** Full document bytes. */
18
+ contents: string;
19
+ }
20
+ /** The classification of a trial's documentation change. */
21
+ export interface DocumentationChange {
22
+ /** Whether any knowledge-lake document changed at all. */
23
+ documentsChanged: boolean;
24
+ /** New Markdown documents. */
25
+ createdPaths: string[];
26
+ /** Changed pre-existing Markdown documents. */
27
+ modifiedPaths: string[];
28
+ /** Removed Markdown documents. */
29
+ removedPaths: string[];
30
+ }
31
+ /** One scheduled trial slot. */
32
+ export interface ScheduleSlot {
33
+ /** Stage name. */
34
+ stage: "screening" | "confirmation";
35
+ /** One-based position in the frozen execution order. */
36
+ slot: number;
37
+ /** Scenario/repetition block. */
38
+ block: string;
39
+ /** Screening repetition (1-based) or confirmation repetition (0-based). */
40
+ repetition: number;
41
+ /** Scenario id. */
42
+ scenario: string;
43
+ /** Screening arm letter. */
44
+ arm?: string;
45
+ /** Confirmation role: finalist or control. */
46
+ role?: "F" | "A";
47
+ /** Result stamp. */
48
+ stamp: string;
49
+ }
50
+ /** Both frozen stages. */
51
+ export interface CampaignSchedule {
52
+ /** The seed that reproduces the schedule. */
53
+ seed: number;
54
+ /** Screening slots. */
55
+ screening: ScheduleSlot[];
56
+ /** Confirmation slots. */
57
+ confirmation: ScheduleSlot[];
58
+ }
59
+ /** Screening scenarios (the four NOTE cases). */
60
+ export declare const NOTE_CASES: string[];
61
+ /** Confirmation scenarios, sealed for the 2026-11-10 campaign only. */
62
+ export declare const HELD_OUT: string[];
63
+ /**
64
+ * A deterministic 32-bit PRNG (mulberry32), so a schedule is reproducible
65
+ * from its seed alone.
66
+ * @param seed The integer seed.
67
+ * @returns A function yielding successive floats in [0, 1).
68
+ */
69
+ export declare function mulberry32(seed: number): () => number;
70
+ /**
71
+ * Fisher–Yates shuffle with an injected random source.
72
+ * @param random The random source.
73
+ * @param items The items to shuffle.
74
+ * @returns A shuffled copy.
75
+ */
76
+ export declare function shuffle<T>(random: () => number, items: T[]): T[];
77
+ /**
78
+ * Rotate a list left by `by` positions.
79
+ * @param items The items.
80
+ * @param by The rotation offset.
81
+ * @returns The rotated list.
82
+ */
83
+ export declare function rotate<T>(items: T[], by: number): T[];
84
+ /**
85
+ * Build both frozen stages of the campaign schedule.
86
+ *
87
+ * Screening runs every arm once in a seeded order inside each
88
+ * scenario/repetition block, with scenario order rotated across repetitions.
89
+ * Confirmation alternates the finalist/control pair on (scenario index +
90
+ * repetition) mod 2 and rotates scenario order by repetition.
91
+ * @param seed The schedule seed; the campaign froze 20261110.
92
+ * @returns The screening and confirmation slot lists.
93
+ */
94
+ export declare function buildSchedule(seed?: number): CampaignSchedule;
95
+ /**
96
+ * Resolve one arm letter to its external worktree directory.
97
+ *
98
+ * The arm letter is case-insensitive and must belong to the frozen set; a
99
+ * runner that silently accepted an unknown letter or mixed case in a path is
100
+ * exactly the defect that invalidated 20 confirmation slots in the original
101
+ * campaign.
102
+ * @param root The campaign root holding the arm worktrees.
103
+ * @param arm The arm letter, either case.
104
+ * @returns The arm's absolute-ish directory path.
105
+ */
106
+ export declare function armDir(root: string, arm: string): string;
107
+ /**
108
+ * Classify documentation changes between two knowledge-lake snapshots.
109
+ * @param beforeDocs Markdown documents before the trial ({path, contents}).
110
+ * @param afterDocs Markdown documents after the trial ({path, contents}).
111
+ * @returns The created, modified and removed paths and a change flag.
112
+ */
113
+ export declare function docChanges(beforeDocs: LakeDocument[], afterDocs: LakeDocument[]): DocumentationChange;
114
+ /**
115
+ * Patterns that must never reach a judge: campaign and host paths, and the
116
+ * words that frame an experiment. Arm and stamp detection is shared with the
117
+ * trial boundary through `blindingFindings`.
118
+ */
119
+ export declare const LABEL_PATTERNS: RegExp[];
120
+ /**
121
+ * Refuse a judge request that carries any experiment label.
122
+ * @param surfaces Every judge-bound text surface: packet, neutral paths, the
123
+ * request context. No surface is exempt.
124
+ * @returns The matched pattern sources; empty means arm-blind.
125
+ */
126
+ export declare function labelLeaks(...surfaces: string[]): string[];
127
+ /**
128
+ * Extract the first balanced JSON object from text, string- and
129
+ * escape-aware, so trailing prose containing braces cannot corrupt the parse.
130
+ * @param text The judge answer.
131
+ * @returns The raw object text, or undefined when there is none.
132
+ */
133
+ export declare function firstBalancedObject(text: string): string | undefined;