~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
148 lines · 7.5 KB Raw History
# 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.