chore: initial commit
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
# 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/<asset_dir>/`; 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/<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.3"
|
||||
```
|
||||
- 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.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 <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
|
||||
|
||||
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/<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.
|
||||
Reference in New Issue
Block a user