eval-quality 1.4.2 → 3.0.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 +6 -6
- package/corpus/dev/README.md +24 -13
- package/corpus/dev/compile-seal-example/brief.json +1 -1
- package/corpus/dev/compile-seal-example/contract.json +1 -1
- package/corpus/dev/contracts/absent-collection-locations.json +1 -1
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
- package/corpus/dev/contracts/absent-success-indicator.json +1 -1
- package/corpus/dev/contracts/captured-read-back.json +1 -0
- package/corpus/dev/contracts/checklist-selection.json +1 -0
- package/corpus/dev/contracts/empty-channel-roles.json +1 -1
- package/corpus/dev/contracts/empty-collection-locations.json +1 -1
- package/corpus/dev/contracts/empty-request-shapes.json +1 -1
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
- package/corpus/dev/contracts/fragment-selection.json +1 -1
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
- package/corpus/dev/contracts/no-operation-inventory.json +1 -1
- package/corpus/dev/contracts/no-read-back-relation.json +1 -1
- package/corpus/dev/contracts/no-state-change-marker.json +1 -1
- package/corpus/dev/contracts/no-type-violating-step.json +1 -1
- package/corpus/dev/contracts/notes-tool-server.json +1 -0
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/review-corpus.json +1 -1
- package/corpus/dev/contracts/satisfied-declarations.json +1 -1
- package/corpus/dev/contracts/single-required-response-key.json +1 -1
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
- package/corpus/dev/index.json +1 -1
- package/dist/adapters/command-line-adapter.js +44 -6
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/mcp-adapter.d.ts +32 -0
- package/dist/adapters/mcp-adapter.js +357 -0
- package/dist/adapters/mcp-target-policy.d.ts +39 -0
- package/dist/adapters/mcp-target-policy.js +30 -0
- package/dist/cli/render.d.ts +8 -3
- package/dist/cli/render.js +13 -6
- package/dist/cli/run.js +2 -1
- package/dist/core/compile/bindings.d.ts +8 -18
- package/dist/core/compile/bindings.js +13 -10
- package/dist/core/compile/compile.js +6 -1
- package/dist/core/compile/interface-inventory.d.ts +81 -3
- package/dist/core/compile/interface-inventory.js +134 -23
- package/dist/core/compile/reachability.d.ts +73 -1
- package/dist/core/compile/reachability.js +134 -19
- package/dist/core/compile/schema-version.d.ts +15 -2
- package/dist/core/compile/schema-version.js +11 -3
- package/dist/core/compile/sensitivity-witness.d.ts +22 -10
- package/dist/core/compile/sensitivity-witness.js +101 -13
- package/dist/core/coverage/operations.d.ts +1 -1
- package/dist/core/coverage/operations.js +1 -1
- package/dist/core/coverage/relevance.d.ts +3 -3
- package/dist/core/coverage/relevance.js +3 -3
- package/dist/core/declared-inputs.d.ts +21 -9
- package/dist/core/declared-inputs.js +51 -16
- package/dist/core/evaluate/evidence-resolution.d.ts +8 -7
- package/dist/core/evaluate/evidence-resolution.js +25 -18
- package/dist/core/preflight/plan.js +43 -6
- package/dist/core/preflight/projection.d.ts +10 -1
- package/dist/core/preflight/projection.js +9 -5
- package/dist/core/preflight/reduce.js +6 -2
- package/dist/core/preflight/witness-evidence.js +31 -9
- package/dist/core/schemas/artifact.d.ts +145 -49
- package/dist/core/schemas/defect-signature.d.ts +280 -23
- package/dist/core/schemas/defect-signature.js +65 -37
- package/dist/core/schemas/eval-contract.d.ts +33 -48
- package/dist/core/schemas/eval-contract.js +3 -3
- package/dist/core/schemas/interface.d.ts +193 -55
- package/dist/core/schemas/interface.js +82 -15
- package/dist/core/schemas/plan.d.ts +45 -1
- package/dist/core/schemas/plan.js +13 -2
- package/dist/core/schemas/pointer.d.ts +23 -9
- package/dist/core/schemas/pointer.js +25 -11
- package/dist/core/schemas/port-messages.d.ts +81 -0
- package/dist/core/schemas/port-messages.js +50 -3
- package/dist/core/schemas/primitives.d.ts +18 -0
- package/dist/core/schemas/primitives.js +29 -0
- package/dist/core/schemas/probe-policy.d.ts +41 -0
- package/dist/core/schemas/probe-policy.js +61 -1
- package/dist/core/schemas/probe.d.ts +156 -2
- package/dist/core/schemas/probe.js +45 -2
- package/dist/core/schemas/sealed-run-record.d.ts +11 -5
- package/dist/core/schemas/sealed-run-record.js +21 -9
- package/dist/core/schemas/sensitivity-witness.d.ts +31 -7
- package/dist/core/schemas/sensitivity-witness.js +32 -9
- package/dist/core/score/bindings.d.ts +1 -1
- package/dist/core/score/bindings.js +4 -4
- package/dist/core/score/qualification.d.ts +7 -5
- package/dist/core/score/qualification.js +92 -23
- package/dist/core/score/score.d.ts +1 -1
- package/dist/core/score/score.js +24 -1
- package/dist/core/seal/derived-reference.js +20 -17
- package/dist/core/seal/plan-index.d.ts +18 -9
- package/dist/core/seal/plan-index.js +79 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ports/environment-probe-port.d.ts +50 -17
- package/dist/ports/environment-probe-port.js +26 -17
- package/dist/testing/conformance.d.ts +3 -2
- package/dist/testing/conformance.js +2 -1
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +1 -1
- package/dist/testing/probe-conformance.d.ts +90 -13
- package/dist/testing/probe-conformance.js +375 -160
- package/package.json +4 -2
- package/schemas/eval-contract.schema.json +571 -18
- package/schemas/probe.schema.json +152 -12
- package/schemas/rubric.schema.json +1 -1
- package/schemas/sealed-run-record.schema.json +24 -17
|
@@ -1,10 +1,115 @@
|
|
|
1
1
|
import { probeParsers } from '../ports/environment-probe-port.js';
|
|
2
2
|
import { buildScenario, countCalls, describeThrown, disposeScenario, faultView, reportOf, runSharedAssertions, settle, titledOutcome, withDispose, } from './conformance.js';
|
|
3
|
+
const DENIED = 'forbidden-target';
|
|
4
|
+
const CAPPED = 'budget-exhausted';
|
|
5
|
+
/**
|
|
6
|
+
* Which of the four echoed fields the observation failed to return unchanged,
|
|
7
|
+
* or `undefined` when it answered the question it was asked.
|
|
8
|
+
*/
|
|
9
|
+
function echoMismatch(request, observation) {
|
|
10
|
+
const echoed = [
|
|
11
|
+
['kind', request.kind, observation.kind],
|
|
12
|
+
['probeId', request.probeId, observation.probeId],
|
|
13
|
+
['interfaceId', request.interfaceId, observation.interfaceId],
|
|
14
|
+
['operationId', request.operationId, observation.operationId],
|
|
15
|
+
];
|
|
16
|
+
for (const [field, asked, answered] of echoed) {
|
|
17
|
+
if (asked === answered)
|
|
18
|
+
continue;
|
|
19
|
+
return `observed ${field} "${answered}" for a request carrying "${asked}", so the answer does not correlate with the question`;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
function checkRejected(assertion, expectedCode, error) {
|
|
24
|
+
const view = faultView(error);
|
|
25
|
+
if (view === undefined) {
|
|
26
|
+
return titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(error)}, which carries no declared AD-28 code`);
|
|
27
|
+
}
|
|
28
|
+
return titledOutcome(assertion.id, assertion.title, view.code === expectedCode, `rejected with code "${view.code}", expected "${expectedCode}"`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Schema, then correlation, then the assertion's own predicate. Generic over
|
|
32
|
+
* the subject rather than written once per arm: a function parameter is
|
|
33
|
+
* checked contravariantly, so an arm's `check` cannot be widened across
|
|
34
|
+
* subject types, and inferring the subject here costs the assertion records no
|
|
35
|
+
* type parameter of their own.
|
|
36
|
+
*/
|
|
37
|
+
function checkResolvedFor(assertion, check, value, request, subject) {
|
|
38
|
+
const parsed = probeParsers.response.safeParse(value);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
return titledOutcome(assertion.id, assertion.title, false, 'the resolved value is not a schema-valid ProbeObservation');
|
|
41
|
+
}
|
|
42
|
+
// Schema validity is not correlation. Both messages are unions, so an
|
|
43
|
+
// adapter can answer a command request or a tool call with a schema-valid
|
|
44
|
+
// HTTP observation and satisfy every assertion below without running a
|
|
45
|
+
// command, opening a session, or consulting a policy. The four echoed
|
|
46
|
+
// fields are what tie one answer to one question, and `ProbeRequest.probeId`'s
|
|
47
|
+
// own description already says the port returns them unchanged.
|
|
48
|
+
const mismatch = echoMismatch(request, parsed.data);
|
|
49
|
+
if (mismatch !== undefined) {
|
|
50
|
+
return titledOutcome(assertion.id, assertion.title, false, mismatch);
|
|
51
|
+
}
|
|
52
|
+
const complaint = check?.(parsed.data, subject);
|
|
53
|
+
return titledOutcome(assertion.id, assertion.title, complaint === undefined, complaint ?? '');
|
|
54
|
+
}
|
|
55
|
+
/** Generic for `checkResolvedFor`'s reason: `expectedCalls` names its own arm's subject type. */
|
|
56
|
+
function checkCallCount(expectedCalls, subject, built, base) {
|
|
57
|
+
if (expectedCalls === undefined)
|
|
58
|
+
return base;
|
|
59
|
+
// Every branch keeps whatever the assertion's own check already said. An
|
|
60
|
+
// outcome that failed its predicate AND hit a subject-side count problem
|
|
61
|
+
// used to report only the second, which hid the reason the assertion was
|
|
62
|
+
// looked at in the first place.
|
|
63
|
+
const also = (complaint) => ({
|
|
64
|
+
...base,
|
|
65
|
+
passed: false,
|
|
66
|
+
detail: base.passed ? complaint : `${base.detail}; ${complaint}`,
|
|
67
|
+
});
|
|
68
|
+
const expected = expectedCalls(subject);
|
|
69
|
+
if (typeof expected === 'string')
|
|
70
|
+
return also(expected);
|
|
71
|
+
const actual = countCalls(built);
|
|
72
|
+
if (typeof actual === 'string')
|
|
73
|
+
return also(actual);
|
|
74
|
+
if (actual === expected)
|
|
75
|
+
return base;
|
|
76
|
+
return also(`underlyingCalls() was ${actual}, expected ${expected}`);
|
|
77
|
+
}
|
|
78
|
+
async function runArmAssertion(assertion, subject) {
|
|
79
|
+
const run = await buildScenario(subject, 'resolves');
|
|
80
|
+
if (!run.ok) {
|
|
81
|
+
return titledOutcome(assertion.id, assertion.title, false, run.detail);
|
|
82
|
+
}
|
|
83
|
+
const request = assertion.request(subject);
|
|
84
|
+
const settled = await settle(run.built.port(request, new AbortController().signal));
|
|
85
|
+
let result;
|
|
86
|
+
if (assertion.expectation.kind === 'resolves') {
|
|
87
|
+
result =
|
|
88
|
+
settled.kind === 'resolved'
|
|
89
|
+
? checkResolvedFor(assertion, assertion.expectation.check, settled.value, request, subject)
|
|
90
|
+
: titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(settled.error)} instead of returning an observation`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result =
|
|
94
|
+
settled.kind === 'rejected'
|
|
95
|
+
? checkRejected(assertion, assertion.expectation.code, settled.error)
|
|
96
|
+
: titledOutcome(assertion.id, assertion.title, false, `resolved instead of rejecting with "${assertion.expectation.code}"`);
|
|
97
|
+
}
|
|
98
|
+
result = checkCallCount(assertion.expectedCalls, subject, run.built, result);
|
|
99
|
+
return withDispose(result, await disposeScenario(run.built));
|
|
100
|
+
}
|
|
101
|
+
/** The six shared assertions, then the arm's own, in list order. */
|
|
102
|
+
async function runArm(subject, port, assertions) {
|
|
103
|
+
const shared = await runSharedAssertions('probe', subject, probeParsers.response);
|
|
104
|
+
const additional = [];
|
|
105
|
+
for (const assertion of assertions) {
|
|
106
|
+
additional.push(await runArmAssertion(assertion, subject));
|
|
107
|
+
}
|
|
108
|
+
return reportOf(subject.name, port, [...shared, ...additional]);
|
|
109
|
+
}
|
|
3
110
|
function authorizationFor(subject, request) {
|
|
4
111
|
return subject.policy.authorizations.find((each) => each.interfaceId === request.interfaceId);
|
|
5
112
|
}
|
|
6
|
-
const DENIED = 'forbidden-target';
|
|
7
|
-
const CAPPED = 'budget-exhausted';
|
|
8
113
|
const PROBE_ASSERTIONS = [
|
|
9
114
|
{
|
|
10
115
|
// Without this one, a subject that denies everything scores twelve of
|
|
@@ -18,6 +123,13 @@ const PROBE_ASSERTIONS = [
|
|
|
18
123
|
{
|
|
19
124
|
// A subject that throws on 5xx fails here and nowhere else, and it makes
|
|
20
125
|
// AD-10's seeded-fault check unimplementable.
|
|
126
|
+
//
|
|
127
|
+
// The non-`api` arm of the detail below names the kind it observed. It
|
|
128
|
+
// is reached when the subject declares a `faultingRequest` of another
|
|
129
|
+
// kind and answers it correlated, which `ProbeSubject` admits because
|
|
130
|
+
// the field is typed over the whole request union. Substituting only
|
|
131
|
+
// the observation's kind does not reach it: `echoMismatch` compares
|
|
132
|
+
// `kind` among the four echoed fields and short-circuits first.
|
|
21
133
|
id: 'probe/observe-anomalous-status',
|
|
22
134
|
title: 'a 500 from an authorized target is an observation, not a fault',
|
|
23
135
|
request: (subject) => subject.faultingRequest,
|
|
@@ -25,7 +137,7 @@ const PROBE_ASSERTIONS = [
|
|
|
25
137
|
kind: 'resolves',
|
|
26
138
|
check: (observation) => observation.kind === 'api' && observation.status === 500
|
|
27
139
|
? undefined
|
|
28
|
-
: `observed ${observation.kind === 'api' ? `status ${observation.status}` :
|
|
140
|
+
: `observed ${observation.kind === 'api' ? `status ${observation.status}` : `an observation of kind "${observation.kind}"`}, expected status 500`,
|
|
29
141
|
},
|
|
30
142
|
},
|
|
31
143
|
{
|
|
@@ -114,92 +226,6 @@ const PROBE_ASSERTIONS = [
|
|
|
114
226
|
expectation: { kind: 'rejects', code: CAPPED },
|
|
115
227
|
},
|
|
116
228
|
];
|
|
117
|
-
function checkResolved(assertion, expectation, value, request) {
|
|
118
|
-
const parsed = probeParsers.response.safeParse(value);
|
|
119
|
-
if (!parsed.success) {
|
|
120
|
-
return titledOutcome(assertion.id, assertion.title, false, 'the resolved value is not a schema-valid ProbeObservation');
|
|
121
|
-
}
|
|
122
|
-
// Schema validity is not correlation. Both messages are unions now, so an
|
|
123
|
-
// adapter can answer a command request with a schema-valid HTTP observation
|
|
124
|
-
// and satisfy every assertion below without running a command or consulting
|
|
125
|
-
// a policy. The four echoed fields are what tie one answer to one question,
|
|
126
|
-
// and `ProbeRequest.probeId`'s own description already says the port returns
|
|
127
|
-
// them unchanged.
|
|
128
|
-
const mismatch = echoMismatch(request, parsed.data);
|
|
129
|
-
if (mismatch !== undefined) {
|
|
130
|
-
return titledOutcome(assertion.id, assertion.title, false, mismatch);
|
|
131
|
-
}
|
|
132
|
-
const complaint = expectation.check?.(parsed.data);
|
|
133
|
-
return titledOutcome(assertion.id, assertion.title, complaint === undefined, complaint ?? '');
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Which of the four echoed fields the observation failed to return unchanged,
|
|
137
|
-
* or `undefined` when it answered the question it was asked.
|
|
138
|
-
*/
|
|
139
|
-
function echoMismatch(request, observation) {
|
|
140
|
-
const echoed = [
|
|
141
|
-
['kind', request.kind, observation.kind],
|
|
142
|
-
['probeId', request.probeId, observation.probeId],
|
|
143
|
-
['interfaceId', request.interfaceId, observation.interfaceId],
|
|
144
|
-
['operationId', request.operationId, observation.operationId],
|
|
145
|
-
];
|
|
146
|
-
for (const [field, asked, answered] of echoed) {
|
|
147
|
-
if (asked === answered)
|
|
148
|
-
continue;
|
|
149
|
-
return `observed ${field} "${answered}" for a request carrying "${asked}", so the answer does not correlate with the question`;
|
|
150
|
-
}
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
/** Reads only `id` and `title`, so both arms' assertion shapes satisfy it structurally. */
|
|
154
|
-
function checkRejected(assertion, expectedCode, error) {
|
|
155
|
-
const view = faultView(error);
|
|
156
|
-
if (view === undefined) {
|
|
157
|
-
return titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(error)}, which carries no declared AD-28 code`);
|
|
158
|
-
}
|
|
159
|
-
return titledOutcome(assertion.id, assertion.title, view.code === expectedCode, `rejected with code "${view.code}", expected "${expectedCode}"`);
|
|
160
|
-
}
|
|
161
|
-
function checkCalls(assertion, subject, built, base) {
|
|
162
|
-
if (assertion.expectedCalls === undefined)
|
|
163
|
-
return base;
|
|
164
|
-
const expected = assertion.expectedCalls(subject);
|
|
165
|
-
if (typeof expected === 'string') {
|
|
166
|
-
return { ...base, passed: false, detail: expected };
|
|
167
|
-
}
|
|
168
|
-
const actual = countCalls(built);
|
|
169
|
-
if (typeof actual === 'string') {
|
|
170
|
-
return { ...base, passed: false, detail: actual };
|
|
171
|
-
}
|
|
172
|
-
if (actual === expected)
|
|
173
|
-
return base;
|
|
174
|
-
return {
|
|
175
|
-
...base,
|
|
176
|
-
passed: false,
|
|
177
|
-
detail: `${base.passed ? '' : `${base.detail}; `}underlyingCalls() was ${actual}, expected ${expected}`,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
async function runProbeAssertion(assertion, subject) {
|
|
181
|
-
const run = await buildScenario(subject, 'resolves');
|
|
182
|
-
if (!run.ok) {
|
|
183
|
-
return titledOutcome(assertion.id, assertion.title, false, run.detail);
|
|
184
|
-
}
|
|
185
|
-
const request = assertion.request(subject);
|
|
186
|
-
const settled = await settle(run.built.port(request, new AbortController().signal));
|
|
187
|
-
let result;
|
|
188
|
-
if (assertion.expectation.kind === 'resolves') {
|
|
189
|
-
result =
|
|
190
|
-
settled.kind === 'resolved'
|
|
191
|
-
? checkResolved(assertion, assertion.expectation, settled.value, request)
|
|
192
|
-
: titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(settled.error)} instead of returning an observation`);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
result =
|
|
196
|
-
settled.kind === 'rejected'
|
|
197
|
-
? checkRejected(assertion, assertion.expectation.code, settled.error)
|
|
198
|
-
: titledOutcome(assertion.id, assertion.title, false, `resolved instead of rejecting with "${assertion.expectation.code}"`);
|
|
199
|
-
}
|
|
200
|
-
result = checkCalls(assertion, subject, run.built, result);
|
|
201
|
-
return withDispose(result, await disposeScenario(run.built));
|
|
202
|
-
}
|
|
203
229
|
/**
|
|
204
230
|
* Nineteen outcomes: the six shared assertions plus AD-35's thirteen.
|
|
205
231
|
* `maxRequestBytes` is the one cap with no assertion; the request shape is the
|
|
@@ -207,12 +233,24 @@ async function runProbeAssertion(assertion, subject) {
|
|
|
207
233
|
* stays declared and adapter-enforced.
|
|
208
234
|
*/
|
|
209
235
|
export async function runEnvironmentProbePortConformance(subject) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
return runArm(subject, 'environment-probe', PROBE_ASSERTIONS);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The authorization the subject's own command policy resolves this request to.
|
|
240
|
+
* Keyed by the pair, since `CommandTargetAuthorization` is.
|
|
241
|
+
*
|
|
242
|
+
* The two denial assertions that call it read it for the reason
|
|
243
|
+
* `mcpAuthorizationFor` exists: a subject whose "unmapped" request is in fact
|
|
244
|
+
* mapped passes its own denial for the wrong reason.
|
|
245
|
+
*
|
|
246
|
+
* It takes the narrowed request, so each caller states what it does about a
|
|
247
|
+
* subject that declared another kind there rather than inheriting a silent
|
|
248
|
+
* `undefined` from here. `mcpAuthorizationFor` needs no such narrowing because
|
|
249
|
+
* it reads `interfaceId`, which every member of the request union carries.
|
|
250
|
+
*/
|
|
251
|
+
function commandAuthorizationFor(subject, request) {
|
|
252
|
+
return subject.policy.authorizations.find((each) => each.interfaceId === request.interfaceId &&
|
|
253
|
+
each.executable === request.executable);
|
|
216
254
|
}
|
|
217
255
|
const COMMAND_ASSERTIONS = [
|
|
218
256
|
{
|
|
@@ -222,6 +260,11 @@ const COMMAND_ASSERTIONS = [
|
|
|
222
260
|
expectation: { kind: 'resolves' },
|
|
223
261
|
},
|
|
224
262
|
{
|
|
263
|
+
// The non-`cli` arm of the detail below is reached the same way the
|
|
264
|
+
// `api` arm's is: `nonZeroExitRequest` is typed over the whole request
|
|
265
|
+
// union, so a subject may declare another kind there and answer it
|
|
266
|
+
// correlated. It names the observed kind; the binary ternary it
|
|
267
|
+
// replaced called every one of them "an api observation".
|
|
225
268
|
id: 'command/observe-nonzero-exit',
|
|
226
269
|
title: 'a non-zero exit from an authorized command is an observation, not a fault',
|
|
227
270
|
request: (subject) => subject.nonZeroExitRequest,
|
|
@@ -229,7 +272,7 @@ const COMMAND_ASSERTIONS = [
|
|
|
229
272
|
kind: 'resolves',
|
|
230
273
|
check: (observation) => observation.kind === 'cli' && observation.exitCode !== 0
|
|
231
274
|
? undefined
|
|
232
|
-
: `observed ${observation.kind === 'cli' ? `exit code ${observation.exitCode}` :
|
|
275
|
+
: `observed ${observation.kind === 'cli' ? `exit code ${observation.exitCode}` : `an observation of kind "${observation.kind}"`}, expected a non-zero exit`,
|
|
233
276
|
},
|
|
234
277
|
},
|
|
235
278
|
{
|
|
@@ -237,21 +280,71 @@ const COMMAND_ASSERTIONS = [
|
|
|
237
280
|
title: 'an interface no authorization names is refused before a process spawns',
|
|
238
281
|
request: (subject) => subject.unmappedInterfaceRequest,
|
|
239
282
|
expectation: { kind: 'rejects', code: DENIED },
|
|
240
|
-
expectedCalls: () =>
|
|
283
|
+
expectedCalls: (subject) => subject.policy.authorizations.some((each) => each.interfaceId === subject.unmappedInterfaceRequest.interfaceId)
|
|
284
|
+
? "the subject's policy names an authorization for unmappedInterfaceRequest.interfaceId, so the request it presents as unmapped is mapped"
|
|
285
|
+
: 0,
|
|
241
286
|
},
|
|
242
287
|
{
|
|
243
288
|
id: 'command/deny-unmapped-executable',
|
|
244
289
|
title: 'an executable the interface is never paired with is refused before a process spawns',
|
|
245
290
|
request: (subject) => subject.unmappedExecutableRequest,
|
|
246
291
|
expectation: { kind: 'rejects', code: DENIED },
|
|
247
|
-
expectedCalls: () =>
|
|
292
|
+
expectedCalls: (subject) => {
|
|
293
|
+
const request = subject.unmappedExecutableRequest;
|
|
294
|
+
if (request.kind !== 'cli') {
|
|
295
|
+
return `unmappedExecutableRequest declares a "${request.kind}" request, which names no executable for the policy to refuse`;
|
|
296
|
+
}
|
|
297
|
+
return commandAuthorizationFor(subject, request) === undefined
|
|
298
|
+
? 0
|
|
299
|
+
: "the subject's policy pairs unmappedExecutableRequest's interface with its executable, so the pair it presents as unmapped is mapped";
|
|
300
|
+
},
|
|
248
301
|
},
|
|
249
302
|
{
|
|
250
303
|
id: 'command/deny-unauthorized-subcommand',
|
|
251
304
|
title: 'a subcommand path outside the authorized set is refused before a process spawns',
|
|
252
305
|
request: (subject) => subject.unauthorizedSubcommandRequest,
|
|
253
306
|
expectation: { kind: 'rejects', code: DENIED },
|
|
254
|
-
expectedCalls: () =>
|
|
307
|
+
expectedCalls: (subject) => {
|
|
308
|
+
const request = subject.unauthorizedSubcommandRequest;
|
|
309
|
+
if (request.kind !== 'cli') {
|
|
310
|
+
return `unauthorizedSubcommandRequest declares a "${request.kind}" request, which names no subcommand path for the policy to refuse`;
|
|
311
|
+
}
|
|
312
|
+
const authorization = commandAuthorizationFor(subject, request);
|
|
313
|
+
if (authorization === undefined) {
|
|
314
|
+
return "the subject's policy names no authorization for unauthorizedSubcommandRequest's interface and executable, so this case cannot tell an unmapped pair from an unauthorized subcommand";
|
|
315
|
+
}
|
|
316
|
+
return authorization.permittedSubcommandPaths.some((permitted) => permitted.length === request.subcommandPath.length &&
|
|
317
|
+
permitted.every((segment, at) => segment === request.subcommandPath[at]))
|
|
318
|
+
? `the subject's policy permits subcommand path ${JSON.stringify(request.subcommandPath)}, so the request it presents as unauthorized is authorized`
|
|
319
|
+
: 0;
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
// The environment channel is the one a contract author declares and an
|
|
324
|
+
// operator has to bound. This is the assertion that holds an adapter to
|
|
325
|
+
// bounding it: without `permittedEnvironmentKeys` enforced, any key the
|
|
326
|
+
// contract names reaches the process.
|
|
327
|
+
id: 'command/deny-unauthorized-environment-key',
|
|
328
|
+
title: 'an environment key the authorization does not permit is refused before a process spawns',
|
|
329
|
+
request: (subject) => subject.unauthorizedEnvironmentKeyRequest,
|
|
330
|
+
expectation: { kind: 'rejects', code: DENIED },
|
|
331
|
+
expectedCalls: (subject) => {
|
|
332
|
+
const request = subject.unauthorizedEnvironmentKeyRequest;
|
|
333
|
+
if (request.kind !== 'cli') {
|
|
334
|
+
return `unauthorizedEnvironmentKeyRequest declares a "${request.kind}" request, which carries no environment channel for the policy to refuse`;
|
|
335
|
+
}
|
|
336
|
+
const authorization = commandAuthorizationFor(subject, request);
|
|
337
|
+
if (authorization === undefined) {
|
|
338
|
+
return "the subject's policy names no authorization for unauthorizedEnvironmentKeyRequest's interface and executable, so this case cannot tell an unmapped pair from an unauthorized environment key";
|
|
339
|
+
}
|
|
340
|
+
const declared = Object.keys(request.channels.environment);
|
|
341
|
+
if (declared.length === 0) {
|
|
342
|
+
return 'unauthorizedEnvironmentKeyRequest declares no environment key, so nothing about the allowlist is exercised';
|
|
343
|
+
}
|
|
344
|
+
return declared.every((key) => authorization.permittedEnvironmentKeys.includes(key))
|
|
345
|
+
? `the subject's policy permits every environment key unauthorizedEnvironmentKeyRequest declares (${JSON.stringify(declared)}), so the request it presents as unauthorized is authorized`
|
|
346
|
+
: 0;
|
|
347
|
+
},
|
|
255
348
|
},
|
|
256
349
|
{
|
|
257
350
|
// The strongest available proof that no shell ever reads a channel value:
|
|
@@ -306,72 +399,194 @@ const COMMAND_ASSERTIONS = [
|
|
|
306
399
|
expectation: { kind: 'rejects', code: CAPPED },
|
|
307
400
|
},
|
|
308
401
|
];
|
|
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
402
|
/**
|
|
365
|
-
*
|
|
403
|
+
* Sixteen outcomes: the six shared assertions plus the ten above. Unlike the
|
|
366
404
|
* `api` arm, every cap here has an assertion: a command subject's fixture
|
|
367
405
|
* script can be told to overrun a byte cap directly, with no oversize-request
|
|
368
406
|
* problem to work around.
|
|
369
407
|
*/
|
|
370
408
|
export async function runCommandLineProbeConformance(subject) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
409
|
+
return runArm(subject, 'command-probe', COMMAND_ASSERTIONS);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* The authorization the subject's own policy resolves this request to.
|
|
413
|
+
*
|
|
414
|
+
* The two denial assertions read it to check the subject is internally
|
|
415
|
+
* consistent: a subject whose "unmapped" request is in fact mapped, or whose
|
|
416
|
+
* "unauthorized" tool is in fact on the allowlist, would pass its own denial
|
|
417
|
+
* for the wrong reason and certify an adapter that never refused anything.
|
|
418
|
+
*/
|
|
419
|
+
function mcpAuthorizationFor(subject, request) {
|
|
420
|
+
return subject.policy.authorizations.find((each) => each.interfaceId === request.interfaceId);
|
|
421
|
+
}
|
|
422
|
+
/** The tool's structured result as a key map, or `undefined` when the observation carries no JSON object there. */
|
|
423
|
+
function structuredResultOf(observation) {
|
|
424
|
+
if (observation.kind !== 'mcp' || observation.result.kind !== 'json') {
|
|
425
|
+
return undefined;
|
|
375
426
|
}
|
|
376
|
-
|
|
427
|
+
const { value } = observation.result;
|
|
428
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
429
|
+
? value
|
|
430
|
+
: undefined;
|
|
431
|
+
}
|
|
432
|
+
const MCP_ASSERTIONS = [
|
|
433
|
+
{
|
|
434
|
+
id: 'mcp/allow-authorized-tool-call',
|
|
435
|
+
title: 'an explicitly authorized tool call reaches its server and is observed',
|
|
436
|
+
request: (subject) => subject.authorizedRequest,
|
|
437
|
+
expectation: { kind: 'resolves' },
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
// The rule a tool-use adapter breaks first. A tool's own error result is
|
|
441
|
+
// the payload AD-10's seeded-fault check reads, so an adapter that
|
|
442
|
+
// throws on it makes every probe written to catch that refusal invisible.
|
|
443
|
+
//
|
|
444
|
+
// The non-`mcp` arm of the detail is reachable the way the other two
|
|
445
|
+
// arms' are: `errorResultRequest` is typed over the whole request union,
|
|
446
|
+
// so a subject may declare another kind there and answer it correlated,
|
|
447
|
+
// which is what gets past `echoMismatch` to this check.
|
|
448
|
+
id: 'mcp/observe-error-result',
|
|
449
|
+
title: 'a tool-reported error from an authorized server is an observation, not a fault',
|
|
450
|
+
request: (subject) => subject.errorResultRequest,
|
|
451
|
+
expectation: {
|
|
452
|
+
kind: 'resolves',
|
|
453
|
+
check: (observation) => {
|
|
454
|
+
if (observation.kind !== 'mcp') {
|
|
455
|
+
return `observed an observation of kind "${observation.kind}", expected the envelope's error flag set`;
|
|
456
|
+
}
|
|
457
|
+
if (!observation.isError) {
|
|
458
|
+
return "observed a tool call reporting no error, expected the envelope's error flag set";
|
|
459
|
+
}
|
|
460
|
+
// The flag alone is half the rule. An adapter that reads the
|
|
461
|
+
// envelope and drops what the tool said about the failure passes a
|
|
462
|
+
// flag-only check, and the seeded-fault oracle downstream then has
|
|
463
|
+
// the error announced and nothing to assert on.
|
|
464
|
+
//
|
|
465
|
+
// The detail names the subject's obligation rather than accusing
|
|
466
|
+
// the adapter, because the two are indistinguishable from here: a
|
|
467
|
+
// tool reporting a failure through a prose `content` array and no
|
|
468
|
+
// structured content produces the same absent channel, and the
|
|
469
|
+
// kind's first version describes structured results only.
|
|
470
|
+
return observation.result.kind === 'absent'
|
|
471
|
+
? 'the error came back on an absent result channel; errorResultRequest has to name a tool whose failure carries structured content, since a tool that publishes none looks the same here as an adapter that dropped it'
|
|
472
|
+
: undefined;
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
id: 'mcp/deny-unmapped-interface',
|
|
478
|
+
title: 'an interface no authorization names is refused before a server process starts',
|
|
479
|
+
request: (subject) => subject.unmappedInterfaceRequest,
|
|
480
|
+
expectation: { kind: 'rejects', code: DENIED },
|
|
481
|
+
expectedCalls: (subject) => mcpAuthorizationFor(subject, subject.unmappedInterfaceRequest) ===
|
|
482
|
+
undefined
|
|
483
|
+
? 0
|
|
484
|
+
: "the subject's policy names an authorization for unmappedInterfaceRequest.interfaceId, so the request it presents as unmapped is mapped",
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
// The tool allowlist is the second and last authorization-scoped field,
|
|
488
|
+
// and it draws AD-35's disclosure boundary inside the server: a tool the
|
|
489
|
+
// list omits is unreachable even when the server publishes it.
|
|
490
|
+
id: 'mcp/deny-unauthorized-tool',
|
|
491
|
+
title: 'a tool outside the authorized list is refused before a server process starts',
|
|
492
|
+
request: (subject) => subject.unauthorizedToolRequest,
|
|
493
|
+
expectation: { kind: 'rejects', code: DENIED },
|
|
494
|
+
expectedCalls: (subject) => {
|
|
495
|
+
const request = subject.unauthorizedToolRequest;
|
|
496
|
+
if (request.kind !== 'mcp') {
|
|
497
|
+
return `unauthorizedToolRequest declares a "${request.kind}" request, which names no tool for the policy to refuse`;
|
|
498
|
+
}
|
|
499
|
+
const authorization = mcpAuthorizationFor(subject, request);
|
|
500
|
+
if (authorization === undefined) {
|
|
501
|
+
return "the subject's policy names no authorization for unauthorizedToolRequest.interfaceId, so this case cannot tell an unmapped interface from an unauthorized tool";
|
|
502
|
+
}
|
|
503
|
+
return authorization.tools.includes(request.toolName)
|
|
504
|
+
? `the subject's policy permits tool "${request.toolName}" on interface "${request.interfaceId}", so the request it presents as unauthorized is authorized`
|
|
505
|
+
: 0;
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
// The tool-call twin of the command arm's literal-argument proof. A
|
|
510
|
+
// declared value crosses a JSON-RPC frame here rather than a command
|
|
511
|
+
// line, so what this catches is a value re-encoded, coerced, or
|
|
512
|
+
// truncated in framing, and the metacharacters are what make a
|
|
513
|
+
// re-encoding visible in the echo.
|
|
514
|
+
id: 'mcp/arguments-passed-as-declared',
|
|
515
|
+
title: 'a declared argument value reaches the tool byte for byte',
|
|
516
|
+
request: (subject) => subject.argumentEchoRequest,
|
|
517
|
+
expectation: {
|
|
518
|
+
kind: 'resolves',
|
|
519
|
+
check: (observation, subject) => {
|
|
520
|
+
if (observation.kind !== 'mcp') {
|
|
521
|
+
return `observed an observation of kind "${observation.kind}", expected a tool call`;
|
|
522
|
+
}
|
|
523
|
+
const result = structuredResultOf(observation);
|
|
524
|
+
if (result === undefined) {
|
|
525
|
+
return 'the observation carries no structured result to read the received argument from';
|
|
526
|
+
}
|
|
527
|
+
const received = result[subject.argumentEchoResultKey];
|
|
528
|
+
return received === subject.argumentEchoValue
|
|
529
|
+
? undefined
|
|
530
|
+
: `the tool reported receiving ${JSON.stringify(received)} on "${subject.argumentEchoResultKey}", expected the declared literal`;
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
// An adapter that reads the envelope and drops `structuredContent`
|
|
536
|
+
// still resolves and still correlates, and every oracle over the result
|
|
537
|
+
// then resolves absent. That reads as a contract nothing satisfies
|
|
538
|
+
// rather than as an adapter that answered nowhere, which is the failure
|
|
539
|
+
// this assertion exists to name.
|
|
540
|
+
id: 'mcp/observe-declared-result-channel',
|
|
541
|
+
title: "the tool's structured result is carried on the channel its descriptor describes",
|
|
542
|
+
request: (subject) => subject.structuredResultRequest,
|
|
543
|
+
expectation: {
|
|
544
|
+
kind: 'resolves',
|
|
545
|
+
check: (observation, subject) => {
|
|
546
|
+
if (observation.kind !== 'mcp') {
|
|
547
|
+
return `observed an observation of kind "${observation.kind}", expected a tool call`;
|
|
548
|
+
}
|
|
549
|
+
const result = structuredResultOf(observation);
|
|
550
|
+
if (result === undefined) {
|
|
551
|
+
return `the result channel carried ${JSON.stringify(observation.result)}, expected the structured result carrying ${JSON.stringify(subject.structuredResultKeys)}`;
|
|
552
|
+
}
|
|
553
|
+
const missing = subject.structuredResultKeys.filter((key) => !(key in result));
|
|
554
|
+
return missing.length === 0
|
|
555
|
+
? undefined
|
|
556
|
+
: `the result channel is missing ${JSON.stringify(missing)}`;
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: 'mcp/cap-elapsed',
|
|
562
|
+
title: 'a session past maxElapsedMs is capped, not left running for the caller',
|
|
563
|
+
request: (subject) => subject.overElapsedRequest,
|
|
564
|
+
expectation: { kind: 'rejects', code: CAPPED },
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: 'mcp/cap-result-bytes',
|
|
568
|
+
title: 'a result past maxOutputBytes is capped, not returned in part',
|
|
569
|
+
request: (subject) => subject.overResultBytesRequest,
|
|
570
|
+
expectation: { kind: 'rejects', code: CAPPED },
|
|
571
|
+
},
|
|
572
|
+
];
|
|
573
|
+
/**
|
|
574
|
+
* Fourteen outcomes: the six shared assertions plus the eight above. Fewer
|
|
575
|
+
* than either other arm because a tool-server authorization scopes two fields:
|
|
576
|
+
* one session is opened against one server and every tool it offers belongs to
|
|
577
|
+
* that server, so the interface identifier is the server identity and the tool
|
|
578
|
+
* allowlist is the only thing left to deny on.
|
|
579
|
+
*
|
|
580
|
+
* Two rules the reference adapter follows are outside what a green run
|
|
581
|
+
* certifies, and an author reading this should know which. A JSON-RPC error
|
|
582
|
+
* answering `tools/call` is an observation on the same terms a tool-reported
|
|
583
|
+
* error is, and the byte cap applies to the server's own stderr as well as its
|
|
584
|
+
* stdout. Neither has an assertion here, because the derivation above gives
|
|
585
|
+
* each arm one denial per authorization-scoped field and one assertion per cap
|
|
586
|
+
* the suite can make a subject exceed, and both of these are a second spelling
|
|
587
|
+
* of an assertion the arm already carries. The reference adapter's own tests
|
|
588
|
+
* cover both.
|
|
589
|
+
*/
|
|
590
|
+
export async function runMcpProbeConformance(subject) {
|
|
591
|
+
return runArm(subject, 'mcp-probe', MCP_ASSERTIONS);
|
|
377
592
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eval-quality",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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",
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
"format": "biome format --write .",
|
|
74
74
|
"check:docs": "node scripts/check-docs.mjs",
|
|
75
75
|
"check:doc-invocations": "node scripts/check-doc-invocations.mjs",
|
|
76
|
+
"check:doc-counts": "node scripts/check-doc-counts.ts",
|
|
77
|
+
"check:doc-claims": "node scripts/check-doc-claims.ts",
|
|
76
78
|
"lint:spine": "python3 scripts/spine-lint/lint_spine.py --registry-ad 5 --workspace-root . --fail-on high",
|
|
77
79
|
"test:spine-lint": "uv run --with pytest pytest scripts/spine-lint/tests -q",
|
|
78
80
|
"build:shareable": "node scripts/build-shareable.mjs",
|
|
@@ -110,7 +112,7 @@
|
|
|
110
112
|
"release:major": "gh workflow run publish.yml --ref main -f bump=major",
|
|
111
113
|
"release:prepare": "node scripts/release-prepare.mjs",
|
|
112
114
|
"release:publish": "gh workflow run publish.yml --ref main -f bump=none",
|
|
113
|
-
"validate": "npm run build && npm run typecheck && npm run lint && npm run check:docs && npm run check:doc-invocations && npm run check:shareable && npm run lint:spine && npm run check:vectors && npm run check:schemas && npm run check:ad5-registry && npm run check:ad28-registry && npm run check:ad31-table && npm run check:ad33-table && npm run check:ad21-table && npm run check:layers && npm run check:lineage && npm run check:boundary && npm run check:corpus && npm run check:worked-example && npm run check:website-deps && npm run test:coverage",
|
|
115
|
+
"validate": "npm run build && npm run typecheck && npm run lint && npm run check:docs && npm run check:doc-invocations && npm run check:shareable && npm run lint:spine && npm run check:vectors && npm run check:schemas && npm run check:ad5-registry && npm run check:ad28-registry && npm run check:ad31-table && npm run check:ad33-table && npm run check:ad21-table && npm run check:layers && npm run check:lineage && npm run check:boundary && npm run check:corpus && npm run check:doc-counts && npm run check:doc-claims && npm run check:worked-example && npm run check:website-deps && npm run test:coverage",
|
|
114
116
|
"prepack": "npm run clean && npm run build",
|
|
115
117
|
"prepublishOnly": "node scripts/assert-publish-authorized.mjs"
|
|
116
118
|
},
|