Skip to content

P2-13: Stage-1 temperature scaling + the non-gated reliability read (T = 0.9896) - #90

Merged
bioedca merged 4 commits into
mainfrom
p2-13-calibration
Jul 30, 2026
Merged

bioedca merged 4 commits into
mainfrom
p2-13-calibration

Conversation

@bioedca

@bioedca bioedca commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Fits the Stage-1 calibration machinery (imp.md P2-13) and reports the non-gated in-distribution reliability read. Nothing here is a calibration claim: GATE-2's ECE ≤ 0.05 is graded at P3 exit on the P3-02 calib carve (PRD.md:56/:248, ADR-0005 D11), and no T fitted here is shipped.

The §7 stop this step opened, and how it closed

Four things P2-13 needs are unpinned — verified absent from PRD.md and all six ADRs: the calibration axis for a per-nucleotide 8-class segmenter (D11 pins its estimator "on the positive-class posterior" — wording written for Stage-2's binary head), single-T vs per-class T, the optimiser, and which fold (D11's "disjoint calibration split" occurs exactly once in the PRD, and the calib column it names is carved at P3-02). Signed as ADR-0005 A11 (2026-07-30):

Pin Decision
1 — axis per-nt 8-class logits; read reduces one-vs-rest per class through the frozen metrics.binned_ece; worst-over-core, no cross-class mean
2 — form one shared scalar T (the method of arXiv:1706.04599, the paper D11 already cites)
3 — fit exact convex 1-D solve in β = 1/T — no seed, no optimiser, no torch (so the unit gate runs in CI, which installs none)
4 — placement before the frozen D3+A3 operator (deployment order); the arg-max consequence is measured, not assumed
5 — fold seeded whole-cluster halving of selection_val: fit on half A, read on half B, disclosed as not D11's split

Result

T = 0.989587 (±5.8e-5, re-derivable from the recorded bracket + iteration count), fitted on 1,778,688 per-window positions; per-position NLL 0.017839 → 0.017836. Per-class one-vs-rest ECE on the read half (1,018,693 nt / 176 clusters): background 0.0071, Stem I 0.0019, Stem II 0.0010, Stem III 0.0013, Terminator 0.0014, Antiterminator 0.0008, Specifier 0.0001, Discriminator 0.0002 — every value moves by ≤ 1.7e-5 under T, in both directions.

Two findings beyond the number:

  1. A single shared T is structurally the wrong instrument for what is actually miscalibrated. The top bin of every rare class is over-confident (Stem I predicts 0.599 against an observed 0.570), while background at 91.2 % prevalence is under-confident — so the pooled NLL, being 91 % background, lands at T slightly below 1, sharpening, the opposite of what the rare classes want. A measured argument for testing vector scaling at P3, which is exactly what A11 declined to pin without evidence.
  2. P2-12's flag is answered "no" — and not by invariance. A single T preserves each window's arg-max, but the D3+A3 operator averages probabilities across overlapping windows, so it can move the reconciled arg-max wherever coverage ≥ 2 — 664,399 of 1,018,693 read positions. The mechanism is live; its magnitude at T = 0.9896 is 2 positions (0.0002 %). GATE-4 stays graded on the uncalibrated posterior.

Defects fixed on the way

Validation

  • unit 66/66, torch-free; eval-gate tier 13/13
  • the binned_ecereliability_bins promotion is bit-exact: 130 old-vs-new comparisons, 0 differences, and the committed tests/fixtures/eval_gate_sample/expected.json values reproduce to the last bit
  • four source-sabotage bites, each with the mutation's byte change shown and restored byte-identically: equal-width binning → RED; either degeneracy guard deleted → RED (separately); fit-half re-derivation deleted → the inversion test RED
  • the artifact's own 7-clause gate re-derives from the report — including re-cutting the halves from the recorded cluster→size map, because two ~equal halves make a swapped fit/read wiring invisible to every count
  • ruff@0.15.15 + black@25.11.0 clean at CI-exact versions
  • LOCAL compute, measured: ≈50 min wall on the laptop 4060, 0.30 GiB peak VRAM (the 200-replicate cluster-blocked bootstrap over the pure-Python estimator, not the GPU, is the cost). No SLURM.

https://claude.ai/code/session_01Tr67opmzHJAGBJPAb2aTdX

Summary by CodeRabbit

  • New Features

    • Added post-hoc temperature calibration for model reliability estimates.
    • Added calibration reports, reliability diagrams, confidence intervals, and validation results.
    • Added per-class reliability analysis and shared reliability-bin calculations.
    • Added command-line support for running calibration and generating figures.
  • Tests

    • Added comprehensive coverage for calibration fitting, reliability metrics, report validation, data splitting, and plotting workflows.

…iability read (P2-13)

The P2-13 machinery, plus the measured result that there is almost nothing to
calibrate: T = 0.989587 on the production checkpoint (job 671 / P2-09 082f8c7),
fitted on 1,778,688 per-window positions of the selection_val fit half, moving
the per-position NLL 0.017839 -> 0.017836.

Four things this step needs were unpinned - verified absent from PRD.md and all
six ADRs: the calibration axis for a per-nt 8-class segmenter (D11 pins its
estimator "on the positive-class posterior", written for Stage-2's binary head),
single-T vs per-class T, the optimiser, and which fold (D11's "disjoint
calibration split" occurs once in the PRD and its `calib` column is carved at
P3-02, not here). ADR-0005 A11, signed 2026-07-30, pins all four.

Inputs:  data/processed/checkpoints/stage1_production/stage1.pt (DVC)
         P2-06a selection_val rung (830 records / 469 clusters)
         metrics.binned_ece (frozen: 15 equal-mass debiased bins, D11)
Outputs: src/tbox_finder/calib/{__init__,temperature}.py
         metrics.reliability_bins (promoted out of binned_ece)
         reports/p2/calibration{,_figure_data}.json
         figures/calib/reliability_selection_val.png (git-LFS)
         tests/unit/test_temperature.py (66 checks, torch-free)
         docs/decisions/ADR-0005 A11 + the phase-2 dev-log stanza

NON-GATED. GATE-2's ECE <= 0.05 is graded at P3 exit on the P3-02 carve; these
numbers are on the fold the P2-06 sweep selected on, and no T here is shipped.
T is fitted on one seeded whole-cluster half and read on the other, so the read
is out-of-sample for T; the halves are validated by IDENTITY (the validator
re-cuts the fit half from the recorded cluster->size map), because two ~equal
halves make a swapped fit/read wiring invisible to every count in the artifact.

Two findings beyond the headline:
 - a single shared T is the wrong instrument for what is miscalibrated here. The
   top bin of every rare class is over-confident (Stem I 0.599 predicted vs 0.570
   observed), while background at 91.2% prevalence is under-confident - so the
   pooled NLL lands at T slightly BELOW 1, sharpening, the opposite of what the
   rare classes want. A measured argument for testing vector scaling at P3.
 - P2-12's flag is answered "no", and not by invariance: a single T preserves each
   WINDOW's arg-max, but the D3+A3 operator averages probabilities across
   overlapping windows, so it can move the RECONCILED arg-max wherever coverage
   >= 2 - which is 664,399 of 1,018,693 read positions. The mechanism is live; its
   magnitude at T=0.9896 is 2 positions. GATE-4 stays on the uncalibrated posterior.

Two defects fixed on the way: the fit's |grad| <= tol stopping rule read an
UNDERFLOWED gradient tail as stationarity and would have certified an arbitrary T
on a perfectly-accurate fold (replaced by the exact beta->inf limit, which cannot
underflow); and the first perfect-accuracy test matched either degeneracy message,
so deleting the exact guard left the suite green - both guards now match their own
message and were re-sabotaged separately.

Validation: unit 66/66 (torch-free, runs in CI's no-torch env); eval-gate tier
13/13; the binned_ece refactor is bit-exact (130 old-vs-new comparisons, 0
differences; the committed expected.json ECE values reproduce to the last bit);
four source-sabotage bites, each with its byte change shown and restored
byte-identically; ruff@0.15.15 + black@25.11.0 clean; the artifact's own 7-clause
gate re-derives from the report and passes.

Claude-Session: https://claude.ai/code/session_01Tr67opmzHJAGBJPAb2aTdX
Copilot AI review requested due to automatic review settings July 30, 2026 15:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: feadb3e2-2fcd-45d5-a727-f1491554e0b7

📥 Commits

Reviewing files that changed from the base of the PR and between f3cd62d and ad711a2.

⛔ Files ignored due to path filters (1)
  • analyses/phase2_log.qmd is excluded by !**/*.qmd
📒 Files selected for processing (2)
  • src/tbox_finder/calib/temperature.py
  • tests/unit/test_temperature.py
📝 Walkthrough

Walkthrough

Adds Stage-1 temperature-scaling calibration with deterministic cluster splitting, certified fitting, reliability metrics, report validation, CLI plotting, tests, and P2-13 JSON artifacts.

Changes

Temperature calibration

Layer / File(s) Summary
Calibration contracts and reliability estimator
src/tbox_finder/calib/__init__.py, src/tbox_finder/calib/temperature.py, src/tbox_finder/metrics.py
Adds the calibration package, TemperatureFit contract, shared reliability-bin computation, and ECE delegation.
Certified fitting and cluster split
src/tbox_finder/calib/temperature.py, tests/unit/test_temperature.py
Implements bisection-based temperature fitting, temperature application, deterministic cluster partitioning, per-class reliability, blocked ECE intervals, and corresponding validation tests.
Calibration pipeline and validation
src/tbox_finder/calib/temperature.py, tests/unit/test_temperature.py
Adds model forwarding, reconciliation agreement checks, calibration execution, report validation, gate derivation, figure extraction, plotting, and CLI wiring with tests.
Calibration report artifacts
reports/p2/calibration.json, reports/p2/calibration_figure_data.json
Adds P2-13 calibrated and uncalibrated metrics, provenance, temperature-fit results, and reliability-curve data.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: Stage-1 temperature scaling and the non-gated reliability read for P2-13.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch p2-13-calibration

Comment @coderabbitai help to get the list of available commands.

…-reproducibility disclosure

Four CLI findings; three were real defects and one was a public-artifact path leak.
The GitHub CodeRabbit check reported only "Review rate limited" again, so the CLI is
what satisfied the CLAUDE.md 5.1 review gate.

(1) The fit could emit a report its own validator rejects. Bisection accepted
    |grad| <= grad_tol OR a collapsed bracket as convergence; on the width path it
    returned converged=True carrying a gradient larger than grad_tol - exactly the
    clause validate_report checks (the P2-12 build_report-vs-validate_report shape,
    one layer in). Only the stationarity residual certifies a fit now; a bracket that
    collapses first raises. The committed artifact is unaffected: it converged on the
    gradient (|grad| = 1.53 <= 1.78 at iteration 24, re-derivable from the recorded
    bracket + n_iterations).

(2) validate_report could raise instead of report. Its contract is "total - returns
    problems, never raises", but it did float(r.get("weight", 0.0)), r["debiased_gap"]
    and per_class[e]["ece"] over unvalidated rows. A validator that crashes reports
    nothing, which is strictly worse than one that reports a problem.

(3) A NaN or out-of-range posterior would have produced a plausible-looking ECE (the
    estimator sorts by posterior, so a NaN sorts arbitrarily and returns a number).
    The check lives in per_class_reliability, NOT in metrics.binned_ece: that kernel is
    frozen and golden-locked, and a per-call scan of its Sequence inputs would run once
    per class per bootstrap replicate (3,200 times here). One vectorised numpy check per
    read covers every value this module hands it.

(4) The artifact recorded an absolute /home/<user>/... checkpoint path, unresolvable for
    any reviewer of a public repo. Now relativised against the running checkout AND its
    main checkout - this step runs from a .claude/worktrees/ worktree while the
    DVC-tracked inputs are materialised only in the main checkout, so relativising
    against the worktree alone would fall back to absolute for every real input.

The artifact is regenerated under the fixed code, and the re-runs measured something the
first run could not show: T = 0.9895867184568677 reproduces BIT-FOR-BIT across three
runs, as do every count (arg-max changes 2, coverage>=2 at 664,399, the cluster halves)
and gate4_core_min_f1 - but the artifact is NOT byte-identical. Per-class ECEs agree to
<= 3e-8 relative, the largest read disagreement is 4.5e-6 on a bin edge, and
grad_at_solution (a cancelling sum over ~1.8e6 terms) differs by 2.7e-4. The cause is the
GPU forward, not this module: the Mamba/CUDA kernels reduce in a run-dependent order. That
now ships as a ninth report disclosure carrying the measured bounds, so a reader re-derives
rather than diffing for equality.

Validation: unit 74/74 (8 new); four further source-sabotage bites, each with its byte
change shown and restored byte-identically (width-only convergence restored -> RED;
posterior-range check dropped -> RED; row shape-check dropped -> 3 RED; path
relativisation dropped -> RED); ruff@0.15.15 + black@25.11.0 clean; the artifact's own
7-clause gate re-derives and passes.

Claude-Session: https://claude.ai/code/session_01Tr67opmzHJAGBJPAb2aTdX

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/unit/test_temperature.py (1)

521-525: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This substitution hides the gap it looks like it covers.

derive_clauses(junk if isinstance(junk, dict) else {}) never passes the junk to derive_clauses, which is exactly where the totality claim breaks (unguarded report.get in derive_clauses, see the comment on src/tbox_finder/calib/temperature.py lines 1018–1026). Once that is fixed, drop the substitution so the test bites.

💚 Proposed tightening
     for junk in (None, [], "report", 3):
         problems = T.validate_report(junk)  # type: ignore[arg-type]
         assert problems and isinstance(problems, list)
-        assert T.derive_clauses(junk if isinstance(junk, dict) else {})["overall_pass"] is False
+        assert T.derive_clauses(junk)["overall_pass"] is False  # type: ignore[arg-type]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_temperature.py` around lines 521 - 525, Update
test_validator_is_total_on_junk to pass each junk value directly to
T.derive_clauses instead of substituting {} for non-dictionaries, preserving the
assertion that it returns overall_pass=False without raising.
src/tbox_finder/calib/temperature.py (1)

689-694: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the per-cluster position grouping out of the class loop.

blocks is rebuilt by walking all read positions once per class (8 × N Python appends). The cluster → position-index grouping is class-independent, so compute it once and index the column inside the loop.

♻️ Suggested restructure
+        pos_by_cluster: dict[int, list[int]] = {}
+        for pos, cid in enumerate(cluster_of_position):
+            pos_by_cluster.setdefault(cid, []).append(pos)
         for i, name in enumerate(CLASS_ORDER):
-            blocks: dict[int, list[tuple[int, float]]] = {}
             col = probs[:, i].tolist()
-            for pos, cid in enumerate(cluster_of_position):
-                blocks.setdefault(cid, []).append((1 if y_all[pos] == i else 0, col[pos]))
+            blocks = {
+                cid: [(1 if y_all[pos] == i else 0, col[pos]) for pos in idxs]
+                for cid, idxs in pos_by_cluster.items()
+            }
             per_class[name]["ece_ci"] = ece_block_ci(blocks, n_boot=n_boot, seed=seed)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tbox_finder/calib/temperature.py` around lines 689 - 694, Move the
cluster-to-position grouping work out of the CLASS_ORDER loop: build a reusable
mapping of cluster IDs to position indices by iterating over cluster_of_position
once, then in the per-class loop construct each class’s blocks from those
indices using y_all and the current probability column. Keep the existing
ece_block_ci call and n_boot/seed behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/tbox_finder/calib/temperature.py`:
- Around line 1176-1184: Update the figure-data write flow alongside the invalid
report handling: when problems is non-empty, divert the payload from
args.figure_data to its corresponding invalid artifact path or skip writing it,
preserving the existing canonical figure-data file. Keep valid runs writing to
args.figure_data as before, using the existing out invalidation logic in this
report-generation block.
- Around line 1018-1026: Update derive_clauses so operator_agrees_bit_exactly
and prior_shift_refused guard report with isinstance(report, Mapping) before
calling report.get, matching the existing guards and ensuring non-mapping inputs
such as None or lists return false clauses instead of raising AttributeError.

---

Nitpick comments:
In `@src/tbox_finder/calib/temperature.py`:
- Around line 689-694: Move the cluster-to-position grouping work out of the
CLASS_ORDER loop: build a reusable mapping of cluster IDs to position indices by
iterating over cluster_of_position once, then in the per-class loop construct
each class’s blocks from those indices using y_all and the current probability
column. Keep the existing ece_block_ci call and n_boot/seed behavior unchanged.

In `@tests/unit/test_temperature.py`:
- Around line 521-525: Update test_validator_is_total_on_junk to pass each junk
value directly to T.derive_clauses instead of substituting {} for
non-dictionaries, preserving the assertion that it returns overall_pass=False
without raising.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c291ee5-42c5-4351-9335-9dee1f5e963b

📥 Commits

Reviewing files that changed from the base of the PR and between 74330dd and f3cd62d.

⛔ Files ignored due to path filters (3)
  • analyses/phase2_log.qmd is excluded by !**/*.qmd
  • docs/decisions/ADR-0005-non-circular-eval-design.md is excluded by !**/*.md, !docs/decisions/**
  • figures/calib/reliability_selection_val.png is excluded by !**/*.png, !figures/**
📒 Files selected for processing (6)
  • reports/p2/calibration.json
  • reports/p2/calibration_figure_data.json
  • src/tbox_finder/calib/__init__.py
  • src/tbox_finder/calib/temperature.py
  • src/tbox_finder/metrics.py
  • tests/unit/test_temperature.py

Comment on lines +1018 to +1026
"operator_agrees_bit_exactly": bool(
isinstance(report.get("operator_agreement"), Mapping)
and report["operator_agreement"].get("bit_exact") is True
),
"not_gated": report.get("gated") is False if isinstance(report, Mapping) else False,
"prior_shift_refused": bool(
isinstance(report.get("stack_order"), Mapping)
and report["stack_order"].get("prior_shift_applied") is False
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

derive_clauses raises AttributeError on a non-mapping report.

Lines 1012–1013 and 1022 guard report with isinstance(..., Mapping), but the operator_agrees_bit_exactly and prior_shift_refused clauses call report.get(...) unguarded. derive_clauses(None) / derive_clauses([]) therefore crashes, while validate_report on the same input correctly returns problems — the "validator that dies reports nothing" failure this module explicitly designs against, one function over. Note that tests/unit/test_temperature.py:525 sidesteps it by substituting {} for non-dict junk, so the gap is not covered.

🛡️ Proposed fix
+    mapping = report if isinstance(report, Mapping) else {}
     clauses = {
         "report_valid": not problems,
         "temperature_certified": bool(isinstance(temp, Mapping) and temp.get("converged") is True),
         "read_out_of_sample_for_T": read_is_out_of_sample(report),
         "operator_agrees_bit_exactly": bool(
-            isinstance(report.get("operator_agreement"), Mapping)
-            and report["operator_agreement"].get("bit_exact") is True
+            isinstance(mapping.get("operator_agreement"), Mapping)
+            and mapping["operator_agreement"].get("bit_exact") is True
         ),
-        "not_gated": report.get("gated") is False if isinstance(report, Mapping) else False,
+        "not_gated": mapping.get("gated") is False,
         "prior_shift_refused": bool(
-            isinstance(report.get("stack_order"), Mapping)
-            and report["stack_order"].get("prior_shift_applied") is False
+            isinstance(mapping.get("stack_order"), Mapping)
+            and mapping["stack_order"].get("prior_shift_applied") is False
         ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"operator_agrees_bit_exactly": bool(
isinstance(report.get("operator_agreement"), Mapping)
and report["operator_agreement"].get("bit_exact") is True
),
"not_gated": report.get("gated") is False if isinstance(report, Mapping) else False,
"prior_shift_refused": bool(
isinstance(report.get("stack_order"), Mapping)
and report["stack_order"].get("prior_shift_applied") is False
),
mapping = report if isinstance(report, Mapping) else {}
clauses = {
"operator_agrees_bit_exactly": bool(
isinstance(mapping.get("operator_agreement"), Mapping)
and mapping["operator_agreement"].get("bit_exact") is True
),
"not_gated": mapping.get("gated") is False,
"prior_shift_refused": bool(
isinstance(mapping.get("stack_order"), Mapping)
and mapping["stack_order"].get("prior_shift_applied") is False
),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tbox_finder/calib/temperature.py` around lines 1018 - 1026, Update
derive_clauses so operator_agrees_bit_exactly and prior_shift_refused guard
report with isinstance(report, Mapping) before calling report.get, matching the
existing guards and ensuring non-mapping inputs such as None or lists return
false clauses instead of raising AttributeError.

Comment thread src/tbox_finder/calib/temperature.py Outdated
bioedcam added 2 commits July 30, 2026 12:28
…nd de-vacuum the worktree path test

(1) An invalid report was diverted to calibration.invalid.json but its figure data kept
    the canonical name - and the reliability diagram is rendered FROM that file, so a
    refused run would still leave a plottable curve under the good name beside a report
    marked invalid. Both artifacts now divert together (_output_paths), and the
    figure-data build is guarded: diverting means execution CONTINUES past the write for
    reports that previously never got there, so an unguarded figure_data(report) would
    trade the validator's diagnostic for a KeyError traceback (the P2-12 round-4 shape,
    reproduced by my own round-1 fix). The summary's numeric format specs get the same
    treatment via _render -> "n/a".

(2) The worktree-aware path assertion was gated on "am I running inside a worktree" -
    true on this laptop, FALSE in CI, which checks the repo out normally. It would have
    certified the branch only where nobody looks. _portable_path now takes an injectable
    checkout and the rule is asserted unconditionally on a synthetic tree, including the
    two directions that must NOT fire (a sibling checkout stays absolute; a plain
    checkout must not climb to its parent).

The committed artifact is untouched: the only behavioural changes are on the invalid path
and in stdout formatting, and _output_paths(..., valid=True) returns exactly the paths the
valid run already used.

Validation: unit 78/78 (4 new); three further source-sabotage bites, each byte-verified
and restored (figure data left at the canonical name -> RED; bare numeric format spec
restored -> RED; worktree->main-checkout root dropped -> RED); a post-refactor CLI smoke
(16 records, --n-boot 5) writes both artifacts and passes its gate; ruff/black clean.

Claude-Session: https://claude.ai/code/session_01Tr67opmzHJAGBJPAb2aTdX
… but never checked

read.n_positions_coverage_ge_2 is reported, printed by the CLI summary, and load-bearing
for the A11 Pin 4 reading (the coverage>=2 mass is what makes the reconciled arg-max
movable at all), yet it was the only counter validate_report did not check. Now validated
like the rest: non-int, boolean, or negative is a reported problem.

The committed artifact is unaffected and this was CHECKED, not assumed: re-validating
reports/p2/calibration.json under the new clause set returns zero problems and re-derives
overall_pass=True (recorded value 664,399). A new gate clause can silently invalidate
every already-committed report, so adding one means re-running the validator over the
artifacts in the tree.

Validation: unit 81/81 (3 new - missing, negative, and a boolean sneaking through
isinstance(x, int)); one byte-verified sabotage bite (clause dropped -> both new cases
RED, restored byte-identically); ruff/black clean.

Claude-Session: https://claude.ai/code/session_01Tr67opmzHJAGBJPAb2aTdX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants