thundercode commited on
Commit
724b09e
Β·
verified Β·
1 Parent(s): b2500ae

release: add docs/ARCHITECTURE.md

Browse files
Files changed (1) hide show
  1. docs/ARCHITECTURE.md +201 -0
docs/ARCHITECTURE.md ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture
2
+
3
+ **Status vocabulary used throughout these docs:** `IMPLEMENTED` (code exists and runs) Β·
4
+ `VERIFIED` (checked against evidence) Β· `MEASURED` (a number was produced) Β· `ATTEMPTED`
5
+ (tried, outcome recorded) Β· `NOT RUN` Β· `BLOCKED` Β· `DEFERRED` Β· `REJECTED` (tried and
6
+ explicitly not accepted). Every architectural claim below carries one of these tags.
7
+
8
+ ---
9
+
10
+ ## 1. What the system is
11
+
12
+ SatQuery AI answers natural-language questions about satellite imagery. It is a **router +
13
+ specialists** system: a small intent router reads the question, dispatches it to one of six
14
+ task specialists, and assembles the specialist's output into a single structured
15
+ `ResultEnvelope` that the browser renders as an answer, an evidence list, and a confidence.
16
+
17
+ It is not a single end-to-end vision-language model. The only learned components are:
18
+
19
+ - a **50,822-parameter router adapter** over a frozen `all-MiniLM-L6-v2` sentence encoder;
20
+ - four trained **task heads/adapters** (change, change_vqa, optical-SAR fusion, grounding);
21
+ - one **LoRA adapter** over a frozen `SmolVLM-500M-Instruct` for caption/VQA.
22
+
23
+ Everything else is frozen, publicly-pinned backbone weights resolved at run time.
24
+
25
+ ## 2. Deployment topology (IMPLEMENTED, VERIFIED)
26
+
27
+ The shipped system is a four-tier chain. **Status: VERIFIED live on 2026-09-25** β€” the health,
28
+ capabilities and infer endpoints were probed against the running deployment.
29
+
30
+ ```
31
+ Browser
32
+ β”‚ HTTPS
33
+ β–Ό
34
+ Cloudflare Pages β€” satquery.pages.dev (static frontend, 11 pages)
35
+ β”‚ HTTPS/JSON
36
+ β–Ό
37
+ Render β€” satquery-backend-m4yv.onrender.com (orchestrator / gateway)
38
+ β”‚ outbound long-poll tunnel POST /tunnel/agent
39
+ β–Ό
40
+ GitHub Codespace β€” FastAPI inference, port 8000 (CPU)
41
+ β”‚ build_space_app()
42
+ β–Ό
43
+ Specialists: SmolVLM Β· RemoteCLIP Β· MiniLM Β· CROMA Β· STANet
44
+ ```
45
+
46
+ ```mermaid
47
+ flowchart LR
48
+ U[Browser] -->|HTTPS| CF["Cloudflare Pages<br/>static frontend"]
49
+ CF -->|"HTTPS JSON<br/>/api/health Β· /api/capabilities Β· /api/infer"| R["Render<br/>orchestrator / gateway"]
50
+ R -->|"outbound long-poll<br/>POST /tunnel/agent"| C["GitHub Codespace<br/>FastAPI inference :8000"]
51
+ C --> S[(SmolVLM Β· RemoteCLIP<br/>MiniLM Β· CROMA Β· STANet)]
52
+ C -->|ResultEnvelope| R
53
+ R -->|"envelope + error translation"| CF
54
+ ```
55
+
56
+ ### 2.1 Why a gateway and a tunnel
57
+
58
+ - **The gateway (Render)** is deliberately thin and stateless: schema validation, size limits,
59
+ per-IP rate limiting, a CORS allowlist (**never** `*`), request IDs, timeouts, secret custody,
60
+ and translation of upstream failures into the documented error envelope. It is **not** a model
61
+ host and has no database, auth, or queue.
62
+ - **The tunnel** exists because the inference host (a Codespace) is not publicly addressable for
63
+ this private repository β€” a forwarded port returns `302`. Instead the Codespace runs
64
+ `tunnel_agent.py`, which **dials out** to `POST /tunnel/agent` and long-polls. Work is executed
65
+ against `http://127.0.0.1:8000` locally. This inverts the usual direction: the inference host
66
+ needs no inbound firewall hole.
67
+
68
+ > **Status:** IMPLEMENTED and VERIFIED. `GET /api/health` reported `tunnel.agent_connected:true`
69
+ > with a non-zero `completed` counter on 2026-09-25.
70
+
71
+ ### 2.2 Cold start and the `transport_mode: auto` fallthrough (KNOWN ISSUE, OPEN)
72
+
73
+ The Render free tier sleeps when idle and the Codespace may be stopped. Cold start is therefore
74
+ tens of seconds and is **documented, not hidden**. A timeout on the tunnel path is surfaced to the
75
+ client as a recoverable error.
76
+
77
+ `SATQUERY_TRANSPORT=auto` means: try the tunnel; if it does not answer within
78
+ `SATQUERY_TUNNEL_TIMEOUT_S` (150 s), **fall through to the forward path**. The forward path to a
79
+ private repo returns `302` quickly, but the wake step still consumes `SATQUERY_WAKE_TIMEOUT_S`
80
+ (120 s) first β€” so a worst-case failed request can take β‰ˆ 249 s. This is the root of the observed
81
+ "transient tunnel gap" and is recorded as **OPEN** (not fixed) in
82
+ [`LIMITATIONS.md`](LIMITATIONS.md). A deployed fix (`codespace_name` newline handling on the
83
+ wake path) was authored and pushed separately; the fallthrough itself is by design.
84
+
85
+ ## 3. Request lifecycle (IMPLEMENTED, VERIFIED)
86
+
87
+ The controller is a nine-state machine, declared in `configs/base.yaml`:
88
+
89
+ ```
90
+ RECEIVE β†’ PARSE β†’ VALIDATE β†’ PLAN β†’ PREPROCESS β†’ EXECUTE β†’ AGGREGATE β†’ VERIFY β†’ RESPOND
91
+ ```
92
+
93
+ 1. **RECEIVE / PARSE** β€” the query and 1–2 image assets are decoded.
94
+ 2. **VALIDATE** β€” modality inference from band count, dimension equality for pairs, byte cap
95
+ (`4,194,304` bytes/file), GeoTIFF acceptance for the optical-SAR path.
96
+ 3. **PLAN** β€” the router embeds the query with the frozen MiniLM encoder and the trained adapter
97
+ predicts a task + modality. Dispatch is asset-count-aware: a single image cannot be a change
98
+ task.
99
+ 4. **PREPROCESS** β€” percentile normalisation for optical (2/98), dB clip for SAR (βˆ’30…+5 dB),
100
+ tiling for large scenes.
101
+ 5. **EXECUTE** β€” the chosen specialist runs.
102
+ 6. **AGGREGATE** β€” the specialist's raw output becomes `Evidence` items
103
+ (`Region` / `ChangeRegion`).
104
+ 7. **VERIFY** β€” confidence is computed; temperature scaling is applied from
105
+ `calibration_v001.json`.
106
+ 8. **RESPOND** β€” a `ResultEnvelope` is assembled and returned.
107
+
108
+ ### 3.1 The eight execution events
109
+
110
+ The frontend renders a live trace from eight named events (source of truth:
111
+ `frontend/assets/js/core.js`, `SQ.EVENT_NAMES`):
112
+
113
+ | # | Event | Emitted when |
114
+ |---|---|---|
115
+ | 1 | `QUERY_RECEIVED` | the request is accepted |
116
+ | 2 | `QUERY_UNDERSTOOD` | the router produces task + modality |
117
+ | 3 | `ROUTE_SELECTED` | the specialist is chosen |
118
+ | 4 | `SPECIALIST_STARTED` | specialist execution begins |
119
+ | 5 | `SPECIALIST_COMPLETED` | specialist returns |
120
+ | 6 | `EVIDENCE_GENERATED` | evidence items are built |
121
+ | 7 | `CONFIDENCE_COMPUTED` | confidence (and calibration) is computed |
122
+ | 8 | `RESULT_ASSEMBLED` | the envelope is finalised |
123
+
124
+ **Live runs emit all eight; the offline preview path emits none of the specialist events and is
125
+ labelled as a preview in the UI.** Measured trace fill on every live run: **94.4444 %**.
126
+
127
+ ## 4. Two reading paths: `interpret()` vs `chooseTask()` (IMPLEMENTED, VERIFIED)
128
+
129
+ There are two separate functions, and they see different information:
130
+
131
+ - **`interpret()`** β€” produces the *human-readable* interpretation shown in the console. It is
132
+ **asset-count-blind**: it only sees the query text.
133
+ - **`chooseTask()`** β€” performs the *dispatch*. It is **asset-count-aware**: it knows how many
134
+ assets are attached and will not route a single-image query to a two-image task.
135
+
136
+ This asymmetry is real and explains a documented behaviour: for the query *"What changed between
137
+ the earlier and later image?"* with a single asset attached, the console can *read* `change` while
138
+ dispatch correctly falls back to `change_vqa`. This is intentional, not a bug, and is covered in
139
+ [`RESEARCH_NOTES.md`](RESEARCH_NOTES.md).
140
+
141
+ ## 5. The four-endpoint inference contract (IMPLEMENTED, VERIFIED)
142
+
143
+ The Codespace exposes exactly four routes; the gateway mirrors each under `/api/*`:
144
+
145
+ | Codespace | Gateway | Cost | Purpose |
146
+ |---|---|---|---|
147
+ | `GET /v1/health` | `GET /api/health` | cheap | liveness, tunnel state, counters |
148
+ | `GET /v1/capabilities` | `GET /api/capabilities` | cheap | per-task availability |
149
+ | `POST /v1/analyze` | `POST /api/infer` | **costly** | run a query |
150
+ | `POST /v1/assets` | `POST /api/assets` | **costly** | stage an asset handle |
151
+
152
+ The gateway must **not** retry `POST /api/infer` on its own β€” a retry would consume inference a
153
+ second time. The client decides on retry. Errors are translated into a stable envelope with a
154
+ machine code (`invalid_request`, `tunnel_offline`, …) and a `recoverable` flag.
155
+
156
+ **Verified:** `POST /api/infer {}` returns `422 invalid_request` with header
157
+ `x-satquery-transport: tunnel`.
158
+
159
+ ## 6. The frozen configuration (VERIFIED)
160
+
161
+ All tunables live in `configs/base.yaml`; **no magic numbers in Python**. The loader validates
162
+ invariants and computes a hash. The frozen hash is:
163
+
164
+ ```
165
+ 78f1e3700da15aa1
166
+ ```
167
+
168
+ The loader **refuses to run** a config that violates a recorded invariant β€” for example
169
+ `fusion.input_dim == 3 * croma.encoder_dim + croma.optical_channels + croma.sar_channels`
170
+ (2318) and `grounding_head.feature_dim == 4 * grounding.encoder_projected_dim` (2048). A mismatch
171
+ in the latter is a *silent* shape error otherwise (torch raises only later, after features are
172
+ cached), so it is enforced at load time.
173
+
174
+ Editing `configs/base.yaml` moves the hash, which invalidates every artifact keyed to it. This is
175
+ why the stale `configs/deploy.yaml` (which still describes an HF-Space/ZeroGPU target) is left
176
+ **undisturbed as frozen paperwork** β€” it is never read at run time.
177
+
178
+ ## 7. Component map
179
+
180
+ | Layer | Path (in the source repo) | Notes |
181
+ |---|---|---|
182
+ | Frontend | `frontend/` | static, staged by `scripts/stage_pages.mjs` |
183
+ | Gateway | `deploy/render/` | `main.py` exposes `app`; `render.yaml` blueprint |
184
+ | Inference entry | `deploy/codespace/serve.py` | serves `build_space_app()` on `$PORT` |
185
+ | Inference app | `app/space_app.py` | `build_space_app()`; 4-endpoint contract |
186
+ | Composition root | `app/serving.py` | reuses the local serving path |
187
+ | Specialists | `specialists/` | vqa/caption (SmolVLM), grounding (RemoteCLIP), change (STANet), optical_sar (CROMA) |
188
+ | Router | `router/` | frozen MiniLM + trained adapter |
189
+ | Config | `configs/base.yaml` | authoritative registry; hashed |
190
+ | Artifacts | `artifacts/` | weights, metrics, calibration, reports |
191
+
192
+ > **Caveat.** `deploy/` inside the monorepo is **stale and untracked**; the deployed sources live in
193
+ > three separate private repositories. See [`DEPLOYMENT.md`](DEPLOYMENT.md).
194
+
195
+ ## 8. What is deliberately absent
196
+
197
+ - **No database, no auth, no queue.** The gateway is stateless by design.
198
+ - **No GPU requirement.** Device is selected via `SATQUERY_DEVICE`; all placement is `.to(device)`,
199
+ never `.cuda()`. The shipped deployment runs CPU-only.
200
+ - **No system-level end-to-end benchmark.** There is no measured end-to-end accuracy number, and
201
+ none is claimed. Per-specialist metrics exist and are reported in [`BENCHMARKS.md`](BENCHMARKS.md).