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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) 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/schema.md +97 -0
  117. package/examples/skills/README.md +13 -0
  118. package/examples/skills/campaign.json +73 -0
  119. package/examples/skills/skills/otel-helper/SKILL.md +10 -0
  120. package/examples/skills/suite.json +29 -0
  121. 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.