README.md is the user’s document and the PyPI page. This is the rest: why
the commands are shaped the way they are, the logs that back each claim, what each one
caught, and how a release is cut. Nothing here is needed to use the tool.
cli/ inside the zoo, alongside the conversion/ scripts and the knowledge/ notes the
rules are transcribed from. That is deliberate and reversible:
knowledge/ transcribed. In one repo a knowledge update and the rule it
implies are one commit. Across two repos the table silently falls behind.conversion/zoo_convert.py doctor was already here and checks the environment. Two
commands named doctor in one workflow is a defect, so they were reconciled rather than
left to coexist: coreai_doctor.py --env runs the same overlay probe, making the artifact
lint a superset, and zoo_convert.py doctor now points at it.export, not doctor. The zoo already carries
traffic, llms.txt and the AIO surface.Path resolution is location-independent — find_zoo_root() walks up for the directory
holding both models/ and conversion/, and falls back to the sibling layout — so lifting
cli/ out into a standalone repo later is a directory move and nothing else. Do that when
export routes beyond the set Apple and the zoo already cover; until then a standalone repo
would be a thinner front door than this one.
The wheel is stdlib-only for local targets and ships these files plus the dated zoo-routes
snapshot (coreai_zoo_routes.py, regenerated by make_zoo_routes.py), nothing else.
python3 cli/release.py 0.2.0 --ios-build 24A353 --dry-run # every step, no changes
python3 cli/release.py 0.2.0 --ios-build 24A353 # the release
One command: regenerate the routes snapshot, run selftest.py, read the host (sw_vers,
Xcode, coreai-build, the runtime wheels, the phone’s build from --ios-build), retitle
CHANGELOG.md’s [Unreleased] section and stamp its Validated on line, update the same
line in README.md, bump pyproject.toml, build the wheel and sdist, twine check. It
refuses to stamp an Apple seed build (26A5xxx) as validated unless told --allow-beta,
because a release that says “validated on macOS 27” has to have been validated on the
release OS. Commit, tag and twine upload are printed, not run — the upload is a public
act. Keep CHANGELOG.md’s [Unreleased] section current as changes land; the release only
retitles it.
export — how the numbers were establishedThe zoo number was 13 until the routing pass. Not because 42 models were unroutable —
because recipe.toml records hf_repo, which is the output repo, and only 13 recipes
happened to name the model they convert from. The other 42 record it somewhere else: the
export script’s argparse default, or the model card’s upstream link. Reading those three
places instead of one closes it. The durable fix is a source_hf_id field in the zoo’s
recipe schema; this reads around its absence rather than editing that repo.
The iOS cliff is a real raise site: raise ValueError("Model 'gemma3' does not support iOS
variant") at export/pipeline.py:150, reached only after AutoConfig has read the
checkpoint. export is convert-only — no AIModel.load, no SpecializationOptions
anywhere in export/pipeline.py — so it does not contend for the exclusive GPU. Running the
bundle does, which is why verify checks the lock and export does not.
| log | what it shows |
|---|---|
logs/export-routing-cases.txt |
seven targets covering every branch: preset (exit 0), generic-with-iOS (0), zoo ×2 (0), iOS-cliff (2), silent-preflight (1), no-route (2) |
logs/export-commands-resolve.txt |
the emitted commands run through the exporter’s own --dry-run and resolve; plus the pipeline.py:150 raise site behind the BLOCKED claim |
logs/end-to-end-qwen3-0.6b.txt |
export --run producing a real bundle, which doctor found clean and verify gated 16/16 |
verify — what it caught on day oneThe canonical gate prompt — "The capital of France is", the one the zoo recommended and
shipped as the default — fails its own margin rule at n=16 on Qwen3-0.6B: positions 1
and 5 sit at 0.0885 and 0.0041. It is deterministic at the first token and not over a
16-token continuation, because after “Paris.” the model free-runs into a list where the next
country is a near-tie. The recommendation and the margin rule were both in the notes and
had conflicted at n=16 for as long as both existed. Nothing surfaced it until a tool checked
the prompt instead of trusting it.
Fixed 2026-08-01 in both coreai_verify.py and the zoo’s conversion/coreai_gate.py, by
measurement (the table in the README). Note the old default is not broken everywhere —
gemma-3 clears it comfortably — which is what made it survive: whether it gates or silently
refuses depends on the model under test. Reciting a fixed sequence holds because there is
nothing to free-run into once the answer is given; counting drifts once the numbers get
long enough to admit a second plausible formatting.
coreai_gate.py covers 7 zoo families and cannot gate a stock-recipe bundle at all; the
stock backend is that gap.
| log | what it shows |
|---|---|
logs/end-to-end-qwen3-0.6b.txt |
export → doctor → verify on one model: real bundle, lint clean, 16/16 token-exact against the fp32 oracle |
logs/verify-validation.txt |
the rejected canonical prompt (exit 3), the full passing run (exit 0), and the zoo delegation |
logs/verify-qwen3-0.6b-transcript.json |
the transcript: input ids, both sides’ output, per-step margins, verdict, environment |
logs/selftest.txt |
the verdict rule, both sides of the margin floor |
doctor — the sweep, the false positives, the catchesA sweep over all 90 local bundles reported findings on 41. Every finding class in that
sweep was hand-verified against the artifact before the README claimed anything; the false
positives found on the way (a vision encoder held to the LanguageBundle contract, a ship
manifest that merely shares the name metadata.json, a Jinja template that renders
eos_token mid-expression, and the working div(x, 2, rounding_mode="floor") form) are
fixed and covered by fixtures. A lint that flags the documented workaround is worse than no
lint.
First real catch: three published repos. The sweep found EOS-NOT-EMITTED-BY-TEMPLATE
live on Hugging Face — gemma-3-4b-it-CoreAI-official, gemma-3-12b-it-CoreAI-official
and functiongemma-270m-coreml all declared eos_token: "<eos>" (id 1, document end) while
their chat template ends a turn with <end_of_turn> (id 106, and upstream
generation_config.eos_token_id is [1, 106]). Any runtime that derives its stop token
from eos_token alone — swift-transformers does — generated to the token cap. Fixed
2026-07-31 by logs/fix_gemma_eos.py (one field, byte-range replacement, verified on
re-read); logs/audit_gemma_eos.py re-swept all 18 published Gemma tokenizer configs
afterwards: none fire.
Worth noting what the audit also corrected. GEMMA4_12B_STATE.md had warned since July
that the published Gemma-4 12B/31B bundles still carried the old <eos>; they did not — the
note was stale, and acting on it would have been wasted work. Reading the artifacts beat
reading the note about the artifacts.
The same sweep flagged CHAT-TEMPLATE-MISSING on the four legacy
gemma-4-E{2,4}B*-coreml ports, which shipped a tokenizer and no template — a runtime
applying one had nothing to apply and fell back to raw completion without a word. Fixed
2026-07-31 by logs/fix_gemma4_coreml_chat.py, which ships google/gemma-4-{E2B,E4B}-it’s
own template verbatim. The two repos that still report no chat template are the
embeddinggemma repos — an embedding model has no chat surface, which is the rule reporting
correctly, not a gap.
The host-build rules (0.2.0), and the premise that did not survive measurement.
IR-040-DEBUG-LOC said a 0.4.0-era asset “loads on beta 1 and nowhere after”, and the
0.2.0 kickoff asked for it to be relaxed because Apple’s OS 27 beta 5 release notes list the
incident (177008303) as fixed. Before relaxing it, the zoo’s new load sweep
(conversion/zoo_smoke.py) downloaded a published 0.4.0-era bundle
(qwen3.5-0.8B-CoreAI/gpu-pipelined/qwen3_5_0_8b_decode_int8hu_perchan_sym) and loaded it
on macOS 27 26A5416b with coreai-core 1.0.0b2: AIModel.load aborted with the July
signature (expected AICode versioned location → LLVM ERROR: cannot unwrap empty
odiec_module_t), and xcrun coreai-build compile (3600.82.1) failed identically while
compiling the bundle’s b2 twin fine. The release note was not a measurement. So the rule is
now host-conditional in the other direction: info on beta 1, fatal on every build from
beta 2 on, and a release build inherits nothing — IR040_MEASURED_OK_FROM in
coreai_doctor.py names the first build a sweep measures to load such an asset, and
stays None until one does. The same producer audit (--all --stamp-only) found 34
published bundles still carrying 0.4.0-era IR that the July recovery left in place because
the catalog did not serve them; they are the strip_debug_info queue.
AOTC-STALE-TOOLCHAIN was wrong in the other direction: it called any AOT artifact older
than the installed toolchain fatal, which is every published h18p artifact (compiled on
beta 3, loading on every later device build). The incident it encodes (181264112) is
specifically a beta-2-or-earlier coreai-build, so that is now the fatal case; merely
older than the installed toolchain is info, and whether a release toolchain still
specializes a beta-3 artifact is a device check, not a lint.
| log | what it shows |
|---|---|
logs/case-a-known-broken.txt |
a 0.4.0-era bundle: 1 fatal, 1 runaway, exit 2 |
logs/case-a-ground-truth-load-abort.txt |
the same asset actually aborting at AIModel.load, so the fatal is not an assertion |
logs/case-b-known-good.txt |
the device-gated nanbeige4.2-3B ship bundle: 0 defects, 4 ship requirements, exit 0 |
logs/case-c-source-lint-rf-detr.txt |
the source lint over stock transformers RF-DETR, independently re-finding the patterns that port hit |
logs/case-d-checkpoint-wna8o8.txt |
the Gemma-4 mobile QAT checkpoint, flagged from its safetensors headers before any export |
logs/case-e-published-gemma3-eos.txt |
the eos rule swept across all 18 published Gemma tokenizer configs, after the fix — none fire |
logs/selftest.txt |
18 fixture checks over 16 source rules |
eval — validationEnd to end on qwen3-0.6b (4-bit, macOS bundle, llm-runner), GSM8K, --thinking off:
| check | result |
|---|---|
| same settings twice | every row identical, delta B - A = +0.0%, protocol matched |
--max-new-tokens 512 vs 256 |
REFUSED, naming max_new_tokens |
| halving the budget | truncated items 1 → 2, as it should |
The accuracy itself was 0/10, and the interesting part is why the tool says so: 1 item
ran out of budget and 7 finished without the marker. Qwen3-0.6B answers in \boxed{0},
not #### 0, in a third of its budget. Raising the budget would not move that number by one
item, and an earlier version of the README said “raise the budget” anyway — it counted every
missing marker as truncation. A real run is what exposed it; the split between truncated
and off-format exists because of that run.
The template digest in the protocol is taken from the rendered prefix, measured on Qwen3-0.6B:
--thinking |
rendered assistant prefix | digest |
|---|---|---|
on |
…<\|im_start\|>assistant\n |
7e77fde99496 |
off |
…assistant\n<think>\n\n</think>\n\n |
5c8507f2b86b |
default |
same as on |
7e77fde99496 |
The zoo’s existing device batch format (g4out.json) carries ids, not text, and eval
refuses it rather than growing a tokenizer — the moment scoring owns one, the two arms are no
longer scored by identical code.
python3 cli/selftest.py — the decision rules, negative half first: 18 fixture checks over
16 source rules (each rule must fire on its trigger and stay silent on the documented
workaround), 8 over verify’s verdict (both sides of the margin floor), 20 over eval’s
refusals, 29 over the host-build comparator and the severity of the two beta-5 rules from
beta 1 through a release build. release.py runs it and will not release on a failure.