nikaia describe Writes the DraftStatus: Superseded by ADR-290 — its D1-D5 are D1-D5 there Date: September 15, 2026 Target Version: Nikaia 0.0.8 Related: ADR-020 D5 (entries a compiler cannot infer are written and reviewed like code — this is that rule for every crate), ADR-028 D5 (an entry exists because a program asked for it), ADR-100 D3 (a ledger believed while its hash holds), ADR-038 D7 and ADR-045 D3 (what a foreign call may be handed), ADR-001 D1 (stable toolchain only), Part III 15.2 (the type mapping the draft applies)
A call into a Rust crate is a call no ledger describes: the compiler reads no
Rust, so it says nothing about the callee, hands the program to rustc, and
whatever rustc refuses arrives in Rust’s words about a file nobody wrote.
That is the one place Part III C.1’s promise — an
untranslated backend error is a compiler bug — could not be kept, because
nothing was known to translate against. It is also where every analysis
loses its footing at once: sync, throws, keeps, touches, the
crossing verdict, all fail closed at the boundary, which is safe and blind.
std’s Rust half already has the answer: its entries are written by hand
and reviewed like code (ADR-020 D5). What a foreign crate
lacks is somebody to write the first draft.
There is no silent third answer for a foreign call. A program that calls
regex::Regex::new with no entry for it is refused with regex is not
described; run nikaia describe regex. What every analysis then reads at
the boundary is an entry, never an absence.
nikaia describe <crate> writes a draft ledger for the functions the program uses.The command finds the crate’s calls in the program’s sources, reads the
crate’s pub signatures, and writes an entry for each called function and
for the types those signatures name — not the whole surface, because an
entry exists because a program asked for it (ADR-028 D5), and
a review is proportional to use rather than to the crate. A later call to
an undescribed function meets D1 and the same command.
| Rust | the entry |
|---|---|
&T |
a view; T by value is kept |
&mut T |
changed in place (mut) |
Result<T, E> |
throws; the error type where 15.2 can name it |
async fn |
may pause; a plain fn is sync |
Option<T> |
T? |
Send on a type |
may cross a thread |
touches, locks |
everything, may — the signature cannot say |
Both produce the same draft; the file’s header records which. rustdoc-JSON
is the better source and is not stable yet; a stable-only toolchain
(ADR-001 D1) is not given up for it, so the source parser is
what runs today and the JSON reader is what runs the day it is stable.
Whatever neither can read — a macro-generated function, a signature over a
trait object — is written as ? and left to D5.
? in it is a person’s to fill.contracts/<crate>.contracts, beside the program’s own ledger, with the
crate’s name, version and source hash in the header. It is believed while
the hash holds and demanded again after a version change, with the diff
shown (ADR-100 D3’s rule). A hand edit is expected: a Rust
signature that lies — unsafe impl Send on a type that is not — is caught
by the reviewer or by nobody, and the file is where the correction lives.
Every analysis reaches to the boundary and reads an entry there. The
crossing verdict, keeps, sync and throws stop failing closed on
foreign calls that a signature answers plainly, and C.1 becomes keepable at
the one place it was not.
What it costs: a review per crate, proportional to what the program
calls; a file per crate under version control; and the four programs in
examples/foreign-runtime/, which call hyper_shim undescribed today and
become either described or the reproduction of D1’s refusal.
What it does not change: what a foreign call may be handed. A lock and
a Shared still do not cross into foreign code (ADR-045 D3,
ADR-061 D1); the entry says what the callee takes, not that the
rule bends.
Step 1, D1’s refusal. A call — or a written type — reaching into a Rust
crate the manifest declares and no ledger describes is NK2504, and the message
is the command. crates/nikaia/src/foreign.rs is the walk; the two sets it
takes come from the build rather than from a unit, which is why it is a pass of
its own beside the type check and not a rule inside it.
Three things about its shape are the decision rather than the implementation.
Only where the manifest declared it with type = "rust": a qualified name
this compiler cannot account for — another package, a std path, a typo — keeps
whatever message it already had, because refusing on a name nobody declared is
Part III C.4’s correct program refused. Once per crate, because the reader’s
next move is one command for the whole crate, with the caret on the first call.
And hyper-shim in the manifest is hyper_shim in a program, which is the
crate name Cargo makes of the key — the translation is the manifest’s, so every
reader sees the name a call writes.
Step 5 by hand, which is what D5 expects.
examples/foreign-runtime/{crossing,serve,smuggled}/contracts/hyper_shim.contracts
is the file nikaia describe will write, produced the way a reviewer would
check it: from the crate’s pub signatures, translated by D3’s table, with no
touches and no locks on any entry — absent touches reads as touches
everything (ADR-033) and absent locks is that column’s third
answer, and neither is something a Rust signature can tell anybody.
And the line that was the honest gap is closed. LocalHandle holds an
Rc<String> and is not Send — which follows from its fields, and a reader
of signatures does not have them. That is why the description used to carry no
crosses at all, and why D3’s table cannot answer it: Send on a type means
it may cross, and the absence of the word is not the claim that it may not.
ADR-123 D2 put the answer where it comes from, and the describer
reads it: a pub struct’s fields decide the column, false for an Rc or a
raw pointer, true where every field is plainly sendable, and nothing where it
cannot tell. LocalHandle’s crosses = false is the command’s now.
And D1’s first sentence is built, which is what the file the refusal asks
for was for: a description’s entries are read by the analyses. The signature
types the call and what it hands back, throws makes it a place that can fail,
and sync makes it one a sync function may not make. A described crate is a
package by the spelling rule and not one of std’s modules, so nothing
about it is imported — without that the merge turned hyper_shim::serve_once
into a demand for use std::hyper_shim.
This was the last thing anybody would have looked for. The file was read, it
was parsed to see whether it parsed, and the parsed ledger was dropped: the only
effect a reviewed description had was silencing the refusal that had asked for
it, while that refusal’s own message promised the reader four answers in return.
crosses is the fourth and is not answered here, because a described call
is not asked the crossing question at all (D7’s wording) — docs/open-decisions.md
carries that one.
Step 2 is built, for a path dependency. nikaia describe <crate> finds
the crate’s declaration in the manifest, reads every .rs under its src/,
takes the pub fn and pub struct items out of the text, translates them by
D3’s table, and writes contracts/<crate>.contracts under a header that says
what the file is and that it is to be reviewed — which is the one line
Ledger::render’s own header could not say, since that one says do not edit
by hand.
It is a signature scraper and not a Rust parser, and the difference is
the scope rather than a gap. An item a macro generates is not in the text and
is not found; a pub item inside a mod block is read as the crate’s own,
because the module path a caller writes is a thing only a real parser knows;
and a type it cannot account for is written ?. Each of those is D5’s case,
and the command names the ones it could not answer rather than leaving a
draft that looks complete.
A path dependency only. A version dependency’s sources are in Cargo’s
registry cache, under a layout ADR-002 D1 hands to Cargo and
never resolves itself — a describer that guessed at that cache’s shape would be
resolving a version. The command says so and names D5’s hand-written file as
what to do instead.
And the measurement is the experiment’s own file. §5 above called
examples/foreign-runtime/*/contracts/hyper_shim.contracts the file nikaia
describe will write, produced the way a reviewer would check it.
crates/nikaia/tests/describing.rs runs the command against the crate in the
tree and asserts the draft against that file, entry for entry and hash for
hash. Two things in the reviewed file are not in the draft and both are D5:
crosses = false on LocalHandle, which is read from a field, and the
comments, which are a reviewer’s.
Step 3 is built: the hash rule holds. A description is believed while the
files it was derived from hash as recorded, and NK2505 says so where one does
not — once per crate, with the caret on the first call into it and the command
in the message.
The one difference from a ledger’s rule is what happens next.
ADR-100 D3’s second row derives the ledger again; a description
cannot be, because what it says is a reviewer’s judgement — a crosses read off
a field, a signature that lies corrected. So the row becomes a refusal and the
command, which is the same thing said to a person instead of to a build.
And only where there is a hash to disagree with. A crate declared by version
has its sources in Cargo’s registry cache, and a description that recorded no
[sources] recorded nothing; both answer nothing moved. Refusing on an
absence is what ADR-169 D1 keeps NK2201 from doing, and here it
would refuse every crate that comes from a registry — Part III
C.4’s correct program refused.
The order of the rest:
path.#100 carries it.