codereview-loop 0.13.0__py3-none-any.whl

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.
codereview/pilot.py ADDED
@@ -0,0 +1,549 @@
1
+ """B3 dev-pilot harness scaffolding (B3 design §8, 2026-11-06).
2
+
3
+ Mechanism-agnostic data collection for the B3 dev pilot. This
4
+ module deliberately contains NO promotion mechanism: no new gate
5
+ profile, no probe-promotion decision, no prediction. It provides
6
+ the three things the pilot needs regardless of which mechanism
7
+ the owner approves:
8
+
9
+ 1. **The split** (§8 step 1): a deterministic hash partition of
10
+ the pilot's dev cases into calibration and evaluation slices,
11
+ plus a manifest that hash-commits the split inputs (salt +
12
+ sorted case ids + the rule), so a split cannot be re-rolled
13
+ silently — recomputing the manifest over different inputs
14
+ yields a different `inputs_sha256`, and `verify_split_manifest`
15
+ detects any drift between a manifest and a case set.
16
+
17
+ 2. **Eligibility screening** (§3.1/§3.2): a deterministic,
18
+ rule-based screen over a finding's own fields — blocking
19
+ tier, a falsifiable behavioural claim in the A5 taxonomy's
20
+ executable bases, and a concrete trigger under the same test
21
+ B2's R4 leg uses (core._trigger_concrete). Leg 3 of §3.1
22
+ (executable reachability in the pinned runtime) cannot be
23
+ judged from finding text; it is NOT screened here — it
24
+ surfaces in the probe outcome the run records, which is why
25
+ the calibration table carries both. The screen is a
26
+ measurement heuristic in A5's spirit (a claim-text estimate,
27
+ not ground truth); every row records its basis so a future
28
+ registration can re-derive or replace it.
29
+
30
+ 3. **The calibration table**: per case — the live run's
31
+ findings with eligibility and probe outcome per finding,
32
+ and every CURRENT gate profile's verdict replayed through
33
+ core.evaluate_gate_policy (the one evaluation entry point;
34
+ replay = live) — plus the raw factor inputs the §8
35
+ prediction formula consumes (p_base / p_elig / p_settle) as
36
+ counts and ratios. The factors are INPUTS ONLY: the formula,
37
+ its Wilson bounds, and any registration of a prediction are
38
+ the pilot registration's business, after the owner's design
39
+ approval — nothing here computes or registers a prediction.
40
+ """
41
+
42
+ import copy
43
+ import hashlib
44
+ import json
45
+ import re
46
+ from collections import Counter
47
+
48
+ from . import bench as bench_mod
49
+ from . import core
50
+
51
+ SPLIT_SCHEMA = "codereview.b3-split/1"
52
+ TABLE_SCHEMA = "codereview.b3-calibration/1"
53
+ SPLIT_SALT_DEFAULT = "b3-pilot-split-v1"
54
+ SLICES = ("calibration", "evaluation")
55
+
56
+ _SPLIT_RULE = ("sha256('<salt>:<case_id>') hex digest; first 8 "
57
+ "hex chars as an integer; even -> calibration, "
58
+ "odd -> evaluation")
59
+
60
+
61
+ # ------------------------------------------------------------------ split
62
+
63
+ def slice_for_case(case_id, salt=SPLIT_SALT_DEFAULT):
64
+ """The slice one case id falls in under the split rule.
65
+ A pure function of (salt, case_id): the same inputs always
66
+ produce the same slice, on any machine, in any order."""
67
+ digest = hashlib.sha256(
68
+ f"{salt}:{case_id}".encode("utf-8")).hexdigest()
69
+ return "calibration" if int(digest[:8], 16) % 2 == 0 \
70
+ else "evaluation"
71
+
72
+
73
+ def split_cases(case_ids, salt=SPLIT_SALT_DEFAULT):
74
+ """Partition case ids into {calibration: [...], evaluation:
75
+ [...]} — disjoint, exhaustive, sorted within each slice."""
76
+ out = {name: [] for name in SLICES}
77
+ for cid in sorted(set(case_ids)):
78
+ out[slice_for_case(cid, salt)].append(cid)
79
+ return out
80
+
81
+
82
+ def _split_inputs_sha256(case_ids, salt):
83
+ payload = {"salt": salt,
84
+ "case_ids": sorted(set(case_ids)),
85
+ "rule": _SPLIT_RULE}
86
+ return hashlib.sha256(json.dumps(
87
+ payload, sort_keys=True).encode("utf-8")).hexdigest()
88
+
89
+
90
+ def build_split_manifest(case_ids, salt=SPLIT_SALT_DEFAULT):
91
+ """The committed split manifest: the partition plus a hash
92
+ of the exact inputs that produced it. The manifest is a
93
+ pure function of (salt, case_ids) — no timestamps — so its
94
+ inputs hash is stable and re-verifiable at any later date."""
95
+ parts = split_cases(case_ids, salt)
96
+ return {
97
+ "schema": SPLIT_SCHEMA,
98
+ "salt": salt,
99
+ "rule": _SPLIT_RULE,
100
+ "inputs_sha256": _split_inputs_sha256(case_ids, salt),
101
+ "case_count": sum(len(v) for v in parts.values()),
102
+ "calibration": parts["calibration"],
103
+ "evaluation": parts["evaluation"],
104
+ }
105
+
106
+
107
+ def verify_split_manifest(manifest, case_ids):
108
+ """True iff `manifest` is exactly the manifest these case
109
+ ids produce — same salt, same rule, same partition, same
110
+ inputs hash. Any re-roll (different set, different salt,
111
+ hand-edited slices) fails."""
112
+ if not isinstance(manifest, dict):
113
+ return False
114
+ rebuilt = build_split_manifest(
115
+ case_ids, salt=manifest.get("salt") or "")
116
+ return all(manifest.get(k) == rebuilt.get(k) for k in (
117
+ "schema", "salt", "rule", "inputs_sha256", "case_count",
118
+ "calibration", "evaluation"))
119
+
120
+
121
+ # ------------------------------------------------------- eligibility (§3)
122
+
123
+ #: The executable bases of the design's §3.2 taxonomy (A5's).
124
+ EXECUTABLE_BASES = (
125
+ "crash_or_exception", "hang_or_timeout",
126
+ "unauthorized_access_or_bypass", "bad_input_accepted",
127
+ "injection_or_traversal", "wrong_concrete_value",
128
+ "duplicate_effect", "data_loss_or_destructive",
129
+ "leak_or_exposure", "error_swallowed",
130
+ "unbounded_resource", "secret_or_debug_artifact",
131
+ "money_or_quantity_dropped")
132
+
133
+ #: Classes that are never probe-eligible (§3.2); the fallback
134
+ #: when no executable basis matches is "vague_or_advisory".
135
+ NONEXECUTABLE_CLASSES = (
136
+ "missing_test_or_coverage", "style_maintainability_docs")
137
+
138
+ # Compact rule screen over title + trigger + evidence text,
139
+ # adapted from A5's pattern vocabulary (bench/analysis-2026-11-05)
140
+ # to the §3.2 basis names, in the design's order. A screening
141
+ # heuristic, recorded per finding — not a ground-truth judge.
142
+ _EXEC_PATTERNS = [
143
+ ("crash_or_exception", re.compile(
144
+ r"crash|exception|traceback|segfault|\bpanic\b|"
145
+ r"null ?pointer|division by zero|index out of "
146
+ r"(range|bounds)|typeerror|valueerror|keyerror|"
147
+ r"throws?\b|unhandled (error|exception)", re.I)),
148
+ ("hang_or_timeout", re.compile(
149
+ r"hangs?\b|deadlock|infinite loop|never returns|"
150
+ r"no timeout|missing timeout|without a timeout|"
151
+ r"times? out|waits? forever|blocks? forever", re.I)),
152
+ ("unauthorized_access_or_bypass", re.compile(
153
+ r"unauthenticated|unauthori[sz]ed|without "
154
+ r"(authentication|authori[sz]ation|permission)|"
155
+ r"bypass|no (auth|permission|access) check|missing "
156
+ r"(auth|permission|access) check|permission (is |was )?"
157
+ r"never (raised|enforced)", re.I)),
158
+ ("bad_input_accepted", re.compile(
159
+ r"(accepts?|allows?|permits?|unvalidated|unsanitised|"
160
+ r"unsanitized|no (input )?validation|without (any )?"
161
+ r"(input )?validation)[^.]{0,80}(null|negative|empty|"
162
+ r"arbitrary|malicious|oversized|user input|any "
163
+ r"(value|input))", re.I)),
164
+ ("injection_or_traversal", re.compile(
165
+ r"sql injection|command injection|cross-site scripting|"
166
+ r"\bxss\b|path traversal|directory traversal|\bssrf\b|"
167
+ r"code injection|template injection", re.I)),
168
+ ("wrong_concrete_value", re.compile(
169
+ r"off-by-one|off by one|rounding|precision loss|"
170
+ r"truncat\w+|wrong (total|amount|value|result|count|"
171
+ r"balance)|incorrect (total|amount|value|result|count|"
172
+ r"balance)|returns?[^.]{0,60}(wrong|incorrect|double|"
173
+ r"twice|negative value)|(integer )?overflow|underflow",
174
+ re.I)),
175
+ ("duplicate_effect", re.compile(
176
+ r"double[- ]charg|charged twice|duplicate (charge|"
177
+ r"payment|write|record|entry|order|request)|processed "
178
+ r"twice|runs? twice|not idempotent|no idempotency", re.I)),
179
+ ("data_loss_or_destructive", re.compile(
180
+ r"data loss|loses? (all )?(data|records|rows)|deletes? "
181
+ r"(all|every)|without a where clause|no where clause|"
182
+ r"drops? (the )?(entire )?(table|database)|destroys? "
183
+ r"(all )?(data|records)|unconditional (row )?deletion",
184
+ re.I)),
185
+ ("leak_or_exposure", re.compile(
186
+ r"(leak\w*|expos\w*|disclos\w*|plaintext|plain text|"
187
+ r"in logs?)[^.]{0,70}(password|secret|api ?key|token|"
188
+ r"credential|private key|sensitive)|(password|secret|"
189
+ r"api ?key|credential|private key)[^.]{0,70}(leak\w*|"
190
+ r"expos\w*|disclos\w*|in logs?)", re.I)),
191
+ ("error_swallowed", re.compile(
192
+ r"bare except|swallow\w* (the )?(error|exception|"
193
+ r"failure)|silently (ignored|swallowed|dropped)|empty "
194
+ r"except|catches? all exceptions|errors? (are |is )?"
195
+ r"(silently )?(ignored|swallowed|dropped)", re.I)),
196
+ ("unbounded_resource", re.compile(
197
+ r"unbounded|fetches? all|loads? all|returns? all rows|"
198
+ r"full table scan|without (a )?(limit|pagination)|"
199
+ r"no (limit|pagination|bound)\b", re.I)),
200
+ ("secret_or_debug_artifact", re.compile(
201
+ r"hard ?coded [^.]{0,20}(secret|api ?key|password|"
202
+ r"credential|token|key)|debug (mode|endpoint) "
203
+ r"(enabled|exposed|left on)|embedded (secret|credential|"
204
+ r"key)|debug breakpoint", re.I)),
205
+ ("money_or_quantity_dropped", re.compile(
206
+ r"(dollars?|amount)[^.]{0,50}(cents|dropped|ignored)|"
207
+ r"cents[^.]{0,40}(dropped|ignored|lost)|(drops?|ignores?|"
208
+ r"omits?) (the )?(quantity|qty|multiplier)|under-?charg\w+|"
209
+ r"missing cents conversion", re.I)),
210
+ ]
211
+
212
+ _NONEXEC_PATTERNS = [
213
+ ("missing_test_or_coverage", re.compile(
214
+ r"(missing|no|without|lacks?|absent)[^.]{0,40}(test|"
215
+ r"coverage|assert)|test (is )?(disabled|skipped|missing)|"
216
+ r"disabled test|not (covered|tested) by", re.I)),
217
+ ("style_maintainability_docs", re.compile(
218
+ r"style|naming|readability|maintainab|documentation|"
219
+ r"comment|typo|formatting|\blint\b", re.I)),
220
+ ]
221
+
222
+
223
+ def _finding_text(finding):
224
+ parts = [finding.get("title") or "",
225
+ finding.get("trigger") or "",
226
+ finding.get("evidence") or "",
227
+ finding.get("evidence_quote") or ""]
228
+ return "\n".join(p for p in parts if p)
229
+
230
+
231
+ def classify_eligibility(finding):
232
+ """Design §3.1 legs 1-2 as a deterministic screen.
233
+
234
+ Returns {"eligible", "basis", "claim_class",
235
+ "trigger_concrete", "reason"}: `basis` is the executable
236
+ basis when the claim text matches one (else None);
237
+ `claim_class` is the basis, a non-executable class, or
238
+ "vague_or_advisory". Reachability (§3.1 leg 3) is not
239
+ judged here — see the module docstring."""
240
+ severity = str(finding.get("severity") or "").lower()
241
+ trigger_ok = bool(core._trigger_concrete(
242
+ finding.get("trigger")))
243
+ text = _finding_text(finding)
244
+ basis = None
245
+ for name, pat in _EXEC_PATTERNS:
246
+ if pat.search(text):
247
+ basis = name
248
+ break
249
+ claim_class = basis
250
+ if claim_class is None:
251
+ claim_class = "vague_or_advisory"
252
+ for name, pat in _NONEXEC_PATTERNS:
253
+ if pat.search(text):
254
+ claim_class = name
255
+ break
256
+ if severity not in bench_mod.BLOCKING_SEVERITIES:
257
+ return {"eligible": False, "basis": basis,
258
+ "claim_class": claim_class,
259
+ "trigger_concrete": trigger_ok,
260
+ "reason": "not_blocking_tier"}
261
+ if basis is None:
262
+ return {"eligible": False, "basis": None,
263
+ "claim_class": claim_class,
264
+ "trigger_concrete": trigger_ok,
265
+ "reason": "non_executable_claim"}
266
+ if not trigger_ok:
267
+ return {"eligible": False, "basis": basis,
268
+ "claim_class": claim_class,
269
+ "trigger_concrete": False,
270
+ "reason": "no_concrete_trigger"}
271
+ return {"eligible": True, "basis": basis,
272
+ "claim_class": claim_class,
273
+ "trigger_concrete": True, "reason": "eligible"}
274
+
275
+
276
+ def finding_probe_outcome(finding):
277
+ """The probe outcome recorded on a finding by a run, or
278
+ None when no probe ran for it. Probes today ride the
279
+ adjudication entry (core's Tech Lead loop); the read is
280
+ shape-tolerant so a future gate-time probe record on the
281
+ finding itself is picked up unchanged."""
282
+ rec = (finding.get("adjudication") or {}).get("probe")
283
+ if not isinstance(rec, dict):
284
+ rec = finding.get("probe")
285
+ if not isinstance(rec, dict):
286
+ return None
287
+ return {"outcome": str(rec.get("outcome") or "inconclusive"),
288
+ "detail": str(rec.get("detail") or "")}
289
+
290
+
291
+ # ------------------------------------------------------- calibration table
292
+
293
+ def _base_findings(record):
294
+ """The record's findings with profile demotions restored —
295
+ the pre-profile finding set the profile replays evaluate
296
+ (the registered scorers' convention)."""
297
+ out = []
298
+ for f in record.get("findings") or []:
299
+ g = copy.deepcopy(f)
300
+ dem = g.get("profile_demotion")
301
+ if dem:
302
+ g["severity"] = dem.get("from") or g.get("severity")
303
+ out.append(g)
304
+ return out
305
+
306
+
307
+ def _match_findings(case, findings):
308
+ """{finding index: expected defect} under bench.score_case's
309
+ greedy rule (worst severity first, then confidence; expected
310
+ defects in severity order) — the same matching, exposed per
311
+ finding so the table can mark true-defect findings."""
312
+ order = sorted(
313
+ range(len(findings)),
314
+ key=lambda i: (core._SEV_RANK.get(
315
+ findings[i].get("severity"), len(core.SEVERITIES)),
316
+ -float(findings[i].get("confidence") or 0)))
317
+ used = set()
318
+ matched = {}
319
+ for exp in sorted(case["expected"],
320
+ key=lambda e: core._SEV_RANK[e["severity"]]):
321
+ for i in order:
322
+ if i in used:
323
+ continue
324
+ ok, _cat = bench_mod._finding_matches(exp, findings[i])
325
+ if ok:
326
+ used.add(i)
327
+ matched[i] = exp
328
+ break
329
+ return matched
330
+
331
+
332
+ def _profile_verdicts(case, base, family_by_source, profiles):
333
+ """Replay every current profile over the base findings
334
+ through the one evaluation entry point and record the
335
+ per-profile primitives: whether the case blocks, how many
336
+ blocking findings stand, and how many expected defects a
337
+ blocking finding matches (bench.score_case)."""
338
+ outcomes = {}
339
+ for f in base:
340
+ corr = f.get("corroboration") or {}
341
+ if corr and f.get("id"):
342
+ outcomes[f["id"]] = corr
343
+ verdicts = {}
344
+ for profile in profiles:
345
+ applied, demotions = core.evaluate_gate_policy(
346
+ copy.deepcopy(base), profile,
347
+ family_by_source=family_by_source,
348
+ confirmation_outcomes=outcomes)
349
+ scored = bench_mod.score_case(case, applied)
350
+ blocking = [f for f in applied
351
+ if (f.get("severity") or "").lower()
352
+ in bench_mod.BLOCKING_SEVERITIES]
353
+ verdicts[profile] = {
354
+ "blocked": bool(blocking),
355
+ "blocking_findings": len(blocking),
356
+ "defects_blocked": sum(
357
+ 1 for d in scored["defects"]
358
+ if d["matched"] and not d["escaped"]),
359
+ "defects_matched": scored["matched"],
360
+ "demotions": len(demotions),
361
+ }
362
+ return verdicts
363
+
364
+
365
+ def _ratio(num, den):
366
+ return (num / den) if den else None
367
+
368
+
369
+ def _slice_factors(rows, profiles):
370
+ """The §8 prediction factors as RAW INPUTS (counts +
371
+ ratios) over one slice's case rows. Not a prediction: the
372
+ formula and its bounds belong to the pilot registration."""
373
+ defect = [r for r in rows if r["expected_total"] > 0]
374
+ clean = [r for r in rows if r["expected_total"] == 0]
375
+ base_blocking = [r for r in defect
376
+ if r["base_blocking_without_probes"]]
377
+ remaining = [r for r in defect
378
+ if not r["base_blocking_without_probes"]]
379
+ elig_cases = [r for r in remaining
380
+ if r["has_eligible_e3_blocking"]]
381
+ elig_true = [fr for r in defect for fr in r["findings"]
382
+ if fr["eligibility"]["eligible"]
383
+ and fr["matches_expected_defect"]]
384
+ probed = [fr for fr in elig_true
385
+ if fr["probe_outcome"] is not None]
386
+ blocking_rows = [fr for r in rows for fr in r["findings"]
387
+ if fr["blocking_tier"]]
388
+ return {
389
+ "defect_cases": len(defect),
390
+ "clean_cases": len(clean),
391
+ # p_base: share of defect cases already blocking
392
+ # without probes (a matched defect carried by an
393
+ # E0/E1 blocking finding in the base set).
394
+ "p_base_inputs": {
395
+ "defect_cases": len(defect),
396
+ "blocking_without_probes": len(base_blocking),
397
+ "ratio": _ratio(len(base_blocking), len(defect))},
398
+ # p_elig: share of the REMAINING defect cases with at
399
+ # least one eligible E3 blocking finding.
400
+ "p_elig_inputs": {
401
+ "remaining_defect_cases": len(remaining),
402
+ "with_eligible_e3_blocking_finding": len(elig_cases),
403
+ "ratio": _ratio(len(elig_cases), len(remaining))},
404
+ # p_settle: share of eligible true-defect findings
405
+ # whose probes settle. No settlement vocabulary exists
406
+ # before the mechanism is approved, so the inputs are
407
+ # the probe-outcome counts themselves.
408
+ "p_settle_inputs": {
409
+ "eligible_true_defect_findings": len(elig_true),
410
+ "with_probe_outcome": len(probed),
411
+ "outcome_histogram": dict(Counter(
412
+ fr["probe_outcome"]["outcome"] for fr in probed)),
413
+ "note": "raw probe outcomes under the existing "
414
+ "falsification channel; a settlement rate "
415
+ "exists only once a promotion mechanism is "
416
+ "approved and run"},
417
+ "eligibility": {
418
+ "blocking_findings": len(blocking_rows),
419
+ "eligible": sum(1 for fr in blocking_rows
420
+ if fr["eligibility"]["eligible"]),
421
+ "by_basis": dict(Counter(
422
+ fr["eligibility"]["basis"] for fr in blocking_rows
423
+ if fr["eligibility"]["basis"])),
424
+ "ineligible_reasons": dict(Counter(
425
+ fr["eligibility"]["reason"] for fr in blocking_rows
426
+ if not fr["eligibility"]["eligible"])),
427
+ },
428
+ "clean_blocked_by_profile": {
429
+ p: {"cases": len(clean),
430
+ "blocked": sum(1 for r in clean
431
+ if r["profiles"][p]["blocked"]),
432
+ "ratio": _ratio(
433
+ sum(1 for r in clean
434
+ if r["profiles"][p]["blocked"]),
435
+ len(clean))}
436
+ for p in profiles},
437
+ "defect_blocked_by_profile": {
438
+ p: {"cases": len(defect),
439
+ "cases_blocked": sum(
440
+ 1 for r in defect if r["profiles"][p]["blocked"]),
441
+ "defects_blocked": sum(
442
+ r["profiles"][p]["defects_blocked"]
443
+ for r in defect),
444
+ "defects_total": sum(r["expected_total"]
445
+ for r in defect)}
446
+ for p in profiles},
447
+ }
448
+
449
+
450
+ def build_calibration_table(records, cases, family_by_source,
451
+ split_manifest=None,
452
+ profiles=core.GATE_PROFILES):
453
+ """Assemble the calibration table from live-run records.
454
+
455
+ `records`: bench-shaped result records (bench.run_bench /
456
+ pilot runner output). `cases`: bench.load_case-shaped case
457
+ dicts (id + expected at minimum). `family_by_source`: seat
458
+ name -> vendor family for the profile replays (the config's
459
+ seat_meta families, plus "deterministic"). When
460
+ `split_manifest` is given it must be the manifest these
461
+ cases produce (verified) and it assigns the slices; without
462
+ one, the split is computed from the case ids directly."""
463
+ cases_by_id = {c["id"]: c for c in cases}
464
+ if split_manifest is not None:
465
+ if not verify_split_manifest(split_manifest,
466
+ sorted(cases_by_id)):
467
+ raise ValueError(
468
+ "split manifest does not match the case set "
469
+ "(inputs hash / partition drift) — refusing to "
470
+ "build a table on a re-rolled split")
471
+ slice_of = {}
472
+ for name in SLICES:
473
+ for cid in split_manifest[name]:
474
+ slice_of[cid] = name
475
+ manifest_ref = split_manifest["inputs_sha256"]
476
+ else:
477
+ parts = split_cases(sorted(cases_by_id))
478
+ slice_of = {cid: name for name in SLICES
479
+ for cid in parts[name]}
480
+ manifest_ref = _split_inputs_sha256(
481
+ sorted(cases_by_id), SPLIT_SALT_DEFAULT)
482
+ rows = []
483
+ for rec in records:
484
+ cid = rec.get("case_id")
485
+ case = cases_by_id.get(cid)
486
+ if case is None:
487
+ raise ValueError(
488
+ f"calibration record names unknown case {cid!r}")
489
+ base = _base_findings(rec)
490
+ matched = _match_findings(case, base)
491
+ finding_rows = []
492
+ for i, f in enumerate(base):
493
+ exp = matched.get(i)
494
+ finding_rows.append({
495
+ "finding_id": f.get("id"),
496
+ "severity": f.get("severity"),
497
+ "category": f.get("category"),
498
+ "file": f.get("file"),
499
+ "line": f.get("line"),
500
+ "title": f.get("title"),
501
+ "sources": list(f.get("sources") or []),
502
+ "evidence_class": f.get("evidence_class"),
503
+ "blocking_tier": (f.get("severity") or "").lower()
504
+ in bench_mod.BLOCKING_SEVERITIES,
505
+ "eligibility": classify_eligibility(f),
506
+ "probe_outcome": finding_probe_outcome(f),
507
+ "matches_expected_defect": exp is not None,
508
+ "expected_defect": (
509
+ {"severity": exp["severity"],
510
+ "file": exp["file"], "line": exp.get("line"),
511
+ "title": exp["title"]} if exp else None),
512
+ })
513
+ rows.append({
514
+ "case_id": cid,
515
+ "slice": slice_of[cid],
516
+ "stratum": "defect" if case["expected"] else "clean",
517
+ "expected_total": len(case["expected"]),
518
+ "live_gate": rec.get("gate"),
519
+ "live_profile": (rec.get("gate_profile") or {}).get(
520
+ "profile"),
521
+ "degraded": bool(rec.get("degraded")),
522
+ "base_blocking_without_probes": any(
523
+ fr["matches_expected_defect"] and fr["blocking_tier"]
524
+ and fr["evidence_class"] in ("E0", "E1")
525
+ for fr in finding_rows),
526
+ "has_eligible_e3_blocking": any(
527
+ fr["eligibility"]["eligible"] and fr["blocking_tier"]
528
+ and fr["evidence_class"] == "E3"
529
+ for fr in finding_rows),
530
+ "findings": finding_rows,
531
+ "profiles": _profile_verdicts(
532
+ case, base, family_by_source, profiles),
533
+ })
534
+ rows.sort(key=lambda r: r["case_id"])
535
+ by_slice = {name: [r for r in rows if r["slice"] == name]
536
+ for name in SLICES}
537
+ return {
538
+ "schema": TABLE_SCHEMA,
539
+ "split_manifest_inputs_sha256": manifest_ref,
540
+ "profiles": list(profiles),
541
+ "cases": rows,
542
+ "factors": {
543
+ "calibration": _slice_factors(
544
+ by_slice["calibration"], profiles),
545
+ "evaluation": _slice_factors(
546
+ by_slice["evaluation"], profiles),
547
+ "all": _slice_factors(rows, profiles),
548
+ },
549
+ }