Files
lucky-cat/AGENTS.md
T

222 lines
18 KiB
Markdown

# Repository Guidelines
## Project Structure & Layering
- `components/platform/{include,internal,src}`: hardware/system abstraction only.
- `components/domain/{include,internal,src}`: business-domain services.
- `components/control_plane/{include,internal,src}`: lifecycle, policy, health, retry/backoff orchestration.
- `components/app_composition/{include,internal,src}`: composition boundary; `app_main` only wires modules.
- `dependency_whitelist.md`: approved exceptions for unavoidable lateral dependencies.
- `components/domain/third_party/`, `components/domain/assets/fonts/`, `tools/`: vendored code, embedded assets, utility scripts.
- `build/`: generated artifacts only (ignored).
## Mandatory Architecture Constraints
1. Dependencies are one-way only: `app_composition -> control_plane -> domain -> platform`.
2. Lateral direct dependencies are forbidden unless listed in `dependency_whitelist.md`.
3. Cross-domain communication must use events or Port interfaces; never include another domain's `internal/*.h`.
4. Each component keeps exactly one public facade header in `include/`; private headers stay in `internal/` via `PRIV_INCLUDE_DIRS`.
- Long-term canonical facade headers in this repo:
- `platform/include/platform.h`
- `domain/include/domain.h`
- `control_plane/include/control_plane.h`
- `app_composition/include/app_composition.h`
5. Upper layers may include only immediate downstream public facade headers; leapfrog includes across multiple layers are forbidden.
6. Cross-cutting contracts must have a single declared owner layer and one public declaration point; duplicated declarations across layers are forbidden.
7. Runtime state must have a single source of truth (SSOT); orchestration layers should query/subscribe rather than duplicate lower-layer lifecycle or readiness state.
8. Blocking APIs must define timeout and idempotent semantics (no destructive background continuation after caller timeout).
9. Facade contracts for commands/events must use strongly typed enums/structs; primitive `int`/`uint` magic values across facade boundaries are forbidden.
10. Non-owner facades must not expose downstream-layer public types directly (for example, `typedef platform_*` in `domain/include`); bridge/mapping belongs in implementation adapters.
## Contract Ownership and Declaration
- Each cross-cutting contract family (shared structs/enums/callback signatures/events) must have exactly one owner layer and one public declaration point.
- Non-owner layers must not redefine semantically equivalent contracts under new names.
- Cross-layer reuse should prefer owner types at owner boundaries, but non-owner facades must not leak downstream types; use explicit adapter mapping at implementation boundaries.
- Owner changes require an explicit architecture decision record and a staged migration plan.
## Canonical Contract Ownership Matrix (Normative)
The following matrix is authoritative for all new and refactored cross-layer contracts in this repository.
| Contract family | Owner layer | Single declaration point | Non-owner usage rule |
| --- | --- | --- | --- |
| Direction catalog, angle math, direction lookup (`domain_direction_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` consumes via `domain` facade only; do not duplicate equivalent structs/enums in upper facades. |
| Direction calibration policy/state model (calibration angle, sweep, search policy) | `domain` | `components/domain/include/domain.h` | Hardware wiring details must be bridged in implementation adapters, not copied into upper-layer public facades. |
| Connectivity semantic prompt IDs/events (connecting/success/fail/config) | `domain` | `components/domain/include/domain.h` | `platform` must not expose duplicated semantic enums in public facade; map semantics in adapters. |
| Mini-program Wi-Fi provisioning semantics (TalkingQ BLE discovery/GATT schema, credential events, status/device-info payloads) | `domain` | `components/domain/include/domain.h` | `platform` exposes generic BLE GATT server primitives only; `control_plane` owns Wi-Fi connection policy and worker lifecycle. |
| Hardware/runtime primitive descriptors (GPIO digital input config, low-level actuator primitives, storage identity/blob KV APIs, wall-clock sync API) | `platform` | `components/platform/include/platform.h` | Upper layers call through owner facade or owner-backed adapters; no semantic redefinition in non-owner public headers. |
| Battery sensing and battery-state policy (`platform_battery_*`, `domain_battery_*`) | `domain` for SOC/level semantics; `platform` for ADC, charger GPIO, and load-quality primitives | `components/domain/include/domain.h` for semantic state; `components/platform/include/platform.h` for hardware samples | `control_plane` consumes domain status only; platform load tracking stays internal and upper layers must not infer SOC from raw ADC values. |
| LAN HTTP transport contract (bounded request/response structs, handler delegate, server lifecycle/state) | `platform` | `components/platform/include/platform.h` | `domain` owns route semantics and maps them in implementation; upper layers must not expose platform HTTP types. |
| Multimodal session transport contract exposed to orchestration (connected/disconnected/error/text/binary event family, audio uplink callback family) | `domain` | `components/domain/include/domain.h` | `platform` internal transport types stay internal; cross-layer public contract is declared once at `domain` facade. |
| Device-auth persistence port (opaque auth key/state record slots and legacy migration targets) | `domain` | `components/domain/include/domain.h` | `control_plane` owns serialized auth record schemas and consumes typed auth slots through `domain`; `platform` remains an opaque blob KV owner only. |
| User birth profile and daily fortune result (`domain_birth_profile_*`, `domain_fortune_result_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` consumes and persists these contracts through the domain facade; platform storage remains opaque and semantics-free. |
| LAN user configuration web application contract (`domain_user_config_server_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` owns connectivity-driven start/stop policy; page routes and user/fortune JSON remain domain-owned. |
| LCD expression semantic catalog and trigger request contract (`domain_expression_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` requests semantic expressions through `domain`; `platform` exposes playback primitives only and must not duplicate semantic expression IDs publicly. |
| LCD presentation playback primitives (asset sequence/slot reels/idle playback request) | `platform` | `components/platform/include/platform.h` | Upper layers must use `domain` expression mapping; playback asset directories and procedural presentation details are bridged in implementation adapters. |
| Connectivity orchestration policy contract (bootstrap flow, retry/backoff/fallback policy APIs) | `control_plane` | `components/control_plane/include/control_plane.h` | `app_composition` triggers these policies but must not implement policy logic. |
| Daily fortune fetch/apply orchestration policy (`control_plane_fortune_*`) | `control_plane` | `components/control_plane/include/control_plane.h` | `app_composition` starts control-plane workers only; birth-profile storage, direction semantics, and transport primitives remain in their owner layers. |
| Model-command and multimodal runtime orchestration contract (model command type/callback, runtime state snapshot) | `control_plane` | `components/control_plane/include/control_plane.h` | Non-owner layers subscribe/query through `control_plane` APIs; avoid duplicated runtime state mirrors. |
| App startup wiring contract (boot profile, startup sequence entrypoints) | `app_composition` | `components/app_composition/include/app_composition.h` | `main/app_main` calls composition entrypoints only; policy and workers must remain below composition. |
Additional enforcement rules:
- Any new cross-layer contract must map to one matrix row above before code is merged.
- If a new contract family does not fit existing rows, add an ADR first, then update this matrix in the same PR.
- Public facade headers must not `typedef` downstream-layer public types; use implementation-layer mapping/adapters instead.
## LCD Expression Management
- Use `docs/lcd-expressions.md` as the operational guide for managing, requesting, and adding LCD expressions.
- New expression semantics must be declared as `domain_expression_*` contracts in `components/domain/include/domain.h` and mapped in the domain runtime adapter catalog.
- Upper layers must request expressions through `domain_runtime_request_expression()`; they must not call `platform_lcd_*` playback APIs directly.
- Platform LCD code must expose playback primitives only and must not reference `DOMAIN_EXPRESSION_*` semantic IDs.
- Asset-backed expressions should use the platform-owned direct LCD asset path documented in `docs/lcd-expressions.md`; custom playback order belongs in the domain catalog, not in duplicated asset files or control-plane logic.
- Do not add one-off expression APIs for each emotion/state unless an ADR explicitly justifies a new contract family.
## Concurrency and Callback Safety
- Any mutable runtime state accessed by multiple tasks/ISRs/callback contexts must be protected by explicit synchronization (mutex/spinlock/task-serialized model).
- Callback registrations (`cb + user_data`) must be read/written under synchronization and invoked outside locks using snapshotted copies.
- Public state-query APIs should return immutable snapshot structs, not pointers/references to mutable internal state.
- When lock acquisition can block, code must use bounded waits and return explicit failure on lock timeout.
## Managed Worker Ownership
- Every long-running worker/task/timer must have one explicit owner component and a documented lifecycle (`create/start/stop/join`).
- Stop paths must request cooperative cancellation first, then perform bounded wait/join, and finally apply deterministic fallback (for example force-delete) if timeout expires.
- Owner modules must clear task handles/state deterministically on worker exit to avoid stale lifecycle signals.
- Composition layer may create bootstrap wiring tasks, but policy workers must be owned by `control_plane` (or lower owner layer), not by `app_composition`.
## Runtime Composition Boundary
- Composition boundary owns application wiring: startup sequence, task graph assembly, and inter-module boot orchestration.
- Policy/orchestration layers own policy logic and coordination rules, but not top-level application boot wiring.
- Entry code (`app_main`) should only invoke composition-level entrypoints.
## Blocking and Lifecycle Contracts
- Any potentially blocking API must define timeout semantics, return behavior, and side effects after timeout.
- Default timeout behavior must be explicit and documented (including non-positive timeout handling).
- Start/Stop APIs must be idempotent and safe to call repeatedly.
- Stateful modules should expose lifecycle query APIs (for example, running/started status) instead of duplicating state in higher layers.
- Stop semantics must include real worker/resource teardown with bounded wait and deterministic fallback behavior.
- Unbounded waits in lifecycle/orchestration paths should be avoided unless explicitly justified and safe.
- Long-running loops in managed components must support cooperative termination.
## Build, Flash, and Validation
- Use the ESP-IDF/toolchain version declared by the project documentation or lockfiles.
- Activate ESP-IDF v5.5.4 before building.
- Before running any `idf.py` command, first determine which activation style exists in the current environment:
- Prefer path expressions that are portable across developer machines. Avoid hardcoded user-specific absolute paths such as `/home/<user>/...` or `/Users/<user>/...` in docs, scripts, and shell snippets when a variable-based form is sufficient.
- For POSIX shells, prefer parameterized paths such as:
```bash
export ESPRESSIF_ROOT="${ESPRESSIF_ROOT:-$HOME/.espressif}"
export IDF_VERSION_TAG="v5.5.4"
```
- If `eim` exists on the current machine, prefer running ESP-IDF commands through `eim run`, for example:
```bash
eim run <COMMAND> [IDF_VERSION]
eim run "idf.py build" v5.5.4
eim run "idf.py --version"
```
- In sandboxed or other write-restricted environments, `eim` logging initialization may fail if the default user state/log directories are not writable. In that case, pass `--log-file` with a writable path, for example:
```bash
eim --log-file ./eim-build.log run "idf.py build" v5.5.4
eim --log-file /tmp/eim.log run "idf.py --version" v5.5.4
```
- If `eim` does not exist, prefer the newer EIM-managed activation script when available:
```bash
source "$ESPRESSIF_ROOT/tools/activate_idf_${IDF_VERSION_TAG}.sh"
```
- If the EIM activation script does not exist, fall back to the legacy ESP-IDF export flow:
```bash
export IDF_PATH="$ESPRESSIF_ROOT/${IDF_VERSION_TAG}/esp-idf"
source "$IDF_PATH/export.sh"
```
- If both `eim` and activation scripts exist, prefer `eim run` first.
- Do not assume one activation path universally applies; verify the current machine/setup first.
- On non-POSIX environments, use the equivalent shell-native path syntax, but keep the same decision order: `eim run` first, activation script second, legacy `export.sh` flow last.
- `idf.py set-target esp32s3`: one-time target setup.
- `idf.py build`: compile and validate.
- `idf.py -p <PORT> flash monitor`: flash and open serial monitor (`Ctrl+]` to exit).
- `idf.py menuconfig`: adjust project options.
- `idf.py fullclean && idf.py build`: clear stale artifacts.
## Coding Style & Naming Conventions
- Use 4-space indentation and keep braces/function style consistent with existing `components/*/*.c`.
- Prefer `lower_snake_case` for functions and variables; use module-prefixed public APIs.
- Use `UPPER_SNAKE_CASE` for macros/constants (`CMD_SEND_DATA`, `WIFI_CONNECTED_BIT`).
- Prefix file-local statics with `s_` (for example `s_server`, `s_jobs`) and keep per-file `TAG` logging constants.
## Language Policy
Developer-facing artifacts may be written in **Chinese or English**. This includes:
- Source code: identifiers, comments, log tags, and string literals used for diagnostics.
- Version control: branch names, commit messages, PR titles and descriptions, and code-review comments.
- Documentation: READMEs, architecture docs, inline API docs, and this file (`AGENTS.md`).
When modifying existing artifacts, prefer the language already used in the surrounding content unless the task explicitly requires another language. Keep identifiers consistent with the existing codebase and use clear, unambiguous terminology.
**User-facing content:** UI strings, end-user help text, localization resource files, and any material rendered directly to end users may use the target locale language.
## Testing Guidelines
There is no dedicated unit-test directory currently. Minimum validation:
- Build check: `idf.py build`.
- For architecture-impacting PRs, also run focused guard checks:
- Verify no duplicated cross-cutting contract families across facades.
- Verify no new unbounded wait is introduced in lifecycle/orchestration paths.
- Verify application boot wiring remains in the composition boundary.
- Verify facade APIs do not use primitive magic command/event IDs in public callback signatures.
- Verify non-owner facade headers do not expose downstream-layer types directly.
- Verify new managed workers have explicit stop control and bounded teardown path.
- Add lightweight static checks (`rg`, linters, or scripts) as needed to enforce these constraints.
## Branching Strategy (GitFlow)
This project follows GitFlow. All work happens on topic branches; direct commits to `main` or `develop` are forbidden.
### Long-lived branches
| Branch | Purpose | Receives merges from |
| ------- | ------- | -------------------- |
| `main` | Production-ready releases; every commit is a tagged release. | `release/*`, `hotfix/*` |
| `develop` | Integration branch; latest delivered features for the next release. | `feature/*`, `release/*`, `hotfix/*` |
### Short-lived branches
| Branch pattern | Base | Merges back into | Naming example |
| -------------- | ---- | ----------------- | -------------- |
| `feature/<topic>` | `develop` | `develop` | `feature/ble-pairing` |
| `release/<version>` | `develop` | `main` **and** `develop` | `release/1.2.0` |
| `hotfix/<topic>` | `main` | `main` **and** `develop` | `hotfix/crash-on-boot` |
### Rules
1. Create feature branches from `develop`; merge back via PR with squash or rebase (no merge commits on `develop`).
2. When `develop` is release-ready, cut a `release/<version>` branch for stabilization; only bug-fixes go there.
3. After release QA, merge `release/*` into `main` with a version tag, then back-merge into `develop`.
4. Hotfixes branch from `main`, merge into both `main` (tagged) and `develop`.
5. Delete short-lived branches after merge.
## Commit & Pull Request Guidelines
Use a clear conventional format:
- Commit style: Strictly follow the Conventional Commits specification.
- Keep commits focused and atomic by layer/module (`platform`, `domain`, `control_plane`, `app_composition`).
- Source code is hosted in Gitea; submit changes through a Gitea Pull Request targeting the appropriate GitFlow branch.
- PRs include purpose, affected layer(s), whitelist changes (if any), config changes, and validation evidence.
- If work is executed from a staged remediation/audit checklist, remove completed checklist items in the same PR so the document stays a live source of truth.
## Security & Configuration Tips
- Never hardcode credentials or API keys in source files; configure through `menuconfig`.
- Do not commit local `sdkconfig` variants, serial-port-specific commands, or generated `build/` artifacts.