eval-quality 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -67
- package/corpus/dev/README.md +17 -11
- package/corpus/dev/compile-seal-example/brief.json +1 -1
- package/corpus/dev/compile-seal-example/contract.json +1 -1
- package/corpus/dev/contracts/absent-collection-locations.json +1 -1
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
- package/corpus/dev/contracts/absent-success-indicator.json +1 -1
- package/corpus/dev/contracts/empty-channel-roles.json +1 -1
- package/corpus/dev/contracts/empty-collection-locations.json +1 -1
- package/corpus/dev/contracts/empty-request-shapes.json +1 -1
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
- package/corpus/dev/contracts/no-operation-inventory.json +1 -1
- package/corpus/dev/contracts/no-read-back-relation.json +1 -1
- package/corpus/dev/contracts/no-state-change-marker.json +1 -1
- package/corpus/dev/contracts/no-type-violating-step.json +1 -1
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/satisfied-declarations.json +1 -1
- package/corpus/dev/contracts/single-required-response-key.json +1 -1
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
- package/corpus/dev/index.json +1 -1
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +1 -0
- package/dist/application/score.d.ts +48 -0
- package/dist/application/score.js +186 -0
- package/dist/cli/arguments.d.ts +7 -3
- package/dist/cli/arguments.js +73 -5
- package/dist/cli/exit-codes.d.ts +6 -2
- package/dist/cli/exit-codes.js +8 -9
- package/dist/cli/main.js +9 -0
- package/dist/cli/render.d.ts +1 -1
- package/dist/cli/render.js +6 -4
- package/dist/cli/run.d.ts +13 -3
- package/dist/cli/run.js +136 -16
- package/dist/core/canonical/canonicalize.d.ts +12 -0
- package/dist/core/canonical/canonicalize.js +18 -4
- package/dist/core/compile/bindings.d.ts +70 -0
- package/dist/core/compile/bindings.js +317 -0
- package/dist/core/compile/compile.d.ts +5 -0
- package/dist/core/compile/compile.js +9 -0
- package/dist/core/compile/expression-legality.d.ts +49 -0
- package/dist/core/compile/expression-legality.js +105 -31
- package/dist/core/compile/forbidden-inputs.d.ts +8 -1
- package/dist/core/compile/forbidden-inputs.js +16 -3
- package/dist/core/compile/interface-inventory.d.ts +23 -1
- package/dist/core/compile/interface-inventory.js +32 -3
- package/dist/core/compile/reachability.d.ts +18 -1
- package/dist/core/compile/reachability.js +46 -1
- package/dist/core/compile/sensitivity-witness.d.ts +1 -1
- package/dist/core/compile/sensitivity-witness.js +1 -1
- package/dist/core/emit/emit.d.ts +9 -0
- package/dist/core/emit/emit.js +214 -0
- package/dist/core/emit/private-artifact-digest.d.ts +11 -0
- package/dist/core/emit/private-artifact-digest.js +31 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
- package/dist/core/evaluate/evidence-resolution.js +4 -1
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +4 -2
- package/dist/core/ingest/conditions.d.ts +201 -0
- package/dist/core/ingest/conditions.js +48 -0
- package/dist/core/ingest/index.d.ts +13 -0
- package/dist/core/ingest/index.js +11 -0
- package/dist/core/ingest/ingest.d.ts +68 -0
- package/dist/core/ingest/ingest.js +310 -0
- package/dist/core/lineage/stage-table.d.ts +21 -4
- package/dist/core/lineage/stage-table.js +35 -3
- package/dist/core/preflight/witness-evidence.js +10 -0
- package/dist/core/schemas/artifact.d.ts +385 -14
- package/dist/core/schemas/constraint-ledger.js +40 -0
- package/dist/core/schemas/defect-signature.d.ts +273 -0
- package/dist/core/schemas/defect-signature.js +127 -0
- package/dist/core/schemas/eval-contract.d.ts +33 -0
- package/dist/core/schemas/eval-contract.js +14 -0
- package/dist/core/schemas/evidence-artifact.d.ts +99 -19
- package/dist/core/schemas/evidence-artifact.js +64 -8
- package/dist/core/schemas/plan.d.ts +112 -0
- package/dist/core/schemas/plan.js +67 -2
- package/dist/core/schemas/probe-qualification.d.ts +168 -0
- package/dist/core/schemas/probe-qualification.js +106 -0
- package/dist/core/schemas/probe.d.ts +312 -0
- package/dist/core/schemas/probe.js +6 -2
- package/dist/core/schemas/scoring-policy.d.ts +1 -0
- package/dist/core/schemas/scoring-policy.js +6 -1
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
- package/dist/core/schemas/sealed-run-record.d.ts +30 -3
- package/dist/core/schemas/sealed-run-record.js +37 -5
- package/dist/core/score/binding-order.d.ts +31 -0
- package/dist/core/score/binding-order.js +88 -0
- package/dist/core/score/bindings.d.ts +93 -0
- package/dist/core/score/bindings.js +297 -0
- package/dist/core/score/ladder-table.d.ts +23 -0
- package/dist/core/score/ladder-table.js +109 -0
- package/dist/core/score/ladder.d.ts +229 -0
- package/dist/core/score/ladder.js +464 -0
- package/dist/core/score/mode-agreement.d.ts +30 -0
- package/dist/core/score/mode-agreement.js +16 -0
- package/dist/core/score/outcome-table.d.ts +17 -0
- package/dist/core/score/outcome-table.js +172 -0
- package/dist/core/score/outcome.d.ts +466 -0
- package/dist/core/score/outcome.js +562 -0
- package/dist/core/score/qualification.d.ts +83 -0
- package/dist/core/score/qualification.js +615 -0
- package/dist/core/score/quotation.d.ts +79 -0
- package/dist/core/score/quotation.js +201 -0
- package/dist/core/score/reduce-trials.d.ts +80 -0
- package/dist/core/score/reduce-trials.js +90 -0
- package/dist/core/score/score.d.ts +72 -0
- package/dist/core/score/score.js +581 -0
- package/dist/core/score/selection.d.ts +80 -0
- package/dist/core/score/selection.js +98 -0
- package/dist/core/score/strength.d.ts +49 -0
- package/dist/core/score/strength.js +193 -0
- package/dist/core/score/witness.d.ts +117 -0
- package/dist/core/score/witness.js +315 -0
- package/dist/core/seal/derived-reference.d.ts +1 -1
- package/dist/core/seal/derived-reference.js +236 -28
- package/dist/core/seal/seal.js +8 -3
- package/dist/core/stage-contracts.d.ts +62 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/package.json +12 -3
- package/schemas/eval-contract.schema.json +100 -2
- package/schemas/evidence-artifact.schema.json +96 -9
- package/schemas/probe.schema.json +508 -3
- package/schemas/scoring-policy.schema.json +8 -1
- package/schemas/sealed-evaluator-brief.schema.json +11 -1
- package/schemas/sealed-run-record.schema.json +19 -2
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owed item 3's compile-time checks over captured input bindings:
|
|
3
|
+
* `binding-cycle` over the ordering graph, `captured-channel-undeclared` on a
|
|
4
|
+
* pointer naming any channel but `response-body`, and the typed residue that
|
|
5
|
+
* fires the shipped `unreachable-check-evidence`.
|
|
6
|
+
*
|
|
7
|
+
* The three run at two registry positions. `checkCapturedReachability` fires
|
|
8
|
+
* `unreachable-check-evidence`, whose rung is third, so it runs beside
|
|
9
|
+
* `checkEvidenceReachability`; the two new codes run at their own inserted
|
|
10
|
+
* position. Running captured reachability late would let a lower-ranked code
|
|
11
|
+
* win on a contract carrying both defects, which `compile.ts`'s own documented
|
|
12
|
+
* priority forbids.
|
|
13
|
+
*
|
|
14
|
+
* `evaluatePointerReachability` decides the step, operation, body-key, and
|
|
15
|
+
* scalar-descent half. The scalar determination, the one-segment tail rule, and
|
|
16
|
+
* type equality are this module's own.
|
|
17
|
+
*/
|
|
18
|
+
import { ARRAY_INDEX_PATTERN } from '../evaluate/evidence-resolution.js';
|
|
19
|
+
import { StructuralFailure } from '../failure-codes.js';
|
|
20
|
+
import { TRANSPORT_CHANNELS, } from '../schemas/pointer.js';
|
|
21
|
+
import { JsonTypeName } from '../schemas/primitives.js';
|
|
22
|
+
import { buildPlanIndex, parseEvidenceTarget, resolveOperation, resolveStep, } from '../seal/plan-index.js';
|
|
23
|
+
import { evaluatePointerReachability } from './reachability.js';
|
|
24
|
+
/**
|
|
25
|
+
* The one channel a captured pointer may name. `ResponseDescriptor` declares
|
|
26
|
+
* `requiredKeys`, `permittedKeys`, `types`, `successIndicator`, `channelRoles`,
|
|
27
|
+
* and `collectionLocations`, every one of them about the body, so the body is
|
|
28
|
+
* the one channel it declares and the criterion's "a channel the referenced
|
|
29
|
+
* operation's response descriptor does not declare" is literally true of the
|
|
30
|
+
* other six. Admitting `response-headers` and `response-status` was tried and
|
|
31
|
+
* dropped: their types would have to be invented by fiat, and
|
|
32
|
+
* `Observation.responseHeaders` admits objects, arrays, numbers, and `null`, so
|
|
33
|
+
* a header capture compiled as a `string` could resolve to an object at score
|
|
34
|
+
* time.
|
|
35
|
+
*/
|
|
36
|
+
const CAPTURABLE_CHANNEL = 'response-body';
|
|
37
|
+
/**
|
|
38
|
+
* Every captured binding one step declares, in fixed transport-channel order
|
|
39
|
+
* then by key name, so which binding a check reports never depends on a
|
|
40
|
+
* caller-keyed map's insertion order. Exported because `score/binding-order.ts`
|
|
41
|
+
* and `score/bindings.ts` need the same reading of which bindings are captures.
|
|
42
|
+
*/
|
|
43
|
+
export function capturedBindings(step) {
|
|
44
|
+
const captures = [];
|
|
45
|
+
for (const transportChannel of TRANSPORT_CHANNELS) {
|
|
46
|
+
const map = step.inputBinding[transportChannel];
|
|
47
|
+
if (map === null)
|
|
48
|
+
continue;
|
|
49
|
+
for (const key of Object.keys(map).sort()) {
|
|
50
|
+
const value = map[key];
|
|
51
|
+
if (value === undefined || !('captured' in value))
|
|
52
|
+
continue;
|
|
53
|
+
captures.push({
|
|
54
|
+
transportChannel,
|
|
55
|
+
key,
|
|
56
|
+
pointer: value.captured,
|
|
57
|
+
target: parseEvidenceTarget(value.captured),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return captures;
|
|
62
|
+
}
|
|
63
|
+
// The one shipped precedent for addressing a caller-keyed binding entry
|
|
64
|
+
// (`interface-inventory.ts`'s undeclared-key throw), reused so the two new
|
|
65
|
+
// codes and the widened `undeclared-mandatory-input` agree on one spelling.
|
|
66
|
+
function bindingPath(step, capture) {
|
|
67
|
+
return `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${capture.transportChannel}[${JSON.stringify(capture.key)}]`;
|
|
68
|
+
}
|
|
69
|
+
// ---- binding-cycle -------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* `binding-cycle`: a cycle over the union of the capture edges and AD-39's
|
|
72
|
+
* `after` edges that contains at least one capture edge.
|
|
73
|
+
*
|
|
74
|
+
* Both edge kinds assert that this step's observation comes after that one, so
|
|
75
|
+
* a capture B to A together with `A.after = B` is an unsatisfiable ordering
|
|
76
|
+
* while each graph alone stays acyclic. A cycle made only of `after` edges is
|
|
77
|
+
* left to `checkNestedTemporalClause`, which already catches every one of them,
|
|
78
|
+
* so requiring a capture edge in the cycle keeps the two codes disjoint.
|
|
79
|
+
*
|
|
80
|
+
* Decided by strongly connected components rather than by a depth-first walk
|
|
81
|
+
* that inspects each back edge. A cycle lies entirely inside one component, and
|
|
82
|
+
* a capture edge whose endpoints share a component is closed by a path back
|
|
83
|
+
* through that component, so "some cycle contains a capture edge" is exactly
|
|
84
|
+
* "some capture edge has both endpoints in one component". A self-capture is
|
|
85
|
+
* the singleton case and needs no rule of its own. The depth-first form was
|
|
86
|
+
* written first and rejected: settling a node after rejecting a pure-`after`
|
|
87
|
+
* back edge can hide a mixed cycle a later root would reach, and whether it
|
|
88
|
+
* does depends on the order edges happen to be enumerated in, which is an
|
|
89
|
+
* invariant nothing states.
|
|
90
|
+
*
|
|
91
|
+
* Nodes are the plan's declared step ids; an edge whose target no step declares
|
|
92
|
+
* is dropped, since a dangling reference is not a cycle and AD-39 makes a
|
|
93
|
+
* dangling `after` permissive besides. The binding reported is the first, in
|
|
94
|
+
* the plan's own declaration order, whose endpoints share a component.
|
|
95
|
+
*/
|
|
96
|
+
export function checkBindingCycle(contract) {
|
|
97
|
+
const declaredStepIds = new Set(contract.interactionPlan.map((step) => step.stepId));
|
|
98
|
+
const edgesFrom = new Map();
|
|
99
|
+
const addEdge = (from, to) => {
|
|
100
|
+
const list = edgesFrom.get(from);
|
|
101
|
+
if (list === undefined)
|
|
102
|
+
edgesFrom.set(from, [to]);
|
|
103
|
+
else
|
|
104
|
+
list.push(to);
|
|
105
|
+
};
|
|
106
|
+
for (const step of contract.interactionPlan) {
|
|
107
|
+
for (const capture of capturedBindings(step)) {
|
|
108
|
+
if (declaredStepIds.has(capture.target.stepId)) {
|
|
109
|
+
addEdge(step.stepId, capture.target.stepId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (step.after !== null && declaredStepIds.has(step.after)) {
|
|
113
|
+
addEdge(step.stepId, step.after);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const component = stronglyConnectedComponents(declaredStepIds, edgesFrom);
|
|
117
|
+
for (const step of contract.interactionPlan) {
|
|
118
|
+
for (const capture of capturedBindings(step)) {
|
|
119
|
+
const target = capture.target.stepId;
|
|
120
|
+
if (!declaredStepIds.has(target))
|
|
121
|
+
continue;
|
|
122
|
+
if (component.get(step.stepId) !== component.get(target))
|
|
123
|
+
continue;
|
|
124
|
+
throw new StructuralFailure('binding-cycle', bindingPath(step, capture), `captured pointer "${capture.pointer}" closes a cycle over the capture and temporal-clause edges; a captured value has no earlier step to resolve from (AD-39)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Tarjan's algorithm, returning each node's component identifier. */
|
|
129
|
+
function stronglyConnectedComponents(nodes, edgesFrom) {
|
|
130
|
+
const order = new Map();
|
|
131
|
+
const lowLink = new Map();
|
|
132
|
+
const onStack = new Set();
|
|
133
|
+
const stack = [];
|
|
134
|
+
const component = new Map();
|
|
135
|
+
let nextOrder = 0;
|
|
136
|
+
let nextComponent = 0;
|
|
137
|
+
const connect = (node) => {
|
|
138
|
+
order.set(node, nextOrder);
|
|
139
|
+
lowLink.set(node, nextOrder);
|
|
140
|
+
nextOrder += 1;
|
|
141
|
+
stack.push(node);
|
|
142
|
+
onStack.add(node);
|
|
143
|
+
for (const next of edgesFrom.get(node) ?? []) {
|
|
144
|
+
if (!order.has(next)) {
|
|
145
|
+
connect(next);
|
|
146
|
+
lowLink.set(node, Math.min(lowLink.get(node) ?? 0, lowLink.get(next) ?? 0));
|
|
147
|
+
}
|
|
148
|
+
else if (onStack.has(next)) {
|
|
149
|
+
lowLink.set(node, Math.min(lowLink.get(node) ?? 0, order.get(next) ?? 0));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (lowLink.get(node) !== order.get(node))
|
|
153
|
+
return;
|
|
154
|
+
const id = nextComponent;
|
|
155
|
+
nextComponent += 1;
|
|
156
|
+
for (;;) {
|
|
157
|
+
const member = stack.pop();
|
|
158
|
+
if (member === undefined)
|
|
159
|
+
break;
|
|
160
|
+
onStack.delete(member);
|
|
161
|
+
component.set(member, id);
|
|
162
|
+
if (member === node)
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
for (const node of nodes) {
|
|
167
|
+
if (!order.has(node))
|
|
168
|
+
connect(node);
|
|
169
|
+
}
|
|
170
|
+
return component;
|
|
171
|
+
}
|
|
172
|
+
// ---- captured-channel-undeclared -----------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* `captured-channel-undeclared`: a captured pointer naming any AD-26 channel
|
|
175
|
+
* but `response-body`. `call-inputs` addresses a step's own request,
|
|
176
|
+
* `stdout`, `stderr`, and `exit-code` are process channels no operation
|
|
177
|
+
* surviving `unsupported-interface-kind` produces, and `response-headers` and
|
|
178
|
+
* `response-status` have no declared structure to give a captured value a
|
|
179
|
+
* type.
|
|
180
|
+
*/
|
|
181
|
+
export function checkCapturedChannel(contract) {
|
|
182
|
+
for (const step of contract.interactionPlan) {
|
|
183
|
+
for (const capture of capturedBindings(step)) {
|
|
184
|
+
if (capture.target.channel === CAPTURABLE_CHANNEL)
|
|
185
|
+
continue;
|
|
186
|
+
throw new StructuralFailure('captured-channel-undeclared', bindingPath(step, capture), `captured pointer "${capture.pointer}" names the ${capture.target.channel} channel, which the referenced operation's response descriptor does not declare (AD-26)`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ---- the typed residue: unreachable-check-evidence -----------------------
|
|
191
|
+
const SCALAR_TYPES = new Set(JsonTypeName.options.filter((name) => name !== 'object' && name !== 'array'));
|
|
192
|
+
/**
|
|
193
|
+
* The type a captured pointer resolves to, or why nothing scalar is declared
|
|
194
|
+
* there. The tail must be exactly one segment: `ResponseDescriptor.types` is
|
|
195
|
+
* keyed by plain key name, so a nested segment has no declared type to compare
|
|
196
|
+
* against, and an array element has none either even where
|
|
197
|
+
* `evaluatePointerReachability` admits the index against a root collection.
|
|
198
|
+
*/
|
|
199
|
+
function capturedType(target, index) {
|
|
200
|
+
const segments = target.tail.length;
|
|
201
|
+
if (segments !== 1) {
|
|
202
|
+
return {
|
|
203
|
+
reason: `addresses ${segments === 0 ? 'the whole response body' : `a response-body path ${segments} segments deep`}, which declares no scalar to capture`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const key = target.tail[0];
|
|
207
|
+
if (key === undefined) {
|
|
208
|
+
// Unreachable: the length check above guarantees one segment.
|
|
209
|
+
throw new TypeError('captured pointer tail is one segment but is empty');
|
|
210
|
+
}
|
|
211
|
+
if (ARRAY_INDEX_PATTERN.test(key)) {
|
|
212
|
+
return {
|
|
213
|
+
reason: `addresses response-body element ${key}, which no declaration gives a type`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const step = resolveStep(index, target.stepId);
|
|
217
|
+
const operation = resolveOperation(index, step.operationId);
|
|
218
|
+
const declared = operation.responseDescriptor.types[key];
|
|
219
|
+
if (declared === undefined || declared === null) {
|
|
220
|
+
return {
|
|
221
|
+
reason: `addresses response-body field "${key}", whose type operation "${operation.operationId}" ${declared === undefined ? 'does not declare' : 'declares indeterminate'}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (!SCALAR_TYPES.has(declared)) {
|
|
225
|
+
return {
|
|
226
|
+
reason: `addresses response-body field "${key}", which operation "${operation.operationId}" declares "${declared}" rather than a scalar`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return { type: declared };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The type the bound parameter itself is declared as. An indeterminate
|
|
233
|
+
* declaration on either side is not equal to anything and fails closed, which
|
|
234
|
+
* is AD-31's disposition for an indeterminate descriptor. A step whose own
|
|
235
|
+
* operation does not resolve is skipped, matching
|
|
236
|
+
* `checkUndeclaredMandatoryInput`: that defect belongs to a separate
|
|
237
|
+
* cross-field rule.
|
|
238
|
+
*/
|
|
239
|
+
function boundParameterType(step, capture, index) {
|
|
240
|
+
const operation = index.operationOf(step.operationId);
|
|
241
|
+
if (operation === undefined)
|
|
242
|
+
return null;
|
|
243
|
+
const shape = operation.requestShape[capture.transportChannel];
|
|
244
|
+
// A key the operation declares in neither list is an input the contract did
|
|
245
|
+
// not declare, which is `undeclared-mandatory-input`'s and strict-only under
|
|
246
|
+
// AD-4. This check runs unconditionally, so claiming it here would reject a
|
|
247
|
+
// non-strict contract under a code that names a different defect, and would
|
|
248
|
+
// treat a captured binding more harshly than the same key bound to a literal.
|
|
249
|
+
if (!shape.requiredKeys.includes(capture.key) &&
|
|
250
|
+
!shape.permittedKeys.includes(capture.key)) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const declared = shape.types[capture.key];
|
|
254
|
+
if (declared === undefined || declared === null) {
|
|
255
|
+
return {
|
|
256
|
+
reason: `binds ${capture.transportChannel} parameter "${capture.key}", whose type operation "${operation.operationId}" ${declared === undefined ? 'does not declare' : 'declares indeterminate'}, so no type equality is decidable`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return { type: declared };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `unreachable-check-evidence` over the captured-pointer residue: an
|
|
263
|
+
* unresolvable step or operation, an undeclared body key, a tail that is not
|
|
264
|
+
* exactly one segment, an array index, an absent, indeterminate, or non-scalar
|
|
265
|
+
* declared type, and a declared type that does not equal the bound
|
|
266
|
+
* parameter's.
|
|
267
|
+
*
|
|
268
|
+
* A capture naming another channel is reported here only for the two conditions
|
|
269
|
+
* that are decidable without a response descriptor: an unresolvable step and an
|
|
270
|
+
* unresolvable operation. Everything else about such a pointer is
|
|
271
|
+
* `checkCapturedChannel`'s. That split is what lets this check run at the
|
|
272
|
+
* registry's third rung without shadowing the fifteenth, and it keeps the
|
|
273
|
+
* higher-ranked code first on a pointer that is unresolvable and off-body at
|
|
274
|
+
* once.
|
|
275
|
+
*/
|
|
276
|
+
export function checkCapturedReachability(contract) {
|
|
277
|
+
let index;
|
|
278
|
+
for (const step of contract.interactionPlan) {
|
|
279
|
+
for (const capture of capturedBindings(step)) {
|
|
280
|
+
const path = bindingPath(step, capture);
|
|
281
|
+
index ??= buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
282
|
+
// Resolved before the channel test, so a pointer that is both
|
|
283
|
+
// unresolvable and on a non-body channel reports the higher-ranked
|
|
284
|
+
// code. Deferring this to `checkCapturedChannel` would fire index 15
|
|
285
|
+
// where AD-5's order gives index 2, and would assert something about
|
|
286
|
+
// a response descriptor that no declared operation supplies. The
|
|
287
|
+
// wording matches `evaluatePointerReachability`'s own two reasons, so
|
|
288
|
+
// one defect reads the same however it is reached.
|
|
289
|
+
const referenced = index.stepOf(capture.target.stepId);
|
|
290
|
+
if (referenced === undefined) {
|
|
291
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `captured pointer "${capture.pointer}" names a step the interaction plan does not declare`);
|
|
292
|
+
}
|
|
293
|
+
if (index.operationOf(referenced.operationId) === undefined) {
|
|
294
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `captured pointer "${capture.pointer}" names step "${capture.target.stepId}", which names operation "${referenced.operationId}", not declared by any permitted interface`);
|
|
295
|
+
}
|
|
296
|
+
if (capture.target.channel !== CAPTURABLE_CHANNEL)
|
|
297
|
+
continue;
|
|
298
|
+
const reachability = evaluatePointerReachability(capture.pointer, index);
|
|
299
|
+
if (!reachability.reachable) {
|
|
300
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `captured pointer "${capture.pointer}" ${reachability.reason}`);
|
|
301
|
+
}
|
|
302
|
+
const captured = capturedType(capture.target, index);
|
|
303
|
+
if (captured.type === undefined) {
|
|
304
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `captured pointer "${capture.pointer}" ${captured.reason}`);
|
|
305
|
+
}
|
|
306
|
+
const bound = boundParameterType(step, capture, index);
|
|
307
|
+
if (bound === null)
|
|
308
|
+
continue;
|
|
309
|
+
if (bound.type === undefined) {
|
|
310
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `captured pointer "${capture.pointer}" resolves to a declared "${captured.type}", but the step ${bound.reason}`);
|
|
311
|
+
}
|
|
312
|
+
if (bound.type !== captured.type) {
|
|
313
|
+
throw new StructuralFailure('unreachable-check-evidence', path, `captured pointer "${capture.pointer}" resolves to a declared "${captured.type}", which is not the "${bound.type}" the bound ${capture.transportChannel} parameter "${capture.key}" is declared as`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* code; the other two witness checks fire different codes and run
|
|
14
14
|
* unconditionally, after the registry order, since AD-5 names no rung for them.
|
|
15
15
|
*
|
|
16
|
+
* `checkCapturedReachability` fires the shipped `unreachable-check-evidence`,
|
|
17
|
+
* so it runs at that code's own third rung, beside `checkEvidenceReachability`.
|
|
18
|
+
* Grouping it with the two codes owed item 3 added, at the inserted position
|
|
19
|
+
* below, would let a lower-ranked code win on a contract carrying both defects.
|
|
20
|
+
*
|
|
16
21
|
* `checkRubricIdentifiers` fires `rubric-unanchored` but runs ahead of
|
|
17
22
|
* `checkRubricReasoningProse`, which outranks it in the registry. A duplicated
|
|
18
23
|
* rubric or criterion id makes every `rubrics[id=...]` path the other three
|
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
* code; the other two witness checks fire different codes and run
|
|
14
14
|
* unconditionally, after the registry order, since AD-5 names no rung for them.
|
|
15
15
|
*
|
|
16
|
+
* `checkCapturedReachability` fires the shipped `unreachable-check-evidence`,
|
|
17
|
+
* so it runs at that code's own third rung, beside `checkEvidenceReachability`.
|
|
18
|
+
* Grouping it with the two codes owed item 3 added, at the inserted position
|
|
19
|
+
* below, would let a lower-ranked code win on a contract carrying both defects.
|
|
20
|
+
*
|
|
16
21
|
* `checkRubricIdentifiers` fires `rubric-unanchored` but runs ahead of
|
|
17
22
|
* `checkRubricReasoningProse`, which outranks it in the registry. A duplicated
|
|
18
23
|
* rubric or criterion id makes every `rubrics[id=...]` path the other three
|
|
19
24
|
* rubric checks emit address two things, so identifiers are settled before any
|
|
20
25
|
* of them reports.
|
|
21
26
|
*/
|
|
27
|
+
import { checkBindingCycle, checkCapturedChannel, checkCapturedReachability, } from './bindings.js';
|
|
22
28
|
import { checkObservableSuccessCriterion, checkRequirementLinkage, } from './declarations.js';
|
|
23
29
|
import { checkOperandLegality, checkQuantifierNesting, checkQuantifierOverNonCollection, checkReferenceSetResolution, checkRegexConstructs, } from './expression-legality.js';
|
|
24
30
|
import { checkForbiddenInputFloor, checkScopedResourceReferences, } from './forbidden-inputs.js';
|
|
@@ -33,6 +39,7 @@ export function compile(contract, options) {
|
|
|
33
39
|
checkRequirementLinkage(contract);
|
|
34
40
|
checkObservableSuccessCriterion(contract);
|
|
35
41
|
checkEvidenceReachability(contract);
|
|
42
|
+
checkCapturedReachability(contract);
|
|
36
43
|
checkBoundElementScope(contract);
|
|
37
44
|
checkOperandLegality(contract);
|
|
38
45
|
checkRegexConstructs(contract);
|
|
@@ -49,6 +56,8 @@ export function compile(contract, options) {
|
|
|
49
56
|
checkInterfaceKind(contract);
|
|
50
57
|
checkNestedTemporalClause(contract);
|
|
51
58
|
checkScriptingBound(contract);
|
|
59
|
+
checkBindingCycle(contract);
|
|
60
|
+
checkCapturedChannel(contract);
|
|
52
61
|
checkRubricIdentifiers(contract);
|
|
53
62
|
checkRubricReasoningProse(contract);
|
|
54
63
|
checkRubricAnchoring(contract);
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
|
+
import type { Expression, Operand, SetOperand } from '../schemas/expression.ts';
|
|
3
|
+
import type { Operation } from '../schemas/interface.ts';
|
|
4
|
+
type OperandPosition = 0 | 1 | 'collection';
|
|
5
|
+
type OperandBearingOp = Exclude<Expression['op'], 'not' | 'all' | 'any'>;
|
|
6
|
+
type Visitor = {
|
|
7
|
+
onOperand?: (operand: Operand, op: OperandBearingOp, position: OperandPosition, path: string, quantifierDepth: number) => void;
|
|
8
|
+
onSetOperand?: (setOperand: SetOperand, path: string) => void;
|
|
9
|
+
onQuantifier?: (expr: {
|
|
10
|
+
op: 'for-all' | 'for-any';
|
|
11
|
+
collection: Operand;
|
|
12
|
+
predicate: Expression;
|
|
13
|
+
}, path: string, quantifierDepth: number) => void;
|
|
14
|
+
onCoversByKey?: (expr: Extract<Expression, {
|
|
15
|
+
op: 'covers-by-key';
|
|
16
|
+
}>, path: string, quantifierDepth: number) => void;
|
|
17
|
+
onRegex?: (pattern: string, path: string) => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The one expression walker in this package. Contract-free and a superset of
|
|
21
|
+
* the narrower walks the checks below need, so a probe-side legality pass
|
|
22
|
+
* reuses it rather than adding a fifth traversal that can drift from it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function walkExpression(expr: Expression, quantifierDepth: number, path: string, visitor: Visitor): void;
|
|
25
|
+
/**
|
|
26
|
+
* `checkOperandLegality` over one bare `Expression`, minus the `covers-by-key`
|
|
27
|
+
* `expectedKey` uniqueness rule, which reads the contract's declared reference
|
|
28
|
+
* sets and has no probe-side form: a probe-side condition may carry no
|
|
29
|
+
* `{ referenceSet }` operand at all.
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkExpressionOperandLegality(expression: Expression, artifactPath: string): void;
|
|
32
|
+
/** `checkRegexConstructs` over one bare `Expression`. */
|
|
33
|
+
export declare function checkExpressionRegexConstructs(expression: Expression, artifactPath: string): void;
|
|
34
|
+
/** `checkQuantifierNesting` over one bare `Expression`. */
|
|
35
|
+
export declare function checkExpressionQuantifierNesting(expression: Expression, artifactPath: string): void;
|
|
2
36
|
/** Checks each operator's operands against its position-specific constraints. */
|
|
3
37
|
export declare function checkOperandLegality(contract: EvalContract): void;
|
|
4
38
|
/** Rejects invalid regexes, partial anchors, backreferences, and lookbehind. */
|
|
5
39
|
export declare function checkRegexConstructs(contract: EvalContract): void;
|
|
6
40
|
/** Rejects nested quantifiers and covers-by-key inside a quantifier. */
|
|
7
41
|
export declare function checkQuantifierNesting(contract: EvalContract): void;
|
|
42
|
+
/**
|
|
43
|
+
* `checkQuantifierOverNonCollection` over one bare `Expression` rooted at a
|
|
44
|
+
* known set of step identifiers, all of which resolve to one operation. The
|
|
45
|
+
* `legIds` argument is load-bearing: omit it and the check silently no-ops on
|
|
46
|
+
* every expression, which is the trap the witness branch below already warns
|
|
47
|
+
* about. It reads the operation's declared response types, never its
|
|
48
|
+
* `collectionLocations`; a declared non-array type beats a contradictory
|
|
49
|
+
* collection location, and a re-derivation that read the other field would
|
|
50
|
+
* disagree with the compiler on a committed fixture.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkExpressionQuantifierOverNonCollection(expression: Expression, artifactPath: string, scope: {
|
|
53
|
+
readonly operation: Operation;
|
|
54
|
+
readonly legIds: readonly string[];
|
|
55
|
+
}): void;
|
|
8
56
|
/** Checks response-body collection pointers, after bound-element substitution. */
|
|
9
57
|
export declare function checkQuantifierOverNonCollection(contract: EvalContract): void;
|
|
10
58
|
/**
|
|
@@ -12,3 +60,4 @@ export declare function checkQuantifierOverNonCollection(contract: EvalContract)
|
|
|
12
60
|
* This preserves legal identifiers such as `constructor`.
|
|
13
61
|
*/
|
|
14
62
|
export declare function checkReferenceSetResolution(contract: EvalContract): void;
|
|
63
|
+
export {};
|
|
@@ -7,7 +7,12 @@ import { StructuralFailure } from '../failure-codes.js';
|
|
|
7
7
|
import { JsonTypeName } from '../schemas/primitives.js';
|
|
8
8
|
import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
9
9
|
import { substitutePointer } from './oracle-alignment.js';
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The one expression walker in this package. Contract-free and a superset of
|
|
12
|
+
* the narrower walks the checks below need, so a probe-side legality pass
|
|
13
|
+
* reuses it rather than adding a fifth traversal that can drift from it.
|
|
14
|
+
*/
|
|
15
|
+
export function walkExpression(expr, quantifierDepth, path, visitor) {
|
|
11
16
|
switch (expr.op) {
|
|
12
17
|
case 'not':
|
|
13
18
|
walkExpression(expr.operands[0], quantifierDepth, `${path}.operands[0]`, visitor);
|
|
@@ -116,24 +121,71 @@ const OPERAND_LEGALITY = {
|
|
|
116
121
|
const artifactKey = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
117
122
|
? `.${key}`
|
|
118
123
|
: `[${JSON.stringify(key)}]`;
|
|
124
|
+
/**
|
|
125
|
+
* One operand's two position rules: the `OPERAND_LEGALITY` table, and the
|
|
126
|
+
* `ordering`-over-`call-inputs` rule that sits beside it. Both are decidable
|
|
127
|
+
* from the operand alone, so both cross over to a bare `Expression`.
|
|
128
|
+
*/
|
|
129
|
+
function checkOperandAtPosition(operand, op, position, artifactPath) {
|
|
130
|
+
const legal = OPERAND_LEGALITY[op]?.[String(position)];
|
|
131
|
+
if (legal === undefined)
|
|
132
|
+
return;
|
|
133
|
+
const kind = kindOf(operand);
|
|
134
|
+
if (!legal.has(kind)) {
|
|
135
|
+
throw new StructuralFailure('malformed-operator-expression', artifactPath, `"${op}" does not accept a ${kind} operand at position ${position} (AD-4, AD-26)`);
|
|
136
|
+
}
|
|
137
|
+
if (op === 'ordering' &&
|
|
138
|
+
'pointer' in operand &&
|
|
139
|
+
!operand.pointer.startsWith('@') &&
|
|
140
|
+
parseEvidenceTarget(operand.pointer).channel === 'call-inputs') {
|
|
141
|
+
throw new StructuralFailure('malformed-operator-expression', artifactPath, '"ordering" accepts observed output only, never call-inputs (AD-4)');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `checkOperandLegality` over one bare `Expression`, minus the `covers-by-key`
|
|
146
|
+
* `expectedKey` uniqueness rule, which reads the contract's declared reference
|
|
147
|
+
* sets and has no probe-side form: a probe-side condition may carry no
|
|
148
|
+
* `{ referenceSet }` operand at all.
|
|
149
|
+
*/
|
|
150
|
+
export function checkExpressionOperandLegality(expression, artifactPath) {
|
|
151
|
+
walkExpression(expression, 0, '', {
|
|
152
|
+
onOperand: (operand, op, position, path) => {
|
|
153
|
+
checkOperandAtPosition(operand, op, position, `${artifactPath}${path}`);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** `checkRegexConstructs` over one bare `Expression`. */
|
|
158
|
+
export function checkExpressionRegexConstructs(expression, artifactPath) {
|
|
159
|
+
walkExpression(expression, 0, '', {
|
|
160
|
+
onRegex: (pattern, path) => {
|
|
161
|
+
const rejection = regexRejection(pattern);
|
|
162
|
+
if (rejection !== null) {
|
|
163
|
+
throw new StructuralFailure('malformed-operator-expression', `${artifactPath}${path}.pattern`, `regex pattern "${pattern}" ${rejection}, a rejected construct (AD-4)`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** `checkQuantifierNesting` over one bare `Expression`. */
|
|
169
|
+
export function checkExpressionQuantifierNesting(expression, artifactPath) {
|
|
170
|
+
walkExpression(expression, 0, '', {
|
|
171
|
+
onQuantifier: (_expr, path, quantifierDepth) => {
|
|
172
|
+
if (quantifierDepth >= 1) {
|
|
173
|
+
throw new StructuralFailure('quantifier-nesting-exceeded', `${artifactPath}${path}`, "a quantifier appears inside another quantifier's predicate; quantifiers may not nest more than one level (AD-4)");
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
onCoversByKey: (_expr, path, quantifierDepth) => {
|
|
177
|
+
if (quantifierDepth >= 1) {
|
|
178
|
+
throw new StructuralFailure('quantifier-nesting-exceeded', `${artifactPath}${path}`, "covers-by-key appears inside a quantifier's predicate, where it may never nest (AD-4)");
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
119
183
|
/** Checks each operator's operands against its position-specific constraints. */
|
|
120
184
|
export function checkOperandLegality(contract) {
|
|
121
185
|
forEachContractExpression(contract, (site) => {
|
|
122
186
|
walkExpression(site.expression, 0, '', {
|
|
123
187
|
onOperand: (operand, op, position, path) => {
|
|
124
|
-
|
|
125
|
-
if (legal === undefined)
|
|
126
|
-
return;
|
|
127
|
-
const kind = kindOf(operand);
|
|
128
|
-
if (!legal.has(kind)) {
|
|
129
|
-
throw new StructuralFailure('malformed-operator-expression', `${site.artifactPath}${path}`, `"${op}" does not accept a ${kind} operand at position ${position} (AD-4, AD-26)`);
|
|
130
|
-
}
|
|
131
|
-
if (op === 'ordering' &&
|
|
132
|
-
'pointer' in operand &&
|
|
133
|
-
!operand.pointer.startsWith('@') &&
|
|
134
|
-
parseEvidenceTarget(operand.pointer).channel === 'call-inputs') {
|
|
135
|
-
throw new StructuralFailure('malformed-operator-expression', `${site.artifactPath}${path}`, '"ordering" accepts observed output only, never call-inputs (AD-4)');
|
|
136
|
-
}
|
|
188
|
+
checkOperandAtPosition(operand, op, position, `${site.artifactPath}${path}`);
|
|
137
189
|
},
|
|
138
190
|
onCoversByKey: (expr) => {
|
|
139
191
|
const expected = expr.operands[0];
|
|
@@ -280,6 +332,44 @@ function forEachQuantifierCollection(expr, boundElementRoot, path, visit) {
|
|
|
280
332
|
return;
|
|
281
333
|
}
|
|
282
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* The check itself, against whatever resolves a step identifier to the
|
|
337
|
+
* operation that answers it. The contract path resolves through the plan index
|
|
338
|
+
* or the witness scope; a probe-side condition resolves the one reserved step
|
|
339
|
+
* identifier to the signature's home operation.
|
|
340
|
+
*/
|
|
341
|
+
function checkQuantifiersAgainst(expression, artifactPath, operationFor) {
|
|
342
|
+
forEachQuantifierCollection(expression, null, '', (pointer, path) => {
|
|
343
|
+
const target = parseEvidenceTarget(pointer);
|
|
344
|
+
if (target.channel !== 'response-body')
|
|
345
|
+
return;
|
|
346
|
+
const operation = operationFor(target.stepId);
|
|
347
|
+
if (operation === undefined)
|
|
348
|
+
return;
|
|
349
|
+
const firstToken = target.tail.length === 1 ? target.tail[0] : undefined;
|
|
350
|
+
const declaredType = firstToken === undefined
|
|
351
|
+
? undefined
|
|
352
|
+
: operation.responseDescriptor.types[firstToken];
|
|
353
|
+
if (declaredType !== undefined &&
|
|
354
|
+
declaredType !== null &&
|
|
355
|
+
NON_COLLECTION_TYPES.has(declaredType)) {
|
|
356
|
+
throw new StructuralFailure('quantifier-over-non-collection', `${artifactPath}${path}`, `"${pointer}" is declared "${declaredType}" by operation "${operation.operationId}", not a collection (AD-4)`);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* `checkQuantifierOverNonCollection` over one bare `Expression` rooted at a
|
|
362
|
+
* known set of step identifiers, all of which resolve to one operation. The
|
|
363
|
+
* `legIds` argument is load-bearing: omit it and the check silently no-ops on
|
|
364
|
+
* every expression, which is the trap the witness branch below already warns
|
|
365
|
+
* about. It reads the operation's declared response types, never its
|
|
366
|
+
* `collectionLocations`; a declared non-array type beats a contradictory
|
|
367
|
+
* collection location, and a re-derivation that read the other field would
|
|
368
|
+
* disagree with the compiler on a committed fixture.
|
|
369
|
+
*/
|
|
370
|
+
export function checkExpressionQuantifierOverNonCollection(expression, artifactPath, scope) {
|
|
371
|
+
checkQuantifiersAgainst(expression, artifactPath, (stepId) => scope.legIds.includes(stepId) ? scope.operation : undefined);
|
|
372
|
+
}
|
|
283
373
|
/** Checks response-body collection pointers, after bound-element substitution. */
|
|
284
374
|
export function checkQuantifierOverNonCollection(contract) {
|
|
285
375
|
const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
@@ -298,23 +388,7 @@ export function checkQuantifierOverNonCollection(contract) {
|
|
|
298
388
|
return index.operationOf(step.operationId);
|
|
299
389
|
};
|
|
300
390
|
forEachContractExpression(contract, (site) => {
|
|
301
|
-
|
|
302
|
-
const target = parseEvidenceTarget(pointer);
|
|
303
|
-
if (target.channel !== 'response-body')
|
|
304
|
-
return;
|
|
305
|
-
const operation = operationFor(site, target.stepId);
|
|
306
|
-
if (operation === undefined)
|
|
307
|
-
return;
|
|
308
|
-
const firstToken = target.tail.length === 1 ? target.tail[0] : undefined;
|
|
309
|
-
const declaredType = firstToken === undefined
|
|
310
|
-
? undefined
|
|
311
|
-
: operation.responseDescriptor.types[firstToken];
|
|
312
|
-
if (declaredType !== undefined &&
|
|
313
|
-
declaredType !== null &&
|
|
314
|
-
NON_COLLECTION_TYPES.has(declaredType)) {
|
|
315
|
-
throw new StructuralFailure('quantifier-over-non-collection', `${site.artifactPath}${path}`, `"${pointer}" is declared "${declaredType}" by operation "${operation.operationId}", not a collection (AD-4)`);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
391
|
+
checkQuantifiersAgainst(site.expression, site.artifactPath, (stepId) => operationFor(site, stepId));
|
|
318
392
|
});
|
|
319
393
|
}
|
|
320
394
|
// Reference-set resolution.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
2
|
/** `forbidden-input-floor-incomplete`: `forbiddenInputs` omits a floor member. */
|
|
3
3
|
export declare function checkForbiddenInputFloor(contract: EvalContract): void;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* `scoped-reference-resolves-forbidden`: any scoped reference is forbidden,
|
|
6
|
+
* wherever the contract writes it down. `scopedResources` is read first and
|
|
7
|
+
* `testData.resources` second, each with its own message and artifact path, so
|
|
8
|
+
* a contract carrying both reports the field AD-16 names by that word. The
|
|
9
|
+
* second address uses the caller-keyed form `interface-inventory.ts` already
|
|
10
|
+
* emits, since that list is keyed by name rather than indexed.
|
|
11
|
+
*/
|
|
5
12
|
export declare function checkScopedResourceReferences(contract: EvalContract): void;
|
|
@@ -10,10 +10,23 @@ export function checkForbiddenInputFloor(contract) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* `scoped-reference-resolves-forbidden`: any scoped reference is forbidden,
|
|
15
|
+
* wherever the contract writes it down. `scopedResources` is read first and
|
|
16
|
+
* `testData.resources` second, each with its own message and artifact path, so
|
|
17
|
+
* a contract carrying both reports the field AD-16 names by that word. The
|
|
18
|
+
* second address uses the caller-keyed form `interface-inventory.ts` already
|
|
19
|
+
* emits, since that list is keyed by name rather than indexed.
|
|
20
|
+
*/
|
|
14
21
|
export function checkScopedResourceReferences(contract) {
|
|
15
22
|
const resource = contract.scopedResources?.[0];
|
|
16
|
-
if (resource
|
|
23
|
+
if (resource !== undefined) {
|
|
24
|
+
throw new StructuralFailure('scoped-reference-resolves-forbidden', 'EvalContract.scopedResources[0].reference', `scoped resource reference "${resource.reference}" is forbidden (AD-16)`);
|
|
25
|
+
}
|
|
26
|
+
// Sorted, so which of several declared resources is reported never depends
|
|
27
|
+
// on the authored key order.
|
|
28
|
+
const declared = Object.keys(contract.testData.resources ?? {}).sort()[0];
|
|
29
|
+
if (declared === undefined)
|
|
17
30
|
return;
|
|
18
|
-
throw new StructuralFailure('scoped-reference-resolves-forbidden',
|
|
31
|
+
throw new StructuralFailure('scoped-reference-resolves-forbidden', `EvalContract.testData.resources[${JSON.stringify(declared)}]`, `declared test-data resource "${declared}" is a scoped resource reference and is forbidden (AD-16)`);
|
|
19
32
|
}
|