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.
@@ -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 { artifact: null, ladder: scored.ladder };
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 { artifact, ladder: scored.ladder };
189
+ return {
190
+ artifact,
191
+ ladder: scored.ladder,
192
+ qualification: scored.probeQualification,
193
+ };
186
194
  }
@@ -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.
@@ -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; the
63
- * resolver in `resolution.ts` intercepts before this runs on a genuinely
64
- * empty collection. The allowed deviation is compared unrounded: `actual` is
65
- * an integer, so `<=` against a fractional deviation is already exact, and
66
- * rounding either direction would move the declared boundary.
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; the
253
- * resolver in `resolution.ts` intercepts before this runs on a genuinely
254
- * empty collection. The allowed deviation is compared unrounded: `actual` is
255
- * an integer, so `<=` against a fractional deviation is already exact, and
256
- * rounding either direction would move the declared boundary.
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 and applied
5
- * uniformly: the resolved value and its operand are the only inputs.
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
- * Two permanent consequences of that uniform reading, the same class as a
8
- * `{ literal: [] }` operand tripping it: a `count-tolerance` node asserting
9
- * `expected: 0` over a genuinely empty collection can never resolve `true`,
10
- * because this interception fires first; and `existence` over a pointer
11
- * resolving to a present-but-empty array resolves `insufficient-evidence`, not
12
- * `true`, even though `existence` only asks about presence.
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 (Array.isArray(resolved) && resolved.length === 0)
16
- return true;
17
- if (resolved !== ABSENT)
18
- return false;
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
- function resolveSingleOperand(operand, boundElement, ctx, evaluate) {
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: the scoring policy digest plus the corpus digest restricted to the probes both results cover. Deliberately weaker than the scoring version, so adding a probe narrows a comparison rather than voiding every prior result."),
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
- * Only the sensitivity leg takes the union. `ManifestationWitness` and
60
- * `FixtureReset` keep the transport spelling, which keeps the probe artifact
61
- * byte-identical and keeps this shape's widening inside the eval contract's own
62
- * version bump. That is truthful rather than merely convenient: both of those
63
- * legs are issued through the environment-probe port, whose `ProbeRequest`
64
- * carries a method, a path template, and the four transport channels, and
65
- * pre-flight rejects a non-api interface for exactly that reason.
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
- * Only the sensitivity leg takes the union. `ManifestationWitness` and
59
- * `FixtureReset` keep the transport spelling, which keeps the probe artifact
60
- * byte-identical and keeps this shape's widening inside the eval contract's own
61
- * version bump. That is truthful rather than merely convenient: both of those
62
- * legs are issued through the environment-probe port, whose `ProbeRequest`
63
- * carries a method, a path template, and the four transport channels, and
64
- * pre-flight rejects a non-api interface for exactly that reason.
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. */
@@ -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
- const qualifiedEntry = sealedProbes.admitted[0] ?? sealedProbes.rejected[0];
249
- const probeQualified = qualifiedEntry === undefined ? false : qualifiedEntry.result.qualified;
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.3.0";
15
+ export declare const VERSION = "1.4.1";
package/dist/index.js CHANGED
@@ -19,4 +19,4 @@
19
19
  // subpath, where AD-37 puts the conformance definition an adapter author
20
20
  // reads; the reference adapters stay at `eval-quality/adapters`.
21
21
  export * from './application/index.js';
22
- export const VERSION = '1.3.0';
22
+ export const VERSION = '1.4.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eval-quality",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Compile disciplined Behavioral Evaluation Contracts and score their ability to catch known defects.",
5
5
  "author": "Murat Ozcan",
6
6
  "license": "Apache-2.0",
@@ -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: the scoring policy digest plus the corpus digest restricted to the probes both results cover. Deliberately weaker than the scoring version, so adding a probe narrows a comparison rather than voiding every prior result."
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: the scoring policy digest plus the corpus digest restricted to the probes both results cover. Deliberately weaker than the scoring version, so adding a probe narrows a comparison rather than voiding every prior result."
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",