Skip to content

P3-15'-f: re-derive criterion (b)'s seven rule parameters from the matched control - #126

Merged
bioedca merged 14 commits into
mainfrom
p3-15f-param-choice
Aug 12, 2026
Merged

bioedca merged 14 commits into
mainfrom
p3-15f-param-choice

Conversation

@bioedca

@bioedca bioedca commented Aug 12, 2026

Copy link
Copy Markdown
Owner

P3-15′-f's block asked for its seven-parameter recommendation to be re-derived, not re-quoted, once the matched positive control landed. -g-iv landed it. This is the re-derivation, over the whole admissible grid rather than the six named tuples the two measurement reports carry.

The headline is not about (b): the round's yield ceiling is 15 of 941

Sparing is a disjunction (ADR-0005 D14), so mining is a conjunction. Criterion (a) fails on 111 of 941, criterion (c) on 96; their intersection is 15, and all 15 have a consensus so (b) is decidable on every one. Declaring the Stage-2 disjunct lowers it to 12 (thr 0.5) or 13 (0.9/0.95/0.99). Over the entire admissible grid the round mines between 1 and 13.

Criterion (b)'s seven parameters are therefore choosing among single-digit differences in a 941-candidate re-mining round. No committed report stated this, and it changes what the §7 question is about.

What the rule selects, and why it is a rule

Four floors are read off ADR-0006 D3's own wording and ADR-0004 D1's element vocabulary before any count is readmin_named_helices ≥ 2, min_helix_pairs ≥ 2, ncca_pairing_nt ≥ 2, a bounded bulge_max_nt. Among the 864 survivors of 2,880 admissible points the rule takes the most permissive as measured, which is ADR-0006 A4's own fail-closed argument for a sparing disjunct. That selects exactly sensitive_core (1 · 2 · 2 · 2 · 50 · 2 · 0), tied only with its proved-inert allow_wobble twin.

Admissibility is measured by running the shipped localizer and catching its refusal, never by re-encoding its guard; each point's yield goes through the shipped spare_rule.is_mining_excluded, not a re-derived conjunction.

The composite (a) ∧ (b) is anti-selective at the strict end

-g-iv compared (b)'s own failure share and found no separation. The object that decides what is mined is the conjunction, and there the control falls through more often than the FP arm at five of six named settings — 9.2 vs 7.2 %, 10.5 vs 11.5 %, 38.2 vs 23.7 %, 43.4 vs 32.0 %, 57.9 vs 37.4 %, 59.2 vs 38.5 % — with disjoint 95 % Wilson intervals at the two strictest. Criterion (a) passes 39.5 % of the control against 60.1 % of the FP arm, so tightening (b) removes protection from a population (a) was already protecting less.

Disclosures carried in the artifact

  • The report enumerates the 8 floor-clearing settings that beat its own recommendation (at most 2 extra mined at identical measured damage) and states why the rule declines them.
  • Criterion (c) is 546 / 96 / 299, not the 541 / 101 / 299 TODO.md and imp.md record; the committed synteny_exclusion_diagnostic.json says so.
  • No committed artifact anchors the Stage-2 posteriors' digest, and a second scoring of the same 941 exists differing by up to 3.8e-2 (a CUDA forward is not bit-reproducible; the two agree at 0.5/0.9/0.95 and disagree on 3 ids at 0.99). That column is a sensitivity only — the ceiling of 15 uses (a) and (c) alone, both digest-anchored to committed reports.
  • A defect downstream, verified not inferred: slurm/p3/stage1_remine.sbatch builds --relaxed-arch-available / --synteny-available into $PLAN_FLAGS but never passes --relaxed-arch-status or --synteny-status, and remine.py:512 refuses exactly that pair. Reported, not patched — off this step's roadmap.

Validation

Zero new failures vs a matched main baseline at 090547f (worktree 13 FAILED, main 26, a strict subset; the delta is the documented DVC/LFS skips, each named). 40 sabotages all RED against their NAMED test + 2 green negative controls; the harness rewrites the original bytes (the module is untracked, so git checkout -- would be a silent no-op), clears __pycache__, and re-reads git status --porcelain around every mutation. Two defects of this step were caught by that harness rather than by review: a duplicated floor filter that let clears_all_floors be neutered with no effect on the selection, and a measured_identically clause that was vacuously TRUE whenever the selection had not moved. Both reports re-derive byte-identically apart from generated_at_utc. ruff + black clean.

pins_nothing: true; no ADR amended — A4 already delegates the value to the round. The seven-parameter §7 choice is the project lead's and this PR does not take it.

https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB

Summary by CodeRabbit

  • New Features
    • Added architecture-parameter analysis across admissible configurations.
    • Added validated input-building and recommendation-report commands with provenance.
    • Added yield, control-impact, sensitivity, frontier, and alternative-comparison results.
    • Added deterministic decision criteria and tie handling without pinning parameters.
  • Documentation
    • Added a comprehensive recommendation report with limitations and threshold caveats.
  • Tests
    • Added extensive coverage for validation, controls, evidence, recommendation logic, and end-to-end reporting.

…ers from the matched control

P3-15'-f measured criterion (b) on 278 de-novo consensuses of round-0
false-positive-manifest candidates and put seven ADR-0006 A4 rule parameters to the
user, who declined to pin them on that evidence and asked for a matched positive
control first. P3-15'-g/-g-ii/-g-iii/-g-iv built and measured it. P3-15'-f's block
asked for the recommendation to be re-derived, not re-quoted, once the control landed.
This is that re-derivation, over the whole grid rather than the six named tuples.

Inputs
- two.amlab:$HOME/tbox-scratch/round_p3_15_supply/msa (278, jobs 1205+1254, 90b2fe1b...)
- two.amlab:$HOME/tbox-scratch/round_p3_15g_control/msa (76, job 1264, 0b5afbec...)
- criterion (a) 941-row status (3215f034..., the digest the FP measurement records)
- criterion (c) 941-row status (3c8ac8e3..., the digest the exclusion diagnostic records)
- P3-15'-b's 941 Stage-2 posteriors (carried as a sensitivity; see below)

Outputs
- src/tbox_finder/mining/architecture_param_recommend.py
- reports/p3/architecture_parameter_recommendation.json (pins_nothing: true)
- data/processed/mining/round0_fp_spare_rule_inputs_v0.json (the committed 941-row join)
- tests/unit/test_architecture_param_recommend.py (69 tests)
- analyses/phase3_log.qmd stanza

The headline is not about (b): the round's yield ceiling is 15 of 941
Sparing is a disjunction, so mining is a conjunction. Criterion (a) fails on 111 of 941
and criterion (c) on 96; their intersection is 15, and all 15 have a consensus.
Declaring the Stage-2 disjunct lowers it to 12 (thr 0.5) or 13 (0.9/0.95/0.99). Over
the entire admissible grid the round mines between 1 and 13. (b)'s seven parameters are
choosing among single-digit differences, which no committed report stated.

The rule is stated before the counts
Four floors are read off ADR-0006 D3's wording and ADR-0004 D1's vocabulary
(min_named_helices >= 2, min_helix_pairs >= 2, ncca_pairing_nt >= 2, a bounded
bulge_max_nt); among the 864 survivors the rule takes the most permissive as measured,
which is A4's own fail-closed argument for a sparing disjunct. That selects exactly
sensitive_core, tied only with its proved-inert allow_wobble twin. Admissibility is
MEASURED by running the shipped localizer (2,880 of 3,840 points; the 960 refusals are
one rule), and each point's yield goes through the shipped spare_rule.is_mining_excluded
rather than a re-derived conjunction.

The composite (a) AND (b) is anti-selective at the strict end
-g-iv compared (b) alone and found no separation. On the conjunction the control falls
through MORE often than the FP arm at five of six named settings (9.2 vs 7.2 %, 10.5 vs
11.5 %, 38.2 vs 23.7 %, 43.4 vs 32.0 %, 57.9 vs 37.4 %, 59.2 vs 38.5 %), with disjoint
95 % Wilson intervals at the two strictest. (a) passes 39.5 % of the control against
60.1 % of the FP arm, so tightening (b) removes protection where (a) protected least.

Disclosures
- The report enumerates the 8 floor-clearing settings that beat its own recommendation
  (at most 2 extra mined) and states why the rule declines them.
- Criterion (c) is 546/96/299, not the 541/101/299 TODO.md and imp.md record.
- No committed artifact anchors the Stage-2 posteriors' digest and a second scoring
  exists differing by up to 3.8e-2; that column is a sensitivity, and the ceiling of 15
  uses (a) and (c) alone.
- slurm/p3/stage1_remine.sbatch never passes --relaxed-arch-status/--synteny-status, so
  remine.py:512 refuses a round declaring either backend live. Reported, not patched.

Validation
Zero new failures vs a matched main baseline at 090547f (worktree 13 FAILED, main 26, a
strict subset; the delta is DVC/LFS skips, named individually). 40 sabotages all RED
against their NAMED test plus 2 green negative controls; two defects of this step were
caught by that harness, not by review (a duplicated floor filter, and a vacuously-true
identity clause). Both artifacts re-derive byte-identically apart from generated_at_utc.
ruff + black clean. pins_nothing: true; no ADR amended.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
@coderabbitai

coderabbitai Bot commented Aug 12, 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: 49 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 Plus

Run ID: d88d32bc-f02a-43b4-9c50-829480949068

📥 Commits

Reviewing files that changed from the base of the PR and between 280fd60 and ca8e2c3.

⛔ Files ignored due to path filters (1)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
📒 Files selected for processing (1)
  • tests/unit/test_architecture_param_recommend.py
📝 Walkthrough

Walkthrough

Adds a complete architecture-parameter recommendation workflow. It validates provenance-bound inputs, evaluates the shipped localizer across a parameter grid, measures control and false-positive outcomes, selects a floor-clearing setting, and writes a non-pinning report.

Changes

Architecture recommendation

Layer / File(s) Summary
Parameter grid and decision contracts
src/tbox_finder/mining/architecture_param_recommend.py, tests/unit/test_architecture_param_recommend.py
Defines the seven-parameter grid, admissibility rules, decision floors, tie handling, and contract tests.
Evidence validation and arm measurements
src/tbox_finder/mining/architecture_param_recommend.py, tests/unit/test_architecture_param_recommend.py
Validates inputs, manifests, supplies, statuses, posterior values, and digests. Measures false-positive yield, control damage, criterion intersections, and locus-based counts.
Selection and comparison analysis
src/tbox_finder/mining/architecture_param_recommend.py, reports/p3/architecture_parameter_recommendation.json, tests/unit/test_architecture_param_recommend.py
Applies floors and tie rules. Computes sensitivity, frontiers, dominance, anti-selectivity, sentinel comparisons, and named-setting results.
Recommendation workflow and report output
src/tbox_finder/mining/architecture_param_recommend.py, reports/p3/architecture_parameter_recommendation.json, tests/unit/test_architecture_param_recommend.py
Builds joined inputs, runs the workflow, records provenance and limitations, exposes build-inputs and recommend, writes JSON, and tests end-to-end behavior.

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

Mergeability Score: 🔵 Low · up to 280fd

The PR is mergeable with explicit owner awareness: a test fixture can miss certain counter inversions, and another test does not exercise the JSON null case it names. These are bounded validation gaps with no supplied evidence of a production behavior defect.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant build_inputs
  participant recommend
  participant evaluate_point
  participant JSONReport
  CLI->>build_inputs: join source tables and record provenance
  build_inputs-->>CLI: write serialized inputs
  CLI->>recommend: validate manifests, supplies, and digests
  recommend->>evaluate_point: evaluate parameter grid
  evaluate_point-->>recommend: return yield and control measurements
  recommend->>JSONReport: assemble recommendation and ceilings
  JSONReport-->>CLI: write JSON report
Loading

Possibly related PRs

  • bioedca/tbox-finder#111: Produces Stage-2 supply, control, manifest, and posterior evidence consumed by this module.
  • bioedca/tbox-finder#115: Provides the relaxed-architecture backend whose parameters are swept by the workflow.
  • bioedca/tbox-finder#125: Provides related ADR-0006 architecture-parameter measurements consumed by this re-derivation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the re-derivation of criterion (b)’s seven parameters from matched control data, which is the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch p3-15f-param-choice

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

@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: 6

🧹 Nitpick comments (7)
src/tbox_finder/mining/architecture_param_recommend.py (4)

1266-1269: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass encoding="utf-8" to write_text.

Every read in this module pins encoding="utf-8". This write relies on the platform default. json.dumps keeps ensure_ascii=True by default, so the current output is ASCII and safe. If someone later sets ensure_ascii=False, the em dashes and in the emitted rationales would raise UnicodeEncodeError on a non-UTF-8 locale.

♻️ Proposed change
-    out.write_text(json.dumps(body, indent=2, sort_keys=True) + "\n")
+    out.write_text(json.dumps(body, indent=2, sort_keys=True) + "\n", encoding="utf-8")
🤖 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/mining/architecture_param_recommend.py` around lines 1266 -
1269, Update the _write function’s out.write_text call to explicitly use
encoding="utf-8", matching the module’s existing read behavior and ensuring
future non-ASCII JSON output is written reliably.

791-794: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Measured counts are hardcoded in prose the report publishes.

The docstring states "2,880 points" and "68 records". DECISION_RULE_STATEMENT (Line 229) and why_the_rule_declines_them (Lines 822-824) carry the same literals, and recommend emits both strings verbatim into the report. n_admissible and n_records_producible are computed at run time. If the supply changes, the report prints stale counts beside the correct derived ones, and a reader cannot tell which is which.

Format these counts from the computed values, or move them out of the published strings.

🤖 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/mining/architecture_param_recommend.py` around lines 791 -
794, Update DECISION_RULE_STATEMENT, why_the_rule_declines_them, and the report
assembly in recommend to avoid hardcoded “2,880 points” and “68 records”;
interpolate the computed n_admissible and n_records_producible values wherever
those published strings mention the counts, or remove the counts from the
reusable prose and emit them from the runtime data.

112-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive DECIDED_STATES from the imported status constants.

The module imports STATUS_PASSED and STATUS_FAILED but re-spells the same two values as literals here. A change to those constants in spare_rule would leave this tuple stale, and every guard that reads it (control_damage, assert_supply_is_the_decided_set) would silently classify records as not producible.

♻️ Proposed change
-DECIDED_STATES: tuple[str, ...] = ("passed", "failed")
+DECIDED_STATES: tuple[str, ...] = (STATUS_PASSED, STATUS_FAILED)
🤖 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/mining/architecture_param_recommend.py` around lines 112 -
113, Update DECIDED_STATES to derive its tuple values from the imported
STATUS_PASSED and STATUS_FAILED constants instead of duplicating string
literals, preserving the existing ordering and consumers such as control_damage
and assert_supply_is_the_decided_set.

950-954: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Add an explicit slug-cardinality check.

candidate_slug uses a 12-hex (48-bit) digest prefix, so it is not strictly injective. A collision would overwrite an entry in either index. The current candidate sets have no collisions, but compare each index size with its candidate-id count before evaluation.

🤖 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/mining/architecture_param_recommend.py` around lines 950 -
954, Add explicit cardinality checks after building fp_id_by_slug and
control_id_by_slug, comparing each index’s size with its corresponding
candidate-ID count before evaluation. Reject or fail clearly when the counts
differ, preserving all entries only when candidate_slug values are unique.
tests/unit/test_architecture_param_recommend.py (3)

106-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the dead if False branch.

p.as_dict()["label"] if False else ... always takes the else arm. The condition is a debug artifact. The dead arm is also wrong: as_dict() omits label, as Line 738 shows by passing label separately, so enabling the branch would raise KeyError.

♻️ Proposed change
-    assert (
-        len({p.as_dict()["label"] if False else tuple(sorted(p.as_dict().items())) for p in points})
-        == expected
-    )
+    assert len({tuple(sorted(p.as_dict().items())) for p in points}) == expected
🤖 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_architecture_param_recommend.py` around lines 106 - 109,
Remove the dead conditional expression from the set comprehension in the
architecture parameter assertion, using the sorted `p.as_dict().items()` tuple
directly. Preserve the existing deduplication and comparison against expected.

45-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Construct ParamTuple with keyword arguments in the fixtures.

LOOSE and CHOSEN pass seven positional values, and about twenty later call sites repeat the pattern. The axis order is stem_i_nt_threshold, min_named_helices, min_helix_pairs, bulge_min_nt, bulge_max_nt, ncca_pairing_nt, allow_wobble, which a reader must reconstruct from grid_points. If a field is reordered in ParamTuple, every fixture silently repoints at a different axis, the floor tests keep passing, and each one then asserts about a parameter it does not name.

♻️ Proposed change
-LOOSE = apm.ParamTuple("loose", 1, 1, 2, 1, 10_000, 1, False)
-CHOSEN = apm.ParamTuple("sensitive_core", 1, 2, 2, 2, 50, 2, False)
+LOOSE = apm.ParamTuple(
+    label="loose",
+    stem_i_nt_threshold=1,
+    min_named_helices=1,
+    min_helix_pairs=2,
+    bulge_min_nt=1,
+    bulge_max_nt=10_000,
+    ncca_pairing_nt=1,
+    allow_wobble=False,
+)
+CHOSEN = apm.ParamTuple(
+    label="sensitive_core",
+    stem_i_nt_threshold=1,
+    min_named_helices=2,
+    min_helix_pairs=2,
+    bulge_min_nt=2,
+    bulge_max_nt=50,
+    ncca_pairing_nt=2,
+    allow_wobble=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 `@tests/unit/test_architecture_param_recommend.py` around lines 45 - 46, Update
the LOOSE and CHOSEN fixtures, along with the repeated ParamTuple call sites in
this test module, to pass all fields by their parameter names rather than
positionally. Use the existing axis names—stem_i_nt_threshold,
min_named_helices, min_helix_pairs, bulge_min_nt, bulge_max_nt, ncca_pairing_nt,
and allow_wobble—while preserving each fixture’s current values and behavior.

331-336: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the case the tie refusal does not catch.

This test pins the refusal for two settings that share a _tie_break_key. It does not cover two settings in argmin that differ in a non-inert parameter and also differ in bulge_max_nt. That pair is resolved silently by sort order today. Refer to the comment on apply_decision_rule at src/tbox_finder/mining/architecture_param_recommend.py Lines 656-673 for the underlying defect.

💚 Proposed additional test
def test_a_tie_across_a_non_inert_axis_is_refused_even_when_bulge_max_differs():
    """Sort order must not decide min_helix_pairs just because bulge_max_nt differs."""
    a = point(params=apm.ParamTuple("a", 1, 2, 2, 2, 20, 2, False), fp_failed=10)
    b = point(params=apm.ParamTuple("b", 1, 2, 3, 2, 50, 2, False), fp_failed=10)
    with pytest.raises(rec.RecommendError, match="tied"):
        rec.apply_decision_rule([a, b])
🤖 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_architecture_param_recommend.py` around lines 331 - 336, Add
a regression test beside test_a_tie_the_break_order_cannot_resolve_is_refused
covering equal fp_failed candidates whose non-inert parameter and bulge_max_nt
differ, such as the proposed ParamTuple values. Assert apply_decision_rule
raises RecommendError matching “tied”, ensuring sort order cannot silently
resolve min_helix_pairs.
🤖 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/mining/architecture_param_recommend.py`:
- Around line 1151-1152: The manifest candidate IDs are not validated for
duplicates, allowing incorrect corpus cardinality and coverage checks. In
src/tbox_finder/mining/architecture_param_recommend.py lines 1151-1152, reject
duplicate manifest_ids before constructing rows; in lines 337-346, compare the
unique-ID count with the total count before the existing set-equality check so
duplicates cannot pass coverage validation.
- Around line 1151-1152: Deduplicate candidate IDs when constructing
manifest_ids in the manifest-loading flow, before the downstream sorted rows and
n_candidates calculations. Reuse the existing candidate_id_of-based extraction
and preserve deterministic ordering for unique IDs, so duplicate manifest
entries cannot inflate the candidate count or trigger a misleading
load_spare_rule_inputs failure.
- Around line 184-191: The rationale in architecture_param_recommend.py around
the bulge_max_nt recommendation must not claim verification through
recommendation.floor_sensitivity, because that field is None when the selection
does not move; remove that cited clause or directly measure and report the
sentinel comparison through an appropriate field. Regenerate
reports/p3/architecture_parameter_recommendation.json so its published rationale
and floor_sensitivity data match the source change.
- Around line 656-673: The tie refusal in _tie_break_key handling must detect
argmin settings that differ on any parameter outside TIE_BREAK_ORDER, even when
their tie-break keys differ; update the selection logic around chosen and
still_tied to reject such unresolved non-inert differences instead of relying on
sort order. In src/tbox_finder/mining/architecture_param_recommend.py lines
656-673, preserve normal tie-break behavior only when differences are limited to
the ordered vocabulary. In tests/unit/test_architecture_param_recommend.py lines
331-336, add coverage for tied settings differing in both min_helix_pairs and
bulge_max_nt and assert RecommendError.
- Around line 328-330: Update the stage2_posterior validation in the
row-processing logic to reject booleans and any numeric value outside the
inclusive range [0, 1]. Preserve acceptance of valid int and float
probabilities, and raise the existing RecommendError with the row path and
candidate ID for invalid values.
- Around line 474-508: Update control_damage so the zero-record case is handled
before calling wilson_interval or dividing by n_producible. When no records
contain producible queries, raise the established RecommendError instead of
producing confidence intervals or shares; preserve the existing calculations for
n_producible greater than zero.

---

Nitpick comments:
In `@src/tbox_finder/mining/architecture_param_recommend.py`:
- Around line 1266-1269: Update the _write function’s out.write_text call to
explicitly use encoding="utf-8", matching the module’s existing read behavior
and ensuring future non-ASCII JSON output is written reliably.
- Around line 791-794: Update DECISION_RULE_STATEMENT,
why_the_rule_declines_them, and the report assembly in recommend to avoid
hardcoded “2,880 points” and “68 records”; interpolate the computed n_admissible
and n_records_producible values wherever those published strings mention the
counts, or remove the counts from the reusable prose and emit them from the
runtime data.
- Around line 112-113: Update DECIDED_STATES to derive its tuple values from the
imported STATUS_PASSED and STATUS_FAILED constants instead of duplicating string
literals, preserving the existing ordering and consumers such as control_damage
and assert_supply_is_the_decided_set.
- Around line 950-954: Add explicit cardinality checks after building
fp_id_by_slug and control_id_by_slug, comparing each index’s size with its
corresponding candidate-ID count before evaluation. Reject or fail clearly when
the counts differ, preserving all entries only when candidate_slug values are
unique.

In `@tests/unit/test_architecture_param_recommend.py`:
- Around line 106-109: Remove the dead conditional expression from the set
comprehension in the architecture parameter assertion, using the sorted
`p.as_dict().items()` tuple directly. Preserve the existing deduplication and
comparison against expected.
- Around line 45-46: Update the LOOSE and CHOSEN fixtures, along with the
repeated ParamTuple call sites in this test module, to pass all fields by their
parameter names rather than positionally. Use the existing axis
names—stem_i_nt_threshold, min_named_helices, min_helix_pairs, bulge_min_nt,
bulge_max_nt, ncca_pairing_nt, and allow_wobble—while preserving each fixture’s
current values and behavior.
- Around line 331-336: Add a regression test beside
test_a_tie_the_break_order_cannot_resolve_is_refused covering equal fp_failed
candidates whose non-inert parameter and bulge_max_nt differ, such as the
proposed ParamTuple values. Assert apply_decision_rule raises RecommendError
matching “tied”, ensuring sort order cannot silently resolve min_helix_pairs.
🪄 Autofix

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 Plus

Run ID: 790a789a-6a7b-4b7f-ab64-b9a057850c75

📥 Commits

Reviewing files that changed from the base of the PR and between 090547f and 9392785.

⛔ Files ignored due to path filters (2)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
  • data/processed/mining/round0_fp_spare_rule_inputs_v0.json is excluded by !data/**
📒 Files selected for processing (3)
  • reports/p3/architecture_parameter_recommendation.json
  • src/tbox_finder/mining/architecture_param_recommend.py
  • tests/unit/test_architecture_param_recommend.py

Comment thread src/tbox_finder/mining/architecture_param_recommend.py
Comment thread src/tbox_finder/mining/architecture_param_recommend.py Outdated
Comment thread src/tbox_finder/mining/architecture_param_recommend.py
Comment thread src/tbox_finder/mining/architecture_param_recommend.py
Comment thread src/tbox_finder/mining/architecture_param_recommend.py Outdated
…rd the empty denominator

Two CodeRabbit CLI findings, both real.

1. The bounded-bulge floor's rationale cited recommendation.floor_sensitivity for the
   claim that keeping the bound costs nothing. That field is null in exactly this case
   (the floor does not move the selection, so the "identical" comparison would be the
   chosen setting against itself), so the citation pointed at the one place it could not
   be read. bulge_sentinel_comparison now measures the chosen setting against its own
   bulge_max_nt = 10000 twin and the rationale cites that: identical on fp_failed (52),
   n_mined (3), records losing (b) (9) and records losing (a) AND (b) (8).

2. control_damage divided by n_producible and called wilson_interval on it with no
   guard, so a control whose every record is unproducible raised SizingError /
   ZeroDivisionError instead of RecommendError — while anti_selectivity already guarded
   the same input. Both paths now refuse by name.

Six tests added (75 total), each with a positive control; four sabotages added (44
total), all RED against their named test plus the two green negative controls. Report
regenerated: the recommendation, the ceiling and the frontier are unchanged.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…ifest ids, posterior validation

Five CodeRabbit GitHub-app findings; two duplicated the CLI round already fixed in
b480caf, three are new and all real.

1. MAJOR — the tie refusal covered three of the seven parameters. It fired only when
   two argmin members shared a _tie_break_key, so two settings differing in BOTH
   min_helix_pairs and bulge_max_nt have different keys and were separated by
   bulge_max_nt alone, publishing a min_helix_pairs the rule never chose. That
   contradicts step (4) of DECISION_RULE_STATEMENT. The refusal now asks whether the
   tied settings disagree on any parameter outside TIE_BREAK_VOCABULARY, which is
   derived from TIE_BREAK_ORDER rather than restated beside it.
   ⚠ The recommendation, the yield ceiling and the frontier are unchanged. The only
   movement in the regenerated report is floor_sensitivity reporting 3 and 7 tied
   settings where it reported 2 and 4 — the old check had been resolving ties it should
   have refused.

2. MAJOR — a duplicated manifest candidate_id was never refused. Every corpus size in
   this report is a row count and every join against it is a set, so a duplicate
   inflates the denominator while assert_covers_manifest still reads as exact. Refused
   now in both assert_covers_manifest and build_inputs.

3. MINOR — stage2_posterior accepted bool (bool subclasses int, so `true` reads as 1.0
   and SPARES at every threshold) and accepted values outside [0, 1] (a mis-scaled
   column would clear every threshold without a refusal). Both refused.

Seven tests added (82 total), each with a positive control; five sabotages added (49
total), all RED against their named test plus two green negative controls. Zero new
failures vs the matched main baseline at 090547f.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
@bioedca

bioedca commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@greptileai

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a full-grid criterion-(b) parameter recommendation pipeline and its generated report, deriving yield, control impact, sensitivity, and frontier analyses.

  • Builds and validates the joined spare-rule inputs used by the analysis.
  • Evaluates admissible architecture configurations through the shipped localizer and spare-rule implementation.
  • Selects a floor-clearing recommendation with explicit tie handling and reports alternatives and limitations.
  • Adds extensive unit and end-to-end coverage for the new analysis.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking hardening issue around validating supply origins inherited from source reports.

The recommendation logic and validation paths are extensively covered, but report assembly can publish a local filesystem path when that value arrives through an input report rather than the explicitly validated argument.

Files Needing Attention: src/tbox_finder/mining/architecture_param_recommend.py

Security Review

The report assembly validates an explicitly supplied origin but copies origins embedded in its two source reports without the same local-path check. A source report containing a local filesystem path would therefore expose that path in the public recommendation artifact.

Important Files Changed

Filename Overview
src/tbox_finder/mining/architecture_param_recommend.py Adds the recommendation pipeline; report-derived supply origins bypass the local-path validation used for the explicit origin argument.
tests/unit/test_architecture_param_recommend.py Adds broad coverage for grid admissibility, spare-rule conjunctions, decision floors, tie handling, input validation, and end-to-end report generation.
reports/p3/architecture_parameter_recommendation.json Adds the generated recommendation artifact with provenance, yield ceilings, control comparisons, frontiers, and the selected parameter tuple.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Manifest and status artifacts] --> B[Build validated spare-rule inputs]
  C[FP consensus supply] --> D[Evaluate admissible parameter grid]
  E[Matched control supply] --> D
  B --> D
  D --> F[Apply semantic floors]
  F --> G[Select most permissive measured setting]
  G --> H[Recommendation report]
  D --> I[Yield and control frontiers]
  I --> H
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
src/tbox_finder/mining/architecture_param_recommend.py:1155-1164
**Report origins bypass path validation**

If an FP or control measurement report contains a local filesystem path in `supply.supply_origin`, `recommend()` copies it verbatim into the public recommendation even though the explicit `supply_origin` argument rejects such paths, exposing host-specific paths or account names. Apply the same validation to both report-derived origins before including them. **How this was verified:** The two report fields flow directly into `arms.fp` and `arms.control` without passing through `is_local_path_shaped()`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(mining): P3-15'-f review round 2 — t..." | Re-trigger Greptile

Comment on lines +1155 to +1164
"supply_origin": fp_report.get("supply", {}).get("supply_origin"),
"ground_truth": (
"unknown — a round-0 false-positive MANIFEST, not verified negatives"
),
},
"control": {
"step": "P3-15'-g-iv",
"n_consensuses": len(control_items),
"supply_digest_sha256": control_digest,
"supply_origin": control_report.get("supply", {}).get("supply_origin"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security Report origins bypass path validation

If an FP or control measurement report contains a local filesystem path in supply.supply_origin, recommend() copies it verbatim into the public recommendation even though the explicit supply_origin argument rejects such paths, exposing host-specific paths or account names. Apply the same validation to both report-derived origins before including them. How this was verified: The two report fields flow directly into arms.fp and arms.control without passing through is_local_path_shaped().

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tbox_finder/mining/architecture_param_recommend.py
Line: 1155-1164

Comment:
**Report origins bypass path validation**

If an FP or control measurement report contains a local filesystem path in `supply.supply_origin`, `recommend()` copies it verbatim into the public recommendation even though the explicit `supply_origin` argument rejects such paths, exposing host-specific paths or account names. Apply the same validation to both report-derived origins before including them. **How this was verified:** The two report fields flow directly into `arms.fp` and `arms.control` without passing through `is_local_path_shaped()`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

…d too, and the floor level was never mine to set

A 5-lens adversarial workflow, Greptile and a second CodeRabbit pass between them broke
two of this step's published claims. Both are corrected, not hedged.

1. THE FP ARM IS PSEUDO-REPLICATED. Of the 278 (a)-decided FP candidates only 199 are
   distinct loci: 79 loci contribute two candidate ids differing solely in the tiling
   window offset, with byte-identical msa.sto, identical (a) and (c) status and
   identical (b) verdicts at every named setting (verified here). The old unit_caveat
   stated the asymmetry BACKWARDS — it claimed the control was the pseudo-replicated
   arm and the FP arm was not — so every FP interval was too narrow while the control
   was refused one for a property both arms had. locus_of() collapses the window offset
   and every FP interval is now on n = 199 loci.

2. "DISJOINT INTERVALS AT THE TWO STRICTEST" IS WITHDRAWN. It was computed from a
   query-level control interval — the one control_damage itself refuses to publish —
   against a candidate-level FP one. On records (68) vs loci (199) NO setting has
   disjoint intervals. The control still falls through more at five of six settings, so
   the direction and the reading are unchanged; the separation claim is not.

3. THE min_named_helices LEVEL IS A §7 CHOICE, NOT A DERIVATION. D3 states no number
   and delegates the value to P6; a second reading is stated by this repo's own
   localizer (architecture.named_elements_present: "the canonical class-I core is Stem
   I + Stem III + antiterminator"), i.e. >= 3. The rule now runs at EVERY level and
   prices each: >=1 undecidable; >=2 sensitive_core (3 mined / 8 of 68); >=3 mnh3·mhp2·
   bulge2-50·ncca2 (11 mined / 24 of 68); >=4 mnh4·mhp2·bulge2-20·ncca2 (13 / 31 of 68).

Also fixed: the min_helix_pairs floor cited D3 for a concept D3 does not contain (its
source now says it is an assumption of this module, and the stack-depth distribution it
appeals to is published beside it); control_records derived the record grouping from the
candidate-id string, which record_of's own docstring forbids, and now reads accession
from the manifest via the shipped load_control_records; a supply_origin carried over
from either measurement report bypassed the is_local_path_shaped guard the flag gets
(Greptile P2); DECIDED_STATES is derived from the status constants; write_text is
explicit about utf-8; and no measured count is written into a published prose string.

UNCHANGED BY ALL SIX CORRECTIONS: the yield ceiling of 15, the 1..13 range over the
grid, the level-2 selection, and the frontier — counts of candidates and loci, none of
which rested on the broken claims.

98 unit tests; 62 sabotages all RED against their NAMED test + 2 green negative
controls. Two sabotages initially stayed green and both were my targeting, not the code
— a dedicated locus test and a retarget fixed them. Zero new failures vs main at
090547f. Report re-derives byte-identically apart from generated_at_utc.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…uous FP fixture, and anchors that are checked

The 5-lens adversarial workflow returned (38 agents, 0 errors). Three more real
corrections, one of them on code this step had just written.

1. THE FP NUMERATOR HAD NO DISCRIMINATING TEST. evaluate_point's fp_with_a — the whole
   FP side of the anti-selectivity reading — was reached by exactly one fixture, and
   that fixture set criterion (a) to `failed` on every candidate. Dropping the (a)
   conjunct, or reading synteny_status instead, left the suite green while the published
   claim went 5-of-6 -> 1-of-6, or was silently STRENGTHENED to 6-of-6. The locus test
   now uses an asymmetric corpus (a third candidate that fails (b) but passes (a)) so
   all three implementations disagree, and both mutations are in the sabotage set.

2. THE CEILING IS 15 MANIFEST ROWS BUT 12 DISTINCT LOCI. The tiling duplication that
   broke the FP intervals also inflates the ceiling: 3 of the 15 rows are second calls
   on a locus already counted. Every yield now carries both units; the recommendation's
   3 mined rows are 3 distinct loci.

3. build_inputs ASSERTED PROVENANCE IT HAD NOT CHECKED. Its anchored_by strings were
   unconditional constants that would keep claiming "report X records this digest" after
   either file changed. anchor_of now reads the named report, compares the digest and
   returns None on a miss; the (a) and (c) columns verify, the Stage-2 column stays
   honestly unanchored. min_mined_if_b_passed_everywhere is relabelled as the structural
   identity it is rather than a measurement.

104 unit tests; 69 sabotages all RED against their NAMED test + 2 green negative
controls. Zero new failures vs main at 090547f. Report re-derives byte-identically.

Still open and disclosed in the dev-log, not fixed here: frontier's nominal CI on a
minimum selected over ~1,000 points; the control damage share being conditioned on
producibility (68 producible, not the 160 drawn); the comparison report carried as prose
without a digest binding.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
min_named_helices level 2 => sensitive_core (user, AskUserQuestion, 2026-08-12):
STEM_I_NT_THRESHOLD=1 · MIN_NAMED_HELICES=2 · MIN_HELIX_PAIRS=2 · BULGE_MIN_NT=2 ·
BULGE_MAX_NT=50 · NCCA_PAIRING_NT=2 · ALLOW_WOBBLE=0.

Chosen against the canonical-core reading (level 3: 11 mined / 24 of 68 control records),
level 4 (13 / 31), and not running the producer at all. It takes the plural reading of
D3 and the most permissive non-vacuous setting on the grid — A4's own fail-closed
direction for a sparing disjunct. Expected yield 3 mined of a 15-row / 12-locus ceiling;
8 of 68 control records lose (a)+(b).

No ADR amended: A4 delegates the value to the round and the round records it in
provenance; D6/D17's P6 freeze is untouched.

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

@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 (3)
src/tbox_finder/mining/architecture_param_recommend.py (2)

420-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the side-effecting comprehension with an explicit count.

seen.add(cid) returns None, so the condition works, but the guard depends on a side effect inside a comprehension filter. A direct count states the same rule without that dependency.

♻️ Proposed refactor
-    seen: set[str] = set()
-    dupes = sorted({cid for cid in manifest_ids if cid in seen or seen.add(cid)})
+    counts = Counter(manifest_ids)
+    dupes = sorted(cid for cid, n in counts.items() if n > 1)

Add from collections import Counter to the imports.

🤖 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/mining/architecture_param_recommend.py` around lines 420 -
427, Replace the side-effecting `dupes` comprehension in the manifest validation
block with an explicit `Counter`-based count of `manifest_ids`, selecting
candidate IDs whose counts exceed one and preserving the existing sorted error
behavior. Add the required `collections.Counter` import.

522-527: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bind the ceiling id list once.

mined_ids(by_id, all_failed, stage2_threshold=None) runs twice for the two adjacent fields. The stage2_not_declared band entry already computed the same list. Bind it once so the published ids and the locus count cannot diverge.

♻️ Proposed refactor
-        "candidate_ids_at_the_ceiling": mined_ids(by_id, all_failed, stage2_threshold=None),
-        "distinct_loci_at_the_ceiling": len(
-            {locus_of(c) for c in mined_ids(by_id, all_failed, stage2_threshold=None)}
-        ),
+        "candidate_ids_at_the_ceiling": at_ceiling,
+        "distinct_loci_at_the_ceiling": len({locus_of(c) for c in at_ceiling}),

Assign at_ceiling = mined_ids(by_id, all_failed, stage2_threshold=None) before the return.

🤖 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/mining/architecture_param_recommend.py` around lines 522 -
527, In the return-building code around candidate_ids_at_the_ceiling and
distinct_loci_at_the_ceiling, bind mined_ids(by_id, all_failed,
stage2_threshold=None) once to at_ceiling before constructing the result, then
reuse at_ceiling for both the published ID list and locus count; align with the
existing stage2_not_declared computation so the same list is used consistently.
tests/unit/test_architecture_param_recommend.py (1)

273-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the expected exception type.

pytest.raises(Exception, match="record|accession") passes on any error whose message contains those words, including a TypeError or AttributeError raised before the contradiction check runs. The test then no longer proves that the delegated refusal is inherited. Assert the concrete error type that load_control_records raises for a contradicting row.

Run the following script to find the exception type the shipped loader raises:

#!/bin/bash
# Description: Locate load_control_records and the error type it raises for a contradicting accession.
fd -t f 'architecture_param_control_compare.py' -x ast-grep outline {} --items all
rg -nP -C 12 'def load_control_records' --type=py
🤖 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_architecture_param_recommend.py` around lines 273 - 277,
Update test_control_records_refuses_a_manifest_row_that_contradicts_its_own_id
to replace the broad pytest.raises(Exception) assertion with the concrete
exception type raised by load_control_records for a contradictory accession,
preserving the existing message match and delegated refusal scenario.
🤖 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/mining/architecture_param_recommend.py`:
- Around line 809-814: Update the undecidable-axis calculation near
chosen.params.as_dict() to use a one-way set difference: include only parameter
keys absent from TIE_BREAK_VOCABULARY, rather than names exclusive to either
set. Keep the disagreeing comprehension’s comparisons unchanged and ensure every
key in undecidable is valid for both parameter dictionaries.
- Around line 137-143: Update the candidate_id validation in the surrounding
recommendation logic to require exactly four colon-separated fields, rejecting
both fewer and more than four fields with the existing RecommendError behavior.
Preserve the locus derivation for valid accession:contig:window:start-end
identifiers and prevent extra fields from being silently discarded by the return
expression.

---

Nitpick comments:
In `@src/tbox_finder/mining/architecture_param_recommend.py`:
- Around line 420-427: Replace the side-effecting `dupes` comprehension in the
manifest validation block with an explicit `Counter`-based count of
`manifest_ids`, selecting candidate IDs whose counts exceed one and preserving
the existing sorted error behavior. Add the required `collections.Counter`
import.
- Around line 522-527: In the return-building code around
candidate_ids_at_the_ceiling and distinct_loci_at_the_ceiling, bind
mined_ids(by_id, all_failed, stage2_threshold=None) once to at_ceiling before
constructing the result, then reuse at_ceiling for both the published ID list
and locus count; align with the existing stage2_not_declared computation so the
same list is used consistently.

In `@tests/unit/test_architecture_param_recommend.py`:
- Around line 273-277: Update
test_control_records_refuses_a_manifest_row_that_contradicts_its_own_id to
replace the broad pytest.raises(Exception) assertion with the concrete exception
type raised by load_control_records for a contradictory accession, preserving
the existing message match and delegated refusal scenario.
🪄 Autofix

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 Plus

Run ID: 6da9a8dc-ddd1-43bb-a1c2-68a2c0a750d3

📥 Commits

Reviewing files that changed from the base of the PR and between 9392785 and ebb999f.

⛔ Files ignored due to path filters (2)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
  • data/processed/mining/round0_fp_spare_rule_inputs_v0.json is excluded by !data/**
📒 Files selected for processing (3)
  • reports/p3/architecture_parameter_recommendation.json
  • src/tbox_finder/mining/architecture_param_recommend.py
  • tests/unit/test_architecture_param_recommend.py

Comment thread src/tbox_finder/mining/architecture_param_recommend.py
Comment thread src/tbox_finder/mining/architecture_param_recommend.py Outdated
… the join, not on the next command

CodeRabbit CLI, 1 major, real. load_spare_rule_inputs refuses a bool posterior and an
out-of-range one; build_inputs — the command that WRITES the committed table — wrote
float(post[cid]) and str(cov[cid]) with no checks. float(True) is 1.0, so a `true` in the
source column would be committed as a value that is not a posterior, clear every
threshold in the Stage-2 band, SPARE the candidate, and be invisible to the loader
thereafter because by then the column really is a float. The guard ran after the artifact
it guards existed.

Both rules are now single functions — checked_status and checked_posterior — called by
the writer AND the reader. The proposed patch would have added a second copy of each rule
beside the existing one, which is how the writer came to have none of it.

6 tests added (110 total), 2 sabotages added (71 total, all RED against their named test
+ 2 green negative controls). The committed table re-derives to the identical sha256
(65aedc88…), so no current artifact changes. Zero new failures vs main at 090547f.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…ce rather than a symmetric one

Two CodeRabbit GitHub-app findings, both real.

1. locus_of refused FEWER than four colon-separated fields but accepted MORE. A
   five-field id would drop its middle fields and merge into another candidate's locus,
   shrinking every locus denominator with no refusal — the same failure control_records
   refuses for the record grouping. It now demands exactly four.

2. The tie refusal built its "axes TIE_BREAK_ORDER cannot speak to" with a SYMMETRIC
   difference. Today every vocabulary name is also a ParamTuple field so the two agree,
   but adding or renaming a non-parameter entry would put that name into `undecidable`
   and raise KeyError inside the guard that exists to prevent an undecided selection.
   Plain set difference now.

2 tests added (112), 2 sabotages added (73, all RED against their named test + 2 green
negative controls). The report re-derives unchanged. Zero new failures vs main at
090547f.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…, and the identity branch that had no test

CodeRabbit CLI round 7 on a359e74: 2 minor findings, both real.

(1) architecture_param_recommend read the FP manifest as
manifest.get("candidates", manifest) at BOTH call sites, meaning to accept an
object carrying a candidates list or the rows themselves. The second shape never
worked: a top-level JSON list has no .get, and AttributeError is outside main's
(RecommendError, ValueError, TypeError, OSError, KeyError), so the CLI ended in a
traceback where a refusal was intended. An object WITHOUT candidates fell back to
the mapping and iterated its keys. read_manifest_ids now serves both sites and
refuses the shape, a non-object row (the producer's reader DROPS one, which here
would silently shrink the 941 denominator behind the coverage check and the
ceiling) and a row carrying neither id key (candidate_id_of raises ProducerError,
a RuntimeError, which escapes the refusal path too). On the committed 941-row
manifest the new reader returns the identical ids, so no published number moves.

(2) floor_sensitivity's measured_identically_to_the_chosen_setting is
three-valued and the test named for its True branch asserted False — its twin was
strictly more permissive, so it won on the metric and the field could not be True.
Renamed for what it asserts, and the True branch now has its own test, reached
through the bulge floor (the one floor whose parameter is in the tie-break
vocabulary). On the shipped grid that field takes only null and False, so the
branch had no coverage at all.

Inputs: reports/p3/architecture_parameter_recommendation.json,
data/processed/mining/round0_fp_manifest.json (941 rows).
Outputs: no artifact change — both reports re-derive unchanged; the manifest ids
are byte-identical old vs new.
Validation: 124 unit tests in the module (112 before); 8 sabotages this round, all
RED against their NAMED test (81 cumulative); tests/unit + tests/golden vs the
matched main baseline at 090547f — worktree 13 FAILED, main 26, a strict subset,
zero new; ruff 0.15.15 + black 25.11.0 clean.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…date the control's statuses, and three refusal-path escapes

CodeRabbit CLI round 8 on fb6afcb: 5 findings (4 major, 1 minor), all real.

(1) arms.comparison_headline_carried_forward copied a READING of the two arms
out of the control-comparison report with no digest binding — the file's
provenance hash identifies the file, not its subject, so a comparison of a
different pair of arms could be quoted beside numbers derived from these two.
assert_comparison_describes_these_supplies now checks arms.fp / arms.control
against the digests measured here, and refuses an ABSENT digest as well as a
disagreeing one. The shipped comparison records 90b2fe1b… / 0b5afbec…, equal to
the two measurement reports', so the binding holds on the committed artifacts.
This was on the step's "disclosed, not fixed" list; it is now fixed.

(2) control_records validated the control's id set but never its status VALUES,
while control_damage reads producibility as `status[q] in DECIDED_STATES` — so a
typo, a null or a number silently left that query out of the 68-record
denominator behind every share and interval on this arm. The FP arm's columns
already pass checked_status in writer and reader.

(3) assert_supply_is_the_decided_set compared set(supply_ids), so a supply naming
one candidate twice merged into an exact-looking equality while every per-item
loop still saw both rows. Ids are materialised and the repeat refused.

(4) checked_posterior called float(value); a JSON integer beyond float range
raises OverflowError, an ArithmeticError outside main's except tuple — a
traceback where every other malformed posterior gets `refused:`.

(5) A dead `if False` operand in the grid-distinctness assertion, whose
unreachable branch read a `label` key as_dict() does not carry.

Inputs: reports/p3/architecture_parameter_control_comparison.json (arms digests),
reports/p3/architecture_parameter_measurement{,_control}.json.
Outputs: no committed artifact changes.
Validation: 135 unit tests in the module (124 before); 7 sabotages this round,
all RED against their NAMED test, including two positive controls (a status guard
that refuses everything; a grid whose points collide in value with the count
preserved) — 88 cumulative; tests/unit + tests/golden vs the matched main
baseline at 090547f — worktree 13 FAILED, main 26, zero new; ruff 0.15.15 +
black 25.11.0 clean.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…s file in its own refusal, and narrow an over-broad raises

CodeRabbit CLI round 9 on 203bd90: 3 findings (1 major, 2 minor), all real. Two
are on code round 8 wrote.

(1) The slug -> candidate_id indexes were dict comprehensions for both arms, so
two ids landing on one slug kept only the last: the consensus measured for that
slug would be attributed to one candidate while the other left arch_by_id
silently, every yield count still reconciling. The orphan check downstream only
finds slugs with NO candidate, so it cannot see this. index_by_slug refuses it.
Two distinct ids cannot collide by construction (candidate_slug appends a 12-hex
digest of the exact id), so the guard states a contract and the test triggers the
one reachable path — a repeated id.

(2) Round 8's status-value refusal printed the MANIFEST path as the location of a
bad STATUS value. control_records now takes status_path. The sabotage that
mattered was the call site, not the helper: dropping the keyword-only argument
left the suite green, so the pinning test asserts recommend's refusal names
ctrl_status.json.

(3) pytest.raises(Exception, ...) in an older test accepted AttributeError or
KeyError as readily as the refusal it pinned; it names RecommendError /
CompareError now.

Outputs: no committed artifact changes.
Validation: 139 unit tests in the module (135 before); 4 sabotages this round,
all RED against their NAMED test (92 cumulative), including a positive control
(an index refusing every id); tests/unit + tests/golden vs the matched main
baseline at 090547f — worktree 13 FAILED, main 26, zero new; ruff 0.15.15 +
black 25.11.0 clean.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
…ss found after the review gate came back clean

CodeRabbit CLI round 10 returned "No new findings" on b521cdc. A 4-lens
adversarial harness (27 agents, 0 errors, 23 candidates, 3 refuted by their own
verifiers) then found 20 more, 17 of them in two families.

FAMILY 1 — five more refusal-path escapes of the class round 7 closed for the
manifest. `report.get("supply", {}).get(...)`,
`comparison.get("headline", {}).get("reading")` and
`(fp_report.get("helix_arm") or {}).get(...)` default only when the key is
ABSENT, so a present-but-not-an-object value reaches `.get` and raises
AttributeError, outside main's except tuple. The headline one is the sharpest:
round 8 bound the comparison report's `arms` and left the field that guard exists
to protect unchecked. read_json_object + object_field now serve all of them, and
the control status table is shape-checked BEFORE index_by_slug consumes it —
twelve lines ahead of control_records' own refusal.

FAMILY 2 — nine published numbers no sabotage could reach. Each of these left all
139 tests green: `all(` -> `any(` on the record-level (a)AND(b) damage rule (the
rule behind the 8-of-68 figure the §7 choice was made against); dropping the (a)
conjunct from the query-level count; len(producible) -> len(queries) in the query
denominator; each share divided by the other numerator; the two Wilson intervals
swapped; frontier's argmin replaced by keep-the-last-seen;
share_failed_of_decided divided by fp_passed; the locus collapse in
mined_loci_by_threshold; the floors conjunct in dominating_alternatives. Cause:
every control fixture was symmetric, so ALL was indistinguishable from ANY and
(a)AND(b) from (b) alone. One asymmetric corpus now carries six quantities no two
of which are equal; point()'s two record counts differ by default; the intervals
are pinned BY IDENTITY (a containment assertion passed with them swapped, since
at n=3 each interval contains the other's point estimate). Two guard call sites
in recommend — the control decided-set check and the orphan check — were also
deletable in silence and are now driven through recommend.

Outputs: no committed artifact changes; the committed manifest still reads 941
ids through the new reader.
Validation: 156 unit tests in the module (139 before); 13 sabotages this round,
all RED against their NAMED test (105 cumulative); tests/unit + tests/golden vs
the matched main baseline at 090547f — worktree 13 FAILED, main 26, zero new;
ruff 0.15.15 + black 25.11.0 clean.

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

@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

🤖 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 `@tests/unit/test_architecture_param_recommend.py`:
- Around line 378-379: Update the asymmetric_control fixture in
tests/unit/test_architecture_param_recommend.py so its published quantities are
all distinct: add one producible record with a passing (b) query or add a second
decided query to record C, then update the related expected counts and docstring
assertions at the other occurrences accordingly.
- Around line 1440-1444: Update
test_recommend_refuses_a_report_whose_supply_block_is_null to parametrize the
supply value with both null and the existing string shape, writing each value
into the report and asserting RecommendError for both cases. Keep the test name,
error match, and tiny_corpus setup aligned with the existing behavior.
🪄 Autofix

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 Plus

Run ID: 0b7a6dc1-b5a0-4457-81f0-a1ef7e5d86e6

📥 Commits

Reviewing files that changed from the base of the PR and between fb6afcb and 280fd60.

⛔ Files ignored due to path filters (1)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
📒 Files selected for processing (2)
  • src/tbox_finder/mining/architecture_param_recommend.py
  • tests/unit/test_architecture_param_recommend.py

Comment thread tests/unit/test_architecture_param_recommend.py
Comment on lines +1440 to +1444
def test_recommend_refuses_a_report_whose_supply_block_is_null(tiny_corpus):
"""`.get("supply", {})` defaulted only for an ABSENT key; `null` reached `.get`."""
Path(tiny_corpus["fp_report_path"]).write_text(json.dumps({"supply": "cluster"}))
with pytest.raises(rec.RecommendError, match="'supply' is"):
rec.recommend(**tiny_corpus)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The test writes a string, not null.

The test name and the docstring both name null as the trigger. The body writes {"supply": "cluster"}, so the JSON null case that reaches .get stays uncovered. Parametrize the value to cover both shapes.

💚 Proposed fix
-def test_recommend_refuses_a_report_whose_supply_block_is_null(tiny_corpus):
+@pytest.mark.parametrize(
+    "supply",
+    [pytest.param(None, id="null"), pytest.param("cluster", id="a_string")],
+)
+def test_recommend_refuses_a_report_whose_supply_block_is_not_an_object(tiny_corpus, supply):
     """`.get("supply", {})` defaulted only for an ABSENT key; `null` reached `.get`."""
-    Path(tiny_corpus["fp_report_path"]).write_text(json.dumps({"supply": "cluster"}))
+    Path(tiny_corpus["fp_report_path"]).write_text(json.dumps({"supply": supply}))
     with pytest.raises(rec.RecommendError, match="'supply' is"):
         rec.recommend(**tiny_corpus)
📝 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
def test_recommend_refuses_a_report_whose_supply_block_is_null(tiny_corpus):
"""`.get("supply", {})` defaulted only for an ABSENT key; `null` reached `.get`."""
Path(tiny_corpus["fp_report_path"]).write_text(json.dumps({"supply": "cluster"}))
with pytest.raises(rec.RecommendError, match="'supply' is"):
rec.recommend(**tiny_corpus)
@pytest.mark.parametrize(
"supply",
[pytest.param(None, id="null"), pytest.param("cluster", id="a_string")],
)
def test_recommend_refuses_a_report_whose_supply_block_is_not_an_object(tiny_corpus, supply):
"""`.get("supply", {})` defaulted only for an ABSENT key; `null` reached `.get`."""
Path(tiny_corpus["fp_report_path"]).write_text(json.dumps({"supply": supply}))
with pytest.raises(rec.RecommendError, match="'supply' is"):
rec.recommend(**tiny_corpus)
🧰 Tools
🪛 ast-grep (0.45.1)

[info] 1441-1441: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"supply": "cluster"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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_architecture_param_recommend.py` around lines 1440 - 1444,
Update test_recommend_refuses_a_report_whose_supply_block_is_null to parametrize
the supply value with both null and the existing string shape, writing each
value into the report and asserting RecommendError for both cases. Keep the test
name, error match, and tiny_corpus setup aligned with the existing behavior.

…ic-control docstring

CodeRabbit CLI round 11 on 280fd60: 1 minor, real. The fixture's summary line was
corrected to "4 decided queries of 5" when the expectation was fixed, but the
record-B bullet still read "(3) differs from the query total (4)". Four tests
read this docstring as the primary statement of the expected counts.

Validation: 156 unit tests green; ruff 0.15.15 + black 25.11.0 clean; no code
change, no artifact change.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
… fires

CodeRabbit CLI round 12 on 6b74067: 1 minor, real. A test named for a `null`
supply block supplied the string "cluster". The two inputs take different
branches: `null` is object_field's permissive case, so the DIGEST gate refuses it
("different supply"), not the shape gate ("'supply' is str"). The scalar case
keeps the shape assertion under a matching name, and the null case is added under
the guard that actually fires.

Validation: 157 unit tests green (156 before); ruff 0.15.15 + black 25.11.0
clean; no code change, no artifact change.

Claude-Session: https://claude.ai/code/session_019FP5o18yYaQvzejBfkjwAB
@bioedca
bioedca merged commit 0ef2569 into main Aug 12, 2026
15 of 17 checks passed
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.

2 participants