Skip to content

Collectors and the evidence boundary

Pacto's architecture is four roles, not three tools. The CLI, dashboard and Kubernetes operator are products built around these roles:

  • Contract — declares stable service intent (pacto.yaml).
  • Collectors / evidence producers — observe one environment and emit Evidence.
  • Evaluation engine — the pure Evaluate(contract, evidence) function.
  • Findings consumers / integration hosts — surface or act on the results.

The stable extension boundary is the EvidenceSet, not a collector interface. A collector is any component that observes a real system and produces a valid EvidenceSet the engine can evaluate. This is modularity through a stable Evidence schema — not a dynamically pluggable collector runtime.

Declaration vs observation

The central V2 split: the author declares intent; a collector observes reality; the engine evaluates one against the other.

flowchart TB
    A["Author intent<br/>pacto.yaml"] --> C["Contract"]
    R["Running environment"] --> COL["Collector"]
    COL --> E["EvidenceSet"]
    C --> EV["Evaluate"]
    E --> EV
    EV --> OUT["Findings + Coverage"]

The collector system

Different environments need different collectors; each produces the same EvidenceSet. The Kubernetes collector is the first shipped one. A custom collector is an architectural extension point, not an included implementation.

flowchart LR
    C["Pacto contract<br/>declared intent"]
    subgraph Collectors["Collectors — environment-specific"]
        K["Kubernetes collector<br/><i>first-party, shipped</i>"]
        X["Custom collector<br/><i>external integration</i>"]
    end
    K --> E["EvidenceSet<br/>observed facts"]
    X -.-> E
    C --> V["Evaluate"]
    E --> V
    V --> F["Findings + Coverage"]
    F --> D["Dashboard"]
    F --> S["Operator status / CI / platform tooling"]

The engine is pure and platform-neutral; the dashboard, operator status and CI are consumers or hosts, not collectors. Pacto does not ship ECS, Nomad, Terraform or cloud collectors — the dotted "custom collector" is a design extension point only.

The roles

Role Responsibility
Contract Declares stable service intent
Collector Observes one environment and emits Evidence
Integration host Schedules collection, handles credentials, owns temporal state
Engine Evaluates Contract × Evidence (pure)
Reporter / consumer Displays or acts on Findings
Plugin Generates artifacts from a contract — a different subsystem from collectors

Collector vs plugin — not interchangeable. A plugin consumes a contract to generate an artifact (e.g. an OpenAPI schema or SBOM). A collector observes a real system to produce Evidence. They are complementary extension mechanisms in different directions and must not be conflated.

Kubernetes composition

In the Kubernetes integration the operator is the host/controller around the collector — it owns reconciliation and stabilization windows. The collector translates Kubernetes facts into Evidence. The core never queries Kubernetes, and Kubernetes-specific bindings stay out of pacto.yaml (they live on the Pacto CR).

flowchart LR
    CR["Pacto CR<br/>contractRef + target + bindings"]
    HOST["Operator host<br/>resolve + reconcile + temporal windows"]
    CONTRACT["Resolved Pacto Contract"]
    K8S["Kubernetes API<br/>Workloads · Services · ConfigMaps · Secrets"]
    COLLECTOR["Kubernetes collector"]
    EVIDENCE["EvidenceSet"]
    ENGINE["Evaluate"]
    STATUS["Pacto CR status<br/>Findings + Coverage"]
    DASH["Dashboard"]

    CR --> HOST
    HOST --> CONTRACT
    HOST --> COLLECTOR
    K8S --> COLLECTOR
    COLLECTOR --> EVIDENCE
    CONTRACT --> ENGINE
    EVIDENCE --> ENGINE
    ENGINE --> STATUS
    STATUS --> DASH

The CR is not the Contract: the operator host resolves the contractRef into a Contract and owns reconciliation + stabilization windows; the collector translates Kubernetes facts into Evidence but does not evaluate compliance; Evaluate receives the Contract and the Evidence (the core never queries Kubernetes); the host writes the results to CR status, which the dashboard consumes (it does not collect or evaluate).

Implementing a third-party collector today

Be exact about what exists. Pacto's engine is collector-agnostic at the library boundary: a custom Go integration constructs and validates EvidenceSet values (pkg/evidence) and calls Evaluate (pkg/validation). The stable surface is:

  • the EvidenceSet Go/JSON model (subject, contract ref, source, observed-at, observations),
  • observation identity (kind + subject),
  • Outcome semantics (Observed / Unsupported / Failed / Stale / Insufficient),
  • payload rules and provenance,
  • the Evidence validation invariants,
  • how Evaluate consumes an EvidenceSet.

A standalone collector process protocol is not currently defined: there is no public CLI, API or wire protocol that accepts third-party Evidence, and collectors cannot be installed or registered dynamically. A custom collector is a Go integration that builds an EvidenceSet and calls the engine directly. This is the compiled, run example ExampleEvaluate in pkg/validation/collector_example_test.go:

// 1. The contract declares intent (loaded from a bundle in real use).
c := contract.Contract{
    Service:    contract.Service{Name: "orders", Version: "1.0.0"},
    Interfaces: []contract.Interface{{Name: "public-api", Type: "openapi", Ref: "interfaces/openapi.yaml"}},
}

// 2. A collector observes the environment and produces an EvidenceSet.
prov := evidence.Provenance{Collector: "example", DetectedAt: time.Unix(0, 0)}
ev := evidence.EvidenceSet{
    Subject:     evidence.SubjectRef{Kind: "service", Name: "orders"},
    ContractRef: "oci://example/orders:1.0.0",
    Source:      "example",
    ObservedAt:  time.Unix(0, 0),
    Observations: []evidence.Observation{
        evidence.NewInterfaceObserved(evidence.SubjectRef{Kind: "interface", Name: "public-api"}, "openapi", true, prov),
    },
}

// 3. The pure engine evaluates Contract x Evidence.
findings, coverage := validation.Evaluate(c, ev)

Every assertion needs a possible Evidence path

A contract assertion is only meaningful if some collector can produce the Evidence to evaluate it. This is why the V2 model has no permanently-unsatisfiable assertions: the verification.conformance opt-in was removed because no collector or evaluator currently produces interface-conformance Evidence — keeping it would only make a service permanently Unknown. Declaring an interfaces[] entry therefore requires runtime availability Evidence, not schema/protocol conformance. Conformance verification can return when there is a real observation kind and evaluator that produce Observed conformance Evidence — at which point it will have a collector path, not just a schema field.