chore(openspec): archive the last two changes, plus pymmseqs and Colab fixes - #446
Merged
Conversation
The migration shipped months ago; its checklist never caught up. Sixteen of thirty boxes were ticked, and the rest were a mix of done-but-unticked, moot, and — for all of phase 5 — superseded. Archiving it as-is would have frozen that as the record and landed a `bundle-contract` capability directly alongside the `bundle-format-contract` that already exists, with nothing to say which is authoritative. Reconciled each open task against reality rather than ticking them off: - 0.1-0.3, 2.8, 3.4, 4.2, 4.3: done or moot. The freeze was never needed (the deterministic re-sync in 1.4 handled it), #295 is closed, both publish paths have since run for real (4.11.1 to PyPI, prep image on every PR), and `tsenoner/protspace-legacy` is archived with its last stale links repointed in #391. - Phase 5: superseded by `add-bundle-contract-test` (archived 2026-07-20), which reached the same goal by a different route. Two of its decisions were not skipped but reversed, and the section now records that: golden fixtures are generated per run, never committed ("a committed fixture goes stale silently"), and the CI job deliberately has no hand-maintained path filter ("a filter is a copy of an import graph, and when it is wrong the job does not fail, it silently does not run"). - The `schema.json` idea is dropped by decision. With one producer and one consumer, both in-repo and both covered by an unskippable CI job, it would be a third copy of the format to keep in step rather than a guarantee. The delta is replaced accordingly. Its three bundle requirements are gone; the fourth had no equivalent anywhere and is already true, so it archives on its own as `prep-source-coupling`: prep depends on protspace as a uv workspace source, so breaking a consumed API fails prep's tests in the same change. It was only "unfinished" because it was written against `services/protspace-prep`, the path the restructure renamed to `apps/prep`. Task 5.6 turned out to be a real gap rather than superseded: the TS side writes `.parquetbundle` and `protspace transfer -b` / `style` read one, so the web-export to CLI flow exists — but the contract only covers Python-write to TS-read. Filed as #445 and carried forward, so the archive does not bury it. proposal.md and design.md keep the superseded plan as history with the supersession marked, since archiving freezes them and an unmarked stale rationale reads as intent. Verified: `openspec validate --all --strict` 19/19; `openspec list` now shows only `unified-visibility-model`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
The last unarchived change, and the same story as merge-protspace-monorepo: the work shipped nine weeks ago, but two boxes stayed open, so the eleven visibility rules it established were absent from `openspec/specs/`. Unlike merge-protspace-monorepo this one needed no design call — `point-visibility` collides with nothing, and every requirement in it is already pinned: the spec's eleven rules map one-to-one onto eleven `describe` blocks in `visibility-model.test.ts` (35 tests, 95 opacity assertions). Both open boxes were *manual* checks, which leave no artifact, so rather than tick them on the strength of the code having shipped, each was re-checked against what is now automated: - 5.2 (fast-path, no full re-sort on legend toggle) is superseded by automation: the invalidation contract is pinned by `scatter-plot.lifecycle.test.ts` and `webgl-renderer.signature.test.ts` — a stronger guarantee than the one-off manual observation the task asked for. - 5.3 (UX parity sweep) is mostly automated since: clickability is unit rule 11, zero-opacity is rule 1, and isolate/reset are covered by three e2e specs. Two items of 5.3 nothing replaced: hide-vs-query axes re-fit, and PNG export excluding hidden points. Neither has any regression coverage — the only `axes` match under `apps/web/tests` is an unrelated PNG-chunk comment, and `figure-editor.spec.ts` never mentions hidden state. Filed as #447 and carried forward rather than ticked on the assumption that shipped means verified. `openspec/changes/` is now empty of active changes for the first time in months. Verified: `openspec validate --all --strict` 19/19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
`pymmseqs` 1.2.0 (2026-08-11) fixes both reasons this dependency was moved into the `similarity` extra. Every release through 1.1.0 shipped cp310-only wheels, so against this package's `requires-python = ">=3.12"` it compiled from sdist for every user on every platform; and it pinned `ipython<9`, which upgraded Colab's pinned ipython and produced the dependency-conflict wall in the notebook. 1.2.0 ships six `py3-none-*` wheels (macOS x86_64/arm64, manylinux x86_64/aarch64, musllinux x86_64/aarch64) and drops both `ipython` and `scikit-learn`, leaving numpy/pandas/pyyaml — all already core deps. The old floor was `>=1.0.4`, so a fresh install already resolved 1.2.0 and already got wheels. Raising the floor makes that a guarantee rather than an accident: a constrained or older resolver can no longer land on a cp310-only release and silently go back to compiling from source. The lock moves 1.1.0 -> 1.2.0 so the dev environment and CI match what users get. Keeping it an extra regardless. The wheel and ipython arguments are gone, but it is still reachable through exactly one flag, and a smaller base install stands on its own — so this does not reverse the upgrade note published hours ago in the README and the CLI guide, which stays correct. Verified against the real thing rather than the metadata alone: installed 1.2.0 as a wheel and ran `compute_similarity` end to end on a 5-sequence FASTA. Three single-residue variants score 0.974-0.987 mutually and 0.0 against two unrelated sequences, diagonal 1.0 — the matrix the pipeline expects. 862 tests pass, `uv sync --locked` is green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
esm2_3b does not fit a Colab GPU — a free T4 has 16 GB — so selecting it with the
Local backend fails partway through a run that has already paid for setup and
input handling. The panel only warned ("esm2_3b may not fit a free Colab T4"),
which does not stop the selection.
The notebook already had the mechanism for exactly this, pointing the other way:
`_drop_incompatible` drops esmc_* on the Biocentral backend, because Biocentral's
ESM-C returns embeddings orthogonal to the real model. Generalized it to filter in
both directions rather than adding a second, separate guard, since the two
constraints are complementary: esm2_3b is Biocentral-only, esmc_* is Local-only,
and each message names the backend that does work.
Scoped to the notebook on purpose. This is a Colab memory limit, not a package
one: `protspace prepare --backend local -e esm2_3b` is correct on real hardware,
so neither the CLI nor the published guide changes.
Verified both directions against the real function lifted out of the notebook:
local drops esm2_3b and keeps esmc_600m, biocentral drops esmc_600m and keeps
esm2_3b, and neither fires when nothing is incompatible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
The esm2_3b block landed in _drop_incompatible only, which is the backstop — not _sync_backend_ui, which is the primary enforcement ESM-C already had. So the model stayed checkable under Local and vanished silently at Generate, and in the UniProt-query branch it did so *after* the sequence download, throwing away a full fetch. Both directions now read one _INCOMPATIBLE table, so the checkbox gating and the drop-at-Generate backstop cannot disagree, and the resolve+filter moves above the fetch so an incompatible selection is rejected before it costs anything. The size rule itself was written twice with contradicting verdicts: local.py said "may exceed a free Colab T4, switch to an L4/A100", the notebook said "does not fit, use Biocentral". It lands in local.py as COLAB_OVERSIZED, which the notebook imports and a test pins to real short keys — a typo there would silently gate nothing, which is the failure the set exists to prevent. The help text no longer restates the rule statically (it was true for one backend only); it points at the backend note, which is live. Also hoists _DOCS_CLI, since the same URL was split mid-path in two places. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
…ad service
Two defects in the esm2_3b gating, both found reviewing the previous commit.
Cell 1 imported COLAB_OVERSIZED, a name added in the same change. But cell 1
pip-installs the *released* protspace while the notebook is served from main,
so on first run the installed package does not have it yet — verified against
the real 4.11.1 wheel, which still carries `if embedder == "esm2_3b"`. Cell 1's
ImportError handler raises SystemExit telling the user to restart the session,
which is both the wrong diagnosis and the wrong blast radius for one optional
constant. It moves to cell 2 behind a try/except with a literal fallback, so
main and PyPI are allowed to be out of step.
"use the {other} backend" was the only remedy offered, and for esm2_3b it is
wrong in the very flow that sends people to this notebook: prep-failure-routing
guarantees a Biocentral outage routes to Colab, where Auto resolves to local on
a GPU runtime — so the panel answered an outage by recommending the service that
just failed. The table's third field now carries a whole remedy rather than a
backend name, which keeps one table and one render path while letting the local
row lead with advice that holds regardless of Biocentral's health.
Also corrects the resource: esm2_3b loses to host RAM (11.4 GB of fp32 weights
against the free tier's 12.7 GB), not to T4 VRAM, and it does so inside
setup_model, which sits outside the OOM backoff — so it kills the kernel rather
than raising. The gate stays identity-keyed: the runner-up is 4.29 GiB, a 2.5x
gap with nothing in between, so there is no threshold to tune. That it also
blocks paid runtimes where the model fits is real, and is issue #448.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
… note The upgrade note told `-s` users that the base install "no longer compiles MMseqs2 from source". Scoped to the base install that stayed literally true, but pymmseqs 1.2.0 ships py3-none wheels, so the extra does not compile either — and in an upgrade note aimed at people who are being told to install that extra, the clause warns about a cost that no longer exists on the path it recommends. Replaced with what this PR actually delivers. Both copies move together; the sync test compares them line-for-line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
…o fallback Closes #448, closes #449, closes #450. Spec first: adds the embedding-backend-selection capability, which had no home at all — grepping the spec corpus for esm/embedder/colab hit only prep-failure-routing's Colab-routing copy. The local backend shipped through #320 without an OpenSpec change, so the two compatibility rules lived as literals in one notebook cell, in two code paths that had already diverged. #448: the size rule blocked hardware that works. It keyed on model identity and never asked what the runtime had, so esm2_3b was refused on every Colab tier — including the paid L4/A100 runtimes where it fits, and where it worked before this PR made the gate hard. It is now conditional on host memory. Host memory, not VRAM, because that is what actually decides: an oversized checkpoint is materialised on the host before it reaches the GPU (esm2_3b loads fp32 — setup_model passes no dtype for ESM — so 11.4 GB against the free tier's 12.7 GB), and losing that race kills the kernel with no traceback, inside setup_model, which sits outside the OOM backoff. Running out of VRAM merely raises. A VRAM probe would also decide nothing extra on any current tier while building a CUDA context to do it. The set stays identity-keyed: the runner-up is 4.29 GiB against esm2_3b's 10.59, so there is no boundary to tune. #449: a free Colab runtime is CPU-only until a GPU is attached, so auto resolves to biocentral — and this notebook is where a Biocentral outage sends people. The default path handed them straight back to the service that had just failed, silently. The panel now says so and says what changes it, and the same hint joins the generation-failure troubleshooting list. Also moves _ESMC into the package as BIOCENTRAL_INVALID beside COLAB_OVERSIZED, so both blocked sets are declared once and pinned by a test — a typo in either would have silently gated nothing. Both are imported behind the tolerant fallback, since the notebook is served from main but installs the released package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
…the fallback The gating landed correct but doubled: the select/resolve/drop/bail block was written out in both the query and fasta branches of _on_gen, and only the query copy got moved ahead of the UniProt fetch. Extract _embedders_and_backend() so the ordering is structural and there is one copy to edit. Alongside it, shed the scaffolding the second constraint added: - _INCOMPATIBLE values 3-tuple -> 2-tuple; `why` and `remedy` were stored apart but re-joined with the same "; " at both call sites. - _host_ram_gb() -> _ram_below_gb(gb), returning the boolean actually wanted instead of a float plus a magic 0.0 "unreadable" sentinel. _OVERSIZED_MIN_RAM_GB folded in, so 16 sits under the comment justifying it. - The two red-<span> message branches append plain text to one list, wrapped once. - _INCOMPATIBLE[eff] -> .get(...): an unknown backend blocked nothing anyway, but it used to raise KeyError at panel-build time and take the whole control panel down with it. - Drop ~14 lines of comment that restated rationale already living at the package definition sites. local.py: the warning generalised to `%s` but still hardcoded esm2_3b's ~11.4 GB, so a second name in COLAB_OVERSIZED would have emitted a wrong figure. The set's comment also said "free Colab GPU (~15 GB usable VRAM)" while the mechanism everywhere else is host RAM. Tests: pin each blocked set to the registry it constrains rather than to whichever was handy — COLAB_OVERSIZED is a fact about LOCAL_CHECKPOINTS, so a local-only oversized checkpoint must not have to be a Biocentral shortcut to be gated. And pin the notebook's `except ImportError` literals, which nothing checked. That copy is the one that runs during the released-package lag it exists for, so emptying BIOCENTRAL_INVALID or adding to COLAB_OVERSIZED would have left the panel gating on stale policy silently. Read structurally off the guarded import so a fallback that is missing, emptied, or written as an annotated assignment / bare set literal fails rather than matching nothing — a guard that can silently disarm itself is worse than none, since the notebook comment claims protection either way. Parses IPython-transformed source, because ProtSpace_Transfer.ipynb already has a `!protspace transfer` cell that raw ast.parse cannot read. Behaviour-preserving except the KeyError hardening, verified by mutation testing each drift case and by executing the rewritten gating against stub widgets for auto/local/biocentral/unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
Raising the floor to 1.2.0 does guarantee a prebuilt wheel, but not everywhere: 1.2.0 publishes six py3-none wheels — macosx 10.15 x86_64, macosx 11.0 arm64, manylinux x86_64/aarch64 and musllinux x86_64/aarch64 — and no win_amd64, so pip on Windows still falls back to the sdist and compiles. Both copies edited identically; test_docs_extras_sync pins them line for line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
Three bookkeeping fixes found reviewing the archive: - `#66`/`#55`/`#60` in the monorepo change's reconciled tasks are protspace-legacy PRs, but bare `#N` autolinks against this repo. `#66` is the silent case: it resolves to a merged, unrelated frontend PR ("enhance StructureService with 3D Beacons API integration") rather than 404ing, so the link looks fine and points somewhere wrong. The intended one is "bundle format v2 — lossless annotation name encoding", which is what the surrounding text describes. `#306`/`#295`/`#233`/`#445` are genuine refs here and stay bare. - design.md still described `_INCOMPATIBLE` as (blocked set, why, remedy); 3cdacf9 collapsed it to (blocked set, note). The archive commit landed before that refactor and nothing re-reconciled after. - A 5.3 checkbox was ticked while its own text read "carried forward to #447 rather than ticked". Reworded so the box and the sentence agree. openspec validate --all --strict: 20 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
…e impact list Two AGENTS.md conformance fixes on the previous commit and the archive: - 53b231f expanded every `#66`/`#55`/`#60` mention inline. AGENTS.md asks for the opposite: qualify once on a definitional line and keep the body bare, since full qualification turns headings into `### 2.3 tsenoner/protspace-legacy#57: ...`. The body goes back to bare `#N` and a `**Refs:**` line in the header blockquote carries the disambiguation. - "Archive before the merge" asks that proposal.md and design.md be reread against the final diff, because archiving freezes them. The archive commit predates the cleanup in 3cdacf9, so its Impact list named neither test file nor CLAUDE.md and described a gating shape that no longer exists. Both recorded; the requirements are unchanged by the refactor. openspec validate --all --strict: 20 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
tsenoner
added a commit
that referenced
this pull request
Aug 13, 2026
… wrong "For anything else, dispatch it" reads as: every docs-only and Python-only PR should fire a full Playwright suite. #446 did exactly that — it touched apps/protspace, docs/, openspec/ and uv.lock, no TS/JS and none of the root resolution files, so it could not reach the web app at all, and the dispatched run spent ~10 minutes confirming that. The intent is worth keeping: e2e.yml's own comments are emphatic that a wrong `paths:` filter does not fail the job, it silently never runs, which reads like a pass. That argues for covering what the filter might *miss*, not for dispatching unconditionally. The "not whenever `paths:` simply didn't match" clause is there so the narrower rule does not read as an oversight and get widened back. Nothing about the workflow changes; e2e.yml still owns the paths list. Net +5 lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
OpenSpec housekeeping plus two small
apps/protspacefixes that came out of it. Reconciles and archives the last two OpenSpec changes. Both shipped months ago and were left part-ticked, soopenspec/specs/had no record of guarantees the code has been enforcing since June.Follow-up to #444, which archived the four changes that needed no judgement. These two did. After this,
openspec/changes/holds no active change for the first time in months.1.
merge-protspace-monorepo(was 16/30)Two problems, either of which would have made a plain archive worse than leaving it alone:
Its delta would have created a competing capability. It declares
bundle-contract;bundle-format-contractalready exists, created a month later byadd-bundle-contract-test. Two specs on one subject, nothing saying which is authoritative.Its phase 5 was reversed, not skipped. The shipped design deliberately contradicts it:
bundle-format-contract).parquetbundlechecked into the repository"So the section is marked superseded with both reversals recorded, rather than ticked. Every other open task was already done or moot: the freeze was never needed (1.4's deterministic re-sync handled it), #295 is closed, both publish paths have since run for real (4.11.1 to PyPI; the prep image on every PR), and
tsenoner/protspace-legacyisarchived: truewith its last stale links repointed in #391.schema.jsonis dropped by decision. With one producer and one consumer, both in-repo and both covered by a CI job that cannot be silently skipped, a declarative schema would be a third copy of the format to keep in step rather than a guarantee.One requirement was alive and hiding. The delta's fourth requirement has nothing to do with bundles: prep must consume
protspacefrom workspace source so a break surfaces in the same change, not after a release. It is already true —protspace = { workspace = true }— and was only "unfinished" because it namesservices/protspace-prep, the path the restructure itself renamed toapps/prep. It lands asprep-source-coupling.2.
unified-visibility-model(was 20/22)No design call needed here —
point-visibilitycollides with nothing, and every requirement in it is already pinned: the spec's eleven rules map one-to-one onto elevendescribeblocks invisibility-model.test.ts(35 tests, 95 opacity assertions).Both open boxes were manual checks, which leave no artifact. Rather than tick them because the code shipped nine weeks ago, each was re-checked against what is now automated:
scatter-plot.lifecycle.test.tsandwebgl-renderer.signature.test.ts, a stronger guarantee than the one-off manual observation the task asked for.3.
pymmseqsfloor raised to 1.2.0pymmseqs1.2.0 (published 2026-08-11, after #391 merged) fixes both reasons that dependency was moved into thesimilarityextra. Every release through 1.1.0 shipped cp310-only wheels, so againstrequires-python = ">=3.12"it compiled from sdist for every user on every platform; and it pinnedipython<9, which upgraded Colab's pinned ipython and produced the dependency-conflict wall in the notebook. 1.2.0 ships sixpy3-none-*wheels (macOS x86_64/arm64, manylinux and musllinux x86_64/aarch64) and drops bothipythonandscikit-learn, leaving numpy/pandas/pyyaml — all already core.The old floor was
>=1.0.4, so a fresh install already resolved 1.2.0 and already got wheels. Raising the floor makes that a guarantee rather than an accident: a constrained or older resolver can no longer land on a cp310-only release and silently go back to compiling from source. The lock moves 1.1.0 → 1.2.0 so dev and CI match what users get.It stays an extra. Both original arguments are gone, but it is reachable through exactly one flag and a smaller base install stands on its own.
CLAUDE.mdrecords that upstream fixed both problems and why the extra remains, so its rationale does not quietly become another stale claim.The published upgrade note did need a correction, though — see §5.
Verified against the artifact rather than the metadata: installed 1.2.0 as a wheel and ran
compute_similarityend to end on a 5-sequence FASTA — three single-residue variants score 0.974–0.987 mutually and 0.0 against two unrelated sequences, diagonal 1.0. The full 3.12/3.13/3.14 CI matrix is the real proof thepy3-nonewheels install everywhere supported.4.
esm2_3bis gated on what the runtime can actually holdCloses #448. Closes #449.
esm2_3b failed partway through a Colab run that had already paid for setup and input handling, and the panel only warned, which does not stop the selection. The notebook already had the mechanism pointing the other way —
_drop_incompatibledropsesmc_*on Biocentral, whose ESM-C returns embeddings orthogonal to the real model — so both constraints now read from one table that drives the checkbox gating and the drop-at-Generate backstop. Previously those were separate code paths and had already diverged: the first knew one rule, the second knew both.The gate keys on host memory, not on the model name. The first cut blocked esm2_3b on every Colab tier, including the paid L4/A100 runtimes where it fits — a capability that worked before this PR. It is now conditional:
Host memory rather than VRAM because that is what actually decides.
setup_modelpasses no dtype for ESM, so esm2_3b loads fp32 — 11.4 GB of weights, materialized on the host byfrom_pretrainedbeforemodel.to(device). Against the free tier's 12.7 GB that is the binding constraint, and it is the worst failure available: a host-RAM blowout kills the kernel with no traceback, taking the fetched sequences with it, andsetup_modelsits outside theOutOfMemoryErrorbackoff that only wraps_embed_batch. Running out of VRAM merely raises. A VRAM probe would also change no outcome on any current tier while building a CUDA context to do it.The set stays identity-keyed rather than computed: esm2_3b is 10.59 GiB of weights, the runner-up (
ankh_large, encoder-only) is 4.29 GiB, and nothing sits between them — so there is no boundary to tune, and a per-model GB column would rot silently across atransformersbump.The remedy no longer names a service that may be down.
prep-failure-routingguarantees a Biocentral outage routes users into this notebook, whereautoresolves tolocalon a GPU runtime — and the panel answered by telling them to use Biocentral. The table's third field now carries a whole remedy instead of a backend name.The same flow strands users more broadly, which is #449: a Colab runtime is CPU-only until a GPU is attached, so
autofalls back to the very service that just failed, silently. The panel now discloses that fallback and names the fix, and the same hint joins the generation-failure troubleshooting list.Still scoped to the notebook deliberately: these are limits of a hosted environment, not of the package.
protspace prepare --backend local -e esm2_3bstays correct on real hardware, and the CLI enforces neither rule.5. Corrections found while reviewing the above
Three defects in this PR's own earlier commits, plus one stale published claim:
COLAB_OVERSIZED, a name added in the same commit — but cell 1 pip-installs the released protspace while the notebook is served frommain, and 4.11.1 does not have it (verified against the actual wheel). ItsImportErrorhandler raisesSystemExittelling the user to restart the session. Both blocked sets now import behind a tolerant fallback, somainand PyPI are allowed to be out of step._ESMCwas a bare notebook literal that nothing pinned. It moves tobiocentral.pyasBIOCENTRAL_INVALID, besideCOLAB_OVERSIZEDinlocal.py, and a test asserts each resolves to a real shortcut — a typo in either would have silently gated nothing. Each set is pinned to the registry it actually constrains (COLAB_OVERSIZED→LOCAL_CHECKPOINTS,BIOCENTRAL_INVALID→ALL_SHORT_KEYS); they coincide today, so pinning both to Biocentral's would have made the first local-only oversized checkpoint fail a test whose message is about Biocentral shortcuts.-susers the base install "no longer compiles MMseqs2 from source". Scoped to the base install that stayed literally true, but after 1.2.0 the extra does not compile either — so an upgrade note telling people to install that extra warned about a cost that no longer exists on the path it recommends. Both synced copies (README.md,docs/guide/python-cli.md) move together; the sync test compares them line-for-line.The replacement needed a hedge of its own: 1.2.0 publishes six
py3-nonewheels and nowin_amd64, so a flat "installs from a prebuilt wheel" is false on Windows, where pip still builds the sdist. Qualified to macOS and Linux.6.
embedding-backend-selectioncapabilityCloses #450.
The local backend shipped through #320 with no OpenSpec change, so nothing in
openspec/specs/described it — grepping the corpus foresm/embedder/colabhits onlyprep-failure-routing's Colab-routing copy. That is why §4's two rules lived as literals in one notebook cell and drifted.Authored spec-first per
AGENTS.mdand through the real workflow rather than hand-adding a file (all 19 existing specs were written by a commit that also archived a change):openspec new change→ proposal/design/tasks/delta →validate --strict --type change→archive, as the last commit on the branch.Six requirements:
autoresolution and its failure modes, the fallback disclosure, gating-plus-backstop from one declaration with rejection before expensive input acquisition, the two compatibility rules, the capacity condition on the size rule, and the deliberate CLI non-enforcement.Gaps carried forward, not closed
.parquetbundleandprotspace transfer -b/styleread one, but the contract only covers the other direction.axesmatch underapps/web/testsis an unrelated PNG-chunk comment, andfigure-editor.spec.tsnever mentions hidden state.Not closed here, and deliberately: loading ESM/ESM-C in fp16 on GPU would halve esm2_3b to 5.29 GiB and let §4's size rule be deleted rather than conditioned. It changes numerics for every
esm2_*andesmc_*key, so it needs the local↔Biocentral parity cross-check re-run andCLAUDE.mdupdated — and esm2_3b was never in that cross-check. Recorded in #448.7. Cleanup pass over §4's gating
A review of the above found the gating correct but doubled, and the pin protecting it
weaker than advertised.
The duplication. The select/resolve/drop/bail block was written out in both the
query and fasta branches of
_on_gen, and only the query copy was moved ahead of theUniProt fetch. Extracted as
_embedders_and_backend(), so the "reject before paying forinput" ordering is structural rather than a comment, and there is one copy to edit.
Alongside it:
_INCOMPATIBLE's values drop from a 3-tuple to a 2-tuple (whyandremedywere stored apart but re-joined with the same"; "at both call sites);_host_ram_gb()becomes_ram_below_gb(gb), returning the boolean actually wantedinstead of a float plus a magic
0.0"unreadable" sentinel; the two red-<span>branches append plain text to one list wrapped once; and
_INCOMPATIBLE[eff]becomes.get(...), since an unknown backend blocked nothing anyway but used to raiseKeyErrorat panel-build time and take the whole control panel down with it.The pin had the failure mode it existed to prevent. The new notebook-fallback guard
asserted only inside a loop over whatever its matcher found, so when the matcher found
nothing it passed green. Three natural edits silently disarmed it: writing the emptied
fallback as
frozenset()(zero args), using the package's own annotated-assignmentstyle, or deleting the fallback outright. It also called raw
ast.parseon cell source— in the one test module whose entire reason for existing is that notebook cells are not
plain Python, and
ProtSpace_Transfer.ipynbalready has a!protspace transfercellthat raw parse cannot read.
Rebuilt to key off the
try/exceptstructure rather than scanning for assignments, soa missing fallback is a failure instead of an empty result, and to parse
IPython-transformed source. Mutation-tested, notebook restored byte-identically after
each:
frozenset()local.py's warning generalised to%sbut still hardcoded esm2_3b's~11.4 GB, so asecond name in
COLAB_OVERSIZEDwould have emitted a wrong figure; dropped. Behaviour isotherwise preserved — the
KeyErrorhardening is the one intentional change — verified byexecuting the rewritten gating against stub widgets across auto/local/biocentral/unknown.
Bookkeeping fixes found reviewing the archive.
#66/#55/#60in the monorepochange's reconciled tasks are
protspace-legacyPRs, but bare#Nautolinks againstthis repo — and
#66is the silent case, resolving to a merged, unrelated frontend PR("enhance StructureService with 3D Beacons API integration") instead of 404ing, so the
link renders fine and points somewhere wrong. Qualified;
#306/#295/#233/#445aregenuine refs here and stay bare.
design.mdstill described_INCOMPATIBLEas(blocked set, why, remedy) because the archive commit predates the refactor above. And a
5.3 checkbox was ticked while its own text read "carried forward to #447 rather than
ticked" — reworded so the box and the sentence agree.
Notes
proposal.mdanddesign.mdkeep the superseded plan as history with the supersession marked at the top of each affected section — archiving freezes them, and an unmarked stale rationale reads as intent.All three new capabilities got a real
## Purpose.openspec archivewrites aTBD - update Purpose after archiveplaceholder that passesvalidate --strict, so it survives unless somebody looks — which is how two from an earlier archive sat there until #444.Verification
openspec validate --all --strict→ 20 passed, 0 failed, re-run after precommit's prettier pass (it rewraps markdown, and--strictonly scans a requirement's first line for the normative clause).openspec list→ no active changes.pnpm precommitgreen on every commit.uv run pytest -m "not slow"→ 825 passed.The gating was exercised across the full matrix — released-lag × capacity × both backends — and renders identically whether or not the installed package carries the new constants.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ar6ME3o6VYLYsie3J4kxE5