Files
STTracker/docs/plans/001-phase1-sdd/task-9-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.1 KiB

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

  1. Base. Your branch task/9 starts at feat/phase1. Task 8 (the sync engine, which provides runSync in packages/core/src/sync/run.ts) is on branch task/8, still in a fix round. First run git merge task/8. Resolve the conflicts in docs/ARCHITECTURE.md, DECISIONS.md and TODO.md by keeping BOTH sides' content: DECISIONS newest-first, ARCHITECTURE combining both module lists. Commit the merge. Task 8 will change internals later, but runSync(deps, plugin, connectionId, mode) and RunSummary are stable.

  2. Rate limits. Add an optional rateLimit?: { perMinute: number; burst?: number } to PluginManifest in packages/plugin-sdk/src/types.ts. makePluginContext builds ctx.http with createHttpClient({ ratePerMinute: manifest.rateLimit?.perMinute ?? 60, burst: manifest.rateLimit?.burst ?? 5, userAgent: "STTracker/0.1 (+self-hosted)" }). Then set rateLimit on each built-in plugin manifest (plugins/*/index.ts):

    • trakt: perMinute 100
    • hardcover: perMinute 50
    • mangabaka: perMinute 150
    • lastfm: perMinute 240
    • listenbrainz: perMinute 60
    • csv: none

    Remove the now-unused local RATE_PER_MINUTE constants if nothing uses them.

  3. Secrets. Config fields whose manifest type is "secret" are persisted ONLY inside the encrypted credentials_enc blob (encryptJson from @sttracker/core/crypto, with the instance secret), never in plain config.

    • createConnection/updateConnection split the incoming config accordingly.
    • makePluginContext merges the decrypted secrets back into ctx.config, and ctx.credentials.get() returns the decrypted blob (secrets plus any OAuth tokens a plugin saved).
    • credentials.save(c) merges c into the blob and persists it encrypted IMMEDIATELY (Review focus 5).
    • listConnections never returns secrets: config only, plus a hasCredentials: boolean.
  4. Lock. Sync jobs use lockKey = "sync:<connectionId>". enqueue returns null if a queued or running job holds the same lockKey. The lock check and insert must be race-safe on Postgres: wrap them in a transaction with pg_advisory_xact_lock(hashtext(lockKey)) on Postgres (SQLite is already serialized).

  5. Job claim. update jobs set status='running', locked_at=… where id=? and status='queued', checking numAffectedRows === 1n. The dialect maps numAffectedRows for both databases.

  6. Weekly full. The sync handler runs full if the plugin lacks pullChanges, if the connection's last successful full run is older than 7 days (look at source_runs with mode full and status ok), or if the payload says mode: "full". Otherwise changes.

  7. Plugin loading uses import(pathToFileURL(file).href + "?v=" + Date.now()) for reload. The echo fixture plugin lives under packages/core/test/fixtures/plugins/echo/index.ts. A broken plugin fixture (a default export without a manifest) sits next to it.

  8. Docs: ARCHITECTURE (the job worker, registry and connections are real) and DECISIONS (top) for the rate-limit manifest field, the secret split and the lock.