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.
Files changed (133) hide show
  1. package/README.md +142 -67
  2. package/corpus/dev/README.md +17 -11
  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/no-collection-quantifier.json +1 -1
  13. package/corpus/dev/contracts/no-operation-inventory.json +1 -1
  14. package/corpus/dev/contracts/no-read-back-relation.json +1 -1
  15. package/corpus/dev/contracts/no-state-change-marker.json +1 -1
  16. package/corpus/dev/contracts/no-type-violating-step.json +1 -1
  17. package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
  18. package/corpus/dev/contracts/satisfied-declarations.json +1 -1
  19. package/corpus/dev/contracts/single-required-response-key.json +1 -1
  20. package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
  21. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
  22. package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
  23. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
  24. package/corpus/dev/index.json +1 -1
  25. package/dist/application/index.d.ts +2 -0
  26. package/dist/application/index.js +1 -0
  27. package/dist/application/score.d.ts +48 -0
  28. package/dist/application/score.js +186 -0
  29. package/dist/cli/arguments.d.ts +7 -3
  30. package/dist/cli/arguments.js +73 -5
  31. package/dist/cli/exit-codes.d.ts +6 -2
  32. package/dist/cli/exit-codes.js +8 -9
  33. package/dist/cli/main.js +9 -0
  34. package/dist/cli/render.d.ts +1 -1
  35. package/dist/cli/render.js +6 -4
  36. package/dist/cli/run.d.ts +13 -3
  37. package/dist/cli/run.js +136 -16
  38. package/dist/core/canonical/canonicalize.d.ts +12 -0
  39. package/dist/core/canonical/canonicalize.js +18 -4
  40. package/dist/core/compile/bindings.d.ts +70 -0
  41. package/dist/core/compile/bindings.js +317 -0
  42. package/dist/core/compile/compile.d.ts +5 -0
  43. package/dist/core/compile/compile.js +9 -0
  44. package/dist/core/compile/expression-legality.d.ts +49 -0
  45. package/dist/core/compile/expression-legality.js +105 -31
  46. package/dist/core/compile/forbidden-inputs.d.ts +8 -1
  47. package/dist/core/compile/forbidden-inputs.js +16 -3
  48. package/dist/core/compile/interface-inventory.d.ts +23 -1
  49. package/dist/core/compile/interface-inventory.js +32 -3
  50. package/dist/core/compile/reachability.d.ts +18 -1
  51. package/dist/core/compile/reachability.js +46 -1
  52. package/dist/core/compile/sensitivity-witness.d.ts +1 -1
  53. package/dist/core/compile/sensitivity-witness.js +1 -1
  54. package/dist/core/emit/emit.d.ts +9 -0
  55. package/dist/core/emit/emit.js +214 -0
  56. package/dist/core/emit/private-artifact-digest.d.ts +11 -0
  57. package/dist/core/emit/private-artifact-digest.js +31 -0
  58. package/dist/core/evaluate/evidence-resolution.d.ts +11 -1
  59. package/dist/core/evaluate/evidence-resolution.js +4 -1
  60. package/dist/core/failure-codes.d.ts +2 -2
  61. package/dist/core/failure-codes.js +4 -2
  62. package/dist/core/ingest/conditions.d.ts +201 -0
  63. package/dist/core/ingest/conditions.js +48 -0
  64. package/dist/core/ingest/index.d.ts +13 -0
  65. package/dist/core/ingest/index.js +11 -0
  66. package/dist/core/ingest/ingest.d.ts +68 -0
  67. package/dist/core/ingest/ingest.js +310 -0
  68. package/dist/core/lineage/stage-table.d.ts +21 -4
  69. package/dist/core/lineage/stage-table.js +35 -3
  70. package/dist/core/preflight/witness-evidence.js +10 -0
  71. package/dist/core/schemas/artifact.d.ts +385 -14
  72. package/dist/core/schemas/constraint-ledger.js +40 -0
  73. package/dist/core/schemas/defect-signature.d.ts +273 -0
  74. package/dist/core/schemas/defect-signature.js +127 -0
  75. package/dist/core/schemas/eval-contract.d.ts +33 -0
  76. package/dist/core/schemas/eval-contract.js +14 -0
  77. package/dist/core/schemas/evidence-artifact.d.ts +99 -19
  78. package/dist/core/schemas/evidence-artifact.js +64 -8
  79. package/dist/core/schemas/plan.d.ts +112 -0
  80. package/dist/core/schemas/plan.js +67 -2
  81. package/dist/core/schemas/probe-qualification.d.ts +168 -0
  82. package/dist/core/schemas/probe-qualification.js +106 -0
  83. package/dist/core/schemas/probe.d.ts +312 -0
  84. package/dist/core/schemas/probe.js +6 -2
  85. package/dist/core/schemas/scoring-policy.d.ts +1 -0
  86. package/dist/core/schemas/scoring-policy.js +6 -1
  87. package/dist/core/schemas/sealed-evaluator-brief.d.ts +1 -0
  88. package/dist/core/schemas/sealed-evaluator-brief.js +4 -1
  89. package/dist/core/schemas/sealed-run-record.d.ts +30 -3
  90. package/dist/core/schemas/sealed-run-record.js +37 -5
  91. package/dist/core/score/binding-order.d.ts +31 -0
  92. package/dist/core/score/binding-order.js +88 -0
  93. package/dist/core/score/bindings.d.ts +93 -0
  94. package/dist/core/score/bindings.js +297 -0
  95. package/dist/core/score/ladder-table.d.ts +23 -0
  96. package/dist/core/score/ladder-table.js +109 -0
  97. package/dist/core/score/ladder.d.ts +229 -0
  98. package/dist/core/score/ladder.js +464 -0
  99. package/dist/core/score/mode-agreement.d.ts +30 -0
  100. package/dist/core/score/mode-agreement.js +16 -0
  101. package/dist/core/score/outcome-table.d.ts +17 -0
  102. package/dist/core/score/outcome-table.js +172 -0
  103. package/dist/core/score/outcome.d.ts +466 -0
  104. package/dist/core/score/outcome.js +562 -0
  105. package/dist/core/score/qualification.d.ts +83 -0
  106. package/dist/core/score/qualification.js +615 -0
  107. package/dist/core/score/quotation.d.ts +79 -0
  108. package/dist/core/score/quotation.js +201 -0
  109. package/dist/core/score/reduce-trials.d.ts +80 -0
  110. package/dist/core/score/reduce-trials.js +90 -0
  111. package/dist/core/score/score.d.ts +72 -0
  112. package/dist/core/score/score.js +581 -0
  113. package/dist/core/score/selection.d.ts +80 -0
  114. package/dist/core/score/selection.js +98 -0
  115. package/dist/core/score/strength.d.ts +49 -0
  116. package/dist/core/score/strength.js +193 -0
  117. package/dist/core/score/witness.d.ts +117 -0
  118. package/dist/core/score/witness.js +315 -0
  119. package/dist/core/seal/derived-reference.d.ts +1 -1
  120. package/dist/core/seal/derived-reference.js +236 -28
  121. package/dist/core/seal/seal.js +8 -3
  122. package/dist/core/stage-contracts.d.ts +62 -2
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/testing/index.d.ts +2 -0
  126. package/dist/testing/index.js +1 -0
  127. package/package.json +12 -3
  128. package/schemas/eval-contract.schema.json +100 -2
  129. package/schemas/evidence-artifact.schema.json +96 -9
  130. package/schemas/probe.schema.json +508 -3
  131. package/schemas/scoring-policy.schema.json +8 -1
  132. package/schemas/sealed-evaluator-brief.schema.json +11 -1
  133. package/schemas/sealed-run-record.schema.json +19 -2
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Owed item 3's score-time half: resolving a captured pointer to the scalar an
3
+ * earlier step's observation carried, and filtering a step's matched
4
+ * observations against its own selection predicate before reporting a
5
+ * cardinality verdict.
6
+ *
7
+ * An AD-39 selector is an input binding and an optional temporal clause, so
8
+ * `selectWithBindings` resolves both halves. `selectObservations` matches on
9
+ * `operationId` alone and is untouched, so its permutation guarantee and its
10
+ * tests stand; this wraps it.
11
+ *
12
+ * Pure and total, like the module it wraps: nothing throws on unresolvable
13
+ * evidence, and no AD-6 outcome state is assigned.
14
+ */
15
+ import { capturedBindings } from '../compile/bindings.js';
16
+ import { channelRoot, walkTail } from '../evaluate/evidence-resolution.js';
17
+ import { ABSENT } from '../evaluate/resolved-value.js';
18
+ import { TRANSPORT_CHANNELS, } from '../schemas/pointer.js';
19
+ import { parseEvidenceTarget } from '../seal/plan-index.js';
20
+ import { bindingOrder } from './binding-order.js';
21
+ import { selectObservations } from './selection.js';
22
+ const ABSENT_RESOLUTION = { status: 'absent' };
23
+ /**
24
+ * The key one captured binding occupies in the resolution map. Keyed by
25
+ * binding site rather than by pointer: one step can carry several captured
26
+ * bindings, and two of them may name the same pointer in different channels.
27
+ * JSON-encoded rather than delimiter-joined, since a parameter key is arbitrary
28
+ * caller-supplied text and could contain any separator.
29
+ */
30
+ export function bindingSiteKey(stepId, transportChannel, key) {
31
+ return JSON.stringify([stepId, transportChannel, key]);
32
+ }
33
+ /**
34
+ * Resolves one captured pointer against the record.
35
+ *
36
+ * The referenced step is selected through `selectWithBindings`, so its own
37
+ * selection predicate applies before a value is read off it. Selecting it with
38
+ * `selectObservations` alone would match on `operationId` and leave a step that
39
+ * only its own bindings separate reporting the named ambiguity, which would
40
+ * make a capture from either half of a literal-bound collision pair resolve
41
+ * nothing at all. `resolved` carries the referenced step's own captured values,
42
+ * already filled by the tier below it, which is what makes the tiering
43
+ * load-bearing.
44
+ *
45
+ * The three dispositions already settled hold unchanged over the filtered
46
+ * result: one match binds, several under a declared `any` binds the lowest
47
+ * `sequence`, and several under `exactly-one`/`at-most-one` is the named
48
+ * ambiguity, returned as data with no value resolved.
49
+ *
50
+ * A pointer whose step the plan does not declare, or whose referenced step
51
+ * matched nothing, resolves `absent`.
52
+ *
53
+ * `resolved` as a status means an observation was selected, which is a
54
+ * different fact from the pointer finding a value in it: a tail that walks off
55
+ * the observed body comes back `resolved` carrying `ABSENT`, with the source
56
+ * observation and its `sequence` intact. That is why `CapturedResolution.value`
57
+ * is `ResolvedValue`. AD-26 makes absent an observation rather than an error,
58
+ * and `deepEquals` is false against it, so the referencing step still selects
59
+ * `none`; keeping the source observation means the ordering floor stays
60
+ * computable either way.
61
+ */
62
+ export function resolveCapturedValue(pointer, index, observations, resolved) {
63
+ const target = parseEvidenceTarget(pointer);
64
+ const step = index.stepOf(target.stepId);
65
+ if (step === undefined)
66
+ return ABSENT_RESOLUTION;
67
+ const selected = selectOne(step, observations, index, resolved, new Set());
68
+ if (selected === null)
69
+ return ABSENT_RESOLUTION;
70
+ if (selected.ambiguous !== null) {
71
+ return { status: 'ambiguous', matchedObservationIds: selected.ambiguous };
72
+ }
73
+ const { source } = selected;
74
+ if (source === null)
75
+ return ABSENT_RESOLUTION;
76
+ return {
77
+ status: 'resolved',
78
+ value: walkTail(channelRoot(source, target), target.tail),
79
+ observationId: source.observationId,
80
+ sequence: source.sequence,
81
+ };
82
+ }
83
+ function selectOne(step, observations, index, resolved, guard) {
84
+ if (guard.has(step.stepId))
85
+ return null;
86
+ const selection = selectFiltered(step, observations, index, resolved, new Set([...guard, step.stepId]));
87
+ const [first] = selection.matchedObservationIds;
88
+ if (selection.result === 'none' || first === undefined) {
89
+ return { source: null, ambiguous: null };
90
+ }
91
+ if (selection.result === 'several' && step.cardinality !== 'any') {
92
+ return { source: null, ambiguous: selection.matchedObservationIds };
93
+ }
94
+ // Ascending-`sequence` order already holds the lowest-sequence match first,
95
+ // which is `resolveTemporalAnchor`'s own rule for a declared `any`.
96
+ const source = observations.find((observation) => observation.observationId === first);
97
+ return { source: source ?? null, ambiguous: null };
98
+ }
99
+ /**
100
+ * Every captured binding in the plan, resolved in `bindingOrder`'s tiers and
101
+ * keyed by binding site. Walking the tiers in order is what lets a step's
102
+ * captured values be resolved against a referenced step whose own captures are
103
+ * already in the map. Steps `bindingOrder` reports cyclic are left out, so
104
+ * their bindings resolve as unlisted and filter every candidate away.
105
+ */
106
+ export function resolveCapturedBindings(interactionPlan, index, observations) {
107
+ const resolved = new Map();
108
+ for (const tier of bindingOrder(interactionPlan).tiers) {
109
+ for (const stepId of tier) {
110
+ const step = index.stepOf(stepId);
111
+ if (step === undefined)
112
+ continue;
113
+ for (const capture of capturedBindings(step)) {
114
+ resolved.set(bindingSiteKey(stepId, capture.transportChannel, capture.key), resolveCapturedValue(capture.pointer, index, observations, resolved));
115
+ }
116
+ }
117
+ }
118
+ return resolved;
119
+ }
120
+ // AD-4 separates `equality` from `deep-equality`; a literal binding is
121
+ // `JsonValue` and admits objects and arrays, so this is the deep one. Spelled
122
+ // here rather than reached for in `core/evaluate`'s operator set: that set
123
+ // resolves operands inside an oracle tree, and a binding filter is not an
124
+ // oracle.
125
+ // Exported so the AD-40 probe-side selector follows these rules rather than
126
+ // re-deriving them: two copies of a deep comparison that drift flip a witness
127
+ // match silently.
128
+ export function deepEquals(a, b) {
129
+ if (a === ABSENT || b === ABSENT)
130
+ return false;
131
+ if (a === b)
132
+ return true;
133
+ if (Array.isArray(a) || Array.isArray(b)) {
134
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
135
+ return false;
136
+ }
137
+ return a.every((element, position) => deepEquals(element, b[position]));
138
+ }
139
+ if (a === null || b === null)
140
+ return false;
141
+ if (typeof a !== 'object' || typeof b !== 'object')
142
+ return false;
143
+ const keys = Object.keys(a);
144
+ if (keys.length !== Object.keys(b).length)
145
+ return false;
146
+ return keys.every((key) => Object.hasOwn(b, key) &&
147
+ deepEquals(a[key], b[key]));
148
+ }
149
+ export function jsonTypeOf(value) {
150
+ if (value === null)
151
+ return 'null';
152
+ if (Array.isArray(value))
153
+ return 'array';
154
+ return typeof value;
155
+ }
156
+ /**
157
+ * The highest `sequence` any of this step's captured bindings resolved from, or
158
+ * `null` when one of them resolved nothing at all. A candidate must come
159
+ * strictly after every captured value, so the highest is the binding bound.
160
+ */
161
+ function capturedFloor(step, resolved) {
162
+ let floor = null;
163
+ for (const capture of capturedBindings(step)) {
164
+ const resolution = resolved.get(bindingSiteKey(step.stepId, capture.transportChannel, capture.key));
165
+ if (resolution === undefined || resolution.status !== 'resolved')
166
+ return null;
167
+ floor =
168
+ floor === null
169
+ ? resolution.sequence
170
+ : Math.max(floor, resolution.sequence);
171
+ }
172
+ return floor;
173
+ }
174
+ /**
175
+ * Whether one observation's recorded call inputs satisfy one step's declared
176
+ * bindings. Four rules over the four tagged forms, and one shared
177
+ * precondition: every channel of `observation.callInputs` is nullable, and a
178
+ * `null` channel means the key is not present, so a binding into one filters
179
+ * the candidate out. That is the same fail-closed disposition as an absent
180
+ * captured value, and AD-26's.
181
+ *
182
+ * - `{ literal: v }`: the observed value deep-equals `v`, key order
183
+ * irrelevant.
184
+ * - `{ captured: p }`: the observed value deep-equals the resolved captured
185
+ * value. The ordering half is `capturedFloor`'s, applied before this runs.
186
+ * - `{ matcher: 'any' }` and `{ principal }`: the key is present, and nothing
187
+ * more. Neither declares a value the contract knows: a principal's value is
188
+ * provisioned by the harness at runtime, which is the whole reason the
189
+ * binding exists. The consequence is that two steps differing only by which
190
+ * principal they bind cannot be separated here, because no field of a sealed
191
+ * run record says which principal the harness used.
192
+ * - `{ matcher: 'type-violating' }`: the observed value's JSON type differs
193
+ * from the operation's declared type for that key. A key whose declared type
194
+ * is absent or `null` fails closed: an indeterminate type cannot prove a
195
+ * violation.
196
+ */
197
+ function satisfiesBindings(step, observation, index, resolved) {
198
+ const operation = index.operationOf(step.operationId);
199
+ for (const channel of TRANSPORT_CHANNELS) {
200
+ const binding = step.inputBinding[channel];
201
+ if (binding === null)
202
+ continue;
203
+ const observed = observation.callInputs[channel];
204
+ if (observed === null)
205
+ return false;
206
+ for (const key of Object.keys(binding)) {
207
+ const value = binding[key];
208
+ if (value === undefined)
209
+ continue;
210
+ if (!Object.hasOwn(observed, key))
211
+ return false;
212
+ const actual = observed[key];
213
+ if ('literal' in value) {
214
+ if (!deepEquals(actual, value.literal))
215
+ return false;
216
+ continue;
217
+ }
218
+ if ('captured' in value) {
219
+ const resolution = resolved.get(bindingSiteKey(step.stepId, channel, key));
220
+ if (resolution === undefined || resolution.status !== 'resolved') {
221
+ return false;
222
+ }
223
+ if (!deepEquals(actual, resolution.value))
224
+ return false;
225
+ continue;
226
+ }
227
+ if ('principal' in value)
228
+ continue;
229
+ if (value.matcher === 'any')
230
+ continue;
231
+ const declared = operation?.requestShape[channel].types[key];
232
+ if (declared === undefined || declared === null)
233
+ return false;
234
+ if (jsonTypeOf(actual) === declared)
235
+ return false;
236
+ }
237
+ }
238
+ return true;
239
+ }
240
+ function temporalFloor(step, index, observations, resolved, guard) {
241
+ if (step.after === null)
242
+ return { kind: 'none' };
243
+ const anchorStep = index.stepOf(step.after);
244
+ if (anchorStep === undefined)
245
+ return { kind: 'none' };
246
+ const anchor = selectOne(anchorStep, observations, index, resolved, guard);
247
+ if (anchor === null || anchor.source === null)
248
+ return { kind: 'unsatisfiable' };
249
+ return { kind: 'after', sequence: anchor.source.sequence };
250
+ }
251
+ /**
252
+ * `selectObservations`, then the step's own selection predicate as a filter
253
+ * over the matches, then the cardinality verdict over what survived.
254
+ *
255
+ * The order is temporal clause, then capture ordering, then the binding
256
+ * filters. Ordering is enforced at score time as well as at compile time: the
257
+ * persistence read-back this exists for is exactly a claim about order, and a
258
+ * record whose `GET` sits at `sequence` 2 and whose `POST` sits at `sequence` 9
259
+ * would otherwise satisfy the binding and pass an oracle proving the opposite.
260
+ *
261
+ * `resolved` is the map `resolveCapturedBindings` fills from `bindingOrder`'s
262
+ * tiers, keyed by binding site. An unlisted or unresolved site filters every
263
+ * candidate out, so a caller that skipped a tier gets `none` rather than a
264
+ * silently wrong match.
265
+ *
266
+ * A filter over zero bindings and no clause separates nothing, so two steps
267
+ * sharing an operation and declaring neither both still return `several`: the
268
+ * declared structure does not distinguish them.
269
+ */
270
+ export function selectWithBindings(step, observations, index, resolved) {
271
+ return selectFiltered(step, observations, index, resolved, new Set());
272
+ }
273
+ // `guard` carries the steps already being resolved further up the call, so an
274
+ // `after` clause cycle in an uncompiled plan terminates instead of recursing.
275
+ function selectFiltered(step, observations, index, resolved, guard) {
276
+ const base = selectObservations(step, observations);
277
+ const temporal = temporalFloor(step, index, observations, resolved, new Set([...guard, step.stepId]));
278
+ const captured = capturedFloor(step, resolved);
279
+ const hasCaptures = capturedBindings(step).length > 0;
280
+ if (temporal.kind === 'unsatisfiable' || (hasCaptures && captured === null)) {
281
+ return { result: 'none', matchedObservationIds: [] };
282
+ }
283
+ const floor = Math.max(temporal.kind === 'after' ? temporal.sequence : Number.NEGATIVE_INFINITY, captured ?? Number.NEGATIVE_INFINITY);
284
+ const byId = new Map(observations.map((observation) => [observation.observationId, observation]));
285
+ const matchedObservationIds = base.matchedObservationIds.filter((id) => {
286
+ const observation = byId.get(id);
287
+ return (observation !== undefined &&
288
+ observation.sequence > floor &&
289
+ satisfiesBindings(step, observation, index, resolved));
290
+ });
291
+ const result = matchedObservationIds.length === 0
292
+ ? 'none'
293
+ : matchedObservationIds.length === 1
294
+ ? 'one'
295
+ : 'several';
296
+ return { result, matchedObservationIds };
297
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * AD-21's published decision table, emitted from the two ladders' own
3
+ * condition rows and from the fixture set that exercises them, mirroring
4
+ * `outcome-table.ts`'s builder: pure, so the writer and the drift check
5
+ * cannot disagree about bytes, and every census count throws on zero rather
6
+ * than publishing a coverage claim the fixture set does not back.
7
+ *
8
+ * `LadderResolution.basis` is free text (`evidence-artifact.ts`'s own
9
+ * `verdictBasis` shape), so the per-condition census does not parse it: it
10
+ * re-evaluates each row's own `reasons` function against the same fixture
11
+ * input the resolution was built from, the same way `resolveProductionVerdict`
12
+ * and `resolveContractVerdict` do internally.
13
+ */
14
+ import { type ContractAssessment, type ProductionAssessment } from './ladder.ts';
15
+ export type LadderFixtureCase = {
16
+ readonly ladder: 'production';
17
+ readonly assessment: ProductionAssessment;
18
+ } | {
19
+ readonly ladder: 'contract-scoring';
20
+ readonly assessment: ContractAssessment;
21
+ };
22
+ /** The whole document. Throws a worded diagnosis on an empty census cell. */
23
+ export declare function ladderDecisionTable(cases: readonly LadderFixtureCase[]): string;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * AD-21's published decision table, emitted from the two ladders' own
3
+ * condition rows and from the fixture set that exercises them, mirroring
4
+ * `outcome-table.ts`'s builder: pure, so the writer and the drift check
5
+ * cannot disagree about bytes, and every census count throws on zero rather
6
+ * than publishing a coverage claim the fixture set does not back.
7
+ *
8
+ * `LadderResolution.basis` is free text (`evidence-artifact.ts`'s own
9
+ * `verdictBasis` shape), so the per-condition census does not parse it: it
10
+ * re-evaluates each row's own `reasons` function against the same fixture
11
+ * input the resolution was built from, the same way `resolveProductionVerdict`
12
+ * and `resolveContractVerdict` do internally.
13
+ */
14
+ import { CONTRACT_LADDER, LADDER_EXIT_CODES, PRODUCTION_LADDER, resolveContractVerdict, resolveProductionVerdict, } from './ladder.js';
15
+ /** `|` escaped: a guard is prose, and a broken table is still byte-stable. */
16
+ const cell = (text) => text.replace(/\|/g, '\\|');
17
+ const row = (cells) => `| ${cells.map(cell).join(' | ')} |`;
18
+ const table = (headers, rows) => [
19
+ row(headers),
20
+ `| ${headers.map(() => '---').join(' | ')} |`,
21
+ ...rows.map(row),
22
+ ];
23
+ const code = (text) => `\`${text}\``;
24
+ const HEADER = [
25
+ '# AD-21 verdict decision',
26
+ '',
27
+ 'Generated by `npm run generate:ad21-table` from the two rule tables in',
28
+ '`src/core/score/ladder.ts` and from the fixture set that exercises them. Guarded by',
29
+ '`npm run check:ad21-table`, a byte-exact drift check. A hand edit fails that check; regenerate.',
30
+ '',
31
+ 'Each ladder is pure and total: every input value returns a defined resolution and nothing',
32
+ 'throws. Precedence is Invalid, then FAIL, then CONCERNS, then WAIVED, then PASS. Within the',
33
+ 'winning tier every condition that holds contributes to the basis; none is masked by another',
34
+ 'that fired first.',
35
+ '',
36
+ 'The exit-code column names the rung, not `--strict`. Every CONCERNS row shows 0 because that is',
37
+ "the rung's own exit code before `--strict` runs; passing `--strict` promotes the resolution to 1,",
38
+ 'except when every firing row for that resolution has "Evidence condition" `yes`, which `--strict`',
39
+ 'never promotes.',
40
+ ];
41
+ const RUNG_LABELS = ['invalid', 'FAIL', 'CONCERNS', 'WAIVED', 'PASS'];
42
+ const rungLabelOf = (verdict) => verdict === null ? 'invalid' : verdict;
43
+ /**
44
+ * Counts each key across one ladder's cases, then reports the first key no
45
+ * case reached. A zero census is what a narrowed fixture set looks like, and
46
+ * no regeneration repairs it.
47
+ */
48
+ function census(ladderName, label, keys, countOf) {
49
+ return keys.map((key) => {
50
+ const count = countOf(key);
51
+ if (count === 0) {
52
+ throw new Error(`ladderDecisionTable: no ${ladderName} case reaches ${label} ${key}, so the published census would claim coverage the fixture set does not have`);
53
+ }
54
+ return [code(key), String(count)];
55
+ });
56
+ }
57
+ function ladderSection(name, ladderKey, ladderRows, cases) {
58
+ const ladderCases = cases.filter((entry) => entry.ladder === ladderKey);
59
+ if (ladderCases.length === 0) {
60
+ throw new Error(`ladderDecisionTable: no resolved cases for the ${ladderKey} ladder`);
61
+ }
62
+ const resolved = ladderCases.map((entry) => entry.ladder === 'production'
63
+ ? resolveProductionVerdict(entry.assessment)
64
+ : resolveContractVerdict(entry.assessment));
65
+ const conditionRows = ladderRows.map((conditionRow) => [
66
+ code(conditionRow.id),
67
+ conditionRow.rung === 'invalid' ? 'Invalid' : conditionRow.rung,
68
+ conditionRow.guard,
69
+ conditionRow.evidenceCondition ? 'yes' : 'no',
70
+ String(LADDER_EXIT_CODES[conditionRow.rung]),
71
+ ]);
72
+ const rungCensus = census(ladderKey, 'the rung', [...RUNG_LABELS], (key) => resolved.filter((entry) => rungLabelOf(entry.verdict) === key).length);
73
+ const conditionCensus = ladderRows.map((conditionRow) => {
74
+ const count = ladderCases.filter((entry) => conditionRow.reasons(entry.assessment).length > 0).length;
75
+ if (count === 0) {
76
+ throw new Error(`ladderDecisionTable: no ${ladderKey} case reaches the condition ${conditionRow.id}, so the published census would claim coverage the fixture set does not have`);
77
+ }
78
+ return [code(conditionRow.id), String(count)];
79
+ });
80
+ return [
81
+ `## ${name}`,
82
+ '',
83
+ ...table(['Condition', 'Rung', 'Guard', 'Evidence condition', 'Exit code'], conditionRows),
84
+ '',
85
+ `### ${name} census, by rung`,
86
+ '',
87
+ `Over the ${ladderCases.length} resolved ${ladderKey} fixture cases.`,
88
+ '',
89
+ ...table(['Rung', 'Cases'], rungCensus),
90
+ '',
91
+ `### ${name} census, by condition`,
92
+ '',
93
+ ...table(['Condition', 'Cases'], conditionCensus),
94
+ ];
95
+ }
96
+ /** The whole document. Throws a worded diagnosis on an empty census cell. */
97
+ export function ladderDecisionTable(cases) {
98
+ if (cases.length === 0) {
99
+ throw new Error('ladderDecisionTable: no resolved cases, so every census below would be empty');
100
+ }
101
+ const lines = [
102
+ ...HEADER,
103
+ '',
104
+ ...ladderSection('Production ladder', 'production', PRODUCTION_LADDER, cases),
105
+ '',
106
+ ...ladderSection('Contract-scoring ladder', 'contract-scoring', CONTRACT_LADDER, cases),
107
+ ];
108
+ return `${lines.join('\n')}\n`;
109
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * AD-21's total decision table, built for the first time. Owed item 4's
3
+ * remaining three clauses: `ProductionAssessment` and `ContractAssessment`
4
+ * as separate input types, each resolved by its own pure, total,
5
+ * first-match-wins ladder over the same seven state categories AD-21 names.
6
+ *
7
+ * Every rung is derived independently per category, so a condition never
8
+ * masks another that fired in the same tier -- the same independence
9
+ * `outcome.ts`'s Stage A uses for AD-6's invalidating conditions. Precedence
10
+ * across tiers is Invalid, then FAIL, then CONCERNS, then WAIVED, then PASS,
11
+ * exactly AD-21's own order; within a tier every condition that holds is
12
+ * recorded, never only the first.
13
+ *
14
+ * `verdict: null` is AD-21's "Invalid" rung. It is not one of the four
15
+ * `Verdict` members: AD-21 itself says a failed run "never becomes a
16
+ * contract verdict", and exit code 3 sits outside the verdict range
17
+ * `src/cli/exit-codes.ts` defines. This module cannot import that file
18
+ * (`core/` imports only `core/schemas`, AD-1), so the three exit-code
19
+ * literals are repeated here rather than shared.
20
+ *
21
+ * AD-21's own "unrecognised evaluator recommendation value" clause is
22
+ * omitted: `verdict.ts:22-25` records that a schema-valid
23
+ * `EvaluatorRecommendation` cannot carry one, since an unrecognised value
24
+ * fails to parse before either ladder runs.
25
+ */
26
+ import type { IngestCondition } from '../ingest/conditions.ts';
27
+ import { type Severity } from '../schemas/eval-contract.ts';
28
+ import type { CoverageGap, LineageChain, Trials, UncitedFindingGap } from '../schemas/evidence-artifact.ts';
29
+ import type { EvidenceDisclosure } from '../schemas/sealed-run-record.ts';
30
+ import type { EvaluatorRecommendation, Verdict } from '../schemas/verdict.ts';
31
+ import type { OutcomeResolution } from './outcome.ts';
32
+ import type { UnwitnessedQuotation } from './quotation.ts';
33
+ /**
34
+ * One oracle's AD-33 resolution, paired with the two fields `OutcomeResolution`
35
+ * does not carry: `severity` is caller-owned on the persisted `Outcome`
36
+ * (`evidence-artifact.ts`'s own field, sourced from `Behavior.severity`), and
37
+ * `checkResolved` is the caller's own `OutcomeInputs.checkResolution !== null`,
38
+ * needed for AD-21's "no required check resolved" Invalid clause, which
39
+ * `OutcomeResolution` has no field for.
40
+ */
41
+ export type ScoredOutcome = {
42
+ readonly oracleId: string;
43
+ readonly required: boolean;
44
+ readonly severity: Severity;
45
+ readonly checkResolved: boolean;
46
+ readonly resolution: OutcomeResolution;
47
+ };
48
+ /**
49
+ * AD-21's "outcome state" category. A composite rather than a bare
50
+ * `OutcomeState[]`: the three new Invalid conditions below each need
51
+ * per-condition detail a state enum alone cannot carry, so this bundles the
52
+ * per-oracle resolutions, the record-level `auditQuotation` result, and
53
+ * `Trials` alongside the re-execution cap (Design Notes: the AD-6
54
+ * re-execution cap, never AD-12's `Remediation.cap`).
55
+ */
56
+ export type OutcomeStateInputs = {
57
+ readonly outcomes: readonly ScoredOutcome[];
58
+ readonly unwitnessedQuotations: readonly UnwitnessedQuotation[];
59
+ readonly trials: Trials;
60
+ readonly reExecutionCap: number;
61
+ };
62
+ /**
63
+ * AD-21's "evidence-integrity state" category, read for the FAIL rung's
64
+ * "incomplete, over-truncated, unavailable, or internally inconsistent under
65
+ * AD-17" clause. `EvidenceDisclosure` carries only the two caller-declared
66
+ * halves (`reportedIncomplete`, `truncationBound`); the other two have no
67
+ * source anywhere in the tree today (`evidence-disclosure`'s own describe:
68
+ * "unavailable is an ArtifactReference that does not resolve through the
69
+ * corpus port, and internally inconsistent is AD-32's cross-artifact
70
+ * agreement check"), so they arrive declared rather than derived, the same
71
+ * posture `outcome.ts`'s `judgeConduct` and `waiver` inputs take.
72
+ * `overTruncated` is declared for the same reason: it compares the disclosed
73
+ * bound against the evidence actually carried, which this pure function does
74
+ * not read.
75
+ */
76
+ export type EvidenceIntegrityInputs = {
77
+ readonly disclosure: EvidenceDisclosure;
78
+ readonly overTruncated: boolean;
79
+ readonly unavailable: boolean;
80
+ readonly internallyInconsistent: boolean;
81
+ /**
82
+ * AD-16's unaccounted-manifest Invalid condition: `IsolationManifest.violation`.
83
+ * Widened from a single nullable string to an array, matching
84
+ * `ValidatedObservations.isolationViolation`'s shape (`ingest.ts:78`): empty
85
+ * when nothing fired, one entry per offending value.
86
+ */
87
+ readonly isolationViolation: readonly string[];
88
+ /**
89
+ * Eight ingest conditions, each a structured payload `ingest` already
90
+ * computed, `Extract`-typed against `IngestCondition` so a rename in
91
+ * `ingest/conditions.ts` collapses the field's type to `never[]` rather
92
+ * than silently reading a stale shape. Ascending `EvidenceIntegrityInputs`
93
+ * field order matches `INGEST_CONDITION_KINDS`' declaration order.
94
+ */
95
+ readonly duplicateRecordIdentifiers: readonly Extract<IngestCondition, {
96
+ kind: 'duplicate-record-identifier';
97
+ }>[];
98
+ readonly danglingCitations: readonly Extract<IngestCondition, {
99
+ kind: 'dangling-citation';
100
+ }>[];
101
+ readonly danglingDispositionCitations: readonly Extract<IngestCondition, {
102
+ kind: 'dangling-disposition-citation';
103
+ }>[];
104
+ readonly forbiddenInputsNotWithheld: readonly Extract<IngestCondition, {
105
+ kind: 'forbidden-input-not-withheld';
106
+ }>[];
107
+ readonly crossArtifactDisagreements: readonly Extract<IngestCondition, {
108
+ kind: 'cross-artifact-disagreement';
109
+ }>[];
110
+ readonly evaluatorConfigurationAbsent: readonly Extract<IngestCondition, {
111
+ kind: 'evaluator-configuration-absent';
112
+ }>[];
113
+ readonly evaluatorConfigurationDigestMismatches: readonly Extract<IngestCondition, {
114
+ kind: 'evaluator-configuration-digest-mismatch';
115
+ }>[];
116
+ readonly judgeResultsUnscored: readonly Extract<IngestCondition, {
117
+ kind: 'judge-result-unscored';
118
+ }>[];
119
+ /**
120
+ * The two score-computed conditions, pre-rendered like `isolationViolation`
121
+ * rather than structured: `score.ts` computes and renders both itself
122
+ * (neither is an `IngestCondition`), so the ladder only needs to display
123
+ * them.
124
+ */
125
+ readonly operationIdentifierCollisions: readonly string[];
126
+ readonly trialSetDisagreements: readonly string[];
127
+ };
128
+ export type FindingConfidence = {
129
+ readonly findingId: string;
130
+ readonly confidence: number;
131
+ };
132
+ /**
133
+ * The seven category values, common to both modes; AD-21 is explicit the two
134
+ * verdicts "never share a field" beyond this. `uncitedDefectFindings` sits in
135
+ * the "coverage condition" category: owed item 5 calls an uncited defect
136
+ * finding, in contract-scoring, "the strongest available evidence of a
137
+ * coverage gap".
138
+ */
139
+ type AssessmentCommon = {
140
+ readonly outcomeState: OutcomeStateInputs;
141
+ readonly evidenceIntegrity: EvidenceIntegrityInputs;
142
+ readonly evaluatorRecommendation: EvaluatorRecommendation;
143
+ readonly coverageGaps: readonly CoverageGap[];
144
+ /**
145
+ * Owed item 5, per `outcome.ts`'s `uncitedDefectFindingGaps`. Only presence
146
+ * and each entry's `findingId` are read below; the rest is carried for the
147
+ * persisted record.
148
+ */
149
+ readonly uncitedDefectFindings: readonly UncitedFindingGap[];
150
+ readonly findings: readonly FindingConfidence[];
151
+ readonly confidenceThreshold: number;
152
+ /** AD-21's "remediation state": `Remediation.lineageChain`'s conjunction only, never `Remediation.cap` (AD-12's contract-revision cap). */
153
+ readonly remediationState: LineageChain;
154
+ /** AD-21's "pre-flight state": `PreflightVerdict.passed`. */
155
+ readonly preflightPassed: boolean;
156
+ readonly severityFloor: Severity;
157
+ };
158
+ /**
159
+ * A literal `mode` discriminant, mirroring `EvidenceArtifact`'s own union, so
160
+ * the two types are nominally distinct rather than structurally compatible in
161
+ * one direction only.
162
+ */
163
+ export type ProductionAssessment = AssessmentCommon & {
164
+ readonly mode: 'production';
165
+ };
166
+ export type ContractAssessment = AssessmentCommon & {
167
+ readonly mode: 'contract-scoring';
168
+ readonly systemRecommendationRecorded: EvaluatorRecommendation;
169
+ readonly systemRecommendationNote: string | null;
170
+ };
171
+ export type LadderResolution = {
172
+ /** `null` is AD-21's Invalid rung: never a `Verdict`, and a run that never becomes a contract verdict. */
173
+ readonly verdict: Verdict | null;
174
+ readonly exitCode: number;
175
+ /** `false` for a CONCERNS whose only firing conditions are the two evidence conditions; `cli/exit-codes.ts`'s `evidenceConditionsOnly` semantics, restated for the ladder to agree with rather than re-derive. */
176
+ readonly strictPromotable: boolean;
177
+ readonly basis: readonly string[];
178
+ };
179
+ /**
180
+ * AD-21's exit codes for this rung. Not imported from `src/cli/exit-codes.ts`:
181
+ * `core/` imports only `core/schemas` (AD-1), so the three numbers are
182
+ * repeated here rather than shared. Exported so a test outside `core/` can
183
+ * assert this restatement still agrees with that file's own `EXIT_*`
184
+ * constants and `evidenceConditionsOnly` rule, which neither file can check
185
+ * of itself.
186
+ */
187
+ export declare const LADDER_EXIT_CODES: {
188
+ readonly invalid: 3;
189
+ readonly FAIL: 2;
190
+ readonly CONCERNS: 0;
191
+ readonly WAIVED: 0;
192
+ readonly PASS: 0;
193
+ };
194
+ /**
195
+ * One condition this table can independently confirm or deny. `reasons`
196
+ * returns zero or more free-text entries (`evidence-artifact.ts`'s own
197
+ * `verdictBasis` shape: non-empty strings, empty array legal); more than one
198
+ * entry names each affected oracle, gap, or finding separately, matching
199
+ * AD-21's "the record carries every condition that fired".
200
+ */
201
+ type LadderConditionRow = {
202
+ readonly id: string;
203
+ readonly rung: 'invalid' | 'FAIL' | 'CONCERNS' | 'WAIVED';
204
+ readonly guard: string;
205
+ /** the two evidence conditions AD-21 names as a thinner measurement rather than a system claim; `strictPromotable` reads this. */
206
+ readonly evidenceCondition: boolean;
207
+ readonly reasons: (inputs: AssessmentCommon) => readonly string[];
208
+ };
209
+ /**
210
+ * Production's ladder: the shared rows plus the two rows that read an
211
+ * ingested evaluator recommendation, which contract-scoring's never does.
212
+ */
213
+ export declare const PRODUCTION_LADDER: readonly LadderConditionRow[];
214
+ /**
215
+ * Contract-scoring's ladder: FAIL/CONCERNS/WAIVED/PASS mirror production's
216
+ * minus every clause that promotes `evaluatorRecommendation`, per AD-21.
217
+ */
218
+ export declare const CONTRACT_LADDER: readonly LadderConditionRow[];
219
+ /** AD-21's production-mode ladder: the subject is the system under test. */
220
+ export declare function resolveProductionVerdict(assessment: ProductionAssessment): LadderResolution;
221
+ /**
222
+ * AD-21's contract-scoring ladder: the subject is the contract. No row here
223
+ * reads `evaluatorRecommendation`; `systemRecommendationRecorded` and
224
+ * `systemRecommendationNote` are carried on the input for symmetry with
225
+ * `EvidenceArtifact`'s own contract-scoring branch and are read by nothing in
226
+ * this module.
227
+ */
228
+ export declare function resolveContractVerdict(assessment: ContractAssessment): LadderResolution;
229
+ export {};