# CLAUDE.md

Guidance for AI agents (and humans) working **in this repository**. If you are instead trying to *use* this library in your own application, read the skills under [`.claude/skills/`](.claude/skills/), they are written for consumers and are the canonical guide to the key/value and transaction APIs.

## What this repository is

A C#/.NET binding for [FoundationDB](https://www.foundationdb.org/) (a distributed, ordered key/value store), plus the general-purpose libraries it is built on. The main solution is **`FoundationDB.Client.slnx`** (the new XML `.slnx` format). Current version: see [`Common/VersionInfo.props`](Common/VersionInfo.props) (`7.4.x`).

The repo holds **two distinct product families**:

- **`SnowBank.*`**, general-purpose foundation, *not* FoundationDB-specific. `SnowBank.Core` is the bedrock (Slices, the Tuple encoding, the CrystalJson stack, collections, async LINQ, UUIDs). Also `SnowBank.Shell`, `SnowBank.Serialization.Json.CodeGen` (a Roslyn source generator), `SnowBank.Networking.*`, `SnowBank.Testing.*`.
- **`FoundationDB.*`**, the actual binding. `FoundationDB.Client` is the core (native interop, transactions, keys/values, subspaces, the Directory layer, tenants, DI). `FoundationDB.Layers.Common` holds demo layers (Map, Index, Vector, Queue, Counter, Blob, …). Aspire, FakeDb, Testing, BindingTester, and the `Fdb*` tools sit around it.

### Dependency direction (do not invert)

```
SnowBank.Core  ◄─  FoundationDB.Client  ◄─  FoundationDB.Layers.Common  ◄─  Layers.Experimental, Linq.Providers
     ▲                                                                  
SnowBank.Shell, SnowBank.Networking.*, SnowBank.Testing.*, SnowBank.Serialization.Json.CodeGen
```

`SnowBank.Core` has **no** project references and must never depend on FoundationDB. `FoundationDB.Client` depends only on `SnowBank.Core`. Tooling/tests reference downward only.

## Build & test

```bash
dotnet build FoundationDB.Client.slnx                  # DEBUG build of everything
dotnet test <Project>.csproj -f net11.0                # ONE suite; always scope the TFM, see "Tests"
```

- **New machine**: the prerequisites (SDK, extra runtimes, Docker, the native client download) are in the [README](README.md) sections *How to build* and *How to test*, verified on Linux. Read them before setting up a host or diagnosing a missing prerequisite.
- **SDK**: pinned in [`global.json`](global.json) to a **.NET 11 preview** SDK (`rollForward: latestMinor`). `LangVersion` is `preview`.
- **Target frameworks**: libraries multi-target `net11.0;net10.0;net8.0` (see [`Directory.Build.props`](Directory.Build.props)). Each project builds once per target. Several projects **also build a `netstandard2.0` "lite" variant**, see [The netstandard2.0 / net472 "lite" targets](#the-netstandard20--net472-lite-targets) below; keeping it green is part of any change to those projects.
- **Build output** goes to `artifacts/` (`ArtifactsPath`), not per-project `bin/obj`.
- **Central package management**: all versions live in [`Directory.Packages.props`](Directory.Packages.props). Add/bump packages there, not in `.csproj` files. **Version policy**: this SDK is a BOM for downstream applications, each pin should be the **latest version that still supports its TFM bucket** (the .NET platform wave is multi-TFM, so net8/net9/net10 usually share one version; ASP.NET Core packages are single-TFM and stay capped per bucket). Deliberate exception: `Microsoft.CodeAnalysis.*` stays low, because it sets the minimum Roslyn/SDK for consumers of the `SnowBank.Serialization.Json.CodeGen` source generator.
- **As a submodule**: a parent repo can override targets via `CoreSdkVersions` (or the finer `CoreSdkRuntimeVersions` / `CoreSdkToolsVersions` / `CloudSdkRuntimeVersions`), and disable the lite targets via `CoreSdkNetStandardEnabled=false`, in its own `Directory.Build.props`. The override import is gated on a `.git` *file* check; bypass with `FDB_BUILD_PROPS_OVERRIDE=1`.
- **Standalone (isolated) build**: to build, test, or pack THIS repo with its own COMPLETE target set (net8/net10/net11 + netstandard2.0 + the net472 validation targets) while it sits under a target-trimming parent, force `CORESDK_STANDALONE_BUILD=true`, which makes `Directory.Build.props` ignore the parent import. **Pass it as an MSBuild `-p:` property, NEVER as an `$env:`/`export`**: a persistent env var leaks into the shell and silently forces every later `dotnet` command into a standalone build (this bit us once; the pack/build scripts were fixed to pass `-p:`). Use [`scripts/build.ps1`](scripts/build.ps1) / [`scripts/build.sh`](scripts/build.sh) for an isolated `restore`+`clean`+`build`, and [`scripts/pack.ps1`](scripts/pack.ps1) / [`scripts/pack.sh`](scripts/pack.sh) for packaging (packing MUST be standalone, or the `.nupkg` ships an incomplete target set).
- **Shared-restore gotcha**: `restore` writes `artifacts/obj/**/project.assets.json` for whatever mode it ran in, and that state is shared. `restore`, `build`, and `clean` MUST use the SAME mode; mixing them fails with `NETSDK1005` ("assets file doesn't have a target for &lt;TFM&gt;"). After a standalone build, the parent must re-run `dotnet restore` to reset the submodule's assets to its trimmed set (and vice-versa); to switch modes cleanly, re-run `dotnet restore` in the target mode (or wipe `artifacts/obj`).
- **Analyzers in the build**: the AoT-compatible libraries (`IsAotCompatible`: `SnowBank.Core`, `FoundationDB.Client`, the layers) run their analyzers in every build ([`Directory.Build.targets`](Directory.Build.targets)): the trim and AoT analysis, and the `SBK` and `FDB` rules of this repository. A trim warning there reaches every trimmed or Native AoT consumer: fix it, or declare `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` so callers see it, and suppress only with a justification that says why it is safe. The other projects keep `RunAnalyzersDuringBuild=false`; [`scripts/dogfood-analyzers.sh`](scripts/dogfood-analyzers.sh) runs the `SBK` and `FDB` rules over the tests and samples.

### Tests

- **NUnit 5 on `master`, NUnit 4 on `release/7.4`**, and the runner **must be 64-bit** (the native client is 64-bit only). Each branch builds, tests and packs against one NUnit version, which is also the minimum the `SnowBank.Testing.*` packages declare: NUnit 5.0.0 from 8.0, NUnit 4.6.1 on 7.4.x.
- **Cherry-picking a test down to `release/7.4`**: NUnit 5's `Assert.ThrowsAsync`, `CatchAsync` and `DoesNotThrowAsync` return a `Task` that must be awaited (`var ex = (await Assert.ThrowsAsync<X>(...))!;`), and that `await` does not compile on NUnit 4, so drop it on the release side. `Assert.That(async () => ..., Throws.InstanceOf<X>(out var captured))` compiles on both and needs no edit. A forgotten `await` on NUnit 5 compiles and asserts nothing: inside an `async` test the compiler warns (CS4014), inside a synchronous test only the analyzer rule NUnit2059 reports it, and test projects build with `RunAnalyzersDuringBuild=false`, so check with `-p:RunAnalyzersDuringBuild=true`.
- **Test order**: `[Canary]` runs a test first (an assumption whose failure explains the failures that follow), `[RunOrder(n)]` ranks basic tests before advanced ones; both come from `SnowBank.Testing` and behave the same on NUnit 4 and 5. Do not use NUnit's `[Order]` (obsolete in NUnit 5) or `[DependsOnTest]` to order tests: a failed dependency makes the runner drop its dependents from the results without reporting them as skipped. Ordering is not a way to share state: each test sets up its own, or uses the OnBeforeAll / OnBeforeEach hooks.
- ⚠️ **`global.json` opts into the MTP mode of `dotnet test`** (`"test": { "runner": "Microsoft.Testing.Platform" }`) and that opt-in is load-bearing: these projects use the NUnit MTP runner, and **MTP v2 refuses to run under the old VSTest bridge on the .NET 10+ SDK**, failing with *"Testing with VSTest target is no longer supported by Microsoft.Testing.Platform"*. Every test project declares `EnableNUnitRunner=true` in its OWN `.csproj`, next to `IsTestProject`, `IsPackable=false` and `RunAnalyzersDuringBuild=false`, so all targets use MTP uniformly. This block is deliberately per-project and NOT shared from `Directory.Build.props`: a shared block can only be conditioned on the project name, and a test project is not required to be named `*.Tests`. **A new test project must carry the block**, or it falls back to VSTest and exits 0 having run nothing, which is indistinguishable from a passing suite.
- **Scoping `dotnet test` to a single project and TFM** (`dotnet test <Project>.csproj -f <tfm>`) is the standard invocation. A solution-wide `dotnet test FoundationDB.Client.slnx` is not supported (MTP requires every project in the run to use the same runner, and the solution includes non-test projects).
- **Running the assembly directly also works and skips the build**, which is faster in a tight loop: `dotnet artifacts/bin/<Project>/debug_net11.0/<Project>.dll`, filtering with `--filter "FullyQualifiedName~<NamePart>"` (not `--treenode-filter`) and `--output Detailed` for per-assert output. The same `--filter` works through `dotnet test`, with or without the `--` separator.
- ⚠️ **Exclude the benchmark categories from normal suite runs**: append `--filter "TestCategory!=LongRunning&TestCategory!=Benchmark"`. The benchmark fixtures are correctly categorised and interactive runners filter them by default, an agent running the assembly UNFILTERED is the only consumer that pays for them (measured: `SnowBank.Core.Tests` net10 5m19s → 4s, net472 6m20s → 8s, keeping 98.4% of tests). Two exceptions: benchmarks stay runnable on purpose via `--filter "TestCategory=Benchmark"`, and a run whose *subject* needs GC/allocation pressure (e.g. the SliceReader GC-safety detector) runs unfiltered, a filtered run is structurally blind to that class.
- `SnowBank.*.Tests` are pure and need no database.
- **`FoundationDB.Tests` requires a running local FoundationDB cluster** and the native `fdb_c` library (`libfdb_c.dylib` on macOS, `libfdb_c.so` on Linux, `fdb_c.dll` on Windows). `FoundationDB.Client.Native` redistributes these.
  - ⚠️ Tests write to a dedicated subspace but **can corrupt data**, only point them at a throwaway local cluster.
- `FoundationDB.FakeDb` provides an in-memory fake for tests that don't want a real cluster.
- Test classes use the `*Facts` naming convention (e.g. `FdbKeyFacts`, `TuPackFacts`).

### Test project layout (which tests go where)

Three test projects, split by what they exercise:

- **`FoundationDB.FakeDb.Tests`**, the FakeDb emulator's OWN tests only: conformance against real FDB semantics, internals, and the differential scenario harness. Docker-free. (FdbLite's emulator tests will land here too.)
- **`FoundationDB.Tests`**, the client binding: transactions, keys, subspaces, values, tenants, error handling, the native handler, plus "don't know where else to put it" tests. Docker-backed (Testcontainers). The Directory-layer tests (`DirectoryFacts`, `FdbPathFacts`) live here and are SPECIAL: they open the database manually at the root, because a test *of* the Directory layer cannot use the Directory-layer-based partition helper (that would test the layer through itself).
- **`FoundationDB.Layers.Tests`**, the home for every LAYER suite (`Layers.Common`, `Layers.Experimental`). Each suite is written ONCE and runs against BOTH the in-memory FakeDb (fast) and a real cluster (validation).

**Dual-backend pattern** (see `FullText/FdbTextIndexFacts.cs`, and `SmokeConformanceFacts` for the original): a layer suite is an `abstract XxxFacts : FdbTest` whose bodies open an isolated partition with `OpenTestPartitionAsync()` and clear it with `CleanLocation(db, location)`, backend-agnostic. Two thin concrete fixtures pick the backend:

```csharp
public abstract class FooFacts : FdbTest { /* the tests, via OpenTestPartitionAsync() / CleanLocation() */ }

[TestFixture] // FAST default: in-memory FakeDb, no Docker, no native client
public sealed class FooFakeDbFacts : FooFacts
{
    private FakeDbTestBackend Backend { get; } = new();
    protected override bool UseRealServer => false;
    [TearDown] public void ResetBackend() => this.Backend.Reset();
    protected override Task<IFdbDatabase> OpenTestDatabaseAsync(bool readOnly = false) => this.Backend.OpenAsync(FdbPath.Root, readOnly);
    protected override Task<IFdbDatabase> OpenTestPartitionAsync(string? testMethod = null) => this.Backend.OpenAsync(GetTestPartitionPath(testMethod));
}

[TestFixture, Explicit("Requires a local Docker daemon"), Category("RealCluster")]
public sealed class FooRealClusterFacts : FooFacts { } // inherits the real-cluster FdbTest behavior
```

**Workflow**: iterate against the `*FakeDbFacts` fixtures (Docker-free, instant); then run the `*RealClusterFacts` fixtures (opt-in via the `RealCluster` category or the Unit Test Sessions UI) to validate against a real cluster. Real-cluster runs require Docker and the native `fdb_c` client (one-time per worktree: `FoundationDB.Client.Native/DownloadBinaries.ps1` on Windows, or `DownloadBinaries.sh` on macOS/Linux, which needs `curl` + `python3`; pass `--rid osx-arm64` to fetch only the current platform's binaries). The same suites double as a ready-made realistic test bed for a future FdbLite backend (a third `*FdbLiteFacts` subclass).

### The netstandard2.0 / net472 "lite" targets

`SnowBank.Core`, `FoundationDB.Client`, `FoundationDB.Client.Native`, `FoundationDB.Layers.Common`, and `FoundationDB.FakeDb` **also build for `netstandard2.0`** (gated by `CoreSdkNetStandardEnabled`, default `true`). This is a deliberate stopgap for applications migrating from .NET Framework 4.7.2 to modern .NET: their legacy `main` branch can already consume the modern APIs (`Contract.NotNull`, CrystalJson, `Slice`, `subspace.Key(...)`), which reduces merge pain with their modernized `vnext` branch. The lite build must **run** on the net472 CLR (including the native `fdb_c` interop), not just compile, and it must produce **byte-identical keys/values**: what is written to the database must never depend on the TFM.

**Any change to these projects must keep the `netstandard2.0` target building** (`dotnet build <proj>.csproj -f netstandard2.0`). When code needs something the old BCL lacks:

1. **Missing BCL API with many call sites** → prefer a polyfill in [`SnowBank.Core/Polyfills/`](SnowBank.Core/Polyfills/) so call sites compile unchanged. Public extension polyfills live in the `SnowBank.Compat` namespace (imported via a guarded `global using` in each project's `GlobalUsings.cs`; this includes C# 14 static extension members like `string.Create(provider, $"...")` and `Span<char>.TryWrite($"...")`). Statics that can't be extensions are internal `*Compat` classes redirected per-file: `using CollectionsMarshal = SnowBank.Compat.CollectionsMarshalCompat;` under `#if NETSTANDARD2_0`.
2. **Point fallback** → `#if`/`#else`/`#endif` with the original implementation FIRST under `#if NETX_0_OR_GREATER` (X = the version that introduced the feature/API), and the fallback in `#else` with a comment on the next line stating what is missing and the cost ("X is not available, doing Y instead (which allocates)").
3. **Needs runtime support, cannot be polyfilled** (default interface members, `static abstract` interface members, covariant return types, ref fields, `allows ref struct`) → guard the member out, or exclude the whole feature from the lite build (e.g. `FdbValue<TValue, TEncoder>` and `FdbValueCodec.FixedSize`, the FQL `Query/` engine, the JSON source-gen proxies).
4. **Portable `Span<T>` gotchas**: on netstandard2.0, `Span<T>` cannot wrap raw pointers/refs of types containing references *or pointers* (e.g. `Span<FdbKeyValue>`), and `MemoryMarshal.CreateSpan` shims only support unmanaged types, use direct pointer indexing or a temporary array instead.

**Validation**: `net472` test targets exist on `SnowBank.Core.Tests`, `FoundationDB.Tests`, `FoundationDB.FakeDb.Tests`, and `FoundationDB.Layers.Tests` (they consume the netstandard2.0 builds and run on the real .NET Framework CLR). Test files covering excluded features are guarded with `#if !NETFRAMEWORK` (or `<Compile Remove>` in the csproj for whole folders). Run them with `dotnet test <Project>.csproj -f net472 --no-build` (all TFMs use MTP uniformly now), on Windows only: the `net472` targets build on Linux and macOS, but `dotnet test` stops there with `NETSDK1243` (.NET Framework runs only on Windows). On net472 the fdb test container is driven through the docker CLI (Docker.DotNet cannot run on the netfx CLR). On every TFM, a freshly created test container self-provisions its database on first start (`Fdb.Provisioning.EnsureDatabaseConfiguredAsync`, called from `FdbServerTestContainer.StartContainer`); no manual `fdbcli` step is needed.

Known accepted netfx differences (do not "fix" by changing shared code): doubles may format with 17 digits instead of shortest-roundtrip (`FastDtoa` is excluded), and `IVarTuple` ↔ `ValueTuple`/`ITuple` interop is unavailable (`System.Runtime.CompilerServices.ITuple` is not visible to netstandard2.0).

## Releasing

The published NuGet packages come from release branches, never from `master`:

- **`master` is the development branch.** It holds the next version with the `dev` suffix in [`Common/VersionInfo.props`](Common/VersionInfo.props) (for example `8.0.0` + `dev`), may carry unfinished work, and is never packed for nuget.org.
- **One `release/X.Y` branch per released minor line** (`release/7.4`, `release/8.0`). It holds the latest released X.Y.z and its `VersionInfo.props` has no suffix.
- **A change lands on `master` first**, then moves down with `git cherry-pick -x <sha>`. A fix that only concerns an older line goes directly on that release branch. Never merge `master` into a release branch, and never merge a release branch back.
- **A new minor line** starts when its first version ships: cut `release/X.Y` from `master`, then open the next version on `master` (`Build: open <next> development`).
- **A point release** happens on its branch: cherry-pick the fixes, bump `VersionInfo.props` to X.Y.z, write the release notes, pack standalone, tag `X.Y.z` on the release branch, publish.

The reference **sample and sandbox projects are not part of the release**: they pin *published* `FoundationDB.*` package versions (so a reader can `restore` them like their own app), which means they legitimately lag the in-development version. Fold this sweep into the pre-tag routine, on the release branch:

- **Before the tag**, write [`Documentation/releases/<N>.md`](Documentation/releases/) from the commit log of the release branch (which follows the `Area: summary` house form: `git log <prev-tag>..HEAD --format='- %s'`, grouped by area prefix, then curated into prose, never the raw log). It opens with the release date (`Released on YYYY-MM-DD.`, the day the packages went live on nuget.org; fill it at publish time). It is the single release-notes and upgrade document: every change from the previous STABLE version (not the previous rc), ordered by what applications use, each behavior change inlining what to do about it, and a final Breaking changes section for the rest. There is no separate migration guide. Follow [`Documentation/writing-style.md`](Documentation/writing-style.md), and add a `releases/<N>.md` entry to [`Documentation/toc.yml`](Documentation/toc.yml). The GitHub release body stays slim: highlights and the breaking-change list, pointing to the published page `https://snowbanksdk.github.io/foundationdb-dotnet-client/releases/<N>.html` for the full detail (never the in-repo `.md`: GitHub renders it degraded, and a NuGet-packed README cannot resolve it at all).
- **Before packing**, bump the pinned `FoundationDB.*` versions in the standalone samples ([`samples/getting-started/`](samples/getting-started/)) and any sandbox projects to the version being released. The same sweep bumps the documentation's own version pins: `version` in [`Documentation/docgen.json`](Documentation/docgen.json), and the package snippets in [`Documentation/fdb/aspire/getting-started.md`](Documentation/fdb/aspire/getting-started.md) plus its `.fr.md` twin (7.4.5 shipped with all three still saying 7.4.2/7.4.3). This puts the correct pins in the commit that gets packed and tagged, so the tag (which targets the packed commit, for SourceLink) also carries samples that point at the release. The bump is a pin, not a verification: the samples do not restore until the packages are live, which is expected. **After** upload, when the packages are live, `restore` + build the samples to confirm they work against the shipping packages. Do NOT defer the pin bump to after upload: the pin then lands in a commit after the packed one, and the tag ships stale sample pins (7.4.4 did this, tag `292796a7` pins the samples to 7.4.3).
- **Push FdbShell with every release**, its pointer package `FdbShell` and the per-platform packages `FdbShell.<rid>`, even when FdbShell did not change: `WithFdbShell()` in `FoundationDB.Aspire.Hosting` runs `dotnet tool exec FdbShell@<its own version>`, so a hosting package without the FdbShell of the same version has a dashboard command that fails.
- **After the push**, publish the documentation from the **newest release branch** (`release/7.4` until 8.0.0 ships, then `release/8.0`): dispatch the [`docs.yml`](.github/workflows/docs.yml) workflow on that branch, and wait for it before publishing the GitHub release, whose body links to `releases/<N>.html`. The workflow builds the API reference from the code of the branch it runs on and republishes the whole site, so a dispatch from `master` would publish unreleased APIs and draft pages. Release notes written on a release branch are cherry-picked to `master`, and also to the newest release branch when they belong to an older line (7.4.8 notes written after 8.0.0 shipped go to `release/7.4`, `release/8.0` and `master`); a page missing from the newest release branch is missing from the site.

## Releasing FoundationDB.Client.Native (one package per FoundationDB branch)

`FoundationDB.Client.Native` ships the native client libraries, and a native client only talks to a
cluster of its own major.minor, so the package exists once per FoundationDB branch: today 7.3.x and 7.4.x
(the 7.4.7 package and the 7.3.78 package). Its version follows the native version, not the binding's.
The csproj takes that version as the MSBuild property `FdbNativeVersion`, which names the
[`FoundationDB.Client.Native/manifest.json`](FoundationDB.Client.Native/manifest.json) entry to ship
and becomes `VersionPrefix`; its default is the manifest's `latest`. The pack refuses to run when
`runtimes\` holds the files of another version: `DownloadBinaries.ps1` (and `.sh`) write the version
they fetched into `runtimes\fdb-native-version.txt`, and the `CheckFdbNativeVersionStamp` target
compares that stamp with `FdbNativeVersion`. The steps, and who does what:

1. **Owner: the binaries exist.** The Windows and macOS files of the version are published on the
   `SnowBankSDK/foundationdb-windows-build` release of that tag (built and checked by that repository's
   kit), the Linux files are the upstream release assets.
2. **Session: the manifest entry.** `manifest.json` gains the `<version>` entry: 10 files (`win-x64`,
   `osx-arm64` and `osx-x64` pairs from our release, `linux-x64` and `linux-arm64` pairs from upstream),
   checksums from the `.sha256` assets. `latest` moves only when the newest branch's newest version is
   meant to be the default package. One commit, `Fdb: add the <version> manifest entry`.
3. **Session: fetch the branch's files.** In `FoundationDB.Client.Native`:
   `.\DownloadBinaries.ps1 -version <version>` (Linux and macOS: `./DownloadBinaries.sh --version <version>`).
   It verifies every checksum and stamps `runtimes\fdb-native-version.txt`. Files of another version
   already present are replaced (the checksum check re-downloads them).
4. **Session: pack the branch.** From the repository root:
   `dotnet pack FoundationDB.Client.Native/FoundationDB.Client.Native.csproj -c Release -p:CORESDK_STANDALONE_BUILD=true -p:FdbNativeVersion=<version> -o artifacts/packages/native-<version>`
   (standalone, so the package carries the complete target set; the property may be omitted when
   `<version>` is the manifest's `latest`). A stamp mismatch stops the pack with the message naming both
   versions. Inspect the `.nupkg`: version `<version>`, `runtimes/<rid>/native/` holding one file per
   supported rid.
5. **Session: prove the package.** Restore it into a throwaway console project (a `nuget.config`
   pointing at the output folder) built for `win-x64` on net10.0, and check that the `fdb_c.dll` in the
   output hashes to the manifest entry's checksum.
6. **Repeat 3 to 5 for the other branch** in the same tree: the download replaces the files and the stamp,
   the pack takes the other `-p:FdbNativeVersion`. Nothing in the csproj changes between branches.
7. **Owner: publish.** Push each `.nupkg` to nuget.org; the two branches are two versions of one package id,
   and applications pin the branch their cluster runs (`7.3.*` or `7.4.*`).


## Coding conventions

Style is enforced by [`.editorconfig`](.editorconfig) and the `.DotSettings` files. Match the surrounding code; notable points:

- **Tabs, not spaces.** (This is deliberate and non-negotiable per the README.)
- **Block-scoped namespaces** (`namespace Foo { … }`), not file-scoped. Every file opens with the BSD-3-Clause copyright header `#region`.
- `Nullable` is **enabled**; `ImplicitUsings` is enabled (shared usings live in each project's `GlobalUsings.cs`).
- Public API is documented with XML doc comments (`///`), keep them accurate and add `<remarks>`/`<example>` for non-obvious behavior, as the existing code does heavily.
- **House writing style** (design docs, notes, documentation, code comments, commit messages). Principles: one idea per sentence, prefer short; active voice with a named actor; concrete words, numbers over adjectives; the same word for the same thing (no synonym rotation); every "this"/"it"/"that" has a clear referent; noun stacks capped at three, broken with prepositions; positive form, no double negatives; no trailing "-ing" pile-ups. Banned everywhere: em-dashes, shouted CAPS mid-sentence, emoji, and the `no-slop` word list (delve, leverage, robust, seamless, ...). In comments only, a beacon at the start is a review marker kept verbatim: `REVIEW`, `BUGBUG`, `HACKHACK`, `PERF`, `OPTIMIZE`, `NOTE`. Comments explain the code; they do not point at design, spike, or campaign documents (state the fact or rationale inline). This is a blacklist plus principles: no approved-word dictionary, no length audit, no sign-off process. When in doubt, write the plainest true sentence.
- Allocation-consciousness is a core value: prefer `Slice`/`ReadOnlySpan<byte>`, pooled buffers, and `struct` keys/values over `byte[]`. Many hot types are `readonly struct` implementing span-based interfaces (`ISpanEncodable`).
- **Guard clauses and assertions use the `Contract.*` family** (`SnowBank.Diagnostics.Contracts`, a global using in these projects), **not** raw `throw new ArgumentNullException(...)`, `ArgumentNullException.ThrowIfNull(...)`, `ArgumentOutOfRangeException.ThrowIf*`, or `System.Diagnostics.Debug.Assert(...)`. Always-on preconditions: `Contract.NotNull(x)`, `Contract.NotNullOrEmpty(...)`, `Contract.Positive(n)`, `Contract.Requires(cond)`. Debug-only invariants: `Contract.Debug.Requires(...)` / `Contract.Debug.Assert(...)` (and `Paranoid.*` for the hottest paths). They carry `[CallerArgumentExpression]` param names and integrate with the test harness's contract-failure interceptor.
- Don't break public surface casually. Retire APIs with `[Obsolete]` (the codebase uses `error: true` for hard-removed ones) rather than deleting outright.

## Working on the key/value API or layers

The single most important thing to get right (and the most common source of incorrect "vibe-coded" usage) is **how keys are encoded and how a custom Layer is structured**. The rules:

- Keys are built with `subspace.Key(item1, …)` and friends, returning **lazy strongly-typed key structs** (`FdbTupleKey<…>`). They are rendered to bytes only when handed to the transaction (`tr.GetAsync(key)`, `tr.Set(key, …)`). Do **not** eagerly call `.ToSlice()` and pass bytes around.
- A Layer is a thin wrapper over an `ISubspaceLocation`; it implements `IFdbLayer<TState>`, and `Resolve(tr)` returns a per-transaction `State` holding the resolved `IKeySubspace`. The state must **never** escape the transaction.

Full guidance, worked examples, and the list of reference layers to imitate are in the skill **[`.claude/skills/foundationdb-keys-and-layers/SKILL.md`](.claude/skills/foundationdb-keys-and-layers/SKILL.md)**. Transaction/retry semantics are in **[`.claude/skills/foundationdb-transactions/SKILL.md`](.claude/skills/foundationdb-transactions/SKILL.md)**. For sophisticated/distributed layers (cluster internals, read-batching/latency, change feeds, version-as-clock leases, retention and fencing), see **[`.claude/skills/foundationdb-advanced-layers/SKILL.md`](.claude/skills/foundationdb-advanced-layers/SKILL.md)**. For the `Slice` type and its companions (`SliceReader`/`SliceWriter`/`SliceOwner`, Span-first I/O, pooled buffers) that underlie all keys/values, see **[`.claude/skills/snowbank-slices-and-buffers/SKILL.md`](.claude/skills/snowbank-slices-and-buffers/SKILL.md)**. For standing up a cluster and connecting to it (the .NET Aspire hosting & client integrations, the native `libfdb_c` client, client⇄cluster version compatibility), i.e. getting the `IFdbDatabaseProvider` the other skills assume, see **[`.claude/skills/foundationdb-aspire/SKILL.md`](.claude/skills/foundationdb-aspire/SKILL.md)**. Read these before writing or reviewing key-encoding or layer code, even when working inside this repo.

These skills are also packaged as a Claude Code **plugin** ([`plugins/foundationdb-skills/`](plugins/foundationdb-skills/)) so consumers can install them (`/plugin marketplace add SnowBankSDK/foundationdb-dotnet-client`). The canonical skills live in `.claude/skills/`; the plugin's `skills/` is a **committed copy** kept in sync by the sync script ([`scripts/sync-plugin-skills.sh`](scripts/sync-plugin-skills.sh), or [`scripts/sync-plugin-skills.ps1`](scripts/sync-plugin-skills.ps1) on Windows), **after editing any skill, run that script** (`--check`/`-Check` verifies they haven't drifted) and bump the `version` in `plugins/foundationdb-skills/.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`.

## Docs

Human-facing docs live in [`Documentation/`](Documentation/) as Markdown (listed in [`Documentation/toc.yml`](Documentation/toc.yml)), rendered to a static site by [`SnowBank.DocGen`](SnowBank.DocGen/), a standalone .NET tool in this repo. It is **not** part of `FoundationDB.Client.slnx`; build and run it on its own:

```bash
dotnet run --project SnowBank.DocGen        # renders to artifacts/_site
```

The tool builds the guide pages, an API reference reflected from the built assemblies (`SnowBank.Core`, `FoundationDB.Client`, `FoundationDB.Aspire`, `FoundationDB.Aspire.Hosting`, so build those in Release first for the reference to appear), and a French twin under `/fr/` from the `*.fr.md` files. It shells out to Node for two build-time passes: mermaid diagrams to static SVG (needs a Chrome, pointed to by `CHROME_PATH`) and Shiki syntax highlighting; run `npm ci` in [`SnowBank.DocGen/tools`](SnowBank.DocGen/tools) once. The [`docs.yml`](.github/workflows/docs.yml) workflow runs all of this and publishes to GitHub Pages (manual dispatch).

The top-level [`README.md`](README.md) is the authoritative getting-started narrative, and it is packed into the NuGet package, so treat it as shipped surface.
