eval-quality 0.1.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 (230) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +355 -0
  3. package/corpus/dev/README.md +36 -0
  4. package/corpus/dev/compile-seal-example/brief.json +1 -0
  5. package/corpus/dev/compile-seal-example/contract.json +1 -0
  6. package/corpus/dev/contracts/absent-collection-locations.json +1 -0
  7. package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
  8. package/corpus/dev/contracts/absent-success-indicator.json +1 -0
  9. package/corpus/dev/contracts/empty-channel-roles.json +1 -0
  10. package/corpus/dev/contracts/empty-collection-locations.json +1 -0
  11. package/corpus/dev/contracts/empty-request-shapes.json +1 -0
  12. package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -0
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -0
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -0
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -0
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
  19. package/corpus/dev/contracts/satisfied-declarations.json +1 -0
  20. package/corpus/dev/contracts/single-required-response-key.json +1 -0
  21. package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
  22. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
  23. package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
  24. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
  25. package/corpus/dev/index.json +1 -0
  26. package/dist/adapters/index.d.ts +12 -0
  27. package/dist/adapters/index.js +3 -0
  28. package/dist/adapters/local-corpus-adapter.d.ts +7 -0
  29. package/dist/adapters/local-corpus-adapter.js +78 -0
  30. package/dist/adapters/node-file-system-adapter.d.ts +7 -0
  31. package/dist/adapters/node-file-system-adapter.js +39 -0
  32. package/dist/adapters/port-boundary.d.ts +18 -0
  33. package/dist/adapters/port-boundary.js +83 -0
  34. package/dist/adapters/system-clock-adapter.d.ts +10 -0
  35. package/dist/adapters/system-clock-adapter.js +18 -0
  36. package/dist/application/compile.d.ts +4 -0
  37. package/dist/application/compile.js +24 -0
  38. package/dist/application/diagnostics.d.ts +22 -0
  39. package/dist/application/diagnostics.js +18 -0
  40. package/dist/application/index.d.ts +23 -0
  41. package/dist/application/index.js +17 -0
  42. package/dist/application/invoke-port.d.ts +2 -0
  43. package/dist/application/invoke-port.js +45 -0
  44. package/dist/application/preflight.d.ts +27 -0
  45. package/dist/application/preflight.js +131 -0
  46. package/dist/application/seal.d.ts +4 -0
  47. package/dist/application/seal.js +28 -0
  48. package/dist/application/serialize.d.ts +6 -0
  49. package/dist/application/serialize.js +9 -0
  50. package/dist/cli/arguments.d.ts +28 -0
  51. package/dist/cli/arguments.js +167 -0
  52. package/dist/cli/exit-codes.d.ts +36 -0
  53. package/dist/cli/exit-codes.js +39 -0
  54. package/dist/cli/main.d.ts +2 -0
  55. package/dist/cli/main.js +143 -0
  56. package/dist/cli/render.d.ts +26 -0
  57. package/dist/cli/render.js +53 -0
  58. package/dist/cli/run.d.ts +41 -0
  59. package/dist/cli/run.js +203 -0
  60. package/dist/core/canonical/canonicalize.d.ts +1 -0
  61. package/dist/core/canonical/canonicalize.js +113 -0
  62. package/dist/core/canonical/digest.d.ts +7 -0
  63. package/dist/core/canonical/digest.js +81 -0
  64. package/dist/core/canonical/scan-json.d.ts +1 -0
  65. package/dist/core/canonical/scan-json.js +298 -0
  66. package/dist/core/canonical/value-domain.d.ts +4 -0
  67. package/dist/core/canonical/value-domain.js +113 -0
  68. package/dist/core/compile/compile.d.ts +24 -0
  69. package/dist/core/compile/compile.js +67 -0
  70. package/dist/core/compile/declarations.d.ts +5 -0
  71. package/dist/core/compile/declarations.js +22 -0
  72. package/dist/core/compile/expression-legality.d.ts +14 -0
  73. package/dist/core/compile/expression-legality.js +346 -0
  74. package/dist/core/compile/forbidden-inputs.d.ts +5 -0
  75. package/dist/core/compile/forbidden-inputs.js +19 -0
  76. package/dist/core/compile/interface-inventory.d.ts +10 -0
  77. package/dist/core/compile/interface-inventory.js +55 -0
  78. package/dist/core/compile/oracle-alignment.d.ts +14 -0
  79. package/dist/core/compile/oracle-alignment.js +93 -0
  80. package/dist/core/compile/reachability.d.ts +17 -0
  81. package/dist/core/compile/reachability.js +177 -0
  82. package/dist/core/compile/rubrics.d.ts +55 -0
  83. package/dist/core/compile/rubrics.js +198 -0
  84. package/dist/core/compile/scripting-bound.d.ts +5 -0
  85. package/dist/core/compile/scripting-bound.js +151 -0
  86. package/dist/core/compile/sensitivity-witness.d.ts +73 -0
  87. package/dist/core/compile/sensitivity-witness.js +232 -0
  88. package/dist/core/compile/waivers.d.ts +3 -0
  89. package/dist/core/compile/waivers.js +21 -0
  90. package/dist/core/coverage/coverage.d.ts +18 -0
  91. package/dist/core/coverage/coverage.js +47 -0
  92. package/dist/core/coverage/relevance.d.ts +75 -0
  93. package/dist/core/coverage/relevance.js +186 -0
  94. package/dist/core/coverage/rules.d.ts +7 -0
  95. package/dist/core/coverage/rules.js +18 -0
  96. package/dist/core/coverage/satisfaction.d.ts +107 -0
  97. package/dist/core/coverage/satisfaction.js +415 -0
  98. package/dist/core/coverage/table.d.ts +26 -0
  99. package/dist/core/coverage/table.js +222 -0
  100. package/dist/core/declared-inputs.d.ts +16 -0
  101. package/dist/core/declared-inputs.js +18 -0
  102. package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
  103. package/dist/core/evaluate/evidence-resolution.js +153 -0
  104. package/dist/core/evaluate/operators.d.ts +87 -0
  105. package/dist/core/evaluate/operators.js +350 -0
  106. package/dist/core/evaluate/resolution.d.ts +40 -0
  107. package/dist/core/evaluate/resolution.js +336 -0
  108. package/dist/core/evaluate/resolved-value.d.ts +9 -0
  109. package/dist/core/evaluate/resolved-value.js +6 -0
  110. package/dist/core/failure-codes.d.ts +18 -0
  111. package/dist/core/failure-codes.js +50 -0
  112. package/dist/core/lineage/chain.d.ts +49 -0
  113. package/dist/core/lineage/chain.js +210 -0
  114. package/dist/core/lineage/freeze.d.ts +12 -0
  115. package/dist/core/lineage/freeze.js +35 -0
  116. package/dist/core/lineage/stage-table.d.ts +43 -0
  117. package/dist/core/lineage/stage-table.js +106 -0
  118. package/dist/core/preflight/plan.d.ts +67 -0
  119. package/dist/core/preflight/plan.js +313 -0
  120. package/dist/core/preflight/projection.d.ts +40 -0
  121. package/dist/core/preflight/projection.js +96 -0
  122. package/dist/core/preflight/reduce.d.ts +8 -0
  123. package/dist/core/preflight/reduce.js +180 -0
  124. package/dist/core/preflight/witness-evidence.d.ts +42 -0
  125. package/dist/core/preflight/witness-evidence.js +88 -0
  126. package/dist/core/probe/target-policy.d.ts +63 -0
  127. package/dist/core/probe/target-policy.js +328 -0
  128. package/dist/core/schemas/artifact-reference.d.ts +19 -0
  129. package/dist/core/schemas/artifact-reference.js +30 -0
  130. package/dist/core/schemas/artifact.d.ts +1153 -0
  131. package/dist/core/schemas/artifact.js +84 -0
  132. package/dist/core/schemas/constraint-ledger.d.ts +57 -0
  133. package/dist/core/schemas/constraint-ledger.js +143 -0
  134. package/dist/core/schemas/eval-contract.d.ts +402 -0
  135. package/dist/core/schemas/eval-contract.js +147 -0
  136. package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
  137. package/dist/core/schemas/evaluator-configuration.js +43 -0
  138. package/dist/core/schemas/evidence-artifact.d.ts +471 -0
  139. package/dist/core/schemas/evidence-artifact.js +232 -0
  140. package/dist/core/schemas/expression.d.ts +197 -0
  141. package/dist/core/schemas/expression.js +284 -0
  142. package/dist/core/schemas/faults.d.ts +9 -0
  143. package/dist/core/schemas/faults.js +32 -0
  144. package/dist/core/schemas/interface.d.ts +417 -0
  145. package/dist/core/schemas/interface.js +115 -0
  146. package/dist/core/schemas/isolation-manifest.d.ts +136 -0
  147. package/dist/core/schemas/isolation-manifest.js +92 -0
  148. package/dist/core/schemas/lineage.d.ts +21 -0
  149. package/dist/core/schemas/lineage.js +28 -0
  150. package/dist/core/schemas/oracle.d.ts +72 -0
  151. package/dist/core/schemas/oracle.js +35 -0
  152. package/dist/core/schemas/plan.d.ts +115 -0
  153. package/dist/core/schemas/plan.js +58 -0
  154. package/dist/core/schemas/pointer.d.ts +63 -0
  155. package/dist/core/schemas/pointer.js +94 -0
  156. package/dist/core/schemas/port-messages.d.ts +103 -0
  157. package/dist/core/schemas/port-messages.js +90 -0
  158. package/dist/core/schemas/preflight-verdict.d.ts +60 -0
  159. package/dist/core/schemas/preflight-verdict.js +38 -0
  160. package/dist/core/schemas/primitives.d.ts +78 -0
  161. package/dist/core/schemas/primitives.js +121 -0
  162. package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
  163. package/dist/core/schemas/private-artifact-manifest.js +46 -0
  164. package/dist/core/schemas/probe-body.d.ts +25 -0
  165. package/dist/core/schemas/probe-body.js +23 -0
  166. package/dist/core/schemas/probe-policy.d.ts +76 -0
  167. package/dist/core/schemas/probe-policy.js +33 -0
  168. package/dist/core/schemas/probe.d.ts +189 -0
  169. package/dist/core/schemas/probe.js +82 -0
  170. package/dist/core/schemas/publish.d.ts +30 -0
  171. package/dist/core/schemas/publish.js +135 -0
  172. package/dist/core/schemas/reference-set.d.ts +18 -0
  173. package/dist/core/schemas/reference-set.js +27 -0
  174. package/dist/core/schemas/rubric.d.ts +62 -0
  175. package/dist/core/schemas/rubric.js +68 -0
  176. package/dist/core/schemas/scoring-policy.d.ts +28 -0
  177. package/dist/core/schemas/scoring-policy.js +51 -0
  178. package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
  179. package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
  180. package/dist/core/schemas/sealed-run-record.d.ts +382 -0
  181. package/dist/core/schemas/sealed-run-record.js +215 -0
  182. package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
  183. package/dist/core/schemas/sensitivity-witness.js +86 -0
  184. package/dist/core/schemas/verdict.d.ts +25 -0
  185. package/dist/core/schemas/verdict.js +20 -0
  186. package/dist/core/schemas/waiver.d.ts +17 -0
  187. package/dist/core/schemas/waiver.js +24 -0
  188. package/dist/core/seal/derived-reference.d.ts +36 -0
  189. package/dist/core/seal/derived-reference.js +383 -0
  190. package/dist/core/seal/direction-prose.d.ts +13 -0
  191. package/dist/core/seal/direction-prose.js +120 -0
  192. package/dist/core/seal/plan-index.d.ts +63 -0
  193. package/dist/core/seal/plan-index.js +155 -0
  194. package/dist/core/seal/scripting-audit.d.ts +15 -0
  195. package/dist/core/seal/scripting-audit.js +51 -0
  196. package/dist/core/seal/seal.d.ts +18 -0
  197. package/dist/core/seal/seal.js +155 -0
  198. package/dist/core/stage-contracts.d.ts +30 -0
  199. package/dist/core/stage-contracts.js +1 -0
  200. package/dist/index.d.ts +15 -0
  201. package/dist/index.js +22 -0
  202. package/dist/ports/clock-port.d.ts +13 -0
  203. package/dist/ports/clock-port.js +7 -0
  204. package/dist/ports/corpus-port.d.ts +16 -0
  205. package/dist/ports/corpus-port.js +7 -0
  206. package/dist/ports/environment-probe-port.d.ts +76 -0
  207. package/dist/ports/environment-probe-port.js +32 -0
  208. package/dist/ports/file-system-port.d.ts +27 -0
  209. package/dist/ports/file-system-port.js +11 -0
  210. package/dist/ports/port.d.ts +33 -0
  211. package/dist/ports/port.js +14 -0
  212. package/dist/testing/conformance.d.ts +88 -0
  213. package/dist/testing/conformance.js +281 -0
  214. package/dist/testing/index.d.ts +26 -0
  215. package/dist/testing/index.js +18 -0
  216. package/dist/testing/probe-conformance.d.ts +45 -0
  217. package/dist/testing/probe-conformance.js +186 -0
  218. package/package.json +130 -0
  219. package/schemas/artifact-reference.schema.json +64 -0
  220. package/schemas/eval-contract.schema.json +2145 -0
  221. package/schemas/evaluator-configuration.schema.json +199 -0
  222. package/schemas/evidence-artifact.schema.json +1229 -0
  223. package/schemas/isolation-manifest.schema.json +394 -0
  224. package/schemas/preflight-verdict.schema.json +114 -0
  225. package/schemas/private-artifact-manifest.schema.json +103 -0
  226. package/schemas/probe.schema.json +1151 -0
  227. package/schemas/rubric.schema.json +148 -0
  228. package/schemas/scoring-policy.schema.json +90 -0
  229. package/schemas/sealed-evaluator-brief.schema.json +268 -0
  230. package/schemas/sealed-run-record.schema.json +875 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * AD-10's three declaration-side witness checks: that every input-bearing
3
+ * operation declares one, that a declared one is shaped so the plan can
4
+ * execute it, and that its leg identifiers are distinct from each other and
5
+ * from every interaction-plan step.
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
10
+ * and a leg-id/step-id collision are identifier collisions, and
11
+ * `malformed-operator-expression` is the closest available code because the
12
+ * relation is illegal in its position once its operands cannot be told apart.
13
+ */
14
+ import { declaresNoRequestKeys } from '../declared-inputs.js';
15
+ import { StructuralFailure } from '../failure-codes.js';
16
+ import { TRANSPORT_CHANNELS, } from '../schemas/pointer.js';
17
+ import { parseEvidenceTarget } from '../seal/plan-index.js';
18
+ export { declaresNoRequestKeys };
19
+ /**
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.
24
+ */
25
+ export function suppliedKeys(inputs, channel) {
26
+ if (channel !== 'body')
27
+ return Object.keys(inputs[channel]);
28
+ const { body } = inputs;
29
+ if (body.kind !== 'json')
30
+ return [];
31
+ const { value } = body;
32
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
33
+ return [];
34
+ return Object.keys(value);
35
+ }
36
+ /**
37
+ * Every channel of one set of witness inputs against the operation that will
38
+ * receive them. All four, not only the differential channel: `planPreflight`
39
+ * copies all four onto the `ProbeRequest` and the port sends them, so a value
40
+ * on an unselected channel is as much an outbound value as one on the selected
41
+ * channel.
42
+ *
43
+ * The two directions have different standing, and the difference matters
44
+ * enough to name, because the next reader will diff this against
45
+ * `checkUndeclaredMandatoryInput` and find the two disagreeing.
46
+ *
47
+ * The permitted direction is that function's rule, carried over: it loops the
48
+ * same four channels and rejects a key the operation declares in neither list.
49
+ * That is what makes `WitnessInputs`'s AD-18 promise about `header` more than a
50
+ * comment.
51
+ *
52
+ * The required direction is new here, because the two shapes differ. An
53
+ * `InputBindingChannel` is nullable, and `null` means "this step binds nothing in
54
+ * this channel", so `checkUndeclaredMandatoryInput` skips an unbound channel and
55
+ * asks nothing about required keys. `WitnessInputs` has all four channels as
56
+ * concrete values, so a leg omitting a required key is a request the port cannot
57
+ * issue. The cost is that a required header forces a literal value into the
58
+ * contract artifact, the surface AD-18 governs; a placeholder satisfies this
59
+ * check, and what pre-flight probes is a fixture.
60
+ */
61
+ export function checkInputsAgainstShape(inputs, operation, owner, artifactPath) {
62
+ for (const channel of TRANSPORT_CHANNELS) {
63
+ const shape = operation.requestShape[channel];
64
+ const supplied = suppliedKeys(inputs, channel);
65
+ for (const key of shape.requiredKeys) {
66
+ if (supplied.includes(key))
67
+ continue;
68
+ throw new StructuralFailure('undeclared-mandatory-input', artifactPath, `${owner} omits "${key}", which operation "${operation.operationId}" declares required in its ${channel} channel (AD-10)`);
69
+ }
70
+ const permitted = new Set([...shape.requiredKeys, ...shape.permittedKeys]);
71
+ for (const key of supplied) {
72
+ if (permitted.has(key))
73
+ continue;
74
+ throw new StructuralFailure('undeclared-mandatory-input', artifactPath, `${owner} supplies "${key}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys of its ${channel} channel (AD-10, AD-18)`);
75
+ }
76
+ }
77
+ }
78
+ const operationPath = (interfaceIndex, operationIndex) => `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
79
+ function forEachWitness(contract, visit) {
80
+ contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
81
+ iface.operations.forEach((operation, operationIndex) => {
82
+ const witness = operation.sensitivityWitness;
83
+ if (witness === null)
84
+ return;
85
+ visit(witness, operation, operationPath(interfaceIndex, operationIndex));
86
+ });
87
+ });
88
+ }
89
+ /**
90
+ * Strict-gated, alongside `checkUndeclaredMandatoryInput`, whose code this
91
+ * shares: `compile.ts` already gates that code behind `options.strict`, and one
92
+ * code with two gating regimes would be worse than one code with two
93
+ * conditions.
94
+ */
95
+ export function checkSensitivityWitnessDeclared(contract) {
96
+ contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
97
+ iface.operations.forEach((operation, operationIndex) => {
98
+ const path = operationPath(interfaceIndex, operationIndex);
99
+ const witness = operation.sensitivityWitness;
100
+ if (witness === null) {
101
+ if (declaresNoRequestKeys(operation))
102
+ 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)`);
104
+ }
105
+ witness.legs.forEach((leg, legIndex) => {
106
+ checkInputsAgainstShape(leg.inputs, operation, `leg "${leg.legId}"`, `${path}.sensitivityWitness.legs[${legIndex}]`);
107
+ });
108
+ });
109
+ });
110
+ }
111
+ /** every interaction-rooted step id an expression addresses, in walk order. */
112
+ function addressedStepIds(expression) {
113
+ const found = new Set();
114
+ const visitOperand = (operand) => {
115
+ const { pointer } = operand;
116
+ if (pointer === undefined || pointer.startsWith('@'))
117
+ return;
118
+ found.add(parseEvidenceTarget(pointer).stepId);
119
+ };
120
+ const walk = (node) => {
121
+ switch (node.op) {
122
+ case 'not':
123
+ case 'all':
124
+ case 'any':
125
+ node.operands.forEach(walk);
126
+ return;
127
+ case 'for-all':
128
+ case 'for-any':
129
+ visitOperand(node.collection);
130
+ walk(node.predicate);
131
+ return;
132
+ default:
133
+ for (const operand of node.operands)
134
+ visitOperand(operand);
135
+ }
136
+ };
137
+ walk(expression);
138
+ return found;
139
+ }
140
+ /** the channel AD-10 selects for an operation, by its state-change marker. */
141
+ const legalChannels = (operation) => operation.stateChangeMarker ? ['body'] : ['path', 'query'];
142
+ /**
143
+ * The shape rules: the differential channel agrees with the state-change
144
+ * marker, the relation reads both legs and nothing else, and a declared
145
+ * fixture reset names a mutating operation the contract declares. Unconditional,
146
+ * since none of these fires `undeclared-mandatory-input`.
147
+ */
148
+ export function checkWitnessLegality(contract) {
149
+ forEachWitness(contract, (witness, operation, path) => {
150
+ const legal = legalChannels(operation);
151
+ if (!legal.includes(witness.channel)) {
152
+ throw new StructuralFailure('malformed-operator-expression', `${path}.sensitivityWitness`, `channel "${witness.channel}" contradicts stateChangeMarker ${operation.stateChangeMarker} on operation "${operation.operationId}"; AD-10 selects ${legal.map((name) => `"${name}"`).join(' or ')}`);
153
+ }
154
+ // AD-10's predicate is a differential. Two legs supplying the same values on
155
+ // the selected channel establish nothing, and at run time that surfaces as a
156
+ // `failed` check pointing at the fixture, when the defect is in the
157
+ // declaration.
158
+ const [first, second] = witness.legs;
159
+ if (first !== undefined &&
160
+ second !== undefined &&
161
+ JSON.stringify(first.inputs[witness.channel]) ===
162
+ JSON.stringify(second.inputs[witness.channel])) {
163
+ 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
+ }
165
+ const legIds = witness.legs.map((leg) => leg.legId);
166
+ const addressed = addressedStepIds(witness.relation);
167
+ // Reported before the coverage rules: an unknown root is the more
168
+ // specific diagnosis, and a relation that reads a third step is
169
+ // unreachable evidence whether or not it also reads both legs.
170
+ for (const stepId of addressed) {
171
+ if (legIds.includes(stepId))
172
+ continue;
173
+ throw new StructuralFailure('unreachable-check-evidence', `${path}.sensitivityWitness.relation`, `the relation addresses "${stepId}", which is neither leg of witness "${witness.witnessId}" (AD-10)`);
174
+ }
175
+ const covered = legIds.filter((legId) => addressed.has(legId));
176
+ if (covered.length !== legIds.length) {
177
+ throw new StructuralFailure('malformed-operator-expression', `${path}.sensitivityWitness.relation`, `the relation addresses ${covered.length} of the two legs of witness "${witness.witnessId}"; a differential that reads one leg establishes no sensitivity (AD-10)`);
178
+ }
179
+ });
180
+ const reset = contract.fixtureReset;
181
+ if (reset === null)
182
+ return;
183
+ const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
184
+ const operation = iface?.operations.find((candidate) => candidate.operationId === reset.operationId);
185
+ if (operation === undefined) {
186
+ 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
+ }
188
+ if (!operation.stateChangeMarker) {
189
+ throw new StructuralFailure('malformed-operator-expression', 'EvalContract.fixtureReset', `the fixture reset names operation "${reset.operationId}", whose stateChangeMarker is false; an operation that changes no state resets nothing (AD-10)`);
190
+ }
191
+ // The reset is one more leg through the same port, so its inputs answer to
192
+ // the same request shape every witness leg does.
193
+ checkInputsAgainstShape(reset.inputs, operation, 'the fixture reset', 'EvalContract.fixtureReset.inputs');
194
+ }
195
+ /**
196
+ * Leg identifiers share one namespace with interaction-plan step identifiers,
197
+ * because a witness relation is an ordinary AD-4 expression and addresses a leg
198
+ * as `/interactions/{legId}/…`. A collision would make a relation pointer
199
+ * resolve against the wrong observation.
200
+ */
201
+ export function checkWitnessLegIdentifiers(contract) {
202
+ const stepIds = new Set(contract.interactionPlan.map((step) => step.stepId));
203
+ const seen = new Map();
204
+ const claim = (legId, owner, path) => {
205
+ if (stepIds.has(legId)) {
206
+ throw new StructuralFailure('malformed-operator-expression', path, `leg id "${legId}" collides with an interaction-plan step id; the two share one pointer namespace (AD-10, AD-26)`);
207
+ }
208
+ const previous = seen.get(legId);
209
+ if (previous !== undefined) {
210
+ throw new StructuralFailure('malformed-operator-expression', path, `leg id "${legId}" is already claimed by ${previous}; leg ids are unique across the contract (AD-10)`);
211
+ }
212
+ seen.set(legId, owner);
213
+ };
214
+ forEachWitness(contract, (witness, _operation, path) => {
215
+ const [first, second] = witness.legs;
216
+ if (first !== undefined &&
217
+ second !== undefined &&
218
+ first.legId === second.legId) {
219
+ throw new StructuralFailure('malformed-operator-expression', `${path}.sensitivityWitness.legs`, `both legs of witness "${witness.witnessId}" carry the leg id "${first.legId}", so the relation cannot tell them apart (AD-10)`);
220
+ }
221
+ witness.legs.forEach((leg, legIndex) => {
222
+ claim(leg.legId, `witness "${witness.witnessId}"`, `${path}.sensitivityWitness.legs[${legIndex}].legId`);
223
+ });
224
+ });
225
+ // The fixture reset is one more leg through the same port, so its id sits in
226
+ // the same namespace. AD-10 names no defect for the collision; the check is
227
+ // added here because two legs sharing a `probeId` would make the reducer read
228
+ // one leg's observation for the other.
229
+ const reset = contract.fixtureReset;
230
+ if (reset !== null)
231
+ claim(reset.legId, 'the fixture reset', 'EvalContract.fixtureReset.legId');
232
+ }
@@ -0,0 +1,3 @@
1
+ import type { EvalContract } from '../schemas/eval-contract.ts';
2
+ /** Reports the first missing required field. */
3
+ export declare function checkWaiverCompleteness(contract: EvalContract): void;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Checks each waiver for its rule, rationale, approval, and expiry.
3
+ * A condition is optional.
4
+ */
5
+ import { StructuralFailure } from '../failure-codes.js';
6
+ const REQUIRED_WAIVER_PARTS = [
7
+ 'rule',
8
+ 'rationale',
9
+ 'approval',
10
+ 'expiresAt',
11
+ ];
12
+ /** Reports the first missing required field. */
13
+ export function checkWaiverCompleteness(contract) {
14
+ for (const waiver of contract.waivers) {
15
+ for (const part of REQUIRED_WAIVER_PARTS) {
16
+ if (waiver[part] === null) {
17
+ throw new StructuralFailure('waiver-incomplete', `EvalContract.waivers[id=${waiver.id}].${part}`, `a waiver requires ${part}; AD-5 requires the named rule, an explicit rationale, the recorded approval, and an RFC 3339 expiry`);
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * AD-31 coverage gaps: relevance fired and satisfaction failed. A satisfied
3
+ * rule gets no record, since AD-21 reads `coverageGaps` as the gap list.
4
+ * Nothing throws; AD-5 emits the artifact with the gap recorded, and only a
5
+ * structural error blocks compilation.
6
+ */
7
+ import { type EvalContract, type Severity } from '../schemas/eval-contract.ts';
8
+ import type { CoverageGap } from '../schemas/evidence-artifact.ts';
9
+ export type CoverageGapRecord = CoverageGap;
10
+ /**
11
+ * Highest declared behaviour severity. `behaviors` carries `.min(1)`, so this
12
+ * is total. The maximum, because the predicates are contract-level and
13
+ * oracle-blind: a lower reduction lets one trivial behaviour push every gap
14
+ * under AD-21's severity floor.
15
+ */
16
+ export declare function coverageSeverity(contract: EvalContract): Severity;
17
+ /** One record per gap, in `DISCIPLINE_RULES` order. Both arrays share it, so the pairing is positional. */
18
+ export declare function evaluateCoverage(contract: EvalContract): readonly CoverageGapRecord[];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * AD-31 coverage gaps: relevance fired and satisfaction failed. A satisfied
3
+ * rule gets no record, since AD-21 reads `coverageGaps` as the gap list.
4
+ * Nothing throws; AD-5 emits the artifact with the gap recorded, and only a
5
+ * structural error blocks compilation.
6
+ */
7
+ import { SEVERITY_LEVELS, } from '../schemas/eval-contract.js';
8
+ import { evaluateRelevance } from './relevance.js';
9
+ import { evaluateSatisfaction } from './satisfaction.js';
10
+ /**
11
+ * Highest declared behaviour severity. `behaviors` carries `.min(1)`, so this
12
+ * is total. The maximum, because the predicates are contract-level and
13
+ * oracle-blind: a lower reduction lets one trivial behaviour push every gap
14
+ * under AD-21's severity floor.
15
+ */
16
+ export function coverageSeverity(contract) {
17
+ let rank = 0;
18
+ for (const behavior of contract.behaviors) {
19
+ rank = Math.max(rank, SEVERITY_LEVELS.indexOf(behavior.severity));
20
+ }
21
+ return SEVERITY_LEVELS[rank];
22
+ }
23
+ /** One record per gap, in `DISCIPLINE_RULES` order. Both arrays share it, so the pairing is positional. */
24
+ export function evaluateCoverage(contract) {
25
+ const relevance = evaluateRelevance(contract);
26
+ const satisfaction = evaluateSatisfaction(contract);
27
+ const severity = coverageSeverity(contract);
28
+ const gaps = [];
29
+ // `!relevant.relevant` is unfalsifiable today: a rule relevant for no site
30
+ // answers satisfied vacuously, which fixture 168 pins over 23 contracts. It
31
+ // stays as the conjunction AD-31 states, so a predicate pair that broke the
32
+ // invariant cannot record a gap for a rule that never fired.
33
+ for (const [index, relevant] of relevance.entries()) {
34
+ const satisfied = satisfaction[index];
35
+ if (satisfied === undefined || !relevant.relevant || satisfied.satisfied) {
36
+ continue;
37
+ }
38
+ gaps.push({
39
+ rule: relevant.rule,
40
+ relevancePredicate: relevant.predicate,
41
+ satisfactionPredicate: satisfied.predicate,
42
+ satisfied: false,
43
+ severity,
44
+ });
45
+ }
46
+ return gaps;
47
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * AD-31's seven relevance predicates: decision procedures over AD-19's
3
+ * declarations. They read no run record, no probe, no outcome, no oracle.
4
+ *
5
+ * Nothing here throws. A coverage gap is recorded and the artifact still ships
6
+ * (AD-5), so there is no failure code. Failing closed means answering
7
+ * `relevant` on an absent declaration.
8
+ */
9
+ import type { EvalContract } from '../schemas/eval-contract.ts';
10
+ import { type DisciplineRule } from './rules.ts';
11
+ export type RelevanceVerdict = {
12
+ readonly rule: DisciplineRule;
13
+ readonly predicate: string;
14
+ readonly relevant: boolean;
15
+ /** which declaration decided it, so a gap is diagnosable. */
16
+ readonly reason: string;
17
+ };
18
+ /** A contract declaring no operation leaves six of the rules nothing to read. */
19
+ export declare const NO_OPERATION = "the contract declares no operation, so the declaration this rule reads is absent";
20
+ /**
21
+ * Rule 1: `successIndicator` declared, and some other pointer carrying a
22
+ * channel role. An operation nominating no indicator is relevant, since a
23
+ * nullable pointer has two spellings and `null` is the absent one.
24
+ * `channelRoles: {}` answers the second conjunct.
25
+ */
26
+ export declare function successIndicatorSeparationRelevance(contract: EvalContract): RelevanceVerdict;
27
+ /**
28
+ * Rule 2: the descriptor declares more than one pointer, counted over distinct
29
+ * `requiredKeys`. That is the set rule 2's satisfaction denominator reads, and
30
+ * AD-20 keeps permitted keys out of coverage. Distinct, because `requiredKeys`
31
+ * carries no uniqueness constraint and one key repeated is one pointer.
32
+ */
33
+ export declare function wholeBodyRelevance(contract: EvalContract): RelevanceVerdict;
34
+ /**
35
+ * Rule 3: some operation declares a request key on any of AD-19's four
36
+ * transport channels. The site is the whole channel triple. A key with no
37
+ * `types` entry has an absent type, a key typed `null` has AD-31's
38
+ * indeterminate one, and both are relevant, so declaring an input and leaving
39
+ * it untyped buys no irrelevance.
40
+ */
41
+ export declare function malformedInputRelevance(contract: EvalContract): RelevanceVerdict;
42
+ /**
43
+ * Rule 4: the descriptor declares at least one collection location. `null` is
44
+ * the absent state and is relevant; `[]` is the explicit empty answer.
45
+ */
46
+ export declare function perRecordRelevance(contract: EvalContract): RelevanceVerdict;
47
+ /**
48
+ * Rule 5: a sibling group over operations or parameters is non-empty. The one
49
+ * contract-level rule of the seven. A group of one cannot exist, since
50
+ * `SIBLING_GROUP_MINIMUM` is two.
51
+ */
52
+ export declare function siblingCrossCheckRelevance(contract: EvalContract): RelevanceVerdict;
53
+ /**
54
+ * Rule 6: a declared collection location names a reference set. The site is
55
+ * `collectionLocations` alone, so an explicitly empty list stays an answer. A
56
+ * location naming an identifier the contract does not declare still names one.
57
+ */
58
+ export declare function omissionAndCompletenessRelevance(contract: EvalContract): RelevanceVerdict;
59
+ /**
60
+ * Rule 7: some operation declares `stateChangeMarker: true`. The one rule with
61
+ * no absent state to grade, since the marker is a required boolean and both
62
+ * values are legal.
63
+ */
64
+ export declare function stateChangeReadBackRelevance(contract: EvalContract): RelevanceVerdict;
65
+ /**
66
+ * One predicate per rule. The mapped type fails the typecheck when a member of
67
+ * `DISCIPLINE_RULES` has no predicate. Same idiom as `operatorHandlers` in
68
+ * `core/evaluate/resolution.ts`, minus its `Object.hasOwn` guard: these keys
69
+ * come from a compile-time tuple, so a runtime guard is unreachable.
70
+ */
71
+ export declare const RELEVANCE_PREDICATES: {
72
+ readonly [Rule in DisciplineRule]: (contract: EvalContract) => RelevanceVerdict;
73
+ };
74
+ /** All seven verdicts, in `DISCIPLINE_RULES` order, over declarations alone. */
75
+ export declare function evaluateRelevance(contract: EvalContract): readonly RelevanceVerdict[];
@@ -0,0 +1,186 @@
1
+ import { TRANSPORT_CHANNELS } from '../schemas/pointer.js';
2
+ import { DISCIPLINE_RULES, relevancePredicateId, } from './rules.js';
3
+ const verdict = (rule, relevant, reason) => ({
4
+ rule,
5
+ predicate: relevancePredicateId(rule),
6
+ relevant,
7
+ reason,
8
+ });
9
+ /** `1 collection location`, `2 collection locations`. A reason is read by people. */
10
+ const plural = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
11
+ /** Every declared operation, flattened. Six of the seven rules range over this list. */
12
+ const operationsOf = (contract) => contract.permittedInterfaces.flatMap((declared) => declared.operations);
13
+ /** A contract declaring no operation leaves six of the rules nothing to read. */
14
+ export const NO_OPERATION = 'the contract declares no operation, so the declaration this rule reads is absent';
15
+ /**
16
+ * Rule 1: `successIndicator` declared, and some other pointer carrying a
17
+ * channel role. An operation nominating no indicator is relevant, since a
18
+ * nullable pointer has two spellings and `null` is the absent one.
19
+ * `channelRoles: {}` answers the second conjunct.
20
+ */
21
+ export function successIndicatorSeparationRelevance(contract) {
22
+ const rule = 'success-indicator-separation';
23
+ const operations = operationsOf(contract);
24
+ if (operations.length === 0)
25
+ return verdict(rule, true, NO_OPERATION);
26
+ for (const operation of operations) {
27
+ const { successIndicator, channelRoles } = operation.responseDescriptor;
28
+ // `=== null`: `DescriptorPointer` admits the empty string, which
29
+ // nominates the whole response body.
30
+ if (successIndicator === null) {
31
+ return verdict(rule, true, `operation ${operation.operationId} nominates no success indicator`);
32
+ }
33
+ if (channelRoles === null) {
34
+ return verdict(rule, true, `operation ${operation.operationId} declares no channel roles`);
35
+ }
36
+ const other = Object.keys(channelRoles).find((pointer) => pointer !== successIndicator);
37
+ if (other !== undefined) {
38
+ return verdict(rule, true, `operation ${operation.operationId} gives pointer ${other} a channel role beside its success indicator`);
39
+ }
40
+ }
41
+ return verdict(rule, false, 'every operation nominates a success indicator and gives no other pointer a channel role');
42
+ }
43
+ /**
44
+ * Rule 2: the descriptor declares more than one pointer, counted over distinct
45
+ * `requiredKeys`. That is the set rule 2's satisfaction denominator reads, and
46
+ * AD-20 keeps permitted keys out of coverage. Distinct, because `requiredKeys`
47
+ * carries no uniqueness constraint and one key repeated is one pointer.
48
+ */
49
+ export function wholeBodyRelevance(contract) {
50
+ const rule = 'whole-body';
51
+ const operations = operationsOf(contract);
52
+ if (operations.length === 0)
53
+ return verdict(rule, true, NO_OPERATION);
54
+ for (const operation of operations) {
55
+ const distinct = new Set(operation.responseDescriptor.requiredKeys);
56
+ if (distinct.size > 1) {
57
+ return verdict(rule, true, `operation ${operation.operationId} declares ${plural(distinct.size, 'distinct required response key')}`);
58
+ }
59
+ }
60
+ return verdict(rule, false, 'no operation declares more than one distinct required response key');
61
+ }
62
+ /**
63
+ * Rule 3: some operation declares a request key on any of AD-19's four
64
+ * transport channels. The site is the whole channel triple. A key with no
65
+ * `types` entry has an absent type, a key typed `null` has AD-31's
66
+ * indeterminate one, and both are relevant, so declaring an input and leaving
67
+ * it untyped buys no irrelevance.
68
+ */
69
+ export function malformedInputRelevance(contract) {
70
+ const rule = 'malformed-input';
71
+ const operations = operationsOf(contract);
72
+ if (operations.length === 0)
73
+ return verdict(rule, true, NO_OPERATION);
74
+ for (const operation of operations) {
75
+ for (const channel of TRANSPORT_CHANNELS) {
76
+ const shape = operation.requestShape[channel];
77
+ // `Object.keys` enumerates own keys only. `KeyName` admits
78
+ // `constructor`, which a keyed lookup would find on the prototype.
79
+ const key = shape.requiredKeys[0] ??
80
+ shape.permittedKeys[0] ??
81
+ Object.keys(shape.types)[0];
82
+ if (key !== undefined) {
83
+ return verdict(rule, true, `operation ${operation.operationId} declares ${channel} key ${key}`);
84
+ }
85
+ }
86
+ }
87
+ return verdict(rule, false, 'no operation declares a request key on any transport channel');
88
+ }
89
+ /**
90
+ * Rule 4: the descriptor declares at least one collection location. `null` is
91
+ * the absent state and is relevant; `[]` is the explicit empty answer.
92
+ */
93
+ export function perRecordRelevance(contract) {
94
+ const rule = 'per-record';
95
+ const operations = operationsOf(contract);
96
+ if (operations.length === 0)
97
+ return verdict(rule, true, NO_OPERATION);
98
+ for (const operation of operations) {
99
+ const { collectionLocations } = operation.responseDescriptor;
100
+ if (collectionLocations === null) {
101
+ return verdict(rule, true, `operation ${operation.operationId} declares no collection-location list, so no collection is declared to range over`);
102
+ }
103
+ if (collectionLocations.length > 0) {
104
+ return verdict(rule, true, `operation ${operation.operationId} declares ${plural(collectionLocations.length, 'collection location')}`);
105
+ }
106
+ }
107
+ return verdict(rule, false, 'every operation declares an explicitly empty collection-location list');
108
+ }
109
+ /**
110
+ * Rule 5: a sibling group over operations or parameters is non-empty. The one
111
+ * contract-level rule of the seven. A group of one cannot exist, since
112
+ * `SIBLING_GROUP_MINIMUM` is two.
113
+ */
114
+ export function siblingCrossCheckRelevance(contract) {
115
+ const rule = 'sibling-cross-check';
116
+ const groups = contract.siblingGroups;
117
+ if (groups === null) {
118
+ return verdict(rule, true, 'the contract declares no sibling groups');
119
+ }
120
+ if (groups.operations.length > 0) {
121
+ return verdict(rule, true, `the contract declares ${plural(groups.operations.length, 'operation sibling group')}`);
122
+ }
123
+ if (groups.parameters.length > 0) {
124
+ return verdict(rule, true, `the contract declares ${plural(groups.parameters.length, 'parameter sibling group')}`);
125
+ }
126
+ return verdict(rule, false, 'the contract declares explicitly empty operation and parameter sibling groups');
127
+ }
128
+ /**
129
+ * Rule 6: a declared collection location names a reference set. The site is
130
+ * `collectionLocations` alone, so an explicitly empty list stays an answer. A
131
+ * location naming an identifier the contract does not declare still names one.
132
+ */
133
+ export function omissionAndCompletenessRelevance(contract) {
134
+ const rule = 'omission-and-completeness';
135
+ const operations = operationsOf(contract);
136
+ if (operations.length === 0)
137
+ return verdict(rule, true, NO_OPERATION);
138
+ for (const operation of operations) {
139
+ const { collectionLocations } = operation.responseDescriptor;
140
+ if (collectionLocations === null) {
141
+ return verdict(rule, true, `operation ${operation.operationId} declares no collection-location list, so no location can name a reference set`);
142
+ }
143
+ for (const location of collectionLocations) {
144
+ if (location.referenceSet !== null) {
145
+ return verdict(rule, true, `operation ${operation.operationId} names reference set ${location.referenceSet} for collection ${location.pointer}`);
146
+ }
147
+ }
148
+ }
149
+ return verdict(rule, false, 'no declared collection location names a reference set');
150
+ }
151
+ /**
152
+ * Rule 7: some operation declares `stateChangeMarker: true`. The one rule with
153
+ * no absent state to grade, since the marker is a required boolean and both
154
+ * values are legal.
155
+ */
156
+ export function stateChangeReadBackRelevance(contract) {
157
+ const rule = 'state-change-read-back';
158
+ const operations = operationsOf(contract);
159
+ if (operations.length === 0)
160
+ return verdict(rule, true, NO_OPERATION);
161
+ for (const operation of operations) {
162
+ if (operation.stateChangeMarker) {
163
+ return verdict(rule, true, `operation ${operation.operationId} declares stateChangeMarker: true`);
164
+ }
165
+ }
166
+ return verdict(rule, false, 'no operation declares stateChangeMarker: true');
167
+ }
168
+ /**
169
+ * One predicate per rule. The mapped type fails the typecheck when a member of
170
+ * `DISCIPLINE_RULES` has no predicate. Same idiom as `operatorHandlers` in
171
+ * `core/evaluate/resolution.ts`, minus its `Object.hasOwn` guard: these keys
172
+ * come from a compile-time tuple, so a runtime guard is unreachable.
173
+ */
174
+ export const RELEVANCE_PREDICATES = {
175
+ 'success-indicator-separation': successIndicatorSeparationRelevance,
176
+ 'whole-body': wholeBodyRelevance,
177
+ 'malformed-input': malformedInputRelevance,
178
+ 'per-record': perRecordRelevance,
179
+ 'sibling-cross-check': siblingCrossCheckRelevance,
180
+ 'omission-and-completeness': omissionAndCompletenessRelevance,
181
+ 'state-change-read-back': stateChangeReadBackRelevance,
182
+ };
183
+ /** All seven verdicts, in `DISCIPLINE_RULES` order, over declarations alone. */
184
+ export function evaluateRelevance(contract) {
185
+ return DISCIPLINE_RULES.map((rule) => RELEVANCE_PREDICATES[rule](contract));
186
+ }
@@ -0,0 +1,7 @@
1
+ /** AD-20's seven discipline rules as identifiers, in AD-20's enumeration order. */
2
+ export declare const DISCIPLINE_RULES: readonly ['success-indicator-separation', 'whole-body', 'malformed-input', 'per-record', 'sibling-cross-check', 'omission-and-completeness', 'state-change-read-back'];
3
+ export type DisciplineRule = (typeof DISCIPLINE_RULES)[number];
4
+ /** `relevancePredicate` on a coverage-gap record. Derived, so a new rule arrives with one. */
5
+ export declare const relevancePredicateId: (rule: DisciplineRule) => string;
6
+ /** `satisfactionPredicate` on a coverage-gap record. Derived, like its relevance twin. */
7
+ export declare const satisfactionPredicateId: (rule: DisciplineRule) => string;
@@ -0,0 +1,18 @@
1
+ /** AD-20's seven discipline rules as identifiers, in AD-20's enumeration order. */
2
+ // AD-20 names its seven rules in prose and gives them no identifiers, which is
3
+ // why `Waiver.rule` and `CoverageGap.rule` are opaque strings. These spellings
4
+ // are the Gate C contract's own `oracles[].rule` values
5
+ // (reviews/gate-c/eval-contract.json:267-446), so a gap joins a waiver.
6
+ export const DISCIPLINE_RULES = [
7
+ 'success-indicator-separation',
8
+ 'whole-body',
9
+ 'malformed-input',
10
+ 'per-record',
11
+ 'sibling-cross-check',
12
+ 'omission-and-completeness',
13
+ 'state-change-read-back',
14
+ ];
15
+ /** `relevancePredicate` on a coverage-gap record. Derived, so a new rule arrives with one. */
16
+ export const relevancePredicateId = (rule) => `${rule}-relevance`;
17
+ /** `satisfactionPredicate` on a coverage-gap record. Derived, like its relevance twin. */
18
+ export const satisfactionPredicateId = (rule) => `${rule}-satisfaction`;