eval-quality 0.3.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.
package/README.md CHANGED
@@ -267,7 +267,7 @@ import spec from 'eval-quality/schemas/eval-contract.schema.json' with { type: '
267
267
  The import attribute is required: ESM on Node 22 and 24 both throw `ERR_IMPORT_ATTRIBUTE_MISSING`
268
268
  without it. The development corpus ships the same way, at `eval-quality/corpus/dev/`, so an adopter
269
269
  can read real compiled contracts and one compiled-and-sealed pair without cloning this repository.
270
- `eval-quality/adapters` is one of the five published subpaths, holding the three reference adapters
270
+ `eval-quality/adapters` is one of the five published subpaths, holding the four reference adapters
271
271
  the conformance suite runs against.
272
272
 
273
273
  Eleven of the twelve published schemas carry a `schemaVersion`. `artifact-reference` is exempt: it
@@ -378,10 +378,10 @@ names as that artifact's producer, which today are `src/core/seal/seal.ts`,
378
378
  The `eval-quality/conformance` subpath publishes the port boundary: the four port types, the message
379
379
  shapes they carry, the AD-28 `RUNTIME_FAULT_CODES` registry and `RuntimeFaultCode` type a conforming
380
380
  adapter throws against, and an executable conformance suite. An adapter is conforming when
381
- `runCorpusPortConformance`, `runClockPortConformance`, `runFileSystemPortConformance`, or
382
- `runEnvironmentProbePortConformance` returns a report whose `passed` is true, which is the definition;
383
- each returns a report, so the suite carries no test framework and runs under whichever one you
384
- already use.
381
+ `runCorpusPortConformance`, `runClockPortConformance`, `runFileSystemPortConformance`,
382
+ `runEnvironmentProbePortConformance`, or `runCommandLineProbeConformance` returns a report whose
383
+ `passed` is true, which is the definition; each returns a report, so the suite carries no test
384
+ framework and runs under whichever one you already use.
385
385
 
386
386
  ```ts
387
387
  import { runCorpusPortConformance, type CorpusPort } from 'eval-quality/conformance'
@@ -390,10 +390,15 @@ import { runCorpusPortConformance, type CorpusPort } from 'eval-quality/conforma
390
390
  The suite drives a subject through four scenarios and checks six assertions per port method: a
391
391
  mechanism failure is a typed fault, exactly one underlying call happens on success and on failure, an
392
392
  aborted signal rejects promptly, an in-band error value is thrown as a fault, and a
393
- successful call returns a response the published schema accepts. The environment-probe port adds
394
- thirteen more from AD-35's default-deny target policy. `npm run test:conformance` runs the suite
395
- against the three adapters this package ships and against an in-repository probe subject that exists
396
- only as the suite's own subject.
393
+ successful call returns a response the published schema accepts. `EnvironmentProbePort` adds a second
394
+ arm per mechanism: `runEnvironmentProbePortConformance` (`api`) adds thirteen more from AD-35's
395
+ default-deny target policy, and `runCommandLineProbeConformance` (`cli`) adds nine, once
396
+ `CommandTargetPolicy` gave that mechanism something to authorize — an unmapped interface, an
397
+ unmapped executable, an unauthorized subcommand path, a non-zero exit read as an observation, a
398
+ shell-metacharacter argument proven to reach the process as one literal token, a declared artifact
399
+ captured, and both caps enforced. `npm run test:conformance` runs the suite against the four
400
+ adapters this package ships and against two in-repository probe subjects, one per mechanism, that
401
+ exist only as the suite's own subjects.
397
402
 
398
403
  `docs/ad21-verdict-decision.generated.md` holds AD-21's two published verdict ladders, production and
399
404
  contract-scoring, emitted from the rule tables in `src/core/score/ladder.ts` together with the
@@ -0,0 +1,44 @@
1
+ import type { CommandProbeRequest } from '../core/schemas/port-messages.ts';
2
+ import type { CommandTargetPolicy } from '../core/schemas/probe-policy.ts';
3
+ import type { EnvironmentProbePort } from '../ports/environment-probe-port.ts';
4
+ /** One process run, already reduced to what an observation needs. No truncation flag: exceeding `maxOutputBytes` rejects with `budget-exhausted` rather than resolving with a partial stream, so a resolved run's output is always the whole thing. */
5
+ export type CommandRunResult = {
6
+ readonly exitCode: number;
7
+ readonly stdout: string;
8
+ readonly stderr: string;
9
+ };
10
+ export type CommandRunRequest = {
11
+ readonly target: string;
12
+ readonly subcommandPath: readonly string[];
13
+ readonly argv: readonly string[];
14
+ readonly env: Readonly<Record<string, string>>;
15
+ readonly stdin: {
16
+ readonly kind: 'json' | 'text' | 'absent';
17
+ readonly value?: unknown;
18
+ };
19
+ readonly cwd: string;
20
+ readonly maxElapsedMs: number;
21
+ readonly maxOutputBytes: number;
22
+ };
23
+ export type ArtifactRead = {
24
+ readonly present: boolean;
25
+ readonly text: string;
26
+ readonly truncated: boolean;
27
+ };
28
+ /** Swappable so the conformance subject can spy on calls and script every scenario, matching the other three shipped adapters. */
29
+ export type CommandMechanism = {
30
+ readonly run: (request: CommandRunRequest, signal: AbortSignal) => Promise<CommandRunResult>;
31
+ readonly readArtifact: (path: string, maxBytes: number) => Promise<ArtifactRead>;
32
+ };
33
+ /** 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. */
34
+ export declare function buildArgv(channels: CommandProbeRequest['channels']): string[];
35
+ /**
36
+ * The real mechanism: an actual child process, an actual file read. Exported,
37
+ * unlike the other three shipped adapters' defaults, because AD-37's own
38
+ * conformance subject for this port has to exercise real process behaviour
39
+ * (a real timeout, a real output cap, a real argv) the way the `api` arm's
40
+ * subject exercises a real loopback server; a synthetic mechanism would prove
41
+ * nothing about the one thing this adapter exists to get right.
42
+ */
43
+ export declare const nodeCommandMechanism: CommandMechanism;
44
+ export declare function createCommandLineAdapter(policy: CommandTargetPolicy, mechanism?: CommandMechanism): EnvironmentProbePort;
@@ -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';
@@ -74,3 +74,39 @@ export declare const ProbeTargetPolicy: z.ZodObject<{
74
74
  }, z.core.$strict>;
75
75
  export type ProbeTargetAuthorization = z.infer<typeof ProbeTargetAuthorization>;
76
76
  export type ProbeTargetPolicy = z.infer<typeof ProbeTargetPolicy>;
77
+ /**
78
+ * One authorized command target, AD-35's mapping for the `cli` mechanism.
79
+ * `probe-conformance.ts`'s own published note had recorded that this shape
80
+ * did not exist: `ProbeTargetPolicy` above is entirely HTTP-shaped, and
81
+ * "a command interface cannot be authorized at all" without one.
82
+ *
83
+ * Keyed by `(interfaceId, executable)` rather than `interfaceId` alone,
84
+ * because `CommandInvocation` is declared per operation (AD-19), not per
85
+ * interface: two operations under one CLI interface may name two different
86
+ * logical executables. Everything else about the pair is shared the way host
87
+ * and port are shared across an HTTP authorization's methods.
88
+ */
89
+ export declare const CommandTargetAuthorization: z.ZodObject<{
90
+ interfaceId: z.ZodString;
91
+ executable: z.ZodString;
92
+ target: z.ZodString;
93
+ permittedSubcommandPaths: z.ZodArray<z.ZodArray<z.ZodString>>;
94
+ cwd: z.ZodString;
95
+ artifacts: z.ZodRecord<z.ZodString, z.ZodString>;
96
+ maxElapsedMs: z.ZodInt;
97
+ maxOutputBytes: z.ZodInt;
98
+ }, z.core.$strict>;
99
+ export declare const CommandTargetPolicy: z.ZodObject<{
100
+ authorizations: z.ZodArray<z.ZodObject<{
101
+ interfaceId: z.ZodString;
102
+ executable: z.ZodString;
103
+ target: z.ZodString;
104
+ permittedSubcommandPaths: z.ZodArray<z.ZodArray<z.ZodString>>;
105
+ cwd: z.ZodString;
106
+ artifacts: z.ZodRecord<z.ZodString, z.ZodString>;
107
+ maxElapsedMs: z.ZodInt;
108
+ maxOutputBytes: z.ZodInt;
109
+ }, z.core.$strict>>;
110
+ }, z.core.$strict>;
111
+ export type CommandTargetAuthorization = z.infer<typeof CommandTargetAuthorization>;
112
+ export type CommandTargetPolicy = z.infer<typeof CommandTargetPolicy>;
@@ -31,3 +31,47 @@ export const ProbeTargetPolicy = z.strictObject({
31
31
  .array(ProbeTargetAuthorization)
32
32
  .describe('An empty array is legal and authorizes nothing, which is the default-deny base case and must stay representable.'),
33
33
  });
34
+ /**
35
+ * One authorized command target, AD-35's mapping for the `cli` mechanism.
36
+ * `probe-conformance.ts`'s own published note had recorded that this shape
37
+ * did not exist: `ProbeTargetPolicy` above is entirely HTTP-shaped, and
38
+ * "a command interface cannot be authorized at all" without one.
39
+ *
40
+ * Keyed by `(interfaceId, executable)` rather than `interfaceId` alone,
41
+ * because `CommandInvocation` is declared per operation (AD-19), not per
42
+ * interface: two operations under one CLI interface may name two different
43
+ * logical executables. Everything else about the pair is shared the way host
44
+ * and port are shared across an HTTP authorization's methods.
45
+ */
46
+ export const CommandTargetAuthorization = z.strictObject({
47
+ interfaceId: Identifier.describe('The logical interface identifier the contract names.'),
48
+ executable: Identifier.describe('The logical executable name a CommandInvocation declares. Paired with interfaceId since one interface may declare more than one.'),
49
+ target: z
50
+ .string()
51
+ .min(1)
52
+ .describe('The real, spawnable command: an absolute path, or a name the adapter resolves through its own PATH. Never taken from the contract, which never carries one (AD-35).'),
53
+ permittedSubcommandPaths: z
54
+ .array(z.array(Identifier))
55
+ .min(1)
56
+ .describe('The exact subcommand paths this authorization allows, compared literally the way AD-40 compares them. An empty inner array authorizes invoking target with no subcommand. A path the request declares that matches none of these is denied before target is ever spawned, the same role methods plays on the HTTP side.'),
57
+ cwd: z
58
+ .string()
59
+ .min(1)
60
+ .describe('The working directory every invocation runs from. Declared rather than inherited from the adapter process, so a relative artifact path below resolves against a target the mapping names rather than wherever the host process happened to start.'),
61
+ artifacts: z
62
+ .record(Identifier, z.string().min(1))
63
+ .describe("Where each declared artifact identifier reads from on disk, relative to cwd unless absolute. `CommandProbeObservation.artifacts` is keyed by these identifiers; one this run did not write resolves `absent` rather than missing the key, since the record itself is mandatory. An identifier absent from this map can never appear in an observation, which is the same disclosure boundary AD-35 draws around target itself: an operation may declare an artifact the mapping has no path for, and that operation's pre-flight runs with that artifact permanently absent until the mapping is extended."),
64
+ maxElapsedMs: z
65
+ .int()
66
+ .min(1)
67
+ .describe('Wall-clock budget for one invocation. Exceeding it kills the process and throws budget-exhausted, the same fault an HTTP cap throws.'),
68
+ maxOutputBytes: z
69
+ .int()
70
+ .min(1)
71
+ .describe('Applies independently to stdout, to stderr, and to each artifact file read back. The first channel to cross it kills the process (for stdout/stderr, mid-run) or fails the read (for an artifact, after exit) with budget-exhausted.'),
72
+ });
73
+ export const CommandTargetPolicy = z.strictObject({
74
+ authorizations: z
75
+ .array(CommandTargetAuthorization)
76
+ .describe('An empty array is legal and authorizes nothing, the same default-deny base case as ProbeTargetPolicy.'),
77
+ });
package/dist/index.d.ts CHANGED
@@ -12,4 +12,4 @@ export type { ScoringPolicy } from './core/schemas/scoring-policy.ts';
12
12
  export type { SealedEvaluatorBrief } from './core/schemas/sealed-evaluator-brief.ts';
13
13
  export type { SealedRunRecord } from './core/schemas/sealed-run-record.ts';
14
14
  export type { FixtureReset, ManifestationWitness, SensitivityWitness, SensitivityWitnessLeg, WitnessChannel, WitnessInputs, } from './core/schemas/sensitivity-witness.ts';
15
- export declare const VERSION = "0.3.0";
15
+ export declare const VERSION = "0.5.0";
package/dist/index.js CHANGED
@@ -19,4 +19,4 @@
19
19
  // subpath, where AD-37 puts the conformance definition an adapter author
20
20
  // reads; the reference adapters stay at `eval-quality/adapters`.
21
21
  export * from './application/index.js';
22
- export const VERSION = '0.3.0';
22
+ export const VERSION = '0.5.0';
@@ -7,13 +7,14 @@ export type ConformanceOutcome = {
7
7
  readonly passed: boolean;
8
8
  readonly detail: string;
9
9
  };
10
- export type ConformancePort = 'corpus' | 'clock' | 'file-system' | 'environment-probe';
10
+ export type ConformancePort = 'corpus' | 'clock' | 'file-system' | 'environment-probe' | 'command-probe';
11
11
  /** how many outcomes a complete run of each port produces. Asserted as literals by fixture 58. */
12
12
  export declare const CONFORMANCE_OUTCOME_COUNTS: {
13
13
  readonly corpus: 6;
14
14
  readonly clock: 6;
15
15
  readonly 'file-system': 12;
16
16
  readonly 'environment-probe': 19;
17
+ readonly 'command-probe': 15;
17
18
  };
18
19
  export type ConformanceReport = {
19
20
  readonly subject: string;
@@ -19,6 +19,7 @@ export const CONFORMANCE_OUTCOME_COUNTS = {
19
19
  clock: 6,
20
20
  'file-system': 12,
21
21
  'environment-probe': 19,
22
+ 'command-probe': 15,
22
23
  };
23
24
  /** "Promptly" with no bound is unfalsifiable. One second clears any real adapter's abort latency and sits well inside a default test timeout. */
24
25
  export const DEFAULT_ABORT_BUDGET_MS = 1000;
@@ -13,7 +13,7 @@
13
13
  export type { RuntimeFaultCode } from '../core/schemas/faults.ts';
14
14
  export { RUNTIME_FAULT_CODES, RuntimeFault } from '../core/schemas/faults.ts';
15
15
  export type { ClockReadRequest, ClockReadResponse, CorpusResolveRequest, CorpusResolveResponse, FileReadRequest, FileReadResponse, FileWriteRequest, FileWriteResponse, ProbeObservation, ProbeObservedBody, ProbeRequest, ProbeRequestBody, } from '../core/schemas/port-messages.ts';
16
- export type { ProbeTargetAuthorization, ProbeTargetPolicy, } from '../core/schemas/probe-policy.ts';
16
+ export type { CommandTargetAuthorization, CommandTargetPolicy, ProbeTargetAuthorization, ProbeTargetPolicy, } from '../core/schemas/probe-policy.ts';
17
17
  export type { ClockPort } from '../ports/clock-port.ts';
18
18
  export { clockReadParsers } from '../ports/clock-port.ts';
19
19
  export type { CorpusPort } from '../ports/corpus-port.ts';
@@ -24,5 +24,5 @@ export type { FileSystemPort } from '../ports/file-system-port.ts';
24
24
  export { fileReadParsers, fileWriteParsers, } from '../ports/file-system-port.ts';
25
25
  export type { BuiltSubject, ConformanceOutcome, ConformancePort, ConformanceReport, PortSubject, ScenarioKind, } from './conformance.ts';
26
26
  export { CONFORMANCE_OUTCOME_COUNTS, formatConformanceReport, runClockPortConformance, runCorpusPortConformance, runFileSystemPortConformance, } from './conformance.ts';
27
- export type { ProbeSubject } from './probe-conformance.ts';
28
- export { runEnvironmentProbePortConformance } from './probe-conformance.ts';
27
+ export type { CommandProbeSubject, ProbeSubject } from './probe-conformance.ts';
28
+ export { runCommandLineProbeConformance, runEnvironmentProbePortConformance, } from './probe-conformance.ts';
@@ -16,4 +16,4 @@ export { corpusResolveParsers } from '../ports/corpus-port.js';
16
16
  export { probeParsers } from '../ports/environment-probe-port.js';
17
17
  export { fileReadParsers, fileWriteParsers, } from '../ports/file-system-port.js';
18
18
  export { CONFORMANCE_OUTCOME_COUNTS, formatConformanceReport, runClockPortConformance, runCorpusPortConformance, runFileSystemPortConformance, } from './conformance.js';
19
- export { runEnvironmentProbePortConformance } from './probe-conformance.js';
19
+ export { runCommandLineProbeConformance, runEnvironmentProbePortConformance, } from './probe-conformance.js';
@@ -1,43 +1,27 @@
1
1
  /**
2
- * AD-35's thirteen extra assertions, for the environment-probe port only. The
3
- * subject supplies its policy and one request per denial, since only it knows
4
- * how its own interface-to-target mapping is wired.
2
+ * AD-35's extra assertions for the environment-probe port, one arm per
3
+ * mechanism. The subject supplies its policy and one request per denial,
4
+ * since only it knows how its own interface-to-target mapping is wired.
5
5
  *
6
- * Each of the thirteen builds its own `'resolves'` subject and reads
6
+ * `runEnvironmentProbePortConformance` is the `api` arm: thirteen assertions,
7
+ * every scenario HTTP (redirects, methods, schemes, an anomalous status).
8
+ * `runCommandLineProbeConformance` is the `cli` arm, added once
9
+ * `CommandTargetPolicy` gave the mechanism something to authorize: an
10
+ * unmapped interface, an unmapped executable, an unauthorized subcommand path,
11
+ * a non-zero exit read as an observation, a shell-metacharacter argument
12
+ * proven to reach the process as one literal token rather than a shell
13
+ * expansion, a declared artifact captured, and both caps enforced. The two
14
+ * arms are separate functions rather than one, because their subjects need
15
+ * disjoint fixtures (an HTTP redirect chain has no command analogue, and a
16
+ * subcommand allowlist has no HTTP one) and a subject presenting for one
17
+ * mechanism is not asked to fake the other's scenarios.
18
+ *
19
+ * Each assertion in either arm builds its own `'resolves'` subject and reads
7
20
  * `underlyingCalls()` from a counter starting at zero, so every count below is
8
21
  * absolute.
9
- *
10
- * **This suite certifies an adapter for the `api` mechanism and says nothing
11
- * about the `cli` one.** Every scenario is HTTP: redirects, methods, schemes,
12
- * an anomalous status. `ProbeSubject` requires an unauthorized-method request,
13
- * a redirecting request and a not-found request, none of which a command
14
- * adapter has. So an adapter that runs commands cannot present a subject here,
15
- * and an adapter that passes all nineteen has been shown nothing about whether
16
- * it can run one safely: it may deny an unmapped executable or execute a shell
17
- * string, and this suite cannot tell the difference.
18
- *
19
- * That is a real gap in the published surface and it is wider than this suite.
20
- * `ProbeTargetPolicy` has one authorization shape and every field in it is
21
- * HTTP: scheme, host, port, resolved addresses, methods, safe methods,
22
- * redirects, request and response byte caps. **A command interface cannot be
23
- * authorized at all**, so AD-35's "an adapter denies by default and permits
24
- * only what that mapping names" has nothing to name for the mechanism this
25
- * release adds, and a conformance arm built on top of a policy that cannot
26
- * express a command authorization would certify against nothing.
27
- *
28
- * Closing it is a declaration first and a suite second: a command
29
- * authorization naming a permitted executable, the subcommand paths and
30
- * environment keys it may carry, and its own elapsed and output-byte caps;
31
- * then the denials to certify — an executable no mapping names, a refusal to
32
- * accept a pre-built argument vector, a non-zero exit as an observation rather
33
- * than a fault, each cap enforced. That is a design addition to the published
34
- * surface rather than a repair, and it is not made here. Until it exists an
35
- * adopter writing a command adapter has no policy to declare and no harness to
36
- * run, and this comment is here so nobody concludes from a green run that they
37
- * have either.
38
22
  */
39
23
  import type { ProbeRequest } from '../core/schemas/port-messages.ts';
40
- import type { ProbeTargetPolicy } from '../core/schemas/probe-policy.ts';
24
+ import type { CommandTargetPolicy, ProbeTargetPolicy } from '../core/schemas/probe-policy.ts';
41
25
  import type { ConformanceReport, PortSubject } from './conformance.ts';
42
26
  export type ProbeSubject = PortSubject<ProbeRequest> & {
43
27
  readonly policy: ProbeTargetPolicy;
@@ -72,3 +56,41 @@ export type ProbeSubject = PortSubject<ProbeRequest> & {
72
56
  * stays declared and adapter-enforced.
73
57
  */
74
58
  export declare function runEnvironmentProbePortConformance(subject: ProbeSubject): Promise<ConformanceReport>;
59
+ /**
60
+ * The `cli` arm's subject. Every field is a request `evaluateCommandTarget`
61
+ * decides one specific way against the subject's own `policy`, the same
62
+ * division `ProbeSubject` draws for the `api` arm: the subject knows how its
63
+ * mapping is wired, the suite only knows what each request should produce.
64
+ */
65
+ export type CommandProbeSubject = PortSubject<ProbeRequest> & {
66
+ readonly policy: CommandTargetPolicy;
67
+ /** a request the policy ALLOWS, run against the subject's own fixture executable. */
68
+ readonly authorizedRequest: ProbeRequest;
69
+ /** an interfaceId no authorization names. */
70
+ readonly unmappedInterfaceRequest: ProbeRequest;
71
+ /** an interfaceId the policy names, paired with an executable it never names. */
72
+ readonly unmappedExecutableRequest: ProbeRequest;
73
+ /** an interface-executable pair the policy names, with a subcommandPath no authorization for it permits. */
74
+ readonly unauthorizedSubcommandRequest: ProbeRequest;
75
+ /** authorized, and answered by the fixture exiting non-zero. */
76
+ readonly nonZeroExitRequest: ProbeRequest;
77
+ /** authorized, carrying a shell-metacharacter value on one argument channel key. */
78
+ readonly injectionRequest: ProbeRequest;
79
+ /** the exact literal value `injectionRequest` carries, so the assertion can confirm the fixture received it byte for byte. */
80
+ readonly injectionArgumentValue: string;
81
+ /** authorized, and its authorization declares one artifact identifier the fixture writes when run. */
82
+ readonly artifactRequest: ProbeRequest;
83
+ readonly artifactId: string;
84
+ readonly artifactExpectedText: string;
85
+ /** authorized against a maxElapsedMs the fixture is made to sleep past. */
86
+ readonly overElapsedRequest: ProbeRequest;
87
+ /** authorized against a maxOutputBytes the fixture is made to write past. */
88
+ readonly overOutputRequest: ProbeRequest;
89
+ };
90
+ /**
91
+ * Fifteen outcomes: the six shared assertions plus the nine above. Unlike the
92
+ * `api` arm, every cap here has an assertion: a command subject's fixture
93
+ * script can be told to overrun a byte cap directly, with no oversize-request
94
+ * problem to work around.
95
+ */
96
+ export declare function runCommandLineProbeConformance(subject: CommandProbeSubject): Promise<ConformanceReport>;
@@ -150,6 +150,7 @@ function echoMismatch(request, observation) {
150
150
  }
151
151
  return undefined;
152
152
  }
153
+ /** Reads only `id` and `title`, so both arms' assertion shapes satisfy it structurally. */
153
154
  function checkRejected(assertion, expectedCode, error) {
154
155
  const view = faultView(error);
155
156
  if (view === undefined) {
@@ -213,3 +214,164 @@ export async function runEnvironmentProbePortConformance(subject) {
213
214
  }
214
215
  return reportOf(subject.name, 'environment-probe', [...shared, ...additional]);
215
216
  }
217
+ const COMMAND_ASSERTIONS = [
218
+ {
219
+ id: 'command/allow-authorized-invocation',
220
+ title: 'an explicitly authorized command runs and is observed',
221
+ request: (subject) => subject.authorizedRequest,
222
+ expectation: { kind: 'resolves' },
223
+ },
224
+ {
225
+ id: 'command/observe-nonzero-exit',
226
+ title: 'a non-zero exit from an authorized command is an observation, not a fault',
227
+ request: (subject) => subject.nonZeroExitRequest,
228
+ expectation: {
229
+ kind: 'resolves',
230
+ check: (observation) => observation.kind === 'cli' && observation.exitCode !== 0
231
+ ? undefined
232
+ : `observed ${observation.kind === 'cli' ? `exit code ${observation.exitCode}` : 'an api observation'}, expected a non-zero exit`,
233
+ },
234
+ },
235
+ {
236
+ id: 'command/deny-unmapped-interface',
237
+ title: 'an interface no authorization names is refused before a process spawns',
238
+ request: (subject) => subject.unmappedInterfaceRequest,
239
+ expectation: { kind: 'rejects', code: DENIED },
240
+ expectedCalls: () => 0,
241
+ },
242
+ {
243
+ id: 'command/deny-unmapped-executable',
244
+ title: 'an executable the interface is never paired with is refused before a process spawns',
245
+ request: (subject) => subject.unmappedExecutableRequest,
246
+ expectation: { kind: 'rejects', code: DENIED },
247
+ expectedCalls: () => 0,
248
+ },
249
+ {
250
+ id: 'command/deny-unauthorized-subcommand',
251
+ title: 'a subcommand path outside the authorized set is refused before a process spawns',
252
+ request: (subject) => subject.unauthorizedSubcommandRequest,
253
+ expectation: { kind: 'rejects', code: DENIED },
254
+ expectedCalls: () => 0,
255
+ },
256
+ {
257
+ // The strongest available proof that no shell ever reads a channel value:
258
+ // an argument built to look like a command substitution, checked to reach
259
+ // the process as the one, unmodified literal token it was declared as.
260
+ id: 'command/argument-passed-literally',
261
+ title: 'a shell-metacharacter argument reaches the process as one unmodified literal token, never a shell expansion',
262
+ request: (subject) => subject.injectionRequest,
263
+ expectation: {
264
+ kind: 'resolves',
265
+ check: (observation, subject) => {
266
+ if (observation.kind !== 'cli' || observation.stdout.kind !== 'json') {
267
+ return 'the observation carries no parsed stdout to check the received argument against';
268
+ }
269
+ const argv = observation.stdout.value.argv;
270
+ const received = Array.isArray(argv) ? argv : [];
271
+ return received.length === 1 &&
272
+ received[0] === subject.injectionArgumentValue
273
+ ? undefined
274
+ : `the process observed argv ${JSON.stringify(received)}, expected exactly one element equal to the declared literal`;
275
+ },
276
+ },
277
+ },
278
+ {
279
+ id: 'command/capture-declared-artifact',
280
+ title: "a declared artifact identifier reads the file the run wrote, keyed by the operation's own identifier",
281
+ request: (subject) => subject.artifactRequest,
282
+ expectation: {
283
+ kind: 'resolves',
284
+ check: (observation, subject) => {
285
+ if (observation.kind !== 'cli') {
286
+ return 'the observation is not a command observation';
287
+ }
288
+ const artifact = observation.artifacts[subject.artifactId];
289
+ return artifact?.kind === 'text' &&
290
+ artifact.value === subject.artifactExpectedText
291
+ ? undefined
292
+ : `artifacts["${subject.artifactId}"] was ${JSON.stringify(artifact)}, expected the fixture's declared text`;
293
+ },
294
+ },
295
+ },
296
+ {
297
+ id: 'command/cap-elapsed',
298
+ title: 'a process past maxElapsedMs is capped, not left to finish',
299
+ request: (subject) => subject.overElapsedRequest,
300
+ expectation: { kind: 'rejects', code: CAPPED },
301
+ },
302
+ {
303
+ id: 'command/cap-output-bytes',
304
+ title: 'output past maxOutputBytes is capped, not silently truncated and returned',
305
+ request: (subject) => subject.overOutputRequest,
306
+ expectation: { kind: 'rejects', code: CAPPED },
307
+ },
308
+ ];
309
+ function checkCommandResolved(assertion, expectation, value, request, subject) {
310
+ const parsed = probeParsers.response.safeParse(value);
311
+ if (!parsed.success) {
312
+ return titledOutcome(assertion.id, assertion.title, false, 'the resolved value is not a schema-valid ProbeObservation');
313
+ }
314
+ const mismatch = echoMismatch(request, parsed.data);
315
+ if (mismatch !== undefined) {
316
+ return titledOutcome(assertion.id, assertion.title, false, mismatch);
317
+ }
318
+ const complaint = expectation.check?.(parsed.data, subject);
319
+ return titledOutcome(assertion.id, assertion.title, complaint === undefined, complaint ?? '');
320
+ }
321
+ /** `checkCalls`'s own logic, typed against `CommandAssertion`/`CommandProbeSubject` rather than the `api` arm's pair: a function parameter type is checked contravariantly, so the two assertion shapes cannot share one checker. */
322
+ function checkCommandCalls(assertion, subject, built, base) {
323
+ if (assertion.expectedCalls === undefined)
324
+ return base;
325
+ const expected = assertion.expectedCalls(subject);
326
+ if (typeof expected === 'string') {
327
+ return { ...base, passed: false, detail: expected };
328
+ }
329
+ const actual = countCalls(built);
330
+ if (typeof actual === 'string') {
331
+ return { ...base, passed: false, detail: actual };
332
+ }
333
+ if (actual === expected)
334
+ return base;
335
+ return {
336
+ ...base,
337
+ passed: false,
338
+ detail: `${base.passed ? '' : `${base.detail}; `}underlyingCalls() was ${actual}, expected ${expected}`,
339
+ };
340
+ }
341
+ async function runCommandAssertion(assertion, subject) {
342
+ const run = await buildScenario(subject, 'resolves');
343
+ if (!run.ok) {
344
+ return titledOutcome(assertion.id, assertion.title, false, run.detail);
345
+ }
346
+ const request = assertion.request(subject);
347
+ const settled = await settle(run.built.port(request, new AbortController().signal));
348
+ let result;
349
+ if (assertion.expectation.kind === 'resolves') {
350
+ result =
351
+ settled.kind === 'resolved'
352
+ ? checkCommandResolved(assertion, assertion.expectation, settled.value, request, subject)
353
+ : titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(settled.error)} instead of returning an observation`);
354
+ }
355
+ else {
356
+ result =
357
+ settled.kind === 'rejected'
358
+ ? checkRejected(assertion, assertion.expectation.code, settled.error)
359
+ : titledOutcome(assertion.id, assertion.title, false, `resolved instead of rejecting with "${assertion.expectation.code}"`);
360
+ }
361
+ result = checkCommandCalls(assertion, subject, run.built, result);
362
+ return withDispose(result, await disposeScenario(run.built));
363
+ }
364
+ /**
365
+ * Fifteen outcomes: the six shared assertions plus the nine above. Unlike the
366
+ * `api` arm, every cap here has an assertion: a command subject's fixture
367
+ * script can be told to overrun a byte cap directly, with no oversize-request
368
+ * problem to work around.
369
+ */
370
+ export async function runCommandLineProbeConformance(subject) {
371
+ const shared = await runSharedAssertions('probe', subject, probeParsers.response);
372
+ const additional = [];
373
+ for (const assertion of COMMAND_ASSERTIONS) {
374
+ additional.push(await runCommandAssertion(assertion, subject));
375
+ }
376
+ return reportOf(subject.name, 'command-probe', [...shared, ...additional]);
377
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eval-quality",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Compile disciplined Behavioral Evaluation Contracts and score their ability to catch known defects.",
5
5
  "author": "Murat Ozcan",
6
6
  "license": "Apache-2.0",