lupin changelog

The reference interpreter. An entry per tagged release, each against a named specification pin.

0.1.52 — 2026-10-12

THE FIFTY-SECOND, the lupin half of wolf 0.2.29. Pin unchanged: 294d626d (wolf-lang v0.2.24). Eleven commits since 0.1.51, all the lupin half of one wolf-lang lane, s232 (the shell's signals; a planted break, 04f2554, and its revert, 5dd150f, among them). Every lane was merged before the release lane (r34) began; r34 adds the version.

The shell's signals: the mirror (wolf-lang s232, #679, #681, #141; ruling #70)

wolf-lang s232 gives every signal POSIX names a bit (twenty a program may catch, eight it may only send), one shape rule for every set, a read of the ignored set, a send to any pid, a meaning's number and this process's id and its parent's. This machine mirrors what std reaches without unsafe (ruling #70, R9, wolf-lang [os.signal.shell.note]):

0.1.51 — 2026-10-11

THE FIFTY-FIRST, the lupin half of wolf 0.2.28. Pin unchanged: 294d626d (wolf-lang v0.2.24). Nine commits since 0.1.50, all the lupin half of one wolf-lang lane, s230 (the calls that change a file; a planted break, 71014ec, and its revert among them). Every lane was merged before the release lane (r33) began; r33 adds the version.

The calls that change a file, and os_error() after exists (s230, wolf-lang#346, #698, #699, #700, #702, #704; #235)

lupin's half of wolf-lang s230, written from [os.fs.mode], [os.fs.create.mode], [os.fs.symlink], [os.fs.link], [os.fs.times], [os.fs.truncate] and [os.fs.umask]. Eight names resolve. Seven are served through the standard library's own calls, no unsafe: fs_set_mode (chmod, a final link followed), fs_open_perm and fs_create_dir_mode (a creation's permission bits, less the umask), fs_symlink (the target verbatim, an empty one invalid), fs_link (a second name; never replaces, never follows), fs_truncate (a length on a handle, the offset unmoved) and fs_set_times. Every shape row (invalid) is answered before containment and before the host, in the compiler's order; windows answers the clause's unsupported row for the mode, the moded creations and the symbolic link.

What the standard library does not reach is refused BY NAME, never answered with a row the host would not give:

Whether lupin may take a safe system-call crate for these two was wolf-lang's [os.fs.change.note] R10, the question ruling #57's R8 answered "no" for the terminal calls. Ruling #67 answers it the same way (R10 = A): both stay refused by name.

os_error() is the host's number after exists and cross_device (#235). fs_create_dir and fs_rename matched those two kinds ahead of the one place the number was noted, so it read 0 after exactly those rows where every compiler machine answers EEXIST and EXDEV. Both old rows are fixed, and every new row notes it.

tests/fs_change_s230.rs runs wolf-lang's six corpus rows and five unix fixtures byte for byte and asserts each refusal on its words; every row of wolf-lang's fs_change_lanes gate agrees with this lupin as the sibling.

0.1.50 — 2026-10-10

THE FIFTIETH, the lupin half of wolf 0.2.27. Pin unchanged: 294d626d (wolf-lang v0.2.24). Thirty-six commits since 0.1.49, the lupin halves of six wolf-lang lanes: s220 (5, a wrapping product past i128), s219 (7, jobs and the signal poll), s225 (9, env_unset and os_exec), s224 (4, json last-wins), s218 (7, the stat record, lstat, readlink, the typed listing) and s204 (4, the std beside the binary). Every lane was merged before the release lane (r32) began; r32 adds the version and one CI line. wolf-lang's gates pin 0.1.49 as pre-mirror in int_truth_lanes, json_dup_lanes, fs_stat_lanes, ctrl_c_lanes and exec_lanes; the 0.2.27 pairing measures each against this release's published archive and drops what it answers.

The std beside the binary is the last std root (wolf-lang s204, ruling #29)

wolf-lang ruling #29 makes the compiler's default std root a std directory beside the wolf binary, where its release archive now ships wolf-std at a pin. lupin reads the same default beside its own binary, last: --std-root, then LUPIN_STD, then a std/ beside the entry (s166's fixture rule), then std/ beside lupin. A lupin unpacked beside such a wolf (the pairing's sibling layout) therefore reads the very tree the compiler reads, and use std.env runs there with nothing set, where it answered unsupported. A lupin with nothing beside it answers as before. lupin's own archive ships no std (the ruling owed is named in wolf-lang's s204 PR). The no-std refusal for a home-module method now names the std/ beside lupin as a third way to configure a root.

A repeated json object key is last-wins (wolf-lang s224, #124)

Ruling B22 (2026-09-11) made a repeated object name last-wins, and wolf-lang s224 writes it as [os.json.dup]: one member per name, at the position where the name first appears, holding its last value. lupin read the FIRST occurrence and counted every one, as the three compiler machines did, so json_get("{\"a\": 1, \"a\": 2}", "a") was 1 and json_len of that object 2; it is now 2 and 1. A path walks through the last container a repeated name holds, and a nested object that json_get re-renders carries the name once ({"a":1,"b":2,"a":3} renders {"a":3,"b":2}); the root's own text is unchanged. Written from the clause, not from wolf-lang's kernel (the track's independence rule). Red at 1c1f096: tests/json_dup_s224.rs, which holds wolf-lang's seven corpus/json/dup_*.lu rows (copied verbatim) to their headers; lupin 0.1.49 answers all seven first-wins. wolf-lang's json_dup_lanes gate pins 0.1.49 (f516a5f) by version and commit until a lupin release carries this.

Exec and env unset: the mirror (wolf-lang s225, #534)

wolf-lang's [os.proc.exec] and [os.env.unset] add two builtins.

A wrapping product past i128 keeps its low bits (wolf-lang s220)

wolf-lang s220's four-machine integer sweep found that lupin multiplied every integer in i128 with checked_mul, so a wrapping[u64] product whose exact value passes 2^127 — (2^64 - 2)^2, or (2^64 - 2) * 16648325558172019491 — trapped overflow where its answer is the product's low 64 bits (4 and 3596837031075064250) on every compiler machine. A wrapping product now falls back to i128's wrapping product, whose low 128 bits reduce reads; a saturating one saturates by sign; a checked one still traps. Red at f516a5f (0.1.49): tests/int_truth_s220.rs, which holds all eighteen of wolf-lang s220's int_truth_* corpus rows (copied verbatim) to their headers; lupin answered the other seventeen already. wolf-lang's int_truth_lanes gate pins 0.1.48 and 0.1.49 by version and commit on this row until a lupin release carries the fix.

Ctrl-C and jobs: what lupin can mirror (wolf-lang s219, #622)

wolf-lang's [os.signal.disp], [os.signal.poll], [os.proc.job], [os.proc.status] and [os.term] add eleven builtins. lupin forbids unsafe and links no libc, so it serves what std reaches and refuses the rest by name ([os.term.note] R8):

lupin's half of wolf-lang s218, written from [os.fs.stat], [os.fs.readlink] and [os.fs.readdir]. fs_stat and fs_lstat answer the twenty-word record (kind with links, fifos, sockets and devices told apart; size; modified_ms; the have mask; mode, nlink, uid, gid, blocks, dev, ino, rdev; access, modification, change and birth times to the nanosecond) from one host call; fs_read_link the target's bytes, invalid on a non-link; fs_read_dir_entries the directory in the host's order, each entry its own kind and its name's bytes, a non-UTF-8 name included. Containment: fs_stat keeps the family's resolved check, so a link whose target leaves the served tree is refused by name; fs_lstat and fs_read_link check the parent only, since they follow nothing. No unsafe. Every row of wolf-lang's fs_stat_lanes gate agrees.

0.1.49 — 2026-10-08

THE FORTY-NINTH, the lupin half of wolf 0.2.26. Pin unchanged: 294d626d (wolf-lang v0.2.24). Fifty-three commits since 0.1.48, in six lanes: is74 (11, volatile and atomics), is75 (9, the CI slot budget), and the lupin halves of wolf-lang's s200 (5, the byte surface), s213 (13, the kernel papercuts, with r31's four), s215 (9, a child's descriptors) and s216 (6, copy region). Integrated by r31 one lane at a time. The 0.2.26 pairing drops the wolf-lang gate pins that waited on #185, #194, #200 and each lane's mirror.

Volatile access and atomics on *T (is74, #185, #194)

kw07 (K3 = B) and kw11 (K5 = A) gave wolf read_volatile/write_volatile and nine raw-pointer atomics with Order as syntax; lupin answered unsupported on every row. On every row lupin now answers what the compiler's lanes answer (64 witnesses in tests/rulings_is74/, measured against wolf 0.2.24), the counts of tasks running atomics included.

CI: the slot budget (is75)

The org's free plan runs 20 jobs at once, five of them macOS, across every repo, and a full wolf-interp run is 17 jobs and about 21 slot-hours. The plan job in .github/workflows/ci.yml now decides the matrix by event: a push to trunk, the nightly, workflow_dispatch and a pull request labelled full-matrix run the full matrix as before; any other pull request runs ubuntu's three shards and ladder in full plus one windows and one macOS smoke (build, fmt, clippy, every test target but the heavy ones SMOKE_OMITS names, and the fast ladder rungs; a 20-minute cap, which r31 found at its edge: one windows smoke timed out at 20.2 minutes with nothing failed, wolf-interp#215). ci/test-shards.sh gains smoke-args; ci/assert-shard-coverage.sh checks the OSes SHARD_OSES names (all three when unset) and says which. CONTRIBUTING.md documents the budget and how to ask for the full matrix before a merge. The plant (a windows-only failing test in cli) went red in the full matrix (run 37549965195, windows shard 3) and was absent from the smoke, as designed; reverted.

The byte surface, mirrored (s200; wolf-lang#405, #411, #417, #407)

wolf-lang's s200 gives a program its standard streams as bytes, a bulk byte scan, a copy the host makes, and the host's error number beside the row. lupin answers each one as the compiler's three lanes do (tests/byte_surface.rs: wolf-lang's four corpus rows and two stdin fixtures, standard input a file, a pipe and an offset the test shares).

The kernel papercuts, lupin's half (s213, #200; wolf-lang #572, #575, #577, #579)

wolf-lang s213 closes four gaps the PAX kernel worked around; lupin now answers each of its rows as the compiler's checked machine does (22 witnesses in tests/rulings_s213/, 19 of them the s213 corpus rows verbatim). Two of the four spellings — -> never and ! on an integer — were ruled by the maintainer on 2026-10-07 as implemented (rulings #50 and #51, A each).

A child's descriptors, a pipe, the working directory, isatty (s215, pelt's H2)

wolf-lang s215 adds four host builtins ([os.proc.fds], [os.proc.pipe], [os.fs.chdir], [os.fs.isatty]); lupin mirrors them, witnessed by the compiler's own rows and fixtures in tests/proc_fd_s215.rs.

copy region { … } (wolf-lang s216, #612)

wolf-lang's [mem.region.copyout]: a region block whose value is copied into the region the block was entered from before the block's region is freed, so a long-running loop keeps each turn's result and frees the rest. lupin read the spelling as a plain copy of a plain block and trapped region-fault at the } on every such program (0.1.47, 0.1.48).

0.1.48 — 2026-10-06

THE FORTY-EIGHTH (is72 and is73), the lupin half of wolf 0.2.25. Pin: 294d626d (wolf-lang v0.2.24, the TAG), two releases forward of 0.1.47's 8e36bc1a (v0.2.22). Thirty-four commits since 0.1.47, in two lanes: is72 (14, the place trace) and is73 (20, the re-pin blockers). lupin now carries the spec and corpus of the compiler it pairs with, and the 0.2.25 pairing drops the sixteen wolf-lang gate pins that waited on #190 and #188.

The re-pin: 8e36bc1a → 294d626d (r30)

r29 found lupin could not re-pin on wolf v0.2.23: kw09's memory/static_init_cycle.lu overflowed the interpreter's stack (SIGABRT, no record) and aborted the corpus walk, and kw08's and kw09's rows parted eight times. On every row lupin now answers what the compiler's three lanes answer (90 witnesses in tests/rulings_is73/, measured against wolf 0.2.23), or refuses by name where the clause says the machines do.

The place trace: lupin --trace-places (is72)

The book's chapter 7 asked what a plain = on a str field does (it copies: str is Copy). lupin now shows it. --trace-places writes one JSON line after every statement of every function activation, and after a body's tail, naming the state of every place in the executing frame: each binding and every struct field, tuple element, and the list elements and map values the program addressed.

0.1.47 — 2026-10-05

THE FORTY-SEVENTH (s209's lupin half), the lupin half of wolf 0.2.24. Pin: 8e36bc1a (wolf-lang v0.2.22, the TAG). This is the same pin as 0.1.46: there is no re-pin in this release. Eleven commits since 0.1.46, all in one lane. lupin now answers ruling #36's row L4 the way the checked machine does, so the 0.2.24 pairing drops the six L4 pins s209 set in raw_align_lanes.rs.

The re-pin on v0.2.23 (8edac3ee) waits (r29, measured at the unshipped c3f1dab, the vendor trees byte-equal to the tag's; the orchestrator ruled to ship without it). Its census would move corpus files 947 → 982 and anchors 569 → 586 (17 added, none dropped). Three things block it:

Every other new row matched or was out of scope; kw07's volatile rows (#185) do not mismatch. L4's own clause, [mem.unsafe.raw.4], is not at v0.2.23 either: it lands at wolf-lang a3465f87, after the tag, so a re-pin on v0.2.23 still would not carry it.

Row L4: a misaligned ordinary raw access (s209, ruling #36 = A, #192)

0.1.46 — 2026-10-04

THE FORTY-SIXTH (s208's lupin half, is70, and r28's re-pin), the lupin half of wolf 0.2.23. Pin: 8e36bc1a (wolf-lang v0.2.22 — the TAG), one release forward of 0.1.45's dfcc2f13 (v0.2.21). Fifty-two commits since 0.1.45 in two lanes, then the re-pin. It closes the gaps the PAX and compiler lanes filed against lupin (#174, #175, #178, #179, #180, #181, #182, #184): on every row they pinned, lupin now answers what the compiler answers, so the 0.2.23 pairing drops 25 of the 26 pins 0.2.22 kept and kw06's 7. **One place lupin was looser than the compiler is closed: *p outside unsafe is E1301.** And it mirrors ruling #34: an else-less if is () whatever its taken block answered.

The re-pin (r28; 60b93e3). vendor/upstream/{spec,corpus} and the upstream gitlink move to 8e36bc1a, an ancestor of the released line (merge-base --is-ancestor dfcc2f13 8e36bc1a holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 892 → 947 (55 new, none leaves), entries 846 → 900, members 46 → 47 (kw02's membrane/geo/geo.lu; 09779e8, 87d8792); anchors 546 → 569 (23 added, none dropped, the key sets diffed both ways); distinct conforms: anchors 379 → 392; the coverage ratchet 276 → 290; the bundle 930/884 → 985/938 programs and records (ff55678). Thirty-four of the fifty-four new entries reach run (3037ef1): twenty-three match their check: and eleven are the dynamic counterparts of s207's E1010 rows (region-fault at run time). kw03's three narrowing-cast traps gain fault snapshots (1bdb791). The three rows is70 ruled ahead of the pin retire (0f2e540, 3037ef1, b0fbceb): memory/unsafe_sig.lu's peek is pub now (be13b445), so it fails E1302 at resolve as pinned and leaves the run ledger; comptime.lu drops its #[noalloc] (e951afbb) and resolves; ffi.lu carries an aarch64 twin of its asm, so every host keeps one and declines it by name, and the CI ladder's check has one answer again. The walk's mismatch count is 3 on every host: DIV-2026-019 and DIV-2026-027's two rows. s203's first_* rows pin E0107 and E0207, so both numbers leave UNPINNED_CODES (0cc24c1; the not-UTF-8 and nesting-rail sites ride them); E0817 and E0818 are resolve-rung codes in tests/conformance.rs; and a phase: lex row may carry a lexical fault after the parse refusal it pins (ruling #28; 0f2e540, fa60f85).

An else-less if is () (s208, ruling #34 = A; #179's if half; 3ec58bc). Whatever its taken block answered, a raise included, the value of an if with no else is (): rows/unit_discard_if_value.lu prints () (). s208 left no changelog text; this paragraph is written from its commits (54d6c3f..8d82031, PR #186).

is70 — the gaps the PAX lanes filed. lupin now answers what the compiler at wolf-lang 50830027 answers on the rows the KWC and compiler lanes pinned against it. Every row is a witness under tests/rulings_is70/, measured on the compiler's three lanes and seen red at trunk first. The vendored pin does not move (still dfcc2f13, v0.2.21); three of its rows are ruled ahead of the pin until the 0.2.22 re-pin carries their re-spelling (below).

0.1.45 — 2026-10-03

THE FORTY-FIFTH (is68, is69, s199's lupin half, and r27's re-pin), the lupin half of wolf 0.2.22. Pin: dfcc2f13 (wolf-lang v0.2.21 — the TAG), one release forward of 0.1.44's cdde128a (v0.2.20). Forty-five commits since 0.1.44 in three lanes, then the re-pin. It heals the one parting wolf 0.2.21 shipped pinned by version: a -> !T function's row is what its body can raise, so rows/eu_bind_empty_row_handled.lu prints 43 42 42 here as on every compiler machine (#176). It fixes lupin's two P0s from the org triage: a view that outlives its region is a region-fault and is never read from freed bytes (#126), and a unit context's value is () (#103 row 1). And it mirrors wolf 0.2.22's fs_seek, fs_tell and fs_read_at and the standard streams (wolf-lang#426, #424): the first handle is 3.

The re-pin (r27; 31d8dfa). vendor/upstream/{spec,corpus} and the upstream gitlink move to dfcc2f13, an ancestor of the released line (merge-base --is-ancestor cdde128a dfcc2f13 holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 861 → 892 (31 new, none leaves), entries 816 → 846, members 45 → 46 (s202's memory/versioned_loop_cross_module/ring/ring.lu; b8fa2a9, fd90571); anchors 544 → 546 (type.row.defer, type.row.match; none dropped, the key sets diffed both ways); distinct conforms: anchors 376 → 379; the coverage ratchet 273 → 276, the two new anchors and conf.trap.assert; the bundle 899/854 → 930/884 programs and records (3dc6dfa). No check: line moves; faults/index_origin_min_overflow.lu annotates its binding, so it now traps at the 1-origin shift it is about rather than at the literal (e0d1b96). Twenty-four of the thirty new entries reach run (79c0d45): nineteen match their check:, three are conservatism rows (s202's typecheck/numlit_binding_literal*.lu pin E0415 at the binding; this machine traps overflow at the literal) and two are filed. The six that do not reach run are is67's refusals, matched at resolve: tests/conformance.rs now reads E0801 and E0816 under [type.row.match] and E0611 under [type.row.defer] as resolve-rung codes, keyed on the clause (2c2cba3, fd414b2).

DIV-2026-027, filed (c2bba7c; wolffe-lang/wolf-lang#556). faults/assert_msg_effect_fails.lu and faults/assert_msg_name_fails.lu trap assert here as pinned. This machine also renders the message as one stdout line before the trap, which [conf.trap.assert] permits ("until then implementations may drop the message"). The rows' stdout= admits only the dropped rendering, so both are a MISMATCH and both are now waived by name. lupin 0.1.44 did the same thing. The walk's mismatch count is 3: DIV-2026-019 and these two.

A -> !T function's row is what its body can raise (is69, wolffe-lang/wolf-interp#176). 01-grammar.md gives -> !T an *inferred private row*, and the compiler seals it per module (s15): the least fixpoint, over the module's inferred functions, of the tags a body raises at its tail or a return (return Neg, Io(3)), the row of a fallible value that flows out (a call with a row, a Map index, a local bound to one) and the row of every ? (a closure's ? is its own). 0.1.44's [type.row.match] reader read every such row as the open row {..}, so a match over the result with no _ was E0801 whatever the body raised — wolf-lang's rows/eu_bind_empty_row_handled.lu (fn f() -> !int { 42 }, match a { v => v + 1 }) answered fail(E0801) where every other machine prints 43 42 42, and r26 pinned 0.1.44's answer by version in fallible_bind_empty_row_lanes.rs. src/rowinfer.rs is the inference, cached on the program; rowmatch::callee_row reads it for an own or a used fn, so sema's E0801 judge, the evaluator's two-half dispatch and the lint all see the body's row, and an uncovered tag is named ("does not cover Neg") as the compiler names it. A binding annotated !T takes its initializer's row. A body this machine cannot read (a method call, a closure call or a spawn whose row would flow out) keeps {..}, 0.1.44's reading. Twenty witnesses under tests/rulings_is69/: nineteen red at 8f08e49 (on 0.1.44's code), green at 675ccea, beside the compiler on every one; one control unchanged.

A view carries its receiver's region (is68, wolffe-lang/wolf-interp#126, the mirror of wolf-lang#392). A [mem.str.view] product — a slice, a trim, a split/words/lines piece, a view of a view — carries the home of the str it views (1a1c14f). A view that outlives the region its receiver was built in is the clause's trap(region-fault) (mem.region.intra.2), never a read of freed bytes. 0.1.44 ran nineteen such shapes to exit(0) from freed bytes; each is E1010 on the compiler. memory/region_str_view_return.lu is now a dynamic counterpart, not a conservatism row (d11cafe).

A unit context's value is () (is68, #103 row 1). An else-less if and a unit fn body yield () (5314dfb). Where this machine can type the tail, a typed tail in a unit context is refused E0401 at wolf's code and span, in a late tier pass (db9ac2a). Rows 2 and 4's static halves stay owed on #103. Found beside it and filed for the compiler: wolf-lang#541 (checked propagates a discarded W0601 row that native and release drop).

trim and its family take no argument (is68, #125). A [mem.str.ws] call that passes an argument is E0402 at resolve where the receiver is visibly a str (a4862f7). Where the receiver cannot be typed, the evaluator declines it by name and never trims a cutset (a77c4a4). trim_cutset_refused.lu is a match at resolve.

str and bool are declared scalars (is68, #138). They join the declared-scalar lattice in a late wide pass (a75adcc), so let s: str = side from an int is E0401 in the root and in a sibling module alike. The sibling's diagnostic names its file. lint_is61's t01 parting heals.

A nested fn's parameter modes are a module fn's (is68, #169). A call to a nested fn is held to its declared modes, E1007 at resolve (3ffdfd7). A moded nested fn runs with the module fn's convention (7dc33ba), so nested_fn_mut_param.lu matches and nested_fn_mut_moveout.lu traps use-after-move. r26's NESTED_MODED_FN_DECLINED waiver went red by name and was retired (b6ea927).

Seek, tell and positional read; the standard streams (s199's lupin half, wolffe-lang/wolf-interp#171, the mirror of wolf-lang#426 and #424). fs_seek, fs_tell and fs_read_at resolve, dispatch and declare the rows [os.host.sigs] gives them, including the new tag unseekable (ESPIPE) (99c35ca, ff53642). Descriptors 0, 1 and 2 are the standard streams for fs_fstat and the three offset calls ([os.fs.std]); this machine duplicates them with no unsafe, and close and read stay io there. The first handle is 3 (it was 1). On windows the four calls on 0..2 are declined by name (a7304a5): GetFileType is the only honest classifier there and the crate forbids unsafe. tests/fs_offset.rs passes 0 of 4 before the mirror and 4 of 4 after.

0.1.44 — 2026-10-02

THE FORTY-FOURTH (is65, is67, and r26's re-pin), the lupin half of wolf 0.2.21. Pin: cdde128a (wolf-lang v0.2.20 — the TAG), one release forward of 0.1.43's c2401f05 (v0.2.19). Twenty-three commits since 0.1.43 in two lanes, then the re-pin. It carries every lupin mirror wolf 0.2.21's gates pin 0.1.43's answers on by version: a match over a fallible value has two halves (the maintainer's ruling #21, [type.row.match]; wolffe-lang/wolf-lang#497) and a ? under a defer or errdefer is refused, E0611 (ruling #19, [type.row.defer]; wolffe-lang/wolf-lang#498); with a flow out of an argument list withdrawing the call's claims and protectors (#163).

The re-pin (r26; eb00a30). vendor/upstream/{spec,corpus} and the upstream gitlink move to cdde128a, an ancestor of the released line (merge-base --is-ancestor c2401f05 cdde128a holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 778 → 861 (83 new, every one an entry, none leaves), entries 733 → 816, members 45 → 45 (76469fa, bfb517d); anchors 542 → 544 (mem.tier0.excl.4, type.row.else; none dropped, the key sets diffed both ways); distinct conforms: anchors 374 → 376; the coverage ratchet 271 → 273, the two new anchors; the bundle 816/771 → 899/854 programs and records (189f141). Two check: lines move, memory/elem_dyn_read_after_mut.lu (run "3 2", eg03) and memory/mut_read_overlap.lu (run, s186), both fail(E1002) before; this machine already ran each, so both turn from conservatism rows into matches. Eighty of the eighty-three new entries reach run (69b3e18): forty-five match their check:, twenty-eight are dynamic counterparts of the compiler's E1002 (trap(exclusivity), twenty-three) or E1001 (trap(use-after-move), five), and seven are conservatism rows (three run-time offsets the compiler cannot prove distinct, two shorthand initializers, two nested read parameters). The three that do not are the nested fns that declare a parameter mode (memory/nested_fn_mut_{param,omitted,moveout}.lu), which this machine declines at resolve under #38's scoped v1; nested_fn_mut_omitted.lu pins E1007, which the resolve rung owns, so tests/conformance.rs waives it by name until the mirror lands (6b7ac41, #169). No new divergence; DIV-2026-019 is still the one filed.

A flow out of an argument list withdraws the call's claims and protectors (is65: wolffe-lang/wolf-interp#163). A ?, a return, a break, a continue or a trap that leaves a call's argument list never enters the call; [mem.tier0.excl.4] has a mut claim take effect at entry and [mem.prov.tag] protects the parameter-entry tag for the call's extent, and with no entry there is no extent. The list is abandoned — every access it held released, every protector it minted withdrawn, a (mut …) receiver's included — and the signal leaves as it left the argument. Through 0.1.43 the protected Reserved child stayed in the tree, so the next write through the place was a foreign write to a protected tag: put(mut xs, v(ok)?) and (mut xs).push(v(ok)?) answered ub(mem.ub) (mem.prov.state, P1) at the caller's next push, and the continue forms answered it inside the loop, where wolf 0.2.19 prints [1, 5, 3] and [1, 1, 3] on checked, native and release; the claim itself stayed held until its scope popped, which the REPL's top level never does ([repl.trap.alive]: trap(exclusivity) on every line after a trap out of a list). Eighteen witnesses under tests/rulings_is65/ — each exit kind, a nested call inside the argument in three shapes, a read lend, a scalar, two muts, every receiver form, and the REPL after a trap — each red at 3e7f230 and green at dfebf6b; six controls (a trap in a program, a take, a field, an element, an impl and a view-set receiver) unchanged. No new divergence; the ratchet holds at 271.

A match over a fallible value has two halves (is67, lupin's half of the maintainer's ruling #21 — wolffe-lang/wolf-lang#497, the clause [type.row.match] s197 writes). The scrutinee has type T ! {row}; a row arm names a tag of the row (binding its payload: Io(e) => …), a value arm is any pattern over T; an identifier that names a tag of the scrutinee's row is a row arm, anything else a value pattern; _ covers what is left on both halves; the row is consumed. A row value is tried against the row arms only and any other value against the value arms only, so match look(m, "a") { none => -1, v => v } prints 5 where 0.1.43 printed -1 (the row arm's name BOUND the value, the first-arm disease on the value half: eleven silent wrong answers under tests/rulings_is67/). What makes a match a row match is what a signature or a binding spells (src/rowmatch.rs, one reader for the evaluator, the lint and sema): a call to a module fn — own or used — by its declared return row, a builtin with a declared row, a Map index (V ! {none}, [mem.map.absent]), a local bound by such an initializer or by a T ! {row} annotation; a scrutinee the reader cannot name keeps the old dispatch unchanged. A non-exhaustive row match is E0801 at the resolve rung naming the missing tag, the missing variant or literal, or the uncovered value half ([proto.cmp.rung] makes the resolve-rung emission of the checker's code agreement, as E0805); a guarded arm counts for nothing; a tag that is also a constructor name reachable from T is E0816 (s197's code, wolf-lang PR #510), whether or not an arm spells it; an or-pattern mixing the halves or an @ at the top of an arm is refused by name before anything runs. The lint reads a row arm as no binder, and E0802 says nothing about a row match. else |e| match e { … } is unchanged. Twenty-one witnesses red at c08285f, green at 79af9df, five controls green on both sides.

A ? inside a defer or errdefer is refused (is67, lupin's half of ruling #19 — wolffe-lang/wolf-lang#498, s196's [type.row.defer] and E0611). The deferred expression runs while the function is already leaving, so the error has nowhere honest to go: the shape is fail(E0611) at the resolve rung, at the ? expression, reading the whole deferred expression (a call argument, an interpolation hole, a binding, a block); a ? inside a closure defined under the defer is that closure's own and runs. 0.1.43 ran the ? and made the row the function's result (… 9); wolf 0.2.20's native and release overflow the compiler's stack on the same programs.

0.1.43 — 2026-10-01

THE FORTY-THIRD (is61, is63, is62, and r25's re-pin), the lupin half of wolf 0.2.20. Pin: c2401f05 (wolf-lang v0.2.19 — the TAG), one release forward of 0.1.42's ec56a08f (v0.2.18). Forty-nine commits since 0.1.42 in three lanes, then the re-pin. It carries every lupin mirror wolf 0.2.20's gates pin 0.1.42's answers on by version: two-phase arguments (the maintainer's ruling #17, [mem.tier0.excl.4]; wolffe-lang/wolf-lang#476), the closure lent into its own call (#160), each operand once under a slice or a receiver (#157, #162), the field shorthand as its longhand (#159, wolffe-lang/wolf-lang#486), a mut receiver claimed while its own arguments run (#164, wolffe-lang/wolf-lang#487) and a view-set receiver's write-back (wolffe-lang/wolf-lang#494); with the lint's W1002 beside a moved-out mut parameter (#155), which retires r24's waiver.

The re-pin (r25; 21413a5). vendor/upstream/{spec,corpus} and the upstream gitlink move to c2401f05, an ancestor of the released line (merge-base --is-ancestor ec56a08f c2401f05 holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 747 → 778 (31 new, every one an entry, none leaves), entries 702 → 733, members 45 → 45 (75c05e4, 28fdb36); anchors 542 → 542, the same key set diffed both ways; the coverage ratchet 268 → 271 (mem.dyn.unsize, mem.iter.excl, mem.tier0.borrow, each cited first by a new memory/elem_* row); the bundle 785/740 → 816/771 programs and records (41be529). One check: line moves: memory/mut_elem_excl.lu, fail(E1002) → run "2" (eg02), which this machine already answered, so it turns from a conservatism row into a match. Thirty of the thirty-one new entries reach run (7da5709): seventeen match their check:, eight are dynamic counterparts of the compiler's E1001/E1002 (the six elem_whole_read_*_after_move rows trap use-after-move, elem_claim_whole_read and elem_prefix_mut_pair trap exclusivity), and five are conservatism rows (elem_dyn_*: a run-time index the compiler cannot prove distinct, whose value this machine sees); elem_header_methods_after_move_pool.lu is out of scope (Pool). No new divergence; DIV-2026-019 is still the one filed.

Every operand once, the shorthand is the longhand, a receiver's own argument (is62: wolffe-lang/wolf-interp#157, #162, #159, #164 and the lupin halves of wolffe-lang/wolf-lang#487 and #494).

The move is the write (is61, wolffe-lang/wolf-interp#155).

Two-phase arguments (is63, the maintainer's ruling #17 of 2026-09-30; [mem.tier0.excl.4], wolffe-lang/wolf-lang#485's clause, not yet at the pin).

0.1.42 — 2026-09-30

THE FORTY-SECOND (is57, is58, is59, is60, and r24's re-pin), the lupin half of wolf 0.2.19. Pin: ec56a08f (wolf-lang v0.2.18 — the TAG), two releases forward of 0.1.41's 93a5fe50 (v0.2.16). Forty-four commits since 0.1.41 in four lanes, then the re-pin. It carries every lupin mirror wolf 0.2.18's gates pin by version — a whole read of a place holding a moved part traps (#143), a Map read moves a non-Copy value out (#144), a nested index store runs each operand once (#145), the caller sees a mut parameter its callee moved out (#146) — and the rulings of 2026-09-29 and -30 on [mem.model.place.elem] 1(c): a member read under an element claim reads the member (wolffe-lang/wolf-lang#472), len, count and is_empty are header reads (wolffe-lang/wolf-lang#474), and every other method or a slice beside a moved part reads the whole container and traps (#149); with #151 (one evaluation) and #152 (an element read inside an expression). wolf-lang's gates pin 0.1.41's answers on fourteen cases (seven lists) by version; each is one of these, measured against its ruled cell by the lane that mirrored it.

The re-pin (r24; c4143cf). vendor/upstream/{spec,corpus} and the upstream gitlink move to ec56a08f, an ancestor of the released line (merge-base --is-ancestor 93a5fe50 ec56a08f holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 690 → 747 (57 new, none leaves), entries 646 → 702, members 44 → 45 (d9d71fe, 611b579); anchors 539 → 542 — mem.model.place.elem, mem.model.place.rhs, os.fs.path.domain, key sets diffed both ways, none dropped — and the coverage ratchet 264 → 268; the bundle 728/684 → 785/740 programs and records (7a7ac64). The fixture set gains s181's with-file-index.json, accepted and round-tripped (4617c65). Fifty-two of the fifty-six new entries reach run (88416d3): thirty-two match their check:, eighteen are dynamic counterparts of the compiler's E1001/E1002/E1014 (fourteen trap use-after-move, four exclusivity), and two are conservatism rows (elem_dyn_move_const_read, elem_dyn_mut_pair: a run-time index the compiler cannot prove distinct, whose value this machine sees); the three Pool rows are out of scope and s181's resolve/sibling_diag/main.lu matches fail(E0302) at resolve (its member, geometry/shapes.lu, holds the duplicate and is refused read alone, bc19fa8). s180's two index stores that take (index_store_take_{list,map}.lu) are dynamic counterparts, not resolve refusals as push(take …) is here (f3801c8).

Header reads, whole reads, element reads and one evaluation (is60; the maintainer's ruling of 2026-09-30 on wolffe-lang/wolf-lang#474 and #149: "len, count, is_empty are header reads … push, get, pop, slices and an impl method's self read the whole container").

Witnesses: tests/rulings_is60/ (82; s185's nine rows from wolf-lang ecaf655a among them), and path tests in src/eval/tests.rs and src/eval/place.rs.

A member read under an element claim reads the member (is59, wolffe-lang/wolf-lang#472, ruled A; [mem.model.place.elem] 1(c) at wolf-lang d3bd49cc: "an element is never the container's header"). bump(mut xs[0], xs.len) trapped exclusivity: a member that is not a stored field (xs.len, m.len) was read by reading its container, and the container met the claimed xs[0] as a prefix. Now, while a claim is held under the container's binding, the check is made on the member's own path (xs.len), which no element or key claim reaches; the rest of the read (the Moved trap, a freed region, provenance) is the container's as before. Both routes a member is read by take it — a dotted path (xs.len, b.xs.len) and a projected base (g[0].len, (xs).len) — on a List and a Map, under a call's claim or a borrow's (1709aec). A claim on the whole container, a prefix of it, or the element whose member is read still traps with the same record, clause and span as before, and a program with no claim on the binding reads exactly as it did. Witnesses: tests/rulings_is59/ (18, 0d45a6a; eg02's elem_member_read_after_mut.lu among them). Not carried: xs.count() and xs.is_empty() are method calls and still read the receiver whole (wolffe-lang/wolf-lang#474 asks for a ruling); an element read inside an expression still checks the whole container (#152); a member read's base operand runs three times (#151). wolf-lang's a_member_read_under_an_element_claim_stays_refused (at d3bd49cc) expects this machine's old trap for any lupin version, so it goes red against any lupin carrying this change until eg02b pins 0.1.41 as pre-mirror.

The four mirrors (is58; wolf 0.2.18's gates pin lupin 0.1.41's answer on three of them by version, so the release that carries this turns those rows to the ruled answer).

A nested index store evaluates each operand once (#145, f44b975; [mem.model.place.rhs], [mem.model.order]). g[i()][j()] = v ran i() twice, and a three-level store its first two operands twice: the store asked whether its base was a raw pointer by evaluating the base, then evaluated the base's operands again to find the place. The place is now found once and kept. Compound, take, a field between the indices, a map inside a list and a list inside a map all run each operand once, outermost first, then the value — wolf 0.2.18's bytes on ctl_store_order_nested_index.

A whole read of a place holding a moved part traps (#143, be3d4b5; [mem.tier0.move.2] under [mem.model.place.elem]). After move xs[0], copy xs, "{xs}", xs == …, let ys = xs, an argument in any mode, a return, a literal's field or element, and a match on it all handed the moved part on; after move p.x, so did let q = p. Each now traps use-after-move, naming the part and its move site; so does a for that binds an element holding a moved part, and a whole move. The base of a projection is not a whole read: xs.len, xs[1], p.y, a method receiver and a for head read only what they pick. The walk costs nothing until a program first moves a part.

A mut parameter left moved-out is moved-out in the caller (#146, c3af852; [mem.tier0.mode.mut], wolf-lang s184). The writeback carried the parameter's value and always marked the caller's place live, so var t = move xs in the callee left the caller reading its original [1]. The parameter slot's state now travels back with its value — through a free call, a trait-qualified call and an impl method's mut self — and the caller's read traps at the callee's move site. A parameter stored back, on the path the call took, runs as before.

else and ? read a Map value out (#144, e965adb; [mem.map.absent]: "A Map index READ copies exactly when V copies"). var v = m[k] else … copied a non-Copy value and left the entry live, so a later read of m[k] saw it. Under else and ? the read now consumes the entry as let r = m[k] always did — a Copy value is copied, anything else moves until a store through the key revives it — through a local, a field, an element of a list, or a mut parameter. An absent key moves nothing. An interpolation hole ("{m[k]}") still reads without moving, as the compiler reads it.

The nineteen corpus rows wolf 0.2.18's three gates name for these clauses run against their ruled lupin cell in tests/rulings_is58.rs; five flip.

CI is sharded on every OS (is57, #123). The linux and macOS test jobs ran unsharded against the 355-minute cap — the trunk run at 0.1.41's 0cfc0cf timed out on linux at 355.3 min. Both now run three test shards and a ladder-and-bundle job, as windows did (229d7b4): the shard key is a function of the target name alone (ci/test-shards.sh, 22617d6), and ci/assert-shard-coverage.sh holds every OS's shards to the declared targets, per test, with a planted-red self-test (eefff0e, f54c791). The per-test lists before and after are identical on ubuntu (1,261) and macOS (1,260) (19849e3, 38a0a7a); the PR run fell from 5 h 52 m to 2 h 55 m (run 36504583782, ci/is57.md).

0.1.41 — 2026-09-27

THE FORTY-FIRST (is56), the lupin half of wolf 0.2.18's point release. Pin unchanged: 93a5fe50 (wolf-lang v0.2.16 — the TAG). Eighteen commits since 0.1.40, one lane. It mirrors the read half of wolf-lang's [mem.model.place.elem] (eg00): a moved element is moved on every read path, which is the answer wolffe-lang/wolf-lang#460's witness needed from this machine. Not carried, and still open: a whole read of a container holding a moved part (#143), a Map read that copies a non-Copy value out (#144), a nested index store that evaluates its outer operands twice (#145) and a caller's read of a whole mut parameter its callee moved out (#146). wolf 0.2.18's gates pin 0.1.41's answers on #144, #145 and #146 by version, so the release that mirrors each one turns its row red by name.

A read of a moved element traps (is56, #141; [mem.tier0.move.2] under wolf-lang's [mem.model.place.elem], eg00). move xs[0] always marked that one slot moved, and a place read (let b = xs[0], xs[0].field) always trapped on it; but every read that picks an element out of a container VALUE handed the moved bytes on. let a = move xs[0]; print(xs[0]) printed 1 1. Now each of those reads traps use-after-move, naming the move site: the index read xs[i] and m[k] (9bc2f89); get, first, last, pop, par, Map.remove and Map.pairs (39782fe); a slice covering the element (08105f4); and a for over a list or map, at the iteration that reaches it (3ce773b). The slice and par trap lines name no element number (bda3ed7, D61): a 0-based position is not the number an origin-1 writer reads, and the move site already says which element it was. Reads of no element are untouched — xs.len, a push, a store, another element or key. eg00's fourteen witnesses run against their ruled lupin cell in tests/rulings_eg00.rs; five flip to the trap, among them wolf-lang#460's, where 0.1.40 printed 2 1 1. Not widened: a read of the WHOLE container or struct holding a moved part (copy xs, "{xs}", let q = p after move p.x) still does not look inside.

One corpus row filed, DIV-2026-026. memory/list_session_struct.lu moves tbl[2] with a plain let and iterates tbl: it traps here now, and its header's exit(0) holds on the compiler only through wolffe-lang/wolf-lang#460. Filed until the pin carries wolf-lang eg01's copy tbl[2] (057c99e, b444fd0).

diff-run retires a waiver only on a full-depth clean compare (6c22d42, ebc65ae). A counterparty tier that stopped short of the rung this machine reached — the default tier reaches run on no entry — used to count as "compared clean", and asked for DIV-2026-026's retirement.

0.1.40 — 2026-09-25

THE FORTIETH (is55). Pin unchanged: 93a5fe50 (wolf-lang v0.2.16 — the TAG). Sixteen commits since 0.1.39, one lane, and it mirrors two rulings the maintainer took on 2026-09-24 and wolf-lang's s182 wrote into the spec (wolf-lang 0468dead). No corpus row moves and no answer in the differential against the published wolf 0.2.16 moves; what moves is lupin's answer on seven of s182's ten ruled witnesses, each now the ruled cell. Measured on kasumi against trunk and head release builds, before and after the rebase onto 0.1.39 (docs/divergence-log.md, "The two mirrors III").

The index store follows push (wolf-lang#438, [mem.region.edge.elem], [gram.expr.assign]). xs[i] = v and m[k] = v copy a non-Copy place in and leave v live, as a plain push(v) does; xs[i] = take v moves it, the one store position an assignment admits a mode in. At 0.1.39 the plain store moved the place (a later (mut xs).push(4) trapped use-after-move) and the take store was E0201. take anywhere else in an assignment is still E0201, and a store through a field still moves (b8e54e0, 60b37fe, 53ec794). The compiler half is wolf-lang s180. A use after a take store is a static E1001 on the compiler and a dynamic trap(use-after-move) here, each machine's ruled cell — the resolve rung's move check keys on call-site markers and is not widened.

Confinement is resolved, not lexical (wolf-lang#386, [os.fs.path.domain]). A path is judged after .. is applied and symlinks are followed: sub/../x inside the tree is served (0.1.39 declined it by name for spelling a ..), and a symlink that carries a path out of the tree is declined by name, unsupported, never a row. A unix socket path takes the same resolved answer, because [os.fs.path.domain] makes a socket path and a file path one object (95cc274, cfea132, c00f6c7). Absolute paths and paths that climb out stay declined by name, as before.

lupin conform-run resolves where it is invoked ([os.fs.path]: "a relative path resolves against the process's working directory, on every tier"). The door observes ONE program per process, as wolf conform-run does, and until now took the private observation root that exists for walks observing many programs in one process — so a program reading a file its harness had put in the cwd, or writing through a link its setup made there, answered not_found about a directory it was never run in. That is the row s182 read as a symlink defect in 0.1.38: lupin run served the same program, and a plain file in the cwd with no link anywhere failed the same way. The corpus walk, diff-run, the conformance export, the explorer, the fuzzer and every in-process test keep the private root (4cc7fa4, 72a19ae, 16b7866); three tests/fs_family.rs tests that pinned the root at the CLI moved to the embedded door, where both of its properties are still asserted, and a fourth pins the cwd read.

The ten ruled witnesses land verbatim under tests/rulings/, each in its own directory with its expected.toml, and tests/rulings_s182.rs runs every one through the built binary's conform-run --json from its own directory and asserts lupin's ruled cell (cafaba5, feb7631). Red at the witnesses' commit on exactly the seven that flip; green at the head. Seven flip — index_store_copies_{list,map} trap → exit(0); index_store_take_{list,map} fail(E0201) → trap(use-after-move); fs_path_inside_after_dotdot unsupported → exit(0); fs_path_symlink_in row → exit(0); fs_path_symlink_out row → unsupported — and three hold: index_store_read_param exit(0), fs_path_absolute and fs_path_climbs_out unsupported.

What did not move. lupin corpus at 0.1.39 and at the head: byte-identical reports over the pinned corpus (646 entries, 44 members). diff-run against the published wolf 0.2.16 on the default, checked, native and release counterparties: 8 / 8 / 10 / 10 divergences on both sides, every report and conservatism ledger identical except the native tier's memory/unsafe_ub_uaf.lu, where the COUNTERPARTY's exit status after its own use-after-free read differs run to run; lupin's side is ub(mem.ub) in both. New divergences: zero. The ruled ones sit outside the corpus; the wolf-lang corpus carries s180's index-store witnesses from 0.2.17, which this release is the first to answer as ruled.

0.1.39 — 2026-09-24

THE THIRTY-NINTH (is54). Pin 2e4ca769 (wolf-lang v0.2.15) -> 93a5fe50 (wolf-lang v0.2.16 — the TAG); git merge-base --is-ancestor 93a5fe50 v0.2.16 answers 0. 666 -> 690 corpus files, 625 -> 646 entries, 41 -> 44 members, 524 -> 539 anchors (fifteen added, none dropped, key sets diffed both ways), coverage 256 -> 264.

The census, predicted before the pin moved and measured after. No pre-existing row moved (the two walks diffed row by row); nineteen of the twenty-one new rows were called right — memory/pool_place_write.lu is out of scope, not a match (a subscript on a pool reaches the map-index path), and rows/negative/error_alias_private/main.lu runs, a conservatism row, where E0304 was predicted. With this release's fixes exactly three rows move, the three predicted: 503 match, 28 dynamic counterpart, 53 conservatism, 61 out of scope, 1 filed mismatch, 504 reaching run.

A row is a use (wolf-interp#134, [type.err.alias.qualified], wolf-lang#434). use disk.IoErrors whose only mention is -> int ! IoErrors was E0305 "never used": the reference walk read a signature's success type and skipped its row, so the machine-applicable fix-it would have deleted a live import. Return rows, fallible parameter types, fn-typed parameters' rows and alias unions all count now, and an alias import nothing names is still E0305. rows/error_alias_qualified/main.lu moves from a mismatch to a match (7a50d0e).

The handles have type names, and a proc talks back (wolf-interp#130, [conc.proc.handle], [conc.proc.join], wolf-lang s170). Scope and Proc[T] are prelude type names: fn fan_out(s: Scope) and fn collect(p: Proc[int]) resolve, Scope[T] and a Proc without exactly one argument are E0401 at the written type (wolf 0.2.16's code and span), and scope/proc in type position stay E0206 with a help naming the capitalised spelling (78395fb). p.join() blocks until the proc exits and yields its value, each abnormal exit — error, killed, cancelled, fault — a payload-free row tag; it is the monitor's exit reason read synchronously, so a join after the exit answers at once (7dc4982). The site's census parting, conc/proc_join_param.lu, is a match, and so is conc/proc_join_value.lu.

The protocol mirror's tail (wolf-interp#129 items 2–5, wolf-lang s169). The record emits trap_message — a failing assert(cond, msg)'s own words — on that trap only, never compared (de020d2, 40c1e2b). --phase stops answer pass, never unsupported, short of the rung where a construct would be refused: measured, and pinned by a test that was green before any change (item 3 needed no code). differ::claim() no longer reads unsupported as a clean pass of its own rung, and pass against a run outcome is never a divergence ([proto.cmp.pass]; the published reading in compare moves with it) — which moves rows/negative/tag_undeclared_arg.lu, a pre-existing gating finding, into the conservatism ledger (f2e6912, c7a5076). And EXIT_REJECTED = 65 retires: lupin lex and lupin parse exit 2 on a rejection, as every front door does under [conf.exit.static] (12fd79a).

A file index on record diagnostics (wolf-lang#437, the lupin half, in the shape s181 fixed on the issue). The validator admits a top-level files table and a file index on each diagnostic and refuses them out of shape (31bdffd); the record emits them when a diagnostic lies outside the entry — package-relative, entry at index 0, every warning attributed, the file-walking module laws attributed — and is byte-identical otherwise (280d9d8–fd6004b); comparison resolves the index (fe0c445). Against wolf 0.2.16, which does not emit it yet, three sibling-file refusals now compare span-or-code: DIV-2026-025, open until the compiler half ships. The resolve checks that walk items rather than files still report entry-relative spans for a sibling's fault.

The linux archives start on glibc 2.35 (wolf-lang#447, the lupin half). 0.1.38's x86-64 and aarch64 archives were built on ubuntu-latest and imported GLIBC_2.39 through two weak symbols Rust std links when the build host has them (pidfd_spawnp, pidfd_getpid), so they did not start on Ubuntu 22.04. Both linux dist jobs build on ubuntu-22.04 / ubuntu-22.04-arm, and ci/glibc-floor.sh measures the shipped binary's highest GLIBC version against the 2.35 floor — in release.yml before upload and in a new dist floor CI job on every PR (6943e79, bbad776, e593963). The README states the floor.

Differential against the published wolf 0.2.16 (diff-run, default, checked and native): 8/8/10 divergences at 0.1.38 and at 0.1.39, three gone at every tier (proc_join_param, error_alias_qualified, tag_undeclared_arg) and three arriving (DIV-2026-025).

0.1.38 — 2026-09-21

THE RE-PIN (is53). Pin 41695e7 (wolf-lang s166, dev-stamped) -> 2e4ca769 (wolf-lang v0.2.15 — the TAG). 654 -> 666 corpus files, 613 -> 625 entries, 514 -> 524 anchors (ten added, none dropped, key sets diffed both ways).

0.1.37 could not be pinned by anybody, and this release is why it can be now. 0.1.37 declared its specification checkout as 41695e7, and git merge-base --is-ancestor 41695e7 v0.2.15 answers no — it is a branch head the rebase-merge landing s166 rewrote under other shas, surviving only because wolf-lang preserved it as refs/tags/lupin-0.1.37-conformance-pin. So no downstream had a truthful way to say "this runs wolf 0.2.15 and lupin 0.1.37" (wolf-interp#127; wolf-web hit it trying to bump three pins and shipped two). The new pin is the released tag itself, so the same command answers yes, reflexively, and the sentence has a referent.

wolf-lang's own 0.2.15 changelog records that a trunk re-pin was tried at the cut and reverted, for two reasons that were both this machine's: it materialized for i in a..b (3.91 GB peak RSS on a 50-million range), and its corpus runner could not parse s163's run(exit=nonzero). is52 fixed both against synthesized witnesses, because the corpus at the old pin had no file that exercised either. Both hold against the real files now: grammar/range_value_wide_iter.lu and grammar/range_header_inclusive_max.lu are matches at first sight, and conc/proc_link_root_death.lu — the first run(exit=nonzero) file this corpus has ever carried — is a match in the ledger. It also found the one site is52 missed: tests/run_corpus.rs keeps its own matcher beside ledger::judge's, and that copy had no Nonzero arm, so the file panicked expected exit=nonzero, observed exit(1) while the census called it a match. A duplicate matcher is only as good as its least-updated copy.

The census, predicted before the pin moved and measured after. 480 -> 491 match, 47 -> 48 static conservatism, 59 -> 59 out of scope, 26 -> 26 dynamic counterparts, 1 -> 1 filed mismatch, 476 -> 485 reaching run, 0 failures. Twelve entries arrive, none leaves, and no pre-existing entry changes class — which was the lane's falsifiable claim, checked by diffing the two corpus walks row by row rather than by comparing totals. The prediction called eleven of the twelve new rows right and missed memory/push_take_moves.lu: it was called conservatism on the reasoning that typecheck is a rung this machine does not perform, and it is a match, because the move analysis lives on resolve here and E1001 is answered there.

trap_message is accepted (wolf-interp#129, [proto.record.trap], wolf-lang s169). OPTIONAL_FIELDS gains a fourth entry, so wolfgang's records for a failing two-argument assert stop reading to this validator as malformed. The field is never compared — [conf.trap.assert] lets an implementation drop the message, and [proto.cmp.defined-divergence] names it — and the seal it widens still closes: a bare key that is not on the list is still refused with "implementation extensions must begin with x-". Both halves were seen red before the fix was trusted, each break planted in turn.

E0206 stopped being this implementation's choice. s163 landed corpus/grammar/type_position_keyword.lu, the file wolf-lang#320 asked for, so "a keyword where a type must begin" is corpus-pinned and comes out of diag::UNPINNED_CODES. Following the counterparty's number cost nothing: is45 had already moved assume noalias off E0206 to E0212 to keep it free.

DIV-2026-021 is RETIRED — the let-group locus row, open since is38. let a, b = 1, 2 is fail(E0201)@parse on both machines, and for eight releases they pointed ten bytes apart: this parser at the comma, the compiler at the end of the initializer list. wolf-lang#228 ruled and s163 moved the compiler onto the comma. Measured before the entry came out, not inferred from the corpus header that announces it: the counterparty answers span [511, 512] under conform-run --json --checked, byte-for-byte this machine's locus, and the differential runner's span-or-code line is present at the old pin and absent from all four tiers at the new one. differ::FILED_DIVERGENCES is one entry again. This machine never moved.

Two new differential findings are named and left un-triaged, because routing a finding is a ruling: memory/push_take_moves.lu (the same E1001 at a different rung, resolve here against mem) and conc/proc_link_root_death.lu (exit(1) here against exit(121), where [conc.proc.root] says the status is implementation-specified and [proto.cmp] compares it anyway). Four further gating findings pre-date this release and are recorded in docs/divergence-log.md for the same reason: CI's differential lane SKIPs without a counterparty, so nothing automatic can see any of them.

The bundle. programs/records 692/651 -> 704/663, moving together for the first time since is47 because every new corpus file is an entry; anchors_covered 248 -> 256; forward_tags holds at 109.

0.1.37 — 2026-09-16

THE METHOD SURFACE (is49, is50, is51). Pin a7f517e (wolf-lang v0.2.12) -> 30731a6 (v0.2.14) -> 41695e7 — two pin moves in one release, the first time this machine has taken two, and the reason is that three lanes landed behind one tag.

The pin this release declares is preserved by a tag. 41695e7 is an s166 branch head, and the rebase-merge that landed s166 on wolf-lang's trunk left it reachable from no branch and no tag — so the sha lupin --version prints could not be checked out of a fresh clone. It is now refs/tags/lupin-0.1.37-conformance-pin in wolf-lang, a tag rather than a branch because it is a fixed point. Re-pinning to a trunk sha was tried at the cut and reverted: at 12ca8acc this machine OOMs on grammar/range_value_wide_iter.lu and range_header_inclusive_max.lu (it materializes the range s161's #381 taught the checked machine to walk) and its corpus runner does not know s163's run(exit=nonzero). Those are this repository's mirrors to take, not the pin's fault.

What a reader can do at 0.1.37 that they could not at 0.1.36. Write xs.map(f), xs.filter(keep), xs.fold(init, step), xs.sorted() and the rest of the ten combinators, and have them run here — as the std wolf code they are, resolved to their home module and called as the free call, never as builtins this machine special-cases. Write xs.par(f) beside them and get map's answer. Write a list literal, name a range[int], alias an error set, match a bare dotted path like io.Eof, and open, create and remove real files and directories through std.fs. Three lanes, one surface.

is49 wrote the three clauses (list literals, range[int]/range[char], named error-set aliases) and took the pin to v0.2.14. is50 mirrored s157's and s160's clauses that is49 had ledgered and left, filled in the std.fs names that did not resolve, and closed all five of its issues. is51 took the pin to s166's tree and mirrored the method surface and the combinator set from the clause text, never from wolfc.

The ten combinators, and why the number is ten. [type.comb.set] names map, filter, fold, sum, sort_by, sorted_by, sorted, enumerate, zip and collect as std wolf code, with par an eleventh row that is the language's own builtin. sorted is the one worth saying out loud: it was written early, withheld while use std.cmp in std.list darkened every importer on the native rung, and carried in on a rebase once that cleared — so wolf-std's own changelog headline still says "nine functions" and calls sorted missing while its body records that sorted shipped. Ten is the number this machine mirrors and ten is the number the pairing below is derived for; a nine-row table drops sorted silently.

THE COMBINATORS (is51). Pin 30731a6 (wolf-lang v0.2.14) -> 41695e7 (wolf-lang s166, dev-stamped, rebased on trunk 4b56441): 615 -> 654 corpus files, 498 -> 514 anchors (sixteen added, none dropped, key sets diffed both ways). The subject is wolf-lang#390, ruled option 1 by the maintainer — par stays — and the clauses s166 wrote for it: [type.method] (methods on std data), [type.comb] (the combinator set), and [conc.task.par] completed with order, failure, captures, chunking, determinism and cost. Mirrored from the clause text, never from wolfc.

The method surface. recv.name(args) IS home.name(recv, args): List -> std.list, Map -> std.map, str -> std.str, range -> std.range, a closed table. Resolution is (1) the builtin methods of the type, (2) the home module's pub fn whose first parameter carries the receiver's type constructor, called as the FREE call — same instance, same code, so xs.any(p) and list.any(xs, p) are one call by construction — and (3) traits, unchanged. A home module needs no use and binds no name: it loads when some method call in the program names one of its pub fns, under a key no source can spell. The refusals are the counterparty's codes by name: E0301 with no std root configured (naming the home module and the ways to configure one), E0403 for no candidate or a first parameter the receiver does not fit (sum(xs: List[int]) on a List[str]), E0402 for arity counted against the written arguments, and E0804 where the spelled receiver mode disagrees with the candidate's first parameter — a mut candidate is (mut xs).sort_by(less) and the bare spelling traps exclusivity, as it does on a builtin. take is never a method name ([type.method.take]), and its refusal names xs[..n], xs[..min(n, xs.len)] and xs[n..]. Step (1) gains clear on List and Map. The combinators themselves are std wolf code this machine RUNS — none of them is a lupin builtin.

par. The one builtin combinator: xs.par(f) opens a scope, splits 0..n into k = min(n, W) contiguous chunks whose lengths differ by at most one, spawns a task per chunk, joins, and reads the slots back in index order — out[i] is f(xs[i]) whatever the schedule. W is fixed at 4 on every host (1 on wasm), because the scheduler runs one task at a time whatever W is, and a host-sized W would make the spawn events differ between two hosts replaying one seed. k == 1 runs on the calling task with no spawn and n == 0 spawns nothing, both permitted. A chunk stops at its first error value, the scheduler cancels its siblings, and the first failure in schedule order becomes the par's row — a failed par has no value. f is checked as a spawned closure's body (E1101), and W1101 does NOT ride along there: its text is a claim about one task's own copy and par has k of them, which is what the witnesses pin.

The census, measured on the rebased tree (trunk 92d98af, is50 underneath): 478 match, 60 out of scope, 26 dynamic counterparts, 48 static-conservatism, 1 mismatch — filed, zero unfiled — with 476 entries reaching run. DIV-2026-024 retires at is50, which is why the filed mismatch falls from two to one. Sixteen of s166's rows are new here: ten match and six are the refusals this machine makes by name at run time rather than at resolve. One prediction missed in each direction, both recorded in docs/divergence-log.md.

A lupin gap the new clauses exposed. [type.comb.set]'s collect is one generic function over range[T], and is49's [type.range.name] check refused that signature outright — T is neither int nor char — so every home module carrying collect was E0401 at its own declaration. The check now admits a RIGID generic parameter, exactly as [type.map.key]'s sibling check already did; range[bool] is still E0401 and bare range still E0405.

THE TWO MIRRORS (is50). No pin move: 30731a6 (wolf-lang v0.2.14), is49's. The subject is s157's and s160's clauses that is49 ledgered and left, wolf-interp#107 and #111, and three smaller issues: #112 (std.fs names that did not resolve), #110 (the fs tier's residue) and #102 (an imported trait alias).

A bare dotted path is a pattern ([gram.pat.nullary], #107). Color.Green => and io.Eof => parse as PatKind::Path and match a payload-less constructor by the payload form's own resolution rule (Machine::constructor_path_matches, shared by both). This covers or-patterns and the else |pat| handler position too. The E0201 "a dotted path in a pattern must carry a payload" is retired, and grammar/match_nullary_variant.lu runs exit(0), green\nfirst\n, so DIV-2026-024 and its waiver go. Arity stays the checker's question: a bare path that names the value's constructor while the value carries a payload declines by name citing E0808, with the payload spelled (Shape.Rgb(_, _, _)), rather than falling to a later arm.

take of a read parameter traps ([mem.tier0.mode.read], #107). eat(take b) where b is a read-mode parameter traps exclusivity at the argument, the row E1014's write half has trapped since 0.1.8. The barrier is the write barrier's (read_param_write), so a projection (take b.items) and a Copy element trap too, while a body-scope shadow (var b = copy b), a take-mode parameter and the plain move-out fn f(b: T) -> T { b } (wolf-lang#359's question, not this one) do not. Every shape was measured against wolf 0.2.14 --checked. memory/read_param_take.lu moves from conservatism to dynamic counterpart.

A declaration wins its own name, pinned ([conf.resolve.ambient], #107). Already right at 0.1.34. tests/resolve_ambient.rs pins the three shapes the corpus file does not: a sibling file's read_line called from the entry file, fn assert (declared and warned, but the call still reaches the primitive) and a declared time_now_ms. Each has the compiler's verdict, stdout and W0304 span.

The materializing str producers are region sites ([mem.region.escape], #111). upper, lower, repeat and replace now charge their answer to the ambient region at exact size and carry it as the value's home (builtin::produced_str). str_from_utf8 had charged since 0.1.21 but was never homed. A read after the region's wholesale free is the [mem.region.intra.2] fault, so memory/region_str_repeat_return.lu and region_str_from_utf8_return.lu trap region-fault (conservatism -> dynamic counterpart), as do repeat(0), an empty-needle replace, a produced str held by a binding outside its block, and one sent out of a proc's own region ([mem.region.proc]). The view side (trim, get, strip_*, the split/words/lines pieces) stays home-less. Projected reads already carried their place's region: a nested field, a tuple element and a list element are pinned beside the corpus's one field. Measured against wolf 0.2.14, the compiler's two tiers disagree on the charge: --native charges all five and trap(alloc-contract)s them under cap: 0, while --checked charges none of them (it does charge +). This machine follows the clause and the native tier; filed as wolf-lang#391. Whether a view of a region-BUILT str carries its receiver's sites is unstated, and all three lanes let it escape clean; filed as wolf-lang#392 instead of decided here.

net_writev_head ([os.net.writev.head], #111). The gather with a str head: the head enters net_writev's vector as one more part, and an empty one contributes none, so the wire, the park and the rows are net_writev's by construction. net/writev_head_gather.lu moves from out of scope to a match, and its degenerate shapes plus a multibyte head are byte-identical to both compiler tiers.

std.fs's missing names, and the rows they exposed (#112). fs_create_dir (one level: exists over anything already there, not_found under a missing parent), fs_remove_dir (one empty directory: io for a non-empty one or a file) and fs_write_chunk (the handle's byte write) are in the tier. The issue's fourth name, fs_rename_atomic, is no builtin on either machine (E0301 on wolf 0.2.14; std.fs names it only to say it does not exist). Reading the three rows off the compiler's own E0602 showed that five of this machine's declared fs rows had drifted: fs_create has no not_found, fs_create_dir_all is {denied, io}, fs_read_dir carries utf8, and fs_rename carries cross_device and exists. It was observable: a handler's cross_device => arm was a binding and caught a plain io. The rows are now the compiler's. fs_answer coarsens any tag outside a call's declared row to io, so a host error can never reach a handler as an undeclared tag again, and fs_rename keeps its two refusals by kind. Recorded on wolf-lang#181. wolf-std's four dark rows (fs/chunk_stream, directories_and_metadata, exists_row, fstat_rows) run with the expected verdicts under this binary, and the other thirteen rows are unchanged.

The fs residue (#110). fs_write_bytes resolves its path before it reads its payload, so a refused path wins over invalid. A handle read retries Interrupted (fs::read_retrying). A listing holding a non-UTF-8 name is the utf8 row fs_read_dir declares, for the whole call (linux-only test). The run-varying os_cwd under observation and the consumed-then-charged handle read are behaviour kept on purpose, stated in the approximation contract §6.19.

An imported trait alias expands its bound (#102). sema::expand_bound reads a qualified bound (tiny.Num) through the alias table of the module its head names, and the alias's own list in that module, so an alias chain behind a renamed import works too. wolf-std's tests/ops/num_alias_tier.lu (Num = Add + Sub + Mul + Div + Rem + cmp.Eq + cmp.Ord, ledgered mirror-lag(E0501)) runs exit(0) with the compiler's stdout sha256, and wolf-std's ledger goes red on it, as designed. The cost was one function.

Census, predicted before the first edit and measured after the last (lupin corpus). Baseline 790c127: 460 match / 54 out of scope / 2 mismatch (both filed) / 23 dynamic counterpart / 41 conservatism / 449 reach run. Predicted and measured: 462 / 53 / 1 / 26 / 38 / 451, every cell, the five rows by name. The compiler's table at wolf-lang v0.2.14, re-derived against the 790c127 binary as control, is 351/148/0/107/8 checked and 381/148/0/77/8 native (agreements / completeness / soundness / unsupported / hard), coverage B 451 and BOTH 331/361. That is five ledger counts moved per tier, all -> agreement, and hard is now the eight #167 warning-parity rows alone. Every cell was predicted before the first edit.

THE THREE CLAUSES (is49). Pin a7f517e (wolf-lang v0.2.12) -> 30731a6 (wolf-lang v0.2.14), two releases in one bump: 580 -> 615 corpus files, 475 -> 498 anchors (twenty-three added, none dropped, key sets diffed both ways). The subject is wolf-interp#106, s158's mirror — three surface additions the compiler carried at 0.2.14 and this machine refused by E0201 at the first new token: list literals ([gram.expr.list], [type.list.lit]), a nameable range[int] / range[char] with start / end ([type.range]), and transparent error-set aliases ([gram.item.error], [type.err.alias]). Eighteen witnesses; the eighteenth, rows/error_alias_ident.lu, needed no mirror at all.

List literals. A [ that BEGINS a primary opens a literal; a [ that follows an expression is index_args, as it always was — position decides and nothing else does, so [10, 20, 30][1] is a literal indexed with no lookahead (grammar/list_lit_index.lu). The value is a fresh List[T] allocated into the ambient region, elements evaluated left to right exactly once each before the list exists, and the ledger agrees with the spelling the clause names: the container is charged as List() charges it and each element charges the growth push would have, at the same stride, to the same birth region — pinned by a test that reads region_bytes under both spellings. The static half, sema::list_lit_check, is a ScalarWalk-shaped pass over literal SHAPES: the first element that does not fit is E0401 at that element with the first element as the "because" (grammar/list_lit_mixed.lu, byte-identical span with the compiler, [368,373]); an annotated let/var pushes its element type into every element and a non-List annotation is E0401 at the literal; a bare let xs = [] is E0419 at the [] with the annotation spelled for that binding (grammar/list_lit_untyped_empty.lu, [409,411] on both machines). A call argument, a field, a declared return supply a context this pass does not check — the permissive direction, the sema boundary's rule.

range[int] as a type — and the claim that was false. range joins sema::PRELUDE_TYPE_NAMES (type position only, so var range = true is not a W0304 hazard; pinned), closed at the two element types: bare range is E0405, range[f64] is E0401 at the argument. .start/.end are two arms of builtin::property. wolf-interp#106 says "lupin already does this — Value::Range has been built half-open since its first range arm". It had not: at 0.1.36 Value::Range carried inclusive: bool to eval_for, which computed if inclusive { end } else { end - 1 } on raw i128, and 0..=int.MAX as a value neither trapped nor normalized. Now a..=b normalizes at construction to end = b + 1 under Machine::checked, so (2..=7).end is 8 and let big = 0..=9223372036854775807 traps overflow at the let (grammar/range_type_overflow.lu, 21:15) — and the for HEADER form is unchanged, a non-materializing counted walk to the inclusive bound, so for i in 9223372036854775805..=9223372036854775807 still iterates three times ([type.range.value]). Value::Range carries elem: RangeElem (Int(IntTy) | Char) and no flag; 'a'..='d' steps the scalar value and range_type_char.lu prints a d e. Recorded on #106.

Error-set aliases are spellings, and the spelling is gone before anything reads a row. error IoErrors = {none, parse} is an item; error is CONTEXTUAL on three tokens (Ident("error"), IDENT, =), so a binding, a function, a field and an assignment target named error parse as they always did (lex::CONTEXTUAL 12 -> 13, §6.2 at the pin names it). A postfix row position admits a bare path — -> int ! IoErrors is -> int ! {IoErrors} — and ! IDENT continues a type the way ! { does. The semantics are one function: sema::expand_error_aliases runs on every unit of a module after all are parsed and before any is collected, and REWRITES every error row through the module's aliases — signatures, fields, annotations, casts, closure parameters, index type arguments — so the raise-tag vocabulary, ?'s widening, the else handler coverage, the lints and every rendered type work on tags as they always have ([type.err.alias.transparent], [type.err.alias.diag]). Aliases compose by naming and flatten by [gram.type.row.flatten]'s rule, a repeated tag one tag (rows/error_alias_union.lu); an alias entry with a payload is E0601; a cycle is E0610, once, at the ENTRY that closes it with the loop named (rows/negative/error_alias_cycle.lu, [434,435] — the compiler's own locus); an open row in an alias is E0201 at the .., in the parser, where the corpus's phase: lex puts it ([368,370]). The clause's prose says "E0515" for the cycle; the registry, docs/diagnostics.md, the compiler and the witness say E0610, which is the code here — the nit is filed upstream.

Census, predicted before the first edit and measured after the last. Baseline re-measured with the 0.1.36 binary at the NEW pin: 443 match, 54 out of scope, 19 mismatch (18 unfiled: the fourteen positives, the three negatives answering E0201 for the wrong reason, and match_nullary_variant), 23 dynamic counterparts, 41 conservatism, 435 reaching run. At the head: 460 match, 54 out of scope, 0 unfiled mismatches (2 filed), 23, 41, 449 reaching run — six of seven cells as predicted, the seventh (449, not 448) an arithmetic slip in the prediction, not a row the census surprised. The rows this lane leaves, by name: grammar/match_nullary_variant.lu (s157's [gram.pat.nullary], filed as DIV-2026-024 against wolf-interp#107 and waived — is50's), net/writev_head_gather.lu (net_writev_head declined by name, #111), memory/region_str_{repeat,from_utf8}_return.lu and memory/read_param_take.lu (static refusals this machine runs clean — conservatism, #111). The compiler's table, re-derived at the head (cargo xtask differ with --control the 0.1.36 release binary, wolf 0.2.14 at 30731a6, both tiers): agreements 332 -> 346 checked and 362 -> 376 native, hard 23 -> 9 on both (8 Diag warning-parity rows + 1 Verdict, match_nullary_variant.lu), completeness 151 and unsupported 108/78 unmoved, coverage B 435 -> 449 and BOTH 315 -> 329 / 345 -> 359 — THE INTERPRETER BUMP MOVED 14 LEDGER COUNT(S), the fourteen files named, three fail-pinned rows moved below the ledger to their right codes. Every cell as predicted, on both tiers; r20 predicts against these.

0.1.36 — 2026-09-12

THE FS TIER (is48). Pin unchanged at a7f517e (wolf-lang v0.2.12); this release adds no spec and no corpus, only an implementation this machine had declined since s38.

The ruling. lupin has answered the whole io/fs family with "this machine has no filesystem by design" for every release up to 0.1.35 — a reading of [proto.cmp.defined-divergence] that put the HOST's filesystem outside the comparison surface. Asked whether that meant file input and output would never be possible in the reference implementation, the maintainer ruled otherwise (BACKLOG B16): build the tier. Real files, no mocks, written from spec/11-os.md §6 and the witnesses under corpus/fs/, never from wolf_rt::fs.

eval::fs implements [os.fs]. Twenty-two names, three of which are one call: fs_open is mode 0 and fs_create is mode 1, and fs_open_mode takes the caller's own out of a set of SIX — 0 read, 1 write (create, truncate), 2 append (create), 3 read-write (create, no truncate), 4 create-new (exclusive), 5 read non-blocking. A mode outside the set is invalid decided before the filesystem is touched, which is the clause's own ordering and is tested as such: fs_open_mode("no-such-dir/x", 99) is invalid and not not_found. Handles are 1-based and never reused, so 0, a negative and a closed handle are all the io row — corpus/fs/fstat.lu's closed_is_io and forged_is_io. fs_fstat answers [kind, size, modified_ms] off ONE metadata read on the handle, in fs_size's and fs_modified_ms's units so the handle's answer and the path's compare without a conversion. fs_read_dir SORTS, bytewise, which is what makes a listing program's stdout the same on ext4, APFS and NTFS.

wolf-interp#86's mode-5 half closes for real. Mode 5 is a read open that cannot park: on unix it carries O_NONBLOCK, and on a regular file it is mode 0 in every respect — same handle, same bytes, same fs_fstat — which is the parity corpus/fs/open_nonblock.lu exists to pin. There is no libc dependency here and unsafe_code is forbid, so the flag is a written-down constant per host; it is pinned BY BEHAVIOUR rather than by inspection, by a test that opens a REAL fifo with no writer and fails on a timeout if the open parks. windows has no equivalent on this path and serves mode 5 as mode 0, by name, exactly as the clause says.

Containment: the one thing the old posture got right, kept. A path that is absolute or climbs out of the working directory is refused BY NAME (unsupported), never by a row — a row would be a claim about the host. The compiled lane does not contain paths this way (probed), so this is a stated narrowing and not an accident: the corpus walk, the differ, the explorer and the fuzzer all run corpus programs in-process, and a path that climbs out is the one bug in this tree that could damage the machine it runs on.

Six bugs found by reviewing the branch against itself, all fixed here. The private observation root paid for most of them: the three predicates asserted away a row that resolution really can answer, so a file planted at $TMPDIR/wolf-obs turned every observed run into a PANIC; a root that could not be built collapsed into the live sentinel and bound a unix socket in the user's real directory; the root's name was predictable in a world-writable directory, so a planted symlink redirected every write (the leaf is claimed with create_dir now, which fails on a symlink, and carries a clock reading); a failure between making the root and making its target/ leaked a directory nothing would remove; path_row produced an exists row that no path call declares, so fs_create_dir_all over a FILE handed a program a tag its own handler could not match (the compiled lane answers io, and so does this one now); and the REPL was treated as an observation, so a session's files were written to a temp root and deleted when it ended — stdout pass-through and "this is somebody's real directory" are different properties and now have different flags.

Windows device names are refused by name. NUL, CON, AUX, PRN, COM1-COM9 and LPT1-LPT9 are ordinary components to a path parser and DEVICES to windows wherever they appear, so the lexical containment check had to name them. Refused on every host, because the corpus is shared across the matrix; console.txt, communication.log, COM0 and COM10 stay admitted.

Two spec gaps filed, not guessed at (wolf-lang#365). [proto.cmp.triage] makes the clause the defendant first, and §6 is silent twice over: it never says whether an fs_* path may be absolute or climb out of the working directory (this machine refuses both by name; wolf 0.2.12 admits both, probed), and it gives fs_remove a row set without saying which row a DIRECTORY takes (the lanes agree today only because both delegate to the same standard library — denied on macOS, io on linux). Both readings are on the issue.

read_line does NOT land. Stdin is not a file, no pinned clause names an injectable one, and nothing in the whole corpus calls it. The name still resolves, so the refusal reads "unsupported feature" and never "unknown name" — the same reason the fs names resolved before they worked.

A handle-level failure is io on every host. [os.fs.fstat] says "on a handle the hosts answer io for nearly everything, the entry being already resolved", and this release takes that literally instead of forwarding the host's error kind. CI is what found it: reading an APPEND handle is EBADF on macOS and ERROR_ACCESS_DENIED on windows, so forwarding the kind made one program answer io on one host and denied on the other — a portability hole in a row set programs branch on. The compiled lane answers io here too (probed at this pin: a write to a read handle, a read from a write handle and a closed handle are all io), so the uniform row is the compatible reading as well as the clause's. The PATH calls keep their kinds, where not_found and denied are the answer rather than noise.

A unix socket lands in the observed program's own directory. A socket path is a filesystem object, so net_listen_unix resolves it exactly where fs_exists looks. corpus/net/unix_echo.lu is why this had to be fixed rather than noted: the witness sweeps a stale socket with fs_remove, binds it, and ends with cleaned = !fs_exists(path). With the two families disagreeing, that sweep swept a path nothing bound, cleaned was vacuously true — the witness passed while checking nothing — and a stale socket in the real directory would have failed the bind at random.

Rows probed rather than guessed. A read past end-of-file is the eof row; fs_read_text over bytes that are not UTF-8 is utf8, never lossy and never a trap; an append handle does not read (io) and a create-new handle does. No corpus file reads twice, so eof is invisible to the census and is pinned by unit tests instead — [os.fs.open]'s mode-5 prose is the clause that names it.

An observed program gets its own project root, and the tier is why. Corpus programs write REAL files at paths of their own choosing, and this crate runs many programs at once by design: several harnesses walk the whole corpus, cargo runs test binaries in parallel on top, and export::export runs a full walk inside any of them. Three symptoms, all measured: fs/fstat.lu read size=0 off a file another thread had just truncated, two concurrent exports of one corpus appended to one log.txt twice (log=one|one|twotwo size=14), and a bundle's sha256 stopped being reproducible.

So an OBSERVED program's relative paths resolve against a private, empty project root — made once per observation, removed when it ends, and given the one target/ directory that makes it a project root, because the witnesses write there without creating it and both lanes run from a directory that has one. A live lupin run is untouched: a user's program writes in the user's own directory, exactly as wolf run does, and os_cwd answers whichever of the two applies so a program's world is never split.

Ordering was tried first and was the wrong answer. One advisory lock per program does serialise correctly — but memory/byte_producers_ledger.lu takes 74 seconds in a debug build, and serialising it across every walker would have added something like twenty minutes to a CI run already near three hours. The private root removes the contention rather than scheduling it: no waiting, no lock files, reproducible BY CONSTRUCTION rather than by every harness remembering to take a lock. It also stops the corpus walk littering the repository's own target/ with witness scratch files.

Census, predicted before the first edit and measured after. The nine files that were out of scope for the fs tier and nothing else:

| class | baseline (0.1.35) | predicted | measured | | --- | --- | --- | --- | | match | 423 | 432 | 432 | | out of scope | 62 | 53 | 53 | | mismatch | 1 | 1 | 1 | | dynamic counterpart | 21 | 21 | 21 | | conservatism | 38 | 38 | 38 | | reach run | 412 | 421 | 421 |

Six of six exact, including the one figure the prediction flagged as uncertain (whether net/unix_echo.lu, which failed at run rather than at resolve, was already inside the 412 — it was not). The files: fs/bytes_dirs.lu, fs/error_row.lu, fs/fstat.lu, fs/open_nonblock.lu, fs/roundtrip.lu, memory/byte_producers_ledger.lu, net/unix_echo.lu, projects/count.lu, projects/count_dir.lu. The row that was in doubt was memory/byte_producers_ledger.lu, because it pins a REGION-accounting relation and not an fs one: read_tight holds only because fs_read_bytes and fs_read_chunk mint their buffer at exact capacity through region::ledger::byte_buffer_bytes, the way s.bytes() and the net byte reader already did.

Two corrections to the record, found while measuring.

1. 0.1.35's census line was one file stale. It reads "413 -> 422 match … 62 -> 63 out of scope" and asserts "every number above is the pin's, measured before a line was edited and unchanged after". It was not unchanged after: is47's own E0413 mirror moved typecheck/interp_spec_on_union.lu from out-of-scope to match, so the 0.1.35 binary answers 423 match / 62 out of scope, not 422 / 63. The figures in the is47 table in docs/divergence-log.md are correct as labelled — they say "measured with the 0.1.34 binary at the new pin" — and it is only the "unchanged after" sentence that was wrong. The baseline column above is the re-measured one. 2. The sprint's premise did not survive measurement. is48 was written expecting an unsupported count of "87-114 rows, most of them fs/net". Measured at a7f517e it is 62, of which fs, net, os and ffi together are 16; the largest single group is comptime at 21. The tier is worth building on the ruling; it was not worth building on that arithmetic, and the number is written down here so no later lane re-derives it.

Pairing: the pin is unchanged, so the compiler's PAIRING at a7f517e still names lupin 0.1.34. The rows this tier moves are listed above by name, for the next compiler release lane to predict against.

0.1.35 — 2026-09-11

THE REMAINDER (is47). Pin c9237c1 -> a7f517e — the v0.2.12 tag. The delta is two sprints and a release lane: s154 (nine papercuts — E0409 on either operand side, W1002 standing down beside a mode error, a !T tail in a unit context as W0601, W0318 over a swallowed term, E0410 on a let binding's fields, the one-line body-less trait member, and [gram.inv.ctx] naming then), s156 (eleven — fmt's break selection and fallback parens, % on a float as fmod, the checked tier's converting casts, an operator's trait being the one reachable, a container-index store lending, E0413 on a format spec over a !T hole, str.find by needle length, and the two stale seed headers re-pinned) and r17. Ten corpus files join (536 -> 545 entries, 34 -> 35 members, 570 -> 580 files) and four anchors (471 -> 475; key sets diffed BOTH ways — four arrive, nothing drops, no owner changes): [gram.fmt.break], [gram.fmt.paren], [mem.region.edge.elem], [type.float.rem]. No new namespace — all four sit under gram/mem/type and spec/05 is untouched in the range, so anchor::REGISTERED_NAMESPACES holds at thirteen.

then joins the contextual table, and the table is held to §6.2 BOTH ways (wolf-interp#100). §6.2 now reads "… self (receiver), then (after a complete if condition, [gram.expr.if]), …", so lex::CONTEXTUAL goes to twelve in the same change as the pin that carries the sentence. Nothing in the parser moves: then was matched by spelling in the one position after a complete condition from is45, which is what "contextual" means. The test that was supposed to make this impossible to get wrong only ran ONE way — over lex::CONTEXTUAL, asserting §6.2 names each entry — so a word the CLAUSE names and this machine does not list, then's exact shape for two pins, asserted nothing. The reverse loop lands with the two rules that keep a mentioned word out: a reserved keyword (assume, if, in — lex::KEYWORDS decides the token kind), or reg, which no production tests for. #100's other half is a guard nothing in this tree pinned either: a body-less signature followed by another declaration on one line is still E0201 at the fn.

free_names collects a binder it never collected (wolf-interp#45). A match fell to walk_child_exprs, so an arm's OWN binding read as a FREE name and a closure that captured nothing could draw W1102 on a later write to the outer name. The for element and the else handler's binder had the identical hole; all three land as one rule. The #44 tag-arm judgement is SHARED rather than restated — Walk::arm_is_tag_pattern becomes lint::pattern_is_tag, which both walks call — and the else binder is what makes it answerable here, carrying an empty declared row exactly as Walk records one when it cannot see a signature. eval calls free_names twice and is fixed by the same change: the nested-fn capture probe REFUSED a program outright for a "capture" that was a match arm's binding. Five witnesses, since no corpus file exercises any of the three shapes, and two of them are controls in the other direction.

The declared-scalar check learns char (wolf-interp#61). [type.char] and [type.byte] state one rule in two documents — a width-bearing scalar "adopts no numeric literal, in every position" — and is37 paid the byte half as a byte-only lattice, deliberately, naming char's false-positive surface as the reason not to smuggle it in. ByteTy becomes ScalarTy with Char and ListChar; 'a' is a char the way 65 is an int; and the rule reads is_width_bearing rather than naming one type, so is37's two guards are generalised rather than joined by a third sibling. Eight programs, each measured against wolf 0.2.12 conform-run --json --checked, and lupin answers every one with the SAME CODE AT THE SAME SPAN: a char parameter handed 65 [77,79], a -> char returning 65 [21,23], var c = 'a'; c = 65 [44,46] — the one that produced a WRONG ANSWER rather than merely accepting a rejected program, silently retyping a live variable — plus a char in an int slot, the two width-bearing scalars against each other in both directions, a List[char] element and a mixed comparison. Four rows of char_is_not_an_integer move from unsupported to fail(E0401); the two + rows stay dynamic, because [type.byte.op]'s widening rule is about byte and this pass will not invent one [type.char] does not state. The false-positive surface, measured over the whole pinned corpus: zero.

Two refusals name the way forward (wolf-lang#158, B28). when with one operand said "call the method on the sync type" and Mutex has no such method; it states the rule now, as wolfc's own message does. for c in "abc" said "for cannot iterate str" and named no way forward; it names chars(), words() and lines() now, and the test runs all three.

B28's three row classes mirrored as wolf-interp#103 (four programs the compiler refuses statically and this machine runs — if-without-else as a value, call-site unification, the element binding in a loop, an omitted result), #104 (the rendering rows) and #105 (the observability surface, B29 folded in). Two rows of #153 HEALED and are recorded rather than filed: the captured-var write now answers E1101/W1101/W1102 byte-identically to wolf 0.2.12, and the element binding outside a loop runs on both machines. #158's byte-offset row healed too — line:col has arrived, the excerpt has not.

Two divergences retire on the pin. wolf-lang#341 re-pinned both seed headers: wordcount.lu teaches tally[w] = (tally[w] else 0) + 1 and grammar/structlit_paren.lu binds its parenthesized literal instead of comparing it. DIV-2026-022 and DIV-2026-023 both match at first sight and leave differ::FILED_DIVERGENCES.

Census 536 -> 545 entries, 404 -> 412 reach run, 413 -> 422 match; 20 -> 21 dynamic counterparts, 38 conservatism, 62 -> 63 out of scope. Mismatches 3 -> 1: DIV-2026-019 alone. The sprint's own three issues are corpus-neutral by construction — no corpus file exercises a match-arm capture, a declared-char boundary or either refusal message — so every number above is the pin's, measured before a line was edited and unchanged after.

Pairing: this release declares v0.2.12; the compiler's PAIRING at a7f517e names lupin 0.1.34, which r17 closed for the previous release — the next release lane closes this one from the other side.

0.1.34 — 2026-09-11

THE MAP MIRROR (is46). Pin 662b14c -> c9237c1 — the v0.2.11 tag. The delta is the five sprints 0.1.33's pin left past its tag: s149 ([os.fs.open] mode 5, the accept posture), s150 ([type.fn.value], [abi.native.closure], [conc.chan.payload]), s151 ([gram.expr.if] — mirrored at 0.1.33, its twelve witnesses arrive now and match at first sight), s152 ([type.map], [type.map.key], [mem.map.absent]), s153 ([exec.checked.budget], [mem.region.escape]) and s155 ([type.trait.op], [type.trait.op.alias]). Thirty-six corpus files (536 entries), thirteen anchors (471; key sets diffed BOTH ways, nothing dropped), and a new namespace: exec joins anchor::REGISTERED_NAMESPACES in the same change as the clause's list ([conf.anchor.ns.admit]) — without it the walk refused strings/bytes_view_walk.lu, the one thing the pre-bump prediction missed. s154 is past the tag: #99 is mirrored from the trunk clause the way is45 mirrored s151, and then stays out of lex::CONTEXTUAL until §6.2 names it at a pin. Pairing: this release declares v0.2.11; the compiler's PAIRING still names lupin 0.1.32 — the next release lane closes that from the other side.

A trait reached through its module dispatches (wolf-interp#96), and a qualified trait in a bound is a use of its import (#97). tiny.Eq.eq(a, b) after use std.tiny — three segments, the head a module and the middle a pub trait in it — fell to the path evaluator and was unsupported at resolve while the byte-identical trait in the entry file dispatched: one trait, two verdicts, decided by which FILE declared it. The trait-qualified call takes the module road too. And fn total[T: ops.Add] with the bound the file's only mention of ops was E0305 "never used", with a fix-it that deleted the line the bound resolves through; the ref walk visits fn, trait and impl generics now. Both witnesses need a staged module and live in tests/std_root.rs; together they unblock wolf-std's [K: Eq] five and every ops impl.

The key protocol (wolf-interp#91, s152). A Map key was its RENDERING: an int key projected as an ordinal index and "did not denote a place" on squares[i] = …; a char or bool key rendered on the way in and compared against the stored value on the way out, so every read missed. MapKey carries the four [type.map.key] admits, compared by value. m[k] on an absent key answers the none row — never (), the one unchecked, untyped read the language had — and the compound path's absent-to-zero default goes with it: m[k] op= v is E0417 at the whole indexed place, decided from the declaration that made m a Map (an initializer, an annotation, a parameter), and refused in the same voice at run time where sema could not name the binding; a key outside str/int/char/bool is E0418 at the key type, in a signature and in Map[K, V]() alike, a rigid K clean. Five witnesses print the pinned bytes; the two refusals are the compiler's spans, [786,801] and [648,653].

The operator bridge (wolf-interp#92, s155). a + b IS Add.add(a, b) when a is a struct; == is Eq.eq and != its negation — an inverting impl is consulted, nothing synthesized (D49), so a struct's == no longer compares structurally; the ordering family reads Ord.cmp against Less/Greater and <=> is the Ordering itself; prefix - is Neg.neg. Never two primitives, which is also what [T: Num] at int runs. The static half is decided from declarations as the other read-back refusals are: a parameter typed as a bare generic carries its name, and the bound (alias-expanded, a cycle cut) must name the table's trait — E0501 at the operand, "the bounds on T say nothing about +"; a struct's operator needs the trait in scope by name (E0301), an impl for the struct (E0502) and the homogeneous shape (E0514), every span the left operand's — golden_arith, golden_eq, op_eq_no_trait, op_missing_impl and op_hetero_add answer the compiler's codes at its bytes, and so does the golden rule's call shape, Show.show(v) under a bare T (golden_missing_bound, E0501 at the argument). The alias form parses: trait Num = Add + Sub + … + Ord is a memberless TraitDef carrying its bound ([gram.item.trait]); its trait heads count as uses. An enum value with an impl dispatches; one without keeps the structural tag comparison, the conservative side.

A built str has a region (wolf-interp#88, s153 / wolf-lang#310). Value::Str is a newtype: the text and an optional home, equal and ordered by the text alone. s + u, s += u and a holed interpolation are charged to the ambient region at the building expression — the site rule a struct literal follows — and carry it out as their home; a literal, a slice and every view product carry none. A region block whose OWN value lives in the region it frees faults at the } (region_str_concat_return.lu printed regions from freed bytes through 0.1.33); a binding whose str died with its region faults at the read (region_str_concat_send.lu, at got) — E1010's dynamic counterpart, both. The charge is the checked machine's, measured: print("{n}") under cap: 0 is trap(alloc-contract) on wolf --checked at c9237c1 and runs on --native.

Four small mirrors. A struct literal missing a field is E0408 over the literal (#94); an unresolved bound name is E0301 at the name (#95), checked before the bridge reads bounds so it is never a later E0501; a temporary cast to dyn is E0810 over the cast (#98 — wollf's wh-002, and traits/dyn_temp_refused.lu leaves the conservatism ledger for a match); a field write through a let binding is E0410 at the place, one level down too, an index left alone (#99, s154's clause from trunk). Every span the compiler's on its reduction. #100: the body-less member followed by another on one line is still E0201, checked; then waits for the pin that carries §6.2.

Two seed files filed upstream (wolf-lang#341). wordcount.lu's tally[w] += 1 is E0417 under s152 and grammar/structlit_paren.lu's struct == is E0301 under s155; both headers still pin the pre-clause verdicts, and the compiler's ledger stops one rung short of seeing it. DIV-2026-022 and DIV-2026-023, waived; both leave RUN_LEDGER, the CLI's exit(2) probe stands on its own program, tests/s151/ retires into the corpus.

Census 500 -> 536 entries, 383 -> 404 reach run, 380 -> 413 match; 16 -> 20 dynamic counterparts, 42 -> 38 conservatism, 61 -> 62 out of scope. Mismatches 1 -> 5 (the pin, measured before a line was edited, every class and every witness as predicted) -> 3: DIV-2026-019, -022, -023.

0.1.33 — 2026-09-11

THE MIRROR TAKES then (is45). Pin e0ce018 -> 662b14c — the v0.2.10 tag, closing the gap 0.1.32 left at a dev stamp 22 commits short of it. The delta is s148: [type.fn.ret], [type.row.operand] and [mem.str.imm], five corpus files (500 entries), five anchors (458; key sets diffed BOTH ways, nothing dropped). s149, s150 and s151 are all past the tag and wait for the next pin; the clause this release mirrors was read from wolf-lang trunk f608487.

The one-line if (wolf-interp#90, wolf-lang#307/s151, [gram.expr.if]). if c then a else b: then closes the condition and each branch is one expression; the if's own else binds first inside a bare branch, so a defaulting else there is written in parentheses ([gram.amb.else]); the two branches of one if share a form and an else if picks its own; then before a block is optional and dropped; a condition followed by neither { nor then is E0201 naming both spellings; a let in a bare branch is E0201 offering the brace. then is contextual — matched by spelling in the one position after a complete condition, so a bool named then is a condition and less.then(greater) is a member — and it is NOT in lex::CONTEXTUAL, because §6.2 does not name it (wolf-lang#318). A bare branch is a brace-less block holding its one expression, so sema, lint and the evaluator are untouched. s151's twelve witnesses live under tests/s151/ byte-identical to trunk until the pin that carries them: nine run to the pinned stdout, three refuse E0201 at the compiler's own spans.

A comparison on a bare row is E0409 on either side (wolf-interp#85, [type.row.operand]). is43 read the rule off [type.interp.union]'s carve-out and answered E0401 for a comparison; s148 wrote the rule down and ruled one code for every operator with a !T operand. The pin's one new mismatch, rows/negative/row_operand_compare.lu, matches. The second half: s[a..b] = v on a str is E0416 at the place ([mem.str.imm]), decided from the declaration that made s a str; typecheck/str_slice_assign.lu leaves the out-of-scope ledger for a match. s[i] = v stays E0411 first (D25).

One type parameter, one type per call (wolf-interp#84). fn sum_areas[T: Area](a: T, b: T) called with a Rect and a Square ran here to exit 58 and is refused by wolf 0.2.9 — the compiler was right. The declarations-read-back walk now names an argument's struct (a literal, an annotation, a parameter, nothing else) and refuses the second binding of one bare T with E0401 at that argument, wolf's sentence and span ([464,465] on wollf's wh-001). No clause states the rule: filed as wolf-lang#319 with the witness.

A type argument in expression position resolves (wolf-interp#89). List[Nonesuch]() constructed a list; on List, Map, Set and Option the brackets are generic application ([gram.amb.brackets]) and each argument is a type name checked against the annotation check's scope plus the declaration's generics and its impl block's — E0301 at the name. A name a local shadows is left alone. And fn f(p: proc) is E0206, the counterparty's number for a keyword in type position, followed the way E0203 was; E_ASSUME_ARITY moves off E0206 to E0212 (wolf-lang#320 asks for the corpus witness).

wolf-interp#86 (fs_open_mode 5) has nothing to answer at this pin: s149 is past v0.2.10, so corpus/fs/open_nonblock.lu is not in the vendored corpus, and the fs tier still declines by name.

Census 495 -> 500 entries, 383 reach run unmoved, 375 -> 380 match; 61 out of scope, 42 conservatism and 16 dynamic counterparts unmoved. Mismatches 1 -> 2 (the pin, measured before a line was edited) -> 1 — DIV-2026-019.

0.1.32 — 2026-09-10

THE MIRROR TAKES THE RANGE ARM (is44). Pin 4c60946 -> e0ce018 — a dev stamp, s147, because [gram.pat.range] landed after the v0.2.9 tag and its four witnesses cannot be mirrored without the pin that carries them. The delta spans two sprints: s147's range arm and s146's [type.unit] family. Eight corpus files join (495 entries), five anchors (453; key sets diffed BOTH ways, nothing dropped).

Range patterns in match arms (wolf-interp#83, wolf-lang#287). 0..10 and 10..=19 on every integer primitive a literal pattern may already be written against, and 'a'..='z' on char, ordered by scalar value ([type.char.order]). Both endpoints are literals; the arm lowers to two comparisons on the scrutinee, with the exclusive high end excluded. Ranges compose with or-patterns, guards and @-bindings, and bind nothing — a range is a test, not a binding.

No open ends, and the refusal says so. 10.. and ..10 are the slice spellings of [gram.expr.primary], refused in pattern position with E0201 and a note that names the range form. That note is the whole deliverable for one of the four witnesses: grammar/match_range_open.lu was a match before this release *and* after it, because the corpus compares codes and a parser with no range production says E0201 too. The clause is explicit that the diagnostic must say the word "range" — the papercut the book carried — so the ledger for it is a test that reads the string, not a corpus directive.

Empty ranges are E0815. 5..5 stops before its own low end and 9..=3 runs backwards; the endpoints are literals, so the emptiness is decided at compile time and refused at the pattern rather than left as a dead arm no program ever takes. A one-value range (5..=5, 5..6) is legal.

Exhaustiveness as the compiler does it: no union is computed. Integer and char columns stay "infinite" for every domain, bounded or not, so a _ or binder arm is still required after range arms. Overlapping ranges are legal and the first arm wins. An arm whose every alternative lies inside ONE earlier range or literal is E0802; a range covered only by the union of several is deliberately silent. The rule is gated on a range having been spelled, so the 495-entry walk is byte-identical across it. This posture is mirrored from spec/01 — the clause states it in full — and not from the counterparty's checker, which the independence doctrine forbids opening.

wolf-interp#79's method half (sema::MethodDef). is43 closed #79 over signature positions and named the half it left: MethodDef recorded the decl and the trait, not the impl block's generic parameters, so impl[T] Stack's fn push(self, v: T) had no scope the annotation check could read T from. The collector records them now and every impl-block method's annotations are inside the check. Trait DEFAULT bodies stay out — a trait's own parameters are still not collected, and guessing is the failure the sema boundary exists to prevent. Corpus motion: zero.

s146 cost zero source motion. typecheck/unit_context_discard.lu, conc/chan_send_closed_row.lu and conc/spawn_tail_send_raised_row.lu all matched at first sight with the 0.1.31 binary at the new pin, as did grammar/match_switch.lu (#286). [type.unit.discard]'s reading was chosen partly for being non-breaking, and a mirror that already ran every witness is the cheapest evidence for that.

Census 487 -> 495 entries, 377 -> 383 reach run, 367 -> 375 match; 61 out of scope, 42 conservatism and 16 dynamic counterparts unmoved. Mismatches 4 -> 1 — DIV-2026-019, which has nothing to do with this pin.

0.1.31 — 2026-09-10

THE TAIL IS CHECKED (is43). Pin 2c03ed9 -> 4c60946 — the v0.2.9 tag, not a dev stamp. The sprint said the pin need not move, and for three of the four items it did not need to: the clauses items 1 and 3 mirror are already at 2c03ed9. #78's are not. wolf-lang#278 is spec commit 8d1e003, s145 merged after is42 pinned, and it is not an ancestor of 2c03ed9, so the char rows could not be mirrored without the pin that carries them — and the vendored corpus would have kept pinning fail(E0409) on a program the compiler runs. The delta is four files: spec/10-types.md, spec/anchors.json (446 -> 448, key sets diffed both ways — type.closure, type.closure.return arrive, nothing drops, nothing moves), corpus/strings/concat_mix_char.lu (flipped to run), and the new corpus/typecheck/closure_return.lu.

Two soundness rows leave the pairing table (wolf-interp#73, #81). They are one leniency wearing two hats: an annotation the frontend parsed and no rung ever read.

A body whose tail slot is () under a declared -> str / -> !int ran here and exited 0 — a -> !int main ending in print(…) handed the process a status by a path the program never wrote, which is #69's shape one level up. And a !T value used bare as an operand ran here too, which is 56 of wollf wl10's 2,153 verified programs passing this machine and refused by wolf 0.2.9 (E0409 x21 on +, E0401 x17 on <=). Both refuse before running now, at the resolve rung, so the record and the process agree: nothing ran, nothing printed.

Neither is a type checker arriving. Both are decided from declarations alone — a signature's return type against the SHAPE of its body's tail slot, an annotation's ! against the syntactic operand — with no inference, no unification and no literal tier, and every shape the walk cannot spell is left running. The tail's closed unit set: an empty body, a last statement that is a binding or an assignment, a while/for, a call to a function declared to return (), and a nested block. A return, a break, a loop, a match, an if, a bare path, a literal, while true (an unconditional loop diverges, and diverging is not ()), and any declared return type this machine has not resolved are all left alone. The row operand's two shapes: a name DECLARED !T, and a direct call to an item declared -> !T. f()?, f() else 0 and a match binding are never operands, and a local shadowing a fallible item is not a call to it.

[proto.cmp.triage] was run before a code was chosen, and in both rows the spec is not the defendant — which is the less common outcome and worth recording. [gram.expr.block] makes a block's value its optional trailing expression, and [gram.expr.tagident] names "the operand of return (and a fallible function's tail) against the declared return row" a CHECKED POSITION: the clause already said the tail is checked, and the implementation was not reading it. [type.interp.union] gives a !T a rendering inside a hole and marks it a carve-out in the same breath — "a reading rule, not a handling rule: ? and else still decide what the program does with the row" — which leaves an operator no reading at all. So the numbers are the corpus's own: E0401 for the tail and for a comparison (six corpus files pin fail(E0401) at phase resolve), E0409 for arithmetic and the bitwise/shift family, which is the number [type.str.concat.mix] fixes for "this operator is not defined on these operand types". Neither joins diag::UNPINNED_CODES; neither was invented. &&/|| are left alone: no clause and no measurement fixes a number for them.

Predicted walk motion: zero. Measured: zero. The prediction was that no corpus file that ran would start to be declined, because the corpus is compiler-accepted programs plus fail-pinned negatives, and a compiler-accepted program carries neither shape; the three conservatism-class fail(E0401) negatives (typecheck/if_branch.lu, typecheck/coerce_no_widening.lu, typecheck/numlit_ambiguity_named.lu) are a branch mismatch, a widening refusal and a literal ambiguity — none of them a tail or a row. The walk after the two checks is byte-identical, entry for entry: 486 entries, 375 reach run, 365 match, 16 counterparts, 42 conservatism, 62 out of scope, 1 mismatch. Nothing was correct by accident. The four witnesses are filed upstream as wolf-lang#284 so r14/r15 can pin them, with the one thing the spec could say and does not: there is no [type.fn.ret] and no [type.row], and the rule that a !T is not a T in operator position is stated nowhere directly — only implied by a clause about string holes.

[type.str.concat] — a char joins a str by + in either order (wolf-lang#278, wolf-interp#78). s + c is "{s}{c}" and c + s is "{c}{s}", the char contributing its scalar's UTF-8 bytes, and s += c is s = s + c in every place shape an assignment admits. A char is text (D58) with exactly one rendering, which is the deciding difference from an int: [type.str.concat.mix] drops the two char rows and keeps str + int / int + str at E0409, because + is not a formatter and an int has a sign, a radix and a width to choose. The mirror is two arms beside the (Str, Str) block in eval::Interp::binary, Add only; += needed nothing, because assign_binop already maps AssignOp::Add onto BinOp::Add. strings/concat_mix_char.lu leaves the out-of-scope class for MATCH with byte-identical stdout, and typecheck/closure_return.lu matches at first sight — [type.closure.return] cost this machine nothing.

A type annotation's name is resolved at last (wolf-interp#79). fn f(x: Bogus) -> int { 1 } ran here and answered 1; the compiler answers E0301: nothing named Bogus is in scope at the name. Seven book samples rested on the hole — saturating[i32], Scope, Proc — each carrying a lupin-run(…) fence that could never graduate by a compiler move, because the programs are not wolf. The pass is deliberately signature-width: a top-level fn item's parameter and return types, single-segment path heads only, recursing through prefixed types, raw pointers, tuples, error unions, fn types and every generic ARGUMENT (which is where saturating[i32]'s head sits). Methods are excluded on purpose — sema::MethodDef records the decl and the trait, not the impl block's generics, so impl Stack[T]'s fn push(self, v: T) has no scope this pass could read T from, and refusing it would be the wrong-guess failure the sema boundary exists to prevent. Qualified paths, extern signatures, dyn T, region and type are all left alone.

The prompt says did you mean :type (wolf-interp#72). type is the alias item's keyword, so type "length" and type t are both correct parse errors and both useless to the person who meant the directive. The note is appended after the diagnostic, never instead of it — type Name = int is a legal REPL line — and it fires only where the line failed to parse, which is why a bare mem still answers unsupported: mem does not resolve with no note. :help gains a first line saying directives start with :.

0.1.30 — 2026-09-09

THE MIRROR LETS else START A LINE (is42). Pin e9a17cb -> 2c03ed9, wolf-lang trunk past the s144 merge, dev-stamped: v0.2.9 is r13's and is being cut in parallel, and the clauses this sprint mirrors do not exist at v0.2.8, so the sha is the only pin this repository can record. Three rulings, four witnesses, and every one of the four is byte-identical from here. The walk goes five mismatches to one, and the one left is DIV-2026-019, which has nothing to do with this pin.

[gram.lex.newline] gains its second lookahead exception (wolf-lang#276, wolf-interp#75). A line whose first token is else continues the previous statement: at a newline the lexer looks one token ahead and, if what it finds is else, withholds the terminator that would orphan it. The trivia between — blank lines, // comments — does not count, and elsewhere is an identifier, so the four bytes must not be followed by an identifier-continue scalar. Which else it is the *binding* decides ([gram.amb.else]), never the line: } newline else { reaches the parser as } else { and f() newline else 0 as f() else 0, so the parser needed no new case — it needed one deleted. E0005 retires with the rule it enforced, and the aligned if / else if / else a reader writes without being taught to is a legal program.

E0005's *number* does not retire, and predicting that it would was this sprint's one wrong prediction. §9 of spec/01-grammar.md still reserves it — "retired … the number is never reused" — and tests/spec_extract.rs diffs our constants against that list at test time, so deleting E_ELSE_NEW_LINE would have claimed the spec dropped a reservation it deliberately kept. It stays, reserved and unreachable, as E0004 has been since 0.1.7. Retiring a code and freeing a number are two different acts; a catalog only ever performs the first. (The prediction's other miss was smaller and in the same direction: there is no E-table in this repository's manual to move a row out of.)

[conc.chan.close] spells closed and cancelled (wolf-lang#273, wolf-interp#76). is41 filed the posture with four line numbers attached and s144 ruled against this machine's spelling — the same shape as [mem.str.parse] one wave earlier, and W0603's same pact: a payload-free mark is a lowercase bare word, CapCase names a payload's type. recv's row is T ! {closed, cancelled}, {err} renders a caught row by its tag's name ([type.interp.row]), so the spelling was observable and Closed was a divergence rather than a detail. Four literals moved. The rename also found that this machine had never agreed with itself: the net tier has spelled the same condition closed since s39, and err.is_cancelled() compared against "cancelled" — a predicate that could not answer true for any value cancelled_error() ever minted.

[mem.list.pop] — the recoverable List reads answer the none row (wolf-lang#274, wolf-interp#77). The one ruling with real behavioural cost. pop on an empty list is none, not a bounds trap: [mem.ub.defined] rules indices and slices, pop takes no index, and reading it as one was an analogy rather than a clause. get outside 0..len answers the same tag, and first / last are get(0) and get(len - 1) by the clause's own words — they were not implemented here at all, so they arrive as new arms. OutOfBounds retires with them, the second CapCase payload-free mark on the builtin surface and the last. The subscript xs[i] stays the faulting twin, and a test now says so, because "these reads never fault" is the sentence a later reader over-applies. All four cite Rule::ErrUnion, which is what [mem.str.get]'s identical miss has cited since is22; the reason names [mem.list.pop] so a --trace carries the clause.

Two tests lost their provocation and neither lost its claim. Both halves of the lent-receiver proof — a_lend_hands_the_receiver_back_when_the_method_traps and the REPL's a_lent_receiver_is_back_in_its_slot_after_the_trap — rode pop on an empty list because it was the shortest trap reachable through a mut receiver, and the ruling took the only trap either had. They are re-provoked through push, which lends at the same check_home_write site and traps overflow on a literal outside the element type, after the lend and before the store. xs.len answering 0 on the next line is the same proof it always was.

Census at this pin: 520 files / 486 entries / 34 members (517/483/34); 375 entries reach run (372 at the same pin before a line was edited), 365 match (360), 16 dynamic counterparts, 42 conservatism, 62 out of scope, one mismatch — DIV-2026-019, unmoved. Anchors 445 -> 446, key sets diffed both ways: one arrives (mem.list.pop), none leaves, none moves file.

wolf-lang#275 (chan.send) is left open upstream and this pin does not reach it. wolf-interp#73 — a function's tail unchecked against its declared return type — is unmoved and stays open; it is the soundness row on the pairing table and it is the next lane's.

0.1.29 — 2026-09-09

THE MIRROR SPELLS parse (is41). Pin 5c729e8 -> e9a17cb, wolf-lang trunk past the s143 merge, dev-stamped: v0.2.9 is not cut this wave, and the clauses this sprint mirrors do not exist at v0.2.8 — 5c729e8 is an ANCESTOR of the merge that lands them, so the newest tag has nothing to mirror and the sha is the only pin this repository can record. e9a17cb is 3103577 (the s143 merge) plus one bench-only commit that folds the five new witnesses into the per-file IR-volume record; the spec/ and corpus/ trees are byte-identical at the two shas, which is why the pin moved to the later one for free. Two families ruled, and the two halves priced in opposite directions.

[mem.str.parse] — to_int's row is the mark parse (wolf-lang#265, wolf-interp#71). str.to_int() was served by every implementation and ruled by none: this one since before 0.1.13, wolf-std's corpus, the book's first chapter, and both compiler tiers from s142 — which copied the spelling from here. The clause writes the family down and renames the row on the way past. NotAnInt was the ONE CapCase payload-free tag on the builtin surface, and the language's own pact says payload-free marks are lowercase bare words while CapCase names a payload's type — W0603 warns a program that breaks it and lists parse among its examples; the OS families spell not_found, denied, closed; the json builtins already answer parse for exactly this condition, text that is not the shape asked for. One string moved (src/eval/builtin.rs), and rows/to_int_parse.lu — the single witness the two machines parted on at 0.1.28 — matches from here. tests/to_int_range.rs gains the_row_is_the_mark_parse, which reads the tag TWO ways because they fail differently: through a named match arm (a missed rename takes the fallback instead) and through {err}, which renders it by name.

[type.interp.value], .agg, .row, .union, .reason, .none — six clauses, ZERO source motion (wolf-lang#268). spec/10 §4c adopts this machine's Display byte for byte and says so in as many words: "these clauses adopt the interpreter's rendering ... it was the only rendering anyone had written down, in code". The prediction was zero and the measurement is zero. The four witnesses were run against the 0.1.28 binary at the new pin BEFORE a line was edited: strings/interp_values.lu (the whole surface in one file — a struct, a nested struct, a tuple, three List shapes, a !T in both states, a caught row with and without a payload, a qualified enum variant, and every one again in value position building a str), conc/reason_interp.lu (normal(1540), error(Corrupt), killed, fault(bounds)) and conc/chan_param_for.lu printed their pinned stdout exactly; grammar/else_default.lu printed it but for the mark. The only edits under §4c are CITATIONS: Rule::StrInterp retires the forward-namespace str.interp for the registered [type.interp.value] — the is26 move at a second address, and the forward-debt list falls from nine rows to eight — and E0412/E0413 cite the clause that now rules a format spec on a hole. [type.interp.none] is uncitable by construction: it says a conformance witness may not interpolate a value with no promised rendering, and it names channel#3 and region#1@2, this machine's bookkeeping, as exactly that.

This entry is the INVERSE of the usual one. A divergence class normally closes when this machine changes to meet a clause; here six clauses were written to meet this machine and one was written against it, and only the second cost a rename.

Two spellings this pin does not rule (wolf-lang#273, #274). Neither is a walk mismatch — no corpus witness reaches either — and this sprint does not choose for the spec. The closed-channel row is Closed here (src/eval/sched.rs:1340, read back in exactly one place at src/eval/conc.rs:674) and closed on the compiler, with Cancelled riding the same question; List.pop() on an empty list traps bounds here (src/eval/builtin.rs:1181) where the compiler answers the none row. docs/divergence-log.md carries both postures with the line numbers, and the cost of each ruling, so s144 can rule with the numbers in front of it.

Census at this pin: 514 -> 517 files / 480 -> 483 entries / 34 members; 368 -> 371 reach run and 358 -> 361 match — the three new witnesses and nothing else moving, with 16 dynamic counterparts, 42 conservatism and 63 out of scope all unmoved, and the one standing walk mismatch still DIV-2026-019. The file count moves by three rather than four because rows/to_int_not_an_int.lu LEFT: it is rows/to_int_parse.lu now, the same witness under the name the mark it pins has. Anchors 436 -> 445 with the key sets diffed both ways (nine added, nothing dropped, no owner changed) and the ratchet floor 192 -> 199: seven rather than nine, because the §4c section heading [type.interp] is cited by no witness and [type.interp.none] cannot be. Distinct conforms: tags 294 -> 301; bundle 552 -> 555 programs and 518 -> 521 records over 569 -> 572 files; conservatism ledger unmoved at 126.

0.1.28 — 2026-09-09

THE SCHEDULE IN THE MIRROR (is39). Pin 398e5f5, wolf-lang v0.2.6, the tag itself. The delta is one corpus file and two anchors, and the reading that matters is what is NOT in it.

wolf-lang#64 — sched — is deferred, deliberately, and the deferral has teeth. s139 ruled spec/07-schedule-points.md normative and admitted sched (anchors 424 -> 431, additive). That landed at ed8f526, one merge after the v0.2.6 tag: at 398e5f5 the [conf.anchor.ns] list registers eleven namespaces and anchors.json publishes zero sched.*. So this machine's eleven are exactly right at this pin, and a twelfth entry would put this side on the *permissive* half of the very silence [conf.anchor.ns.admit] was written to stop — the #239 shape, which this list wore for four pins with diag, ct, type and os. The clause says a namespace is admitted in one change or not at all; the one change is s139's, and it is not at this pin.

What lands instead is the gate that was missing on both sides: tests/anchor_admission.rs reads [conf.anchor.ns] out of the pinned spec and diffs it against anchor::REGISTERED_NAMESPACES both ways, then diffs the registered set against the namespaces anchors.json actually publishes, both ways again — every registered namespace publishes, every published namespace is registered. Until now that list was a hand-copy kept honest by whoever bumped the pin noticing, which is precisely the defect #120, #239 and #246 each name. The s139 clause text ships beside it as a planted control, so the deferral is checkable *now*, and the moment the pin advances the gate goes red until REGISTERED_NAMESPACES grows sched and its length in the type.

#242 is taken, and is38's reason for deferring it does not survive contact. [os.net.accept] arrives at this pin and names the checked machine directly: "its budgeted accept polls a non-blocking listener and retries would_block against the same budget". Nothing moved to meet it — poll_accept answers Poll::NotYet on WouldBlock and net_park measures one started against one armed(fd), which has been the posture since is18. The clause's arrival is what makes it testable rather than incidental, and it is now tested the hard way: a lost race needs two HANDS, not two PROCESSES. Two tasks in one scope accepting one listener, one dial — one takes it, the other's net_accept finds nothing there and comes back as timeout inside the budget the call began with, not a fresh one (tests/net_accept_race.rs, which asserts the elapsed bound because a count alone would not separate same-budget from re-armed). The corpus witness net/accept_race.lu reaches for os_spawn_with with a non-empty inherit set and is refused BY NAME with s137's own construct string — a verdict, never the unsupported ROW, which would be a claim about the host — so it ledgers out-of-scope, which is the row s138's own directive anticipated for this machine.

Census at this pin: 507 -> 508 files / 473 -> 474 entries / 34 members; 363 reach run and 353 match, both unmoved — the one new file is the only motion, 61 -> 62 out of scope — 16 dynamic counterparts, 42 conservatism, and the one standing walk mismatch is still DIV-2026-019. Anchors 422 -> 424 with the key sets diffed both ways ([os.net.accept], [conf.anchor.ns.admit] added; nothing dropped, no owner changed) and the ratchet floor 186 -> 187: one rather than two, because [conf.anchor.ns.admit] is a conformance clause no corpus program cites. Distinct conforms: tags 288 -> 289; bundle 545 -> 546 programs and 511 -> 512 records; conservatism ledger 122 -> 124. #239 asked nothing of this machine, as 0.1.27 predicted — both standing-waiver lists stay empty and the clause's letter simply caught up to what this side already did.

The sibling-checkout gap gets a named answer, not a build (docs/conformance-bundle.md). wolf-lang#253 names it from the other side; on this side the differential gate wants a wolf binary that CI cannot have — private submodule, a vendored snapshot that carries no crates/ by doctrine, and a second toolchain build nobody wants in a three-host matrix — so CI's only claim about diff-run today is that it correctly refuses to run. The door is already open and already walked: conformance check --replay <RECORDS> takes the counterparty as *data*, and CI feeds it this machine's own records, which is a tautology differ::retired_waivers explicitly declines to score. Vendor the compiler's records.jsonl beside PIN the way spec/ and corpus/ are already vendored and the same job becomes a real gate for one file read — with the pin-equality refusal that keeps it from going stale, the per-host record set the os tier requires, and the honest boundary that it closes the regression half and not the exploratory one.

THE MIRROR WRITES VECTORED (is40). Pin 398e5f5 -> 5c729e8, wolf-lang v0.2.8, the tag itself. The mirror was written against trunk bd7caff (the s141 merge) because v0.2.7 could not carry it: b482c52 is an ANCESTOR of b3208b6, the commit that lands the clauses, so the newest tag at the time had nothing to mirror. r11 cut v0.2.8 during the sprint and the pin moved to it before anything was committed, so the tag is the only pin this repository records — and it brings s142 along, which cost this side nothing it had not already paid.

Census at this pin: 508 -> 514 files / 474 -> 480 entries / 34 members; 363 -> 368 reach run and 353 -> 358 match, which is five of the six new witnesses and nothing else moving — 16 dynamic counterparts and 42 conservatism unmoved, 62 -> 63 out of scope (s142's fs/fstat.lu, which this machine declines by design), and the one standing walk mismatch is still DIV-2026-019. Anchors 424 -> 436 with the key sets diffed both ways (twelve added, nothing dropped, no owner changed) and the ratchet floor 187 -> 192: five rather than twelve, because three of s141's anchors are cited by the witness that ships with each, the fourth is sched.stable whose citation arrives with the pin, and the fifth is s142's os.fs.fstat — coverage counts the CITATION, so a witness this machine declines still covers its clause. The six remaining [sched.*] and s142's [os.fs] section heading are published and uncited, which is a document outrunning the corpus that names it and not a gap. Distinct conforms: tags 289 -> 294; bundle 546 -> 552 programs and 512 -> 518 records over 563 -> 569 files; conservatism ledger 124 -> 126.

s142 arrives with the tag and asks for nothing. strings/to_int.lu and rows/to_int_not_an_int.lu (wolf-lang#263) are twenty-three readings of str.to_int row for row, and both run and match here — because wolf-interp#69 landed first, below. fs/fstat.lu ([os.fs.fstat], wolf-lang#261) is a stat on an open HANDLE, and it ledgers out-of-scope with the rest of corpus/fs/: this machine has no filesystem by design (wolf-interp#18 item 6), which is a verdict and not an absence. Zero source motion for the s142 delta.

net_writev and net_nodelay ([os.net.writev], [os.net.nodelay], wolf-lang#254 / wolf-interp#67). The gathered write sends every part in order as ONE write_vectored, resumed from the byte the kernel stopped at in whichever part it stopped in — a (part, byte) cursor, empty parts skipped rather than handed over as zero-length slices, and a gather that is all empty completing with no syscall at all. Its rows are net_write's exactly, with one deliberate difference the clause dictates: Ok(0) with bytes still to send is io here where poll_write's is closed, because std calls that WriteZero and the compiler's tables answer io. invalid is NOT in the row — the clause says a typed List[List[byte]] cannot present the shape that would raise it — so the untyped shapes machinery can still build are refused BY NAME rather than given a tag no handler's arms could resolve. net_nodelay sets TCP_NODELAY either way on a TCP stream and answers io for everything else the clause lists: a listener, a unix-domain stream (the option is TCP's), a forged handle, a closed one. The DEFAULT half lives in one function, adopt_tcp: every TCP stream this table mints goes through it — accepted, dialed, or taken off the queue by a net_wait answering a readiness question — so a fourth mint site cannot forget the default.

[os.net.io] was supposed to cost nothing, and it cost exactly one thing. The clause names this machine's posture directly ("the reference interpreter polls its non-blocking sockets first and waits only on not-yet"), and that half needed no source motion, as [os.net.accept] needed none at the previous pin. But the clause also puts a write's WHOLE DRAIN under the call's budget, and corpus/net/syscall_first.lu asserts that a budgeted large net_write comes back with net_write's io. This machine answered a bare timeout — a tag outside the row net_write declares, which is wolf-interp#47's defect at a new address, because [gram.expr.tagident] makes a handler's arms exactly as wide as the DECLARED row. budget_row is the fix and the whole of the cost: a fired net_deadline answers timeout where the call declares one and io where it does not, read out of builtin::declared_row so a call cannot hold two opinions about its own row. The clause states the coarsening in those words; the bug was older than the clause that exposed it.

sched is admitted, in the same change as the pin that carries it. is39 deferred the namespace because s139 admitted it one merge AFTER the v0.2.6 tag, and mechanized the deferral so it could not go inert. This pin is past ed8f526: the pinned [conf.anchor.ns] registers twelve namespaces, anchors.json publishes seven sched.*, and corpus/test/conc_schedules_test.lu now carries a real conforms: … sched.stable tag instead of the prose comment it wore before. Both gates went red on the pin bump alone and green on the admission — which is the deferral working, not a lucky catch — and the planted s139 control is kept rather than deleted, with its assertion flipped.

wolf-interp#68 — the D57 suffix can go stale, and in a WORKTREE the invalidation set was empty outright. r10 found target/release/lupin announcing the bare lupin 0.1.27, a release claim, from a tree that had moved off the tag. This lane did not reconstruct which build produced r10's particular binary; what it did do is find a hole that is total and reproducible on demand. The probe was Path::new(".git/HEAD").exists(), and in a git worktree — which is how every lane in this org builds — .git is a FILE (gitdir: …), so the probe missed, the loop continued, and build.rs declared nothing but itself. Not a weak set: an empty one, so a branch move, a tag landing and a re-vendor were all invisible there. The metadata directory is now ASKED FOR (git rev-parse --absolute-git-dir) and a worktree's split between its own HEAD and the repository's shared refs is followed through commondir; vendor/upstream/PIN joins the set, because a re-vendor is a change of identity. tests/build_stamp.rs runs #68's own sequence against the real build.rs — build at a tag, move HEAD, rebuild, see +dev appear — once in a plain checkout and once from a worktree, with a negative control asserting the script does NOT re-run when nothing changed (a script that re-ran always would make the whole file vacuous).

What cargo cannot be made to re-run on, stated rather than implied. rerun-if-changed is an mtime comparison against a path, not a hook on a git event, and D57 asks a question no path answers directly: does a tag point at HEAD right now? The paths cover how that answer changes in practice, because each writes into .git. They do not cover a change whose mtime is not NEWER than the cached output — a fresh clone, a checkout restoring an older stamp, a coarse-granularity filesystem — nor anything outside .git and the package. So the honest fallback is not a cleverer path list: the release build is made fresh, at the tag, in a clean tree (r01's ritual, and CI's release job runs on a runner with no target/), and WOLF_INTERP_STAMP_NONCE is a declared rerun-if-env-changed escape hatch for a human who knows better than the mtimes.

wolf-interp#63 — diff-run refuses an unoptimized build of itself. The harness scores a timeout as a verdict, and that is right for a program the counterparty finishes and this machine does not. It is not right when the only thing that produced it is the profile: memory/byte_list_ledger.lu is under a second at --release and was still running at 60 against a 30-second budget under debug, and is38 wrote that row down as a load artefact because nothing in the output could tell it apart from a divergence. Of #63's three dispositions, a per-row budget is the weakest and the issue says so — it makes one row pass and leaves the cliff unnamed for the next allocation-heavy witness. Stating the profile in the verdict makes the record honest but still produces the record, and #63 exists because somebody read one and got it wrong. Refusal is the only one that makes the failure mode unreachable, and it is this repository's own precedent: the toolchain tools refuse on identity drift rather than reporting a drifted answer. The refusal lands AFTER the counterparty is resolved, so a lane with no compiler still SKIPs loudly and green whatever it was built at; --allow-debug-self is the named door, and taking it does not restore the silence — the warning names the build, every divergence carries x-self-profile, on the release side too, and the harness profile: line prints on every run.

wolf-interp#69 (wolf-lang#263, s142) — str.to_int minted an int that is not one. int is 64 bits, signed and checked; to_int parsed into an i128, so "9223372036854775808" answered a value no int can hold. Nothing rejected it at the mint site, and the program's first arithmetic on it trapped with an overflow it never wrote — a range error at the parse displaced into a trap somewhere else. Parsed as i64 now, which is what the type is, and one past either extreme is the NotAnInt row the compiler already answers (X3 forbids a quiet wrap, and there is no int the text names); the row already means "this string is not an int", which is exactly true of a number that does not fit in one. wolf-lang held the overflow input in its own crate tests so the corpus pinned neither side while the two disagreed; it can move into the corpus from here.

0.1.27 — 2026-09-06

THE CORES IN THE MIRROR (is38). r08 shipped a server's other half: several processes holding one listening address, a parent handing a listener down to its children, one call that waits on a whole SET of sockets instead of time-slicing across them, and a program that can ask how many cores it may actually be scheduled on. This release is the reference interpreter catching up to all five anchors at once, and the half worth reading is that catching up does not mean serving everything. Two of the five are refused BY NAME here, in s137's own words, because the reason the checked machine refuses them is the reason this machine does.

Released against pin 6ade878, wolf-lang v0.2.5, the tag itself; trunk's head 6263ffa carries a byte-identical spec/ and corpus/, so the check-the-tag pattern lands on the tag as it did at v0.2.4. Census at this release: 507 files / 473 entries / 34 members; 363 reach run (360 at 0.1.26), 353 match (350), 16 dynamic counterparts, 42 conservatism, 61 out of scope, and the one standing walk mismatch is still DIV-2026-019. Anchors 417 -> 422 with the key sets diffed both ways (five added, nothing dropped) and the ratchet floor 182 -> 186. Bundle 545 programs / 511 records; conservatism ledger 120 -> 122.

Three corpus files newly reach a verdict that matches, and none stopped.

wolf-lang v0.2.6 tagged while this sprint was in flight and is NOT taken here. r09's delta is one corpus file (net/accept_race.lu), spec/05's [conf.anchor.ns] amendment for #239, and a new [os.net.accept] in spec/11 for #242, the fair-accept re-wait, which is an implementation surface and not a mirror of anything in this release. A pin bump lands in its own commit (docs/manual/00-building.md), and taking one mid-sprint would have moved every census, anchor and ratchet number in the two commits already gated against 6ade878. #239 asks nothing of this machine either way: anchor::REGISTERED_NAMESPACES has carried os, type, ct and diag since the 90c90df, 77466a3, da8582d and f0da6e6 pins, so the clause's letter is catching up to what this side already did, and both standing-waiver lists (export::FILED_REGISTRY_FINDINGS, FILED_REGISTRY_HOLES) are empty.

The count of cores, and this machine's own path ([os.cpus], wolf-lang#233)

os_cpus() answers how many cores this process may actually be SCHEDULED on, which is the whole content of the clause: on linux the number honours a cgroup cpu quota and a cpu affinity mask, so a container given two cpus of quota on a sixty-four-core host answers 2, where counting processor rows in /proc/cpuinfo answers 64 and starts sixty-two workers that will never get a core between them. available_parallelism reads exactly the sources the clause names on every host, which is what makes the clause's stronger claim true here: every tier answers from the same source, so wolf run, wolf run --checked and lupin agree on the NUMBER on one host and not merely on a relation. A host that cannot answer is the io row and never a silent 1; the distinction #233 was filed about, kept where a program can act on it.

s90's os_exe() lands beside it, because the corpus calls it for the first time at this pin: net/inherit_listener.lu re-executes itself. The honest answer here is this binary (lupin is the process that is running, and a wolf program it interprets has no separate image) and the witness asserts a predicate over the answer, never a path.

Readiness over a set ([os.net.wait], wolf-lang#127)

net_wait(fds, deadline_ms) is the one s137 clause that names no host and no refusal, so it serves on every lane this machine has. There is no poll(2) to reach from here (this crate forbids unsafe and std::net exposes no readiness surface), so the question is asked of each socket in turn, on the non-blocking handles this machine has held since is18:

All three keep the sentences a program can actually check, which is what the clause is made of: an empty answer is the deadline expiring with nothing ready, an answer, not a failure; readiness is level-triggered, so asking consumes nothing and two waits answer the same until something is actually read or accepted; a stream whose peer has gone is READY and the net_read that follows is what reports closed. io is a forged handle anywhere in the set (the set is validated WHOLE before a single socket is asked, so a bad member never leaves a held connection behind on a good one) or an empty set with an unbounded deadline, which nothing could ever end.

A listener several hands can hold ([os.net.listen.opts], wolf-lang#234)

net_listen_with(addr, reuse_port, backlog) serves, and the group is a MODEL that is written down where it lives rather than a SO_REUSEPORT this machine cannot reach (the option must be set before the bind, and setsockopt needs unsafe).

What the clause promises a caller is narrower than the option, and that is the opening: every dial is accepted by SOME member, and the survivor takes every dial after the others close. What the kernel does INSIDE a live group is explicitly the host's (linux distributes by 4-tuple hash, macOS hands every SYN to the newest bound socket) and a program may not depend on it, which is why corpus/net/reuse_port.lu asserts the two guarantees and prints nothing about delivery. So a group here is ONE listening socket with a duplicated handle per member (TcpListener::try_clone), which is [os.proc.inherit]'s own shape (one queue, several accepters) and both guarantees hold by construction: every member accepts from the same queue, and the queue outlives any member that closes.

Two smaller things the clause decides and this release takes: net_listen_with( addr, false, 0) is net_listen(addr), call for call, on the ordinary listener every other net call already serves; and this call admits a fixed LOOPBACK port where net_listen refuses one, because exists and denied are rows about a NAMED port and a machine that refuses every fixed port can never answer either. Non-loopback stays refused by name, and backlog is a hint std gives no setter for and no observation in the protocol can see.

The two constructs refused BY NAME ([os.proc.inherit], wolf-lang#235)

net_adopt_listener(fd) and a non-empty os_spawn_with inherit set decline here, with the construct strings s137 published for the checked machine, verbatim:

listener adoption in checked execution
fd inheritance across os_spawn_with in checked execution

Never the unsupported ROW; that would be a claim about the HOST, and both hosts this machine runs on serve the handoff. The reason the clause gives the checked machine is the reason here: this is a binary INTERPRETING a program, so a descriptor handed to "the program's child" would be handed to the interpreter's child. The mechanism seals it independently; the handoff is a pre_exec hook running dup2 and the adopt is FromRawFd, both unsafe, which this crate forbids. [proto.cmp.defined-divergence] never compares an unsupported, so nothing in the differential would ever notice these two strings drifting; tests/cores_s137.rs asserts them on the words.

An empty inherit set is served, exactly as the clause requires: it is os_spawn with the program named apart, so it reaches the spawn and answers the spawn's own row.

wolf-lang#228 / DIV-2026-021 — the locus row gets a gate, not a ruling

is38 offered two branches: implement the span comparison at that rung so the row becomes measurable, or rule the divergence and close it.

The first is already done and has been since is01. compare::compare and differ::compare_deep compare the first diagnostic's code AND span at every rung through mem, and lupin diff-run reports the row today. r08's "no harness surfaces it" is true of the harness it was written about (wolf-lang's pairing ritual compares codes at parse) and not of this one. Re-measured at the v0.2.5 pin, neither locus moved: lupin E0201 [364,365) at the comma, wolf 0.2.5 E0201 [374,375) at the end of the initializer list.

The second this lane may not take. CONTRIBUTING's standing divergence-filing rule is [proto.cmp.triage] in one sentence (neither implementation is patched to match the other before the clause is fixed) and [gram.item.let] still says what a D63 let-group IS and what the bare-tuple shape is not, without saying where refusing it reports. Moving this machine's locus onto the counterparty's would close the row with the clause having decided nothing.

So what landed is the two gates the row actually lacked, and neither touches the locus:

A divergence no gate can see is the shape this project keeps finding the hard way; so is a waiver no gate can retire. Both are closed, and the locus stays where the clause left it.

zext from bool, and why there is nothing to do about it here

v0.2.5's second fix makes zext from bool legal WIR, so channel[bool] stops compiling to an internal compiler error. This machine has no site for it: there is no WIR here and no lowering to verify, channel is one of the names carried in the ambient stub with no pinned semantics (so a program that uses one is declined by name rather than guessed at), and neither zext nor channel[bool] appears anywhere in the pinned spec/ or corpus/ at this pin. Recorded rather than absorbed: a fix on the other side with no mirror image is still a fact about the pair.

0.1.26 — 2026-09-04

THE BYTE HAS A DOMAIN (is37). Last release gave this machine the byte TYPE and left the DOMAIN to the compilers. sc35 measured what that cost: push(256) into a List[byte] stored 256, an un-cast int flowed into a byte slot, and eight wolf-std rows carried divergent(…), the word for "the compilers refuse this at typecheck and the interpreter runs it to an honest end", used for the first time outside the take-mode pair. All eight flip to agreement here, and two more rows with them.

Released against pin 982f857, wolf-lang v0.2.4; unchanged: s137 has not merged, and trunk's head (1323c4e, the r07 merge) is tree-identical to the tag, so there was nothing newer to take. Census at this release: 503 files / 469 entries / 34 members; 360 reach run, 350 match (348 at 0.1.25), 16 dynamic counterparts, 42 conservatism, 60 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Anchors 417 / 182 covered and bundle 541 / 507, both unchanged; no corpus moved, only this machine's answers.

0.1.25 — 2026-09-03

THE BYTE ARRIVES (is36). Last release deferred byte by name: D72 had ruled the scalar in and s135 had not merged, so this machine answered E0301 ("nothing with this name is in scope") to every program that wrote the type. s135 and s136 merged together, and this release is the mirror catching up to both halves at once: the type, its two casts, its widening operators and its octet compares; the producers that speak it; the 1-byte ledger slot the whole ruling exists for; the unix-domain socket family beside them; and D74's string-layout codes, which this implementation had exactly one of.

Released against pin 982f857, wolf-lang v0.2.4, the tag itself, trunk's r07 merge (1323c4e) carries a byte-identical spec/ and corpus/, so the check-the-tag pattern lands on the tag as it did at v0.2.3. Census at this release: 503 files / 469 entries / 34 members; 360 reach run, 348 match, 16 dynamic counterparts, 42 conservatism, 62 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Eight files newly reach run and not one stopped.

0.1.24 — 2026-09-02

THE BYTE IN THE MIRROR (is35). A diagnostic code is a name two machines say to each other, and this release is about one that meant two things. lupin answered E0103 for an escape wolf does not have ("a\qb"), while [gram.lex.str.escape] had read "any other \ is E0101 at the escape" since v0.2.2, and while wolfc's catalog spends E0103 on something else entirely: text after a multiline's opening """. A program refused for a bad escape and a program refused for a badly shaped """ were the same record here, so the differential could not tell them apart. r06 found it the moment a corpus file carried an unknown escape (wolf-lang#225).

The span was identical on both machines before the change and after it. Only the number moved, which is exactly why 484 files never showed it: #198's two witnesses pin the \u{…} DIGIT bound, where both machines already agreed at E0101, and the corpus walk compares the check: code rather than the span.

Released against pin 3befc3e, wolf-lang v0.2.3, the tag itself: trunk and the r06 merge add nothing to spec/ or corpus/ beyond it. Census at this release: 486 files / 452 entries / 34 members; 352 reach run, 335 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Every one of the 482 files carried over from 0.1.23 is verdict-IDENTICAL, class for class, the single field that moved anywhere is one file's human-only x-unsupported sentence.

0.1.23 — 2026-09-02

THE LETTERS IN THE MIRROR (is34). Three letters came back from r05 with v0.2.2, and the one that mattered was a sentence about death: a trap runs no defer or errdefer, anywhere. is33 implemented the proc half of [conf.trap.exit] and left the ROOT path alone by name, because the clause ruled the proc and was silent about the root, the correct posture when the spec is the defendant. wolf-lang#209 closed that silence the consistent way, and lupin's root domain took it: faults/trap_skips_root_defers.lu printed inner inner-defer before-trap root-defer at 0.1.22 and prints inner inner-defer before-trap here, which is what all three wolfc lanes print. The inner block's defer still runs, at that block's own exit, before the trap; that is an ordinary scope exit and not a trap path at all.

The second letter is why nobody could see the first. Through 0.1.22 this implementation reported stdout_inline: null on every trapping program, so the two machines were verdict-identical whatever they printed, and #209 (a divergence made of nothing but trap-path output), survived unmeasured from D66 to r05. wolf-interp#55 is fixed: the record now carries the output for every verdict that reports a completed run. Two records moved over the 487-record bundle, both traps, and both agree.

The third is a kindness. wolf-interp#56: the D67/D69 comma refusals now say what to write and point at where it goes, which the strict implementation (the one that has always demanded the separator) was the one not saying.

Released against pin 8cda3aa, wolf-lang v0.2.2, the tag itself: trunk and the r05 merge add nothing to spec/ or corpus/ beyond it, so the check-the-tag pattern lands on the tag rather than a merge sha. Census at this release: 482 files / 449 entries / 33 members; 350 reach run, 332 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Every one of the 479 files carried over from 0.1.22 is verdict-IDENTICAL, class for class.

E0201: expected }, found identifier y; the members of a struct literal are separated, add the comma [gram.expr.primary] at 18:26 the comma goes here at 18:25

Span parity holds and is asserted structurally: every primary span is byte-identical to 0.1.22's, because the note is additive and never a relocation. The multi-line form is the one that needed care (D69 reports at the FIELD the missing comma should precede while the comma belongs after the previous field, one line up), so the insertion point is captured before the terminator run is skipped. The note is not a blanket suffix: an empty list, a list that ended at its closer, and end-of-input (E0203) get none, because "add the comma" would be teaching a lie there. Wording is outside the differential protocol (D22), so no record moved.

0.1.22 — 2026-09-01

THE MIRROR HOLDS (is33). The region cap lands whole-pipe, and with it the last thing lupin 0.1.21 deferred by name: region r(cap: n) bounds a region's ledger, a charge past the budget is trap(alloc-contract) at the allocating site, and inside a proc that trap is contained at the boundary and reaches the join as fault(alloc-contract), s132's ruled shape for [mem.region.cap.1-3] and [conc.proc.exit] (D68, wolf-lang#187, now closed). All three of the wave's cap witnesses flip from parse-refusal to running, and they compare relations rather than byte counts for the reason the account witnesses did: the clause denominates the budget in each tier's own ledger units, and wolf-lang#203 measured an order of magnitude between ledger units and payload bytes. Released against pin 2bfbe5e, wolf-lang trunk's head, the s132 merge; no v0.2.2 tag existed at the release step, so the merge sha is the pin (the is31 check-the-tag pattern). And the release now HAS binaries: wolf-interp had no dist workflow at all, so 0.1.22 is the first lupin with archives on its release page for all four tier-1 hosts, including the lupin.exe the learners' path has been waiting for. Census at this release: 479 files / 446 entries / 33 members; 348 reach run, 329 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every one of the 474 files carried over from 0.1.21 is verdict-IDENTICAL, class for class.

| witness | 0.1.21 | 0.1.22 | |---|---|---| | memory/region_cap_boundary | fail(E0201)@parse — MISMATCH | exit(0) "at-cap true \| zero-cap true \| query true" — match | | faults/region_cap_breach | fail(E0201)@parse — MISMATCH | trap(alloc-contract) — match | | conc/proc_cap_fault_join | fail(E0201)@parse — MISMATCH | exit(0) "fault true \| alloc-contract true \| reclaimed true \| main-defer-ran" — match | | grammar/struct_literal_no_separator | fail(E0201)@parse | fail(E0201)@parse — match | | grammar/closure_params_no_separator | fail(E0201)@parse | fail(E0201)@parse — match |

Three witnesses flip (every one of them the sprint's own work, since 0.1.21 deferred the cap by name and cap: was unknown syntax) and two answer at first sight, at the same code and the same span.

0.1.21 — 2026-09-01

THE LEDGER IN THE MIRROR (is32). The region machine learns to say how much it has charged: region_bytes(r) and live_region_bytes() land against [mem.region.account.1/.2] (s131, wolf-lang#187), and the interesting part is *what the two machines are allowed to disagree about*. The clause leaves the UNITS per tier ("what charges, and by how much, are implementation-measured facts per tier, not comparison surface") and pins four RELATIONS instead. So lupin's ledger is its own honest arena model, the native tier's is alignment-rounded arena storage, the checked machine's is its shadow memory, the three numbers differ, and the two witnesses still agree byte for byte, because what they print is booleans. That is the differential working as designed rather than in spite of a divergence. Released against pin e6cf24e, wolf-lang trunk's head, the s131 merge plus the 2026-09-01 ledger ritual; the newest tag v0.2.1 sits seven commits behind it, so the merge sha is the pin (the is31 check-the-tag pattern). Census at this release: 474 files / 441 entries / 33 members; 345 reach run, 324 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every one of the 463 files carried over from 0.1.20 is verdict-IDENTICAL, class for class.

| witness | 0.1.20 | 0.1.21 | |---|---|---| | memory/region_bytes_query | unsupported@resolve — "live_region_bytes does not resolve" | exit(0) "create true \| grew true \| stable true \| live_up true \| reclaimed true" — match | | memory/region_bytes_value | unsupported@resolve — "region_bytes does not resolve" | exit(0) "create true \| attributed true \| birth true" — match | | grammar/char_uni_seven_digits | fail(E0110)@lex — MISMATCH | fail(E0101)@lex — match | | lints/region_call_allocates | exit(0) | exit(0) — match | | memory/region_unit_tail_call | exit(0) | exit(0) — match | | grammar/defer_loop_turn | exit(0) | exit(0) — match | | grammar/match_arm_or_over_product | exit(0) | exit(0) — match (c06 residue) | | grammar/match_arm_or_inside_product | exit(0) | exit(0) — match (c06 residue) | | grammar/struct_pattern_no_separator | fail(E0201)@parse | fail(E0201)@parse — match | | grammar/struct_pattern_rest_bare | fail(E0201)@parse | fail(E0201)@parse — match | | grammar/tuple_pattern_no_separator | fail(E0201)@parse | fail(E0201)@parse — match |

Two witnesses flip (the ones this sprint exists for), one is fixed (the escape), and eight answer at first sight against work already in this tree, D67's comma trio included, whose letter this parser already held, and #196's two or-pattern residue pins, where the compiler's native pipe refuses by name and this machine runs: a divergence wolf-lang filed on purpose, carried in the differ's ledger rather than papered over.

0.1.20 — 2026-08-31

THE ARMS AGREE (is31). Match arms take the product domain, and with them the last pattern asymmetry between the two machines closes: the c06 refusal family s130 retired on the compiler's side had a deliberately symmetric twin here, and it dies in the same motion (s130, wolf-lang#179). Released against pin b80d239 (wolf-lang's s130 merge; no v0.2.1 tag existed at the release step, so the merge sha is the pin, and the spec tree it carries is BYTE-IDENTICAL to 0.1.19's 83f83bb (404 anchors, nothing gained, nothing dropped), s130's whole delta is lowering, corpus and CHANGELOG). Census at this release: 463 files / 430 entries / 33 members; 338 reach run, 313 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every one of the 455 files carried over from 0.1.19 is verdict-IDENTICAL, class for class.

| witness | 0.1.19 | 0.1.20 | |---|---|---| | grammar/struct_pattern_match_arm | unsupported@resolve (out-of-scope) | exit(0) — match | | grammar/match_arm_product_nested | unsupported@resolve | exit(0) — match | | grammar/match_arm_at_binding | unsupported@resolve | exit(0) — match | | grammar/match_arm_deep_tree | unsupported@resolve | exit(0) — match | | typecheck/match_arm_product_unreachable | unsupported@resolve | exit(0) + E0802 — match | | memory/match_arm_whole_move | unsupported@resolve | trap(use-after-move) — E1001's counterpart | | typecheck/match_arm_product_nonexhaustive | unsupported@resolve | exit(0) — conservatism |

The tuple twin (tuple_pattern_match_arm) and match_arm_str_in_product already agreed and still do. Six of the seven join the agreement class outright; the seventh is honest conservatism, not agreement; exhaustiveness is the type checker's and E0801 has no dynamic half, so match_arm_product_nonexhaustive sits beside match_missing and match_str_nonexhaustive in the same column they have always occupied.

0.1.19 — 2026-08-31

THE SHAPE BINDS (is30). Struct patterns land whole-pipe from the [gram.pat.struct] text alone (s129, wolf-lang#179), the destructure tier they generalize learns the element-move discipline it owed since s128, and the s106 net byte tier resolves at last (F-0102). Released against pin 83f83bb (wolf-lang's s129 merge, two bumps past 0.1.18's addcd7f) and the pin question 0.1.18 left open is closed twice over: the first bump (v0.2.0, c88ab64) retired the FILED_REGISTRY_HOLES waiver for wolf-lang#177 the day r03's spec-extract fix re-gained gram.lex.ident (403 anchors, zero export notices), and the second bump took the s129 merge itself the moment it reached origin, so the full witness set rides the census (404 anchors, +gram.pat.struct). Census at this release: 455 files / 422 entries / 33 members; 329 reach run, 306 match, 15 dynamic counterparts, 41 conservatism, 59 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every pre-existing file is verdict-identical across the span except the two movers named below.

0.1.18 — 2026-08-30

THE FIRST ARM YIELDS (is29). The two thrice-measured lupin-side correctness debts behind wolf-std's four divergent(…) ledger rows, paid from the spec clauses and the F-0079 lineage, never from the compiler's source: a multi-arm handler over a BUILTIN-raised row took its first arm for every tag (#47, wolf-std F-0097), and take-mode reuse (a static E1001 on both compiler rungs), executed here to its dynamic outcome (#48, wolf-std F-0098).

Released against pin addcd7f, unchanged from 0.1.17; wolf-lang has no v0.2.0 tag at this release, so the FILED_REGISTRY_HOLES waiver for wolf-lang#177 (gram.lex.ident) carries to is30 and the pin question re-opens there. Every pre-existing corpus file is verdict-identical before and after every commit in this span: the judge counts are 310 run / 287 match / 13 counterpart / 41 conservatism / 55 out of scope / 1 mismatch, byte-for-byte the 0.1.17 per-file table, three times over.

Downstream, pre-recorded in wolf-std's row comments and verified on this build directly against wolf-std's tests/ before tagging: net/refused_row and net/closed_row print their directives' stdout (handled: -1 / peer-gone: handled, then the propagated tag), and net/use_after_close + process/use_after_wait are fail(E1001) where 0.1.17 trapped use-after-move / diverted at start(), the four divergent(…) words flip to run at sc-track's next binary bump, which is the red the runner was built to read.

0.1.17 — 2026-08-29

THE INTERPRETER KEEPS TWO PROMISES (is28). Two user-visible fixes, both measured live on 0.1.16: a scratch directory where every file says //! member: false still collided on main (#49, the run-door half of D59 was never landed), and the origin marker the language ruled in D61 either failed at lex (#![index(1)] → E0101) or, in the dangerous class, was a silently ignored statement attribute that ran grammar/index_origin_scopes.lu to the WRONG answer (wolf-lang#169). Both promises implemented from the ruling texts and the spec clauses, never from the compiler's source.

Released against pin addcd7f (the s126 wave; one bump, e561c6f → addcd7f). Corpus 422 → 430 files (397 entries + 33 members); coverage ratchets 157 → 159; anchors 396 → 397. All six run-reaching index_origin_* witnesses MATCH at first sight (including the byte-exact stdout pins on the file-wide and scopes witnesses) and the two fail pins land the spec's own codes (E0211 by position, E0813 by name). Every pre-existing corpus file is verdict-identical before and after: the judge counts moved 304→310 run / 279→287 match / 13/41/55/1 unchanged, exactly the eight new witnesses, nothing else.

0.1.16 — 2026-08-28

THE INTERPRETER NAMES THE LINE (is27, the lupin half of s125's trap-site pair, sequenced after 0.1.15 as that contract required). At 0.1.15 a trap said at 51..58, byte offsets no editor jump-to and no human counts out (#158, measured in the s125 report). The renderer HAD the span; the gap was purely rendering. This release renders it: implemented from [conf.trap.render], never from the compiler's rendering (the independence doctrine).

Released against pin e561c6f (the s122–s125 wave; one bump, a900b8c → e561c6f). Corpus 403 → 422 files (389 entries + 33 members, including the corpus's FIRST four bare members, D59); coverage ratchets 153 → 157; anchors 393 → 396; twelve of the thirteen new run-reaching entries match at first sight.

0.1.15 — 2026-08-28

THE SCALAR RELEASE (is26, one sprint behind s121/D58). At 0.1.14 the lexer refused ' outright (fail(E0101), "' begins no token"), so every char witness the compiler landed was wolfc-lane evidence only: wolf accepted programs its oracle could not even lex, the weakest position a differential oracle can hold. This release teaches lupin the scalar, independently, from [type.char], [gram.lex.char] and [mem.str.chars], never from the compiler's source.

Released against pin a900b8c (the s117–s121 wave; one bump, 90c90df → a900b8c). Corpus 385 → 403 files (374 entries + 29 member files), all 14 new run-reaching entries matching at first sight because the scalar landed here BEFORE the bump; coverage ratchets 144 → 153. The seven char witnesses (char_battery, char_order, char_interp, chars_walk, and the three faults/char_cast_* twins), compare for the first time: 0 → 7, and the walk holds 0 mismatches.

0.1.14 — 2026-08-27

THE CATCH-UP RELEASE (r02, sprints is14 → is25). 0.1.13 shipped on 2026-08-15, and then twelve sprints landed, 58 commits, with the upstream pin re-vendored eight times, while the version, this file, and the tag never moved. Two materially different interpreters could both answer lupin 0.1.13 while declaring different conformance pins: the version had stopped identifying a state. This entry is the correction, and it is honest about the gap: one release covering the whole span, grouped by theme, not twelve retroactive versions pretending each had shipped.

Released against pin 90c90df (the s114–s116 wave). The span advanced the pin 02c1e88 → c9da6d9 → b522b8a → 1b149ba → 87405ac → 21b129e → da8582d → 77466a3 → 90c90df; each re-vendor sits under its sprint's merge in the log, and each registered what its wave demanded (the ct namespace at da8582d, type at 77466a3, os and pkg at 90c90df). Corpus 294 → 385 files, the walk green at the pin (385 file(s), 0 failure(s)), and the run-ledger holds 0 mismatches throughout.

0.1.13 — 2026-08-15

THE ARM-SELECTION PASS (sprint is13). One silent wrong answer and one complexity bug; no corpus verdict moves, and every entry file of the pinned corpus records byte-identically before and after; the only file whose record changes is the one added to catch the miss.

Released against pin 02c1e88, which advances upstream across the s88/s89/s90 wave: the corpus grows 283 -> 294 files, the bundle 298 -> 308 records, anchors 316 -> 323, and coverage ratchets 107 -> 114. grammar/range_bare.lu pins E0201, so that code left this implementation's UNPINNED_CODES; the corpus is its authority now. The differential is GREEN at the new pin: 308 entries, 0 divergences.

Whether a bare lowercase identifier in a pattern is a *row-tag pattern* or a *binding* is a question about the scrutinee's row, and the checker answers it from the row type. This machine asked the matching module's own signatures (sema::Module::row_tags, collected per module from that module's own fn headers). A handler in the entry file over tagmod.miss()'s row therefore found no declared alpha, read every arm's pattern as a fresh binding (and a binding matches anything), so the first arm won for every tag. Reversing the arms reversed the output, which is what tells "first arm always" from "one tag happened to be right".

The row now travels with the value it was raised through (ErrorValue::row, recorded at the raise site from the enclosing function's declared return row, and carried through ?, through payload application, through every copy). A row that crosses a module boundary is still the same row, so arm resolution asks the value which row it came from instead of asking the far side's declarations again. The module's own vocabulary stays in the union behind it, so a tag with no declared row still resolves where its module declares it, and a lowercase name the row does *not* declare still binds: else |err| keeps its binder and an other => rest arm keeps catching.

corpus/rows/cross_module_arms/ is the witness, three arms in both orders over an imported module's row, a ? hop on the far side, and the two binder shapes as the counterweight. It runs 10 20 30 on both compiler rungs and here; before the fix this machine printed 10 10 10 forward and 30 30 30 reversed. rows/propagate was the corpus's only cross-module handler and it took else |_|, which is why nothing noticed. The unit-sized half, which does not wait on a pin bump, is tests/rows_option.rs's a_row_raised_across_a_module_boundary_still_dispatches_by_tag.

Measured, a List[int] of N built by push then read back by index (release build, CPU seconds, min of three, this box):

| N | before | after | |---|---|---| | 2 000 | 0.156 s | 0.042 s | | 4 000 | 0.576 s | 0.080 s | | 8 000 | 2.317 s | 0.157 s | | 32 000 | 21.500 s | 0.333 s |

Four times the work per doubling down to under two (the curve the issue's for v in xs walk already had) and 65× at 32k.

One shape moved, and it moved onto the compiler's answer. The copy path snapshotted the container *before* evaluating the index, so a write hidden in the index expression was invisible to the read that followed it: xs[bump(mut xs) - 1] trapped bounds against the stale length in interpolation position, while the identical let v = xs[bump(mut xs) - 1] answered 9, this machine disagreeing with itself over two spellings of one expression. The lend is taken *after* the index is evaluated (#24's ordering), so both answer 9, which is what wolfgang answers. Pinned in eval::tests.

The other half of #28 stays open: a read-mode List argument still copies, on the way in and again on the way out (call-by-value-result reads every parameter's final value back). That is the same value-model question as #25 and is not a lend away; a callee runs arbitrary user code, which can reach the caller's place by other names.

0.1.12 — 2026-08-13

THE OWN-LEDGER PASS, then the re-pin. Two of this machine's own defects first (wolf-interp#24 fixed, wolf-interp#25 measured and deliberately deferred) and then pin f8dca42 → 4e316ad (latest green trunk, CI run 31742412670, headSha matching). Corpus grows 3: 280 → 283 files. Ratchet holds 107, anchors hold 316 (the range touches no spec/ file), bundle 320 programs / 298 records.

The fix is to lend the receiver instead of copying it (Lend in src/eval/mod.rs). A builtin container (List, Map, str), always dispatches to builtin::method (method_of answers only for Value::Struct), and no container arm of builtin::method re-enters the machine, so the value can leave its slot for the duration of the call and return to it. Nothing observable moves: the read is charged at the same moment in the same order, and the lend is taken *after* the arguments are evaluated so xs.push(xs.len) still reads xs through its parent while the receiver's tag is Reserved (corpus/memory/prov_two_phase.lu's two-phase window, untouched).

Two details carry the correctness. Whether the lend ends in a *write* is decided by an O(1) witness (List.push and List.pop are the only two arms of builtin::method that mutate a receiver (builtin::mutates_receiver), and both change the element count exactly when they change the value), so [mem.region.freeze.4] (issue #20, a read-only method must not write back) still holds without the whole-value comparison. And a *mutating* lend asks writeback_would_trap first: write_path faults before it stores, so on that path the receiver must survive the call unchanged, which only a copy can promise; the lend is declined there and the old path runs. In a debug build the whole-value comparison is kept as a debug_assert_eq! against the witness, so the entire test suite and corpus check the classification on every method call rather than trusting it.

Measured, same program at four sizes (var xs = List[int](), N pushes, release build, this box):

| N | before | after | |---|---|---| | 4 000 | 0.40 s | 0.038 s | | 8 000 | 1.44 s | 0.055 s | | 16 000 | 6.18 s | 0.100 s | | 32 000 | 30.33 s | 0.191 s |

Quadratic (4× per doubling) to linear (~1.9× per doubling); 159× at 32k, and the compiler's two run-reaching rungs do 32k in 0.14 s, so the oracle is now within a small factor of the thing it is oracle for instead of 200× off it. The gate that matters is not the clock: the full lupin corpus report (human and --json) is byte-identical before and after the change.

0.1.11 — 2026-08-13

THE ORACLE HELD: the semantics-wave re-pin. Pin bumped 613c3dc → f8dca42 (latest green trunk, CI run 31676830124; the two red runs in the range were not taken). This is the largest semantic movement the compiler has had in one wave, s74 (the correctness cluster), s75 (List element access lowers to a load, bounds checks caller-side and eliminated when a relational range channel proves them), s76 (containers allocate in the ambient region at the allocation site, dynamically scoped per D12), s77 (s.bytes() is a view over the receiver's own storage), s78 (an affine relational channel in the range analysis), plus s53 (script mode) and the D43/D44 rulings. The corpus grows 13: 267 → 280 files. Ratchet 103 → 107, anchors 315 → 316, bundle 317 programs / 295 records.

0.1.10 — 2026-08-12

THE RUN TIER FINALLY COMPARES: the mid-end/whole-program re-pin. Pin bumped 0b4e79c → 613c3dc (latest green trunk, CI run 31651577695: the wave landed s42 (the mid-end optimizer), s43 (whole-program, clusters, body dedup, a frozen summary index) and s63 (diagnostics polish: 144 codes / 30 warnings, error cascades capped with --error-limit=N), plus the dv01 prose pass). The corpus grows 4: 263 → 267 files (conc/select_two_timeouts.lu, the #64 GVN cross-arm-dominance litmus, and the s42 kernel tier kernels/hot_counter.lu, kernels/hot_scale_versioned.lu, kernels/churn_b3.lu). All four reach run on this machine at first sight and match their check:, so the run ledger grows 4. Coverage ratchets 102 → 103: [conc.select.timeout] is covered for the first time, by the new select litmus alone. spec/ is UNCHANGED in this range (anchors stay 315), so no clause needed a new reading. Bundle 304 programs / 282 records.

0.1.9 — 2026-08-12

THE CONC TIER RUNS ON BOTH MACHINES: the c09-wave re-pin. Pin bumped 26fa98e → 0b4e79c (latest green trunk: the wave landed s41 (release tier), s51 (package manager), s73 (native concurrency) and four fmt idempotence fixes; trunk head 17ea078 is CI-red on both test lanes and was not taken). The corpus grows 1: 262 → 263 files (test/conc_schedules_test.lu, the s73 --schedules=N dogfood witness); nine files' headers advance to phase run (8× conc/ + procs.lu; wolfgang executes concurrency natively now, and this machine's run-ledger claims for all nine predate the wave, so both machines now execute the tier), memory/prov_holy_grail.lu moves typecheck → mem, and two conc files gain -> !int rows (E0604/D30). Anchors stay 315, ratchet stays 102; bundle 300 programs / 278 records.

0.1.8 — 2026-08-11

The v0.1.0 RELEASE PAIRING build: this is the version wolf 0.1.0's --version line names as its reference interpreter. Pin bumped e94b879 → 3d5cee6 (wave seven: r01 prep + s71 release polish; the corpus grows 5: the [mem.str.empty]/[mem.str.repeat] witnesses, the else |Tag(p)| pair, the ctfe fold witness; the spec grows [mem.str.empty], [mem.str.repeat] and §10 [gram.version] (grammar/1)). Then the ONE lawful mid-pass re-pin 3d5cee6 → 26fa98e when s72's mode teeth merged upstream (the corpus grows 3 more: the D39/D40/overlap fail-files memory/read_param_write.lu E1014, memory/mut_read_overlap.lu E1002, memory/list_mutate_while_iter.lu E1013; the spec grows [mem.iter.excl] and the trap map's E1013 row). Net: 254 → 262 files, 306 → 315 anchors, ratchet 97 → 102. The D39/D40 mirrors below were built on the rulings' authority (wolf planning 02-decisions.md, 2026-08-12) while s72 ran concurrently; the re-pin brought their spec text and fail-files into the pin, and all three fail-files pair with this machine's traps as dynamic counterparts (ledger::dynamic_meaning gains E1013/E1014 → exclusivity; the pinned map prose names E1013 but not yet E1014, a noted upstream-worthy nit).

0.1.7 — 2026-08-11

The release-runway pass for r01 (wolf v0.1.0 names this version as the reference interpreter). Pin bumped 13b811f → e94b879 (wave six: s40 os/time/json, s70 match tier + X3 value paths, s69 idiom lints; the corpus grows lints/ ×15, os/ ×4, json/ ×2, faults/ ×5, time/ ×1 and the match-tier witnesses: 221 → 254 files; the registry grows [proto.cmp.rung], 305 → 306 anchors, ratchet 96 → 97). Two issues closed, every open divergence family closed, the differential compares CLEAN.

0.1.6 — 2026-08-11

The wave-four re-pin and the lint wave (issues #19, #20). Pin bumped f0da6e6 → 13b811f (#41 capture law, #42 checked-lane determinism, s34 procs, s35 io reactor, s39/s40/#40 native str/List/fs, s68 lints: the corpus grows lints/ ×12, conc/ ×3, projects/ ×3, net/ ×2, comptime/ ×1, test/ ×1: 199 → 221 files; the registry grows [conc.chan.default] and [mem.region.freeze.4], 303 → 305 anchors, and the test namespace joins the reserved forward set). Two issues closed, one divergence family filed, one closed.

0.1.5 — 2026-08-11

The unsafe-tier batch (issue #18, the book's ch09 differential) and the five-lane re-pin. Pin bumped ad6cef7 → f0da6e6 (s32 tasks, s33 channels, s37 str core, s38 fmt/io/fs, s67 warnings: the corpus grows strings/ ×9, lints/ ×4, fs/ ×2, io/ ×1: 183 → 199 files; the registry grows the diag.* block, [mem.str.get] and [proto.record.warn]/[proto.cmp.warn], 290 → 303 anchors). One issue closed (#18, six items), three divergences filed.

0.1.4 — 2026-08-10

The held maintenance wave, released once s30 shipped upstream. Pin bumped d147a54 → ad6cef7 (s29+s30: the corpus grows memory/unsafe_c_alloc_native.lu, rows/eu_main_err_exit.lu, typecheck/float_nan_cmp.lu and resolve/same_name/: 177 → 183 files; anchors hold at 290; the two E0410 fail-files re-pin phase: resolve → parse). Four issues closed, one divergence resolved, one filed.

0.1.3 — 2026-08-10

The rows half, and the s27/s28 catch-up. Pin bumped a0c4564 → d147a54 (the corpus grows rows/qmark_defer.lu and faults/assert_msg_holds.lu: 175 → 177 files; the spec grows [mem.iter.*], [mem.str.*], [conf.trap.assert] and the postfix-row type grammar: 281 → 290 anchors). Three issues closed, one divergence resolved, one filed.

0.1.2 — 2026-08-10

The lupin maintenance pass: five filed issues, four fixed, one routed upstream. Pin bumped cbde620 → a0c4564 (the corpus grows the E0410 fail-files and the unsafe/checked memory tier: 164 → 175 files).

0.1.1 — 2026-08-10

The bs00x maintenance pass: four filed issues, three fixed, one routed upstream.

0.1.0 — 2026-08-10

The binary is named lupin; the package and repository stay wolf-interp.