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,464 @@
1
+ import { SEVERITY_LEVELS } from '../schemas/eval-contract.js';
2
+ /**
3
+ * AD-21's exit codes for this rung. Not imported from `src/cli/exit-codes.ts`:
4
+ * `core/` imports only `core/schemas` (AD-1), so the three numbers are
5
+ * repeated here rather than shared. Exported so a test outside `core/` can
6
+ * assert this restatement still agrees with that file's own `EXIT_*`
7
+ * constants and `evidenceConditionsOnly` rule, which neither file can check
8
+ * of itself.
9
+ */
10
+ export const LADDER_EXIT_CODES = {
11
+ invalid: 3,
12
+ FAIL: 2,
13
+ CONCERNS: 0,
14
+ WAIVED: 0,
15
+ PASS: 0,
16
+ };
17
+ const BEHAVIOURAL_FAILURE_STATES = [
18
+ 'missed',
19
+ 'abstained',
20
+ 'bypassed',
21
+ 'false-positive',
22
+ ];
23
+ const INVALIDATING_STATES = [
24
+ 'oracle-error',
25
+ 'judge-error',
26
+ 'infrastructure-error',
27
+ ];
28
+ const isBehaviouralFailure = (state) => BEHAVIOURAL_FAILURE_STATES.includes(state);
29
+ const isInvalidatingState = (state) => INVALIDATING_STATES.includes(state);
30
+ const atOrAboveFloor = (severity, floor) => SEVERITY_LEVELS.indexOf(severity) >= SEVERITY_LEVELS.indexOf(floor);
31
+ const requiredOutcomes = (inputs) => inputs.outcomeState.outcomes.filter((outcome) => outcome.required);
32
+ const lineageChainPassed = (chain) => chain.lengthConsistent && chain.noRepeatedDigest && chain.noGap;
33
+ /**
34
+ * Invalid, identical between the two ladders: AD-21's text carries no mode
35
+ * split for it. Nine of AD-21's own clauses plus three owed item 4 leaves
36
+ * for this module to close: selector-ambiguity and unwitnessed-claim, an
37
+ * AD-6 selector-cardinality condition and an AD-40 witness-match result, and
38
+ * a third, separately assigned condition -- the unwitnessed-quotation audit
39
+ * `quotation.ts` ships with no caller. The
40
+ * "unrecognised evaluator recommendation value" clause is omitted per this
41
+ * module's own header comment.
42
+ *
43
+ * `selector-ambiguity` and `unwitnessed-claim` already coincide with an
44
+ * `infrastructure-error` state and so are already covered by
45
+ * `invalidating-state`; both are still named explicitly so `verdictBasis`
46
+ * carries the specific condition AD-21's spine prose never spelled out, not
47
+ * only the generic state.
48
+ *
49
+ * Ten more rows follow: eight previously-rungless ingest conditions, each
50
+ * newly given a rung, plus two conditions `score.ts` itself computes and no
51
+ * `ingest` condition names -- an ambiguous `operationId` across
52
+ * `permittedInterfaces`, and a trial set disagreeing with itself on
53
+ * `mode` or `evaluatorRecommendation`.
54
+ */
55
+ const INVALID_ROWS = [
56
+ {
57
+ id: 'invalidating-state',
58
+ rung: 'invalid',
59
+ guard: 'an outcome resolved an AD-6 invalidating state (oracle-error, judge-error, or infrastructure-error)',
60
+ evidenceCondition: false,
61
+ reasons: (inputs) => inputs.outcomeState.outcomes
62
+ .filter((outcome) => isInvalidatingState(outcome.resolution.state))
63
+ .map((outcome) => `oracle ${outcome.oracleId} resolved ${outcome.resolution.state}`),
64
+ },
65
+ {
66
+ id: 'failed-preflight',
67
+ rung: 'invalid',
68
+ guard: 'a failed pre-flight',
69
+ evidenceCondition: false,
70
+ reasons: (inputs) => inputs.preflightPassed ? [] : ['pre-flight verdict did not pass'],
71
+ },
72
+ {
73
+ id: 'isolation-manifest-violation',
74
+ rung: 'invalid',
75
+ guard: 'an unaccounted isolation manifest under AD-16',
76
+ evidenceCondition: false,
77
+ reasons: (inputs) => inputs.evidenceIntegrity.isolationViolation.map((violation) => `isolation manifest violation: ${violation}`),
78
+ },
79
+ {
80
+ id: 're-execution-cap-breach',
81
+ rung: 'invalid',
82
+ guard: 'a re-execution cap breach under AD-6',
83
+ evidenceCondition: false,
84
+ reasons: (inputs) => {
85
+ const { invalidatedAttempts } = inputs.outcomeState.trials;
86
+ return invalidatedAttempts.length > inputs.outcomeState.reExecutionCap
87
+ ? [
88
+ `${invalidatedAttempts.length} invalidated attempts exceeded the re-execution cap of ${inputs.outcomeState.reExecutionCap}`,
89
+ ]
90
+ : [];
91
+ },
92
+ },
93
+ {
94
+ id: 'disposition-missing',
95
+ rung: 'invalid',
96
+ guard: 'a required oracle carrying no disposition in the run record under AD-23',
97
+ evidenceCondition: false,
98
+ reasons: (inputs) => inputs.outcomeState.outcomes
99
+ .filter((outcome) => outcome.resolution.invalidatingConditions.includes('disposition-missing'))
100
+ .map((outcome) => `oracle ${outcome.oracleId} is required and carries no disposition`),
101
+ },
102
+ {
103
+ id: 'required-check-unresolved',
104
+ rung: 'invalid',
105
+ guard: 'not every required check resolved',
106
+ evidenceCondition: false,
107
+ reasons: (inputs) => requiredOutcomes(inputs)
108
+ .filter((outcome) => !outcome.checkResolved)
109
+ .map((outcome) => `oracle ${outcome.oracleId} is required and its check never resolved`),
110
+ },
111
+ {
112
+ id: 'selector-ambiguity',
113
+ rung: 'invalid',
114
+ guard: 'a step matched several observations under a single-valued cardinality (owed item 2)',
115
+ evidenceCondition: false,
116
+ reasons: (inputs) => inputs.outcomeState.outcomes
117
+ .filter((outcome) => outcome.resolution.invalidatingConditions.includes('selector-ambiguity'))
118
+ .map((outcome) => `oracle ${outcome.oracleId}: selector ambiguity`),
119
+ },
120
+ {
121
+ id: 'unwitnessed-claim',
122
+ rung: 'invalid',
123
+ guard: 'a defect finding claimed a detection no candidate observation witnesses (AD-40)',
124
+ evidenceCondition: false,
125
+ reasons: (inputs) => inputs.outcomeState.outcomes
126
+ .filter((outcome) => outcome.resolution.invalidatingConditions.includes('unwitnessed-detection-claim'))
127
+ .map((outcome) => `oracle ${outcome.oracleId}: unwitnessed detection claim`),
128
+ },
129
+ {
130
+ id: 'unwitnessed-quotation',
131
+ rung: 'invalid',
132
+ guard: "a defect finding's quoted evidence appears in no cited observation (AD-32)",
133
+ evidenceCondition: false,
134
+ reasons: (inputs) => inputs.outcomeState.unwitnessedQuotations.map((quoted) => `finding ${quoted.findingId}: unwitnessed quotation on channel ${quoted.channel}`),
135
+ },
136
+ // Eight previously-rungless conditions, each newly given a rung here.
137
+ // `ingest` already shipped the detection; this is the first ladder row
138
+ // that reports it.
139
+ {
140
+ id: 'duplicate-record-identifier',
141
+ rung: 'invalid',
142
+ guard: 'the record uses one observation, finding, or oracle-disposition identifier twice',
143
+ evidenceCondition: false,
144
+ reasons: (inputs) => inputs.evidenceIntegrity.duplicateRecordIdentifiers.map((condition) => `duplicate ${condition.subject} identifier "${condition.identifier}" (${condition.occurrences} occurrences)`),
145
+ },
146
+ {
147
+ id: 'dangling-citation',
148
+ rung: 'invalid',
149
+ guard: 'a finding cites an observation identifier the record does not declare',
150
+ evidenceCondition: false,
151
+ reasons: (inputs) => inputs.evidenceIntegrity.danglingCitations.map((condition) => `finding ${condition.findingId} cites unresolved observation(s): ${condition.unresolvedObservationIds.join(', ')}`),
152
+ },
153
+ {
154
+ id: 'dangling-disposition-citation',
155
+ rung: 'invalid',
156
+ guard: 'an oracle disposition cites an observation identifier the record does not declare',
157
+ evidenceCondition: false,
158
+ reasons: (inputs) => inputs.evidenceIntegrity.danglingDispositionCitations.map((condition) => `oracle ${condition.oracleId} disposition cites unresolved observation(s): ${condition.unresolvedObservationIds.join(', ')}`),
159
+ },
160
+ {
161
+ id: 'forbidden-input-not-withheld',
162
+ rung: 'invalid',
163
+ guard: 'AD-16 forbidden input admitted rather than withheld',
164
+ evidenceCondition: false,
165
+ reasons: (inputs) => inputs.evidenceIntegrity.forbiddenInputsNotWithheld.flatMap((condition) => condition.inputs.map((input) => `forbidden input admitted rather than withheld: ${input}`)),
166
+ },
167
+ {
168
+ id: 'cross-artifact-disagreement',
169
+ rung: 'invalid',
170
+ guard: 'the sealed run record and the isolation manifest disagree on a field AD-32 requires them to agree on',
171
+ evidenceCondition: false,
172
+ reasons: (inputs) => inputs.evidenceIntegrity.crossArtifactDisagreements.map((condition) => `record and manifest disagree on ${condition.field}: record "${condition.recordValue}", manifest "${condition.manifestValue}"`),
173
+ },
174
+ {
175
+ id: 'evaluator-configuration-absent',
176
+ rung: 'invalid',
177
+ guard: 'the evaluator configuration artifact is absent',
178
+ evidenceCondition: false,
179
+ reasons: (inputs) => inputs.evidenceIntegrity.evaluatorConfigurationAbsent.map(() => 'evaluator configuration absent'),
180
+ },
181
+ {
182
+ id: 'evaluator-configuration-digest-mismatch',
183
+ rung: 'invalid',
184
+ guard: 'the evaluator configuration digest the record declares does not recompute from the artifact',
185
+ evidenceCondition: false,
186
+ reasons: (inputs) => inputs.evidenceIntegrity.evaluatorConfigurationDigestMismatches.map((condition) => `evaluator configuration digest mismatch: declared "${condition.declaredDigest}", computed "${condition.computedDigest}"`),
187
+ },
188
+ {
189
+ id: 'judge-result-unscored',
190
+ rung: 'invalid',
191
+ guard: 'a judge result carries `score: null`',
192
+ evidenceCondition: false,
193
+ reasons: (inputs) => inputs.evidenceIntegrity.judgeResultsUnscored.map((condition) => `judge result unscored: rubric ${condition.rubricId} criterion ${condition.criterionId}`),
194
+ },
195
+ // The two score-computed conditions. Neither is an `IngestCondition`, so
196
+ // `score.ts` renders each entry itself and this row only adds the outer
197
+ // category label, following `isolation-manifest-violation`'s own
198
+ // double-wrap precedent above.
199
+ {
200
+ id: 'operation-identifier-collision',
201
+ rung: 'invalid',
202
+ guard: "an observation's operationId matches an operation in more than one permittedInterfaces entry",
203
+ evidenceCondition: false,
204
+ reasons: (inputs) => inputs.evidenceIntegrity.operationIdentifierCollisions.map((collision) => `operation identifier collision: ${collision}`),
205
+ },
206
+ {
207
+ id: 'trial-set-field-disagreement',
208
+ rung: 'invalid',
209
+ guard: 'two trials in the same trial set disagree on `mode` or `evaluatorRecommendation`',
210
+ evidenceCondition: false,
211
+ reasons: (inputs) => inputs.evidenceIntegrity.trialSetDisagreements.map((disagreement) => `trial-set field disagreement: ${disagreement}`),
212
+ },
213
+ ];
214
+ /**
215
+ * FAIL, shared between the two ladders. `evaluator-recommendation-fail` is
216
+ * production-only and spliced in by `PRODUCTION_LADDER` alone: AD-21 states
217
+ * plainly that the contract-scoring recommendation "is recorded as an input
218
+ * rather than promoted to a rung".
219
+ */
220
+ const FAIL_ROWS_SHARED = [
221
+ {
222
+ id: 'behavioural-failure-at-or-above-floor',
223
+ rung: 'FAIL',
224
+ guard: "an AD-6 behavioural failure at or above the scoring policy's severity floor",
225
+ evidenceCondition: false,
226
+ reasons: (inputs) => inputs.outcomeState.outcomes
227
+ .filter((outcome) => isBehaviouralFailure(outcome.resolution.state) &&
228
+ atOrAboveFloor(outcome.severity, inputs.severityFloor))
229
+ .map((outcome) => `oracle ${outcome.oracleId} resolved ${outcome.resolution.state} at or above the severity floor`),
230
+ },
231
+ {
232
+ id: 'evidence-incomplete',
233
+ rung: 'FAIL',
234
+ guard: 'evidence reported incomplete under AD-17',
235
+ evidenceCondition: false,
236
+ reasons: (inputs) => inputs.evidenceIntegrity.disclosure.reportedIncomplete
237
+ ? ['evidence reported incomplete']
238
+ : [],
239
+ },
240
+ {
241
+ id: 'evidence-over-truncated',
242
+ rung: 'FAIL',
243
+ guard: 'evidence truncated past its disclosed bound under AD-17',
244
+ evidenceCondition: false,
245
+ reasons: (inputs) => inputs.evidenceIntegrity.overTruncated
246
+ ? ['evidence truncated past its disclosed bound']
247
+ : [],
248
+ },
249
+ {
250
+ id: 'evidence-unavailable',
251
+ rung: 'FAIL',
252
+ guard: 'evidence unavailable under AD-17',
253
+ evidenceCondition: false,
254
+ reasons: (inputs) => inputs.evidenceIntegrity.unavailable ? ['evidence unavailable'] : [],
255
+ },
256
+ {
257
+ id: 'evidence-internally-inconsistent',
258
+ rung: 'FAIL',
259
+ guard: 'evidence internally inconsistent under AD-17',
260
+ evidenceCondition: false,
261
+ reasons: (inputs) => inputs.evidenceIntegrity.internallyInconsistent
262
+ ? ['evidence internally inconsistent']
263
+ : [],
264
+ },
265
+ {
266
+ id: 'lineage-chain-inconsistent',
267
+ rung: 'FAIL',
268
+ guard: 'a presented lineage chain internally inconsistent under AD-12',
269
+ evidenceCondition: false,
270
+ reasons: (inputs) => lineageChainPassed(inputs.remediationState)
271
+ ? []
272
+ : ['presented lineage chain is internally inconsistent'],
273
+ },
274
+ ];
275
+ const EVALUATOR_RECOMMENDATION_FAIL_ROW = {
276
+ id: 'evaluator-recommendation-fail',
277
+ rung: 'FAIL',
278
+ guard: 'an ingested evaluator recommendation of FAIL',
279
+ evidenceCondition: false,
280
+ reasons: (inputs) => inputs.evaluatorRecommendation === 'FAIL'
281
+ ? ['evaluator recommendation FAIL']
282
+ : [],
283
+ };
284
+ /**
285
+ * CONCERNS, shared between the two ladders. `below-minimum-trial-count` and
286
+ * `oracle-unreached` are AD-21's own two evidence conditions: "a run that
287
+ * completed fewer trials than the policy's declared minimum, or any oracle
288
+ * resolving `unreached`. The last two are evidence conditions."
289
+ */
290
+ const CONCERNS_ROWS_SHARED = [
291
+ {
292
+ id: 'behavioural-failure-below-floor',
293
+ rung: 'CONCERNS',
294
+ guard: "an AD-6 behavioural failure below the scoring policy's severity floor",
295
+ evidenceCondition: false,
296
+ reasons: (inputs) => inputs.outcomeState.outcomes
297
+ .filter((outcome) => isBehaviouralFailure(outcome.resolution.state) &&
298
+ !atOrAboveFloor(outcome.severity, inputs.severityFloor))
299
+ .map((outcome) => `oracle ${outcome.oracleId} resolved ${outcome.resolution.state} below the severity floor`),
300
+ },
301
+ {
302
+ id: 'coverage-gap-at-or-above-floor',
303
+ rung: 'CONCERNS',
304
+ guard: 'an unsatisfied coverage gap at or above the severity floor under AD-20',
305
+ evidenceCondition: false,
306
+ reasons: (inputs) => inputs.coverageGaps
307
+ .filter((gap) => !gap.satisfied &&
308
+ atOrAboveFloor(gap.severity, inputs.severityFloor))
309
+ .map((gap) => `coverage gap ${gap.rule} unsatisfied at or above the severity floor`),
310
+ },
311
+ {
312
+ id: 'finding-confidence-below-threshold',
313
+ rung: 'CONCERNS',
314
+ guard: 'a finding whose confidence falls below the policy threshold',
315
+ evidenceCondition: false,
316
+ reasons: (inputs) => inputs.findings
317
+ .filter((finding) => finding.confidence < inputs.confidenceThreshold)
318
+ .map((finding) => `finding ${finding.findingId} confidence ${finding.confidence} below the policy threshold`),
319
+ },
320
+ {
321
+ id: 'uncited-defect-finding',
322
+ rung: 'CONCERNS',
323
+ guard: 'an ingested defect finding citing no oracle',
324
+ // No severity-floor gate, unlike the two rows above: an uncited defect
325
+ // finding is an evaluator already catching something real, not the
326
+ // possibly-harmless under-declared corner a floor exists to excuse.
327
+ evidenceCondition: false,
328
+ reasons: (inputs) => inputs.uncitedDefectFindings.map((gap) => `finding ${gap.findingId}: defect finding citing no oracle`),
329
+ },
330
+ {
331
+ id: 'below-minimum-trial-count',
332
+ rung: 'CONCERNS',
333
+ guard: "the run completed fewer trials than the policy's declared minimum",
334
+ evidenceCondition: true,
335
+ reasons: (inputs) => {
336
+ const { completed, declaredMinimum } = inputs.outcomeState.trials;
337
+ return completed < declaredMinimum
338
+ ? [
339
+ `${completed} completed trials below the declared minimum of ${declaredMinimum}`,
340
+ ]
341
+ : [];
342
+ },
343
+ },
344
+ {
345
+ id: 'oracle-unreached',
346
+ rung: 'CONCERNS',
347
+ guard: 'an oracle resolved `unreached`',
348
+ evidenceCondition: true,
349
+ reasons: (inputs) => inputs.outcomeState.outcomes
350
+ .filter((outcome) => outcome.resolution.state === 'unreached')
351
+ .map((outcome) => `oracle ${outcome.oracleId} resolved unreached`),
352
+ },
353
+ ];
354
+ const EVALUATOR_RECOMMENDATION_CONCERNS_ROW = {
355
+ id: 'evaluator-recommendation-concerns',
356
+ rung: 'CONCERNS',
357
+ guard: 'an ingested recommendation of CONCERNS',
358
+ evidenceCondition: false,
359
+ reasons: (inputs) => inputs.evaluatorRecommendation === 'CONCERNS'
360
+ ? ['evaluator recommendation CONCERNS']
361
+ : [],
362
+ };
363
+ /**
364
+ * WAIVED, shared between the two ladders. Reads `OutcomeResolution.waiverRule`
365
+ * directly rather than re-deriving waiver state: AD-33's own Stage C already
366
+ * decided which oracle earned `not-applicable` against an unexpired waiver,
367
+ * and `waiver-honoured` is that decision's own name for it.
368
+ */
369
+ const WAIVED_ROWS = [
370
+ {
371
+ id: 'waiver-honoured',
372
+ rung: 'WAIVED',
373
+ guard: 'every required check resolved and at least one resolved `not-applicable` against an unexpired waiver',
374
+ evidenceCondition: false,
375
+ reasons: (inputs) => inputs.outcomeState.outcomes
376
+ .filter((outcome) => outcome.resolution.waiverRule === 'waiver-honoured')
377
+ .map((outcome) => `oracle ${outcome.oracleId} waived (not-applicable)`),
378
+ },
379
+ ];
380
+ /**
381
+ * Production's ladder: the shared rows plus the two rows that read an
382
+ * ingested evaluator recommendation, which contract-scoring's never does.
383
+ */
384
+ export const PRODUCTION_LADDER = [
385
+ ...INVALID_ROWS,
386
+ ...FAIL_ROWS_SHARED,
387
+ EVALUATOR_RECOMMENDATION_FAIL_ROW,
388
+ ...CONCERNS_ROWS_SHARED,
389
+ EVALUATOR_RECOMMENDATION_CONCERNS_ROW,
390
+ ...WAIVED_ROWS,
391
+ ];
392
+ /**
393
+ * Contract-scoring's ladder: FAIL/CONCERNS/WAIVED/PASS mirror production's
394
+ * minus every clause that promotes `evaluatorRecommendation`, per AD-21.
395
+ */
396
+ export const CONTRACT_LADDER = [
397
+ ...INVALID_ROWS,
398
+ ...FAIL_ROWS_SHARED,
399
+ ...CONCERNS_ROWS_SHARED,
400
+ ...WAIVED_ROWS,
401
+ ];
402
+ const RUNG_PRECEDENCE = ['invalid', 'FAIL', 'CONCERNS', 'WAIVED'];
403
+ /**
404
+ * First-match-wins over tiers, in AD-21's own precedence order; within the
405
+ * winning tier every row that holds contributes to `basis`, so a persistent
406
+ * judge fault cannot mask a real regression, which is AD-21's own stated
407
+ * reason for recording every condition that fired. PASS is the explicit
408
+ * final rung: reached only when every tier above is empty, never an
409
+ * `otherwise` branch written into the loop.
410
+ */
411
+ function resolve(ladder, inputs) {
412
+ for (const rung of RUNG_PRECEDENCE) {
413
+ const fired = ladder
414
+ .filter((row) => row.rung === rung)
415
+ .map((row) => ({ row, reasons: row.reasons(inputs) }))
416
+ .filter((candidate) => candidate.reasons.length > 0);
417
+ if (fired.length === 0)
418
+ continue;
419
+ const basis = fired.flatMap((candidate) => candidate.reasons);
420
+ if (rung === 'invalid') {
421
+ return {
422
+ verdict: null,
423
+ exitCode: LADDER_EXIT_CODES.invalid,
424
+ strictPromotable: true,
425
+ basis,
426
+ };
427
+ }
428
+ if (rung === 'CONCERNS') {
429
+ const strictPromotable = fired.some((candidate) => !candidate.row.evidenceCondition);
430
+ return {
431
+ verdict: 'CONCERNS',
432
+ exitCode: LADDER_EXIT_CODES.CONCERNS,
433
+ strictPromotable,
434
+ basis,
435
+ };
436
+ }
437
+ return {
438
+ verdict: rung,
439
+ exitCode: LADDER_EXIT_CODES[rung],
440
+ strictPromotable: true,
441
+ basis,
442
+ };
443
+ }
444
+ return {
445
+ verdict: 'PASS',
446
+ exitCode: LADDER_EXIT_CODES.PASS,
447
+ strictPromotable: true,
448
+ basis: [],
449
+ };
450
+ }
451
+ /** AD-21's production-mode ladder: the subject is the system under test. */
452
+ export function resolveProductionVerdict(assessment) {
453
+ return resolve(PRODUCTION_LADDER, assessment);
454
+ }
455
+ /**
456
+ * AD-21's contract-scoring ladder: the subject is the contract. No row here
457
+ * reads `evaluatorRecommendation`; `systemRecommendationRecorded` and
458
+ * `systemRecommendationNote` are carried on the input for symmetry with
459
+ * `EvidenceArtifact`'s own contract-scoring branch and are read by nothing in
460
+ * this module.
461
+ */
462
+ export function resolveContractVerdict(assessment) {
463
+ return resolve(CONTRACT_LADDER, assessment);
464
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AD-32's cross-artifact mode-agreement check: a sealed run record's `mode`
3
+ * must agree with the evidence artifact's own `mode` discriminant, in both
4
+ * directions. A hand-written function at the assembly boundary rather than a
5
+ * schema refinement, mirroring `isolation-manifest.ts`'s
6
+ * `evaluatorConfigurationDigest` agreement note: no single schema sees both
7
+ * artifacts.
8
+ *
9
+ * `sealed-run-record.ts`'s own `mode` description states the rule this
10
+ * function enforces: mode is "fixed before ingest" and the evidence artifact
11
+ * "restates it and is never the source". A mismatch is therefore always a
12
+ * caller defect, never a legitimate re-labelling.
13
+ */
14
+ import type { EvidenceArtifact } from '../schemas/evidence-artifact.ts';
15
+ import type { RunModeValue, SealedRunRecord } from '../schemas/sealed-run-record.ts';
16
+ export type ModeAgreement = {
17
+ readonly agrees: true;
18
+ readonly mode: RunModeValue;
19
+ } | {
20
+ readonly agrees: false;
21
+ readonly recordMode: RunModeValue;
22
+ readonly artifactMode: RunModeValue;
23
+ };
24
+ /**
25
+ * A plain equality check, which is what "rejects, in both directions"
26
+ * amounts to: `(production, contract-scoring)` and its reverse pairing are
27
+ * both instances of the two arguments disagreeing, and neither is a
28
+ * distinguished direction this function reads differently.
29
+ */
30
+ export declare function checkModeAgreement(record: Pick<SealedRunRecord, 'mode'>, artifact: Pick<EvidenceArtifact, 'mode'>): ModeAgreement;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A plain equality check, which is what "rejects, in both directions"
3
+ * amounts to: `(production, contract-scoring)` and its reverse pairing are
4
+ * both instances of the two arguments disagreeing, and neither is a
5
+ * distinguished direction this function reads differently.
6
+ */
7
+ export function checkModeAgreement(record, artifact) {
8
+ if (record.mode === artifact.mode) {
9
+ return { agrees: true, mode: record.mode };
10
+ }
11
+ return {
12
+ agrees: false,
13
+ recordMode: record.mode,
14
+ artifactMode: artifact.mode,
15
+ };
16
+ }
@@ -0,0 +1,17 @@
1
+ import { type OutcomeResolution } from './outcome.ts';
2
+ /** One named implication over the declared input domains. */
3
+ export type StructuralConstraint = {
4
+ readonly id: string;
5
+ readonly implication: string;
6
+ };
7
+ /** A pair of domain values no tuple satisfying the constraints contains. */
8
+ export type InfeasiblePair = {
9
+ readonly left: string;
10
+ readonly right: string;
11
+ };
12
+ export type OutcomeTableConstraints = {
13
+ readonly constraints: readonly StructuralConstraint[];
14
+ readonly infeasiblePairs: readonly InfeasiblePair[];
15
+ };
16
+ /** The whole document. Throws a worded diagnosis on an empty census cell. */
17
+ export declare function outcomeDecisionTable(cases: readonly OutcomeResolution[], constraints: OutcomeTableConstraints): string;