Skip to content

INFO

This page is CLAUDE.md from the repository, as it stands.

Working on it ​

Working notes for anyone — human or model — changing this repository. The guide to using the app is at docs.planet4.us; this file and its siblings are published there too, under Developers, as they stand.

The documents, and the rule that keeps them honest ​

  • README.md — how to run, configure and deploy it.
  • ARCHITECTURE.md — what exists, in the present tense. Any change to the Firestore schema, the security rules, a Cloud Function's contract, or the assistant pipeline updates it in the same commit. Documentation drift is a bug.
  • DECISIONS.md — why it is the way it is, one entry per non-obvious call. Add an entry when you make one; when you reverse one, mark the old entry superseded rather than deleting it — the reasoning is the part that isn't recoverable from the code.
  • BACKLOG.md — ideas not committed to. An entry records the intent, the prior art already in the codebase, and the open questions.

The user guide (docs/guide/*.md) is the same claim made to a person: a change to what a person can see or do — a screen, a control, a limit — updates the guide in the same commit. npm run docs:dev shows the page while you write it. The Developers pages under docs/developers/ include the four documents above from the repository root and never need editing themselves.

Code style — going forward ​

Prefer proper, maintainable front-end techniques over expedient forks:

  • Reusable components over duplicated JSX. When phone and wide layouts share content (pills, headers, action rows), extract a small component and render it in both branches — don't paste the markup twice. Existing duplication hotspot: the plan-banner fork in src/screens/Chat.tsx duplicates the status/ready pills across its wide and phone branches.
  • Media queries over wide ternaries where possible. When a difference is purely presentational (sizes, spacing, wrapping, visibility), express it in CSS (@media (min-width: 820px) — keep it in step with WIDE_QUERY in src/useWide.ts) rather than branching JSX on useWide(). Reserve the wide boolean for genuinely structural differences (pane vs sheet, different component trees).
  • Clean-code hygiene. Factor repeated inline style strings into shared constants/helpers when used 3+ times; keep design values on the CSS tokens in src/index.css (never re-hardcode hex values); prefer deleting a fork over growing it.

These are preferences, not a mandate to refactor unrelated code mid-task — but new work should follow them, and touched code should be nudged toward them.

Privacy — bias toward it, always ​

Prioritize members' personal privacy over convenience, and default to the private option when in doubt:

  • Private profile data stays opt-in. A feature that needs a member's private information (home location, work, travel limits, memory notes, dietary or other personal preferences) must say so plainly in the UI and give that member an explicit opt in / opt out that honors their choice everywhere the data could surface — the prompt, other members' screens, persisted hangout fields, emails. Don't infer consent from the data being present in their profile: a saved home is not permission to use it.
  • One gate, applied everywhere. Consent checks live in one shared helper (e.g. deriveGroupAreaFor for the crew's search area, gated on shareLocation), never re-derived per call site — inconsistent gates are how an un-consented value leaks through the one path that forgot.
  • Coarsening is not consent. A rounded, aggregated or coarsened value derived from private data still needs the opt-in; it only limits the blast radius once consent exists (e.g. coarseArea strips street addresses from a shared home before it enters a prompt or a member-visible field).
  • Call it out in code and in the summary. When touching a feature that consumes private data, document the gate at the helper, and when a task would loosen or bypass one, flag it to the user instead of doing it — even if the request seems to ask for it.

Boundaries to keep ​

  • firestore.rules is the authorization boundary; client gates are UX. Every write the Admin SDK makes from a function is one the rules deliberately deny clients — if a new feature needs a client to write something the rules forbid, the answer is usually a callable, not a looser rule. A rules change comes with a scenario in tests/firestore-rules.test.ts, written as the threat it closes.
  • functions/src/logic.ts stays pure — no Firestore, no network, no firebase-admin — which is what makes it unit-testable. Policy (what a push says, how a card mutates, what the model may request) goes there; functions/src/index.ts orchestrates.
  • Mirrors stay in step. Some constants are enforced in the rules and both runtimes and must match: the member display-name rule, the photo-URL rule (validPhotoUrl / safePhotoUrl ×2), the reaction palette, the widget kinds, the avatar palette (src/types.ts, functions/src/membership.ts, scripts/seed.mjs), the assistant's name (src/config.ts, functions/src/anthropic.ts). Each site names its twins in a comment; change all of them or none.
  • Nothing outside src/screens/admin/ imports src/adminData.ts, or the lazy admin chunk silently collapses into the main bundle.
  • The assistant never locks the plan, never speaks unprompted, and never sees what a member didn't share. See DECISIONS.md for the reasoning behind each; re-introducing any of them is a regression, not a feature.

Checking your work ​

  • npm run build and npm run build:functions — the two typechecks; npm run lint.
  • npm test — the pure server logic (Vitest, no network). A bug in policy code becomes a fixture here.
  • npm run test:rules — the security rules against the Firestore emulator.
  • npm run docs:build — the docs site, which also fails on a dead link in any included document.
  • End to end: .claude/skills/verify/SKILL.md — build, start the emulators, seed, and drive the app with Playwright. There is no CI; run these before pushing.