chore: add contribution guidance

This commit is contained in:
ayo 2026-07-04 09:19:22 +02:00
parent 062b912549
commit 5ec22fe37e
5 changed files with 105 additions and 3 deletions

View file

@ -0,0 +1,30 @@
# 0001. Backend-agnostic package boundary
**Status:** Accepted · **Repo:** cozy-games · **Date:** 2026-07
## Context
cozy-games is a collection of reusable game modules (`@ayo-run/mnswpr`, the
leaderboard package, future extractions). Applications built on these packages
differ in how they store data, authenticate users, and enforce rules.
## Decision
Packages in this repo are **backend-agnostic and permission-agnostic**: they
expose capability, never storage or authorization. Any backend — database, auth,
server-side logic — is supplied by the consuming application through injected
adapters and hooks (see 0002). Package code contains no storage-, deployment-, or
authorization-specific logic.
## Rationale (technical)
- Permission-agnostic packages are correct library design: authorization and
storage belong to each deployment (e.g. via security rules and server-side
contexts), not to library code.
- Backend-agnostic packages are more adoptable, testable, and contributable; any
app or backend can consume them.
- Standalone packages are more reusable and testable than a monolithic app.
## Consequences
- Public API changes in these packages are semver events for downstream consumers.
- Contributions must not introduce coupling to any specific backend or deployment.
- These decision records cover package and architecture decisions only; storage,
deployment, and operations choices belong to each consuming application, not to
this repo.

View file

@ -0,0 +1,34 @@
# 0002. Game-agnostic cores behind a game-adapter interface
**Status:** Accepted · **Repo:** cozy-games · **Date:** 2026-07
## Context
Multiple games (Minesweeper today, Sudoku planned) share generic infrastructure:
move logging, replay, timing, and leaderboards. Building this per-game duplicates
logic; abstracting prematurely risks wrong boundaries.
## Decision
Core modules are written **game-agnostic from day one**; each game supplies an
**adapter**. Package extraction and npm publishing are deferred until a second
game consumes the seam ("create seams, not packages").
### The adapter contract (v0, will be frozen after the second game ships)
A game adapter supplies:
1. **Event vocabulary** — a typed set of move events (Minesweeper: `reveal | flag | unflag | chord`). Core code handles only the generic envelope `MoveEvent<T>`: `{ seq, clientTs, type: T, payload }` wrapped in a log carrying `schema_version`.
2. **Progress reducer**`progress(events) → percent` for progress display.
3. **State reducer**`apply(state, event) → state` for full replay and validation.
4. **Terminal predicate**`isTerminal(state) → win | loss | null`; generic timing code measures first event → terminal event.
5. **Board payload type** — serialized layout (e.g. grid + mine positions) stored as typed JSON under a `game_type` discriminator.
6. **Headless core** — pure functions (`generateBoard`, reducers, predicates) runnable in Node with no DOM dependency; presentation is a separate layer.
7. **Board injection & resume** — constructors accepting an externally supplied board and a mid-game state snapshot.
### Rules
- Core/engine modules import no game-specific types.
- The event vocabulary and log schema_version live in the game's package; recorded logs are replayable forever (schema changes are versioned, never breaking).
- Generic storage columns (id, player, outcome, timestamps, game_type) never require migration to add a game; new games add a payload type and adapter only.
## Consequences
- Adding a game = writing one adapter; infrastructure is untouched.
- The replay engine and the log envelope become extractable packages once validated by the second adapter.
- The adapter interface is frozen and versioned after the second game ships; breaking changes thereafter require a new decision record.

View file

@ -0,0 +1,23 @@
# 0003. Stored board layouts, not reproducible seeds
**Status:** Accepted · **Repo:** cozy-games · **Date:** 2026-07
## Context
Replay and shared-board features require two plays of the same board. Two options:
(a) seeded PRNG generation, where a seed reproduces the board; (b) serialize and
store the full board layout at game start, referenced by an id.
## Decision
Store the full layout (b). Boards are serialized as the game's payload (per
0002 §5) and referenced by an unguessable `game_id`.
## Rationale
- **No generator lock-in:** seeded reproduction breaks if the generation algorithm ever changes (bugfix, difficulty tuning, library swap). Stored layouts are immune to generator-version drift — old games replay forever.
- **No refactor required:** existing generators keep working; serialization is additive.
- **Simpler single-use semantics:** "a board is played once per player" is a fact about a stored entity, not a rule about seed distribution.
- Layout size is trivial (a Minesweeper expert board < 1KB).
## Consequences
- Games recorded before layout storage existed cannot be replayed (archived instead).
- `game_id` values may appear in URLs → must be unguessable (no sequential IDs).
- Stored layout data is provided to clients as game rules require.

14
docs/decisions/README.md Normal file
View file

@ -0,0 +1,14 @@
# Decision records
Short documents that capture a significant technical decision — its context, the
choice made, the reasoning, and the consequences. One file per decision, numbered
and append-only. (This format is commonly called an **Architecture Decision
Record**, or ADR.)
Records are immutable once accepted. A later decision that changes an earlier one
is added as a new record that supersedes it, rather than editing the old one — so
the history of *why* stays intact.
- [0001 — Backend-agnostic package boundary](0001-package-boundary.md)
- [0002 — Game-agnostic cores behind a game-adapter interface](0002-game-adapter-pattern.md)
- [0003 — Stored board layouts, not reproducible seeds](0003-stored-boards-not-seeds.md)

View file

@ -12,7 +12,8 @@ Goals, in priority order:
1. **Headless core** — game state is a plain data model, no DOM, no wall clock. 1. **Headless core** — game state is a plain data model, no DOM, no wall clock.
2. **Server-authoritative capable** — the same core can run on a server that owns 2. **Server-authoritative capable** — the same core can run on a server that owns
the RNG, the clock, and the move sequence, so leaderboard times are witnessed, the RNG, the clock, and the move sequence, so leaderboard times are witnessed,
not claimed. (Closes the score-spoofing gap from the security review.) not claimed — closing the gap where a client can otherwise report any finish
time it likes.
3. **Backwards compatible** — the existing DOM UI, CSS, and jsdom tests keep 3. **Backwards compatible** — the existing DOM UI, CSS, and jsdom tests keep
working; today's offline play is just "the core with a local transport." working; today's offline play is just "the core with a local transport."
4. **Extraction-ready** — a clean seam between generic (grid/session) and 4. **Extraction-ready** — a clean seam between generic (grid/session) and
@ -183,8 +184,8 @@ Two decisions that unlock everything:
replay(rules, { seed, config, log }) // → { status, time, valid, reason? } replay(rules, { seed, config, log }) // → { status, time, valid, reason? }
``` ```
This is the lighter "verifiable replay" anti-cheat path from the prior discussion: This is the lighter "verifiable replay" anti-cheat path: no live per-move server
no live per-move server needed — just call `replay()` in a Cloud Function at needed — just call `replay()` in a Cloud Function at
submit time. It requires exactly this headless core and nothing else. submit time. It requires exactly this headless core and nothing else.
> **Determinism is a hard rule for Layers 12:** no `Date.now()`, no > **Determinism is a hard rule for Layers 12:** no `Date.now()`, no