Skip to main content

data-model

PitLog's entities (SPEC §6.5), the multi-tenancy rule every query must obey, and which table to reach for. Covers only what packages/db/src/schema.ts does not make obvious — the tenancy invariant, the identity-table exceptions, the sync column contract, and the modelling calls that look wrong until you know why. TRIGGER when writing any database query, adding or changing a table, or deciding where a new piece of data belongs. DO NOT trigger for UI-only changes, docs, or config.

インストールへ移動

ソース情報

リポジトリ
weaties/pitlog
ソースの最終更新活動
2026年8月19日 15:32
検出された SKILL.md の言語
英語
スター
0
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
data-model
description
PitLog's entities (SPEC §6.5), the multi-tenancy rule every query must obey, and which table to reach for. Covers only what packages/db/src/schema.ts does not make obvious — the tenancy invariant, the identity-table exceptions, the sync column contract, and the modelling calls that look wrong until you know why. TRIGGER when writing any database query, adding or changing a table, or deciding where a new piece of data belongs. DO NOT trigger for UI-only changes, docs, or config.
# PitLog data model `packages/db/src/schema.ts` is the source of truth for columns, types, and indexes — read it, don't ask this skill to restate it. What follows is the rules that are not visible in the file. ## The tenancy rule **Team is the tenancy boundary (SPEC §4). Every domain table carries a non-null `team_id`, and every query filters on it.** There is deliberately **no ambient tenant** in the db client. `createDb()` returns a plain Drizzle handle, so a missing scope shows up in the query text rather than hiding inside a wrapper that someone will later bypass. ```ts // Right — the scope is visible at the call site. await db.select().from(s.stints) .where(and(eq(s.stints.team_id, teamId), eq(s.stints.session_id, sessionId))) // Wrong — reachable from any team that can guess a session id. await db.select().from(s.stints).where(eq(s.stints.session_id, sessionId)) ``` Filtering by a child key (`session_id`, `expense_id`) is **not** a substitute. Ids are UUIDs, not secrets: they appear in URLs, in sync payloads, and in exported data. ### The four exceptions `teams`, `users`, `login_tokens`, `auth_sessions` have no `team_id`: - `teams` **is** the tenant — its `id` is the team id. - `users` is cross-tenant: one account, zero or more teams. Tenancy lives in `memberships`. - `login_tokens` is issued before we know which team a login lands in. - `auth_sessions` belongs to the account, not a team. `packages/db/src/schema.test.ts` asserts this allowlist **in both directions** — a new domain table without `team_id` fails, and adding `team_id` to an identity table also fails. If you are about to edit that allowlist, stop and think about whether you actually want a cross-tenant table. ## The sync column contract Every table a pit client can write to carries: | Column | Why | |---|---| | `id` (uuid, PK) | **Client-generated.** A phone with no signal must be able to create a row and know its id. Never use a database default here. | | `client_updated_at` | The LWW comparator. Comes from the **writing device's** clock. | | `server_updated_at` | Receipt stamp. **Never** the comparator. | | `deleted_at` | Soft delete, so an offline delete can be replayed and surfaced. | | `updated_by` | Ties the write to a user; also the deterministic LWW tie-break. | Using `server_updated_at` to resolve conflicts is the single most tempting mistake here: it silently reorders a write made offline hours earlier behind one made online seconds ago. See `/offline-sync`. **Reads must exclude soft-deleted rows** (`isNull(table.deleted_at)`) unless you are deliberately building a history or conflict view. ## Which table | You want to record | Table | Note | |---|---|---| | "someone tapped a button at this instant" | `log_entries` | The append-mostly stream. Kinds are the SPEC §5.2 list plus `clapper`. | | a seat, planned or actual | `stints` | `planned_*` vs `started_at`/`ended_at`. A plan is a stint row with no actuals yet. | | fuel going into the car | `fuel_fills` | `filled_to_full` matters — only a brim fill is a usable burn-rate datapoint. | | a lap time | `laps` | Both `official` and `gps` rows coexist. Never merge them. | | tyres or pads as a unit | `consumable_sets` + `consumable_events` | The set is the thing; events are what happened to it. | | money spent | `expenses` (+ `expense_shares`, `receipts`) | Integer cents. Shares must sum to the parent. | | a YouTube video tied to a session | `media_assets` | `t0` is wall-clock at video time zero; `clock_scale` is the optional drift fit. | | a GPX/IMU capture | `telemetry_files` | Manifest + object key only. | **There is no row-per-sample table and there must not be one** (SPEC §6.5). Telemetry lives in files referenced by manifests. A schema change that adds a samples table is a spec change — open a PR against `docs/SPEC.md` first. ## Modelling calls that look wrong until you know why - **`series` is team-scoped**, not shared reference data. Duplicated rows are the price of a tenancy rule with zero exceptions, and it lets a team fork a rule set without an admin UI for editing global rows. - **`rule_configs` is append-only.** An edit is a new `version` row, not an `UPDATE`. The active one is flagged `is_active`. History matters: a schedule run last season was run against specific rules. - **Expenses reference `drivers`, not `memberships` or `users`.** SPEC §5.3 says "split costs among drivers"; `drivers` is the per-team person record. Crew who share costs but never take a seat get `can_drive = false`. - **`laps` has no unique constraint on `(session, source, lap_number)`**, only an index. Official timing renumbers after a protest. The unique constraint is on `external_id` where present, so a re-pull updates instead of duplicating. - **Drivers have `first_name` and `last_name` separately** because visitors see first names only (SPEC §4: no PII beyond driver first names). Any visitor-facing serializer must respect that. ## Adding a table 1. Add it to `schema.ts` with `team_id` and the sync columns (copy the `syncColumns` spread from a neighbour). 2. Run `npm run db:generate` — see `/new-migration` for the mechanics. 3. `schema.test.ts` will fail if you missed the invariants. That is the test doing its job; fix the schema, not the test. 4. If the table is not in SPEC §6.5, say so in the PR body and explain why the spec's first cut needs widening.
GitHubで見る