Fail before apply
Resolver enforcement for cross-tier contracts.
The resolver is the release-path refusal point. It takes the consumer's producer, key, context, and expected type, then returns a bound value or stops before any provider call changes infrastructure.
Contract addresses and ownership boundaries remain advisory while a consumer can reach into another tier's outputs and still get a plan. The apply path must reject that dependency; review alone leaves the shortcut executable.
Where the resolver runs
The resolver belongs in the earliest place that sees both sides of the contract: the consumer request and the producer's published values. Common hooks are CI before terraform plan, a generation step that writes stack inputs, or the IaC program before it instantiates provider resources.
The producer publishes typed values for consumers to resolve during a release through stack outputs, a registry, or a generated manifest. In Pulumi, StackReference reads those values from stack outputs. The reference is the transport, not the public contract: the consumer request is environment/network.primary as NetworkRef in prod, and the resolver maps that request to exactly one backing stack/output value or refuses.
Stack dependencies model sequencing when they are declared, but ordering alone does not prove composition. A StackReference wrapper becomes the resolver when it validates the value, producer, key, and context at runtime rather than casting the output to the expected type.
Any backend still has to preserve producer namespace, key, context, type, and ownership; without those constraints, it is raw outputs behind another API.
What the gate refuses
A generic provider error tells the operator where the plan happened to break. A resolver error names the contract that could not be satisfied before anything changed.
The gate refuses five contract failures:
- unknown producer:
environmentis not published in this context - missing key:
environment/network.primaryexists in staging but not prod - wrong type: the value resolves, but not as
NetworkRef - ambiguous binding: more than one producer satisfies the same address in the same context
- cycle: the producer cannot publish because it depends on the consumer
Unknown producer and missing key are cheap failures. The missing side either needs to ship, or the consumer needs to drop the dependency. A retry against the same published set should fail the same way.
Wrong type is a stronger warning because the address resolved. The value exists, but its shape does not carry the capability the consumer asked for. A string array where NetworkRef was expected cannot drift into a provider call because the values look syntactically usable.
Ambiguous binding means a migration leaked into the catalogue: two stacks publishing environment/network.primary, a copied stack that kept the original producer identity, or a context rewire that points two environments at the same surface. The resolver names both candidates and refuses to choose.
An environment tier cannot publish network.primary if it first needs workload-api/network-config, while the workload needs environment/network.primary to start. Graph tools catch many of these in the stack graph; the resolver still needs to refuse the same shape when the cycle appears through a catalogue or generated manifest.
A resolved contract can remain unusable while another cloud API converges, as with IAM propagation or DNS delegation. The release path needs a readiness check and backoff at that boundary. Weakening the contract would only hide a propagation problem as a dependency problem.
Shared surfaces need binding rules
Changing a shared surface needs more than a resolved value. A binding rule names the surface, the permitted request and operations, and the deploy identity allowed to make the change. DNS, listener attachments, and shared IAM surfaces need this contract before a consumer mutates them.
DNS makes the extra fields visible: delegation admits the name pattern, the permission policy allows the record operations and types, and the principal binding identifies who may exercise them. Delegation alone leaves permission scope and holder identity unchecked.
binding rule: organisation/dns.workload-api-zone
admits: *.api.example.com
permits: A | CNAME | TXT
ops: CREATE | UPDATE | DELETE
holder: workload-api/deployBefore calling the DNS provider, the resolver checks the requested name, record type, operation, and authenticated principal against the binding rule. Any mismatch stops the apply. The same check governs IAM changes on organisation-owned surfaces and listener changes on shared load balancers.
Existing systems need a migration path: publish the new contract beside the old output, move consumers, then remove the old path. The next part moves from enforcement to repair: the order for reaching this shape in a live system.