Story: Let the plan state domain rows, not a bag

Table of Contents

This page documents a story in Sprint 29. It captures the goal, current status, acceptance criteria, and the tasks that compose it.

1. Goal

A trade has one description, in the domain. The planner draws a product and constructs the domain's own rows; the writer persists them. The per-asset-class *_terms bag disappears, and a product can no longer be assembled from whichever fields happen to be present.

2. Status

Field Value
State DONE
Parent sprint Sprint 29
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-30

3. Acceptance

  • The plan carries no *_terms bag: the slot states domain rows, and the writer persists them. Each planner keeps a struct of what it draws, which is the generator's own shape and not what a slot carries.
  • Every product the planners draw is constructed by a named constructor that states what that product is, and that constructor cannot fill a column the product does not have. Which products a population draws is a coverage question and lives in Populate the ACME books with a realistic trade population.
  • The writer persists and no longer translates.
  • The families that book today still book, proved by a provisioning run and not by a unit test alone.

4. Tasks

Task State Start End Description
Scaffold story: Let the plan state domain rows, not a bag DONE 2026-10-28 2026-10-28 Story scaffolding rides this task: documents, sprint wiring, and the scaffold PR. Close it before merging that PR.
Construct one product properly DONE 2026-10-28 2026-10-28 Prove the story's shape on one product before five families move. Add a named constructor for EquityCliquetOption beside the equity option instrument type, make the writer build that product through it, and show that a frequency cannot be stated on a product that has none.
Construct every commodity product by shape DONE   2026-10-28 The commodity writer copies every field of the bag into one row for all twenty-five codes, and the table has no cross-column check to stop a forward carrying a barrier. Give each code a named constructor whose terms hold only the columns its shape states, so a mixture cannot be assembled.
Give the commodity products their own facts where the family can state them DONE 2026-10-28 2026-10-29 The commodity constructors fixed each product's shape, but several products are drawn as a plainer shape than they are: the Asian option states no averaging, and the basket products state no members even though the family has a constituent table nothing writes to. Give those products their own facts, and record the ones the family still cannot state.
Construct every bond product by name ABANDONED 2026-10-29   The bond writer assembles one table's rows from the terms bag for ten routed codes. Give each code a constructor that states its own facts, so the bond family joins equity and commodities in constructing rather than assembling, and the bag has one fewer family to carry.
Construct every rate product by name DONE 2026-10-29 2026-10-29 Eight rate products are drawn from one terms bag and translated into a header row plus fact rows by a writer arm several hundred lines long. Give each product a constructor that states its own facts, and let the writer hand over what it is given.
Construct every fx product by name DONE 2026-10-29 2026-10-29 The fx writer is the last family that assembles products from the terms bag, across six instrument tables and sixteen routed codes. Give each drawn code a constructor, so every family bookable today constructs rather than assembles and the bag has one family left.
State what a credit reference entity is DONE 2026-10-29 2026-10-29 The credit writer and its routing exist and the credit catalogue is seeded, but no ACME party has a credit generation config, so a provisioned tenant holds no credit trades at all. Measured on a recreated database: zero rows in the credit instrument table, and the demo profile seeds bond, commodity, equity, fx and interest_rates only. Unlike commodities, which were chartered in the same sprint, credit has neither a book nor a config.
Flip bond onto its rows DONE   2026-10-29 The first family to stop carrying a bag. The slot gains an instrument batch, the bond planner constructs its instrument, and the writer persists it. The security stays in the bag until the next slice, because the run writes the issue before it books the trade.
Flip commodity onto its rows DONE 2026-10-30 2026-10-30 Twenty-five commodity codes construct today, but the planner still fills a bag and the writer translates it. Move that translation into the planner so the plan carries the rows, and the writer only persists them.
Flip rates onto its rows DONE 2026-10-30 2026-10-30 Ten rate codes construct today, but the planner still fills a bag and the writer translates it. Move that translation into the planner so the plan carries the rows, and the writer only persists them.
Flip fx onto its rows DONE 2026-10-30 2026-10-30 Sixteen fx codes construct today, but the planner still fills a bag and the writer translates it. Move that translation into the planner so the plan carries the rows, and the writer only persists them.
Flip equity onto its rows DONE 2026-10-30 2026-10-30 Seventeen equity codes construct today, but the planner still fills a bag and the writer translates it. Move that translation into the planner so the plan carries the rows, and the writer only persists them.
Flip credit onto its rows DONE 2026-10-30 2026-10-30 Six credit products construct today, but the planner still fills a bag and the writer translates it. Move that translation into the planner so the plan carries the row, and the writer only persists it.
Flip the bond security DONE 2026-10-30 2026-10-30 The bond instrument states its own row now, but the security still travels in the bag: ensure_issue builds a bond_issue composite, its legs, amounts and rates from bond_terms before the trade is booked. Move that construction to the planner so the bag carries nothing for bonds and the family can be deleted from it.

5. Decisions

  • Constructors beside the types, not in the synthetic component. The couplings are facts about the products, so they live with the products. The synthetic component then depends on the trading domain the way any producer does, which it already does through the writer.
  • One family at a time. Each family's move deletes that family's bag and proves itself with a run, so there is never a state where nothing books.
  • The config and the scope stay. They are the generator's own language and they do not describe trades.
  • The drawing struct stays, under the family's name. trade_terms and the slot's terms are deleted, so the plan states rows and nothing else. Each planner still holds a struct of what it draws; that is the generator's vocabulary, it never reaches a slot, and its comment says so.
  • A slot that states rows holds no bag at all. The bag's variant defaulted to its first alternative, which was a bond, so a family that stopped filling it read as a bond with empty terms and the run stopped at the bond issue table. The variant held std::monostate first while any bag remained, and then the variant itself was deleted with the last one.
  • A flip is a move, not a redesign. Each family books the same rows after it flips, and the provisioning run is the proof: a changed count is a defect in the move rather than a decision.
  • Which products a planner draws is a population question. A family that draws one of its ten codes still cannot produce the other nine, but that is what a population covers, not what a plan states, so it is tracked in the population story.

6. Out of scope

  • The trading model's own normalisation, where several products share a table and a check constraint says which columns each may fill. That stays as the backstop; this story stops a generator from having to guess at it.

7. Why

The synthetic population carries each trade's values in a per-asset-class bag. trade_terms is a std::variant of bond_terms, commodity_terms, rate_terms, fx_terms, equity_terms and now credit_terms, and equity_terms alone is about forty members. A planner fills the bag and a writer reads it and constructs the trading domain's rows from it.

The cost is measured. In one family the bag let eight values through that the schema refuses, and each one was found by a provisioning run: a target on an accumulator, an option type on a touch, a forward with no quantity, a swap returning a value nobody defined, a kind that named a classification rather than a vocabulary. None of them was visible by reading the schema, because the invariant is not in the schema a reader reads; it is in the check constraints.

8. What the bag is

Three kinds of thing mixed together:

Kind Examples Owner
The generator's own facts book, counterparty, trader, trade date, netting set, trade id the generator
Drawing parameters seed, tenor band, notional band, which catalogue the generator
The product's economic content strike, expiry, barrier, basket the domain

Only the first two are the generator's language. The third belongs to the domain, and the bag is a second, weaker description of it.

It is not an anti-corruption layer: an ACL translates once, at a boundary, in one direction, against a contract a test can hold still. This bag is interpreted independently by both ends, is per asset class rather than per product, and leaves the product's identity as a string that the writer branches on.

9. The defect underneath

The trading model states a product's identity as data: a trade_type_code column plus check constraints naming the columns each code may fill. That is a legitimate storage normalisation – two products whose facts coincide share a table – but it puts half of a product's type in SQL, where a generator cannot see it.

So this story is not only about the bag. It is about giving a product one description, in the domain, and a way to construct it that cannot state an illegal combination.

10. The model

Part Owner What it is
trade_generation_config generator A specification: asset class, book, bands, seed, counts.
the scope generator The resolved drawing context: party, books, counterparties, catalogues, as-of date, seed.
the population domain A list of trades: a trade envelope and its instrument batch.

The planner draws the product and constructs the domain rows; the writer persists them. The generator keeps specification and drawing and stops describing products.

The couplings move into named constructors beside the types, one per routed product, each taking that product's own facts and filling exactly the columns it states. The rule becomes: a product is constructed, never assembled. The SQL check stays as the backstop it should always have been.

11. The flip: what it changes, and the order

Every family now constructs its products, but the population still carries each trade as a bag of drawn fields and the writer still translates one. The last step removes both: a slot states the domain rows, and the writer persists them.

1. The slot carries an instrument batch. trade_population_slot.terms is a std::variant of eight bags; it becomes one instrument_batch, the shape the booking request already takes. The envelope the slot already carries – book, counterparty, trader, trade date, structure – is untouched, because it belongs to the trade and not to the product.

2. Construction moves into the planner. A planner already resolves the scope; it will call the constructors the last four tasks produced and put the rows in the batch. A planner that cannot construct a product will fail where the drawing happens, with the product named, instead of failing in the writer after the trade has been half-built.

3. The writer stops translating. Its per-family arms go. It stamps the audit columns on every row in the batch, builds the booking request's anchor and booking from the envelope, and sends. Stamping becomes one walk over the batch rather than six hand-written ones.

4. The bag dies last, and only when empty. trade_terms and the eight *_terms structs are deleted when the last family stops filling them. Until then they stay and one family uses each, which is what makes the move safe: a family that has flipped cannot regress, and a family that has not keeps booking.

12. Order of families

Bond, commodity, rates, fx, equity, credit. Smallest first, and bond first because it draws one product, so the seam is proved where there is least to change. Each family flips in its own task with its own provisioning run, and the population it books must match what it booked before the flip: the run is the proof, and a changed count is a defect in the move rather than a decision.

13. What must not change

  • The rows the population writes. A flip is a move, not a redesign; the eight families book the same trades afterwards.
  • The envelope. Book, counterparty, trader and dates stay the generator's.
  • The config and the scope. They are the generator's own language.

14. What this does not fix

Products the planner does not draw. Bonds draw one of ten, and their fact rows are empty; credit draws five of six. Those are population decisions recorded as their own tasks, and the flip does not hide them: a family that draws one product still books one product.

15. Design record

The reasoning is written up in What a trade population is made of, which states the three kinds of thing, the defect underneath and the order of work.

16. Result

The plan states rows and the bag is gone. Every family that books constructs its products through named constructors, the planner puts the rows in the batch, and the writer persists what it is given: no family translates a bag any more, and trade_terms, the slot's terms and the writer's six bag arms were deleted with the last of them.

Six families flipped, in the order the story set — bond, commodity, rates, fx, equity, credit — and the bond security followed, which is what made the bag removable. Each flip was proved by a provisioning run against a fresh tenant, and every one booked the population it booked before its flip: 24 bonds, 25 commodity rows and 12 constituents, 24 rate headers with their legs, amounts, rates and strikes, 32 fx rows with 4 range bounds, 17 equity rows with their children, and 5 credit rows. The tenant holds 127 trades after every run.

Two defects surfaced that were not the story's own.

  • A slot that filled no bag read as a bond, because the bag's variant defaulted to its first alternative. Commodity flipped first and the run died on the bond issue table with a nil id. The variant held std::monostate first until the bag was deleted.
  • main could not provision any tenant: the catalogues work had removed the registrar entry for the legal entity publish route while the model still declared it, so the subject had no handler and every run stalled for twenty minutes. Fixed in #4092, without which no flip could be proved.

What this story did not do: it did not widen coverage. Bond still draws one of its ten codes, the rate planner never reaches its knock-out swap, the fx planner never reaches its variance swap or accumulator, and a swaption's drawn strike is dropped by its constructor. Those are population questions and are tracked there, in Populate the ACME books with a realistic trade population.

Emacs 29.3 (Org mode 9.6.15)