Target reader: backend engineer / Codex / Claude Code / team manager
Date: 2026-04-11 Hackathon: NVIDIA Spark Hack NYC 2026 Hardware: Acer Veriton GN100 / DGX Spark / NVIDIA GB10 Grace Blackwell / 128GB unified memory Runtime target: 100% local, offline-capable Scope owned by us: backend analysis engine, overview service, detail-time ranking, API contract, evidence, brief generation, backend-only smoke tests Not our scope: frontend UI implementation, final dataset selection, final Skill implementation owned by teammates
This v3.5.3 document is the practical execution version after one more team constraint became clear:
- the Skill side is still not settled
- backend still needs to be testable now
- we do not want to throw away the Skill interface that may arrive later
So v3.5.3 formalizes a dual-path backend:
- Preferred path
- use the standardized Skill/data interface when it becomes available
- Current test path
- use hardcoded direct data retrieval in backend code
- query local parquet / CSV / processed extracts directly
The backend must behave the same at the API level in both modes.
That means:
- the frontend should not care whether data came from Skill or direct query
- the ranking / evidence / report logic should not care either
- only the data-provider layer changes
This makes v3.5.3 the most practical handoff version:
- we can test backend immediately
- we preserve the future Skill integration path
- we reduce dependency risk on another workstream
v3.5.2 already got the product split right:
- overview = overall + single-category fixed layers
- detail = clicked-point + radius + drag ranking + real-time recompute
- watchlist = batch reuse of detail logic
v3.5.3 adds one new implementation rule:
Skill is no longer a runtime dependency for testing. Skill becomes an optional provider behind a stable backend interface.
So the architecture is now:
- same product behavior as v3.5.2
- different runtime fallback strategy
Urban Dossier remains:
- a fully local civic analysis backend
- with a pre-rendered overview layer
- and a real-time detail layer
But v3.5.3 adds a more realistic delivery rule:
- if Skill is ready, use Skill outputs
- if Skill is not ready, backend directly queries the available local data assets
The product promise to frontend stays the same:
- overview buttons switch between fixed layers
- clicking a point enters detail mode
- drag order only affects local real-time priority ranking
The latest frontend requirement still stands exactly as in v3.5.2.
Overview should only support:
- one overall map
- one single-category map per button
Overview does not depend on user drag ranking.
After the user clicks a map point:
- the frontend can expose drag ordering
- the backend receives
priority_order - the backend recomputes local priorities in real time
Watchlist is not user-personalized.
It should reuse the detail engine with:
- fixed default platform ordering
- fixed area seeds or map cells
This version exists to solve a very practical delivery problem.
If we require Skill before backend testing, we block ourselves on another workstream.
If we remove Skill entirely, we create migration cost later.
So the right compromise is:
- preserve Skill-facing contracts
- implement direct-query fallback immediately
- make direct-query the currently enabled test mode
This is a better engineering move than waiting.
+------------------------------+
| API Layer |
| overview / detail / watchlist|
+--------------+---------------+
|
v
+------------------------------+
| Shared Analysis Core |
| trends / patterns / priority |
| evidence / report |
+--------------+---------------+
|
+--------------+---------------+
| Data Provider Abstraction |
+-------+----------------------+
|
+--------------+--------------+
| |
v v
+------------------------+ +--------------------------+
| SkillDataProvider | | DirectQueryDataProvider |
| preferred future path | | current test default |
+------------------------+ +--------------------------+
The big idea is simple:
- all product logic is above the provider layer
- all source-specific mess stays below it
Add an explicit backend config:
URBAN_DOSSIER_DATA_MODE = auto | skill | direct
Recommended meaning:
direct- use hardcoded backend retrieval only
- current default for testing
skill- require Skill outputs
- fail clearly if unavailable
auto- use Skill if ready
- otherwise fall back to direct query
For now:
URBAN_DOSSIER_DATA_MODE=direct
This is the correct current choice because Skill delivery is not stable yet.
Once Skill is confirmed working on the official machine, recommended default becomes:
URBAN_DOSSIER_DATA_MODE=auto
That gives us:
- no rewrite of backend API
- graceful use of Skill
- safe fallback if Skill breaks on-site
The provider contract is the key new addition in v3.5.3.
Everything above this layer should consume a stable interface.
Recommended interface:
class DataProvider:
def get_overview_layer(self, view_mode: str, category_id: str | None, viewport: dict | None, zoom: int | None) -> dict: ...
def get_point_signals(self, latitude: float, longitude: float, radius_m: int, time_window_days: int) -> dict: ...
def get_local_timeseries(self, latitude: float, longitude: float, radius_m: int, time_window_days: int) -> dict: ...
def get_baselines(self) -> dict: ...
def get_context_items(self, latitude: float, longitude: float, radius_m: int) -> dict: ...
def get_coverage(self) -> dict: ...This is intentionally backend-facing, not Skill-facing.
If we keep this interface stable:
- the frontend API does not change
- the detail engine does not change
- only the provider implementation changes
That is exactly what we want.
This is the preferred future path.
It should read from Skill-prepared outputs such as:
- standardized signal tables
- precomputed overview layers
- area timeseries
- baselines
- manifest files
This path should assume cleaner inputs and less ad hoc filtering.
This is the currently enabled test path.
It should use hardcoded backend retrieval logic against:
- processed parquet files
- precomputed JSON artifacts
- local CSVs when needed
It should be built from the older code path already present in the local backend repo.
This is not "throwaway hack code." It is the operational fallback path.
This section is important because the fallback should not be vague.
The existing local backend already proves direct retrieval can work:
- bounding-box + radius filtering
- direct DuckDB parquet queries
- ZIP-level aggregate lookup for EMS / Fire
- evidence creation from matched rows
v3.5.3 should explicitly preserve that approach as the fallback test backend.
Direct query mode can read from:
- processed parquet
- cache/precomputed JSON
- cache/report markdown
- demo fixtures
Priority order for direct mode:
- precomputed overview artifacts if they exist
- processed parquet / local extracts
- demo fixtures only when real data is unavailable
We already confirmed over SSH that direct local queries are feasible against real data for:
- collisions
- rodent
- 311
- public toilets
That means direct mode is not theoretical. It is the current practical route for backend-only smoke tests.
Overview logic stays the same as v3.5.2, but the source of data now depends on provider mode.
- overall
- one map per single category
Overview should try:
- pre-rendered overview parquet / JSON if present
- lightweight direct aggregate generation if feasible
- fallback response with
overview_ready = false
Overview should read:
- Skill-prepared overview layers
- Skill-generated manifest
Fallback remains:
{
"schema_version": "v3.5.3",
"mode": "overview",
"view_mode": "overall",
"category_id": null,
"cells": [],
"coverage": {
"overview_ready": false,
"missing_categories": ["overall", "safety", "traffic", "facilities"]
},
"ui_message": "Overview not yet available. Click a point for realtime detail analysis."
}Detail remains the real product core.
The logic is unchanged from v3.5.2:
- accept clicked point + radius + priority order
- query local signals
- compute trends
- detect patterns
- apply ranking weights
- emit
priority_actions,why_now,current_state,detail_items,evidence
The only new rule is:
- data retrieval comes from the active provider
That means detail mode is where we can test the product now even before Skill is ready.
No change from v3.5.2.
Preference only matters in detail mode.
Recommended first-release formula:
weight(rank) = decay ^ (rank - 1)
decay = 0.72
Approximate weights:
- rank 1 ->
1.00 - rank 2 ->
0.72 - rank 3 ->
0.52 - rank 4 ->
0.37 - rank 5 ->
0.27
Weights should influence:
- final priority ranking
- report emphasis
- tie-breaking across categories
Weights should not influence:
- raw retrieval counts
- evidence integrity
- overview map choice
The analysis core should remain source-agnostic.
That means the same core functions should work with either provider:
- trend engine
- pattern detector
- priority engine
- evidence builder
- report generation
This is the whole point of introducing the provider abstraction.
Trend logic remains as previously defined.
Each signal should compute:
recent_deltaseasonal_deltabaseline_gappersistence
def compute_recent_delta(values_by_period):
recent = values_by_period.get("last_30d", 0)
previous = values_by_period.get("prev_30d", 0)
if previous == 0:
return {"change_pct": None, "label": "no_baseline"}
pct = (recent - previous) / previous * 100
return {
"change_pct": round(pct, 1),
"label": "rising" if pct > 10 else "falling" if pct < -10 else "stable",
}
def compute_seasonal_delta(values_by_period):
current = values_by_period.get("last_90d", 0)
same_last_year = values_by_period.get("same_90d_last_year", 0)
if same_last_year == 0:
return {"change_pct": None, "label": "no_baseline"}
pct = (current - same_last_year) / same_last_year * 100
return {
"change_pct": round(pct, 1),
"label": "rising" if pct > 15 else "falling" if pct < -15 else "stable",
}In direct mode, timeseries may initially be rougher because they are assembled directly from local tables.
That is acceptable for testing as long as:
- schema is stable
- output shape is stable
- evidence and caveats remain honest
Pattern detection remains rule-based.
Recommended first rules:
- sanitation complaints rising + rodent rising
- collision pressure rising + EMS response worsening
- housing violations unresolved + AEP/building risk present
This layer should not care whether source data came from Skill or direct query.
Priority logic also remains the same.
priority_score =
category_weight
* severity
* momentum
* confidence
* actionability
def compute_priority_actions(current_state, trends, baselines, priority_weights, signal_to_category):
candidates = []
for signal_name, trend in trends.items():
config = PRIORITY_SIGNALS.get(signal_name)
if not config:
continue
category_id = signal_to_category.get(signal_name, "other")
category_weight = priority_weights.get(category_id, 0.30)
severity = compute_severity(current_state, signal_name, baselines)
momentum = compute_momentum(trend)
confidence = compute_confidence(trend, current_state, signal_name)
actionability = config.get("actionability", 0.5)
priority_score = category_weight * severity * momentum * confidence * actionability
if priority_score > 0.05:
candidates.append({
"signal": signal_name,
"category_id": category_id,
"action": config["action_template"],
"priority_score": round(priority_score, 3),
"evidence_ids": config["evidence_ids"],
})
candidates.sort(key=lambda x: x["priority_score"], reverse=True)
for i, c in enumerate(candidates, start=1):
c["rank"] = i
return candidates[:5]Evidence remains essential.
In direct mode this is even more important, because direct retrieval can be rougher than future Skill-curated outputs.
So v3.5.3 should enforce:
- all surfaced issues need evidence ids
- data gaps must be explicit
- fallback mode must never present rough retrieval as certainty
The frontend API must not change depending on provider mode.
Request:
{
"view_mode": "overall",
"category_id": null,
"viewport": {
"north": 40.92,
"south": 40.49,
"east": -73.68,
"west": -74.27
},
"zoom": 11,
"render_mode": "h3_cells"
}Response:
{
"schema_version": "v3.5.3",
"mode": "overview",
"view_mode": "overall",
"category_id": null,
"layer_mode": "h3_r8",
"cells": [],
"coverage": {
"overview_ready": true,
"available_categories": ["overall", "safety", "traffic", "facilities"],
"missing_categories": []
},
"data_mode": "direct"
}Request:
{
"latitude": 40.7579,
"longitude": -73.9999,
"radius_m": 500,
"priority_order": ["safety", "traffic", "facilities"],
"time_window_days": 365
}Response:
{
"schema_version": "v3.5.3",
"mode": "detail",
"data_mode": "direct",
"target": {},
"priority_profile": {
"order": ["safety", "traffic", "facilities"],
"weights": {
"safety": 1.0,
"traffic": 0.72,
"facilities": 0.52
}
},
"priority_actions": [],
"why_now": [],
"current_state": {},
"detail_items": {
"map_points": [],
"nearby_facilities": [],
"building_flags": [],
"recent_incidents": []
},
"trends": {},
"patterns": [],
"evidence_table": [],
"data_gaps": [],
"scores": {},
"report_summary": "",
"report_markdown": ""
}The extra data_mode field is useful during testing and debugging.
No change.
Should now additionally report:
- active data mode
- whether Skill provider is available
- whether direct provider is available
No user preference order. Use fixed platform ordering and whichever provider is active.
The Skill-facing contract remains in place.
That means we still want the future Skill/data side to provide:
point_eventsarea_timeseriesbuilding_signalscontext_placesbaselinesmanifest
Even though we are not currently depending on Skill, we keep the contract because:
- it preserves the future integration path
- it keeps backend expectations explicit
- it avoids a rewrite when Skill is finally ready
Direct mode is allowed to build provider outputs from whatever local raw/processed assets exist, as long as it adapts them into the same internal shapes expected above.
This is the most important practical section for v3.5.3.
We should immediately support backend-only testing for:
- one overview fallback endpoint
- one overview success path if layer exists
- one detail clicked-point analysis path
- one ranking sensitivity test
Direct mode can already test against a small real subset such as:
- collisions
- rodent
- 311
- public toilets
- EMS ZIP aggregate
This is enough to validate:
- request shape
- ranking logic
- evidence flow
- report fallback flow
One real point, one radius, one ranking order.
Expected:
- valid response
- non-empty
current_state - non-empty
priority_actions
Same point, same radius, two ranking orders.
Expected:
- different
priority_profile.weights - meaningfully different top action ordering
No precomputed layer available.
Expected:
overview_ready = false- frontend-safe message
At least one category layer exists.
Expected:
- non-empty
cells - correct
category_id
The remote machine already proved direct mode is viable.
We confirmed over SSH that local queries can run against real remote data for:
- collisions
- rodent
- 311
- public toilets
So v3.5.3 should treat:
- direct mode as the current execution path
- skill mode as the preferred future upgrade path
This is exactly the right order for delivery.
Teammates are handling the NVIDIA ecosystem optimization path, so our backend should remain compatible rather than prescriptive.
v3.5.3 should naturally support:
- overview precompute on cuDF/RAPIDS
- detail queries on DuckDB/parquet
- batch watchlist runs
- local model endpoint abstraction
The provider split makes this easier:
- direct provider can use current DuckDB/parquet testing immediately
- skill provider can later consume GPU-accelerated curated outputs
The local apex folder still matters for architecture thinking.
The most useful lessons are:
- API-first organization
- shared core, multiple consumers
- runtime/provider abstraction
That lines up exactly with v3.5.3:
- overview, detail, and watchlist share one core
- Skill and direct query are swappable providers
We still should not make Apex the product story itself.
src/
api.py
config.py
categories.py
providers/
base.py
skill_provider.py
direct_provider.py
overview.py
detail.py
metrics.py
trend_engine.py
pattern_detector.py
priority_engine.py
evidence.py
scoring.py
report.py
watchlist.py
The old direct-query code from the previous backend should not be deleted.
It should be refactored into providers/direct_provider.py.
That is the main implementation move in v3.5.3.
- add provider abstraction
- implement
DirectQueryDataProvider - set
URBAN_DOSSIER_DATA_MODE=direct - wire
/api/analyze-pointthrough provider layer
- implement overview fallback API
- add one or two direct-mode overview layers
- expose
data_modein responses
- add
SkillDataProviderskeleton - keep method signatures stable
- keep it disabled until Skill outputs are confirmed
- switch default to
autowhen Skill is actually usable
- final first-pass category list
- exact frontend expectation for overview rendering
- detail radius options exposed in UI
- first standardized Skill/data outputs that will actually ship
- final local model endpoint choice on the official machine
But none of these should block direct-mode backend testing.
The correct v3.5.3 direction is:
- keep the v3.5.2 product behavior
- overview = overall + single-category fixed layers
- detail = click + radius + drag order -> live local priorities
- but change runtime dependency strategy
- current test mode = direct hardcoded backend retrieval
- future preferred mode = Skill-backed provider
- stable backend interface for both
This is the most practical version for the team right now because it lets the backend move immediately without breaking the future Skill integration path.