# ADR-0006: Single `Plugin` base class + type/subtype registry as the extensibility mechanism

| Field         | Value       |
|---------------|-------------|
| Kind          | ADR         |
| Status        | Accepted    |
| Decided       | historical  |
| Deciders      | Antoine Berchet |
| Supersedes    | —           |
| Superseded by | —           |

## Context

CIF needs many interchangeable implementations along many independent axes — transport
models, observation operators, minimizers, domains, control vectors, HPC platforms,
transforms, inversion modes, and more (16 recognised plugin types today: `chemistry`,
`controlvect`, `datavect`, `datastream`, `domain`, `measurements`, `minimizer`, `mode`,
`model`, `obsoperator`, `obsparser`, `obsvect`, `platform`, `simulator`, `transform`,
`setup`). Every one of them needed a common way to be declared in the user's YAML
configuration, discovered on disk, dynamically imported, instantiated, validated against
its accepted arguments, and wired to the plugins it depends on — without inventing a
bespoke loading mechanism per subsystem.

CIF's contributors are largely atmospheric scientists, not software engineers, so the
mechanism also had to keep the barrier to adding a new plugin low: a contributor should
be able to add a new model, obs operator, or transform by writing the required routines
and declaring their arguments inside a plugin directory, without needing to understand
Python class syntax, inheritance, or how CIF's internals wire plugins together — CIF
builds and interfaces the instance dynamically from that directory's conventions.

See {doc}`/documentation/index` for the plugin catalogue this produces, and
[`pycif/utils/classes/baseclass.py`](https://gitlab.in2p3.fr/satinv/cif/-/blob/devel/pycif/utils/classes/baseclass.py)
for the implementation.

## Decision

We implement extensibility as a single `Plugin` base class with:
- a class-level `plugin_types` registry mapping each recognised type name to its module
  path and class name, plus `plugin_subtypes` for nested categories (e.g. `datastream` →
  meteo / flux / background / field; `transform` → basic / complex / system);
- a YAML `plugin: {name, version, type, subtype}` declaration every plugin instance uses
  to identify itself;
- global registries (`registered`, `loaded_instances`, `reference_instances`,
  `subreference_instances`) that dynamically `importlib`-load and cache plugin
  modules/instances;
- an `input_arguments` convention each plugin module declares for its own accepted YAML
  keys, checked against at load time (`unauthorized_arguments`).

Every plugin category subclasses `Plugin` (e.g. `Platform(Plugin)`, `Model(Plugin)`, ...)
rather than defining its own independent loading mechanism.

## Consequences

- Adding a new plugin *category* means one entry in `plugin_types` / `plugin_subtypes`
  plus one subclass — loading, registry, and YAML-wiring are inherited for free.
- Adding a new plugin *within* an existing category means writing a module with the
  expected `_name` / `_version` / `input_arguments` conventions under
  `pycif/plugins/<type>/<name>/` — there is no registration code to write by hand.
- This base class is the "plugin interface contract" referred to elsewhere in the
  contribution rules (a new plugin conforming to an existing interface needs no RFC;
  changing the interface itself does) — see `docs/decisions/README.md`.
- It also underpins config validation and JSON-Schema generation (ADR-0008): a plugin's
  declared `input_arguments` is the single source of truth for what it accepts. Any
  documentation describing plugin arguments that isn't generated from `input_arguments`
  can silently drift from what the code actually validates.
