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
@@ -2,14 +2,24 @@
2
2
  * Checks interface kinds, inventory-wide operation signatures, and step input
3
3
  * bindings against each operation's request shape.
4
4
  */
5
+ import { boundChannelsOf, declaredArtifactsOf, descriptorArtifactOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
5
6
  import { StructuralFailure } from '../failure-codes.js';
6
- import { TRANSPORT_CHANNELS } from '../schemas/pointer.js';
7
- import { buildPlanIndex } from '../seal/plan-index.js';
8
- /** Rejects permitted interface kinds that this contract version cannot run. */
7
+ import { operationsOf } from '../schemas/interface.js';
8
+ import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
9
+ import { forEachArtifactPointer } from './reachability.js';
10
+ /**
11
+ * Rejects permitted interface kinds whose probe semantics are undeclared.
12
+ *
13
+ * AD-10 closed all three non-api kinds and named the condition for opening
14
+ * one: the semantics have to be declared. They are declared for `cli`, so it
15
+ * runs; `web` and `mcp` are still undeclared and still fail here, which is
16
+ * what keeps this code fireable and keeps AD-10's sentence true of them.
17
+ */
18
+ const SUPPORTED_INTERFACE_KINDS = ['api', 'cli'];
9
19
  export function checkInterfaceKind(contract) {
10
20
  for (const iface of contract.permittedInterfaces) {
11
- if (iface.kind !== 'api') {
12
- throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported in v0; only "api" is (AD-10)`);
21
+ if (!SUPPORTED_INTERFACE_KINDS.includes(iface.kind)) {
22
+ throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported; "api" and "cli" are (AD-10)`);
13
23
  }
14
24
  }
15
25
  }
@@ -26,12 +36,36 @@ const erase = (pathTemplate) => pathTemplate.replace(PARAMETER_SEGMENT_PATTERN,
26
36
  export function operationSignature(operation) {
27
37
  return `${operation.method} ${erase(operation.pathTemplate)}`;
28
38
  }
39
+ /**
40
+ * The separator between a command's executable and each subcommand segment,
41
+ * declared once so the contract side and AD-40's corpus side cannot disagree
42
+ * about it. A space, matching the way the identity is written on a terminal.
43
+ */
44
+ export const COMMAND_SIGNATURE_SEPARATOR = ' ';
45
+ /**
46
+ * The command counterpart of `operationSignature`, compared literally.
47
+ *
48
+ * There is no erasure step. A subcommand path carries no parameters: a
49
+ * command's variable inputs are its arguments and options, which live in the
50
+ * request shape. Erasing a subcommand segment would make `tool review` and
51
+ * `tool report` one signature, which is the opposite of what erasure is for.
52
+ */
53
+ export function commandSignature(operation) {
54
+ return [
55
+ operation.invocation.executable,
56
+ ...operation.invocation.subcommandPath,
57
+ ].join(COMMAND_SIGNATURE_SEPARATOR);
58
+ }
59
+ /** The transport identity of an operation of either kind. */
60
+ export const anyOperationSignature = (operation) => isCommandOperation(operation)
61
+ ? commandSignature(operation)
62
+ : operationSignature(operation);
29
63
  /** Finds duplicate method and path signatures across the full inventory. */
30
64
  export function checkDuplicateOperationSignature(contract) {
31
65
  const seen = new Map();
32
66
  for (const iface of contract.permittedInterfaces) {
33
- for (const operation of iface.operations) {
34
- const signature = operationSignature(operation);
67
+ for (const operation of operationsOf(iface)) {
68
+ const signature = anyOperationSignature(operation);
35
69
  const collision = seen.get(signature);
36
70
  if (collision !== undefined) {
37
71
  throw new StructuralFailure('duplicate-operation-signature', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}]`, `collides with permittedInterfaces[logicalId=${collision.logicalId}].operations[operationId=${collision.operation.operationId}] after parameter-name erasure ("${signature}") (AD-19, AD-40)`);
@@ -40,6 +74,44 @@ export function checkDuplicateOperationSignature(contract) {
40
74
  }
41
75
  }
42
76
  }
77
+ /**
78
+ * `unresolved-artifact-reference`: an artifact identifier nothing declares.
79
+ *
80
+ * Two sites name one: an evidence pointer's identifier segment, and a command
81
+ * operation's own `descriptorChannel` when it nominates an artifact. Both are
82
+ * authoring faults the compiler can see, and both take a code rather than
83
+ * resolving `absent`, on AD-26's own precedent for a dangling reference-set
84
+ * identifier: `absent` is defined over pointers that do not resolve against
85
+ * observed evidence, and a dangling declaration is neither.
86
+ */
87
+ export function checkArtifactReferences(contract) {
88
+ for (const iface of contract.permittedInterfaces) {
89
+ for (const operation of operationsOf(iface)) {
90
+ const declared = declaredArtifactsOf(operation);
91
+ const nominated = descriptorArtifactOf(operation);
92
+ if (nominated !== null && !declared.includes(nominated)) {
93
+ throw new StructuralFailure('unresolved-artifact-reference', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}].descriptorChannel.artifactId`, `nominates "${nominated}", which this operation does not declare it writes (AD-19, AD-26)`);
94
+ }
95
+ }
96
+ }
97
+ const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
98
+ forEachArtifactPointer(contract, (pointer, path) => {
99
+ const target = parseEvidenceTarget(pointer);
100
+ if (target.artifactId === null)
101
+ return;
102
+ const step = index.stepOf(target.stepId);
103
+ if (step === undefined)
104
+ return;
105
+ const operation = anyOperationOf(index, step.operationId);
106
+ // An unresolvable step or operation is `unreachable-check-evidence`'s,
107
+ // at a higher rung; this check has nothing to compare against.
108
+ if (operation === undefined)
109
+ return;
110
+ if (declaredArtifactsOf(operation).includes(target.artifactId))
111
+ return;
112
+ throw new StructuralFailure('unresolved-artifact-reference', path, `"${pointer}" names the "${target.artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes (AD-26)`);
113
+ });
114
+ }
43
115
  /**
44
116
  * Checks each binding key against its operation's request shape, and each
45
117
  * `{ principal }` value against the contract's declared principals.
@@ -59,14 +131,19 @@ export function checkUndeclaredMandatoryInput(contract) {
59
131
  const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
60
132
  const principals = new Set(Object.keys(contract.testData.principals ?? {}));
61
133
  for (const step of contract.interactionPlan) {
62
- const operation = index.operationOf(step.operationId);
134
+ const operation = anyOperationOf(index, step.operationId);
63
135
  if (operation === undefined)
64
136
  continue;
65
- for (const channel of TRANSPORT_CHANNELS) {
66
- const binding = step.inputBinding[channel];
137
+ for (const { channel, bound: binding } of boundChannelsOf(step.inputBinding)) {
67
138
  if (binding === null)
68
139
  continue;
69
- const { requiredKeys, permittedKeys } = operation.requestShape[channel];
140
+ const shape = requestShapeOf(operation, channel);
141
+ // A step binding a channel of the other kind has no declared shape
142
+ // to answer to. Reporting it as an undeclared input is true as far
143
+ // as it goes: the operation declares no such channel, and so
144
+ // declares no such key on it.
145
+ const requiredKeys = shape?.requiredKeys ?? [];
146
+ const permittedKeys = shape?.permittedKeys ?? [];
70
147
  for (const key of Object.keys(binding)) {
71
148
  const path = `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${channel}[${JSON.stringify(key)}]`;
72
149
  if (!requiredKeys.includes(key) && !permittedKeys.includes(key)) {
@@ -1,7 +1,17 @@
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';
3
+ import type { AnyOperation } from '../schemas/interface.ts';
4
4
  import { type PlanIndex } from '../seal/plan-index.ts';
5
+ /**
6
+ * Every interaction-rooted pointer the contract writes down, wherever it sits:
7
+ * an oracle's check and its direction's evidence targets, a rubric criterion's
8
+ * evidence, and each operation's sensitivity-witness relation.
9
+ *
10
+ * Broader than `forEachCheckPointer`, which walks oracle checks alone, because
11
+ * an artifact identifier is an authoring fault at every site that names one and
12
+ * a check that walked only the checks would report half of them.
13
+ */
14
+ export declare function forEachArtifactPointer(contract: EvalContract, visit: (pointer: string, artifactPath: string) => void): void;
5
15
  export declare function checkBoundElementScope(contract: EvalContract): void;
6
16
  /** `checkBoundElementScope` over one bare `Expression`. */
7
17
  export declare function checkExpressionBoundElementScope(expression: Expression, artifactPath: string): void;
@@ -13,7 +23,7 @@ export declare function checkExpressionBoundElementScope(expression: Expression,
13
23
  */
14
24
  export declare function forEachExpressionPointer(expression: Expression, visit: (pointer: string, path: string) => void): void;
15
25
  /** `checkEvidenceReachability` over one bare `Expression` and one operation. */
16
- export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation: Operation): void;
26
+ export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation: AnyOperation): void;
17
27
  type ReachabilityResult = {
18
28
  readonly reachable: true;
19
29
  } | {
@@ -7,10 +7,12 @@
7
7
  * `evaluatePointerReachability` is exported separately as the non-throwing
8
8
  * per-pointer core, for reuse and direct testing.
9
9
  */
10
+ import { declaredArtifactsOf, descriptorArtifactOf, descriptorChannelOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
10
11
  import { ARRAY_INDEX_PATTERN } from '../evaluate/evidence-resolution.js';
11
12
  import { StructuralFailure } from '../failure-codes.js';
13
+ import { operationsOf } from '../schemas/interface.js';
12
14
  import { JsonTypeName } from '../schemas/primitives.js';
13
- import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
15
+ import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
14
16
  function visitOperand(operand, path, insideQuantifier, visit) {
15
17
  if ('pointer' in operand)
16
18
  visit({ pointer: operand.pointer, path, insideQuantifier });
@@ -52,6 +54,42 @@ function forEachCheckPointer(contract, visit) {
52
54
  visitExpression(oracle.check, 'check', false, (site) => visit(site, oracle.id));
53
55
  });
54
56
  }
57
+ /**
58
+ * Every interaction-rooted pointer the contract writes down, wherever it sits:
59
+ * an oracle's check and its direction's evidence targets, a rubric criterion's
60
+ * evidence, and each operation's sensitivity-witness relation.
61
+ *
62
+ * Broader than `forEachCheckPointer`, which walks oracle checks alone, because
63
+ * an artifact identifier is an authoring fault at every site that names one and
64
+ * a check that walked only the checks would report half of them.
65
+ */
66
+ export function forEachArtifactPointer(contract, visit) {
67
+ const seen = (pointer, artifactPath) => {
68
+ if (pointer.startsWith('@'))
69
+ return;
70
+ visit(pointer, artifactPath);
71
+ };
72
+ contract.oracles.forEach((oracle) => {
73
+ if (oracle.check !== null)
74
+ visitExpression(oracle.check, 'check', false, (site) => seen(site.pointer, `EvalContract.oracles[id=${oracle.id}].${site.path}`));
75
+ oracle.direction?.evidenceTargets.forEach((target, index) => {
76
+ seen(target, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets[${index}]`);
77
+ });
78
+ });
79
+ contract.rubrics.forEach((rubric) => {
80
+ rubric.criteria.forEach((criterion) => {
81
+ seen(criterion.evidence, `EvalContract.rubrics[id=${rubric.id}].criteria[id=${criterion.id}].evidence`);
82
+ });
83
+ });
84
+ contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
85
+ operationsOf(iface).forEach((operation, operationIndex) => {
86
+ const witness = operation.sensitivityWitness;
87
+ if (witness === null)
88
+ return;
89
+ visitExpression(witness.relation, 'relation', false, (site) => seen(site.pointer, `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}].sensitivityWitness.${site.path}`));
90
+ });
91
+ });
92
+ }
55
93
  // ---- malformed-operator-expression: @/ outside any quantifier -----------
56
94
  export function checkBoundElementScope(contract) {
57
95
  forEachCheckPointer(contract, (site, oracleId) => {
@@ -125,12 +163,50 @@ export function evaluatePointerReachability(pointer, index) {
125
163
  if (step === undefined) {
126
164
  return unreachable('names a step the interaction plan does not declare');
127
165
  }
128
- const operation = index.operationOf(step.operationId);
166
+ const operation = anyOperationOf(index, step.operationId);
129
167
  if (operation === undefined) {
130
168
  return unreachable(`names step "${target.stepId}", which names operation "${step.operationId}", not declared by any permitted interface`);
131
169
  }
132
170
  return evaluateReachabilityAgainstOperation(pointer, operation);
133
171
  }
172
+ /**
173
+ * Descent through the operation's response descriptor, from whichever channel
174
+ * that descriptor describes. `channel` is carried only so the reason names the
175
+ * pointer's own root back to the author.
176
+ */
177
+ function descendThroughDescriptor(descriptor, target, operationId, channel) {
178
+ if (target.tail.length === 0)
179
+ return reachable();
180
+ const firstToken = target.tail[0];
181
+ if (firstToken === undefined) {
182
+ // Unreachable: the length check above guarantees an element.
183
+ throw new TypeError('evidence-target tail is non-empty but has no first token');
184
+ }
185
+ const { requiredKeys, permittedKeys, types, collectionLocations } = descriptor;
186
+ // A root-declared collection (`pointer: ''`) indexes directly, bypassing
187
+ // the key check below. `expectedCardinality` bounds the array size
188
+ // (`exact` is the true count; `at-most`/`page-bounded` is an upper
189
+ // bound), so an index at or past it is unreachable.
190
+ const rootCollection = collectionLocations?.find((location) => location.pointer === '');
191
+ if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
192
+ const { expectedCardinality } = rootCollection;
193
+ const bound = expectedCardinality.mode === 'exact'
194
+ ? expectedCardinality.count
195
+ : expectedCardinality.max;
196
+ if (Number(firstToken) >= bound) {
197
+ return unreachable(`addresses ${channel} index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
198
+ }
199
+ return reachable();
200
+ }
201
+ if (!requiredKeys.includes(firstToken) &&
202
+ !permittedKeys.includes(firstToken)) {
203
+ return unreachable(`addresses ${channel} field "${firstToken}", which operation "${operationId}" declares in neither requiredKeys nor permittedKeys`);
204
+ }
205
+ if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
206
+ return unreachable(`descends into ${channel} field "${firstToken}", which operation "${operationId}" declares a scalar with no further structure`);
207
+ }
208
+ return reachable();
209
+ }
134
210
  /**
135
211
  * The same rules, against an operation the caller already resolved. Without
136
212
  * this check a probe-side condition addressing an undeclared channel or key
@@ -142,47 +218,54 @@ function evaluateReachabilityAgainstOperation(pointer, operation) {
142
218
  if (pointer.startsWith('@'))
143
219
  return reachable();
144
220
  const target = parseEvidenceTarget(pointer);
145
- if (target.channel === 'stdout' || target.channel === 'stderr') {
146
- // stdout/stderr are always bare strings. A non-empty tail proves the
147
- // pointer unreachable.
148
- if (target.tail.length > 0) {
149
- return unreachable(`addresses a field inside ${target.channel}, which never carries structure to descend into`);
221
+ const descriptorChannel = descriptorChannelOf(operation);
222
+ const command = isCommandOperation(operation);
223
+ if (target.channel === 'artifact') {
224
+ const { artifactId } = target;
225
+ if (artifactId === null) {
226
+ // Unreachable: parseEvidenceTarget's own guarantee.
227
+ throw new TypeError('artifact evidence target names no artifact');
150
228
  }
151
- return reachable();
152
- }
153
- if (target.channel === 'response-body') {
154
- if (target.tail.length === 0)
155
- return reachable();
156
- const firstToken = target.tail[0];
157
- if (firstToken === undefined) {
158
- // Unreachable: the length check above guarantees an element.
159
- throw new TypeError('evidence-target tail is non-empty but has no first token');
229
+ // An identifier the operation does not declare produces no evidence, so
230
+ // it is unreachable and this says so. `checkArtifactReferences` runs
231
+ // earlier in `compile` and reports the more specific
232
+ // `unresolved-artifact-reference` for a contract, so the two never race
233
+ // there; this answer is the only one on the probe side, where that check
234
+ // does not run because it walks a contract rather than a signature.
235
+ if (!declaredArtifactsOf(operation).includes(artifactId)) {
236
+ return unreachable(`names the "${artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes`);
160
237
  }
161
- const { requiredKeys, permittedKeys, types, collectionLocations } = operation.responseDescriptor;
162
- // A root-declared collection (`pointer: ''`) indexes directly, bypassing
163
- // the key check below. `expectedCardinality` bounds the array size
164
- // (`exact` is the true count; `at-most`/`page-bounded` is an upper
165
- // bound), so an index at or past it is unreachable.
166
- const rootCollection = collectionLocations?.find((location) => location.pointer === '');
167
- if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
168
- const { expectedCardinality } = rootCollection;
169
- const bound = expectedCardinality.mode === 'exact'
170
- ? expectedCardinality.count
171
- : expectedCardinality.max;
172
- if (Number(firstToken) >= bound) {
173
- return unreachable(`addresses response-body index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
238
+ if (artifactId !== descriptorArtifactOf(operation)) {
239
+ // Declared to exist, and nothing declares its structure: the
240
+ // operation's one descriptor describes a different channel.
241
+ if (target.tail.length > 0) {
242
+ return unreachable(`addresses a field inside the "${artifactId}" artifact, which operation "${operation.operationId}" declares it writes but declares no structure for`);
174
243
  }
175
244
  return reachable();
176
245
  }
177
- if (!requiredKeys.includes(firstToken) &&
178
- !permittedKeys.includes(firstToken)) {
179
- return unreachable(`addresses response-body field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
180
- }
181
- if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
182
- return unreachable(`descends into response-body field "${firstToken}", which operation "${operation.operationId}" declares a scalar with no further structure`);
246
+ return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, `the "${artifactId}" artifact`);
247
+ }
248
+ // The declared output channel descends through the descriptor whatever it
249
+ // is called. This is the one rule; the channels below are the cases where
250
+ // no descriptor applies.
251
+ if (target.channel === descriptorChannel) {
252
+ return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, target.channel);
253
+ }
254
+ if (target.channel === 'stdout' || target.channel === 'stderr') {
255
+ // The stream this operation's descriptor does not describe carries no
256
+ // declared structure, so a non-empty tail proves the pointer
257
+ // unreachable.
258
+ if (target.tail.length > 0) {
259
+ return unreachable(`addresses a field inside ${target.channel}, which operation "${operation.operationId}" declares no structure for`);
183
260
  }
184
261
  return reachable();
185
262
  }
263
+ if (command &&
264
+ (target.channel === 'response-body' ||
265
+ target.channel === 'response-headers' ||
266
+ target.channel === 'response-status')) {
267
+ return unreachable(`addresses ${target.channel} on operation "${operation.operationId}", which runs behind a command and produces no HTTP response`);
268
+ }
186
269
  if (target.channel === 'call-inputs') {
187
270
  if (target.tail.length === 0)
188
271
  return reachable();
@@ -195,7 +278,11 @@ function evaluateReachabilityAgainstOperation(pointer, operation) {
195
278
  if (firstToken === undefined) {
196
279
  throw new TypeError('evidence-target tail is non-empty but has no first token');
197
280
  }
198
- const { requiredKeys, permittedKeys, types } = operation.requestShape[transportChannel];
281
+ const shape = requestShapeOf(operation, transportChannel);
282
+ if (shape === undefined) {
283
+ return unreachable(`addresses call-inputs ${transportChannel}, a channel operation "${operation.operationId}" does not accept input on`);
284
+ }
285
+ const { requiredKeys, permittedKeys, types } = shape;
199
286
  if (!requiredKeys.includes(firstToken) &&
200
287
  !permittedKeys.includes(firstToken)) {
201
288
  return unreachable(`addresses call-inputs ${transportChannel} field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
@@ -0,0 +1,2 @@
1
+ /** `schema-version-mismatch`: the stamp is not the version this build reads. */
2
+ export declare function checkSchemaVersion(stamped: number, accepted: number, artifactPath: string): void;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * AD-11's version equality, performed by the one reader that has the contract.
3
+ *
4
+ * AD-11's rule is that "readers accept an equal `schemaVersion` only and throw
5
+ * `schema-version-mismatch` outside that". `lineage.ts` deliberately keeps the
6
+ * field a plain integer rather than a literal, so that a wrong version arrives
7
+ * as AD-28's named fault instead of an anonymous parse failure, which puts the
8
+ * comparison on whoever reads the artifact. For the eval contract nobody did:
9
+ * a contract stamped 3 parsed, compiled, sealed, and put its stale version into
10
+ * the scoring version digest, and AD-11 exists to keep that number comparable.
11
+ *
12
+ * `RuntimeFault` rather than `StructuralFailure`, against the grain of every
13
+ * other check in this directory. The two registries are disjoint by the
14
+ * Consistency Conventions, AD-5 has no version code, and AD-11 names
15
+ * `schema-version-mismatch` literally. Minting an AD-5 twin would give one
16
+ * condition two codes and make a caller's handler depend on which reader
17
+ * happened to notice first.
18
+ */
19
+ import { RuntimeFault } from '../schemas/faults.js';
20
+ /** `schema-version-mismatch`: the stamp is not the version this build reads. */
21
+ export function checkSchemaVersion(stamped, accepted, artifactPath) {
22
+ if (stamped === accepted)
23
+ return;
24
+ throw new RuntimeFault('schema-version-mismatch', artifactPath, `carries "schemaVersion" ${stamped} where this build reads ${accepted}; a contract written for another version is not read leniently, since its stale version would travel into the scoring version (AD-11)`);
25
+ }
@@ -4,26 +4,45 @@
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
14
  import { declaresNoRequestKeys } from '../declared-inputs.ts';
15
15
  import type { EvalContract } from '../schemas/eval-contract.ts';
16
- import type { Operation } from '../schemas/interface.ts';
17
- import { type TransportChannelName } from '../schemas/pointer.ts';
18
- import type { WitnessInputs } from '../schemas/sensitivity-witness.ts';
16
+ import type { AnyOperation } from '../schemas/interface.ts';
17
+ import type { InputChannelName } from '../schemas/pointer.ts';
18
+ import { type ApiWitnessInputs, type WitnessChannel, type WitnessInputs } from '../schemas/sensitivity-witness.ts';
19
19
  export { declaresNoRequestKeys };
20
20
  /**
21
- * The keys one set of witness inputs supplies on one channel. A body that is
22
- * absent, or JSON that is not an object, supplies no keys: the channel is
23
- * declared as a keyed shape, so such a leg omits every required key rather
24
- * than being exempt from the comparison.
21
+ * The transport spelling, for the consumers that can only send one: the probe
22
+ * port carries a method, a path template, and the four transport channels.
25
23
  */
26
- export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportChannelName): readonly string[];
24
+ export declare const isApiWitnessInputs: (inputs: WitnessInputs) => inputs is ApiWitnessInputs;
25
+ /**
26
+ * Whether the leg supplies this channel as an undifferentiated stream.
27
+ *
28
+ * Standard input written as text is one such stream. A `KeyedShapeDescriptor`
29
+ * can say what keys a channel carries and cannot say "one opaque stream", so a
30
+ * command that reads a prompt is declared by naming the one thing the stream
31
+ * carries, and the leg supplies that thing as the text it actually is.
32
+ */
33
+ export declare function suppliesOpaquely(inputs: WitnessInputs, channel: InputChannelName): boolean;
34
+ /**
35
+ * The keys one set of witness inputs supplies on one channel, or none where
36
+ * the leg's spelling has no such channel.
37
+ *
38
+ * `body` and `stdin` are the two channels a leg supplies as a tagged value
39
+ * rather than as a key map, because a leg has to tell an absent value from one
40
+ * carrying JSON null. This function is the bridge between that spelling and
41
+ * the keyed shape the request declares.
42
+ */
43
+ export declare function suppliedKeys(inputs: WitnessInputs, channel: InputChannelName): readonly string[];
44
+ /** One channel's supplied value, for the differential comparison. */
45
+ export declare function suppliedValue(inputs: WitnessInputs, channel: WitnessChannel): unknown;
27
46
  /**
28
47
  * Every channel of one set of witness inputs against the operation that will
29
48
  * receive them. All four, not only the differential channel: `planPreflight`
@@ -49,7 +68,7 @@ export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportCh
49
68
  * contract artifact, the surface AD-18 governs; a placeholder satisfies this
50
69
  * check, and what pre-flight probes is a fixture.
51
70
  */
52
- export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: Operation, owner: string, artifactPath: string): void;
71
+ export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: AnyOperation, owner: string, artifactPath: string): void;
53
72
  /**
54
73
  * Strict-gated, alongside `checkUndeclaredMandatoryInput`, whose code this
55
74
  * shares: `compile.ts` already gates that code behind `options.strict`, and one