Image-to-Text
PyTorch
Safetensors
PEFT
English
remote-sensing
satellite-imagery
earth-observation
change-detection
visual-grounding
image-captioning
visual-question-answering
optical-sar-fusion
sar
multimodal
lora
Instructions to use thundercode/SatQuery with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thundercode/SatQuery with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 69,335 Bytes
280cc90 c049289 280cc90 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 | # Development Guide
**Status tags:** `IMPLEMENTED` Β· `VERIFIED` Β· `MEASURED` Β· `ATTEMPTED` Β· `NOT RUN` Β· `BLOCKED` Β·
`DEFERRED` Β· `OPEN` Β· `RESOLVED` Β· `BY DESIGN`.
This is the contributor-facing guide to the SatQuery AI codebase. It answers: *what do I need to
install*; *how do I run it*; *what is each directory for*; *what rules will break the build if I break
them*; *how do I add a component*; *how do I test*; and *which environment traps will cost me an hour if
nobody tells me about them*.
Every command, path, timeout and rule below comes from a file that was read. Where the evidence does not
settle a question, the text says `UNKNOWN β not established from the available evidence` rather than
guessing.
> **Read this first.** `configs/base.yaml` is the single registry, and its hash β **`78f1e3700da15aa1`**
> β is frozen. **Editing the config moves the hash and invalidates every artifact keyed to it.** This is
> the one rule in the repository whose violation is not locally reversible (Β§7).
---
## Table of contents
**Part I β Prerequisites and setup**
1. Prerequisites
2. Local setup
3. Running the service locally
**Part II β Repository layout**
4. The top-level directory map
5. Where the contracts live
6. The package layout inside `core/`, `specialists/`, `app/`
**Part III β The configuration discipline**
7. "No magic numbers in Python"
8. What happens if you break the rule
9. The hash-exempt path: environment overrides
10. The invariants the loader enforces
**Part IV β Contract-first development**
11. `core/schemas.py` is the binding contract
12. The specialist interface
13. How to add a new specialist
14. How evidence is emitted
**Part V β The test workflow**
15. Running tests
16. The evidence-class markers
17. The known failures, and why they are not regressions
**Part VI β Scripts and notebooks**
18. The class of scripts under `scripts/`
19. `notebooks/`
**Part VII β Training entry points**
20. Local training (router, grounding, change, optical-SAR)
21. External GPU training (change-VQA, VLM LoRA)
22. Calibration
**Part VIII β Coding conventions**
23. What the code actually does
**Part IX β Known development traps**
24. The traps, in one table
25. Trap 1 β the stale untracked `deploy/`
26. Trap 2 β the dead sandbox proxy
27. Trap 3 β pytest only in the venv
28. Trap 4 β Chrome drops synthetic CDP key events
29. Trap 5 β Cloudflare `_headers` concatenate
30. Trap 6 β the annotation-scope trap
31. Trap 7 β the full-suite bulk-delete guard
32. Trap 8 β the stale serve process
**Part X β Status and evidence**
33. `NOT RUN` / `OPEN` / `BLOCKED` / `UNKNOWN` for development
34. Where the evidence lives
---
# Part I β Prerequisites and setup
## 1. Prerequisites
| Requirement | Value | Source |
|---|---|---|
| Python | **3.11+** | `release/repo/README.md` Β§Installation |
| Device | **a CPU is sufficient**; no CUDA requirement | `release/repo/README.md` Β§Installation |
| GPU | not required | `docs/DEPLOYMENT_DECISION.md` Β§5 |
| Git | needed to clone | `release/repo/README.md` Β§Installation |
The codebase is CPU-first and the deployment is CPU-only. This is not a fallback β it is the design:
- `core/config.py:86-91` β `device_preference` honours the `SATQUERY_DEVICE` env override, else
`"cuda" if torch.cuda.is_available() else "cpu"`.
- Every specialist defaults to `device="cpu"` (`docs/DEPLOYMENT_DECISION.md` Β§5).
- **No `.cuda()` call exists anywhere**; all placement is `.to(device)` (`docs/DEPLOYMENT_DECISION.md`
Β§5).
- `configs/base.yaml:293` β `cpu_mode_required: true`.
The development container that runs the inference tier pins Python 3.12
(`.devcontainer/devcontainer.json:3`, `image: mcr.microsoft.com/devcontainers/python:3.12`). The
repository's own guidance is 3.11+; a 3.11 or 3.12 interpreter both work.
**Not implemented** (noted, not needed): no thread capping (`torch.set_num_threads`) and no
quantisation. `lazy_load` and `cache_max_models` are only *reported* (`app/deployment.py:609-610`,
`:896-897`); **UNVERIFIED** whether any code enforces a one-model cache (`docs/DEPLOYMENT_DECISION.md`
Β§5).
## 2. Local setup
```bash
git clone https://github.com/Anish-lab-blip/SatQuery-AI
cd SatQuery-AI
python -m venv .venv
source .venv/Scripts/activate # Windows git-bash; use .venv/bin/activate on Linux/macOS
pip install -r requirements.txt
```
(`release/repo/README.md` Β§Installation)
### 2.1 The dependency manifest and its two profiles
`requirements.txt` is **frozen at architecture v1.0** and declares two install profiles in its header
comment (`requirements.txt:1-8`):
```
# Two install profiles:
# CPU (local dev / schema / geo / unit tests):
# pip install -r requirements.txt
# GPU (Kaggle T4x2 / HF ZeroGPU): torch is preinstalled on both.
# Do NOT pin torch here β Kaggle and HF ship their own builds.
```
**torch is deliberately not pinned** (`requirements.txt:21`). The platform supplies it. This is why the
local CPU install works on a machine with no CUDA and why the Kaggle/ZeroGPU targets get their own
builds.
The manifest's sections, with the contract notes the file itself carries:
| Section | Packages | Note |
|---|---|---|
| core | `numpy`, `pyyaml`, `pydantic` | β |
| geospatial | `rasterio`, `pyproj`, `opencv-python-headless` | β |
| models | `transformers>=4.52`, `open-clip-torch>=2.24`, `sentence-transformers>=2.7`, `peft>=0.10`, `huggingface_hub>=0.23`, `safetensors>=0.4`, `einops>=0.7` | see below |
| ui + reporting | `gradio>=4.44`, `reportlab>=4.1` | β |
| dev | `pytest>=8.0`, `pytest-cov>=5.0` | β |
Three contract notes in the file are load-bearing (`requirements.txt:43-53`):
- `transformers>=4.52` β SmolVLM via `AutoModelForImageTextToText`; `AutoModelForVision2Seq` is
deprecated (finding C-2).
- `open-clip-torch` β the RemoteCLIP checkpoint is loaded via `pretrained=<path>` so
`load_checkpoint()` runs its state-dict fixups (C-4).
- `huggingface_hub` β used to pin the RemoteCLIP / SmolVLM / CROMA revisions.
> **The `einops` lesson.** `einops>=0.7` is **not optional**. It is required by the vendored
> `specialists/optical_sar/vendor/use_croma.py` (`from einops import rearrange`). It was found missing on
> 2026-09-18 by executing the vendored module: it raised `ModuleNotFoundError`, so the CROMA path was
> blocked on a dependency that no document declared (`requirements.txt:28-33`).
### 2.2 Device selection
Set `SATQUERY_DEVICE` to force a device:
```bash
export SATQUERY_DEVICE=cpu # cpu | cuda | mps | null
```
`device_preference` reads the env var first and only falls back to a torch probe if it is unset
(`core/config.py:87-91`). The value is read **without importing torch** on the metadata path, which is
what keeps the health route cheap (`docs/DEPLOYMENT_TOPOLOGY.md` Β§3.3).
## 3. Running the service locally
The inference service is served by the same launcher the Codespace runs
(`release/repo/README.md` Β§Local development):
```bash
# Inference service, CPU (this is the launcher the Codespace runs)
PORT=8000 python deploy/codespace/serve.py
# Health
curl localhost:8000/v1/health
```
`serve.py` builds the app through `build_space_app()` and binds it with uvicorn on `$PORT` (default 8000)
(`deploy/codespace/serve.py:16-25`). It imports cheaply β FastAPI is imported inside `build_space_app()`
and no model is loaded at module scope β so it stays import-safe on a CPU host with no GPU and no weights
present (`deploy/codespace/serve.py:1-13`).
The server answers four routes (`app/space_app.py`):
| Route | Line | Purpose |
|---|---|---|
| `GET /v1/health` | `app/space_app.py:521` | liveness, derived status, torch-free device probe |
| `GET /v1/capabilities` | `app/space_app.py:549` | the six declared capabilities |
| `POST /v1/assets` | `app/space_app.py:555` | handle-based upload (off unless enabled) |
| `POST /v1/analyze` | `app/space_app.py:661` | the analysis path |
`POST /v1/assets` is **off unless explicitly enabled** (`SATQUERY_ASSET_ENABLED` **and**
`SATQUERY_ASSET_DIR` must both be set; otherwise it answers `503` rather than defaulting to a temp
directory) (`docs/BACKEND_DEPLOYMENT_RUNBOOK.md` Β§3.1.1).
### 3.1 Serving the frontend locally
The frontend is fully static and needs no build step (`release/repo/README.md` Β§Local development):
```bash
python -m http.server 5500 --directory frontend
```
The gateway's dev-origin allowlist includes `localhost` and `127.0.0.1` on ports 3000, 5500, 5173, 8000
and 8080 (`deploy/render/main.py:139-143`), so a local dev server on any of those ports is accepted
without an env-var change.
### 3.2 Running the gateway locally
```bash
PORT=10000 \
GITHUB_TOKEN=<token> \
CODESPACE_NAME=<codespace> \
SATQUERY_ALLOWED_ORIGINS="https://satquery.pages.dev" \
uvicorn deploy.render.main:app --port 10000
```
(`deploy/render/README.md` Β§Running locally β the placeholder is the repository's own.)
Then exercise it:
```bash
curl http://localhost:10000/api/health
curl -X POST http://localhost:10000/api/infer -H 'content-type: application/json' -d '{"query":"..."}'
curl http://localhost:10000/api/capabilities
```
> **The monorepo `deploy/` is stale** (Β§25). Use it for local experimentation only; it is **not** the
> deployed source and it lacks the tunnel code the live service runs.
---
# Part II β Repository layout
## 4. The top-level directory map
| Directory / file | Purpose | Source |
|---|---|---|
| `app/` | the serving composition root and the FastAPI entrypoint | `app/serving.py`, `app/space_app.py`, `app/deployment.py` |
| `core/` | the config loader, the typed schemas, the controller, the planner, the registry, the error taxonomy | `core/config.py`, `core/schemas.py`, `core/controller.py`, `core/planner.py`, `core/registry.py`, `core/errors.py`, `core/code_revision.py` |
| `router/` | the MiniLM intent router: encoder, classifier, adapter, training, label space, lexical fallback | `router/encoder.py`, `router/classifier.py`, `router/adapter.py`, `router/train.py`, `router/dataset.py`, `router/fallback.py`, `router/label_space.py` |
| `specialists/` | the six specialist implementations behind one interface | `specialists/base.py`, `specialists/vqa/`, `specialists/grounding/`, `specialists/change/`, `specialists/optical_sar/` |
| `preprocessing/` | raster loading, imagery, quality checks | `preprocessing/raster.py`, `preprocessing/imagery.py`, `preprocessing/quality.py` |
| `geospatial/` | CRS handling and transforms | `geospatial/crs.py`, `geospatial/transform.py` |
| `evidence/` | the evidence engine and confidence | `evidence/engine.py`, `evidence/confidence.py` |
| `evaluation/` | manifests, leakage, metrics, normalisation, the runner, benchmark adapters, the frozen prompt set | `evaluation/manifests.py`, `evaluation/leakage.py`, `evaluation/metrics/`, `evaluation/normalize.py`, `evaluation/runner.py`, `evaluation/benchmark_adapters/`, `evaluation/prompt_freeze.json`, `evaluation/manifest_freeze.json` |
| `training/` | the training loops and data adapters, split by task | `training/router/`, `training/grounding/`, `training/change/`, `training/change_vqa/`, `training/fusion/`, `training/vlm/`, `training/calibration/`, `training/data/` |
| `configs/` | the frozen registry and the frozen deploy manifest | `configs/base.yaml`, `configs/deploy.yaml` |
| `gateway/` | the standalone gateway: policy (decisions) + app (HTTP plumbing) | `gateway/policy.py`, `gateway/app.py`, `gateway/assets.py` |
| `deploy/` | the deployment launchers β **stale and untracked in the monorepo** | `deploy/codespace/`, `deploy/render/` (Β§25) |
| `tests/` | the suites, split by concern and evidence class | `tests/unit/`, `tests/integration/`, `tests/routing/`, `tests/geospatial/`, `tests/leakage/`, `tests/model/`, `tests/e2e/` |
| `scripts/` | the flat set of executable helpers | Β§18 |
| `notebooks/` | the Kaggle notebooks | Β§19 |
| `artifacts/` | trained heads, checkpoints, caches, evidence archives | ~3.7 GB total (`release/CURRENT_RELEASE_STATE.md` Β§3) |
| `frontend/` | the static site | staged by `scripts/stage_pages.mjs` |
| `docs/` | the project's own engineering records | ~60 files |
| `hf/` | the Hugging Face project card / model cards | β |
| `demo/`, `benchmark/`, `reports/`, `data/`, `logs/` | supporting material | β |
> **`training/router/` is empty** in the working copy; the router's training lives in `router/train.py`
> and `router/dataset.py` and is driven by `scripts/train_router.py` (Β§20).
> **The repository has no `pyproject.toml` and no `setup.py`.** `app` is a plain package, so the repo root
> must be on `sys.path` for `from app.space_app import ...` to resolve
> (`deploy/codespace/launch.sh:31-34`; `pytest.ini` sets `pythonpath = .`).
## 5. Where the contracts live
Three files are authoritative, and the code β not a document β wins when they disagree:
| Contract | File | What it fixes |
|---|---|---|
| the config registry | `configs/base.yaml` | every tunable value; hashed (Β§7) |
| the wire/data schemas | `core/schemas.py` | the request, result, evidence, trace and health shapes |
| the specialist interface | `specialists/base.py` | the four-method contract every specialist implements |
| the capability table | `core/registry.py` | which capabilities exist and how to build them |
| the planner's mapping | `core/planner.py` | `TASK_CAPABILITY` and `CAPABILITY_ASSETS` |
When `docs/API_CONTRACT.md` and `core/schemas.py` disagree, the contract document's own authority clause
resolves it: *"the request/response shapes are not invented here. They are the existing, tested Pydantic
models in `core/schemas.py`. This document describes them; it does not declare new ones."*
(`docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§15). The known instance of this β the forward-compatibility
promise versus `extra="forbid"` β is recorded as **C-2** and left as a documented contradiction rather
than silently resolved (`docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§15).
## 6. The package layout inside `core/`, `specialists/`, `app/`
### 6.1 `core/`
| File | Role |
|---|---|
| `core/config.py` | the loader, the validator, the hash (Β§7, Β§10) |
| `core/schemas.py` | every Pydantic model (Β§11) |
| `core/controller.py` | the nine-state controller that runs a plan and produces the trace |
| `core/planner.py` | the policy layer: `TASK_CAPABILITY`, `CAPABILITY_ASSETS`, `plan()` |
| `core/registry.py` | the capability registry: spec table, lazy construction, degradation states |
| `core/errors.py` | the typed error taxonomy and `scrub_paths` |
| `core/code_revision.py` | the code revision recorded in a run |
The three-tier control split is deliberate: the **registry** knows *which* specialists exist and *how* to
construct them; the **planner** *decides* what runs; the **controller** executes. The registry's own
docstring states it: *"It never decides what runs β that is `core.planner`'s job alone"*
(`core/registry.py:6-8`).
### 6.2 `specialists/`
| Package | Files | Capability |
|---|---|---|
| `specialists/vqa/` | `inference.py`, `model.py`, `prompts.py` | `vqa`, `caption` |
| `specialists/grounding/` | `specialist.py`, `remoteclip.py`, `head.py`, `inference.py` | `grounding` |
| `specialists/change/` | `specialist.py`, `stanet.py`, `vqa_specialist.py`, `postprocess.py` | `change`, `change_vqa` |
| `specialists/optical_sar/` | `specialist.py`, `croma.py`, `fusion_head.py`, `sensor_adapter.py`, `radiometry.py`, `inference.py`, `prompts.py`, `vendor/` | `optical_sar` |
### 6.3 `app/`
| File | Role |
|---|---|
| `app/serving.py` | the composition root β wires checkpoints through the registry's `builders=` override |
| `app/space_app.py` | `build_space_app()` β the FastAPI app and the four routes |
| `app/deployment.py` | the deployment description and device resolution |
---
# Part III β The configuration discipline
## 7. "No magic numbers in Python"
`configs/base.yaml` opens with the rule, in the file itself:
```yaml
# RULE: no magic numbers anywhere in Python. Everything tunable lives here.
# Every value below is loaded, validated and hashed by core/config.py.
```
(`configs/base.yaml:4-5`)
The loader reinforces it: *"One config system. No duplicated constants. Every value in configs/base.yaml
is loaded, validated against the frozen architecture, and hashed so evaluation runs are reproducible."*
(`core/config.py:1-5`).
**Access pattern.** Never read the YAML directly; import the singleton:
```python
from core.config import get_config
cfg = get_config()
cfg.get("croma.image_resolution") # dotted-path access
cfg.require("change.encoder") # raises ConfigError if missing
cfg.seed # project.seed, default 42
```
`get_config()` is an `lru_cache(maxsize=1)` singleton β *"Import this, do not re-read YAML"*
(`core/config.py:270-273`).
## 8. What happens if you break the rule
Two distinct failures, and both are loud.
### 8.1 The loader fails startup
`Config.__init__` calls `_validate()`, which collects **every** violation and raises a single
`ConfigError` naming them all (`core/config.py:46-49,94-222`):
```python
if errors:
raise ConfigError(
"configuration failed frozen-architecture validation:\n - "
+ "\n - ".join(errors)
)
```
The header states the intent: *"if a config tries to violate a frozen decision (e.g. bf16 on T4,
torch.compile on ZeroGPU, a CROMA image_resolution that is not a multiple of 8), it fails loudly rather
than at runtime"* (`core/config.py:6-8`).
### 8.2 Editing the config MOVES the hash
The hash is a sha256 over the whole registry, truncated to 16 hex characters
(`core/config.py:76-80`):
```python
@property
def hash(self) -> str:
"""Stable hash of the whole registry. Recorded in every evaluation run."""
blob = json.dumps(self._data, sort_keys=True, default=str).encode()
return hashlib.sha256(blob).hexdigest()[:16]
```
Because it is computed over the entire registry, **any** edit to `configs/base.yaml` changes it. The
frozen value is **`78f1e3700da15aa1`** (`release/repo/README.md` Β§Reproducibility;
`docs/PHASE18_DEPLOYMENT_PACKAGING.md` Β§3). Every artifact records the hash it was produced against, so a
hash change **invalidates every artifact keyed to it**.
> **This is the one non-reversible action in the repository.** `docs/BACKEND_DEPLOYMENT_RUNBOOK.md` Β§6.1
> lists it as the single row in the rollback table whose answer to "Reversible?" is **no**: *"The recorded
> benchmark hash is gone; the frozen benchmark no longer matches."*
**Verify the hash after any config-adjacent change:**
```bash
$PY -c "from core.config import get_config; print(get_config().hash)"
# expected: 78f1e3700da15aa1
```
(`docs/BACKEND_DEPLOYMENT_RUNBOOK.md` Β§1.1, where `$PY` is `$REPO/.venv/Scripts/python.exe` on Windows.)
**Two things that do *not* move the hash** β both verified:
1. **`configs/deploy.yaml` is inert.** It carries a `registry: false` marker and is never merged into the
registry, so editing it cannot move the hash. Verify with `$PY scripts/validate_deploy_config.py` β
exit 0, `"deploy.yaml is inert (not in the registry) and C-8-consistent."`
(`docs/BACKEND_DEPLOYMENT_RUNBOOK.md` Β§2.4). **But** `scripts/validate_deploy_config.py` hard-fails if
the `deployment:` block in `deploy.yaml` differs key-for-key from `base.yaml`'s, so a change to one
alone fails the validator (`docs/DEPLOYMENT_DECISION.md` Β§4).
2. **The serving wiring uses `builders=`.** `app/serving.py` wires the change and change-VQA heads through
the registry's `builders=` override instead of config, which is *"the seam that keeps `Config.hash`
unchanged while still pointing serving at the trained artifacts"* (`docs/PHASE19_FINAL_HARDENING.md`
Β§3.4).
## 9. The hash-exempt path: environment overrides
Two registry values can be overridden from the environment without editing the YAML
(`core/config.py:261-265`):
| Variable | Effect |
|---|---|
| `SATQUERY_PRECISION` | overrides `training.precision` |
| `SATQUERY_TORCH_COMPILE` | overrides `deployment.torch_compile` (`"true"` β `True`) |
**Both are still validated.** Setting `SATQUERY_TORCH_COMPILE=true` **fails startup** because finding C-8
forbids `torch.compile` (`release/repo/docs/DEPLOYMENT.md` Β§6.3; `core/config.py:114-119`).
This is the general pattern for deployment state that must not move the hash: read it from the
environment. The asset-store capacity, TTL and per-file cap follow the same rule
(`release/repo/README.md` Β§Installation).
## 10. The invariants the loader enforces
`core/config.py::_validate` is not documentation β it is a check that raises `ConfigError`. Each invariant
exists because a specific finding proved the failure mode.
| Invariant | Why it exists | Finding |
|---|---|---|
| `croma.image_resolution % 8 == 0` | CROMA asserts this; native 120 β 225 patches | C-7 |
| `training.precision β {fp16, bf16, fp32}` | the T4 is SM 7.5, so bf16 is unavailable | C-6 |
| `deployment.torch_compile is not true` | ZeroGPU does not support `torch.compile` | C-8 |
| `vlm.processor_longest_edge β€ image.tile_size` | the processor's default `longest_edge` is 2048, which upscales a 512 px tile 4Γ and then splits it into **17** sub-images β a ~17Γ overrun, not the 4Γ the plan estimated | F5-2 |
| `vlm.prompt_must_use_chat_template is true` | SmolVLM raises `ValueError` on prompts lacking one `<image>` token per image | F5-3 |
| `fusion.input_dim == 3*encoder_dim + optical_channels + sar_channels` (= 2318) | CROMA emits optical/SAR/joint GAP vectors; the availability mask is consumed by the head | C-1 |
| `croma.optical_channels == 12` and `croma.sar_channels == 2` | CROMA's `s2_channels` / `s1_channels` are fixed | β |
| `grounding_head.feature_dim == 4 * grounding.encoder_projected_dim` (= 2048) | a mismatch is a **silent** shape error β torch raises only at the similarity step, after patch features are already cached | P7-1 |
| `router.tasks` includes `unsupported` and `router.num_tasks == len(router.tasks)` | the ontology and its declared size cannot drift apart | β |
| `change.sa_mode β {BAM, PAM}` and `change.encoder` is set | the change architecture is not implicit | C-9 |
| `image.top_k_tiles β€ image.max_tiles` | the dispatch ceiling cannot exceed the examination ceiling | β |
(`core/config.py:94-222`; `release/repo/README.md` Β§Installation.)
> **Why the grounding-head guard matters most.** A mismatch there is a *silent* shape error: torch raises
> only at the similarity step, by which point the patch features have already been computed and cached β
> *"the failure surfaces far from its cause"* (`core/config.py:170-196`).
---
# Part IV β Contract-first development
## 11. `core/schemas.py` is the binding contract
`core/schemas.py` (462 lines) holds every model the system exchanges. The rule is simple: **no specialist
may invent its own result shape.** Every specialist returns exactly `SpecialistResult`
(`core/schemas.py:325-406`), whose docstring says so: *"Every specialist returns exactly this. No
exceptions."*
The models, in order:
| Model | Line | Role |
|---|---|---|
| `Task` (enum) | 35 | the six-task ontology (`vqa`, `caption`, `grounding`, `change`, `optical_sar`, `change_vqa`) + `unsupported` |
| `Modality` (enum) | 50 | `optical` / `sar` / `joint` |
| `CoordinateSystem` (enum) | 57 | `normalized_0_1` / `geo` |
| `EvidenceType` (enum) | 65 | the evidence classes |
| `ControllerState` (enum) | 79 | the nine-state spine |
| `Intent` | 94 | the router's output |
| `GeoMetadata` | 120 | CRS / transform metadata |
| `SensorDescriptor` | 138 | sensor identity |
| `AssetMetadata` | 153 | one input asset |
| `Box` | 171 | a flat box (see the flat-vs-nested trap below) |
| `Region` / `ChangeRegion` | 189 / 202 | spatial outputs |
| `Evidence` | 216 | one observable artefact |
| `ConfidenceBreakdown` | 259 | measurable confidence |
| `TraceStep` / `ModelRef` / `ExecutionTrace` | 279 / 288 / 296 | the trace |
| `SpecialistResult` | 325 | the master contract |
| `AnalysisRequest` | 412 | the request |
| `ResultEnvelope` | 421 | the response wrapper |
| `HealthStatus` | 430 | the health shape |
**Every model sets `model_config = ConfigDict(extra="forbid")`.** This is deliberate and has a documented
consequence (C-2, Β§5): the models reject a body carrying an unknown key, which contradicts
`API_CONTRACT.md` Β§1.1's forward-compatibility promise. The code is authoritative; the document is the
inaccurate half (`docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§15).
> **The flat-vs-nested trap.** `Box` is **flat**, not nested. An earlier documentation draft described it
> with nested geometry, and *"every box the frontend drew would have been at the origin"*
> (`docs/PHASE19_FINAL_HARDENING.md` Β§3.6). Documentation is validated against the real models by tests
> (`test_api_contract_doc.py`, `test_frontend_guide_doc.py`), which is what caught it.
### 11.1 The two cross-field validators you must not break
`SpecialistResult` carries a `_task_output_consistency` validator (`core/schemas.py:353-406`) with two
rules:
- **Grounding with no localisation is degraded, not a crash.** If `task == GROUNDING` and there are no
boxes or regions, the validator appends a warning and sets `degraded = True`.
- **Change-VQA with no answer text is degraded.** If `task == CHANGE_VQA` and the answer is blank, the
validator marks it degraded β but only if the specialist has not already set `degraded` itself.
The `CHANGE` clause that once lived there was **removed** (F-16c): it had collapsed to `not regions`, and
a *successful* no-change analysis began reporting `degraded: true`. **CHANGE is the one task whose
`degraded` flag is now set entirely by its specialist** (`core/schemas.py:362-395`).
## 12. The specialist interface
Every specialist implements exactly one abstract base class, `Specialist`, in `specialists/base.py`. The
interface is **four methods**, and the split is deliberate (`specialists/base.py:1-17`):
```
validate_request -> can this specialist serve this request at all?
execute -> do the work, return a SpecialistResult
produce_evidence -> what observable artefacts support the result?
estimate_confidence -> what measurable signals support the score?
```
The class attributes a specialist must set (`specialists/base.py:48-58`):
| Attribute | Meaning |
|---|---|
| `name` | stable machine-readable name, used in traces and evidence sources |
| `version` | semantic version; *"bump when behaviour changes, not when code moves"* |
| `capabilities` | the capability strings this specialist serves, e.g. `("vqa", "caption")` |
The four abstract methods (`specialists/base.py:62-96`):
| Method | Contract |
|---|---|
| `validate_request(request)` | raise the **most specific** typed error available (`InvalidRequestError`, `PairMisalignmentError`, β¦), never a bare `Exception` |
| `execute(request)` | return a normalised `SpecialistResult`; *"Never returns None."* |
| `produce_evidence(result)` | return `list[Evidence]`; *"Must not invent anything the specialist did not actually compute."* |
| `estimate_confidence(result)` | return a `ConfidenceBreakdown`; *"Never an LLM utterance."* |
The base class also provides helpers a specialist should use rather than re-implement
(`specialists/base.py:98-134`): `supports()`, `require_assets()` (asserts an exact asset count and raises
a typed error), `require_capability()`, `model_refs()` (for the trace), and `describe()`.
`SpecialistRequest` (`specialists/base.py:34-45`) is the input: `assets`, `query`, `params`, `run_id`.
Its docstring states the isolation rule: *"Everything a specialist is given. No specialist reads global
state."*
## 13. How to add a new specialist
This is the verified procedure, read from the code. There are **five** steps, and skipping any one fails
loudly (which is the design).
### Step 1 β implement the `Specialist` ABC
Subclass `specialists.base.Specialist`, set `name`, `version` and `capabilities`, and implement the four
methods. The builder for the class is a module-level function
`build_<x>_specialist(config, *, device, **kwargs)` β the same shape as the four existing builders
(`core/registry.py:185-259` names them: `build_vqa_specialist`, `build_grounding_specialist`,
`build_change_specialist`, `build_change_vqa_specialist`, `build_optical_sar_specialist`).
### Step 2 β add a row to the spec table
Add a `SpecialistSpec` to `default_specs()` in `core/registry.py:171-260`. The spec's fields
(`core/registry.py:121-165`):
| Field | Meaning |
|---|---|
| `name` | the registry key β **the capability string**, never the specialist's own `name` |
| `capabilities` | the tuple the built object must declare; **asserted** after construction |
| `module` | dotted module path containing the builder |
| `builder` | builder function name inside `module` |
| `requires_assets` | exact asset count, or `None` for "any" |
| `config_keys` | `{builder_kwarg: dotted.config.key}` β only keys that resolve are passed |
| `optional_config_keys` | as above, but a missing key contributes nothing |
| `failure_states` | registry state to use when construction raises, keyed by exception class name |
The `SpecialistSpec.__post_init__` refuses a spec whose `name` is not in its own `capabilities`
(`core/registry.py:158-165`): *"the registry keys on capability, so this spec would be unreachable."*
### Step 3 β understand the capability-vs-name asymmetry
**This is the trap the registry exists to encode** (`core/registry.py:30-45`). The VQA specialist declares:
```python
name = "vlm" # specialists/vqa/inference.py:114
capabilities = ("vqa", "caption") # specialists/vqa/inference.py:116
```
Every other specialist's `name` equals its single capability. A registry keyed on `name` would make `vqa`
permanently unfindable while every other specialist kept working. So the registry keys on **capability**
and **asserts** the capability tuple against the constructed object
(`core/registry.py:497-515`). If your spec's `capabilities` disagrees with what your specialist declares,
construction fails with a `SpecialistError` naming both tuples.
### Step 4 β register the task (only if it is a *new* task)
If the new specialist serves an existing task, nothing else is needed. If it is a new task, add it to:
- `Task` in `core/schemas.py:35-49`, and
- `TASK_CAPABILITY` in `core/planner.py:121-128`, and
- `CAPABILITY_ASSETS` in `core/planner.py:133-142`.
`CAPABILITY_ASSETS` mirrors each specialist's own `validate_request`, which **stays authoritative** β the
planner's copy is a *"cheap precondition so it can refuse before construction is attempted"*
(`core/planner.py:130-132`). A test asserts `CAPABILITY_ASSETS` equals `SpecialistSpec.requires_assets`
in both directions, and asserts the gateway carries **no third copy** of the asset-count table
(`docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§3).
> **Adding a task touches `router.tasks`.** `router.num_tasks` must equal `len(router.tasks)`
> (`core/config.py:199-206`), so a new task means a config edit β **which moves the hash** (Β§8.2). This is
> the one part of "add a specialist" that has a global consequence.
### Step 5 β return the right shapes
`execute` must return a `SpecialistResult`; `produce_evidence` must return `list[Evidence]`;
`estimate_confidence` must return a `ConfidenceBreakdown`. The registry will construct your specialist
lazily and record its state.
### What the registry does with your specialist
| Behaviour | Mechanism |
|---|---|
| lazy construction | the builder is imported via `importlib.import_module` **inside** `build()` β no module-level specialist import (`core/registry.py:15-25,462-475`) |
| memoisation | the second `build(cap)` returns the cached entry (`core/registry.py:432-434`) |
| three states | `AVAILABLE` / `DEGRADED` / `UNAVAILABLE` (`core/registry.py:102-107`) |
| degradation detection | duck-typed on `has_checkpoint` / `has_head` / `has_encoder` / `model is None` (`core/registry.py:527-548`) |
| failure is retained | a construction failure returns an `UNAVAILABLE` entry rather than raising; **only an unknown capability raises** (`core/registry.py:418-450`) |
| corrupt β missing | `ModelLoadError` / `ModelUnavailableError` map to `UNAVAILABLE`, never retried as `DEGRADED` (`core/registry.py:550-612`) |
| path scrubbing | the client-visible `detail` is `scrub_paths(...)`; the raw string goes to the log only (`core/registry.py:560-598`) |
> **The corrupt-vs-missing rule is load-bearing.** *"silently running an untrained model because a real
> checkpoint failed to load would be the worst outcome"* (`specialists/change/specialist.py:834-838`,
> quoted in `core/registry.py:63-75`). Do not add a fallback that re-adds a degradation the builder
> refused.
## 14. How evidence is emitted
Evidence is produced by `Specialist.produce_evidence(result)` and returned as `list[Evidence]`. The
`Evidence` model (`core/schemas.py:216-256`):
| Field | Type | Note |
|---|---|---|
| `evidence_id` | `str` | auto-generated `ev_<hex>`; **must be unique within a result** |
| `type` | `EvidenceType` | the evidence class |
| `source_specialist` | `str` | which specialist computed it |
| `coordinate_system` | `CoordinateSystem \| None` | required for spatial evidence |
| `coordinates` | `list[float] \| None` | the geometry |
| `score` | `float \| None` | `0.0 β€ score β€ 1.0` |
| `artifact_ref` | `str \| None` | **never a filesystem path** (F-16) |
| `payload` | `dict[str, Any]` | structured detail |
Two validators enforce correctness:
- `SpecialistResult._unique_evidence_ids` rejects a result with duplicate `evidence_id` values
(`core/schemas.py:345-351`).
- `Evidence._spatial_needs_crs` rejects spatial evidence (`BOUNDING_BOX`, `MASK`, `CHANGE_MAP`, `TILE`,
`IMAGE_CROP`, `JOINT_FEATURE_REGION`) that carries coordinates but **no** `coordinate_system`
(`core/schemas.py:241-255`).
> **`artifact_ref` is never a filesystem path.** v1 exposes no artifact-serving endpoint, so it is null
> unless a deployment supplies a client-fetchable reference. *"An artifact may still be written
> server-side where configured; being written is not the same as being retrievable."*
> (`core/schemas.py:227-238`.)
---
# Part V β The test workflow
## 15. Running tests
### 15.1 Always use the repository venv interpreter
pytest is installed **only** in the repository virtualenv. Invoking the system `pytest` fails or resolves
to a different interpreter (`release/repo/docs/REPRODUCIBILITY.md` Β§10.2):
```bash
.venv/Scripts/python.exe -m pytest tests/unit/test_frontend_live_wiring.py -q
```
On Windows, pytest must be given `-p no:cacheprovider` because the sandbox refuses `.pytest_cache` writes
(`docs/BACKEND_DEPLOYMENT_RUNBOOK.md` Β§0).
### 15.2 Run targeted files, not the whole tree
**A full `tests/unit` run trips the sandbox's bulk-delete guard** (4Γ `test_safe_delete_shim` failures)
(`release/repo/docs/REPRODUCIBILITY.md` Β§10.3). The workaround is to run the targeted suites:
```bash
.venv/Scripts/python.exe -m pytest tests/unit/test_frontend_live_wiring.py -q # 106 passed
```
The runbook notes that **multi-suite invocations in one command have been refused by the environment
before; single suites are reliable** (`docs/BACKEND_DEPLOYMENT_RUNBOOK.md` Β§2.6):
```bash
$PY -m pytest tests/unit/test_deploy_config.py -p no:cacheprovider -q
$PY -m pytest tests/unit/test_app_serving.py -p no:cacheprovider -q
$PY -m pytest tests/unit/test_api_contract_doc.py tests/unit/test_frontend_guide_doc.py -p no:cacheprovider -q
```
> **Do not pipe pytest through `grep`** in the authoring sandbox β output is block-buffered and a killed
> pipeline swallows it. Redirect to a file instead (`release/repo/docs/REPRODUCIBILITY.md` Β§5.6).
### 15.3 `pytest.ini`
```ini
[pytest]
testpaths = tests
pythonpath = .
addopts = -q --tb=short
```
(`pytest.ini:1-4`)
`pythonpath = .` is what makes `from core.config import ...` resolve from the repo root without an
installed package.
## 16. The evidence-class markers
`pytest.ini` registers four markers whose purpose is to make a test's **evidence class** explicit
(`pytest.ini:23-27`):
| Marker | Meaning |
|---|---|
| `unit` | fast, no I/O, no server, no network. Evidence about a component in isolation. |
| `integration` | exercises two or more real components wired together in-process. |
| `smoke` | a minimal end-to-end path run against real local artifacts, not a mock. |
| `real_inference` | ran the real model on real inputs in this environment. |
The file's comment states the discipline:
> *"a unit test never claims a deployment was exercised, and no test may be reported as 'real inference'
> unless it ran the real model on real data in this environment."* (`pytest.ini:13-17`)
**`environment_blocked` is deliberately not a marker.** *"a blocked path is reported in the STEP 7 report
rather than encoded as a permanently-skipped test, because a skip can be mistaken for coverage"*
(`pytest.ini:19-21`). An unregistered mark is an **error** rather than a silent no-op (`pytest.ini:14-15`).
## 17. The known failures, and why they are not regressions
Running the entire unit tree trips **5β6** failures, classified by cause
(`release/repo/docs/REPRODUCIBILITY.md` Β§5.4):
| # | Failure | Cause | Regression? |
|---|---|---|---|
| 1β4 | `test_safe_delete_shim` (Γ4) | the sandbox's bulk-**delete guard** (Windows verbatim-path behaviour) | **No** |
| 5 | one ordering flake in the router route test | passes in isolation; order/collection-dependent | **No** |
| 6 | one stale adapter test | asserts `optical_sar` absent when CROMA is *unshipped* β **CROMA is now shipped** | **No** |
Re-running the affected files together passes **137** tests, which is what isolates them as environmental
rather than behavioural (`release/repo/docs/REPRODUCIBILITY.md` Β§5.4).
**Known per-suite results:**
| Suite | Command | Expected |
|---|---|---|
| Frontend live-wiring | `pytest tests/unit/test_frontend_live_wiring.py` | **106 passed** |
| Doc/frontend suite | `pytest` on the 5 doc/frontend files | **183 passed** |
| Full unit suite | `pytest tests/unit` | 5β6 **environmental** failures |
| Gateway policy | `pytest tests/unit/test_gateway_policy.py` | **51 passed** |
| Evidence engine | `pytest tests/unit/test_evidence_engine.py` | **73 passed** |
(`release/repo/README.md` Β§The test suites; `docs/PHASE19_FINAL_HARDENING.md` Β§6.)
> **The precise full-suite *collected* count is `UNKNOWN β not established from the available evidence`.**
> Per-suite counts are known; the single collected total is not (`release/repo/docs/REPRODUCIBILITY.md`
> Β§5.4).
---
# Part VI β Scripts and notebooks
## 18. The class of scripts under `scripts/`
`scripts/` is a **flat** directory of executable helpers β no sub-packages. The listing holds **51 script
files** (47 `.py`, 3 `.ps1`, 1 `.mjs`) plus `__init__.py`. They fall into clear classes:
| Class | Examples | Purpose |
|---|---|---|
| **Training entry points** | `train_router.py`, `train_grounding.py`, `train_change.py`, `train_fusion.py`, `train_change_vqa.py` | thin CLIs over the `training/` modules (Β§20) |
| **Evaluation** | `eval_change.py`, `eval_fusion_115.py`, `eval_grounding_head.py`, `evaluate_change_vqa.py` | per-task evaluation |
| **Data preparation** | `prepare_bigearthnet.py`, `prepare_change_vqa.py`, `select_bigearthnet_slice.py` | build corpora and selection manifests |
| **Contract probes** | `probe_grounding_head_contract.py`, `probe_remoteclip_contract.py`, `probe_vlm_contract.py` | measure a real model's contract before relying on it |
| **Smoke tests** | `smoke_test_change.py`, `smoke_test_grounding_head.py`, `smoke_test_vlm.py` | a minimal real path per specialist |
| **Verification / gates** | `verify_gate1.py`, `verify_croma_forward.py`, `verify_grounding_e2e.py`, `verify_levir_real.py`, `verify_cdvqa_imagery.py` | prove a property end to end |
| **Threshold / hyperparameter sweeps** | `sweep_change_threshold.py`, `sweep_router_threshold.py`, `fusion_seed_variance.py` | sweep a frozen knob |
| **Phase-6 (VLM) tooling** | `phase6_train_vlm.py`, `phase6_adjudicate_test.py`, `phase6_close.py`, `phase6_rerule.py`, `phase6_recover_baseline_test.py` | the VLM acceptance workflow |
| **Phase-12 (fusion) tooling** | `p12_integrity_verify.py`, `p12_preflight_verify.py`, `run_phase12_extraction.ps1`, `run_phase12_resume_ref.ps1`, `run_phase12_resume_s1_ref.ps1` | the fusion feature-extraction workflow (PowerShell drivers) |
| **Calibration** | `fit_calibration.py` | fit the temperature-scaling artifact on validation (Β§22) |
| **Kaggle packaging / rehearsal** | `package_kaggle_code.py`, `rehearse_kaggle_notebook.py`, `rehearse_change_notebook.py` | package and dry-run the notebooks |
| **Deployment validation** | `validate_deploy_config.py` | prove `configs/deploy.yaml` is inert |
| **Frontend staging** | `stage_pages.mjs` | build the Cloudflare Pages bundle |
| **Environment / diagnostics** | `check_env.py`, `diagnose_feature_cache.py`, `croma_normalisation_arm_probe.py`, `analyze_grounding_resolution.py`, `analyze_reben_labels.py`, `check_cdvqa_second_overlap.py`, `establish_cdvqa_temporal_order.py`, `exp_grounding_resolution.py`, `extract_fusion_features.py` | environment and dataset diagnostics |
**The training entry points share one convention**, stated in their docstrings: *"A THIN CLI over
`training/<task>/train.py` β¦ Everything that computes lives in the module; this file parses arguments,
reports the environment, prints the accounting, and returns an exit code."* (`scripts/train_change.py:1-6`,
`scripts/train_fusion.py:1-6`). The exit codes are a contract (`scripts/train_change.py:17-24`):
```
0 training completed (or --dry-run validated a present dataset)
2 the dataset is missing, empty, or cannot be split -- NOT a crash
3 the run started and the trainer raised a typed error
```
> **Re-exported names are part of the contract.** Several scripts re-export their trainer's symbols at
> module scope, and a test asserts it. E.g. `tests/unit/test_change_train_script_contract.py` asserts
> `change_loss`, `train_change_head` and `evaluate` are reachable as `scripts.train_change.*`
> (`scripts/train_change.py:26-30`).
## 19. `notebooks/`
| Notebook | Purpose |
|---|---|
| `kaggle_change_vqa_train.ipynb` | R-02 change-VQA training (Β§21) |
| `kaggle_phase6_vlm_lora.ipynb` | Phase-6 SmolVLM LoRA training (Β§21) |
| `kaggle_change_training.ipynb` | the change **detector** β *"frozen and out of scope"* (`docs/R02_KAGGLE_TRAINING_GUIDE.md` Β§4) |
| `kaggle_grounding_resolution.ipynb` | Phase-7 grounding resolution β unrelated to R-02 |
> **Do not confuse the two change notebooks.** For change-VQA use `kaggle_change_vqa_train.ipynb`; do
> **not** use `kaggle_change_training.ipynb` (that one trains the change *detector*) or
> `kaggle_grounding_resolution.ipynb` (Phase 7) (`docs/R02_KAGGLE_TRAINING_GUIDE.md` Β§4).
Two `.bak-*` files sit beside the Phase-6 notebook; they are editor backups, not runnable notebooks.
---
# Part VII β Training entry points
The six artifacts train in two places: four locally, two on an external GPU. This is a **deliberate
boundary** β the release ships *"frozen artifacts with provenance, not a retraining harness"*
(`release/repo/docs/REPRODUCIBILITY.md` Β§8.5).
| Artifact | Where it trains | Reproducible from this release? |
|---|---|---|
| router adapter | local CPU | **yes** β `configs/base.yaml` Β§`router.training` |
| grounding head | local | **yes** β `configs/base.yaml` Β§`grounding_training` |
| change head | local | **yes** β `configs/base.yaml` Β§`change` |
| optical_sar fusion head | local, seed sweep | **yes** |
| change_vqa head | **external GPU (Kaggle)** | **partly** β the promotion gate, evaluation and serving wiring are reproducible; there is no one-command retrain |
| vlm LoRA adapter | **external GPU** | **partly** β same |
(`release/repo/README.md` Β§What "reproduce" means.)
## 20. Local training (router, grounding, change, optical-SAR)
### 20.1 Router β CPU-only, and fast
```bash
python scripts/train_router.py --smoke # fast, stub encoder
python scripts/train_router.py --stub # fast, no model download
python scripts/train_router.py # full run, real MiniLM
```
(`scripts/train_router.py:1-10`)
The script's own measured note: *"Runs entirely on CPU. Measured cost with the real encoder: ~40 s to load
MiniLM on a cold cache, ~0.1 s to embed the corpus, ~1 s to train 60 epochs. There is no reason to spend
Kaggle GPU quota on this."* The encoder is frozen, so embeddings are cached and the adapter trains on
cached vectors (`configs/base.yaml:67-69`).
### 20.2 Grounding β two separable stages
```bash
# stage 1 only β measure the corpus before committing to training
python scripts/train_grounding.py --data-root <root> --extract-only
# both stages, real run
python scripts/train_grounding.py --data-root <root> --checkpoint <ckpt.pt>
# 2-batch forward+backward+checkpoint+reload, on CPU
python scripts/train_grounding.py --data-root <root> --debug
```
(`scripts/train_grounding.py:6-22`)
Splitting extraction from training means a hyperparameter change does not re-encode 15,699 images, and a
crashed training run does not lose the cache. The stated bar: *"the Phase 7 zero-shot baseline scored
0.0972 mean best IoU over 16,159 real eval records. This head must beat it by MIN_IMPROVEMENT_IOU to
justify existing. The comparison is printed whether or not it passes."*
### 20.3 Change β a thin CLI over `training/change/train.py`
```bash
# validate the data and the split; build nothing, train nothing
python scripts/train_change.py --data-root <LEVIR-CD root> --dry-run
# real run on CPU
python scripts/train_change.py --data-root <LEVIR-CD root> --device cpu
# a first real-data run that does not commit an hour
python scripts/train_change.py --data-root <root> --limit 256 --epochs 3
```
(`scripts/train_change.py:8-16`)
`--dry-run` is *"the leakage check without the cost: it loads the items, performs the scene-disjoint
split, runs `assert_image_disjoint`, prints the accounting and exits. It does NOT build the detector, so
it cannot trigger a pretrained-weights download, and it writes no files."*
### 20.4 Optical-SAR fusion β CPU-only, no result claimed
```bash
# validate the caches and the split; train nothing, write nothing
python scripts/train_fusion.py --train-cache train.npz --val-cache val.npz --dry-run
# a real run on CPU (the fusion head is CPU-only)
python scripts/train_fusion.py --train-cache train.npz --val-cache val.npz --arm A --epochs 20
```
(`scripts/train_fusion.py:8-15`)
> **No performance number is a result here.** The script states it: *"This loop has never seen the real
> paired BigEarthNet-S1+S2 corpus. Every run record it writes carries `result_status` and
> `pre_registered_metric_computed: false`; the pre-registered 11.5 metric is not computed."*
> (`scripts/train_fusion.py:17-20`.)
## 21. External GPU training (change-VQA, VLM LoRA)
Both external runs happen on **Kaggle** with **GPU T4 Γ2**. Neither is a one-command retrain from this
release.
### 21.1 Change-VQA (R-02)
**Guide:** `docs/R02_KAGGLE_TRAINING_GUIDE.md` (37 KB). **Runbook:** `RUNBOOK_CHANGE_VQA_KAGGLE.md`.
| Property | Value | Source |
|---|---|---|
| notebook | `notebooks/kaggle_change_vqa_train.ipynb` | `docs/R02_KAGGLE_TRAINING_GUIDE.md` Β§4 |
| accelerator | **GPU T4 Γ2** | Β§5 |
| internet | **On** (MiniLM downloads) | Β§5 |
| cells | **all 34, top to bottom** | Β§7 |
| hard stop | `HARD_STOP_SECONDS = 3 * 3600` (the plan's 3 h) | Β§7 |
| trainer defaults | `epochs=40, batch_size=128, seed=42, patience=6, time_limit=10800s` | Β§7 |
The path-discovery cells find the code root (four markers) and the CDVQA root (12 annotations + 4 image
dirs), and section 3c verifies the frozen STANet checkpoint by size and SHA256
(`RUNBOOK_CHANGE_VQA_KAGGLE.md` Β§5). **Test splits are not readable by the trainer:**
`training/change_vqa/train.py` loads only Train and Val, and *"there is no option in either file that
changes that"* (`scripts/train_change_vqa.py:11-15`).
### 21.2 VLM LoRA (Phase 6)
**Runbook:** `RUNBOOK_PHASE6_VLM_KAGGLE.md`.
| Property | Value | Source |
|---|---|---|
| notebook | `notebooks/kaggle_phase6_vlm_lora.ipynb` | `RUNBOOK_PHASE6_VLM_KAGGLE.md` Β§1 |
| accelerator | **GPU T4 Γ2** | Β§4 |
| internet | **off** (base model attached as input) | Β§4 |
| precision | **`fp16`, not the plan's `bf16`** β T4 is SM 7.5 | Β§4 |
| base model | `HuggingFaceTB/SmolVLM-500M-Instruct`, ~1.02 GB `model.safetensors` | Β§4 |
The trainer raises a `ConfigError` on `bf16` rather than silently falling back, and the deviation is
recorded in the manifest under `plan_deviations` (`RUNBOOK_PHASE6_VLM_KAGGLE.md` Β§4).
> **The VLM adapter's status is `ACCEPTANCE-REJECTED`.** Its metrics are *usable* (`exact_match 0.963`)
> but it was not promoted. **USABLE β ACCEPTED** (`DOCS_STYLE_GUIDE.md` Β§3). Do not describe the VLM path
> as accepted.
## 22. Calibration
```bash
python scripts/fit_calibration.py --dry-run # check inputs, exit
python scripts/fit_calibration.py # fit, write artifact
```
(`scripts/fit_calibration.py:19-21`)
It fits on **validation** data, and both the fitter entry point and the artifact writer **refuse a
held-out split** β *"Fitting on Test or Test2 would make the reported confidence a function of the answers
it is used to score β a leak, not a calibration."* (`scripts/fit_calibration.py:7-13`.)
> **Calibration made ECE worse** β `0.013755 β 0.014929` β and is **retained only because it is in the
> frozen config**. Never present it as an improvement (`DOCS_STYLE_GUIDE.md` Β§3).
---
# Part VIII β Coding conventions
## 23. What the code actually does
These conventions are visible across the files read. They are not aspirational style rules; each is
observable in the code.
### 23.1 Every module has a substantial docstring stating *why*
The files read are densely commented at the module and function level, and the comments explain decisions
and failure modes rather than restating the code. Examples: `core/registry.py:1-76` (a 76-line module
docstring on the capability-key trap), `deploy/codespace/launch.sh:1-24` (why the launcher is defensive),
`gateway/app.py:53-106` (the annotation-scope trap). **A change that removes the reasoning from a
comment removes the reason a future reader will not re-introduce the bug.**
### 23.2 `from __future__ import annotations` is standard
It appears at the top of nearly every module (`core/config.py:11`, `core/registry.py:78`,
`specialists/base.py:19`, `gateway/app.py:48`, `deploy/render/main.py:40`, `deploy/render/codespaces.py:16`,
`deploy/codespace/warm_cache.py:28`, `scripts/*.py`). It is convenient β and it is the direct cause of
Trap 6 (Β§30).
### 23.3 Findings are recorded as short codes, inline
The code refers to findings by code (`C-1`, `C-6`, `C-8`, `F5-2`, `P7-1`, `F-15`, `F-16`, `F-17`) and
states the failure each guard prevents. This is a documentation convention enforced by comments, e.g.
`core/config.py:121-142` (finding F5-2, the 17Γ overrun), `core/registry.py:227-235` (F-17, a config
surface removed rather than left as a silent no-op).
### 23.4 Pydantic models set `extra="forbid"` and carry validators
Every schema model forbids extra fields and uses `@field_validator` / `@model_validator` for cross-field
rules (Β§11). New models should follow the same pattern.
### 23.5 Loggers are module-level and named `satquery.<area>`
`logging.getLogger("satquery.orchestrator")` (`deploy/render/main.py:74`),
`logging.getLogger("satquery.registry")` (`core/registry.py:95`), `logging.getLogger(__name__)`
(`gateway/app.py:123`). Log calls use `%s`-style free text β there are no structured/JSON logs
(`docs/architecture/10-observability-and-ops.md` Β§4.4).
### 23.6 Client-visible strings are scrubbed
Server-side diagnostics that name absolute paths must not reach a client. `core/errors.scrub_paths` reduces
a path to a basename before it is published (`core/registry.py:297-312,560-598`;
`core/errors.py:39`). Any new client-visible field that could carry a path or an exception string must go
through the same scrub.
### 23.7 `noqa` comments state the reason
Where a lint suppression is used, the reason is written, e.g. `# noqa: E402 (see comment above)` in
`gateway/app.py:85-106`, and `# noqa: BLE001 - report, never crash the warm step` in
`deploy/codespace/warm_cache.py:74`.
### 23.8 Tests assert the *cause*, not the symptom
The regression test for the annotation-scope trap asserts that the route has no query parameters and that
`gateway.app.Request is starlette.requests.Request` β *"rather than the symptom, because asserting the
symptom would be brittle"* (`docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§13). Follow this when writing a
regression test.
### 23.9 The 88-column soft limit
The files read wrap around 88 characters. There is no committed linter config in the files read, so this
is a convention, not an enforced rule β **the exact formatter/linter configuration is `UNKNOWN β not
established from the available evidence`.**
---
# Part IX β Known development traps
## 24. The traps, in one table
| # | Trap | One-line consequence |
|---|---|---|
| 1 | the monorepo `deploy/` is stale and untracked | it is **not** the deployed source |
| 2 | the sandbox proxy is dead | outbound calls need `--noproxy '*'` / `ProxyHandler({})` |
| 3 | pytest exists only in the venv | the system `pytest` resolves to the wrong interpreter |
| 4 | Chrome drops synthetic CDP key events without OS focus | a browser-driven run silently answers the default query |
| 5 | Cloudflare `_headers` rules concatenate | a later rule cannot "fix" an earlier one |
| 6 | `from __future__ import annotations` + FastAPI | an unresolvable `Request` annotation becomes a **required query parameter** |
| 7 | the full-suite run trips the bulk-delete guard | 4 spurious `test_safe_delete_shim` failures |
| 8 | a stale serve process keeps answering | it reports the **previous revision's** capabilities |
## 25. Trap 1 β the stale untracked `deploy/`
**Symptom.** You edit `deploy/render/main.py`, deploy your change, and nothing changes in production β or
you read `deploy/render/main.py` and cannot find the tunnel code the live service runs.
**Root cause.** `deploy/` inside the monorepo is **stale and untracked**. `git status` reports
`?? deploy/` (verified in the working copy). It is **not** the deployed source
(`release/repo/docs/DEPLOYMENT.md` Β§1; `release/CURRENT_RELEASE_STATE.md` Β§6).
**Evidence of divergence.** The monorepo `deploy/render/main.py` (532 lines) exposes `/api/health` with a
`config` block that has **no** `tunnel` field and no `transport_mode` / `tunnel_timeout_s` /
`wake_timeout_s` keys (`deploy/render/main.py:444-466`), whereas the **live** payload carries all of them
(`release/repo/docs/DEPLOYMENT.md` Β§5). The monorepo copy also lacks `tunnel_agent.py` and `doctor.sh`,
both of which `launch.sh` references (`deploy/codespace/launch.sh:83,89,153,160-168,179`).
**Fix.** Fetch the deployed file from the private repository and diff before editing. Treat the monorepo
`deploy/` as documentation of intent, not as source.
## 26. Trap 2 β the dead sandbox proxy needs `--noproxy '*'`
**Symptom.** Outbound HTTP calls fail or hang in the authoring sandbox.
**Root cause.** The sandbox proxy is dead; requests are routed to it and never reach the target.
**Fix.** Disable proxies for the call (`release/repo/docs/REPRODUCIBILITY.md` Β§10.1):
```bash
curl --noproxy '*' https://<backend-host>/api/health
```
```python
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
```
`release/tools/hf_verify.py:37-38` does exactly this (`opener_no_proxy()`). In a normal environment the
flag is harmless; in the sandbox it is mandatory.
## 27. Trap 3 β pytest only in the venv
**Symptom.** Invoking the system `pytest` fails or resolves to a different interpreter.
**Root cause.** pytest is installed only in `.venv`.
**Fix.** Always invoke the venv interpreter explicitly (`release/repo/docs/REPRODUCIBILITY.md` Β§10.2):
```bash
.venv/Scripts/python.exe -m pytest tests/unit/test_frontend_live_wiring.py -q
```
## 28. Trap 4 β Chrome drops synthetic CDP key events
**Symptom.** A browser-driven run silently answers the *default* query; the query box looks untouched;
`mock_nodes` is non-zero.
**Root cause.** Chrome **drops synthesized key events when the browser window does not hold OS focus**.
`press_key` / `fill_input` (real CDP key events) are focus-gated; `Input.insertText` (`type_text`) is not.
**Measured.** With Chrome backgrounded, `press_key("Z")` left `#qtext.value` unchanged, while
`type_text("Q")` inserted fine (`release/repo/docs/REPRODUCIBILITY.md` Β§6.4).
**Fix.** Use `type_text` (not `fill_input`), and **assert the input state before dispatch** β `q_ok`
(the query box really held the query), `obs_ok` (`#obsTail == 'ready'`), `t0_ok` (both frames present
where required). This is the single most dangerous trap because it produces a **silent false pass** β the
pipeline "works", it just answered a different question
(`release/repo/docs/REPRODUCIBILITY.md` Β§10.5).
## 29. Trap 5 β Cloudflare `_headers` concatenate
**Symptom.** A specific cache-control rule does not take effect; the browser caches a file you expected it
to revalidate.
**Root cause.** Cloudflare `_headers` rules **concatenate, they do not override.** Two matching rules are
merged: a specific rule nested under a broad `/assets/img/*` rule yields
`max-age=604800, β¦, max-age=0, must-revalidate` β and **Chromium takes the FIRST `max-age`**. The file's
own "later rules override" comment is **false** (`release/repo/docs/DEPLOYMENT.md` Β§10).
**Fix.** Order rules so the *broadest* rule appears last, and never rely on a later rule overriding an
earlier one. Related: Cloudflare **308-redirects `X.html` β `/X`**, so reference the extensionless path
(`release/repo/docs/REPRODUCIBILITY.md` Β§10.4).
## 30. Trap 6 β the annotation-scope trap
**Symptom.** Every `POST` route returns `422` with FastAPI's own shape, without ever entering the handler:
```
POST /v1/analyze β 422
{"detail":[{"type":"missing","loc":["query","request"],"msg":"Field required"}]}
```
(`docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§13.)
**Root cause.** The module uses `from __future__ import annotations`, so every annotation is a **string**
at runtime. FastAPI resolves those strings via `get_typed_signature`, which calls
`eval(annotation, func.__globals__)`. If `Request` is imported **inside** `create_app`, the route closures
capture the name as a *local* of `create_app`; it never appears in `gateway.app.__globals__`, so
resolution fails and FastAPI is left holding a bare `ForwardRef('Request')`. **FastAPI does not raise** β
it silently falls back to treating the parameter as a **required query parameter named `request`**
(`gateway/app.py:53-73`).
Three things were wrong at once: the request body was never read, the gateway's own validation never ran,
and the error shape was FastAPI's `{"detail": ...}` rather than the contract's `{"error": {...}}`. **Every**
POST route was affected, including `/v1/assets`.
**The asymmetry to internalise.** An unresolvable **parameter** annotation is *silently reinterpreted*
(the handler never runs), while an unresolvable **return** annotation *raises*
(`pydantic.errors.PydanticUndefinedAnnotation: name 'JSONResponse' is not defined`, which made
`create_app()` unbuildable). Same root cause, opposite diagnosability
(`gateway/app.py:88-106`).
**Fix.** Bind the annotation subjects at **module scope** (`gateway/app.py:79-106`):
```python
if TYPE_CHECKING: # pragma: no cover
from starlette.requests import Request
from starlette.responses import Response
#: Runtime bindings used as annotation subjects in this module. Deliberately
#: module-level so `eval()` can find them. See the comment above.
from starlette.requests import Request # noqa: E402 (see comment above)
from starlette.responses import Response # noqa: E402 (see comment above)
from fastapi.responses import JSONResponse # noqa: E402 (see comment above)
```
The regression test asserts the **cause**: the route has no query parameters, and
`gateway.app.Request is starlette.requests.Request`.
> **The general rule.** Any name used as a FastAPI route annotation in a module with
> `from __future__ import annotations` **must** be importable from that module's globals. Import it at
> module scope, not inside the factory.
## 31. Trap 7 β the full-suite bulk-delete guard
**Symptom.** Running the whole `tests/unit` tree trips 4Γ `test_safe_delete_shim` failures.
**Root cause.** Windows **verbatim-path** defects in the sandbox's bulk-delete guard. The precise
condition under which the shim intermittently triggers on Windows is `UNKNOWN β not established from the
available evidence` (`release/repo/docs/REPRODUCIBILITY.md` Β§10.3).
**Fix / workaround.** Run the targeted suites (106 and 183 pass cleanly); treat the 4 shim failures as
environmental, not regressions (Β§17).
## 32. Trap 8 β the stale serve process
**Symptom.** The Codespace answers `/v1/health` and `/v1/capabilities`, but reports the **previous
revision's** capabilities.
**Root cause.** A serve process started before a code or environment change keeps serving from old code.
The serve process reads its environment exactly once, at startup (`deploy/codespace/launch.sh:100-102`).
**Fix.** `launch.sh` already handles it: it records a **stamp** of the revision and the asset
configuration and restarts the server when the stamp disagrees
(`deploy/codespace/launch.sh:100-147`). The rule for a developer: **a restart, not a reload, is required
after any env or revision change.**
> *"A stale serve process is worse than no process: it answers /v1/health and /v1/capabilities from OLD
> code, so the deployment looks alive while reporting the previous revision's capabilities."*
> (`deploy/codespace/launch.sh:111-113`)
### Related platform traps worth knowing
| Trap | Detail |
|---|---|
| a forwarded Codespace port returns `302` for a private repo | this is *why* the outbound tunnel exists (`release/repo/docs/DEPLOYMENT.md` Β§10) |
| the tunnel agent must be started by the devcontainer `postStartCommand` | a restarted Codespace comes up with `agent_connected: false` otherwise |
| never retry `POST /api/infer` at the gateway | a retry consumes inference twice |
| `containerEnv` applies only at container **creation** | an env change needs a restart, which is why `launch.sh` re-exports on every start (`deploy/codespace/launch.sh:49-52`) |
| the Codespace filesystem is **ephemeral** | uploaded assets and logs vanish with the Codespace (`deploy/codespace/launch.sh:44-47`) |
---
# Part X β Status and evidence
## 33. `NOT RUN` / `OPEN` / `BLOCKED` / `UNKNOWN` for development
| # | Item | Status |
|---|---|---|
| 1 | B-07 β tunnel gaps; patch prepared, **not deployed** | **`OPEN`** |
| 2 | B-02 β `/api/health` `codespace_name` trailing `\n` | **`OPEN` (cosmetic)** |
| 3 | A `LICENSE` file | **`OPEN`** β **no LICENSE file exists**; README says to add one before public release |
| 4 | The monorepo README | **materially stale** β it calls the frontend "hermetic", describes a 4-endpoint `/v1/*` contract, omits the tunnel, and points at the stale `deploy/` (`release/CURRENT_RELEASE_STATE.md` Β§6) |
| 5 | `hf/SETUP.md` and `hf/README.md` | **stale** β they assert the project ships no weights, which is now false (`release/CURRENT_RELEASE_STATE.md` Β§6) |
| 6 | The exact formatter / linter configuration | **`UNKNOWN`** β no committed config in the files read (Β§23.9) |
| 7 | The precise full-suite collected test count | **`UNKNOWN`** β per-suite counts are known, the total is not |
| 8 | The exact Windows trigger for the delete-shim flake | **`UNKNOWN`** |
| 9 | Whether `doctor.sh` / `tunnel_agent.py` exist in the deployed inference repo | **`UNKNOWN`** β the monorepo copy lacks them |
| 10 | A system-level end-to-end benchmark | **`NOT RUN`** β none exists |
| 11 | The router **test**-split number | **`NOT RUN`** β only validation (`n = 86`, ungated) exists |
| 12 | Captioning benchmark | **`NOT RUN`** β implemented, not benchmarked |
| 13 | A one-command retrain for the two external artifacts | **not implemented** β deliberate boundary (Β§21) |
| 14 | `torch.compile`, CUDA, quantisation, thread capping | **not implemented** β CPU-first by design (Β§1) |
| 15 | Enforcement of a one-model cache (`cache_max_models: 1`) | **UNVERIFIED** β the value is reported, not proven enforced (`docs/DEPLOYMENT_DECISION.md` Β§5) |
> **The stale-README trap is worth its own line.** `README.md` in the monorepo is *"materially stale"*: it
> calls the frontend *"hermetic β no backend calls"* (it calls `/api/*` on Render), puts Render/Codespace
> as *"in progress"* (both deployed), describes a 4-endpoint `/v1/*` contract (the live contract is
> `/api/*`), omits the tunnel, and points at the stale untracked `deploy/` as the deployment source
> (`release/CURRENT_RELEASE_STATE.md` Β§6). The public release documentation is authoritative; the
> monorepo README is not.
## 34. Where the evidence lives
| What | Where |
|---|---|
| the frozen config, invariants and hash | `configs/base.yaml`; `core/config.py:76-80,94-222` |
| the binding schemas | `core/schemas.py` |
| the specialist interface | `specialists/base.py` |
| the capability table and lazy construction | `core/registry.py` |
| the planner mapping | `core/planner.py:121-142` |
| the dependency profiles and contract notes | `requirements.txt` |
| the test markers | `pytest.ini` |
| the launcher and warm-up | `deploy/codespace/launch.sh`, `deploy/codespace/warm_cache.py` |
| the annotation-scope trap | `gateway/app.py:53-106`; `docs/STEP7_BACKEND_CHAIN_REPORT.md` Β§13 |
| the deploy manifest is inert | `docs/PHASE18_DEPLOYMENT_PACKAGING.md`; `scripts/validate_deploy_config.py` |
| the change-VQA Kaggle guide | `docs/R02_KAGGLE_TRAINING_GUIDE.md`; `RUNBOOK_CHANGE_VQA_KAGGLE.md` |
| the VLM LoRA Kaggle runbook | `RUNBOOK_PHASE6_VLM_KAGGLE.md` |
| the test suites and their expected results | `release/repo/docs/REPRODUCIBILITY.md` Β§5 |
| the environment traps | `release/repo/docs/REPRODUCIBILITY.md` Β§10 |
| the platform traps | `release/repo/docs/DEPLOYMENT.md` Β§10 |
| the live topology, env vars, cold start | `release/repo/docs/DEPLOYMENT.md` |
| the operations manual | [../OPERATIONS.md](OPERATIONS.md) |
| the factual inventory | `release/CURRENT_RELEASE_STATE.md` |
### Cross-references
| For | See |
|---|---|
| the frozen config and `Config.hash == 78f1e3700da15aa1` | [07 β Configuration and Freeze](architecture/07-configuration-freeze.md) |
| the specialist contract in depth | [05 β Specialists](architecture/05-specialists.md) |
| the router's five heads and the label space | [04 β Router](architecture/04-router.md) |
| the evidence and confidence stages | [06 β Evidence and Confidence](architecture/06-evidence-and-confidence.md) |
| the request lifecycle and the nine-state spine | [03 β Request Lifecycle](architecture/03-request-lifecycle.md) |
| the four endpoints and error codes | [08 β The API Contract](architecture/08-api-contract.md) |
| how to operate the live stack | [../OPERATIONS.md](OPERATIONS.md) |
| per-artifact hyperparameters | [../TRAINING.md](TRAINING.md) |
| what a third party can and cannot reproduce | [../REPRODUCIBILITY.md](REPRODUCIBILITY.md) |
---
> **Chapter summary.** Python 3.11+ and a CPU are sufficient; there is no CUDA requirement, and all
> placement is `.to(device)`, never `.cuda()`. The single registry is `configs/base.yaml`, its hash
> **`78f1e3700da15aa1`** is frozen, and **editing the config moves the hash and invalidates every artifact
> keyed to it** β the one non-reversible action in the repository. `core/schemas.py` is the binding
> contract and every specialist returns exactly `SpecialistResult`; the specialist interface is four
> methods in `specialists/base.py`, and a new specialist is added by implementing it and adding a
> `SpecialistSpec` row keyed on **capability**, not name. Tests run from the venv, targeted, because the
> full suite trips the sandbox's bulk-delete guard. Four artifacts train locally; two β change-VQA and the
> VLM LoRA β train on an external GPU and are not one-command reproducible. Eight development traps are
> recorded, of which the most dangerous are the stale untracked `deploy/`, the annotation-scope trap that
> turns a `Request` parameter into a required query parameter, and the Chrome focus trap that produces a
> silent false pass. `NOT RUN`/`OPEN` items include B-07 and B-02 (both `OPEN`), the absent `LICENSE`, the
> stale monorepo README and `hf/` docs, and several `UNKNOWN β not established from the available
> evidence` gaps.
|