Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Compendium is a shared, real-time knowledge workspace: one SvelteKit app where a
- [`persistence.md`](docs/specifications/persistence.md) — SQLite via Drizzle (snapshots, audit, tokens, read model)
- [`service-layer.md`](docs/specifications/service-layer.md) / [`service-layer-manifest.md`](docs/specifications/service-layer-manifest.md) — where permission+audit logic must live
- [`e2e-testing.md`](docs/specifications/e2e-testing.md) — why/how the Tier A + Tier B suites exist
- [`crdt-capacity-baseline-2026-08-30.md`](docs/benchmarks/crdt-capacity-baseline-2026-08-30.md) — current measured global-workspace envelope and sharding decision gates
- [`design-system.md`](docs/specifications/design-system.md) — UI tokens/conventions

## Commands
Expand All @@ -36,6 +37,9 @@ npm run test:e2e:tier-a # vitest, protocol-level MCP+Yjs parity tests (tes
npm run test:e2e:tier-b # playwright, DOM-level (requires `npm run build` first — serves via build/handler.js)
npm run test:e2e # both tiers

npm run benchmark:workspace # bounded CRDT capacity profile; isolated temp DB + localhost server
npm run benchmark:workspace:large # manual CRDT capacity profile; run before/after shard or persistence changes

npm run test:coverage # coverage; thresholds are 80% stmts/branches/functions/lines (vite.config.ts)
npm run check # svelte-kit sync && svelte-check (typecheck)
npm run lint # prettier --check . && eslint .
Expand Down Expand Up @@ -108,3 +112,22 @@ Unit tests calling `records.ts`/`services/*.ts` directly, and manual/Playwright-
- **Tier B** (`tests/e2e/tier-b.spec.ts`, Playwright): real browser, but the triggering action still comes from a real MCP client call in the test's Node context. Reserved for behavior that specifically needs a rendered DOM (held-block shimmer, live sidebar tree updates) — keep this tier small.
- Shared harness: `tests/e2e/harness.ts` (the only place that should know how to boot a full server instance for tests).
- Vitest is split into three projects (`vite.config.ts`): `server` (node env, most of `src/**` + `tests/**`), `client` (jsdom, `src/lib/client/**`), `component` (jsdom + `browser` resolve condition, `src/**/*.svelte.test.ts` — needed because Vitest's default SSR condition resolves `svelte` to a build without `mount()`).

## Capacity benchmark: required QA for CRDT and sharding work

Read [`docs/specifications/e2e-testing.md`](docs/specifications/e2e-testing.md) §6 and the current
[`docs/benchmarks/crdt-capacity-baseline-2026-08-30.md`](docs/benchmarks/crdt-capacity-baseline-2026-08-30.md)
before changing the Yjs schema, WebSocket routing or fan-out, workspace/shard resolution, snapshot persistence,
or the document/collection ownership model.

- Run `npm run benchmark:workspace` for every PR that changes one of those boundaries or any other Yjs sync
behavior. It is deliberately separate from `npm run test` and coverage so normal checks stay bounded.
- Run `npm run benchmark:workspace:large` manually before and after shard-aware routing, catalog/SSE integration,
compaction, snapshot-format, persistence, or sync-protocol redesign. Compare like-for-like runs; performance
Comment thread
coderabbitai[bot] marked this conversation as resolved.
values are trend evidence, not universal SLOs. [`e2e-testing.md`](docs/specifications/e2e-testing.md) §6 is
canonical if this short list needs interpretation.
- Keep benchmark clients on WebSocket transport (`disableBc: true`) when reporting sync bytes. Do not re-enable
BroadcastChannel sharing to make a profile look smaller.
- If a guardrail fails or the result crosses a documented sharding trigger, stop treating it as a test-only
regression: record the result in a dated benchmark note and link the implementation issue/PR. Update the
baseline only after explaining the fixture or architecture change that makes the comparison valid.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ for the shared record primitive.
```sh
npm run test # unit and component tests
npm run test:e2e # real MCP↔Yjs and browser-level flows
npm run benchmark:workspace # bounded CRDT capacity profile
npm run benchmark:workspace:large # manual sharding/persistence profile
npm run check # Svelte and TypeScript checks
npm run lint # formatting and lint rules
npm run build # production build
Expand All @@ -193,6 +195,14 @@ The E2E suites intentionally cross real transport boundaries. A feature is not
considered integrated merely because its UI and service functions pass in
isolation.

The capacity benchmark is intentionally separate from routine tests so it can
measure a real temporary SQLite + WebSocket + MCP workspace without making
ordinary checks environment-sensitive. Run the bounded profile for CRDT,
sync, snapshot, or routing changes; run both profiles before and after
shard-aware routing, catalog/SSE integration, compaction, snapshot-format,
persistence, or sync-protocol redesign. See the [testing strategy](docs/specifications/e2e-testing.md#6-capacity-benchmark--crdt-and-sharding-regression-gate)
for the canonical selection rules and the [current baseline](docs/benchmarks/crdt-capacity-baseline-2026-08-30.md).

## Roadmap

The [Compendium project board](https://github.com/users/brylie/projects/6) is
Expand Down
86 changes: 86 additions & 0 deletions docs/benchmarks/crdt-capacity-baseline-2026-08-30.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CRDT capacity baseline — 2026-08-30

Issue [#31](https://github.com/brylie/compendium/issues/31) establishes a
measured baseline before the workspace catalog and shard design in [#112](https://github.com/brylie/compendium/issues/112)
is approved. This is deliberately a real transport benchmark: it uses the
application's temporary SQLite database, Yjs WebSocket endpoint, and MCP HTTP
server rather than measuring isolated Yjs objects only.

## Method

The benchmark seeds deterministic documents, blocks, collections, and rows;
then connects Yjs clients, performs a human-originated WebSocket mutation, and
runs sequential MCP `hold_records` and `write_record` calls. BroadcastChannel
sharing is disabled for benchmark clients, so initial-sync bytes are observed
only at the WebSocket boundary. It captures:

- encoded global Yjs state and persisted snapshot sizes;
- initial client-sync bytes and elapsed time;
- receiver-side WebSocket bytes for one fan-out mutation;
- MCP write p50/p95 elapsed time;
- process heap delta, CPU time, and event-loop-delay p99;
- snapshot-backed context restart time and restored state size.

The bounded `daily` profile is suitable for CI. The `large` profile is a
manual regression check. Run them with:

```sh
npm run benchmark:workspace
npm run benchmark:workspace:large
```

The harness uses a fresh temporary database and random localhost port, so it
cannot read or modify a running daily workspace database.

## Results

Measurements were taken on 2026-08-30 in the local Node test environment.
They are a baseline and trend signal, not universal production SLOs.

| Metric | Daily: 12 docs, 192 blocks, 3 collections / 120 rows, 3 clients, 12 MCP writes | Large: 120 docs, 2,880 blocks, 8 collections / 3,200 rows, 8 clients, 80 MCP writes |
| ----------------------------------- | -----------------------------------------------------------------------------: | ----------------------------------------------------------------------------------: |
| Encoded global state | 142,694 B | 2,848,008 B |
| Aggregate initial-sync bytes | 570,858 B | 28,480,307 B |
| All-clients initial-sync elapsed | 87.9 ms | 1,421.8 ms |
| Receiver fan-out bytes (one edit) | 114 B | 406 B |
| Fan-out convergence elapsed | 26.9 ms | 26.2 ms |
| MCP write p50 / p95 | 6.0 / 8.6 ms | 5.5 / 9.2 ms |
| Persisted snapshot | 143,421 B | 2,852,778 B |
| Snapshot-backed restart | 9.5 ms | 85.7 ms |
| Process heap delta | 50.8 MB | 518.9 MB |
| Event-loop p99 | 43.55 ms | 417.07 ms |
| One-document shard state projection | 6,795 B | 10,147 B |

## Interpretation and decision boundary

The daily profile fits comfortably inside the initial CI guardrails: less than
2 MiB aggregate initial sync, less than 2 seconds initial sync and restart,
and less than 1.5 seconds MCP write p95. It is therefore safe to start using
Compendium for a small daily Tech with Brylie workspace while the workspace
work proceeds.

The large profile exposes the existing global-document cost: every new client
receives the whole 2.85 MB workspace state, and the one-process benchmark
showed a 518.9 MB heap increase with a 417.07 ms event-loop p99. A same-shape
single-document state is roughly 10 KB, which makes document-level Yjs shards
the appropriate next boundary. The catalog/SSE design in #112 keeps titles and
navigation outside that document state so unrelated document edits need not
grow client synchronization or CRDT fan-out.

This projection is intentionally not presented as a shipped shard-aware
transport result: current Phase 0 routing still resolves every connection to
the global shard. #113 must rerun these profiles with real document and
collection routes before comparing end-to-end shard transport bytes.

## Gates for the next implementation phase

- Keep the `daily` profile in CI with its current conservative limits.
- Treat a global snapshot at or above 2 MiB, event-loop p99 at or above 100 ms,
or a daily CI guardrail failure as a trigger to prioritize sharding or
compaction work rather than increasing the global-state envelope.
- #113 must report per-shard encoded state, sync bytes, apply latency, and
receiver fan-out with two active document shards plus a collection shard.
- Browser heap remains a follow-up measurement: this benchmark's heap value is
the Node host running simulated Yjs clients, not a browser DevTools heap
snapshot. Add browser-memory collection when #24 establishes the user-facing
sync-latency SLO.
2 changes: 1 addition & 1 deletion docs/specifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Canonical specification for each implemented subsystem, translating [`prd.md`](.
- [`persistence.md`](./persistence.md) — SQLite via Drizzle: snapshots, audit log, access tokens, and the query read model.
- [`audit-coverage.md`](./audit-coverage.md) — how direct UI mutations (which bypass the service layer entirely) and denied MCP attempts get an audit trail, and what's deliberately excluded.
- [`service-layer.md`](./service-layer.md) / [`service-layer-manifest.md`](./service-layer-manifest.md) — how permission/audit logic is centralized once and shared by MCP and UI.
- [`e2e-testing.md`](./e2e-testing.md) — the MCP/UI parity testing strategy.
- [`e2e-testing.md`](./e2e-testing.md) — MCP/UI parity and CRDT capacity benchmark strategy.
- [`design-system.md`](./design-system.md) — UI tokens and conventions.

## Out of scope for the current architecture
Expand Down
62 changes: 62 additions & 0 deletions docs/specifications/e2e-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,65 @@ This harness is the only thing that should know how to boot a full server instan
## 5. Relationship to existing and future unit tests

This spec doesn't replace `records.ts`'s existing unit tests, or the equivalent tests the service layer (`service-layer.md`) will need — those stay valuable for fast, fine-grained coverage of CRDT and business-rule logic. Tier A is specifically for the narrower, higher-value class of bug that only exists at the transport boundary between two independent real clients — write a Tier A test whenever a change touches anything permission-, grant-, hold-, or attribution-related, since those are exactly the categories where "worked in the unit test, broke for a real second agent call" has already happened once.

## 6. Capacity benchmark — CRDT and sharding regression gate

Tier A proves that a small number of real clients converge correctly. It does
not establish that the shared state stays within a workable resource envelope
as documents, Collections, concurrent clients, and MCP activity grow. The
capacity benchmark fills that gap while preserving the same real server, Yjs
WebSocket, MCP HTTP, and temporary SQLite boundaries.

### Profiles and commands

```sh
npm run benchmark:workspace # `daily`: bounded profile, suitable for CI
npm run benchmark:workspace:large # `large`: manual pre/post-change comparison
```

The benchmark lives in `tests/benchmark/workspace-capacity.test.ts` and runs
in its own Vitest project. It is intentionally excluded from `npm run test`
and coverage: performance work must stay discoverable and repeatable without
making ordinary correctness checks slow or environment-sensitive. Every run
creates a temporary SQLite database and random local port; it must never point
at a developer's running workspace database.

`daily` uses a small knowledgebase fixture and carries conservative CI
guardrails. `large` is deliberately not a CI gate: use it before and after a
change where state topology or transport cost could change, then publish the
two results with the environment and fixture in a dated note under
`docs/benchmarks/`.

### When an engineer or agent must run it

Run the bounded profile for a PR that changes any of the following:

- the Yjs record schema, encoded representation, or document/Collection
ownership model;
- `attach-ws.ts`, WebSocket connection lifecycle, routing, or update fan-out;
- `workspace-store.ts`, shard selection, context lifecycle, or snapshot load/
flush behavior;
- MCP write paths that change edit churn, cross-client update application, or
persistence behavior.

Run both profiles for shard-aware routing, catalog/SSE integration, compaction,
snapshot-format, or persistence redesign. For the workspace-catalog work,
this is a required before-and-after acceptance check, not an optional
optimization exercise.

### How to interpret and maintain results

The suite records encoded state/snapshot size, client-visible initial-sync and
fan-out bytes, convergence and MCP-write timing, restart time, and Node host
resource signals. Initial-sync measurements must have `disableBc: true` for
every benchmark provider, including the seed client: otherwise same-process
BroadcastChannel sharing bypasses WebSocket traffic and makes the transport
envelope appear smaller than it is.

Use the current [CRDT capacity baseline](../benchmarks/crdt-capacity-baseline-2026-08-30.md)
as the decision record. Compare like-for-like results rather than treating
machine-specific timings as universal SLOs. A daily-profile guardrail failure,
a global snapshot of 2 MiB or more, or event-loop p99 of 100 ms or more is a
sharding/compaction escalation: document it and link the related issue or PR.
When an intentional fixture or architecture change makes a new baseline valid,
write a new dated note; do not silently overwrite an old decision record.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"test:e2e:tier-a": "vitest --run tests/e2e/tier-a.test.ts",
"test:e2e:tier-b": "playwright test",
"test:e2e": "npm run test:e2e:tier-a && npm run test:e2e:tier-b",
"benchmark:workspace": "vitest run --project benchmark",
"benchmark:workspace:large": "COMPENDIUM_BENCHMARK_PROFILE=large vitest run --project benchmark",
"test": "npm run test:unit -- --run",
"test:coverage": "npm run test:unit -- --run --coverage",
"lint": "prettier --check . && eslint .",
Expand Down
Loading
Loading