89 lines
8 KiB
Text
89 lines
8 KiB
Text
|
|
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",
|
||
|
|
},
|
||
|
|
}
|