eval-quality 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/corpus/dev/README.md +3 -2
- 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/fragment-selection.json +1 -0
- 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/review-corpus.json +1 -0
- 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/cli/render.d.ts +2 -5
- package/dist/cli/render.js +51 -1
- package/dist/core/compile/bindings.d.ts +26 -7
- package/dist/core/compile/bindings.js +49 -32
- package/dist/core/compile/compile.d.ts +12 -0
- package/dist/core/compile/compile.js +28 -1
- package/dist/core/compile/excluded-content.d.ts +11 -0
- package/dist/core/compile/excluded-content.js +42 -0
- package/dist/core/compile/expression-legality.d.ts +2 -2
- package/dist/core/compile/expression-legality.js +58 -12
- package/dist/core/compile/interface-inventory.d.ts +34 -1
- package/dist/core/compile/interface-inventory.js +88 -11
- package/dist/core/compile/reachability.d.ts +12 -2
- package/dist/core/compile/reachability.js +123 -36
- package/dist/core/compile/schema-version.d.ts +2 -0
- package/dist/core/compile/schema-version.js +25 -0
- package/dist/core/compile/sensitivity-witness.d.ts +31 -12
- package/dist/core/compile/sensitivity-witness.js +110 -23
- package/dist/core/compile/step-reference.d.ts +2 -0
- package/dist/core/compile/step-reference.js +49 -0
- package/dist/core/coverage/operations.d.ts +62 -0
- package/dist/core/coverage/operations.js +57 -0
- package/dist/core/coverage/relevance.d.ts +4 -2
- package/dist/core/coverage/relevance.js +22 -23
- package/dist/core/coverage/satisfaction.d.ts +2 -2
- package/dist/core/coverage/satisfaction.js +73 -48
- package/dist/core/declared-inputs.d.ts +83 -4
- package/dist/core/declared-inputs.js +105 -8
- package/dist/core/evaluate/evidence-resolution.d.ts +9 -12
- package/dist/core/evaluate/evidence-resolution.js +70 -10
- package/dist/core/evaluate/operators.d.ts +22 -0
- package/dist/core/evaluate/operators.js +17 -1
- package/dist/core/evaluate/resolution.d.ts +14 -3
- package/dist/core/evaluate/resolution.js +63 -3
- package/dist/core/excluded-content.d.ts +65 -0
- package/dist/core/excluded-content.js +113 -0
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +5 -2
- package/dist/core/ingest/conditions.d.ts +1 -1
- package/dist/core/ingest/ingest.js +8 -0
- package/dist/core/preflight/plan.d.ts +7 -5
- package/dist/core/preflight/plan.js +75 -32
- package/dist/core/preflight/projection.d.ts +6 -3
- package/dist/core/preflight/projection.js +22 -2
- package/dist/core/preflight/reduce.js +47 -7
- package/dist/core/preflight/witness-evidence.d.ts +5 -5
- package/dist/core/preflight/witness-evidence.js +63 -20
- package/dist/core/schemas/artifact.d.ts +683 -35
- package/dist/core/schemas/constraint-ledger.js +11 -0
- package/dist/core/schemas/defect-signature.d.ts +438 -16
- package/dist/core/schemas/defect-signature.js +58 -10
- package/dist/core/schemas/eval-contract.d.ts +517 -12
- package/dist/core/schemas/eval-contract.js +18 -2
- package/dist/core/schemas/evidence-artifact.d.ts +14 -4
- package/dist/core/schemas/evidence-artifact.js +1 -1
- package/dist/core/schemas/interface.d.ts +741 -13
- package/dist/core/schemas/interface.js +120 -7
- package/dist/core/schemas/isolation-manifest.js +16 -9
- package/dist/core/schemas/plan.d.ts +217 -3
- package/dist/core/schemas/plan.js +22 -1
- package/dist/core/schemas/pointer.d.ts +47 -1
- package/dist/core/schemas/pointer.js +89 -8
- package/dist/core/schemas/port-messages.d.ts +194 -10
- package/dist/core/schemas/port-messages.js +73 -6
- package/dist/core/schemas/probe-body.d.ts +18 -0
- package/dist/core/schemas/probe-body.js +13 -0
- package/dist/core/schemas/probe.d.ts +122 -3
- package/dist/core/schemas/probe.js +1 -1
- package/dist/core/schemas/sealed-run-record.d.ts +131 -9
- package/dist/core/schemas/sealed-run-record.js +56 -11
- package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
- package/dist/core/schemas/sensitivity-witness.js +61 -5
- package/dist/core/score/bindings.d.ts +2 -2
- package/dist/core/score/bindings.js +25 -13
- package/dist/core/score/qualification.d.ts +5 -5
- package/dist/core/score/qualification.js +67 -37
- package/dist/core/score/quotation.d.ts +1 -24
- package/dist/core/score/quotation.js +29 -4
- package/dist/core/score/score.js +28 -2
- package/dist/core/score/strength.d.ts +9 -0
- package/dist/core/score/strength.js +35 -1
- package/dist/core/score/witness.d.ts +21 -0
- package/dist/core/score/witness.js +9 -4
- package/dist/core/seal/derived-reference.d.ts +2 -9
- package/dist/core/seal/derived-reference.js +62 -23
- package/dist/core/seal/plan-index.d.ts +18 -3
- package/dist/core/seal/plan-index.js +44 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ports/environment-probe-port.d.ts +60 -4
- package/dist/testing/probe-conformance.d.ts +29 -0
- package/dist/testing/probe-conformance.js +34 -5
- package/package.json +1 -1
- package/schemas/eval-contract.schema.json +1356 -464
- package/schemas/evidence-artifact.schema.json +59 -29
- package/schemas/isolation-manifest.schema.json +17 -10
- package/schemas/probe.schema.json +197 -71
- package/schemas/rubric.schema.json +1 -1
- package/schemas/sealed-run-record.schema.json +296 -39
|
@@ -4,28 +4,65 @@
|
|
|
4
4
|
* execute it, and that its leg identifiers are distinct from each other and
|
|
5
5
|
* from every interaction-plan step.
|
|
6
6
|
*
|
|
7
|
-
* No new AD-5 code is minted.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* No new AD-5 code is minted here. `check:ad5-registry` pins the registry
|
|
8
|
+
* against the spine's own table, so each defect below takes the code that
|
|
9
|
+
* already names it. Two rows stretch that reading: a leg-id equality
|
|
10
10
|
* and a leg-id/step-id collision are identifier collisions, and
|
|
11
11
|
* `malformed-operator-expression` is the closest available code because the
|
|
12
12
|
* relation is illegal in its position once its operands cannot be told apart.
|
|
13
13
|
*/
|
|
14
|
-
import { declaresNoRequestKeys } from '../declared-inputs.js';
|
|
14
|
+
import { declaresNoRequestKeys, isCommandOperation, requestChannelsOf, } from '../declared-inputs.js';
|
|
15
15
|
import { StructuralFailure } from '../failure-codes.js';
|
|
16
|
-
import {
|
|
16
|
+
import { operationsOf } from '../schemas/interface.js';
|
|
17
|
+
import { COMMAND_WITNESS_CHANNELS, } from '../schemas/sensitivity-witness.js';
|
|
17
18
|
import { parseEvidenceTarget } from '../seal/plan-index.js';
|
|
18
19
|
export { declaresNoRequestKeys };
|
|
20
|
+
/** Which spelling this leg uses. `stdin` is the command shape's own key. */
|
|
21
|
+
const isCommandWitnessInputs = (inputs) => 'stdin' in inputs;
|
|
19
22
|
/**
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
22
|
-
* declared as a keyed shape, so such a leg omits every required key rather
|
|
23
|
-
* than being exempt from the comparison.
|
|
23
|
+
* The transport spelling, for the consumers that can only send one: the probe
|
|
24
|
+
* port carries a method, a path template, and the four transport channels.
|
|
24
25
|
*/
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
export const isApiWitnessInputs = (inputs) => 'body' in inputs;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the leg supplies this channel as an undifferentiated stream.
|
|
29
|
+
*
|
|
30
|
+
* Standard input written as text is one such stream. A `KeyedShapeDescriptor`
|
|
31
|
+
* can say what keys a channel carries and cannot say "one opaque stream", so a
|
|
32
|
+
* command that reads a prompt is declared by naming the one thing the stream
|
|
33
|
+
* carries, and the leg supplies that thing as the text it actually is.
|
|
34
|
+
*/
|
|
35
|
+
export function suppliesOpaquely(inputs, channel) {
|
|
36
|
+
return (isCommandWitnessInputs(inputs) &&
|
|
37
|
+
channel === 'stdin' &&
|
|
38
|
+
inputs.stdin.kind === 'text');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What an opaque stream fills, and when it cannot say.
|
|
42
|
+
*
|
|
43
|
+
* A text leg supplies the channel's ONE declared key: the stream is the value
|
|
44
|
+
* of the single thing the operation says it reads. Where the channel declares
|
|
45
|
+
* no required key there is nothing for the text to be, and where it declares
|
|
46
|
+
* more than one there is no way to say which the text fills, so both are the
|
|
47
|
+
* authoring fault this reports rather than a comparison that quietly abstains.
|
|
48
|
+
*
|
|
49
|
+
* That is what a keyed `stdin` means for a command that reads text, and it is
|
|
50
|
+
* the reason the declaration stays keyed: an operation that genuinely parses a
|
|
51
|
+
* structured document off standard input declares several keys and supplies
|
|
52
|
+
* them through the leg's `json` arm, and the same descriptor serves both.
|
|
53
|
+
*/
|
|
54
|
+
function checkOpaqueStream(shape, channel, operationId, owner, artifactPath) {
|
|
55
|
+
if (shape.requiredKeys.length === 1)
|
|
56
|
+
return;
|
|
57
|
+
throw new StructuralFailure('undeclared-mandatory-input', artifactPath, `${owner} supplies its ${channel} channel as text, which fills the one key the channel declares, but operation "${operationId}" declares ${shape.requiredKeys.length === 0 ? 'no required key there, so the text fills nothing' : `${shape.requiredKeys.length} required keys there, so nothing says which the text fills`} (AD-10, AD-19)`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The keys a tagged body value supplies. A body that is absent, or JSON that
|
|
61
|
+
* is not an object, supplies no keys: the channel is declared as a keyed
|
|
62
|
+
* shape, so such a leg omits every required key rather than being exempt from
|
|
63
|
+
* the comparison.
|
|
64
|
+
*/
|
|
65
|
+
function bodyKeys(body) {
|
|
29
66
|
if (body.kind !== 'json')
|
|
30
67
|
return [];
|
|
31
68
|
const { value } = body;
|
|
@@ -33,6 +70,37 @@ export function suppliedKeys(inputs, channel) {
|
|
|
33
70
|
return [];
|
|
34
71
|
return Object.keys(value);
|
|
35
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The keys one set of witness inputs supplies on one channel, or none where
|
|
75
|
+
* the leg's spelling has no such channel.
|
|
76
|
+
*
|
|
77
|
+
* `body` and `stdin` are the two channels a leg supplies as a tagged value
|
|
78
|
+
* rather than as a key map, because a leg has to tell an absent value from one
|
|
79
|
+
* carrying JSON null. This function is the bridge between that spelling and
|
|
80
|
+
* the keyed shape the request declares.
|
|
81
|
+
*/
|
|
82
|
+
export function suppliedKeys(inputs, channel) {
|
|
83
|
+
if (isCommandWitnessInputs(inputs)) {
|
|
84
|
+
if (channel === 'stdin')
|
|
85
|
+
return bodyKeys(inputs.stdin);
|
|
86
|
+
if (channel === 'argument' || channel === 'option')
|
|
87
|
+
return Object.keys(inputs[channel]);
|
|
88
|
+
if (channel === 'environment')
|
|
89
|
+
return Object.keys(inputs.environment);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
if (channel === 'body')
|
|
93
|
+
return bodyKeys(inputs.body);
|
|
94
|
+
if (channel === 'path' || channel === 'query')
|
|
95
|
+
return Object.keys(inputs[channel]);
|
|
96
|
+
if (channel === 'header')
|
|
97
|
+
return Object.keys(inputs.header);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
/** One channel's supplied value, for the differential comparison. */
|
|
101
|
+
export function suppliedValue(inputs, channel) {
|
|
102
|
+
return inputs[channel];
|
|
103
|
+
}
|
|
36
104
|
/**
|
|
37
105
|
* Every channel of one set of witness inputs against the operation that will
|
|
38
106
|
* receive them. All four, not only the differential channel: `planPreflight`
|
|
@@ -59,8 +127,11 @@ export function suppliedKeys(inputs, channel) {
|
|
|
59
127
|
* check, and what pre-flight probes is a fixture.
|
|
60
128
|
*/
|
|
61
129
|
export function checkInputsAgainstShape(inputs, operation, owner, artifactPath) {
|
|
62
|
-
for (const channel of
|
|
63
|
-
|
|
130
|
+
for (const { channel, shape } of requestChannelsOf(operation)) {
|
|
131
|
+
if (suppliesOpaquely(inputs, channel)) {
|
|
132
|
+
checkOpaqueStream(shape, channel, operation.operationId, owner, artifactPath);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
64
135
|
const supplied = suppliedKeys(inputs, channel);
|
|
65
136
|
for (const key of shape.requiredKeys) {
|
|
66
137
|
if (supplied.includes(key))
|
|
@@ -78,7 +149,7 @@ export function checkInputsAgainstShape(inputs, operation, owner, artifactPath)
|
|
|
78
149
|
const operationPath = (interfaceIndex, operationIndex) => `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
|
|
79
150
|
function forEachWitness(contract, visit) {
|
|
80
151
|
contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
|
|
81
|
-
iface.
|
|
152
|
+
operationsOf(iface).forEach((operation, operationIndex) => {
|
|
82
153
|
const witness = operation.sensitivityWitness;
|
|
83
154
|
if (witness === null)
|
|
84
155
|
return;
|
|
@@ -94,13 +165,13 @@ function forEachWitness(contract, visit) {
|
|
|
94
165
|
*/
|
|
95
166
|
export function checkSensitivityWitnessDeclared(contract) {
|
|
96
167
|
contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
|
|
97
|
-
iface.
|
|
168
|
+
operationsOf(iface).forEach((operation, operationIndex) => {
|
|
98
169
|
const path = operationPath(interfaceIndex, operationIndex);
|
|
99
170
|
const witness = operation.sensitivityWitness;
|
|
100
171
|
if (witness === null) {
|
|
101
172
|
if (declaresNoRequestKeys(operation))
|
|
102
173
|
return;
|
|
103
|
-
throw new StructuralFailure('undeclared-mandatory-input', path, `operation "${operation.operationId}" declares request keys but no sensitivity witness; only an operation declaring no keys in any channel is exempt (AD-10)`);
|
|
174
|
+
throw new StructuralFailure('undeclared-mandatory-input', path, `operation "${operation.operationId}" declares request keys but no sensitivity witness; only an operation declaring no keys in any channel is exempt (AD-10). The relation is the author's to choose and need not assert the two legs differ: an operation that is insensitive to its declared inputs by design declares a witness whose relation says so, which is a true and checkable claim about it, and gets the weaker guarantee that follows`);
|
|
104
175
|
}
|
|
105
176
|
witness.legs.forEach((leg, legIndex) => {
|
|
106
177
|
checkInputsAgainstShape(leg.inputs, operation, `leg "${leg.legId}"`, `${path}.sensitivityWitness.legs[${legIndex}]`);
|
|
@@ -137,8 +208,22 @@ function addressedStepIds(expression) {
|
|
|
137
208
|
walk(expression);
|
|
138
209
|
return found;
|
|
139
210
|
}
|
|
140
|
-
|
|
141
|
-
const
|
|
211
|
+
const MUTATING_CHANNELS = ['body'];
|
|
212
|
+
const READ_CHANNELS = ['path', 'query'];
|
|
213
|
+
/**
|
|
214
|
+
* The channels AD-10 admits for an operation's witness.
|
|
215
|
+
*
|
|
216
|
+
* Off an interface that speaks HTTP the state-change marker selects one, since
|
|
217
|
+
* a read carries its identifier in the URL and a write carries it in the body.
|
|
218
|
+
* A command carries its inputs the same way whether or not it changes state,
|
|
219
|
+
* so the marker selects nothing there and all four command channels are
|
|
220
|
+
* admitted. The author picks the one their operation is actually sensitive on.
|
|
221
|
+
*/
|
|
222
|
+
const legalChannels = (operation) => {
|
|
223
|
+
if (isCommandOperation(operation))
|
|
224
|
+
return COMMAND_WITNESS_CHANNELS;
|
|
225
|
+
return operation.stateChangeMarker ? MUTATING_CHANNELS : READ_CHANNELS;
|
|
226
|
+
};
|
|
142
227
|
/**
|
|
143
228
|
* The shape rules: the differential channel agrees with the state-change
|
|
144
229
|
* marker, the relation reads both legs and nothing else, and a declared
|
|
@@ -158,8 +243,8 @@ export function checkWitnessLegality(contract) {
|
|
|
158
243
|
const [first, second] = witness.legs;
|
|
159
244
|
if (first !== undefined &&
|
|
160
245
|
second !== undefined &&
|
|
161
|
-
JSON.stringify(first.inputs
|
|
162
|
-
JSON.stringify(second.inputs
|
|
246
|
+
JSON.stringify(suppliedValue(first.inputs, witness.channel)) ===
|
|
247
|
+
JSON.stringify(suppliedValue(second.inputs, witness.channel))) {
|
|
163
248
|
throw new StructuralFailure('malformed-operator-expression', `${path}.sensitivityWitness.legs`, `both legs of witness "${witness.witnessId}" supply the same ${witness.channel} value, so the pair is not a differential (AD-10)`);
|
|
164
249
|
}
|
|
165
250
|
const legIds = witness.legs.map((leg) => leg.legId);
|
|
@@ -181,7 +266,9 @@ export function checkWitnessLegality(contract) {
|
|
|
181
266
|
if (reset === null)
|
|
182
267
|
return;
|
|
183
268
|
const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
|
|
184
|
-
const operation = iface
|
|
269
|
+
const operation = iface === undefined
|
|
270
|
+
? undefined
|
|
271
|
+
: operationsOf(iface).find((candidate) => candidate.operationId === reset.operationId);
|
|
185
272
|
if (operation === undefined) {
|
|
186
273
|
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)`);
|
|
187
274
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `irreducible-step-reference`: two steps one direction references render to
|
|
3
|
+
* the same derived reference even after full escalation.
|
|
4
|
+
*
|
|
5
|
+
* AD-16 keeps step identifiers out of the sealed brief, so a direction names
|
|
6
|
+
* each step it references by a phrase derived from what the contract declares
|
|
7
|
+
* about it. Where two steps of one operation declare nothing that tells them
|
|
8
|
+
* apart, the escalation ladder runs out and the renderer has no way to name one
|
|
9
|
+
* without naming the other.
|
|
10
|
+
*
|
|
11
|
+
* The check runs the seal-side renderer rather than reimplementing its ladder.
|
|
12
|
+
* A second implementation of a three-rung escalation with a budget would be a
|
|
13
|
+
* second thing to keep in step with the first, and the failure this reports is
|
|
14
|
+
* defined as "what that renderer cannot do" rather than as a rule of its own.
|
|
15
|
+
* That is the same reasoning `checkEvidenceReachability` follows in reusing
|
|
16
|
+
* `evaluatePointerReachability`.
|
|
17
|
+
*
|
|
18
|
+
* Without it a contract passes every other check and then fails inside `seal`,
|
|
19
|
+
* which is the one authoring fault in the tree that reached a caller as a
|
|
20
|
+
* stack trace instead of a code and a path.
|
|
21
|
+
*/
|
|
22
|
+
import { StructuralFailure } from '../failure-codes.js';
|
|
23
|
+
import { renderEvidenceReferences } from '../seal/derived-reference.js';
|
|
24
|
+
import { buildPlanIndex } from '../seal/plan-index.js';
|
|
25
|
+
export function checkStepReferenceReducibility(contract) {
|
|
26
|
+
let index;
|
|
27
|
+
for (const oracle of contract.oracles) {
|
|
28
|
+
const { direction } = oracle;
|
|
29
|
+
if (direction === null || direction.evidenceTargets.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
index ??= buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
32
|
+
try {
|
|
33
|
+
renderEvidenceReferences(direction.evidenceTargets, index);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (!(error instanceof StructuralFailure)) {
|
|
37
|
+
// Every other way this renderer fails is a precondition violation
|
|
38
|
+
// on input `compile` has already checked, so it is a fault rather
|
|
39
|
+
// than an authoring fault and propagates unchanged.
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
// Re-thrown with the contract-side address. `StructuralFailure`
|
|
43
|
+
// carries the code and the path and folds the detail into its
|
|
44
|
+
// message, so the detail is recovered from the message rather than
|
|
45
|
+
// from a field the class does not expose.
|
|
46
|
+
throw new StructuralFailure(error.code, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets`, error.message.slice(`${error.code} in ${error.artifactPath}: `.length));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One declared operation joined to the interface that declares it, plus the
|
|
3
|
+
* two facts the coverage predicates need and neither the operation nor the
|
|
4
|
+
* interface states: where a response descriptor's pointers resolve, and which
|
|
5
|
+
* channels a call's inputs may be keyed by.
|
|
6
|
+
*
|
|
7
|
+
* `relevance.ts` and `satisfaction.ts` each flattened `permittedInterfaces`
|
|
8
|
+
* with `flatMap((declared) => declared.operations)` and dropped the interface
|
|
9
|
+
* one line before every loop that ranges over it, so six of AD-20's seven
|
|
10
|
+
* rules could not see the kind that declared the operation they were grading.
|
|
11
|
+
* This is that flattening, written once and keeping the join.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here throws, which is the promise both predicate modules make in
|
|
14
|
+
* their own headers and which `evaluateCoverage`'s callers rely on. Every
|
|
15
|
+
* field is read off an already-parsed contract, so there is no precondition
|
|
16
|
+
* left to assert.
|
|
17
|
+
*/
|
|
18
|
+
import { type RequestChannel } from '../declared-inputs.ts';
|
|
19
|
+
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
20
|
+
import type { AnyOperation, InterfaceKindName, ResponseDescriptor } from '../schemas/interface.ts';
|
|
21
|
+
import type { InputChannelName } from '../schemas/pointer.ts';
|
|
22
|
+
/**
|
|
23
|
+
* RFC 6901 escaping, `~` before `/`. Lives here rather than beside the
|
|
24
|
+
* pointer-building helpers that use it, because the descriptor root is built
|
|
25
|
+
* here and both spellings have to escape the same way.
|
|
26
|
+
*/
|
|
27
|
+
export declare const encodeToken: (token: string) => string;
|
|
28
|
+
export type ResolvedOperation = {
|
|
29
|
+
readonly operation: AnyOperation;
|
|
30
|
+
/** the declaring interface's kind. An operation states no kind of its own. */
|
|
31
|
+
readonly kind: InterfaceKindName;
|
|
32
|
+
/** the declaring interface. Two interfaces may declare one `operationId`, and this is what separates them. */
|
|
33
|
+
readonly logicalId: string;
|
|
34
|
+
readonly descriptor: ResponseDescriptor;
|
|
35
|
+
/**
|
|
36
|
+
* The interaction-rooted segment a descriptor pointer hangs off, which is
|
|
37
|
+
* the channel this operation's own descriptor describes, and on the artifact
|
|
38
|
+
* channel the file as well. `/artifact` alone is not a root any real
|
|
39
|
+
* evidence pointer starts with, since an artifact pointer carries its
|
|
40
|
+
* identifier before its tail, so a root without it matched nothing and made
|
|
41
|
+
* every pointer-building rule answer against a pointer that cannot exist.
|
|
42
|
+
*/
|
|
43
|
+
readonly descriptorRoot: string;
|
|
44
|
+
/**
|
|
45
|
+
* The channels a call's inputs may be keyed by. Names only: the two sites
|
|
46
|
+
* that build candidate pointers need the names and never the shapes.
|
|
47
|
+
*/
|
|
48
|
+
readonly transportChannels: readonly InputChannelName[];
|
|
49
|
+
/**
|
|
50
|
+
* The same channels already paired with the shapes they declare. Predicates
|
|
51
|
+
* that read a declared shape take these rather than indexing the request
|
|
52
|
+
* shape with a name, because under the operation union TypeScript cannot
|
|
53
|
+
* prove a name drawn from one kind's tuple is a key of the other's shape.
|
|
54
|
+
*/
|
|
55
|
+
readonly requestChannels: readonly RequestChannel[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Every declared operation, in declaration order, each carrying the interface
|
|
59
|
+
* that declares it. Six of AD-20's seven rules range over this list; rule 5 is
|
|
60
|
+
* contract-level and reads the sibling groups instead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveOperations(contract: EvalContract): readonly ResolvedOperation[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One declared operation joined to the interface that declares it, plus the
|
|
3
|
+
* two facts the coverage predicates need and neither the operation nor the
|
|
4
|
+
* interface states: where a response descriptor's pointers resolve, and which
|
|
5
|
+
* channels a call's inputs may be keyed by.
|
|
6
|
+
*
|
|
7
|
+
* `relevance.ts` and `satisfaction.ts` each flattened `permittedInterfaces`
|
|
8
|
+
* with `flatMap((declared) => declared.operations)` and dropped the interface
|
|
9
|
+
* one line before every loop that ranges over it, so six of AD-20's seven
|
|
10
|
+
* rules could not see the kind that declared the operation they were grading.
|
|
11
|
+
* This is that flattening, written once and keeping the join.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here throws, which is the promise both predicate modules make in
|
|
14
|
+
* their own headers and which `evaluateCoverage`'s callers rely on. Every
|
|
15
|
+
* field is read off an already-parsed contract, so there is no precondition
|
|
16
|
+
* left to assert.
|
|
17
|
+
*/
|
|
18
|
+
import { descriptorArtifactOf, descriptorChannelOf, inputChannelsOf, requestChannelsOf, } from '../declared-inputs.js';
|
|
19
|
+
import { operationsOf } from '../schemas/interface.js';
|
|
20
|
+
/**
|
|
21
|
+
* RFC 6901 escaping, `~` before `/`. Lives here rather than beside the
|
|
22
|
+
* pointer-building helpers that use it, because the descriptor root is built
|
|
23
|
+
* here and both spellings have to escape the same way.
|
|
24
|
+
*/
|
|
25
|
+
export const encodeToken = (token) => token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
26
|
+
/**
|
|
27
|
+
* Where a descriptor-relative pointer hangs off an interaction root for this
|
|
28
|
+
* operation. Two segments on the artifact channel, one everywhere else.
|
|
29
|
+
*/
|
|
30
|
+
const descriptorRootOf = (operation) => {
|
|
31
|
+
const channel = descriptorChannelOf(operation);
|
|
32
|
+
if (channel !== 'artifact')
|
|
33
|
+
return `/${channel}`;
|
|
34
|
+
const artifactId = descriptorArtifactOf(operation);
|
|
35
|
+
// Unreachable: `descriptorChannelOf` answers `artifact` only for the arm
|
|
36
|
+
// that carries an identifier.
|
|
37
|
+
if (artifactId === null) {
|
|
38
|
+
throw new TypeError(`operation "${operation.operationId}" describes an artifact and names none`);
|
|
39
|
+
}
|
|
40
|
+
return `/${channel}/${encodeToken(artifactId)}`;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Every declared operation, in declaration order, each carrying the interface
|
|
44
|
+
* that declares it. Six of AD-20's seven rules range over this list; rule 5 is
|
|
45
|
+
* contract-level and reads the sibling groups instead.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveOperations(contract) {
|
|
48
|
+
return contract.permittedInterfaces.flatMap((declared) => operationsOf(declared).map((operation) => ({
|
|
49
|
+
operation,
|
|
50
|
+
kind: declared.kind,
|
|
51
|
+
logicalId: declared.logicalId,
|
|
52
|
+
descriptor: operation.responseDescriptor,
|
|
53
|
+
descriptorRoot: descriptorRootOf(operation),
|
|
54
|
+
transportChannels: inputChannelsOf(operation),
|
|
55
|
+
requestChannels: requestChannelsOf(operation),
|
|
56
|
+
})));
|
|
57
|
+
}
|
|
@@ -32,8 +32,10 @@ export declare function successIndicatorSeparationRelevance(contract: EvalContra
|
|
|
32
32
|
*/
|
|
33
33
|
export declare function wholeBodyRelevance(contract: EvalContract): RelevanceVerdict;
|
|
34
34
|
/**
|
|
35
|
-
* Rule 3: some operation declares a request key on any of
|
|
36
|
-
*
|
|
35
|
+
* Rule 3: some operation declares a request key on any of its own input
|
|
36
|
+
* channels, AD-19's four transport channels off an interface that speaks HTTP
|
|
37
|
+
* and the four command channels off one that does not. The site is the whole
|
|
38
|
+
* channel triple. A key with no
|
|
37
39
|
* `types` entry has an absent type, a key typed `null` has AD-31's
|
|
38
40
|
* indeterminate one, and both are relevant, so declaring an input and leaving
|
|
39
41
|
* it untyped buys no irrelevance.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOperations } from './operations.js';
|
|
2
2
|
import { DISCIPLINE_RULES, relevancePredicateId, } from './rules.js';
|
|
3
3
|
const verdict = (rule, relevant, reason) => ({
|
|
4
4
|
rule,
|
|
@@ -8,8 +8,6 @@ const verdict = (rule, relevant, reason) => ({
|
|
|
8
8
|
});
|
|
9
9
|
/** `1 collection location`, `2 collection locations`. A reason is read by people. */
|
|
10
10
|
const plural = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
11
|
-
/** Every declared operation, flattened. Six of the seven rules range over this list. */
|
|
12
|
-
const operationsOf = (contract) => contract.permittedInterfaces.flatMap((declared) => declared.operations);
|
|
13
11
|
/** A contract declaring no operation leaves six of the rules nothing to read. */
|
|
14
12
|
export const NO_OPERATION = 'the contract declares no operation, so the declaration this rule reads is absent';
|
|
15
13
|
/**
|
|
@@ -20,11 +18,11 @@ export const NO_OPERATION = 'the contract declares no operation, so the declarat
|
|
|
20
18
|
*/
|
|
21
19
|
export function successIndicatorSeparationRelevance(contract) {
|
|
22
20
|
const rule = 'success-indicator-separation';
|
|
23
|
-
const operations =
|
|
21
|
+
const operations = resolveOperations(contract);
|
|
24
22
|
if (operations.length === 0)
|
|
25
23
|
return verdict(rule, true, NO_OPERATION);
|
|
26
|
-
for (const operation of operations) {
|
|
27
|
-
const { successIndicator, channelRoles } =
|
|
24
|
+
for (const { operation, descriptor } of operations) {
|
|
25
|
+
const { successIndicator, channelRoles } = descriptor;
|
|
28
26
|
// `=== null`: `DescriptorPointer` admits the empty string, which
|
|
29
27
|
// nominates the whole response body.
|
|
30
28
|
if (successIndicator === null) {
|
|
@@ -48,11 +46,11 @@ export function successIndicatorSeparationRelevance(contract) {
|
|
|
48
46
|
*/
|
|
49
47
|
export function wholeBodyRelevance(contract) {
|
|
50
48
|
const rule = 'whole-body';
|
|
51
|
-
const operations =
|
|
49
|
+
const operations = resolveOperations(contract);
|
|
52
50
|
if (operations.length === 0)
|
|
53
51
|
return verdict(rule, true, NO_OPERATION);
|
|
54
|
-
for (const operation of operations) {
|
|
55
|
-
const distinct = new Set(
|
|
52
|
+
for (const { operation, descriptor } of operations) {
|
|
53
|
+
const distinct = new Set(descriptor.requiredKeys);
|
|
56
54
|
if (distinct.size > 1) {
|
|
57
55
|
return verdict(rule, true, `operation ${operation.operationId} declares ${plural(distinct.size, 'distinct required response key')}`);
|
|
58
56
|
}
|
|
@@ -60,20 +58,21 @@ export function wholeBodyRelevance(contract) {
|
|
|
60
58
|
return verdict(rule, false, 'no operation declares more than one distinct required response key');
|
|
61
59
|
}
|
|
62
60
|
/**
|
|
63
|
-
* Rule 3: some operation declares a request key on any of
|
|
64
|
-
*
|
|
61
|
+
* Rule 3: some operation declares a request key on any of its own input
|
|
62
|
+
* channels, AD-19's four transport channels off an interface that speaks HTTP
|
|
63
|
+
* and the four command channels off one that does not. The site is the whole
|
|
64
|
+
* channel triple. A key with no
|
|
65
65
|
* `types` entry has an absent type, a key typed `null` has AD-31's
|
|
66
66
|
* indeterminate one, and both are relevant, so declaring an input and leaving
|
|
67
67
|
* it untyped buys no irrelevance.
|
|
68
68
|
*/
|
|
69
69
|
export function malformedInputRelevance(contract) {
|
|
70
70
|
const rule = 'malformed-input';
|
|
71
|
-
const operations =
|
|
71
|
+
const operations = resolveOperations(contract);
|
|
72
72
|
if (operations.length === 0)
|
|
73
73
|
return verdict(rule, true, NO_OPERATION);
|
|
74
|
-
for (const operation of operations) {
|
|
75
|
-
for (const channel of
|
|
76
|
-
const shape = operation.requestShape[channel];
|
|
74
|
+
for (const { operation, requestChannels } of operations) {
|
|
75
|
+
for (const { channel, shape } of requestChannels) {
|
|
77
76
|
// `Object.keys` enumerates own keys only. `KeyName` admits
|
|
78
77
|
// `constructor`, which a keyed lookup would find on the prototype.
|
|
79
78
|
const key = shape.requiredKeys[0] ??
|
|
@@ -92,11 +91,11 @@ export function malformedInputRelevance(contract) {
|
|
|
92
91
|
*/
|
|
93
92
|
export function perRecordRelevance(contract) {
|
|
94
93
|
const rule = 'per-record';
|
|
95
|
-
const operations =
|
|
94
|
+
const operations = resolveOperations(contract);
|
|
96
95
|
if (operations.length === 0)
|
|
97
96
|
return verdict(rule, true, NO_OPERATION);
|
|
98
|
-
for (const operation of operations) {
|
|
99
|
-
const { collectionLocations } =
|
|
97
|
+
for (const { operation, descriptor } of operations) {
|
|
98
|
+
const { collectionLocations } = descriptor;
|
|
100
99
|
if (collectionLocations === null) {
|
|
101
100
|
return verdict(rule, true, `operation ${operation.operationId} declares no collection-location list, so no collection is declared to range over`);
|
|
102
101
|
}
|
|
@@ -132,11 +131,11 @@ export function siblingCrossCheckRelevance(contract) {
|
|
|
132
131
|
*/
|
|
133
132
|
export function omissionAndCompletenessRelevance(contract) {
|
|
134
133
|
const rule = 'omission-and-completeness';
|
|
135
|
-
const operations =
|
|
134
|
+
const operations = resolveOperations(contract);
|
|
136
135
|
if (operations.length === 0)
|
|
137
136
|
return verdict(rule, true, NO_OPERATION);
|
|
138
|
-
for (const operation of operations) {
|
|
139
|
-
const { collectionLocations } =
|
|
137
|
+
for (const { operation, descriptor } of operations) {
|
|
138
|
+
const { collectionLocations } = descriptor;
|
|
140
139
|
if (collectionLocations === null) {
|
|
141
140
|
return verdict(rule, true, `operation ${operation.operationId} declares no collection-location list, so no location can name a reference set`);
|
|
142
141
|
}
|
|
@@ -155,10 +154,10 @@ export function omissionAndCompletenessRelevance(contract) {
|
|
|
155
154
|
*/
|
|
156
155
|
export function stateChangeReadBackRelevance(contract) {
|
|
157
156
|
const rule = 'state-change-read-back';
|
|
158
|
-
const operations =
|
|
157
|
+
const operations = resolveOperations(contract);
|
|
159
158
|
if (operations.length === 0)
|
|
160
159
|
return verdict(rule, true, NO_OPERATION);
|
|
161
|
-
for (const operation of operations) {
|
|
160
|
+
for (const { operation } of operations) {
|
|
162
161
|
if (operation.stateChangeMarker) {
|
|
163
162
|
return verdict(rule, true, `operation ${operation.operationId} declares stateChangeMarker: true`);
|
|
164
163
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type EvalContract } from '../schemas/eval-contract.ts';
|
|
2
2
|
import type { Expression } from '../schemas/expression.ts';
|
|
3
|
-
import type { Operation } from '../schemas/interface.ts';
|
|
4
3
|
import { type PlanIndex } from '../seal/plan-index.ts';
|
|
4
|
+
import { type ResolvedOperation } from './operations.ts';
|
|
5
5
|
import { type DisciplineRule } from './rules.ts';
|
|
6
6
|
export type SatisfactionVerdict = {
|
|
7
7
|
readonly rule: DisciplineRule;
|
|
@@ -40,7 +40,7 @@ type OracleView = {
|
|
|
40
40
|
* own, which is what every per-rule fixture does.
|
|
41
41
|
*/
|
|
42
42
|
export type SatisfactionContext = {
|
|
43
|
-
readonly operations: readonly
|
|
43
|
+
readonly operations: readonly ResolvedOperation[];
|
|
44
44
|
readonly index: PlanIndex;
|
|
45
45
|
readonly oracles: readonly OracleView[];
|
|
46
46
|
};
|