Story: Clean ores.trading to the component clean standard
Table of Contents
This page documents a story in Sprint 26. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
ores.trading meets the Component Clean Standard: every item: All, Protocol and Entity pass, each item that does not apply is recorded with its reason, and ores.trading joins COMPONENTS_UNDER_TEST. Order 32 of 35 in the sprint 26 component clean-up, tier 3 (domain component).
ores.trading is the largest component in the tree and the last one whose regeneration no gate has ever measured. It cannot pass the standard by running the same pass the smaller components ran, because a single regeneration touches 1751 files and the component's data model still carries an unresolved identity question inherited from sprint 25. This story therefore lands in six PRs, and it records the decisions that bound each one.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 26 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-02 |
3. Acceptance
- Every checklist item that applies to a component of kind Entity passes, with its evidence recorded on the task.
- Every item that does not apply is recorded as not applicable, with the reason.
- Every generator defect found is fixed at its source, with a regression test.
- ores.trading is listed in
COMPONENTS_UNDER_TEST, andcheck_component_drift.py --allpasses.
4. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| Baseline, decisions and evidence for ores.trading | DONE | 2026-09-26 | 2026-09-26 | PR 1. Record the B01 to B06 baseline, the consumer census, the open work and the seven decisions, with the evidence files. No code change. |
| Sync the ores.trading tree to its models | STARTED | 2026-09-26 | PR 2. Regenerate the modelled component, land the canonical protocol and the canonical notify triggers, delete the hand consolidation and the superseded hand-written twins, and make the tree build and test green. | |
| Model the unmodelled trading families and bind the unbound | DONE | 2026-09-28 | PR 3. Author the missing entity models, bind the 26 unbound entities to profiles or record the no-match, clear the legacy model fragments, and close the type defects the trade model investigation lists. | |
| Collapse the instrument identity into the trade | DONE | 2026-09-28 | 2026-09-28 | PR 4. One instrument identity, keyed by the trade, with the relationship declared where the schema can hold it. The bond security split moved to PR 5 by D17. |
| Split the bond security from the trade | DONE | 2026-09-29 | 2026-09-30 | PR 5. Key the shared bond entities by the ORE reference datum id, hang the leg family off the security, give bond_issue the fields ORE has and take bond_notional onto the trade. Decides defect 15 as D21. |
| Finish the bond pilot | DONE | 2026-09-30 | 2026-10-01 | PR 6. Re-model the bond future against the current ORE schema, add the missing constraints and child tables, fix the delete and add paths, run the corpus-wide round trip, and carry defects 14 and 16. |
| Shell, wiring, hygiene and verification for ores.trading | DONE | 2026-10-01 | 2026-10-02 | PR 7. Generate the shell units and recipes, seed the permissions, run the audits and the per-file survey, and enter the component in the registry. |
5. Where ores.trading stands (measured 2026-09-26)
Every figure below was measured on 2026-09-26 at commit b79609d808,
on a still tree, with the codegen virtual environment. The commands and
their raw output are in the task's evidence/ folder.
5.1. Size and shape
| Measure | Value |
|---|---|
| C++ files in the component | 1763, of which 35 carry the generated marker (2.0%) |
| Non-test C++ sources | 732, of which 12 are generated |
| Test sources | 58 files, 215 test cases across three executables |
Models under projects/ores.trading/modeling/ |
65: 57 entity, 6 field group, 1 module, 1 component overview |
| Tables owned | 65, declared by 137 scripts under ores.sql/create/trading/ |
| Protocol headers | 49, of which 1 is generated (trade_protocol.hpp) |
| TypeScript twins | 0 |
| Raw NATS subject literals in the component | 249 distinct, none declared in a model |
| Consumers outside the component | 38 files in ores.shell, 17 in ores.ore, 1 in ores.reporting, 126 distinct headers |
| Web consumers | 0 |
5.2. The drift is mostly a banner
check_component_drift.py --component trading-cpp --dry-run reports
1636 files that would change and 115 that would be created, 1751 in
total. Every block was classified by cause:
| Cause | Files |
|---|---|
| The generated-file banner and its template note, and nothing else | 1434 |
The eventing rename: entity_change_event to entity_event_traits, *_changed_event.hpp to *_event.hpp |
78 |
| Substantive template change | 123 |
The root CMakeLists.txt |
1 |
The evidence file groups the banner and the root CMakeLists.txt into
one bucket and reports 1435 marker-only files; the table above splits
them. The two views are the same 1636 changes.
The 123 substantive changes are the 114 database scripts under
ores.sql/create/trading/ plus the trade protocol, handler,
registrar, repository and service. The 114 are the trigger and
notify-trigger templates: the write-path version rule and the canonical
change payload.
The 115 creates are 57 event headers, 9 per-family instrument protocols, 9 handler headers, 9 registrar headers, 9 registrar sources and 22 shell recipes.
The conclusion that matters: the generator already reproduces the tree for the modelled families apart from the banner. The sprint 25 design predicted that a regeneration would "flatten and drop tree semantics". That is no longer true. The bond pilot, the regenerable trade entity and later template work removed the semantic gap, and what remains is stale template output.
5.3. The test suite fails for one reason
ctest -R "ores.trading.core.tests" reports 102 cases, 81 pass and 21
fail. ores.trading.api.tests and ores.trading.service.tests pass.
All 21 failures are *_publishes_nats_changed_event cases in the
eventing integration tests. The cause is already captured at
Finish the eventing payload migration:
123 of the repository's 209 notify triggers emit
{entity, timestamp, entity_ids, tenant_id}, the reader in
postgres_event_source.cpp requires {event_id, entity, key, action,
version, occurred_at, tenant_id}, the parse fails and the
notification is dropped. Trading owns 114 of those trigger files, which
is why its share of the drift includes them and why its suite fails.
5.4. The gates read green for the wrong reason
| Gate | Output today | What it means |
|---|---|---|
check_component_drift.py --component trading-cpp --dry-run |
1751 files differ, exit 1 | The only honest failure |
check_model_drift.py --summary |
164 of 309 models bound, no drift | ores.trading's unbound entities are invisible to it |
check_handler_permissions.py |
174 codes seeded across 4 components, exit 0 | Trading is not in the script's REGISTRY, so the gate does not read trading's 98 permission codes at all, and 92 of them are unseeded |
check_protocol_twin_coverage.py |
38 headers in 6 components, exit 0 | Trading is not counted; it has 0 TypeScript twins |
regenerate_shell_recipe_inventory.py --check |
silent, exit 0 | Trading has no recipes, so nothing can be stale |
validate_docs.py |
40 components pass | Genuine |
check_populate_references.py |
references resolve | Genuine |
regenerate_physical_space_inventories.py --check |
42 tables current | Genuine |
regenerate_cmake_component_files.py --component ores.trading.<sub> --check |
2 stale lists per subproject, exit 1 | Six stale lists in total |
6. The threads inherited from sprint 25
The refactor the user asked about is four threads, and sprint 25 closed all of them as story wrappers rather than as work. Thirteen of twenty tasks in the first story are DONE, and every task in the second is DONE. What was abandoned is the part that changed the shape of the data.
6.1. Thread 1: the data-oriented redesign
Story Redesign ores.trading on data-oriented principles, ABANDONED
2026-09-23. Its design
doc/agile/versions/v0/sprint_25/data-oriented-trading-model/design_trading_data_oriented_model.org
scored the component 2 of 5 on the persisted-data regime and 2 of 5 on
the wire regime, and named 13 findings. Its September 8 regeneration
oracle produced 47 modified and 65 untracked files; the same check
today produces 1751, but 88 per cent of the 1636 changes are the banner.
Landed: the bond relational family, a regenerable trade entity with
five field groups and org-declared foreign keys, insert trigger and
indexes (2478b1f879), its generated service layer (30a7cfbb2f), the
generated-file marker (e7e9491467), the product identity document
(e1ef999975, PR #2085), the untagged-variant fix, and the ORE
import, export and round-trip shell verbs.
Abandoned and still open: 7B23DB0F collapse the instrument identity
into the trade, 0C525D24 split the bond security from the trade,
A3DA0BB5 keep stated but default-valued elements in the export,
F628C22E product short code, 27FC1760 retire the rfl complexity
flags. Two more, 3E14AEC0 and 5A01EEBC, are moot because the whole
ores.qt tree was deleted at af67db3062.
6.2. Thread 2: the linkage between trade and instrument
The design question is already answered in the repository.
doc/knowledge/architecture/trade_modeling.org, lines 152 to 166,
states that the economics vary by product type, that they belong to one
agreement, and then:
The relationship between trade and instrument is therefore one to one, and the trade id identifies both.
doc/knowledge/domain/trade.org repeats it: a trade is four entities,
the product type, the security issue where one exists, the trade and
the instrument carrying the economic terms.
The code carries three identities instead of two:
instrument_identity_field_group.orgdeclaresinstrument_idas a key andtrade_idas an optional soft back-reference. Twenty-six models inherit the group.ores.trading.trade.orgaddsinstrument_idto the trade, andtrading_trades_create.sqldeclares ituuid null.- The declaration carries 91
instrument_idcolumns, and no foreign key anywhere targets an instrument table. Two nullable pointers face each other and nothing can make them agree. - There is no instrument table at all. The apparent
instrumententity is type glue: a concept, an alias and a nine-alternative variant.
Two smaller defects sit on the same ground.
trade_classification.hpp documents instrument_id as a soft foreign
key to ores_refdata_instruments_tbl, which does not exist. That file
also cites trade_identity_field_group.json, which does not exist
either.
The independent investigation
doc/agile/versions/v0/sprint_25/ore_trade_generator_trade_populations/trade_model_defects.org
reaches the same place from the persistence side, as its defect 13:
twenty-five instrument tables are keyed on instrument_id and five are
keyed on id, together with trades, swap_legs, composite_legs,
trade_identifiers and party_roles.
So 7B23DB0F and 0C525D24 are not an open design question. They are
the implementation of a decision the knowledge base already records,
and the abandoned split task left the reason to do the collapse first:
doing the security split first reshapes the same tables twice.
6.3. Thread 3: the bond pilot
The pilot proved its claim for the bond products and the trade
envelope. The zero-loss gate read 0 lost and 0 unexplained over 1566
documents. The mapper is a hand-written 1694-line
bond_instrument_mapper.cpp over a hand-written container of 241 and
425 lines. Forty-seven cases in six ores.ore test files cover it,
plus eleven cases in service_bond_instrument_reader_tests.cpp.
What silently stopped, with no task and no decision recorded:
- The design's
instrument_underlyingstable was never built. bond_legsand its rate, amount and amortization children are still keyed byinstrument_id, so bond static data is stored once per trade rather than once per security.- No
REFERENCESconstraint exists in any bond script, so the design's foreign key forissue_idis a comment rather than a constraint. - Eight
bondDataelements still have no column: CreditGroup, VolatilityCurveId, PriceQuoteMethod, PriceQuoteBaseValue, PriceType, Payer, CreditRisk and SubType. bond_futuresis a pre-v17 table with 15 columns while the currentinstruments.xsddeclares 5 members.ApplyConversionFactorandUseFuturePricehave nowhere to go. The zero-loss gate cannot see this because no ORE example contains a bond future.- The corpus-wide sweep over the roughly 50 bond documents never ran. Two documents were checked live.
- Deleting a bond instrument over the shell removes the header row and leaves the facts and the issue behind.
- Adding a bond is three saves with no transaction.
- Default-valued but stated elements are still dropped on export.
6.4. Thread 4: the trade model cleanup
trade_model_defects.org records sixteen defects found by loading every
table rather than by reading the model. The story that would have
consumed it, trading_model_cleanup, was scaffolded on the unmerged
branch feature/trading-model-cleanup and never landed. The defects
belong to this story:
| # | Defect | Owner here |
|---|---|---|
| 1 | Schedules and collections held as JSON text | PR 3 |
| 2 | Dates held as text, 58 fields | PR 3 |
| 3 | Monetary amounts held as double, 18 fields | PR 3 |
| 4 | Enumerations held as free text with no reference table | PR 3 |
| 5 | Code fields whose reference table does not exist | PR 3 |
| 6 | One concept under two names | PR 3 |
| 7 | Referential integrity in PL/pgSQL; 866 check constraints and no foreign keys | Partly closed in PR 4 (D15): 30 instrument-to-trade links are declared and trigger-enforced. The DDL foreign key is not declarable against a bi-temporal parent, and the 866 checks stand. Recorded |
| 8 | Enumerations enforced in PL/pgSQL, 27 of them | PR 3 |
| 9 | One table holding two products, five equity tables | Recorded in PR 4 and still open: the collapse does not change the row shape, so it stays with the equity modelling work |
| 10 | Product type names an asset class | Recorded in PR 4 and still open: product_type_t is still a database enum on the trades table; modelling it reshapes the product dimension rather than the identity |
| 11 | Staged reference data is never published | Out of scope, dq work |
| 12 | The publish guard collapses every office onto one party | Out of scope, dq work |
| 13 | Two names for an instrument's identity | Closed in PR 4: instrument_id is gone from the field group, from all 58 models that carried it, from the generated C++ and SQL and from every hand-written caller |
| 14 | Declared coverage is incomplete, and elsewhere undeclared | Decided in PR 6 as D28: every instrument table but one carries a trade_type_code constraint and their union covers every code, but ten general tables share one 121-code list, and the one table without a constraint has no producer. Giving each table its own list is the exclusive-or split (defect 9) of the families that own them |
| 15 | An underlying is an unqualified name | Decided in PR 5 as D21 and re-scoped in PR 6 as D26: no bond product states an underlying, so a table built here would have no producer. The fix belongs to the nine families that state one |
| 16 | No observation schedule | Decided in PR 6 as D29: the shared instrument_schedules table is the home and the bond family writes there; the twelve products that need one are equity, fx and commodity, and moving their scalar frequency columns into it is those families' reshape work |
| 17 | The trade's declared key is nullable, and its fixture left it empty | PR 3 |
| 18 | The import writes an instrument but not its legs for the nine rates families | Recorded, not the bond pilot's: the rates families own it. Found by the PR 6 corpus sweep, which reads one KnockOutSwap with no `LegData` in the export |
Defect 17 surfaced in PR 2, not in PR 3, because the regenerated trade
eventing test reads the version history by the declared key. The model
declares external_id as the key and as nullable with the generator
std::string(), so the fixture wrote no key, the mapper stored NULL, and
the lookup by '' found nothing. PR 2 gives the fixture a value, which is
a test-shape fix; making the column non-nullable, and making every writer
supply it, is PR 3's.
evidence/pr2_trade_key_fixture_defect.txt.
6.5. Open work that could collide
Checked 2026-09-26. No worktree is working on ores.trading. The open
pull requests are clean-marketdata, workflow-identity, clean-iam,
clean-variability and clean-service. One remote branch touches trading,
origin/feature/trading-model-cleanup, and it holds a single unmerged
commit from 2026-09-08 that scaffolds the sprint 25
trading_model_cleanup story. This story supersedes it. The branch is
not ours, so the PR 1 task records it rather than deletes it.
The backlog captures that touch this work are listed in the PR 1 task.
One latent defect belongs to another component but surfaces here. Two
models on main name ores.storage/net/storage_transfer.hpp, a path that
does not exist: ores.trading.trade.org and
ores.marketdata.market_series.org. The header is at
ores.storage.core/net/storage_transfer.hpp, and the hand-written
consumers use that path. Trading was regenerated in PR 2, so its model was
corrected to the path that exists; neither component is under the drift
gate, which is why the wrong path sat unnoticed. The marketdata half is
left to that component's own clean-up.
7. Environment blockers
Measured 2026-09-26 and 2026-09-27. The database would not rebuild from
main's tree, and the default build would not compile, for eight defects.
None of them is trading's. Six stopped the rebuild, and each repair
revealed the next: five dq population-function scripts that never close
their header comment; the storage service missing from SERVICE_NAMES,
from the service account seed and from the service registry; a missing
unique constraint on ores_dq_tags_artefact_tbl; a catalogue upsert that
relied on an ON CONFLICT the versioning trigger pre-empts, which this
programme's PR #2232 fixed; and a publish function that states version 0.
Two stopped the default build, which had been red since PR #2216: the FSM
remodelling deleted fsm_protocol.hpp and the machine filter with it, and
the dataset remodelling replaced get_datasets_request, while the
workflow, reporting and iam consumers stayed behind. Main's PR #2240
migrated those consumers and cleared the SQL. On that base the rebuild
needs no workaround, the build exits zero, and the fleet reaches
twenty-four units in two seconds.
task_model-missing-trading-families.org §2d holds the defect table, with
each symptom and its state.
8. Target shape
- One trade identity, one instrument identity and one security issue
identity, per
trade_modeling.org: the trade id identifies the trade and its instrument, and the security issue is a separate reference datum where the product has one. - Every table that carries persisted trading data has an org model, and every model is on the current format and bound to a profile or recorded as a no-match.
- Every wire type is a projection of a model. The nine rates families ship as generated per-family protocols. Hand consolidation is deleted. Expressing consolidation in the model is a later option, recorded as a decision.
- The database declares its relationships. A foreign key replaces every soft link the schema can hold.
- Regeneration is byte-identical and idempotent across SQL, C++, shell units, shell recipes and the CMake source lists.
9. Checklist
One row per item of the Component Clean Standard, with the result measured on 2026-09-26 and the task that owns it. The rows PR 3 owns were re-measured on 2026-09-28. The row is updated as each PR lands.
| Id | Applies | State (PR 3 rows 2026-09-28) | Owner |
|---|---|---|---|
| B01 | All | Met: evidence/b01_component_drift_dry_run.txt and b01_model_drift.txt |
PR 1 |
| B02 | All | Met: evidence/b02_b03_b04_survey.md |
PR 1 |
| B03 | All | Met: evidence/b02_b03_b04_survey.md |
PR 1 |
| B04 | Protocol | Met: evidence/b02_b03_b04_survey.md |
PR 1 |
| B05 | All | Met in PR 1 | PR 1 |
| B06 | All | Met in PR 6: no header has a test as its only consumer, so nothing is test-only dead code. The artefact tabulates the headers consumed outside the component; the wider 1273-header classification is stated in the record rather than tabulated | PR 6 |
| M01 | Entity | Met: no legacy JSON model, no lookup_entity |
- |
| M02 | Entity | Met: 51 of the 80 entity models bind a profile at 2026-09-28 (32 trading-instrument, 17 simple-lookup, 1 workspace-scoped-lookup, 1 trading-record); the survey measured 31 of the 65 org files on 2026-09-26. The other 29 entity models bind none, and every one is recorded as a no-match with its key shape, its feature values and its reason. The 8 field groups, the module and the component overview are not domain_entity models. Survey in the task's §1 |
PR 3 |
| M03 | Entity | Met: no model drift, one known exception elsewhere | - |
| M04 | Entity | Met: 80 entity models checked against org_loader.py's _FEATURE_NAMESPACE. Every stated feature sits in the drawer the loader reads (has_tenant_id and has_workspace_id at the entity root; party_id_from_session, system_scope, nullable_tenant_id and extra_checks under * SQL; has_change_reason_cache, has_pagination and has_uuid_primary_key in the Presentation drawer), and each bind is proven by the generated output. Zero inert assignments. has_parent_id, read_only, client_read_only, party_id_is_target and current_state are stated by no trading model. Task §5h |
PR 3 |
| M05 | Entity | Met: the 24 hand-written domain types with no model are classified – 5 are field groups, 9 are models to author (8 authored here, product_type waits for PR 4), 10 are messages (the instrument variants, the polymorphic container and the three *_data shapes), and none is infrastructure. No joined or denormalised shape is left as an entity. The five equity tables that hold two products are defect 9 and stay with PR 4. Task §3 |
PR 3 |
| M06 | Protocol | Met except two messages: all 85 protocol headers are generated and the twin gate reads 301 of 301 across 15 components. The two export verbs' request and response structs are a cpp block pasted into trade_protocol.hpp, which is P04's gap; capture model the export operations. Measured 2026-10-02 |
PR 7, capture |
| M07 | Entity | Not applicable: no junction metatype exists | Recorded in PR 1 |
| M08 | Entity | Met: defects 2, 3, 4 and 6 are closed – every date column is typed, 57 money columns are decimals and 30 non-money columns stay double, every closed set has a reference entity (the last, average_type, trading-local by decision), and the day count and payment frequency now have one name each. SQL spellings are canonical and flags are booleans. Task §5g, §5i |
PR 3 |
| M09 | All | Met: the 25 singular generator_facet_name values are plural, so the second generator tree is deleted and its 50 files are replaced under generators/; and the 5 presentation drawers that named Qt-era properties are gone. grep over the modeling directory finds no retired keyword. Task §0, §4 |
PR 3 |
| M10 | All | Met: composite overview with three parts; validate_docs passes | - |
| P01 | Protocol | Met in PR 2: every modelled family has its generated protocol, and the consolidated rates header is deleted | PR 2 |
| P02 | Protocol | Met: no hand-written file in the tree (C++, TypeScript or Python) names a trading.v1 subject; every subject is a generated nats_subject constant. Measured 2026-10-02 |
PR 2, PR 7 |
| P03 | Protocol | Met: 536 subjects across the protocol headers, no duplicate, and every message that carries one is served in the core or service sources; wire_trading_registrars.py --check reports all 12 late registrars wired, and V04 replays 449 answering. Measured 2026-10-02 |
PR 7 |
| P04 | Protocol | Not met: trading.v1.trades.portfolio.export and trading.v1.trades.export-to-storage are not entity verbs and are not operation models. Their structs are pasted from ores.trading.trade.org and their handlers are hand-written. Capture model the export operations |
Capture |
| P05 | Protocol | Met in PR 2: the shell and ORE consumers are migrated and nothing is carried for compatibility | PR 2 |
| P06 | Protocol | Not applicable: no warmed event cache | Recorded in PR 1 |
| G01 | Entity | Met in PR 2: the drift dry run reports nothing to change and nothing to create, on two runs | PR 2 |
| G02 | All | Met: survey B03 re-run on 2026-10-02 finds 2365 of 2448 C++ files generated and 83 hand-written. No hand-written file has a model or a generated copy, so generation supersedes none of them. The survey's path rule calls 27 of them generatable: the composition-root registrars, the two readers, the instrument variants and payload, and product_type, whose model is defect 10, open with the product work |
PR 2 to PR 7 |
| G03 | All | Met: 2365 files carry the generated marker and 83 do not, and the 83 are exactly the hand-written set B03 lists; the drift gate regenerates the marked files with no drift. Measured 2026-10-02 | PR 2 to PR 7 |
| G04 | Entity | Partly met: evidence/g04_generated_code_review.txt reads the generated trade_service.cpp against the hand-written bond reader. It reads like the baseline except one template defect: the stamp loop repeats its comment and reason expression once per member. Capture stamp loop comment; the fix needs a full regeneration with the pinned clang-format |
Capture |
| G05 | All | Met: each generator defect this story found is fixed at source with a regression test. Field-group equality: test_field_group_equality.py. Group-prefixed members: test_grouped_parent_seeding.py. Per-member audit stamp: test_service_write_stamps_audit.py. Shell aggregator: test_shell_command_aggregator.py. The enum write arm, the decimal sentinel and the TypeScript field-group projection had none, and gained test_enum_write_fields.py, test_shell_decimal_sentinel.py and test_field_group_typescript.py on 2026-10-02; each fails when its defect is put back |
PR 2 to PR 7, close-out |
| G06 | All | Met in PR 2: the api, core and service source lists are up to date | PR 2 |
| G07 | All | To honour: dry run first, never refresh while a defect is open | Policy |
| G08 | All | Met: core and service namespace headers; no api header, as in iam | - |
| W01 | Entity | Met in PR 2: 57 handler registrars, 31 history providers and 61 event mappings are composed; the read is evidence/pr2_w01_composition_read.txt |
PR 2 |
| W02 | Entity | Met in PR 7: the trading codes are seeded and the component is in the gate's registry, which reports 474 codes across 8 components | PR 7 |
| W03 | Entity | Met: populate references resolve | - |
| W04 | Entity | Met in PR 7: the database recreates from scratch and the fleet serves, and the component's suite passes against it | PR 7 |
| S01 | Entity | Met in PR 7 for the 56 entity models that enable the shell facet: the component-scoped aggregator archetype is built in codegen, every unit and test is generated and the hand-written units are deleted. Twenty-nine do not enable it and are the fact and child rows a parent security or trade owns, so they are addressed through the parent's menu. The strict reading, a menu per entity, is not met | PR 7 |
| S02 | Entity | Met in PR 7: 318 recipes in 265 groups, the inventory check passes, and the destructive verbs are declared destructive and recorded rather than run | PR 7 |
| H01 | All | Met in PR 7: audit record written, all three diagrams re-authored in two passes, all three rasters whole and read — the first render was clipped and reading it is what found out | PR 7 |
| H02 | All | Met in PR 6: the two generator trees have collapsed to one, and a scan finds no #if 0, no commented-out code, no work marker and no hand-written duplicate of a generated artefact | PR 6 |
| H03 | All | Met in PR 7: the pass changes nothing — the added comments sit above the code, narrate no edit and comment out no code | PR 7 |
| H04 | All | Met: only the standard _WIN32 export macro |
- |
| H05 | All | Met in PR 7: all four overviews carry the six sections and a current diagram, and the composite root gained its Diagram section with a rendered puml and png pair. The stale ER diagram is a repository-wide sweep, recorded as its own finding | PR 7 |
| V01 | All | Met in PR 7: the whole tree builds with the component's tests and every consumer, on the rebased tree | PR 7 |
| V02 | All | Met in PR 7: ctest -R ores.trading is 3 of 3 and ctest -R ores.shell is 16 of 16 with the fleet up, and the codegen suite is 1064 passed | PR 7 |
| V03 | Entity | Met in PR 7: compass db recreate -y -k from scratch after stopping the fleet, and the fleet starts and serves | PR 7 |
| V04 | Entity | Met in PR 7: 564 generated recipes, 454 replayed against the live fleet with 449 answering. It found three defects: twelve registrars no composition root called and a required decimal with no parsable sentinel, both fixed, plus two stale by-instrument-id scripts, since deleted, and three enum aborts recorded, and the loss of the hand-written basket editor found by review and restored by enabling the constituent's shell facet | PR 7 |
| V05 | All | Met in PR 7: every codegen gate is green with the component listed – component drift, model drift, pasted block symbols, protocol twin coverage, handler permissions, populate references, recipe inventory, test case reachability, sql hygiene, exported types, enum attributes, route auth, shell menus and validate_docs | PR 7 |
| V06 | All | Met in PR 7: trading-cpp is listed, --all exits 0 with no drift, the twin gate is 293 of 293, the registry gate is 16 components and 5 exceptions |
PR 7 |
| V07 | All | Met in PR 7: the census over 1175 cases went from 71 with no strong assertion to 0, with no case deleted and none weakened | PR 7 |
| V08 | All | Met in PR 7: 456 of 1062 sources reached; of the 606 untouched, 593 are generated and 13 hand-written and each is recorded with its reason | PR 7 |
10. Decisions
- D1, the rates wire. The nine consolidated rates families ship as
generated per-family protocols. The hand consolidation is deleted:
the per-family sections of
instrument_protocol.hpp,rates_instrument_handler.hpp, andregistrar_rates.cppis rewritten to call the nine generated registrar functions. The generated protocol renames two verbs per family,savetoputandhistoryto_versions.list, so the nine hand-written shell units and the ORE import handler migrate in the same PR. The alternative, making consolidation a model decision, needs generator work and is recorded as a later option rather than taken here. Reason: the generator already emits the per-family shape, and leaving both shapes in place registers the same subjects twice. - D2, the instrument identity. Implement the documented rule. The trade id identifies the trade and its instrument. The security issue is a separate reference datum. Collapse the identity first, then split the bond security, in that order, because the reverse reshapes the same tables twice.
- D3, the unmodelled families. Model all of them in PR 3: commodity, composite, composite leg, credit, scripted, swap legs and product type. The registry entry depends on it. Record each as a profile no-match with its feature values where no profile fits.
- D4, M07. Record the item as not applicable, because the component
has no junction metatype. Record that
bond_leg,trade_party_role,trade_identifierandinstrument_optionare junction shaped, so a later reclassification opens the item rather than leaving it silently skipped. - D5, the sixteen trade model defects. Fold the model-shape defects into PR 3 and PR 4 as the table above assigns them. Leave defect 11 and defect 12 with the reference-data and dq work and record them as out of scope here.
- D6, the stale remote branch. Record
feature/trading-model-cleanupand do not delete it. It is not this worktree's branch, and the story it scaffolds is superseded. - D7, the shape of PR 1. Documentation and evidence only, so that the decisions are on the record before 1751 files move.
- D8, how a rollback names a trade. The trade model declares its wire
key as
external_id, so the canonical protocol cannot remove a row by the uuid the import rollback held. Decided, on the reviewer's ruling, to record the external ids: the handler has the value in hand at the save site, so it stores the external identifier, and the rollback builds atrade_removalwhose key is that identifier. Because the field now carries a key and not a uuid, the ores.ore model renames itsaved_trade_external_ids, and its generated C++ and TypeScript and the hand-drawn diagram follow it. The trading protocol does not change. The trading model changes in one place: the trade key's fixture generator gains a value, which is defect 17's PR 2 half; no column and no shape change. The other two options stay recorded: declaring the storage key as the wire key is PR 3 model work, and a removal by storage key is a generator change. If PR 3 settles the key the other way, the handler change is what reverts, and it is one line. The key's nullability is the same thread and is recorded as defect 17. - D9, the shape of the rollback's saved lists. The rollback request
carries one string list per entity kind:
saved_currency_iso_codes,saved_portfolio_ids,saved_book_idsandsaved_trade_external_ids. A single ordered collection of kind-and-key entries is the cleaner shape long term: an enumerated kind with its keys, or a flat list of key-value pairs. Adding a kind then changes one model field instead of four lists, two handlers and a wire name, and the order the rollback traverses becomes explicit. It is deferred, because the rollback must dispatch a different delete request per kind whatever the payload shape, so a generic bag trades a typed field for a stringly kind and moves exhaustiveness from the compiler to a switch. Recommendation: take it up when the ore import workflow is next revised, not inside PR 2. Recorded as a capture on the product backlog. - D10, the swap legs shape. One model over the shared table, not one
per family.
ores_trading_swap_legs_tblis one table with one surrogate key, one parent column (instrument_id) and aleg_type_codediscriminator; the leg type is data, not a table. The generator holds one entity per table, so several models would each claim the same DDL and drift against each other. The model isswap_leg, child-shaped: uuid key, tenant and workspace scope, a parent foreign key to the instrument. PR 3 authors it. - D11, monetary amounts. An amount is a decimal, built on
boost::multiprecision, never adouble. The database already stores these columns asnumeric, so the SQL does not change: the C++ domain type, the JSON and table I/O, the mapper and the generator's type separation move to it. The 87doublecolumns that carry a price, strike, premium, payout or notional adopt it; rates, volatilities and correlations staydouble, because they are not money. The type lands before the columns move, and the generator must first separate an entity's type from its domain type, which is the same seam the plain date column needs. The wire carries a decimal as its exact text, read and written by a customrfl::ReflectorwithReflType = std::string, the way dates and uuids already travel. The repository entity keeps a string member, because sqlgen binds a fixed set of types andboost::multiprecisionis not one of them; the mapper parses and formats at that boundary. - D12, collections. The trading domain model carries no JSON
collection. A collection is a child table with a foreign key to its
parent. The four columns that still hold JSON –
call_dates_jsononcallable_swap_instrument,basket_jsononcommodity_instrumentandequity_swap_instrument, andoption_data_jsononequity_position_instrument– become child entities, and any other serialised collection the census finds goes the same way. The model states the shape and the database enforces it. - D13, enumerations. A closed set of values gets a reference table and a foreign key, and its check leaves the PL/pgSQL functions for the schema. The missing reference tables are populated from the standard, FpML or ORE, never invented; where the data belongs to another component the dependency is recorded instead of a local copy. This closes defects 4, 5 and 8 as one thread.
- D14, the instrument key. The instrument's key is the trade it belongs
to.
instrument_identitydropsinstrument_id, every instrument table is keyed by a non-nulltrade_id, every fact and child table that named an instrument names that trade, andtrade.instrument_idis deleted. Reason: ORE carries two identities, the trade and the security, and a one-to-one relationship modelled as two nullable pointers has nothing that can make them agree. This closes defect 13. - D15, the relationship the schema can hold. The instrument-to-trade
link is declared in the model and enforced by the generated insert
trigger's existence check (errcode 23503), not by a DDL
referencesclause. A foreign key cannot be held against a bi-temporal parent: the only unique index on(tenant_id, id)is partial, and PostgreSQL refuses a foreign key to a partial unique index, which was proven against the live database. The shape that would allow one – the child carrying the parent version'svalid_fromand the insert trigger deriving it instead of stampingclock_timestamp()– pins the two version histories together and is recorded rather than taken here. Defect 7 is partly closed by this and the remainder is recorded. - D16, the standalone instrument. An instrument with no real trade is
expressed as a trade of a reserved type, so there is one shape and no
nullable back door. The nullability is gone with D14; ORE's own
placeholder type is
Failed, deliberately absent from the product catalogue, and no caller in the tree writes an instrument without a trade after the collapse, so no reserved type is seeded. - D17, the bond security split moves out of PR 4. It reshapes the bond
family around a security identity and is the largest part of the diff,
so it becomes
task_split-bond-security-from-trade.organd PR 5. PR 4 stops after the collapse with the tree green, which the task's own note authorises. The story ladder grows by one: the split is PR 5, the bond pilot PR 6 and the shell and verification work PR 7. By the owner's ruling of 2026-09-28, defects 14 and 16 move to the bond pilot and defect 15 to the split. - D22, how the sweep holds the corpus to one statement per security.
The round-trip gate imports one document and exports it again, so the
source it compares has to be a document the database can hold. The
corpus is not one: it states five security ids across ten documents
that disagree about the issuer, the credit curve, the issue date and the
leg schedule, and about 117 trades name
SECURITY_1between them. Our model holds one issue per security – that is the split's point – so a comparison against every restatement reads the corpus's own contradiction as a loss. The sweep therefore folds the union before it imports it: the first trade that names a security states the issue, and every later trade is rewritten to that statement, leaving the two members the model keeps on the trade's own instrument,SecurityIdandBondNotional, as the trade stated them. The sweep prints what it overrode, by member, and--raw-corpusskips the fold so the contradiction can be measured. The gate is then a statement about the code, and the contradiction is recorded as the corpus's own. - D23, the KnockOutSwap's home.
swap_legnames the knock-out swap among the nine rates families whose legs it holds, andknock_out_swap_instrumentholds the barrier, the start and the maturity as typed columns. The import sent the type toscripted_instrumentinstead, where the closed trade-type check refused the row, so the corpus's one trade of the type never reached the database and the export dropped the trade it could not rebuild. KnockOutSwap is routed to the swap family, which is what the legs table states, andscripted_instrumentstops naming it. ORE's own AmericanMonteCarlo readme lists KnockOutSwap under "Scripted Trade Types (AMC)", which is how the wrong home was available; D12's ban on a JSON collection decides it the other way, because the type's twoLegDataelements have nowhere to go in a flat scripted row. - D24, what an empty element states. ORE's
datetype spells an unset date as the empty string and the elements that carry one areminOccurs"0", so a document may write an unset date as an empty element or omit the element, and the corpus does both: 414 =FirstDate=/=LastDateelements corpus-wide are empty and 278 are absent. Both spell the same statement – ORE's own reader resolves either to an unset date – so the gate compares content rather than element presence: an empty element is no difference whichever side states it, and a boolean spelling is paired first, because an empty element is also how ORE spellstrueand that one has to be echoed as a boolean. The model therefore needs one state and not three, and no presence flag is modelled for an unset date. Where a member's value equals its type's default, the optional column is the presence record: absent is null and a statedfalseor0is the value, which is what the bond future's two optional booleans rely on (definition of done, presence bullet). - D25, the bond future's columns. The synced XSD is the ground truth and
the table is older than it.
bondFutureDatastates three required members –ContractName,ContractNotionalandLongShort– and two optional booleans,ApplyConversionFactorandUseFuturePrice, which the mapper has never read and the table has never held. The other thirteen columns leave the table: twelve of them are elements ofbondFutureReferenceDatum, which v17 moved the contract terms to, and the thirteenth,fair_price, is not in the schema at all. None of them can be read from a trade, because the reference datum reaches only theReferenceDatadocument root and the generated ORE bindings cover thePortfolioroot, soinput.xsdnever emits the type. The shared entity that will hold them is keyed by the contract name, the waybond_issueis keyed by the security id, and it lands with the reference-data import; recorded rather than invented.bond_future_delivery_basketis in the same position and is retired separately: ORE'sDeliveryBasketis that datum's element, the table is keyed by the trade, and the exporter never writes it, so nothing fills it from a trade and nothing reads it back. Removing a generated entity touches five components and no tool prunes the files it leaves behind, so it is its own unit rather than a footnote of this one. The two optional booleans are nullable, so a statedfalseand an absent element stay different values, which is D24's rule. - D26, the underlyings table. D21 settled its shape – keyed by the
trade, holding an oresmd URI and an optional weight – and left the
second arm, for an underlying that is itself a product, to the pilot. The
pilot records the table as out of scope for the bond rebuild, with the
reason: no bond product states an underlying.
underlyingTypesis referenced by eleven sites and every one is another family's –varianceSwapData,cliquetOptionData,settlementData,pricesType,equityOptionData,eqBarrierOptionData,equityFutureOptionData,eqDigitalOptionData,eqTouchOptionData,equityForwardDataandgenericBarrierOptionData– while the bond family's own types (bondData,bondOptionData,bondFutureData,bondRepoData,convertibleBondData,callableBondDataandascotData) state none. A table built here would have no producer and no consumer, which is the shape this pilot is retiring elsewhere (bond_future_delivery_basket). The defect 15 fix is a producer change across nine families and about sixteen trade types, which the split task already recorded as not a bond reshape. D21's shape stands as the design for that work; the second arm is recorded as a kind that says URI or product, the URI, and the product's own name. - D27, how a delete removes the family. The row a caller deletes is the
header of a family, so the store closes the family with it rather than
leaving it to each caller to remember. An entity names the rows it owns
in a
* Delete cascadesection – a table and the column in it that names the entity's key – and the generated delete rule calls a generated function that closes them. An optional:cascade_orphan:on a foreign key takes the referenced row too when the deleted row was the last to name it, which is what a parent shared by security id needs. The cascade is a plpgsql function the rule calls and not a rule body, because a rule resolves the tables it names when it is created and a child may be created after its parent. The rule is D15's, applied to the delete side: the relationship is enforced where the schema can hold it, so the shell verb, the import rollback and every later caller get it without repeating it. - D28, what defect 14's coverage declaration now says. Every instrument
table but one carries a
trade_type_codeconstraint, and the union of the lists covers every code inores_trading_trade_types_tbl, so a generator can ask each table what it holds. The bond table is the tightest:bond_instrumentsnames exactly the ten bond codes. But ten tables – the rates, credit and commodity general tables – share one identical list of all 121 codes, so their lists do not separate the products they hold from the ones they do not, andrpa_instrumentscarries no constraint at all because nothing writes it: the mapper sendsRiskParticipationAgreementtocredit_instruments. The defect's prescription, "declare coverage everywhere first", is met; what remains is giving each table its own list and deciding the unproduced table, which is the exclusive-or split defect 9 asks for and belongs to the families that own those tables, not to the bond pilot. - D29, where defect 16's observation schedule lives.
instrument_schedulesholds one row per schedule an instrument's owner states, keyed by the trade, the owner, the schedule's role and the entry's ordinal, withschedule_kindchoosing the rule arm or the date arm andschedule_rolenamingschedule,payment_schedule,fixing_scheduleorreset_schedule. It is keyed by the instrument rather than by a bond row, so any family whose product states a schedule writes there without a new table; the bond family's trade-owned schedules already do, and its security-owned leg schedules live inbond_issue_leg_schedule. The scalars the defect names are still spread over four tables (commodity_instruments,equity_asian_option_instruments,equity_accumulator_instrumentsandequity_option_instruments), and the twelve products that need an observation schedule are equity, fx and commodity. Moving those scalars into the shared table is those families' reshape work, not the bond pilot's. - D30, the story closes with two accepted exceptions. The owner ruled on 2026-10-02 to close the story with P04 and G04 open, each with a capture. P04: the two export verbs are not operation models (capture); the same two messages are M06's only gap. G04: the stamp loop in the service template repeats its comment and reason expression once per member (capture). Its fix regenerates every grouped entity in every component and needs the clang-format that CI pins. Reason: both are bounded, recorded and owned by a capture, and neither changes behaviour. Every other applicable row is met, measured on main on 2026-10-02.
11. Delivery plan
| PR | Task | Lands | Gate |
|---|---|---|---|
| 1 | Baseline, decisions and evidence for ores.trading | The analysis, the B01 to B06 evidence and the decisions | compass lint, validate_docs.sh, drift unchanged |
| #2204 | Sync the ores.trading tree to its models | The generated tree, the canonical protocol, the canonical triggers, the deletions | Whole-tree build, four suites green, drift dry run empty, D1 applied |
| #2333 | Model the unmodelled trading families and bind the unbound | The missing orgs and their ports, the profile bindings and no-matches, the closed sets (defects 4, 5 and 8), the defect 6 renames, the M04 sweep, the local average_type with its producer fix, the call_type deletion, and defects 14 to 16 carried to PR 4 |
Merged as 9631df6bff on 2026-09-28 with fifteen checks green. compass db recreate -y -k exit 0; the whole-tree build exits 0 with zero error:; the full ctest suite 89 of 89 on a fresh database with the fleet up; pytest projects/ores.codegen/tests -q 978 passed; check_component_drift.py --component trading-cpp --dry-run twice, --all --dry-run and --sweep --dry-run, all no drift; validate_docs.sh 40 components |
| #2348 | Collapse the instrument identity into the trade | One instrument identity, keyed by the trade, with the relationship declared where the schema can hold it (D14 to D16). The bond security split moved to PR 5 by D17 | Merged as cd00344ddb on 2026-09-29 with fifteen checks green. Rebased onto e2bda9e108. The whole-tree build exits 0 with zero error:; compass db recreate -y -k exits 0; the fleet restarts to 25 of 25; ctest is 89 of 89; pytest projects/ores.codegen/tests -q is 995 passed; component drift, --all --dry-run and --sweep --dry-run clean; check_sql_hygiene.py 1676 files, 0 problems; validate_docs.sh 40 components |
| #2386 | Split the bond security from the trade | The shared bond entities keyed by the ORE reference datum id, the leg family on the security, bond_issue with the six fields ORE has and without bond_notional, the coupon terms held once, and defect 15 decided as D21 |
Merged as 710bd22883 on 2026-09-30 with fifteen checks green. The whole-tree build exits 0; compass db recreate -y -k exits 0; ctest -j 1 is 89 of 89 on the recreated database; pytest projects/ores.codegen/tests -q is 1016 passed; component drift, --all --dry-run and --sweep, are clean; check_sql_hygiene.py 1770 files, 0 problems; validate_docs.sh 40 components. The round trip is blocked before it reaches any bond code and is recorded on the bond pilot |
| 6 | Finish the bond pilot | The v17 bond future, the missing child table and constraints, the delete and add paths, the corpus sweep, the underlyings table D21 decides, and defects 14 and 16 | The corpus-wide round trip, drift empty |
| 7 | Shell, wiring, hygiene and verification for ores.trading | Shell units and recipes, permissions, audits, surveys, the registry entry | Every gate green with the component listed, --all passes |
12. Risks
- The PR 2 regeneration is large. A whole-tree build and the four suites, not the diff, are the evidence that it is safe.
- Regenerating the nine rates families while the consolidated handler remains would register the same NATS subjects twice. D1 must be applied inside PR 2, not after it.
- The identity reshape in PR 4 is the largest model change. It must land in one PR with its regeneration, and the ORE round trip must stay at zero loss.
- The in-place drift check diffs the whole repository, so any untracked
file makes it fail. Use
--dry-run, keep the tree still, and never run a writing gate while a build reads the tree. - Eleven untracked shell library directories exist from the scheduler pass. They are the recorded open item 10 of that task and they break an in-place drift check until they are swept.
13. Out of scope
- The reference-data and publish defects 11 and 12 of
trade_model_defects.org. They belong to the dq and reference-data work. - The Qt estate. It was deleted at
af67db3062. - Numerical kernels. Trading consumes their results.
- The models of other components, except the org keys the trading models need.
- The stale
ores_schema.pumlER diagram, which is a repository-wide sweep and not a trading fix. PR 6 records it.
14. Result
ores.trading meets the Component Clean Standard, with two accepted exceptions (D30). Seven PRs delivered it, #2496 the last of them.
- The component is in
COMPONENTS_UNDER_TEST, and the drift gate checks it at every address with no drift. 2365 of its 2448 C++ files are generated; the 83 hand-written ones are the composition root, the two readers, the instrument variants, the service host and the tests. - All 85 protocol headers are generated and twinned in TypeScript. No hand-written file names a subject.
- The shell surface, its recipes and its inventory are generated. The V04 replay answers 449 of 452 replayed recipes; the three that do not are the enum sentinel capture.
- Every generator defect the story found is fixed at source with a regression test in the codegen suite.
- Open: P04, the two export verbs as operation models, and G04, the stamp-loop template defect. Each has a capture in the inbox.