thundercode commited on
Commit
b73a692
·
verified ·
1 Parent(s): 43104ce

release: add docs/BENCHMARKS.md

Browse files
Files changed (1) hide show
  1. docs/BENCHMARKS.md +830 -58
docs/BENCHMARKS.md CHANGED
@@ -1,77 +1,535 @@
1
- # Benchmarks
2
 
3
- **Every number here is artifact-backed.** Where a metric exists, its source file is named. Where a
4
- benchmark does **not** exist, that is stated explicitly and tagged `NOT RUN` — an absent number is
5
- never silently omitted or replaced with an estimate.
 
6
 
7
- **Status tags:** `VERIFIED` · `MEASURED` · `NOT RUN` · `OPEN` · `REJECTED`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  ---
10
 
11
  ## 1. Headline table
12
 
13
- | Capability | Metric | Value | Split / protocol | Status |
14
- |---|---|---|---|---|
15
- | Change detection | pooled IoU | **0.8122** | LEVIR-CD-256 test, n = 2048, thr 0.50 | **VERIFIED** |
16
- | Change detection | macro IoU | **0.8457** | same | **VERIFIED** |
17
- | Change detection | pooled F1 | **0.8964** | same | **VERIFIED** |
18
- | Grounding | mean best IoU | **0.2838** / **0.2566** | VRSBench n = 16159, canonical / matched6 | MEASURED (2 protocols) |
19
- | Grounding | recall@0.5 | **0.2198** / **0.1938** | same | MEASURED (2 protocols) |
20
- | Grounding | head-argmax IoU | **0.1215** | canonical | MEASURED |
21
- | Grounding | zero-shot baseline IoU | **0.0972** | canonical | MEASURED (baseline) |
22
- | Optical-SAR fusion | accuracy | **0.931** | held-out test n = 4000, 19 classes | MEASURED, ruling **OPEN** |
23
- | Optical-SAR fusion | macro F1 | **0.434161** | same | MEASURED, ruling **OPEN** |
24
- | Change-VQA | accuracy | **0.697626** | test n = 39686 | MEASURED, ruling **OPEN** |
25
- | Change-VQA | macro F1 | **0.378373** | same | MEASURED, ruling **OPEN** |
26
- | Change-VQA | accuracy (2nd set) | **0.651469** | test2 | MEASURED, ruling **OPEN** |
27
- | Change-VQA | macro F1 (2nd set) | **0.372309** | test2 | MEASURED, ruling **OPEN** |
28
- | VLM (adapted) | exact_match | **0.963** | frozen 1000-question subset | MEASURED, **ACCEPTANCE-REJECTED** |
29
- | VLM (adapted) | F1 | **0.96432** | same | MEASURED, **ACCEPTANCE-REJECTED** |
30
- | Router | overall **ungated** accuracy | **0.965116** | val n = 86, corpus-limited | MEASURED — **TEST NOT RUN** |
31
- | Calibration | ECE before / after | **0.013755 → 0.014929** | val n = 16441, T = 0.9773 | MEASURED — **worse** |
32
-
33
- ### Source artifacts
34
-
35
- | Metric family | Artifact |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  |---|---|
37
  | change | `artifacts/change/eval_test/eval_result.json` |
38
- | grounding | `artifacts/grounding/remoteclip_grounding_v001/eval_result_canonical.json`, `…_matched6.json` |
 
39
  | optical-SAR | `artifacts/optical_sar/fusion_head_production_v001/pre_registered_115_metric.json` |
40
  | change-VQA | `artifacts/change_vqa/run/PROMOTION.json` |
41
  | router | `artifacts/router/threshold_sweep_val.json` |
42
  | calibration | `artifacts/calibration_v001.json` |
43
  | VLM | `artifacts/vlm/phase6_closure.json` |
44
 
45
- > All 20 values in the table above are checked against these files by
 
 
 
 
 
46
  > [`../tools/verify_readme_metrics.py`](../tools/verify_readme_metrics.py). Its output
47
- > (`ALL CLAIMS VERIFIED`) is committed as `../tools/readme_metrics_report.txt`.
 
48
 
49
- ## 2. Rules this table follows
50
 
51
- 1. **Two protocols are never collapsed.** Grounding is reported under *both* the canonical and
52
- matched6 protocols. Quoting 0.2838 alone would be selective.
53
- 2. **Two test sets are never collapsed.** Change-VQA is reported on `test` **and** `test2`.
54
- 3. **accuracy never travels without macro-F1.** For imbalanced multi-class heads (optical-SAR,
55
- change-VQA) the macro-F1 is reported alongside accuracy, always.
56
- 4. **Validation is not test.** The router number is labelled "overall **ungated** accuracy", val,
57
- n = 86. It is not a test result.
58
- 5. **A negative result stays negative.** Calibration ECE worsened and is shown worsening.
59
- 6. **USABLE ≠ ACCEPTED.** The VLM metrics are real; the artifact is nevertheless acceptance-rejected.
60
 
61
- ## 3. What is NOT benchmarked
 
62
 
63
- | Benchmark | Status | Note |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  |---|---|---|
65
- | **System-level end-to-end accuracy** | **NOT RUN — none exists** | There is no measured end-to-end benchmark of the full router→specialist→envelope pipeline. No such number is claimed anywhere. |
66
- | **Router test split** | **NOT RUN** | Only the validation split (n = 86) was scored. |
67
- | **Benchmark adapters** | **NOT RUN** | Adapter-based benchmark runs were not executed. |
68
- | **Efficiency / latency benchmark** | not systematically measured | Per-specialist latency is recorded incidentally in artifacts (e.g. grounding `latency_ms_per_image` 2.205 ms for the head), but there is no end-to-end latency benchmark. |
69
- | **Cross-dataset generalisation** | **NOT RUN** | Each specialist is evaluated only on its own training-family test split. |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- ## 4. Live validation (behavioural, not accuracy)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  Accuracy is separate from **behavioural** validation. The deployed stack was driven end-to-end in a
74
- headed browser, one upload per case, with per-case screenshots and recorded run ids:
75
 
76
  | Property | Result |
77
  |---|---|
@@ -84,17 +542,331 @@ headed browser, one upload per case, with per-case screenshots and recorded run
84
  | Trace fill | **94.4444 %** on every live run |
85
  | Frontend regression suite | **106 passed** (`tests/unit/test_frontend_live_wiring.py`) |
86
 
87
- Every pass produced **fresh run identifiers** — no run id is shared between passes. This is
88
- behavioural evidence that the pipeline runs and routes correctly; it is **not** an accuracy claim.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- ## 5. How to reproduce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  ```bash
93
- # verify every README/benchmark number against its artifact
94
  python release/tools/verify_readme_metrics.py
95
  ```
96
 
97
- The script resolves nested artifact keys (including keys that themselves contain dots, such as the
98
- `recall` dict keyed `"0.10"/"0.25"/"0.50"`) and compares each value at the precision printed in the
99
- README. It exits non-zero if any claim fails. See [`REPRODUCIBILITY.md`](REPRODUCIBILITY.md) for the
100
- full reproduction guide.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Benchmarks — deep reference
2
 
3
+ **Every number here is artifact-backed.** Where a metric exists, its source file and the **exact key
4
+ path** inside that file are named. Where a benchmark does **not** exist, that is stated explicitly and
5
+ tagged `NOT RUN` — an absent number is never silently omitted, never estimated, and never replaced with
6
+ a plausible-looking substitute.
7
 
8
+ **Status tags:** `VERIFIED` · `MEASURED` · `NOT RUN` · `OPEN` · `REJECTED` · `BLOCKED`.
9
+
10
+ **The single most important rule in this document: do not fabricate.** If a value is not in an
11
+ artifact that was read, it does not appear here. Where a fact is not established, this document writes
12
+ `UNKNOWN — not established from the available evidence`.
13
+
14
+ > **There is no system-level end-to-end benchmark.** The router → specialist → envelope pipeline has
15
+ > never been scored end-to-end, and no end-to-end accuracy is claimed anywhere in this repository. What
16
+ > exists is (a) per-specialist metrics on their own training-family test splits, and (b) a behavioural
17
+ > live-validation record (§5) that proves the pipeline *runs and routes*, not that it is *accurate*.
18
+
19
+ ---
20
+
21
+ ## Table of contents
22
+
23
+ 1. [Headline table](#1-headline-table)
24
+ 2. [Source artifacts](#2-source-artifacts)
25
+ 3. [The honesty rules this document follows](#3-the-honesty-rules-this-document-follows)
26
+ 4. [Per-task evaluation protocols](#4-per-task-evaluation-protocols)
27
+ - 4.1 [Change detection](#41-change-detection--verified)
28
+ - 4.2 [Grounding](#42-grounding--measured-two-protocols--two-decode-variants)
29
+ - 4.3 [Optical-SAR fusion](#43-optical-sar-fusion--measured-ruling-open)
30
+ - 4.4 [Change-VQA](#44-change-vqa--measured-ruling-open)
31
+ - 4.5 [VLM adapter](#45-vlm-adapter--measured-acceptance-rejected)
32
+ - 4.6 [Router](#46-router--measured-test-not-run)
33
+ - 4.7 [Calibration](#47-calibration--measured-negative-result)
34
+ 5. [Live validation — behavioural, not accuracy](#5-live-validation--behavioural-not-accuracy)
35
+ 6. [What is NOT benchmarked](#6-what-is-not-benchmarked)
36
+ 7. [How to reproduce](#7-how-to-reproduce)
37
+ 8. [The verification tool — exactly what it checks](#8-the-verification-tool--exactly-what-it-checks)
38
+ 9. [Artifact schemas and key-path resolution](#9-artifact-schemas-and-key-path-resolution)
39
+ 10. [Metric glossary and aggregation arithmetic](#10-metric-glossary-and-aggregation-arithmetic)
40
+ 11. [Evidence index](#11-evidence-index)
41
 
42
  ---
43
 
44
  ## 1. Headline table
45
 
46
+ Every row names its source artifact and the exact key path within it. The `n` and `split` columns are
47
+ part of the claim, not decoration: a metric without its population is not a result.
48
+
49
+ | Capability | Metric | Value | Split / protocol | n | Source artifact → key path | Status |
50
+ |---|---|---|---|---|---|---|
51
+ | Change detection | pooled IoU | **0.8122** | LEVIR-CD-256 test, thr 0.50 | 2,048 | `artifacts/change/eval_test/eval_result.json` → `metrics.pooled.iou` | **VERIFIED** |
52
+ | Change detection | macro IoU | **0.8457** | same | 2,048 | `��/eval_result.json` → `metrics.macro.miou` | **VERIFIED** |
53
+ | Change detection | pooled F1 | **0.8964** | same | 2,048 | `…/eval_result.json` → `metrics.pooled.f1` | **VERIFIED** |
54
+ | Grounding | mean best IoU | **0.2838** | VRSBench, canonical (head threshold, top_k 20) | 16,159 | `…/eval_result_canonical.json` → `results.head_threshold.mean_best_iou` | MEASURED (2 protocols) |
55
+ | Grounding | recall@0.5 | **0.2198** | canonical | 16,159 | `…/eval_result_canonical.json` → `results.head_threshold.recall.0.50` | MEASURED (2 protocols) |
56
+ | Grounding | mean best IoU | **0.2566** | VRSBench, matched6 (head threshold, top_k 6) | 16,159 | `…/eval_result_matched6.json` → `results.head_threshold.mean_best_iou` | MEASURED (2 protocols) |
57
+ | Grounding | recall@0.5 | **0.1938** | matched6 | 16,159 | `…/eval_result_matched6.json` → `results.head_threshold.recall.0.50` | MEASURED (2 protocols) |
58
+ | Grounding | head-argmax IoU | **0.1215** | canonical (argmax decode) | 16,159 | `…/eval_result_canonical.json` → `results.head_argmax.mean_best_iou` | MEASURED |
59
+ | Grounding | zero-shot baseline IoU | **0.0972** | canonical (no trained head) | 16,159 | `…/eval_result_canonical.json` → `results.zero_shot_matched.mean_best_iou` | MEASURED (baseline) |
60
+ | Optical-SAR fusion | accuracy | **0.931** | held-out test, 19 classes | 4,000 | `…/fusion_head_production_v001/pre_registered_115_metric.json` → `accuracy` | MEASURED, ruling **OPEN** |
61
+ | Optical-SAR fusion | macro F1 | **0.434161** | same | 4,000 | `…/pre_registered_115_metric.json` → `macro_f1` | MEASURED, ruling **OPEN** |
62
+ | Change-VQA | accuracy | **0.697626** | `test` | 39,686 | `artifacts/change_vqa/run/PROMOTION.json` → `verification.test_accuracy` | MEASURED, ruling **OPEN** |
63
+ | Change-VQA | macro F1 | **0.378373** | `test` | 39,686 | `…/PROMOTION.json` → `verification.test_macro_f1` | MEASURED, ruling **OPEN** |
64
+ | Change-VQA | accuracy (2nd set) | **0.651469** | `test2` | 31,036 | `…/PROMOTION.json` → `verification.test2_accuracy` | MEASURED, ruling **OPEN** |
65
+ | Change-VQA | macro F1 (2nd set) | **0.372309** | `test2` | 31,036 | `…/PROMOTION.json` → `verification.test2_macro_f1` | MEASURED, ruling **OPEN** |
66
+ | VLM (adapted) | exact_match | **0.963** | frozen 1,000-question subset | 1,000 | `artifacts/vlm/phase6_closure.json` → `why_usable_verified.adapted_test.exact_match` | MEASURED, **ACCEPTANCE-REJECTED** |
67
+ | VLM (adapted) | F1 | **0.96432** | same | 1,000 | `…/phase6_closure.json` → `why_usable_verified.adapted_test.f1` | MEASURED, **ACCEPTANCE-REJECTED** |
68
+ | Router | overall **ungated** accuracy | **0.965116** | val, corpus-limited | 86 | `artifacts/router/threshold_sweep_val.json` → `overall_ungated_accuracy` | MEASURED — **TEST NOT RUN** |
69
+ | Calibration | ECE before / after | **0.013755 → 0.014929** | val, T = 0.9773 | 16,441 | `artifacts/calibration_v001.json` → `metrics.ece_before` / `metrics.ece_after` | MEASURED — **worse** |
70
+
71
+ ### 1.1 The three facts most likely to be misread
72
+
73
+ 1. **Grounding is reported under two protocols and two decode variants.** The trained head scores
74
+ **0.2838** (canonical) and **0.2566** (matched6) mean best IoU; the head-argmax decode scores
75
+ **0.1215** and the zero-shot baseline **0.0972**. Quoting one cell of that grid alone is selective.
76
+ The **matched6** protocol is the decode-matched comparison (head emits 6 boxes, baseline emits
77
+ 5.99); the **canonical** protocol is the config default (top_k 20).
78
+ 2. **Change-VQA is reported on two test sets.** `test` (0.697626 / 0.378373) and `test2`
79
+ (0.651469 / 0.372309). Quoting only the better one would be selective.
80
+ 3. **Router 0.965116 is validation, ungated, n = 86.** It is not a test result; the router test split
81
+ was **NOT RUN** for the shipped artifact.
82
+
83
+ ### 1.2 The only `VERIFIED` headline
84
+
85
+ The change-detection triple — **pooled IoU 0.8122 / macro IoU 0.8457 / pooled F1 0.8964** — is the
86
+ **only** headline in this repository that carries the `VERIFIED` tag, because it is the only one
87
+ measured against a single, immutable public test split with a frozen threshold. Everything else is
88
+ `MEASURED` (real, artifact-backed, but with an open ruling, a protocol caveat, or a validation-only
89
+ population).
90
+
91
+ ---
92
+
93
+ ## 2. Source artifacts
94
+
95
+ | Metric family | Artifact (paths relative to the source repository root) |
96
  |---|---|
97
  | change | `artifacts/change/eval_test/eval_result.json` |
98
+ | grounding (canonical) | `artifacts/grounding/remoteclip_grounding_v001/eval_result_canonical.json` |
99
+ | grounding (matched6) | `artifacts/grounding/remoteclip_grounding_v001/eval_result_matched6.json` |
100
  | optical-SAR | `artifacts/optical_sar/fusion_head_production_v001/pre_registered_115_metric.json` |
101
  | change-VQA | `artifacts/change_vqa/run/PROMOTION.json` |
102
  | router | `artifacts/router/threshold_sweep_val.json` |
103
  | calibration | `artifacts/calibration_v001.json` |
104
  | VLM | `artifacts/vlm/phase6_closure.json` |
105
 
106
+ Each of these artifacts carries the frozen config hash `78f1e3700da15aa1` (where it records a hash at
107
+ all), which ties it to the same `configs/base.yaml`. A config edit moves the hash and detaches the
108
+ number from its configuration — which is why the change head is wired into serving via a registry
109
+ override rather than a config key (`docs/MODELS.md` §3.1).
110
+
111
+ > **All 20 numeric claims in the headline table are checked against these files by
112
  > [`../tools/verify_readme_metrics.py`](../tools/verify_readme_metrics.py). Its output
113
+ > (`ALL CLAIMS VERIFIED`) is committed as [`../tools/readme_metrics_report.txt`](../tools/readme_metrics_report.txt).**
114
+ > §8 documents exactly what that tool checks.
115
 
116
+ ---
117
 
118
+ ## 3. The honesty rules this document follows
 
 
 
 
 
 
 
 
119
 
120
+ These are not aspirations. Each one is a rule that changed what is reported, and each is checkable
121
+ against the artifacts.
122
 
123
+ 1. **Evidence before claims.** Every reported number has an artifact path. A number with no artifact
124
+ does not appear in this document.
125
+ 2. **Two protocols are never collapsed.** Grounding is reported under *both* the canonical and
126
+ matched6 protocols. Quoting 0.2838 alone would be selective; quoting 0.2566 alone would be too.
127
+ 3. **Two decode variants are never collapsed into the trained-head claim.** `head_argmax` (0.1215) is
128
+ **not** apples-to-apples with the multi-box baseline (mean best IoU is a max over predictions, so
129
+ 1 box vs ~6 boxes flatters the head). It is reported because it is the number comparable to the
130
+ Phase 7 resolution experiment's zero-shot argmax — not because it decides anything.
131
+ 4. **Two test sets are never collapsed.** Change-VQA is reported on `test` **and** `test2`.
132
+ 5. **accuracy never travels without macro-F1.** For imbalanced multi-class heads (optical-SAR,
133
+ change-VQA) the macro-F1 is reported alongside accuracy, always, in the same table, with the same
134
+ split and the same `n`.
135
+ 6. **Validation is not test.** The router figure is labelled "overall **ungated** accuracy", val,
136
+ n = 86, corpus-limited. It is not a test result.
137
+ 7. **A negative result stays negative.** Calibration ECE worsened (0.013755 → 0.014929) and is shown
138
+ worsening. It is retained because it is in the frozen config, **not** because it helped.
139
+ 8. **USABLE ≠ ACCEPTED.** The VLM adapter's metrics are real and reproducible; the artifact is
140
+ nevertheless `ACCEPTANCE-REJECTED`. Both statements appear together.
141
+ 9. **A metric is never promoted to a system-level claim.** There is no end-to-end benchmark, and none
142
+ is invented by averaging the per-task numbers.
143
+ 10. **An absent number is stated, not omitted.** §6 lists everything that is `NOT RUN`.
144
+ 11. **A protocol-sensitive number carries its protocol.** Grounding always names canonical vs matched6
145
+ and the decode variant; optical-SAR always names the single-label subset and the 19-slot
146
+ denominator.
147
+ 12. **A pre-registered rule is applied as written.** The grounding resolution decision was decided by a
148
+ rule fixed *before* the result was seen, and the artifact records
149
+ `rule_changed_since_preregistration: false` (`docs/MODELS.md` §4.1).
150
+ 13. **A metric with a definitional detail states it.** The optical-SAR macro-F1 of 0.434161 is the mean
151
+ over **all 19 class slots**, not the 14 present classes — averaging over the present classes gives
152
+ 0.589218, and the artifact records `macro_f1_denominator`, `classes_present`, `classes_absent` and
153
+ the full 19-term vector so the difference cannot be misread.
154
+
155
+ ---
156
+
157
+ ## 4. Per-task evaluation protocols
158
+
159
+ ### 4.1 Change detection — `VERIFIED`
160
+
161
+ - **Artifact:** `artifacts/change/eval_test/eval_result.json`.
162
+ - **Split:** LEVIR-CD-256 **test**, n = **2,048** (immutable public split).
163
+ - **Threshold:** **0.50**, frozen in `configs/base.yaml` (`change.threshold`).
164
+ - **Metrics:** pooled IoU / macro IoU / pooled F1, **plus the full confusion counts** so any metric can
165
+ be recomputed rather than trusted.
166
+ - **Protocol detail:** tiles of 256 px (`change.tile_size: 256`), overlap 0
167
+ (`change.tile_overlap: 0`), minimum component 32 px (`change.min_component_pixels: 32`).
168
+
169
+ **Why both pooled and macro.** The test split is only ≈ 5 % changed pixels
170
+ (`metrics.mean_change_fraction: 0.0509`), and the **median tile has zero changed pixels**
171
+ (`change_fraction_quantiles.p50: 0.0`). Pooled IoU (0.8122) and macro IoU (0.8457) answer different
172
+ questions about that imbalance.
173
+
174
+ **Measured numbers** (all key paths under `metrics`):
175
+
176
+ | Metric | Value | Key path |
177
  |---|---|---|
178
+ | pooled IoU | **0.8122** | `pooled.iou` |
179
+ | pooled F1 | **0.8964** | `pooled.f1` |
180
+ | pooled mean IoU (`miou`) | 0.9007 | `pooled.miou` |
181
+ | pooled precision | 0.9195 | `pooled.precision` |
182
+ | pooled recall | 0.8745 | `pooled.recall` |
183
+ | macro IoU | **0.8457** | `macro.miou` |
184
+ | macro F1 | 0.7962 | `macro.f1` |
185
+ | macro IoU (`iou`, change-class IoU) | 0.718 | `macro.iou` |
186
+ | macro precision | 0.8506 | `macro.precision` |
187
+ | macro recall | 0.7757 | `macro.recall` |
188
+ | tp | 5,978,997 | `pooled.tp` |
189
+ | fp | 523,658 | `pooled.fp` |
190
+ | fn | 858,407 | `pooled.fn` |
191
+ | tn | 126,856,666 | `pooled.tn` |
192
+ | n_pixels | 134,217,728 | `pooled.n_pixels` |
193
+
194
+ **Population:** `n: 2048`, `n_images_with_change: 935`,
195
+ `change_fraction_quantiles: {min: 0.0, p50: 0.0, p90: 0.197205, max: 0.684937}`.
196
+
197
+ **Environment and integrity:** device `cuda`, torch `2.10.0+cu128`, Python `3.12.13`,
198
+ `seconds: 55.359`; `config_hash: 78f1e3700da15aa1`, `checkpoint_config_hash: 78f1e3700da15aa1`,
199
+ `checkpoint_config_hash_checked: true`, `config_drift: false`.
200
+
201
+ **Status:** `VERIFIED`. See [`EVALUATION.md`](EVALUATION.md) §2.1 and [`MODELS.md`](MODELS.md) §3.1.
202
+
203
+ ---
204
+
205
+ ### 4.2 Grounding — `MEASURED`, two protocols × two decode variants
206
+
207
+ - **Artifacts:** `artifacts/grounding/remoteclip_grounding_v001/eval_result_canonical.json`
208
+ (`head_decode.top_k: 20`) and `…/eval_result_matched6.json` (`head_decode.top_k: 6`).
209
+ - **Split:** VRSBench **eval** split, n = **16,159** records, scored once, no sampling
210
+ (`n_eval_records: 16159`, `limited_run: false`).
211
+ - **Resolution:** **224** (`resolution: 224`, `grid: 7`), frozen; 448 rejected by a pre-registered
212
+ paired test (`docs/MODELS.md` §4.1).
213
+ - **Box convention:** VRSBench stores boxes normalised to **0–100**; this project stores **0–1**, via
214
+ the declared `grounding.benchmark_box_scale: 100.0` so the conversion cannot be applied twice.
215
+ - **Protocols:** *canonical* and *matched6* — both reported.
216
+ - **Decode variants:** *head threshold* (the shipped decode), *head argmax*, and *zero-shot matched*
217
+ (the baseline).
218
+
219
+ **The `head_decode` block is recorded in each artifact** — `config_default_top_k: 20`,
220
+ `nms_iou: 0.5`, `score_threshold: 0.4`, `top_k: 20` (canonical) / `top_k: 6` (matched6). Without it
221
+ the artifact could not say which setting produced its number, and a re-run at the config default would
222
+ silently yield a different figure.
223
+
224
+ **The full grid — canonical** (`eval_result_canonical.json` → `results`):
225
+
226
+ | Strategy | `mean_best_iou` | `recall.0.10` | `recall.0.25` | `recall.0.50` | `latency_ms_per_image` | `seconds` |
227
+ |---|---|---|---|---|---|---|
228
+ | `head_argmax` | **0.1215** | 0.3183 | 0.2088 | 0.0795 | 0.655 | 10.6 |
229
+ | `head_threshold` (top_k 20) | **0.2838** | 0.6882 | 0.5047 | **0.2198** | 2.205 | 35.6 |
230
+ | `zero_shot_matched` | **0.0972** | 0.3298 | 0.1188 | 0.0234 | — | 17.9 |
231
+
232
+ **The full grid — matched6** (`eval_result_matched6.json` → `results`):
233
+
234
+ | Strategy | `mean_best_iou` | `recall.0.10` | `recall.0.25` | `recall.0.50` | `latency_ms_per_image` | `seconds` |
235
+ |---|---|---|---|---|---|---|
236
+ | `head_argmax` | **0.1215** | 0.3183 | 0.2088 | 0.0795 | 0.652 | 10.5 |
237
+ | `head_threshold` (top_k 6) | **0.2566** | 0.6315 | 0.4545 | **0.1938** | 2.158 | 34.9 |
238
+ | `zero_shot_matched` | **0.0972** | 0.3298 | 0.1188 | 0.0234 | — | 15.5 |
239
+
240
+ Both artifacts carry `phase7_reference: {mean_best_iou: 0.0972, recall_at_0.50: 0.0234,
241
+ source: "docs/PHASE7_RESOLUTION_DECISION.md"}` — the recorded baseline against which the head is
242
+ judged.
243
+
244
+ **Decode-matched delta (head threshold top_k = 6 vs zero-shot): +0.1594 IoU, +0.1704 Recall@0.50.**
245
+ The pre-registered bar was `MIN_IMPROVEMENT_IOU = 0.02`; the measured margin is **8× the bar**.
246
+
247
+ **Why `head_argmax` is not the trained-head claim.** 1 box against ~6 boxes flatters the head, because
248
+ mean best IoU takes the max over predictions. It is reported for comparability with the Phase 7
249
+ resolution experiment, not as the head's result.
250
+
251
+ **A decode defect this protocol had to fix first** (`docs/PHASE8_GROUNDING_HEAD_DECISION.md`). The
252
+ evaluation script originally built its own single-box baseline with `argmax_candidate`, while Phase 7
253
+ measured the baseline through `ground_phrase` (threshold box + up to 5 local maxima). Same 16,159
254
+ records, same metric, same cached features — **different decode**: 0.0972 vs 0.0092 (a 10× gap). The
255
+ eval printed `head beats zero-shot: True (+0.1123)` when the matched comparison was +0.0243. The fix
256
+ extracted the decode into one function both paths call
257
+ (`specialists/grounding/inference.py::decode_candidates_from_features`), and re-running the eval
258
+ reproduced the Phase 7 number **exactly** (`|difference| = 0.0000`) — proof the fix is real, not
259
+ cosmetic.
260
+
261
+ **Status:** `MEASURED`. The trained head beats the zero-shot baseline; the absolute IoU is low, which
262
+ is an honest limitation. See [`EVALUATION.md`](EVALUATION.md) §2.2 and [`MODELS.md`](MODELS.md) §3.4.
263
+
264
+ ---
265
+
266
+ ### 4.3 Optical-SAR fusion — `MEASURED`, ruling `OPEN`
267
+
268
+ - **Artifact:** `artifacts/optical_sar/fusion_head_production_v001/pre_registered_115_metric.json`.
269
+ - **Tool:** `scripts/eval_fusion_115.py` (read-only) — a **separate, later** step from training.
270
+ - **Split:** held-out **test**, n = **4,000** (`n_scored: 4000`, `split: "test"`).
271
+ - **Label space:** 19 CLC classes; **14 present, 5 absent** in the scored split.
272
+ - **macro-F1 denominator:** all 19 classes (absent classes contribute 0.0) — recorded explicitly.
273
+
274
+ **Why the metric is computed by a separate tool.** Every `run_record.json` carries
275
+ `pre_registered_metric_computed = false` and `result_status = "PLUMBING_ONLY — fixture/loop evidence,
276
+ NOT a result; the pre-registered 11.5 metric is not computed"`. That flag is **correct about the
277
+ trainer** and must not be "fixed": the trainer fits on `train`/`val` and never opens the test split,
278
+ deliberately, so the held-out split cannot be contaminated by the search over 10 runs, 2 arms and 5
279
+ seeds. The 11.5 metric therefore belongs to a separate, read-only step.
280
+
281
+ **The tool refuses (exit 2) rather than guessing** when: the head or cache is missing; the split is
282
+ empty; the cache `config_hash` is not `78f1e3700da15aa1` (a different experiment); or the head cannot
283
+ be built against the frozen `(B, 2318)` input contract.
284
+
285
+ **Measured numbers:**
286
+
287
+ | Metric | Value | Exact key path |
288
+ |---|---|---|
289
+ | **accuracy** | **0.931** | `accuracy` |
290
+ | **macro F1** | **0.434161** | `macro_f1` |
291
+ | loss | 0.254592 | `loss` |
292
+ | n scored | 4,000 | `n_scored` |
293
+ | num classes | 19 | `num_classes` |
294
+ | macro-F1 denominator | all 19 classes (absent contribute 0.0) | `macro_f1_denominator` |
295
+ | classes present | `[0,2,3,4,5,6,7,8,9,10,12,13,17,18]` | `classes_present` |
296
+ | classes absent | `[1,11,14,15,16]` | `classes_absent` |
297
+ | deciding statistic | **false** | `is_deciding_statistic` |
298
+ | head sha256 | `785815729a3a39fc34dc41894efaf00d8739365d970a3f830a326e68ae888dab` | `head_sha256` |
299
+ | head bytes | 14,427,457 | `head_bytes` |
300
+ | cache arm | A | `cache_arm` |
301
+ | cache config hash | `78f1e3700da15aa1` | `cache_config_hash` |
302
 
303
+ **Optical-SAR accuracy 0.931 must ALWAYS travel with macro-F1 0.434161.** The two are recorded side by
304
+ side in the artifact precisely so neither can be quoted alone.
305
+
306
+ **Per-class F1 (`_per_class_f1`, 19 terms):**
307
+
308
+ ```
309
+ [1.000, 0.000, 0.428, 0.571, 0.887, 0.000, 0.000, 0.800, 0.929, 0.940,
310
+ 0.182, 0.000, 0.200, 0.438, 0.000, 0.000, 0.000, 0.877, 0.998]
311
+ ```
312
+
313
+ **Two averages from the same vector — the denominator trap:**
314
+
315
+ | Averaged over | Value |
316
+ |---|---|
317
+ | **all 19 slots** — the pre-registered definition | **0.434161** |
318
+ | the 14 present classes only | **0.589218** |
319
+
320
+ A reader who computes the second and compares it to the recorded scalar will wrongly conclude the
321
+ recorded figure is wrong. It is the 19-slot mean by construction. Reproducible:
322
+ `sum(per_class)/19 = 0.43416082670034284`, `sum(per_class[c] for c in present)/14 = 0.5892182648076082`
323
+ (`docs/PHASE12_115_METRIC_COMPUTED.md` §3.5).
324
+
325
+ **Five absent classes contribute `0.0` by definition — but two PRESENT classes also score `0.0`.**
326
+ Classes **1, 11, 14, 15, 16** are absent; classes **5** and **6** are **present** and score `0.0` — a
327
+ **real total miss**. So part of the low macro-F1 is populational, and part is genuine per-class failure.
328
+ **High accuracy with a wide per-class spread is the signature of prediction dominated by frequent
329
+ classes.**
330
+
331
+ **Measured test-set label distribution** (`docs/PHASE12_115_METRIC_COMPUTED.md` §3.4):
332
+
333
+ ```
334
+ [13, 0, 63, 39, 846, 4, 1, 6, 453, 246, 4, 0, 9, 18, 0, 0, 0, 34, 2264]
335
+ ```
336
+
337
+ 14 of 19 classes present; five have zero samples; top-to-bottom ratio **2,264 : 1** (class 18 vs class
338
+ 6). The majority class holds 2,264 / 4,000 = **0.566**; the head scores **0.931**, so the accuracy is
339
+ not a constant predictor. Per-class recall (selected): class 18 n=2264 → **0.997**; class 9 n=246 →
340
+ 0.951; class 17 n=34 → 0.941; class 8 n=453 → 0.905; class 4 n=846 → 0.809; class 3 n=39 → 0.410;
341
+ class 12 n=9 → 0.111; classes 5 and 6 → **0.000**.
342
+
343
+ **Four independent verification checks** (`docs/PHASE12_115_METRIC_COMPUTED.md` §3): (1)
344
+ `evaluate_fusion_head` directly returns `n=4000, accuracy=0.931, macro_f1=0.43416082670034284`; (2)
345
+ rebuilding the input tensor by hand — `concat([optical_gap, sar_gap, joint_gap])` then
346
+ `concat([…, optical_mask, sar_mask])`, widths `(4000,768)×3 + (4000,12) + (4000,2) = 2318` — gives
347
+ 0.931 again (this checks feature **ordering**); (3) `sklearn.metrics` `accuracy_score = 0.931`,
348
+ `f1_score(average="macro", labels=range(19), zero_division=0) = 0.43416082670034284`; (4) the per-class
349
+ breakdown above.
350
+
351
+ **⚠️ The caveat that governs how these numbers may be used.** The cache metadata records
352
+ `label_policy = require_single_label`, `n_skipped_by_policy = 0`. reBEN v2.0 is a **multi-label**
353
+ corpus while the frozen head is a **single-label 19-class softmax**. The extraction used policy (b) —
354
+ restrict to single-label patches — which preserves the frozen architecture exactly but **changes the
355
+ evaluation population**. Single-label patches are **17.57 %** of the corpus (96,537 of 549,488), and the
356
+ rarest class survives as **1 patch** (a 59,204 : 1 imbalance). So this metric **may not** be presented
357
+ as a multi-label BigEarthNet/reBEN result, as comparable to published BigEarthNet numbers, or as a
358
+ statement about all 19 classes. It **may** be presented as the pre-registered 11.5 metric under the
359
+ single-label extraction policy on the held-out split.
360
+
361
+ **Status:** `MEASURED`, ruling **OPEN**. `is_deciding_statistic: false`; the artifact's `advisory`
362
+ states it *"selects no head, ranks nothing and compares no arms. Whether this constitutes a Phase 12
363
+ pass is the owner's ruling."* No numerical threshold is attached. See [`EVALUATION.md`](EVALUATION.md)
364
+ §2.3 and [`MODELS.md`](MODELS.md) §3.3.
365
+
366
+ ---
367
+
368
+ ### 4.4 Change-VQA — `MEASURED`, ruling `OPEN`
369
+
370
+ - **Artifact:** `artifacts/change_vqa/run/PROMOTION.json` (`verification` block).
371
+ - **Splits:** `test` (n = **39,686**) and `test2` (n = **31,036**).
372
+ - **Selection:** epoch **8**, chosen on **val answer accuracy 0.700018**, stopped by early stopping
373
+ (`identity.selected_on`, `identity.stop_reason`).
374
+ - **Answer space:** a **closed 19-answer space** over the eight CDVQA question types; type mask applied
375
+ by default (`verification.mask_gain: 0.0`).
376
+
377
+ **Two test sets are reported. Quoting only the better one would be selective.**
378
+
379
+ | Test set | n | accuracy | macro F1 |
380
+ |---|---|---|---|
381
+ | `test` | 39,686 | **0.697626** | **0.378373** |
382
+ | `test2` | 31,036 | **0.651469** | **0.372309** |
383
+
384
+ | Field | Value | Exact key path |
385
+ |---|---|---|
386
+ | test accuracy | 0.697626367 | `verification.test_accuracy` |
387
+ | test macro F1 | 0.378373275 | `verification.test_macro_f1` |
388
+ | test2 accuracy | 0.651469262 | `verification.test2_accuracy` |
389
+ | test2 macro F1 | 0.372308516 | `verification.test2_macro_f1` |
390
+ | n scored, test | 39,686 | `verification.n_scored_test` |
391
+ | n scored, test2 | 31,036 | `verification.n_scored_test2` |
392
+ | global majority baseline, test | 0.311546 | `verification.global_majority_baseline_test` |
393
+ | global majority baseline, test2 | 0.178728 | `verification.global_majority_baseline_test2` |
394
+ | mask gain | 0.0 | `verification.mask_gain` |
395
+ | checks passed / failed / unverified | 93 / 0 / 0 | `verification.checks_*` |
396
+ | metric ruling | `OPEN — the plan leaves the accuracy/macro-F1 interpretation owner-gated. No official aggregate metric is asserted here.` | `verification.metric_ruling` |
397
+
398
+ **The gap between accuracy and macro-F1 means the head is carried by common classes.** The
399
+ global-majority baselines make it concrete: 0.311546 on `test` means a constant predictor scores 0.31
400
+ and the head scores 0.70 — but the macro-F1 of 0.378 shows the per-class picture is far weaker than the
401
+ aggregate.
402
+
403
+ **Status:** `MEASURED`, ruling **OPEN**. No promotion/acceptance decision has been recorded
404
+ (`PROMOTION.json` → `state.note`). See [`EVALUATION.md`](EVALUATION.md) §2.4 and
405
+ [`MODELS.md`](MODELS.md) §3.2.
406
+
407
+ ---
408
+
409
+ ### 4.5 VLM adapter — `MEASURED`, `ACCEPTANCE-REJECTED`
410
+
411
+ - **Artifact:** `artifacts/vlm/phase6_closure.json` (`status: CLOSED`).
412
+ - **Split:** a frozen **1,000-question subset** of the test split, whose identity was proven without a
413
+ model (Gate A″): `available_per_split {val: 6750, test: 7772}`, subset `n = 1000`, test per-class
414
+ counts an **exact** match to the recovery subset (19 classes, sum 1000).
415
+
416
+ **The "usable" numbers** (`why_usable_verified.adapted_test`):
417
+
418
+ | Metric | Value | Exact key path |
419
+ |---|---|---|
420
+ | **exact_match** | **0.963** | `why_usable_verified.adapted_test.exact_match` |
421
+ | **F1** | **0.96432** | `why_usable_verified.adapted_test.f1` |
422
+ | n | 1000 | `…adapted_test.n` |
423
+ | precision | 0.963391 | `…adapted_test.precision` |
424
+ | recall | 0.965251 | `…adapted_test.recall` |
425
+ | tp / fp / tn / fn | 500 / 19 / 463 / 18 | `…adapted_test.confusion` |
426
+ | aggregate test delta | **+49.50 pp** (46.80 → 96.30 pp) | `why_usable_verified.aggregate_test_delta_pp` |
427
+
428
+ **The "rejected" side** (`why_acceptance_rejected`, rule version **v002**, decision split **test**):
429
+
430
+ | | |
431
+ |---|---|
432
+ | **V1** | **passes** — test `46.80 → 96.30 pp`, delta **+49.50 pp** (required ≥ +5.00) |
433
+ | **V2** | **fails** — **Mixed forest**: `n=33`, `100.00 → 87.8788 pp`, `drop 12.1212 pp`, `lost_questions 4`, `z 2.1335` |
434
+
435
+ It fails **both** halves of v002 (`lost ≥ 4` **and** `z ≥ 1.96`). Per item V, a complete run that fails
436
+ V2 is `REJECTED`. The rejection is **narrow**: `n_classes_failed: 1`, `n_classes_held: 5`,
437
+ `n_classes_improved: 11`, `n_classes_total: 19`. The next-worst class (Inland wetlands, −6.6667 pp,
438
+ n=30) lost only 2 questions and sits below V2's materiality floor.
439
+
440
+ **It is not a split artefact.** The same class also degraded on **val** in Run 1 (drop 6.4516 pp,
441
+ n=31). The adapter hurts Mixed forest on **both** splits. Mixed forest also sits at a 100.00 pp baseline
442
+ on test, so any loss is a drop from the ceiling.
443
+
444
+ **Residual risk, reported not resolved.** The verdict rests on **4 questions in one class of 33**. With
445
+ no `n ≥ N` floor in V2, a 33-question class can flip the verdict of a run whose aggregate endpoint
446
+ improved by 49.5 pp.
447
+
448
+ **Both rejection records are preserved** (`preserved_records`): Run 1's manifest `v001` on **val**
449
+ `REJECTED` (3 classes, `val_delta_pp 42.0`, `test_delta_pp null`); the independent test-split
450
+ adjudication `v002` on **test** `REJECTED`; and the recovery manifest `v002` on **val** `ACCEPTED` —
451
+ recorded **for completeness only, NOT final acceptance**, because it decides on the same val subset
452
+ that motivated v002.
453
+
454
+ **Status:** `MEASURED`; artifact `CLOSED`; headline **`ACCEPTANCE-REJECTED`**. **USABLE ≠ ACCEPTED.**
455
+ The deployed caption/VQA path uses the unadapted model. See [`EVALUATION.md`](EVALUATION.md) §2.5 and
456
+ [`MODELS.md`](MODELS.md) §3.6.
457
+
458
+ ---
459
+
460
+ ### 4.6 Router — `MEASURED`, `TEST NOT RUN`
461
+
462
+ - **Artifact:** `artifacts/router/threshold_sweep_val.json`.
463
+ - **Split scored:** **validation**, n = **86** (`n_val: 86`, `n_val_examples_scored: 86`,
464
+ `n_test_examples_scored: 0`, `test_split_touched: false`).
465
+ - **Metric:** overall **ungated** accuracy **0.965116** (`overall_ungated_accuracy`).
466
+ - **Corpus:** **corpus-limited** (`corpus_limited: true`); `corpus_total: 576`, `corpus_groups: 54`;
467
+ plan minima `plan_min_val_queries: 500`, `plan_min_hard_negatives: 100`; `val_min_support: 8`;
468
+ `hard_negatives_in_val: 0` (by design — `hn_*` families are held out to TEST).
469
+ - **Sweep:** thresholds 0.50 → 0.99; `select_by: "covered_accuracy"`;
470
+ `selected.threshold: 0.76` (coverage 0.790698, covered-task accuracy 1.0);
471
+ `shipped_threshold: 0.70` (coverage 0.848837, covered-task accuracy 0.972603);
472
+ `delta_vs_shipped: {coverage: -0.0581, covered_task_accuracy: 0.0274}`.
473
+ - **Split sizes:** `{train: 410, test: 80, val: 86}`.
474
+
475
+ **The artifact's own note, quoted verbatim in substance:** *"corpus-limited: val n=86 vs plan >=500.
476
+ This is NOT a calibration — the corpus is synthetic and too small (min per-class support 8, caption)
477
+ and val carries 0 hard negatives (hn_* families are held out to TEST by design). Selecting a threshold
478
+ here yields a justified default, not a calibrated value. The corpus was NOT padded with generated
479
+ queries. Backlog P1-9's 'n=80' is the TEST split; the sweep target is val n=86. **The test split was
480
+ NOT touched.**"*
481
+
482
+ **The router TEST split was NOT RUN.** Do not read 0.965116 as a test result. The sweep scored 86
483
+ validation examples and **zero** test examples.
484
+
485
+ > **Historical record, kept distinct.** `docs/PHASE4_ROUTER_REPORT.md` (verified 2026-09-16, Gate 2
486
+ > PASS) records an **earlier** gate-2 evaluation that included a test column — test task accuracy
487
+ > **0.975**, macro F1 0.976, hard-negative accuracy **0.800**. That evaluation predates the shipped
488
+ > threshold sweep, and its own standing caveat is that *"the 0.975 headline is partly earned on
489
+ > templates the split kept in training. Treat the router as working, not as benchmarked."* The release's
490
+ > position for the shipped artifact is **TEST NOT RUN**; the Phase 4 numbers are retained as a
491
+ > historical record, not promoted to a release benchmark.
492
+
493
+ **Status:** `MEASURED` — **TEST NOT RUN**. See [`EVALUATION.md`](EVALUATION.md) §2.6 and
494
+ [`MODELS.md`](MODELS.md) §3.5.
495
+
496
+ ---
497
+
498
+ ### 4.7 Calibration — `MEASURED`, negative result
499
+
500
+ - **Artifact:** `artifacts/calibration_v001.json` (`schema: calibration_v1`).
501
+ - **Fit split:** **val**, n = **16,441** (`temperature_scaling.fitted_on: "Val"`,
502
+ `temperature_scaling.n_samples: 16441`).
503
+ - **Temperature:** **T = 0.9772731820958189** (`temperature_scaling.temperature`).
504
+ - **Scope:** the R-02 change-VQA head's answer confidence only; other specialists emit their own raw
505
+ scores and are unaffected (`scope.note`).
506
+
507
+ | Metric | Before | After | Improvement | Key path |
508
+ |---|---|---|---|---|
509
+ | **ECE** | **0.013755** | **0.014929** | **−0.001174** | `metrics.ece_before` / `metrics.ece_after` / `metrics.ece_improvement` |
510
+ | NLL | 0.689741 | 0.689631 | +0.00011 | `metrics.nll_before` / `metrics.nll_after` |
511
+
512
+ `ece_improvement = −0.001174` is **negative**: expected calibration error **increased**. NLL improved
513
+ by a negligible 0.00011 (`fit_diagnostics.nll_improvement: 0.0001103574982127542`).
514
+ `metrics.n_bins: 15`, `metrics.n_classes: 19`.
515
+
516
+ **The scaling is retained because it is part of the frozen configuration, not because it helped.**
517
+ `provenance.held_out_splits_excluded: ["Test", "Test2"]` — the temperature was fitted only on val.
518
+
519
+ **The reliability diagram is explicitly labelled pre-scaling.** `reliability_diagram.ece` is
520
+ **0.013755** (the *before* value), with the note: *"Equal-width bins over predicted-class confidence.
521
+ ECE is bin-count sensitive and is not an aggregate score."* The diagram on the Benchmark page is
522
+ labelled **pre-scaling** so a reader cannot mistake it for the calibrated result.
523
+
524
+ **Status:** `MEASURED` — **worse**. A negative result, kept negative. See [`EVALUATION.md`](EVALUATION.md)
525
+ §2.7 and [`MODELS.md`](MODELS.md) §5.
526
+
527
+ ---
528
+
529
+ ## 5. Live validation — behavioural, not accuracy
530
 
531
  Accuracy is separate from **behavioural** validation. The deployed stack was driven end-to-end in a
532
+ headed browser, one upload per case, with per-case screenshots and recorded run ids.
533
 
534
  | Property | Result |
535
  |---|---|
 
542
  | Trace fill | **94.4444 %** on every live run |
543
  | Frontend regression suite | **106 passed** (`tests/unit/test_frontend_live_wiring.py`) |
544
 
545
+ **3 passes × 8 cases = 24 runs, 8/8 each, 0 mock nodes, 94.4444 % trace fill.** Every pass produced
546
+ **fresh run identifiers** — no run id is shared between passes. This is behavioural evidence that the
547
+ pipeline runs and routes correctly; it is **not** an accuracy claim.
548
+
549
+ ### 5.1 Harness integrity — a false-pass bug that was caught
550
+
551
+ An earlier harness revision typed queries with **synthetic CDP key events**, which Chrome **silently
552
+ drops when the window lacks OS focus**. The harness therefore dispatched the page's *default* query and
553
+ still recorded a "result" — a **false pass**.
554
+
555
+ The current harness **asserts form state before dispatch**: that the query box really holds the intended
556
+ query, that `#obsTail` reads `ready`, and that both frames are attached for pair tasks. The earlier 8/8
557
+ run was independently checked and confirmed **not** to have been infected (its answers were
558
+ query-specific and the query text was embedded in the answers). This failure mode is recorded because
559
+ it is exactly the kind of silent false-positive an evaluation harness must not have.
560
+
561
+ ### 5.2 Deployed HEADs
562
+
563
+ The live validation ran against these deployed revisions: Frontend `2d7ae53b482d`,
564
+ Backend `89d80eaddec5`, Inference `5a0936ace491`.
565
+
566
+ ### 5.3 What live validation does and does not establish
567
 
568
+ - **Establishes:** the pipeline runs end-to-end; the router dispatches the right specialist for the 8
569
+ cases in each pass; no mock nodes contaminate a live run; the trace is populated to 94.4444 %.
570
+ - **Does not establish:** any accuracy figure. There is no labelled ground truth attached to the 24
571
+ live runs, so no accuracy can be computed from them.
572
+
573
+ ---
574
+
575
+ ## 6. What is NOT benchmarked
576
+
577
+ An exhaustive list. Absent numbers are stated, never silently omitted or replaced with an estimate.
578
+
579
+ | Benchmark | Status | Note |
580
+ |---|---|---|
581
+ | **System-level end-to-end accuracy** | **NOT RUN — none exists** | There is no measured end-to-end benchmark of the full router → specialist → envelope pipeline. No such number is claimed anywhere. |
582
+ | **Router test split** | **NOT RUN** | Only the validation split (n = 86) was scored by the shipped threshold sweep. |
583
+ | **Grounding at 448 for the trained head** | **NOT RUN** | Only the *zero-shot baseline* was measured at 448 (`docs/MODELS.md` §4.1). Re-opening the resolution question for the trained head would be a new pre-registered experiment. |
584
+ | **Benchmark adapters** | **NOT RUN** | Adapter-based benchmark runs were not executed. |
585
+ | **Efficiency / latency benchmark** | **NOT RUN (system-level)** | Per-specialist latency is recorded incidentally in artifacts (e.g. grounding `latency_ms_per_image` 2.205 ms for the threshold decode, 0.655 ms for argmax; CROMA forward pass 0.89 s for a batch of 2 at 120 px), but there is **no end-to-end latency benchmark**. |
586
+ | **Cross-dataset generalisation** | **NOT RUN** | Each specialist is evaluated only on its own training-family test split. |
587
+ | **Human evaluation** | **NOT RUN** | No human rating of answers was performed. |
588
+ | **Robustness / adversarial evaluation** | **NOT RUN** | No perturbation, adversarial, or out-of-distribution robustness study was performed. |
589
+ | **Multi-label BigEarthNet / reBEN result** | **NOT PRODUCED** | The local subset is 100 % single-label against the official 1–11 multi-label scheme. Metrics are **not comparable** to published multi-label numbers. |
590
+ | **Grounding head wired and benchmarked in the serving path** | **NOT RUN** | `specialists/grounding/inference.py` exposes only the zero-shot path as of the Phase 8 record; wiring the head is the remaining integration step. |
591
+ | **Change-VQA served end-to-end** | **NOT RUN** | Serving needs both a trained head and a trained feature extractor; neither is wired by default, and an untrained head refuses to answer. |
592
+ | **Optical-SAR multi-label evaluation** | **NOT RUN** | See the single-label caveat (§4.3). |
593
+ | **Change-VQA on a third held-out set** | **NOT RUN** | Only `test` and `test2` exist. |
594
+ | **Any private / hidden evaluation data** | **NOT ACCESSED** | `evaluation.hidden_data_access: false` in `configs/base.yaml`. |
595
+ | **Calibrated reliability curve** | **NOT PLOTTED** | Only the pre-scaling diagram exists; the calibrated curve is not plotted. |
596
+ | **Per-class support for the router test split** | **NOT RUN** | The test split was not scored, so it has no support table. |
597
+
598
+ ---
599
+
600
+ ## 7. How to reproduce
601
+
602
+ ### 7.1 Verify every number in this document against its artifact
603
 
604
  ```bash
605
+ # verify every README/benchmark number against its source artifact
606
  python release/tools/verify_readme_metrics.py
607
  ```
608
 
609
+ The script resolves nested artifact keys — including keys that themselves contain dots, such as the
610
+ `recall` dict keyed `"0.10"/"0.25"/"0.50"` — and compares each value at the precision printed in the
611
+ README. It exits **non-zero if any claim fails**, and prints `ALL CLAIMS VERIFIED` (exit 0) only when
612
+ all 20 numeric claims match and the status assertions hold. Committed output:
613
+ [`../tools/readme_metrics_report.txt`](../tools/readme_metrics_report.txt).
614
+
615
+ ### 7.2 Reproduce the individual metrics
616
+
617
+ | Metric | Reproduction path |
618
+ |---|---|
619
+ | change | `scripts/eval_change.py` (refuses to score on a config-hash drift, exit 3) |
620
+ | grounding | `scripts/exp_grounding_resolution.py` + `scripts/analyze_grounding_resolution.py`; the two eval JSONs are recomputable from the per-sample JSONL without re-running the encoder |
621
+ | optical-SAR | `python scripts/eval_fusion_115.py --head … --cache-dir artifacts/optical_sar/fusion_features --split test --report …` (exit 0 = computed; exit 2 = refused) |
622
+ | change-VQA | `scripts/evaluate_change_vqa.py` (default checkpoint `artifacts/change_vqa/run/head.pt`) |
623
+ | VLM | `scripts/phase6_close.py` regenerates the closure record from the evidence; `scripts/phase6_adjudicate_test.py` re-runs the independent test-split adjudication |
624
+ | calibration | `load_calibration(config, base_dir='configs')` consumes `calibration_v001.json` |
625
+ | router | `scripts/train_router.py` (cached-embedding training); the sweep artifact is `threshold_sweep_val.json` |
626
+
627
+ See [`REPRODUCIBILITY.md`](REPRODUCIBILITY.md) for the full reproduction guide and
628
+ [`EVALUATION.md`](EVALUATION.md) for how each number was produced.
629
+
630
+ ### 7.3 Reproduce the live validation
631
+
632
+ The 24 live runs were driven in a headed browser with per-case screenshots and recorded run ids. The
633
+ harness asserts form state before dispatch (§5.1). See [`REPRODUCIBILITY.md`](REPRODUCIBILITY.md) for
634
+ the harness entry point.
635
+
636
+ ### 7.4 Verify the released artifacts
637
+
638
+ ```bash
639
+ sha256sum -c models/checksums.sha256
640
+ ```
641
+
642
+ ---
643
+
644
+ ## 8. The verification tool — exactly what it checks
645
+
646
+ `release/tools/verify_readme_metrics.py` is read-only: it writes nothing and prints a report suitable
647
+ for pasting into a release-verification record. It holds a hard-coded `CLAIMS` list of
648
+ `(label, artifact path, dotted key, printed value)` tuples. Key names were **discovered by walking the
649
+ artifacts, not assumed** — several live under nested paths (change metrics are `metrics.pooled.iou`;
650
+ grounding is `results.head_threshold.mean_best_iou`; VLM is `why_usable_verified.adapted_test.*`).
651
+
652
+ ### 8.1 The 20 numeric claims
653
+
654
+ | # | Claim | Artifact | Key path | Printed |
655
+ |---|---|---|---|---|
656
+ | 1 | change pooled IoU | `artifacts/change/eval_test/eval_result.json` | `metrics.pooled.iou` | 0.8122 |
657
+ | 2 | change macro IoU | `…/eval_result.json` | `metrics.macro.miou` | 0.8457 |
658
+ | 3 | change pooled F1 | `…/eval_result.json` | `metrics.pooled.f1` | 0.8964 |
659
+ | 4 | grounding canonical head_threshold mean_best_IoU | `…/eval_result_canonical.json` | `results.head_threshold.mean_best_iou` | 0.2838 |
660
+ | 5 | grounding canonical head_threshold recall@0.5 | `…/eval_result_canonical.json` | `results.head_threshold.recall.0.50` | 0.2198 |
661
+ | 6 | grounding matched6 head_threshold mean_best_IoU | `…/eval_result_matched6.json` | `results.head_threshold.mean_best_iou` | 0.2566 |
662
+ | 7 | grounding matched6 head_threshold recall@0.5 | `…/eval_result_matched6.json` | `results.head_threshold.recall.0.50` | 0.1938 |
663
+ | 8 | grounding head_argmax mean_best_IoU (canonical) | `…/eval_result_canonical.json` | `results.head_argmax.mean_best_iou` | 0.1215 |
664
+ | 9 | grounding zero-shot baseline IoU (canonical) | `…/eval_result_canonical.json` | `results.zero_shot_matched.mean_best_iou` | 0.0972 |
665
+ | 10 | optical-SAR fusion accuracy | `…/pre_registered_115_metric.json` | `accuracy` | 0.931 |
666
+ | 11 | optical-SAR fusion macro_F1 | `…/pre_registered_115_metric.json` | `macro_f1` | 0.434161 |
667
+ | 12 | change_vqa test accuracy | `artifacts/change_vqa/run/PROMOTION.json` | `verification.test_accuracy` | 0.697626 |
668
+ | 13 | change_vqa test macro_F1 | `…/PROMOTION.json` | `verification.test_macro_f1` | 0.378373 |
669
+ | 14 | change_vqa test2 accuracy | `…/PROMOTION.json` | `verification.test2_accuracy` | 0.651469 |
670
+ | 15 | change_vqa test2 macro_F1 | `…/PROMOTION.json` | `verification.test2_macro_f1` | 0.372309 |
671
+ | 16 | router overall ungated accuracy | `artifacts/router/threshold_sweep_val.json` | `overall_ungated_accuracy` | 0.965116 |
672
+ | 17 | calibration ECE before scaling | `artifacts/calibration_v001.json` | `metrics.ece_before` | 0.013755 |
673
+ | 18 | calibration ECE after scaling | `artifacts/calibration_v001.json` | `metrics.ece_after` | 0.014929 |
674
+ | 19 | VLM adapter exact_match | `artifacts/vlm/phase6_closure.json` | `why_usable_verified.adapted_test.exact_match` | 0.963 |
675
+ | 20 | VLM adapter F1 | `artifacts/vlm/phase6_closure.json` | `why_usable_verified.adapted_test.f1` | 0.96432 |
676
+
677
+ ### 8.2 The status assertions
678
+
679
+ Beyond the 20 numeric claims, the tool asserts artifact-declared **statuses**:
680
+
681
+ ```
682
+ VLM headline contains ACCEPTANCE-REJECTED : True
683
+ VLM status : CLOSED
684
+ router corpus_limited : True
685
+ router n_val : 86
686
+ calibration temperature : 0.9772731820958189
687
+ calibration ece_improvement : -0.001174 (negative => calibration did NOT help)
688
+ ```
689
+
690
+ ### 8.3 The committed output
691
+
692
+ `release/tools/readme_metrics_report.txt` records all 20 rows as `MATCH`, all six status assertions as
693
+ expected, and the final line:
694
+
695
+ ```
696
+ RESULT: ALL CLAIMS VERIFIED
697
+ ```
698
+
699
+ The tool exits `0` on that result and `1` otherwise, so a drifted artifact fails the release check
700
+ rather than passing silently.
701
+
702
+ ### 8.4 What the tool does not check
703
+
704
+ - It does **not** check the `n` or `split` fields. A value could match while the population changed;
705
+ the `n`/`split` columns in §1 are therefore maintained by hand against the artifacts.
706
+ - It does **not** check the calibration reliability diagram, the grounding per-sample JSONL, or the
707
+ VLM per-class tables.
708
+ - It does **not** verify the live-validation record (§5), which is behavioural and not artifact-scored.
709
+
710
+ ---
711
+
712
+ ## 9. Artifact schemas and key-path resolution
713
+
714
+ Every metric in §1 is addressed by a dotted **key path** into a JSON artifact. Two mechanics matter,
715
+ because getting either wrong produces a `NOKEY` failure that looks like a missing metric rather than a
716
+ bug in the reader.
717
+
718
+ ### 9.1 The dotted-key problem
719
+
720
+ Some artifact keys themselves contain dots. The grounding artifacts key their recall dict as
721
+ `{"0.10": …, "0.25": …, "0.50": …}`. A naive `dotted.split(".")` walk would break
722
+ `results.head_threshold.recall.0.50` into `… recall → 0 → 50` and fail — even though the value exists.
723
+
724
+ `verify_readme_metrics.py` resolves this by **preferring the longest matching key at each step**: at a
725
+ dict node it tries the longest possible joined key first, and only shortens when no key matches. That
726
+ is why the recall claim is written `results.head_threshold.recall.0.50` and resolves to the `"0.50"`
727
+ entry rather than to a nested `0` object.
728
+
729
+ ### 9.2 Top-level schema of each artifact family
730
+
731
+ These are the top-level keys actually present in each artifact, read from the files:
732
+
733
+ | Artifact | Top-level keys (abridged to the load-bearing ones) |
734
+ |---|---|
735
+ | `change/eval_test/eval_result.json` | `artifact`, `checkpoint`, `checkpoint_config_hash`, `checkpoint_config_hash_checked`, `checkpoint_embedded_config`, `config_drift`, `config_hash`, `created_at`, `device`, `environment`, `metrics`, `n`, `n_images_with_change`, `change_fraction_quantiles`, `per_image_change_fraction` |
736
+ | `grounding/…/eval_result_canonical.json` | `checkpoint_config_hash`, `config_drift`, `config_hash`, `created_at`, `device`, `environment`, `eval_cache`, `frozen_config_evaluation`, `grid`, `head_decode`, `limited_run`, `n_eval_records`, `phase7_reference`, `resolution`, `results` |
737
+ | `grounding/…/eval_result_matched6.json` | same shape; `head_decode.top_k` differs (6 vs 20) |
738
+ | `optical_sar/…/pre_registered_115_metric.json` | `tool`, `metric`, `definition`, `split`, `head_path`, `head_sha256`, `head_bytes`, `cache_path`, `cache_arm`, `cache_config_hash`, `n_scored`, `accuracy`, `macro_f1`, `loss`, `num_classes`, `macro_f1_denominator`, `classes_present`, `classes_absent`, `_per_class_f1`, `is_deciding_statistic`, `advisory` |
739
+ | `change_vqa/run/PROMOTION.json` | `schema`, `promoted_utc`, `promoted_by`, `artifact`, `source`, `identity`, `frozen_dependency`, `verification`, `state`, `serving_wiring` |
740
+ | `router/threshold_sweep_val.json` | `adapter_config_hash`, `adapter_encoder`, `adapter_path`, `artifact`, `config_hash`, `corpus_groups`, `corpus_limited`, `corpus_total`, `created_at`, `delta_vs_shipped`, `device`, `encoder_type`, `environment`, `force`, `hard_negatives_in_val`, `n_test_examples_scored`, `n_val`, `n_val_examples_scored`, `note`, `overall_ungated_accuracy`, `plan_min_hard_negatives`, `plan_min_val_queries`, `rows`, `seconds`, `select_by`, `selected`, `shipped_row`, `shipped_threshold`, `split`, `split_sizes`, `test_split_touched`, `thresholds`, `val_min_support`, `val_task_counts` |
741
+ | `calibration_v001.json` | `consumer_contract`, `created_utc`, `fit_diagnostics`, `metrics`, `provenance`, `reliability_diagram`, `schema`, `scope`, `temperature_scaling`, `type_mask_applied` |
742
+ | `vlm/phase6_closure.json` | `artifact_verification`, `evidence_index`, `forward_rule`, `generated_at`, `headline`, `kind`, `not_a_retrain`, `phase`, `preserved_records`, `production_adapter`, `rule_unchanged_by_closure`, `status`, `verified_vs_accepted`, `what_closure_does_not_claim`, `why_acceptance_rejected`, `why_usable_verified` |
743
+
744
+ ### 9.3 Status and provenance fields carried by every artifact
745
+
746
+ The artifacts are not just numbers; each carries the fields that make its number auditable:
747
+
748
+ | Field | Meaning | Where |
749
+ |---|---|---|
750
+ | `config_hash` | the frozen registry hash the run was made against (`78f1e3700da15aa1`) | change, grounding, optical-SAR, change-VQA, router, calibration, VLM |
751
+ | `checkpoint_config_hash` | the config hash **baked into the weights** | change, grounding |
752
+ | `config_drift` | whether the two hashes disagreed at evaluation time | change, grounding (both `false`) |
753
+ | `created_at` / `generated_at` | when the artifact was produced | all |
754
+ | `device` / `environment` | device and library versions | change, grounding, router |
755
+ | `split` | the population scored | optical-SAR (`test`), router (`val`) |
756
+ | `n` / `n_scored` / `n_eval_records` | the population size | all |
757
+
758
+ **Why `config_drift: false` matters.** The evaluation is only valid against the configuration the
759
+ weights were trained under. `checkpoint_config_hash_checked: true` and `config_drift: false` together
760
+ record that the check was *performed* and *passed* — not merely omitted.
761
+
762
+ ### 9.4 The `is_deciding_statistic` and ruling fields
763
+
764
+ Two artifacts carry an explicit statement that their number is **not** a decision:
765
+
766
+ - optical-SAR: `is_deciding_statistic: false`, with an `advisory` that the tool *"selects no head,
767
+ ranks nothing and compares no arms."*
768
+ - change-VQA: `verification.metric_ruling: "OPEN — … No official aggregate metric is asserted here."`
769
+
770
+ These fields exist so that a later reader cannot promote a measurement into a ruling by quoting it out
771
+ of context. Where a ruling exists, it is recorded separately (e.g. the optical-SAR A/B arm ruling was
772
+ made on `best_val_accuracy` — A 0.837100 vs B 0.839100, floor 0.0285 → **Arm A retained**, ruling
773
+ R-14 — not on the 0.931/0.434161 held-out metric).
774
+
775
+ ---
776
+
777
+ ## 10. Metric glossary and aggregation arithmetic
778
+
779
+ Definitions, so that a reader can recompute every number from the stored confusion counts or per-class
780
+ vectors.
781
+
782
+ ### 10.1 Pooled vs macro
783
+
784
+ | Term | Definition | Where used |
785
+ |---|---|---|
786
+ | **pooled IoU** | IoU over the union of all pixels, computed from the global `tp/fp/fn` counts | change (`metrics.pooled.iou` = 0.8122) |
787
+ | **macro IoU** | unweighted mean of per-class IoU. For binary change this is the mean over {change, no-change} | change (`metrics.macro.miou` = 0.8457) |
788
+ | **pooled F1** | `2·tp / (2·tp + fp + fn)` from the global counts | change (`metrics.pooled.f1` = 0.8964) |
789
+ | **macro F1** | unweighted mean of per-class F1 | change (`metrics.macro.f1` = 0.7962); optical-SAR (`macro_f1` = 0.434161) |
790
+
791
+ For the change artifact, `metrics.macro.iou` = 0.718 is the **change-class** IoU, while
792
+ `metrics.macro.miou` = 0.8457 is the **mean over both classes** — these are different numbers from the
793
+ same block, and the headline uses `miou`.
794
+
795
+ ### 10.2 Mean best IoU, mean matched IoU, recall@IoU
796
+
797
+ | Term | Definition | Where used |
798
+ |---|---|---|
799
+ | **mean best IoU** | for each record, the maximum IoU over all predicted boxes; then the mean over records | grounding (`mean_best_iou` = 0.2838 / 0.2566 / 0.1215 / 0.0972) |
800
+ | **mean matched IoU** | the IoU of the *matched* prediction only, so unmatched records contribute 0 | grounding (`mean_matched_iou`, equal to `mean_best_iou` in all three strategies) |
801
+ | **recall@0.10 / 0.25 / 0.50** | fraction of records whose best predicted box reaches that IoU | grounding (`results.*.recall`) |
802
+
803
+ **Mean best IoU is a max over predictions, so emitting more boxes raises it mechanically.** That is
804
+ exactly why the candidate count is recorded (`head_decode.top_k`) and why the decode-matched protocol
805
+ (top_k 6 vs the baseline's 5.99) is reported alongside the config default (top_k 20). See §4.2.
806
+
807
+ ### 10.3 The macro-F1 denominator
808
+
809
+ For a K-class head, `macro_f1 = (1/K) · Σ_c F1_c`. The **denominator is the label space**, not the
810
+ number of classes present in the scored split.
811
+
812
+ - optical-SAR: `K = 19` (`num_classes`), 14 present, 5 absent. Absent classes contribute `0.0`.
813
+ All-19 mean = **0.434161**; present-14 mean = **0.589218**.
814
+ - The artifact records `macro_f1_denominator: "all 19 classes (absent classes contribute 0.0)"`,
815
+ `classes_present`, `classes_absent`, and the full 19-term `_per_class_f1` vector so the denominator
816
+ is auditable rather than assumed.
817
+
818
+ ### 10.4 Percentage points vs fractions
819
+
820
+ Some artifacts record a **fraction** (0.963) and some a **percentage-point delta** (49.5 pp). The VLM
821
+ record uses both: `exact_match: 0.963` is a fraction, while `aggregate_test_delta_pp: 49.5` and the
822
+ baseline/adapted `46.80 → 96.30 pp` are percentage points. A reader must not confuse `+49.5 pp` with
823
+ `+0.495` of a fraction — the former is a 49.5-point move on a 0–100 scale.
824
+
825
+ ### 10.5 ECE and NLL
826
+
827
+ | Term | Definition | Where used |
828
+ |---|---|---|
829
+ | **ECE** | expected calibration error: bin-count-sensitive average gap between confidence and accuracy over equal-width bins | calibration (`metrics.ece_before` = 0.013755, `metrics.ece_after` = 0.014929, `n_bins` = 15) |
830
+ | **NLL** | mean negative log-likelihood | calibration (`metrics.nll_before` = 0.689741, `metrics.nll_after` = 0.689631) |
831
+
832
+ **ECE is bin-count sensitive and is not an aggregate score** — recorded verbatim in the artifact's
833
+ `reliability_diagram.note`. A smaller ECE is better; here it **increased** (`ece_improvement` =
834
+ −0.001174), which is a negative result.
835
+
836
+ ### 10.6 Coverage, fallback rate and ungated accuracy (router)
837
+
838
+ | Term | Definition | Where used |
839
+ |---|---|---|
840
+ | **overall ungated accuracy** | task accuracy over all examples, whether or not the confidence gate fired | router (`overall_ungated_accuracy` = 0.965116) |
841
+ | **coverage** | fraction of examples the gate accepted | router (`rows[*].coverage`; shipped 0.848837 at threshold 0.70) |
842
+ | **covered task accuracy** | task accuracy **among gated-in examples only** | router (`rows[*].covered_task_accuracy`; shipped 0.972603 at 0.70) |
843
+ | **fallback rate** | fraction routed to the deterministic lexical fallback | router (`rows[*].fallback_rate`; shipped 0.151163 at 0.70) |
844
+
845
+ The headline is the **ungated** figure, because a gated accuracy without its coverage is not comparable
846
+ across thresholds. That is why the sweep reports both, and why "ungated" is part of the label.
847
+
848
+ ### 10.7 Why the router's `selected` threshold differs from the `shipped` threshold
849
+
850
+ `select_by: "covered_accuracy"` maximises accuracy among gated-in examples, which is monotone in the
851
+ threshold (at 0.76 the covered accuracy is 1.0 on 68 examples). The **shipped** threshold is 0.70
852
+ (coverage 0.848837, covered accuracy 0.972603), which is the config default. The `delta_vs_shipped`
853
+ block records the difference: `{coverage: -0.0581, covered_task_accuracy: 0.0274}` — i.e. the selected
854
+ threshold covers 5.81 points fewer examples for 2.74 points more covered accuracy. The artifact is
855
+ explicit that this is *"a justified default, not a calibrated value."*
856
+
857
+ ---
858
+
859
+ ## 11. Evidence index
860
+
861
+ | Topic | Evidence |
862
+ |---|---|
863
+ | Verification tool | `release/tools/verify_readme_metrics.py`; committed output `release/tools/readme_metrics_report.txt` |
864
+ | Change | `artifacts/change/eval_test/eval_result.json` |
865
+ | Grounding (canonical / matched6) | `artifacts/grounding/remoteclip_grounding_v001/eval_result_canonical.json`, `…/eval_result_matched6.json`; `docs/PHASE7_RESOLUTION_DECISION.md`, `docs/PHASE8_GROUNDING_HEAD_DECISION.md` |
866
+ | Optical-SAR | `artifacts/optical_sar/fusion_head_production_v001/pre_registered_115_metric.json`; `docs/PHASE12_115_METRIC_COMPUTED.md`, `docs/PHASE14_OPTICAL_SAR_DECISIONS.md` |
867
+ | Change-VQA | `artifacts/change_vqa/run/PROMOTION.json` |
868
+ | Router | `artifacts/router/threshold_sweep_val.json`; `docs/PHASE4_ROUTER_REPORT.md` |
869
+ | Calibration | `artifacts/calibration_v001.json` |
870
+ | VLM | `artifacts/vlm/phase6_closure.json`; `docs/PHASE6_CLOSURE.md` |
871
+ | Live validation | the live-validation record and per-case screenshots; `tests/unit/test_frontend_live_wiring.py` (106 passed) |
872
+ | Cross-links | [`EVALUATION.md`](EVALUATION.md) (how each number was produced), [`MODELS.md`](MODELS.md) (the artifacts behind each number), [`DATASETS.md`](DATASETS.md) (the corpora), [`TRAINING.md`](TRAINING.md) (how each head was trained), [`LIMITATIONS.md`](LIMITATIONS.md) (the honest catalogue of gaps), [`REPRODUCIBILITY.md`](REPRODUCIBILITY.md) (reproduction guide) |