Skip to main content

ADR-0118: Finding-derived UI reads the case record, not the accumulated entity-network graph

Date: 2026-07-21 Status: Accepted Deciders: Adrian (Soft4U), Claude (Opus 4.8) Refines: ADR-0117

Decision context:

  • Latency: none — removes graph iteration from the Verdict/Evidence; reads caseDetail already loaded.
  • Dependency surface: none — frontend-only, no new packages.
  • Debuggability: high — the finding count now equals the case's investigation_results length, trivially checkable; the graph accumulation no longer leaks into a headline number.
  • Reversibility: single source swap per component (~3 spots); ~1 hour to undo.
  • Blast radius: 3 components (VerdictStep red flags + Findings KPI, EvidenceStep); the graph visualization (nodes/edges/companies/persons/risk-map) is untouched.
  • Alternative considered: dedup/scope Finding nodes in the graph backend — rejected (the graph is deliberately bitemporal, ADR-0014; historical findings are a feature there, not a bug — the defect is only presenting the cumulative count as the current case's evidence).

Context

The Network Intelligence view derives several compliance-facing numbers from the entity-network graph (allNodes): the Verdict's red flags, its "Findings" KPI, and the Evidence tab's material/routine finding counts.

Finding (and FollowUpTask) graph nodes are created per-investigation and never deduped — the ETL (populate_knowledge_graph) ingests a fresh set on every run, keyed by case_id/investigation_id. This is correct for the bitemporal graph (ADR-0014), which keeps history. But the entity-network fetch (find_entity_network) returns all Finding nodes within N hops across every investigation, with no scoping to the current run.

Measured on the live graph (2026-07-21): OB Holding (reg 14975047) carries 1,097 Finding nodes across 52 distinct investigations (dev/test/demo runs plus production re-runs); the depth-2 network sees ~1,123 across 53 cases. So the Evidence tab reported "Showing 40 of 751 material findings. 1,239 routine findings … 742 follow-up tasks" — when the current investigation has only ~15 material / ~26 routine findings and 12 tasks. The number was ~40 recurring findings multiplied across 52 runs.

ADR-0117 had (a) unioned graph Finding/SanctionMatch nodes into the Verdict red flags to "never drop a graph-only signal", and (b) floored the Findings KPI with nodeTypeCounts.Finding. Both re-inject the accumulation: the union assumed the entity-network fetch was subject-and-run-scoped; it is subject-scoped but NOT run-scoped.

Decision

Source every finding-derived UI element from the current case record (caseDetail.investigation_results[].findings), not the accumulated graph:

  • Verdict red flags — critical/high findings from the case record only (drop the ADR-0117 graph union).
  • Verdict "Findings" KPI — the case record's finding count (drop the Math.max with nodeTypeCounts.Finding).
  • Evidence tab — material list, per-severity summary, routine count and follow-up-task count all from the case record; and the material table paginates (40 per page, "show more") instead of a hard 40-row cap.

The graph visualization (Network/Ownership steps, the companies/persons KPI, the risk map) continues to read the graph — those are entity-structure signals, and Company/Person nodes ARE deduped by identity (survivorship), so they do not accumulate.

Never-suppress holds without the graph union: the current investigation already covers the subject and its network (e.g. the network_adverse_association finding), so investigation_results is the complete, authoritative evidence for this decision. A graph-only finding is another run's artifact or another entity's own case — not this case's evidence.

Consequences

Positive

  • Finding counts are truthful and defensible: the Evidence tab and Findings KPI reflect the investigation the officer is actually reviewing (~15 vs 751).
  • Removes accumulation duplicates from the Verdict red flags (52 near-duplicate criticals could otherwise crowd the 6-slot list).
  • The Evidence table paginates — no material finding is silently truncated.

Negative

  • The Evidence tab no longer surfaces a related entity's own-case findings (e.g. a subsidiary investigated separately). Acceptable: those belong to that entity's case; this case's investigation already folds the relevant network signal into its own findings.
  • Two finding sources now exist conceptually (case record for evidence, graph for structure). Documented here to prevent a future re-union.

Neutral

  • The graph keeps accumulating history (by design, ADR-0014); nothing is deleted.
  • ADR-0117's other decisions (case-scoped view, entity-less empty-state, motif subject-scoping) are unchanged and remain in force.

Alternatives Considered

Alternative 1: Dedup / latest-run-scope Finding nodes in the graph backend

  • Collapse Finding nodes per entity, or have find_entity_network return only the latest investigation's findings.
  • Why rejected: the bitemporal graph (ADR-0014) intentionally retains historical findings for time-scoped and audit queries; deleting/hiding them there would damage a deliberate capability. The defect is purely presentational, so it is fixed at the presentation layer.

Alternative 2: Keep the union but scope graph findings to the current case_id

  • Filter allNodes Finding nodes to properties.case_id === caseDetail.case_id before unioning.
  • Why rejected: adds complexity and depends on case_id being present on every frontend node; and a current-run graph finding is already a copy of a investigation_results finding (same ETL source), so the union would add nothing the case record doesn't already hold.