eval-quality 0.2.0 → 0.5.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 +17 -12
- 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/adapters/command-line-adapter.d.ts +44 -0
- package/dist/adapters/command-line-adapter.js +284 -0
- package/dist/adapters/command-target-policy.d.ts +36 -0
- package/dist/adapters/command-target-policy.js +33 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +1 -0
- 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-policy.d.ts +36 -0
- package/dist/core/schemas/probe-policy.js +44 -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/conformance.d.ts +2 -1
- package/dist/testing/conformance.js +1 -0
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +1 -1
- package/dist/testing/probe-conformance.d.ts +56 -5
- package/dist/testing/probe-conformance.js +196 -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
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
2
|
import type { Expression } from '../schemas/expression.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AnyOperation } from '../schemas/interface.ts';
|
|
4
4
|
import { type PlanIndex } from '../seal/plan-index.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Every interaction-rooted pointer the contract writes down, wherever it sits:
|
|
7
|
+
* an oracle's check and its direction's evidence targets, a rubric criterion's
|
|
8
|
+
* evidence, and each operation's sensitivity-witness relation.
|
|
9
|
+
*
|
|
10
|
+
* Broader than `forEachCheckPointer`, which walks oracle checks alone, because
|
|
11
|
+
* an artifact identifier is an authoring fault at every site that names one and
|
|
12
|
+
* a check that walked only the checks would report half of them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function forEachArtifactPointer(contract: EvalContract, visit: (pointer: string, artifactPath: string) => void): void;
|
|
5
15
|
export declare function checkBoundElementScope(contract: EvalContract): void;
|
|
6
16
|
/** `checkBoundElementScope` over one bare `Expression`. */
|
|
7
17
|
export declare function checkExpressionBoundElementScope(expression: Expression, artifactPath: string): void;
|
|
@@ -13,7 +23,7 @@ export declare function checkExpressionBoundElementScope(expression: Expression,
|
|
|
13
23
|
*/
|
|
14
24
|
export declare function forEachExpressionPointer(expression: Expression, visit: (pointer: string, path: string) => void): void;
|
|
15
25
|
/** `checkEvidenceReachability` over one bare `Expression` and one operation. */
|
|
16
|
-
export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation:
|
|
26
|
+
export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation: AnyOperation): void;
|
|
17
27
|
type ReachabilityResult = {
|
|
18
28
|
readonly reachable: true;
|
|
19
29
|
} | {
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* `evaluatePointerReachability` is exported separately as the non-throwing
|
|
8
8
|
* per-pointer core, for reuse and direct testing.
|
|
9
9
|
*/
|
|
10
|
+
import { declaredArtifactsOf, descriptorArtifactOf, descriptorChannelOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
|
|
10
11
|
import { ARRAY_INDEX_PATTERN } from '../evaluate/evidence-resolution.js';
|
|
11
12
|
import { StructuralFailure } from '../failure-codes.js';
|
|
13
|
+
import { operationsOf } from '../schemas/interface.js';
|
|
12
14
|
import { JsonTypeName } from '../schemas/primitives.js';
|
|
13
|
-
import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
15
|
+
import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
14
16
|
function visitOperand(operand, path, insideQuantifier, visit) {
|
|
15
17
|
if ('pointer' in operand)
|
|
16
18
|
visit({ pointer: operand.pointer, path, insideQuantifier });
|
|
@@ -52,6 +54,42 @@ function forEachCheckPointer(contract, visit) {
|
|
|
52
54
|
visitExpression(oracle.check, 'check', false, (site) => visit(site, oracle.id));
|
|
53
55
|
});
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Every interaction-rooted pointer the contract writes down, wherever it sits:
|
|
59
|
+
* an oracle's check and its direction's evidence targets, a rubric criterion's
|
|
60
|
+
* evidence, and each operation's sensitivity-witness relation.
|
|
61
|
+
*
|
|
62
|
+
* Broader than `forEachCheckPointer`, which walks oracle checks alone, because
|
|
63
|
+
* an artifact identifier is an authoring fault at every site that names one and
|
|
64
|
+
* a check that walked only the checks would report half of them.
|
|
65
|
+
*/
|
|
66
|
+
export function forEachArtifactPointer(contract, visit) {
|
|
67
|
+
const seen = (pointer, artifactPath) => {
|
|
68
|
+
if (pointer.startsWith('@'))
|
|
69
|
+
return;
|
|
70
|
+
visit(pointer, artifactPath);
|
|
71
|
+
};
|
|
72
|
+
contract.oracles.forEach((oracle) => {
|
|
73
|
+
if (oracle.check !== null)
|
|
74
|
+
visitExpression(oracle.check, 'check', false, (site) => seen(site.pointer, `EvalContract.oracles[id=${oracle.id}].${site.path}`));
|
|
75
|
+
oracle.direction?.evidenceTargets.forEach((target, index) => {
|
|
76
|
+
seen(target, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets[${index}]`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
contract.rubrics.forEach((rubric) => {
|
|
80
|
+
rubric.criteria.forEach((criterion) => {
|
|
81
|
+
seen(criterion.evidence, `EvalContract.rubrics[id=${rubric.id}].criteria[id=${criterion.id}].evidence`);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
|
|
85
|
+
operationsOf(iface).forEach((operation, operationIndex) => {
|
|
86
|
+
const witness = operation.sensitivityWitness;
|
|
87
|
+
if (witness === null)
|
|
88
|
+
return;
|
|
89
|
+
visitExpression(witness.relation, 'relation', false, (site) => seen(site.pointer, `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}].sensitivityWitness.${site.path}`));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
55
93
|
// ---- malformed-operator-expression: @/ outside any quantifier -----------
|
|
56
94
|
export function checkBoundElementScope(contract) {
|
|
57
95
|
forEachCheckPointer(contract, (site, oracleId) => {
|
|
@@ -125,12 +163,50 @@ export function evaluatePointerReachability(pointer, index) {
|
|
|
125
163
|
if (step === undefined) {
|
|
126
164
|
return unreachable('names a step the interaction plan does not declare');
|
|
127
165
|
}
|
|
128
|
-
const operation = index
|
|
166
|
+
const operation = anyOperationOf(index, step.operationId);
|
|
129
167
|
if (operation === undefined) {
|
|
130
168
|
return unreachable(`names step "${target.stepId}", which names operation "${step.operationId}", not declared by any permitted interface`);
|
|
131
169
|
}
|
|
132
170
|
return evaluateReachabilityAgainstOperation(pointer, operation);
|
|
133
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Descent through the operation's response descriptor, from whichever channel
|
|
174
|
+
* that descriptor describes. `channel` is carried only so the reason names the
|
|
175
|
+
* pointer's own root back to the author.
|
|
176
|
+
*/
|
|
177
|
+
function descendThroughDescriptor(descriptor, target, operationId, channel) {
|
|
178
|
+
if (target.tail.length === 0)
|
|
179
|
+
return reachable();
|
|
180
|
+
const firstToken = target.tail[0];
|
|
181
|
+
if (firstToken === undefined) {
|
|
182
|
+
// Unreachable: the length check above guarantees an element.
|
|
183
|
+
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
184
|
+
}
|
|
185
|
+
const { requiredKeys, permittedKeys, types, collectionLocations } = descriptor;
|
|
186
|
+
// A root-declared collection (`pointer: ''`) indexes directly, bypassing
|
|
187
|
+
// the key check below. `expectedCardinality` bounds the array size
|
|
188
|
+
// (`exact` is the true count; `at-most`/`page-bounded` is an upper
|
|
189
|
+
// bound), so an index at or past it is unreachable.
|
|
190
|
+
const rootCollection = collectionLocations?.find((location) => location.pointer === '');
|
|
191
|
+
if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
|
|
192
|
+
const { expectedCardinality } = rootCollection;
|
|
193
|
+
const bound = expectedCardinality.mode === 'exact'
|
|
194
|
+
? expectedCardinality.count
|
|
195
|
+
: expectedCardinality.max;
|
|
196
|
+
if (Number(firstToken) >= bound) {
|
|
197
|
+
return unreachable(`addresses ${channel} index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
|
|
198
|
+
}
|
|
199
|
+
return reachable();
|
|
200
|
+
}
|
|
201
|
+
if (!requiredKeys.includes(firstToken) &&
|
|
202
|
+
!permittedKeys.includes(firstToken)) {
|
|
203
|
+
return unreachable(`addresses ${channel} field "${firstToken}", which operation "${operationId}" declares in neither requiredKeys nor permittedKeys`);
|
|
204
|
+
}
|
|
205
|
+
if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
|
|
206
|
+
return unreachable(`descends into ${channel} field "${firstToken}", which operation "${operationId}" declares a scalar with no further structure`);
|
|
207
|
+
}
|
|
208
|
+
return reachable();
|
|
209
|
+
}
|
|
134
210
|
/**
|
|
135
211
|
* The same rules, against an operation the caller already resolved. Without
|
|
136
212
|
* this check a probe-side condition addressing an undeclared channel or key
|
|
@@ -142,47 +218,54 @@ function evaluateReachabilityAgainstOperation(pointer, operation) {
|
|
|
142
218
|
if (pointer.startsWith('@'))
|
|
143
219
|
return reachable();
|
|
144
220
|
const target = parseEvidenceTarget(pointer);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
221
|
+
const descriptorChannel = descriptorChannelOf(operation);
|
|
222
|
+
const command = isCommandOperation(operation);
|
|
223
|
+
if (target.channel === 'artifact') {
|
|
224
|
+
const { artifactId } = target;
|
|
225
|
+
if (artifactId === null) {
|
|
226
|
+
// Unreachable: parseEvidenceTarget's own guarantee.
|
|
227
|
+
throw new TypeError('artifact evidence target names no artifact');
|
|
150
228
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
229
|
+
// An identifier the operation does not declare produces no evidence, so
|
|
230
|
+
// it is unreachable and this says so. `checkArtifactReferences` runs
|
|
231
|
+
// earlier in `compile` and reports the more specific
|
|
232
|
+
// `unresolved-artifact-reference` for a contract, so the two never race
|
|
233
|
+
// there; this answer is the only one on the probe side, where that check
|
|
234
|
+
// does not run because it walks a contract rather than a signature.
|
|
235
|
+
if (!declaredArtifactsOf(operation).includes(artifactId)) {
|
|
236
|
+
return unreachable(`names the "${artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes`);
|
|
160
237
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const rootCollection = collectionLocations?.find((location) => location.pointer === '');
|
|
167
|
-
if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
|
|
168
|
-
const { expectedCardinality } = rootCollection;
|
|
169
|
-
const bound = expectedCardinality.mode === 'exact'
|
|
170
|
-
? expectedCardinality.count
|
|
171
|
-
: expectedCardinality.max;
|
|
172
|
-
if (Number(firstToken) >= bound) {
|
|
173
|
-
return unreachable(`addresses response-body index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
|
|
238
|
+
if (artifactId !== descriptorArtifactOf(operation)) {
|
|
239
|
+
// Declared to exist, and nothing declares its structure: the
|
|
240
|
+
// operation's one descriptor describes a different channel.
|
|
241
|
+
if (target.tail.length > 0) {
|
|
242
|
+
return unreachable(`addresses a field inside the "${artifactId}" artifact, which operation "${operation.operationId}" declares it writes but declares no structure for`);
|
|
174
243
|
}
|
|
175
244
|
return reachable();
|
|
176
245
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
246
|
+
return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, `the "${artifactId}" artifact`);
|
|
247
|
+
}
|
|
248
|
+
// The declared output channel descends through the descriptor whatever it
|
|
249
|
+
// is called. This is the one rule; the channels below are the cases where
|
|
250
|
+
// no descriptor applies.
|
|
251
|
+
if (target.channel === descriptorChannel) {
|
|
252
|
+
return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, target.channel);
|
|
253
|
+
}
|
|
254
|
+
if (target.channel === 'stdout' || target.channel === 'stderr') {
|
|
255
|
+
// The stream this operation's descriptor does not describe carries no
|
|
256
|
+
// declared structure, so a non-empty tail proves the pointer
|
|
257
|
+
// unreachable.
|
|
258
|
+
if (target.tail.length > 0) {
|
|
259
|
+
return unreachable(`addresses a field inside ${target.channel}, which operation "${operation.operationId}" declares no structure for`);
|
|
183
260
|
}
|
|
184
261
|
return reachable();
|
|
185
262
|
}
|
|
263
|
+
if (command &&
|
|
264
|
+
(target.channel === 'response-body' ||
|
|
265
|
+
target.channel === 'response-headers' ||
|
|
266
|
+
target.channel === 'response-status')) {
|
|
267
|
+
return unreachable(`addresses ${target.channel} on operation "${operation.operationId}", which runs behind a command and produces no HTTP response`);
|
|
268
|
+
}
|
|
186
269
|
if (target.channel === 'call-inputs') {
|
|
187
270
|
if (target.tail.length === 0)
|
|
188
271
|
return reachable();
|
|
@@ -195,7 +278,11 @@ function evaluateReachabilityAgainstOperation(pointer, operation) {
|
|
|
195
278
|
if (firstToken === undefined) {
|
|
196
279
|
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
197
280
|
}
|
|
198
|
-
const
|
|
281
|
+
const shape = requestShapeOf(operation, transportChannel);
|
|
282
|
+
if (shape === undefined) {
|
|
283
|
+
return unreachable(`addresses call-inputs ${transportChannel}, a channel operation "${operation.operationId}" does not accept input on`);
|
|
284
|
+
}
|
|
285
|
+
const { requiredKeys, permittedKeys, types } = shape;
|
|
199
286
|
if (!requiredKeys.includes(firstToken) &&
|
|
200
287
|
!permittedKeys.includes(firstToken)) {
|
|
201
288
|
return unreachable(`addresses call-inputs ${transportChannel} field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-11's version equality, performed by the one reader that has the contract.
|
|
3
|
+
*
|
|
4
|
+
* AD-11's rule is that "readers accept an equal `schemaVersion` only and throw
|
|
5
|
+
* `schema-version-mismatch` outside that". `lineage.ts` deliberately keeps the
|
|
6
|
+
* field a plain integer rather than a literal, so that a wrong version arrives
|
|
7
|
+
* as AD-28's named fault instead of an anonymous parse failure, which puts the
|
|
8
|
+
* comparison on whoever reads the artifact. For the eval contract nobody did:
|
|
9
|
+
* a contract stamped 3 parsed, compiled, sealed, and put its stale version into
|
|
10
|
+
* the scoring version digest, and AD-11 exists to keep that number comparable.
|
|
11
|
+
*
|
|
12
|
+
* `RuntimeFault` rather than `StructuralFailure`, against the grain of every
|
|
13
|
+
* other check in this directory. The two registries are disjoint by the
|
|
14
|
+
* Consistency Conventions, AD-5 has no version code, and AD-11 names
|
|
15
|
+
* `schema-version-mismatch` literally. Minting an AD-5 twin would give one
|
|
16
|
+
* condition two codes and make a caller's handler depend on which reader
|
|
17
|
+
* happened to notice first.
|
|
18
|
+
*/
|
|
19
|
+
import { RuntimeFault } from '../schemas/faults.js';
|
|
20
|
+
/** `schema-version-mismatch`: the stamp is not the version this build reads. */
|
|
21
|
+
export function checkSchemaVersion(stamped, accepted, artifactPath) {
|
|
22
|
+
if (stamped === accepted)
|
|
23
|
+
return;
|
|
24
|
+
throw new RuntimeFault('schema-version-mismatch', artifactPath, `carries "schemaVersion" ${stamped} where this build reads ${accepted}; a contract written for another version is not read leniently, since its stale version would travel into the scoring version (AD-11)`);
|
|
25
|
+
}
|
|
@@ -4,26 +4,45 @@
|
|
|
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
14
|
import { declaresNoRequestKeys } from '../declared-inputs.ts';
|
|
15
15
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
16
|
-
import type {
|
|
17
|
-
import {
|
|
18
|
-
import type
|
|
16
|
+
import type { AnyOperation } from '../schemas/interface.ts';
|
|
17
|
+
import type { InputChannelName } from '../schemas/pointer.ts';
|
|
18
|
+
import { type ApiWitnessInputs, type WitnessChannel, type WitnessInputs } from '../schemas/sensitivity-witness.ts';
|
|
19
19
|
export { declaresNoRequestKeys };
|
|
20
20
|
/**
|
|
21
|
-
* The
|
|
22
|
-
*
|
|
23
|
-
* declared as a keyed shape, so such a leg omits every required key rather
|
|
24
|
-
* than being exempt from the comparison.
|
|
21
|
+
* The transport spelling, for the consumers that can only send one: the probe
|
|
22
|
+
* port carries a method, a path template, and the four transport channels.
|
|
25
23
|
*/
|
|
26
|
-
export declare
|
|
24
|
+
export declare const isApiWitnessInputs: (inputs: WitnessInputs) => inputs is ApiWitnessInputs;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the leg supplies this channel as an undifferentiated stream.
|
|
27
|
+
*
|
|
28
|
+
* Standard input written as text is one such stream. A `KeyedShapeDescriptor`
|
|
29
|
+
* can say what keys a channel carries and cannot say "one opaque stream", so a
|
|
30
|
+
* command that reads a prompt is declared by naming the one thing the stream
|
|
31
|
+
* carries, and the leg supplies that thing as the text it actually is.
|
|
32
|
+
*/
|
|
33
|
+
export declare function suppliesOpaquely(inputs: WitnessInputs, channel: InputChannelName): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The keys one set of witness inputs supplies on one channel, or none where
|
|
36
|
+
* the leg's spelling has no such channel.
|
|
37
|
+
*
|
|
38
|
+
* `body` and `stdin` are the two channels a leg supplies as a tagged value
|
|
39
|
+
* rather than as a key map, because a leg has to tell an absent value from one
|
|
40
|
+
* carrying JSON null. This function is the bridge between that spelling and
|
|
41
|
+
* the keyed shape the request declares.
|
|
42
|
+
*/
|
|
43
|
+
export declare function suppliedKeys(inputs: WitnessInputs, channel: InputChannelName): readonly string[];
|
|
44
|
+
/** One channel's supplied value, for the differential comparison. */
|
|
45
|
+
export declare function suppliedValue(inputs: WitnessInputs, channel: WitnessChannel): unknown;
|
|
27
46
|
/**
|
|
28
47
|
* Every channel of one set of witness inputs against the operation that will
|
|
29
48
|
* receive them. All four, not only the differential channel: `planPreflight`
|
|
@@ -49,7 +68,7 @@ export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportCh
|
|
|
49
68
|
* contract artifact, the surface AD-18 governs; a placeholder satisfies this
|
|
50
69
|
* check, and what pre-flight probes is a fixture.
|
|
51
70
|
*/
|
|
52
|
-
export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation:
|
|
71
|
+
export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: AnyOperation, owner: string, artifactPath: string): void;
|
|
53
72
|
/**
|
|
54
73
|
* Strict-gated, alongside `checkUndeclaredMandatoryInput`, whose code this
|
|
55
74
|
* shares: `compile.ts` already gates that code behind `options.strict`, and one
|
|
@@ -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
|
+
}
|