dodil/erp-brick-contracts

AGENTS.md

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 evidencesrc/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 templatesdomain/library.py; contract types domain/contract_type.py
what a customer configuresa ROW (keep_contract_type / keep_clause / keep_template / keep_settings), never code
a country's checksa localisation pack in domain/packs/ (it records and refuses; it never judges legal effect)
who may do something, which rows they seeapplication/support.py (View) and the permission on the declaration
a new action for people and agentsa 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 tableinfrastructure/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_event row (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_consumer namespace).
  • 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_consumer rows (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).