fix(notebook): recompute projections on every generate - #404
fix(notebook): recompute projections on every generate#404FlorinSenoner wants to merge 7 commits into
Conversation
Automated reviewDoes it solve #338? Partially — forcing Found 4 issues:
protspace/apps/protspace/src/protspace/data/processors/pipeline.py Lines 396 to 413 in 3d9d670
protspace/apps/protspace/notebooks/ProtSpace_Preparation.ipynb Lines 600 to 609 in 3d9d670
protspace/apps/protspace/tests/test_issue_338_reproduction.py Lines 15 to 30 in 3d9d670 🤖 Generated with Claude Code |
|
Independent triage against current head
All current CI checks are green, but none of the added coverage exercises the upstream cache-identity cases in items 2-3. No thread is being resolved by this response. |
|
Implemented the verified review items in
Verification: focused regressions |
| " print(\"Select at least one embedder.\")\n", | ||
| " return\n", | ||
| " fasta_path = Path(inp[\"path\"])\n", | ||
| " cache_dir = _input_cache_dir(cache_root, fasta_path)\n", |
There was a problem hiding this comment.
[P1] Scope the embedding cache by backend
This content-owned directory is later paired with only {emb_name}.h5, although the notebook lets the user switch between Local and Biocentral. Both production backends resume by skipping IDs already present in that H5 (local.py:283-285, biocentral.py:150-167). On this head I seeded this exact cache layout with a Local-produced vector, then called embed_fasta(..., backend="biocentral"); it returned the unchanged Local vector without invoking Biocentral. A backend change can therefore silently generate projections and bundles from the previously selected backend. Please include the backend (and any embedding-affecting configuration) in cache ownership, or validate producer metadata before reuse, and add a backend-switch regression.
There was a problem hiding this comment.
Implemented in 82b6dbb2. The existing input-content directory now uses backend/model-owned H5 names ({backend}-{embedder}.h5), so Local and Biocentral cannot satisfy each other’s resume-by-ID cache while identical input/backend/model runs still select the same file. Regression coverage exercises both the Local→Biocentral switch (Biocentral is invoked and its vector returned) and same-backend reuse. Verification: focused cache/backend/query suite 21 passed; full non-slow Python suite 801 passed, 6 deselected; Ruff, notebook parse, strict OpenSpec, and pnpm precommit all passed.
| " return\n", | ||
| " step_html.value = \"<b>Step 1/6: Downloading FASTA...</b>\"\n", | ||
| " fasta_cache = cache_dir / \"sequences.fasta\"\n", | ||
| " fasta_cache = _query_fasta_cache_path(cache_root, inp[\"query\"])\n", |
There was a problem hiding this comment.
[P2] Publish query FASTA caches atomically
The new query-addressed path is still accepted solely when it exists and has nonzero size. query_uniprot writes save_to directly, so interruption during gzip extraction can leave a truncated-but-nonempty FASTA; the next Generate extracts whatever headers are present and permanently treats that subset as the full query result. I reproduced acceptance with a one-record partial file at this path. Please write to a temporary sibling and atomically rename only after extraction succeeds (or persist equivalent completion metadata), and cover partial-cache recovery.
There was a problem hiding this comment.
Implemented in 82b6dbb2. query_uniprot now extracts into a temporary sibling, verifies the complete write and ordered identifiers against the downloaded gzip, atomically replaces the query-addressed final path only after validation, and cleans compressed/staged artifacts in finally. The interruption regression writes partial bytes and proves neither the final cache nor a staged sibling survives; successful publication and same-query path reuse are covered. Verification: focused suite 21 passed; full non-slow Python suite 801 passed, 6 deselected; Ruff, notebook parse, strict OpenSpec, and pnpm precommit all passed.
- Remove the `written != len(content)` guard in query_uniprot; TextIOWrapper.write always returns len(s), so the OSError was unreachable. A real truncation still surfaces as an OSError from the close-time flush. - Patch `open` on the query module instead of `builtins` in the interrupted-extraction test, so the fake only intercepts opens made by query.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qoU16kDQxz6U3H2UWbbm2
Adversarial reviewReviewed in an isolated worktree by three independent lenses (code quality, adversarial correctness, issue-resolution audit), with every finding then put through a refuter whose default position was that it is a false positive. 13 raised, 8 survived refutation. Applied and pushed (
|
|
Follow-up to the 2026-08-06 adversarial review, addressed in DispositionsCleanups already applied in
Issue-audit gaps
Findings needing a decision
Verification
No review threads were resolved. |
Summary
Scope clarification
Issue #338 reports stale projections after changing a dimensionality-reduction slider. The existing projection cache key already includes all reducer parameters, so that exact symptom is not reproduced by the current code or by this PR regression.
The audit found a separate reproducible cache-identity problem in the same notebook flow: one shared cache directory allowed changed queries, disjoint FASTA inputs, same-ID changed sequences, and different H5 datasets to reuse incompatible FASTA, embedding, annotation, or projection intermediates. This PR addresses that verified problem and keeps explicit projection refresh as a notebook-level correctness guarantee.
Fix
ReductionPipelinerebuilds an annotation cache when its identifier multiset differs from the current request.refetch_stages=frozenset({"projections"}).Verification
openspec validate fix-notebook-projection-cache --strict: passedpnpm precommit: passed before commit and in the commit hookRelated to #338; this PR no longer auto-closes it because the reported slider-only symptom remains unconfirmed.