eval-quality 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +355 -0
- package/corpus/dev/README.md +36 -0
- package/corpus/dev/compile-seal-example/brief.json +1 -0
- package/corpus/dev/compile-seal-example/contract.json +1 -0
- package/corpus/dev/contracts/absent-collection-locations.json +1 -0
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
- package/corpus/dev/contracts/absent-success-indicator.json +1 -0
- package/corpus/dev/contracts/empty-channel-roles.json +1 -0
- package/corpus/dev/contracts/empty-collection-locations.json +1 -0
- package/corpus/dev/contracts/empty-request-shapes.json +1 -0
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
- package/corpus/dev/contracts/no-operation-inventory.json +1 -0
- package/corpus/dev/contracts/no-read-back-relation.json +1 -0
- package/corpus/dev/contracts/no-state-change-marker.json +1 -0
- package/corpus/dev/contracts/no-type-violating-step.json +1 -0
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
- package/corpus/dev/contracts/satisfied-declarations.json +1 -0
- package/corpus/dev/contracts/single-required-response-key.json +1 -0
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
- package/corpus/dev/index.json +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/local-corpus-adapter.d.ts +7 -0
- package/dist/adapters/local-corpus-adapter.js +78 -0
- package/dist/adapters/node-file-system-adapter.d.ts +7 -0
- package/dist/adapters/node-file-system-adapter.js +39 -0
- package/dist/adapters/port-boundary.d.ts +18 -0
- package/dist/adapters/port-boundary.js +83 -0
- package/dist/adapters/system-clock-adapter.d.ts +10 -0
- package/dist/adapters/system-clock-adapter.js +18 -0
- package/dist/application/compile.d.ts +4 -0
- package/dist/application/compile.js +24 -0
- package/dist/application/diagnostics.d.ts +22 -0
- package/dist/application/diagnostics.js +18 -0
- package/dist/application/index.d.ts +23 -0
- package/dist/application/index.js +17 -0
- package/dist/application/invoke-port.d.ts +2 -0
- package/dist/application/invoke-port.js +45 -0
- package/dist/application/preflight.d.ts +27 -0
- package/dist/application/preflight.js +131 -0
- package/dist/application/seal.d.ts +4 -0
- package/dist/application/seal.js +28 -0
- package/dist/application/serialize.d.ts +6 -0
- package/dist/application/serialize.js +9 -0
- package/dist/cli/arguments.d.ts +28 -0
- package/dist/cli/arguments.js +167 -0
- package/dist/cli/exit-codes.d.ts +36 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +143 -0
- package/dist/cli/render.d.ts +26 -0
- package/dist/cli/render.js +53 -0
- package/dist/cli/run.d.ts +41 -0
- package/dist/cli/run.js +203 -0
- package/dist/core/canonical/canonicalize.d.ts +1 -0
- package/dist/core/canonical/canonicalize.js +113 -0
- package/dist/core/canonical/digest.d.ts +7 -0
- package/dist/core/canonical/digest.js +81 -0
- package/dist/core/canonical/scan-json.d.ts +1 -0
- package/dist/core/canonical/scan-json.js +298 -0
- package/dist/core/canonical/value-domain.d.ts +4 -0
- package/dist/core/canonical/value-domain.js +113 -0
- package/dist/core/compile/compile.d.ts +24 -0
- package/dist/core/compile/compile.js +67 -0
- package/dist/core/compile/declarations.d.ts +5 -0
- package/dist/core/compile/declarations.js +22 -0
- package/dist/core/compile/expression-legality.d.ts +14 -0
- package/dist/core/compile/expression-legality.js +346 -0
- package/dist/core/compile/forbidden-inputs.d.ts +5 -0
- package/dist/core/compile/forbidden-inputs.js +19 -0
- package/dist/core/compile/interface-inventory.d.ts +10 -0
- package/dist/core/compile/interface-inventory.js +55 -0
- package/dist/core/compile/oracle-alignment.d.ts +14 -0
- package/dist/core/compile/oracle-alignment.js +93 -0
- package/dist/core/compile/reachability.d.ts +17 -0
- package/dist/core/compile/reachability.js +177 -0
- package/dist/core/compile/rubrics.d.ts +55 -0
- package/dist/core/compile/rubrics.js +198 -0
- package/dist/core/compile/scripting-bound.d.ts +5 -0
- package/dist/core/compile/scripting-bound.js +151 -0
- package/dist/core/compile/sensitivity-witness.d.ts +73 -0
- package/dist/core/compile/sensitivity-witness.js +232 -0
- package/dist/core/compile/waivers.d.ts +3 -0
- package/dist/core/compile/waivers.js +21 -0
- package/dist/core/coverage/coverage.d.ts +18 -0
- package/dist/core/coverage/coverage.js +47 -0
- package/dist/core/coverage/relevance.d.ts +75 -0
- package/dist/core/coverage/relevance.js +186 -0
- package/dist/core/coverage/rules.d.ts +7 -0
- package/dist/core/coverage/rules.js +18 -0
- package/dist/core/coverage/satisfaction.d.ts +107 -0
- package/dist/core/coverage/satisfaction.js +415 -0
- package/dist/core/coverage/table.d.ts +26 -0
- package/dist/core/coverage/table.js +222 -0
- package/dist/core/declared-inputs.d.ts +16 -0
- package/dist/core/declared-inputs.js +18 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
- package/dist/core/evaluate/evidence-resolution.js +153 -0
- package/dist/core/evaluate/operators.d.ts +87 -0
- package/dist/core/evaluate/operators.js +350 -0
- package/dist/core/evaluate/resolution.d.ts +40 -0
- package/dist/core/evaluate/resolution.js +336 -0
- package/dist/core/evaluate/resolved-value.d.ts +9 -0
- package/dist/core/evaluate/resolved-value.js +6 -0
- package/dist/core/failure-codes.d.ts +18 -0
- package/dist/core/failure-codes.js +50 -0
- package/dist/core/lineage/chain.d.ts +49 -0
- package/dist/core/lineage/chain.js +210 -0
- package/dist/core/lineage/freeze.d.ts +12 -0
- package/dist/core/lineage/freeze.js +35 -0
- package/dist/core/lineage/stage-table.d.ts +43 -0
- package/dist/core/lineage/stage-table.js +106 -0
- package/dist/core/preflight/plan.d.ts +67 -0
- package/dist/core/preflight/plan.js +313 -0
- package/dist/core/preflight/projection.d.ts +40 -0
- package/dist/core/preflight/projection.js +96 -0
- package/dist/core/preflight/reduce.d.ts +8 -0
- package/dist/core/preflight/reduce.js +180 -0
- package/dist/core/preflight/witness-evidence.d.ts +42 -0
- package/dist/core/preflight/witness-evidence.js +88 -0
- package/dist/core/probe/target-policy.d.ts +63 -0
- package/dist/core/probe/target-policy.js +328 -0
- package/dist/core/schemas/artifact-reference.d.ts +19 -0
- package/dist/core/schemas/artifact-reference.js +30 -0
- package/dist/core/schemas/artifact.d.ts +1153 -0
- package/dist/core/schemas/artifact.js +84 -0
- package/dist/core/schemas/constraint-ledger.d.ts +57 -0
- package/dist/core/schemas/constraint-ledger.js +143 -0
- package/dist/core/schemas/eval-contract.d.ts +402 -0
- package/dist/core/schemas/eval-contract.js +147 -0
- package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
- package/dist/core/schemas/evaluator-configuration.js +43 -0
- package/dist/core/schemas/evidence-artifact.d.ts +471 -0
- package/dist/core/schemas/evidence-artifact.js +232 -0
- package/dist/core/schemas/expression.d.ts +197 -0
- package/dist/core/schemas/expression.js +284 -0
- package/dist/core/schemas/faults.d.ts +9 -0
- package/dist/core/schemas/faults.js +32 -0
- package/dist/core/schemas/interface.d.ts +417 -0
- package/dist/core/schemas/interface.js +115 -0
- package/dist/core/schemas/isolation-manifest.d.ts +136 -0
- package/dist/core/schemas/isolation-manifest.js +92 -0
- package/dist/core/schemas/lineage.d.ts +21 -0
- package/dist/core/schemas/lineage.js +28 -0
- package/dist/core/schemas/oracle.d.ts +72 -0
- package/dist/core/schemas/oracle.js +35 -0
- package/dist/core/schemas/plan.d.ts +115 -0
- package/dist/core/schemas/plan.js +58 -0
- package/dist/core/schemas/pointer.d.ts +63 -0
- package/dist/core/schemas/pointer.js +94 -0
- package/dist/core/schemas/port-messages.d.ts +103 -0
- package/dist/core/schemas/port-messages.js +90 -0
- package/dist/core/schemas/preflight-verdict.d.ts +60 -0
- package/dist/core/schemas/preflight-verdict.js +38 -0
- package/dist/core/schemas/primitives.d.ts +78 -0
- package/dist/core/schemas/primitives.js +121 -0
- package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
- package/dist/core/schemas/private-artifact-manifest.js +46 -0
- package/dist/core/schemas/probe-body.d.ts +25 -0
- package/dist/core/schemas/probe-body.js +23 -0
- package/dist/core/schemas/probe-policy.d.ts +76 -0
- package/dist/core/schemas/probe-policy.js +33 -0
- package/dist/core/schemas/probe.d.ts +189 -0
- package/dist/core/schemas/probe.js +82 -0
- package/dist/core/schemas/publish.d.ts +30 -0
- package/dist/core/schemas/publish.js +135 -0
- package/dist/core/schemas/reference-set.d.ts +18 -0
- package/dist/core/schemas/reference-set.js +27 -0
- package/dist/core/schemas/rubric.d.ts +62 -0
- package/dist/core/schemas/rubric.js +68 -0
- package/dist/core/schemas/scoring-policy.d.ts +28 -0
- package/dist/core/schemas/scoring-policy.js +51 -0
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
- package/dist/core/schemas/sealed-run-record.d.ts +382 -0
- package/dist/core/schemas/sealed-run-record.js +215 -0
- package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
- package/dist/core/schemas/sensitivity-witness.js +86 -0
- package/dist/core/schemas/verdict.d.ts +25 -0
- package/dist/core/schemas/verdict.js +20 -0
- package/dist/core/schemas/waiver.d.ts +17 -0
- package/dist/core/schemas/waiver.js +24 -0
- package/dist/core/seal/derived-reference.d.ts +36 -0
- package/dist/core/seal/derived-reference.js +383 -0
- package/dist/core/seal/direction-prose.d.ts +13 -0
- package/dist/core/seal/direction-prose.js +120 -0
- package/dist/core/seal/plan-index.d.ts +63 -0
- package/dist/core/seal/plan-index.js +155 -0
- package/dist/core/seal/scripting-audit.d.ts +15 -0
- package/dist/core/seal/scripting-audit.js +51 -0
- package/dist/core/seal/seal.d.ts +18 -0
- package/dist/core/seal/seal.js +155 -0
- package/dist/core/stage-contracts.d.ts +30 -0
- package/dist/core/stage-contracts.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +22 -0
- package/dist/ports/clock-port.d.ts +13 -0
- package/dist/ports/clock-port.js +7 -0
- package/dist/ports/corpus-port.d.ts +16 -0
- package/dist/ports/corpus-port.js +7 -0
- package/dist/ports/environment-probe-port.d.ts +76 -0
- package/dist/ports/environment-probe-port.js +32 -0
- package/dist/ports/file-system-port.d.ts +27 -0
- package/dist/ports/file-system-port.js +11 -0
- package/dist/ports/port.d.ts +33 -0
- package/dist/ports/port.js +14 -0
- package/dist/testing/conformance.d.ts +88 -0
- package/dist/testing/conformance.js +281 -0
- package/dist/testing/index.d.ts +26 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/probe-conformance.d.ts +45 -0
- package/dist/testing/probe-conformance.js +186 -0
- package/package.json +130 -0
- package/schemas/artifact-reference.schema.json +64 -0
- package/schemas/eval-contract.schema.json +2145 -0
- package/schemas/evaluator-configuration.schema.json +199 -0
- package/schemas/evidence-artifact.schema.json +1229 -0
- package/schemas/isolation-manifest.schema.json +394 -0
- package/schemas/preflight-verdict.schema.json +114 -0
- package/schemas/private-artifact-manifest.schema.json +103 -0
- package/schemas/probe.schema.json +1151 -0
- package/schemas/rubric.schema.json +148 -0
- package/schemas/scoring-policy.schema.json +90 -0
- package/schemas/sealed-evaluator-brief.schema.json +268 -0
- package/schemas/sealed-run-record.schema.json +875 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-34's planning half for AD-10's pre-flight: the contract and its probes in,
|
|
3
|
+
* a description of every request the environment-probe port must issue out.
|
|
4
|
+
* Pure and synchronous; nothing here awaits, reads a clock, or observes.
|
|
5
|
+
*
|
|
6
|
+
* The plan carries everything the reducer reads, because `ReduceStage` takes a
|
|
7
|
+
* plan and observations and nothing else. That is why a `PlannedCheck` embeds
|
|
8
|
+
* the operation and the witness themselves: there is nothing to look an
|
|
9
|
+
* identifier up in at reduce time.
|
|
10
|
+
*/
|
|
11
|
+
import { checkInputsAgainstShape } from '../compile/sensitivity-witness.js';
|
|
12
|
+
import { declaresNoRequiredKeys } from '../declared-inputs.js';
|
|
13
|
+
import { StructuralFailure } from '../failure-codes.js';
|
|
14
|
+
import { referenceSetMembers } from './witness-evidence.js';
|
|
15
|
+
const requestOf = (legId, interfaceId, operation, inputs) => ({
|
|
16
|
+
// NFR9's correlation by identifier: the port echoes this back, and it is
|
|
17
|
+
// the only thing that tells two legs of one operation apart.
|
|
18
|
+
probeId: legId,
|
|
19
|
+
interfaceId,
|
|
20
|
+
operationId: operation.operationId,
|
|
21
|
+
method: operation.method,
|
|
22
|
+
pathTemplate: operation.pathTemplate,
|
|
23
|
+
channels: {
|
|
24
|
+
path: inputs.path,
|
|
25
|
+
query: inputs.query,
|
|
26
|
+
header: inputs.header,
|
|
27
|
+
body: inputs.body,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* The contract declares no identifier for a control leg, so one is minted, and it
|
|
32
|
+
* has to avoid every identifier the contract already spends. The same input
|
|
33
|
+
* always produces the same suffix, which is what fixture 104 asserts.
|
|
34
|
+
*/
|
|
35
|
+
const mintLegId = (base, taken) => {
|
|
36
|
+
if (!taken.has(base))
|
|
37
|
+
return base;
|
|
38
|
+
for (let ordinal = 2;; ordinal++) {
|
|
39
|
+
const candidate = `${base}-${ordinal}`;
|
|
40
|
+
if (!taken.has(candidate))
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Every identifier a minted control leg has to avoid. The probes' manifestation
|
|
46
|
+
* witnesses are in the set even though they are not part of the contract: a
|
|
47
|
+
* minted id colliding with one is a collision the minting caused, and the
|
|
48
|
+
* duplicate scan below would report it against the probe's leg instead.
|
|
49
|
+
*/
|
|
50
|
+
const declaredLegIds = (contract, probes) => {
|
|
51
|
+
const taken = new Set(contract.interactionPlan.map((step) => step.stepId));
|
|
52
|
+
for (const probe of probes)
|
|
53
|
+
if (!probe.expectedClean)
|
|
54
|
+
for (const defect of probe.defects) {
|
|
55
|
+
const legId = defect.manifestationWitness?.legId;
|
|
56
|
+
if (legId !== undefined)
|
|
57
|
+
taken.add(legId);
|
|
58
|
+
}
|
|
59
|
+
for (const iface of contract.permittedInterfaces)
|
|
60
|
+
for (const operation of iface.operations)
|
|
61
|
+
for (const leg of operation.sensitivityWitness?.legs ?? [])
|
|
62
|
+
taken.add(leg.legId);
|
|
63
|
+
if (contract.fixtureReset !== null)
|
|
64
|
+
taken.add(contract.fixtureReset.legId);
|
|
65
|
+
return taken;
|
|
66
|
+
};
|
|
67
|
+
const EMPTY_INPUTS = {
|
|
68
|
+
path: {},
|
|
69
|
+
query: {},
|
|
70
|
+
header: {},
|
|
71
|
+
body: { kind: 'absent' },
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The inputs a control leg sends. A witness supplies them when the operation has
|
|
75
|
+
* one; an operation AD-10 exempts has no required key to fill, so empty inputs
|
|
76
|
+
* are a legal request. Returns `null` only when required keys exist that nothing
|
|
77
|
+
* here can supply.
|
|
78
|
+
*
|
|
79
|
+
* Reading a witness alone left the immutability branch unreachable for the
|
|
80
|
+
* contract that most needs it: a read-only contract whose only safe read is a
|
|
81
|
+
* parameterless GET is exempt, so it carried no witness, so no control leg was
|
|
82
|
+
* planned and the verdict passed with no immutability evidence at all.
|
|
83
|
+
*/
|
|
84
|
+
const controlInputs = (operation) => {
|
|
85
|
+
const declared = operation.sensitivityWitness?.legs[0]?.inputs;
|
|
86
|
+
if (declared !== undefined)
|
|
87
|
+
return declared;
|
|
88
|
+
return declaresNoRequiredKeys(operation) ? EMPTY_INPUTS : null;
|
|
89
|
+
};
|
|
90
|
+
const targetOf = (iface, operation) => {
|
|
91
|
+
const inputs = controlInputs(operation);
|
|
92
|
+
return inputs === null ? null : { iface, operation, inputs };
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* AD-10 rule 3's selection, by declaration order so a fixture catches a later
|
|
96
|
+
* change to the rule. The observed operation is the first safe read anywhere in
|
|
97
|
+
* the contract that can be given inputs; the mutating one is the first
|
|
98
|
+
* marker-true operation on the interface the fixture reset names, resolved
|
|
99
|
+
* independently of where the observed read came from.
|
|
100
|
+
*/
|
|
101
|
+
const selectControl = (contract) => {
|
|
102
|
+
let observed = null;
|
|
103
|
+
for (const iface of contract.permittedInterfaces) {
|
|
104
|
+
for (const operation of iface.operations) {
|
|
105
|
+
if (operation.stateChangeMarker)
|
|
106
|
+
continue;
|
|
107
|
+
observed = targetOf(iface, operation);
|
|
108
|
+
if (observed !== null)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (observed !== null)
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (observed === null)
|
|
115
|
+
return null;
|
|
116
|
+
const reset = contract.fixtureReset;
|
|
117
|
+
if (reset === null)
|
|
118
|
+
return { observed, mutating: null };
|
|
119
|
+
const resetInterface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
|
|
120
|
+
let mutating = null;
|
|
121
|
+
for (const operation of resetInterface?.operations ?? []) {
|
|
122
|
+
if (!operation.stateChangeMarker)
|
|
123
|
+
continue;
|
|
124
|
+
mutating = targetOf(resetInterface, operation);
|
|
125
|
+
if (mutating !== null)
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
return { observed, mutating };
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* AD-10's plan, derived from the interfaces the contract's probes exercise.
|
|
132
|
+
*
|
|
133
|
+
* Three constructions worth knowing. An `interface-present` check is emitted
|
|
134
|
+
* only for an operation that has at least one leg, because a check over no legs
|
|
135
|
+
* is satisfied vacuously and asserts nothing. The `state-reset` and
|
|
136
|
+
* `clean-control` checks are emitted only when control legs could be planned at
|
|
137
|
+
* all, for the same reason. And the fixture reset's own leg is one more leg
|
|
138
|
+
* through the same port, so its identifier shares the namespace every other leg
|
|
139
|
+
* identifier sits in.
|
|
140
|
+
*/
|
|
141
|
+
export const planPreflight = (input) => {
|
|
142
|
+
const { contract, probes, runId } = input;
|
|
143
|
+
for (const iface of contract.permittedInterfaces) {
|
|
144
|
+
// Already thrown at compile; asserted again because the plan is reachable
|
|
145
|
+
// from a caller who assembled a contract by hand.
|
|
146
|
+
if (iface.kind !== 'api') {
|
|
147
|
+
throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported in v0; only "api" is (AD-10)`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const origins = [];
|
|
151
|
+
const checks = [];
|
|
152
|
+
// Keyed by interface AND operation: `Operation.operationId` is scoped to a
|
|
153
|
+
// `PermittedInterface`, so two interfaces may legally declare the same one
|
|
154
|
+
// (its own schema description says so, and `duplicate-operation-signature`
|
|
155
|
+
// covers method plus path template only). Keyed by operation id alone, one
|
|
156
|
+
// interface's legs become another's clean legs.
|
|
157
|
+
const legIdsByOperation = new Map();
|
|
158
|
+
const scopeKey = (interfaceId, operationId) => `${interfaceId}\u0000${operationId}`;
|
|
159
|
+
const addLeg = (legId, purpose, interfaceId, operation, inputs, artifactPath) => {
|
|
160
|
+
origins.push({
|
|
161
|
+
leg: {
|
|
162
|
+
legId,
|
|
163
|
+
purpose,
|
|
164
|
+
request: requestOf(legId, interfaceId, operation, inputs),
|
|
165
|
+
operation,
|
|
166
|
+
inputs,
|
|
167
|
+
},
|
|
168
|
+
artifactPath,
|
|
169
|
+
});
|
|
170
|
+
const key = scopeKey(interfaceId, operation.operationId);
|
|
171
|
+
const group = legIdsByOperation.get(key);
|
|
172
|
+
if (group === undefined)
|
|
173
|
+
legIdsByOperation.set(key, [legId]);
|
|
174
|
+
else
|
|
175
|
+
group.push(legId);
|
|
176
|
+
};
|
|
177
|
+
// 1. the sensitivity legs and their checks
|
|
178
|
+
contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
|
|
179
|
+
iface.operations.forEach((operation, operationIndex) => {
|
|
180
|
+
const witness = operation.sensitivityWitness;
|
|
181
|
+
const path = `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
|
|
182
|
+
if (witness !== null)
|
|
183
|
+
witness.legs.forEach((leg, legIndex) => {
|
|
184
|
+
addLeg(leg.legId, 'sensitivity', iface.logicalId, operation, leg.inputs, `${path}.sensitivityWitness.legs[${legIndex}].legId`);
|
|
185
|
+
});
|
|
186
|
+
checks.push({
|
|
187
|
+
kind: 'input-sensitivity',
|
|
188
|
+
interfaceId: iface.logicalId,
|
|
189
|
+
operationId: operation.operationId,
|
|
190
|
+
witness,
|
|
191
|
+
operation,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// 3. the control legs, once per contract
|
|
196
|
+
const control = selectControl(contract);
|
|
197
|
+
const controlLegIds = [];
|
|
198
|
+
if (control !== null) {
|
|
199
|
+
const taken = declaredLegIds(contract, probes);
|
|
200
|
+
const mintControl = (base) => {
|
|
201
|
+
const legId = mintLegId(base, taken);
|
|
202
|
+
taken.add(legId);
|
|
203
|
+
return legId;
|
|
204
|
+
};
|
|
205
|
+
const { observed } = control;
|
|
206
|
+
const observe = (legId) => {
|
|
207
|
+
addLeg(legId, 'control-observe', observed.iface.logicalId, observed.operation, observed.inputs, `the minted control-observe leg against "${observed.operation.operationId}"`);
|
|
208
|
+
controlLegIds.push(legId);
|
|
209
|
+
};
|
|
210
|
+
const first = mintControl('preflight-control-observe');
|
|
211
|
+
observe(first);
|
|
212
|
+
const reset = contract.fixtureReset;
|
|
213
|
+
if (control.mutating !== null && reset !== null) {
|
|
214
|
+
const { mutating } = control;
|
|
215
|
+
const mutate = mintControl('preflight-control-mutate');
|
|
216
|
+
addLeg(mutate, 'control-mutate', mutating.iface.logicalId, mutating.operation, mutating.inputs, `the minted control-mutate leg against "${mutating.operation.operationId}"`);
|
|
217
|
+
controlLegIds.push(mutate);
|
|
218
|
+
const resetOperation = mutating.iface.operations.find((candidate) => candidate.operationId === reset.operationId);
|
|
219
|
+
if (resetOperation === undefined) {
|
|
220
|
+
throw new StructuralFailure('unreachable-check-evidence', 'EvalContract.fixtureReset', `the fixture reset names operation "${reset.operationId}" on interface "${reset.interfaceId}", which the contract does not declare (AD-10)`);
|
|
221
|
+
}
|
|
222
|
+
addLeg(reset.legId, 'control-reset', reset.interfaceId, resetOperation, reset.inputs, 'EvalContract.fixtureReset.legId');
|
|
223
|
+
controlLegIds.push(reset.legId);
|
|
224
|
+
}
|
|
225
|
+
const last = mintControl('preflight-control-observe');
|
|
226
|
+
observe(last);
|
|
227
|
+
checks.push({ kind: 'state-reset', legIds: [first, last] });
|
|
228
|
+
// AD-10's own worked example is two distinct nonexistent identifiers both
|
|
229
|
+
// returning 404, so a `clean-control` reading sensitivity legs would fail
|
|
230
|
+
// on a contract that followed the architecture verbatim.
|
|
231
|
+
checks.push({ kind: 'clean-control', legIds: [...controlLegIds] });
|
|
232
|
+
}
|
|
233
|
+
// 5. one leg and two checks per seeded defect
|
|
234
|
+
for (const probe of probes) {
|
|
235
|
+
if (probe.expectedClean)
|
|
236
|
+
continue;
|
|
237
|
+
for (const defect of probe.defects) {
|
|
238
|
+
const witness = defect.manifestationWitness;
|
|
239
|
+
const path = `Probe[probeId=${probe.probeId}].defects[defectId=${defect.defectId}].manifestationWitness`;
|
|
240
|
+
if (witness === null) {
|
|
241
|
+
checks.push({
|
|
242
|
+
kind: 'seeded-fault-fired',
|
|
243
|
+
defectId: defect.defectId,
|
|
244
|
+
witness: null,
|
|
245
|
+
operation: null,
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === witness.interfaceId);
|
|
250
|
+
const operation = iface?.operations.find((candidate) => candidate.operationId === witness.operationId);
|
|
251
|
+
if (operation === undefined) {
|
|
252
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `the manifestation witness names operation "${witness.operationId}" on interface "${witness.interfaceId}", which the contract does not declare (AD-10)`);
|
|
253
|
+
}
|
|
254
|
+
// A manifestation witness lives on the probe, so the compiler never sees
|
|
255
|
+
// its inputs. They reach the port all the same, so they are checked here.
|
|
256
|
+
checkInputsAgainstShape(witness.inputs, operation, `the manifestation witness of ${defect.defectId}`, `${path}.inputs`);
|
|
257
|
+
// Read before the fault leg joins the group, which keeps the fault leg out
|
|
258
|
+
// of its own clean-leg set.
|
|
259
|
+
const cleanLegIds = [
|
|
260
|
+
...(legIdsByOperation.get(scopeKey(witness.interfaceId, operation.operationId)) ?? []),
|
|
261
|
+
];
|
|
262
|
+
addLeg(witness.legId, 'seeded-fault', witness.interfaceId, operation, witness.inputs, `${path}.legId`);
|
|
263
|
+
checks.push({
|
|
264
|
+
kind: 'seeded-faults-scoped',
|
|
265
|
+
defectId: defect.defectId,
|
|
266
|
+
witness,
|
|
267
|
+
operation,
|
|
268
|
+
cleanLegIds,
|
|
269
|
+
});
|
|
270
|
+
checks.push({
|
|
271
|
+
kind: 'seeded-fault-fired',
|
|
272
|
+
defectId: defect.defectId,
|
|
273
|
+
witness,
|
|
274
|
+
operation,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Two legs sharing an identifier would share a `probeId`, and the reducer
|
|
279
|
+
// indexes observations by it, so one leg would answer for the other. The
|
|
280
|
+
// contract side is already checked at compile; a manifestation witness is
|
|
281
|
+
// not, since `Probe` is not part of the contract.
|
|
282
|
+
const seen = new Set();
|
|
283
|
+
for (const origin of origins) {
|
|
284
|
+
if (seen.has(origin.leg.legId)) {
|
|
285
|
+
throw new StructuralFailure('malformed-operator-expression', origin.artifactPath, `leg id "${origin.leg.legId}" is already claimed by another planned leg; two legs sharing a probeId cannot be told apart (AD-10, NFR9)`);
|
|
286
|
+
}
|
|
287
|
+
seen.add(origin.leg.legId);
|
|
288
|
+
}
|
|
289
|
+
// AD-11 makes the fixture digest a required field of the verdict, and a digest
|
|
290
|
+
// over no observation would certify a pre-flight that verified nothing. A
|
|
291
|
+
// contract that offers pre-flight nothing to probe is refused here, where the
|
|
292
|
+
// contract is in hand and the failure can carry a code.
|
|
293
|
+
if (origins.length === 0) {
|
|
294
|
+
throw new StructuralFailure('unreachable-check-evidence', 'EvalContract.permittedInterfaces', 'the contract declares no operation pre-flight can probe, so there is no fixture to verify or digest (AD-10, AD-11)');
|
|
295
|
+
}
|
|
296
|
+
// 2. one interface-present check per operation that has a leg, in
|
|
297
|
+
// declaration order, ahead of the rest
|
|
298
|
+
const presence = [...legIdsByOperation].map(([key, legIds]) => {
|
|
299
|
+
const [interfaceId = '', operationId = ''] = key.split('\u0000');
|
|
300
|
+
return {
|
|
301
|
+
kind: 'interface-present',
|
|
302
|
+
interfaceId,
|
|
303
|
+
operationId,
|
|
304
|
+
legIds: [...legIds],
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
runId,
|
|
309
|
+
legs: origins.map((origin) => origin.leg),
|
|
310
|
+
checks: [...presence, ...checks],
|
|
311
|
+
referenceSets: referenceSetMembers(contract),
|
|
312
|
+
};
|
|
313
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Operation } from '../schemas/interface.ts';
|
|
2
|
+
import type { ProbeObservation, ProbeObservedBody } from '../schemas/port-messages.ts';
|
|
3
|
+
export declare const PREFLIGHT_ARTIFACT_PATH = "PreflightVerdict";
|
|
4
|
+
/**
|
|
5
|
+
* The five fields and no others. Response headers are outside the projection
|
|
6
|
+
* because `volatilePointers` is a `DescriptorPointer` and the response
|
|
7
|
+
* descriptor is body-scoped, so no declaration can mark a header volatile, and
|
|
8
|
+
* unprunable headers would fail the repeated-read immutability branch on any
|
|
9
|
+
* fixture that echoes a request identifier back in one.
|
|
10
|
+
*
|
|
11
|
+
* Two costs to know about: two fixture versions differing only in a response
|
|
12
|
+
* header digest identically, and a witness relation can read a header this
|
|
13
|
+
* projection cannot see. The projection says what the fixture is; a relation
|
|
14
|
+
* addressing a header is the author asserting that header is stable.
|
|
15
|
+
*/
|
|
16
|
+
export type ProjectedObservation = {
|
|
17
|
+
readonly legId: string;
|
|
18
|
+
readonly interfaceId: string;
|
|
19
|
+
readonly operationId: string;
|
|
20
|
+
readonly status: number;
|
|
21
|
+
readonly body: ProbeObservedBody;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Removes every declared volatile pointer from a `json` body. `text` and
|
|
25
|
+
* `absent` pass through untouched, so a body that changed its content type
|
|
26
|
+
* stays visible to the digest. The input is cloned before anything is deleted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function pruneVolatile(body: ProbeObservedBody, volatilePointers: readonly string[], artifactPath: string): ProbeObservedBody;
|
|
29
|
+
/** One observation reduced to the projection, with its volatile pointers gone. */
|
|
30
|
+
export declare function projectObservation(observation: ProbeObservation, operation: Operation, artifactPath: string): ProjectedObservation;
|
|
31
|
+
/**
|
|
32
|
+
* AD-11's fixture digest. Sorted by leg id first, because NFR9 forbids any
|
|
33
|
+
* stage from reading array position: two runs whose observations arrived in a
|
|
34
|
+
* different order describe the same fixture and must digest the same.
|
|
35
|
+
*
|
|
36
|
+
* The empty case throws here. `digestComposite` rejects an empty field bag, and
|
|
37
|
+
* `{ observations: [] }` has one field, so it would happily digest a pre-flight
|
|
38
|
+
* that verified nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fixtureDigest(projections: readonly ProjectedObservation[], artifactPath: string): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-11's named closed projection of a probe observation, and the fixture
|
|
3
|
+
* digest computed over it. The projection is what "the fixture" means for a
|
|
4
|
+
* scoring version: two runs whose projections agree describe the same fixture.
|
|
5
|
+
*/
|
|
6
|
+
import { digestComposite } from '../canonical/digest.js';
|
|
7
|
+
import { DESCRIPTOR_POINTER_PATTERN } from '../schemas/pointer.js';
|
|
8
|
+
import { decodeTail } from '../seal/plan-index.js';
|
|
9
|
+
export const PREFLIGHT_ARTIFACT_PATH = 'PreflightVerdict';
|
|
10
|
+
const isJsonObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
const ARRAY_INDEX = /^(?:0|[1-9][0-9]*)$/;
|
|
12
|
+
/** Deletes one already-decoded pointer from a cloned body value, in place. */
|
|
13
|
+
function deleteAt(root, tokens) {
|
|
14
|
+
let parent = root;
|
|
15
|
+
for (const token of tokens.slice(0, -1)) {
|
|
16
|
+
if (Array.isArray(parent)) {
|
|
17
|
+
if (!ARRAY_INDEX.test(token))
|
|
18
|
+
return;
|
|
19
|
+
const next = parent[Number(token)];
|
|
20
|
+
if (next === undefined)
|
|
21
|
+
return;
|
|
22
|
+
parent = next;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!isJsonObject(parent) || !Object.hasOwn(parent, token))
|
|
26
|
+
return;
|
|
27
|
+
parent = parent[token];
|
|
28
|
+
}
|
|
29
|
+
const last = tokens.at(-1);
|
|
30
|
+
if (last === undefined)
|
|
31
|
+
return;
|
|
32
|
+
if (Array.isArray(parent)) {
|
|
33
|
+
if (!ARRAY_INDEX.test(last))
|
|
34
|
+
return;
|
|
35
|
+
const index = Number(last);
|
|
36
|
+
if (index >= parent.length)
|
|
37
|
+
return;
|
|
38
|
+
parent.splice(index, 1);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!isJsonObject(parent))
|
|
42
|
+
return;
|
|
43
|
+
// A pointer that resolves to nothing is a no-op: a volatile field the fixture
|
|
44
|
+
// did not return this time is exactly what the declaration exists for.
|
|
45
|
+
if (!Object.hasOwn(parent, last))
|
|
46
|
+
return;
|
|
47
|
+
delete parent[last];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Removes every declared volatile pointer from a `json` body. `text` and
|
|
51
|
+
* `absent` pass through untouched, so a body that changed its content type
|
|
52
|
+
* stays visible to the digest. The input is cloned before anything is deleted.
|
|
53
|
+
*/
|
|
54
|
+
export function pruneVolatile(body, volatilePointers, artifactPath) {
|
|
55
|
+
for (const pointer of volatilePointers) {
|
|
56
|
+
if (DESCRIPTOR_POINTER_PATTERN.test(pointer))
|
|
57
|
+
continue;
|
|
58
|
+
throw new TypeError(`${artifactPath}: "${pointer}" is not a descriptor-relative pointer`);
|
|
59
|
+
}
|
|
60
|
+
if (body.kind !== 'json' || volatilePointers.length === 0)
|
|
61
|
+
return body;
|
|
62
|
+
// RFC 6901's empty pointer addresses the whole document, so a contract
|
|
63
|
+
// declaring it says the entire body is volatile.
|
|
64
|
+
if (volatilePointers.some((pointer) => pointer === ''))
|
|
65
|
+
return { kind: 'absent' };
|
|
66
|
+
const value = structuredClone(body.value);
|
|
67
|
+
for (const pointer of volatilePointers)
|
|
68
|
+
deleteAt(value, decodeTail(pointer));
|
|
69
|
+
return { kind: 'json', value };
|
|
70
|
+
}
|
|
71
|
+
/** One observation reduced to the projection, with its volatile pointers gone. */
|
|
72
|
+
export function projectObservation(observation, operation, artifactPath) {
|
|
73
|
+
return {
|
|
74
|
+
legId: observation.probeId,
|
|
75
|
+
interfaceId: observation.interfaceId,
|
|
76
|
+
operationId: observation.operationId,
|
|
77
|
+
status: observation.status,
|
|
78
|
+
body: pruneVolatile(observation.body, operation.volatilePointers, artifactPath),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* AD-11's fixture digest. Sorted by leg id first, because NFR9 forbids any
|
|
83
|
+
* stage from reading array position: two runs whose observations arrived in a
|
|
84
|
+
* different order describe the same fixture and must digest the same.
|
|
85
|
+
*
|
|
86
|
+
* The empty case throws here. `digestComposite` rejects an empty field bag, and
|
|
87
|
+
* `{ observations: [] }` has one field, so it would happily digest a pre-flight
|
|
88
|
+
* that verified nothing.
|
|
89
|
+
*/
|
|
90
|
+
export function fixtureDigest(projections, artifactPath) {
|
|
91
|
+
if (projections.length === 0) {
|
|
92
|
+
throw new TypeError(`${artifactPath}: a fixture digest over no observation would certify a pre-flight that verified nothing`);
|
|
93
|
+
}
|
|
94
|
+
const sorted = [...projections].sort((left, right) => left.legId < right.legId ? -1 : left.legId > right.legId ? 1 : 0);
|
|
95
|
+
return digestComposite({ observations: sorted }, artifactPath);
|
|
96
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProbeObservation } from '../schemas/port-messages.ts';
|
|
2
|
+
import type { PreflightVerdict } from '../schemas/preflight-verdict.ts';
|
|
3
|
+
import type { ReduceStage } from '../stage-contracts.ts';
|
|
4
|
+
import type { PreflightPlan } from './plan.ts';
|
|
5
|
+
export type PreflightObservations = {
|
|
6
|
+
readonly observations: readonly ProbeObservation[];
|
|
7
|
+
};
|
|
8
|
+
export declare const reducePreflight: ReduceStage<PreflightPlan, PreflightObservations, PreflightVerdict>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-34's reducing half: the plan plus the observations the port returned,
|
|
3
|
+
* reduced to a `PreflightVerdict`. Pure over the observations, and the only
|
|
4
|
+
* place AD-10's verdict semantics live.
|
|
5
|
+
*
|
|
6
|
+
* Observations are indexed by `probeId` (NFR9), and one `PreflightCheck` is
|
|
7
|
+
* emitted per `PlannedCheck`, so a verdict carries only the kinds its plan
|
|
8
|
+
* asked for.
|
|
9
|
+
*/
|
|
10
|
+
import { digestArtifact } from '../canonical/digest.js';
|
|
11
|
+
import { declaresNoRequestKeys } from '../declared-inputs.js';
|
|
12
|
+
import { freezeArtifact } from '../lineage/freeze.js';
|
|
13
|
+
import { RuntimeFault } from '../schemas/faults.js';
|
|
14
|
+
import { fixtureDigest, PREFLIGHT_ARTIFACT_PATH, projectObservation, } from './projection.js';
|
|
15
|
+
import { evidenceOf, resolveWitnessRelation } from './witness-evidence.js';
|
|
16
|
+
/**
|
|
17
|
+
* AD-10 names no threshold, and this is the one the repository already speaks:
|
|
18
|
+
* the published conformance suite ships `probe/observe-anomalous-status`, and
|
|
19
|
+
* `ProbeObservation.status` is bounded to 100-599 at the port, so HTTP is
|
|
20
|
+
* already assumed at that boundary.
|
|
21
|
+
*/
|
|
22
|
+
const ANOMALOUS_STATUS = 400;
|
|
23
|
+
const check = (kind, operationId, outcome, note) => ({ kind, operationId, outcome, note });
|
|
24
|
+
/**
|
|
25
|
+
* Two projections describe the same fixture state. `legId` is excluded because
|
|
26
|
+
* it is the one field that necessarily differs between the two legs compared, so
|
|
27
|
+
* deep-equality over the whole projection could never be satisfied.
|
|
28
|
+
*
|
|
29
|
+
* Compared through the canonical digest, because `JSON.stringify` is key-order
|
|
30
|
+
* sensitive: two adapters serialising one body's keys in different orders
|
|
31
|
+
* describe the same fixture, and failing there would invalidate a run over a
|
|
32
|
+
* difference that is not one.
|
|
33
|
+
*/
|
|
34
|
+
const sameFixtureState = (left, right) => {
|
|
35
|
+
const state = ({ legId: _legId, ...rest }) => rest;
|
|
36
|
+
return (digestArtifact(state(left), PREFLIGHT_ARTIFACT_PATH) ===
|
|
37
|
+
digestArtifact(state(right), PREFLIGHT_ARTIFACT_PATH));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a manifestation witness against one leg. Returns `null` when that
|
|
41
|
+
* leg produced no observation, which the two seeded-fault rows read
|
|
42
|
+
* differently: the fired row fails on it, the scoped row has nothing to
|
|
43
|
+
* contradict.
|
|
44
|
+
*/
|
|
45
|
+
const resolveAgainst = (witness, state, plan, artifactPath) => {
|
|
46
|
+
if (state === undefined)
|
|
47
|
+
return null;
|
|
48
|
+
// Keyed by the witness's own leg id, since the relation addresses
|
|
49
|
+
// `/interactions/{witness.legId}/…`. The scoped row asks whether that same
|
|
50
|
+
// relation would fire on a clean leg's observation.
|
|
51
|
+
return resolveWitnessRelation(witness.relation, { [witness.legId]: state.evidence }, state.leg.operation, plan.referenceSets, artifactPath).resolution;
|
|
52
|
+
};
|
|
53
|
+
export const reducePreflight = (plan, { observations }) => {
|
|
54
|
+
const byProbeId = new Map();
|
|
55
|
+
for (const observation of observations) {
|
|
56
|
+
// A repeated `probeId` is a broken echo: `ProbeRequest.probeId` comes
|
|
57
|
+
// back unchanged by contract, so two observations claiming one leg means
|
|
58
|
+
// the port answered a request nobody made. Left as a last-write-wins
|
|
59
|
+
// `Map` it would also make the verdict depend on array order, which is
|
|
60
|
+
// the class AD-30's permutation family exists to catch.
|
|
61
|
+
if (byProbeId.has(observation.probeId)) {
|
|
62
|
+
throw new RuntimeFault('port-contract-violation', PREFLIGHT_ARTIFACT_PATH, `two observations echoed the probe id "${observation.probeId}", so one leg was answered twice`);
|
|
63
|
+
}
|
|
64
|
+
byProbeId.set(observation.probeId, observation);
|
|
65
|
+
}
|
|
66
|
+
const states = new Map();
|
|
67
|
+
for (const leg of plan.legs) {
|
|
68
|
+
const observation = byProbeId.get(leg.legId);
|
|
69
|
+
if (observation === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
const projected = projectObservation(observation, leg.operation, PREFLIGHT_ARTIFACT_PATH);
|
|
72
|
+
states.set(leg.legId, {
|
|
73
|
+
leg,
|
|
74
|
+
observation,
|
|
75
|
+
projected,
|
|
76
|
+
evidence: evidenceOf(projected, observation, leg.inputs),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const reduceCheck = (planned) => {
|
|
80
|
+
switch (planned.kind) {
|
|
81
|
+
case 'interface-present': {
|
|
82
|
+
for (const legId of planned.legIds) {
|
|
83
|
+
const state = states.get(legId);
|
|
84
|
+
if (state === undefined)
|
|
85
|
+
return check(planned.kind, planned.operationId, 'failed', `leg "${legId}" produced no observation`);
|
|
86
|
+
const { request } = state.leg;
|
|
87
|
+
const echoed = state.observation;
|
|
88
|
+
if (echoed.probeId !== request.probeId ||
|
|
89
|
+
echoed.interfaceId !== request.interfaceId ||
|
|
90
|
+
echoed.operationId !== request.operationId)
|
|
91
|
+
return check(planned.kind, planned.operationId, 'failed', `leg "${legId}" echoed an identifier the request did not carry`);
|
|
92
|
+
}
|
|
93
|
+
return check(planned.kind, planned.operationId, 'satisfied', null);
|
|
94
|
+
}
|
|
95
|
+
case 'input-sensitivity': {
|
|
96
|
+
const { witness, operation } = planned;
|
|
97
|
+
if (witness === null)
|
|
98
|
+
return check(planned.kind, planned.operationId, declaresNoRequestKeys(operation) ? 'exempt' : 'failed', declaresNoRequestKeys(operation)
|
|
99
|
+
? 'The operation declares no inputs in any channel.'
|
|
100
|
+
: 'The operation declares request keys and no sensitivity witness.');
|
|
101
|
+
const evidence = {};
|
|
102
|
+
for (const leg of witness.legs) {
|
|
103
|
+
const state = states.get(leg.legId);
|
|
104
|
+
if (state === undefined)
|
|
105
|
+
return check(planned.kind, planned.operationId, 'failed', `witness leg "${leg.legId}" produced no observation`);
|
|
106
|
+
evidence[leg.legId] = state.evidence;
|
|
107
|
+
}
|
|
108
|
+
const { resolution } = resolveWitnessRelation(witness.relation, evidence, operation, plan.referenceSets, PREFLIGHT_ARTIFACT_PATH);
|
|
109
|
+
// AD-10's own sentence, and the most load-bearing line here: a
|
|
110
|
+
// sensitivity check that examined nothing has established nothing,
|
|
111
|
+
// so `insufficient-evidence` fails.
|
|
112
|
+
if (resolution === 'insufficient-evidence')
|
|
113
|
+
return check(planned.kind, planned.operationId, 'failed', 'The witness relation resolved insufficient-evidence.');
|
|
114
|
+
return check(planned.kind, planned.operationId, resolution === 'true' ? 'satisfied' : 'failed', resolution === 'true' ? null : 'The witness relation resolved false.');
|
|
115
|
+
}
|
|
116
|
+
case 'state-reset': {
|
|
117
|
+
const [firstId, lastId] = planned.legIds;
|
|
118
|
+
const first = states.get(firstId);
|
|
119
|
+
const last = states.get(lastId);
|
|
120
|
+
if (first === undefined || last === undefined)
|
|
121
|
+
return check(planned.kind, null, 'failed', 'a control-observe leg produced no observation');
|
|
122
|
+
return sameFixtureState(first.projected, last.projected)
|
|
123
|
+
? check(planned.kind, null, 'satisfied', null)
|
|
124
|
+
: check(planned.kind, null, 'failed', `the projections of "${firstId}" and "${lastId}" differ`);
|
|
125
|
+
}
|
|
126
|
+
case 'clean-control': {
|
|
127
|
+
for (const legId of planned.legIds) {
|
|
128
|
+
const state = states.get(legId);
|
|
129
|
+
if (state === undefined)
|
|
130
|
+
return check(planned.kind, null, 'failed', `control leg "${legId}" produced no observation`);
|
|
131
|
+
if (state.observation.status >= ANOMALOUS_STATUS)
|
|
132
|
+
return check(planned.kind, null, 'failed', `control leg "${legId}" observed status ${state.observation.status}`);
|
|
133
|
+
}
|
|
134
|
+
return check(planned.kind, null, 'satisfied', null);
|
|
135
|
+
}
|
|
136
|
+
case 'seeded-faults-scoped': {
|
|
137
|
+
const { witness, defectId } = planned;
|
|
138
|
+
for (const legId of planned.cleanLegIds) {
|
|
139
|
+
const resolved = resolveAgainst(witness, states.get(legId), plan, PREFLIGHT_ARTIFACT_PATH);
|
|
140
|
+
if (resolved === 'true')
|
|
141
|
+
return check(planned.kind, witness.operationId, 'failed', `${defectId}: the manifestation witness fires on clean leg "${legId}"`);
|
|
142
|
+
}
|
|
143
|
+
return check(planned.kind, witness.operationId, 'satisfied', defectId);
|
|
144
|
+
}
|
|
145
|
+
case 'seeded-fault-fired': {
|
|
146
|
+
const { witness, defectId } = planned;
|
|
147
|
+
if (witness === null)
|
|
148
|
+
return check(planned.kind, null, 'failed', `${defectId}: the defect declares no manifestation witness, so it cannot be observed to fire`);
|
|
149
|
+
const resolved = resolveAgainst(witness, states.get(witness.legId), plan, PREFLIGHT_ARTIFACT_PATH);
|
|
150
|
+
if (resolved === 'true')
|
|
151
|
+
return check(planned.kind, witness.operationId, 'satisfied', defectId);
|
|
152
|
+
return check(planned.kind, witness.operationId, 'failed', resolved === null
|
|
153
|
+
? `${defectId}: the fault leg produced no observation`
|
|
154
|
+
: `${defectId}: the manifestation witness resolved ${resolved} on its own fault leg`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
// The plan named legs and not one observation answered to a planned
|
|
159
|
+
// `probeId`. `ProbeRequest.probeId` is echoed unchanged by contract, so the
|
|
160
|
+
// port broke that contract and there is no observation to digest a fixture
|
|
161
|
+
// from. Thrown here as a typed fault: `fixtureDigest` would otherwise raise
|
|
162
|
+
// an untyped throw from inside the returned object literal, discarding the
|
|
163
|
+
// checks already computed.
|
|
164
|
+
if (states.size === 0) {
|
|
165
|
+
throw new RuntimeFault('port-contract-violation', PREFLIGHT_ARTIFACT_PATH, `no observation echoed any of the ${plan.legs.length} planned probe ids, so no fixture was observed`);
|
|
166
|
+
}
|
|
167
|
+
const checks = plan.checks.map(reduceCheck);
|
|
168
|
+
const projections = [...states.values()].map((state) => state.projected);
|
|
169
|
+
return freezeArtifact({
|
|
170
|
+
// A pre-flight verdict is an origin artifact, so AD-29's lineage fields
|
|
171
|
+
// carry their origin values.
|
|
172
|
+
schemaVersion: 1,
|
|
173
|
+
parentDigest: null,
|
|
174
|
+
revisionCount: 0,
|
|
175
|
+
runId: plan.runId,
|
|
176
|
+
fixtureDigest: fixtureDigest(projections, PREFLIGHT_ARTIFACT_PATH),
|
|
177
|
+
passed: checks.every((entry) => entry.outcome !== 'failed'),
|
|
178
|
+
checks,
|
|
179
|
+
});
|
|
180
|
+
};
|