Files
STTracker/docs/plans/001-phase1-sdd/task-8-rulings.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

3.0 KiB

Task 8: controller rulings (binding, in addition to the brief)

These come from earlier tasks' reviews. Read the actual signatures in packages/core/src/{works,matching,entries,engagements,history}.ts before relying on the summaries below.

  1. field_sources contract (Task 6): entries.field_sources and engagements.field_sources are JSON maps from field name to the Actor that last set it. Plugin actor = plugin:<pluginId>:<connectionId>. upsertEntry/updateEngagement re-stamp field_sources for EVERY key present in the patch. So the engine must send only fields whose value actually changed and that it is allowed to apply. Never echo unchanged values, or a plugin takes ownership of a user's field.

  2. No reviving deletes. If the target entry or engagement is soft-deleted (the user deleted it, or a removal was accepted), the engine must NOT revive it via upsertEntry/updateEngagement for a plugin actor. Record the record as seen. Changes to it produce nothing, or at most a pending changed event. Revival is a user action only.

  3. Root ancestor entry (hierarchy ruling): whenever the engine creates an entry on a child work (a work with parent_id), it also ensures the user has an entry on the ROOT ancestor work (follow parent_id to the top). If none exists, create it with status "none" and the plugin actor. Never modify an existing root entry for this purpose.

  4. Map records to targets via source_records. A record already mapped (target_entity/target_id set) must be updated through that mapping, not re-matched. ID-less hints (CSV) would otherwise create a new private draft on every sync. Only new records go through matchWork.

  5. External-ID namespaces are type-specific (trakt_movie, tmdb_show, anilist_manga, isbn13, …). Match generically. The only namespace the engine names itself is musicbrainz_recording, for listens.

  6. Engagement dedupe (brief rule 2) compares partial dates by precision. Day-precision values compare as dates. A datetime value's date part is its first 10 characters. Never convert through Date in a way that shifts timezone.

  7. matchWork already handles unique-violation retries in its own transaction, so do not call it inside an outer transaction that would be poisoned by a retry. Pattern per record: matchWork first (own transaction), then open the apply transaction and pass tx to the user-layer functions.

  8. Inside any transaction use tx, never db.k. SQLite runs on one connection behind a mutex, so db.k inside a transaction deadlocks.

  9. Rate limits and secrets (Task 9's concern, not yours): runSync receives a makeContext from the caller. Do not build http clients here.

  10. Listens: the listens unique key is (user_id, source, source_key). source = the plugin id, and source_key = the record key.

  11. Docs duties: update docs/ARCHITECTURE.md (the sync data flow is now real) and append to docs/DECISIONS.md any non-obvious choice: the hash composition, anti-flip-flop, no-revive, root-ancestor. Put new entries at the TOP, newest first.