Skip to content

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.