eval-quality 0.1.0 → 0.2.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 (133) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +17 -11
  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/no-collection-quantifier.json +1 -1
  13. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  14. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  15. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  16. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  17. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  18. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  19. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  20. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  21. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  22. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  23. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  24. package/corpus/dev/index.json +1 -1
  25. package/dist/application/index.d.ts +2 -0
  26. package/dist/application/index.js +1 -0
  27. package/dist/application/score.d.ts +48 -0
  28. package/dist/application/score.js +186 -0
  29. package/dist/cli/arguments.d.ts +7 -3
  30. package/dist/cli/arguments.js +73 -5
  31. package/dist/cli/exit-codes.d.ts +6 -2
  32. package/dist/cli/exit-codes.js +8 -9
  33. package/dist/cli/main.js +9 -0
  34. package/dist/cli/render.d.ts +1 -1
  35. package/dist/cli/render.js +6 -4
  36. package/dist/cli/run.d.ts +13 -3
  37. package/dist/cli/run.js +136 -16
  38. package/dist/core/canonical/canonicalize.d.ts +12 -0
  39. package/dist/core/canonical/canonicalize.js +18 -4
  40. package/dist/core/compile/bindings.d.ts +70 -0
  41. package/dist/core/compile/bindings.js +317 -0
  42. package/dist/core/compile/compile.d.ts +5 -0
  43. package/dist/core/compile/compile.js +9 -0
  44. package/dist/core/compile/expression-legality.d.ts +49 -0
  45. package/dist/core/compile/expression-legality.js +105 -31
  46. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  47. package/dist/core/compile/forbidden-inputs.js +16 -3
  48. package/dist/core/compile/interface-inventory.d.ts +23 -1
  49. package/dist/core/compile/interface-inventory.js +32 -3
  50. package/dist/core/compile/reachability.d.ts +18 -1
  51. package/dist/core/compile/reachability.js +46 -1
  52. package/dist/core/compile/sensitivity-witness.d.ts +1 -1
  53. package/dist/core/compile/sensitivity-witness.js +1 -1
  54. package/dist/core/emit/emit.d.ts +9 -0
  55. package/dist/core/emit/emit.js +214 -0
  56. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  57. package/dist/core/emit/private-artifact-digest.js +31 -0
  58. package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
  59. package/dist/core/evaluate/evidence-resolution.js +4 -1
  60. package/dist/core/failure-codes.d.ts +2 -2
  61. package/dist/core/failure-codes.js +4 -2
  62. package/dist/core/ingest/conditions.d.ts +201 -0
  63. package/dist/core/ingest/conditions.js +48 -0
  64. package/dist/core/ingest/index.d.ts +13 -0
  65. package/dist/core/ingest/index.js +11 -0
  66. package/dist/core/ingest/ingest.d.ts +68 -0
  67. package/dist/core/ingest/ingest.js +310 -0
  68. package/dist/core/lineage/stage-table.d.ts +21 -4
  69. package/dist/core/lineage/stage-table.js +35 -3
  70. package/dist/core/preflight/witness-evidence.js +10 -0
  71. package/dist/core/schemas/artifact.d.ts +385 -14
  72. package/dist/core/schemas/constraint-ledger.js +40 -0
  73. package/dist/core/schemas/defect-signature.d.ts +273 -0
  74. package/dist/core/schemas/defect-signature.js +127 -0
  75. package/dist/core/schemas/eval-contract.d.ts +33 -0
  76. package/dist/core/schemas/eval-contract.js +14 -0
  77. package/dist/core/schemas/evidence-artifact.d.ts +99 -19
  78. package/dist/core/schemas/evidence-artifact.js +64 -8
  79. package/dist/core/schemas/plan.d.ts +112 -0
  80. package/dist/core/schemas/plan.js +67 -2
  81. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  82. package/dist/core/schemas/probe-qualification.js +106 -0
  83. package/dist/core/schemas/probe.d.ts +312 -0
  84. package/dist/core/schemas/probe.js +6 -2
  85. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  86. package/dist/core/schemas/scoring-policy.js +6 -1
  87. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  88. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  89. package/dist/core/schemas/sealed-run-record.d.ts +30 -3
  90. package/dist/core/schemas/sealed-run-record.js +37 -5
  91. package/dist/core/score/binding-order.d.ts +31 -0
  92. package/dist/core/score/binding-order.js +88 -0
  93. package/dist/core/score/bindings.d.ts +93 -0
  94. package/dist/core/score/bindings.js +297 -0
  95. package/dist/core/score/ladder-table.d.ts +23 -0
  96. package/dist/core/score/ladder-table.js +109 -0
  97. package/dist/core/score/ladder.d.ts +229 -0
  98. package/dist/core/score/ladder.js +464 -0
  99. package/dist/core/score/mode-agreement.d.ts +30 -0
  100. package/dist/core/score/mode-agreement.js +16 -0
  101. package/dist/core/score/outcome-table.d.ts +17 -0
  102. package/dist/core/score/outcome-table.js +172 -0
  103. package/dist/core/score/outcome.d.ts +466 -0
  104. package/dist/core/score/outcome.js +562 -0
  105. package/dist/core/score/qualification.d.ts +83 -0
  106. package/dist/core/score/qualification.js +615 -0
  107. package/dist/core/score/quotation.d.ts +79 -0
  108. package/dist/core/score/quotation.js +201 -0
  109. package/dist/core/score/reduce-trials.d.ts +80 -0
  110. package/dist/core/score/reduce-trials.js +90 -0
  111. package/dist/core/score/score.d.ts +72 -0
  112. package/dist/core/score/score.js +581 -0
  113. package/dist/core/score/selection.d.ts +80 -0
  114. package/dist/core/score/selection.js +98 -0
  115. package/dist/core/score/strength.d.ts +49 -0
  116. package/dist/core/score/strength.js +193 -0
  117. package/dist/core/score/witness.d.ts +117 -0
  118. package/dist/core/score/witness.js +315 -0
  119. package/dist/core/seal/derived-reference.d.ts +1 -1
  120. package/dist/core/seal/derived-reference.js +236 -28
  121. package/dist/core/seal/seal.js +8 -3
  122. package/dist/core/stage-contracts.d.ts +62 -2
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/testing/index.d.ts +2 -0
  126. package/dist/testing/index.js +1 -0
  127. package/package.json +12 -3
  128. package/schemas/eval-contract.schema.json +100 -2
  129. package/schemas/evidence-artifact.schema.json +96 -9
  130. package/schemas/probe.schema.json +508 -3
  131. package/schemas/scoring-policy.schema.json +8 -1
  132. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  133. package/schemas/sealed-run-record.schema.json +19 -2
@@ -0,0 +1,581 @@
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, } 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
+ /** Every `IngestCondition` of one `kind` across every trial, in trial order. */
199
+ function conditionsAcrossTrials(trials, kind) {
200
+ return trials.flatMap((trial) => trial.conditions.filter((condition) => condition.kind === kind));
201
+ }
202
+ /**
203
+ * Reused from `reduce-trials.ts` rather than a second literal: `resolveOutcome`
204
+ * is the one AD-6 state assigner, and re-listing the three AD-6 invalidating
205
+ * states here would be a second place that vocabulary could drift from the
206
+ * reducer's own `TRIAL_VOTE_STATES.invalidating` grouping.
207
+ */
208
+ const INVALIDATING_OUTCOME_STATES = new Set(TRIAL_VOTE_STATES.invalidating);
209
+ /**
210
+ * The stage. Signature order matches `ScoreStage`'s own: the five declared
211
+ * artifact inputs, then `waiver` and `evaluationFault`, the two documented
212
+ * caller-supplied parameters -- neither has a source among those five, and
213
+ * each arrives named and explicit rather than a hardcoded literal.
214
+ */
215
+ export const score = (contract, trials, probe, preflightVerdict, policy, waiver, evaluationFault) => {
216
+ // Probe sealing: once per run, never per trial, since qualification reads
217
+ // the probe and the contract's operation inventory alone. `probeQualified`
218
+ // reads whichever bucket the probe actually lands in -- never a throw on
219
+ // rejection, since a rejected probe is a legitimate domain outcome the
220
+ // existing `unqualified-probe-in-sealed-set` condition already reports.
221
+ const homeOperationOf = (candidate) => candidate.expectedClean || candidate.defectSignature === null
222
+ ? null
223
+ : resolveHomeOperation(candidate.defectSignature, contract.permittedInterfaces);
224
+ const sealedProbes = sealProbeSet([probe], homeOperationOf);
225
+ const qualifiedEntry = sealedProbes.admitted[0] ?? sealedProbes.rejected[0];
226
+ const probeQualified = qualifiedEntry === undefined ? false : qualifiedEntry.result.qualified;
227
+ const signedProbe = signedProbeOf(probe);
228
+ const designatedOracleId = designatedOracleIdOf(probe, contract);
229
+ const probeSigned = !probe.expectedClean && probe.defectSignature !== null;
230
+ // Plan indexing and the resolvers built from it: contract-only, so built
231
+ // once and reused across every trial. `resolveCapturedBindings` walks
232
+ // `bindingOrder`'s own tiers internally and already degrades a cyclic
233
+ // plan to "resolves as unlisted, filters every candidate away" without
234
+ // throwing, so this stage does not re-run that check itself.
235
+ // `duplicateIds: 'unresolved'` rather than `buildPlanIndex`'s own default
236
+ // `'throw'`: two `permittedInterfaces` entries sharing an operationId is
237
+ // exactly the domain input `operationIdentifierCollisionsOf` below
238
+ // reports as `operation-identifier-collision`, so this stage cannot let
239
+ // the index builder crash on the same shape its own new Invalid row
240
+ // exists to describe.
241
+ const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, {
242
+ duplicateIds: 'unresolved',
243
+ });
244
+ const pointerDenotesCollection = makePointerDenotesCollection(contract, index);
245
+ const referenceSets = Object.fromEntries(Object.entries(contract.referenceSets ?? {}).map(([id, declaration]) => [
246
+ id,
247
+ declaration.members,
248
+ ]));
249
+ const judgeConduct = judgeConductOf(contract, trials);
250
+ const allOutcomes = [];
251
+ // The full `EvidenceArtifact.outcomes` shape, built alongside `allOutcomes`
252
+ // above in the same loop rather than derived from it after the fact: the
253
+ // two carry different fields from the same per-oracle locals and neither
254
+ // is reconstructible from the other.
255
+ const outcomes = [];
256
+ const votes = [];
257
+ for (const trial of trials) {
258
+ const captured = resolveCapturedBindings(contract.interactionPlan, index, trial.observations);
259
+ const selectionOf = new Map();
260
+ for (const step of contract.interactionPlan) {
261
+ selectionOf.set(step.stepId, selectWithBindings(step, trial.observations, index, captured));
262
+ }
263
+ // One observation per step, mirroring `resolveTemporalAnchor`'s own
264
+ // rule: one match binds, several under a declared `any` binds the
265
+ // lowest sequence, anything else binds nothing. A step this trial
266
+ // never selected, or whose matched id names no observation, is
267
+ // skipped rather than failed: a check addressing that step simply
268
+ // resolves against an absent operand, which `resolveCheck` already
269
+ // handles.
270
+ const observationById = new Map(trial.observations.map((observation) => [
271
+ observation.observationId,
272
+ observation,
273
+ ]));
274
+ const stepObservations = {};
275
+ for (const step of contract.interactionPlan) {
276
+ const selection = selectionOf.get(step.stepId);
277
+ if (selection === undefined)
278
+ continue;
279
+ const [first] = selection.matchedObservationIds;
280
+ if (first === undefined)
281
+ continue;
282
+ if (selection.result === 'several' && step.cardinality !== 'any')
283
+ continue;
284
+ const observation = observationById.get(first);
285
+ if (observation === undefined)
286
+ continue;
287
+ stepObservations[step.stepId] = observation;
288
+ }
289
+ const resolveOperand = makeResolveOperand(stepObservations, referenceSets);
290
+ const recordPick = recordPickOf(trial);
291
+ const witness = signedProbe === null
292
+ ? null
293
+ : matchProbeWitness(signedProbe, contract.permittedInterfaces, recordPick);
294
+ const findingMap = mapFindings([probe], contract.permittedInterfaces, recordPick);
295
+ const bucketOf = new Map();
296
+ for (const bucket of FINDING_BUCKETS) {
297
+ for (const entry of findingMap[bucket])
298
+ bucketOf.set(entry.findingId, bucket);
299
+ }
300
+ let designatedState;
301
+ let firstInvalidatingState;
302
+ let firstState;
303
+ for (const oracle of contract.oracles) {
304
+ const check = oracle.check;
305
+ const steps = check === null ? new Set() : addressedSteps(check);
306
+ const addressed = contract.interactionPlan
307
+ .filter((step) => steps.has(step.stepId))
308
+ .map((step) => ({
309
+ step,
310
+ selection: selectionOf.get(step.stepId) ?? {
311
+ result: 'none',
312
+ matchedObservationIds: [],
313
+ },
314
+ }));
315
+ const selections = addressed.map((entry) => entry.selection);
316
+ const selectorAmbiguity = addressed.some((entry) => entry.selection.result === 'several' &&
317
+ entry.step.cardinality !== 'any');
318
+ const checkResolution = check === null
319
+ ? null
320
+ : resolveCheck(check, resolveOperand, pointerDenotesCollection, policy.regexMatchStepBudget, `EvalContract.oracles[id=${oracle.id}].check`);
321
+ // Every disposition this trial records for this oracle, not just
322
+ // the first: `SealedRunRecord.oracleDispositions` carries no
323
+ // uniqueness constraint on `oracleId`, and ingest's
324
+ // `duplicate-record-identifier` condition only advisory-flags a
325
+ // repeat, never rejects it, so two dispositions naming one oracle
326
+ // is a legal, ambiguous input. More than one match is treated as
327
+ // ambiguous -- `disposition: null` -- rather than silently
328
+ // picking the array's first entry, the same guard-rather-than-pick
329
+ // idiom `citedFinding` below already uses.
330
+ const matchingDispositions = trial.dispositions.filter((entry) => entry.oracleId === oracle.id);
331
+ const [onlyDisposition] = matchingDispositions;
332
+ const disposition = matchingDispositions.length === 1 && onlyDisposition !== undefined
333
+ ? onlyDisposition
334
+ : null;
335
+ // Every defect finding this trial cites against this oracle, not
336
+ // just the first: two distinct findings citing the same oracle is
337
+ // schema-legal and not caught by ingest's `duplicate-record-identifier`
338
+ // (that one is keyed by `findingId`, not `oracleId`). Picking either
339
+ // one arbitrarily would make the outcome depend on array order, so
340
+ // more than one match is treated as ambiguous -- `citedFinding: null`
341
+ // -- rather than silently choosing one, the same guard-rather-than-pick
342
+ // idiom `outcomesByProbeId` (`strength.ts`) already uses.
343
+ const defectFindingsForOracle = trial.findings.filter((finding) => finding.findingType === 'defect' && finding.oracleId === oracle.id);
344
+ const [onlyDefectFinding] = defectFindingsForOracle;
345
+ const citedFinding = defectFindingsForOracle.length === 1 && onlyDefectFinding !== undefined
346
+ ? {
347
+ findingId: onlyDefectFinding.findingId,
348
+ // `mapFindings` buckets every defect finding in this trial, so a
349
+ // miss here cannot happen for a finding this same trial
350
+ // produced; the fallback is fail-closed, never a throw.
351
+ bucket: bucketOf.get(onlyDefectFinding.findingId) ?? 'dangling',
352
+ }
353
+ : null;
354
+ const inputs = {
355
+ required: true,
356
+ disposition,
357
+ citedFinding,
358
+ witness: designatedOracleId !== null && oracle.id === designatedOracleId
359
+ ? witness
360
+ : null,
361
+ selections,
362
+ selectorAmbiguity,
363
+ checkResolution: checkResolution === null ? null : checkResolution.resolution,
364
+ polarity: oracle.polarity,
365
+ probeClass: probe.probeClass,
366
+ expectedClean: probe.expectedClean,
367
+ probeSigned,
368
+ probeQualified,
369
+ waiver,
370
+ judgeConduct,
371
+ evaluationFault,
372
+ };
373
+ const resolution = resolveOutcome(inputs);
374
+ // The same ambiguity guard as `disposition` and `citedFinding`
375
+ // above: `findingId` carries no uniqueness constraint either (also
376
+ // only advisory-flagged by `duplicate-record-identifier`, subject
377
+ // `'finding'`), so two findings sharing `resolution.resolvedFrom`
378
+ // is legal. Reading either one's `.severity` would make the
379
+ // outcome depend on array order; more than one match falls
380
+ // through to the same behaviour-severity floor a missing match
381
+ // already uses, rather than picking one arbitrarily.
382
+ const findingsResolvedFrom = resolution.resolvedFrom === null
383
+ ? []
384
+ : trial.findings.filter((finding) => finding.findingId === resolution.resolvedFrom);
385
+ const [onlyFindingResolvedFrom] = findingsResolvedFrom;
386
+ const severity = findingsResolvedFrom.length === 1 &&
387
+ onlyFindingResolvedFrom !== undefined
388
+ ? onlyFindingResolvedFrom.severity
389
+ : severityOfBehaviourFor(contract, oracle.id);
390
+ allOutcomes.push({
391
+ oracleId: oracle.id,
392
+ required: true,
393
+ severity,
394
+ checkResolved: inputs.checkResolution !== null,
395
+ resolution,
396
+ });
397
+ outcomes.push({
398
+ oracleId: oracle.id,
399
+ // Constant across every entry: one `score()` call scores exactly
400
+ // one probe.
401
+ probeId: probe.probeId,
402
+ state: resolution.state,
403
+ severity,
404
+ // `ORACLE_DISPOSITIONS`' third member, `'not-attempted'`, on a
405
+ // `null` local `disposition`: no disposition was recorded for
406
+ // this oracle, or the ambiguity guard above fired. Both mean
407
+ // "nothing was recorded", which is the honest reading of the
408
+ // one closed-three member that says so.
409
+ disposition: disposition === null ? 'not-attempted' : disposition.disposition,
410
+ resolvedFrom: resolution.resolvedFrom,
411
+ corroboration: resolution.corroboration,
412
+ selectedObservationIds: [...resolution.selectedObservationIds],
413
+ checkResolution,
414
+ });
415
+ if (firstState === undefined)
416
+ firstState = resolution.state;
417
+ if (firstInvalidatingState === undefined &&
418
+ INVALIDATING_OUTCOME_STATES.has(resolution.state)) {
419
+ firstInvalidatingState = resolution.state;
420
+ }
421
+ // First match only: `Oracle.id` carries no schema- or compile-time
422
+ // uniqueness constraint, so two oracles could share
423
+ // `id === designatedOracleId`. Guarding against overwrite here is
424
+ // the same last-write-wins fix `outcomesByProbeId` (`strength.ts`)
425
+ // already applies, for the identical reason.
426
+ if (designatedOracleId !== null &&
427
+ oracle.id === designatedOracleId &&
428
+ designatedState === undefined) {
429
+ designatedState = resolution.state;
430
+ }
431
+ }
432
+ // One vote per trial, ordinarily: the trial set's own cardinality
433
+ // (`Trials.completed`) is `votes.length`, so a trial still
434
+ // contributes a vote when the probe has no designated oracle (a
435
+ // clean control, a canary, or a malformed defect chain) -- the
436
+ // fallback order is the first invalidating state this trial's
437
+ // oracles produced, else the first oracle's state. Every candidate
438
+ // is a state `resolveOutcome` itself assigned this trial, never a
439
+ // literal this module invents: `resolveOutcome` stays the one
440
+ // assigner of an AD-6 state. `contract.oracles` declaring none at
441
+ // all is the one shape with no state to vote with, and that trial
442
+ // contributes no vote rather than a fabricated one.
443
+ const voteState = designatedState ?? firstInvalidatingState ?? firstState;
444
+ if (voteState !== undefined) {
445
+ votes.push({ state: voteState });
446
+ }
447
+ }
448
+ const reduced = reduceTrialSet(votes, policy.catchThreshold);
449
+ const trialsField = {
450
+ declaredMinimum: policy.minimumTrialCount,
451
+ completed: votes.length,
452
+ invalidatedAttempts: [...reduced.invalidatedAttempts],
453
+ };
454
+ const unwitnessedQuotations = trials.flatMap((trial) => trial.unwitnessedQuotations);
455
+ const isolationViolation = trials.flatMap((trial) => trial.isolationViolation);
456
+ const findings = trials.flatMap((trial) => trial.findings.map((finding) => ({
457
+ findingId: finding.findingId,
458
+ confidence: finding.confidence,
459
+ })));
460
+ const uncitedDefectFindings = trials.flatMap((trial) => uncitedDefectFindingGaps(recordPickOf(trial)));
461
+ // Every finding across every trial citing no oracle (`emit`'s own
462
+ // `uncitedFindings` field): broader than `uncitedDefectFindings` above
463
+ // (every finding type, not `defect` only) and thinner (an identifier
464
+ // only), per `outcome.ts`'s own doc comment on the two functions
465
+ // coexisting rather than one replacing the other.
466
+ const uncitedFindings = trials.flatMap((trial) => uncitedFindingIds(recordPickOf(trial)));
467
+ const coverageGaps = evaluateCoverage(contract);
468
+ const operationIdentifierCollisions = operationIdentifierCollisionsOf(contract, trials);
469
+ const trialSetDisagreements = trialSetDisagreementsOf(trials);
470
+ const evidenceIntegrity = {
471
+ // Declared, not derived: no declared input or caller-supplied
472
+ // parameter carries `EvidenceDisclosure`, `ingest`'s product does not
473
+ // restate it, and the other three siblings are declared `false` for
474
+ // the identical reason (see this module's own header comment).
475
+ disclosure: { truncationBound: null, reportedIncomplete: false },
476
+ overTruncated: false,
477
+ unavailable: false,
478
+ internallyInconsistent: false,
479
+ isolationViolation,
480
+ duplicateRecordIdentifiers: conditionsAcrossTrials(trials, 'duplicate-record-identifier'),
481
+ danglingCitations: conditionsAcrossTrials(trials, 'dangling-citation'),
482
+ danglingDispositionCitations: conditionsAcrossTrials(trials, 'dangling-disposition-citation'),
483
+ forbiddenInputsNotWithheld: conditionsAcrossTrials(trials, 'forbidden-input-not-withheld'),
484
+ crossArtifactDisagreements: conditionsAcrossTrials(trials, 'cross-artifact-disagreement'),
485
+ evaluatorConfigurationAbsent: conditionsAcrossTrials(trials, 'evaluator-configuration-absent'),
486
+ evaluatorConfigurationDigestMismatches: conditionsAcrossTrials(trials, 'evaluator-configuration-digest-mismatch'),
487
+ judgeResultsUnscored: conditionsAcrossTrials(trials, 'judge-result-unscored'),
488
+ operationIdentifierCollisions,
489
+ trialSetDisagreements,
490
+ };
491
+ // Every trial in the set is asserted to agree with the
492
+ // first on `mode` and `evaluatorRecommendation`; a disagreement is
493
+ // `trial-set-field-disagreement` above, never a throw. The first
494
+ // trial's own values build the one assessment TypeScript's
495
+ // discriminated union still requires -- non-silence comes from the
496
+ // Invalid row's basis line, not from withholding a value. A caller
497
+ // supplying zero trials has no first value to read; both fields fall
498
+ // back to a neutral default, and `below-minimum-trial-count` already
499
+ // reports zero completed trials against the declared minimum.
500
+ const firstTrial = trials[0];
501
+ const mode = firstTrial?.mode ?? 'production';
502
+ const evaluatorRecommendation = firstTrial?.evaluatorRecommendation ?? 'PASS';
503
+ // Same posture as `mode`/`evaluatorRecommendation` above: read off the
504
+ // first trial, never derived or defaulted from anything richer. A caller
505
+ // supplying zero trials has no first value to read either; the empty
506
+ // string is the honest "no run identifier was presented" reading, on the
507
+ // same terms `below-minimum-trial-count` already reports the shortfall.
508
+ const runId = firstTrial?.runId ?? '';
509
+ const commonBody = {
510
+ outcomeState: {
511
+ outcomes: allOutcomes,
512
+ unwitnessedQuotations,
513
+ trials: trialsField,
514
+ reExecutionCap: policy.reExecutionCap,
515
+ },
516
+ evidenceIntegrity,
517
+ evaluatorRecommendation,
518
+ coverageGaps,
519
+ uncitedDefectFindings,
520
+ findings,
521
+ confidenceThreshold: policy.confidenceThreshold,
522
+ // Declared, not derived, on the same posture as `disclosure` below:
523
+ // `AD-12`'s remediation chain needs a caller-presented ancestor
524
+ // sequence, and score's five declared inputs carry only the current
525
+ // contract, never a chain. `validateLineageChain([contract], {...})`
526
+ // looks buildable from what score already has, but is not: it treats
527
+ // the one-element array as the WHOLE chain, so any contract with
528
+ // `revisionCount > 0` -- an ordinarily revised one -- fails the root,
529
+ // parent-resolution, and length checks unconditionally, firing the
530
+ // existing `lineage-chain-inconsistent` FAIL row on every such run
531
+ // regardless of whether anything is actually wrong. The vacuously
532
+ // true value below is what "no chain was presented to validate"
533
+ // honestly means.
534
+ remediationState: {
535
+ lengthConsistent: true,
536
+ noRepeatedDigest: true,
537
+ noGap: true,
538
+ },
539
+ preflightPassed: preflightVerdict.passed,
540
+ severityFloor: policy.severityFloor,
541
+ };
542
+ if (mode === 'production') {
543
+ const assessment = {
544
+ mode: 'production',
545
+ ...commonBody,
546
+ };
547
+ return {
548
+ assessment,
549
+ ladder: resolveProductionVerdict(assessment),
550
+ runId,
551
+ contract,
552
+ policy,
553
+ probe,
554
+ sealedProbes,
555
+ trialSetResult: reduced,
556
+ outcomes,
557
+ uncitedFindings,
558
+ };
559
+ }
560
+ const assessment = {
561
+ mode: 'contract-scoring',
562
+ ...commonBody,
563
+ systemRecommendationRecorded: evaluatorRecommendation,
564
+ // No declared input or caller-supplied parameter carries authored
565
+ // prose for this field; `null` is its own legal, honest value rather
566
+ // than an invented note.
567
+ systemRecommendationNote: null,
568
+ };
569
+ return {
570
+ assessment,
571
+ ladder: resolveContractVerdict(assessment),
572
+ runId,
573
+ contract,
574
+ policy,
575
+ probe,
576
+ sealedProbes,
577
+ trialSetResult: reduced,
578
+ outcomes,
579
+ uncitedFindings,
580
+ };
581
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Owed item 2's fix: a step selects observations by `operationId`, ordered by
3
+ * the record's monotonic `sequence` (ADR-006 forbids array position as
4
+ * ordering). `selectObservations` reports a several-match ambiguity as data;
5
+ * no first-match or last-match convention picks a winner. Pure and total:
6
+ * every input produces a defined result, nothing throws, and no AD-6 outcome
7
+ * state is assigned. Assigning outcome state is AD-33's reference decision
8
+ * procedure's job.
9
+ *
10
+ * `resolveTemporalAnchor` is a separate, narrower resolution built on top:
11
+ * picking a concrete point in time for an `any`-cardinality temporal anchor
12
+ * with several matches, by lowest sequence. The policy above still holds
13
+ * here: a single-valued cardinality's several-match ambiguity still comes
14
+ * back unresolved from `resolveTemporalAnchor` too.
15
+ *
16
+ * No stage claims this module yet; `stage-table.ts`'s `score` row stays
17
+ * `module: null` throughout.
18
+ */
19
+ import type { InteractionStep } from '../schemas/plan.ts';
20
+ import type { Observation } from '../schemas/sealed-run-record.ts';
21
+ /** How many observations a step's selector matched. Never a count past two: past one match, only "several" is recorded. */
22
+ export type SelectionCount = 'none' | 'one' | 'several';
23
+ export type StepSelection = {
24
+ readonly result: SelectionCount;
25
+ /**
26
+ * In ascending-`sequence` order, never array order. Empty for `none`, one
27
+ * member for `one`, two or more for `several`.
28
+ */
29
+ readonly matchedObservationIds: readonly Observation['observationId'][];
30
+ };
31
+ /**
32
+ * Matches `step.operationId` against every observation's `operationId`,
33
+ * ordered by `sequence` ascending. Reads no other field of `step`: whether
34
+ * `several` is the named ambiguity condition or a legitimate `any`-cardinality
35
+ * match is a fact about the step's declared cardinality, decided by whoever
36
+ * reads this result, not by this function.
37
+ *
38
+ * Sorts a copy; the input `observations` array is never mutated, and its own
39
+ * order is never read as meaning anything (NFR9: a permutation of the same
40
+ * observations yields byte-identical `matchedObservationIds`). The schema
41
+ * layer enforces per-record `sequence` uniqueness, so a real `SealedRunRecord`
42
+ * never presents a tie. `observationId` is the secondary sort key regardless:
43
+ * this function's own permutation invariance holds even against a hand-built
44
+ * or malformed `observations` array carrying a duplicate or non-finite
45
+ * `sequence`, decided by this comparator alone.
46
+ *
47
+ * Matching on `operationId` alone, ignoring `step.inputBinding`, is a
48
+ * deliberate scope boundary that stays: candidate-tuple resolution lives in
49
+ * `selectWithBindings` (`score/bindings.ts`), which wraps this function and
50
+ * filters its matches against the step's own resolved bindings. Splitting them
51
+ * keeps this function's permutation guarantee provable on its own.
52
+ *
53
+ * `tests/seal/fixtures.ts`'s `irreducibleCollisionPair` was once cited here as
54
+ * a case "distinguishable only by input binding". It is not: its two steps
55
+ * bind nothing in any channel, so a filter over zero bindings separates
56
+ * nothing and both stay `several` even with `selectWithBindings`. The pair
57
+ * that separates is `literalCollisionPair`, whose two steps bind one key to
58
+ * two different literals.
59
+ */
60
+ export declare function selectObservations(step: InteractionStep, observations: readonly Observation[]): StepSelection;
61
+ /**
62
+ * Resolves an `after` temporal clause to the single observation it denotes,
63
+ * built directly from `selectObservations`'s own result over the anchor.
64
+ *
65
+ * Takes the anchor step itself, already resolved from the dependent step's
66
+ * `after` identifier (a one-line lookup against whichever declared plan the
67
+ * caller holds). A step whose clause is `null`, or whose clause names a step
68
+ * the plan does not declare (AD-39's permissive dangling reference), names no
69
+ * anchor and never reaches this function.
70
+ */
71
+ export declare function resolveTemporalAnchor(anchorStep: InteractionStep, observations: readonly Observation[]): TemporalAnchorResolution;
72
+ export type TemporalAnchorResolution = {
73
+ readonly resolved: true;
74
+ readonly observationId: Observation['observationId'];
75
+ readonly matchedObservationIds: readonly Observation['observationId'][];
76
+ } | {
77
+ readonly resolved: false;
78
+ readonly result: Extract<SelectionCount, 'none' | 'several'>;
79
+ readonly matchedObservationIds: readonly Observation['observationId'][];
80
+ };