Commit graph

267 commits

Author SHA1 Message Date
88b61f7522 docs(porch-store): call replies are scalars — response goes via the table
- WO-E226: call's reply must be a copyable scalar, and every receive
  program-wide must declare the same return type. Verified by fixture:
  "call's reply type `Out` is not a copyable scalar"
- the spec had the actor return the response object, which cannot cross
  the mailbox. Corrected: the actor stores the response and returns an
  outcome code; the middleware reads the row and builds the Resp
- owner and duplicate now read the SAME durable row, so byte-identical
  replay is structural rather than careful copying
- blocking, exactly-once execution and the mailbox queue are unchanged

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 77e06c1690b92d456a9bc53503695fdaa2b4b44e)
2026-09-15 01:15:30 +02:00
4be826f9ab feat(porch-store): key pool actor for serialized per-key counting
- add docs/examples/porch/middleware/keypool.wo: one PoolMsg (kind 1 =
  count, kind 2 = begin placeholder for task 4), a Verdict class, a
  fixed-size actor pool with a byte-sum-mod-N selector
- KeyActor.receive implements kind 1: reads the row, writes the new
  count via field assignment (writes through, never delete+insert),
  prunes a fully-elapsed window's row instead of resetting it
- window arithmetic on time.ticks(); reset instant sent back is built
  from time.now() only
- call's reply must be a copyable scalar (WO-E226), so the count and
  remaining window time are packed into one Int by the actor and
  unpacked into Verdict by pool_count — the packing stays inside this
  file, callers only ever see Verdict
- gate leg in scripts/web-app-accept.sh: a flat copy of porch (manifest
  stripped) with a driver dropped beside keypool.wo asserts two
  sequential counts return 1 then 2; verified failing (E403, make_pool
  undeclared) before this file existed, passing after
- woc docs/examples/porch/ exits 0; full web-app-accept.sh: 47 checks,
  0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 676e651d808ef2c3619f88c3808adc372cd0be6c)
2026-09-15 01:15:30 +02:00
377808b936 feat(porch-store): add digest column to IdempotencyKey table
- Add digest field to store sha256(method|path|body) separately from key
- Enables detection of "same key, different body" in future tasks
- Update idempotent.wo insert to compute and store digest value
- Typechecker passes: exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3a9bddcd1e3c11f5b371ce54cafc373685ca08b6)
2026-09-15 01:15:30 +02:00
560987d969 docs(porch-store): plan corrections from the pre-flight scan
- one message class with a kind discriminator, not a receive per
  message type: an actor handle is typed to one message class, so a
  second receive compiles but is unreachable. chat/main.wo is the
  precedent. Verified by fixture before amending
- Task 4's Files list omitted keypool.wo, which its step 4 edits
- clarified that the delete-then-insert ban targets using that pair as
  an UPDATE; pruning an expired row is a plain delete and is required

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a96ebe20e92e2dc6dfecc59a955d4c6e75d74689)
2026-09-15 01:15:30 +02:00
dbee71a9e5 docs(porch-store): implementation plan, and a spec correction
- the spec's blocking design was unimplementable: call's reply IS the
  return value of receive, so an actor cannot hold a waiter. Holding
  means never returning, and an actor that never returns cannot process
  the completion it waits for — deadlock
- corrected shape: the actor RUNS the handler inside its own receive, so
  a duplicate waits in the mailbox and is served after the owner. The
  queue blocking needs is the mailbox; nothing is held
- verified before adopting it, not after: an actor can receive a message
  carrying an interface-typed value and invoke it, so the route's
  Handler passes through the mailbox
- spec History records the reasoning error — "the primitives landed" was
  taken as "blocking needs no new surface", which does not follow
- plan: 5 tasks. Counting and replay live in one new keypool.wo; both
  middlewares become thin key-choosers, so porch 2 and 3 inherit one
  serialization convention instead of re-implementing it
- self-review added two legs it was missing: exact counting under real
  concurrency (the criterion the pool exists for), and pruning an
  elapsed limiter row rather than resetting it, which otherwise leaks a
  row per IP ever seen
- plan is code-free per house convention; the writing-plans skill wants
  code blocks and the project rule overrides it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f079455755a189a86bb12e07cc11549ed7a78b91)
2026-09-15 01:15:30 +02:00
aee5adddc4 fix(porch-store): add the missing use json import
- docs/examples/porch/ did not typecheck: WO-E403 "cannot resolve the
  receiver's type for the call to `encode`" on json.encode
- every other example that calls json.encode/decode imports it; this
  file did not, so the whole porch library was uncompilable on dev
- woc docs/examples/porch/ now exits 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5c3544d524fa98dbb7a363600cd2eeb6dd1badac)
2026-09-15 01:15:30 +02:00
9f2d19083e docs(porch-store): spec for porch 1 store-backed middleware
- supersedes Phases B and C as built: a store-after-completion
  middleware cannot satisfy three of the story's seven criteria
- in-flight collision is undetectable (the row is written after the
  handler ran, so concurrent duplicates both miss and both execute)
- the 10s in-flight heuristic is inverted: created_at is stamped at
  store time, so it fires on legitimate fast replays and never on a
  genuinely concurrent request
- "reused key, different body is refused" is unreachable while the
  digest is folded into the key — nothing looks the bare key up
- design: sharded actor pool serializes per key, @table persists;
  actors own volatile state, tables own durability. Inherited by
  porch 2 and 3
- limiter joins the pool for exact counting, writes through instead of
  delete+insert, keys on net.peer unless trust_proxy is declared, and
  uses monotonic ticks for arithmetic but wall clock for the header
- idempotency blocks rather than answering 409: call parks the
  duplicate until the owner reports. Digest becomes a column
- saturation fails closed with 503 for both: saturating the pool must
  not become the limiter bypass
- records that the story's "time.after is still reserved" is stale;
  spawn/send/call/monitor/time.after all landed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fc09e94373db65837ff5eb620fec67bab02c1931)
2026-09-15 01:15:30 +02:00
c53a335286 docs(commit-history): register porch-store Phase C
- Idempotent middleware (aee7926) added to the porch-store row
- records that Phase C is unverified: no `use json` import despite
  calling json.decode and json.encode

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit aa8abfb4b7a1dabaa0c68afa0ee7fdfccf1b4689)
2026-09-15 01:15:30 +02:00
bf343045ab feat(porch-store): Idempotent middleware (Phase C, in progress)
- replays a stored response for a repeated Idempotency-Key: before()
  checks the key, after() stores status/body/content-type on a 2xx/3xx
- key is "idem:<header>:<value>", optionally plus a sha256 digest of
  method|path|body when include_body is set
- 409 while a key is in flight (stored within the last 10s), lazy TTL
  expiry on access, default 24h
- replay allowlists content-type only — never Set-Cookie or Date
- backed by IdempotencyKey from Phase A (519d411)

Written by a parallel session and committed here as-is because its
branch was consolidated away. NOT verified: it calls json.decode and
json.encode without a `use json` import, which every other example that
uses json has. Left unedited rather than fixed blind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit aee79264095eea3b2c38c92789c44b31f1c9ef8a)
2026-09-15 01:15:30 +02:00
b21cfde1bf docs(commit-history): record the branch consolidation
- porch-store and query-corpus prefixes registered; both replayed onto
  dev, so dev is a superset of porch-store-middleware
- three branches could not be replayed and are preserved as annotated
  tags rather than merged or discarded:
  - cleanup/pre-existing-changes carries crates/ + Cargo.toml, the Rust
    runtime master deleted; replaying it would resurrect it
  - ipc-attach refactors wo_row_insert/wo_row_update_field into
    encoded cores, which db2-keys rewrote for keys-residency — two
    overlapping refactors of one function
  - keypair-auth builds on ipc-attach, blocked by the same overlap
- names the specific hazard: 9c transfers ownership of vals on failure,
  dev's keys-resident arm returns early without freeing, so a merge
  that compiles and passes could still leak or double-free

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit bc8fec01d565d0ad54696fb3d2f85a9d1e0531a1)
2026-09-15 01:15:30 +02:00
5941a092f7 feat(query-corpus): iteration 9g corpus #1 — skillhost needs no new query grammar
- resolved 9g's forks EMPIRICALLY against the running compiler:
  - count(<query>) and len(<query>) already work (fork 2 collapses to
    zero code)
  - skillhost's correlated NOT EXISTS is a backlink emptiness in
    writeonce (`where len(x.children) == 0`), using only 9b machinery
    (fork 1) — verified on a self-referential ?ref/backlink table
  => corpus #1 forces NO new grammar; per the method ("add only what a
     corpus uses"), exists/not-exists was NOT built
- docs/examples/skill-catalog: mirrors skillhost's `skills` table
  (name @unique, description/location/root, parent ?ref Skill, children
  backlink) and translates all five of its SQL statements 1:1
  (insert+dup-trap, get-by-name, list, roots via backlink-emptiness,
  count); scripts/skill-catalog-accept.sh 7/0, WAL-durable, dup trap
  persists across restart
- fixture run/db-query-corpus (count(query) + backlink NOT EXISTS);
  just skill-catalog module; target/ gitignored
- general exists/not-exists left unbuilt and recorded as "enters when a
  corpus forces a non-relation correlation"
- gates: oop-e2e 80/0, woc-test 566/0, skill-catalog 7/0; story + board
  record the finding

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4c82461634d45f11eca1a252702031c03d999c7f)
2026-09-15 01:15:30 +02:00
e159b5a754 feat(porch-store): Limiter middleware (Phase B)
(cherry picked from commit 5b1e82ab4bf3bad39bb6762a52b2dfaafd18a110)
2026-09-15 01:15:30 +02:00
192d451f5e feat(porch-store): store tables for rate limit + idempotency (Phase A)
(cherry picked from commit 519d4117fd0d6d0bd7d51f80bcb20de4d6294503)
2026-09-15 01:15:30 +02:00
4f6dc2dcfc docs(commit-history): record the lang42 cherry-pick
- registry: lang42 on master 2026-09-01
- pick row: 8 commits mapped dev -> master, zero conflicts
- verified on master after full rebuild: 38 runtime suites 0 fail both
  flavors (test_proc 128/0, test_wal 5966/0), woc-test 557/0 forced,
  subprocess-accept 12/0, site-accept 23/0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:27:02 +02:00
b17b848403 docs(lang42): close out iteration 42
- story frontmatter status: done, Progress section records what landed
  vs the spec (everything, same day as the brainstorm)
- board: NEXT PLAN entry with the six standup answers (deadlock proven
  real: 5 s hang, 8192-byte truncation; 15 ms after; ping 2 ms during a
  parked child; 1000 spawns fd-flat; SIGTERM leaves no child); pending
  row flipped to DONE
- graph: node 42 class done, same change as the board row
- runtime/src/CODE-LOGIC.md: the bounded-subprocess section (bundle
  park, slot registry, ownership sweeps, raw pidfd syscalls)
- full belt at close: 19 runtime suites 0 fail (test_proc 128/0),
  woc-test 557/0, subprocess-accept 12/0, site-accept 23/0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:19:25 +02:00
c91027bcc6 feat(lang42): subprocess example + gate
- docs/examples/subprocess: line-oriented TCP service, one Handler actor
  per request — ping/run/slow/deadline/cap/long exercise the whole
  bounded surface from .wo, traps caught with try/catch in the language
- scripts/subprocess-accept.sh + `just subprocess`: 12 checks, 0 failures
  first run — deadline and cap messages verbatim, ping answered in 2 ms
  while a sleep-2 child was parked, SIGTERM exit 0 with the sleep-30
  child verifiably gone (pid checked from outside)
- service logs to /tmp/subprocess.log, banner-separated per run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:19:25 +02:00
8bcd24969e docs(lang42): story, spec and plan for bounded subprocess
- claim the lang42 prefix; iteration 42 story (readiness: ready), approved
  spec, and the 11-task implementation plan
- board: pending row for 42; graph: node 42 with green edges (11, 24)
- graph: porch track section added (same sweep)
- parity studies that motivated 42: alacritty, tmux, zen-browser under
  docs/plan/exploration/ — staged path, gap lists, refused routes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:19:25 +02:00
bed1167ca9 docs(commit-history): record the iteration-12 cherry-pick
- seven commits dev to master, zero conflicts: the six db2-migrate
  commits plus site-deploy, which the close-out edits and which had
  been dev-only
- verified on master after the pick: 36 suites 0 fail (test_wal
  5966/0), woc-test clean, residency-accept 14/0, site-accept 23/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 22:02:18 +02:00
4ad24d6381 docs(db2-migrate): close out iteration 12
- crash-before-rename test: a COMPLETE valid migrated temp beside the
  untouched original is discarded and the boot re-migrates — the
  sharpest point on the crash timeline, deterministic, no fault
  injection needed
- story: all six tasks done with commit hashes, all eight criteria met
  with the test that proves each, plus the three deviations from the
  plan and why (transcode over replay, lazy head, poison forces
  transcode)
- CODE-LOGIC: migration section; also corrected limitation 3, which
  still claimed unbounded hot-row chains — iteration 11 closed that
- status board row 12; deploy guide's rollback section gets its real
  answer (rolling back across a migration is a migration backwards:
  expect the refusal, restore the .bak)
- test_wal 5966 pass, 0 fail

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4bb6ece2531e2123eb958c91d9bef4a6528eab3b)
2026-08-31 21:54:27 +02:00
1b6d633ed1 docs(db2-migrate): spec + story for schema migrations v1
- brainstorm settled: declarative and automatic at boot; v1 verbs are
  add and delete only; data/seed migrations deferred to v2
- added fields zero-fill by kind: the grammar has no field-default
  syntax and v1 refuses to grow compiler surface for it
- same-kind delete+add refuses as a disguised rename; retype and
  vanished classes refuse by name
- schema lives in the log itself: WO_WAL_SCHEMA head record, written by
  fresh-log open and compaction; name-keyed diff also closes the
  silent cid-renumbering hole
- story is iteration 12, board row added, db2-migrate prefix claimed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 072e007b144ff6689b6ff920ea10665900c1a2ef)
2026-08-31 21:54:27 +02:00
552c129ce3 docs(site-deploy): redeploy runbook for writeonce.de
- new docs/guides/deploying-site.md: build, content refresh, systemd
  unit, post-deploy verification, rollback, and the gaps behind each
  workaround
- leads with the trap that costs the most: shipping a binary does NOT
  update chapters. seed_if_empty only fills an EMPTY table and
  AdminEdit answers not_found for an unknown slug, so a host with an
  existing WO_DATA shows the old chapter list with no error anywhere
- that claim is measured, not argued: a 9-chapter build seeded a data
  dir, then the 10-chapter binary against it still 404'd /ch/storage
  and rendered 9 nav entries; wiping WO_DATA gave 200 and 10
- records two more blockers found while writing it: both site deps
  (porch, writeonce-view) 404 on GitHub and wo.lock is untracked, so
  the site submodule cannot build standalone; and the embedded wovm
  sets the glibc floor (this machine: 2.38, above Ubuntu 22.04's 2.35)
- build recipe run verbatim before publishing; releasing.md points here

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 930a715c4a3d847529e3e341edc71c65a7e11d1c)
2026-08-31 21:54:27 +02:00
e31a037533 docs(commit-history): record the site-submodule cherry-pick
- 4b56348 -> 7d9d526, 4eead89 -> 653a91c
- the registry rows for lang41, porch-store and query-corpus came with
  the pick and are kept: the registry is a global claim ledger, so a
  copy that silently omits three claimed prefixes is worse than one
  that names them and says they are dev-only
- site-submodule row corrected on the way in — it said "Not picked to
  master", which this pick is precisely what falsifies
- verified on master after the pick: site-accept 23 checks, 0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 22:03:43 +02:00
653a91c702 docs(commit-history): register the site-submodule prefix
- records that docs/examples/site is now a submodule on dev only
- states the consequence plainly: master still carries the site inline,
  so the branches differ structurally at that path until this is
  cherry-picked

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4eead8938c7292a130723aeabe7b74bdd1ba60f6)
2026-08-30 22:03:18 +02:00
7d9d526bb6 refactor(site-submodule): docs/examples/site becomes a submodule
- extracted to github.com/shoneyJ/writeonce-site with `git subtree
  split`, so the site keeps its own 9 commits of history rather than
  landing there as a flattened snapshot
- .gitmodules gains the third entry, alongside reference/writeonce-app
  and reference/writeonce-api; path is unchanged, so every doc and
  script that names docs/examples/site still resolves
- site-accept.sh fails early and says `git submodule update --init`
  when the directory is empty. Without it a clone lacking submodules
  copies an empty app and fails later as a build error naming nothing
- releasing.md: the steps that edit install/view.wo now say that edit
  is a commit in the site repo plus a pointer bump here — editing and
  committing only in this repo would record nothing
- gate re-run against the submodule: site-accept 23 checks, 0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4b5634801d5379890bad24c8129cfb23ab6b98df)
2026-08-30 22:02:55 +02:00
e93284befb docs(commit-history): record the databasev2 residency cherry-pick
- first cherry-pick under this convention: 37 commits, dev to master,
  mapped one-to-one with titles
- iteration 11 could not travel alone — its commits touch
  wo_wal_fold_row_at, keys_fold_into and row_apply_field_keys, none of
  which existed on master, so the whole db2-keys/db2-delta stack came
- porch-store (26 commits) deliberately left on dev: porch 1 was
  re-scoped mid-flight, which is what "ready, not merely green" is for
- records the three docs conflicts and how each was resolved, including
  keeping only the databasev2 half of a status entry that would
  otherwise have had master claiming porch 1 was done
- records what is still outstanding: task 6's byte-budget refusal, a
  missing guard rather than an unhonoured annotation

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 20:45:39 +02:00
92d2600ae7 docs(commit-history): feature-to-cherry-pick reference
- records the workflow: develop on dev, feature prefix as the
  conventional-commit scope, cherry-pick onto master when ready
- prefix registry so two features cannot claim the same prefix; the
  prefix is claimed before the feature's first commit
- cherry-pick log maps dev hashes to the master hashes they produced —
  they differ, and that mapping is what makes a feature traceable or
  revertible as a unit after dev moves on
- notes the db2-keys seam: written pre-convention on
  porch-store-middleware, replayed onto dev, replay verified identical
- work before 2026-08-29 landed by merge; git log --merges covers it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 41923eb1f9510ab53804ca8dc6fe30a9a7eac849)
2026-08-30 20:44:14 +02:00
aee78c2296 feat(site): tutorial chapter for durable and resident storage modes
- new chapter 7, "Storage modes: durable and resident", covering what
  master gains with the databasev2 cherry-pick: durable: false for a
  RAM-only table, resident: keys for a table that outgrows RAM
- states the parts a reader would otherwise hit as surprises: a
  keys-resident table is REFUSED at startup without WO_DATA, an update
  appends a delta rather than rewriting the row, and the chain is
  bounded at 16 links so a hot row does not degrade reads or replay
- quotes the measured 2.55x smaller resident set, not an estimate
- actors/deps/serving shift to ord 8/9/10; seeding is ord-driven so an
  existing WO_DATA keeps its rows and only a fresh boot reseeds
- home card says a table can be RAM-only or outgrow RAM
- two gate legs pin the new chapter; site-accept 23 checks, 0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b503c0db50aa737dd06ef6d17151c654a42c59b)
2026-08-30 20:38:03 +02:00
710325b94a docs(db2-chain): close out iteration 11 on the board
- status: done in the story frontmatter (was the non-conventional
  "complete"; the board's axis uses done/in-progress/pending/hold)
- board row 11: what landed, the WO_WAL_UPDATE correction, the ceiling
  removed as unreachable, and the one criterion still weaker than
  written (expected value, not a resident: all oracle)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit de39a88e81e97bf6f8b75e9f15331aae20f7ab29)
2026-08-30 20:38:03 +02:00
68dd3d88b8 test(db2-chain): cover flattening, and drop a ceiling no input could reach
- flattened row image is WO_WAL_UPDATE, not WO_WAL_INSERT: the row's
  original INSERT is already in a live log, so a second one for the same
  id is a duplicate replay refuses as corruption. INSERT is right only
  for compaction, which builds a fresh log
- remove WO_CKPT_MAX_GARBAGE: with the absolute term at 64 MiB, garbage
  large enough to reach a 256 MiB ceiling has already tripped it, so the
  branch was unreachable. Postgres needs both constants because it
  thresholds on tuples with its pair at opposite ends; this thresholds
  on bytes, where one constant does both jobs
- test_delta_chain_flattens_at_k: chain depth stays <= WO_DELTA_MAX_HOPS
  across 2K+2 updates, and a reset is observed
- test_delta_chain_flatten_replays: a flattened chain replays correctly
- test_keys_resident_indexed_across_flatten: a delta on an indexed
  column composes with flattening, checked at every step across the
  bound and after restart. Found no product defect
- test_should_compact_absolute_and_ceiling: pins the absolute term, the
  boundary just under it, and the small-log case the ratio still governs
- test_wal 5700 pass / 0 fail; wovm-test and woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f93b5d9db753305c297e868d977670e6d703684c)
2026-08-30 20:38:03 +02:00
2cad84b7c6 feat(db2-chains): bound a keys-resident row's delta chain
TESTS DELIBERATELY HELD at the developer's instruction — logic only.
The existing suite passes (36 suites, 0 fail) but exercises NEITHER new
behaviour: nothing builds a 16-deep chain, and no checkpoint test uses a
log near 64 MiB. Green here means "did not break what existed".

- tier 1: wo_wal_fold_row_at gains hops_out. The walk already visits
  every hop, so the depth is free — this is the design's pd_prune_xid,
  a cheap "is work worth doing" hint taken from work already happening
- the update path branches on it: past WO_DELTA_MAX_HOPS (16) it writes
  a full-row image instead of a delta, terminating the chain. `r`
  already holds the complete post-update row because index maintenance
  required folding it, so flattening costs bytes, not an extra read
- wo_wal_append_row_image encodes from a caller-held row, as
  WO_WAL_INSERT: a chain's base must replay into a database where
  nothing precedes it, so replay/compaction/fold need no change
- tier 2: should_compact gains a TRIGGERING absolute term and a ceiling.
  Our `floor` SUPPRESSES on a small log — the opposite of postgres's
  vac_base_thresh, which triggers on a small absolute problem the
  proportion hides. We had the proportion and the suppressor and
  neither real guard
- verified by construction, not test: both update entry points converge
  on row_apply_field_keys; db.c captures next_offset BEFORE calling in,
  so the re-point is transparent to which record type was written

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1b808abd5942de81c3a6416714d1302384103040)
2026-08-30 20:38:03 +02:00
841f6c8f3f feat(db2-keys): gate the residency measurement, close out task 7
- new `residency` leg in db-bench.py driving docs/examples/residency-bench:
  two tables identical except the annotation, control cap + binding cap
- ITS OWN PROGRAM, not a db-bench mode: declaring a resident: keys table
  is a WHOLE-PROGRAM constraint, so the no-WO_DATA refusal fires for
  every mode in the module. Putting those classes in db-bench's shared
  types made growth/ceiling/randread — which run without WO_DATA —
  refuse to start. Caught by running the leg, not by reading it
- gates the RATIOS, waives the absolutes: ops/sec under a cap is swap
  and disk I/O and belongs to the box. Same split randread makes
- rss_ratio 2.55 floor 2.0 tol 10% (structural, like bytes_per_row);
  overcap_vs_swap_x 1.53 floor 1.0; in_ram_cost_x 4.23 ceiling 8.0;
  all_collapse_x 105.4 floor 2.0
- all_collapse_x exists because the leg's FIRST run silently measured
  nothing: at QUICK's 40k rows a 48 MiB cap binds neither mode, so the
  "over-cap" half was not over cap. The cap now scales with N and the
  leg asserts it binds
- verified the gate bites: rss_ratio 1.4, overcap_vs_swap_x 0.6 and
  in_ram_cost_x 12.0 are all rejected
- task 7 closed: both criteria moved to Met with how each was verified

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a310496664982c372f51b43113465eb8ad9e9fb5)
2026-08-30 20:38:03 +02:00
6fc5b4b7d4 feat(db2-keys): GB-scale bench modes, unmeasured
- hreadall/hreadkeys: the same resident A/B as wread_*, but ~2 KB per
  row so a GB of data is reachable in a few hundred thousand inserts
- the insert path is fsync-bound at roughly 2 000 rows/s, so row COUNT
  is the expensive axis and row SIZE is nearly free — 20k rows already
  produce 38 MB
- NOT RUN: the GB-scale measurement was called off. These modes are
  committed working and typechecking so the leg can be run later
  without rebuilding it, not because a result exists

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit abc276ac39dd45a1052b6ae24d25aedb9eea3e1e)
2026-08-30 20:38:03 +02:00
882c1f7d24 feat(db2-keys): task 7 — measure resident: keys against swapping
- two tables identical except the annotation, 200k rows, 40k reads in
  one key order, WAL on ext4 (not /tmp, which is tmpfs here and would
  have put the log in RAM), rootless cgroup v2 cap
- WIDE shape, 2.55x smaller resident set: 34.4 MB vs 87.5 MB. That is
  the real win and the thing the mode was built for
- under a 48 MB cap (between the two resident sets): keys 19635 ops/s
  vs all 12854 — only 1.53x faster than letting the kernel swap
- degradation is far gentler though: all collapses 105x from its own
  uncapped throughput, keys 16x
- costs 4.2x read throughput when memory is not tight, and writes are
  markedly slower — the keys fill did not finish in 2 min where the
  resident fill plus 40k reads did. No design doc had costed writes
- THE UNANTICIPATED FINDING: cgroup limits charge the PAGE CACHE, so
  moving rows to a file does not escape a container memory limit. WAL
  37 MB + RSS 34 MB cannot both live under a 48 MB cap, so every pread
  reaches disk. The premise "the page cache will hold the hot rows"
  fails in exactly the deployment this targets
- first attempt used Int-only rows and showed parity; recorded, because
  drop_payload frees a field's VALUE and an Int's value is its inline
  slot word, so that shape cannot benefit and would have condemned the
  feature for the wrong reason
- verdict: keep it, to fit ~2.5x more data in given RAM — not to make
  an over-capacity table fast

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7cba9b1174b0bf581314b3147e25cc49e6f49464)
2026-08-30 20:38:03 +02:00
cfd660a5e6 docs(db2-chains): spec + story for bounding a row's delta chain
- fixes a limitation iteration 2 shipped: compaction was supposed to
  bound chain length, but wo_wal_should_compact triggers on a whole-log
  byte ratio and cannot see one hot row's chain
- tier 1, flatten on update: the update path ALREADY folds the row for
  index maintenance and the fold already walks hop by hop, so it reports
  depth for free. Past a fixed K it writes a full row instead of a
  delta. Read <= K+1 reads, replay O(K^2) per row. No format change, no
  per-row RAM, no new trigger
- tier 2: our compaction policy has a proportional term and a
  SUPPRESSOR misleadingly called a floor; postgres's floor TRIGGERS on
  small absolute garbage. Add that term and a ceiling
- design read from .dev/reference/postgresql, not recalled:
  heap_page_prune_opt gates on an O(1) on-page hint then page fullness
  against Max(fillfactor, BLCKSZ/10); autovacuum uses base + scale *
  reltuples clamped by a max (50, 0.2, 1e8). Neither thresholds on
  new-bytes-versus-old-bytes
- K deliberately does NOT scale with table size: postgres scales a
  table-level aggregate with proportional harm, ours is per-row with
  additive cost, so scaling up would make big databases boot worst
- the story says plainly it should NOT be next: task 7 has still never
  measured whether resident: keys beats the kernel's own paging

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f667cad2cfbe187b5973440ab1af015b2df288f8)
2026-08-30 20:38:03 +02:00
b058feb517 docs(db2-delta): guide to log-structured rows for a new reader
- explains replay, the row chain, how a checkpoint flattens it, and why
  replay of a long chain is quadratic
- worked SKU example with the actual record layout and back-pointers,
  and a trace of the fold showing first-seen-wins
- states plainly why the checkpoint does not bound the hot-row case:
  both triggers are ratios over the whole log and nothing counts
  per-row chain length
- records the bounded-memory vs linear-time conflict behind the O(N^2)
  replay rather than presenting it as an oversight
- closes with the reviewing lesson, since this shape survived several
  rounds: complexity bugs hide in the caller's loop, not in the linear
  helper being read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e6434403d566b5d72a24e9c0dcad1f25a2c16320)
2026-08-30 20:38:03 +02:00
64035bf177 docs(db2-delta): resident:keys has storage; move done criteria to Met
- "no storage behind it" / "nothing yet stores a table that way" was
  false — CRUD, checkpoint survival and updates all landed; replaced
  with an accurate summary naming task 6/7 as what remains
- the three checked delete/delete-replay/update criteria sat in
  Outstanding despite being done; moved to Met, leaving Outstanding
  holding only genuine task 6/7 work

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b575678ee95fa3125fa4e8145320a9cdca10ba38)
2026-08-30 20:38:03 +02:00
e08c26309a feat(db2-delta): lift the resident:keys refusal, prove it end to end
- loader.c: delete the INCOMPLETE-update BAIL; durable:false +
  resident:keys stays refused (nowhere to read from)
- table.c: root-cause fix for the Text-index gap — a keys-resident
  borrow now holds ENGINE values, matching wo_row_ptr's contract
  (table.h's "no VM pointer" doctrine), not a VM-decoded row. Fixes
  idx_hash/idx_cols_equal/wo_idx_probe AND db.c's GET_FIELD/PROBE
  arms with one change; reproduced pre-fix as an ASan
  heap-buffer-overflow
- docs/examples/residency: Product is genuinely resident:keys;
  residency-accept.sh's refusal leg replaced by proving the program
  runs and stock survives a restart (11/0)
- test_wal.c: oracle test drives resident:all and resident:keys
  through the same update sequence and asserts identical rows;
  Text-indexed-update test catches the representation bug; five
  pre-existing tests corrected to the fixed contract (4746/0)
- story, README, status board, CODE-LOGIC.md updated; three known
  limitations documented: mid-drain stale reads, O(N^2) replay in
  chain length, compaction blind to per-row chain length

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b87c68f950f01aa5e572fbb86a0f374adc83d813)
2026-08-30 20:37:49 +02:00
048633bf27 docs(db2-delta): correct a line citation before execution
- the plan placed the fold near wo_wal_read_row_at at "line ~600";
  it is at line 794
- every other citation verified: wal.h:46, table.c:452/487,
  db.c:88/289, wal.c:738, wal.c:861

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c6cd48679e21e510696391b340c735aa0ad098f9)
2026-08-30 20:37:27 +02:00
3f88b07abb docs(db2-delta): implementation plan for keys-resident delta updates
- 6 tasks: the record kind, the fold, updates + indexes, the request
  path and group commit, replay + compaction, then lifting the loader
  refusal and proving it end to end
- the fold is written ONCE and called from three places; the tests are
  arranged to prove each caller separately, and the plan says that
  wanting a second fold "just for this caller" means the design failed
- Task 2 includes a same-field ordering test specifically, because a
  fold walking the chain backwards the wrong way returns plausible data
  and is otherwise invisible
- Task 6 step 1 audits the db.c request arms BEFORE lifting anything —
  they were never audited for keys-residency the way the inline path
  was, and the last audit of that kind found delete corrupting memory
- self-review found the spec's crash criterion had no task: added a step
  that commits a delta, skips the re-point, and replays, which is the
  state a crash between barrier and flush leaves behind
- every symbol the plan names verified to exist in database/src
- code-free per house convention; the writing-plans skill wants code
  blocks and the project rule overrides it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit abb8fc9454bdca9d8d703c8cfeb224f89a93d14d)
2026-08-30 20:37:27 +02:00
04017aea26 docs(db2-keys): spec — delta records for keys-resident updates
- updates append a DELTA (class, id, field, value, back-pointer), not a
  full row. The workload decides it: a product catalogue changes one
  narrow field of a wide row on every order, so a full-row append would
  rewrite every field to move one integer on a shop's hottest path
- the back-pointer keeps the id map at one slot per row, which is the
  mode's whole premise; a map growing per update would defeat it
- ONE fold function, three callers (read, replay, compaction). Three
  implementations of one rule is how they drift, and a fold that differs
  between reading and replaying is a database that changes its mind at
  boot. Named as the design's principal risk
- indexed columns MAY change: price is exactly what a catalogue indexes,
  so forbidding it would be a restriction users meet immediately
- no chain cap, deliberately. Compaction already rewrites live rows, so
  every checkpoint resets every chain, and deltas grow the log which
  pulls the next checkpoint forward — the workload that lengthens chains
  triggers the fold that shortens them
- the risk that accepts: one hot SKU under an otherwise quiet write
  rate. Task 7's benchmark must include it
- supersedes the 2026-08-26 spec's one-line full-row Update sketch,
  marked in place rather than left as a second design in the tree

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c9a88b05c4c62a5df13253686c990aaccc3f9cbb)
2026-08-30 20:37:27 +02:00
0b0d54121f docs(db2-keys): the residency example becomes a product catalogue
- the motivating workload was an audit log, which is append-only and so
  argues for nothing. A catalogue is the real case: stable ids, and
  stock moving on every order while name/price/sku sit still
- Product (durable, resident: all) and Cart (durable: false), with
  place_order decrementing stock through a write-through field assign
- run `order` twice and stock goes 10 -> 7 -> 4: a level below the
  seeded 10 can only mean an earlier order's UPDATE replayed. That is
  the stronger claim — not just that inserts survive, but that a field
  change does
- caught by running it three times: my first assertion required
  before == 10, which only holds on a fresh seed and failed on the
  third run even though the data was correct
- gate gains a leg for the update-replay claim; residency 12/0
- the commented resident: keys block now argues the DESIGN too: only
  stock changes per sale, so appending the whole row would rewrite
  every field to move one integer on a shop's hottest write path

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d4104dcc5e3a460459dbf2d24043ce25b113bcdf)
2026-08-30 20:37:27 +02:00
7b39da7eb6 fix(db2-keys): a logged delete must replay on a keys-resident table
- wo_row_remove's keys arm borrows the row from the log to find its
  index entries, and a borrow reads through db->rt->wal. At boot that
  pointer is not wired yet: main.c replays first (main.c:226) and
  assigns rt.wal afterwards (main.c:268)
- so the borrow found no log, the remove failed, and replay reported a
  valid tombstone as CORRUPTION. An UPDATE record would have failed the
  same way, since replay applies it as remove-then-recreate
- replay now lends the runtime a read-only view over the fd it already
  has open, for the replay's duration only, and restores what was there
- broken by the delete fix in 76b8fd9 — deletes worked in-process but
  their tombstones broke the next boot. Unreachable in production only
  because the loader still refuses the annotation
- pinned by test_keys_resident_delete_then_replay, verified failing
  against the unfixed code (2 failures) and clean with it
- found by asking whether the read-modify-append plan was ready, not by
  a gate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit dc25462461b9f79d70c803f7174adc90fa16c90e)
2026-08-30 20:37:27 +02:00
516bd8362d docs(db2-keys): a runnable example for per-table storage
- docs/examples/residency: one program, two tables filled by the same
  loop, differing only in the annotation. Run twice against one WO_DATA
  and orders replay while sessions do not
- the example checks its own claim (exits 1 if a durable:false table
  survives, or a durable:true one fails to replay) rather than narrating
  it in a print
- resident: keys is written out as a commented block with the loader's
  exact refusal, so the frontier is visible in the example rather than
  only in a story. It documents WHERE the refusal happens: woc compiles
  it and emits a .wob; wovm exits 2, because the annotation is a
  load-time property
- residency-accept gains two legs: the example runs and its restart
  claim holds, and the refusal message the README quotes is checked so
  doc and code cannot drift apart
- the gate writes the example's output to /tmp/residency.log,
  banner-separated, for tail -F
- README commands verified verbatim; they needed mkdir -p because wovm
  will not create WO_DATA

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c9c7e03e62c3918cb65ef7994d1e33a0c5337b71)
2026-08-30 20:37:27 +02:00
7ad52937b2 fix(db2-keys): delete on a keys-resident table was memory corruption
- wo_row_remove read the id map's value as a slot, but on a keys table
  that value is a LOG OFFSET (hput(t, id, wal_off + 1)). slot_row does
  no bounds check, so a delete indexed t->slabs[] with a byte offset and
  then called db_val_free on whatever it landed on — arbitrary frees,
  not a wrong answer
- keys tables now take their own arm: no slab slot, no bitmap bit, no
  free-list entry to return. The index hook needs the row's values, so
  the row is borrowed from the log for exactly that long
- wo_row_ptr carried the same trap and is public. It cannot refuse keys
  tables outright (insert legitimately calls it while the map still
  holds a slot), so it now detects the offset case — index past the
  slabs, or bitmap bit clear — and returns NULL. Callers all handle NULL
- test_keys_resident_delete pins it; it SEGVs against the old code,
  verified by reverting the fix rather than assumed
- found while auditing every hget() reader before narrowing the loader
  refusal to allow benchmarking. The refusal was justified in the docs
  by "updates are unimplemented" while actually standing in front of
  this too: a guard whose stated reason is narrower than its real one
  gets removed by someone who believes the stated reason

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 76b8fd944af9ed062467bdf9ab93c2e96dd198cf)
2026-08-30 20:37:27 +02:00
97c7c40fd8 docs(db2-chain-review): review the databasev2 chain and dependency graph
- chain is a cross-track field (1-6); only databasev2 3 and 4 carry it,
  and iteration 2 — critical path, in-progress — carries none, so the
  board's chain query cannot see it
- 00-story.md's ASCII graph draws 3 before 4; the chain field and the
  history both say 4 first (4 part A 08-28, 3 08-29). Graph is wrong
- graph also contradicts its own prose on edge direction, and still
  draws the 2-5-6 path the 2026-08-27 amendment retired
- iteration 3's hazard section is stale: it says nothing fails "because
  iteration 2's storage half is unimplemented", which 5c/5d ended
- and it was incomplete: it named offsets going stale, but compaction
  walked the bitmap, which a keys row has no bit in, so those rows
  would have been dropped from the new log outright — data loss, not a
  bad pointer, and offset-rebuilding would not have caught it
- coupling is now bidirectional: wal.c compaction calls iteration 2's
  wo_row_next_id / wo_row_offset1 / wo_row_set_offset
- nothing in the track is blocked on anything else in the track

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2ecaf0c95f40c6d6c8cc2fde687c82d63cda1f7e)
2026-08-30 20:37:21 +02:00
8311330531 docs(db2-keys): reconcile databasev2 and porch markdown with the code
- loader's resident:keys refusal said "rows are still fully resident"
  and "until tasks 5c/5d land". Both false since f606fc9. Corrected to
  name the real blocker: UPDATE needs read-modify-append
- databasev2 00-story: the sequence graph drew 2->3->4, which reads as
  3 needing 2 and 4 needing 3. Both backwards, and it still drew the
  2->5->6 path the 2026-08-27 amendment retired. Redrawn stating only
  real dependencies, with 4 and 3 shown as composing rather than
  ordered, and the execution order that actually happened
- databasev2 03: the hazard and its Outstanding entry both claimed
  nothing fails "because iteration 2's storage half is unimplemented".
  Marked discharged, and recorded that the hazard named only half the
  danger — the bitmap walk would have dropped keys rows outright
- databasev2 06: pending -> hold (largely superseded, revisit only on
  a measurement); dated its 5c/5d references
- porch 01: rewritten to the settled shape. readiness ready, status
  in-progress, phases B and C marked superseded with why
- porch 01 claimed time.after "is still a reserved builtin id". False —
  builtin 90, implemented. That claim is what made the iteration look
  cheaper than it is
- porch README gains honest ledger rows for both features (partial,
  being rebuilt), not shipped
- skill-catalog README pointed at a story path that moved tracks;
  linkcheck now 0 broken

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b3d8c403e1d19ac27ec966de85cb293e0765795c)
2026-08-30 20:36:55 +02:00
533fc5294f feat(db2-keys): rewire remaining readers, survive compaction
- wo_row_read and the @unique shadow probe go through borrow/release;
  release runs before every exit, including wo_row_read's early return
- updates on a keys table refused explicitly in wo_row_update_field and
  the slot variant: no slab slot to mutate, and writing the borrow's
  scratch would discard the write silently. Needs read-modify-append
- compaction walked the bitmap, which a keys row has no bit in — every
  such row would have been dropped from the new log. Now walks
  wo_row_next_id and re-points each row to where it lands
- moves records byte-for-byte (copy_record) rather than decoding: a
  borrowed row holds VM values, enc_val expects engine values, and ASan
  caught that mismatch as a 4294967292-byte memcpy
- wo_row_set_offset updates a value in place and never rehashes, so a
  wo_row_next_id cursor stays valid while compaction re-points
- a compaction that fails after moving rows is fatal: the map would name
  an unlinked temp file, and the intact log replays correctly
- test_keys_resident_survives_compaction pins both failure modes; rows
  rewrite in hash order so offsets really move
- loader still refuses resident: keys — updates are not implemented

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f606fc9b76d983cac2b348f03f7d4f01433cd905)
2026-08-30 20:36:31 +02:00
02b4b13a52 Merge master into db-residency-doctrine — and close the two half-exposed features
The branch was 17 ahead / 25 behind with 11 conflicting files, and drifting
further: db.c had been rewritten twice on master since (group commit, then
compaction). Resolved rather than rebased so both histories stay legible.

Conflicts, and how each was settled:

- db.c: BOTH semantics kept. Master's fatal path and compaction check now sit
  behind the branch's `table_is_durable` predicate, in all three inline arms —
  a volatile table reaches neither the barrier nor the compaction check
- db-bench sample: every mode from both sides (growth, growth-verify, randread,
  replayseed, wmix) and ONE `boot` mode, which both sides had added
  independently
- db-bench.py: all six legs kept. Both sides had also grown the same
  WAL-size helper under different names; collapsed into one
- perf-targets: the branch's §5 (RAM ceiling) then master's §6/§7 — master's
  numbering had already assumed a §5 it did not have
- story frontmatter: master's `status` (the landing truth) plus the branch's
  `readiness` axis. 03 would have read `done` + `refine`, which is a
  contradiction — it was brainstormed and landed on master, so `ready`
- board: both standup blocks newest-first; master's chain rows (a superset);
  the branch's databasev2 1-2 rows with master's 3-4. Fixed a stray `|` in
  master's row 3
- baseline: master's, then REGENERATED from a full campaign — 143 metrics,
  132 checks, 0 failures with both sides' legs present

TWO HALF-EXPOSED FEATURES FIXED, because the merge rule is that master gets
no feature that is honoured in name only:

- `resident: keys` PARSED, set a .wob flag, and did nothing: rows stayed fully
  resident. A developer could declare a 120 GB table keys-resident, watch it
  compile, and be OOM-killed. The loader now REFUSES it with a message naming
  what to write instead, until tasks 5c/5d land. The compiler still parses it
  and its AST golden still passes, so the grammar work stays tested
- `durable: false` was honoured ONLY on the inline path. wo_db_exec_req had no
  guard at all, so a volatile table written from an actor on a worker shard
  would still be logged — precisely porch's session-table case, and precisely
  what iteration 2 exists to provide. All three request-path arms now carry the
  same predicate. Found by reading the merged code, not by a test: the obvious
  probe runs main() on the primary and therefore only exercises the inline path

Verified on the merged tree: wovm-test 0, woc-test 0, oop-e2e 122/0,
residency-accept 8/0, db-bench 132/0, linkcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 10:14:25 +02:00
8b29eb492c docs(db): T6 closeout — checkpoint documented, chain's last link lands
databasev2 3, task 6. Documentation, plus three gate-tolerance
corrections that are justified rather than silent.

- 04-db-binding.md: the NORMATIVE rule — compaction may run only where
  nothing is staged (a correctness requirement, not scheduling), recovery
  is unchanged, and a failed compaction is a missed optimisation rather
  than a durability event
- database/src/CODE-LOGIC.md: why one file and not snapshot-plus-tail
  (Postgres CANNOT compact — page deltas; ours are full row images, so a
  compacted log IS a store), why rename is the whole crash-safety story,
  why the dump flushes but does NOT fsync when it does, why the
  replacement is preallocated, and where the trigger is checked
- README: the checkpoint knobs, the extended walstats line, the boot mode
- story -> status: done, with criteria split met/outstanding
- board: standup entry in the six-question shape, both rows rewritten

THE OBLIGATION IS AT THE COMPACTOR, not only in a spec: compaction moves
every record, so it invalidates every WAL offset iteration 2's
`resident: keys` stores, and the loop that knows each record's new
position must rebuild that map. Nothing fails today because that storage
half is unimplemented — it would fail later, looking like corruption.

Board claim corrected before it shipped: I wrote that the concurrency
chain is "complete". It is not — chain 5 stays in-progress because
databasev2 4's part B was never done and its premise was invalidated by
part A. Every link has landed its PLANNED work; that is a different
statement.

Gate tolerances, each with the measurement that justifies it:

- ckpt.pause_us_max is no longer gated relatively. The raw pause scales
  with the live set and this workload's live set is not fixed (wmix's
  hist_dump inserts a row per latency bucket), so gating it gates the
  box. Added ckpt.pause_us_per_mb — the engine's own rate, gated for
  real, and the metric that would have caught the 8x dump regression —
  with the absolute 50ms budget still guarding the raw pause
- ram.*.msgrate 15% -> 70%. PRE-EXISTING, and measured: 10.7M-17.9M
  msgs/sec across ten full runs, several predating this work — a 1.67x
  spread against a 15% gate
- durable.sN.*.p99us 100% -> 300%, with more evidence than the first
  widening: mixread 1043/2318/4147us, mixwrite 1623/4446us on the same
  build. Floors stay the real guard and are not slack

Battery: wovm-test 36 suites 0 fail, woc-test, oop-e2e 119/0,
db-bench 117 checks 0 failures, linkcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 06:48:34 +02:00
40d56c4664 perf(wal): checkpoint measured — 2.16x space, 1.78x boot, 2.7ms pause — T5
databasev2 3, task 5.

Full campaign, same workload twice, differing only in whether
checkpointing may fire:

- WAL used 1962358 -> 907094 bytes (2.16x reclaimed)
- boot 114 -> 64 ms (1.78x), median of 3
- stop-the-world pause max 2651us against a STATED 50ms budget

The budget is asserted, not assumed: 50ms is a stall a serving process
can absorb without a client seeing a timeout, and the leg fails if it is
exceeded. The pause is O(live rows) — at ~181 MB/s a 1GB live set implies
~5.5s, which is the number an incremental design must be bought against.
The spec deliberately did not buy it in advance.

FOUND BY MEASURING: the dump was 8x slower than it needed to be. It
flushed through wo_wal_commit, which fdatasyncs, so it paid one barrier
per 256 records. Intermediate durability there is worthless — the temp is
not authoritative until the rename and is fsynced once immediately before
it. With a single final barrier:

- ~107KB live: 23948us -> 2903us
- ~500KB live: 36361us -> 7526us
- ~1.98MB live: 107649us -> 13212us
- marginal ~22 MB/s -> ~181 MB/s, sync-bound to bandwidth-bound

Correctness re-proven after that change: wovm-test 36 suites 0 fail,
test_wal 760 pass including the 40-round kill-during-compaction battery.

Two measurement defects of my own, fixed rather than reported:

- boot measured through the driver's run() helper reported 251ms both
  with and without checkpointing — run() samples RSS on a 250ms poll, so
  every timing floors at the quantum. Measured directly instead, median
  of 3
- ckpt.reclaim_x was recorded as lower-is-better by the default detector,
  which would have PASSED "reclaimed nothing" and FAILED an improvement:
  the feature's central claim, gated backwards. Now higher-is-better,
  gated at 15% while the wall-clock metrics stay wide — waiving them all
  would have left the leg ungated, part A's task 4 mistake

- sample gains a `boot` mode that does nothing, so boot time is boot time
- walstats now reports compactions, pause max/total and compacted bytes
- baseline refreshed from the FULL campaign (N=20000, crash_reps=3), and
  a fresh full run passes 116 checks 0 failures
- gate bites: reclaim_x doctored to 1.0 -> FAIL on exactly that metric

One flake seen and checked, not papered over: durable.sN.query.ops_sec
failed once at 53% below baseline. It is a read-only metric that touches
no WAL code, and a re-run passed 116/0 with the box at load 1.85.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 06:20:13 +02:00