| title | CLAUDE.md — agent guidance for baerly-storage | |||
|---|---|---|---|---|
| audience | agent | |||
| summary | Toolchain, verification matrix, module map, conventions, anti-patterns. The main agent entry point. | |||
| last-reviewed | 2026-05-12 | |||
| tags |
|
|||
| related |
|
Guidance for AI coding agents working in this repo. Keep this file lean — only content that cannot be inferred from the code belongs here.
baerly-storage is a vendorless document database that runs over
any S3-compatible storage API. The data lives in your bucket; the
protocol kernel is small enough that an LLM can use the public API
zero-shot from the .d.ts files alone. Theoretical foundations live
in docs/.
Current state: Open source under Apache-2.0, published publicly as
@gusto/baerly-storage (and the @gusto/create-baerly-storage
scaffolder) on npm (npmjs.com) with publishConfig.access: "public".
See
docs/contributing/publishing.md for
the publish workflow.
The protocol kernel and HTTP server are landed. Day-1 templates ship for Cloudflare Workers and self-hosted Node; both are first-class. AWS Lambda / Bun / Deno / Fly are an adapter package away.
- Package manager: pnpm (
packageManager: pnpm@11.1.2). - Test runner: vitest (
vitest run). Tests import from"vitest". - Type checker: TypeScript 7 /
tsgo(@typescript/native-preview). - Formatter: oxfmt.
- Linter: oxlint.
- Bundler: rolldown (
rolldown.config.ts).
Don't introduce alternate tooling without justification.
Under Claude Code, vitest runs use the compact minimal reporter —
vitest 4.1 auto-detects AI-agent environments, and the repo config
(vitest.config.ts) additionally pins this behavior when
CLAUDECODE=1 is set so it isn't silently broken by detection
changes. Failures still print in full. Override with
--reporter=dot for long suites (test:randomize,
test:fuzz-maintenance) when progress signal matters more than
compactness, or --reporter=default to force the full reporter.
pnpm verify / pnpm test is what humans run before pushing. pnpm verify is split into verify:code (the whole chain minus markdown) and
verify:docs (markdown/mermaid validation); verify runs both. The
lefthook pre-commit hook runs them by glob — verify:code when a
.ts/.tsx file is staged, verify:docs when a .md file is staged
(so a docs-only commit is now validated locally too, and a code-only
commit skips the markdown checks it can't affect) — plus a scoped
pnpm bundle-sizes (see below), but NOT the full pnpm test suite.
pnpm verify:agent / pnpm test:agent are explicit compact-output
variants for environments where the env var isn't propagated.
Agents: don't pipe
verify:agent/test:agentthrough| tail -Nor| head -N. Both scripts are already compact — one finding per line, with full detail preserved on failures. Piping totail/headremoves the lines you need; if the first run prints nothing useful, the output is empty because the gate passed, not because the tail was wrong. Same applies topnpm bundle-sizes.
| Command | What it catches | Runtime | Clean on main? |
|---|---|---|---|
pnpm verify |
typecheck (tsgo --noEmit) + verify:examples + lint (oxlint) + format:check (oxfmt --check ., whole-repo) + lint-format-coverage (ownership guard) + verify:docs (markdown validation) + check-spec-drift + check-version-matrix |
~seconds | ✅ — non-zero exit is your regression |
pnpm verify:agent |
same gate as pnpm verify (code + verify:docs), with tsgo --pretty false + oxlint --format=unix --quiet for one-line-per-finding output (warnings hidden — pnpm verify still surfaces them). Shares the common check spine with verify:code via verify:rest, so a new check is added once; the two differ only in the compact typecheck/lint flags and that verify:agent runs the whole gate (it's invoked whole by agents, not glob-dispatched like the lefthook hook). |
~seconds | ✅ — same gate as verify, just quieter |
node scripts/lint-package-layers.mjs |
enforces the package import allow list (docs/architecture.md §Package layers, which also carries the rationale). Runs as part of pnpm verify. |
~ms | ✅ |
node scripts/lint-format-coverage.mjs |
anti-silent-gap guard for the format/validate ownership model: every git-tracked file must be owned by exactly one mechanism — oxfmt formats it (code/markup), it is validated markdown, or it is intentionally-unformatted data/config/binary (allowlisted). Fails on a new unowned file type (.py, .toml, …) so it can't slip through green. Keep allowlists in sync with .oxfmtrc.json + .remarkignore. Runs as part of pnpm verify. |
~ms | ✅ |
pnpm verify:docs |
verify-docs.mjs (frontmatter related: cross-link resolver + audience: field audit + 180-day last-reviewed: staleness check — scoped to docs/, where that frontmatter convention lives) + verify-docs-taxonomy.mjs (controlled doc_type: vocabulary + docs/spec / docs/adr index coverage and section routing) + remark-validate-links (inline Markdown link + heading-anchor validation across all tracked Markdown — docs/, root README/CLAUDE/AGENTS, packages/**/API.md, examples — runnable standalone as verify:doc-links) + verify-mermaid.mjs (parses every fenced ```mermaid block in tracked Markdown with the real mermaid engine under a happy-dom shim — the parser GitHub uses; catches unquoted-label syntax errors remark-validate-links can't see; standalone as verify:mermaid). Runs as part of pnpm verify. |
~seconds | ✅ |
pnpm verify:examples |
runs each scaffoldable example's tsc -b --noEmit (minimal-cloudflare, minimal-node, react-cloudflare, react-node) so SPA + Worker bugs in the templates fail fast |
~seconds | ✅ |
pnpm test |
vitest unit + integration (zero infra) — includes the memory + local-fs variants of randomized.test.ts |
~3s | ✅ — Minio + credentials tests are gated, see below |
pnpm test:agent |
same gate as pnpm test, with --reporter=minimal --silent=passed-only baked in (failures still full-detail). Works regardless of CLAUDECODE |
~3s | ✅ — same gate as test, just quieter |
pnpm test:minio |
adds the Minio-gated suites: the clock behavior block of time.test.ts, the node-minio variant of randomized.test.ts, and adapter-node Minio conformance |
~10s | ✅ when pnpm dev:storage is up |
pnpm test:conformance |
adds conformance.test.ts (needs Minio + credentials files) |
~30s | requires credentials in credentials/{aws,gcs,cloudflare}.json |
pnpm test:export-smoke |
adds export-smoke.test.ts (LogEntry round-trip into Postgres; needs local Postgres on :5433) |
~5s | ✅ when pnpm dev:storage is up |
pnpm test:export-round-trip |
full export → SQLite → restore → byte-equal dump | ~5–10s | ✅ when sqlite3 is on PATH (auto-skips otherwise) |
pnpm test:adapter-cloudflare |
runs r2BindingStorage conformance, the cloudflare-r2 variant of randomized.test.ts, the cloudflare-r2 variant of collection-api.test.ts, and the cloudflare-r2 variant of http-conformance.test.ts under miniflare (@cloudflare/vitest-pool-workers, project cloudflare-pool) |
~3s | ✅ — first run downloads the workerd binary |
pnpm test:http-conformance |
runs the HTTP cascade on memory + local-fs (default project) |
~3s | ✅ |
pnpm test:adapter-node |
runs s3HttpStorage conformance against local Minio |
~10s | ✅ when pnpm dev:storage is up |
pnpm test:adapters |
sequential wrapper: test:adapter-cloudflare then test:adapter-node |
~10s | ✅ when pnpm dev:storage is up |
pnpm test:parity |
cross-adapter parity — one shared Storage conformance + cascade contract across memory / local-fs (+ minio under MINIO=1); the canonical green locally ⇒ green in cloud gate. See docs/contributing/conventions/tests.md. |
~3s (base) | ✅ — no-infra rows; minio gated; R2 via test:adapter-cloudflare |
pnpm format:check |
oxfmt formatting, whole-repo (oxfmt --check ., config-driven via .oxfmtrc.json; respects .gitignore). oxfmt owns code/markup (.ts/.tsx/.mts/.cts/.mjs/.cjs/.js/.json/.jsonc/.html/.css) only — **/*.md/.mdx + docs/spec/attachments/** (regenerated evidence) are in ignorePatterns, so markdown is never reformatted (it is validated by verify:docs instead). Part of pnpm verify and auto-fixed by the lefthook pre-commit format hook |
~seconds | ✅ |
pnpm build |
rolldown bundle to dist/ |
~seconds | ✅ |
pnpm test:randomize |
property-based fuzzer (cranks FC_NUM_RUNS for fast-check arbitraries). The randomized cascade itself is fault-injection-driven so FC_NUM_RUNS is a no-op for randomized.test.ts — all four variants (memory / local-fs / cloudflare-r2 / node-minio) still run, but only the property tests in the rest of the suite scale up |
run for minutes | use when changing protocol code |
pnpm test:fuzz-maintenance |
crash-injection fuzzer for the maintenance loop (maintenance-crash-fuzz.test.ts) — aborts the K-th storage op inside Writer / compact() / runGc() and asserts the reader still sees a consistent row set |
minutes-hours at FC_NUM_RUNS=10000 |
use after touching compactor.ts / gc.ts / writer.ts |
pnpm test:mutate |
StrykerJS mutation testing scoped to packages/protocol/src/** (pure kernel) — measures test quality, surfaces uncovered behavior as surviving mutants. Manual only: not in pnpm verify / pnpm test / CI, exits 0 regardless of score. See docs/contributing/mutation-testing.md |
minutes | ✅ — report-only, never gates |
pnpm worktree:bootstrap |
pnpm install --frozen-lockfile + pnpm run build. Run this once after git worktree add to prime dist/ so baerly, pnpm bundle-sizes, and any dist-consuming test work. verify:agent itself doesn't need it; everything else does |
~10-30s | n/a |
pnpm dev:storage |
brings up Minio :9102 + Toxiproxy :9104 + Postgres :5433 |
n/a | required for test:minio / test:conformance / test:export-smoke / test:adapter-node / test:adapters |
pnpm test:manual-e2e |
runs manual-e2e/cloudflare/e2e.test.ts + manual-e2e/node/e2e.test.ts against deployed URLs (HTTP conformance cascade + latency probe + long-poll wall-clock + 401 sniff) |
minutes per run | requires CF_DEPLOY_URL + NODE_DEPLOY_URL + SHARED_SECRET (+ CF_R2_* / AWS_* for the conformance cascade); manual deploy lifecycle in manual-e2e/README.md |
pnpm bench:r2 |
one-shot R2-contention bench (S1 / S2-idle / S3-toxic); validates the idle-reader bound on the wire — exit 0 when bound holds, 1 when violated | ~1–5 min per scenario | requires pnpm dev:storage; see bench/README.md |
pnpm bench:load |
one-shot load harness on memory backend (no infra); writes one JSON per run to bench/results/load/ |
~seconds per preset | ✅ on main — no infra required; see bench/README.md |
pnpm bench:load:minio |
same as bench:load but with --variant=node-minio against local Minio |
~30s–2 min per preset | requires MINIO=1 + pnpm dev:storage |
pnpm bench:load:matrix |
sequential sweep over presets × variants × cache modes; writes one timestamped subdirectory under bench/results/load/ |
minutes–tens of minutes | partial: memory + local-fs rows always; node-minio rows require MINIO=1 + pnpm dev:storage |
pnpm bench:lsn-reverse-walk |
quantifies bytes-listed reduction of descending base-32 LSN encoding vs. ascending forward-list + in-memory reverse (patent C3 evidence). Populates 100k synthetic LSN-shaped keys into two MemoryStorage buckets (DESC + ASC arms), measures sum-of-key-lengths yielded by Storage.list for K∈{10,100,1000,10000}, writes JSON to bench/results/lsn-reverse-walk/. Baseline checked in at docs/spec/attachments/lsn-reverse-walk-baseline.json |
~seconds | ✅ no infra |
pnpm build && pnpm baerly deploy |
runs baerly deploy for a scaffolded app; dispatches on baerly.config.ts:target. Deploys to Cloudflare via wrangler deploy --x-provision --x-auto-create with a wrangler r2 bucket create fallback. The node target self-deploys via your PaaS, VM, or container build (docker build with --with=docker), so it is not accepted here |
seconds to minutes | requires wrangler login |
baerly doctor --target=cloudflare |
walks the deploy invariants and reports findings: wrangler.jsonc, R2 bindings, required secrets, CF Access audience tag, cron triggers, domain/routes coherence | seconds | requires wrangler login; --fix auto-creates missing R2 buckets |
pnpm build && pnpm baerly export --target=sqlite ... |
snapshot dump one collection to SQL | seconds | ✅ no infra |
pnpm create @gusto/baerly-storage@latest . |
bolt-on path for an existing wrangler project: writes baerly.config.ts, patches wrangler.jsonc, prints a worker-entry snippet, and drops/refreshes an AGENTS.md router in the repo. No baerly init CLI subcommand exists |
seconds | ✅ no infra |
pnpm build && pnpm baerly {inspect,admin dump,admin restore} ... |
operator surface: inspect prints a read-only summary of one collection's snapshot / log / index state; admin dump emits canonical NDJSON of the materialised view; admin restore re-imports that NDJSON into a fresh bucket |
seconds | ✅ no infra |
pnpm build && pnpm baerly admin fsck ... |
maintenance surface: admin fsck walks current.json → snapshot hash → log range → index prefixes read-only and exits 4 on any finding |
seconds | ✅ no infra |
cat node_modules/@gusto/baerly-storage/dist/API.md |
~12k-token (~1097-line) public-API reference (soft budget ~12k tokens — now at the ceiling; net-new prose should land in a sibling RECIPES.md rather than grow this file). Read this BEFORE walking the hash-suffixed dist/*.d.ts chain. Named API.md (not AGENTS.md) so it never collides with a scaffolded app's project-root AGENTS.md. Source lives at packages/server/API.md; the rolldown closeBundle step copies it to dist/API.md on every build |
n/a | ✅ |
cat node_modules/@gusto/baerly-storage/dist/CHANGELOG.md |
migration-shaped record of what changed across versions; read it when a remembered API no longer type-checks. Maintained by Changesets at the repo root; the rolldown closeBundle step copies it to dist/CHANGELOG.md on every build |
n/a | ✅ |
pnpm verify is also enforced as a lefthook
pre-commit hook (lefthook.yml); pnpm install wires it up via the
prepare script. The hook runs oxfmt --fix + oxlint --fix, then
verify:code (code-globbed) and/or verify:docs (markdown-globbed) —
together equivalent to pnpm verify on a mixed commit, with neither
running for a commit that stages nothing in its glob — plus
pnpm bundle-sizes only when a
staged file matches packages/**/_.{ts,tsx}(excluding_.test.\*) or
rolldown.config.ts — bundle budgets live in pnpm test/pnpm bundle-sizes, NOT in pnpm verify, so the scoped hook is what
keeps a budget-blowing kernel edit from committing green and surfacing
late (in CI or a manual pnpm test). The hook does not run the
full pnpm testsuite. Bypass withgit commit --no-verify when needed.
pnpm test runs green on a fresh checkout with zero infrastructure
deps. Tests requiring Minio or credentials are gated by env:
- Minio-required tests (the
clock behaviorblock oftests/integration/time.test.ts, and thenode-miniovariant oftests/integration/randomized.test.ts) skip by default. Run them withMINIO=1 pnpm test(alias:pnpm test:minio) afterpnpm dev:storage. tests/integration/conformance.test.tsneeds both Minio and credentials incredentials/{aws,gcs,cloudflare}.json(gitignored). Excluded from the default test glob. Run withpnpm test:conformance.tests/integration/export-smoke.test.tsneeds a local Postgres on127.0.0.1:5433(provisioned bypnpm dev:storage). Excluded from the default test glob. Run withpnpm test:export-smoke.packages/adapter-cloudflare/src/r2-binding-storage.conformance.test.tsruns inside Workerd via thecloudflare-poolvitest project (@cloudflare/vitest-pool-workers, miniflare-backed). The R2 bindingBUCKETis wired invitest.config.tsand re-published onglobalThis.__BAERLY_R2_BINDING__bytests/setup/r2-binding.tsso the conformance factory can consume it. Excluded from the default project's glob; run withpnpm test:adapter-cloudflare(the script also setsADAPTER_CLOUDFLARE=1for any future in-test conditionals). No external network, no credentials.packages/adapter-node/src/s3-http.conformance.test.tsruns against the same local Minio thatpnpm dev:storageprovisions. Gated byMINIO=1viadescribe.runIf; the bucketbaerly-conformance-adapter-nodeis auto-created in the suite'sbeforeAll(409 BucketAlreadyOwnedByYou is tolerated). Run withpnpm test:adapter-node, or both adapter suites in sequence withpnpm test:adapters.tests/integration/collection-api.test.tsdrives the lockeddb.collection(...).{first,all,count,insert,update,replace,delete}surface across three Node-side adapters (memory,local-fs,node-minio).memory+local-fsrun by default;node-miniois gated onMINIO=1(viapnpm test:minio). The Workerd-sidecloudflare-r2variant lives atpackages/adapter-cloudflare/src/collection-api.test.tsand runs under thecloudflare-poolvitest project (viapnpm test:adapter-cloudflare). All variants share the backend-agnostic driver intests/fixtures/collection-api-cascade.ts.tests/integration/maintenance-e2e.test.tsis the end-to-end durability gate: seeds 5000 entries, runsrunScheduledMaintenanceto quiescence, then asserts find() parity, bucket-object-count drop,log_seq_startadvance, and the "< 1 Class A op / writer / hour" idle-reader cost-model bound via a hand-rolled countingStorageproxy. Runsmemory+local-fsvariants in the default project;node-minioandcloudflare-r2are deferred.
randomized.test.ts drives the all-to-all single-key causal-
consistency cascade through Db + Writer (from
@baerly/server) over four storage adapters:
memory—MemoryStorage, shared per-bucket viagetOrCreateMemoryStorageForBucket. Default project, no infra, runs in <1s on every PR.local-fs—LocalFsStorageover a freshmkdtemproot. Default project, no infra, runs in ~1s on every PR.cloudflare-r2—r2BindingStorageover the miniflare R2 binding wired bytests/setup/r2-binding.ts. Lives atpackages/adapter-cloudflare/src/randomized.test.tsand runs under thecloudflare-poolvitest project (Workerd). Excluded from the default glob; run withpnpm test:adapter-cloudflare.node-minio—S3HttpStorageagainst Toxiproxy → Minio with a fault-injection twiddler flipping the proxy every 100 ms. Default project, gated onMINIO=1; run withpnpm test:minio.
The cascade body is shared across projects via
tests/fixtures/randomized-cascade.ts (Node-import-free, so it loads
inside Workerd). The Node-side variant table is in
tests/integration/randomized.test.ts; the Workerd-side entry is in
packages/adapter-cloudflare/src/randomized.test.ts. Each variant
constructs N Storage handles sharing the same backing store, then
spins up N Db + Writer writers all contending on a single
collection log tail / next log/<seq> slot.
Pure-unit tests that always pass: packages/protocol/src/hashing.test.ts,
tests/unit/consistency.test.ts, packages/protocol/src/xml.test.ts,
packages/protocol/src/json.test.ts,
packages/protocol/src/log.test.ts,
packages/protocol/src/storage/memory.test.ts,
packages/adapter-node/src/s3-http.test.ts,
packages/dev/src/local-fs.test.ts,
tests/unit/datatypes.test.ts,
tests/integration/bundle-size.test.ts,
tests/integration/log-emit.test.ts,
tests/integration/put-all-partial-failure.test.ts,
tests/regressions.test.ts,
tests/integration/write-amp.test.ts.
Pattern A/B/C drift across examples/*/AGENTS.md is fenced by
tests/integration/agents-md-drift.test.ts. The Node example servers'
storage-resolution guard (examples/{minimal,react}-node/src/server/resolve-storage.ts)
is unit-tested and kept byte-identical by
tests/integration/node-storage-resolution.test.ts.
Integration tests can run against a local Minio + Toxiproxy stack:
pnpm dev:storage # docker compose up -d --wait (Minio :9102, Toxiproxy :9104, Postgres :5433)
pnpm dev:storage:stop # docker compose downTwo worktrees, two stacks? Set
BAERLY_MINIO_HOST_PORT,BAERLY_TOXIPROXY_HOST_PORT,BAERLY_TOXIPROXY_ADMIN_PORT, andBAERLY_POSTGRES_HOST_PORTin a per-worktree.env.local(or just inline before the command) and the compose stack will bind those host ports instead of the defaults. The test setup helper attests/setup/ports.tsreads the same variables, so consumers stay in sync. Without overrides the secondcompose upstill fails withport already allocated.
See docs/contributing/development.md for full setup.
Read in this order to build a mental model:
packages/server/src/index.ts— public barrel; bundler entry point. The@gusto/baerly-storagenpm package is bundled from here.packages/server/src/db.ts— theDbclass. Public read/write surface for application code.packages/server/src/collection.ts,packages/server/src/query.ts—Collection<T>/Query<T>SQL-shape API + predicate AST.packages/server/src/writer.ts—Writerstateless commit path: PUT content → PUT new index entries → createlog/<seq>viaIf-None-Match: "*"(that create IS the commit; nocurrent.jsonwrite on the commit path) → DELETE stale index entries after the commit.packages/server/src/indexes.ts—IndexDefinition, key encoding (lex-order-preserving base-32), and per-doc projection helpers. Consumed by the writer's hybrid index emission (new markers before the committing log create, stale marker deletes after it) and byrebuildIndex.packages/server/src/rebuild-index.ts—rebuildIndex(storage, currentJsonKey, def)idempotent reconciliation; whatbaerly admin rebuild-indexcalls.packages/server/src/compactor.ts,packages/server/src/gc.ts,packages/server/src/maintenance.ts— durability sweep loops.@baerly/protocol(pure modules; no I/O):packages/protocol/src/json.ts,packages/protocol/src/types.ts,packages/protocol/src/constants.ts,packages/protocol/src/errors.ts,packages/protocol/src/hashing.ts,packages/protocol/src/o-map.ts,packages/protocol/src/time.ts,packages/protocol/src/xml.ts,packages/protocol/src/collection-api.ts(on-the-wireCollection/Query/Predicatecontracts),packages/protocol/src/query/(validate / matches / merge — the kernel half of the predicate algebra),packages/protocol/src/storage/(Storageinterface +MemoryStorageonly — concrete S3/R2 adapters live in@baerly/adapter-node/@baerly/adapter-cloudflare).@baerly/dev(Node-onlyStorageimpls + dev harness):packages/dev/src/local-fs.ts(LocalFsStorage— directory-treeStoragewith content-addressed ETags and atomic writes; used by futurebaerly devand by tests that need cross-Db-instance visibility without Minio).manual-e2e/— manual end-to-end check that drives the HTTP conformance cascade + latency / long-poll / 401 probes against real R2 / real S3. Each subdir now holds only itse2e.test.ts; the deploy artifacts are the production scaffoldsexamples/minimal-cloudflare(viapnpm baerly deploy) andexamples/minimal-node --with=docker(viadocker build && docker run). Manual lifecycle inmanual-e2e/README.md; driven bypnpm test:manual-e2e. Sits at the root alongsidebench/because it's manual maintainer infrastructure, not part of the automated test suite.examples/— runnable example apps that double as the CLI template source.examples/minimal-cloudflare/(R2 +cloudflareAccess→sharedSecret),examples/minimal-node/(S3 + JWKS→sharedSecret, any host that runsnode server.js),examples/react-cloudflare/(full React + Vite SPA over aNoteSchemacollection; dev uses workerd-in-Vite via@cloudflare/vite-plugin+baerlyDevAuthfrom@gusto/baerly-storage/dev/vite), andexamples/react-node/(same SPA +NoteSchema; dev usesbaerlyDev()from@gusto/baerly-storage/dev/viteoverLocalFsStorageas a single-Vite-process middleware) are the production-shaped scaffolds. Each scaffoldable example carries a.baerly/scaffold.jsonmanifest declaring rename sentinels, copy exclusions, and devDep drops. The CLI consumes them at scaffold time viaSTARTER_TO_EXAMPLEinpackages/create-baerly-storage/src/scaffold.ts; the rolldown build copies them intodist/templates/<name>/so the published@gusto/create-baerly-storagebinary is self-contained. Opt-in add-ons live alongside the examples atpackages/create-baerly-storage/templates/addons/<name>/(today: justdocker/— Dockerfile + healthcheck.js + .dockerignore). The scaffolder layers an add-on on top of the base template when--with=<name>is passed (Docker requires--target=node).rolldown.config.tsmirrorstemplates/addons/intodist/templates/addons/so the published binary ships them too. Catalog index inexamples/README.md. Seepackages/create-baerly-storage/AGENTS.mdfor the full scaffold pipeline (examples → dist/templates → tgz → user dir).
The full lifecycle of db.collection().insert() is in
docs/architecture.md — read it before
changing packages/server/src/writer.ts or the query
evaluation path. architecture.md also has a Mermaid dependency
graph if you need finer-grained roles than the groups above.
Path-scoped conventions. Read the matching file before editing.
| When you're editing… | Read first |
|---|---|
tests/** |
docs/contributing/conventions/tests.md |
docs/** |
docs/contributing/conventions/docs.md |
packages/server/src/writer.ts |
docs/spec/sync-protocol.md + docs/spec/causal-consistency-checking.md |
packages/protocol/src/json.ts |
docs/spec/json-merge-patch.md |
packages/protocol/src/log.ts, the log-emit path in writer.ts |
docs/spec/log-entry-shape.md |
packages/server/src/observability/** |
docs/contributing/conventions/observability.md |
Public API on Db / Collection |
docs/contributing/extending.md |
packages/server/src/schema.ts or CollectionDefinition.schema |
docs/contributing/extending.md §"Declare a schema for a collection" |
| Durable schemas, wire, or version axes | docs/contributing/conventions/versioning.md |
Claude users: .claude/rules/{tests,docs,change-discipline}.md
auto-load on matching edits and point at the same files.
- Imports are relative, with explicit
.ts/.tsxextensions.tsconfig.jsonusesmoduleResolution: "bundler"and nobaseUrl. Insidepackages/server/src/writeimport { UUID } from "@baerly/protocol"for cross-package types andimport { makeCollection } from "./collection.ts"for siblings. The.tsextension is required so that Node's native--experimental-strip-typesruntime — used by theexamples/minimal-node/andexamples/react-node/scaffolds, which consume the workspaceexports."."→./src/*.tspaths directly — can resolve relative specifiers. Enforced byoxlint'simport/extensions: ["error", "always", { ignorePackages: true }]onpackages/**+tests/**, and byscripts/add-ts-extensions.mjs --checkon the paths oxlint doesn't lint (bench/,deploy/,examples/,scripts/, root*.config.ts). - Branded types are load-bearing.
UUIDandContentVersionIdexist to prevent confusion bugs. Don't paper over a type mismatch withas string; widen only if you understand why. - Magic values live in
packages/protocol/src/constants.tswith a JSDoc citing where the value comes from (oftendocs/spec/sync-protocol.md). - Errors must be
BaerlyErrorinstances (re-exported from@baerly/protocol). Use thecodediscriminant (error.code === "NetworkError"), notinstanceofchains. Hierarchy lives inpackages/protocol/src/errors.ts. - Tests use vitest.
import { describe, test, it, expect } from "vitest". Don't add jest, mocha, orbun:test. - Public API docs live as JSDoc on
packages/server/src/db.tsandpackages/server/src/table.ts. IDE hover and tsgo consume them directly — no rendered markdown ref to maintain. - Per-collection linearizability is a hard invariant. The
log/<seq>If-None-Match: "*"create linearizes each collection; cross-collection writes are unordered and non-atomic. docs/spec/sync-protocol.md and docs/spec/causal-consistency-checking.md describe how it works. Read those before touchingpackages/server/src/writer.ts.
- ❌ Adding runtime dependencies to anything that ships to user
apps. The runtime footprint of
baerly-storageand the adapters is intentionally small (aws4fetch,@rgrove/parse-xml,hono,jose); every additional dep widens the kernel bundle and the audit surface for users. Justify any addition. - ✅ Build-time / CLI / dev-tooling deps are fair game. Inside
packages/create-baerly-storage/,packages/cli/,packages/dev/,bench/,manual-e2e/,scripts/, andexamples/*/devDependencies, prefer a well-maintained dep over reinventing it in-house. None of this code ends up in a user's production bundle, so the trade-off flips: the cost is one more line in our lockfile, the benefit is less undifferentiated heavy lifting we own forever. Examples worth reaching for here:@clack/prompts,nypm,citty. Still pick maintained, narrow, ESM-friendly packages — but the default answer is "yes, take the dep" rather than "justify it." - ❌ Widening a branded type to its base (
as string,as number). - ❌ Skipping or
.skip()'ing a test to ship. If a test is wrong, fix it; if the code is wrong, fix the code. - ❌ Hard-coding new magic numbers. Add to
packages/protocol/src/constants.ts. - ❌ Trimming JSDoc, comments, or error-message text to squeeze under the
raw/gz bundle-size budget. Those two axes are creep tripwires;
min-gzis the only hard ceiling (it's the real shipped-to-browser cost — see thePOLICYnote intests/integration/bundle-size.test.ts, which the failing assertion now also prints). On an intentional change, rebaseline the KiB line with a dated baseline note — don't golf docs/identifiers/messages. Comments ship un-stripped, but doc + error quality outweigh a few raw bytes. (An axis over onmin-gzis different: that's a real regression — shrink the closure, don't rebaseline.) - ❌ Reintroducing
bun:test, Rome, or baseUrl imports — all replaced. - ❌ Extensionless relative imports (
from "./foo"). Always writefrom "./foo.ts"orfrom "./foo/index.ts". Node's strip-types runtime can't resolve them; oxlint'simport/extensionsrule fails the lint. - ❌ Calling
vitestviapnpm exec vitestor./node_modules/.bin/vitest— both skip thepretesthook (pnpm run build), leavingdist/empty or stale and producing spurious failures in bundle-size / dist-consuming tests. Usepnpm test:agentorpnpm bundle-sizes(whose script self-builds). Same idea for the other tools: preferpnpm verify:agent/pnpm buildoverpnpm exec tsgo/pnpm exec rolldownso the canonical flags (--pretty false,--format=unix --quiet, etc.) come along. - ❌ Proposing maintenance/cleanup/coordination mechanisms that require
operator-installed scheduling (
wrangler.jsonctriggers.crons,node-cron, k8sCronJob, systemd timer, DO Alarms). The kernel must work on a bare bucket with zero operator infrastructure — the pitch is "Just a Bucket" (thesis criterion #6, thesis.md). Scheduled cron is a user-opt-in acceleration via the exportedrunScheduledMaintenanceSDK, never a default and never a requirement. In-band write-tick maintenance is the sanctioned default and it SATISFIES this doctrine (zero operator infrastructure): writes opportunistically tick — a budgetedrunGcslice, then a go/no-go fold bounded by a static two-way snapshot ceiling (Cbytes ANDErows); Cloudflare relocates the fold past the response viactx.waitUntil, everywhere else runs inline. Reads are pure — they never tick. No timer, nosetInterval, no sweep thread, no lease, no environment-divergent behavior, no operator cron, no app-plane knob — the only operator surface is theBAERLY_MAINTENANCE_*env vars read by the adapters.runBoundedMaintenance(packages/server/src/maintenance.ts) is the runner; the in-band tick and the opt-inrunScheduledMaintenanceare the two maintenance triggers that exist. The anti-pattern below still stands — don't require operator-installed scheduling; in-band is the sanctioned shape that needs none. Anti-precedent: Cassandra'sread_repair_chance(removed in 4.0, CASSANDRA-13910) — unbounded probabilistic on-request maintenance is harmful; bounded under a per-pass CPU budget (PostgreSQL HOT pruning precedent) is the safe shape. Use the operator-burden test at docs/contributing/conventions/change-discipline.md before proposing any new background-work mechanism.
- Bugfix? Reproduce with a failing test first. Pick the right test file
by topic (
json.test.ts,time.test.ts, etc.). - New public API method on
Db/Collection? See docs/contributing/extending.md. Add JSDoc with@example— IDEs and tsgo consume it directly. - Touching the sync protocol? Read
docs/spec/sync-protocol.mdanddocs/spec/causal-consistency-checking.md. Add a property-based test intests/integration/randomized.test.tsor a check intests/unit/consistency.test.ts. - Performance change? Run
pnpm test:randomizefor a few minutes. Randomized tests catch races the conformance suite misses. - Scoping from an inbound brief / gap report? Verify each cited
file:line and each named feature with
grep/Readbefore drafting tickets. Inbound briefs can hallucinate file paths, miscount the API surface, or claim missing features that already exist. ~10 minutes of verification up front beats hours of work stuck on phantom references.
- Doc topic map: docs/README.md — start here if unsure where to look.
- Feature → code map: docs/contributing/features.md
- Architecture overview: docs/architecture.md
- Local dev setup: docs/contributing/development.md
- How to add a feature / module / test: docs/contributing/extending.md
- Protocol theory: docs/spec/sync-protocol.md, docs/spec/causal-consistency-checking.md, docs/spec/json-merge-patch.md
- Architecture decisions ("why"): docs/adr/
- Troubleshooting: docs/contributing/development.md#troubleshooting
- Path-scoped conventions: docs/contributing/conventions/ (table at top)