Commit ba872226 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: Add ADR-0039 to define `source_spec` as a publisher-stated provenance...

feat: Add ADR-0039 to define `source_spec` as a publisher-stated provenance for network capability commands
parent 8caf4367
Loading
Loading
Loading
Loading
+132 −0
Changes for decisions/0039-source-spec-stated-by-publisher.md: 132 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0039: `source_spec` is stated by the publishing component, never derived or synthesized by SRM"
description: "Network capability commands carry the canonical source_spec so OEG and FM each state which external specification their payload was mapped from; SRM stores it, falls back to the catalog template only while OEG is the sole publisher, and never derives provenance from the envelope source field."
tags: [adr]
timestamp: 2026-08-27T00:00:00Z
status: "accepted"
---

# ADR-0039: `source_spec` is stated by the publishing component, never derived or synthesized by SRM

**Status:** accepted
**Date:** 2026-08-27
**Affects:** [srm/interface-contract.md](/srm/interface-contract.md), [srm/canonical-parameters-schema.md](/srm/canonical-parameters-schema.md), [oeg/interface-contract.md](/oeg/interface-contract.md), [fm/overview-and-architecture.md](/fm/overview-and-architecture.md), extends [ADR-0003](/decisions/0003-canonical-jsonb-parameter-shapes.md)

## Context

Every canonical Capability Request carries `source_spec` — the family, API, and version of the
external specification the payload was normalized **from**
([srm/canonical-parameters-schema.md](/srm/canonical-parameters-schema.md) §1). It is audit-only:
SRM stores it and never branches on it (ADR-0003).

Only the publisher can state it honestly. OEG and the TMF/GSMA mappers own the
external-spec-to-canonical translation and are the only components that ever saw the original
request (ADR-0003, canonical §5 rule 4). By the time a command reaches SRM, the CAMARA or OPG
payload is gone.

But `NetworkCapabilityPayloadV1` ([srm/interface-contract.md](/srm/interface-contract.md) §B.5)
had no field for it. With nowhere for the publisher to put the truth, SRM synthesized one:
`family=internal`, `api="network.capability.activate"`, `version="1.0.0"` — a version
corresponding to no real API. The true answer for a CAMARA QoD activate is
`camara` / `quality-on-demand` / `0.10.0`, which the platform already records correctly on the
seeded QoD service specification (ADR-0035).

Because `source_spec` is audit-only, a wrong value is worse than an absent one. An empty field
makes an investigator go looking; a confident wrong one stops them looking.

**Two publishers, mapping from different specifications.** FM publishes the same
`command.srm.network.capability.{activate,update,deactivate}` subjects that OEG does
([fm/overview-and-architecture.md](/fm/overview-and-architecture.md)), and it maps from
**GSMA OPG.04 v6.0** over the EWBI, while OEG maps from **CAMARA**. Both resolve to the same
`service_capability_requirement` — for QoD, the single platform-seeded specification of ADR-0035
— whose template holds exactly one `source_spec`. One template value cannot describe both
publishers.

Options considered:

- **(a) Keep synthesizing, but honestly** — `family=internal`, `api` naming the internal command,
  `version` from the envelope `schema_version`. Free and no longer false, but it records that SRM
  built the object from a command, which is not the question `source_spec` asks. The real
  provenance stays lost.
- **(b) Derive it from the envelope `source` field.** `source` is a four-value enum
  (`nbi_camara`, `nbi_tmf`, `operator_portal`, `federation`). It can suggest a *family* and can
  never supply `api` or `version` — the two parts that actually answer "which specification
  version did we map from", so those must still be invented. It also conflates transport with
  provenance: `source` names the component that published the message, not the specification the
  payload was mapped from, and §B.1 already fixes `source` as provenance-of-publisher only.
- **(c) Make `source_spec` nullable and omit rather than guess.** Honest, but
  `Parameters.source_spec` is required, so this is a canonical shape change, and it gives up
  provenance that the publisher already holds.
- **(d, chosen) Add the field to the command** so the publisher states it.

## Decision

`source_spec` becomes an **optional** field on `NetworkCapabilityPayloadV1` (§B.5) and
`NetworkCapabilityUpdatePayloadV1` (§B.6), carrying the canonical `srm.params/v1 → source_spec`
object.

SRM resolves it in this order:

1. The command's `source_spec`, stored verbatim.
2. Otherwise the `source_spec` on the matched `service_capability_requirement.parameters` — the
   catalog template written at onboarding, which is real recorded provenance rather than a
   synthesized one.
3. Otherwise the activation is invalid.

SRM **never** derives `source_spec` from the envelope `source`, and **never** synthesizes a
family, API name, or version.

### The template fallback is transitional, and its end is part of this decision

Arm 2 is correct only while OEG is the sole publisher, because OEG's provenance matches the
provenance seeded on the template. It is not a permanent design. Therefore:

- **FM sets `source_spec` on its first network-capability publish** (`gsma_opg`, the OPG.04 API,
  `6.0`). FM never relies on the fallback.
- **OEG sets `source_spec` before FM begins publishing**, so that no publisher depends on the
  fallback once two exist.
- Once both publishers set it, `source_spec` becomes **required** and arm 2 is deleted.

Optional is a migration state with a defined end, not an open-ended compromise. The window in
which the fallback would silently misattribute an OPG.04 request as CAMARA is the reason the
ordering constraint above is part of this decision rather than deployment advice.

## Consequences

What becomes easier:

- Provenance becomes true rather than decorative. "Which specification version was this mapped
  from?" is answerable from `capability_instance.parameters_snapshot` months later.
- Removing the derivation deleted the `source`-to-family mapping, the hardcoded per-capability API
  names, the fabricated `1.0.0` version, and the activation context object that carried it.
- The rule is uniform across activate and update, so neither subject invents its own convention.

What becomes harder:

- A two-phase migration across three components (SRM, OEG, FM), and until it completes a reader
  must know the two-arm precedence to interpret a stored `source_spec`.
- Publishers must now track the vendored version of the specification they map from, rather than
  leaving it implicit.

Follow-ups this decision creates:

- **OEG** — set `source_spec` in `build_activate_command` (`camara` / `quality-on-demand` /
  the vendored `info.version`). Required before FM ships.
- **FM** — set `source_spec` on every `command.srm.network.capability.*` publish
  (`gsma_opg` / OPG.04 API / `6.0`). Required from FM's first publish.
- **SRM** — make the field required and delete the template fallback once both are done.

Deferred or excluded:

- **Deploy commands.** `SrmServiceDeployV1` does not carry a Capability Request, so it has no
  `source_spec` to state. Unchanged by this record.
- **Sync query endpoints** (§E.2). They persist nothing, so provenance has no row to live on. If
  a query ever becomes persisted, this rule applies to it unchanged.

Revisit trigger:

- **Already fired.** FM publishing network-capability commands is the trigger, and it is planned
  rather than hypothetical — this record therefore ships with the migration scheduled. The next
  genuine revisit is a third publisher, or a publisher that maps one subject from more than one
  specification family, which would make `source_spec` vary per request within one publisher.
+1 −0
Changes for decisions/index.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -41,3 +41,4 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0036](0036-srm-owns-network-capability-expiry.md) - SRM owns expiry of async network capabilities and reports it via event.srm.operation.status metadata (qos_status_info); OEG runs no timer and maps it to CAMARA DURATION_EXPIRED
* [ADR-0037](0037-canonical-target-ports.md) - target.device.ports / target.application_server.ports join srm.params/v1's canonical target descriptor, generic across every capability with a device/application-server target rather than allowlisted per capability_type
* [ADR-0038](0038-device-targeted-control-path-resolution.md) - device-targeted capabilities resolve their control path by probing candidate adapters with can_serve_target; the loop and the choice stay in SRM, the per-backend answer comes from TF-SDK
* [ADR-0039](0039-source-spec-stated-by-publisher.md) - source_spec is stated by the publishing component (OEG from CAMARA, FM from OPG.04) on network capability commands; SRM never derives it from the envelope source and never synthesizes a family, API, or version
+15 −0
Changes for srm/interface-contract.md: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -184,6 +184,20 @@ Top-level (envelope +):
| `target` | object | Yes | Canonical target (`srm.params/v1` → `target`) |
| `profile_ref` | string | No | Catalog/profile reference when applicable |
| `parameters` | object | Yes | Canonical parameters (`srm.params/v1` → `parameters`); capability-specific only |
| `source_spec` | object | No | Canonical provenance (`srm.params/v1` → `source_spec`): which external spec this payload was normalized **from**. See below. |

**`source_spec` — the publisher states the provenance.** `source_spec` records which external
API family, API, and version the canonical payload was translated from
([canonical-parameters-schema.md](/srm/canonical-parameters-schema.md) §1). Only the publisher
knows it: OEG and the TMF/GSMA mappers own the external-spec-to-canonical translation
(ADR-0003), so only they have seen the original request. SRM stores it and never branches on it.

Optional for additive compatibility (§A evolution rule). When absent, SRM falls back to the
`source_spec` on the matched `service_capability_requirement.parameters` — the catalog template
written at onboarding, which is a real recorded provenance rather than a synthesized one. SRM
does **not** derive `source_spec` from the envelope `source` field: `source` records which
component published the message, not which specification the payload was mapped from, and the
two differ (FM may relay a CAMARA-shaped request with `source=federation`).

**`profile_ref` → snapshot merge.** Command payloads may carry `profile_ref` at the top of
`network_capability` (this contract). When persisting `capability_instance.parameters_snapshot`,
@@ -280,6 +294,7 @@ until separately specified.
| `target` | object | No | Replacement target when the capability allows retargeting |
| `profile_ref` | string | No | Replacement profile; merged into snapshot as `parameters.profile_ref` |
| `parameters` | object | Yes | Canonical parameter patch (`srm.params/v1` → `parameters`) |
| `source_spec` | object | No | Canonical provenance (`srm.params/v1` → `source_spec`), same rule as §B.5 |

Exactly one of `service_instance_id` or `external_ref` must identify the realization
(both or neither → invalid; `failed_before_start`, §A). `service_instance_id` alone is valid only when the instance