let d = import "adr-defaults.ncl" in

d.make_adr {
  id     = "adr-013",
  title  = "VCS Abstraction Layer — Uniform jj/git API via vcs.nu",
  status = 'Accepted,
  date   = "2026-04-07",

  context = "ontoref modules that interact with version control (opmode.nu, git-event.nu, jjw.nu, init-repo.nu) historically hardcoded ^git subcommands. As jj adoption grows among contributors and orchestration projects (vapora), hardcoded git calls produce silent failures: jj repos have .jj/ but may lack a .git/ HEAD in expected locations, and jj semantics differ (the working copy is always a commit, @- is the parent, `jj file show` replaces `git show HEAD:path`). The dual-VCS problem had two layers: (1) detection — which VCS is active in a given project root, and (2) semantics — same logical operation (show last committed state, restore a file, get remote URL) expressed differently per VCS. Spreading detection logic across modules produces duplication and makes future VCS additions (e.g. Pijul, Sapling) a multi-file change.",

  decision = "Introduce reflection/modules/vcs.nu as the single VCS abstraction layer. It exports: detect (returns 'jj' | 'git' | 'none' via filesystem check — .jj/ presence), is-repo, show-committed (jj: `jj file show -r @-`; git: `git show HEAD:path`), restore-file (jj: `jj restore --from @-`; git: `git checkout --`), remote-url (jj: `jj git remote list`; git: `git remote get-url origin`), current-branch (jj: `jj log -r @ --no-graph -T bookmarks`; git: `git branch --show-current`), uncommitted-files (jj: `jj diff --summary -r @`; git: `git status --porcelain`), commit-count (jj: `jj log --no-graph -T '' | lines | length`; git: `git rev-list --count HEAD`). All ontoref modules must import vcs.nu and call these exports — direct ^git or ^jj subprocess calls inside modules are prohibited. jj and rad are not listed as requirements in ontoref's manifest: they are opt-in tools whose requirements belong in orchestration projects that depend on them.",

  rationale = [
    {
      claim  = "Filesystem detection is the correct boundary — no config, no env var",
      detail = "Requiring contributors to set ONTOREF_VCS=jj or maintain a config entry creates state that can desync from reality. .jj/ presence is authoritative: if the directory exists, jj is the VCS regardless of any other indicator. This also makes detection work correctly in multi-VCS scenarios (jj colocated repos have both .jj/ and .git/ — detection correctly prefers jj).",
    },
    {
      claim  = "Single module boundary isolates VCS semantic differences from all callers",
      detail = "jj's working-copy-as-commit model means @- (parent) is the last deliberately committed state, not HEAD. show-committed encodes this semantic difference once in vcs.nu. Every caller that needs 'the last committed content' calls show-committed and gets the right answer for both VCS backends without knowing which one is active.",
    },
    {
      claim  = "opt-in jj/rad — requirements live in the orchestration layer, not in ontoref",
      detail = "ontoref ships jjw.nu and vcs.nu as tooling; whether a project uses jj or Radicle is decided by the orchestration project (e.g. vapora). Listing jj/rad as ontoref requirements would force every ontoref consumer to acknowledge tools they will never use. Requirements for opt-in tools belong in the manifest of the project that orchestrates them.",
    },
  ],

  consequences = {
    positive = [
      "All VCS interaction is centralized — adding Pijul or Sapling support requires changes only to vcs.nu",
      "opmode.nu and git-event.nu work correctly in both git and jj repos without caller changes",
      "jjw.nu agent workspace lifecycle works transparently over jj without any git fallback complexity in the orchestration layer",
      "Detection is sub-millisecond — no nickel export, no network, no subprocess for the detection itself",
    ],
    negative = [
      "Modules that previously called ^git directly must be updated to import vcs.nu — migration cost for existing modules",
      "vcs.nu adds a Nu module import to every module that touches VCS — minor parse-time overhead",
    ],
  },

  alternatives_considered = [
    {
      option       = "Env var ONTOREF_VCS to select backend",
      why_rejected = "Creates mutable state that can desync from the actual repo state. A repo cloned fresh has no env var set; a contributor switching between git and jj repos would need to update the env var manually. Filesystem detection is always correct without configuration.",
    },
    {
      option       = "Per-module inline detection (duplicate detect logic in each file)",
      why_rejected = "Already the de-facto state before vcs.nu. Duplicated detection means any change to jj semantics (e.g. a jj CLI flag change) requires hunting every module. The abstraction cost is one import line per module.",
    },
    {
      option       = "Wrap the entire CLI in a shim that translates git commands to jj",
      why_rejected = "Shim-layer translation is fragile — git and jj command surfaces are not isomorphic (jj has no git stash equivalent; jj describe vs git commit -m). The operations ontoref needs are a small, well-defined set; a typed Nu module is a cleaner contract than a command-translation shim.",
    },
  ],

  constraints = [
    {
      id        = "vcs-module-single-source",
      claim     = "All VCS subprocess calls in reflection/modules/ and reflection/bin/ must go through vcs.nu exports — no direct ^git or ^jj calls outside vcs.nu itself",
      scope     = "reflection/modules/, reflection/bin/",
      severity  = 'Hard,
      check     = { tag = 'NuCmd, cmd = "glob $\"($env.ONTOREF_ROOT)/reflection/{modules,bin}/*.nu\" | where { |f| ($f | path basename) != 'vcs.nu' } | each { |f| let hits = open --raw $f | lines | where { |l| ($l | str trim | str starts-with '^git ') or ($l | str trim | str starts-with '^jj ') }; if ($hits | is-not-empty) { error make { msg: $\"($f): direct VCS call found\" } } }; true", expect_exit = 0 },
      rationale = "The abstraction only holds if it is the single call site. Any direct ^git call in a module bypasses the detection logic and breaks jj repos silently.",
    },
    {
      id        = "jj-rad-not-in-ontoref-requirements",
      claim     = "jj and rad must not appear as required = true entries in .ontology/manifest.ncl requirements[]",
      scope     = ".ontology/manifest.ncl",
      severity  = 'Hard,
      check     = { tag = 'NuCmd, cmd = "let reqs = (do { ^nickel export $\"($env.ONTOREF_PROJECT_ROOT)/.ontology/manifest.ncl\" } | complete | if $in.exit_code == 0 { $in.stdout | from json | get requirements? | default [] } else { [] }); let forced = $reqs | where { |r| ($r.id == 'jj' or $r.id == 'rad') and $r.required == true }; if ($forced | is-not-empty) { error make { msg: $\"jj/rad must not be required=true in ontoref manifest\" } }; true", expect_exit = 0 },
      rationale = "jj and rad are opt-in tools. Marking them required=true in ontoref's manifest propagates a false requirement to every consumer that reads describe requirements.",
    },
  ],

  related_adrs = ["adr-012"],

  ontology_check = {
    decision_string    = "vcs abstraction jj git uniform module",
    invariants_at_risk = [],
    verdict            = 'Safe,
  },
}
