Ruslan Akchurin

Appendix B: The contract, end to end

The resolution contract in one file.

Appendix A made the whole shape runnable. This one narrows to a single mechanism: the contract from Resolve by contract, enforced the way Fail before apply describes. One file: a producer publishes a typed value, a consumer names an address, and a resolver returns the bound value or refuses.

It is placeholder and self-contained. No provider SDKs, state backend, or network. The catalogue is an in-memory array standing in for stack outputs, a registry, or a generated manifest; that source is the implementation detail the contract hides.

The type

The address locates a value; the type decides whether that value is the capability the consumer asked for. Names alone create accidental compatibility, so NetworkRef names the network identity and its attachable subnets, and a bare string[] cannot pass as one.

contract.ts
type Context = 'staging' | 'prod'

// A logical infrastructure type: the network identity and the
// subnets a workload may attach to, not the raw values behind them.
type NetworkRef = {
  readonly kind: 'NetworkRef'
  readonly vpcId: string
  readonly privateSubnetIds: readonly string[]
}

// The check that stops a string[] from resolving as a network.
function isNetworkRef(v: unknown): v is NetworkRef {
  return (
    typeof v === 'object' &&
    v !== null &&
    (v as NetworkRef).kind === 'NetworkRef' &&
    typeof (v as NetworkRef).vpcId === 'string' &&
    Array.isArray((v as NetworkRef).privateSubnetIds)
  )
}

What the producer publishes

The producer owns the published values; where they live is not the consumer's concern. Each entry is keyed and scoped to a context, so the same producer and key can resolve in one context and be absent in another. It also records its source: the stack output, registry entry, or manifest key behind the address. The resolver names those sources when two collide on one address. requires records what a producer must resolve before it can publish — the edge the resolver walks to catch a cycle.

contract.ts
type Addr = { producer: string; key: string }

type Entry = Addr & {
  context: Context
  source: string // the stack output, registry entry, or manifest key
  requires?: Addr[] // must resolve before this entry can publish
  value: unknown // the backing value; its type is checked at resolve
}

// The environment producer publishes network.primary in two contexts.
// This array stands in for stack outputs, a registry, or a manifest.
const catalogue: Entry[] = [
  {
    producer: 'environment',
    key: 'network.primary',
    context: 'staging',
    source: 'env-stack',
    value: {
      kind: 'NetworkRef',
      vpcId: 'vpc-123',
      privateSubnetIds: ['subnet-a', 'subnet-b'],
    },
  },
  {
    producer: 'environment',
    key: 'network.primary',
    context: 'prod',
    source: 'env-stack',
    value: {
      kind: 'NetworkRef',
      vpcId: 'vpc-789',
      privateSubnetIds: ['subnet-x', 'subnet-y'],
    },
  },
]

The consumer request

The consumer selects the address (a producer namespace and a key), the context it resolves in, and the type it expects back — not the backing value. It carries the type check with it, so a value that resolves as the wrong shape is refused before the consumer can use it.

contract.ts
type Request<T> = {
  producer: string
  key: string
  context: Context
  expects: string // type name, for the refusal message
  is: (v: unknown) => v is T // the type check itself
}

// workload-api asks the environment for its primary network in prod.
const request: Request<NetworkRef> = {
  producer: 'environment',
  key: 'network.primary',
  context: 'prod',
  expects: 'NetworkRef',
  is: isNetworkRef,
}

The resolver

The resolver is the release-path refusal point. locate walks the address and the publish graph, and fails on the four that live there. resolve adds the type check. Every refusal names the part of the contract that failed before any provider call runs: the producer, plus the context on address failures, the key once the producer is known, and the expected type on the wrong-type refusal.

contract.ts
class ContractError extends Error {}

// Address and publish-graph failures: unknown producer, missing key,
// ambiguous binding, cycle.
function locate(
  cat: Entry[],
  producer: string,
  key: string,
  context: Context,
  path: string[],
): Entry {
  const at = `${producer}/${key}`

  if (path.includes(at)) {
    throw new ContractError(`cycle: ${[...path, at].join(' -> ')} in ${context}`)
  }

  const byProducer = cat.filter((e) => e.producer === producer)
  if (byProducer.length === 0) {
    throw new ContractError(
      `unknown producer: '${producer}' is not published in ${context}`,
    )
  }

  const matches = byProducer.filter(
    (e) => e.key === key && e.context === context,
  )
  if (matches.length === 0) {
    const elsewhere = byProducer.some((e) => e.key === key)
    throw new ContractError(
      elsewhere
        ? `missing key: '${at}' exists, but not in ${context}`
        : `missing key: '${at}' is not published`,
    )
  }
  if (matches.length > 1) {
    const sources = matches.map((e) => e.source).join(', ')
    throw new ContractError(
      `ambiguous binding: '${at}' in ${context} is published by ${matches.length} sources: ${sources}`,
    )
  }

  const [entry] = matches
  // The producer publishes only once its own dependencies resolve.
  for (const dep of entry.requires ?? []) {
    locate(cat, dep.producer, dep.key, context, [...path, at])
  }
  return entry
}

// Address failures first, then the type check.
function resolve<T>(cat: Entry[], req: Request<T>): T {
  const entry = locate(cat, req.producer, req.key, req.context, [])
  if (!req.is(entry.value)) {
    throw new ContractError(
      `wrong type: '${req.producer}/${req.key}' resolved, but not as ${req.expects}`,
    )
  }
  return entry.value
}

Resolve, or refuse

A good composition resolves and the consumer proceeds. Then five compositions are wrong on purpose, and each stops at a named refusal before anything is applied.

One resolver, one resolved path, five refusals A consumer request names a producer, key, context, and type, and enters the resolver. One path resolves: the resolver returns the bound value and the apply proceeds. The other path is refusal, and it is not one outcome but five, each on its own arrow and each ending where the apply never runs — unknown producer, missing key, wrong type, ambiguous binding, and cycle. Every one is named before any provider call runs. RESOLVE OR REFUSE one resolver · five refusals request producer · key · context · type resolver locate() walks the address then checks the type apply proceeds resolver returns the bound value resolved apply never runs unknown producer producer not published missing key key absent in this context wrong type resolved, not the expected type ambiguous binding two sources, one address cycle requires forms a loop refused
One resolver, one resolved path, five refusals
contract.ts
// Resolves: apply may proceed.
const net = resolve(catalogue, request)
console.log(net.vpcId, net.privateSubnetIds)
//   vpc-789 [ 'subnet-x', 'subnet-y' ]

// refuse runs one bad composition and prints its refusal, so the file
// runs top to bottom instead of stopping at the first thrown error.
const refuse = (cat: Entry[], req: Request<NetworkRef>) => {
  try {
    resolve(cat, req)
  } catch (e) {
    console.log((e as Error).message)
  }
}

// Each of the following is refused before any provider call.

// unknown producer
refuse(catalogue, { ...request, producer: 'enviroment' })
//   unknown producer: 'enviroment' is not published in prod

// missing key: published in staging, not in prod
refuse([catalogue[0]], request)
//   missing key: 'environment/network.primary' exists, but not in prod

// wrong type: the value resolves, but it is a bare string[]
refuse(
  [
    {
      producer: 'environment',
      key: 'network.primary',
      context: 'prod',
      source: 'env-stack',
      value: ['subnet-x', 'subnet-y'],
    },
  ],
  request,
)
//   wrong type: 'environment/network.primary' resolved, but not as NetworkRef

// ambiguous binding: a copied stack kept the original producer identity,
// so two sources publish the same address with different values
refuse(
  [
    ...catalogue,
    {
      producer: 'environment',
      key: 'network.primary',
      context: 'prod',
      source: 'env-stack-copy',
      value: {
        kind: 'NetworkRef',
        vpcId: 'vpc-999',
        privateSubnetIds: ['subnet-p', 'subnet-q'],
      },
    },
  ],
  request,
)
//   ambiguous binding: 'environment/network.primary' in prod is published
//   by 2 sources: env-stack, env-stack-copy

// cycle: the environment cannot publish until a workload value resolves
refuse(
  [
    {
      producer: 'environment',
      key: 'network.primary',
      context: 'prod',
      source: 'env-stack',
      requires: [{ producer: 'workload-api', key: 'network-config' }],
      value: catalogue[1].value,
    },
    {
      producer: 'workload-api',
      key: 'network-config',
      context: 'prod',
      source: 'workload-api-stack',
      requires: [{ producer: 'environment', key: 'network.primary' }],
      value: null,
    },
  ],
  request,
)
//   cycle: environment/network.primary -> workload-api/network-config
//          -> environment/network.primary in prod

Map it back to the series

  • NetworkRef and isNetworkRef are the typed capability a producer publishes, from Resolve by contract. The type carries the semantic promise; the subnet IDs are only the backing values.
  • Request names the address (a producer and a key), the context, and the expected type. The consumer selects the address, and resolution returns the value bound to it in the current context.
  • locate and resolve are the resolver gate from Fail before apply, and the five refusals are its five contract failures.
  • The catalogue array is the transport, not the public contract: stack outputs, a registry, or a manifest, kept behind the address so the consumer never reads it directly.

This leaves out, on purpose, everything that is not the contract: the tier cut from Start with the shape, lifecycle membership from Define tier membership, the binding rules that govern shared-surface mutation in Fail before apply, wave ordering, and the deletion order from Re-cut the system. Appendix A runs those together in the companion repository. This is only the seam where a consumer asks and the resolver answers or refuses.