{ "alternatives_considered": [ { "option": "Free-form capability tags (Array String) instead of typed CapabilityEntry", "why_rejected": "Free strings cannot be validated for kind, cannot be queried by type, and cannot carry descriptions. The typed record is required for `provisioning catalog capabilities --type Service` to function and for the `provisioning-dag-integrity` mode to distinguish Required from Optional resolution failures." }, { "option": "Single `capabilities` array with a `direction` discriminant (provides/requires encoded as field)", "why_rejected": "A flat array conflates semantically different operations — providing a capability and requiring one have different validation rules and different consumers. Separate `provides` and `requires` arrays make intent explicit and allow independent schema validation." }, { "option": "Encode conflicts_with as capability-level conflicts (A.provides X conflicts with B.provides X)", "why_rejected": "Capability-level conflicts are more granular but harder to author — taskserv authors must reason about every capability pair. Taskserv-level mutual exclusion (`conflicts_with: [\"containerd\"]`) is the correct granularity for installation-time enforcement and maps directly to the package manager mental model." }, { "option": "Central capability registry file (a single capabilities.ncl across all extensions)", "why_rejected": "A central registry creates a write-contention hotspot when multiple extensions are developed in parallel. Distributed declarations in each metadata.ncl, aggregated by the reflection mode and CLI, achieve the same discoverability with independent authoring." } ], "consequences": { "negative": [ "New taskservs must populate provides/requires/conflicts_with or fail schema validation — increases authoring burden", "Capability IDs are not validated against a central registry — a typo in `requires[].capability` fails silently if no provider declares the misspelled ID", "CapabilityKind enum is closed — adding a new kind requires updating `schemas/lib/dag/contracts.ncl` and re-exporting all metadata files that use it" ], "positive": [ "All 10 built-in taskservs now have typed capability declarations — `provisioning-dag-integrity` runs clean", "CLI `provisioning catalog capabilities` and `provisioning extensions graph` are powered by these declarations", "WorkspaceComposition dependency resolution can validate inter-formula capability chains at dag.ncl export time", "Capability declarations are a first-class artifact — diffable, auditable, versionable in git" ] }, "constraints": [ { "check": { "must_be_empty": true, "paths": [ "catalog/taskservs/" ], "pattern": "id = \"[^.]+\"", "tag": "Grep" }, "claim": "All capability IDs in provides[].id and requires[].capability must use dot-namespaced format: `.` (e.g. `kubernetes.api-server`, `storage.ceph-block`)", "id": "capability-ids-dot-namespaced", "rationale": "Flat IDs (no dot) are ambiguous and collision-prone. The dot namespace convention is the only disambiguation mechanism without a central registry.", "scope": "catalog/taskservs/*/metadata.ncl", "severity": "Hard" }, { "check": { "cmd": "provisioning catalog capabilities 2>/dev/null | length | test { $in > 0 }", "expect_exit": 0, "tag": "NuCmd" }, "claim": "Every taskserv metadata.ncl must declare provides, requires, and conflicts_with — even if as empty arrays", "id": "all-taskservs-must-declare-capability-fields", "rationale": "Missing fields are caught by the schema contract but also by `provisioning-dag-integrity`. A taskserv without declarations is invisible to capability resolution — it will never be identified as a provider or dependency.", "scope": "catalog/taskservs/*/metadata.ncl, schemas/lib/extension-metadata.ncl", "severity": "Hard" }, { "check": { "cmd": "nu -c 'ls catalog/taskservs/ | get name | each { |d| $d | path basename }'", "expect_exit": 0, "tag": "NuCmd" }, "claim": "conflicts_with[] must contain taskserv directory names (e.g. `\"containerd\"`), not capability IDs", "id": "conflicts-with-holds-taskserv-names-not-capability-ids", "rationale": "The conflict resolution algorithm in `provisioning-validate-formula` looks up taskserv names in the extensions registry. Capability IDs in conflicts_with would never match and silently fail to enforce the constraint.", "scope": "catalog/taskservs/*/metadata.ncl", "severity": "Hard" } ], "context": "ADR-016 introduced typed Formula DAGs for intra-server taskserv execution order. To enable formula dependency resolution at the workspace composition layer (inter-formula DAGs, ADR-021), the Orchestrator needs a machine-readable declaration of what each taskserv produces and what it depends on. Without this, a workspace composition DAG cannot validate that a Formula consuming `kubernetes-api-server` has at least one upstream Formula that provides it — the constraint is implicit and unenforced. Ten built-in taskservs existed with only `name/version/description/supported_providers` metadata — no capability declarations.", "date": "2026-04-03", "decision": "Every taskserv `metadata.ncl` file must declare three fields: `provides: Array CapabilityEntry` (capabilities this taskserv makes available after successful execution), `requires: Array CapabilityRequirement` (capabilities this taskserv needs from another provider before it can run), and `conflicts_with: Array String` (taskserv names that are mutually exclusive — installing both would produce an irreconcilable conflict). A `CapabilityEntry` carries `id: String` (dot-namespaced, e.g. `kubernetes.api-server`), `kind: CapabilityKind` (`'Service | 'StorageClass | 'NetworkPolicy | 'Runtime | 'CertManager | 'Monitoring | 'Registry | 'DNS | 'Auth`), and `description: String`. A `CapabilityRequirement` carries `capability: String` (the capability `id`), `kind: RequirementKind` (`'Required | 'Optional`), and `description: String`. These fields are validated by `schemas/lib/extension-metadata.ncl` at Nickel typecheck time and audited at runtime by the `provisioning-dag-integrity` reflection mode.", "id": "adr-020", "ontology_check": { "decision_string": "Extension capability declarations via provides/requires/conflicts_with typed fields in metadata.ncl, validated by extension-metadata schema and provisioning-dag-integrity reflection mode", "invariants_at_risk": [ "type-safety-nickel", "config-driven-always" ], "verdict": "Safe" }, "rationale": [ { "claim": "Machine-readable capability declarations enable schema-time resolution validation", "detail": "The `provisioning-dag-integrity` reflection mode cross-checks every `Required` capability in `requires[]` against the set of `provides[].id` values across all taskservs. An unresolved Required capability is a hard error surfaced before any deployment attempt. Without typed declarations, this check requires reading code comments or documentation." }, { "claim": "ConflictsWith enforces mutual exclusion at the registry level", "detail": "A taskserv pair in `conflicts_with` that both appear in a Formula is caught by `provisioning-validate-formula`. The registry-level declaration makes conflicts auditable and tool-enforceable — no runtime failure needed to discover an incompatible combination." }, { "claim": "CapabilityKind enum scopes the semantic surface of each capability", "detail": "Using a typed enum (`'Service | 'StorageClass | ...`) rather than free-form strings prevents capability ID sprawl. The DAG resolution query `extensions capabilities --type Service` is only possible with a bounded kind set." }, { "claim": "Optional vs Required separation allows partial-graph deployments", "detail": "`'Optional` requirements express soft preferences (e.g. coredns optionally uses an upstream DNS). `'Required` requirements are hard blockers. The distinction enables the orchestrator to warn on unresolved Optional capabilities while failing on unresolved Required ones." }, { "claim": "Dot-namespaced capability IDs provide scoping without a global registry", "detail": "IDs like `kubernetes.api-server`, `storage.ceph-block`, `network.cni` are self-documenting and conflict-resistant without requiring a central registry. The namespace prefix is the domain (kubernetes, storage, network, container, tls, dns, monitoring, identity)." } ], "related_adrs": [ "adr-016-workspace-formula-dag" ], "status": "Accepted", "title": "Extension Capability Declarations: provides/requires/conflicts_with Taxonomy" }