Target reader: backend engineer / frontend engineer / Codex / Claude Code / team manager
Date: 2026-04-11 Scope: full backend contract aligned to the current demo frontend, the v3.6 backend architecture, and the v3.7 open-question decisions This is the new working engineering source for interface alignment.
v3.6 established the backend architecture:
- overview mode
- detail mode
- watchlist mode
- direct provider vs skill provider
- deterministic trend / priority / evidence / report pipeline
v3.7 then captured the decisions still being settled:
- report trigger timing
- radius logic
- building handling
- prompt style direction
- overview vs detail boundary
At the same time, the imported demo frontend introduced a very concrete implementation reality:
- it already ships a working offline map resource path
- it already expects
general / safety / transit / amenities - it already has a local map render API shape
- it does not yet speak the current backend contract directly
Therefore, v3.7.5 exists to answer one specific question:
How do we make the backend line up with the current frontend demo without losing the stronger architecture we already agreed on?
This document is the answer.
The product has three backend-relevant states:
- Overview
- whole-map view
- no report generation
- fixed map buttons only
- Detail Preview
- after a map click
- user chooses radius and ranking
- backend recomputes local structured results
- no LLM report yet
- Detail Final
- user presses a confirm / generate button
- backend reuses the same local computation and adds the final report
Overview helps the user locate potentially important areas.
Overview should support:
- one general / overall map
- one single-category map per category button
Overview should not:
- run LLM report generation
- apply user drag order
- produce custom weighted permutations of citywide maps
Detail is where the real local analysis happens.
Once the user clicks a point:
- the backend uses the selected point
- the backend uses the selected radius
- the backend uses the user priority order
- the backend recomputes local issues in realtime
- the backend returns local structure
- on confirm, the backend additionally returns the final report
Watchlist is still part of the architecture, but it is not the primary frontend path right now.
For v3.7.5:
- keep watchlist in the backend contract
- keep it structured-first
- do not let it drive the main frontend interaction design
The imported demo frontend currently uses:
- a local Node/Express server in server.js
- a local MBTiles file in osm-2020-02-10-v3.11_new-york_new-york.mbtiles
- frontend category labels:
AmenitiesTransitSafety
- frontend map tags:
generalamenitiestransitsafety
The Python backend already has:
- deterministic point analysis
- trend engine
- priority engine
- evidence generation and verification
- overview fallback
- direct vs skill provider abstraction
But before this revision, the backend and frontend still disagreed on:
- category naming
- preview vs final report separation
- API route shapes
- data types expected by the frontend
We want the system to remain compatible with a future Pensar/Apex hardening pass.
Therefore:
- we should avoid multiplying business logic across duplicate endpoints
- we should prefer a canonical backend contract
- any compatibility layer should be thin
- the browser should not need to learn many separate analysis endpoints if the local frontend server can proxy
That matters directly for v3.7.5.
From this version onward, the canonical backend category IDs should match the frontend language:
amenitiestransitsafetybuilding
building remains:
- available in detail
- not one of the user-drag main categories
- not a primary overview button
The backend should still accept these aliases:
facilities -> amenitiestraffic -> transitgeneral -> overalloverall -> overall
This is important for:
- older docs
- older code
- teammate experiments
- compatibility during transition
Safety
- rodent
- sanitation 311
- EMS ZIP proxy
- Fire ZIP proxy
Transit
- collisions
- mobility-related local incident pressure
Amenities
- parks access proxy
- public toilets
- LinkNYC
- restaurant / local facility context
Building
- housing violations
- AEP
- future building-specific stress signals
User interaction:
- open map
- choose one of:
- general
- safety
- transit
- amenities
Backend behavior:
- returns overview cells for the chosen map layer
- does not generate a report
- does not use drag order
User interaction:
- click point
- choose radius
- drag category order
- preview local structure
Backend behavior:
- recomputes local deterministic analysis
- returns:
- current_state
- detail_items
- priority_actions
- why_now
- evidence_table
- data_gaps
- scores
- does not call the LLM
User interaction:
- click confirm / generate report
Backend behavior:
- reuses the same local deterministic analysis
- then generates the final report
This separation is now explicit and intentional.
The Python backend should own the canonical analysis contract:
GET /api/healthGET /api/categoriesGET /api/coveragePOST /api/overviewPOST /api/detail/previewPOST /api/analyze-pointPOST /api/watchlist/run
The frontend demo currently expects:
GET /api/render/globalPOST /api/render/local
These should not become independent business APIs in the backend.
Instead, v3.7.5 recommends:
- keep the canonical analysis API listed above
- let the local demo server provide a thin adapter / proxy if needed
Recommended mapping:
GET /api/render/global?tag=general|safety|transit|amenities-> translate toPOST /api/overviewPOST /api/render/local-> translate toPOST /api/detail/preview
This gives us:
- backend-first architecture
- frontend compatibility
- less duplicated logic
- easier Pensar review later
- cleaner migration path once the frontend fully speaks canonical endpoints
Purpose:
- tell the frontend which category IDs and labels exist
- provide default order
- provide alias information
Example response:
{
"schema_version": "v3.7.5",
"default_order": ["amenities", "transit", "safety"],
"overview_tags": ["general", "safety", "transit", "amenities"],
"aliases": {
"general": "overall",
"overall": "overall",
"facilities": "amenities",
"traffic": "transit"
},
"categories": [
{
"category_id": "amenities",
"label": "Amenities",
"map_driving": true,
"detail_rankable": true,
"signals": ["parks_access", "public_toilets", "linknyc", "restaurant_context"]
}
]
}Purpose:
- return the selected overview layer
Request:
{
"view_mode": "overall",
"category_id": null,
"viewport": {
"north": 40.86,
"south": 40.68,
"east": -73.90,
"west": -74.05
},
"zoom": 11,
"render_mode": "h3_cells"
}Category request variant:
{
"view_mode": "category",
"category_id": "transit",
"viewport": {
"north": 40.86,
"south": 40.68,
"east": -73.90,
"west": -74.05
},
"zoom": 11,
"render_mode": "h3_cells"
}Response shape:
{
"schema_version": "v3.7.5",
"mode": "overview",
"view_mode": "category",
"category_id": "transit",
"layer_mode": "h3_r8",
"cells": [],
"coverage": {
"overview_ready": true,
"available_categories": ["overall", "safety", "transit", "amenities"],
"missing_categories": []
},
"resolved_data_mode": "direct"
}Purpose:
- compute full local deterministic analysis
- do not generate final report
Request:
{
"latitude": 40.7580,
"longitude": -73.9855,
"radius_m": 500,
"priority_order": ["amenities", "transit", "safety"],
"time_window_days": 365
}Important:
radius_mis restricted to:2005001000
priority_orderaccepts aliases but is normalized to canonical category IDs
Response shape:
{
"schema_version": "v3.7.5",
"mode": "detail_preview",
"target": {},
"priority_profile": {
"order": ["amenities", "transit", "safety"],
"weights": {
"amenities": 1.0,
"transit": 0.72,
"safety": 0.52
}
},
"priority_actions": [],
"why_now": [],
"current_state": {},
"detail_items": {},
"trends": {},
"patterns": [],
"evidence_table": [],
"data_gaps": [],
"scores": {},
"preview_ready": true
}Purpose:
- run the same detail computation as preview
- then generate the final report
Request:
{
"latitude": 40.7580,
"longitude": -73.9855,
"radius_m": 500,
"priority_order": ["amenities", "transit", "safety"],
"time_window_days": 365,
"include_report": true
}Response shape:
{
"schema_version": "v3.7.5",
"mode": "detail",
"target": {},
"priority_profile": {},
"priority_actions": [],
"why_now": [],
"current_state": {},
"detail_items": {},
"trends": {},
"patterns": [],
"evidence_table": [],
"data_gaps": [],
"scores": {},
"report_summary": "",
"report_markdown": ""
}Purpose:
- batch reuse of detail logic
For v3.7.5, this remains backend-facing and secondary.
The current demo already owns:
- MBTiles storage
- vector tile serving
- glyph serving
- local static map resource delivery
Therefore, the Python backend should not take ownership of:
/tiles/{z}/{x}/{y}.pbf- font PBFs
- map style resources
The Python backend owns:
- overview analysis payloads
- detail preview payloads
- detail final report payloads
- categories / coverage / health metadata
This separation is important because:
- it keeps the analysis backend focused
- it reduces accidental duplication
- it makes Pensar review cleaner
The only user-facing radii for v3.7.5 are:
200m500m1000m
The backend should reject arbitrary radii.
These signals should use the selected radius directly:
- collisions
- rodent inspections
- 311 complaints
- restaurant inspections
- LinkNYC
- public toilets
- street trees
- other point/facility context datasets
Building-specific signals should use:
building_radius = min(selected_radius, 250m)
This means:
- selected
200m-> building radius200m - selected
500m-> building radius250m - selected
1000m-> building radius250m
These do not follow the selected radius:
- EMS
- Fire
- future ZIP/CD service proxies
They should remain:
- area-level context
- not point-radius claims
For the first implementation:
- use nearby access / local proxy logic
- allow acreage to remain approximate
- state that approximation explicitly in
data_gaps
Changing radius changes raw counts.
Therefore:
- raw count alone must not drive severity
Preferred long-term direction:
- radius-specific baselines for:
200m500m1000m
Allowed interim fallback:
- density / area-normalized scaling
Preview and final report should both use the same deterministic core:
- resolve local target context
- query local signals
- query local historical windows
- compute trends
- detect patterns
- compute priority actions
- build evidence
- compute scores
The final report call simply adds:
- generate markdown brief
If preview and final use different logic:
- the UI will drift
- the report will not match the preview
- trust will degrade
Therefore:
- preview is not a toy
- final is not a second analysis pass with different rules
They must share the same computed state.
User ranking should affect:
- preview priority ordering
- final report emphasis
User ranking should not affect:
- overview layer generation
Use exponential decay:
weight = decay ^ (rank - 1)
With:
decay = 0.72
If user order is:
amenitiestransitsafety
Then weights are approximately:
amenities = 1.00transit = 0.72safety = 0.52
Frontend may still send:
AmenitiesTransitSafety
or older IDs like:
facilitiestraffic
Backend should normalize them before ranking.
Building signals should be a dedicated detail section, not a main draggable category.
That means:
- building is computed in detail mode
- building is visible in detail mode
- building does not have to appear as a top-level overview button
It avoids mixing two different questions:
- “Is this area under local pressure?”
- “Is there a nearby building-specific issue?”
Those are related, but not identical.
In current_state:
buildingmodule summary
In detail_items:
building_flags
Future-ready output examples:
{
"building_stress_score": 31,
"severity_level": "high",
"open_class_c_count": 3,
"aep_flag": true
}Per the decision:
- building section should appear only if there are actual building findings
No empty building section should be forced.
The final report should only be generated after:
- point selected
- radius selected
- priority order selected
- confirm button pressed
The report must explicitly include:
- selected point
- selected radius
- borough if available
- ZIP if available
- active priority order
The report must aim for:
- concrete conclusions
- concrete local priorities
- evidence-backed interpretation
The report must avoid:
- bland summaries
- metric dumping
- pretending every result is alarming
The prompt should be:
- strict about direction
- flexible about surface writing
Meaning:
- the model must write real conclusions
- the model may decide how to phrase them
- the model must faithfully reflect whether the result is:
- good
- bad
- mixed
- sparse
The prompt must instruct the model to:
- explicitly mention the selected radius
- distinguish local radius-based findings from ZIP-based context
This is essential for correctness.
This is optional, not required for first run.
Add a light overview_context object to the detail payload:
{
"overview_context": {
"overall_cell_level": "elevated",
"category_cells": {
"safety": { "level": "high", "percentile_city": 88 },
"transit": { "level": "medium", "percentile_city": 61 },
"amenities": { "level": "low", "percentile_city": 35 }
}
}
}The report may add a single light sentence like:
- “The selected point sits in an elevated safety overview cell citywide.”
It should not let overview dominate the report.
The current frontend still works on a legacy mock type:
scoresaiSummaryevidence
That is not enough for v3.7.5.
Frontend must be updated to understand:
- overview response
- detail preview response
- detail final response
- priority actions
- detail items
- building flags
- data gaps
These are the most important current frontend mismatches:
- it does not call canonical backend analysis endpoints yet
- its map render calls still target demo-only
/api/render/* - it generates random local scores on click
- it has no true report-confirm step yet
- it still treats the right panel as a score-first card instead of action-first preview/final
The minimum successful integration path is:
- keep map resource server as-is
- keep overview tag buttons:
generalsafetytransitamenities
- on click:
- enter preview state
- on priority/radius change:
- call preview endpoint
- on confirm:
- call final report endpoint
Recommended topology:
Browser
-> local demo server (Node/Express)
-> static frontend
-> MBTiles/vector tile endpoints
-> thin proxy for preview/final analysis if needed
-> Python analysis backend
Because it gives us:
- easier same-origin frontend development
- less CORS exposure
- cleaner future Pensar story
- preserved separation between map assets and analysis engine
Even if Node provides proxy endpoints, the canonical business logic still belongs to Python.
That means:
- Node = adapter/proxy/static layer
- Python = analysis source of truth
The current frontend demo imported a permissive local map server.
If we add analysis directly into that same server without discipline, we risk:
- duplicate logic
- loose request validation
- wider attack surface
The security-friendly design is:
- canonical analysis logic in Python
- thin compatibility layer only
- strict canonical request validation
- minimal accepted radii
- normalized category allowlist
This makes future Pensar review easier because:
- there is one real analysis core
- compatibility routes are adapters, not shadow logic
- category IDs and radii are bounded
For current testing:
- use
DirectQueryDataProvider
Keep:
SkillDataProvider- mode switching
- future
automode
But:
- do not block frontend/backend integration on Skill readiness
It protects progress.
The frontend can integrate now. The backend can test now. Skill can drop in later.
- finish category rename cleanup everywhere
- keep
detail previewanddetail finalon the same deterministic core - add alias-safe overview handling for
general/overall - preserve direct-mode reliability
- optionally expose thin compatibility endpoints or support proxy integration
- replace mock click analysis with preview call
- replace mock
Generateusage with report-confirm workflow - update right panel to consume:
priority_actionswhy_nowdetail_itemsreport_markdown
- keep map resource pipeline intact
- verify local preview latency
- verify final report generation after confirm
- verify category alias handling
- verify radii
200/500/1000 - verify building section only appears when findings exist
v3.7.5 is considered correctly implemented when:
- frontend category buttons map cleanly to:
generalsafetytransitamenities
- backend canonical categories are:
amenitiestransitsafetybuilding
- click + radius + priority order can produce a preview response without LLM
- confirm can produce a final report response
- report explicitly names the selected radius and local context
- building findings appear only when they exist
- overview remains non-reporting and non-ranked
- the product still works if Skill is absent
v3.7.5 is the version where we stop treating the frontend and backend as separate thought experiments.
From this point onward:
- frontend map resources stay where they already work
- backend owns canonical analysis logic
- category naming follows the frontend
- preview and final report are formally separated
- building handling follows Option 4A
- report writing aims for concrete conclusions, not generic summaries
- overview remains navigation, not personalized reasoning
This is the correct integration version to build against next.