eval-quality 0.2.0 → 0.5.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 (137) hide show
  1. package/README.md +17 -12
  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/adapters/command-line-adapter.d.ts +44 -0
  28. package/dist/adapters/command-line-adapter.js +284 -0
  29. package/dist/adapters/command-target-policy.d.ts +36 -0
  30. package/dist/adapters/command-target-policy.js +33 -0
  31. package/dist/adapters/index.d.ts +2 -0
  32. package/dist/adapters/index.js +1 -0
  33. package/dist/cli/render.d.ts +2 -5
  34. package/dist/cli/render.js +51 -1
  35. package/dist/core/compile/bindings.d.ts +26 -7
  36. package/dist/core/compile/bindings.js +49 -32
  37. package/dist/core/compile/compile.d.ts +12 -0
  38. package/dist/core/compile/compile.js +28 -1
  39. package/dist/core/compile/excluded-content.d.ts +11 -0
  40. package/dist/core/compile/excluded-content.js +42 -0
  41. package/dist/core/compile/expression-legality.d.ts +2 -2
  42. package/dist/core/compile/expression-legality.js +58 -12
  43. package/dist/core/compile/interface-inventory.d.ts +34 -1
  44. package/dist/core/compile/interface-inventory.js +88 -11
  45. package/dist/core/compile/reachability.d.ts +12 -2
  46. package/dist/core/compile/reachability.js +123 -36
  47. package/dist/core/compile/schema-version.d.ts +2 -0
  48. package/dist/core/compile/schema-version.js +25 -0
  49. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  50. package/dist/core/compile/sensitivity-witness.js +110 -23
  51. package/dist/core/compile/step-reference.d.ts +2 -0
  52. package/dist/core/compile/step-reference.js +49 -0
  53. package/dist/core/coverage/operations.d.ts +62 -0
  54. package/dist/core/coverage/operations.js +57 -0
  55. package/dist/core/coverage/relevance.d.ts +4 -2
  56. package/dist/core/coverage/relevance.js +22 -23
  57. package/dist/core/coverage/satisfaction.d.ts +2 -2
  58. package/dist/core/coverage/satisfaction.js +73 -48
  59. package/dist/core/declared-inputs.d.ts +83 -4
  60. package/dist/core/declared-inputs.js +105 -8
  61. package/dist/core/evaluate/evidence-resolution.d.ts +9 -12
  62. package/dist/core/evaluate/evidence-resolution.js +70 -10
  63. package/dist/core/evaluate/operators.d.ts +22 -0
  64. package/dist/core/evaluate/operators.js +17 -1
  65. package/dist/core/evaluate/resolution.d.ts +14 -3
  66. package/dist/core/evaluate/resolution.js +63 -3
  67. package/dist/core/excluded-content.d.ts +65 -0
  68. package/dist/core/excluded-content.js +113 -0
  69. package/dist/core/failure-codes.d.ts +2 -2
  70. package/dist/core/failure-codes.js +5 -2
  71. package/dist/core/ingest/conditions.d.ts +1 -1
  72. package/dist/core/ingest/ingest.js +8 -0
  73. package/dist/core/preflight/plan.d.ts +7 -5
  74. package/dist/core/preflight/plan.js +75 -32
  75. package/dist/core/preflight/projection.d.ts +6 -3
  76. package/dist/core/preflight/projection.js +22 -2
  77. package/dist/core/preflight/reduce.js +47 -7
  78. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  79. package/dist/core/preflight/witness-evidence.js +63 -20
  80. package/dist/core/schemas/artifact.d.ts +683 -35
  81. package/dist/core/schemas/constraint-ledger.js +11 -0
  82. package/dist/core/schemas/defect-signature.d.ts +438 -16
  83. package/dist/core/schemas/defect-signature.js +58 -10
  84. package/dist/core/schemas/eval-contract.d.ts +517 -12
  85. package/dist/core/schemas/eval-contract.js +18 -2
  86. package/dist/core/schemas/evidence-artifact.d.ts +14 -4
  87. package/dist/core/schemas/evidence-artifact.js +1 -1
  88. package/dist/core/schemas/interface.d.ts +741 -13
  89. package/dist/core/schemas/interface.js +120 -7
  90. package/dist/core/schemas/isolation-manifest.js +16 -9
  91. package/dist/core/schemas/plan.d.ts +217 -3
  92. package/dist/core/schemas/plan.js +22 -1
  93. package/dist/core/schemas/pointer.d.ts +47 -1
  94. package/dist/core/schemas/pointer.js +89 -8
  95. package/dist/core/schemas/port-messages.d.ts +194 -10
  96. package/dist/core/schemas/port-messages.js +73 -6
  97. package/dist/core/schemas/probe-body.d.ts +18 -0
  98. package/dist/core/schemas/probe-body.js +13 -0
  99. package/dist/core/schemas/probe-policy.d.ts +36 -0
  100. package/dist/core/schemas/probe-policy.js +44 -0
  101. package/dist/core/schemas/probe.d.ts +122 -3
  102. package/dist/core/schemas/probe.js +1 -1
  103. package/dist/core/schemas/sealed-run-record.d.ts +131 -9
  104. package/dist/core/schemas/sealed-run-record.js +56 -11
  105. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  106. package/dist/core/schemas/sensitivity-witness.js +61 -5
  107. package/dist/core/score/bindings.d.ts +2 -2
  108. package/dist/core/score/bindings.js +25 -13
  109. package/dist/core/score/qualification.d.ts +5 -5
  110. package/dist/core/score/qualification.js +67 -37
  111. package/dist/core/score/quotation.d.ts +1 -24
  112. package/dist/core/score/quotation.js +29 -4
  113. package/dist/core/score/score.js +28 -2
  114. package/dist/core/score/strength.d.ts +9 -0
  115. package/dist/core/score/strength.js +35 -1
  116. package/dist/core/score/witness.d.ts +21 -0
  117. package/dist/core/score/witness.js +9 -4
  118. package/dist/core/seal/derived-reference.d.ts +2 -9
  119. package/dist/core/seal/derived-reference.js +62 -23
  120. package/dist/core/seal/plan-index.d.ts +18 -3
  121. package/dist/core/seal/plan-index.js +44 -8
  122. package/dist/index.d.ts +1 -1
  123. package/dist/index.js +1 -1
  124. package/dist/ports/environment-probe-port.d.ts +60 -4
  125. package/dist/testing/conformance.d.ts +2 -1
  126. package/dist/testing/conformance.js +1 -0
  127. package/dist/testing/index.d.ts +3 -3
  128. package/dist/testing/index.js +1 -1
  129. package/dist/testing/probe-conformance.d.ts +56 -5
  130. package/dist/testing/probe-conformance.js +196 -5
  131. package/package.json +1 -1
  132. package/schemas/eval-contract.schema.json +1356 -464
  133. package/schemas/evidence-artifact.schema.json +59 -29
  134. package/schemas/isolation-manifest.schema.json +17 -10
  135. package/schemas/probe.schema.json +197 -71
  136. package/schemas/rubric.schema.json +1 -1
  137. package/schemas/sealed-run-record.schema.json +296 -39
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The environment-probe port over a real child process, for the `cli`
3
+ * mechanism, which `probe-conformance.ts` had documented as unauthorizable
4
+ * and unshipped. `evaluateCommandTarget` closes the authorization half; this
5
+ * file closes the execution half.
6
+ *
7
+ * Every rule below is what "what's a command allowed to do" resolves to, a
8
+ * decision settled here in the implementation rather than as an architecture
9
+ * revision:
10
+ *
11
+ * 1. `target` is spawned directly with an argv array (`shell: false`,
12
+ * Node's own default, stated anyway since it is the one thing this
13
+ * adapter must never turn off). No channel value is ever concatenated
14
+ * into a shell string; a value containing `;`, `$(...)`, or a quote
15
+ * reaches the child as one literal argv element, not a metacharacter.
16
+ * 2. `argument` and `option` build the argv the same way a well-behaved CLI
17
+ * parser reads one: options first as `--{key}` (a boolean `true` is a
18
+ * bare flag, `false` is omitted, anything else gets one value token),
19
+ * positionals after in the record's own key order. `environment` passes
20
+ * through as declared, plus the host's own `PATH` so a `target` naming a
21
+ * bare command still resolves; a declared `PATH` key wins over that
22
+ * default. `stdin` is written and the stream is closed; `absent` closes
23
+ * it with nothing written.
24
+ * 3. `maxElapsedMs` and `maxOutputBytes` are enforced by this adapter, not
25
+ * borrowed from `AbortSignal`: exceeding either kills the process with
26
+ * `SIGKILL` and throws `budget-exhausted`, exactly as an HTTP cap does.
27
+ * `maxOutputBytes` applies independently to stdout, to stderr, and to
28
+ * each artifact file read back after exit.
29
+ * 4. A non-zero exit is an observation, never a fault, matching AD-10's rule
30
+ * for an HTTP status. Only a policy denial, a cap, an abort, or a failure
31
+ * to start the process throws. `exitCode` is negative when a signal
32
+ * ended the process, the same convention `CommandProbeObservation`
33
+ * documents for itself.
34
+ */
35
+ import { spawn } from 'node:child_process';
36
+ import { open } from 'node:fs/promises';
37
+ import { constants as osConstants } from 'node:os';
38
+ import { isAbsolute, resolve as resolvePath } from 'node:path';
39
+ import { RuntimeFault } from '../core/schemas/faults.js';
40
+ import { probeParsers } from '../ports/environment-probe-port.js';
41
+ import { evaluateCommandTarget } from './command-target-policy.js';
42
+ import { runPortMethod } from './port-boundary.js';
43
+ function stringifyScalar(value) {
44
+ if (typeof value === 'string')
45
+ return value;
46
+ if (typeof value === 'number' || typeof value === 'boolean')
47
+ return String(value);
48
+ return JSON.stringify(value);
49
+ }
50
+ /** Options first as `--{key}`, positionals after, both in the record's own key order. Exported for its own unit tests: this is the one place shell-injection safety is decided. */
51
+ export function buildArgv(channels) {
52
+ const optionTokens = [];
53
+ for (const [key, value] of Object.entries(channels.option)) {
54
+ if (value === false)
55
+ continue;
56
+ optionTokens.push(`--${key}`);
57
+ if (value !== true)
58
+ optionTokens.push(stringifyScalar(value));
59
+ }
60
+ const argumentTokens = Object.values(channels.argument).map(stringifyScalar);
61
+ return [...optionTokens, ...argumentTokens];
62
+ }
63
+ function buildEnv(declared) {
64
+ const base = {};
65
+ if (process.env.PATH !== undefined)
66
+ base.PATH = process.env.PATH;
67
+ return { ...base, ...declared };
68
+ }
69
+ function buildStdin(stdin) {
70
+ if (stdin.kind === 'absent')
71
+ return { kind: 'absent' };
72
+ if (stdin.kind === 'json')
73
+ return { kind: 'json', value: stdin.value };
74
+ return { kind: 'text', value: stdin.value };
75
+ }
76
+ /** A heuristic, not a declared content type: the port carries none for a process stream. JSON-shaped text reads as `json`; anything else as `text`. */
77
+ function bodyFromText(text) {
78
+ if (text.length === 0)
79
+ return { kind: 'text', value: text };
80
+ try {
81
+ return { kind: 'json', value: JSON.parse(text) };
82
+ }
83
+ catch {
84
+ return { kind: 'text', value: text };
85
+ }
86
+ }
87
+ function capped(detail) {
88
+ return new RuntimeFault('budget-exhausted', 'CommandProbeRequest', detail);
89
+ }
90
+ function forbidden(detail) {
91
+ return new RuntimeFault('forbidden-target', 'ProbeRequest', detail);
92
+ }
93
+ function writeStdin(child, stdin) {
94
+ const stream = child.stdin;
95
+ if (stream === null)
96
+ return;
97
+ // A child that exits before reading stdin closes its end of the pipe, and
98
+ // the pending `end()` write below then raises EPIPE on this stream, not on
99
+ // `child` itself. With no listener that is an unhandled 'error' and crashes
100
+ // the host process; the run itself is unaffected, since `close` still fires
101
+ // and carries the exit code this adapter already reads it from.
102
+ stream.on('error', () => { });
103
+ if (stdin.kind === 'absent') {
104
+ stream.end();
105
+ return;
106
+ }
107
+ stream.end(stdin.kind === 'json' ? JSON.stringify(stdin.value) : String(stdin.value));
108
+ }
109
+ /** `-signalNumber` when a signal ended the process, matching `CommandProbeObservation.exitCode`'s own documented convention. */
110
+ function exitCodeOf(code, signalName) {
111
+ if (code !== null)
112
+ return code;
113
+ if (signalName === null)
114
+ return -1;
115
+ const numeric = osConstants.signals[signalName];
116
+ return numeric === undefined ? -1 : -numeric;
117
+ }
118
+ async function runChildProcess(request, signal) {
119
+ return new Promise((settlePromise, rejectPromise) => {
120
+ const child = spawn(request.target, [...request.subcommandPath, ...request.argv], {
121
+ cwd: request.cwd,
122
+ env: request.env,
123
+ shell: false,
124
+ signal,
125
+ });
126
+ let stdout = '';
127
+ let stderr = '';
128
+ let settled = false;
129
+ let timedOut = false;
130
+ let overCap = false;
131
+ const finish = (action) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ clearTimeout(timer);
136
+ action();
137
+ };
138
+ const timer = setTimeout(() => {
139
+ timedOut = true;
140
+ child.kill('SIGKILL');
141
+ }, request.maxElapsedMs);
142
+ const capture = (chunk, append, currentLength) => {
143
+ if (currentLength() > request.maxOutputBytes)
144
+ return;
145
+ append(chunk.toString('utf8'));
146
+ if (currentLength() > request.maxOutputBytes) {
147
+ overCap = true;
148
+ child.kill('SIGKILL');
149
+ }
150
+ };
151
+ child.stdout?.on('data', (chunk) => capture(chunk, (next) => {
152
+ stdout += next;
153
+ }, () => Buffer.byteLength(stdout, 'utf8')));
154
+ child.stderr?.on('data', (chunk) => capture(chunk, (next) => {
155
+ stderr += next;
156
+ }, () => Buffer.byteLength(stderr, 'utf8')));
157
+ child.once('error', (error) => {
158
+ finish(() => rejectPromise(error));
159
+ });
160
+ child.once('close', (code, signalName) => {
161
+ finish(() => {
162
+ if (timedOut) {
163
+ rejectPromise(capped(`exceeded maxElapsedMs (${request.maxElapsedMs}ms) and was killed`));
164
+ return;
165
+ }
166
+ if (overCap) {
167
+ rejectPromise(capped(`stdout or stderr exceeded maxOutputBytes (${request.maxOutputBytes}) and the process was killed`));
168
+ return;
169
+ }
170
+ settlePromise({
171
+ exitCode: exitCodeOf(code, signalName),
172
+ stdout,
173
+ stderr,
174
+ });
175
+ });
176
+ });
177
+ writeStdin(child, request.stdin);
178
+ });
179
+ }
180
+ /** Reads at most `maxBytes + 1` bytes rather than the whole file: an oversize artifact is capped before its bytes are loaded, not after, so a run that wrote past the limit cannot make this adapter allocate the excess first. */
181
+ async function readArtifactFile(path, maxBytes) {
182
+ let handle;
183
+ try {
184
+ handle = await open(path, 'r');
185
+ }
186
+ catch (error) {
187
+ if (error.code === 'ENOENT') {
188
+ return { present: false, text: '', truncated: false };
189
+ }
190
+ throw error;
191
+ }
192
+ try {
193
+ const buffer = Buffer.alloc(maxBytes + 1);
194
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
195
+ const truncated = bytesRead > maxBytes;
196
+ const text = buffer
197
+ .subarray(0, Math.min(bytesRead, maxBytes))
198
+ .toString('utf8');
199
+ return { present: true, text, truncated };
200
+ }
201
+ finally {
202
+ await handle.close();
203
+ }
204
+ }
205
+ /**
206
+ * The real mechanism: an actual child process, an actual file read. Exported,
207
+ * unlike the other three shipped adapters' defaults, because AD-37's own
208
+ * conformance subject for this port has to exercise real process behaviour
209
+ * (a real timeout, a real output cap, a real argv) the way the `api` arm's
210
+ * subject exercises a real loopback server; a synthetic mechanism would prove
211
+ * nothing about the one thing this adapter exists to get right.
212
+ */
213
+ export const nodeCommandMechanism = {
214
+ run: runChildProcess,
215
+ readArtifact: readArtifactFile,
216
+ };
217
+ export function createCommandLineAdapter(policy, mechanism = nodeCommandMechanism) {
218
+ return {
219
+ probe: (request, signal) => runPortMethod({
220
+ request,
221
+ requestParser: probeParsers.request,
222
+ responseParser: probeParsers.response,
223
+ requestPath: 'ProbeRequest',
224
+ responsePath: 'ProbeObservation',
225
+ signal,
226
+ mechanism: async (parsed, innerSignal) => {
227
+ if (parsed.kind !== 'cli') {
228
+ // This adapter authorizes no `api` target, so an `api` request
229
+ // meets the same "the mapping names nothing" denial an
230
+ // unmapped interfaceId would, before any process spawns.
231
+ throw forbidden('this adapter runs cli requests only; no api target is ever authorized');
232
+ }
233
+ const decision = evaluateCommandTarget(policy, {
234
+ interfaceId: parsed.interfaceId,
235
+ executable: parsed.executable,
236
+ subcommandPath: parsed.subcommandPath,
237
+ });
238
+ if (!decision.allowed)
239
+ throw forbidden(decision.detail);
240
+ const { authorization } = decision;
241
+ const runResult = await mechanism.run({
242
+ target: authorization.target,
243
+ subcommandPath: parsed.subcommandPath,
244
+ argv: buildArgv(parsed.channels),
245
+ env: buildEnv(parsed.channels.environment),
246
+ stdin: buildStdin(parsed.channels.stdin),
247
+ cwd: authorization.cwd,
248
+ maxElapsedMs: authorization.maxElapsedMs,
249
+ maxOutputBytes: authorization.maxOutputBytes,
250
+ }, innerSignal);
251
+ const artifactEntries = await Promise.all(Object.entries(authorization.artifacts).map(async ([artifactId, relativePath]) => {
252
+ const absolute = isAbsolute(relativePath)
253
+ ? relativePath
254
+ : resolvePath(authorization.cwd, relativePath);
255
+ const read = await mechanism.readArtifact(absolute, authorization.maxOutputBytes);
256
+ if (read.truncated) {
257
+ throw capped(`artifact "${artifactId}" exceeded maxOutputBytes (${authorization.maxOutputBytes})`);
258
+ }
259
+ return [artifactId, read];
260
+ }));
261
+ return { parsed, runResult, artifactEntries };
262
+ },
263
+ assemble: (raw) => {
264
+ const { parsed, runResult, artifactEntries } = raw;
265
+ const observation = {
266
+ kind: 'cli',
267
+ probeId: parsed.probeId,
268
+ interfaceId: parsed.interfaceId,
269
+ operationId: parsed.operationId,
270
+ exitCode: runResult.exitCode,
271
+ stdout: bodyFromText(runResult.stdout),
272
+ stderr: bodyFromText(runResult.stderr),
273
+ artifacts: Object.fromEntries(artifactEntries.map(([id, read]) => [
274
+ id,
275
+ read.present
276
+ ? bodyFromText(read.text)
277
+ : { kind: 'absent' },
278
+ ])),
279
+ };
280
+ return observation;
281
+ },
282
+ }),
283
+ };
284
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * AD-35's default-deny decision for the `cli` mechanism, as a pure function.
3
+ *
4
+ * Lives under `adapters/` rather than beside `core/probe/target-policy.ts`
5
+ * (the `api` evaluator), even though the two are otherwise siblings: the
6
+ * repository's mechanically-enforced dependency direction permits
7
+ * `adapters/` to import `core/schemas` but not `core/`, so a
8
+ * target-authorization decision an adapter calls has to be adapter-owned
9
+ * infrastructure, not a `core/` module. `target-policy.ts` itself is never
10
+ * imported from `src/adapters/` for the same reason — no shipped `api`
11
+ * adapter exists to call it, which is the gap this file closes for `cli`.
12
+ */
13
+ import type { CommandTargetAuthorization, CommandTargetPolicy } from '../core/schemas/probe-policy.ts';
14
+ /** Why a command target was denied. Thrown as the single AD-28 `forbidden-target` fault, same as the HTTP reasons. */
15
+ export declare const COMMAND_DENIAL_REASONS: readonly ['interface-not-authorized', 'executable-not-authorized', 'subcommand-not-authorized'];
16
+ export type CommandDenialReason = (typeof COMMAND_DENIAL_REASONS)[number];
17
+ export type CommandResolvedTarget = {
18
+ readonly interfaceId: string;
19
+ readonly executable: string;
20
+ readonly subcommandPath: readonly string[];
21
+ };
22
+ export type CommandPolicyDecision = {
23
+ readonly allowed: true;
24
+ readonly authorization: CommandTargetAuthorization;
25
+ } | {
26
+ readonly allowed: false;
27
+ readonly reason: CommandDenialReason;
28
+ readonly detail: string;
29
+ };
30
+ /**
31
+ * The interface-and-executable check runs first, so an unmapped pair never
32
+ * reaches the subcommand comparison. Where several authorizations name one
33
+ * pair, each is tried in declaration order and the first that allows wins;
34
+ * mirrors `evaluateTarget`'s own ordering rule for the same reason.
35
+ */
36
+ export declare function evaluateCommandTarget(policy: CommandTargetPolicy, target: CommandResolvedTarget): CommandPolicyDecision;
@@ -0,0 +1,33 @@
1
+ /** Why a command target was denied. Thrown as the single AD-28 `forbidden-target` fault, same as the HTTP reasons. */
2
+ export const COMMAND_DENIAL_REASONS = [
3
+ 'interface-not-authorized',
4
+ 'executable-not-authorized',
5
+ 'subcommand-not-authorized',
6
+ ];
7
+ function subcommandPathsMatch(a, b) {
8
+ return (a.length === b.length && a.every((segment, index) => segment === b[index]));
9
+ }
10
+ function deny(reason, detail) {
11
+ return { allowed: false, reason, detail };
12
+ }
13
+ /**
14
+ * The interface-and-executable check runs first, so an unmapped pair never
15
+ * reaches the subcommand comparison. Where several authorizations name one
16
+ * pair, each is tried in declaration order and the first that allows wins;
17
+ * mirrors `evaluateTarget`'s own ordering rule for the same reason.
18
+ */
19
+ export function evaluateCommandTarget(policy, target) {
20
+ const matchingPair = policy.authorizations.filter((authorization) => authorization.interfaceId === target.interfaceId &&
21
+ authorization.executable === target.executable);
22
+ if (matchingPair.length === 0) {
23
+ return deny(policy.authorizations.some((authorization) => authorization.interfaceId === target.interfaceId)
24
+ ? 'executable-not-authorized'
25
+ : 'interface-not-authorized', `no authorization names interface "${target.interfaceId}" and executable "${target.executable}" together`);
26
+ }
27
+ for (const authorization of matchingPair) {
28
+ if (authorization.permittedSubcommandPaths.some((permitted) => subcommandPathsMatch(permitted, target.subcommandPath))) {
29
+ return { allowed: true, authorization };
30
+ }
31
+ }
32
+ return deny('subcommand-not-authorized', `subcommand path [${target.subcommandPath.join(', ')}] is not among the authorized paths for interface "${target.interfaceId}" and executable "${target.executable}"`);
33
+ }
@@ -4,6 +4,8 @@
4
4
  * subpath and the root barrel keeps the `root -> adapters` edge the matrix
5
5
  * does not grant.
6
6
  */
7
+ export type { CommandMechanism, CommandRunRequest, CommandRunResult, } from './command-line-adapter.ts';
8
+ export { createCommandLineAdapter, nodeCommandMechanism, } from './command-line-adapter.ts';
7
9
  export type { CorpusMechanism } from './local-corpus-adapter.ts';
8
10
  export { createLocalCorpusAdapter } from './local-corpus-adapter.ts';
9
11
  export type { FileSystemMechanism } from './node-file-system-adapter.ts';
@@ -1,3 +1,4 @@
1
+ export { createCommandLineAdapter, nodeCommandMechanism, } from './command-line-adapter.js';
1
2
  export { createLocalCorpusAdapter } from './local-corpus-adapter.js';
2
3
  export { createNodeFileSystemAdapter } from './node-file-system-adapter.js';
3
4
  export { createSystemClockAdapter } from './system-clock-adapter.js';
@@ -1,8 +1,3 @@
1
- /**
2
- * The four shapes the binary writes, and the exit-code table two documents
3
- * share. Every line the CLI emits is produced here, so a format change is one
4
- * file.
5
- */
6
1
  import { type Diagnostic } from '../application/index.ts';
7
2
  /**
8
3
  * Delegates to `serializeArtifact`; the canonical bytes are not re-derived
@@ -13,6 +8,8 @@ export declare function renderArtifact(artifact: unknown, artifactPath: string):
13
8
  export declare function renderDiagnostic(diagnostic: Diagnostic): string;
14
9
  /**
15
10
  * `eval-quality: <code>: <artifactPath>: <detail>` for either error class.
11
+ * A fault carrying a Zod error as its cause prints that error's issues under
12
+ * the first line, one indented `<location>: <message>` per issue.
16
13
  * Anything else falls back to `String(error)`, which is what a defect in our
17
14
  * own code looks like from outside.
18
15
  */
@@ -3,8 +3,55 @@
3
3
  * share. Every line the CLI emits is produced here, so a format change is one
4
4
  * file.
5
5
  */
6
+ import { z } from 'zod';
6
7
  import { RuntimeFault, StructuralFailure, serializeArtifact, } from '../application/index.js';
7
8
  const PREFIX = 'eval-quality';
9
+ /**
10
+ * How many parse issues the renderer prints before it stops and counts the
11
+ * rest.
12
+ * An author fixing a contract reads the first few and edits; a wall of four
13
+ * hundred lines from one missing required key teaches nothing and buries the
14
+ * first line, which is the one that names the code.
15
+ */
16
+ const ISSUE_LIMIT = 20;
17
+ /**
18
+ * An RFC 6901 pointer over the failing value, so the location the renderer
19
+ * prints is the same spelling the contract itself uses to address evidence.
20
+ * A numeric segment is an array index and is printed as one.
21
+ */
22
+ function issueLocation(path) {
23
+ if (path.length === 0)
24
+ return '(root)';
25
+ return path
26
+ .map((segment) => String(segment).replaceAll('~', '~0').replaceAll('/', '~1'))
27
+ .map((segment) => `/${segment}`)
28
+ .join('');
29
+ }
30
+ /**
31
+ * The issue list a failed parse already carries, which the first line of the
32
+ * error alone does not expose.
33
+ * A contract author outside this repository has the published schema and this
34
+ * message and nothing else, so a parse failure that names no field costs a
35
+ * bisect over the whole document.
36
+ * Issues are sorted by location so two runs over the same input print the same
37
+ * bytes.
38
+ * A Zod issue message names the expectation and never echoes the value that
39
+ * failed, which matters because this goes to stderr and a contract declares an
40
+ * environment channel; the one thing a message does quote is an unrecognized
41
+ * key name, which is a field name.
42
+ */
43
+ function renderParseIssues(cause) {
44
+ if (!(cause instanceof z.ZodError))
45
+ return '';
46
+ const lines = cause.issues
47
+ .map((issue) => ` ${issueLocation(issue.path)}: ${issue.message}`)
48
+ .sort();
49
+ const shown = lines.slice(0, ISSUE_LIMIT);
50
+ const hidden = lines.length - shown.length;
51
+ if (hidden > 0)
52
+ shown.push(` ... and ${hidden} more`);
53
+ return shown.length === 0 ? '' : `\n${shown.join('\n')}`;
54
+ }
8
55
  /**
9
56
  * Delegates to `serializeArtifact`; the canonical bytes are not re-derived
10
57
  * here, so the text written to stdout is the text `digestArtifact` hashes.
@@ -18,6 +65,8 @@ export function renderDiagnostic(diagnostic) {
18
65
  }
19
66
  /**
20
67
  * `eval-quality: <code>: <artifactPath>: <detail>` for either error class.
68
+ * A fault carrying a Zod error as its cause prints that error's issues under
69
+ * the first line, one indented `<location>: <message>` per issue.
21
70
  * Anything else falls back to `String(error)`, which is what a defect in our
22
71
  * own code looks like from outside.
23
72
  */
@@ -27,7 +76,8 @@ export function renderError(error) {
27
76
  const detail = error.message.startsWith(prefix)
28
77
  ? error.message.slice(prefix.length)
29
78
  : error.message;
30
- return `${PREFIX}: ${error.code}: ${error.artifactPath}: ${detail}`;
79
+ const issues = renderParseIssues(error.cause);
80
+ return `${PREFIX}: ${error.code}: ${error.artifactPath}: ${detail}${issues}`;
31
81
  }
32
82
  return `${PREFIX}: ${String(error)}`;
33
83
  }
@@ -1,10 +1,23 @@
1
1
  import type { EvalContract } from '../schemas/eval-contract.ts';
2
2
  import type { InteractionStep } from '../schemas/plan.ts';
3
- import { type TransportChannelName } from '../schemas/pointer.ts';
3
+ import type { InputChannelName } from '../schemas/pointer.ts';
4
4
  import { type EvidenceTarget } from '../seal/plan-index.ts';
5
+ /**
6
+ * The one channel a captured pointer may name: whichever channel the
7
+ * referenced operation's own response descriptor describes.
8
+ *
9
+ * `ResponseDescriptor` declares `requiredKeys`, `permittedKeys`, `types`,
10
+ * `successIndicator`, `channelRoles`, and `collectionLocations`, and every one
11
+ * of them is about the channel the operation nominates. Off an interface that
12
+ * speaks HTTP that is the response body; off a command it is the stream the
13
+ * operation names. `response-headers` and `response-status` are never it,
14
+ * because `Observation.responseHeaders` admits objects, arrays, numbers, and
15
+ * `null`, so a header capture compiled as a `string` could resolve to an
16
+ * object at score time.
17
+ */
5
18
  /** One `{ captured }` binding, resolved to the pointer target it addresses. */
6
19
  export type CapturedBinding = {
7
- readonly transportChannel: TransportChannelName;
20
+ readonly transportChannel: InputChannelName;
8
21
  readonly key: string;
9
22
  readonly pointer: string;
10
23
  readonly target: EvidenceTarget;
@@ -45,11 +58,17 @@ export declare function capturedBindings(step: InteractionStep): readonly Captur
45
58
  export declare function checkBindingCycle(contract: EvalContract): void;
46
59
  /**
47
60
  * `captured-channel-undeclared`: a captured pointer naming any AD-26 channel
48
- * but `response-body`. `call-inputs` addresses a step's own request,
49
- * `stdout`, `stderr`, and `exit-code` are process channels no operation
50
- * surviving `unsupported-interface-kind` produces, and `response-headers` and
51
- * `response-status` have no declared structure to give a captured value a
52
- * type.
61
+ * but the one the referenced operation's response descriptor describes. That
62
+ * is `response-body` off an interface that speaks HTTP and whichever channel
63
+ * `descriptorChannel` nominates off one that runs behind a command, which is
64
+ * the same rule read against the declaration rather than assumed.
65
+ *
66
+ * Every other channel is refused for a reason that does not depend on the
67
+ * kind: `call-inputs` addresses a step's own request, and `response-headers`,
68
+ * `response-status`, and `exit-code` have no declared structure to give a
69
+ * captured value a type. On the `artifact` channel the identifier is compared
70
+ * too, so a capture from a file the operation writes but does not describe is
71
+ * refused alongside one from a file it never writes.
53
72
  */
54
73
  export declare function checkCapturedChannel(contract: EvalContract): void;
55
74
  /**