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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ coverage/
# Screenshots
Screenshot*.png

# Loading scripts
# Loading scripts (one-off tournament data dumps, kept local only)
packages/bot/scripts/load*.ts
# ...except the committed loader-harness template
!packages/bot/scripts/load-example-spec.ts

# Database backups
*.backup.sql
Expand Down
51 changes: 51 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Scorebot

Live score tracking for KC Ultimate (Tech) games, fed by WhatsApp chat messages. This glossary is the canonical language for game and stats concepts across bot, shared, and web packages.

## Language

### Game structure

**Game**:
One tracked match between our team and an opponent, made up of an ordered list of events.

**Event**:
A single recorded happening in a game: a goal, halftime, timeout, note, game start, or game end.

**Point**:
One unit of play from a pull to a goal. Every goal event ends exactly one point.
_Avoid_: possession, rally

**Point Ledger**:
The derived, per-point account of a game: for each point, who scored, whether it was a hold or a break, which of our lines played it, and whether we forced a turn. All break/hold and line questions are answered from the ledger.

**Starting on Offense**:
Whether our team received the first pull. Determines possession at the start of each half; the receiving team flips at halftime.

### Point outcomes

**Hold**:
A point won by the team that started it on offense.

**Break**:
A point won by the team that started it on defense.

**Dirty Hold**:
A hold where our O-line first lost possession and had to force a turn to win the point back. Counts as a hold, signals sloppy offense.

**Failed Conversion**:
A D-line point where we forced at least one turn but still conceded the goal.

**Forced Turn**:
A change of possession our defense caused (block or steal), as opposed to an opponent's unforced error. Only our own forced turns are logged.

**Inferred Result**:
A hold/break call made without knowing starting possession, guessed from consecutive scoring. Display-only: shown on the timeline, excluded from efficiency stats.

### Lines

**O-line**:
The lineup fielded when we start a point on offense.

**D-line**:
The lineup fielded when we start a point on defense.
12 changes: 12 additions & 0 deletions docs/adr/0001-do-first-dual-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The Durable Object is authoritative; every write goes DO-first, then D1

Every game mutation is a dual-write: the GameState Durable Object holds live state, D1 holds the durable mirror and query index. We decided the DO is authoritative for a live game and all writes flow DO → D1, with D1 failures surfaced as errors rather than swallowed. The GameStore module is the only place this ordering lives; callers never coordinate the two stores themselves.

## Considered Options

- **D1-first with the DO as cache** — one always-authoritative store, but forfeits the DO's single-threaded per-game ordering and invariant enforcement, and would require invalidation plumbing.
- **Keep the historical split** — metadata updates used to write D1-first with a fire-and-forget DO update, while event writes went DO-first. Rejected: a failed fire-and-forget silently diverged the stores until eviction, and the ambiguity already produced a data-loss bug (`INSERT OR REPLACE` cascade-deleted a game's events; see the comment in `packages/bot/src/db/database.ts`).

## Consequences

On cold start (DO evicted) the store rehydrates the DO from D1, so D1 is briefly the seed — but never the write target of record. Any future write path added outside GameStore should be treated as a bug.
7 changes: 7 additions & 0 deletions docs/adr/0002-do-rpc-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# GameState is called via Durable Object RPC, not fetch routing

GameState originally exposed a hand-written `fetch()` switch over ~10 magic paths because pre-2024 Workers runtimes required an HTTP boundary for DOs. We decided to bump `compatibility_date` (from 2024-01-01 to a current date, ≥ 2024-04-03) and expose typed RPC methods instead, deleting the path switch and all request/response JSON marshalling in callers and tests.

## Consequences

The compatibility-date bump flips other runtime defaults accumulated since 2024-01-01; the deploy needs a smoke test of game creation, event add/undo, and the stats endpoints, with rollback being a redeploy of the previous compat date. Tests call DO methods directly instead of constructing 104 `new Request(...)` objects.
398 changes: 398 additions & 0 deletions docs/architecture-review-2026-07-06.html

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/bot/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ async function build() {
format: 'esm',
platform: 'browser',
target: 'es2022',
external: [],
// `cloudflare:workers` is provided by the Workers runtime — keep it
// external so workerd resolves it instead of esbuild trying to bundle it.
external: ['cloudflare:workers'],
conditions: ['worker', 'browser'],
mainFields: ['browser', 'module', 'main'],
resolveExtensions: ['.ts', '.js'],
Expand Down
Loading
Loading