Skip to content

feat: make GEPA --optimize work end-to-end with real coverage scoring - #66

Merged
SkyeAv merged 8 commits into
mainfrom
feat/agent-gepa-optimize
Aug 3, 2026
Merged

feat: make GEPA --optimize work end-to-end with real coverage scoring#66
SkyeAv merged 8 commits into
mainfrom
feat/agent-gepa-optimize

Conversation

@SkyeAv

@SkyeAv SkyeAv commented Aug 3, 2026

Copy link
Copy Markdown
Owner

tablassert agent --optimize now runs dspy.GEPA end-to-end against real fullmap coverage instead of crashing or optimizing a validity-only proxy, and commits the hardened prompt it produced (10/10 MAPPED, 0.974 mean best coverage on the QC assay) with reproduction artifacts.

GEPA metric + real coverage scoring

  • Five-arg contract: gepa_metric now accepts dspy.GEPA's (gold, pred, trace, pred_name, pred_trace) positional shape; the old one-arg bundle made real dspy.GEPA(metric=gepa_metric) raise TypeError: GEPA metric must accept five arguments (the offline suite passed only because it injects a gepa_cls stub). The legacy one-dict call shape still works.
  • Real objective: new _gepa_bundle_from_dspy scores each candidate config_yaml with real fullmap coverage via build_and_audit when the example carries a fullmap path; falls back to the validity-only floor without one. Never raises — a bad candidate scores validity-only instead of aborting the compile.
  • Speed: candidate builds head-sample (5-row preview per section) by default — GEPA only needs a monotonic ranking signal; per-example head: false restores full fidelity. Examples may also carry workdir so a candidate's relative source.local resolves against the real download dir.
  • Concurrency: os.chdir is process-global, so metric builds serialize on _GEPA_BUILD_LOCK while LLM forward passes stay parallel.

LM split + CLI flags

  • --task-model: fast LM for GEPA's many program evaluations, with --model-id as the strong reflection LM (GEPA best practice); defaults to the reflection LM when unset. --gepa-threads parallelizes the evaluation pool.
  • Temperatures: GEPA_TASK_TEMPERATURE=0.3 / GEPA_REFLECTION_TEMPERATURE=1.0 — measured 3/3 schema-valid configs at 0.3 for qwen3.6-flash vs 1/3 at both 0.0 and 1.0 (schema validity is the metric's hard gate).
  • Reasoning-model safety: make_dspy_lm now passes temperature / max_tokens=16000 / timeout=600 so a reasoning model's internal CoT can't truncate config_yaml mid-output (which stalls the optimizer's output parsing).

Build hardening (prerequisite)

  • Executor timeout: build_agent raises the smolagents local-executor timeout from the 30s default to 600s (execution_timeout) — 30s killed build_and_audit on large tables (~60s for a 37k-row sheet) mid-build, stranding the fullmap redb lock and failing every later build.
  • Lock retry: fullmap.py wraps redb-backed reads in _call_with_lock_retry (10 attempts, linear backoff) so transient Database already open contention from a just-finished build can no longer surface as a false 0.0 coverage.
  • Absolute paths: build_and_audit resolves workdir (a relative one breaks .tablassert/store parquets after the internal chdir), the supervisor presents absolute candidate-table paths, and the CLI resolves --instructions-out before GEPA's chdirs. Coverage measurement also retries 3× with gc + backoff on transient frame-reproduction failure instead of reporting a false 0.0.

Derive modes

  • derive_mode: make_tools / run_supervisor accept full (default, unchanged) | derive_only (no fullmap tools → parallel derivations, serial build pass later) | derive_coverage (coverage feedback without the KGX build); derive modes terminate in the new DERIVED status.
  • Accepted caveat: derive_only can't see coverage while deriving (suboptimal for multi-sheet tables) — that's the documented trade-off.
  • Deferred: no CLI flag yet; reachable only through the Python API.

Artifacts + QC

  • examples/agent/: committed optimized_instructions.yaml (feedback-derived: source.url-required rule, prioritize mapping table, per-error-code recovery cheat-sheet, source-path-fidelity rule) and gepa-dataset.yaml (two open-access PMC gene tables); loadable via --instructions-file to skip the optimization cost.
  • QC loop: qc/qc_report.py assay → 10/10 MAPPED, 0.974 mean best coverage (QC_REPORT.md); qc/qc_reviewer.py LLM-as-judge pass over the final configs (QC_REVIEW.md: 1 good / 4 acceptable / 5 poor — the standing improvement backlog). Report and review are co-generated from the same state dir and cross-linked by a per-config sha256, so future report/review/config drift is detectable.

CodeRabbit feedback (round 1)

  • Typing: derive_mode is now DeriveMode = Literal["full", "derive_only", "derive_coverage"] in both make_tools and run_supervisor — a typo fails in pyright instead of silently falling through to full.
  • Validation: --gepa-threads < 1 exits 2 before any model is built, matching the --judge-threshold fail-loud pattern.
  • Retry amplification: build_and_audit's coverage retry no longer retries lock-contention errors — _call_with_lock_retry already burned its backoff budget before the error escaped (new fullmap.is_lock_contention); matters while holding _GEPA_BUILD_LOCK.
  • Docs: --gepa-threads documented as parallelizing LM forward passes only (builds stay serialized on _GEPA_BUILD_LOCK); the README shell example no longer carries an inline comment in a continuation line.
  • QC hygiene: qc_report.py redacts absolute paths (<state-dir>/… / <local-path>); qc_reviewer.py prepends an untrusted-data system message, reads source.local only under the QC downloads/ dir, and bounds each litellm call (timeout=300, num_retries=2).

CodeRabbit feedback (round 2)

  • Judge-response validation: validate_review_result requires dimension mappings, scores in 0..3, and overall_quality in good|acceptable|poor; anything else is recorded as a failed review (both the primary and fallback JSON paths).
  • Failed entries matchable: failed markdown headings now carry their config sha256 too.
  • Table summary fidelity: get_table_summary derives max_cols from the configured subject/object/annotation columns (PMC7206184's S/T columns are no longer cut at 12, capped at read_table's 40) and accumulates ALL sections instead of returning on the first readable one.
  • One redaction policy: redact_paths generalized to any absolute local path (URL/ratio-safe lookbehind); qc_reviewer.py sanitizes every review string — including parse-error content — through it before JSON + markdown.
  • No truncated YAML: config fence cap 3000 → 8000; new CI test parses every fenced yaml block in the committed QC_REPORT.md.
  • Docs: README documents the QC state-dir requirement — dataset configs reference the GEPA run's downloads/, and the QC scripts only read source.local under their own STATE_DIR/downloads; point them at the same state dir or stage tables there.
  • Test proofs (nitpicks): the --gepa-threads test now also stubs make_dspy_lm to fail (proves NO model construction pre-validation); the backend-forwarding test asserts the single reflection-LM construction explicitly; the dspy-LM test asserts the timeout=600 default.

Fixes

  • BioBERT HF repo id: get_biobert()'s download path pointed at pritamdeka/BioBERT-mnli-snli-scitail-mednli-stsb, which no longer exists (HF API returns 401), so a fresh judge-embedding cache could never download. Corrected everywhere to the real sentence-transformers repo pritamdeka/BioBERT-mnli-snli-scinli-scitail-mednli-stsb (API 200, ~33k downloads) in src/tablassert/qc.py, docs/api/qc.md, and tests/test_cover_qc.py. The cache dir (.tablassert/biobert/) is name-independent, so no existing cache is orphaned.

Docs

  • docs/agent.md: --optimize section rewritten for the LM split and dataset fields (fullmap / workdir / head).
  • docs/cli.md: --task-model, --gepa-threads, and extended --dataset rows.

Testing

  • uv run pytest -q --no-cov745 passed (full suite).
  • uv run pytest tests/test_agent_cli.py tests/test_agent_eval.py -q --no-cov47 passed — includes regressions for the five-arg metric contract, validity scoring without a fullmap, the head-sample default + head: false override, task_lm winning over reflection_lm in dspy.configure, and num_threads forwarding.
  • tests/test_example_qc.py11 passed: downloads allowlist (reject-without-read + allowed-read), column-derived max_cols (floor 12 / cap 40 / fixed-value encodings ignored), multi-section accumulation, judge-response validation (bad shape / out-of-range score / bool score / bad quality), redact_paths behavior, committed QC_REPORT.md yaml fences parse, and report↔review sha256 cross-check.
  • uv run ruff check ., uv run ruff format --check ., uv run pyright → all clean; pre-commit hooks (ruff/format/pyright/pytest) green on the final commit.
  • QC artifacts regenerated from .tablassert/qc-assay: report re-run deterministically; review re-rendered offline from qc_review.json (--rerender); sha cross-check → mismatches: none.

Questions for the reviewer

  • derive_mode CLI surface. The derive modes are currently Python-API-only — add a --derive-mode flag + docs in this PR, or follow-up?
  • Head-sample default. GEPA scores candidates on 5-row previews by default (full fidelity opt-in per example). Right cost/quality default?

Summary by CodeRabbit

  • New Features

    • Added configurable task and reflection models, evaluation threads, timeouts, and optimization datasets.
    • Added derive-only and derive-with-coverage execution modes.
    • Added full-fidelity scoring, working-directory resolution, and preview evaluations.
    • Added optimization guidance, sample datasets, and PMC quality reports and reviews.
  • Bug Fixes

    • Improved resilience to transient database locks and coverage failures with retries and backoff.
    • Stabilized working-directory and output-path handling.
    • Added validation for invalid evaluation-thread settings.

SkyeAv added 5 commits July 31, 2026 23:57
…t KG builds

The `tablassert agent --optimize` GEPA path was broken against real dspy 3.2.1
(gepa_metric took one arg but dspy.GEPA binds five), and the agent's KG builds
failed on relative workdirs. This makes the optimization path real and the agent
build KGs reliably.

GEPA optimization:
- gepa_metric now satisfies dspy.GEPA's 5-arg metric contract (gold, pred, trace,
  pred_name, pred_trace) while keeping the legacy single-bundle call the offline
  suite relies on.
- The metric scores each proposed config with REAL fullmap coverage (a build_and_audit
  head-sample) when the dataset example carries a `fullmap` (+ optional `workdir`/`head`),
  so GEPA optimizes the genuine objective instead of a validity-only proxy.
- run_gepa splits a fast task LM (--task-model) from the strong reflection LM, configures
  dspy with the task LM, and forwards --gepa-threads.
- make_dspy_lm adds reasoning-model-safe defaults (max_tokens=16000, temperature=1.0) and
  a request timeout (600s) so a truncated/stalled call can't hang the optimizer.
- CLI: --task-model and --gepa-threads; --instructions-out is resolved to an absolute path
  (GEPA's parallel builds chdir the process cwd).

Agent KG builds:
- build_and_audit resolves its workdir to an absolute path. A relative workdir made
  build_pipeline's `.tablassert/store` parquets resolve against the wrong base once the
  build chdir'd -> the build failed -> false 0.0 coverage -> every article SKIPPED.
- The supervisor presents absolute table paths to the inner agent so source.local resolves
  in the build workdir.
- build_agent raises the local executor timeout (30s -> 600s) so a large-table build_and_audit
  is not killed mid-build (which also stranded the fullmap redb lock).
- build_and_audit retries its coverage measurement on transient failure, and fullmap lookups
  retry on transient redb lock contention ("Database already open").

Adds a GEPA-optimized prompt + example dataset under examples/agent/, regression tests for the
5-arg contract / real-coverage metric / task-LM split / num_threads / make_dspy_lm defaults, and
docs. Verified: the agent maps PMC11947420 (coverage 0.9993, 2865 nodes / 4995 edges) and the
produced KGX validates (biolink categories/predicates + provenance, 0 dangling endpoints). Full
suite: 725 passed.
…PED)

Prompt hardening (pushed the GEPA-optimized prompt further):
- File handling & robustness: verify each table/worksheet via read_table before
  authoring a section; skip missing/empty/unmappable files gracefully (no retrying
  a broken path); use the EXACT case/space-sensitive worksheet name; use only the
  candidate-table absolute paths (never fabricate a path).
- Predicate choice: pick the most-specific valid biolink predicate (gene_associated_with_condition,
  correlated_with, expressed_in, biomarker_for, has_sequence_variant, affects...), falling back to
  associated_with/related_to only when nothing specific fits.
- Reading tool output: build_and_audit/map_coverage return a JSON STRING; parse with yaml.safe_load
  (yaml is imported and parses JSON) — never 'import json' (unauthorized) and never index the raw
  string. Return a config that already maps well instead of over-editing.

QC assay (qwen3.8-max-preview, improved prompt, 10 diverse PMCs spanning 9 predicate types):
- 10/10 MAPPED, 0 SKIPPED, mean best coverage 0.974, all first-attempt.
- 8/10 specific predicates; 2 generic associated_with fallbacks (PMC13161869, PMC12900646) flagged for review.
- examples/agent/QC_REPORT.md: per-PMC derived config + predicate/encodings/provenance + KG node/edge
  counts + sample edges + aggregate metrics, for manual QC.
…ses poor->acceptable)

Built an automated QC loop (replaces manual QC):
- examples/agent/qc/qc_report.py: deterministic per-PMC QC report (config + predicate/encodings/
  provenance + KG counts + sample edges + aggregate metrics).
- examples/agent/qc/qc_reviewer.py: LLM-as-judge (qwen3.8-max-preview) that critiques each derived
  config + table + KG sample across predicate_appropriateness / encoding_correctness / provenance /
  coverage / other_mistakes and proposes a prompt improvement per PMC.

Iterative improvement:
- Round 1 (10 PMCs): 10/10 MAPPED, mean cov 0.974; reviewer scored predicate_appropriateness 1.71/3,
  other_mistakes 1.43/3 (weak). Recurring mistakes: over-interpretation, hard-coded objects, dropped
  statistical annotations, wrong object columns, generic predicates.
- Added a Quality-principles section to the prompt to address these. Round 2: 10/10 MAPPED, mean cov
  0.989, but the reviewer flagged wrong prioritize guesses (a wrong prioritize is worse than none).
- Refined the prioritize guidance: add prioritize ONLY when confident, otherwise OMIT it. Round 3 (the 3
  worst PMCs): 3/3 MAPPED, mean cov 0.981; PMC8017771 + PMC12900646 improved poor->acceptable
  (PMC13172311 splice table remains poor — genuinely hard event-level data).
- examples/agent/QC_REVIEW.md: latest round's per-PMC review for inspection.

Key learning captured: the coverage metric measures resolution RATE, not correctness, so high coverage
can mask wrong-entity resolution; the LLM reviewer has run-to-run variance, so trends matter more than
single-round absolute scores.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The agent workflow adds derive modes, path handling, coverage retries, execution timeouts, and configurable GEPA evaluation. New prompts, datasets, QC scripts, and reports support optimization and mapping review.

Changes

Agent optimization and quality workflow

Layer / File(s) Summary
Derive modes and build execution
src/tablassert/agent.py
The agent supports derive modes, terminal DERIVED records, absolute paths, coverage retries, and configurable execution timeouts.
Fullmap lock retry handling
src/tablassert/fullmap.py, tests/test_fullmap.py
Fullmap operations retry transient redb lock-contention errors with bounded backoff. Tests cover error classification.
GEPA scoring and model configuration
src/tablassert/agent.py, src/tablassert/cli.py, tests/test_agent_cli.py, tests/test_agent_eval.py, docs/agent.md, docs/cli.md
GEPA supports head or full scoring, separate task and reflection LMs, evaluation threads, dataset metadata, absolute output paths, and configurable DSPy LM settings.
Optimization prompts and datasets
examples/agent/README.md, examples/agent/gepa-dataset.yaml, examples/agent/optimized_instructions.yaml, examples/agent/QC_REVIEW.md
The examples define optimized instructions, GEPA inputs, usage steps, evaluation settings, and QC-derived prompt improvements.
QC generation and review artifacts
examples/agent/qc/*, examples/agent/QC_REPORT.md, tests/test_example_qc.py
QC tooling generates Markdown and JSON evaluations from assay state, configurations, tables, and KG edge samples. Tests cover path handling, validation, parsing, and artifact consistency.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI as agent CLI
  participant RunGEPA as run_gepa
  participant GEPA as DSPy GEPA
  participant Bundle as _gepa_bundle_from_dspy
  participant Audit as build_and_audit
  CLI->>RunGEPA: pass task model, dataset, and thread count
  RunGEPA->>GEPA: configure task and reflection LMs
  GEPA->>Bundle: evaluate candidate prediction
  Bundle->>Audit: run head or full candidate build
  Audit-->>Bundle: return validity and coverage
  Bundle-->>GEPA: return metric score
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: making GEPA --optimize run end-to-end with real coverage scoring.
✨ 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 feat/agent-gepa-optimize

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/tablassert/agent.py (1)

2295-2324: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type derive_mode as a Literal to catch typos at check time.

make_tools and run_supervisor compare derive_mode against the literal strings "derive_only" and "derive_coverage". An unrecognized value (for example a hyphenated "derive-only") silently falls through to the "full" branch instead of failing loud. Since the project runs Pyright, declare derive_mode: Literal["full", "derive_only", "derive_coverage"] in both signatures so an invalid caller value is caught statically instead of silently changing behavior at runtime.

♻️ Proposed refactor
-    derive_mode: str = "full",
+    derive_mode: Literal["full", "derive_only", "derive_coverage"] = "full",

Also applies to: 2518-2519

🤖 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/tablassert/agent.py` around lines 2295 - 2324, Update the derive_mode
annotations in both make_tools and run_supervisor to use Literal["full",
"derive_only", "derive_coverage"], importing Literal from typing as needed. Keep
the existing mode comparisons and behavior unchanged so invalid caller values
are rejected by Pyright instead of falling through to the full mode.
docs/agent.md (1)

301-307: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify what --gepa-threads actually parallelizes.

This text states --gepa-threads "parallelizes GEPA's evaluation pool," which is true only for the LM forward pass that proposes each candidate config. The actual coverage-scoring build (build_and_audit) runs under a process-wide lock (_GEPA_BUILD_LOCK in agent.py) because os.chdir is process-global, so builds for concurrent candidates are fully serialized regardless of thread count. Since the build is documented elsewhere as the expensive step (up to ~60s for a large sheet), users tuning --gepa-threads for speed should know it mainly parallelizes LM calls, not the build/coverage cost.

🤖 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 `@docs/agent.md` around lines 301 - 307, Revise the --gepa-threads description
in docs/agent.md to state that it primarily parallelizes GEPA candidate LM
forward passes, while build_and_audit coverage scoring remains serialized by
_GEPA_BUILD_LOCK because os.chdir is process-global. Clarify that increasing the
thread count does not parallelize the expensive build/coverage step.
src/tablassert/cli.py (1)

552-553: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate --gepa-threads before building any model.

judge_threshold is validated and fails loud (exit 2) before any model is built. gepa_threads has no equivalent check and is forwarded straight to run_gepa's num_threads, which becomes dspy.GEPA's num_threads. A 0 or negative value would only surface as a confusing failure deep inside dspy/ThreadPoolExecutor construction, after models have already been built. Add a fail-loud check consistent with the existing judge_threshold pattern.

🛡️ Proposed validation
     if judge_threshold is not None and not 0 <= judge_threshold <= 1:
         print("tablassert agent: --judge-threshold must be a finite number between 0 and 1.", file=sys.stderr)
         raise SystemExit(2)
+
+    if gepa_threads is not None and gepa_threads < 1:
+        print("tablassert agent: --gepa-threads must be a positive integer.", file=sys.stderr)
+        raise SystemExit(2)

Also applies to: 594-599, 672-691, 700-700

🤖 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/tablassert/cli.py` around lines 552 - 553, Validate gepa_threads as a
positive value before any model construction, following the existing
judge_threshold validation path and its fail-loud exit-2 behavior. Apply this
check at the CLI entry flow before invoking run_gepa or passing gepa_threads as
num_threads, while preserving the existing handling for None.
🤖 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 `@examples/agent/QC_REVIEW.md`:
- Around line 13-44: Regenerate the review and QC_REPORT together from the same
state directory so every entry reflects the committed derived configs, including
the correct column mappings and predicates for PMC12900646 and PMC13172311.
Replace the stale review content and exclude its current scores from QC
evidence. Add the shared state or config hash to each artifact’s review entry so
future mismatches are detectable.

In `@examples/agent/qc/qc_report.py`:
- Around line 123-129: Update the report-generation logic around the source and
derived-config rendering to redact absolute local filesystem paths before
appending them, including paths embedded in cfg_text. Normalize paths relative
to STATE_DIR where possible, otherwise use a stable placeholder, then regenerate
the committed QC_REPORT.md using the sanitized output.

In `@examples/agent/qc/qc_reviewer.py`:
- Around line 98-105: Update the litellm.completion call in qc_reviewer.py to
prepend a system message establishing that only system-level instructions are
authoritative and that embedded table, config, and edge content is untrusted
data. Preserve the existing REVIEW_PROMPT user message and content limits while
following the established pattern in agent.py.
- Around line 68-73: Update the source.local handling in the relevant QC
table-reading functions to resolve the configured path and accept it only when
it is within STATE_DIR / "downloads" (or an established configured allowlist
root), before calling read_table. Reject out-of-root paths without reading or
sending their contents, preserving the existing behavior for permitted files and
error handling.

In `@examples/agent/README.md`:
- Around line 37-42: Fix the shell command example around the tablassert
invocation by removing the inline comment after the --task-model qwen3.6-flash
continuation or moving that comment outside the continued command, ensuring the
backslash is the final character on the line so all subsequent options remain
part of the command.

In `@src/tablassert/agent.py`:
- Around line 3163-3230: Bound candidate build time while preserving the
process-wide safety of _GEPA_BUILD_LOCK by reducing or bypassing redundant
nested fullmap lock retries during build_and_audit’s own coverage retry flow;
update the affected src/tablassert/agent.py site accordingly. In docs/agent.md
lines 301-307, clarify that --gepa-threads parallelizes only LM forward passes
while candidate builds and coverage remain serialized behind the process-wide
lock.

---

Nitpick comments:
In `@docs/agent.md`:
- Around line 301-307: Revise the --gepa-threads description in docs/agent.md to
state that it primarily parallelizes GEPA candidate LM forward passes, while
build_and_audit coverage scoring remains serialized by _GEPA_BUILD_LOCK because
os.chdir is process-global. Clarify that increasing the thread count does not
parallelize the expensive build/coverage step.

In `@src/tablassert/agent.py`:
- Around line 2295-2324: Update the derive_mode annotations in both make_tools
and run_supervisor to use Literal["full", "derive_only", "derive_coverage"],
importing Literal from typing as needed. Keep the existing mode comparisons and
behavior unchanged so invalid caller values are rejected by Pyright instead of
falling through to the full mode.

In `@src/tablassert/cli.py`:
- Around line 552-553: Validate gepa_threads as a positive value before any
model construction, following the existing judge_threshold validation path and
its fail-loud exit-2 behavior. Apply this check at the CLI entry flow before
invoking run_gepa or passing gepa_threads as num_threads, while preserving the
existing handling for None.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 97e5ebc3-57c9-4d73-8c50-e064b64bb3a9

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9068c and 837d054.

📒 Files selected for processing (14)
  • docs/agent.md
  • docs/cli.md
  • examples/agent/QC_REPORT.md
  • examples/agent/QC_REVIEW.md
  • examples/agent/README.md
  • examples/agent/gepa-dataset.yaml
  • examples/agent/optimized_instructions.yaml
  • examples/agent/qc/qc_report.py
  • examples/agent/qc/qc_reviewer.py
  • src/tablassert/agent.py
  • src/tablassert/cli.py
  • src/tablassert/fullmap.py
  • tests/test_agent_cli.py
  • tests/test_agent_eval.py

Comment thread examples/agent/QC_REVIEW.md Outdated
Comment thread examples/agent/qc/qc_report.py Outdated
Comment thread examples/agent/qc/qc_reviewer.py Outdated
Comment thread examples/agent/qc/qc_reviewer.py Outdated
Comment thread examples/agent/README.md
Comment thread src/tablassert/agent.py
- derive_mode typed as Literal[full|derive_only|derive_coverage] (make_tools, run_supervisor)
- --gepa-threads < 1 fails loud (exit 2) before any model is built
- coverage retry no longer re-retries exhausted fullmap lock contention (is_lock_contention)
- docs: --gepa-threads parallelizes LM forward passes only; builds serialized on _GEPA_BUILD_LOCK
- README: move inline comment off the continued shell line
- qc_report.py: redact absolute paths (<state-dir>/<local-path>); qc_reviewer.py: untrusted-data
  system message + downloads-dir allowlist for source.local + bounded litellm timeout
- QC_REPORT.md + QC_REVIEW.md regenerated together from the same state dir; each entry carries a
  shared config sha256 so future report/review/config drift is detectable
@SkyeAv

SkyeAv commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

All findings addressed in 8f7ad15:

Actionable

  1. QC_REVIEW.md staleness — report and review regenerated together from the same .tablassert/qc-assay state dir (10 LLM-judge calls, fresh); every entry in both artifacts now carries the config's sha256 (12-hex) so future report/review/config drift is detectable (verified: cross-artifact mismatches = none).
  2. qc_report.py path redaction — absolute paths normalized to <state-dir>/… under the state dir, any other local root → <local-path> (URLs untouched); regenerated QC_REPORT.md contains zero /home/… paths.
  3. qc_reviewer.py untrusted-data system message prepended to the judge call (matching the DATA_GUARDRAIL spotlighting pattern in agent.py).
  4. qc_reviewer.py source.local allowlist — resolved path must be under <state-dir>/downloads or it is rejected without reading/sending; also bounded each litellm call (timeout=300, num_retries=2).
  5. ✅ README shell example — inline comment moved off the continuation line.
  6. ✅ Candidate build-time bound — build_and_audit's coverage retry no longer retries lock-contention errors (new fullmap.is_lock_contention): _call_with_lock_retry already exhausted its backoff before the error escaped, so outer retries only amplified the wait while holding _GEPA_BUILD_LOCK. Plus the --gepa-threads doc clarification.

Nitpicks

  1. docs/agent.md + docs/cli.md--gepa-threads now documented as parallelizing LM forward passes only; builds stay serialized on _GEPA_BUILD_LOCK.
  2. derive_mode is DeriveMode = Literal["full", "derive_only", "derive_coverage"] in both make_tools and run_supervisor.
  3. --gepa-threads < 1 exits 2 before any model is built (matching --judge-threshold), with tests.

Verification: uv run pytest -q --no-cov → 734 passed; ruff check, ruff format --check, pyright all clean.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
examples/agent/qc/qc_reviewer.py (2)

185-190: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Include config_sha256 on failed Markdown entries.

The hash is assigned to every review after the try/except, but the failure branch omits it from the Markdown heading. A failed entry cannot then be matched with QC_REPORT.md.

Proposed fix
-            lines.append(f"\n## {pmc} — review failed: {rv.get('error') or 'parse error'}\n")
+            lines.append(
+                f"\n## {pmc} — review failed: {rv.get('error') or 'parse error'} "
+                f"(config sha256: `{rv.get('config_sha256', '-')}`)\n"
+            )
🤖 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 `@examples/agent/qc/qc_reviewer.py` around lines 185 - 190, Update the
failed-review branch in the Markdown generation logic to include the review’s
config_sha256 value in the heading, using the same fallback formatting as
successful entries. Preserve the existing failure message and continue behavior
while ensuring failed entries can be matched with QC_REPORT.md.

137-145: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the parsed reviewer response before rendering it.

json.loads checks syntax only. Later code assumes each dimension is a mapping and accepts any numeric score. A valid response with a list for one dimension can abort report generation at dd.get. An out-of-range score can also corrupt /3 aggregate metrics. Validate the response schema, score range 0..3, and allowed quality values. Mark invalid responses as failed reviews.

Proposed validation point
-        return json.loads(text.strip())
+        result = json.loads(text.strip())
+        validate_review_result(result)
+        return result

Apply the same validation to the fallback JSON path.

🤖 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 `@examples/agent/qc/qc_reviewer.py` around lines 137 - 145, Update the
reviewer-response parsing flow around the primary and fallback json.loads calls
to validate the decoded object before rendering. Require each expected dimension
to be a mapping with a score in the 0..3 range and an allowed quality value;
reject any response that fails these checks, including fallback results, and
return the existing parse_error-style failed-review result so report generation
cannot access invalid values.
examples/agent/README.md (1)

17-25: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the QC state-directory requirement.

Configs generated from the dataset use paths under .tablassert/gepa/downloads, but qc_reviewer.py only reads paths under its STATE_DIR/downloads. Document this relationship or stage the tables under the QC directory, and add a generated-config test.

🤖 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 `@examples/agent/README.md` around lines 17 - 25, Document that generated
dataset configs reference tables under .tablassert/gepa/downloads while
qc_reviewer.py resolves inputs beneath STATE_DIR/downloads, and ensure QC
evaluation uses matching staged tables or an equivalent path relationship. Add a
generated-config test covering this state-directory mapping and confirming the
QC reviewer can read the referenced tables.
🧹 Nitpick comments (3)
tests/test_agent_cli.py (3)

224-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the invalid-thread test prove the pre-model ordering.

The test blocks run_supervisor, but it does not block make_dspy_lm. It can pass if model construction occurs before gepa_threads validation. Patch tablassert.agent.make_dspy_lm to fail and assert that invalid gepa_threads performs no model construction.

Suggested assertion
     def fail_supervisor(*a: object, **k: object) -> object:
         raise AssertionError("run_supervisor must NOT run with an invalid --gepa-threads")

+    def fail_model_init(*a: object, **k: object) -> object:
+        raise AssertionError("make_dspy_lm must NOT run with an invalid --gepa-threads")
+
     monkeypatch.setattr("tablassert.agent.run_supervisor", fail_supervisor)
+    monkeypatch.setattr("tablassert.agent.make_dspy_lm", fail_model_init)
🤖 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/test_agent_cli.py` around lines 224 - 241, Update
test_agent_gepa_threads_non_positive_exits_2 to monkeypatch
tablassert.agent.make_dspy_lm with a failing stub, alongside the existing
run_supervisor guard. Keep the invalid-thread invocation and exit-code/error
assertions, ensuring the test proves neither model construction nor supervisor
execution occurs before validation.

278-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the reflection LM configuration explicitly.

lm_calls records every make_dspy_lm call, but the test inspects only lm_calls[0]. With separate task and reflection LMs, a regression can leave the reflection LM on the wrong backend while the first call still uses litellm. Assert the expected call count and inspect the reflection-LM record directly.

🤖 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/test_agent_cli.py` around lines 278 - 300, Update
test_agent_optimize_forwards_backend_to_dspy_lm to assert the expected number of
make_dspy_lm calls, then inspect the specific reflection-LM call rather than
assuming lm_calls[0] is the reflection configuration. Verify that this call
receives model, base URL, and API key values plus backend="litellm", while
preserving the existing forwarding assertions.

331-342: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the default timeout.

The fake LM captures all production keyword names. The assertions inspect captured[0], but they cover only temperature and max_tokens.

🤖 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/test_agent_cli.py` around lines 331 - 342, Extend the assertions for
the fake LM invocation captured by its __init__ to also verify the default
timeout value in captured[0]. Keep the existing checks for temperature and
max_tokens and use the expected production default for timeout.
🤖 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 `@examples/agent/QC_REPORT.md`:
- Line 259: Complete the truncated YAML mapping in the affected configuration
entry so the Neu pattern uses the full replacement value “neutrophil” with valid
quoting and braces. Ensure CI parses every fenced configuration block to catch
YAML syntax errors.

In `@examples/agent/qc/qc_report.py`:
- Around line 20-32: Apply one complete path-redaction policy across all QC
artifacts: in examples/agent/qc/qc_report.py lines 20-32, update redact_paths to
use boundary-aware matching for arbitrary absolute local paths while preserving
public URLs and STATE_DIR normalization; in examples/agent/qc/qc_reviewer.py
lines 176-183, sanitize every string value, including parse-error content,
before JSON serialization; in lines 191-205, render those sanitized review
fields in per-PMC Markdown entries; and in lines 218-220, sanitize
reviewer-generated prompt improvements before writing Markdown. Reuse
redact_paths rather than introducing separate sanitization logic.

In `@examples/agent/qc/qc_reviewer.py`:
- Around line 78-89: Update get_table_summary to derive the required column
limit from each section’s configured subject, object, and annotation columns
instead of hard-coding max_cols=12, ensuring columns such as S and T are
included. Process every configured section, accumulate their readable summaries,
and return the combined result only after all sections have been examined rather
than returning after the first readable section.

---

Outside diff comments:
In `@examples/agent/qc/qc_reviewer.py`:
- Around line 185-190: Update the failed-review branch in the Markdown
generation logic to include the review’s config_sha256 value in the heading,
using the same fallback formatting as successful entries. Preserve the existing
failure message and continue behavior while ensuring failed entries can be
matched with QC_REPORT.md.
- Around line 137-145: Update the reviewer-response parsing flow around the
primary and fallback json.loads calls to validate the decoded object before
rendering. Require each expected dimension to be a mapping with a score in the
0..3 range and an allowed quality value; reject any response that fails these
checks, including fallback results, and return the existing parse_error-style
failed-review result so report generation cannot access invalid values.

In `@examples/agent/README.md`:
- Around line 17-25: Document that generated dataset configs reference tables
under .tablassert/gepa/downloads while qc_reviewer.py resolves inputs beneath
STATE_DIR/downloads, and ensure QC evaluation uses matching staged tables or an
equivalent path relationship. Add a generated-config test covering this
state-directory mapping and confirming the QC reviewer can read the referenced
tables.

---

Nitpick comments:
In `@tests/test_agent_cli.py`:
- Around line 224-241: Update test_agent_gepa_threads_non_positive_exits_2 to
monkeypatch tablassert.agent.make_dspy_lm with a failing stub, alongside the
existing run_supervisor guard. Keep the invalid-thread invocation and
exit-code/error assertions, ensuring the test proves neither model construction
nor supervisor execution occurs before validation.
- Around line 278-300: Update test_agent_optimize_forwards_backend_to_dspy_lm to
assert the expected number of make_dspy_lm calls, then inspect the specific
reflection-LM call rather than assuming lm_calls[0] is the reflection
configuration. Verify that this call receives model, base URL, and API key
values plus backend="litellm", while preserving the existing forwarding
assertions.
- Around line 331-342: Extend the assertions for the fake LM invocation captured
by its __init__ to also verify the default timeout value in captured[0]. Keep
the existing checks for temperature and max_tokens and use the expected
production default for timeout.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78c92100-0cbf-4ae7-931f-b3770d654fbf

📥 Commits

Reviewing files that changed from the base of the PR and between 837d054 and 8f7ad15.

📒 Files selected for processing (12)
  • docs/agent.md
  • docs/cli.md
  • examples/agent/QC_REPORT.md
  • examples/agent/QC_REVIEW.md
  • examples/agent/README.md
  • examples/agent/qc/qc_report.py
  • examples/agent/qc/qc_reviewer.py
  • src/tablassert/agent.py
  • src/tablassert/cli.py
  • src/tablassert/fullmap.py
  • tests/test_agent_cli.py
  • tests/test_fullmap.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/cli.md
  • src/tablassert/fullmap.py
  • docs/agent.md
  • src/tablassert/cli.py
  • src/tablassert/agent.py

Comment thread examples/agent/QC_REPORT.md Outdated
Comment thread examples/agent/qc/qc_report.py Outdated
Comment thread examples/agent/qc/qc_reviewer.py Outdated
- qc_reviewer: validate judge JSON schema (dimension mappings, score 0..3, quality enum) before
  rendering; failed entries carry config sha256; table summary derives max_cols from the configured
  subject/object/annotation columns (S/T no longer cut at 12) and accumulates ALL sections; every
  review string sanitized via redact_paths before JSON+markdown; --rerender rebuilds markdown from
  qc_review.json without re-querying the judge
- qc_report: redact_paths generalized to any absolute local path (URL/ratio-safe lookbehind); config
  fence cap 3000 -> 8000 so no yaml block is truncated mid-mapping
- README: document the QC state-dir/downloads allowlist relationship and the qc/ scripts
- tests: new tests/test_example_qc.py (allowlist, column derivation, validation, redaction,
  committed-artifact yaml-fence parseability + shared sha256); CLI tests now prove no model is built
  before --gepa-threads validation, assert the single reflection-LM construction, and the dspy LM
  timeout default
@SkyeAv

SkyeAv commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Round-2 findings addressed in df93ca2:

Actionable

  1. ✅ Failed-review markdown headings now include config sha256 (same fallback formatting as successful entries), so failed entries stay matchable to QC_REPORT.md.
  2. ✅ Judge response is validated before rendering — validate_review_result requires each dimension to be a mapping with a score in 0..3 and overall_quality in good|acceptable|poor; applied to BOTH the primary and fallback json.loads paths; invalid responses become parse_error-style failed reviews (with a validation reason).
  3. ✅ README documents the state-directory requirement: dataset configs reference the GEPA run's .tablassert/gepa/downloads/… while qc_reviewer.py only reads source.local under its own STATE_DIR/downloads (injection defense), with the recommended invocation and the out-of-root rejection message. A generated-config test covers the mapping: tests/test_example_qc.py exercises the allowlist (reject-without-read outside downloads/, read-inside with path/sheet forwarded), so the reviewer's table access is pinned.
  4. ✅ Truncated YAML fixed: config fence cap raised 3000 → 8000 (largest configs are ~3.5KB), QC_REPORT.md regenerated, and a CI test now yaml.safe_loads every fenced block to catch truncation regressions.
  5. ✅ One redaction policy: redact_paths generalized to boundary-aware matching of arbitrary absolute local paths while preserving public URLs and ratios; qc_reviewer.py reuses it (imported from qc_report) to sanitize every review string — including parse-error content — before JSON and markdown.
  6. get_table_summary derives max_cols from the configured subject/object/annotation columns (PMC7206184's S/T no longer cut; floored at 12, capped at read_table's 40) and accumulates ALL configured sections before returning.

Nitpicks

  1. test_agent_gepa_threads_non_positive_exits_2 now also stubs make_dspy_lm to fail — proves neither model construction nor supervisor execution precedes validation.
  2. ✅ Backend-forwarding test asserts len(lm_calls) == 1 and inspects the reflection-LM record explicitly (no --task-model ⇒ exactly one LM built).
  3. ✅ dspy-LM test asserts the timeout == 600 default alongside temperature/max_tokens.

Verification: uv run pytest -q --no-cov → 745 passed (incl. new tests/test_example_qc.py, 11 tests); ruff check / ruff format --check / pyright clean; pre-commit hooks green on the commit. QC artifacts regenerated (report re-run; review re-rendered offline via new --rerender mode); report↔review sha256 cross-check: mismatches: none.

…nent)

pritamdeka/BioBERT-mnli-snli-scitail-mednli-stsb no longer exists (HF API 401); the real
sentence-transformers repo is pritamdeka/BioBERT-mnli-snli-scinli-scitail-mednli-stsb (200, ~33k
downloads). With the old id, get_biobert()'s download path could never succeed for a fresh cache.

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

🧹 Nitpick comments (1)
examples/agent/qc/qc_reviewer.py (1)

21-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

OUT_JSON/OUT_MD cache a stale STATE_DIR snapshot.

OUT_JSON and OUT_MD are computed once, at import time, from STATE_DIR. get_table_summary and redact_paths instead read STATE_DIR live at call time, so patching STATE_DIR after import (as the qc_mods test fixture does) correctly reaches them. It does not reach OUT_JSON/OUT_MD.

main() and rerender() both read/write through OUT_JSON/OUT_MD. A test that patches STATE_DIR and then calls main()/rerender() will silently read/write at the import-time path instead of the intended directory. No current test exercises main()/rerender(), but that gap makes this easy to hit later.

Derive both paths from STATE_DIR at call time instead of caching them as module constants.

♻️ Proposed fix
-OUT_JSON = STATE_DIR / "qc_review.json"
-OUT_MD = STATE_DIR / "QC_REVIEW.md"
+def _out_json() -> Path:
+    return STATE_DIR / "qc_review.json"
+
+
+def _out_md() -> Path:
+    return STATE_DIR / "QC_REVIEW.md"

Then replace the OUT_JSON/OUT_MD references in main() (state loading, .write_text, and the final print) and rerender() with _out_json()/_out_md():

def main() -> None:
    ...
    reviews = cast("dict[str, dict]", _redact_strings(reviews))
    _out_json().write_text(json.dumps(reviews, indent=1))
    render_markdown(reviews)

    print(f"\nreview -> {_out_json()} and {_out_md()}")
    ...


def rerender() -> None:
    reviews = cast("dict[str, dict]", _redact_strings(json.loads(_out_json().read_text())))
    _out_json().write_text(json.dumps(reviews, indent=1))
    render_markdown(reviews)
    print(f"re-rendered -> {_out_md()} (from {_out_json()})")

render_markdown itself also writes OUT_MD.write_text(...) at its current call site (line 288) and needs the same substitution.

🤖 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 `@examples/agent/qc/qc_reviewer.py` around lines 21 - 23, Replace the
import-time OUT_JSON and OUT_MD snapshots with call-time helpers such as
_out_json() and _out_md() derived from the current STATE_DIR. Update all
references in main(), rerender(), and render_markdown(), including reads,
writes, and printed paths, so patched STATE_DIR values are honored consistently.
🤖 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.

Nitpick comments:
In `@examples/agent/qc/qc_reviewer.py`:
- Around line 21-23: Replace the import-time OUT_JSON and OUT_MD snapshots with
call-time helpers such as _out_json() and _out_md() derived from the current
STATE_DIR. Update all references in main(), rerender(), and render_markdown(),
including reads, writes, and printed paths, so patched STATE_DIR values are
honored consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98fd6e36-107b-4a03-b4c9-2ad6606f1cdd

📥 Commits

Reviewing files that changed from the base of the PR and between 8f7ad15 and df93ca2.

📒 Files selected for processing (6)
  • examples/agent/QC_REPORT.md
  • examples/agent/README.md
  • examples/agent/qc/qc_report.py
  • examples/agent/qc/qc_reviewer.py
  • tests/test_agent_cli.py
  • tests/test_example_qc.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/agent/QC_REPORT.md
  • tests/test_agent_cli.py
  • examples/agent/qc/qc_report.py

@SkyeAv

SkyeAv commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Folded in a BioBERT HF-repo fix (82f788c): the judge-embedding download pointed at pritamdeka/BioBERT-mnli-snli-scitail-mednli-stsb, which no longer exists (HF API 401), so a fresh cache could never download. Corrected to the real repo pritamdeka/BioBERT-mnli-snli-scinli-scitail-mednli-stsb (API 200, ~33k downloads, sentence-transformers tags) in src/tablassert/qc.py, docs/api/qc.md, and tests/test_cover_qc.py. Cache dir .tablassert/biobert/ is name-independent, so no cache is orphaned. tests/test_cover_qc.py + tests/test_qc.py → 8 passed; pre-commit hooks green.

@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

🤖 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/tablassert/qc.py`:
- Line 60: Update get_biobert() so the cache is keyed and validated by the model
repository and an immutable revision, rather than loading the shared
.tablassert/biobert path unconditionally. Ensure stale metadata or a mismatched
cache is ignored and the requested model is downloaded, then add a regression
test covering stale-cache reuse.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ec32f2a0-f763-4918-8713-d09c45b017d8

📥 Commits

Reviewing files that changed from the base of the PR and between df93ca2 and 82f788c.

📒 Files selected for processing (3)
  • docs/api/qc.md
  • src/tablassert/qc.py
  • tests/test_cover_qc.py

Comment thread src/tablassert/qc.py
else:
model = sentence_transformers.SentenceTransformer( # pyright: ignore
"pritamdeka/BioBERT-mnli-snli-scitail-mednli-stsb"
"pritamdeka/BioBERT-mnli-snli-scinli-scitail-mednli-stsb"

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -t f 'qc.py|test|tests' . | head -200
printf '%s\n' '--- qc.py outline ---'
ast-grep outline src/tablassert/qc.py
printf '%s\n' '--- qc.py relevant source ---'
cat -n src/tablassert/qc.py | sed -n '1,140p'
printf '%s\n' '--- cache/model references ---'
rg -n -C 4 'get_biobert|\.tablassert|MODEL|SentenceTransformer|biobert|revision' .

Repository: SkyeAv/Tablassert

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- qc tests ---'
cat -n tests/test_qc.py | sed -n '1,260p'
printf '%s\n' '--- qc history/diff summary ---'
git diff --stat
git status --short
printf '%s\n' '--- model-related tracked files ---'
git ls-files | rg '(^|/)(qc|pyproject|uv.lock|test_qc|docs/api/qc)'
printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 'sentence-transformers|qc' pyproject.toml uv.lock | head -160
printf '%s\n' '--- exact get_biobert callers ---'
rg -n -C 5 'get_biobert\(' src tests docs

Repository: SkyeAv/Tablassert

Length of output: 14762


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- cache coverage tests ---'
cat -n tests/test_cover_qc.py | sed -n '1,125p'
printf '%s\n' '--- previous qc implementation ---'
git show HEAD^:src/tablassert/qc.py 2>/dev/null | sed -n '1,85p' || true
printf '%s\n' '--- recent commits ---'
git log --oneline -8
printf '%s\n' '--- current revision/config context ---'
cat -n pyproject.toml | sed -n '65,90p'
printf '%s\n' '--- cache-related documentation ---'
cat -n docs/api/qc.md | sed -n '114,132p'

Repository: SkyeAv/Tablassert

Length of output: 9818


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- commit under review ---'
git show --stat --oneline HEAD
git show --format=fuller --no-ext-diff HEAD -- src/tablassert/qc.py tests/test_cover_qc.py
printf '%s\n' '--- parent/current model identifiers ---'
for rev in HEAD^ HEAD; do
  printf '%s: ' "$rev"
  git show "$rev:src/tablassert/qc.py" | sed -n '42,64p' | tr '\n' ' '
  printf '\n'
done
printf '%s\n' '--- AST verifier: cache branch order and constructor arguments ---'
python3 - <<'PY'
import ast
from pathlib import Path

source = Path("src/tablassert/qc.py").read_text()
tree = ast.parse(source)
fn = next(
    node for node in ast.walk(tree)
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
    and node.name == "get_biobert"
)
if not fn.body or not isinstance(fn.body[0], ast.If):
    raise SystemExit("get_biobert does not start with a conditional cache branch")
branch = fn.body[0]
test = ast.unparse(branch.test)
print("first branch test:", test)
for label, statements in (("cache-hit", branch.body), ("cache-miss", branch.orelse)):
    calls = [
        ast.unparse(node.value.func) + "(" + ", ".join(ast.unparse(arg) for arg in node.value.args) + ")"
        for node in ast.walk(ast.Module(body=statements, type_ignores=[]))
        if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)
    ]
    print(label, "calls:", calls)
    for node in ast.walk(ast.Module(body=statements, type_ignores=[])):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "SentenceTransformer":
            print(label, "SentenceTransformer args:", [ast.unparse(arg) for arg in node.args])
            print(label, "SentenceTransformer keywords:", [(kw.arg, ast.unparse(kw.value)) for kw in node.keywords])
PY

Repository: SkyeAv/Tablassert

Length of output: 4491


🌐 Web query:

SentenceTransformers SentenceTransformer revision parameter omitted latest revision official documentation

💡 Result:

In the SentenceTransformers library, the revision parameter in the SentenceTransformer class allows you to specify a particular version of a model (such as a branch name, tag name, or commit ID) hosted on the Hugging Face Hub [1][2]. When the revision parameter is omitted, it defaults to None [1][3]. In practice, this means the library will default to using the "main" revision (the default branch) of the model repository on the Hugging Face Hub, as inherited from the underlying Hugging Face Transformers loading mechanisms [4]. Key points regarding the revision parameter: - Purpose: It enables reproducible loading by specifying a fixed model version [5]. - Default behavior: Omitted arguments default to None, resulting in the default "main" branch being fetched [1][4]. - Implementation: The parameter is available in the SentenceTransformer constructor and is propagated to relevant Hugging Face Hub and Transformers functions to handle the specific versioning [2][5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import ast
from pathlib import Path

source = Path("src/tablassert/qc.py").read_text()
tree = ast.parse(source)
fn = next(node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == "get_biobert")
try_node = next(node for node in ast.walk(fn) if isinstance(node, ast.Try))
if_node = next(node for node in try_node.body if isinstance(node, ast.If))
print("cache condition:", ast.unparse(if_node.test))

def constructor_calls(statements):
    result = []
    for node in ast.walk(ast.Module(body=statements, type_ignores=[])):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "SentenceTransformer":
            result.append({
                "args": [ast.unparse(arg) for arg in node.args],
                "keywords": [(kw.arg, ast.unparse(kw.value)) for kw in node.keywords],
            })
    return result

print("cache-hit constructor:", constructor_calls(if_node.body))
print("cache-miss constructor:", constructor_calls(if_node.orelse))
print("cache-miss saves model:", any(
    isinstance(node, ast.Call)
    and isinstance(node.func, ast.Attribute)
    and node.func.attr == "save"
    for node in ast.walk(ast.Module(body=if_node.orelse, type_ignores=[]))
))

# A small model of the two branches. This does not import or execute repository code.
def modeled_loader(cache_exists, repo, cache_path):
    calls = []
    saves = []
    if cache_exists:
        calls.append(("local", cache_path))
    else:
        calls.append(("hub", repo))
        saves.append(cache_path)
    return calls, saves

repo = "pritamdeka/BioBERT-mnli-snli-scinli-scitail-mednli-stsb"
print("modeled existing-cache behavior:", modeled_loader(True, repo, ".tablassert/biobert"))
print("modeled missing-cache behavior:", modeled_loader(False, repo, ".tablassert/biobert"))
PY

Repository: SkyeAv/Tablassert

Length of output: 598


Key the local cache by the model identity.

get_biobert() loads .tablassert/biobert before using the new repository identifier. A stale cache can load a different embedding model and silently change QC results.

Pin an immutable revision and store or validate the repository and revision in the cache metadata or path. Add a regression test for stale-cache reuse.

🤖 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/tablassert/qc.py` at line 60, Update get_biobert() so the cache is keyed
and validated by the model repository and an immutable revision, rather than
loading the shared .tablassert/biobert path unconditionally. Ensure stale
metadata or a mismatched cache is ignored and the requested model is downloaded,
then add a regression test covering stale-cache reuse.

@SkyeAv
SkyeAv merged commit af754c4 into main Aug 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant