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
@@ -4,28 +4,65 @@
4
4
  * execute it, and that its leg identifiers are distinct from each other and
5
5
  * from every interaction-plan step.
6
6
  *
7
- * No new AD-5 code is minted. The registry is closed at twenty-three 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
7
+ * No new AD-5 code is minted here. `check:ad5-registry` pins the registry
8
+ * against the spine's own table, so each defect below takes the code that
9
+ * already names it. Two rows stretch that reading: a leg-id equality
10
10
  * and a leg-id/step-id collision are identifier collisions, and
11
11
  * `malformed-operator-expression` is the closest available code because the
12
12
  * relation is illegal in its position once its operands cannot be told apart.
13
13
  */
14
- import { declaresNoRequestKeys } from '../declared-inputs.js';
14
+ import { declaresNoRequestKeys, isCommandOperation, requestChannelsOf, } from '../declared-inputs.js';
15
15
  import { StructuralFailure } from '../failure-codes.js';
16
- import { TRANSPORT_CHANNELS, } from '../schemas/pointer.js';
16
+ import { operationsOf } from '../schemas/interface.js';
17
+ import { COMMAND_WITNESS_CHANNELS, } from '../schemas/sensitivity-witness.js';
17
18
  import { parseEvidenceTarget } from '../seal/plan-index.js';
18
19
  export { declaresNoRequestKeys };
20
+ /** Which spelling this leg uses. `stdin` is the command shape's own key. */
21
+ const isCommandWitnessInputs = (inputs) => 'stdin' in inputs;
19
22
  /**
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.
23
+ * The transport spelling, for the consumers that can only send one: the probe
24
+ * port carries a method, a path template, and the four transport channels.
24
25
  */
25
- export function suppliedKeys(inputs, channel) {
26
- if (channel !== 'body')
27
- return Object.keys(inputs[channel]);
28
- const { body } = inputs;
26
+ export const isApiWitnessInputs = (inputs) => 'body' in inputs;
27
+ /**
28
+ * Whether the leg supplies this channel as an undifferentiated stream.
29
+ *
30
+ * Standard input written as text is one such stream. A `KeyedShapeDescriptor`
31
+ * can say what keys a channel carries and cannot say "one opaque stream", so a
32
+ * command that reads a prompt is declared by naming the one thing the stream
33
+ * carries, and the leg supplies that thing as the text it actually is.
34
+ */
35
+ export function suppliesOpaquely(inputs, channel) {
36
+ return (isCommandWitnessInputs(inputs) &&
37
+ channel === 'stdin' &&
38
+ inputs.stdin.kind === 'text');
39
+ }
40
+ /**
41
+ * What an opaque stream fills, and when it cannot say.
42
+ *
43
+ * A text leg supplies the channel's ONE declared key: the stream is the value
44
+ * of the single thing the operation says it reads. Where the channel declares
45
+ * no required key there is nothing for the text to be, and where it declares
46
+ * more than one there is no way to say which the text fills, so both are the
47
+ * authoring fault this reports rather than a comparison that quietly abstains.
48
+ *
49
+ * That is what a keyed `stdin` means for a command that reads text, and it is
50
+ * the reason the declaration stays keyed: an operation that genuinely parses a
51
+ * structured document off standard input declares several keys and supplies
52
+ * them through the leg's `json` arm, and the same descriptor serves both.
53
+ */
54
+ function checkOpaqueStream(shape, channel, operationId, owner, artifactPath) {
55
+ if (shape.requiredKeys.length === 1)
56
+ return;
57
+ throw new StructuralFailure('undeclared-mandatory-input', artifactPath, `${owner} supplies its ${channel} channel as text, which fills the one key the channel declares, but operation "${operationId}" declares ${shape.requiredKeys.length === 0 ? 'no required key there, so the text fills nothing' : `${shape.requiredKeys.length} required keys there, so nothing says which the text fills`} (AD-10, AD-19)`);
58
+ }
59
+ /**
60
+ * The keys a tagged body value supplies. A body that is absent, or JSON that
61
+ * is not an object, supplies no keys: the channel is declared as a keyed
62
+ * shape, so such a leg omits every required key rather than being exempt from
63
+ * the comparison.
64
+ */
65
+ function bodyKeys(body) {
29
66
  if (body.kind !== 'json')
30
67
  return [];
31
68
  const { value } = body;
@@ -33,6 +70,37 @@ export function suppliedKeys(inputs, channel) {
33
70
  return [];
34
71
  return Object.keys(value);
35
72
  }
73
+ /**
74
+ * The keys one set of witness inputs supplies on one channel, or none where
75
+ * the leg's spelling has no such channel.
76
+ *
77
+ * `body` and `stdin` are the two channels a leg supplies as a tagged value
78
+ * rather than as a key map, because a leg has to tell an absent value from one
79
+ * carrying JSON null. This function is the bridge between that spelling and
80
+ * the keyed shape the request declares.
81
+ */
82
+ export function suppliedKeys(inputs, channel) {
83
+ if (isCommandWitnessInputs(inputs)) {
84
+ if (channel === 'stdin')
85
+ return bodyKeys(inputs.stdin);
86
+ if (channel === 'argument' || channel === 'option')
87
+ return Object.keys(inputs[channel]);
88
+ if (channel === 'environment')
89
+ return Object.keys(inputs.environment);
90
+ return [];
91
+ }
92
+ if (channel === 'body')
93
+ return bodyKeys(inputs.body);
94
+ if (channel === 'path' || channel === 'query')
95
+ return Object.keys(inputs[channel]);
96
+ if (channel === 'header')
97
+ return Object.keys(inputs.header);
98
+ return [];
99
+ }
100
+ /** One channel's supplied value, for the differential comparison. */
101
+ export function suppliedValue(inputs, channel) {
102
+ return inputs[channel];
103
+ }
36
104
  /**
37
105
  * Every channel of one set of witness inputs against the operation that will
38
106
  * receive them. All four, not only the differential channel: `planPreflight`
@@ -59,8 +127,11 @@ export function suppliedKeys(inputs, channel) {
59
127
  * check, and what pre-flight probes is a fixture.
60
128
  */
61
129
  export function checkInputsAgainstShape(inputs, operation, owner, artifactPath) {
62
- for (const channel of TRANSPORT_CHANNELS) {
63
- const shape = operation.requestShape[channel];
130
+ for (const { channel, shape } of requestChannelsOf(operation)) {
131
+ if (suppliesOpaquely(inputs, channel)) {
132
+ checkOpaqueStream(shape, channel, operation.operationId, owner, artifactPath);
133
+ continue;
134
+ }
64
135
  const supplied = suppliedKeys(inputs, channel);
65
136
  for (const key of shape.requiredKeys) {
66
137
  if (supplied.includes(key))
@@ -78,7 +149,7 @@ export function checkInputsAgainstShape(inputs, operation, owner, artifactPath)
78
149
  const operationPath = (interfaceIndex, operationIndex) => `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}]`;
79
150
  function forEachWitness(contract, visit) {
80
151
  contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
81
- iface.operations.forEach((operation, operationIndex) => {
152
+ operationsOf(iface).forEach((operation, operationIndex) => {
82
153
  const witness = operation.sensitivityWitness;
83
154
  if (witness === null)
84
155
  return;
@@ -94,13 +165,13 @@ function forEachWitness(contract, visit) {
94
165
  */
95
166
  export function checkSensitivityWitnessDeclared(contract) {
96
167
  contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
97
- iface.operations.forEach((operation, operationIndex) => {
168
+ operationsOf(iface).forEach((operation, operationIndex) => {
98
169
  const path = operationPath(interfaceIndex, operationIndex);
99
170
  const witness = operation.sensitivityWitness;
100
171
  if (witness === null) {
101
172
  if (declaresNoRequestKeys(operation))
102
173
  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)`);
174
+ 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). The relation is the author's to choose and need not assert the two legs differ: an operation that is insensitive to its declared inputs by design declares a witness whose relation says so, which is a true and checkable claim about it, and gets the weaker guarantee that follows`);
104
175
  }
105
176
  witness.legs.forEach((leg, legIndex) => {
106
177
  checkInputsAgainstShape(leg.inputs, operation, `leg "${leg.legId}"`, `${path}.sensitivityWitness.legs[${legIndex}]`);
@@ -137,8 +208,22 @@ function addressedStepIds(expression) {
137
208
  walk(expression);
138
209
  return found;
139
210
  }
140
- /** the channel AD-10 selects for an operation, by its state-change marker. */
141
- const legalChannels = (operation) => operation.stateChangeMarker ? ['body'] : ['path', 'query'];
211
+ const MUTATING_CHANNELS = ['body'];
212
+ const READ_CHANNELS = ['path', 'query'];
213
+ /**
214
+ * The channels AD-10 admits for an operation's witness.
215
+ *
216
+ * Off an interface that speaks HTTP the state-change marker selects one, since
217
+ * a read carries its identifier in the URL and a write carries it in the body.
218
+ * A command carries its inputs the same way whether or not it changes state,
219
+ * so the marker selects nothing there and all four command channels are
220
+ * admitted. The author picks the one their operation is actually sensitive on.
221
+ */
222
+ const legalChannels = (operation) => {
223
+ if (isCommandOperation(operation))
224
+ return COMMAND_WITNESS_CHANNELS;
225
+ return operation.stateChangeMarker ? MUTATING_CHANNELS : READ_CHANNELS;
226
+ };
142
227
  /**
143
228
  * The shape rules: the differential channel agrees with the state-change
144
229
  * marker, the relation reads both legs and nothing else, and a declared
@@ -158,8 +243,8 @@ export function checkWitnessLegality(contract) {
158
243
  const [first, second] = witness.legs;
159
244
  if (first !== undefined &&
160
245
  second !== undefined &&
161
- JSON.stringify(first.inputs[witness.channel]) ===
162
- JSON.stringify(second.inputs[witness.channel])) {
246
+ JSON.stringify(suppliedValue(first.inputs, witness.channel)) ===
247
+ JSON.stringify(suppliedValue(second.inputs, witness.channel))) {
163
248
  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
249
  }
165
250
  const legIds = witness.legs.map((leg) => leg.legId);
@@ -181,7 +266,9 @@ export function checkWitnessLegality(contract) {
181
266
  if (reset === null)
182
267
  return;
183
268
  const iface = contract.permittedInterfaces.find((candidate) => candidate.logicalId === reset.interfaceId);
184
- const operation = iface?.operations.find((candidate) => candidate.operationId === reset.operationId);
269
+ const operation = iface === undefined
270
+ ? undefined
271
+ : operationsOf(iface).find((candidate) => candidate.operationId === reset.operationId);
185
272
  if (operation === undefined) {
186
273
  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
274
  }
@@ -0,0 +1,2 @@
1
+ import type { EvalContract } from '../schemas/eval-contract.ts';
2
+ export declare function checkStepReferenceReducibility(contract: EvalContract): void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `irreducible-step-reference`: two steps one direction references render to
3
+ * the same derived reference even after full escalation.
4
+ *
5
+ * AD-16 keeps step identifiers out of the sealed brief, so a direction names
6
+ * each step it references by a phrase derived from what the contract declares
7
+ * about it. Where two steps of one operation declare nothing that tells them
8
+ * apart, the escalation ladder runs out and the renderer has no way to name one
9
+ * without naming the other.
10
+ *
11
+ * The check runs the seal-side renderer rather than reimplementing its ladder.
12
+ * A second implementation of a three-rung escalation with a budget would be a
13
+ * second thing to keep in step with the first, and the failure this reports is
14
+ * defined as "what that renderer cannot do" rather than as a rule of its own.
15
+ * That is the same reasoning `checkEvidenceReachability` follows in reusing
16
+ * `evaluatePointerReachability`.
17
+ *
18
+ * Without it a contract passes every other check and then fails inside `seal`,
19
+ * which is the one authoring fault in the tree that reached a caller as a
20
+ * stack trace instead of a code and a path.
21
+ */
22
+ import { StructuralFailure } from '../failure-codes.js';
23
+ import { renderEvidenceReferences } from '../seal/derived-reference.js';
24
+ import { buildPlanIndex } from '../seal/plan-index.js';
25
+ export function checkStepReferenceReducibility(contract) {
26
+ let index;
27
+ for (const oracle of contract.oracles) {
28
+ const { direction } = oracle;
29
+ if (direction === null || direction.evidenceTargets.length === 0)
30
+ continue;
31
+ index ??= buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
32
+ try {
33
+ renderEvidenceReferences(direction.evidenceTargets, index);
34
+ }
35
+ catch (error) {
36
+ if (!(error instanceof StructuralFailure)) {
37
+ // Every other way this renderer fails is a precondition violation
38
+ // on input `compile` has already checked, so it is a fault rather
39
+ // than an authoring fault and propagates unchanged.
40
+ throw error;
41
+ }
42
+ // Re-thrown with the contract-side address. `StructuralFailure`
43
+ // carries the code and the path and folds the detail into its
44
+ // message, so the detail is recovered from the message rather than
45
+ // from a field the class does not expose.
46
+ throw new StructuralFailure(error.code, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets`, error.message.slice(`${error.code} in ${error.artifactPath}: `.length));
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * One declared operation joined to the interface that declares it, plus the
3
+ * two facts the coverage predicates need and neither the operation nor the
4
+ * interface states: where a response descriptor's pointers resolve, and which
5
+ * channels a call's inputs may be keyed by.
6
+ *
7
+ * `relevance.ts` and `satisfaction.ts` each flattened `permittedInterfaces`
8
+ * with `flatMap((declared) => declared.operations)` and dropped the interface
9
+ * one line before every loop that ranges over it, so six of AD-20's seven
10
+ * rules could not see the kind that declared the operation they were grading.
11
+ * This is that flattening, written once and keeping the join.
12
+ *
13
+ * Nothing here throws, which is the promise both predicate modules make in
14
+ * their own headers and which `evaluateCoverage`'s callers rely on. Every
15
+ * field is read off an already-parsed contract, so there is no precondition
16
+ * left to assert.
17
+ */
18
+ import { type RequestChannel } from '../declared-inputs.ts';
19
+ import type { EvalContract } from '../schemas/eval-contract.ts';
20
+ import type { AnyOperation, InterfaceKindName, ResponseDescriptor } from '../schemas/interface.ts';
21
+ import type { InputChannelName } from '../schemas/pointer.ts';
22
+ /**
23
+ * RFC 6901 escaping, `~` before `/`. Lives here rather than beside the
24
+ * pointer-building helpers that use it, because the descriptor root is built
25
+ * here and both spellings have to escape the same way.
26
+ */
27
+ export declare const encodeToken: (token: string) => string;
28
+ export type ResolvedOperation = {
29
+ readonly operation: AnyOperation;
30
+ /** the declaring interface's kind. An operation states no kind of its own. */
31
+ readonly kind: InterfaceKindName;
32
+ /** the declaring interface. Two interfaces may declare one `operationId`, and this is what separates them. */
33
+ readonly logicalId: string;
34
+ readonly descriptor: ResponseDescriptor;
35
+ /**
36
+ * The interaction-rooted segment a descriptor pointer hangs off, which is
37
+ * the channel this operation's own descriptor describes, and on the artifact
38
+ * channel the file as well. `/artifact` alone is not a root any real
39
+ * evidence pointer starts with, since an artifact pointer carries its
40
+ * identifier before its tail, so a root without it matched nothing and made
41
+ * every pointer-building rule answer against a pointer that cannot exist.
42
+ */
43
+ readonly descriptorRoot: string;
44
+ /**
45
+ * The channels a call's inputs may be keyed by. Names only: the two sites
46
+ * that build candidate pointers need the names and never the shapes.
47
+ */
48
+ readonly transportChannels: readonly InputChannelName[];
49
+ /**
50
+ * The same channels already paired with the shapes they declare. Predicates
51
+ * that read a declared shape take these rather than indexing the request
52
+ * shape with a name, because under the operation union TypeScript cannot
53
+ * prove a name drawn from one kind's tuple is a key of the other's shape.
54
+ */
55
+ readonly requestChannels: readonly RequestChannel[];
56
+ };
57
+ /**
58
+ * Every declared operation, in declaration order, each carrying the interface
59
+ * that declares it. Six of AD-20's seven rules range over this list; rule 5 is
60
+ * contract-level and reads the sibling groups instead.
61
+ */
62
+ export declare function resolveOperations(contract: EvalContract): readonly ResolvedOperation[];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * One declared operation joined to the interface that declares it, plus the
3
+ * two facts the coverage predicates need and neither the operation nor the
4
+ * interface states: where a response descriptor's pointers resolve, and which
5
+ * channels a call's inputs may be keyed by.
6
+ *
7
+ * `relevance.ts` and `satisfaction.ts` each flattened `permittedInterfaces`
8
+ * with `flatMap((declared) => declared.operations)` and dropped the interface
9
+ * one line before every loop that ranges over it, so six of AD-20's seven
10
+ * rules could not see the kind that declared the operation they were grading.
11
+ * This is that flattening, written once and keeping the join.
12
+ *
13
+ * Nothing here throws, which is the promise both predicate modules make in
14
+ * their own headers and which `evaluateCoverage`'s callers rely on. Every
15
+ * field is read off an already-parsed contract, so there is no precondition
16
+ * left to assert.
17
+ */
18
+ import { descriptorArtifactOf, descriptorChannelOf, inputChannelsOf, requestChannelsOf, } from '../declared-inputs.js';
19
+ import { operationsOf } from '../schemas/interface.js';
20
+ /**
21
+ * RFC 6901 escaping, `~` before `/`. Lives here rather than beside the
22
+ * pointer-building helpers that use it, because the descriptor root is built
23
+ * here and both spellings have to escape the same way.
24
+ */
25
+ export const encodeToken = (token) => token.replace(/~/g, '~0').replace(/\//g, '~1');
26
+ /**
27
+ * Where a descriptor-relative pointer hangs off an interaction root for this
28
+ * operation. Two segments on the artifact channel, one everywhere else.
29
+ */
30
+ const descriptorRootOf = (operation) => {
31
+ const channel = descriptorChannelOf(operation);
32
+ if (channel !== 'artifact')
33
+ return `/${channel}`;
34
+ const artifactId = descriptorArtifactOf(operation);
35
+ // Unreachable: `descriptorChannelOf` answers `artifact` only for the arm
36
+ // that carries an identifier.
37
+ if (artifactId === null) {
38
+ throw new TypeError(`operation "${operation.operationId}" describes an artifact and names none`);
39
+ }
40
+ return `/${channel}/${encodeToken(artifactId)}`;
41
+ };
42
+ /**
43
+ * Every declared operation, in declaration order, each carrying the interface
44
+ * that declares it. Six of AD-20's seven rules range over this list; rule 5 is
45
+ * contract-level and reads the sibling groups instead.
46
+ */
47
+ export function resolveOperations(contract) {
48
+ return contract.permittedInterfaces.flatMap((declared) => operationsOf(declared).map((operation) => ({
49
+ operation,
50
+ kind: declared.kind,
51
+ logicalId: declared.logicalId,
52
+ descriptor: operation.responseDescriptor,
53
+ descriptorRoot: descriptorRootOf(operation),
54
+ transportChannels: inputChannelsOf(operation),
55
+ requestChannels: requestChannelsOf(operation),
56
+ })));
57
+ }
@@ -32,8 +32,10 @@ export declare function successIndicatorSeparationRelevance(contract: EvalContra
32
32
  */
33
33
  export declare function wholeBodyRelevance(contract: EvalContract): RelevanceVerdict;
34
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
35
+ * Rule 3: some operation declares a request key on any of its own input
36
+ * channels, AD-19's four transport channels off an interface that speaks HTTP
37
+ * and the four command channels off one that does not. The site is the whole
38
+ * channel triple. A key with no
37
39
  * `types` entry has an absent type, a key typed `null` has AD-31's
38
40
  * indeterminate one, and both are relevant, so declaring an input and leaving
39
41
  * it untyped buys no irrelevance.
@@ -1,4 +1,4 @@
1
- import { TRANSPORT_CHANNELS } from '../schemas/pointer.js';
1
+ import { resolveOperations } from './operations.js';
2
2
  import { DISCIPLINE_RULES, relevancePredicateId, } from './rules.js';
3
3
  const verdict = (rule, relevant, reason) => ({
4
4
  rule,
@@ -8,8 +8,6 @@ const verdict = (rule, relevant, reason) => ({
8
8
  });
9
9
  /** `1 collection location`, `2 collection locations`. A reason is read by people. */
10
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
11
  /** A contract declaring no operation leaves six of the rules nothing to read. */
14
12
  export const NO_OPERATION = 'the contract declares no operation, so the declaration this rule reads is absent';
15
13
  /**
@@ -20,11 +18,11 @@ export const NO_OPERATION = 'the contract declares no operation, so the declarat
20
18
  */
21
19
  export function successIndicatorSeparationRelevance(contract) {
22
20
  const rule = 'success-indicator-separation';
23
- const operations = operationsOf(contract);
21
+ const operations = resolveOperations(contract);
24
22
  if (operations.length === 0)
25
23
  return verdict(rule, true, NO_OPERATION);
26
- for (const operation of operations) {
27
- const { successIndicator, channelRoles } = operation.responseDescriptor;
24
+ for (const { operation, descriptor } of operations) {
25
+ const { successIndicator, channelRoles } = descriptor;
28
26
  // `=== null`: `DescriptorPointer` admits the empty string, which
29
27
  // nominates the whole response body.
30
28
  if (successIndicator === null) {
@@ -48,11 +46,11 @@ export function successIndicatorSeparationRelevance(contract) {
48
46
  */
49
47
  export function wholeBodyRelevance(contract) {
50
48
  const rule = 'whole-body';
51
- const operations = operationsOf(contract);
49
+ const operations = resolveOperations(contract);
52
50
  if (operations.length === 0)
53
51
  return verdict(rule, true, NO_OPERATION);
54
- for (const operation of operations) {
55
- const distinct = new Set(operation.responseDescriptor.requiredKeys);
52
+ for (const { operation, descriptor } of operations) {
53
+ const distinct = new Set(descriptor.requiredKeys);
56
54
  if (distinct.size > 1) {
57
55
  return verdict(rule, true, `operation ${operation.operationId} declares ${plural(distinct.size, 'distinct required response key')}`);
58
56
  }
@@ -60,20 +58,21 @@ export function wholeBodyRelevance(contract) {
60
58
  return verdict(rule, false, 'no operation declares more than one distinct required response key');
61
59
  }
62
60
  /**
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
61
+ * Rule 3: some operation declares a request key on any of its own input
62
+ * channels, AD-19's four transport channels off an interface that speaks HTTP
63
+ * and the four command channels off one that does not. The site is the whole
64
+ * channel triple. A key with no
65
65
  * `types` entry has an absent type, a key typed `null` has AD-31's
66
66
  * indeterminate one, and both are relevant, so declaring an input and leaving
67
67
  * it untyped buys no irrelevance.
68
68
  */
69
69
  export function malformedInputRelevance(contract) {
70
70
  const rule = 'malformed-input';
71
- const operations = operationsOf(contract);
71
+ const operations = resolveOperations(contract);
72
72
  if (operations.length === 0)
73
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];
74
+ for (const { operation, requestChannels } of operations) {
75
+ for (const { channel, shape } of requestChannels) {
77
76
  // `Object.keys` enumerates own keys only. `KeyName` admits
78
77
  // `constructor`, which a keyed lookup would find on the prototype.
79
78
  const key = shape.requiredKeys[0] ??
@@ -92,11 +91,11 @@ export function malformedInputRelevance(contract) {
92
91
  */
93
92
  export function perRecordRelevance(contract) {
94
93
  const rule = 'per-record';
95
- const operations = operationsOf(contract);
94
+ const operations = resolveOperations(contract);
96
95
  if (operations.length === 0)
97
96
  return verdict(rule, true, NO_OPERATION);
98
- for (const operation of operations) {
99
- const { collectionLocations } = operation.responseDescriptor;
97
+ for (const { operation, descriptor } of operations) {
98
+ const { collectionLocations } = descriptor;
100
99
  if (collectionLocations === null) {
101
100
  return verdict(rule, true, `operation ${operation.operationId} declares no collection-location list, so no collection is declared to range over`);
102
101
  }
@@ -132,11 +131,11 @@ export function siblingCrossCheckRelevance(contract) {
132
131
  */
133
132
  export function omissionAndCompletenessRelevance(contract) {
134
133
  const rule = 'omission-and-completeness';
135
- const operations = operationsOf(contract);
134
+ const operations = resolveOperations(contract);
136
135
  if (operations.length === 0)
137
136
  return verdict(rule, true, NO_OPERATION);
138
- for (const operation of operations) {
139
- const { collectionLocations } = operation.responseDescriptor;
137
+ for (const { operation, descriptor } of operations) {
138
+ const { collectionLocations } = descriptor;
140
139
  if (collectionLocations === null) {
141
140
  return verdict(rule, true, `operation ${operation.operationId} declares no collection-location list, so no location can name a reference set`);
142
141
  }
@@ -155,10 +154,10 @@ export function omissionAndCompletenessRelevance(contract) {
155
154
  */
156
155
  export function stateChangeReadBackRelevance(contract) {
157
156
  const rule = 'state-change-read-back';
158
- const operations = operationsOf(contract);
157
+ const operations = resolveOperations(contract);
159
158
  if (operations.length === 0)
160
159
  return verdict(rule, true, NO_OPERATION);
161
- for (const operation of operations) {
160
+ for (const { operation } of operations) {
162
161
  if (operation.stateChangeMarker) {
163
162
  return verdict(rule, true, `operation ${operation.operationId} declares stateChangeMarker: true`);
164
163
  }
@@ -1,7 +1,7 @@
1
1
  import { type EvalContract } from '../schemas/eval-contract.ts';
2
2
  import type { Expression } from '../schemas/expression.ts';
3
- import type { Operation } from '../schemas/interface.ts';
4
3
  import { type PlanIndex } from '../seal/plan-index.ts';
4
+ import { type ResolvedOperation } from './operations.ts';
5
5
  import { type DisciplineRule } from './rules.ts';
6
6
  export type SatisfactionVerdict = {
7
7
  readonly rule: DisciplineRule;
@@ -40,7 +40,7 @@ type OracleView = {
40
40
  * own, which is what every per-rule fixture does.
41
41
  */
42
42
  export type SatisfactionContext = {
43
- readonly operations: readonly Operation[];
43
+ readonly operations: readonly ResolvedOperation[];
44
44
  readonly index: PlanIndex;
45
45
  readonly oracles: readonly OracleView[];
46
46
  };