# CLAUDE.md

See @GUIDELINES.md for the full project coding guidelines. All rules there are mandatory.

## Module handbook

When **building or documenting a module/submodule**, first read
`docs/modules_handbook/README.md` and follow its conventions for that module's `readMe.md` —
one folder per module (`<module>/readMe.md`), related modules grouped under a shared parent
folder, the three core sections (What it does / How it works / Related files), and a
**`Reference usage`** section for shared services (e.g. `MediaService`).

## Project catalogue — read the map first

The **project catalogue** (`catalog_projects` and its family: floor plans, media, sources,
developers, the `market_*` reference data) is this codebase's largest shared module. Its
knowledge spans ~30 markdown files across four locations, and it has sub-modules, a second
database, a second migration directory and several retired-but-still-present tables.

**Before touching anything catalogue-related, read
[`docs/modules_handbook/shared/project-catalogue/start-here.md`](docs/modules_handbook/shared/project-catalogue/start-here.md).**
It is the entry point for the whole module and every sub-module: a 60-second model, the traps
that have already cost real time, a routing table from "my task is X" to the two or three files
that answer it, and the full inventory of every catalogue doc. Start there rather than opening
`readMe.md` directly — that file is ~1,100 lines and is one of the map's destinations, not its
front door.

Two rules that apply to every catalogue change:

- **The docs are not a substitute for the code.** Parts of this doc set go stale — a previous
  session changed behaviour and did not update the markdown. Treat a doc as a map: it tells you
  where to look and *why* something is the way it is. Before you assert how something behaves,
  **open the file the doc names and read it.** `start-here.md` says this too, and lists the
  spots known to be wrong today.
- **If you change the catalogue or any of its sub-modules, update the markdown in the same
  change.** Correct the affected file, and when the change introduces something the doc set has
  no home for, **write a new `.md`** beside the others in
  `docs/modules_handbook/shared/project-catalogue/` (handbook convention: extra files live
  beside the module's `readMe.md`; a self-contained sub-module gets its own folder, like
  `vr360/`). Then **add a line for it in `start-here.md`'s inventory and, when relevant, its
  routing table** — a doc that the map does not list is a doc nobody will find.

## Member-facing learning copy

Before writing or editing any screen a MEMBER reads to learn (the DMAIC road, the Glossary
course, 案例复盘 case debriefs, lesson blurbs, AI Coach teaching copy, Area Tutorial scripts), read
[`docs/modules_handbook/main/dmaic-road/content-rules.md`](docs/modules_handbook/main/dmaic-road/content-rules.md)
and run its **first-time reader test** on the screen: every term, letter, name and number must be
answerable from this screen or an earlier one on the same path — define on first use, never
reference forward by acronym, introduce a character before quoting them. The rules came from a
founder review (2026-09-02) of copy that assumed the reader had read the book.

## Using a shared service

Before calling a shared service from a feature, **read its handbook `readMe.md` first** — each
has a *Reference usage* section showing the canonical way to use it, so every consumer stays on
the same patterns instead of re-inventing them:

- **List downloads (Excel / CSV) — the `ExportsResource` + `ExportMenu` PATTERN** (not a service:
  one export class per list, the shared trait names the file, `EscapesCsvFormulas` makes it safe
  to open) → [`docs/modules_handbook/shared/export/readMe.md`](docs/modules_handbook/shared/export/readMe.md)
- **File storage / uploads — `MediaService`** (GCS-backed `Src\Common\Media`; signed URLs,
  collections) → [`docs/modules_handbook/shared/media/readMe.md`](docs/modules_handbook/shared/media/readMe.md)
- **AI calls — `AiClient`** (provider-agnostic chat/prompt, prompt-key registry, `ai_requests`
  logging, the resilient `AiJob` queue lane) → [`docs/modules_handbook/shared/ai/readMe.md`](docs/modules_handbook/shared/ai/readMe.md)
- **White-label AI routing + chargeback — the PropertyLab AI Gateway** (`AI_GATEWAY_MODE=client`
  relays every AI call to the Hub and bills credits; `AI_GATEWAY_HUB_ENABLED` serves it. Touch
  nothing that shows a provider, model or dollar figure for AI without reading this)
  → [`docs/modules_handbook/ai-gateway/readMe.md`](docs/modules_handbook/ai-gateway/readMe.md)
- **SMS — `SmsSender`** (provider-agnostic interface over SMS360; every send costs money, the
  gateway is IP-whitelisted, and it is synchronous — queue anything bulk)
  → [`docs/modules_handbook/shared/sms/readMe.md`](docs/modules_handbook/shared/sms/readMe.md)
- **Voice calls — `VoiceCaller`** (provider-agnostic interface over Twilio Programmable Voice;
  plays a pre-rendered AI-voiced audio, never conversational. Every answered call is billed per
  minute, calls only 9am–9pm, voicemail is hung up on — queue + pace anything bulk)
  → [`docs/modules_handbook/shared/voice/readMe.md`](docs/modules_handbook/shared/voice/readMe.md)
- **AI conversation calls — `ConversationalCaller`** (real-time two-way AI voice agent over
  Retell; the agent listens and talks back — the sibling of `VoiceCaller`, never a variant of
  it. Billed per minute by Retell AND Twilio; the agent must disclose it is an AI (Retell ToS);
  same 9am–9pm window; the CRM keeps its own transcript record in `ai_voice_calls`)
  → [`docs/modules_handbook/shared/voice-agent/readMe.md`](docs/modules_handbook/shared/voice-agent/readMe.md)
- **Email — Mailables + `EmailSender`** (two paths: a Mailable per transactional email via the
  `Mail` facade; the provider-agnostic `Src\Common\Email\EmailSender` interface for automation
  sends — never throws, returns bool) → [`docs/modules_handbook/shared/email/readMe.md`](docs/modules_handbook/shared/email/readMe.md)
- **Delivery credentials — `MessagingCredentialProvider`** (DB-first SMS/SMTP/GetResponse
  credentials, managed on Messages → Settings → Delivery APIs; consumers just read `config(...)`
  — never call the provider directly)
  → [`docs/modules_handbook/shared/messaging-credentials/readMe.md`](docs/modules_handbook/shared/messaging-credentials/readMe.md)
- **Activity trail — `ActivityLogger`** (records what a person DID, shown on Leads → Show;
  one call, never throws at its caller, coalesces repeated edits; NOT `Src\Calendar\Activity`,
  which is a calendar entry) → [`docs/modules_handbook/shared/activity-log/readMe.md`](docs/modules_handbook/shared/activity-log/readMe.md)
- **Webhook forwarding — `WebhookForwarder`** (`Src\Common\Webhooks`; relay a verified
  webhook, byte-identical, to admin-configured targets with a per-delivery ledger —
  a provider gives ONE webhook URL, this is how a second site sees the same events.
  Loop-guarded, https/SSRF-checked, queued, fail-soft)
  → [`docs/modules_handbook/shared/webhook-forwarding/readMe.md`](docs/modules_handbook/shared/webhook-forwarding/readMe.md)
- **Phone alerts to STAFF — `Notifier`** (`Src\Common\Notify`; push an operational message to
  an admin's phone via Telegram. Callers name a registered *event* — never a person or a
  channel; admins pick their own destinations + subscriptions on Manage → Notifications.
  Queued, throttled, fail-soft, every attempt logged. This is for alerting colleagues — customer
  messaging is WhatsApp/SMS) → [`docs/modules_handbook/shared/notify/readMe.md`](docs/modules_handbook/shared/notify/readMe.md)

The **project catalogue** is a shared *module*, not a service, and is large enough to have its
own map — see *Project catalogue — read the map first* above, or go straight to
[`docs/modules_handbook/shared/project-catalogue/start-here.md`](docs/modules_handbook/shared/project-catalogue/start-here.md).

---