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.
- package/README.md +142 -67
- package/corpus/dev/README.md +17 -11
- package/corpus/dev/compile-seal-example/brief.json +1 -1
- package/corpus/dev/compile-seal-example/contract.json +1 -1
- package/corpus/dev/contracts/absent-collection-locations.json +1 -1
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
- package/corpus/dev/contracts/absent-success-indicator.json +1 -1
- package/corpus/dev/contracts/empty-channel-roles.json +1 -1
- package/corpus/dev/contracts/empty-collection-locations.json +1 -1
- package/corpus/dev/contracts/empty-request-shapes.json +1 -1
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
- package/corpus/dev/contracts/no-operation-inventory.json +1 -1
- package/corpus/dev/contracts/no-read-back-relation.json +1 -1
- package/corpus/dev/contracts/no-state-change-marker.json +1 -1
- package/corpus/dev/contracts/no-type-violating-step.json +1 -1
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/satisfied-declarations.json +1 -1
- package/corpus/dev/contracts/single-required-response-key.json +1 -1
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
- package/corpus/dev/index.json +1 -1
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +1 -0
- package/dist/application/score.d.ts +48 -0
- package/dist/application/score.js +186 -0
- package/dist/cli/arguments.d.ts +7 -3
- package/dist/cli/arguments.js +73 -5
- package/dist/cli/exit-codes.d.ts +6 -2
- package/dist/cli/exit-codes.js +8 -9
- package/dist/cli/main.js +9 -0
- package/dist/cli/render.d.ts +1 -1
- package/dist/cli/render.js +6 -4
- package/dist/cli/run.d.ts +13 -3
- package/dist/cli/run.js +136 -16
- package/dist/core/canonical/canonicalize.d.ts +12 -0
- package/dist/core/canonical/canonicalize.js +18 -4
- package/dist/core/compile/bindings.d.ts +70 -0
- package/dist/core/compile/bindings.js +317 -0
- package/dist/core/compile/compile.d.ts +5 -0
- package/dist/core/compile/compile.js +9 -0
- package/dist/core/compile/expression-legality.d.ts +49 -0
- package/dist/core/compile/expression-legality.js +105 -31
- package/dist/core/compile/forbidden-inputs.d.ts +8 -1
- package/dist/core/compile/forbidden-inputs.js +16 -3
- package/dist/core/compile/interface-inventory.d.ts +23 -1
- package/dist/core/compile/interface-inventory.js +32 -3
- package/dist/core/compile/reachability.d.ts +18 -1
- package/dist/core/compile/reachability.js +46 -1
- package/dist/core/compile/sensitivity-witness.d.ts +1 -1
- package/dist/core/compile/sensitivity-witness.js +1 -1
- package/dist/core/emit/emit.d.ts +9 -0
- package/dist/core/emit/emit.js +214 -0
- package/dist/core/emit/private-artifact-digest.d.ts +11 -0
- package/dist/core/emit/private-artifact-digest.js +31 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
- package/dist/core/evaluate/evidence-resolution.js +4 -1
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +4 -2
- package/dist/core/ingest/conditions.d.ts +201 -0
- package/dist/core/ingest/conditions.js +48 -0
- package/dist/core/ingest/index.d.ts +13 -0
- package/dist/core/ingest/index.js +11 -0
- package/dist/core/ingest/ingest.d.ts +68 -0
- package/dist/core/ingest/ingest.js +310 -0
- package/dist/core/lineage/stage-table.d.ts +21 -4
- package/dist/core/lineage/stage-table.js +35 -3
- package/dist/core/preflight/witness-evidence.js +10 -0
- package/dist/core/schemas/artifact.d.ts +385 -14
- package/dist/core/schemas/constraint-ledger.js +40 -0
- package/dist/core/schemas/defect-signature.d.ts +273 -0
- package/dist/core/schemas/defect-signature.js +127 -0
- package/dist/core/schemas/eval-contract.d.ts +33 -0
- package/dist/core/schemas/eval-contract.js +14 -0
- package/dist/core/schemas/evidence-artifact.d.ts +99 -19
- package/dist/core/schemas/evidence-artifact.js +64 -8
- package/dist/core/schemas/plan.d.ts +112 -0
- package/dist/core/schemas/plan.js +67 -2
- package/dist/core/schemas/probe-qualification.d.ts +168 -0
- package/dist/core/schemas/probe-qualification.js +106 -0
- package/dist/core/schemas/probe.d.ts +312 -0
- package/dist/core/schemas/probe.js +6 -2
- package/dist/core/schemas/scoring-policy.d.ts +1 -0
- package/dist/core/schemas/scoring-policy.js +6 -1
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
- package/dist/core/schemas/sealed-run-record.d.ts +30 -3
- package/dist/core/schemas/sealed-run-record.js +37 -5
- package/dist/core/score/binding-order.d.ts +31 -0
- package/dist/core/score/binding-order.js +88 -0
- package/dist/core/score/bindings.d.ts +93 -0
- package/dist/core/score/bindings.js +297 -0
- package/dist/core/score/ladder-table.d.ts +23 -0
- package/dist/core/score/ladder-table.js +109 -0
- package/dist/core/score/ladder.d.ts +229 -0
- package/dist/core/score/ladder.js +464 -0
- package/dist/core/score/mode-agreement.d.ts +30 -0
- package/dist/core/score/mode-agreement.js +16 -0
- package/dist/core/score/outcome-table.d.ts +17 -0
- package/dist/core/score/outcome-table.js +172 -0
- package/dist/core/score/outcome.d.ts +466 -0
- package/dist/core/score/outcome.js +562 -0
- package/dist/core/score/qualification.d.ts +83 -0
- package/dist/core/score/qualification.js +615 -0
- package/dist/core/score/quotation.d.ts +79 -0
- package/dist/core/score/quotation.js +201 -0
- package/dist/core/score/reduce-trials.d.ts +80 -0
- package/dist/core/score/reduce-trials.js +90 -0
- package/dist/core/score/score.d.ts +72 -0
- package/dist/core/score/score.js +581 -0
- package/dist/core/score/selection.d.ts +80 -0
- package/dist/core/score/selection.js +98 -0
- package/dist/core/score/strength.d.ts +49 -0
- package/dist/core/score/strength.js +193 -0
- package/dist/core/score/witness.d.ts +117 -0
- package/dist/core/score/witness.js +315 -0
- package/dist/core/seal/derived-reference.d.ts +1 -1
- package/dist/core/seal/derived-reference.js +236 -28
- package/dist/core/seal/seal.js +8 -3
- package/dist/core/stage-contracts.d.ts +62 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/package.json +12 -3
- package/schemas/eval-contract.schema.json +100 -2
- package/schemas/evidence-artifact.schema.json +96 -9
- package/schemas/probe.schema.json +508 -3
- package/schemas/scoring-policy.schema.json +8 -1
- package/schemas/sealed-evaluator-brief.schema.json +11 -1
- 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
|
+
};
|