Instructions for agents working on brick-contracts
This brick is built on erp-platform (0.5.x). Read its BRICKS.md before changing anything here — it holds the
rules (ownership, use cases, data, identity, the public door, capabilities, tests) and why each one exists. The spec
is modules/contracts/module.yaml (blog repo, specs/hr-contracts): its words are the names here (the glossary is
at the top of src/brick_contracts/domain/__init__.py).
Where a change goes
| You are changing… | It goes in |
|---|---|
| a rule about a contract, its signers, approvals, versions or evidence | src/brick_contracts/domain/contract.py (the Contract aggregate) and a test in tests/test_rules.py named for the story |
| a rule about clauses or templates | domain/library.py; contract types domain/contract_type.py |
| what a customer configures | a ROW (keep_contract_type / keep_clause / keep_template / keep_settings), never code |
| a country's checks | a localisation pack in domain/packs/ (it records and refuses; it never judges legal effect) |
| who may do something, which rows they see | application/support.py (View) and the permission on the declaration |
| a new action for people and agents | a new @CT.use_case in application/ — it becomes a route AND a tool |
| what a commit causes (deliveries, the executed copy, superseding an original) | application/workflow.py, each its own unit of work, idempotent on its own key |
| a neighbour (business partner, mail.send, the public door) | its anti-corruption layer in infrastructure/ |
| a column or table | infrastructure/tables.py, then python scripts/gen_schema.py > src/brick_contracts/schema.sql (additive: a new column at the END and as an explicit ALTER) |
Never add an HTTP route or an MCP tool by hand. Never import SQLAlchemy, FastAPI, MCP or a PDF engine in domain/.
Before you commit
.venv/bin/python -m pytest -q --tb=short # ~1 min: rules, doors, tier 1, SC-CT scenarios + probes
.venv/bin/python scripts/mutation_check.py # after touching an invariant: every mutation must be CAUGHT
A new use case: a test in tests/test_doors.py (both doors, the 404, the 403), and a recipe in
tests/scenarios/catalogue.py for the idempotency and mass-assignment probes — a command with no recipe FAILS them.
After changing repositories or schema: BUCKET=erp-platform-test DODIL_ORG=dodil python scripts/live_check.py.
Don't
- Don't let anything change a sent version's words, or accept a signature for a version other than the current frozen one. A re-send is version N+1.
- Don't update or delete a
ct_signature_eventrow (within retention), and don't write IP or user agent into free text — they are fields, chained. - Don't mark a signer SIGNED without a SIGNED event of ours (an upload is SIGNED_ELSEWHERE), and don't make an executed copy that is not a real file with its own sha256.
- Don't store or log a link: it is returned once (copy_signing_link / reissue_link opt out of Idempotency-Key for that reason). Manual delivery tasks carry the recipient and message, never the link.
- Don't claim more than "signed electronically". Don't let a DEED type be sent online.
- Don't answer 403 for a row the caller may not see — 404. An employment body needs contract:employment:read.
- Don't release the executed copy to a consumer that is not its originator (
ct_consumernamespace). - Don't reload configuration per request: read the snapshot (
uow.config.load()), and bump the token on every configuration write. Don't loop over every contract in the tick: select what is due. - Don't write another brick's tables (business-partner is read through its route). Don't hand-write an upsert or a DELETE — the repository. Don't mint ids: contract_id derives from source_ref|type_code.
- Don't commit credentials or
.env. Don't change erp-platform, brick-hr or brick-business-partner from here.
Known gaps (0.1)
- The platform's seam does not say which brick is calling, so the originator is matched by
ct_consumerrows (sub / role / email); in process the "sub" is the actor string a consumer passes. - Use cases receive no client IP, user agent or session id: our side's events (POOL_SESSION) carry the pool sub but no IP / agent / session.
- A missing-values contracts.draft answers 422 (claim released), not "failed with missing[] in the output", because a stored failure would make the corrected call a 409.
- hr does not poll a contract that needed approval (tests/test_tier1.py xfail).