feat(graph): daily-digest (יומון) discovery layer (corpus graph PR E)

Chaim's idea: surface the downloaded "כל יום" digests in the graph. Each digest
COVERS the ruling it analyses — a corpus precedent when we have it (16), or a
synthesized gap node from its underlying_citation when we don't (269). So the
digest layer doubles as a discovery signal: it makes visible that the daily
feed overwhelmingly covers rulings NOT yet in the corpus.

Backend (web/graph_api.py — read-only, G2):
- "digest" added to VALID_NODE_TYPES (off by default).
- _digest_nodes_and_edges(): dig:<id> nodes from completed digests, `covers`
  edge → cl:precedent (linked_case_law_id in view) or → gap:<underlying_citation>
  (synthesized, deduped against the gap layer — real in-degree wins). Carries
  concept_tag (label), headline_holding (note), underlying_court/date.
- _add_digests() appends the layer with gap dedup. Wired into both build
  functions. GraphNode += note, digest_id. Gated via node_types (no app.py
  change). Validated: 16 covers→precedent, 269 covers→gap.

Frontend:
- graph.ts: GraphNodeType += "digest"; GraphEdgeType += "covers"; node fields.
- graph-filter-panel: toggle "יומונים (כל יום)" (off by default).
- graph-canvas: digest = teal node (r=4); `covers` edges teal.
- graph-node-panel: digest branch — concept + holding + court/date + link to
  /digests.

web-ui build + lint pass. Invariants: G2 (SELECT-only), UI2. api:types post-deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:31:04 +00:00
parent 8dc0a268fb
commit fc5d69902f
6 changed files with 147 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ const TYPE_LABELS: Record<string, string> = {
topic: "נושא",
practice_area: "תחום",
gap: "פסיקה חסרה",
digest: "יומון",
};
const GAP_STATUS_LABELS: Record<string, string> = {
@@ -52,6 +53,7 @@ export function GraphNodePanel({
}) {
const isPrecedentLike = node.type === "precedent" || node.type === "halacha";
const isGap = node.type === "gap";
const isDigest = node.type === "digest";
return (
<Card className="bg-surface border-rule shadow-sm w-80 shrink-0 overflow-y-auto">
<CardContent className="space-y-4 p-4">
@@ -98,7 +100,19 @@ export function GraphNodePanel({
</p>
</>
)}
{!isPrecedentLike && !isGap && (
{isDigest && (
<>
{node.note && (
<p className="text-ink text-sm leading-relaxed m-0">{node.note}</p>
)}
{node.court && <Row label="ערכאה" value={node.court} />}
{node.date && <Row label="תאריך" value={node.date.slice(0, 10)} />}
<p className="text-ink-muted text-xs leading-relaxed m-0">
סיכום יומי מ״כל יום״ מצביע על הפסיקה שהוא מנתח.
</p>
</>
)}
{!isPrecedentLike && !isGap && !isDigest && (
<p className="text-ink-muted text-xs leading-relaxed m-0">
לחיצה על נקודה זו מתמקדת בשכניה כל הפסיקות המשויכות אליה.
</p>
@@ -122,6 +136,15 @@ export function GraphNodePanel({
</Link>
</Button>
)}
{isDigest && (
<Button asChild variant="outline" className="w-full">
<Link href="/digests">
<ExternalLink className="size-4 me-2" />
לעמוד היומונים
</Link>
</Button>
)}
</CardContent>
</Card>
);