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,79 @@
|
|
|
1
|
+
import type { EvidenceChannelName } from '../schemas/pointer.ts';
|
|
2
|
+
import type { Observation, SealedRunRecord } from '../schemas/sealed-run-record.ts';
|
|
3
|
+
type DefectFinding = Extract<SealedRunRecord['findings'][number], {
|
|
4
|
+
findingType: 'defect';
|
|
5
|
+
}>;
|
|
6
|
+
type QuotedEvidence = DefectFinding['quotedEvidence'][number];
|
|
7
|
+
/**
|
|
8
|
+
* One evidence channel of one observation, rendered as the text a quotation is
|
|
9
|
+
* asked to be a substring of.
|
|
10
|
+
*
|
|
11
|
+
* `stdout` and `stderr` project as themselves; `response-status` and
|
|
12
|
+
* `exit-code` through the ordinary integer rendering; `response-body`,
|
|
13
|
+
* `response-headers`, and `call-inputs` through the same RFC 8785 serialization
|
|
14
|
+
* the digest uses.
|
|
15
|
+
*
|
|
16
|
+
* No case folding and no whitespace normalization: AD-23 requires the evidence
|
|
17
|
+
* verbatim, and a normalizing match would accept a quote the record does not
|
|
18
|
+
* contain. Canonical serialization re-spells a body with sorted keys and no
|
|
19
|
+
* whitespace, so a quote taken from a pretty-printed rendering is not a
|
|
20
|
+
* substring of it. The record stores JSON values and has no pretty-printed form
|
|
21
|
+
* to quote.
|
|
22
|
+
*
|
|
23
|
+
* A `null` channel projects to nothing and witnesses nothing. On
|
|
24
|
+
* `response-body` that also swallows a body that genuinely was JSON `null`,
|
|
25
|
+
* which is the schema's own stated cost of one uniform spelling for "absent".
|
|
26
|
+
*
|
|
27
|
+
* Should canonicalization fault on a caller's record, the fault propagates,
|
|
28
|
+
* which is what keeps this procedure from being circular with it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function projectChannel(observation: Observation, channel: EvidenceChannelName, artifactPath: string): string | null;
|
|
31
|
+
export type UnwitnessedQuotation = {
|
|
32
|
+
readonly findingId: string;
|
|
33
|
+
/** the position of the quotation within the finding's own list. */
|
|
34
|
+
readonly quoteIndex: number;
|
|
35
|
+
readonly channel: EvidenceChannelName;
|
|
36
|
+
readonly quote: string;
|
|
37
|
+
/** the observations the finding cited and this audit could read. */
|
|
38
|
+
readonly citedObservationIds: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Every defect finding's quoted evidence, checked against the observations that
|
|
42
|
+
* finding itself cited.
|
|
43
|
+
*
|
|
44
|
+
* Over every defect finding in the record regardless of which bucket the
|
|
45
|
+
* finding maps to, because AD-40 makes an unwitnessed quote a property of the
|
|
46
|
+
* finding rather than of the probe it maps to. A cited identifier naming no
|
|
47
|
+
* observation in the record contributes nothing to read, which is a separate
|
|
48
|
+
* cross-artifact inconsistency and is not this audit's to report.
|
|
49
|
+
*/
|
|
50
|
+
export declare function auditQuotation(record: Pick<SealedRunRecord, 'observations' | 'findings'>): readonly UnwitnessedQuotation[];
|
|
51
|
+
export type ReconstructedDetection = {
|
|
52
|
+
/**
|
|
53
|
+
* AD-40 forbids pooling a detection derived this way with a measured catch
|
|
54
|
+
* rate, so the label travels with the result and is never optional.
|
|
55
|
+
*/
|
|
56
|
+
readonly basis: 'reconstructed';
|
|
57
|
+
readonly detected: boolean;
|
|
58
|
+
readonly witnessObservationIds: readonly string[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The containment procedure AD-40 keeps defined for exactly one purpose:
|
|
62
|
+
* re-deriving detection from records written before observation identifiers
|
|
63
|
+
* were required.
|
|
64
|
+
*
|
|
65
|
+
* Nothing in v0 calls it, and nothing can. At least one observation identifier
|
|
66
|
+
* has been required on the defect branch since the schema's first version, so
|
|
67
|
+
* no version of this schema predates the identifier requirement, and AD-11
|
|
68
|
+
* makes a reader reject an unequal version anyway. The procedure exists for
|
|
69
|
+
* records from outside this schema entirely.
|
|
70
|
+
*
|
|
71
|
+
* It takes the satisfying partition the witness match already computed and asks
|
|
72
|
+
* which of those observations the finding's own quotations appear in. Detection
|
|
73
|
+
* so derived is `reconstructed`; the witness match's is `measured`, and the two
|
|
74
|
+
* are never pooled.
|
|
75
|
+
*/
|
|
76
|
+
export declare function reconstructDetection(finding: {
|
|
77
|
+
readonly quotedEvidence: readonly QuotedEvidence[];
|
|
78
|
+
}, satisfying: readonly Observation[], artifactPath: string): ReconstructedDetection;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-40's quotation half, kept out of the verdict path on purpose.
|
|
3
|
+
*
|
|
4
|
+
* Every defect finding carries two operands: the identifiers of the
|
|
5
|
+
* observations it relies on, and the verbatim quoted evidence with its channel.
|
|
6
|
+
* Requiring both without ranking them left the disagreement case with three
|
|
7
|
+
* conforming answers, and ADR-009 Decision 2 ranks them: "the match resolves
|
|
8
|
+
* over identifiers alone; quoted evidence that appears in no cited observation
|
|
9
|
+
* invalidates the run as an AD-32 declared-versus-observed inconsistency." So
|
|
10
|
+
* the witness match reads identifiers and this module audits quotation, and no
|
|
11
|
+
* verdict path reads what this module returns.
|
|
12
|
+
*
|
|
13
|
+
* The invalidation itself belongs to ingest, which owns no module yet, so
|
|
14
|
+
* `auditQuotation` ships with no caller by design.
|
|
15
|
+
*
|
|
16
|
+
* Nothing in `src/` compared a quote against an observation before this, so
|
|
17
|
+
* there was no procedure to inherit. Two shipped mechanisms look like
|
|
18
|
+
* candidates and are not: AD-4's containment operator falls through to `false`
|
|
19
|
+
* on an object container rather than serializing it, and the canonical digest
|
|
20
|
+
* returns bytes rather than the string a substring test needs.
|
|
21
|
+
*/
|
|
22
|
+
import { serialize } from '../canonical/canonicalize.js';
|
|
23
|
+
/**
|
|
24
|
+
* One evidence channel of one observation, rendered as the text a quotation is
|
|
25
|
+
* asked to be a substring of.
|
|
26
|
+
*
|
|
27
|
+
* `stdout` and `stderr` project as themselves; `response-status` and
|
|
28
|
+
* `exit-code` through the ordinary integer rendering; `response-body`,
|
|
29
|
+
* `response-headers`, and `call-inputs` through the same RFC 8785 serialization
|
|
30
|
+
* the digest uses.
|
|
31
|
+
*
|
|
32
|
+
* No case folding and no whitespace normalization: AD-23 requires the evidence
|
|
33
|
+
* verbatim, and a normalizing match would accept a quote the record does not
|
|
34
|
+
* contain. Canonical serialization re-spells a body with sorted keys and no
|
|
35
|
+
* whitespace, so a quote taken from a pretty-printed rendering is not a
|
|
36
|
+
* substring of it. The record stores JSON values and has no pretty-printed form
|
|
37
|
+
* to quote.
|
|
38
|
+
*
|
|
39
|
+
* A `null` channel projects to nothing and witnesses nothing. On
|
|
40
|
+
* `response-body` that also swallows a body that genuinely was JSON `null`,
|
|
41
|
+
* which is the schema's own stated cost of one uniform spelling for "absent".
|
|
42
|
+
*
|
|
43
|
+
* Should canonicalization fault on a caller's record, the fault propagates,
|
|
44
|
+
* which is what keeps this procedure from being circular with it.
|
|
45
|
+
*/
|
|
46
|
+
export function projectChannel(observation, channel, artifactPath) {
|
|
47
|
+
switch (channel) {
|
|
48
|
+
case 'stdout':
|
|
49
|
+
return observation.stdout;
|
|
50
|
+
case 'stderr':
|
|
51
|
+
return observation.stderr;
|
|
52
|
+
case 'response-status':
|
|
53
|
+
return observation.responseStatus === null
|
|
54
|
+
? null
|
|
55
|
+
: observation.responseStatus.toString();
|
|
56
|
+
case 'exit-code':
|
|
57
|
+
return observation.exitCode === null
|
|
58
|
+
? null
|
|
59
|
+
: observation.exitCode.toString();
|
|
60
|
+
case 'response-body':
|
|
61
|
+
return observation.responseBody === null
|
|
62
|
+
? null
|
|
63
|
+
: serialize(observation.responseBody, artifactPath);
|
|
64
|
+
case 'response-headers':
|
|
65
|
+
return observation.responseHeaders === null
|
|
66
|
+
? null
|
|
67
|
+
: serialize(observation.responseHeaders, artifactPath);
|
|
68
|
+
case 'call-inputs':
|
|
69
|
+
return serialize(observation.callInputs, artifactPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether one quotation appears in one observation's named channel, verbatim.
|
|
74
|
+
*/
|
|
75
|
+
function quotationWitnessed(quoted, observation, artifactPath) {
|
|
76
|
+
const projected = projectChannel(observation, quoted.channel, artifactPath);
|
|
77
|
+
// A null projection witnesses nothing, and `false` is the answer: an
|
|
78
|
+
// optional chain here would hand a caller `undefined` where the return type
|
|
79
|
+
// says boolean.
|
|
80
|
+
if (projected === null)
|
|
81
|
+
return false;
|
|
82
|
+
return projected.includes(quoted.quote);
|
|
83
|
+
}
|
|
84
|
+
/** total over strings, so a tie is a genuine equality rather than a coin toss. */
|
|
85
|
+
const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
86
|
+
/**
|
|
87
|
+
* Every defect finding's quoted evidence, checked against the observations that
|
|
88
|
+
* finding itself cited.
|
|
89
|
+
*
|
|
90
|
+
* Over every defect finding in the record regardless of which bucket the
|
|
91
|
+
* finding maps to, because AD-40 makes an unwitnessed quote a property of the
|
|
92
|
+
* finding rather than of the probe it maps to. A cited identifier naming no
|
|
93
|
+
* observation in the record contributes nothing to read, which is a separate
|
|
94
|
+
* cross-artifact inconsistency and is not this audit's to report.
|
|
95
|
+
*/
|
|
96
|
+
export function auditQuotation(record) {
|
|
97
|
+
// Indexed to a list rather than a value. `observations` is refined unique on
|
|
98
|
+
// `sequence` and not on `observationId`, so two observations may share an
|
|
99
|
+
// identifier, and a last-write-wins map would turn the existential below into
|
|
100
|
+
// a pick decided by array position: one permutation of a record answers that
|
|
101
|
+
// a quotation is witnessed and the other that it is not. AD-40 states the
|
|
102
|
+
// rule as "at least one observation satisfying the condition", so a repeated
|
|
103
|
+
// identifier names both and the `.some` ranges over both.
|
|
104
|
+
const byId = new Map();
|
|
105
|
+
for (const observation of record.observations) {
|
|
106
|
+
const sharing = byId.get(observation.observationId);
|
|
107
|
+
if (sharing === undefined) {
|
|
108
|
+
byId.set(observation.observationId, [observation]);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
sharing.push(observation);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const unwitnessed = [];
|
|
115
|
+
for (const finding of record.findings) {
|
|
116
|
+
if (finding.findingType !== 'defect')
|
|
117
|
+
continue;
|
|
118
|
+
// Deduplicated and sorted by identifier. `observationIds` is an array with
|
|
119
|
+
// no uniqueness refinement, so a finding may cite one observation twice,
|
|
120
|
+
// and carrying the citation array's order into `citedObservationIds` would
|
|
121
|
+
// put a position with no declared meaning on the payload. The identifier
|
|
122
|
+
// list stays deduplicated even where two observations share one: it names
|
|
123
|
+
// what the finding cited, not what resolved.
|
|
124
|
+
const citedObservationIds = [];
|
|
125
|
+
const cited = [];
|
|
126
|
+
for (const identifier of [...new Set(finding.observationIds)].sort(order)) {
|
|
127
|
+
const sharing = byId.get(identifier);
|
|
128
|
+
if (sharing === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
citedObservationIds.push(identifier);
|
|
131
|
+
cited.push(...sharing);
|
|
132
|
+
}
|
|
133
|
+
const artifactPath = `SealedRunRecord.findings[findingId=${finding.findingId}]`;
|
|
134
|
+
finding.quotedEvidence.forEach((quoted, quoteIndex) => {
|
|
135
|
+
const witnessed = cited.some((observation) => quotationWitnessed(quoted, observation, artifactPath));
|
|
136
|
+
if (witnessed)
|
|
137
|
+
return;
|
|
138
|
+
unwitnessed.push({
|
|
139
|
+
findingId: finding.findingId,
|
|
140
|
+
quoteIndex,
|
|
141
|
+
channel: quoted.channel,
|
|
142
|
+
quote: quoted.quote,
|
|
143
|
+
citedObservationIds,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// `findings` carries no ordering field, so reading it in array order would
|
|
148
|
+
// make the result depend on a position NFR9 forbids reading. Sorted by
|
|
149
|
+
// finding identifier, then by the quotation's index within that finding,
|
|
150
|
+
// which is the one order the record itself declares.
|
|
151
|
+
//
|
|
152
|
+
// `findingId` and `quoteIndex` do not separate every pair: `findings` has no
|
|
153
|
+
// uniqueness refinement, so two findings may share an identifier and each
|
|
154
|
+
// carry an unwitnessed quote at index 0, and a stable sort would then fall
|
|
155
|
+
// back to the position this sort exists to stop reading. Unlike two entries
|
|
156
|
+
// that tie on everything, those two are distinguishable — different quote,
|
|
157
|
+
// different channel, two basis lines that say different things — so the key
|
|
158
|
+
// runs to the whole payload.
|
|
159
|
+
//
|
|
160
|
+
// `quote` is compared directly rather than through a joined key. It is free
|
|
161
|
+
// text and may contain any code point including U+0000, so no delimiter is
|
|
162
|
+
// below every character it can carry. `citedObservationIds` may be joined:
|
|
163
|
+
// its members are `Identifier`, whose lowest code point is U+002D.
|
|
164
|
+
return unwitnessed.sort((a, b) => order(a.findingId, b.findingId) ||
|
|
165
|
+
a.quoteIndex - b.quoteIndex ||
|
|
166
|
+
order(a.channel, b.channel) ||
|
|
167
|
+
order(a.quote, b.quote) ||
|
|
168
|
+
order(a.citedObservationIds.join('\u0000'), b.citedObservationIds.join('\u0000')));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The containment procedure AD-40 keeps defined for exactly one purpose:
|
|
172
|
+
* re-deriving detection from records written before observation identifiers
|
|
173
|
+
* were required.
|
|
174
|
+
*
|
|
175
|
+
* Nothing in v0 calls it, and nothing can. At least one observation identifier
|
|
176
|
+
* has been required on the defect branch since the schema's first version, so
|
|
177
|
+
* no version of this schema predates the identifier requirement, and AD-11
|
|
178
|
+
* makes a reader reject an unequal version anyway. The procedure exists for
|
|
179
|
+
* records from outside this schema entirely.
|
|
180
|
+
*
|
|
181
|
+
* It takes the satisfying partition the witness match already computed and asks
|
|
182
|
+
* which of those observations the finding's own quotations appear in. Detection
|
|
183
|
+
* so derived is `reconstructed`; the witness match's is `measured`, and the two
|
|
184
|
+
* are never pooled.
|
|
185
|
+
*/
|
|
186
|
+
export function reconstructDetection(finding, satisfying, artifactPath) {
|
|
187
|
+
// A finding quoting nothing proves nothing, which the schema already forbids
|
|
188
|
+
// on this branch; a record from outside it can still present the shape, and
|
|
189
|
+
// an `every` over an empty list would otherwise report every satisfying
|
|
190
|
+
// observation as a witness.
|
|
191
|
+
const witnessObservationIds = finding.quotedEvidence.length === 0
|
|
192
|
+
? []
|
|
193
|
+
: satisfying
|
|
194
|
+
.filter((observation) => finding.quotedEvidence.every((quoted) => quotationWitnessed(quoted, observation, artifactPath)))
|
|
195
|
+
.map((observation) => observation.observationId);
|
|
196
|
+
return {
|
|
197
|
+
basis: 'reconstructed',
|
|
198
|
+
detected: witnessObservationIds.length > 0,
|
|
199
|
+
witnessObservationIds,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-7's trial-set reducer, closing Owed item 1: repeated trials had no
|
|
3
|
+
* reducer, so the default three-trial minimum was unreachable.
|
|
4
|
+
*
|
|
5
|
+
* Pure and total: no clock, filesystem, or randomness, and nothing thrown for
|
|
6
|
+
* a domain input. Stage one, collapsing several outcome resolutions for one
|
|
7
|
+
* `(probeId, trialIndex)` down to one vote, is a lookup rather than a genuine
|
|
8
|
+
* multi-value fold, because AD-40's discriminating condition pairs a probe
|
|
9
|
+
* with exactly one designated oracle; the caller performs that lookup and
|
|
10
|
+
* this module receives the resulting one-vote-per-trial sequence directly.
|
|
11
|
+
* Stage two, folding across trials for one probe, is `reduceTrialSet` below.
|
|
12
|
+
*/
|
|
13
|
+
import type { InvalidatedAttempt, OUTCOME_STATES } from '../schemas/evidence-artifact.ts';
|
|
14
|
+
type OutcomeStateValue = (typeof OUTCOME_STATES)[number];
|
|
15
|
+
/**
|
|
16
|
+
* The reducer's own vote, narrower than `resolveOutcome`'s
|
|
17
|
+
* `OutcomeResolution`: only `state` decides which of the three groups below a
|
|
18
|
+
* trial falls into, and the reducer has no use for a rule identifier, a
|
|
19
|
+
* resolved-from finding, or a corroboration value.
|
|
20
|
+
*/
|
|
21
|
+
export type TrialVote = {
|
|
22
|
+
readonly state: OutcomeStateValue;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The reducer's three-way grouping of AD-6's closed twelve states. Three
|
|
26
|
+
* invalidate a trial for this probe and are excluded from both the vote and
|
|
27
|
+
* the valid-trial count; two leave a trial's probe unvoted without
|
|
28
|
+
* invalidating it, so an unexercised trial contributes to neither the
|
|
29
|
+
* numerator nor the denominator; the remaining seven are valid votes, of
|
|
30
|
+
* which only `caught` counts toward the numerator.
|
|
31
|
+
*/
|
|
32
|
+
export declare const TRIAL_VOTE_STATES: {
|
|
33
|
+
readonly invalidating: readonly ["oracle-error", "judge-error", "infrastructure-error"];
|
|
34
|
+
readonly unvoted: readonly ["not-applicable", "unreached"];
|
|
35
|
+
readonly voted: readonly ["caught", "confirmed", "missed", "abstained", "bypassed", "passed-clean-control", "false-positive"];
|
|
36
|
+
};
|
|
37
|
+
export type TrialVoteState = keyof typeof TRIAL_VOTE_STATES;
|
|
38
|
+
export type TrialSetResult = {
|
|
39
|
+
/** whether at least one trial voted, i.e. `validCount > 0`. */
|
|
40
|
+
readonly exercised: boolean;
|
|
41
|
+
/** `caughtCount / validCount > catchThreshold`, strict, so a tie is unreachable. */
|
|
42
|
+
readonly caught: boolean;
|
|
43
|
+
/** the count of voted (valid, non-`unvoted`) trials: the majority's denominator. */
|
|
44
|
+
readonly validCount: number;
|
|
45
|
+
/** the count of voted trials whose state is `caught`: the majority's numerator. */
|
|
46
|
+
readonly caughtCount: number;
|
|
47
|
+
readonly invalidatedAttempts: readonly InvalidatedAttempt[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Folds one probe's trial votes to one result. `votes[i]`'s position is the
|
|
51
|
+
* attempt number `i + 1`, since stage one has already reduced each trial's
|
|
52
|
+
* outcome resolutions to one vote per `(probeId, trialIndex)`.
|
|
53
|
+
*
|
|
54
|
+
* A probe with zero voted trials is `exercised: false` and contributes
|
|
55
|
+
* nothing to `ClassStrength`; `caught` is `false` in that case too, since a
|
|
56
|
+
* majority over zero trials decides nothing.
|
|
57
|
+
*
|
|
58
|
+
* `catchThreshold` is checked against `ScoringPolicy.catchThreshold`'s own
|
|
59
|
+
* declared domain (`.min(0).max(1)`) and a `vote.state` outside the closed
|
|
60
|
+
* twelve is checked explicitly, rather than silently falling through into
|
|
61
|
+
* `votedStates.push`: both were reachable only through a type-system bypass
|
|
62
|
+
* while this reducer shipped with no caller, and now that `score.ts` calls
|
|
63
|
+
* it for real, a bypass is exactly the input this pure function must still
|
|
64
|
+
* answer without absorbing it silently.
|
|
65
|
+
*
|
|
66
|
+
* Both checks are written to survive a bypass that also defeats the naive
|
|
67
|
+
* form of the check, not only a bypass of the declared type. `catchThreshold`
|
|
68
|
+
* is asserted a finite primitive `number` before the range comparison runs:
|
|
69
|
+
* `>=`/`<=` coerce their operands, so `null`, a numeric string, or a boxed
|
|
70
|
+
* `Number` would each satisfy `0 <= x <= 1` without ever being the number the
|
|
71
|
+
* range check means to bound. `vote.state`'s lookup uses `Object.hasOwn`
|
|
72
|
+
* rather than trusting `=== undefined`: `TRIAL_VOTE_STATE_OF` is a plain
|
|
73
|
+
* object and so inherits `Object.prototype`, and a state string that happens
|
|
74
|
+
* to name an inherited member (`"toString"`, `"constructor"`,
|
|
75
|
+
* `"hasOwnProperty"`, `"valueOf"`) resolves to that inherited function
|
|
76
|
+
* instead of `undefined`, which would silently clear the `=== undefined`
|
|
77
|
+
* guard and vote the bogus state in.
|
|
78
|
+
*/
|
|
79
|
+
export declare function reduceTrialSet(votes: readonly TrialVote[], catchThreshold: number): TrialSetResult;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reducer's three-way grouping of AD-6's closed twelve states. Three
|
|
3
|
+
* invalidate a trial for this probe and are excluded from both the vote and
|
|
4
|
+
* the valid-trial count; two leave a trial's probe unvoted without
|
|
5
|
+
* invalidating it, so an unexercised trial contributes to neither the
|
|
6
|
+
* numerator nor the denominator; the remaining seven are valid votes, of
|
|
7
|
+
* which only `caught` counts toward the numerator.
|
|
8
|
+
*/
|
|
9
|
+
export const TRIAL_VOTE_STATES = {
|
|
10
|
+
invalidating: ['oracle-error', 'judge-error', 'infrastructure-error'],
|
|
11
|
+
unvoted: ['not-applicable', 'unreached'],
|
|
12
|
+
voted: [
|
|
13
|
+
'caught',
|
|
14
|
+
'confirmed',
|
|
15
|
+
'missed',
|
|
16
|
+
'abstained',
|
|
17
|
+
'bypassed',
|
|
18
|
+
'passed-clean-control',
|
|
19
|
+
'false-positive',
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A total map from every one of the twelve states to its group, built once
|
|
24
|
+
* from `TRIAL_VOTE_STATES` rather than duplicated as a second literal that
|
|
25
|
+
* could drift from it.
|
|
26
|
+
*/
|
|
27
|
+
const TRIAL_VOTE_STATE_OF = Object.fromEntries(Object.entries(TRIAL_VOTE_STATES).flatMap(([group, states]) => states.map((state) => [state, group])));
|
|
28
|
+
/**
|
|
29
|
+
* Folds one probe's trial votes to one result. `votes[i]`'s position is the
|
|
30
|
+
* attempt number `i + 1`, since stage one has already reduced each trial's
|
|
31
|
+
* outcome resolutions to one vote per `(probeId, trialIndex)`.
|
|
32
|
+
*
|
|
33
|
+
* A probe with zero voted trials is `exercised: false` and contributes
|
|
34
|
+
* nothing to `ClassStrength`; `caught` is `false` in that case too, since a
|
|
35
|
+
* majority over zero trials decides nothing.
|
|
36
|
+
*
|
|
37
|
+
* `catchThreshold` is checked against `ScoringPolicy.catchThreshold`'s own
|
|
38
|
+
* declared domain (`.min(0).max(1)`) and a `vote.state` outside the closed
|
|
39
|
+
* twelve is checked explicitly, rather than silently falling through into
|
|
40
|
+
* `votedStates.push`: both were reachable only through a type-system bypass
|
|
41
|
+
* while this reducer shipped with no caller, and now that `score.ts` calls
|
|
42
|
+
* it for real, a bypass is exactly the input this pure function must still
|
|
43
|
+
* answer without absorbing it silently.
|
|
44
|
+
*
|
|
45
|
+
* Both checks are written to survive a bypass that also defeats the naive
|
|
46
|
+
* form of the check, not only a bypass of the declared type. `catchThreshold`
|
|
47
|
+
* is asserted a finite primitive `number` before the range comparison runs:
|
|
48
|
+
* `>=`/`<=` coerce their operands, so `null`, a numeric string, or a boxed
|
|
49
|
+
* `Number` would each satisfy `0 <= x <= 1` without ever being the number the
|
|
50
|
+
* range check means to bound. `vote.state`'s lookup uses `Object.hasOwn`
|
|
51
|
+
* rather than trusting `=== undefined`: `TRIAL_VOTE_STATE_OF` is a plain
|
|
52
|
+
* object and so inherits `Object.prototype`, and a state string that happens
|
|
53
|
+
* to name an inherited member (`"toString"`, `"constructor"`,
|
|
54
|
+
* `"hasOwnProperty"`, `"valueOf"`) resolves to that inherited function
|
|
55
|
+
* instead of `undefined`, which would silently clear the `=== undefined`
|
|
56
|
+
* guard and vote the bogus state in.
|
|
57
|
+
*/
|
|
58
|
+
export function reduceTrialSet(votes, catchThreshold) {
|
|
59
|
+
if (typeof catchThreshold !== 'number' || !Number.isFinite(catchThreshold)) {
|
|
60
|
+
throw new TypeError(`reduceTrialSet: catchThreshold ${String(catchThreshold)} is not a finite number`);
|
|
61
|
+
}
|
|
62
|
+
if (!(catchThreshold >= 0 && catchThreshold <= 1)) {
|
|
63
|
+
throw new TypeError(`reduceTrialSet: catchThreshold ${catchThreshold} is outside ScoringPolicy's declared 0..1 domain`);
|
|
64
|
+
}
|
|
65
|
+
const invalidatedAttempts = [];
|
|
66
|
+
const votedStates = [];
|
|
67
|
+
votes.forEach((vote, index) => {
|
|
68
|
+
if (!Object.hasOwn(TRIAL_VOTE_STATE_OF, vote.state)) {
|
|
69
|
+
throw new TypeError(`reduceTrialSet: vote ${index + 1} carries an out-of-domain state "${vote.state}"`);
|
|
70
|
+
}
|
|
71
|
+
const group = TRIAL_VOTE_STATE_OF[vote.state];
|
|
72
|
+
if (group === 'invalidating') {
|
|
73
|
+
invalidatedAttempts.push({ attempt: index + 1, reason: vote.state });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (group === 'unvoted')
|
|
77
|
+
return;
|
|
78
|
+
votedStates.push(vote.state);
|
|
79
|
+
});
|
|
80
|
+
const validCount = votedStates.length;
|
|
81
|
+
const caughtCount = votedStates.filter((state) => state === 'caught').length;
|
|
82
|
+
const exercised = validCount > 0;
|
|
83
|
+
// Divide rather than multiply: `caughtCount > catchThreshold * validCount`
|
|
84
|
+
// rounds `catchThreshold * validCount` under IEEE-754 for some non-power-of-
|
|
85
|
+
// two thresholds (0.29 * 100 = 28.999999999999996), which turns a
|
|
86
|
+
// mathematically exact tie (29/100 = 0.29) into a false caught. Division by
|
|
87
|
+
// a positive integer `validCount` has no such failure mode here.
|
|
88
|
+
const caught = exercised && caughtCount / validCount > catchThreshold;
|
|
89
|
+
return { exercised, caught, validCount, caughtCount, invalidatedAttempts };
|
|
90
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-24's score stage: the one orchestration over the reference functions
|
|
3
|
+
* `core/score/` already ships, closing owed item 1's remaining half (a
|
|
4
|
+
* trial-set shape for `validated-observations`) and giving `score.module`
|
|
5
|
+
* its first real value.
|
|
6
|
+
*
|
|
7
|
+
* The body lifts `scripts/worked-example-target.ts:1071-1381`'s order
|
|
8
|
+
* rather than designing a second one, generalised two ways that file never
|
|
9
|
+
* needed: over `readonly ValidatedObservations[]` instead of one record, and
|
|
10
|
+
* without ever throwing on a domain input. Every `fail()` call that script
|
|
11
|
+
* used to stop on a malformed-but-schema-legal shape (a rejected probe, a
|
|
12
|
+
* clean-control or canary probe with no seeded defect, an oracle with no
|
|
13
|
+
* check, a resolution naming a finding the trial does not carry) is replaced
|
|
14
|
+
* here by a graceful, documented fallback: a `schema-parse-failure` already
|
|
15
|
+
* happened at the application boundary before any of these values reached
|
|
16
|
+
* this function, so nothing left to see here is a reason to crash.
|
|
17
|
+
*
|
|
18
|
+
* Two of `AssessmentCommon`'s fields have no source among this stage's five
|
|
19
|
+
* declared inputs and are not among its two caller-supplied parameters
|
|
20
|
+
* either: `evidenceIntegrity.disclosure` (`EvidenceDisclosure`, which lives
|
|
21
|
+
* only on `SealedRunRecord`, a field `ingest`'s product does not carry
|
|
22
|
+
* through) and `remediationState` (`AD-12`'s remediation chain, which needs
|
|
23
|
+
* a caller-presented ancestor sequence score's five inputs never carry, only
|
|
24
|
+
* the current contract). Both arrive declared with a neutral value, exactly
|
|
25
|
+
* the posture `ladder.ts`'s own `EvidenceIntegrityInputs` doc comment
|
|
26
|
+
* already states for `overTruncated`, `unavailable`, and
|
|
27
|
+
* `internallyInconsistent`: "they arrive declared rather than derived."
|
|
28
|
+
* `disclosure` and `remediationState` join them on the same reasoning
|
|
29
|
+
* rather than a new one.
|
|
30
|
+
*/
|
|
31
|
+
import type { ValidatedObservations } from '../ingest/ingest.ts';
|
|
32
|
+
import { type EvalContract } from '../schemas/eval-contract.ts';
|
|
33
|
+
import type { Outcome } from '../schemas/evidence-artifact.ts';
|
|
34
|
+
import type { Probe } from '../schemas/probe.ts';
|
|
35
|
+
import type { ScoringPolicy } from '../schemas/scoring-policy.ts';
|
|
36
|
+
import type { ScoreStage } from '../stage-contracts.ts';
|
|
37
|
+
import type { ContractAssessment, LadderResolution, ProductionAssessment } from './ladder.ts';
|
|
38
|
+
import { type SealedProbeSet } from './qualification.ts';
|
|
39
|
+
import { type TrialSetResult } from './reduce-trials.ts';
|
|
40
|
+
/**
|
|
41
|
+
* `score`'s owned product: the assessment/ladder pairing AD-24 names, "the
|
|
42
|
+
* outcome and verdict values emit serializes", widened with eight more
|
|
43
|
+
* fields `emit` needs to mint an `EvidenceArtifact` and cannot re-derive from
|
|
44
|
+
* that pairing alone. Every one of the eight is a value this function
|
|
45
|
+
* already holds locally or already receives as a parameter; none is fetched
|
|
46
|
+
* anew, only carried one step further. Still not a new artifact: a plain
|
|
47
|
+
* TypeScript type with no Zod schema, matching `ValidatedObservations`'
|
|
48
|
+
* precedent.
|
|
49
|
+
*/
|
|
50
|
+
export type ScoredOutcomesAndVerdict = {
|
|
51
|
+
readonly assessment: ProductionAssessment | ContractAssessment;
|
|
52
|
+
readonly ladder: LadderResolution;
|
|
53
|
+
/** the trial set's own run identifier, read off the first trial the same way `mode`/`evaluatorRecommendation` are. */
|
|
54
|
+
readonly runId: string;
|
|
55
|
+
readonly contract: EvalContract;
|
|
56
|
+
readonly policy: ScoringPolicy;
|
|
57
|
+
readonly probe: Probe;
|
|
58
|
+
readonly sealedProbes: SealedProbeSet;
|
|
59
|
+
/** this probe's own AD-7 trial-set fold, keyed by `emit` under `probe.probeId` to build the strength vector. */
|
|
60
|
+
readonly trialSetResult: TrialSetResult;
|
|
61
|
+
/** 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. */
|
|
62
|
+
readonly outcomes: readonly Outcome[];
|
|
63
|
+
/** every finding across every trial citing no oracle, per `outcome.ts`'s `uncitedFindingIds`. */
|
|
64
|
+
readonly uncitedFindings: readonly string[];
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The stage. Signature order matches `ScoreStage`'s own: the five declared
|
|
68
|
+
* artifact inputs, then `waiver` and `evaluationFault`, the two documented
|
|
69
|
+
* caller-supplied parameters -- neither has a source among those five, and
|
|
70
|
+
* each arrives named and explicit rather than a hardcoded literal.
|
|
71
|
+
*/
|
|
72
|
+
export declare const score: ScoreStage<ValidatedObservations, ScoredOutcomesAndVerdict>;
|