eval-quality 0.2.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 (125) hide show
  1. package/README.md +3 -3
  2. package/corpus/dev/README.md +3 -2
  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/cli/render.d.ts +2 -5
  28. package/dist/cli/render.js +51 -1
  29. package/dist/core/compile/bindings.d.ts +26 -7
  30. package/dist/core/compile/bindings.js +49 -32
  31. package/dist/core/compile/compile.d.ts +12 -0
  32. package/dist/core/compile/compile.js +28 -1
  33. package/dist/core/compile/excluded-content.d.ts +11 -0
  34. package/dist/core/compile/excluded-content.js +42 -0
  35. package/dist/core/compile/expression-legality.d.ts +2 -2
  36. package/dist/core/compile/expression-legality.js +58 -12
  37. package/dist/core/compile/interface-inventory.d.ts +34 -1
  38. package/dist/core/compile/interface-inventory.js +88 -11
  39. package/dist/core/compile/reachability.d.ts +12 -2
  40. package/dist/core/compile/reachability.js +123 -36
  41. package/dist/core/compile/schema-version.d.ts +2 -0
  42. package/dist/core/compile/schema-version.js +25 -0
  43. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  44. package/dist/core/compile/sensitivity-witness.js +110 -23
  45. package/dist/core/compile/step-reference.d.ts +2 -0
  46. package/dist/core/compile/step-reference.js +49 -0
  47. package/dist/core/coverage/operations.d.ts +62 -0
  48. package/dist/core/coverage/operations.js +57 -0
  49. package/dist/core/coverage/relevance.d.ts +4 -2
  50. package/dist/core/coverage/relevance.js +22 -23
  51. package/dist/core/coverage/satisfaction.d.ts +2 -2
  52. package/dist/core/coverage/satisfaction.js +73 -48
  53. package/dist/core/declared-inputs.d.ts +83 -4
  54. package/dist/core/declared-inputs.js +105 -8
  55. package/dist/core/evaluate/evidence-resolution.d.ts +9 -12
  56. package/dist/core/evaluate/evidence-resolution.js +70 -10
  57. package/dist/core/evaluate/operators.d.ts +22 -0
  58. package/dist/core/evaluate/operators.js +17 -1
  59. package/dist/core/evaluate/resolution.d.ts +14 -3
  60. package/dist/core/evaluate/resolution.js +63 -3
  61. package/dist/core/excluded-content.d.ts +65 -0
  62. package/dist/core/excluded-content.js +113 -0
  63. package/dist/core/failure-codes.d.ts +2 -2
  64. package/dist/core/failure-codes.js +5 -2
  65. package/dist/core/ingest/conditions.d.ts +1 -1
  66. package/dist/core/ingest/ingest.js +8 -0
  67. package/dist/core/preflight/plan.d.ts +7 -5
  68. package/dist/core/preflight/plan.js +75 -32
  69. package/dist/core/preflight/projection.d.ts +6 -3
  70. package/dist/core/preflight/projection.js +22 -2
  71. package/dist/core/preflight/reduce.js +47 -7
  72. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  73. package/dist/core/preflight/witness-evidence.js +63 -20
  74. package/dist/core/schemas/artifact.d.ts +683 -35
  75. package/dist/core/schemas/constraint-ledger.js +11 -0
  76. package/dist/core/schemas/defect-signature.d.ts +438 -16
  77. package/dist/core/schemas/defect-signature.js +58 -10
  78. package/dist/core/schemas/eval-contract.d.ts +517 -12
  79. package/dist/core/schemas/eval-contract.js +18 -2
  80. package/dist/core/schemas/evidence-artifact.d.ts +14 -4
  81. package/dist/core/schemas/evidence-artifact.js +1 -1
  82. package/dist/core/schemas/interface.d.ts +741 -13
  83. package/dist/core/schemas/interface.js +120 -7
  84. package/dist/core/schemas/isolation-manifest.js +16 -9
  85. package/dist/core/schemas/plan.d.ts +217 -3
  86. package/dist/core/schemas/plan.js +22 -1
  87. package/dist/core/schemas/pointer.d.ts +47 -1
  88. package/dist/core/schemas/pointer.js +89 -8
  89. package/dist/core/schemas/port-messages.d.ts +194 -10
  90. package/dist/core/schemas/port-messages.js +73 -6
  91. package/dist/core/schemas/probe-body.d.ts +18 -0
  92. package/dist/core/schemas/probe-body.js +13 -0
  93. package/dist/core/schemas/probe.d.ts +122 -3
  94. package/dist/core/schemas/probe.js +1 -1
  95. package/dist/core/schemas/sealed-run-record.d.ts +131 -9
  96. package/dist/core/schemas/sealed-run-record.js +56 -11
  97. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  98. package/dist/core/schemas/sensitivity-witness.js +61 -5
  99. package/dist/core/score/bindings.d.ts +2 -2
  100. package/dist/core/score/bindings.js +25 -13
  101. package/dist/core/score/qualification.d.ts +5 -5
  102. package/dist/core/score/qualification.js +67 -37
  103. package/dist/core/score/quotation.d.ts +1 -24
  104. package/dist/core/score/quotation.js +29 -4
  105. package/dist/core/score/score.js +28 -2
  106. package/dist/core/score/strength.d.ts +9 -0
  107. package/dist/core/score/strength.js +35 -1
  108. package/dist/core/score/witness.d.ts +21 -0
  109. package/dist/core/score/witness.js +9 -4
  110. package/dist/core/seal/derived-reference.d.ts +2 -9
  111. package/dist/core/seal/derived-reference.js +62 -23
  112. package/dist/core/seal/plan-index.d.ts +18 -3
  113. package/dist/core/seal/plan-index.js +44 -8
  114. package/dist/index.d.ts +1 -1
  115. package/dist/index.js +1 -1
  116. package/dist/ports/environment-probe-port.d.ts +60 -4
  117. package/dist/testing/probe-conformance.d.ts +29 -0
  118. package/dist/testing/probe-conformance.js +34 -5
  119. package/package.json +1 -1
  120. package/schemas/eval-contract.schema.json +1356 -464
  121. package/schemas/evidence-artifact.schema.json +59 -29
  122. package/schemas/isolation-manifest.schema.json +17 -10
  123. package/schemas/probe.schema.json +197 -71
  124. package/schemas/rubric.schema.json +1 -1
  125. package/schemas/sealed-run-record.schema.json +296 -39
@@ -12,9 +12,11 @@
12
12
  * the artifact still ships (AD-5), so there is no failure code.
13
13
  */
14
14
  import { substitutePointer } from '../compile/oracle-alignment.js';
15
+ import { boundChannelsOf } from '../declared-inputs.js';
15
16
  import { SIBLING_GROUP_MINIMUM, } from '../schemas/eval-contract.js';
16
- import { TRANSPORT_CHANNELS } from '../schemas/pointer.js';
17
- import { buildPlanIndex } from '../seal/plan-index.js';
17
+ import { INPUT_CHANNELS } from '../schemas/pointer.js';
18
+ import { anyOperationOf, buildPlanIndex, } from '../seal/plan-index.js';
19
+ import { encodeToken, resolveOperations, } from './operations.js';
18
20
  import { DISCIPLINE_RULES, satisfactionPredicateId, } from './rules.js';
19
21
  const verdict = (rule, satisfied, reason) => ({
20
22
  rule,
@@ -26,21 +28,22 @@ const verdict = (rule, satisfied, reason) => ({
26
28
  export const NO_RELEVANT_SITE = 'the rule is relevant for no site, so satisfaction holds vacuously';
27
29
  /** A contract declaring no operation leaves six of the rules an absent site. */
28
30
  export const NO_OPERATION_WITNESS = 'the contract declares no operation, so the site this rule fires on has no declaration to witness';
29
- /** Every declared operation, flattened. `relevance.ts` keeps its own copy. */
30
- const operationsOf = (contract) => contract.permittedInterfaces.flatMap((declared) => declared.operations);
31
31
  /** `unresolved` never throws; a duplicate identifier resolves to nothing. */
32
32
  const planIndexOf = (contract) => buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, {
33
33
  duplicateIds: 'unresolved',
34
34
  });
35
35
  // ---- the join between the two pointer spellings -------------------------
36
- /** RFC 6901 escaping, `~` before `/`. */
37
- const encodeToken = (token) => token.replace(/~/g, '~0').replace(/\//g, '~1');
38
36
  /** Everything one step produced or was given. */
39
37
  const stepRoot = (stepId) => `/interactions/${stepId}`;
40
- /** A descriptor pointer read at one step, spelled interaction-rooted. */
41
- const bodyPointer = (stepId, descriptorPointer) => `/interactions/${stepId}/response-body${descriptorPointer}`;
42
- /** One declared response key at one step. */
43
- const keyPointer = (stepId, key) => `/interactions/${stepId}/response-body/${encodeToken(key)}`;
38
+ /**
39
+ * A descriptor pointer read at one step, spelled interaction-rooted. The root
40
+ * travels on the resolved operation, so an interface kind whose response
41
+ * arrives on another channel is one change in `operations.ts` and none at the
42
+ * five call sites below.
43
+ */
44
+ const bodyPointer = (resolved, stepId, descriptorPointer) => `${stepRoot(stepId)}${resolved.descriptorRoot}${descriptorPointer}`;
45
+ /** One declared response key at one step, rooted the way `bodyPointer` is. */
46
+ const keyPointer = (resolved, stepId, key) => `${stepRoot(stepId)}${resolved.descriptorRoot}/${encodeToken(key)}`;
44
47
  /** One declared parameter on one transport channel at one step. */
45
48
  const parameterPointer = (stepId, channel, key) => `/interactions/${stepId}/call-inputs/${channel}/${encodeToken(key)}`;
46
49
  /** A pointer addresses a root when it names it or descends into it. */
@@ -113,7 +116,7 @@ const bothChannelsAddress = (oracle, root) => someAddresses(oracle.directionTarg
113
116
  someAddresses(oracle.checkPointers, root);
114
117
  const definedPointers = (node) => node.operandPointers.filter((pointer) => pointer !== null);
115
118
  const contextOf = (contract) => ({
116
- operations: operationsOf(contract),
119
+ operations: resolveOperations(contract),
117
120
  index: planIndexOf(contract),
118
121
  oracles: oracleViewsOf(contract),
119
122
  });
@@ -130,8 +133,9 @@ export function successIndicatorSeparationSatisfaction(contract, context = conte
130
133
  if (operations.length === 0)
131
134
  return verdict(rule, false, NO_OPERATION_WITNESS);
132
135
  let sites = 0;
133
- for (const operation of operations) {
134
- const { successIndicator, channelRoles } = operation.responseDescriptor;
136
+ for (const resolved of operations) {
137
+ const { operation, descriptor } = resolved;
138
+ const { successIndicator, channelRoles } = descriptor;
135
139
  if (successIndicator === null) {
136
140
  return verdict(rule, false, `operation ${operation.operationId} nominates no success indicator, so no oracle can separate one from the body`);
137
141
  }
@@ -147,8 +151,8 @@ export function successIndicatorSeparationSatisfaction(contract, context = conte
147
151
  .map(([pointer]) => pointer);
148
152
  const witnessed = index
149
153
  .stepsUsing(operation.operationId)
150
- .some((step) => oracles.some((oracle) => bothChannelsAddress(oracle, bodyPointer(step.stepId, successIndicator)) &&
151
- others.some((pointer) => bothChannelsAddress(oracle, bodyPointer(step.stepId, pointer)))));
154
+ .some((step) => oracles.some((oracle) => bothChannelsAddress(oracle, bodyPointer(resolved, step.stepId, successIndicator)) &&
155
+ others.some((pointer) => bothChannelsAddress(oracle, bodyPointer(resolved, step.stepId, pointer)))));
152
156
  if (!witnessed) {
153
157
  return verdict(rule, false, `no oracle addresses operation ${operation.operationId}'s success indicator beside another roled pointer at one step, in both channels`);
154
158
  }
@@ -169,14 +173,15 @@ export function wholeBodySatisfaction(contract, context = contextOf(contract)) {
169
173
  if (operations.length === 0)
170
174
  return verdict(rule, false, NO_OPERATION_WITNESS);
171
175
  let sites = 0;
172
- for (const operation of operations) {
173
- const required = [...new Set(operation.responseDescriptor.requiredKeys)];
176
+ for (const resolved of operations) {
177
+ const { operation, descriptor } = resolved;
178
+ const required = [...new Set(descriptor.requiredKeys)];
174
179
  if (required.length <= 1)
175
180
  continue;
176
181
  sites += 1;
177
182
  const witnessed = index
178
183
  .stepsUsing(operation.operationId)
179
- .some((step) => oracles.some((oracle) => required.every((key) => bothChannelsAddress(oracle, keyPointer(step.stepId, key)))));
184
+ .some((step) => oracles.some((oracle) => required.every((key) => bothChannelsAddress(oracle, keyPointer(resolved, step.stepId, key)))));
180
185
  if (!witnessed) {
181
186
  return verdict(rule, false, `no oracle covers every required response key of operation ${operation.operationId} at one addressed step, in both channels`);
182
187
  }
@@ -185,19 +190,19 @@ export function wholeBodySatisfaction(contract, context = contextOf(contract)) {
185
190
  ? NO_RELEVANT_SITE
186
191
  : 'every operation declaring more than one required response key has an oracle covering all of them at one step');
187
192
  }
188
- /** The site condition rule 3 relevance reads: a key on any of the four channels. */
189
- const declaresRequestKey = (operation) => TRANSPORT_CHANNELS.some((channel) => {
190
- const shape = operation.requestShape[channel];
191
- return (shape.requiredKeys.length > 0 ||
192
- shape.permittedKeys.length > 0 ||
193
- Object.keys(shape.types).length > 0);
194
- });
195
- /** AD-39's matcher, on any transport channel of one step. */
196
- const bindsTypeViolating = (step) => TRANSPORT_CHANNELS.some((channel) => {
197
- const binding = step.inputBinding[channel];
198
- if (binding === null)
193
+ /** The site condition rule 3 relevance reads: a key on any input channel. */
194
+ const declaresRequestKey = (resolved) => resolved.requestChannels.some(({ shape }) => shape.requiredKeys.length > 0 ||
195
+ shape.permittedKeys.length > 0 ||
196
+ Object.keys(shape.types).length > 0);
197
+ /**
198
+ * AD-39's matcher, on any input channel of one step. The step carries no
199
+ * operation, so the bound channels are read off the binding itself rather than
200
+ * off a channel tuple that would name four channels the binding does not have.
201
+ */
202
+ const bindsTypeViolating = (step) => boundChannelsOf(step.inputBinding).some(({ bound }) => {
203
+ if (bound === null)
199
204
  return false;
200
- return Object.values(binding).some((value) => 'matcher' in value && value.matcher === 'type-violating');
205
+ return Object.values(bound).some((value) => 'matcher' in value && value.matcher === 'type-violating');
201
206
  });
202
207
  /**
203
208
  * Rule 3: for every operation declaring a request key, some step invoking it
@@ -211,8 +216,9 @@ export function malformedInputSatisfaction(contract, context = contextOf(contrac
211
216
  if (operations.length === 0)
212
217
  return verdict(rule, false, NO_OPERATION_WITNESS);
213
218
  let sites = 0;
214
- for (const operation of operations) {
215
- if (!declaresRequestKey(operation))
219
+ for (const resolved of operations) {
220
+ const { operation } = resolved;
221
+ if (!declaresRequestKey(resolved))
216
222
  continue;
217
223
  sites += 1;
218
224
  const witnessed = index
@@ -240,8 +246,9 @@ export function perRecordSatisfaction(contract, context = contextOf(contract)) {
240
246
  if (operations.length === 0)
241
247
  return verdict(rule, false, NO_OPERATION_WITNESS);
242
248
  let sites = 0;
243
- for (const operation of operations) {
244
- const { collectionLocations } = operation.responseDescriptor;
249
+ for (const resolved of operations) {
250
+ const { operation, descriptor } = resolved;
251
+ const { collectionLocations } = descriptor;
245
252
  if (collectionLocations === null) {
246
253
  return verdict(rule, false, `operation ${operation.operationId} declares no collection-location list, so no quantifier can range over a declared collection`);
247
254
  }
@@ -249,7 +256,7 @@ export function perRecordSatisfaction(contract, context = contextOf(contract)) {
249
256
  for (const location of collectionLocations) {
250
257
  sites += 1;
251
258
  const witnessed = steps.some((step) => {
252
- const collection = bodyPointer(step.stepId, location.pointer);
259
+ const collection = bodyPointer(resolved, step.stepId, location.pointer);
253
260
  return oracles.some((oracle) => oracle.nodes.some((node) => node.collection === collection));
254
261
  });
255
262
  if (!witnessed) {
@@ -289,7 +296,13 @@ export function siblingCrossCheckSatisfaction(contract, context = contextOf(cont
289
296
  for (const group of groups.parameters) {
290
297
  sites += 1;
291
298
  const members = [...new Set(group)];
292
- const witnessed = oracles.some((oracle) => members.filter((parameter) => contract.interactionPlan.some((step) => TRANSPORT_CHANNELS.some((channel) => bothChannelsAddress(oracle, parameterPointer(step.stepId, channel, parameter))))).length >= SIBLING_GROUP_MINIMUM);
299
+ const witnessed = oracles.some((oracle) => members.filter((parameter) => contract.interactionPlan.some((step) =>
300
+ // Building candidate pointers, not reading a declared
301
+ // shape: `groups.parameters` carries no operation, so
302
+ // every input channel is tried and whichever one the
303
+ // oracle actually addresses is the match. It looks
304
+ // identical to `declaresRequestKey` above and is not.
305
+ INPUT_CHANNELS.some((channel) => bothChannelsAddress(oracle, parameterPointer(step.stepId, channel, parameter))))).length >= SIBLING_GROUP_MINIMUM);
293
306
  if (!witnessed) {
294
307
  return verdict(rule, false, `no oracle addresses two members of the parameter sibling group ${members.join(' and ')} in both channels`);
295
308
  }
@@ -330,8 +343,9 @@ export function omissionAndCompletenessSatisfaction(contract, context = contextO
330
343
  if (operations.length === 0)
331
344
  return verdict(rule, false, NO_OPERATION_WITNESS);
332
345
  let sites = 0;
333
- for (const operation of operations) {
334
- const { collectionLocations } = operation.responseDescriptor;
346
+ for (const resolved of operations) {
347
+ const { operation, descriptor } = resolved;
348
+ const { collectionLocations } = descriptor;
335
349
  if (collectionLocations === null) {
336
350
  return verdict(rule, false, `operation ${operation.operationId} declares no collection-location list, so no location can be reconciled against a reference set`);
337
351
  }
@@ -342,7 +356,7 @@ export function omissionAndCompletenessSatisfaction(contract, context = contextO
342
356
  continue;
343
357
  sites += 1;
344
358
  const witnessed = steps.some((step) => {
345
- const collection = bodyPointer(step.stepId, location.pointer);
359
+ const collection = bodyPointer(resolved, step.stepId, location.pointer);
346
360
  return oracles.some((oracle) => oracle.nodes.some((node) => reconciles(node, collection, location, referenceSet)));
347
361
  });
348
362
  if (!witnessed) {
@@ -355,17 +369,28 @@ export function omissionAndCompletenessSatisfaction(contract, context = contextO
355
369
  : 'every collection location naming a reference set is reconciled against it in the declared form');
356
370
  }
357
371
  /** AD-39: a read-back step names the write in its temporal clause and changes no state itself. */
358
- const readBackStepsFor = (contract, index, writeStepId) => contract.interactionPlan.filter((step) => {
372
+ const readBackStepsFor = (contract, context, writeStepId) => contract.interactionPlan.flatMap((step) => {
359
373
  if (step.stepId === writeStepId || step.after !== writeStepId)
360
- return false;
361
- const operation = index.operationOf(step.operationId);
362
- return operation !== undefined && !operation.stateChangeMarker;
374
+ return [];
375
+ const operation = anyOperationOf(context.index, step.operationId);
376
+ if (operation === undefined || operation.stateChangeMarker)
377
+ return [];
378
+ // The index and the resolver both read `permittedInterfaces`, so the
379
+ // index answers with the very object the resolver carries. Matching on
380
+ // identity leaves the index's duplicate-identifier rule as the one
381
+ // thing deciding which operations resolve.
382
+ return context.operations
383
+ .filter((resolved) => resolved.operation === operation)
384
+ .map((resolved) => ({ step, resolved }));
363
385
  });
364
386
  /** One node holding a pointer into each side of the read-back relation. */
365
- const relates = (node, writeStepId, readStepId) => {
387
+ const relates = (node, writeStepId, readBack) => {
366
388
  const pointers = definedPointers(node);
367
- return (someAddresses(pointers, `${stepRoot(writeStepId)}/call-inputs`) &&
368
- someAddresses(pointers, `${stepRoot(readStepId)}/response-body`));
389
+ return (
390
+ // `call-inputs` is the same segment for every interface kind, so the
391
+ // write side takes no root from its operation.
392
+ someAddresses(pointers, `${stepRoot(writeStepId)}/call-inputs`) &&
393
+ someAddresses(pointers, `${stepRoot(readBack.step.stepId)}${readBack.resolved.descriptorRoot}`));
369
394
  };
370
395
  /**
371
396
  * Rule 7: for every operation declaring `stateChangeMarker: true`, some check
@@ -380,13 +405,13 @@ export function stateChangeReadBackSatisfaction(contract, context = contextOf(co
380
405
  if (operations.length === 0)
381
406
  return verdict(rule, false, NO_OPERATION_WITNESS);
382
407
  let sites = 0;
383
- for (const operation of operations) {
408
+ for (const { operation } of operations) {
384
409
  if (!operation.stateChangeMarker)
385
410
  continue;
386
411
  sites += 1;
387
412
  const witnessed = index
388
413
  .stepsUsing(operation.operationId)
389
- .some((writeStep) => readBackStepsFor(contract, index, writeStep.stepId).some((readStep) => oracles.some((oracle) => oracle.nodes.some((node) => relates(node, writeStep.stepId, readStep.stepId)))));
414
+ .some((writeStep) => readBackStepsFor(contract, context, writeStep.stepId).some((readBack) => oracles.some((oracle) => oracle.nodes.some((node) => relates(node, writeStep.stepId, readBack)))));
390
415
  if (!witnessed) {
391
416
  return verdict(rule, false, `no check relates operation ${operation.operationId}'s call inputs to the response body of a later step that changes no state`);
392
417
  }
@@ -1,16 +1,95 @@
1
1
  /**
2
- * What an operation declares it accepts, as predicates over `RequestShape`.
2
+ * What an operation declares it accepts, as predicates over a request shape,
3
+ * plus the resolution that lets a caller read one without knowing its kind.
3
4
  *
4
5
  * At the `core/` root because `core/schemas/` holds Zod definitions only, and
5
6
  * because AD-10's exemption is read by two modules: the compile check that
6
7
  * enforces it and the reducer that records it.
7
8
  */
8
- import type { Operation } from './schemas/interface.ts';
9
+ import type { AnyOperation, CommandOperation, Operation } from './schemas/interface.ts';
10
+ import type { BindingChannel, InputBinding } from './schemas/plan.ts';
11
+ import { type EvidenceChannelName, type InputChannelName } from './schemas/pointer.ts';
12
+ import type { KeyedShapeDescriptor } from './schemas/primitives.ts';
13
+ /**
14
+ * Which output channel the operation's one response descriptor describes.
15
+ *
16
+ * AD-19 gives every operation exactly one descriptor, and the channel that
17
+ * descriptor describes is what makes its keys addressable. For an interface
18
+ * that speaks HTTP that channel is the response body; a command operation
19
+ * declares its own. One rule, three roots: every consumer that used to hard-code
20
+ * `response-body` asks this instead.
21
+ */
22
+ export declare const descriptorChannelOf: (operation: AnyOperation) => EvidenceChannelName;
23
+ /**
24
+ * Which artifact the descriptor describes, or `null` when it describes a
25
+ * stream. An artifact pointer descends through the descriptor only when it
26
+ * names this one; every other declared artifact is known to exist and declares
27
+ * no structure.
28
+ */
29
+ export declare const descriptorArtifactOf: (operation: AnyOperation) => string | null;
30
+ /**
31
+ * Whether an evidence target addresses the channel this operation's response
32
+ * descriptor describes.
33
+ *
34
+ * The channel name alone is not the answer on the artifact channel: an
35
+ * operation may declare several files while its one descriptor describes one of
36
+ * them, so a pointer at a different file names a channel with no declared
37
+ * structure. Every consumer asks this rather than comparing the channel itself,
38
+ * because comparing only the channel was a defect at three separate sites and a
39
+ * fourth site would have made the same mistake for the same reason.
40
+ */
41
+ export declare const targetsDescribedChannel: (operation: AnyOperation, target: {
42
+ readonly channel: EvidenceChannelName;
43
+ readonly artifactId: string | null;
44
+ }) => boolean;
45
+ /** Every artifact identifier the operation declares it writes. */
46
+ export declare const declaredArtifactsOf: (operation: AnyOperation) => readonly string[];
47
+ /** One declared input channel with the shape it declares, already paired. */
48
+ export type RequestChannel = {
49
+ readonly channel: InputChannelName;
50
+ readonly shape: KeyedShapeDescriptor;
51
+ };
52
+ /**
53
+ * Which operation shape this is. Reads `invocation` rather than a kind field,
54
+ * because an operation does not carry its interface's kind; the two shapes are
55
+ * distinguished by a required field only one of them declares.
56
+ */
57
+ export declare const isCommandOperation: (operation: AnyOperation) => operation is CommandOperation;
58
+ /** The same question the other way round, for the callers that filter. */
59
+ export declare const isApiOperation: (operation: AnyOperation) => operation is Operation;
60
+ /**
61
+ * The operation's input channels paired with the shapes they declare.
62
+ *
63
+ * Callers take the pairs rather than a channel list they then index the
64
+ * request shape with. Indexing is what breaks under the operation union:
65
+ * TypeScript cannot prove that a channel name drawn from one kind's tuple is a
66
+ * key of the other kind's shape, and both ways around that are casts. Pairing
67
+ * dereferences the union once, here.
68
+ */
69
+ export declare function requestChannelsOf(operation: AnyOperation): readonly RequestChannel[];
70
+ /** The shape one named channel declares, or `undefined` off this kind. */
71
+ export declare function requestShapeOf(operation: AnyOperation, channel: InputChannelName): KeyedShapeDescriptor | undefined;
72
+ /** The channel names an operation of this kind may declare inputs on. */
73
+ export declare const inputChannelsOf: (operation: AnyOperation) => readonly InputChannelName[];
74
+ /**
75
+ * A step's bound channels paired with what each binds, for the same reason
76
+ * `requestChannelsOf` pairs: `InputBinding` is a union and a channel name
77
+ * drawn from one branch is not a key of the other.
78
+ *
79
+ * Ordered by `INPUT_CHANNELS` rather than by the parsed object's own key
80
+ * order, so which binding a check reports never depends on how the document
81
+ * happened to be written.
82
+ */
83
+ export declare function boundChannelsOf(binding: InputBinding): readonly {
84
+ readonly channel: InputChannelName;
85
+ readonly bound: BindingChannel;
86
+ }[];
9
87
  /**
10
88
  * AD-10's exemption predicate. Any of a channel's three lists naming a key
11
89
  * counts: a permitted-only or types-only channel is still a surface a witness
12
90
  * can vary.
13
91
  */
14
- export declare function declaresNoRequestKeys(operation: Operation): boolean;
92
+ export declare function declaresNoRequestKeys(operation: AnyOperation): boolean;
15
93
  /** Whether no channel declares a required key. */
16
- export declare function declaresNoRequiredKeys(operation: Operation): boolean;
94
+ export declare function declaresNoRequiredKeys(operation: AnyOperation): boolean;
95
+ export type { AnyOperation, CommandOperation, Operation };
@@ -1,18 +1,115 @@
1
- import { TRANSPORT_CHANNELS } from './schemas/pointer.js';
1
+ import { COMMAND_CHANNELS, INPUT_CHANNELS, TRANSPORT_CHANNELS, } from './schemas/pointer.js';
2
+ /**
3
+ * Which output channel the operation's one response descriptor describes.
4
+ *
5
+ * AD-19 gives every operation exactly one descriptor, and the channel that
6
+ * descriptor describes is what makes its keys addressable. For an interface
7
+ * that speaks HTTP that channel is the response body; a command operation
8
+ * declares its own. One rule, three roots: every consumer that used to hard-code
9
+ * `response-body` asks this instead.
10
+ */
11
+ export const descriptorChannelOf = (operation) => {
12
+ if (!isCommandOperation(operation))
13
+ return 'response-body';
14
+ const { descriptorChannel } = operation;
15
+ return descriptorChannel.kind === 'stream'
16
+ ? descriptorChannel.channel
17
+ : 'artifact';
18
+ };
19
+ /**
20
+ * Which artifact the descriptor describes, or `null` when it describes a
21
+ * stream. An artifact pointer descends through the descriptor only when it
22
+ * names this one; every other declared artifact is known to exist and declares
23
+ * no structure.
24
+ */
25
+ export const descriptorArtifactOf = (operation) => {
26
+ if (!isCommandOperation(operation))
27
+ return null;
28
+ const { descriptorChannel } = operation;
29
+ return descriptorChannel.kind === 'artifact'
30
+ ? descriptorChannel.artifactId
31
+ : null;
32
+ };
33
+ /**
34
+ * Whether an evidence target addresses the channel this operation's response
35
+ * descriptor describes.
36
+ *
37
+ * The channel name alone is not the answer on the artifact channel: an
38
+ * operation may declare several files while its one descriptor describes one of
39
+ * them, so a pointer at a different file names a channel with no declared
40
+ * structure. Every consumer asks this rather than comparing the channel itself,
41
+ * because comparing only the channel was a defect at three separate sites and a
42
+ * fourth site would have made the same mistake for the same reason.
43
+ */
44
+ export const targetsDescribedChannel = (operation, target) => {
45
+ if (target.channel !== descriptorChannelOf(operation))
46
+ return false;
47
+ if (target.channel !== 'artifact')
48
+ return true;
49
+ return target.artifactId === descriptorArtifactOf(operation);
50
+ };
51
+ /** Every artifact identifier the operation declares it writes. */
52
+ export const declaredArtifactsOf = (operation) => isCommandOperation(operation) ? operation.artifacts : [];
53
+ /**
54
+ * Which operation shape this is. Reads `invocation` rather than a kind field,
55
+ * because an operation does not carry its interface's kind; the two shapes are
56
+ * distinguished by a required field only one of them declares.
57
+ */
58
+ export const isCommandOperation = (operation) => 'invocation' in operation;
59
+ /** The same question the other way round, for the callers that filter. */
60
+ export const isApiOperation = (operation) => !isCommandOperation(operation);
61
+ /**
62
+ * The operation's input channels paired with the shapes they declare.
63
+ *
64
+ * Callers take the pairs rather than a channel list they then index the
65
+ * request shape with. Indexing is what breaks under the operation union:
66
+ * TypeScript cannot prove that a channel name drawn from one kind's tuple is a
67
+ * key of the other kind's shape, and both ways around that are casts. Pairing
68
+ * dereferences the union once, here.
69
+ */
70
+ export function requestChannelsOf(operation) {
71
+ if (isCommandOperation(operation)) {
72
+ return COMMAND_CHANNELS.map((channel) => ({
73
+ channel,
74
+ shape: operation.requestShape[channel],
75
+ }));
76
+ }
77
+ return TRANSPORT_CHANNELS.map((channel) => ({
78
+ channel,
79
+ shape: operation.requestShape[channel],
80
+ }));
81
+ }
82
+ /** The shape one named channel declares, or `undefined` off this kind. */
83
+ export function requestShapeOf(operation, channel) {
84
+ return requestChannelsOf(operation).find((entry) => entry.channel === channel)
85
+ ?.shape;
86
+ }
87
+ /** The channel names an operation of this kind may declare inputs on. */
88
+ export const inputChannelsOf = (operation) => isCommandOperation(operation) ? COMMAND_CHANNELS : TRANSPORT_CHANNELS;
89
+ /**
90
+ * A step's bound channels paired with what each binds, for the same reason
91
+ * `requestChannelsOf` pairs: `InputBinding` is a union and a channel name
92
+ * drawn from one branch is not a key of the other.
93
+ *
94
+ * Ordered by `INPUT_CHANNELS` rather than by the parsed object's own key
95
+ * order, so which binding a check reports never depends on how the document
96
+ * happened to be written.
97
+ */
98
+ export function boundChannelsOf(binding) {
99
+ const present = binding;
100
+ return INPUT_CHANNELS.filter((channel) => channel in binding).map((channel) => ({ channel, bound: present[channel] ?? null }));
101
+ }
2
102
  /**
3
103
  * AD-10's exemption predicate. Any of a channel's three lists naming a key
4
104
  * counts: a permitted-only or types-only channel is still a surface a witness
5
105
  * can vary.
6
106
  */
7
107
  export function declaresNoRequestKeys(operation) {
8
- return TRANSPORT_CHANNELS.every((channel) => {
9
- const shape = operation.requestShape[channel];
10
- return (shape.requiredKeys.length === 0 &&
11
- shape.permittedKeys.length === 0 &&
12
- Object.keys(shape.types).length === 0);
13
- });
108
+ return requestChannelsOf(operation).every(({ shape }) => shape.requiredKeys.length === 0 &&
109
+ shape.permittedKeys.length === 0 &&
110
+ Object.keys(shape.types).length === 0);
14
111
  }
15
112
  /** Whether no channel declares a required key. */
16
113
  export function declaresNoRequiredKeys(operation) {
17
- return TRANSPORT_CHANNELS.every((channel) => operation.requestShape[channel].requiredKeys.length === 0);
114
+ return requestChannelsOf(operation).every(({ shape }) => shape.requiredKeys.length === 0);
18
115
  }
@@ -9,7 +9,7 @@ import type { EvalContract } from '../schemas/eval-contract.ts';
9
9
  import type { JsonValue } from '../schemas/primitives.ts';
10
10
  import type { Observation } from '../schemas/sealed-run-record.ts';
11
11
  import { type EvidenceTarget, type PlanIndex } from '../seal/plan-index.ts';
12
- import type { PointerDenotesCollection, ResolveOperand } from './resolution.ts';
12
+ import type { PointerDenotesCollection, ReferenceSetKeys, ResolveOperand } from './resolution.ts';
13
13
  import { type ResolvedValue } from './resolved-value.ts';
14
14
  /**
15
15
  * A canonical RFC 6901 array-index token: no leading zero except "0" itself,
@@ -26,7 +26,7 @@ export declare const ARRAY_INDEX_PATTERN: RegExp;
26
26
  * falls through to `Object.prototype`. Any miss, type mismatch, or tail
27
27
  * running past a scalar collapses to `ABSENT` uniformly (AD-26).
28
28
  */
29
- export declare function walkTail(root: JsonValue, tail: readonly string[]): ResolvedValue;
29
+ export declare function walkTail(root: ResolvedValue, tail: readonly string[]): ResolvedValue;
30
30
  /**
31
31
  * Decodes `BoundElementPointer`'s tail. "@/" is the shortest legal pointer
32
32
  * form and denotes the element itself. `decodeTail` special-cases only a
@@ -34,16 +34,7 @@ export declare function walkTail(root: JsonValue, tail: readonly string[]): Reso
34
34
  * calling it.
35
35
  */
36
36
  export declare function decodeBoundElementTail(pointer: string): readonly string[];
37
- /**
38
- * Selects the channel `target` names off one `Observation`. `stdout`/`stderr`
39
- * can carry a tail even though they're bare strings; `walkTail` already
40
- * resolves any non-empty tail against a string to `ABSENT`, so no special
41
- * case is needed here.
42
- *
43
- * Exported so `core/score/bindings.ts` reads a captured pointer's channel off
44
- * an observation through this one spelling, keeping the switch in one place.
45
- */
46
- export declare function channelRoot(observation: Observation, target: EvidenceTarget): JsonValue;
37
+ export declare function channelRoot(observation: Observation, target: EvidenceTarget): ResolvedValue;
47
38
  /**
48
39
  * The `ResolveOperand`. `stepObservations` holds one already-selected
49
40
  * `Observation` per interaction step; `referenceSets` mirrors the contract's
@@ -52,6 +43,12 @@ export declare function channelRoot(observation: Observation, target: EvidenceTa
52
43
  * prototype-chain gotcha `walkTail` guards against.
53
44
  */
54
45
  export declare function makeResolveOperand(stepObservations: Readonly<Record<string, Observation>>, referenceSets: Readonly<Record<string, JsonValue[]>>): ResolveOperand;
46
+ /**
47
+ * The `keys` a contract's reference sets declare. Built from the same
48
+ * `contract.referenceSets` the members map handed to `makeResolveOperand` is
49
+ * built from, so a set that resolves to members always has its keys here.
50
+ */
51
+ export declare function referenceSetKeysOf(contract: EvalContract): ReferenceSetKeys;
55
52
  /**
56
53
  * Only `response-body` can ever answer `true` (AD-19: `collectionLocations`
57
54
  * is the only declared-collection surface, scoped to the body alone). The
@@ -5,7 +5,8 @@
5
5
  * bound-element `@/` form, and walk it into an already-selected
6
6
  * `Observation`.
7
7
  */
8
- import { buildPlanIndex, decodeTail, parseEvidenceTarget, } from '../seal/plan-index.js';
8
+ import { targetsDescribedChannel } from '../declared-inputs.js';
9
+ import { anyOperationOf, buildPlanIndex, decodeTail, parseEvidenceTarget, } from '../seal/plan-index.js';
9
10
  import { ABSENT } from './resolved-value.js';
10
11
  /**
11
12
  * A canonical RFC 6901 array-index token: no leading zero except "0" itself,
@@ -23,6 +24,12 @@ export const ARRAY_INDEX_PATTERN = /^(?:0|[1-9][0-9]*)$/;
23
24
  * running past a scalar collapses to `ABSENT` uniformly (AD-26).
24
25
  */
25
26
  export function walkTail(root, tail) {
27
+ // A root that did not resolve stays unresolved however short the tail is.
28
+ // The bare-pointer case is the one that matters: without this, an absent
29
+ // channel with no tail returned its own root and a caller could not tell it
30
+ // from a resolved value.
31
+ if (root === ABSENT)
32
+ return ABSENT;
26
33
  let current = root;
27
34
  for (const token of tail) {
28
35
  if (current === null || typeof current !== 'object')
@@ -53,14 +60,29 @@ export function decodeBoundElementTail(pointer) {
53
60
  return tailSource === '/' ? [] : decodeTail(tailSource);
54
61
  }
55
62
  /**
56
- * Selects the channel `target` names off one `Observation`. `stdout`/`stderr`
57
- * can carry a tail even though they're bare strings; `walkTail` already
58
- * resolves any non-empty tail against a string to `ABSENT`, so no special
59
- * case is needed here.
63
+ * Selects the channel `target` names off one `Observation`.
64
+ *
65
+ * `stdout`, `stderr`, and each written artifact are tagged, so a channel a
66
+ * harness captured as JSON resolves to the value and a tail walks into it,
67
+ * while one captured as text resolves to the string and a tail over it
68
+ * resolves `ABSENT`. That is what makes a command operation's nominated output
69
+ * channel resolvable at score time on the same terms as a response body.
60
70
  *
61
71
  * Exported so `core/score/bindings.ts` reads a captured pointer's channel off
62
72
  * an observation through this one spelling, keeping the switch in one place.
63
73
  */
74
+ /**
75
+ * The value a tagged observed channel resolves to.
76
+ *
77
+ * A JSON body resolves to the value itself, so a tail walks into it; text
78
+ * resolves to the string, so a tail over it resolves `ABSENT` through
79
+ * `walkTail`, which is the truthful answer for output nothing parsed. An absent
80
+ * channel resolves to `ABSENT` and never to `null`: the tag exists precisely to
81
+ * tell "nothing was observed here" from "a value that was JSON null", and
82
+ * collapsing the two would make `existence` answer true for a file the run
83
+ * never wrote, since AD-26 counts `null` as present.
84
+ */
85
+ const observedValue = (body) => body.kind === 'absent' ? ABSENT : body.value;
64
86
  export function channelRoot(observation, target) {
65
87
  switch (target.channel) {
66
88
  case 'response-body':
@@ -70,9 +92,9 @@ export function channelRoot(observation, target) {
70
92
  case 'response-status':
71
93
  return observation.responseStatus;
72
94
  case 'stdout':
73
- return observation.stdout;
95
+ return observedValue(observation.stdout);
74
96
  case 'stderr':
75
- return observation.stderr;
97
+ return observedValue(observation.stderr);
76
98
  case 'exit-code':
77
99
  return observation.exitCode;
78
100
  case 'call-inputs': {
@@ -84,6 +106,27 @@ export function channelRoot(observation, target) {
84
106
  }
85
107
  return observation.callInputs[transportChannel];
86
108
  }
109
+ case 'artifact': {
110
+ const { artifactId } = target;
111
+ if (artifactId === null) {
112
+ // parseEvidenceTarget sets artifactId exactly when the channel
113
+ // is 'artifact', so this throw should never fire.
114
+ throw new TypeError('artifact evidence target names no artifact');
115
+ }
116
+ // `Object.hasOwn` for the reason this module's header gives for
117
+ // `stepObservations` and `referenceSets`: `Identifier` admits
118
+ // `constructor`, `toString` and `valueOf`, so a bare index on an
119
+ // unwritten file of one of those names resolves to an inherited
120
+ // function rather than to nothing.
121
+ //
122
+ // A file the run did not write is absent evidence, not a `null`
123
+ // value. `null` would read as present under AD-26 and invert every
124
+ // oracle asserting the file exists or does not.
125
+ if (!Object.hasOwn(observation.artifacts, artifactId))
126
+ return ABSENT;
127
+ const written = observation.artifacts[artifactId];
128
+ return written === undefined ? ABSENT : observedValue(written);
129
+ }
87
130
  }
88
131
  }
89
132
  /**
@@ -118,6 +161,17 @@ export function makeResolveOperand(stepObservations, referenceSets) {
118
161
  return walkTail(channelRoot(observation, target), target.tail);
119
162
  };
120
163
  }
164
+ /**
165
+ * The `keys` a contract's reference sets declare. Built from the same
166
+ * `contract.referenceSets` the members map handed to `makeResolveOperand` is
167
+ * built from, so a set that resolves to members always has its keys here.
168
+ */
169
+ export function referenceSetKeysOf(contract) {
170
+ return Object.fromEntries(Object.entries(contract.referenceSets ?? {}).map(([id, set]) => [
171
+ id,
172
+ set.keys,
173
+ ]));
174
+ }
121
175
  function tokensEqual(a, b) {
122
176
  return a.length === b.length && a.every((token, index) => token === b[index]);
123
177
  }
@@ -140,14 +194,20 @@ export function makePointerDenotesCollection(contract, providedIndex) {
140
194
  if (pointer.startsWith('@'))
141
195
  return false;
142
196
  const target = parseEvidenceTarget(pointer);
143
- if (target.channel !== 'response-body')
144
- return false;
145
197
  const step = getIndex().stepOf(target.stepId);
146
198
  if (step === undefined)
147
199
  return false;
148
- const operation = getIndex().operationOf(step.operationId);
200
+ const operation = anyOperationOf(getIndex(), step.operationId);
149
201
  if (operation === undefined)
150
202
  return false;
203
+ // The channel is tested against the operation's own descriptor rather
204
+ // than against `response-body`. AD-4's empty-collection resolution
205
+ // applies to whichever channel an operation says carries its declared
206
+ // collections, and hard-coding the body left it inapplicable to every
207
+ // command contract: a quantifier over an empty declared collection
208
+ // resolved `false` instead of `insufficient-evidence`.
209
+ if (!targetsDescribedChannel(operation, target))
210
+ return false;
151
211
  const { collectionLocations } = operation.responseDescriptor;
152
212
  if (collectionLocations === null)
153
213
  return false;