Image-to-Text
PyTorch
Safetensors
PEFT
English
remote-sensing
satellite-imagery
earth-observation
change-detection
visual-grounding
image-captioning
visual-question-answering
optical-sar-fusion
sar
multimodal
lora
Instructions to use thundercode/SatQuery with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thundercode/SatQuery with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
release: add docs/FRONTEND.md
Browse files- docs/FRONTEND.md +1396 -0
docs/FRONTEND.md
ADDED
|
@@ -0,0 +1,1396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SatQuery AI — Frontend
|
| 2 |
+
|
| 3 |
+
**Chapter scope.** This chapter documents the SatQuery AI web frontend end to end: the static tier
|
| 4 |
+
and every page in it, the staging and deploy path that publishes it, the Analyze console in full
|
| 5 |
+
depth (every DOM handle, every state, every event), the eight-event execution protocol and the trace
|
| 6 |
+
bar it drives, the REAL-vs-PREVIEW driver split, the captured-run page, the Hugging Face header
|
| 7 |
+
link, cache-busting, and the Cloudflare platform traps that shape all of the above.
|
| 8 |
+
|
| 9 |
+
**Grounding.** Every claim below is taken from a file that was read for this chapter. Where a claim
|
| 10 |
+
comes from code, the file is cited inline, e.g. `(frontend/assets/js/mission.js)`. Where a number is
|
| 11 |
+
quoted it is a number that appears in a file; none is estimated. Where the evidence does not exist,
|
| 12 |
+
the text says exactly: `UNKNOWN — not established from the available evidence`.
|
| 13 |
+
|
| 14 |
+
**Status vocabulary** follows `release/DOCS_STYLE_GUIDE.md` §2: `IMPLEMENTED` · `VERIFIED` ·
|
| 15 |
+
`MEASURED` · `ATTEMPTED` · `NOT RUN` · `BLOCKED` · `DEFERRED` · `REJECTED` · `OPEN` · `RESOLVED` ·
|
| 16 |
+
`CLOSED`.
|
| 17 |
+
|
| 18 |
+
**Nothing in this chapter is a system-level accuracy claim.** Per `release/DOCS_STYLE_GUIDE.md` §3
|
| 19 |
+
there is **no end-to-end benchmark** for SatQuery AI; the frontend is a *client* of the service, and
|
| 20 |
+
the only system-level numbers quoted here are the ones the delivery documents themselves recorded
|
| 21 |
+
(live validation 3 passes × 8 cases, 8/8 each, 24 runs, 0 mock nodes, trace fill 94.4444 %).
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## 1. What the frontend is, and what it is not
|
| 26 |
+
|
| 27 |
+
SatQuery AI's frontend is a **static site**. It is HTML, CSS, and ES modules served from Cloudflare
|
| 28 |
+
Pages. There is no build step that compiles application code, no bundler, no framework, no server
|
| 29 |
+
rendering, and no runtime dependency on a Node process. The staging tool
|
| 30 |
+
(`scripts/stage_pages.mjs`) copies a *reference-closed subset* of `frontend/` into an output
|
| 31 |
+
directory and then hands that directory to `wrangler`.
|
| 32 |
+
|
| 33 |
+
The site is **hermetic except for one page**. The staging tool computes and prints a reference
|
| 34 |
+
integrity and external-dependency audit, and it reports `HERMETIC` when a page's reference closure
|
| 35 |
+
contains zero network dependencies (`scripts/stage_pages.mjs`). The single deliberate exception is
|
| 36 |
+
`frontend/mission.html`, the Analyze console, which carries a live API base in a `<meta>` tag and can
|
| 37 |
+
call the deployed service. Everything else — the homepage, the essay, the atlas, the architecture
|
| 38 |
+
tour, the benchmark and research pages, the captured-run page — is designed to render without any
|
| 39 |
+
network call beyond its own assets.
|
| 40 |
+
|
| 41 |
+
> **Honesty note (drift recorded, not hidden).** An older comment inside `frontend/_headers` claimed
|
| 42 |
+
> the site was "100% static, zero network calls". That claim is **stale** and is not repeated here as
|
| 43 |
+
> current truth: `mission.html` is a live-calling page, and `mission.html` is one of the eleven
|
| 44 |
+
> shipped pages. The correct current statement is: *ten of eleven pages are hermetic; `mission.html`
|
| 45 |
+
> is the one live-calling page.*
|
| 46 |
+
|
| 47 |
+
### 1.1 The design law the frontend was built under
|
| 48 |
+
|
| 49 |
+
`frontend/HANDOFF.md` is the governing design document for the frontend. Its §1 states the design
|
| 50 |
+
law; §2 defines the token system as CSS custom properties on `:root`; §3–§10 lay out build phases
|
| 51 |
+
A–G; §9 names the integration seam (`SQ.run().ingest`); §13 lists known hard limits; §14 lists the
|
| 52 |
+
real SatQuery schema type names that the frontend is allowed to speak.
|
| 53 |
+
|
| 54 |
+
The practical consequences of that design law, as they appear in the shipped code:
|
| 55 |
+
|
| 56 |
+
- **No fabricated imagery is presented as real.** Synthetic imagery produced at runtime carries a
|
| 57 |
+
`synthetic: true` flag (`frontend/assets/js/core.js`, `SQ.scene`), and pages that use placeholder
|
| 58 |
+
numbers say so in their own prose (e.g. `frontend/atlas.html` states its numbers are placeholders).
|
| 59 |
+
- **The eight-event vocabulary is fixed.** The frontend may not invent event names; it emits exactly
|
| 60 |
+
the eight names declared in `SQ.EVENT_NAMES` (`frontend/assets/js/core.js`).
|
| 61 |
+
- **The event stream is the seam.** Any driver — mock, live, or a captured replay — talks to the UI
|
| 62 |
+
only by calling `ingest(type, payload)`. Nothing else may mutate the console.
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 2. The static tier: file layout
|
| 67 |
+
|
| 68 |
+
The shipped frontend is a flat set of pages plus three asset trees.
|
| 69 |
+
|
| 70 |
+
```
|
| 71 |
+
frontend/
|
| 72 |
+
*.html top-level pages (the staging seed set)
|
| 73 |
+
_headers Cloudflare Pages header rules (see §12)
|
| 74 |
+
HANDOFF.md the frontend design/handoff document
|
| 75 |
+
assets/
|
| 76 |
+
css/ stylesheets
|
| 77 |
+
js/
|
| 78 |
+
core.js SQ namespace: rng, scene synthesis, policy router,
|
| 79 |
+
event names, mock run driver, shared components
|
| 80 |
+
live.js the real HTTP ingestion client (assets + infer)
|
| 81 |
+
mission.js the Analyze console driver (PREVIEW + LIVE)
|
| 82 |
+
run.js the captured-run ("Anatomy of a Run") driver
|
| 83 |
+
<page drivers> per-page behaviour
|
| 84 |
+
data/
|
| 85 |
+
anatomy-run.js the captured real ResultEnvelope (sanitized)
|
| 86 |
+
img/ real EO imagery (eo/…), plates, thumbnails
|
| 87 |
+
video/ the launch film and clips
|
| 88 |
+
fonts/ webfonts
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Two facts about this layout matter for deployment:
|
| 92 |
+
|
| 93 |
+
1. The **staging seed** is the set of top-level `frontend/*.html` files
|
| 94 |
+
(`scripts/stage_pages.mjs`). Pages are discovered from HTML, and then their reference closure
|
| 95 |
+
(CSS `@import`/`url()`, JS `import`/`export … from`, and dynamic imports) is walked so that only
|
| 96 |
+
referenced assets ship.
|
| 97 |
+
2. Because the closure is reference-driven, **an asset that is not referenced by a reachable page
|
| 98 |
+
does not ship**. This is deliberate: it keeps the uploaded tree small and it makes dead assets
|
| 99 |
+
visible (they simply do not appear in the staged tree report).
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## 3. The eleven pages
|
| 104 |
+
|
| 105 |
+
Eleven HTML pages ship. Each was read for this chapter. The table gives the page's purpose and its
|
| 106 |
+
`data-view` (the attribute each page's `<body>` carries, which the CSS uses to scope page-specific
|
| 107 |
+
rules).
|
| 108 |
+
|
| 109 |
+
| # | File | Purpose | Notes |
|
| 110 |
+
|---|---|---|---|
|
| 111 |
+
| 1 | `frontend/index.html` | Homepage / front door. Orbit hero, invitation form, open questions, essay film, discover/evidence/understand/measure/atlas sections. | Carries the launch video and the delta pair. |
|
| 112 |
+
| 2 | `frontend/mission.html` | **The Analyze console.** Query box, two upload widgets, intent panel, viewer, comparison, answer, evidence, confidence, provenance, trace bar, event drawer. | The **only** live-calling page. Carries `<meta name="satquery-api-base">`. |
|
| 113 |
+
| 3 | `frontend/architecture.html` | Architecture tour: how a query becomes an answer, stage by stage. | Footer discloses that its transmission is driven by the prototype mock event stream. |
|
| 114 |
+
| 4 | `frontend/run.html` | **Anatomy of a Run** — renders a real captured `ResultEnvelope` (`run_d124d8b9adea`). | Driven by `frontend/assets/js/run.js` over `frontend/assets/data/anatomy-run.js`. |
|
| 115 |
+
| 5 | `frontend/benchmark.html` | Benchmark page: measured results with an evidence-state legend. | Legend vocabulary: VERIFIED / SUPPORTED / UNVERIFIED / BLOCKED / NOT RUN. |
|
| 116 |
+
| 6 | `frontend/research.html` | Research notes: methods, calibration, honest caveats. | Links into the measurement story. |
|
| 117 |
+
| 7 | `frontend/journey.html` | The build journey / narrative page. | Carries the HF + GitHub header links. |
|
| 118 |
+
| 8 | `frontend/atlas.html` | Atlas of four real EO thumbnails. | Page states its numbers are placeholders. |
|
| 119 |
+
| 9 | `frontend/references.html` | References / citations page. | — |
|
| 120 |
+
| 10 | `frontend/video.html` | Video library: four clips, three planned shorts named. | — |
|
| 121 |
+
| 11 | `frontend/404.html` | Not-found page. | Prose says "Ten pages exist" but links five — drift, recorded in §13. |
|
| 122 |
+
|
| 123 |
+
### 3.1 Page-by-page detail
|
| 124 |
+
|
| 125 |
+
**`index.html` (homepage).** 424 lines. Structure: a navigation bar carrying the GitHub and Hugging
|
| 126 |
+
Face links; an orbit hero using `assets/img/eo/nile-wide.jpg`; an invitation form whose action is
|
| 127 |
+
`mission.html`; an "open questions" list containing three `mission.html?q=…` links (so a visitor can
|
| 128 |
+
land in the Analyze console with a question pre-filled); an essay-film section using
|
| 129 |
+
`assets/video/satquery-launch-50s.mp4` with eight `data-chapters` markers; an "ask" section using
|
| 130 |
+
`assets/img/eo/delta-plain.jpg`; a "discover" section that presents the **delta-growth t0/t1 pair** as
|
| 131 |
+
a wipe slider (`delta-growth-t0-720` / `delta-growth-t1-720`); an "evidence" section using
|
| 132 |
+
`delta-growth-t2-2075.jpg`; an "understand" section listing six layers; a "measure" section with
|
| 133 |
+
benchmark and research cards; and an "atlas" section with four real EO thumbnails. The footer notes
|
| 134 |
+
name the event span `QUERY_RECEIVED` → `RESULT_ASSEMBLED`, i.e. the first and last of the eight
|
| 135 |
+
events.
|
| 136 |
+
|
| 137 |
+
**`mission.html` (Analyze console).** 297 lines. This is the page this chapter spends most of its
|
| 138 |
+
length on; see §5.
|
| 139 |
+
|
| 140 |
+
**`architecture.html`.** The architecture tour. It walks the reader from a natural-language query
|
| 141 |
+
through routing, planning, specialists, evidence, confidence, and result assembly. Its footer makes
|
| 142 |
+
an explicit honesty disclosure: the transmission shown on the page is driven by the **prototype mock
|
| 143 |
+
event stream**, not by a live run. That disclosure is the page doing the right thing — the animation
|
| 144 |
+
is real UI driven by the same eight-event seam, but the data behind it on this page is the mock
|
| 145 |
+
driver's.
|
| 146 |
+
|
| 147 |
+
**`run.html` ("Anatomy of a Run").** Renders a **real captured** envelope. See §9.
|
| 148 |
+
|
| 149 |
+
**`benchmark.html`.** Presents measured results. It carries an evidence-state legend whose
|
| 150 |
+
vocabulary is VERIFIED / SUPPORTED / UNVERIFIED / BLOCKED / NOT RUN, and it notes the measured
|
| 151 |
+
reliability curve. Per `release/DOCS_STYLE_GUIDE.md` §3, this page is the right place for the
|
| 152 |
+
per-artifact numbers (grounding under two protocols, change IoU, optical-SAR accuracy-with-macro-F1,
|
| 153 |
+
change-VQA two test sets, router validation-only), and it must never present them as a system-level
|
| 154 |
+
score.
|
| 155 |
+
|
| 156 |
+
**`research.html`.** Research notes: method, calibration, and caveats. This is where the calibration
|
| 157 |
+
result belongs, and per the style guide it must be stated correctly: ECE went **0.013755 → 0.014929
|
| 158 |
+
— worse**, and the transform is retained only because it is in the frozen config.
|
| 159 |
+
|
| 160 |
+
**`journey.html`.** Narrative page for the build process.
|
| 161 |
+
|
| 162 |
+
**`atlas.html`.** Four real EO thumbnails. The page states in its own prose that its numbers are
|
| 163 |
+
placeholders. That statement is correct and must be preserved: the atlas is a *gallery*, not a
|
| 164 |
+
measurement.
|
| 165 |
+
|
| 166 |
+
**`references.html`.** Citations.
|
| 167 |
+
|
| 168 |
+
**`video.html`.** Lists four clips and names three planned shorts. The planned shorts are labelled as
|
| 169 |
+
planned, not shipped.
|
| 170 |
+
|
| 171 |
+
**`404.html`.** The not-found page. Its prose says "Ten pages exist" while eleven do, and it links
|
| 172 |
+
five. This is documentation drift inside a shipped page; it is recorded here and in §13 rather than
|
| 173 |
+
silently corrected, because correcting it would be an edit outside this chapter's scope.
|
| 174 |
+
|
| 175 |
+
### 3.2 The Hugging Face header link — present on all eleven pages
|
| 176 |
+
|
| 177 |
+
Every one of the eleven pages carries, in its navigation, both:
|
| 178 |
+
|
| 179 |
+
- a **GitHub** link to `https://github.com/Anish-lab-blip/SatQuery-AI`, and
|
| 180 |
+
- a **Hugging Face** link to `https://huggingface.co/thundercode/SatQuery`.
|
| 181 |
+
|
| 182 |
+
This was verified by searching all `frontend/*.html` for `huggingface.co` and `github.com` and
|
| 183 |
+
confirming a match in each of: `index`, `journey`, `mission`, `404`, `video`, `atlas`,
|
| 184 |
+
`architecture`, `benchmark`, `run`, `research`, `references` — eleven files, eleven matches each.
|
| 185 |
+
`docs/FINAL_DELIVERY_TODO.md` records this as a post-handoff sprint outcome ("the HF link on all 11
|
| 186 |
+
pages").
|
| 187 |
+
|
| 188 |
+
The reason this is called out as its own subsection: the public release is *GitHub + Hugging Face*,
|
| 189 |
+
and the requirement that the HF link appear on **all** pages (not just the homepage) is a delivery
|
| 190 |
+
requirement, so it is stated as a verified fact with the method of verification.
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## 4. Staging and deploy path
|
| 195 |
+
|
| 196 |
+
### 4.1 `scripts/stage_pages.mjs` — the reference-closed staging tool
|
| 197 |
+
|
| 198 |
+
`scripts/stage_pages.mjs` is 378 lines and is the tool that turns the working `frontend/` directory
|
| 199 |
+
into a deployable tree. It is deliberately conservative.
|
| 200 |
+
|
| 201 |
+
**Constants.**
|
| 202 |
+
|
| 203 |
+
| Constant | Value | Meaning |
|
| 204 |
+
|---|---|---|
|
| 205 |
+
| `PAGES_FILE_LIMIT` | `26214400` (25 MiB) | Cloudflare Pages per-file hard limit. |
|
| 206 |
+
| `BIG_WARN_BYTES` | `10485760` (10 MiB) | Warn threshold for a large file. |
|
| 207 |
+
|
| 208 |
+
**Reference extraction.** The tool uses a small set of regexes to find references inside each file
|
| 209 |
+
type:
|
| 210 |
+
|
| 211 |
+
- `RE_HTML` — HTML references (`<script src>`, `<link href>`, `<img src>`, etc.)
|
| 212 |
+
- `RE_CSS_IMPORT` — CSS `@import`
|
| 213 |
+
- `RE_CSS_URL` — CSS `url(...)`
|
| 214 |
+
- `RE_JS_IMPORT` — JS `import … from`
|
| 215 |
+
- `RE_JS_EXPORT` — JS `export … from`
|
| 216 |
+
- `RE_JS_DYN` — JS dynamic `import(...)`
|
| 217 |
+
|
| 218 |
+
Supporting helpers: `stripComments()` (so a reference inside a comment does not become a false
|
| 219 |
+
edge), `extractRefs()`, `isExternal()` (absolute URLs and protocol-relative URLs are not followed),
|
| 220 |
+
`stripQueryHash()` (so `app.js?v=2` resolves to `app.js`), and `insideFrontend()` (a guard so a
|
| 221 |
+
reference cannot escape the `frontend/` root).
|
| 222 |
+
|
| 223 |
+
**Algorithm.**
|
| 224 |
+
|
| 225 |
+
1. **Seed.** Take the set of top-level `frontend/*.html` files.
|
| 226 |
+
2. **Closure walk.** For each file in the frontier, extract its references, resolve each to a
|
| 227 |
+
path inside `frontend/`, and add the new ones to the frontier. Repeat until the frontier is empty.
|
| 228 |
+
3. **Copy.** Copy every file in the closure into the output root, preserving relative paths.
|
| 229 |
+
4. **Size gate.** If any file exceeds `PAGES_FILE_LIMIT` (25 MiB), hard-fail with **exit code 2**.
|
| 230 |
+
Files above `BIG_WARN_BYTES` (10 MiB) produce a warning.
|
| 231 |
+
5. **Verify.** Re-walk the *staged* tree and confirm the closure is intact (no dangling reference).
|
| 232 |
+
Failure is **exit code 3**.
|
| 233 |
+
6. **Report.** Print four report blocks:
|
| 234 |
+
- `=== STAGED TREE ===`
|
| 235 |
+
- `=== REFERENCE INTEGRITY ===`
|
| 236 |
+
- `=== EXTERNAL DEPENDENCY AUDIT ===` — prints `HERMETIC` when zero network dependencies are
|
| 237 |
+
found.
|
| 238 |
+
- `=== OPTIONS APPLIED ===`
|
| 239 |
+
7. **Hint.** Print the deploy command to run next:
|
| 240 |
+
`npx wrangler pages deploy "<OUT_ROOT>" --project-name <name>`.
|
| 241 |
+
|
| 242 |
+
**Why the exit codes matter.** A staging run that silently produced an incomplete tree would deploy
|
| 243 |
+
a broken site; a staging run that silently produced an over-limit tree would deploy a site that
|
| 244 |
+
Cloudflare rejects. The tool therefore fails loudly *before* upload (exit 2 for size, exit 3 for
|
| 245 |
+
integrity) rather than letting `wrangler` discover the problem.
|
| 246 |
+
|
| 247 |
+
**Argument parsing.** `parseArgs()` handles the CLI surface and `usage()` prints help. The tool is
|
| 248 |
+
invoked as a Node script (`node scripts/stage_pages.mjs …`).
|
| 249 |
+
|
| 250 |
+
### 4.2 The deploy command
|
| 251 |
+
|
| 252 |
+
The tool's own final hint is the deploy step:
|
| 253 |
+
|
| 254 |
+
```
|
| 255 |
+
npx wrangler pages deploy "<OUT_ROOT>" --project-name <name>
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
Deployment is therefore: **stage to a directory → `wrangler pages deploy` that directory**. There is
|
| 259 |
+
no compile step between the two. The deployed frontend HEAD recorded in the delivery documents is
|
| 260 |
+
`2d7ae53b482d` (`docs/FINAL_DELIVERY_TODO.md`, `release/DOCS_STYLE_GUIDE.md` §3).
|
| 261 |
+
|
| 262 |
+
> **Superseded-topology note.** `docs/DEPLOYMENT_ARCHITECTURE.md` opens with a superseded-topology
|
| 263 |
+
> banner, and its body still names Railway / HF-Space hosts while the active topology is
|
| 264 |
+
> Render / Codespace (`docs/DEPLOYMENT_TOPOLOGY.md`). For the frontend specifically, the host is
|
| 265 |
+
> Cloudflare Pages in both readings; the drift concerns the *backend* hosts, not the static tier.
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## 5. The Analyze console (`frontend/mission.html`) in depth
|
| 270 |
+
|
| 271 |
+
The Analyze console is the frontend's centre of gravity. This section documents its markup (every
|
| 272 |
+
handle), its state machine, its two drivers, and its event rendering.
|
| 273 |
+
|
| 274 |
+
### 5.1 Markup and DOM handles
|
| 275 |
+
|
| 276 |
+
`frontend/mission.html` is 297 lines. Its `<head>` carries the live API base:
|
| 277 |
+
|
| 278 |
+
```html
|
| 279 |
+
<meta name="satquery-api-base" content="https://satquery-backend-m4yv.onrender.com">
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
That meta tag is the second entry in the API-base resolution order (see §5.4). The page loads three
|
| 283 |
+
scripts, in order:
|
| 284 |
+
|
| 285 |
+
```html
|
| 286 |
+
<script src="assets/js/core.js"></script>
|
| 287 |
+
<script src="assets/js/live.js"></script>
|
| 288 |
+
<script src="assets/js/mission.js"></script>
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
`core.js` defines the `SQ` namespace and the mock driver; `live.js` defines the real HTTP client;
|
| 292 |
+
`mission.js` is the page driver that decides which of the two to use. The load order is significant:
|
| 293 |
+
`mission.js` runs last because it consumes both.
|
| 294 |
+
|
| 295 |
+
The console's handles, by region:
|
| 296 |
+
|
| 297 |
+
**Query and run.**
|
| 298 |
+
|
| 299 |
+
| Handle | Role |
|
| 300 |
+
|---|---|
|
| 301 |
+
| `#qtext` | The natural-language query input. |
|
| 302 |
+
| `#btnRun` | The Run button. |
|
| 303 |
+
| `#runid` | Displays the run identifier for the current run. |
|
| 304 |
+
|
| 305 |
+
**Observation / upload.**
|
| 306 |
+
|
| 307 |
+
| Handle | Role |
|
| 308 |
+
|---|---|
|
| 309 |
+
| `#obsTail` | The observation status tail. Its **initial text content is `none`** — i.e. no asset loaded yet. |
|
| 310 |
+
| `#dropZone` | The drop target for a file. |
|
| 311 |
+
| `#fileInput` | The primary file input (the single observation). |
|
| 312 |
+
| `#obsNote` | The note under the observation widget. |
|
| 313 |
+
|
| 314 |
+
**Metadata.**
|
| 315 |
+
|
| 316 |
+
| Handle | Role |
|
| 317 |
+
|---|---|
|
| 318 |
+
| `#metaHost` | Container for the metadata readout. |
|
| 319 |
+
| `#mFile` | Metadata: file name. |
|
| 320 |
+
| `#mAcq` | Metadata: acquisition date (one of the `#m*` fields inside `#metaHost`). |
|
| 321 |
+
| `#metaEmpty` | The empty-state placeholder for the metadata block. |
|
| 322 |
+
|
| 323 |
+
**Intent panel.**
|
| 324 |
+
|
| 325 |
+
| Handle | Role |
|
| 326 |
+
|---|---|
|
| 327 |
+
| `#intentTail` | Intent status tail. |
|
| 328 |
+
| `#intentHost` | Container for the parsed intent (task, assets, route). |
|
| 329 |
+
| `#pairTail` | Pair status tail (for the two-asset change tasks). |
|
| 330 |
+
| `#pairNote` | Note under the pair widget. |
|
| 331 |
+
| `#fileInputT0` | The **second** file input — the `t0` (before) image for paired tasks. |
|
| 332 |
+
|
| 333 |
+
**Viewer.**
|
| 334 |
+
|
| 335 |
+
| Handle | Role |
|
| 336 |
+
|---|---|
|
| 337 |
+
| `#vbtns` | Viewer mode buttons. |
|
| 338 |
+
| `#viewerState` | Viewer state label. |
|
| 339 |
+
| `#plate` | The plate container. |
|
| 340 |
+
| `#plateImg` | The plate image; its `src` is `assets/img/eo/reservoir-low.jpg`. |
|
| 341 |
+
| `#ev` | The evidence overlay layer on the plate. |
|
| 342 |
+
| `#evNote` | Note under the evidence overlay. |
|
| 343 |
+
| `#plateCreditLead` | Plate credit lead-in text. |
|
| 344 |
+
| `#plateCredit` | Plate credit text. |
|
| 345 |
+
|
| 346 |
+
**Comparison (paired tasks).**
|
| 347 |
+
|
| 348 |
+
| Handle | Role |
|
| 349 |
+
|---|---|
|
| 350 |
+
| `#cmpWrap` | Comparison wrapper. |
|
| 351 |
+
| `#cmpT0` | The t0 pane. |
|
| 352 |
+
| `#cmpT1` | The t1 pane. |
|
| 353 |
+
| `#cmpRange` | The comparison range/slider control. |
|
| 354 |
+
| `#cmpCredit` | Comparison credit. |
|
| 355 |
+
| `#cmpEmpty` | Comparison empty state. |
|
| 356 |
+
|
| 357 |
+
**Answer.**
|
| 358 |
+
|
| 359 |
+
| Handle | Role |
|
| 360 |
+
|---|---|
|
| 361 |
+
| `#answerHost` | Container for the rendered answer. |
|
| 362 |
+
| `#ansTail` | Answer status tail. |
|
| 363 |
+
|
| 364 |
+
**Evidence.**
|
| 365 |
+
|
| 366 |
+
| Handle | Role |
|
| 367 |
+
|---|---|
|
| 368 |
+
| `#evHost` | Container for the evidence list. |
|
| 369 |
+
| `#evEmpty` | Evidence empty state. |
|
| 370 |
+
| `#evTail` | Evidence status tail. |
|
| 371 |
+
|
| 372 |
+
**Confidence.**
|
| 373 |
+
|
| 374 |
+
| Handle | Role |
|
| 375 |
+
|---|---|
|
| 376 |
+
| `#confHost` | Container for the confidence readout. |
|
| 377 |
+
| `#confC` | The confidence value. |
|
| 378 |
+
| `#confNote` | Note under the confidence value. |
|
| 379 |
+
|
| 380 |
+
**Provenance.**
|
| 381 |
+
|
| 382 |
+
| Handle | Role |
|
| 383 |
+
|---|---|
|
| 384 |
+
| `#provHost` | Container for provenance. |
|
| 385 |
+
| `#pRun` | Provenance: run id. |
|
| 386 |
+
| `#pPolicy` | Provenance: policy. |
|
| 387 |
+
| `#pProtocol` | Provenance: protocol. |
|
| 388 |
+
| `#pSchema` | Provenance: schema version. |
|
| 389 |
+
|
| 390 |
+
**Report and trace.**
|
| 391 |
+
|
| 392 |
+
| Handle | Role |
|
| 393 |
+
|---|---|
|
| 394 |
+
| `#btnReport` | The report button. |
|
| 395 |
+
| `#ctrace` | The trace container. |
|
| 396 |
+
| `#traceNow` | The "now" label on the trace bar. |
|
| 397 |
+
| `#trace` | The trace bar (the element whose width is animated). |
|
| 398 |
+
| `#traceNote` | Note under the trace bar. |
|
| 399 |
+
|
| 400 |
+
**Event drawer.**
|
| 401 |
+
|
| 402 |
+
| Handle | Role |
|
| 403 |
+
|---|---|
|
| 404 |
+
| `#drawer` | The event-log drawer. |
|
| 405 |
+
| `#evlog` | The event log list. |
|
| 406 |
+
| `#btnClose` | Close-drawer button. |
|
| 407 |
+
| `#btnEvents` | Open-drawer button. |
|
| 408 |
+
|
| 409 |
+
### 5.2 The intent panel
|
| 410 |
+
|
| 411 |
+
The intent panel (`#intentHost`, `#intentTail`) is rendered by `renderIntent()` in
|
| 412 |
+
`frontend/assets/js/mission.js`. It shows the *interpreted* query: which task the router chose, which
|
| 413 |
+
assets the task requires, and which route (live vs mock) will be taken.
|
| 414 |
+
|
| 415 |
+
The interpretation itself is `interpret()` in `mission.js` — a lexical router that runs in the
|
| 416 |
+
browser. Its notable features, as read from the file:
|
| 417 |
+
|
| 418 |
+
- a change stem `/chang/` (no `\b` word boundary) so "change"/"changed"/"changes" all match;
|
| 419 |
+
- a `newAsChange` rule so phrasing like "new …" can be read as a change request;
|
| 420 |
+
- a caption regex for caption/describe phrasings.
|
| 421 |
+
|
| 422 |
+
`interpret()` is deliberately simple and deterministic. It exists so the console can show the user a
|
| 423 |
+
*reason* for the task it is about to run, and so the console can decide which file inputs are
|
| 424 |
+
relevant. It is **not** the server-side router: the server has its own deterministic policy planner
|
| 425 |
+
(see the `SERVING.md` chapter and `core/controller.py`). The browser-side `interpret()` is a UI
|
| 426 |
+
affordance; the authoritative routing decision is the server's, and the console renders what the
|
| 427 |
+
server returns.
|
| 428 |
+
|
| 429 |
+
### 5.3 Task selection and asset requirements
|
| 430 |
+
|
| 431 |
+
`mission.js` maps the interpreted intent onto a server task name via `ROUTE_TASK_TO_SERVER`. The
|
| 432 |
+
paired tasks are declared in `PAIRED_TASKS`:
|
| 433 |
+
|
| 434 |
+
```js
|
| 435 |
+
PAIRED_TASKS = { change, change_vqa, optical_sar }
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
These three tasks need **two** assets (a before/after pair), which is why the console has a second
|
| 439 |
+
file input (`#fileInputT0`) and a comparison region (`#cmpWrap`). When a paired task is selected but
|
| 440 |
+
only one asset is available, the console falls back to a single-asset task via
|
| 441 |
+
`SINGLE_ASSET_FALLBACK = 'vqa'`. This is a UI-level fallback: rather than failing the run, the
|
| 442 |
+
console narrows the request to something one image can answer.
|
| 443 |
+
|
| 444 |
+
For optical-SAR there is a dedicated precondition check, `validateOpticalSar()`, because that task
|
| 445 |
+
has modality-specific requirements. `assetsForTask()` assembles the asset list the chosen task needs.
|
| 446 |
+
|
| 447 |
+
### 5.4 API-base resolution
|
| 448 |
+
|
| 449 |
+
`frontend/assets/js/live.js` defines the resolution order for the API base in
|
| 450 |
+
`SQ.live.baseUrl()`:
|
| 451 |
+
|
| 452 |
+
1. `window.SATQUERY_API_BASE` (a runtime override, useful for testing), then
|
| 453 |
+
2. `<meta name="satquery-api-base">` (the page's declared base — on `mission.html` this is
|
| 454 |
+
`https://satquery-backend-m4yv.onrender.com`), then
|
| 455 |
+
3. the default `/api` (a same-origin path).
|
| 456 |
+
|
| 457 |
+
`_normalizeBase()` normalises trailing slashes, and `SQ.live.url()` composes the final URL.
|
| 458 |
+
`SQ.ENDPOINTS` names the four endpoints the client talks to:
|
| 459 |
+
|
| 460 |
+
```js
|
| 461 |
+
SQ.ENDPOINTS = { assets: '/assets', infer: '/infer', capabilities: '/capabilities', health: '/health' }
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
With the default `/api` base these resolve to `/api/assets`, `/api/infer`, `/api/capabilities`, and
|
| 465 |
+
`/api/health`. On the deployed configuration the base is the Render orchestrator host, which is the
|
| 466 |
+
`/api/*` mirror of the four-endpoint contract (see the `SERVING.md` chapter).
|
| 467 |
+
|
| 468 |
+
### 5.5 Upload widgets
|
| 469 |
+
|
| 470 |
+
Two file inputs exist: `#fileInput` (primary) and `#fileInputT0` (the before image for paired
|
| 471 |
+
tasks). Both are wired through `handleFile()` in `mission.js`, and both feed
|
| 472 |
+
`SQ.live.uploadAsset()` in `live.js`.
|
| 473 |
+
|
| 474 |
+
`live.js` declares the accepted content types:
|
| 475 |
+
|
| 476 |
+
```js
|
| 477 |
+
SQ.CONTENT_TYPES = { tif, tiff, png, jpg, jpeg }
|
| 478 |
+
```
|
| 479 |
+
|
| 480 |
+
and maps a file to its MIME type via `SQ.contentTypeFor()`. The upload is a **raw-bytes POST with a
|
| 481 |
+
`Content-Type` header** — not a multipart form. This mirrors the server contract: `POST /v1/assets`
|
| 482 |
+
takes the file as the request body with its content type in the header, and `POST /v1/analyze` takes
|
| 483 |
+
JSON (multipart is explicitly *not* implemented — see `docs/API_CONTRACT.md` §2.4 and the `SERVING.md`
|
| 484 |
+
chapter).
|
| 485 |
+
|
| 486 |
+
`uploadAsset()` asserts that the response contains an `asset_id`; `uploadAssets()` uploads a list
|
| 487 |
+
**sequentially** (so the second upload cannot race the first). The returned `asset_id` is an opaque
|
| 488 |
+
handle — the client never parses it, it only passes it back. The asset store's TTL and the fact that
|
| 489 |
+
handles are ephemeral are documented in `SERVING.md`.
|
| 490 |
+
|
| 491 |
+
### 5.6 The observation tail: `none` → ready
|
| 492 |
+
|
| 493 |
+
`#obsTail` starts with text content `none`. When an asset is uploaded successfully, the tail is
|
| 494 |
+
updated to a ready state. This is the console's way of making the *precondition* for a run visible:
|
| 495 |
+
a query can be typed at any time, but a run that requires an asset cannot produce evidence until an
|
| 496 |
+
asset is present. The `#obsNote` field carries the supporting note.
|
| 497 |
+
|
| 498 |
+
### 5.7 The Run button, `#runid`, and `#answerHost`
|
| 499 |
+
|
| 500 |
+
Pressing `#btnRun` calls `runQuery()` in `mission.js`. `runQuery()` decides between the two drivers
|
| 501 |
+
(§6) and then dispatches. `#runid` is populated with the run identifier the service returns
|
| 502 |
+
(`run_…`); `#answerHost` receives the rendered answer.
|
| 503 |
+
|
| 504 |
+
### 5.8 The trace bar and the eight events
|
| 505 |
+
|
| 506 |
+
The console's most load-bearing UI element is the trace bar. It is driven entirely by the eight
|
| 507 |
+
execution events.
|
| 508 |
+
|
| 509 |
+
**The eight event names** are declared once, in `frontend/assets/js/core.js`:
|
| 510 |
+
|
| 511 |
+
```js
|
| 512 |
+
SQ.EVENT_NAMES = [
|
| 513 |
+
'QUERY_RECEIVED',
|
| 514 |
+
'QUERY_UNDERSTOOD',
|
| 515 |
+
'ROUTE_SELECTED',
|
| 516 |
+
'SPECIALIST_STARTED',
|
| 517 |
+
'SPECIALIST_COMPLETED',
|
| 518 |
+
'EVIDENCE_GENERATED',
|
| 519 |
+
'CONFIDENCE_COMPUTED',
|
| 520 |
+
'RESULT_ASSEMBLED'
|
| 521 |
+
]
|
| 522 |
+
```
|
| 523 |
+
|
| 524 |
+
(Declared at `core.js:616–620`.) These names are the protocol between any driver and the UI. The
|
| 525 |
+
`architecture.html` footer's disclosure — that its transmission is driven by the mock event stream —
|
| 526 |
+
is a statement about *which driver* feeds these names, not about the names themselves.
|
| 527 |
+
|
| 528 |
+
**The nine UI states.** `mission.js` declares `STATES` (nine `ControllerState` values) and maps each
|
| 529 |
+
event to a state via `EVENT_TO_STATE`, with per-state explanatory text in `STATE_NOTE`. Nine states
|
| 530 |
+
over eight events is not an inconsistency: there is a state for "idle / not started" plus the eight
|
| 531 |
+
event-driven states.
|
| 532 |
+
|
| 533 |
+
**The fill formula.** `markState()` sets the trace bar width with:
|
| 534 |
+
|
| 535 |
+
```js
|
| 536 |
+
traceFill.style.width = ((traceProgress + 0.5) / STATES.length) * 100 + '%'
|
| 537 |
+
```
|
| 538 |
+
|
| 539 |
+
With eight events completed against nine states, the final fill is
|
| 540 |
+
`((8 + 0.5) / 9) × 100` = **94.4444 %**. This is why the delivery documents record the trace fill as
|
| 541 |
+
94.4444 %: it is the arithmetic consequence of the formula, not a measurement of a rendering. The
|
| 542 |
+
`+ 0.5` means the bar advances *half a step* on entry to each state, so a completed eight-event run
|
| 543 |
+
lands at 8.5/9 rather than 8/9 or 9/9. The remaining 5.5556 % corresponds to the ninth state, which
|
| 544 |
+
a completed run does not enter.
|
| 545 |
+
|
| 546 |
+
`buildTrace()` constructs the trace bar's segments; `logEvent()` appends to the event log
|
| 547 |
+
(`#evlog`); `resetUI()` clears the console back to its initial state (including resetting `#obsTail`
|
| 548 |
+
to `none`).
|
| 549 |
+
|
| 550 |
+
### 5.9 The event drawer
|
| 551 |
+
|
| 552 |
+
`#drawer` is the event log, opened by `#btnEvents` and closed by `#btnClose`. `#evlog` is the list
|
| 553 |
+
itself. Each event appended by `logEvent()` records the event type and its payload summary, so a
|
| 554 |
+
reader can see the full ordered sequence rather than only the current state. The drawer is what makes
|
| 555 |
+
the "0 mock nodes" / "9 preview nodes" distinction auditable by a human: the live driver's log
|
| 556 |
+
contains no mock nodes; the preview driver's log contains nine.
|
| 557 |
+
|
| 558 |
+
---
|
| 559 |
+
|
| 560 |
+
## 6. REAL vs PREVIEW: two drivers, one event seam
|
| 561 |
+
|
| 562 |
+
`mission.js` opens with the comment "TWO DRIVERS, ONE EVENT SEAM". That is the whole design: two
|
| 563 |
+
driver implementations, one `ingest()` seam, one UI.
|
| 564 |
+
|
| 565 |
+
### 6.1 The seam
|
| 566 |
+
|
| 567 |
+
`SQ.run(opts)` in `core.js` owns an `ingest()` switch (lines ~742–788) that dispatches each of the
|
| 568 |
+
eight event types to the UI handlers. Any driver that wants to drive the console calls
|
| 569 |
+
`ingest(type, payload)`; it does not touch the DOM. The console boot sequence builds the engine with
|
| 570 |
+
`engine = SQ.run(...)`, then calls `runMock(QUERY)` to paint an initial state, then
|
| 571 |
+
`loadCapabilities()` to fetch the service's capability block.
|
| 572 |
+
|
| 573 |
+
### 6.2 PREVIEW (`runMock`)
|
| 574 |
+
|
| 575 |
+
`runMock()` is the **preview** driver. Its properties, as read from `mission.js` and `core.js`:
|
| 576 |
+
|
| 577 |
+
- It emits **empty payloads** — the payloads carry the shape of the data but not real values, because
|
| 578 |
+
there is no real run behind it.
|
| 579 |
+
- It labels the console as a preview (`is-mock`).
|
| 580 |
+
- It emits **nine mock nodes** — the event log for a preview run contains nine mock nodes.
|
| 581 |
+
- It drives the trace bar through the same `markState()` path, so the fill arithmetic is identical.
|
| 582 |
+
|
| 583 |
+
`core.js`'s `startMock()` drives the sequence with `setTimeout` timings, so the preview is *animated*:
|
| 584 |
+
each event arrives after a short delay, which is what makes the trace bar and the event drawer move.
|
| 585 |
+
|
| 586 |
+
**What preview does not emit.** The preview driver emits **no specialist events** — i.e. no
|
| 587 |
+
`SPECIALIST_STARTED` / `SPECIALIST_COMPLETED` for a real specialist. This is the honest distinction
|
| 588 |
+
between the two paths: the preview can show the *envelope* of a run, but it cannot show a specialist
|
| 589 |
+
that actually ran, because no specialist ran.
|
| 590 |
+
|
| 591 |
+
### 6.3 REAL (`runLive`)
|
| 592 |
+
|
| 593 |
+
`runLive()` is the **live** driver. Its properties:
|
| 594 |
+
|
| 595 |
+
- It makes **real HTTP calls** via `SQ.live` (`live.js`).
|
| 596 |
+
- It sets a `liveRun` flag.
|
| 597 |
+
- It reads two response headers from `SQ.live.infer()`: `X-SatQuery-State` and
|
| 598 |
+
`x-satquery-transport`. The state header carries the controller's state (see the nine
|
| 599 |
+
`ControllerState` values); the transport header records how the response was carried (the tunnel
|
| 600 |
+
transport vs a direct/forwarded transport).
|
| 601 |
+
- It translates failures with `translateError()` and, for upload/inference failures,
|
| 602 |
+
`SQ.live.describeFailure()` / `LiveError` in `live.js`.
|
| 603 |
+
- A live run shows **0 mock nodes** — the event log contains no mock nodes at all.
|
| 604 |
+
|
| 605 |
+
### 6.4 Why the 0-vs-9 distinction is the honesty test
|
| 606 |
+
|
| 607 |
+
The delivery documents record that live validation produced **24 runs** (3 passes × 8 cases, 8/8 each)
|
| 608 |
+
with **0 mock nodes**. That number is only meaningful because the preview path *does* produce mock
|
| 609 |
+
nodes (nine of them). The console's event drawer therefore lets a reader distinguish, from the UI
|
| 610 |
+
alone, whether what they are looking at is a real run or a preview. This is the frontend's
|
| 611 |
+
contribution to the project's truthfulness discipline: the same eight-event vocabulary is used for
|
| 612 |
+
both, and the drawer is what tells them apart.
|
| 613 |
+
|
| 614 |
+
### 6.5 `loadCapabilities()` and `setMode()`
|
| 615 |
+
|
| 616 |
+
`loadCapabilities()` calls `SQ.live.capabilities()` (i.e. `GET /api/capabilities` on the deployed
|
| 617 |
+
base) and renders the capability block. `setMode()` switches the console between modes. Because
|
| 618 |
+
capabilities are fetched live, the console can show which tasks are available *right now* on the
|
| 619 |
+
deployed service — which matters because the deployed device is CPU and because some capabilities
|
| 620 |
+
are gated on artifacts that may be absent (the `SERVING.md` chapter documents the capability adapter
|
| 621 |
+
and the five-word vocabulary it emits).
|
| 622 |
+
|
| 623 |
+
### 6.6 The test hook
|
| 624 |
+
|
| 625 |
+
`mission.js` exposes `window.SQ_MISSION` as a test hook. It lets an automated harness drive the
|
| 626 |
+
console (select a task, inject a file, press run) without synthesising DOM events. This is how the
|
| 627 |
+
live validation runs in the delivery documents were executed against the page.
|
| 628 |
+
|
| 629 |
+
### 6.7 `translateError()`
|
| 630 |
+
|
| 631 |
+
`translateError()` maps a service error into human-readable text in the console. It is the frontend
|
| 632 |
+
half of the error contract: the service returns a machine code and an HTTP status
|
| 633 |
+
(`docs/API_CONTRACT.md` §5.1–§5.3; `gateway/policy.py` `_CODE_STATUS`), and the console turns that
|
| 634 |
+
into a sentence a person can act on. The console does not invent codes; it renders the ones it
|
| 635 |
+
receives. One consequence worth stating: a `422` from the service is *not* necessarily a validation
|
| 636 |
+
failure of the user's data — see the G-1 annotation-scope defect in the `SERVING.md` chapter, where a
|
| 637 |
+
`422 {"detail":[{"loc":["query","request"]}]}` is a *server-side* bug that masquerades as a client
|
| 638 |
+
validation error. `translateError()` will render it as an error; only the backend fix removes it.
|
| 639 |
+
|
| 640 |
+
---
|
| 641 |
+
|
| 642 |
+
## 7. The captured-run page: "Anatomy of a Run" (`run.html`)
|
| 643 |
+
|
| 644 |
+
`frontend/run.html` renders a **real captured** `ResultEnvelope`. This is the page that lets a reader
|
| 645 |
+
inspect an actual run without running anything.
|
| 646 |
+
|
| 647 |
+
### 7.1 The captured envelope
|
| 648 |
+
|
| 649 |
+
The data lives in `frontend/assets/data/anatomy-run.js` (329 lines), assigned to
|
| 650 |
+
`window.SATQUERY_ANATOMY_RUN`. Its header states the provenance:
|
| 651 |
+
|
| 652 |
+
- `_source`: "Captured live 2026-09-25 … Sanitized".
|
| 653 |
+
|
| 654 |
+
The fields that matter, all read from the file:
|
| 655 |
+
|
| 656 |
+
| Field | Value |
|
| 657 |
+
|---|---|
|
| 658 |
+
| `run_id` | `run_d124d8b9adea` |
|
| 659 |
+
| `task` | `grounding` |
|
| 660 |
+
| `query` | "Where is the reservoir?" |
|
| 661 |
+
| `answer` | "[grounding] Located 3 candidate region(s) … Highest objectness 0.61." |
|
| 662 |
+
| `config_hash` | `78f1e3700da15aa1` |
|
| 663 |
+
| `transport` | `tunnel` |
|
| 664 |
+
| `intent.source` | `forced` |
|
| 665 |
+
| plan | `step_001` grounding, `requires_assets` |
|
| 666 |
+
| steps | 8 steps, `RECEIVE` → `RESPOND` |
|
| 667 |
+
| `selected_models` | ViT-B-32 (RemoteCLIP path) → GroundingHead (`params=1052677`) |
|
| 668 |
+
| evidence | 4 items: 3 `bounding_box` + 1 `statistic` |
|
| 669 |
+
| regions | 3 (`region_1cd3973de749`, …) |
|
| 670 |
+
| confidence | raw `0.5231253252136926` / calibrated `0.5236623182649384` (`temperature_scaling`) |
|
| 671 |
+
| calibration component | `temperature: 0.9772731820958189`, `calibration_samples: 16441.0` |
|
| 672 |
+
| timings | `step_001: 209.873` |
|
| 673 |
+
| geospatial | 730×730, `has_crs false` |
|
| 674 |
+
| warnings | 2 — no CRS; contradictory spatial claims |
|
| 675 |
+
|
| 676 |
+
### 7.2 How the page renders it
|
| 677 |
+
|
| 678 |
+
`frontend/assets/js/run.js` (380 lines) is the driver. It reads `window.SATQUERY_ANATOMY_RUN` and
|
| 679 |
+
exposes the envelope through a set of named views: `QUERY`, `TASK`, `RUN_ID`, `MODELS`, `EVIDENCE`,
|
| 680 |
+
`CONF`, `TIMINGS`, `GEO`, `INTENT`, `PLAN`, `HASH`, `PLATE`.
|
| 681 |
+
|
| 682 |
+
- `REGIONS` is built from `A.regions`, so the three captured regions drive the plate overlays.
|
| 683 |
+
- `paintAll()` loads the **real plate image** and clears the `t0` and `diff` layers (this run has no
|
| 684 |
+
before/after pair, so those layers are empty rather than faked).
|
| 685 |
+
- `buildEvidence()` renders the four evidence items; `evCandidates()`, `evLock()`, and
|
| 686 |
+
`evConfirmed()` render the three stages of the evidence story (candidates → locked → confirmed).
|
| 687 |
+
- `SPECIALISTS_FOR_TASK` maps the task to the specialists that would run, so the page can show the
|
| 688 |
+
specialist panel even though this run's only specialist is grounding.
|
| 689 |
+
- `buildLattice()` builds the step lattice from the 8 captured steps.
|
| 690 |
+
- `DATA` is a table of eight key/value views, one per stage, and **each entry names the event** that
|
| 691 |
+
corresponds to that stage — i.e. the captured page is wired to the same eight-event vocabulary.
|
| 692 |
+
- `setStage()`, `resetEvidence()`, and `gotoStep()` drive the page as the reader scrolls or uses the
|
| 693 |
+
keyboard.
|
| 694 |
+
|
| 695 |
+
### 7.3 What the page proves, and what it does not
|
| 696 |
+
|
| 697 |
+
It **proves**: a real grounding run was captured, sanitized, and shipped with its full envelope —
|
| 698 |
+
run id, task, query, answer, config hash, transport, intent source, plan, steps, selected models with
|
| 699 |
+
parameter counts, evidence with types, regions, raw and calibrated confidence with the calibration
|
| 700 |
+
component and sample count, timings, geospatial facts, and warnings. A reader can verify that the
|
| 701 |
+
number shown as "confidence" on the page is a *calibrated* value with a documented temperature and a
|
| 702 |
+
documented calibration-sample count.
|
| 703 |
+
|
| 704 |
+
It **does not prove**: any system-level accuracy. One captured run is one run. Per
|
| 705 |
+
`release/DOCS_STYLE_GUIDE.md` §3 there is **no end-to-end benchmark**, and this page does not create
|
| 706 |
+
one. The calibrated confidence `0.5236623182649384` is a per-run confidence, not an accuracy.
|
| 707 |
+
|
| 708 |
+
The two captured warnings are also part of the honest record: `has_crs false` (the imagery had no
|
| 709 |
+
coordinate reference system) and "contradictory spatial claims". Both are shown rather than
|
| 710 |
+
suppressed.
|
| 711 |
+
|
| 712 |
+
---
|
| 713 |
+
|
| 714 |
+
## 8. Benchmark, Research, and Lab pages
|
| 715 |
+
|
| 716 |
+
The frontend has a measurement-facing tier whose job is to present numbers *with their status*.
|
| 717 |
+
|
| 718 |
+
- **`benchmark.html`** carries the evidence-state legend: **VERIFIED / SUPPORTED / UNVERIFIED /
|
| 719 |
+
BLOCKED / NOT RUN**. It notes the measured reliability curve. This page is where the per-artifact
|
| 720 |
+
results live, and per the style guide each must be stated with its correct qualification:
|
| 721 |
+
grounding under **two protocols** (canonical 0.2838 / matched6 0.2566) and **two decode variants**
|
| 722 |
+
(head_argmax 0.1215, zero-shot 0.0972) — never one alone; optical-SAR accuracy **0.931 with
|
| 723 |
+
macro-F1 0.434161**, ruling **OPEN**; change-VQA **two** test sets (test 0.697626/0.378373 and
|
| 724 |
+
test2 0.651469/0.372309), ruling **OPEN**; router **0.965116 = validation, ungated, n = 86**, test
|
| 725 |
+
split **NOT RUN**; the VLM adapter **usable** (exact_match 0.963) but **ACCEPTANCE-REJECTED**.
|
| 726 |
+
- **`research.html`** carries the method and caveat material, including the calibration result stated
|
| 727 |
+
correctly: ECE **0.013755 → 0.014929 — worse**.
|
| 728 |
+
- **The Lab page.** The brief for this chapter names a "Lab" page. `frontend/HANDOFF.md` §12 gives the
|
| 729 |
+
file map, and the eleven shipped pages are enumerated in §3 above. A page named "Lab" is **not**
|
| 730 |
+
among the eleven HTML files read for this chapter. The nearest things are the Analyze console
|
| 731 |
+
(`mission.html`) and the captured-run page (`run.html`), which are the pages where a reader can
|
| 732 |
+
"do" or "inspect" work. Whether a page named "Lab" existed at any point and was renamed or dropped
|
| 733 |
+
is `UNKNOWN — not established from the available evidence`.
|
| 734 |
+
|
| 735 |
+
---
|
| 736 |
+
|
| 737 |
+
## 9. The Hugging Face header link (delivery requirement)
|
| 738 |
+
|
| 739 |
+
Stated separately because it is a delivery requirement with a verification method. See §3.2: the
|
| 740 |
+
Hugging Face link `https://huggingface.co/thundercode/SatQuery` and the GitHub link
|
| 741 |
+
`https://github.com/Anish-lab-blip/SatQuery-AI` are present in the navigation of **all eleven**
|
| 742 |
+
pages, verified by searching every `frontend/*.html` for both hostnames.
|
| 743 |
+
|
| 744 |
+
---
|
| 745 |
+
|
| 746 |
+
## 10. Cache-busting behaviour
|
| 747 |
+
|
| 748 |
+
The frontend uses **URL-versioned assets** plus **header rules** to control caching. The header rules
|
| 749 |
+
live in `frontend/_headers` (a Cloudflare Pages file), and the versioning is visible in the markup.
|
| 750 |
+
|
| 751 |
+
### 10.1 The `_headers` rules
|
| 752 |
+
|
| 753 |
+
`frontend/_headers` declares:
|
| 754 |
+
|
| 755 |
+
| Path pattern | Rule |
|
| 756 |
+
|---|---|
|
| 757 |
+
| `/*` | Baseline security headers: `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`, `Cross-Origin-Opener-Policy`. |
|
| 758 |
+
| `/` and `/*.html` | Revalidate. |
|
| 759 |
+
| `/assets/video/*` | `max-age=604800` (7 days). |
|
| 760 |
+
| `/assets/fonts/*` | `max-age=31536000` (1 year). |
|
| 761 |
+
| `/assets/css/*` | `public, max-age=0, must-revalidate`. |
|
| 762 |
+
| `/assets/js/*` | `public, max-age=0, must-revalidate`. |
|
| 763 |
+
| `/assets/img/*` | `max-age=604800` (7 days). |
|
| 764 |
+
|
| 765 |
+
### 10.2 The rule that matters for correctness
|
| 766 |
+
|
| 767 |
+
**JS and CSS are served `public, max-age=0, must-revalidate`.** That is the safe setting for code:
|
| 768 |
+
the browser may cache a copy but must revalidate before using it. This is what makes a code change
|
| 769 |
+
take effect without asking users to hard-refresh. Images, fonts, and video get long lifetimes because
|
| 770 |
+
they are content, not logic — and because a *changed* image is given a **new URL** rather than
|
| 771 |
+
overwriting the old one.
|
| 772 |
+
|
| 773 |
+
### 10.3 The delta-growth pair as the worked example
|
| 774 |
+
|
| 775 |
+
`frontend/_headers` itself carries a note that the delta-growth image pair was given **new URLs**
|
| 776 |
+
when it changed (`delta-growth-t0-720` / `delta-growth-t1-720`, referenced from `index.html`). That is
|
| 777 |
+
the correct pattern for a long-cached asset: change the URL, keep the long `max-age`. The homepage's
|
| 778 |
+
wipe slider uses that pair, and the "evidence" section uses `delta-growth-t2-2075.jpg` — a third URL
|
| 779 |
+
in the same family.
|
| 780 |
+
|
| 781 |
+
---
|
| 782 |
+
|
| 783 |
+
## 11. Cloudflare platform traps
|
| 784 |
+
|
| 785 |
+
Two Cloudflare behaviours shape this frontend. Both are recorded because a future maintainer will hit
|
| 786 |
+
them.
|
| 787 |
+
|
| 788 |
+
### 11.1 `_headers` rules CONCATENATE (they do not override)
|
| 789 |
+
|
| 790 |
+
This is the single most surprising Cloudflare Pages behaviour in this project, and
|
| 791 |
+
`frontend/_headers` documents it **verbatim in a comment inside the file**. The rule is: when more
|
| 792 |
+
than one `_headers` rule matches a path, Cloudflare **concatenates** the header values rather than
|
| 793 |
+
letting the more specific rule override the more general one.
|
| 794 |
+
|
| 795 |
+
The practical consequence: if two rules both set `Cache-Control`, the client receives **two**
|
| 796 |
+
`Cache-Control` values. Chromium honours the **first** `max-age` it sees. So a broad rule that sets
|
| 797 |
+
`max-age=0` and a specific rule that sets `max-age=604800` do not "resolve" to the specific one — the
|
| 798 |
+
client sees both, in order, and takes the first.
|
| 799 |
+
|
| 800 |
+
`docs/FINAL_DELIVERY_TODO.md` §1.7 lists this as known blocker item 9. The mitigation, as evidenced
|
| 801 |
+
by the shipped `_headers`, is to **scope the patterns so that they do not overlap** where the value
|
| 802 |
+
must be exact — i.e. write one rule per asset tree rather than a general rule plus an override. The
|
| 803 |
+
`/assets/js/*` and `/assets/css/*` rules are separate from `/assets/img/*` precisely so that each
|
| 804 |
+
tree has exactly one matching rule and there is nothing to concatenate.
|
| 805 |
+
|
| 806 |
+
### 11.2 The 308 `.html` → extensionless redirect
|
| 807 |
+
|
| 808 |
+
Cloudflare Pages issues a **308** redirect from a path that ends in `.html` to the extensionless
|
| 809 |
+
path: a request for `/run.html` redirects to `/run`. A 308 preserves the method (unlike 301/302 in
|
| 810 |
+
some clients), so a `POST` is not silently turned into a `GET`, but the redirect still happens and the
|
| 811 |
+
final URL differs from the requested one.
|
| 812 |
+
|
| 813 |
+
The second, related trap is the **trailing-slash 307**: Starlette's `redirect_slashes` behaviour
|
| 814 |
+
issues a **307** when a request's trailing slash does not match the route. This is documented for the
|
| 815 |
+
*API* in `docs/API_CONTRACT.md` §5.1 as a footgun, and it matters to the frontend because the
|
| 816 |
+
frontend is the caller: `SQ.live.url()` and `_normalizeBase()` exist partly to make the client's URL
|
| 817 |
+
composition predictable so that the client is not relying on a redirect to reach an endpoint.
|
| 818 |
+
|
| 819 |
+
Both traps share a lesson: **the frontend must link to the canonical URL.** A page that links to
|
| 820 |
+
`/run` (extensionless) never triggers the 308; a page that links to `/run.html` does.
|
| 821 |
+
|
| 822 |
+
---
|
| 823 |
+
|
| 824 |
+
## 12. Accessibility and UX caveats
|
| 825 |
+
|
| 826 |
+
This section states what can be established from the files read, and marks the rest.
|
| 827 |
+
|
| 828 |
+
### 12.1 What is established
|
| 829 |
+
|
| 830 |
+
- **Keyboard driving exists on the captured-run page.** `run.js` supports keyboard input to move
|
| 831 |
+
between steps (`gotoStep()` plus key handling), so `run.html` is operable without a mouse.
|
| 832 |
+
- **Reduced-motion and focus styling** are governed by the token system in `frontend/HANDOFF.md` §2
|
| 833 |
+
(CSS custom properties on `:root`). The handoff document is the design authority for the token
|
| 834 |
+
layer.
|
| 835 |
+
- **The event drawer is a named, focusable pair of controls** (`#btnEvents` / `#btnClose`) with a
|
| 836 |
+
labelled region (`#drawer` → `#evlog`), so the event log is not hover-only.
|
| 837 |
+
- **The upload widgets are real `<input type="file">` elements** (`#fileInput`, `#fileInputT0`),
|
| 838 |
+
which are natively keyboard- and screen-reader-operable, and they are paired with a `#dropZone`
|
| 839 |
+
for pointer drag-and-drop. Drag-and-drop is an *addition* to the file input, not a replacement.
|
| 840 |
+
|
| 841 |
+
### 12.2 What is not established
|
| 842 |
+
|
| 843 |
+
- **A formal accessibility audit** (axe / Lighthouse / WCAG conformance level) has not been
|
| 844 |
+
performed: `UNKNOWN — not established from the available evidence`.
|
| 845 |
+
- **Contrast ratios** for the token palette: `UNKNOWN — not established from the available evidence`.
|
| 846 |
+
- **Screen-reader behaviour** of the trace bar's animated width (whether a live region announces each
|
| 847 |
+
state transition): `UNKNOWN — not established from the available evidence`. The trace bar is a
|
| 848 |
+
visual affordance driven by `markState()`; whether its state changes are announced is not
|
| 849 |
+
determinable from the code read.
|
| 850 |
+
- **Mobile/responsive breakpoints** beyond what the CSS declares: `UNKNOWN — not established from the
|
| 851 |
+
available evidence`.
|
| 852 |
+
- **The 404 page's page count** is stale: `404.html` says "Ten pages exist" and links five, while
|
| 853 |
+
eleven ship. This is drift, recorded here and not silently repaired.
|
| 854 |
+
|
| 855 |
+
---
|
| 856 |
+
|
| 857 |
+
## 13. Documentation drift recorded (not propagated as current truth)
|
| 858 |
+
|
| 859 |
+
Per the project's practice (mirrored from `P10-T02`), drift found during this chapter's research is
|
| 860 |
+
recorded honestly rather than smoothed over:
|
| 861 |
+
|
| 862 |
+
| Location | Stale claim | Correct current statement |
|
| 863 |
+
|---|---|---|
|
| 864 |
+
| `frontend/_headers` comment | "100% static, zero network calls" | Ten of eleven pages are hermetic; `mission.html` calls the live service. |
|
| 865 |
+
| `frontend/404.html` prose | "Ten pages exist" (links five) | Eleven pages ship. |
|
| 866 |
+
| `frontend/mission.html` / `_headers` relationship | (implicit) | The live API base is declared in `<meta name="satquery-api-base">`, which is a *live* dependency the hermeticity audit must be read as exempting. |
|
| 867 |
+
|
| 868 |
+
None of these is a code defect; each is a documentation statement inside a shipped file that no
|
| 869 |
+
longer matches the tree. They are listed so a reader is not misled by them.
|
| 870 |
+
|
| 871 |
+
---
|
| 872 |
+
|
| 873 |
+
## 14. What the frontend does NOT do
|
| 874 |
+
|
| 875 |
+
Stated explicitly, because the depth of §5–§7 could otherwise imply more capability than exists:
|
| 876 |
+
|
| 877 |
+
- **No framework and no build step for application code.** Pages are hand-written HTML plus ES
|
| 878 |
+
modules; `scripts/stage_pages.mjs` copies, it does not compile.
|
| 879 |
+
- **No client-side model inference.** The browser never runs a model. All inference happens on the
|
| 880 |
+
service (`POST /api/infer` → the tunnel → the inference service).
|
| 881 |
+
- **No multipart upload.** Uploads are raw-bytes POSTs with a `Content-Type` header, matching the
|
| 882 |
+
server contract (`docs/API_CONTRACT.md` §2.4: multipart is *not* implemented).
|
| 883 |
+
- **No streaming.** There is no server-sent-events or websocket channel. The eight events are
|
| 884 |
+
*client-side UI states*; on a live run they are derived from the single inference response (plus
|
| 885 |
+
the two response headers `X-SatQuery-State` and `x-satquery-transport`), not pushed from the
|
| 886 |
+
server. (See the `SERVING.md` chapter: the service does not stream.)
|
| 887 |
+
- **No authentication UI.** The service has no auth (`docs/API_CONTRACT.md` §7), so there is no login.
|
| 888 |
+
- **No persistence of runs.** Nothing in the frontend stores a run; the console's state is in-memory,
|
| 889 |
+
and the captured-run page reads a static data file.
|
| 890 |
+
- **No offline mode** beyond the fact that ten pages need no network.
|
| 891 |
+
|
| 892 |
+
---
|
| 893 |
+
|
| 894 |
+
## 4.3 The staging tool in detail: closure algorithm and report format
|
| 895 |
+
|
| 896 |
+
This subsection expands §4.1 because the staging tool is the *only* build-like step in the frontend
|
| 897 |
+
and its behaviour determines what ships.
|
| 898 |
+
|
| 899 |
+
### 4.3.1 Why a closure walk instead of "copy the directory"
|
| 900 |
+
|
| 901 |
+
Copying `frontend/` wholesale would ship unreferenced assets: draft images, superseded JS, experiment
|
| 902 |
+
files. A closure walk ships exactly the transitive set of files reachable from the eleven seed pages.
|
| 903 |
+
The consequences are worth stating precisely:
|
| 904 |
+
|
| 905 |
+
- **Adding a page is a deliberate act.** Because the seed set is `frontend/*.html` (top level only),
|
| 906 |
+
a page placed in a subdirectory is *not* a seed. It ships only if a seed page references it.
|
| 907 |
+
- **Removing a reference removes a file from the deploy.** If the last page that used
|
| 908 |
+
`assets/img/eo/old.jpg` stops referencing it, that image silently stops shipping. This is a feature
|
| 909 |
+
(smaller tree) and a hazard (an asset can disappear without an error) — which is exactly why the
|
| 910 |
+
tool prints the staged tree and the integrity report, so the disappearance is visible in the build
|
| 911 |
+
log rather than only in production.
|
| 912 |
+
- **Query strings and hashes are normalised away.** `stripQueryHash()` means `app.js?v=3` and
|
| 913 |
+
`app.js` are the same edge, so versioned references do not create phantom files.
|
| 914 |
+
- **External URLs are not followed.** `isExternal()` stops the walk at `https://…` and `//…`, which
|
| 915 |
+
is why the external-dependency audit can report `HERMETIC`: any external URL that *was* followed
|
| 916 |
+
would show up as a network dependency.
|
| 917 |
+
- **References cannot escape the root.** `insideFrontend()` rejects a resolved path that leaves
|
| 918 |
+
`frontend/`, so a stray `../../secret` reference cannot pull a file from outside the tree.
|
| 919 |
+
|
| 920 |
+
### 4.3.2 The four report blocks
|
| 921 |
+
|
| 922 |
+
The tool prints four blocks. Reading them in order answers the four questions a deployer has.
|
| 923 |
+
|
| 924 |
+
1. `=== STAGED TREE ===` — *what will be uploaded?* A listing of every file copied into the output
|
| 925 |
+
root, with sizes. Files over `BIG_WARN_BYTES` (10 MiB) are flagged.
|
| 926 |
+
2. `=== REFERENCE INTEGRITY ===` — *is the closure complete?* The staged tree is re-walked and every
|
| 927 |
+
reference must resolve inside it. A dangling reference fails with exit code 3. This is the check
|
| 928 |
+
that catches the case where a file was referenced but not copied (e.g. because of a
|
| 929 |
+
case-sensitivity difference between the developer's filesystem and Linux).
|
| 930 |
+
3. `=== EXTERNAL DEPENDENCY AUDIT ===` — *is the site hermetic?* External URLs found in the closure
|
| 931 |
+
are listed. When the list is empty the block prints `HERMETIC`. This is the check that keeps the
|
| 932 |
+
"ten of eleven pages are hermetic" claim honest: if a page gained a CDN script, the audit would
|
| 933 |
+
stop printing `HERMETIC`.
|
| 934 |
+
4. `=== OPTIONS APPLIED ===` — *what flags were used?* The effective options, so a build log is
|
| 935 |
+
self-describing.
|
| 936 |
+
|
| 937 |
+
### 4.3.3 The two hard gates and their exit codes
|
| 938 |
+
|
| 939 |
+
| Condition | Exit code | Why it is fatal |
|
| 940 |
+
|---|---|---|
|
| 941 |
+
| Any file exceeds `PAGES_FILE_LIMIT` (25 MiB) | **2** | Cloudflare Pages rejects a file over the limit; deploying would fail *after* upload. Failing before upload is cheaper and clearer. |
|
| 942 |
+
| Staged tree fails reference-integrity re-walk | **3** | A dangling reference means a broken page in production. |
|
| 943 |
+
|
| 944 |
+
The deliberate design choice is **fail before upload**. Both gates run locally, on the staged tree,
|
| 945 |
+
before `wrangler` is invoked. A non-zero exit stops a shell pipeline (`&&`) before the deploy command
|
| 946 |
+
can run.
|
| 947 |
+
|
| 948 |
+
### 4.3.4 The deploy hint
|
| 949 |
+
|
| 950 |
+
The last thing the tool prints is the command to run:
|
| 951 |
+
|
| 952 |
+
```
|
| 953 |
+
npx wrangler pages deploy "<OUT_ROOT>" --project-name <name>
|
| 954 |
+
```
|
| 955 |
+
|
| 956 |
+
Note that the tool does **not** run the deploy itself. Staging and deploying are separate steps, which
|
| 957 |
+
means a human (or CI) can inspect the staged tree between them. This is consistent with the project's
|
| 958 |
+
general posture: make the artifact inspectable before it is published.
|
| 959 |
+
|
| 960 |
+
---
|
| 961 |
+
|
| 962 |
+
## 5.10 The nine console states
|
| 963 |
+
|
| 964 |
+
`mission.js` declares nine `ControllerState` values in `STATES`, an `EVENT_TO_STATE` map from the
|
| 965 |
+
eight event names onto those states, and a `STATE_NOTE` table of human-readable text per state.
|
| 966 |
+
`markState()` is the single function that advances the UI from one state to the next, and it is the
|
| 967 |
+
only place the trace-bar width is written.
|
| 968 |
+
|
| 969 |
+
The relationship between the nine states and the eight events is:
|
| 970 |
+
|
| 971 |
+
- One state is the **idle / pre-run** state — the state the console is in before `QUERY_RECEIVED`.
|
| 972 |
+
`resetUI()` returns the console to it (and resets `#obsTail` to `none`).
|
| 973 |
+
- The other eight states are entered by the eight events, in order. `EVENT_TO_STATE` is the mapping,
|
| 974 |
+
so the console's state names and the protocol's event names are kept in one place rather than
|
| 975 |
+
duplicated across `if` branches.
|
| 976 |
+
|
| 977 |
+
`STATE_NOTE` gives each state a sentence, which is what `#traceNow` and `#traceNote` display while the
|
| 978 |
+
run progresses. The point of the separate state text is that the event name is protocol
|
| 979 |
+
(`SPECIALIST_STARTED`) while the state text is human ("running the grounding specialist"). The console
|
| 980 |
+
shows both: the event name in the drawer's log, the state text in the trace region.
|
| 981 |
+
|
| 982 |
+
### 5.10.1 Why the fill formula uses `(traceProgress + 0.5) / STATES.length`
|
| 983 |
+
|
| 984 |
+
The formula is:
|
| 985 |
+
|
| 986 |
+
```js
|
| 987 |
+
traceFill.style.width = ((traceProgress + 0.5) / STATES.length) * 100 + '%'
|
| 988 |
+
```
|
| 989 |
+
|
| 990 |
+
Three observations about it:
|
| 991 |
+
|
| 992 |
+
1. **`STATES.length` is 9, not 8.** The denominator is the number of states, which includes the idle
|
| 993 |
+
state. So the maximum reachable fill from events alone is `(8 + 0.5) / 9 = 94.4444 %`.
|
| 994 |
+
2. **The `+ 0.5` is a half-step lead.** Entering state *n* shows the bar at *(n + 0.5)/9*, i.e. the
|
| 995 |
+
midpoint of that state's band. The bar therefore never sits exactly on a boundary, which reads
|
| 996 |
+
better visually and means the bar is always "inside" a labelled state.
|
| 997 |
+
3. **The remaining 5.5556 % is the idle state's band.** A completed run does not enter idle, so a
|
| 998 |
+
completed run does not fill the bar. This is the arithmetic origin of the 94.4444 % figure the
|
| 999 |
+
delivery documents record.
|
| 1000 |
+
|
| 1001 |
+
Stated as a status: the **formula** is `IMPLEMENTED`; the 94.4444 % figure is `MEASURED` *as the
|
| 1002 |
+
arithmetic consequence of the formula against nine states*, and it is corroborated by the live
|
| 1003 |
+
validation runs recorded in the delivery documents. It is not a claim about anything else.
|
| 1004 |
+
|
| 1005 |
+
### 5.10.2 `onEvent()` — the single funnel
|
| 1006 |
+
|
| 1007 |
+
`onEvent()` is the console's event handler: every event delivered through the `ingest()` seam passes
|
| 1008 |
+
through it. It is responsible for
|
| 1009 |
+
|
| 1010 |
+
- appending to the event log via `logEvent()` (which writes to `#evlog` in the drawer),
|
| 1011 |
+
- advancing the state via `markState()` (which writes the trace bar),
|
| 1012 |
+
- routing the payload to the appropriate renderer (`renderEvidence()`, `renderConfidence()`,
|
| 1013 |
+
`renderIntent()`, the answer renderer into `#answerHost`, and the provenance writers into
|
| 1014 |
+
`#pRun` / `#pPolicy` / `#pProtocol` / `#pSchema`).
|
| 1015 |
+
|
| 1016 |
+
Having a single funnel is what makes the REAL/PREVIEW distinction safe: both drivers call the same
|
| 1017 |
+
`onEvent()`, so the rendering path is identical and only the *payload source* differs. It is also why
|
| 1018 |
+
the "0 mock nodes vs 9 mock nodes" property is checkable at one place — the drawer's contents are
|
| 1019 |
+
produced by one function.
|
| 1020 |
+
|
| 1021 |
+
---
|
| 1022 |
+
|
| 1023 |
+
## 5.11 The viewer and comparison regions
|
| 1024 |
+
|
| 1025 |
+
### 5.11.1 The viewer
|
| 1026 |
+
|
| 1027 |
+
The viewer is the plate at the top of the console's results area. Handles: `#vbtns` (mode buttons),
|
| 1028 |
+
`#viewerState` (state label), `#plate` (container), `#plateImg` (the image, `src` initially
|
| 1029 |
+
`assets/img/eo/reservoir-low.jpg`), `#ev` (evidence overlay), `#evNote` (overlay note),
|
| 1030 |
+
`#plateCreditLead` and `#plateCredit` (attribution).
|
| 1031 |
+
|
| 1032 |
+
The initial `src` is a **real EO image** (`reservoir-low.jpg`), not a synthetic one. That matters for
|
| 1033 |
+
honesty: before any run, the console shows a real image with a credit, so a visitor is never looking
|
| 1034 |
+
at invented imagery while the console is idle.
|
| 1035 |
+
|
| 1036 |
+
`#vbtns` selects a viewer mode and `#viewerState` names it. The evidence overlay `#ev` is where the
|
| 1037 |
+
grounding result's regions are drawn — for the captured grounding run there are three regions
|
| 1038 |
+
(`region_1cd3973de749`, …), which is why the overlay is a layer separate from the plate image rather
|
| 1039 |
+
than something painted into the image.
|
| 1040 |
+
|
| 1041 |
+
### 5.11.2 The comparison region
|
| 1042 |
+
|
| 1043 |
+
For paired tasks (`change`, `change_vqa`, `optical_sar`), the console shows a comparison region:
|
| 1044 |
+
`#cmpWrap` (wrapper), `#cmpT0` (before pane), `#cmpT1` (after pane), `#cmpRange` (the range/slider
|
| 1045 |
+
control), `#cmpCredit` (attribution), `#cmpEmpty` (empty state).
|
| 1046 |
+
|
| 1047 |
+
The two panes are fed from the two upload widgets (`#fileInput` for t1, `#fileInputT0` for t0). When
|
| 1048 |
+
only one asset is available for a paired task, `SINGLE_ASSET_FALLBACK = 'vqa'` narrows the request so
|
| 1049 |
+
the console can still produce an answer instead of failing. `#cmpEmpty` is the state shown when there
|
| 1050 |
+
is nothing to compare.
|
| 1051 |
+
|
| 1052 |
+
The homepage uses the same visual idiom for its delta-growth wipe slider, which is a nice consistency:
|
| 1053 |
+
the *idea* of "two dates, one place, slide to compare" appears both as a landing-page illustration and
|
| 1054 |
+
as a functional control in the console.
|
| 1055 |
+
|
| 1056 |
+
---
|
| 1057 |
+
|
| 1058 |
+
## 5.12 Provenance and report controls
|
| 1059 |
+
|
| 1060 |
+
### 5.12.1 The provenance block
|
| 1061 |
+
|
| 1062 |
+
`#provHost` contains four fields that together answer "what exactly produced this?":
|
| 1063 |
+
|
| 1064 |
+
| Handle | Field | Meaning |
|
| 1065 |
+
|---|---|---|
|
| 1066 |
+
| `#pRun` | run id | The `run_…` identifier. |
|
| 1067 |
+
| `#pPolicy` | policy | The routing/planning policy that produced the plan. |
|
| 1068 |
+
| `#pProtocol` | protocol | The protocol under which the result was produced. |
|
| 1069 |
+
| `#pSchema` | schema | The schema version of the response. |
|
| 1070 |
+
|
| 1071 |
+
The reason a provenance block is worth four fields: the project's measurement discipline depends on
|
| 1072 |
+
being able to say *which* protocol a number came from. The style guide's grounding rule — that
|
| 1073 |
+
grounding was measured under **two protocols** (canonical 0.2838 / matched6 0.2566) and **two decode
|
| 1074 |
+
variants** (head_argmax 0.1215, zero-shot 0.0972) — is exactly the kind of fact that a protocol field
|
| 1075 |
+
exists to disambiguate. A result rendered without its protocol is a result that cannot be compared to
|
| 1076 |
+
anything.
|
| 1077 |
+
|
| 1078 |
+
### 5.12.2 The report button and the event drawer
|
| 1079 |
+
|
| 1080 |
+
`#btnReport` triggers the console's report action. `#btnEvents` opens `#drawer`, whose `#evlog`
|
| 1081 |
+
contains the ordered event log; `#btnClose` closes it. The drawer is the console's audit surface: it
|
| 1082 |
+
is the one place where a reader can count events and check for mock nodes.
|
| 1083 |
+
|
| 1084 |
+
---
|
| 1085 |
+
|
| 1086 |
+
## 5.13 The mock data model inside `core.js`
|
| 1087 |
+
|
| 1088 |
+
`core.js` (1029 lines) is not only the event seam; it is also the source of everything the preview
|
| 1089 |
+
driver draws. Its internals, as read:
|
| 1090 |
+
|
| 1091 |
+
**Utilities.** `SQ.util` provides `rnd` (random), `rng` (a seeded random-number generator — which is
|
| 1092 |
+
what makes the preview *deterministic* across reloads), `pad`, and `ms` (formatting).
|
| 1093 |
+
|
| 1094 |
+
**Raster synthesis.** The synthetic imagery path:
|
| 1095 |
+
|
| 1096 |
+
| Symbol | Role |
|
| 1097 |
+
|---|---|
|
| 1098 |
+
| `BIOMES` | The biome definitions the synthesised terrain is drawn from. |
|
| 1099 |
+
| `CANON` | `{w: 900, h: 600}` — the canonical raster size. |
|
| 1100 |
+
| `buildMasks()` | Builds the masks (land/water/etc.) the raster is composed from. |
|
| 1101 |
+
| `fbm` | Fractal Brownian motion — the noise function that gives the terrain texture. |
|
| 1102 |
+
| `SQ.scene` | Produces a scene; it sets a **`synthetic: true` flag** and fills placeholder `gsd`, `aoi`, and `dates`. |
|
| 1103 |
+
|
| 1104 |
+
The `synthetic: true` flag is the load-bearing honesty mechanism: synthetic imagery is *labelled* as
|
| 1105 |
+
synthetic in the data, so any renderer can disclose it. The placeholder `gsd` (ground sample
|
| 1106 |
+
distance), `aoi` (area of interest), and `dates` are placeholders, not measurements — and the flag
|
| 1107 |
+
says so.
|
| 1108 |
+
|
| 1109 |
+
**Imagery.** `SQ.imagery` resolves which image to show.
|
| 1110 |
+
|
| 1111 |
+
**Stages.** `SQ.STAGES` is the eight-stage list from `QUERY` through `ANSWER`. This is the *narrative*
|
| 1112 |
+
stage list (what a human sees), distinct from the eight *event* names (the protocol). The two are
|
| 1113 |
+
aligned but not identical: `SQ.STAGES` is the visual progression; `SQ.EVENT_NAMES` is the wire
|
| 1114 |
+
vocabulary.
|
| 1115 |
+
|
| 1116 |
+
**The deterministic policy.** `SQ.policy()` is a deterministic router used by the preview. Its
|
| 1117 |
+
documented quirks: a **`where`-first** fix (a query containing "where" is routed to grounding before
|
| 1118 |
+
other rules are considered), the removal of a `built` keyword, and the `newAsChange` rule. Because it
|
| 1119 |
+
is deterministic and seeded, the same query produces the same preview every time — which is what makes
|
| 1120 |
+
the preview useful as a UI demo and useless as a measurement.
|
| 1121 |
+
|
| 1122 |
+
**Answer material.** `SQ.ANSWER_BANK` supplies canned answers for the preview; `SQ.COMPONENTS` lists
|
| 1123 |
+
**seven components** with their model strings, which is what the preview's model panel shows.
|
| 1124 |
+
|
| 1125 |
+
**Shared components.** `SQ.frame`, `SQ.reliabilityPlot`, and `SQ.chip` are reusable renderers. The
|
| 1126 |
+
`SQ.reliabilityPlot` is the component that draws the reliability curve referenced on
|
| 1127 |
+
`benchmark.html`.
|
| 1128 |
+
|
| 1129 |
+
**The run engine.** `SQ.run(opts)` is the engine; its `ingest()` switch (lines ~742–788) dispatches
|
| 1130 |
+
the eight event types to handlers. `startMock()` drives a preview run by calling `ingest()` on a
|
| 1131 |
+
schedule of `setTimeout` delays.
|
| 1132 |
+
|
| 1133 |
+
> **Honesty note on `SQ.policy()` vs the server router.** The browser's `SQ.policy()` and
|
| 1134 |
+
> `mission.js`'s `interpret()` are **UI-side** interpretations. The authoritative router is
|
| 1135 |
+
> server-side (`core/controller.py`, `core/registry.py`). The preview's routing can therefore differ
|
| 1136 |
+
> from what the server would do, and that is acceptable precisely because the preview is labelled a
|
| 1137 |
+
> preview and emits no specialist events. A reader must not read `SQ.policy()` as the routing
|
| 1138 |
+
> specification.
|
| 1139 |
+
|
| 1140 |
+
---
|
| 1141 |
+
|
| 1142 |
+
## 6.8 `live.js` API surface reference
|
| 1143 |
+
|
| 1144 |
+
`frontend/assets/js/live.js` is 392 lines. Its header documents the end-to-end flow and **three
|
| 1145 |
+
design rules**. The module's public surface, as read:
|
| 1146 |
+
|
| 1147 |
+
| Symbol | Kind | Behaviour |
|
| 1148 |
+
|---|---|---|
|
| 1149 |
+
| `SQ.ENDPOINTS` | const | `{ assets: '/assets', infer: '/infer', capabilities: '/capabilities', health: '/health' }`. |
|
| 1150 |
+
| `SQ.CONTENT_TYPES` | const | `{ tif, tiff, png, jpg, jpeg }` — the accepted upload types. |
|
| 1151 |
+
| `SQ.contentTypeFor(file)` | fn | Maps a file to its MIME type; used to set the upload `Content-Type`. |
|
| 1152 |
+
| `SQ.live.baseUrl()` | fn | Resolution order: `window.SATQUERY_API_BASE` → `<meta name="satquery-api-base">` ��� `/api`. |
|
| 1153 |
+
| `_normalizeBase(base)` | fn (internal) | Normalises the base (trailing slashes). |
|
| 1154 |
+
| `SQ.live.url(endpoint)` | fn | Composes the final URL from the normalised base and an endpoint. |
|
| 1155 |
+
| `LiveError` | class | The client's error type, carrying enough detail for `describeFailure()`. |
|
| 1156 |
+
| `describeFailure(err)` | fn | Turns a `LiveError` into human-readable text. |
|
| 1157 |
+
| `SQ.live.uploadAsset(file)` | fn | Raw-bytes POST with `Content-Type`; **asserts** the response contains `asset_id`. |
|
| 1158 |
+
| `SQ.live.uploadAssets(files)` | fn | Uploads a list **sequentially** (no parallel uploads). |
|
| 1159 |
+
| `SQ.live.infer(request)` | fn | POSTs the analysis request; **reads `X-SatQuery-State` and `x-satquery-transport`** from the response. |
|
| 1160 |
+
| `SQ.live.run(opts)` | fn | Composes upload + infer into one run. |
|
| 1161 |
+
| `SQ.live.capabilities()` | fn | `GET` the capability block (used by `loadCapabilities()`). |
|
| 1162 |
+
|
| 1163 |
+
### 6.8.1 The three design rules (as stated in the file's header)
|
| 1164 |
+
|
| 1165 |
+
The file's header states three rules that govern the client. They are worth restating because they
|
| 1166 |
+
explain several behaviours that would otherwise look arbitrary:
|
| 1167 |
+
|
| 1168 |
+
1. **Raw bytes, not multipart.** The upload is a body-with-content-type POST because that is what the
|
| 1169 |
+
service accepts (`docs/API_CONTRACT.md` §2.4: multipart is *not* implemented). A client that sent
|
| 1170 |
+
multipart would be rejected.
|
| 1171 |
+
2. **Sequential uploads.** `uploadAssets()` uploads one at a time because the server's asset store is
|
| 1172 |
+
a small ephemeral store with a file cap (`SERVING.md`: default `_asset_max_files()` = 32), and
|
| 1173 |
+
because a paired task's second upload depends on the first succeeding. Parallel uploads would make
|
| 1174 |
+
partial failure harder to reason about.
|
| 1175 |
+
3. **The asset handle is opaque.** The client asserts the handle exists and passes it back
|
| 1176 |
+
unexamined. The handle's shape (`asset_<32 hex>`) and its TTL are server facts; the client must not
|
| 1177 |
+
depend on either.
|
| 1178 |
+
|
| 1179 |
+
### 6.8.2 The two response headers
|
| 1180 |
+
|
| 1181 |
+
`SQ.live.infer()` reads two custom headers:
|
| 1182 |
+
|
| 1183 |
+
| Header | Meaning |
|
| 1184 |
+
|---|---|
|
| 1185 |
+
| `X-SatQuery-State` | The controller state for the response (the same vocabulary as the console's `STATES`). |
|
| 1186 |
+
| `x-satquery-transport` | How the response was carried (the captured envelope records `transport: "tunnel"`). |
|
| 1187 |
+
|
| 1188 |
+
These two headers are how the console can display a state and a transport *without* a streaming
|
| 1189 |
+
channel. They are the reason the console can show a live run's progress truthfully: the state and the
|
| 1190 |
+
transport come from the server's own response, not from a client-side guess.
|
| 1191 |
+
|
| 1192 |
+
---
|
| 1193 |
+
|
| 1194 |
+
## 7.4 The captured envelope, field by field
|
| 1195 |
+
|
| 1196 |
+
This subsection expands §7.1 into a complete inventory, because the captured envelope is the frontend's
|
| 1197 |
+
single richest piece of real data and a reader should be able to reconstruct it.
|
| 1198 |
+
|
| 1199 |
+
**Provenance and identity.**
|
| 1200 |
+
|
| 1201 |
+
| Field | Value | Note |
|
| 1202 |
+
|---|---|---|
|
| 1203 |
+
| `_source` | "Captured live 2026-09-25 … Sanitized" | The capture date and the fact that the payload was sanitized before shipping. |
|
| 1204 |
+
| `run_id` | `run_d124d8b9adea` | The run identifier. |
|
| 1205 |
+
| `config_hash` | `78f1e3700da15aa1` | The frozen config hash — the same value recorded in the style guide §3. |
|
| 1206 |
+
|
| 1207 |
+
**Request.**
|
| 1208 |
+
|
| 1209 |
+
| Field | Value |
|
| 1210 |
+
|---|---|
|
| 1211 |
+
| `query` | "Where is the reservoir?" |
|
| 1212 |
+
| `task` | `grounding` |
|
| 1213 |
+
| `intent.source` | `forced` (the task was forced rather than inferred). |
|
| 1214 |
+
| `transport` | `tunnel` |
|
| 1215 |
+
|
| 1216 |
+
**Plan.**
|
| 1217 |
+
|
| 1218 |
+
| Field | Value |
|
| 1219 |
+
|---|---|
|
| 1220 |
+
| plan | `step_001`, task `grounding`, `requires_assets` |
|
| 1221 |
+
| steps | 8 steps, `RECEIVE` → `RESPOND` |
|
| 1222 |
+
| `timings.step_001` | `209.873` (ms) |
|
| 1223 |
+
|
| 1224 |
+
**Models.**
|
| 1225 |
+
|
| 1226 |
+
| Field | Value |
|
| 1227 |
+
|---|---|
|
| 1228 |
+
| `selected_models` | ViT-B-32 (the RemoteCLIP path) → GroundingHead |
|
| 1229 |
+
| GroundingHead `params` | `1052677` |
|
| 1230 |
+
|
| 1231 |
+
**Result.**
|
| 1232 |
+
|
| 1233 |
+
| Field | Value |
|
| 1234 |
+
|---|---|
|
| 1235 |
+
| `answer` | "[grounding] Located 3 candidate region(s) … Highest objectness 0.61." |
|
| 1236 |
+
| evidence | 4 items: 3 × `bounding_box`, 1 × `statistic` |
|
| 1237 |
+
| regions | 3, e.g. `region_1cd3973de749` |
|
| 1238 |
+
|
| 1239 |
+
**Confidence.**
|
| 1240 |
+
|
| 1241 |
+
| Field | Value |
|
| 1242 |
+
|---|---|
|
| 1243 |
+
| raw | `0.5231253252136926` |
|
| 1244 |
+
| calibrated | `0.5236623182649384` |
|
| 1245 |
+
| method | `temperature_scaling` |
|
| 1246 |
+
| `temperature` | `0.9772731820958189` |
|
| 1247 |
+
| `calibration_samples` | `16441.0` |
|
| 1248 |
+
|
| 1249 |
+
The `calibration_samples` value `16441.0` is the size of the validation set the temperature was fitted
|
| 1250 |
+
on; `docs/API_CONTRACT.md` §4 records the same figure as 16,441 Val rows. The temperature
|
| 1251 |
+
`0.9772731820958189` is also recorded in `docs/API_CONTRACT.md` §4. This is a real cross-check: the
|
| 1252 |
+
number on the public page matches the number in the API contract.
|
| 1253 |
+
|
| 1254 |
+
**Geospatial and warnings.**
|
| 1255 |
+
|
| 1256 |
+
| Field | Value |
|
| 1257 |
+
|---|---|
|
| 1258 |
+
| geospatial | 730 × 730, `has_crs false` |
|
| 1259 |
+
| warnings | 2 — no CRS; contradictory spatial claims |
|
| 1260 |
+
|
| 1261 |
+
The presence of the warnings in the shipped envelope is itself a design statement: the capture was not
|
| 1262 |
+
cleaned up to look better than it was.
|
| 1263 |
+
|
| 1264 |
+
**Why this page is important to the release.** It is the one place where a reader can see a complete,
|
| 1265 |
+
real, sanitized result envelope — including its imperfections — rendered by the same event vocabulary
|
| 1266 |
+
the live console uses. It is a *sample of one*, and the page does not present it as more than that.
|
| 1267 |
+
|
| 1268 |
+
---
|
| 1269 |
+
|
| 1270 |
+
## 11.3 A worked path through the platform traps
|
| 1271 |
+
|
| 1272 |
+
The following Mermaid diagram shows where the two traps (§11.1, §11.2) bite. It is a description of
|
| 1273 |
+
the behaviours documented in `frontend/_headers`, `docs/API_CONTRACT.md` §5.1, and the Cloudflare
|
| 1274 |
+
redirect behaviour recorded in the delivery documents — not a measurement.
|
| 1275 |
+
|
| 1276 |
+
```mermaid
|
| 1277 |
+
flowchart TD
|
| 1278 |
+
A["Browser requests /run.html"] --> B{"Cloudflare Pages"}
|
| 1279 |
+
B -->|"308 (method preserved)"| C["/run"]
|
| 1280 |
+
C --> D["run.html served from the staged tree"]
|
| 1281 |
+
|
| 1282 |
+
E["Browser loads the page"] --> F{"Assets referenced"}
|
| 1283 |
+
F -->|"/assets/js/run.js"| G["Rule: /assets/js/*"]
|
| 1284 |
+
F -->|"/assets/img/…"| H["Rule: /assets/img/*"]
|
| 1285 |
+
G --> I["Cache-Control: public, max-age=0, must-revalidate"]
|
| 1286 |
+
H --> J["Cache-Control: max-age=604800"]
|
| 1287 |
+
|
| 1288 |
+
K["If two rules matched one path"] --> L["Values CONCATENATE"]
|
| 1289 |
+
L --> M["Chromium honours the FIRST max-age"]
|
| 1290 |
+
M --> N["Mitigation: scope patterns so they do not overlap"]
|
| 1291 |
+
```
|
| 1292 |
+
|
| 1293 |
+
Read together, the traps say: **link to the canonical extensionless URL** (so the 308 never fires for
|
| 1294 |
+
an internal navigation) and **give each asset tree exactly one matching `_headers` rule** (so there is
|
| 1295 |
+
nothing to concatenate).
|
| 1296 |
+
|
| 1297 |
+
The third, API-side trap — the Starlette trailing-slash **307** documented in `docs/API_CONTRACT.md`
|
| 1298 |
+
§5.1 — is the client's concern rather than the static tier's: `_normalizeBase()` and `SQ.live.url()`
|
| 1299 |
+
exist so the client composes a URL that matches the route exactly, rather than relying on a redirect
|
| 1300 |
+
to reach it.
|
| 1301 |
+
|
| 1302 |
+
---
|
| 1303 |
+
|
| 1304 |
+
## 14.1 What the frontend is a client *of*
|
| 1305 |
+
|
| 1306 |
+
Because this chapter documents a client, it is worth stating precisely what contract the client is
|
| 1307 |
+
written against, so a reader can follow the thread into the `SERVING.md` chapter.
|
| 1308 |
+
|
| 1309 |
+
- The client posts **raw bytes** to `/api/assets` and receives an opaque `asset_id`
|
| 1310 |
+
(`docs/API_CONTRACT.md` §2.5).
|
| 1311 |
+
- The client posts **JSON** to `/api/infer` (`docs/API_CONTRACT.md` §2.4) and receives a
|
| 1312 |
+
`ResultEnvelope`.
|
| 1313 |
+
- The client reads `GET /api/capabilities` (`docs/API_CONTRACT.md` §2.2) to know which tasks are
|
| 1314 |
+
available now.
|
| 1315 |
+
- The client may read `GET /api/health` (`docs/API_CONTRACT.md` §2.1) for the service's health block.
|
| 1316 |
+
- The client reads two custom response headers (`X-SatQuery-State`, `x-satquery-transport`).
|
| 1317 |
+
- The client renders errors from the service's machine codes (`docs/API_CONTRACT.md` §5.2 — a 23-code
|
| 1318 |
+
taxonomy in `core/errors.py`, plus the gateway-origin `rate_limited`, mapped by
|
| 1319 |
+
`gateway/policy.py` `_CODE_STATUS`).
|
| 1320 |
+
- The client sends **no credentials**; the service has no auth (`docs/API_CONTRACT.md` §7). CORS is
|
| 1321 |
+
configured on the orchestrator (`deploy/render/main.py` `_PRODUCTION_ORIGINS` includes the Pages
|
| 1322 |
+
origin).
|
| 1323 |
+
|
| 1324 |
+
Every one of those six interactions is documented from the *server* side in the `SERVING.md` chapter,
|
| 1325 |
+
which is the other half of this pair.
|
| 1326 |
+
|
| 1327 |
+
---
|
| 1328 |
+
|
| 1329 |
+
## 15. Status summary
|
| 1330 |
+
|
| 1331 |
+
| Subsystem | Status |
|
| 1332 |
+
|---|---|
|
| 1333 |
+
| Static tier (11 pages, CSS, JS modules, assets) | `IMPLEMENTED` |
|
| 1334 |
+
| Staging tool `scripts/stage_pages.mjs` (closure, size gate, integrity gate, hermeticity report) | `IMPLEMENTED` |
|
| 1335 |
+
| Deploy path (`wrangler pages deploy` of the staged tree) | `IMPLEMENTED`; deployed HEAD `2d7ae53b482d` |
|
| 1336 |
+
| Analyze console (`mission.html` + `mission.js` + `live.js` + `core.js`) | `IMPLEMENTED` |
|
| 1337 |
+
| Eight-event protocol + trace bar (94.4444 % fill) | `IMPLEMENTED`; fill `MEASURED` as the arithmetic consequence of the formula |
|
| 1338 |
+
| PREVIEW driver (`runMock`, 9 mock nodes, no specialist events) | `IMPLEMENTED` |
|
| 1339 |
+
| REAL driver (`runLive`, real HTTP, 0 mock nodes) | `IMPLEMENTED`; exercised in the 24-run live validation recorded in the delivery docs |
|
| 1340 |
+
| Captured-run page (`run.html` over `anatomy-run.js`) | `IMPLEMENTED`; data is a real sanitized capture (`run_d124d8b9adea`) |
|
| 1341 |
+
| Hugging Face + GitHub header links on all 11 pages | `VERIFIED` by search across `frontend/*.html` |
|
| 1342 |
+
| Cache-busting (`_headers` rules + URL versioning) | `IMPLEMENTED` |
|
| 1343 |
+
| `_headers` concatenation trap | `KNOWN` (blocker item 9 in `docs/FINAL_DELIVERY_TODO.md` §1.7); mitigated by non-overlapping patterns |
|
| 1344 |
+
| Accessibility audit | `NOT RUN` |
|
| 1345 |
+
|
| 1346 |
+
---
|
| 1347 |
+
|
| 1348 |
+
## 16. NOT RUN / OPEN / BLOCKED (frontend)
|
| 1349 |
+
|
| 1350 |
+
Per `release/DOCS_STYLE_GUIDE.md` §4, every doc ends with this list.
|
| 1351 |
+
|
| 1352 |
+
**NOT RUN**
|
| 1353 |
+
- No formal accessibility audit (axe / Lighthouse / WCAG conformance level).
|
| 1354 |
+
- No measured contrast-ratio audit of the token palette.
|
| 1355 |
+
- No screen-reader behaviour verification for the trace bar's state transitions.
|
| 1356 |
+
- No responsive-breakpoint verification beyond the CSS as written.
|
| 1357 |
+
- No end-to-end benchmark of the system (this is project-wide, per `release/DOCS_STYLE_GUIDE.md`
|
| 1358 |
+
§3 — it is *not* a frontend gap, it is a project-level fact that the frontend must not contradict).
|
| 1359 |
+
|
| 1360 |
+
**OPEN**
|
| 1361 |
+
- `frontend/404.html` prose says "Ten pages exist" while eleven ship — documentation drift, OPEN.
|
| 1362 |
+
- The `_headers` concatenation behaviour remains a known platform trap (blocker item 9); the shipped
|
| 1363 |
+
rules avoid overlap, but the underlying platform behaviour is unchanged and OPEN as a hazard.
|
| 1364 |
+
- A page named "Lab" is not among the eleven shipped pages; whether it existed is
|
| 1365 |
+
`UNKNOWN — not established from the available evidence`.
|
| 1366 |
+
- Accessibility conformance level: `UNKNOWN — not established from the available evidence`.
|
| 1367 |
+
|
| 1368 |
+
**BLOCKED**
|
| 1369 |
+
- Nothing in the frontend is blocked. The frontend's live path depends on the backend, and the
|
| 1370 |
+
backend's own blockers (e.g. B-07, tunnel gaps; patch prepared, NOT deployed) are recorded in the
|
| 1371 |
+
`SERVING.md` chapter and the delivery documents. A backend blocker surfaces in the console only as
|
| 1372 |
+
an error rendered by `translateError()`.
|
| 1373 |
+
|
| 1374 |
+
---
|
| 1375 |
+
|
| 1376 |
+
## 17. Where the evidence lives
|
| 1377 |
+
|
| 1378 |
+
| Claim area | Evidence file(s) |
|
| 1379 |
+
|---|---|
|
| 1380 |
+
| Page inventory, purposes, `data-view`, section structure | `frontend/*.html` (11 files, each read) |
|
| 1381 |
+
| Homepage structure, video chapters, delta pair, open-question links | `frontend/index.html` |
|
| 1382 |
+
| Analyze console markup and every DOM handle | `frontend/mission.html` |
|
| 1383 |
+
| Console driver, `interpret()`, `chooseTask()`, `assetsForTask()`, `validateOpticalSar()`, `translateError()`, `routeSpecialists()`, `renderIntent()`, `STATES`/`EVENT_TO_STATE`/`STATE_NOTE`, `markState()` (fill formula), `buildTrace()`, `logEvent()`, `resetUI()`, `renderEvidence()`, `renderConfidence()`, `onEvent()`, `runMock()`, `runLive()`, `runQuery()`, `loadCapabilities()`, `setMode()`, `handleFile()`, `window.SQ_MISSION` | `frontend/assets/js/mission.js` |
|
| 1384 |
+
| `SQ` namespace, rng/util, synthetic scene flag, `SQ.STAGES`, `SQ.EVENT_NAMES`, `SQ.policy()`, `SQ.run().ingest()`, `startMock()`, `SQ.COMPONENTS` | `frontend/assets/js/core.js` |
|
| 1385 |
+
| Live client: `SQ.ENDPOINTS`, `SQ.CONTENT_TYPES`, `SQ.contentTypeFor()`, `SQ.live.baseUrl()`, `_normalizeBase()`, `SQ.live.url()`, `LiveError`, `describeFailure()`, `uploadAsset()`, `uploadAssets()`, `infer()` (header reads), `run()`, `capabilities()` | `frontend/assets/js/live.js` |
|
| 1386 |
+
| Captured-run driver: `QUERY…PLATE`, `REGIONS`, `paintAll()`, `buildEvidence()`, `evCandidates()/evLock()/evConfirmed()`, `SPECIALISTS_FOR_TASK`, `buildLattice()`, `DATA` (8 KV tables with event names), `setStage()`, `resetEvidence()`, `gotoStep()` | `frontend/assets/js/run.js` |
|
| 1387 |
+
| Captured envelope (run id, task, answer, config hash, transport, plan, steps, models, evidence, regions, confidence + calibration, timings, geospatial, warnings) | `frontend/assets/data/anatomy-run.js` |
|
| 1388 |
+
| Cache rules and the concatenation trap (verbatim comment) | `frontend/_headers` |
|
| 1389 |
+
| Design law, token system, phases A–G, file map, hard limits, schema types, `ingest` seam, 8 event names | `frontend/HANDOFF.md` |
|
| 1390 |
+
| Staging tool: constants, regexes, closure walk, exit codes 2/3, report blocks, `HERMETIC`, deploy hint | `scripts/stage_pages.mjs` |
|
| 1391 |
+
| Deployed frontend HEAD `2d7ae53b482d`; HF link on all 11 pages; blocker item 9 | `docs/FINAL_DELIVERY_TODO.md` |
|
| 1392 |
+
| Live topology and per-component responsibilities | `docs/DEPLOYMENT_TOPOLOGY.md` |
|
| 1393 |
+
| Superseded-topology banner; entrypoint requirements; failure-mode table | `docs/DEPLOYMENT_ARCHITECTURE.md` |
|
| 1394 |
+
| API contract the client speaks (endpoints, enums, confidence, errors, no-auth, CORS, multipart-not-implemented, 307 footgun, 23-code taxonomy) | `docs/API_CONTRACT.md` |
|
| 1395 |
+
| Captured run ids per task; metrics table; blockers | `docs/FINAL_DELIVERY_REPORT.md` |
|
| 1396 |
+
| Style, grounding rules, status vocabulary, facts-that-must-not-be-wrong, 94.4444 % trace fill | `release/DOCS_STYLE_GUIDE.md` |
|