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

release: add docs/ARCHITECTURE.md

Browse files
Files changed (1) hide show
  1. docs/ARCHITECTURE.md +114 -170
docs/ARCHITECTURE.md CHANGED
@@ -1,201 +1,145 @@
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).
 
1
+ # SatQuery AI — Architecture
2
 
3
+ This is the architecture reference for SatQuery AI. It is written as a **hub plus ten deep
4
+ sub-documents**, because the system is large enough that a single file would either be superficial or
5
+ unreadable.
 
6
 
7
+ > **Status vocabulary used everywhere in these docs:** `IMPLEMENTED` (the code exists and runs) ·
8
+ > `VERIFIED` (checked against evidence) · `MEASURED` (a number was produced) · `ATTEMPTED` (tried,
9
+ > outcome recorded) · `NOT RUN` · `BLOCKED` · `DEFERRED` · `REJECTED` (tried and explicitly not
10
+ > accepted) · `OPEN` (known, unresolved) · `RESOLVED` · `CLOSED`.
11
+ >
12
+ > Every substantive claim in this set carries one of these tags, and every non-obvious claim cites
13
+ > the file it came from.
14
 
15
+ ---
 
 
 
16
 
17
+ ## 1. The thesis in one paragraph
18
 
19
+ SatQuery AI answers natural-language questions about satellite imagery. It is **not** one large
20
+ vision-language model. It is a **router plus specialists** system: a small learned router reads the
21
+ question and decides *which capability* is being asked for; the controller then runs exactly one
22
+ specialist; an evidence engine aggregates what that specialist produced; a confidence stage attaches
23
+ a calibrated (or honestly uncalibrated) number; and the whole thing is returned as one typed
24
+ `ResultEnvelope`. The only trained parameters in the system are six small modules sitting on frozen,
25
+ publicly-pinned backbones.
26
 
27
+ `docs/ARCHITECTURE_FREEZE.md` section 5 gives each layer exactly one verb, and the whole design
28
+ follows from that sentence:
29
 
30
+ > router **understands**; policy engine **decides**; specialists **compute**; VLM **explains**;
31
+ > evidence engine **proves**.
32
 
33
+ ## 2. Sub-documents
 
34
 
35
+ | # | Document | What it covers |
36
+ |---|---|---|
37
+ | 01 | [System overview](architecture/01-system-overview.md) | the thesis, the component inventory, the frozen-backbone strategy, what is deliberately absent |
38
+ | 02 | [Deployment topology](architecture/02-deployment-topology.md) | the four tiers, the gateway, the outbound tunnel, wake flow, cold start, `transport_mode` |
39
+ | 03 | [Request lifecycle](architecture/03-request-lifecycle.md) | the nine-state controller, validation rules, modality inference, tiling |
40
+ | 04 | [Router](architecture/04-router.md) | frozen MiniLM, the five-head adapter, `interpret()` vs `chooseTask()`, the lexical fallback, the label space |
41
+ | 05 | [Specialists](architecture/05-specialists.md) | all six tasks: entry points, preprocessing, postprocessing, outputs |
42
+ | 06 | [Evidence and confidence](architecture/06-evidence-and-confidence.md) | the evidence schema, the aggregation pipeline, temperature scaling, the eight execution events |
43
+ | 07 | [Configuration freeze](architecture/07-configuration-freeze.md) | the registry, the enforced invariants, the config hash, why it is frozen |
44
+ | 08 | [API contract](architecture/08-api-contract.md) | the four endpoints, the envelopes, error codes, transport headers |
45
+ | 09 | [Frontend](architecture/09-frontend.md) | the static pages, the Analyze console, real-vs-preview, platform traps |
46
+ | 10 | [Observability and operations](architecture/10-observability-and-ops.md) | health, counters, traces, what is and is not observed |
47
+
48
+ ## 3. The system at a glance
 
49
 
50
  ```mermaid
51
+ flowchart TB
52
+ subgraph Client
53
+ U[Browser]
54
+ end
55
+ subgraph Static["Static tier"]
56
+ CF["Cloudflare Pages<br/>satquery.pages.dev"]
57
+ end
58
+ subgraph Gateway["Gateway tier (Render)"]
59
+ R["satquery-orchestrator<br/>validate · CORS · limits · timeouts · errors"]
60
+ end
61
+ subgraph Inference["Inference tier (GitHub Codespace, CPU)"]
62
+ A["FastAPI · build_space_app()"]
63
+ CTRL["Controller (9-state FSM)"]
64
+ ROUTER["Router<br/>MiniLM + 5-head adapter"]
65
+ SPEC["Specialists<br/>vqa · caption · grounding · change · change_vqa · optical_sar"]
66
+ EV["Evidence engine<br/>dedup · order · renumber · cap"]
67
+ CONF["Confidence<br/>temperature scaling"]
68
+ A --> CTRL --> ROUTER --> SPEC --> EV --> CONF
69
+ end
70
+ U -->|HTTPS| CF
71
+ CF -->|"HTTPS JSON /api/*"| R
72
+ R -->|"outbound long-poll POST /tunnel/agent"| A
73
+ CONF -->|ResultEnvelope| R
74
+ R -->|envelope + error translation| CF
75
  ```
76
 
77
+ ## 4. Cross-cutting principles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ These recur in every sub-document and are the reason the code looks the way it does.
 
 
 
 
 
 
80
 
81
+ ### 4.1 One config system, no magic numbers
82
 
83
+ Every tunable value lives in `configs/base.yaml`. The loader (`core/config.py`) validates it against
84
+ the frozen architecture and hashes it. **No number is hard-coded in Python.** This is enforced
85
+ socially and structurally: a reviewer who finds a literal in a specialist has found a bug.
86
 
87
+ ### 4.2 Frozen backbones, trained modules
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ No backbone is fine-tuned. `all-MiniLM-L6-v2`, `SmolVLM-500M-Instruct`, `RemoteCLIP ViT-B/32` and
90
+ `CROMA-base` are all pinned **by revision** and fetched from the Hub at run time. What this project
91
+ trains is small: a 50,822-parameter router adapter, four heads, and one LoRA adapter. This is what
92
+ makes the system CPU-runnable.
93
 
94
+ ### 4.3 Typed contracts between every layer
95
 
96
+ `core/schemas.py` is the binding contract. No specialist may invent its own result shape; every
97
+ specialist returns a `SpecialistResult`. The schemas carry validators that encode real findings —
98
+ for example `Evidence` **refuses** spatial coordinates without a `coordinate_system`, because a bare
99
+ box is meaningless.
100
 
101
+ ### 4.4 Honest degradation over confident fabrication
 
 
 
102
 
103
+ The system is built so that "we could not do this" is representable and preferred to a plausible
104
+ guess. Concretely:
105
 
106
+ - a missing calibration artifact yields `method="uncalibrated"`, `calibrated=None` — **never** a
107
+ fabricated fitted number;
108
+ - a missing optional artifact **degrades** a capability rather than crashing the service;
109
+ - a *corrupt* artifact **raises**, because silently treating a corrupt file as "no file" would hide
110
+ an operational defect;
111
+ - the evidence engine records `dropped_over_limit` rather than silently truncating;
112
+ - the grounding specialist marks a result `degraded` when it produces no localisation.
113
 
114
+ ### 4.5 Reproducibility is a structural property
 
 
 
 
 
115
 
116
+ `EvidenceEngine.aggregate` is pure and deterministic — no clock, no RNG, no I/O. Evidence ids are
117
+ assigned from *sorted position*, not input order, so the same inputs produce byte-identical output.
118
+ `evidence_digest()` exists so that reproducibility is a test assertion rather than a hope.
119
 
120
+ ### 4.6 No chain-of-thought anywhere
 
121
 
122
+ `ExecutionTrace` records **observable facts only** — states, timings, counts, config hash, model
123
+ refs. There is no field for model reasoning and no LLM-generated confidence. This is a deliberate
124
+ constraint from the architecture freeze, not an omission.
125
 
126
+ ## 5. What the system deliberately does not have
 
127
 
128
+ | Absent | Why |
129
+ |---|---|
130
+ | Database, auth, queue | the gateway is stateless by design |
131
+ | GPU requirement | device is chosen via `SATQUERY_DEVICE`; all placement is `.to(device)` |
132
+ | Gradio GUI | the frontend is a separate static tier; `app/space_app.py` serves JSON only |
133
+ | End-to-end benchmark | none exists; none is claimed |
134
+ | Chain-of-thought | traces carry observable facts only |
135
+ | Backbone redistribution | backbones are fetched, pinned by revision |
 
136
 
137
+ ## 6. Where to start reading
 
 
138
 
139
+ - **New to the project** → [01 System overview](architecture/01-system-overview.md), then
140
+ [03 Request lifecycle](architecture/03-request-lifecycle.md).
141
+ - **Running it** → [`DEPLOYMENT.md`](DEPLOYMENT.md) and
142
+ [02 Deployment topology](architecture/02-deployment-topology.md).
143
+ - **Auditing the numbers** → [`BENCHMARKS.md`](BENCHMARKS.md) and [`EVALUATION.md`](EVALUATION.md).
144
+ - **Understanding the router defect** → [04 Router](architecture/04-router.md) and
145
+ [`RESEARCH_NOTES.md`](RESEARCH_NOTES.md).