A capability's policy lives in a module a second caller can load — the entry point may detect, never invoke

Accepted

ontoref
Every capability in this project reaches its four declared surfaces — CLI, daemon,

Context

Every capability in this project reaches its four declared surfaces — CLI, daemon, MCP, GraphQL — by ONE mechanism: its logic sits in a nu module a caller loads with `use` from a project root. The daemon never passes through the shell wrapper. It spawns `nu -c "use reflection/modules/<x>.nu *; …"` with current_dir(root), at five points: api.rs:1825, api.rs:1982, mcp/mod.rs:2343, 2487, 2559.

MEASURED 2026-08-25 (expediente 5/0). Domain command dispatch was the one capability implemented in the WRAPPER instead — alias resolution, the two activation paths (ADR-012 repo_kind ⊕ ADR-073 bond), the required_extensions gate, every diagnostic and the invocation, all inside `install/ontoref-global`, a bash file. It worked: `ontoref pm scope` answered, the alias resolved, the diagnostics were better than the tree's average. And `grep -rn "commands.nu" code/crates/ontoref-daemon/src` returned 0.

THAT ZERO WAS NOT A DEFERRED DECISION. It was a surface that could not be built with the existing pattern, because the module a caller would `use` did not exist. The same absence explained a second symptom that had looked unrelated: `code/ontoref pm scope` died on `nu::parser::extra_positional` while the installed binary answered the same command, because only ONE of the two wrappers carried the policy (`grep -c _dispatch_domain code/ontoref` → 0). One cause, two symptoms, and neither was legible from the other.

WHY NOTHING CAUGHT IT. The rule was universal in practice and written nowhere. Reading the wrapper shows correct code; reach is not a property of code that works, it is a property of who can load it, and no reader was ever asked that question. The only symptom available was a grep returning zero — and a zero reads as «not yet» rather than «cannot».

WHAT THE FIX ALSO REVEALED, and it belongs in the context because it bounds the claim below. Giving the source wrapper detection created a second dispatcher, and the biop cell written to guard the first would have watched one of two. Widening it exposed that the cell's delegation half was satisfiable by a HEADER COMMENT: both wrappers name `domain dispatch` in prose, so deleting the delegation and keeping the comment left the check green. A check anchored to a topic is not anchored to a call.

Decision

A capability's POLICY — which path activates it, what it requires, what its refusal says, what it invokes — lives in a module under `reflection/modules/` that any caller can load from a project root.

The shell entry point keeps DETECTION and only detection: the single question «is argv[1] a domain id or alias?», answered with file tests, then delegation. It must not invoke a domain's `commands.nu` itself.

THE SPLIT IS AT THAT EXACT LINE, and both halves are kept deliberately:

DETECTION stays in the wrapper because it is asked on EVERY ontoref invocation and must not cost a nu spawn. Two file reads, and it stays.

INVOCATION goes to the module because it has no such excuse, and because a second invocation path is a second place for the policy to drift. The obvious fix — duplicate the policy into both wrappers — is refused for that reason.

This is why the decision is `scope = 'Spiral` rather than a choice: it holds the speed pole and the reach pole inclusively, and which one manifests depends on which question is asked of the entry point, not on a winner declared here.

Constraints

  • Hard No shell entry point may invoke a domain's commands.nu itself. Detection and delegation only.
  • Hard `reflection/modules/domain.nu` declares `domain dispatch`, so a caller that is not the terminal has something to `use`.
  • Soft A capability's policy does not have its only home in a shell entry point.

Alternatives considered

  • Leave the policy in `install/ontoref-global` and duplicate it into `code/ontoref`rejected: The obvious fix, and it multiplies the defect instead of closing it. Two copies of an activation policy drift the moment one is edited, and the failure would be invisible from either side — which is precisely how the original asymmetry survived: one wrapper had it, the other did not, and neither was legible from the other. The biop cell forbids a second invocation path for this reason.
  • Teach the daemon to shell out to the wrapper for domain commandsrejected: It would make the daemon depend on a bash entry point it deliberately never passes through, and it would give MCP and GraphQL a different path again. The existing pattern — `nu -c "use …"` with current_dir(root) — already reaches all of them, and adding a second mechanism for one capability is the shape adr-081 refuses.
  • Move detection into the module too, so the wrapper is a pure passthroughrejected: Detection is asked on EVERY invocation. Loading a module to find out whether a module is needed puts a nu spawn on the hot path of every command in the CLI, paid by users who never touch a domain. The split exists because the two questions have different costs, not because bash deserves a share.
  • State the rule as guidance in CLAUDE.md and rely on reviewrejected: The rule was already universal in practice and written nowhere, and that is exactly how it was violated once without anyone noticing for months. ADR-074 names the shape: a carrier holds a copy and binds no one. Guidance that a grep cannot check is folklore, and folklore is what this corpus counts rather than trusts.

Anti-patterns

  • Policy implemented in the entry point — A capability's activation rules, gates, diagnostics and invocation are written inside the shell wrapper. The code is correct and the capability reaches exactly one surface, because the wrapper is the one file no other caller loads.
  • Duplicating the policy into every entry point — On finding that one wrapper carries a capability and another does not, the policy is copied into the second rather than moved out of the first. Both work, immediately and visibly, and they begin drifting on the next edit.
  • A check anchored to the topic instead of the call — The verifier greps a phrase that names the rule — here, `domain dispatch` — and the file's own header comment, which EXPLAINS the rule, satisfies it. Deleting the mechanism while keeping the prose leaves the check green.

Related ADRs

ADR-012 · ADR-073 · ADR-074 · ADR-072 · ADR-087

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.