# AGENTS.md Guidance for coding agents and contributors working on Crafter.DB itself. The [README](README.md) is the user-facing manual, and [AGENTS.consumer.md](AGENTS.consumer.md) is what agents working in a *consumer* of this library read: crafter-build copies it into the dependency-notes section of the consumer's `AGENTS.md` on every build. When any of the three disagrees with the code, the code wins; the interfaces under `interfaces/` are the contract, and `tests/ShouldKeepLayoutGolden/main.cpp` is the contract for the bytes on disk. ## Project overview Crafter.DB is the Catcrafts embedded table store: one C++26 module, `Crafter.DB`, with twelve partitions behind the umbrella (thirteen interface files) and two implementation units, no dependencies, Linux only. A table is a plain struct with a `version`; a file is a memory-mapped array of fixed-width rows plus a heap for strings and blobs; reads are zero-copy views; queries are lambdas or column expressions; schema changes are C++ migrations run on open. It replaces a hand-rolled, iostream-based transaction log in the first Catcrafts project that needed one (that port is still to do) and serves any Crafter project that keeps rows in a file. | Path | What lives there | |---|---| | `interfaces/Crafter.DB.cppm` | The umbrella: `export import`s every partition. | | `interfaces/Crafter.DB-.cppm` | One partition per file, each but `:Error` opening with a comment that says what the partition owns. All but `:File`, `:Storage` and two plain functions in `:Migration` (`DescribeColumns`, `PeekVersion`) are templates and inline code. | | `implementations/Crafter.DB-File.cpp`, `-Storage.cpp` | The two implementation units, `Crafter.DB:File_impl` and `Crafter.DB:Storage_impl`. `File` is the only unit that includes a C header. | | `project.cpp` | The build description: the static library `Crafter.DB` and, for `*-linux-gnu` targets, the fourteen tests. No dependencies, no lint rules. | | `tests//main.cpp` | One test program per directory, declared in `project.cpp`. | | `AGENTS.template.md`, `AGENTS.consumer.md` | The source of this file, and the notes for consumers. Every build and test run writes `AGENTS.md` from the template; the library has no dependencies, so the dependency-notes section never appears here. `AGENTS.md` is gitignored: edit the template. | | `bin/`, `build/` | Generated and gitignored. `bin/Crafter.DB----/` holds the PCMs, `std.pcm` and `libCrafter.DB.a`, `build//` the objects, and each test has a pair of its own. `build/test-logs/.log` keeps the output of failed tests. | Partitions, from the bottom up (each imports only the ones above it): - `:Error`: `Error : std::runtime_error` and its subclasses `IOError` (with `code` and `path`), `CorruptError`, `SchemaError`, `MigrationError`, `LockedError`, `OrderError`, `ReadOnlyError`. Everything the library throws derives from `Error`, except the `std::out_of_range` of `At()` and friends and the `std::length_error` of a `FixedString` that does not fit. - `:Reflect`: compile-time introspection of aggregates without P2996: `memberCount`, `Member`, `MemberType`, `MemberName()` (parsed out of `std::source_location::function_name()`), `MemberIndex()`, `MemberIndexOf`, `TypeName()`, `ForEachMember`, the `Aggregate` concept. The `static_assert`s at the bottom fail the library's own build if a compiler changes the shape of `function_name()`. - `:Format`: the on-disk PODs (`FileHeader`, `CommitRecord`, `Extent`, `SchemaBlobHeader`, `ColumnDescriptor`, `Var`), `Kind`, the column flags, FNV-1a, CRC-32C, `SchemaInfo` (serialise, parse with range checks, fingerprint). Every struct size and the load-bearing offsets are pinned by `static_assert`s. - `:Formatter`: `Formatter`, the customisation point, with the `Scalar`, `FixedFormatter` and `VarFormatter` concepts; `Heap` and `HeapWriter`; `EncodeVar`/`VarBytes`; `Key`, `StringRef`, `BlobRef`, `FixedString`; the built-in formatters for `std::string`, `std::vector`, `std::chrono::duration` and `time_point`. - `:Schema`: `Layout`, computed `consteval`: members classified by `Shape`, nested aggregates flattened to dotted names, leaves placed by ascending alignment after the flags byte, the fingerprint; `TableType`, `HasOrderedBy`, `HasPrevious`, `HasMigrate`, `OrderableKey`; `CheckTable()` with the diagnostics for the struct and its members (the size limits sit in `Layout` and `Arrange`); `MakeSchemaInfo()`; the `SchemaTests` namespace of compile-time checks. - `:File`: everything that talks to the OS: `OpenOptions`, `OpenMode`, `Durability`; `File` (descriptors, `flock`, `fallocate` with the `posix_fallocate` fallback, `pread`/`pwrite`, `fdatasync`/`fsync`); `MappedRegion` (a `PROT_NONE` reservation filled with `MAP_FIXED` extents); `MappedPages`; the `Os` namespace (private `mkdir -p`, rename without replace, hard link, directory sync, random bytes, page size, pid). Nothing here knows what a table is. - `:Storage`: the type-erased engine. `TableFile` creates, opens, validates, recovers, grows, flushes, compacts and verifies a table knowing rows only as `rowSize` bytes and columns only through the file's descriptors; `Inspection` and `TableFile::Inspect`, `DynamicRow`, `TableStats`, `VerifyResult`, `CreateParams`. - `:Codec`: typed encode/decode over `Layout`: `EncodeRow`, `EmplaceRow`, `EncodeMember`, `DecodeRow`, `DecodeMember`, `View`, `CopyLeaves`/`ZeroLeaves`, the ordered-column helpers (`LowerBoundSlot`, `UpperBoundSlot`, `CheckOrder`) and `AppendRows`. - `:Row`: `RowId`, `Column`, `Row` (the 16-byte view), `LiveIterator`/`LiveRange` (sentinel-terminated, skips tombstones), `SlotView` (random access over every slot), `DescribeRow`, `std::formatter>` and `std::formatter`. - `:Query`: the expression types `Compare`, `And`, `Or`, `Not` and their operators on `Column` tags, `Bounds` and `CollectBounds` (bound extraction from a top-level `&&`), the chainable `Query`. - `:Migration`: `CopyByName`, `ConvertChain`, `RequireFingerprint`, `WriteConverted`/`RewriteFrom`/`WalkChain`, `MigrateTable`, and the exported `Migrate(path)` and `PeekVersion(path)`. - `:Table`: `Table`, the user-facing handle: open/create/migrate, every write, iteration, `Where`/`Select`, the ordered-column searches, and `Compact`/`Clear` through `Rebuild`. Numbers the code and the tests pin (`tests/ShouldKeepLayoutGolden`, the `SchemaTests` namespace): the README's `Transaction` is 88 bytes, 8-aligned, with `currency` at offset 2, `name` at 8, `bic` at 24, `iban` at 40, `description` at 56, `amount` at 72 and `timePoint` at 80; a `Var` handle is 16 bytes with 12 inline; the header is one 4 KiB page followed by at least one schema page; the first extent of each region is 16 KiB, extents double up to 1 GiB, and a region holds at most 32 of them (roughly 17 GiB); 32 GiB of address space is reserved per region; a row is at most 65535 bytes and a table at most 65535 columns; a value is below 4 GiB. A fresh table is 24 KiB without string or blob columns and 40 KiB with. ## Dev environment crafter-build is the only tool, from its Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; this machine has its `stable` and `beta` branches, both 4.0.0, and a build installed from a Crafter.Build checkout is branch `local`, chosen with `--branch=`). It compiles with its bundled clang 23.1.2 and libc++ 23.1.2, `-std=c++26`, `import std;`. The host's `/usr/bin/clang++` is not used and is not a supported compiler for this library. The project has no dependencies, so nothing is fetched and no network is needed. The host is Linux x86_64. What that compiler does and does not have, checked on 2026-10-17 (the library leans on all of it): - No P2996 reflection: `-freflection` is accepted and ignored, and there is no ``. `:Reflect` exists because of that and is the only partition to replace when a clang ships `std::meta`. - Structured-binding packs (`auto&& [...m] = t;`), pack indexing (`m...[I]`), user-generated `static_assert` messages, deducing `this`, `[[clang::no_destroy]]`, `std::atomic_ref`, `std::ranges::fold_left`, `std::format` and `std::println`: all in use. - libc++ 23 lacks `std::start_lifetime_as`, `std::inplace_vector` and `std::simd`, so row fields are read with constant-size `memcpy` (which folds to a load) and SIMD scans are not attempted. - libc++'s `system_clock::duration` is microseconds (libstdc++'s is nanoseconds). The chrono period is part of the fingerprint for that reason, and the golden test pins `paramDen == 1000000`. The Flatpak sandbox sees the host filesystem but has its own `/tmp` (`fs::temp_directory_path()` inside a test is a per-app directory under `$XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp`, shared by instances running at the same time and gone once none is), keeps its caches under `~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/`, and a binary it built runs only inside it: `flatpak run --command=$PWD/bin/-/ net.catcrafts.CrafterBuild`. That is how you see a passing test's output, which the runner does not keep, the Benchmark's table in particular. `touch tests//main.cpp && crafter-build -v test ` prints the exact compiler command, the model for a scratch program against the built PCMs. `std.pcm` is built once per target, `--march` and compiler into `~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/---/` and copied into each variant's directory; a toolchain update rebuilds it once, and a warm run of the whole suite takes about 2.5 seconds. The variant hash includes the compiler, so the directories of earlier compilers, and of the stub this repository started as (`bin/Crafter.DB-…-97c53ceb1aa99864/`, with colon-named PCMs), stay until `crafter-build clean`; glob for a directory rather than predicting its hash. ## Build, test and style commands ```bash crafter-build # bin/Crafter.DB-/libCrafter.DB.a and the PCMs crafter-build test # build and run the fourteen tests, Benchmark included crafter-build test 'ShouldM*' # name glob crafter-build test --list crafter-build test --jobs=1 --timeout=120 crafter-build -v test ShouldQuery # with the compiler commands crafter-build clean # delete bin/ and build/ ``` `project.cpp` registers no lint rules and does not pull in Crafter.LintRules (a `#pragma crafter dependency` would `git pull` and rebuild it on every load of `project.cpp`), so `crafter-build lint` and `crafter-build format` exit 1 with "No lint rules defined." Style is checked by eye, see "Code style". There is no CI. Before a change is done, `crafter-build test` passes: 14 passed as of 2026-10-31. Failed, crashed and timed-out tests write `build/test-logs/.log`; a later passing run does not delete the file (three from the first day are still there), so check its timestamp before trusting it. ## Testing instructions Tests are declared, not discovered: one `cfg.AddTest("").Dependencies({ &cfg });` line in `project.cpp`, inside the `if (cfg.target.ends_with("-linux-gnu"))` block (the engine uses `flock`, `fallocate` and fixed-address `mmap`; the musl target gets the library and no tests). The source is `tests//main.cpp`, an `int main()` linked against the local `libCrafter.DB.a`: exit 0 passes, nonzero fails, 77 skips (unused here). The runner gives each test 60 s in its own process, in parallel. Conventions the fourteen follow: - Names are `Should`; `Benchmark` is the exception. Open `main.cpp` with a comment that states the behaviour under test and the failure it guards against; `tests/ShouldRecoverCommits/main.cpp` and `tests/ShouldReadWhileAppending/main.cpp` show the shape, and a regression test names the bug. - `Check(condition, what)` and `CheckEqual(actual, expected, what)` in an anonymous namespace print `FAIL: …` (with both values for `CheckEqual`) and count; `main` prints `ok` and returns 0 only when nothing failed. Do not stop at the first failure unless later steps are meaningless without it. - Each test works under `fs::temp_directory_path() / "crafter-db-"`, `remove_all`ed first and last, with a name of its own because the runner is parallel. Table structs are declared per test: the README's `Transaction` where the reference layout matters, a one- or two-member struct where it does not (`Item`, `Entry`, `Tick`). - Process-level behaviour is tested with `fork()`. `ShouldLockAcrossProcesses` opens the table in a child and maps the outcome to an exit code; `ShouldRecoverCommits` runs the writer in a child that ends with `_exit(0)` while its `Table` is still in scope, so no destructor and no `Flush` runs, which is the only honest way to simulate a crash. Those two `#include ` and `` before their imports. Damage to a file is done with a `Patch(path, offset, bytes)` helper over `std::fstream` in `ShouldRecoverCommits`, `ShouldRejectSchemaChange` and `ShouldVerify`. - Concurrency is tested with `std::jthread` readers scanning while the main thread appends and erases (`ShouldReadWhileAppending`), and such a run ends with `table.Verify().Ok()`. - `Benchmark` never fails. It prints the legacy format (a cleaned-up copy of the hand-rolled iostream log Crafter.DB replaces) against Crafter.DB on 100 000 rows with a warm cache; raise `rowCount` for a serious run, and quote its numbers with the machine (the README's are a Xeon w7-3465X). What is covered: layout and fingerprint (`ShouldKeepLayoutGolden`), encode/decode and reopen (`ShouldRoundTrip`), pointer stability across extents (`ShouldKeepViewsStableAcrossGrowth`), `StringRef`/`BlobRef` semantics (`ShouldCompareStringRefs`), every query form and the ordered column (`ShouldQuery`), tombstones and `Compact` (`ShouldEraseAndCompact`), every member shape and both formatter shapes (`ShouldFormatCustomTypes`), the migration chain and its refusals (`ShouldMigrate`), schema and corruption refusals (`ShouldRejectSchemaChange`), the commit protocol under crashes (`ShouldRecoverCommits`), locking (`ShouldLockAcrossProcesses`), `Verify` and `DynamicRow` (`ShouldVerify`), readers against a writer (`ShouldReadWhileAppending`). Not covered: a table on a 16 KiB-page kernel, `reserveBytes` exhaustion, the `posix_fallocate` fallback, `prefetch`. A bug fix ships with a test that fails before it and passes after. A change to the bytes on disk makes `ShouldKeepLayoutGolden` fail: that is the point of it, so read design rule 4 before updating its expectations. ## Code style Nothing is enforced by tooling; match the file you are in. The conventions are those of Crafter.LintRules' full set, applied by hand. - The first two lines of every source are `// SPDX-License-Identifier: LGPL-3.0-only` and `// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®`, with a space after `//` (as Crafter.Build spells it; Crafter.Math, Network and Asset do not), then a blank line. Every interface but `:Error` then carries a comment block saying what the partition owns, before `export module Crafter.DB:

;`; keep it true when the partition changes. - Four-space indentation, no tabs, LF, a final newline, K&R braces, `if (x) return;` on one line when it fits, one declaration per statement, `if (` and `for (` with a space and no space before a call's `(`. - `import std;` everywhere. The only C headers are in the global module fragment of `implementations/Crafter.DB-File.cpp` (after `module;`, with `#define _GNU_SOURCE 1`) and the two POSIX headers the forking tests include before their imports. `__builtin_offsetof` rather than ``. - `PascalCase` for types, functions and concepts; `camelCase` for variables, members and constants alike (`inlineVarBytes`, `fnvOffset`, `rowFlagDeleted`), which is why the table statics are `version` and `orderedBy` while `Previous` and `Migrate` are a type and a function. Internal namespaces are `Reflect`, `Detail`, `Os` and `SchemaTests`. `enum class` only. Fixed-width integers; plain `int` and `long` appear only where POSIX or the language demand them (`main`, `waitpid`'s `status`, `operator++(int)`, what `sysconf` returns) and in the tests' `failures` counters. - `std::format` and `std::println`; iostream appears only in the Benchmark's copy of the legacy format and in the tests' `Patch` helpers and fixture writers. Exception messages start with the path where one is known (`{}: …`; `Heap::Read`, `EncodeVar`, `CompactInto`, the reservation and the `msync` failure carry none). The `static_assert`s of `CheckTable`, `CopyByName` and `CheckEmplaceArgument` start with `Crafter.DB: ` and name the table type and the member (`Message` and `MemberMessage` in `:Schema`); a handful of internal limits assert without the prefix. - Scalars and handles are read with `std::memcpy` into a local, and the flags byte through `std::atomic_ref` because readers race with `Erase`; the only reinterpreted pointers are the zero-copy views (`StringRef::View`, `BlobRef::View`, `DynamicRow::Text`) and the schema name pool. - Hot paths carry no indirection: predicates are template parameters, column offsets are `constexpr`, and `HeapWriter::Grow` is the only virtual call, reached only when an extent fills. A change to the scan loop, `StringRef`'s comparisons, `Row::operator[]` or the encoders comes with evidence, the Benchmark before and after or the `-O3` assembly of the loop; the maintainer reviews performance claims at that level. - Comments say why: the failure mode, the race, the rejected alternative (`MADV_DONTFORK` in `-File.cpp`, the `external` object in `:Reflect`, the sentinel iterator in `:Row`). The code carries long comments on purpose; do not strip them, write them for new non-obvious code, and give commit bodies the same treatment. ## Design rules Decided deliberately; change them only with the maintainer's say-so. 1. **Structs are the schema.** No macros, registration, traits to specialise or code generation. Every rule about a table struct's members is a `static_assert` in `CheckTable()` with a message naming the table and member, the size limits are `static_assert`s and consteval throws in `Layout` and `Arrange`, and a type with no `Formatter` is a clean concept failure. Misuse is a compile error, never a runtime surprise. 2. **One file per table, two regions, fixed addresses.** Rows and heap are chains of doubling extents appended at the end of the file and mapped `MAP_FIXED` into a `PROT_NONE` reservation, so row `i` is `base + i * rowSize` and no pointer moves while the table is open. No free-space management: `Compact()` right-sizes. Region slots 2 and 3 are reserved for a persisted index and a WAL. 3. **Fixed-width rows, 16-byte handles, a bump-arena heap.** Flags byte at offset 0, leaves stable-sorted by alignment, row size rounded to `rowAlign`. Strings and blobs are Umbra handles (length, 4-byte prefix, then 8 inline bytes or a heap offset), so values of up to 12 bytes live inline (strings, and blobs whose elements are aligned to 4 or less). The heap has no per-entry header; `Update`, `Set` and `Erase` orphan bytes that only `Compact()` reclaims. Little-endian only. 4. **The file format is a contract pinned twice**: by the `static_assert`s on every struct size and the load-bearing offsets in `:Format`, and by `tests/ShouldKeepLayoutGolden` with an independent spelling of the fingerprint and the exact handle bytes. `Kind` is append-only. The fingerprint covers names, kinds, offsets, sizes, flags, chrono period, blob element type, clock and the ordered column, and what it covers is what a `version` bump is required for. Changing any byte an existing file holds, or the fingerprint's inputs, is a format change: bump `formatVersion` (1 today, exact match required) and say so; never reinterpret old files silently. 5. **Durability by two commit records, not a journal.** An append is visible to in-process readers at once (release stores of the published counts) and durable at `Flush()`: `fdatasync`, the inactive commit slot with its CRC, `msync`. On open the newest valid commit wins and everything past it, uncommitted extents included, is discarded; an unclean close recounts tombstones. `Update` and `Set` commit their new heap bytes before the row points at them, and the row bytes themselves are not atomic against a power loss; that limitation is documented, and the WAL region is the v2 answer, not an `fsync` per row. 6. **A type-erased engine under a thin typed layer.** `TableFile` knows rows as `rowSize` bytes and columns through the file's own descriptors, so `Inspect`, `Verify`, `Compact`, `DynamicRow` and a migration from a struct that no longer exists work without the C++ type; `Table` adds compile-time offsets and encoders on top; `:File` knows nothing about tables at all. Keep each layer ignorant of the one above it. 7. **Queries are C++ and inline.** A predicate is a template parameter stored by value in the view: no `std::function`, no virtual call, no per-row call. Search keys are encoded once (`Key`, or automatically in a column expression). Bound extraction reads only comparisons on the `orderedBy` column in a top-level `&&` chain; `||` and `!` leave the whole predicate as a filter, which is always correct and only ever a narrowing. No SQL and no planner; hash indexes (`indexedBy`) are deferred, not refused. 8. **One writer, many in-process readers, a standard container's contract plus a little.** `flock` exclusive for a writer, shared for `readOnly`, between processes and between two `Table` objects in one. `Append`, `Emplace` and `Erase` are safe while other threads read (published counts, a monotonic tombstone flag, sentinel-terminated iteration that never compares against a moving end); `Update`, `Set`, `Compact` and `Clear` need exclusive access from the caller. `Row` views and `StringRef`s survive `Append` and `Erase`. 9. **Private by default, and never replace.** Files `0600` in `0700` directories, `O_NOFOLLOW`, `MADV_DONTDUMP`, creation through an `O_EXCL` temp file and a rename that fails when the target exists (racing creators share one file), rewrites through a temp file and `rename` (a visible table file is always valid). `MADV_DONTFORK` was tried and rejected: a forked child may read an inherited table, and its writes, `Flush` and destructor flush are refused by the owner-process check instead. 10. **Every byte from a file is checked before it is used**: header fields, both commit CRCs, extents against the committed length, every descriptor field in `SchemaInfo::Parse`, every heap handle against the published heap length (`Heap::Read`), the page-size granule against the running kernel. A new field read from a file gets its check and a case in `ShouldRejectSchemaChange` or `ShouldVerify`. 11. **Linux, no dependencies, no host split.** The library needs `flock`, `fallocate` and fixed-address `mmap` and says so; there is no `#ifdef` for another platform and no portability layer to grow. It depends on nothing, so a consumer's build fetches nothing for it. ## Commit and PR guidelines - Branch off `master` (`claude/` is the pattern for agent work in the sibling repositories) and open the PR against `master` on the project's Forgejo. The repository is `Catcrafts/Crafter.DB` on the project's Forgejo: `forgejo@forgejo.catcrafts.net:Catcrafts/Crafter.DB.git` over SSH, `https://forgejo.catcrafts.net/Catcrafts/Crafter.DB.git` for consumers. A checkout whose `origin` still names the personal namespace the repository moved from can fetch (HTTPS redirects) but not push over SSH ("Push to create is not enabled"); repoint `origin`. Pushes go over SSH. - The history has one commit, so take the voice from the sibling repositories: `type(scope): imperative summary` with `feat`, `fix`, `refactor`, `docs` or `test` and the partition as the scope (`storage`, `schema`, `query`, `migration`, `file`, `format`, `reflect`). Body: what was wrong, why, and why the fix has this shape, wrapped at about 75 columns, with the numbers when something was measured. Add a `Co-Authored-By:` trailer when an agent wrote it. - Separate commits for the fix, its test and the README change when each stands on its own. The README is the manual: keep its module table, examples and "Performance" table true to the code. Update `AGENTS.consumer.md` in the same change when a signature, a default, a limit or an error behaviour consumers lean on changes; consumers pick it up on their next build. - A format change (design rule 4) updates `ShouldKeepLayoutGolden` in the same commit, and the commit message says it is one. - `crafter-build test` green before pushing. There is no CI. - Never commit `bin/`, `build/` or `AGENTS.md`. ## Security considerations - A table file is untrusted input: anything on disk may have been truncated, bit-flipped or crafted. Design rule 10 is the defence, and `ShouldRejectSchemaChange` and `ShouldVerify` are its regression suite. `Verify()` is the deep check and returns problems rather than throwing, so a tool can print them all. There is no per-row checksum: a flipped byte inside a scalar column goes unnoticed. - The defaults keep data private (`0600`, `0700`, `O_NOFOLLOW` against a swapped symlink, `MADV_DONTDUMP` against core dumps). Do not loosen a default; a consumer that wants a readable file sets `filePermissions`. - The lock is advisory (`flock`). A process that ignores it, or `cp`, can read the file mid-write, and what it reads is valid up to the writer's last `Flush` because only committed rows count on open. - `reserveBytes` (32 GiB per region, two regions per table) is address space, not memory, but a tight `ulimit -v` or hundreds of open tables turns it into an `IOError` at open. - `project.cpp` has no dependencies, so loading it executes nothing fetched from the network; keep it that way, or pin a `commit` on anything added. - Tests write only under `fs::temp_directory_path()`, fork only to exercise locking and crashes, and never touch the network. ## Gotchas - Adding a partition is three edits: the interface file, the `ifaces` array in `project.cpp` and the umbrella `Crafter.DB.cppm`; an implementation unit also goes into the `impls` array. Keep the order of the "Project overview" list: a partition imports only what is above it. - `MemberName` depends on clang spelling `function_name()` as `… [P = &external.iban]` and `TypeName` on `… [T = ]`; the `static_assert`s at the bottom of `:Reflect` catch a change at the library's build. `Reflect::external` is `[[clang::no_destroy]] inline const T` on purpose: an undefined `extern` would be cheaper but warns at every instantiation. - Leaf indices follow declaration order, depth first; column offsets do not. A `std::optional` contributes a presence leaf named `name?` before its payload, a nested aggregate flattens to `outer.inner`, a `std::array` of scalars to `name[i]`, and the flags byte is column 0 but no leaf. `Layout::Offset(leaf)` and `Column(leaf)` translate. - Reordering two members of the same alignment class changes offsets and therefore the fingerprint; the README's `Transaction` has `currency` at offset 2, ahead of every string. Both follow from rule 3, and a `SchemaError` is the symptom in a consumer that forgot the `version` bump. - `char` is always `Kind::Int8` whatever the platform's signedness, `wchar_t` depends on its size, `long double` is not storable, `std::optional>` is rejected (no distinct column names), and a `Formatter` may override `kind` only when its `Stored` is a single column. - The `Transaction` inside `SchemaTests` and the `Probe` inside `:Reflect` are compile-time tests that run on every build of the library, not examples to copy from. - `TableFile::Inspect` is a static member; the README's module table calls it `Inspect(path)`. The tests spell it `DB::TableFile::Inspect(path)`. - Extents are aligned to the larger of 4 KiB and the running kernel's page size at creation, and `header.pageSize` records it. A file created on a 4 KiB kernel is refused on a 16 KiB one with an `IOError` (`not_supported`) rather than mapped badly; nothing tests that path. - A region tops out at 32 extents (`IOError`, `file_too_large`, "Compact() it"); `Compact` sizes the first extent of the new file to the live payload, which resets the chain. A reservation that fills throws `IOError` (`not_enough_memory`) naming `OpenOptions::reserveBytes`. - `Table::Open` is a loop for a reason: the first open fails with `no_such_file_or_directory`, the create may lose a race and fail with `file_exists`, and the second pass opens the other process's file. Both halves of the race are needed, and `TableFile::Create` never replaces. - `RemoveStaleTemps` runs on every writable open, after the exclusive lock is taken, and deletes the `.tmp-*` siblings it can lock exclusively; the probe spares a temp another process is still writing. The twelve-hex-digit suffix comes from `Os::RandomBytes`. - A migration's backup `.v.bak` is a hard link to the old inode taken before the rename, so it costs nothing until the original is replaced, and it fails on a filesystem without hard links. - `Deleted()` reads the flags byte through a relaxed `atomic_ref`, `MarkDeleted` raises the monotonic `tombstones` flag before the byte, and `LiveRange::begin()` reads that flag when iteration starts rather than when the range was built, so a `Query` built before an `Erase` still skips the row. A scan that started before the `Erase` may yield it; that is documented, not a bug. - `LowerBound`/`UpperBound` search every slot, tombstones included, because an erased row keeps its key; only `Compact` drops keys. A `Where` with bounds on the ordered column does the same. - A `Query` over `Rows`, `Range`, `From` and the like can be iterated as a `const` object; one built with `Where` cannot, because `std::ranges::filter_view` has no const iteration (the deducing-`this` `requires` clauses in `Query` are what makes the first case work). `Query::Select` is rvalue-only: `std::move` a named query first. - `Emplace` converts arguments implicitly, never with `static_cast`, so an `int` for an enum member or swapped arguments fail to compile with a message naming the member. `Append` of a range that is not both sized and random-access buffers it first, so the order check covers the whole batch before anything is written. - `Set<&T::member>` copies only that member's leaves back into the slot (`CopyLeaves`) and leaves the flags byte alone, so an `Erase` that raced with it is not undone even though the contract gives `Set` exclusive access. Do not "simplify" it to a whole-row `memcpy`. - After `fork()` the child's `Table` reads fine and refuses every write, `Flush` and its destructor's flush (`OwnedByThisProcess`): the parent's counters in the child are stale and must never reach the file. Do not make the destructor flush there. - The Benchmark's `last timestamp (1000x open+read)` row is the one operation where the legacy format wins (about 2 ms against 30 ms): a mapped open costs one `mmap` per extent. The answer is to open once per process, which the README and the consumer notes say; do not try to win that row by changing the format. - `crafter-build test` prints "Building std PCM" once per configuration (the library and each test) on every run; the step returns at once when the cached PCM is current, and only a toolchain update rebuilds it.