Skip to content

Trigger cross-projects - #6

Open
tpeulen wants to merge 86 commits into
mainfrom
dev
Open

Trigger cross-projects#6
tpeulen wants to merge 86 commits into
mainfrom
dev

Conversation

@tpeulen

@tpeulen tpeulen commented May 19, 2026

Copy link
Copy Markdown
Member

Trigger build of CS

tpeulen added 30 commits May 19, 2026 11:21
GitHub Actions should only run from the active default branch now that the repo has been renamed to main/dev. This change limits the conda workflow triggers to main for both push and pull_request events, avoiding stale runs from legacy branch names.

Constraint: Branch rename already completed on GitHub
Rejected: Rely on default-branch behavior alone | too implicit for explicit CI routing
Confidence: high
Scope-risk: narrow
Directive: Keep workflow branch filters in sync with future branch renames
Tested: Reviewed workflow diff and verified no other .github/workflows branch selectors remain
Not-tested: Live GitHub Actions run after push
The old ignore file predates the current tree: it listed the generated
Cython sources under quest/lib/, which is gone, and nothing else.

Replace it with rules covering the surfaces that actually exist now -
Python builds and caches, the Node/Next.js frontend, pixi's materialised
.pixi/ environment (pixi.toml and pixi.lock are committed instead), and
the usual editor and OS noise.

Also ignore simulation output. simulate_project writes jobs/<uuid>/ under
the working directory whenever save_outputs is true, so anything run from
the repo root leaves a directory behind; that had grown to 2292 runs and
32 GB. The scratch files left by manual runs and the molecule viewer are
ignored with it.
okf/ is the durable, agent-readable knowledge layer that sits beside the
code: plain markdown with YAML frontmatter, grouped as architecture/,
subsystems/, workflows/, specs/ and references/ under a root index.md,
plus an append-only log.md and a session handover.

It records what is not derivable from the source - what actually runs
during a simulation, the project-JSON contract every surface shares, the
physics and its approximations, the assessment backlog and where the
architecture is heading, and the defects that are known but not yet
fixed. CLAUDE.md points at it and states the working practices that keep
it in step with the tree: it is updated in the same change as the code,
never in a later pass.

No source changes.
quest/lib/ was a vendored slice of an older application - a genealogy
mixin, a math package with its own linalg, an I/O layer that reimplemented
PDB parsing, and the simulation buried under tools/dye_diffusion/. Almost
none of it was reachable from the library entry points.

What survives moves to quest/core/ under names that say what it is:
simulation.py (was core.py), structure.py, pdb.py, av.py, imp_av.py,
photon.py, dye_diffusion.py, io.py, fps_json.py. The Qt-era class
hierarchy, the genealogy mixin and the unused math package are gone.

Two changes here alter what the science computes:

- Radii come from IMP. read_pdb already decorates every particle with a
  radius, so the reader takes IMP.core.XYZR().get_radius() directly
  instead of consulting a private element table. That table
  (quest/lib/common.py, 271 lines) is deleted, and with it the last
  reason the two accessible-volume backends disagreed - fed the same
  atoms with the same radii they now agree to a 0.90 point ratio with
  centroids within 0.5 A, where before it was 0.78 and was misread as an
  algorithmic difference. IMP's CHARMM radii are ~19% larger than the
  Bondi-ish values QuEst carried, so accessible volumes shrink and some
  sites that were labelable no longer are.
- imp_av.py takes the atom array rather than a filename, so coarse
  graining can no longer be silently discarded and mmCIF works wherever
  the reader works. It had shipped a Model.get_particles() call that does
  not exist: the backend had no test coverage at all, because AV_BACKENDS
  starts with LabelLib and nothing ever entered that branch.

quest/imp_tricks.py splices the imp-tricks checkout onto sys.path
deliberately without installing it, since that source tree is what is
changing.

Tests are re-baselined for the new radii rather than loosened; each
replaced site was chosen to satisfy its own test's assertion, not just to
be labelable. conftest.py pins NUMBA_NUM_THREADS and
NUMBA_THREADING_LAYER, because a warm numba cache otherwise hides
compile-time failures in the jitted kernels.
Logic that lived in a surface cannot be reached from the others, and that
is how the GUI came to ship different quenching chemistry from the CLI for
as long as nobody looked. This adds the layer that makes that structurally
impossible.

- backend/dispatcher.py registers QuEst's capabilities as named RPC
  methods behind a single envelope, {ok: true, result} or
  {ok: false, error}, so callers never sniff for a field to tell success
  from failure. It is deliberately small: a host application can pass its
  own dispatcher to register_services() instead, since registration only
  needs register(name, handler).
- backend/services.py registers the methods themselves. manifest.json
  declares them, along with the state schema, so a host can learn what
  QuEst offers without importing it.
- project.py owns the project-JSON contract - the one document the CLI,
  the web backend, the GUI and notebooks all exchange.
- jobs.py owns the run directory; scan.py the labelling scan;
  structure_fetch.py fetching a structure by PDB ID; structure_info.py
  the chain, residue and site inventory a caller needs before it can
  choose an attachment site.

The CLI is rewritten as a thin wrapper over these methods rather than a
second implementation, which is most of its diff.
Parameter help text was written three times - in the Qt form, in the web
UI, and in doc/parameters.md - so the three drifted, and a parameter could
mean one thing in a tooltip and another in the docs.

quest/settings/parameter_catalog.json is now the single description. Both
doc/parameters.md and the Qt form's view spec are generated from it, and
the web API serves it, so a parameter is edited in one place and the
change reaches every surface at once. Never hand-edit the generated files;
regenerate with doc/generate_parameter_docs.py and
quest.gui.generate_view_spec.

Because the description exists once, translating it is tractable:
quest/i18n.py plus locales/{en,de,fr}.json give English, German and French
across the docs, the desktop form and the web UI together. The tests
assert what makes that safe - a translation is complete, structurally
identical to English, and never changes what the software computes. Locale
files are cached per process, so a running uvicorn must be restarted after
editing one.

quest/settings/dye_repository.json replaces the dye presets that were
tucked under quest/lib/tools/dye_diffusion/, and the loader fills in the
stickiness fields a preset may omit so every entry is complete before it
reaches the simulation.
Every .ui file is gone. The desktop form is quest/gui/quest.view.json,
generated from the parameter catalog by generate_view_spec.py and rendered
by ChiSurf's AutoForm, with de/fr spec variants produced from the same
locale files as the docs. Adding a parameter to the catalog therefore adds
it to the form; it can no longer be forgotten there.

The form is bound to ProjectFormModel, whose state *is* a project dict.
That is the point of the rewrite: the GUI edits the same document the CLI
loads, so the two cannot drift the way they did when the GUI shipped its
own quenching chemistry.

chisurf_host.py reaches a ChiSurf checkout to borrow AutoForm. Where
ChiSurf is absent QuEst still runs on the in-tree fallback, and nothing
outside quest.gui imports it - Qt stays an optional dependency, which is
what lets the CLI, the API and the web backend run in environments with no
Qt binding at all.

This removes the last of quest/lib/: the widget layer, the plot classes,
the Qt resource module and the icon .qrc. The window icon survives as a
plain .ico file.

Note for running the tests: -p no:pytest-qt is required only where there
is no Qt binding, because the plugin aborts collection there. In an
environment that has one the flag is harmful - it removes the qtbot
fixture these tests need.
A browser surface over the same service layer as the CLI and the GUI:
FastAPI in webui/backend calls the registered methods and serves the
parameter catalog, so labels, tooltips and translations come from the
catalog rather than being written a second time in TypeScript.

The frontend covers simulation, labelling, scanning and an advanced view,
with an NGL structure viewer. NGL is vendored under public/vendor rather
than pulled from a CDN so the desktop bundle works offline.

webui/desktop wraps the two in Electron and prepares a Python environment
for them.

Two things that cost time and are easy to hit again: uvicorn does not
reload, so it must be restarted before a browser run - and since locale
files are cached per process, editing one has no effect until it is. The
frontend needs Node >= 20; Next refuses to start on 18.

The browser suite is marked and deselected by default. Chromium's renderer
dies once a single pytest process has also imported IMP, numba and FastAPI
and executed the notebooks; the suite passes on its own and fails at the
end of a combined run, so it gets its own process. Deselecting says that
out loud, where silently skipping would not.
…ples

The old notebooks were snapshots of a working directory - stored outputs,
duplicate copies with '(2)' and '(1)' in their names, and imports of
modules that no longer exist. None of them executed.

Four remain, numbered by what they teach: donor quenching, a FRET pair,
the ET distance distribution, and diffusion-modulated FRET.
quest_notebook.py holds the setup they share so each notebook is about
the physics rather than about paths and imports.

examples/ is the same material as plain scripts, for readers who do not
want a kernel.

Both are executed by the test suite rather than trusted, so an example
that stops working fails CI. The notebook suite is marked and deselected
by default - it spawns kernels, so it needs its own process.
pixi.toml declares four environments and the tasks that run in each:
default (library, CLI, web backend - test, test-web, serve), desktop (the
above plus Qt - gui, test-gui), frontend (the Node toolchain - dev, build,
typecheck) and build (rattler-build - build-pkg). Python is a pixi
dependency like any other, so a contributor needs pixi and nothing else,
and CI runs the same task names a developer does rather than a parallel
set of shell steps.

The conda package is built by rattler-build from rattler-recipe/recipe.yaml;
conda-recipe/meta.yaml and the conda mambabuild workflow it drove are
removed rather than kept alongside, since two recipes would only diverge.

pyproject.toml follows the tree: quest.gui replaces quest.quest_gui as the
GUI entry point, the web dependencies become a 'web' extra, and package
data no longer ships *.ui - there are none - while shipping the .ico that
replaced the Qt resource module. The numpy>=2.0 floor is relaxed to plain
numpy: the installed IMP extension modules are compiled against numpy 1.x
and abort under 2.x, so the declared floor was promising something the
stack could not deliver.

The pytest markers and the default deselection of the browser and notebook
suites are declared here too, with the reason in a comment.
The handover opened by saying everything was uncommitted, which is no
longer true of quest - it is nine commits on main, listed in log.md with
what each carries. imp-tricks is still dirty, and still blocks decision 4.

Also records what staging turned up: the size jobs/ had reached, the
duplicate 148l.pdb fixture, and the stale quest/core_new/ snapshot the
index was holding from an abandoned rename.

Verified after the split: 350 passed / 4 skipped in arm64, 313 passed /
25 skipped in base plus the web backend - both unchanged from the
pre-commit baseline.
Decision 1 of four taken 2026-07-27. AV_BACKENDS becomes
("imp_bff", "labellib"); LabelLib stays as the fallback for platforms
without a compiled IMP.bff, Windows above all. The test class that
existed solely to assert LabelLib was first is gone - it did its job by
making the promotion deliberate.

Promoting it surfaced three defects that had been invisible for as long
as nothing entered the branch. None of them announced itself:

- The density grid was transposed. IMP numbers voxels with x fastest, so
  reshaping the flat tile values C-order into (nx, ny, nz) exchanges x
  and z. An accessible volume is globular enough that the transpose still
  overlapped the truth by 83% of its voxels, so the point count, the
  bounding box and the total volume were all right and only the shape was
  mirrored. Downstream the contact volume landed on the wrong side of the
  protein: quenchers in reach were not seen and donors came out
  unquenched.
- The grid origin was one voxel out. IMP sizes the map itself and the
  attachment atom is on the middle voxel only when the edge length is odd
  - it is 86 on the reference site. The origin now comes from the PathMap
  header. Relatedly _points_from_centred_grid uses the integer offset
  (ng - 1) // 2 so it agrees with _center_grid_indices; the float form
  put the point cloud half a voxel off the grid the quenching kernels
  index into, for even ng only.
- It was not thread-safe. A pooled scan - which the web backend runs -
  died in SWIG with 'XYZR_setup_particle' receiving a particle name,
  the same failure quest/core/pdb.py already locks the parse against.
  _IMP_BUILD_LOCK guards construction and extraction but deliberately not
  av.resample(): resample is 636 ms of a 662 ms build and the SWIG work
  is 22 ms, so locking the compute would cost a threaded scan nearly all
  its parallelism to guard the phase that was not failing.

The regression test for the first two is that AV(av_backend='imp_bff')
must reproduce build_imp_accessible_volume() exactly as a set. Comparing
counts or centroids passes with the grid transposed.

This changes what QuEst computes, by more than the 10% volume difference
between the backends. Over all 151 CB sites of 148l chain E under the
reference project, LabelLib leaves 43 labelable and 21 quenching;
IMP.bff leaves 147 and 60. Most of that is not the algorithm but
allowed_sphere_radius - IMP's is floored at 2.0 while LabelLib gets the
project's 0.5 - which is what decision 3 is about.

QUENCHED_SITES is re-picked from sites that quench under *both* backends.
Picking them from the IMP numbers alone turned the entire base
environment red: it has no compiled IMP.bff, so it runs LabelLib, and
this file tests seeded reproducibility rather than AV chemistry.

Verified: 350 passed / 4 skipped in arm64, 313 / 25 in base plus the web
backend.
Decision 2 of four. A bound ligand occludes the dye, so the reader passes
no selector and takes IMP's default - everything but waters and
hydrogens. _atom_record_selector is deleted. On 148l that is 1363 atoms
against 1322; the extra 41 are FGA, API, DAL, MUB, NAG and BME.

Both candidates were measured over all 151 CB sites of 148l chain E,
because the atom count does not decide this:

  ATOM records only (before)   1322   148/151 labelable   mean AV 9485 A^3
  no selector (IMP default)    1363   150/151             mean AV 9320 A^3
  NonHydrogen (waters in)      1503   148/151             mean AV 7900 A^3

Waters cost 17% of every accessible volume, and crystallographic solvent
is mobile on the timescale a tethered dye explores - treating it as a
rigid wall claims more than the physics supports. Rejected.

Admitting ligands cost 1.7% of mean volume and gained two labelable
sites. More obstacles cannot open a pocket; what changes is the grid IMP
sizes around the site, so a search that previously started nowhere can
succeed.

Two latent defects surfaced the moment HETATM records arrived, both being
code that had only ever seen standard amino acids:

- to_coarse raised KeyError: 'FGA'. Residues with no coarse template are
  now kept whole - coarse-graining reduces an amino acid to backbone plus
  CB and a bound sugar has no such reduction. Dropping them would have
  reinstated this same stripping one layer down.
- Every hetero atom was named 'HET:'. IMP names them 'HET: C1 ' and the
  atom_name field is |U5, so the real name was truncated away and all of
  them collided on one string. Atom names are how the quenching chemistry
  and the coarse templates identify atoms, so this was not cosmetic.

A third defect is recorded but deliberately not fixed here: coarse-grain
residue matching ignores the chain, so in any multi-chain structure one
chain's side chains are folded into another's CB. Fixing it moves
coarse-grained numbers and belongs in its own change.

Verified: 351 passed / 4 skipped in arm64, 314 / 25 in base plus the web
backend.
Decision 3 of four. DEFAULT_ALLOWED_SPHERE_RADIUS replaces QuEst's 0.5
default and the separate 2.0 floor IMP's path was clamped to. 2.1 A is
carbon's CHARMM radius: the attachment atom is a CB in every default
workflow, and the sphere that lets the linker escape it should be the
size of that atom. It also sits just above the cliff below which IMP's
path search starts nowhere and returns an empty volume without saying so.

The clamp survives for callers who explicitly ask for less, but it now
warns instead of substituting silently. A caller asking for 0.5 and
getting 2.0 never learned the number it reasoned about was not the number
that ran.

Effect, measured over all 151 CB sites of 148l chain E: IMP.bff goes from
147 to 151 labelable sites and from 60 to 67 quenching ones.

It also corrects a claim made when IMP.bff was promoted. That commit said
most of the 43-vs-147 labelability gap between the backends was this
parameter rather than the algorithm. It is not: raising the default from
0.5 to 2.1 left LabelLib at exactly 43/151, because calculate1R and
calculate3R accept a linkersphere argument and never pass it on -
ll.dyeDensityAV1 has no such parameter, and the wrapper zeroes the
attachment atom's own vdW radius instead. The real cause is that LabelLib
frees only the attachment atom, so a CB hemmed in by CHARMM-sized
neighbours has no route out, while IMP ignores every obstacle inside the
sphere. Only the IMP.bff backend honours this parameter at all, and both
the constant and the AV concept now say so.

Verified: 351 passed / 4 skipped in arm64, 314 / 25 in base plus the web
backend.
TestTheUpstreamAvMatchesQuEsts is the precondition for DUP-01 - deleting
imp_av.py and the AV class in favour of IMP.bff.av.compute.compute_av -
and it only ever compared the LabelLib implementations, because upstream's
imp_bff branch raised on every input. That branch is fixed in imp-tricks
(936a17c, b95c000), so the fixture is parametrised over both and the path
QuEst actually defaults to is pinned too.

Equality is to 1e-4 A rather than bit-exact, and the reason is worth
keeping in the test: on the imp_bff path the two sides reach the same
points by different arithmetic - QuEst rebuilds its cloud from the density
grid in float64, upstream takes IMP's get_xyz_density(), which has been
through float32. They agree to ~2e-6 A, a millionth of the 0.5 A voxel.
A real disagreement is a whole voxel, five orders of magnitude larger.

DUP-01 is no longer blocked on correctness, only on packaging: QuEst still
reaches imp-tricks through a path shim rather than a declared dependency.

Verified: 353 passed / 4 skipped in arm64, 314 / 27 in base plus the web
backend.
Decision 4 - imp-tricks as a declared dependency - is blocked on
packaging rather than correctness. Both of its AV backends now work and
are pinned against QuEst's, but imp-tricks is version 0.1.0 on a private
GitLab, absent from PyPI and conda-forge, and the two fixes are committed
locally and unpushed. Declaring the dependency would make QuEst
uninstallable for anyone who cannot resolve that name and would fail the
packaging job in CI, so the path shim stays until it is published.

Also corrects the state table. The previous handover recorded 16 passing
notebook and example tests; that total only ever came from running the two
files separately. As one selection, pytest tests -m notebooks gives 6
failed / 10 passed, from a dynamic-loader conflict in the child process -
pre-existing, verified in a worktree at 1807e72, and now written up rather
than carried forward as a number that no single command produces.

The browser and ChiSurf rows are marked explicitly as not re-run since the
AV changes, instead of being left to read as current.
Decision 4. It was blocked on imp-tricks being unpublished; the owner has
cleared that ("nobody installs quest, will make imp-tricks public later"),
so the declaration lands now and publication catches up later.

Declared in the two manifests that describe the *shipped* package —
rattler-recipe/recipe.yaml and pixi.toml — and deliberately not in
pyproject.toml. That is the convention `imp` already follows here: neither
is pip-installable, so a pip install of QuEst has never produced a working
science stack and this changes nothing about that.

The cost, stated rather than hidden: imp-tricks is on a private GitLab and
on no public channel, so the `py` solve-group no longer solves and all four
CI jobs go red — not just packaging. The constraint is `*`, so publication
alone fixes it, with no further change here. Local work is unaffected: the
arm64 environment reaches the checkout through quest/imp_tricks.py.
QuEst's own _density2points is deleted; density2points is now an adapter
over IMP.bff.av._kernels.density2points, which selects exactly the same
voxels (checked on 5x7x9, 8x8x8 and 11x11x11 grids) and additionally keeps
the per-voxel weight this signature discards.

Two preconditions the plan did not mention, both created rather than
worked around:

Upstream's kernels were bare @nb.njit. QuEst's carried cache=True and
nogil=True, and nogil is what keeps a threaded site scan -- the web
backend -- from serialising on the GIL inside the loop that visits every
voxel. Contributed upstream as imp-tricks a49e17c, so this is a swap and
not a regression.

Upstream enumerates ix outermost where QuEst enumerated iz, so the point
cloud comes back permuted. That is safe only because nothing reads
AV.points positionally -- verified against every consumer: the walk runs
on the density grid, _ran_dist samples at random, Rmp averages, and the
acceptor cloud is averaged in full. The only visible effect is .xyz row
order. TestDensityToPoints asserted a sequence and now compares sorted.

The import is lazy on purpose. At module scope it failed
test_importing_the_domain_does_not_import_qt and test_the_model_needs_no_qt,
via the documented dylib conflict rather than an ImportError: both probe
the domain layer in a subprocess, and importing IMP.bff.av reaches IMP.

arm64 353 passed / 4 skipped; base + web backend 314 passed / 27 skipped.
Both match the pre-change baseline exactly.
RDAMean and RDAMeanE now aggregate IMP.bff.av._kernels.random_distances
and IMP.bff.distance_metrics.av_pair_statistics; QuEst's _ran_dist is
deleted. On 148l E15/E90 the two agree inside their own sampling noise:
<R_DA> 55.164 +- 0.037 against 55.171 +- 0.032 A, <R_DA>_E 54.467 +- 0.022
against 54.473 +- 0.029.

It is 5x slower -- 3.4 ms a call against 0.68 ms -- and that is taken
knowingly rather than missed. QuEst's kernel was parallel=True; upstream's
is sequential because it seeds, and numba's per-thread RNG cannot be
seeded reproducibly, so determinism and parallelism genuinely conflict in
this kernel. A further 0.87 ms is widening (N,3) clouds to the (N,4)
weighted points upstream takes; QuEst's grids are binary, so those weights
are all ones. Acceptable only because this runs once per simulation, about
0.4 s across a 151-site scan. It must not be adopted on a per-frame path.

In exchange the result is reproducible, which it was not: the old kernel
drew from an unseeded per-thread RNG and gave a different answer every
call. The seed is a parameter now.

dRmp stays local. av_pair_statistics returns r_mp as a copy of r_da_mean
by its own admission, and R_mp cannot be recovered from a sample of pair
distances at all -- 47.65 A here against 51.53 A there. Only indices 0
and 2 are read, and dRmp's docstring says why.

What guarded these before was one assertion that a distance lay between
10 and 100 A, so TestTheAdoptedDistanceKernelsBehave is new. It uses
E55/E124: it first used E15/E90, the pair the spec quotes, and errored
under LabelLib, which labels 43 of 151 CB sites and not that one.

arm64 356 passed / 4 skipped; base + web backend 317 passed / 27 skipped;
16 GUI tests. Baselines were 353 and 314, plus the 3 new tests.
simulation_type="AV3" reached calculate3R, which was deleted in 7ecf209,
so it raised NameError: name 'calculate3R' is not defined -- from eight
frames inside a running simulation, naming nothing a caller could act on.
simulation_type is a project field a scan can set, so this was reachable
from configuration alone. Nothing caught it because no test set the field,
and reachability analysis cannot see a value-driven branch.

Neither backend has ever implemented AV3 here: the IMP.bff path ignores
simulation_type outright, and calculate1R drives ll.dyeDensityAV1. So the
honest fix is to say so rather than to grow a three-radius model. Validated
first thing in AV.__init__, before the structure is read.

okf/subsystems/accessible-volume.md said AV3 "only takes effect on the
LabelLib path". It took effect on neither; corrected there.

arm64 358 passed / 4 skipped; base + web backend 319 passed / 27 skipped.
The largest deletion QuEst had left. quest/core/imp_av.py (329 lines) and
calculate1R (69) are gone; AV.__init__ makes a single compute_av call with
an explicit backend=, so the choice is never implicit. QuEst keeps the
application half -- site selection, the cubic-grid and binarisation
adapter, and AV's derived grids.

The volumes are verified unchanged, which the suite alone would not show:
148l E36/E55/E118 under both backends give identical grid shape, identical
binary density voxel-for-voxel, and identical point counts -- 147011 /
164783 / 67889 (IMP.bff) and 172964 / 200283 / 91585 (LabelLib).

Adopting compute_av would have re-introduced the SWIG race QuEst fixed on
2026-07-27: it had no thread safety at all. The lock is contributed
upstream instead (imp-tricks 9cd5c13), keeping the construction-locked /
resample-unlocked split, because wrapping the whole call would cost a
threaded scan 96 % of its parallelism.

The two grid-origin conventions are preserved rather than unified. The IMP
path offsets by the integer (ng-1)//2, matching every grid kernel; the
LabelLib path places its cloud on the float corner. For an even ng -- 92
here -- those differ by half a voxel, so on that backend AV.points sits
0.25 A per axis off its own density grid. That is a real defect, but
unifying it moves LabelLib results and the correct convention is a
judgement call, so it is recorded in okf/references/known-issues.md for
its own change rather than altered inside a deduplication.

Tests that reached the deleted module are rewritten against AV itself. The
one asserting on the *source text* of build_imp_accessible_volume now
asserts on compute_av's signature: it cannot re-read a file because it is
never given a path.

arm64 358 passed / 4 skipped; base + web backend 319 / 27; 16 GUI tests --
all identical to the pre-change counts.
The handover now reads as the state after decision 4 and the deduplication
rather than before them: all four decisions closed, DUP-01/DUP-04 and half
of DUP-05 done, CI deliberately red until imp-tricks is published.

Three things worth a fresh session's attention are recorded rather than
left in the diff. That none of the three adoptions was a drop-in -- each
needed an upstream fix first (thread safety, njit annotations) or a
knowing trade (5x on the RDA sampling). That the DUP-* numbers this file
used did not match specs/assessment.md, which is authoritative, so two
commit messages are mislabelled. And that a concurrency or grid test which
has never been seen to fail is not evidence -- that has now cost time three
separate times here.
`simulate_project` wrote `Path.cwd()/jobs/<uuid>` whenever `save_outputs` was
true, and it was true by default. Every library call, notebook and forgetful
test therefore left a run behind: 2870 directories, 39 GB, in this checkout.

`save_outputs` now defaults to False, and an opt-in run with no `project_dir`
writes to the per-user artefact root rather than the working directory. The web
backend stops passing REPO_ROOT -- that is what steered browser runs into the
tree -- and reads its job list from the same root, so the list no longer depends
on where uvicorn was started. `quest jobs clean` drives `jobs.purge_jobs`, which
reports without deleting unless told otherwise.

Two suite tests asserted the old default. One of them would have gone vacuous
rather than red: it compared a "full" run against a "fast" one, and both would
now have been fast.

Also fixes `pytest tests -m notebooks`, which has given 6 failed / 10 passed as
one selection since 2026-07-26 and now gives 16 passed. The recorded diagnosis
was wrong twice -- it is neither base-versus-arm64 boost skew (it reproduces
with base retired) nor an ordering effect between the two test files (they pass
together). Importing ChiSurf sets DYLD_LIBRARY_PATH=<mambaforge>/lib into
os.environ; that is inert in-process but every subprocess inherits it, so an
arm64 child resolves libboost_filesystem against base's copy and `import IMP`
dies. It happens during collection, in the one module that calls
autoform_available() at import, which is what made it look like ordering.
Contained here with a collection hook; the real fix belongs in ChiSurf.

And deletes quest/utils.py, which nothing imported: a coverage hook writing
.coverage.* into the working directory, a set_search_paths left over from the
sys.path injection LAY-08 removed, and unittest monkey-patching for Python 2.6.
…se used to

The mambaforge base interpreter is retired. arm64 gains fastapi, uvicorn,
python-multipart, pytest-asyncio, httpx and playwright, so one command now
covers library, Qt GUI and web backend: 404 passed, 1 skipped, against 358 in
arm64 and 319 in base separately.

playwright has to come from pip. conda-forge's `playwright` is the Node
package; installing it leaves `import playwright` failing.

Four backend tests broke, correctly: they monkeypatched REPO_ROOT, which the
job routes stopped consulting when artefacts moved out of the checkout. They
set QUEST_JOBS_DIR now, and REPO_ROOT is gone.

Retiring base removes what enforced "keep the optional dependencies optional".
That rule held because base genuinely lacked Qt and a compiled IMP.bff, so
every fallback ran for free. arm64 has all three, so nothing would enter those
branches again -- which is the exact condition under which imp_av.py once
shipped a Model.get_particles() call that does not exist.
tests/test_optional_dependencies.py simulates the absence instead: backend
resolution with each capability flag forced false, and `import quest.core` in a
subprocess with PyQt5, qtpy, LabelLib or IMP blocked at import. Verified by
adding a module-scope `import IMP` to quest/core/av.py and watching it go red.

Base was not measured to be broken; its backend suite gave 16 passed on the day
it was retired. Recorded as an owner ruling, not as a measurement.
…ites

A green suite does not tell you an accessible volume is right. The AV grid was
once transposed x<->z and still overlapped the truth by 83 % of its voxels --
point count, bounding box and volume all correct, only the shape mirrored, so
the contact volume landed on the wrong side of the protein and quenching
silently vanished. Nothing in the suite noticed.

tests/baselines/{labellib,imp_bff}.json record what this tree computes for 148l
chain E sites 96/139/129 under each backend: grid edge, point count, occupied
voxels, x0, cloud centroid, per-axis extent, then the seeded simulation, the
E55-E124 distance metrics, one FRET run, and coarse-graining on both 148l.pdb
and mc4r_dimer.cif. generate.py rebuilds them after clearing the numba cache;
test_baselines.py asserts against them.

Centroid and extent are asserted separately from the sizes. A cloud shifted half
a voxel has the same point count, the same volume and the same bounding box --
only the centroid moves, and that is the defect the next phase fixes.

Verified by defeating it: adding 0.5*dg to the LabelLib x0 turns 7 tests red --
three centroids, three simulations, the FRET channel -- and leaves every imp_bff
case and every size check green.

The baselines caught two of their own generation bugs first. The initial run
produced identical simulation blocks for both backends while their volumes
differed by 7000 points, because simulate_project reads QUEST_AV_BACKEND from
the environment and ignores the backend passed to AV(); there is a canary test
for that now. And tests/__init__.py was briefly added so the generator could be
imported -- that makes tests/ a package and changes pytest's import mode for
every file in it, so it is loaded by path instead.

Also runs the two suites handover.md had italicised as unverified across two
sessions of AV changes: browser e2e 26 passed (under arm64 for the first time),
ChiSurf quenching_estimator 1 passed.
Five call sites converted between Angstrom and a voxel index, and they did not
agree. `grid_center_index` is now the single map: voxel i of a grid anchored at
x0 sits at x0 + (i - npm)*dg with npm = (ng-1)//2, and the inverse is
floor((p - x0)/dg) + npm.

The integer-versus-float choice was recorded as a genuine judgement call. It is
not one. On the IMP path x0 = grid_origin + npm*dg, so voxel i is at
grid_origin + i*dg by construction; the integer offset is the map that inverts
that, and integer voxel indexing cannot express the float corner. LabelLib
anchored x0 on the attachment atom, which is on a node only for odd ng.

Writing the test found a second and larger defect. After unifying the offsets,
asserting that the trajectory sampler and the kernels agree probe-for-probe
still failed: _center_grid_indices used int((p - r0)/dg), and int() truncates
toward zero, so a centre on the negative side of the anchor rounded up while
every other map rounds down -- one voxel per axis for half the grid, on both
backends, independent of parity. Its mirror was in _sample_grid_at_trajectory,
where np.trunc maps [-1, 0) to 0, so a dye one voxel below the grid was treated
as inside and read voxel 0.

The measurement that settles it: run every AV.points point through the kernels'
index formula and ask whether it lands on an occupied voxel of AV.density.
Before 37478/37859 = 98.99 % on LabelLib, 100 % on IMP; after, 100 % on both.

Numbers move on both backends and are re-baselined. Point clouds and all
distance metrics are unchanged -- only the registration of the stamped grids.
Corroboration: the two backends now agree far better, donor QY at E96 going from
0.089 vs 0.458 (5.1x apart) to 0.073 vs 0.085 (1.2x).

Three tests broke correctly and none was relaxed. test_grid_kernels' reference
implementation had copied the kernel's int(), so it was a self-comparison and
structurally could not see this; it derives the index from the specification
now. test_api asserts a tyrosine is in reach, and which sites qualify changed
wholesale -- rescanned residues 2..164 under both backends, intersection {97,
160}, old fixture E36 in neither. test_core_api required an overlap between two
stickiness spheres that no longer falls inside the contact volume; that factors
multiply is now asserted directly instead.

Also corrects the documented numba cache clearing: conftest imports ChiSurf,
which sets NUMBA_CACHE_DIR=~/.chisurf/cache, so under pytest numba reads and
writes there and not beside the source. 193 kernels were cached in it.
A PDB residue number restarts per chain, so `res_id` names a set of residues in
any multi-chain structure. Structure.residue_dict and residue_ids keyed on it,
so on tests/data/mc4r_dimer.cif -- where all 534 numbers occur in both chains --
each entry held two residues' atoms, whichever chain came second overwrote the
first atom name by atom name, and move_center_of_mass folded both chains' side
chains into a single CB.

known-issues proposed matching (chain, res_id) inside move_center_of_mass. That
function was a symptom: residue_dict was itself keyed on res_id, so the
CA/CB/C/N/H lookup tables, the sequence, the dihedrals and which CB a labelling
site attaches to all inherited the collapse. l_residue was also filled in
residue_dict order while move_center_of_mass indexed it by position in
residue_ids -- two independently constructed orderings that agreed only because
both iterated a set() of small integers.

The reader already held IMP's Chain, Residue.get_index() and
get_insertion_code() and wrote only a bare res_id. It now assigns a
residue_index per distinct (chain, res_id, insertion_code) in file order and
keeps the insertion code as a column; everything groups on that. The domain
still passes plain numpy arrays, so no IMP object crosses into it and the
lazy-import constraint stands.

Exactly one number moves, and it is the whole baseline diff: the dimer's CB
centroid, [-1.400, 2.220, 1.264] -> [0.733, -0.419, -0.851]. 148l.pdb is
byte-identical -- three chains, no colliding residue number, which is what makes
it the control. Verified by reverting the three groupings to res_id and watching
3 of the 7 new tests go red.

Closes DUP-06, which was filed "low -- a hazard that has not bitten yet".
fret.kappa2 is now an explicit project field -- template, parameter catalog,
German and French, Qt form, and validated to [0, 4] by validate_project before a
structure is even resolved. A project carrying kappa2 = 9 would otherwise have
multiplied the transfer rate by 13.5 and surfaced only as an implausible
efficiency.

The default changes no number by construction: a published Forster radius is
already quoted at the isotropic dynamic average, so 2/3 is neutral, and a test
asserts the rate arrays are bit-identical with and without the parameter. The
rate carries kappa2/(2/3), the literature's 1.5*kappa2.

DUP-03 said fret_rate_trace belongs with IMP.bff.cgdye.analysis.fret, "which
additionally handles kappa2". Both halves are wrong. calculate_fret_exact takes
an (nd x na) distance matrix plus transition matrices for both dyes, forms
np.kron(p_d, p_a) and solves a Markov kinetic ensemble -- a rotamer-library
formulation, O((nd*na)^2), against QuEst's ~1e5-point clouds. It is not a slower
drop-in, it is a different model at a different scale. And it does not compute
kappa2, it accepts a kappa2 matrix. The direction is reversed: QuEst's
trajectory kernel is the general one and should go upstream, like dRmp.

kappa2 cannot be modelled in the current dye model at all -- a structureless
point in a volume has no transition dipole to orient, and the upstream kernel
that does compute it takes the two dipole vectors as input. OBJ-01 item 6 closes
as chosen and documented, not as modelled, and the objective and the tests both
say so in as many words.

Also records OBJ-02's decision: the host route. QuEst contributes a model,
ChiSurf's optimiser drives it. Nothing implemented, but it already constrains
the facade work -- an optimiser calling simulate thousands of times must not pay
a numpy-to-list conversion per iteration.
… question

subav is now a thin adapter over IMP.bff.av._kernels.split_av_acv and the
93-line _subav kernel is deleted; dRmp delegates to
IMP.bff.distance_metrics.mean_position_distance, which was contributed from it.

Neither was a drop-in, and both needed fixing upstream first (imp-tricks 0.2.0,
3dbefc8). split_av_acv carried the float corner and the truncate-toward-zero
defects this tree was fixed for two commits ago, so adopting it as written would
have reintroduced both wholesale; and it returned float64 masks, 12.5 MB per
site at ng=92 against 1.6 MB. av_pair_statistics returned <R_DA> under the name
R_mp, and its test asserted r_mp == 50.0, passing only because of the bug.

All numbers unchanged: the kernel-equivalence tests and both baselines pass
untouched. That is the point of verifying a kernel before adopting it rather
than after.

HAS_IMP_BFF now asks both halves of the real question -- the compiled
IMP.bff.AV class and an importable IMP.bff.av.compute_av that drives it.
hasattr(IMP.bff, "AV") was the compiled half only, so an environment with a
compiled IMP.bff but no imp-tricks reported the backend available and failed on
first use. compute_av is imported rather than probed, because IMP.bff.av is a
namespace subpackage spliced in at runtime. Closes DUP-02.

Also corrects environment.md, which claimed `import quest` bridges an
imp-tricks checkout and named a module DUP-01 deleted. It does not: quest.core,
quest.core.av and quest.api do. That error had already cost something -- "fix
the IMP.cgmol import" entered the plan as an upstream defect, from a probe that
imported quest.imp_tricks without calling enable_imp_tricks(). There was no
defect.
Phases 0-7 are done and every row of the state table was measured today; the
two italicised 'unverified' rows are gone. Records what is left in dependency
order, what still needs a decision, and four traps learned or corrected today --
the numba cache location, a reference implementation that copied the code under
test, the fifth test found pinning the bug it should have caught, and a probe
that blamed the wrong repository.
tpeulen added 29 commits August 7, 2026 13:57
- Replace custom web-like QSS dark theme with standard native Fusion/OS Qt style in quest/gui/theme.py
- Refactor InTreeCollapsiblePanel in quest/gui/autoform_panel.py to use native checkable QGroupBoxes
- Update pyqtgraph plot canvases in quest/gui/plots.py to standard clean light desktop rendering
- Standardize toolbars, menus, and action buttons across QuEstWindow and TransientDecayGenerator
…or Web UI and PyQt

- Add shared tour definition in quest/settings/tour.json and tour loader in quest/tour.py
- Add /api/tour and /api/demo-project endpoints in FastAPI backend
- Integrate 148L demo mode project loading in Web UI AppLayout when Tour button is clicked
- Add Demo Mode button and GuidedTour controller in PyQt desktop workspace (using chisurf.gui.widgets.tools.guided_tour or InTreeGuidedTour)
- All unit tests passing (5/5)
`.gitignore` carried the standard Python-packaging block, in which `lib/` is
unanchored. It therefore matched `webui/frontend/src/lib/` as well, and three
frontend sources were never committed:

  autoformPaths.ts   spec `attr` -> nested project path (the 2026-08-09 fix)
  parameterCatalog.ts
  projectHelpers.ts

The web UI imports all three, so a clean clone could not build the frontend —
the working tree was the only copy. Anchoring `/lib/` and `/lib64/` keeps the
Python build directories ignored (neither exists today) without reaching into
subdirectories that happen to be named `lib`.
…face

IMP's selector already drops waters, but nothing guaranteed it: a caller that
passes a different selector, a future parser, or the ChiMol viewer (which loads
with keep_water=True) each reached a surface with `HOH`/`WAT`/`TIP3`/`SOL`
atoms in it. Waters then showed up as labelable residues, and a dye could be
attached to one.

`_WATER_RESIDUE_NAMES` names the residues, `strip_water_pdb_text` filters raw
PDB text by columns 18-20 so a viewer can be handed a clean file, and
`_strip_waters` runs after the IMP parse. `structure_metadata` filters PDB text
through the same function and skips water rows while parsing CIF, so the
pdb_text it returns to the browser viewer carries none either.

Baselines move accordingly: 148L atom_count 1525 -> 1385, chain E residues
291 -> 164. Both assertions updated rather than relaxed.

tests/test_water_stripping.py covers the filter directly.
Suite: 695 passed, 1 skipped (library + Qt + web backend).
…d requests

Two defects in the ZMQ server, plus the test coverage that would have caught
them.

`process_message` returned the dispatcher's envelope untouched, so any handler
returning a native object (a numpy array, a DecaySimulationResult) raised
inside `json.dumps` and the caller got nothing back. The transport is the
boundary the contract names for `to_payload`, so it converts there — and only
when the envelope actually carries a `result`, because a failure envelope has
no result and should not be given a null one. `serve_one` no longer lets an
unhandled error kill the loop without a reply.

`invalid_request` joins ERROR_CODES rather than being folded into
`operation_failed`. A malformed envelope — unparsable payload, or no method
named — is the caller's fault, and a host reacts to it differently from an
operation that ran and failed. `service_error` validates codes against the
table, which is why the two call sites had been silently downgraded.

New tests: test_rpc_methods.py (every registered method through the in-process
transport), test_zmq_rpc.py (every read-only method over a real REQ/REP socket,
plus the wire protocol), test_api_facade.py (every public verb on quest.api).

Suite: 695 passed, 1 skipped.
…ny more

Both presentation surfaces now render one declarative view spec: the desktop
renders quest.view.json through ChiSurf's AutoForm, the web renders the same
spec from /api/autoform-spec through components/autoform/. Adding a parameter
is a one-place edit in parameter_catalog.json instead of three hand-synchronised
surfaces — the web was the one that fell behind (fret.kappa2 shipped with two
of three wired).

AppLayout drops its three-way ternary and the ~540-line legacy block; only the
JSON overlay toggle remains. The scan residue picker and Calculate Scan button
moved to a Scan Settings card in app/scan/page.tsx so that workflow survives.

The migration exposed a real bug: a view-spec `attr` is the flat
ProjectFormModel attribute (attachment_chain, fret_enabled, fret_R0), not the
project path (attachment.chain, fret.enabled, fret.R0_matrix[0][1]). The web
read and wrote the flat keys, which the backend rejects — FRET never seeded its
acceptor and ran as a one-dye donor simulation that looked like it worked.
autoformPaths.ts maps every attr to its project path; fret.enabled routes
through setFretEnabled (which seeds the acceptor dye and R0) and
fret.R0_matrix.0.1 through setForsterRadius, a list cell setNested cannot index.
test_autoform_blueprint.py fails if the catalog grows a parameter the map does
not cover.

Browser e2e rewritten for AutoForm selectors: _use_legacy_form is gone,
_autoform_field/_autoform_input/_autoform_check locate fields by label, and
_server_is_up waits 10 s (a cold Next.js dev server takes >3 s, which had been
silently skipping the whole suite).

Browser e2e: 54 passed (measured 2026-08-09, both servers up).
Picking a labeling site meant typing a chain and a residue number into a form
while looking at the structure beside it. Now a click on the 3D structure or
the sequence bar sets the attachment, on the desktop and in the browser alike —
the divergence AGENTS.md warns about is the reason both halves are in one
commit.

Desktop: ChiMolStructureWidget embeds ChiSurf's SequenceDock next to MolView
rather than carrying a second sequence widget (okf/subsystems/chisurf-widgets.md
records the mapping of what to embed instead of rebuild), and donor/acceptor
toggle buttons say which attachment the next click sets. Web: ProteinViewer
wires NGL's picking proxy to the same onResidueSelect the sequence bar calls.

ProjectFormModel gains auto_attach_to_structure, run when a project is loaded:
the template's default site (chain A, residue 1) frequently does not exist in
the structure just opened, which left a project that validates and cannot run.
It only overwrites an attachment that is missing or invalid, prefers CB/CA/OG/SG
and never selects a non-standard residue — a water could previously be chosen.

structure_label is a derived read-only property so the dock's structure display
and the file picker are no longer two controls bound to `pdb`; the drift guard
test_each_scalar_field_appears_exactly_once passes again. View specs
regenerated for all three locales, and the web.guide key the Guide button
references added to en/de/fr.

Suite: 695 passed, 1 skipped.
…he RPC fixes

The bundle described the AutoForm migration and the desktop viewer work as
landed while the tree held all of it uncommitted, and its headline sentence
said everything was committed and clean. Both are true now; the handover opens
with what actually changed and the library baseline is re-measured at 695
passed, 1 skipped.

Adds the trap that cost the most here: an ignored file never shows in
`git status`, so a clean status is not evidence that the tree is committed.
`git status --ignored=matching --untracked-files=all` is the check.

CLAUDE.md was renamed to AGENTS.md (the rename itself rode along in 74d7421);
its content update states the rule that a feature reaching only one of the two
surfaces is a bug.

okf_validate: 0 errors, 0 warnings; 20 with --check-paths (was 21).
The raw transcript of the session that produced the ChiMol viewer and guided
tour work. It is not a knowledge artefact — okf/ is — but it is the only record
of the reasoning behind commits cd0d058..d3dc195, so it is kept rather than
discarded. Checked for credentials before committing; the only "token" matches
are CSS design tokens.
Step 2 of the change-tracking loop had been skipped while both surfaces
changed. webui.md still described a hand-written React form, a labeling page
carrying the FRET panel, a backend importing quest.cli's private helpers
(LAY-01, removed in phase 8), and data-testid="tour-btn"; its route list was
five routes short and its "Design language" section appeared twice verbatim.

Corrected against the tree, not the prose: the generated sidebar form and why
lib/autoformPaths.ts is load-bearing, the two fields that bypass setNested,
click-to-select on both surfaces, and the browser-suite locators with the two
mechanical traps (10 s _server_is_up, hidden checkbox behind a styled toggle).

gui.md gains structure_label, auto_attach_to_structure and the
ChiMolStructureWidget embedding, and loses two sentences that still assumed the
retired base environment — one of them recommending -p no:pytest-qt, which the
handover lists as a trap.

One finding is a gap rather than a stale sentence: LAY-07 is closed and
quest/tasks.py owns the one mechanism, but ProjectFormModel.run_simulation
still runs on the GUI thread instead of registering a job, so a desktop run
blocks the window and cannot be cancelled. Recorded as current state.

okf_validate: 0 errors, 0 warnings; 20 with --check-paths.
…26-08-06

They were flagged because the refactor moved everything they describe and only
their paths had been corrected, never their prose. Read against the code, with
every symbol resolved by hasattr rather than by eye:

  core-api.md            clean; DecaySimulationResult's fields match the
                         documented list in both directions
  structure-io.md        clean, including its removal claims — every name it
                         says is gone was checked to be absent. Its water
                         paragraph described one filter where there are three
  simulation-pipeline.md clean; gained the implicit stage 0, a water-free
                         structure
  surfaces.md            the one that had rotted: Qt is imported in nine files,
                         not "exactly three", all still under quest/gui/. The
                         rule survived, the count did not

Also corrected in surfaces.md: two sentences asserting the checked environment
has no PyQt5, untrue since the base environment was retired.

The lesson is which concepts got flagged. Three of the four were fine; the two
that were actually stale (webui.md, gui.md) were not on the list, because
nothing structural had moved under them — the surfaces simply changed behaviour
while their concepts sat still. Only the first kind of rot is visible to a path
checker.

okf_validate: 0 errors, 0 warnings; 20 with --check-paths.
LAY-07 closed on 2026-07-28 by building quest/tasks.py — one observable,
cooperatively cancelable mechanism for long work. The desktop went on calling
simulate_project on the GUI thread anyway, so the surface that motivated the
mechanism was the one not using it: the window froze for the whole run and
nothing could stop it.

ProjectFormModel gains start_simulation (registers a job, spawns the worker,
returns the job_id), wait_simulation(timeout), poll_simulation and
cancel_simulation. run_simulation is those four joined, so headless callers
keep the synchronous contract — including raising — and there is still one code
path rather than two that can drift.

The worker never notifies. A listener redraws widgets, and touching a widget
off the GUI thread is undefined, so the worker only stores its outcome and
poll_simulation applies it; test_the_worker_never_notifies records the thread
every listener fires on. TransientDecayGenerator.update_all pumps the event
queue between joins and ignores re-entrant clicks, which pumping makes
possible, and a Cancel button is enabled for the duration.

Two limits are in the code, not left to be discovered: the trajectory kernels
are njit(nogil=True) so the GUI thread does get the interpreter back during the
dominant cost, but the AV grid kernels are parallel=True without nogil and
still hold it — a smaller freeze, not none; and cancelling a single simulation
cannot interrupt it, because _simulate_traj is one njit call and a jitted
kernel cannot poll a flag, so Cancel discards the result rather than stopping
the work. A scan, which checks between sites, does stop.

Clicking the run button on a broken project no longer raises out of the Qt
slot; the failure is reported in the status line.

Suite: 706 passed, 1 skipped (11 new tests).
… anything

`default` and `desktop` could not solve, so `test` and `gui` had been red for
long enough to train people to ignore the light. Two causes, not the one that
was known:

- imp-tricks is on no public channel (owner ruling: it stays local). It is now
  a feature *no environment uses* — anything requiring it would take pixi lock
  down with it — one word in [environments] from being switched on the day it
  is published.
- labellib is not on conda-forge either, and on the owner's channel the `main`
  label stops at the 2020 builds: py37-py39, pinning numpy <2 against this
  project's numpy >=2.0. The numpy-2 builds are on the channel's `dev` label,
  for linux-64, osx-arm64 and win-64 only. [feature.py] points there and
  restricts itself to those three platforms, which is exactly the CI matrix.

pixi lock now solves all four environments.

The cost is measured, not assumed. Both AV backends dispatch through
IMP.bff.av, so "LabelLib is the fallback" never meant an AV could be computed
without imp-tricks: with IMP_TRICKS_SRC pointed at nothing, 98 tests failed and
8 errored, including the labellib baselines. Turning that into signal takes two
mechanisms in conftest, because the failures arrive two ways — hookwrapper
hooks rewrite QuEst's own sentinel message into a skip (most of the suite, no
annotations), and @pytest.mark.needs_av marks the tests that swallow the error
and assert on a status instead. Neither converts anything where imp-tricks is
present, so a real regression cannot be laundered into a skip.

Without imp-tricks: 550 passed, 141 skipped, 0 failed. Twelve of those skips
are over-skip from marking by module rather than by test, recorded as such.
With it, unchanged: 706 passed, 1 skipped.

What CI covers now is the contract, the project schema, the RPC envelopes,
i18n, the view spec and the form model. What it still cannot cover is the
simulation path; publishing imp-tricks remains the only thing that would.
… check

The two surfaces draw the same structure with two different engines — chimol on
the desktop, NGL in the browser — which is the two-implementations shape the
AutoForm migration removed from the forms. ChiSurf's chimol → WebGPU work exists
to close it with one WGSL source and a driver each side.

Checked 2026-08-10 and not yet actionable: the desktop WGSL renderer runs, the
browser/JS driver has not been started. Points at ChiSurf's agent board and
okf/plugins/chimol-web.md rather than restating a status that will go stale
here.
…rt is done

Upstream deleted chimol's OpenGL renderer (chisurf 76df2fcfd), so the browser
half is next and its handover says it has not been started. Writing down what
NGL actually does in ProteinViewer.tsx — loadFile, three representations, a
selection language, autoView, resize, and atom picking — states the first
consumer's requirements before the driver is designed rather than after.

Picking is called out because it is the one upstream still lists as GL-only,
and it is the feature QuEst's click-to-select depends on.
…t core

Found while checking ChiSurf's agent board for the chimol browser work.
ChiSurf ruled on 2026-08-10 (a2ff49665) that the labelling plugin — the
LabelStructure widget that owns the fps.json payload this row is about —
belongs in ChiMOL: placing a dye on a structure is structural work done while
looking at the structure. It stays in modelling/ only until ChiMOL hardens.

That resolves the "where" this row was holding open (neither chisurf/core nor
here) and leaves only "when". It also names a constraint QuEst should not break:
the fps_json_payload getter/setter seam is what the eventual move rides on.
Found on ChiSurf's board while checking the chimol browser work. IMP.bff's C++
fps.json reader (AV::set_av_parameter, AV.cpp:208-210) overwrites radius2 and
radius3 with radius1, so every three-radius AV built through it is silently a
one-radius AV. The fix belongs in imp.bff.

QuEst is unaffected, checked rather than assumed: it passes dye_radii straight
into IMP.bff.av.compute_av from Python and never reaches that setter, and it is
AV1-only, so radius2/radius3 reach no kernel that reads them.

Recorded anyway because quest/core/fps_json.py *writes* those two fields: a
reader carrying this bug discards them without saying so, and the symptom would
look like a QuEst export bug.
…view

ChiSurf classified its av.py (the LabelLib/IMP backend selection) and its
fps.json round-trip to move into imp.bff, on the same rule this spec states.
Its own PRD counts three accessible-volume paths landing in one repository as a
result; QuEst's resolve_av_backend is a fourth copy of that same choice.

Recorded as a watch item, not work: QuEst already dispatches through
IMP.bff.av.compute_av, so its selector only adds the QUEST_AV_BACKEND
preference plumbing and its messages. When PRD-97 stage 2 lands, the question
is whether QuEst keeps a selector or passes backend= through. Until then, do
not add a fifth path.
FPSIMP renamed CLAUDE.md to AGENTS.md today, so the family convention is
uniform: a root AGENTS.md in every repository, no per-tool instruction
file. AGENTS.md now links ../fpsimp/AGENTS.md and FPSIMP's
okf-conventions.md -- the canonical copy of the rule set both bundles
share -- and says which validator copy is canonical. One passage in
programme-2026-07.md named the root CLAUDE.md; it names AGENTS.md now
and dates the old name, since the reversal it records is about the
environments and not the file.
Owner's rule, 2026-08-11: a simple method reachable only through a
complex package gets transcribed and A/B'd against the reference, not
depended on. Recorded here because it reads as a reversal of QuEst's
standing 'prefer depending on IMP.bff' instruction if met without the
scoping.

It reopens nothing: the question is asked when a dependency would be
added, and QuEst already depends on IMP.bff for compute_av, so the five
smaller kernels adopted beside it are settled.

Also notes that imp-tricks handed IMP.bff to ../imp.bff on 2026-08-10,
so quest/hosts/imp_tricks.py names a checkout that no longer carries
it.
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