Independent review of the adapter resolution work found six defects, all
real and all fixed here.
A kind whose only candidates were rejected had no winning path, so its row
carried path=None and the rejected candidate fell into the shadow loop: the
report said "another adapter of this kind wins for the current mount" while
nothing won, and the real deferred reason was buried. A shadow row now
requires a winner and names it.
"loaded" was backed by a process-global record, so a fresh runtime with no
model still claimed a live adapter. The record is now held by the instance
that attached it, and the report keeps a weak reference: while the model
lives the row says loaded, once it is gone the row says available and
explains that the model is gone.
The sort key had lost the manifest's cartridge tag, which changed which
adapter loads on any machine holding a tagged library copy, and neither the
commit message nor the checklist said so. The tag is back as the middle key.
Proof checks resolved through the library while generation resolved through
the mount, so one proof payload could name two different copies. Both now
resolve the way generation does.
loaded_at was written and never read, so it is gone. The checklist claim that
peft is lazily imported in one place was true only for generation and is now
scoped that way.
The four generation paths and status() had no test coverage, so "no function
was lost" rested on reading. One test now walks the transformers prompt path
end to end with stub weights and asserts the generation answer carries the
adapter it attached.
pytest -q: 225 passed. ruff clean. mypy on the three touched source files:
2 errors, the identical 2 on a stashed HEAD tree, net zero. pyright: 2
errors, both the known qwen_vl_utils tuple artifact, unchanged from HEAD.
runtime.py resolved adapters at 7 call sites and only ever looked in the
adapter library, so an adapter attached to a cartridge (cartridge.py:230
copies it into <cartridge>/adapters/{kind}/) was never loaded by anything.
Resolution now lives in adapters.py: it reads the mounted cartridge set,
prefers a mounted cartridge's copy over the library, and answers every one
of the six kinds with a state the facts support — loaded, available,
deferred (base_model mismatch or missing weights, both models named in the
reason), missing — plus a shadowed row for each adapter that lost the
choice. A mounted cartridge is the scenario selection; neither manifest
carries a scenario field, so none was invented.
Two bugs the new tests caught: the preference sort lost reverse=True, so a
library adapter beat a mounted cartridge's copy, and the load branch had
never been executed by a test. Stub weights now prove peft is asked to
attach, that the returned model is put in eval mode, and that the endpoint
reports loaded until unload clears it.
Faces: GET /api/runtime/adapters, `ely-eye adapters`, and a log line naming
the reason whenever a generation runs without an adapter.
Gates: 222 passed; ruff clean; mypy cold cache on the 7 touched files gives
the same 2 errors as HEAD's runtime.py, and pyright gives the same 2 as
HEAD — both are qwen_vl_utils typing artifacts (process_vision_info returns
2 or 3 values depending on return_video_kwargs; the product takes 2).
The suite had fifteen checks that could not lose. The visual HashHop arena
painted a cyan line and a white ring on the answer slot, then asked the model
to find the answer by looking; the collector re-rolled puzzles until the
running rate crossed its target; the cache check passed if any layer had ever
recorded an entry; the repository check passed on a call graph built in memory
without touching the store; the research-module check asserted non-empty lists
of SHA256-derived placeholder vectors and so could not fail.
Each check now answers for one claim and reports a verdict the reader can act
on: passed, failed, deferred, or removed. A deferred row names what is missing
and the command that would settle it; a removed row says why the old claim was
withdrawn. The suite verdict is failed > deferred > passed, so a claim that
never ran holds the whole report back from saying "proven".
- evals.py (2427 lines) becomes the proofs/ package; largest module 445 lines.
- proofs/__main__.py deleted: it duplicated PRDProofSuite.latest() with a
different glob. proofs/table.py renders; latest-proof-suite is the one face
and exits 1 on a failed verdict.
- The arena paints nothing. The puzzle id is printed on its tile instead,
because removing the ink without it removes the question.
- HashHop runs a fixed 8 puzzles per depth instead of re-rolling to target.
- The contradiction corpus moves from code to contradiction_scenarios.json;
the check stays deferred while the corpus is the one this repo authored.
- The cache check compiles a nonce question over a real atom twice and reads
the miss to hit movement; an empty store fails instead of agreeing by luck.
- The repository check requires rows in the store; the research-module check is
removed.
Two product bugs surfaced on the way:
- text_index.fts_query joined Latin terms with a space, which FTS5 reads as
AND, so a question longer than its answer retrieved nothing. Terms are ORed
and bm25 rank orders the candidates.
- profiles.windows_path_to_wsl prefixed /mnt/ onto an already-POSIX path with
an empty drive, producing /mnt//home/... and failing every launcher probe.
Verification: 168 passed (pytest backend/tests bench/tests); ruff clean on the
proofs package, cli.py, text_index.py, profiles.py, and both test files; mypy
clean on 16 files with a fresh cache; pyright reports 0 errors on proofs/ and
the test file. Ruff debt elsewhere is unchanged: 19 errors in seven files this
commit does not touch, measured against the same files at HEAD.