eval-quality 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/corpus/dev/README.md +3 -2
- 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/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 -0
- 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/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/review-corpus.json +1 -0
- 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/cli/render.d.ts +2 -5
- package/dist/cli/render.js +51 -1
- package/dist/core/compile/bindings.d.ts +26 -7
- package/dist/core/compile/bindings.js +49 -32
- package/dist/core/compile/compile.d.ts +12 -0
- package/dist/core/compile/compile.js +28 -1
- package/dist/core/compile/excluded-content.d.ts +11 -0
- package/dist/core/compile/excluded-content.js +42 -0
- package/dist/core/compile/expression-legality.d.ts +2 -2
- package/dist/core/compile/expression-legality.js +58 -12
- package/dist/core/compile/interface-inventory.d.ts +34 -1
- package/dist/core/compile/interface-inventory.js +88 -11
- package/dist/core/compile/reachability.d.ts +12 -2
- package/dist/core/compile/reachability.js +123 -36
- package/dist/core/compile/schema-version.d.ts +2 -0
- package/dist/core/compile/schema-version.js +25 -0
- package/dist/core/compile/sensitivity-witness.d.ts +31 -12
- package/dist/core/compile/sensitivity-witness.js +110 -23
- package/dist/core/compile/step-reference.d.ts +2 -0
- package/dist/core/compile/step-reference.js +49 -0
- package/dist/core/coverage/operations.d.ts +62 -0
- package/dist/core/coverage/operations.js +57 -0
- package/dist/core/coverage/relevance.d.ts +4 -2
- package/dist/core/coverage/relevance.js +22 -23
- package/dist/core/coverage/satisfaction.d.ts +2 -2
- package/dist/core/coverage/satisfaction.js +73 -48
- package/dist/core/declared-inputs.d.ts +83 -4
- package/dist/core/declared-inputs.js +105 -8
- package/dist/core/evaluate/evidence-resolution.d.ts +9 -12
- package/dist/core/evaluate/evidence-resolution.js +70 -10
- package/dist/core/evaluate/operators.d.ts +22 -0
- package/dist/core/evaluate/operators.js +17 -1
- package/dist/core/evaluate/resolution.d.ts +14 -3
- package/dist/core/evaluate/resolution.js +63 -3
- package/dist/core/excluded-content.d.ts +65 -0
- package/dist/core/excluded-content.js +113 -0
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +5 -2
- package/dist/core/ingest/conditions.d.ts +1 -1
- package/dist/core/ingest/ingest.js +8 -0
- package/dist/core/preflight/plan.d.ts +7 -5
- package/dist/core/preflight/plan.js +75 -32
- package/dist/core/preflight/projection.d.ts +6 -3
- package/dist/core/preflight/projection.js +22 -2
- package/dist/core/preflight/reduce.js +47 -7
- package/dist/core/preflight/witness-evidence.d.ts +5 -5
- package/dist/core/preflight/witness-evidence.js +63 -20
- package/dist/core/schemas/artifact.d.ts +683 -35
- package/dist/core/schemas/constraint-ledger.js +11 -0
- package/dist/core/schemas/defect-signature.d.ts +438 -16
- package/dist/core/schemas/defect-signature.js +58 -10
- package/dist/core/schemas/eval-contract.d.ts +517 -12
- package/dist/core/schemas/eval-contract.js +18 -2
- package/dist/core/schemas/evidence-artifact.d.ts +14 -4
- package/dist/core/schemas/evidence-artifact.js +1 -1
- package/dist/core/schemas/interface.d.ts +741 -13
- package/dist/core/schemas/interface.js +120 -7
- package/dist/core/schemas/isolation-manifest.js +16 -9
- package/dist/core/schemas/plan.d.ts +217 -3
- package/dist/core/schemas/plan.js +22 -1
- package/dist/core/schemas/pointer.d.ts +47 -1
- package/dist/core/schemas/pointer.js +89 -8
- package/dist/core/schemas/port-messages.d.ts +194 -10
- package/dist/core/schemas/port-messages.js +73 -6
- package/dist/core/schemas/probe-body.d.ts +18 -0
- package/dist/core/schemas/probe-body.js +13 -0
- package/dist/core/schemas/probe.d.ts +122 -3
- package/dist/core/schemas/probe.js +1 -1
- package/dist/core/schemas/sealed-run-record.d.ts +131 -9
- package/dist/core/schemas/sealed-run-record.js +56 -11
- package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
- package/dist/core/schemas/sensitivity-witness.js +61 -5
- package/dist/core/score/bindings.d.ts +2 -2
- package/dist/core/score/bindings.js +25 -13
- package/dist/core/score/qualification.d.ts +5 -5
- package/dist/core/score/qualification.js +67 -37
- package/dist/core/score/quotation.d.ts +1 -24
- package/dist/core/score/quotation.js +29 -4
- package/dist/core/score/score.js +28 -2
- package/dist/core/score/strength.d.ts +9 -0
- package/dist/core/score/strength.js +35 -1
- package/dist/core/score/witness.d.ts +21 -0
- package/dist/core/score/witness.js +9 -4
- package/dist/core/seal/derived-reference.d.ts +2 -9
- package/dist/core/seal/derived-reference.js +62 -23
- package/dist/core/seal/plan-index.d.ts +18 -3
- package/dist/core/seal/plan-index.js +44 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ports/environment-probe-port.d.ts +60 -4
- package/dist/testing/probe-conformance.d.ts +29 -0
- package/dist/testing/probe-conformance.js +34 -5
- package/package.json +1 -1
- package/schemas/eval-contract.schema.json +1356 -464
- package/schemas/evidence-artifact.schema.json +59 -29
- package/schemas/isolation-manifest.schema.json +17 -10
- package/schemas/probe.schema.json +197 -71
- package/schemas/rubric.schema.json +1 -1
- package/schemas/sealed-run-record.schema.json +296 -39
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
* Checks interface kinds, inventory-wide operation signatures, and step input
|
|
3
3
|
* bindings against each operation's request shape.
|
|
4
4
|
*/
|
|
5
|
+
import { boundChannelsOf, declaredArtifactsOf, descriptorArtifactOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
|
|
5
6
|
import { StructuralFailure } from '../failure-codes.js';
|
|
6
|
-
import {
|
|
7
|
-
import { buildPlanIndex } from '../seal/plan-index.js';
|
|
8
|
-
|
|
7
|
+
import { operationsOf } from '../schemas/interface.js';
|
|
8
|
+
import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
9
|
+
import { forEachArtifactPointer } from './reachability.js';
|
|
10
|
+
/**
|
|
11
|
+
* Rejects permitted interface kinds whose probe semantics are undeclared.
|
|
12
|
+
*
|
|
13
|
+
* AD-10 closed all three non-api kinds and named the condition for opening
|
|
14
|
+
* one: the semantics have to be declared. They are declared for `cli`, so it
|
|
15
|
+
* runs; `web` and `mcp` are still undeclared and still fail here, which is
|
|
16
|
+
* what keeps this code fireable and keeps AD-10's sentence true of them.
|
|
17
|
+
*/
|
|
18
|
+
const SUPPORTED_INTERFACE_KINDS = ['api', 'cli'];
|
|
9
19
|
export function checkInterfaceKind(contract) {
|
|
10
20
|
for (const iface of contract.permittedInterfaces) {
|
|
11
|
-
if (iface.kind
|
|
12
|
-
throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported
|
|
21
|
+
if (!SUPPORTED_INTERFACE_KINDS.includes(iface.kind)) {
|
|
22
|
+
throw new StructuralFailure('unsupported-interface-kind', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].kind`, `"${iface.kind}" is not supported; "api" and "cli" are (AD-10)`);
|
|
13
23
|
}
|
|
14
24
|
}
|
|
15
25
|
}
|
|
@@ -26,12 +36,36 @@ const erase = (pathTemplate) => pathTemplate.replace(PARAMETER_SEGMENT_PATTERN,
|
|
|
26
36
|
export function operationSignature(operation) {
|
|
27
37
|
return `${operation.method} ${erase(operation.pathTemplate)}`;
|
|
28
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The separator between a command's executable and each subcommand segment,
|
|
41
|
+
* declared once so the contract side and AD-40's corpus side cannot disagree
|
|
42
|
+
* about it. A space, matching the way the identity is written on a terminal.
|
|
43
|
+
*/
|
|
44
|
+
export const COMMAND_SIGNATURE_SEPARATOR = ' ';
|
|
45
|
+
/**
|
|
46
|
+
* The command counterpart of `operationSignature`, compared literally.
|
|
47
|
+
*
|
|
48
|
+
* There is no erasure step. A subcommand path carries no parameters: a
|
|
49
|
+
* command's variable inputs are its arguments and options, which live in the
|
|
50
|
+
* request shape. Erasing a subcommand segment would make `tool review` and
|
|
51
|
+
* `tool report` one signature, which is the opposite of what erasure is for.
|
|
52
|
+
*/
|
|
53
|
+
export function commandSignature(operation) {
|
|
54
|
+
return [
|
|
55
|
+
operation.invocation.executable,
|
|
56
|
+
...operation.invocation.subcommandPath,
|
|
57
|
+
].join(COMMAND_SIGNATURE_SEPARATOR);
|
|
58
|
+
}
|
|
59
|
+
/** The transport identity of an operation of either kind. */
|
|
60
|
+
export const anyOperationSignature = (operation) => isCommandOperation(operation)
|
|
61
|
+
? commandSignature(operation)
|
|
62
|
+
: operationSignature(operation);
|
|
29
63
|
/** Finds duplicate method and path signatures across the full inventory. */
|
|
30
64
|
export function checkDuplicateOperationSignature(contract) {
|
|
31
65
|
const seen = new Map();
|
|
32
66
|
for (const iface of contract.permittedInterfaces) {
|
|
33
|
-
for (const operation of iface
|
|
34
|
-
const signature =
|
|
67
|
+
for (const operation of operationsOf(iface)) {
|
|
68
|
+
const signature = anyOperationSignature(operation);
|
|
35
69
|
const collision = seen.get(signature);
|
|
36
70
|
if (collision !== undefined) {
|
|
37
71
|
throw new StructuralFailure('duplicate-operation-signature', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}]`, `collides with permittedInterfaces[logicalId=${collision.logicalId}].operations[operationId=${collision.operation.operationId}] after parameter-name erasure ("${signature}") (AD-19, AD-40)`);
|
|
@@ -40,6 +74,44 @@ export function checkDuplicateOperationSignature(contract) {
|
|
|
40
74
|
}
|
|
41
75
|
}
|
|
42
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* `unresolved-artifact-reference`: an artifact identifier nothing declares.
|
|
79
|
+
*
|
|
80
|
+
* Two sites name one: an evidence pointer's identifier segment, and a command
|
|
81
|
+
* operation's own `descriptorChannel` when it nominates an artifact. Both are
|
|
82
|
+
* authoring faults the compiler can see, and both take a code rather than
|
|
83
|
+
* resolving `absent`, on AD-26's own precedent for a dangling reference-set
|
|
84
|
+
* identifier: `absent` is defined over pointers that do not resolve against
|
|
85
|
+
* observed evidence, and a dangling declaration is neither.
|
|
86
|
+
*/
|
|
87
|
+
export function checkArtifactReferences(contract) {
|
|
88
|
+
for (const iface of contract.permittedInterfaces) {
|
|
89
|
+
for (const operation of operationsOf(iface)) {
|
|
90
|
+
const declared = declaredArtifactsOf(operation);
|
|
91
|
+
const nominated = descriptorArtifactOf(operation);
|
|
92
|
+
if (nominated !== null && !declared.includes(nominated)) {
|
|
93
|
+
throw new StructuralFailure('unresolved-artifact-reference', `EvalContract.permittedInterfaces[logicalId=${iface.logicalId}].operations[operationId=${operation.operationId}].descriptorChannel.artifactId`, `nominates "${nominated}", which this operation does not declare it writes (AD-19, AD-26)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
98
|
+
forEachArtifactPointer(contract, (pointer, path) => {
|
|
99
|
+
const target = parseEvidenceTarget(pointer);
|
|
100
|
+
if (target.artifactId === null)
|
|
101
|
+
return;
|
|
102
|
+
const step = index.stepOf(target.stepId);
|
|
103
|
+
if (step === undefined)
|
|
104
|
+
return;
|
|
105
|
+
const operation = anyOperationOf(index, step.operationId);
|
|
106
|
+
// An unresolvable step or operation is `unreachable-check-evidence`'s,
|
|
107
|
+
// at a higher rung; this check has nothing to compare against.
|
|
108
|
+
if (operation === undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (declaredArtifactsOf(operation).includes(target.artifactId))
|
|
111
|
+
return;
|
|
112
|
+
throw new StructuralFailure('unresolved-artifact-reference', path, `"${pointer}" names the "${target.artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes (AD-26)`);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
43
115
|
/**
|
|
44
116
|
* Checks each binding key against its operation's request shape, and each
|
|
45
117
|
* `{ principal }` value against the contract's declared principals.
|
|
@@ -59,14 +131,19 @@ export function checkUndeclaredMandatoryInput(contract) {
|
|
|
59
131
|
const index = buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
60
132
|
const principals = new Set(Object.keys(contract.testData.principals ?? {}));
|
|
61
133
|
for (const step of contract.interactionPlan) {
|
|
62
|
-
const operation = index
|
|
134
|
+
const operation = anyOperationOf(index, step.operationId);
|
|
63
135
|
if (operation === undefined)
|
|
64
136
|
continue;
|
|
65
|
-
for (const channel of
|
|
66
|
-
const binding = step.inputBinding[channel];
|
|
137
|
+
for (const { channel, bound: binding } of boundChannelsOf(step.inputBinding)) {
|
|
67
138
|
if (binding === null)
|
|
68
139
|
continue;
|
|
69
|
-
const
|
|
140
|
+
const shape = requestShapeOf(operation, channel);
|
|
141
|
+
// A step binding a channel of the other kind has no declared shape
|
|
142
|
+
// to answer to. Reporting it as an undeclared input is true as far
|
|
143
|
+
// as it goes: the operation declares no such channel, and so
|
|
144
|
+
// declares no such key on it.
|
|
145
|
+
const requiredKeys = shape?.requiredKeys ?? [];
|
|
146
|
+
const permittedKeys = shape?.permittedKeys ?? [];
|
|
70
147
|
for (const key of Object.keys(binding)) {
|
|
71
148
|
const path = `EvalContract.interactionPlan[stepId=${step.stepId}].inputBinding.${channel}[${JSON.stringify(key)}]`;
|
|
72
149
|
if (!requiredKeys.includes(key) && !permittedKeys.includes(key)) {
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
2
|
import type { Expression } from '../schemas/expression.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AnyOperation } from '../schemas/interface.ts';
|
|
4
4
|
import { type PlanIndex } from '../seal/plan-index.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Every interaction-rooted pointer the contract writes down, wherever it sits:
|
|
7
|
+
* an oracle's check and its direction's evidence targets, a rubric criterion's
|
|
8
|
+
* evidence, and each operation's sensitivity-witness relation.
|
|
9
|
+
*
|
|
10
|
+
* Broader than `forEachCheckPointer`, which walks oracle checks alone, because
|
|
11
|
+
* an artifact identifier is an authoring fault at every site that names one and
|
|
12
|
+
* a check that walked only the checks would report half of them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function forEachArtifactPointer(contract: EvalContract, visit: (pointer: string, artifactPath: string) => void): void;
|
|
5
15
|
export declare function checkBoundElementScope(contract: EvalContract): void;
|
|
6
16
|
/** `checkBoundElementScope` over one bare `Expression`. */
|
|
7
17
|
export declare function checkExpressionBoundElementScope(expression: Expression, artifactPath: string): void;
|
|
@@ -13,7 +23,7 @@ export declare function checkExpressionBoundElementScope(expression: Expression,
|
|
|
13
23
|
*/
|
|
14
24
|
export declare function forEachExpressionPointer(expression: Expression, visit: (pointer: string, path: string) => void): void;
|
|
15
25
|
/** `checkEvidenceReachability` over one bare `Expression` and one operation. */
|
|
16
|
-
export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation:
|
|
26
|
+
export declare function checkExpressionEvidenceReachability(expression: Expression, artifactPath: string, operation: AnyOperation): void;
|
|
17
27
|
type ReachabilityResult = {
|
|
18
28
|
readonly reachable: true;
|
|
19
29
|
} | {
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* `evaluatePointerReachability` is exported separately as the non-throwing
|
|
8
8
|
* per-pointer core, for reuse and direct testing.
|
|
9
9
|
*/
|
|
10
|
+
import { declaredArtifactsOf, descriptorArtifactOf, descriptorChannelOf, isCommandOperation, requestShapeOf, } from '../declared-inputs.js';
|
|
10
11
|
import { ARRAY_INDEX_PATTERN } from '../evaluate/evidence-resolution.js';
|
|
11
12
|
import { StructuralFailure } from '../failure-codes.js';
|
|
13
|
+
import { operationsOf } from '../schemas/interface.js';
|
|
12
14
|
import { JsonTypeName } from '../schemas/primitives.js';
|
|
13
|
-
import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
15
|
+
import { anyOperationOf, buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
14
16
|
function visitOperand(operand, path, insideQuantifier, visit) {
|
|
15
17
|
if ('pointer' in operand)
|
|
16
18
|
visit({ pointer: operand.pointer, path, insideQuantifier });
|
|
@@ -52,6 +54,42 @@ function forEachCheckPointer(contract, visit) {
|
|
|
52
54
|
visitExpression(oracle.check, 'check', false, (site) => visit(site, oracle.id));
|
|
53
55
|
});
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Every interaction-rooted pointer the contract writes down, wherever it sits:
|
|
59
|
+
* an oracle's check and its direction's evidence targets, a rubric criterion's
|
|
60
|
+
* evidence, and each operation's sensitivity-witness relation.
|
|
61
|
+
*
|
|
62
|
+
* Broader than `forEachCheckPointer`, which walks oracle checks alone, because
|
|
63
|
+
* an artifact identifier is an authoring fault at every site that names one and
|
|
64
|
+
* a check that walked only the checks would report half of them.
|
|
65
|
+
*/
|
|
66
|
+
export function forEachArtifactPointer(contract, visit) {
|
|
67
|
+
const seen = (pointer, artifactPath) => {
|
|
68
|
+
if (pointer.startsWith('@'))
|
|
69
|
+
return;
|
|
70
|
+
visit(pointer, artifactPath);
|
|
71
|
+
};
|
|
72
|
+
contract.oracles.forEach((oracle) => {
|
|
73
|
+
if (oracle.check !== null)
|
|
74
|
+
visitExpression(oracle.check, 'check', false, (site) => seen(site.pointer, `EvalContract.oracles[id=${oracle.id}].${site.path}`));
|
|
75
|
+
oracle.direction?.evidenceTargets.forEach((target, index) => {
|
|
76
|
+
seen(target, `EvalContract.oracles[id=${oracle.id}].direction.evidenceTargets[${index}]`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
contract.rubrics.forEach((rubric) => {
|
|
80
|
+
rubric.criteria.forEach((criterion) => {
|
|
81
|
+
seen(criterion.evidence, `EvalContract.rubrics[id=${rubric.id}].criteria[id=${criterion.id}].evidence`);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
contract.permittedInterfaces.forEach((iface, interfaceIndex) => {
|
|
85
|
+
operationsOf(iface).forEach((operation, operationIndex) => {
|
|
86
|
+
const witness = operation.sensitivityWitness;
|
|
87
|
+
if (witness === null)
|
|
88
|
+
return;
|
|
89
|
+
visitExpression(witness.relation, 'relation', false, (site) => seen(site.pointer, `EvalContract.permittedInterfaces[${interfaceIndex}].operations[${operationIndex}].sensitivityWitness.${site.path}`));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
55
93
|
// ---- malformed-operator-expression: @/ outside any quantifier -----------
|
|
56
94
|
export function checkBoundElementScope(contract) {
|
|
57
95
|
forEachCheckPointer(contract, (site, oracleId) => {
|
|
@@ -125,12 +163,50 @@ export function evaluatePointerReachability(pointer, index) {
|
|
|
125
163
|
if (step === undefined) {
|
|
126
164
|
return unreachable('names a step the interaction plan does not declare');
|
|
127
165
|
}
|
|
128
|
-
const operation = index
|
|
166
|
+
const operation = anyOperationOf(index, step.operationId);
|
|
129
167
|
if (operation === undefined) {
|
|
130
168
|
return unreachable(`names step "${target.stepId}", which names operation "${step.operationId}", not declared by any permitted interface`);
|
|
131
169
|
}
|
|
132
170
|
return evaluateReachabilityAgainstOperation(pointer, operation);
|
|
133
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Descent through the operation's response descriptor, from whichever channel
|
|
174
|
+
* that descriptor describes. `channel` is carried only so the reason names the
|
|
175
|
+
* pointer's own root back to the author.
|
|
176
|
+
*/
|
|
177
|
+
function descendThroughDescriptor(descriptor, target, operationId, channel) {
|
|
178
|
+
if (target.tail.length === 0)
|
|
179
|
+
return reachable();
|
|
180
|
+
const firstToken = target.tail[0];
|
|
181
|
+
if (firstToken === undefined) {
|
|
182
|
+
// Unreachable: the length check above guarantees an element.
|
|
183
|
+
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
184
|
+
}
|
|
185
|
+
const { requiredKeys, permittedKeys, types, collectionLocations } = descriptor;
|
|
186
|
+
// A root-declared collection (`pointer: ''`) indexes directly, bypassing
|
|
187
|
+
// the key check below. `expectedCardinality` bounds the array size
|
|
188
|
+
// (`exact` is the true count; `at-most`/`page-bounded` is an upper
|
|
189
|
+
// bound), so an index at or past it is unreachable.
|
|
190
|
+
const rootCollection = collectionLocations?.find((location) => location.pointer === '');
|
|
191
|
+
if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
|
|
192
|
+
const { expectedCardinality } = rootCollection;
|
|
193
|
+
const bound = expectedCardinality.mode === 'exact'
|
|
194
|
+
? expectedCardinality.count
|
|
195
|
+
: expectedCardinality.max;
|
|
196
|
+
if (Number(firstToken) >= bound) {
|
|
197
|
+
return unreachable(`addresses ${channel} index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
|
|
198
|
+
}
|
|
199
|
+
return reachable();
|
|
200
|
+
}
|
|
201
|
+
if (!requiredKeys.includes(firstToken) &&
|
|
202
|
+
!permittedKeys.includes(firstToken)) {
|
|
203
|
+
return unreachable(`addresses ${channel} field "${firstToken}", which operation "${operationId}" declares in neither requiredKeys nor permittedKeys`);
|
|
204
|
+
}
|
|
205
|
+
if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
|
|
206
|
+
return unreachable(`descends into ${channel} field "${firstToken}", which operation "${operationId}" declares a scalar with no further structure`);
|
|
207
|
+
}
|
|
208
|
+
return reachable();
|
|
209
|
+
}
|
|
134
210
|
/**
|
|
135
211
|
* The same rules, against an operation the caller already resolved. Without
|
|
136
212
|
* this check a probe-side condition addressing an undeclared channel or key
|
|
@@ -142,47 +218,54 @@ function evaluateReachabilityAgainstOperation(pointer, operation) {
|
|
|
142
218
|
if (pointer.startsWith('@'))
|
|
143
219
|
return reachable();
|
|
144
220
|
const target = parseEvidenceTarget(pointer);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
221
|
+
const descriptorChannel = descriptorChannelOf(operation);
|
|
222
|
+
const command = isCommandOperation(operation);
|
|
223
|
+
if (target.channel === 'artifact') {
|
|
224
|
+
const { artifactId } = target;
|
|
225
|
+
if (artifactId === null) {
|
|
226
|
+
// Unreachable: parseEvidenceTarget's own guarantee.
|
|
227
|
+
throw new TypeError('artifact evidence target names no artifact');
|
|
150
228
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
229
|
+
// An identifier the operation does not declare produces no evidence, so
|
|
230
|
+
// it is unreachable and this says so. `checkArtifactReferences` runs
|
|
231
|
+
// earlier in `compile` and reports the more specific
|
|
232
|
+
// `unresolved-artifact-reference` for a contract, so the two never race
|
|
233
|
+
// there; this answer is the only one on the probe side, where that check
|
|
234
|
+
// does not run because it walks a contract rather than a signature.
|
|
235
|
+
if (!declaredArtifactsOf(operation).includes(artifactId)) {
|
|
236
|
+
return unreachable(`names the "${artifactId}" artifact, which operation "${operation.operationId}" does not declare it writes`);
|
|
160
237
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const rootCollection = collectionLocations?.find((location) => location.pointer === '');
|
|
167
|
-
if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
|
|
168
|
-
const { expectedCardinality } = rootCollection;
|
|
169
|
-
const bound = expectedCardinality.mode === 'exact'
|
|
170
|
-
? expectedCardinality.count
|
|
171
|
-
: expectedCardinality.max;
|
|
172
|
-
if (Number(firstToken) >= bound) {
|
|
173
|
-
return unreachable(`addresses response-body index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
|
|
238
|
+
if (artifactId !== descriptorArtifactOf(operation)) {
|
|
239
|
+
// Declared to exist, and nothing declares its structure: the
|
|
240
|
+
// operation's one descriptor describes a different channel.
|
|
241
|
+
if (target.tail.length > 0) {
|
|
242
|
+
return unreachable(`addresses a field inside the "${artifactId}" artifact, which operation "${operation.operationId}" declares it writes but declares no structure for`);
|
|
174
243
|
}
|
|
175
244
|
return reachable();
|
|
176
245
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
246
|
+
return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, `the "${artifactId}" artifact`);
|
|
247
|
+
}
|
|
248
|
+
// The declared output channel descends through the descriptor whatever it
|
|
249
|
+
// is called. This is the one rule; the channels below are the cases where
|
|
250
|
+
// no descriptor applies.
|
|
251
|
+
if (target.channel === descriptorChannel) {
|
|
252
|
+
return descendThroughDescriptor(operation.responseDescriptor, target, operation.operationId, target.channel);
|
|
253
|
+
}
|
|
254
|
+
if (target.channel === 'stdout' || target.channel === 'stderr') {
|
|
255
|
+
// The stream this operation's descriptor does not describe carries no
|
|
256
|
+
// declared structure, so a non-empty tail proves the pointer
|
|
257
|
+
// unreachable.
|
|
258
|
+
if (target.tail.length > 0) {
|
|
259
|
+
return unreachable(`addresses a field inside ${target.channel}, which operation "${operation.operationId}" declares no structure for`);
|
|
183
260
|
}
|
|
184
261
|
return reachable();
|
|
185
262
|
}
|
|
263
|
+
if (command &&
|
|
264
|
+
(target.channel === 'response-body' ||
|
|
265
|
+
target.channel === 'response-headers' ||
|
|
266
|
+
target.channel === 'response-status')) {
|
|
267
|
+
return unreachable(`addresses ${target.channel} on operation "${operation.operationId}", which runs behind a command and produces no HTTP response`);
|
|
268
|
+
}
|
|
186
269
|
if (target.channel === 'call-inputs') {
|
|
187
270
|
if (target.tail.length === 0)
|
|
188
271
|
return reachable();
|
|
@@ -195,7 +278,11 @@ function evaluateReachabilityAgainstOperation(pointer, operation) {
|
|
|
195
278
|
if (firstToken === undefined) {
|
|
196
279
|
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
197
280
|
}
|
|
198
|
-
const
|
|
281
|
+
const shape = requestShapeOf(operation, transportChannel);
|
|
282
|
+
if (shape === undefined) {
|
|
283
|
+
return unreachable(`addresses call-inputs ${transportChannel}, a channel operation "${operation.operationId}" does not accept input on`);
|
|
284
|
+
}
|
|
285
|
+
const { requiredKeys, permittedKeys, types } = shape;
|
|
199
286
|
if (!requiredKeys.includes(firstToken) &&
|
|
200
287
|
!permittedKeys.includes(firstToken)) {
|
|
201
288
|
return unreachable(`addresses call-inputs ${transportChannel} field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-11's version equality, performed by the one reader that has the contract.
|
|
3
|
+
*
|
|
4
|
+
* AD-11's rule is that "readers accept an equal `schemaVersion` only and throw
|
|
5
|
+
* `schema-version-mismatch` outside that". `lineage.ts` deliberately keeps the
|
|
6
|
+
* field a plain integer rather than a literal, so that a wrong version arrives
|
|
7
|
+
* as AD-28's named fault instead of an anonymous parse failure, which puts the
|
|
8
|
+
* comparison on whoever reads the artifact. For the eval contract nobody did:
|
|
9
|
+
* a contract stamped 3 parsed, compiled, sealed, and put its stale version into
|
|
10
|
+
* the scoring version digest, and AD-11 exists to keep that number comparable.
|
|
11
|
+
*
|
|
12
|
+
* `RuntimeFault` rather than `StructuralFailure`, against the grain of every
|
|
13
|
+
* other check in this directory. The two registries are disjoint by the
|
|
14
|
+
* Consistency Conventions, AD-5 has no version code, and AD-11 names
|
|
15
|
+
* `schema-version-mismatch` literally. Minting an AD-5 twin would give one
|
|
16
|
+
* condition two codes and make a caller's handler depend on which reader
|
|
17
|
+
* happened to notice first.
|
|
18
|
+
*/
|
|
19
|
+
import { RuntimeFault } from '../schemas/faults.js';
|
|
20
|
+
/** `schema-version-mismatch`: the stamp is not the version this build reads. */
|
|
21
|
+
export function checkSchemaVersion(stamped, accepted, artifactPath) {
|
|
22
|
+
if (stamped === accepted)
|
|
23
|
+
return;
|
|
24
|
+
throw new RuntimeFault('schema-version-mismatch', artifactPath, `carries "schemaVersion" ${stamped} where this build reads ${accepted}; a contract written for another version is not read leniently, since its stale version would travel into the scoring version (AD-11)`);
|
|
25
|
+
}
|
|
@@ -4,26 +4,45 @@
|
|
|
4
4
|
* execute it, and that its leg identifiers are distinct from each other and
|
|
5
5
|
* from every interaction-plan step.
|
|
6
6
|
*
|
|
7
|
-
* No new AD-5 code is minted.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* No new AD-5 code is minted here. `check:ad5-registry` pins the registry
|
|
8
|
+
* against the spine's own table, so each defect below takes the code that
|
|
9
|
+
* already names it. Two rows stretch that reading: a leg-id equality
|
|
10
10
|
* and a leg-id/step-id collision are identifier collisions, and
|
|
11
11
|
* `malformed-operator-expression` is the closest available code because the
|
|
12
12
|
* relation is illegal in its position once its operands cannot be told apart.
|
|
13
13
|
*/
|
|
14
14
|
import { declaresNoRequestKeys } from '../declared-inputs.ts';
|
|
15
15
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
16
|
-
import type {
|
|
17
|
-
import {
|
|
18
|
-
import type
|
|
16
|
+
import type { AnyOperation } from '../schemas/interface.ts';
|
|
17
|
+
import type { InputChannelName } from '../schemas/pointer.ts';
|
|
18
|
+
import { type ApiWitnessInputs, type WitnessChannel, type WitnessInputs } from '../schemas/sensitivity-witness.ts';
|
|
19
19
|
export { declaresNoRequestKeys };
|
|
20
20
|
/**
|
|
21
|
-
* The
|
|
22
|
-
*
|
|
23
|
-
* declared as a keyed shape, so such a leg omits every required key rather
|
|
24
|
-
* than being exempt from the comparison.
|
|
21
|
+
* The transport spelling, for the consumers that can only send one: the probe
|
|
22
|
+
* port carries a method, a path template, and the four transport channels.
|
|
25
23
|
*/
|
|
26
|
-
export declare
|
|
24
|
+
export declare const isApiWitnessInputs: (inputs: WitnessInputs) => inputs is ApiWitnessInputs;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the leg supplies this channel as an undifferentiated stream.
|
|
27
|
+
*
|
|
28
|
+
* Standard input written as text is one such stream. A `KeyedShapeDescriptor`
|
|
29
|
+
* can say what keys a channel carries and cannot say "one opaque stream", so a
|
|
30
|
+
* command that reads a prompt is declared by naming the one thing the stream
|
|
31
|
+
* carries, and the leg supplies that thing as the text it actually is.
|
|
32
|
+
*/
|
|
33
|
+
export declare function suppliesOpaquely(inputs: WitnessInputs, channel: InputChannelName): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The keys one set of witness inputs supplies on one channel, or none where
|
|
36
|
+
* the leg's spelling has no such channel.
|
|
37
|
+
*
|
|
38
|
+
* `body` and `stdin` are the two channels a leg supplies as a tagged value
|
|
39
|
+
* rather than as a key map, because a leg has to tell an absent value from one
|
|
40
|
+
* carrying JSON null. This function is the bridge between that spelling and
|
|
41
|
+
* the keyed shape the request declares.
|
|
42
|
+
*/
|
|
43
|
+
export declare function suppliedKeys(inputs: WitnessInputs, channel: InputChannelName): readonly string[];
|
|
44
|
+
/** One channel's supplied value, for the differential comparison. */
|
|
45
|
+
export declare function suppliedValue(inputs: WitnessInputs, channel: WitnessChannel): unknown;
|
|
27
46
|
/**
|
|
28
47
|
* Every channel of one set of witness inputs against the operation that will
|
|
29
48
|
* receive them. All four, not only the differential channel: `planPreflight`
|
|
@@ -49,7 +68,7 @@ export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportCh
|
|
|
49
68
|
* contract artifact, the surface AD-18 governs; a placeholder satisfies this
|
|
50
69
|
* check, and what pre-flight probes is a fixture.
|
|
51
70
|
*/
|
|
52
|
-
export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation:
|
|
71
|
+
export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: AnyOperation, owner: string, artifactPath: string): void;
|
|
53
72
|
/**
|
|
54
73
|
* Strict-gated, alongside `checkUndeclaredMandatoryInput`, whose code this
|
|
55
74
|
* shares: `compile.ts` already gates that code behind `options.strict`, and one
|