# schemas/lib/radicle.ncl — Radicle Heartwood governance substrate types (ADR-038) # # Three repo families per workspace: policy, desired, state — each with a distinct # delegation profile. Used by the audit-mirror crate and governance domain commands. # # Usage: # let rad = import "schemas/lib/radicle.ncl" in # { repos | rad.WorkspaceRepos = rad.make_workspace_repos "libre-wuji" & { ... } } let _RepoRole = [| 'policy, 'desired, 'state |] in let _PatchStatus = [| 'open, 'merged, 'rejected |] in # M-of-N delegation profile attached to a Radicle repo. # threshold <= length(signers) is a business invariant enforced by the Rust caller. let _DelegationProfile = { threshold | Number | doc "Minimum signatures required to merge a patch (M in M-of-N)", signers | Array String | doc "Key IDs of authorized delegates (Radicle DID or human-readable alias)", } in # A Radicle repo descriptor: RID + role + delegation profile. # rid is empty string until the repo is initialised via 'rad init'. let _RadicleRepo = { name | String | doc "Human-readable name (e.g., 'policy-libre-wuji')", rid | String | doc "Radicle Identifier assigned by 'rad init' (rad:...); empty before init" | default = "", role | _RepoRole | doc "Functional role in the three-repo split", delegates | _DelegationProfile | doc "M-of-N delegation profile for patches to this repo", } in # A proposed change patch — governance domain commands surface these for operator review. let _Patch = { id | String | doc "Radicle patch ID", proposed_by | String | doc "Key ID or alias of the patch author", status | _PatchStatus | doc "Current lifecycle state" | default = 'open, signatures | Array String | doc "Key IDs that have signed this patch" | default = [], payload | String | doc "Short human-readable description of what this patch changes", } in # Snapshot of signature satisfaction for a pending patch. let _SignatureSet = { required | Number | doc "Threshold from the repo's DelegationProfile (M)", present | Array String | doc "Key IDs that have already signed", satisfied | Bool | doc "True when length(present) >= required", } in # The three repos belonging to one workspace — the canonical three-repo split. let _WorkspaceRepos = { policy | _RadicleRepo | doc "policy-: keeper auto-sign policy + authorized-signers set; M-of-N operator delegates", desired | _RadicleRepo | doc "-desired: version-controlled workspace declaration; M-of-N operators + CI keys", state | _RadicleRepo | doc "-state: immutable applied-ops ledger; exactly one delegate (ops-controller key)", } in { RepoRole = _RepoRole, PatchStatus = _PatchStatus, DelegationProfile = _DelegationProfile, RadicleRepo = _RadicleRepo, Patch = _Patch, SignatureSet = _SignatureSet, WorkspaceRepos = _WorkspaceRepos, # Returns a _WorkspaceRepos template with empty RIDs and placeholder signer lists. # `rid` and `delegates` fields carry `| default` priority so callers can override via merge: # (rad.make_workspace_repos "libre-wuji") & { # policy.rid = "rad:abc", # policy.delegates = { threshold = 2, signers = ["jpl-yubikey", "alice-key"] }, # state.rid = "rad:ghi", # state.delegates = { threshold = 1, signers = ["ops-controller-wuji-key"] }, # } # Alternatively, use `{ ... } | rad.WorkspaceRepos` directly with all fields populated. make_workspace_repos | not_exported = fun workspace => { policy = { name = "policy-%{workspace}", rid | default = "", role = 'policy, delegates | default = { threshold = 1, signers = [] }, }, desired = { name = "%{workspace}-desired", rid | default = "", role = 'desired, delegates | default = { threshold = 1, signers = [] }, }, state = { name = "%{workspace}-state", rid | default = "", role = 'state, delegates | default = { threshold = 1, signers = [] }, }, }, }