Drivine Observed Schema Source
Drivine / Neo4j implementation of ObservedSchemaSource: asks a live graph what it contains, so a DeclaredObservedDiffer can compare it against what was declared.
There are two observation paths, because the database offers no single query that answers both:
Whole graph (
contextId == null) reads the database's own catalogue,db.labels()anddb.relationshipTypes(), keeps the labels that carry at least one node, and subtracts what dice owns from both sides. Ownership goes by node shape; see DiceOwnedSchema. A dice label the domain is also using stays in the observation, so an undeclared type can still be reported.
It then asks a second question, and reports the answer in its own set: the distinct Mention.type values on dice's own propositions, across the whole graph, returned as ObservedSchema.mentionTypeNames. A mention type is what an extractor claimed a span was, and it becomes a graph label only when something projects it, so a graph can hold live propositions mentioning Ghost while db.labels() has never heard of it. Reading labels alone left that type invisible to every unscoped check, which is what this query fixes.
One context (
contextId != null) cannot use those procedures: they have no notion of a context and answer for the whole database. It derives both sides from that context's own data:entity types are the distinct
Mention.typevalues on that context's propositions;relationship types come from the
sourcePropositionsproperty the graph writer stamps on every edge it persists, holding the ids of the propositions that produced it. An edge belongs to a context's set when at least one of those ids names a proposition in that context. It is a join on that property, and it does not collapse: an edge sourced from two contexts appears in both. An undeclared relationship type present in a context's data is drift in that context whoever else produced it.
Mention types are reported as extraction wrote them, on both paths. Dice's ownership rules cover Neo4j labels, while a mention's type is a domain type name an extractor produced, so the two live in different namespaces and subtracting one from the other would hide real drift from an app governing a type called Source.
Two kinds of name, kept apart
The whole-graph observation answers with labels in ObservedSchema.entityTypeNames, tagged ObservedSchema.EntityTypeBasis.GRAPH_LABELS, and mention types in ObservedSchema.mentionTypeNames. The differ then judges each by its own rule: a label against every label a declared type carries, so an inherited parent label of a governed type reads as declared, and a mention type against declared type names and their declared former names alone. Merging the two would have to pick one rule for both, and picking the label rule reopens what the mention rule exists to close — a mention typed Agent passing under a schema that governs Person with parent label Agent and declares no Agent type. An unscoped check and a context-scoped one now read mention types the same strict way.
A label with no nodes is no observation
The whole-graph label side counts only labels carrying at least one node. Neo4j's db.labels() is a catalogue of label tokens, and a token is minted the moment a constraint or an index names a label, before any node wears it — probed directly against the neo4j:2026.05 image this runs on, where a uniqueness constraint on an empty label puts that label in db.labels() for good. Constraint DDL is schema machinery an application declared; an observation reports what data the graph holds. So a type a host has declared constraints for and never populated is silently clean, which is the honest answer: there is nothing there to have drifted. The check costs one label lookup per label, each stopping at the first node it finds.
Two limits follow from working off names and shape:
Ownership is decided per label, and never per node. If any node wearing a dice label fails dice's shape, the whole label stays observed, dice's own nodes included, so a graph mixing a domain
Sourcewith dice's own reportsSourceevery run until the domain type is declared.Deciding it costs a scan of dice's own labels on every unscoped observation. Each probe stops at the first non-conforming node, so it is cheap only where one exists. Context-scoped checks don't pay it.
Parameters
Drivine's handle on the neo datasource.
What this application's own dice storage looks like, built from the DiceStorageSchema beans it registered. Required, with no default, because an observer that guessed at ownership would report some other slice's store as domain drift forever; whoever wires an observer has to say what dice owns in that application.
Supplies the snapshot's capture instant. Injectable because that instant ends up in a drift report's natural key, so a test has to be able to choose whether two checks record one observation or two.
Constructors
Functions
Carries its own Transactional annotation, which is why this override exists at all. ObservedSchemaSource.observe's default body, = observe(null), calls observe(contextId) on this from inside the bean's own compiled code — a self-invocation. Spring's proxy applies @Transactional advice only to calls that arrive through the proxy from outside the bean, so a caller invoking the interface's no-argument observe() on this class, absent this override, would reach observeWholeGraph with no transaction started at all, and its several queries would run as separate implicit transactions again, exactly what annotating the two-argument overload was meant to close.
Both branches issue several queries that get assembled into one ObservedSchema, and this annotation is what keeps them from running as separate implicit transactions: everything below runs inside one Neo4j transaction, the same pattern DrivineCollectorTraceStore.findEdgesByRun uses to rehydrate one answer out of more than one query. Where these queries run as separate implicit transactions, a concurrent graph write landing between two of them shows up in only one, the other having already run by the time it committed, and the resulting ObservedSchema describes a combination of graph states that existed at no single instant.