@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,45 @@
1
+ import { assessSkillTrial } from "../evaluators/skills/assess.ts";
2
+ import { assessDocsTrial } from "../evaluators/docs/assess.ts";
3
+ /** One stored assessment. */
4
+ export type StoredAssessment = ReturnType<typeof assessSkillTrial> | ReturnType<typeof assessDocsTrial>;
5
+ /** Aggregate counts for a status/report line. */
6
+ export interface AssessmentSummary {
7
+ /** The assessed value. */
8
+ assessed: number;
9
+ /** The pass value. */
10
+ pass: number;
11
+ /** The fail value. */
12
+ fail: number;
13
+ /** The unknown value. */
14
+ unknown: number;
15
+ /** The invalid value. */
16
+ invalid: number;
17
+ /** The unrun value. */
18
+ unrun: number;
19
+ }
20
+ /** The result of one assess invocation. */
21
+ export interface AssessResult {
22
+ /** The ok value. */
23
+ ok: boolean;
24
+ /** The summary value. */
25
+ summary: AssessmentSummary;
26
+ /** The problems value. */
27
+ problems: string[];
28
+ }
29
+ /** Path of one deterministic assessment. */
30
+ export declare function assessmentPath(root: string, slotId: string): string;
31
+ /**
32
+ * Deterministically assess every scheduled slot from captured evidence.
33
+ * Deterministic suites make zero model calls.
34
+ * @param root The frozen campaign root.
35
+ * @param options Slot selection and injected clock.
36
+ * @param options.slotIds The selected slots.
37
+ * @param options.now The injected clock.
38
+ * @returns Aggregate counts and any refusals.
39
+ */
40
+ export declare function assessCampaign(root: string, options?: {
41
+ slotIds?: readonly string[];
42
+ now?: () => string;
43
+ }): AssessResult;
44
+ /** Read all stored assessments, sorted by slot id. */
45
+ export declare function readAssessments(root: string): StoredAssessment[];
@@ -0,0 +1,208 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { canonicalJson, sha256Of } from "../contracts/canonical.js";
4
+ import { isRecord } from "../contracts/validate.js";
5
+ import { loadPreparedManifest } from "./prepare.js";
6
+ import { loadFrozen } from "./freeze.js";
7
+ import { appendLedgerEvent, readLedger } from "./ledger.js";
8
+ import { trialDirectory } from "./run.js";
9
+ import { assessSkillTrial } from "../evaluators/skills/assess.js";
10
+ import { assessDocsTrial } from "../evaluators/docs/assess.js";
11
+ /** An empty aggregate. */
12
+ function emptySummary() {
13
+ return { assessed: 0, pass: 0, fail: 0, unknown: 0, invalid: 0, unrun: 0 };
14
+ }
15
+ /** Path of one deterministic assessment. */
16
+ export function assessmentPath(root, slotId) {
17
+ return join(root, "evidence", "assessments", `${slotId}.json`);
18
+ }
19
+ /**
20
+ * Write an assessment immutably: an existing different record is refused, not
21
+ * overwritten. Reassessing unchanged evidence is idempotent.
22
+ * @param path The assessment path.
23
+ * @param assessment The assessment to store.
24
+ * @returns Null on success, or the refusal.
25
+ */
26
+ function storeAssessment(path, assessment) {
27
+ const text = `${JSON.stringify(assessment, null, 2)}\n`;
28
+ if (!existsSync(path)) {
29
+ mkdirSync(join(path, ".."), { recursive: true });
30
+ writeFileSync(path, text, "utf8");
31
+ return null;
32
+ }
33
+ const existing = readFileSync(path, "utf8");
34
+ try {
35
+ const parsed = JSON.parse(existing);
36
+ if (isRecord(parsed) &&
37
+ canonicalJson(parsed) === canonicalJson(assessment)) {
38
+ return null;
39
+ }
40
+ }
41
+ catch {
42
+ return `${path} is unreadable; refusing to overwrite it`;
43
+ }
44
+ return `${path} already holds a different assessment; historical assessments are immutable`;
45
+ }
46
+ /** Tally one assessment. */
47
+ function tally(summary, assessment) {
48
+ summary.assessed += 1;
49
+ switch (assessment.outcome) {
50
+ case "pass":
51
+ summary.pass += 1;
52
+ break;
53
+ case "fail":
54
+ summary.fail += 1;
55
+ break;
56
+ default:
57
+ summary.unknown += 1;
58
+ }
59
+ if (assessment.validity === "invalid")
60
+ summary.invalid += 1;
61
+ if (assessment.validity === "unrun")
62
+ summary.unrun += 1;
63
+ }
64
+ /**
65
+ * Deterministically assess every scheduled slot from captured evidence.
66
+ * Deterministic suites make zero model calls.
67
+ * @param root The frozen campaign root.
68
+ * @param options Slot selection and injected clock.
69
+ * @returns Aggregate counts and any refusals.
70
+ * @param options.slotIds The slotIds value.
71
+ * @param options.now The now value.
72
+ */
73
+ /** Why assessment is refused now, or null when it may proceed. */
74
+ function assessmentRefusal(root) {
75
+ const frozen = loadFrozen(root);
76
+ if (!frozen.ok)
77
+ return frozen.problem;
78
+ const ledger = readLedger(root);
79
+ return ledger.state === "malformed"
80
+ ? `attempt ledger is malformed: ${ledger.problems.join("; ")}`
81
+ : null;
82
+ }
83
+ /** Build the deterministic assessment for one slot. */
84
+ function assessmentForSlot(root, manifest, slot, now) {
85
+ const testCase = manifest.suite.cases.find((entry) => entry.caseId === slot.caseId);
86
+ if (testCase === undefined)
87
+ return null;
88
+ const trialDir = trialDirectory(root, slot.slotId);
89
+ const common = {
90
+ campaignId: manifest.campaignId,
91
+ slotId: slot.slotId,
92
+ caseId: slot.caseId,
93
+ armId: slot.armId,
94
+ repetitionId: slot.repetitionId,
95
+ attemptId: `deterministic-${slot.slotId}`,
96
+ assessedAt: now(),
97
+ };
98
+ if (testCase.kind === "skill-trigger") {
99
+ return assessSkillTrial({ ...common, testCase, trialDir });
100
+ }
101
+ const preparedCase = manifest.cases.find((entry) => entry.caseId === slot.caseId);
102
+ return assessDocsTrial({
103
+ ...common,
104
+ testCase,
105
+ trialDir,
106
+ workspace: join(trialDir, "workspace"),
107
+ prepared: new Map((preparedCase?.workspace ?? []).map((entry) => [
108
+ entry.path,
109
+ entry.sha256,
110
+ ])),
111
+ });
112
+ }
113
+ /**
114
+ * Deterministically assess every scheduled slot from captured evidence.
115
+ * Deterministic suites make zero model calls.
116
+ * @param root The frozen campaign root.
117
+ * @param options Slot selection and injected clock.
118
+ * @returns Aggregate counts and any refusals.
119
+ * @param options.slotIds The slotIds value.
120
+ * @param options.now The now value.
121
+ */
122
+ /** Store one assessment and append its ledger event once. */
123
+ function recordAssessment(root, manifest, slot, now, recorded) {
124
+ const assessment = assessmentForSlot(root, manifest, slot, now);
125
+ if (assessment === null) {
126
+ return {
127
+ assessment: null,
128
+ problems: [`slot ${slot.slotId} has no case ${slot.caseId}`],
129
+ };
130
+ }
131
+ const refusal = storeAssessment(assessmentPath(root, slot.slotId), assessment);
132
+ if (refusal !== null) {
133
+ return { assessment: null, problems: [refusal] };
134
+ }
135
+ if (!recorded.has(assessment.attemptId)) {
136
+ appendLedgerEvent(root, {
137
+ event: "assessment",
138
+ campaignId: manifest.campaignId,
139
+ slotId: slot.slotId,
140
+ attemptId: assessment.attemptId,
141
+ outcome: assessment.outcome,
142
+ validity: assessment.validity,
143
+ assessmentSha256: sha256Of(assessment),
144
+ }, assessment.assessedAt);
145
+ recorded.add(assessment.attemptId);
146
+ }
147
+ return { assessment, problems: [] };
148
+ }
149
+ /**
150
+ * Deterministically assess every scheduled slot from captured evidence.
151
+ * Deterministic suites make zero model calls.
152
+ * @param root The frozen campaign root.
153
+ * @param options Slot selection and injected clock.
154
+ * @param options.slotIds The selected slots.
155
+ * @param options.now The injected clock.
156
+ * @returns Aggregate counts and any refusals.
157
+ */
158
+ export function assessCampaign(root, options = {}) {
159
+ const prepared = loadPreparedManifest(root);
160
+ if (!prepared.ok) {
161
+ return {
162
+ ok: false,
163
+ summary: emptySummary(),
164
+ problems: [prepared.problem],
165
+ };
166
+ }
167
+ const refusal = assessmentRefusal(root);
168
+ if (refusal !== null) {
169
+ return { ok: false, summary: emptySummary(), problems: [refusal] };
170
+ }
171
+ const ledger = readLedger(root);
172
+ const now = options.now ?? (() => new Date().toISOString());
173
+ const summary = emptySummary();
174
+ const problems = [];
175
+ const recorded = new Set(ledger.events
176
+ .filter((event) => event.event === "assessment")
177
+ .map((event) => typeof event.attemptId === "string" ? event.attemptId : ""));
178
+ const manifest = prepared.manifest;
179
+ const slots = manifest.campaign.schedule.slots.filter((slot) => options.slotIds === undefined ||
180
+ options.slotIds.includes(slot.slotId));
181
+ for (const slot of slots) {
182
+ const result = recordAssessment(root, manifest, slot, now, recorded);
183
+ problems.push(...result.problems);
184
+ if (result.assessment !== null)
185
+ tally(summary, result.assessment);
186
+ }
187
+ return { ok: problems.length === 0, summary, problems };
188
+ }
189
+ /** Read all stored assessments, sorted by slot id. */
190
+ export function readAssessments(root) {
191
+ const directory = join(root, "evidence", "assessments");
192
+ if (!existsSync(directory))
193
+ return [];
194
+ return readdirSync(directory)
195
+ .filter((name) => name.endsWith(".json"))
196
+ .sort()
197
+ .flatMap((name) => {
198
+ try {
199
+ const value = JSON.parse(readFileSync(join(directory, name), "utf8"));
200
+ return isRecord(value)
201
+ ? [value]
202
+ : [];
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ });
208
+ }
@@ -0,0 +1,17 @@
1
+ /** The closure outcome. */
2
+ export type CloseResult = {
3
+ ok: true;
4
+ alreadyClosed: boolean;
5
+ closedAt: string;
6
+ } | {
7
+ ok: false;
8
+ problems: string[];
9
+ };
10
+ /**
11
+ * Close a campaign's ledger. Closure is an append-only event and is
12
+ * idempotent; a malformed ledger or an unresolved launch refusal prevents it.
13
+ * @param root The campaign root.
14
+ * @param now Injected clock for tests.
15
+ * @returns The closure outcome.
16
+ */
17
+ export declare function closeCampaign(root: string, now?: () => string): CloseResult;
@@ -0,0 +1,112 @@
1
+ import { appendLedgerEvent, readLedger, trialStarts } from "./ledger.js";
2
+ import { loadPreparedManifest } from "./prepare.js";
3
+ import { loadFrozen } from "./freeze.js";
4
+ import { readAssessments } from "./assess.js";
5
+ import { checkedSelection, packetIntegrityProblems, policyOf, readJudgeAttempts, } from "./judge.js";
6
+ import { readClaims } from "../evaluators/docs/analysis/budget.js";
7
+ import { join } from "node:path";
8
+ /** Unsettled spend claims that must stop closure, or an empty list. */
9
+ function unsettledClaims(root) {
10
+ return ["judge", "recovery"].flatMap((kind) => readClaims(join(root, "evidence"), kind)
11
+ .filter((claim) => claim.state !== "closed")
12
+ .map((claim) => `${kind} claim ${claim.index} is ${claim.state}${claim.stamp === null ? "" : ` for ${claim.stamp}`}`));
13
+ }
14
+ /** Completed slots with neither a selected judgement nor a terminal failure. */
15
+ function unjudgedCompleted(root, manifest, completed) {
16
+ if (manifest.campaign.assessment.judge === undefined)
17
+ return [];
18
+ const policy = policyOf(manifest);
19
+ return manifest.campaign.schedule.slots.flatMap((slot) => {
20
+ if (!completed.has(slot.slotId))
21
+ return [];
22
+ const checked = policy.ok
23
+ ? checkedSelection({
24
+ root,
25
+ manifest,
26
+ slot,
27
+ policy: policy.policy,
28
+ })
29
+ : { ok: false, problems: policy.problems };
30
+ if (checked.ok)
31
+ return [];
32
+ const terminal = readJudgeAttempts(root, slot.slotId).some((attempt) => attempt.judgementStatus === "invalid-verdict" ||
33
+ attempt.judgementStatus === "judge-error");
34
+ return terminal ? [] : [slot.slotId];
35
+ });
36
+ }
37
+ /** Every reason closure must refuse now, in reporting order. */
38
+ function closureRefusals(root, manifest, starts, ended) {
39
+ const unresolved = starts
40
+ .map((event) => (typeof event.slotId === "string" ? event.slotId : ""))
41
+ .filter((slotId) => !ended.has(slotId));
42
+ if (unresolved.length > 0) {
43
+ return [
44
+ `unresolved trial launch(es): ${unresolved.join(", ")}; closure would hide unknown spend`,
45
+ ];
46
+ }
47
+ const claims = unsettledClaims(root);
48
+ if (claims.length > 0) {
49
+ return [
50
+ `unsettled judge spend: ${claims.join("; ")}; closure would hide an unknown outcome`,
51
+ ];
52
+ }
53
+ const packetProblems = manifest.campaign.schedule.slots.flatMap((slot) => packetIntegrityProblems(root, manifest, slot).map((problem) => `${slot.slotId}: ${problem}`));
54
+ if (packetProblems.length > 0) {
55
+ return ["judge packet integrity failed", ...packetProblems];
56
+ }
57
+ const policy = policyOf(manifest);
58
+ if (!policy.ok)
59
+ return policy.problems;
60
+ const pending = unjudgedCompleted(root, manifest, ended);
61
+ return pending.length > 0
62
+ ? [
63
+ `unjudged completed slot(s): ${pending.join(", ")}; run otc judge before closure`,
64
+ ]
65
+ : [];
66
+ }
67
+ /**
68
+ * Close a campaign's ledger. Closure is an append-only event and is
69
+ * idempotent; a malformed ledger or an unresolved launch refusal prevents it.
70
+ * @param root The campaign root.
71
+ * @param now Injected clock for tests.
72
+ * @returns The closure outcome.
73
+ */
74
+ export function closeCampaign(root, now = () => new Date().toISOString()) {
75
+ const prepared = loadPreparedManifest(root);
76
+ if (!prepared.ok)
77
+ return { ok: false, problems: [prepared.problem] };
78
+ const frozen = loadFrozen(root);
79
+ if (!frozen.ok)
80
+ return { ok: false, problems: [frozen.problem] };
81
+ const ledger = readLedger(root);
82
+ if (ledger.state === "malformed") {
83
+ return { ok: false, problems: ledger.problems };
84
+ }
85
+ if (ledger.state === "closed") {
86
+ return {
87
+ ok: true,
88
+ alreadyClosed: true,
89
+ closedAt: ledger.closedAt ?? "unknown",
90
+ };
91
+ }
92
+ const starts = trialStarts(ledger.events);
93
+ const ended = new Set(ledger.events
94
+ .filter((event) => event.event === "trial-end")
95
+ .map((event) => typeof event.slotId === "string" ? event.slotId : ""));
96
+ const refusals = closureRefusals(root, prepared.manifest, starts, ended);
97
+ if (refusals.length > 0) {
98
+ return { ok: false, problems: refusals };
99
+ }
100
+ const assessments = readAssessments(root);
101
+ const closedAt = now();
102
+ appendLedgerEvent(root, {
103
+ event: "close",
104
+ campaignId: prepared.manifest.campaignId,
105
+ closedAt,
106
+ trialStarts: starts.length,
107
+ trialEnds: ended.size,
108
+ assessments: assessments.length,
109
+ slots: prepared.manifest.campaign.schedule.slots.length,
110
+ }, closedAt);
111
+ return { ok: true, alreadyClosed: false, closedAt };
112
+ }
@@ -0,0 +1,24 @@
1
+ /** The outcome of a guarded evidence operation. */
2
+ export interface EvidenceGuardResult {
3
+ /** Whether the operation was allowed. */
4
+ ok: boolean;
5
+ /** Human-readable reason, safe to log. */
6
+ reason: string;
7
+ /** Entries that protect the directory when the operation refused. */
8
+ entries?: string[];
9
+ }
10
+ /** Everything in a directory except the reservation marker. */
11
+ export declare function recordedEntries(directory: string): string[];
12
+ /**
13
+ * Clear one abandoned reservation without touching recorded evidence.
14
+ * @param directory The result directory holding the reservation.
15
+ * @returns The outcome and, on refusal, the entries that protect the result.
16
+ */
17
+ export declare function clearReservation(directory: string): EvidenceGuardResult;
18
+ /**
19
+ * Claim a new attempt directory. An existing directory is never reused or
20
+ * overwritten, even when it is empty.
21
+ * @param directory The attempt directory to create.
22
+ * @returns Whether the claim succeeded or why it was refused.
23
+ */
24
+ export declare function claimAttemptDir(directory: string): EvidenceGuardResult;
@@ -0,0 +1,49 @@
1
+ // Evidence-preservation guards for recorded trial and judge attempts.
2
+ //
3
+ // A reservation is a lock marker, never evidence. Clearing one must remove
4
+ // only `.reservation` and must refuse when any other entry exists. An existing
5
+ // attempt directory is immutable: a second claim never overwrites its bytes.
6
+ import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ /** Everything in a directory except the reservation marker. */
9
+ export function recordedEntries(directory) {
10
+ if (!existsSync(directory))
11
+ return [];
12
+ return readdirSync(directory).filter((name) => name !== ".reservation");
13
+ }
14
+ /**
15
+ * Clear one abandoned reservation without touching recorded evidence.
16
+ * @param directory The result directory holding the reservation.
17
+ * @returns The outcome and, on refusal, the entries that protect the result.
18
+ */
19
+ export function clearReservation(directory) {
20
+ if (!existsSync(directory))
21
+ return { ok: false, reason: "directory does not exist" };
22
+ if (!existsSync(join(directory, ".reservation")))
23
+ return { ok: false, reason: "no reservation present" };
24
+ const entries = recordedEntries(directory);
25
+ if (entries.length > 0)
26
+ return {
27
+ ok: false,
28
+ reason: "recorded evidence is present; refusing to clear",
29
+ entries,
30
+ };
31
+ rmSync(join(directory, ".reservation"));
32
+ return { ok: true, reason: "reservation cleared; no evidence removed" };
33
+ }
34
+ /**
35
+ * Claim a new attempt directory. An existing directory is never reused or
36
+ * overwritten, even when it is empty.
37
+ * @param directory The attempt directory to create.
38
+ * @returns Whether the claim succeeded or why it was refused.
39
+ */
40
+ export function claimAttemptDir(directory) {
41
+ if (existsSync(directory))
42
+ return {
43
+ ok: false,
44
+ reason: "attempt directory already exists; refusing to overwrite",
45
+ entries: recordedEntries(directory),
46
+ };
47
+ mkdirSync(directory, { recursive: false });
48
+ return { ok: true, reason: "attempt directory created" };
49
+ }
@@ -0,0 +1,94 @@
1
+ import { runCommand } from "../adapters/process.ts";
2
+ import { type PreparedManifest } from "./prepare.ts";
3
+ import { type ToolIdentity } from "../contracts/version.ts";
4
+ /** The immutable freeze receipt. */
5
+ export interface FrozenRecord {
6
+ /** The schema version value. */
7
+ schemaVersion: 1;
8
+ /** The kind value. */
9
+ kind: "otc-freeze";
10
+ /** The campaign id value. */
11
+ campaignId: string;
12
+ /** The suite id value. */
13
+ suiteId: string;
14
+ /** The freeze sha256 value. */
15
+ freezeSha256: string;
16
+ /** The manifest sha256 value. */
17
+ manifestSha256: string;
18
+ /** The campaign sha256 value. */
19
+ campaignSha256: string;
20
+ /** The suite sha256 value. */
21
+ suiteSha256: string;
22
+ /** The schedule sha256 value. */
23
+ scheduleSha256: string;
24
+ /** The subjects sha256 value. */
25
+ subjectsSha256: string;
26
+ /** The workspaces sha256 value. */
27
+ workspacesSha256: string;
28
+ /** The prompts sha256 value. */
29
+ promptsSha256: string;
30
+ /** The staged primary-check payload sha256 value. */
31
+ primaryChecksSha256: string;
32
+ /** The artefact that froze this campaign. */
33
+ tool: ToolIdentity;
34
+ /** The image value. */
35
+ image: string;
36
+ /** The image id value. */
37
+ imageId: string;
38
+ /** The opencode version value. */
39
+ opencodeVersion: string;
40
+ /** The provider value. */
41
+ provider: {
42
+ route: string;
43
+ model: string;
44
+ };
45
+ /** The frozen at value. */
46
+ frozenAt: string;
47
+ }
48
+ /** A freeze attempt's outcome. */
49
+ export type FreezeResult = {
50
+ ok: true;
51
+ record: FrozenRecord;
52
+ alreadyFrozen: boolean;
53
+ } | {
54
+ ok: false;
55
+ problems: string[];
56
+ };
57
+ /** The freeze fields whose change must refuse a later freeze/run. */
58
+ export type FreezeIdentity = Omit<FrozenRecord, "freezeSha256" | "frozenAt">;
59
+ /** The canonical freeze identity, independent of the freeze timestamp. */
60
+ export declare function freezeIdentity(record: FreezeIdentity): string;
61
+ /** Path of the freeze receipt. */
62
+ export declare function frozenPath(root: string): string;
63
+ /**
64
+ * Read a freeze receipt without trusting its shape.
65
+ * @param root The campaign root.
66
+ * @returns The record, or the reason it is unusable.
67
+ */
68
+ export declare function loadFrozen(root: string): {
69
+ ok: true;
70
+ record: FrozenRecord;
71
+ } | {
72
+ ok: false;
73
+ problem: string;
74
+ };
75
+ /** Strip volatile and derived fields from a freeze record. */
76
+ export declare function identityOf(record: FrozenRecord): FreezeIdentity;
77
+ /** Compare prepared inputs with a freeze receipt. */
78
+ export declare function freezeDifferences(manifest: PreparedManifest, frozen: FrozenRecord, current?: ToolIdentity): string[];
79
+ /** Check that prepared files and the stored campaign copy are intact. */
80
+ export declare function inputDifferences(root: string, manifest: PreparedManifest): string[];
81
+ /**
82
+ * Freeze a prepared campaign: resolve the image to its immutable identity,
83
+ * verify the CLI version it contains, and record all input identities.
84
+ * @param root The prepared campaign root.
85
+ * @param run Process adapter, injected for tests.
86
+ * @returns The receipt, or every refusal.
87
+ */
88
+ export declare function freezeCampaign(root: string, run?: typeof runCommand): Promise<FreezeResult>;
89
+ /**
90
+ * The read-only delivery check: is the frozen payload complete and unchanged?
91
+ * @param root The campaign root.
92
+ * @returns Differences; empty means delivery is intact.
93
+ */
94
+ export declare function deliveryCheck(root: string, current?: ToolIdentity): string[];