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
@@ -7,10 +7,12 @@
7
7
  * `evaluatePointerReachability` is exported separately as the non-throwing
8
8
  * per-pointer core, for reuse and direct testing.
9
9
  */
10
+ import { declaredArtifactsOf, descriptorArtifactOf, descriptorChannelOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
10
11
  import { ARRAY_INDEX_PATTERN } from '../evaluate/evidence-resolution.js';
11
12
  import { StructuralFailure } from '../failure-codes.js';
13
+ import { operationsOf } from '../schemas/interface.js';
12
14
  import { JsonTypeName } from '../schemas/primitives.js';
13
- import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
15
+ import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
14
16
  function visitOperand(operand, path, insideQuantifier, visit) {
15
17
  if ('pointer' in operand)
16
18
  visit({ pointer: operand.pointer, path, insideQuantifier });
@@ -52,6 +54,42 @@ function forEachCheckPointer(contract, visit) {
52
54
  visitExpression(oracle.check, 'check', false, (site) => visit(site, oracle.id));
53
55
  });
54
56
  }
57
+ /**
58
+ * Every interaction-rooted pointer the contract writes down, wherever it sits:
59
+ * an oracle's check and its direction's evidence targets, a rubric criterion's
60
+ * evidence, and each operation's sensitivity-witness relation.
61
+ *
62
+ * Broader than `forEachCheckPointer`, which walks oracle checks alone, because
63
+ * an artifact identifier is an authoring fault at every site that names one and
64
+ * a check that walked only the checks would report half of them.
65
+ */
66
+ export function forEachArtifactPointer(contract, visit) {
67
+ const seen = (pointer, artifactPath) => {
68
+ if (pointer.startsWith('@'))
69
+ return;
70
+ visit(pointer, artifactPath);
71
+ };
72
+ contract.oracles.forEach((oracle) => {
73
+ if (oracle.check !== null)
74
+ visitExpression(oracle.check, 'check', false, (site) => seen(site.pointer, `EvalContract.oracles[id=${oracle.id}].${site.path}`));
75
+ oracle.direction?.evidenceTargets.forEach((target, index) => {
76
+ seen(target, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets[${index}]`);
77
+ });
78
+ });
79
+ contract.rubrics.forEach((rubric) => {
80
+ rubric.criteria.forEach((criterion) => {
81
+ seen(criterion.evidence, `EvalContract.rubrics[id=${rubric.id}].criteria[id=${criterion.id}].evidence`);
82
+ });
83
+ });
84
+ contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
85
+ operationsOf(iface).forEach((operation, operationIndex) => {
86
+ const witness = operation.sensitivityWitness;
87
+ if (witness === null)
88
+ return;
89
+ visitExpression(witness.relation, 'relation', false, (site) => seen(site.pointer, `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}].sensitivityWitness.${site.path}`));
90
+ });
91
+ });
92
+ }
55
93
  // ---- malformed-operator-expression: @/ outside any quantifier -----------
56
94
  export function checkBoundElementScope(contract) {
57
95
  forEachCheckPointer(contract, (site, oracleId) => {
@@ -60,6 +98,34 @@ export function checkBoundElementScope(contract) {
60
98
  }
61
99
  });
62
100
  }
101
+ /** `checkBoundElementScope` over one bare `Expression`. */
102
+ export function checkExpressionBoundElementScope(expression, artifactPath) {
103
+ visitExpression(expression, '', false, (site) => {
104
+ if (site.pointer.startsWith('@') && !site.insideQuantifier) {
105
+ throw new StructuralFailure('malformed-operator-expression', `${artifactPath}${site.path}`, `bound-element pointer "${site.pointer}" appears outside any quantifier's predicate; "@/" binds only inside a quantifier (AD-26)`);
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * Visits every `{ pointer }` operand of one bare `Expression`, which a
111
+ * probe-side legality pass needs for the rules a contract has no equivalent of:
112
+ * that every pointer roots at the reserved step identifier, and which evidence
113
+ * channels the condition actually names.
114
+ */
115
+ export function forEachExpressionPointer(expression, visit) {
116
+ visitExpression(expression, '', false, (site) => {
117
+ visit(site.pointer, site.path);
118
+ });
119
+ }
120
+ /** `checkEvidenceReachability` over one bare `Expression` and one operation. */
121
+ export function checkExpressionEvidenceReachability(expression, artifactPath, operation) {
122
+ visitExpression(expression, '', false, (site) => {
123
+ const result = evaluateReachabilityAgainstOperation(site.pointer, operation);
124
+ if (!result.reachable) {
125
+ throw new StructuralFailure('unreachable-check-evidence', `${artifactPath}${site.path}`, `"${site.pointer}" ${result.reason}`);
126
+ }
127
+ });
128
+ }
63
129
  const reachable = () => ({ reachable: true });
64
130
  const unreachable = (reason) => ({
65
131
  reachable: false,
@@ -81,7 +147,11 @@ function descendsIntoDeclaredScalar(types, tail, firstToken) {
81
147
  }
82
148
  /**
83
149
  * Non-throwing core that `checkEvidenceReachability` wraps; also called
84
- * directly by tests.
150
+ * directly by tests. The two lines below are the whole of what ties this to a
151
+ * declared interaction plan; everything past them reads the operation alone,
152
+ * which is what `evaluateReachabilityAgainstOperation` exposes for a probe-side
153
+ * condition, whose one step identifier is reserved and resolves to the
154
+ * signature's home operation with no plan in sight.
85
155
  */
86
156
  export function evaluatePointerReachability(pointer, index) {
87
157
  // A `@/` pointer is relative to a bound element. Nothing declared to
@@ -93,51 +163,109 @@ export function evaluatePointerReachability(pointer, index) {
93
163
  if (step === undefined) {
94
164
  return unreachable('names a step the interaction plan does not declare');
95
165
  }
96
- const operation = index.operationOf(step.operationId);
166
+ const operation = anyOperationOf(index, step.operationId);
97
167
  if (operation === undefined) {
98
168
  return unreachable(`names step "${target.stepId}", which names operation "${step.operationId}", not declared by any permitted interface`);
99
169
  }
100
- if (target.channel === 'stdout' || target.channel === 'stderr') {
101
- // stdout/stderr are always bare strings. A non-empty tail proves the
102
- // pointer unreachable.
103
- if (target.tail.length > 0) {
104
- return unreachable(`addresses a field inside ${target.channel}, which never carries structure to descend into`);
170
+ return evaluateReachabilityAgainstOperation(pointer, operation);
171
+ }
172
+ /**
173
+ * Descent through the operation's response descriptor, from whichever channel
174
+ * that descriptor describes. `channel` is carried only so the reason names the
175
+ * pointer's own root back to the author.
176
+ */
177
+ function descendThroughDescriptor(descriptor, target, operationId, channel) {
178
+ if (target.tail.length === 0)
179
+ return reachable();
180
+ const firstToken = target.tail[0];
181
+ if (firstToken === undefined) {
182
+ // Unreachable: the length check above guarantees an element.
183
+ throw new TypeError('evidence-target tail is non-empty but has no first token');
184
+ }
185
+ const { requiredKeys, permittedKeys, types, collectionLocations } = descriptor;
186
+ // A root-declared collection (`pointer: ''`) indexes directly, bypassing
187
+ // the key check below. `expectedCardinality` bounds the array size
188
+ // (`exact` is the true count; `at-most`/`page-bounded` is an upper
189
+ // bound), so an index at or past it is unreachable.
190
+ const rootCollection = collectionLocations?.find((location) => location.pointer === '');
191
+ if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
192
+ const { expectedCardinality } = rootCollection;
193
+ const bound = expectedCardinality.mode === 'exact'
194
+ ? expectedCardinality.count
195
+ : expectedCardinality.max;
196
+ if (Number(firstToken) >= bound) {
197
+ return unreachable(`addresses ${channel} index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
105
198
  }
106
199
  return reachable();
107
200
  }
108
- if (target.channel === 'response-body') {
109
- if (target.tail.length === 0)
110
- return reachable();
111
- const firstToken = target.tail[0];
112
- if (firstToken === undefined) {
113
- // Unreachable: the length check above guarantees an element.
114
- throw new TypeError('evidence-target tail is non-empty but has no first token');
201
+ if (!requiredKeys.includes(firstToken) &&
202
+ !permittedKeys.includes(firstToken)) {
203
+ return unreachable(`addresses ${channel} field "${firstToken}", which operation "${operationId}" declares in neither requiredKeys nor permittedKeys`);
204
+ }
205
+ if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
206
+ return unreachable(`descends into ${channel} field "${firstToken}", which operation "${operationId}" declares a scalar with no further structure`);
207
+ }
208
+ return reachable();
209
+ }
210
+ /**
211
+ * The same rules, against an operation the caller already resolved. Without
212
+ * this check a probe-side condition addressing an undeclared channel or key
213
+ * resolves absent, every comparison over it resolves `false`, and the probe
214
+ * reports its defect as never triggered — a silently passing run on a signature
215
+ * that was never writable.
216
+ */
217
+ function evaluateReachabilityAgainstOperation(pointer, operation) {
218
+ if (pointer.startsWith('@'))
219
+ return reachable();
220
+ const target = parseEvidenceTarget(pointer);
221
+ const descriptorChannel = descriptorChannelOf(operation);
222
+ const command = isCommandOperation(operation);
223
+ if (target.channel === 'artifact') {
224
+ const { artifactId } = target;
225
+ if (artifactId === null) {
226
+ // Unreachable: parseEvidenceTarget's own guarantee.
227
+ throw new TypeError('artifact evidence target names no artifact');
115
228
  }
116
- const { requiredKeys, permittedKeys, types, collectionLocations } = operation.responseDescriptor;
117
- // A root-declared collection (`pointer: ''`) indexes directly, bypassing
118
- // the key check below. `expectedCardinality` bounds the array size
119
- // (`exact` is the true count; `at-most`/`page-bounded` is an upper
120
- // bound), so an index at or past it is unreachable.
121
- const rootCollection = collectionLocations?.find((location) => location.pointer === '');
122
- if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
123
- const { expectedCardinality } = rootCollection;
124
- const bound = expectedCardinality.mode === 'exact'
125
- ? expectedCardinality.count
126
- : expectedCardinality.max;
127
- if (Number(firstToken) >= bound) {
128
- return unreachable(`addresses response-body index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
229
+ // An identifier the operation does not declare produces no evidence, so
230
+ // it is unreachable and this says so. `checkArtifactReferences` runs
231
+ // earlier in `compile` and reports the more specific
232
+ // `unresolved-artifact-reference` for a contract, so the two never race
233
+ // there; this answer is the only one on the probe side, where that check
234
+ // does not run because it walks a contract rather than a signature.
235
+ if (!declaredArtifactsOf(operation).includes(artifactId)) {
236
+ return unreachable(`names the "${artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes`);
237
+ }
238
+ if (artifactId !== descriptorArtifactOf(operation)) {
239
+ // Declared to exist, and nothing declares its structure: the
240
+ // operation's one descriptor describes a different channel.
241
+ if (target.tail.length > 0) {
242
+ return unreachable(`addresses a field inside the "${artifactId}" artifact, which operation "${operation.operationId}" declares it writes but declares no structure for`);
129
243
  }
130
244
  return reachable();
131
245
  }
132
- if (!requiredKeys.includes(firstToken) &&
133
- !permittedKeys.includes(firstToken)) {
134
- return unreachable(`addresses response-body field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
135
- }
136
- if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
137
- return unreachable(`descends into response-body field "${firstToken}", which operation "${operation.operationId}" declares a scalar with no further structure`);
246
+ return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, `the "${artifactId}" artifact`);
247
+ }
248
+ // The declared output channel descends through the descriptor whatever it
249
+ // is called. This is the one rule; the channels below are the cases where
250
+ // no descriptor applies.
251
+ if (target.channel === descriptorChannel) {
252
+ return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, target.channel);
253
+ }
254
+ if (target.channel === 'stdout' || target.channel === 'stderr') {
255
+ // The stream this operation's descriptor does not describe carries no
256
+ // declared structure, so a non-empty tail proves the pointer
257
+ // unreachable.
258
+ if (target.tail.length > 0) {
259
+ return unreachable(`addresses a field inside ${target.channel}, which operation "${operation.operationId}" declares no structure for`);
138
260
  }
139
261
  return reachable();
140
262
  }
263
+ if (command &&
264
+ (target.channel === 'response-body' ||
265
+ target.channel === 'response-headers' ||
266
+ target.channel === 'response-status')) {
267
+ return unreachable(`addresses ${target.channel} on operation "${operation.operationId}", which runs behind a command and produces no HTTP response`);
268
+ }
141
269
  if (target.channel === 'call-inputs') {
142
270
  if (target.tail.length === 0)
143
271
  return reachable();
@@ -150,7 +278,11 @@ export function evaluatePointerReachability(pointer, index) {
150
278
  if (firstToken === undefined) {
151
279
  throw new TypeError('evidence-target tail is non-empty but has no first token');
152
280
  }
153
- const { requiredKeys, permittedKeys, types } = operation.requestShape[transportChannel];
281
+ const shape = requestShapeOf(operation, transportChannel);
282
+ if (shape === undefined) {
283
+ return unreachable(`addresses call-inputs ${transportChannel}, a channel operation "${operation.operationId}" does not accept input on`);
284
+ }
285
+ const { requiredKeys, permittedKeys, types } = shape;
154
286
  if (!requiredKeys.includes(firstToken) &&
155
287
  !permittedKeys.includes(firstToken)) {
156
288
  return unreachable(`addresses call-inputs ${transportChannel} field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
@@ -0,0 +1,2 @@
1
+ /** `schema-version-mismatch`: the stamp is not the version this build reads. */
2
+ export declare function checkSchemaVersion(stamped: number, accepted: number, artifactPath: string): void;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * AD-11's version equality, performed by the one reader that has the contract.
3
+ *
4
+ * AD-11's rule is that "readers accept an equal `schemaVersion` only and throw
5
+ * `schema-version-mismatch` outside that". `lineage.ts` deliberately keeps the
6
+ * field a plain integer rather than a literal, so that a wrong version arrives
7
+ * as AD-28's named fault instead of an anonymous parse failure, which puts the
8
+ * comparison on whoever reads the artifact. For the eval contract nobody did:
9
+ * a contract stamped 3 parsed, compiled, sealed, and put its stale version into
10
+ * the scoring version digest, and AD-11 exists to keep that number comparable.
11
+ *
12
+ * `RuntimeFault` rather than `StructuralFailure`, against the grain of every
13
+ * other check in this directory. The two registries are disjoint by the
14
+ * Consistency Conventions, AD-5 has no version code, and AD-11 names
15
+ * `schema-version-mismatch` literally. Minting an AD-5 twin would give one
16
+ * condition two codes and make a caller's handler depend on which reader
17
+ * happened to notice first.
18
+ */
19
+ import { RuntimeFault } from '../schemas/faults.js';
20
+ /** `schema-version-mismatch`: the stamp is not the version this build reads. */
21
+ export function checkSchemaVersion(stamped, accepted, artifactPath) {
22
+ if (stamped === accepted)
23
+ return;
24
+ throw new RuntimeFault('schema-version-mismatch', artifactPath, `carries "schemaVersion" ${stamped} where this build reads ${accepted}; a contract written for another version is not read leniently, since its stale version would travel into the scoring version (AD-11)`);
25
+ }
@@ -4,26 +4,45 @@
4
4
  * execute it, and that its leg identifiers are distinct from each other and
5
5
  * from every interaction-plan step.
6
6
  *
7
- * No new AD-5 code is minted. The registry is closed at twenty-one and
8
- * `check:ad5-registry` pins it against the spine, so each defect below takes the
9
- * code that already names it. Two rows stretch that reading: a leg-id equality
7
+ * No new AD-5 code is minted here. `check:ad5-registry` pins the registry
8
+ * against the spine's own table, so each defect below takes the code that
9
+ * already names it. Two rows stretch that reading: a leg-id equality
10
10
  * and a leg-id/step-id collision are identifier collisions, and
11
11
  * `malformed-operator-expression` is the closest available code because the
12
12
  * relation is illegal in its position once its operands cannot be told apart.
13
13
  */
14
14
  import { declaresNoRequestKeys } from '../declared-inputs.ts';
15
15
  import type { EvalContract } from '../schemas/eval-contract.ts';
16
- import type { Operation } from '../schemas/interface.ts';
17
- import { type TransportChannelName } from '../schemas/pointer.ts';
18
- import type { WitnessInputs } from '../schemas/sensitivity-witness.ts';
16
+ import type { AnyOperation } from '../schemas/interface.ts';
17
+ import type { InputChannelName } from '../schemas/pointer.ts';
18
+ import { type ApiWitnessInputs, type WitnessChannel, type WitnessInputs } from '../schemas/sensitivity-witness.ts';
19
19
  export { declaresNoRequestKeys };
20
20
  /**
21
- * The keys one set of witness inputs supplies on one channel. A body that is
22
- * absent, or JSON that is not an object, supplies no keys: the channel is
23
- * declared as a keyed shape, so such a leg omits every required key rather
24
- * than being exempt from the comparison.
21
+ * The transport spelling, for the consumers that can only send one: the probe
22
+ * port carries a method, a path template, and the four transport channels.
25
23
  */
26
- export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportChannelName): readonly string[];
24
+ export declare const isApiWitnessInputs: (inputs: WitnessInputs) => inputs is ApiWitnessInputs;
25
+ /**
26
+ * Whether the leg supplies this channel as an undifferentiated stream.
27
+ *
28
+ * Standard input written as text is one such stream. A `KeyedShapeDescriptor`
29
+ * can say what keys a channel carries and cannot say "one opaque stream", so a
30
+ * command that reads a prompt is declared by naming the one thing the stream
31
+ * carries, and the leg supplies that thing as the text it actually is.
32
+ */
33
+ export declare function suppliesOpaquely(inputs: WitnessInputs, channel: InputChannelName): boolean;
34
+ /**
35
+ * The keys one set of witness inputs supplies on one channel, or none where
36
+ * the leg's spelling has no such channel.
37
+ *
38
+ * `body` and `stdin` are the two channels a leg supplies as a tagged value
39
+ * rather than as a key map, because a leg has to tell an absent value from one
40
+ * carrying JSON null. This function is the bridge between that spelling and
41
+ * the keyed shape the request declares.
42
+ */
43
+ export declare function suppliedKeys(inputs: WitnessInputs, channel: InputChannelName): readonly string[];
44
+ /** One channel's supplied value, for the differential comparison. */
45
+ export declare function suppliedValue(inputs: WitnessInputs, channel: WitnessChannel): unknown;
27
46
  /**
28
47
  * Every channel of one set of witness inputs against the operation that will
29
48
  * receive them. All four, not only the differential channel: `planPreflight`
@@ -49,7 +68,7 @@ export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportCh
49
68
  * contract artifact, the surface AD-18 governs; a placeholder satisfies this
50
69
  * check, and what pre-flight probes is a fixture.
51
70
  */
52
- export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: Operation, owner: string, artifactPath: string): void;
71
+ export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: AnyOperation, owner: string, artifactPath: string): void;
53
72
  /**
54
73
  * Strict-gated, alongside `checkUndeclaredMandatoryInput`, whose code this
55
74
  * shares: `compile.ts` already gates that code behind `options.strict`, and one
@@ -4,28 +4,65 @@
4
4
  * execute it, and that its leg identifiers are distinct from each other and
5
5
  * from every interaction-plan step.
6
6
  *
7
- * No new AD-5 code is minted. The registry is closed at twenty-one and
8
- * `check:ad5-registry` pins it against the spine, so each defect below takes the
9
- * code that already names it. Two rows stretch that reading: a leg-id equality
7
+ * No new AD-5 code is minted here. `check:ad5-registry` pins the registry
8
+ * against the spine's own table, so each defect below takes the code that
9
+ * already names it. Two rows stretch that reading: a leg-id equality
10
10
  * and a leg-id/step-id collision are identifier collisions, and
11
11
  * `malformed-operator-expression` is the closest available code because the
12
12
  * relation is illegal in its position once its operands cannot be told apart.
13
13
  */
14
- import { declaresNoRequestKeys } from '../declared-inputs.js';
14
+ import { declaresNoRequestKeys, isCommandOperation, requestChannelsOf, } from '../declared-inputs.js';
15
15
  import { StructuralFailure } from '../failure-codes.js';
16
- import { TRANSPORT_CHANNELS, } from '../schemas/pointer.js';
16
+ import { operationsOf } from '../schemas/interface.js';
17
+ import { COMMAND_WITNESS_CHANNELS, } from '../schemas/sensitivity-witness.js';
17
18
  import { parseEvidenceTarget } from '../seal/plan-index.js';
18
19
  export { declaresNoRequestKeys };
20
+ /** Which spelling this leg uses. `stdin` is the command shape's own key. */
21
+ const isCommandWitnessInputs = (inputs) => 'stdin' in inputs;
19
22
  /**
20
- * The keys one set of witness inputs supplies on one channel. A body that is
21
- * absent, or JSON that is not an object, supplies no keys: the channel is
22
- * declared as a keyed shape, so such a leg omits every required key rather
23
- * than being exempt from the comparison.
23
+ * The transport spelling, for the consumers that can only send one: the probe
24
+ * port carries a method, a path template, and the four transport channels.
24
25
  */
25
- export function suppliedKeys(inputs, channel) {
26
- if (channel !== 'body')
27
- return Object.keys(inputs[channel]);
28
- const { body } = inputs;
26
+ export const isApiWitnessInputs = (inputs) => 'body' in inputs;
27
+ /**
28
+ * Whether the leg supplies this channel as an undifferentiated stream.
29
+ *
30
+ * Standard input written as text is one such stream. A `KeyedShapeDescriptor`
31
+ * can say what keys a channel carries and cannot say "one opaque stream", so a
32
+ * command that reads a prompt is declared by naming the one thing the stream
33
+ * carries, and the leg supplies that thing as the text it actually is.
34
+ */
35
+ export function suppliesOpaquely(inputs, channel) {
36
+ return (isCommandWitnessInputs(inputs) &&
37
+ channel === 'stdin' &&
38
+ inputs.stdin.kind === 'text');
39
+ }
40
+ /**
41
+ * What an opaque stream fills, and when it cannot say.
42
+ *
43
+ * A text leg supplies the channel's ONE declared key: the stream is the value
44
+ * of the single thing the operation says it reads. Where the channel declares
45
+ * no required key there is nothing for the text to be, and where it declares
46
+ * more than one there is no way to say which the text fills, so both are the
47
+ * authoring fault this reports rather than a comparison that quietly abstains.
48
+ *
49
+ * That is what a keyed `stdin` means for a command that reads text, and it is
50
+ * the reason the declaration stays keyed: an operation that genuinely parses a
51
+ * structured document off standard input declares several keys and supplies
52
+ * them through the leg's `json` arm, and the same descriptor serves both.
53
+ */
54
+ function checkOpaqueStream(shape, channel, operationId, owner, artifactPath) {
55
+ if (shape.requiredKeys.length === 1)
56
+ return;
57
+ throw new StructuralFailure('undeclared-mandatory-input', artifactPath, `${owner} supplies its ${channel} channel as text, which fills the one key the channel declares, but operation "${operationId}" declares ${shape.requiredKeys.length === 0 ? 'no required key there, so the text fills nothing' : `${shape.requiredKeys.length} required keys there, so nothing says which the text fills`} (AD-10, AD-19)`);
58
+ }
59
+ /**
60
+ * The keys a tagged body value supplies. A body that is absent, or JSON that
61
+ * is not an object, supplies no keys: the channel is declared as a keyed
62
+ * shape, so such a leg omits every required key rather than being exempt from
63
+ * the comparison.
64
+ */
65
+ function bodyKeys(body) {
29
66
  if (body.kind !== 'json')
30
67
  return [];
31
68
  const { value } = body;
@@ -33,6 +70,37 @@ export function suppliedKeys(inputs, channel) {
33
70
  return [];
34
71
  return Object.keys(value);
35
72
  }
73
+ /**
74
+ * The keys one set of witness inputs supplies on one channel, or none where
75
+ * the leg's spelling has no such channel.
76
+ *
77
+ * `body` and `stdin` are the two channels a leg supplies as a tagged value
78
+ * rather than as a key map, because a leg has to tell an absent value from one
79
+ * carrying JSON null. This function is the bridge between that spelling and
80
+ * the keyed shape the request declares.
81
+ */
82
+ export function suppliedKeys(inputs, channel) {
83
+ if (isCommandWitnessInputs(inputs)) {
84
+ if (channel === 'stdin')
85
+ return bodyKeys(inputs.stdin);
86
+ if (channel === 'argument' || channel === 'option')
87
+ return Object.keys(inputs[channel]);
88
+ if (channel === 'environment')
89
+ return Object.keys(inputs.environment);
90
+ return [];
91
+ }
92
+ if (channel === 'body')
93
+ return bodyKeys(inputs.body);
94
+ if (channel === 'path' || channel === 'query')
95
+ return Object.keys(inputs[channel]);
96
+ if (channel === 'header')
97
+ return Object.keys(inputs.header);
98
+ return [];
99
+ }
100
+ /** One channel's supplied value, for the differential comparison. */
101
+ export function suppliedValue(inputs, channel) {
102
+ return inputs[channel];
103
+ }
36
104
  /**
37
105
  * Every channel of one set of witness inputs against the operation that will
38
106
  * receive them. All four, not only the differential channel: `planPreflight`
@@ -59,8 +127,11 @@ export function suppliedKeys(inputs, channel) {
59
127
  * check, and what pre-flight probes is a fixture.
60
128
  */
61
129
  export function checkInputsAgainstShape(inputs, operation, owner, artifactPath) {
62
- for (const channel of TRANSPORT_CHANNELS) {
63
- const shape = operation.requestShape[channel];
130
+ for (const { channel, shape } of requestChannelsOf(operation)) {
131
+ if (suppliesOpaquely(inputs, channel)) {
132
+ checkOpaqueStream(shape, channel, operation.operationId, owner, artifactPath);
133
+ continue;
134
+ }
64
135
  const supplied = suppliedKeys(inputs, channel);
65
136
  for (const key of shape.requiredKeys) {
66
137
  if (supplied.includes(key))
@@ -78,7 +149,7 @@ export function checkInputsAgainstShape(inputs, operation, owner, artifactPath)
78
149
  const operationPath = (interfaceIndex, operationIndex) => `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
79
150
  function forEachWitness(contract, visit) {
80
151
  contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
81
- iface.operations.forEach((operation, operationIndex) => {
152
+ operationsOf(iface).forEach((operation, operationIndex) => {
82
153
  const witness = operation.sensitivityWitness;
83
154
  if (witness === null)
84
155
  return;
@@ -94,13 +165,13 @@ function forEachWitness(contract, visit) {
94
165
  */
95
166
  export function checkSensitivityWitnessDeclared(contract) {
96
167
  contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
97
- iface.operations.forEach((operation, operationIndex) => {
168
+ operationsOf(iface).forEach((operation, operationIndex) => {
98
169
  const path = operationPath(interfaceIndex, operationIndex);
99
170
  const witness = operation.sensitivityWitness;
100
171
  if (witness === null) {
101
172
  if (declaresNoRequestKeys(operation))
102
173
  return;
103
- throw new StructuralFailure('undeclared-mandatory-input', path, `operation "${operation.operationId}" declares request keys but no sensitivity witness; only an operation declaring no keys in any channel is exempt (AD-10)`);
174
+ throw new StructuralFailure('undeclared-mandatory-input', path, `operation "${operation.operationId}" declares request keys but no sensitivity witness; only an operation declaring no keys in any channel is exempt (AD-10). The relation is the author's to choose and need not assert the two legs differ: an operation that is insensitive to its declared inputs by design declares a witness whose relation says so, which is a true and checkable claim about it, and gets the weaker guarantee that follows`);
104
175
  }
105
176
  witness.legs.forEach((leg, legIndex) => {
106
177
  checkInputsAgainstShape(leg.inputs, operation, `leg "${leg.legId}"`, `${path}.sensitivityWitness.legs[${legIndex}]`);
@@ -137,8 +208,22 @@ function addressedStepIds(expression) {
137
208
  walk(expression);
138
209
  return found;
139
210
  }
140
- /** the channel AD-10 selects for an operation, by its state-change marker. */
141
- const legalChannels = (operation) => operation.stateChangeMarker ? ['body'] : ['path', 'query'];
211
+ const MUTATING_CHANNELS = ['body'];
212
+ const READ_CHANNELS = ['path', 'query'];
213
+ /**
214
+ * The channels AD-10 admits for an operation's witness.
215
+ *
216
+ * Off an interface that speaks HTTP the state-change marker selects one, since
217
+ * a read carries its identifier in the URL and a write carries it in the body.
218
+ * A command carries its inputs the same way whether or not it changes state,
219
+ * so the marker selects nothing there and all four command channels are
220
+ * admitted. The author picks the one their operation is actually sensitive on.
221
+ */
222
+ const legalChannels = (operation) => {
223
+ if (isCommandOperation(operation))
224
+ return COMMAND_WITNESS_CHANNELS;
225
+ return operation.stateChangeMarker ? MUTATING_CHANNELS : READ_CHANNELS;
226
+ };
142
227
  /**
143
228
  * The shape rules: the differential channel agrees with the state-change
144
229
  * marker, the relation reads both legs and nothing else, and a declared
@@ -158,8 +243,8 @@ export function checkWitnessLegality(contract) {
158
243
  const [first, second] = witness.legs;
159
244
  if (first !== undefined &&
160
245
  second !== undefined &&
161
- JSON.stringify(first.inputs[witness.channel]) ===
162
- JSON.stringify(second.inputs[witness.channel])) {
246
+ JSON.stringify(suppliedValue(first.inputs, witness.channel)) ===
247
+ JSON.stringify(suppliedValue(second.inputs, witness.channel))) {
163
248
  throw new StructuralFailure('malformed-operator-expression', `${path}.sensitivityWitness.legs`, `both legs of witness "${witness.witnessId}" supply the same ${witness.channel} value, so the pair is not a differential (AD-10)`);
164
249
  }
165
250
  const legIds = witness.legs.map((leg) => leg.legId);
@@ -181,7 +266,9 @@ export function checkWitnessLegality(contract) {
181
266
  if (reset === null)
182
267
  return;
183
268
  const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
184
- const operation = iface?.operations.find((candidate) => candidate.operationId === reset.operationId);
269
+ const operation = iface === undefined
270
+ ? undefined
271
+ : operationsOf(iface).find((candidate) => candidate.operationId === reset.operationId);
185
272
  if (operation === undefined) {
186
273
  throw new StructuralFailure('unreachable-check-evidence', 'EvalContract.fixtureReset', `the fixture reset names operation "${reset.operationId}" on interface "${reset.interfaceId}", which the contract does not declare (AD-10)`);
187
274
  }
@@ -0,0 +1,2 @@
1
+ import type { EvalContract } from '../schemas/eval-contract.ts';
2
+ export declare function checkStepReferenceReducibility(contract: EvalContract): void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `irreducible-step-reference`: two steps one direction references render to
3
+ * the same derived reference even after full escalation.
4
+ *
5
+ * AD-16 keeps step identifiers out of the sealed brief, so a direction names
6
+ * each step it references by a phrase derived from what the contract declares
7
+ * about it. Where two steps of one operation declare nothing that tells them
8
+ * apart, the escalation ladder runs out and the renderer has no way to name one
9
+ * without naming the other.
10
+ *
11
+ * The check runs the seal-side renderer rather than reimplementing its ladder.
12
+ * A second implementation of a three-rung escalation with a budget would be a
13
+ * second thing to keep in step with the first, and the failure this reports is
14
+ * defined as "what that renderer cannot do" rather than as a rule of its own.
15
+ * That is the same reasoning `checkEvidenceReachability` follows in reusing
16
+ * `evaluatePointerReachability`.
17
+ *
18
+ * Without it a contract passes every other check and then fails inside `seal`,
19
+ * which is the one authoring fault in the tree that reached a caller as a
20
+ * stack trace instead of a code and a path.
21
+ */
22
+ import { StructuralFailure } from '../failure-codes.js';
23
+ import { renderEvidenceReferences } from '../seal/derived-reference.js';
24
+ import { buildPlanIndex } from '../seal/plan-index.js';
25
+ export function checkStepReferenceReducibility(contract) {
26
+ let index;
27
+ for (const oracle of contract.oracles) {
28
+ const { direction } = oracle;
29
+ if (direction === null || direction.evidenceTargets.length === 0)
30
+ continue;
31
+ index ??= buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
32
+ try {
33
+ renderEvidenceReferences(direction.evidenceTargets, index);
34
+ }
35
+ catch (error) {
36
+ if (!(error instanceof StructuralFailure)) {
37
+ // Every other way this renderer fails is a precondition violation
38
+ // on input `compile` has already checked, so it is a fault rather
39
+ // than an authoring fault and propagates unchanged.
40
+ throw error;
41
+ }
42
+ // Re-thrown with the contract-side address. `StructuralFailure`
43
+ // carries the code and the path and folds the detail into its
44
+ // message, so the detail is recovered from the message rather than
45
+ // from a field the class does not expose.
46
+ throw new StructuralFailure(error.code, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets`, error.message.slice(`${error.code} in ${error.artifactPath}: `.length));
47
+ }
48
+ }
49
+ }