R19 ENG-STD (2026-07-24). These are the conventions the codebase actually runs on — codified from shipped practice, not aspiration. New code follows them; drift found against them is a fix ticket, not a debate. Style baseline: PEP 8 via ruff (configured in-repo); typing on public signatures; Python ≥ 3.10 (prefer 3.11+ for new venvs).
- Engine-leaf / adapter / one-gate. Domain logic lives in a pure engine module
(
aec_api/<engine>.pyoraec_data/<engine>.py): plain functions, dict/dataclass in → dict out, no HTTP and (where possible) no DB. The router is a thin adapter: parse/validate → call the engine → shape the response; exactly one authorization gate per route (require_role), never logic in two layers. Engines are what tests import. - Layering rule:
aec_apiMAY importaec_data; never the reverse.aec_datais model/geometry-only (ifcopenshell world), reusable by the CLI and workers. - Reference by GUID. Model elements are referenced by IFC GlobalId everywhere — API bodies, stored records, recipes. Transient viewer ids never cross the API boundary.
- Module records go through the module engine (
modules.py+module_schema.pyas the single source of truth formodule.json). Create wraps the field map:{"data": {...}}; PATCH takes the field map directly. Same-fieldset fields stay adjacent (test-enforced). - Selectors compose on QUERY-DSL. Any new scoping/filter feature reuses
query_dsl.select()— one grammar for model elements, topics, and rules. No second selector language. - Stored-config caps. Anything an editor can store that a viewer-level GET later evaluates gets
count/size caps at save time (the
rule_library.MAX_*pattern). Caps keep the newest rows (desc().limit()then re-sort), never silently hide new data.
- Dependency-injected
Sessionper request; explicit commits; no session escapes its request. Internal engines that need a session in tests useSessionLocal()context managers. - Long/CPU work goes to the jobs lane (durable queue), never inline in a request. Geometry worker
count is env-tunable (
AEC_GEOM_WORKERS). - Public (unauthenticated) endpoints are exceptional, hardened by construction: whitelists, length caps, hard count caps, revocation → 404 (the ShareToken pattern).
- Errors: raise
HTTPExceptionwith a usefuldetail(include the offending field/name); 409 for optimistic-concurrency conflicts; 422 for validation.
- SQLAlchemy 2.x typed mappings (
Mapped[...]/mapped_column). Aggregations in SQL, not Python loops (the P0 SQL-aggregate helpers). Neverdict(cursor)— use.all()(ruff C416 autofix is unsafe on cursors). - Every new table/column ships an Alembic autogenerate revision in the same release. SQLite
non-null adds need
server_default. - Postgres-only DDL discipline: anything in an index expression must be IMMUTABLE (
concat_wsis STABLE — use all-coalesced||). Every post-baseline migration creating amod_<key>table hand-adds the Postgres-only FTS GIN block (static-guard-enforced intest_alembic_migrations). The real gate isdb-migrations.ymlagainst genuine Postgres — check its run after every migration release; SQLite cannot catch this class.
- Engines are deterministic: same input → same output, asserted byte-identical where it matters (view-template resolve is the reference). No wall-clock/randomness inside engines; timestamps come from the caller or the DB.
- Money at the API/storage boundary goes through
money.py(Decimal). Pure analytical engines may compute in float but round once at the edge; Pythonround()is banker's rounding — test expectations accordingly. Never accumulate currency by repeated float addition across thousands of rows without a final Decimal reconciliation. - User-supplied expressions are never
eval'd:calc_fields.py(AST whitelist + node/length caps) is the only expression path. Regexes over user text: bounded quantifiers + input caps (ReDoS discipline).
- Runner:
run_tests.py(no pytest); tests aretest_*.pyinservices/api/, hardcoded in the TESTS list — register every new test file (the runner tolerates missing files; a count drop is a signal, not noise). - Full gate:
cd services/api && AEC_GEOM_WORKERS=1 PYTHONUTF8=1 PYTHONPATH="src;../data/src" ./.venv/Scripts/python.exe -X utf8 run_tests.py— cleantest_*.db/_storage_*artifacts first (cleanup globs must be artifact-shaped; nevertest_*bare). - Test shape: each file is a self-contained scenario — env vars up top (
DATABASE_URLsqlite scratch,STORAGE_DIR), engine asserts first, then route asserts viaTestClient, one loudprint("... OK - ...")summarizing what was proven. Golden/hand-computed fixtures for numeric engines. - Targeted test per feature while building; the full suite gates the release, per sprint.
require_role on every project route (test-enforced) · privileged operations gate every path, not
just the front door · no secrets in code/repo — env/operator config only · untrusted XML through
defusedxml · outbound fetches through the SSRF guard · non-crypto hashes flagged
usedforsecurity=False · CodeQL at 0 after every push (the security-monitoring skill is the
operating procedure).
Version-numbered releases to main: bump apps/web/package.json + src-tauri/tauri.conf.json,
CHANGELOG newest-first (no competitor names — standing directive), roadmap marks, full suite green,
tag v0.3.NNN, then verify CI + CodeQL. Load-bearing decisions get an ADR (docs/adr/).