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>
6.3 KiB
Task 5 report
Built packages/core/src/works.ts (createWork, getWork, updateWorkFromHint, findWorkByExternalIds, searchWorks, resolveAlias, adminMergeWorks, Work, plus *In(k) variants usable inside transactions) and matching.ts (fuzzyScore, matchWork). Tests in packages/core/test/{works,matching}.test.ts. acceptSuggestion/rejectSuggestion omitted per controller ruling (Task 6). TDD: first run of the suggestions test failed on both DBs (0 suggestions); fixed; final: sqlite 11 pass, sqlite+postgres 21 pass, typecheck clean. Decisions / deviations:
- fuzzyScore: a component missing on either side (year, creators) is dropped and weights renormalised; otherwise "The Expanse: Leviathan Wakes" vs "Leviathan Wakes" scores 0.467, not >0.6, and Pokemon=1 with absent year/creators would be 0.7.
- Candidate LIKE bounding uses OR over up to 5 significant tokens (>=3 chars), not only the first token: first-token-only misses "Expanse" vs "Leviathan Wakes". Still limit 200, scored in TS.
- Parent without ids/position: reuse shared work with identical media type, lower(title), year, parent; else create shared.
- Position child lookup: parent_id + normalized position + shared + unmerged.
- adminMergeWorks does not repoint children, list_items, copies or listens (getWork follows tombstones). Role read from users.role. Logs work_history for both works; nothing in user_history.
- Work type keeps snake_case column names with creators/metadata/position parsed.
- createWork writes a work_history "created" row.
Fix round 1
Commit 437ca14 on task/5: fix(core): race-safe matching, visibility-checked ID matches, child-preserving merges.
Changes:
- 1a:
packages/db/src/migrations/0002_works_position_unique.ts(unique indexworks_parent_id_position_uqonworks(parent_id, position), withdown), registered inmigrations/index.ts.schema.test.ts"migrating down" now usesmigrateTo(NO_MIGRATIONS)(onemigrateDownonly undoes 0002). - 1b:
matchWorkre-runs the whole resolve in a fresh transaction on a unique violation, max 3 attempts.isUniqueViolation(err)(exported from matching.ts) checkscode/errno"23505", or a message with "UNIQUE constraint failed" / "duplicate key", walking up to 5 levels ofcause. - 1c:
addMissingExternalIdsinserts withonConflict(columns namespace,value).doNothing(). It records an id in history only ifnumInsertedOrUpdatedRows > 0. - 2:
adminMergeWorksrepointsfrom's children toto, except a child whose non-null positiontoalready has. Moved child ids are logged into's work_history (children). - 3:
findWorkByExternalIds(db, ids, userId = null)/findWorkByExternalIdsIn(k, ids, userId)follow merges and accept a work only if it is shared or owned by userId (null = shared only). An invisible match is skipped and the next namespace is tried.matchWorkpasses userId; parent resolution passes null.createWorkwithownerUserId+ external ids throwsValidationError("externalIds", "private works cannot carry external ids"). - 4:
draftreads the owner of the aliased workId.adminMergeWorksthrowsValidationErrorunless both works are shared. - Docs: DECISIONS.md entry (race safety, visibility, merge children). TODO.md known issue: a shared parent resolved without IDs or position (title/year reuse) has no unique key, so concurrent matches can still duplicate it.
New tests:
- schema: works (parent_id, position) uniqueness; NULL parents and NULL positions are not constrained.
- works: createWork refuses ext ids on private; findWorkByExternalIds visibility (user B gets the shared work, never A's private one, even when A's private work owns one of the hint's IDs; A gets its own); getWork merged_into cycle → null; literal
_in search; adminMergeWorks refuses private; merge show A into B, then matching A's episodes creates no works (s1 moved, s2 left under A because B had s2). - matching: concurrent (Promise.all ×3) same new external id → 1 work and 1 external_id; concurrent same new episode chain → 3 works, same id; concurrent new season/episode under an existing show → same id, 3 works; concurrent addition of the same new ext id → no failure, 1 row; draft follows the alias.
RED (before the fix): sqlite 17 pass / 5 fail. Postgres run: 29 pass / 14 fail; the concurrency tests failed with PostgresError: duplicate key value violates unique constraint "external_ids_namespace_value_uq", errno "23505". Before the migration, the schema test failed on sqlite.
GREEN:
bun test packages/core packages/db: 30 pass, 0 fail (92 expect calls).TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres bun test packages/core packages/db: 58 pass, 0 fail (175 expect calls).- Postgres matching.test.ts repeated 5×: 21 pass, 0 fail each time.
bun run typecheck:tsc --noEmit -p tsconfig.json, exit 0, no output.
Note: on SQLite the concurrency tests pass trivially, because the single connection serialises transactions. Only Postgres exercises the retry.
Fix round 2
Commit on task/5: fix(core): follow merged tombstones in the position lookup.
Changes:
matching.tsstep 2: the position lookup no longer filtersmerged_into_id is null. A found child is followed throughfinalWorkId, and that surviving work is the one updated and returned. Before, a merged tombstone still held its (parent_id, position) slot underworks_parent_id_position_uq, socreateWorkIncollided on every retry.- docs/TODO.md known issue: "Admin merge does not merge colliding children recursively; colliding children stay under the tombstoned parent."
Test (matching.test.ts): "position lookup follows a merged tombstone instead of colliding with it". It creates episodes E1 (s1e1) and E2 (s1e2) under one season and admin-merges E1 into E2. Matching an ID-less hint for that season at E1's position then returns { workId: E2, created: false, draft: false } and creates no works.
RED: bun test packages/core/test/matching.test.ts: 11 pass, 1 fail, with SQLiteError: UNIQUE constraint failed: works.parent_id, works.position.
GREEN:
bun test packages/core packages/db: 31 pass, 0 fail.TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres bun test packages/core packages/db: 60 pass, 0 fail.bun run typecheck: exit 0.
Note: following the tombstone applies the hint's display fields to the surviving work (E2), the same as an external-ID match through a merge in step 1.