# CLAUDE.md Conventions for working in this repo. These are the ones an agent gets wrong without being told; everything else, read from the code. ## Comments **Write a comment only when the information is not obtainable from reading the code.** Then keep it concise. Earns its place: - A constraint with no syntax — a construction order one factory depends on, why an unused `await` is not dead code, why a magic number in an assertion is 3. - A domain fact the file cannot show — a single-account server answering `RepoNotFound` for any other DID. - A current bug or quirk an assertion would otherwise look wrong against. Delete otherwise: - Restating the signature, the type, or the next line. - Announcing what a well-named function does. - Boilerplate repeated across sibling files. - Justifying the design decision that produced the code. That belongs in the commit message. - Meta-framing: "documents a bug rather than endorsing it", "note that…". **Phrase what stays plainly.** Four rules, taken from ASD-STE100 — the parts of it that transfer to code. This is a house style, not a compliance claim; the standard's approved-word list is not applied. - One idea per sentence. Twenty words maximum. - Active voice. Keep the articles. Not "Only guards this isolate" but "This flag applies to one isolate". - No metaphorical verbs. A `catch` *receives* an error; it does not *swallow* it. Code does not *sit*, *live*, *bail*, *wrap*, *punt*, or *massage*. - The same word for the same thing, every time. Not `handler` in one line and `endpoint` in the next. The third rule is the one that earns its place. A metaphorical verb is the most common way a comment reads well and means two things. **Describe the present, never the previous arrangement.** No "previously X", "used to be Y", "the old 24h cap". In a refactor the tell is usually one small word rather than an explicit "previously": | Don't | Do | | --- | --- | | "this delegation **stays** because…" | "the scheduler is reachable here because…" | | "accountApi is **still** bound off the class" | "accountApi binds off the class" | | "the handlers that **remain** methods" | "the handlers that are methods" | | "warms the cache the same as **before the move**" | "the call warms the DID cache" | | "handlers **extracted out of** pds.js get an entry" | "each handler module gets an entry" | Each of those only parses if the reader knows the prior arrangement. Commit messages and changesets are exactly where "what changed relative to before" belongs — that distinction is the point. ## Types No `any` in JSDoc annotations in library or adapter code. Define real typedefs for row and record shapes, or use the core port types (`import('@pdsjs/core/ports').SessionRecord`). For genuinely dynamic values, type as `unknown` and cast at the use site: `/** @type {SomeType} */ (value)`. Test mocks are the exception — casting a mock storage to `/** @type {any} */` on the way into a typed constructor is the established pattern across this suite. Prefer structural types for a handler module's context over indexing off the class (`PersonalDataServer['accountApi']`). An indexed annotation breaks as soon as the method it names moves. ## User-facing copy No em dashes in anything a user sees — UI strings, banners, notices. Split the clauses into separate sentences instead. ## Handler modules The route handlers live in `core/src/handlers/*.js`, following the shape `@pdsjs/spaces` uses: a `createXHandlers(ctx)` factory taking an explicit context and returning a route table fragment, merged into `this.routes` in the `PersonalDataServer` constructor alongside `spaceRoutes`. The class keeps dispatch, authentication, and the commit and blob machinery. `docs/pds-decomposition.md` is the record of how the handlers got there and the mistakes that cost time. Read it before moving code out of `pds.js`. - Name helpers for what they know (`crypto`, `mst`, `scope`), handlers for what they serve (`xrpc-sync`, `account-api`). The `handlers/` prefix avoids colliding with the same-named helper modules in `core/src/`. - Handler modules stay out of the package export map. - Never mix a move with a logic change in one commit. This touches DPoP, client auth and grants; bisectability is the safety net. - Before moving a cluster, check inbound references from **elsewhere in the same file**, not just other packages. `handleApiOverview` called `this.backupsAvailable()` from 1,500 lines away and broke three tests. ## Account interface Two route tables describe `/account`, and they do not match. The React routes are in `packages/account-ui/src/app.jsx`. The `SECTIONS` array in `core/src/account-ui.js` feeds the server-rendered shell, and it names paths the React router does not have. A request for a `SECTIONS`-only path renders the same page as `/account`, with no error. Take the routes from `app.jsx`. ## Checks `npm run ci` is the gate CI runs: `build:ui`, biome, `tsc --noEmit`, then `test:coverage`. CI enforces it on every push, so run the targeted steps below while iterating and the full gate before committing. - `pnpm install`, not `npm install`. The workspace dependencies use the `workspace:*` protocol and npm exits with `EUNSUPPORTEDPROTOCOL`. CI installs with `pnpm install --frozen-lockfile`. The `npm run` scripts work either way. - `npm run build:ui` first on a fresh clone. `@pdsjs/account-ui` exports a gitignored bundle, and without it the node and cloudflare entrypoints fail to resolve. - Two typecheck projects, both run by `npm run typecheck`. `tsconfig.json` covers the server packages against Workers and node types. `tsconfig.ui.json` covers the browser packages' `src/lib` against the DOM, because the Workers types override it and make a `Uint8Array` unassignable to `BodyInit`. `types/vite-env.d.ts` declares the two `import.meta.env` values, since `vite/client` resolves only inside a package that depends on vite. - The `.jsx` files are in neither project. A component's destructured props read as required, so every call site omitting an optional one is an error: 1738 of them, against 136 for the lib modules. Typing the props is the work that would let them in. Until then `noUndeclaredVariables` is what catches a missing import there, which is the failure that reaches the browser. - Coverage floors live in `vitest.config.js`, per area rather than global. A glob key aggregates every file it matches, so give each module its own entry rather than letting a well-covered one carry a bare one. - A minor changeset on a package outside the `fixed` group in `.changeset/config.json` escalates the whole group to a major release. The mechanism: `@pdsjs/node` lists the feature packages in `peerDependencies`, and changesets major-bumps a dependent when a peer moves by minor. `version-packages.js` rejects the escalation, but only at release time. Mark such packages `patch` and give the group its own bump line. A new package's first release needs no changeset at all: set its version directly and `changeset publish` picks up anything not yet on npm. - Changesets may not mix `@pdsjs/account-ui` with public packages: it is private and unversioned, and `changeset version` rejects the mix. Leave it out of frontmatter; describe interface work under the packages that embed the built bundle. - The e2e suite is not part of the gate; it needs docker-compose. Run it with `PLATFORM=node USE_LOCAL_INFRA=false npx vitest run test/e2e.test.js`. Three PLC tests fail without the local infra, which is expected. It binds port 2471, so a `just dev` stack in another workspace will block it.