- Rules: public functions need type hints + docstring; new domain entities go through Pydantic validation; ops >50 ms run off the GUI thread (`AnalysisWorker` in QThread, cancel via `isInterruptionRequested()`, results cross threads as lightweight `ResultsHandle` to HDF5).
Runner emits OpenSeesPy commands in fixed order (`docs/architecture.md`): `wipe → model → node → fix → material → section → geomTransf → element → timeSeries → pattern/load → recorder → system/numberer/... → analyze`. Never reorder.
- Do all feature/fix work on `develop` (or short-lived branches off `develop`, merged back into it). `main` is release-only — never commit directly to it.
ruff format src tests # line-length 100, E501 ignored
mypy src/otko/core src/otko/services
pytest -m "not slow" # CI gate: lint → this, on 3.10/3.11/3.12 × ubuntu/windows/macos
```
Focused runs: `pytest tests/unit` (pure logic, ms), `pytest tests/gui -k <name>` (pytest-qt, needs display), `pytest tests/integration -k <name>` (real OpenSeesPy runs). Single test: `pytest tests/unit/test_project.py::test_name -q`. Markers: `gui`, `slow`. Linux GUI tests need `xvfb-run -a pytest ...` plus system Qt libs (see `ci.yml` apt list).
Notes: `tests/conftest.py` auto-`ops.wipe()`s the OpenSees domain between tests (lazy import so core tests stay Qt/OpenSees-free). Coverage omits `views/`. Commit style: Conventional Commits (`feat:`, `fix:`, ...). `pre-commit install` runs ruff + mypy (mypy hook scoped to `core|services|viewmodels`).
## Examples & persistence
-`examples/*.py` are source of truth; `examples/*.osmodel` are generated artifacts (checked in). Never hand-edit `.osmodel` — change the script and regen: `python examples/cantilever.py` (each script saves, reloads, asserts clean round-trip).
- Projects persist as single Pydantic-validated JSON `.osmodel` (diffable); analysis output goes to `*.osresults.h5` (HDF5, one group per case).
- Quick smoke: open `examples/cantilever.osmodel` → run `Tip-Load` → M3 peaks 50 kN·m at fixed end.