eval-quality 0.1.0 → 0.3.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.
Files changed (179) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +20 -13
  3. package/corpus/dev/compile-seal-example/brief.json +1 -1
  4. package/corpus/dev/compile-seal-example/contract.json +1 -1
  5. package/corpus/dev/contracts/absent-collection-locations.json +1 -1
  6. package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
  7. package/corpus/dev/contracts/absent-success-indicator.json +1 -1
  8. package/corpus/dev/contracts/empty-channel-roles.json +1 -1
  9. package/corpus/dev/contracts/empty-collection-locations.json +1 -1
  10. package/corpus/dev/contracts/empty-request-shapes.json +1 -1
  11. package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
  12. package/corpus/dev/contracts/fragment-selection.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  19. package/corpus/dev/contracts/review-corpus.json +1 -0
  20. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  21. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  22. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  23. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  24. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  25. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  26. package/corpus/dev/index.json +1 -1
  27. package/dist/application/index.d.ts +2 -0
  28. package/dist/application/index.js +1 -0
  29. package/dist/application/score.d.ts +48 -0
  30. package/dist/application/score.js +186 -0
  31. package/dist/cli/arguments.d.ts +7 -3
  32. package/dist/cli/arguments.js +73 -5
  33. package/dist/cli/exit-codes.d.ts +6 -2
  34. package/dist/cli/exit-codes.js +8 -9
  35. package/dist/cli/main.js +9 -0
  36. package/dist/cli/render.d.ts +3 -6
  37. package/dist/cli/render.js +57 -5
  38. package/dist/cli/run.d.ts +13 -3
  39. package/dist/cli/run.js +136 -16
  40. package/dist/core/canonical/canonicalize.d.ts +12 -0
  41. package/dist/core/canonical/canonicalize.js +18 -4
  42. package/dist/core/compile/bindings.d.ts +89 -0
  43. package/dist/core/compile/bindings.js +334 -0
  44. package/dist/core/compile/compile.d.ts +17 -0
  45. package/dist/core/compile/compile.js +37 -1
  46. package/dist/core/compile/excluded-content.d.ts +11 -0
  47. package/dist/core/compile/excluded-content.js +42 -0
  48. package/dist/core/compile/expression-legality.d.ts +49 -0
  49. package/dist/core/compile/expression-legality.js +158 -38
  50. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  51. package/dist/core/compile/forbidden-inputs.js +16 -3
  52. package/dist/core/compile/interface-inventory.d.ts +57 -2
  53. package/dist/core/compile/interface-inventory.js +119 -13
  54. package/dist/core/compile/reachability.d.ts +28 -1
  55. package/dist/core/compile/reachability.js +167 -35
  56. package/dist/core/compile/schema-version.d.ts +2 -0
  57. package/dist/core/compile/schema-version.js +25 -0
  58. package/dist/core/compile/sensitivity-witness.d.ts +31 -12
  59. package/dist/core/compile/sensitivity-witness.js +110 -23
  60. package/dist/core/compile/step-reference.d.ts +2 -0
  61. package/dist/core/compile/step-reference.js +49 -0
  62. package/dist/core/coverage/operations.d.ts +62 -0
  63. package/dist/core/coverage/operations.js +57 -0
  64. package/dist/core/coverage/relevance.d.ts +4 -2
  65. package/dist/core/coverage/relevance.js +22 -23
  66. package/dist/core/coverage/satisfaction.d.ts +2 -2
  67. package/dist/core/coverage/satisfaction.js +73 -48
  68. package/dist/core/declared-inputs.d.ts +83 -4
  69. package/dist/core/declared-inputs.js +105 -8
  70. package/dist/core/emit/emit.d.ts +9 -0
  71. package/dist/core/emit/emit.js +214 -0
  72. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  73. package/dist/core/emit/private-artifact-digest.js +31 -0
  74. package/dist/core/evaluate/evidence-resolution.d.ts +10 -3
  75. package/dist/core/evaluate/evidence-resolution.js +74 -11
  76. package/dist/core/evaluate/operators.d.ts +22 -0
  77. package/dist/core/evaluate/operators.js +17 -1
  78. package/dist/core/evaluate/resolution.d.ts +14 -3
  79. package/dist/core/evaluate/resolution.js +63 -3
  80. package/dist/core/excluded-content.d.ts +65 -0
  81. package/dist/core/excluded-content.js +113 -0
  82. package/dist/core/failure-codes.d.ts +2 -2
  83. package/dist/core/failure-codes.js +7 -2
  84. package/dist/core/ingest/conditions.d.ts +201 -0
  85. package/dist/core/ingest/conditions.js +48 -0
  86. package/dist/core/ingest/index.d.ts +13 -0
  87. package/dist/core/ingest/index.js +11 -0
  88. package/dist/core/ingest/ingest.d.ts +68 -0
  89. package/dist/core/ingest/ingest.js +318 -0
  90. package/dist/core/lineage/stage-table.d.ts +21 -4
  91. package/dist/core/lineage/stage-table.js +35 -3
  92. package/dist/core/preflight/plan.d.ts +7 -5
  93. package/dist/core/preflight/plan.js +75 -32
  94. package/dist/core/preflight/projection.d.ts +6 -3
  95. package/dist/core/preflight/projection.js +22 -2
  96. package/dist/core/preflight/reduce.js +47 -7
  97. package/dist/core/preflight/witness-evidence.d.ts +5 -5
  98. package/dist/core/preflight/witness-evidence.js +73 -20
  99. package/dist/core/schemas/artifact.d.ts +1049 -30
  100. package/dist/core/schemas/constraint-ledger.js +51 -0
  101. package/dist/core/schemas/defect-signature.d.ts +695 -0
  102. package/dist/core/schemas/defect-signature.js +175 -0
  103. package/dist/core/schemas/eval-contract.d.ts +550 -12
  104. package/dist/core/schemas/eval-contract.js +32 -2
  105. package/dist/core/schemas/evidence-artifact.d.ts +109 -19
  106. package/dist/core/schemas/evidence-artifact.js +64 -8
  107. package/dist/core/schemas/interface.d.ts +741 -13
  108. package/dist/core/schemas/interface.js +120 -7
  109. package/dist/core/schemas/isolation-manifest.js +16 -9
  110. package/dist/core/schemas/plan.d.ts +329 -3
  111. package/dist/core/schemas/plan.js +89 -3
  112. package/dist/core/schemas/pointer.d.ts +47 -1
  113. package/dist/core/schemas/pointer.js +89 -8
  114. package/dist/core/schemas/port-messages.d.ts +194 -10
  115. package/dist/core/schemas/port-messages.js +73 -6
  116. package/dist/core/schemas/probe-body.d.ts +18 -0
  117. package/dist/core/schemas/probe-body.js +13 -0
  118. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  119. package/dist/core/schemas/probe-qualification.js +106 -0
  120. package/dist/core/schemas/probe.d.ts +431 -0
  121. package/dist/core/schemas/probe.js +6 -2
  122. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  123. package/dist/core/schemas/scoring-policy.js +6 -1
  124. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  125. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  126. package/dist/core/schemas/sealed-run-record.d.ts +161 -12
  127. package/dist/core/schemas/sealed-run-record.js +92 -15
  128. package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
  129. package/dist/core/schemas/sensitivity-witness.js +61 -5
  130. package/dist/core/score/binding-order.d.ts +31 -0
  131. package/dist/core/score/binding-order.js +88 -0
  132. package/dist/core/score/bindings.d.ts +93 -0
  133. package/dist/core/score/bindings.js +309 -0
  134. package/dist/core/score/ladder-table.d.ts +23 -0
  135. package/dist/core/score/ladder-table.js +109 -0
  136. package/dist/core/score/ladder.d.ts +229 -0
  137. package/dist/core/score/ladder.js +464 -0
  138. package/dist/core/score/mode-agreement.d.ts +30 -0
  139. package/dist/core/score/mode-agreement.js +16 -0
  140. package/dist/core/score/outcome-table.d.ts +17 -0
  141. package/dist/core/score/outcome-table.js +172 -0
  142. package/dist/core/score/outcome.d.ts +466 -0
  143. package/dist/core/score/outcome.js +562 -0
  144. package/dist/core/score/qualification.d.ts +83 -0
  145. package/dist/core/score/qualification.js +645 -0
  146. package/dist/core/score/quotation.d.ts +56 -0
  147. package/dist/core/score/quotation.js +226 -0
  148. package/dist/core/score/reduce-trials.d.ts +80 -0
  149. package/dist/core/score/reduce-trials.js +90 -0
  150. package/dist/core/score/score.d.ts +72 -0
  151. package/dist/core/score/score.js +607 -0
  152. package/dist/core/score/selection.d.ts +80 -0
  153. package/dist/core/score/selection.js +98 -0
  154. package/dist/core/score/strength.d.ts +58 -0
  155. package/dist/core/score/strength.js +227 -0
  156. package/dist/core/score/witness.d.ts +138 -0
  157. package/dist/core/score/witness.js +320 -0
  158. package/dist/core/seal/derived-reference.d.ts +2 -9
  159. package/dist/core/seal/derived-reference.js +293 -46
  160. package/dist/core/seal/plan-index.d.ts +18 -3
  161. package/dist/core/seal/plan-index.js +44 -8
  162. package/dist/core/seal/seal.js +8 -3
  163. package/dist/core/stage-contracts.d.ts +62 -2
  164. package/dist/index.d.ts +1 -1
  165. package/dist/index.js +1 -1
  166. package/dist/ports/environment-probe-port.d.ts +60 -4
  167. package/dist/testing/index.d.ts +2 -0
  168. package/dist/testing/index.js +1 -0
  169. package/dist/testing/probe-conformance.d.ts +29 -0
  170. package/dist/testing/probe-conformance.js +34 -5
  171. package/package.json +12 -3
  172. package/schemas/eval-contract.schema.json +1787 -797
  173. package/schemas/evidence-artifact.schema.json +126 -9
  174. package/schemas/isolation-manifest.schema.json +17 -10
  175. package/schemas/probe.schema.json +636 -5
  176. package/schemas/rubric.schema.json +1 -1
  177. package/schemas/scoring-policy.schema.json +8 -1
  178. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  179. package/schemas/sealed-run-record.schema.json +314 -40
@@ -0,0 +1,56 @@
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
+ export declare function projectChannel(observation: Observation, channel: EvidenceChannelName, artifactPath: string, artifactId?: string | null): string | null;
8
+ export type UnwitnessedQuotation = {
9
+ readonly findingId: string;
10
+ /** the position of the quotation within the finding's own list. */
11
+ readonly quoteIndex: number;
12
+ readonly channel: EvidenceChannelName;
13
+ readonly quote: string;
14
+ /** the observations the finding cited and this audit could read. */
15
+ readonly citedObservationIds: readonly string[];
16
+ };
17
+ /**
18
+ * Every defect finding's quoted evidence, checked against the observations that
19
+ * finding itself cited.
20
+ *
21
+ * Over every defect finding in the record regardless of which bucket the
22
+ * finding maps to, because AD-40 makes an unwitnessed quote a property of the
23
+ * finding rather than of the probe it maps to. A cited identifier naming no
24
+ * observation in the record contributes nothing to read, which is a separate
25
+ * cross-artifact inconsistency and is not this audit's to report.
26
+ */
27
+ export declare function auditQuotation(record: Pick<SealedRunRecord, 'observations' | 'findings'>): readonly UnwitnessedQuotation[];
28
+ export type ReconstructedDetection = {
29
+ /**
30
+ * AD-40 forbids pooling a detection derived this way with a measured catch
31
+ * rate, so the label travels with the result and is never optional.
32
+ */
33
+ readonly basis: 'reconstructed';
34
+ readonly detected: boolean;
35
+ readonly witnessObservationIds: readonly string[];
36
+ };
37
+ /**
38
+ * The containment procedure AD-40 keeps defined for exactly one purpose:
39
+ * re-deriving detection from records written before observation identifiers
40
+ * were required.
41
+ *
42
+ * Nothing in v0 calls it, and nothing can. At least one observation identifier
43
+ * has been required on the defect branch since the schema's first version, so
44
+ * no version of this schema predates the identifier requirement, and AD-11
45
+ * makes a reader reject an unequal version anyway. The procedure exists for
46
+ * records from outside this schema entirely.
47
+ *
48
+ * It takes the satisfying partition the witness match already computed and asks
49
+ * which of those observations the finding's own quotations appear in. Detection
50
+ * so derived is `reconstructed`; the witness match's is `measured`, and the two
51
+ * are never pooled.
52
+ */
53
+ export declare function reconstructDetection(finding: {
54
+ readonly quotedEvidence: readonly QuotedEvidence[];
55
+ }, satisfying: readonly Observation[], artifactPath: string): ReconstructedDetection;
56
+ export {};
@@ -0,0 +1,226 @@
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
+ /**
47
+ * One tagged observed value as the text a quotation is compared against. A
48
+ * text body is its own value; a JSON body is its canonical serialisation, the
49
+ * same one `response-body` gets; an absent channel has no text.
50
+ */
51
+ function observedText(body, artifactPath) {
52
+ if (body.kind === 'absent')
53
+ return null;
54
+ if (body.kind === 'text')
55
+ return body.value;
56
+ return serialize(body.value, artifactPath);
57
+ }
58
+ export function projectChannel(observation, channel, artifactPath, artifactId = null) {
59
+ switch (channel) {
60
+ case 'stdout':
61
+ return observedText(observation.stdout, artifactPath);
62
+ case 'stderr':
63
+ return observedText(observation.stderr, artifactPath);
64
+ case 'artifact': {
65
+ // One named file's own text. Serializing the whole map instead was
66
+ // wrong twice over: it escaped every newline and quotation mark, so
67
+ // a quotation from a file with more than one line could never match,
68
+ // and it searched every file at once, so a quotation from a file the
69
+ // finding did not cite matched anyway.
70
+ if (artifactId === null)
71
+ return null;
72
+ if (!Object.hasOwn(observation.artifacts, artifactId))
73
+ return null;
74
+ const written = observation.artifacts[artifactId];
75
+ return written === undefined ? null : observedText(written, artifactPath);
76
+ }
77
+ case 'response-status':
78
+ return observation.responseStatus === null
79
+ ? null
80
+ : observation.responseStatus.toString();
81
+ case 'exit-code':
82
+ return observation.exitCode === null
83
+ ? null
84
+ : observation.exitCode.toString();
85
+ case 'response-body':
86
+ return observation.responseBody === null
87
+ ? null
88
+ : serialize(observation.responseBody, artifactPath);
89
+ case 'response-headers':
90
+ return observation.responseHeaders === null
91
+ ? null
92
+ : serialize(observation.responseHeaders, artifactPath);
93
+ case 'call-inputs':
94
+ return serialize(observation.callInputs, artifactPath);
95
+ }
96
+ }
97
+ /**
98
+ * Whether one quotation appears in one observation's named channel, verbatim.
99
+ */
100
+ function quotationWitnessed(quoted, observation, artifactPath) {
101
+ const projected = projectChannel(observation, quoted.channel, artifactPath, quoted.artifactId);
102
+ // A null projection witnesses nothing, and `false` is the answer: an
103
+ // optional chain here would hand a caller `undefined` where the return type
104
+ // says boolean.
105
+ if (projected === null)
106
+ return false;
107
+ return projected.includes(quoted.quote);
108
+ }
109
+ /** total over strings, so a tie is a genuine equality rather than a coin toss. */
110
+ const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
111
+ /**
112
+ * Every defect finding's quoted evidence, checked against the observations that
113
+ * finding itself cited.
114
+ *
115
+ * Over every defect finding in the record regardless of which bucket the
116
+ * finding maps to, because AD-40 makes an unwitnessed quote a property of the
117
+ * finding rather than of the probe it maps to. A cited identifier naming no
118
+ * observation in the record contributes nothing to read, which is a separate
119
+ * cross-artifact inconsistency and is not this audit's to report.
120
+ */
121
+ export function auditQuotation(record) {
122
+ // Indexed to a list rather than a value. `observations` is refined unique on
123
+ // `sequence` and not on `observationId`, so two observations may share an
124
+ // identifier, and a last-write-wins map would turn the existential below into
125
+ // a pick decided by array position: one permutation of a record answers that
126
+ // a quotation is witnessed and the other that it is not. AD-40 states the
127
+ // rule as "at least one observation satisfying the condition", so a repeated
128
+ // identifier names both and the `.some` ranges over both.
129
+ const byId = new Map();
130
+ for (const observation of record.observations) {
131
+ const sharing = byId.get(observation.observationId);
132
+ if (sharing === undefined) {
133
+ byId.set(observation.observationId, [observation]);
134
+ }
135
+ else {
136
+ sharing.push(observation);
137
+ }
138
+ }
139
+ const unwitnessed = [];
140
+ for (const finding of record.findings) {
141
+ if (finding.findingType !== 'defect')
142
+ continue;
143
+ // Deduplicated and sorted by identifier. `observationIds` is an array with
144
+ // no uniqueness refinement, so a finding may cite one observation twice,
145
+ // and carrying the citation array's order into `citedObservationIds` would
146
+ // put a position with no declared meaning on the payload. The identifier
147
+ // list stays deduplicated even where two observations share one: it names
148
+ // what the finding cited, not what resolved.
149
+ const citedObservationIds = [];
150
+ const cited = [];
151
+ for (const identifier of [...new Set(finding.observationIds)].sort(order)) {
152
+ const sharing = byId.get(identifier);
153
+ if (sharing === undefined)
154
+ continue;
155
+ citedObservationIds.push(identifier);
156
+ cited.push(...sharing);
157
+ }
158
+ const artifactPath = `SealedRunRecord.findings[findingId=${finding.findingId}]`;
159
+ finding.quotedEvidence.forEach((quoted, quoteIndex) => {
160
+ const witnessed = cited.some((observation) => quotationWitnessed(quoted, observation, artifactPath));
161
+ if (witnessed)
162
+ return;
163
+ unwitnessed.push({
164
+ findingId: finding.findingId,
165
+ quoteIndex,
166
+ channel: quoted.channel,
167
+ quote: quoted.quote,
168
+ citedObservationIds,
169
+ });
170
+ });
171
+ }
172
+ // `findings` carries no ordering field, so reading it in array order would
173
+ // make the result depend on a position NFR9 forbids reading. Sorted by
174
+ // finding identifier, then by the quotation's index within that finding,
175
+ // which is the one order the record itself declares.
176
+ //
177
+ // `findingId` and `quoteIndex` do not separate every pair: `findings` has no
178
+ // uniqueness refinement, so two findings may share an identifier and each
179
+ // carry an unwitnessed quote at index 0, and a stable sort would then fall
180
+ // back to the position this sort exists to stop reading. Unlike two entries
181
+ // that tie on everything, those two are distinguishable — different quote,
182
+ // different channel, two basis lines that say different things — so the key
183
+ // runs to the whole payload.
184
+ //
185
+ // `quote` is compared directly rather than through a joined key. It is free
186
+ // text and may contain any code point including U+0000, so no delimiter is
187
+ // below every character it can carry. `citedObservationIds` may be joined:
188
+ // its members are `Identifier`, whose lowest code point is U+002D.
189
+ return unwitnessed.sort((a, b) => order(a.findingId, b.findingId) ||
190
+ a.quoteIndex - b.quoteIndex ||
191
+ order(a.channel, b.channel) ||
192
+ order(a.quote, b.quote) ||
193
+ order(a.citedObservationIds.join('\u0000'), b.citedObservationIds.join('\u0000')));
194
+ }
195
+ /**
196
+ * The containment procedure AD-40 keeps defined for exactly one purpose:
197
+ * re-deriving detection from records written before observation identifiers
198
+ * were required.
199
+ *
200
+ * Nothing in v0 calls it, and nothing can. At least one observation identifier
201
+ * has been required on the defect branch since the schema's first version, so
202
+ * no version of this schema predates the identifier requirement, and AD-11
203
+ * makes a reader reject an unequal version anyway. The procedure exists for
204
+ * records from outside this schema entirely.
205
+ *
206
+ * It takes the satisfying partition the witness match already computed and asks
207
+ * which of those observations the finding's own quotations appear in. Detection
208
+ * so derived is `reconstructed`; the witness match's is `measured`, and the two
209
+ * are never pooled.
210
+ */
211
+ export function reconstructDetection(finding, satisfying, artifactPath) {
212
+ // A finding quoting nothing proves nothing, which the schema already forbids
213
+ // on this branch; a record from outside it can still present the shape, and
214
+ // an `every` over an empty list would otherwise report every satisfying
215
+ // observation as a witness.
216
+ const witnessObservationIds = finding.quotedEvidence.length === 0
217
+ ? []
218
+ : satisfying
219
+ .filter((observation) => finding.quotedEvidence.every((quoted) => quotationWitnessed(quoted, observation, artifactPath)))
220
+ .map((observation) => observation.observationId);
221
+ return {
222
+ basis: 'reconstructed',
223
+ detected: witnessObservationIds.length > 0,
224
+ witnessObservationIds,
225
+ };
226
+ }
@@ -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>;