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, and check_component_drift.py --all passes.

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.org declares instrument_id as a key and trade_id as an optional soft back-reference. Twenty-six models inherit the group.
  • ores.trading.trade.org adds instrument_id to the trade, and trading_trades_create.sql declares it uuid null.
  • The declaration carries 91 instrument_id columns, 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 instrument entity 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_underlyings table was never built.
  • bond_legs and its rate, amount and amortization children are still keyed by instrument_id, so bond static data is stored once per trade rather than once per security.
  • No REFERENCES constraint exists in any bond script, so the design's foreign key for issue_id is a comment rather than a constraint.
  • Eight bondData elements still have no column: CreditGroup, VolatilityCurveId, PriceQuoteMethod, PriceQuoteBaseValue, PriceType, Payer, CreditRisk and SubType.
  • bond_futures is a pre-v17 table with 15 columns while the current instruments.xsd declares 5 members. ApplyConversionFactor and UseFuturePrice have 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, and registrar_rates.cpp is rewritten to call the nine generated registrar functions. The generated protocol renames two verbs per family, save to put and history to _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_identifier and instrument_option are 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-cleanup and 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 a trade_removal whose key is that identifier. Because the field now carries a key and not a uuid, the ores.ore model renames it saved_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_ids and saved_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_tbl is one table with one surrogate key, one parent column (instrument_id) and a leg_type_code discriminator; 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 is swap_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 a double. The database already stores these columns as numeric, 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 87 double columns that carry a price, strike, premium, payout or notional adopt it; rates, volatilities and correlations stay double, 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 custom rfl::Reflector with ReflType = std::string, the way dates and uuids already travel. The repository entity keeps a string member, because sqlgen binds a fixed set of types and boost::multiprecision is 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_json on callable_swap_instrument, basket_json on commodity_instrument and equity_swap_instrument, and option_data_json on equity_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_identity drops instrument_id, every instrument table is keyed by a non-null trade_id, every fact and child table that named an instrument names that trade, and trade.instrument_id is 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 references clause. 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's valid_from and the insert trigger deriving it instead of stamping clock_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.org and 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_1 between 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, SecurityId and BondNotional, as the trade stated them. The sweep prints what it overrode, by member, and --raw-corpus skips 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_leg names the knock-out swap among the nine rates families whose legs it holds, and knock_out_swap_instrument holds the barrier, the start and the maturity as typed columns. The import sent the type to scripted_instrument instead, 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, and scripted_instrument stops 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 two LegData elements have nowhere to go in a flat scripted row.
  • D24, what an empty element states. ORE's date type spells an unset date as the empty string and the elements that carry one are minOccurs"0", so a document may write an unset date as an empty element or omit the element, and the corpus does both: 414 =FirstDate=/=LastDate elements 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 spells true and 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 stated false or 0 is 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. bondFutureData states three required members – ContractName, ContractNotional and LongShort – and two optional booleans, ApplyConversionFactor and UseFuturePrice, which the mapper has never read and the table has never held. The other thirteen columns leave the table: twelve of them are elements of bondFutureReferenceDatum, 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 the ReferenceData document root and the generated ORE bindings cover the Portfolio root, so input.xsd never emits the type. The shared entity that will hold them is keyed by the contract name, the way bond_issue is keyed by the security id, and it lands with the reference-data import; recorded rather than invented. bond_future_delivery_basket is in the same position and is retired separately: ORE's DeliveryBasket is 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 stated false and 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. underlyingTypes is referenced by eleven sites and every one is another family's – varianceSwapData, cliquetOptionData, settlementData, pricesType, equityOptionData, eqBarrierOptionData, equityFutureOptionData, eqDigitalOptionData, eqTouchOptionData, equityForwardData and genericBarrierOptionData – while the bond family's own types (bondData, bondOptionData, bondFutureData, bondRepoData, convertibleBondData, callableBondData and ascotData) 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 cascade section – 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_code constraint, and the union of the lists covers every code in ores_trading_trade_types_tbl, so a generator can ask each table what it holds. The bond table is the tightest: bond_instruments names 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, and rpa_instruments carries no constraint at all because nothing writes it: the mapper sends RiskParticipationAgreement to credit_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_schedules holds one row per schedule an instrument's owner states, keyed by the trade, the owner, the schedule's role and the entry's ordinal, with schedule_kind choosing the rule arm or the date arm and schedule_role naming schedule, payment_schedule, fixing_schedule or reset_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 in bond_issue_leg_schedule. The scalars the defect names are still spread over four tables (commodity_instruments, equity_asian_option_instruments, equity_accumulator_instruments and equity_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.puml ER 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.

Emacs 29.3 (Org mode 9.6.15)