Skip to content

Add /graph — a knowledge graph over a query's result set - #37

Merged
barkz merged 1 commit into
mainfrom
feat/knowledge-graph
Sep 12, 2026
Merged

barkz merged 1 commit into
mainfrom
feat/knowledge-graph

Conversation

@barkz

@barkz barkz commented Sep 12, 2026

Copy link
Copy Markdown
Owner

A list of search results says what matched. It doesn't say what the material is, how it hangs together, or who owns it. /graph answers that.

/graph "quarterly planning"
/graph "checkout incident" --html incident-graph.html

Why it's built locally

The Client API has no graph endpoint. What it does return on every result is the author, datasource, container and snippets — enough to build the graph client-side. Three consequences, stated in the docs rather than glossed over:

  • It's the graph of one query's result set, not a crawl of the index; --page-size is the size of the graph.
  • It works identically in mock, local and live, because all three return that same shape. No endpoint to stub, no mock shape to keep in sync.
  • Nothing leaves the process — graph, layout and HTML are all local.

The model

Node From Edge Meaning
doc a result authored_by docperson
person metadata.author in_source docsource
source metadata.datasource in_container doccontainer
container metadata.container shares_term docdoc, weighted by rarity

shares_term is the one worth having. Title and snippet vocabulary is weighted by how rare each term is within the result set, so terms carried by most of the results — usually the query itself — are dropped before scoring. What's left is what actually separates one pair of documents from the rest, and every edge names its shared words:

strongest content links
  Quarterly Access Review Procedure
    ↓ shares account, long-lived, access, service (5.1)
  SEC-233 — Rotate service account keys before audit window

So shares capacity, model, fy26 (4.7) can be judged rather than believed — the same principle /flow links on.

Two renderers

Terminal — counts by kind, hubs by degree, connected clusters with the node anchoring each, and the strongest content links with their evidence.

--html — a self-contained force-directed page under the same rule as /flow timeline: no CDN, no framework, no network. Deterministic ring seeding so the same graph opens the same way, nodes coloured by kind and sized by degree, click a node for every edge it has with evidence and score. The view frames the graph while it relaxes and yields the moment you pan. prefers-reduced-motion draws the settled layout directly.

Two bugs the tests found

  • Zero-weight edges. A term carried by every document scores log(1) = 0, which drew edges between documents sharing nothing but the query. Those terms are dropped, and a non-positive score no longer draws an edge.
  • XSS through document titles. The detail panel built markup from titles and edge evidence — tenant content — via innerHTML. Everything interpolated is escaped now, and a document link is only rendered when its url is really http(s), so a javascript: url in a connector's metadata can't become a live link.

Tests

1,109 → 1,148, in tests/test_graph.py: author dedup across documents, structural vs term edges, the rarity filter, --min-shared, empty and malformed results, summary ordering, cluster detection, terminal truncation at width, HTML self-containment, deterministic layout, the escaping above, and the command itself in mock mode.

Docs: docs/GRAPH.md for the model and scoring, an entry in docs/COMMANDS.md, a DOCS entry so /help graph and the NL planner both pick it up automatically, and two README rows.

🤖 Generated with Claude Code

A list of search results says what matched. It does not say what the
material is, how it hangs together, or who owns it. /graph answers
that: documents, their authors, their datasources and their
containers, with an edge for every relationship and the evidence that
produced it.

Glean's Client API has no graph endpoint, so the graph is synthesised
from the search response — which already carries author, datasource,
container and snippets. Three consequences, all stated in the docs
rather than glossed: it is the graph of one query's result set and not
a crawl of the index, it works identically in mock, local and live
because all three return that shape, and nothing leaves the process.

Four node kinds (doc, person, source, container) and four edge kinds.
Three are structural. The fourth, shares_term, is the one worth
having: title and snippet vocabulary weighted by rarity within the
result set, so terms carried by most of the results — usually the
query itself — are dropped before scoring, and what is left is what
actually separates one pair of documents from the rest. Every edge
names its shared words, so "shares capacity, model, fy26 (4.7)" can be
judged instead of believed. Same principle as /flow's links.

Two renderers: a terminal summary (counts by kind, hubs by degree,
connected clusters, strongest content links) and --html, a
self-contained force-directed page under the same rule as
/flow timeline — no CDN, no framework, no network. Deterministic ring
seeding, so the same graph opens the same way; the view frames the
graph while it relaxes and yields the moment the reader pans.

Two bugs the tests found on the way:

- A term carried by every document scores log(1) = 0, which drew
  zero-weight edges between documents sharing nothing but the query.
  Those terms are dropped and a non-positive score no longer draws an
  edge.
- The detail panel built markup from document titles and edge
  evidence — tenant content — through innerHTML. Everything
  interpolated is escaped now, and a document link is only rendered
  when its url is really http(s), so a javascript: url in a
  connector's metadata cannot become a live link.

Tests: 1,109 -> 1,148.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@barkz
barkz merged commit 882aeb8 into main Sep 12, 2026
7 checks passed
@barkz
barkz deleted the feat/knowledge-graph branch September 12, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant