@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.
- 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/release-0.1.0.md +52 -0
- package/docs/releasing.md +82 -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,79 @@
|
|
|
1
|
+
/** The derived report document. */
|
|
2
|
+
export interface Report {
|
|
3
|
+
/** The schema version value. */
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
/** The campaign id value. */
|
|
6
|
+
campaignId: string;
|
|
7
|
+
/** The suite id value. */
|
|
8
|
+
suiteId: string;
|
|
9
|
+
/** The report sha256 value. */
|
|
10
|
+
reportSha256: string;
|
|
11
|
+
/** The closed at value. */
|
|
12
|
+
closedAt: string | null;
|
|
13
|
+
/** The image id value. */
|
|
14
|
+
imageId: string;
|
|
15
|
+
/** The generated at value. */
|
|
16
|
+
generatedAt: string;
|
|
17
|
+
/** The denominators value. */
|
|
18
|
+
denominators: {
|
|
19
|
+
scheduled: number;
|
|
20
|
+
assessed: number;
|
|
21
|
+
valid: number;
|
|
22
|
+
invalid: number;
|
|
23
|
+
unknown: number;
|
|
24
|
+
unrun: number;
|
|
25
|
+
judged: number;
|
|
26
|
+
unjudged: number;
|
|
27
|
+
};
|
|
28
|
+
/** The totals value. */
|
|
29
|
+
totals: {
|
|
30
|
+
pass: number;
|
|
31
|
+
fail: number;
|
|
32
|
+
unknown: number;
|
|
33
|
+
};
|
|
34
|
+
/** The slots value. */
|
|
35
|
+
slots: Array<{
|
|
36
|
+
slotId: string;
|
|
37
|
+
caseId: string;
|
|
38
|
+
armId: string;
|
|
39
|
+
repetitionId: string;
|
|
40
|
+
expected: string;
|
|
41
|
+
observed: string;
|
|
42
|
+
validity: string;
|
|
43
|
+
outcome: string;
|
|
44
|
+
reasons: string[];
|
|
45
|
+
counts: Record<string, number>;
|
|
46
|
+
usage: Record<string, unknown> | null;
|
|
47
|
+
judgement: {
|
|
48
|
+
selectedAttemptId: string;
|
|
49
|
+
attempts: number;
|
|
50
|
+
documentationOutcome: string | null;
|
|
51
|
+
utility: number | null;
|
|
52
|
+
status: string;
|
|
53
|
+
} | null;
|
|
54
|
+
judgeProblems: string[];
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
/** A report render result. */
|
|
58
|
+
export type ReportResult = {
|
|
59
|
+
ok: true;
|
|
60
|
+
report: Report;
|
|
61
|
+
markdown: string;
|
|
62
|
+
outDir: string;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
problems: string[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Render the final analysis from checked evidence. Requires a closed ledger
|
|
69
|
+
* and writes only to a fresh output directory outside the evidence tree.
|
|
70
|
+
* @param root The campaign root.
|
|
71
|
+
* @param outDir The fresh report directory.
|
|
72
|
+
* @param now Injected clock.
|
|
73
|
+
* @returns The report, or the refusals.
|
|
74
|
+
*/
|
|
75
|
+
export declare function analyseCampaign(root: string, outDir: string, now?: () => string): ReportResult;
|
|
76
|
+
/** The canonical identity of a report without its own identity field. */
|
|
77
|
+
export declare function reportIdentity(report: Report): string;
|
|
78
|
+
/** Ensure a report's identity still matches its content. */
|
|
79
|
+
export declare function reportIntact(report: Report): boolean;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sha256Of } from "../contracts/canonical.js";
|
|
4
|
+
import { isRecord } from "../contracts/validate.js";
|
|
5
|
+
import { loadPreparedManifest, } from "../campaign/prepare.js";
|
|
6
|
+
import { loadFrozen } from "../campaign/freeze.js";
|
|
7
|
+
import { readLedger } from "../campaign/ledger.js";
|
|
8
|
+
import { readAssessments } from "../campaign/assess.js";
|
|
9
|
+
import { checkedSelectionForSlot, readSelectedJudgement, } from "../campaign/judge.js";
|
|
10
|
+
import { trialDirectory } from "../campaign/run.js";
|
|
11
|
+
/** Read a trial's usage record, or null. */
|
|
12
|
+
function readUsage(root, slotId) {
|
|
13
|
+
const path = join(trialDirectory(root, slotId), "usage.json");
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
return isRecord(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The expected/observed pair for either evaluator. */
|
|
25
|
+
function observedOf(assessment) {
|
|
26
|
+
if (assessment.kind === "skill-activation") {
|
|
27
|
+
return {
|
|
28
|
+
expected: assessment.expected,
|
|
29
|
+
observed: assessment.observed,
|
|
30
|
+
counts: assessment.counts,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
expected: assessment.expected,
|
|
35
|
+
observed: assessment.outcome,
|
|
36
|
+
counts: assessment.counts,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The selected judgement summary for one slot. */
|
|
40
|
+
function judgementOf(root, slotId) {
|
|
41
|
+
const selected = readSelectedJudgement(root, slotId);
|
|
42
|
+
if (selected === null)
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
selectedAttemptId: selected.selection.selectedAttemptId,
|
|
46
|
+
attempts: selected.selection.attempts.length,
|
|
47
|
+
documentationOutcome: selected.judgement.documentationOutcome,
|
|
48
|
+
utility: selected.judgement.utility,
|
|
49
|
+
status: selected.judgement.status,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** The packet and selection integrity diagnostics for one judged slot. */
|
|
53
|
+
function judgeProblemsOf(root, manifest, slotId) {
|
|
54
|
+
if (manifest.campaign.assessment.judge === undefined)
|
|
55
|
+
return [];
|
|
56
|
+
const checked = checkedSelectionForSlot(root, slotId);
|
|
57
|
+
return checked.ok ? [] : checked.problems;
|
|
58
|
+
}
|
|
59
|
+
/** The slot table rows. */
|
|
60
|
+
function slotRows(report) {
|
|
61
|
+
return report.slots.map((slot) => {
|
|
62
|
+
const counts = Object.entries(slot.counts)
|
|
63
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
64
|
+
.join(" ");
|
|
65
|
+
return `| ${slot.slotId} | ${slot.caseId} | ${slot.armId} | ${slot.repetitionId} | ${slot.expected} | ${slot.observed} | ${slot.validity} | ${slot.outcome} | ${counts} | ${slot.judgement === null ? "unjudged" : `${slot.judgement.documentationOutcome} (${String(slot.judgement.utility)}); ${slot.judgement.selectedAttemptId}`} |`;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** The refusal and unknown section, when any slot has a reason. */
|
|
69
|
+
function reasonSection(report) {
|
|
70
|
+
const reasons = report.slots.filter((slot) => slot.reasons.length > 0);
|
|
71
|
+
if (reasons.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
return [
|
|
74
|
+
"",
|
|
75
|
+
"## Refusals and unknowns",
|
|
76
|
+
"",
|
|
77
|
+
...reasons.map((slot) => `- **${slot.slotId}**: ${slot.reasons.join("; ")}`),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
/** The judge integrity section, when any slot has a diagnostic. */
|
|
81
|
+
function judgeIntegritySection(report) {
|
|
82
|
+
const problems = report.slots.filter((slot) => slot.judgeProblems.length > 0);
|
|
83
|
+
if (problems.length === 0)
|
|
84
|
+
return [];
|
|
85
|
+
return [
|
|
86
|
+
"",
|
|
87
|
+
"## Judge integrity",
|
|
88
|
+
"",
|
|
89
|
+
...problems.flatMap((slot) => slot.judgeProblems.map((problem) => `- **${slot.slotId}**: ${problem}`)),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
/** The per-run usage section. */
|
|
93
|
+
function usageSection(report) {
|
|
94
|
+
return report.slots.flatMap((slot) => {
|
|
95
|
+
const usage = slot.usage;
|
|
96
|
+
if (usage === null)
|
|
97
|
+
return [`- ${slot.slotId}: unknown (no usage record)`];
|
|
98
|
+
return [
|
|
99
|
+
`- ${slot.slotId}: input ${String(usage.inputTokens ?? "unknown")}, output ${String(usage.outputTokens ?? "unknown")}, cache read ${String(usage.cacheReadTokens ?? "unknown")}, reported ${String(usage.reported ?? false)}`,
|
|
100
|
+
];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Render the human-readable report. */
|
|
104
|
+
function markdownOf(report) {
|
|
105
|
+
return [
|
|
106
|
+
`# OTC report — ${report.campaignId}`,
|
|
107
|
+
"",
|
|
108
|
+
`- Suite: \`${report.suiteId}\``,
|
|
109
|
+
`- Image: \`${report.imageId}\``,
|
|
110
|
+
`- Closed: ${report.closedAt ?? "no"}`,
|
|
111
|
+
`- Generated: ${report.generatedAt}`,
|
|
112
|
+
`- Report identity: \`${report.reportSha256}\``,
|
|
113
|
+
"",
|
|
114
|
+
"## Denominator",
|
|
115
|
+
"",
|
|
116
|
+
`Scheduled ${report.denominators.scheduled}; assessed ${report.denominators.assessed}; valid ${report.denominators.valid}; invalid ${report.denominators.invalid}; unknown ${report.denominators.unknown}; unrun ${report.denominators.unrun}; judged ${report.denominators.judged}; unjudged ${report.denominators.unjudged}.`,
|
|
117
|
+
"",
|
|
118
|
+
`Pass ${report.totals.pass}; fail ${report.totals.fail}; unknown ${report.totals.unknown}. The valid denominator is pass + fail (${report.totals.pass + report.totals.fail}); invalid and unrun runs are excluded and listed below.`,
|
|
119
|
+
"",
|
|
120
|
+
"## Slots",
|
|
121
|
+
"",
|
|
122
|
+
"| Slot | Case | Arm | Rep | Expected | Observed | Validity | Outcome | Counts | Judgement |",
|
|
123
|
+
"| ---- | ---- | --- | --- | -------- | -------- | -------- | ------- | ------ | --------- |",
|
|
124
|
+
...slotRows(report),
|
|
125
|
+
...reasonSection(report),
|
|
126
|
+
...judgeIntegritySection(report),
|
|
127
|
+
"",
|
|
128
|
+
"## Per-run usage",
|
|
129
|
+
"",
|
|
130
|
+
...usageSection(report),
|
|
131
|
+
"",
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Render the final analysis from checked evidence. Requires a closed ledger
|
|
136
|
+
* and writes only to a fresh output directory outside the evidence tree.
|
|
137
|
+
* @param root The campaign root.
|
|
138
|
+
* @param outDir The fresh report directory.
|
|
139
|
+
* @param now Injected clock.
|
|
140
|
+
* @returns The report, or the refusals.
|
|
141
|
+
*/
|
|
142
|
+
export function analyseCampaign(root, outDir, now = () => new Date().toISOString()) {
|
|
143
|
+
const prepared = loadPreparedManifest(root);
|
|
144
|
+
if (!prepared.ok) {
|
|
145
|
+
return { ok: false, problems: [prepared.problem] };
|
|
146
|
+
}
|
|
147
|
+
const frozen = loadFrozen(root);
|
|
148
|
+
if (!frozen.ok)
|
|
149
|
+
return { ok: false, problems: [frozen.problem] };
|
|
150
|
+
const ledger = readLedger(root);
|
|
151
|
+
if (ledger.state !== "closed") {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
problems: [
|
|
155
|
+
`campaign is ${ledger.state}; close it before the final analysis`,
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (existsSync(outDir)) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
problems: [
|
|
163
|
+
`${outDir} already exists; reports are never overwritten`,
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const assessments = readAssessments(root);
|
|
168
|
+
const slots = prepared.manifest.campaign.schedule.slots.map((slot) => {
|
|
169
|
+
const assessment = assessments.find((entry) => entry.slotId === slot.slotId);
|
|
170
|
+
if (assessment === undefined) {
|
|
171
|
+
return {
|
|
172
|
+
slotId: slot.slotId,
|
|
173
|
+
caseId: slot.caseId,
|
|
174
|
+
armId: slot.armId,
|
|
175
|
+
repetitionId: slot.repetitionId,
|
|
176
|
+
expected: "unknown",
|
|
177
|
+
observed: "unrun",
|
|
178
|
+
validity: "unrun",
|
|
179
|
+
outcome: "unknown",
|
|
180
|
+
reasons: ["no assessment was recorded"],
|
|
181
|
+
counts: {},
|
|
182
|
+
usage: readUsage(root, slot.slotId),
|
|
183
|
+
judgement: judgementOf(root, slot.slotId),
|
|
184
|
+
judgeProblems: judgeProblemsOf(root, prepared.manifest, slot.slotId),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const observed = observedOf(assessment);
|
|
188
|
+
return {
|
|
189
|
+
slotId: slot.slotId,
|
|
190
|
+
caseId: slot.caseId,
|
|
191
|
+
armId: slot.armId,
|
|
192
|
+
repetitionId: slot.repetitionId,
|
|
193
|
+
...observed,
|
|
194
|
+
validity: assessment.validity,
|
|
195
|
+
outcome: assessment.outcome,
|
|
196
|
+
reasons: assessment.reasons,
|
|
197
|
+
usage: readUsage(root, slot.slotId),
|
|
198
|
+
judgement: judgementOf(root, slot.slotId),
|
|
199
|
+
judgeProblems: judgeProblemsOf(root, prepared.manifest, slot.slotId),
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
const body = {
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
campaignId: prepared.manifest.campaignId,
|
|
205
|
+
suiteId: prepared.manifest.suiteId,
|
|
206
|
+
closedAt: ledger.closedAt,
|
|
207
|
+
imageId: frozen.record.imageId,
|
|
208
|
+
generatedAt: now(),
|
|
209
|
+
denominators: {
|
|
210
|
+
scheduled: slots.length,
|
|
211
|
+
assessed: assessments.length,
|
|
212
|
+
valid: slots.filter((slot) => slot.validity === "valid").length,
|
|
213
|
+
invalid: slots.filter((slot) => slot.validity === "invalid").length,
|
|
214
|
+
unknown: slots.filter((slot) => slot.validity === "unknown").length,
|
|
215
|
+
unrun: slots.filter((slot) => slot.validity === "unrun").length,
|
|
216
|
+
judged: slots.filter((slot) => slot.judgement !== null).length,
|
|
217
|
+
unjudged: slots.filter((slot) => slot.judgement === null).length,
|
|
218
|
+
},
|
|
219
|
+
totals: {
|
|
220
|
+
pass: slots.filter((slot) => slot.outcome === "pass").length,
|
|
221
|
+
fail: slots.filter((slot) => slot.outcome === "fail").length,
|
|
222
|
+
unknown: slots.filter((slot) => slot.outcome === "unknown").length,
|
|
223
|
+
},
|
|
224
|
+
slots,
|
|
225
|
+
};
|
|
226
|
+
const report = { ...body, reportSha256: sha256Of(body) };
|
|
227
|
+
const markdown = markdownOf(report);
|
|
228
|
+
mkdirSync(outDir, { recursive: true });
|
|
229
|
+
writeFileSync(join(outDir, "report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
230
|
+
writeFileSync(join(outDir, "report.md"), markdown, "utf8");
|
|
231
|
+
return { ok: true, report, markdown, outDir };
|
|
232
|
+
}
|
|
233
|
+
/** The canonical identity of a report without its own identity field. */
|
|
234
|
+
export function reportIdentity(report) {
|
|
235
|
+
const body = { ...report };
|
|
236
|
+
delete body.reportSha256;
|
|
237
|
+
return sha256Of(body);
|
|
238
|
+
}
|
|
239
|
+
/** Ensure a report's identity still matches its content. */
|
|
240
|
+
export function reportIntact(report) {
|
|
241
|
+
return reportIdentity(report) === report.reportSha256;
|
|
242
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Architecture and extraction ownership
|
|
2
|
+
|
|
3
|
+
Status: implemented 2026-11-15. Source repository:
|
|
4
|
+
`markstanden/opencode-docs-plugin` @ `76ed331` (`main`, clean at review).
|
|
5
|
+
Destination: this repository.
|
|
6
|
+
|
|
7
|
+
## Decision
|
|
8
|
+
|
|
9
|
+
Two repositories, one OTC package. The docs plugin owns the product and its
|
|
10
|
+
real evaluation corpus; OTC owns the reusable lifecycle, sandbox adapters,
|
|
11
|
+
evidence boundary and evaluator implementations. OTC does not depend on the
|
|
12
|
+
docs plugin at runtime, and the docs plugin does not depend on OTC at runtime.
|
|
13
|
+
The docs repository's development tooling pins an immutable OTC artifact.
|
|
14
|
+
|
|
15
|
+
The extraction is not a directory move. The new lifecycle validates versioned
|
|
16
|
+
suite/campaign data, stages immutable subject payloads, freezes the resolved
|
|
17
|
+
image and CLI identity, appends an explicit ledger, separates capture validity
|
|
18
|
+
from criterion outcome, and derives read-only status. Docs-specific semantics
|
|
19
|
+
live only under `src/evaluators/docs`.
|
|
20
|
+
|
|
21
|
+
## Why these seams
|
|
22
|
+
|
|
23
|
+
- **Subject vs runtime vs suite vs campaign.** A skill-only subject must run
|
|
24
|
+
without a plugin or lake; a paired docs comparison must not force a skill
|
|
25
|
+
case into a docs schema. One discriminated contract serves both.
|
|
26
|
+
- **Capture vs outcome.** A complete stream ending normally is valid evidence;
|
|
27
|
+
an activation expectation can then fail. A truncated stream, missing key or
|
|
28
|
+
failed target load is an operational unknown, never a passing negative.
|
|
29
|
+
- **Trusted evaluators.** The CLI wires evaluator implementations. Campaign
|
|
30
|
+
data names an assessment kind; it never names executable JavaScript loaded
|
|
31
|
+
from evidence.
|
|
32
|
+
- **Historical identity.** Old stamps, scenario ids and frozen `.mjs` bytes are
|
|
33
|
+
records. `legacy/prod014.ts` translates them without renaming. Old roots are
|
|
34
|
+
not resumed by this engine.
|
|
35
|
+
|
|
36
|
+
## Source-to-destination manifest
|
|
37
|
+
|
|
38
|
+
| Source (`opencode-docs-plugin` @ 76ed331) | Destination | Class |
|
|
39
|
+
| ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------- |
|
|
40
|
+
| `tests/fixtures/placement-harness/harness.mts` (case schemas, generation, run, capture) | `src/contracts`, `src/campaign`, `src/adapters` | split + rewrite |
|
|
41
|
+
| `.../analysis/{budget,closure,freeze,ledger,recovery,selector}.ts` | `src/campaign/{ledger,freeze,close,run,assess}.ts` | split + rewrite |
|
|
42
|
+
| `.../analysis/journal.ts` | `src/evaluators/docs/journal.ts` | move (bytes preserved apart from header) |
|
|
43
|
+
| `.../analysis/{accounting,records,guardrails,measures,render,model,decision,pairing,bounds,closure}.ts` | `src/evaluators/docs/analysis/*` | move |
|
|
44
|
+
| `.../analysis/screening/*` | `src/evaluators/docs/analysis/screening/*` | move |
|
|
45
|
+
| `.../ac/*`, `prod014/campaign/active/*`, `status.mts` | `src/legacy/prod014.ts` readers; old engine stays pinned in docs | legacy |
|
|
46
|
+
| `.../Containerfile`, `container-run.sh` | `container/Containerfile`, `src/adapters/podman` | rewrite |
|
|
47
|
+
| `.../primary-checks/*`, placement corpus, truth, measurement | docs `evaluations/docs-placement/`; OTC stages and executes supplied checkers | data stays; execution moves |
|
|
48
|
+
| `.../ac-scoring.mts` judge prompt, classification, recovery policy | `src/evaluators/docs/scoring.ts`, `src/campaign/judge.ts` | move + rewrite |
|
|
49
|
+
| docs `src/index.ts` (metadata, ranking, collection, convention) | docs `src/{metadata,ranking,collection,convention}.ts` | split |
|
|
50
|
+
| docs `src/index.ts` (lakes, publication, tools, plugin) | docs `src/lakes.ts`, `src/publication.ts`, `src/index.ts` | split |
|
|
51
|
+
|
|
52
|
+
## Dependency direction
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
cli -> campaign lifecycle -> contracts
|
|
56
|
+
| |
|
|
57
|
+
+-> adapters (opencode, podman, process)
|
|
58
|
+
+-> evaluators (docs, skills) -> evidence/contracts
|
|
59
|
+
+-> reporting
|
|
60
|
+
legacy -> contracts only
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`src/contracts` imports only Node built-ins. `src/evaluators/docs/journal.ts`
|
|
64
|
+
imports only `node:crypto` and `node:util`. No core module imports a docs
|
|
65
|
+
checkout or reads an ambient Git repository at import time.
|
|
66
|
+
|
|
67
|
+
The migrated `harness/run.ts` CLI has been removed: it could spawn the docs
|
|
68
|
+
repository's `tests/fixtures/placement-harness/harness.mts`, which is an active
|
|
69
|
+
cross-repository engine dependency. The reusable evidence guard now lives at
|
|
70
|
+
`src/campaign/evidence-guard.ts`, and the one judge-operation renderer is local
|
|
71
|
+
to `src/evaluators/docs/assess.ts`. Product-supplied suites and checker bytes
|
|
72
|
+
remain external data; OTC stages, hashes and executes them.
|
|
73
|
+
|
|
74
|
+
## Sandbox boundary
|
|
75
|
+
|
|
76
|
+
- Subject payloads are staged into `prepare/subjects/<armId>`, inventories
|
|
77
|
+
hashed, and mounted read-only at `/subjects`.
|
|
78
|
+
- Case workspaces are staged once and copied per trial; only that copy is
|
|
79
|
+
writable at `/workspace`.
|
|
80
|
+
- The config home (skills catalogue and `opencode.json`) is read-only at
|
|
81
|
+
`/config-home`.
|
|
82
|
+
- The docs journal is the only other writable mount, and only for docs cases.
|
|
83
|
+
- A product checker is staged at `prepare/primary-checks/<caseId>` and run in
|
|
84
|
+
a separate `network: "none"` container with the completed workspace and the
|
|
85
|
+
pristine workspace read-only. It is never mounted into the trial.
|
|
86
|
+
- The judge runs in a neutral container with the packet read-only, a neutral
|
|
87
|
+
config home and no arm subjects, campaign manifest or evidence root. Its
|
|
88
|
+
standing-permission block comes from the pristine prepared workspace, never
|
|
89
|
+
the trial's live `AGENTS.md`.
|
|
90
|
+
- Suite, campaign, manifest, truth, criteria and other slots are outside the
|
|
91
|
+
model-visible filesystem.
|
|
92
|
+
- Credentials travel through a 0600 env file; argv and receipts carry names and
|
|
93
|
+
hashes only.
|
|
94
|
+
- `network: "host"` is honest host networking, not provider-only egress.
|
|
95
|
+
|
|
96
|
+
## Removal condition for the docs bridge
|
|
97
|
+
|
|
98
|
+
The old prod014 engine remains in the docs repository, pinned to its original
|
|
99
|
+
runtime and code, until an OTC campaign reaches parity on the same frozen
|
|
100
|
+
evidence. It is not a second active engine: new campaigns use OTC only. The
|
|
101
|
+
docs `evaluations/README.md` names the owner and the removal condition.
|
|
102
|
+
|
|
103
|
+
Primary-check execution now has an OTC home; the docs repository still owns
|
|
104
|
+
the checker bytes and corpus. The removal condition therefore waits on the
|
|
105
|
+
docs agent repinning the new OTC artifact and, if it chooses, deleting the old
|
|
106
|
+
prod014 route after its own parity check. OTC does not rewrite old roots.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Diagnosis capability matrix
|
|
2
|
+
|
|
3
|
+
Status at the reconciliation head. "Parity" means the same recorded evidence
|
|
4
|
+
produced identical structured analysis; permitted differences are listed
|
|
5
|
+
separately.
|
|
6
|
+
|
|
7
|
+
| Capability | Original implementation | OTC implementation | Tests | Offline parity evidence | Remaining owner |
|
|
8
|
+
| ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
9
|
+
| Preparation / materialisation | docs `harness.mts`, `prod014` materialise | `src/campaign/prepare.ts` (new campaigns) | OTC lifecycle/prepare/freeze tests | not applicable (dispatch-side) | Docs legacy for frozen prod014 roots; OTC for new campaigns |
|
|
10
|
+
| Recording credit / journal | docs `analysis/journal.ts` | `src/evaluators/docs/journal.ts` | OTC docs assessment tests + ported analysis tests | mini + confirmation + screening exact | none |
|
|
11
|
+
| Utility / opportunity counts | docs `analysis/{accounting,measures}.ts` | same modules under `src/evaluators/docs/analysis` | ported analysis aggregation/measures, decision-authority | mini utility totals (10/10, 20/20), screening 24/24 | none |
|
|
12
|
+
| Retrieval measures | docs `analysis/measures.ts`, `records.ts` | same | ported measures suite + `measures-gaps` (path/lake/contradiction) | recorded campaigns declare no measurement spec; synthetic/contract coverage only | OTC; real-declaration parity needs a campaign with `measures` |
|
|
13
|
+
| Preservation | docs `analysis/{scoring,measures,guardrails}.ts` | same | ported measures/guardrails/screening tests | mini R02 preservation and screening findings exact | none |
|
|
14
|
+
| Guardrails | docs `analysis/guardrails.ts` | same | ported decision/guardrails tests | mini both guardrails clean; screening exact | none |
|
|
15
|
+
| Pairing / bounds | docs `analysis/{pairing,bounds}.ts` | same | ported aggregation/decision tests | mini 2 complete pairs, delta 0 [0,0]; confirmation bounds exact | none |
|
|
16
|
+
| Screening | docs `analysis/screening/*` | same | ported screening suite (rules/inventory/scan/reconcile) | screening campaign `SCREENING.json` byte-identical | none |
|
|
17
|
+
| Judge output classification | docs `ac-scoring.mts` | `src/evaluators/docs/scoring.ts` (`judgeFailureClass`, parse/validate) + `src/campaign/judge.ts` | ported ac-scoring + judge-recovery-policy + OTC judge tests | fresh scripted judge lifecycle; classification/recovery pure contracts exact | none |
|
|
18
|
+
| Judge recovery | docs `analysis/recovery.ts`, `analysis/budget.ts` | same + OTC judge dispatch | ported judge-recovery-policy (28 tests, concurrent ceilings) + OTC recovery tests | fresh scripted malformed-then-valid judge; selection attempt-002 | none |
|
|
19
|
+
| Primary checks / corpus generation | docs harness and `primary-checks/` | OTC stages and executes supplied checkers; docs owns case data, truth and checker bytes | OTC primary tests + fresh rehearsal | fresh checker pass/fail/timeout; checker never in trial payload | Docs product corpus ownership |
|
|
20
|
+
| Budgets | docs `analysis/{budget,closure}.ts` + campaign ledger | migrated analysis budget/closure + OTC campaign ledger | ported closure/launch-boundary + OTC lifecycle budget tests | exact analysis accounting/ledger sections | none |
|
|
21
|
+
| Selection | docs `analysis/selector.ts`, `recovery.ts` | same | ported recovery/selection tests | exact `assessmentSelection` in parity outputs | none |
|
|
22
|
+
| Closure | docs `analysis/closure.ts` + `close-campaign.mts` | migrated closure + `otc close` | ported closure tests + OTC close idempotence | transaction/ledger state exact | none |
|
|
23
|
+
| Reporting | docs `analysis/{render,measures,decision}.ts`, `analyse.mts` | migrated render/measures/decision + `otc replay`, OTC campaign report | ported report/integrity + OTC report tests | `analysis.json`, `REPORT.md`, `SCREENING.json` exact; receipt only `renderedAt` differs | none for recorded replay; OTC campaign report separate |
|
|
24
|
+
| Status | docs `prod014/status.mts` | `src/campaign/status.ts` | OTC CLI/status tests | not applicable (new roots) | Docs legacy for frozen roots |
|
|
25
|
+
| Container / process adapters | docs `Containerfile`, `container-run.sh` | `container/Containerfile`, `src/adapters/podman` | OTC adapter tests + real rehearsal | real Podman/OpenCode rehearsal | none |
|
|
26
|
+
| Skill activation | none | `src/evaluators/skills` | OTC skill tests + real scripted rehearsal | real adapter rehearsal | none |
|
|
27
|
+
|
|
28
|
+
## Fresh lifecycle capability
|
|
29
|
+
|
|
30
|
+
The migrated `harness/run.ts` CLI is removed. No OTC source can spawn the docs
|
|
31
|
+
repository's old `tests/fixtures/placement-harness/harness.mts`; only
|
|
32
|
+
product-supplied suite, truth and checker bytes remain external data. A
|
|
33
|
+
campaign can now execute the complete path without the docs repository's old
|
|
34
|
+
harness:
|
|
35
|
+
|
|
36
|
+
| Stage | OTC implementation | Evidence |
|
|
37
|
+
| ------------------------ | ------------------------------------------------- | --------------------------------------------------------------- |
|
|
38
|
+
| Validate / prepare | `contracts/validate.ts`, `campaign/prepare.ts` | unit + CLI tests; staged immutably |
|
|
39
|
+
| Delivery / freeze | `campaign/freeze.ts` | image ID, suite, subjects, workspaces, prompts and checkers |
|
|
40
|
+
| Trial dispatch | `campaign/run.ts`, `adapters/{opencode,podman}` | real Podman/OpenCode rehearsal; dry-run plan |
|
|
41
|
+
| Product primary check | `campaign/primary.ts` | separate no-network container; pass/fail/timeout/error recorded |
|
|
42
|
+
| Deterministic assessment | `evaluators/{docs,skills}` | primary outcome folded into the docs criterion |
|
|
43
|
+
| Blind judge | `campaign/judge.ts`, `evaluators/docs/scoring.ts` | neutral packet; established parse/validate/failure classes |
|
|
44
|
+
| Bounded recovery | `analysis/recovery.ts`, `analysis/budget.ts` | one `json-syntax` retry, identical packet, durable claims |
|
|
45
|
+
| Validated selection | `analysis/selector.ts`, `campaign/judge.ts` | per-slot selection record checked against attempts |
|
|
46
|
+
| Closure / analysis | `campaign/close.ts`, `reporting/report.ts` | pending judgement and unsettled claims block closure |
|
|
47
|
+
| Status | `campaign/status.ts` | read-only counts, judgement and next actions |
|
|
48
|
+
|
|
49
|
+
Evidence guarantees added at the repair head:
|
|
50
|
+
|
|
51
|
+
- one checked-selection reader is used by judge, status, closure and reporting;
|
|
52
|
+
a readable object is not a judgement, and slot/attempt packet bytes are
|
|
53
|
+
re-hashed and reconstructed from current evidence;
|
|
54
|
+
- judge and recovery claims share `<root>/evidence` as their only location and
|
|
55
|
+
both are settled by the dispatch outcome;
|
|
56
|
+
- the judge's standing permission comes from the pristine prepared workspace,
|
|
57
|
+
never the trial's live `AGENTS.md`, so A/B instruction overlays stay out of
|
|
58
|
+
the packet;
|
|
59
|
+
- the executing tool's artifact hash is compared with the freeze before
|
|
60
|
+
dispatch, derived from the executing module's `src`/`dist` location so
|
|
61
|
+
changed code is detected under the same package version.
|
|
62
|
+
|
|
63
|
+
## Semantic parity runs
|
|
64
|
+
|
|
65
|
+
All runs replay recorded evidence offline; no model call was made.
|
|
66
|
+
|
|
67
|
+
| Corpus | Trials | Original vs OTC |
|
|
68
|
+
| ---------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| committed mini replay (`tests/fixtures/docs-parity`) | 4 | `analysis.json`, `REPORT.md`, `SCREENING.json` deep-equal; receipt equal after removing `renderedAt` |
|
|
70
|
+
| recorded confirmation (`recording-confirm-20261110`) | 40 | all three artefacts byte-identical; receipt only `renderedAt` |
|
|
71
|
+
| recorded screening (`docs-desc-fresh-20261110`) | 48 | all three artefacts byte-identical |
|
|
72
|
+
|
|
73
|
+
The committed parity test exercises the mini corpus in `npm test`. The two
|
|
74
|
+
larger corpora were replayed manually because they total ~56 MB and are not
|
|
75
|
+
committed. The frozen published `ANALYSIS-confirmation.json` uses an older
|
|
76
|
+
schema and was not treated as a byte target; the comparison is between the
|
|
77
|
+
original analyser at the extraction base and the OTC port on the same evidence.
|
|
78
|
+
|
|
79
|
+
## Permitted differences
|
|
80
|
+
|
|
81
|
+
- `renderedAt` in `analysis-receipt.json` (wall-clock).
|
|
82
|
+
- Absolute evidence/manifest paths when a caller supplies a different root.
|
|
83
|
+
- OTC campaign reports (`analysis`/`report`) use the new lifecycle schema; they
|
|
84
|
+
are not claimed to be byte-compatible with docs analysis reports.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Offline real-adapter rehearsals
|
|
2
|
+
|
|
3
|
+
## Baseline (`npm run rehearse:offline`)
|
|
4
|
+
|
|
5
|
+
Recorded 2026-11-15 from `npm run rehearse:offline` on the dev box.
|
|
6
|
+
|
|
7
|
+
- Image: `localhost/opencode-placement-trials:latest`
|
|
8
|
+
- Image ID: `sha256:58505eac2fe9161a02db4719e6a8a34e9106b644e9eee0aaa00184269b643beb`
|
|
9
|
+
- OpenCode: `v2.2.6` (the tested CLI; not upgraded by this work)
|
|
10
|
+
- Provider: scripted loopback OpenAI-compatible server
|
|
11
|
+
- Paid provider calls: **0**
|
|
12
|
+
- Scripted chat requests: **22**
|
|
13
|
+
- Readiness/model-list probes: **0**
|
|
14
|
+
|
|
15
|
+
| Suite | Slot | Validity | Observed | Outcome | Chat requests |
|
|
16
|
+
| ------ | --------------- | -------- | ------------------ | ------- | ------------- |
|
|
17
|
+
| skills | positive | valid | target-activation | pass | 2 |
|
|
18
|
+
| skills | negative | valid | no-invocation | pass | 1 |
|
|
19
|
+
| skills | wrong-skill | valid | other-skill-only | pass | 2 |
|
|
20
|
+
| skills | failed-load | unknown | failed-target-load | unknown | 2 |
|
|
21
|
+
| skills | invalid-capture | invalid | unrun | unknown | 11 |
|
|
22
|
+
| docs | useful-write | valid | — | pass | 2 |
|
|
23
|
+
| docs | gate-blocked | valid | — | pass | 2 |
|
|
24
|
+
|
|
25
|
+
Docs counts: useful write credited 1 call / 1 write; gate-blocked credited 1
|
|
26
|
+
call / **0 writes**, attempted 1. Both journals verified with the per-run key.
|
|
27
|
+
The truncated capture retried at the adapter level (11 HTTP requests) and is
|
|
28
|
+
recorded as invalid, not as a passing negative. A failed target load is
|
|
29
|
+
`unknown`, not a negative.
|
|
30
|
+
|
|
31
|
+
Isolation checks: no suite, campaign, manifest or freeze file appeared in a
|
|
32
|
+
trial workspace or config home; no hidden criteria string appeared in either;
|
|
33
|
+
every slot made at least one provider request.
|
|
34
|
+
|
|
35
|
+
This proves the machinery and the evidence boundary offline. It is **not**
|
|
36
|
+
evidence of real model quality.
|
|
37
|
+
|
|
38
|
+
## Fresh docs lifecycle (`npm run rehearse:fresh-docs`)
|
|
39
|
+
|
|
40
|
+
Same image and scripted provider, plus the product primary check, blind judge
|
|
41
|
+
and recovery path:
|
|
42
|
+
|
|
43
|
+
| Slot | Primary | Outcome | Selected attempt | Trial chat | Judge chat |
|
|
44
|
+
| --------------- | ------- | ------- | ---------------- | ---------- | ---------- |
|
|
45
|
+
| useful-write | pass | pass | attempt-001 | 2 | 1 |
|
|
46
|
+
| gate-blocked | pass | pass | attempt-001 | 2 | 1 |
|
|
47
|
+
| primary-failure | fail | fail | attempt-001 | 1 | 1 |
|
|
48
|
+
| recovery | — | pass | attempt-002 | 1 | 2 |
|
|
49
|
+
|
|
50
|
+
The recovery's first judge attempt returned prose, the second a valid
|
|
51
|
+
`none` verdict; both recorded the same `packetSha256`. The separate
|
|
52
|
+
exhausted-budget root refused recovery after one judge claim, made one chat
|
|
53
|
+
request and wrote no selection. The gate-blocked write kept zero credited
|
|
54
|
+
writes. The checker never appeared in the trial workspace or config home.
|
|
55
|
+
Post-close `run` and `judge` both exited 1; analysis after closure recorded all
|
|
56
|
+
four judgements.
|
|
57
|
+
|
|
58
|
+
Totals: 6 trial + 5 judge + 1 exhausted scripted chat requests, 0 readiness
|
|
59
|
+
probes, 0 paid provider calls.
|
|
60
|
+
|
|
61
|
+
## Installed tarball (`npm run rehearse:installed`)
|
|
62
|
+
|
|
63
|
+
The checkout is packed, installed into a consumer directory whose path
|
|
64
|
+
contains a space, and the same fresh docs lifecycle runs through
|
|
65
|
+
`node_modules/.bin/otc`. The 2026-11-15 run used image
|
|
66
|
+
`sha256:58505eac…`, OpenCode 2.2.6 and installed version `0.1.0`; it produced
|
|
67
|
+
the same four-slot result, one recovery, one exhausted refusal and zero paid
|
|
68
|
+
calls. The receipt records the tarball filename, SHA-256 and npm integrity;
|
|
69
|
+
the exact identity is reported with the handback attestation.
|
|
70
|
+
The development-only provider stays outside the tarball.
|
|
71
|
+
|
|
72
|
+
The migrated `harness/run.ts` CLI is gone: it could spawn the docs
|
|
73
|
+
repository's old `placement-harness/harness.mts`. The generic evidence guard
|
|
74
|
+
lives at `src/campaign/evidence-guard.ts`; OTC now accepts only product data
|
|
75
|
+
(suite, truth, checker bytes), never a sibling engine.
|