Skip to content

Contributor readiness: retire text-pinning tests, cut onboarding, split the Store seam #635

Closed

Description

@Mathih13

Contributor readiness: plan

Status: complete. All seven phases are merged. Final progress checked on 2026-10-11 against 5fac28fc8b0a4dcec5df5c31d5427b2a65139fc4 on main. The original plan below was written against b8113305.

No phases remain. Phase 6 and phase 7 each landed through one Core branch and one merge. Package API version 4 and the migration notes are in docs/package-api.md and CHANGELOG.md.

Problem Statement

LyraCore is about to accept outside contributors. The design is sound: the Module owns rules, the
Gateway owns protocol, and the Store seam sits where docs/architecture.md says it should. What
will drive a contributor away is the plumbing and the enforcement around those seams.

A contributor's first pull request fails for reasons unrelated to its behaviour. About 250 tests
assert that a function body contains a literal string, and three test harnesses pin a production
adapter's source verbatim. A rename turns CI red and the only fix is to re-bless text.

A contributor's first change touches too many files. Adding one spell effect touched 21 files.
Adding one relayed opcode touches about 13 gateway files. The order of a 23-link dispatch chain is
significant and undocumented. A WorldStore adapter must satisfy about 290 methods, so every
family change edits a 4,800-line shared Fake.

A contributor's first hour is a reading list. Four documents each claim to be the start. The
required reading is about 23,000 words. The repo tracks research, triage output, a retired design,
spikes, production operations tooling and the maintainer's private agent runbook as if they were
contributor material.

Solution

Seven moves, in two groups.

Before opening up (phases 1 and 2): remove the text-pinning tests and history narration, and cut
the onboarding path to three reads. Neither changes behaviour.

After opening up, as capacity allows (phases 3 to 7): split the WorldStore interface into
families, collapse the Coordinator's forwarding hops, make dispatch order data, unflatten the Module
crate root and narrow the Package API, and move the rules that leaked into the Gateway back behind
the seam.

When all seven land, a contributor adds an opcode by editing one match arm and one family file,
tests it through a family-sized Fake, and never sees a test that reads source code.

User Stories

  1. As a first-time contributor, I want a single ordered list of what to read, so that I can start a
    fix in under an hour.
  2. As a first-time contributor, I want the required reading to be short, so that I do not give up
    before the first commit.
  3. As a first-time contributor, I want a glossary page of the thirty terms I will meet first, so
    that I can read the code without the full 250-term glossary.
  4. As a first-time contributor, I want the full glossary to have an alphabetical index, so that I
    can find a term I already know.
  5. As a first-time contributor, I want one documented command per test tier, so that I can run
    the same checks CI runs before I push.
  6. As a first-time contributor, I want the durable tier documented outside the CI YAML, so that I
    do not reverse-engineer a workflow file.
  7. As a first-time contributor, I want a recipe for adding an opcode, so that I know every file the
    change touches before I begin.
  8. As a first-time contributor, I want a recipe for adding a spell effect, so that I know the seed,
    taxonomy, binding and schema steps before I begin.
  9. As a first-time contributor, I want the pull request template to list exactly the commands CI
    runs, so that a green local run predicts a green CI run.
  10. As a first-time contributor, I want a test that needs client data to report as skipped, so that
    a green run does not hide an untested path.
  11. As a contributor, I want a rename or a reformat to keep the build green, so that I can refactor
    with confidence.
  12. As a contributor, I want a failing Architecture Test to name the invariant it protects, so that
    I know what to change.
  13. As a contributor, I want style rules enforced by a lint with a one-line message, so that I do
    not learn them from a 2,000-line failing test file.
  14. As a contributor, I want comments to describe current behaviour, so that I can tell what is
    load-bearing from what is history.
  15. As a contributor, I want comments free of bare work-item numbers, so that I am not sent to a
    tracker entry that does not resolve.
  16. As a contributor, I want the test Fake for a family to be small and local, so that I can add a
    method to the family without touching unrelated families.
  17. As a contributor, I want a handler to take the family Store it needs, so that I know the whole
    interface the handler depends on from its signature.
  18. As a contributor, I want a new Store method that no adapter implements to fail to compile, so
    that a Fake never silently answers with a no-op.
  19. As a contributor, I want one hop between a handler and a reducer call, so that I can trace a
    Durable Request without reading three forwarding layers.
  20. As a contributor, I want a Refusal and a transport loss told apart by type, so that I do not
    match on an error string.
  21. As a contributor, I want the zero-guid guard in one place, so that I do not copy it into every
    reducer wrapper.
  22. As a contributor, I want each opcode to have exactly one owning family, so that dispatch order
    is not a convention I have to discover.
  23. As a contributor, I want one handler shape, so that I do not choose between two styles.
  24. As a contributor, I want a family's mod.rs to show what the family exposes, so that I can
    tell its interface from its implementation.
  25. As a contributor, I want the crate root free of glob re-exports, so that a name I introduce
    cannot shadow one from another family.
  26. As a contributor, I want the spell taxonomy defined once, so that I do not keep three copies in
    lockstep by hand.
  27. As a contributor, I want gameplay rules to live only in the Module, so that I fix a rule once.
  28. As a contributor, I want a relay leg declared in one place, so that adding a relayed table does
    not touch four files.
  29. As a contributor, I want the build script to do only code generation, so that a lint failure
    reads as a test failure with a name.
  30. As a Package author, I want the Package API to name a small surface, so that a core refactor
    does not break my Package.
  31. As a Package author, I want an actor verb for each thing my Package needs, so that I never
    reach into a family's internals.
  32. As a Package author, I want one durable-test template, so that I can test my Package the way
    core tests itself.
  33. As a maintainer, I want the real invariants still enforced, so that a shard crossing never
    drops rows and a publish never wipes a Realm.
  34. As a maintainer, I want my agent runbook separate from contributor rules, so that outsiders do
    not read realm-operations guidance.
  35. As a maintainer, I want production operations tooling out of the contributor repo, so that CI
    does not spend minutes on it and contributors do not see it.
  36. As a maintainer, I want retired designs, research and triage output out of the tree, so that a
    link in the index never points at something false.
  37. As a maintainer, I want the test count baseline in the docs to be true, so that a lower count
    is a real signal.
  38. As a maintainer, I want the schema parity check kept, so that hand-spliced bindings cannot
    drift.
  39. As a maintainer, I want debug_repair_after_publish outside the debug feature, so that the
    production-critical reducer is not gated behind a flag named debug.
  40. As a reviewer, I want a pull request that adds an opcode to be small, so that I can read it in
    one sitting.
  41. As a reviewer, I want tests that cross the same seam callers do, so that a passing test means
    the behaviour holds.
  42. As a reviewer, I want the shared InMemoryStore to shrink to the families that need a
    multi-shard topology, so that most tests do not depend on it.
  43. As an operator, I want the publish safety checks unchanged, so that this plan carries no
    production risk.

Implementation Decisions

Vocabulary

Use CONTEXT.md terms throughout. Tests that scan source are Architecture Tests, not tripwires.
Test doubles are Fakes, not mocks. Gateway handlers issue Durable Reads and Durable Requests
across the Store seam. The Module owns gameplay rules.

Sequencing

Seven phases. Each is one branch and one pull request. Phases 1 and 2 land before the repo opens
to contributors. Phases 3, 4 and 5 are the Gateway restructure and land in that order. Phases 6
and 7 change the Package API contract and need a version bump and a note to Package authors.

A phase that turns out to need a behaviour change the user must weigh stops and names it. A phase
that spreads into another phase's files stops and reorders.

Phase 1: retire text-pinning tests and history narration

Delete every test assertion that checks a function body contains a literal string. In the Module
these are the code_of(...).contains(...) assertions. In the Gateway they are the include_str!
scans over handler, store, view and subscription sources, and the decoy-defence tests on the
WorldStore adapter. Delete the three tests that pin a production adapter's source verbatim in the
creature cycle, mail escrow and transfer harnesses.

Keep the Architecture Tests that protect an invariant:

  • every table keyed by a Character-capable guid carries the character_owned marker or a reasoned
    exclusion,
  • no whole-table scan of a spatial table and no gameplay logic reads a shard id,
  • no raw character lookup outside the transfer fence chokepoint,
  • every TTL event table is named in the gc reaper,
  • a write to grid coordinates also writes the packed cell,
  • no destructive publish flag in scripts and no published column name collision,
  • generated bindings match the Module schema.

Delete the issue-reference ban and the dead-code allowance rule from the Architecture Tests. Move
the print-macro ban to a clippy configuration or a one-line CI grep.

Move the shared code_of scanner into the test-support crate so the Module and Gateway stop
carrying two copies.

Where a deleted scan guarded a real wiring fact, cover it with a behaviour test through an existing
Fake or an existing durable test. The provision path in the Gateway binary already has a CLI test
seam; use it.

Sweep comments. Remove review narration, mutation-review stories, "this PR" and "formerly"
history, bare work-item numbers and phase labels. Keep present-tense invariants. Fix the two known
stale lines: the debug module's claim about where the gate lives, and the crate root's claim about
the SpacetimeDB version. Move module-level docs longer than about sixty lines into docs/.

Fix the false claim in the debug module that production publishes leave debug_reducers off.
Move debug_repair_after_publish into an ungated operations module.

Phase 2: cut the onboarding path and empty the attic

CONTRIBUTING.md names exactly three required reads: itself, CODING_STANDARDS.md, and a new
short glossary page. Every other document is "look it up when needed". Remove the competing
"start here" pointers from the README and the architecture index.

Split CONTEXT.md: a short core-terms page of about thirty entries for people, the full glossary
kept as a lookup with an alphabetical index at the top. Move the Package, Stat Kind and Runtime
Script specifications to the documents that own them.

Split AGENTS.md into contributor rules of about twenty-five lines, and a maintainer runbook that
holds realm operations, visual-work procedure and cross-repo CLI guidance.

Move out of the tree, to an archive repository or a wiki: docs/research, docs/triage,
docs/history, the issue-26 research note, the retired region sharding design and its fixture
under content/, the TSTL spikes, and the production operations scripts under deploy/ with their
CI job. Collect the verification write-ups into one folder with one index. Untrack the tickets
directory the gitignore already excludes.

Replace the CLI command reference with a pointer to the CLI repository. Make the README
requirements name the pinned SpacetimeDB version. Resolve the two quickstart lines that disagree
about version acceptance. Remove the "verified against the tree on" headers or replace them with a
check CI runs.

Add two recipes: adding an opcode, and adding a spell effect. Each lists every file family touched
and every convention, in order, including binding regeneration and schema append rules.

Document the test tiers in one place: the unit commands, the durable command for one target, the
prerequisites, and the data environment variables. Add the wasm check and the test-support command
to the pull request template. Replace the stale test-count baseline in the danger-zones document.
Change the terrain test that returns early without client data to an ignored test. Collapse the
two near-duplicate ignore messages into one.

Phase 3: split WorldStore by family

Remove the fifteen family traits from the WorldStore supertrait list. A handler takes the family
Store it needs, as a trait object or generic, and the session holds one routed handle that
implements every family.

Move the remaining WorldStore methods into family traits: session and login, transfer, party,
mail, social, character, templates. Each family trait has its own small Fake next to its tests.
Delete every default method body on a Store trait; a method an adapter does not implement must fail
to compile.

The shared InMemoryStore shrinks to the multi-shard topology the transfer and party families
need, and moves out of the main Gateway test file into a named test-support module. The remaining
tests in that file split by family, following the existing per-family test files.

Replace the fixed world-entry packet count used by the login helper with a drain-until-marker
helper, so the login sequence can change without editing every test that enters the world.

Phase 4: collapse the Coordinator to one hop

Implement each family Store trait directly on the Coordinator with the reducer call inside the
trait method. Delete the inherent reducer wrappers that only forward, and the WorldStore adapter
block that only forwards. Delete the unused account-id parameters. Move the zero-guid guard into
the session actor resolution so it exists once.

Add one function that classifies an error as fatal transport loss or gameplay Refusal by
downcasting to the existing typed reducer-call error. Replace every string match on the transport
message with it.

The Coordinator still bundles a connection, the shard set and a session owner. Splitting those
three is out of scope for this plan.

Phase 5: make dispatch order data

One routing function maps an opcode to its owning family. One family trait with one handle method
returns the outbound messages. Delete the per-family outcome enums, the per-family player structs,
the pass-through threading, and the older handler style. Opcode ownership is a table: one opcode,
one family, and a test asserts no opcode has two owners.

Phase 6: unflatten the Module crate root and narrow the Package API

Replace the glob re-exports at the crate root with explicit re-exports of table accessor traits and
the row types the schema parity test needs. Remove the nested globs inside families. Make every
non-reducer public function crate-private unless a Package API root names it.

Shrink the Package API roots from twenty-three to about five: actor verbs, hooks, the package
modules, tables, and the runtime script binding. Add actor verbs where the official Packages
currently reach into family internals. Bump the Package API version and state the removed roots in
the changelog.

Split the build script: code generation stays; the Package API lint becomes a test in its own
crate or test target.

Phase 7: move leaked rules behind the seam and make relays table-driven

Define the spell effect taxonomy once as a typed enum in the shared crate. Delete the importer's
copy, the test-file copies, the hand-maintained count assertion and the reserved-range constant.
The effect application matches exhaustively.

Move every Gateway rule that a comment says "mirrors the Module" either into the shared crate as a
pure function both tiers call, or into a Module-published derived value: armor and resistance
folds, quest completion, accept and reward resolution, trainer and talent gates, raid quest credit,
the copied aura constants, ammo pick.

Each relay leg declares its table, subscription query, replay mode, row callbacks and outbound
builder in its family file. The subscription list and the per-shard callback registration are
generated from those declarations. Split the subscription builders by family next to their codec.

Seams

The test seams, highest first. Existing seams are preferred; no new seam is introduced before
phase 3.

  • Durable tests against a standalone SpacetimeDB through the test-support Standalone harness:
    the reducer interface. Unchanged by this plan.
  • Gateway family dispatchers through their family Store Fake. Existing; phase 3 makes it the only
    Gateway unit seam.
  • Module in-memory adapters behind the creature cycle, transfer, mail escrow and auction traits.
    Existing; keep and copy into other families when they need one.
  • The CLI test seam for the Gateway binary's provision path. Existing; absorbs the deleted main
    scans.

The lead agent confirms this list with the user before phase 3 begins.

Testing Decisions

A good test crosses the same seam a caller does and survives any refactor that preserves
behaviour. It calls a reducer, a dispatcher with a Fake, or a pure function. It never reads source
text, never pins a call sequence, and never asserts on a Fake's recording format.

Prior art to copy:

  • the weather durable test for a reducer-level test,
  • the vendor and guild dispatcher tests for a family test with a local Fake,
  • the mail test builder and the party topology builder for setup in one line,
  • the creature cycle scenario builder for a fluent in-memory world,
  • the package-delta crate's one-test-file-per-concern layout.

What each phase proves:

  • Phase 1: every test still passes after deletion; each retained Architecture Test has a negative
    test that shows it still fires.
  • Phase 2: every internal markdown link resolves; the documented commands are the CI commands.
  • Phase 3: each family's tests compile against only that family's Fake; the schema parity and
    durable suites are unchanged.
  • Phase 4: one test per family shows a transport loss and a Refusal produce different outcomes
    through the typed classifier.
  • Phase 5: an opcode ownership test fails when two families claim one opcode.
  • Phase 6: the official Packages build against the narrowed roots; the Package API lint runs as a
    named test.
  • Phase 7: the taxonomy enum is exhaustive in the match; each moved rule has a shared-crate test
    that both tiers call.

CI, the durable suite and the publish safety tests stay green at every merge.

Out of Scope

  • Splitting the Coordinator's three roles (connection, router, session owner).
  • Any change to the wire protocol, the schema, or Realm topology.
  • Changing the gate-then-delegate shape of the gw_* reducers, which stays as is.
  • Building a ReducerContext harness for the Module; the durable tier remains the reducer seam.
  • Splitting the importer's main file, and the typed Standalone fixture. Both are worth doing and
    can follow as their own issues.
  • Rewriting the verification write-ups; they are collected, not edited.
  • Production operations: no publish, no Realm host touched.

Further Notes

Counts in this plan were measured on b8113305 on 2026-10-10: 185 WorldStore methods plus 105
from supertraits, 85 defaults of which 78 are never used, 188 Module text assertions, 16 Gateway
files with source scans, 75 unused account-id parameters, 47 string matches on the transport
message, 23 dispatch links, 23 Package API roots, 21 files for the Distract effect.

The parts worth keeping and copying: the gw_* gate-then-delegate pattern, the in-memory adapters
behind the cycle, transfer, mail escrow and auction seams, the Lua Runtime Script surface, and the
package-delta crate's layout.

Memory notes that apply to the lead agent: one branch and one merge per phase, no shippable
intermediates; give subagents absolute paths; never bulk-remove worktrees; the publish safety trio
in the Module is never relaxed.

Activity

  1. added
    ready-for-agentComplete enough to build from the issue alone, by a person or their agent
    on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentComplete enough to build from the issue alone, by a person or their agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions