Sidecar API
The Workbench sidecar (src/lunartrace/workbench/server.py) is a Starlette application served by
Uvicorn on 127.0.0.1:4318 (--port). The Vite development server proxies /py/* to it and removes
the /py prefix, so the paths below are relative to the sidecar root.
The sidecar computes every number it returns through the qualified Python method
lunartrace.slope_quality/v1 (METHOD_ID, implementation primary-gdal). The browser computes
nothing.
Conventions
Section titled “Conventions”Origin and Host guard. Every route checks the Origin and Host headers. A request without
Origin is accepted. An Origin must parse as an http or https origin whose host name is
127.0.0.1, localhost or lunartrace.localhost, on any port, with no user info or path. Look-alike
names such as localhost.evil.example are refused. Any other origin receives
403 {"error": "non-local origin"}. The Host header must name the same local hosts; a page that
re-binds its own DNS name to 127.0.0.1 therefore receives 403 {"error": "non-local host"}.
Size limits. A request body over 64 KiB (MAX_BODY_BYTES) returns 413. At most 64 thresholds
(MAX_TAUS) are accepted per request.
Errors. Failures that are input or method errors return HTTP 422 with {"error": "<message>"}.
The 422 class covers RoiError (and its subclass CellNotFound, which is caught earlier on /cell),
MethodInputError, KeyError, and ValueError (including JSON decoding errors). Other exceptions
are not mapped: they surface as HTTP 500 from Starlette. A failure to start the Lab returns 503 (see
POST /lab/open).
Bodies. POST routes accept a JSON object. A body that is not valid JSON, or is not a JSON
object, returns 422.
Shared request fields (used by POST /roi/evaluate, POST /figure and POST /lab/open):
| Field | Type | Required | Limits | Default |
|---|---|---|---|---|
ring | list of [lon, lat] | yes | 3 to 512 vertices; each pair has exactly two finite JSON numbers (not booleans or strings); longitude in [−180, 180]; latitude in [−90, 90]; closure implicit | none |
taus | list of numbers | no | 1 to 64 entries; each a finite JSON number ≥ 0; list given in order | [0.5, 2.0, 25.0] |
The ring must project to finite grid coordinates and a valid polygon with positive area, and at least one grid cell centre must lie inside it. Invalid geometry returns 422.
Routes
Section titled “Routes”GET /health
Section titled “GET /health”Liveness check.
- 200
{"ok": true, "method": "lunartrace.slope_quality/v1"}
GET /site
Section titled “GET /site”The loaded Site: provenance and reference geometry.
- 200
provenance(object):source_witness(string): manifest identifier, or"?"if absent;manifest_sha256(string);inputs(object): keysslopeandslope_rms, each{name, sha256};grid(object):shapeas[rows, cols], andcell_size_m(number).
default_taus(list of numbers):[0.5, 2.0, 25.0].reference_rois(list): each{roi, ring}whereroiis the string index ("0","1", …) andringis a list of[lon, lat]pairs, with the closing vertex included.
- 422 or 500 if the Site cannot be loaded (see Errors).
The Site is loaded once per process and cached.
GET /cell
Section titled “GET /cell”Resolve a geographic pick to its containing grid cell.
Query parameters:
| Name | Type | Required | Notes |
|---|---|---|---|
lon | float | yes | Any finite value; projected by Python |
lat | float | yes | Finite, in [−90, 90] |
tau | float, repeatable | no | Each finite and ≥ 0; absent means the default τ values |
ring | JSON text: list of [lon, lat] | no | Same limits as the shared ring. When present, the pick must lie inside it |
- 200
row(int),col(int): the containing pixel, fromfloorof the inverse affine transform;lon(float),lat(float): the cell-centre coordinates;slope(float ornull): slope in degrees;nullwhen not finite;err(float ornull): slope RMS;nullwhen not finite;included(object): keyed by the τ label formatted with%g(for example"0.5","2","25"), each value a boolean for that cell’s inclusion underslope_quality/v1.
- 404
{"error": "..."}when the pick is outside the Site grid ("location is outside the Site grid"), or outside the suppliedring("location is outside the selected ROI"). The grid test is applied first. - 422 when
lonorlatis missing or not a number, whenlatis outside [−90, 90] or the coordinates are not finite, whenringis not valid JSON or fails the ring checks, or when atauis invalid.
Pixel footprints are half-open: top and left edges are included, bottom and right edges excluded. See ROI and brushing.
POST /roi/evaluate
Section titled “POST /roi/evaluate”Evaluate slope_quality/v1 rows over the mask of a ring.
Body: ring (required), taus (optional).
-
200
roi(object):id(string):source:<index>when the ring matches a served source ring, otherwiseuser:<first 12 hex of ring_sha256>;kind(string):source-ringoruser-drawn;source_index(int): present only forsource-ring;ring_sha256(string);ring(list of[lon, lat]): the ring as received;mask_rule(string):cell-centre-inside (user polygon; not the v1 lattice closed rule);cells(int): cells in the mask.
method(string):lunartrace.slope_quality/v1;implementation(string):primary-gdal;status(string):METHOD_EXECUTED;admission(string):NOT_ADMITTED;taus(list of float): the validated τ values in the order given;rows(list): one object per τ, with the fields below;provenance: as inGET /site;samples(object): see below.
Row fields:
Field Type Meaning roistring The ROI label, as in roi.idtaufloat Cutoff roi_cellsint Cells in the mask valid_slope_cellsint Cells with slope in [0, 90] valid_err_cellsint Cells with slope RMS finite and > 0 joint_cellsint Cells valid on both included_cellsint Joint cells with slope RMS ≤ τ included_fractionfloat or nullincluded_cells / joint_cells;nullwhenjoint_cellsis 0slope_quantilesobject or nullKeys "0.05","0.25","0.5","0.75","0.95"; inverted-CDF quantiles of included slopes;nullwhen none are includedslope_meanfloat or nullMean slope of included cells; nullwhen none are includedslope_histogramlist of 45 int Included-cell counts in 2-degree bins over [0, 90] samplesobject:Field Type Meaning cells_in_roiint Cells in the mask before subsampling strideint ceil(cells_in_roi / 50000), minimum 1countint Sampled cells returned columnsobject Column arrays, one entry per sampled cell columnskeys:row,col(int);lon_deg,lat_deg(float, rounded to 7 decimals);slope_deg,slope_rms_deg(float ornull);jointly_valid(bool); and oneincluded_tau_<tau>boolean array per τ, with τ formatted%g. -
422 when the body is invalid, the ring fails the ring checks, the ring self-intersects and cannot be repaired to a polygon, the ring has zero area, no grid cell centre lies inside it (
"ROI contains no grid cell centre (outside the Site grid or too small)"), or a τ is invalid.
POST /figure
Section titled “POST /figure”Render a deterministic figure for a ring.
Body: ring (required), taus (optional), kind (optional), format (optional), tau (optional).
| Field | Type | Values | Default |
|---|---|---|---|
kind | string | histogram, inclusion, map | histogram |
format | string | svg, png, pdf | svg |
tau | number | Used only for map; any number is accepted and used directly | first τ |
Non-numeric tau values are ignored, and the first τ is used.
- 200 the figure bytes. The
Content-Typeisimage/svg+xml,image/pngorapplication/pdf. Thecontent-dispositionheader isinline; filename="lunartrace-<roi id with ':' replaced by '-'>-<kind>.<format>". - 422
{"error": "kind must be one of ('histogram', 'inclusion', 'map'), format one of svg/png/pdf"}for an unknown kind or format; and the ring and τ errors listed underPOST /roi/evaluate.
Figure content is described in Export.
GET /presets
Section titled “GET /presets”The setup block and presets used by the Python instrument and the Lab.
- 200
setup(string): the setup block with this sidecar’s manifest and store paths bound;presets(list): each{id, title, summary, code}.
Preset ids: roi-rows, tau-sweep, slope-vs-rms, compare-reference, steep-share, figure.
POST /lab/open
Section titled “POST /lab/open”Create (if needed) and register an editable marimo notebook for a ring, and return its URL.
Body: ring and taus as above, plus:
| Field | Type | Default | Notes |
|---|---|---|---|
presets | list of string | ["roi-rows"] | Each must be a known preset id, otherwise 422 |
- 200
{"url": "<marimo URL>", "file": "roi-<16 hex>.py", "created": <bool>}urlhas the formhttp://127.0.0.1:4319/?file=<file>&access_token=<token>. The token is generated per sidecar process.createdistruewhen this request wrote the notebook andfalsewhen an identical one existed.
- 422 for an invalid ring, τ or preset id.
- 503 if the Lab cannot start: port 4319 is busy,
marimoexits during start, or it does not open its port within 30 seconds.
The notebook file name is the first 16 hex digits of the SHA-256 of the whole generated notebook source (ring, τ values, presets and input paths). A changed request therefore gets a new notebook, and an existing file with the same name has the same source, so it is reused without being rewritten.
Limits summary
Section titled “Limits summary”| Limit | Value | Source |
|---|---|---|
| Ring vertices | 3 to 512 | MAX_RING_VERTICES |
| Sampled cells per evaluation | at most 50,000 (stride subsample) | SAMPLE_LIMIT |
| Default τ values | 0.5, 2.0, 25.0 | DEFAULT_TAUS |
| Histogram bins | 45 bins of 2 degrees over [0, 90] | HISTOGRAM_EDGES |
| Lab port | 4319 | --lab-port |
| Sidecar port | 4318 | --port |
python -m lunartrace.workbench --check # load the Site once, print provenance JSON, exitpython -m lunartrace.workbench --port 4318 # serve (127.0.0.1 only)Options: --manifest (default research/sources/pgda78-site01.manifest.json), --store (no default;
falls back to LUNARTRACE_SOURCE_STORE), --lab-dir (default build/lab), --port, --lab-port.