0.14.0¶
Released 2026-08-17.
Reachability, and the first deprecation. A fourteenth entity type makes why can't A reach B a graph question, and the 1.0 freeze audit opens by naming what will not survive it.
No wire-contract break, no data migration. Everything is additive except one log-format change, called out below.
The network segment¶
Two workloads on the same segment can address each other; two on different segments cannot, whatever the firewall says. That boundary is the first question a deployment incident asks, and the graph could not answer it at all — none of the network types named it, so producers carried overlays as metric labels: queryable, not traversable.
service.instance --has_segment--> network.segment (the cluster declares it)
container --attached_to--> network.segment (the workload joins it)
Verified on a real four-node Swarm before shipping:
api attached_to -> ingress, frontend
cache attached_to -> backend
find_path(api -> cache, attached_to) -> reachable: false
One hop. That is what the type exists for.
Only one identity subtype is frozen¶
Identity is a subtype-prefixed value by precedence, like network.device.id, and
only swarm:<network-id> is frozen — the cluster assigns it.
k8s: and vlan: are deliberately left open, which is the decision rather than
an omission. Kubernetes has no object to identify in the default flat-network
case: a NetworkPolicy is a restriction that composes, a Namespace is an
administrative boundary the network ignores. And a VLAN id is not globally
unique, while a VLAN trunked across five switches is one segment rather than
five. An identity engraved wrongly costs a migration; an open one costs a
sentence in the contract, and the sentence says what would have to be true for it
to close.
What the edges do not promise¶
Do not alert on segment-failure propagation
attached_to's impact direction states a dependency, not that an event
will arrive. An overlay is a control-plane construct: it does not fail on its
own, and no producer can mark a segment down. impact_of stays meaningful
over a hypothetically failing segment — if this overlay broke, what goes
with it is a real question during a fabric change — but nothing will
propagate at runtime.
The alertable event exists and sits on the edge: a container losing its attachment is observable and reported by the runtime.
Membership is also necessary and not sufficient for reachability. Network policies restrict on top of it, and on a flat cluster network shared membership says almost nothing.
A segment is emitted with its has_segment edge or not at all — a segment
nobody can say whose it is has no business in the graph.
See ADR 0034.
The 1.0 freeze audit opens¶
After 1.0 the stability policy binds us to whatever the surfaces look like. The audit asks of each item whether we would defend it in three years — and the four first findings do not all get the same treatment, which is the substance.
Deprecated: three legacy relation types¶
routes_via, forwards_to and adjacent_to were superseded under
topology-as-entities (ADR 0022) and documented as "do not emit" since. They
will be removed in 1.0.
They remain accepted and still appear in wire.RelationTypes() — that list states
what the boundary accepts, not what a producer should emit — and now carry a Go
Deprecated: marker, so an IDE or linter flags remaining use. Replacements:
| Deprecated | Use instead |
|---|---|
routes_via |
network.route + has_route + next_hop_via |
forwards_to |
connected_to to the learned port |
adjacent_to |
port-to-port connected_to |
This notice is what makes the removal possible: the policy requires a deprecation in a release preceding the removal.
ENTITY_IDENTITY_CHANGED is never emitted¶
Not deprecated — it cannot be removed. It is retained to replay logs written before exact identity matching, under which an identity change is a different entity. But the change taxonomy listed it with no hint that nothing produces it, so a consumer could reasonably have written a handler for an event that will never arrive. Now stated in the schema and the guide.
recentChanges takes an optional window¶
It defaults to 1h, matching the MCP recent_changes tool where it was already
optional with that default. Backward compatible. Freezing two surfaces that
answer the same question differently is what the audit exists to prevent.
Parity between MCP and GraphQL is defined¶
The surfaces are not meant to be feature-equal: GraphQL is the typed query surface, MCP adds traversal and interpretation. What is promised is narrower and more useful — where both answer the same question, they answer it identically: same walk, same thresholds, same defaults. A new MCP tool does not oblige a matching GraphQL query.
The contract¶
pkg/emit/wire staying stdlib-only is now enforced¶
A producer that already owns its OTLP transport must be able to take the vocabulary without the protocol stack — that is what let the reference producer adopt the contract while keeping its single OTLP rail. The promise lived in a doc comment and nothing enforced it; a test now fails on any import outside the standard library.
Measured, not assumed: a module importing only wire requires pkg/emit and
nothing else after go mod tidy. Module-graph pruning keeps grpc and pdata out.
entity.delete.reason is a distinct axis from delete_source¶
delete_source says who authored the disappearance and Toise writes it;
entity.delete.reason says why the producer decided and the producer writes it.
Producer values are not a subset of the source's — that would restate a fact
the consumer owns, and drift as soon as Toise gains a fourth source.
The enum stays open, and gains a recommended core. Six values say the resource
ended — terminated, evicted, scaled_down, user_requested, expired,
parent_removed. One says the observation did:
unmonitored — the resource may well still be running
A probe removed, or a target that left the scope, says nothing about the
resource. A database entity that disappears because someone edited a probe
list must not read as "the database is gone" — that is the reading that
causes an incident. It is the producer-side mirror of liveness_expiry:
there Toise says I stopped hearing, here the producer says I stopped
looking.
parent_removed is deliberately not spelled cascade, which belongs to the other
axis and means Toise removed an edge whose endpoint died.
Upgrading¶
Nothing to do. Existing types, relations and stored events are untouched.
One log-format change, if you parse it
Carried over from 0.13.0: the liveness sweep line is
liveness sweep expired stale records with entities= and relations=
separately, rather than a single count= under a label that said entities.