live + empty when the matter is genuinely quiet โ
never the ambiguous silence of a stub.
The Context Engine has four layers; L3 (Validation) is the graph of record โ
assertions, contradictions, chronology, evidence โ living on Neptune
(RFD ยง5.3). CE-8 built the substrate: the openCypher client, the
closed label/edge registry, and the one tenancy-scoped helper
(ScopedGraph) that is the only door to the graph. CE-9/10/11
(still landing) write nodes and edges. This story, CE-12,
reads them: three of the ONE tool catalog's seven executors
(RFD ยง8), backing the brain-facing DTOs that CE-1 already froze.
| Executor | Consumer sentence (RFD ยง8) | Reads |
|---|---|---|
find_contradictions(graph, target, k) |
critic: "does anything contradict this?" | CONTRADICTS edges touching target + the grounding spans of both assertions |
get_chronology(graph, spans, date_from?, date_to?, k) |
proposer: "what happened, in order, between the notice and the filing?" | (:Event) nodes ordered by the PRECEDES (date-order) basis |
get_assertion_evidence(graph, spans, assertion_id) |
critic: the Why chain โ assertion → edges → spans, with a support roll-up | the (:Assertion) node, every edge touching it, and its spans |
Every executor takes a ScopedGraph โ bound to one
(firm_id, matter_id) at construction, and it never accepts a
firm/matter argument on a method. Neptune has no RLS, so this helper
is the tenancy story: the firm/matter predicates come from the bound
scope, never from caller input, so a caller cannot widen its own reach. The
tool signatures therefore carry no matter_id to disagree with the
scope โ the caller resolves can_read_matter before it ever
builds the scope (ยง8's three authz layers: app-route dep, broker
re-resolution, tenancy helper). The cross_matter_scope_sees_nothing
test proves it: seed under matter A, read through a matter-B scope on the same
connection, get nothing.
Each Lambda invocation talks to the graph over the network, so what must stay bounded is the number of round-trips, not the rows per trip. Every executor fetches whole matter-scoped sets โ one trip per fixed edge type, never one per row (DR-2) โ and joins them in memory:
find_contradictions : 1 edge fetch (+ 1 grouped node fetch when spans wanted) get_chronology : 1 node fetch get_assertion_evidence: 1 node get + 1 fetch per edge TYPE (4) = 5
The query_count_is_bounded_regardless_of_row_count test pins
this: 2 events vs 20 events both cost 1 graph call; 1 edge vs
15 edges both cost 5. Traversal depth is โค 1 hop, well inside
the ยง5.3 โค 3 bound; k caps the result set.
live + empty is a real answer, stubbed is not
The backing is built (CE-8), so these executors never return
stubbed. A quiet matter is live + empty. The only
downgrade is partial, and only for an honest reason: a known
event/assertion whose spans cannot be hydrated is dropped,
not emitted spanless โ "an unspanned assertion does not exist" (ยง5.3). A
fabricated span could be cited into a real brief, so the tool would rather
admit a degraded leg than invent evidence.
Think of the graph node as an index card that lists call numbers
(span_ids), not the book pages themselves. The byte-exact
SourceSpan bodies live in document_pages and are
resolved by CE-2's helper. So the executors that must emit spanned DTOs take a
SpanResolver the caller injects (not imports) โ this story ships
the traversal and the seam; CE-2 slots in the real byte-exact resolver. That
is also why CE-12 depends only on CE-8: it reads the graph, and hands span
bodies off to a collaborator.
Neptune's properties(n) returns the business key (it is part of
the MERGE pattern), but the in-memory test seam was dropping it โ
so match_nodes returned events/assertions with no id. CE-12 is the
first match_nodes consumer, so it surfaced here. Fixed in the seam
(client.py now stores the key field, fed by the helper) so the
local and real clients agree.
uv run ruff check # clean uv run mypy src/shared/context/graph_read_tools.py # clean bash .github/scripts/graph-tenancy-gate.sh . # passed (no raw cypher outside the package) uv run pytest tests/unit/context tests/unit/graph tests/unit/context_contracts # 92 passed
No route/DTO changes (contracts were frozen by CE-1), so no OpenAPI snapshot regeneration and no migration.
CE-12 reads the graph of record through the one tenancy-scoped door in
a constant number of round-trips, and answers live + empty for a
quiet matter so "nothing contradicts you" can never be a silent stub.