Files
siftlode/CLAUDE.md
T
peter 429fbde866 docs: add CLAUDE.md working agreement for AI agents
The codebase is fully AI-authored and developed across many agent sessions; the
canonical conventions have lived only in a private memory store, which the
2026-08-17 review flagged as a real gap (unwritten rules → asymmetric fixes and
comment-vs-code drift, e.g. the cancelled/canceled literal bug).

Add a short, auto-loaded rules-of-the-road: the local gate (never merge/ship
red, no --skip), the trunk workflow and no-AI-co-author commit rule, the
one-way architecture shape, a canonical-pattern table (the review's §6
consistency choices), and the house rules learned from real bugs. Kept
deliberately thin; slims to a pointer once docs/development.md lands.
2026-08-17 23:33:01 +02:00

5.5 KiB

CLAUDE.md — working agreement for AI agents on Siftlode

This repository is fully AI-authored — every line was written by an AI agent, and it continues to be developed that way. This file is the rules of the road: read it before you touch anything. It is loaded into every agent session, so keep it short and true. A rule here that has gone stale is worse than no rule — if you change how the code works, change this file in the same commit.

Siftlode is a self-hosted YouTube reader (+ a local Plex browser/player and a download center). It is a modular monolith: one Postgres, one FastAPI process (uvicorn + APScheduler), one worker process (yt-dlp/ffmpeg), one PO-token sidecar. React 18 + react-query frontend with a router-free navigation provider.

The gate — never merge or ship red

The local gate is ruff + typecheck (3 tsc projects) + eslint + prettier --check + knip + vitest + pytest. It runs on every publish, which refuses to build on failure — there is deliberately no --skip flag. Run it and get it green before any merge, release, or handing work to review.

  • Full gate (agents with the private ops dispatcher): siftlode check.
  • Directly, without the dispatcher: cd frontend && npm run typecheck && npm run lint && npm run knip && npm run test, plus backend ruff check backend and pytest (the DB-lane needs Postgres).
  • New DB-backed test? It must actually run, not silently skip — assume SIFTLODE_TEST_REQUIRE_DB=1.

Workflow

  • Trunk model. One long-lived branch, main. Branch off it (feature/, bug/, improvement/), keep commits logical and Conventional-Commit-styled.
  • No AI attribution in commits. No Co-Authored-By trailer, no "generated by" lines. (This file being AI-written is the intended, visible exception; commits are not.)
  • Release is a deliberate, user-owned step (siftlode release from the feature branch: merge to main → publish → deploy → verify /api/version). Do not self-ship; bring work to a green, committed state and stop for the user's go-ahead.
  • Public repo. Keep ops/topology/secrets out — those live in a separate private repo. Nothing here should assume or leak them.

Architecture shape (don't fight it)

routes/* (HTTP only: validation, auth-dependency, serialization) → domain packages sync/, plex/, downloads/, youtube/ (shared by routes + scheduler + worker) → flat core (auth, quota, sysconfig, state, audit, notifications, userscope, realtime, ratelimit, progress, security) → models/db/config. Dependency direction is one-way: core never imports routes. Frontend: main.tsx provider tree → App.tsx shell → moduleRegistry route-chunks → components; data via react-query (qk factory), per-account localStorage, and server prefs; API through lib/api/* slices over core.req<T>.

Not wanted (reviewed and rejected as over-engineering for this size): microservices, event bus, CQRS, a repository/DI framework, a router library, a generic CRUD-router factory or base serializer.

Canonical patterns — when you have a choice, pick THIS

Area Use Not
ORM SQLAlchemy 2.0 select() db.query(...)
Request body a Pydantic model payload: dict
Ownership check owned_or_404 / get_or_404 ad-hoc _own_playlist/_owned/_own_job
Admin gate import app.auth the app.routes.admin re-export
Upstream failure status 422 with JSON detail 502 (reserve 502 for raw byte-proxies)
User-facing error structured detail ({code, reason}) a bare English string under a bilingual UI
Mutation response {"ok": true, …} ({"deleted": id}) ad-hoc {"unsubscribed"}/{"cleared": n}/echo
Transaction commit the handler/job-wrapper commits; helpers flush helpers that commit (only quota owns its session)
Admin knob storage sysconfig (values) env / app_state / scheduler_settings scatter
Job status word the downloads/status.py StrEnum loose "canceled"/"cancelled" literals
Current time inline datetime.now(timezone.utc) new now_utc/_now helpers
FE per-account state usePref raw readAccount/writeAccount or bespoke hooks
FE control the chip/segmented primitives (Select only for >6 options) a native <select>
FE loading/empty/error QueryState / StateMessage ad-hoc conditionals

House rules that bite (learned from real bugs)

  • A rule stated in a comment must also be enforced in code. The worst bugs here came from a comment and the code/migration drifting apart (a cancelled≠canceled literal killed a cancel path). Put the invariant in an enum/constant/shared function, not in prose.
  • Fix a principle everywhere, in one sweep. If you correct one owner/commit/serialization site, grep for the siblings and fix them too — asymmetric fixes are a recurring smell here.
  • Honest states. An error must not render as an empty state; a swallowed failure must not look like "no results". Reads degrade honestly; writes are conditional (UPDATE … WHERE status='running').
  • i18n parity. HU/EN, zero key drift. Every user-facing string is translated.
  • Comment the "why", keep it current. This codebase uses comments as its architecture doc; a stale "why" is a liability, so update it when the behavior changes.

A fuller written-conventions doc (docs/development.md) is planned; once it lands, this file slims to a pointer plus the gate/workflow/no-attribution rules. Until then, the table above is the source of truth.