The Pacto Operational Graph¶
The operational graph answers questions that span many services: what depends
on payments-api, which revision runs in production-eu, is any of it failing its
contract and how sure are we? It joins contracts, their published revisions and
the places they run into one view that people, CLIs, platforms and agents can
query.
pacto fleet search # services found under .
pacto fleet graph payments-api --direction dependents # who depends on it
pacto fleet status # what needs attention
The graph only reads. It watches nothing and judges nothing: it collects
results other parts of Pacto produced. The code lives in pkg/fleet. Internally the frozen view is called a Fleet Snapshot and the
query layer over it a Fleet Query.
Three identities, never flattened¶
The graph models three distinct things and never collapses them into one: a name, a revision and a running instance are different questions with different answers.
| Identity | What it is | Example |
|---|---|---|
| Logical service | A stable name and owner. It has revisions and runs in targets, but it is neither. | payments-api (owner: payments) |
| Contract revision | An immutable resolved revision. Identity is the service plus a content digest: the source's immutable digest, or one derived from the whole bundle when the source has none. Never a ref, never a version; a revision that can be neither pinned nor hashed is omitted rather than given a weaker identity. | payments-api@sha256:… |
| Operational target | A concrete place a revision runs, generic as scope/kind/name. |
production-eu/customer-a → kubernetes-workload payments/payments-api |
How certainly a target is matched to a revision¶
Knowing a target exists is not the same as knowing which revision it runs, so every target records how the link was made.
| Match | What Pacto knows |
|---|---|
exact |
The target's content digest matches a revision's. Authoritative: this is the revision running there. |
inferred |
A unique correlation by mutable tag or version suffix. Probably right, not proof. |
ambiguous |
Several revisions match that mutable reference. No link is made — a guess is never presented as fact — and the target carries a REVISION_LINK_AMBIGUOUS limitation. |
unresolved |
Nothing matched, or the target's identity contradicts itself. No link, and a limitation on the target saying so. |
The bottom two sit on the target itself, so a consumer can classify a link without parsing snapshot-level messages. The two read surfaces spell it differently, and the CLI's spelling has a trap:
- Snapshot and
pacto fleet get --targetJSON carryrevisionMatch, which isomitemptyand only everexactorinferred. An absentrevisionMatchis the finding — it means ambiguous or unresolved. Read the target'slimitationsto learn which. - The entity-detail API (
GET /api/fleet/entities/target?key=…, what the dashboard reads) carrieslinkState, always present, with all four values.
Its inferred is a different word from the inferred relationship provenance
below: one is how a target was matched to a revision, the other how an edge
was derived.
Relationships: declared, observed, inferred¶
Edges carry a provenance discriminator so a fact's origin is never ambiguous:
- declared — from a contract (
dependencies[], and config/policyrefs). - observed — seen in running traffic, kept in a separate adjacency index from the declared graph so the two can never leak into each other's answers.
- inferred — deduced heuristically. Reserved, not yet produced.
- declared+observed — one edge backed by both. The neighborhood projection merges the two indexes for display and emits this combined value, so a consumer branching on provenance must handle all four.
Sources¶
The graph is assembled from sources, each contributing the revisions and targets it can observe right now. The dashboard lists them as Data sources. A source is where the graph reads records from, not a collector, which produces the compliance evidence a source then carries.
- Local bundles (
--local) — the revision a developer is editing. The scan defaults to the working directory, descends 8 levels and skips hidden directories,node_modulesandvendor. A directory the operating system refuses is reported as a gap and stepped over. - A contract root and its closure (
--root <path|oci://ref>) — the root you name plus every revision it declares, followed transitively. It is the same discoverypacto mcp --rootfreezes into a catalog, through the same resolver. Roots and dependencies that do not resolve stay visible as limitations rather than vanishing. - Contracts in OCI (
--oci <ref>) — the published revision catalogue, resolved cache-first so a pulled ref works offline. - Local OCI cache (
--cache) — every bundle already pulled to disk. Opt-in: on a machine that has been pulling contracts for months the cache is a record of everything anyone ever fetched, which is an offline baseline worth asking for and a fleet nobody operates. - Live Kubernetes (
--k8s [--namespace]) — Pacto CRs read from a running cluster: which revision runs in which target, and its operator-computed compliance, findings, coverage and observed runtime. - Ingested external evidence (
--evidence-url <url>) — a running Evidence Server's read-only contribution over HTTP, exposing a remote environment's signedEvidenceSetreports as operational targets. - Offline target-state fixtures (
--target-state) — an unsigned demo and test adapter for supplying targets without a cluster. - Offline trace exports (
--traces, or--trace-source NAME=PATHonpacto dashboardonly) — observed dependency edges rather than revisions and targets. Pacto ships no live OTLP receiver: nothing listens on 4317 or 4318, and no collector ships with the dashboard. Run a Collector you own and point a source at the file it exports.
pacto fleet and pacto impact read every source flag above. pacto mcp
--fleet reads all but --root, which on pacto mcp selects a frozen catalog
and cannot be combined with --fleet. Two are narrower: impact takes one
--traces file, not a repeatable list, and --trace-source is pacto dashboard
only.
What a target-state fixture looks like¶
--target-state is the only source you author yourself: a single YAML or JSON
document. A second --- document, an unknown field or a schemaVersion other
than pacto.dev/fleet-targets/v1 is rejected.
schemaVersion: pacto.dev/fleet-targets/v1
targets:
- service: orders-service # required: the service this target runs
name: commerce/orders-service # required: unique within the scope
scope: production-eu # the environment, e.g. a cluster
kind: kubernetes-workload # what sort of target it is
labels: { env: production, region: eu }
requestedRef: oci://ghcr.io/acme/orders-service:1.2.0
resolvedRef: oci://ghcr.io/acme/orders-service:1.2.0
digest: sha256:…
compliance: NonCompliant
coverage: { evaluated: 5, required: 5 }
evidenceAt: 2026-07-29T09:40:00Z # when the evidence was gathered
reconciledAt: 2026-07-29T09:41:00Z # when the target was last reconciled
observedRuntime: { replicas: 3 } # free-form; surfaced as a bounded preview
findings:
- code: STATELESS_PERSISTENT_CONFLICT # required within a finding
severity: error
category: RuntimeDrift
subjectKind: state
subjectName: orders-db
message: declared stateless but the observed workload mounts a volume
state: # optional: the source's own health
status: available
message: ""
Only service and name are required, and an omitted evidenceAt is exactly how
you model a target the collector could not observe. A file may declare at most
5000 targets. An entry that fails validation is skipped with a
SOURCE_RECORD_INVALID limitation and the rest is kept; a failure of the file
drops the whole source with SOURCE_UNAVAILABLE and marks the snapshot
partial. Three fields are closed sets:
compliance—Compliant,NonCompliant,Unknown,Invalid,Reference,NotEvaluated,Warningor emptyfindings[].severity—error,warning,info,unknownor emptystate.status—available,stale,partialorunavailable. Unlike the other two, a value outside this set is silently coerced toavailable, so a typo here reads as a healthy source rather than a skipped record
A malformed fixture is reported the same way as a missing one
Both produce exactly SOURCE_UNAVAILABLE, and the parse error itself is not
surfaced — -v does not add it. If the source drops and the path is right,
suspect the file: check schemaVersion first, then field spelling and the
closed sets above.
examples/demo/fleet-targets.yaml
is a complete worked fixture, and it is the file the live demo runs on.
Knowledge¶
Every Pacto answer carries how much of the world it actually saw. The words below are not degrees of the same thing — they are different claims.
| Word | Claim |
|---|---|
| complete | Every source answered. Nothing is missing. |
| empty | Every source answered and there is genuinely nothing. This is complete knowledge of an empty result. |
| partial | At least one source was unavailable, stale or itself partial. What you see is a floor, not a total. |
| stale | Every source answered, but one of them last saw the world a while ago. Its records are real and may have moved on since. |
| unavailable | A source did not answer at all. Whatever it knows is missing from this answer entirely. |
| unknown | We never received a completeness we could assert. Not the same as empty. |
Three of these travel on the wire, in every answer's meta.completeness:
complete, partial and empty. The other three are a consumer's reading of the
same envelope — stale and unavailable come from the per-source health reported
alongside it, and unknown is what is left when no envelope arrived at all. The
dashboard takes the worst of the six and gates every all-clear on that: a source
that is down must not be masked by the word the snapshot chose for itself.
An unavailable source is never an empty result, and a partial answer with zero rows is not an empty result set — it is a set Pacto could not finish building.
Query semantics¶
The read model answers five kinds of question, each a pure operation. None performs I/O, and a single snapshot serves concurrent queries.
| Query | Answers |
|---|---|
| search | Which logical services match this filter (owner, label, status, compliance, capability, dependency, readiness). Bounded and deterministically ordered. |
| get | Everything about one service (its revisions, targets, declared dependencies, dependents, tools and skills) or one target. |
| graph | Traverse dependencies or dependents from a service — direct or transitive, cycle-safe, with unresolved edges surfaced. |
| status | What needs attention: non-compliant or unknown targets, invalid contracts, stale evidence, missing readiness, unresolved dependencies. |
| explain | Deterministic, structured reasons for a subject's state. Pacto embeds no model — it hands an agent structured reasons to turn into prose. |
Two more operations are not queries: snapshot emits the whole read model and
reconcile compares declared dependencies with observed ones. Every answer
carries a meta envelope, here from
pacto fleet search --output-format json with an unreachable OCI source:
{
"meta": {
"schemaVersion": "pacto.dev/fleet/v1",
"snapshotId": "sha256:c81df8fd572ecaa7c884968e728daff5b47c40b8e8c5cbcf1926c19740db95a9",
"asOf": "2026-07-29T10:00:00Z",
"completeness": "partial",
"limitations": [
{ "code": "SOURCE_UNAVAILABLE", "source": "oci",
"message": "source oci is unavailable; its records are missing from this snapshot" }
],
"sources": [
{ "id": "local", "kind": "local", "status": "available",
"lastSuccessfulSync": "2026-07-29T10:00:00Z",
"revisionCount": 25, "targetCount": 0 },
{ "id": "oci", "kind": "oci", "status": "unavailable",
"error": { "code": "UNAVAILABLE", "message": "the source is unavailable" },
"revisionCount": 0, "targetCount": 0 }
]
},
"total": 1,
"count": 1,
"services": [
{ "key": "payments-service", "name": "payments-service",
"owner": "team/payments", "status": "NotEvaluated",
"revisionCount": 6, "targetCount": 0, "sources": ["local"] }
]
}
schemaVersion is the compatibility contract to branch on and snapshotId the
content digest that proves two answers came from the same system view. On this
envelope count is how many rows the page carries and total the whole matched
population; key is the canonical identity to match on because name is not
unique across domains. A bounded preview nested in a get answer is the
exception: it omits total when the walk was itself bounded, so an absent
total means "we stopped counting", never "there are none".
Beside the rows, the dashboard's entity list and the TUI carry an aggregate over the complete matched population, computed before paging:
| Tally | Partitions | Buckets |
|---|---|---|
serviceCompliance |
matched services | compliance states, rolled up from each service's targets |
targetCompliance |
matched operational targets | compliance states as observed per target |
ownership |
matched services | consistent · conflicting · unowned |
readiness |
matched contract revisions | passing · belowThreshold · expired · notDeclared |
notDeclared is its own readiness bucket because "nobody wrote an assessment" is
not "the assessment does not pass".
get, graph and explain name a single subject and carry no envelope: a
missing subject is a failure. pacto fleet get ghost exits 1
with service "ghost" not found in the fleet snapshot on stderr. With no meta
there is no completeness, so read that from search before reading a subject
miss as an absence.
Ownership and the canonical owner key¶
The graph aggregates and navigates by a canonical owner key namespaced by which
field named the owner, written kind:name:
- If owner has
team→team:<team> - If owner has
dri(no team) →dri:<dri> - If owner has neither (contacts only) → no canonical key. The service is still owned and counted as such, but there is no owner to rank or link to.
The namespace is part of the identity: team:payments never resolves to
dri:payments, and only the name is shown on screen, with a Team / DRI badge
where two owners would otherwise be indistinguishable. The separate free-text
owner filter is a human search over team, DRI and contacts — deliberately not
an identity, and it may match several owners at once.
Who consumes it¶
A human portal and an agent consume the same graph.
flowchart LR
subgraph Sources["Sources"]
OCI["Contracts in OCI<br/>published revisions"]
LOCAL["Local bundles<br/>revision being edited"]
K8S["Live Kubernetes<br/>Pacto CRs: which revision runs where"]
EVI["Evidence Server<br/>signed EvidenceSet reports"]
end
OTEL["OTel trace file<br/>offline analysis"]
OCI --> OG
LOCAL --> OG
K8S --> OG
EVI --> OG
OTEL -->|--traces| OG
OG["Operational Graph<br/><i>Fleet Snapshot · immutable read model</i>"]
OTEL --> RECON["reconcile · impact<br/>declared vs observed"]
OG --> RECON
OG --> Q["Fleet Query<br/>search · get · graph · status · explain"]
Q --> DASH["Dashboard"]
Q --> CLI["CLI<br/>pacto fleet …"]
Q --> MCP["MCP fleet tools"]
Q --> PLAT["Platforms"]
Q --> AGENT["Agents"]
- Dashboard — the visual front door. It builds one snapshot from every source
it detects and serves the graph and change analysis through
/api/fleet/*. The endpoints the UI reads — overview, entities, neighborhood, attention — carry their ownschemaVersion,pacto.dev/fleet-product/v1, so a UI versions independently of thepacto.dev/fleet/v1snapshot export above. The Operational Graph view offers three perspectives — Services, Revisions and Operational targets — and a Knowledge control (Expected · Observed · Differences). It is honest about what it cannot know. An operational target links to the dependency service it depends on, never to each peer target, because a full target-to-target mesh would assert runtime routing the snapshot never observed. - CLI (
pacto fleet …) — the five queries on the command line, plusreconcileandsnapshot. Scriptable, deterministic output. - MCP fleet tools —
pacto_fleet_search,pacto_fleet_get,pacto_fleet_graph,pacto_fleet_statusandpacto_fleet_explain, pluspacto_impact, give an agent read-only understanding of the operational system. See MCP integration for how they differ from authoring tools and generated service tools.
A read model around many evaluations, not a new evaluator¶
Compliance is still the pure Evaluate(Contract, EvidenceSet) function producing
findings for one service in one environment (see the Pacto
model). The graph is the read model around many such
evaluations — it references each target's findings and coverage rather than
re-computing them.
flowchart TB
subgraph Eval["Per-target evaluation (the engine — one at a time)"]
C["Contract"] --> EV["Evaluate"]
E["EvidenceSet"] --> EV
EV --> F["Findings + Coverage"]
end
C -.-> OG
F -.-> OG
OG["Operational Graph<br/>composes many revisions + many targets"]
OG --> ANS["Query answers<br/>with asOf · completeness · limitations"]
The graph also maintains a reverse-dependency index, which is what impact analysis traverses to answer "if this revision ships, which consumers are affected, directly and transitively".
Observed dependencies and reconciliation¶
Declared intent is half the picture; the other half is what traffic actually does.
The OTel observer (pacto otel observe <traces.json>) is an offline analyzer:
it reads an exported OTLP/JSON trace file and derives the caller-to-callee edges
its outbound spans prove. It never asserts a dependency is absent. It can also
emit signable EvidenceSets (pacto otel observe --evidence) so observed
dependencies travel the same evidence protocol as any
other report.
Those observed edges meet the declared graph in three places:
- The snapshot itself —
pacto fleet --traces <file>andpacto dashboard --tracesresolve each raw observed endpoint name to a unique domain-qualified service and fold resolved edges in asobservedrelationships. A name that matches zero or more than one service is never coerced to a domain; it is preserved as anOBSERVED_IDENTITY_UNRESOLVEDlimitation, so observed traffic can never be misattributed across domains. - Reconciliation —
pacto fleet reconcile --traces <file>labels each dependency matched, declared-not-observed (dormant or simply unseen in the window) or observed-not-declared (a shadow dependency the contract never mentions). Anything unresolvable is reported in a distinct unresolved category rather than force-fit to the default domain. - Impact —
pacto impact --traces <file>feeds the same observed edges into the affected-consumer list; the confidence model has what they change there.
Reconciliation is an explicit backend fact, not a frontend guess: every declared
edge carries a state computed against the snapshot's observed edges. A snapshot
with no observation data returns insufficient and says so, rather than drawing
an empty Observed view that would read as "there is no traffic".
Why the fleet is not a new contract kind¶
There is no kind: Fleet and no fleet: section in a contract. The graph
is discovered from the sources you already have. A fleet manifest would recreate
the problem Pacto exists to remove: a hand-maintained aggregate that drifts the
moment a service is added. The fleet is a view, computed on demand and versioned
by its as-of time.
See also¶
- The Pacto model — the engine, the compliance states and the boundaries the graph inherits
- Impact analysis — the consumers a change affects, projected onto this graph
- Fleet tools — the dashboard,
pacto fleetand the terminal UI - MCP integration — the three MCP tool families