@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
package/dist/cli/otc.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `otc` command line: thin handlers over the lifecycle modules. Every
|
|
4
|
+
* command validates at its boundary, prints actionable problems, and exits
|
|
5
|
+
* 0 (ok), 1 (operational failure) or 2 (usage/validation refusal).
|
|
6
|
+
*/
|
|
7
|
+
import { realpathSync } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { prepareCampaign, prepareSummary } from "../campaign/prepare.js";
|
|
10
|
+
import { deliveryCheck, freezeCampaign } from "../campaign/freeze.js";
|
|
11
|
+
import { runCampaign } from "../campaign/run.js";
|
|
12
|
+
import { judgeCampaign } from "../campaign/judge.js";
|
|
13
|
+
import { assessCampaign } from "../campaign/assess.js";
|
|
14
|
+
import { closeCampaign } from "../campaign/close.js";
|
|
15
|
+
import { statusOf } from "../campaign/status.js";
|
|
16
|
+
import { analyseCampaign } from "../reporting/report.js";
|
|
17
|
+
import { runAnalysis } from "../evaluators/docs/analysis/replay.js";
|
|
18
|
+
import { loadCampaignBundle } from "../contracts/validate.js";
|
|
19
|
+
import { packageVersion } from "../contracts/version.js";
|
|
20
|
+
/** The commands `otc` exposes. */
|
|
21
|
+
export const COMMANDS = [
|
|
22
|
+
"validate",
|
|
23
|
+
"prepare",
|
|
24
|
+
"delivery-check",
|
|
25
|
+
"freeze",
|
|
26
|
+
"run",
|
|
27
|
+
"judge",
|
|
28
|
+
"assess",
|
|
29
|
+
"close",
|
|
30
|
+
"analyse",
|
|
31
|
+
"status",
|
|
32
|
+
"replay",
|
|
33
|
+
"help",
|
|
34
|
+
"version",
|
|
35
|
+
];
|
|
36
|
+
/** Usage text, kept identical to the documented command list. */
|
|
37
|
+
export const USAGE = `otc — an isolated test sandbox for OpenCode plugins, skills and agent instructions
|
|
38
|
+
|
|
39
|
+
Usage: otc <command> [options]
|
|
40
|
+
|
|
41
|
+
Commands:
|
|
42
|
+
validate Validate a campaign and its suite; no writes
|
|
43
|
+
prepare Validate, then stage an immutable campaign root
|
|
44
|
+
delivery-check Recheck staged payloads and the freeze receipt
|
|
45
|
+
freeze Resolve the image ID and freeze all input identities
|
|
46
|
+
run Execute scheduled slots through Podman and OpenCode
|
|
47
|
+
judge Blindly judge completed docs trials from checked evidence
|
|
48
|
+
assess Deterministically assess captured evidence
|
|
49
|
+
close Append closure to the ledger (idempotent)
|
|
50
|
+
analyse Render final analysis after closure
|
|
51
|
+
status Read-only state and next actions
|
|
52
|
+
replay Replay recorded docs evidence through the docs evaluator
|
|
53
|
+
help This text
|
|
54
|
+
version Print the package version
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--campaign <file> Campaign JSON (default: $OTC_CAMPAIGN)
|
|
58
|
+
--root <dir> Campaign root (default: $OTC_ROOT)
|
|
59
|
+
--out <dir> Fresh report directory (analyse)
|
|
60
|
+
--slot <id> Select a slot; repeat or comma-separate
|
|
61
|
+
--passthrough <name> Pass one named environment variable into the trial
|
|
62
|
+
--manifest <file> Recorded docs campaign manifest (replay)
|
|
63
|
+
--evidence-root <dir> Recorded evidence root (replay)
|
|
64
|
+
--rerender Replace an existing analysis output (replay)
|
|
65
|
+
--allow-legacy-unbound Allow unbound legacy answers (replay)
|
|
66
|
+
--dry-run Plan a trial without dispatching it
|
|
67
|
+
--json Machine-readable output (status, validate, assess)
|
|
68
|
+
--help This text
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
otc validate --campaign examples/skills/otel-skill-container.json
|
|
72
|
+
otc prepare --campaign campaign.json --root /tmp/otc-run
|
|
73
|
+
otc freeze --root /tmp/otc-run
|
|
74
|
+
otc run --root /tmp/otc-run --dry-run
|
|
75
|
+
otc run --root /tmp/otc-run
|
|
76
|
+
otc judge --root /tmp/otc-run
|
|
77
|
+
otc assess --root /tmp/otc-run
|
|
78
|
+
otc close --root /tmp/otc-run
|
|
79
|
+
otc analyse --root /tmp/otc-run --out /tmp/otc-report
|
|
80
|
+
otc status --root /tmp/otc-run --json
|
|
81
|
+
otc replay --manifest campaign.json --evidence-root evidence --out report
|
|
82
|
+
`;
|
|
83
|
+
/** Parse argv into a command and its options. */
|
|
84
|
+
/** Take one option's value, refusing a missing or flag-like value. */
|
|
85
|
+
function optionValue(name, args) {
|
|
86
|
+
const next = args.shift();
|
|
87
|
+
if (next === undefined || next.startsWith("--")) {
|
|
88
|
+
throw new UsageError(`${name} needs a value`);
|
|
89
|
+
}
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
/** Apply one value-bearing option; false when the argument is unknown. */
|
|
93
|
+
function applyOption(options, arg, args) {
|
|
94
|
+
switch (arg) {
|
|
95
|
+
case "--campaign":
|
|
96
|
+
options.campaign = optionValue(arg, args);
|
|
97
|
+
return true;
|
|
98
|
+
case "--root":
|
|
99
|
+
options.root = optionValue(arg, args);
|
|
100
|
+
return true;
|
|
101
|
+
case "--out":
|
|
102
|
+
options.out = optionValue(arg, args);
|
|
103
|
+
return true;
|
|
104
|
+
case "--slot":
|
|
105
|
+
options.slots.push(...optionValue(arg, args)
|
|
106
|
+
.split(",")
|
|
107
|
+
.map((entry) => entry.trim())
|
|
108
|
+
.filter((entry) => entry !== ""));
|
|
109
|
+
return true;
|
|
110
|
+
case "--passthrough":
|
|
111
|
+
options.passthrough.push(optionValue(arg, args));
|
|
112
|
+
return true;
|
|
113
|
+
case "--manifest":
|
|
114
|
+
options.manifest = optionValue(arg, args);
|
|
115
|
+
return true;
|
|
116
|
+
case "--evidence-root":
|
|
117
|
+
options.evidenceRoot = optionValue(arg, args);
|
|
118
|
+
return true;
|
|
119
|
+
default:
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Resolve the command name, refusing an unknown one. */
|
|
124
|
+
function commandOf(first) {
|
|
125
|
+
if (!COMMANDS.includes(first)) {
|
|
126
|
+
throw new UsageError(`unknown command ${first}`);
|
|
127
|
+
}
|
|
128
|
+
return first;
|
|
129
|
+
}
|
|
130
|
+
/** Apply every remaining argument to the options. */
|
|
131
|
+
function parseOptionLoop(options, args) {
|
|
132
|
+
while (args.length > 0) {
|
|
133
|
+
const arg = args.shift();
|
|
134
|
+
if (arg === undefined)
|
|
135
|
+
break;
|
|
136
|
+
if (arg === "--help" || arg === "-h")
|
|
137
|
+
options.help = true;
|
|
138
|
+
else if (arg === "--json")
|
|
139
|
+
options.json = true;
|
|
140
|
+
else if (arg === "--dry-run")
|
|
141
|
+
options.dryRun = true;
|
|
142
|
+
else if (arg === "--rerender")
|
|
143
|
+
options.rerender = true;
|
|
144
|
+
else if (arg === "--allow-legacy-unbound") {
|
|
145
|
+
options.allowLegacyUnbound = true;
|
|
146
|
+
}
|
|
147
|
+
else if (!applyOption(options, arg, args)) {
|
|
148
|
+
throw new UsageError(`unknown option ${arg}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parse argv into a command and its options.
|
|
154
|
+
* @param argv Arguments after the executable.
|
|
155
|
+
* @returns The parsed options.
|
|
156
|
+
*/
|
|
157
|
+
export function parseArgs(argv) {
|
|
158
|
+
const options = {
|
|
159
|
+
command: "help",
|
|
160
|
+
campaign: process.env.OTC_CAMPAIGN ?? null,
|
|
161
|
+
root: process.env.OTC_ROOT ?? null,
|
|
162
|
+
out: null,
|
|
163
|
+
slots: [],
|
|
164
|
+
json: false,
|
|
165
|
+
dryRun: false,
|
|
166
|
+
passthrough: [],
|
|
167
|
+
manifest: null,
|
|
168
|
+
evidenceRoot: null,
|
|
169
|
+
rerender: false,
|
|
170
|
+
allowLegacyUnbound: false,
|
|
171
|
+
help: false,
|
|
172
|
+
};
|
|
173
|
+
const args = [...argv];
|
|
174
|
+
const first = args.shift();
|
|
175
|
+
if (first === undefined || first === "--help" || first === "-h") {
|
|
176
|
+
return options;
|
|
177
|
+
}
|
|
178
|
+
if (first === "--version" || first === "-v") {
|
|
179
|
+
options.command = "version";
|
|
180
|
+
return options;
|
|
181
|
+
}
|
|
182
|
+
options.command = commandOf(first);
|
|
183
|
+
parseOptionLoop(options, args);
|
|
184
|
+
return options;
|
|
185
|
+
}
|
|
186
|
+
/** A command-line usage refusal. */
|
|
187
|
+
export class UsageError extends Error {
|
|
188
|
+
}
|
|
189
|
+
/** Require a campaign path for commands that read one. */
|
|
190
|
+
function requireCampaign(options) {
|
|
191
|
+
if (options.campaign === null) {
|
|
192
|
+
throw new UsageError("--campaign (or $OTC_CAMPAIGN) is required");
|
|
193
|
+
}
|
|
194
|
+
return options.campaign;
|
|
195
|
+
}
|
|
196
|
+
/** Require a root for commands that operate on staged state. */
|
|
197
|
+
function requireRoot(options) {
|
|
198
|
+
if (options.root === null) {
|
|
199
|
+
throw new UsageError("--root (or $OTC_ROOT) is required");
|
|
200
|
+
}
|
|
201
|
+
return options.root;
|
|
202
|
+
}
|
|
203
|
+
/** Print a failure block and return the exit code. */
|
|
204
|
+
function fail(problems, code = 2) {
|
|
205
|
+
for (const problem of problems)
|
|
206
|
+
console.error(`otc: ${problem}`);
|
|
207
|
+
return code;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Run one command and return its exit code.
|
|
211
|
+
* @param argv Arguments after the executable.
|
|
212
|
+
* @returns The process exit code.
|
|
213
|
+
*/
|
|
214
|
+
export async function main(argv) {
|
|
215
|
+
let options;
|
|
216
|
+
try {
|
|
217
|
+
options = parseArgs(argv);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error instanceof UsageError) {
|
|
221
|
+
console.error(`otc: ${error.message}`);
|
|
222
|
+
console.error(USAGE);
|
|
223
|
+
return 2;
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
if (options.help || options.command === "help") {
|
|
228
|
+
console.log(USAGE);
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
if (options.command === "version") {
|
|
232
|
+
console.log(packageVersion());
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (options.command === "analyse" && options.out === null) {
|
|
236
|
+
return fail(["analyse needs --out <fresh-directory>"]);
|
|
237
|
+
}
|
|
238
|
+
const handler = HANDLERS[options.command];
|
|
239
|
+
return handler === undefined ? 0 : handler(options);
|
|
240
|
+
}
|
|
241
|
+
/** Validate a campaign bundle and print its summary. */
|
|
242
|
+
function validate(options) {
|
|
243
|
+
const loaded = loadCampaignBundle(requireCampaign(options));
|
|
244
|
+
if (!loaded.ok)
|
|
245
|
+
return fail(loaded.problems);
|
|
246
|
+
const bundle = loaded.value;
|
|
247
|
+
const summary = {
|
|
248
|
+
campaignId: bundle.campaign.campaignId,
|
|
249
|
+
suiteId: bundle.suite.suiteId,
|
|
250
|
+
assessment: bundle.suite.assessment,
|
|
251
|
+
cases: bundle.suite.cases.length,
|
|
252
|
+
arms: bundle.campaign.subjects.length,
|
|
253
|
+
slots: bundle.campaign.schedule.slots.length,
|
|
254
|
+
};
|
|
255
|
+
console.log(options.json
|
|
256
|
+
? JSON.stringify({ ok: true, ...summary }, null, 2)
|
|
257
|
+
: `valid: ${summary.campaignId} / ${summary.suiteId}; ${summary.cases} case(s), ${summary.arms} arm(s), ${summary.slots} slot(s)`);
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
/** Stage a fresh campaign root. */
|
|
261
|
+
function prepare(options) {
|
|
262
|
+
const prepared = prepareCampaign(requireCampaign(options), requireRoot(options));
|
|
263
|
+
if (!prepared.ok)
|
|
264
|
+
return fail(prepared.problems);
|
|
265
|
+
console.log(prepareSummary(prepared.manifest));
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
/** Recheck staged payloads and the freeze receipt. */
|
|
269
|
+
function checkDelivery(options) {
|
|
270
|
+
const root = requireRoot(options);
|
|
271
|
+
const problems = deliveryCheck(root);
|
|
272
|
+
if (problems.length > 0)
|
|
273
|
+
return fail(problems, 1);
|
|
274
|
+
console.log(`delivery intact: ${root}`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
/** Resolve and freeze the runtime identity. */
|
|
278
|
+
async function freeze(options) {
|
|
279
|
+
const frozen = await freezeCampaign(requireRoot(options));
|
|
280
|
+
if (!frozen.ok)
|
|
281
|
+
return fail(frozen.problems, 1);
|
|
282
|
+
console.log(frozen.alreadyFrozen
|
|
283
|
+
? `already frozen: ${frozen.record.imageId}`
|
|
284
|
+
: `frozen: ${frozen.record.imageId} (${frozen.record.opencodeVersion})`);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
/** Execute the selected slots. */
|
|
288
|
+
async function run(options) {
|
|
289
|
+
const summary = await runCampaign(requireRoot(options), {
|
|
290
|
+
...(options.slots.length === 0 ? {} : { slotIds: options.slots }),
|
|
291
|
+
dryRun: options.dryRun,
|
|
292
|
+
passthrough: options.passthrough,
|
|
293
|
+
});
|
|
294
|
+
if (options.json) {
|
|
295
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
for (const slotId of summary.ran) {
|
|
299
|
+
console.log(`${options.dryRun ? "planned" : "ran"} ${slotId}`);
|
|
300
|
+
}
|
|
301
|
+
for (const problem of summary.problems) {
|
|
302
|
+
console.error(`otc: ${problem}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return summary.ok ? 0 : 1;
|
|
306
|
+
}
|
|
307
|
+
/** Blindly judge selected docs slots from checked evidence. */
|
|
308
|
+
async function judge(options) {
|
|
309
|
+
const summary = await judgeCampaign(requireRoot(options), {
|
|
310
|
+
...(options.slots.length === 0 ? {} : { slotIds: options.slots }),
|
|
311
|
+
dryRun: options.dryRun,
|
|
312
|
+
});
|
|
313
|
+
if (options.json) {
|
|
314
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
for (const slotId of summary.judged) {
|
|
318
|
+
console.log(`${options.dryRun ? "would judge" : "judged"} ${slotId}`);
|
|
319
|
+
}
|
|
320
|
+
for (const slotId of summary.skipped) {
|
|
321
|
+
console.log(`already judged ${slotId}`);
|
|
322
|
+
}
|
|
323
|
+
for (const problem of summary.problems) {
|
|
324
|
+
console.error(`otc: ${problem}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return summary.ok ? 0 : 1;
|
|
328
|
+
}
|
|
329
|
+
/** Deterministically assess captured evidence. */
|
|
330
|
+
function assess(options) {
|
|
331
|
+
const summary = assessCampaign(requireRoot(options), {
|
|
332
|
+
...(options.slots.length === 0 ? {} : { slotIds: options.slots }),
|
|
333
|
+
});
|
|
334
|
+
if (options.json) {
|
|
335
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
console.log(`assessed ${summary.summary.assessed}: pass ${summary.summary.pass}, fail ${summary.summary.fail}, unknown ${summary.summary.unknown}, invalid ${summary.summary.invalid}, unrun ${summary.summary.unrun}`);
|
|
339
|
+
for (const problem of summary.problems) {
|
|
340
|
+
console.error(`otc: ${problem}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return summary.ok ? 0 : 1;
|
|
344
|
+
}
|
|
345
|
+
/** Close the campaign ledger. */
|
|
346
|
+
function close(options) {
|
|
347
|
+
const closed = closeCampaign(requireRoot(options));
|
|
348
|
+
if (!closed.ok)
|
|
349
|
+
return fail(closed.problems, 1);
|
|
350
|
+
console.log(closed.alreadyClosed
|
|
351
|
+
? `already closed at ${closed.closedAt}`
|
|
352
|
+
: `closed at ${closed.closedAt}`);
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
/** Render final analysis after closure. */
|
|
356
|
+
function analyse(options) {
|
|
357
|
+
const analysed = analyseCampaign(requireRoot(options), options.out ?? "");
|
|
358
|
+
if (!analysed.ok)
|
|
359
|
+
return fail(analysed.problems, 1);
|
|
360
|
+
console.log(analysed.markdown);
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
/** Print checked, read-only status. */
|
|
364
|
+
function status(options) {
|
|
365
|
+
const status = statusOf(requireRoot(options));
|
|
366
|
+
if (options.json) {
|
|
367
|
+
console.log(JSON.stringify(status, null, 2));
|
|
368
|
+
return status.state === "blocked" ? 1 : 0;
|
|
369
|
+
}
|
|
370
|
+
console.log([
|
|
371
|
+
`campaign ${status.campaignId ?? "unknown"} (${status.state})`,
|
|
372
|
+
`slots ${status.counts.slots}; started ${status.counts.started}; assessed ${status.counts.assessed}`,
|
|
373
|
+
...(status.problems.length === 0
|
|
374
|
+
? []
|
|
375
|
+
: [`problems: ${status.problems.join("; ")}`]),
|
|
376
|
+
...status.nextActions.map((action) => `next: ${action.command} — ${action.reason}`),
|
|
377
|
+
].join("\n"));
|
|
378
|
+
return status.state === "blocked" ? 1 : 0;
|
|
379
|
+
}
|
|
380
|
+
/** Replay a recorded docs campaign through the checked docs evaluator. */
|
|
381
|
+
function replay(options) {
|
|
382
|
+
if (options.manifest === null ||
|
|
383
|
+
options.evidenceRoot === null ||
|
|
384
|
+
options.out === null) {
|
|
385
|
+
return fail(["replay needs --manifest, --evidence-root and --out"]);
|
|
386
|
+
}
|
|
387
|
+
const flags = [
|
|
388
|
+
"--campaign",
|
|
389
|
+
options.manifest,
|
|
390
|
+
"--evidence-root",
|
|
391
|
+
options.evidenceRoot,
|
|
392
|
+
"--out",
|
|
393
|
+
options.out,
|
|
394
|
+
...(options.rerender ? ["--rerender"] : []),
|
|
395
|
+
...(options.allowLegacyUnbound ? ["--allow-legacy-unbound"] : []),
|
|
396
|
+
];
|
|
397
|
+
const result = runAnalysis(flags);
|
|
398
|
+
for (const message of result.messages)
|
|
399
|
+
console.log(message);
|
|
400
|
+
return result.status;
|
|
401
|
+
}
|
|
402
|
+
/** The command dispatch table. */
|
|
403
|
+
const HANDLERS = {
|
|
404
|
+
validate,
|
|
405
|
+
prepare,
|
|
406
|
+
"delivery-check": checkDelivery,
|
|
407
|
+
freeze,
|
|
408
|
+
run,
|
|
409
|
+
judge,
|
|
410
|
+
assess,
|
|
411
|
+
close,
|
|
412
|
+
analyse,
|
|
413
|
+
status,
|
|
414
|
+
replay,
|
|
415
|
+
};
|
|
416
|
+
const invokedDirectly = process.argv[1] !== undefined &&
|
|
417
|
+
(() => {
|
|
418
|
+
try {
|
|
419
|
+
return (realpathSync(process.argv[1]) ===
|
|
420
|
+
realpathSync(fileURLToPath(import.meta.url)));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
})();
|
|
426
|
+
if (invokedDirectly) {
|
|
427
|
+
main(process.argv.slice(2)).then((code) => {
|
|
428
|
+
process.exitCode = code;
|
|
429
|
+
}, (error) => {
|
|
430
|
+
console.error(error instanceof Error
|
|
431
|
+
? (error.stack ?? String(error))
|
|
432
|
+
: String(error));
|
|
433
|
+
process.exitCode = 1;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable JSON for identity purposes: object keys are sorted recursively,
|
|
3
|
+
* array order is preserved, and `undefined` members are omitted. Freeze
|
|
4
|
+
* identities and receipts use this serialisation so that formatting cannot
|
|
5
|
+
* change an identity.
|
|
6
|
+
* @param value Any JSON-compatible value.
|
|
7
|
+
* @returns Canonical JSON text.
|
|
8
|
+
*/
|
|
9
|
+
export declare function canonicalJson(value: unknown): string;
|
|
10
|
+
/**
|
|
11
|
+
* The SHA-256 identity of a value's canonical JSON.
|
|
12
|
+
* @param value Any JSON-compatible value.
|
|
13
|
+
* @returns The lower-case hex digest.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sha256Of(value: unknown): string;
|
|
16
|
+
/**
|
|
17
|
+
* The SHA-256 identity of exact bytes.
|
|
18
|
+
* @param bytes The bytes to hash.
|
|
19
|
+
* @returns The lower-case hex digest.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sha256Bytes(bytes: Buffer | string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Stable JSON for identity purposes: object keys are sorted recursively,
|
|
4
|
+
* array order is preserved, and `undefined` members are omitted. Freeze
|
|
5
|
+
* identities and receipts use this serialisation so that formatting cannot
|
|
6
|
+
* change an identity.
|
|
7
|
+
* @param value Any JSON-compatible value.
|
|
8
|
+
* @returns Canonical JSON text.
|
|
9
|
+
*/
|
|
10
|
+
export function canonicalJson(value) {
|
|
11
|
+
return JSON.stringify(sortValue(value));
|
|
12
|
+
}
|
|
13
|
+
/** Recursively sort object keys, leaving arrays and primitives alone. */
|
|
14
|
+
function sortValue(value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.map(sortValue);
|
|
17
|
+
if (value === null || typeof value !== "object")
|
|
18
|
+
return value;
|
|
19
|
+
const sorted = {};
|
|
20
|
+
for (const key of Object.keys(value).sort()) {
|
|
21
|
+
const entry = value[key];
|
|
22
|
+
if (entry !== undefined)
|
|
23
|
+
sorted[key] = sortValue(entry);
|
|
24
|
+
}
|
|
25
|
+
return sorted;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The SHA-256 identity of a value's canonical JSON.
|
|
29
|
+
* @param value Any JSON-compatible value.
|
|
30
|
+
* @returns The lower-case hex digest.
|
|
31
|
+
*/
|
|
32
|
+
export function sha256Of(value) {
|
|
33
|
+
return createHash("sha256")
|
|
34
|
+
.update(canonicalJson(value), "utf8")
|
|
35
|
+
.digest("hex");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The SHA-256 identity of exact bytes.
|
|
39
|
+
* @param bytes The bytes to hash.
|
|
40
|
+
* @returns The lower-case hex digest.
|
|
41
|
+
*/
|
|
42
|
+
export function sha256Bytes(bytes) {
|
|
43
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
44
|
+
}
|