Files
lucky-cat/AGENTS.md
T
2026-06-25 00:34:34 +08:00

17 KiB

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:
      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:
      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:
      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:
      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:
      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.