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.tsxduplicates the status/ready pills across its wide and phone branches. - Media queries over
wideternaries where possible. When a difference is purely presentational (sizes, spacing, wrapping, visibility), express it in CSS (@media (min-width: 820px)— keep it in step withWIDE_QUERYinsrc/useWide.ts) rather than branching JSX onuseWide(). Reserve thewideboolean 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.
deriveGroupAreaForfor the crew's search area, gated onshareLocation), 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.
coarseAreastrips 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.rulesis 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 intests/firestore-rules.test.ts, written as the threat it closes.functions/src/logic.tsstays pure — no Firestore, no network, nofirebase-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.tsorchestrates.- 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/importssrc/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.mdfor the reasoning behind each; re-introducing any of them is a regression, not a feature.
Checking your work
npm run buildandnpm 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.