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:
@@ -51,6 +51,7 @@ const NODE_COLORS: Record<string, string> = {
|
||||
topic: "#a97d3a", // gold — hubs stand out
|
||||
practice_area: "#475569", // slate
|
||||
gap: "#94a3b8", // faint slate — research gap (not in corpus)
|
||||
digest: "#2f6f7a", // teal — daily-digest (יומון) discovery layer
|
||||
};
|
||||
|
||||
const TREATMENT_COLORS: Record<string, string> = {
|
||||
@@ -114,6 +115,7 @@ export function colorForNode(n: GraphNode, colorBy: ColorBy): string {
|
||||
|
||||
export function radiusForNode(n: GraphNode, sizeBy: SizeBy): number {
|
||||
if (n.type === "topic" || n.type === "practice_area") return 5;
|
||||
if (n.type === "digest") return 4;
|
||||
if (sizeBy === "pagerank" && n.pagerank != null) {
|
||||
return 3 + Math.sqrt(n.pagerank) * 18;
|
||||
}
|
||||
@@ -301,6 +303,9 @@ export function GraphCanvas({
|
||||
if (link.type === "tagged" || link.type === "in_area") {
|
||||
return "rgba(169,125,58,0.16)";
|
||||
}
|
||||
if (link.type === "covers") {
|
||||
return "rgba(47,111,122,0.45)"; // teal — digest → ruling
|
||||
}
|
||||
return "rgba(80,90,110,0.22)";
|
||||
},
|
||||
[activeId],
|
||||
|
||||
@@ -44,6 +44,7 @@ export type GraphControls = {
|
||||
showPracticeAreas: boolean;
|
||||
showHalachot: boolean;
|
||||
showGaps: boolean;
|
||||
showDigests: boolean;
|
||||
};
|
||||
|
||||
const ALL = "__all__";
|
||||
@@ -265,6 +266,11 @@ export function GraphFilterPanel({
|
||||
checked={controls.showGaps}
|
||||
onCheckedChange={(v) => onChange({ showGaps: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="יומונים (כל יום)"
|
||||
checked={controls.showDigests}
|
||||
onCheckedChange={(v) => onChange({ showDigests: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="הלכות (שלב ב׳)"
|
||||
checked={controls.showHalachot}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -66,6 +66,7 @@ export function GraphView() {
|
||||
showPracticeAreas: true,
|
||||
showHalachot: false,
|
||||
showGaps: false,
|
||||
showDigests: false,
|
||||
});
|
||||
const facets = useGraphFacets().data;
|
||||
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
|
||||
@@ -80,12 +81,14 @@ export function GraphView() {
|
||||
if (controls.showPracticeAreas) t.push("practice_area");
|
||||
if (controls.showHalachot) t.push("halacha");
|
||||
if (controls.showGaps) t.push("gap");
|
||||
if (controls.showDigests) t.push("digest");
|
||||
return t.join(",");
|
||||
}, [
|
||||
controls.showTopics,
|
||||
controls.showPracticeAreas,
|
||||
controls.showHalachot,
|
||||
controls.showGaps,
|
||||
controls.showDigests,
|
||||
]);
|
||||
|
||||
// Metrics are needed when colouring by cluster or sizing by a centrality.
|
||||
|
||||
@@ -19,7 +19,8 @@ export type GraphNodeType =
|
||||
| "halacha"
|
||||
| "topic"
|
||||
| "practice_area"
|
||||
| "gap";
|
||||
| "gap"
|
||||
| "digest";
|
||||
|
||||
export type GraphEdgeType =
|
||||
| "cites"
|
||||
@@ -27,7 +28,8 @@ export type GraphEdgeType =
|
||||
| "tagged"
|
||||
| "in_area"
|
||||
| "corroborates"
|
||||
| "equivalent";
|
||||
| "equivalent"
|
||||
| "covers";
|
||||
|
||||
export type GraphNode = {
|
||||
id: string;
|
||||
@@ -45,6 +47,8 @@ export type GraphNode = {
|
||||
community: number | null; // dense cluster id, 0 = largest
|
||||
gap_status: string | null; // gap nodes only — open|uploaded|closed|irrelevant
|
||||
missing_precedent_id: string | null; // gap nodes only
|
||||
note: string | null; // digest nodes only — the holding line
|
||||
digest_id: string | null; // digest nodes only
|
||||
};
|
||||
|
||||
export type GraphFacets = {
|
||||
|
||||
Reference in New Issue
Block a user