ontoref/adrs/adr-012-domain-extension-system.ncl
Jesús Pérez 472952e29b
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
feat: domain extension system, VCS abstraction, personal/provisioning domains, web subpages
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.
2026-04-07 23:08:29 +01:00

88 lines
8 KiB
XML

let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-012",
title = "Domain Extension System — Bash-Layer Dispatch for repo_kind-Conditional CLI Domains",
status = 'Accepted,
date = "2026-04-05",
context = "Consumer projects have project-type-specific CLI needs that ontoref's generic command set cannot satisfy. A PersonalOntology project has CFP pipelines, career schemas, and opportunity tracking that no other repo_kind needs. A DevWorkspace project has workspace cards, cluster state dimensions, and production-readiness gates. These capabilities lived as local scripts (scripts/jpl.nu) — invisible to ontoref's help system, absent from describe capabilities, and not portable to other PersonalOntology projects. The problem had two layers: (1) how to conditionally load domain-specific Nu commands without breaking the existing dispatcher, and (2) how to make domain commands discoverable (help, describe capabilities) and aliasable (ore prov, personal state). Nu's module system (`use`, `source`, `overlay use`) is compile-time: paths must be known at parse time and `overlay use` creates an isolated namespace that cannot extend `def main` in the existing dispatcher. Runtime loading of arbitrary Nu modules is architecturally impossible in the current Nu model.",
decision = "Implement a bash-layer domain dispatch system. The ontoref bash wrapper (install/ontoref-global) resolves the first CLI argument against $ONTOREF_ROOT/domains/{arg}/repo_kinds.txt before delegating to the Nu dispatcher. If the argument matches a domain directory name (or a registered alias from domains/aliases.txt) AND the current project's repo_kind appears in that domain's repo_kinds.txt, dispatch directly to nu domains/{id}/commands.nu passing remaining args. Each domain ships three files: domain.ncl (NCL contract declaring commands, pages, repo_kinds, short_alias), commands.nu (Nu script with def main [...args] entry point), and repo_kinds.txt (plain-text list of matching repo_kind values, grep-readable by the bash wrapper without running nickel). install.nu copies the entire domains/ tree to $data_dir/domains/ and generates domains/aliases.txt mapping short_alias → domain_id. Short aliases also create standalone bin wrappers at $bin_dir/alias.",
rationale = [
{
claim = "Bash layer is the correct dispatch boundary for runtime-conditional Nu module selection",
detail = "Nu resolves `use` and `source` at parse time — runtime loading of arbitrary module paths is impossible without spawning a new Nu process. The bash wrapper already mediates between the caller and the Nu dispatcher; extending it with a domain lookup before calling Nu is a natural seam. The domain's commands.nu is a self-contained Nu script with def main as its entry point — no module loading involved, just a new process.",
},
{
claim = "repo_kinds.txt is grep-readable without nickel — critical for dispatch performance",
detail = "The bash wrapper calls domain dispatch on every invocation. Using nickel export to read repo_kind from domain.ncl would add 200-400ms per invocation. repo_kinds.txt is a plain-text file (one repo_kind per line) that grep can check in under 1ms. Similarly, repo_kind is extracted from the project manifest via grep on the NCL source (repo_kind = 'Tag) rather than nickel export — avoiding import-path resolution failures for manifests that import schema files.",
},
{
claim = "NCL domain.ncl contract provides discoverability without hardcoding",
detail = "help.nu and describe.nu read domain.ncl at runtime to render domain commands in ore help and ore describe capabilities. The Nu dispatcher never needs to know about domain commands at parse time — it only sees them if ore help <domain-id> is invoked, at which point it shells out to nickel export domain.ncl. This keeps the Nu dispatcher static while allowing dynamic domain registration.",
},
{
claim = "short_alias enables both ore-level and standalone invocation with one declaration",
detail = "domain.ncl declares short_alias (e.g. 'prov', 'personal'). install.nu generates two artifacts: domains/aliases.txt (consumed by the bash wrapper for ore prov → ore provisioning resolution) and $bin_dir/alias (standalone wrapper for prov state). Both derived from the same field — single source of truth per domain.",
},
],
consequences = {
positive = [
"PersonalOntology and DevWorkspace/Mixed projects have discoverable, aliasable CLI commands without local scripts",
"ore help personal / ore help provisioning renders from domain.ncl — no hardcoded help text",
"ore describe capabilities shows DOMAIN EXTENSION section automatically for any matching project",
"New domains require only three files: domain.ncl, commands.nu, repo_kinds.txt — no changes to the Nu dispatcher",
"Short aliases (personal, prov) work both as ore prov and standalone prov with the same domain-membership enforcement",
],
negative = [
"Domain commands always spawn a new Nu process (cannot share session state with the main dispatcher)",
"commands.nu cannot import from the main reflection/ Nu library without explicit path setup",
"Domain dispatch adds one grep + one stat call per invocation (sub-millisecond, but measurable)",
],
},
alternatives_considered = [
{
option = "Nu overlay use for runtime domain loading",
why_rejected = "overlay use creates an isolated namespace — commands defined in an overlayed module cannot be called by name in the parent scope. It is also parse-time in module context. Confirmed broken: nu -c 'use FILE *; command args' causes infinite recursion when called from def main.",
},
{
option = "Add domain commands directly to reflection/bin/ontoref.nu",
why_rejected = "Would require hardcoding every domain's commands in the main dispatcher, or using dynamic path strings in `use` which Nu forbids. Also violates the no-enforcement axiom — the main dispatcher should not know about PersonalOntology specifics.",
},
{
option = "Project-local scripts/ (scripts/jpl.nu approach)",
why_rejected = "Invisible to ore help and ore describe capabilities. Not portable across PersonalOntology projects. Namespace requires prefix (jpl cfp vs cfp). Dispatch requires knowing the script path.",
},
],
constraints = [
{
id = "domain-files-required",
claim = "Every domain directory under $ONTOREF_ROOT/domains/{id}/ must contain domain.ncl, commands.nu, and repo_kinds.txt",
scope = "domains/",
severity = "Hard",
check = { tag = 'NuCmd, cmd = "ls $env.ONTOREF_ROOT/domains/ | where type == 'dir' | get name | each { |d| let missing = ['domain.ncl' 'commands.nu' 'repo_kinds.txt'] | where { |f| not ($\"($d)/($f)\" | path exists) }; if ($missing | is-not-empty) { error make { msg: $\"domain ($d | path basename) missing: ($missing | str join ', ')\" } } }; true", expect_exit = 0 },
rationale = "The bash wrapper assumes all three files exist once a domain directory is found. Missing files produce confusing errors at dispatch time.",
},
{
id = "commands-nu-def-main",
claim = "commands.nu must declare def main [...args: string] as its entry point — no dynamic use/source calls inside Nu scripts",
scope = "domains/*/commands.nu",
severity = "Hard",
check = { tag = 'NuCmd, cmd = "glob $\"($env.ONTOREF_ROOT)/domains/*/commands.nu\" | each { |f| if not (open --raw $f | str contains 'def main') { error make { msg: $\"($f): missing def main\" } } }; true", expect_exit = 0 },
rationale = "The bash wrapper calls nu commands.nu <args>. Nu invokes def main with the remaining args. Without def main, all args are ignored. Dynamic use/source cause infinite recursion.",
},
],
related_adrs = ["adr-001", "adr-006"],
ontology_check = {
decision_string = "domain extension bash dispatch repo_kind conditional Nu",
invariants_at_risk = [],
verdict = "Safe",
},
}