feat(graph): metadata filters + facets (corpus graph PR A)

Adds legal-metadata filtering and the payload to color by it (foundation for
the color-by selector in the analytics PR).

Backend (web/graph_api.py, web/app.py) — read-only, G2:
- GraphNode += court, date (ISO) — precedents carry them for filter/color-by.
- build_corpus_graph += server-side WHERE filters (G5): court, precedent_level,
  chair, district, year_from, year_to (EXTRACT(YEAR FROM date)). Neighborhood
  query also selects court/date.
- New GET /api/graph/facets (response_model GraphFacets, UI2) → distinct
  courts/levels/chairs/districts so the UI doesn't hardcode Hebrew strings.

Frontend:
- graph.ts: GraphNode += court/date; GraphFilters += the six params;
  buildParams; useGraphFacets() hook.
- graph-filter-panel: an "advanced" Accordion with court/precedent_level/chair/
  district Selects (from facets) + year-from/year-to Selects.
- graph-view: new controls wired into filters; facets fetched and passed down.

Verified read-only against the live DB (precedent_level=עליון&year_from=2015
filters correctly; facets populated: 36 courts / 3 levels / 19 chairs / 4
districts). web-ui build + lint pass.

Invariants: G2 (SELECT-only via db.get_pool), G5 (filters server-side),
UI2 (explicit response_models). api:types to be regenerated post-deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 20:52:13 +00:00
parent bcd5fd5f8d
commit 8258f09228
5 changed files with 267 additions and 3 deletions

View File

@@ -5774,6 +5774,12 @@ async def graph_corpus(
min_citations: int = 0,
limit: int = graph_api.NODE_CAP_DEFAULT,
q: str = "",
court: str = "",
precedent_level: str = "",
chair: str = "",
district: str = "",
year_from: int = 0,
year_to: int = 0,
):
"""Full corpus graph under the given filters (most-cited nodes survive the cap)."""
if practice_area and practice_area not in _PRACTICE_AREAS:
@@ -5787,9 +5793,22 @@ async def graph_corpus(
min_citations=min_citations,
limit=limit,
q=q,
court=court,
precedent_level=precedent_level,
chair=chair,
district=district,
year_from=year_from,
year_to=year_to,
)
@app.get("/api/graph/facets", response_model=graph_api.GraphFacets)
async def graph_facets():
"""Distinct filter values (courts / levels / chairs / districts) for the UI."""
pool = await db.get_pool()
return await graph_api.build_facets(pool)
@app.get("/api/graph/node/{node_id}/neighborhood", response_model=graph_api.CorpusGraph)
async def graph_node_neighborhood(node_id: str, depth: int = 1, node_types: str = ""):
"""Local-graph focus: the node + its neighbors out to ``depth`` (1-2)."""