A project's root is supplied by the machine's registry, never written in its project.ncl — no versioned file of a project carries where that project lives
Proposed
Context
Every project registered with ontoref carries `.ontoref/project.ncl`, versioned in its own repository, and every one writes in it where it lives on the machine that onboarded it: `root = "/Users/Akasha/Development/<project>"` and `nickel_import_paths` with the same absolute prefix (ontoref's own .ontoref/project.ncl:5-9; DD-eca, provisioning, the opus domain the same). The schema asks for it — `root | String`, «Absolute local path to the project root» (code/install/resources/schemas/ontoref-project.ncl:53-54) — and both writers produce it: `setup` replaces the template's placeholder with `($cwd)` (.ontoref/reflection/bin/ontoref.nu:2438-2452) and `project-add` with `(path expand)` (ontoref.nu:2924-2947).
The location is therefore written twice. The machine's registry, ~/.config/ontoref/projects.ncl, generated by code/install/gen-projects.nu and «AUTO-GENERATED … Do not edit by hand» (gen-projects.nu:74-76), already is a list of `[import "<root>/.ontoref/project.ncl"]`, and three readers derive the root from that line and never read the field (registry.nu:51-63, biop.nu:570-583, gen-projects.nu:83-98). Every other reader takes `root` from the exported record: `ontoref project root` (ontoref.nu:3016-3044) and through it every domain's `schema_cmd`, the wrapper's _resolve_domain_paths (ontoref-global:437-458), domain.nu load-registry and everything built on it (provided-index, domain consumers, domain show), validate.nu's level chain (adr-045 mechanism 2), shared.nu, bond.nu, checks.nu, positioning.nu, and the daemon, which receives the registry through the config pipe (adr-004) and canonicalizes RegistryEntry.root (crates/ontoref-daemon/src/registry.rs:194-211, 375-386). One reader scrapes the literal `root "…"` line out of project.ncl (describe.nu:4319-4337).
Measured on 2026-10-06, the second copy is the one that drifts. Moving one checkout (/Users/Akasha/Development/librosys to …/opus to …/opus-ontoref, opus adr-006) needed the registry line and the project.ncl root changed together, in two repositories, with a commit in the project's history recording nothing but where one machine keeps it. A clone of any registered project on another machine, or under another directory, starts with a project.ncl that names a path that does not exist there. adr-045 already ruled this out for manifests — a parent is resolved by name through the registry so that «no absolute path of one machine enters a versioned manifest» — and bl-048 names the class (a machine path committed in a versioned file). project.ncl was left out of that rule.
Nickel has no primitive for the location of the file being evaluated, so a project.ncl cannot compute its own root; whoever imports it has to supply it. Measured in a scratch tree on 2026-10-06: with `root | String | optional` in the schema, a registry entry written `[(import "<root>/.ontoref/project.ncl") & { root = "<root>" }]` exports the record with that root, and project.ncl evaluated alone still exports (slug, keys, sops) for the readers that open it directly (run.nu:81, modes.nu:208, secrets.nu, vault.nu). Without the parentheses `import "…" & {…}` does not parse; with the current schema a project.ncl without root fails with «missing definition for `root`».
The project.ncl list of nickel_import_paths is read only by the daemon, which already joins a relative entry with the canonical root (registry.rs:590-625, main.rs:1766-1773) and already appends the layout schema dirs and ~/.config/ontoref/schemas itself (registry.rs:547-588). No Nushell reader reads that list (backlog.ncl:285-291 records the gap).
Decision
THE REGISTRY SUPPLIES THE ROOT. A project's root is where its .ontoref/project.ncl is, and the one place that says where that is on a machine is that machine's registry. gen-projects.nu writes each local entry as `[(import "<root>/.ontoref/project.ncl") & { root = "<root>" }]`: the location written once, in the one file that is per-machine by nature and never versioned. Every reader of the exported registry — `ontoref project root`, the domain modules, the level chain, the daemon's config pipe — keeps reading `root` from the record and changes nothing.
PROJECT.NCL DECLARES NO LOCATION. The schema's `root` becomes `| String | optional`; project.ncl does not write it, and its nickel_import_paths are relative to the root (`"."`, `".ontoref/ontology"`). An entry outside the project is not the project's to declare. The daemon already appends ~/.config/ontoref/schemas and the project's own layout schema dirs (registry.rs:540-625); it does NOT append the installed tool's ontology dir, which every project.ncl writes by hand today («…/Library/Application Support/ontoref/ontology»). The daemon appends it from the tool root it resolves (adr-116), so no project names it. The template (code/templates/project.ncl), `setup` and `project-add` stop writing a root and write relative import paths.
AN EMPTY ROOT STILL MEANS REMOTE. `make_remote_project` keeps `root = ""` for push_only entries (ontoref-project.ncl:69, registry.rs:355-372). A local entry never has an empty root, because the registry supplies it; absence in project.ncl is not emptiness in the registry.
THE READERS THAT PARSE THE REGISTRY'S TEXT ACCEPT THE NEW ENTRY. registry.nu's registry-paths, which requires a line starting `[import` and ending `"]`, accepts `[(import "…") & { root = "…" }]`; biop.nu's and gen-projects.nu's patterns already match the quoted import path. describe.nu stops scraping a `root` line from project.ncl and takes the root the way registry.nu does.
EXISTING PROJECTS MIGRATE ONCE. A protocol migration removes `root` from a consumer's project.ncl, rewrites its import paths relative to the root, and regenerates the registry in the new shape; the registry is regenerated, never hand-edited, as its header already says.
Constraints
- Hard Neither ontoref's own project.ncl nor the project template writes a root
- Hard The project schema declares root optional
- Hard gen-projects writes each local entry with the root merged at the import site
Alternatives considered
- An environment variable for the development tree (ONTOREF_DEV_ROOT) expanded in root — rejected: Nickel cannot read the environment, so every reader would have to expand it, and the project.ncl would still carry a path — relative to a variable instead of to itself. The registry is already the per-machine answer and needs no new variable.
- A relative root in project.ncl (root = ".") — rejected: Every exported-record reader resolves a relative path against its own working directory — the daemon's canonicalize against the daemon's cwd — so `.` means a different directory to each reader. Measured in the inventory: relative roots misresolve in project root's callers, domain.nu, validate.nu and the daemon.
- An empty root meaning «derive it» — rejected: Empty already means push_only/remote in the daemon and «skip» in every CLI reader; giving it a second meaning would make a local project indistinguishable from a remote one.
- Have every reader derive the root from the import line, as registry.nu does — rejected: About twenty readers, in Nushell and Rust, would change, and the daemon receives the registry as exported JSON through the config pipe (adr-004), where the import line no longer exists. Supplying the field at the import site reaches all of them at once.
- Leave project.ncl as it is and fix moves by hand — rejected: It is the state measured on 2026-10-06: every move is a commit in the moved project, and every clone elsewhere starts wrong.
Related ADRs
· reads
Perspectives