provisioning-code/.ncl-cache/23e80f937250cd5ae2fff798c36c1e849da8197ea68fbabc7eb57b9bb0ae68ec.json

113 lines
8.8 KiB
JSON

{
"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: `<domain>.<name>` (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"
}