Skip to main content

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

  1. Edit pure domain in kxinspect_backend_python/app/domain/charge_state.py or deadline.py.
  2. Regenerate vectors via scripts/export_fixtures.py --output /tmp/bundle (or state_vectors()).
  3. Verify both replays: uv run pytest -q tests/unit/test_domain.py and flutter test test/unit/*_vectors*.
  4. 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

AreaEntry points to touchTests to update
New charge eventapp/domain/charge_state.pyapp/services/charge_service.pyapp/schemasapp/api/*tests/unit/test_domain.py, tests/api/test_commands.py, tests/contract/test_contract_v1.py + Dart vectors
New read endpointapp/schemas, app/api/v1/endpoints/reads.py, app/services/charge_service.pytests/api/test_reads.py
Attachment ruleapp/core/config.py (constants), app/services/attachment_service.pytests/api/test_attachments_events_dev.py
New Flutter featurelib/features/<feature>/{application,data,presentation} + lib/router/feature_slots.darttest/widget, test/golden, integration_test
Design tokenlib/design_system/foundations/* + lib/design_system/theme/kx_theme.darttest/design_system/*, golden regeneration

Rules:

  • Thin UI — widgets never contain transition logic; features/*/application and core/* own it.
  • Central routing — all navigation via lib/router/app_router.dart; no Navigator.push ad-hoc.
  • No raw literals — colors/spacing/radius/typography via design_system/foundations.
  • No print — use logger (backend) or maybeLog* (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:

  1. 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
  2. Launch frontend: flutter run -d chrome --dart-define=DATA_SOURCE=remote --dart-define=API_BASE_URL=http://127.0.0.1:8000
  3. Exercise SSE: open two tabs, contest in one, observe the other via GET /api/v1/events or in-app live sync.
  4. Test idempotency: repeat a POST .../accept with same Idempotency-Key — second response must be byte-identical, no second event/version bump.
  5. Test deadline: set KX_DEMO_NOW past a charge's deadlineAt, hit GET /api/v1/charges/{id} and confirm accepted + acceptanceOrigin=deadline.

Troubleshooting during dev

  • RuntimeLockedError → another backend holds runtime/runtime.lock; lsof runtime/runtime.lock or delete stale PID after crash, then python scripts/reset_runtime.py --runtime-root runtime (guarded by .kxinspect-runtime marker).
  • store.epoch_mismatch 409 → client sent stale expectedStateEpoch; take a fresh Hub snapshot before retrying.
  • Drift build errors → delete lib/core/database/kx_database.g.dart and rerun dart run build_runner build --delete-conflicting-outputs.
  • Golden failures on CI → regenerate on pinned image (netlify.toml pins toolchain), never widen tolerance.