eval-quality 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +355 -0
  3. package/corpus/dev/README.md +36 -0
  4. package/corpus/dev/compile-seal-example/brief.json +1 -0
  5. package/corpus/dev/compile-seal-example/contract.json +1 -0
  6. package/corpus/dev/contracts/absent-collection-locations.json +1 -0
  7. package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
  8. package/corpus/dev/contracts/absent-success-indicator.json +1 -0
  9. package/corpus/dev/contracts/empty-channel-roles.json +1 -0
  10. package/corpus/dev/contracts/empty-collection-locations.json +1 -0
  11. package/corpus/dev/contracts/empty-request-shapes.json +1 -0
  12. package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -0
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -0
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -0
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -0
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
  19. package/corpus/dev/contracts/satisfied-declarations.json +1 -0
  20. package/corpus/dev/contracts/single-required-response-key.json +1 -0
  21. package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
  22. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
  23. package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
  24. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
  25. package/corpus/dev/index.json +1 -0
  26. package/dist/adapters/index.d.ts +12 -0
  27. package/dist/adapters/index.js +3 -0
  28. package/dist/adapters/local-corpus-adapter.d.ts +7 -0
  29. package/dist/adapters/local-corpus-adapter.js +78 -0
  30. package/dist/adapters/node-file-system-adapter.d.ts +7 -0
  31. package/dist/adapters/node-file-system-adapter.js +39 -0
  32. package/dist/adapters/port-boundary.d.ts +18 -0
  33. package/dist/adapters/port-boundary.js +83 -0
  34. package/dist/adapters/system-clock-adapter.d.ts +10 -0
  35. package/dist/adapters/system-clock-adapter.js +18 -0
  36. package/dist/application/compile.d.ts +4 -0
  37. package/dist/application/compile.js +24 -0
  38. package/dist/application/diagnostics.d.ts +22 -0
  39. package/dist/application/diagnostics.js +18 -0
  40. package/dist/application/index.d.ts +23 -0
  41. package/dist/application/index.js +17 -0
  42. package/dist/application/invoke-port.d.ts +2 -0
  43. package/dist/application/invoke-port.js +45 -0
  44. package/dist/application/preflight.d.ts +27 -0
  45. package/dist/application/preflight.js +131 -0
  46. package/dist/application/seal.d.ts +4 -0
  47. package/dist/application/seal.js +28 -0
  48. package/dist/application/serialize.d.ts +6 -0
  49. package/dist/application/serialize.js +9 -0
  50. package/dist/cli/arguments.d.ts +28 -0
  51. package/dist/cli/arguments.js +167 -0
  52. package/dist/cli/exit-codes.d.ts +36 -0
  53. package/dist/cli/exit-codes.js +39 -0
  54. package/dist/cli/main.d.ts +2 -0
  55. package/dist/cli/main.js +143 -0
  56. package/dist/cli/render.d.ts +26 -0
  57. package/dist/cli/render.js +53 -0
  58. package/dist/cli/run.d.ts +41 -0
  59. package/dist/cli/run.js +203 -0
  60. package/dist/core/canonical/canonicalize.d.ts +1 -0
  61. package/dist/core/canonical/canonicalize.js +113 -0
  62. package/dist/core/canonical/digest.d.ts +7 -0
  63. package/dist/core/canonical/digest.js +81 -0
  64. package/dist/core/canonical/scan-json.d.ts +1 -0
  65. package/dist/core/canonical/scan-json.js +298 -0
  66. package/dist/core/canonical/value-domain.d.ts +4 -0
  67. package/dist/core/canonical/value-domain.js +113 -0
  68. package/dist/core/compile/compile.d.ts +24 -0
  69. package/dist/core/compile/compile.js +67 -0
  70. package/dist/core/compile/declarations.d.ts +5 -0
  71. package/dist/core/compile/declarations.js +22 -0
  72. package/dist/core/compile/expression-legality.d.ts +14 -0
  73. package/dist/core/compile/expression-legality.js +346 -0
  74. package/dist/core/compile/forbidden-inputs.d.ts +5 -0
  75. package/dist/core/compile/forbidden-inputs.js +19 -0
  76. package/dist/core/compile/interface-inventory.d.ts +10 -0
  77. package/dist/core/compile/interface-inventory.js +55 -0
  78. package/dist/core/compile/oracle-alignment.d.ts +14 -0
  79. package/dist/core/compile/oracle-alignment.js +93 -0
  80. package/dist/core/compile/reachability.d.ts +17 -0
  81. package/dist/core/compile/reachability.js +177 -0
  82. package/dist/core/compile/rubrics.d.ts +55 -0
  83. package/dist/core/compile/rubrics.js +198 -0
  84. package/dist/core/compile/scripting-bound.d.ts +5 -0
  85. package/dist/core/compile/scripting-bound.js +151 -0
  86. package/dist/core/compile/sensitivity-witness.d.ts +73 -0
  87. package/dist/core/compile/sensitivity-witness.js +232 -0
  88. package/dist/core/compile/waivers.d.ts +3 -0
  89. package/dist/core/compile/waivers.js +21 -0
  90. package/dist/core/coverage/coverage.d.ts +18 -0
  91. package/dist/core/coverage/coverage.js +47 -0
  92. package/dist/core/coverage/relevance.d.ts +75 -0
  93. package/dist/core/coverage/relevance.js +186 -0
  94. package/dist/core/coverage/rules.d.ts +7 -0
  95. package/dist/core/coverage/rules.js +18 -0
  96. package/dist/core/coverage/satisfaction.d.ts +107 -0
  97. package/dist/core/coverage/satisfaction.js +415 -0
  98. package/dist/core/coverage/table.d.ts +26 -0
  99. package/dist/core/coverage/table.js +222 -0
  100. package/dist/core/declared-inputs.d.ts +16 -0
  101. package/dist/core/declared-inputs.js +18 -0
  102. package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
  103. package/dist/core/evaluate/evidence-resolution.js +153 -0
  104. package/dist/core/evaluate/operators.d.ts +87 -0
  105. package/dist/core/evaluate/operators.js +350 -0
  106. package/dist/core/evaluate/resolution.d.ts +40 -0
  107. package/dist/core/evaluate/resolution.js +336 -0
  108. package/dist/core/evaluate/resolved-value.d.ts +9 -0
  109. package/dist/core/evaluate/resolved-value.js +6 -0
  110. package/dist/core/failure-codes.d.ts +18 -0
  111. package/dist/core/failure-codes.js +50 -0
  112. package/dist/core/lineage/chain.d.ts +49 -0
  113. package/dist/core/lineage/chain.js +210 -0
  114. package/dist/core/lineage/freeze.d.ts +12 -0
  115. package/dist/core/lineage/freeze.js +35 -0
  116. package/dist/core/lineage/stage-table.d.ts +43 -0
  117. package/dist/core/lineage/stage-table.js +106 -0
  118. package/dist/core/preflight/plan.d.ts +67 -0
  119. package/dist/core/preflight/plan.js +313 -0
  120. package/dist/core/preflight/projection.d.ts +40 -0
  121. package/dist/core/preflight/projection.js +96 -0
  122. package/dist/core/preflight/reduce.d.ts +8 -0
  123. package/dist/core/preflight/reduce.js +180 -0
  124. package/dist/core/preflight/witness-evidence.d.ts +42 -0
  125. package/dist/core/preflight/witness-evidence.js +88 -0
  126. package/dist/core/probe/target-policy.d.ts +63 -0
  127. package/dist/core/probe/target-policy.js +328 -0
  128. package/dist/core/schemas/artifact-reference.d.ts +19 -0
  129. package/dist/core/schemas/artifact-reference.js +30 -0
  130. package/dist/core/schemas/artifact.d.ts +1153 -0
  131. package/dist/core/schemas/artifact.js +84 -0
  132. package/dist/core/schemas/constraint-ledger.d.ts +57 -0
  133. package/dist/core/schemas/constraint-ledger.js +143 -0
  134. package/dist/core/schemas/eval-contract.d.ts +402 -0
  135. package/dist/core/schemas/eval-contract.js +147 -0
  136. package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
  137. package/dist/core/schemas/evaluator-configuration.js +43 -0
  138. package/dist/core/schemas/evidence-artifact.d.ts +471 -0
  139. package/dist/core/schemas/evidence-artifact.js +232 -0
  140. package/dist/core/schemas/expression.d.ts +197 -0
  141. package/dist/core/schemas/expression.js +284 -0
  142. package/dist/core/schemas/faults.d.ts +9 -0
  143. package/dist/core/schemas/faults.js +32 -0
  144. package/dist/core/schemas/interface.d.ts +417 -0
  145. package/dist/core/schemas/interface.js +115 -0
  146. package/dist/core/schemas/isolation-manifest.d.ts +136 -0
  147. package/dist/core/schemas/isolation-manifest.js +92 -0
  148. package/dist/core/schemas/lineage.d.ts +21 -0
  149. package/dist/core/schemas/lineage.js +28 -0
  150. package/dist/core/schemas/oracle.d.ts +72 -0
  151. package/dist/core/schemas/oracle.js +35 -0
  152. package/dist/core/schemas/plan.d.ts +115 -0
  153. package/dist/core/schemas/plan.js +58 -0
  154. package/dist/core/schemas/pointer.d.ts +63 -0
  155. package/dist/core/schemas/pointer.js +94 -0
  156. package/dist/core/schemas/port-messages.d.ts +103 -0
  157. package/dist/core/schemas/port-messages.js +90 -0
  158. package/dist/core/schemas/preflight-verdict.d.ts +60 -0
  159. package/dist/core/schemas/preflight-verdict.js +38 -0
  160. package/dist/core/schemas/primitives.d.ts +78 -0
  161. package/dist/core/schemas/primitives.js +121 -0
  162. package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
  163. package/dist/core/schemas/private-artifact-manifest.js +46 -0
  164. package/dist/core/schemas/probe-body.d.ts +25 -0
  165. package/dist/core/schemas/probe-body.js +23 -0
  166. package/dist/core/schemas/probe-policy.d.ts +76 -0
  167. package/dist/core/schemas/probe-policy.js +33 -0
  168. package/dist/core/schemas/probe.d.ts +189 -0
  169. package/dist/core/schemas/probe.js +82 -0
  170. package/dist/core/schemas/publish.d.ts +30 -0
  171. package/dist/core/schemas/publish.js +135 -0
  172. package/dist/core/schemas/reference-set.d.ts +18 -0
  173. package/dist/core/schemas/reference-set.js +27 -0
  174. package/dist/core/schemas/rubric.d.ts +62 -0
  175. package/dist/core/schemas/rubric.js +68 -0
  176. package/dist/core/schemas/scoring-policy.d.ts +28 -0
  177. package/dist/core/schemas/scoring-policy.js +51 -0
  178. package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
  179. package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
  180. package/dist/core/schemas/sealed-run-record.d.ts +382 -0
  181. package/dist/core/schemas/sealed-run-record.js +215 -0
  182. package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
  183. package/dist/core/schemas/sensitivity-witness.js +86 -0
  184. package/dist/core/schemas/verdict.d.ts +25 -0
  185. package/dist/core/schemas/verdict.js +20 -0
  186. package/dist/core/schemas/waiver.d.ts +17 -0
  187. package/dist/core/schemas/waiver.js +24 -0
  188. package/dist/core/seal/derived-reference.d.ts +36 -0
  189. package/dist/core/seal/derived-reference.js +383 -0
  190. package/dist/core/seal/direction-prose.d.ts +13 -0
  191. package/dist/core/seal/direction-prose.js +120 -0
  192. package/dist/core/seal/plan-index.d.ts +63 -0
  193. package/dist/core/seal/plan-index.js +155 -0
  194. package/dist/core/seal/scripting-audit.d.ts +15 -0
  195. package/dist/core/seal/scripting-audit.js +51 -0
  196. package/dist/core/seal/seal.d.ts +18 -0
  197. package/dist/core/seal/seal.js +155 -0
  198. package/dist/core/stage-contracts.d.ts +30 -0
  199. package/dist/core/stage-contracts.js +1 -0
  200. package/dist/index.d.ts +15 -0
  201. package/dist/index.js +22 -0
  202. package/dist/ports/clock-port.d.ts +13 -0
  203. package/dist/ports/clock-port.js +7 -0
  204. package/dist/ports/corpus-port.d.ts +16 -0
  205. package/dist/ports/corpus-port.js +7 -0
  206. package/dist/ports/environment-probe-port.d.ts +76 -0
  207. package/dist/ports/environment-probe-port.js +32 -0
  208. package/dist/ports/file-system-port.d.ts +27 -0
  209. package/dist/ports/file-system-port.js +11 -0
  210. package/dist/ports/port.d.ts +33 -0
  211. package/dist/ports/port.js +14 -0
  212. package/dist/testing/conformance.d.ts +88 -0
  213. package/dist/testing/conformance.js +281 -0
  214. package/dist/testing/index.d.ts +26 -0
  215. package/dist/testing/index.js +18 -0
  216. package/dist/testing/probe-conformance.d.ts +45 -0
  217. package/dist/testing/probe-conformance.js +186 -0
  218. package/package.json +130 -0
  219. package/schemas/artifact-reference.schema.json +64 -0
  220. package/schemas/eval-contract.schema.json +2145 -0
  221. package/schemas/evaluator-configuration.schema.json +199 -0
  222. package/schemas/evidence-artifact.schema.json +1229 -0
  223. package/schemas/isolation-manifest.schema.json +394 -0
  224. package/schemas/preflight-verdict.schema.json +114 -0
  225. package/schemas/private-artifact-manifest.schema.json +103 -0
  226. package/schemas/probe.schema.json +1151 -0
  227. package/schemas/rubric.schema.json +148 -0
  228. package/schemas/scoring-policy.schema.json +90 -0
  229. package/schemas/sealed-evaluator-brief.schema.json +268 -0
  230. package/schemas/sealed-run-record.schema.json +875 -0
@@ -0,0 +1,383 @@
1
+ import { parseEvidenceTarget, resolveOperation, resolveStep, } from './plan-index.js';
2
+ function resolveEvidenceTarget(pointer, index) {
3
+ const target = parseEvidenceTarget(pointer);
4
+ const step = resolveStep(index, target.stepId);
5
+ const operation = resolveOperation(index, step.operationId);
6
+ return { target, step, operation };
7
+ }
8
+ // ---- joining ---------------------------------------------------------
9
+ function joinWithAnd(items) {
10
+ const [first, ...rest] = items;
11
+ if (first === undefined)
12
+ return '';
13
+ if (rest.length === 0)
14
+ return first;
15
+ const last = rest[rest.length - 1];
16
+ if (rest.length === 1)
17
+ return `${first} and ${last}`;
18
+ const middle = rest.slice(0, -1);
19
+ return `${[first, ...middle].join(', ')}, and ${last}`;
20
+ }
21
+ // ---- operation identity -----------------------------------------------
22
+ // Humanizing a kebab-case operationId is injective on distinct ids, and
23
+ // `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.
27
+ function operationReference(operation) {
28
+ return `the ${operation.operationId.split('-').join(' ')} endpoint`;
29
+ }
30
+ const ESCALATION_LEVELS = [
31
+ 'generic',
32
+ 'kind',
33
+ 'literal',
34
+ ];
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
42
+ // never depends on a binding map's insertion order and the rendered prose
43
+ // stays permutation-invariant.
44
+ function bindingEntries(step) {
45
+ const entries = [];
46
+ for (const transportChannel of TRANSPORT_ORDER) {
47
+ const map = step.inputBinding[transportChannel];
48
+ if (map === null)
49
+ continue;
50
+ for (const key of Object.keys(map).sort()) {
51
+ const value = map[key];
52
+ if (value !== undefined)
53
+ entries.push({ transportChannel, key, value });
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+ // The transport channel is part of the rendered name as well as the sort
59
+ // key: two bindings can share a parameter name across channels (path.id and
60
+ // query.id), and without the qualifier both would render as "the supplied
61
+ // id", hiding two different bindings behind identical text.
62
+ function entryName(entry) {
63
+ return `${entry.transportChannel} ${entry.key}`;
64
+ }
65
+ function isTypeViolating(value) {
66
+ return 'matcher' in value && value.matcher === 'type-violating';
67
+ }
68
+ // Matches `canonicalize.ts`'s key-sorting rule (AD-27) so two contracts
69
+ // differing only in one literal binding's declared key order still render
70
+ // identical prose from that one canonical digest.
71
+ function canonicalizeForDisplay(value) {
72
+ if (Array.isArray(value))
73
+ return value.map(canonicalizeForDisplay);
74
+ if (value !== null && typeof value === 'object') {
75
+ const sorted = {};
76
+ for (const key of Object.keys(value).sort()) {
77
+ sorted[key] = canonicalizeForDisplay(value[key]);
78
+ }
79
+ return sorted;
80
+ }
81
+ return value;
82
+ }
83
+ function formatLiteral(literal) {
84
+ return JSON.stringify(canonicalizeForDisplay(literal));
85
+ }
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.
90
+ function renderBindingValue(entry, level) {
91
+ const name = entryName(entry);
92
+ if (isTypeViolating(entry.value)) {
93
+ return `a malformed ${name} value`;
94
+ }
95
+ if (level === 'generic' || 'matcher' in entry.value) {
96
+ return `the supplied ${name}`;
97
+ }
98
+ if (level === 'kind') {
99
+ return `the stated ${name}`;
100
+ }
101
+ return `the ${name} ${formatLiteral(entry.value.literal)}`;
102
+ }
103
+ // Every declared binding key escalates together: narrowing to only the
104
+ // discriminating key would make one step's shown detail depend on which
105
+ // other steps happen to collide with it elsewhere. Naming a step's own
106
+ // bound keys is not the operation inventory AD-16 withholds; that scope is
107
+ // which other calls exist, not this call's own fields.
108
+ //
109
+ // When a type-violating binding is present, the clause names only that one:
110
+ // it is the semantically salient binding, matching AD-16's worked example.
111
+ function bindingClause(step, level) {
112
+ const entries = bindingEntries(step);
113
+ if (entries.length === 0)
114
+ return null;
115
+ const malformed = entries.filter((entry) => isTypeViolating(entry.value));
116
+ const chosen = malformed.length > 0 ? malformed : entries;
117
+ return `with ${joinWithAnd(chosen.map((entry) => renderBindingValue(entry, level)))}`;
118
+ }
119
+ // Parenthesized rather than comma-joined onto the operation reference: a
120
+ // step reference can itself be joined with other phrases at a higher level
121
+ // (`joinWithAnd`, or a temporal pair's "compared with"), and `bindingClause`
122
+ // already carries its own internal ", and" separators. A second comma layer
123
+ // on top would produce indistinguishable comma soup once two or more
124
+ // phrases are joined.
125
+ function stepReferenceAtLevel(step, operation, level) {
126
+ const base = operationReference(operation);
127
+ const clause = bindingClause(step, level);
128
+ return clause === null ? base : `${base} (${clause})`;
129
+ }
130
+ /**
131
+ * Escalates through `ESCALATION_LEVELS` in order until every step in
132
+ * `siblings` renders distinctly.
133
+ *
134
+ * `siblings` is direction-scoped: the caller passes only the steps the
135
+ * current direction's own resolved evidence targets share this operation
136
+ * with, not every step in the plan (AD-16), so an unrelated collision
137
+ * elsewhere never blocks an otherwise renderable direction.
138
+ *
139
+ * Throws the same precondition-violation `TypeError` as `buildPlanIndex`'s
140
+ * duplicate `stepId` check if two siblings still collide after full
141
+ * escalation.
142
+ */
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);
148
+ }
149
+ }
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`);
151
+ }
152
+ function siblingsByOperation(resolved) {
153
+ const map = new Map();
154
+ const seenSteps = new Set();
155
+ for (const entry of resolved) {
156
+ if (seenSteps.has(entry.step.stepId))
157
+ continue;
158
+ seenSteps.add(entry.step.stepId);
159
+ const list = map.get(entry.operation.operationId);
160
+ if (list === undefined)
161
+ map.set(entry.operation.operationId, [entry.step]);
162
+ else
163
+ list.push(entry.step);
164
+ }
165
+ return map;
166
+ }
167
+ // ---- the channel framing ----------------------------------------------
168
+ // An empty token is RFC 6901's legal spelling for a zero-length key but
169
+ // names no field worth naming, so it is filtered out rather than spliced in
170
+ // verbatim (which would produce a blank "its field" or a stray "."). The
171
+ // remaining segments join by "." rather than keeping only the last, so two
172
+ // nested pointers sharing one leaf field name (`.../a/id` and `.../b/id`)
173
+ // never collapse to the same phrase.
174
+ function fieldPath(tail) {
175
+ const named = tail.filter((token) => token !== '');
176
+ return named.length === 0 ? null : named.join('.');
177
+ }
178
+ function localTargetPhrase(target) {
179
+ const field = fieldPath(target.tail);
180
+ switch (target.channel) {
181
+ case 'response-status':
182
+ return 'its transport status';
183
+ case 'exit-code':
184
+ return 'its exit code';
185
+ case 'call-inputs': {
186
+ if (target.transportChannel === null) {
187
+ // Unreachable: `parseEvidenceTarget` sets this exactly when the
188
+ // channel is 'call-inputs'.
189
+ throw new TypeError('call-inputs evidence target carries no transport channel');
190
+ }
191
+ // The transport channel is named here too, not only in the no-tail
192
+ // fallback below: same path.id / query.id collision `entryName`
193
+ // above guards against.
194
+ return field !== null
195
+ ? `the ${target.transportChannel} ${field} value you sent`
196
+ : `the ${target.transportChannel} you sent`;
197
+ }
198
+ case 'response-body':
199
+ return field !== null
200
+ ? `its ${field} field`
201
+ : 'the response body you obtained';
202
+ case 'response-headers':
203
+ return field !== null
204
+ ? `its ${field} header`
205
+ : 'the response headers you obtained';
206
+ case 'stdout':
207
+ // Distinguished from response-body's "field" wording and from
208
+ // stderr below, so two channels naming the same field never render
209
+ // identical text.
210
+ return field !== null
211
+ ? `the ${field} field of its standard output`
212
+ : 'the standard output you obtained';
213
+ case 'stderr':
214
+ return field !== null
215
+ ? `the ${field} field of its standard error`
216
+ : 'the standard error you obtained';
217
+ }
218
+ }
219
+ function fullTargetPhrase(resolved, siblingsOf) {
220
+ const local = localTargetPhrase(resolved.target);
221
+ const stepRef = renderStepReference(resolved.step, resolved.operation, siblingsOf(resolved.operation.operationId));
222
+ const preposition = resolved.target.channel === 'call-inputs' ? 'to' : 'from';
223
+ return `${local} ${preposition} ${stepRef}`;
224
+ }
225
+ // Encoded as a JSON array rather than a `/`-joined string: a `/`-join would
226
+ // conflate a tail token containing a literal `/` (reachable via the RFC 6901
227
+ // `~1` escape, e.g. `['a/b']`) with an equivalent multi-segment tail (`['a',
228
+ // 'b']`). Both would join to the same `".../a/b"` string, producing a false
229
+ // tie in `sentFirstOrder` below.
230
+ function channelSignature(target) {
231
+ return JSON.stringify([target.channel, target.transportChannel, target.tail]);
232
+ }
233
+ // A total order over one pair, independent of argument order. The final
234
+ // reference-comparison rung fires only on a genuine tie, such as the same
235
+ // field read from two different `after`-linked steps' response bodies; it
236
+ // never degenerates to argument order, since colliding same-operation
237
+ // siblings already throw in `renderStepReference`, and different operations
238
+ // always humanize to different names.
239
+ function sentFirstOrder(a, b, siblingsOf) {
240
+ const rank = (r) => r.target.channel === 'call-inputs' ? 0 : 1;
241
+ const rankA = rank(a);
242
+ const rankB = rank(b);
243
+ if (rankA !== rankB)
244
+ return rankA < rankB ? [a, b] : [b, a];
245
+ const signatureA = channelSignature(a.target);
246
+ const signatureB = channelSignature(b.target);
247
+ if (signatureA !== signatureB)
248
+ 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));
251
+ return referenceA <= referenceB ? [a, b] : [b, a];
252
+ }
253
+ // Two evidence targets whose steps are declared in an `after` relationship
254
+ // render as one relational phrase, so concatenation never discloses which
255
+ // step came first. A same-step multi-target group stays `single` regardless
256
+ // of `after`; forcing a pair there would guess at which target on each side
257
+ // pairs with which.
258
+ //
259
+ // Pairing is resolved from sorted step ids, never from discovery order over
260
+ // `resolved`, so it stays permutation-invariant: when a predecessor is named
261
+ // by more than one successor (AD-39 admits this), the winner is the successor
262
+ // whose own stepId sorts first, and the losing successor renders as its own
263
+ // `single` group.
264
+ function groupResolvedTargets(resolved) {
265
+ const byStep = new Map();
266
+ for (const entry of resolved) {
267
+ const list = byStep.get(entry.step.stepId);
268
+ if (list === undefined)
269
+ byStep.set(entry.step.stepId, [entry]);
270
+ else
271
+ list.push(entry);
272
+ }
273
+ const stepIds = [...byStep.keys()].sort();
274
+ const singleTargetStepIds = new Set(stepIds.filter((id) => byStep.get(id)?.length === 1));
275
+ // Every eligible successor, grouped by the predecessor its `after` names.
276
+ const successorsOf = new Map();
277
+ for (const stepId of singleTargetStepIds) {
278
+ const step = byStep.get(stepId)?.[0]?.step;
279
+ const after = step?.after ?? null;
280
+ if (after === null || !singleTargetStepIds.has(after))
281
+ continue;
282
+ const successors = successorsOf.get(after);
283
+ if (successors === undefined)
284
+ successorsOf.set(after, [stepId]);
285
+ else
286
+ successors.push(stepId);
287
+ }
288
+ // One winner per predecessor: the successor whose own stepId sorts first.
289
+ const chosenSuccessorOf = new Map();
290
+ for (const predecessorId of stepIds) {
291
+ const successors = successorsOf.get(predecessorId);
292
+ const winner = successors === undefined ? undefined : [...successors].sort()[0];
293
+ if (winner !== undefined)
294
+ chosenSuccessorOf.set(predecessorId, winner);
295
+ }
296
+ // `chosenSuccessorOf` maps each predecessor to at most one successor, and
297
+ // each successor wins at most one predecessor, so it forms a disjoint
298
+ // union of simple chains (or cycles, on a malformed `after` graph).
299
+ // Walking only from steps absent from `chosenAsSuccessor` starts at true
300
+ // chain roots and can never enter a cycle, since every cycle node is by
301
+ // construction someone's chosen successor.
302
+ //
303
+ // Each root is walked and paired every other link: (1st, 2nd), (3rd,
304
+ // 4th), with a leftover final step left to render alone. This replaced an
305
+ // earlier single-link collapse that silently dropped a chain's trailing
306
+ // links. `pairedAway` marks each pair's second half so the final loop
307
+ // below skips rendering it standalone.
308
+ const chosenAsSuccessor = new Set(chosenSuccessorOf.values());
309
+ const pairPartnerOf = new Map();
310
+ const pairedAway = new Set();
311
+ for (const stepId of stepIds) {
312
+ if (chosenAsSuccessor.has(stepId))
313
+ continue; // reached by walking its root below
314
+ let predecessorId = stepId;
315
+ while (predecessorId !== undefined) {
316
+ const successorId = chosenSuccessorOf.get(predecessorId);
317
+ if (successorId === undefined)
318
+ break;
319
+ pairPartnerOf.set(predecessorId, successorId);
320
+ pairedAway.add(successorId);
321
+ predecessorId = chosenSuccessorOf.get(successorId);
322
+ }
323
+ }
324
+ const groups = [];
325
+ for (const stepId of stepIds) {
326
+ // Rendered as part of its predecessor's pair elsewhere in this loop.
327
+ if (pairedAway.has(stepId))
328
+ continue;
329
+ const list = byStep.get(stepId);
330
+ if (list === undefined)
331
+ continue;
332
+ const successorId = pairPartnerOf.get(stepId);
333
+ const first = list[0];
334
+ const partnerFirst = successorId === undefined ? undefined : byStep.get(successorId)?.[0];
335
+ if (first !== undefined && partnerFirst !== undefined) {
336
+ groups.push({ kind: 'temporal-pair', a: first, b: partnerFirst });
337
+ continue;
338
+ }
339
+ groups.push({ kind: 'single', resolved: list });
340
+ }
341
+ return groups;
342
+ }
343
+ function renderPhraseGroup(group, siblingsOf) {
344
+ 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)}`;
347
+ }
348
+ const first = group.resolved[0];
349
+ if (first === undefined) {
350
+ throw new TypeError('evidence-target group is empty');
351
+ }
352
+ if (group.resolved.length === 1) {
353
+ return fullTargetPhrase(first, siblingsOf);
354
+ }
355
+ const stepRef = renderStepReference(first.step, first.operation, siblingsOf(first.operation.operationId));
356
+ // Sorted, so a same-step group's field order is permutation-invariant like
357
+ // the top-level join below; in declaration order, permuting
358
+ // `evidenceTargets` would change which field is named first here.
359
+ const locals = group.resolved
360
+ .map((entry) => localTargetPhrase(entry.target))
361
+ .sort();
362
+ return `${stepRef}: ${joinWithAnd(locals)}`;
363
+ }
364
+ /**
365
+ * Renders every evidence target a direction declares as one canonically
366
+ * ordered, order-independent clause: byte-identical under any permutation of
367
+ * `evidenceTargets`, `interactionPlan`, or `permittedInterfaces`, since every
368
+ * phrase comes from Map-keyed lookups and structural properties, never array
369
+ * position.
370
+ *
371
+ * A repeated pointer is deduplicated first: left in place it would push its
372
+ * step out of the "exactly one target" shape temporal pairing requires,
373
+ * silently disabling the relational phrase for an otherwise-valid pair.
374
+ */
375
+ export function renderEvidenceReferences(pointers, index) {
376
+ const uniquePointers = [...new Set(pointers)];
377
+ const resolved = uniquePointers.map((pointer) => resolveEvidenceTarget(pointer, index));
378
+ const siblings = siblingsByOperation(resolved);
379
+ const siblingsOf = (operationId) => siblings.get(operationId) ?? [];
380
+ const groups = groupResolvedTargets(resolved);
381
+ const phrases = groups.map((group) => renderPhraseGroup(group, siblingsOf));
382
+ return joinWithAnd([...phrases].sort());
383
+ }
@@ -0,0 +1,13 @@
1
+ import type { Direction } from '../schemas/oracle.ts';
2
+ import type { PlanIndex } from './plan-index.ts';
3
+ /**
4
+ * An empty `evidenceTargets` array reaches here on a contract that compiles
5
+ * clean: `core/schemas/oracle.ts` puts no `.min(1)` on the array, and AD-3's
6
+ * alignment predicate (`core/compile/oracle-alignment.ts`) is a universal
7
+ * over it, so an empty list is contained in any `check` vacuously. Rendering
8
+ * has nothing to name, so this throws the same precondition-violation
9
+ * `TypeError` as the other should-never-happen shapes in `core/seal/`, and
10
+ * `application/seal.ts` converts it to `RuntimeFault('schema-parse-failure')`
11
+ * at the boundary.
12
+ */
13
+ export declare function renderDirectionText(direction: Direction, index: PlanIndex): string;
@@ -0,0 +1,120 @@
1
+ import { renderEvidenceReferences } from './derived-reference.js';
2
+ // Uppercases only the first letter found, so an author-written `scope` or
3
+ // `negativeDomain` string that starts lowercase still reads as its own
4
+ // sentence once joined after another.
5
+ function capitalizeFirst(text) {
6
+ const match = /[a-zA-Z]/.exec(text);
7
+ if (match === null)
8
+ return text;
9
+ const index = match.index;
10
+ const char = text[index];
11
+ if (char === undefined)
12
+ return text;
13
+ return `${text.slice(0, index)}${char.toUpperCase()}${text.slice(index + 1)}`;
14
+ }
15
+ const ensureSentence = (text) => {
16
+ const capitalized = capitalizeFirst(text.trim());
17
+ return /[.!?]$/.test(capitalized) ? capitalized : `${capitalized}.`;
18
+ };
19
+ // `for-all`/`for-any` carry no nested predicate on `Direction`; that lives on
20
+ // `check`, which this module does not read. So the claim names the
21
+ // quantification itself, saying nothing about a condition it cannot see.
22
+ function quantifierClaim(relation, evidence) {
23
+ return relation === 'for-all'
24
+ ? `Every element reachable through ${evidence} is asserted to meet the declared condition.`
25
+ : `At least one element reachable through ${evidence} is asserted to meet the declared condition.`;
26
+ }
27
+ // A shared affirmative skeleton over `not` would tell the sealed evaluator
28
+ // the opposite of the declared claim.
29
+ function connectiveClaim(relation, evidence) {
30
+ switch (relation) {
31
+ case 'all':
32
+ return `${evidence} is asserted to satisfy every declared condition together.`;
33
+ case 'any':
34
+ return `${evidence} is asserted to satisfy at least one declared condition.`;
35
+ case 'not':
36
+ return `${evidence} is asserted to fail the declared condition, not to satisfy it.`;
37
+ }
38
+ }
39
+ function presenceClaim(relation, evidence) {
40
+ return relation === 'existence'
41
+ ? `${evidence} is asserted to be present.`
42
+ : `${evidence} is asserted to be absent.`;
43
+ }
44
+ function comparisonClaim(relation, evidence) {
45
+ switch (relation) {
46
+ case 'equality':
47
+ return `${evidence} is asserted to be equal.`;
48
+ case 'deep-equality':
49
+ return `${evidence} is asserted to be deeply, structurally equal.`;
50
+ case 'containment':
51
+ return `${evidence} is asserted to contain the declared member.`;
52
+ }
53
+ }
54
+ function structuralClaim(relation, evidence) {
55
+ return `${evidence} is asserted to satisfy the declared "${relation}" condition.`;
56
+ }
57
+ function renderRelationClaim(relation, evidence) {
58
+ if (relation === 'for-all' || relation === 'for-any') {
59
+ return quantifierClaim(relation, evidence);
60
+ }
61
+ if (relation === 'all' || relation === 'any' || relation === 'not') {
62
+ return connectiveClaim(relation, evidence);
63
+ }
64
+ if (relation === 'existence' || relation === 'absence') {
65
+ return presenceClaim(relation, evidence);
66
+ }
67
+ if (relation === 'equality' ||
68
+ relation === 'deep-equality' ||
69
+ relation === 'containment') {
70
+ return comparisonClaim(relation, evidence);
71
+ }
72
+ return structuralClaim(relation, evidence);
73
+ }
74
+ function renderPolarityClause(polarity) {
75
+ return polarity === 'expects-hold'
76
+ ? 'The declared polarity expects this relation to hold.'
77
+ : 'The declared polarity expects this relation to be a violation.';
78
+ }
79
+ // `negativeDomain` is author-written, evaluator-facing free text.
80
+ // Any trailing terminator the author already wrote is stripped first, since
81
+ // the text becomes the subject of a larger sentence rather than standing
82
+ // alone: an author-terminated string would otherwise read "...did not
83
+ // return. is treated as a defect." (a mid-sentence period followed by a
84
+ // lowercase clause).
85
+ function renderNegativeDomainClause(negativeDomain) {
86
+ const trimmed = negativeDomain.trim().replace(/[.!?]+$/, '');
87
+ return `${trimmed} is treated as a defect.`;
88
+ }
89
+ /**
90
+ * An empty `evidenceTargets` array reaches here on a contract that compiles
91
+ * clean: `core/schemas/oracle.ts` puts no `.min(1)` on the array, and AD-3's
92
+ * alignment predicate (`core/compile/oracle-alignment.ts`) is a universal
93
+ * over it, so an empty list is contained in any `check` vacuously. Rendering
94
+ * has nothing to name, so this throws the same precondition-violation
95
+ * `TypeError` as the other should-never-happen shapes in `core/seal/`, and
96
+ * `application/seal.ts` converts it to `RuntimeFault('schema-parse-failure')`
97
+ * at the boundary.
98
+ */
99
+ export function renderDirectionText(direction, index) {
100
+ if (direction.evidenceTargets.length === 0) {
101
+ throw new TypeError('direction carries no evidenceTargets; there is no observation for the generated prose to name');
102
+ }
103
+ const evidence = renderEvidenceReferences(direction.evidenceTargets, index);
104
+ const parts = [
105
+ ensureSentence(renderRelationClaim(direction.relation, evidence)),
106
+ ensureSentence(renderPolarityClause(direction.polarity)),
107
+ ];
108
+ // `null` and `''` both drop the clause: the schema allows `''` as distinct
109
+ // from `null` with no minimum length, and an absent clause is omitted
110
+ // rather than rendered as an empty sentence. Checking `!== null` alone
111
+ // would still render that empty sentence for `''`.
112
+ if (direction.scope !== null && direction.scope.trim() !== '') {
113
+ parts.push(ensureSentence(direction.scope));
114
+ }
115
+ if (direction.negativeDomain !== null &&
116
+ direction.negativeDomain.trim() !== '') {
117
+ parts.push(ensureSentence(renderNegativeDomainClause(direction.negativeDomain)));
118
+ }
119
+ return parts.join(' ');
120
+ }
@@ -0,0 +1,63 @@
1
+ /** resolves a pointer to its step and operation; nothing about reachability. */
2
+ import type { Operation, PermittedInterface } from '../schemas/interface.ts';
3
+ import type { InteractionStep } from '../schemas/plan.ts';
4
+ import { type EvidenceChannelName, type TransportChannelName } from '../schemas/pointer.ts';
5
+ /**
6
+ * Exported so `core/evaluate/evidence-resolution.ts` decodes pointer tails
7
+ * with these same two functions, avoiding a second private copy (the drift
8
+ * `IDENTIFIER_CHARSET_SOURCE`'s own precedent warns against).
9
+ */
10
+ export declare const decodeToken: (token: string) => string;
11
+ export declare const decodeTail: (tailSource: string) => readonly string[];
12
+ /**
13
+ * One evidence target, resolved locally to its step id and channel. The
14
+ * channel decides whether the rendered phrase says "the response you
15
+ * obtained" or "the value you sent".
16
+ */
17
+ export type EvidenceTarget = {
18
+ stepId: string;
19
+ channel: EvidenceChannelName;
20
+ transportChannel: TransportChannelName | null;
21
+ tail: readonly string[];
22
+ };
23
+ /**
24
+ * Parses one `InteractionPointer` string (`INTERACTION_POINTER_PATTERN` in
25
+ * `pointer.ts`) into its step id, channel, transport channel, and tail, using
26
+ * the schema's own channel partition so this accepts exactly what
27
+ * `InteractionPointer.safeParse` accepts. A should-never-happen precondition
28
+ * violation throws `TypeError`, per `digest.ts`'s precedent.
29
+ */
30
+ export declare function parseEvidenceTarget(pointer: string): EvidenceTarget;
31
+ /**
32
+ * What a pointer names: step, operation, and every step naming a given
33
+ * operation, which `derived-reference.ts`'s escalation needs. Says nothing
34
+ * about reachability or channel typing; the general addressing-grammar
35
+ * resolver lives in `core/evaluate/evidence-resolution.ts` and reachability
36
+ * in `core/compile/reachability.ts`.
37
+ */
38
+ export type PlanIndex = {
39
+ stepOf: (stepId: string) => InteractionStep | undefined;
40
+ operationOf: (operationId: string) => Operation | undefined;
41
+ stepsUsing: (operationId: string) => readonly InteractionStep[];
42
+ };
43
+ export type PlanIndexOptions = {
44
+ duplicateIds?: 'throw' | 'unresolved';
45
+ };
46
+ /**
47
+ * Builds the index once over the whole plan and interface set. Neither
48
+ * schema enforces `stepId`/`operationId` uniqueness. Strict callers keep the
49
+ * default throw instead of resolving by array order. Standalone structural
50
+ * checks can select `unresolved`, which removes every ambiguous identifier
51
+ * from lookup while preserving all unambiguous entries.
52
+ */
53
+ export declare function buildPlanIndex(interactionPlan: readonly InteractionStep[], permittedInterfaces: readonly PermittedInterface[], options?: PlanIndexOptions): PlanIndex;
54
+ /**
55
+ * Resolves a step id through the index or throws. Split from `stepOf` so the
56
+ * index itself stays a plain lookup (`| undefined`, per
57
+ * `noUncheckedIndexedAccess`) while callers get one function instead of
58
+ * repeating the `undefined` check. A precondition violation, not a
59
+ * `RuntimeFault`.
60
+ */
61
+ export declare function resolveStep(index: PlanIndex, stepId: string): InteractionStep;
62
+ /** Resolves an operation id through the index or throws. See `resolveStep`. */
63
+ export declare function resolveOperation(index: PlanIndex, operationId: string): Operation;