Guided tour¶
This tour answers seven questions people ask about services they did not
build. Each answer uses one or two pacto commands on a fixed set of example
contracts and explains the output. The tour covers contracts and the checks on
them. Delivery has its own demos.
Before you start¶
Everything here runs offline except pacto dashboard. The fixture is
committed to the repository, so you need no cluster, registry, running service
or network.
You need the Pacto CLI on your PATH and a clone:
Paste every command below from the repository root.
To drive a real stack, see the Docker Compose demo. It needs neither a clone nor the CLI.
The tour shows runtime conformance (checking what a service really does on the network) as a recorded finding only: story 2 shows one the kernel saw. To watch it live, use the Compose demo or the node agent on a Kubernetes cluster.
The fleet¶
Thirty-six services are in the snapshot. Nine carry the stories:
| Service | Owner | Its part |
|---|---|---|
payments-service |
payments-team | six revisions; the change stories 3 and 4 analyse |
orders-service |
commerce-team | declares a dependency on payments, and is observed calling it |
api-gateway |
platform-foundations | declares the same dependency, and has an expired readiness assessment |
auth-service |
identity-team | deployed and never observed: the Unknown target |
audit-log |
platform-foundations-security | calls payments and declares nothing: the shadow consumer |
storefront-web |
frontend-team | a production environment whose values fail the chart schema |
catalog-service |
catalog-team | an enum value added behind a $ref |
shipping-rates |
logistics-team | a raised compatibilityEpoch |
inventory-service |
catalog-team | stops promising a guarantee two consumers rely on |
A target is one running deployment of a service. A readiness assessment is a dated list of claims in a contract, and a shadow consumer is a service seen calling another that no contract declares; the glossary has these and the other words the tour uses.
A contract says what a service exposes, what it needs and how it behaves. How the service is deployed stays in its own files. The contract reference has every section.
Story 1: List an inherited fleet and its state¶
Enumerate everything from contracts alone, then find out what state it is in.
36 of 36 service(s):
analytics-pipeline NotEvaluated owner=data-platform revs=2 targets=0
api-gateway NotEvaluated owner=platform-foundations revs=3 targets=0
audit-log NotEvaluated owner=platform-foundations-security revs=1 targets=0
auth-service NotEvaluated owner=identity-team revs=1 targets=0
campaign-service NotEvaluated owner=growth-team revs=2 targets=0
carrier-gateway NotEvaluated owner=logistics-team revs=2 targets=0
catalog-service NotEvaluated owner=catalog-team revs=3 targets=0
email-provider NotEvaluated owner=- revs=1 targets=0
event-bus NotEvaluated owner=data-platform revs=2 targets=0
experiment-service NotEvaluated owner=growth-team revs=1 targets=0
feature-store NotEvaluated owner=data-platform revs=1 targets=0
fraud-service NotEvaluated owner=payments-team revs=1 targets=0
frontend NotEvaluated owner=frontend-team revs=1 targets=0
fulfillment-service NotEvaluated owner=logistics-team revs=2 targets=0
inventory-service NotEvaluated owner=catalog-team revs=3 targets=0
loyalty-service NotEvaluated owner=growth-team revs=2 targets=0
notification-worker NotEvaluated owner=commerce-team revs=1 targets=0
orders-service NotEvaluated owner=commerce-team revs=3 targets=0
pacto-demo NotEvaluated owner=platform-foundations revs=1 targets=0
payments-service NotEvaluated owner=payments-team revs=6 targets=0
platform-app-config NotEvaluated owner=platform-foundations revs=1 targets=0
platform-http-policy NotEvaluated owner=platform-foundations revs=1 targets=0
platform-worker-config NotEvaluated owner=platform-foundations revs=1 targets=0
postgresql NotEvaluated owner=infra-data revs=1 targets=0
pricing-service NotEvaluated owner=catalog-team revs=2 targets=0
recommendations NotEvaluated owner=growth-team revs=2 targets=0
redis NotEvaluated owner=infra-data revs=1 targets=0
reporting-api NotEvaluated owner=data-platform revs=1 targets=0
returns-service NotEvaluated owner=logistics-team revs=2 targets=0
search-indexer NotEvaluated owner=catalog-team revs=1 targets=0
shipping-rates NotEvaluated owner=logistics-team revs=3 targets=0
storefront-web Invalid owner=frontend-team revs=3 targets=0
stripe-api NotEvaluated owner=external/stripe revs=1 targets=0
support-assistant NotEvaluated owner=support-team revs=1 targets=0
ticketing-service NotEvaluated owner=support-team revs=2 targets=0
warehouse-scanner NotEvaluated owner=logistics-team revs=1 targets=0
Thirty-six services, each with its owner, how many contract revisions it has
published and how many deployed targets are running it. NotEvaluated means
Pacto knows no deployment of the service yet, so it has nothing to evaluate.
--target-state folds in what a platform observed about the running deployments.
Three of its targets, abridged:
schemaVersion: pacto.dev/fleet-targets/v1
targets:
- { scope: production-eu, kind: kubernetes-workload, name: payments/payments-service,
service: payments-service, compliance: Compliant,
coverage: { evaluated: 6, required: 6 }, evidenceAt: 2026-07-29T09:40:00Z }
- scope: production-eu
kind: kubernetes-workload
name: commerce/orders-service
service: orders-service
compliance: NonCompliant
coverage: { evaluated: 5, required: 5 }
evidenceAt: 2026-07-29T09:40:00Z
findings:
- code: STATELESS_PERSISTENT_CONFLICT
severity: error
category: RuntimeDrift
message: declared stateless but the observed workload mounts a persistent volume
- code: INTERFACE_ABSENT
severity: error
category: RuntimeDrift
message: nothing listens on the declared http interface
evidenceRefs: [{ source: k8s, collector: k8s-observer+ebpf }]
- { scope: production-eu, kind: kubernetes-workload, name: identity/auth-service,
service: auth-service, compliance: Unknown,
coverage: { evaluated: 1, required: 4 } }
36 of 36 service(s):
analytics-pipeline NotEvaluated owner=data-platform revs=2 targets=0
api-gateway NotEvaluated owner=platform-foundations revs=3 targets=0
audit-log NotEvaluated owner=platform-foundations-security revs=1 targets=0
auth-service Unknown owner=identity-team revs=1 targets=1
campaign-service NotEvaluated owner=growth-team revs=2 targets=0
carrier-gateway NotEvaluated owner=logistics-team revs=2 targets=0
catalog-service NotEvaluated owner=catalog-team revs=3 targets=0
email-provider NotEvaluated owner=- revs=1 targets=0
event-bus NotEvaluated owner=data-platform revs=2 targets=0
experiment-service NotEvaluated owner=growth-team revs=1 targets=0
feature-store NotEvaluated owner=data-platform revs=1 targets=0
fraud-service Compliant owner=payments-team revs=1 targets=1
frontend NotEvaluated owner=frontend-team revs=1 targets=0
fulfillment-service NotEvaluated owner=logistics-team revs=2 targets=0
inventory-service NotEvaluated owner=catalog-team revs=3 targets=0
loyalty-service NotEvaluated owner=growth-team revs=2 targets=0
notification-worker NotEvaluated owner=commerce-team revs=1 targets=0
orders-service NonCompliant owner=commerce-team revs=3 targets=1
pacto-demo NotEvaluated owner=platform-foundations revs=1 targets=0
payments-service Compliant owner=payments-team revs=6 targets=1
platform-app-config NotEvaluated owner=platform-foundations revs=1 targets=0
platform-http-policy NotEvaluated owner=platform-foundations revs=1 targets=0
platform-worker-config NotEvaluated owner=platform-foundations revs=1 targets=0
postgresql NotEvaluated owner=infra-data revs=1 targets=0
pricing-service NotEvaluated owner=catalog-team revs=2 targets=0
recommendations NotEvaluated owner=growth-team revs=2 targets=0
redis NotEvaluated owner=infra-data revs=1 targets=0
reporting-api NotEvaluated owner=data-platform revs=1 targets=0
returns-service NotEvaluated owner=logistics-team revs=2 targets=0
search-indexer NotEvaluated owner=catalog-team revs=1 targets=0
shipping-rates NotEvaluated owner=logistics-team revs=3 targets=0
storefront-web Invalid owner=frontend-team revs=3 targets=0
stripe-api NotEvaluated owner=external/stripe revs=1 targets=0
support-assistant NotEvaluated owner=support-team revs=1 targets=0
ticketing-service NotEvaluated owner=support-team revs=2 targets=0
warehouse-scanner NotEvaluated owner=logistics-team revs=1 targets=0
Five states in one screen. Compliant is a verdict backed by evidence,
NonCompliant is a confirmed contradiction, Unknown is evidence that never
arrived and NotEvaluated means nobody is deployed there to evaluate.
Invalid means the newest contract fails validation; story 7 shows why.
Story 2: Find the reason behind an Unknown¶
Get the reason behind a verdict. Missing evidence counts as Unknown.
pacto fleet explain identity/auth-service --local examples/demo/bundles --target-state examples/demo/fleet-targets.yaml --freshness 24h
target production-eu/kubernetes-workload/identity%2Fauth-service: Unknown
[EVIDENCE_MISSING] no evidence has been observed for this target
Service: api-gateway@1.2.0
Owner: platform-foundations
Pacto Version: 2.0
Workload: service
State:
Type: stateless
Persistence: local/ephemeral
Data Criticality: low
Interfaces (1):
- http (openapi: interfaces/openapi.json, public)
Dependencies (3):
- auth-service: oci://ghcr.io/trianalab/pacto/auth-service (^1.0.0, required)
- orders-service: oci://ghcr.io/trianalab/pacto/orders-service (^1.0.0, required)
- payments-service: oci://ghcr.io/trianalab/pacto/payments-service (^1.0.0, required)
Readiness:
Score: 0
Gate: FAIL (score 0 / minScore 75)
Earned Weight: 0
Total Weight: 150
Expires: 2025-01-01 (EXPIRED)
Status: 5 done, 0 partial, 0 not-done, 0 deferred
Checks:
- gateway-dashboard observability done weight=30 earned=0 evidence=https://grafana.acme.com/d/api-gateway
- rate-limiting infrastructure done weight=25 earned=0 evidence=ENG-1234
- security-review security done weight=40 earned=0 evidence=SEC-2045
- load-test testing done weight=35 earned=0 evidence=https://k6.acme.com/api-gateway-load-2024-12
- runbook documentation done weight=20 earned=0 evidence=docs/runbooks/api-gateway.md
EVIDENCE_MISSING describes the observer and leaves the service's own
verdict open.
api-gateway declares five readiness claims, every one marked done with
evidence attached, and the assessment earns zero because it expired on
2025-01-01. Pacto fails closed on both.
A confirmed violation reads differently:
pacto fleet get --target commerce/orders-service --local examples/demo/bundles --target-state examples/demo/fleet-targets.yaml
Target: production-eu/kubernetes-workload/commerce%2Forders-service
Service: orders-service
Compliance: NonCompliant
Revision: orders-service@sha256:abf75f569260779a932066089e72cf0c2e6c8ac61a22fcf849cced91aaa75736 (inferred)
Stale: false
Coverage: 5/5 evaluated
finding [error] STATELESS_PERSISTENT_CONFLICT: declared stateless but the observed workload mounts a persistent volume
finding [error] INTERFACE_ABSENT: nothing listens on the declared http interface (seen by k8s-observer+ebpf)
Coverage: 5/5 evaluated marks the difference from the Unknown above: every
check ran, so the verdict is a conclusion. Each finding names
one exact contradiction. The contract declares the workload stateless and the
observation found a persistent volume mounted. The contract declares an http
interface and nothing listens on its port: seen by k8s-observer+ebpf says the
node agent saw that in the kernel, and Kubernetes agreed. Each finding is one
fact that disagrees with one declaration.
Story 3: Check a change before you ship it¶
Classify a change from two contracts, with nothing running, then gate CI on it.
pacto diff examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1
Classification: BREAKING
Required bump: major
Changes (39):
[NON_BREAKING] service.version (modified): service.version modified [1.2.1 -> 2.0.1]
[POTENTIAL_BREAKING] capabilities (removed): capabilities removed [- extension (payments.trianalab.com/refund)]
[NON_BREAKING] capabilities (added): capabilities added [+ extension (payments.trianalab.com/dispute)]
[POTENTIAL_BREAKING] dependencies.required (modified): dependencies.required modified [fraud-service: required=false -> fraud-service: required=true]
[BREAKING] asyncapi.channels[payment.completed] (removed): channel payment.completed removed [- payment.completed]
[BREAKING] asyncapi.channels[payment.failed] (removed): channel payment.failed removed [- payment.failed]
[BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.properties.charge_id (removed): asyncapi.channels[payment.refunded].publish.message.payload.properties.charge_id removed [- map[type:string]]
[NON_BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.properties.payment_intent_id (added): asyncapi.channels[payment.refunded].publish.message.payload.properties.payment_intent_id added [+ map[type:string]]
[BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.required[payment_intent_id] (added): asyncapi.channels[payment.refunded].publish.message.payload.required payment_intent_id added [+ payment_intent_id]
[BREAKING] asyncapi.channels[payment.refunded].publish.message.payload.required[charge_id] (removed): asyncapi.channels[payment.refunded].publish.message.payload.required charge_id removed [- charge_id]
[NON_BREAKING] asyncapi.channels[payment.intent.created] (added): channel payment.intent.created added [+ payment.intent.created]
[NON_BREAKING] asyncapi.channels[payment.intent.failed] (added): channel payment.intent.failed added [+ payment.intent.failed]
[NON_BREAKING] asyncapi.channels[payment.intent.succeeded] (added): channel payment.intent.succeeded added [+ payment.intent.succeeded]
[BREAKING] openapi.paths[/charges] (removed): API path /charges removed [- /charges]
[BREAKING] openapi.paths[/charges/{id}] (removed): API path /charges/{id} removed [- /charges/{id}]
[POTENTIAL_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.charge_id (removed): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.charge_id removed [- map[type:string]]
[NON_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.payment_intent_id (added): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.payment_intent_id added [+ map[type:string]]
[BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required[payment_intent_id] (added): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required payment_intent_id added [+ payment_intent_id]
[NON_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required[charge_id] (removed): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.required charge_id removed [- charge_id]
[NON_BREAKING] openapi.paths[/refunds].methods[POST].responses[404].description (modified): openapi.paths[/refunds].methods[POST].responses[404].description changed [Charge not found -> Payment intent not found]
[POTENTIAL_BREAKING] openapi.paths[/webhooks/stripe].methods[POST].request-body (removed): POST /webhooks/stripe request body removed [- POST /webhooks/stripe]
[NON_BREAKING] openapi.paths[/webhooks/stripe].methods[POST].responses[400].description (modified): openapi.paths[/webhooks/stripe].methods[POST].responses[400].description changed [Invalid webhook signature -> Invalid signature]
[NON_BREAKING] openapi.paths[/payment-intents] (added): API path /payment-intents added [+ /payment-intents]
[NON_BREAKING] openapi.paths[/payment-intents/{id}] (added): API path /payment-intents/{id} added [+ /payment-intents/{id}]
[NON_BREAKING] openapi.paths[/payment-intents/{id}/cancel] (added): API path /payment-intents/{id}/cancel added [+ /payment-intents/{id}/cancel]
[NON_BREAKING] openapi.paths[/payment-intents/{id}/confirm] (added): API path /payment-intents/{id}/confirm added [+ /payment-intents/{id}/confirm]
[POTENTIAL_BREAKING] schema.properties.ENABLE_REFUNDS (removed): schema.properties.ENABLE_REFUNDS removed [- map[default:true description:Feature flag to enable/disable refund processing type:boolean]]
[POTENTIAL_BREAKING] schema.properties.FRAUD_CHECK_ENABLED.default (modified): schema.properties.FRAUD_CHECK_ENABLED.default changed [false -> true]
[NON_BREAKING] schema.properties.FRAUD_CHECK_ENABLED.description (modified): schema.properties.FRAUD_CHECK_ENABLED.description changed [Enable fraud detection checks before processing payments -> Fraud detection is now enabled by default and required]
[POTENTIAL_BREAKING] schema.properties.STRIPE_API_KEY (removed): schema.properties.STRIPE_API_KEY removed [- map[description:Stripe API secret key for payment processing type:string]]
[POTENTIAL_BREAKING] schema.properties.STRIPE_SECRET_KEY (added): schema.properties.STRIPE_SECRET_KEY added [+ map[description:Stripe secret key (renamed from STRIPE_API_KEY) type:string]]
[NON_BREAKING] schema.properties.WEBHOOK_SECRET.description (modified): schema.properties.WEBHOOK_SECRET.description changed [Stripe webhook signing secret for signature verification -> Stripe webhook signing secret (now required)]
[BREAKING] schema.required[STRIPE_SECRET_KEY] (added): schema.required STRIPE_SECRET_KEY added [+ STRIPE_SECRET_KEY]
[BREAKING] schema.required[WEBHOOK_SECRET] (added): schema.required WEBHOOK_SECRET added [+ WEBHOOK_SECRET]
[BREAKING] schema.required[STRIPE_API_KEY] (removed): schema.required STRIPE_API_KEY removed [- STRIPE_API_KEY]
[NON_BREAKING] configurations[app].values.ENABLE_REFUNDS (removed): configurations[app].values.ENABLE_REFUNDS removed [- true]
[NON_BREAKING] configurations[app].values.FRAUD_CHECK_ENABLED (modified): configurations[app].values.FRAUD_CHECK_ENABLED changed [false -> true]
[NON_BREAKING] configurations[app].values.STRIPE_API_KEY (removed): configurations[app].values.STRIPE_API_KEY removed [- ${STRIPE_API_KEY}]
[NON_BREAKING] configurations[app].values.STRIPE_SECRET_KEY (added): configurations[app].values.STRIPE_SECRET_KEY added [+ ${STRIPE_SECRET_KEY}]
SBOM changes (1):
~ stripe-go version: 1.0.0 -> 2.0.0
breaking changes detected
Thirty-nine changes, one verdict and a non-zero exit so CI can gate on it. Two API paths removed, two event channels withdrawn, a required request field swapped for a differently-named one, two configuration keys becoming required. A capability dropped, an optional dependency became mandatory and one SBOM package version moved. Pacto found all of it by comparing one contract with another, with no running service. The classification rules are a published table.
The same service, one pair earlier, shows a release that breaks nobody:
pacto diff examples/demo/bundles/payments-service/v1.0.0 examples/demo/bundles/payments-service/v1.1.0
Classification: POTENTIAL_BREAKING
Required bump: minor
Changes (6):
[NON_BREAKING] service.version (modified): service.version modified [1.0.0 -> 1.1.0]
[NON_BREAKING] asyncapi.channels[payment.failed] (added): channel payment.failed added [+ payment.failed]
[NON_BREAKING] openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.amount.description (modified): openapi.paths[/refunds].methods[POST].request-body.content.application/json.schema.properties.amount.description changed [Refund amount in cents (partial refund if less than charge) -> Refund amount in cents]
[NON_BREAKING] openapi.paths[/webhooks/stripe] (added): API path /webhooks/stripe added [+ /webhooks/stripe]
[POTENTIAL_BREAKING] schema.properties.WEBHOOK_SECRET (added): schema.properties.WEBHOOK_SECRET added [+ map[description:Stripe webhook signing secret for signature verification type:string]]
[NON_BREAKING] configurations[app].values.WEBHOOK_SECRET (added): configurations[app].values.WEBHOOK_SECRET added [+ ${STRIPE_WEBHOOK_SECRET}]
The result is POTENTIAL_BREAKING. A new endpoint, a new event channel and a
new optional configuration property break nobody by themselves. Adding a
property to a schema can still surprise a consumer that validates strictly. The
third classification gives additive changes like these their own verdict.
Story 4: Find who a breaking change affects¶
Turn a classification into a list of consumers, each graded by how it is known.
pacto impact examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1 --local examples/demo/bundles
Impact: payments-service 1.2.1 -> 2.0.1
Classification: BREAKING
Breaking changes: 11
Potentially breaking changes: 8
Affected consumers (4):
api-gateway direct confidence=contractual compat=incompatible owner=platform-foundations
frontend transitive confidence=inferred compat=unknown owner=frontend-team
orders-service direct confidence=contractual compat=incompatible owner=commerce-team
pacto-demo transitive confidence=inferred compat=unknown owner=platform-foundations
Release gate: unverified
unverified: the change is BREAKING and 4 consumers depend on payments-service, but none is shown to be both incompatible and running. Review each consumer, and add a source that knows where services run (for example --target-state) to tell deployed impact apart
Four consumers. confidence=contractual means the consumer declared this
dependency in its own contract; confidence=inferred means it was reached
transitively. compat=incompatible is a second judgement: 2.0.1 falls outside
the consumer's declared version range. The release gate is unverified and the
command exits 2. The bundles say nothing about where anything runs, so Pacto
cannot show a consumer is hit and does not call the change safe either.
Contracts find only the consumers that declare the dependency. Add observed traffic:
pacto impact examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1 --local examples/demo/bundles --traces examples/demo/traces.json
Impact: payments-service 1.2.1 -> 2.0.1
Classification: BREAKING
Breaking changes: 11
Potentially breaking changes: 8
Affected consumers (5):
api-gateway direct confidence=contractual compat=incompatible owner=platform-foundations
audit-log direct confidence=observed compat=unknown owner=platform-foundations-security
frontend transitive confidence=inferred compat=unknown owner=frontend-team
orders-service direct confidence=corroborated compat=incompatible owner=commerce-team
pacto-demo transitive confidence=inferred compat=unknown owner=platform-foundations
Release gate: unverified
unverified: the change is BREAKING and 5 consumers depend on payments-service, but none is shown to be both incompatible and running. Review each consumer, and add a source that knows where services run (for example --target-state) to tell deployed impact apart
Five now. audit-log calls payments-service in production and declares
nothing, so only observed traffic finds it. It arrives as
confidence=observed. orders-service, which both declared the dependency
and was seen using it, is upgraded to confidence=corroborated.
Then ask where the change actually lands:
pacto impact examples/demo/bundles/payments-service/v1.2.1 examples/demo/bundles/payments-service/v2.0.1 --local examples/demo/bundles --traces examples/demo/traces.json --target-state examples/demo/fleet-targets.yaml
Impact: payments-service 1.2.1 -> 2.0.1
Classification: BREAKING
Breaking changes: 11
Potentially breaking changes: 8
Affected consumers (5):
api-gateway direct confidence=contractual compat=incompatible owner=platform-foundations
audit-log direct confidence=observed compat=unknown owner=platform-foundations-security
frontend transitive confidence=inferred compat=unknown owner=frontend-team
orders-service direct confidence=corroborated compat=incompatible owner=commerce-team
pacto-demo transitive confidence=inferred compat=unknown owner=platform-foundations
Active targets (2): [production-eu/kubernetes-workload/commerce%2Forders-service production-eu/kubernetes-workload/payments%2Fpayments-service]
Release gate: blocked
breaking changes affect active consumers, which run on known targets and do not accept payments-service 2.0.1: orders-service
The consumer list is identical. The active targets are new, and the gate turns
from unverified to blocked. Two deployed targets sit in its path:
orders-service, one of the five consumers, and payments-service itself. The
command exits 1. Story 3 refused a change for what it is. This command refuses
it for where it lands.
Story 5: Compare declared and observed dependencies¶
Reconcile the edges contracts declare against the edges something observed.
Reconciliation: 1 matched, 50 declared-not-observed, 1 observed-not-declared
[declared-not-observed] analytics-pipeline -> event-bus (count=0)
[declared-not-observed] analytics-pipeline -> postgresql (count=0)
[declared-not-observed] api-gateway -> auth-service (count=0)
[declared-not-observed] api-gateway -> orders-service (count=0)
[declared-not-observed] api-gateway -> payments-service (count=0)
[observed-not-declared] audit-log -> payments-service (count=1)
[declared-not-observed] auth-service -> redis (count=0)
[declared-not-observed] campaign-service -> email-provider (count=0)
[declared-not-observed] campaign-service -> recommendations (count=0)
[declared-not-observed] carrier-gateway -> parcel-carrier (count=0)
[declared-not-observed] catalog-service -> postgresql (count=0)
[declared-not-observed] experiment-service -> redis (count=0)
[declared-not-observed] feature-store -> redis (count=0)
[declared-not-observed] fraud-service -> redis (count=0)
[declared-not-observed] frontend -> api-gateway (count=0)
[declared-not-observed] fulfillment-service -> event-bus (count=0)
[declared-not-observed] fulfillment-service -> inventory-service (count=0)
[declared-not-observed] fulfillment-service -> shipping-rates (count=0)
[declared-not-observed] inventory-service -> event-bus (count=0)
[declared-not-observed] inventory-service -> postgresql (count=0)
[declared-not-observed] loyalty-service -> event-bus (count=0)
[declared-not-observed] loyalty-service -> postgresql (count=0)
[declared-not-observed] notification-worker -> email-provider (count=0)
[declared-not-observed] orders-service -> notification-worker (count=0)
[matched] orders-service -> payments-service (count=1)
[declared-not-observed] orders-service -> postgresql (count=0)
[declared-not-observed] pacto-demo -> frontend (count=0)
[declared-not-observed] payments-service -> fraud-service (count=0)
[declared-not-observed] payments-service -> postgresql (count=0)
[declared-not-observed] payments-service -> stripe-api (count=0)
[declared-not-observed] pricing-service -> catalog-service (count=0)
[declared-not-observed] pricing-service -> inventory-service (count=0)
[declared-not-observed] pricing-service -> redis (count=0)
[declared-not-observed] recommendations -> catalog-service (count=0)
[declared-not-observed] recommendations -> experiment-service (count=0)
[declared-not-observed] recommendations -> feature-store (count=0)
[declared-not-observed] reporting-api -> analytics-pipeline (count=0)
[declared-not-observed] reporting-api -> auth-service (count=0)
[declared-not-observed] returns-service -> shipping-rates (count=0)
[declared-not-observed] search-indexer -> catalog-service (count=0)
[declared-not-observed] search-indexer -> inventory-service (count=0)
[declared-not-observed] shipping-rates -> carrier-gateway (count=0)
[declared-not-observed] storefront-web -> auth-service (count=0)
[declared-not-observed] storefront-web -> catalog-service (count=0)
[declared-not-observed] storefront-web -> inventory-service (count=0)
[declared-not-observed] support-assistant -> model (count=0)
[declared-not-observed] support-assistant -> returns-service (count=0)
[declared-not-observed] support-assistant -> ticketing-service (count=0)
[declared-not-observed] ticketing-service -> auth-service (count=0)
[declared-not-observed] ticketing-service -> fulfillment-service (count=0)
[declared-not-observed] ticketing-service -> postgresql (count=0)
[declared-not-observed] warehouse-scanner -> inventory-service (count=0)
Three verdicts. Reconciling means comparing each declared edge with what was
observed. observed-not-declared is the shadow dependency from story 4,
stated as a reconciliation result. declared-not-observed is the opposite
risk: an edge in the diagram that no traffic has taken.
Story 6: Give an agent read-only tools¶
Serve a contract as MCP tools, and check what the default withholds.
pacto mcp turns the read-only operations of a bundle's OpenAPI interface
into MCP tools. Point a human and an agent at one process:
pacto dashboard examples/demo/bundles --port 8899
pacto mcp examples/demo/pacto-dashboard --base-url http://127.0.0.1:8899
Then open http://127.0.0.1:8899/#/fleet, and connect your MCP client to the
second command. The dashboard is itself a Pacto bundle with a real OpenAPI
contract, so the agent's tools are generated from the contract of the very
server the human is looking at. The server also registers Pacto's own authoring
tools: pacto_create and pacto_edit write contract files to disk.
Serve the demo's payments service, and Pacto drops its five mutating
operations, every POST, PUT, PATCH and DELETE, with a warning on stderr:
The base URL is a dead address on purpose: Pacto registers and lists the tools and calls none of them.
Add --allow-writes and both the warning and the restriction disappear.
--allow-writes governs the interface tools only. To withhold the authoring
tools, register this server only where you want contracts written. See
MCP integration for the full wiring.
Story 7: Check what a provider promises¶
Check behavior, retirement schedules and environment values.
examples/demo/bundles/storefront-web/v1.2.0 is invalid
PASS load
SKIP lock no pacto.lock
PASS structural
PASS cross-field
PASS policy
FAIL environments
examples/demo/bundles/storefront-web/v1.2.0/environments/production.yaml:5:3: ERROR [ENVIRONMENT_VALUES_INVALID] environments[1].file: environment production: configuration chart at /chart/ingress: missing property 'host'
WARN [ENVIRONMENT_VALUES_UNVERIFIED] environments[2].file: environment preview: cannot check the values for configuration platform: its schema comes from the contract oci://ghcr.io/trianalab/pacto/platform-app-config, and Pacto checks values only against a schema file in this bundle
SKIP guarantees no dependency declares requires
SKIP readiness not requested (--readiness)
SKIP closure not requested (--closure)
Next: fix environments[1].file at examples/demo/bundles/storefront-web/v1.2.0/environments/production.yaml:5:3 [ENVIRONMENT_VALUES_INVALID], then run pacto validate examples/demo/bundles/storefront-web/v1.2.0
validation failed with 1 error(s)
Each stage has its own line, and Next names the first fix: the new chart
schema requires an ingress host that production leaves out. preview sets
values for platform, whose schema is in another bundle, so Pacto could not
check them.
pacto diff examples/demo/bundles/catalog-service/v1.1.0 examples/demo/bundles/catalog-service/v1.2.0
Classification: BREAKING
Required bump: major
Changes (3):
[NON_BREAKING] pactoVersion (modified): pactoVersion modified [2.1 -> 2.2]
[NON_BREAKING] service.version (modified): service.version modified [1.1.0 -> 1.2.0]
[BREAKING] openapi.paths[/products].methods[GET].responses[200].content.application/json.schema.items.properties.status.enum[preorder] (added): openapi.paths[/products].methods[GET].responses[200].content.application/json.schema.items.properties.status.enum preorder added (in #/components/schemas/ProductStatus) [+ preorder]
breaking changes detected
Classification: BREAKING
Required bump: major
Changes (2):
[NON_BREAKING] service.version (modified): service.version modified [1.1.0 -> 2.0.0]
[BREAKING] interfaces.compatibilityEpoch (modified): interface rates: compatibilityEpoch raised from 0 to 1: its behavior changed incompatibly, so every consumer must review the change [rates: 0 -> rates: 1]
breaking changes detected
Every product response reaches ProductStatus through a $ref, so one new
enum value is breaking in a minor release. The rates document did not change,
and the raised compatibilityEpoch says its meaning did.
pacto impact examples/demo/bundles/inventory-service/v2.1.0 examples/demo/bundles/inventory-service/v3.0.0 --local examples/demo/bundles
Impact: inventory-service 2.1.0 -> 3.0.0
Classification: BREAKING
Breaking changes: 1
Potentially breaking changes: 0
Affected consumers (7):
fulfillment-service direct confidence=contractual compat=incompatible owner=logistics-team
refused: fulfillment-service requires stock-events dedupeWindow >= 5m from inventory-service, which offers nothing
pricing-service direct confidence=contractual compat=incompatible owner=catalog-team
refused: pricing-service requires stock-v2 timeout <= 2s from inventory-service, which offers 5s
search-indexer direct confidence=contractual compat=incompatible owner=catalog-team
storefront-web direct confidence=contractual compat=incompatible owner=frontend-team
support-assistant transitive confidence=inferred compat=unknown owner=support-team
ticketing-service transitive confidence=inferred compat=unknown owner=support-team
warehouse-scanner direct confidence=contractual compat=incompatible owner=logistics-team
Release gate: unverified
unverified: the change is BREAKING and 7 consumers depend on inventory-service, but none is shown to be both incompatible and running. inventory-service 3.0.0 does not meet a guarantee required by fulfillment-service, pricing-service. Review each consumer, and add a source that knows where services run (for example --target-state) to tell deployed impact apart
3.0.0 drops the dedupe window fulfillment-service requires, and its 5s timeout already refuses pricing-service, which requires 2s.
What's next¶
The Quickstart takes an empty directory to a validated
contract and a caught breaking change in about five minutes. The
contract reference is the complete list of
sections, and pacto diff
is the complete table of what counts as a breaking change.