From 429fbde866f54f774a606160b1e818dfcc9ca233 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Mon, 17 Aug 2026 23:33:01 +0200 Subject: [PATCH] docs: add CLAUDE.md working agreement for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a4a5cde8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# 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`. + +**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 `