Workflow¶
Reading order, commits, branch flow, migrations, tests.
The mechanics a contributor (human or LLM) has to internalise before touching the code. Each section is the rule, not the rationale; rationale lives next to the artifact it constrains.
Reading order¶
Stop at any step and you have a working mental model of the layer above.
- One vertical slice end-to-end: features/register_actor/. Five files, ~430 lines.
command.py(input),decider.py(pure rule),handler.py(shell),route.py+tool.py(REST + MCP). Every slice follows this shape. - The aggregate: aggregates/actor/. State, events, evolver. Pure.
- The ports: infrastructure/ports/. Six
Protocols (clock, id_generator, event_store, idempotency, authorize, event_publisher). - One fitness test: test_slice_contract.py. What's enforced mechanically.
- Vocabulary: Glossary.
Commits¶
Conventional Commits with scope: type(scope): subject. Imperative, lowercase, no trailing period, under 72 chars.
Types:
| Type | Use for |
|---|---|
feat |
New caller-visible capability |
fix |
Bug fix |
refactor |
Internal restructure, no behavior change |
perf |
Performance |
test |
Tests only |
docs |
Docs only |
build |
Build, deps, packaging |
ci |
CI, pre-commit, hooks |
chore |
Anything else not user-visible |
Scopes:
- Cross-cutting:
infra,api,db,obs,auth,arch - BCs:
equipment,access,recipe,run,campaign,supply,operation,trust,data,subject,decision,strategy,budget - Repo:
repo,deps
Multiple scopes: pick the dominant or omit.
Examples:
feat(infra): add port protocols and structured logging
feat(equipment): add register_device decider with optimistic concurrency
fix(db): drop redundant index on events(stream_id)
test(access): cover register_actor invariants
ci: add lint+typecheck+test workflow
Granularity: one commit = one cohesive change that compiles and passes tests. Port + adapter + test for one capability is one commit. Refactor + feature is two.
Branch flow¶
Solo: commit directly to main. CI must be green before pushing.
Cross-cutting work while WIP is in flight: use a worktree. git worktree add ../cora-<task> main, work there, commit, return. Pre-commit stashes unstaged changes to tracked files but never to untracked ones — a half-staged WIP slice (untracked handler.py + unstaged wire.py edits hidden by stash) will false-fail architecture fitness functions and force --no-verify to land an otherwise-clean commit. Worktrees isolate the cleanup from the WIP entirely. Also avoid git commit -- <paths> with mixed staged/unstaged state — the path-form bypasses the index in a way pre-commit's stash flow doesn't expect.
Migrations¶
Schema changes in infra/atlas/migrations/<timestamp>_<short_name>.sql.
make migrate-new name=add_foo # new empty migration
# edit the .sql file
make migrate-hash # update infra/atlas/atlas.sum
make migrate-apply # apply locally
CI verifies atlas.sum and runs a grep-based safety scan on net-new files (blocks DROP TABLE, DROP COLUMN, TRUNCATE, ALTER COLUMN ... TYPE). Atlas's migrate lint is behind atlas-cloud login (skipped). For genuine destructives, add -- atlas:safety:allow=<reason> per line. Forward-only: a rollback is a new compensating migration.
Tests¶
Descriptive-sentence pytest style: snake_case prefixed with test_. Adapts Roy Osherove's MethodName_Scenario_ExpectedBehavior to Python.
- subject: unit under test (endpoint, function, layer, behavior)
- expected_outcome: the property pinned, not the inputs
- scenario (optional): conditions; introduce with
when_orfor_
Optimize for the property, not the inputs.
Good:
test_decide_emits_method_defined_when_stream_is_empty
test_handler_returns_capability_for_known_id
test_evolve_asset_relocated_mutates_parent_id_to_target
test_post_methods_returns_201_with_method_id
Avoid:
test_post_methods_with_three_capabilities_in_order_b_a_c # describes inputs
test_handler_3 # opaque
test_register_subject_works # outcome too vague
Markers:
@pytest.mark.unit: pure / in-process@pytest.mark.integration: real Postgres viadb_pool@pytest.mark.contract:TestClient(create_app())
Marker is the category; name is the property. Don't repeat the category in the name. Long names are fine.
File naming (integration tier). Four suffix shapes cover everything under tests/integration/. Same spirit as the function-name rule: lead with what the file pins, not the inputs.
test_<slice>_handler_postgres.py— single-slice, single-aggregate handler against real PG. Dominant pattern, most files._postgresis load-bearing here: it disambiguates from the in-memory twin attests/unit/<bc>/test_<slice>_handler.py.test_postgres_<infra>.py— Postgres adapter itself is the subject (event store,append_streams, idempotency, lookup tables, summary projections).tests/integration/scenarios/test_<beamline-or-facility>_<routine>.py— cross-BC scenario walk stitching many slices to express one real beamline routine or facility-topology setup (today:test_2bm_alignment_center.py,test_aps_facility.py). No_scenariosuffix on the filename: thescenarios/folder is the marker. No_postgressuffix either: there's no in-memory twin to disambiguate against. One routine per scenario, no compendiums. The seven-name phase vocabulary (install,shakedown,commissioning,beta,operations,shutdown,decommission) is a thinking aid only: it lives in the docstring first line (for example,"""Phase: shakedown. Routine: motor homing at APS 2-BM.""") and optionally as a@pytest.mark.<phase>marker for runtime selection. It is not part of the filename, and it is not a CI gate. When a single routine eventually needs scenarios at two different maturities, add the phase token to the filename at that point astest_<beamline>_<routine>_<phase>.py.test_<subject>_postgres.py— anything else against real PG: full-FSM walks, cross-aggregate / multi-stream atomic writes, projection-worker behavior, race tests. Use a descriptive infix when the test's specialness needs to be named (_cross_bc_,_atomic_,_full_fsm_cycle_,_fsm_walk_,_race_); don't force one infix where several capture different scopes.
The _scenario term follows DDD / BDD / Event-Storming vocabulary (Domain Storytelling, Gherkin scenarios). Avoid _pilot for the test tier — "pilot" decays as more beamlines integrate; the scenario shape is time-invariant. "Pilot" stays the right word for the real-world meaning (first beamline deployment) when it appears in domain text.
Test coverage per slice¶
A fitness function in tests/architecture/test_slice_test_coverage.py enforces the slice-pyramid convention. New slices follow the matrix below or fail CI.
| slice shape | decider | handler | endpoint | mcp_tool | handler_postgres |
|---|---|---|---|---|---|
| command | ✓ | ✓ | ✓ | ✓ | create-style only |
| entry-append | — | ✓ | ✓ | ✓ | create-style only |
| query | — | ✓ | ✓ | ✓ | — |
Create-style = verb in {define_*, register_*, add_*}. These introduce a new aggregate or event stream, so the jsonb round-trip + ON CONFLICT + unique-constraint behavior gets pinned per-slice against real PG. State-transition slices (abort_*, complete_*, resume_*, hold_*, and so on) lean on cross-BC scenario coverage in tests/integration/scenarios/ instead.
Detection is lenient: a slice is considered covered if either the 1:1 file test_<slice>_<suffix>.py exists OR another test file in the right tier mentions the slice name as a substring (catches resource-plural grouped files like test_actors_endpoint.py covering register_actor, and bundles like test_iter2_mcp_tools.py). The EXEMPT_FROM_* allowlists in test_slice_test_coverage.py document existing divergences with citations.
Idempotency contract tests¶
Create-style slices that accept Idempotency-Key get a dedicated test_<slice>_idempotency.py contract test. State-transition slices don't need them; the FSM rejects duplicate transitions naturally.
Event-sourcing aggregate conventions¶
Three architecture tests pin the shape of every cora/<bc>/aggregates/<agg>/events.py:
test_decider_purity— everydecider.pyis referentially transparent (no I/O, no clock, no UUID generation).test_from_stored_wraps_payload— everycase "X":infrom_storedwrapsKeyError/TypeError/AttributeErrorasraise ValueError(f"Malformed X payload {payload!r}: {exc}") from exc. Decided 2026-05-18 after a 3-agent corpus survey (Marten, pyeventsourcing, Pydantic, msgspec, cattrs all wrap). Without the wrap, Sentry / Datadog group every aggregate'sKeyErrorinto one undifferentiated issue.test_projection_idempotency— every projection'sapply()is safe to re-run on the same event.
Per-BC test helpers¶
When a BC accumulates its own seeding / setup helpers (typically at rule-of-three), they live in tests/unit/<bc>/_helpers.py, the same name as the shared tests/unit/_helpers.py and tests/integration/_helpers.py. The architecture test test_helper_naming_convention.py rejects divergent names like _iter2_seed.py or _seed_helpers.py.