eval-quality 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +20 -13
  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/fragment-selection.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  19. package/corpus/dev/contracts/review-corpus.json +1 -0
  20. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  21. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  22. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  23. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  24. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  25. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  26. package/corpus/dev/index.json +1 -1
  27. package/dist/application/index.d.ts +2 -0
  28. package/dist/application/index.js +1 -0
  29. package/dist/application/score.d.ts +48 -0
  30. package/dist/application/score.js +186 -0
  31. package/dist/cli/arguments.d.ts +7 -3
  32. package/dist/cli/arguments.js +73 -5
  33. package/dist/cli/exit-codes.d.ts +6 -2
  34. package/dist/cli/exit-codes.js +8 -9
  35. package/dist/cli/main.js +9 -0
  36. package/dist/cli/render.d.ts +3 -6
  37. package/dist/cli/render.js +57 -5
  38. package/dist/cli/run.d.ts +13 -3
  39. package/dist/cli/run.js +136 -16
  40. package/dist/core/canonical/canonicalize.d.ts +12 -0
  41. package/dist/core/canonical/canonicalize.js +18 -4
  42. package/dist/core/compile/bindings.d.ts +89 -0
  43. package/dist/core/compile/bindings.js +334 -0
  44. package/dist/core/compile/compile.d.ts +17 -0
  45. package/dist/core/compile/compile.js +37 -1
  46. package/dist/core/compile/excluded-content.d.ts +11 -0
  47. package/dist/core/compile/excluded-content.js +42 -0
  48. package/dist/core/compile/expression-legality.d.ts +49 -0
  49. package/dist/core/compile/expression-legality.js +158 -38
  50. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  51. package/dist/core/compile/forbidden-inputs.js +16 -3
  52. package/dist/core/compile/interface-inventory.d.ts +57 -2
  53. package/dist/core/compile/interface-inventory.js +119 -13
  54. package/dist/core/compile/reachability.d.ts +28 -1
  55. package/dist/core/compile/reachability.js +167 -35
  56. package/dist/core/compile/schema-version.d.ts +2 -0
  57. package/dist/core/compile/schema-version.js +25 -0
  58. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  59. package/dist/core/compile/sensitivity-witness.js +110 -23
  60. package/dist/core/compile/step-reference.d.ts +2 -0
  61. package/dist/core/compile/step-reference.js +49 -0
  62. package/dist/core/coverage/operations.d.ts +62 -0
  63. package/dist/core/coverage/operations.js +57 -0
  64. package/dist/core/coverage/relevance.d.ts +4 -2
  65. package/dist/core/coverage/relevance.js +22 -23
  66. package/dist/core/coverage/satisfaction.d.ts +2 -2
  67. package/dist/core/coverage/satisfaction.js +73 -48
  68. package/dist/core/declared-inputs.d.ts +83 -4
  69. package/dist/core/declared-inputs.js +105 -8
  70. package/dist/core/emit/emit.d.ts +9 -0
  71. package/dist/core/emit/emit.js +214 -0
  72. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  73. package/dist/core/emit/private-artifact-digest.js +31 -0
  74. package/dist/core/evaluate/evidence-resolution.d.ts +10 -3
  75. package/dist/core/evaluate/evidence-resolution.js +74 -11
  76. package/dist/core/evaluate/operators.d.ts +22 -0
  77. package/dist/core/evaluate/operators.js +17 -1
  78. package/dist/core/evaluate/resolution.d.ts +14 -3
  79. package/dist/core/evaluate/resolution.js +63 -3
  80. package/dist/core/excluded-content.d.ts +65 -0
  81. package/dist/core/excluded-content.js +113 -0
  82. package/dist/core/failure-codes.d.ts +2 -2
  83. package/dist/core/failure-codes.js +7 -2
  84. package/dist/core/ingest/conditions.d.ts +201 -0
  85. package/dist/core/ingest/conditions.js +48 -0
  86. package/dist/core/ingest/index.d.ts +13 -0
  87. package/dist/core/ingest/index.js +11 -0
  88. package/dist/core/ingest/ingest.d.ts +68 -0
  89. package/dist/core/ingest/ingest.js +318 -0
  90. package/dist/core/lineage/stage-table.d.ts +21 -4
  91. package/dist/core/lineage/stage-table.js +35 -3
  92. package/dist/core/preflight/plan.d.ts +7 -5
  93. package/dist/core/preflight/plan.js +75 -32
  94. package/dist/core/preflight/projection.d.ts +6 -3
  95. package/dist/core/preflight/projection.js +22 -2
  96. package/dist/core/preflight/reduce.js +47 -7
  97. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  98. package/dist/core/preflight/witness-evidence.js +73 -20
  99. package/dist/core/schemas/artifact.d.ts +1049 -30
  100. package/dist/core/schemas/constraint-ledger.js +51 -0
  101. package/dist/core/schemas/defect-signature.d.ts +695 -0
  102. package/dist/core/schemas/defect-signature.js +175 -0
  103. package/dist/core/schemas/eval-contract.d.ts +550 -12
  104. package/dist/core/schemas/eval-contract.js +32 -2
  105. package/dist/core/schemas/evidence-artifact.d.ts +109 -19
  106. package/dist/core/schemas/evidence-artifact.js +64 -8
  107. package/dist/core/schemas/interface.d.ts +741 -13
  108. package/dist/core/schemas/interface.js +120 -7
  109. package/dist/core/schemas/isolation-manifest.js +16 -9
  110. package/dist/core/schemas/plan.d.ts +329 -3
  111. package/dist/core/schemas/plan.js +89 -3
  112. package/dist/core/schemas/pointer.d.ts +47 -1
  113. package/dist/core/schemas/pointer.js +89 -8
  114. package/dist/core/schemas/port-messages.d.ts +194 -10
  115. package/dist/core/schemas/port-messages.js +73 -6
  116. package/dist/core/schemas/probe-body.d.ts +18 -0
  117. package/dist/core/schemas/probe-body.js +13 -0
  118. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  119. package/dist/core/schemas/probe-qualification.js +106 -0
  120. package/dist/core/schemas/probe.d.ts +431 -0
  121. package/dist/core/schemas/probe.js +6 -2
  122. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  123. package/dist/core/schemas/scoring-policy.js +6 -1
  124. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  125. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  126. package/dist/core/schemas/sealed-run-record.d.ts +161 -12
  127. package/dist/core/schemas/sealed-run-record.js +92 -15
  128. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  129. package/dist/core/schemas/sensitivity-witness.js +61 -5
  130. package/dist/core/score/binding-order.d.ts +31 -0
  131. package/dist/core/score/binding-order.js +88 -0
  132. package/dist/core/score/bindings.d.ts +93 -0
  133. package/dist/core/score/bindings.js +309 -0
  134. package/dist/core/score/ladder-table.d.ts +23 -0
  135. package/dist/core/score/ladder-table.js +109 -0
  136. package/dist/core/score/ladder.d.ts +229 -0
  137. package/dist/core/score/ladder.js +464 -0
  138. package/dist/core/score/mode-agreement.d.ts +30 -0
  139. package/dist/core/score/mode-agreement.js +16 -0
  140. package/dist/core/score/outcome-table.d.ts +17 -0
  141. package/dist/core/score/outcome-table.js +172 -0
  142. package/dist/core/score/outcome.d.ts +466 -0
  143. package/dist/core/score/outcome.js +562 -0
  144. package/dist/core/score/qualification.d.ts +83 -0
  145. package/dist/core/score/qualification.js +645 -0
  146. package/dist/core/score/quotation.d.ts +56 -0
  147. package/dist/core/score/quotation.js +226 -0
  148. package/dist/core/score/reduce-trials.d.ts +80 -0
  149. package/dist/core/score/reduce-trials.js +90 -0
  150. package/dist/core/score/score.d.ts +72 -0
  151. package/dist/core/score/score.js +607 -0
  152. package/dist/core/score/selection.d.ts +80 -0
  153. package/dist/core/score/selection.js +98 -0
  154. package/dist/core/score/strength.d.ts +58 -0
  155. package/dist/core/score/strength.js +227 -0
  156. package/dist/core/score/witness.d.ts +138 -0
  157. package/dist/core/score/witness.js +320 -0
  158. package/dist/core/seal/derived-reference.d.ts +2 -9
  159. package/dist/core/seal/derived-reference.js +293 -46
  160. package/dist/core/seal/plan-index.d.ts +18 -3
  161. package/dist/core/seal/plan-index.js +44 -8
  162. package/dist/core/seal/seal.js +8 -3
  163. package/dist/core/stage-contracts.d.ts +62 -2
  164. package/dist/index.d.ts +1 -1
  165. package/dist/index.js +1 -1
  166. package/dist/ports/environment-probe-port.d.ts +60 -4
  167. package/dist/testing/index.d.ts +2 -0
  168. package/dist/testing/index.js +1 -0
  169. package/dist/testing/probe-conformance.d.ts +29 -0
  170. package/dist/testing/probe-conformance.js +34 -5
  171. package/package.json +12 -3
  172. package/schemas/eval-contract.schema.json +1787 -797
  173. package/schemas/evidence-artifact.schema.json +126 -9
  174. package/schemas/isolation-manifest.schema.json +17 -10
  175. package/schemas/probe.schema.json +636 -5
  176. package/schemas/rubric.schema.json +1 -1
  177. package/schemas/scoring-policy.schema.json +8 -1
  178. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  179. package/schemas/sealed-run-record.schema.json +314 -40
@@ -3,11 +3,18 @@
3
3
  * reference-set resolution. Each check reports the first structural failure.
4
4
  */
5
5
  import { digestArtifact } from '../canonical/digest.js';
6
+ import { targetsDescribedChannel } from '../declared-inputs.js';
6
7
  import { StructuralFailure } from '../failure-codes.js';
8
+ import { operationsOf } from '../schemas/interface.js';
7
9
  import { JsonTypeName } from '../schemas/primitives.js';
8
- import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
10
+ import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
9
11
  import { substitutePointer } from './oracle-alignment.js';
10
- function walkExpression(expr, quantifierDepth, path, visitor) {
12
+ /**
13
+ * The one expression walker in this package. Contract-free and a superset of
14
+ * the narrower walks the checks below need, so a probe-side legality pass
15
+ * reuses it rather than adding a fifth traversal that can drift from it.
16
+ */
17
+ export function walkExpression(expr, quantifierDepth, path, visitor) {
11
18
  switch (expr.op) {
12
19
  case 'not':
13
20
  walkExpression(expr.operands[0], quantifierDepth, `${path}.operands[0]`, visitor);
@@ -63,7 +70,7 @@ function forEachContractExpression(contract, visit) {
63
70
  });
64
71
  });
65
72
  contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
66
- iface.operations.forEach((operation, operationIndex) => {
73
+ operationsOf(iface).forEach((operation, operationIndex) => {
67
74
  const witness = operation.sensitivityWitness;
68
75
  if (witness === null)
69
76
  return;
@@ -116,41 +123,129 @@ const OPERAND_LEGALITY = {
116
123
  const artifactKey = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
117
124
  ? `.${key}`
118
125
  : `[${JSON.stringify(key)}]`;
126
+ /**
127
+ * One operand's two position rules: the `OPERAND_LEGALITY` table, and the
128
+ * `ordering`-over-`call-inputs` rule that sits beside it. Both are decidable
129
+ * from the operand alone, so both cross over to a bare `Expression`.
130
+ */
131
+ function checkOperandAtPosition(operand, op, position, artifactPath) {
132
+ const legal = OPERAND_LEGALITY[op]?.[String(position)];
133
+ if (legal === undefined)
134
+ return;
135
+ const kind = kindOf(operand);
136
+ if (!legal.has(kind)) {
137
+ throw new StructuralFailure('malformed-operator-expression', artifactPath, `"${op}" does not accept a ${kind} operand at position ${position} (AD-4, AD-26)`);
138
+ }
139
+ if (op === 'ordering' &&
140
+ 'pointer' in operand &&
141
+ !operand.pointer.startsWith('@') &&
142
+ parseEvidenceTarget(operand.pointer).channel === 'call-inputs') {
143
+ throw new StructuralFailure('malformed-operator-expression', artifactPath, '"ordering" accepts observed output only, never call-inputs (AD-4)');
144
+ }
145
+ }
146
+ /**
147
+ * `checkOperandLegality` over one bare `Expression`, minus the `covers-by-key`
148
+ * `expectedKey` uniqueness rule, which reads the contract's declared reference
149
+ * sets and has no probe-side form: a probe-side condition may carry no
150
+ * `{ referenceSet }` operand at all.
151
+ */
152
+ export function checkExpressionOperandLegality(expression, artifactPath) {
153
+ walkExpression(expression, 0, '', {
154
+ onOperand: (operand, op, position, path) => {
155
+ checkOperandAtPosition(operand, op, position, `${artifactPath}${path}`);
156
+ },
157
+ });
158
+ }
159
+ /** `checkRegexConstructs` over one bare `Expression`. */
160
+ export function checkExpressionRegexConstructs(expression, artifactPath) {
161
+ walkExpression(expression, 0, '', {
162
+ onRegex: (pattern, path) => {
163
+ const rejection = regexRejection(pattern);
164
+ if (rejection !== null) {
165
+ throw new StructuralFailure('malformed-operator-expression', `${artifactPath}${path}.pattern`, `regex pattern "${pattern}" ${rejection}, a rejected construct (AD-4)`);
166
+ }
167
+ },
168
+ });
169
+ }
170
+ /** `checkQuantifierNesting` over one bare `Expression`. */
171
+ export function checkExpressionQuantifierNesting(expression, artifactPath) {
172
+ walkExpression(expression, 0, '', {
173
+ onQuantifier: (_expr, path, quantifierDepth) => {
174
+ if (quantifierDepth >= 1) {
175
+ 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)");
176
+ }
177
+ },
178
+ onCoversByKey: (_expr, path, quantifierDepth) => {
179
+ if (quantifierDepth >= 1) {
180
+ throw new StructuralFailure('quantifier-nesting-exceeded', `${artifactPath}${path}`, "covers-by-key appears inside a quantifier's predicate, where it may never nest (AD-4)");
181
+ }
182
+ },
183
+ });
184
+ }
185
+ /**
186
+ * The declaration a reference-set operand names, and `undefined` when the
187
+ * contract declares none by that identifier. An undeclared identifier is
188
+ * `checkReferenceSetResolution`'s `unresolved-reference-set`, so the
189
+ * shape-aware checks below stay quiet about it.
190
+ */
191
+ function declaredReferenceSetOf(contract, referenceSetId) {
192
+ const declarations = contract.referenceSets;
193
+ if (declarations === null || !Object.hasOwn(declarations, referenceSetId)) {
194
+ return undefined;
195
+ }
196
+ return declarations[referenceSetId];
197
+ }
119
198
  /** Checks each operator's operands against its position-specific constraints. */
120
199
  export function checkOperandLegality(contract) {
121
200
  forEachContractExpression(contract, (site) => {
122
201
  walkExpression(site.expression, 0, '', {
123
202
  onOperand: (operand, op, position, path) => {
124
- const legal = OPERAND_LEGALITY[op]?.[String(position)];
125
- if (legal === undefined)
203
+ checkOperandAtPosition(operand, op, position, `${site.artifactPath}${path}`);
204
+ },
205
+ // Fires only for `set-membership`'s set position, which is the one
206
+ // position that reads a reference set by its single declared key
207
+ // (`reference-set.ts`). `keys` is `.min(1)` with no maximum, so a
208
+ // two-key set names no one key to read there, and a member missing
209
+ // the declared key has nothing to read at all. AD-26 assigns
210
+ // `malformed-operator-expression` to an operand type the operator
211
+ // does not accept, which is what each of those is. Rejecting them
212
+ // here is what makes the resolver's projection total.
213
+ onSetOperand: (setOperand, path) => {
214
+ if (!('referenceSet' in setOperand))
215
+ return;
216
+ const { referenceSet } = setOperand;
217
+ const declaration = declaredReferenceSetOf(contract, referenceSet);
218
+ if (declaration === undefined)
126
219
  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)`);
220
+ if (declaration.keys.length !== 1) {
221
+ throw new StructuralFailure('malformed-operator-expression', `${site.artifactPath}${path}`, `referenceSet "${referenceSet}" declares ${declaration.keys.length} keys, and set-membership reads exactly one, so only a single-key set is legal in its set position (AD-4, AD-26)`);
130
222
  }
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)');
223
+ const key = declaration.keys[0];
224
+ for (const [index, member] of declaration.members.entries()) {
225
+ if (Object.hasOwn(member, key))
226
+ continue;
227
+ throw new StructuralFailure('malformed-operator-expression', `EvalContract.referenceSets[id=${referenceSet}].members[${index}]${artifactKey(key)}`, `referenceSet "${referenceSet}" has a member carrying no key "${key}", which it declares and which set-membership reads (AD-4, AD-26)`);
136
228
  }
137
229
  },
138
230
  onCoversByKey: (expr) => {
139
231
  const expected = expr.operands[0];
140
232
  if (!('referenceSet' in expected))
141
233
  return;
142
- const declarations = contract.referenceSets;
143
- if (declarations === null ||
144
- !Object.hasOwn(declarations, expected.referenceSet))
145
- return;
146
- const declaration = declarations[expected.referenceSet];
234
+ const declaration = declaredReferenceSetOf(contract, expected.referenceSet);
147
235
  if (declaration === undefined)
148
236
  return;
149
237
  const seen = new Set();
150
238
  for (const [index, member] of declaration.members.entries()) {
151
- if (!Object.hasOwn(member, expr.expectedKey))
152
- continue;
153
239
  const memberPath = `EvalContract.referenceSets[id=${expected.referenceSet}].members[${index}]${artifactKey(expr.expectedKey)}`;
240
+ // `coversByKey` returns false for the whole collection when one
241
+ // declared member lacks `expectedKey`, which reads as a detected
242
+ // defect in the system under test, when it is an authoring
243
+ // mistake. This check used to walk past that member to keep the
244
+ // duplicate-detection loop total, which left the compiler silent
245
+ // about the one shape that makes the operator answer wrongly.
246
+ if (!Object.hasOwn(member, expr.expectedKey)) {
247
+ throw new StructuralFailure('malformed-operator-expression', memberPath, `referenceSet "${expected.referenceSet}" has a member carrying no expectedKey "${expr.expectedKey}", which covers-by-key reads on every member (AD-4)`);
248
+ }
154
249
  const digest = digestArtifact(member[expr.expectedKey], memberPath);
155
250
  if (seen.has(digest)) {
156
251
  throw new StructuralFailure('malformed-operator-expression', memberPath, `referenceSet "${expected.referenceSet}" repeats expectedKey "${expr.expectedKey}" for covers-by-key (AD-4)`);
@@ -280,6 +375,47 @@ function forEachQuantifierCollection(expr, boundElementRoot, path, visit) {
280
375
  return;
281
376
  }
282
377
  }
378
+ /**
379
+ * The check itself, against whatever resolves a step identifier to the
380
+ * operation that answers it. The contract path resolves through the plan index
381
+ * or the witness scope; a probe-side condition resolves the one reserved step
382
+ * identifier to the signature's home operation.
383
+ */
384
+ function checkQuantifiersAgainst(expression, artifactPath, operationFor) {
385
+ forEachQuantifierCollection(expression, null, '', (pointer, path) => {
386
+ const target = parseEvidenceTarget(pointer);
387
+ const operation = operationFor(target.stepId);
388
+ if (operation === undefined)
389
+ return;
390
+ // The operation is resolved before the target is tested, because which
391
+ // channel carries the declared structure is the operation's own answer,
392
+ // and on the artifact channel so is which file.
393
+ if (!targetsDescribedChannel(operation, target))
394
+ return;
395
+ const firstToken = target.tail.length === 1 ? target.tail[0] : undefined;
396
+ const declaredType = firstToken === undefined
397
+ ? undefined
398
+ : operation.responseDescriptor.types[firstToken];
399
+ if (declaredType !== undefined &&
400
+ declaredType !== null &&
401
+ NON_COLLECTION_TYPES.has(declaredType)) {
402
+ throw new StructuralFailure('quantifier-over-non-collection', `${artifactPath}${path}`, `"${pointer}" is declared "${declaredType}" by operation "${operation.operationId}", not a collection (AD-4)`);
403
+ }
404
+ });
405
+ }
406
+ /**
407
+ * `checkQuantifierOverNonCollection` over one bare `Expression` rooted at a
408
+ * known set of step identifiers, all of which resolve to one operation. The
409
+ * `legIds` argument is load-bearing: omit it and the check silently no-ops on
410
+ * every expression, which is the trap the witness branch below already warns
411
+ * about. It reads the operation's declared response types, never its
412
+ * `collectionLocations`; a declared non-array type beats a contradictory
413
+ * collection location, and a re-derivation that read the other field would
414
+ * disagree with the compiler on a committed fixture.
415
+ */
416
+ export function checkExpressionQuantifierOverNonCollection(expression, artifactPath, scope) {
417
+ checkQuantifiersAgainst(expression, artifactPath, (stepId) => scope.legIds.includes(stepId) ? scope.operation : undefined);
418
+ }
283
419
  /** Checks response-body collection pointers, after bound-element substitution. */
284
420
  export function checkQuantifierOverNonCollection(contract) {
285
421
  const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
@@ -295,26 +431,10 @@ export function checkQuantifierOverNonCollection(contract) {
295
431
  const step = index.stepOf(stepId);
296
432
  if (step === undefined)
297
433
  return undefined;
298
- return index.operationOf(step.operationId);
434
+ return anyOperationOf(index, step.operationId);
299
435
  };
300
436
  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
- });
437
+ checkQuantifiersAgainst(site.expression, site.artifactPath, (stepId) => operationFor(site, stepId));
318
438
  });
319
439
  }
320
440
  // 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
  }
@@ -1,10 +1,65 @@
1
1
  import type { EvalContract } from '../schemas/eval-contract.ts';
2
- /** Rejects permitted interface kinds that this contract version cannot run. */
2
+ import type { AnyOperation } from '../schemas/interface.ts';
3
3
  export declare function checkInterfaceKind(contract: EvalContract): void;
4
+ /**
5
+ * The transport identity AD-40 resolves a defect signature against: the method
6
+ * and the path template with parameter names erased, so `/notes/{id}` and
7
+ * `/notes/{noteId}` share one signature. Takes the two fields rather than an
8
+ * `Operation`, since AD-40's corpus-side signature declares the same pair and
9
+ * must produce the same string from it or the comparison is not a comparison.
10
+ */
11
+ export declare function operationSignature(operation: {
12
+ readonly method: string;
13
+ readonly pathTemplate: string;
14
+ }): string;
15
+ /**
16
+ * The separator between a command's executable and each subcommand segment,
17
+ * declared once so the contract side and AD-40's corpus side cannot disagree
18
+ * about it. A space, matching the way the identity is written on a terminal.
19
+ */
20
+ export declare const COMMAND_SIGNATURE_SEPARATOR = " ";
21
+ /**
22
+ * The command counterpart of `operationSignature`, compared literally.
23
+ *
24
+ * There is no erasure step. A subcommand path carries no parameters: a
25
+ * command's variable inputs are its arguments and options, which live in the
26
+ * request shape. Erasing a subcommand segment would make `tool review` and
27
+ * `tool report` one signature, which is the opposite of what erasure is for.
28
+ */
29
+ export declare function commandSignature(operation: {
30
+ readonly invocation: {
31
+ readonly executable: string;
32
+ readonly subcommandPath: readonly string[];
33
+ };
34
+ }): string;
35
+ /** The transport identity of an operation of either kind. */
36
+ export declare const anyOperationSignature: (operation: AnyOperation) => string;
4
37
  /** Finds duplicate method and path signatures across the full inventory. */
5
38
  export declare function checkDuplicateOperationSignature(contract: EvalContract): void;
6
39
  /**
7
- * Checks each binding key against its operation's request shape.
40
+ * `unresolved-artifact-reference`: an artifact identifier nothing declares.
41
+ *
42
+ * Two sites name one: an evidence pointer's identifier segment, and a command
43
+ * operation's own `descriptorChannel` when it nominates an artifact. Both are
44
+ * authoring faults the compiler can see, and both take a code rather than
45
+ * resolving `absent`, on AD-26's own precedent for a dangling reference-set
46
+ * identifier: `absent` is defined over pointers that do not resolve against
47
+ * observed evidence, and a dangling declaration is neither.
48
+ */
49
+ export declare function checkArtifactReferences(contract: EvalContract): void;
50
+ /**
51
+ * Checks each binding key against its operation's request shape, and each
52
+ * `{ principal }` value against the contract's declared principals.
8
53
  * Steps with unresolved operation IDs belong to a separate cross-field rule.
54
+ *
55
+ * The two conditions share one code and are not the same predicate: the key
56
+ * case is an input the contract did not declare, the principal case has a
57
+ * declared key whose referenced name the contract did not declare. AD-5's row
58
+ * is widened to say so. Placing the principal case here rather than in a root
59
+ * Zod refinement is deliberate: a constraint reaching from
60
+ * `interactionPlan[].inputBinding` into `testData.principals` cannot survive
61
+ * the export, and the published-schema differential sweep synthesises a
62
+ * `{ principal }` union-branch witness naming no declared principal, which a
63
+ * refinement would reject and ajv would accept.
9
64
  */
10
65
  export declare function checkUndeclaredMandatoryInput(contract: EvalContract): void;
@@ -2,26 +2,70 @@
2
2
  * Checks interface kinds, inventory-wide operation signatures, and step input
3
3
  * bindings against each operation's request shape.
4
4
  */
5
+ import { boundChannelsOf, declaredArtifactsOf, descriptorArtifactOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
5
6
  import { StructuralFailure } from '../failure-codes.js';
6
- import { TRANSPORT_CHANNELS } from '../schemas/pointer.js';
7
- import { buildPlanIndex } from '../seal/plan-index.js';
8
- /** Rejects permitted interface kinds that this contract version cannot run. */
7
+ import { operationsOf } from '../schemas/interface.js';
8
+ import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
9
+ import { forEachArtifactPointer } from './reachability.js';
10
+ /**
11
+ * Rejects permitted interface kinds whose probe semantics are undeclared.
12
+ *
13
+ * AD-10 closed all three non-api kinds and named the condition for opening
14
+ * one: the semantics have to be declared. They are declared for `cli`, so it
15
+ * runs; `web` and `mcp` are still undeclared and still fail here, which is
16
+ * what keeps this code fireable and keeps AD-10's sentence true of them.
17
+ */
18
+ const SUPPORTED_INTERFACE_KINDS = ['api', 'cli'];
9
19
  export function checkInterfaceKind(contract) {
10
20
  for (const iface of contract.permittedInterfaces) {
11
- if (iface.kind !== 'api') {
12
- throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported in v0; only "api" is (AD-10)`);
21
+ if (!SUPPORTED_INTERFACE_KINDS.includes(iface.kind)) {
22
+ throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported; "api" and "cli" are (AD-10)`);
13
23
  }
14
24
  }
15
25
  }
16
26
  /** Erases parameter names so equivalent path templates share a signature. */
17
27
  const PARAMETER_SEGMENT_PATTERN = /\{[A-Za-z0-9_-]+\}/g;
18
28
  const erase = (pathTemplate) => pathTemplate.replace(PARAMETER_SEGMENT_PATTERN, '{}');
29
+ /**
30
+ * The transport identity AD-40 resolves a defect signature against: the method
31
+ * and the path template with parameter names erased, so `/notes/{id}` and
32
+ * `/notes/{noteId}` share one signature. Takes the two fields rather than an
33
+ * `Operation`, since AD-40's corpus-side signature declares the same pair and
34
+ * must produce the same string from it or the comparison is not a comparison.
35
+ */
36
+ export function operationSignature(operation) {
37
+ return `${operation.method} ${erase(operation.pathTemplate)}`;
38
+ }
39
+ /**
40
+ * The separator between a command's executable and each subcommand segment,
41
+ * declared once so the contract side and AD-40's corpus side cannot disagree
42
+ * about it. A space, matching the way the identity is written on a terminal.
43
+ */
44
+ export const COMMAND_SIGNATURE_SEPARATOR = ' ';
45
+ /**
46
+ * The command counterpart of `operationSignature`, compared literally.
47
+ *
48
+ * There is no erasure step. A subcommand path carries no parameters: a
49
+ * command's variable inputs are its arguments and options, which live in the
50
+ * request shape. Erasing a subcommand segment would make `tool review` and
51
+ * `tool report` one signature, which is the opposite of what erasure is for.
52
+ */
53
+ export function commandSignature(operation) {
54
+ return [
55
+ operation.invocation.executable,
56
+ ...operation.invocation.subcommandPath,
57
+ ].join(COMMAND_SIGNATURE_SEPARATOR);
58
+ }
59
+ /** The transport identity of an operation of either kind. */
60
+ export const anyOperationSignature = (operation) => isCommandOperation(operation)
61
+ ? commandSignature(operation)
62
+ : operationSignature(operation);
19
63
  /** Finds duplicate method and path signatures across the full inventory. */
20
64
  export function checkDuplicateOperationSignature(contract) {
21
65
  const seen = new Map();
22
66
  for (const iface of contract.permittedInterfaces) {
23
- for (const operation of iface.operations) {
24
- const signature = `${operation.method} ${erase(operation.pathTemplate)}`;
67
+ for (const operation of operationsOf(iface)) {
68
+ const signature = anyOperationSignature(operation);
25
69
  const collision = seen.get(signature);
26
70
  if (collision !== undefined) {
27
71
  throw new StructuralFailure('duplicate-operation-signature', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}]`, `collides with permittedInterfaces[logicalId=${collision.logicalId}].operations[operationId=${collision.operation.operationId}] after parameter-name erasure ("${signature}") (AD-19, AD-40)`);
@@ -31,23 +75,85 @@ export function checkDuplicateOperationSignature(contract) {
31
75
  }
32
76
  }
33
77
  /**
34
- * Checks each binding key against its operation's request shape.
78
+ * `unresolved-artifact-reference`: an artifact identifier nothing declares.
79
+ *
80
+ * Two sites name one: an evidence pointer's identifier segment, and a command
81
+ * operation's own `descriptorChannel` when it nominates an artifact. Both are
82
+ * authoring faults the compiler can see, and both take a code rather than
83
+ * resolving `absent`, on AD-26's own precedent for a dangling reference-set
84
+ * identifier: `absent` is defined over pointers that do not resolve against
85
+ * observed evidence, and a dangling declaration is neither.
86
+ */
87
+ export function checkArtifactReferences(contract) {
88
+ for (const iface of contract.permittedInterfaces) {
89
+ for (const operation of operationsOf(iface)) {
90
+ const declared = declaredArtifactsOf(operation);
91
+ const nominated = descriptorArtifactOf(operation);
92
+ if (nominated !== null && !declared.includes(nominated)) {
93
+ throw new StructuralFailure('unresolved-artifact-reference', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}].descriptorChannel.artifactId`, `nominates "${nominated}", which this operation does not declare it writes (AD-19, AD-26)`);
94
+ }
95
+ }
96
+ }
97
+ const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
98
+ forEachArtifactPointer(contract, (pointer, path) => {
99
+ const target = parseEvidenceTarget(pointer);
100
+ if (target.artifactId === null)
101
+ return;
102
+ const step = index.stepOf(target.stepId);
103
+ if (step === undefined)
104
+ return;
105
+ const operation = anyOperationOf(index, step.operationId);
106
+ // An unresolvable step or operation is `unreachable-check-evidence`'s,
107
+ // at a higher rung; this check has nothing to compare against.
108
+ if (operation === undefined)
109
+ return;
110
+ if (declaredArtifactsOf(operation).includes(target.artifactId))
111
+ return;
112
+ throw new StructuralFailure('unresolved-artifact-reference', path, `"${pointer}" names the "${target.artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes (AD-26)`);
113
+ });
114
+ }
115
+ /**
116
+ * Checks each binding key against its operation's request shape, and each
117
+ * `{ principal }` value against the contract's declared principals.
35
118
  * Steps with unresolved operation IDs belong to a separate cross-field rule.
119
+ *
120
+ * The two conditions share one code and are not the same predicate: the key
121
+ * case is an input the contract did not declare, the principal case has a
122
+ * declared key whose referenced name the contract did not declare. AD-5's row
123
+ * is widened to say so. Placing the principal case here rather than in a root
124
+ * Zod refinement is deliberate: a constraint reaching from
125
+ * `interactionPlan[].inputBinding` into `testData.principals` cannot survive
126
+ * the export, and the published-schema differential sweep synthesises a
127
+ * `{ principal }` union-branch witness naming no declared principal, which a
128
+ * refinement would reject and ajv would accept.
36
129
  */
37
130
  export function checkUndeclaredMandatoryInput(contract) {
38
131
  const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
132
+ const principals = new Set(Object.keys(contract.testData.principals ?? {}));
39
133
  for (const step of contract.interactionPlan) {
40
- const operation = index.operationOf(step.operationId);
134
+ const operation = anyOperationOf(index, step.operationId);
41
135
  if (operation === undefined)
42
136
  continue;
43
- for (const channel of TRANSPORT_CHANNELS) {
44
- const binding = step.inputBinding[channel];
137
+ for (const { channel, bound: binding } of boundChannelsOf(step.inputBinding)) {
45
138
  if (binding === null)
46
139
  continue;
47
- const { requiredKeys, permittedKeys } = operation.requestShape[channel];
140
+ const shape = requestShapeOf(operation, channel);
141
+ // A step binding a channel of the other kind has no declared shape
142
+ // to answer to. Reporting it as an undeclared input is true as far
143
+ // as it goes: the operation declares no such channel, and so
144
+ // declares no such key on it.
145
+ const requiredKeys = shape?.requiredKeys ?? [];
146
+ const permittedKeys = shape?.permittedKeys ?? [];
48
147
  for (const key of Object.keys(binding)) {
148
+ const path = `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${channel}[${JSON.stringify(key)}]`;
49
149
  if (!requiredKeys.includes(key) && !permittedKeys.includes(key)) {
50
- throw new StructuralFailure('undeclared-mandatory-input', `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${channel}[${JSON.stringify(key)}]`, `operation "${operation.operationId}" declares "${key}" in neither requiredKeys nor permittedKeys of its ${channel} channel (AD-4)`);
150
+ throw new StructuralFailure('undeclared-mandatory-input', path, `operation "${operation.operationId}" declares "${key}" in neither requiredKeys nor permittedKeys of its ${channel} channel (AD-4)`);
151
+ }
152
+ const value = binding[key];
153
+ if (value !== undefined &&
154
+ 'principal' in value &&
155
+ !principals.has(value.principal)) {
156
+ throw new StructuralFailure('undeclared-mandatory-input', path, `binds the principal "${value.principal}", which testData.principals does not declare (AD-4, AD-19)`);
51
157
  }
52
158
  }
53
159
  }
@@ -1,6 +1,29 @@
1
1
  import type { EvalContract } from '../schemas/eval-contract.ts';
2
+ import type { Expression } from '../schemas/expression.ts';
3
+ import type { AnyOperation } from '../schemas/interface.ts';
2
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;
3
15
  export declare function checkBoundElementScope(contract: EvalContract): void;
16
+ /** `checkBoundElementScope` over one bare `Expression`. */
17
+ export declare function checkExpressionBoundElementScope(expression: Expression, artifactPath: string): void;
18
+ /**
19
+ * Visits every `{ pointer }` operand of one bare `Expression`, which a
20
+ * probe-side legality pass needs for the rules a contract has no equivalent of:
21
+ * that every pointer roots at the reserved step identifier, and which evidence
22
+ * channels the condition actually names.
23
+ */
24
+ export declare function forEachExpressionPointer(expression: Expression, visit: (pointer: string, path: string) => void): void;
25
+ /** `checkEvidenceReachability` over one bare `Expression` and one operation. */
26
+ export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation: AnyOperation): void;
4
27
  type ReachabilityResult = {
5
28
  readonly reachable: true;
6
29
  } | {
@@ -9,7 +32,11 @@ type ReachabilityResult = {
9
32
  };
10
33
  /**
11
34
  * Non-throwing core that `checkEvidenceReachability` wraps; also called
12
- * directly by tests.
35
+ * directly by tests. The two lines below are the whole of what ties this to a
36
+ * declared interaction plan; everything past them reads the operation alone,
37
+ * which is what `evaluateReachabilityAgainstOperation` exposes for a probe-side
38
+ * condition, whose one step identifier is reserved and resolves to the
39
+ * signature's home operation with no plan in sight.
13
40
  */
14
41
  export declare function evaluatePointerReachability(pointer: string, index: PlanIndex): ReachabilityResult;
15
42
  /** `unreachable-check-evidence`: an interaction-rooted pointer the declared interfaces cannot produce. */