ADR-286: A Package Is a Directory and a Crate; use Names One and Brings No Name In; a Dependency Is Found by Path or by Version Through Cargo

Status: Accepted Date: October 3, 2026 History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached Answers: what a program of more than one file is and how its names resolve; what use does, and whether a name can be brought in (#229’s cross-file type defect); how one Nikaia package depends on another, and where the boundary of privacy is; how a library that uses a library is built; how a trait a package publishes becomes callable where it is implemented (#234); how a package is found by version (#99); whether a name may denote two things; what may stand in front of a :: (#141); what a path in nikaia.toml is relative to (#150) Related: ADR-002 D1 (one CLI wraps Cargo; the Rust arm reaches it) and D4 (std by path into a sysroot, the shape a package dependency generalises); ADR-296 D17 (the lowering is name for name: a qualified name reaches the emitter as written); ADR-020 (the ledger, keyed by qualified name); ADR-021 D2 and D13 (the lockfile records resolved versions; a cache hit, narrowed by D8 here); ADR-277 D2 (one form, not two spellings of it — the ground D11 is taken on); ADR-285 D8 (overflow checks on for the program, off for foreign packages); ADR-312 D6 (a library built at one user_parallelism stays usable at the other); ADR-100 (a consumer reads a dependency’s ledger and believes it while its hashes match); ADR-290 (a described Rust crate); ADR-140 D5 (a std module nothing describes yet is left alone); ADR-260 D1-D2 (a file inside a project is a file of that project; outside one it is standalone with no dependencies); Part I 9.1-9.3, Part III 13.3

1. Question

What is the unit a Nikaia program is divided into, how does a name written in one unit reach a declaration in another, and how does a unit arrive from somewhere else?

Without an answer a program is one file, a library cannot exist, privacy has no boundary to stand on, and every name the compiler cannot place is left to rustc, whose message is about a file nobody wrote (Part III C.1).

2. How others do it

Language / tool Its answer What it costs there See
Java, Go, Rust The boundary of privacy and publication is above the file (package, crate) — D1
Python The boundary is the file; __init__.py re-offers names by hand A front door maintained by hand, forgotten names and all D1
Rust, Python, Elm A glob import exists Rust lints it, Python’s style guide forbids it, Elm discourages it for libraries D11
Java, Python, Rust, Haskell, Elm, OCaml A selected import (use a::{B, C}) exists and is withdrawn nowhere Two spellings of every imported name; Haskell’s practice moved toward qualified names D11
Go No import selection; every name qualified Fifteen years without serious movement to add it D11
Go The fetch location is the package’s identity Years without a registry D15
Rust, JavaScript Registries built early Decisions made when there were three packages D15
Cargo An index, a semver grammar, a lockfile, git with a tag, and package = "…" renaming — D24
Rust A trait must be in scope before its methods are called The backend asks for an import Nikaia cannot write D28

What does not transfer: a separately maintained re-export layer (Python), and a registry of Nikaia’s own before there are libraries to put in it.

3. Options, weighed

Option What it is Why it lost See
File = module, plus a re-export form Keep each file a module, add a way to offer a sibling’s name A new form in a language that had just withdrawn two; upkeep on every library author forever; a library’s file layout stays its public surface D1
A glob import use pool::* Regretted everywhere it exists D11
A selected import use pool::{Conn} Two spellings of one name across the whole surface — exactly what ADR-277 D2 withdrew. Not irreversible: adding it later breaks no program D11
A rule for which of two same-named units wins — A rule a reader must know to read a line D14
nikaia.toml lists the modules As Cargo lists crates A second place to forget; the use already names the unit D1
A ledger per file — sync inference would depend on which file was read first D7
Privacy re-implemented in the compiler Instead of emitting pub Two enforcers of one rule disagree eventually; NK1110 is a message, not a second enforcer D2-D3
Every package flattened into one crate One crate for the program and its packages The transitive module is at the root, so rule 2 breaks silently and two packages can want one mod name D19
One crate, modules named by package identity Key becomes an alias The leak stays; D13’s check must widen to qualified names; pulls the registry question forward D20-D21
One crate, nested modules mod b { mod c } A package reached through two parents is two modules: breaks D15 rule 3 D20
An index of Nikaia’s own — A second place to publish; not ruled out, since the compiler reads a directory D25
A Rust path kept verbatim ../../../../../shim A number derived from a build layout that already changed once unnoticed D17
A path head refused on absence Refuse any unknown head A module of a package, an item of a foreign crate and an undescribed name look alike from inside the checker; it would refuse correct programs D34

What made D31 answerable: of the seven distinct paths the corpus uses as values, not one has a head the compiler has not read. D19’s shape was run as four hand-built crates before it was decided: the transitive crate does not resolve (E0433), the overflow check reaches a library crate, a crate not on the list wraps, and the diamond unifies.

4. Decision

What a package is

D1 — A package is a directory, and its files share one namespace.

The files in it see one another with no use at all; a name declared in any of them can be written in any other. What the package offers outward is whatever says pub, in whichever file it is declared. A consumer writes use http and names the package, never a file inside it; a use naming a file of the same package is refused. A library’s internal file layout is therefore not its public surface: moving a declaration between files breaks no consumer, and there is nothing to re-export. A package is a directory of a project — what a nikaia.toml declares; outside one a .nika file is compiled on its own (ADR-260 D2).

D2 — Privacy is per package, and pub is emitted as pub.

A declaration is private to its package unless it says pub, an item and a field alike, and pub on a field parses. The rule is put where the language below can act on it rather than re-implemented.

D3 — NK1110: privacy in Nikaia’s words, before rustc is asked.

Reaching a private item or field of another package — a call, a field read, a field given in a struct literal — is NK1110, naming the package that keeps it. Only a name written prefix::item can come from another package, so the checker needs only to know which prefixes are packages. A field’s pub is carried in the ledger, because a consumer needs it and nothing in the package does.

D4 — A name denotes one thing, and the second declaration is refused.

struct Foo beside fn Foo, two struct Foos, two fn fs: NK1148, the caret on the second declaration and the note naming the first. Two of a kind are the same rule and the same message. The rule holds whichever way the build was started, because it is asked by the checker every build runs.

D5 — What declares a name: fn, struct, enum, trait, grammar.

Those five and nothing else. A method belongs to its type, a grammar’s rule to its grammar (Json::value); a field, a variant, a type parameter and an impl declare nothing at this level. struct Foo’s constructor and fn Foo collide here, where a reader reads, even though the ledger keys them apart.

D6 — Across files the same rule keeps its own message.

Two files of one package declaring one name is refused by the module layer, naming both paths; it counts the same five kinds and leaves the one-file case to D4.

D7 — One ledger per package, inferred over all its files at once.

sync is a fixpoint over the call graph, and a graph that stopped at a file boundary would answer differently depending on which file was read first. The inference graph is the package; its ledger is written in its own root, modules in name order.

D8 — The cache key holds every file, and a hit means nothing is parsed twice.

The parse is what tells the compiler which files the program is, so ADR-021 D13’s on a hit nothing is parsed is narrowed to nothing is parsed twice; a hit still saves lowering, checks and inference. A single file’s key is unchanged.

D9 — The source map records which file.

Each unit is emitted with offsets of its own and the driver stamps them as it joins the text, so a rustc message about one file is traded back for a place in that file.

What use does

D10 — use makes a package reachable, and does nothing else.

Every name from it is written with its prefix, at every use: in a type, a call, a struct literal, nested in another type. The prefix falls where a name is written, not where a value is used — d.id carries none. use std::fs is the one use with a path in it, and names a module of the standard library.

D11 — No name is brought in: not by a glob, not by a list, not one at a time.

use pool::*, use pool::{Conn} and use http::server are each refused with a sentence of their own that names use pool and the qualified spelling. Taken on ADR-277 D2’s ground — one form, not two spellings of it — and firm because it is reversible: a selected form added later breaks no program.

D12 — use pool as p shortens the prefix, once, in one place.

It also settles a collision: two packages that both want to be http are the consumer’s to name apart. The alias is resolved away in the file that wrote it, and a diagnostic names the package, not the alias.

D13 — A prefix must be introduced.

pool::Conn without use pool is refused, so a file lists what it depends on at the top; use http where neither a dependency nor a directory of the project (D37) is called http is refused, naming where a package is declared.

D14 — One name per file.

Two units under one name in one file — by alias or by collision — is an error, not a rule about which wins. The alias counts.

How a package arrives

D15 — A package is depended on by path, and five rules come with it.

http = { path = "../http" } generalises how std already arrives (ADR-002 D4). A library needs no main.nika.

  1. The manifest key is the name use writes; the path says only where it comes from.
  2. Transitive dependencies are not visible. If A depends on B and B on C, A does not see C. Structural (D21).
  3. The same path is the same package; a different path is a different package, identical contents included.
  4. A dependency’s [build] is ignored, and the compiler says so once per build: a package is built with the settings of the program that uses it (ADR-312 D6). std is the one exception: its Nikaia half is lowered once and linked at either setting, which is sound only because nothing in it lowers differently per switch, and a test keeps it so (ADR-002 D4).
  5. A Nikaia dependency is part of the program, not a foreign package: the overflow checks of ADR-285 D8 reach it. Generated (D22).

D16 — A path in nikaia.toml is relative to nikaia.toml, for both arms of [dependencies].

../shim means the directory beside this project’s, for a Nikaia package and for a type = "rust" crate alike.

D17 — The generated manifest carries a resolved path absolute.

That is the one narrowing of ADR-002 D1’s verbatim passthrough: everything else in a Rust entry — version, git, tag, features, default-features, a package rename — reaches Cargo untouched. It is how the generated manifest already names nikaia-std.

D18 — What a path is relative to is a property of the file, not of the ecosystem.

One rule for both arms, because a difference between them would be about which generated file holds the value.

D19 — A package the project depends on is generated as its own Rust crate.

A build emits a Cargo workspace, always: the project as the binary crate and every package it depends on through [dependencies], transitively, as a library crate beside it, each with its own Cargo.toml under its own manifest keys. The packages inside the project are modules of its crate (D38). A dependency’s files are still read — for its ledger and the checks — and emitted by its own member. A type’s identity is the package’s directory, not the key a consumer reached it through, so a diamond unifies in the checker as in Cargo.

Changed by D38: a package inside the project is a module of the project’s crate, not a crate of its own.

D20 — The manifest key is the crate’s local name for the package, and nothing more.

A dependency is emitted in Cargo’s renaming form, c = { package = "…", path = "…" }, so two packages under one key are in two crates and never meet, and one package reached through two parents is one crate (D15 rule 3).

D21 — Rule 2 is structural.

A transitive package is not a dependency of the crate that would name it, so naming it fails to resolve below, reported against the .nika line. D13 does not have to widen to qualified names.

D22 — Rule 5 is generated.

Each generated Nikaia crate is named on the program’s side of the profile, [profile.dev.package.<crate>] overflow-checks = true - dev being the one profile a build has (ADR-002 D5) - beside the rows that turn the checks off for foreign packages.

D23 — A published Nikaia package is a crate named nikaia_<name>; the package’s own name is <name>.

The prefix is the index’s business: no .nika file, use, diagnostic or ledger entry ever carries it.

D24 — A version resolves through Cargo, and the generated manifest writes the rename.

http = "1.2" becomes http = { package = "nikaia_http", version = "1.2" }; "1.2" is Cargo’s semver; { git = …, tag = … } is the same arm without an index; the path arm stays. The resolved version is recorded in nikaia.lock (ADR-021 D2). A key is an identifier, because it is a use name.

D25 — Cargo finds and fetches; the compiler reads what it finds.

A published crate holds nikaia.toml, src/*.nika, nikaia.contracts and a stub Cargo.toml that exists for the resolver only. Nothing lowered is published; the consumer reads the sources and ledger from where Cargo put them and emits the package as a member (D19). The stub is never linked.

D26 — A crate under the prefix that is not a Nikaia package is refused by name.

One without .nika sources or without nikaia.contracts is not a Nikaia package; one with both is read like any package (ADR-100 D3).

D27 — A fetched package’s own dependencies resolve in the same resolution and stay invisible.

The stub lists them, so Cargo fetches the graph at once; a path dependency’s nikaia.toml is followed the same way. What the consumer may name is unchanged (D21).

D28 — The emitter writes a use for every trait an impl in this unit names with a package in front of it.

impl http::Handler for Fixed makes the emitter import http::Handler, so its methods are callable: Rust needs the trait in scope, and D11 gives the program no way to write the import. The path written is the import; nothing resolves it.

D29 — Qualified is the whole of the test.

A :: in the trait’s name is what says it is in another crate. A trait of this package, and one the compiler reads (impl Error for …, ADR-023 D3), carry none and are left alone.

D30 — The import carries allow(unused_imports).

A program may implement a trait and never call it; a warning about a line nobody wrote is not one to show.

What may stand in front of ::

D31 — A path’s head is one of five things.

A type declared in this package or recorded by either ledger; a package use introduces (D13); a package the manifest declares; a crate a description covers (ADR-290); a std module. A head that is none of them is refused, NK1181, in a call as in a value. A file of this package is never a head: its names are already in the package’s one namespace (D1). NK1171 is the other half: a member a known head does not have.

D32 — A ledger that records anything under head:: answers for the head.

A described crate is a list of its items, with no line for the crate’s own word, so the last question is a prefix. Without it nikaia describe’s output would be refused by the compiler that asked for it.

D33 — The way out is a use, never a new file.

A head is introduced by use (D13), so that is what the refusal offers: use {head} - with a [dependencies] line in nikaia.toml where {head} is a package not yet declared, and nikaia describe {head} where it is a Rust crate (Part III C.2). A .nika file beside this one is not offered: it joins this package’s namespace (D1) and makes no head. Where the use is already written and the path still fails, the head is known and the name after :: is what is missing - declared in that package’s source, or misspelt - which is NK1171’s question, not this one’s. Where a known head is one edit away (fx::read beside std::fs), the help names it first: did you mean fs? with its use line.

Amended October 6, 2026 (#488).

D34 — It fails open.

The list is of permissions: the refusal fires only where all five are silent. A missing refusal costs a rustc message; a wrong one costs a correct program (ADR-010 D1’s polarity, applied to a refusal).

D35 — A head the package’s other files declare is a head.

D1’s one namespace, read off the other files’ item trees rather than a ledger, so a private enum in a neighbouring file counts.

D36 — What is written down is what can be called; a member a known head or receiver does not have is refused.

Where the type of a receiver or the head of a path is known, the members it has are the ones written down for it: the program’s own declarations, std’s ledger, a crate’s description (ADR-290) and an extern block. A method, an associated function, a module’s function or a field that none of them holds is refused with NK1171, also where the backend has one by that name: String::from("x"), "x".as_str(), opt.is_some(), fs::frobnicate(), and a method a program’s own struct does not declare. The help names the language’s spelling where there is one ("x", .clone(), != null) and otherwise the nearest name.

std’s ledger is that list, and it is written by hand. An entry it lacks is a function a program cannot call, and the refusal is how the gap is found: the entry is added, with its signature, in the change that needs it. No entry is inferred from the backend’s surface, so a name Rust has and Nikaia does not stays refused.

This is not D34’s polarity. D34 is about a head nothing names; here the head or the type is known, the list for it is complete by definition, and a name that is not on it is not a correct program. An entry that is on the list with an unanswered signature (-> ?) is a known name of unknown type, and stays C.4’s silence until #497 closes it.

The same holds for a type’s name. The types a program writes are the language’s (Part I 2.2), not the backend’s: a written usize, isize, i8, i16, u16, i128, u128 or str names nothing and is NK1135, outside an extern "C" declaration, where usize is C’s size_t (Part III 15.1). The help names i64 for an integer, String for text.

Decided October 8, 2026 (#509, #470).

D37 — A directory under src/ that holds .nika files is a package of the project.

src/model/ is the package model, src/model/rows/ the package model::rows: the path from src/ is the name, and nothing declares it, so the tree on disk and the prefixes in the code cannot disagree. It is reached with use model or use model::rows, with no [dependencies] entry; the prefix is the last segment (rows::Row), and as shortens it as for any package. A dependency and a directory of the project under one name are refused. Whether a directory is a Nikaia package is read off what it holds, as D26 reads a crate: .nika sources make it one, with or without a Cargo.toml beside them.

D38 — The packages of a project are modules of one crate.

Each package of the project, the root included, is emitted as a mod of the project’s crate, nested as the directories are, each checked and lowered against its own entries as the build-time program already writes a dependency (pub mod <package>, ADR-321 D13). A module is a namespace of its own, so the separation the directories draw holds; one crate is one run of the backend for the whole project. A package the manifest marks crate (D40) is a crate of its own instead.

D39 — Packages of a project may use one another in a cycle.

http may use model while model uses http, as modules of one crate may in the backend. A cycle through a package marked crate is refused, because crates cannot depend on one another in a cycle. A cycle of build-time values is not a question of packages and stays NK1168’s.

D40 — [directories] in the manifest treats a directory otherwise.

"src/fixtures" = "ignore": not read, whatever it holds. "src/assets" = "resources": not code, published with the package and read by asset(…). "src/testkit" = "tests": its packages are built only under nikaia test. "src/ffi" = "crate": built as a crate of its own, which is the way for a directory whose emitted code cannot share a crate (an extern symbol) or that holds Rust beside its Nikaia, as nikaia-std does. A directory the table does not name is D37’s.

D37-D40 decided October 9, 2026 (#538).

D41 — An expression whose type is not known is refused.

An expression whose type the compiler cannot tell is NK1245. Exempt is what its uses type later (a number literal, an open number, an integer comptime without a type, ADR-285 D23, ADR-287 D21), what is unknown only because a part of it is (an operator, a branch, a block, a jump, a panic), a reflected field’s of in a generic body, and an expression inside which a part was already refused. Where the file holds another error, NK1245 is not said. A name is asked where it is bound and not where it is read; what a number nothing typed passes through (a generic call or member that hands it back, x ?? 0 over a T? nothing typed) is as open as the number; asset("…") is its comptime’s type; and a DSL driver’s Self::dsl is each call site’s own, as field.of is each unrolling’s. A call through a package whose ledger this build does not read is that package’s build’s to ask, and a dsl block or grammar entry the lowering refuses by name is left to that refusal. Part III C.4’s says nothing is withdrawn.

Why. What is not known is not checked, and a mistake there reached the language below as an error about a file nobody wrote (Part III C.1). #497 measured what failing closed costs and closed the gaps until none was left in std and ten in the programs; those are refused now and built forward. A value another refusal left without a type - a name never declared, a member a type does not have - is unknown wherever it is used, lines away; saying it again there names the same cause twice, so the other error is said and what is still unknown once it is fixed is said then. A name is bound in one place and read in many: the question belongs where the type would have come from, and asked at every read it is the same cause again. A number nothing typed is decided by its uses (ADR-285 D1), and what merely hands it on has no type of its own to be asked for. Rejected: keep failing open (D34), which leaves every gap silent; a warning in between, which the five backend errors found on 6 October would have scrolled past.

Decided October 10, 2026 (#497).

5. Consequences

6. What this does not decide

This site is open source. Improve this page.