21 KiB
AGENTS.md
Canonical engineering instructions for AI coding agents working in IoT DC3.
Scope and precedence
- This file applies to the whole repository.
- Follow the user's request first, then this repository-level guidance.
- Keep changes focused. Do not revert unrelated work in a dirty worktree.
- Do not create a Git commit without the explicit, commit-specific confirmation described below.
Sources of truth
Avoid copying volatile versions or generated state into documentation. Verify them at the source:
| Concern | Source of truth |
|---|---|
| Java, Spring, Maven plugins and reactor modules | root pom.xml and affected module POMs |
| Backend build commands | root Makefile |
| Frontend dependencies and scripts | dc3-web/package.json and dc3-web/pnpm-lock.yaml |
| Frontend build image/tool pins | dc3-web/Dockerfile |
| Containers and registries | root Makefile, .env.example, and dc3/docker-compose*.yml |
| CI behaviour | .github/workflows/ |
| Release notes | dc3/bin/changelog.py and generated dc3/doc/CHANGE.md |
If this file disagrees with executable configuration, treat the executable configuration as current and update this file as part of the same change when appropriate.
Project overview
IoT DC3 is a multi-protocol, cloud-native, open-source industrial IoT platform evolving toward AI agents. Its main runtime areas are:
- Gateway: HTTP entrypoint through Spring Cloud Gateway.
- Auth Center: tenant, token, user, role, resource, and API authorization.
- Manager Center: drivers, devices, points, profiles, and metadata.
- Data Center: point-value ingestion, queries, commands, and dashboards.
- Agentic Center: AI-assisted operations through OpenAI-compatible APIs.
- Drivers: protocol adapters built on the shared driver SDK.
- Web: Vue-based management UI in
dc3-web/.
The backend targets Java 21 and is a Maven multi-module Spring Boot/Spring Cloud project. Check the root POM for current framework versions.
Repository map
iot-dc3/
├── dc3-api/ protobuf and gRPC contracts
├── dc3-common/ shared models, services, DAL, facades, driver SDK, and infrastructure
├── dc3-center/ deployable auth, manager, data, agentic, and single-process applications
├── dc3-driver/ protocol driver implementations
├── dc3-gateway/ HTTP gateway
├── dc3-web/ Vue/TypeScript frontend
├── dc3-coverage/ aggregate JaCoCo report and absolute coverage gate
├── dc3-e2e/ Testcontainers-backed end-to-end tests
├── dc3/ compose files, environment files, scripts, and generated release notes
├── docs/design/ design documents (e.g. `mq-abstraction.md` — proposed pluggable broker port)
├── Makefile preferred backend/container command entrypoint
└── .mvn/settings.xml local Maven mirror configuration
Backend architecture
Layering
Business modules follow this flow:
Controller (WebFlux) -> Service (BO) -> Manager (DO) -> Mapper (SQL)
- Controllers implement the
BaseControllerinterface and returnMono<R<T>>. - Use
BaseController.async(...)to move blocking JDBC work to the bounded-elastic scheduler. - Services extend
BaseService<B, Q>, work in business objects, and own business rules. - Managers extend MyBatis-Plus
IService<DO>; implementations extendServiceImpl<Mapper, DO>. - Mappers extend MyBatis-Plus
BaseMapper<DO>and contain persistence-level SQL operations. - Do not expose persistence objects from controllers or facade contracts.
Common types:
| Type | Module | Role |
|---|---|---|
BaseService<B,Q> |
dc3-common-public |
base CRUD service contract |
BaseController |
dc3-common-web |
reactive controller helpers and user/tenant context |
R<T> |
dc3-common-public |
standard response envelope; use R.ok(...) and R.fail(...) |
BaseBO, BaseVO, BaseDTO |
dc3-common-model |
shared business, web, and transfer fields |
BaseBuilder |
dc3-common-model |
MapStruct conversion base |
TenantOwned |
dc3-common-public |
marker for tenant-scoped entities |
Shared code placement
Place code according to ownership and dependency direction, not Java visibility. A public method is not automatically
shared infrastructure.
- Put framework-neutral contracts, request/pagination/tree entities, tenant markers, and broadly reusable utilities in
dc3-common-public. - Put platform-wide constants and top-level shared domain/wire/persistence enums in
dc3-common-constant. - Put BO/VO/DTO bases, builders, validation groups, extensions, and shared transport models in
dc3-common-model; those models may reference enums owned bydc3-common-constant. - Keep framework- or capability-specific public helpers in the narrowest owning module, such as gRPC conversion in
dc3-common-api, WebFlux helpers indc3-common-web, and RabbitMQ helpers indc3-common-rabbitmq. - Keep constants and nested enums used by only one module, protocol, configuration object, or implementation beside
that owner. Reserve top-level
*Constantclasses and top-level public enums fordc3-common-constant; use a concern-specific local name such as*Limits/*Defaults, a nested enum, or a private field until the concept becomes a stable cross-module contract. - Do not duplicate cross-module wire names, header names, routing identifiers, cache-key fragments, or persistence codes.
Define one canonical symbol in
dc3-common-constantand migrate callers together. - Preserve the dependency floor:
dc3-common-constantmust not depend on other DC3 modules, anddc3-common-publicmust not depend on capability modules.
Tenant safety
Tenant isolation is a hard requirement.
- Preserve tenant scope in every new query and mutation.
- Carry tenant IDs through gRPC requests whenever the contract supports them.
- Include tenant context in cache keys for tenant-owned data.
- Validate ownership before returning or mutating data across service boundaries.
- Do not add
tenantId IS NULLshortcuts unless the data model explicitly defines global records. - Treat missing tenant validation as a correctness and security defect, not a convenience trade-off.
Facade boundaries
Business code must use facade interfaces for cross-service calls.
- Contracts belong in
dc3-common-facade-api. - Transport-backed implementations belong in
dc3-common-facade-grpc. - In-process implementations belong in the matching module:
dc3-common-facade-local-auth,dc3-common-facade-local-data, ordc3-common-facade-local-manager. dc3-common-facade-localis a dependency aggregator and does not contain implementation sources.- Keep controllers and services independent from transport details unless they are explicit transport adapters.
gRPC contracts
Proto files live under dc3-api/*/src/main/protobuf.
When changing a contract:
- Update the
.protofile. - Compile the affected API module to regenerate sources.
- Update server implementations and client builders/stubs together.
- Preserve backward compatibility where practical.
- Verify tenant propagation and the
GrpcRerror envelope.
Servers are Spring beans extending generated *ImplBase classes. Reuse shared stub configuration; do not construct ad
hoc channels in business code.
Driver SDK
Drivers implement protocol behaviour through the SPI types in dc3-common-driver; shared runtime services handle
registration, scheduling, and value dispatch.
Primary extension points are DriverProtocol, DriverLifecycle, DriverMetadataListener, DriverHealth,
DeviceHealth, and DriverCommand. Prefer existing SDK plumbing over driver-specific infrastructure.
Driver application.yml metadata is user-facing:
- Keep
name,attribute-name, andremarkin English. - Treat driver
codevalues as routing-stable identifiers. Changing one requires a metadata and RabbitMQ migration plan.
API and domain conventions
CRUD verbs
CRUD-shaped names reflect result cardinality across Service, Controller, Facade, gRPC server, and proto RPCs:
| Action | Java | HTTP | gRPC |
|---|---|---|---|
| create one | add(BO) |
/add |
n/a |
| delete by ID | delete(Long) |
/delete |
n/a |
| update one | update(BO) |
/update |
n/a |
| return one | getXxx(...) |
/get_xxx |
GetXxx |
| return many | listXxx(...) |
/list_xxx |
ListXxx |
- Base CRUD comes from
BaseService<B,Q>:add,delete,update,getById, andlist(Q). - Reserve
select*for raw Mapper/Manager persistence operations. - Reserve MyBatis-Plus
remove*for the Manager layer; business deletion usesdelete*. - Do not introduce
find*,query*, orfetch*as primary CRUD verbs. - HTTP paths are lowercase snake_case and mirror Java names.
- Use
getStatusByPage(Q)for status maps anddispatchRead/dispatchWritefor command dispatch, following existing contracts.
Models and enums
- DOs model database storage; BOs model business semantics; VOs/DTOs model web or transport input/output.
- Persistent write paths accept BOs in services. Controllers and transport adapters convert VO/DTO input to BO.
- Read-only projections may return VOs directly when a duplicate BO would add no business meaning.
- Use MapStruct builders for VO/BO/DO conversion, including enum/index conversion.
- Do not leak database-coded
Byte,Integer, orStringflags when a domain enum exists or should exist. *FlagEnumis for boolean-like toggles,*StatusEnumfor state machines, and*TypeEnumfor classifications.- Enum constants use descriptive
UPPER_SNAKE_CASE; enumcodevalues use lowercase tokens. - Do not introduce magic flag constants such as
private static final Byte DEFAULT = 1. - Do not expose secrets in VOs. Exclude
apiKey,password,secret,token, and credential fields from serialization and Lombok@ToString.
Web API and OpenAPI
- Controllers return the standard
R<T>envelope and never expose DOs. - Apply grouped validation consistently and keep validation/exception messages in English.
- Document REST endpoints with springdoc annotations; do not maintain a parallel handwritten OpenAPI spec.
- Each business controller package needs the appropriate
GroupedOpenApibean, gateway aggregation route, and Swagger UI entry. - Shared WebFlux/springdoc configuration belongs in
dc3-common-weband must be registered throughAutoConfiguration.importswhen component scanning will not discover it. - Docs are enabled in development-style profiles and disabled in production. Export a running stack with
make openapi.
Configuration and logging
- Custom configuration-property prefixes use
dc3.*. - Prefer validated, typed
@ConfigurationPropertiesover scattered@Valuefields. - YAML deployment values use
${ENV:default}placeholders. - WebFlux base paths use
spring.webflux.base-path, notserver.servlet.context-path. - Use English, stable event names and parameterized SLF4J messages.
- Prefer structured fields such as
tenantId={}, userId={}, deviceId={}. - Never log tokens, passwords, credentials, full request bodies, or raw private payloads at info level.
- Pass caught exceptions to warn/error logs unless stack-trace suppression is intentional.
Frontend conventions
Frontend code lives in dc3-web/. Its executable configuration is the source of truth:
- dependencies, package-manager version, and scripts:
dc3-web/package.jsonandpnpm-lock.yaml; - TypeScript behaviour:
dc3-web/tsconfig.json; - Vite, proxy, environment, and SCSS behaviour:
dc3-web/vite.config.ts; - test configuration:
dc3-web/vitest.config.tsandplaywright.config.ts; - container toolchain pins:
dc3-web/Dockerfile.
Use pnpm only; do not create npm or Yarn lockfiles. Keep package-manager pins aligned between package.json and the
Dockerfile.
Key rules:
verbatimModuleSyntaxis enabled. Useimport typefor every type-only import; Vue components, functions, and icons remain normal value imports.- Use
<Entity>Formfor create/update payloads and<Entity>Recordfor read responses. - Represent Java 64-bit IDs as strings. The backend emits identifiers as JSON strings on the HTTP contract, so standard JSON parsing (no JSONBigInt) is sufficient.
- API wrappers mirror backend cardinality:
getXxxfor one value,listXxxfor collections/maps/pages, andaddXxx/updateXxx/deleteXxxfor mutations. - Reuse CRUD helpers from
src/api/common.tsand API bases fromsrc/config/constant/api.ts; keep API wrappers thin. - Prefer
<script setup>, Composition API, setup-style Pinia stores, and existing composables. - Every router-guard branch must settle navigation. Prefer return-style guards for new code and cover guard changes with tests.
- Axios interceptors own authentication headers and 401 handling; do not duplicate that logic in feature APIs.
- Vite dotenv files live under
src/config/env/and use theAPP_prefix. - Global Element Plus variables are injected by Vite; do not duplicate their
@usedirectives in components. - Menu changes may require synchronized backend seed data,
settingsNav.ts, router definitions, i18n locales,Layout.vue, andSettings.vuechanges.
Common frontend checks, run from dc3-web/:
pnpm check
pnpm lint:check
pnpm test:guard
pnpm test:ci
pnpm build
Use affected Vitest suites for focused changes and Playwright for browser-level workflows. Coverage thresholds belong in
vitest.config.ts; do not duplicate their numbers here.
Commands
Prefer a root Makefile target when one exists. It selects Docker Compose or Podman Compose from the local environment
and centralizes Maven settings.
make up-db
make up-optional
make up-dev
make package
make test
make test-it
make test-e2e
make coverage
make changelog
make validate-documentation
make validate-javadoc
For direct Maven work, use the checked-in settings file locally:
mvn -s .mvn/settings.xml -q -DskipTests compile
mvn -s .mvn/settings.xml test -pl dc3-common/dc3-common-manager
mvn -s .mvn/settings.xml test -pl dc3-common/dc3-common-manager -Dtest=DriverControllerTest
mvn -s .mvn/settings.xml test -pl dc3-common/dc3-common-public \
-Dtest="RTest#okWithDataExposesDataAndDefaultEnvelope"
When using -am together with -Dtest, add -Dsurefire.failIfNoSpecifiedTests=false so dependency modules without the
selected test do not fail spuriously.
GitHub Actions should normally use public Maven repositories rather than the local mirror settings unless a workflow is intentionally testing that mirror.
Environment and Compose
.env.exampleis the Compose interpolation template..envis local and untracked; create it throughmake init-env.dc3/env/dev.envis for IDE/local Java process variables withoutexport.dc3/env/dev.env.shis shell-sourceable and usesexport.- Do not treat these files as interchangeable.
Canonical Compose files are under dc3/:
docker-compose.ymldocker-compose-dev.ymldocker-compose-db.ymldocker-compose-optional.yml
Registry choice is controlled through Make arguments and environment variables, not duplicated Compose files. After a
Compose change, validate every touched stack with its corresponding make config-* target or an equivalent
docker compose ... config / podman compose ... config command.
Testing and verification
Test types
- Unit tests (
*Test.java,*Tests.java) run with Surefire, JUnit 5, Mockito, AssertJ, and ReactorStepVerifierwhere appropriate. Do not start a Spring context for a test that can use direct construction. - Integration tests (
*IT.java) run with Failsafe and may usedc3-common-testTestcontainers and harnesses. - E2E tests live in
dc3-e2e/and are gated by theDC3_E2Eenvironment variable.
Reusable test infrastructure includes GrpcInProcessExtension, RabbitTestHarness, PgTimescaleContainer,
RabbitContainer, MqttContainer, FixedClockConfig, JsonAssertions, EnumContractTest, and
SecretFieldContractTest.
Coverage
make coverage generates the aggregate report under dc3-coverage/target/site/jacoco-aggregate/. The current gate is
an absolute minimum configured in dc3-coverage/pom.xml; dc3-coverage/scripts/check_coverage.py validates the
aggregate XML. Do not claim a relative regression gate unless the build implements one.
Proportional validation
Run checks proportionate to the change:
- Java/shared behaviour:
mvn -s .mvn/settings.xml -q -DskipTests compile. - Tested behaviour: affected unit tests, then
make testwhen warranted. - DAL/SQL or infrastructure integration: affected ITs or
make test-itwith a container runtime. - gRPC: compile generated sources and run matching client/server contract tests.
- Coverage-sensitive changes:
make coverageand inspect the aggregate report. - Changelog tooling:
python3 -m py_compile dc3/bin/changelog.py. - Compose: render/validate every touched configuration.
- YAML: parse after accounting for Maven placeholders such as
@project.artifactId@. - Agent/docs changes: validate referenced paths, targets, scripts, test selectors, and links.
- Documentation or public Javadoc changes: run
make validate-documentationandmake validate-javadoc.
Report what was verified and what was not verified before handing off public-behaviour changes.
Release notes
dc3/doc/CHANGE.md is generated from Git history. Do not hand-edit the current release block unless fixing generator
output.
make changelog
make changelog FROM=<previous-tag-or-ref> TO=HEAD VERSION=<version>
Commit subjects feed the generated changelog. Reject vague or non-English subjects. A changelog-only commit must use exactly:
docs(release): update generated changelog
Commit that file separately from behaviour, configuration, or tooling changes.
The root pom.xml version is the release identity. After committing the version and generated changelog on main, run
make tag; the script creates only the matching annotated v<project.version> tag. It must not calculate a new version
or create a GitHub Release directly. The Docker Images workflow owns release verification, image publishing, and
GitHub Release creation. Keep its release environment protected with required reviewers and tag restrictions.
If a version lands in CHANGE.md without a matching GitHub Release, backfill it with make release-backfill
(dry-run) or make release-backfill-apply. The tool maps date-formatted versions to same-day commits, assembles
the standard release body (TITLE.md + changelog block + RELEASE-FOOTER.md quick start that links docs.dc3.site),
and creates tags through the API - no image publishing, no latest pointer change. After TITLE.md or
RELEASE-FOOTER.md evolves, make release-backfill-refresh re-renders existing backfilled release bodies.
Commit rules
AI coding agents must not commit without explicit confirmation for that specific commit.
Before each commit, present:
- proposed commit message;
- exact files included;
- why those files form one coherent change;
- verification already completed.
Wait for approval before running git commit. For multiple commits, present the sequence first and obtain approval for
each commit before creating it.
Use Conventional Commit subjects:
<type>(optional-scope): <english imperative summary>
Allowed types are feat, fix, perf, refactor, docs, build, ci, test, chore, style, security,
revert. Use ! for breaking changes and explain the impact in the body. Keep subjects specific enough for release
notes.
The tracked .husky/pre-commit hook runs frontend lint-staged. Do not claim commit-message enforcement unless a
tracked commit-msg validation hook is added.
Editing and documentation
- Preserve AGPL headers where they already exist.
- Prefer existing patterns and helpers over new abstractions.
- Keep public/user-facing project text in English unless editing a localized document.
- Document public types and non-override methods with their contract, constraints, nullability, tenant scope,
concurrency semantics, or other non-obvious invariants. An inherited contract is sufficient for an
@Overridemethod. Do not restate the signature or implementation line by line. - Keep code comments focused on intent and design constraints. Remove comments that merely narrate the next statement, and update comments in the same change as the behavior they describe.
- Do not use volatile
@versionJavadoc tags. Use@sinceonly when it records a stable public API milestone. - Avoid generated metadata churn unless required by the task.
- Use structured parsers or project toolchains for structured files when practical.
- Keep multilingual root READMEs structurally aligned.
- Runtime/environment changes should update the environment guide in
pnoker/iot-dc3-docs. - Driver-authoring changes should update the driver-authoring guide in
pnoker/iot-dc3-docs. - Test strategy, harness, or coverage changes should update the testing guide in
pnoker/iot-dc3-docs. - Release-workflow changes should update
CONTRIBUTING.mdand this file.