Development Workflow
Branching & quality gates
Both repos follow conventional commits and a static-first gate order. Every commit should pass the local gates below; CI reruns the same set (no private grader).
Backend gate order
cd kxinspect_backend_python
uv sync --locked
uv run ruff format --check .
uv run ruff check .
uv run mypy app # strict
uv run pytest -q --cov=app --cov-fail-under=85
uv run python scripts/export_openapi.py --check docs/openapi-v1.json
uv run python scripts/verify_fixture_manifest.py
Notes: ruff line length 110, target py313. No test touches runtime/ on disk or the network; each builds an app with ManualClock + manual scheduler + temp runtime_root.
Frontend gate order
cd kxinspect_frontend_flutter
dart format --set-exit-if-changed .
flutter analyze
flutter test
./tool/test_all.sh # canonical one-command gate
l10n.yaml → flutter gen-l10n must be rerun after any ARB change; build_runner after Drift/schema changes.
Domain-first development
The contract vectors are the shared oracle. Workflow for any lifecycle/deadline change (hypothetical — contract is frozen):
- Edit pure domain in
kxinspect_backend_python/app/domain/charge_state.pyordeadline.py. - Regenerate vectors via
scripts/export_fixtures.py --output /tmp/bundle(orstate_vectors()). - Verify both replays:
uv run pytest -q tests/unit/test_domain.pyandflutter test test/unit/*_vectors*. - Only after vectors agree, update schemas/OpenAPI and regenerate Dart models.
Because app/domain has zero framework imports, the Python→Dart export stays trivial and divergence is impossible to hide — the contract test fails fast.
Making changes
| Area | Entry points to touch | Tests to update |
|---|---|---|
| New charge event | app/domain/charge_state.py → app/services/charge_service.py → app/schemas → app/api/* | tests/unit/test_domain.py, tests/api/test_commands.py, tests/contract/test_contract_v1.py + Dart vectors |
| New read endpoint | app/schemas, app/api/v1/endpoints/reads.py, app/services/charge_service.py | tests/api/test_reads.py |
| Attachment rule | app/core/config.py (constants), app/services/attachment_service.py | tests/api/test_attachments_events_dev.py |
| New Flutter feature | lib/features/<feature>/{application,data,presentation} + lib/router/feature_slots.dart | test/widget, test/golden, integration_test |
| Design token | lib/design_system/foundations/* + lib/design_system/theme/kx_theme.dart | test/design_system/*, golden regeneration |
Rules:
- Thin UI — widgets never contain transition logic;
features/*/applicationandcore/*own it. - Central routing — all navigation via
lib/router/app_router.dart; noNavigator.pushad-hoc. - No raw literals — colors/spacing/radius/typography via
design_system/foundations. - No
print— uselogger(backend) ormaybeLog*(frontend analytics).
Fixture discipline
Seed is canonical: kxinspect_backend_python/app/seed/*.json → exported bundle → copied into kxinspect_frontend_flutter/assets/fixtures/ and committed. Rules:
# after any seed edit:
cd kxinspect_backend_python
uv run python scripts/export_fixtures.py --output /tmp/bundle
uv run python scripts/verify_fixture_manifest.py --bundle /tmp/bundle
# copy into frontend and commit:
cp /tmp/bundle/* ../kxinspect_frontend_flutter/assets/fixtures/
# keep vectors too:
cp vectors/*.json ../kxinspect_frontend_flutter/assets/fixtures/vectors/
Fixture mode must never require a server: flutter run --dart-define=DATA_SOURCE=fixture proves it. tool/check_evidence.dart guards submission.yaml hashes.
Running against a real backend locally
Backend and frontend evolve independently. To test the seam:
- Start backend:
KX_DEMO_NOW=2026-08-01T12:00:00Z uv run uvicorn app.main:create_app --factory --host 127.0.0.1 --port 8000 - Launch frontend:
flutter run -d chrome --dart-define=DATA_SOURCE=remote --dart-define=API_BASE_URL=http://127.0.0.1:8000 - Exercise SSE: open two tabs, contest in one, observe the other via
GET /api/v1/eventsor in-app live sync. - Test idempotency: repeat a
POST .../acceptwith sameIdempotency-Key— second response must be byte-identical, no second event/version bump. - Test deadline: set
KX_DEMO_NOWpast a charge'sdeadlineAt, hitGET /api/v1/charges/{id}and confirmaccepted+acceptanceOrigin=deadline.
Troubleshooting during dev
RuntimeLockedError→ another backend holdsruntime/runtime.lock;lsof runtime/runtime.lockor delete stale PID after crash, thenpython scripts/reset_runtime.py --runtime-root runtime(guarded by.kxinspect-runtimemarker).store.epoch_mismatch 409→ client sent staleexpectedStateEpoch; take a fresh Hub snapshot before retrying.- Drift build errors → delete
lib/core/database/kx_database.g.dartand rerundart run build_runner build --delete-conflicting-outputs. - Golden failures on CI → regenerate on pinned image (
netlify.tomlpins toolchain), never widen tolerance.