eval-quality 0.1.0 → 0.3.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 (179) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +20 -13
  3. package/corpus/dev/compile-seal-example/brief.json +1 -1
  4. package/corpus/dev/compile-seal-example/contract.json +1 -1
  5. package/corpus/dev/contracts/absent-collection-locations.json +1 -1
  6. package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
  7. package/corpus/dev/contracts/absent-success-indicator.json +1 -1
  8. package/corpus/dev/contracts/empty-channel-roles.json +1 -1
  9. package/corpus/dev/contracts/empty-collection-locations.json +1 -1
  10. package/corpus/dev/contracts/empty-request-shapes.json +1 -1
  11. package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
  12. package/corpus/dev/contracts/fragment-selection.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  19. package/corpus/dev/contracts/review-corpus.json +1 -0
  20. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  21. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  22. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  23. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  24. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  25. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  26. package/corpus/dev/index.json +1 -1
  27. package/dist/application/index.d.ts +2 -0
  28. package/dist/application/index.js +1 -0
  29. package/dist/application/score.d.ts +48 -0
  30. package/dist/application/score.js +186 -0
  31. package/dist/cli/arguments.d.ts +7 -3
  32. package/dist/cli/arguments.js +73 -5
  33. package/dist/cli/exit-codes.d.ts +6 -2
  34. package/dist/cli/exit-codes.js +8 -9
  35. package/dist/cli/main.js +9 -0
  36. package/dist/cli/render.d.ts +3 -6
  37. package/dist/cli/render.js +57 -5
  38. package/dist/cli/run.d.ts +13 -3
  39. package/dist/cli/run.js +136 -16
  40. package/dist/core/canonical/canonicalize.d.ts +12 -0
  41. package/dist/core/canonical/canonicalize.js +18 -4
  42. package/dist/core/compile/bindings.d.ts +89 -0
  43. package/dist/core/compile/bindings.js +334 -0
  44. package/dist/core/compile/compile.d.ts +17 -0
  45. package/dist/core/compile/compile.js +37 -1
  46. package/dist/core/compile/excluded-content.d.ts +11 -0
  47. package/dist/core/compile/excluded-content.js +42 -0
  48. package/dist/core/compile/expression-legality.d.ts +49 -0
  49. package/dist/core/compile/expression-legality.js +158 -38
  50. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  51. package/dist/core/compile/forbidden-inputs.js +16 -3
  52. package/dist/core/compile/interface-inventory.d.ts +57 -2
  53. package/dist/core/compile/interface-inventory.js +119 -13
  54. package/dist/core/compile/reachability.d.ts +28 -1
  55. package/dist/core/compile/reachability.js +167 -35
  56. package/dist/core/compile/schema-version.d.ts +2 -0
  57. package/dist/core/compile/schema-version.js +25 -0
  58. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  59. package/dist/core/compile/sensitivity-witness.js +110 -23
  60. package/dist/core/compile/step-reference.d.ts +2 -0
  61. package/dist/core/compile/step-reference.js +49 -0
  62. package/dist/core/coverage/operations.d.ts +62 -0
  63. package/dist/core/coverage/operations.js +57 -0
  64. package/dist/core/coverage/relevance.d.ts +4 -2
  65. package/dist/core/coverage/relevance.js +22 -23
  66. package/dist/core/coverage/satisfaction.d.ts +2 -2
  67. package/dist/core/coverage/satisfaction.js +73 -48
  68. package/dist/core/declared-inputs.d.ts +83 -4
  69. package/dist/core/declared-inputs.js +105 -8
  70. package/dist/core/emit/emit.d.ts +9 -0
  71. package/dist/core/emit/emit.js +214 -0
  72. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  73. package/dist/core/emit/private-artifact-digest.js +31 -0
  74. package/dist/core/evaluate/evidence-resolution.d.ts +10 -3
  75. package/dist/core/evaluate/evidence-resolution.js +74 -11
  76. package/dist/core/evaluate/operators.d.ts +22 -0
  77. package/dist/core/evaluate/operators.js +17 -1
  78. package/dist/core/evaluate/resolution.d.ts +14 -3
  79. package/dist/core/evaluate/resolution.js +63 -3
  80. package/dist/core/excluded-content.d.ts +65 -0
  81. package/dist/core/excluded-content.js +113 -0
  82. package/dist/core/failure-codes.d.ts +2 -2
  83. package/dist/core/failure-codes.js +7 -2
  84. package/dist/core/ingest/conditions.d.ts +201 -0
  85. package/dist/core/ingest/conditions.js +48 -0
  86. package/dist/core/ingest/index.d.ts +13 -0
  87. package/dist/core/ingest/index.js +11 -0
  88. package/dist/core/ingest/ingest.d.ts +68 -0
  89. package/dist/core/ingest/ingest.js +318 -0
  90. package/dist/core/lineage/stage-table.d.ts +21 -4
  91. package/dist/core/lineage/stage-table.js +35 -3
  92. package/dist/core/preflight/plan.d.ts +7 -5
  93. package/dist/core/preflight/plan.js +75 -32
  94. package/dist/core/preflight/projection.d.ts +6 -3
  95. package/dist/core/preflight/projection.js +22 -2
  96. package/dist/core/preflight/reduce.js +47 -7
  97. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  98. package/dist/core/preflight/witness-evidence.js +73 -20
  99. package/dist/core/schemas/artifact.d.ts +1049 -30
  100. package/dist/core/schemas/constraint-ledger.js +51 -0
  101. package/dist/core/schemas/defect-signature.d.ts +695 -0
  102. package/dist/core/schemas/defect-signature.js +175 -0
  103. package/dist/core/schemas/eval-contract.d.ts +550 -12
  104. package/dist/core/schemas/eval-contract.js +32 -2
  105. package/dist/core/schemas/evidence-artifact.d.ts +109 -19
  106. package/dist/core/schemas/evidence-artifact.js +64 -8
  107. package/dist/core/schemas/interface.d.ts +741 -13
  108. package/dist/core/schemas/interface.js +120 -7
  109. package/dist/core/schemas/isolation-manifest.js +16 -9
  110. package/dist/core/schemas/plan.d.ts +329 -3
  111. package/dist/core/schemas/plan.js +89 -3
  112. package/dist/core/schemas/pointer.d.ts +47 -1
  113. package/dist/core/schemas/pointer.js +89 -8
  114. package/dist/core/schemas/port-messages.d.ts +194 -10
  115. package/dist/core/schemas/port-messages.js +73 -6
  116. package/dist/core/schemas/probe-body.d.ts +18 -0
  117. package/dist/core/schemas/probe-body.js +13 -0
  118. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  119. package/dist/core/schemas/probe-qualification.js +106 -0
  120. package/dist/core/schemas/probe.d.ts +431 -0
  121. package/dist/core/schemas/probe.js +6 -2
  122. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  123. package/dist/core/schemas/scoring-policy.js +6 -1
  124. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  125. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  126. package/dist/core/schemas/sealed-run-record.d.ts +161 -12
  127. package/dist/core/schemas/sealed-run-record.js +92 -15
  128. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  129. package/dist/core/schemas/sensitivity-witness.js +61 -5
  130. package/dist/core/score/binding-order.d.ts +31 -0
  131. package/dist/core/score/binding-order.js +88 -0
  132. package/dist/core/score/bindings.d.ts +93 -0
  133. package/dist/core/score/bindings.js +309 -0
  134. package/dist/core/score/ladder-table.d.ts +23 -0
  135. package/dist/core/score/ladder-table.js +109 -0
  136. package/dist/core/score/ladder.d.ts +229 -0
  137. package/dist/core/score/ladder.js +464 -0
  138. package/dist/core/score/mode-agreement.d.ts +30 -0
  139. package/dist/core/score/mode-agreement.js +16 -0
  140. package/dist/core/score/outcome-table.d.ts +17 -0
  141. package/dist/core/score/outcome-table.js +172 -0
  142. package/dist/core/score/outcome.d.ts +466 -0
  143. package/dist/core/score/outcome.js +562 -0
  144. package/dist/core/score/qualification.d.ts +83 -0
  145. package/dist/core/score/qualification.js +645 -0
  146. package/dist/core/score/quotation.d.ts +56 -0
  147. package/dist/core/score/quotation.js +226 -0
  148. package/dist/core/score/reduce-trials.d.ts +80 -0
  149. package/dist/core/score/reduce-trials.js +90 -0
  150. package/dist/core/score/score.d.ts +72 -0
  151. package/dist/core/score/score.js +607 -0
  152. package/dist/core/score/selection.d.ts +80 -0
  153. package/dist/core/score/selection.js +98 -0
  154. package/dist/core/score/strength.d.ts +58 -0
  155. package/dist/core/score/strength.js +227 -0
  156. package/dist/core/score/witness.d.ts +138 -0
  157. package/dist/core/score/witness.js +320 -0
  158. package/dist/core/seal/derived-reference.d.ts +2 -9
  159. package/dist/core/seal/derived-reference.js +293 -46
  160. package/dist/core/seal/plan-index.d.ts +18 -3
  161. package/dist/core/seal/plan-index.js +44 -8
  162. package/dist/core/seal/seal.js +8 -3
  163. package/dist/core/stage-contracts.d.ts +62 -2
  164. package/dist/index.d.ts +1 -1
  165. package/dist/index.js +1 -1
  166. package/dist/ports/environment-probe-port.d.ts +60 -4
  167. package/dist/testing/index.d.ts +2 -0
  168. package/dist/testing/index.js +1 -0
  169. package/dist/testing/probe-conformance.d.ts +29 -0
  170. package/dist/testing/probe-conformance.js +34 -5
  171. package/package.json +12 -3
  172. package/schemas/eval-contract.schema.json +1787 -797
  173. package/schemas/evidence-artifact.schema.json +126 -9
  174. package/schemas/isolation-manifest.schema.json +17 -10
  175. package/schemas/probe.schema.json +636 -5
  176. package/schemas/rubric.schema.json +1 -1
  177. package/schemas/scoring-policy.schema.json +8 -1
  178. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  179. package/schemas/sealed-run-record.schema.json +314 -40
@@ -0,0 +1,607 @@
1
+ /**
2
+ * AD-24's score stage: the one orchestration over the reference functions
3
+ * `core/score/` already ships, closing owed item 1's remaining half (a
4
+ * trial-set shape for `validated-observations`) and giving `score.module`
5
+ * its first real value.
6
+ *
7
+ * The body lifts `scripts/worked-example-target.ts:1071-1381`'s order
8
+ * rather than designing a second one, generalised two ways that file never
9
+ * needed: over `readonly ValidatedObservations[]` instead of one record, and
10
+ * without ever throwing on a domain input. Every `fail()` call that script
11
+ * used to stop on a malformed-but-schema-legal shape (a rejected probe, a
12
+ * clean-control or canary probe with no seeded defect, an oracle with no
13
+ * check, a resolution naming a finding the trial does not carry) is replaced
14
+ * here by a graceful, documented fallback: a `schema-parse-failure` already
15
+ * happened at the application boundary before any of these values reached
16
+ * this function, so nothing left to see here is a reason to crash.
17
+ *
18
+ * Two of `AssessmentCommon`'s fields have no source among this stage's five
19
+ * declared inputs and are not among its two caller-supplied parameters
20
+ * either: `evidenceIntegrity.disclosure` (`EvidenceDisclosure`, which lives
21
+ * only on `SealedRunRecord`, a field `ingest`'s product does not carry
22
+ * through) and `remediationState` (`AD-12`'s remediation chain, which needs
23
+ * a caller-presented ancestor sequence score's five inputs never carry, only
24
+ * the current contract). Both arrive declared with a neutral value, exactly
25
+ * the posture `ladder.ts`'s own `EvidenceIntegrityInputs` doc comment
26
+ * already states for `overTruncated`, `unavailable`, and
27
+ * `internallyInconsistent`: "they arrive declared rather than derived."
28
+ * `disclosure` and `remediationState` join them on the same reasoning
29
+ * rather than a new one.
30
+ */
31
+ import { walkExpression } from '../compile/expression-legality.js';
32
+ import { evaluateCoverage } from '../coverage/coverage.js';
33
+ import { makePointerDenotesCollection, makeResolveOperand, referenceSetKeysOf, } from '../evaluate/evidence-resolution.js';
34
+ import { resolveCheck } from '../evaluate/resolution.js';
35
+ import { SEVERITY_LEVELS, } from '../schemas/eval-contract.js';
36
+ import { buildPlanIndex } from '../seal/plan-index.js';
37
+ import { resolveCapturedBindings, selectWithBindings } from './bindings.js';
38
+ import { resolveContractVerdict, resolveProductionVerdict } from './ladder.js';
39
+ import { FINDING_BUCKETS, resolveOutcome, uncitedDefectFindingGaps, uncitedFindingIds, } from './outcome.js';
40
+ import { resolveHomeOperation, sealProbeSet, } from './qualification.js';
41
+ import { reduceTrialSet, TRIAL_VOTE_STATES, } from './reduce-trials.js';
42
+ import { mapFindings, matchProbeWitness } from './witness.js';
43
+ /** A mutable copy of the two record-shaped arrays every witness/finding function this stage calls wants, since `ValidatedObservations`' own arrays are `readonly`. */
44
+ const recordPickOf = (trial) => ({
45
+ observations: [...trial.observations],
46
+ findings: [...trial.findings],
47
+ });
48
+ /**
49
+ * Every interaction-rooted step identifier one oracle's `check` addresses.
50
+ * Reimplemented from `scripts/worked-example-target.ts`'s own private
51
+ * helper of the same name: nothing in `src/` exports it, and `core/` cannot
52
+ * import `scripts/`.
53
+ */
54
+ function addressedSteps(expression) {
55
+ const found = new Set();
56
+ const take = (operand) => {
57
+ if (!('pointer' in operand))
58
+ return;
59
+ const { pointer } = operand;
60
+ if (pointer.startsWith('@'))
61
+ return;
62
+ const [, root, stepId] = pointer.split('/');
63
+ if (root === 'interactions' && stepId !== undefined)
64
+ found.add(stepId);
65
+ };
66
+ walkExpression(expression, 0, '', { onOperand: take, onSetOperand: take });
67
+ return found;
68
+ }
69
+ /**
70
+ * AD-40 pairs a probe with exactly one designated oracle: the one
71
+ * discharging the behaviour its seeded defect breaks. Anchored on
72
+ * `probe.behaviorId` rather than `probe.defects[0]?.behaviorId` (the worked
73
+ * example's own anchor): AD-9 puts the behaviour on the probe itself, on
74
+ * every branch and every class, while `defects` is empty on a canary and
75
+ * absent-shaped on a clean control, so an anchor scoped to `defects[0]`
76
+ * cannot generalise across probe classes the way this stage's "never throw
77
+ * on a domain input" rule requires. Returns `null`, never throws, when the
78
+ * probe's own behaviour resolves to no oracle or to more than one: a real,
79
+ * reportable authoring gap rather than a crash.
80
+ */
81
+ function designatedOracleIdOf(probe, contract) {
82
+ const behavior = contract.behaviors.find((entry) => entry.id === probe.behaviorId);
83
+ if (behavior === undefined)
84
+ return null;
85
+ if (behavior.oracles.length !== 1)
86
+ return null;
87
+ return behavior.oracles[0] ?? null;
88
+ }
89
+ /** A probe on the seeding branch whose signature is present, or `null` for a clean control, a canary, or a signature-less defect probe -- none of which AD-40's witness match applies to. */
90
+ function signedProbeOf(probe) {
91
+ if (probe.expectedClean)
92
+ return null;
93
+ if (probe.defectSignature === null)
94
+ return null;
95
+ return probe;
96
+ }
97
+ /**
98
+ * The highest severity among every behaviour this oracle discharges, the
99
+ * same source the worked example reads (`resolution.resolvedFrom === null`
100
+ * case). The maximum, not the first, on `coverage.ts`'s `coverageSeverity`
101
+ * precedent: a lower reduction would let one trivial behaviour understate a
102
+ * shared oracle's real severity. `'low'` when no behaviour names the oracle
103
+ * at all -- a genuine authoring gap this pure function has no better signal
104
+ * for, and never a reason to throw.
105
+ */
106
+ function severityOfBehaviourFor(contract, oracleId) {
107
+ let best = null;
108
+ for (const behavior of contract.behaviors) {
109
+ if (!behavior.oracles.includes(oracleId))
110
+ continue;
111
+ if (best === null ||
112
+ SEVERITY_LEVELS.indexOf(behavior.severity) > SEVERITY_LEVELS.indexOf(best)) {
113
+ best = behavior.severity;
114
+ }
115
+ }
116
+ return best ?? 'low';
117
+ }
118
+ /**
119
+ * `judgeConduct` derives once per run, not per oracle-criterion, since no
120
+ * schema field maps a rubric criterion to an oracle. `'absent'` is
121
+ * `outcome.ts:75`'s own "ordinary value" case; `'malformed'` when any trial
122
+ * carries a `judge-result-unscored` condition; `'conforming'` otherwise.
123
+ */
124
+ function judgeConductOf(contract, trials) {
125
+ if (contract.rubrics.length === 0)
126
+ return 'absent';
127
+ const anyMalformed = trials.some((trial) => trial.conditions.some((condition) => condition.kind === 'judge-result-unscored'));
128
+ return anyMalformed ? 'malformed' : 'conforming';
129
+ }
130
+ /**
131
+ * The ninth new Invalid condition: an observation whose `operationId`
132
+ * matches an operation declared in more than one `permittedInterfaces`
133
+ * entry. Not an `IngestCondition` -- ingest never computes it, since it has
134
+ * no `eval-contract` input and `Observation` carries no interface
135
+ * qualifier -- so `score.ts` renders each basis line itself.
136
+ */
137
+ function operationIdentifierCollisionsOf(contract, trials) {
138
+ const interfacesByOperationId = new Map();
139
+ for (const iface of contract.permittedInterfaces) {
140
+ // Deduplicated per interface first: `PermittedInterface.operations`
141
+ // carries no uniqueness constraint, so one interface declaring the
142
+ // same `operationId` on two different operations must still count as
143
+ // one interface, not two, or this row would falsely fire a collision
144
+ // naming the same interface twice.
145
+ const operationIdsInThisInterface = new Set(iface.operations.map((operation) => operation.operationId));
146
+ for (const operationId of operationIdsInThisInterface) {
147
+ const entry = interfacesByOperationId.get(operationId);
148
+ if (entry === undefined) {
149
+ interfacesByOperationId.set(operationId, [iface.logicalId]);
150
+ }
151
+ else {
152
+ entry.push(iface.logicalId);
153
+ }
154
+ }
155
+ }
156
+ const collisions = [];
157
+ trials.forEach((trial, trialIndex) => {
158
+ for (const observation of trial.observations) {
159
+ const interfaces = interfacesByOperationId.get(observation.operationId) ?? [];
160
+ if (interfaces.length <= 1)
161
+ continue;
162
+ collisions.push(`trial ${trialIndex + 1} observation ${observation.observationId}: operationId "${observation.operationId}" matches operations in ${interfaces.length} permittedInterfaces entries (${interfaces.join(', ')})`);
163
+ }
164
+ });
165
+ return collisions;
166
+ }
167
+ /**
168
+ * The tenth new Invalid condition: a caller assembling a trial set from
169
+ * records that disagree on `mode`, `evaluatorRecommendation`, or `runId`.
170
+ * Every trial is compared against the first: a trial set is not a genuine
171
+ * set once one trial's own value is picked as authoritative, regardless of
172
+ * which one, so basis lines name every disagreeing pair. `runId` joined the
173
+ * other two once `ValidatedObservations` carried it: batching trials from
174
+ * two different runs into one trial set is the single most important
175
+ * cross-trial mixup this check exists to catch, and it read the same
176
+ * fallback posture as the other two without being compared like them.
177
+ */
178
+ function trialSetDisagreementsOf(trials) {
179
+ const first = trials[0];
180
+ if (first === undefined)
181
+ return [];
182
+ const disagreements = [];
183
+ trials.forEach((trial, index) => {
184
+ if (index === 0)
185
+ return;
186
+ if (trial.mode !== first.mode) {
187
+ disagreements.push(`mode: trial 1 = "${first.mode}", trial ${index + 1} = "${trial.mode}"`);
188
+ }
189
+ if (trial.evaluatorRecommendation !== first.evaluatorRecommendation) {
190
+ disagreements.push(`evaluatorRecommendation: trial 1 = "${first.evaluatorRecommendation}", trial ${index + 1} = "${trial.evaluatorRecommendation}"`);
191
+ }
192
+ if (trial.runId !== first.runId) {
193
+ disagreements.push(`runId: trial 1 = "${first.runId}", trial ${index + 1} = "${trial.runId}"`);
194
+ }
195
+ });
196
+ return disagreements;
197
+ }
198
+ /**
199
+ * Cross-trial identifier reuse is NOT a collision, and this is where that was
200
+ * settled after being carried as an open question.
201
+ *
202
+ * `ingest` computes `duplicate-record-identifier` per sealed run record, since
203
+ * it sees one trial at a time, and nothing checks whether two DIFFERENT trials
204
+ * of one set reuse an observation, finding, or oracle-disposition identifier.
205
+ * That asymmetry looks like a gap beside `trial-set-field-disagreement`, which
206
+ * does compare `mode` and `evaluatorRecommendation` across trials.
207
+ *
208
+ * It is not one. A trial set is n independent evaluator runs of one contract,
209
+ * each producing its own record, and a harness that names its first observation
210
+ * `obs-1` names it that in every run. Reporting the second run for it would
211
+ * make a repeated run Invalid by construction, which is the opposite of what a
212
+ * trial set is for. The fields that ARE compared across trials are compared
213
+ * because they describe the SET rather than a run within it: two trials
214
+ * disagreeing about which mode they ran under cannot both be true of one set,
215
+ * while two trials both carrying `obs-1` are two true statements about two
216
+ * runs.
217
+ *
218
+ * The pooled basis this stage builds carries each entry with the trial it came
219
+ * from, so a repeated identifier is never resolved against the wrong row.
220
+ */
221
+ /** Every `IngestCondition` of one `kind` across every trial, in trial order. */
222
+ function conditionsAcrossTrials(trials, kind) {
223
+ return trials.flatMap((trial) => trial.conditions.filter((condition) => condition.kind === kind));
224
+ }
225
+ /**
226
+ * Reused from `reduce-trials.ts` rather than a second literal: `resolveOutcome`
227
+ * is the one AD-6 state assigner, and re-listing the three AD-6 invalidating
228
+ * states here would be a second place that vocabulary could drift from the
229
+ * reducer's own `TRIAL_VOTE_STATES.invalidating` grouping.
230
+ */
231
+ const INVALIDATING_OUTCOME_STATES = new Set(TRIAL_VOTE_STATES.invalidating);
232
+ /**
233
+ * The stage. Signature order matches `ScoreStage`'s own: the five declared
234
+ * artifact inputs, then `waiver` and `evaluationFault`, the two documented
235
+ * caller-supplied parameters -- neither has a source among those five, and
236
+ * each arrives named and explicit rather than a hardcoded literal.
237
+ */
238
+ export const score = (contract, trials, probe, preflightVerdict, policy, waiver, evaluationFault) => {
239
+ // Probe sealing: once per run, never per trial, since qualification reads
240
+ // the probe and the contract's operation inventory alone. `probeQualified`
241
+ // reads whichever bucket the probe actually lands in -- never a throw on
242
+ // rejection, since a rejected probe is a legitimate domain outcome the
243
+ // existing `unqualified-probe-in-sealed-set` condition already reports.
244
+ const homeOperationOf = (candidate) => candidate.expectedClean || candidate.defectSignature === null
245
+ ? null
246
+ : resolveHomeOperation(candidate.defectSignature, contract.permittedInterfaces);
247
+ const sealedProbes = sealProbeSet([probe], homeOperationOf);
248
+ const qualifiedEntry = sealedProbes.admitted[0] ?? sealedProbes.rejected[0];
249
+ const probeQualified = qualifiedEntry === undefined ? false : qualifiedEntry.result.qualified;
250
+ const signedProbe = signedProbeOf(probe);
251
+ const designatedOracleId = designatedOracleIdOf(probe, contract);
252
+ const probeSigned = !probe.expectedClean && probe.defectSignature !== null;
253
+ // Plan indexing and the resolvers built from it: contract-only, so built
254
+ // once and reused across every trial. `resolveCapturedBindings` walks
255
+ // `bindingOrder`'s own tiers internally and already degrades a cyclic
256
+ // plan to "resolves as unlisted, filters every candidate away" without
257
+ // throwing, so this stage does not re-run that check itself.
258
+ // `duplicateIds: 'unresolved'` rather than `buildPlanIndex`'s own default
259
+ // `'throw'`: two `permittedInterfaces` entries sharing an operationId is
260
+ // exactly the domain input `operationIdentifierCollisionsOf` below
261
+ // reports as `operation-identifier-collision`, so this stage cannot let
262
+ // the index builder crash on the same shape its own new Invalid row
263
+ // exists to describe.
264
+ const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, {
265
+ duplicateIds: 'unresolved',
266
+ });
267
+ const pointerDenotesCollection = makePointerDenotesCollection(contract, index);
268
+ const referenceSets = Object.fromEntries(Object.entries(contract.referenceSets ?? {}).map(([id, declaration]) => [
269
+ id,
270
+ declaration.members,
271
+ ]));
272
+ // The declared keys the members map above drops. `set-membership`'s set
273
+ // position reads the single declared key off each member.
274
+ const referenceSetKeys = referenceSetKeysOf(contract);
275
+ const judgeConduct = judgeConductOf(contract, trials);
276
+ const allOutcomes = [];
277
+ // The full `EvidenceArtifact.outcomes` shape, built alongside `allOutcomes`
278
+ // above in the same loop rather than derived from it after the fact: the
279
+ // two carry different fields from the same per-oracle locals and neither
280
+ // is reconstructible from the other.
281
+ const outcomes = [];
282
+ const votes = [];
283
+ for (const trial of trials) {
284
+ const captured = resolveCapturedBindings(contract.interactionPlan, index, trial.observations);
285
+ const selectionOf = new Map();
286
+ for (const step of contract.interactionPlan) {
287
+ selectionOf.set(step.stepId, selectWithBindings(step, trial.observations, index, captured));
288
+ }
289
+ // One observation per step, mirroring `resolveTemporalAnchor`'s own
290
+ // rule: one match binds, several under a declared `any` binds the
291
+ // lowest sequence, anything else binds nothing. A step this trial
292
+ // never selected, or whose matched id names no observation, is
293
+ // skipped rather than failed: a check addressing that step simply
294
+ // resolves against an absent operand, which `resolveCheck` already
295
+ // handles.
296
+ const observationById = new Map(trial.observations.map((observation) => [
297
+ observation.observationId,
298
+ observation,
299
+ ]));
300
+ const stepObservations = {};
301
+ for (const step of contract.interactionPlan) {
302
+ const selection = selectionOf.get(step.stepId);
303
+ if (selection === undefined)
304
+ continue;
305
+ const [first] = selection.matchedObservationIds;
306
+ if (first === undefined)
307
+ continue;
308
+ if (selection.result === 'several' && step.cardinality !== 'any')
309
+ continue;
310
+ const observation = observationById.get(first);
311
+ if (observation === undefined)
312
+ continue;
313
+ stepObservations[step.stepId] = observation;
314
+ }
315
+ const resolveOperand = makeResolveOperand(stepObservations, referenceSets);
316
+ const recordPick = recordPickOf(trial);
317
+ const witness = signedProbe === null
318
+ ? null
319
+ : matchProbeWitness(signedProbe, contract.permittedInterfaces, recordPick);
320
+ const findingMap = mapFindings([probe], contract.permittedInterfaces, recordPick);
321
+ const bucketOf = new Map();
322
+ for (const bucket of FINDING_BUCKETS) {
323
+ for (const entry of findingMap[bucket])
324
+ bucketOf.set(entry.findingId, bucket);
325
+ }
326
+ let designatedState;
327
+ let firstInvalidatingState;
328
+ let firstState;
329
+ for (const oracle of contract.oracles) {
330
+ const check = oracle.check;
331
+ const steps = check === null ? new Set() : addressedSteps(check);
332
+ const addressed = contract.interactionPlan
333
+ .filter((step) => steps.has(step.stepId))
334
+ .map((step) => ({
335
+ step,
336
+ selection: selectionOf.get(step.stepId) ?? {
337
+ result: 'none',
338
+ matchedObservationIds: [],
339
+ },
340
+ }));
341
+ const selections = addressed.map((entry) => entry.selection);
342
+ const selectorAmbiguity = addressed.some((entry) => entry.selection.result === 'several' &&
343
+ entry.step.cardinality !== 'any');
344
+ const checkResolution = check === null
345
+ ? null
346
+ : resolveCheck(check, resolveOperand, pointerDenotesCollection, referenceSetKeys, policy.regexMatchStepBudget, `EvalContract.oracles[id=${oracle.id}].check`);
347
+ // Every disposition this trial records for this oracle, not just
348
+ // the first: `SealedRunRecord.oracleDispositions` carries no
349
+ // uniqueness constraint on `oracleId`, and ingest's
350
+ // `duplicate-record-identifier` condition only advisory-flags a
351
+ // repeat, never rejects it, so two dispositions naming one oracle
352
+ // is a legal, ambiguous input. More than one match is treated as
353
+ // ambiguous -- `disposition: null` -- rather than silently
354
+ // picking the array's first entry, the same guard-rather-than-pick
355
+ // idiom `citedFinding` below already uses.
356
+ const matchingDispositions = trial.dispositions.filter((entry) => entry.oracleId === oracle.id);
357
+ const [onlyDisposition] = matchingDispositions;
358
+ const disposition = matchingDispositions.length === 1 && onlyDisposition !== undefined
359
+ ? onlyDisposition
360
+ : null;
361
+ // Every defect finding this trial cites against this oracle, not
362
+ // just the first: two distinct findings citing the same oracle is
363
+ // schema-legal and not caught by ingest's `duplicate-record-identifier`
364
+ // (that one is keyed by `findingId`, not `oracleId`). Picking either
365
+ // one arbitrarily would make the outcome depend on array order, so
366
+ // more than one match is treated as ambiguous -- `citedFinding: null`
367
+ // -- rather than silently choosing one, the same guard-rather-than-pick
368
+ // idiom `outcomesByProbeId` (`strength.ts`) already uses.
369
+ const defectFindingsForOracle = trial.findings.filter((finding) => finding.findingType === 'defect' && finding.oracleId === oracle.id);
370
+ const [onlyDefectFinding] = defectFindingsForOracle;
371
+ const citedFinding = defectFindingsForOracle.length === 1 && onlyDefectFinding !== undefined
372
+ ? {
373
+ findingId: onlyDefectFinding.findingId,
374
+ // `mapFindings` buckets every defect finding in this trial, so a
375
+ // miss here cannot happen for a finding this same trial
376
+ // produced; the fallback is fail-closed, never a throw.
377
+ bucket: bucketOf.get(onlyDefectFinding.findingId) ?? 'dangling',
378
+ }
379
+ : null;
380
+ const inputs = {
381
+ required: true,
382
+ disposition,
383
+ citedFinding,
384
+ witness: designatedOracleId !== null && oracle.id === designatedOracleId
385
+ ? witness
386
+ : null,
387
+ selections,
388
+ selectorAmbiguity,
389
+ checkResolution: checkResolution === null ? null : checkResolution.resolution,
390
+ polarity: oracle.polarity,
391
+ probeClass: probe.probeClass,
392
+ expectedClean: probe.expectedClean,
393
+ probeSigned,
394
+ probeQualified,
395
+ waiver,
396
+ judgeConduct,
397
+ evaluationFault,
398
+ };
399
+ const resolution = resolveOutcome(inputs);
400
+ // The same ambiguity guard as `disposition` and `citedFinding`
401
+ // above: `findingId` carries no uniqueness constraint either (also
402
+ // only advisory-flagged by `duplicate-record-identifier`, subject
403
+ // `'finding'`), so two findings sharing `resolution.resolvedFrom`
404
+ // is legal. Reading either one's `.severity` would make the
405
+ // outcome depend on array order; more than one match falls
406
+ // through to the same behaviour-severity floor a missing match
407
+ // already uses, rather than picking one arbitrarily.
408
+ const findingsResolvedFrom = resolution.resolvedFrom === null
409
+ ? []
410
+ : trial.findings.filter((finding) => finding.findingId === resolution.resolvedFrom);
411
+ const [onlyFindingResolvedFrom] = findingsResolvedFrom;
412
+ const severity = findingsResolvedFrom.length === 1 &&
413
+ onlyFindingResolvedFrom !== undefined
414
+ ? onlyFindingResolvedFrom.severity
415
+ : severityOfBehaviourFor(contract, oracle.id);
416
+ allOutcomes.push({
417
+ oracleId: oracle.id,
418
+ required: true,
419
+ severity,
420
+ checkResolved: inputs.checkResolution !== null,
421
+ resolution,
422
+ });
423
+ outcomes.push({
424
+ oracleId: oracle.id,
425
+ // Constant across every entry: one `score()` call scores exactly
426
+ // one probe.
427
+ probeId: probe.probeId,
428
+ state: resolution.state,
429
+ severity,
430
+ // `ORACLE_DISPOSITIONS`' third member, `'not-attempted'`, on a
431
+ // `null` local `disposition`: no disposition was recorded for
432
+ // this oracle, or the ambiguity guard above fired. Both mean
433
+ // "nothing was recorded", which is the honest reading of the
434
+ // one closed-three member that says so.
435
+ disposition: disposition === null ? 'not-attempted' : disposition.disposition,
436
+ resolvedFrom: resolution.resolvedFrom,
437
+ corroboration: resolution.corroboration,
438
+ selectedObservationIds: [...resolution.selectedObservationIds],
439
+ checkResolution,
440
+ });
441
+ if (firstState === undefined)
442
+ firstState = resolution.state;
443
+ if (firstInvalidatingState === undefined &&
444
+ INVALIDATING_OUTCOME_STATES.has(resolution.state)) {
445
+ firstInvalidatingState = resolution.state;
446
+ }
447
+ // First match only: `Oracle.id` carries no schema- or compile-time
448
+ // uniqueness constraint, so two oracles could share
449
+ // `id === designatedOracleId`. Guarding against overwrite here is
450
+ // the same last-write-wins fix `outcomesByProbeId` (`strength.ts`)
451
+ // already applies, for the identical reason.
452
+ if (designatedOracleId !== null &&
453
+ oracle.id === designatedOracleId &&
454
+ designatedState === undefined) {
455
+ designatedState = resolution.state;
456
+ }
457
+ }
458
+ // One vote per trial, ordinarily: the trial set's own cardinality
459
+ // (`Trials.completed`) is `votes.length`, so a trial still
460
+ // contributes a vote when the probe has no designated oracle (a
461
+ // clean control, a canary, or a malformed defect chain) -- the
462
+ // fallback order is the first invalidating state this trial's
463
+ // oracles produced, else the first oracle's state. Every candidate
464
+ // is a state `resolveOutcome` itself assigned this trial, never a
465
+ // literal this module invents: `resolveOutcome` stays the one
466
+ // assigner of an AD-6 state. `contract.oracles` declaring none at
467
+ // all is the one shape with no state to vote with, and that trial
468
+ // contributes no vote rather than a fabricated one.
469
+ const voteState = designatedState ?? firstInvalidatingState ?? firstState;
470
+ if (voteState !== undefined) {
471
+ votes.push({ state: voteState });
472
+ }
473
+ }
474
+ const reduced = reduceTrialSet(votes, policy.catchThreshold);
475
+ const trialsField = {
476
+ declaredMinimum: policy.minimumTrialCount,
477
+ completed: votes.length,
478
+ invalidatedAttempts: [...reduced.invalidatedAttempts],
479
+ };
480
+ const unwitnessedQuotations = trials.flatMap((trial) => trial.unwitnessedQuotations);
481
+ const isolationViolation = trials.flatMap((trial) => trial.isolationViolation);
482
+ const findings = trials.flatMap((trial) => trial.findings.map((finding) => ({
483
+ findingId: finding.findingId,
484
+ confidence: finding.confidence,
485
+ })));
486
+ const uncitedDefectFindings = trials.flatMap((trial) => uncitedDefectFindingGaps(recordPickOf(trial)));
487
+ // Every finding across every trial citing no oracle (`emit`'s own
488
+ // `uncitedFindings` field): broader than `uncitedDefectFindings` above
489
+ // (every finding type, not `defect` only) and thinner (an identifier
490
+ // only), per `outcome.ts`'s own doc comment on the two functions
491
+ // coexisting rather than one replacing the other.
492
+ const uncitedFindings = trials.flatMap((trial) => uncitedFindingIds(recordPickOf(trial)));
493
+ const coverageGaps = evaluateCoverage(contract);
494
+ const operationIdentifierCollisions = operationIdentifierCollisionsOf(contract, trials);
495
+ const trialSetDisagreements = trialSetDisagreementsOf(trials);
496
+ const evidenceIntegrity = {
497
+ // Declared, not derived: no declared input or caller-supplied
498
+ // parameter carries `EvidenceDisclosure`, `ingest`'s product does not
499
+ // restate it, and the other three siblings are declared `false` for
500
+ // the identical reason (see this module's own header comment).
501
+ disclosure: { truncationBound: null, reportedIncomplete: false },
502
+ overTruncated: false,
503
+ unavailable: false,
504
+ internallyInconsistent: false,
505
+ isolationViolation,
506
+ duplicateRecordIdentifiers: conditionsAcrossTrials(trials, 'duplicate-record-identifier'),
507
+ danglingCitations: conditionsAcrossTrials(trials, 'dangling-citation'),
508
+ danglingDispositionCitations: conditionsAcrossTrials(trials, 'dangling-disposition-citation'),
509
+ forbiddenInputsNotWithheld: conditionsAcrossTrials(trials, 'forbidden-input-not-withheld'),
510
+ crossArtifactDisagreements: conditionsAcrossTrials(trials, 'cross-artifact-disagreement'),
511
+ evaluatorConfigurationAbsent: conditionsAcrossTrials(trials, 'evaluator-configuration-absent'),
512
+ evaluatorConfigurationDigestMismatches: conditionsAcrossTrials(trials, 'evaluator-configuration-digest-mismatch'),
513
+ judgeResultsUnscored: conditionsAcrossTrials(trials, 'judge-result-unscored'),
514
+ operationIdentifierCollisions,
515
+ trialSetDisagreements,
516
+ };
517
+ // Every trial in the set is asserted to agree with the
518
+ // first on `mode` and `evaluatorRecommendation`; a disagreement is
519
+ // `trial-set-field-disagreement` above, never a throw. The first
520
+ // trial's own values build the one assessment TypeScript's
521
+ // discriminated union still requires -- non-silence comes from the
522
+ // Invalid row's basis line, not from withholding a value. A caller
523
+ // supplying zero trials has no first value to read; both fields fall
524
+ // back to a neutral default, and `below-minimum-trial-count` already
525
+ // reports zero completed trials against the declared minimum.
526
+ const firstTrial = trials[0];
527
+ const mode = firstTrial?.mode ?? 'production';
528
+ const evaluatorRecommendation = firstTrial?.evaluatorRecommendation ?? 'PASS';
529
+ // Same posture as `mode`/`evaluatorRecommendation` above: read off the
530
+ // first trial, never derived or defaulted from anything richer. A caller
531
+ // supplying zero trials has no first value to read either; the empty
532
+ // string is the honest "no run identifier was presented" reading, on the
533
+ // same terms `below-minimum-trial-count` already reports the shortfall.
534
+ const runId = firstTrial?.runId ?? '';
535
+ const commonBody = {
536
+ outcomeState: {
537
+ outcomes: allOutcomes,
538
+ unwitnessedQuotations,
539
+ trials: trialsField,
540
+ reExecutionCap: policy.reExecutionCap,
541
+ },
542
+ evidenceIntegrity,
543
+ evaluatorRecommendation,
544
+ coverageGaps,
545
+ uncitedDefectFindings,
546
+ findings,
547
+ confidenceThreshold: policy.confidenceThreshold,
548
+ // Declared, not derived, on the same posture as `disclosure` below:
549
+ // `AD-12`'s remediation chain needs a caller-presented ancestor
550
+ // sequence, and score's five declared inputs carry only the current
551
+ // contract, never a chain. `validateLineageChain([contract], {...})`
552
+ // looks buildable from what score already has, but is not: it treats
553
+ // the one-element array as the WHOLE chain, so any contract with
554
+ // `revisionCount > 0` -- an ordinarily revised one -- fails the root,
555
+ // parent-resolution, and length checks unconditionally, firing the
556
+ // existing `lineage-chain-inconsistent` FAIL row on every such run
557
+ // regardless of whether anything is actually wrong. The vacuously
558
+ // true value below is what "no chain was presented to validate"
559
+ // honestly means.
560
+ remediationState: {
561
+ lengthConsistent: true,
562
+ noRepeatedDigest: true,
563
+ noGap: true,
564
+ },
565
+ preflightPassed: preflightVerdict.passed,
566
+ severityFloor: policy.severityFloor,
567
+ };
568
+ if (mode === 'production') {
569
+ const assessment = {
570
+ mode: 'production',
571
+ ...commonBody,
572
+ };
573
+ return {
574
+ assessment,
575
+ ladder: resolveProductionVerdict(assessment),
576
+ runId,
577
+ contract,
578
+ policy,
579
+ probe,
580
+ sealedProbes,
581
+ trialSetResult: reduced,
582
+ outcomes,
583
+ uncitedFindings,
584
+ };
585
+ }
586
+ const assessment = {
587
+ mode: 'contract-scoring',
588
+ ...commonBody,
589
+ systemRecommendationRecorded: evaluatorRecommendation,
590
+ // No declared input or caller-supplied parameter carries authored
591
+ // prose for this field; `null` is its own legal, honest value rather
592
+ // than an invented note.
593
+ systemRecommendationNote: null,
594
+ };
595
+ return {
596
+ assessment,
597
+ ladder: resolveContractVerdict(assessment),
598
+ runId,
599
+ contract,
600
+ policy,
601
+ probe,
602
+ sealedProbes,
603
+ trialSetResult: reduced,
604
+ outcomes,
605
+ uncitedFindings,
606
+ };
607
+ };