Skip to content

Latest commit

 

History

History
172 lines (134 loc) · 8.4 KB

CLAUDE.md

File metadata and controls

172 lines (134 loc) · 8.4 KB

source.coop

Conventions that aren't visible from the code itself. See CONTRIBUTING.md for architecture and the PR process.

Local services and checks

npm run type-check is the quick way to validate a change — it checks types without the dev server's interference, and it is the one to reach for rather than npm run build, which is for shipping or for debugging the build itself. npm run type-check:watch leaves it running.

The dev server, DynamoDB and the Ory tunnel belong to whoever is at the keyboard, and are usually already up. Don't start or restart them, and don't run npm run init-local — it reseeds the local database, and the fixtures it lays down are not the state someone may be in the middle of debugging. When a local lookup comes back empty, the cause is in the code far more often than in the data.

Glossary

When communicating about the system, use these terms:

  • objects (not files) - the system is object storage based, as such data is stored as objects rather than files.
  • requests (not downloads) - the system is built to work with cloud native formats meaning that clients will send many byte requests rather than download the entire object. As such, we shouldn't assume that a request downloads the entire file.

Storybook

Stories are published at ui.source.coop. Run it locally with npm run storybook.

Every UI change lands in Storybook in the same PR: a new component gets a .stories.tsx, a changed one gets its stories updated, a removed one gets its stories removed. A state that can only be reached by clicking through the running app is a state nobody reviews.

Stories render in a browser bundle, so a component that fetches its own data can't have one. ProductSummaryCard is an async server component whose first act is await getPageSession() — cookies and a DynamoDB lookup, neither of which exists in the browser — so no prop you pass can make it render.

Split such a component in two: the server half keeps the async, the session and the permission checks, and passes plain values down; the presentational half is an ordinary function of its props, knows nothing about sessions, and is the half that gets a story. ProductDoi is that half of ProductSummaryCard — the card calls <ProductDoi doi={...} />, and the story renders ProductDoi with any DOI it likes.

A component that imports a server action needs sb.mock() in .storybook/preview.tsx, or its story fails with __filename is not defined.

Show responsive behavior by pinning a viewport, never by wrapping a story in a fixed-width box. A box exactly as wide as the frame has none of the page padding the real layout has, so it invents overflow the page doesn't have.

The JSDoc above meta, and above each story export, is published prose on ui.source.coop — not a code comment. Write it for a reader.

Account names

Wherever the UI names an account, show it with its card on hover: DisplayNameLink or AvatarLinkCompact, or AccountInfoHoverCard around anything else. A tooltip can't hold the card, so keep names out of tooltips. Look up every account a page names in one fetchManyByIds call, pass the browser its publicView, and show an id that doesn't resolve as plain text.

Docs and ADRs

Two repositories outside this one hold prose that a change here can quietly invalidate. Check both while the change is still in your head — whoever finds a stale page a month from now has no idea which PR made it stale.

docs.source.coop is the user-facing documentation. A change to a flow a user walks through — creating an account or a product, uploading data, bringing your own bucket — dates the page in docs/using-source/ that describes it, and a change to how the platform is put together dates docs/about-source/.

data.source.coop is the data proxy, and its adrs/ directory records the decisions this app shares with it: authorization, STS credentials, API keys, outbound federation. An ADR records a decision, not the code that implements it, so a change that merely implements one leaves it alone. A change that moves the decision needs a new ADR, or an amendment to the one it supersedes, in that repository.

public/llms.txt is the same documentation written for AI agents, and it lives in this repository, so it ships in the same PR as the change that dates it. A new feature an agent could use — a public endpoint, a new way to read or publish data, a new docs page worth pointing at — gets a line there, and a renamed docs slug, moved endpoint or removed feature gets its line fixed. Every URL in the file should resolve in production without auth.

The PR description says which way it went: name the doc, ADR or llms.txt line the change affects and link the PR carrying that update, or name the ones you checked and say why they still hold. "Checked the data-proxy ADRs; 005 still describes the model" is worth writing, because silence reads the same whether the docs were checked or forgotten.

API

Read API.md before adding or changing an endpoint under /api/v1. It covers the operations layer every route goes through, how a route documents itself, the shapes of lists, errors and methods, and which consumers a change has to stay compatible with.

Every error the API answers is an RFC 9457 problem, served as application/problem+json and built from the table in src/lib/api/problems.ts. Answer one through toResponse or problemResponse, never a hand-built error body. API.md's "Errors" says why.

New functionality ships with its /api/v1 endpoint in the same PR, and an issue that plans it names the endpoint. A feature only the UI can reach is one that scripts, the CLI and agents can't use. The UI and the endpoint go through one operation, so the two can't disagree about a rule — see API.md's "One operation, thin adapters".

Pull requests

A PR that touches the UI links the stories it affects, on that branch's Storybook deploy:

https://<deploy-host>/?path=/story/<story-id>

Vercel caps a hostname label at 63 characters, so a long branch name is cut short and given a hash (source-coop-ui-git-feat-github-immutable-re-82950d-radiantearth.vercel.app), and a hostname pasted together from the full branch name doesn't resolve. The deploy doesn't exist yet when the description is first written, so compute the host from the branch:

b=$(git branch --show-current) p=source-coop-ui t=radiantearth
l="$p-git-$(printf %s "$b" | tr -c 'a-zA-Z0-9' - | tr A-Z a-z)-$t"
[ ${#l} -gt 63 ] && l="${l:0:$((55 - ${#t}))}-$(printf %s "git-$b$p" | shasum -a 256 | cut -c1-6)-$t"
echo "https://$l.vercel.app"

Once the PR is open, the source-coop-ui row of the Vercel bot's comment (gh pr view <n> --comments) has the host Vercel actually assigned; check the links against it.

Link the specific stories rather than the root, and include a screenshot of anything visual — it shows the change in the review itself, and it outlives the branch deployment, which goes away with the branch. (Branch deployments ask for a Vercel login; ui.source.coop is public.)

The title and description describe the branch as it stands, not as it stood when the PR was opened. After pushing to a branch with an open PR, re-read both: if the approach has moved on, rewrite them; if they still hold, say so rather than leaving it unsaid. A description is read as the spec, so a stale one sends a reviewer looking for code that isn't there. Two parts rot first — a Testing section asserts checks that may predate the current commits, and a title has to keep earning its Conventional Commits type once the work changes shape.

Write PR bodies, issue bodies and commit message bodies as unwrapped paragraphs — one long line per paragraph, no hard line breaks. GitHub wraps them to the reader's window; hard breaks inserted at some other window's width wrap twice and read ragged. The commit subject line is the exception and stays short, because nothing wraps it. (This file and the rest of the repo's Markdown still wrap at 80.)

Comments

A comment says why the code is the way it is, in the present tense. What it used to do, what broke, and what a change fixed belong in the commit message and the PR description — that is where someone goes looking for history, and it stays accurate there as the code moves on. This matters twice over in story JSDoc, which is published.