eval-quality 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -67
- package/corpus/dev/README.md +17 -11
- package/corpus/dev/compile-seal-example/brief.json +1 -1
- package/corpus/dev/compile-seal-example/contract.json +1 -1
- package/corpus/dev/contracts/absent-collection-locations.json +1 -1
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
- package/corpus/dev/contracts/absent-success-indicator.json +1 -1
- package/corpus/dev/contracts/empty-channel-roles.json +1 -1
- package/corpus/dev/contracts/empty-collection-locations.json +1 -1
- package/corpus/dev/contracts/empty-request-shapes.json +1 -1
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
- package/corpus/dev/contracts/no-operation-inventory.json +1 -1
- package/corpus/dev/contracts/no-read-back-relation.json +1 -1
- package/corpus/dev/contracts/no-state-change-marker.json +1 -1
- package/corpus/dev/contracts/no-type-violating-step.json +1 -1
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/satisfied-declarations.json +1 -1
- package/corpus/dev/contracts/single-required-response-key.json +1 -1
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
- package/corpus/dev/index.json +1 -1
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +1 -0
- package/dist/application/score.d.ts +48 -0
- package/dist/application/score.js +186 -0
- package/dist/cli/arguments.d.ts +7 -3
- package/dist/cli/arguments.js +73 -5
- package/dist/cli/exit-codes.d.ts +6 -2
- package/dist/cli/exit-codes.js +8 -9
- package/dist/cli/main.js +9 -0
- package/dist/cli/render.d.ts +1 -1
- package/dist/cli/render.js +6 -4
- package/dist/cli/run.d.ts +13 -3
- package/dist/cli/run.js +136 -16
- package/dist/core/canonical/canonicalize.d.ts +12 -0
- package/dist/core/canonical/canonicalize.js +18 -4
- package/dist/core/compile/bindings.d.ts +70 -0
- package/dist/core/compile/bindings.js +317 -0
- package/dist/core/compile/compile.d.ts +5 -0
- package/dist/core/compile/compile.js +9 -0
- package/dist/core/compile/expression-legality.d.ts +49 -0
- package/dist/core/compile/expression-legality.js +105 -31
- package/dist/core/compile/forbidden-inputs.d.ts +8 -1
- package/dist/core/compile/forbidden-inputs.js +16 -3
- package/dist/core/compile/interface-inventory.d.ts +23 -1
- package/dist/core/compile/interface-inventory.js +32 -3
- package/dist/core/compile/reachability.d.ts +18 -1
- package/dist/core/compile/reachability.js +46 -1
- package/dist/core/compile/sensitivity-witness.d.ts +1 -1
- package/dist/core/compile/sensitivity-witness.js +1 -1
- package/dist/core/emit/emit.d.ts +9 -0
- package/dist/core/emit/emit.js +214 -0
- package/dist/core/emit/private-artifact-digest.d.ts +11 -0
- package/dist/core/emit/private-artifact-digest.js +31 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
- package/dist/core/evaluate/evidence-resolution.js +4 -1
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +4 -2
- package/dist/core/ingest/conditions.d.ts +201 -0
- package/dist/core/ingest/conditions.js +48 -0
- package/dist/core/ingest/index.d.ts +13 -0
- package/dist/core/ingest/index.js +11 -0
- package/dist/core/ingest/ingest.d.ts +68 -0
- package/dist/core/ingest/ingest.js +310 -0
- package/dist/core/lineage/stage-table.d.ts +21 -4
- package/dist/core/lineage/stage-table.js +35 -3
- package/dist/core/preflight/witness-evidence.js +10 -0
- package/dist/core/schemas/artifact.d.ts +385 -14
- package/dist/core/schemas/constraint-ledger.js +40 -0
- package/dist/core/schemas/defect-signature.d.ts +273 -0
- package/dist/core/schemas/defect-signature.js +127 -0
- package/dist/core/schemas/eval-contract.d.ts +33 -0
- package/dist/core/schemas/eval-contract.js +14 -0
- package/dist/core/schemas/evidence-artifact.d.ts +99 -19
- package/dist/core/schemas/evidence-artifact.js +64 -8
- package/dist/core/schemas/plan.d.ts +112 -0
- package/dist/core/schemas/plan.js +67 -2
- package/dist/core/schemas/probe-qualification.d.ts +168 -0
- package/dist/core/schemas/probe-qualification.js +106 -0
- package/dist/core/schemas/probe.d.ts +312 -0
- package/dist/core/schemas/probe.js +6 -2
- package/dist/core/schemas/scoring-policy.d.ts +1 -0
- package/dist/core/schemas/scoring-policy.js +6 -1
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
- package/dist/core/schemas/sealed-run-record.d.ts +30 -3
- package/dist/core/schemas/sealed-run-record.js +37 -5
- package/dist/core/score/binding-order.d.ts +31 -0
- package/dist/core/score/binding-order.js +88 -0
- package/dist/core/score/bindings.d.ts +93 -0
- package/dist/core/score/bindings.js +297 -0
- package/dist/core/score/ladder-table.d.ts +23 -0
- package/dist/core/score/ladder-table.js +109 -0
- package/dist/core/score/ladder.d.ts +229 -0
- package/dist/core/score/ladder.js +464 -0
- package/dist/core/score/mode-agreement.d.ts +30 -0
- package/dist/core/score/mode-agreement.js +16 -0
- package/dist/core/score/outcome-table.d.ts +17 -0
- package/dist/core/score/outcome-table.js +172 -0
- package/dist/core/score/outcome.d.ts +466 -0
- package/dist/core/score/outcome.js +562 -0
- package/dist/core/score/qualification.d.ts +83 -0
- package/dist/core/score/qualification.js +615 -0
- package/dist/core/score/quotation.d.ts +79 -0
- package/dist/core/score/quotation.js +201 -0
- package/dist/core/score/reduce-trials.d.ts +80 -0
- package/dist/core/score/reduce-trials.js +90 -0
- package/dist/core/score/score.d.ts +72 -0
- package/dist/core/score/score.js +581 -0
- package/dist/core/score/selection.d.ts +80 -0
- package/dist/core/score/selection.js +98 -0
- package/dist/core/score/strength.d.ts +49 -0
- package/dist/core/score/strength.js +193 -0
- package/dist/core/score/witness.d.ts +117 -0
- package/dist/core/score/witness.js +315 -0
- package/dist/core/seal/derived-reference.d.ts +1 -1
- package/dist/core/seal/derived-reference.js +236 -28
- package/dist/core/seal/seal.js +8 -3
- package/dist/core/stage-contracts.d.ts +62 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/package.json +12 -3
- package/schemas/eval-contract.schema.json +100 -2
- package/schemas/evidence-artifact.schema.json +96 -9
- package/schemas/probe.schema.json +508 -3
- package/schemas/scoring-policy.schema.json +8 -1
- package/schemas/sealed-evaluator-brief.schema.json +11 -1
- package/schemas/sealed-run-record.schema.json +19 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owed item 2's fix: a step selects observations by `operationId`, ordered by
|
|
3
|
+
* the record's monotonic `sequence` (ADR-006 forbids array position as
|
|
4
|
+
* ordering). `selectObservations` reports a several-match ambiguity as data;
|
|
5
|
+
* no first-match or last-match convention picks a winner. Pure and total:
|
|
6
|
+
* every input produces a defined result, nothing throws, and no AD-6 outcome
|
|
7
|
+
* state is assigned. Assigning outcome state is AD-33's reference decision
|
|
8
|
+
* procedure's job.
|
|
9
|
+
*
|
|
10
|
+
* `resolveTemporalAnchor` is a separate, narrower resolution built on top:
|
|
11
|
+
* picking a concrete point in time for an `any`-cardinality temporal anchor
|
|
12
|
+
* with several matches, by lowest sequence. The policy above still holds
|
|
13
|
+
* here: a single-valued cardinality's several-match ambiguity still comes
|
|
14
|
+
* back unresolved from `resolveTemporalAnchor` too.
|
|
15
|
+
*
|
|
16
|
+
* No stage claims this module yet; `stage-table.ts`'s `score` row stays
|
|
17
|
+
* `module: null` throughout.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Matches `step.operationId` against every observation's `operationId`,
|
|
21
|
+
* ordered by `sequence` ascending. Reads no other field of `step`: whether
|
|
22
|
+
* `several` is the named ambiguity condition or a legitimate `any`-cardinality
|
|
23
|
+
* match is a fact about the step's declared cardinality, decided by whoever
|
|
24
|
+
* reads this result, not by this function.
|
|
25
|
+
*
|
|
26
|
+
* Sorts a copy; the input `observations` array is never mutated, and its own
|
|
27
|
+
* order is never read as meaning anything (NFR9: a permutation of the same
|
|
28
|
+
* observations yields byte-identical `matchedObservationIds`). The schema
|
|
29
|
+
* layer enforces per-record `sequence` uniqueness, so a real `SealedRunRecord`
|
|
30
|
+
* never presents a tie. `observationId` is the secondary sort key regardless:
|
|
31
|
+
* this function's own permutation invariance holds even against a hand-built
|
|
32
|
+
* or malformed `observations` array carrying a duplicate or non-finite
|
|
33
|
+
* `sequence`, decided by this comparator alone.
|
|
34
|
+
*
|
|
35
|
+
* Matching on `operationId` alone, ignoring `step.inputBinding`, is a
|
|
36
|
+
* deliberate scope boundary that stays: candidate-tuple resolution lives in
|
|
37
|
+
* `selectWithBindings` (`score/bindings.ts`), which wraps this function and
|
|
38
|
+
* filters its matches against the step's own resolved bindings. Splitting them
|
|
39
|
+
* keeps this function's permutation guarantee provable on its own.
|
|
40
|
+
*
|
|
41
|
+
* `tests/seal/fixtures.ts`'s `irreducibleCollisionPair` was once cited here as
|
|
42
|
+
* a case "distinguishable only by input binding". It is not: its two steps
|
|
43
|
+
* bind nothing in any channel, so a filter over zero bindings separates
|
|
44
|
+
* nothing and both stay `several` even with `selectWithBindings`. The pair
|
|
45
|
+
* that separates is `literalCollisionPair`, whose two steps bind one key to
|
|
46
|
+
* two different literals.
|
|
47
|
+
*/
|
|
48
|
+
export function selectObservations(step, observations) {
|
|
49
|
+
const matched = observations
|
|
50
|
+
.filter((observation) => observation.operationId === step.operationId)
|
|
51
|
+
.sort((a, b) => a.sequence - b.sequence || (a.observationId < b.observationId ? -1 : 1));
|
|
52
|
+
const matchedObservationIds = matched.map((observation) => observation.observationId);
|
|
53
|
+
const result = matchedObservationIds.length === 0
|
|
54
|
+
? 'none'
|
|
55
|
+
: matchedObservationIds.length === 1
|
|
56
|
+
? 'one'
|
|
57
|
+
: 'several';
|
|
58
|
+
return { result, matchedObservationIds };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolves an `after` temporal clause to the single observation it denotes,
|
|
62
|
+
* built directly from `selectObservations`'s own result over the anchor.
|
|
63
|
+
*
|
|
64
|
+
* Takes the anchor step itself, already resolved from the dependent step's
|
|
65
|
+
* `after` identifier (a one-line lookup against whichever declared plan the
|
|
66
|
+
* caller holds). A step whose clause is `null`, or whose clause names a step
|
|
67
|
+
* the plan does not declare (AD-39's permissive dangling reference), names no
|
|
68
|
+
* anchor and never reaches this function.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveTemporalAnchor(anchorStep, observations) {
|
|
71
|
+
const selection = selectObservations(anchorStep, observations);
|
|
72
|
+
if (selection.result === 'one') {
|
|
73
|
+
return {
|
|
74
|
+
resolved: true,
|
|
75
|
+
// `result === 'one'` guarantees exactly one member.
|
|
76
|
+
observationId: selection
|
|
77
|
+
.matchedObservationIds[0],
|
|
78
|
+
matchedObservationIds: selection.matchedObservationIds,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (selection.result === 'several' && anchorStep.cardinality === 'any') {
|
|
82
|
+
// Ascending-`sequence` order already holds the lowest-sequence match first.
|
|
83
|
+
return {
|
|
84
|
+
resolved: true,
|
|
85
|
+
observationId: selection
|
|
86
|
+
.matchedObservationIds[0],
|
|
87
|
+
matchedObservationIds: selection.matchedObservationIds,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// `none`, or `several` under a single-valued cardinality: no single
|
|
91
|
+
// observation resolves. Reported as data; routing this to a verdict rung
|
|
92
|
+
// is later work.
|
|
93
|
+
return {
|
|
94
|
+
resolved: false,
|
|
95
|
+
result: selection.result,
|
|
96
|
+
matchedObservationIds: selection.matchedObservationIds,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-7's rate vector and its four-valued dominance relation.
|
|
3
|
+
*
|
|
4
|
+
* `buildStrengthVector` is a pure aggregation over a qualified probe set and
|
|
5
|
+
* the trial-set reducer's per-probe results: unweighted, per probe class,
|
|
6
|
+
* unique qualified probe identifiers over unique qualified probe identifiers
|
|
7
|
+
* exercised, with canary probes and clean controls excluded regardless of
|
|
8
|
+
* class or trial outcome. `compareDominance` takes two already-computed
|
|
9
|
+
* results and never re-derives a vector, reads a port, a corpus, or a clock;
|
|
10
|
+
* comparability is checked first, and the severity-floor override can only
|
|
11
|
+
* push the relation toward `incomparable`.
|
|
12
|
+
*/
|
|
13
|
+
import { type Severity } from '../schemas/eval-contract.ts';
|
|
14
|
+
import type { Outcome, Strength, StrengthVector } from '../schemas/evidence-artifact.ts';
|
|
15
|
+
import type { QualifiedProbe } from './qualification.ts';
|
|
16
|
+
import type { TrialSetResult } from './reduce-trials.ts';
|
|
17
|
+
export declare const DOMINANCE_RELATIONS: readonly ['a-dominates-b', 'b-dominates-a', 'equivalent', 'incomparable'];
|
|
18
|
+
export type DominanceRelationValue = (typeof DOMINANCE_RELATIONS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* The slice the dominance comparator reads: the aggregate `Strength` plus the
|
|
21
|
+
* per-probe `outcomes` array the severity-floor override needs, since that
|
|
22
|
+
* identity is lost once probes are aggregated into `ClassStrength` counts,
|
|
23
|
+
* and the `comparabilityKey` the comparator checks before comparing anything
|
|
24
|
+
* else. Every field already lives on `EvidenceArtifact`; this is the read
|
|
25
|
+
* projection the comparator needs from it, not a new artifact shape.
|
|
26
|
+
*/
|
|
27
|
+
export type ComparableResult = {
|
|
28
|
+
readonly outcomes: readonly Outcome[];
|
|
29
|
+
readonly strength: Strength;
|
|
30
|
+
readonly comparabilityKey: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* AD-7's rate vector: per probe class, the catch rate over unique qualified
|
|
34
|
+
* probe identifiers, with raw counts alongside. `admitted` carries each
|
|
35
|
+
* probe's identifier once by construction, so grouping by class needs no
|
|
36
|
+
* deduplication of its own.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildStrengthVector(admitted: readonly QualifiedProbe[], results: ReadonlyMap<string, TrialSetResult>): StrengthVector;
|
|
39
|
+
/**
|
|
40
|
+
* AD-7's four-valued dominance relation. `comparabilityKey` and each side's
|
|
41
|
+
* own `strength.comparable` are checked before any component-wise comparison
|
|
42
|
+
* runs: a key mismatch means the two runs are not measuring a shared probe
|
|
43
|
+
* set, and `comparable: false` means AD-21 already marked that one side's own
|
|
44
|
+
* vector as thinner than the policy's declared minimum, so a `caught`/`rate`
|
|
45
|
+
* on it is not fit to decide a comparison either way. The severity-floor
|
|
46
|
+
* override runs only against the side the raw comparison favoured, and only
|
|
47
|
+
* ever downgrades that result to `incomparable`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function compareDominance(a: ComparableResult, b: ComparableResult, severityFloor: Severity): DominanceRelationValue;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-7's rate vector and its four-valued dominance relation.
|
|
3
|
+
*
|
|
4
|
+
* `buildStrengthVector` is a pure aggregation over a qualified probe set and
|
|
5
|
+
* the trial-set reducer's per-probe results: unweighted, per probe class,
|
|
6
|
+
* unique qualified probe identifiers over unique qualified probe identifiers
|
|
7
|
+
* exercised, with canary probes and clean controls excluded regardless of
|
|
8
|
+
* class or trial outcome. `compareDominance` takes two already-computed
|
|
9
|
+
* results and never re-derives a vector, reads a port, a corpus, or a clock;
|
|
10
|
+
* comparability is checked first, and the severity-floor override can only
|
|
11
|
+
* push the relation toward `incomparable`.
|
|
12
|
+
*/
|
|
13
|
+
import { SEVERITY_LEVELS } from '../schemas/eval-contract.js';
|
|
14
|
+
export const DOMINANCE_RELATIONS = [
|
|
15
|
+
'a-dominates-b',
|
|
16
|
+
'b-dominates-a',
|
|
17
|
+
'equivalent',
|
|
18
|
+
'incomparable',
|
|
19
|
+
];
|
|
20
|
+
const STRENGTH_VECTOR_CLASSES = [
|
|
21
|
+
'defect',
|
|
22
|
+
'gameability',
|
|
23
|
+
'zero-action',
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* `admitted`, after excluding `canary` and every `expectedClean: true` probe:
|
|
27
|
+
* AD-7's "canary probes and clean controls never enter the vector" applies
|
|
28
|
+
* regardless of class or trial outcome, and a canary carries `expectedClean:
|
|
29
|
+
* false` on its own schema branch, so both conditions are checked.
|
|
30
|
+
*/
|
|
31
|
+
const vectorEligible = (admitted) => admitted.filter(({ probe }) => probe.probeClass !== 'canary' && !probe.expectedClean);
|
|
32
|
+
/**
|
|
33
|
+
* One class's aggregate, or `null` when the eligible set admits no probe of
|
|
34
|
+
* that class. A probe with no `TrialSetResult`, or one that is `exercised:
|
|
35
|
+
* false`, contributes to neither `caught` nor `exercised`, matching the
|
|
36
|
+
* reducer's own "zero valid trials excludes a probe entirely" rule. A class
|
|
37
|
+
* with admitted probes but zero exercised ones is still a present
|
|
38
|
+
* `ClassStrength` of `{ caught: 0, exercised: 0, rate: null }`, never a
|
|
39
|
+
* `null` class: `rate`'s nullability exists specifically for that case, and
|
|
40
|
+
* collapsing the whole class to `null` would make it unobservable.
|
|
41
|
+
*/
|
|
42
|
+
const classStrengthOf = (probesInClass, results) => {
|
|
43
|
+
if (probesInClass.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
let exercised = 0;
|
|
46
|
+
let caught = 0;
|
|
47
|
+
for (const { probe } of probesInClass) {
|
|
48
|
+
const result = results.get(probe.probeId);
|
|
49
|
+
if (result === undefined || !result.exercised)
|
|
50
|
+
continue;
|
|
51
|
+
exercised += 1;
|
|
52
|
+
if (result.caught)
|
|
53
|
+
caught += 1;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
exercised,
|
|
57
|
+
caught,
|
|
58
|
+
rate: exercised === 0 ? null : caught / exercised,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* AD-7's rate vector: per probe class, the catch rate over unique qualified
|
|
63
|
+
* probe identifiers, with raw counts alongside. `admitted` carries each
|
|
64
|
+
* probe's identifier once by construction, so grouping by class needs no
|
|
65
|
+
* deduplication of its own.
|
|
66
|
+
*/
|
|
67
|
+
export function buildStrengthVector(admitted, results) {
|
|
68
|
+
const eligible = vectorEligible(admitted);
|
|
69
|
+
const byClass = Object.fromEntries(STRENGTH_VECTOR_CLASSES.map((probeClass) => [
|
|
70
|
+
probeClass,
|
|
71
|
+
classStrengthOf(eligible.filter(({ probe }) => probe.probeClass === probeClass), results),
|
|
72
|
+
]));
|
|
73
|
+
return {
|
|
74
|
+
defect: byClass.defect,
|
|
75
|
+
gameability: byClass.gameability,
|
|
76
|
+
'zero-action': byClass['zero-action'],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A class contributes to the comparison only when it is a non-null
|
|
81
|
+
* `ClassStrength` on both sides and both sides' `rate` is also non-null; a
|
|
82
|
+
* class absent on either side, or present with a `null` rate on either side,
|
|
83
|
+
* carries no comparative evidence and is skipped exactly alike. `equivalent`
|
|
84
|
+
* compares `caught` and `exercised` rather than the derived `rate`, avoiding
|
|
85
|
+
* a floating-point equality check.
|
|
86
|
+
*
|
|
87
|
+
* A class whose two sides tie on `rate` while disagreeing on `caught` or
|
|
88
|
+
* `exercised` contributes to the comparison, blocks `equivalent` (the counts
|
|
89
|
+
* are not equal), and hands neither side a win (neither `rate` is strictly
|
|
90
|
+
* greater). That third possibility has no named outcome of its own in AD-7's
|
|
91
|
+
* three stated cases, and `incomparable` is where a tied vector with no
|
|
92
|
+
* winner on either side belongs: the same value the "no class contributes at
|
|
93
|
+
* all" case already returns.
|
|
94
|
+
*/
|
|
95
|
+
function componentComparison(a, b) {
|
|
96
|
+
let aWinsAClass = false;
|
|
97
|
+
let bWinsAClass = false;
|
|
98
|
+
let everyContributingClassEqual = true;
|
|
99
|
+
let contributingClasses = 0;
|
|
100
|
+
for (const key of STRENGTH_VECTOR_CLASSES) {
|
|
101
|
+
const left = a[key];
|
|
102
|
+
const right = b[key];
|
|
103
|
+
if (left === null || right === null)
|
|
104
|
+
continue;
|
|
105
|
+
if (left.rate === null || right.rate === null)
|
|
106
|
+
continue;
|
|
107
|
+
contributingClasses += 1;
|
|
108
|
+
if (left.caught !== right.caught || left.exercised !== right.exercised) {
|
|
109
|
+
everyContributingClassEqual = false;
|
|
110
|
+
}
|
|
111
|
+
if (left.rate > right.rate)
|
|
112
|
+
aWinsAClass = true;
|
|
113
|
+
if (right.rate > left.rate)
|
|
114
|
+
bWinsAClass = true;
|
|
115
|
+
}
|
|
116
|
+
if (contributingClasses === 0)
|
|
117
|
+
return 'incomparable';
|
|
118
|
+
if (everyContributingClassEqual)
|
|
119
|
+
return 'equivalent';
|
|
120
|
+
if (aWinsAClass && !bWinsAClass)
|
|
121
|
+
return 'a-dominates-b';
|
|
122
|
+
if (bWinsAClass && !aWinsAClass)
|
|
123
|
+
return 'b-dominates-a';
|
|
124
|
+
return 'incomparable';
|
|
125
|
+
}
|
|
126
|
+
const atOrAboveFloor = (severity, floor) => SEVERITY_LEVELS.indexOf(severity) >= SEVERITY_LEVELS.indexOf(floor);
|
|
127
|
+
/**
|
|
128
|
+
* Keyed by the first outcome carrying each `probeId`, not the last: two
|
|
129
|
+
* `Outcome` entries sharing one `probeId` is itself a defect somewhere
|
|
130
|
+
* upstream (this map has no way to tell which entry is the real one), and a
|
|
131
|
+
* silent last-write-wins overwrite would drop the earlier entry from the
|
|
132
|
+
* severity-floor scan below with no trace it was ever there.
|
|
133
|
+
*/
|
|
134
|
+
const outcomesByProbeId = (outcomes) => {
|
|
135
|
+
const byProbeId = new Map();
|
|
136
|
+
for (const outcome of outcomes) {
|
|
137
|
+
if (outcome.probeId === null)
|
|
138
|
+
continue;
|
|
139
|
+
if (byProbeId.has(outcome.probeId))
|
|
140
|
+
continue;
|
|
141
|
+
byProbeId.set(outcome.probeId, outcome);
|
|
142
|
+
}
|
|
143
|
+
return byProbeId;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Whether `favored` failed to catch a probe that `other` caught at or above
|
|
147
|
+
* `severityFloor`: the condition that disqualifies `favored` from dominating,
|
|
148
|
+
* per AD-7's "a contract that missed a behaviour at or above the scoring
|
|
149
|
+
* policy's severity floor never dominates one that caught it, regardless of
|
|
150
|
+
* the rest of the vector". An outcome with `probeId: null` is not tied to any
|
|
151
|
+
* probe and is outside the vector entirely, so both sides skip it.
|
|
152
|
+
*/
|
|
153
|
+
function favoredMissesWhatOtherCaught(favored, other, severityFloor) {
|
|
154
|
+
const favoredByProbeId = outcomesByProbeId(favored.outcomes);
|
|
155
|
+
const otherByProbeId = outcomesByProbeId(other.outcomes);
|
|
156
|
+
for (const [probeId, otherOutcome] of otherByProbeId) {
|
|
157
|
+
if (otherOutcome.state !== 'caught')
|
|
158
|
+
continue;
|
|
159
|
+
if (!atOrAboveFloor(otherOutcome.severity, severityFloor))
|
|
160
|
+
continue;
|
|
161
|
+
const favoredOutcome = favoredByProbeId.get(probeId);
|
|
162
|
+
if (favoredOutcome === undefined || favoredOutcome.state !== 'caught') {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* AD-7's four-valued dominance relation. `comparabilityKey` and each side's
|
|
170
|
+
* own `strength.comparable` are checked before any component-wise comparison
|
|
171
|
+
* runs: a key mismatch means the two runs are not measuring a shared probe
|
|
172
|
+
* set, and `comparable: false` means AD-21 already marked that one side's own
|
|
173
|
+
* vector as thinner than the policy's declared minimum, so a `caught`/`rate`
|
|
174
|
+
* on it is not fit to decide a comparison either way. The severity-floor
|
|
175
|
+
* override runs only against the side the raw comparison favoured, and only
|
|
176
|
+
* ever downgrades that result to `incomparable`.
|
|
177
|
+
*/
|
|
178
|
+
export function compareDominance(a, b, severityFloor) {
|
|
179
|
+
if (a.comparabilityKey !== b.comparabilityKey)
|
|
180
|
+
return 'incomparable';
|
|
181
|
+
if (!a.strength.comparable || !b.strength.comparable)
|
|
182
|
+
return 'incomparable';
|
|
183
|
+
const raw = componentComparison(a.strength.vector, b.strength.vector);
|
|
184
|
+
if (raw === 'a-dominates-b' &&
|
|
185
|
+
favoredMissesWhatOtherCaught(a, b, severityFloor)) {
|
|
186
|
+
return 'incomparable';
|
|
187
|
+
}
|
|
188
|
+
if (raw === 'b-dominates-a' &&
|
|
189
|
+
favoredMissesWhatOtherCaught(b, a, severityFloor)) {
|
|
190
|
+
return 'incomparable';
|
|
191
|
+
}
|
|
192
|
+
return raw;
|
|
193
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { DefectSignature } from '../schemas/defect-signature.ts';
|
|
2
|
+
import type { PermittedInterface } from '../schemas/interface.ts';
|
|
3
|
+
import type { Probe } from '../schemas/probe.ts';
|
|
4
|
+
import type { SealedRunRecord } from '../schemas/sealed-run-record.ts';
|
|
5
|
+
/**
|
|
6
|
+
* A module constant, following pre-flight's precedent for the same problem:
|
|
7
|
+
* `resolveCheck`'s budget parameter is a bare required number whose declared
|
|
8
|
+
* home is the scoring policy, and a probe is not scored under a policy at match
|
|
9
|
+
* time. Two implementations choosing two budgets would disagree about when the
|
|
10
|
+
* evaluator reports an exhausted budget, which is the disagreement one shared
|
|
11
|
+
* constant removes.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PROBE_REGEX_MATCH_STEP_BUDGET = 1000000;
|
|
14
|
+
/**
|
|
15
|
+
* The six results, declared in the evaluation order the match applies, so a
|
|
16
|
+
* reader cannot mistake this constant for a different ordering.
|
|
17
|
+
*
|
|
18
|
+
* - `unexercised`: the evaluator never invoked the home operation.
|
|
19
|
+
* - `unwitnessed-claim`: a defect finding cited this probe and cited home-operation
|
|
20
|
+
* observations, none of which satisfies the condition. An AD-32
|
|
21
|
+
* declared-versus-observed inconsistency, and it outranks a competing
|
|
22
|
+
* `matched` so a broken reporter cannot mask it.
|
|
23
|
+
* - `matched`: a defect finding cited an observation the condition satisfies.
|
|
24
|
+
* - `manifested-unclaimed`: the condition was satisfied and no finding claimed
|
|
25
|
+
* it. This is what makes non-detection reachable.
|
|
26
|
+
* - `not-triggered`: the system was examined and did not manifest the seeded
|
|
27
|
+
* defect. AD-6 calls this the common case on any defect probe. Named for what
|
|
28
|
+
* it is rather than "not manifested", because AD-40 already uses that phrase
|
|
29
|
+
* for the vacuous case and the two route to opposite verdicts.
|
|
30
|
+
* - `vacuous`: every candidate resolved insufficient-evidence, so the corpus
|
|
31
|
+
* presented no defect to detect. A fact about the instrument.
|
|
32
|
+
*/
|
|
33
|
+
export declare const PROBE_WITNESS_RESULTS: readonly ['unexercised', 'unwitnessed-claim', 'matched', 'manifested-unclaimed', 'not-triggered', 'vacuous'];
|
|
34
|
+
export type ProbeWitnessResultValue = (typeof PROBE_WITNESS_RESULTS)[number];
|
|
35
|
+
/** A probe on the seeding branch whose signature is present. */
|
|
36
|
+
export type SignedProbe = Extract<Probe, {
|
|
37
|
+
expectedClean: false;
|
|
38
|
+
}> & {
|
|
39
|
+
readonly defectSignature: DefectSignature;
|
|
40
|
+
};
|
|
41
|
+
export type WitnessPartition = {
|
|
42
|
+
/** the candidates the condition resolved `true` over. */
|
|
43
|
+
readonly satisfying: readonly string[];
|
|
44
|
+
/** the candidates it resolved `false` over: the system examined and behaving. */
|
|
45
|
+
readonly refuting: readonly string[];
|
|
46
|
+
/** the candidates it could not examine. Never detection, and never manifestation. */
|
|
47
|
+
readonly inconclusive: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
export type ProbeWitnessMatch = {
|
|
50
|
+
readonly result: ProbeWitnessResultValue;
|
|
51
|
+
/**
|
|
52
|
+
* AD-40 forbids pooling a quotation-reconstructed detection with a measured
|
|
53
|
+
* catch rate, so every result says which it is. The witness match resolves
|
|
54
|
+
* over cited identifiers and is always `measured`.
|
|
55
|
+
*/
|
|
56
|
+
readonly basis: 'measured';
|
|
57
|
+
readonly homeOperationResolved: boolean;
|
|
58
|
+
readonly exercised: boolean;
|
|
59
|
+
/** every candidate identifier the match read, ascending by `sequence`. */
|
|
60
|
+
readonly observationIds: readonly string[];
|
|
61
|
+
readonly partition: WitnessPartition;
|
|
62
|
+
readonly partitionSizes: {
|
|
63
|
+
readonly satisfying: number;
|
|
64
|
+
readonly refuting: number;
|
|
65
|
+
readonly inconclusive: number;
|
|
66
|
+
};
|
|
67
|
+
/** the satisfying observations a defect finding actually cited. */
|
|
68
|
+
readonly witnessObservationIds: readonly string[];
|
|
69
|
+
/** the defect findings whose home-operation citations witness nothing. */
|
|
70
|
+
readonly unwitnessedFindingIds: readonly string[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* AD-40's deterministic witness match over one probe and one sealed run record.
|
|
74
|
+
*
|
|
75
|
+
* The candidates are partitioned by AD-4 resolution into satisfying, refuting,
|
|
76
|
+
* and inconclusive, and the result is read off that partition rather than off a
|
|
77
|
+
* precedence list. A partition is exhaustive and totally ordered over the whole
|
|
78
|
+
* input space; a precedence list left mixed refuting/inconclusive with no home
|
|
79
|
+
* and let an empty candidate set satisfy two rows that route to opposite
|
|
80
|
+
* verdicts.
|
|
81
|
+
*
|
|
82
|
+
* The verdict reads cited identifiers alone and never quotation. Quoted evidence
|
|
83
|
+
* that appears in no cited observation is a separate audit, and its invalidation
|
|
84
|
+
* belongs to ingest.
|
|
85
|
+
*
|
|
86
|
+
* No output reads an array's position. Observation identifiers follow the
|
|
87
|
+
* record's `sequence`; finding identifiers are sorted by identifier, because
|
|
88
|
+
* `findings` carries no ordering field of its own.
|
|
89
|
+
*/
|
|
90
|
+
export declare function matchProbeWitness(probe: SignedProbe, interfaces: readonly PermittedInterface[], record: Pick<SealedRunRecord, 'observations' | 'findings'>): ProbeWitnessMatch;
|
|
91
|
+
export type MappedFinding = {
|
|
92
|
+
readonly findingId: string;
|
|
93
|
+
readonly probeId: string;
|
|
94
|
+
};
|
|
95
|
+
export type FindingMap = {
|
|
96
|
+
/** cited to a signed probe, and touching that signature's home operation. */
|
|
97
|
+
readonly mapped: readonly MappedFinding[];
|
|
98
|
+
/**
|
|
99
|
+
* cited to a signed probe whose home operation the cited observations never
|
|
100
|
+
* touch. An unexpected real defect under AD-23, never a catch.
|
|
101
|
+
*/
|
|
102
|
+
readonly unmapped: readonly MappedFinding[];
|
|
103
|
+
/** cited to a probe identifier the set does not declare: an AD-32 cross-artifact dangling reference. */
|
|
104
|
+
readonly dangling: readonly MappedFinding[];
|
|
105
|
+
/** cited to a canary or a clean control, which carry no signature to map against. */
|
|
106
|
+
readonly signatureless: readonly MappedFinding[];
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Sorts every defect finding in the record into the four buckets AD-40 and
|
|
110
|
+
* AD-23 between them require, so no finding is silently dropped and none is
|
|
111
|
+
* counted as a catch it did not earn.
|
|
112
|
+
*
|
|
113
|
+
* Defect findings only: they are the only type that enters a detection measure.
|
|
114
|
+
* The uncited case proper, a finding naming no oracle, is a different rule and
|
|
115
|
+
* a different record.
|
|
116
|
+
*/
|
|
117
|
+
export declare function mapFindings(probes: readonly Probe[], interfaces: readonly PermittedInterface[], record: Pick<SealedRunRecord, 'observations' | 'findings'>): FindingMap;
|