eval-quality 1.3.0 → 1.4.1
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/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +1 -0
- package/dist/application/score.d.ts +12 -0
- package/dist/application/score.js +10 -2
- package/dist/cli/render.d.ts +9 -1
- package/dist/cli/render.js +12 -1
- package/dist/cli/run.js +9 -1
- package/dist/core/evaluate/operators.d.ts +9 -5
- package/dist/core/evaluate/operators.js +9 -5
- package/dist/core/evaluate/resolution.js +36 -27
- package/dist/core/preflight/reduce.js +85 -0
- package/dist/core/schemas/evidence-artifact.js +1 -1
- package/dist/core/schemas/sensitivity-witness.d.ts +10 -7
- package/dist/core/schemas/sensitivity-witness.js +10 -7
- package/dist/core/score/score.d.ts +8 -1
- package/dist/core/score/score.js +14 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/schemas/evidence-artifact.schema.json +2 -2
|
@@ -15,6 +15,8 @@ export type { RuntimeFaultCode } from '../core/schemas/faults.ts';
|
|
|
15
15
|
export { RUNTIME_FAULT_CODES, RuntimeFault } from '../core/schemas/faults.ts';
|
|
16
16
|
export type { EvaluatorRecommendation, Verdict, } from '../core/schemas/verdict.ts';
|
|
17
17
|
export { EVALUATOR_RECOMMENDATIONS, VERDICTS, } from '../core/schemas/verdict.ts';
|
|
18
|
+
export type { QualificationFailure, QualificationFailureCode, QualificationResult, } from '../core/score/qualification.ts';
|
|
19
|
+
export { QUALIFICATION_FAILURES } from '../core/score/qualification.ts';
|
|
18
20
|
export { compile } from './compile.ts';
|
|
19
21
|
export type { Diagnostic, DiagnosticSink } from './diagnostics.ts';
|
|
20
22
|
export type { PreflightFromObservationsOptions, RunPreflightOptions, } from './preflight.ts';
|
|
@@ -11,6 +11,7 @@ export { validateLineageChain } from '../core/lineage/chain.js';
|
|
|
11
11
|
export { INTERCHANGE_ARTIFACT_KEYS } from '../core/schemas/artifact.js';
|
|
12
12
|
export { RUNTIME_FAULT_CODES, RuntimeFault } from '../core/schemas/faults.js';
|
|
13
13
|
export { EVALUATOR_RECOMMENDATIONS, VERDICTS, } from '../core/schemas/verdict.js';
|
|
14
|
+
export { QUALIFICATION_FAILURES } from '../core/score/qualification.js';
|
|
14
15
|
export { compile } from './compile.js';
|
|
15
16
|
export { preflightFromObservations, runPreflight } from './preflight.js';
|
|
16
17
|
export { runScore } from './score.js';
|
|
@@ -8,6 +8,7 @@ import { Probe } from '../core/schemas/probe.ts';
|
|
|
8
8
|
import { ScoringPolicy } from '../core/schemas/scoring-policy.ts';
|
|
9
9
|
import { SealedRunRecord } from '../core/schemas/sealed-run-record.ts';
|
|
10
10
|
import type { LadderResolution } from '../core/score/ladder.ts';
|
|
11
|
+
import type { QualificationResult } from '../core/score/qualification.ts';
|
|
11
12
|
import { type CorpusPort } from '../ports/corpus-port.ts';
|
|
12
13
|
export type RunScoreOptions = {
|
|
13
14
|
readonly record: SealedRunRecord;
|
|
@@ -44,5 +45,16 @@ export type RunScoreResult = {
|
|
|
44
45
|
*/
|
|
45
46
|
readonly artifact: EvidenceArtifact | null;
|
|
46
47
|
readonly ladder: LadderResolution;
|
|
48
|
+
/**
|
|
49
|
+
* AD-9's gate over `options.probe`, carried out of `score` unchanged. A
|
|
50
|
+
* rejected probe resolves an oracle to `infrastructure-error` wherever no
|
|
51
|
+
* higher-precedence AD-33 row already resolved it, and any of those states
|
|
52
|
+
* lands the run on the Invalid rung, where `artifact` is `null` and there
|
|
53
|
+
* is no artifact field for the reason to travel in. A contract declaring no
|
|
54
|
+
* oracles resolves no outcome at all: that run mints an artifact carrying
|
|
55
|
+
* no trace of the rejection. `failures` is the reason in both cases, in the
|
|
56
|
+
* closed `QualificationFailureCode` vocabulary.
|
|
57
|
+
*/
|
|
58
|
+
readonly qualification: QualificationResult;
|
|
47
59
|
};
|
|
48
60
|
export declare function runScore(options: RunScoreOptions): Promise<RunScoreResult>;
|
|
@@ -176,11 +176,19 @@ export async function runScore(options) {
|
|
|
176
176
|
// identical gap.
|
|
177
177
|
'none', false);
|
|
178
178
|
if (scored.ladder.verdict === null) {
|
|
179
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
artifact: null,
|
|
181
|
+
ladder: scored.ladder,
|
|
182
|
+
qualification: scored.probeQualification,
|
|
183
|
+
};
|
|
180
184
|
}
|
|
181
185
|
const artifact = emit(scored, corpusDigest,
|
|
182
186
|
// AD-11 names the same fixture digest `PreflightVerdict.fixtureDigest`
|
|
183
187
|
// already carries: restated, never re-derived.
|
|
184
188
|
preflightVerdict.fixtureDigest, record.evaluatorConfigurationDigest);
|
|
185
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
artifact,
|
|
191
|
+
ladder: scored.ladder,
|
|
192
|
+
qualification: scored.probeQualification,
|
|
193
|
+
};
|
|
186
194
|
}
|
package/dist/cli/render.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Diagnostic } from '../application/index.ts';
|
|
1
|
+
import { type Diagnostic, type QualificationFailure } from '../application/index.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Delegates to `serializeArtifact`; the canonical bytes are not re-derived
|
|
4
4
|
* here, so the text written to stdout is the text `digestArtifact` hashes.
|
|
@@ -14,6 +14,14 @@ export declare function renderDiagnostic(diagnostic: Diagnostic): string;
|
|
|
14
14
|
* own code looks like from outside.
|
|
15
15
|
*/
|
|
16
16
|
export declare function renderError(error: unknown): string;
|
|
17
|
+
/**
|
|
18
|
+
* One AD-9 qualification failure, in the same
|
|
19
|
+
* `<code>: <artifactPath>: <detail>` shape `renderError` uses. An unqualified
|
|
20
|
+
* probe is a domain outcome the ladder resolves to Invalid, so no
|
|
21
|
+
* `StructuralFailure` and no `RuntimeFault` carries it. It reads like one on
|
|
22
|
+
* stderr because the reader's question is the same.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderQualificationFailure(failure: QualificationFailure): string;
|
|
17
25
|
/**
|
|
18
26
|
* AD-21's seven exit codes, one line each. The `--help` output and the README
|
|
19
27
|
* table are this text, so the two cannot drift.
|
package/dist/cli/render.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The four shapes the binary writes, and the exit-code table two documents
|
|
3
3
|
* share. Every line the CLI emits is produced here, so a format change is one
|
|
4
|
-
* file.
|
|
4
|
+
* file. Five renderers over the four shapes: a qualification failure and an
|
|
5
|
+
* error both print `<code>: <artifactPath>: <detail>`.
|
|
5
6
|
*/
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { RuntimeFault, StructuralFailure, serializeArtifact, } from '../application/index.js';
|
|
@@ -81,6 +82,16 @@ export function renderError(error) {
|
|
|
81
82
|
}
|
|
82
83
|
return `${PREFIX}: ${String(error)}`;
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* One AD-9 qualification failure, in the same
|
|
87
|
+
* `<code>: <artifactPath>: <detail>` shape `renderError` uses. An unqualified
|
|
88
|
+
* probe is a domain outcome the ladder resolves to Invalid, so no
|
|
89
|
+
* `StructuralFailure` and no `RuntimeFault` carries it. It reads like one on
|
|
90
|
+
* stderr because the reader's question is the same.
|
|
91
|
+
*/
|
|
92
|
+
export function renderQualificationFailure(failure) {
|
|
93
|
+
return `${PREFIX}: ${failure.code}: ${failure.artifactPath}: ${failure.detail}`;
|
|
94
|
+
}
|
|
84
95
|
/**
|
|
85
96
|
* AD-21's seven exit codes, one line each. The `--help` output and the README
|
|
86
97
|
* table are this text, so the two cannot drift.
|
package/dist/cli/run.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* run in memory.
|
|
7
7
|
*/
|
|
8
8
|
import { compile, preflightFromObservations, RuntimeFault, runScore, StructuralFailure, seal, } from '../application/index.js';
|
|
9
|
-
import { EXIT_CODE_TABLE, renderArtifact, renderDiagnostic, renderError, renderUsage, } from './render.js';
|
|
9
|
+
import { EXIT_CODE_TABLE, renderArtifact, renderDiagnostic, renderError, renderQualificationFailure, renderUsage, } from './render.js';
|
|
10
10
|
export const APPLICATION = {
|
|
11
11
|
compile,
|
|
12
12
|
seal,
|
|
@@ -316,6 +316,14 @@ async function runScoreCommand(invocation, environment, application, target) {
|
|
|
316
316
|
port: corpusRoot === null ? undefined : environment.corpusPort(corpusRoot),
|
|
317
317
|
signal: environment.signal,
|
|
318
318
|
});
|
|
319
|
+
// A rejected probe resolves an oracle to `infrastructure-error` wherever no
|
|
320
|
+
// higher-precedence AD-33 row already resolved it, and no artifact field
|
|
321
|
+
// carries the reason on any rung. Written whatever the rung, since the gate
|
|
322
|
+
// also runs on a probe the ladder never invalidated, a contract declaring
|
|
323
|
+
// no oracles above all: the reasons are the same either way.
|
|
324
|
+
for (const failure of result.qualification.failures) {
|
|
325
|
+
environment.writeDiagnostic(renderQualificationFailure(failure));
|
|
326
|
+
}
|
|
319
327
|
if (result.artifact !== null) {
|
|
320
328
|
await emitArtifact(environment, result.artifact, 'score', target);
|
|
321
329
|
}
|
|
@@ -59,11 +59,15 @@ export declare function regexMatch(value: ResolvedValue, pattern: string, matchS
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function ordering(collection: ResolvedValue, key: string, order: 'ascending' | 'descending', _artifactPath: string): boolean;
|
|
61
61
|
/**
|
|
62
|
-
* An empty array is a legitimate zero count, never special-cased
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* An empty array is a legitimate zero count, never special-cased, and this
|
|
63
|
+
* function is called with one. `count-tolerance` reads the collection's
|
|
64
|
+
* cardinality, so `resolution.ts` marks it `total` over a collection observed
|
|
65
|
+
* to be present and empty and hands the `[]` straight through; the guard below
|
|
66
|
+
* is load-bearing on `collection.length === 0`. A collection-typed pointer
|
|
67
|
+
* that resolved `absent` is still intercepted there and never reaches here.
|
|
68
|
+
* The allowed deviation is compared unrounded: `actual` is an integer, so `<=`
|
|
69
|
+
* against a fractional deviation is already exact, and rounding either
|
|
70
|
+
* direction would move the declared boundary.
|
|
67
71
|
*/
|
|
68
72
|
export declare function countTolerance(collection: ResolvedValue, expected: number, tolerance: number, relative: boolean, _artifactPath: string): boolean;
|
|
69
73
|
/**
|
|
@@ -249,11 +249,15 @@ export function ordering(collection, key, order, _artifactPath) {
|
|
|
249
249
|
return true;
|
|
250
250
|
}
|
|
251
251
|
/**
|
|
252
|
-
* An empty array is a legitimate zero count, never special-cased
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
252
|
+
* An empty array is a legitimate zero count, never special-cased, and this
|
|
253
|
+
* function is called with one. `count-tolerance` reads the collection's
|
|
254
|
+
* cardinality, so `resolution.ts` marks it `total` over a collection observed
|
|
255
|
+
* to be present and empty and hands the `[]` straight through; the guard below
|
|
256
|
+
* is load-bearing on `collection.length === 0`. A collection-typed pointer
|
|
257
|
+
* that resolved `absent` is still intercepted there and never reaches here.
|
|
258
|
+
* The allowed deviation is compared unrounded: `actual` is an integer, so `<=`
|
|
259
|
+
* against a fractional deviation is already exact, and rounding either
|
|
260
|
+
* direction would move the declared boundary.
|
|
257
261
|
*/
|
|
258
262
|
export function countTolerance(collection, expected, tolerance, relative, _artifactPath) {
|
|
259
263
|
if (collection === ABSENT || !Array.isArray(collection))
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { absence, containment, countTolerance, coversByKey, deepEquality, equality, existence, keyValueOf, ordering, regexMatch, setMembership, shape, } from './operators.js';
|
|
2
2
|
import { ABSENT } from './resolved-value.js';
|
|
3
3
|
/**
|
|
4
|
-
* AD-4's one closed introduction condition, checked per operand
|
|
5
|
-
*
|
|
4
|
+
* AD-4's one closed introduction condition, checked per operand. Two inputs
|
|
5
|
+
* decide it: what the operand resolved to, and whether the operator can answer
|
|
6
|
+
* from an empty collection.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* An operand that resolved to a present, empty array reached an evidence
|
|
9
|
+
* channel that worked and had nothing in it. That stops a `needs-a-member`
|
|
10
|
+
* operator, which would otherwise report a vacuous truth over no elements.
|
|
11
|
+
* A `total` operator has its answer: the cardinality is zero, the value is
|
|
12
|
+
* present. AD-4's own rule keeps a detected defect a detection, and that is
|
|
13
|
+
* what makes the second case a resolution.
|
|
14
|
+
*
|
|
15
|
+
* The `absent` branch does not vary. A collection-typed pointer that did not
|
|
16
|
+
* resolve is the missing page AD-4 folds into this condition to close the
|
|
17
|
+
* soft-delete fail-open, and no operator gets to read a missing collection as
|
|
18
|
+
* an empty one.
|
|
13
19
|
*/
|
|
14
|
-
function operandDenotesEmptyCollection(resolved, operand, pointerDenotesCollection) {
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
20
|
+
function operandDenotesEmptyCollection(resolved, operand, pointerDenotesCollection, totality) {
|
|
21
|
+
if (resolved !== ABSENT) {
|
|
22
|
+
if (totality === 'total')
|
|
23
|
+
return false;
|
|
24
|
+
return Array.isArray(resolved) && resolved.length === 0;
|
|
25
|
+
}
|
|
19
26
|
// Only a `{ pointer }` operand can carry a declared collection type.
|
|
20
27
|
// `{ literal }` never resolves ABSENT, and an ABSENT `{ referenceSet }`
|
|
21
28
|
// means `unresolved-reference-set` slipped past compilation, which this
|
|
@@ -38,8 +45,8 @@ function booleanResult(result) {
|
|
|
38
45
|
}
|
|
39
46
|
// Checked across every operand before any operator runs, so the interception
|
|
40
47
|
// replaces a leaf's own two-valued answer outright.
|
|
41
|
-
function anyOperandEmpty(pairs, pointerDenotesCollection) {
|
|
42
|
-
return pairs.some(({ operand, resolved }) => operandDenotesEmptyCollection(resolved, operand, pointerDenotesCollection));
|
|
48
|
+
function anyOperandEmpty(pairs, pointerDenotesCollection, totality) {
|
|
49
|
+
return pairs.some(({ operand, resolved }) => operandDenotesEmptyCollection(resolved, operand, pointerDenotesCollection, totality));
|
|
43
50
|
}
|
|
44
51
|
/** `not(insufficient-evidence)` is terminal under both polarities (AD-4). */
|
|
45
52
|
function notOf(child) {
|
|
@@ -107,10 +114,12 @@ function resolveQuantifier(op, collectionOperand, predicate, boundElement, ctx)
|
|
|
107
114
|
};
|
|
108
115
|
}
|
|
109
116
|
// Shared by the six single-operand leaves: resolve, intercept on the
|
|
110
|
-
// empty-collection condition, otherwise hand the value to the operator.
|
|
111
|
-
|
|
117
|
+
// empty-collection condition, otherwise hand the value to the operator. Each
|
|
118
|
+
// caller declares its own totality, since that is the one thing the six
|
|
119
|
+
// disagree on.
|
|
120
|
+
function resolveSingleOperand(operand, boundElement, ctx, totality, evaluate) {
|
|
112
121
|
const resolved = ctx.resolveOperand(operand, boundElement, ctx.artifactPath);
|
|
113
|
-
if (anyOperandEmpty([{ operand, resolved }], ctx.pointerDenotesCollection)) {
|
|
122
|
+
if (anyOperandEmpty([{ operand, resolved }], ctx.pointerDenotesCollection, totality)) {
|
|
114
123
|
return emptyCollectionResult();
|
|
115
124
|
}
|
|
116
125
|
return booleanResult(evaluate(resolved));
|
|
@@ -124,7 +133,7 @@ function resolveEqualityLike(operands, evaluate, boundElement, ctx) {
|
|
|
124
133
|
if (anyOperandEmpty([
|
|
125
134
|
{ operand: aOperand, resolved: a },
|
|
126
135
|
{ operand: bOperand, resolved: b },
|
|
127
|
-
], ctx.pointerDenotesCollection)) {
|
|
136
|
+
], ctx.pointerDenotesCollection, 'needs-a-member')) {
|
|
128
137
|
return emptyCollectionResult();
|
|
129
138
|
}
|
|
130
139
|
return booleanResult(evaluate(a, b, ctx.artifactPath));
|
|
@@ -212,7 +221,7 @@ function resolveContainmentNode(expression, boundElement, ctx) {
|
|
|
212
221
|
if (anyOperandEmpty([
|
|
213
222
|
{ operand: containerOperand, resolved: container },
|
|
214
223
|
{ operand: candidateOperand, resolved: candidate },
|
|
215
|
-
], ctx.pointerDenotesCollection)) {
|
|
224
|
+
], ctx.pointerDenotesCollection, 'needs-a-member')) {
|
|
216
225
|
return emptyCollectionResult();
|
|
217
226
|
}
|
|
218
227
|
// The array-narrowing guard applies only to a `{ referenceSet }` candidate.
|
|
@@ -231,16 +240,16 @@ function resolveContainmentNode(expression, boundElement, ctx) {
|
|
|
231
240
|
}
|
|
232
241
|
function resolveExistenceNode(expression, boundElement, ctx) {
|
|
233
242
|
const [operand] = expression.operands;
|
|
234
|
-
return resolveSingleOperand(operand, boundElement, ctx, (resolved) => existence(resolved, ctx.artifactPath));
|
|
243
|
+
return resolveSingleOperand(operand, boundElement, ctx, 'total', (resolved) => existence(resolved, ctx.artifactPath));
|
|
235
244
|
}
|
|
236
245
|
function resolveAbsenceNode(expression, boundElement, ctx) {
|
|
237
246
|
const [operand] = expression.operands;
|
|
238
|
-
return resolveSingleOperand(operand, boundElement, ctx, (resolved) => absence(resolved, ctx.artifactPath));
|
|
247
|
+
return resolveSingleOperand(operand, boundElement, ctx, 'total', (resolved) => absence(resolved, ctx.artifactPath));
|
|
239
248
|
}
|
|
240
249
|
function resolveRegexNode(expression, boundElement, ctx) {
|
|
241
250
|
const [operand] = expression.operands;
|
|
242
251
|
const { pattern } = expression;
|
|
243
|
-
return resolveSingleOperand(operand, boundElement, ctx, (resolved) => regexMatch(resolved, pattern, ctx.regexMatchStepBudget, ctx.artifactPath));
|
|
252
|
+
return resolveSingleOperand(operand, boundElement, ctx, 'needs-a-member', (resolved) => regexMatch(resolved, pattern, ctx.regexMatchStepBudget, ctx.artifactPath));
|
|
244
253
|
}
|
|
245
254
|
/**
|
|
246
255
|
* Reads the single declared key off each member of a `{ referenceSet }` set
|
|
@@ -308,7 +317,7 @@ function resolveSetMembershipNode(expression, boundElement, ctx) {
|
|
|
308
317
|
if (anyOperandEmpty([
|
|
309
318
|
{ operand: valueOperand, resolved: value },
|
|
310
319
|
{ operand: setOperand, resolved: resolvedSet },
|
|
311
|
-
], ctx.pointerDenotesCollection)) {
|
|
320
|
+
], ctx.pointerDenotesCollection, 'needs-a-member')) {
|
|
312
321
|
return emptyCollectionResult();
|
|
313
322
|
}
|
|
314
323
|
// The set position needs the `JsonValue[]` the schema already guarantees
|
|
@@ -326,17 +335,17 @@ function resolveSetMembershipNode(expression, boundElement, ctx) {
|
|
|
326
335
|
function resolveOrderingNode(expression, boundElement, ctx) {
|
|
327
336
|
const [operand] = expression.operands;
|
|
328
337
|
const { key, order } = expression;
|
|
329
|
-
return resolveSingleOperand(operand, boundElement, ctx, (resolved) => ordering(resolved, key, order, ctx.artifactPath));
|
|
338
|
+
return resolveSingleOperand(operand, boundElement, ctx, 'needs-a-member', (resolved) => ordering(resolved, key, order, ctx.artifactPath));
|
|
330
339
|
}
|
|
331
340
|
function resolveCountToleranceNode(expression, boundElement, ctx) {
|
|
332
341
|
const [operand] = expression.operands;
|
|
333
342
|
const { expected, tolerance, relative } = expression;
|
|
334
|
-
return resolveSingleOperand(operand, boundElement, ctx, (resolved) => countTolerance(resolved, expected, tolerance, relative, ctx.artifactPath));
|
|
343
|
+
return resolveSingleOperand(operand, boundElement, ctx, 'total', (resolved) => countTolerance(resolved, expected, tolerance, relative, ctx.artifactPath));
|
|
335
344
|
}
|
|
336
345
|
function resolveShapeNode(expression, boundElement, ctx) {
|
|
337
346
|
const [operand] = expression.operands;
|
|
338
347
|
const { descriptor } = expression;
|
|
339
|
-
return resolveSingleOperand(operand, boundElement, ctx, (resolved) => shape(resolved, descriptor, ctx.artifactPath));
|
|
348
|
+
return resolveSingleOperand(operand, boundElement, ctx, 'needs-a-member', (resolved) => shape(resolved, descriptor, ctx.artifactPath));
|
|
340
349
|
}
|
|
341
350
|
// One entry per `Expression['op']`; the mapped type below fails to compile if
|
|
342
351
|
// an op is missing or misassigned, since each key demands the handler typed
|
|
@@ -65,6 +65,62 @@ const sameFixtureState = (left, right) => {
|
|
|
65
65
|
return (digestArtifact(state(left), PREFLIGHT_ARTIFACT_PATH) ===
|
|
66
66
|
digestArtifact(state(right), PREFLIGHT_ARTIFACT_PATH));
|
|
67
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* The canonical digest of one value, or `null` where the value holds something
|
|
70
|
+
* RFC 8785 cannot serialise. `JsonValue` admits an integer outside the safe
|
|
71
|
+
* range and a lone surrogate, and a 64-bit identifier in a query parameter is
|
|
72
|
+
* ordinary, so this is reachable from a contract that parses. A verdict is what
|
|
73
|
+
* this stage owes its caller, and `null` compares equal to nothing, so a value
|
|
74
|
+
* that cannot be digested leaves the two sides distinguishable and the check
|
|
75
|
+
* still reads the leg.
|
|
76
|
+
*/
|
|
77
|
+
const digestOrNull = (value) => {
|
|
78
|
+
try {
|
|
79
|
+
return digestArtifact(value, PREFLIGHT_ARTIFACT_PATH);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof RuntimeFault)
|
|
83
|
+
return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Whether two legs issued one request and received one answer, which makes them
|
|
89
|
+
* one probe under two labels. A manifestation witness firing on such a leg is
|
|
90
|
+
* the fault leg's own manifestation read a second time, and it establishes
|
|
91
|
+
* nothing about where the defect is scoped.
|
|
92
|
+
*
|
|
93
|
+
* Both halves are required. Answers alone would drop AD-10's own worked example,
|
|
94
|
+
* two distinct nonexistent identifiers both returning 404: those legs ask
|
|
95
|
+
* different questions and are exactly the legs this check exists to read.
|
|
96
|
+
* Requests alone are what the plan can see, and identical requests can still be
|
|
97
|
+
* answered differently, which is why the comparison lives here where the
|
|
98
|
+
* answers are in hand.
|
|
99
|
+
*
|
|
100
|
+
* The answer half compares the evidence, which is everything a relation can
|
|
101
|
+
* address: two legs with equal evidence resolve one relation to one value. It
|
|
102
|
+
* carries AD-11's projected body, so a field the operation declares volatile is
|
|
103
|
+
* already out of it and a server-minted identifier stops being a difference,
|
|
104
|
+
* which is what makes the same request to a mutating operation comparable at
|
|
105
|
+
* all. The raw observation is the wrong side of this comparison for that exact
|
|
106
|
+
* reason: two writes to one collection differ on a minted id by design, and
|
|
107
|
+
* reading that as a difference puts the false failure this check just lost back
|
|
108
|
+
* one stage over.
|
|
109
|
+
*
|
|
110
|
+
* The correlation identifiers are neutralised on both sides, since they are the
|
|
111
|
+
* leg id and differ by construction. A digest that comes back `null` matches
|
|
112
|
+
* nothing, so a pair that cannot be compared stays a pair the check reads.
|
|
113
|
+
*/
|
|
114
|
+
const answeredAlike = (left, right) => {
|
|
115
|
+
const request = (state) => digestOrNull({ ...state.leg.request, probeId: '' });
|
|
116
|
+
const answer = (state) => digestOrNull({ ...state.evidence, observationId: '' });
|
|
117
|
+
const leftRequest = request(left);
|
|
118
|
+
const leftAnswer = answer(left);
|
|
119
|
+
return (leftRequest !== null &&
|
|
120
|
+
leftAnswer !== null &&
|
|
121
|
+
leftRequest === request(right) &&
|
|
122
|
+
leftAnswer === answer(right));
|
|
123
|
+
};
|
|
68
124
|
/**
|
|
69
125
|
* Resolves a manifestation witness against one leg. Returns `null` when that
|
|
70
126
|
* leg produced no observation, which the two seeded-fault rows read
|
|
@@ -175,7 +231,36 @@ export const reducePreflight = (plan, { observations }) => {
|
|
|
175
231
|
}
|
|
176
232
|
case 'seeded-faults-scoped': {
|
|
177
233
|
const { witness, defectId } = planned;
|
|
234
|
+
const fault = states.get(witness.legId);
|
|
235
|
+
// The legs that answered a different question than the fault leg's, and
|
|
236
|
+
// the legs dropped for answering the same one.
|
|
237
|
+
const examined = [];
|
|
238
|
+
const dropped = [];
|
|
178
239
|
for (const legId of planned.cleanLegIds) {
|
|
240
|
+
const state = states.get(legId);
|
|
241
|
+
if (state !== undefined &&
|
|
242
|
+
fault !== undefined &&
|
|
243
|
+
answeredAlike(state, fault)) {
|
|
244
|
+
dropped.push(legId);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
examined.push(legId);
|
|
248
|
+
}
|
|
249
|
+
// Emptiness is tested on what survived the drop. A check over no clean
|
|
250
|
+
// leg examined nothing, and a check that examined nothing has
|
|
251
|
+
// established nothing, which is the rule the `input-sensitivity` row
|
|
252
|
+
// above already runs on. Satisfied here would certify scoping from zero
|
|
253
|
+
// evidence on the three contracts least able to afford it: one whose
|
|
254
|
+
// defect names the only leg its operation has, one whose every other leg
|
|
255
|
+
// repeats the fault leg's probe, and one where the plan named legs and
|
|
256
|
+
// the drop took all of them. The note says which.
|
|
257
|
+
if (examined.length === 0) {
|
|
258
|
+
const named = dropped.map((legId) => `"${legId}"`).join(', ');
|
|
259
|
+
return check(planned.kind, witness.operationId, 'failed', dropped.length === 0
|
|
260
|
+
? `${defectId}: the operation has no leg besides the fault leg, so nothing here establishes that the defect is scoped to it`
|
|
261
|
+
: `${defectId}: every other leg of the operation issued the fault leg's own request and received its answer (${named}), so nothing here establishes that the defect is scoped to it`);
|
|
262
|
+
}
|
|
263
|
+
for (const legId of examined) {
|
|
179
264
|
const resolved = resolveAgainst(witness, states.get(legId), plan, PREFLIGHT_ARTIFACT_PATH);
|
|
180
265
|
if (resolved === 'true')
|
|
181
266
|
return check(planned.kind, witness.operationId, 'failed', `${defectId}: the manifestation witness fires on clean leg "${legId}"`);
|
|
@@ -231,7 +231,7 @@ const evidenceCommonFields = {
|
|
|
231
231
|
runId: z.string().min(1),
|
|
232
232
|
scoringVersion: Digest.describe('AD-11: computed by the scorer over the six named inputs below and never caller-supplied.'),
|
|
233
233
|
scoringVersionInputs: ScoringVersionInputs,
|
|
234
|
-
comparabilityKey: Digest.describe("AD-7's declared key
|
|
234
|
+
comparabilityKey: Digest.describe("AD-7's declared key, computed by `emit` over the scoring policy digest and the sorted list of admitted probe identifiers. AD-7 words it as the scoring policy digest plus the corpus digest restricted to the probes both results cover, and that identifier list is what the restriction resolves to: the corpus digest bytes are not an input, so two results over one scoring policy and one admitted set share a key whatever corpus each was attested against. Deliberately weaker than the scoring version, so adding a probe narrows a comparison rather than voiding every prior result."),
|
|
235
235
|
excludedProbeIds: z
|
|
236
236
|
.array(ProbeId)
|
|
237
237
|
.describe('The probes a narrowed comparison excluded, per AD-7. Empty is the ordinary case.'),
|
|
@@ -56,13 +56,16 @@ export type CommandWitnessInputs = z.infer<typeof CommandWitnessInputs>;
|
|
|
56
56
|
* interface declaring it lives in another subtree, so no discriminator is
|
|
57
57
|
* available to the schema and the agreement is a compile-time check.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
59
|
+
* All three leg shapes take it: `SensitivityWitnessLeg.inputs`,
|
|
60
|
+
* `ManifestationWitness.inputs`, and `FixtureReset.inputs`. The sensitivity leg
|
|
61
|
+
* took it from 0.3.0 and the other two followed in 1.3.0, since the transport
|
|
62
|
+
* spelling left a seeded defect against a command-line system under test
|
|
63
|
+
* unrepresentable. The reach is the port's: all three legs are issued through
|
|
64
|
+
* the environment-probe port, whose `ProbeRequest` is itself a union of
|
|
65
|
+
* `ApiProbeRequest` and `CommandProbeRequest`, and `preflight/plan.ts` admits
|
|
66
|
+
* `api` and `cli`, rejecting `web` and `mcp` under `unsupported-interface-kind`.
|
|
67
|
+
* A leg shape narrower than the port it feeds leaves a kind the adapter can run
|
|
68
|
+
* with no way to declare a leg for it.
|
|
66
69
|
*/
|
|
67
70
|
export declare const WitnessInputs: z.ZodUnion<readonly [z.ZodObject<{
|
|
68
71
|
path: z.ZodType<import("./primitives.ts").JsonObject, unknown, z.core.$ZodTypeInternals<import("./primitives.ts").JsonObject, unknown>>;
|
|
@@ -55,13 +55,16 @@ export const CommandWitnessInputs = z.strictObject({
|
|
|
55
55
|
* interface declaring it lives in another subtree, so no discriminator is
|
|
56
56
|
* available to the schema and the agreement is a compile-time check.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
58
|
+
* All three leg shapes take it: `SensitivityWitnessLeg.inputs`,
|
|
59
|
+
* `ManifestationWitness.inputs`, and `FixtureReset.inputs`. The sensitivity leg
|
|
60
|
+
* took it from 0.3.0 and the other two followed in 1.3.0, since the transport
|
|
61
|
+
* spelling left a seeded defect against a command-line system under test
|
|
62
|
+
* unrepresentable. The reach is the port's: all three legs are issued through
|
|
63
|
+
* the environment-probe port, whose `ProbeRequest` is itself a union of
|
|
64
|
+
* `ApiProbeRequest` and `CommandProbeRequest`, and `preflight/plan.ts` admits
|
|
65
|
+
* `api` and `cli`, rejecting `web` and `mcp` under `unsupported-interface-kind`.
|
|
66
|
+
* A leg shape narrower than the port it feeds leaves a kind the adapter can run
|
|
67
|
+
* with no way to declare a leg for it.
|
|
65
68
|
*/
|
|
66
69
|
export const WitnessInputs = z.union([ApiWitnessInputs, CommandWitnessInputs]);
|
|
67
70
|
/**
|
|
@@ -35,7 +35,7 @@ import type { Probe } from '../schemas/probe.ts';
|
|
|
35
35
|
import type { ScoringPolicy } from '../schemas/scoring-policy.ts';
|
|
36
36
|
import type { ScoreStage } from '../stage-contracts.ts';
|
|
37
37
|
import type { ContractAssessment, LadderResolution, ProductionAssessment } from './ladder.ts';
|
|
38
|
-
import { type SealedProbeSet } from './qualification.ts';
|
|
38
|
+
import { type QualificationResult, type SealedProbeSet } from './qualification.ts';
|
|
39
39
|
import { type TrialSetResult } from './reduce-trials.ts';
|
|
40
40
|
/**
|
|
41
41
|
* `score`'s owned product: the assessment/ladder pairing AD-24 names, "the
|
|
@@ -56,6 +56,13 @@ export type ScoredOutcomesAndVerdict = {
|
|
|
56
56
|
readonly policy: ScoringPolicy;
|
|
57
57
|
readonly probe: Probe;
|
|
58
58
|
readonly sealedProbes: SealedProbeSet;
|
|
59
|
+
/**
|
|
60
|
+
* The one probe's own qualification result, lifted out of `sealedProbes`
|
|
61
|
+
* so a caller reading a run's product holds the closed reason set that
|
|
62
|
+
* decided it. `emit` mints no field from this: AD-9's reasons stay off the
|
|
63
|
+
* `EvidenceArtifact` and travel the return path.
|
|
64
|
+
*/
|
|
65
|
+
readonly probeQualification: QualificationResult;
|
|
59
66
|
/** this probe's own AD-7 trial-set fold, keyed by `emit` under `probe.probeId` to build the strength vector. */
|
|
60
67
|
readonly trialSetResult: TrialSetResult;
|
|
61
68
|
/** the full `EvidenceArtifact.outcomes` shape, a parallel array to `ScoredOutcome[]` above: `ScoredOutcome` carries `resolution` but not `disposition` or the raw `CheckResolution` tree this shape needs, so the two are not reconstructible from one another. */
|
package/dist/core/score/score.js
CHANGED
|
@@ -37,7 +37,7 @@ import { buildPlanIndex } from '../seal/plan-index.js';
|
|
|
37
37
|
import { resolveCapturedBindings, selectWithBindings } from './bindings.js';
|
|
38
38
|
import { resolveContractVerdict, resolveProductionVerdict } from './ladder.js';
|
|
39
39
|
import { FINDING_BUCKETS, resolveOutcome, uncitedDefectFindingGaps, uncitedFindingIds, } from './outcome.js';
|
|
40
|
-
import { resolveHomeOperation, sealProbeSet, } from './qualification.js';
|
|
40
|
+
import { qualifyProbe, resolveHomeOperation, sealProbeSet, } from './qualification.js';
|
|
41
41
|
import { reduceTrialSet, TRIAL_VOTE_STATES, } from './reduce-trials.js';
|
|
42
42
|
import { mapFindings, matchProbeWitness } from './witness.js';
|
|
43
43
|
/** A mutable copy of the two record-shaped arrays every witness/finding function this stage calls wants, since `ValidatedObservations`' own arrays are `readonly`. */
|
|
@@ -245,8 +245,17 @@ export const score = (contract, trials, probe, preflightVerdict, policy, waiver,
|
|
|
245
245
|
? null
|
|
246
246
|
: resolveHomeOperation(candidate.defectSignature, contract.permittedInterfaces);
|
|
247
247
|
const sealedProbes = sealProbeSet([probe], homeOperationOf);
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
// `sealProbeSet` over a one-probe array puts that probe in exactly one
|
|
249
|
+
// bucket, so the third branch is unreachable. It re-runs the gate rather
|
|
250
|
+
// than composing a result here: `qualifyProbe` holds
|
|
251
|
+
// `qualified === (failures.length === 0)`, and a hand-built
|
|
252
|
+
// `{ qualified: false, failures: [] }` would hand a consumer a rejection
|
|
253
|
+
// with no code to route on, which is the silent state this field removes.
|
|
254
|
+
// The gate is pure, so a second call over the same probe answers the same.
|
|
255
|
+
const probeQualification = sealedProbes.admitted[0]?.result ??
|
|
256
|
+
sealedProbes.rejected[0]?.result ??
|
|
257
|
+
qualifyProbe(probe, homeOperationOf(probe));
|
|
258
|
+
const probeQualified = probeQualification.qualified;
|
|
250
259
|
const signedProbe = signedProbeOf(probe);
|
|
251
260
|
const designatedOracleId = designatedOracleIdOf(probe, contract);
|
|
252
261
|
const probeSigned = !probe.expectedClean && probe.defectSignature !== null;
|
|
@@ -578,6 +587,7 @@ export const score = (contract, trials, probe, preflightVerdict, policy, waiver,
|
|
|
578
587
|
policy,
|
|
579
588
|
probe,
|
|
580
589
|
sealedProbes,
|
|
590
|
+
probeQualification,
|
|
581
591
|
trialSetResult: reduced,
|
|
582
592
|
outcomes,
|
|
583
593
|
uncitedFindings,
|
|
@@ -600,6 +610,7 @@ export const score = (contract, trials, probe, preflightVerdict, policy, waiver,
|
|
|
600
610
|
policy,
|
|
601
611
|
probe,
|
|
602
612
|
sealedProbes,
|
|
613
|
+
probeQualification,
|
|
603
614
|
trialSetResult: reduced,
|
|
604
615
|
outcomes,
|
|
605
616
|
uncitedFindings,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,4 +12,4 @@ export type { ScoringPolicy } from './core/schemas/scoring-policy.ts';
|
|
|
12
12
|
export type { SealedEvaluatorBrief } from './core/schemas/sealed-evaluator-brief.ts';
|
|
13
13
|
export type { SealedRunRecord } from './core/schemas/sealed-run-record.ts';
|
|
14
14
|
export type { FixtureReset, ManifestationWitness, SensitivityWitness, SensitivityWitnessLeg, WitnessChannel, WitnessInputs, } from './core/schemas/sensitivity-witness.ts';
|
|
15
|
-
export declare const VERSION = "1.
|
|
15
|
+
export declare const VERSION = "1.4.1";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"comparabilityKey": {
|
|
87
87
|
"type": "string",
|
|
88
88
|
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
89
|
-
"description": "AD-7's declared key
|
|
89
|
+
"description": "AD-7's declared key, computed by `emit` over the scoring policy digest and the sorted list of admitted probe identifiers. AD-7 words it as the scoring policy digest plus the corpus digest restricted to the probes both results cover, and that identifier list is what the restriction resolves to: the corpus digest bytes are not an input, so two results over one scoring policy and one admitted set share a key whatever corpus each was attested against. Deliberately weaker than the scoring version, so adding a probe narrows a comparison rather than voiding every prior result."
|
|
90
90
|
},
|
|
91
91
|
"excludedProbeIds": {
|
|
92
92
|
"type": "array",
|
|
@@ -666,7 +666,7 @@
|
|
|
666
666
|
"comparabilityKey": {
|
|
667
667
|
"type": "string",
|
|
668
668
|
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
669
|
-
"description": "AD-7's declared key
|
|
669
|
+
"description": "AD-7's declared key, computed by `emit` over the scoring policy digest and the sorted list of admitted probe identifiers. AD-7 words it as the scoring policy digest plus the corpus digest restricted to the probes both results cover, and that identifier list is what the restriction resolves to: the corpus digest bytes are not an input, so two results over one scoring policy and one admitted set share a key whatever corpus each was attested against. Deliberately weaker than the scoring version, so adding a probe narrows a comparison rather than voiding every prior result."
|
|
670
670
|
},
|
|
671
671
|
"excludedProbeIds": {
|
|
672
672
|
"type": "array",
|