Fixed facts a builder otherwise looks up itself (counted over 48 builder runs,
2026-10-25: median 2, up to 19 grep -r calls per run to find where a name is
defined; four pre-push failures on the same badge; seven runs puzzled by
the same dirty lock file). Read once; the brief adds only what is specific.
Guards of the gate that are red on develop before your change, with their
figure; whoever finds such a red or fixes it rewrites this list (y7a,
2026-10-30: a builder spent a build and two runs on a guard that was red
without it). A change that leaves the figure where it stands, or better, does
not make your gate red: report Gate: green and the figure under Tests:.
sequences::the_corpus_has_no_more_unanswered_method_calls_than_it_had: 18
against a ceiling of 16, since 75e25ed8 (43312d30); fix-forward
docs/briefs/b-fixforward-corpus-unanswered.md (main lane).| What | Where |
|---|---|
| Checker, moved parts | crates/nikaia-std/src/tools/check_*.nika (87 files); Rust adapter in crates/nikaia/src/check/ |
| Emitter | crates/nikaia/src/emit/mod.rs, emit/host.rs; moved parts tools/emit_*.nika |
std methods by receiver |
std_receivers in tools/check_core_method.nika |
| Generated Rust of a tools file | tools/lowered/<file>.rs, index tools/lowered.rs (never edit, lower-std writes them) |
| Program tests (new tests) | tests/compiler/tests/NAME.nika + .stdout / .stderr |
| Rust tests by NK code | grep -rln NK1234 crates/nikaia/tests/it/ |
| Specification | docs/specification/10-nikaia-light.md, 20-nikaia-advance.md, 30-nikaia-tooling.md (code table) |
| A name, Rust or Nikaia | nikaia run scripts/where.nika -- NAME...: definitions (fn, struct, enum, type) with file:line, the files that call it; for an NKnnnn the tests and the spec row (a field is not found: grep it); -- --outline FILE lists each item of a .rs / .nika with its line range |
| An unfixed binary, for before/after probes | copy target/fast/nikaia to your scratchpad before your first build (the suite runs on CI since 2026-11-01; /home/user/nikaia-suite is no longer updated) |
return x and as let r = x; return r, with the struct’s field really a view (built from a trim() inside a function; built from a literal in main the field is owned and the probe passes). That pair found #1071 and #1085 (y21 runs 2-3).tests/compiler programs run target/fast/nikaia, so nothing may rebuild it while they run (j2, 2026-10-31: the it binary started beside them relinked it and five tests failed for nothing). Run nikaia test --project tests/compiler and the it binary one after the other, never together. One program alone: nikaia test has no filter and nikaia run refuses a file inside a project, so copy it into a scratch project: D=<scratchpad>/one && mkdir -p $D/tests && printf '[package]\nname = "one"\nversion = "0.1.0"\n' > $D/nikaia.toml && mkdir -p $D/src && echo 'fn main() {}' > $D/src/main.nika && cp tests/compiler/tests/NAME.* $D/tests/ && target/fast/nikaia test --project $D.nikaia check FILE in tests/compiler/tests/ reads its sibling files and prints their errors, and nikaia test without src/main.nika crashes. Copy probes into the scratchpad, outside any project.nikaia check --bounds FILE checks a file alone; a tools file is measured only with lower-std --bounds --asserts [--optimization prove-through-loops:on] > R (45 s) and nikaia run scripts/proved.nika -- R, before and after on the same tree. Where the walk stops (2^62 goal cap, no local types, loop guesses, if join): docs/formal-verification.md §2. A test’s .bounds / .loops.bounds key sites by line: an edited comment keeps its line count, or bless again. A precondition on a parameter’s length is carried to the function’s start (ADR-269 D20), so a refused-call test row stands in main with a value the prover cannot read (let k = xs[2]). A precondition change changes what is refused: run grep -rln 'assert(' crates/nikaia/tests/it/ modules first (seconds), not the full it binary. Nikaia has no i8; widening is i64::from with use std::i64.nikaia run scripts, p1): text to integer text::parse_i64(s) (use std::text); arguments for a in cli::args() { args.push(a) }; a text is cut only at a position find returned (NK1214), else strip_suffix(..) ?? "".git log --grep), never by short hash: the driver’s rebases change hashes (p9, 2026-10-31).Kinds and the catch-all in a match on a kind (j2): the kind names are Part II 10.3’s (I64, F64, Text, Bool, Maybe(A), List(A), Map(K, V), Struct(S)), not Int; the catch-all arm is else, never _ (Part I, Patterns).
rust-toolchain.toml says channel = "stable": CI installs the newest stable, this machine’s stable lags until someone runs rustup update (2026-10-31: CI 1.99.1, here 1.97.0; clippy 1.99’s new chunks_exact_to_as_chunks kept develop red from c80e63ac). rustup check shows the gap. When it shows one and your change touches Rust, also run CARGO_TARGET_DIR=/home/user/nikaia-clippy-new cargo +<that version> clippy --all-targets -- -D warnings (rustup toolchain install <version> --profile minimal -c clippy once) and delete that directory at the end; never rustup update stable yourself (every target/ of every agent rebuilds).lowered.rs; git add -p is interactive and refused here). Stage only your hunks: git diff FILE > /tmp/p.diff (use your scratchpad), delete the other agent’s hunks from the copy, git apply --cached /tmp/p.diff, then git diff --cached FILE to check. Never commit their hunks, never restore them.drive.nika rebased the shared checkout (git pull --rebase --autostash after a rejected push, scripts/drive.nika drive_push; b1043, 2026-10-31: one probe round lost, UU files left). When a probe behaves as if your change were not there, first check git status and that your edit is in the file, then rebuild; never resolve or stage the UU files of others, report them under Needs:.winnow_grammar” or “Cannot find type E“: target/fast/nikaia has an old winnow-grammar rev baked in by crates/nikaia/build.rs, never your change (y12, 8 turns lost). Before the first grammar probe: grep -c "$(grep -o 'rev = "[0-9a-f]*"' Cargo.toml | grep -o '[0-9a-f]\{7,\}' | head -1)" target/fast/nikaia must print 1 or more; if 0, touch crates/nikaia/build.rs and cargo build --profile fast -p nikaia.match instead of catch on an entry, Bytes(".."), a thrown String). This runs and prints 42; the generated Rust is under ~/.cache/nikaia/run/main-*/target/nikaia/gen/ (newest first: ls -dt). A lowercase rule skips whitespace before each element; rule WS = "" -> { } turns that off.
grammar G {
rule WS = "" -> { }
entry rule doc(n: i64) -> i64 = d:dec[i64](digit+) eof -> { d + n }
}
fn main() {
let input = Bytes("41".bytes().collect())
let r = G::doc(input, 1) catch {
println("error")
return
}
println(f"{r}")
}
use yaml in packages/yaml/tests): the compiler’s own test project is a package too (tests/compiler, name compiler_tests), so its pub items in tests/compiler/src/main.nika are reached from a test with use compiler_tests; that is how a cross-package case gets a test CI runs (#1037).nikaia.contracts and nikaia.derived when they changed, never nikaia.lock or a nikaia.proofs your change did not cause. Write a new package test’s .nika before the first nikaia test --project run (one run is 3-4 minutes), and fill its .stdout with --bless in that same run. Only once nikaia check on the changed file is clean: a package that fails to build blesses a .stderr for every test (y27, 2026-10-31: 18 files to delete). An NK1127 on a default (limits: Limits = default_limits()) after your change means rustc refused the lowered file; to see rustc’s error, nikaia run a scratch copy with the defaults written out.A loop that takes the elements away is for x in v.drain() (Part I 6.5); a plain for x in v over an owned Vec only borrows them.
views.nika before one probe found the cause): nikaia check --tethers F.nika prints what each result views, and the nikaia.contracts file written beside a lowered probe shows the ledger the lowering used. Run both on the probe first; they name the pass (buffers.nika, tether.nika, views.nika).selfhosting_defects::a_run_and_a_kept_lambda_in_a_statement_differ, which asserts move |x| Box::pin; develop stayed red for the next three briefs). Before pushing, grep -rn '<a fragment of the old lowering>' crates/nikaia/tests/it/ and run each test it names (cargo test -p nikaia --test it <module>::<name>).cargo check --profile fast -p nikaia-std --features compiler --message-format short, right after the first lower-std and before writing a Rust host. Without --features compiler the tools module is not compiled and the check is a false green (k3a2 run 4).nikaia test has no name filter: write its whole output to a file (> out.txt 2>&1) and grep that, never | tail (b933 ran tests/compiler a second time, 10 minutes, to find one test’s line).tests/compiler/nikaia.lock is git-ignored, like the other test
packages’ locks (it changes with every compiler build). Never commit it.scripts/pre-push.nika fails with out of date: self-hosted.svg when a
.nika changed the self-hosting share: regenerate it from what you commit,
not from the shared tree (other agents’ files count there): git
checkout-index -a --prefix=target/badge/, python3 scripts/badges.py in
that copy, copy its assets/badges/self-hosted.svg back and add it to
Files: (the only badge you touch).cargo test ... --test it -- a:: b::: the -- before the filters is needed.export A=1 && S=x cmd & sends the whole chain to the background, and the
output is lost (P4b ran clippy and its tests twice): put the & on cmd alone.nikaia run script cannot use another script and cannot find its own
binary; a test program cannot set RUSTFLAGS or see git state. Such a
change is checked by hand runs; say so under Deviation.x86-64-v3.Deviation and build from the brief’s title only.tools/*.nika: the list below, Moving Rust into Nikaia.range_spelling, shipped_ledger, the std ledger in
contracts) scan the whole repository: a script’s a..b or a new
unsafe fn fails them. They are in the gate (builder.md) for that reason.target/fast. CI builds target/release only; a test that runs the toolchain takes the nikaia that runs it (readlink /proc/$(cut -d' ' -f4 /proc/$PPID/stat)/exe), never a fixed target/fast/nikaia (four script tests kept develop red that way, 2026-10-26). A red test that passes locally: check what CI lacks first.it binary (cargo test --profile fast -p nikaia --test it, no filter): many it tests hold one-line programs as Rust strings, and accepted_compiles lowers the corpus; 9936c70 ran -- parse only and turned develop red (2026-10-26).scripts/X.nika runs the tests that name it (grep -l X tests/compiler/tests), also from the main session: a --dry-run line added without its .stdout kept develop red (7970db8, drive_actions, 2026-10-26).tests/compiler test is run through nikaia test --project tests/compiler, never only with nikaia run (7c1643d, 2026-10-29: print(m) writes 1 without a newline, the .stdout held 1\n, CI went red on bootstrap_compiler and arm64). print writes no newline, println one; the .stdout holds the bytes exactly.nikaia run scratch.nika, a copy in the scratchpad (b1006, 2026-10-30: three turns lost). Plain rustc cannot link nikaia_std; nikaia lower F.nika writes F.rs itself, so never redirect its stdout into a file; nikaia run refuses a file inside tests/compiler (not the project’s entry), so copy it out first. Wait for a background test run’s wake-up, never a sleep loop. A method std.contracts declares that Rust also has under the same name (iter) needs only the contract: the emitter passes the name through; std.contracts is built into the binary, so rebuild after editing it.target/fast/nikaia mid-run: after your build, cp target/fast/nikaia "$SCRATCH/nikaia" and run long tests (nikaia test --project tests/compiler) with that copy (P5a lost two runs, 2026-10-28). Some tests still spawn target/fast/nikaia by path even then (b1034b, 2026-10-31: 5 of 6 failures were a vanished binary): such a failure is not yours, re-run that test singly. An untracked test another agent is writing may fail: name it under Tests:, do not touch it..map(fn(x) { x.clone() }) in tools/*.nika lowers as written and trips clippy::map_clone: the tools module in crates/nikaia-std/src/lib.rs allows it since P5d (0f9a525); another lowered lint goes into the same allow list, never into the lowered file.A package is lowered file by file, each with its beside files (cv2, 2026-11-01: half a run): a new mutual call between two files of a package can meet an emitter analysis that reads only one file. Probe it first as a two-file scratch package against the same calls joined into one file. The pausing graph is package-wide since 72e7789c; code_on_a_cycle (a lambda round a cycle) is not yet (#1129).
--asserts lists only the checker’s claims.sync (#1014, y5): a body change that makes a tools/*.nika function async leaves its callers’ lowered files stale, and a touch or NIKAIA_LOWER_CACHE_SALT does not refresh them; declare such functions sync. Remove this line when #1014 closes.nikaia fmt on crates/nikaia-std/src/tools/*.nika (b1012b, 2026-10-31): those files keep 4-space indentation and long lines, and fmt does what Part III says (one tab per level, breaks at 100 columns), so it rewrites the whole file and the diff buries your change. Format by hand there; fmt is for packages, tests and examples.Mismatched types, Cannot find value) is the checker’s or the emitter’s: run nikaia check (it passes) and nikaia lower FILE (writes the .rs, shows each rule’s lowered type) before reading code; the backend winnow-grammar is a pinned git dependency, not changed here (y9, 2026-10-31). A label’s type comes from CheckState::pattern_type (check_grammar.nika). A rule’s result tier (view or owned) is decided a second time by grammar_pattern_kinds in text_tiers.nika: a new pattern kind needs an arm in both, or the checker refuses with NK1104/NK1106 (y10, 2026-10-31). A guard is a case of its own for both passes: its calls are tiered by bind_grammar_pattern (text_tiers.nika, #1032) and its text labels lowered by the Pattern::Guard arm (emit_pattern.nika, #1033); probe a guard alone before bisecting a whole rule (y2c, 2026-10-31).nikaia test --both-settings --project tests/compiler runs past the 10-minute default of a background call: give it run_in_background with timeout: 3600000, and put no & on a chain (y9 lost two runs).NK1127 and no reason: the generated sub-program of a run with a project is <project>/target/nikaia/build-time/comptime/<hash>/main.rs, none when lowering refused it. A new comptime value shape must also survive as the literal the sub-program carries for earlier comptimes (comptime_literal_of, re-checked) (j5, 2026-10-31).target/fast/nikaia to your scratchpad before your first build (Where things live) (j5, 2026-10-31).peek, not, a cut, a guard and fail cannot be bound (winnow-grammar, winnow-grammar-model/src/parser.rs); a binding inside text(..) is #1013 (decided: supported). nikaia fmt can break a long rule line holding if(cond, "label") (#1015): check the formatted file still parses. A published field may not hold both a view and owned text (ADR-282 D22): such a field is private and read through a method.ready issue, a brief names the one it meets and never writes around it): #1001 a group whose branches yield different types reaches rustc; #1002 hex_digit is an undefined rule (check a digit with a guard instead, x:any if(...)); #1003 an integer as scalar (decided: scalar::from); #1004 a temporary Bytes(..) as an entry argument (bind it with let). In a grammar: an option after ; needs a default; a helper called from an action is sync; --bless writes nothing for a test with no .stdout yet (create it empty). Remove a line when its issue closes.Rules from the sessions that moved the most; they save tokens, not just time.
Checker and the
emitter’s state become Nikaia structs (#558, owner’s direction: C directly,
no described Rust type in between). Rust’s struct holds the Nikaia state as
one field and a method moves into an impl in Nikaia once everything it
calls has moved; the large recursive core (value_of, stmt, call;
expr_as_written, a_stmt) moves in one step. grep 'Finding {' and
format!( find the cheap message-building first. A walk that cannot move yet
takes a callback (fn(ref Expr) -> i64 sync; see bounds_basic.nika). Code
keyed on expression addresses waits for the expression id (#558); changing a
mut parameter in a match is ADR-094 D7 / #576.libraries.nika + libraries.rs
(small), assets.nika, parse_numbers.nika (callbacks), interpreter.nika.
The Rust module stays as the adapter that calls nikaia_std::tools::<m>::f.tools/*.nika. Type and function names are global
(Decision clashed): prefix them with the module. Calls to other tools files
are unqualified..nika needs one lower-std run with the old binary before
cargo build (the module only appears in lowered.rs after that run), and a
row in the table in scripts/self_hosting.py. The share counts code lines,
not comments.*.nika or described.contracts change, run lower-std with a
binary built from that tree, or the sysroot test fails on a stale
lowered/*.rs. lower-std reads and writes the checkout the binary was
built in (CARGO_MANIFEST_DIR, unless NIKAIA_SYSROOT says otherwise),
not the working directory. A lowering change can change the generated files:
run lower-std after building and commit the diff. It reports every refusal
of a file, in file order, as nikaia check does: fix them all before the next
cycle.git archive copy with the shared target dir
replaces target/fast/nikaia, and that binary looks for std in the copy.
Once the copy is deleted, nikaia run, test and lower-std fail with
“failed to read .../crates/nikaia-std/Cargo.toml”. Set
NIKAIA_SYSROOT=/home/user/Nikaia/crates for every call of the shared binary.target/fast/nikaia lower f.nika or nikaia run f.nika. A defect that shows
only in tools/*.nika needs a throwaway probe function in a tools file and
nikaia lower-std; look at tools/lowered/<file>.rs, then restore both.target/fast/nikaia run). Not a licence to write
around (CLAUDE.md A missing form stops the work): [design] holds, build
that way; [gap #N] is a decision issue: meeting it stops the slice
(Result: blocked), no way around; [defect #N] is a ready issue: fix
it or wait; [works] is a form that exists today: use it, and do not copy
the old way around from the moves that did (#953).
if let: x ?? default, match, or
starts_with then strip_prefix(..) ?? "";*x; loop variables over a ref Vec are values;.to_owned() is NK1189: write .clone();String; never ref on a
number, bool or a data-less enum (NK1257). Until #654 step 0 is built,
ref String is written in exactly these places and nowhere else: a field;
a type argument (Vec[ref String], a map’s key); an option after ;; a
function’s or a rule’s result; a parameter the result points into; a
parameter used as a key of a map keyed by ref String. Everywhere else,
above all a parameter that is only read, it is String;opt ?? "" moves the option:
(opt ?? "").clone() for a second use; a method on a String? is
opt?.len() ?? 0;{{ }}; a plain string takes braces literally;fn(ref X) -> T sync;
[works] a Span that arrives by reference is copied with s.clone();tools/*.nika, above] view_of resolves to
signature::view_of, not check_types::view_of_type: a false NK1104
(returns ref String, but declared ref String) means the wrong one was called;Some(..)/None are no patterns of a T?:
bind how ?? ... first and match the plain enum;T? local needs ?.clone() (NK1125);ref String? parameter takes a bare String argument (and
ref String? is NK1261, write String?);mut on one name of a tuple
(for (k, mut v) in m, let (a, mut b) = …), and so a map’s
values_mut() / iter_mut(), is refused now, possible later (NK1307):
a move that needs it stops (Result: blocked);a..b includes b; the half-open range is a..<b;match is else =>;
_ => and None => are refused;String? field is matched as it is (match s.f { null => .., x => .. });BTreeMap has .iter() (m.iter().filter_map(..))
and for (k, v) in m; [works: ADR-293 D20] a.iter().zip(b.iter());
[works: ADR-282 D7, #816] in-place change of a map’s value is
let mut c = m[k] ?? continue (or m.get(k) ?? continue, the same get_mut since 0.0.1277, #952);
[gap #950] a BTreeSet has no extend, a BTreeMap no entry
(HashMap::entry works); [gap #949] no mem::take, and no part taken out
of a let mut view (NK1131); [design: Part I 2.6] a text is cut only at a
position it handed out (NK1214): a prefix of a hex digest is
hex.scalars.take(16).collect() or hex.bytes[0..<16].text();
[design: Part III fs] fs::read is async and gives Bytes (the Rust
caller needs block_on; a Vec[u8] is filled by a loop, as
sysroot_cache.nika does); read_dir gives sorted names, not paths. Two
host parameters [M: A, H: B] lower.let mut out: String = "a"); not probed in tools/ since: if
lower-std reads String? again, reopen #943 rather than type it.throws lowers to
Result<T, Box<dyn std::error::Error>>; to hand an anyhow refusal back
unchanged, keep it in a RefCell of the Rust host and return it after the
call. A body-less trait method’s text parameter reads as owned: write
ref String. Rust state the host keeps (checked units, a source map) is a
RefCell field; the Nikaia side names units by i64 index.it run: filter with grep -E '^test result:|FAILED|panicked',
anchored: a bare test result also matches test names such as
result_source:: and hides the summary (M3b repeated a 2-minute run).String is &str where a view suffices and String
where it is owned (the compiler decides, ADR-282 D2), ref T is &T, Vec[String] is
Vec<String> (a ref one a slice), String? is Option<String>. Most Rust
errors after a move are & against owned. Check that Display equals
Ty::text() before replacing {ty} with it.std::ptr::eq): a resolver that returns an owned value breaks it silently.
Destructure map loops (for (name, value) in map), not entry.0/entry.1,
which the checker cannot type; a named _ (for (name, _) in map) avoids the
unused-variable clippy error in the lowering. A layout change breaks tests
keyed on an address by allocator luck.cargo check lists
them as unused), and re-grep function names instead of trusting line numbers
from an earlier listing: every commit shifts them.str.replace('', ...) (an empty-slice replace corrupted emit/mod.rs once)..iter().map(..).sum(), .any(..), .filter(..).map(..).collect())
are the house style in tools/*.nika (owner, 2026-10-27). Open
lowering defects are GitHub issues, the language options against the usual
hurdles in docs/history/mitigation-hurdles.md.The cycle, clippy beside the tests, the report and commit.nika:
.claude/agents/builder.md. Disk at the end: the same file, last step.