* test(b3-6): fuzz seeds — epoch-1 corpora for every target, protocol + predicate-parity fuzz targets Workstream 3. Every Fuzz* target `make fuzz` loops over now has a committed corpus, so a plain `go test ./...` replays the real wire and not only the hand-written f.Add shapes. 17 -> 20 targets, 3 -> 20 with a corpus, 98 corpus files added. Two new targets: - ws/protocol_fuzz_test.go — FuzzHandleMessageDecode drives the inbound envelope decoder (handlers.go) through a headless NewHubForTest + NewTestClient; FuzzCommandPayloads drives all 24 payload decoders in commandConstructors, which are pure funcs of (userID, reqID, raw) and so need no hub at all. Between them they pin: a rejected frame yields no log fields and one invalid-count tick, an accepted frame yields the 64-byte capped fields and re-encodes to an equal envelope, a rejected payload never returns a command alongside its error, and a decoded command always carries the authenticated sender rather than a user id lifted from the payload. - permissions/predicates_fuzz_test.go — FuzzPredicateParity continues the B2-5 parity tables by machine: each predicate against the two-layer override formula written out longhand, sentinel included (so "an unauthorized caller never learns a channel is archived" is pinned), plus CanAdmitSession == CanViewChannel and CanType == CanSendMessage. Corpus entries are generated from protocol/fixtures/epoch-1 — every distinct c2s frame of the 11 journeys for the two ws targets, and the role permission values, channel types, message bodies, usernames, avatar URL and channel ids those journeys carry for the rest. Replay costs <= 0.02s per target. Test-only: no production file changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * docs(b3-6): evidence block for item 5 (fuzz seeds) Seed counts per target, the two RED negative controls with their failing excerpts, the replay wall clock, and — as the shared rules require — what was found stale at HEAD for each of the item's four pointers and what was done instead: the inbound decoders live in handlers.go/command.go not messages.go, permissions.Subject has no wire form so parity replaces "round-trips", there is no pure upload-admission function to fuzz, and there is no recovery-token parser at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * test(b3-6): FuzzParseMentionTokens compares with db.LowerASCII, the OC-0131 rule — make fuzz green again The target still asserted the Unicode fold (strings.ToLower) that OC-0131 removed from parseMentionTokens: usernames.username is COLLATE NOCASE, which folds ASCII A-Z only, so the parser folds with db.LowerASCII to stay in step with GetUserIDsByUsernames' equally ASCII-folded map key. Any mention of a name starting with an uppercase non-ASCII letter (@Ǥ0, @Ł) therefore failed the assertion, and `make fuzz` found one within four seconds. The assertion now uses the same fold the code under test does. Nothing else in the file changes, and no production behaviour is involved — the fold was already correct; only the check disagreed with it. 30s of fuzzing on a cleared cache: PASS at 1,159,227 execs (it failed at 66,255 before). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * test(b3-6): fuzz seeds — every command constructor seeded, the auth payload decoder gets its own target, evidence corrected commandConstructors registers 26 decoders, not the 24 the evidence block claimed (the count missed the two E2EE keys), and only 16 had any input: ten commands appear in no epoch-1 journey, so presence_update, call_ring, call_decline, voice_token_refresh, voice_mute, voice_deafen, voice_camera, voice_screenshare, voice_mod_deafen and voice_mod_kick were reachable only if the fuzzer guessed the type string. Each now has a corpus entry carrying a minimal valid payload taken from its own decoder struct, with the fixture channel and user ids where they apply. TestCommandPayloadSeedsCoverEveryConstructor is the guardrail that keeps that true: it unions the hand-written seed list with the committed corpus and fails when a registered command has neither, or when a seed names a command nothing registers. Removing one corpus entry fails it by name. auth was decoded by neither target. It is not in the constructor table — authenticateConn reads it before the hub knows the client — so its two corpus entries were inert under FuzzCommandPayloads. They move to FuzzAuthPayload, which pins the property that matters in a handshake a stranger controls: no numeric field takes a value its Go type cannot hold, and the token that will be hashed is the string the JSON carried. The production decode is inline behind a live socket read and a session lookup, so the target mirrors the struct and the comment says why rather than reshaping production to expose it. Corpus entries now credit the journey that owns the frame: the ping frame to ping.json, the auth frame to fresh-connect.json. Test-only: no production file changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * test(b3-6): fuzz seeds — auth target gates on the real epoch constants; corpus reader fails on a malformed entry FuzzAuthPayload was proving encoding/json behaviour against a copy of the handshake struct and nothing more. It now mirrors the two rejections authenticateConn actually makes — the decode error and the empty token as one (serve_auth.go:58), then the epoch window (:62) — using minClientEpoch and ProtocolEpoch themselves, so moving either constant or that gate turns the target red instead of leaving it quietly stale. The load-bearing case is the absent epoch: every client up to v1.2.0-alpha.4 predates the field and relies on the zero value being inside the window, so raising minClientEpoch above 0 now fails here rather than in the field. Setting it to 1 locally fails both fixture-derived corpus entries and two seeds. Deciding "absent" needed care, and fuzzing found that out in three seconds: encoding/json falls back to a case-INSENSITIVE tag match, so "epoCh" populates Epoch while an exact key lookup calls the field missing. The probe now decodes into a *int, which is the same matching the server does, and three seeds pin the rule. corpusFirstString skipped a corpus file with no string(...) argument, which would have let a malformed entry masquerade as a seeded command while the coverage test still passed. It is now a failure naming the file. The struct comment cited serve_auth.go:44; the struct starts at :45. Test-only: no production file changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * test(b3-6): fuzz seeds — token expectation uses struct decoding semantics; parity oracle states the zero-permission ordering (Codex P2s on #1457) FuzzAuthPayload derived the expected token from an exact key lookup, so {"token":"a","TOKEN":"b"} failed the target: encoding/json resolves both keys to the tagged field and the last one wins, leaving the handshake holding "b" while the lookup expected "a". The expectation now comes from a probe struct carrying the same json:"token" tag, so it follows the decoder's field resolution rather than the raw key set — the same correction the epoch probe already needed. A corpus entry pins it; reverting the probe fails on that entry by name. rawHas mirrors Subject.Has, which applies the Administrator bypass before the zero-permission refusal, so an administrator holds an empty mask where HasPerm(_, 0) is false. Parity with production is this target's purpose, so the ordering stays; what changes is that the oracle's contract comment now states it instead of claiming the tidier rule, and TestSubjectHasZeroPermIsAdminBypassed records the divergence as observed behaviour with a message that says to move both together if it is ever changed deliberately. The evidence block gains the call-site survey behind that: every leaf caller of Subject.Has names a permissions.* constant, the variable-forwarding wrappers are all reached with named constants, and the one table-driven site has two rows. No production code changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Documentation index
Every document in docs/ is listed here. If it is not on this page it is not
current guidance.
Docs fall into four kinds, and the difference matters when you are deciding whether to trust one: guidance tells you how to do something, reference describes a contract the code actually implements, audits are dated snapshots that were true when written and were never updated, and plans record intent. Read an audit as history, not as status.
Start here
| I want to… | Read |
|---|---|
| Run a server | quick-start.md |
| Deploy for real | deployment.md |
| Contribute a change | contributing.md |
| Understand the system | architecture/ |
| Report a bug | Issues |
| Ask, or suggest an idea | Discussions |
| Report a vulnerability | security.md |
| Know who can read what | trust-model.md |
Guidance
| Document | Covers |
|---|---|
| quick-start.md | Getting a server running with the fewest steps. |
| deployment.md | Production deployment on Windows and Linux. |
| contributing.md | Environment setup, the branch and PR model, coding standards, how to run the checks CI runs. |
| security.md | How to report a vulnerability, and how findings are handled in public vs private. |
| trust-model.md | Who can read what: operator-readable text and files, E2EE media, transport, at rest, what beta does not claim. |
| livekit-setup.md | Standing up the LiveKit SFU for voice and video. |
| port-forwarding.md | Making a server reachable from outside the LAN. |
| tailscale.md | Remote access without port forwarding. |
| mcp-introspect.md | Dev-only MCP server for introspecting a running instance. |
Reference
These describe contracts the code implements. If one disagrees with the code, the code is right and the document is a bug.
| Document | Covers |
|---|---|
| api.md | REST API under /api/v1. |
| protocol.md | WebSocket protocol — frames, sequencing, reconnect. |
| schema.md | SQLite schema and migrations. |
| server-configuration.md | Every server configuration option. |
| credential-storage.md | What the desktop client persists, and where. |
| ../protocol/schema.json | Generated-code source of truth, at the repository root because it is owned by neither side. Server/ws/message_types.go and Client/src/lib/protocolTypes.ts are generated from it — never hand-edit either. See ../protocol/README.md. |
Architecture
architecture/README.md indexes the blueprints and carries the maintenance rule: each blueprint names its source-of-truth files, and a PR touching those updates the blueprint in the same change.
- system-overview.md, server.md, client.md
- data-model.md, websocket.md, voice-e2ee.md
- ux/ — target-state UX spec, per-view states and event→reaction maps
- platform-contracts.md — target-state desktop/browser seam: what has to move behind a contract before the client can run in a browser (B7)
- plugins.md — the experimental WASM plugin boundary: off by default, compiled out of releases, no API promise, what may become a plugin after beta and what never moves
client-architecture.md is a redirect stub; the live document is architecture/client.md.
Audits — dated, not maintained
Point-in-time snapshots. They are not updated as the code moves, and they are deliberately left alone when paths change, so links from commit messages keep resolving. Anything here may be stale; the ledger and the plan index carry current status.
| Audit | Scope |
|---|---|
| audit-2026-08-23-repository-layout.md | Repository layout and contributor experience (RL-01…RL-22). |
| audit-2026-08-23-repository-health.md | Full repository health. |
| audit-2026-08-19.md | Repo health. States "0 open findings" — untrue since; see the ledger. |
| audit-test-coverage-2026-08-19.md | Test audit (T-*, a separate register from the OC-* ledger). |
| audit-2026-08-04-docs-and-coverage.md | Documentation accuracy and UI/UX test coverage. |
| audit-2026-08-04.md | Security review. |
| audit-test-coverage-2026-07-25.md | Test-coverage audit. |
| audit-2026-07-19.md | Architecture and spec-conformance review. |
| audit-2026-04-07.md | First comprehensive audit. |
Plans
plans/README.md indexes every plan with a recorded state — active, partially implemented, design-only, or shipped — and is the authority over a plan's own header, which can drift.
Where status actually lives
Do not read a defect count, or a "what works" claim, out of a document on this page. Status has owners:
| Concern | Source of truth |
|---|---|
| Defect status | .superpowers/findings-ledger.json (FINDINGS.md is rendered from it) |
| Security-sensitive defects | Private GitHub Security Advisories |
| Phase order and gates | plans/repo-health-roadmap-2026-08-23.md |
| Current measured baseline | plans/b0-baseline-2026-08-25.md |
| Generated-code contracts | CLAUDE.md, "Generated code — never hand-edit" |
A CI job checks that documents on this page do not contradict the ledger's
counts. Adding a count to a document means adding it to that check's allow-list
in scripts/check-doc-counts.mjs.