# Model artefacts **Three capabilities have a model behind them.** `cattle_detection` and `poultry_count` run a checksummed YOLOX-m ONNX graph (ADR 0017, ADR 0018), and `cattle_identity` has a frozen DINOv2-small embedding backbone (ADR 0020) — a backbone, not an identity model, and the capability stays `coming_soon` because nothing has tested it on the same animal twice. The rest have nothing, and the service says so rather than inventing a result. ``` models/ _artefacts/ yolox_m-0.1.1rc0.onnx the shipped detector, shared by two capabilities dinov3_vits16-onnx-1.onnx frozen embeddings, locally exported cattle_detection/ model_card.json poultry_count/ model_card.json cattle_identity/ model_card.json alternates/ yolox_onnx/ Apache-2.0, kept working, never registered model_card.json yolox_s-0.1.1rc0.onnx dinov2_embedding/ Apache-2.0 fallback; measurably worse, kept working model_card.json dinov2_small-onnx-1.onnx megadescriptor/ CC-BY-NC-4.0. Installed to be measured, never served model_card.json megadescriptor_l_384-onnx-1.onnx miewid/ no licence at all. Same: measured, never served model_card.json miewid_msv3-onnx-1.onnx ``` **Two of those alternates are weights Animap may not serve.** They are on disk because a licence stopped being a reason to leave a model unmeasured — directive §40.2 asked for MegaDescriptor against DINOv3 and the honest answer needed the benchmark, not an argument about why it could not be run. Nothing about the terms changed: `app/adapters/licences.py` still reports both as non-servable, `registry.refused()` still names them, and they load only under `ANIMAP_LICENCE_POLICY=record`, which logs every load at ERROR and keeps it. Under the default `enforce` policy they are refused and their adapters report unavailable. An alternate keeps its artefact **inside its own directory**. `load_card` refuses a path that climbs out of the card's grandparent, which is what stops a card being a way to load whatever is on the box. Weights are not in git. Cards are — a governance record nobody can see in a diff is not one. ## Installing them ```bash .venv/bin/python scripts/install_models.py # fetch what the cards name .venv/bin/python scripts/install_models.py --check # verify, never fetch ``` **Some artefacts have no URL to fetch.** An ONNX export of a published checkpoint is not a file the publisher hosts, so its card carries `produced_by` naming the script that makes it, and `install_models.py` refuses to download rather than writing a model-card web page into a `.onnx` file: ```bash .venv/bin/pip install -r requirements-export.txt # build-time only, never shipped .venv/bin/python scripts/export_embedding.py ``` The export is checked against torch before it is written and is byte-reproducible, so the sha256 on a card pins the exact bytes that were validated. **It is reproducible from the same exporter code, not from the weights alone** — renaming a wrapper attribute changes the graph's node names and therefore the checksum, without changing a single weight. The script reads the committed cards, downloads what each one names, and refuses anything whose sha256 does not match. It has no list of its own, so there is nothing to drift out of date. **The service never calls it.** A model that is not on disk leaves its capability unavailable. ## Permissive licences only **A card naming AGPL-3.0 is refused at load, not warned about.** `discover()` skips it and the capability stays unavailable. **And a card is not trusted about its own licence.** `app/adapters/licences.py` holds what each *runtime* really loads under, read from a primary source and dated, and `discover()` refuses a card whose declared licence disagrees with it. That closes the exploit ADR 0017 records — `"license": "Apache-2.0"` written over a path to copyleft weights — without depending on somebody having hard-coded the right runtime name. It also catches the version of the trap that arrives from upstream: `BVRA/MegaDescriptor-L-384/config.json` declares `"license": "mit"` inside the checkpoint config while the repository is CC-BY-NC-4.0. Ultralytics YOLO was the shipped detector until ADR 0017 and is now not installed at all; read [`docs/adr/0017-ultralytics-licence.md`](../../../docs/adr/0017-ultralytics-licence.md) before reaching for it again. Four mechanisms hold that position, because it is the failure that is cheap now and expensive later: `requirements.txt` omits the package, `.dockerignore` excludes `*.pt`, the `Dockerfile` fails the build if one arrives anyway, and `providers.discover()` refuses the card. `GET /health` reports `artefact_licenses`, so a deployment that picked one up says so from outside. ## Adding one Create a directory named for the capability, containing a `model_card.json`. Every field below is required; a card missing any of them is refused at load, and the capability stays unavailable: ```json { "model_id": "animap-cattle-weight", "version": "0.1.0", "capability_key": "cattle_weight", "runtime": "ultralytics", "artefact": "../_artefacts/model.pt", "source": "https://… where it came from, exactly", "license": "the licence, and it must permit this use", "sha256": "checksum of the artefact file", "validation_notes": "What was tested, on what data, with what result.", "geography_validated": ["NG"] } ``` ### Why each field is enforced **`source` and `license`** — a model of unknown origin cannot be shipped, and a licence that forbids commercial use is a legal problem discovered too late. **`sha256`** — verified on every load. The artefact on disk must be the one that was validated, not one swapped in afterwards. **`validation_notes`** — must be non-empty, and whitespace does not count. This is the field that says somebody checked the model works. A card can otherwise be filled in purely to get past the loader. **`geography_validated`** — a body-condition model trained on European dairy cattle is not validated for White Fulani in Kaduna. Recording where it was tested is what stops that being assumed. The sentinel `global-coco` means "validated nowhere, only pretrained", and `Capability.may_be_enabled` refuses to promote a capability past `beta` while it is present. **`runtime`** — not required by governance, because governance is about provenance. But a card without it can be inspected and not run: `detectors.build` refuses rather than defaulting, since a default would let a typo change which licensed model produced a farmer's result. ### Where the artefact may live The `artefact` path may point sideways — several capabilities share one detector, and storing the file twice would mean two things to keep in step. It may not point out of the models tree; `load_card` refuses that. A card under `alternates/` is two levels down and outside the discovery glob, so an alternate can never silently become the model that answered a request. ## What is not allowed - **No runtime downloads.** The service never fetches a model. An artefact arrives through a reviewed change, or it does not arrive. - **No binaries in git.** `.gitignore` excludes `*.tflite`, `*.onnx`, `*.pt` and `*.pth`, and everything in this directory except the READMEs and the cards. - **No placeholder adapters.** A capability with a validated artefact but no implemented adapter returns `501`, not a plausible-looking result. An adapter that returns something believable is precisely the failure ADR 0005 exists to prevent. ## Promoting a capability The path from nothing to trusted, per ADR 0006: 1. Register the capability — already done for all 21. 2. Install a validated artefact here. 3. Implement its adapter and check the output against this contract. 4. Move `coming_soon` → `beta`. The app shows a Beta badge; results are stored with their confidence, and nothing acts on them automatically. 5. Collect corrections. `corrections.selected_interpretation_id` captures when the model's second-ranked candidate was the right one, which is the highest value training signal the schema produces. 6. Promote to `enabled` when the corrections say it earns it. Step 6 is a decision about evidence, not a configuration change — and today it is also blocked mechanically, because both beta capabilities carry the `global-coco` sentinel.