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.
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 backendruff check backendandpytest(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-Bytrailer, 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 releasefrom the feature branch: merge tomain→ 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≠canceledliteral 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,
grepfor 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.