Domain extension system (ADR-012): bash-layer dispatch activates repo_kind-conditional CLI domains. install.nu copies domains/ tree; short_alias wrappers generated (personal, prov). ore help and describe capabilities domain-aware. personal domain (PersonalOntology): career skills/talks/publications/positioning, CFP pipeline (Watching→Delivered), opportunities lifecycle, content pipeline, Sessionize integration. Daemon pages: /career, /personal. provisioning domain (DevWorkspace/Mixed): FSM state, next transitions, connections graph, gates, workspace card, capabilities, backlog. Daemon page: /provisioning. VCS abstraction layer (ADR-013): reflection/modules/vcs.nu — uniform jj/git API via filesystem detection (.jj/ vs .git/). opmode.nu and git-event.nu migrated off ^git. reflection/bin/jjw.nu — jj + ontoref + Radicle agent workspace lifecycle. jjw-ncl-merge.nu registered as jj merge tool for .ontology/ NCL conflicts. init-repo.nu for new_project mode. jj/rad not in ontoref requirements — belong in orchestration project manifests. 'Framework RepoKind: ontology/schemas/manifest.ncl gains 'Framework variant; ontoref self-identifies as framework — no domain activates for the protocol itself. Web presence: personal.html and provisioning.html domain subpages. index.html gains "Project Types — Domain Extensions" section with type cards and subpage links. Nav compacted (Arch/Prov labels, solid backdrop-filter background). on+re: vcs-abstraction (adrs: adr-013) and agent-workspace-orchestration Practice nodes; 21 manifest capabilities; state.ncl catalysts updated.
82 lines
7.2 KiB
XML
82 lines
7.2 KiB
XML
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,
|
|
},
|
|
}
|