lib/closure/src/qualify/owner.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const model = @import("model.zig");
2 const schema = @import("../schema/root.zig");
3 const std = @import("std");
4
5 const OwnerError = error{
6 BoundRangeInvalid,
7 CapacityPairInvalid,
8 CellCapacityExceeded,
9 CellDuplicate,
10 CellInvalid,
11 EvidenceMismatch,
12 PlanDigestMissing,
13 PlanEmpty,
14 PlanRequiredMissing,
15 PlanWorkOverflow,
16 ResultCountMismatch,
17 ResultOrderMismatch,
18 SeedRangeInvalid,
19 VerdictMismatch,
20 };
21
22 pub const Error = OwnerError;
23
24 pub fn validatePlan(plan: model.Plan) Error!void {
25 if (plan.cells.len == 0) return error.PlanEmpty;
26 if (plan.cells.len > model.cells_max or
27 plan.bounds.len > model.bounds_max or
28 plan.seeds.len > model.seeds_max)
29 {
30 return error.CellCapacityExceeded;
31 }
32 if (plan.claim.isEmpty() or
33 !plan.artifact_sha256.isKnown() or
34 !plan.execution_profile_sha256.isKnown())
35 {
36 return error.PlanDigestMissing;
37 }
38 var required_count: u16 = 0;
39 var work_max: u64 = 0;
40 for (plan.cells, 0..) |*cell, index| {
41 try validateCell(plan, cell);
42 if (cell.requirement == .required) {
43 required_count += 1;
44 work_max = std.math.add(
45 u64,
46 work_max,
47 cell.work_max,
48 ) catch return error.PlanWorkOverflow;
49 }
50 for (plan.cells[0..index]) |*prior| {
51 if (cell.id.eql(&prior.id)) return error.CellDuplicate;
52 }
53 }
54 if (required_count == 0) return error.PlanRequiredMissing;
55 try validateCapacityPairs(plan);
56 }
57
58 pub fn evaluate(
59 frozen: model.Frozen,
60 rows: []const model.Row,
61 ) Error!model.Summary {
62 try validatePlan(frozen.plan);
63 if (!frozen.sha256.isKnown()) return error.PlanDigestMissing;
64 if (rows.len != frozen.plan.cells.len) {
65 return error.ResultCountMismatch;
66 }
67 var summary = model.Summary{
68 .verdict = .pass,
69 .passed = 0,
70 .refuted = 0,
71 .inconclusive = 0,
72 .not_exercised = 0,
73 .work = 0,
74 .high_water = 0,
75 .allocation_count_after_seal = 0,
76 };
77 for (frozen.plan.cells, rows, 0..) |*cell, *row, index| {
78 if (!cell.id.eql(&row.cell)) return error.ResultOrderMismatch;
79 if (row.evidence != cell.evidence) return error.EvidenceMismatch;
80 var expected = classify(cell, row);
81 if (expected == .pass and
82 cell.capacity_case != .none and
83 !pairEvidenceEquivalent(
84 frozen.plan,
85 rows,
86 index,
87 ))
88 {
89 expected = .refuted;
90 }
91 if (expected == .pass and
92 cell.kind == .slo and
93 (!row.statistics.baseline_profile_sha256.eql(
94 &frozen.plan.execution_profile_sha256,
95 ) or
96 !row.statistics.candidate_profile_sha256.eql(
97 &frozen.plan.execution_profile_sha256,
98 )))
99 {
100 expected = .refuted;
101 }
102 if (row.verdict != expected) return error.VerdictMismatch;
103 switch (row.verdict) {
104 .pass => summary.passed += 1,
105 .refuted => summary.refuted += 1,
106 .inconclusive => summary.inconclusive += 1,
107 .not_exercised => summary.not_exercised += 1,
108 }
109 if (cell.requirement == .required) {
110 summary.work = std.math.add(
111 u64,
112 summary.work,
113 row.work,
114 ) catch std.math.maxInt(u64);
115 summary.high_water = @max(summary.high_water, row.high_water);
116 summary.allocation_count_after_seal = std.math.add(
117 u64,
118 summary.allocation_count_after_seal,
119 row.allocation_count_after_seal,
120 ) catch std.math.maxInt(u64);
121 }
122 }
123 summary.verdict = if (summary.refuted != 0)
124 .refuted
125 else if (summary.inconclusive != 0)
126 .inconclusive
127 else
128 .pass;
129 return summary;
130 }
131
132 pub fn classify(
133 cell: *const model.Cell,
134 row: *const model.Row,
135 ) schema.Verdict {
136 if (cell.requirement == .excluded) return .not_exercised;
137 if (row.capture != .complete) return .inconclusive;
138 if (cell.threshold.kind == .none) return .inconclusive;
139 if (cell.evidence == .assumption) return .inconclusive;
140 if (!evidenceComplete(cell, row)) return .refuted;
141 if (!row.contract_pass) return .refuted;
142 return switch (cell.kind) {
143 .correctness => if (cell.threshold.kind == .correctness)
144 .pass
145 else
146 .inconclusive,
147 .slo => sloVerdict(cell, row.statistics),
148 };
149 }
150
151 fn validateCell(plan: model.Plan, cell: *const model.Cell) Error!void {
152 if (cell.id.isEmpty() or
153 cell.owner.isEmpty() or
154 cell.workload.isEmpty() or
155 cell.fault.isEmpty() or
156 cell.overload_counter.isEmpty() or
157 cell.loss_counter.isEmpty() or
158 cell.repetitions == 0 or
159 cell.work_max == 0 or
160 cell.trace_steps_max == 0)
161 {
162 return error.CellInvalid;
163 }
164 switch (cell.threshold.kind) {
165 .none, .correctness => if (cell.threshold.value != 0 or
166 cell.threshold.confidence_ppm != 0)
167 {
168 return error.CellInvalid;
169 },
170 .absolute_max, .baseline_delta_max => if (cell.threshold.confidence_ppm == 0 or
171 cell.threshold.confidence_ppm > 1_000_000)
172 {
173 return error.CellInvalid;
174 },
175 }
176 const bound_end = std.math.add(
177 usize,
178 cell.bound_start,
179 cell.bound_count,
180 ) catch return error.BoundRangeInvalid;
181 if (cell.bound_count == 0 or bound_end > plan.bounds.len) {
182 return error.BoundRangeInvalid;
183 }
184 const seed_end = std.math.add(
185 usize,
186 cell.seed_start,
187 cell.seed_count,
188 ) catch return error.SeedRangeInvalid;
189 if (cell.seed_count == 0 or seed_end > plan.seeds.len) {
190 return error.SeedRangeInvalid;
191 }
192 switch (cell.kind) {
193 .correctness => if (cell.requirement == .required and
194 cell.threshold.kind != .correctness and
195 cell.threshold.kind != .none)
196 {
197 return error.CellInvalid;
198 },
199 .slo => if (cell.requirement == .required and
200 (cell.build_mode != .release_fast or
201 cell.evidence != .measurement or
202 cell.repetitions < 2 or
203 cell.threshold.kind == .correctness or
204 (cell.threshold.kind != .none and
205 cell.threshold.confidence_ppm == 0)))
206 {
207 return error.CellInvalid;
208 },
209 }
210 switch (cell.capacity_case) {
211 .none => {},
212 .at_capacity => if (cell.scale != cell.capacity) {
213 return error.CapacityPairInvalid;
214 },
215 .over_capacity => {
216 const over = std.math.add(
217 u64,
218 cell.capacity,
219 1,
220 ) catch return error.CapacityPairInvalid;
221 if (cell.scale != over) return error.CapacityPairInvalid;
222 },
223 }
224 }
225
226 fn validateCapacityPairs(plan: model.Plan) Error!void {
227 for (plan.cells) |*cell| {
228 if (cell.capacity_case == .none) continue;
229 const wanted: model.CapacityCase =
230 if (cell.capacity_case == .at_capacity)
231 .over_capacity
232 else
233 .at_capacity;
234 var matches: u8 = 0;
235 for (plan.cells) |*candidate| {
236 if (candidate.capacity_case != wanted or
237 candidate.capacity != cell.capacity or
238 !capacityCellsEquivalent(plan, cell, candidate))
239 {
240 continue;
241 }
242 matches += 1;
243 }
244 if (matches != 1) return error.CapacityPairInvalid;
245 }
246 }
247
248 fn capacityCellsEquivalent(
249 plan: model.Plan,
250 left: *const model.Cell,
251 right: *const model.Cell,
252 ) bool {
253 if (left.requirement != right.requirement or
254 left.kind != right.kind or
255 left.evidence != right.evidence or
256 left.phase != right.phase or
257 left.build_mode != right.build_mode or
258 left.repetitions != right.repetitions or
259 left.threshold.kind != right.threshold.kind or
260 left.threshold.value != right.threshold.value or
261 left.threshold.confidence_ppm !=
262 right.threshold.confidence_ppm or
263 left.work_max != right.work_max or
264 left.trace_steps_max != right.trace_steps_max or
265 !left.owner.eql(&right.owner) or
266 !left.workload.eql(&right.workload) or
267 !left.fault.eql(&right.fault) or
268 !left.overload_counter.eql(&right.overload_counter) or
269 !left.loss_counter.eql(&right.loss_counter) or
270 left.bound_count != right.bound_count or
271 left.seed_count != right.seed_count)
272 {
273 return false;
274 }
275 const left_bound_start: usize = left.bound_start;
276 const right_bound_start: usize = right.bound_start;
277 const bound_count: usize = left.bound_count;
278 for (
279 plan.bounds[left_bound_start..][0..bound_count],
280 plan.bounds[right_bound_start..][0..bound_count],
281 ) |*left_bound, *right_bound| {
282 if (left_bound.value != right_bound.value or
283 !left_bound.name.eql(&right_bound.name))
284 {
285 return false;
286 }
287 }
288 const left_seed_start: usize = left.seed_start;
289 const right_seed_start: usize = right.seed_start;
290 const seed_count: usize = left.seed_count;
291 return std.mem.eql(
292 u64,
293 plan.seeds[left_seed_start..][0..seed_count],
294 plan.seeds[right_seed_start..][0..seed_count],
295 );
296 }
297
298 fn pairEvidenceEquivalent(
299 plan: model.Plan,
300 rows: []const model.Row,
301 index: usize,
302 ) bool {
303 const cell = &plan.cells[index];
304 const wanted: model.CapacityCase =
305 if (cell.capacity_case == .at_capacity)
306 .over_capacity
307 else
308 .at_capacity;
309 for (plan.cells, rows) |*candidate, *row| {
310 if (candidate.capacity_case != wanted or
311 candidate.capacity != cell.capacity or
312 !capacityCellsEquivalent(plan, cell, candidate))
313 {
314 continue;
315 }
316 const evidence = &rows[index];
317 return evidence.before_digest.eql(&row.before_digest) and
318 evidence.effect_before_digest.eql(
319 &row.effect_before_digest,
320 ) and
321 evidence.storage_address_before ==
322 row.storage_address_before and
323 evidence.storage_capacity_before ==
324 row.storage_capacity_before and
325 evidence.overload_count_before ==
326 row.overload_count_before and
327 evidence.loss_count_before == row.loss_count_before;
328 }
329 return false;
330 }
331
332 fn evidenceComplete(
333 cell: *const model.Cell,
334 row: *const model.Row,
335 ) bool {
336 if (!row.before_digest.isKnown() or
337 !row.after_digest.isKnown() or
338 !row.effect_before_digest.isKnown() or
339 !row.effect_after_digest.isKnown() or
340 row.evidence_path.isEmpty() or
341 row.trace_path.isEmpty() or
342 row.allocation_count_after_seal != 0 or
343 row.storage_address_before == 0 or
344 row.storage_address_before != row.storage_address_after or
345 row.storage_capacity_before != row.storage_capacity_after or
346 row.storage_capacity_before != cell.capacity or
347 row.high_water > cell.capacity or
348 row.work > cell.work_max or
349 row.undeclared_effect_count != 0 or
350 row.undeclared_state_change_count != 0)
351 {
352 return false;
353 }
354 const accepted = std.math.add(
355 u64,
356 row.outcomes.completed,
357 row.outcomes.dropped,
358 ) catch return false;
359 const conserved = std.math.add(
360 u64,
361 accepted,
362 row.outcomes.outstanding,
363 ) catch return false;
364 const offered = std.math.add(
365 u64,
366 row.outcomes.accepted,
367 row.outcomes.rejected,
368 ) catch return false;
369 if (conserved != row.outcomes.accepted or
370 offered != row.outcomes.offered or
371 row.outcomes.offered != cell.scale)
372 {
373 return false;
374 }
375 if (!capacityTransitionComplete(cell, row)) return false;
376 return switch (cell.capacity_case) {
377 .none => row.transition == .none,
378 .at_capacity => row.transition == .none and
379 row.outcomes.accepted == row.outcomes.offered and
380 row.outcomes.completed == row.outcomes.accepted and
381 row.outcomes.rejected == 0 and
382 row.outcomes.dropped == 0 and
383 row.outcomes.outstanding == 0,
384 .over_capacity => row.transition != .none and
385 row.outcomes.outstanding == 0 and
386 row.outcomes.completed +| row.outcomes.dropped ==
387 row.outcomes.accepted,
388 };
389 }
390
391 fn capacityTransitionComplete(
392 cell: *const model.Cell,
393 row: *const model.Row,
394 ) bool {
395 if (cell.capacity_case != .over_capacity) {
396 return row.overload_count_before ==
397 row.overload_count_after and
398 row.loss_count_before == row.loss_count_after;
399 }
400 const overload_after = std.math.add(
401 u64,
402 row.overload_count_before,
403 1,
404 ) catch return false;
405 if (row.overload_count_after != overload_after) return false;
406 return switch (row.transition) {
407 .reject, .backpressure => row.outcomes.rejected == 1 and
408 row.outcomes.dropped == 0 and
409 row.loss_count_before == row.loss_count_after,
410 .drop, .replace => blk: {
411 const loss_after = std.math.add(
412 u64,
413 row.loss_count_before,
414 1,
415 ) catch break :blk false;
416 break :blk row.outcomes.rejected == 0 and
417 row.outcomes.dropped == 1 and
418 row.loss_count_after == loss_after;
419 },
420 .none => false,
421 };
422 }
423
424 fn sloVerdict(
425 cell: *const model.Cell,
426 statistics: model.Statistics,
427 ) schema.Verdict {
428 if (!statistics.present or
429 !statistics.interval_present or
430 !statistics.interleaved or
431 !statistics.baseline_profile_sha256.isKnown() or
432 !statistics.candidate_profile_sha256.isKnown() or
433 !statistics.baseline_profile_sha256.eql(
434 &statistics.candidate_profile_sha256,
435 ) or
436 statistics.sample_count < cell.repetitions or
437 statistics.baseline_count != statistics.candidate_count or
438 statistics.baseline_count == 0 or
439 @as(u64, statistics.sample_count) !=
440 @as(u64, statistics.baseline_count) +
441 statistics.candidate_count)
442 {
443 return .inconclusive;
444 }
445 if (statistics.minimum > statistics.p50 or
446 statistics.p50 > statistics.p95 or
447 statistics.p95 > statistics.p99 or
448 statistics.p99 > statistics.maximum or
449 statistics.interval_low > statistics.interval_high)
450 {
451 return .inconclusive;
452 }
453 if (statistics.baseline_high_water > cell.capacity or
454 statistics.candidate_high_water > cell.capacity)
455 {
456 return .refuted;
457 }
458 return switch (cell.threshold.kind) {
459 .absolute_max, .baseline_delta_max => if (statistics.interval_high <= cell.threshold.value)
460 .pass
461 else
462 .refuted,
463 .none, .correctness => .inconclusive,
464 };
465 }
466
467 fn name(value: []const u8) schema.Name {
468 return schema.Name.init(value) catch unreachable;
469 }
470
471 fn descriptor(value: []const u8) schema.Descriptor {
472 return schema.Descriptor.init(value) catch unreachable;
473 }
474
475 fn digest(byte: u8) schema.Digest {
476 return .{ .bytes = @splat(byte) };
477 }
478
479 fn capacityPlan(cells: []const model.Cell) model.Plan {
480 const bounds = struct {
481 const values = [_]model.Bound{.{
482 .name = name("queue"),
483 .value = 8,
484 }};
485 }.values;
486 const seeds = struct {
487 const values = [_]u64{7};
488 }.values;
489 return .{
490 .claim = descriptor("bounded queue qualification"),
491 .artifact_sha256 = digest(1),
492 .execution_profile_sha256 = digest(2),
493 .cells = cells,
494 .bounds = &bounds,
495 .seeds = &seeds,
496 };
497 }
498
499 fn capacityCell(
500 id: []const u8,
501 scale: u64,
502 case: model.CapacityCase,
503 ) model.Cell {
504 return .{
505 .id = name(id),
506 .owner = name("queue"),
507 .requirement = .required,
508 .kind = .correctness,
509 .evidence = .finite_exhaustive,
510 .workload = descriptor("enqueue"),
511 .phase = .validation,
512 .scale = scale,
513 .build_mode = .debug,
514 .fault = descriptor("none"),
515 .overload_counter = name("queue_overloads"),
516 .loss_counter = name("queue_losses"),
517 .bound_start = 0,
518 .bound_count = 1,
519 .seed_start = 0,
520 .seed_count = 1,
521 .repetitions = 1,
522 .threshold = .{ .kind = .correctness },
523 .capacity_case = case,
524 .capacity = 8,
525 .work_max = 100,
526 .trace_steps_max = 100,
527 };
528 }
529
530 fn makeRow(
531 id: []const u8,
532 scale: u64,
533 transition: model.Transition,
534 ) model.Row {
535 const rejected: u64 = if (transition == .none) 0 else 1;
536 const accepted = scale - rejected;
537 return .{
538 .cell = name(id),
539 .evidence = .finite_exhaustive,
540 .verdict = .pass,
541 .capture = .complete,
542 .contract_pass = true,
543 .outcomes = .{
544 .offered = scale,
545 .accepted = accepted,
546 .completed = accepted,
547 .rejected = rejected,
548 .dropped = 0,
549 .outstanding = 0,
550 },
551 .before_digest = digest(3),
552 .after_digest = digest(4),
553 .effect_before_digest = digest(5),
554 .effect_after_digest = digest(6),
555 .high_water = accepted,
556 .work = scale,
557 .allocation_count_after_seal = 0,
558 .storage_address_before = 0x1000,
559 .storage_address_after = 0x1000,
560 .storage_capacity_before = 8,
561 .storage_capacity_after = 8,
562 .transition = transition,
563 .overload_count_before = 7,
564 .overload_count_after = if (transition == .none) 7 else 8,
565 .loss_count_before = 3,
566 .loss_count_after = 3,
567 .undeclared_effect_count = 0,
568 .undeclared_state_change_count = 0,
569 .statistics = .{},
570 .evidence_path = descriptor("evidence.jsonl"),
571 .trace_path = descriptor("trace.jsonl"),
572 };
573 }
574
575 test "qualification evaluates paired C and C plus one evidence" {
576 const cells = [_]model.Cell{
577 capacityCell("queue-c", 8, .at_capacity),
578 capacityCell("queue-c-plus-one", 9, .over_capacity),
579 };
580 const plan = capacityPlan(&cells);
581 try validatePlan(plan);
582 const rows = [_]model.Row{
583 makeRow("queue-c", 8, .none),
584 makeRow("queue-c-plus-one", 9, .reject),
585 };
586 const summary = try evaluate(.{
587 .plan = plan,
588 .sha256 = digest(7),
589 .bytes = "{}",
590 }, &rows);
591 try std.testing.expectEqual(schema.Verdict.pass, summary.verdict);
592 try std.testing.expectEqual(@as(u16, 2), summary.passed);
593 try std.testing.expectEqual(@as(u64, 0), summary.allocation_count_after_seal);
594 }
595
596 test "qualification forces incomplete capture to inconclusive" {
597 const cells = [_]model.Cell{
598 capacityCell("queue-c", 8, .at_capacity),
599 capacityCell("queue-c-plus-one", 9, .over_capacity),
600 };
601 const plan = capacityPlan(&cells);
602 var rows = [_]model.Row{
603 makeRow("queue-c", 8, .none),
604 makeRow("queue-c-plus-one", 9, .reject),
605 };
606 rows[1].capture = .trace_capacity_exceeded;
607 rows[1].verdict = .inconclusive;
608 const summary = try evaluate(.{
609 .plan = plan,
610 .sha256 = digest(7),
611 .bytes = "{}",
612 }, &rows);
613 try std.testing.expectEqual(
614 schema.Verdict.inconclusive,
615 summary.verdict,
616 );
617 try std.testing.expectEqual(@as(u16, 1), summary.inconclusive);
618 }
619
620 test "qualification refutes broken conservation and overload transitions" {
621 const cell = capacityCell("queue-c-plus-one", 9, .over_capacity);
622 var evidence = makeRow("queue-c-plus-one", 9, .reject);
623 evidence.outcomes.completed = 7;
624 try std.testing.expectEqual(
625 schema.Verdict.refuted,
626 classify(&cell, &evidence),
627 );
628 evidence = makeRow("queue-c-plus-one", 9, .none);
629 try std.testing.expectEqual(
630 schema.Verdict.refuted,
631 classify(&cell, &evidence),
632 );
633 evidence = makeRow("queue-c-plus-one", 9, .reject);
634 evidence.outcomes.completed = 7;
635 evidence.outcomes.outstanding = 1;
636 try std.testing.expectEqual(
637 schema.Verdict.refuted,
638 classify(&cell, &evidence),
639 );
640 const at_capacity = capacityCell("queue-c", 8, .at_capacity);
641 evidence = makeRow("queue-c", 8, .none);
642 evidence.outcomes.completed = 7;
643 evidence.outcomes.outstanding = 1;
644 try std.testing.expectEqual(
645 schema.Verdict.refuted,
646 classify(&at_capacity, &evidence),
647 );
648 }
649
650 test "qualification requires interval-backed interleaved SLO evidence" {
651 var cell = capacityCell("queue-slo", 8, .none);
652 cell.kind = .slo;
653 cell.evidence = .measurement;
654 cell.build_mode = .release_fast;
655 cell.repetitions = 4;
656 cell.threshold = .{
657 .kind = .absolute_max,
658 .value = 100,
659 .confidence_ppm = 950_000,
660 };
661 var evidence = makeRow("queue-slo", 8, .none);
662 evidence.evidence = .measurement;
663 evidence.statistics = .{
664 .present = true,
665 .baseline_profile_sha256 = digest(2),
666 .candidate_profile_sha256 = digest(2),
667 .sample_count = 4,
668 .baseline_count = 2,
669 .candidate_count = 2,
670 .minimum = 50,
671 .p50 = 60,
672 .p95 = 70,
673 .p99 = 80,
674 .maximum = 90,
675 .baseline_high_water = 8,
676 .candidate_high_water = 8,
677 };
678 try std.testing.expectEqual(
679 schema.Verdict.inconclusive,
680 classify(&cell, &evidence),
681 );
682 evidence.statistics.interval_present = true;
683 evidence.statistics.interleaved = true;
684 evidence.statistics.interval_low = 55;
685 evidence.statistics.interval_high = 95;
686 try std.testing.expectEqual(
687 schema.Verdict.pass,
688 classify(&cell, &evidence),
689 );
690 evidence.statistics.interval_high = 101;
691 try std.testing.expectEqual(
692 schema.Verdict.refuted,
693 classify(&cell, &evidence),
694 );
695 }
696
697 test "qualification binds SLO evidence to the frozen execution profile" {
698 var cell = capacityCell("queue-slo", 8, .none);
699 cell.kind = .slo;
700 cell.evidence = .measurement;
701 cell.build_mode = .release_fast;
702 cell.repetitions = 4;
703 cell.threshold = .{
704 .kind = .baseline_delta_max,
705 .value = 100,
706 .confidence_ppm = 950_000,
707 };
708 const cells = [_]model.Cell{cell};
709 const plan = capacityPlan(&cells);
710 var row = makeRow("queue-slo", 8, .none);
711 row.evidence = .measurement;
712 row.verdict = .refuted;
713 row.statistics = .{
714 .present = true,
715 .interval_present = true,
716 .interleaved = true,
717 .baseline_profile_sha256 = digest(9),
718 .candidate_profile_sha256 = digest(9),
719 .sample_count = 4,
720 .baseline_count = 2,
721 .candidate_count = 2,
722 .minimum = 50,
723 .p50 = 60,
724 .p95 = 70,
725 .p99 = 80,
726 .maximum = 90,
727 .interval_low = 55,
728 .interval_high = 95,
729 .baseline_high_water = 8,
730 .candidate_high_water = 8,
731 };
732 const rows = [_]model.Row{row};
733 const summary = try evaluate(.{
734 .plan = plan,
735 .sha256 = digest(7),
736 .bytes = "{}",
737 }, &rows);
738 try std.testing.expectEqual(schema.Verdict.refuted, summary.verdict);
739 }
740
741 test "qualification leaves excluded cells not exercised" {
742 var cell = capacityCell("queue-excluded", 8, .none);
743 cell.requirement = .excluded;
744 const cells = [_]model.Cell{
745 capacityCell("queue-required", 8, .none),
746 cell,
747 };
748 const plan = capacityPlan(&cells);
749 var excluded = makeRow("queue-excluded", 8, .none);
750 excluded.verdict = .not_exercised;
751 excluded.work = std.math.maxInt(u64);
752 const rows = [_]model.Row{
753 makeRow("queue-required", 8, .none),
754 excluded,
755 };
756 const summary = try evaluate(.{
757 .plan = plan,
758 .sha256 = digest(7),
759 .bytes = "{}",
760 }, &rows);
761 try std.testing.expectEqual(schema.Verdict.pass, summary.verdict);
762 try std.testing.expectEqual(@as(u16, 1), summary.not_exercised);
763 try std.testing.expectEqual(@as(u64, 8), summary.work);
764 }
765
766 test "qualification rejects vacuous plans and assumptions cannot pass" {
767 var excluded = capacityCell("queue-excluded", 8, .none);
768 excluded.requirement = .excluded;
769 const excluded_cells = [_]model.Cell{excluded};
770 try std.testing.expectError(
771 error.PlanRequiredMissing,
772 validatePlan(capacityPlan(&excluded_cells)),
773 );
774 var assumption = capacityCell("queue-assumption", 8, .none);
775 assumption.evidence = .assumption;
776 var row = makeRow("queue-assumption", 8, .none);
777 row.evidence = .assumption;
778 try std.testing.expectEqual(
779 schema.Verdict.inconclusive,
780 classify(&assumption, &row),
781 );
782 }
783
784 test "qualification rejects malformed threshold confidence" {
785 var cell = capacityCell("queue-slo", 8, .none);
786 cell.kind = .slo;
787 cell.evidence = .measurement;
788 cell.build_mode = .release_fast;
789 cell.repetitions = 2;
790 cell.threshold = .{
791 .kind = .absolute_max,
792 .value = 100,
793 .confidence_ppm = 1_000_001,
794 };
795 const cells = [_]model.Cell{cell};
796 try std.testing.expectError(
797 error.CellInvalid,
798 validatePlan(capacityPlan(&cells)),
799 );
800 }
801
802 test "qualification rejects an unpaired bounded owner" {
803 const cells = [_]model.Cell{
804 capacityCell("queue-c", 8, .at_capacity),
805 };
806 try std.testing.expectError(
807 error.CapacityPairInvalid,
808 validatePlan(capacityPlan(&cells)),
809 );
810 }
811
812 test "qualification records missing thresholds as inconclusive" {
813 var cells = [_]model.Cell{
814 capacityCell("queue-c", 8, .at_capacity),
815 capacityCell("queue-c-plus-one", 9, .over_capacity),
816 };
817 cells[0].threshold = .{ .kind = .none };
818 cells[1].threshold = .{ .kind = .none };
819 const plan = capacityPlan(&cells);
820 try validatePlan(plan);
821 var rows = [_]model.Row{
822 makeRow("queue-c", 8, .none),
823 makeRow("queue-c-plus-one", 9, .reject),
824 };
825 rows[0].capture = .missing_threshold;
826 rows[0].verdict = .inconclusive;
827 rows[1].capture = .missing_threshold;
828 rows[1].verdict = .inconclusive;
829 const summary = try evaluate(.{
830 .plan = plan,
831 .sha256 = digest(7),
832 .bytes = "{}",
833 }, &rows);
834 try std.testing.expectEqual(
835 schema.Verdict.inconclusive,
836 summary.verdict,
837 );
838 try std.testing.expectEqual(@as(u16, 2), summary.inconclusive);
839 }
840
841 test "qualification requires equivalent C and C plus one state" {
842 const cells = [_]model.Cell{
843 capacityCell("queue-c", 8, .at_capacity),
844 capacityCell("queue-c-plus-one", 9, .over_capacity),
845 };
846 const plan = capacityPlan(&cells);
847 var rows = [_]model.Row{
848 makeRow("queue-c", 8, .none),
849 makeRow("queue-c-plus-one", 9, .reject),
850 };
851 rows[1].before_digest = digest(9);
852 rows[0].verdict = .refuted;
853 rows[1].verdict = .refuted;
854 const summary = try evaluate(.{
855 .plan = plan,
856 .sha256 = digest(7),
857 .bytes = "{}",
858 }, &rows);
859 try std.testing.expectEqual(schema.Verdict.refuted, summary.verdict);
860 try std.testing.expectEqual(@as(u16, 2), summary.refuted);
861 rows[1] = makeRow("queue-c-plus-one", 9, .reject);
862 rows[1].overload_count_before += 1;
863 rows[1].overload_count_after += 1;
864 rows[0].verdict = .refuted;
865 rows[1].verdict = .refuted;
866 const counter_summary = try evaluate(.{
867 .plan = plan,
868 .sha256 = digest(7),
869 .bytes = "{}",
870 }, &rows);
871 try std.testing.expectEqual(
872 schema.Verdict.refuted,
873 counter_summary.verdict,
874 );
875 }
876
877 test "qualification requires declared overload and loss counters" {
878 const cell = capacityCell("queue-c-plus-one", 9, .over_capacity);
879 var rejected = makeRow("queue-c-plus-one", 9, .reject);
880 rejected.overload_count_after = rejected.overload_count_before;
881 try std.testing.expectEqual(
882 schema.Verdict.refuted,
883 classify(&cell, &rejected),
884 );
885 var dropped = makeRow("queue-c-plus-one", 9, .drop);
886 dropped.outcomes.accepted = 9;
887 dropped.outcomes.completed = 8;
888 dropped.outcomes.rejected = 0;
889 dropped.outcomes.dropped = 1;
890 dropped.loss_count_after = dropped.loss_count_before + 1;
891 try std.testing.expectEqual(
892 schema.Verdict.pass,
893 classify(&cell, &dropped),
894 );
895 }