docs: remove the second contributing note and its dead pointers

CONTRIBUTING-AI.md pointed contributors at a CLAUDE.md "checked in and
kept current"; that file is not in the repository, so the pointer and
the twenty references to it in folder READMEs, docs and comments led
nowhere. The note goes; its one lasting rule (whoever commits a change
stands behind it, no real names, no live health figures, no secrets)
moves into CONTRIBUTING.md. The folder READMEs keep their own content
and drop the dead link, and a reference to the unpublished planning
folder goes with it.

The checked-in .mcp.json was a local editor config no workflow reads;
it is untracked and ignored now. The translation tiers are described
as machine-translated first drafts reviewed by the community, and the
list names Russian, which it missed.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:24:16 +01:00
1 parent 8f6081de89
commit 4a40a63656
23 files changed
+26 -60

No files matched your search

-1
View File
@@ -82,7 +82,6 @@ README.md
CHANGELOG.md
CLAUDE.md
CONTRIBUTING.md
CONTRIBUTING-AI.md
CODE_OF_CONDUCT.md
SECURITY.md
# LICENSE / LICENSE.md are deliberately NOT ignored: the PolyForm
+3 -4
View File
@@ -72,11 +72,10 @@ next-env.d.ts
.cursor/
.codex/
.aider*
.mcp.json
# Planning scratch — all internal per-release reports, QA findings, ad-hoc
# backlog, and the iOS coordination channel. These are internal working
# notes; everything user-facing lives in the public surfaces (README,
# CHANGELOG, docs/). Kept on disk for the working session, never tracked.
# Internal working notes (per-release reports, backlog, iOS coordination).
# Everything user-facing lives in README, CHANGELOG and docs/.
/.planning/
# Docs scratch — temporary spec drafts that live in the repo only
-8
View File
@@ -1,8 +0,0 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["-y", "shadcn@latest", "mcp"]
}
}
}
-16
View File
@@ -1,16 +0,0 @@
# Contributing with AI tooling
HealthLog is built with AI assistance, and contributions made the same way are welcome. This file used to carry a copy of the internal conventions document; the copy aged badly and started contradicting the codebase, so it now points at the living sources instead.
## Where the real conventions live
- `CLAUDE.md` at the repo root is the maintainer's operating manual for this codebase: architecture walk, code conventions, security patterns, test commands, the do-not list. It is checked in and kept current; whatever an AI agent (or a human) needs to work on the server is in there.
- `docs/CONTRIBUTING-CLIENTS.md` covers building a client app (Android, iOS, CLI) against the server: the API contract, auth from a client's point of view, sync semantics, and how coordination between repos works.
- `docs/api/openapi.yaml` is the wire contract, generated from the server's Zod schemas. CI fails on drift, so it is always accurate for the commit it sits in.
## Ground rules, whatever tooling you use
- The human who commits a change has read it and stands behind it. The git history knows authors, not tools: no AI attribution trailers, no assistant vocabulary in commit messages, PR text, or release notes.
- No real names of users or reporters in committed text, and no live health figures anywhere, including fixtures and examples. Invent data.
- No secrets in code, fixtures, or docs, and nothing secret-shaped either.
- Run the gates before pushing (`pnpm typecheck`, `pnpm lint`, `pnpm test`) and fix what they find rather than suppressing it. A guard that fails is telling you something; the fix goes in the code, not in the guard.
+4 -4
View File
@@ -76,7 +76,7 @@ pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build
- **shadcn/ui** components (new-york style) for UI primitives.
- **Dracula theme** -- dark mode is the default; a `light`/`system` toggle is exposed in the topbar. Chart and accent colors use the `--dracula-*` CSS tokens so both themes stay coherent.
See `CONTRIBUTING-AI.md` for detailed architecture and patterns.
Whoever commits a change has read it and stands behind it. Committed text names no real users or reporters and carries no live health figures; fixtures and examples use invented data. Nothing secret, and nothing secret-shaped, goes into code, fixtures or docs.
## Database Changes
@@ -121,7 +121,7 @@ Contributors: branch off `main`; PRs target `main`.
## Translations
HealthLog ships with English (`en`) and German (`de`) maintained by the project, plus French (`fr`), Spanish (`es`), Italian (`it`), Polish (`pl`), Korean (`ko`), Dutch (`nl`), and Brazilian Portuguese (`pt-BR`) that are **AI-initial, community-maintained**. The maintained locales are reviewed line-by-line; the community-maintained ones surface a small banner inviting users to improve them.
HealthLog ships with English (`en`) and German (`de`) maintained by the project, plus French (`fr`), Spanish (`es`), Italian (`it`), Polish (`pl`), Korean (`ko`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`) and Russian (`ru`), which started as machine-translated first drafts and are reviewed by the community. The maintained locales are reviewed line by line; the community-reviewed ones show a small banner inviting users to improve them.
### Improving an existing locale
@@ -168,9 +168,9 @@ To propose a validated version for a language that has none, or a better source
If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`):
1. Open an issue first using the **Translation request** template. We'll confirm the locale is in scope and decide whether it joins the AI-initial-community-maintained tier or the maintained tier.
1. Open an issue first using the **Translation request** template. We'll confirm the locale is in scope and decide whether it joins the community-reviewed tier or the maintained tier.
2. Copy `messages/en.json` to `messages/<locale>.json`. Questionnaire text is not in it; the new language needs a wording file per instrument instead (see Questionnaire wording above), which step 4 makes impossible to forget.
3. Translate. AI assistance is welcome as a starting point but the PR description should note whether it's an AI-initial pass or a fully human-reviewed one — that information drives the banner state.
3. Translate. A machine translation is fine as a starting point, but the PR description should say whether the pass is machine-translated or fully reviewed by someone who reads the language; that decides the banner state.
4. Add the locale to `locales` in `src/lib/i18n/config.ts` and to `locales` in `project.inlang/settings.json`, then run `pnpm typecheck`. Every per-locale table that still lacks the new locale fails to compile, and that list is the work: date and number formatting, the reply-language footers of the Coach and insights prompts, the outbound safety banks, the prose-grounding patterns, the no-key fallback copy and the questionnaire wording registry. Include those in the same PR.
5. Run `pnpm test` before submitting. The i18n guards hold the new bundle to the English keys and placeholders.
+2 -2
View File
@@ -1,6 +1,6 @@
# Building a native client against HealthLog
Working notes for anyone building a client app (Android, iOS, CLI, anything else) against the HealthLog server, with or without AI tooling. Everything in here is a rule we actually follow between the server and the iOS app, written down so a second client does not have to rediscover it. Pair it with `CLAUDE.md` (server-side conventions) and `docs/api/openapi.yaml` (the wire contract).
Working notes for anyone building a client app (Android, iOS, CLI, anything else) against the HealthLog server. Everything in here is a rule we actually follow between the server and the iOS app, written down so a second client does not have to rediscover it. Pair it with [`CONTRIBUTING.md`](../CONTRIBUTING.md) (server-side conventions) and `docs/api/openapi.yaml` (the wire contract).
## The contract, and where truth lives
@@ -43,7 +43,7 @@ The corollary: if a resolved value you need is missing from a payload, the fix i
## Ground rules for the repo itself
The voice-and-privacy rules from `CLAUDE.md` apply to any repo under the project, including a client repo: commit messages and public text carry no tooling attribution and no assistant vocabulary, no real names of users or reporters, and no live health figures, ever. Fixtures use invented data. If an AI tool wrote part of a change, the human who commits it has read it and stands behind it; the git history knows only authors.
The privacy rules apply to any repo under the project, including a client repo: commit messages and public text carry no real names of users or reporters and no live health figures, ever. Fixtures use invented data. Whoever commits a change has read it and stands behind it.
Secrets never appear in code, fixtures, or examples, not even placeholder-shaped real ones. Anything user-identifying stays out of logs on the client just as it does on the server.
+1 -1
View File
@@ -21,4 +21,4 @@ The Playwright config (`../playwright.config.ts`) builds the app (`next build`)
Import `test` and `expect` from `./setup/test`, never from `@playwright/test` — types (`Page`, `Route`, …) still come from the package. The entry point extends Playwright's `test` with an automatic fixture that drops route handlers and page listeners after every test, so a handler left in flight cannot fail the test it belonged to once the context closes. `src/__tests__/e2e-route-cleanup-guard.test.ts` enforces the import boundary.
Assert against stable `data-*` attributes (`data-slot`, `data-display-step`, root display attrs), not viewport-dependent visible text. Responsive `sm:hidden` UI changes break `getByText` on desktop while the underlying element is still present — anchoring on data attributes keeps the specs stable across breakpoints. See [`../CLAUDE.md`](../CLAUDE.md).
Assert against stable `data-*` attributes (`data-slot`, `data-display-step`, root display attrs), not viewport-dependent visible text. Responsive `sm:hidden` UI changes break `getByText` on desktop while the underlying element is still present — anchoring on data attributes keeps the specs stable across breakpoints.
+1 -7
View File
@@ -16,13 +16,7 @@
# Per project policy (`--no-verify` is banned), prefer fixing the
# finding over bypassing the hook.
# Commit-message guard: the project voice rule (CLAUDE.md, "Voice and
# privacy") states that commits are authored by the maintainer's git
# identity alone and carry no tooling co-author trailer or session link.
# Editors and CLIs add those trailers by default, and a message that
# picks one up is only removable from the object store by a history
# rewrite — so the check belongs here, before the commit exists, not in
# a review pass afterwards.
# Reject tool attribution trailers.
commit-msg:
commands:
no-tooling-trailer:
+1 -1
View File
@@ -23,6 +23,6 @@ Number a new migration with the next free prefix. The production entrypoint runs
- Encryption is fail-closed: `*Encrypted` columns are written through `@/lib/crypto` (AES-256-GCM, versioned key ids); never store plaintext in an encrypted column. Key rotation is `scripts/rotate-encryption-key.ts` + `docs/ops/encryption-key-rotation.md`.
- The generated client under `src/generated/prisma` is build output — do not edit it.
- Schema-touching changes that affect the API surface must regenerate `docs/api/openapi.yaml` (see [`../CLAUDE.md`](../CLAUDE.md)).
- Schema-touching changes that affect the API surface must regenerate `docs/api/openapi.yaml` (`pnpm openapi:generate`; CI fails on drift).
Related: [`../src/lib/README.md`](../src/lib/README.md) (`db.ts`, `rollups/`), [`../docs/ops/`](../docs/ops/) (migrations + backup runbooks).
+1 -1
View File
@@ -42,4 +42,4 @@ typecheck.
## Conventions
See [`../CLAUDE.md`](../CLAUDE.md) "Tests and commands" + "Self-hosting gotchas". Operator runbooks that drive these scripts live in [`../docs/ops/`](../docs/ops/).
Operator runbooks that drive these scripts live in [`../docs/ops/`](../docs/ops/).
+1 -1
View File
@@ -23,4 +23,4 @@ client → src/proxy.ts → src/app/<route>/route.ts → src/lib/api-handler.ts
## Conventions
See [`../CLAUDE.md`](../CLAUDE.md) for the full set. The load-bearing ones: RSC by default (`"use client"` only when a hook/state/browser API needs it); every API route wraps `apiHandler` and parses its body with Zod `safeParse`; `userId` is always narrowed from session or Bearer, never accepted as a body field; TanStack Query keys come only from `src/lib/query-keys/`; outbound HTTP goes through `src/lib/safe-fetch.ts`.
The load-bearing ones: RSC by default (`"use client"` only when a hook/state/browser API needs it); every API route wraps `apiHandler` and parses its body with Zod `safeParse`; `userId` is always narrowed from session or Bearer, never accepted as a body field; TanStack Query keys come only from `src/lib/query-keys/`; outbound HTTP goes through `src/lib/safe-fetch.ts`.
@@ -22,8 +22,7 @@
* that file does, or is listed below by file and function with the reason
* its rows cannot reach a prompt. A new reader fails here until it does one
* of those. The data routes are not in scope: a module decides what a model
* is given, not what the owner can read (`CLAUDE.md`, "An AI capability
* gates model work").
* is given, not what the owner can read.
*
* The check is per read, not per file. It used to ask whether a file that
* reads the table mentions the gate anywhere, so one ungated read in a file
+1 -1
View File
@@ -43,7 +43,7 @@ const SRC = join(process.cwd(), "src");
* Every non-test `.ts` / `.tsx` under `src/app/`, which is the whole of the
* request-handling tree: API routes, RSC pages, layouts, route handlers.
* The generated Prisma client is excluded by construction (it is not under
* `app/`) and never read — see CLAUDE.md.
* `app/`) and never read.
*/
function appFiles(): string[] {
return walkSourceFiles(SRC, { floor: 3000 })
+1 -1
View File
@@ -161,7 +161,7 @@ const BARE_REQUIRE_AUTH_FLOOR = 200;
* omits the three routes under `src/app/.well-known/` — three request handlers
* that a glob-based sweep reports as if they did not exist. Leg (a) pins one of
* them by name so this cannot quietly regress. Names only; nothing under
* `generated/` is ever read (CLAUDE.md).
* `generated/` is ever read.
*/
function allPaths(): string[] {
return readdirSync(SRC, { recursive: true })
+1 -1
View File
@@ -22,4 +22,4 @@ Every endpoint wraps its handler in `apiHandler` (`@/lib/api-handler`), parses a
## Conventions
See [`../../CLAUDE.md`](../../CLAUDE.md). The request-flow walk in `.planning/codebase/arch.md` annotates each layer with file:line citations. Related dirs: handlers compose [`../lib/`](../lib/README.md); pages render [`../components/`](../components/README.md).
Related dirs: handlers compose [`../lib/`](../lib/README.md); pages render [`../components/`](../components/README.md).
+1 -1
View File
@@ -21,4 +21,4 @@ React components. The UI primitives are shadcn/ui (new-york style, zinc base) ov
## Conventions
Kebab-case filenames; React component exports stay PascalCase regardless. (A handful of PascalCase-filename outliers under `medications/` + `onboarding/` are pre-existing drift, noted in [`../../CLAUDE.md`](../../CLAUDE.md).) Every `t()` call needs a matching key in `messages/en.json` — guarded by `i18n-call-site-coverage.test.ts`. Data reads go through TanStack Query with keys from `@/lib/query-keys` and unwrap `(await res.json()).data`. Tests live in [`__tests__/`](__tests__/).
Kebab-case filenames; React component exports stay PascalCase regardless. Every `t()` call needs a matching key in `messages/en.json` — guarded by `i18n-call-site-coverage.test.ts`. Data reads go through TanStack Query with keys from `@/lib/query-keys` and unwrap `(await res.json()).data`. Tests live in [`__tests__/`](__tests__/).
+1 -1
View File
@@ -30,4 +30,4 @@ The server and runtime toolkit. Everything that is not a page, component, or rou
## Conventions
Kebab-case filenames. See [`../../CLAUDE.md`](../../CLAUDE.md) "Code conventions" and "Security-relevant patterns"; the critical-files map there cites the exact entry points. Most modules carry tests in a sibling `__tests__/`.
Kebab-case filenames. Most modules carry tests in a sibling `__tests__/`.
+1 -1
View File
@@ -46,7 +46,7 @@
* provider-less account costs one cheap chain-resolve and no LLM call.
*
* Recurring pg-boss task — never runs inside an HTTP request and never
* shells out to `tsx` (CLAUDE.md DO-NOTs).
* shells out to `tsx` (the production image has none).
*/
import { activeAccount } from "@/lib/account-deletion/active-account";
import { createHash, randomUUID } from "node:crypto";
+1 -1
View File
@@ -16,7 +16,7 @@
* polls the read-only GET until it lands.
*
* Recurring/event pg-boss task — never runs inside an HTTP request and
* never shells out to `tsx` (CLAUDE.md DO-NOTs). Registered in the
* never shells out to `tsx` (the production image has none). Registered in the
* reminder-worker `allQueues` loop + a `boss.work` handler; a queue built
* but never registered silently never drains (the v1.4.37 W10 incident),
* so a guard test pins the registration.
+1 -1
View File
@@ -17,7 +17,7 @@
* so a provider-less / sparse account costs only the context build.
*
* Recurring pg-boss task — never runs inside an HTTP request, never shells out
* to `tsx` (CLAUDE.md DO-NOTs).
* to `tsx` (the production image has none).
*/
import { ACTIVE_ACCOUNT } from "@/lib/account-deletion/active-account";
import type { PrismaClient } from "@/generated/prisma/client";
+1 -1
View File
@@ -150,7 +150,7 @@ export function redactSecrets(input: string): string {
// calendar-feed link (#1350).
// Same boundary-aware shape as the `sk-` rule. The
// idempotency replay-cache already rejects bodies containing
// these prefixes (CLAUDE.md headless-client-API patterns); this
// these prefixes; this
// is the matching egress guard for log/error surfaces.
.replace(/(^|[^A-Za-z0-9])hl[kpresvf]_[A-Fa-f0-9]{32,}/g, "$1[REDACTED]")
// v1.22.0 — the MCP OAuth bridge artifacts: the authorization code
+1 -1
View File
@@ -10,7 +10,7 @@
* both directions.
*
* Everything is built field by field from the parsed body. Nothing is spread
* from a request (`CLAUDE.md`, no mass assignment), and the two multi-select
* from a request (no mass assignment), and the two multi-select
* lists go through the vocabulary module's encoder rather than a local
* `JSON.stringify`.
*/
+1 -2
View File
@@ -3,8 +3,7 @@
* (PHQ-9 / GAD-7; v1.27.9 adds WHO-5 + SCI). The item answers are small
* integers whose ceiling is instrument-specific (PHQ/GAD 0–3, SCI 0–4,
* WHO-5 0–5); the array length must match the instrument. `userId` is NEVER
* a body field — it is narrowed from the session in the handler (CLAUDE.md
* convention).
* a body field — it is narrowed from the session in the handler.
*/
import { z } from "zod/v4";
import {