@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,1069 @@
1
+ /** The measurement declaration schema version this module parses. */
2
+ export const MEASURES_SCHEMA_VERSION = 1;
3
+ /** Whether a value is a plain object. */
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ /** A nonblank string array, or undefined when the value is malformed. */
8
+ function stringArray(value) {
9
+ if (!Array.isArray(value))
10
+ return undefined;
11
+ const strings = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
12
+ return strings.length === value.length ? strings : undefined;
13
+ }
14
+ /** Parse one eligible-scenario list, collecting its problem. */
15
+ function idList(value, label, problems) {
16
+ const ids = stringArray(value);
17
+ if (ids === undefined) {
18
+ problems.push(`${label} must be a nonblank string array`);
19
+ return undefined;
20
+ }
21
+ if (new Set(ids).size !== ids.length) {
22
+ problems.push(`${label} repeats a scenario`);
23
+ }
24
+ return ids;
25
+ }
26
+ /** Parse one retrieval scenario's preservation fact ids. */
27
+ function factIds(entry, id, problems) {
28
+ const preservation = entry.preservation;
29
+ if (preservation === undefined)
30
+ return [];
31
+ if (!isRecord(preservation)) {
32
+ problems.push(`scenarios.${id}.preservation must be an object`);
33
+ return undefined;
34
+ }
35
+ const facts = preservation.facts;
36
+ if (facts === undefined)
37
+ return [];
38
+ if (!Array.isArray(facts)) {
39
+ problems.push(`scenarios.${id}.preservation.facts must be an array`);
40
+ return undefined;
41
+ }
42
+ const ids = facts.map((fact) => isRecord(fact) && typeof fact.id === "string" ? fact.id.trim() : "");
43
+ if (ids.some((value) => value === "")) {
44
+ problems.push(`scenarios.${id}.preservation.facts must carry nonblank ids`);
45
+ return undefined;
46
+ }
47
+ return ids;
48
+ }
49
+ /** Parse one retrieval scenario declaration. */
50
+ function retrievalDeclaration(entry, id, problems) {
51
+ if (!isRecord(entry)) {
52
+ problems.push(`scenarios.${id} is missing`);
53
+ return undefined;
54
+ }
55
+ const retrieval = entry.retrieval;
56
+ if (!isRecord(retrieval)) {
57
+ problems.push(`scenarios.${id}.retrieval is missing`);
58
+ return undefined;
59
+ }
60
+ if (typeof retrieval.note !== "string" || retrieval.note.trim() === "") {
61
+ problems.push(`scenarios.${id}.retrieval.note must be nonblank`);
62
+ return undefined;
63
+ }
64
+ const facts = factIds(entry, id, problems);
65
+ if (facts === undefined)
66
+ return undefined;
67
+ if (typeof retrieval.lake !== "string" || retrieval.lake.trim() === "") {
68
+ problems.push(`scenarios.${id}.retrieval.lake must be nonblank; a lake-less retrieval declaration cannot be attributed`);
69
+ return undefined;
70
+ }
71
+ return { note: retrieval.note, lake: retrieval.lake.trim(), facts };
72
+ }
73
+ /**
74
+ * Parse and validate the measurement declaration. The declaration is data:
75
+ * every problem is collected instead of throwing.
76
+ * @param value The parsed `measurement.json` value.
77
+ * @returns The declarations or the collected problems.
78
+ */
79
+ export function parseMeasureDeclarations(value) {
80
+ if (!isRecord(value)) {
81
+ return { problems: ["measurement declaration is not an object"] };
82
+ }
83
+ const problems = [];
84
+ const recording = idList(value.recordingOpportunities, "recordingOpportunities", problems);
85
+ const retrievalIds = idList(value.retrievalOpportunities, "retrievalOpportunities", problems);
86
+ const scenarios = isRecord(value.scenarios) ? value.scenarios : {};
87
+ const retrieval = {};
88
+ for (const id of retrievalIds ?? []) {
89
+ const declaration = retrievalDeclaration(scenarios[id], id, problems);
90
+ if (declaration !== undefined)
91
+ retrieval[id] = declaration;
92
+ }
93
+ if (problems.length > 0 || recording === undefined) {
94
+ return { problems };
95
+ }
96
+ return { declarations: { recording, retrieval }, problems };
97
+ }
98
+ /** Parse one journal line into an operation, or a problem. */
99
+ function operationOf(line, index) {
100
+ let value;
101
+ try {
102
+ value = JSON.parse(line);
103
+ }
104
+ catch {
105
+ return { problem: `journal line ${String(index + 1)} is not JSON` };
106
+ }
107
+ if (!isRecord(value) || !isRecord(value.input)) {
108
+ return {
109
+ problem: `journal line ${String(index + 1)} has no input`,
110
+ };
111
+ }
112
+ const input = value.input;
113
+ const add = isRecord(input.add) ? input.add : undefined;
114
+ const identity = reportedIdentityOf(value);
115
+ const verb = verbOf(input, add);
116
+ return {
117
+ operation: {
118
+ status: typeof value.status === "string" ? value.status : "unknown",
119
+ success: servedSuccessOf(value, verb),
120
+ surface: typeof value.surface === "string" ? value.surface : "unknown",
121
+ verb,
122
+ target: targetOf(input, add),
123
+ lake: identity.lake,
124
+ path: identity.path,
125
+ line,
126
+ },
127
+ };
128
+ }
129
+ /**
130
+ * The lake and path a completed operation reports in its result metadata.
131
+ * @param value The journal entry.
132
+ * @returns The reported identity, with nulls when it is absent.
133
+ */
134
+ function reportedIdentityOf(value) {
135
+ const result = isRecord(value.result) ? value.result : undefined;
136
+ const metadata = result !== undefined && isRecord(result.metadata)
137
+ ? result.metadata
138
+ : undefined;
139
+ return {
140
+ lake: typeof metadata?.lake === "string" ? metadata.lake : null,
141
+ path: typeof metadata?.path === "string" ? metadata.path : null,
142
+ };
143
+ }
144
+ /**
145
+ * Whether the served result says the operation succeeded.
146
+ *
147
+ * The journal's top-level `status` is only "completed" or "error": the plugin
148
+ * returns refusals as completed results. Success is the result envelope's own
149
+ * fact — `written` for an add, a non-null `read` for a read — and null where
150
+ * the verb carries no success fact.
151
+ * @param value The journal entry.
152
+ * @param verb The parsed verb.
153
+ * @returns True, false, or null when the verb has no success fact.
154
+ */
155
+ function servedSuccessOf(value, verb) {
156
+ if (value.status !== "completed")
157
+ return false;
158
+ const result = isRecord(value.result) ? value.result : undefined;
159
+ const metadata = result !== undefined && isRecord(result.metadata)
160
+ ? result.metadata
161
+ : undefined;
162
+ if (metadata === undefined)
163
+ return null;
164
+ if (verb === "add")
165
+ return metadata.written === true;
166
+ if (verb === "read")
167
+ return typeof metadata.read === "string";
168
+ return null;
169
+ }
170
+ /** The verb one journal input expresses. */
171
+ function verbOf(input, add) {
172
+ if (add !== undefined)
173
+ return "add";
174
+ if (typeof input.read === "string")
175
+ return "read";
176
+ if (typeof input.search === "string")
177
+ return "search";
178
+ if (input.check !== undefined)
179
+ return "check";
180
+ return "help";
181
+ }
182
+ /** The typed target one journal input names, when any. */
183
+ function targetOf(input, add) {
184
+ if (typeof input.read === "string")
185
+ return input.read;
186
+ if (add !== undefined && typeof add.path === "string")
187
+ return add.path;
188
+ return null;
189
+ }
190
+ /**
191
+ * Parse the plugin journal lines into operations, keeping unreadable lines
192
+ * explicit.
193
+ * @param lines The raw journal lines.
194
+ * @returns The operations and any parse problems.
195
+ */
196
+ export function parseOperations(lines) {
197
+ const operations = [];
198
+ const problems = [];
199
+ for (const [index, line] of lines.entries()) {
200
+ const parsed = operationOf(line, index);
201
+ if (parsed.operation !== undefined)
202
+ operations.push(parsed.operation);
203
+ if (parsed.problem !== undefined)
204
+ problems.push(parsed.problem);
205
+ }
206
+ return { operations, problems };
207
+ }
208
+ /** Strip an optional `lake:` prefix from a typed target. */
209
+ function barePath(path) {
210
+ return path.replace(/^[a-z0-9_-]+:/i, "");
211
+ }
212
+ /**
213
+ * Whether an operation names the declared note path exactly. A suffix match is
214
+ * deliberately not accepted: `shared/db/pool.md` is a different file from
215
+ * `db/pool.md`.
216
+ * @param operation The journal operation.
217
+ * @param declaration The declared note.
218
+ * @returns True when the operation's reported path or target is the note.
219
+ */
220
+ export function operationMatchesNotePath(operation, declaration) {
221
+ const expected = declaration.note;
222
+ const qualified = `${declaration.lake}/${expected}`;
223
+ const reported = operation.path === null
224
+ ? null
225
+ : lakeRelative(operation.path, declaration.lake);
226
+ const target = operation.target === null ? null : barePath(operation.target);
227
+ // The returned identity is authoritative. A request target that contradicts
228
+ // it is contradictory evidence and cannot earn credit.
229
+ if (reported !== null && target !== null && reported !== target) {
230
+ return false;
231
+ }
232
+ const identity = reported ?? target;
233
+ if (identity === null)
234
+ return false;
235
+ return identity === expected || identity === qualified;
236
+ }
237
+ /**
238
+ * Whether an operation's reported lake matches the declaration. A declaration
239
+ * without a lake accepts only an exact path; a declared lake must match.
240
+ * @param operation The journal operation.
241
+ * @param declaration The declared note.
242
+ * @returns True when the lake identity is satisfied.
243
+ */
244
+ export function operationMatchesNoteLake(operation, declaration) {
245
+ return operation.lake === declaration.lake;
246
+ }
247
+ /** Build the context once so each measure stays small. */
248
+ function contextOf(record, declarations) {
249
+ const journal = journalContextOf(record);
250
+ const parsed = journal.verified
251
+ ? parseOperations(journal.lines)
252
+ : undefined;
253
+ const journalProblem = journal.problem ?? parseProblemOf(parsed);
254
+ return {
255
+ record,
256
+ operations: parsed?.operations ?? [],
257
+ parsed: parsed !== undefined,
258
+ journalProblem,
259
+ gate: record.gate.state === "available" ? record.gate.value.status : null,
260
+ outcome: record.assessment.outcome ?? null,
261
+ primary: record.primary.status,
262
+ notRun: record.attempt.state === "absent" ||
263
+ record.assessment.status === "not-run",
264
+ recordingEligible: declarations.recording.includes(record.identity.scenario),
265
+ retrievalDeclaration: declarations.retrieval[record.identity.scenario],
266
+ writes: writesOf(record),
267
+ };
268
+ }
269
+ /**
270
+ * The operation journal's parse problems, or null when every line parsed.
271
+ * @param parsed The parsed journal, when it was verified.
272
+ * @returns One problem line, or null.
273
+ */
274
+ function parseProblemOf(parsed) {
275
+ return parsed !== undefined && parsed.problems.length > 0
276
+ ? `docs-operation journal has unreadable lines: ${parsed.problems.join("; ")}`
277
+ : null;
278
+ }
279
+ /**
280
+ * The trial's writes report as the measure reads it.
281
+ * @param record The trial record.
282
+ * @returns The report state and text.
283
+ */
284
+ function writesOf(record) {
285
+ const writes = record.artefacts.writesText;
286
+ return {
287
+ state: writes.state,
288
+ text: writes.state === "available" ? writes.value : "",
289
+ };
290
+ }
291
+ /**
292
+ * Read the changed lake documents from a trial's writes report.
293
+ *
294
+ * Only `lakes/<lake>/<path>` entries are documents; a work-file change never
295
+ * makes a documentation write verified. An unreadable report is a problem, not
296
+ * an empty change set.
297
+ * @param writes The recorded writes report.
298
+ * @param writes.state The report's explicit load state.
299
+ * @param writes.text The report's text.
300
+ * @returns The changed documents and any parse problem.
301
+ */
302
+ export function changedLakeDocuments(writes) {
303
+ if (writes.state !== "available") {
304
+ return {
305
+ documents: [],
306
+ problem: `the writes report is ${writes.state}; the changed document cannot be identified`,
307
+ };
308
+ }
309
+ const documents = writes.text
310
+ .split("\n")
311
+ .map(changedDocument)
312
+ .filter((document) => document !== undefined);
313
+ return { documents, problem: null };
314
+ }
315
+ /**
316
+ * Read one writes-report line as a changed lake document.
317
+ * @param raw The raw line.
318
+ * @returns The document, or undefined for a blank line or a non-lake path.
319
+ */
320
+ function changedDocument(raw) {
321
+ const line = raw.trim();
322
+ if (line === "" || line === "none")
323
+ return undefined;
324
+ // A directory is not a document and cannot compete for the credit.
325
+ if (entryKindOf(line) === "dir")
326
+ return undefined;
327
+ const match = /^lakes\/([^/]+)\/(.+)$/.exec(documentPathOf(line));
328
+ if (match === null)
329
+ return undefined;
330
+ return {
331
+ lake: match[1],
332
+ path: match[2],
333
+ kind: documentKindOf(line),
334
+ };
335
+ }
336
+ /** The producer's parenthesised entry kind, when one is present. */
337
+ function entryKindOf(line) {
338
+ return / \(([a-z]+)\)$/.exec(line)?.[1];
339
+ }
340
+ /** The path one wins-report line names, without its change or entry labels. */
341
+ function documentPathOf(line) {
342
+ return line.replace(/^[a-z]+:?\s*/, "").replace(/ \([a-z]+\)$/, "");
343
+ }
344
+ /** The change kind one report line declares. */
345
+ function documentKindOf(line) {
346
+ const word = /^([a-z]+):?\s/.exec(line)?.[1];
347
+ if (word === "added" || word === "modified" || word === "removed") {
348
+ return word;
349
+ }
350
+ return "unknown";
351
+ }
352
+ /**
353
+ * Whether a completed plugin write targets exactly one changed document.
354
+ * @param operation The completed add operation.
355
+ * @param document The evidenced changed document.
356
+ * @returns True when lake and relative path agree.
357
+ */
358
+ export function operationMatchesChangedDocument(operation, document) {
359
+ const identity = operation.path ?? operation.target;
360
+ if (identity === null)
361
+ return false;
362
+ // A completed write reports its resolved lake. Without one the document
363
+ // cannot be tied to the write.
364
+ if (operation.lake === null || operation.lake !== document.lake) {
365
+ return false;
366
+ }
367
+ return lakeRelative(identity, document.lake) === document.path;
368
+ }
369
+ /** Strip an optional root-relative `lakes/<lake>/` prefix. */
370
+ function lakeRelative(identity, lake) {
371
+ const bare = barePath(identity);
372
+ const prefix = `lakes/${lake}/`;
373
+ return bare.startsWith(prefix) ? bare.slice(prefix.length) : bare;
374
+ }
375
+ /** The verified journal lines, or why they are unusable. */
376
+ function journalContextOf(record) {
377
+ const verification = record.journal;
378
+ if (verification === undefined) {
379
+ return {
380
+ verified: false,
381
+ lines: [],
382
+ problem: "the operation journal was not verified; no journal line can be credited",
383
+ };
384
+ }
385
+ if (verification.state === "absent") {
386
+ return {
387
+ verified: false,
388
+ lines: [],
389
+ problem: "the docs-operation journal is absent (no plugin journal captured)",
390
+ };
391
+ }
392
+ if (verification.state === "unverifiable-key") {
393
+ return {
394
+ verified: false,
395
+ lines: [],
396
+ problem: verification.problems[0] ??
397
+ "the docs-operation journal has no usable signing key",
398
+ };
399
+ }
400
+ const rejected = verification.rejected;
401
+ const problem = rejected.length === 0
402
+ ? null
403
+ : `the docs-operation journal rejected ${String(rejected.length)} line(s): ${[
404
+ ...new Set(rejected.map((entry) => entry.reason)),
405
+ ].join(", ")}`;
406
+ return {
407
+ verified: true,
408
+ lines: verification.credited.map((entry) => JSON.stringify(entry.record)),
409
+ problem,
410
+ };
411
+ }
412
+ /** The dispositions decided before any write is inspected. */
413
+ function earlyDisposition(context) {
414
+ if (!context.recordingEligible) {
415
+ return { disposition: "not-eligible", evidence: [] };
416
+ }
417
+ if (context.notRun) {
418
+ return { disposition: "not-run", evidence: [] };
419
+ }
420
+ if (context.gate === "blocked") {
421
+ const reason = context.record.gate.state === "available"
422
+ ? String(context.record.gate.value.reason)
423
+ : "unknown";
424
+ return {
425
+ disposition: "gate-blocked",
426
+ evidence: [`gate blocked: ${reason}`],
427
+ };
428
+ }
429
+ return null;
430
+ }
431
+ /** The deterministic disposition of a trial's documentation write. */
432
+ function dispositionOf(context, completedAdds, failedAdds) {
433
+ const early = earlyDisposition(context);
434
+ if (early !== null)
435
+ return early;
436
+ if (completedAdds.length > 0) {
437
+ return completedWrite(context, completedAdds);
438
+ }
439
+ if (failedAdds.length > 0) {
440
+ return context.journalProblem === null
441
+ ? {
442
+ disposition: "tool-error",
443
+ evidence: ["a docs add operation failed"],
444
+ }
445
+ : {
446
+ disposition: "unknown",
447
+ evidence: [
448
+ `${context.journalProblem}; the unreadable line could hide a successful write`,
449
+ ],
450
+ };
451
+ }
452
+ if (context.journalProblem !== null) {
453
+ return {
454
+ disposition: "unknown",
455
+ evidence: [
456
+ `${context.journalProblem}; a write cannot be attributed`,
457
+ ],
458
+ };
459
+ }
460
+ if (context.outcome === "useful-create" ||
461
+ context.outcome === "useful-update") {
462
+ if (context.gate === "pass") {
463
+ return {
464
+ disposition: "filesystem-only",
465
+ evidence: [
466
+ "the documentation surface changed and passed the gate, but no completed docs add operation was journaled",
467
+ ],
468
+ };
469
+ }
470
+ if (context.gate === null) {
471
+ return {
472
+ disposition: "unknown",
473
+ evidence: [
474
+ "a useful judgement exists but the gate result is missing",
475
+ ],
476
+ };
477
+ }
478
+ }
479
+ return { disposition: "no-write", evidence: [] };
480
+ }
481
+ /** The disposition when at least one completed plugin write exists. */
482
+ function completedWrite(context, completedAdds) {
483
+ const blocker = writeBlocker(context);
484
+ if (blocker !== null)
485
+ return blocker;
486
+ return boundWrite(context, completedAdds);
487
+ }
488
+ /**
489
+ * The outcome and gate checks that precede document binding.
490
+ * @param context The trial context.
491
+ * @returns A terminal disposition, or null when binding is still needed.
492
+ */
493
+ function writeBlocker(context) {
494
+ if (context.outcome === null) {
495
+ return {
496
+ disposition: "unknown",
497
+ evidence: [
498
+ "a completed plugin write exists but no judgement was recorded",
499
+ ],
500
+ };
501
+ }
502
+ if (context.outcome !== "useful-create" &&
503
+ context.outcome !== "useful-update") {
504
+ return {
505
+ disposition: "no-write",
506
+ evidence: [
507
+ `judgement recorded ${context.outcome} for a completed plugin write`,
508
+ ],
509
+ };
510
+ }
511
+ if (context.gate === "blocked") {
512
+ return { disposition: "gate-blocked", evidence: [] };
513
+ }
514
+ if (context.gate !== "pass") {
515
+ return {
516
+ disposition: "unknown",
517
+ evidence: [
518
+ "a completed plugin write and a useful judgement exist but the gate result is missing",
519
+ ],
520
+ };
521
+ }
522
+ return null;
523
+ }
524
+ /**
525
+ * Bind a useful plugin write to the one useful changed lake document.
526
+ * @param context The trial context.
527
+ * @param completedAdds The completed add operations.
528
+ * @returns The disposition and its evidence.
529
+ */
530
+ function boundWrite(context, completedAdds) {
531
+ const changed = changedLakeDocuments(context.writes);
532
+ if (changed.problem !== null) {
533
+ return { disposition: "unknown", evidence: [changed.problem] };
534
+ }
535
+ const expectedKind = context.outcome === "useful-create" ? "added" : "modified";
536
+ const candidates = changed.documents.filter((document) => document.kind === expectedKind || document.kind === "unknown");
537
+ if (candidates.length === 0) {
538
+ return {
539
+ disposition: "filesystem-only",
540
+ evidence: [
541
+ `the judgement recorded ${String(context.outcome)} but no lake document change of that kind was reported`,
542
+ ],
543
+ };
544
+ }
545
+ if (candidates.length > 1) {
546
+ return {
547
+ disposition: "unknown",
548
+ evidence: [
549
+ `the trial changed ${String(candidates.length)} lake documents (${candidates
550
+ .map((document) => `${document.lake}/${document.path}`)
551
+ .join(", ")}); the plugin write cannot be tied to the useful one`,
552
+ ],
553
+ };
554
+ }
555
+ const document = candidates[0];
556
+ const matching = completedAdds.filter((operation) => operationMatchesChangedDocument(operation, document));
557
+ if (matching.length === 0) {
558
+ return {
559
+ disposition: "filesystem-only",
560
+ evidence: [
561
+ `the useful document ${document.lake}/${document.path} changed, but no completed plugin add targeted it`,
562
+ ],
563
+ };
564
+ }
565
+ return {
566
+ disposition: "verified-write",
567
+ evidence: [
568
+ `plugin add of ${document.lake}/${document.path} matched the changed document`,
569
+ ],
570
+ };
571
+ }
572
+ /** The judge's retrieval state, from the loaded assessment. */
573
+ function judgeRetrieval(record) {
574
+ const retrieval = record.assessment.retrieval;
575
+ return retrieval.state === "available"
576
+ ? { state: retrieval.value.state, quote: retrieval.value.quote }
577
+ : { state: null, quote: null };
578
+ }
579
+ /** The retrieval state and evidence for one trial. */
580
+ function retrievalOf(context) {
581
+ const declaration = context.retrievalDeclaration;
582
+ if (declaration === undefined) {
583
+ return {
584
+ state: "not-eligible",
585
+ read: false,
586
+ judgeState: null,
587
+ note: null,
588
+ trace: [],
589
+ evidence: [],
590
+ };
591
+ }
592
+ if (context.notRun) {
593
+ return {
594
+ state: "not-run",
595
+ read: false,
596
+ judgeState: null,
597
+ note: declaration.note,
598
+ trace: [],
599
+ evidence: [],
600
+ };
601
+ }
602
+ const matching = context.operations.filter((operation) => operation.verb === "read" &&
603
+ operation.status === "completed" &&
604
+ operation.success !== false &&
605
+ operationMatchesNotePath(operation, declaration));
606
+ const reads = matching.filter((operation) => operationMatchesNoteLake(operation, declaration));
607
+ const lakeUnknown = matching.some((operation) => operation.lake === null);
608
+ const judge = judgeRetrieval(context.record);
609
+ const base = {
610
+ read: reads.length > 0,
611
+ judgeState: judge.state,
612
+ note: declaration.note,
613
+ trace: reads.map((operation) => operation.line),
614
+ };
615
+ if (!base.read && context.journalProblem !== null) {
616
+ return {
617
+ ...base,
618
+ state: "unknown",
619
+ evidence: [`${context.journalProblem}; attribution is unknown`],
620
+ };
621
+ }
622
+ if (!base.read && lakeUnknown) {
623
+ return {
624
+ ...base,
625
+ state: "unknown",
626
+ evidence: [
627
+ `a read of ${declaration.note} was found but it reports no lake; attribution is unknown`,
628
+ ],
629
+ };
630
+ }
631
+ if (!base.read) {
632
+ return {
633
+ ...base,
634
+ state: "miss",
635
+ evidence: [
636
+ `no completed read of ${declaration.lake}/${declaration.note} was attributed to the session`,
637
+ ],
638
+ };
639
+ }
640
+ return { ...base, ...appliedState(context, judge) };
641
+ }
642
+ /** The applied/miss/unknown decision once an attributable read exists. */
643
+ function appliedState(context, judge) {
644
+ if (judge.state === null) {
645
+ return {
646
+ state: "unknown",
647
+ evidence: [
648
+ "an attributable read exists but no retrieval judgement was recorded",
649
+ ],
650
+ };
651
+ }
652
+ if (judge.state === "applied" && context.primary === "pass") {
653
+ return {
654
+ state: "applied",
655
+ evidence: [
656
+ `judge applied the note: ${judge.quote ?? "(no quote)"}`,
657
+ ],
658
+ };
659
+ }
660
+ if (judge.state === "applied") {
661
+ return {
662
+ state: "unknown",
663
+ evidence: [
664
+ `judge reports applied but the independent primary check is ${context.primary}`,
665
+ ],
666
+ };
667
+ }
668
+ if (judge.state === "unknown") {
669
+ return { state: "unknown", evidence: [] };
670
+ }
671
+ return {
672
+ state: "miss",
673
+ evidence: [`judge recorded ${judge.state}`],
674
+ };
675
+ }
676
+ /**
677
+ * Measure one trial against the declared opportunities.
678
+ * @param record The loaded trial record.
679
+ * @param declarations The validated declarations.
680
+ * @param arm The declared arm id.
681
+ * @returns The trial measures.
682
+ */
683
+ export function measureTrial(record, declarations, arm) {
684
+ const context = contextOf(record, declarations);
685
+ const completedAdds = context.operations.filter((operation) => operation.verb === "add" &&
686
+ operation.status === "completed" &&
687
+ operation.success === true);
688
+ const failedAdds = context.operations.filter((operation) => operation.verb === "add" &&
689
+ (operation.status !== "completed" || operation.success === false));
690
+ const write = dispositionOf(context, completedAdds, failedAdds);
691
+ const retrieval = retrievalOf(context);
692
+ return {
693
+ stamp: record.identity.stamp,
694
+ scenario: record.identity.scenario,
695
+ arm,
696
+ recording: {
697
+ eligible: context.recordingEligible,
698
+ disposition: write.disposition,
699
+ success: write.disposition === "verified-write",
700
+ outcome: context.outcome,
701
+ gate: context.gate,
702
+ writes: completedAdds.map((operation) => operation.line),
703
+ evidence: write.evidence,
704
+ },
705
+ retrieval: {
706
+ eligible: context.retrievalDeclaration !== undefined,
707
+ state: retrieval.state,
708
+ read: retrieval.read,
709
+ judgeState: retrieval.judgeState,
710
+ note: retrieval.note,
711
+ trace: retrieval.trace,
712
+ evidence: retrieval.evidence,
713
+ },
714
+ };
715
+ }
716
+ /** Empty counts for one opportunity type. */
717
+ function emptyCounts() {
718
+ return {
719
+ success: 0,
720
+ miss: 0,
721
+ unknown: 0,
722
+ notRun: 0,
723
+ dispositions: {},
724
+ };
725
+ }
726
+ /** Add one trial to an arm's opportunity counts. */
727
+ function countTrial(trial, recording, retrieval) {
728
+ if (trial.recording.eligible) {
729
+ const disposition = trial.recording.disposition;
730
+ recording.dispositions[disposition] =
731
+ (recording.dispositions[disposition] ?? 0) + 1;
732
+ if (trial.recording.success)
733
+ recording.success += 1;
734
+ else if (disposition === "unknown")
735
+ recording.unknown += 1;
736
+ else if (disposition === "not-run")
737
+ recording.notRun += 1;
738
+ else
739
+ recording.miss += 1;
740
+ }
741
+ if (!trial.retrieval.eligible)
742
+ return;
743
+ const state = trial.retrieval.state;
744
+ if (state === "applied")
745
+ retrieval.success += 1;
746
+ else if (state === "unknown")
747
+ retrieval.unknown += 1;
748
+ else if (state === "not-run")
749
+ retrieval.notRun += 1;
750
+ else
751
+ retrieval.miss += 1;
752
+ }
753
+ /**
754
+ * Summarise the measures per arm.
755
+ * @param trials The trial measures.
756
+ * @param planned The frozen slots.
757
+ * @param declarations The validated declarations.
758
+ * @returns The per-arm counts.
759
+ */
760
+ export function summariseMeasures(trials, planned, declarations) {
761
+ const arms = [...new Set(planned.map((slot) => slot.arm))].sort();
762
+ const result = {};
763
+ for (const arm of arms) {
764
+ const recording = emptyCounts();
765
+ const retrieval = emptyCounts();
766
+ for (const trial of trials.filter((one) => one.arm === arm)) {
767
+ countTrial(trial, recording, retrieval);
768
+ }
769
+ result[arm] = {
770
+ recording: {
771
+ planned: planned.filter((slot) => slot.arm === arm &&
772
+ declarations.recording.includes(slot.scenario)).length,
773
+ ...recording,
774
+ },
775
+ retrieval: {
776
+ planned: planned.filter((slot) => slot.arm === arm &&
777
+ declarations.retrieval[slot.scenario] !== undefined).length,
778
+ applied: retrieval.success,
779
+ miss: retrieval.miss,
780
+ unknown: retrieval.unknown,
781
+ notRun: retrieval.notRun,
782
+ states: retrieval.dispositions,
783
+ },
784
+ };
785
+ }
786
+ return result;
787
+ }
788
+ /** The labelled utility-threshold component. */
789
+ function thresholdComponent(threshold) {
790
+ return {
791
+ component: "utility-threshold",
792
+ status: threshold.status,
793
+ lead: threshold.lead,
794
+ interval: threshold.interval,
795
+ note: "One component only: a zero-delta 'qualified' at threshold 0 is not the personal-trial recommendation.",
796
+ };
797
+ }
798
+ /** The known and possible bounds of the two opportunity counts. */
799
+ function countBounds(control, candidate) {
800
+ return {
801
+ extraKnown: candidate.recording.success >= control.recording.success + 1 ||
802
+ candidate.retrieval.applied >= control.retrieval.applied + 1,
803
+ extraRobust: candidate.recording.success >=
804
+ control.recording.success + control.recording.unknown + 1 ||
805
+ candidate.retrieval.applied >=
806
+ control.retrieval.applied + control.retrieval.unknown + 1,
807
+ neitherLowerKnown: candidate.recording.success >= control.recording.success &&
808
+ candidate.retrieval.applied >= control.retrieval.applied,
809
+ neitherLowerRobust: candidate.recording.success >=
810
+ control.recording.success + control.recording.unknown &&
811
+ candidate.retrieval.applied >=
812
+ control.retrieval.applied + control.retrieval.unknown,
813
+ certainLoss: candidate.recording.success + candidate.recording.unknown <
814
+ control.recording.success ||
815
+ candidate.retrieval.applied + candidate.retrieval.unknown <
816
+ control.retrieval.applied ||
817
+ (candidate.recording.success + candidate.recording.unknown <=
818
+ control.recording.success &&
819
+ candidate.retrieval.applied + candidate.retrieval.unknown <=
820
+ control.retrieval.applied),
821
+ unknowns: control.recording.unknown +
822
+ candidate.recording.unknown +
823
+ control.retrieval.unknown +
824
+ candidate.retrieval.unknown,
825
+ };
826
+ }
827
+ /** The opportunity-count criterion, with unknowns kept explicit. */
828
+ function opportunityCriterion(control, candidate) {
829
+ const bounds = countBounds(control, candidate);
830
+ const reasons = [
831
+ `recording: control ${String(control.recording.success)}/${String(control.recording.planned)}, candidate ${String(candidate.recording.success)}/${String(candidate.recording.planned)} (unknown control ${String(control.recording.unknown)}, candidate ${String(candidate.recording.unknown)})`,
832
+ `retrieval: control ${String(control.retrieval.applied)}/${String(control.retrieval.planned)}, candidate ${String(candidate.retrieval.applied)}/${String(candidate.retrieval.planned)} (unknown control ${String(control.retrieval.unknown)}, candidate ${String(candidate.retrieval.unknown)})`,
833
+ ];
834
+ const positive = bounds.extraRobust && bounds.neitherLowerRobust;
835
+ if (bounds.certainLoss) {
836
+ return {
837
+ status: "fail",
838
+ reasons: [
839
+ ...reasons,
840
+ "the candidate cannot be at least equal on both counts and strictly ahead on one under any completion (a tie gives no promotion)",
841
+ ],
842
+ };
843
+ }
844
+ if (positive) {
845
+ return {
846
+ status: "pass",
847
+ reasons: [
848
+ ...reasons,
849
+ "the candidate has at least one additional opportunity with neither count lower, even if every control unknown is a success",
850
+ ],
851
+ };
852
+ }
853
+ if (bounds.unknowns > 0) {
854
+ return {
855
+ status: "inconclusive",
856
+ reasons: [
857
+ ...reasons,
858
+ "opportunity outcomes or attribution are unknown and could change the count criterion",
859
+ ],
860
+ };
861
+ }
862
+ return {
863
+ status: "fail",
864
+ reasons: [
865
+ ...reasons,
866
+ "the candidate has no additional successful recording or applied retrieval with neither count lower (a tie gives no promotion)",
867
+ ],
868
+ };
869
+ }
870
+ /** The utility, primary and guardrail quality criterion. */
871
+ function qualityCriterion(control, candidate, guardrails) {
872
+ if (candidate.utilityTotal < control.utilityTotal) {
873
+ return {
874
+ status: "fail",
875
+ reasons: [
876
+ `weighted utility is lower (control ${String(control.utilityTotal)}, candidate ${String(candidate.utilityTotal)})`,
877
+ ],
878
+ };
879
+ }
880
+ if (candidate.primary.pass < control.primary.pass) {
881
+ return {
882
+ status: "fail",
883
+ reasons: [
884
+ `primary passes are lower (control ${String(control.primary.pass)}, candidate ${String(candidate.primary.pass)})`,
885
+ ],
886
+ };
887
+ }
888
+ if (candidate.outcomes.misleading > control.outcomes.misleading) {
889
+ return {
890
+ status: "fail",
891
+ reasons: [
892
+ `misleading outcomes increased (control ${String(control.outcomes.misleading)}, candidate ${String(candidate.outcomes.misleading)})`,
893
+ ],
894
+ };
895
+ }
896
+ if (guardrails.status === "breached") {
897
+ return {
898
+ status: "fail",
899
+ reasons: [
900
+ `a candidate guardrail is breached: ${guardrails.detail}`,
901
+ ],
902
+ };
903
+ }
904
+ if (guardrails.status === "unresolved") {
905
+ return {
906
+ status: "inconclusive",
907
+ reasons: [
908
+ `a candidate guardrail is unresolved: ${guardrails.detail}`,
909
+ ],
910
+ };
911
+ }
912
+ return {
913
+ status: "pass",
914
+ reasons: [
915
+ "weighted utility and primary passes are not lower, no new misleading outcome, and the candidate guardrails are clean",
916
+ ],
917
+ };
918
+ }
919
+ /**
920
+ * The paired scenario criterion: a misleading outcome that appears only on the
921
+ * candidate side of a pair defeats promotion, even when the aggregate counts
922
+ * are equal. A pair where the candidate is not misleading, or both sides are,
923
+ * is not candidate-specific.
924
+ * @param pairs The complete paired outcomes.
925
+ * @returns The criterion.
926
+ */
927
+ export function pairedMisleadingCriterion(pairs) {
928
+ if (pairs.length === 0) {
929
+ return {
930
+ status: "pass",
931
+ reasons: ["no paired scenario comparison was supplied"],
932
+ };
933
+ }
934
+ const candidateOnly = pairs.filter((pair) => pair.candidate.includes("misleading") &&
935
+ !pair.control.includes("misleading"));
936
+ if (candidateOnly.length > 0) {
937
+ return {
938
+ status: "fail",
939
+ reasons: [
940
+ `candidate-only misleading outcome in ${candidateOnly
941
+ .map((pair) => pair.pairId)
942
+ .join(", ")}: a paired loss defeats promotion even at equal aggregate counts`,
943
+ ],
944
+ };
945
+ }
946
+ return {
947
+ status: "pass",
948
+ reasons: [
949
+ "no paired scenario records a misleading outcome only for the candidate",
950
+ ],
951
+ };
952
+ }
953
+ /**
954
+ * The incomplete-input checks that make the screen inconclusive before any
955
+ * opportunity or quality comparison.
956
+ * @param control The control arm summary, when present.
957
+ * @param candidate The candidate arm summary, when present.
958
+ * @param controlCounts The control accounting counts, when present.
959
+ * @param candidateCounts The candidate accounting counts, when present.
960
+ * @param coverage The assessment-coverage limb.
961
+ * @param coverage.status The limb's status.
962
+ * @param coverage.detail The limb's reason.
963
+ * @param threshold The labelled utility-threshold component.
964
+ * @returns An inconclusive decision, or null when the inputs are complete.
965
+ */
966
+ function earlyInconclusive(control, candidate, controlCounts, candidateCounts, coverage, threshold) {
967
+ if (control === undefined || candidate === undefined) {
968
+ return {
969
+ recommendation: "inconclusive",
970
+ reasons: ["an arm has no opportunity summary"],
971
+ utilityThreshold: threshold,
972
+ };
973
+ }
974
+ if (coverage.status !== "pass") {
975
+ return {
976
+ recommendation: "inconclusive",
977
+ reasons: [`coverage: ${coverage.detail}`],
978
+ utilityThreshold: threshold,
979
+ };
980
+ }
981
+ if (controlCounts === undefined || candidateCounts === undefined) {
982
+ return {
983
+ recommendation: "inconclusive",
984
+ reasons: ["accounting counts are missing for an arm"],
985
+ utilityThreshold: threshold,
986
+ };
987
+ }
988
+ if (controlCounts.primary.unknown > 0 ||
989
+ candidateCounts.primary.unknown > 0) {
990
+ return {
991
+ recommendation: "inconclusive",
992
+ reasons: [
993
+ "a primary-task result is unknown and could change the pass-count criterion",
994
+ ],
995
+ utilityThreshold: threshold,
996
+ };
997
+ }
998
+ return null;
999
+ }
1000
+ /**
1001
+ * The personal-trial screen. The utility threshold is one component; the
1002
+ * opportunity counts and quality checks decide the recommendation alongside
1003
+ * it.
1004
+ * @param controlArm The control arm id.
1005
+ * @param candidateArm The candidate arm id.
1006
+ * @param measures The per-arm opportunity summaries.
1007
+ * @param counts The per-arm accounting counts.
1008
+ * @param guardrails The candidate arm's aggregate guardrail status and detail.
1009
+ * @param guardrails.status The aggregate status (clean, breached, unresolved).
1010
+ * @param guardrails.detail The guardrail evidence.
1011
+ * @param coverage The assessment-coverage limb.
1012
+ * @param coverage.status The limb's status.
1013
+ * @param coverage.detail The limb's reason.
1014
+ * @param utilityThreshold The analyser's utility-threshold limb.
1015
+ * @param utilityThreshold.status The limb's status.
1016
+ * @param utilityThreshold.lead The assessed lead.
1017
+ * @param utilityThreshold.interval The full-population interval.
1018
+ * @param pairs The complete paired scenario outcomes, when available.
1019
+ * @returns The recommendation and every criterion's reason.
1020
+ */
1021
+ export function personalTrialDecision(controlArm, candidateArm, measures, counts, guardrails, coverage, utilityThreshold, pairs = []) {
1022
+ const threshold = thresholdComponent(utilityThreshold);
1023
+ const control = measures[controlArm];
1024
+ const candidate = measures[candidateArm];
1025
+ const controlCounts = counts[controlArm];
1026
+ const candidateCounts = counts[candidateArm];
1027
+ const early = earlyInconclusive(control, candidate, controlCounts, candidateCounts, coverage, threshold);
1028
+ if (early !== null)
1029
+ return early;
1030
+ const opportunities = opportunityCriterion(control, candidate);
1031
+ if (opportunities.status !== "pass") {
1032
+ return {
1033
+ recommendation: opportunities.status === "fail"
1034
+ ? "no-promotion"
1035
+ : "inconclusive",
1036
+ reasons: opportunities.reasons,
1037
+ utilityThreshold: threshold,
1038
+ };
1039
+ }
1040
+ const quality = qualityCriterion(controlCounts, candidateCounts, guardrails);
1041
+ if (quality.status !== "pass") {
1042
+ return {
1043
+ recommendation: quality.status === "fail" ? "no-promotion" : "inconclusive",
1044
+ reasons: [...opportunities.reasons, ...quality.reasons],
1045
+ utilityThreshold: threshold,
1046
+ };
1047
+ }
1048
+ const paired = pairedMisleadingCriterion(pairs);
1049
+ if (paired.status !== "pass") {
1050
+ return {
1051
+ recommendation: paired.status === "fail" ? "no-promotion" : "inconclusive",
1052
+ reasons: [
1053
+ ...opportunities.reasons,
1054
+ ...quality.reasons,
1055
+ ...paired.reasons,
1056
+ ],
1057
+ utilityThreshold: threshold,
1058
+ };
1059
+ }
1060
+ return {
1061
+ recommendation: "personal-trial",
1062
+ reasons: [
1063
+ ...opportunities.reasons,
1064
+ ...quality.reasons,
1065
+ ...paired.reasons,
1066
+ ],
1067
+ utilityThreshold: threshold,
1068
+ };
1069
+ }