@jpgilldev / steid

steid/CLAUDE.md
10.7 KBRaw
1# Steid
2
3A personal-first gitforge in Rust. Hosts git repos, writing, and projects for one
4developer or an organisation. **Portfolio-first, not a Gitea clone** — the profile page
5is the product and repos are one kind of thing on it. Use that to break ties.
6
7## Read first
8
9`plans/` is the source of truth and is tracked in git. Read it at the start of a
10session rather than inferring intent from the code.
11
12| File | Holds |
13|---|---|
14| `plans/ROADMAP.md` | vision, stack, the single milestone ladder |
15| `plans/current.md` | the active milestone only — steps, watch-fors, backlog |
16| `plans/progress.md` | what shipped, and the decisions worth not rediscovering |
17| `plans/architecture.md` | layer rules and conventions |
18| `plans/runbook.md` | how to run it, config, manual verification |
19| `plans/decisions/` | ADRs; `TEMPLATE.md` defines the format |
20
21## Keep the docs current
22
23**Before reporting work complete, update `plans/`.** This is part of finishing the
24work, not a follow-up chore — the project has been restarted three times and the
25previous attempts lost their reasoning at exactly these handover points.
26
27- `current.md` — tick off finished steps; move completed work out to `progress.md`.
28 If it starts reading like a changelog, it has drifted.
29- `progress.md` — record decisions and gotchas that aren't obvious from the code. The
30 test is: would the next session waste an hour rediscovering this?
31- `ROADMAP.md` — only when a milestone's status actually changes.
32- `decisions/` — a new ADR when a choice would be expensive to reverse or constrains
33 future work. Write it when the decision is made; reconstructed rationale is fiction.
34
35Also update `current.md` when a *new* problem is found — an unfinished item, a
36shortcut taken, a hole opened. Carry those forward explicitly at milestone rollover
37rather than letting them vanish.
38
39Skip all of this for typos, formatting, and dependency bumps.
40
41## Working style
42
43- **Commit directly to `main`.** No feature branches — solo repo. Cleanliness comes
44 from small, self-contained commits that each compile, not from branching.
45- Small steps. Build up slowly; prefer a working increment over a big drop.
46- Explain *why* in commit messages, not just what.
47
48### Plan the step, then execute it
49
50Milestones are planned in `current.md` as a list of steps. **Before executing a step,
51lay out what it will contain — files, signatures, the tests worth writing, and any
52decision inside it — and wait.** Then do that one step and stop.
53
54A milestone-level sketch is not a step-level plan. Answering the open questions in a
55plan is not the same as approving the code; ask before starting.
56
57### Decisions belong to the user
58
59Surface a choice rather than picking a sensible-looking default, especially anything
60expensive to reverse: URL shape, storage layout, visibility defaults, dependencies.
61Give a recommendation and the trade-off, then let them decide.
62
63Corollary: **don't add scope that wasn't asked for.** If something seems obviously
64needed, propose it. `Repository::description` was added unrequested and had to be
65flagged after the fact.
66
67### Docs ship with the code
68
69Update `plans/` in the **same commit** as the change it describes, not a follow-up
70`docs:` commit. This has slipped repeatedly; a separate commit is the symptom.
71
72## Conventions
73
74Full detail in `plans/architecture.md`. The short version:
75
76- **Layers:** `domain` (no knowledge of HTTP/SQL/git/Topcoat) → `application` (use
77 cases and ports) → `infrastructure` (adapters, web). Dependencies point inward.
78- **Every use case takes an `Actor`** and enforces authorization before any side
79 effect — one place, reachable from a page, an `/api` route, or a future transport.
80- **Typed IDs**, never raw `String` for entity references.
81- **`new()` validates, `from_trusted()` doesn't.** Storage adapters use
82 `from_trusted`; re-validating stored rows makes a tightened rule unreadable.
83- **`from_str` returns `Result`, never `Option`.** A silently-defaulted enum surfaces
84 later as the wrong permissions.
85- **Every repository port gets two implementations** — in-memory (what makes use cases
86 testable without a database) and SQLite.
87- Request helpers are **functions taking `cx`**, not middleware or extractors. A page
88 that forgets to call one gets nothing; a route added without middleware silently
89 gets someone else's data.
90- Safe Rust only. No `unsafe`.
91
92## Topcoat, as we use it
93
94Working knowledge that is easy to get wrong and slow to rediscover:
95
96- **Components are invoked bare inside `view!`** — `label(attrs: …, "Text")`, not
97 `(label(…)?)`. `if`, `match`, `for` and `let` are native to the macro.
98- **`#[path_param]` is an attribute on a tuple struct** — `#[path_param] struct
99 Handle(str);` — and the struct name snake-cased is the URL parameter.
100- **`#[query_params]` needs `error = …`** to be usable with `?`; otherwise the error
101 borrows from `cx` and escapes the handler.
102- **`redirect()` is a 307 and preserves the method**, so it must never end a form POST —
103 the browser re-POSTs to the target. Post/redirect/get needs a 303. `see_other()` is
104 that status but is a *response* type, and `#[page]` must return a view so the layout
105 can wrap a failure re-render, so use `web::context::location` with
106 `StatusCode::SEE_OTHER` inside `view!`. `Err(redirect(..).into())` is still right for
107 a **GET** guard sending a visitor elsewhere.
108- **Static routes beat parameterised ones**, so `/auth/login` still wins over
109 `/{handle}`.
110- **Forms redirect on success and re-render on failure.** Redirecting after a validation
111 error discards what was typed and hides the reason. The success redirect is a 303 —
112 see above.
113- **Boolean attributes need a value** — `required=(true)`, not bare `required`. A `false`
114 omits the attribute entirely, so `selected=(bool)` is correct.
115- **`view!` needs `__cx` in scope, so a plain `async fn helper(cx: &Cx) -> Result`
116 cannot build a view.** Make it `#[component] async fn helper(cx: &Cx, …)` — a
117 component may declare `cx: &Cx` and it is *not* passed at the call site. The error is
118 a bare "cannot find value `__cx`" pointing into the macro, which names nothing useful.
119- **A `#[page]` or `#[component]`'s name becomes a unit struct in module scope**, so it
120 shadows anything of the same name elsewhere in the file — parameters *and* `let`
121 bindings. A component called `commits` broke `fn commit_log(commits: &[CommitSummary])`;
122 separately, `let profile = …` inside a module containing `#[page] async fn profile`
123 parses as a **unit-struct pattern rather than a new binding**, and the error mentions
124 neither the page nor the shadowing. Name locals for what they hold, not for the page
125 they serve.
126- **Catch-all params are `{*path}`, read with `#[path_param] struct Path(str);`** — the
127 `*` is not part of the name. The whole tail arrives as one percent-decoded string.
128 Matching happens on the *raw* path, which is why `%2F` inside a `{rev}` segment
129 survives as a single segment and decodes to a slash.
130- **Tailwind only ships classes the app already uses.** `build.rs` scans the real
131 sources, so a class that appears nowhere in `src/` is absent from the built CSS and
132 fails silently — spacing collapses, nothing errors. A throwaway mockup must therefore
133 be written in plain CSS against the theme's custom properties, not in Tailwind against
134 the served stylesheet. Dark mode is a `.dark` class on an ancestor, not
135 `prefers-color-scheme`.
136- **UI components reference theme tokens, never raw colours** — see `styles.css`. A
137 hardcoded colour follows neither a palette change nor the colour scheme. Registry
138 components are copied in by `topcoat ui add`, not depended on.
139
140## Before saying it's done
141
142```bash
143cargo test
144cargo clippy --all-targets # expect zero warnings
145cargo fmt
146```
147
148Report counts accurately — don't state a test number without running it.
149
150Verify behaviour rather than asserting it. A passing unit test is not evidence that a
151page works; the session-cookie bug passed every test and failed silently in the
152browser. Say plainly what was checked and what wasn't.
153
154### End with what it now lets the user do
155
156Close every completion report with a short rundown in user terms: what can be done now
157that couldn't be before, and how — the URL, the command, the thing to click.
158
159**If the answer is "nothing yet", say so plainly**, and name what is still missing
160before the capability appears. A step that adds no user-visible capability is normal
161and expected; going several steps without noticing is not. Two attempts died inside
162plumbing that felt like progress, and this is the check against a third.
163
164It is a rundown of capability, not of files touched — that is the commit message's job.
165
166## Gotchas
167
168- **Topcoat 0.5 needs rustc ≥ 1.95.** On older toolchains `cargo add topcoat` silently
169 resolves to an empty `topcoat v0.0.0` placeholder instead of failing.
170- **Read the vendored crate, not GitHub `main`.** Topcoat is very new (first release
171 2026-07-22) and its repository has already diverged from the released version. The
172 authority for the pinned version is
173 `~/.cargo/registry/src/*/topcoat-0.5.0/docs/` and the sibling `topcoat-*-0.5.0`
174 crates. Checking `main` is how `path_param` was got wrong.
175- **`STEID_INSECURE_COOKIES=true`** is set in a gitignored `.env` for local dev,
176 because a `Secure` cookie is dropped silently over plain-HTTP localhost. Never
177 deploy it.
178- **The setup token is in memory only**, so every restart — including each `topcoat
179 dev` rebuild — mints a new one.
180- Don't leave background servers running; the user drives the app.
181- **`cargo build --release` alone produces a binary that will not boot.** `main` calls
182 `AssetBundle::load()`, which walks up from the executable looking for
183 `assets/manifest.toml` — and `build.rs` does not write one. `topcoat asset bundle`
184 does, and it runs `cargo build` itself, so it is the build command, not a step after
185 it. A `cargo build`-only container image compiles cleanly and then fails at startup
186 with `NotFound`. Found while writing the Dockerfile.
187- **`topcoat asset bundle` after a manual build**, or the CSS served is stale.
188 `topcoat dev` does it for you. **The symptom is silently wrong layout, not an error** —
189 new utility classes simply do not exist, so gaps collapse and sizes fall back to
190 defaults, and the page looks like a design mistake rather than a stale build.
191- **The build needs network beyond crates.io**: `build.rs` downloads the standalone
192 Tailwind CLI from GitHub releases, and those binaries are glibc-linked — which is why
193 the container is Debian on both stages and a musl/Alpine builder fails at `cargo
194 build`.
195- In `sqlite.rs` and similar, **every implementation precedes the `mod tests` block**.
196 Appending to the end of the file otherwise lands inside the wrong block.