Add the TF-SDK spec and ADR-0055 to ADR-0060

This adds tf-sdk/ with the usual three-part spec, same layout as fm/, plus six ADRs for rules the existing records assume but never write down.

ADR What it says
0055 Topology discovery gets its own interface instead of optional methods on CapabilityExecutionAdapter. Amends ADR-0027.
0056 capability_kind names what a capability is, not what you do to it. So deploy_workload is a kind and scale_workload is not.
0057 If an adapter can't execute the target SRM resolved, it raises instead of picking another one.
0058 supported_actions and supported_runtime_kinds come from TF-SDK, not from the topology declaration.
0059 SRM bootstraps its capability surface by calling discovery, not by reading a file itself.
0060 Rename the package from sunrise6g-opensdk to openop-tfsdk.

I started at 0055 because 0045, 0047, 0048 and 0053 are each used twice on different branches, so 0054 is the highest number that's safely free.

Read the three docs in order: overview, requirements, implementation. The ADRs have the reasoning behind anything the specs just state as a rule.

Nothing else changes except one line in ADR-0027's status, recording that 0055 amends it.

Merge request reports

Loading
+2 −2
Changes for decisions/0027-tfsdk-generic-capability-adapter-interface.md: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@ status: "accepted"

# ADR-0027: TF-SDK exposes one generic `CapabilityExecutionAdapter` interface across all domains

**Status:** accepted
**Status:** accepted — the four discovery methods are moved off this interface onto a separate `TopologyDiscoveryAdapter` by [ADR-0055](/decisions/0055-topology-discovery-is-a-separate-adapter-interface.md) (they were "optional" members here, which left an empty result ambiguous). [ADR-0061](/decisions/0061-use-short-execution-action-vocabulary.md) proposes shorter method names; the generic execution interface and `capability_kind` dispatch stand.
**Date:** 2026-07-07
**Affects:** [platform/architecture.md](/platform/architecture.md), [platform/future-architecture.md](/platform/future-architecture.md), [srm/architecture.md](/srm/architecture.md), TF-SDK

@@ -41,7 +41,7 @@ TF-SDK exposes a single generic interface — `activate_capability` / `update_ca
discovery methods (`get_zones`, `get_domains`, `get_capabilities`, `get_control_path_bindings`) —
implemented uniformly across compute, network, and future domains (charging, consent,
intelligence, data, business, trust, custom). Capability-specific behavior is selected by a
`capability_kind` string (e.g. `deploy_workload`, `qod_session`, `traffic_influence`) carried in
`capability_kind` string (e.g. `workload`, `qod_session`, `traffic_influence`) carried in
the normalized request, never by a distinct adapter base class per domain. Adapter/executor
resolution itself is governed separately by ADR-0004 (`control_path_ref` via the runtime
ControlPathRegistry); this decision is scoped to the shape of the interface SRM codes against,
+137 −0
Changes for decisions/0055-topology-discovery-is-a-separate-adapter-interface.md: 137 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0055: Topology discovery is a separate TopologyDiscoveryAdapter interface, not optional methods on CapabilityExecutionAdapter"
description: "Discovery moves off the generic execution interface onto its own abstract interface, so an implementation can answer what can be reached without claiming it can execute anything, and vice versa."
tags: [adr, tf-sdk, srm]
timestamp: 2026-09-23T00:00:00Z
status: "accepted"
---

# ADR-0055: Topology discovery is a separate `TopologyDiscoveryAdapter` interface

**Status:** accepted
**Date:** 2026-09-23
**Affects:** [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md), [tf-sdk/requirements-and-design.md](/tf-sdk/requirements-and-design.md), [srm/architecture.md](/srm/architecture.md), TF-SDK; amends [ADR-0027](/decisions/0027-tfsdk-generic-capability-adapter-interface.md)

## Context

[ADR-0027](/decisions/0027-tfsdk-generic-capability-adapter-interface.md) put
the four discovery methods on the generic interface as **optional** members:

> a single generic interface — `activate_capability` / `update_capability` /
> `deactivate_capability` / `query_capability` / `get_capability_runtime_state`,
> plus optional discovery methods (`get_zones`, `get_domains`,
> `get_capabilities`, `get_control_path_bindings`)

The quotation preserves ADR-0027's original names; [ADR-0061](/decisions/0061-use-short-execution-action-vocabulary.md)
proposes the current public execution names.

Implementing the discovery slice made the cost of "optional" concrete. The two
method groups differ in every property that matters:

- **Who implements them.** Execution is per backend: one adapter per NEF, per
  cluster. Discovery is per *source of the surface*, and the first working
  implementation reads a bootstrap declaration and calls no backend at all. It
  can answer every discovery method and execute nothing.
- **When they are called.** Discovery runs at bootstrap and reconciliation,
  against SRM's planning surface. Execution runs per operation, against a
  target SRM has already resolved.
- **What a missing method means.** An execution adapter that cannot answer
  `get_zones` is not broken — its zones come from elsewhere. But an optional
  method has no honest "not mine" answer: it returns an empty list, which is
  also what "I looked and there are none" returns. A planner cannot tell a
  silent non-implementer from an empty backend.

`can_serve_target` is genuinely optional in the ADR-0040 sense — it has a
specified default (`can_serve`) that is correct when unimplemented. The
discovery methods have no such default, which is what makes them a different
kind of optional.

Options considered:

- **(a) Keep them optional on one interface.** No new type; leaves empty-list
  ambiguity and forces every execution adapter to carry four methods it has no
  source for.
- **(b) A capability-declaration flag** — an adapter states whether it supports
  discovery. Removes the ambiguity, adds a runtime check where a type would
  do, and the flag can disagree with the implementation.
- **(c, chosen) A separate abstract interface.** An implementation declares by
  its type which questions it answers.

## Decision

Topology discovery is `TopologyDiscoveryAdapter`, an abstract interface with
four required methods: `get_zones`, `get_domains`, `get_capabilities`,
`get_control_path_bindings`. Every argument is a filter; a ref matching nothing
yields an empty list, never an error.

Capability execution stays on `CapabilityExecutionAdapter` exactly as ADR-0027
decided, keeping `can_serve_target` as its one genuinely optional member.

A concrete adapter may implement either interface or both. Implementing
`TopologyDiscoveryAdapter` means every discovery method is answered; the empty
list therefore recovers its single meaning.

**The interface speaks platform vocabulary, not backend vocabulary.**
`get_zones` asks what the platform's capability surface looks like within the
adapter's configured scope — it does not ask what objects the backend holds. A
NEF has no zones; a cluster does not know it is a compute domain. This is why
the two interfaces normally have different implementers rather than being two
halves of one adapter: the execution half speaks Helm releases and
`AsSessionWithQoSSubscription`, the discovery half speaks `zone`, `domain` and
`capability`, and only the second is a concept the platform owns.

This amends ADR-0027's "plus optional discovery methods" clause. Everything
else in that record stands: one generic execution interface across every
domain, behavior selected by `capability_kind`, no per-domain base class, and
no change to SRM's adapter-dispatch code when a domain is added.

## Consequences

What becomes easier:

- A bootstrap-declaration topology source is a first-class implementation
  rather than a partial adapter, and the shipped one is exactly that.
- An empty discovery result means one thing.
- An execution adapter carries four fewer methods it has no business answering.
  `zone`, `domain` and `capability` are OOP's normalization vocabulary, not
  backend concepts — a NEF has no zones, and a cluster does not know it is a
  compute domain — so the surface is declared by the platform, not reported by
  the backend an execution adapter is bound to.

What becomes harder:

- Two interfaces to version instead of one — three surfaces once
  [ADR-0058](/decisions/0058-adapters-declare-their-own-executable-surface.md)'s
  `capability_surface` function is counted. A change to the normalized topology
  models now touches a contract that execution adapters may not implement, so
  the compatibility rules in
  [tf-sdk/requirements-and-design.md](/tf-sdk/requirements-and-design.md) §I
  apply to each independently.
- Naming a "discovery interface" invites backend introspection to be bolted
  onto it — list the namespaces, list the configured DNNs. Those are backend
  vocabulary and answer a different question, so they belong behind their own
  surface rather than behind these four methods.

  Reaching `zone`/`domain`/`capability` from such results takes two steps that
  should not be collapsed. Translating backend facts into normalized form is
  TF-SDK's by charter. **Assigning** them a place in the topology — this cluster
  *is* domain X in zone Y — is not a translation at all: no property of a
  cluster makes it a compute domain, and the assignment is an operator decision
  recorded in a declaration. Serving that decision is this interface's job
  ([ADR-0059](/decisions/0059-topology-bootstrap-goes-through-the-discovery-interface.md)); making it is nobody's code's.

Deferred or excluded:

- Whether SRM bootstraps from its own configuration or from these methods at
  launch. Settled separately by [ADR-0059](/decisions/0059-topology-bootstrap-goes-through-the-discovery-interface.md): it calls these methods.
- Adapter construction, which stays with
  [ADR-0041](/decisions/0041-tfsdk-owns-adapter-construction.md): the factory
  returns whatever the adapter kind builds, and the caller uses whichever
  interface it needs.

Revisit trigger:

- A backend requiring discovery and execution to share state — a session, a
  connection pool, a cursor — such that splitting them costs a round trip per
  operation.
+126 −0
Changes for decisions/0056-capability-kind-names-a-capability-not-an-operation.md: 126 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0056: capability_kind names what a capability is, never what is being done to it"
description: "A capability is a thing, not an action. It is a kind of its own unless it operates on something another capability already created, in which case it is an action carried by the method and by supported_actions."
tags: [adr, tf-sdk, srm]
timestamp: 2026-09-23T00:00:00Z
status: "accepted"
---

# ADR-0056: `capability_kind` names what a capability is, never what is being done to it

**Status:** accepted
**Date:** 2026-09-23
**Affects:** [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md), [tf-sdk/requirements-and-design.md](/tf-sdk/requirements-and-design.md), [srm/persistence-model.md](/srm/persistence-model.md), [srm/interface-contract.md](/srm/interface-contract.md), SRM, TF-SDK; composes with [ADR-0027](/decisions/0027-tfsdk-generic-capability-adapter-interface.md), [ADR-0004](/decisions/0004-capability-surface-topology.md)

## Context

[ADR-0027](/decisions/0027-tfsdk-generic-capability-adapter-interface.md) made
`capability_kind` the discriminator for adapter behavior without saying what a
kind may name. A working contract revision then named kinds after operations:
`update_workload`, `scale_workload`, `terminate_workload` and
`get_workload_runtime_state` alongside `deploy_workload` — five names for one
thing and the four things you do to it. This revision renames the remaining
kind to `workload` to name the resource rather than its creation operation.

Two failures follow, and neither announces itself.

**A request finds nothing, and "nothing" is ambiguous.** SRM narrows candidates
by `capability.kind` before any other filter
([interface-contract.md §B.5](/srm/interface-contract.md) step 1). Ask for
`scale_workload` when no adapter registered that name and you get zero
candidates, which surfaces as "no available zone/domain" — indistinguishable
from a real capacity problem. The operator debugs topology; the fault is
vocabulary.

**The catalogue starts claiming impossible things.** `scale_workload` as its own
capability says a domain could support scaling without supporting deployment.
No backend is ever in that state.

The shape that avoids both is resource orientation, as in Kubernetes RBAC
(`resources: [deployments]`, `verbs: [get, create, update]`), which has
`kind: Deployment` and no `kind: ScaleDeployment`.

Options: **(a)** a naming convention in guidance, which decides no ambiguous
case; **(b)** a closed enum of kinds, which removes ADR-0027's property that a
new domain needs only a new value and an adapter; **(c, chosen)** a structural
test.

## Decision

**A capability is a thing, not an action.** `kind` names the thing. Verbs live
on the invoked method and on `ControlPathBinding.supported_actions`, where SRM
reads them during planning — and on `ResolvedCapabilityTarget.action` for as
long as that field survives [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md) §F2.

**The test is one question:**

> *Am I operating on something another capability already created?*
> **Yes** → it is an action on that capability. **No** → it is a kind of its own.

| What you do | Operating on another capability's creation? | Verdict |
|---|---|---|
| Deploy a workload | No — you create it | kind `workload` |
| Change its replicas | Yes — same object | action `update` |
| Terminate it | Yes — same object | action `terminate` |
| Read its state | Yes — same object | action `read_runtime_state` |
| Open a QoD session | No — your own object, your own `external_id` | kind `qod_session` |
| Ask where a device is | No — you touch nobody's realization | kind `location_context` |

The contracted vocabulary is therefore `workload`, `qod_session`,
`traffic_influence`, `location_context`, `device_status` and
`query_available_resources`. `update_workload`, `scale_workload`,
`terminate_workload` and `get_workload_runtime_state` are **not kinds** — each
is an action on `workload`'s realization.

The same question settles what is not contracted yet: a one-off SIM-swap check
and a SIM-swap subscription are **two kinds**, because the subscription creates
an object of its own and the check creates nothing.

**The question decides whether an operation is a separate kind.** The old name
`deploy_workload` passed that structural test, but `workload` names the
capability more clearly and is the sole contracted kind for its lifecycle.
`query_available_resources` also passes the test because it does not operate
on another capability's creation; its operation word is a naming smell, not a
second resource identity. Its name is unchanged by this decision.

Adapters must not advertise one capability under more than one kind.
SRM requirements, topology declarations and TF-SDK adapter surfaces must use
`workload` together. `deploy_workload` is not a compatibility alias; migrate
persisted requirements and capability rows before switching an existing
deployment, and do not mix the old and new values during planning.

## Consequences

What becomes easier:

- The silent non-match is closed at source: there is no second name for a
  request to miss, and `supported_actions` becomes the single place a planner
  reads what can be done.
What becomes harder:

- `capability.kind` carries no CHECK in `srm_db`
  ([persistence-model.md §B.3](/srm/persistence-model.md)) and gains none here,
  so the schema cannot enforce this. Neither can a test, fully: the rule turns on
  capability identity, and two kinds may share a surface legitimately —
  `location_context` and `device_status` do. The contract test in
  [tf-sdk/implementation-and-delivery.md](/tf-sdk/implementation-and-delivery.md) §S.2
  asserts the named non-kinds and leaves the rest a review obligation.
  Whether a lifecycle category on the capability definition could make part of
  this structural instead is [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md) §F7.
- Answering the question needs an adapter-implementer's knowledge of whether a
  backend object exists with its own lifecycle. Review a new domain's kinds
  before advertising them: a wrong call is costly to undo once bindings exist.
- The rule is silent below the kind. Update and scale both invoke
  `update`, so `supported_actions` cannot advertise them apart.
  Kubernetes answers this with subresources; the equivalent here is widening the
  action vocabulary to intent, left open as
  [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md) §F2.
Deferred or excluded:

- The mechanics of migrating already persisted kind values; the coordinated
  SRM/TF-SDK rollout must provide them before adopting `workload` in a running
  deployment.

Revisit trigger: an operation that creates a backend object whose lifetime is
not its own, where the question needs a third answer rather than a judgement.
+120 −0
Changes for decisions/0057-no-silent-substitution-of-a-resolved-target.md: 120 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0057: an adapter that cannot execute the resolved target fails with a typed error and never substitutes another"
description: "Once SRM has resolved a zone, domain, capability, binding and control path, an adapter executes that target or raises InvalidTargetRef. Choosing a different one would make SRM's inventory a record of something that did not happen."
tags: [adr, tf-sdk, srm]
timestamp: 2026-09-23T00:00:00Z
status: "accepted"
---

# ADR-0057: an adapter never silently substitutes a resolved target

**Status:** accepted
**Date:** 2026-09-23
**Affects:** [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md), [tf-sdk/requirements-and-design.md](/tf-sdk/requirements-and-design.md), [srm/persistence-model.md](/srm/persistence-model.md), TF-SDK; composes with [ADR-0038](/decisions/0038-device-targeted-control-path-resolution.md), [ADR-0040](/decisions/0040-target-serveability-is-a-tri-state.md), [ADR-0045](/decisions/0045-pin-narrows-candidates-not-probing.md)

## Context

Several accepted records place selection firmly in SRM.
[ADR-0038](/decisions/0038-device-targeted-control-path-resolution.md) keeps the
candidate loop and the choice in SRM.
[ADR-0040](/decisions/0040-target-serveability-is-a-tri-state.md) has the
adapter answer only for its own control path.
[srm/interface-contract.md](/srm/interface-contract.md) §B states it directly:
"Selection is never delegated to TF-SDK."

What none of them state is what an adapter must do when the target it was
handed turns out to be unusable at execution time — the backend moved, the
namespace is gone, the binding points at a cluster this instance cannot reach.
At that moment the adapter frequently *can* see an alternative: a second
cluster in the same domain, another endpoint on the same NEF. Reaching for it
looks like resilience.

It is not. SRM records the resolved binding in
`capability_instance.control_path_binding_id` and reconciles against it. An
adapter that executed elsewhere produces a success whose inventory row points
at a backend that holds nothing, and reconciliation will find drift it cannot
explain — or, worse, find none, because the adapter reports state from the
backend it actually used. The failure is silent, durable, and surfaces as an
unrelated incident weeks later.

The hazard is not hypothetical for this boundary specifically: an adapter
holds exactly the vendor knowledge that makes a plausible alternative easy to
find, and none of the orchestration context that would say whether it is
allowed.

Options considered:

- **(a) Leave it implied.** [interface-contract.md §B.5](/srm/interface-contract.md)
  already says "Selection is never delegated to TF-SDK", and the TF-SDK working
  contract carries it in an anti-pattern list. But both speak about *resolution*
  time; neither states what an adapter must do when the target fails at
  *execution* time, which is when the temptation appears.
- **(b) Allow substitution within the same domain**, on the grounds that the
  domain was what SRM actually chose. Rejected: the binding, not the domain, is
  what is persisted and reconciled against, and a domain may span backends with
  different state.
- **(c, chosen) Fail with a typed error, always.**

## Decision

When an adapter cannot execute through the `control_path_ref` or binding SRM
resolved, it raises a typed `TFSDKError` — `InvalidTargetRef`
(`INVALID_TARGET_REF`, not retryable) for an unresolvable zone, domain,
capability, binding or control-path reference.

It does not switch to another backend, zone, domain, capability or binding, and
it does not widen its own scope to find one. The route check happens **before**
the backend call, so a refusal leaves nothing partially applied — and the check
itself is local. A route check that queried the backend would turn an outage
into `InvalidTargetRef`, which is not retryable, so SRM would permanently fail
an operation it should have retried.

SRM owns what happens next: fallback or failover policy, re-resolution against a
different candidate, or failing the operation — that policy needs orchestration
context the adapter does not have. SRM's existing refusal-mapping rules apply
unchanged: a typed error that reports the control path could not answer is an
operator-side fault, answered `503 .../capability-execution-failed` on the sync
path, never a `4xx` that would assert something about the subscriber
([interface-contract.md §E.2](/srm/interface-contract.md)).

This makes explicit what ADR-0038, ADR-0040 and ADR-0045 assume at resolution
time and extend to execution time.

## Consequences

What becomes easier:

- An inventory row's `control_path_binding_id` is true by construction: either
  the recorded binding executed, or the operation failed.
- Reconciliation compares like with like, because observed state comes from the
  backend the record names.
- Fallback becomes an SRM policy that can be stated, reviewed and changed in
  one place, rather than per-adapter behaviour discovered during an incident.

What becomes harder:

- A deployment whose backends genuinely are interchangeable loses a free
  retry: SRM must model the alternative as a second binding for the same
  capability and let placement choose it.
- An adapter author must resist an obviously available fix. The contract test
  in [tf-sdk/implementation-and-delivery.md](/tf-sdk/implementation-and-delivery.md) §S.2
  asserts that an unresolvable target raises *and performs no backend call*,
  because raising after acting is the version of this mistake that review
  misses.

Deferred or excluded:

- Bounded technical retries against the **same** target are unaffected. A
  connection reset retried against the resolved backend is not substitution.
- Whether SRM re-resolves automatically after `InvalidTargetRef`, and under
  what policy, is SRM's and not settled here.
- Detection of the related fault — a binding whose `control_path_ref` the
  running registry never loaded — is
  [srm/architecture.md](/srm/architecture.md) §F5, still open.

Revisit trigger:

- A capability whose execution semantics make the specific backend genuinely
  irrelevant to the recorded realization, so that substitution could not make
  an inventory row false.
+185 −0
Changes for decisions/0058-adapters-declare-their-own-executable-surface.md: 185 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0058: supported_actions and supported_runtime_kinds are reported by TF-SDK, not read from declared topology"
description: "A binding's fields split into what the operator decides and what the system reports. TF-SDK reports the executable surface without constructing an adapter; an unknown adapter kind raises UnknownAdapterKind, while a registered kind that cannot serve a capability reports empty lists."
tags: [adr, tf-sdk, srm]
timestamp: 2026-09-24T00:00:00Z
status: "accepted"
---

# ADR-0058: TF-SDK reports the executable surface, the declaration does not

**Status:** accepted
**Date:** 2026-09-24
**Affects:** [tf-sdk/requirements-and-design.md](/tf-sdk/requirements-and-design.md), [tf-sdk/implementation-and-delivery.md](/tf-sdk/implementation-and-delivery.md), [srm/persistence-model.md](/srm/persistence-model.md), TF-SDK; composes with [ADR-0004](/decisions/0004-capability-surface-topology.md), [ADR-0041](/decisions/0041-tfsdk-owns-adapter-construction.md), [ADR-0055](/decisions/0055-topology-discovery-is-a-separate-adapter-interface.md)

## Context

Placement selects a binding by capability kind **plus**
`supported_runtime_kinds` and `supported_actions`
([ADR-0004](/decisions/0004-capability-surface-topology.md)). Those two fields
decide where work is sent, so a wrong value routes an operation to a backend
that cannot perform it — and the failure lands after placement has committed,
at the last possible moment.

Both fields sit on `ControlPathBinding`, a topology model, so the obvious
implementation reads them from the topology declaration alongside `ref`,
`priority` and `is_default`. Implementing discovery showed why that is wrong.

Think of the declaration as a **wiring diagram** and the adapter as the
**device plugged in**. The diagram is authoritative that there is a socket in
that room and how to reach it. Only the device knows whether it can run a
kettle — and that answer changes when you swap the device or reconfigure it,
without a single line of the diagram changing.

Two failures follow from asking the diagram.

**The declaration goes stale in silence.** Upgrade the SDK so an adapter gains
an operation, and every declaration still advertises the old list. Nothing
errors; the new operation is simply never matched.

**A control path with no adapter advertises a full surface.** If the declared
adapter kind is one the running distribution does not recognise — a deployment
fault under
[ADR-0041](/decisions/0041-tfsdk-owns-adapter-construction.md) — the binding
would still list every action, and placement would happily select it. Nothing
behind it can execute anything.

Options: **(a)** read both from the declaration, which requires its author to
know every adapter's capabilities and goes stale as above; **(b)** declare and
then validate against the adapter, which needs a rule for which side wins and
two places to edit; **(c, chosen)** have TF-SDK report both for the
control path's adapter kind and config.

## Decision

**A binding's fields split in two.** Some record what the operator decided;
others report what the system found. A declaration is authoritative for the
first group and can only guess at the second.

| Field | Who knows it | Why |
|---|---|---|
| `ref`, `control_path_ref`, `priority`, `is_default`, and the zone/domain/capability structure | **the operator** | Choices about how the topology is laid out |
| `supported_actions`, `supported_runtime_kinds` | **TF-SDK**, for that adapter kind and config | Change with an SDK upgrade or a config change, while the topology stays identical |
| `state` | **the operator** | Whether this part of the surface is *offered* — an administrative statement, not an observation of whether a backend is up |

So `supported_actions` and `supported_runtime_kinds` are **not topology**. They
are reported by TF-SDK for the binding's **adapter kind and its configuration**,
asked per `capability_kind`.

**This is a question about a kind, not the behaviour of an object.** TF-SDK
answers it from `capability_surface(adapter_kind, capability_kind, config)`, a
module-level function that resolves through the **same adapter-kind registry the
factory uses** — a lookup and a static call, so **no adapter instance is
built**. One registry, so the two cannot disagree about which kinds exist. A read path that had to construct adapters would need the factory, and
would inherit whatever a constructor does.

Four rules follow:

1. **Unknown adapter kind raises; unsupported capability kind reports empty.**
   These are different conditions and were once conflated. A kind no adapter
   registers is a misconfiguration, and
   [ADR-0041](/decisions/0041-tfsdk-owns-adapter-construction.md) is explicit
   that a missing adapter "is a deployment fault, not a per-request refusal", so
   the surface function raises exactly as the factory does. A *registered* kind
   asked about a capability it does not serve reports empty lists — there the
   honest answer really is "nothing can be executed here".
2. **No realization, no runtime kind.** A capability that deploys no artifact
   declares none — `query_available_resources` advertising `helm` would let
   placement match a runtime it will never use.
3. **The vocabulary is the five execution operations**: `activate`, `update`,
   `terminate`, `query`, `read_runtime_state` ([ADR-0061](/decisions/0061-use-short-execution-action-vocabulary.md)). One value per operation SRM can invoke,
   because SRM selects candidates by `supported_actions` covering the action
   ([interface-contract.md §B.5](/srm/interface-contract.md) step 1). Omit the
   runtime-state value and reconciliation silently loses every binding it could
   have selected (REQ-SRM-11).
4. **Answering must not touch the backend.** Both questions are about TF-SDK's
   own code and the supplied config, so they are answered locally. Since
   bootstrap runs through discovery
   ([ADR-0059](/decisions/0059-topology-bootstrap-goes-through-the-discovery-interface.md)),
   an answer that required a backend call would make SRM unable to start while
   any backend was down.

**`state` sits on the decided side, and discovery must not invent it.** It says
whether a part of the surface is offered, so the operator owns it exactly as
they own `ref` and `priority`. What discovery owes is not to fabricate one:
where a source declares no state it reports `unknown`, which exists in all four
topology tables precisely so "nobody said" stays distinguishable from "somebody
said yes".

**Topology `state` is not runtime status.** The two layers carry different
vocabularies for that reason — topology is `active | offline | unknown`, while
`capability_instance.state` is `creating | active | updating | degraded |
failed | terminating | terminated`
([persistence-model.md §B.3](/srm/persistence-model.md)). A backend that dies
does not turn its zone `offline`; the failure surfaces on the next call and
reconciliation records it against the realization, not against the topology.
Nothing updates topology `state` at runtime, and nothing should.

This is ITU-T X.731's separation of **administrative state** — what the operator
decided — from **operational** and **usage** state, which are observed.
[srm/architecture.md](/srm/architecture.md) states the same from its side in
assumption E5: zones, domains and capabilities are *registered*, and SRM does
not auto-discover. The vocabulary is the trap. `active` is an X.731 usage-state
word and `offline` reads as operational, so an administrative column is spelled
in the registers of the two things it is not; X.731's own administrative values
are `locked`, `unlocked` and `shuttingDown`. The values are fixed by an accepted
schema and not worth renaming — naming the pattern is enough to stop the
misreading.

This does not conflict with the DDL but does not compose with it automatically:
`srm_db` defaults `state` to `'active'` on insert
([persistence-model.md §B.3](/srm/persistence-model.md)), so SRM must write a
reported `unknown` **explicitly** rather than omit the column. Getting it wrong
breaks both ways, because placement matches only `state='active'`
([persistence-model.md §B.2](/srm/persistence-model.md)): a surface that
silently becomes `active` asserts reachability nobody declared, one that
silently becomes `unknown` is invisible to placement. A declaration is
consequently expected to state `state` at every level.

## Consequences

What becomes easier:

- An adapter upgrade advertises its new operation with no declaration edited,
  so the surface cannot go stale against the code.
- A missing adapter is visible in the surface and fails placement, rather than
  hiding until an operation is in flight.
- Two identically declared control paths with different configuration can
  report different surfaces at all.
- Discovery needs no adapter instance and no construction in its read path: a
  registry lookup and a static call, so it inherits neither backend
  availability nor whatever a constructor does.

What becomes harder:

- A declaration alone no longer describes the full surface: reviewing a
  bootstrap file does not tell an operator what is executable. Discovery output
  does.
- Adding an adapter means writing its surface as well as its class, in one
  place. An earlier draft kept a second registry and had to reconcile the two at
  startup, because a kind registered in one and not the other advertised nothing
  and failed as though the backend simply did not offer the capability. One
  registry removes that failure rather than detecting it.
- Each adapter hand-writes the map from capability kind to operations. A
  lifecycle category on the capability definition would make it derivable —
  [tf-sdk/overview-and-architecture.md](/tf-sdk/overview-and-architecture.md) §F7.
- An adapter must answer honestly for kinds it has never seen, and must not
  grant lifecycle actions to a kind that should not exist — which is how a kind
  forbidden by
  [ADR-0056](/decisions/0056-capability-kind-names-a-capability-not-an-operation.md)
  would acquire an executable surface. The contract tests in
  [tf-sdk/implementation-and-delivery.md](/tf-sdk/implementation-and-delivery.md) §S.2
  cover rules 1-4.

Deferred or excluded:

- **How often the reported values are refreshed.** That they *are* persisted is
  settled — both are JSONB columns on `control_path_binding` with GIN indexes
  (persistence-model.md §B.3). This record governs where their values come
  from, not whether they are stored. Refresh cadence is left open by ADR-0059.
- Detecting a binding whose `control_path_ref` the registry never loaded, at
  write time — [srm/architecture.md](/srm/architecture.md) §F5.

Revisit trigger: a surface that genuinely cannot be answered without a backend
call, which would break rule 4 and make a declared cache necessary.
Loading
Loading