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,615 @@
1
+ /**
2
+ * AD-9's corpus gate: whether one probe earned its ground truth, and which
3
+ * probes may enter a sealed set.
4
+ *
5
+ * Pure and total, like the rest of `core/score`: every verdict and every reason
6
+ * comes back as data, nothing throws on a badly-shaped probe, and no AD-6
7
+ * outcome state is assigned. The failure vocabulary is closed and separate from
8
+ * AD-5's, because AD-5 is compile-time over contracts and `compile` never sees
9
+ * a probe.
10
+ *
11
+ * The gate is what keeps the witness match's ordinary path total. `resolveCheck`
12
+ * throws in seven places; four of them are reachable only through operand
13
+ * classes rejected here, `{ referenceSet }` above all, which resolves absent
14
+ * against the probe side's empty reference-set map and then throws a plain
15
+ * `Error`.
16
+ */
17
+ import { checkExpressionOperandLegality, checkExpressionQuantifierNesting, checkExpressionQuantifierOverNonCollection, checkExpressionRegexConstructs, walkExpression, } from '../compile/expression-legality.js';
18
+ import { operationSignature } from '../compile/interface-inventory.js';
19
+ import { checkExpressionBoundElementScope, checkExpressionEvidenceReachability, forEachExpressionPointer, } from '../compile/reachability.js';
20
+ import { StructuralFailure } from '../failure-codes.js';
21
+ import { OBSERVED_STEP_ID, } from '../schemas/defect-signature.js';
22
+ import { TRANSPORT_CHANNELS, } from '../schemas/pointer.js';
23
+ import { parseEvidenceTarget } from '../seal/plan-index.js';
24
+ /**
25
+ * The closed reason set. Nothing outside it can come back from `qualifyProbe`,
26
+ * so a caller routing a rejection to a rung has a finite table to write.
27
+ */
28
+ export const QUALIFICATION_FAILURES = [
29
+ 'qualification-route-incompatible',
30
+ 'qualification-defect-sources-mixed',
31
+ 'qualification-evidence-unverified',
32
+ 'signature-absent',
33
+ 'signature-present-on-canary',
34
+ 'signature-interface-kind-unsupported',
35
+ 'signature-observable-channel-not-response-side',
36
+ 'condition-channels-underspecified',
37
+ 'condition-disjunct-without-response-channel',
38
+ 'condition-selector-key-undeclared',
39
+ 'condition-pointer-not-observation-rooted',
40
+ 'condition-pointer-unwritable',
41
+ 'condition-text-channel-on-api',
42
+ 'condition-reference-set-operand',
43
+ 'condition-operand-illegal',
44
+ 'condition-regex-illegal',
45
+ 'condition-quantifier-nesting',
46
+ 'condition-quantifier-over-non-collection',
47
+ 'condition-bound-element-outside-quantifier',
48
+ ];
49
+ /**
50
+ * Resolves a signature's home operation against a contract's operation
51
+ * inventory, comparing method plus path template with parameter names erased
52
+ * first, so a corpus signature on `/notes/{id}` binds a contract declaring
53
+ * `/notes/{noteId}`. A post-erasure collision inside one contract has already
54
+ * failed compilation under `duplicate-operation-signature`, so the first match
55
+ * is the only match for any contract that compiled.
56
+ */
57
+ export function resolveHomeOperation(signature, interfaces) {
58
+ const wanted = operationSignature(signature);
59
+ for (const iface of interfaces) {
60
+ for (const operation of iface.operations) {
61
+ if (operationSignature(operation) === wanted)
62
+ return operation;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ // AD-26's channels that carry what came back, as opposed to what was sent. The
68
+ // channel rule below needs at least one of these, or a condition naming two
69
+ // channels could name `call-inputs` twice and pass.
70
+ const RESPONSE_SIDE_CHANNELS = new Set([
71
+ 'response-body',
72
+ 'response-headers',
73
+ 'response-status',
74
+ 'stdout',
75
+ 'stderr',
76
+ 'exit-code',
77
+ ]);
78
+ // The three channels an `api` signature can never manifest in. The contract
79
+ // side cannot decide this: reachability rejects a tailed `stdout` pointer and
80
+ // returns reachable for a bare one unconditionally, because an operation
81
+ // carries no interface kind. The signature's own declared kind is what makes
82
+ // the rule decidable, so it is spent here rather than left as prose.
83
+ const TEXT_CHANNELS = new Set([
84
+ 'stdout',
85
+ 'stderr',
86
+ 'exit-code',
87
+ ]);
88
+ const probePath = (probe, tail) => `Probe[probeId=${probe.probeId}]${tail}`;
89
+ /**
90
+ * Which routes this probe's class and control status admit. An empty list is
91
+ * the illegal cell: a pairing no route can satisfy, which the schema admits so
92
+ * this gate can name it.
93
+ *
94
+ * `expectedClean` is read first, because it is the schema's own discriminator,
95
+ * which is what makes a canary clean control an illegal cell rather than a
96
+ * probe owing two routes.
97
+ */
98
+ function admissibleRoutes(probe) {
99
+ if (probe.expectedClean) {
100
+ return probe.probeClass === 'zero-action' ? ['clean-control'] : [];
101
+ }
102
+ switch (probe.probeClass) {
103
+ case 'gameability':
104
+ return ['gameability'];
105
+ case 'canary':
106
+ return ['canary'];
107
+ default: {
108
+ // A class that seeds admits no route when it seeds nothing. Both
109
+ // source rules read "every seeded source is X", which is vacuously
110
+ // true of an empty array both ways, so a literal reading hands a
111
+ // probe with no defect BOTH routes and lets a corpus author attach
112
+ // either route's evidence to a defect that does not exist. AD-9
113
+ // qualifies a seeded defect, so with none there is nothing for the
114
+ // evidence to be about.
115
+ if (probe.defects.length === 0)
116
+ return [];
117
+ const sources = new Set(probe.defects.map((defect) => defect.source));
118
+ const routes = [];
119
+ if (!sources.has('controlled-mutation'))
120
+ routes.push('historical');
121
+ if (!sources.has('natural'))
122
+ routes.push('controlled-mutation');
123
+ return routes;
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * The route rules: a route the pairing admits, and a `defects` array whose
129
+ * sources agree with a single route.
130
+ *
131
+ * A mixed array is its own failure rather than being resolved by majority or by
132
+ * first entry: a probe seeding one mined defect and one introduced mutation has
133
+ * two qualification arguments and AD-9 gives it one record.
134
+ */
135
+ function checkRoute(probe, failures) {
136
+ const path = probePath(probe, '.qualification.route');
137
+ if (!probe.expectedClean &&
138
+ probe.probeClass !== 'gameability' &&
139
+ probe.probeClass !== 'canary') {
140
+ const sources = new Set(probe.defects.map((defect) => defect.source));
141
+ if (sources.size > 1) {
142
+ failures.push({
143
+ code: 'qualification-defect-sources-mixed',
144
+ artifactPath: probePath(probe, '.defects'),
145
+ detail: 'seeds both a natural defect and a controlled mutation, which no single AD-9 route qualifies (AD-9)',
146
+ });
147
+ }
148
+ }
149
+ const admissible = admissibleRoutes(probe);
150
+ if (!admissible.includes(probe.qualification.route)) {
151
+ failures.push({
152
+ code: 'qualification-route-incompatible',
153
+ artifactPath: path,
154
+ // Three conditions empty the admissible list and they send a corpus
155
+ // author to three different fields, so the detail names which one
156
+ // fired rather than blaming the class pairing for all of them.
157
+ detail: admissible.length > 0
158
+ ? `route "${probe.qualification.route}" is not one of ${admissible.map((route) => `"${route}"`).join(', ')} for probeClass "${probe.probeClass}" with expectedClean ${probe.expectedClean} (AD-9)`
159
+ : emptyRouteReason(probe),
160
+ });
161
+ }
162
+ }
163
+ /** Why no route is admissible, in the terms of the field that caused it. */
164
+ function emptyRouteReason(probe) {
165
+ if (!probe.expectedClean && probe.defects.length === 0) {
166
+ return `probeClass "${probe.probeClass}" seeds a defect and this probe's defects array is empty, so no AD-9 route has a seeded defect to qualify (AD-9)`;
167
+ }
168
+ if (new Set(probe.defects.map((defect) => defect.source)).size > 1) {
169
+ return 'the defects array seeds both a natural defect and a controlled mutation, so no single AD-9 route qualifies this probe (AD-9)';
170
+ }
171
+ return `probeClass "${probe.probeClass}" with expectedClean ${probe.expectedClean} admits no AD-9 route at all, so no qualification record can be written for it (AD-9)`;
172
+ }
173
+ /** The two routes whose evidence carries its own verified/not-verified flag. */
174
+ function checkRouteEvidence(probe, failures) {
175
+ const { qualification } = probe;
176
+ if (qualification.route === 'historical' &&
177
+ !qualification.oracleStableAcrossRevisions) {
178
+ failures.push({
179
+ code: 'qualification-evidence-unverified',
180
+ artifactPath: probePath(probe, '.qualification.oracleStableAcrossRevisions'),
181
+ detail: 'AD-9 qualifies a historical probe only with the oracle stable across both revisions, and this record states it was not',
182
+ });
183
+ }
184
+ if (qualification.route === 'controlled-mutation' &&
185
+ !qualification.rollbackVerified) {
186
+ failures.push({
187
+ code: 'qualification-evidence-unverified',
188
+ artifactPath: probePath(probe, '.qualification.rollbackVerified'),
189
+ detail: 'AD-9 qualifies a controlled mutation only with verified rollback or cleanup, and this record states it was not verified',
190
+ });
191
+ }
192
+ }
193
+ /**
194
+ * The predicate's own operands: no reference set, every fully-rooted pointer at
195
+ * the reserved step identifier, and no text channel on an `api` signature.
196
+ * Returns the distinct channels the fully-rooted pointers name, which is what
197
+ * the channel rule counts.
198
+ */
199
+ function checkOperandsAndCollectChannels(probe, signature, failures) {
200
+ const conditionPath = probePath(probe, '.defectSignature.condition.predicate');
201
+ const channels = new Set();
202
+ walkExpression(signature.condition.predicate, 0, '', {
203
+ onOperand: (operand, _op, _position, path) => {
204
+ if ('referenceSet' in operand) {
205
+ failures.push({
206
+ code: 'condition-reference-set-operand',
207
+ artifactPath: `${conditionPath}${path}`,
208
+ detail: `names referenceSet "${operand.referenceSet}"; a corpus signature declares no reference sets, so the operand resolves absent and faults the evaluator rather than discriminating (AD-4, AD-26)`,
209
+ });
210
+ }
211
+ },
212
+ onSetOperand: (setOperand, path) => {
213
+ if ('referenceSet' in setOperand) {
214
+ failures.push({
215
+ code: 'condition-reference-set-operand',
216
+ artifactPath: `${conditionPath}${path}`,
217
+ detail: `names referenceSet "${setOperand.referenceSet}"; a corpus signature declares no reference sets (AD-4, AD-26)`,
218
+ });
219
+ }
220
+ },
221
+ });
222
+ forEachExpressionPointer(signature.condition.predicate, (pointer, path) => {
223
+ // A bound-element pointer is relative to whatever a quantifier bound and
224
+ // roots at no step identifier at all.
225
+ if (pointer.startsWith('@'))
226
+ return;
227
+ const target = parseEvidenceTarget(pointer);
228
+ if (target.stepId !== OBSERVED_STEP_ID) {
229
+ failures.push({
230
+ code: 'condition-pointer-not-observation-rooted',
231
+ artifactPath: `${conditionPath}${path}`,
232
+ detail: `"${pointer}" roots at step "${target.stepId}"; a signature is authored against a corpus and roots every pointer at "${OBSERVED_STEP_ID}", since a step identifier is contract-relative and resolves nothing against a second contract (AD-40)`,
233
+ });
234
+ return;
235
+ }
236
+ channels.add(target.channel);
237
+ if (signature.interfaceKind === 'api' &&
238
+ TEXT_CHANNELS.has(target.channel)) {
239
+ failures.push({
240
+ code: 'condition-text-channel-on-api',
241
+ artifactPath: `${conditionPath}${path}`,
242
+ detail: `"${pointer}" addresses ${target.channel}, which an api interface never produces (AD-19, AD-26)`,
243
+ });
244
+ }
245
+ });
246
+ return channels;
247
+ }
248
+ /**
249
+ * The selector's own keys against the home operation's declared request shape.
250
+ *
251
+ * A selector binding a key the operation declares in neither `requiredKeys`
252
+ * nor `permittedKeys` matches no observation at all, so every candidate is
253
+ * filtered out, the probe reports `not-triggered`, and a typo becomes a
254
+ * silently passing run. Same shape as the pointer writability check, reached
255
+ * through the selector.
256
+ *
257
+ * Stricter than the contract side deliberately. `compile/bindings.ts` leaves the
258
+ * equivalent key rule to `undeclared-mandatory-input`, which AD-4 makes
259
+ * strict-only, because a contract may be compiled either way. A corpus gate has
260
+ * no lenient mode: AD-9's "an unqualified probe cannot enter a sealed set" states
261
+ * one bar, so the probe side applies the rule unconditionally.
262
+ *
263
+ * Declaration-dependent, so it runs only where the caller supplied an inventory,
264
+ * and `declarationChecksRan` says when it did not.
265
+ */
266
+ function checkSelectorKeys(probe, signature, operation, failures) {
267
+ const { inputBinding } = signature.condition.selector;
268
+ for (const channel of TRANSPORT_CHANNELS) {
269
+ const binding = inputBinding[channel];
270
+ if (binding === null)
271
+ continue;
272
+ const { requiredKeys, permittedKeys, types } = operation.requestShape[channel];
273
+ for (const key of Object.keys(binding)) {
274
+ const at = probePath(probe, `.defectSignature.condition.selector.inputBinding.${channel}[${JSON.stringify(key)}]`);
275
+ if (!requiredKeys.includes(key) && !permittedKeys.includes(key)) {
276
+ failures.push({
277
+ code: 'condition-selector-key-undeclared',
278
+ artifactPath: at,
279
+ detail: `operation "${operation.operationId}" declares "${key}" in neither requiredKeys nor permittedKeys of its ${channel} channel, so this selector matches no observation and the probe can never be triggered (AD-4, AD-40)`,
280
+ });
281
+ continue;
282
+ }
283
+ // "Declared" has a second, narrower meaning for the one member that
284
+ // reads the type map: a key may be permitted and still carry no
285
+ // declared type, and a type-violating binding fails closed on an
286
+ // indeterminate type because it cannot prove a violation. Checking
287
+ // only key presence would leave that member matching nothing, which
288
+ // is the same silent pass through the other definition of declared.
289
+ // The other two members never read the type map and get no rule here.
290
+ const value = binding[key];
291
+ if (value === undefined || !('matcher' in value))
292
+ continue;
293
+ if (value.matcher !== 'type-violating')
294
+ continue;
295
+ const declared = types[key];
296
+ if (declared !== undefined && declared !== null)
297
+ continue;
298
+ failures.push({
299
+ code: 'condition-selector-key-undeclared',
300
+ artifactPath: at,
301
+ detail: `operation "${operation.operationId}" ${declared === undefined ? 'does not declare' : 'declares an indeterminate'} type for "${key}" in its ${channel} channel, and a type-violating binding cannot prove a violation against one, so this selector matches no observation (AD-4, AD-40)`,
302
+ });
303
+ }
304
+ }
305
+ }
306
+ /**
307
+ * The declared observable channel's own two rules, which the pointer walk above
308
+ * cannot supply because it only sees what the predicate wrote.
309
+ *
310
+ * AD-40 calls it "the observable channel it manifests in", so it names what came
311
+ * back. `call-inputs` names what was sent and can never be where a defect
312
+ * manifests, and the channel rule reads this field first, so leaving it
313
+ * unchecked would make an unchecked field load-bearing. On an `api` signature
314
+ * the three text channels are rejected for the same reason a pointer into one
315
+ * is: an api interface never produces them, which is the same rule the pointer
316
+ * walk applies, read on the declaration too.
317
+ */
318
+ function checkObservableChannel(probe, signature, failures) {
319
+ const path = probePath(probe, '.defectSignature.observableChannel');
320
+ if (!RESPONSE_SIDE_CHANNELS.has(signature.observableChannel)) {
321
+ failures.push({
322
+ code: 'signature-observable-channel-not-response-side',
323
+ artifactPath: path,
324
+ detail: `declares observableChannel "${signature.observableChannel}", which records what was sent rather than what came back, so no defect manifests in it (AD-40)`,
325
+ });
326
+ return;
327
+ }
328
+ if (signature.interfaceKind === 'api' &&
329
+ TEXT_CHANNELS.has(signature.observableChannel)) {
330
+ failures.push({
331
+ code: 'condition-text-channel-on-api',
332
+ artifactPath: path,
333
+ detail: `declares observableChannel "${signature.observableChannel}", which an api interface never produces (AD-19, AD-26)`,
334
+ });
335
+ }
336
+ }
337
+ /**
338
+ * AD-40's "name the response channel or at least two channels", made decidable:
339
+ * the set of distinct channels the predicate's fully-rooted pointers name must
340
+ * contain the declared observable channel, or hold two or more members with at
341
+ * least one on the response side.
342
+ *
343
+ * The selector's own bindings do not count, since they describe what was sent.
344
+ * The rule exists to reject "the evidence contains the string I sent", so
345
+ * `call-inputs` twice must not pass, and does not.
346
+ */
347
+ function checkChannels(probe, signature, channels, failures) {
348
+ // Guarded on the response side rather than testing membership alone. An
349
+ // unguarded early return lets `observableChannel: 'call-inputs'` pass a
350
+ // predicate whose only pointer is a call-inputs pointer, which is the
351
+ // "the evidence contains the string I sent" condition this rule exists to
352
+ // reject, arriving through the field that names the rule. The declared
353
+ // channel is rejected outright one check up, and this guard keeps the rule
354
+ // true of this function on its own.
355
+ if (RESPONSE_SIDE_CHANNELS.has(signature.observableChannel) &&
356
+ channels.has(signature.observableChannel))
357
+ return;
358
+ const namesTwo = channels.size >= 2;
359
+ const namesResponse = [...channels].some((channel) => RESPONSE_SIDE_CHANNELS.has(channel));
360
+ if (namesTwo && namesResponse)
361
+ return;
362
+ failures.push({
363
+ code: 'condition-channels-underspecified',
364
+ artifactPath: probePath(probe, '.defectSignature.condition.predicate'),
365
+ detail: `names ${channels.size === 0 ? 'no channel' : [...channels].map((channel) => `"${channel}"`).join(', ')}, which is neither the declared observableChannel "${signature.observableChannel}" nor two channels with a response-side member (AD-40)`,
366
+ });
367
+ }
368
+ /**
369
+ * Every distinct channel one expression reads.
370
+ *
371
+ * A bound-element pointer roots at no step identifier, so it names no channel of
372
+ * its own; it reads whatever the enclosing quantifier's collection is rooted in,
373
+ * which `boundChannel` carries. Without that inheritance a disjunct made
374
+ * entirely of `@/` comparisons over a response-body collection would look like
375
+ * it reads nothing at all.
376
+ */
377
+ function channelsNamedBy(expression, boundChannel) {
378
+ const named = new Set();
379
+ forEachExpressionPointer(expression, (pointer) => {
380
+ if (pointer.startsWith('@')) {
381
+ if (boundChannel !== null)
382
+ named.add(boundChannel);
383
+ return;
384
+ }
385
+ named.add(parseEvidenceTarget(pointer).channel);
386
+ });
387
+ return named;
388
+ }
389
+ /** The channel a quantifier's collection is rooted in, where it has one. */
390
+ function collectionChannel(collection) {
391
+ if (!('pointer' in collection))
392
+ return null;
393
+ if (collection.pointer.startsWith('@'))
394
+ return null;
395
+ return parseEvidenceTarget(collection.pointer).channel;
396
+ }
397
+ /**
398
+ * A disjunct that names only what was sent decides the whole condition on its
399
+ * own, so the channel rule above passes on a predicate that discriminates
400
+ * nothing.
401
+ *
402
+ * The channel rule counts the channels a condition names and cannot see where
403
+ * the truth value comes from. `any(existence(call-inputs/body/title),
404
+ * existence(response-body/message))` names two channels, one of them
405
+ * response-side, and passes. Its first disjunct is true of every candidate,
406
+ * because the selector's own binding already guarantees that key is present on
407
+ * anything that became a candidate. The condition then resolves
408
+ * `true` on the observation where the system behaved correctly, and a finding
409
+ * citing it reports a catch: the catch rate 1.00 by construction, one level in
410
+ * from the direct spelling.
411
+ *
412
+ * The rule is scoped to `any` and nothing else, because only a disjunction lets
413
+ * one operand carry the verdict alone. Under `all` no single operand can, so a
414
+ * sent-side conjunct is a legitimate half of the two-channel conditions AD-40's
415
+ * wording exists to admit: "the response echoes the request body" is exactly
416
+ * that shape. Syntactic and decidable from the expression, rather than an
417
+ * attempt to decide what a predicate's truth actually depends on, which
418
+ * quantifiers make undecidable.
419
+ */
420
+ function checkDisjuncts(probe, signature, failures) {
421
+ const conditionPath = probePath(probe, '.defectSignature.condition.predicate');
422
+ walkAnyNodes(signature.condition.predicate, '', null, (operand, path, bound) => {
423
+ const named = channelsNamedBy(operand, bound);
424
+ if ([...named].some((channel) => RESPONSE_SIDE_CHANNELS.has(channel))) {
425
+ return;
426
+ }
427
+ failures.push({
428
+ code: 'condition-disjunct-without-response-channel',
429
+ artifactPath: `${conditionPath}${path}`,
430
+ detail: `this operand of "any" names ${named.size === 0 ? 'no channel at all' : [...named].map((channel) => `"${channel}"`).join(', ')}, so it can satisfy the whole condition without examining anything that came back (AD-40)`,
431
+ });
432
+ });
433
+ }
434
+ /**
435
+ * Visits each direct operand of every `any` node, wherever it sits, carrying the
436
+ * channel the nearest enclosing quantifier's collection is rooted in. Path
437
+ * spellings follow `walkExpression`'s, so a failure's artifact path reads the
438
+ * same as every other one this module emits.
439
+ */
440
+ function walkAnyNodes(expression, path, boundChannel, visit) {
441
+ if (expression.op === 'any') {
442
+ expression.operands.forEach((operand, index) => {
443
+ visit(operand, `${path}.operands[${index}]`, boundChannel);
444
+ });
445
+ }
446
+ switch (expression.op) {
447
+ case 'not':
448
+ case 'all':
449
+ case 'any':
450
+ expression.operands.forEach((operand, index) => {
451
+ walkAnyNodes(operand, `${path}.operands[${index}]`, boundChannel, visit);
452
+ });
453
+ return;
454
+ case 'for-all':
455
+ case 'for-any':
456
+ walkAnyNodes(expression.predicate, `${path}.predicate`, collectionChannel(expression.collection), visit);
457
+ return;
458
+ default:
459
+ return;
460
+ }
461
+ }
462
+ // Each shipped legality check, run over the bare predicate and reported as a
463
+ // qualification reason. The structural failure's own artifact path and message
464
+ // are carried through, so a rejection reads the same on both sides of the
465
+ // boundary.
466
+ const LEGALITY_CHECKS = [
467
+ {
468
+ code: 'condition-bound-element-outside-quantifier',
469
+ needsOperation: false,
470
+ run: (signature, artifactPath) => {
471
+ checkExpressionBoundElementScope(signature.condition.predicate, artifactPath);
472
+ },
473
+ },
474
+ {
475
+ code: 'condition-operand-illegal',
476
+ needsOperation: false,
477
+ run: (signature, artifactPath) => {
478
+ checkExpressionOperandLegality(signature.condition.predicate, artifactPath);
479
+ },
480
+ },
481
+ {
482
+ code: 'condition-regex-illegal',
483
+ needsOperation: false,
484
+ run: (signature, artifactPath) => {
485
+ checkExpressionRegexConstructs(signature.condition.predicate, artifactPath);
486
+ },
487
+ },
488
+ {
489
+ code: 'condition-quantifier-nesting',
490
+ needsOperation: false,
491
+ run: (signature, artifactPath) => {
492
+ checkExpressionQuantifierNesting(signature.condition.predicate, artifactPath);
493
+ },
494
+ },
495
+ {
496
+ code: 'condition-quantifier-over-non-collection',
497
+ needsOperation: true,
498
+ run: (signature, artifactPath, operation) => {
499
+ if (operation === null)
500
+ return;
501
+ checkExpressionQuantifierOverNonCollection(signature.condition.predicate, artifactPath, { operation, legIds: [OBSERVED_STEP_ID] });
502
+ },
503
+ },
504
+ {
505
+ code: 'condition-pointer-unwritable',
506
+ needsOperation: true,
507
+ run: (signature, artifactPath, operation) => {
508
+ if (operation === null)
509
+ return;
510
+ checkExpressionEvidenceReachability(signature.condition.predicate, artifactPath, operation);
511
+ },
512
+ },
513
+ ];
514
+ function runLegalityChecks(probe, signature, homeOperation, failures) {
515
+ const artifactPath = probePath(probe, '.defectSignature.condition.predicate');
516
+ for (const check of LEGALITY_CHECKS) {
517
+ if (check.needsOperation && homeOperation === null)
518
+ continue;
519
+ try {
520
+ check.run(signature, artifactPath, homeOperation);
521
+ }
522
+ catch (error) {
523
+ if (!(error instanceof StructuralFailure))
524
+ throw error;
525
+ failures.push({
526
+ code: check.code,
527
+ artifactPath: error.artifactPath,
528
+ detail: error.message,
529
+ });
530
+ }
531
+ }
532
+ }
533
+ /**
534
+ * AD-9's gate over one probe. `homeOperation` is the operation the signature
535
+ * resolves to in whatever inventory the caller is qualifying against, or `null`
536
+ * when there is none to qualify against; the three declaration-dependent checks
537
+ * are skipped in that case and `declarationChecksRan` says so.
538
+ */
539
+ export function qualifyProbe(probe, homeOperation) {
540
+ const failures = [];
541
+ checkRoute(probe, failures);
542
+ checkRouteEvidence(probe, failures);
543
+ const signature = probe.expectedClean ? null : probe.defectSignature;
544
+ if (!probe.expectedClean) {
545
+ if (probe.probeClass === 'canary') {
546
+ if (signature !== null) {
547
+ failures.push({
548
+ code: 'signature-present-on-canary',
549
+ artifactPath: probePath(probe, '.defectSignature'),
550
+ detail: 'a canary seeds no defect, so it declares no signature; AD-9 qualifies it by demonstrating that non-detection indicts the corpus or the fixture',
551
+ });
552
+ }
553
+ }
554
+ else if (signature === null) {
555
+ failures.push({
556
+ code: 'signature-absent',
557
+ artifactPath: probePath(probe, '.defectSignature'),
558
+ detail: `probeClass "${probe.probeClass}" seeds a defect, and AD-33 assigns an outcome only from a signature match, so a null signature makes this probe unscoreable (AD-40)`,
559
+ });
560
+ }
561
+ }
562
+ if (signature !== null) {
563
+ if (signature.interfaceKind !== 'api') {
564
+ failures.push({
565
+ code: 'signature-interface-kind-unsupported',
566
+ artifactPath: probePath(probe, '.defectSignature.interfaceKind'),
567
+ detail: `"${signature.interfaceKind}" declares a method and a path template that mean nothing off an api interface; v0 keeps all four kinds in the enum so unsupported-interface-kind stays fireable contract-side (AD-19)`,
568
+ });
569
+ }
570
+ checkObservableChannel(probe, signature, failures);
571
+ const channels = checkOperandsAndCollectChannels(probe, signature, failures);
572
+ checkChannels(probe, signature, channels, failures);
573
+ checkDisjuncts(probe, signature, failures);
574
+ if (homeOperation !== null) {
575
+ checkSelectorKeys(probe, signature, homeOperation, failures);
576
+ }
577
+ runLegalityChecks(probe, signature, homeOperation, failures);
578
+ }
579
+ return {
580
+ qualified: failures.length === 0,
581
+ failures,
582
+ declarationChecksRan: signature === null || homeOperation !== null,
583
+ };
584
+ }
585
+ /**
586
+ * AD-9's "an unqualified probe cannot enter a sealed set", as a
587
+ * construction-time filter that reports its exclusions.
588
+ *
589
+ * Construction-time, and score never re-filters. Silently dropping an
590
+ * unqualified probe at score time would shrink AD-7's denominator and
591
+ * desynchronise the AD-8 corpus digest from the probes actually scored, while
592
+ * AD-7 makes comparability the corpus digest restricted to the probes both
593
+ * results cover. A sealed set that nonetheless contains an unqualified probe is
594
+ * an invalidating condition for whoever scores it, never something the witness
595
+ * match quietly repairs.
596
+ *
597
+ * `homeOperationOf` is required rather than defaulted. A default resolving
598
+ * nothing would make the unchecked path the one a caller reaches by writing
599
+ * less, and the three checks it skips are the ones that catch a signature that
600
+ * was never writable and a selector that matches nothing. A caller who holds
601
+ * no inventory writes `() => null` at the call site, where the choice shows up
602
+ * in a diff.
603
+ */
604
+ export function sealProbeSet(probes, homeOperationOf) {
605
+ const admitted = [];
606
+ const rejected = [];
607
+ for (const probe of probes) {
608
+ const result = qualifyProbe(probe, homeOperationOf(probe));
609
+ if (result.qualified)
610
+ admitted.push({ probe, result });
611
+ else
612
+ rejected.push({ probe, result });
613
+ }
614
+ return { admitted, rejected };
615
+ }