Skip to content

P3-15′-d: the ADR-0006 D3 criterion-(b) relaxed-architecture backend - #115

Merged
bioedca merged 17 commits into
mainfrom
p3-15-prime-d
Aug 8, 2026
Merged

bioedca merged 17 commits into
mainfrom
p3-15-prime-d

Conversation

@bioedca

@bioedca bioedca commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes the last missing disjunct of the ADR-0005 D14 mining spare rule. Both §7 route gates were answered 2026-08-07 and ADR-0006 Amendment A4 is signed and carried in 695e874 — no new ADR here.

What lands

file what
mining/architecture.py criterion_b verbatim per D3, plus the de-novo localizer: Stockholm parse (delegated to the existing msa_shuffle.read_pfam_alignment, not forked), WUSS/dot-bracket pair table, maximal-helix + flanked-bulge decomposition, NCCA-pairing test
mining/architecture_producer.py per-candidate run-shard/merge status table, D3's held-out-canonical freeze, derive_relaxed_arch_supply_available
reports/p3/architecture_freeze.json the D3 measurement — pins nothing
slurm/p3/architecture_producer.sbatch the CPU-only (b) leg
slurm/p2/mine_round_producer.sbatch promotes each msa.sto out of $BUILD/work before the EXIT trap destroys it (A4 cross-reference impact)
mine_round / remine RELAXED_ARCH_SUPPLY_AVAILABLE, --relaxed-arch-status + paired refusals, the two-way --relaxed-arch-available/--no-… pair, exit-4 unevidenced preflight

The D3 freeze — measured, not fabricated

On the 8,715 held-out canonical records (source=='corpus' ∧ nested_role=='heldout', joined by ingest.record_hashnot positionally; the join is asserted to bind):

  • the flanked unpaired run containing the curated discriminator is 7 nt in 7,983 of 8,605 (92.8 %) — the literature's 7-nt antiterminator bulge, recovered independently from structure;
  • the derived UGGN acceptor motif is present in 99.87 % at 4 nt of pairing (99.93 % at 3 nt).

Every rule parameter ships keyword-required-with-no-default; the round supplies it and it rides in provenance. D17's P6 freeze is not consumed.

Two judgements, disclosed rather than absorbed

  1. D3's two relaxations are not symmetric. Read as a plain requirement-drop they fire together on every translational locus (D6 passes on translational, and a translational locus is class II), so criterion_b would be unconditionally True across the class-II corpus — and by D9 row 5 ((c)✓ ∧ (a)✓ ∧ (b)✗) that makes Tier-2N unreachable: the flagship class defined out of existence by its own predicate. criterion_b keeps the ADR's literal text so the P6 freeze tests D3 rather than my fix; the localizer resolves "required only where detectable" three-valued (absent = detectable-and-missing, a real failure; undetectable = nothing to judge, the only state the relaxation excuses), and architecture_status refuses passed when both relaxations are active with neither element observed → unavailable ⇒ spared.
  2. The NCCA motif is derived, not transcribed (§10.3). acceptor_pairing_motif computes it from the tRNA 3′ terminus N73-C74-C75-A76 by antiparallel Watson–Crick pairing → UGGN. §10.1 gate cleared with four agreeing peer-reviewed sources [PMID:12547201; DOI:10.1016/j.bbagrm.2008.09.001; PMID:15879350; DOI:10.1016/j.bpj.2009.12.2831]; the repo's own curated corpus agrees at 99.79 % over 23,208 records.

AS BUILT — the defect a hand-built fixture would have missed

The first bulge finder sized runs in alignment columns, and returned zero bulges on the real certified consensus: a 16-column run is only 8 residues in the candidate's own row, a 37-column run 11. Sizing now happens in the degapped residues of the candidate's own sequence. A gapless fixture passes either way — smoke-testing on the real job-766 artifact is what exposed it.

Validation

  • unit+golden 5,032 passed; failure set identical to a matched main baseline apart from three parquet-only tests this local env cannot run (CI installs pyarrow==24.0.0; their logic was executed directly in tbox-finder-data and all three hold).
  • 19 sabotages / 19 RED against their NAMED test.
  • ruff 0.15.15 + black 25.11.0 clean; snakemake --lint OK.
  • One real regression found and fixed en route: _refuse_unevidenced indexed derivations[key], so adding the (b) entry KeyError'd a sibling test. It now .gets and fails closed with the gap named.

⚠ This does not move n_mined off 0, and says so

Per A4, (b) reads the same consensus (a) does, so its supply derivation delegates to derive_msa_supply_available rather than probing twice. Until the full-corpus (a)/(b) producer array has run, every msa.sto is absent and the producer reports unavailablespared — fail-closed and honest. relaxed_architecture also stays out of TIER2N_PROTECTIVE_DISJUNCTS, so a (b)-only round is still refused at readiness; a test pins that building the backend did not change that.

https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM

Summary by CodeRabbit

  • New Features

    • Added relaxed architecture detection for candidate structures, including helix, bulge, motif, and stem analysis.
    • Added parallel scoring, status reporting, result merging, and architecture freeze reporting.
    • Integrated architecture evidence into mining, round planning, and re-mining workflows.
    • Promoted current candidate alignments for downstream analysis.
  • Bug Fixes

    • Improved fail-closed handling for unavailable evidence and required inputs.
    • Prevented stale or incomplete alignments from being reused.
    • Updated diagnostic messaging for relaxed architecture support.
  • Tests

    • Added comprehensive coverage for detection, production, validation, CLI behavior, and workflow integration.

…tive consensus

Signed 2026-08-07 (bioedca, CLAUDE.md §7 item 2). D3 named "R2DT 2.0 /
cmalign"; both fail, so A4 pins criterion (b)'s element localization to the
A3 per-candidate mlocarna `#=GC SS_cons` comparative consensus instead.

Why, measured rather than argued:
- R2DT is unreachable here. api.anaconda.org returns "could not be found"
  for bioconda/r2dt, conda-forge/r2dt AND bioconda/traveler, while
  bioconda/infernal resolves on the same call (the probe is powered).
  ADR-0002 D1 records no container runtime on the cluster.
- R2DT would add no independent structure anyway: its Rfam path IS cmalign
  against Rfam CMs (PMID:39921562, PMID:34108470); Traveler adds layout.
  With no template hit it "will not attempt to draw a structure".
- RF00230 is class-I scoped (PMID:32882008); the cmalign route also needs
  D8's alignability cutoff, which D17 freezes at P6 with no value.

Pins the instrument and its anti-circularity constraints only — never
RF00230, never an Rfam/class-I template, never the model's own output. No
algorithm is fabricated (§10.3); D3's freeze-on-held-out-canonical +
unit-test obligation is unchanged.

D6's short-Stem-I threshold ships keyword-required with no default, supplied
per round; D17's P6 freeze is NOT consumed. The relaxation only ever spares
more, so an imperfect value errs fail-closed.

Disclosed cost: (a) and (b) now share the homolog-MSA supply, so ADR-0005
D14's "three independent model-independent disjuncts" no longer holds for
two of them. Recorded, not absorbed.

Not amended: D3's predicate logic + both relaxations, D6/D7/D8 values,
D17, D9 row 5, D5/D8's specifier path, A1/A2/A3. No ADR-0002 amendment is
required (envs/locarna.yml + envs/rscape.yml already pinned and locked).
R2DT as a §17 figure deliverable is untouched.

Validation: doc-only; no code, no value pinned, no DVC object, no SLURM job.
No test parses ADR-0006 (only ADR-0005 is read, by test_aux_ablation_check).

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
P3-15'-d. Implements criterion (b) off the A3/A4 de-novo comparative consensus,
completing the last missing disjunct of the ADR-0005 D14 mining spare rule.

Inputs
  data/interim/homolog_msa/certified_positive.sto  (real job-766 mlocarna consensus)
  data/processed/master_clean_v0.parquet            (curated Structure/discriminator)
  data/processed/splits/split_assignments.parquet   (ADR-0004 held-out carve)

Outputs
  src/tbox_finder/mining/architecture.py           criterion_b verbatim per D3, plus the
      de-novo localizer: Stockholm parse (DELEGATED to msa_shuffle.read_pfam_alignment,
      not forked), WUSS/dot-bracket pair table, maximal-helix + flanked-bulge
      decomposition, and the NCCA-pairing test.
  src/tbox_finder/mining/architecture_producer.py  per-candidate run-shard/merge status
      table, D3's held-out-canonical freeze, derive_relaxed_arch_supply_available.
  reports/p3/architecture_freeze.json              the D3 measurement (pins nothing).
  slurm/p3/architecture_producer.sbatch            the CPU-only (b) leg.
  slurm/p2/mine_round_producer.sbatch              PROMOTES each msa.sto out of
      $BUILD/work before the EXIT trap destroys it (A4 cross-reference impact).
  mine_round.RELAXED_ARCH_SUPPLY_AVAILABLE + --relaxed-arch-status + paired refusals;
  remine's two-way --relaxed-arch-available/--no- pair + its exit-4 preflight.

Two judgements, disclosed not absorbed
  1. D3's relaxations are NOT symmetric. Read as a plain requirement-drop they fire
     together on every translational locus, so criterion_b would be unconditionally True
     across the class-II corpus -- and by D9 row 5 that makes Tier-2N unreachable.
     criterion_b keeps the ADR's literal text (so the P6 freeze tests D3, not this fix);
     the localizer resolves "required only where detectable" three-valued, and
     architecture_status refuses `passed` on no evidence, routing to unavailable/spared.
  2. The NCCA motif is DERIVED, not transcribed (10.3): computed from the tRNA 3' end
     N73-C74-C75-A76 by antiparallel Watson-Crick pairing, giving UGGN. Four agreeing
     peer-reviewed sources back the fact (PMID:12547201; DOI:10.1016/j.bbagrm.2008.09.001;
     PMID:15879350; DOI:10.1016/j.bpj.2009.12.2831); the repo's own curated corpus agrees
     at 99.79% over 23,208 records.

Validation
  freeze over 8,715 held-out canonical records (joined by ingest.record_hash, not
  positionally): the run containing the curated discriminator is 7 nt in 7,983 of 8,605
  (92.8%) and carries the derived UGGN motif in 99.87% at 4 nt of pairing.
  unit+golden 5,032 passed; failure set identical to a matched main baseline apart from
  three parquet-only tests this local env cannot run (CI installs pyarrow==24.0.0; their
  logic was executed directly in tbox-finder-data and all three hold).
  19 sabotages / 19 RED against their NAMED test. ruff + black clean; snakemake --lint OK.

AS BUILT: sizing bulges in ALIGNMENT COLUMNS returned zero bulges on the real consensus --
a 16-column run is 8 residues in the candidate's own row. Sizing is now in degapped
residues of the candidate's own sequence. A gapless hand-built fixture would not have
caught it.

n_mined is NOT moved off 0 by this: per A4, (b) reads the same consensus (a) does, so its
supply delegates to derive_msa_supply_available and every candidate reports `unavailable`
=> spared until the full-corpus (a)/(b) producer array has run. relaxed_architecture also
stays out of TIER2N_PROTECTIVE_DISJUNCTS (D9 row 5), pinned by a test.

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

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a relaxed-architecture backend for Stockholm consensus evaluation. The change includes distributed status production, held-out freeze measurements, MSA promotion, Slurm execution, and mining and re-mining supply integration.

Changes

Relaxed architecture backend

Layer / File(s) Summary
Architecture localization and status evaluation
src/tbox_finder/mining/architecture.py, tests/unit/test_architecture.py
Adds Stockholm parsing, helix and bulge detection, NCCA motif evaluation, D6 relaxation, criterion-B evaluation, and three-valued architecture status handling.
Status production and shard merging
src/tbox_finder/mining/architecture_producer.py, tests/unit/test_architecture_producer.py
Adds candidate evaluation, deterministic sharding, status-table validation and merging, required threshold configuration, and CLI workflows.
Held-out freeze and provenance
src/tbox_finder/mining/architecture_producer.py, reports/p3/architecture_freeze.json, tests/unit/test_architecture_producer.py
Adds held-out canonical measurements, hash joins, provenance metadata, external-input hashes, and the P3-15'-d freeze artifact.
MSA promotion and Slurm execution
src/tbox_finder/mining/covariation_producer.py, slurm/p2/mine_round_producer.sbatch, slurm/p3/architecture_producer.sbatch
Centralizes the MSA filename, clears stale alignments, promotes current MSAs, and runs guarded shard production and merging through a Slurm array.
Round and re-mining evidence integration
src/tbox_finder/mining/mine_round.py, src/tbox_finder/mining/remine.py, src/tbox_finder/mining/synteny_producer.py, tests/unit/test_architecture_producer.py, tests/unit/test_synteny_producer.py
Propagates architecture statuses and fail-closed supply declarations through mining, re-mining, spare-rule checks, and joint diagnostics.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant mine_round_producer
  participant architecture_producer
  participant architecture_slurm
  participant mine_round
  mine_round_producer->>mine_round_producer: clear stale and promote current msa.sto files
  architecture_slurm->>architecture_producer: run shard with explicit thresholds
  architecture_producer-->>architecture_slurm: write shard status table and marker
  architecture_slurm->>architecture_producer: merge complete shard tables
  architecture_producer-->>architecture_slurm: write merged architecture status table
  mine_round->>architecture_producer: load candidate status table
  architecture_producer-->>mine_round: return per-candidate architecture status
  mine_round->>mine_round: apply status to candidate evidence and spare-rule checks
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.25% 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 and concisely identifies the ADR-0006 D3 criterion-(b) relaxed-architecture backend, which is the main change.
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 p3-15-prime-d

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

… merge

Two majors from the CodeRabbit CLI on eb0cd44, both real.

1. slurm/p2/mine_round_producer.sbatch (major, Data Integrity): the promotion loop
   copied only on success, so a candidate that produced an msa.sto on an EARLIER run
   but is not producible on this one kept the stale alignment in $ROUND_DIR/msa. The
   (b) producer would then score that stale consensus instead of resolving
   `unavailable` => spared. Each candidate's promoted msa.sto is now cleared FIRST,
   whether or not this run produced one. The loop iterates this shard's own workroot,
   so the cleared set is exactly the candidates this task owns -- no cross-shard race.

2. slurm/p3/architecture_producer.sbatch (major, Stability): two tasks could pass the
   marker-completeness check concurrently and both merge, and `_write` uses
   Path.write_text, which is not an atomic replace -- a second writer can interleave
   and leave a truncated table that still parses. Now uses the same BLOCKING flock +
   MERGE_OK idiom as slurm/p3/stage2_producer.sbatch, and reads the merged artifact
   back through the CONSUMER (load_status_map) rather than the writer.

tests/unit/test_sbatch_rm_targets.py 41 passed (up from 40 -- the new `rm -f` target is
parsed and accepted, not an unresolvable command-substitution widening to `*`).
ruff + black clean.

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

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

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

540-554: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The AST scan misses parameter names and keyword-argument names.

The walk collects ast.Name, ast.Attribute, ast.Constant, and import names. It does not collect ast.arg or ast.keyword. A parameter named cmalign_bitscore, or a call such as f(rf00230_path=...), would pass the guard. Add both node types so the scan covers the whole executable surface.

♻️ Proposed change
             elif isinstance(node, ast.Attribute):
                 tokens.append(node.attr)
+            elif isinstance(node, ast.arg):
+                tokens.append(node.arg)
+            elif isinstance(node, ast.keyword) and node.arg:
+                tokens.append(node.arg)
             elif isinstance(node, (ast.Import, ast.ImportFrom)):
🤖 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.py` around lines 540 - 554, Extend the AST token
collection in the architecture scan to include parameter names from ast.arg and
keyword-argument names from ast.keyword, alongside the existing ast.Name and
ast.Attribute handling. Ensure both names are added to haystack so forbidden
identifiers such as cmalign_bitscore and rf00230_path are detected by the
existing assertions.
tests/unit/test_architecture_producer.py (1)

163-207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add merge tests for the two silent-loss cases.

TestMerge covers duplicates, config mismatch, and the empty path list. It does not cover a shard whose rows list is empty, or a shard with no config block. Both merge cleanly today. See the comment on src/tbox_finder/mining/architecture_producer.py lines 292-317. Add the two cases together with the refusals, so the guard cannot regress.

🤖 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_producer.py` around lines 163 - 207, Extend
TestMerge with refusal tests for a shard containing an empty rows list and for a
shard missing its config block. Construct each malformed shard directly, call
architecture_producer.merge_status_tables, and assert ProducerError with the
appropriate existing validation message, ensuring both silent-loss cases are
guarded.
🤖 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 `@slurm/p2/mine_round_producer.sbatch`:
- Around line 94-103: Update the MSA publication flow around the loop that
copies `msa.sto` into `MSA_OUT` so each producer attempt starts from a clean,
run-scoped authoritative set. Remove or replace any existing `$MSA_OUT` contents
before iterating, ensuring candidates that fail to produce an MSA cannot retain
artifacts from earlier attempts while preserving the current promotion logic.

In `@slurm/p3/architecture_producer.sbatch`:
- Around line 55-59: The documented sbatch submission command must provide all
six rule parameters required by the script’s variables at lines 97-102. Update
the submit command’s --export value to include each required parameter alongside
ROUND_DIR, or replace it with a checked wrapper that validates and passes them
before invoking sbatch.
- Around line 169-190: Guard the merge path after confirming all shard markers
with an atomic mkdir lock at $MARKER_DIR/.merge.lock. If acquiring the lock
fails, exit successfully as a non-merger; only the task that creates the lock
may validate status files and run the architecture_producer merge writing
MERGED_OUT.

In `@src/tbox_finder/mining/architecture_producer.py`:
- Around line 482-490: Update the ncca_recovery measurement loop around
ncca_bulge_status so its bulge_size_range targets only the curated containing
bulge rather than all flanked bulges in the record; retain the existing
ncca_hits counting behavior, then regenerate reports/p3/architecture_freeze.json
with the corrected per-bulge results.
- Around line 292-317: Update the shard-table merge logic around the payload
loop to reject tables with a missing or empty rows collection and reject tables
without a config mapping containing the required rule keys, rather than
defaulting either value to empty data. Validate these conditions before
appending to rows or configs, preserve the existing duplicate candidate and
cross-config checks, and align the validation with the sibling synteny merge
behavior.

In `@src/tbox_finder/mining/architecture.py`:
- Around line 190-192: Update the wobble check in the NCCA bulge matching logic
to compare the bulge base with the recovered acceptor base, using
WATSON_CRICK[motif_base] before consulting WOBBLE_PAIRS; apply the same
correction to the additional occurrence. Update
test_wobble_is_off_unless_asked_for to use a real G·U case, such as bulge G with
motif base A, while preserving the default allow_wobble=False behavior.

In `@src/tbox_finder/mining/remine.py`:
- Around line 802-807: Update the remine argument setup around _supply_flag_pair
to add the matching relaxed-arch-status input for the relaxed-arch-available
declaration. In remine’s apply-spare-rule flow, validate the
declaration/status-table pair consistently with mine_round.apply_spare_rule,
load the table via architecture_producer.load_status_map, and pass the resulting
relaxed_arch_status map to candidate_evidence and read_fp_manifest.

---

Nitpick comments:
In `@tests/unit/test_architecture_producer.py`:
- Around line 163-207: Extend TestMerge with refusal tests for a shard
containing an empty rows list and for a shard missing its config block.
Construct each malformed shard directly, call
architecture_producer.merge_status_tables, and assert ProducerError with the
appropriate existing validation message, ensuring both silent-loss cases are
guarded.

In `@tests/unit/test_architecture.py`:
- Around line 540-554: Extend the AST token collection in the architecture scan
to include parameter names from ast.arg and keyword-argument names from
ast.keyword, alongside the existing ast.Name and ast.Attribute handling. Ensure
both names are added to haystack so forbidden identifiers such as
cmalign_bitscore and rf00230_path are detected by the existing assertions.
🪄 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: 3cbe0527-51ad-4678-929a-26d9d75a4ebd

📥 Commits

Reviewing files that changed from the base of the PR and between e228717 and eb0cd44.

⛔ Files ignored due to path filters (2)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
  • docs/decisions/ADR-0006-validation-decision-rule-and-tiering.md is excluded by !**/*.md, !docs/decisions/**
📒 Files selected for processing (11)
  • reports/p3/architecture_freeze.json
  • slurm/p2/mine_round_producer.sbatch
  • slurm/p3/architecture_producer.sbatch
  • src/tbox_finder/mining/architecture.py
  • src/tbox_finder/mining/architecture_producer.py
  • src/tbox_finder/mining/mine_round.py
  • src/tbox_finder/mining/remine.py
  • src/tbox_finder/mining/synteny_producer.py
  • tests/unit/test_architecture.py
  • tests/unit/test_architecture_producer.py
  • tests/unit/test_synteny_producer.py

Comment thread slurm/p2/mine_round_producer.sbatch Outdated
Comment thread slurm/p3/architecture_producer.sbatch Outdated
Comment thread slurm/p3/architecture_producer.sbatch
Comment thread src/tbox_finder/mining/architecture_producer.py
Comment thread src/tbox_finder/mining/architecture_producer.py
Comment thread src/tbox_finder/mining/architecture.py
Comment thread src/tbox_finder/mining/remine.py
…nrunnable

Third major from the GitHub app on eb0cd44, and the only one of its three not
already closed by 18307fe.

The documented submit command exported only ROUND_DIR, but the script `${VAR:?}`-
requires six rule parameters (STEM_I_NT_THRESHOLD, MIN_NAMED_HELICES,
MIN_HELIX_PAIRS, BULGE_MIN_NT, BULGE_MAX_NT, NCCA_PAIRING_NT). A submission
copied from the header would have exited before producing a single shard. All six
are now in the documented --export, flagged as placeholders rather than pins, and
pointed at reports/p3/architecture_freeze.json for the choice.

Also records that `sbatch --test-only` validates the SLURM HEADERS ONLY and does
not run the body, so it cannot catch a missing rule parameter -- the §9.3
preflight now names `bash -n` beside it.

The app's other two majors were already fixed in 18307fe (it marks the stale-MSA
one "Addressed in commit 18307fe" itself). ⚠ Its suggested fix for the merge race
-- a non-blocking `mkdir` lock -- is NOT taken: that is the exact interleaving
slurm/p3/stage2_producer.sbatch documents as leaving NOBODY merging (task A holds
the lock and counts 1 marker; B writes its marker, fails the lock, exits; A finds
its count short and exits too). The blocking flock already in 18307fe is the repo's
own precedent and does not have that hole. Finding right, suggested fix wrong.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
…status table

Major, and the most consequential finding of the step. `mine_round`'s
apply-spare-rule gained `--relaxed-arch-status` in eb0cd44; `remine`'s — the leg
the P3 round actually invokes — did not. A P3 round could therefore declare
relaxed_arch_available=True, pass its exit-4 preflight, and still read
`unavailable` for every candidate: a clean-looking zero yield indistinguishable
from an honest one, which is exactly the state the paired refusals exist to make
unrepresentable.

[[fixed-one-of-two-identical-things]] once more: two apply paths, one wired.

Threaded through the whole chain rather than at the CLI alone:
  remine_candidate_evidence(..., relaxed_arch_status=)  delegates to the P2 builder
  read_remine_manifest(..., relaxed_arch_status=)       delegates to read_fp_manifest
  _load_relaxed_arch_status()                           beside _load_synteny_status,
      same broad except-list (a truncated table raises JSONDecodeError, a non-object
      root AttributeError, a malformed row KeyError/TypeError -- all input faults that
      would otherwise bypass the documented refusal path)
  run_remine_round(..., relaxed_arch_status_table=)     + BOTH paired refusals
  apply-spare-rule --relaxed-arch-status

Adds TestTheP3RoundCarriesBToo (8 tests) -- the end-to-end coverage the review asked
for, each naming a specific link in the chain rather than only the end state.
Sabotage-verified: reverting the remine wiring turns exactly the new end-to-end
tests RED (3 of 8), and the restore is clean.

unit+golden 5,040 passed; failure set unchanged vs the matched main baseline apart
from the three parquet-only tests CI covers. ruff + black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
…ble test my own

test had locked in

⚠ I acted on only 1 of r2's 14 findings at first, having read the CLI output while it
was still streaming and seen only its tail. Re-read in full; the rest are addressed
here or recorded for the next round.

CORRECTNESS (all major unless noted):

1. architecture._pairs_with — THE WOBBLE ARM WAS INVERTED, and my own test locked it in
   ([[tests-can-specify-the-bug]]). `motif_base` is ALREADY the Watson-Crick complement
   of the acceptor base, so comparing the candidate base against it is not a pairing
   question. A G·U wobble is between the bulge base and the ACCEPTOR base. Measured
   consequence on the default NCCA/UGGN: allow_wobble=True admitted only NON-pairing
   bases and could never admit a real wobble, since UGGN carries no A or C motif
   position for (G,U)/(U,G) to match. Now compares against WATSON_CRICK[motif_base].
   test_wobble_is_off_unless_asked_for asserted the inverted rule; rewritten to the real
   interface, plus a direction-only unit pin independent of bulge geometry.

2. architecture.degapped_span — deleted IUPAC ambiguity codes as if they were gaps.
   That understated the residue count the bulge-size test reads AND made residues either
   side of an ambiguity code adjacent in the scanned window, so a contiguous motif match
   could be reported where none exists. A FALSE POSITIVE — the one direction this module
   must not get wrong, since it does not err toward sparing. Only GAP_SYMBOLS are dropped
   now; ambiguity codes survive as opaque residues and _pairs_with already rejects them.

3. architecture.ncca_bulge_status — an all-wildcard register matched every base, so
   ncca_pairing_nt=1 (reachable: no default, the round supplies it) reported
   BULGE_DETECTED on every admissibly-sized bulge regardless of sequence. Unconstrained
   registers are dropped; a motif with no constrained position at all now refuses.

4. mine_round._cmd_apply_spare_rule — validated only the MSA declaration, so it accepted
   --relaxed-arch-available on a checkout that cannot evidence the (b) supply. The
   fail-OPEN direction, and the one `remine` already refuses: two apply paths must agree
   about what "declared" costs or the weaker one is the way around the gate. Now exits 4,
   with a positive control that proves the gate was passed rather than merely not hit.

5. NOT taken, with the reason: the duplicate-row-name guard. The finding assumed `names`
   and `sequences` are built independently; they are not — `names` derives FROM the
   delegate's output, which already accumulates by name, so n_sequences == len(sequences)
   by construction and cannot overstate the A2 depth floor. A guard there would be
   unreachable code asserting an invariant the construction provides. Replaced with the
   invariant stated in a comment and pinned by
   test_n_sequences_cannot_exceed_the_rows_actually_present.

unit+golden 5,050 passed; failure set unchanged vs the matched main baseline apart from
the three parquet-only tests CI covers. ruff + black clean.

STILL OPEN from r2 (recorded in TODO.md, not yet addressed): atomic msa.sto promotion;
per-candidate rather than per-shard read-error handling; bulge-range validation at
ArchitectureRunConfig construction; shard-table row-shape validation before merge; a
shared MSA-filename constant; ncca_recovery scoping in the freeze; --allow-wobble never
supplied by the sbatch; an unrecognized bulge_state degrading to failed; report value
(not only key) mismatches.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
Closes r2. Final round tally was 15 findings (10 major, 5 minor), not the 14 I
reported: six were fixed in cf1cfc7, one declined with reasons there, and these
nine are the rest.

MAJOR
1. architecture_producer.freeze — ncca_recovery scanned EVERY flanked bulge while
   the denominator counted only records whose discriminator-bearing bulge was
   located, so an unrelated bulge could satisfy the motif and inflate the
   numerator. `ncca_bulge_status` gains `target=`; the freeze now scores the
   located bulge alone. RE-DERIVED: only k=3 moved (99.930% -> 99.884%, 4
   records). The headline k=4 99.87% and the modal 7 nt are UNCHANGED, so the
   number I flagged as unverified in the last summary holds.
2. Atomic promotion in mine_round_producer.sbatch — `cp` wrote straight to the
   path (b) reads, so a task killed mid-copy left a partial file. A truncation
   that drops trailing rows but KEEPS #=GC SS_cons parses as a valid alignment of
   reduced depth and would be scored as real evidence against the A2 floor. Now
   copies to a temp name in the destination dir and `mv`s (atomic within a fs).
3. ArchitectureRunConfig.__post_init__ — an inverted bulge range is empty, so no
   bulge satisfies it, every candidate resolves to `failed` => MINED, and the round
   reports a sweep it never evaluated. Reachable from the CLI precisely because
   nothing is defaulted. Refuses now, as do non-positive counts.
4. merge_status_tables — three separate shape faults in tables written by other
   processes: a missing config coerced to {} makes all shards compare EQUAL and
   publishes an empty config into provenance; a non-Mapping row raises
   AttributeError, which `main` does not catch, so the merge leg exits with a
   traceback instead of FATAL/exit 1; a row missing candidate_id passes the
   duplicate check as "" and a SECOND such row reports "appears in more than one
   shard table", naming the wrong fault. All three named explicitly.
5. MSA_FILENAME now DEFINED in covariation_producer (which writes the file) and
   IMPORTED by architecture_producer. My comment had claimed this already; it did
   not. A forked literal would let a rename make every (b) candidate silently
   `unavailable` while the shard tables still merged cleanly.

MINOR
6. Localization.__post_init__ validates bulge_state against BULGE_STATES —
   architecture_status tests two states by equality, so a typo or a state
   round-tripped through a serialized payload fell through to a DECIDED NEGATIVE
   (failed => minable) rather than a refusal.
7. slurm/p3: ALLOW_WOBBLE is now a required export. Without it the round's G-U
   policy was decided by a code default, the outcome the producer docstring says
   must not happen. --min-sequences stays omitted: it is the A2 Pin 2 pin.
8. validate_status_payload reports CONFLICTING VALUES, not only key-set diffs. On
   same-key/different-value drift both lists were empty and the operator read
   "(declared-only [], rows-only [])" -- naming no fault at all.
9. evaluate_candidate's except widened as asked -- but SABOTAGE SHOWED IT
   REDUNDANT: `parse_stockholm` already converts OSError/UnicodeError/ValueError
   to ArchitectureError, so narrowing it back stayed GREEN. The test therefore
   moved to the line that actually carries the guarantee
   (TestReadErrorsBecomeArchitectureErrors); sabotaging THAT turns 3 of 4 red. The
   wider clause is kept as defence for a future caller that reaches `localize`
   without going through `parse_stockholm`, and says so.

Validation: unit+golden 5,074 passed, failure set unchanged vs the matched main
baseline apart from the three parquet-only tests CI covers. 10/11 new sabotages RED
against their NAMED test; the 11th is finding 9's leak, resolved above by moving
the test rather than by claiming the count. ruff + black clean; snakemake --lint OK.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
3 findings (2 major, 1 minor), down from r2's 15. All real.

1. MAJOR — `producer_entry_points_present` WAS VACUOUS. It imported the module that
   was already executing, so neither the import nor the hasattr could ever fail:
   the clause handed `all(clauses.values())` a hardcoded True. That is the exact
   failure mode this function's own docstring warns about, and the parametrized
   clause test never caught it because it iterates SUPPLY_CLAUSES and this clause
   was not in that tuple — a clause outside the tuple is a clause nothing breaks.
   Compare mine_round.derive_msa_supply_available, whose `producer_present` probes
   a DIFFERENT module, which is what makes it breakable.

   Replaced, not merely deleted, by the clause that derivation uses for the same
   job and which IS breakable: `producer_status_wired` measures that the round
   actually STAMPS a produced (b) status onto the candidate, by calling the wiring
   rather than reading a signature. A producer that ships but whose output the
   round drops is the silent no-op the gate exists to refuse. PRODUCER_ENTRY_POINTS
   removed; a test pins that the vacuous clause is not re-added.

2. MAJOR — validate_status_payload did not guard non-Mapping rows, though
   merge_status_tables does. `.get` on a string row raises AttributeError, which
   `main` does not catch, so the leg exits with a traceback instead of FATAL/exit 1.
   load_status_map reads tables written by other processes: row shape is an input,
   not an invariant. Same finding as r2's merge guard, one call site over --
   "you fixed one of two identical things", fourth time this step.

3. MINOR — `n_heldout_canonical_measured` re-tested `isinstance(n_measured, int)`,
   which accepts True, while the clause excluded bool. A report carrying `true`
   made the clause read False while the field reported `true`: two fields
   disagreeing inside one payload. Reuses the clause's verdict now.

Validation: 3/3 new sabotages RED against their NAMED test; the full campaign
re-run on this head is 19/19 + 10/11 (the documented leak whose test was relocated)
+ 3/3. unit+golden 5,080 passed, failure set unchanged vs the matched main baseline
apart from the three parquet-only tests CI covers. ruff + black clean.

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

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

🧹 Nitpick comments (1)
tests/unit/test_architecture_producer.py (1)

869-886: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse MSA_FILENAME instead of the hardcoded "msa.sto" literal.

test_the_msa_filename_is_imported_from_the_producer_that_writes_it on Lines 893-898 exists to prevent a second filename literal. These two tests then hardcode "msa.sto". If the producer renames the file, these tests keep constructing the old path and both candidates read unavailable for the wrong reason.

♻️ Proposed change
-        target = tmp_path / candidate_slug("c1") / "msa.sto"
+        target = tmp_path / candidate_slug("c1") / architecture_producer.MSA_FILENAME
-        bad = tmp_path / candidate_slug("bad") / "msa.sto"
+        bad = tmp_path / candidate_slug("bad") / architecture_producer.MSA_FILENAME
🤖 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_producer.py` around lines 869 - 886, Replace the
hardcoded "msa.sto" paths in the affected tests with the imported MSA_FILENAME
constant from the producer that writes the file. Update both target and bad path
construction while preserving the existing unreadable-file scenarios.
🤖 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.py`:
- Around line 104-112: Update the docstring of
test_duplicate_row_names_refuse_rather_than_collapsing to state that
read_pfam_alignment accumulates duplicate names into one concatenated row and
that names therefore contains one entry; remove the incorrect claim that names
retains both rows or that n_sequences is overstated. Leave the test logic and
inline comment unchanged.

---

Nitpick comments:
In `@tests/unit/test_architecture_producer.py`:
- Around line 869-886: Replace the hardcoded "msa.sto" paths in the affected
tests with the imported MSA_FILENAME constant from the producer that writes the
file. Update both target and bad path construction while preserving the existing
unreadable-file scenarios.
🪄 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: 4cb5a61e-66b9-4df6-876a-76fc8a0b9545

📥 Commits

Reviewing files that changed from the base of the PR and between 2745d78 and 0f39ced.

📒 Files selected for processing (9)
  • reports/p3/architecture_freeze.json
  • slurm/p2/mine_round_producer.sbatch
  • slurm/p3/architecture_producer.sbatch
  • src/tbox_finder/mining/architecture.py
  • src/tbox_finder/mining/architecture_producer.py
  • src/tbox_finder/mining/covariation_producer.py
  • src/tbox_finder/mining/mine_round.py
  • tests/unit/test_architecture.py
  • tests/unit/test_architecture_producer.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • reports/p3/architecture_freeze.json
  • slurm/p2/mine_round_producer.sbatch
  • slurm/p3/architecture_producer.sbatch
  • src/tbox_finder/mining/architecture_producer.py
  • src/tbox_finder/mining/architecture.py

Comment thread tests/unit/test_architecture.py
… were vacuous

4 findings, ALL MINOR — no majors for the first time this step (15 -> 3 -> 4, but
the r4 set is entirely non-critical). All real.

1. The freeze's PROVENANCE was unvalidated. It is evidence, so how it was produced
   is part of the claim, and `build_provenance` permits an empty `inputs` and a null
   env lock — the committed report had BOTH. Two fixes: `_provenance` now passes
   `env_lock=ENV_LOCK` (envs/data.conda-lock.yml; stage2_producer sets one,
   synteny_producer does not — followed the better precedent), and a new
   `freeze_provenance_names_its_inputs` clause. ⚠ The clause counts EITHER
   repo-relative `inputs` OR sha256'd `extra.external_inputs`: the corpus is
   DVC-tracked and absent from a worktree, so the freeze reads it by absolute path
   and the hashed form is what gets recorded — and that form is strictly MORE
   traceable. What must not pass is a report naming neither. Freeze re-derived; the
   measured numbers are unchanged.

2. `derive-supply` could exit with a traceback. The `candidate_evidence` probe is
   now wrapped fail-closed. ⚠ The test for that guard then failed from a DIFFERENT
   place: the delegation to `derive_msa_supply_available`, whose own probe is
   unwrapped, so a broken round took this function down THROUGH the delegation even
   with the local probe guarded. One of two call sites again — fifth time this step.
   Both are wrapped now.

3. MY OWN TEST WAS VACUOUS. `test_the_recovery_shares_reconcile_against_their_own
   _counts` iterates `ncca_recovery`; on an empty mapping the body never runs and the
   test passes having checked nothing — the exact species this file names elsewhere,
   in this file. Now asserts the arm set and the denominator are non-empty first.
   Sabotage-verified by emptying the DATA while KEEPING the guard (my first attempt
   removed the guard too, which is why it read as a leak — a malformed sabotage
   proves nothing).

4. A test name promised an exit code its body never exercised
   (`..._is_refused_with_exit_4` only checked the predicate). Renamed to name the
   predicate, with a pointer to the test that does assert 4; a failure would
   otherwise send an operator to the wrong gate.

⚠ AND ONE MORE OF MINE, CAUGHT BY SABOTAGE RATHER THAN BY REVIEW:
`test_the_committed_freeze_names_its_env_lock` read the COMMITTED report, so
dropping `env_lock=ENV_LOCK` from the writer left it GREEN — the bytes on disk were
written by the fixed code. An assertion over an artifact cannot see the producer
that wrote it. Split into a producer-side test that CALLS `_provenance` (red under
sabotage) plus the cheap artifact-side check.

Validation: 5/5 r4 sabotages RED against their NAMED test after both corrections.
unit+golden 5,085 passed; failure set unchanged vs the matched main baseline apart
from the three parquet-only tests CI covers. ruff + black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
The app reviewed eb0cd44 with SEVEN line comments; I had read only three (truncated
output, the same mistake as with the r2 CLI buffer). Reading all seven: four
converge with CLI findings already fixed (wobble direction, ncca_recovery scoping,
remine --relaxed-arch-status, config-less merge). Two gaps were NOT covered by the
CLI, which is exactly why both paths run:

1. MAJOR — a shard table with ZERO ROWS merged cleanly. This is the invisible loss:
   a dropped or truncated shard contributes nothing, every candidate it should have
   carried is simply absent from the merged table, and `mine_round` reads each absent
   candidate as `unavailable` => SPARED. Every count still reconciles against every
   other count, so nothing in the report looks wrong -- the round quietly decided
   less than it claims. Refused now.

2. MAJOR (extension of the r2 config fix) -- a config that is non-empty but
   INCOMPLETE still passed. REQUIRED_CONFIG_KEYS now names the six rule parameters
   explicitly (listed, not derived from whatever keys a payload carries: a
   required-key set read from its own evidence is vacuously satisfied exactly when
   the evidence is missing). Each key is broken ALONE by a parametrized test.

3. MINOR (on 0f39ced) -- a stale docstring of mine claimed `names` keeps both rows
   while the dict merges. It does not: `read_pfam_alignment` accumulates by name, so
   duplicates CONCATENATE. The inline comment two lines below already said so, and
   the docstring contradicted it. Corrected to describe what the test actually pins.

Validation: 2/2 new sabotages RED against their NAMED test. unit+golden green;
failure set unchanged vs the matched main baseline apart from the three parquet-only
tests CI covers. ruff + black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
…put"

2 findings, both minor. Same underlying principle as r2's merge guards: an on-disk
artifact written by another process has a shape that is an INPUT, not an invariant.

1. `len()` on a non-sized `provenance["inputs"]` raised TypeError out of a function
   documented as fail-closed on every clause. `main` does not catch TypeError, so
   `derive-supply` would exit with a traceback instead of the FATAL / exit 1 contract
   every other leg follows. The existing malformed-report test covered only the "not
   JSON" case. Type-guarded now.

2. `stem1_length` was type-checked with `isinstance(s1, (int, float))`. On an
   integer-dtype column `itertuples` yields `np.int64`, which is NOT a Python `int`,
   so every value would be silently DROPPED and the extent range would read as if the
   column were empty. Uses `numbers.Real` with bool excluded (bool IS a Real, which is
   exactly why it must be excluded explicitly).
   ⚠ LATENT, not active: the current corpus carries NaN so the column is float64,
   which is why the committed report has 8,578 values. Freeze re-derived and verified
   BYTE-IDENTICAL apart from provenance -- k=4 = 99.87%, modal bulge 7 nt, stem_i n =
   8,578 all unchanged.

Validation: 1/1 new sabotage RED against its NAMED test. unit+golden green; failure
set unchanged vs the matched main baseline apart from the three parquet-only tests CI
covers. ruff + black clean.

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

bioedca commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

@greptileai

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds the relaxed-architecture evidence backend and integrates its fail-closed status into mining and re-mining decisions.

  • Parses promoted Stockholm consensus alignments and localizes helices, bulges, and acceptor-pairing motifs.
  • Produces, validates, shards, and merges per-candidate architecture status tables.
  • Adds availability derivation, CLI gating, provenance checks, and MSA promotion for downstream scoring.

Confidence Score: 5/5

The PR appears safe to merge; no concrete changed-code defect remains after tracing the architecture evidence pipeline and its failure handling.

The new producer and consumers consistently fail closed, validate shard and merged artifacts, reject inconsistent availability declarations, and preserve the candidate-row and promoted-alignment invariants required by localization.

Important Files Changed

Filename Overview
src/tbox_finder/mining/architecture.py Adds validated Stockholm parsing, consensus decomposition, motif localization, and three-valued architecture-status evaluation.
src/tbox_finder/mining/architecture_producer.py Adds candidate evaluation, shard production, strict table merging and validation, freeze reporting, and supply derivation.
src/tbox_finder/mining/mine_round.py Integrates relaxed-architecture evidence and fail-closed availability checks into the mining round.
src/tbox_finder/mining/remine.py Adds paired availability controls and relaxed-architecture status handling to re-mining.
slurm/p2/mine_round_producer.sbatch Atomically promotes per-candidate consensus alignments while clearing stale outputs.
slurm/p3/architecture_producer.sbatch Adds parameter-required architecture shard execution and a serialized, validated merge workflow.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Candidate and homolog search] --> B[mlocarna Stockholm alignment]
  B --> C[Promoted msa.sto]
  C --> D[Architecture shard evaluation]
  D --> E[Validated merged status table]
  E --> F[Mining and re-mining evidence]
  F --> G{Relaxed architecture status}
  G -->|passed| H[Spare candidate]
  G -->|unavailable| H
  G -->|failed| I[Continue remaining mining-rule evaluation]
Loading

Reviews (1): Last reviewed commit: "fix(mining): CodeRabbit CLI r5 — two min..." | Re-trigger Greptile

Records the review history, the two truncated-buffer misreads that made me report a
round as handled when it was not, the CLI/app disagreement that justifies running both
paths, the four vacuous-or-wrong tests of my own (two caught by sabotage rather than
review), and the confirmation that the published freeze numbers survived re-derivation.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
4 findings (1 major, 3 minor), all acted on. Plus a correction to a claim I
propagated from r5 without checking it.

⚠ CORRECTION — r5's "Accept NumPy integer stem1_length values" WAS WRONG, and I
repeated its reasoning in a code comment and a commit message. The claim was that
`itertuples` yields np.int64 on an integer-dtype column, so `isinstance(s1, (int,
float))` silently dropped every value. MEASURED on this pandas: `itertuples` yields
a plain Python `int` for BOTH mixed- and homogeneous-dtype frames. The narrower
check was never dropping anything and the committed report was never wrong. The
`numbers.Real` change is KEPT — it is strictly more permissive, costs nothing, and
the numpy-scalar behaviour is genuinely version-dependent — but the comment now says
what is true rather than what the review asserted.

r6 findings:
1. MAJOR — the stem-I test exercised np.int64/Real semantics in ISOLATION, not the
   `freeze_report` loop. Now builds an integer-dtype parquet fixture and calls
   `freeze_report`, asserting n/min/max and that the record was genuinely measured.
2. MINOR — the discriminator needle was normalised `.upper()` but not T->U, while
   `degapped_span` maps T->U on the haystack. A DNA-spelled discriminator would fail
   the substring test, the record would be skipped, and the freeze's DENOMINATOR
   would shrink with nothing raising. (The review said the asymmetry was in CASE;
   it was in ALPHABET -- the helper already upper-cases. Right finding, wrong axis.)
3. MINOR — `supply_declaration_unevidenced(derivation=None)` had no test, so
   replacing `(derivation or {})` with `derivation.get(...)` would raise
   AttributeError with nothing turning red.
4. MINOR — `derive-supply` REPORTS, it does not gate; an honest `available: false`
   must still exit 0. Now pinned.

⚠ TWO OF MY r6 TESTS WERE THEMSELVES ISOLATION-ONLY and stayed GREEN under sabotage
of the code they claimed to cover -- the same anti-pattern this whole step is about,
caught by sabotage rather than by review. Both now go through `freeze_report`;
sabotaging the needle drops the carve from 1 to 0, verified by execution.

Validation: 3/3 CLI-runnable sabotages RED; the two parquet-path sabotages verified
by direct execution in tbox-finder-data (stem-I n 1->1 exposed the false premise
above; needle 1->0 confirms the real one). unit+golden 5,098 passed; failure set
unchanged vs the matched main baseline apart from the three parquet-only tests CI
covers. ruff + black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
2 findings. One real and important, one whose premise does not hold here.

1. MAJOR — the (b) merge checked each shard's internal consistency and refused an
   empty or duplicated shard, but NOTHING compared the merged result against the
   corpus it was meant to cover. A partial or substituted candidate set merged
   cleanly, every absent candidate classified `unavailable` => SPARED, and the round
   decided less than it reported while every count still reconciled. This is the
   corpus-level counterpart of r6's empty-shard refusal, and it is what
   stage2_producer's --min-coverage does for the (a) leg.

   `assert_covers_manifest` requires EXACT equality, not coverage >= a fraction: an
   EXTRA id is as wrong as a missing one, since it means the merge consumed a table
   from a different manifest. Wired as `merge --manifest`, made REQUIRED in
   slurm/p3/architecture_producer.sbatch's merge leg (before MERGE_OK), and recorded
   in the merged table's `coverage` block and provenance. A test pins that the sbatch
   actually passes it -- a guard the production leg does not invoke is worthless.

2. NOT TAKEN, premise checked and false — "CI excludes data/ through sparse checkout,
   so both (a)-evidence tests fail". CI uses a plain actions/checkout@v5 with no
   sparse checkout; all three evidence files are git-tracked and present; and CI
   pytest is GREEN on this branch, which includes the test asserting the (a) supply
   IS evidenced. Recorded rather than silently skipped.

Validation: 4/4 new sabotages RED against their NAMED test, including one that
strips --manifest from the sbatch. unit+golden green; failure set unchanged vs the
matched main baseline apart from the three parquet-only tests CI covers. ruff + black
clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
…isagree on an id

5 findings (1 major, 4 minor), all real.

1. MAJOR — `evaluate_candidate` accepted `candidate_id` OR `id`; `assert_covers_manifest`
   read `candidate_id` only. A manifest carrying just `id` produced status rows fine and
   then raised KeyError('candidate_id') at the coverage check, which `main` catches and
   reports as `FATAL: 'candidate_id'` -- naming no fault at all. One reader
   (`candidate_id_of`) now serves both, so they cannot drift.

2. MINOR — `stem_i_nt_threshold` was not validated positive. A non-positive value
   silently DISABLES the D6 short-Stem-I relaxation for every non-translational
   candidate (no extent is < 0), so candidates that depend on it resolve `failed` =>
   MINED. Fail-open; now refused with the other counts.

3. MINOR — the supply probe called `candidate_evidence` positionally. A parameter
   reorder would mis-bind, the broad `except` would swallow the TypeError, and the
   clause would fail closed for a reason unrelated to the wiring it measures. Bound by
   keyword now.

4. MINOR — the freeze conflated TWO different skips in one shrinking denominator. They
   mean different things: no usable curated discriminator is a CURATION gap; a
   discriminator in no flanked bulge is a LOCALIZER miss. Counted separately now, and
   the decomposition is informative: of 8,715 held-out records, 92 curation gaps + 18
   localizer misses + 8,605 measured (they reconcile exactly). So on records that HAVE a
   usable discriminator the localizer finds its bulge in 8,605/8,623 = 99.79%.

5. MINOR — my r7 sbatch guard did `split(marker)[1][:200]`, which raises IndexError when
   the leg is absent (reporting the wrong fault) and fails for the wrong reason if the
   invocation runs past 200 chars. Asserts the marker first and slices to the next
   command boundary.

Freeze re-derived: k=4 = 99.87%, modal bulge 7 nt, ncca_recovery unchanged; only the new
carve counters are added.

Validation: 4/4 CLI-runnable sabotages RED against their NAMED test (incl. renaming the
sbatch merge marker); the parquet-path skip-counter test verified by direct execution in
tbox-finder-data (1 counted / 1 curation gap / 1 localizer miss). unit+golden 5,108
passed; failure set unchanged vs the matched main baseline apart from the three
parquet-only tests CI covers. ruff + black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
…MINABLE

4 findings (1 major, 3 minor), all real.

1. MAJOR — `ncca_bulge_status` refused a bulge_size_range whose MAXIMUM could not hold
   the motif, but not one whose MINIMUM could not. A bulge of low..want-1 residues
   PASSES the size filter, is therefore "considered", cannot possibly hold the motif,
   and so reads BULGE_ABSENT -- a decided negative => MINABLE -- instead of
   BULGE_UNDETECTABLE => spared. Fail-open, and reachable because nothing is defaulted.
   Guarding `low` subsumes the old check since `high >= low` is already enforced.

2. MINOR — `validate_status_payload`'s `rederived` is a dict comprehension, so two rows
   with the SAME candidate_id collapse to the last; if they carry the same status,
   `rederived` still equals `declared` and the table passes. `merge_status_tables`
   refuses duplicates but `load_status_map` reads tables written by other processes and
   reaches this function without that guard. ("A duplicate key merges instead of
   colliding" -- the same species as the contig-id case already in memory.)

3. MINOR — the module docstring pointed at `named_elements_present`, which is a
   parameter and a dataclass field but not a function. The function is
   `named_elements_status`.

4. MINOR — my own r8 sbatch guard sliced to the first BLANK LINE, which is not a shell
   command boundary: a `--manifest` belonging to a FOLLOWING command would satisfy both
   assertions, so a merge leg that dropped the manifest could still read green. Now
   consumes the marker line plus its backslash continuations only, with a test that
   builds exactly that adversarial case.

Validation: 3/3 CLI-runnable sabotages RED against their NAMED test. unit+golden 5,114
passed; failure set unchanged vs the matched main baseline apart from the three
parquet-only tests CI covers. ruff + black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
… number

4 findings (1 major, 3 minor), all real.

1. MAJOR — `min_sequences` was the ONE outcome-deciding number in the predicate module
   that was not range-checked (`min_pairs`, `min_named_helices`, `ncca_pairing_nt` and
   `bulge_size_range` all were). A floor below 1 makes `n_sequences < min_sequences`
   always false, silently DISABLING the ADR-0006 A2 Pin 2 depth check, so a candidate
   reaches criterion_b on an alignment of any depth -- fail-open. Refused now, and the
   applied floor is recorded on every arm of the returned detail so provenance shows
   WHICH floor was used rather than merely that one was.

2. MINOR — `freeze` did not expose `--max-ncca-pairing-nt`, so the report always covered
   arms 1..4 while `run-shard` accepts any `--ncca-pairing-nt`. A round with a wider
   motif would not have its actual arm measured. Exposed, defaulting to the acceptor
   motif's width rather than a literal 4.

3. MINOR — three tests imported pandas directly while their siblings used
   `importorskip`, so they ERRORED rather than skipped without a parquet engine. ⚠ This
   also clears the three long-standing local "regressions": the branch's failure set is
   now IDENTICAL to the matched main baseline, with the parquet tests skipping cleanly.

4. MINOR — two tests encoded conflicting contracts: one asserted the real checkout
   always evidences the supply, the other only that the derivation and the constant
   agree. A deliberate "unavailable on this machine" declaration would have failed the
   first for a reason that is not a defect. The first is now gated on the constant.

⚠ AND ONE MORE OF MY TESTS WAS TOO WEAK, caught by sabotage: the arm-width test checked
that the parser EXPOSES the option, so hardcoding the `main` call to 4 stayed green. It
now drives the CLI end to end and reads the written report — verified by execution:
sabotaged it emits arms 1..4 where 1..2 were requested.

Validation: 3/3 CLI-runnable sabotages RED plus the CLI-path one verified by execution.
unit+golden 5,119 passed with ZERO regressions vs the matched main baseline. ruff +
black clean.

Claude-Session: https://claude.ai/code/session_017VRtJL7TgZioPxpCUuENmM
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