Skip to content

P3-01: the Stage-2 supervised dataset + the pinned RiNALMo tokenizer adapter - #93

Merged
bioedca merged 3 commits into
mainfrom
p3-01-stage2-dataset
Jul 31, 2026
Merged

bioedca merged 3 commits into
mainfrom
p3-01-stage2-dataset

Conversation

@bioedca

@bioedca bioedca commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Assembles data/processed/stage2_dataset.parquet (DVC) — 30,542 rows × 34 columns: the 23,535 T-box positives plus the 7,007 unmasked §9.1 static decoys, as RNA (T→U transcribed), each carrying its binary label, per-nucleotide boundary target, §8 aux targets (regulatory mode / specifier codon / cognate AA / tRNA family), a dot-bracket pairing target for the P3-05 structure-consistency loss, and the ADR-0004 fold + parentage columns.

PRD §6, §8, §10.2, §11 · ADR ADR-0004 (D4/D5/D7), ADR-0002 (A4/A8/A9) · Compute LOCAL, ~8 s, no GPU, no network.

The three load-bearing choices

1. flank_nt = 0 for both classes, refused otherwise. PRD §6 hands Stage 2 a "locus ± flank", but the named P3-01 inputs supply real genomic flank for positives only (P2-00's context_v0); the four decoy pools are generated, Rfam-sourced or shuffled and have none. A positives-only flank makes "is embedded in real genomic context" a perfectly separable shortcut for the binary head — the §5 circularity failure the project exists to avoid. flank_nt is a pinned parameter recorded per row and in provenance; a non-zero value raises until a matching flank exists for every pool. Structure appears only as a target column, never a Stage-2 input (PRD §6).

2. Folds inherited, never invented. Every row records fold_basis:

  • corpus_record (23,535) — a positive copies every scheme column from its own split-table row;
  • parent_record (2,000) — a dinucleotide-shuffled decoy inherits its parent's fold across every scheme (ADR-0004 D7's variant→parent→fold rule on the negative side), refused fail-closed if the parent is absent;
  • decoy_pool_random (5,007) — decoys with no corpus parent (generated GC background, Rfam structured RNAs, tboxevo leader decoys) belong to no cluster and no clade, so no leave-clade-out holdout unit can contain them. They are handled the way ADR-0004 D4 handles taxonomy-incomplete positives: kept only in the random split, every clade-scheme column null, clade_holdout_eligible False, fold_random from a deterministic order-independent keyed hash. nested_train is left null — whether such a decoy may enter the nested training fold is a P3-03 sampling policy, and True here would be a policy decision disguised as a data field.

⚠️ (2) is a reading of ADR-0004 D4/D7, not a pinned decision. P3-02's calibration carve inherits it, so it is flagged for sign-off at the P3-exit gate rather than self-certified here.

3. The pairing target is projected and measured, never assumed. TBDB's Structure (WUSS) is aligned to Sequence, the gapped cmalign row — not to the genomic FASTA_sequence locus (median 238 vs 281 nt; co-extensive with each other on 99.996 % of records but with the locus on 0.5 %). The builder validates the alignment row, strips gaps, and requires the gap-stripped sequence to occur exactly once in the locus before mapping pairs through that offset. Measured over the full corpus: 18,269 / 23,535 (77.62 %) anchor, with zero no-hit and zero multi-hit rows — every clean-alphabet row anchors uniquely, an internal consistency check rather than a tuned threshold. The other 5,266 render unaligned inserts as ~ / [n] / * with the nucleotides elided (after sanitising the junk glyphs, 4,590 of 5,259 still have no locus match — measured), so they carry pairing_status = "unanchorable_alignment" and a null target. Nothing approximate or re-folded is substituted (CLAUDE.md §10.3).

A first implementation rejected any record whose consensus structure paired into a column this member had deleted — discarding 8,634 further records. Measurement showed the rule was wrong: Structure is a consensus line and only 20,854 of 850,029 pairs (2.5 %) have a deleted partner (per-row median 0.000, max 0.426). Such a pair is not formed in this sequence, so it is now dropped and counted (n_pairs_dropped_gap) — never re-based onto a neighbouring nucleotide, never grounds for discarding the row's other 97.5 % of real pairs.

The tokenizer adapter

src/tbox_finder/stage2/tokenizer.py pins RiNALMo's 28-token alphabet in pure Python so the build runs in envs/data.yml alongside every other data-layer rule (multimolecule lives only in envs/ml-rna.yml per the ADR-0002 A4 env split and pulls deepspeed at import), and so the golden test stays runnable in CI. The 1022-nucleotide bound is derivedRiNALMoConfig.max_position_embeddings 1024 minus <cls>/<eos> — not copied from prose. Behaviour was read off the live tokenizer: upper-case, TU, the 15 IUPAC codes plus - . * | ? X I N in-vocabulary, everything else <unk> rather than dropped, so token length always equals sequence length and the per-nucleotide target stays index-aligned.

Two guards fail in different environments, so neither is vacuous: the digest/id tests are stdlib-only (bare CI), and test_pinned_vocab_matches_live_tokenizer asserts the pin equals the live RnaTokenizer at the ADR-0002 A9 revision 2a71f6f9… in tbox-ml-rna.

Validation — PASS

  • 54 passed / 2 skipped in tbox-data; the 2 skips are the multimolecule parity legs, which run 23 passed / 0 skipped in tbox-ml-rna.
  • Round-trip decode(encode(s)) == s exact on all 100 real T-box loci of the committed ingest slice and on a hand-authored alphabet fixture.
  • Every row carries a non-null fold_random, parent_record_id and a closed-set fold_basis; longest row 550 nt / 552 tokens ≤ 1022.
  • ruff check + black --check clean; snakemake --lint "workflow is in a good condition"; snakemake -n parses.
  • No pytest exists in any local env, so both suites were executed under a stdlib pytest shim in tbox-data and tbox-ml-rna; CI runs the real pytest.

Seven sabotage bites, __pycache__ cleared each time and every restore proved by cmp (these files are new, so git checkout -- would be a silent no-op): wrong split row for a derived decoy → test_derived_decoy_inherits_every_scheme_value_from_its_parent (+ golden); gap-partner pair re-based instead of dropped → test_projection_drops_a_pair_whose_partner_is_a_deleted_column (+ golden); ambiguous anchor takes the first hit → test_projection_refuses_an_ambiguous_anchor; invented clade units on a parentless decoy → test_parentless_decoys_are_random_only_and_invent_no_clade (+ golden); a vocabulary id edited → test_vocab_digest_is_pinned (+3); the T→U transcription removed → 14 tests incl. the golden. The seventh edited a vocabulary id and recomputed the pinned digest so every CI-side guard passes — caught only by the live-parity test, proving that leg bites.

Notes for review

  • The golden fixture runs with require_full_join=False: 6 of the 100 ingest-fixture records predate the corpus de-duplication and have no split row. The refused counts (6 records, and the 4 dinucleotide decoys parented on them) are asserted, so the relaxation is pinned rather than silent — and it makes ADR-0004 D7's fail-closed rule fire on real data inside the golden.
  • The imp.md gate names "the 9-PDB + hand-checked fixture" for the round-trip, but those P0-21 fixtures record element extents and window lengths, not sequences. They are used for what they can assert (the token axis stays index-aligned with the per-nucleotide label axis at exactly those 11 window lengths); the round-trip itself runs over real T-box loci.
  • README.md / paper/manuscript.qmd are untouched: this step lands no analysis result, and the repo batches both at the phase-exit gate (the P2 precedent).
  • dvc push is deferred to the phase-exit gate (CLAUDE.md §5.2).

https://claude.ai/code/session_01Tr67opmzHJAGBJPAb2aTdX

Summary by CodeRabbit

  • New Features

    • Added Stage 2 supervised dataset generation with positive and decoy examples, fold assignments, validation, provenance, and audit reports.
    • Added RNA sequence tokenization with DNA-to-RNA transcription, encoding/decoding, vocabulary validation, and context-length checks.
    • Added an automated workflow for producing the Stage 2 dataset and related artifacts.
  • Tests

    • Added comprehensive unit and golden regression coverage for dataset assembly, structure handling, tokenization, validation, and reproducibility.

…o tokenizer adapter (P3-01)

Emits data/processed/stage2_dataset.parquet (DVC): 30,542 rows x 34 columns —
23,535 T-box positives + 7,007 unmasked §9.1 decoys — as RNA (T->U), each with
its binary label, per-nucleotide boundary target, §8 aux targets, a dot-bracket
pairing target for the P3-05 structure-consistency loss, and the ADR-0004
fold/parentage columns.

Inputs (LOCAL, ~8 s, no GPU/network): master_clean_v0.parquet, labels_v0.parquet,
the git-LFS split_assignments.parquet, decoys_v0.parquet.

Three load-bearing choices, all stated in the module docstring:

* flank_nt = 0 for BOTH classes and refused otherwise. The named inputs supply
  real genomic flank for positives only; a positives-only flank makes "sits in
  real genomic context" a separable shortcut for the binary head (PRD §5).
* Folds inherited, never invented (fold_basis). A positive copies its own split
  row; a dinucleotide decoy inherits its parent's fold across every scheme
  (ADR-0004 D7, fail-closed on a missing parent); the 5,007 parentless decoys
  belong to no cluster or clade and are kept random-split-only with null clade
  columns and a null nested_train — the ADR-0004 D4 pattern extended to a class
  D4 does not name, flagged for sign-off at the P3-exit gate.
* The pairing target is projected and measured. TBDB Structure is aligned to the
  gapped cmalign row, not the locus; requiring an exact UNIQUE anchor gives
  18,269/23,535 (77.62 %) with zero no-hit and zero multi-hit rows. The rest
  render unaligned inserts lossily and carry a null target, never a substitute.
  Structure appears only as a target, never a Stage-2 input (PRD §6).

Rejecting rows whose consensus structure pairs into a deleted column was
measured wrong (it discarded 8,634 more records for 2.5 % of pairs); such pairs
are now dropped and counted in n_pairs_dropped_gap.

The tokenizer adapter pins RiNALMo's 28-token alphabet in pure Python so the
build runs in envs/data.yml (multimolecule is ml-rna-only, ADR-0002 A4/A8) and
the golden stays CI-runnable; 1022 is derived from max_position_embeddings 1024
minus <cls>/<eos>, and the pin is asserted equal to the live RnaTokenizer at the
ADR-0002 A9 revision 2a71f6f9.

Validation: PASS. 54 passed / 2 skipped in tbox-data; the 2 skips are the parity
legs, 23 passed / 0 skipped in tbox-ml-rna. Round-trip exact on 100 real T-box
loci + the alphabet fixture; every row has a fold, a parent and a closed-set
fold_basis; max 552 tokens. ruff + black clean; snakemake --lint/-n clean.
Seven sabotage bites, each caught by its named test, restores proved by cmp.
Copilot AI review requested due to automatic review settings July 31, 2026 23:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@coderabbitai

coderabbitai Bot commented Jul 31, 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: 45 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b060f760-f412-40b1-8970-d0c3e904a670

📥 Commits

Reviewing files that changed from the base of the PR and between 51e4669 and cd5209b.

⛔ Files ignored due to path filters (2)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
  • data/processed/stage2_dataset.provenance.json is excluded by !data/**
📒 Files selected for processing (4)
  • src/tbox_finder/stage2/dataset.py
  • src/tbox_finder/stage2/tokenizer.py
  • tests/unit/test_stage2_dataset.py
  • tests/unit/test_stage2_tokenizer.py
📝 Walkthrough

Walkthrough

Stage 2 adds a pinned RiNALMo tokenizer, WUSS structure projection, supervised positive and decoy dataset assembly, invariant checks, digest generation, CLI outputs, Snakemake integration, and unit and golden regression tests.

Changes

Stage 2 dataset pipeline

Layer / File(s) Summary
Pinned RNA tokenizer adapter
src/tbox_finder/stage2/tokenizer.py, tests/unit/test_stage2_tokenizer.py
Adds the pinned vocabulary, DNA-to-RNA transcription, aligned encoding and decoding, context validation, lazy reference-tokenizer loading, batch encoding, and parity tests.
Structure projection and target construction
src/tbox_finder/stage2/dataset.py, tests/unit/test_stage2_dataset.py
Adds WUSS and dot-bracket parsing, aligned structure projection, rejection reasons, and tests for malformed, ambiguous, missing, and gapped structures.
Positive and decoy dataset assembly
src/tbox_finder/stage2/dataset.py, tests/unit/test_stage2_dataset.py
Builds labeled rows, applies fold inheritance or deterministic decoy routing, validates joins and invariants, and tests reports and digests.
Build outputs and workflow integration
src/tbox_finder/stage2/__init__.py, src/tbox_finder/stage2/dataset.py, workflow/rules/stage2.smk, tests/golden/*, tests/fixtures/stage2_dataset/*
Adds CLI and Snakemake outputs for Parquet, provenance, and audit reports. Golden tests verify fixture integrity and the expected dataset digest.

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

Sequence Diagram(s)

sequenceDiagram
  participant Inputs
  participant build_dataset
  participant tokenizer
  participant DatasetFrame
  participant ParquetOutputs
  Inputs->>build_dataset: corpus, labels, splits, and decoys
  build_dataset->>tokenizer: validate and encode RNA loci
  tokenizer-->>build_dataset: token lengths and token IDs
  build_dataset->>DatasetFrame: assemble positive and decoy rows
  DatasetFrame-->>build_dataset: validated dataset and report
  build_dataset->>ParquetOutputs: write Parquet, provenance, and audit JSON
Loading

Suggested reviewers: copilot

🚥 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 two primary changes: the Stage-2 supervised dataset and the pinned RiNALMo tokenizer adapter.
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-01-stage2-dataset

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

🤖 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/stage2/dataset.py`:
- Line 498: Update the parent_record_id assignment in the dataset row
construction to use the existing _text helper instead of str, ensuring missing
parent values remain null and _assert_dataset_invariants can reject them through
its parent_record_id gate.
- Around line 336-343: Update the anchor-matching logic around probe and locus
in the stage2 dataset function to reject an empty probe as REJECT_NO_ANCHOR, and
detect all overlapping probe occurrences rather than using str.count. Return
REJECT_NO_ANCHOR for no matches, REJECT_MULTI_ANCHOR for multiple matches
including overlaps, and retain the unique match offset; add regression tests
covering an empty probe and overlapping matches.

In `@workflow/rules/stage2.smk`:
- Around line 28-60: Update the stage2_dataset rule to declare corpus, labels,
split_table, and decoys as named input dependencies using their existing config
defaults, remove those source declarations from params, and pass them to the
command via the corresponding {input.<name>:q} references. Leave output,
provenance, directory, and other parameter handling unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a1038a92-0adc-4d68-b132-953e748fdeb9

📥 Commits

Reviewing files that changed from the base of the PR and between 30248f0 and 51e4669.

⛔ Files ignored due to path filters (5)
  • analyses/phase3_log.qmd is excluded by !**/*.qmd
  • data/processed/.gitignore is excluded by !data/**
  • data/processed/audits/stage2_dataset_report.json is excluded by !data/**
  • data/processed/stage2_dataset.parquet.dvc is excluded by !data/**
  • data/processed/stage2_dataset.provenance.json is excluded by !data/**
📒 Files selected for processing (8)
  • src/tbox_finder/stage2/__init__.py
  • src/tbox_finder/stage2/dataset.py
  • src/tbox_finder/stage2/tokenizer.py
  • tests/fixtures/stage2_dataset/expected.sha256
  • tests/golden/test_stage2_dataset_golden.py
  • tests/unit/test_stage2_dataset.py
  • tests/unit/test_stage2_tokenizer.py
  • workflow/rules/stage2.smk

Comment thread src/tbox_finder/stage2/dataset.py Outdated
Comment on lines +336 to +343
probe = "".join(ungapped_chars).replace("U", "T")
locus = str(locus_dna).upper()
hits = locus.count(probe)
if hits == 0:
return None, REJECT_NO_ANCHOR, 0
if hits > 1:
return None, REJECT_MULTI_ANCHOR, 0
offset = locus.find(probe)

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
locus, probe = "ATATA", "ATA"
print("count:", locus.count(probe))
i, n = locus.find(probe), 0
while i != -1:
    n += 1
    i = locus.find(probe, i + 1)
print("overlapping occurrences:", n)
PY

Repository: bioedca/tbox-finder

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target function ---'
sed -n '260,365p' src/tbox_finder/stage2/dataset.py
printf '%s\n' '--- related constants and tests ---'
rg -n -C 3 'REJECT_(NO_ANCHOR|MULTI_ANCHOR)|project_structure_to_locus|all-gap|aligned_sequence|aligned_structure' src tests
printf '%s\n' '--- deterministic probe ---'
python3 - <<'PY'
cases = [
    ("ATATA", "ATA"),
    ("AAAA", "AA"),
    ("ATGC", ""),
]
for locus, probe in cases:
    positions = []
    pos = locus.find(probe)
    step = 1 if probe else 1
    while pos != -1:
        positions.append(pos)
        pos = locus.find(probe, pos + step)
    print({
        "locus": locus,
        "probe": probe,
        "str_count": locus.count(probe),
        "overlap_aware_positions": positions,
        "find": locus.find(probe),
    })
PY

Repository: bioedca/tbox-finder

Length of output: 12886


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target function ---'
sed -n '260,365p' src/tbox_finder/stage2/dataset.py
printf '%s\n' '--- related constants and tests ---'
rg -n -C 3 'REJECT_(NO_ANCHOR|MULTI_ANCHOR)|project_structure_to_locus|all-gap|aligned_sequence|aligned_structure' src tests
printf '%s\n' '--- deterministic probe ---'
python3 - <<'PY'
cases = [
    ("ATATA", "ATA"),
    ("AAAA", "AA"),
    ("ATGC", ""),
]
for locus, probe in cases:
    positions = []
    pos = locus.find(probe)
    while pos != -1:
        positions.append(pos)
        pos = locus.find(probe, pos + 1)
    print({
        "locus": locus,
        "probe": probe,
        "str_count": locus.count(probe),
        "overlap_aware_positions": positions,
        "find": locus.find(probe),
    })
PY

Repository: bioedca/tbox-finder

Length of output: 12886


Use an overlap-aware anchor check.

str.count misses overlapping matches. For example, locus="ATATA" and probe="ATA" produce one count but two matches. The function then places the structure at the first offset. Reject overlapping matches as REJECT_MULTI_ANCHOR. Return REJECT_NO_ANCHOR when probe is empty. Add regression tests for both cases.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 342-342: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: locus.find(probe)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').

(xpath-injection-python)

🤖 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/stage2/dataset.py` around lines 336 - 343, Update the
anchor-matching logic around probe and locus in the stage2 dataset function to
reject an empty probe as REJECT_NO_ANCHOR, and detect all overlapping probe
occurrences rather than using str.count. Return REJECT_NO_ANCHOR for no matches,
REJECT_MULTI_ANCHOR for multiple matches including overlaps, and retain the
unique match offset; add regression tests covering an empty probe and
overlapping matches.

Comment thread src/tbox_finder/stage2/dataset.py Outdated
Comment thread workflow/rules/stage2.smk
Comment on lines +28 to +60
rule stage2_dataset:
"""Assemble `data/processed/stage2_dataset.parquet` (P3-01; PRD §6/§8/§10.2/§11)."""
output:
dataset=f"{_STAGE2_PROCESSED_DIR}/stage2_dataset.parquet",
provenance=f"{_STAGE2_PROCESSED_DIR}/stage2_dataset.provenance.json",
report=f"{_STAGE2_AUDIT_DIR}/stage2_dataset_report.json",
params:
# inputs + dirs derived from the outputs (not hardcoded prefixes) so
# `snakemake --lint` stays clean; the module writes both sidecars itself.
corpus=config.get("stage2_corpus", "data/processed/master_clean_v0.parquet"),
labels=config.get("stage2_labels", "data/processed/labels/labels_v0.parquet"),
split_table=config.get(
"stage2_split_table", "data/processed/splits/split_assignments.parquet"
),
decoys=config.get("stage2_decoys", "data/processed/negatives/decoys_v0.parquet"),
out_dir=lambda wildcards, output: os.path.dirname(output.dataset),
audit_dir=lambda wildcards, output: os.path.dirname(output.report),
flank_nt=config.get("stage2_flank_nt", 0),
env_lock="envs/data.conda-lock.yml",
log:
"logs/stage2_dataset.log",
conda:
"../../envs/data.yml"
shell:
"python -m tbox_finder.stage2.dataset "
"--corpus {params.corpus:q} "
"--labels {params.labels:q} "
"--split-table {params.split_table:q} "
"--decoys {params.decoys:q} "
"--out-dir {params.out_dir:q} "
"--audit-dir {params.audit_dir:q} "
"--flank-nt {params.flank_nt} "
"--env-lock {params.env_lock:q} >{log} 2>&1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '28,60p' workflow/rules/stage2.smk
rg -n -C 2 --glob '*.yml' --glob '*.yaml' \
  'stage2_(corpus|labels|split_table|decoys)' .

Repository: bioedca/tbox-finder

Length of output: 1781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tracked files ---'
git ls-files | sed -n '1,240p'

printf '%s\n' '--- source path references ---'
rg -n -C 3 \
  'master_clean_v0\.parquet|labels_v0\.parquet|split_assignments\.parquet|decoys_v0\.parquet|stage2_dataset|stage2_(corpus|labels|split_table|decoys)' \
  --glob '!*.lock' --glob '!*.jsonl' .

printf '%s\n' '--- workflow rule declarations and input sections ---'
rg -n -C 4 \
  '^(rule|checkpoint|use rule)|^[[:space:]]+input:|^[[:space:]]+output:' \
  workflow Snakefile* 2>/dev/null || true

printf '%s\n' '--- tracking metadata ---'
find . -maxdepth 4 -type f \( -name '*.dvc' -o -name '.gitattributes' -o -name 'dvc.yaml' -o -name 'dvc.lock' \) -print \
  -exec sh -c 'for f do echo "--- $f"; sed -n "1,180p" "$f"; done' sh {} +

Repository: bioedca/tbox-finder

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow files ---'
git ls-files workflow | sed -n '1,160p'

printf '%s\n' '--- config and tracking files ---'
git ls-files | rg '(^|/)([^/]+/)*(config|conf|envs|data)(/|$)|\.dvc$|(^|/)\.gitattributes$|(^|/)dvc\.(yaml|lock)$' | sed -n '1,240p'

printf '%s\n' '--- relevant workflow references ---'
rg -n -C 5 \
  'master_clean_v0\.parquet|labels_v0\.parquet|split_assignments\.parquet|decoys_v0\.parquet|stage2_dataset|stage2_(corpus|labels|split_table|decoys)' \
  workflow conf config data Snakefile 2>/dev/null || true

printf '%s\n' '--- tracking metadata files ---'
git ls-files | rg '(^|/)([^/]+/)*(\.gitattributes|[^/]+\.dvc|dvc\.yaml|dvc\.lock)$' |
while IFS= read -r f; do
  echo "--- $f"
  sed -n '1,180p' "$f"
done

printf '%s\n' '--- stage2-related source files ---'
git ls-files | rg 'stage2|dataset' | sed -n '1,160p'

Repository: bioedca/tbox-finder

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- stage2 rule and upstream output ranges ---'
sed -n '1,75p' workflow/rules/stage2.smk
sed -n '65,100p;325,360p;700,745p;530,565p' workflow/rules/data.smk

printf '%s\n' '--- workflow inclusion order ---'
sed -n '1,180p' workflow/Snakefile

printf '%s\n' '--- git attributes and DVC metadata ---'
sed -n '1,160p' .gitattributes
for f in \
  data/processed/master_clean_v0.parquet.dvc \
  data/processed/labels/labels_v0.parquet.dvc \
  data/processed/negatives/decoys_v0.parquet.dvc \
  data/interim/splits/split_assignments.parquet.dvc
do
  echo "--- $f"
  sed -n '1,40p' "$f"
done

printf '%s\n' '--- read-only structural verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

stage2 = Path("workflow/rules/stage2.smk").read_text()
data = Path("workflow/rules/data.smk").read_text()

sources = {
    "corpus": "data/processed/master_clean_v0.parquet",
    "labels": "data/processed/labels/labels_v0.parquet",
    "split_table": "data/processed/splits/split_assignments.parquet",
    "decoys": "data/processed/negatives/decoys_v0.parquet",
}

print("stage2 input section:", bool(re.search(r"rule stage2_dataset:.*?\n    input:", stage2, re.S)))
for name, path in sources.items():
    print(f"{name}: param={bool(re.search(rf'^[ \t]+{name}=config\\.get', stage2, re.M))}, "
          f"source_path_present={path in stage2}, "
          f"input_ref={bool(re.search(rf'\\{{input\\.{name}:q\\}}', stage2))}")

for path in sources.values():
    print(f"data.smk output for {path}:",
          bool(re.search(rf'^[ \t]+(?:processed|labels|table|decoys)?=?[\"\\']?{re.escape(path)}[\"\\']?', data, re.M))
          or path in data)

for dvc in (
    "data/processed/master_clean_v0.parquet.dvc",
    "data/processed/labels/labels_v0.parquet.dvc",
    "data/processed/negatives/decoys_v0.parquet.dvc",
    "data/interim/splits/split_assignments.parquet.dvc",
):
    print(f"{dvc}: exists={Path(dvc).is_file()}")
PY

Repository: bioedca/tbox-finder

Length of output: 14180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

stage2_text = Path("workflow/rules/stage2.smk").read_text()
data_text = Path("workflow/rules/data.smk").read_text()
attributes = Path(".gitattributes").read_text()

start = stage2_text.index("rule stage2_dataset:")
rule_text = stage2_text[start:]

sources = {
    "corpus": "data/processed/master_clean_v0.parquet",
    "labels": "data/processed/labels/labels_v0.parquet",
    "split_table": "data/processed/splits/split_assignments.parquet",
    "decoys": "data/processed/negatives/decoys_v0.parquet",
}

print("stage2 has input section:",
      bool(re.search(r"^[ \t]+input:", rule_text, re.M)))

for name, path in sources.items():
    param_pattern = rf"^[ \t]+{re.escape(name)}=config\.get"
    param_ref = "{params." + name + ":q}"
    input_ref = "{input." + name + ":q}"
    print(
        name,
        "param_declaration=", bool(re.search(param_pattern, rule_text, re.M)),
        "params_shell_ref=", param_ref in rule_text,
        "input_shell_ref=", input_ref in rule_text,
        "source_path_in_rule=", path in rule_text,
    )

upstream_markers = {
    "corpus": 'processed="data/processed/master_clean_v0.parquet"',
    "labels": 'labels=f"{_LABELS_DIR}/labels_v0.parquet"',
    "split_table": 'table=f"{_PROCESSED_SPLITS_DIR}/split_assignments.parquet"',
    "decoys": 'decoys=f"{_NEG_DIR}/decoys_v0.parquet"',
}
for name, marker in upstream_markers.items():
    print("matching upstream output", name, "=", marker in data_text)

for path in (
    "data/processed/master_clean_v0.parquet.dvc",
    "data/processed/labels/labels_v0.parquet.dvc",
    "data/processed/negatives/decoys_v0.parquet.dvc",
):
    print("DVC sidecar", path, "=", Path(path).is_file())

print(
    "split table Git-LFS attribute =",
    "data/processed/splits/split_assignments.parquet filter=lfs" in attributes,
)
print(
    "stage2 provenance records source hashes =",
    all(path in Path("data/processed/stage2_dataset.provenance.json").read_text()
        for path in sources.values()),
)
PY

Repository: bioedca/tbox-finder

Length of output: 1042


Declare the source files as input dependencies.

stage2_dataset currently tracks the four DVC/Git-LFS sources only as params, so Snakemake cannot detect updated content or connect the matching upstream rules. Move corpus, labels, split_table, and decoys into named input entries and pass them through {input.<name>:q}. The provenance hashes do not affect Snakemake scheduling.

🤖 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 `@workflow/rules/stage2.smk` around lines 28 - 60, Update the stage2_dataset
rule to declare corpus, labels, split_table, and decoys as named input
dependencies using their existing config defaults, remove those source
declarations from params, and pass them to the command via the corresponding
{input.<name>:q} references. Leave output, provenance, directory, and other
parameter handling unchanged.

bioedcam added 2 commits July 31, 2026 18:12
…stringified parent link

2 of 3 findings applied, 1 skipped with a tested reason.

* (major, applied) The pairing-target uniqueness check used `str.count`, which
  counts NON-overlapping occurrences ("AAA".count("AA") == 1), so an ambiguous
  probe could be reported unique and the structure silently placed at the first
  offset — the exact mis-placement the rule exists to prevent. Replaced with an
  overlap-aware scan. Re-measured over the full corpus: 0 of 23,535 records
  differ between the two counts, anchored stays 18,269, and the artifact digest
  is byte-identical — a latent hole closed, not a number moved.
* (minor, applied) parent_record_id was built with str(cell), which turns a null
  into the present-looking "None" (pandas 2) / "nan" (pandas 3) and would sail
  past the null gate as a fabricated parent link; now `_text`.
* (major, skipped with reason) Declaring the four source artifacts as Snakemake
  `input:` was tested, not argued: it fails `snakemake --lint` ("Param ... is a
  prefix of input or output file but hardcoded", the CI-blocking rule) and makes
  `snakemake -n stage2_dataset` schedule a 6-job rebuild ending in
  split_assignment_table — regenerating the committed ADR-0004 split table the
  no-leakage gate is anchored on. DVC/git-LFS inputs stay out of the DAG, the
  documented house convention (data.smk::derive_labels). Restored byte-identical.

Both fixes carry a new test and were sabotage-verified: reverting the overlap
scan fails only test_projection_refuses_an_OVERLAPPING_ambiguous_anchor;
reverting `_text` fails only test_a_null_parent_link_is_refused_not_stringified.

55 passed / 2 skipped in tbox-data (the 2 skips are the parity legs, 23 passed in
tbox-ml-rna); ruff + black clean; snakemake --lint clean and `-n stage2_dataset`
reports "Nothing to be done".
…he r1 comment

Both minor findings applied.

* `decode`/`id_to_token` cast with `int()`, and `int(6.9) == 6`, so a corrupted
  float id would silently decode as the nucleotide at 6 — the opposite of the
  docstring's "an id outside the vocabulary is an error, not a silent <unk>".
  Now `operator.index`, which still accepts the NumPy integers a parquet
  round-trip yields (asserted in the test) and refuses floats, strings and None.
* Prose: the r1 comment said "scan every start position" while the code does two
  `find`s; corrected to describe the second search from `offset + 1`. No code
  change (the reviewer asked for the comment only).

New test test_decode_refuses_a_non_integer_id, sabotage-verified: reverting
`operator.index` to `int()` fails that test and nothing else.

57 passed / 2 skipped in tbox-data; 24 passed / 0 skipped in tbox-ml-rna;
ruff + black clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants