Task: Add the immutable trade anchor
Table of Contents
This page documents a task in the Split the trade into an anchor and its components story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
The anchor exists beside the wide trade as a generated immutable entity, with database foreign keys to its three classification lookups, an insert-only write, and a sanctioned tenant purge path.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Split the trade into an anchor and its components |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-03 |
3. Acceptance
- trade_anchor is immutable and keyed by (tenant_id, id); its party, counterparty and trade type are checked, and its three closed classifications are C++ enums with enforced foreign keys to immutable lookup tables.
- The generated write inserts and never replaces, so the key refuses a second write of the same trade id. The booking operation, which task 2 adds, maps that refusal to a domain error.
- A tenant purge removes anchors through a sanctioned session switch; any other update, delete or truncate is refused.
- Build, ctest, pgTAP and the local check set pass.
4. Plan
- Three lookup entities,
entry_channel_type,counterparty_scope_typeandbooking_nature_type:read-only-current-stateprofile,:immutable:, no tenant, seeded by populate scripts. The codes are the enumerators of three hand-written C++ enums, in the manner ofbinding_mode; enum generation was retired in sprint 20, so there is no generator to extend. - The anchor,
trade_anchor:read-only-current-state,:immutable:, tenant-scoped, so its key is(tenant_id, id). Party, counterparty and trade type are trigger checks, because their tables are temporal. The three classifications are enforced foreign keys with:use_no_tenant:. A check requires a counterparty unless the scope isintra_entity. - Generator: the immutable guard lets a
DELETEthrough while the transaction-local purge switch is on (ores_utility_allow_immutable_purge_fn). The notify trigger states a null tenant for a table with no tenant. - Hand wiring: the trading create and drop masters, the populate master, the message registrar, the anchor event mapping, the anchor RLS policies, the tenant purger, the custom type registry and the CMake source lists.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
6. Notes
trade_trades_test.sql,trade_identifiers_test.sql,trade_party_roles_test.sqlandtrade_ore_envelope_view_test.sqlstop at their fixture: each books a party id it never creates. This predates the task.
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2569 | [trading] Add the immutable trade anchor |
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Local review: the lookup generator emits codes outside the closed set | *_type_generator.cpp |
Decline | The template does this for every lookup; nothing generates synthetic lookup rows. |
| 2 | Local review: notify triggers and event registrars of the immutable lookups never fire | lookup notify triggers | Decline | The facet emits them for every entity; they cost nothing. |
| 3 | Local review: four trade pgTAP files fail on a fixture party they never create | projects/ores.sql/test/trade_*_test.sql |
Decline | Predates this task; the wide trade's tests go with it in the last task of the story. |
| 4 | @claude: the purge switch helper has no execute revoke | utility_functions_create.sql |
Decline | Any role can call set_config on the setting itself, so a revoke on the helper closes nothing; the version replace signal is the same. Table delete grants are the boundary. |
| 5 | @claude: the switch stays on for the rest of the purge transaction | iam_tenant_purger_create.sql |
Accept | The purger turns it off right after the anchor delete. |
| 6 | @claude: the trigger re-checks no tenant scope under the switch | immutable guard | Decline | The purger's delete is tenant-scoped; the guard decides only whether a delete may happen. |
| 7 | @claude: the trigger comment overstates who can reach the branch | template | Accept | Reworded. |
| 8 | @claude: track the stale trade pgTAP files | trade_*_test.sql |
Decline | See row 3. |
| 9 | @claude: test that the switch does not leak into the next transaction | pgTAP | Decline | A pgTAP file is one transaction; the transaction-local set_config is the PostgreSQL guarantee. |
| 10 | Rerun: the anchor test fixture took a party from another tenant | trading_trade_anchor_test.sql |
Accept | The fixture now reads the system tenant's party and counterparty. |
10. Result
trade_anchoris generated beside the wide trade: immutable, keyed by(tenant_id, id), with trigger checks for party, counterparty and trade type, and enforced foreign keys to three immutable lookups with no tenant. A check requires a counterparty unless the scope isintra_entity.entry_channel,counterparty_scopeandbooking_natureare C++ enums whose values are the seeded lookup codes.- The immutable guard admits a
DELETEonly under the transaction-local purge switch, which the tenant purger turns on before it deletes anchors. - The generator now handles a table with no tenant: the notify trigger and the synthetic data generator.
- The duplicate-to-domain-error mapping moves to task 2, where the booking operation that owns it is built.
- Verified: build clean (
linux-clang-debug-make); ctest 90/90; pgTAPtrading_trade_anchor_test.sql18/18; codegen tests pass; drift shows only the knowntrade_service.cppclang-format diff; sweep, paste blocks, reachability, permissions, populate references, CMake, inventory, ER strict, SQL hygiene, shell scripts, lint, structure notes and site page all clean.