thundercode commited on
Commit
ecb0365
·
verified ·
1 Parent(s): a8612a4

release: add docs/REPRODUCIBILITY.md

Browse files
Files changed (1) hide show
  1. docs/REPRODUCIBILITY.md +1185 -76
docs/REPRODUCIBILITY.md CHANGED
@@ -1,130 +1,1239 @@
1
  # Reproducibility
2
 
3
- **Status tags:** `IMPLEMENTED` · `VERIFIED` · `MEASURED` · `NOT RUN` · `BLOCKED`.
 
4
 
5
- This document states exactly what a third party can reproduce, with what, and what they cannot.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  ---
8
 
9
  ## 1. The reproducibility contract
10
 
11
- | Guarantee | How it is enforced |
12
- |---|---|
13
- | **Frozen configuration** | All tunables live in `configs/base.yaml`; no magic numbers in Python. The loader validates invariants and computes a hash. |
14
- | **Frozen config hash** | `78f1e3700da15aa1`. Every artifact records the hash it was produced against. |
15
- | **Pinned backbones** | Every backbone is pinned by revision in `configs/base.yaml`; the Hub resolves the exact commit. |
16
- | **Seed** | `project.seed: 42`. |
17
- | **Immutable public test** | `evaluation.immutable_public_test: true`; `hidden_data_access: false`. |
18
- | **Byte-verified artifacts** | Every released artifact ships with a sha256 in `models/checksums.sha256`. |
19
- | **Verified metrics** | Every quoted number is checked against its artifact by `tools/verify_readme_metrics.py`. |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- ### 1.1 Invariants the loader enforces
22
 
23
- Editing `configs/base.yaml` moves the hash and **invalidates every artifact keyed to it**. Two
24
- invariants exist because their violation is a *silent* error:
 
 
 
 
25
 
26
  ```
27
- fusion.input_dim == 3 * croma.encoder_dim + croma.optical_channels + croma.sar_channels # 2318
28
- grounding_head.feature_dim == 4 * grounding.encoder_projected_dim # 2048
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  ```
30
 
31
- The second is especially load-bearing: a mismatch is a silent shape error that torch only raises at
32
- the similarity step, after patch features are already cached. It is therefore rejected at load time.
 
 
 
 
 
 
 
 
 
33
 
34
- ## 2. Reproduce the metric verification (cheap, no GPU)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  ```bash
37
  python release/tools/verify_readme_metrics.py
38
  ```
39
 
40
- - **Reads** the artifacts under `artifacts/`.
41
- - **Compares** each of the 20 quoted metrics at the precision printed in the README.
42
- - **Also asserts** statuses (VLM headline contains `ACCEPTANCE-REJECTED`; router `corpus_limited` /
43
- `n_val`; calibration temperature and `ece_improvement`).
44
- - **Exits 0** and prints `ALL CLAIMS VERIFIED` only when everything matches.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- Committed output: `release/tools/readme_metrics_report.txt`.
 
 
47
 
48
- ## 3. Reproduce the environment (local run)
49
 
50
  ```bash
51
  git clone https://github.com/Anish-lab-blip/SatQuery-AI
52
  cd SatQuery-AI
53
  python -m venv .venv
54
- source .venv/Scripts/activate # Windows git-bash; .venv/bin/activate on Linux/macOS
55
  pip install -r requirements.txt
56
  ```
57
 
58
- Python 3.11+ and a CPU are sufficient. Device is selected via `SATQUERY_DEVICE`; all placement is
59
- `.to(device)`, never `.cuda()`. Backbones are fetched from the Hugging Face Hub on first use, pinned
60
- by revision.
61
 
62
- ## 4. Reproduce the tests
 
 
 
63
 
64
- | Suite | Command | Expected |
 
 
 
 
 
65
  |---|---|---|
66
- | Frontend live-wiring | `pytest tests/unit/test_frontend_live_wiring.py` | **106 passed** |
67
- | Doc/frontend suite | `pytest` on the 5 doc/frontend files | **183 passed** |
68
- | Full unit suite | `pytest tests/unit` | 5–6 **environmental** failures (sandbox delete guard × 4, 1 ordering flake, 1 stale adapter test) — see [`EVALUATION.md`](EVALUATION.md) §4 |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- > **Environment note.** In the authoring sandbox the full suite trips a **bulk-delete guard** and
71
- > pytest is only installed in the repository virtualenv. Neither affects a normal user environment.
 
 
 
72
 
73
- ## 5. Reproduce a live run
 
 
 
74
 
75
- The deployed stack is reachable:
76
 
77
  ```bash
78
  curl --noproxy '*' https://satquery-backend-m4yv.onrender.com/api/health
79
  curl --noproxy '*' https://satquery-backend-m4yv.onrender.com/api/capabilities
80
  ```
81
 
82
- `/api/capabilities` returns six tasks, all `available: true`. A live run requires the tunnel agent to
83
- be connected (`agent_connected:true`); if the Codespace is stopped, the request parks until the
84
- tunnel timeout. See [`DEPLOYMENT.md`](DEPLOYMENT.md) §5–6.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- ## 6. Reproduce the trained artifacts
 
87
 
88
- Backbones are public and pinned; the **trained modules** are released on the Hugging Face Hub under
89
- `thundercode/SatQuery`, each with its backbone dependency and checksum. To reproduce training:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  | Artifact | Where it trains | Guide |
92
  |---|---|---|
93
- | router adapter | local CPU | `configs/base.yaml` §router.training |
94
- | grounding head | local | `configs/base.yaml` §grounding_training |
95
- | change head | local | `configs/base.yaml` §change |
96
- | optical_sar fusion head | local, seed sweep | `docs/PHASE14_OPTICAL_SAR_DECISIONS.md` |
97
- | change_vqa head | **external GPU (Kaggle)** | `docs/R02_KAGGLE_TRAINING_GUIDE.md` |
98
- | vlm LoRA adapter | **external GPU** | `configs/base.yaml` §training |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- ### 6.1 What "reproduce" means for externally-trained artifacts
 
101
 
102
- The change-VQA and VLM artifacts were trained **outside this repository**. Reproducing them requires
103
- the external GPU environment described in their guides. The repository reproduces:
104
- - the **promotion gate** (byte-identity, sha256, zero non-finite tensors);
105
- - the **evaluation**;
106
- - the **serving wiring**.
107
 
108
- It does **not** ship a one-command retrain for those two artifacts. This is stated rather than
109
- implied.
 
 
 
 
 
110
 
111
- ## 7. What is NOT reproducible from this release
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  | Item | Reason |
114
  |---|---|
115
- | The private deployment repos | They are private; the deployed sources are not in this release. |
116
- | System-level end-to-end benchmark | **No such benchmark exists.** |
117
- | Router test-split number | **Not run.** |
118
- | CDVQA / SECOND imagery | Public but large; the release documents the acquisition + name-verification procedure, not the data. |
119
- | BigEarthNet full corpus | **Not downloaded** (only a 28k S2 subset was used). |
120
- | The historical ZeroGPU/Gradio deploy target | Frozen paperwork only; no runtime exists in code. |
121
-
122
- ## 8. Environment traps recorded for reproducibility
123
-
124
- - **Dead proxy in the authoring sandbox** — outbound calls need `--noproxy '*'` (curl) or
125
- `ProxyHandler({})` (Python).
126
- - **pytest only in the repo virtualenv** (`.venv/Scripts/python.exe`).
127
- - **Full-suite pytest trips a bulk-delete guard** in the sandbox.
128
- - **Cloudflare 308-redirects `X.html` → `/X`.**
129
- - **Chrome drops synthetic CDP key events when the window lacks OS focus** — relevant to any
130
- browser-driven reproduction of the live validation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Reproducibility
2
 
3
+ **Status tags:** `IMPLEMENTED` · `VERIFIED` · `MEASURED` · `ATTEMPTED` · `NOT RUN` · `BLOCKED` ·
4
+ `DEFERRED` · `REJECTED` · `OPEN` · `RESOLVED` · `CLOSED`.
5
 
6
+ This document states **exactly what a third party can reproduce, with what, and — just as
7
+ importantly — what they cannot**. It is written to the same depth as the rest of this documentation
8
+ set: real commands, real file paths, real hashes, real expected output, and an explicit status for
9
+ every claim.
10
+
11
+ It is deliberately **not** a "quickstart". A quickstart tells you the happy path and hides the
12
+ edges. This document records the edges, because in this project the edges are where the real
13
+ debugging time went — a dead sandbox proxy, a pytest that only exists in one virtualenv, a Chrome
14
+ that silently drops synthetic key events, a Hugging Face helper that returned an *empty file* and
15
+ would have produced a **false failure**. Those are documented in §10 so that a third party does not
16
+ rediscover them.
17
+
18
+ > **The one rule this document obeys, like every document in `docs/`:** *do not fabricate*. Every
19
+ > claim below is grounded in a file that was read (`core/config.py`, `configs/base.yaml`,
20
+ > `release/tools/*.py`, `artifacts/**`, `docs/**`). Where the evidence does not establish something,
21
+ > the text says `UNKNOWN — not established from the available evidence`.
22
+
23
+ ---
24
+
25
+ ## Table of contents
26
+
27
+ 1. [The reproducibility contract](#1-the-reproducibility-contract)
28
+ 2. [The frozen configuration](#2-the-frozen-configuration)
29
+ 3. [Reproduce the metric verification (cheap, no GPU)](#3-reproduce-the-metric-verification-cheap-no-gpu)
30
+ 4. [Reproduce the environment](#4-reproduce-the-environment)
31
+ 5. [Reproduce the tests](#5-reproduce-the-tests)
32
+ 6. [Reproduce a live run](#6-reproduce-a-live-run)
33
+ 7. [Reproduce and verify the released artifacts](#7-reproduce-and-verify-the-released-artifacts)
34
+ 8. [What "reproduce" means for the two externally-trained artifacts](#8-what-reproduce-means-for-the-two-externally-trained-artifacts)
35
+ 9. [What is NOT reproducible from this release — exhaustive](#9-what-is-not-reproducible-from-this-release--exhaustive)
36
+ 10. [Environment traps recorded for reproducibility](#10-environment-traps-recorded-for-reproducibility)
37
+ 11. [What is NOT RUN / OPEN / BLOCKED for this topic](#11-what-is-not-run--open--blocked-for-this-topic)
38
+ 12. [Where the evidence lives](#12-where-the-evidence-lives)
39
 
40
  ---
41
 
42
  ## 1. The reproducibility contract
43
 
44
+ Reproducibility here is not a slogan; it is a set of **enforced guarantees**. Each row below states a
45
+ guarantee, how it is enforced in code or in a tool, and how a third party can independently check
46
+ it.
47
+
48
+ | Guarantee | How it is enforced | How to check it |
49
+ |---|---|---|
50
+ | **Frozen configuration** | All tunables live in `configs/base.yaml`; there are no magic numbers in Python. The loader validates invariants and computes a hash. | Read `configs/base.yaml`; run the loader; compare the hash. |
51
+ | **Frozen config hash** | `Config.hash` = `sha256(json.dumps(data, sort_keys=True, default=str))[:16]` (`core/config.py:76-80`). Every evaluation run and every artifact records the hash it was produced against. | `Config.hash` returns `78f1e3700da15aa1`; the same string appears in `models/manifest.json` and each artifact. |
52
+ | **Pinned backbones** | Every backbone is pinned by `repo_id` **and** `revision` (a commit prefix, not a floating tag) in `configs/base.yaml`. | The four pins are listed in §4.3; each resolves to an exact Hub commit. |
53
+ | **Seed** | `project.seed: 42` (`core/config.py:82-84`). | Read the key; it is in the registry. |
54
+ | **Immutable public test** | `evaluation.immutable_public_test: true` and `evaluation.hidden_data_access: false` (`configs/base.yaml:273-277`). | Read the two keys; the corpus module refuses to mutate a sealed corpus. |
55
+ | **Leakage isolation** | `evaluation.leakage_split_key: scene_id` — splits are assigned by *scene*, never by *tile*. | Read the key; `evaluation/leakage.py` audits manifests. |
56
+ | **No official aggregate** | `evaluation.official_aggregate_weights: null`. There is no weighted composite score. | Read the key; `normalize.py` raises on aggregate requests. |
57
+ | **Byte-verified artifacts** | Every released artifact ships with a sha256 in `models/checksums.sha256`, generated from the actual files. | `sha256sum -c models/checksums.sha256`. |
58
+ | **Verified metrics** | Every number quoted in the README is checked against its source artifact by `release/tools/verify_readme_metrics.py`. | Run the tool; it prints `ALL CLAIMS VERIFIED`. |
59
+ | **Independent Hub verification** | `release/tools/hf_verify.py` re-downloads every uploaded file over direct HTTPS and re-hashes the bytes it received. | Run the tool; it prints `artifacts failed : 0`. |
60
+ | **Negative results preserved** | Rejected and open rulings stay in the record (`PHASE7_RESOLUTION_DECISION.md`, `phase6_closure.json`, `threshold_sweep_val.json`). | Read the artifacts; they contain the rejections, not scrubbed versions. |
61
+
62
+ ### 1.1 What the contract is *not*
63
+
64
+ The contract does **not** promise:
65
+
66
+ - a single command that retrains every artifact from scratch (§8);
67
+ - an end-to-end system-level accuracy number (§9 — no such benchmark exists);
68
+ - that the two externally-trained artifacts can be regenerated inside this repository (§8);
69
+ - bit-identical *training* on arbitrary hardware (only bit-identical *released artifacts* are
70
+ guaranteed, by hash).
71
+
72
+ ### 1.2 The reproducibility ladder
73
+
74
+ Reproduction here comes in four tiers, from cheapest to most expensive. A reviewer who wants to
75
+ spend five minutes gets tier 1; a reviewer who wants to audit the whole thing walks all four.
76
+
77
+ | Tier | What it proves | Cost | Section |
78
+ |---|---|---|---|
79
+ | **1. Metric verification** | Every quoted number matches its artifact | seconds, CPU | §3 |
80
+ | **2. Environment + tests** | The code installs and the suites behave as recorded | minutes, CPU | §4–§5 |
81
+ | **3. Live run** | The deployed stack answers correctly on unseen imagery | minutes (plus cold start), network | §6 |
82
+ | **4. Artifact + Hub verification** | The released weights are byte-identical to what was uploaded | minutes, network | §7 |
83
+
84
+ ---
85
+
86
+ ## 2. The frozen configuration
87
+
88
+ ### 2.1 What "frozen" means here
89
+
90
+ `configs/base.yaml` is the **single registry** for every tunable in the system. The design rule is:
91
+ *no magic numbers in Python*. If a value can change behaviour — an image size, a channel count, a
92
+ threshold, a precision, a split key — it lives in the registry, not in a function body.
93
+
94
+ This matters for reproducibility because it means the **entire behavioural surface of the system is
95
+ captured by one file**, and that file has a hash.
96
+
97
+ ### 2.2 The hash and how it is computed
98
+
99
+ ```python
100
+ @property
101
+ def hash(self) -> str:
102
+ """Stable hash of the whole registry. Recorded in every evaluation run."""
103
+ blob = json.dumps(self._data, sort_keys=True, default=str).encode()
104
+ return hashlib.sha256(blob).hexdigest()[:16]
105
+ ```
106
+
107
+ — `core/config.py:76-80`.
108
+
109
+ Three properties make this hash **stable**:
110
+
111
+ 1. **`sort_keys=True`** — key order in the YAML file does not change the hash.
112
+ 2. **`default=str`** — non-JSON-native values (dates, paths) are stringified deterministically.
113
+ 3. **`[:16]`** — the first 16 hex characters are used as the human-readable identifier.
114
+
115
+ The frozen value is:
116
+
117
+ ```
118
+ 78f1e3700da15aa1
119
+ ```
120
+
121
+ It is recorded in `models/manifest.json` (`"config_hash": "78f1e3700da15aa1"`), in each artifact's
122
+ metadata, and in the live evidence (each live run's panel shows the hash). This is the single
123
+ identifier that ties "the thing that produced this number" to "the configuration a reviewer is
124
+ reading".
125
+
126
+ ### 2.3 The enforced invariants, with exact arithmetic
127
+
128
+ Editing `configs/base.yaml` moves the hash and **invalidates every artifact keyed to it**. To make
129
+ that failure loud rather than silent, the loader rejects a config that violates a set of recorded
130
+ invariants. Two of them exist specifically because their violation is a *silent* error that torch
131
+ only surfaces much later.
132
+
133
+ #### Invariant 1 — fusion input dimension (finding C-1)
134
+
135
+ ```
136
+ fusion.input_dim == 3 * croma.encoder_dim + croma.optical_channels + croma.sar_channels
137
+ ```
138
+
139
+ With the frozen values `encoder_dim = 768`, `optical_channels = 12`, `sar_channels = 2`:
140
+
141
+ ```
142
+ fusion.input_dim == 3 * 768 + 12 + 2
143
+ == 2304 + 12 + 2
144
+ == 2318
145
+ ```
146
+
147
+ This is the concatenation of CROMA's three 768-d GAP vectors (`optical_GAP`, `SAR_GAP`,
148
+ `joint_GAP`) plus the 12 optical channels and 2 SAR channels of the sensor adapter. The loader
149
+ recomputes `expected` from the config and raises `ConfigError` if `fusion.input_dim` disagrees
150
+ (`core/config.py:154-163`). The docstring is explicit about *why*: "CROMA emits optical/SAR/joint
151
+ GAP vectors; the [declared value must match]". The upstream contract was independently reproduced
152
+ in `docs/PHASE14_OPTICAL_SAR_DECISIONS.md §1`, where the fusion input is recomputed at runtime
153
+ (`fusion_head.py:74`, `:311`) rather than hardcoded.
154
 
155
+ Two related guards fire alongside it (`core/config.py:165-168`):
156
 
157
+ ```
158
+ croma.optical_channels == 12 # CROMA s2_channels is fixed
159
+ croma.sar_channels == 2 # CROMA s1_channels is fixed
160
+ ```
161
+
162
+ #### Invariant 2 — grounding head feature dimension (finding P7-1)
163
 
164
  ```
165
+ grounding_head.feature_dim == 4 * grounding.encoder_projected_dim
166
+ ```
167
+
168
+ With `encoder_projected_dim = 512`:
169
+
170
+ ```
171
+ grounding_head.feature_dim == 4 * 512 == 2048
172
+ ```
173
+
174
+ The loader's own comment explains why this is **load-bearing**:
175
+
176
+ > A mismatch here is a SILENT shape error. torch only raises at the [similarity step], after patch
177
+ > features are already cached. It is therefore rejected at load time.
178
+
179
+ — `core/config.py:176-195`.
180
+
181
+ The "4×" arises because the grounding head consumes four projected feature streams; the projected
182
+ dimension is declared in config precisely so this guard can exist without hardcoding 512.
183
+
184
+ #### Further load-time guards
185
+
186
+ The `_validate()` method (`core/config.py:94`) also enforces:
187
+
188
+ | Guard | Rule | Finding |
189
+ |---|---|---|
190
+ | CROMA resolution | `croma.image_resolution` is an int and a multiple of 8 (CROMA asserts `% 8 == 0`) | C-7 |
191
+ | Training precision | `training.precision ∈ {fp16, bf16, fp32}` | C-6 |
192
+ | ZeroGPU compile ban | `deployment.torch_compile` must not be `True` — ZeroGPU does not support `torch.compile` | C-8 |
193
+ | VLM prompt template | `vlm.prompt_must_use_chat_template` must be `true` — SmolVLM raises otherwise | — |
194
+
195
+ These are the invariants a reviewer can verify by reading one method. They are the reason a
196
+ *plausible-looking* config edit fails at load instead of producing a wrong number three hours later.
197
+
198
+ ### 2.4 The evaluation invariants
199
+
200
+ ```yaml
201
+ evaluation:
202
+ immutable_public_test: true
203
+ hidden_data_access: false
204
+ official_aggregate_weights: null
205
+ leakage_split_key: scene_id
206
  ```
207
 
208
+ — `configs/base.yaml:273-277`.
209
+
210
+ | Key | Value | Meaning |
211
+ |---|---|---|
212
+ | `immutable_public_test` | `true` | The public-test corpus is sealed; it cannot be mutated by an evaluation. |
213
+ | `hidden_data_access` | `false` | No code path may read hidden data during a scored run. |
214
+ | `official_aggregate_weights` | `null` | **There is no composite score.** A single "overall accuracy" is forbidden by construction. |
215
+ | `leakage_split_key` | `scene_id` | Splits are assigned by scene, so tiles from one scene never straddle train/test. |
216
+
217
+ The evaluation-honesty rules that these keys serve are stated in full in
218
+ [`EVALUATION.md`](EVALUATION.md) §2.
219
 
220
+ ### 2.5 The deployment section is frozen paperwork
221
+
222
+ ```yaml
223
+ deployment:
224
+ platform: huggingface-spaces
225
+ sdk: gradio
226
+ zerogpu: true
227
+ torch_compile: false
228
+ ...
229
+ ```
230
+
231
+ — `configs/base.yaml:282-295`.
232
+
233
+ The deployment target described here is the **superseded** HF-Space/ZeroGPU design. The active
234
+ topology is Cloudflare Pages → Render → Codespace (`docs/DEPLOYMENT.md §1`). This section is
235
+ retained as **frozen paperwork**: editing it would move the config hash, and the hash is what every
236
+ artifact is keyed to. No Gradio runtime exists in code. The `torch_compile: false` line is the
237
+ C-8 guard and remains meaningful regardless of host.
238
+
239
+ ### 2.6 The hash-exempt path
240
+
241
+ One decision (DEV-2, the CROMA input-normalisation question) needed to become inspectable **without**
242
+ adding a `base.yaml` key — because adding a key would have moved the hash away from `78f1e3700da15aa1`
243
+ and invalidated every artifact. The resolution was a hash-exempt path
244
+ (`docs/PHASE14_OPTICAL_SAR_DECISIONS.md`, item 7):
245
+
246
+ ```
247
+ SATQUERY_CROMA_USE_8_BIT (env) → croma.use_8_bit (config) → default True
248
+ ```
249
+
250
+ It is inspectable at runtime via `CROMAEncoder.describe()["input_normalisation"]`. This is recorded
251
+ because it is the one place where "frozen" and "configurable" were reconciled deliberately, and a
252
+ reviewer should know the escape hatch exists and is documented rather than hidden.
253
+
254
+ ### 2.7 What invalidates the hash
255
+
256
+ Anything that changes `self._data` when serialised with `sort_keys=True`. Concretely:
257
+
258
+ - editing any value in `configs/base.yaml`;
259
+ - adding or removing a key;
260
+ - changing a value's *type* such that its `default=str` rendering changes.
261
+
262
+ The consequence is not merely a different hash; it is that **every artifact keyed to
263
+ `78f1e3700da15aa1` no longer describes the running configuration**, and the honest action is to
264
+ re-run the evaluation rather than reinterpret the old numbers.
265
+
266
+ ---
267
+
268
+ ## 3. Reproduce the metric verification (cheap, no GPU)
269
+
270
+ This is the cheapest, highest-value reproduction in the whole release. It takes seconds, needs no
271
+ GPU and no network, and it is the check that a skeptical reviewer should run **first**.
272
+
273
+ ### 3.1 The verifier
274
 
275
  ```bash
276
  python release/tools/verify_readme_metrics.py
277
  ```
278
 
279
+ `release/tools/verify_readme_metrics.py` is read-only. Its own docstring states its contract:
280
+
281
+ > Verify every metric quoted in the release README against its source artifact. Read-only. Each
282
+ > claim is compared at the precision at which the README states it.
283
+
284
+ The tool is **not** a re-computation of the metrics. It is a **provenance check**: it reads the
285
+ artifact that the README cites, resolves a dotted key into it, and compares the value against the
286
+ literal string the README prints — at the README's own precision.
287
+
288
+ ### 3.2 The 20 claims and their exact key paths
289
+
290
+ The claim list is not assumed; the key names were **discovered by walking the artifacts**. Several
291
+ live under nested paths, and the tool's docstring says so:
292
+
293
+ > Key names were discovered by walking the artifacts, NOT assumed: several live under nested paths
294
+ > (e.g. change metrics are `metrics.pooled.iou`, grounding is `results.head_threshold.mean_best_iou`,
295
+ > VLM is `why_usable_verified.adapted_test.*`).
296
+
297
+ | # | Claim | Artifact | Dotted key | README value |
298
+ |---|---|---|---|---|
299
+ | 1 | change pooled IoU | `artifacts/change/eval_test/eval_result.json` | `metrics.pooled.iou` | `0.8122` |
300
+ | 2 | change macro IoU | `artifacts/change/eval_test/eval_result.json` | `metrics.macro.miou` | `0.8457` |
301
+ | 3 | change pooled F1 | `artifacts/change/eval_test/eval_result.json` | `metrics.pooled.f1` | `0.8964` |
302
+ | 4 | grounding canonical head_threshold mean_best_IoU | `artifacts/grounding/remoteclip_grounding_v001/eval_result_canonical.json` | `results.head_threshold.mean_best_iou` | `0.2838` |
303
+ | 5 | grounding canonical head_threshold recall@0.5 | `…/eval_result_canonical.json` | `results.head_threshold.recall.0.50` | `0.2198` |
304
+ | 6 | grounding matched6 head_threshold mean_best_IoU | `…/eval_result_matched6.json` | `results.head_threshold.mean_best_iou` | `0.2566` |
305
+ | 7 | grounding matched6 head_threshold recall@0.5 | `…/eval_result_matched6.json` | `results.head_threshold.recall.0.50` | `0.1938` |
306
+ | 8 | grounding head_argmax mean_best_IoU (canonical) | `…/eval_result_canonical.json` | `results.head_argmax.mean_best_iou` | `0.1215` |
307
+ | 9 | grounding zero-shot baseline IoU (canonical) | `…/eval_result_canonical.json` | `results.zero_shot_matched.mean_best_iou` | `0.0972` |
308
+ | 10 | optical-SAR fusion accuracy | `artifacts/optical_sar/fusion_head_production_v001/pre_registered_115_metric.json` | `accuracy` | `0.931` |
309
+ | 11 | optical-SAR fusion macro_F1 | `…/pre_registered_115_metric.json` | `macro_f1` | `0.434161` |
310
+ | 12 | change_vqa test accuracy | `artifacts/change_vqa/run/PROMOTION.json` | `verification.test_accuracy` | `0.697626` |
311
+ | 13 | change_vqa test macro_F1 | `artifacts/change_vqa/run/PROMOTION.json` | `verification.test_macro_f1` | `0.378373` |
312
+ | 14 | change_vqa test2 accuracy | `artifacts/change_vqa/run/PROMOTION.json` | `verification.test2_accuracy` | `0.651469` |
313
+ | 15 | change_vqa test2 macro_F1 | `artifacts/change_vqa/run/PROMOTION.json` | `verification.test2_macro_f1` | `0.372309` |
314
+ | 16 | router overall ungated accuracy | `artifacts/router/threshold_sweep_val.json` | `overall_ungated_accuracy` | `0.965116` |
315
+ | 17 | calibration ECE before scaling | `artifacts/calibration_v001.json` | `metrics.ece_before` | `0.013755` |
316
+ | 18 | calibration ECE after scaling | `artifacts/calibration_v001.json` | `metrics.ece_after` | `0.014929` |
317
+ | 19 | VLM adapter exact_match | `artifacts/vlm/phase6_closure.json` | `why_usable_verified.adapted_test.exact_match` | `0.963` |
318
+ | 20 | VLM adapter F1 | `artifacts/vlm/phase6_closure.json` | `why_usable_verified.adapted_test.f1` | `0.96432` |
319
+
320
+ Note rows 1–3, 6–7, 12–15, and 16: these are the rows that keep the honesty rules honest. The
321
+ change numbers are quoted as **pooled and macro together** (rule 4); grounding is quoted under
322
+ **both protocols** (rule 2); change-VQA is quoted under **both test sets** (rule 3); the router
323
+ number is quoted with its **validation-only** caveat (rule 5). A verifier that checked only one
324
+ number per task would be reproducible and *wrong*.
325
+
326
+ ### 3.3 The status assertions
327
+
328
+ Metrics are not the only claims. After the 20 numeric comparisons, the tool asserts four statuses
329
+ that the README also states (`verify_readme_metrics.py:117-131`):
330
+
331
+ ```
332
+ VLM headline contains ACCEPTANCE-REJECTED : True
333
+ VLM status : CLOSED
334
+ router corpus_limited : True
335
+ router n_val : 86
336
+ calibration temperature (temperature_scaling.temperature) : 0.9772731820958189
337
+ calibration ece_improvement : -0.001174 (negative => calibration did NOT help)
338
+ ```
339
+
340
+ These are the checks that prevent the documentation from quietly upgrading a status. In particular:
341
+
342
+ - the **VLM** metric is usable but the artifact's own headline contains `ACCEPTANCE-REJECTED` — so
343
+ `USABLE ≠ ACCEPTED` (rule 7);
344
+ - the **router** number is flagged `corpus_limited: true` with `n_val: 86` — so it can never be
345
+ presented as a test number (rule 5);
346
+ - the **calibration** `ece_improvement` is **negative**, i.e. calibration made ECE *worse*, and the
347
+ tool prints that interpretation inline (rule 6).
348
+
349
+ ### 3.4 The nested-key resolution trick
350
+
351
+ Two subtleties in the resolver are worth recording, because they are the kind of thing that turns a
352
+ verifier into a **false-pass** machine if done naively (`verify_readme_metrics.py:69-90`):
353
+
354
+ 1. **Keys that themselves contain dots.** The grounding recall dictionary is keyed by the string
355
+ `"0.10"`, `"0.25"`, `"0.50"`. A naive `dotted.split(".")` walk would break
356
+ `results.head_threshold.recall.0.50` into `…recall → 0 → 50` and fail. The resolver therefore
357
+ tries the **longest matching key at each step first**, so `0.50` stays intact.
358
+ 2. **Precision matching.** The comparison is
359
+ `round(float(val), decimals(claimed)) == float(claimed)` — i.e. the artifact value is rounded to
360
+ the number of decimals the README prints, then compared. This is why row 15 shows the artifact
361
+ value `0.372308516` matching the README's `0.372309`: the README states six decimals and the
362
+ artifact rounds to it.
363
+
364
+ A verifier that compared raw floats would report false differences on every rounded value. A
365
+ verifier that split dots naively would report false differences on the recall rows. Both failure
366
+ modes are **silent false failures** — the same class of bug discussed in §7.3 and §10.
367
+
368
+ ### 3.5 The committed output
369
+
370
+ ```
371
+ STATUS claim artifact readme source
372
+ ----------------------------------------------------------------------------------------------------------------------
373
+ MATCH change pooled IoU 0.8122 0.8122 artifacts/change/eval_test/eval_result.json#metrics.pooled.iou
374
+ MATCH change macro IoU 0.8457 0.8457 artifacts/change/eval_test/eval_result.json#metrics.macro.miou
375
+ MATCH change pooled F1 0.8964 0.8964 artifacts/change/eval_test/eval_result.json#metrics.pooled.f1
376
+ MATCH grounding canonical head_threshold mean_best_IoU 0.2838 0.2838 artifacts/grounding/remoteclip_grounding_v001/eval_result_canonical.json#results.head_threshold.mean_best_iou
377
+ ...
378
+ MATCH VLM adapter exact_match 0.963 0.963 artifacts/vlm/phase6_closure.json#why_usable_verified.adapted_test.exact_match
379
+ MATCH VLM adapter F1 0.96432 0.96432 artifacts/vlm/phase6_closure.json#why_usable_verified.adapted_test.f1
380
+
381
+ === status assertions ===
382
+ VLM headline contains ACCEPTANCE-REJECTED : True
383
+ VLM status : CLOSED
384
+ router corpus_limited : True
385
+ router n_val : 86
386
+ calibration temperature (temperature_scaling.temperature) : 0.9772731820958189
387
+ calibration ece_improvement : -0.001174 (negative => calibration did NOT help)
388
+
389
+ RESULT: ALL CLAIMS VERIFIED
390
+ ```
391
+
392
+ — `release/tools/readme_metrics_report.txt` (committed).
393
+
394
+ The tool **exits 0** and prints `ALL CLAIMS VERIFIED` only when every numeric comparison is `MATCH`
395
+ and no artifact is missing or lacks the key. Any `NOFILE`, `NOKEY`, or `DIFFER` line increments the
396
+ failure counter, the final line reads `N CLAIM(S) FAILED`, and the process exits 1. This makes the
397
+ check usable in CI: a non-zero exit is a hard failure.
398
+
399
+ ### 3.6 Why this is a real check and not a tautology
400
+
401
+ A skeptic might object: "you wrote the README from the artifacts, so of course they match." The
402
+ answer is that the check is not that the README was *derived* from the artifacts, but that the
403
+ README's published claims remain **mechanically traceable** to files a third party also has. If
404
+ someone edits the README to state a nicer number, the tool fails. If someone edits an artifact to a
405
+ different number, the tool fails. The check binds the published prose to the recorded evidence, and
406
+ it does so without trusting either.
407
+
408
+ ---
409
+
410
+ ## 4. Reproduce the environment
411
+
412
+ ### 4.1 What you need
413
+
414
+ - **Python 3.11+** (`README.md` §Installation: "Python 3.11+ and a CPU are sufficient").
415
+ - A **CPU is enough**. There is no CUDA requirement.
416
+ - Network access on first use, to fetch the pinned backbones from the Hugging Face Hub.
417
+
418
+ ### 4.2 The two install profiles
419
+
420
+ `requirements.txt` documents two profiles explicitly:
421
+
422
+ ```text
423
+ # CPU (local dev / schema / geo / unit tests):
424
+ # pip install -r requirements.txt
425
+ # GPU (Kaggle T4x2 / HF ZeroGPU): torch is preinstalled on both.
426
+ # Do NOT pin torch here — Kaggle and HF ship their own builds.
427
+ ```
428
+
429
+ — `requirements.txt` (header comment).
430
+
431
+ The pins, with the contract each one carries (`requirements.txt`):
432
+
433
+ | Package | Pin | Why |
434
+ |---|---|---|
435
+ | `numpy` | `>=1.26,<3` | core |
436
+ | `pyyaml` | `>=6.0` | config loading |
437
+ | `pydantic` | `>=2.6,<3` | schema |
438
+ | `rasterio` | `>=1.3` | geospatial IO |
439
+ | `pyproj` | `>=3.6` | CRS handling |
440
+ | `opencv-python-headless` | `>=4.9` | image ops |
441
+ | `transformers` | `>=4.52` | SmolVLM via `AutoModelForImageTextToText`; `AutoModelForVision2Seq` is deprecated (finding **C-2**) |
442
+ | `open-clip-torch` | `>=2.24` | RemoteCLIP loaded via `pretrained=<path>` so `load_checkpoint()` runs its state-dict fixups (**C-4**) |
443
+ | `sentence-transformers` | `>=2.7` | router embedding |
444
+ | `peft` | `>=0.10` | LoRA adapter |
445
+ | `huggingface_hub` | `>=0.23` | pin RemoteCLIP / SmolVLM / CROMA revisions |
446
+ | `safetensors` | `>=0.4` | adapter serialisation |
447
+ | `einops` | `>=0.7` | **required** by the vendored `use_croma.py` (`from einops import rearrange`) |
448
+ | `gradio` | `>=4.44` | UI (frozen deployment target) |
449
+ | `reportlab` | `>=4.1` | reporting |
450
+ | `pytest` | `>=8.0` | tests |
451
+ | `pytest-cov` | `>=5.0` | coverage |
452
+
453
+ The comment block closes with an instruction a reviewer should heed: *"do not 'fix' by changing these
454
+ pins"* — the pins encode verified contracts, not preferences.
455
+
456
+ ### 4.3 The `einops` lesson — a dependency no document declared
457
+
458
+ `requirements.txt` carries an unusually explicit note about `einops`:
459
+
460
+ > REQUIRED by the vendored `specialists/optical_sar/vendor/use_croma.py` (`from einops import
461
+ > rearrange`). Found missing on 2026-09-18 by executing the vendored module: it raised
462
+ > `ModuleNotFoundError`, so the CROMA path was blocked on a dependency that no document declared.
463
+ > Not optional: without it the encoder cannot be imported at all.
464
 
465
+ This is recorded verbatim because it is a reproducibility fact: a missing dependency that no
466
+ document mentioned was found **by executing the code**, not by reading a spec. A third party
467
+ building the environment will hit the same wall if they install from a partial list.
468
 
469
+ ### 4.4 Create the environment
470
 
471
  ```bash
472
  git clone https://github.com/Anish-lab-blip/SatQuery-AI
473
  cd SatQuery-AI
474
  python -m venv .venv
475
+ source .venv/Scripts/activate # Windows git-bash; use .venv/bin/activate on Linux/macOS
476
  pip install -r requirements.txt
477
  ```
478
 
479
+ — `README.md` §Installation.
 
 
480
 
481
+ > **Trap (Windows, recorded in §10.2).** In the authoring environment, `pytest` exists **only** in
482
+ > the repository virtualenv. Invoking the system `pytest` fails or resolves to a different
483
+ > interpreter. Always invoke the venv interpreter explicitly:
484
+ > `.venv/Scripts/python.exe -m pytest …`.
485
 
486
+ ### 4.5 Backbones are fetched, pinned by revision, and never redistributed
487
+
488
+ The four third-party backbones are pinned by `repo_id` **and** `revision` (`README.md`
489
+ §Reproducibility item 2; `models/manifest.json` per-artifact `backbone` fields):
490
+
491
+ | Model | Revision | Role |
492
  |---|---|---|
493
+ | `HuggingFaceTB/SmolVLM-500M-Instruct` | `a7da5b986cb5` | VQA + captioning backbone |
494
+ | `chendelong/RemoteCLIP` | `bf1d8a3ccf2d` | remote-sensing grounding encoder |
495
+ | `sentence-transformers/all-MiniLM-L6-v2` | `1110a243fdf4` | router embedding |
496
+ | `antofuller/CROMA` | `0dd28e3d633b` | optical/SAR fusion encoder |
497
+
498
+ **No backbone weights are redistributed** in this release (`release/HF_RELEASE_VERIFICATION.md §4`).
499
+ The released artifacts are small trained modules *over* these frozen backbones; anyone using them
500
+ must fetch the backbones too. Pinning by commit prefix (not by a floating tag like `main`) is what
501
+ makes "the same backbone" reproducible months later.
502
+
503
+ ### 4.6 Device selection
504
+
505
+ Device is selected via `SATQUERY_DEVICE`, and **all placement is `.to(device)`, never `.cuda()`**
506
+ (`README.md` §Installation; `core/config.py:86-91`):
507
+
508
+ ```python
509
+ @property
510
+ def device_preference(self) -> str:
511
+ override = os.environ.get("SATQUERY_DEVICE")
512
+ if override:
513
+ return override
514
+ return "cuda" if _torch_cuda_available() else "cpu"
515
+ ```
516
+
517
+ This is why a CPU-only reviewer can run everything except the training paths. The env override is
518
+ read **without importing torch** in the deployment context (`docs/DEPLOYMENT.md §3.2`), so a
519
+ misconfigured device cannot crash the service at import time.
520
+
521
+ ---
522
+
523
+ ## 5. Reproduce the tests
524
+
525
+ ### 5.1 The suites and their expected results
526
+
527
+ | Suite | Command | Expected | Status |
528
+ |---|---|---|---|
529
+ | Frontend live-wiring | `python -m pytest tests/unit/test_frontend_live_wiring.py -q` | **106 passed** | `VERIFIED` |
530
+ | Doc/frontend suite | `pytest` on the 5 doc/frontend files | **183 passed** | `VERIFIED` |
531
+ | Full unit suite | `python -m pytest tests/unit` | **5–6 environmental/ordering failures**, rest pass; a re-run passes 137 | `MEASURED` |
532
+
533
+ The three rows tell a story a single number would hide: the two **targeted** suites are clean, and
534
+ the **full** suite has 5–6 failures that are environmental or ordering-related, not regressions. The
535
+ next subsections reproduce each.
536
+
537
+ ### 5.2 Frontend live-wiring regression suite — 106 passed
538
+
539
+ ```bash
540
+ python -m pytest tests/unit/test_frontend_live_wiring.py -q
541
+ ```
542
+
543
+ — `README.md` §Local development.
544
+
545
+ This suite is the regression net for the frontend↔backend wiring: the query box, the intent panel,
546
+ the task tags, the run identifiers, and the evidence rendering. It is the suite that would catch a
547
+ re-introduction of the router defect described in §6.4.
548
+
549
+ **Expected: 106 passed.** Note the historical figure, because a reviewer may see both numbers:
550
+ `docs/FINAL_DELIVERY_REPORT.md §7` records **94 passed** for this file, while
551
+ `CURRENT_RELEASE_STATE.md:121` and `README.md` §The test suites record **106 passed** ("re-run this
552
+ session"). The difference is the suite growing after the delivery report was written — the
553
+ regression tests added for the router fix took this file from 100 → 106 tests
554
+ (`DELIVERY_REPORT_2026-09-25.md`, regression-tests note). This document quotes **106**, the
555
+ re-run-this-session figure, and cites both. (The exact count of tests *collected* in a single
556
+ full-suite invocation is `UNKNOWN — not established from the available evidence`; the per-suite
557
+ counts are what the evidence establishes.)
558
+
559
+ ### 5.3 Doc/frontend suite — 183 passed
560
+
561
+ The documentation-and-frontend suite is **five files**, named explicitly in
562
+ `docs/FINAL_DELIVERY_REPORT.md §7`:
563
+
564
+ - `test_frontend_guide_doc`
565
+ - `test_frontend_live_wiring`
566
+ - `test_api_contract_doc`
567
+ - `test_runbook_doc`
568
+ - `test_deploy_config`
569
+
570
+ Together they report **183 passed**. These are the checks that the docs' internal links resolve, the
571
+ frontend assets are present, the API contract document matches the code, the runbook is consistent,
572
+ and the deploy config is coherent.
573
+
574
+ The same three-row test table is published in this release at `README.md` §The test suites
575
+ (`README.md:1063-1074`), so a public reader can see the counts without the project-internal report.
576
+ The underlying record is `docs/FINAL_DELIVERY_REPORT.md §7` (project-internal); the frontend count
577
+ was re-run this session to **106** (`release/CURRENT_RELEASE_STATE.md:121`). See also
578
+ [`EVALUATION.md`](EVALUATION.md) §7.2.
579
+
580
+ ### 5.4 Full `tests/unit` — the 5–6 failures, reported honestly
581
+
582
+ Running the entire unit tree trips **5–6** failures. They are reported here rather than hidden, and
583
+ they are classified by cause exactly as `docs/FINAL_DELIVERY_REPORT.md §7` records them:
584
+
585
+ | # | Failure | Cause | Regression? |
586
+ |---|---|---|---|
587
+ | 1–4 | `test_safe_delete_shim` (×4) | the sandbox's bulk-**delete guard** (Windows verbatim-path behaviour) | **No** |
588
+ | 5 | one ordering flake in the **router route test** | passes in isolation; order/collection-dependent | **No** |
589
+ | 6 | one **stale adapter test** | asserts `optical_sar` absent when CROMA is *unshipped* — **CROMA is now shipped** | **No** |
590
+
591
+ **Why these are not regressions.** They were isolated by a re-run of the affected files **together**,
592
+ which passes **137** tests (`docs/FINAL_DELIVERY_REPORT.md §7`;
593
+ [`EVALUATION.md`](EVALUATION.md) §7.3–7.4). The `test_safe_delete_shim` failures are Windows-specific
594
+ verbatim-path behaviour that does not occur on the deployment host; the ordering flake disappears when
595
+ the router route test runs in isolation; the stale adapter test encodes an assumption (CROMA not yet
596
+ shipped) that the codebase has since outgrown. None of them is a behavioural defect in shipped code.
597
+
598
+ > **Honesty note.** The precise full-suite *collected* count is
599
+ > `UNKNOWN — not established from the available evidence`. What the evidence establishes is: the two
600
+ > targeted suites pass (106, 183), the full suite has 5–6 environmental/ordering failures, and a
601
+ > re-run of the affected files passes 137.
602
+
603
+ ### 5.5 The evidence-engine determinism suite
604
+
605
+ The evidence engine (the component that turns specialist output into a *proof*) has its own suite —
606
+ `tests/unit/test_evidence_engine.py` (73 tests) — covering **determinism** and **purity**: the same
607
+ input produces the same evidence, and computing evidence does not mutate inputs. This is what makes
608
+ the "evidence is independent of the verdict" property in §6.5 hold.
609
+
610
+ ### 5.6 Invocation traps
611
+
612
+ - **Use the venv interpreter.** `.venv/Scripts/python.exe -m pytest …` on Windows.
613
+ - **The full-suite run trips a bulk-delete guard** in the authoring sandbox (§10.3). Run the
614
+ targeted suites to avoid it.
615
+ - **Do not pipe pytest through `grep`** in the authoring sandbox — output is block-buffered and a
616
+ killed pipeline swallows it (§10.6). Redirect to a file instead.
617
+
618
+ ---
619
+
620
+ ## 6. Reproduce a live run
621
+
622
+ ### 6.1 The deployed stack
623
+
624
+ The live topology is **Cloudflare Pages → Render → outbound tunnel → GitHub Codespace**
625
+ (`docs/DEPLOYMENT.md §1`; `README.md` §Deployment):
626
+
627
+ | Layer | Role | Host |
628
+ |---|---|---|
629
+ | Cloudflare Pages | static frontend | `https://satquery.pages.dev` |
630
+ | Render | orchestrator / API gateway (`/api/*`, CORS, wake flow) | `https://satquery-backend-m4yv.onrender.com` |
631
+ | GitHub Codespace | FastAPI inference host, CPU, port 8000 | `potential-space-trout-r4ppw969w45j2pvvw` |
632
+
633
+ Deployed revisions (VERIFIED):
634
 
635
+ | Component | Revision |
636
+ |---|---|
637
+ | Frontend | `2d7ae53b482d` |
638
+ | Backend / orchestrator | `89d80eaddec5` |
639
+ | Inference | `5a0936ace491` |
640
 
641
+ > **Trap.** The deployment sources are **private** repositories, separate from this release. The
642
+ > `deploy/` directory inside the monorepo working copy is **stale and untracked** — it is *not* the
643
+ > deployed source (`docs/DEPLOYMENT.md §1`). Reproducing a live run means driving the deployed
644
+ > endpoints, not rebuilding from `deploy/`.
645
 
646
+ ### 6.2 Health and capabilities
647
 
648
  ```bash
649
  curl --noproxy '*' https://satquery-backend-m4yv.onrender.com/api/health
650
  curl --noproxy '*' https://satquery-backend-m4yv.onrender.com/api/capabilities
651
  ```
652
 
653
+ The `/api/health` payload (recorded live, `docs/DEPLOYMENT.md §2`):
654
+
655
+ ```json
656
+ {"status":"ok","service":"satquery-orchestrator",
657
+ "tunnel":{"agent_connected":true,"agent_id":"codespaces-fd1038","pending":0,"completed":97},
658
+ "config":{"codespace_name":"potential-space-trout-r4ppw969w45j2pvvw\n","codespace_port":8000,
659
+ "transport_mode":"auto","tunnel_timeout_s":150.0,"wake_timeout_s":120.0,
660
+ "upstream_timeout_s":90.0,"device":"cpu","has_github_token":true}}
661
+ ```
662
+
663
+ `/api/capabilities` returns **six tasks, all `available: true`**. A live run requires
664
+ `tunnel.agent_connected: true`; if the Codespace is stopped, the request parks until the tunnel
665
+ timeout (`docs/DEPLOYMENT.md §5`).
666
+
667
+ > **`--noproxy '*'` is not optional in the authoring sandbox** (§10.1). The sandbox proxy is dead;
668
+ > without the flag, the request fails before reaching Render. In a normal environment the flag is
669
+ > harmless.
670
+
671
+ ### 6.3 What was run, and how to re-run it
672
+
673
+ The behavioural validation is **3 passes × 8 cases = 24 live runs**, each pass **8/8**, with **0
674
+ mock nodes** and a live trace bar at **94.4444 %** (`.workbuddy-ai/scratch/live_validation/
675
+ LIVE_VALIDATION_POSTFIX.md`). The eight cases exercise all six specialist tasks plus the two
676
+ router-defect queries (B1, B2).
677
+
678
+ | pass | HEAD | harness | result |
679
+ |---|---|---|---|
680
+ | 1 | `ff46eba42b18`+`d413d3672311` | v1 (`fill_input`) | 8/8 |
681
+ | 2 | `2d7ae53b482d` | v2 asserting | 8/8 |
682
+ | 3 | `2d7ae53b482d` | v2 asserting (pre-discriminator-fix) | 8/8 (recomputed) |
683
+
684
+ To re-run: drive `https://satquery.pages.dev` in a **headed** browser (the headed requirement is
685
+ explained in §6.4), upload **one asset per case** for single-image tasks and a **pair** for the
686
+ temporal tasks, submit the query, and read the run identifier, the `mock_nodes` count, the trace
687
+ fill, and the answer's `[task]` tag. The eight expected cases are tabulated in
688
+ [`EVALUATION.md`](EVALUATION.md) §6.2.
689
+
690
+ ### 6.4 The Chrome focus trap — why the harness asserts
691
+
692
+ Pass 1's harness drove the query box with `fill_input()`, which types with **real CDP key events**.
693
+ A re-run attempt failed on case 1 with `run_id=0002`, `mock_nodes=9`, `answer="No answer yet"`, and
694
+ only a `capabilities` call — the **mock path**. Root cause:
695
+
696
+ > Chrome **drops synthesized key events when the browser window does not hold OS focus**; the harness
697
+ > had **no assertion**, so it clicked Run with the page's *default* query still in the box.
698
+
699
+ Measured directly: with Chrome backgrounded, `press_key("Z")` left `#qtext.value` unchanged, while
700
+ `type_text("Q")` (CDP `Input.insertText`, not focus-gated) inserted fine
701
+ (`LIVE_VALIDATION_POSTFIX.md`, "Why this pass needed a new harness").
702
+
703
+ This is the single most important trap for anyone reproducing a live run, because it produces a
704
+ **silent false pass** — the pipeline "works", it just answered a different question. The fix was to
705
+ **assert the input state before dispatching**, with three pre-dispatch assertions:
706
+
707
+ - `q_ok` — the query box really held the query;
708
+ - `obs_ok` — `#obsTail == 'ready'`;
709
+ - `t0_ok` — both frames present where required.
710
+
711
+ The full account, including the check that pass 1 was **not** infected, is in
712
+ [`EVALUATION.md`](EVALUATION.md) §6.4–6.5. In short: pass 1's intents are query-specific (A1 reads
713
+ `taskvqa…temporalnone`, not the default's `taskchange…temporalrequired`), their answers embed the
714
+ query text, and A6's answer proves two files were uploaded — so pass 1's results are clean.
715
+
716
+ ### 6.5 The recompute-verdicts safety property
717
+
718
+ Pass 3 was launched with a harness build that still carried two *discriminator* bugs (the answer
719
+ `[task]` tag exists only for region tasks; the intent panel is a concatenated string needing a
720
+ non-greedy match). Its raw output says `SUMMARY 0/8`; the verdicts are recomputed from the recorded
721
+ evidence by `recompute_verdicts.py`, giving 8/8. This is not a workaround — it is the intended
722
+ safety property:
723
+
724
+ > The recorded evidence (run id, `mock_nodes`, intent, answer, assertions) is **independent of the
725
+ > verdict computation**, so a harness bug can never silently turn a real failure into a pass, and
726
+ > never costs a re-run to correct.
727
+
728
+ — `LIVE_VALIDATION_POSTFIX.md`, Pass 3.
729
+
730
+ Two further harness bugs were found and fixed, both causing **false failures** (not false passes):
731
+ the answer `[task]` tag exists only for region tasks, and the intent panel needs a non-greedy match.
732
+ The harness now computes the dispatched task as `answer_tag` when present, else the reading. Both
733
+ are recorded because a false failure and a false pass are different risks and the project tracks
734
+ them separately.
735
+
736
+ ### 6.6 Cold start and tunnel caveats
737
+
738
+ - **Cold start.** Render's free tier sleeps and the Codespace may be stopped. The first request can
739
+ exceed the client timeout while weights are fetched; a retry a few seconds later normally
740
+ succeeds. Warm the stack before any demonstration and confirm `agent_connected: true`
741
+ (`README.md` §Deployment caveats).
742
+ - **Tunnel gaps (B-07, `OPEN`).** The tunnel agent can be briefly absent; a request during a gap may
743
+ hang or return 504. **This is not fixed in production.** Root cause: in `auto` mode a tunnel
744
+ timeout falls through to the forwarded-port path, spending the 120 s wake timeout on a 302 — the
745
+ observed ~249 s failure (`docs/DEPLOYMENT.md §6`). A patch exists and was deliberately **not**
746
+ deployed.
747
+ - **Never retry `POST /api/infer` at the gateway** — a retry consumes inference twice
748
+ (`docs/DEPLOYMENT.md §7`).
749
+
750
+ ---
751
+
752
+ ## 7. Reproduce and verify the released artifacts
753
+
754
+ ### 7.1 The manifest and checksums are generated, not typed
755
+
756
+ `models/manifest.json` and `models/checksums.sha256` are produced by
757
+ `release/tools/generate_model_manifest.py`, which reads the actual files:
758
+
759
+ > Generated by reading the files. No byte count or hash is typed by hand. Backbones are NOT
760
+ > redistributed; they are fetched from the Hugging Face Hub, pinned by revision.
761
+
762
+ — `models/manifest.json` (`"note"` field).
763
+
764
+ Verify the local weights:
765
+
766
+ ```bash
767
+ sha256sum -c models/checksums.sha256
768
+ ```
769
+
770
+ `models/checksums.sha256` (committed):
771
+
772
+ ```
773
+ c5ef31277b67aa01a593aec0eac503eeaccc6d674349fda20ca44c9cc6f8e9fa change/head.pt
774
+ cfae5e43b97ca930f568dc5b8ae4f36b24e9ff717af226159802206ffd63a82a change_vqa/head.pt
775
+ 785815729a3a39fc34dc41894efaf00d8739365d970a3f830a326e68ae888dab optical_sar/head.pt
776
+ 93432f7034be91a8ffd9c1a84e3eeec00bed7832c043fe7f83d2be230284c6bb grounding/head.pt
777
+ 8527c3ed28a293e13293d48601d48e3ceafa137b9acabddaf5de31a58a509b5c router/adapter.pt
778
+ 07c76a75fa04624880ed7730590f5fdd7b145a8232e3c0af411c3c545a5adf5e vlm/adapter_model.safetensors
779
+ ```
780
+
781
+ The manifest records, per artifact: `id`, `task`, `kind`, `path` (in the project), `hf_path` (on the
782
+ Hub), `backbone`, `architecture`, `source_metric_artifact`, `config_hash`, `bytes`, `sha256`, and
783
+ `status`. The manifest's own header records `config_hash: 78f1e3700da15aa1` and
784
+ `artifact_count: 6`, generated `2026-09-25T18:15:38+00:00`.
785
+
786
+ ### 7.2 Independent Hub verification
787
+
788
+ ```bash
789
+ export HF_TOKEN=... # token with repo.content.read
790
+ python release/tools/hf_verify.py
791
+ ```
792
+
793
+ `release/tools/hf_verify.py` is an **independent** check: it does not trust the upload step. It
794
+ reads the manifest, **re-downloads each artifact from the Hub over direct HTTPS**, hashes the bytes
795
+ it received, and compares against the locally-computed sha256. Its docstring is explicit that it
796
+ uses direct HTTPS *deliberately*:
797
+
798
+ > Note: `hf_hub_download` is deliberately NOT used here — in this environment it returned an empty
799
+ > file (sha256 e3b0c442…), which would have produced a false FAIL. Direct HTTPS is the honest check.
800
+
801
+ Recorded result (`release/tools/hf_verify_report.txt`):
802
+
803
+ ```
804
+ repo : thundercode/SatQuery
805
+ private : False
806
+ sha (HEAD) : 55681e0cddb91a4a5655da98a49bc025e537b657
807
+ lastModified : 2026-09-25T18:21:52.000Z
808
+ files on Hub : 22
809
+
810
+ STATUS hf_path remote bytes local bytes
811
+ ----------------------------------------------------------------------------------
812
+ MATCH change/head.pt 63,231,009 63,231,009
813
+ MATCH change_vqa/head.pt 5,822,809 5,822,809
814
+ MATCH optical_sar/head.pt 14,427,457 14,427,457
815
+ MATCH grounding/head.pt 12,639,041 12,639,041
816
+ MATCH router/adapter.pt 211,961 211,961
817
+ MATCH vlm/adapter_model.safetensors 34,798,048 34,798,048
818
+
819
+ artifacts verified : 6
820
+ artifacts failed : 0
821
+
822
+ OK README.md
823
+ OK MODEL_CARD.md
824
+ OK models/manifest.json
825
+ OK models/checksums.sha256
826
+ ```
827
+
828
+ Total released weight payload: **131,130,325 bytes (~125 MiB)** across the six artifacts
829
+ (`release/HF_RELEASE_VERIFICATION.md §4`). Exit code 0 with `artifacts failed : 0` means the release
830
+ is intact.
831
+
832
+ ### 7.3 A verification method that was itself wrong — the empty-file false FAIL
833
+
834
+ This is the most instructive reproducibility incident in the release, and it is recorded because a
835
+ verifier that silently hashes an **empty file** is a verifier that can lie in *both* directions.
836
+
837
+ The **first** verification attempt reported all six artifacts `DIFFER`, with every remote hash equal
838
+ to:
839
+
840
+ ```
841
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
842
+ ```
843
+
844
+ — the sha256 of **empty content**. The cause was **not** the upload: `hf_hub_download` returned an
845
+ empty file in the authoring environment (a download-path problem), so the verifier hashed nothing.
846
+
847
+ It was caught by a **second, independent method** — a direct `curl` download — which produced the
848
+ correct hash `8527c3ed28a293e13293d48601d48e3ceafa137b9acabddaf5de31a58a509b5c` for
849
+ `router/adapter.pt`, byte-identical to the local file and confirmed to be a real PyTorch zip
850
+ (`PK\x03\x04`, containing `adapter/data.pkl`). The verifier was then rewritten to use direct HTTPS
851
+ with proxies disabled (`ProxyHandler({})`), and now reports 6/6 `MATCH`.
852
+
853
+ `release/HF_RELEASE_VERIFICATION.md §5.1` states the lesson directly:
854
+
855
+ > The failed first attempt is recorded because a verifier that silently hashes an empty file would
856
+ > have produced a **false failure** — and, with a different bug, could just as easily have produced
857
+ > a **false pass**.
858
+
859
+ **What a third party should take from this.** Do not use `hf_hub_download` as the *sole* verification
860
+ path. The honest check is a direct HTTPS download with proxies disabled, hashing the bytes received,
861
+ and comparing sizes as well as hashes. `hf_verify.py` does all three
862
+ (`size_ok = remote_bytes == local_bytes == len(content)`; `hash_ok = remote_hash == local_hash`).
863
+
864
+ ### 7.4 Cross-checks against independently-recorded hashes
865
 
866
+ Two of the six hashes can be checked against values recorded **elsewhere** in the project,
867
+ independent of this release (`release/HF_RELEASE_VERIFICATION.md §6`):
868
 
869
+ | Artifact | Recorded elsewhere | Computed here | Agree |
870
+ |---|---|---|---|
871
+ | `change_vqa_head` | `cfae5e43…d63a82a` (`artifacts/change_vqa/run/PROMOTION.json`) | `cfae5e43…d63a82a` | **yes** |
872
+ | `vlm_lora_adapter` | `07c76a75…a5adf5e` (adapter provenance) | `07c76a75…a5adf5e` | **yes** |
873
+
874
+ This is a stronger form of verification than self-consistency: it means the released bytes match
875
+ hashes that were recorded by the training/export process, not merely by the release step.
876
+
877
+ ### 7.5 The evidence archive
878
+
879
+ `release/tools/build_archive.py` builds the Phase-7 evidence archive. Per owner decision **G3**,
880
+ everything under `artifacts/` is included **verbatim** — including duplicates and caches — so the
881
+ archive is **~4 GB** and requires ZIP64. The layout:
882
+
883
+ ```
884
+ SatQuery_AI_Final_Archive_2026-09-25/
885
+ README_ARCHIVE.md (what is inside, what was excluded and why)
886
+ release/ (the curated public release tree, verbatim)
887
+ artifacts/ (VERBATIM, per owner decision G3)
888
+ evidence/
889
+ live_validation/ (3 validation passes + screenshots)
890
+ delivery/ (delivery report + handoff)
891
+ verification/ (state, manifests, verification reports)
892
+ ```
893
+
894
+ Weights and caches are stored with `ZIP_STORED` (no recompression) to avoid wasting hours on
895
+ already-compressed data. The archive **excludes** secret/token files, OS junk, virtualenvs,
896
+ `node_modules`, the HF hub cache, and temp browser profiles — documented in `README_ARCHIVE.md`,
897
+ never silent. It is verified by `release/tools/verify_archive.py`, which extracts to a separate temp
898
+ directory and checks the CRC of every member plus the sha256 of every artifact against the live
899
+ files.
900
+
901
+ ---
902
+
903
+ ## 8. What "reproduce" means for the two externally-trained artifacts
904
+
905
+ Six trained artifacts are released. **Four were trained inside the project's own pipeline; two were
906
+ trained outside this repository** on external GPU. This distinction is the heart of the
907
+ reproducibility contract, so it is stated plainly rather than implied.
908
 
909
  | Artifact | Where it trains | Guide |
910
  |---|---|---|
911
+ | `router/adapter.pt` | local CPU | `configs/base.yaml` §router |
912
+ | `grounding/head.pt` | local | `configs/base.yaml` §grounding_training |
913
+ | `change/head.pt` | local | `configs/base.yaml` §change |
914
+ | `optical_sar/head.pt` | local, seed sweep | `docs/PHASE14_OPTICAL_SAR_DECISIONS.md` |
915
+ | `change_vqa/head.pt` | **external GPU (Kaggle)** | `docs/R02_KAGGLE_TRAINING_GUIDE.md` |
916
+ | `vlm/adapter_model.safetensors` | **external GPU** | `configs/base.yaml` §training |
917
+
918
+ ### 8.1 What the repository reproduces for the external artifacts
919
+
920
+ For the two externally-trained artifacts, this repository reproduces:
921
+
922
+ 1. **The promotion gate** — the checks the returned checkpoint must pass before it is accepted
923
+ (byte-identity, sha256 match against the recorded digest, zero non-finite tensors).
924
+ 2. **The evaluation** — the scoring path that turns a checkpoint into the recorded metrics.
925
+ 3. **The serving wiring** — the code path that loads the artifact and routes to it.
926
+
927
+ It does **not** ship a one-command retrain for those two artifacts. That is stated, not implied.
928
+
929
+ ### 8.2 The change-VQA path (Kaggle)
930
+
931
+ `docs/R02_KAGGLE_TRAINING_GUIDE.md` is the authoritative guide; it states its own scope:
932
+
933
+ > **Audience:** the person running the external GPU job. **Scope:** everything needed to go from
934
+ > this repository to a returned, reviewable checkpoint. **Not in scope:** deciding whether R-02 is
935
+ > done — that happens after the artifact comes back.
936
+
937
+ Its status is `IMPLEMENTATION_READY_FOR_EXTERNAL_TRAINING`, and it is explicit that *"training
938
+ produces an artifact, not a verified capability, and the run record says `TRAINED_UNVERIFIED`."*
939
+
940
+ The reproduction is **byte-scoped**: the upload set is defined by a digest so the external run can be
941
+ shown to have started from exactly this code.
942
+
943
+ | Item | Value |
944
+ |---|---|
945
+ | Upload set | **249 files**, at the root of the archive |
946
+ | Digest-defining subset | **248 files, 67,225,247 bytes** |
947
+ | Manifest hash | `e98d3854db5f3edec76c1748c0c1bc3041d006ae5858debee31701539af7d0d6` |
948
+ | Archive | `satquery-ai.zip` — 250 entries, 60.0 MB compressed |
949
+ | Inside the archive | `UPLOAD_MANIFEST.txt` — every file with size and SHA256, plus the digest |
950
+
951
+ The digest covers **248 files, not 249**, for a principled reason the guide states: the guide itself
952
+ lives in `docs/`, so it is part of the upload set — but it also *records* the digest, and a document
953
+ cannot contain the hash of itself. The digest is therefore defined over the upload set **excluding
954
+ this one file**; that is the only definition that can be reproduced. For the same reason the byte
955
+ total for all 249 files is **not** quoted in the guide — it changes whenever the guide is edited; the
956
+ authoritative value is in `UPLOAD_MANIFEST.txt`.
957
+
958
+ The frozen STANet checkpoint the head depends on is verified by size and SHA256 in the notebook:
959
+
960
+ ```
961
+ checkpoint : <CODE_ROOT>/artifacts/change/levir_change_v001/head.pt
962
+ bytes : 63,231,009
963
+ sha256 : c5ef31277b67aa01a593aec0eac503eeaccc6d674349fda20ca44c9cc6f8e9fa
964
+ checkpoint verified against the frozen digest
965
+ ```
966
+
967
+ Training knobs are fixed, not tuned-to-pass: `HARD_STOP_SECONDS = 3 * 3600`, `EPOCHS = 40`,
968
+ `BATCH_SIZE = 256`, `DEVICE = "cuda"`, `SEED = 42` (the trainer's own defaults are
969
+ `epochs=40, batch_size=128, seed=42, patience=6, time_limit=10800s`). The guide instructs: *"Do not
970
+ change these to get past an error."*
971
+
972
+ Split integrity is measured, not assumed:
973
+
974
+ ```
975
+ measured corpus ground truth (unique scenes / resolved questions):
976
+ Train 1600 scenes 65967 questions
977
+ Val 400 scenes 16441 questions
978
+ Test 968 scenes 39686 questions
979
+ Test2 968 scenes 31036 questions
980
+ integrity clean : True
981
+ scenes/split : {'Train': 1600, 'Val': 400}
982
+ overlap : {'Train|Val': 0}
983
+ ```
984
 
985
+ The two test sets (Test 39686, Test2 31036) are why change-VQA is quoted under **two** test sets
986
+ (rule 3) — and why the promotion artifact carries both.
987
 
988
+ ### 8.3 The VLM path
 
 
 
 
989
 
990
+ The VLM artifact is a PEFT LoRA adapter (`r=16, alpha=32, dropout=0.05`) on the text-model
991
+ projections of `HuggingFaceTB/SmolVLM-500M-Instruct` (`models/manifest.json`). Its acceptance status
992
+ is **`ACCEPTANCE-REJECTED`** — the metrics are usable (`exact_match 0.963`, `f1 0.96432`) but the
993
+ adapter was **not promoted**. `USABLE ≠ ACCEPTED` (rule 7). Reproducing this artifact's *training*
994
+ requires the external GPU environment described in `configs/base.yaml` §training; reproducing its
995
+ *evaluation* and its *serving wiring* is done by this repository. The rejection is preserved in the
996
+ record (`artifacts/vlm/phase6_closure.json`), not scrubbed.
997
 
998
+ ### 8.4 The optical-SAR seed sweep
999
+
1000
+ The optical-SAR head was trained locally with a **seed sweep**; the selected production artifact is
1001
+ `fusion_head_production_v001`, and its pre-registered metric is the 11.5 target recorded in
1002
+ `docs/PHASE12_115_METRIC_COMPUTED.md` and `artifacts/optical_sar/fusion_head_production_v001/
1003
+ pre_registered_115_metric.json`. The input dimension is **derived at runtime** (not hardcoded) —
1004
+ `2318 = 3×768 + 12 + 2` (`fusion_head.py:74`, `:311`), independently confirmed in
1005
+ `docs/PHASE14_OPTICAL_SAR_DECISIONS.md §1`. The ruling is **`OPEN`**, and the accuracy (`0.931`) is
1006
+ never quoted without the macro-F1 (`0.434161`) — rare classes are poorly handled (rule 4).
1007
+
1008
+ ### 8.5 What is *not* one-command
1009
+
1010
+ There is no script that takes this repository and produces all six artifacts in one invocation. The
1011
+ four locally-trained artifacts have local training paths, and the two external ones require an
1012
+ external GPU environment. This is a deliberate boundary: the release ships **frozen artifacts with
1013
+ provenance**, not a retraining harness.
1014
+
1015
+ ---
1016
+
1017
+ ## 9. What is NOT reproducible from this release — exhaustive
1018
+
1019
+ This section is the honest counterweight to §1. Every item below is something a reader might
1020
+ reasonably expect to reproduce and **cannot** from this release alone.
1021
+
1022
+ ### 9.1 Not reproducible because the source is not released
1023
 
1024
  | Item | Reason |
1025
  |---|---|
1026
+ | The three **private deployment repositories** (`SatQuery-Frontend`, `SatQuery-Backend`, `SatQuery-Inference`) | Private by design; their links 404 for an outside audience (`docs/DEPLOYMENT.md §6`). |
1027
+ | The **live deployment** itself | Requires the private sources plus the Render/Codespace/Cloudflare accounts. |
1028
+ | The **monorepo working copy** | Local only, no remote; 334 dirty entries (`docs/DEPLOYMENT.md §1`). |
1029
+
1030
+ ### 9.2 Not reproducible because it does not exist or was not run
1031
+
1032
+ | Item | Reason |
1033
+ |---|---|
1034
+ | **System-level end-to-end benchmark** | **No such benchmark exists.** Per-specialist metrics are real; a single end-to-end number is `NOT RUN` (`README.md` §Known limitations 1). |
1035
+ | **Router test-split number** | **The test split was never run.** The only router number is validation, ungated, `n = 86` (rule 5). |
1036
+ | **Captioning benchmark** | `IMPLEMENTED` but the benchmark is `NOT RUN` ([`EVALUATION.md`](EVALUATION.md) §4.8). |
1037
+ | **A composite / "overall accuracy"** | Forbidden by construction: `official_aggregate_weights: null`; `normalize.py` raises on aggregate requests (rule 8). |
1038
+
1039
+ ### 9.3 Not reproducible because the data is large or external
1040
+
1041
+ | Item | Reason |
1042
+ |---|---|
1043
+ | **CDVQA / SECOND imagery** | Public but large. The release documents the acquisition + name-verification procedure, not the data. |
1044
+ | **BigEarthNet full corpus** | **Not downloaded** — only a 28k S2 subset was used. |
1045
+ | **LEVIR-CD-256 / VRSBench raw data** | Not redistributed; the splits (7120/1024/2048, and VRSBench `n=16159`) are documented, not shipped. |
1046
+ | **Fusion held-out test (n=4000)** | Documented, not shipped. |
1047
+ | **Feature caches** | Reproducible but included only in the verbatim archive, not in the release. |
1048
+
1049
+ ### 9.4 Not reproducible because it was external-GPU training
1050
+
1051
+ | Item | Reason |
1052
+ |---|---|
1053
+ | **change-VQA retrain** | External GPU (Kaggle); the repo reproduces the *promotion gate*, *evaluation*, and *serving wiring*, not a one-command retrain (§8.2). |
1054
+ | **VLM LoRA retrain** | External GPU; and the adapter is `ACCEPTANCE-REJECTED` anyway (§8.3). |
1055
+
1056
+ ### 9.5 Not reproducible because it is frozen paperwork
1057
+
1058
+ | Item | Reason |
1059
+ |---|---|
1060
+ | **The historical ZeroGPU / Gradio deploy target** | `configs/deploy.yaml` describes the old HF-Space/ZeroGPU target and is left undisturbed as frozen paperwork; **no Gradio runtime exists in code** (`docs/DEPLOYMENT.md §9`). Editing it would move the config hash. |
1061
+
1062
+ ### 9.6 Not reproducible *as a bit-identical training run*
1063
+
1064
+ The released **artifacts** are byte-identical (guaranteed by hash). The **training runs** that
1065
+ produced them are not claimed to be bit-reproducible on arbitrary hardware: floating-point
1066
+ non-determinism, different accelerators, and library-version drift all affect a training run. What is
1067
+ guaranteed is: the *config* is frozen, the *seed* is recorded (`42`), and the *artifact* is hashable.
1068
+
1069
+ ### 9.7 Explicit non-claims
1070
+
1071
+ To remove any ambiguity, this release does **not** claim:
1072
+
1073
+ - that the live system achieves any single accuracy number;
1074
+ - that the router's `0.965116` is a test result (it is validation-only, `n = 86`);
1075
+ - that the VLM adapter is accepted (it is rejected);
1076
+ - that calibration improves ECE (it worsens it, `0.013755 → 0.014929`);
1077
+ - that optical-SAR fusion is production-ready (ruling `OPEN`, macro-F1 `0.434161`);
1078
+ - that change-VQA is settled (ruling `OPEN`, two test sets);
1079
+ - that grounding is solved (`0.2838` canonical IoU — useful, not solved);
1080
+ - that B-07 tunnel gaps are fixed (patch prepared, `NOT deployed`).
1081
+
1082
+ ---
1083
+
1084
+ ## 10. Environment traps recorded for reproducibility
1085
+
1086
+ These are the edges that cost real debugging time in the authoring environment. They are recorded so
1087
+ a third party does not rediscover them — and, more importantly, so that a reviewer can distinguish
1088
+ "this is a documented environment quirk" from "this is a defect in the released code".
1089
+
1090
+ ### 10.1 The dead sandbox proxy needs `--noproxy '*'` / `ProxyHandler({})`
1091
+
1092
+ **Symptom.** Outbound HTTP calls fail or hang in the authoring sandbox.
1093
+ **Root cause.** The sandbox proxy is dead; requests are routed to it and never reach the target.
1094
+ **Fix.** Disable proxies for the call:
1095
+
1096
+ ```bash
1097
+ curl --noproxy '*' https://satquery-backend-m4yv.onrender.com/api/health
1098
+ ```
1099
+
1100
+ ```python
1101
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
1102
+ ```
1103
+
1104
+ `release/tools/hf_verify.py:37-38` does exactly this (`opener_no_proxy()`). In a normal environment
1105
+ the flag is harmless; in the sandbox it is mandatory.
1106
+
1107
+ ### 10.2 pytest exists only in the repository virtualenv
1108
+
1109
+ **Symptom.** Invoking the system `pytest` fails or resolves to a different interpreter.
1110
+ **Root cause.** pytest is installed only in `.venv`.
1111
+ **Fix.** Always invoke the venv interpreter explicitly:
1112
+
1113
+ ```bash
1114
+ .venv/Scripts/python.exe -m pytest tests/unit/test_frontend_live_wiring.py -q
1115
+ ```
1116
+
1117
+ ### 10.3 The full-suite pytest trips a bulk-delete guard
1118
+
1119
+ **Symptom.** Running the whole `tests/unit` tree trips 4× `test_safe_delete_shim` failures.
1120
+ **Root cause.** Windows **verbatim-path** defects in the sandbox's bulk-delete guard. The precise
1121
+ condition under which the shim intermittently triggers on Windows is
1122
+ `UNKNOWN — not established from the available evidence`.
1123
+ **Fix / workaround.** Run the targeted suites (106 and 183 pass cleanly); treat the 4 shim failures
1124
+ as environmental, not regressions (§5.4).
1125
+
1126
+ ### 10.4 Cloudflare 308-redirects `X.html` → `/X`
1127
+
1128
+ **Symptom.** A request to `something.html` returns a 308 redirect.
1129
+ **Root cause.** Cloudflare Pages 308-redirects `X.html` → `/X`.
1130
+ **Fix.** Reference the **extensionless** path (`docs/DEPLOYMENT.md §7`).
1131
+
1132
+ ### 10.5 Chrome drops synthetic CDP key events without OS focus
1133
+
1134
+ **Symptom.** A browser-driven run silently answers the *default* query; the query box looks
1135
+ untouched; `mock_nodes` is non-zero.
1136
+ **Root cause.** Chrome **drops synthesized key events when the browser window does not hold OS
1137
+ focus**. `press_key`/`fill_input` (real CDP key events) are focus-gated; `Input.insertText`
1138
+ (`type_text`) is not.
1139
+ **Fix.** Use `type_text` (not `fill_input`), and **assert the input state before dispatch**
1140
+ (`q_ok`/`obs_ok`/`t0_ok`). This is the single most dangerous trap because it produces a **silent
1141
+ false pass** (§6.4; [`EVALUATION.md`](EVALUATION.md) §6.4).
1142
+
1143
+ ### 10.6 Buffering hides liveness (two separate causes)
1144
+
1145
+ **Symptom 1.** A redirected browser-harness log stays at 0 bytes until the process exits —
1146
+ indistinguishable from a stall.
1147
+ **Root cause 1.** `browser-use` block-buffers stdout even when redirected.
1148
+ **Fix 1.** Force line-buffering and print a per-case marker (`CASE_START <id>`).
1149
+
1150
+ **Symptom 2.** Piping the harness through `grep` swallows all output if the pipeline is killed.
1151
+ **Root cause 2.** `grep` block-buffers when piped.
1152
+ **Fix 2.** Redirect to a file instead of piping through `grep`.
1153
+
1154
+ ### 10.7 `hf_hub_download` returned an EMPTY file → a false FAIL
1155
+
1156
+ **Symptom.** Every artifact hashes to `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
1157
+ (empty content) and the verification reports all `DIFFER`.
1158
+ **Root cause.** `hf_hub_download` returned an empty file in the authoring environment (a download-path
1159
+ problem), so the verifier hashed nothing. The upload was fine.
1160
+ **Fix.** Use a **direct HTTPS download with proxies disabled**, hash the bytes received, and compare
1161
+ **size as well as hash**. `release/tools/hf_verify.py` is the honest check (§7.3).
1162
+
1163
+ ### 10.8 `codespace_name` carries a trailing `\n`
1164
+
1165
+ **Symptom.** `/api/health` reports `"codespace_name":"potential-space-trout-r4ppw969w45j2pvvw\n"`.
1166
+ **Root cause.** B-02; the wake path strips it, so it is **cosmetic**.
1167
+ **State.** `OPEN (cosmetic)`; the fix is not deployed (`docs/DEPLOYMENT.md §2, §6`).
1168
+
1169
+ ### 10.9 A forwarded Codespace port returns 302 for a private repo
1170
+
1171
+ **Symptom.** A forwarded Codespace port returns HTTP 302 instead of serving.
1172
+ **Root cause.** Private-repo port forwarding returns 302 — which is *why* the outbound tunnel exists
1173
+ (`docs/DEPLOYMENT.md §7`).
1174
+ **Fix.** Use the tunnel, not the forwarded port.
1175
+
1176
+ ### 10.10 The tunnel agent must be started by the devcontainer
1177
+
1178
+ **Symptom.** A restarted Codespace comes up with `agent_connected: false`.
1179
+ **Root cause.** The tunnel agent is started by the devcontainer `postStartCommand`; if it does not
1180
+ run, the agent is absent.
1181
+ **Fix.** Ensure `.devcontainer/` starts the agent on start (`docs/DEPLOYMENT.md §7`).
1182
+
1183
+ ---
1184
+
1185
+ ## 11. What is NOT RUN / OPEN / BLOCKED for this topic
1186
+
1187
+ Per the documentation standard, this section lists, for the topic of reproducibility itself, what is
1188
+ not settled.
1189
+
1190
+ | Item | Status | Note |
1191
+ |---|---|---|
1192
+ | End-to-end system benchmark | **NOT RUN** | No such benchmark exists; nothing to reproduce. |
1193
+ | Router **test**-split number | **NOT RUN** | Only validation (`n = 86`, ungated) exists. |
1194
+ | Captioning benchmark | **NOT RUN** | Implemented; not benchmarked. |
1195
+ | VLM adapter acceptance | **REJECTED** | Metrics usable; not promoted. `USABLE ≠ ACCEPTED`. |
1196
+ | Optical-SAR fusion ruling | **OPEN** | Accuracy without macro-F1 is never quoted. |
1197
+ | Change-VQA ruling | **OPEN** | Two test sets. |
1198
+ | Grounding acceptance | **OPEN** | Two protocols; modest IoU. |
1199
+ | Calibration improvement | **REJECTED (retained in frozen config)** | ECE worsened `0.013755 → 0.014929`. |
1200
+ | B-07 tunnel gaps | **OPEN** | Patch prepared, **NOT deployed**. |
1201
+ | B-02 trailing `\n` | **OPEN (cosmetic)** | Not deployed. |
1202
+ | Licence | **OPEN** | **No LICENSE file exists**; owner decision. |
1203
+ | Exact full-suite *collected* count | **UNKNOWN** | Per-suite counts are known (106, 183, 137 re-run); the single collected total is not established. |
1204
+ | Exact Windows trigger for the delete-shim flake | **UNKNOWN** | The intermittent condition is not established from the available evidence. |
1205
+ | Bit-identical training on arbitrary hardware | **NOT CLAIMED** | Only artifacts are guaranteed byte-identical, by hash. |
1206
+
1207
+ ---
1208
+
1209
+ ## 12. Where the evidence lives
1210
+
1211
+ | Evidence | Location |
1212
+ |---|---|
1213
+ | The 20-claim metric verifier | `release/tools/verify_readme_metrics.py` |
1214
+ | Its committed output | `release/tools/readme_metrics_report.txt` (`ALL CLAIMS VERIFIED`) |
1215
+ | The Hub verifier | `release/tools/hf_verify.py` |
1216
+ | Its committed output | `release/tools/hf_verify_report.txt` (6/6 `MATCH`) |
1217
+ | Hub release verification write-up | `release/HF_RELEASE_VERIFICATION.md` |
1218
+ | Model manifest (generated) | `models/manifest.json` |
1219
+ | Checksums (generated) | `models/checksums.sha256` |
1220
+ | Manifest generator | `release/tools/generate_model_manifest.py` |
1221
+ | Archive builder / verifier | `release/tools/build_archive.py`, `release/tools/verify_archive.py` |
1222
+ | Config loader + invariants | `core/config.py` (hash `:76-80`; `_validate` `:94`) |
1223
+ | Frozen config registry | `configs/base.yaml` (§evaluation `:273`, §deployment `:282`) |
1224
+ | Dependency manifest + profiles | `requirements.txt` |
1225
+ | Live topology, env vars, traps | `docs/DEPLOYMENT.md` |
1226
+ | Live validation (3 passes, 24 runs) | `.workbuddy-ai/scratch/live_validation/LIVE_VALIDATION_POSTFIX.md`, `results_final.json`, `results_pass3.json`, `run_final2.txt`, `run_final3.txt` |
1227
+ | Verdict recomputation | `recompute_verdicts.py` |
1228
+ | Kaggle training guide (change-VQA) | `docs/R02_KAGGLE_TRAINING_GUIDE.md` |
1229
+ | Optical-SAR decisions | `docs/PHASE14_OPTICAL_SAR_DECISIONS.md` |
1230
+ | Pre-registered 11.5 metric | `docs/PHASE12_115_METRIC_COMPUTED.md` |
1231
+ | Grounding resolution decision | `docs/PHASE7_RESOLUTION_DECISION.md` |
1232
+ | Delivery test results (94/183/5–6/137) | `docs/FINAL_DELIVERY_REPORT.md §7` |
1233
+ | Frontend suite re-run count (106) | `release/CURRENT_RELEASE_STATE.md:121`, `README.md` §The test suites |
1234
+ | Evaluation protocols and honesty rules | [`EVALUATION.md`](EVALUATION.md) |
1235
+ | Benchmarks and their statuses | [`BENCHMARKS.md`](BENCHMARKS.md) |
1236
+ | Model cards and artifact details | [`MODELS.md`](MODELS.md) |
1237
+ | Datasets and splits | [`DATASETS.md`](DATASETS.md) |
1238
+ | Training paths | [`TRAINING.md`](TRAINING.md) |
1239
+ | Known limitations | [`LIMITATIONS.md`](LIMITATIONS.md) |