Skip to content

Impact analysis (pacto impact)

A semantic diff tells you how a revision changed. The operational graph tells you who depends on the service and where it runs. Impact analysis composes the two to answer the question a reviewer actually asks before merging: if this revision ships, what is the real blast radius?

Impact is framework-independent (pkg/impact). It consumes the pure diff engine (change classification) and the immutable operational-graph read model, and imports no Kubernetes, OCI, dashboard, MCP or HTTP code. The same analysis therefore backs the CLI, an MCP tool and the dashboard, and given the same snapshot every one of them returns the identical answer. What differs between the three is where the snapshot may come from: the CLI takes contract sources only (which sources, exactly), while the MCP server and the dashboard can also be pointed at a cluster or a live fleet. impact is the name of the CLI command, the MCP tool and the Go package; in the dashboard the same analysis is presented as the Change analysis workspace, alongside the semantic diff it composes with. This page is the CLI; the other two surfaces are in Impact on the other surfaces.


Diff × graph → impact

flowchart LR
    OLD["Old revision<br/>contract + files"] --> DIFF["Semantic diff<br/>pkg/diff"]
    NEW["New revision<br/>contract + files"] --> DIFF
    DIFF --> CLASS["Classification<br/>NON_BREAKING · POTENTIAL_BREAKING · BREAKING<br/>breaking and potentially-breaking, kept separate"]

    SNAP["Fleet Snapshot<br/>pkg/fleet · immutable read model"] --> GRAPH["Dependents traversal<br/>direct + transitive"]

    CLASS --> IMPACT["Impact result"]
    GRAPH --> IMPACT
    IMPACT --> CONS["Affected consumers<br/>compatibility verdict · confidence"]
    IMPACT --> TARGETS["Active targets<br/>where the change lands"]
    IMPACT --> OWNERS["Owners to notify"]

The diff is computed once over the old→new revision. The changed service is then looked up in the operational graph and traversed in the dependents direction, direct and transitive. Each dependent becomes an affected consumer, annotated with the evidence Pacto actually has for that edge. The result also rolls up the active targets the change would land in and the owners to notify.

Every answer inherits the snapshot's asOf time, completeness and limitations, so a partial fleet is never presented as a complete blast radius. If the changed service is not present in the graph at all, the result says so with a SERVICE_NOT_IN_FLEET limitation rather than reporting an empty blast radius.


What an affected consumer carries

For each dependent the analysis records:

Field Meaning
service / domain / owner Who is affected and who owns them.
depth / direct / path depth 1 is a direct dependent, >1 is transitive. path is the dependency chain from the consumer to the changed service.
required Whether the consumer declared this dependency as required.
compatibility The consumer's declared compatibility range against the changed service.
compatibilityVerdict Whether the new version satisfies that range (see below).
provenance Where the edge came from: declared, observed, declared+observed or inferred.
confidence How strongly the evidence supports the claim (see below).
status / targets The consumer's aggregate status and the operational targets it runs in.

Compatibility verdict

The verdict checks the new version against the consumer's declared compatibility range:

Verdict When
compatible The consumer declares a range and the new version satisfies it.
incompatible The consumer declares a range and the new version does not satisfy it.
unknown No declared range, or a range or version that cannot be parsed. Absence of a range is not a pass — it is uncertainty.

Confidence model

Confidence grades how strongly the available evidence supports each affected-consumer claim.

Confidence Exact meaning
contractual A declared dependency with a usable compatibility range. The consumer's own contract says it depends on this service and pins the versions it accepts.
declared A declared dependency without a usable compatibility range. The dependency is stated, but no version constraint was pinned, so a compatibility verdict cannot be computed.
observed Runtime use of the dependency was observed in a window. Requires --include-observed.
corroborated The declared dependency and an observed one agree — the strongest grade, contract and runtime saying the same thing.
inferred A transitive effect reached through another affected service (depth > 1). It follows from the graph, not from a direct declaration or observation.
unknown A direct edge with no declaration and no observation — the effect is possible but unverified.

Two rules follow directly from this model and are load-bearing:

An inferred path is not a confirmed runtime impact. A transitive consumer is reached through the graph. It tells you where to look, not that the consumer will break. Treat inferred as a lead to verify, never as a settled fact.

Observed evidence only raises confidence when you opt in. Without --include-observed the analysis is declared-only. Runtime observations then let a direct edge become observed or corroborated.

Observed edges come from OpenTelemetry traces via --traces <file> (which implies --include-observed). Beyond corroborating declared consumers, traces surface observed-only (shadow) consumers — services seen calling the changed service that never declared the dependency. A declared-only analysis cannot see them; with traces they appear as direct consumers at observed confidence, so a release check is not blind to undeclared traffic.


CLI

pacto impact <old> <new> --local .

<old> and <new> are the two revisions to compare — bundle paths or refs.

pacto impact takes a subset of the source flags pacto fleet accepts: --local (repeatable, defaults to .), --root, --target-state and --traces. There is no --k8s, --oci, --cache or --evidence-url here, and passing one fails with unknown flagno live fleet, no cluster and no registry catalogue. To analyse a fleet you do not have on disk, pull those bundles first with pacto pull and point --local at the directory.

The one source that can reach the network is --root, and only where you point it at one: it follows a bundle's declarations, so a root that depends on oci://ghcr.io/acme/payments:2.1.0 resolves that reference rather than leaving a dangling edge. Point --root at local paths whose closure is also local and the whole analysis stays offline. The two revisions being compared are separate from the fleet snapshot and may be oci:// references either way.

Turn on runtime corroboration with --include-observed, or supply an OTLP/JSON trace export with --traces (which implies it) so observed traffic raises consumer confidence and surfaces shadow consumers:

pacto impact ./payments-api@1.4.0 ./payments-api@2.0.0 \
  --local ./services \
  --traces ./traces.json

The output reports the classification, the breaking and potentially-breaking changes (kept separate — a potential break is never counted as a confirmed one), every affected consumer with its compatibility verdict and confidence, the active targets and the owners to notify — along with the snapshot's completeness and any limitations.

Exit status: what makes a consumer active

pacto impact exits 1 only when both halves are true — the change is BREAKING and at least one incompatible consumer is active. Anything else exits 0, including a run that prints Classification: BREAKING and a list of consumers every one of which says compat=incompatible.

Active means the snapshot knows of somewhere that consumer is deployed — at least one operational target. Compatibility is a statement about contracts; active is a statement about the world. A consumer that is incompatible on paper but is running nowhere the snapshot can see is a review item, not a release blocker, so it does not fail the command.

The consequence catches people out, because --local defaults to . and local bundles declare no targets: a declared-only run can never exit non-zero.

$ pacto impact ./api-v1 ./api-v2 --local ./fleet
Classification: BREAKING
Affected consumers (1):
  web    direct   confidence=contractual  compat=incompatible  owner=frontend
$ echo $?
0

The exit is non-zero only when there exists at least one consumer that is BOTH incompatible and has at least one active target. Give the snapshot a source that knows where things run and the same command blocks:

$ pacto impact ./api-v1 ./api-v2 --local ./fleet --target-state ./targets.yaml
Classification: BREAKING
Affected consumers (1):
  web    direct   confidence=contractual  compat=incompatible  owner=frontend
Active targets (1): [production/kubernetes-workload/shop%2Fweb]
breaking changes affect active consumers
$ echo $?
1

Gating in CI

In CI that means one of two deliberate choices. Gate on the exit code and you are gating on deployed impact, which is what you want in a promotion pipeline — but only if the job actually supplies targets. Gate on the JSON instead (--output-format json, then classification == "BREAKING") and you are gating on the contract alone, which is what you want before anything is deployed at all. That JSON carries schemaVersion: pacto.dev/impact/v1 — the compatibility contract to branch on before reading any other field. The file --target-state expects is documented under target-state fixtures; pacto impact accepts no source that observes where things run, so that file is the only way to make the exit code mean anything.


It recommends review, it does not act

Impact analysis lists what to review. It never recommends or performs an autonomous action, and it never authorizes one.

  • It does not act. Rolling back, blocking a deploy, paging an owner or gating a pipeline is done by external controllers and delivery systems. Impact tells them what is affected and how sure it is.
  • It does not authorize. Whether a change may proceed stays with policy and IAM systems. Impact supplies evidence for that decision, not the decision.
  • It never overstates certainty. A partial snapshot is incomplete knowledge, an unknown verdict is uncertainty and an inferred consumer is a lead to verify — none of them is a confirmed runtime impact.

See also