# 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. | | 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. | | 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 place `.bin` frames under `components/platform/assets/lvgl_animations//`; 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.3 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//...` or `/Users//...` 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.3" ``` - If `eim` exists on the current machine, prefer running ESP-IDF commands through `eim run`, for example: ```bash eim run [IDF_VERSION] eim run "idf.py build" v5.5.3 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.3 eim --log-file /tmp/eim.log run "idf.py --version" v5.5.3 ``` - 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 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 All developer-facing artifacts must be written in **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`). **Exception — 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/` | `develop` | `develop` | `feature/ble-pairing` | | `release/` | `develop` | `main` **and** `develop` | `release/1.2.0` | | `hotfix/` | `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/` 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.