# schemas/lib/op.ncl — Op (Operation) governance contracts # # An Op is the atomic unit of workspace state management — it records intent, # authorization, execution artifacts, and state transitions as a DAG node, # enabling audit, rollback, and concurrent agent control. # # Storage (per op): # ops/{id}/op.json — runtime instance record (JSON, not NCL) # ops/{id}/pre.json — pre-execution state snapshot # ops/{id}/post.json — post-execution state snapshot (absent on failure) # .ops-archive/ — restic repo (S3 backend): logs + bundles, encrypted # # Identity: # actor.nid = Radicle Node ID (rad self --nid). Falls back to "local:{user}". # Op ID = {nid-short}:{uuid} — globally attributable, DAG-safe # # DAG semantics: # Each Op is a node. lineage.parent_op is the incoming edge. # Rollback is a new forward Op — lineage.rollback_of points to the Op being undone. # The DAG remains acyclic; rollback is a forward move restoring an earlier snapshot. let _OpActor = { nid | String | doc "Radicle Node ID (rad self --nid) or 'local:{user}' fallback", identity | String | doc "Human-readable label — username or agent name", source | [| 'cli, 'agent, 'api |] | default = 'cli, } in let _Constraint = { kind | [| 'backup, 'restore, 'health_check, 'dry_run_check, 'concurrent_lock |], resource | String | doc "Component name or volume/resource identifier", scope | [| 'direct, 'indirect |] | default = 'direct, params | { .. } | default = {}, } in let _RecoveryAction = { kind | [| 'backup, 'restore, 'health_check, 'dry_run_check, 'concurrent_lock |], resource | String, from | [| 'pre_backup, 'last_known_good |] | default = 'pre_backup, params | { .. } | default = {}, } in let _OpConstraints = { pre | Array _Constraint | doc "Gates evaluated before execution starts" | default = [], on_failure | Array _RecoveryAction | doc "Recovery actions if op fails" | default = [], } in let _OpSnapshots = { pre | String | doc "Relative path to pre.json from workspace root", post | String | optional | doc "Relative path to post.json — absent if op failed before completion", } in let _OpArtifacts = { archive_snapshot | String | optional | doc "Snapshot ID in the configured archive backend (restic/kopia)", bundles | Array String | doc "Bundle tar.gz paths within the archive snapshot" | default = [], } in let _OpLineage = { parent_op | String | optional | doc "Op ID this state was derived from (incoming DAG edge)", rollback_of | String | optional | doc "If this op is a rollback, the ID of the op it undoes", } in { OpActor = _OpActor, Constraint = _Constraint, RecoveryAction = _RecoveryAction, OpConstraints = _OpConstraints, OpSnapshots = _OpSnapshots, OpArtifacts = _OpArtifacts, OpLineage = _OpLineage, # Enum type exports — use these as field annotation values in workspace NCL OpSource = [| 'cli, 'agent, 'api |], OpOperation = [| 'install, 'update, 'delete, 'rollback, 'dry_run |], OpStatus = [| 'pending, 'running, 'constraint_failed, 'recovering, 'success, 'failed, 'rolled_back, 'cancelled |], Op = { id | String | doc "Op ID: {nid-short}:{uuid}", actor | _OpActor, intent | String | doc "Human description of why this op is needed", workspace | String | doc "Workspace name from config/provisioning.ncl", component | String | doc "Component being operated on", operation | [| 'install, 'update, 'delete, 'rollback, 'dry_run |], targets | Array String | doc "Server hostnames targeted by this op", constraints | _OpConstraints | default = { pre = [], on_failure = [] }, snapshots | _OpSnapshots, artifacts | _OpArtifacts | default = { bundles = [] }, lineage | _OpLineage | default = {}, status | [| 'pending, 'running, 'constraint_failed, 'recovering, 'success, 'failed, 'rolled_back, 'cancelled |] | default = 'pending, jj_change | String | optional | doc "jj change ID created for this op", radicle_rid | String | optional | doc "Radicle Repository ID after rad sync — globally unique RID of this workspace", started_at | String | doc "ISO 8601 UTC timestamp", ended_at | String | optional, }, # Workspace-level ops configuration — added under `ops` in config/provisioning.ncl OpsConfig = { archive = { backend | [| 's3, 'local |] | default = 's3, tool | [| 'restic, 'kopia |] | doc "Backup provider — must have a matching entry in extensions/providers/backup/" | default = 'restic, endpoint | String | optional | doc "S3-compatible endpoint URL", bucket | String | optional, prefix | String | default = "ops", }, retention = { keep_last | Number | default = 50, keep_monthly | Number | default = 12, keep_yearly | Number | default = 3, }, }, }