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