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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eleven conditions `core/ingest` detects, and the ladder input each one
|
|
3
|
+
* feeds.
|
|
4
|
+
*
|
|
5
|
+
* Each condition is a cross-artifact rule a shipped schema names `core/ingest`
|
|
6
|
+
* as the enforcement point for, and none of them is a parse failure: the
|
|
7
|
+
* artifacts arrive already parsed, and a rule two conforming artifacts break
|
|
8
|
+
* between them is a finding about the run rather than a rejection of its bytes.
|
|
9
|
+
*
|
|
10
|
+
* The kinds are a runtime tuple first and the union draws from it, the idiom
|
|
11
|
+
* `RUNTIME_FAULT_CODES`, `FORBIDDEN_INPUT_FLOOR`, and `ORACLE_DISPOSITIONS` are
|
|
12
|
+
* written in. A union's `kind` literals are erased at compile time, so a drift
|
|
13
|
+
* check over a bare union could only compare the mapping below against itself or
|
|
14
|
+
* against a hand-written second list, which is the drift it exists to catch.
|
|
15
|
+
*
|
|
16
|
+
* `ladder.ts` is imported type-only on purpose. `isolatedModules` without
|
|
17
|
+
* `verbatimModuleSyntax` compiles a value import used only in type position
|
|
18
|
+
* silently, which would put the whole ladder module on this module's runtime
|
|
19
|
+
* load path; `preflight/reduce.ts` imports `ReduceStage` the same way.
|
|
20
|
+
*/
|
|
21
|
+
import type { ForbiddenInput } from '../schemas/eval-contract.ts';
|
|
22
|
+
import type { EvidenceIntegrityInputs, OutcomeStateInputs } from '../score/ladder.ts';
|
|
23
|
+
import type { UnwitnessedQuotation } from '../score/quotation.ts';
|
|
24
|
+
/**
|
|
25
|
+
* The eleven kinds, in the order `ingest` records them. The two isolation-family
|
|
26
|
+
* kinds and the two evaluator-configuration ones are sequenced here; the order
|
|
27
|
+
* of the values inside one `isolation-manifest-violation` is the manifest's own
|
|
28
|
+
* array order and belongs to the stage, not to this tuple.
|
|
29
|
+
*/
|
|
30
|
+
export declare const INGEST_CONDITION_KINDS: readonly ['duplicate-record-identifier', 'dangling-citation', 'dangling-disposition-citation', 'unwitnessed-quotation', 'isolation-manifest-absent', 'isolation-manifest-violation', 'forbidden-input-not-withheld', 'cross-artifact-disagreement', 'evaluator-configuration-absent', 'evaluator-configuration-digest-mismatch', 'judge-result-unscored'];
|
|
31
|
+
export type IngestConditionKind = (typeof INGEST_CONDITION_KINDS)[number];
|
|
32
|
+
/**
|
|
33
|
+
* The three fields AD-32 requires the sealed run record and the isolation
|
|
34
|
+
* manifest to agree on, in the order a disagreement is reported.
|
|
35
|
+
*
|
|
36
|
+
* `conditionArm` sits on both artifacts and is deliberately excluded: both
|
|
37
|
+
* schemas describe it as "an opaque caller label with no product semantics, per
|
|
38
|
+
* AD-24", so making a mismatch invalidate would mint a normative rule with no
|
|
39
|
+
* AD behind it and would be the one invalidating condition in the design with
|
|
40
|
+
* nothing downstream reading it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const AGREEMENT_FIELDS: readonly ['runId', 'contractDigest', 'evaluatorConfigurationDigest'];
|
|
43
|
+
export type AgreementField = (typeof AGREEMENT_FIELDS)[number];
|
|
44
|
+
/**
|
|
45
|
+
* An identifier the record uses twice where everything downstream addresses by
|
|
46
|
+
* it once.
|
|
47
|
+
*
|
|
48
|
+
* `SealedRunRecord` refines exactly one array for uniqueness, and it refines it
|
|
49
|
+
* on `sequence` rather than on an identifier, so all three of `observationId`,
|
|
50
|
+
* `findingId`, and `oracleId` may repeat in a record that parses. Nothing
|
|
51
|
+
* downstream can then tell the pair apart: `verdictBasis` names a finding by its
|
|
52
|
+
* identifier, `FindingConfidence` is keyed by it, `resolveOutcome` reads one
|
|
53
|
+
* disposition per oracle, and `auditQuotation` indexes observations into a `Map`
|
|
54
|
+
* keyed on `observationId`, which is last-write-wins. Recorded rather than
|
|
55
|
+
* repaired, because choosing which of two entries an identifier means is not a
|
|
56
|
+
* choice this stage can make.
|
|
57
|
+
*
|
|
58
|
+
* The observation case is the one with teeth. A repeated `observationId` decides
|
|
59
|
+
* which body a quotation is checked against, so it changes whether a defect
|
|
60
|
+
* finding's evidence is witnessed at all, which is the AD-32
|
|
61
|
+
* declared-versus-observed property this stage exists to check. The other two
|
|
62
|
+
* change an order; this one changes an answer.
|
|
63
|
+
*
|
|
64
|
+
* It is also what makes the product's `findings` and `dispositions` order total,
|
|
65
|
+
* and `unwitnessedQuotations` a function of the record's content, in every
|
|
66
|
+
* record that does not carry this condition.
|
|
67
|
+
*/
|
|
68
|
+
export type DuplicateRecordIdentifier = {
|
|
69
|
+
readonly kind: Extract<IngestConditionKind, 'duplicate-record-identifier'>;
|
|
70
|
+
readonly subject: 'observation' | 'finding' | 'oracle-disposition';
|
|
71
|
+
readonly identifier: string;
|
|
72
|
+
readonly occurrences: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* A finding citing an observation identifier the record does not declare. One
|
|
76
|
+
* per finding, carrying every identifier that resolved to nothing, so a
|
|
77
|
+
* consumer never has to re-derive which half of the citation was bad.
|
|
78
|
+
*/
|
|
79
|
+
export type DanglingCitation = {
|
|
80
|
+
readonly kind: Extract<IngestConditionKind, 'dangling-citation'>;
|
|
81
|
+
readonly findingId: string;
|
|
82
|
+
readonly unresolvedObservationIds: readonly string[];
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A disposition citing an observation identifier the record does not declare.
|
|
86
|
+
*
|
|
87
|
+
* The record's second citation site, and the one nothing else reads. AD-33 wants
|
|
88
|
+
* "every disposition citing supporting observations, and an unsupported
|
|
89
|
+
* disposition invalidating cross-artifact agreement rather than being believed";
|
|
90
|
+
* `resolveOutcome`'s `unsupported-disposition` covers the empty list only, so a
|
|
91
|
+
* `violated` disposition corroborated by a citation to nothing would otherwise
|
|
92
|
+
* be believed. Ingest is the only stage holding the dispositions and the
|
|
93
|
+
* observations at once.
|
|
94
|
+
*/
|
|
95
|
+
export type DanglingDispositionCitation = {
|
|
96
|
+
readonly kind: Extract<IngestConditionKind, 'dangling-disposition-citation'>;
|
|
97
|
+
readonly oracleId: string;
|
|
98
|
+
readonly unresolvedObservationIds: readonly string[];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The quotation audit's own return value, unchanged. `auditQuotation` already
|
|
102
|
+
* shapes the payload its consumer declares, so re-projecting it here would be a
|
|
103
|
+
* second spelling of one result.
|
|
104
|
+
*/
|
|
105
|
+
export type UnwitnessedQuotationCondition = {
|
|
106
|
+
readonly kind: Extract<IngestConditionKind, 'unwitnessed-quotation'>;
|
|
107
|
+
readonly quotations: readonly UnwitnessedQuotation[];
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* AD-16's absent manifest. No payload: there is no artifact to describe, which
|
|
111
|
+
* is why no schema can carry this rule and why the manifest's own `.meta` hands
|
|
112
|
+
* the violating case to `core/ingest`.
|
|
113
|
+
*/
|
|
114
|
+
export type IsolationManifestAbsent = {
|
|
115
|
+
readonly kind: Extract<IngestConditionKind, 'isolation-manifest-absent'>;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* A declared violation, an observed mount, network target, or tool call outside
|
|
119
|
+
* its allowlist, or any combination. One condition rather than four, each
|
|
120
|
+
* exceeded allowlist carrying its own offending values in the manifest's array
|
|
121
|
+
* order, so a consumer reads values rather than parsing them back out of a
|
|
122
|
+
* joined summary.
|
|
123
|
+
*/
|
|
124
|
+
export type IsolationManifestViolation = {
|
|
125
|
+
readonly kind: Extract<IngestConditionKind, 'isolation-manifest-violation'>;
|
|
126
|
+
readonly violation: string | null;
|
|
127
|
+
readonly mountsOutsideAllowlist: readonly string[];
|
|
128
|
+
readonly networkTargetsOutsideAllowlist: readonly string[];
|
|
129
|
+
readonly toolCallsOutsideAllowlist: readonly string[];
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* AD-16's first clause: a prohibited input the manifest accounts for with
|
|
133
|
+
* `withheld: false`. Named in `FORBIDDEN_INPUT_FLOOR` order, since that list is
|
|
134
|
+
* the floor's one home and the manifest's key order is the caller's.
|
|
135
|
+
*/
|
|
136
|
+
export type ForbiddenInputNotWithheld = {
|
|
137
|
+
readonly kind: Extract<IngestConditionKind, 'forbidden-input-not-withheld'>;
|
|
138
|
+
readonly inputs: readonly ForbiddenInput[];
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* One field the two artifacts disagree on, carrying both values. Per field
|
|
142
|
+
* rather than per record: a run whose two artifacts disagree about which
|
|
143
|
+
* configuration produced it is a different finding from one that disagrees
|
|
144
|
+
* about which run it is.
|
|
145
|
+
*/
|
|
146
|
+
export type CrossArtifactDisagreement = {
|
|
147
|
+
readonly kind: Extract<IngestConditionKind, 'cross-artifact-disagreement'>;
|
|
148
|
+
readonly field: AgreementField;
|
|
149
|
+
readonly recordValue: string;
|
|
150
|
+
readonly manifestValue: string;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* AD-24's absent evaluator configuration. No payload, for the same reason
|
|
154
|
+
* `IsolationManifestAbsent` carries none: there is no artifact to describe.
|
|
155
|
+
*/
|
|
156
|
+
export type EvaluatorConfigurationAbsent = {
|
|
157
|
+
readonly kind: Extract<IngestConditionKind, 'evaluator-configuration-absent'>;
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* The digest the record declares for the evaluator configuration against the
|
|
161
|
+
* one that artifact actually produces.
|
|
162
|
+
*
|
|
163
|
+
* The operand is the record's declaration. A disagreement between the record's
|
|
164
|
+
* and the manifest's declarations is already `cross-artifact-disagreement`, and
|
|
165
|
+
* what this condition adds is the half AD-32 calls the trust boundary: both
|
|
166
|
+
* declarations are caller-attested, and only the recomputation reads the
|
|
167
|
+
* artifact.
|
|
168
|
+
*/
|
|
169
|
+
export type EvaluatorConfigurationDigestMismatch = {
|
|
170
|
+
readonly kind: Extract<IngestConditionKind, 'evaluator-configuration-digest-mismatch'>;
|
|
171
|
+
readonly declaredDigest: string;
|
|
172
|
+
readonly computedDigest: string;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* AD-17's record-decidable half: a judge result carrying `score: null`. The
|
|
176
|
+
* other half, that the criterion is one the cited rubric declares, needs the
|
|
177
|
+
* rubric, which no stage row declares as an input.
|
|
178
|
+
*/
|
|
179
|
+
export type JudgeResultUnscored = {
|
|
180
|
+
readonly kind: Extract<IngestConditionKind, 'judge-result-unscored'>;
|
|
181
|
+
readonly rubricId: string;
|
|
182
|
+
readonly criterionId: string;
|
|
183
|
+
};
|
|
184
|
+
export type IngestCondition = DuplicateRecordIdentifier | DanglingCitation | DanglingDispositionCitation | UnwitnessedQuotationCondition | IsolationManifestAbsent | IsolationManifestViolation | ForbiddenInputNotWithheld | CrossArtifactDisagreement | EvaluatorConfigurationAbsent | EvaluatorConfigurationDigestMismatch | JudgeResultUnscored;
|
|
185
|
+
/**
|
|
186
|
+
* The ladder field a condition feeds. Every one of the eleven kinds now
|
|
187
|
+
* carries a rung, so the union below names all ten `EvidenceIntegrityInputs`
|
|
188
|
+
* fields that carry an ingest condition and `null` is dropped entirely: a
|
|
189
|
+
* kind with no rung is a compile error now, not a documented gap.
|
|
190
|
+
*
|
|
191
|
+
* Ten pairs built with `Extract` rather than a bare `keyof` product: the
|
|
192
|
+
* product would also pull in fields like `internallyInconsistent`, whose row
|
|
193
|
+
* is FAIL and whose guard reads "internally inconsistent under AD-17" rather
|
|
194
|
+
* than reading an `IngestCondition`, and a bare key would lose which of the
|
|
195
|
+
* two input types a member came from. Renaming a field in `ladder.ts`
|
|
196
|
+
* collapses its branch here to `never`, so the mapping below stops compiling
|
|
197
|
+
* instead of pointing at a field that no longer exists.
|
|
198
|
+
*/
|
|
199
|
+
export type LadderTarget = Extract<keyof EvidenceIntegrityInputs, 'isolationViolation'> | Extract<keyof OutcomeStateInputs, 'unwitnessedQuotations'> | Extract<keyof EvidenceIntegrityInputs, 'duplicateRecordIdentifiers'> | Extract<keyof EvidenceIntegrityInputs, 'danglingCitations'> | Extract<keyof EvidenceIntegrityInputs, 'danglingDispositionCitations'> | Extract<keyof EvidenceIntegrityInputs, 'forbiddenInputsNotWithheld'> | Extract<keyof EvidenceIntegrityInputs, 'crossArtifactDisagreements'> | Extract<keyof EvidenceIntegrityInputs, 'evaluatorConfigurationAbsent'> | Extract<keyof EvidenceIntegrityInputs, 'evaluatorConfigurationDigestMismatches'> | Extract<keyof EvidenceIntegrityInputs, 'judgeResultsUnscored'>;
|
|
200
|
+
/** Total over the kinds tuple: a new kind fails to compile until it declares its rung. */
|
|
201
|
+
export declare const LADDER_TARGETS: Record<IngestConditionKind, LadderTarget>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eleven kinds, in the order `ingest` records them. The two isolation-family
|
|
3
|
+
* kinds and the two evaluator-configuration ones are sequenced here; the order
|
|
4
|
+
* of the values inside one `isolation-manifest-violation` is the manifest's own
|
|
5
|
+
* array order and belongs to the stage, not to this tuple.
|
|
6
|
+
*/
|
|
7
|
+
export const INGEST_CONDITION_KINDS = [
|
|
8
|
+
'duplicate-record-identifier',
|
|
9
|
+
'dangling-citation',
|
|
10
|
+
'dangling-disposition-citation',
|
|
11
|
+
'unwitnessed-quotation',
|
|
12
|
+
'isolation-manifest-absent',
|
|
13
|
+
'isolation-manifest-violation',
|
|
14
|
+
'forbidden-input-not-withheld',
|
|
15
|
+
'cross-artifact-disagreement',
|
|
16
|
+
'evaluator-configuration-absent',
|
|
17
|
+
'evaluator-configuration-digest-mismatch',
|
|
18
|
+
'judge-result-unscored',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* The three fields AD-32 requires the sealed run record and the isolation
|
|
22
|
+
* manifest to agree on, in the order a disagreement is reported.
|
|
23
|
+
*
|
|
24
|
+
* `conditionArm` sits on both artifacts and is deliberately excluded: both
|
|
25
|
+
* schemas describe it as "an opaque caller label with no product semantics, per
|
|
26
|
+
* AD-24", so making a mismatch invalidate would mint a normative rule with no
|
|
27
|
+
* AD behind it and would be the one invalidating condition in the design with
|
|
28
|
+
* nothing downstream reading it.
|
|
29
|
+
*/
|
|
30
|
+
export const AGREEMENT_FIELDS = [
|
|
31
|
+
'runId',
|
|
32
|
+
'contractDigest',
|
|
33
|
+
'evaluatorConfigurationDigest',
|
|
34
|
+
];
|
|
35
|
+
/** Total over the kinds tuple: a new kind fails to compile until it declares its rung. */
|
|
36
|
+
export const LADDER_TARGETS = {
|
|
37
|
+
'duplicate-record-identifier': 'duplicateRecordIdentifiers',
|
|
38
|
+
'dangling-citation': 'danglingCitations',
|
|
39
|
+
'dangling-disposition-citation': 'danglingDispositionCitations',
|
|
40
|
+
'unwitnessed-quotation': 'unwitnessedQuotations',
|
|
41
|
+
'isolation-manifest-absent': 'isolationViolation',
|
|
42
|
+
'isolation-manifest-violation': 'isolationViolation',
|
|
43
|
+
'forbidden-input-not-withheld': 'forbiddenInputsNotWithheld',
|
|
44
|
+
'cross-artifact-disagreement': 'crossArtifactDisagreements',
|
|
45
|
+
'evaluator-configuration-absent': 'evaluatorConfigurationAbsent',
|
|
46
|
+
'evaluator-configuration-digest-mismatch': 'evaluatorConfigurationDigestMismatches',
|
|
47
|
+
'judge-result-unscored': 'judgeResultsUnscored',
|
|
48
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ingest stage's surface for the rest of `core/`: the stage itself, the
|
|
3
|
+
* product it owns, the condition vocabulary, and the ladder input each
|
|
4
|
+
* condition feeds.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here reaches `src/index.ts` or `src/application/index.ts`.
|
|
7
|
+
* `validated-observations` is an internal stage product AD-24 exempts from
|
|
8
|
+
* publication, so it has no schema, no registry entry, and no published type.
|
|
9
|
+
*/
|
|
10
|
+
export type { AgreementField, CrossArtifactDisagreement, DanglingCitation, DanglingDispositionCitation, DuplicateRecordIdentifier, EvaluatorConfigurationAbsent, EvaluatorConfigurationDigestMismatch, ForbiddenInputNotWithheld, IngestCondition, IngestConditionKind, IsolationManifestAbsent, IsolationManifestViolation, JudgeResultUnscored, LadderTarget, UnwitnessedQuotationCondition, } from './conditions.ts';
|
|
11
|
+
export { AGREEMENT_FIELDS, INGEST_CONDITION_KINDS, LADDER_TARGETS, } from './conditions.ts';
|
|
12
|
+
export type { ValidatedObservations } from './ingest.ts';
|
|
13
|
+
export { ingest } from './ingest.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ingest stage's surface for the rest of `core/`: the stage itself, the
|
|
3
|
+
* product it owns, the condition vocabulary, and the ladder input each
|
|
4
|
+
* condition feeds.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here reaches `src/index.ts` or `src/application/index.ts`.
|
|
7
|
+
* `validated-observations` is an internal stage product AD-24 exempts from
|
|
8
|
+
* publication, so it has no schema, no registry entry, and no published type.
|
|
9
|
+
*/
|
|
10
|
+
export { AGREEMENT_FIELDS, INGEST_CONDITION_KINDS, LADDER_TARGETS, } from './conditions.js';
|
|
11
|
+
export { ingest } from './ingest.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Observation, OracleDisposition, RunModeValue, SealedRunRecord } from '../schemas/sealed-run-record.ts';
|
|
2
|
+
import type { EvaluatorRecommendation } from '../schemas/verdict.ts';
|
|
3
|
+
import { type UnwitnessedQuotation } from '../score/quotation.ts';
|
|
4
|
+
import type { IngestStage } from '../stage-contracts.ts';
|
|
5
|
+
import { type IngestCondition } from './conditions.ts';
|
|
6
|
+
/**
|
|
7
|
+
* `validated-observations`, the internal product AD-24 exempts from
|
|
8
|
+
* publication: a TypeScript type with no Zod schema, no registry entry, and no
|
|
9
|
+
* `schemaVersion`.
|
|
10
|
+
*
|
|
11
|
+
* `unwitnessedQuotations` and `isolationViolation` are derived here rather than
|
|
12
|
+
* left for the next stage to rebuild out of condition entries, because both are
|
|
13
|
+
* shapes the ladder already declares.
|
|
14
|
+
*
|
|
15
|
+
* The three carried arrays are copies and their elements are not: an
|
|
16
|
+
* observation, a finding, or a disposition on this product is the same object
|
|
17
|
+
* the record holds. The stage is pure and mutates nothing, so a caller that also
|
|
18
|
+
* does not mutate the record it passed in sees a stable product; deep-copying
|
|
19
|
+
* every observation to defend against a caller mutating its own input is a cost
|
|
20
|
+
* no consumer in this package asks for. `isolationViolation` is a list where
|
|
21
|
+
* `EvidenceIntegrityInputs` still declares one nullable string: the ladder's own
|
|
22
|
+
* row type says a multi-entry `reasons` "names each affected oracle, gap, or
|
|
23
|
+
* finding separately, matching AD-21's 'the record carries every condition that
|
|
24
|
+
* fired'", so the single-valued field is the shape that is wrong. Collapsing to
|
|
25
|
+
* it here would mean inventing a separator and an ordering rule that the field's
|
|
26
|
+
* widening then deletes.
|
|
27
|
+
*/
|
|
28
|
+
export type ValidatedObservations = {
|
|
29
|
+
/** `record.runId`, restated for the same reason `mode` is: `score.ts` reads it off the first trial to build `emit`'s own `runId` field, and no later stage may re-derive or default it. */
|
|
30
|
+
readonly runId: string;
|
|
31
|
+
readonly mode: RunModeValue;
|
|
32
|
+
/**
|
|
33
|
+
* `AssessmentCommon.evaluatorRecommendation` (`score/ladder.ts:107`) is
|
|
34
|
+
* required and two shared ladder rows read it directly; nothing before
|
|
35
|
+
* this field supplied it to anything score-shaped. Read off
|
|
36
|
+
* `record.evaluatorRecommendation` the same way `mode` already is above:
|
|
37
|
+
* restated, never derived, recomputed, or defaulted.
|
|
38
|
+
*/
|
|
39
|
+
readonly evaluatorRecommendation: EvaluatorRecommendation;
|
|
40
|
+
/** ascending `sequence`, then `observationId`, so the record's array order is never what a consumer reads. */
|
|
41
|
+
readonly observations: readonly Observation[];
|
|
42
|
+
/** ascending `findingId`; entries sharing one keep their presented order, which is all the record supplies. */
|
|
43
|
+
readonly findings: readonly SealedRunRecord['findings'][number][];
|
|
44
|
+
/** ascending `oracleId`, on the same terms as `findings`. */
|
|
45
|
+
readonly dispositions: readonly OracleDisposition[];
|
|
46
|
+
readonly conditions: readonly IngestCondition[];
|
|
47
|
+
readonly unwitnessedQuotations: readonly UnwitnessedQuotation[];
|
|
48
|
+
/** empty when nothing fired; one entry per offending value, never a joined summary. */
|
|
49
|
+
readonly isolationViolation: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The stage. Reads nothing outside its three declared inputs: a rule needing
|
|
53
|
+
* the eval contract, the rubric, or bytes resolved through the corpus port is
|
|
54
|
+
* routed to whoever declares them rather than absorbed here on the strength of
|
|
55
|
+
* being nearby.
|
|
56
|
+
*
|
|
57
|
+
* Two rules read the evaluator configuration and they are separate. AD-32's
|
|
58
|
+
* agreement rule compares the digest the record declares against the one the
|
|
59
|
+
* manifest declares, which is `cross-artifact-disagreement` and never reads the
|
|
60
|
+
* artifact. AD-24 and AD-11 require the digest to be recomputed from the
|
|
61
|
+
* artifact, which is the only one of the three that is not caller-attested and
|
|
62
|
+
* is why the artifact is a declared input at all.
|
|
63
|
+
*
|
|
64
|
+
* AD-24's "absent or incomplete" splits the same way the manifest's does:
|
|
65
|
+
* incomplete is a schema rejection the application boundary already raises, and
|
|
66
|
+
* absent is a shape no schema can describe, so the parameter admits `null`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const ingest: IngestStage<ValidatedObservations>;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-24's ingest stage: three already-parsed artifacts in, the validated
|
|
3
|
+
* observations plus every condition detected out.
|
|
4
|
+
*
|
|
5
|
+
* The organizing rule is the isolation manifest's own `.meta` sentence, "a
|
|
6
|
+
* schema rejection is the correct expression of unparseable and incomplete, so
|
|
7
|
+
* nothing is admitted for their sake, and the violating case is
|
|
8
|
+
* `core/ingest`'s". Bytes that will not parse are the application layer's to
|
|
9
|
+
* reject; bytes that parse and then contradict each other are a finding about
|
|
10
|
+
* the run. So a detected problem is returned as data and nothing here throws.
|
|
11
|
+
*
|
|
12
|
+
* One fault does propagate and is named, from two call sites into the same
|
|
13
|
+
* module. `auditQuotation` canonicalizes the three structured evidence
|
|
14
|
+
* channels, and the evaluator configuration's digest is recomputed by
|
|
15
|
+
* canonicalizing the whole artifact, so an unsafe integer or a lone surrogate in
|
|
16
|
+
* either raises AD-28's `non-canonicalizable-value` out of `core/canonical`. It
|
|
17
|
+
* is neither caught nor re-raised here, which is what lets a caller tell an
|
|
18
|
+
* invalid run from a crashed one.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here re-derives what `resolveOutcome` already decides. Its ten
|
|
21
|
+
* invalidating conditions are per oracle and reach the ladder through
|
|
22
|
+
* `outcome.resolution.invalidatingConditions`; these eleven are per record and
|
|
23
|
+
* reach it through `EvidenceIntegrityInputs` and `OutcomeStateInputs`. Two
|
|
24
|
+
* pairs are related rather than identical: `judge-result-unscored` is the
|
|
25
|
+
* derivation `OutcomeInputs.judgeConduct` has never had, and a
|
|
26
|
+
* `dangling-citation` and an `unwitnessed-detection-claim` can both fire on one
|
|
27
|
+
* finding, in which case the basis names both and the rung is the same either
|
|
28
|
+
* way.
|
|
29
|
+
*/
|
|
30
|
+
import { digestArtifact } from '../canonical/digest.js';
|
|
31
|
+
import { FORBIDDEN_INPUT_FLOOR } from '../schemas/eval-contract.js';
|
|
32
|
+
import { auditQuotation, } from '../score/quotation.js';
|
|
33
|
+
import { AGREEMENT_FIELDS } from './conditions.js';
|
|
34
|
+
/**
|
|
35
|
+
* Observed values with no entry in their allowlist, in the manifest's own array
|
|
36
|
+
* order and each named once. A value observed twice outside its allowlist is one
|
|
37
|
+
* value outside its allowlist; the payload is what a consumer renders, and
|
|
38
|
+
* `isolationViolation` is specified as one entry per offending value.
|
|
39
|
+
*/
|
|
40
|
+
const outsideAllowlist = (observed, allowed) => {
|
|
41
|
+
const permitted = new Set(allowed);
|
|
42
|
+
return [...new Set(observed)].filter((value) => !permitted.has(value));
|
|
43
|
+
};
|
|
44
|
+
/** The path AD-27 digests the evaluator configuration under, matching `seal.ts`'s treatment of the contract. */
|
|
45
|
+
const EVALUATOR_CONFIGURATION_ARTIFACT_PATH = 'EvaluatorConfiguration';
|
|
46
|
+
/**
|
|
47
|
+
* A total order over the two condition families whose source array declares no
|
|
48
|
+
* order of its own. A finding carries no sequence field and a judge result is
|
|
49
|
+
* addressed by its `(rubricId, criterionId)` pair, so reading either in array
|
|
50
|
+
* position would make the recorded conditions depend on a position NFR9 forbids
|
|
51
|
+
* reading; `auditQuotation` sorts its own result for the same reason.
|
|
52
|
+
*
|
|
53
|
+
* Keyed on the whole payload rather than on the identifier alone. `observations`
|
|
54
|
+
* is the only one of the three arrays the record schema refines for uniqueness,
|
|
55
|
+
* so a repeated `findingId` and a repeated `(rubricId, criterionId)` are both
|
|
56
|
+
* representable, and a comparator keyed on the identifier would leave the tied
|
|
57
|
+
* pair ordered by the array position the sort exists to stop reading. Entries
|
|
58
|
+
* that still tie here are equal values, so the order between them cannot be
|
|
59
|
+
* observed.
|
|
60
|
+
*
|
|
61
|
+
* Joined on U+0000 rather than compared position by position. Every identifier
|
|
62
|
+
* in a key is drawn from `IDENTIFIER_SOURCE` or a `X-digits` prefix form, whose
|
|
63
|
+
* lowest code point is U+002D, so the separator sorts below every character a
|
|
64
|
+
* segment can carry and the joined comparison is the element-wise one.
|
|
65
|
+
*/
|
|
66
|
+
const compareKeys = (left, right) => {
|
|
67
|
+
const a = left.join('\u0000');
|
|
68
|
+
const b = right.join('\u0000');
|
|
69
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The stage. Reads nothing outside its three declared inputs: a rule needing
|
|
73
|
+
* the eval contract, the rubric, or bytes resolved through the corpus port is
|
|
74
|
+
* routed to whoever declares them rather than absorbed here on the strength of
|
|
75
|
+
* being nearby.
|
|
76
|
+
*
|
|
77
|
+
* Two rules read the evaluator configuration and they are separate. AD-32's
|
|
78
|
+
* agreement rule compares the digest the record declares against the one the
|
|
79
|
+
* manifest declares, which is `cross-artifact-disagreement` and never reads the
|
|
80
|
+
* artifact. AD-24 and AD-11 require the digest to be recomputed from the
|
|
81
|
+
* artifact, which is the only one of the three that is not caller-attested and
|
|
82
|
+
* is why the artifact is a declared input at all.
|
|
83
|
+
*
|
|
84
|
+
* AD-24's "absent or incomplete" splits the same way the manifest's does:
|
|
85
|
+
* incomplete is a schema rejection the application boundary already raises, and
|
|
86
|
+
* absent is a shape no schema can describe, so the parameter admits `null`.
|
|
87
|
+
*/
|
|
88
|
+
export const ingest = (record, manifest, configuration) => {
|
|
89
|
+
const conditions = [];
|
|
90
|
+
// Prior to every other check: a record that uses one identifier twice cannot
|
|
91
|
+
// address its own entries, and every consumer downstream addresses by them.
|
|
92
|
+
// Reported before the conditions that name one, so a reader meets the
|
|
93
|
+
// ambiguity before a condition that relies on it.
|
|
94
|
+
//
|
|
95
|
+
// Observations lead because a repeat there changes an answer rather than an
|
|
96
|
+
// order: `auditQuotation` indexes them into a `Map` keyed on `observationId`,
|
|
97
|
+
// so the last entry with a repeated identifier wins and decides which body a
|
|
98
|
+
// quotation is checked against.
|
|
99
|
+
for (const [subject, identifiers] of [
|
|
100
|
+
[
|
|
101
|
+
'observation',
|
|
102
|
+
record.observations.map((observation) => observation.observationId),
|
|
103
|
+
],
|
|
104
|
+
['finding', record.findings.map((finding) => finding.findingId)],
|
|
105
|
+
[
|
|
106
|
+
'oracle-disposition',
|
|
107
|
+
record.oracleDispositions.map((disposition) => disposition.oracleId),
|
|
108
|
+
],
|
|
109
|
+
]) {
|
|
110
|
+
const counts = new Map();
|
|
111
|
+
for (const identifier of identifiers) {
|
|
112
|
+
counts.set(identifier, (counts.get(identifier) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
const repeated = [...counts.entries()]
|
|
115
|
+
.filter(([, occurrences]) => occurrences > 1)
|
|
116
|
+
.sort((a, b) => compareKeys([a[0]], [b[0]]));
|
|
117
|
+
for (const [identifier, occurrences] of repeated) {
|
|
118
|
+
conditions.push({
|
|
119
|
+
kind: 'duplicate-record-identifier',
|
|
120
|
+
subject,
|
|
121
|
+
identifier,
|
|
122
|
+
occurrences,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// A cited identifier matching a declared observation is the cross-artifact
|
|
127
|
+
// rule `observationIds` leaves to this stage. Declared on every finding
|
|
128
|
+
// branch, so the check is too: AD-23's word is "additionally", a floor on
|
|
129
|
+
// `defect` rather than a prohibition on the other two.
|
|
130
|
+
const declared = new Set(record.observations.map((observation) => observation.observationId));
|
|
131
|
+
const dangling = record.findings
|
|
132
|
+
.map((finding) => ({
|
|
133
|
+
findingId: finding.findingId,
|
|
134
|
+
// Deduplicated first, in first-citation order: the condition names
|
|
135
|
+
// which identifiers resolved to nothing, and a finding citing one
|
|
136
|
+
// twice has one bad citation to report rather than two.
|
|
137
|
+
unresolvedObservationIds: [...new Set(finding.observationIds)].filter((observationId) => !declared.has(observationId)),
|
|
138
|
+
}))
|
|
139
|
+
.filter((entry) => entry.unresolvedObservationIds.length > 0)
|
|
140
|
+
.sort((a, b) => compareKeys([a.findingId, ...a.unresolvedObservationIds], [b.findingId, ...b.unresolvedObservationIds]));
|
|
141
|
+
for (const entry of dangling) {
|
|
142
|
+
conditions.push({ kind: 'dangling-citation', ...entry });
|
|
143
|
+
}
|
|
144
|
+
// The same rule over the record's second citation site. `resolveOutcome`
|
|
145
|
+
// reads a disposition's `observationIds` only for emptiness, so a `violated`
|
|
146
|
+
// disposition whose one citation names nothing would otherwise be believed,
|
|
147
|
+
// which is the shape AD-33 says invalidates rather than being believed.
|
|
148
|
+
const danglingDispositions = record.oracleDispositions
|
|
149
|
+
.map((disposition) => ({
|
|
150
|
+
oracleId: disposition.oracleId,
|
|
151
|
+
unresolvedObservationIds: [...new Set(disposition.observationIds)].filter((observationId) => !declared.has(observationId)),
|
|
152
|
+
}))
|
|
153
|
+
.filter((entry) => entry.unresolvedObservationIds.length > 0)
|
|
154
|
+
.sort((a, b) => compareKeys([a.oracleId, ...a.unresolvedObservationIds], [b.oracleId, ...b.unresolvedObservationIds]));
|
|
155
|
+
for (const entry of danglingDispositions) {
|
|
156
|
+
conditions.push({ kind: 'dangling-disposition-citation', ...entry });
|
|
157
|
+
}
|
|
158
|
+
// ADR-009 Decision 2's precedence: cited identifiers govern the witness
|
|
159
|
+
// match and quotation audits it. This is that audit's caller.
|
|
160
|
+
const unwitnessedQuotations = auditQuotation(record);
|
|
161
|
+
if (unwitnessedQuotations.length > 0) {
|
|
162
|
+
conditions.push({
|
|
163
|
+
kind: 'unwitnessed-quotation',
|
|
164
|
+
quotations: unwitnessedQuotations,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const isolationViolation = [];
|
|
168
|
+
if (manifest === null) {
|
|
169
|
+
// AD-16's absent case. The agreement and forbidden-input checks read
|
|
170
|
+
// manifest fields and cannot run; every record-internal check still does,
|
|
171
|
+
// so a run that is both manifest-less and internally inconsistent reports
|
|
172
|
+
// both rather than only the first.
|
|
173
|
+
conditions.push({ kind: 'isolation-manifest-absent' });
|
|
174
|
+
isolationViolation.push('isolation manifest absent');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const mounts = outsideAllowlist(manifest.observedMounts, manifest.allowedMounts);
|
|
178
|
+
const networkTargets = outsideAllowlist(manifest.observedNetworkTargets, manifest.networkAllowlist);
|
|
179
|
+
const toolCalls = outsideAllowlist(manifest.observedToolCalls, manifest.toolAllowlist);
|
|
180
|
+
// One count over the three allowlists. The condition is "some observed
|
|
181
|
+
// value sits outside its allowlist", and asking it as three
|
|
182
|
+
// short-circuiting disjuncts would leave two of them evaluated in one
|
|
183
|
+
// direction only.
|
|
184
|
+
const exceeded = mounts.length + networkTargets.length + toolCalls.length;
|
|
185
|
+
if (manifest.violation !== null || exceeded > 0) {
|
|
186
|
+
conditions.push({
|
|
187
|
+
kind: 'isolation-manifest-violation',
|
|
188
|
+
violation: manifest.violation,
|
|
189
|
+
mountsOutsideAllowlist: mounts,
|
|
190
|
+
networkTargetsOutsideAllowlist: networkTargets,
|
|
191
|
+
toolCallsOutsideAllowlist: toolCalls,
|
|
192
|
+
});
|
|
193
|
+
// The declared violation first, then mounts, network targets, and tool
|
|
194
|
+
// calls, each in the manifest's own array order and one entry per
|
|
195
|
+
// offending value.
|
|
196
|
+
if (manifest.violation !== null) {
|
|
197
|
+
isolationViolation.push(`manifest violation: ${manifest.violation}`);
|
|
198
|
+
}
|
|
199
|
+
for (const value of mounts) {
|
|
200
|
+
isolationViolation.push(`mount outside allowlist: ${value}`);
|
|
201
|
+
}
|
|
202
|
+
for (const value of networkTargets) {
|
|
203
|
+
isolationViolation.push(`network target outside allowlist: ${value}`);
|
|
204
|
+
}
|
|
205
|
+
for (const value of toolCalls) {
|
|
206
|
+
isolationViolation.push(`tool call outside allowlist: ${value}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// AD-16's "account for each forbidden input by name". Read off the floor
|
|
210
|
+
// rather than off the manifest's own key order, so the seven are named in
|
|
211
|
+
// the order the list that generates them declares.
|
|
212
|
+
const admitted = FORBIDDEN_INPUT_FLOOR.filter((input) => !manifest.forbiddenInputAccounting[input].withheld);
|
|
213
|
+
if (admitted.length > 0) {
|
|
214
|
+
conditions.push({
|
|
215
|
+
kind: 'forbidden-input-not-withheld',
|
|
216
|
+
inputs: admitted,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// AD-32's agreement rule. Both artifacts carry all three fields, so the
|
|
220
|
+
// comparison is between them and never against the configuration artifact
|
|
221
|
+
// standing behind the digest.
|
|
222
|
+
for (const field of AGREEMENT_FIELDS) {
|
|
223
|
+
if (record[field] === manifest[field])
|
|
224
|
+
continue;
|
|
225
|
+
conditions.push({
|
|
226
|
+
kind: 'cross-artifact-disagreement',
|
|
227
|
+
field,
|
|
228
|
+
recordValue: record[field],
|
|
229
|
+
manifestValue: manifest[field],
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// AD-24: "ingest computes its digest from the artifact and invalidates the
|
|
234
|
+
// run when it is absent or incomplete", restated by AD-11 as the one
|
|
235
|
+
// scoring-version input this stage recomputes. The record's declaration is
|
|
236
|
+
// the operand because AD-32 puts the trust boundary there: a caller that
|
|
237
|
+
// declares one configuration and runs another leaves the two declarations
|
|
238
|
+
// agreeing and only the recomputation disagreeing.
|
|
239
|
+
if (configuration === null) {
|
|
240
|
+
conditions.push({ kind: 'evaluator-configuration-absent' });
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const computedDigest = digestArtifact(configuration, EVALUATOR_CONFIGURATION_ARTIFACT_PATH);
|
|
244
|
+
if (computedDigest !== record.evaluatorConfigurationDigest) {
|
|
245
|
+
conditions.push({
|
|
246
|
+
kind: 'evaluator-configuration-digest-mismatch',
|
|
247
|
+
declaredDigest: record.evaluatorConfigurationDigest,
|
|
248
|
+
computedDigest,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// AD-17's record-decidable half. `null` is the shape AD-6's `judge-error`
|
|
253
|
+
// fires on, which is why the schema must parse it and why this is a condition
|
|
254
|
+
// rather than a parse failure.
|
|
255
|
+
//
|
|
256
|
+
// Deduplicated by the pair that addresses the result: `judgeResults` carries
|
|
257
|
+
// no uniqueness refinement, so one criterion may appear twice, and two
|
|
258
|
+
// byte-identical conditions naming one criterion are one finding a basis line
|
|
259
|
+
// can act on rather than two.
|
|
260
|
+
//
|
|
261
|
+
// The two citation families deduplicate identifiers inside an entry and never
|
|
262
|
+
// the entries themselves, which is the opposite answer to the same shape and
|
|
263
|
+
// is deliberate. A repeated `(rubricId, criterionId)` is one criterion scored
|
|
264
|
+
// twice, so the second entry names nothing new. Two findings sharing a
|
|
265
|
+
// `findingId` are two findings the record cannot address apart, and their
|
|
266
|
+
// citations may differ, so collapsing them would drop a bad citation on the
|
|
267
|
+
// floor.
|
|
268
|
+
const unscored = [
|
|
269
|
+
...new Map(record.judgeResults
|
|
270
|
+
.filter((result) => result.score === null)
|
|
271
|
+
.map((result) => [
|
|
272
|
+
`${result.rubricId}/${result.criterionId}`,
|
|
273
|
+
{ rubricId: result.rubricId, criterionId: result.criterionId },
|
|
274
|
+
])).values(),
|
|
275
|
+
].sort((a, b) => compareKeys([a.rubricId, a.criterionId], [b.rubricId, b.criterionId]));
|
|
276
|
+
for (const entry of unscored) {
|
|
277
|
+
conditions.push({ kind: 'judge-result-unscored', ...entry });
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
// Read off the record and restated, never derived, recomputed, or
|
|
281
|
+
// defaulted, the same posture `mode` below takes.
|
|
282
|
+
runId: record.runId,
|
|
283
|
+
// Read off the record and restated, never derived, recomputed, or
|
|
284
|
+
// defaulted: AD-21 fixes mode before ingest and there is no fourth
|
|
285
|
+
// parameter for a caller to disagree with it through.
|
|
286
|
+
mode: record.mode,
|
|
287
|
+
evaluatorRecommendation: record.evaluatorRecommendation,
|
|
288
|
+
// `selectObservations`' own sort, matched exactly so two readers of one
|
|
289
|
+
// record cannot disagree about which observation came first. The
|
|
290
|
+
// identifier tie-break cannot fire on a record that parsed, since
|
|
291
|
+
// `sequence` is refined unique per record; it is kept for comparator
|
|
292
|
+
// identity with the shipped sort rather than because the case is real.
|
|
293
|
+
observations: [...record.observations].sort((a, b) => a.sequence - b.sequence || (a.observationId < b.observationId ? -1 : 1)),
|
|
294
|
+
// Sorted for the same reason the conditions are: neither array declares an
|
|
295
|
+
// order, so carrying the record's would make the product depend on a
|
|
296
|
+
// position nothing may read. The identifier is a total key on any record
|
|
297
|
+
// that does not repeat one, and a record that does repeats it into a
|
|
298
|
+
// `duplicate-record-identifier` condition, so the one input whose product
|
|
299
|
+
// still varies with presentation order is one the run already reports as
|
|
300
|
+
// unaddressable. Ordering that pair by content instead would mean
|
|
301
|
+
// canonicalizing every finding, which buys determinism for a record
|
|
302
|
+
// already flagged at the cost of making `non-canonicalizable-value`
|
|
303
|
+
// reachable from a field no quotation cites.
|
|
304
|
+
findings: [...record.findings].sort((a, b) => compareKeys([a.findingId], [b.findingId])),
|
|
305
|
+
dispositions: [...record.oracleDispositions].sort((a, b) => compareKeys([a.oracleId], [b.oracleId])),
|
|
306
|
+
conditions,
|
|
307
|
+
unwitnessedQuotations,
|
|
308
|
+
isolationViolation,
|
|
309
|
+
};
|
|
310
|
+
};
|