Files
STTracker/docs/plans/001-phase1-sdd/reports/task-1-report.md
T
Lucas WintherandClaude Opus 5.5 be04fab098 docs: phase 1 handoff, execution ledger and task reports
Captures the owner's requirements and decisions from the planning
conversation, current branch/worktree state, open tasks 17-19, pending
owner decisions and the controller's rulings so a new agent can resume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 04:53:02 +01:00

119 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task 1 report: `packages/db`
**Status:** DONE_WITH_CONCERNS (concerns are minor; see the end)
**Branch:** `feat/phase1`, base fb4ff36
## Commits
- `6c5a9fb` chore: workspace package stubs with approved dependencies
- `8248a2a` feat(db): Bun.SQL Kysely dialect for sqlite and postgres
- `27ec24e` feat(core): shared domain error classes
## What was built
- **`packages/db/src/dialect.ts`**:
- `type Dialect = "sqlite" | "postgres"` and `dialectOf(url)`, which throws on an unknown scheme.
- `BunSqlDialect` is one Kysely `Dialect` over `new SQL(url)`. It picks the Sqlite or Postgres adapter, compiler and introspector by URL scheme.
- `executeQuery` calls `runner.unsafe(sql, params)`. It sets `numAffectedRows` only for INSERT/UPDATE/DELETE/MERGE, and `insertId` only for SQLite INSERT.
- Transactions are `begin`/`commit`/`rollback` on the held connection. On Postgres, `begin` can carry an isolation level and access mode.
- **Deviation from the brief (forced by Bun):** Bun's SQLite adapter throws `This adapter doesn't support connection reservation` on `reserve()`.
- Postgres uses `sql.reserve()` as the brief says.
- SQLite uses the single `SQL` instance as its connection, guarded by a mutex. The lock is held from `acquireConnection` to `releaseConnection`, so a transaction never interleaves with other queries.
- This is recorded in DECISIONS.
- **`db.ts`:** `createDb(url): Promise<Db>`, with `Db { k: Kysely<Database>; dialect; close() }`.
- SQLite: runs `PRAGMA foreign_keys = ON`, plus `PRAGMA journal_mode = WAL` unless the URL is `:memory:`.
- Postgres: runs `select 1` to fail fast on a bad URL.
- If init fails, the Kysely instance is destroyed before the error is rethrown.
- **`ids.ts`:** `newId()` returns `Bun.randomUUIDv7()`; `nowIso()` returns `new Date().toISOString()`.
- **`json.ts`:** `toJson` and `fromJson<T>(s, fallback)`. The fallback is returned for null, undefined or invalid JSON.
- **`migrate.ts`:** `migrateToLatest(db)` runs a Kysely `Migrator` (imported from `kysely/migration`, as Kysely 0.29 requires) with a static provider over `migrations/index.ts`. An empty set works. It throws with the name of the failing migration.
- **`migrations/index.ts`:** `export const migrations: Record<string, Migration> = {}`.
- **`schema.ts`:** placeholder `export interface Database {}`.
- **`testing.ts`:** `forEachDialect(name, fn)` registers `describe("<name> [sqlite]")`, plus `[postgres]` when `TEST_POSTGRES_URL` is set.
- `beforeEach` builds a fresh migrated DB: SQLite uses `:memory:`; Postgres runs `create database "st_<uuid hex>"` through an admin connection and uses the derived URL.
- `afterEach` closes the DB.
- `afterAll` drops every created database `with (force)`.
- **`packages/core/src/errors.ts`:** `AuthError(code)`, `ValidationError(field, reason)` (code `validation`), `CryptoError(message)` (code `crypto`), `ImportError(code)` and `NotFoundError(what)` (code `not_found`).
- All of them extend Error and have a readonly `code` and a `name` set.
- **Package stubs:** these get name, private, type module, `exports {"./*": "./src/*.ts"}`, and the dependencies from the ruling:
- packages: db, core, plugin-sdk;
- apps: server, web;
- plugins: trakt, hardcover, mangabaka, csv, listenbrainz, lastfm.
Versions are caret ranges of the npm latest on 2026-10-30:
- kysely ^0.29.7, zod ^4.6.7, fflate ^0.8.3;
- react and react-dom ^19.3.1, wouter ^3.13.0, @tanstack/react-query ^5.105.0;
- @types/react and @types/react-dom ^19.3.0.
`bun install` was run and `bun.lock` committed. `bun install --frozen-lockfile` reports no changes.
- **Docs:**
- DECISIONS: three new entries, newest first (single Bun.SQL dialect; JSON as text; UUIDv7 instead of ULID).
- ARCHITECTURE: the map row for `packages/db` is updated.
## Files changed
- `bun.lock`
- `docs/ARCHITECTURE.md`, `docs/DECISIONS.md`
- `apps/{server,web}/package.json`
- `packages/{core,db,plugin-sdk}/package.json`
- `plugins/{trakt,hardcover,mangabaka,csv,listenbrainz,lastfm}/package.json`
- `packages/db/src/{dialect,db,ids,json,migrate,schema,testing}.ts`, `packages/db/src/migrations/index.ts`
- `packages/db/test/db.test.ts`
- `packages/core/src/errors.ts`
## TDD evidence
**Tests:**
- Inside `forEachDialect`, per dialect:
- "round-trips a row";
- "transactions roll back";
- "transactions commit" (extra);
- "newId is time-sortable" (1000 ids, sorted and unique).
- Outside it, once: a `fromJson` fallback test.
RED, written before any source existed:
```
$ bun test packages/db
error: Cannot find module '../src/ids.ts' from '/home/user/STTracker/packages/db/test/db.test.ts'
0 pass
1 fail
1 error
```
GREEN on SQLite:
```
$ bun test packages/db
5 pass
0 fail
12 expect() calls
Ran 5 tests across 1 file. [371.00ms]
```
GREEN on SQLite and Postgres:
```
$ TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres bun test packages/db
9 pass
0 fail
20 expect() calls
Ran 9 tests across 1 file. [1.87s]
```
**Mutation check:** I temporarily changed `rollbackTransaction` to issue `commit`. "transactions roll back" then failed on both `[sqlite]` and `[postgres]`, and I restored the code. After the Postgres runs, no `st_%` databases were left over.
**Manual probe:**
- `createDb("sqlite://rel.db")` creates `./rel.db` with `journal_mode=wal` and `foreign_keys=1`.
- `createDb("mysql://x")` throws `Unsupported database URL scheme: mysql`.
## Typecheck
```
$ bun run typecheck
$ tsc --noEmit -p tsconfig.json
```
It exits clean with no output.
## Self-review findings
- The mutex is released through Kysely's `try/finally` in its connection provider, so a failing query cannot leave the SQLite lock held.
- `numAffectedRows` is not set for SELECTs, because Bun's `count` there is the row count.
- Postgres `count(*)` comes back as a string (bigint). The test uses `Number()`. Later tasks should do the same or cast in SQL.
- Not implemented, on purpose: savepoints (Kysely raises a clear error if they are used), and true streaming (`streamQuery` yields the whole result as one chunk). Both are recorded in DECISIONS.
- Fixed during review: one claim I could not verify, about how Bun.SQL returns `jsonb`, was removed from the DECISIONS text.
## Concerns
1. **SQLite has no `reserve()`.** That is why it uses a single connection behind a mutex, which deviates from the brief's "acquireConnection returns `sql.reserve()`". The public interface is unchanged.
2. **Postgres test cost.** `create database` takes about 100–300 ms per test, and once took about 1 s. One run of 4 PG tests took 11 s; reruns took 1.6–3.8 s. With many DB tests in later tasks, the Postgres suite may get slow. A faster option is one DB per `describe` with a schema reset per test. That would change the harness contract, so I did not do it.
3. **ULID wording.** `AGENTS.md` (Rules: "IDs are ULIDs") still says ULID. The DECISIONS entry says to read it as UUIDv7. Editing `AGENTS.md` was outside my ruling, so the maintainer or controller may want a one-word fix.
4. **Kysely 0.29 import path.** `Migrator`, `Migration` and `MigrationProvider` must be imported from `kysely/migration`; the root exports are deprecated type errors. Task 2's migration files should import `Migration` from `kysely/migration`.