eval-quality 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +355 -0
- package/corpus/dev/README.md +36 -0
- package/corpus/dev/compile-seal-example/brief.json +1 -0
- package/corpus/dev/compile-seal-example/contract.json +1 -0
- package/corpus/dev/contracts/absent-collection-locations.json +1 -0
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
- package/corpus/dev/contracts/absent-success-indicator.json +1 -0
- package/corpus/dev/contracts/empty-channel-roles.json +1 -0
- package/corpus/dev/contracts/empty-collection-locations.json +1 -0
- package/corpus/dev/contracts/empty-request-shapes.json +1 -0
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
- package/corpus/dev/contracts/no-operation-inventory.json +1 -0
- package/corpus/dev/contracts/no-read-back-relation.json +1 -0
- package/corpus/dev/contracts/no-state-change-marker.json +1 -0
- package/corpus/dev/contracts/no-type-violating-step.json +1 -0
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
- package/corpus/dev/contracts/satisfied-declarations.json +1 -0
- package/corpus/dev/contracts/single-required-response-key.json +1 -0
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
- package/corpus/dev/index.json +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/local-corpus-adapter.d.ts +7 -0
- package/dist/adapters/local-corpus-adapter.js +78 -0
- package/dist/adapters/node-file-system-adapter.d.ts +7 -0
- package/dist/adapters/node-file-system-adapter.js +39 -0
- package/dist/adapters/port-boundary.d.ts +18 -0
- package/dist/adapters/port-boundary.js +83 -0
- package/dist/adapters/system-clock-adapter.d.ts +10 -0
- package/dist/adapters/system-clock-adapter.js +18 -0
- package/dist/application/compile.d.ts +4 -0
- package/dist/application/compile.js +24 -0
- package/dist/application/diagnostics.d.ts +22 -0
- package/dist/application/diagnostics.js +18 -0
- package/dist/application/index.d.ts +23 -0
- package/dist/application/index.js +17 -0
- package/dist/application/invoke-port.d.ts +2 -0
- package/dist/application/invoke-port.js +45 -0
- package/dist/application/preflight.d.ts +27 -0
- package/dist/application/preflight.js +131 -0
- package/dist/application/seal.d.ts +4 -0
- package/dist/application/seal.js +28 -0
- package/dist/application/serialize.d.ts +6 -0
- package/dist/application/serialize.js +9 -0
- package/dist/cli/arguments.d.ts +28 -0
- package/dist/cli/arguments.js +167 -0
- package/dist/cli/exit-codes.d.ts +36 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +143 -0
- package/dist/cli/render.d.ts +26 -0
- package/dist/cli/render.js +53 -0
- package/dist/cli/run.d.ts +41 -0
- package/dist/cli/run.js +203 -0
- package/dist/core/canonical/canonicalize.d.ts +1 -0
- package/dist/core/canonical/canonicalize.js +113 -0
- package/dist/core/canonical/digest.d.ts +7 -0
- package/dist/core/canonical/digest.js +81 -0
- package/dist/core/canonical/scan-json.d.ts +1 -0
- package/dist/core/canonical/scan-json.js +298 -0
- package/dist/core/canonical/value-domain.d.ts +4 -0
- package/dist/core/canonical/value-domain.js +113 -0
- package/dist/core/compile/compile.d.ts +24 -0
- package/dist/core/compile/compile.js +67 -0
- package/dist/core/compile/declarations.d.ts +5 -0
- package/dist/core/compile/declarations.js +22 -0
- package/dist/core/compile/expression-legality.d.ts +14 -0
- package/dist/core/compile/expression-legality.js +346 -0
- package/dist/core/compile/forbidden-inputs.d.ts +5 -0
- package/dist/core/compile/forbidden-inputs.js +19 -0
- package/dist/core/compile/interface-inventory.d.ts +10 -0
- package/dist/core/compile/interface-inventory.js +55 -0
- package/dist/core/compile/oracle-alignment.d.ts +14 -0
- package/dist/core/compile/oracle-alignment.js +93 -0
- package/dist/core/compile/reachability.d.ts +17 -0
- package/dist/core/compile/reachability.js +177 -0
- package/dist/core/compile/rubrics.d.ts +55 -0
- package/dist/core/compile/rubrics.js +198 -0
- package/dist/core/compile/scripting-bound.d.ts +5 -0
- package/dist/core/compile/scripting-bound.js +151 -0
- package/dist/core/compile/sensitivity-witness.d.ts +73 -0
- package/dist/core/compile/sensitivity-witness.js +232 -0
- package/dist/core/compile/waivers.d.ts +3 -0
- package/dist/core/compile/waivers.js +21 -0
- package/dist/core/coverage/coverage.d.ts +18 -0
- package/dist/core/coverage/coverage.js +47 -0
- package/dist/core/coverage/relevance.d.ts +75 -0
- package/dist/core/coverage/relevance.js +186 -0
- package/dist/core/coverage/rules.d.ts +7 -0
- package/dist/core/coverage/rules.js +18 -0
- package/dist/core/coverage/satisfaction.d.ts +107 -0
- package/dist/core/coverage/satisfaction.js +415 -0
- package/dist/core/coverage/table.d.ts +26 -0
- package/dist/core/coverage/table.js +222 -0
- package/dist/core/declared-inputs.d.ts +16 -0
- package/dist/core/declared-inputs.js +18 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
- package/dist/core/evaluate/evidence-resolution.js +153 -0
- package/dist/core/evaluate/operators.d.ts +87 -0
- package/dist/core/evaluate/operators.js +350 -0
- package/dist/core/evaluate/resolution.d.ts +40 -0
- package/dist/core/evaluate/resolution.js +336 -0
- package/dist/core/evaluate/resolved-value.d.ts +9 -0
- package/dist/core/evaluate/resolved-value.js +6 -0
- package/dist/core/failure-codes.d.ts +18 -0
- package/dist/core/failure-codes.js +50 -0
- package/dist/core/lineage/chain.d.ts +49 -0
- package/dist/core/lineage/chain.js +210 -0
- package/dist/core/lineage/freeze.d.ts +12 -0
- package/dist/core/lineage/freeze.js +35 -0
- package/dist/core/lineage/stage-table.d.ts +43 -0
- package/dist/core/lineage/stage-table.js +106 -0
- package/dist/core/preflight/plan.d.ts +67 -0
- package/dist/core/preflight/plan.js +313 -0
- package/dist/core/preflight/projection.d.ts +40 -0
- package/dist/core/preflight/projection.js +96 -0
- package/dist/core/preflight/reduce.d.ts +8 -0
- package/dist/core/preflight/reduce.js +180 -0
- package/dist/core/preflight/witness-evidence.d.ts +42 -0
- package/dist/core/preflight/witness-evidence.js +88 -0
- package/dist/core/probe/target-policy.d.ts +63 -0
- package/dist/core/probe/target-policy.js +328 -0
- package/dist/core/schemas/artifact-reference.d.ts +19 -0
- package/dist/core/schemas/artifact-reference.js +30 -0
- package/dist/core/schemas/artifact.d.ts +1153 -0
- package/dist/core/schemas/artifact.js +84 -0
- package/dist/core/schemas/constraint-ledger.d.ts +57 -0
- package/dist/core/schemas/constraint-ledger.js +143 -0
- package/dist/core/schemas/eval-contract.d.ts +402 -0
- package/dist/core/schemas/eval-contract.js +147 -0
- package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
- package/dist/core/schemas/evaluator-configuration.js +43 -0
- package/dist/core/schemas/evidence-artifact.d.ts +471 -0
- package/dist/core/schemas/evidence-artifact.js +232 -0
- package/dist/core/schemas/expression.d.ts +197 -0
- package/dist/core/schemas/expression.js +284 -0
- package/dist/core/schemas/faults.d.ts +9 -0
- package/dist/core/schemas/faults.js +32 -0
- package/dist/core/schemas/interface.d.ts +417 -0
- package/dist/core/schemas/interface.js +115 -0
- package/dist/core/schemas/isolation-manifest.d.ts +136 -0
- package/dist/core/schemas/isolation-manifest.js +92 -0
- package/dist/core/schemas/lineage.d.ts +21 -0
- package/dist/core/schemas/lineage.js +28 -0
- package/dist/core/schemas/oracle.d.ts +72 -0
- package/dist/core/schemas/oracle.js +35 -0
- package/dist/core/schemas/plan.d.ts +115 -0
- package/dist/core/schemas/plan.js +58 -0
- package/dist/core/schemas/pointer.d.ts +63 -0
- package/dist/core/schemas/pointer.js +94 -0
- package/dist/core/schemas/port-messages.d.ts +103 -0
- package/dist/core/schemas/port-messages.js +90 -0
- package/dist/core/schemas/preflight-verdict.d.ts +60 -0
- package/dist/core/schemas/preflight-verdict.js +38 -0
- package/dist/core/schemas/primitives.d.ts +78 -0
- package/dist/core/schemas/primitives.js +121 -0
- package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
- package/dist/core/schemas/private-artifact-manifest.js +46 -0
- package/dist/core/schemas/probe-body.d.ts +25 -0
- package/dist/core/schemas/probe-body.js +23 -0
- package/dist/core/schemas/probe-policy.d.ts +76 -0
- package/dist/core/schemas/probe-policy.js +33 -0
- package/dist/core/schemas/probe.d.ts +189 -0
- package/dist/core/schemas/probe.js +82 -0
- package/dist/core/schemas/publish.d.ts +30 -0
- package/dist/core/schemas/publish.js +135 -0
- package/dist/core/schemas/reference-set.d.ts +18 -0
- package/dist/core/schemas/reference-set.js +27 -0
- package/dist/core/schemas/rubric.d.ts +62 -0
- package/dist/core/schemas/rubric.js +68 -0
- package/dist/core/schemas/scoring-policy.d.ts +28 -0
- package/dist/core/schemas/scoring-policy.js +51 -0
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
- package/dist/core/schemas/sealed-run-record.d.ts +382 -0
- package/dist/core/schemas/sealed-run-record.js +215 -0
- package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
- package/dist/core/schemas/sensitivity-witness.js +86 -0
- package/dist/core/schemas/verdict.d.ts +25 -0
- package/dist/core/schemas/verdict.js +20 -0
- package/dist/core/schemas/waiver.d.ts +17 -0
- package/dist/core/schemas/waiver.js +24 -0
- package/dist/core/seal/derived-reference.d.ts +36 -0
- package/dist/core/seal/derived-reference.js +383 -0
- package/dist/core/seal/direction-prose.d.ts +13 -0
- package/dist/core/seal/direction-prose.js +120 -0
- package/dist/core/seal/plan-index.d.ts +63 -0
- package/dist/core/seal/plan-index.js +155 -0
- package/dist/core/seal/scripting-audit.d.ts +15 -0
- package/dist/core/seal/scripting-audit.js +51 -0
- package/dist/core/seal/seal.d.ts +18 -0
- package/dist/core/seal/seal.js +155 -0
- package/dist/core/stage-contracts.d.ts +30 -0
- package/dist/core/stage-contracts.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +22 -0
- package/dist/ports/clock-port.d.ts +13 -0
- package/dist/ports/clock-port.js +7 -0
- package/dist/ports/corpus-port.d.ts +16 -0
- package/dist/ports/corpus-port.js +7 -0
- package/dist/ports/environment-probe-port.d.ts +76 -0
- package/dist/ports/environment-probe-port.js +32 -0
- package/dist/ports/file-system-port.d.ts +27 -0
- package/dist/ports/file-system-port.js +11 -0
- package/dist/ports/port.d.ts +33 -0
- package/dist/ports/port.js +14 -0
- package/dist/testing/conformance.d.ts +88 -0
- package/dist/testing/conformance.js +281 -0
- package/dist/testing/index.d.ts +26 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/probe-conformance.d.ts +45 -0
- package/dist/testing/probe-conformance.js +186 -0
- package/package.json +130 -0
- package/schemas/artifact-reference.schema.json +64 -0
- package/schemas/eval-contract.schema.json +2145 -0
- package/schemas/evaluator-configuration.schema.json +199 -0
- package/schemas/evidence-artifact.schema.json +1229 -0
- package/schemas/isolation-manifest.schema.json +394 -0
- package/schemas/preflight-verdict.schema.json +114 -0
- package/schemas/private-artifact-manifest.schema.json +103 -0
- package/schemas/probe.schema.json +1151 -0
- package/schemas/rubric.schema.json +148 -0
- package/schemas/scoring-policy.schema.json +90 -0
- package/schemas/sealed-evaluator-brief.schema.json +268 -0
- package/schemas/sealed-run-record.schema.json +875 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two compile-time checks over an EvalContract's oracle trees:
|
|
3
|
+
* `malformed-operator-expression` (a `@/` bound-element pointer outside any
|
|
4
|
+
* quantifier's predicate) and `unreachable-check-evidence` (a pointer the
|
|
5
|
+
* declared interfaces cannot produce). Both throw `StructuralFailure` on the
|
|
6
|
+
* first violation, matching `auditBriefScripting`'s fail-fast convention.
|
|
7
|
+
* `evaluatePointerReachability` is exported separately as the non-throwing
|
|
8
|
+
* per-pointer core, for reuse and direct testing.
|
|
9
|
+
*/
|
|
10
|
+
import { ARRAY_INDEX_PATTERN } from '../evaluate/evidence-resolution.js';
|
|
11
|
+
import { StructuralFailure } from '../failure-codes.js';
|
|
12
|
+
import { JsonTypeName } from '../schemas/primitives.js';
|
|
13
|
+
import { buildPlanIndex, parseEvidenceTarget, } from '../seal/plan-index.js';
|
|
14
|
+
function visitOperand(operand, path, insideQuantifier, visit) {
|
|
15
|
+
if ('pointer' in operand)
|
|
16
|
+
visit({ pointer: operand.pointer, path, insideQuantifier });
|
|
17
|
+
// `{ literal }` and `{ referenceSet }` address no interaction evidence.
|
|
18
|
+
}
|
|
19
|
+
// `set-membership`'s second operand is a `SetOperand` (`{ referenceSet }` or
|
|
20
|
+
// `{ literal: [...] }`). That type excludes `{ pointer }`, so only its first
|
|
21
|
+
// operand can carry one. Every other tuple-shaped op falls through to the
|
|
22
|
+
// default branch, since each declares `operands: Operand[]`.
|
|
23
|
+
function visitExpression(expression, path, insideQuantifier, visit) {
|
|
24
|
+
switch (expression.op) {
|
|
25
|
+
case 'not':
|
|
26
|
+
visitExpression(expression.operands[0], `${path}.operands[0]`, insideQuantifier, visit);
|
|
27
|
+
return;
|
|
28
|
+
case 'all':
|
|
29
|
+
case 'any':
|
|
30
|
+
expression.operands.forEach((child, index) => {
|
|
31
|
+
visitExpression(child, `${path}.operands[${index}]`, insideQuantifier, visit);
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
case 'for-all':
|
|
35
|
+
case 'for-any':
|
|
36
|
+
visitOperand(expression.collection, `${path}.collection`, insideQuantifier, visit);
|
|
37
|
+
visitExpression(expression.predicate, `${path}.predicate`, true, visit);
|
|
38
|
+
return;
|
|
39
|
+
case 'set-membership':
|
|
40
|
+
visitOperand(expression.operands[0], `${path}.operands[0]`, insideQuantifier, visit);
|
|
41
|
+
return;
|
|
42
|
+
default:
|
|
43
|
+
expression.operands.forEach((operand, index) => {
|
|
44
|
+
visitOperand(operand, `${path}.operands[${index}]`, insideQuantifier, visit);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function forEachCheckPointer(contract, visit) {
|
|
49
|
+
contract.oracles.forEach((oracle) => {
|
|
50
|
+
if (oracle.check === null)
|
|
51
|
+
return;
|
|
52
|
+
visitExpression(oracle.check, 'check', false, (site) => visit(site, oracle.id));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// ---- malformed-operator-expression: @/ outside any quantifier -----------
|
|
56
|
+
export function checkBoundElementScope(contract) {
|
|
57
|
+
forEachCheckPointer(contract, (site, oracleId) => {
|
|
58
|
+
if (site.pointer.startsWith('@') && !site.insideQuantifier) {
|
|
59
|
+
throw new StructuralFailure('malformed-operator-expression', `EvalContract.oracles[id=${oracleId}].${site.path}`, `bound-element pointer "${site.pointer}" appears outside any quantifier's predicate; "@/" binds only inside a quantifier (AD-26)`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const reachable = () => ({ reachable: true });
|
|
64
|
+
const unreachable = (reason) => ({
|
|
65
|
+
reachable: false,
|
|
66
|
+
reason,
|
|
67
|
+
});
|
|
68
|
+
// Every JsonTypeName value except `object` and `array` is scalar. Widened
|
|
69
|
+
// to `ReadonlySet<string>` so `.has(declaredType)` below still typechecks.
|
|
70
|
+
const SCALAR_TYPES = new Set(JsonTypeName.options.filter((name) => name !== 'object' && name !== 'array'));
|
|
71
|
+
// A field blocks further descent only when its declared type is definitely
|
|
72
|
+
// scalar. `undefined` (not declared) and `null` (declared, type not stated)
|
|
73
|
+
// both stay permissive: nothing rules out descent.
|
|
74
|
+
function descendsIntoDeclaredScalar(types, tail, firstToken) {
|
|
75
|
+
if (tail.length <= 1)
|
|
76
|
+
return false;
|
|
77
|
+
const declaredType = types[firstToken];
|
|
78
|
+
return (declaredType !== undefined &&
|
|
79
|
+
declaredType !== null &&
|
|
80
|
+
SCALAR_TYPES.has(declaredType));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Non-throwing core that `checkEvidenceReachability` wraps; also called
|
|
84
|
+
* directly by tests.
|
|
85
|
+
*/
|
|
86
|
+
export function evaluatePointerReachability(pointer, index) {
|
|
87
|
+
// A `@/` pointer is relative to a bound element. Nothing declared to
|
|
88
|
+
// check reachability against.
|
|
89
|
+
if (pointer.startsWith('@'))
|
|
90
|
+
return reachable();
|
|
91
|
+
const target = parseEvidenceTarget(pointer);
|
|
92
|
+
const step = index.stepOf(target.stepId);
|
|
93
|
+
if (step === undefined) {
|
|
94
|
+
return unreachable('names a step the interaction plan does not declare');
|
|
95
|
+
}
|
|
96
|
+
const operation = index.operationOf(step.operationId);
|
|
97
|
+
if (operation === undefined) {
|
|
98
|
+
return unreachable(`names step "${target.stepId}", which names operation "${step.operationId}", not declared by any permitted interface`);
|
|
99
|
+
}
|
|
100
|
+
if (target.channel === 'stdout' || target.channel === 'stderr') {
|
|
101
|
+
// stdout/stderr are always bare strings. A non-empty tail proves the
|
|
102
|
+
// pointer unreachable.
|
|
103
|
+
if (target.tail.length > 0) {
|
|
104
|
+
return unreachable(`addresses a field inside ${target.channel}, which never carries structure to descend into`);
|
|
105
|
+
}
|
|
106
|
+
return reachable();
|
|
107
|
+
}
|
|
108
|
+
if (target.channel === 'response-body') {
|
|
109
|
+
if (target.tail.length === 0)
|
|
110
|
+
return reachable();
|
|
111
|
+
const firstToken = target.tail[0];
|
|
112
|
+
if (firstToken === undefined) {
|
|
113
|
+
// Unreachable: the length check above guarantees an element.
|
|
114
|
+
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
115
|
+
}
|
|
116
|
+
const { requiredKeys, permittedKeys, types, collectionLocations } = operation.responseDescriptor;
|
|
117
|
+
// A root-declared collection (`pointer: ''`) indexes directly, bypassing
|
|
118
|
+
// the key check below. `expectedCardinality` bounds the array size
|
|
119
|
+
// (`exact` is the true count; `at-most`/`page-bounded` is an upper
|
|
120
|
+
// bound), so an index at or past it is unreachable.
|
|
121
|
+
const rootCollection = collectionLocations?.find((location) => location.pointer === '');
|
|
122
|
+
if (rootCollection !== undefined && ARRAY_INDEX_PATTERN.test(firstToken)) {
|
|
123
|
+
const { expectedCardinality } = rootCollection;
|
|
124
|
+
const bound = expectedCardinality.mode === 'exact'
|
|
125
|
+
? expectedCardinality.count
|
|
126
|
+
: expectedCardinality.max;
|
|
127
|
+
if (Number(firstToken) >= bound) {
|
|
128
|
+
return unreachable(`addresses response-body index ${firstToken}, out of bounds for the declared root collection's expectedCardinality (${expectedCardinality.mode} ${bound})`);
|
|
129
|
+
}
|
|
130
|
+
return reachable();
|
|
131
|
+
}
|
|
132
|
+
if (!requiredKeys.includes(firstToken) &&
|
|
133
|
+
!permittedKeys.includes(firstToken)) {
|
|
134
|
+
return unreachable(`addresses response-body field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
|
|
135
|
+
}
|
|
136
|
+
if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
|
|
137
|
+
return unreachable(`descends into response-body field "${firstToken}", which operation "${operation.operationId}" declares a scalar with no further structure`);
|
|
138
|
+
}
|
|
139
|
+
return reachable();
|
|
140
|
+
}
|
|
141
|
+
if (target.channel === 'call-inputs') {
|
|
142
|
+
if (target.tail.length === 0)
|
|
143
|
+
return reachable();
|
|
144
|
+
const { transportChannel } = target;
|
|
145
|
+
if (transportChannel === null) {
|
|
146
|
+
// Unreachable: parseEvidenceTarget's own guarantee.
|
|
147
|
+
throw new TypeError('call-inputs evidence target carries no transport channel');
|
|
148
|
+
}
|
|
149
|
+
const firstToken = target.tail[0];
|
|
150
|
+
if (firstToken === undefined) {
|
|
151
|
+
throw new TypeError('evidence-target tail is non-empty but has no first token');
|
|
152
|
+
}
|
|
153
|
+
const { requiredKeys, permittedKeys, types } = operation.requestShape[transportChannel];
|
|
154
|
+
if (!requiredKeys.includes(firstToken) &&
|
|
155
|
+
!permittedKeys.includes(firstToken)) {
|
|
156
|
+
return unreachable(`addresses call-inputs ${transportChannel} field "${firstToken}", which operation "${operation.operationId}" declares in neither requiredKeys nor permittedKeys`);
|
|
157
|
+
}
|
|
158
|
+
if (descendsIntoDeclaredScalar(types, target.tail, firstToken)) {
|
|
159
|
+
return unreachable(`descends into call-inputs ${transportChannel} field "${firstToken}", which operation "${operation.operationId}" declares a scalar with no further structure`);
|
|
160
|
+
}
|
|
161
|
+
return reachable();
|
|
162
|
+
}
|
|
163
|
+
// response-headers, response-status, and exit-code declare no shape, so
|
|
164
|
+
// a resolving operation is the entire compile-time check available.
|
|
165
|
+
return reachable();
|
|
166
|
+
}
|
|
167
|
+
/** `unreachable-check-evidence`: an interaction-rooted pointer the declared interfaces cannot produce. */
|
|
168
|
+
export function checkEvidenceReachability(contract) {
|
|
169
|
+
let index;
|
|
170
|
+
forEachCheckPointer(contract, (site, oracleId) => {
|
|
171
|
+
index ??= buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
172
|
+
const result = evaluatePointerReachability(site.pointer, index);
|
|
173
|
+
if (!result.reachable) {
|
|
174
|
+
throw new StructuralFailure('unreachable-check-evidence', `EvalContract.oracles[id=${oracleId}].${site.path}`, `"${site.pointer}" ${result.reason}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Runs ahead of the three checks below, reversing AD-5's registry order for
|
|
4
|
+
* this one pair. Every other rubric check reports a path shaped
|
|
5
|
+
* `rubrics[id=...]`, and a duplicated id makes that path address two rubrics,
|
|
6
|
+
* so identifiers are settled first and every later path is unambiguous by
|
|
7
|
+
* construction.
|
|
8
|
+
*
|
|
9
|
+
* Criterion ids are unique within a rubric and may repeat across rubrics: a
|
|
10
|
+
* judge-call score cites `rubricId` and `criterionId` together, so the pair is
|
|
11
|
+
* what has to be unique.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkRubricIdentifiers(contract: EvalContract): void;
|
|
14
|
+
/** The first forbidden term in `text`, or `undefined`. */
|
|
15
|
+
export declare function findReasoningProseTerm(text: string): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Criterion text is scanned first: AD-17 and AD-5 both scope this code to a
|
|
18
|
+
* criterion, so where a rubric carries the defect in more than one place the
|
|
19
|
+
* in-scope site is the one reported. Anchors and penalties are a deliberate
|
|
20
|
+
* expansion. An anchor reading "level 3: the reasoning is sound" scores
|
|
21
|
+
* reasoning through every criterion at once, and a penalty deducting for an
|
|
22
|
+
* unstated rationale scores it through a deduction. Leave either unscanned and
|
|
23
|
+
* an author walks around the criterion scan in one edit.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkRubricReasoningProse(contract: EvalContract): void;
|
|
26
|
+
/**
|
|
27
|
+
* AD-22's three shapes, spelled out: an unanchored scale (rules 1 to 3), an
|
|
28
|
+
* unbounded length (rule 4), and missing named failure-mode penalties (rules 5
|
|
29
|
+
* and 6). Rule 7 applies rule 3's argument to a criterion and is the one rule
|
|
30
|
+
* that stretches AD-22's wording: a criterion stating no question is
|
|
31
|
+
* unanswerable in a way `rubric-evidence-unreachable` cannot see, since its
|
|
32
|
+
* pointer may resolve perfectly.
|
|
33
|
+
*
|
|
34
|
+
* A single-level scale, a repeated penalty name, and a rubric with no criteria
|
|
35
|
+
* are all legal, each with an accept test guarding it. The first two were
|
|
36
|
+
* proposed and rejected in review; AD-19 settles the third by stating that
|
|
37
|
+
* AD-17's rules bind only when a rubric names criteria.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkRubricAnchoring(contract: EvalContract): void;
|
|
40
|
+
/**
|
|
41
|
+
* `evaluatePointerReachability` is reused, so the two evidence codes give the
|
|
42
|
+
* same reason for the same broken pointer. Two limits come with it. A `@/`
|
|
43
|
+
* pointer cannot parse into `criterion.evidence`, an `InteractionPointer`, so
|
|
44
|
+
* that branch is dead here. And a channel that declares no shape resolves as
|
|
45
|
+
* soon as its step does, so a criterion rooted at `stdout` compiles on an
|
|
46
|
+
* `api`-kind contract that can never produce one. That gap belongs to
|
|
47
|
+
* `unreachable-check-evidence` as well, and a second implementation written to
|
|
48
|
+
* close it for rubrics alone would leave the two codes disagreeing about the
|
|
49
|
+
* same pointer.
|
|
50
|
+
*
|
|
51
|
+
* `duplicateIds: 'unresolved'` is load-bearing. `buildPlanIndex` otherwise
|
|
52
|
+
* throws a `TypeError` on a duplicate step or operation id, and a caller has
|
|
53
|
+
* no way to classify a `TypeError` that escapes a compile check.
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkRubricEvidenceReachability(contract: EvalContract): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-22's compile-time rubric checks. `checkRubricIdentifiers` runs first and
|
|
3
|
+
* fires `rubric-unanchored`; the other three follow AD-5's registry order,
|
|
4
|
+
* `rubric-scores-reasoning-prose`, `rubric-unanchored`, then
|
|
5
|
+
* `rubric-evidence-unreachable`. Each throws `StructuralFailure` on the first
|
|
6
|
+
* violation, the convention every `core/compile/` module keeps. An empty
|
|
7
|
+
* `rubrics` array is a no-op in all four, which is what makes a zero-rubric
|
|
8
|
+
* contract compile clean.
|
|
9
|
+
*/
|
|
10
|
+
import { StructuralFailure } from '../failure-codes.js';
|
|
11
|
+
import { buildPlanIndex } from '../seal/plan-index.js';
|
|
12
|
+
import { evaluatePointerReachability } from './reachability.js';
|
|
13
|
+
const rubricPath = (rubric) => `EvalContract.rubrics[id=${rubric.id}]`;
|
|
14
|
+
// `trim()` leaves U+200B through U+200D and U+FEFF standing, and a zero-width
|
|
15
|
+
// space states no observable condition either.
|
|
16
|
+
const INVISIBLE = /[\u200b-\u200d\ufeff]/g;
|
|
17
|
+
const blank = (value) => value.replace(INVISIBLE, '').trim().length === 0;
|
|
18
|
+
// Function declarations, so TypeScript narrows control flow through the
|
|
19
|
+
// `never` return type: a guard below reads a nullable field straight after the
|
|
20
|
+
// call that rejects its null case.
|
|
21
|
+
function unanchored(path, detail) {
|
|
22
|
+
throw new StructuralFailure('rubric-unanchored', path, detail);
|
|
23
|
+
}
|
|
24
|
+
function scoresReasoning(path, term) {
|
|
25
|
+
throw new StructuralFailure('rubric-scores-reasoning-prose', path, `scores stated-reasoning prose: matched "${term}" (AD-17, AD-22)`);
|
|
26
|
+
}
|
|
27
|
+
// ---- rubric-unanchored: addressable identifiers ---------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Runs ahead of the three checks below, reversing AD-5's registry order for
|
|
30
|
+
* this one pair. Every other rubric check reports a path shaped
|
|
31
|
+
* `rubrics[id=...]`, and a duplicated id makes that path address two rubrics,
|
|
32
|
+
* so identifiers are settled first and every later path is unambiguous by
|
|
33
|
+
* construction.
|
|
34
|
+
*
|
|
35
|
+
* Criterion ids are unique within a rubric and may repeat across rubrics: a
|
|
36
|
+
* judge-call score cites `rubricId` and `criterionId` together, so the pair is
|
|
37
|
+
* what has to be unique.
|
|
38
|
+
*/
|
|
39
|
+
export function checkRubricIdentifiers(contract) {
|
|
40
|
+
const seenRubricIds = new Set();
|
|
41
|
+
contract.rubrics.forEach((rubric, index) => {
|
|
42
|
+
if (seenRubricIds.has(rubric.id)) {
|
|
43
|
+
unanchored(`EvalContract.rubrics[${index}].id`, `repeats rubric id "${rubric.id}", which a finding could then cite ambiguously (AD-22)`);
|
|
44
|
+
}
|
|
45
|
+
seenRubricIds.add(rubric.id);
|
|
46
|
+
});
|
|
47
|
+
// Second pass, so no `rubrics[id=...]` path is emitted while a rubric id
|
|
48
|
+
// could still address two rubrics.
|
|
49
|
+
for (const rubric of contract.rubrics) {
|
|
50
|
+
const seenCriterionIds = new Set();
|
|
51
|
+
rubric.criteria.forEach((criterion, index) => {
|
|
52
|
+
if (seenCriterionIds.has(criterion.id)) {
|
|
53
|
+
unanchored(`${rubricPath(rubric)}.criteria[${index}].id`, `repeats criterion id "${criterion.id}" inside one rubric (AD-22)`);
|
|
54
|
+
}
|
|
55
|
+
seenCriterionIds.add(criterion.id);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ---- rubric-scores-reasoning-prose ---------------------------------------
|
|
60
|
+
// AD-17 forbids scoring chain-of-thought or stated-reasoning prose. Nothing in
|
|
61
|
+
// the schema declares which channel carries reasoning, so this check reads
|
|
62
|
+
// wording, the same trade `auditBriefScripting` makes. The alternative was a
|
|
63
|
+
// rung with no throw site.
|
|
64
|
+
//
|
|
65
|
+
// A term earns a place only when it has no ordinary sense in an API contract.
|
|
66
|
+
// A compile-time code has no waiver path, so a term that fires on legal
|
|
67
|
+
// authoring gets reworded around. Review dropped `scratchpad` (a file),
|
|
68
|
+
// `deliberation` (an endpoint), the `justif-` family (an error body justifies
|
|
69
|
+
// a 409), `explanation` (an error message explains what went wrong), bare
|
|
70
|
+
// `thinking` (a measurement, as in thinking time), bare `CoT` (an ordinary
|
|
71
|
+
// English noun), and `explain why` (a criterion can ask why an observable
|
|
72
|
+
// status was returned).
|
|
73
|
+
//
|
|
74
|
+
// `reasoning` and `rationale` stay despite their field-name senses. AD-17's
|
|
75
|
+
// own words are "stated-reasoning prose", and a criterion carries an
|
|
76
|
+
// `evidence` pointer so its text never has to name a field. Plurals and
|
|
77
|
+
// `-ing` are covered because one letter would otherwise walk past the check.
|
|
78
|
+
// ASCII hyphen, U+2010 hyphen, U+2011 non-breaking hyphen, or whitespace, one
|
|
79
|
+
// or more. A paste out of a styled document carries the two non-ASCII forms,
|
|
80
|
+
// and a single-character class made a doubled separator an evasion.
|
|
81
|
+
const SEPARATOR = '[-\\u2010\\u2011\\s]+';
|
|
82
|
+
// Built from fragments the way `pointer.ts` builds its pointer grammar, so the
|
|
83
|
+
// separator is stated once. The pattern is not global, so no `lastIndex`
|
|
84
|
+
// carries between calls. The first match is the whole answer.
|
|
85
|
+
const REASONING_PROSE_PATTERN = new RegExp(`\\b(?:chains?${SEPARATOR}of${SEPARATOR}thoughts?|trains?${SEPARATOR}of${SEPARATOR}thoughts?|thought${SEPARATOR}process(?:es|ing)?|thinking${SEPARATOR}process(?:es)?|(?:internal|inner)${SEPARATOR}monologues?|self${SEPARATOR}explanations?|reasonings?|rationales?)\\b`, 'i');
|
|
86
|
+
/** The first forbidden term in `text`, or `undefined`. */
|
|
87
|
+
export function findReasoningProseTerm(text) {
|
|
88
|
+
return REASONING_PROSE_PATTERN.exec(text)?.[0];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Criterion text is scanned first: AD-17 and AD-5 both scope this code to a
|
|
92
|
+
* criterion, so where a rubric carries the defect in more than one place the
|
|
93
|
+
* in-scope site is the one reported. Anchors and penalties are a deliberate
|
|
94
|
+
* expansion. An anchor reading "level 3: the reasoning is sound" scores
|
|
95
|
+
* reasoning through every criterion at once, and a penalty deducting for an
|
|
96
|
+
* unstated rationale scores it through a deduction. Leave either unscanned and
|
|
97
|
+
* an author walks around the criterion scan in one edit.
|
|
98
|
+
*/
|
|
99
|
+
export function checkRubricReasoningProse(contract) {
|
|
100
|
+
for (const rubric of contract.rubrics) {
|
|
101
|
+
const path = rubricPath(rubric);
|
|
102
|
+
for (const criterion of rubric.criteria) {
|
|
103
|
+
const term = findReasoningProseTerm(criterion.text);
|
|
104
|
+
if (term !== undefined)
|
|
105
|
+
scoresReasoning(`${path}.criteria[id=${criterion.id}].text`, term);
|
|
106
|
+
}
|
|
107
|
+
rubric.scaleLevels?.forEach((level, index) => {
|
|
108
|
+
const term = findReasoningProseTerm(level.anchor);
|
|
109
|
+
if (term !== undefined)
|
|
110
|
+
scoresReasoning(`${path}.scaleLevels[${index}].anchor`, term);
|
|
111
|
+
});
|
|
112
|
+
rubric.failureModePenalties?.forEach((penalty, index) => {
|
|
113
|
+
const penaltyPath = `${path}.failureModePenalties[${index}]`;
|
|
114
|
+
const nameTerm = findReasoningProseTerm(penalty.name);
|
|
115
|
+
if (nameTerm !== undefined)
|
|
116
|
+
scoresReasoning(`${penaltyPath}.name`, nameTerm);
|
|
117
|
+
const descriptionTerm = findReasoningProseTerm(penalty.description);
|
|
118
|
+
if (descriptionTerm !== undefined)
|
|
119
|
+
scoresReasoning(`${penaltyPath}.description`, descriptionTerm);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ---- rubric-unanchored: the scoring instrument ---------------------------
|
|
124
|
+
/**
|
|
125
|
+
* AD-22's three shapes, spelled out: an unanchored scale (rules 1 to 3), an
|
|
126
|
+
* unbounded length (rule 4), and missing named failure-mode penalties (rules 5
|
|
127
|
+
* and 6). Rule 7 applies rule 3's argument to a criterion and is the one rule
|
|
128
|
+
* that stretches AD-22's wording: a criterion stating no question is
|
|
129
|
+
* unanswerable in a way `rubric-evidence-unreachable` cannot see, since its
|
|
130
|
+
* pointer may resolve perfectly.
|
|
131
|
+
*
|
|
132
|
+
* A single-level scale, a repeated penalty name, and a rubric with no criteria
|
|
133
|
+
* are all legal, each with an accept test guarding it. The first two were
|
|
134
|
+
* proposed and rejected in review; AD-19 settles the third by stating that
|
|
135
|
+
* AD-17's rules bind only when a rubric names criteria.
|
|
136
|
+
*/
|
|
137
|
+
export function checkRubricAnchoring(contract) {
|
|
138
|
+
for (const rubric of contract.rubrics) {
|
|
139
|
+
const path = rubricPath(rubric);
|
|
140
|
+
const { scaleLevels, failureModePenalties } = rubric;
|
|
141
|
+
if (scaleLevels === null || scaleLevels.length === 0)
|
|
142
|
+
unanchored(`${path}.scaleLevels`, 'declares no anchored scale levels (AD-22)');
|
|
143
|
+
const seenOrdinals = new Set();
|
|
144
|
+
scaleLevels.forEach((level, index) => {
|
|
145
|
+
if (seenOrdinals.has(level.level)) {
|
|
146
|
+
unanchored(`${path}.scaleLevels[${index}].level`, `repeats scale-level ordinal ${level.level}, so the ordinal addresses two anchors (AD-22)`);
|
|
147
|
+
}
|
|
148
|
+
seenOrdinals.add(level.level);
|
|
149
|
+
});
|
|
150
|
+
scaleLevels.forEach((level, index) => {
|
|
151
|
+
if (blank(level.anchor)) {
|
|
152
|
+
unanchored(`${path}.scaleLevels[${index}].anchor`, 'states no observable condition, so the level is unanchored (AD-22)');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
if (rubric.maxLength === null)
|
|
156
|
+
unanchored(`${path}.maxLength`, 'declares no bounded length (AD-22)');
|
|
157
|
+
if (failureModePenalties === null || failureModePenalties.length === 0)
|
|
158
|
+
unanchored(`${path}.failureModePenalties`, 'names no failure-mode penalties (AD-22)');
|
|
159
|
+
failureModePenalties.forEach((penalty, index) => {
|
|
160
|
+
if (blank(penalty.name)) {
|
|
161
|
+
unanchored(`${path}.failureModePenalties[${index}].name`, 'is blank, so the penalty is unnamed (AD-22)');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
for (const criterion of rubric.criteria) {
|
|
165
|
+
if (blank(criterion.text)) {
|
|
166
|
+
unanchored(`${path}.criteria[id=${criterion.id}].text`, 'states no question, so the criterion is unanchored (AD-22)');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ---- rubric-evidence-unreachable -----------------------------------------
|
|
172
|
+
/**
|
|
173
|
+
* `evaluatePointerReachability` is reused, so the two evidence codes give the
|
|
174
|
+
* same reason for the same broken pointer. Two limits come with it. A `@/`
|
|
175
|
+
* pointer cannot parse into `criterion.evidence`, an `InteractionPointer`, so
|
|
176
|
+
* that branch is dead here. And a channel that declares no shape resolves as
|
|
177
|
+
* soon as its step does, so a criterion rooted at `stdout` compiles on an
|
|
178
|
+
* `api`-kind contract that can never produce one. That gap belongs to
|
|
179
|
+
* `unreachable-check-evidence` as well, and a second implementation written to
|
|
180
|
+
* close it for rubrics alone would leave the two codes disagreeing about the
|
|
181
|
+
* same pointer.
|
|
182
|
+
*
|
|
183
|
+
* `duplicateIds: 'unresolved'` is load-bearing. `buildPlanIndex` otherwise
|
|
184
|
+
* throws a `TypeError` on a duplicate step or operation id, and a caller has
|
|
185
|
+
* no way to classify a `TypeError` that escapes a compile check.
|
|
186
|
+
*/
|
|
187
|
+
export function checkRubricEvidenceReachability(contract) {
|
|
188
|
+
let index;
|
|
189
|
+
for (const rubric of contract.rubrics) {
|
|
190
|
+
for (const criterion of rubric.criteria) {
|
|
191
|
+
index ??= buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
192
|
+
const result = evaluatePointerReachability(criterion.evidence, index);
|
|
193
|
+
if (!result.reachable) {
|
|
194
|
+
throw new StructuralFailure('rubric-evidence-unreachable', `${rubricPath(rubric)}.criteria[id=${criterion.id}].evidence`, `"${criterion.evidence}" ${result.reason}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
|
+
/** `nested-temporal-clause`: a step's declared parent itself carries a temporal clause. */
|
|
3
|
+
export declare function checkNestedTemporalClause(contract: EvalContract): void;
|
|
4
|
+
/** `plan-exceeds-scripting-bound`: the graph predicate over the whole plan. */
|
|
5
|
+
export declare function checkScriptingBound(contract: EvalContract): void;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-39's two interaction-plan graph checks: `nested-temporal-clause` and
|
|
3
|
+
* `plan-exceeds-scripting-bound`, each walking `contract.interactionPlan`
|
|
4
|
+
* once. Both throw `StructuralFailure` on the first violation, matching
|
|
5
|
+
* every other `core/compile/` module's fail-fast convention.
|
|
6
|
+
*/
|
|
7
|
+
import { StructuralFailure } from '../failure-codes.js';
|
|
8
|
+
import { buildPlanIndex } from '../seal/plan-index.js';
|
|
9
|
+
/**
|
|
10
|
+
* A dangling `after` (naming no declared step, including one made
|
|
11
|
+
* unresolvable by a duplicate id) resolves to `undefined`, the same as a
|
|
12
|
+
* genuinely absent clause.
|
|
13
|
+
*/
|
|
14
|
+
function parentOf(step, index) {
|
|
15
|
+
if (step.after === null)
|
|
16
|
+
return undefined;
|
|
17
|
+
return index.stepOf(step.after);
|
|
18
|
+
}
|
|
19
|
+
function planIndexOf(contract) {
|
|
20
|
+
return buildPlanIndex(contract.interactionPlan, contract.permittedInterfaces, { duplicateIds: 'unresolved' });
|
|
21
|
+
}
|
|
22
|
+
// ---- nested-temporal-clause ------------------------------------------------
|
|
23
|
+
/** `nested-temporal-clause`: a step's declared parent itself carries a temporal clause. */
|
|
24
|
+
export function checkNestedTemporalClause(contract) {
|
|
25
|
+
const index = planIndexOf(contract);
|
|
26
|
+
for (const step of contract.interactionPlan) {
|
|
27
|
+
const parent = parentOf(step, index);
|
|
28
|
+
if (parent !== undefined && parentOf(parent, index) !== undefined) {
|
|
29
|
+
throw new StructuralFailure('nested-temporal-clause', `EvalContract.interactionPlan[stepId=${step.stepId}].after`, `names step "${parent.stepId}", which itself carries a temporal clause; a chain may not nest more than one level (AD-39)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ---- plan-exceeds-scripting-bound ------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Bounds are exclusive ceilings: a metric sitting exactly at its bound is
|
|
36
|
+
* legal. Each is set against this codebase's own two whole-contract
|
|
37
|
+
* fixtures as the accept floor and AD-39's two counterexample plans as the
|
|
38
|
+
* reject ceiling: the eight-step single-root chain and the sixty-four
|
|
39
|
+
* independent `write-N`/`read-N` pairs, which AD-39 records as passing a
|
|
40
|
+
* depth bound alone. AD-5 mandates one authored reject fixture per shape
|
|
41
|
+
* this predicate rejects, and `tests/compile/scripting-bound.test.ts`
|
|
42
|
+
* carries them.
|
|
43
|
+
*/
|
|
44
|
+
const WIDTH_MAX = 2; // gateCContract's own submit -> {poll, first-page} sits exactly here.
|
|
45
|
+
const SHARED_ANCHOR_MAX = 2; // twice gateCContract's own shared-anchor count of 1 (no real fixture reaches 2).
|
|
46
|
+
const DISJOINT_PAIR_MAX = 4; // populatedContract's own create -> list pair is 1.
|
|
47
|
+
const STEP_COUNT_MAX = 16; // gateCContract is 6, populatedContract is 2; roughly 2.7x the larger.
|
|
48
|
+
/**
|
|
49
|
+
* One pass over the plan's `after` edges, building three views at once: the
|
|
50
|
+
* one-hop nesting test (shared with `checkNestedTemporalClause`), each
|
|
51
|
+
* anchor's child count (width, shared anchors), and an undirected adjacency
|
|
52
|
+
* map for the connected-component scan below (disjoint pairs). O(n) in the
|
|
53
|
+
* step count.
|
|
54
|
+
*
|
|
55
|
+
* Every internal map is keyed by each step's array position. `stepId` is
|
|
56
|
+
* schema-legal to duplicate, so keying on it would merge two distinct
|
|
57
|
+
* steps sharing an id into one adjacency entry and corrupt
|
|
58
|
+
* `maxWidth`/`sharedAnchorCount`/`disjointPairCount`.
|
|
59
|
+
*/
|
|
60
|
+
function computeGraphMetrics(plan, index) {
|
|
61
|
+
const positionOf = new Map();
|
|
62
|
+
for (const [position, step] of plan.entries())
|
|
63
|
+
positionOf.set(step, position);
|
|
64
|
+
const children = new Map();
|
|
65
|
+
const adjacency = new Map();
|
|
66
|
+
for (const [position] of plan.entries())
|
|
67
|
+
adjacency.set(position, new Set());
|
|
68
|
+
let hasNestedChain = false;
|
|
69
|
+
for (const [position, step] of plan.entries()) {
|
|
70
|
+
const parent = parentOf(step, index);
|
|
71
|
+
if (parent === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
if (parentOf(parent, index) !== undefined)
|
|
74
|
+
hasNestedChain = true;
|
|
75
|
+
const parentPosition = positionOf.get(parent);
|
|
76
|
+
if (parentPosition === undefined) {
|
|
77
|
+
// Should-never-happen precondition violation, matching `resolveStep`'s
|
|
78
|
+
// convention (`plan-index.ts`): `index` is always built from this same
|
|
79
|
+
// `plan` array, so a resolved parent is always one of its elements.
|
|
80
|
+
throw new TypeError(`scripting-bound: resolved parent for step "${step.stepId}" is not present in the plan passed to computeGraphMetrics`);
|
|
81
|
+
}
|
|
82
|
+
const siblings = children.get(parentPosition);
|
|
83
|
+
if (siblings === undefined)
|
|
84
|
+
children.set(parentPosition, [position]);
|
|
85
|
+
else
|
|
86
|
+
siblings.push(position);
|
|
87
|
+
adjacency.get(position)?.add(parentPosition);
|
|
88
|
+
adjacency.get(parentPosition)?.add(position);
|
|
89
|
+
}
|
|
90
|
+
let maxWidth = 0;
|
|
91
|
+
let sharedAnchorCount = 0;
|
|
92
|
+
for (const siblings of children.values()) {
|
|
93
|
+
if (siblings.length > maxWidth)
|
|
94
|
+
maxWidth = siblings.length;
|
|
95
|
+
if (siblings.length >= 2)
|
|
96
|
+
sharedAnchorCount += 1;
|
|
97
|
+
}
|
|
98
|
+
// Connected components over the undirected `after` graph; a component of
|
|
99
|
+
// exactly two nodes is one disjoint witness pair (the 64-pair fixture's
|
|
100
|
+
// own shape). A plain stack (`.pop()`) is enough for an unordered scan;
|
|
101
|
+
// `.shift()` would cost O(n) per call.
|
|
102
|
+
let disjointPairCount = 0;
|
|
103
|
+
const visited = new Set();
|
|
104
|
+
for (const [position] of plan.entries()) {
|
|
105
|
+
if (visited.has(position))
|
|
106
|
+
continue;
|
|
107
|
+
let size = 0;
|
|
108
|
+
const stack = [position];
|
|
109
|
+
visited.add(position);
|
|
110
|
+
while (stack.length > 0) {
|
|
111
|
+
const id = stack.pop();
|
|
112
|
+
if (id === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
size += 1;
|
|
115
|
+
for (const neighbor of adjacency.get(id) ?? []) {
|
|
116
|
+
if (!visited.has(neighbor)) {
|
|
117
|
+
visited.add(neighbor);
|
|
118
|
+
stack.push(neighbor);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (size === 2)
|
|
123
|
+
disjointPairCount += 1;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
hasNestedChain,
|
|
127
|
+
maxWidth,
|
|
128
|
+
sharedAnchorCount,
|
|
129
|
+
disjointPairCount,
|
|
130
|
+
stepCount: plan.length,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** `plan-exceeds-scripting-bound`: the graph predicate over the whole plan. */
|
|
134
|
+
export function checkScriptingBound(contract) {
|
|
135
|
+
const index = planIndexOf(contract);
|
|
136
|
+
const metrics = computeGraphMetrics(contract.interactionPlan, index);
|
|
137
|
+
const violation = metrics.hasNestedChain
|
|
138
|
+
? 'its deepest temporal chain exceeds the published one-level bound'
|
|
139
|
+
: metrics.maxWidth > WIDTH_MAX
|
|
140
|
+
? `one step anchors ${metrics.maxWidth} other steps, past the published width bound of ${WIDTH_MAX}`
|
|
141
|
+
: metrics.sharedAnchorCount > SHARED_ANCHOR_MAX
|
|
142
|
+
? `${metrics.sharedAnchorCount} steps each anchor more than one other step, past the published bound of ${SHARED_ANCHOR_MAX} shared anchors`
|
|
143
|
+
: metrics.disjointPairCount > DISJOINT_PAIR_MAX
|
|
144
|
+
? `${metrics.disjointPairCount} mutually disjoint two-step witness pairs appear in one plan, past the published bound of ${DISJOINT_PAIR_MAX}`
|
|
145
|
+
: metrics.stepCount > STEP_COUNT_MAX
|
|
146
|
+
? `the plan declares ${metrics.stepCount} steps, past the published bound of ${STEP_COUNT_MAX}: an exhaustive operation inventory rather than a bounded set of witness relations`
|
|
147
|
+
: undefined;
|
|
148
|
+
if (violation !== undefined) {
|
|
149
|
+
throw new StructuralFailure('plan-exceeds-scripting-bound', 'EvalContract.interactionPlan', `${violation} (AD-5, AD-39)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-10's three declaration-side witness checks: that every input-bearing
|
|
3
|
+
* operation declares one, that a declared one is shaped so the plan can
|
|
4
|
+
* execute it, and that its leg identifiers are distinct from each other and
|
|
5
|
+
* from every interaction-plan step.
|
|
6
|
+
*
|
|
7
|
+
* No new AD-5 code is minted. The registry is closed at twenty-one and
|
|
8
|
+
* `check:ad5-registry` pins it against the spine, so each defect below takes the
|
|
9
|
+
* code that already names it. Two rows stretch that reading: a leg-id equality
|
|
10
|
+
* and a leg-id/step-id collision are identifier collisions, and
|
|
11
|
+
* `malformed-operator-expression` is the closest available code because the
|
|
12
|
+
* relation is illegal in its position once its operands cannot be told apart.
|
|
13
|
+
*/
|
|
14
|
+
import { declaresNoRequestKeys } from '../declared-inputs.ts';
|
|
15
|
+
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
16
|
+
import type { Operation } from '../schemas/interface.ts';
|
|
17
|
+
import { type TransportChannelName } from '../schemas/pointer.ts';
|
|
18
|
+
import type { WitnessInputs } from '../schemas/sensitivity-witness.ts';
|
|
19
|
+
export { declaresNoRequestKeys };
|
|
20
|
+
/**
|
|
21
|
+
* The keys one set of witness inputs supplies on one channel. A body that is
|
|
22
|
+
* absent, or JSON that is not an object, supplies no keys: the channel is
|
|
23
|
+
* declared as a keyed shape, so such a leg omits every required key rather
|
|
24
|
+
* than being exempt from the comparison.
|
|
25
|
+
*/
|
|
26
|
+
export declare function suppliedKeys(inputs: WitnessInputs, channel: TransportChannelName): readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Every channel of one set of witness inputs against the operation that will
|
|
29
|
+
* receive them. All four, not only the differential channel: `planPreflight`
|
|
30
|
+
* copies all four onto the `ProbeRequest` and the port sends them, so a value
|
|
31
|
+
* on an unselected channel is as much an outbound value as one on the selected
|
|
32
|
+
* channel.
|
|
33
|
+
*
|
|
34
|
+
* The two directions have different standing, and the difference matters
|
|
35
|
+
* enough to name, because the next reader will diff this against
|
|
36
|
+
* `checkUndeclaredMandatoryInput` and find the two disagreeing.
|
|
37
|
+
*
|
|
38
|
+
* The permitted direction is that function's rule, carried over: it loops the
|
|
39
|
+
* same four channels and rejects a key the operation declares in neither list.
|
|
40
|
+
* That is what makes `WitnessInputs`'s AD-18 promise about `header` more than a
|
|
41
|
+
* comment.
|
|
42
|
+
*
|
|
43
|
+
* The required direction is new here, because the two shapes differ. An
|
|
44
|
+
* `InputBindingChannel` is nullable, and `null` means "this step binds nothing in
|
|
45
|
+
* this channel", so `checkUndeclaredMandatoryInput` skips an unbound channel and
|
|
46
|
+
* asks nothing about required keys. `WitnessInputs` has all four channels as
|
|
47
|
+
* concrete values, so a leg omitting a required key is a request the port cannot
|
|
48
|
+
* issue. The cost is that a required header forces a literal value into the
|
|
49
|
+
* contract artifact, the surface AD-18 governs; a placeholder satisfies this
|
|
50
|
+
* check, and what pre-flight probes is a fixture.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkInputsAgainstShape(inputs: WitnessInputs, operation: Operation, owner: string, artifactPath: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Strict-gated, alongside `checkUndeclaredMandatoryInput`, whose code this
|
|
55
|
+
* shares: `compile.ts` already gates that code behind `options.strict`, and one
|
|
56
|
+
* code with two gating regimes would be worse than one code with two
|
|
57
|
+
* conditions.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkSensitivityWitnessDeclared(contract: EvalContract): void;
|
|
60
|
+
/**
|
|
61
|
+
* The shape rules: the differential channel agrees with the state-change
|
|
62
|
+
* marker, the relation reads both legs and nothing else, and a declared
|
|
63
|
+
* fixture reset names a mutating operation the contract declares. Unconditional,
|
|
64
|
+
* since none of these fires `undeclared-mandatory-input`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function checkWitnessLegality(contract: EvalContract): void;
|
|
67
|
+
/**
|
|
68
|
+
* Leg identifiers share one namespace with interaction-plan step identifiers,
|
|
69
|
+
* because a witness relation is an ordinary AD-4 expression and addresses a leg
|
|
70
|
+
* as `/interactions/{legId}/…`. A collision would make a relation pointer
|
|
71
|
+
* resolve against the wrong observation.
|
|
72
|
+
*/
|
|
73
|
+
export declare function checkWitnessLegIdentifiers(contract: EvalContract): void;
|