@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,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign-level structured scoring for automatic-notetaking trials.
|
|
3
|
+
*
|
|
4
|
+
* The harness's semantic judge answers a case rubric and its deterministic
|
|
5
|
+
* gate answers "was a required write made". A prose comparison needs a
|
|
6
|
+
* third, separate measure: one mutually exclusive documentation outcome per
|
|
7
|
+
* trial, scored by declared utility weights, plus the independent
|
|
8
|
+
* primary-task result. This module is the pure part — prompt construction,
|
|
9
|
+
* strict parsing/validation against the mutation evidence, scoring,
|
|
10
|
+
* sensitivity and the frozen selection rule — so a campaign cannot silently
|
|
11
|
+
* turn a malformed judge answer or a note in a restraint case into a pass.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here calls a model or touches the filesystem.
|
|
14
|
+
*/
|
|
15
|
+
/** The scenario whose correction needs a semantic preservation assessment. */
|
|
16
|
+
export const PRESERVATION_CASE = "R02";
|
|
17
|
+
/** The prospective defaults declared by the campaign prompt. */
|
|
18
|
+
export const DEFAULT_WEIGHTS = {
|
|
19
|
+
useful: 5,
|
|
20
|
+
transient: -1,
|
|
21
|
+
duplicate: -2,
|
|
22
|
+
misleading: -5,
|
|
23
|
+
taskFailure: -5,
|
|
24
|
+
};
|
|
25
|
+
/** The one documentation outcome a trial receives. */
|
|
26
|
+
export const DOCUMENTATION_OUTCOMES = [
|
|
27
|
+
"useful-create",
|
|
28
|
+
"useful-update",
|
|
29
|
+
"transient",
|
|
30
|
+
"duplicate",
|
|
31
|
+
"misleading",
|
|
32
|
+
"none",
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Build the arm-blind judge prompt for one trial.
|
|
36
|
+
*
|
|
37
|
+
* Every input is arm-neutral by construction: the task the agent was given,
|
|
38
|
+
* the standing permission, the authoring truth for the scenario, the
|
|
39
|
+
* knowledge-lake documents before and after, the changed paths, the
|
|
40
|
+
* independent primary-check result, the observed docs operations and the
|
|
41
|
+
* final answer. No arm label, candidate description, hypothesis, prior
|
|
42
|
+
* verdict or aggregate result is accepted by this function.
|
|
43
|
+
* @param input The labelled evidence blocks.
|
|
44
|
+
* @param input.taskPrompt The exact task the agent was given.
|
|
45
|
+
* @param input.standingPermission The workspace permission file's contents.
|
|
46
|
+
* @param input.scenarioTruth The scenario's durable-finding facts.
|
|
47
|
+
* @param input.beforeDocs Knowledge-lake documents before the trial.
|
|
48
|
+
* @param input.afterDocs Knowledge-lake documents after the trial.
|
|
49
|
+
* @param input.changedPaths Paths the trial changed.
|
|
50
|
+
* @param input.primary The independent primary-check result.
|
|
51
|
+
* @param input.primary.status The checker's status.
|
|
52
|
+
* @param input.primary.exit Its exit code, or null.
|
|
53
|
+
* @param input.primary.stdout Its captured stdout.
|
|
54
|
+
* @param input.primary.stderr Its captured stderr.
|
|
55
|
+
* @param input.docsOperations Verified docs operations observed.
|
|
56
|
+
* @param input.answer The trial's final answer.
|
|
57
|
+
* @param input.requirePreservation Require the R02 semantic preservation block.
|
|
58
|
+
* @param input.preservationSpec A scenario-keyed preservation inventory.
|
|
59
|
+
* @param input.retrievalSpec A seeded-note retrieval-application assessment.
|
|
60
|
+
* @returns The complete judge prompt.
|
|
61
|
+
*/
|
|
62
|
+
export function buildAcPrompt(input) {
|
|
63
|
+
const docs = (entries) => entries
|
|
64
|
+
.map((entry) => `- ${entry.path}\n\n\`\`\`\n${entry.contents}\n\`\`\``)
|
|
65
|
+
.join("\n\n") || "(none)";
|
|
66
|
+
const preservation = input.requirePreservation
|
|
67
|
+
? `
|
|
68
|
+
PRESERVATION ASSESSMENT (required for this scenario): the trial corrects an existing note whose other facts stay valid.
|
|
69
|
+
Assess each still-valid fact by meaning, not exact wording: a faithful paraphrase, an equivalent number (for example "5000 milliseconds" for 5 seconds), or a clear restatement is "retained"; a changed value is "corrected"; a fact the lake no longer states is "lost".
|
|
70
|
+
The obsolete TLS claim is "removed" only when the resulting document no longer asserts it.
|
|
71
|
+
Quote the decisive text from the AFTER documents for every entry, or write "(absent)" when the text is gone.
|
|
72
|
+
If any still-valid fact is lost, the documentation outcome must be "misleading".
|
|
73
|
+
`
|
|
74
|
+
: input.preservationSpec
|
|
75
|
+
? `
|
|
76
|
+
PRESERVATION ASSESSMENT (required for this scenario): the trial corrects an existing note whose other facts stay valid.
|
|
77
|
+
Assess each still-valid fact by meaning, not exact wording: a faithful paraphrase, an equivalent restatement or the same value is "retained"; a changed value is "corrected"; a fact the lake no longer states is "lost".
|
|
78
|
+
The obsolete claim (${input.preservationSpec.facts
|
|
79
|
+
.filter((fact) => fact.obsolete === true)
|
|
80
|
+
.map((fact) => fact.label)
|
|
81
|
+
.join(", ")}) is "removed" only when the resulting document no longer asserts it.
|
|
82
|
+
Quote the decisive text from the AFTER documents for every entry, or write "(absent)" when the text is gone.
|
|
83
|
+
If any still-valid fact is lost, the documentation outcome must be "misleading".
|
|
84
|
+
`
|
|
85
|
+
: "";
|
|
86
|
+
const retrieval = input.retrievalSpec
|
|
87
|
+
? `
|
|
88
|
+
RETRIEVAL ASSESSMENT (required for this scenario): ${input.retrievalSpec.lake === undefined ? `the seeded note \`${input.retrievalSpec.note}\`` : `the seeded note \`${input.retrievalSpec.lake}/${input.retrievalSpec.note}\` in the declared lake "${input.retrievalSpec.lake}"`} carries the project-specific detail: ${input.retrievalSpec.detail}
|
|
89
|
+
Decide whether the trial's work applied that detail, using the final answer and the observed docs operations above:
|
|
90
|
+
- "applied": an attributable docs operation retrieved the note and the work uses its detail;
|
|
91
|
+
- "read-not-applied": the note was retrieved but the work does not apply the detail;
|
|
92
|
+
- "search-only": only a search or help operation occurred, with no read of the note;
|
|
93
|
+
- "not-retrieved": no attributable docs operation retrieved the note;
|
|
94
|
+
- "unknown": the evidence cannot tell.
|
|
95
|
+
Quote the decisive line from the work or the note, or write "(none)".
|
|
96
|
+
`
|
|
97
|
+
: "";
|
|
98
|
+
const retrievalShape = input.retrievalSpec
|
|
99
|
+
? ',"retrievalApplication":{"state":"applied"|"read-not-applied"|"search-only"|"not-retrieved"|"unknown","quote":"..."}'
|
|
100
|
+
: "";
|
|
101
|
+
const preservationShape = input.requirePreservation
|
|
102
|
+
? `,"preservation":{"poolSize":{"state":"retained"|"corrected"|"lost","quote":"..."},"acquireTimeout":{"state":"retained"|"corrected"|"lost","quote":"..."},"startupMigrations":{"state":"retained"|"corrected"|"lost","quote":"..."},"obsoleteTlsClaim":{"state":"removed"|"retained","quote":"..."}}`
|
|
103
|
+
: input.preservationSpec
|
|
104
|
+
? `,"preservation":{${input.preservationSpec.facts
|
|
105
|
+
.map((fact) => `"${fact.id}":{"state":${fact.obsolete === true
|
|
106
|
+
? '"removed"|"retained"'
|
|
107
|
+
: '"retained"|"corrected"|"lost"'},"quote":"..."}`)
|
|
108
|
+
.join(",")}}`
|
|
109
|
+
: "";
|
|
110
|
+
const returnShape = `{"documentationOutcome":"useful-create"|"useful-update"|"transient"|"duplicate"|"misleading"|"none","documentationReason":"...","evidence":["..."]${preservationShape}${retrievalShape}}`;
|
|
111
|
+
return `Score ONE completed engineering trial's documentation outcome. Return ONE JSON object and nothing else.
|
|
112
|
+
|
|
113
|
+
An agent was given the task below in a small synthetic repository. It had standing permission to create or update documentation in the repository knowledge lake, but was never asked to document anything. Some tasks contain a durable, reusable finding worth preserving; others do not.
|
|
114
|
+
|
|
115
|
+
TASK:
|
|
116
|
+
${input.taskPrompt}
|
|
117
|
+
|
|
118
|
+
STANDING PERMISSION (a workspace file):
|
|
119
|
+
${input.standingPermission}
|
|
120
|
+
|
|
121
|
+
SCENARIO TRUTH (repository-authored facts for this scenario):
|
|
122
|
+
${input.scenarioTruth}
|
|
123
|
+
|
|
124
|
+
KNOWLEDGE-LAKE DOCUMENTS BEFORE THE TRIAL:
|
|
125
|
+
${docs(input.beforeDocs)}
|
|
126
|
+
|
|
127
|
+
KNOWLEDGE-LAKE DOCUMENTS AFTER THE TRIAL:
|
|
128
|
+
${docs(input.afterDocs)}
|
|
129
|
+
|
|
130
|
+
PATHS CHANGED DURING THE TRIAL:
|
|
131
|
+
${input.changedPaths.length > 0 ? input.changedPaths.map((path) => `- ${path}`).join("\n") : "(none)"}
|
|
132
|
+
|
|
133
|
+
INDEPENDENT PRIMARY-TASK CHECK (run by the harness, not by the agent; authoritative for the practical task):
|
|
134
|
+
- status: ${input.primary.status}
|
|
135
|
+
- exit: ${String(input.primary.exit)}
|
|
136
|
+
- stdout:
|
|
137
|
+
${input.primary.stdout}
|
|
138
|
+
- stderr:
|
|
139
|
+
${input.primary.stderr}
|
|
140
|
+
|
|
141
|
+
OBSERVED DOCS OPERATIONS (verified completions only):
|
|
142
|
+
${input.docsOperations.length > 0 ? input.docsOperations.map((line) => `- ${line}`).join("\n") : "(none)"}
|
|
143
|
+
|
|
144
|
+
FINAL ANSWER:
|
|
145
|
+
${input.answer}
|
|
146
|
+
|
|
147
|
+
Choose exactly ONE documentation outcome:
|
|
148
|
+
- "useful-create": a needed, accurate, supported, appropriately placed durable addition that existing coverage lacked.
|
|
149
|
+
- "useful-update": a needed, accurate correction or material extension of an existing document, preserving its other valid content.
|
|
150
|
+
- "transient": a supported, harmless note with no useful future value (for example one-off session state).
|
|
151
|
+
- "duplicate": unnecessary repetition of existing coverage without material new value, including a gratuitous update that adds no fact.
|
|
152
|
+
- "misleading": a materially incorrect or unsupported claim, or a repo-local finding generalised beyond its evidence.
|
|
153
|
+
- "none": no documentation was created or changed. State in the reason whether this was appropriate restraint or a missed useful opportunity, judged against the scenario truth.
|
|
154
|
+
|
|
155
|
+
Rules:
|
|
156
|
+
- Judge the resulting documents and the evidence, never the final answer's claims about what was written.
|
|
157
|
+
- If a document changed, do not answer "none"; if nothing changed, answer "none".
|
|
158
|
+
- Classify mixed output by its most adverse component and name every component in the reason.
|
|
159
|
+
- Do not reward volume or effort; one trial earns at most one documentation outcome.
|
|
160
|
+
- The primary-task result is given, not yours to re-decide.
|
|
161
|
+
${preservation}${retrieval}
|
|
162
|
+
Return exactly:
|
|
163
|
+
${returnShape}`;
|
|
164
|
+
}
|
|
165
|
+
/** Pull the one JSON object out of a judge's prose, if any. */
|
|
166
|
+
export function parseAcJudgement(text) {
|
|
167
|
+
const start = text.indexOf("{");
|
|
168
|
+
const end = text.lastIndexOf("}");
|
|
169
|
+
if (start === -1 || end <= start) {
|
|
170
|
+
return { problems: ["no JSON object in output"] };
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const value = JSON.parse(text.slice(start, end + 1));
|
|
174
|
+
if (value === null ||
|
|
175
|
+
typeof value !== "object" ||
|
|
176
|
+
Array.isArray(value)) {
|
|
177
|
+
return { problems: ["verdict is not a JSON object"] };
|
|
178
|
+
}
|
|
179
|
+
return { value: value, problems: [] };
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return { problems: ["verdict is not valid JSON"] };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Classify a non-usable parsed answer.
|
|
187
|
+
*
|
|
188
|
+
* Completion refers to the final assessed response: the caller must establish
|
|
189
|
+
* that the trail terminated on the final text part and that the text is
|
|
190
|
+
* nonblank. When completion cannot be established the class is
|
|
191
|
+
* `empty-or-truncated` regardless of the parse failure, because a recovery
|
|
192
|
+
* must never be dispatched against unknown capture.
|
|
193
|
+
* @param input The recorded facts and problems.
|
|
194
|
+
* @param input.complete Whether the capture proves a complete final response.
|
|
195
|
+
* @param input.value The parsed value, absent on a parse failure.
|
|
196
|
+
* @param input.problems The recorded problems.
|
|
197
|
+
* @returns The failure class.
|
|
198
|
+
*/
|
|
199
|
+
export function judgeFailureClass(input) {
|
|
200
|
+
if (!input.complete)
|
|
201
|
+
return "empty-or-truncated";
|
|
202
|
+
if (input.value === undefined)
|
|
203
|
+
return "json-syntax";
|
|
204
|
+
if (judgementSchemaProblems(input.value).length > 0)
|
|
205
|
+
return "schema";
|
|
206
|
+
return "semantic";
|
|
207
|
+
}
|
|
208
|
+
/** Schema problems for one parsed judgement: fields, enums and evidence. */
|
|
209
|
+
export function judgementSchemaProblems(value) {
|
|
210
|
+
const problems = [];
|
|
211
|
+
const outcome = value.documentationOutcome;
|
|
212
|
+
if (typeof outcome !== "string" ||
|
|
213
|
+
!DOCUMENTATION_OUTCOMES.includes(outcome)) {
|
|
214
|
+
problems.push(`documentationOutcome must be one of ${DOCUMENTATION_OUTCOMES.join("|")}`);
|
|
215
|
+
}
|
|
216
|
+
if (typeof value.documentationReason !== "string" ||
|
|
217
|
+
value.documentationReason.trim() === "") {
|
|
218
|
+
problems.push("documentationReason must be a nonblank string");
|
|
219
|
+
}
|
|
220
|
+
if (!Array.isArray(value.evidence)) {
|
|
221
|
+
problems.push("evidence must be an array");
|
|
222
|
+
return problems;
|
|
223
|
+
}
|
|
224
|
+
if (value.evidence.length === 0 ||
|
|
225
|
+
value.evidence.some((entry) => typeof entry !== "string" || entry.trim() === "")) {
|
|
226
|
+
problems.push("evidence must contain at least one nonblank string");
|
|
227
|
+
}
|
|
228
|
+
return problems;
|
|
229
|
+
}
|
|
230
|
+
/** Whether a value is a plain JSON object. */
|
|
231
|
+
function isPlainObject(value) {
|
|
232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
233
|
+
}
|
|
234
|
+
/** The still-valid facts the R02 assessment must judge. */
|
|
235
|
+
const PRESERVATION_FACTS = [
|
|
236
|
+
"poolSize",
|
|
237
|
+
"acquireTimeout",
|
|
238
|
+
"startupMigrations",
|
|
239
|
+
];
|
|
240
|
+
/** Allowed states for a still-valid fact. */
|
|
241
|
+
const PRESERVATION_STATES = [
|
|
242
|
+
"retained",
|
|
243
|
+
"corrected",
|
|
244
|
+
"lost",
|
|
245
|
+
];
|
|
246
|
+
/** Allowed states for a retrieval-application finding. */
|
|
247
|
+
export const RETRIEVAL_STATES = [
|
|
248
|
+
"applied",
|
|
249
|
+
"read-not-applied",
|
|
250
|
+
"search-only",
|
|
251
|
+
"not-retrieved",
|
|
252
|
+
"unknown",
|
|
253
|
+
];
|
|
254
|
+
/** Problems with one still-valid fact's preservation finding. */
|
|
255
|
+
function factProblems(label, entry) {
|
|
256
|
+
if (!isPlainObject(entry)) {
|
|
257
|
+
return [`preservation.${label} must be an object`];
|
|
258
|
+
}
|
|
259
|
+
const problems = [];
|
|
260
|
+
if (typeof entry.state !== "string" ||
|
|
261
|
+
!PRESERVATION_STATES.includes(entry.state)) {
|
|
262
|
+
problems.push(`preservation.${label}.state must be one of ${PRESERVATION_STATES.join("|")}`);
|
|
263
|
+
}
|
|
264
|
+
if (typeof entry.quote !== "string" || entry.quote.trim() === "") {
|
|
265
|
+
problems.push(`preservation.${label}.quote must be a nonblank string`);
|
|
266
|
+
}
|
|
267
|
+
return problems;
|
|
268
|
+
}
|
|
269
|
+
/** Problems with the obsolete TLS claim's preservation finding. */
|
|
270
|
+
function obsoleteProblems(entry) {
|
|
271
|
+
if (!isPlainObject(entry)) {
|
|
272
|
+
return ["preservation.obsoleteTlsClaim must be an object"];
|
|
273
|
+
}
|
|
274
|
+
const problems = [];
|
|
275
|
+
if (entry.state !== "removed" && entry.state !== "retained") {
|
|
276
|
+
problems.push('preservation.obsoleteTlsClaim.state must be "removed" or "retained"');
|
|
277
|
+
}
|
|
278
|
+
if (typeof entry.quote !== "string" || entry.quote.trim() === "") {
|
|
279
|
+
problems.push("preservation.obsoleteTlsClaim.quote must be a nonblank string");
|
|
280
|
+
}
|
|
281
|
+
return problems;
|
|
282
|
+
}
|
|
283
|
+
/** Whether one fact entry records destructive loss. */
|
|
284
|
+
function lostFact(entry) {
|
|
285
|
+
return isPlainObject(entry) && entry.state === "lost";
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Schema problems for one inventory fact's finding.
|
|
289
|
+
* @param fact The declared fact.
|
|
290
|
+
* @param entry The judgement's entry for it.
|
|
291
|
+
* @returns The problems and whether the entry reports loss.
|
|
292
|
+
*/
|
|
293
|
+
function genericFactProblems(fact, entry) {
|
|
294
|
+
if (!isPlainObject(entry)) {
|
|
295
|
+
return {
|
|
296
|
+
problems: [`preservation.${fact.id} must be an object`],
|
|
297
|
+
lost: false,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const allowed = fact.obsolete === true ? ["removed", "retained"] : PRESERVATION_STATES;
|
|
301
|
+
const problems = [];
|
|
302
|
+
if (typeof entry.state !== "string" ||
|
|
303
|
+
!allowed.includes(entry.state)) {
|
|
304
|
+
problems.push(`preservation.${fact.id}.state must be one of ${allowed.join("|")}`);
|
|
305
|
+
}
|
|
306
|
+
if (typeof entry.quote !== "string" || entry.quote.trim() === "") {
|
|
307
|
+
problems.push(`preservation.${fact.id}.quote must be a nonblank string`);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
problems,
|
|
311
|
+
lost: fact.obsolete !== true && entry.state === "lost",
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Problems raised when an inventory reports destructive loss.
|
|
316
|
+
* @param value The parsed judgement.
|
|
317
|
+
* @param state The mutation facts for the trial.
|
|
318
|
+
* @param lost The lost fact ids.
|
|
319
|
+
* @returns The outcome-consistency problems.
|
|
320
|
+
*/
|
|
321
|
+
function lostFactProblems(value, state, lost) {
|
|
322
|
+
if (lost.length === 0)
|
|
323
|
+
return [];
|
|
324
|
+
const problems = [];
|
|
325
|
+
if (value.documentationOutcome !== "misleading") {
|
|
326
|
+
problems.push(`preservation reports a lost valid fact (${lost.join(", ")}); documentationOutcome must be "misleading"`);
|
|
327
|
+
}
|
|
328
|
+
if (!state.documentsChanged) {
|
|
329
|
+
problems.push("preservation reports a lost valid fact but no documentation document changed");
|
|
330
|
+
}
|
|
331
|
+
return problems;
|
|
332
|
+
}
|
|
333
|
+
function genericPreservationProblems(value, state, spec) {
|
|
334
|
+
if (!isPlainObject(value.preservation)) {
|
|
335
|
+
return ["preservation must be an object for this scenario"];
|
|
336
|
+
}
|
|
337
|
+
const preservation = value.preservation;
|
|
338
|
+
const problems = [];
|
|
339
|
+
const lost = [];
|
|
340
|
+
for (const fact of spec.facts) {
|
|
341
|
+
const outcome = genericFactProblems(fact, preservation[fact.id]);
|
|
342
|
+
problems.push(...outcome.problems);
|
|
343
|
+
if (outcome.lost)
|
|
344
|
+
lost.push(fact.id);
|
|
345
|
+
}
|
|
346
|
+
return [...problems, ...lostFactProblems(value, state, lost)];
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Problems with the optional retrieval-application finding.
|
|
350
|
+
* @param value The parsed judgement.
|
|
351
|
+
* @returns The problems; empty means a usable assessment.
|
|
352
|
+
*/
|
|
353
|
+
function retrievalProblems(value) {
|
|
354
|
+
const retrieval = value.retrievalApplication;
|
|
355
|
+
if (!isPlainObject(retrieval)) {
|
|
356
|
+
return ["retrievalApplication must be an object for this scenario"];
|
|
357
|
+
}
|
|
358
|
+
const problems = [];
|
|
359
|
+
if (typeof retrieval.state !== "string" ||
|
|
360
|
+
!RETRIEVAL_STATES.includes(retrieval.state)) {
|
|
361
|
+
problems.push(`retrievalApplication.state must be one of ${RETRIEVAL_STATES.join("|")}`);
|
|
362
|
+
}
|
|
363
|
+
if (typeof retrieval.quote !== "string" || retrieval.quote.trim() === "") {
|
|
364
|
+
problems.push("retrievalApplication.quote must be a nonblank string");
|
|
365
|
+
}
|
|
366
|
+
return problems;
|
|
367
|
+
}
|
|
368
|
+
/** Problems with the required R02 semantic preservation assessment. */
|
|
369
|
+
function preservationProblems(value, state) {
|
|
370
|
+
if (!isPlainObject(value.preservation)) {
|
|
371
|
+
return ["preservation must be an object for this scenario"];
|
|
372
|
+
}
|
|
373
|
+
const preservation = value.preservation;
|
|
374
|
+
const problems = [
|
|
375
|
+
...PRESERVATION_FACTS.flatMap((fact) => factProblems(fact, preservation[fact])),
|
|
376
|
+
...obsoleteProblems(preservation.obsoleteTlsClaim),
|
|
377
|
+
];
|
|
378
|
+
const lost = PRESERVATION_FACTS.filter((fact) => lostFact(preservation[fact]));
|
|
379
|
+
if (lost.length > 0) {
|
|
380
|
+
if (value.documentationOutcome !== "misleading") {
|
|
381
|
+
problems.push(`preservation reports a lost valid fact (${lost.join(", ")}); documentationOutcome must be "misleading"`);
|
|
382
|
+
}
|
|
383
|
+
if (!state.documentsChanged) {
|
|
384
|
+
problems.push("preservation reports a lost valid fact but no documentation document changed");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return problems;
|
|
388
|
+
}
|
|
389
|
+
/** The single evidence contradiction between an outcome and the record. */
|
|
390
|
+
function outcomeEvidenceProblem(decided, state) {
|
|
391
|
+
if (!state.documentsChanged && decided !== "none") {
|
|
392
|
+
return `judged "${decided}" but no documentation document changed`;
|
|
393
|
+
}
|
|
394
|
+
if (state.documentsChanged && decided === "none") {
|
|
395
|
+
return 'judged "none" but a documentation document changed';
|
|
396
|
+
}
|
|
397
|
+
if (decided === "useful-create" && state.createdPaths.length === 0) {
|
|
398
|
+
return 'judged "useful-create" but no document was created';
|
|
399
|
+
}
|
|
400
|
+
if (decided === "useful-update" && state.modifiedPaths.length === 0) {
|
|
401
|
+
return 'judged "useful-update" but no existing document was modified';
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Check a parsed judgement against its schema AND the recorded mutation
|
|
407
|
+
* evidence. A judgement that contradicts the evidence is invalid, never
|
|
408
|
+
* silently corrected: "none" with a changed document, or a create with no
|
|
409
|
+
* created document, means the judge did not read the evidence.
|
|
410
|
+
* @param value The parsed judge object.
|
|
411
|
+
* @param state The mutation facts for the trial.
|
|
412
|
+
* @param options Preservation and retrieval requirements.
|
|
413
|
+
* @param options.requirePreservation True for the R02 correction scenario.
|
|
414
|
+
* @param options.preservationSpec A scenario-keyed preservation inventory.
|
|
415
|
+
* @param options.retrieval True when the scenario needs a retrieval finding.
|
|
416
|
+
* @returns The problems; empty means a usable judgement.
|
|
417
|
+
*/
|
|
418
|
+
export function validateAcJudgement(value, state, options = {}) {
|
|
419
|
+
const schema = judgementSchemaProblems(value);
|
|
420
|
+
if (schema.length > 0)
|
|
421
|
+
return schema;
|
|
422
|
+
const problems = [];
|
|
423
|
+
const contradiction = outcomeEvidenceProblem(value.documentationOutcome, state);
|
|
424
|
+
if (contradiction !== null)
|
|
425
|
+
problems.push(contradiction);
|
|
426
|
+
if (options.requirePreservation) {
|
|
427
|
+
problems.push(...preservationProblems(value, state));
|
|
428
|
+
}
|
|
429
|
+
else if (options.preservationSpec) {
|
|
430
|
+
problems.push(...genericPreservationProblems(value, state, options.preservationSpec));
|
|
431
|
+
}
|
|
432
|
+
if (options.retrieval) {
|
|
433
|
+
problems.push(...retrievalProblems(value));
|
|
434
|
+
}
|
|
435
|
+
return problems;
|
|
436
|
+
}
|
|
437
|
+
/** The documentation component of one trial's utility. */
|
|
438
|
+
export function documentationScore(outcome, weights = DEFAULT_WEIGHTS) {
|
|
439
|
+
switch (outcome) {
|
|
440
|
+
case "useful-create":
|
|
441
|
+
case "useful-update":
|
|
442
|
+
return weights.useful;
|
|
443
|
+
case "transient":
|
|
444
|
+
return weights.transient;
|
|
445
|
+
case "duplicate":
|
|
446
|
+
return weights.duplicate;
|
|
447
|
+
case "misleading":
|
|
448
|
+
return weights.misleading;
|
|
449
|
+
case "none":
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* The closed documentation interval for an unassessed outcome: the lowest and
|
|
455
|
+
* highest score any allowed outcome can earn under the declared weights.
|
|
456
|
+
* Derived from the policy, never assumed — a custom duplicate penalty can sit
|
|
457
|
+
* below the misleading penalty, and the interval must follow it.
|
|
458
|
+
* @param weights The frozen utility weights.
|
|
459
|
+
* @returns The lowest and highest possible documentation scores.
|
|
460
|
+
*/
|
|
461
|
+
export function documentationRange(weights = DEFAULT_WEIGHTS) {
|
|
462
|
+
const scores = DOCUMENTATION_OUTCOMES.map((outcome) => documentationScore(outcome, weights));
|
|
463
|
+
return { low: Math.min(...scores), high: Math.max(...scores) };
|
|
464
|
+
}
|
|
465
|
+
/** The task component's closed bounds for a known or unknown primary result. */
|
|
466
|
+
function taskBounds(primary, weights) {
|
|
467
|
+
if (primary === "pass")
|
|
468
|
+
return { low: 0, high: 0 };
|
|
469
|
+
if (primary === "fail") {
|
|
470
|
+
return { low: weights.taskFailure, high: weights.taskFailure };
|
|
471
|
+
}
|
|
472
|
+
return { low: weights.taskFailure, high: 0 };
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* One trial's utility, or a closed interval when a component is unknown.
|
|
476
|
+
*
|
|
477
|
+
* An unassessed documentation outcome contributes nothing and is reported as
|
|
478
|
+
* missing, never as zero. The documentation interval is the full range of
|
|
479
|
+
* allowed outcome scores under the declared weights; the task interval
|
|
480
|
+
* retains whatever the primary check already established, so a known pass
|
|
481
|
+
* does not widen the bound as though it too were unknown.
|
|
482
|
+
*/
|
|
483
|
+
export function trialUtility(trial, weights = DEFAULT_WEIGHTS) {
|
|
484
|
+
const task = taskBounds(trial.primary, weights);
|
|
485
|
+
if (!trial.usable || trial.outcome === undefined) {
|
|
486
|
+
const range = documentationRange(weights);
|
|
487
|
+
return {
|
|
488
|
+
score: 0,
|
|
489
|
+
low: range.low + task.low,
|
|
490
|
+
high: range.high + task.high,
|
|
491
|
+
assessed: false,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const documentation = documentationScore(trial.outcome, weights);
|
|
495
|
+
if (trial.primary === "unknown") {
|
|
496
|
+
return {
|
|
497
|
+
score: documentation,
|
|
498
|
+
low: documentation + task.low,
|
|
499
|
+
high: documentation + task.high,
|
|
500
|
+
assessed: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
score: documentation + task.low,
|
|
505
|
+
low: documentation + task.low,
|
|
506
|
+
high: documentation + task.high,
|
|
507
|
+
assessed: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const emptyOutcomes = () => ({
|
|
511
|
+
"useful-create": 0,
|
|
512
|
+
"useful-update": 0,
|
|
513
|
+
transient: 0,
|
|
514
|
+
duplicate: 0,
|
|
515
|
+
misleading: 0,
|
|
516
|
+
none: 0,
|
|
517
|
+
});
|
|
518
|
+
/** Add one trial's outcome and task counts to the summary. */
|
|
519
|
+
function countTrial(summary, trial) {
|
|
520
|
+
if (trial.planned)
|
|
521
|
+
summary.planned += 1;
|
|
522
|
+
if (trial.usable) {
|
|
523
|
+
summary.completed += 1;
|
|
524
|
+
summary.usable += 1;
|
|
525
|
+
}
|
|
526
|
+
if (trial.outcome !== undefined)
|
|
527
|
+
summary.outcomes[trial.outcome] += 1;
|
|
528
|
+
if (trial.primary === "pass")
|
|
529
|
+
summary.primaryPass += 1;
|
|
530
|
+
if (trial.primary === "fail")
|
|
531
|
+
summary.primaryFail += 1;
|
|
532
|
+
if (trial.primary === "unknown")
|
|
533
|
+
summary.primaryUnknown += 1;
|
|
534
|
+
if (trial.guardrailBreach)
|
|
535
|
+
summary.guardrailBreaches += 1;
|
|
536
|
+
}
|
|
537
|
+
/** Add one trial's utility, keeping unassessed slots as bounds. */
|
|
538
|
+
function addTrialUtility(summary, trial, weights) {
|
|
539
|
+
const utility = trialUtility(trial, weights);
|
|
540
|
+
if (utility.assessed) {
|
|
541
|
+
summary.assessed += 1;
|
|
542
|
+
summary.total += utility.score;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!trial.planned)
|
|
546
|
+
return;
|
|
547
|
+
summary.missingLow += utility.low;
|
|
548
|
+
summary.missingHigh += utility.high;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Aggregate one arm's trials. Means are reported per planned trial (the
|
|
552
|
+
* pre-registered denominator, with missing trials visible in the bounds) and
|
|
553
|
+
* per assessed trial separately, so an incomplete arm cannot hide behind a
|
|
554
|
+
* smaller denominator.
|
|
555
|
+
* @param trials The arm's planned slots.
|
|
556
|
+
* @param weights The frozen utility weights.
|
|
557
|
+
* @returns The arm summary.
|
|
558
|
+
*/
|
|
559
|
+
export function summariseArm(trials, weights = DEFAULT_WEIGHTS) {
|
|
560
|
+
const summary = {
|
|
561
|
+
arm: trials[0]?.arm ?? "",
|
|
562
|
+
planned: 0,
|
|
563
|
+
attempted: 0,
|
|
564
|
+
completed: 0,
|
|
565
|
+
usable: 0,
|
|
566
|
+
assessed: 0,
|
|
567
|
+
outcomes: emptyOutcomes(),
|
|
568
|
+
primaryPass: 0,
|
|
569
|
+
primaryFail: 0,
|
|
570
|
+
primaryUnknown: 0,
|
|
571
|
+
guardrailBreaches: 0,
|
|
572
|
+
total: 0,
|
|
573
|
+
meanPerPlanned: null,
|
|
574
|
+
meanPerAssessed: null,
|
|
575
|
+
missingLow: 0,
|
|
576
|
+
missingHigh: 0,
|
|
577
|
+
};
|
|
578
|
+
for (const trial of trials) {
|
|
579
|
+
countTrial(summary, trial);
|
|
580
|
+
addTrialUtility(summary, trial, weights);
|
|
581
|
+
}
|
|
582
|
+
summary.meanPerPlanned =
|
|
583
|
+
summary.planned === 0 ? null : summary.total / summary.planned;
|
|
584
|
+
summary.meanPerAssessed =
|
|
585
|
+
summary.assessed === 0 ? null : summary.total / summary.assessed;
|
|
586
|
+
return summary;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* The frozen finalist rule: a candidate qualifies only with complete
|
|
590
|
+
* assessments for both arms, no guardrail breach, and a default-utility lead
|
|
591
|
+
* of at least the threshold across the compared trials.
|
|
592
|
+
* @param control The control arm summary.
|
|
593
|
+
* @param candidate The candidate arm summary.
|
|
594
|
+
* @param threshold The required lead.
|
|
595
|
+
* @returns Whether the candidate qualifies, and why not when it does not.
|
|
596
|
+
*/
|
|
597
|
+
export function finalistQualifies(control, candidate, threshold) {
|
|
598
|
+
if (control.assessed !== control.planned ||
|
|
599
|
+
candidate.assessed !== candidate.planned) {
|
|
600
|
+
return {
|
|
601
|
+
qualifies: false,
|
|
602
|
+
reason: `incomplete assessment coverage (control ${control.assessed}/${control.planned}, candidate ${candidate.assessed}/${candidate.planned})`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (candidate.guardrailBreaches > 0) {
|
|
606
|
+
return {
|
|
607
|
+
qualifies: false,
|
|
608
|
+
reason: `candidate guardrail breach (${candidate.guardrailBreaches})`,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const lead = candidate.total - control.total;
|
|
612
|
+
return lead >= threshold
|
|
613
|
+
? { qualifies: true, reason: `lead ${lead} >= ${threshold}` }
|
|
614
|
+
: { qualifies: false, reason: `lead ${lead} < ${threshold}` };
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Rank the eligible candidates by the frozen order: default utility, then
|
|
618
|
+
* fewer primary-task failures, then fewer misleading outcomes, then fewer
|
|
619
|
+
* description characters, then arm id.
|
|
620
|
+
* @param candidates The candidate summaries with their description lengths.
|
|
621
|
+
* @returns The same list, best first.
|
|
622
|
+
*/
|
|
623
|
+
export function rankCandidates(candidates) {
|
|
624
|
+
return [...candidates].sort((left, right) => right.summary.total - left.summary.total ||
|
|
625
|
+
left.summary.primaryFail - right.summary.primaryFail ||
|
|
626
|
+
left.summary.outcomes.misleading -
|
|
627
|
+
right.summary.outcomes.misleading ||
|
|
628
|
+
left.characters - right.characters ||
|
|
629
|
+
left.arm.localeCompare(right.arm));
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Re-score every arm under the requested weight grid, holding the other
|
|
633
|
+
* penalties fixed at the campaign's declared weights. This explores
|
|
634
|
+
* preferences; it is not nine experiments.
|
|
635
|
+
* @param arms The planned trials, keyed by arm.
|
|
636
|
+
* @param usefulWeights The useful create/update rewards to try.
|
|
637
|
+
* @param transientPenalties The transient penalties to try, as positive
|
|
638
|
+
* magnitudes applied as negative scores.
|
|
639
|
+
* @param baseWeights The campaign's non-grid weights; defaults to the
|
|
640
|
+
* prospective defaults so legacy callers keep their frozen baseline.
|
|
641
|
+
* @returns One cell per combination, with totals and the resulting ranking.
|
|
642
|
+
*/
|
|
643
|
+
export function sensitivity(arms, usefulWeights, transientPenalties, baseWeights = DEFAULT_WEIGHTS) {
|
|
644
|
+
const cells = [];
|
|
645
|
+
for (const useful of usefulWeights) {
|
|
646
|
+
for (const transient of transientPenalties) {
|
|
647
|
+
const weights = {
|
|
648
|
+
...baseWeights,
|
|
649
|
+
useful,
|
|
650
|
+
transient: -Math.abs(transient),
|
|
651
|
+
};
|
|
652
|
+
const totals = {};
|
|
653
|
+
for (const [arm, trials] of Object.entries(arms)) {
|
|
654
|
+
totals[arm] = summariseArm(trials, weights).total;
|
|
655
|
+
}
|
|
656
|
+
const ranking = Object.keys(totals).sort((left, right) => (totals[right] ?? 0) - (totals[left] ?? 0) ||
|
|
657
|
+
left.localeCompare(right));
|
|
658
|
+
cells.push({ useful, transient, totals, ranking });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return cells;
|
|
662
|
+
}
|