eval-quality 0.1.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 (179) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +20 -13
  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/application/index.d.ts +2 -0
  28. package/dist/application/index.js +1 -0
  29. package/dist/application/score.d.ts +48 -0
  30. package/dist/application/score.js +186 -0
  31. package/dist/cli/arguments.d.ts +7 -3
  32. package/dist/cli/arguments.js +73 -5
  33. package/dist/cli/exit-codes.d.ts +6 -2
  34. package/dist/cli/exit-codes.js +8 -9
  35. package/dist/cli/main.js +9 -0
  36. package/dist/cli/render.d.ts +3 -6
  37. package/dist/cli/render.js +57 -5
  38. package/dist/cli/run.d.ts +13 -3
  39. package/dist/cli/run.js +136 -16
  40. package/dist/core/canonical/canonicalize.d.ts +12 -0
  41. package/dist/core/canonical/canonicalize.js +18 -4
  42. package/dist/core/compile/bindings.d.ts +89 -0
  43. package/dist/core/compile/bindings.js +334 -0
  44. package/dist/core/compile/compile.d.ts +17 -0
  45. package/dist/core/compile/compile.js +37 -1
  46. package/dist/core/compile/excluded-content.d.ts +11 -0
  47. package/dist/core/compile/excluded-content.js +42 -0
  48. package/dist/core/compile/expression-legality.d.ts +49 -0
  49. package/dist/core/compile/expression-legality.js +158 -38
  50. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  51. package/dist/core/compile/forbidden-inputs.js +16 -3
  52. package/dist/core/compile/interface-inventory.d.ts +57 -2
  53. package/dist/core/compile/interface-inventory.js +119 -13
  54. package/dist/core/compile/reachability.d.ts +28 -1
  55. package/dist/core/compile/reachability.js +167 -35
  56. package/dist/core/compile/schema-version.d.ts +2 -0
  57. package/dist/core/compile/schema-version.js +25 -0
  58. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  59. package/dist/core/compile/sensitivity-witness.js +110 -23
  60. package/dist/core/compile/step-reference.d.ts +2 -0
  61. package/dist/core/compile/step-reference.js +49 -0
  62. package/dist/core/coverage/operations.d.ts +62 -0
  63. package/dist/core/coverage/operations.js +57 -0
  64. package/dist/core/coverage/relevance.d.ts +4 -2
  65. package/dist/core/coverage/relevance.js +22 -23
  66. package/dist/core/coverage/satisfaction.d.ts +2 -2
  67. package/dist/core/coverage/satisfaction.js +73 -48
  68. package/dist/core/declared-inputs.d.ts +83 -4
  69. package/dist/core/declared-inputs.js +105 -8
  70. package/dist/core/emit/emit.d.ts +9 -0
  71. package/dist/core/emit/emit.js +214 -0
  72. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  73. package/dist/core/emit/private-artifact-digest.js +31 -0
  74. package/dist/core/evaluate/evidence-resolution.d.ts +10 -3
  75. package/dist/core/evaluate/evidence-resolution.js +74 -11
  76. package/dist/core/evaluate/operators.d.ts +22 -0
  77. package/dist/core/evaluate/operators.js +17 -1
  78. package/dist/core/evaluate/resolution.d.ts +14 -3
  79. package/dist/core/evaluate/resolution.js +63 -3
  80. package/dist/core/excluded-content.d.ts +65 -0
  81. package/dist/core/excluded-content.js +113 -0
  82. package/dist/core/failure-codes.d.ts +2 -2
  83. package/dist/core/failure-codes.js +7 -2
  84. package/dist/core/ingest/conditions.d.ts +201 -0
  85. package/dist/core/ingest/conditions.js +48 -0
  86. package/dist/core/ingest/index.d.ts +13 -0
  87. package/dist/core/ingest/index.js +11 -0
  88. package/dist/core/ingest/ingest.d.ts +68 -0
  89. package/dist/core/ingest/ingest.js +318 -0
  90. package/dist/core/lineage/stage-table.d.ts +21 -4
  91. package/dist/core/lineage/stage-table.js +35 -3
  92. package/dist/core/preflight/plan.d.ts +7 -5
  93. package/dist/core/preflight/plan.js +75 -32
  94. package/dist/core/preflight/projection.d.ts +6 -3
  95. package/dist/core/preflight/projection.js +22 -2
  96. package/dist/core/preflight/reduce.js +47 -7
  97. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  98. package/dist/core/preflight/witness-evidence.js +73 -20
  99. package/dist/core/schemas/artifact.d.ts +1049 -30
  100. package/dist/core/schemas/constraint-ledger.js +51 -0
  101. package/dist/core/schemas/defect-signature.d.ts +695 -0
  102. package/dist/core/schemas/defect-signature.js +175 -0
  103. package/dist/core/schemas/eval-contract.d.ts +550 -12
  104. package/dist/core/schemas/eval-contract.js +32 -2
  105. package/dist/core/schemas/evidence-artifact.d.ts +109 -19
  106. package/dist/core/schemas/evidence-artifact.js +64 -8
  107. package/dist/core/schemas/interface.d.ts +741 -13
  108. package/dist/core/schemas/interface.js +120 -7
  109. package/dist/core/schemas/isolation-manifest.js +16 -9
  110. package/dist/core/schemas/plan.d.ts +329 -3
  111. package/dist/core/schemas/plan.js +89 -3
  112. package/dist/core/schemas/pointer.d.ts +47 -1
  113. package/dist/core/schemas/pointer.js +89 -8
  114. package/dist/core/schemas/port-messages.d.ts +194 -10
  115. package/dist/core/schemas/port-messages.js +73 -6
  116. package/dist/core/schemas/probe-body.d.ts +18 -0
  117. package/dist/core/schemas/probe-body.js +13 -0
  118. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  119. package/dist/core/schemas/probe-qualification.js +106 -0
  120. package/dist/core/schemas/probe.d.ts +431 -0
  121. package/dist/core/schemas/probe.js +6 -2
  122. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  123. package/dist/core/schemas/scoring-policy.js +6 -1
  124. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  125. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  126. package/dist/core/schemas/sealed-run-record.d.ts +161 -12
  127. package/dist/core/schemas/sealed-run-record.js +92 -15
  128. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  129. package/dist/core/schemas/sensitivity-witness.js +61 -5
  130. package/dist/core/score/binding-order.d.ts +31 -0
  131. package/dist/core/score/binding-order.js +88 -0
  132. package/dist/core/score/bindings.d.ts +93 -0
  133. package/dist/core/score/bindings.js +309 -0
  134. package/dist/core/score/ladder-table.d.ts +23 -0
  135. package/dist/core/score/ladder-table.js +109 -0
  136. package/dist/core/score/ladder.d.ts +229 -0
  137. package/dist/core/score/ladder.js +464 -0
  138. package/dist/core/score/mode-agreement.d.ts +30 -0
  139. package/dist/core/score/mode-agreement.js +16 -0
  140. package/dist/core/score/outcome-table.d.ts +17 -0
  141. package/dist/core/score/outcome-table.js +172 -0
  142. package/dist/core/score/outcome.d.ts +466 -0
  143. package/dist/core/score/outcome.js +562 -0
  144. package/dist/core/score/qualification.d.ts +83 -0
  145. package/dist/core/score/qualification.js +645 -0
  146. package/dist/core/score/quotation.d.ts +56 -0
  147. package/dist/core/score/quotation.js +226 -0
  148. package/dist/core/score/reduce-trials.d.ts +80 -0
  149. package/dist/core/score/reduce-trials.js +90 -0
  150. package/dist/core/score/score.d.ts +72 -0
  151. package/dist/core/score/score.js +607 -0
  152. package/dist/core/score/selection.d.ts +80 -0
  153. package/dist/core/score/selection.js +98 -0
  154. package/dist/core/score/strength.d.ts +58 -0
  155. package/dist/core/score/strength.js +227 -0
  156. package/dist/core/score/witness.d.ts +138 -0
  157. package/dist/core/score/witness.js +320 -0
  158. package/dist/core/seal/derived-reference.d.ts +2 -9
  159. package/dist/core/seal/derived-reference.js +293 -46
  160. package/dist/core/seal/plan-index.d.ts +18 -3
  161. package/dist/core/seal/plan-index.js +44 -8
  162. package/dist/core/seal/seal.js +8 -3
  163. package/dist/core/stage-contracts.d.ts +62 -2
  164. package/dist/index.d.ts +1 -1
  165. package/dist/index.js +1 -1
  166. package/dist/ports/environment-probe-port.d.ts +60 -4
  167. package/dist/testing/index.d.ts +2 -0
  168. package/dist/testing/index.js +1 -0
  169. package/dist/testing/probe-conformance.d.ts +29 -0
  170. package/dist/testing/probe-conformance.js +34 -5
  171. package/package.json +12 -3
  172. package/schemas/eval-contract.schema.json +1787 -797
  173. package/schemas/evidence-artifact.schema.json +126 -9
  174. package/schemas/isolation-manifest.schema.json +17 -10
  175. package/schemas/probe.schema.json +636 -5
  176. package/schemas/rubric.schema.json +1 -1
  177. package/schemas/scoring-policy.schema.json +8 -1
  178. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  179. package/schemas/sealed-run-record.schema.json +314 -40
@@ -1,8 +1,20 @@
1
- import { parseEvidenceTarget, resolveOperation, resolveStep, } from './plan-index.js';
1
+ /**
2
+ * The derived-reference vocabulary (AD-16). Renders an evidence-target
3
+ * pointer, or a temporally-paired pair, as a description of the step's
4
+ * operation and selection predicate; the step's own identifier never appears.
5
+ * Resolves pointers through a `PlanIndex` into a phrase, kept apart from
6
+ * `plan-index.ts`'s resolving and `direction-prose.ts`'s relation templates.
7
+ */
8
+ import { boundChannelsOf, isCommandOperation } from '../declared-inputs.js';
9
+ import { StructuralFailure } from '../failure-codes.js';
10
+ import { anyOperationOf, parseEvidenceTarget, resolveStep, } from './plan-index.js';
2
11
  function resolveEvidenceTarget(pointer, index) {
3
12
  const target = parseEvidenceTarget(pointer);
4
13
  const step = resolveStep(index, target.stepId);
5
- const operation = resolveOperation(index, step.operationId);
14
+ const operation = anyOperationOf(index, step.operationId);
15
+ if (operation === undefined) {
16
+ throw new TypeError(`step names an operation the permitted interfaces do not declare: ${step.operationId}`);
17
+ }
6
18
  return { target, step, operation };
7
19
  }
8
20
  // ---- joining ---------------------------------------------------------
@@ -21,30 +33,31 @@ function joinWithAnd(items) {
21
33
  // ---- operation identity -----------------------------------------------
22
34
  // Humanizing a kebab-case operationId is injective on distinct ids, and
23
35
  // `buildPlanIndex` already rejects a duplicate `operationId` across
24
- // interfaces, so two resolved operations never share this phrase. `method`
25
- // and `pathTemplate` are never printed here: AD-16 withholds the operation
26
- // inventory from the brief.
36
+ // interfaces, so two resolved operations never share this phrase. The
37
+ // transport identity is never printed here, whichever kind it is: AD-16
38
+ // withholds the operation inventory from the brief, so neither a method and a
39
+ // path template nor an executable and a subcommand path reach an evaluator.
40
+ //
41
+ // The noun follows the kind. Calling a command an endpoint told the evaluator
42
+ // something false about what it was reading, and the word is the only thing
43
+ // this phrase says beyond the operation's own name.
27
44
  function operationReference(operation) {
28
- return `the ${operation.operationId.split('-').join(' ')} endpoint`;
45
+ const name = operation.operationId.split('-').join(' ');
46
+ return isCommandOperation(operation)
47
+ ? `the ${name} command`
48
+ : `the ${name} endpoint`;
29
49
  }
30
50
  const ESCALATION_LEVELS = [
31
51
  'generic',
32
52
  'kind',
33
53
  'literal',
34
54
  ];
35
- const TRANSPORT_ORDER = [
36
- 'path',
37
- 'query',
38
- 'header',
39
- 'body',
40
- ];
41
- // Sorted by transport channel in fixed order, then by key name, so this
55
+ // Sorted by input channel in `INPUT_CHANNELS` order, then by key name, so this
42
56
  // never depends on a binding map's insertion order and the rendered prose
43
- // stays permutation-invariant.
57
+ // stays permutation-invariant. `boundChannelsOf` supplies the channel order.
44
58
  function bindingEntries(step) {
45
59
  const entries = [];
46
- for (const transportChannel of TRANSPORT_ORDER) {
47
- const map = step.inputBinding[transportChannel];
60
+ for (const { channel: transportChannel, bound: map } of boundChannelsOf(step.inputBinding)) {
48
61
  if (map === null)
49
62
  continue;
50
63
  for (const key of Object.keys(map).sort()) {
@@ -83,22 +96,171 @@ function canonicalizeForDisplay(value) {
83
96
  function formatLiteral(literal) {
84
97
  return JSON.stringify(canonicalizeForDisplay(literal));
85
98
  }
86
- // Base rendering: `type-violating` always names the input malformed.
87
- // `any` and, at the 'generic' level, `literal` share the same generic wording
88
- // on purpose; the escalation ladder below tells them apart once two steps
89
- // would otherwise collide.
99
+ // Base rendering, one arm per tagged form. `type-violating` always names the
100
+ // input malformed. `any` and, at the 'generic' level, every other form share
101
+ // the same generic wording on purpose; the escalation ladder below tells them
102
+ // apart once two steps would otherwise collide.
103
+ //
104
+ // `captured` escalates to a real derived reference at the 'literal' rung: the
105
+ // local phrase for the pointer's channel and tail, then the referenced step
106
+ // rendered through `stepReferenceAtLevel`, which is the same phrase
107
+ // `fullTargetPhrase` already prints for an evidence target. Naming the
108
+ // referenced OPERATION alone was tried and dropped: two predecessors sharing
109
+ // one operation and one body key then render identically, so two siblings
110
+ // capturing from them collide at every rung and `renderStepReference` throws
111
+ // out of `seal()` on a contract that compiles clean. Recursing into the
112
+ // predecessor separates them by its own binding clause. Two predecessors that
113
+ // are themselves irreducible still tie, and the throw there is correct: the
114
+ // declared structure does not distinguish them, which is the same answer
115
+ // `irreducibleCollisionPair` already gets. That throw is a coded
116
+ // `StructuralFailure` under `irreducible-step-reference`, and it is
117
+ // hoisted to compile time by `checkStepReferenceReducibility`, which runs this
118
+ // same ladder and reports the same code before `seal` is ever called.
119
+ //
120
+ // It does NOT call `renderStepReference`, so none of that function's
121
+ // constraints apply: no direction-scoped sibling list is consulted, so AD-16's
122
+ // scoping stays intact, and there is no tie to throw on. A step identifier
123
+ // never appears, which is the property AD-16 actually requires.
124
+ //
125
+ // `rendering` holds the steps already on the render path, and a capture back
126
+ // into one of them falls back to the level-independent phrase. A fixed depth
127
+ // bound was tried and dropped: at depth one, a two-link chain whose every link
128
+ // is genuinely distinguishable still collides, because only the immediate
129
+ // predecessor's binding shows and that is the half the two siblings share.
130
+ // Following the chain as far as it goes is what separates them, and the
131
+ // on-path set is what makes that terminate. Each recursion adds one step id, so
132
+ // the depth is bounded by the plan; `plan-exceeds-scripting-bound` bounds that
133
+ // too. Same guard the score module uses for an `after` chain, for the same
134
+ // reason: `binding-cycle` rejects a capture cycle at compile time, so this only
135
+ // covers a plan driven straight into the renderer.
136
+ //
137
+ // A pointer whose step or operation the index cannot resolve falls back to the
138
+ // 'kind' phrase. `seal()` runs after `compile`, which rejects such a pointer
139
+ // under `unreachable-check-evidence`, so this is a fallback for a
140
+ // directly-driven caller and adds no throw path.
141
+ //
142
+ // One thing a grep for step identifiers will find and should not be surprised
143
+ // by: `localTargetPhrase` prints the pointer's tail, and a response-body key
144
+ // the author happened to name the same as a step prints with it. That is the
145
+ // author's own declared key rather than a plan identifier, and it is what every
146
+ // ordinary evidence target has always printed.
147
+ //
148
+ // `principal` tops out at the declared name, which AD-18 keeps an opaque label
149
+ // and is therefore safe on the brief.
150
+ // One or more entries of a step's clause that capture from the same earlier
151
+ // step, rendered together so that step's phrase is expanded once. Expanding it
152
+ // per entry made the clause's size multiply down a capture chain: a step with k
153
+ // entries pointing at its predecessor expanded that predecessor k times, and the
154
+ // predecessor did the same to its own, so a sixteen-step chain with four keys a
155
+ // link produced a phrase past V8's maximum string length. Grouping makes the
156
+ // clause linear in keys and reads better besides.
157
+ //
158
+ // Grouping alone bounds one axis of three, and the size is their product. The
159
+ // other two are the number of DISTINCT predecessors a step references, which
160
+ // doubles per level on a complete capture DAG, and the length of one step's own
161
+ // clause text, which nothing bounds: `formatLiteral` prints a `JsonValue` with
162
+ // no length cap and a binding channel is a record with a minimum of one entry
163
+ // and no maximum. Measured on a sixteen-step complete DAG that passes every
164
+ // plan-side check, the multiplier is 8,192, so a single 65,536-character
165
+ // literal on the seed step, or two hundred ordinary forty-character ones,
166
+ // reaches `RangeError: Invalid string length`.
167
+ //
168
+ // `budget` is what bounds all three at once. Each expansion spends the
169
+ // characters it produced, and once the budget is gone a capture renders as the
170
+ // level-independent phrase it already had. Every `stepReferenceAtLevel` call
171
+ // from `renderStepReference` starts a fresh budget, so one sibling's expansion
172
+ // never degrades another's and the distinctness test stays order-independent.
173
+ //
174
+ // The budget is charged after an expansion rather than before, so the bound is
175
+ // the budget plus whatever one step's own clause text costs; a step binding a
176
+ // single 200,000-character literal renders about 204,000. That is the point of
177
+ // charging late: an expansion cannot be priced before its nested expansions are
178
+ // known, and stopping the NEXT one is what keeps the product finite. Measured
179
+ // on the sixteen-step complete DAG that reached `RangeError` before: 36 KB with
180
+ // a minimal seed, 75 KB with two hundred forty-character literals, 135 KB with
181
+ // a 65,536-character one, and, with a one-million-character literal on every one
182
+ // of its fourteen nodes, 1,000,564. All in single-digit milliseconds. That last
183
+ // row is the one that pins the bound: one literal's worth of overshoot rather
184
+ // than fourteen, which is why the exhaustion test sits in the render pass below
185
+ // rather than where the slots are built.
186
+ //
187
+ // A budget alone would couple things that have nothing to do with each other:
188
+ // growing a literal on a step neither sibling references can starve the
189
+ // expansion that separates them, so an unrelated field decides whether a
190
+ // contract seals. `renderStepReference`'s retry at the ceiling is what removes
191
+ // that, and it fires only when a collision came of a suppressed expansion. What
192
+ // remains is the honest case: siblings the declared structure does not
193
+ // distinguish still throw the precondition `TypeError`, which is the
194
+ // pre-existing failure mode `deferred-work.md` already carries.
195
+ // The ordinary limit, then the ceiling a starved render retries at. No
196
+ // realistic contract reaches the first; only one that would otherwise fail to
197
+ // render at all pays for the second.
198
+ const RENDER_BUDGET_LIMITS = [100_000, 4_000_000];
199
+ const newBudget = (limit) => ({
200
+ remaining: limit,
201
+ spent: false,
202
+ });
203
+ const unexpandedGroup = (group) => `the ${joinWithAnd(group.entries.map(entryName))} you obtained earlier`;
204
+ function renderCaptureGroup(group, level, index, rendering, budget) {
205
+ const names = joinWithAnd(group.entries.map(entryName));
206
+ const locals = joinWithAnd(group.targets.map(localTargetPhrase));
207
+ const step = index.stepOf(group.stepId);
208
+ const operation = step === undefined ? undefined : anyOperationOf(index, step.operationId);
209
+ if (step === undefined || operation === undefined) {
210
+ return unexpandedGroup(group);
211
+ }
212
+ const source = stepReferenceAtLevel(step, operation, level, index, rendering, budget);
213
+ // Charged after the fact, so a nested expansion pays before its parent does
214
+ // and an inner blowup is what stops the outer one.
215
+ budget.remaining -= source.length;
216
+ return `the ${names} you obtained as ${locals} from ${source}`;
217
+ }
218
+ // Records that an expansion was suppressed, which is what `renderStepReference`
219
+ // reads before deciding whether a collision is worth retrying at the ceiling.
220
+ function exhausted(budget) {
221
+ if (budget.remaining > 0)
222
+ return false;
223
+ budget.spent = true;
224
+ return true;
225
+ }
226
+ // Whether this entry expands into a group, or renders as the level-independent
227
+ // phrase on its own.
228
+ function expandableCapture(entry, level, index, rendering, budget) {
229
+ if (level !== 'literal' || !('captured' in entry.value))
230
+ return null;
231
+ if (exhausted(budget))
232
+ return null;
233
+ const target = parseEvidenceTarget(entry.value.captured);
234
+ if (rendering.has(target.stepId))
235
+ return null;
236
+ const step = index.stepOf(target.stepId);
237
+ if (step === undefined)
238
+ return null;
239
+ return anyOperationOf(index, step.operationId) === undefined ? null : target;
240
+ }
241
+ // Renders one entry on its own. Every captured entry that expands lands in a
242
+ // group instead, so the arm here is the level-independent fallback.
90
243
  function renderBindingValue(entry, level) {
91
244
  const name = entryName(entry);
92
- if (isTypeViolating(entry.value)) {
93
- return `a malformed ${name} value`;
245
+ const { value } = entry;
246
+ if ('matcher' in value) {
247
+ return value.matcher === 'type-violating'
248
+ ? `a malformed ${name} value`
249
+ : `the supplied ${name}`;
94
250
  }
95
- if (level === 'generic' || 'matcher' in entry.value) {
251
+ if (level === 'generic') {
96
252
  return `the supplied ${name}`;
97
253
  }
254
+ if ('principal' in value) {
255
+ return `the ${name} of the ${value.principal} account`;
256
+ }
257
+ if ('captured' in value) {
258
+ return `the ${name} you obtained earlier`;
259
+ }
98
260
  if (level === 'kind') {
99
261
  return `the stated ${name}`;
100
262
  }
101
- return `the ${name} ${formatLiteral(entry.value.literal)}`;
263
+ return `the ${name} ${formatLiteral(value.literal)}`;
102
264
  }
103
265
  // Every declared binding key escalates together: narrowing to only the
104
266
  // discriminating key would make one step's shown detail depend on which
@@ -108,13 +270,49 @@ function renderBindingValue(entry, level) {
108
270
  //
109
271
  // When a type-violating binding is present, the clause names only that one:
110
272
  // it is the semantically salient binding, matching AD-16's worked example.
111
- function bindingClause(step, level) {
273
+ function bindingClause(step, level, index, rendering, budget) {
112
274
  const entries = bindingEntries(step);
113
275
  if (entries.length === 0)
114
276
  return null;
115
277
  const malformed = entries.filter((entry) => isTypeViolating(entry.value));
116
278
  const chosen = malformed.length > 0 ? malformed : entries;
117
- return `with ${joinWithAnd(chosen.map((entry) => renderBindingValue(entry, level)))}`;
279
+ // Each expandable capture joins the group for the step it references, held
280
+ // at the position that step was first referenced from, so the clause's order
281
+ // still comes from `bindingEntries`'s sort rather than from a map's keys.
282
+ const slots = [];
283
+ const groups = new Map();
284
+ for (const entry of chosen) {
285
+ const target = expandableCapture(entry, level, index, rendering, budget);
286
+ if (target === null) {
287
+ slots.push(entry);
288
+ continue;
289
+ }
290
+ const existing = groups.get(target.stepId);
291
+ if (existing !== undefined) {
292
+ existing.entries.push(entry);
293
+ existing.targets.push(target);
294
+ continue;
295
+ }
296
+ const group = {
297
+ stepId: target.stepId,
298
+ entries: [entry],
299
+ targets: [target],
300
+ };
301
+ groups.set(target.stepId, group);
302
+ slots.push(group);
303
+ }
304
+ // Tested here rather than while the slots are built: a clause commits to its
305
+ // groups in one pass and renders them in the next, so an entry-time test let
306
+ // every group of an already-committed clause render in full, and the bound
307
+ // grew by the group count rather than by one expansion.
308
+ const phrases = slots.map((slot) => {
309
+ if (!('stepId' in slot))
310
+ return renderBindingValue(slot, level);
311
+ return exhausted(budget)
312
+ ? unexpandedGroup(slot)
313
+ : renderCaptureGroup(slot, level, index, rendering, budget);
314
+ });
315
+ return `with ${joinWithAnd(phrases)}`;
118
316
  }
119
317
  // Parenthesized rather than comma-joined onto the operation reference: a
120
318
  // step reference can itself be joined with other phrases at a higher level
@@ -122,9 +320,11 @@ function bindingClause(step, level) {
122
320
  // already carries its own internal ", and" separators. A second comma layer
123
321
  // on top would produce indistinguishable comma soup once two or more
124
322
  // phrases are joined.
125
- function stepReferenceAtLevel(step, operation, level) {
323
+ function stepReferenceAtLevel(step, operation, level, index, rendering = new Set(), budget = newBudget(RENDER_BUDGET_LIMITS[0])) {
126
324
  const base = operationReference(operation);
127
- const clause = bindingClause(step, level);
325
+ // The step being rendered joins the path before its own clause is built, so
326
+ // a self-capture falls back on the first hop.
327
+ const clause = bindingClause(step, level, index, new Set([...rendering, step.stepId]), budget);
128
328
  return clause === null ? base : `${base} (${clause})`;
129
329
  }
130
330
  /**
@@ -140,14 +340,42 @@ function stepReferenceAtLevel(step, operation, level) {
140
340
  * duplicate `stepId` check if two siblings still collide after full
141
341
  * escalation.
142
342
  */
143
- export function renderStepReference(step, operation, siblings) {
144
- for (const level of ESCALATION_LEVELS) {
145
- const phrases = siblings.map((sibling) => stepReferenceAtLevel(sibling, operation, level));
146
- if (new Set(phrases).size === phrases.length) {
147
- return stepReferenceAtLevel(step, operation, level);
343
+ export function renderStepReference(step, operation, siblings, index) {
344
+ // Two passes over the ladder, at the ordinary budget and then at the ceiling.
345
+ // The retry fires only when every level collided AND some render had an
346
+ // expansion suppressed, which is what breaks the coupling the budget would
347
+ // otherwise introduce: without it, growing a literal on a step unrelated to
348
+ // either sibling can starve the expansion that separates them and make an
349
+ // otherwise-sealable contract throw. A collision with nothing suppressed is
350
+ // the declared structure's own tie, and a larger budget renders the same
351
+ // text, so that case leaves the loop rather than paying for a second pass.
352
+ for (const limit of RENDER_BUDGET_LIMITS) {
353
+ let starved = false;
354
+ for (const level of ESCALATION_LEVELS) {
355
+ const rendered = siblings.map((sibling) => {
356
+ const budget = newBudget(limit);
357
+ return {
358
+ phrase: stepReferenceAtLevel(sibling, operation, level, index, new Set(), budget),
359
+ spent: budget.spent,
360
+ };
361
+ });
362
+ const phrases = rendered.map((entry) => entry.phrase);
363
+ if (new Set(phrases).size === phrases.length) {
364
+ return stepReferenceAtLevel(step, operation, level, index, new Set(), newBudget(limit));
365
+ }
366
+ if (rendered.some((entry) => entry.spent))
367
+ starved = true;
148
368
  }
369
+ if (!starved)
370
+ break;
149
371
  }
150
- throw new TypeError(`two or more steps invoking operation "${operation.operationId}" that this direction references render to the same derived reference even fully escalated; the declared structure does not distinguish them`);
372
+ // Coded rather than a bare `TypeError`. A contract reaching here passed
373
+ // every compile check and then failed at seal time with a stack trace, and
374
+ // the defect is an ordinary authoring fault: two steps the declared
375
+ // structure does not tell apart. `checkStepReferenceReducibility` runs this
376
+ // same ladder at compile time so the fault is reported before seal, and
377
+ // this throw is what it catches.
378
+ throw new StructuralFailure('irreducible-step-reference', `EvalContract.interactionPlan[operationId=${operation.operationId}]`, `two or more steps invoking operation "${operation.operationId}" that one direction references render to the same derived reference even fully escalated; the declared structure does not distinguish them (AD-16, AD-3)`);
151
379
  }
152
380
  function siblingsByOperation(resolved) {
153
381
  const map = new Map();
@@ -214,11 +442,25 @@ function localTargetPhrase(target) {
214
442
  return field !== null
215
443
  ? `the ${field} field of its standard error`
216
444
  : 'the standard error you obtained';
445
+ case 'artifact': {
446
+ if (target.artifactId === null) {
447
+ // Unreachable: `parseEvidenceTarget` sets this exactly when the
448
+ // channel is 'artifact'.
449
+ throw new TypeError('artifact evidence target names no artifact');
450
+ }
451
+ // The artifact is named for the same reason the transport channel
452
+ // is: two files can carry the same field name, and an evaluator
453
+ // reading "its findings field" twice cannot tell which file it
454
+ // means.
455
+ return field !== null
456
+ ? `the ${field} field of the ${target.artifactId} it wrote`
457
+ : `the ${target.artifactId} it wrote`;
458
+ }
217
459
  }
218
460
  }
219
- function fullTargetPhrase(resolved, siblingsOf) {
461
+ function fullTargetPhrase(resolved, siblingsOf, index) {
220
462
  const local = localTargetPhrase(resolved.target);
221
- const stepRef = renderStepReference(resolved.step, resolved.operation, siblingsOf(resolved.operation.operationId));
463
+ const stepRef = renderStepReference(resolved.step, resolved.operation, siblingsOf(resolved.operation.operationId), index);
222
464
  const preposition = resolved.target.channel === 'call-inputs' ? 'to' : 'from';
223
465
  return `${local} ${preposition} ${stepRef}`;
224
466
  }
@@ -228,7 +470,12 @@ function fullTargetPhrase(resolved, siblingsOf) {
228
470
  // 'b']`). Both would join to the same `".../a/b"` string, producing a false
229
471
  // tie in `sentFirstOrder` below.
230
472
  function channelSignature(target) {
231
- return JSON.stringify([target.channel, target.transportChannel, target.tail]);
473
+ return JSON.stringify([
474
+ target.channel,
475
+ target.transportChannel,
476
+ target.artifactId,
477
+ target.tail,
478
+ ]);
232
479
  }
233
480
  // A total order over one pair, independent of argument order. The final
234
481
  // reference-comparison rung fires only on a genuine tie, such as the same
@@ -236,7 +483,7 @@ function channelSignature(target) {
236
483
  // never degenerates to argument order, since colliding same-operation
237
484
  // siblings already throw in `renderStepReference`, and different operations
238
485
  // always humanize to different names.
239
- function sentFirstOrder(a, b, siblingsOf) {
486
+ function sentFirstOrder(a, b, siblingsOf, index) {
240
487
  const rank = (r) => r.target.channel === 'call-inputs' ? 0 : 1;
241
488
  const rankA = rank(a);
242
489
  const rankB = rank(b);
@@ -246,8 +493,8 @@ function sentFirstOrder(a, b, siblingsOf) {
246
493
  const signatureB = channelSignature(b.target);
247
494
  if (signatureA !== signatureB)
248
495
  return signatureA < signatureB ? [a, b] : [b, a];
249
- const referenceA = renderStepReference(a.step, a.operation, siblingsOf(a.operation.operationId));
250
- const referenceB = renderStepReference(b.step, b.operation, siblingsOf(b.operation.operationId));
496
+ const referenceA = renderStepReference(a.step, a.operation, siblingsOf(a.operation.operationId), index);
497
+ const referenceB = renderStepReference(b.step, b.operation, siblingsOf(b.operation.operationId), index);
251
498
  return referenceA <= referenceB ? [a, b] : [b, a];
252
499
  }
253
500
  // Two evidence targets whose steps are declared in an `after` relationship
@@ -340,19 +587,19 @@ function groupResolvedTargets(resolved) {
340
587
  }
341
588
  return groups;
342
589
  }
343
- function renderPhraseGroup(group, siblingsOf) {
590
+ function renderPhraseGroup(group, siblingsOf, index) {
344
591
  if (group.kind === 'temporal-pair') {
345
- const [first, second] = sentFirstOrder(group.a, group.b, siblingsOf);
346
- return `${fullTargetPhrase(first, siblingsOf)}, compared with ${fullTargetPhrase(second, siblingsOf)}`;
592
+ const [first, second] = sentFirstOrder(group.a, group.b, siblingsOf, index);
593
+ return `${fullTargetPhrase(first, siblingsOf, index)}, compared with ${fullTargetPhrase(second, siblingsOf, index)}`;
347
594
  }
348
595
  const first = group.resolved[0];
349
596
  if (first === undefined) {
350
597
  throw new TypeError('evidence-target group is empty');
351
598
  }
352
599
  if (group.resolved.length === 1) {
353
- return fullTargetPhrase(first, siblingsOf);
600
+ return fullTargetPhrase(first, siblingsOf, index);
354
601
  }
355
- const stepRef = renderStepReference(first.step, first.operation, siblingsOf(first.operation.operationId));
602
+ const stepRef = renderStepReference(first.step, first.operation, siblingsOf(first.operation.operationId), index);
356
603
  // Sorted, so a same-step group's field order is permutation-invariant like
357
604
  // the top-level join below; in declaration order, permuting
358
605
  // `evidenceTargets` would change which field is named first here.
@@ -378,6 +625,6 @@ export function renderEvidenceReferences(pointers, index) {
378
625
  const siblings = siblingsByOperation(resolved);
379
626
  const siblingsOf = (operationId) => siblings.get(operationId) ?? [];
380
627
  const groups = groupResolvedTargets(resolved);
381
- const phrases = groups.map((group) => renderPhraseGroup(group, siblingsOf));
628
+ const phrases = groups.map((group) => renderPhraseGroup(group, siblingsOf, index));
382
629
  return joinWithAnd([...phrases].sort());
383
630
  }
@@ -1,7 +1,7 @@
1
1
  /** resolves a pointer to its step and operation; nothing about reachability. */
2
- import type { Operation, PermittedInterface } from '../schemas/interface.ts';
2
+ import type { AnyOperation, CommandOperation, InterfaceKindName, Operation, PermittedInterface } from '../schemas/interface.ts';
3
3
  import type { InteractionStep } from '../schemas/plan.ts';
4
- import { type EvidenceChannelName, type TransportChannelName } from '../schemas/pointer.ts';
4
+ import { type EvidenceChannelName, type InputChannelName } from '../schemas/pointer.ts';
5
5
  /**
6
6
  * Exported so `core/evaluate/evidence-resolution.ts` decodes pointer tails
7
7
  * with these same two functions, avoiding a second private copy (the drift
@@ -17,7 +17,8 @@ export declare const decodeTail: (tailSource: string) => readonly string[];
17
17
  export type EvidenceTarget = {
18
18
  stepId: string;
19
19
  channel: EvidenceChannelName;
20
- transportChannel: TransportChannelName | null;
20
+ transportChannel: InputChannelName | null;
21
+ artifactId: string | null;
21
22
  tail: readonly string[];
22
23
  };
23
24
  /**
@@ -34,10 +35,18 @@ export declare function parseEvidenceTarget(pointer: string): EvidenceTarget;
34
35
  * about reachability or channel typing; the general addressing-grammar
35
36
  * resolver lives in `core/evaluate/evidence-resolution.ts` and reachability
36
37
  * in `core/compile/reachability.ts`.
38
+ *
39
+ * `operationOf` stays narrow on purpose. It hands a resolved `Operation` to
40
+ * every downstream consumer, so widening its return type to the operation
41
+ * union would retype seventeen files at once. A command operation resolves to
42
+ * `undefined` from it and to a value from `commandOperationOf`, and each
43
+ * caller that has to branch reads the declaring kind first.
37
44
  */
38
45
  export type PlanIndex = {
39
46
  stepOf: (stepId: string) => InteractionStep | undefined;
40
47
  operationOf: (operationId: string) => Operation | undefined;
48
+ commandOperationOf: (operationId: string) => CommandOperation | undefined;
49
+ interfaceKindOf: (operationId: string) => InterfaceKindName | undefined;
41
50
  stepsUsing: (operationId: string) => readonly InteractionStep[];
42
51
  };
43
52
  export type PlanIndexOptions = {
@@ -59,5 +68,11 @@ export declare function buildPlanIndex(interactionPlan: readonly InteractionStep
59
68
  * `RuntimeFault`.
60
69
  */
61
70
  export declare function resolveStep(index: PlanIndex, stepId: string): InteractionStep;
71
+ /**
72
+ * The declared operation of whichever kind, for the callers that read only
73
+ * fields both shapes carry. Callers reading a kind-specific field ask
74
+ * `interfaceKindOf` first and then take the matching accessor.
75
+ */
76
+ export declare const anyOperationOf: (index: PlanIndex, operationId: string) => AnyOperation | undefined;
62
77
  /** Resolves an operation id through the index or throws. See `resolveStep`. */
63
78
  export declare function resolveOperation(index: PlanIndex, operationId: string): Operation;
@@ -1,4 +1,5 @@
1
- import { SCALAR_CHANNELS, TAIL_BEARING_CHANNELS, TRANSPORT_CHANNELS, TRANSPORT_ROOTED_CHANNEL, } from '../schemas/pointer.js';
1
+ import { operationsOf } from '../schemas/interface.js';
2
+ import { IDENTIFIER_ROOTED_CHANNEL, INPUT_CHANNELS, SCALAR_CHANNELS, TAIL_BEARING_CHANNELS, TRANSPORT_ROOTED_CHANNEL, } from '../schemas/pointer.js';
2
3
  import { IDENTIFIER_CHARSET_SOURCE } from '../schemas/primitives.js';
3
4
  // An RFC 6901 reference token, respelled here because pointer.ts keeps its
4
5
  // own TOKEN/TAIL fragments private. IDENTIFIER_CHARSET_SOURCE and the channel
@@ -14,10 +15,10 @@ const alternation = (members) => members.join('|');
14
15
  // Named capture groups replace positional indices, avoiding a fragile
15
16
  // dependency on `IDENTIFIER_CHARSET_SOURCE` staying free of its own capturing
16
17
  // groups.
17
- const EVIDENCE_TARGET_PATTERN = new RegExp(`^/interactions/(?<stepId>${IDENTIFIER_CHARSET_SOURCE})/(?:(?<tailBearingChannel>${alternation(TAIL_BEARING_CHANNELS)})(?<tailBearingTail>${TAIL_SOURCE})|(?<scalarChannel>${alternation(SCALAR_CHANNELS)})|${TRANSPORT_ROOTED_CHANNEL}/(?<transportChannel>${alternation(TRANSPORT_CHANNELS)})(?<callInputsTail>${TAIL_SOURCE}))$`);
18
+ const EVIDENCE_TARGET_PATTERN = new RegExp(`^/interactions/(?<stepId>${IDENTIFIER_CHARSET_SOURCE})/(?:(?<tailBearingChannel>${alternation(TAIL_BEARING_CHANNELS)})(?<tailBearingTail>${TAIL_SOURCE})|(?<scalarChannel>${alternation(SCALAR_CHANNELS)})|${TRANSPORT_ROOTED_CHANNEL}/(?<transportChannel>${alternation(INPUT_CHANNELS)})(?<callInputsTail>${TAIL_SOURCE})|${IDENTIFIER_ROOTED_CHANNEL}/(?<artifactId>${IDENTIFIER_CHARSET_SOURCE})(?<artifactTail>${TAIL_SOURCE}))$`);
18
19
  const isEvidenceChannel = (value) => TAIL_BEARING_CHANNELS.includes(value) ||
19
20
  SCALAR_CHANNELS.includes(value);
20
- const isTransportChannel = (value) => TRANSPORT_CHANNELS.includes(value);
21
+ const isInputChannel = (value) => INPUT_CHANNELS.includes(value);
21
22
  /**
22
23
  * Exported so `core/evaluate/evidence-resolution.ts` decodes pointer tails
23
24
  * with these same two functions, avoiding a second private copy (the drift
@@ -47,6 +48,7 @@ export function parseEvidenceTarget(pointer) {
47
48
  stepId,
48
49
  channel: groups.scalarChannel,
49
50
  transportChannel: null,
51
+ artifactId: null,
50
52
  tail: [],
51
53
  };
52
54
  }
@@ -59,21 +61,32 @@ export function parseEvidenceTarget(pointer) {
59
61
  stepId,
60
62
  channel: groups.tailBearingChannel,
61
63
  transportChannel: null,
64
+ artifactId: null,
62
65
  tail: decodeTail(groups.tailBearingTail ?? ''),
63
66
  };
64
67
  }
65
68
  if (groups.transportChannel !== undefined) {
66
- if (!isTransportChannel(groups.transportChannel)) {
67
- // Unreachable: TRANSPORT_CHANNELS is exactly what this group can match.
69
+ if (!isInputChannel(groups.transportChannel)) {
70
+ // Unreachable: INPUT_CHANNELS is exactly what this group can match.
68
71
  throw new TypeError(`call-inputs evidence target names no transport channel: ${pointer}`);
69
72
  }
70
73
  return {
71
74
  stepId,
72
75
  channel: 'call-inputs',
73
76
  transportChannel: groups.transportChannel,
77
+ artifactId: null,
74
78
  tail: decodeTail(groups.callInputsTail ?? ''),
75
79
  };
76
80
  }
81
+ if (groups.artifactId !== undefined) {
82
+ return {
83
+ stepId,
84
+ channel: IDENTIFIER_ROOTED_CHANNEL,
85
+ transportChannel: null,
86
+ artifactId: groups.artifactId,
87
+ tail: decodeTail(groups.artifactTail ?? ''),
88
+ };
89
+ }
77
90
  // Unreachable: the pattern's three branches are exhaustive once stepId matched.
78
91
  throw new TypeError(`not an interaction-rooted evidence pointer of a recognized channel: ${pointer}`);
79
92
  }
@@ -109,25 +122,42 @@ export function buildPlanIndex(interactionPlan, permittedInterfaces, options = {
109
122
  }
110
123
  }
111
124
  const operations = new Map();
125
+ const commandOperations = new Map();
126
+ const kinds = new Map();
112
127
  const duplicateOperationIds = new Set();
113
128
  for (const iface of permittedInterfaces) {
114
- for (const operation of iface.operations) {
115
- if (operations.has(operation.operationId) ||
129
+ for (const operation of operationsOf(iface)) {
130
+ if (kinds.has(operation.operationId) ||
116
131
  duplicateOperationIds.has(operation.operationId)) {
117
132
  if (duplicateIds === 'throw') {
118
133
  throw new TypeError(`duplicate operation id across permitted interfaces: ${operation.operationId}`);
119
134
  }
120
135
  operations.delete(operation.operationId);
136
+ commandOperations.delete(operation.operationId);
137
+ kinds.delete(operation.operationId);
121
138
  duplicateOperationIds.add(operation.operationId);
122
139
  }
123
140
  else {
124
- operations.set(operation.operationId, operation);
141
+ kinds.set(operation.operationId, iface.kind);
142
+ // Sorted into the two maps by the interface's own kind rather
143
+ // than by probing the operation for a field: `web` and `mcp`
144
+ // carry the api operation shape and belong in the same map as
145
+ // `api`, since every consumer of a resolved operation reads
146
+ // the same declared fields off all three.
147
+ if (iface.kind === 'cli') {
148
+ commandOperations.set(operation.operationId, operation);
149
+ }
150
+ else {
151
+ operations.set(operation.operationId, operation);
152
+ }
125
153
  }
126
154
  }
127
155
  }
128
156
  return {
129
157
  stepOf: (stepId) => steps.get(stepId),
130
158
  operationOf: (operationId) => operations.get(operationId),
159
+ commandOperationOf: (operationId) => commandOperations.get(operationId),
160
+ interfaceKindOf: (operationId) => kinds.get(operationId),
131
161
  stepsUsing: (operationId) => stepsByOperation.get(operationId) ?? [],
132
162
  };
133
163
  }
@@ -145,6 +175,12 @@ export function resolveStep(index, stepId) {
145
175
  }
146
176
  return step;
147
177
  }
178
+ /**
179
+ * The declared operation of whichever kind, for the callers that read only
180
+ * fields both shapes carry. Callers reading a kind-specific field ask
181
+ * `interfaceKindOf` first and then take the matching accessor.
182
+ */
183
+ export const anyOperationOf = (index, operationId) => index.operationOf(operationId) ?? index.commandOperationOf(operationId);
148
184
  /** Resolves an operation id through the index or throws. See `resolveStep`. */
149
185
  export function resolveOperation(index, operationId) {
150
186
  const operation = index.operationOf(operationId);
@@ -75,8 +75,10 @@ export function seal(contract) {
75
75
  // and stateless with no "prior brief" argument (AD-12), so the only
76
76
  // honest artifact is a lineage root: `parentDigest` null,
77
77
  // `revisionCount` 0, independent of the contract's own lineage.
78
- // `schemaVersion` is the brief schema's current version.
79
- schemaVersion: 1,
78
+ // `schemaVersion` is the brief schema's current version; 2 since owed
79
+ // item 3 added `principals` as a required field, which AD-11 counts as
80
+ // a breaking change.
81
+ schemaVersion: 2,
80
82
  parentDigest: null,
81
83
  revisionCount: 0,
82
84
  // A plain digest of the literal input: two differently-ordered
@@ -98,8 +100,11 @@ export function seal(contract) {
98
100
  // Copied rather than aliased, for the same reason as `behaviors` above.
99
101
  budgets: { ...contract.budgets },
100
102
  // Sorted lexicographically; sort-key duplicates are impossible here
101
- // since equal strings are interchangeable, so no duplicate guard.
103
+ // since equal strings are interchangeable, so no duplicate guard. The
104
+ // principal names are sorted the same way rather than through
105
+ // `sortedByKey`, for the same reason: there is no key to guard.
102
106
  safetyLimits: [...contract.safetyLimits].sort(),
107
+ principals: Object.keys(contract.testData.principals ?? {}).sort(),
103
108
  probeStepBound: contract.probeStepBound,
104
109
  };
105
110
  // The parse returns Zod's own deep clone, so the freeze lands on the