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.
This commit is contained in:
@@ -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<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.*
|
||||
Reference in New Issue
Block a user