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,313 @@
1
+ /**
2
+ * AD-34's planning half for AD-10's pre-flight: the contract and its probes in,
3
+ * a description of every request the environment-probe port must issue out.
4
+ * Pure and synchronous; nothing here awaits, reads a clock, or observes.
5
+ *
6
+ * The plan carries everything the reducer reads, because `ReduceStage` takes a
7
+ * plan and observations and nothing else. That is why a `PlannedCheck` embeds
8
+ * the operation and the witness themselves: there is nothing to look an
9
+ * identifier up in at reduce time.
10
+ */
11
+ import { checkInputsAgainstShape } from '../compile/sensitivity-witness.js';
12
+ import { declaresNoRequiredKeys } from '../declared-inputs.js';
13
+ import { StructuralFailure } from '../failure-codes.js';
14
+ import { referenceSetMembers } from './witness-evidence.js';
15
+ const requestOf = (legId, interfaceId, operation, inputs) => ({
16
+ // NFR9's correlation by identifier: the port echoes this back, and it is
17
+ // the only thing that tells two legs of one operation apart.
18
+ probeId: legId,
19
+ interfaceId,
20
+ operationId: operation.operationId,
21
+ method: operation.method,
22
+ pathTemplate: operation.pathTemplate,
23
+ channels: {
24
+ path: inputs.path,
25
+ query: inputs.query,
26
+ header: inputs.header,
27
+ body: inputs.body,
28
+ },
29
+ });
30
+ /**
31
+ * The contract declares no identifier for a control leg, so one is minted, and it
32
+ * has to avoid every identifier the contract already spends. The same input
33
+ * always produces the same suffix, which is what fixture 104 asserts.
34
+ */
35
+ const mintLegId = (base, taken) => {
36
+ if (!taken.has(base))
37
+ return base;
38
+ for (let ordinal = 2;; ordinal++) {
39
+ const candidate = `${base}-${ordinal}`;
40
+ if (!taken.has(candidate))
41
+ return candidate;
42
+ }
43
+ };
44
+ /**
45
+ * Every identifier a minted control leg has to avoid. The probes' manifestation
46
+ * witnesses are in the set even though they are not part of the contract: a
47
+ * minted id colliding with one is a collision the minting caused, and the
48
+ * duplicate scan below would report it against the probe's leg instead.
49
+ */
50
+ const declaredLegIds = (contract, probes) => {
51
+ const taken = new Set(contract.interactionPlan.map((step) => step.stepId));
52
+ for (const probe of probes)
53
+ if (!probe.expectedClean)
54
+ for (const defect of probe.defects) {
55
+ const legId = defect.manifestationWitness?.legId;
56
+ if (legId !== undefined)
57
+ taken.add(legId);
58
+ }
59
+ for (const iface of contract.permittedInterfaces)
60
+ for (const operation of iface.operations)
61
+ for (const leg of operation.sensitivityWitness?.legs ?? [])
62
+ taken.add(leg.legId);
63
+ if (contract.fixtureReset !== null)
64
+ taken.add(contract.fixtureReset.legId);
65
+ return taken;
66
+ };
67
+ const EMPTY_INPUTS = {
68
+ path: {},
69
+ query: {},
70
+ header: {},
71
+ body: { kind: 'absent' },
72
+ };
73
+ /**
74
+ * The inputs a control leg sends. A witness supplies them when the operation has
75
+ * one; an operation AD-10 exempts has no required key to fill, so empty inputs
76
+ * are a legal request. Returns `null` only when required keys exist that nothing
77
+ * here can supply.
78
+ *
79
+ * Reading a witness alone left the immutability branch unreachable for the
80
+ * contract that most needs it: a read-only contract whose only safe read is a
81
+ * parameterless GET is exempt, so it carried no witness, so no control leg was
82
+ * planned and the verdict passed with no immutability evidence at all.
83
+ */
84
+ const controlInputs = (operation) => {
85
+ const declared = operation.sensitivityWitness?.legs[0]?.inputs;
86
+ if (declared !== undefined)
87
+ return declared;
88
+ return declaresNoRequiredKeys(operation) ? EMPTY_INPUTS : null;
89
+ };
90
+ const targetOf = (iface, operation) => {
91
+ const inputs = controlInputs(operation);
92
+ return inputs === null ? null : { iface, operation, inputs };
93
+ };
94
+ /**
95
+ * AD-10 rule 3's selection, by declaration order so a fixture catches a later
96
+ * change to the rule. The observed operation is the first safe read anywhere in
97
+ * the contract that can be given inputs; the mutating one is the first
98
+ * marker-true operation on the interface the fixture reset names, resolved
99
+ * independently of where the observed read came from.
100
+ */
101
+ const selectControl = (contract) => {
102
+ let observed = null;
103
+ for (const iface of contract.permittedInterfaces) {
104
+ for (const operation of iface.operations) {
105
+ if (operation.stateChangeMarker)
106
+ continue;
107
+ observed = targetOf(iface, operation);
108
+ if (observed !== null)
109
+ break;
110
+ }
111
+ if (observed !== null)
112
+ break;
113
+ }
114
+ if (observed === null)
115
+ return null;
116
+ const reset = contract.fixtureReset;
117
+ if (reset === null)
118
+ return { observed, mutating: null };
119
+ const resetInterface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
120
+ let mutating = null;
121
+ for (const operation of resetInterface?.operations ?? []) {
122
+ if (!operation.stateChangeMarker)
123
+ continue;
124
+ mutating = targetOf(resetInterface, operation);
125
+ if (mutating !== null)
126
+ break;
127
+ }
128
+ return { observed, mutating };
129
+ };
130
+ /**
131
+ * AD-10's plan, derived from the interfaces the contract's probes exercise.
132
+ *
133
+ * Three constructions worth knowing. An `interface-present` check is emitted
134
+ * only for an operation that has at least one leg, because a check over no legs
135
+ * is satisfied vacuously and asserts nothing. The `state-reset` and
136
+ * `clean-control` checks are emitted only when control legs could be planned at
137
+ * all, for the same reason. And the fixture reset's own leg is one more leg
138
+ * through the same port, so its identifier shares the namespace every other leg
139
+ * identifier sits in.
140
+ */
141
+ export const planPreflight = (input) => {
142
+ const { contract, probes, runId } = input;
143
+ for (const iface of contract.permittedInterfaces) {
144
+ // Already thrown at compile; asserted again because the plan is reachable
145
+ // from a caller who assembled a contract by hand.
146
+ if (iface.kind !== 'api') {
147
+ throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported in v0; only "api" is (AD-10)`);
148
+ }
149
+ }
150
+ const origins = [];
151
+ const checks = [];
152
+ // Keyed by interface AND operation: `Operation.operationId` is scoped to a
153
+ // `PermittedInterface`, so two interfaces may legally declare the same one
154
+ // (its own schema description says so, and `duplicate-operation-signature`
155
+ // covers method plus path template only). Keyed by operation id alone, one
156
+ // interface's legs become another's clean legs.
157
+ const legIdsByOperation = new Map();
158
+ const scopeKey = (interfaceId, operationId) => `${interfaceId}\u0000${operationId}`;
159
+ const addLeg = (legId, purpose, interfaceId, operation, inputs, artifactPath) => {
160
+ origins.push({
161
+ leg: {
162
+ legId,
163
+ purpose,
164
+ request: requestOf(legId, interfaceId, operation, inputs),
165
+ operation,
166
+ inputs,
167
+ },
168
+ artifactPath,
169
+ });
170
+ const key = scopeKey(interfaceId, operation.operationId);
171
+ const group = legIdsByOperation.get(key);
172
+ if (group === undefined)
173
+ legIdsByOperation.set(key, [legId]);
174
+ else
175
+ group.push(legId);
176
+ };
177
+ // 1. the sensitivity legs and their checks
178
+ contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
179
+ iface.operations.forEach((operation, operationIndex) => {
180
+ const witness = operation.sensitivityWitness;
181
+ const path = `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
182
+ if (witness !== null)
183
+ witness.legs.forEach((leg, legIndex) => {
184
+ addLeg(leg.legId, 'sensitivity', iface.logicalId, operation, leg.inputs, `${path}.sensitivityWitness.legs[${legIndex}].legId`);
185
+ });
186
+ checks.push({
187
+ kind: 'input-sensitivity',
188
+ interfaceId: iface.logicalId,
189
+ operationId: operation.operationId,
190
+ witness,
191
+ operation,
192
+ });
193
+ });
194
+ });
195
+ // 3. the control legs, once per contract
196
+ const control = selectControl(contract);
197
+ const controlLegIds = [];
198
+ if (control !== null) {
199
+ const taken = declaredLegIds(contract, probes);
200
+ const mintControl = (base) => {
201
+ const legId = mintLegId(base, taken);
202
+ taken.add(legId);
203
+ return legId;
204
+ };
205
+ const { observed } = control;
206
+ const observe = (legId) => {
207
+ addLeg(legId, 'control-observe', observed.iface.logicalId, observed.operation, observed.inputs, `the minted control-observe leg against "${observed.operation.operationId}"`);
208
+ controlLegIds.push(legId);
209
+ };
210
+ const first = mintControl('preflight-control-observe');
211
+ observe(first);
212
+ const reset = contract.fixtureReset;
213
+ if (control.mutating !== null && reset !== null) {
214
+ const { mutating } = control;
215
+ const mutate = mintControl('preflight-control-mutate');
216
+ addLeg(mutate, 'control-mutate', mutating.iface.logicalId, mutating.operation, mutating.inputs, `the minted control-mutate leg against "${mutating.operation.operationId}"`);
217
+ controlLegIds.push(mutate);
218
+ const resetOperation = mutating.iface.operations.find((candidate) => candidate.operationId === reset.operationId);
219
+ if (resetOperation === undefined) {
220
+ 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)`);
221
+ }
222
+ addLeg(reset.legId, 'control-reset', reset.interfaceId, resetOperation, reset.inputs, 'EvalContract.fixtureReset.legId');
223
+ controlLegIds.push(reset.legId);
224
+ }
225
+ const last = mintControl('preflight-control-observe');
226
+ observe(last);
227
+ checks.push({ kind: 'state-reset', legIds: [first, last] });
228
+ // AD-10's own worked example is two distinct nonexistent identifiers both
229
+ // returning 404, so a `clean-control` reading sensitivity legs would fail
230
+ // on a contract that followed the architecture verbatim.
231
+ checks.push({ kind: 'clean-control', legIds: [...controlLegIds] });
232
+ }
233
+ // 5. one leg and two checks per seeded defect
234
+ for (const probe of probes) {
235
+ if (probe.expectedClean)
236
+ continue;
237
+ for (const defect of probe.defects) {
238
+ const witness = defect.manifestationWitness;
239
+ const path = `Probe[probeId=${probe.probeId}].defects[defectId=${defect.defectId}].manifestationWitness`;
240
+ if (witness === null) {
241
+ checks.push({
242
+ kind: 'seeded-fault-fired',
243
+ defectId: defect.defectId,
244
+ witness: null,
245
+ operation: null,
246
+ });
247
+ continue;
248
+ }
249
+ const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === witness.interfaceId);
250
+ const operation = iface?.operations.find((candidate) => candidate.operationId === witness.operationId);
251
+ if (operation === undefined) {
252
+ throw new StructuralFailure('unreachable-check-evidence', path, `the manifestation witness names operation "${witness.operationId}" on interface "${witness.interfaceId}", which the contract does not declare (AD-10)`);
253
+ }
254
+ // A manifestation witness lives on the probe, so the compiler never sees
255
+ // its inputs. They reach the port all the same, so they are checked here.
256
+ checkInputsAgainstShape(witness.inputs, operation, `the manifestation witness of ${defect.defectId}`, `${path}.inputs`);
257
+ // Read before the fault leg joins the group, which keeps the fault leg out
258
+ // of its own clean-leg set.
259
+ const cleanLegIds = [
260
+ ...(legIdsByOperation.get(scopeKey(witness.interfaceId, operation.operationId)) ?? []),
261
+ ];
262
+ addLeg(witness.legId, 'seeded-fault', witness.interfaceId, operation, witness.inputs, `${path}.legId`);
263
+ checks.push({
264
+ kind: 'seeded-faults-scoped',
265
+ defectId: defect.defectId,
266
+ witness,
267
+ operation,
268
+ cleanLegIds,
269
+ });
270
+ checks.push({
271
+ kind: 'seeded-fault-fired',
272
+ defectId: defect.defectId,
273
+ witness,
274
+ operation,
275
+ });
276
+ }
277
+ }
278
+ // Two legs sharing an identifier would share a `probeId`, and the reducer
279
+ // indexes observations by it, so one leg would answer for the other. The
280
+ // contract side is already checked at compile; a manifestation witness is
281
+ // not, since `Probe` is not part of the contract.
282
+ const seen = new Set();
283
+ for (const origin of origins) {
284
+ if (seen.has(origin.leg.legId)) {
285
+ throw new StructuralFailure('malformed-operator-expression', origin.artifactPath, `leg id "${origin.leg.legId}" is already claimed by another planned leg; two legs sharing a probeId cannot be told apart (AD-10, NFR9)`);
286
+ }
287
+ seen.add(origin.leg.legId);
288
+ }
289
+ // AD-11 makes the fixture digest a required field of the verdict, and a digest
290
+ // over no observation would certify a pre-flight that verified nothing. A
291
+ // contract that offers pre-flight nothing to probe is refused here, where the
292
+ // contract is in hand and the failure can carry a code.
293
+ if (origins.length === 0) {
294
+ throw new StructuralFailure('unreachable-check-evidence', 'EvalContract.permittedInterfaces', 'the contract declares no operation pre-flight can probe, so there is no fixture to verify or digest (AD-10, AD-11)');
295
+ }
296
+ // 2. one interface-present check per operation that has a leg, in
297
+ // declaration order, ahead of the rest
298
+ const presence = [...legIdsByOperation].map(([key, legIds]) => {
299
+ const [interfaceId = '', operationId = ''] = key.split('\u0000');
300
+ return {
301
+ kind: 'interface-present',
302
+ interfaceId,
303
+ operationId,
304
+ legIds: [...legIds],
305
+ };
306
+ });
307
+ return {
308
+ runId,
309
+ legs: origins.map((origin) => origin.leg),
310
+ checks: [...presence, ...checks],
311
+ referenceSets: referenceSetMembers(contract),
312
+ };
313
+ };
@@ -0,0 +1,40 @@
1
+ import type { Operation } from '../schemas/interface.ts';
2
+ import type { ProbeObservation, ProbeObservedBody } from '../schemas/port-messages.ts';
3
+ export declare const PREFLIGHT_ARTIFACT_PATH = "PreflightVerdict";
4
+ /**
5
+ * The five fields and no others. Response headers are outside the projection
6
+ * because `volatilePointers` is a `DescriptorPointer` and the response
7
+ * descriptor is body-scoped, so no declaration can mark a header volatile, and
8
+ * unprunable headers would fail the repeated-read immutability branch on any
9
+ * fixture that echoes a request identifier back in one.
10
+ *
11
+ * Two costs to know about: two fixture versions differing only in a response
12
+ * header digest identically, and a witness relation can read a header this
13
+ * projection cannot see. The projection says what the fixture is; a relation
14
+ * addressing a header is the author asserting that header is stable.
15
+ */
16
+ export type ProjectedObservation = {
17
+ readonly legId: string;
18
+ readonly interfaceId: string;
19
+ readonly operationId: string;
20
+ readonly status: number;
21
+ readonly body: ProbeObservedBody;
22
+ };
23
+ /**
24
+ * Removes every declared volatile pointer from a `json` body. `text` and
25
+ * `absent` pass through untouched, so a body that changed its content type
26
+ * stays visible to the digest. The input is cloned before anything is deleted.
27
+ */
28
+ export declare function pruneVolatile(body: ProbeObservedBody, volatilePointers: readonly string[], artifactPath: string): ProbeObservedBody;
29
+ /** One observation reduced to the projection, with its volatile pointers gone. */
30
+ export declare function projectObservation(observation: ProbeObservation, operation: Operation, artifactPath: string): ProjectedObservation;
31
+ /**
32
+ * AD-11's fixture digest. Sorted by leg id first, because NFR9 forbids any
33
+ * stage from reading array position: two runs whose observations arrived in a
34
+ * different order describe the same fixture and must digest the same.
35
+ *
36
+ * The empty case throws here. `digestComposite` rejects an empty field bag, and
37
+ * `{ observations: [] }` has one field, so it would happily digest a pre-flight
38
+ * that verified nothing.
39
+ */
40
+ export declare function fixtureDigest(projections: readonly ProjectedObservation[], artifactPath: string): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * AD-11's named closed projection of a probe observation, and the fixture
3
+ * digest computed over it. The projection is what "the fixture" means for a
4
+ * scoring version: two runs whose projections agree describe the same fixture.
5
+ */
6
+ import { digestComposite } from '../canonical/digest.js';
7
+ import { DESCRIPTOR_POINTER_PATTERN } from '../schemas/pointer.js';
8
+ import { decodeTail } from '../seal/plan-index.js';
9
+ export const PREFLIGHT_ARTIFACT_PATH = 'PreflightVerdict';
10
+ const isJsonObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ const ARRAY_INDEX = /^(?:0|[1-9][0-9]*)$/;
12
+ /** Deletes one already-decoded pointer from a cloned body value, in place. */
13
+ function deleteAt(root, tokens) {
14
+ let parent = root;
15
+ for (const token of tokens.slice(0, -1)) {
16
+ if (Array.isArray(parent)) {
17
+ if (!ARRAY_INDEX.test(token))
18
+ return;
19
+ const next = parent[Number(token)];
20
+ if (next === undefined)
21
+ return;
22
+ parent = next;
23
+ continue;
24
+ }
25
+ if (!isJsonObject(parent) || !Object.hasOwn(parent, token))
26
+ return;
27
+ parent = parent[token];
28
+ }
29
+ const last = tokens.at(-1);
30
+ if (last === undefined)
31
+ return;
32
+ if (Array.isArray(parent)) {
33
+ if (!ARRAY_INDEX.test(last))
34
+ return;
35
+ const index = Number(last);
36
+ if (index >= parent.length)
37
+ return;
38
+ parent.splice(index, 1);
39
+ return;
40
+ }
41
+ if (!isJsonObject(parent))
42
+ return;
43
+ // A pointer that resolves to nothing is a no-op: a volatile field the fixture
44
+ // did not return this time is exactly what the declaration exists for.
45
+ if (!Object.hasOwn(parent, last))
46
+ return;
47
+ delete parent[last];
48
+ }
49
+ /**
50
+ * Removes every declared volatile pointer from a `json` body. `text` and
51
+ * `absent` pass through untouched, so a body that changed its content type
52
+ * stays visible to the digest. The input is cloned before anything is deleted.
53
+ */
54
+ export function pruneVolatile(body, volatilePointers, artifactPath) {
55
+ for (const pointer of volatilePointers) {
56
+ if (DESCRIPTOR_POINTER_PATTERN.test(pointer))
57
+ continue;
58
+ throw new TypeError(`${artifactPath}: "${pointer}" is not a descriptor-relative pointer`);
59
+ }
60
+ if (body.kind !== 'json' || volatilePointers.length === 0)
61
+ return body;
62
+ // RFC 6901's empty pointer addresses the whole document, so a contract
63
+ // declaring it says the entire body is volatile.
64
+ if (volatilePointers.some((pointer) => pointer === ''))
65
+ return { kind: 'absent' };
66
+ const value = structuredClone(body.value);
67
+ for (const pointer of volatilePointers)
68
+ deleteAt(value, decodeTail(pointer));
69
+ return { kind: 'json', value };
70
+ }
71
+ /** One observation reduced to the projection, with its volatile pointers gone. */
72
+ export function projectObservation(observation, operation, artifactPath) {
73
+ return {
74
+ legId: observation.probeId,
75
+ interfaceId: observation.interfaceId,
76
+ operationId: observation.operationId,
77
+ status: observation.status,
78
+ body: pruneVolatile(observation.body, operation.volatilePointers, artifactPath),
79
+ };
80
+ }
81
+ /**
82
+ * AD-11's fixture digest. Sorted by leg id first, because NFR9 forbids any
83
+ * stage from reading array position: two runs whose observations arrived in a
84
+ * different order describe the same fixture and must digest the same.
85
+ *
86
+ * The empty case throws here. `digestComposite` rejects an empty field bag, and
87
+ * `{ observations: [] }` has one field, so it would happily digest a pre-flight
88
+ * that verified nothing.
89
+ */
90
+ export function fixtureDigest(projections, artifactPath) {
91
+ if (projections.length === 0) {
92
+ throw new TypeError(`${artifactPath}: a fixture digest over no observation would certify a pre-flight that verified nothing`);
93
+ }
94
+ const sorted = [...projections].sort((left, right) => left.legId < right.legId ? -1 : left.legId > right.legId ? 1 : 0);
95
+ return digestComposite({ observations: sorted }, artifactPath);
96
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProbeObservation } from '../schemas/port-messages.ts';
2
+ import type { PreflightVerdict } from '../schemas/preflight-verdict.ts';
3
+ import type { ReduceStage } from '../stage-contracts.ts';
4
+ import type { PreflightPlan } from './plan.ts';
5
+ export type PreflightObservations = {
6
+ readonly observations: readonly ProbeObservation[];
7
+ };
8
+ export declare const reducePreflight: ReduceStage<PreflightPlan, PreflightObservations, PreflightVerdict>;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * AD-34's reducing half: the plan plus the observations the port returned,
3
+ * reduced to a `PreflightVerdict`. Pure over the observations, and the only
4
+ * place AD-10's verdict semantics live.
5
+ *
6
+ * Observations are indexed by `probeId` (NFR9), and one `PreflightCheck` is
7
+ * emitted per `PlannedCheck`, so a verdict carries only the kinds its plan
8
+ * asked for.
9
+ */
10
+ import { digestArtifact } from '../canonical/digest.js';
11
+ import { declaresNoRequestKeys } from '../declared-inputs.js';
12
+ import { freezeArtifact } from '../lineage/freeze.js';
13
+ import { RuntimeFault } from '../schemas/faults.js';
14
+ import { fixtureDigest, PREFLIGHT_ARTIFACT_PATH, projectObservation, } from './projection.js';
15
+ import { evidenceOf, resolveWitnessRelation } from './witness-evidence.js';
16
+ /**
17
+ * AD-10 names no threshold, and this is the one the repository already speaks:
18
+ * the published conformance suite ships `probe/observe-anomalous-status`, and
19
+ * `ProbeObservation.status` is bounded to 100-599 at the port, so HTTP is
20
+ * already assumed at that boundary.
21
+ */
22
+ const ANOMALOUS_STATUS = 400;
23
+ const check = (kind, operationId, outcome, note) => ({ kind, operationId, outcome, note });
24
+ /**
25
+ * Two projections describe the same fixture state. `legId` is excluded because
26
+ * it is the one field that necessarily differs between the two legs compared, so
27
+ * deep-equality over the whole projection could never be satisfied.
28
+ *
29
+ * Compared through the canonical digest, because `JSON.stringify` is key-order
30
+ * sensitive: two adapters serialising one body's keys in different orders
31
+ * describe the same fixture, and failing there would invalidate a run over a
32
+ * difference that is not one.
33
+ */
34
+ const sameFixtureState = (left, right) => {
35
+ const state = ({ legId: _legId, ...rest }) => rest;
36
+ return (digestArtifact(state(left), PREFLIGHT_ARTIFACT_PATH) ===
37
+ digestArtifact(state(right), PREFLIGHT_ARTIFACT_PATH));
38
+ };
39
+ /**
40
+ * Resolves a manifestation witness against one leg. Returns `null` when that
41
+ * leg produced no observation, which the two seeded-fault rows read
42
+ * differently: the fired row fails on it, the scoped row has nothing to
43
+ * contradict.
44
+ */
45
+ const resolveAgainst = (witness, state, plan, artifactPath) => {
46
+ if (state === undefined)
47
+ return null;
48
+ // Keyed by the witness's own leg id, since the relation addresses
49
+ // `/interactions/{witness.legId}/…`. The scoped row asks whether that same
50
+ // relation would fire on a clean leg's observation.
51
+ return resolveWitnessRelation(witness.relation, { [witness.legId]: state.evidence }, state.leg.operation, plan.referenceSets, artifactPath).resolution;
52
+ };
53
+ export const reducePreflight = (plan, { observations }) => {
54
+ const byProbeId = new Map();
55
+ for (const observation of observations) {
56
+ // A repeated `probeId` is a broken echo: `ProbeRequest.probeId` comes
57
+ // back unchanged by contract, so two observations claiming one leg means
58
+ // the port answered a request nobody made. Left as a last-write-wins
59
+ // `Map` it would also make the verdict depend on array order, which is
60
+ // the class AD-30's permutation family exists to catch.
61
+ if (byProbeId.has(observation.probeId)) {
62
+ throw new RuntimeFault('port-contract-violation', PREFLIGHT_ARTIFACT_PATH, `two observations echoed the probe id "${observation.probeId}", so one leg was answered twice`);
63
+ }
64
+ byProbeId.set(observation.probeId, observation);
65
+ }
66
+ const states = new Map();
67
+ for (const leg of plan.legs) {
68
+ const observation = byProbeId.get(leg.legId);
69
+ if (observation === undefined)
70
+ continue;
71
+ const projected = projectObservation(observation, leg.operation, PREFLIGHT_ARTIFACT_PATH);
72
+ states.set(leg.legId, {
73
+ leg,
74
+ observation,
75
+ projected,
76
+ evidence: evidenceOf(projected, observation, leg.inputs),
77
+ });
78
+ }
79
+ const reduceCheck = (planned) => {
80
+ switch (planned.kind) {
81
+ case 'interface-present': {
82
+ for (const legId of planned.legIds) {
83
+ const state = states.get(legId);
84
+ if (state === undefined)
85
+ return check(planned.kind, planned.operationId, 'failed', `leg "${legId}" produced no observation`);
86
+ const { request } = state.leg;
87
+ const echoed = state.observation;
88
+ if (echoed.probeId !== request.probeId ||
89
+ echoed.interfaceId !== request.interfaceId ||
90
+ echoed.operationId !== request.operationId)
91
+ return check(planned.kind, planned.operationId, 'failed', `leg "${legId}" echoed an identifier the request did not carry`);
92
+ }
93
+ return check(planned.kind, planned.operationId, 'satisfied', null);
94
+ }
95
+ case 'input-sensitivity': {
96
+ const { witness, operation } = planned;
97
+ if (witness === null)
98
+ return check(planned.kind, planned.operationId, declaresNoRequestKeys(operation) ? 'exempt' : 'failed', declaresNoRequestKeys(operation)
99
+ ? 'The operation declares no inputs in any channel.'
100
+ : 'The operation declares request keys and no sensitivity witness.');
101
+ const evidence = {};
102
+ for (const leg of witness.legs) {
103
+ const state = states.get(leg.legId);
104
+ if (state === undefined)
105
+ return check(planned.kind, planned.operationId, 'failed', `witness leg "${leg.legId}" produced no observation`);
106
+ evidence[leg.legId] = state.evidence;
107
+ }
108
+ const { resolution } = resolveWitnessRelation(witness.relation, evidence, operation, plan.referenceSets, PREFLIGHT_ARTIFACT_PATH);
109
+ // AD-10's own sentence, and the most load-bearing line here: a
110
+ // sensitivity check that examined nothing has established nothing,
111
+ // so `insufficient-evidence` fails.
112
+ if (resolution === 'insufficient-evidence')
113
+ return check(planned.kind, planned.operationId, 'failed', 'The witness relation resolved insufficient-evidence.');
114
+ return check(planned.kind, planned.operationId, resolution === 'true' ? 'satisfied' : 'failed', resolution === 'true' ? null : 'The witness relation resolved false.');
115
+ }
116
+ case 'state-reset': {
117
+ const [firstId, lastId] = planned.legIds;
118
+ const first = states.get(firstId);
119
+ const last = states.get(lastId);
120
+ if (first === undefined || last === undefined)
121
+ return check(planned.kind, null, 'failed', 'a control-observe leg produced no observation');
122
+ return sameFixtureState(first.projected, last.projected)
123
+ ? check(planned.kind, null, 'satisfied', null)
124
+ : check(planned.kind, null, 'failed', `the projections of "${firstId}" and "${lastId}" differ`);
125
+ }
126
+ case 'clean-control': {
127
+ for (const legId of planned.legIds) {
128
+ const state = states.get(legId);
129
+ if (state === undefined)
130
+ return check(planned.kind, null, 'failed', `control leg "${legId}" produced no observation`);
131
+ if (state.observation.status >= ANOMALOUS_STATUS)
132
+ return check(planned.kind, null, 'failed', `control leg "${legId}" observed status ${state.observation.status}`);
133
+ }
134
+ return check(planned.kind, null, 'satisfied', null);
135
+ }
136
+ case 'seeded-faults-scoped': {
137
+ const { witness, defectId } = planned;
138
+ for (const legId of planned.cleanLegIds) {
139
+ const resolved = resolveAgainst(witness, states.get(legId), plan, PREFLIGHT_ARTIFACT_PATH);
140
+ if (resolved === 'true')
141
+ return check(planned.kind, witness.operationId, 'failed', `${defectId}: the manifestation witness fires on clean leg "${legId}"`);
142
+ }
143
+ return check(planned.kind, witness.operationId, 'satisfied', defectId);
144
+ }
145
+ case 'seeded-fault-fired': {
146
+ const { witness, defectId } = planned;
147
+ if (witness === null)
148
+ return check(planned.kind, null, 'failed', `${defectId}: the defect declares no manifestation witness, so it cannot be observed to fire`);
149
+ const resolved = resolveAgainst(witness, states.get(witness.legId), plan, PREFLIGHT_ARTIFACT_PATH);
150
+ if (resolved === 'true')
151
+ return check(planned.kind, witness.operationId, 'satisfied', defectId);
152
+ return check(planned.kind, witness.operationId, 'failed', resolved === null
153
+ ? `${defectId}: the fault leg produced no observation`
154
+ : `${defectId}: the manifestation witness resolved ${resolved} on its own fault leg`);
155
+ }
156
+ }
157
+ };
158
+ // The plan named legs and not one observation answered to a planned
159
+ // `probeId`. `ProbeRequest.probeId` is echoed unchanged by contract, so the
160
+ // port broke that contract and there is no observation to digest a fixture
161
+ // from. Thrown here as a typed fault: `fixtureDigest` would otherwise raise
162
+ // an untyped throw from inside the returned object literal, discarding the
163
+ // checks already computed.
164
+ if (states.size === 0) {
165
+ throw new RuntimeFault('port-contract-violation', PREFLIGHT_ARTIFACT_PATH, `no observation echoed any of the ${plan.legs.length} planned probe ids, so no fixture was observed`);
166
+ }
167
+ const checks = plan.checks.map(reduceCheck);
168
+ const projections = [...states.values()].map((state) => state.projected);
169
+ return freezeArtifact({
170
+ // A pre-flight verdict is an origin artifact, so AD-29's lineage fields
171
+ // carry their origin values.
172
+ schemaVersion: 1,
173
+ parentDigest: null,
174
+ revisionCount: 0,
175
+ runId: plan.runId,
176
+ fixtureDigest: fixtureDigest(projections, PREFLIGHT_ARTIFACT_PATH),
177
+ passed: checks.every((entry) => entry.outcome !== 'failed'),
178
+ checks,
179
+ });
180
+ };