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.
Files changed (133) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +17 -11
  3. package/corpus/dev/compile-seal-example/brief.json +1 -1
  4. package/corpus/dev/compile-seal-example/contract.json +1 -1
  5. package/corpus/dev/contracts/absent-collection-locations.json +1 -1
  6. package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
  7. package/corpus/dev/contracts/absent-success-indicator.json +1 -1
  8. package/corpus/dev/contracts/empty-channel-roles.json +1 -1
  9. package/corpus/dev/contracts/empty-collection-locations.json +1 -1
  10. package/corpus/dev/contracts/empty-request-shapes.json +1 -1
  11. package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
  12. package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
  13. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  14. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  15. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  16. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  17. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  18. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  19. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  20. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  21. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  22. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  23. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  24. package/corpus/dev/index.json +1 -1
  25. package/dist/application/index.d.ts +2 -0
  26. package/dist/application/index.js +1 -0
  27. package/dist/application/score.d.ts +48 -0
  28. package/dist/application/score.js +186 -0
  29. package/dist/cli/arguments.d.ts +7 -3
  30. package/dist/cli/arguments.js +73 -5
  31. package/dist/cli/exit-codes.d.ts +6 -2
  32. package/dist/cli/exit-codes.js +8 -9
  33. package/dist/cli/main.js +9 -0
  34. package/dist/cli/render.d.ts +1 -1
  35. package/dist/cli/render.js +6 -4
  36. package/dist/cli/run.d.ts +13 -3
  37. package/dist/cli/run.js +136 -16
  38. package/dist/core/canonical/canonicalize.d.ts +12 -0
  39. package/dist/core/canonical/canonicalize.js +18 -4
  40. package/dist/core/compile/bindings.d.ts +70 -0
  41. package/dist/core/compile/bindings.js +317 -0
  42. package/dist/core/compile/compile.d.ts +5 -0
  43. package/dist/core/compile/compile.js +9 -0
  44. package/dist/core/compile/expression-legality.d.ts +49 -0
  45. package/dist/core/compile/expression-legality.js +105 -31
  46. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  47. package/dist/core/compile/forbidden-inputs.js +16 -3
  48. package/dist/core/compile/interface-inventory.d.ts +23 -1
  49. package/dist/core/compile/interface-inventory.js +32 -3
  50. package/dist/core/compile/reachability.d.ts +18 -1
  51. package/dist/core/compile/reachability.js +46 -1
  52. package/dist/core/compile/sensitivity-witness.d.ts +1 -1
  53. package/dist/core/compile/sensitivity-witness.js +1 -1
  54. package/dist/core/emit/emit.d.ts +9 -0
  55. package/dist/core/emit/emit.js +214 -0
  56. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  57. package/dist/core/emit/private-artifact-digest.js +31 -0
  58. package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
  59. package/dist/core/evaluate/evidence-resolution.js +4 -1
  60. package/dist/core/failure-codes.d.ts +2 -2
  61. package/dist/core/failure-codes.js +4 -2
  62. package/dist/core/ingest/conditions.d.ts +201 -0
  63. package/dist/core/ingest/conditions.js +48 -0
  64. package/dist/core/ingest/index.d.ts +13 -0
  65. package/dist/core/ingest/index.js +11 -0
  66. package/dist/core/ingest/ingest.d.ts +68 -0
  67. package/dist/core/ingest/ingest.js +310 -0
  68. package/dist/core/lineage/stage-table.d.ts +21 -4
  69. package/dist/core/lineage/stage-table.js +35 -3
  70. package/dist/core/preflight/witness-evidence.js +10 -0
  71. package/dist/core/schemas/artifact.d.ts +385 -14
  72. package/dist/core/schemas/constraint-ledger.js +40 -0
  73. package/dist/core/schemas/defect-signature.d.ts +273 -0
  74. package/dist/core/schemas/defect-signature.js +127 -0
  75. package/dist/core/schemas/eval-contract.d.ts +33 -0
  76. package/dist/core/schemas/eval-contract.js +14 -0
  77. package/dist/core/schemas/evidence-artifact.d.ts +99 -19
  78. package/dist/core/schemas/evidence-artifact.js +64 -8
  79. package/dist/core/schemas/plan.d.ts +112 -0
  80. package/dist/core/schemas/plan.js +67 -2
  81. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  82. package/dist/core/schemas/probe-qualification.js +106 -0
  83. package/dist/core/schemas/probe.d.ts +312 -0
  84. package/dist/core/schemas/probe.js +6 -2
  85. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  86. package/dist/core/schemas/scoring-policy.js +6 -1
  87. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  88. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  89. package/dist/core/schemas/sealed-run-record.d.ts +30 -3
  90. package/dist/core/schemas/sealed-run-record.js +37 -5
  91. package/dist/core/score/binding-order.d.ts +31 -0
  92. package/dist/core/score/binding-order.js +88 -0
  93. package/dist/core/score/bindings.d.ts +93 -0
  94. package/dist/core/score/bindings.js +297 -0
  95. package/dist/core/score/ladder-table.d.ts +23 -0
  96. package/dist/core/score/ladder-table.js +109 -0
  97. package/dist/core/score/ladder.d.ts +229 -0
  98. package/dist/core/score/ladder.js +464 -0
  99. package/dist/core/score/mode-agreement.d.ts +30 -0
  100. package/dist/core/score/mode-agreement.js +16 -0
  101. package/dist/core/score/outcome-table.d.ts +17 -0
  102. package/dist/core/score/outcome-table.js +172 -0
  103. package/dist/core/score/outcome.d.ts +466 -0
  104. package/dist/core/score/outcome.js +562 -0
  105. package/dist/core/score/qualification.d.ts +83 -0
  106. package/dist/core/score/qualification.js +615 -0
  107. package/dist/core/score/quotation.d.ts +79 -0
  108. package/dist/core/score/quotation.js +201 -0
  109. package/dist/core/score/reduce-trials.d.ts +80 -0
  110. package/dist/core/score/reduce-trials.js +90 -0
  111. package/dist/core/score/score.d.ts +72 -0
  112. package/dist/core/score/score.js +581 -0
  113. package/dist/core/score/selection.d.ts +80 -0
  114. package/dist/core/score/selection.js +98 -0
  115. package/dist/core/score/strength.d.ts +49 -0
  116. package/dist/core/score/strength.js +193 -0
  117. package/dist/core/score/witness.d.ts +117 -0
  118. package/dist/core/score/witness.js +315 -0
  119. package/dist/core/seal/derived-reference.d.ts +1 -1
  120. package/dist/core/seal/derived-reference.js +236 -28
  121. package/dist/core/seal/seal.js +8 -3
  122. package/dist/core/stage-contracts.d.ts +62 -2
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/testing/index.d.ts +2 -0
  126. package/dist/testing/index.js +1 -0
  127. package/package.json +12 -3
  128. package/schemas/eval-contract.schema.json +100 -2
  129. package/schemas/evidence-artifact.schema.json +96 -9
  130. package/schemas/probe.schema.json +508 -3
  131. package/schemas/scoring-policy.schema.json +8 -1
  132. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  133. 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
- function walkExpression(expr, quantifierDepth, path, visitor) {
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
- const legal = OPERAND_LEGALITY[op]?.[String(position)];
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
- forEachQuantifierCollection(site.expression, null, '', (pointer, path) => {
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
- /** `scoped-reference-resolves-forbidden`: any scoped reference is forbidden. */
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
- /** `scoped-reference-resolves-forbidden`: any scoped reference is forbidden. */
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 === undefined)
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', 'EvalContract.scopedResources[0].reference', `scoped resource reference "${resource.reference}" is forbidden (AD-16)`);
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
  }