<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>sonar, branch HEAD</title>
<subtitle>Scan files at memory bandwidth speed. </subtitle>
<entry>
<id>ea224f9aa3c3bade4472ab3060784c9ac6a268d1</id>
<published>2026-09-24T10:54:23Z</published>
<updated>2026-09-24T10:54:23Z</updated>
<title type="text">sonar: test every imported package in one run</title>
<link rel="alternate" type="text/html" href="commit/ea224f9aa3c3bade4472ab3060784c9ac6a268d1.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit ea224f9aa3c3bade4472ab3060784c9ac6a268d1
parent 6f4e2885b63de10a9fc2ff4a8128bb3f5e378d7b
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Thu, 24 Sep 2026 07:54:23 -0300

sonar: test every imported package in one run

`odin test . -all-packages` follows the imports, so the justfile no longer
lists packages by name and a new package, local or from the collection, is
tested the moment something imports it.

</content>
</entry>
<entry>
<id>6f4e2885b63de10a9fc2ff4a8128bb3f5e378d7b</id>
<published>2026-09-24T09:51:32Z</published>
<updated>2026-09-24T09:51:32Z</updated>
<title type="text">sonar: follow the collection rename from jfm to jm</title>
<link rel="alternate" type="text/html" href="commit/6f4e2885b63de10a9fc2ff4a8128bb3f5e378d7b.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 6f4e2885b63de10a9fc2ff4a8128bb3f5e378d7b
parent a266c75f41755ec3cb9350528d2440c31d04078c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Thu, 24 Sep 2026 06:51:32 -0300

sonar: follow the collection rename from jfm to jm

The collection now lives at ~/Source/Personal/jm and is imported as jm:;
the justfile override variable is JM to match.

</content>
</entry>
<entry>
<id>a266c75f41755ec3cb9350528d2440c31d04078c</id>
<published>2026-09-23T23:41:32Z</published>
<updated>2026-09-23T23:41:32Z</updated>
<title type="text">sonar: import flow from the jfm collection</title>
<link rel="alternate" type="text/html" href="commit/a266c75f41755ec3cb9350528d2440c31d04078c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit a266c75f41755ec3cb9350528d2440c31d04078c
parent 599c622fdbb6874898f7078730abe40199bc1ab5
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 23 Sep 2026 20:41:32 -0300

sonar: import flow from the jfm collection

The worker-pool shapes are not sonar-specific, so they now live in
~/Source/Personal/odin as jfm:flow beside the debug allocator, resolved by
path through the same JFM variable until sonar ships.

</content>
</entry>
<entry>
<id>599c622fdbb6874898f7078730abe40199bc1ab5</id>
<published>2026-09-23T19:26:17Z</published>
<updated>2026-09-23T19:26:17Z</updated>
<title type="text">sonar: import the debug allocator from the jfm collection</title>
<link rel="alternate" type="text/html" href="commit/599c622fdbb6874898f7078730abe40199bc1ab5.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 599c622fdbb6874898f7078730abe40199bc1ab5
parent 620f76b84c78cee762f8f3d100051ecf62398cc6
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 23 Sep 2026 16:26:17 -0300

sonar: import the debug allocator from the jfm collection

The allocator now lives in ~/Source/Personal/odin as jfm:debug, where
prelude and other tools share it. Keeping a copy here had already drifted
by 22 lines. The justfile resolves the collection by path (JFM overrides)
until sonar is ready to ship, when a pinned submodule replaces the path.

</content>
</entry>
<entry>
<id>620f76b84c78cee762f8f3d100051ecf62398cc6</id>
<published>2026-09-23T17:59:00Z</published>
<updated>2026-09-23T17:59:00Z</updated>
<title type="text">sonar: replace makefile with justfile</title>
<link rel="alternate" type="text/html" href="commit/620f76b84c78cee762f8f3d100051ecf62398cc6.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 620f76b84c78cee762f8f3d100051ecf62398cc6
parent 0e96291b65c4c9de2ef9a858cf45d5fe85349977
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 23 Sep 2026 14:59:00 -0300

sonar: replace makefile with justfile

The recipes are the same: ASan debug build, optimised release, per-package
tests, check, clean, plus the standard install. What goes is make&#39;s
timestamp-based skipping of an up-to-date binary; an Odin build of this size
is quick enough that a rebuild on every call costs less than the second
build system it took to avoid it.

</content>
</entry>
<entry>
<id>0e96291b65c4c9de2ef9a858cf45d5fe85349977</id>
<published>2026-09-20T19:43:22Z</published>
<updated>2026-09-20T19:43:22Z</updated>
<title type="text">main: charge the tree once, however it was read</title>
<link rel="alternate" type="text/html" href="commit/0e96291b65c4c9de2ef9a858cf45d5fe85349977.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 0e96291b65c4c9de2ef9a858cf45d5fe85349977
parent 262badd3caa0e0906782e324cd98f02de2a276df
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 16:43:22 -0300

main: charge the tree once, however it was read

The batch path had its own roll-up beside the incremental one the builder uses.
Two answers to &quot;what does this directory hold&quot;, agreeing only because their output
was compared, and differing in a detail nobody chose: the batch copy charged a
node whose parent never resolved, the incremental one waits for a settled chain.

Batch now runs the same charging, once, over a tree nobody is still filling.
Neither engine&#39;s output moves, on /usr, /home/jfm or a 491 GiB volume. Ranking is
bounded by what has been charged rather than by the tree, which is what the live
path already did.

</content>
</entry>
<entry>
<id>262badd3caa0e0906782e324cd98f02de2a276df</id>
<published>2026-09-20T19:27:24Z</published>
<updated>2026-09-20T19:27:44Z</updated>
<title type="text">scan: move the snapshot to the package that uses it</title>
<link rel="alternate" type="text/html" href="commit/262badd3caa0e0906782e324cd98f02de2a276df.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 262badd3caa0e0906782e324cd98f02de2a276df
parent a55548c334572566dbf89c145f657909cae99366
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 16:27:24 -0300

scan: move the snapshot to the package that uses it

Nothing in scan wrote a snapshot, read one, or mentioned one. The builder is its
only writer and the drawing loop its only reader, and both are in main, so the
seqlock now sits between them rather than in the package they happen to share.

TOP_ROWS and TOP_N were the same twenty written twice, free to drift apart; there
is one of them now. The two seqlock tests move with it, and `make test` gains the
root package, which had none at all despite holding the ranking and the roll-up.

</content>
</entry>
<entry>
<id>a55548c334572566dbf89c145f657909cae99366</id>
<published>2026-09-20T19:18:41Z</published>
<updated>2026-09-20T19:18:41Z</updated>
<title type="text">main: reach the tree through the fill that is filling it</title>
<link rel="alternate" type="text/html" href="commit/a55548c334572566dbf89c145f657909cae99366.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit a55548c334572566dbf89c145f657909cae99366
parent cf8be8d72882a8164f7685ce206f6773d1af058f
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 16:18:41 -0300

main: reach the tree through the fill that is filling it

run_live took a tree beside the fill that already pointed at it, so there were two
ways to name the same thing and nothing keeping them the same one. The parameter
was left over from moving the reader out of this file.

The two file-private markers in here become package-private as well, finishing the
sweep begun a couple of commits ago; that half was already in the working tree.

</content>
</entry>
<entry>
<id>cf8be8d72882a8164f7685ce206f6773d1af058f</id>
<published>2026-09-20T19:06:05Z</published>
<updated>2026-09-20T19:06:05Z</updated>
<title type="text">walk: name the entry point for what it reads</title>
<link rel="alternate" type="text/html" href="commit/cf8be8d72882a8164f7685ce206f6773d1af058f.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit cf8be8d72882a8164f7685ce206f6773d1af058f
parent 5e1620f451e18659038ee547fb0e2e4dedc99b12
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 16:06:05 -0300

walk: name the entry point for what it reads

`scan` collided with the package of the same name that it writes into, to the
point that the tests had to import that package under an alias to keep the name
free. `read` sits beside ntfs.read_mft and leaves the collision behind.

Its docstring described the concurrency shape it happens to be built on rather
than what calling it does. It now says what the tree holds afterwards and what
each error means for what is in it.

</content>
</entry>
<entry>
<id>5e1620f451e18659038ee547fb0e2e4dedc99b12</id>
<published>2026-09-20T19:02:46Z</published>
<updated>2026-09-20T19:02:46Z</updated>
<title type="text">scan: widen file-private helpers to the package</title>
<link rel="alternate" type="text/html" href="commit/5e1620f451e18659038ee547fb0e2e4dedc99b12.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 5e1620f451e18659038ee547fb0e2e4dedc99b12
parent e247b0cba247a06b478e752df7ca44a0c07b681e
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 16:02:46 -0300

scan: widen file-private helpers to the package

A file-private symbol is one the language server will not follow from another
file, and these are read from other files more often than they are hidden from
them. target_windows.odin already marked its helpers package-private; the one
beside them that did not was the one I had added.

Nothing here becomes visible outside its package. The mirror files keep sharing
names such as can_open, since their build tags already make only one of them real.

</content>
</entry>
<entry>
<id>e247b0cba247a06b478e752df7ca44a0c07b681e</id>
<published>2026-09-20T18:54:24Z</published>
<updated>2026-09-20T18:54:24Z</updated>
<title type="text">main: run a reader from one place</title>
<link rel="alternate" type="text/html" href="commit/e247b0cba247a06b478e752df7ca44a0c07b681e.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit e247b0cba247a06b478e752df7ca44a0c07b681e
parent 60efb2ca4d52b4907e976ebe1c948315dcf570bd
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 15:54:24 -0300

main: run a reader from one place

Driving a reader was written twice, once for the batch path and once for the live
one, which left a file about drawing frames holding an MFT read loop. The two
agreed, but had already drifted on a point worth not drifting on: the batch path
named the root after folding rather than inside the projection, which is what once
let a watcher catch the table&#39;s &quot;.&quot;.

fill.odin owns it now. Whether the tree is folded as it fills or once at the end
is the only difference left between a watched scan and an unwatched one, and
live.odin imports scan and nothing else.

</content>
</entry>
<entry>
<id>60efb2ca4d52b4907e976ebe1c948315dcf570bd</id>
<published>2026-09-20T18:11:51Z</published>
<updated>2026-09-20T18:11:51Z</updated>
<title type="text">main: let any reader fill a live scan</title>
<link rel="alternate" type="text/html" href="commit/60efb2ca4d52b4907e976ebe1c948315dcf570bd.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 60efb2ca4d52b4907e976ebe1c948315dcf570bd
parent 564dc3c0f4b56980178e084cb56e54f8b85adbe9
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 15:11:51 -0300

main: let any reader fill a live scan

The builder folded an ntfs.Mft itself and charged from the projection&#39;s list of
what had settled, so watching a scan only ever worked for one reader. It now
charges whatever the tree says is settled and knows nothing else; live.odin picks
a filler with the same switch the batch path already uses, and a walk can be
watched.

Watching a tree means it has to be safe to watch, which it was not: readers now
publish a node&#39;s flags last and atomically, `slots` stops at what is both claimed
and backed by a block, and hard links are folded only once the table says nothing
more is coming. Each of those raced the moment a second thread read the tree.

</content>
</entry>
<entry>
<id>564dc3c0f4b56980178e084cb56e54f8b85adbe9</id>
<published>2026-09-20T17:47:24Z</published>
<updated>2026-09-20T17:47:24Z</updated>
<title type="text">scan: choose a reader from whether the volume opens</title>
<link rel="alternate" type="text/html" href="commit/564dc3c0f4b56980178e084cb56e54f8b85adbe9.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 564dc3c0f4b56980178e084cb56e54f8b85adbe9
parent 956dcddfe266dbecb479a07c5901848031460c80
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:47:24 -0300

scan: choose a reader from whether the volume opens

`elevated` asked about the process, which is the wrong question and only Windows
could answer it. Off Windows it returned a constant true, justified by a comment
reasoning about what `resolve` had already done. Being in the group that owns a
device grants the read without root, so the honest question is whether this
volume opens, which also covers one that is missing or locked.

`resolve` finds that out and now returns the reader with the target, since it is
the only thing that knew. Choosing stays a separate procedure so the decision
table can still be tested without a disk, but nothing outside needs to call it.

</content>
</entry>
<entry>
<id>956dcddfe266dbecb479a07c5901848031460c80</id>
<published>2026-09-20T17:26:05Z</published>
<updated>2026-09-20T17:26:05Z</updated>
<title type="text">main: parse the command line with core:flags</title>
<link rel="alternate" type="text/html" href="commit/956dcddfe266dbecb479a07c5901848031460c80.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 956dcddfe266dbecb479a07c5901848031460c80
parent d8d1bb6166350dff4811a0c6e942ca9e10ad1b21
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:26:05 -0300

main: parse the command line with core:flags

Every flag was matched by hand against a prefix, which meant the usage line was
written separately from the flags it described and drifted from them twice: once
losing --walk-workers entirely, once keeping a default the code no longer used.

A struct with usage tags is the one description now, and the help text is printed
from it. The flags, their names and the exit codes are unchanged; UNIX style also
accepts `--flag value` beside `--flag=value`, which the old parser refused. The
home directory default moves to the temp allocator so it is freed with everything
else.

</content>
</entry>
<entry>
<id>d8d1bb6166350dff4811a0c6e942ca9e10ad1b21</id>
<published>2026-09-20T17:22:57Z</published>
<updated>2026-09-20T17:22:57Z</updated>
<title type="text">main: say what TOP_N counts</title>
<link rel="alternate" type="text/html" href="commit/d8d1bb6166350dff4811a0c6e942ca9e10ad1b21.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit d8d1bb6166350dff4811a0c6e942ca9e10ad1b21
parent f5d01177941a03e44cec53242f009b9f9a218206
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:22:57 -0300

main: say what TOP_N counts

</content>
</entry>
<entry>
<id>f5d01177941a03e44cec53242f009b9f9a218206</id>
<published>2026-09-20T17:17:57Z</published>
<updated>2026-09-20T17:17:57Z</updated>
<title type="text">flow: stop the manage tests racing on one thread&#39;s arena</title>
<link rel="alternate" type="text/html" href="commit/f5d01177941a03e44cec53242f009b9f9a218206.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit f5d01177941a03e44cec53242f009b9f9a218206
parent f9291789afacefe4e45359a117b4eeac39a009a9
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:17:57 -0300

flow: stop the manage tests racing on one thread&#39;s arena

A dynamic array remembers the allocator it was made with. The worker states were
built on the calling thread&#39;s temp allocator, which is a per-thread arena with no
lock, and then appended to from every worker at once. The arena was being grown
from eight threads.

It miscounted about once in twelve runs, reporting 20,064 visits to a tree of
20,000, which reads as manage handing an item out twice. Fifty runs on the heap
allocator have not reproduced it. The states are freed explicitly now, since they
no longer go when the temp allocator is reset.

</content>
</entry>
<entry>
<id>f9291789afacefe4e45359a117b4eeac39a009a9</id>
<published>2026-09-20T17:15:47Z</published>
<updated>2026-09-20T17:15:47Z</updated>
<title type="text">style: run odinfmt over the repository</title>
<link rel="alternate" type="text/html" href="commit/f9291789afacefe4e45359a117b4eeac39a009a9.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit f9291789afacefe4e45359a117b4eeac39a009a9
parent 63847c8b4b9d2a2ee39d6042e527be24c4f48ae2
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:15:47 -0300

style: run odinfmt over the repository

Every file now matches the configuration beside it, so a formatted change shows
only what it changed. Formatting only: the release binary&#39;s output over /usr,
/usr/share/doc and /home/jfm is unchanged line for line, the tests pass, and a
second pass moves nothing.

debug.odin accounts for most of it. It was written well past a hundred columns
and had never been through a formatter.

</content>
</entry>
<entry>
<id>63847c8b4b9d2a2ee39d6042e527be24c4f48ae2</id>
<published>2026-09-20T17:11:30Z</published>
<updated>2026-09-20T17:13:16Z</updated>
<title type="text">build: settle the formatting rules in the repository</title>
<link rel="alternate" type="text/html" href="commit/63847c8b4b9d2a2ee39d6042e527be24c4f48ae2.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 63847c8b4b9d2a2ee39d6042e527be24c4f48ae2
parent 17d958e303f8ee131ba092bba12e64b3cafcecf2
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:11:30 -0300

build: settle the formatting rules in the repository

odinfmt looks for its configuration beside the file it is formatting, not beside
itself, so with nothing here every contributor was formatting to whatever their
own defaults happened to be.

The width is the one this code was already written to: of 6862 lines, 71 pass a
hundred columns and those are foreign declarations that cannot be broken.
Constant alignment is on because the code aligns them by hand in fifteen places,
and the default would have pulled every one of those apart. LF is stated because
odinfmt&#39;s own default is CRLF.

</content>
</entry>
<entry>
<id>17d958e303f8ee131ba092bba12e64b3cafcecf2</id>
<published>2026-09-20T17:09:12Z</published>
<updated>2026-09-20T17:09:12Z</updated>
<title type="text">style: run odinfmt over the files this branch touched</title>
<link rel="alternate" type="text/html" href="commit/17d958e303f8ee131ba092bba12e64b3cafcecf2.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 17d958e303f8ee131ba092bba12e64b3cafcecf2
parent c853f8bfac1302ef74c28630be418a1e7864dcd4
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 14:09:12 -0300

style: run odinfmt over the files this branch touched

Formatting only, no behaviour: the binary&#39;s output over /usr and /usr/share/doc
is unchanged line for line, and the tests pass as before.

Only the files this branch already changed. The repository is not odinfmt-clean
as a whole, and rewriting the twenty-odd untouched files would bury real changes
in alignment churn. Some of what is here is that older drift rather than this
branch&#39;s, since a formatter takes the whole file or none of it.

</content>
</entry>
<entry>
<id>c853f8bfac1302ef74c28630be418a1e7864dcd4</id>
<published>2026-09-20T12:49:51Z</published>
<updated>2026-09-20T12:49:51Z</updated>
<title type="text">build: run the walk tests</title>
<link rel="alternate" type="text/html" href="commit/c853f8bfac1302ef74c28630be418a1e7864dcd4.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit c853f8bfac1302ef74c28630be418a1e7864dcd4
parent f2ded2b7f07f5ce56156cc841a8acb74bba14244
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:49:51 -0300

build: run the walk tests

`make test` named the three packages that had tests when it was written. walk has
them now, and a test suite that is not in the target that runs them is one nobody
runs.

</content>
</entry>
<entry>
<id>f2ded2b7f07f5ce56156cc841a8acb74bba14244</id>
<published>2026-09-20T12:49:39Z</published>
<updated>2026-09-20T12:49:39Z</updated>
<title type="text">walk: test what a walk records</title>
<link rel="alternate" type="text/html" href="commit/f2ded2b7f07f5ce56156cc841a8acb74bba14244.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit f2ded2b7f07f5ce56156cc841a8acb74bba14244
parent 90ccc9b0f4c1ab0d69919657d991d6b4fa842730
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:49:39 -0300

walk: test what a walk records

The package had no tests, which is how the linux reader shipped in draft opening
directories with NOFOLLOW: correct for a directory, and a refusal of exactly the
symlinks the caller had asked to follow.

One fixture, walked both ways, pins what a node means: every entry recorded once,
directories marked, a symlink left as a reparse until following is asked for, and
a cancelled walk stopping. The same tests pass against either reader, which is
what the seam is for. Allocation is the one answer that differs, so the hole is
tested where the exact answer lives.

</content>
</entry>
<entry>
<id>90ccc9b0f4c1ab0d69919657d991d6b4fa842730</id>
<published>2026-09-20T12:46:13Z</published>
<updated>2026-09-20T12:49:28Z</updated>
<title type="text">walk: read directories with linux syscalls</title>
<link rel="alternate" type="text/html" href="commit/90ccc9b0f4c1ab0d69919657d991d6b4fa842730.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 90ccc9b0f4c1ab0d69919657d991d6b4fa842730
parent b612a0a8d351cbd4631354ede35479e696a8d266
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:46:13 -0300

walk: read directories with linux syscalls

core:os describes every entry in full: an fd opened and closed around it, a statx
on that fd, and the entry&#39;s path read back through /proc/self/fd. Four syscalls
where one statx against the directory answers everything the tree records.
Walking 14,478 entries cost 64,235 calls against du&#39;s 21,511, and the procfs
readlink was over half of it. It now costs 18,122, fewer than du. /usr falls from
272 ms to 83 ms at eight workers and 1560 to 480 at one; eight is still the knee.

statx reports allocated blocks as readily as length, so the on-disk figure is
exact rather than the length rounded to a guessed block. A 1 GiB sparse file was
charged 1 GiB and is now charged what it occupies, agreeing with du.

</content>
</entry>
<entry>
<id>b612a0a8d351cbd4631354ede35479e696a8d266</id>
<published>2026-09-20T12:45:31Z</published>
<updated>2026-09-20T12:45:51Z</updated>
<title type="text">walk: read a directory behind a seam</title>
<link rel="alternate" type="text/html" href="commit/b612a0a8d351cbd4631354ede35479e696a8d266.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit b612a0a8d351cbd4631354ede35479e696a8d266
parent f58720b55051a177b5cdfd15d835a37e170faeb0
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:45:31 -0300

walk: read a directory behind a seam

Enumerating one directory is the only part of walking that differs by platform,
and it was welded to the part that does not: claiming a slot, interning the name,
setting the flags. Nothing could be specialised without duplicating the meaning
of a node alongside it.

`read_dir` is now the whole of the platform&#39;s job and `record` the whole of the
tree&#39;s, so a second reader cannot quietly disagree about what a node is. No
behaviour changes here: over /usr, /usr/share/doc and /home/jfm the output is
identical line for line apart from the time it reports.

</content>
</entry>
<entry>
<id>f58720b55051a177b5cdfd15d835a37e170faeb0</id>
<published>2026-09-20T12:08:00Z</published>
<updated>2026-09-20T12:08:00Z</updated>
<title type="text">scan: resolve a target on unix</title>
<link rel="alternate" type="text/html" href="commit/f58720b55051a177b5cdfd15d835a37e170faeb0.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit f58720b55051a177b5cdfd15d835a37e170faeb0
parent 5eb2ae2863549b9b3d5db84505618b56fc819969
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:08:00 -0300

scan: resolve a target on unix

The reader could run here but nothing could name a target for it, so the binary
refused every path with Unsupported_Platform. A device or an image is identified
by the signature at the front of it, a directory by the mount table; naming a
filesystem is this layer&#39;s job and stays free of every backend.

`choose` now turns down a volume it could not name rather than offering the
walker, which had reported an empty tree as a successful scan of a text file. An
NTFS image needs no elevation either: it is a file, and opening it is the whole
of the permission.

Scanning /dev/nvme0n1p3 end to end: 2,021,620 records in 1.4 s, $Bitmap agreeing
to 0.00003%.

</content>
</entry>
<entry>
<id>5eb2ae2863549b9b3d5db84505618b56fc819969</id>
<published>2026-09-20T12:07:48Z</published>
<updated>2026-09-20T12:07:48Z</updated>
<title type="text">scan: name the top of a target and the path below it</title>
<link rel="alternate" type="text/html" href="commit/5eb2ae2863549b9b3d5db84505618b56fc819969.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 5eb2ae2863549b9b3d5db84505618b56fc819969
parent 295a060f196d345d4b0a1c85fa8f81d16590a842
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:07:48 -0300

scan: name the top of a target and the path below it

main built the path of a subtree by trimming a trailing separator off the mount
point and concatenating, in two places, with a third spelling for the banner. The
banner&#39;s was wrong: it printed the subtree alone, so scanning /usr/share/doc
announced usr/share/doc.

`top` and `location` answer both from the target itself, and cover a volume that
is mounted nowhere by naming the volume instead. That case had rendered as an
empty string.

</content>
</entry>
<entry>
<id>295a060f196d345d4b0a1c85fa8f81d16590a842</id>
<published>2026-09-20T12:07:27Z</published>
<updated>2026-09-20T12:07:27Z</updated>
<title type="text">scan: build paths with the host&#39;s separator</title>
<link rel="alternate" type="text/html" href="commit/295a060f196d345d4b0a1c85fa8f81d16590a842.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 295a060f196d345d4b0a1c85fa8f81d16590a842
parent e226b37980469783df491a048e17799b366160e2
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 09:07:27 -0300

scan: build paths with the host&#39;s separator

Paths were joined with a backslash whatever was running, which the walker made
visible as soon as it ran anywhere else: a scan of /usr/share/doc reported
/usr/share/doc\ghostscript\Ghostscript.pdf.

A path is written for the machine showing it rather than for the filesystem it
came from, so an NTFS volume read from unix still prints something the shell
there would take back. The test spelled a Windows path out; it now builds one.

</content>
</entry>
<entry>
<id>e226b37980469783df491a048e17799b366160e2</id>
<published>2026-09-20T11:33:55Z</published>
<updated>2026-09-20T11:33:55Z</updated>
<title type="text">ntfs: test the unix volume against an image file</title>
<link rel="alternate" type="text/html" href="commit/e226b37980469783df491a048e17799b366160e2.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit e226b37980469783df491a048e17799b366160e2
parent c8e5117b684a722c6ce7c10d5954dad3b3f81bee
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 08:33:55 -0300

ntfs: test the unix volume against an image file

An image opens the same way a block device does, so a file stands in for one and
the tests need neither a device nor the rights to read it.

They cover what the reader depends on and nothing else: bytes land where asked
from four offset shapes, a read running off the end reports Short_Read rather
than leaving the tail of the buffer to parse as records, and closing a clone
leaves the handle it came from readable. Both halves were checked by breaking
them first.

</content>
</entry>
<entry>
<id>c8e5117b684a722c6ce7c10d5954dad3b3f81bee</id>
<published>2026-09-20T11:32:19Z</published>
<updated>2026-09-20T11:32:19Z</updated>
<title type="text">ntfs: read volumes on unix</title>
<link rel="alternate" type="text/html" href="commit/c8e5117b684a722c6ce7c10d5954dad3b3f81bee.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit c8e5117b684a722c6ce7c10d5954dad3b3f81bee
parent 5097ba0a660e13247abb1fa0c29c2d56aa6e34a1
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 08:32:19 -0300

ntfs: read volumes on unix

Everything above the volume layer was already portable; only the four procedures
that touch a handle were not, so the reader could be compiled off Windows but
never run. Positional reads through core:os supply them.

Reading /dev/nvme0n1p3, a 491 GiB NTFS volume, takes 1411 ms for 2,021,620
records with none unreadable, against 1357 ms for the same work on Windows. Run
lists account for 470,901,706,752 bytes and $Bitmap marks 470,901,846,016, a gap
of 0.00003%.

Reads go through the page cache. O_DIRECT would want per-platform handling for no
throughput the Windows sweep could measure.

</content>
</entry>
<entry>
<id>5097ba0a660e13247abb1fa0c29c2d56aa6e34a1</id>
<published>2026-09-20T11:29:25Z</published>
<updated>2026-09-20T11:29:25Z</updated>
<title type="text">walk: drop the allocator nobody reads</title>
<link rel="alternate" type="text/html" href="commit/5097ba0a660e13247abb1fa0c29c2d56aa6e34a1.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 5097ba0a660e13247abb1fa0c29c2d56aa6e34a1
parent 3e69628b753954fae92418a20c64a834148d0b6c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 08:29:25 -0300

walk: drop the allocator nobody reads

Config carried an allocator field that `scan` never consulted; it took
context.allocator and passed that everywhere. Offering a knob that does nothing
is worse than not offering one.

It cannot be honoured as things stand either: the discard callback that frees
unwalked directories takes no state, so it can only reach the context allocator.
Wiring the field up would strand those paths again.

</content>
</entry>
<entry>
<id>3e69628b753954fae92418a20c64a834148d0b6c</id>
<published>2026-09-20T11:26:58Z</published>
<updated>2026-09-20T11:26:58Z</updated>
<title type="text">walk: drop the error a directory that will not open never raises</title>
<link rel="alternate" type="text/html" href="commit/3e69628b753954fae92418a20c64a834148d0b6c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 3e69628b753954fae92418a20c64a834148d0b6c
parent 03b698ff54d3b59dad8785d9d6076337f35a81c8
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 08:26:58 -0300

walk: drop the error a directory that will not open never raises

Nothing has assigned Open_Failed since the manager became the one thing that ends
a scan. An unreadable directory is skipped and the volume carries on, which is
the intended behaviour, so the variant only suggested a failure mode the package
does not have.

Out_Of_Memory and Cancelled renumber. No consumer switches on this enum.

</content>
</entry>
<entry>
<id>03b698ff54d3b59dad8785d9d6076337f35a81c8</id>
<published>2026-09-20T10:44:43Z</published>
<updated>2026-09-20T10:44:43Z</updated>
<title type="text">main: refuse an option it does not understand</title>
<link rel="alternate" type="text/html" href="commit/03b698ff54d3b59dad8785d9d6076337f35a81c8.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 03b698ff54d3b59dad8785d9d6076337f35a81c8
parent 05918e2de6bea36e9679c983aa138feb6bf1e831
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:44:43 -0300

main: refuse an option it does not understand

`--walk-workers=N` was accepted but never listed, and everything else a dash
could start fell through to the target, so `--lives` was scanned for as a volume
and reported as a missing one. A value that did not parse was dropped in silence,
which is worse in a tool whose flags exist to be swept: a mistyped width reads as
a result for the default.

Both now stop the run and say which argument was wrong.

</content>
</entry>
<entry>
<id>05918e2de6bea36e9679c983aa138feb6bf1e831</id>
<published>2026-09-20T10:43:50Z</published>
<updated>2026-09-20T10:43:50Z</updated>
<title type="text">main: stop a parent chain that leaves the tree</title>
<link rel="alternate" type="text/html" href="commit/05918e2de6bea36e9679c983aa138feb6bf1e831.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 05918e2de6bea36e9679c983aa138feb6bf1e831
parent 183118dae0d139cd30192c990ffa06f3839385a5
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:43:50 -0300

main: stop a parent chain that leaves the tree

Rolling up in batch and rebuilding a path both stop when a parent points past the
last slot; charging a node incrementally did not. Every reachable parent is
clamped upstream today, so this is a guard rather than a repair, but it is the
one walk of the three that runs against a tree a reader is still filling, and it
indexes a slice sized to the count it does not check.

</content>
</entry>
<entry>
<id>183118dae0d139cd30192c990ffa06f3839385a5</id>
<published>2026-09-20T10:43:37Z</published>
<updated>2026-09-20T10:43:37Z</updated>
<title type="text">main: stop emptying the tree under the thread reading it</title>
<link rel="alternate" type="text/html" href="commit/183118dae0d139cd30192c990ffa06f3839385a5.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 183118dae0d139cd30192c990ffa06f3839385a5
parent a2e9d3f38ef8770ee7d2407055dfb60b83112a5c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:43:37 -0300

main: stop emptying the tree under the thread reading it

The final pass zeroed the node count before reprojecting, and the UI thread walks
parent links out of the same tree to render the paths of the rows it drew. A
count of zero makes every parent look out of range, so for the width of that pass
the loop printed a bare name instead of a path, once per live run.

Extra names now land on fixed slots, so the reprojection overwrites in place and
the count never has to move.

</content>
</entry>
<entry>
<id>a2e9d3f38ef8770ee7d2407055dfb60b83112a5c</id>
<published>2026-09-20T10:43:00Z</published>
<updated>2026-09-20T10:43:00Z</updated>
<title type="text">ntfs: put extra names at a fixed slot so a replay repeats itself</title>
<link rel="alternate" type="text/html" href="commit/a2e9d3f38ef8770ee7d2407055dfb60b83112a5c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit a2e9d3f38ef8770ee7d2407055dfb60b83112a5c
parent b65b98b0476133970c7bc70285c076776b8f5c62
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:43:00 -0300

ntfs: put extra names at a fixed slot so a replay repeats itself

Extra names claimed the next free slot, so replaying a projection appended a
second node for every hard link unless the caller emptied the tree first. That
requirement is what forced the builder to zero the node count mid-scan, in full
view of a thread resolving paths from it.

Sizing for records and links together, and seating each link at its ordinal above
the record range, makes a repeated projection write the same slots as the pass
before it. Nothing has to be emptied, and the claim goes with it.

</content>
</entry>
<entry>
<id>b65b98b0476133970c7bc70285c076776b8f5c62</id>
<published>2026-09-20T10:42:03Z</published>
<updated>2026-09-20T10:42:03Z</updated>
<title type="text">main: free the table when projecting it fails</title>
<link rel="alternate" type="text/html" href="commit/b65b98b0476133970c7bc70285c076776b8f5c62.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit b65b98b0476133970c7bc70285c076776b8f5c62
parent d9269bfe5f0a6e8c089176269eff2f9d75c9dd64
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:42:03 -0300

main: free the table when projecting it fails

The defer that destroys the table sat below the switch that fills it, so the
early return from a failed `to_tree` skipped it and dropped a whole MFT. The
projection&#39;s first act is to reserve a node per record slot, which is exactly
where an out-of-memory lands, so the path that leaks most is the one most likely
to be taken.

Registering it beside the declaration covers every return instead. Ordering is
unchanged: it still runs before the tree is destroyed, and the table still
outlives the names the tree borrows from it.

</content>
</entry>
<entry>
<id>d9269bfe5f0a6e8c089176269eff2f9d75c9dd64</id>
<published>2026-09-20T10:41:32Z</published>
<updated>2026-09-20T10:41:32Z</updated>
<title type="text">walk: free the directories a stopped scan never reached</title>
<link rel="alternate" type="text/html" href="commit/d9269bfe5f0a6e8c089176269eff2f9d75c9dd64.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit d9269bfe5f0a6e8c089176269eff2f9d75c9dd64
parent 6319df6d084a88159255796eaffd6a8d75c1b5eb
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:41:32 -0300

walk: free the directories a stopped scan never reached

A worker frees the path it is handed, but a scan called off mid-volume leaves the
rest of the queue untouched, and those paths were owned by nobody once `manage`
dropped the queue. Cancelling a walk of /usr after 40 ms stranded 910
allocations, 17 KB; the whole volume would strand far more, and a UI that offers
a cancel button pays it every time.

`drop` is the discard half of the contract `manage` now states. It runs on the
thread that called `scan`, where the paths were cloned, so it frees them from the
allocator that made them.

</content>
</entry>
<entry>
<id>6319df6d084a88159255796eaffd6a8d75c1b5eb</id>
<published>2026-09-20T10:40:30Z</published>
<updated>2026-09-20T10:40:30Z</updated>
<title type="text">flow: hand back the work a stopped run never took</title>
<link rel="alternate" type="text/html" href="commit/6319df6d084a88159255796eaffd6a8d75c1b5eb.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 6319df6d084a88159255796eaffd6a8d75c1b5eb
parent 8059c606ef926a920089a0a4fe36de15e610ad7b
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:40:30 -0300

flow: hand back the work a stopped run never took

A manager that ends the run leaves items queued, and the queue is local to
`manage`, so an item owning memory leaked with no way for the caller to reach it.
Stopping after 100 of a branching workload leaked 101 allocations, one per item
still waiting.

`discard` closes the contract: every item reaches `work` or `discard`, never both
and never neither. It defaults to nil, which is right for an item that owns
nothing. Sweeping is deferred so it covers the inline path and the pooled one at
once, and runs while the queue is still alive.

</content>
</entry>
<entry>
<id>8059c606ef926a920089a0a4fe36de15e610ad7b</id>
<published>2026-09-20T10:39:23Z</published>
<updated>2026-09-20T10:39:23Z</updated>
<title type="text">scan: answer the elevation question on every platform</title>
<link rel="alternate" type="text/html" href="commit/8059c606ef926a920089a0a4fe36de15e610ad7b.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 8059c606ef926a920089a0a4fe36de15e610ad7b
parent 33cca93aefa426d04d9950c42baa0b5c2f9dbebf
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Sun, 20 Sep 2026 07:39:23 -0300

scan: answer the elevation question on every platform

`elevated` was declared only in the Windows half of the package, but main calls
it unconditionally, so the tree did not compile for Linux or macOS at all. The
build was green only because nobody had pointed the compiler anywhere else.

Nothing here can reach a volume yet either way, since `resolve` still returns
Unsupported_Platform off Windows. Whoever implements that implements this beside
it: on unix the answer is file permissions on the device, not a process token.

</content>
</entry>
<entry>
<id>33cca93aefa426d04d9950c42baa0b5c2f9dbebf</id>
<published>2026-09-18T20:57:26Z</published>
<updated>2026-09-18T20:57:26Z</updated>
<title type="text">flow: let the manager judge a failed item</title>
<link rel="alternate" type="text/html" href="commit/33cca93aefa426d04d9950c42baa0b5c2f9dbebf.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 33cca93aefa426d04d9950c42baa0b5c2f9dbebf
parent 2c30a3334f018f8de99f3dca9e59f88c7a5a2143
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 16:57:26 -0400

flow: let the manager judge a failed item

`manage` gave `work` one boolean for two unrelated answers: this item did not
work out, and abandon the whole run. Callers had to pick one. The walker picked
the first, reporting success for a directory it could not open, and so had no
way to report running out of room for the tree: it halted and returned .None,
losing a truncated scan silently.

`work` returning false now marks only the item. The manager is told which item
it was and what happened, and decides: skip it, queue it again, or stop. That
is flowmatic&#39;s split, which our version had collapsed.

</content>
</entry>
<entry>
<id>2c30a3334f018f8de99f3dca9e59f88c7a5a2143</id>
<published>2026-09-18T20:57:14Z</published>
<updated>2026-09-18T20:57:14Z</updated>
<title type="text">walk: free a directory path when the scan is cancelled</title>
<link rel="alternate" type="text/html" href="commit/2c30a3334f018f8de99f3dca9e59f88c7a5a2143.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 2c30a3334f018f8de99f3dca9e59f88c7a5a2143
parent 69a16c1b504e0c21eaa0ba7d999b5ce077b32eea
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 16:57:14 -0400

walk: free a directory path when the scan is cancelled

The early return for cancellation sat above the defer that frees the path,
so every directory in flight at the moment of a cancel leaked its path.
Moving the defer up covers that exit too.

</content>
</entry>
<entry>
<id>69a16c1b504e0c21eaa0ba7d999b5ce077b32eea</id>
<published>2026-09-18T17:21:29Z</published>
<updated>2026-09-18T17:21:29Z</updated>
<title type="text">walk: traverse with manage instead of rounds</title>
<link rel="alternate" type="text/html" href="commit/69a16c1b504e0c21eaa0ba7d999b5ce077b32eea.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 69a16c1b504e0c21eaa0ba7d999b5ce077b32eea
parent 46562ae97ab8c0142e5930c97d3115bd8743418a
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 13:21:29 -0400

walk: traverse with manage instead of rounds

Walking a level at a time meant the pool idled at every level boundary waiting
for its slowest directory, and a deep narrow tree spends most of its time
there. Directories now go through one queue and a worker takes the next as
soon as it is known.

On System32, warm, four workers go from 234 ms to 206 and eight from 251 to
207. The barrier is why more workers used to make it worse; sixteen now costs
the same as four rather than more. A worker also frees a directory&#39;s path once
it has walked it, which the round loop did in bulk.

</content>
</entry>
<entry>
<id>46562ae97ab8c0142e5930c97d3115bd8743418a</id>
<published>2026-09-18T17:21:28Z</published>
<updated>2026-09-18T17:21:28Z</updated>
<title type="text">flow: add the manager shape for work that discovers itself</title>
<link rel="alternate" type="text/html" href="commit/46562ae97ab8c0142e5930c97d3115bd8743418a.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 46562ae97ab8c0142e5930c97d3115bd8743418a
parent 65178051fd1bbb812c06eba5a065631e2769d3d8
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 13:21:28 -0400

flow: add the manager shape for work that discovers itself

each needs its work known up front, so a traversal had to be run in rounds:
walk a level, collect what it found, walk the next. Every round ends at a
barrier where the pool waits for its slowest member, and starts its threads
again.

manage keeps one queue instead. Workers take the next item the moment one
exists, and what they discover goes back through a procedure serialised by the
queue&#39;s own lock, so deciding what to explore next needs no reasoning about
order. The threads are started once for the whole traversal.

Same rule as each: a worker owns one state, so the work needs no locks.

</content>
</entry>
<entry>
<id>65178051fd1bbb812c06eba5a065631e2769d3d8</id>
<published>2026-09-18T17:04:46Z</published>
<updated>2026-09-18T17:04:46Z</updated>
<title type="text">scan: give the snapshot retry count a reason</title>
<link rel="alternate" type="text/html" href="commit/65178051fd1bbb812c06eba5a065631e2769d3d8.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 65178051fd1bbb812c06eba5a065631e2769d3d8
parent d918c915f381f21176c929da719576a4724df86f
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 13:04:46 -0400

scan: give the snapshot retry count a reason

Sixteen attempts was a number picked rather than derived, and it did less than
it appeared to. Only one failure benefits from retrying: a write still in
progress, which copies a few hundred bytes and so finishes in tens of
nanoseconds against under ten for an attempt. Four covers that. If the builder
has been descheduled partway through instead, sixteen attempts fail as surely
as four and a caller keeping last frame&#39;s answer has lost nothing either way.

The two sequence reads being sequentially consistent is what makes this a
seqlock rather than a race that happens to work, so that now says so where
someone might otherwise reorder it.

</content>
</entry>
<entry>
<id>d918c915f381f21176c929da719576a4724df86f</id>
<published>2026-09-18T16:54:24Z</published>
<updated>2026-09-18T16:54:24Z</updated>
<title type="text">main: move the graph work onto a builder thread</title>
<link rel="alternate" type="text/html" href="commit/d918c915f381f21176c929da719576a4724df86f.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit d918c915f381f21176c929da719576a4724df86f
parent 813560cfb6d33ff7d146f356680b6aa589b4a800
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 12:54:24 -0400

main: move the graph work onto a builder thread

A frame cost 8 to 13 ms because the UI thread projected the table, charged
ancestors and ranked the tree itself, all of which scale with the volume
rather than with what changed. It now reads a published snapshot and resolves
twenty paths: 0.01 ms median, 0.05 worst, and no frame skipped across 86 of
them.

A single builder owns the totals, which is what keeps charging free of atomics
even though ancestors are shared between readers. It runs flat out rather than
on a clock, and the snapshot is what makes a slow pass invisible.

Two bugs this found: the builder freed the totals the caller prints afterwards,
and the root name flickered to the &quot;.&quot; NTFS calls it, so the projection sets
that name itself rather than the caller fixing it up after.

</content>
</entry>
<entry>
<id>813560cfb6d33ff7d146f356680b6aa589b4a800</id>
<published>2026-09-18T16:54:20Z</published>
<updated>2026-09-18T16:54:20Z</updated>
<title type="text">scan: publish snapshots so a frame never waits on a build</title>
<link rel="alternate" type="text/html" href="commit/813560cfb6d33ff7d146f356680b6aa589b4a800.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 813560cfb6d33ff7d146f356680b6aa589b4a800
parent 7bc10afc050d521c9808f06a2969574929ce5d04
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 12:54:20 -0400

scan: publish snapshots so a frame never waits on a build

A UI has to draw on a clock and the work behind it does not run on one, so the
two need somewhere to meet that neither can stall. A snapshot is a fixed
twenty rows and a few counters whatever the volume holds, handed over behind a
sequence number raised before a write and again after. A reader takes a copy
and checks the number is unchanged and even; if a write was in progress it
keeps the answer it already had rather than waiting.

</content>
</entry>
<entry>
<id>7bc10afc050d521c9808f06a2969574929ce5d04</id>
<published>2026-09-18T16:31:31Z</published>
<updated>2026-09-18T16:31:31Z</updated>
<title type="text">main: time each phase of a simulated frame</title>
<link rel="alternate" type="text/html" href="commit/7bc10afc050d521c9808f06a2969574929ce5d04.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 7bc10afc050d521c9808f06a2969574929ce5d04
parent e47006ad123714c9601ce80cf7dd4ba0189c2e09
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 12:31:31 -0400

main: time each phase of a simulated frame

The frame was timed as a whole, which said it fit the budget but not what it
was spending on. Split three ways it is plain that only one part is
incremental: projecting costs 4.7 ms and ranking 3.7 ms whatever changed,
because both walk the whole tree, while rolling up varies with the work and
averages 1.2 ms. Across a scan that is 276 ms projecting and 220 ranking
against 84 rolling up.

</content>
</entry>
<entry>
<id>e47006ad123714c9601ce80cf7dd4ba0189c2e09</id>
<published>2026-09-18T16:28:26Z</published>
<updated>2026-09-18T16:28:26Z</updated>
<title type="text">main: keep directory totals across frames instead of rebuilding them</title>
<link rel="alternate" type="text/html" href="commit/e47006ad123714c9601ce80cf7dd4ba0189c2e09.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit e47006ad123714c9601ce80cf7dd4ba0189c2e09
parent caf098ddba148a3c3b24df91a2d2bfdf1f737f9c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 12:28:26 -0400

main: keep directory totals across frames instead of rebuilding them

Recomputing cost the whole tree every frame, so the simulated UI climbed from
14 ms to 50 as the table filled, which is three frames of budget. Totals now
carry over and each node is charged once, so a frame costs what changed. It
holds flat at 8 to 13 ms for the whole scan.

Charging once is only correct if the chain is final, and it often is not:
workers publish out of order, so a child frequently arrives before the
directory holding it. A node whose parent is merely late would be rooted at
itself and its bytes would never reach its ancestors. So a node carries
whether its parent link can still change, the projection sets that only once
the parent has been published, and a node is charged to every ancestor at once
or held back and retried. Pending peaks around 31,000 and drains to nothing.

A settled record is then skipped, which also means its size is never re-read,
and extension records add to a base record after it was published. That cost
106 GiB of a badly fragmented volume. The scan-complete frame therefore resets
the projection and recomputes exactly; live and batch now agree to the byte.

</content>
</entry>
<entry>
<id>caf098ddba148a3c3b24df91a2d2bfdf1f737f9c</id>
<published>2026-09-18T16:05:02Z</published>
<updated>2026-09-18T16:05:24Z</updated>
<title type="text">ntfs: let a scan be watched while it runs, and watch one</title>
<link rel="alternate" type="text/html" href="commit/caf098ddba148a3c3b24df91a2d2bfdf1f737f9c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit caf098ddba148a3c3b24df91a2d2bfdf1f737f9c
parent bbd06a1f05949b10b0f6a248e07d525a3a3a5506
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 12:05:02 -0400

ntfs: let a scan be watched while it runs, and watch one

Nothing could see a scan until it ended, so a UI had to wait the whole 1.4
seconds before it had anything to draw. Three things stood in the way, and
none can be changed without the others, so they land together.

The caller now owns the table and passes it in, holding the pointer from the
start rather than receiving it at the end. Folding writes an entry&#39;s flags
last and atomically, so a watcher seeing them set is looking at a record whose
name and sizes are already there; without that order it could catch a name
half written, being a pointer and a length stored separately. The projection
borrows names instead of copying them, so repeating it allocates nothing.

live.odin is what those changes are for: the reader on its own thread, the
tree polled from the main one every 16 ms. Results appear 308 ms into a 1364
ms scan with the largest directory already correct. It also found the next
problem, which is what it was built for: per-frame cost climbs from 14 ms to
50 as the tree fills, because rolling up re-walks every ancestor chain and
re-zeroes every total each frame.

</content>
</entry>
<entry>
<id>bbd06a1f05949b10b0f6a248e07d525a3a3a5506</id>
<published>2026-09-18T15:55:20Z</published>
<updated>2026-09-18T15:55:20Z</updated>
<title type="text">main: let the walker&#39;s width be set from the command line</title>
<link rel="alternate" type="text/html" href="commit/bbd06a1f05949b10b0f6a248e07d525a3a3a5506.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit bbd06a1f05949b10b0f6a248e07d525a3a3a5506
parent a94743f49fab7e9acf6aad0352638c2c4523bd18
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:55:20 -0400

main: let the walker&#39;s width be set from the command line

Sweeping it needed a rebuild, which is how a cold cache came to be compared
against a warm one and read as a thirty-fold regression. On System32 the
walker takes 420 ms on one worker and 234 on four, and the same tree cold
takes seconds regardless, because directory metadata that is not in the cache
dominates everything else.

</content>
</entry>
<entry>
<id>a94743f49fab7e9acf6aad0352638c2c4523bd18</id>
<published>2026-09-18T15:47:53Z</published>
<updated>2026-09-18T15:47:53Z</updated>
<title type="text">main: report from the tree rather than the table</title>
<link rel="alternate" type="text/html" href="commit/a94743f49fab7e9acf6aad0352638c2c4523bd18.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit a94743f49fab7e9acf6aad0352638c2c4523bd18
parent 69f47ec1cf70d06ce660d1cef60b83d682c5f4c0
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:47:53 -0400

main: report from the tree rather than the table

Reporting knew about MFT entries, so it worked for one reader and would have
had to be written again for the next. It reads the tree now, which means the
same rollup and the same output serve both, and the reader is chosen by a
switch over the resolved target.

The native NTFS figures are still printed when that reader ran, since the
tree cannot carry them.

</content>
</entry>
<entry>
<id>69f47ec1cf70d06ce660d1cef60b83d682c5f4c0</id>
<published>2026-09-18T15:47:52Z</published>
<updated>2026-09-18T15:47:52Z</updated>
<title type="text">ntfs: project the table into the shared tree</title>
<link rel="alternate" type="text/html" href="commit/69f47ec1cf70d06ce660d1cef60b83d682c5f4c0.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 69f47ec1cf70d06ce660d1cef60b83d682c5f4c0
parent 41cc6023bd599f1ff4ff77306dcc1ad32bbf190e
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:47:52 -0400

ntfs: project the table into the shared tree

The table stays the native form and keeps what only NTFS has: record and
sequence numbers, per-stream allocation, resident-file accounting. A caller
that wants the truth still reads it. This adds the lossy view it shares with
the other readers, rather than replacing it.

Record numbers become node indices unchanged, so every parent reference stays
valid without a second mapping, at the cost of a slot per dead record. Hard
links become nodes of their own so a file can be found at each of its paths,
flagged so rolling up does not charge it twice.

</content>
</entry>
<entry>
<id>41cc6023bd599f1ff4ff77306dcc1ad32bbf190e</id>
<published>2026-09-18T15:47:52Z</published>
<updated>2026-09-18T15:47:52Z</updated>
<title type="text">walk: add a portable directory reader</title>
<link rel="alternate" type="text/html" href="commit/41cc6023bd599f1ff4ff77306dcc1ad32bbf190e.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 41cc6023bd599f1ff4ff77306dcc1ad32bbf190e
parent 9531748a327e2823cd2c3340b0cc326ccdc6c734
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:47:52 -0400

walk: add a portable directory reader

The seam had one implementation, which proves nothing about whether its types
suit anything but the MFT. This is the other shape: work discovered rather
than known, paths in hand instead of assembled from parent references, and no
native form of its own because a directory listing is already the shape of the
tree.

It walks a generation of directories at a time, and what that generation
discovers becomes the next. core:os reports a logical length and nothing about
allocation, so on-disk size is the length rounded up to a block, which is a
guess this reader cannot improve on.

</content>
</entry>
<entry>
<id>9531748a327e2823cd2c3340b0cc326ccdc6c734</id>
<published>2026-09-18T15:47:47Z</published>
<updated>2026-09-18T15:47:47Z</updated>
<title type="text">scan: add the tree every reader writes into</title>
<link rel="alternate" type="text/html" href="commit/9531748a327e2823cd2c3340b0cc326ccdc6c734.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 9531748a327e2823cd2c3340b0cc326ccdc6c734
parent 6f5c7296f2f94d717e5c954129228396482242b2
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:47:47 -0400

scan: add the tree every reader writes into

Readers differ in how they find files and must not differ in how they record
them, so this is the one structure above the seam. Nodes live in fixed blocks
because a reader that discovers work as it goes grows the store while other
threads are writing into it, and a reallocation would move a node out from
under one.

Two ways in, matching the two shapes a reader has. One that knows its size up
front reserves the range and indexes by its own numbering. One that discovers
as it goes claims a run at a time. Either way a slot belongs to a single
worker, so filling it needs no lock.

</content>
</entry>
<entry>
<id>6f5c7296f2f94d717e5c954129228396482242b2</id>
<published>2026-09-18T15:32:51Z</published>
<updated>2026-09-18T15:32:51Z</updated>
<title type="text">main: resolve the target before choosing a reader</title>
<link rel="alternate" type="text/html" href="commit/6f5c7296f2f94d717e5c954129228396482242b2.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 6f5c7296f2f94d717e5c954129228396482242b2
parent e3bb97356ea248a9a15a6fcb657f652324c24491
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:32:51 -0400

main: resolve the target before choosing a reader

The drive letter went straight to the MFT reader, so a path below the root was
unusable, a non-NTFS volume failed with an open error from three layers down,
and nothing could say whether elevating would help. Resolution now happens
once up front and the engine follows from what the OS reported. Paths print
against the real mount point rather than the first character of the argument.

Subtree targets resolve but are not yet applied; the scan still reports the
whole volume.

</content>
</entry>
<entry>
<id>e3bb97356ea248a9a15a6fcb657f652324c24491</id>
<published>2026-09-18T15:32:50Z</published>
<updated>2026-09-18T15:32:50Z</updated>
<title type="text">scan: add the seam between readers and the tree</title>
<link rel="alternate" type="text/html" href="commit/e3bb97356ea248a9a15a6fcb657f652324c24491.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit e3bb97356ea248a9a15a6fcb657f652324c24491
parent 85353cbace6545d9c421898f6fc755e9f7c5c45e
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 11:32:50 -0400

scan: add the seam between readers and the tree

Readers for NTFS, for plain directory walking, and later for other filesystems
all answer the same question and must agree on how the answer is recorded.
This package is where that agreement lives.

It starts with the two things every reader needs before it runs. Resolving a
target asks the OS which volume a path sits on and what filesystem that volume
runs, rather than guessing from the shape of the string, so the failures that
are knowable up front are reported by the layer that understands them.
Choosing a reader is then a question about a struct.

Dispatch is a switch, not an interface: the set of readers is fixed when the
program is built.

</content>
</entry>
<entry>
<id>85353cbace6545d9c421898f6fc755e9f7c5c45e</id>
<published>2026-09-18T14:38:17Z</published>
<updated>2026-09-18T14:38:17Z</updated>
<title type="text">ntfs: pass each the same load the workers were sized with</title>
<link rel="alternate" type="text/html" href="commit/85353cbace6545d9c421898f6fc755e9f7c5c45e.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 85353cbace6545d9c421898f6fc755e9f7c5c45e
parent 0c616a13350973f9e8f37ef7e02b0cf1168a5e2a
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 10:38:17 -0400

ntfs: pass each the same load the workers were sized with

The width came from width(.., .Io) but the run was handed .Mixed, so the two
disagreed about what kind of work this is. Nothing changed behaviourally,
since the worker count binds first either way, but a reader comparing the two
lines had to work that out to be sure.

</content>
</entry>
<entry>
<id>0c616a13350973f9e8f37ef7e02b0cf1168a5e2a</id>
<published>2026-09-18T14:34:37Z</published>
<updated>2026-09-18T14:34:37Z</updated>
<title type="text">main: expose the reader&#39;s knobs</title>
<link rel="alternate" type="text/html" href="commit/0c616a13350973f9e8f37ef7e02b0cf1168a5e2a.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 0c616a13350973f9e8f37ef7e02b0cf1168a5e2a
parent 13ee0a66101f41787171d45d3f9f61e68200ee9c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 10:34:37 -0400

main: expose the reader&#39;s knobs

Sweeping the worker count and chunk size took a rebuild each, which made the
measurements tedious to run and impossible to reproduce from the shell. Both
are flags now, alongside the existing skip controls, and the summary reports
how many workers the scan actually ran on.

</content>
</entry>
<entry>
<id>13ee0a66101f41787171d45d3f9f61e68200ee9c</id>
<published>2026-09-18T14:34:37Z</published>
<updated>2026-09-18T14:34:37Z</updated>
<title type="text">ntfs: read the MFT in parallel</title>
<link rel="alternate" type="text/html" href="commit/13ee0a66101f41787171d45d3f9f61e68200ee9c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 13ee0a66101f41787171d45d3f9f61e68200ee9c
parent ddd3faabc02a028b253014b1f0025e8410ba74f1
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 10:34:37 -0400

ntfs: read the MFT in parallel

The scan spent two thirds of its time waiting on one read at a time. Chunks
of the read plan now go through flow.each, each worker holding its own
handle, buffer, arena and stats. The only shared structure is the entry
table, and every write lands at a record number no other worker claims.

Extension records are the exception, crediting a base record another worker
may own, so they are copied aside and folded in serially afterwards.

Four readers take 1357 ms against 1999 for one. The drive saturates almost at
once, so more is worse: thirty-two takes 2339 ms.

</content>
</entry>
<entry>
<id>ddd3faabc02a028b253014b1f0025e8410ba74f1</id>
<published>2026-09-18T14:34:36Z</published>
<updated>2026-09-18T14:34:36Z</updated>
<title type="text">ntfs: let a volume be reopened for concurrent reads</title>
<link rel="alternate" type="text/html" href="commit/ddd3faabc02a028b253014b1f0025e8410ba74f1.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit ddd3faabc02a028b253014b1f0025e8410ba74f1
parent ac798c62c06ff9fc7044068c1731ec67c42d27bc
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 10:34:36 -0400

ntfs: let a volume be reopened for concurrent reads

Windows serialises I/O on a synchronous handle, so threads sharing one would
queue behind each other rather than keeping the drive busy. A volume now
remembers the device path and mode it was opened with, and volume_clone hands
out an independent handle to the same place. Reopening is the only way to get
real concurrency: duplicating the handle would share one file object and
serialise just the same.

</content>
</entry>
<entry>
<id>ac798c62c06ff9fc7044068c1731ec67c42d27bc</id>
<published>2026-09-18T14:21:24Z</published>
<updated>2026-09-18T14:21:24Z</updated>
<title type="text">flow: add a package for this project&#39;s concurrency shapes</title>
<link rel="alternate" type="text/html" href="commit/ac798c62c06ff9fc7044068c1731ec67c42d27bc.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit ac798c62c06ff9fc7044068c1731ec67c42d27bc
parent 68d04bca0de2d92746edf00037eb9802aab2806a
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 10:21:24 -0400

flow: add a package for this project&#39;s concurrency shapes

Threading decisions were about to be made inline in the MFT reader, where the
next place needing them would rediscover the reasoning. The package rests on
one rule: a worker owns its state and never shares it, so the work needs no
locks and the caller merges the slots afterwards.

each claims items with one atomic increment, so an early finisher takes more
rather than waiting on a slow neighbour. width sizes the pool from the core
count, the load, and the item count; each takes that as a ceiling, so an
implausible width cannot become thousands of threads.

</content>
</entry>
<entry>
<id>68d04bca0de2d92746edf00037eb9802aab2806a</id>
<published>2026-09-18T13:19:26Z</published>
<updated>2026-09-18T13:19:26Z</updated>
<title type="text">ntfs: skip dead record runs using $MFT&#39;s own bitmap</title>
<link rel="alternate" type="text/html" href="commit/68d04bca0de2d92746edf00037eb9802aab2806a.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 68d04bca0de2d92746edf00037eb9802aab2806a
parent fbed86a27fbc7a64a0f445a929c552404749467c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 09:19:26 -0400

ntfs: skip dead record runs using $MFT&#39;s own bitmap

A third of this volume&#39;s MFT records are deleted entries that the scan reads
in full and throws away. $MFT carries a $BITMAP marking which slots are live,
so the dead ones can be known for the cost of one 300 KB read and jumped over
instead.

Skipping is not free: each break in the sequential stream costs about 140
microseconds, so short dead runs are cheaper to read through than to avoid.
Sweeping the threshold on a live volume put the floor at a megabyte, where
16.5% of the table is skipped and the read drops from 1295 ms to 1163 ms.
Skipping every dead run instead doubles the scan time.

</content>
</entry>
<entry>
<id>fbed86a27fbc7a64a0f445a929c552404749467c</id>
<published>2026-09-18T12:11:38Z</published>
<updated>2026-09-18T12:11:38Z</updated>
<title type="text">ntfs: read the volume unbuffered, with a flag to compare</title>
<link rel="alternate" type="text/html" href="commit/fbed86a27fbc7a64a0f445a929c552404749467c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit fbed86a27fbc7a64a0f445a929c552404749467c
parent b351ce96c62247bdc5ed83dca067eac5db84433f
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 08:11:38 -0400

ntfs: read the volume unbuffered, with a flag to compare

The MFT is read once front to back and never re-read, so passing it through
the Windows cache manager buys nothing and costs an extra copy plus the
eviction of whatever the user had cached. Unbuffered reads land in our buffer
directly; the reader already works in whole clusters, so the alignment rules
are met once the last chunk rounds up. Measured on a live volume this made no
difference at all, both modes holding about 1.9 GB/s, which rules the cache
out as the bottleneck and points at read queue depth instead. Kept as the
default because it removes a variable before the read path goes parallel, and
--buffered keeps the comparison available.

</content>
</entry>
<entry>
<id>b351ce96c62247bdc5ed83dca067eac5db84433f</id>
<published>2026-09-18T12:01:49Z</published>
<updated>2026-09-18T12:01:49Z</updated>
<title type="text">main: report where scan time goes</title>
<link rel="alternate" type="text/html" href="commit/b351ce96c62247bdc5ed83dca067eac5db84433f.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit b351ce96c62247bdc5ed83dca067eac5db84433f
parent 385967ee523ba3745ae38971da391e94ed74053f
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 08:01:49 -0400

main: report where scan time goes

Scan latency is now a product requirement, so the tool has to say which phase
owns it rather than leaving the answer to guesswork. The reader accumulates
time spent in volume reads separately from time spent folding records, and
main times the two aggregation passes. On a warm cache the split is roughly
two thirds reading and one fifth parsing, which points any optimisation work
at the read path first.

</content>
</entry>
<entry>
<id>385967ee523ba3745ae38971da391e94ed74053f</id>
<published>2026-09-18T11:53:25Z</published>
<updated>2026-09-18T11:53:25Z</updated>
<title type="text">main: check the attributed sum against $Bitmap</title>
<link rel="alternate" type="text/html" href="commit/385967ee523ba3745ae38971da391e94ed74053f.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 385967ee523ba3745ae38971da391e94ed74053f
parent 58ce9575b74e8f145475b7311ccd556336771733
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 07:53:25 -0400

main: check the attributed sum against $Bitmap

The summary now prints what every file claims next to what the volume says
is allocated, so a reader can see at a glance whether the parser accounts for
the disk. On a live NTFS volume the two agree to within a rounding error, and
a wide gap would mean a decoding bug rather than missing space. The resident
file line explains the one category the per-file sums cannot attribute.

</content>
</entry>
<entry>
<id>58ce9575b74e8f145475b7311ccd556336771733</id>
<published>2026-09-18T11:53:25Z</published>
<updated>2026-09-18T11:53:25Z</updated>
<title type="text">ntfs: count allocated clusters from $Bitmap</title>
<link rel="alternate" type="text/html" href="commit/58ce9575b74e8f145475b7311ccd556336771733.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 58ce9575b74e8f145475b7311ccd556336771733
parent 7890196b13854057d3820bd10a6b118af4f03107
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 07:53:25 -0400

ntfs: count allocated clusters from $Bitmap

Nothing so far checked the per-file sums against reality: if the run list
decoding dropped or double counted extents, the total would simply be wrong
with no way to tell. $Bitmap holds one bit per cluster and is maintained by
the file system itself, so counting its set bits gives an independent total
to compare against. Padding bits past the last cluster are excluded, and
reads are rounded up to whole clusters because raw volume reads must cover
whole sectors. The extent reader can now seek backwards, since record 6 sits
at the start of a table it has already streamed past.

</content>
</entry>
<entry>
<id>7890196b13854057d3820bd10a6b118af4f03107</id>
<published>2026-09-18T11:53:19Z</published>
<updated>2026-09-18T11:53:19Z</updated>
<title type="text">ntfs: count files whose data lives inside their MFT record</title>
<link rel="alternate" type="text/html" href="commit/7890196b13854057d3820bd10a6b118af4f03107.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 7890196b13854057d3820bd10a6b118af4f03107
parent 3aa3d09131f904760208dd4dd980c51d3018e16e
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Fri, 18 Sep 2026 07:53:19 -0400

ntfs: count files whose data lives inside their MFT record

A file small enough to fit in its own record allocates no clusters, so it
contributes nothing to any directory total and looks free to a disk usage
tool. Its real cost is the record, which is charged to $MFT instead. Counting
these files and their bytes makes the size of that blind spot visible rather
than leaving it as an unexplained difference between what the tool reports
and what the volume says is in use.

</content>
</entry>
<entry>
<id>3aa3d09131f904760208dd4dd980c51d3018e16e</id>
<published>2026-09-17T11:31:21Z</published>
<updated>2026-09-17T11:31:21Z</updated>
<title type="text">build: run debug and test targets under AddressSanitizer</title>
<link rel="alternate" type="text/html" href="commit/3aa3d09131f904760208dd4dd980c51d3018e16e.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 3aa3d09131f904760208dd4dd980c51d3018e16e
parent a5103b3d75e0cd738d7ad2c59311c9218b3e083d
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Thu, 17 Sep 2026 07:31:21 -0400

build: run debug and test targets under AddressSanitizer

The debug allocator&#39;s poisoning only pays off when the program is
instrumented, so debug and test builds now pass -sanitize:address. The SAN
variable lets a build drop it when a third-party library misbehaves under
the sanitizer.

</content>
</entry>
<entry>
<id>a5103b3d75e0cd738d7ad2c59311c9218b3e083d</id>
<published>2026-09-17T11:31:20Z</published>
<updated>2026-09-17T11:31:20Z</updated>
<title type="text">debug: poison guards and freed blocks for AddressSanitizer</title>
<link rel="alternate" type="text/html" href="commit/a5103b3d75e0cd738d7ad2c59311c9218b3e083d.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit a5103b3d75e0cd738d7ad2c59311c9218b3e083d
parent 15e476006e3d5713ecff8a8370da79d5ca931309
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Thu, 17 Sep 2026 07:31:20 -0400

debug: poison guards and freed blocks for AddressSanitizer

On Windows ASan does not see Odin heap blocks: the default allocator uses
HeapAlloc, not the libc malloc that ASan intercepts, so an out-of-bounds read
or a read after free passed silently. The allocator now poisons its guard
bytes and every freed block through base:sanitizer, which makes an
instrumented access into them trap at the faulting instruction. That covers
the one class a pure allocator cannot catch. ASan only knows the address is
poisoned, so a death callback appends the block, its allocation site, and
its free site to the report. Memory is unpoisoned before returning to the
backing allocator so unrelated code never inherits poison.

</content>
</entry>
<entry>
<id>15e476006e3d5713ecff8a8370da79d5ca931309</id>
<published>2026-09-17T01:39:12Z</published>
<updated>2026-09-17T01:39:12Z</updated>
<title type="text">ntfs: size decoded run lists to their length before returning</title>
<link rel="alternate" type="text/html" href="commit/15e476006e3d5713ecff8a8370da79d5ca931309.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 15e476006e3d5713ecff8a8370da79d5ca931309
parent 9f8e834ac504d28c329b7ffe1d98a9a416b965ce
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:39:12 -0400

ntfs: size decoded run lists to their length before returning

decode_runlist returned a slice over a dynamic array that still had spare
capacity, and the caller freed it as a plain slice. The debug allocator
flagged the free as 352 bytes against a 512-byte allocation. The default heap
ignores the size so nothing broke, but a size-aware allocator such as an
arena or pool would corrupt its accounting. Shrinking the array to its length
makes the slice&#39;s length and the allocation agree.

</content>
</entry>
<entry>
<id>9f8e834ac504d28c329b7ffe1d98a9a416b965ce</id>
<published>2026-09-17T01:37:55Z</published>
<updated>2026-09-17T01:37:55Z</updated>
<title type="text">debug: replace lifetime allocator with a bug-finding debug allocator</title>
<link rel="alternate" type="text/html" href="commit/9f8e834ac504d28c329b7ffe1d98a9a416b965ce.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 9f8e834ac504d28c329b7ffe1d98a9a416b965ce
parent babaf2f99e3fc420d2eac85fb37ace10d1455a48
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:37:55 -0400

debug: replace lifetime allocator with a bug-finding debug allocator

The lifetime allocator only listed leaks. An agent reading its output could
not see heap overflows, double frees, writes through pointers kept across an
append, or deletes with the wrong length, which are the allocation bugs that
actually cost time. The debug allocator guards every block, quarantines and
poisons freed memory, verifies everything at each free, phase change, and
exit, and prints each issue where it is detected with the allocating site,
the freeing site, the damage, and what it usually means. Resizes always move
the block so stale pointers surface immediately.

</content>
</entry>
<entry>
<id>babaf2f99e3fc420d2eac85fb37ace10d1455a48</id>
<published>2026-09-17T01:22:50Z</published>
<updated>2026-09-17T01:22:50Z</updated>
<title type="text">build: replace build.cmd with a Makefile writing to build/</title>
<link rel="alternate" type="text/html" href="commit/babaf2f99e3fc420d2eac85fb37ace10d1455a48.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit babaf2f99e3fc420d2eac85fb37ace10d1455a48
parent 91d48e5ecf812c99605f6667ee983a4b5a1edd5c
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:22:50 -0400

build: replace build.cmd with a Makefile writing to build/

A Makefile works from any shell on Windows, macOS, and Linux, which matters
because the parser is meant to go cross-platform. Debug and release binaries
land in build/debug and build/release so both can exist at once, and the
source tree stays free of compiler output. Targets rebuild only when a source
file changes. Odin does not create output directories, so each output has an
order-only prerequisite that makes its directory.

</content>
</entry>
<entry>
<id>91d48e5ecf812c99605f6667ee983a4b5a1edd5c</id>
<published>2026-09-17T01:18:32Z</published>
<updated>2026-09-17T01:18:32Z</updated>
<title type="text">build: add build script with debug, release, test, and check targets</title>
<link rel="alternate" type="text/html" href="commit/91d48e5ecf812c99605f6667ee983a4b5a1edd5c.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 91d48e5ecf812c99605f6667ee983a4b5a1edd5c
parent 2f6bf9010e22a6ebf403a2e9e0d30ce9064326b9
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:18:32 -0400

build: add build script with debug, release, test, and check targets

One place for the compiler flags so nobody has to remember -vet and
-strict-style, and so the debug target always carries -debug for backtraces.

</content>
</entry>
<entry>
<id>2f6bf9010e22a6ebf403a2e9e0d30ce9064326b9</id>
<published>2026-09-17T01:18:31Z</published>
<updated>2026-09-17T01:18:31Z</updated>
<title type="text">main: print largest files and directories from the MFT</title>
<link rel="alternate" type="text/html" href="commit/2f6bf9010e22a6ebf403a2e9e0d30ce9064326b9.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 2f6bf9010e22a6ebf403a2e9e0d30ce9064326b9
parent d66668cf8754a802792ffc92319889d3f5148f0a
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:18:31 -0400

main: print largest files and directories from the MFT

First end-to-end use of the ntfs package and the smoke test for a live
volume. Directory totals are computed by crediting each entry&#39;s allocation to
every ancestor, a single linear pass since average depth is under ten. Top-N
selection keeps a small sorted array instead of sorting millions of entries.
The debug build wires in the lifetime allocator one frame below main so its
report runs before os.exit; the import is @(require) so release builds, where
that block compiles away, do not reject it as unused.

</content>
</entry>
<entry>
<id>d66668cf8754a802792ffc92319889d3f5148f0a</id>
<published>2026-09-17T01:18:31Z</published>
<updated>2026-09-17T01:18:31Z</updated>
<title type="text">ntfs: measure allocation from run lists instead of header sizes</title>
<link rel="alternate" type="text/html" href="commit/d66668cf8754a802792ffc92319889d3f5148f0a.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit d66668cf8754a802792ffc92319889d3f5148f0a
parent 3bff5ee1bb8fdb6edd17ae05d2c12cfcf4f3d1ee
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:18:31 -0400

ntfs: measure allocation from run lists instead of header sizes

A live scan reported $BadClus at 491 GiB and the root directory at 942 GiB
on a 491 GiB volume. Its $Bad stream spans the volume with entirely
unallocated runs yet carries no sparse flag, so allocated_size is fiction.
The run list is the ground truth: summing its non-sparse clusters is correct
for plain, compressed, and sparse attributes alike and needs no flag checks.
Each extent maps only its own VCN range, so every extent of a fragmented
attribute now contributes, with the header sizes kept as a fallback for a
malformed list. mft_init takes the cluster size to do the multiplication.

</content>
</entry>
<entry>
<id>3bff5ee1bb8fdb6edd17ae05d2c12cfcf4f3d1ee</id>
<published>2026-09-17T01:15:51Z</published>
<updated>2026-09-17T01:15:51Z</updated>
<title type="text">ntfs: add parser tests</title>
<link rel="alternate" type="text/html" href="commit/3bff5ee1bb8fdb6edd17ae05d2c12cfcf4f3d1ee.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 3bff5ee1bb8fdb6edd17ae05d2c12cfcf4f3d1ee
parent 2a1da1214b05efa72bf1ce53082cfac1d4e030a6
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:51 -0400

ntfs: add parser tests

Real MFT records make poor fixtures because they carry a volume&#39;s file names,
so tests build records byte by byte with the same layout rules the parser
enforces. Coverage: boot sector size encodings, run list deltas and sparse
runs, torn-write detection in fixups, resident and non-resident sizes, DOS
alias handling, extension records arriving before their base, hard links,
free records, and path reconstruction including stale parent references.

</content>
</entry>
<entry>
<id>2a1da1214b05efa72bf1ce53082cfac1d4e030a6</id>
<published>2026-09-17T01:15:51Z</published>
<updated>2026-09-17T01:15:51Z</updated>
<title type="text">ntfs: read $MFT from a Windows volume or image</title>
<link rel="alternate" type="text/html" href="commit/2a1da1214b05efa72bf1ce53082cfac1d4e030a6.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 2a1da1214b05efa72bf1ce53082cfac1d4e030a6
parent 4c32ba4db0569448115ac42eca612443640c6551
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:51 -0400

ntfs: read $MFT from a Windows volume or image

Reading a live volume means opening \.\C: with administrator rights and
issuing sector-aligned reads. The reader parses the boot sector, fixes up
record 0, decodes $MFT&#39;s own run list, then streams the table in 16 MiB
chunks through those runs, folding each record into the table. The MFT is
addressed as a logical byte range mapped onto runs at read time, so a record
straddling two runs needs no special case. A stub for other platforms keeps
the pure parsing portable.

</content>
</entry>
<entry>
<id>4c32ba4db0569448115ac42eca612443640c6551</id>
<published>2026-09-17T01:15:51Z</published>
<updated>2026-09-17T01:15:51Z</updated>
<title type="text">ntfs: build entry table from records</title>
<link rel="alternate" type="text/html" href="commit/4c32ba4db0569448115ac42eca612443640c6551.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 4c32ba4db0569448115ac42eca612443640c6551
parent dc6fe10448fbabbd07277d00b9d318bd6c159389
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:51 -0400

ntfs: build entry table from records

Mft holds one Entry per record slot plus a list of extra hard links. Records
may arrive in any order; extension records credit their attributes to the
base record, which removes any need to parse $ATTRIBUTE_LIST. Free records
are skipped because their contents are stale. Sizes come from the first
extent of each non-resident attribute only, so fragmented files are not
double counted, and every stream and index is summed into allocated because
they all occupy clusters. Names are interned as UTF-8 in a growing arena so
they never move. mft_path rebuilds paths and detects stale parents via
sequence numbers.

</content>
</entry>
<entry>
<id>dc6fe10448fbabbd07277d00b9d318bd6c159389</id>
<published>2026-09-17T01:15:50Z</published>
<updated>2026-09-17T01:15:50Z</updated>
<title type="text">ntfs: parse FILE records and attributes</title>
<link rel="alternate" type="text/html" href="commit/dc6fe10448fbabbd07277d00b9d318bd6c159389.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit dc6fe10448fbabbd07277d00b9d318bd6c159389
parent fb720cfe1676b227f1d97c4d3ef6aa3d6331d40b
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:50 -0400

ntfs: parse FILE records and attributes

NTFS stamps the last two bytes of each 512-byte block of a record with a
sequence number and keeps the originals in an array, so a torn write is
detectable. apply_fixups verifies and restores in place. The attribute
iterator yields one decoded view per attribute and stops at the first
malformed header, so corruption yields a truncated record, not garbage. Typed
views cover $STANDARD_INFORMATION and $FILE_NAME. attr_disk_size returns
compressed_size for compressed or sparse attributes because allocated_size is
only logical for those.

</content>
</entry>
<entry>
<id>fb720cfe1676b227f1d97c4d3ef6aa3d6331d40b</id>
<published>2026-09-17T01:15:50Z</published>
<updated>2026-09-17T01:15:50Z</updated>
<title type="text">ntfs: decode run lists</title>
<link rel="alternate" type="text/html" href="commit/fb720cfe1676b227f1d97c4d3ef6aa3d6331d40b.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit fb720cfe1676b227f1d97c4d3ef6aa3d6331d40b
parent 2ae06661a23209d266b50e04c1f7f988d398ea9e
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:50 -0400

ntfs: decode run lists

Non-resident attributes store their clusters as variable-width mapping pairs
with each LCN relative to the previous run. The MFT itself is a non-resident
file, so this decoder is needed before any record beyond the first can be
located. Sparse runs, which have no LCN, are represented explicitly so callers
can reject or zero-fill them.

</content>
</entry>
<entry>
<id>2ae06661a23209d266b50e04c1f7f988d398ea9e</id>
<published>2026-09-17T01:15:49Z</published>
<updated>2026-09-17T01:15:49Z</updated>
<title type="text">ntfs: parse the boot sector</title>
<link rel="alternate" type="text/html" href="commit/2ae06661a23209d266b50e04c1f7f988d398ea9e.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 2ae06661a23209d266b50e04c1f7f988d398ea9e
parent 82bbcce6882010f6464b0a9a1094bdb23eec5aeb
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:49 -0400

ntfs: parse the boot sector

The boot sector gives the three numbers everything else depends on: cluster
size, record size, and the MFT&#39;s first cluster. Two fields use a signed
exponent encoding, where 0xF6 means 2^10 bytes, so a record can be smaller
than a cluster and a cluster can reach 2 MiB; the decoder handles both forms.
Validation rejects sizes that would make later volume reads misaligned.

</content>
</entry>
<entry>
<id>82bbcce6882010f6464b0a9a1094bdb23eec5aeb</id>
<published>2026-09-17T01:15:49Z</published>
<updated>2026-09-17T01:15:49Z</updated>
<title type="text">ntfs: add package doc, error type, and file references</title>
<link rel="alternate" type="text/html" href="commit/82bbcce6882010f6464b0a9a1094bdb23eec5aeb.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit 82bbcce6882010f6464b0a9a1094bdb23eec5aeb
parent fe70fec19875e96a4562deee5c641cbfa768a5a1
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:49 -0400

ntfs: add package doc, error type, and file references

The ntfs package reads the Master File Table directly instead of walking
directories. Every file&#39;s metadata sits in one mostly contiguous file, so a
volume scan becomes a few sequential reads rather than millions of random
ones. This commit lays the groundwork: the error enum shared by all layers,
the file reference type (48-bit record number plus 16-bit sequence), the
well-known record numbers, and little-endian byte readers. Parsing is kept
pure over byte slices so it can be tested without a volume.

</content>
</entry>
<entry>
<id>fe70fec19875e96a4562deee5c641cbfa768a5a1</id>
<published>2026-09-17T01:15:49Z</published>
<updated>2026-09-17T01:15:49Z</updated>
<title type="text">lifetime: add debug allocator with grouped leak report</title>
<link rel="alternate" type="text/html" href="commit/fe70fec19875e96a4562deee5c641cbfa768a5a1.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit fe70fec19875e96a4562deee5c641cbfa768a5a1
parent c6d0c19f76b34198d2dc53b1f92765090efc87e3
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:49 -0400

lifetime: add debug allocator with grouped leak report

Debug builds swap the context allocator for this one. It wraps the tracking
allocator from core:debug/trace, so leak and bad-free detection stay core
code, and adds what that report lacks: a phase label recording which program
stage each allocation was born in, and grouping of leaks by call site sorted
by bytes. Runtime and CRT frames are dropped and paths print relative to the
project, so the output can be handed to a reviewer or an LLM with no other
context.

</content>
</entry>
<entry>
<id>c6d0c19f76b34198d2dc53b1f92765090efc87e3</id>
<published>2026-09-17T01:15:48Z</published>
<updated>2026-09-17T01:15:48Z</updated>
<title type="text">build: ignore compiler outputs</title>
<link rel="alternate" type="text/html" href="commit/c6d0c19f76b34198d2dc53b1f92765090efc87e3.html" />
<author>
<name>Jack Mordaunt</name>
<email>jackmordaunt.dev@gmail.com</email>
</author>
<content type="text">commit c6d0c19f76b34198d2dc53b1f92765090efc87e3
Author: Jack Mordaunt &lt;jackmordaunt.dev@gmail.com&gt;
Date:   Wed, 16 Sep 2026 21:15:48 -0400

build: ignore compiler outputs

</content>
</entry>
</feed>
