Task: Bring the bond mapper roundtrip to zero loss
Table of Contents
- 1. Goal
- 2. Status
- 3. Acceptance
- 4. Plan
- 5. Notes
- 5.1. Wave A.1, the presence pass
- 5.2. Wave A.2a, the leg scalars
- 5.3. Wave A.2b, the leg groups
- 5.4. Wave A.3a, the bond members the issue row has no column for
- 5.5. Wave A.3b, the per-product residue
- 5.6. Wave A.3c, the option block
- 5.7. Wave A.4, the trade envelope
- 5.7.1. The envelope is keyed by the trade, not by the product
- 5.7.2. The carrier and the mapper pair
- 5.7.3. The shell writes the trades no reverse mapper can rebuild
- 5.7.4. The count that has to be maintained by hand
- 5.7.5. The measured effect
- 5.7.6. The trade count now matches the written documents
- 5.7.7. The literal diff is ordering, tag spelling and number formatting
- 5.7.8. The database still has no home for the envelope
- 5.7.9. The full suite is green, and the flake did not recur
- 5.7.10. No generated code was touched
- 5.7.11. What the gate's exit code counts, and why it is not zero
- 5.7.12. Finding: the puml generator cannot reach the nested components
- 5.7.13. Finding: the roundtrip classifies a document by substring, and rewrites what it misfiles
- 5.8. Wave B.1 and B.2, the tables that hold what the container carries
- 5.9. Wave B.3, the schedule reshape and the option block
- 5.10. Wave B.4, the forward, the future basket and the TRS residue
- 5.10.1. The return side states three members no row held
- 5.10.2. Finding: the forward's two date columns were typed as dates
- 5.10.3. The forward block, member by member
- 5.10.4. The delivery basket is a keyed child table
- 5.10.5. Finding: the new tables had no row-level security
- 5.10.6. The tables stay empty until B.5
- 5.10.7. No generated file was edited by hand
- 5.11. Wave B.5, the mapper switch-over to the tables
- 5.11.1. Finding: the sixteen new tables have no nats surface
- 5.11.2. Finding: the read path needs a query shape the generator does not emit
- 5.11.3. Finding:
trade_handler.hppis a generated path kept by hand - 5.11.4. Finding: the sixteen tables are absent from the development database
- 5.11.5. The switch-over lands in four parts
- 5.11.6. Wave B.5b, the identity on the write path
- 5.11.7. Decision: an issue is found by security id, never minted per trade
- 5.11.8. The container holds no second copy of a bond-level member
- 5.11.9. Finding: no case covered the two new child readers
- 5.11.10. Finding: the fact row is chosen by the instrument's type code
- 5.11.11. What B.5b does not carry
- 5.11.12. No generated file was edited by hand
- 5.11.13. The bond read assembly becomes a reader service
- 5.11.14. Finding: the case for two instruments of one issue caught a real defect
- 5.11.15. Finding: the in-place refresh cannot add a column to an existing table
- 5.12. Wave B.5c, the leg family and the shared schedule tables
- 5.12.1. The six tables get a surface
- 5.12.2. The six reads the generated repository does not offer
- 5.12.3. The reader rebuilds the legs
- 5.12.4. The writer saves the legs
- 5.12.5. Finding:
payment_datesis a fifth schedule role - 5.12.6. Two type wrinkles at the column boundary
- 5.12.7. What B.5c does not carry
- 5.12.8. No generated file was edited by hand
- 5.13. Wave B.5d, the option block, the forward and the residue
- 5.13.1. The seven tables get a surface
- 5.13.2. The seven reads the generated repository does not offer
- 5.13.3. The reader rebuilds the option block and the residue
- 5.13.4. Three members live on the fact row and nowhere else
- 5.13.5. The writer saves them
- 5.13.6. The gate reads zero, and it exits zero
- 5.13.7. Finding: the check counted a document the exporter never writes
- 5.13.8. Finding: the currency maps were missing two codes
- 5.13.9. Finding: the exporter classified a document by substring
- 5.13.10. Finding: the exporter's classification has no test
- 5.13.11. What the two repairs moved, and what they did not
- 5.13.12. What B.5d does not carry
- 5.13.13. No generated file was edited by hand
- 5.14. Verification
- 5.14.1. Wave B.5d, the build and the sweep
- 5.14.2. Finding: the refdata core suite dies on its own failure path
- 5.14.3. Wave B.5c, the build and the sweep
- 5.14.4. Finding: the listener loss-window test fails under load
- 5.14.5. The reader extraction, the build and the sweep
- 5.14.6. Wave B.5b, the build and the sweep
- 5.14.7. Wave B.5a, the build and the sweep
- 5.14.8. Wave B.4, the build and the sweep
- 5.14.9. Wave B.3, the build and the sweep
- 5.14.10. The trading component is not drift free
- 5.14.11. The new files had to be registered, and the component overshoots
- 5.14.12. A test case costs one second of dead time
- 5.14.13. The sweep is green, all seventy one suites
- 6. Test Scenarios
- 7. PRs
- 8. Review
- 9. Result
This page documents a task in the Redesign ores.trading on data-oriented principles story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
The bond products and the trade envelope round trip at zero loss on the mapper path, and everything the container carries has a home in the database so the full XML to database to XML round trip can be lossless.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Redesign ores.trading on data-oriented principles |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-12 |
3. Acceptance
scripts/ore_mapper_roundtrip_diff.pyreports zero lost pairs and zero unexplained pairs under the bond products and the trade envelope, over every document inexternal/ore/examples.- The script reports the other instrument families as counted debt. It does not fail the run for them.
- The container carries every member the mapper needs, so the mapper path loses nothing. The generated binding stays untouched.
- Every member the container carries has a column or a keyed child row, so the database can hold what the container holds.
4. Plan
Four waves of container and mapper work, then the tables. Each wave ends green, and each wave is a commit.
The mapper path is in-memory: exporter::roundtrip reads a document
and maps it straight back out, with no database anywhere on the path.
The container alone can therefore make the mapper path lossless.
Two root causes hold the losses. The container flattens
xsd::optional<T> into a plain member and tests .empty(), so an
element the document states empty collapses into an absent element.
And the container never held most of legData, so the reverse mappers
rebuild a leg from the issue row and lose everything the row has no
column for.
| Wave | Scope | Pairs |
|---|---|---|
| A.1 | Presence pass: the container carries std::optional, the mappers emit on presence |
1,779 |
| A.2 | bond_leg_data grows to mirror legData: the scalars, then the nested groups |
~780 |
| A.3 | The bond-level residue: calendar, curve identifiers, BondNotional |
~990 |
| A.4 | The trade envelope: CounterParty, AdditionalFields, party_id, valuation_date |
6,510 |
| A.5 | The diff script separates the bond scope from the other families | gate |
The container is the specification for the tables. Unit B authors the
shared instrument-keyed schedule tables and the tables for the rest of
the residue, as modeling orgs under projects/ores.trading/modeling/
and generated, never hand-written. Unit B lands after the whole of the
container growth, so one pass adds every table the container needs.
The scope is measured now, not assumed. bond_schedule_data carries 136
members and no table holds any of them: the only references to the type
outside its own header are the two members of bond_instrument_data,
so the schedule lives in memory alone. Of the 105 create scripts under
projects/ores.sql/create/trading/, none names a schedule, an envelope,
an amortization, a notional or a payment.
The envelope already has a relational design, and it disagrees with the carrier wave A.4 built:
| Carrier member | Schema today | Gap |
|---|---|---|
netting_set_id |
trades.netting_set_id |
closed |
counter_party |
resolved through a party role to refdata_counterparties by INNER JOIN |
a name that resolves to no counterparty row is dropped |
portfolio_ids, unbounded |
one trades.portfolio_id |
a document naming several loses all but one |
additional_fields |
no table and no column | no home at all |
ores_trade_ore_envelope_vw, in trading_ore_envelope_view_create.sql,
is the existing statement of that design. Unit B reconciles it with the
carrier rather than designing the envelope again from nothing.
Two further facts fix the shape of the work. A field group flattens into
its entity with rfl::Flatten<T>, so no list member can live in a field
group and every list needs a keyed child table of its own. And the export
handler builds trade_export_item from the trade row alone, so it cannot
fill the carrier until those tables exist.
Unit 3 of the story, the qt bridge, stays BACKLOG until this task closes.
5. Notes
5.1. Wave A.1, the presence pass
5.1.1. The container carries std::optional
Every member the schema declares optional is now an std::optional, so
an element the document states empty stays distinct from one it omits.
The reverse mappers emit on presence rather than on content, and a flag
emits the canonical Y or N because the bool type has no empty
spelling to hand back.
The measured effect over external/ore/examples, both trees read with
the same classifier:
| Measure | Before | After |
|---|---|---|
| In-scope lost pairs | 9,593 | 8,979 |
| In-scope unexplained pairs | 47 | 47 |
BondData/LegData/ScheduleData/Rules/FirstDate |
278 | 3 |
BondData/.../LastDate |
278 | 3 |
ForwardBondData/BondData/.../FirstDate |
34 | 3 |
ForwardBondData/BondData/.../LastDate |
34 | 3 |
The full by-path comparison shows no path got worse. Every other path holds the same count in both trees.
5.1.2. A bool element the document states empty reads as on
The generated reader pre-fills the value:
domain.cpp _get_scheduleData_Rules_t_EndOfMonth emplaces
domain::bool_(), whose value is index 0, bool_::Y. So an empty
EndOfMonth already read as on before this wave, and the 307 pairs the
classifier tolerates are correct. flag_of(bool_) now reads the empty
spelling as on as well, so the two agree whatever the default becomes.
The suite asserts the mechanism directly, in
an_empty_schedule_element_survives_the_round_trip.
EndOfMonth is the only bool-typed element the corpus exercises, at
3,753 empty, 103 false, 12 true and 2 N across all documents. The
other two (AdjustEndDateToPreviousMonthEnd, IncludeDuplicateDates)
appear in no document. Wave A.3c re-measured this and corrects the
empty count, which A.1 recorded as 3,442. The false and true counts
reproduce exactly, so the corpus has not moved between the two waves.
5.1.3. A leg with neither currency nor notional is a document the schema allows
Currency and Notionals are minOccurs"0", and the reverse mapper
emitted the whole =LegData only when one of them, or the issue's face
value, was present. A leg the document carries with neither came back as
no leg at all. The guard now asks the container whether it holds a leg,
through bond_leg_data::is_empty(), so a member added to the leg
reaches the guard in one place. The new test found this.
5.1.4. Finding for A.2: the second leg of a bond
Nine BondData blocks in three documents carry two LegData elements:
Cash_Bonds.xml, Legacy/Example_18/Input/portfolio.xml and
Products/Input/portfolio.xml. The container holds one leg and the
issue row holds one leg's terms, so the second leg is dropped whole.
The residual 3 lost pairs at each of the four schedule paths above are
this class, and they are the visible tip: the second leg's other pairs
match pairs other trades in the same document state, so the multiset
comparison hides them.
A "Fixed then Floating" bond is the shape: leg 0 is Fixed with a
EUR notional to 2035, leg 1 is Floating from 2035 to 2040 with its
own FloatingLegData. Giving the container a leg list is not enough;
the nine tables have no home for a second leg's rate and currency, so
the model needs a decision in Unit B.
5.1.5. Latent bound: an empty convention, term convention or rule
Convention, TermConvention and Rule are parsed into canonical
enumeration spellings through parse_code, so a document that states
one of them empty would still lose it. No document in the corpus does,
and all 1,566 were checked.
5.2. Wave A.2a, the leg scalars
5.2.1. The container mirrors the scalars legData states
bond_leg_data held three members: the payer, the leg type and the
schedule. It now holds eleven. The payment convention, the payment lag,
the notional payment lag, the payment calendar, the day counter, the
last period day counter and the strict notional dates join them, and
every member is an optional.
Two of the new members, the currency and the day counter, already had an issue column. The mapper mirrors them onto the row and the leg holds the document's own statement. Export prefers the document's value and falls back to the row, which is the path a payload built from a row set takes. The other two legs, the repo leg and the TRS funding leg and the ascot swap leg, have no issue row at all, so the leg is their only home.
5.2.2. map_leg carries the leg type
The three legs with no fact row dropped their type: reverse_leg wrote
a default and the reverse mappers for the repo and the TRS then
overwrote it from the fact row. The Ascot swap leg had neither, so a
Floating swap leg came back as Fixed. map_leg now carries the
type, and the repo and TRS reverse mappers assign from the row only when
the container holds no leg.
5.2.3. The issue row writes the day counter back
map_bond_data mirrored the coupon leg's day counter onto the issue row
and reverse_bond_data never read it back, so all 314 pairs were lost
at the write. The reverse now reads it, and the currency with it.
5.2.4. The measured effect
Both trees were read with the same classifier.
| Measure | A.1 | A.2a |
|---|---|---|
| In-scope lost pairs | 8,979 | 8,333 |
| In-scope unexplained pairs | 47 | 44 |
BondData/LegData/DayCounter |
280 | 3 |
BondData/LegData/PaymentConvention |
280 | 3 |
ForwardBondData/BondData/LegData/DayCounter |
34 | 3 |
ForwardBondData/BondData/LegData/PaymentConvention |
34 | 3 |
BondTRSData/FundingData/LegData/Currency |
4 | 0 |
BondRepoData/RepoData/LegData/Currency |
2 | 0 |
AscotData/ReferenceSwapData/LegData/LegType |
3 | 0 |
646 pairs came back, no path got worse, and ten paths left the list whole. The three unexplained pairs that came back are the Ascot leg type pairs, which were both lost and unexplained.
The residual 3 at each of the four schedule paths is the second-leg class the A.1 note describes.
5.2.5. Decision for A.2b: the container takes the leg list
Three documents carry two LegData elements in one BondData, and the
schema declares LegData unbounded. A container that holds one leg
cannot round trip those documents, so the list belongs in unit A and not
in the recorded debt. It waits for A.2b because the second leg's rate and
index live in legDataType, which A.2b adds: a list without it would
lose the second leg's rate anyway.
The other three leg slots stay single. The schema declares one
FundingData/LegData, one RepoData/LegData and one
ReferenceSwapData/LegData.
5.2.6. No generated code was touched
git status lists three hand-written files: the container header, the
mapper and the test suite. projects/ores.ore/core/src/domain/domain.cpp
and every other xsdcpp output are untouched.
5.3. Wave A.2b, the leg groups
5.3.1. The container takes the leg list
bond_instrument_data held one bond_leg_data. It now holds
std::vector<bond_leg_data> bond_legs, because the schema declares
LegData unbounded and three documents state two legs.
The issue row mirrors the first leg only. One row cannot hold two
coupons, so reverse_bond_data writes one leg per element of the list,
and falls back to the issue terms for a payload that came from a row set
rather than from a document.
That fallback also fixed two defects. The issue's face value and coupon rate were written over the leg's own notionals and rates, so a document that stated a list of either lost all but the first. Both now apply only when the leg is silent.
The three other leg slots stay single, because the schema declares one
each: FundingData/LegData, RepoData/LegData and
ReferenceSwapData/LegData.
5.3.2. The nested groups the leg states
The scope rule mirrors a group whole when the corpus exercises it and its members are scalars or lists of scalars. A member that is itself a structured group is mirrored only when the corpus exercises it.
legData gained these members, each with its own selectable type:
amortizations (a list of bond_amortization_data), notionals,
payment_dates, indexings_from_asset_leg, payment_schedule,
settlement and rate. The rate member is a
bond_leg_rate_data, a choice of three alternatives: fixed, floating and
formula-based. The floating alternative carries the two stub
interpolations, the two inner schedules and the four number lists.
is_empty grew the new members in the same change, so a writer that
skips an empty leg still sees the whole of what the leg holds.
5.3.3. What A.2b does not carry
Four bounds stay open, and the corpus exercises none of them:
- The fifteen
legDataTypealternatives other than fixed, floating and formula-based. A bond leg carries a coupon, and the corpus states three of the eighteen. legData_Notionals_t/FXResetandExchanges. The corpus statesNotionalonly.legData_Indexings_t/Indexing. The container carriesFromAssetLegonly. AnindexingDatarow names a quantity and a fixing calendar that the leg has no other home for._FloatingLegData_t/HistoricalFixings. Its date member is namedfixingDateand notstartDate, so the shared number mapping does not fit it. A container field would need its own name and its own mapper.
One more case stays open by construction: an element the document states
bare, as <ScheduleData/> with no Rules and no Dates. The container
holds two lists and no presence flag, so it cannot tell that document
from one that omits the element. No corpus document does this.
5.3.4. The measured effect
Both trees were read with the same classifier, over external/ore/examples.
| Measure | A.2a | A.2b |
|---|---|---|
| In-scope lost pairs | 8,333 | 7,751 |
| In-scope unexplained pairs | 44 | 44 |
| In-scope lost paths | 152 | 65 |
582 pairs came back and 87 paths left the list whole. No path got worse: the path diff holds removals only.
Every LegData path left the list, for every product. What remains is
the bond-level residue of A.3 and the trade envelope of A.4.
5.3.5. No generated code was touched
git status lists four hand-written files: the two container headers,
the mapper and the test suite. projects/ores.ore/core/src/domain/domain.cpp
and every other xsdcpp output are untouched.
5.4. Wave A.3a, the bond members the issue row has no column for
bondData states eighteen members. The nine trading tables hold the
terms map_bond_data reads. Five members had no home at all:
Calendar, CreditCurveId, ReferenceCurveId, IncomeCurveId and
BondNotional. The corpus states four of them on every bond product.
5.4.1. The container carries the five whole
bond_instrument_data grew five std::optional<std::string> members:
calendar, credit_curve_id, reference_curve_id, income_curve_id
and bond_notional. An unengaged member means the document omitted the
element. That is the presence rule wave A.1 set.
The five are xsd::string wrappers in the binding, so the map direction
reads them as std::string(*bd.Calendar). The reverse direction calls
set_present_text, which emits on presence. It keeps the difference
between an element the document states empty and one it omits.
BondNotional is a string on both sides, not a number. The binding
declares it xsd:string and the corpus spells it 8000000.
bond_issue holds no calendar, no curve identifier and no notional
column, so the container is the only home for the five. Unit B adds
those columns.
5.4.2. The measured effect
Both trees were read with the same classifier, over external/ore/examples.
| Measure | A.2b | A.3a |
|---|---|---|
| In-scope lost pairs | 7,751 | 6,761 |
| In-scope unexplained pairs | 44 | 44 |
| In-scope lost paths | 65 | 50 |
990 pairs came back and 15 paths left the list whole. No path got worse: the path diff holds removals only.
Every bondData residue path left the list, for every product that
carries one: BondData, ForwardBondData/BondData,
AscotData/ConvertibleBondData, BondOptionData, BondRepoData,
BondTRSData, CallableBondData and ConvertibleBondData. One
Calendar path remains, BondTRSData/TotalReturnData/ScheduleData/Dates/Calendar,
and that one belongs to wave A.3b.
5.4.3. A defect in the gate, found while measuring
scripts/ore_mapper_roundtrip_diff.py classifies a lost pair as a
boolean spelling when either side is empty and the other side is one of
the ORE bool spellings:
def is_boolean_pair(left: str, right: str) -> bool: # lines 143-144 return (left == "" and right in ORE_TRUE) or (right == "" and left in ORE_TRUE)
The docstring gives the reason for one direction only: "the examples
spell true as an empty element". The second clause is broader than that
reason. It swallows a real loss whenever a string element happens to
hold the text true and our output states it empty.
Two such paths are in the corpus today, and the gate hides both:
/Portfolio/Trade/ForwardBondData/LongInForward, 35 pairs. The schema declares itxs:stringand required, and the corpus statestrue./Portfolio/Trade/BondTRSData/TotalReturnData/Payer, 4 pairs. The schema declares it a required string.
A strict classifier, with the second clause removed, reports 39 more lost pairs and 39 more unexplained pairs on the same tree: 6,800 and 83 in place of 6,761 and 44. The boolean class falls from 352 pairs to 313, which is the same 39.
The gate now holds the one-directional rule. Re-running it over the same tree gives exactly those numbers, so the rule change moves the 39 pairs and nothing else.
The prediction here was that A.3b restores 6,761 and 44. It carried the two hidden paths, so it did restore those, and it carried the ForwardBond and BondTRS residue as well. Both fell further.
5.5. Wave A.3b, the per-product residue
Two products stated members that had no home in the container and no column in any row. A forward bond states a flag, a settlement block and a premium. A TRS states a payer, an initial price and a return schedule. The classifier reported all six, and two of them only after the previous wave made the boolean rule one-directional.
5.5.1. The container carries the forward settlement and premium whole
bond_schedule_data.hpp gained two types.
bond_forward_settlement mirrors settlementData: the forward maturity
date is required and the other seven members are optional, so a forward
bond always states the first and the container keeps the difference for
the rest. bond_forward_premium holds the amount and the date, both
required strings. The schema spells the amount as text, not as a number.
bond_instrument_data gained six members: forward_long_in_forward,
forward_settlement, forward_premium, trs_payer,
trs_initial_price and trs_schedule. The last is a
bond_schedule_data, the type wave A.2b built for the legs, so the TRS
return schedule costs no new type.
The settlement block states its amount, lock rate and dv01 as xs:float.
The container holds them as double and the two directions cast, the
way map_number and reverse_number already do.
The premium and the settlement block belong to the product and not to the issue, so they sit on the container beside the fact rows and not in the fact rows.
5.5.2. The measured effect
Both trees were read with the strict classifier, over
external/ore/examples.
| Measure | A.3a | A.3b |
|---|---|---|
| In-scope lost pairs | 6,800 | 6,582 |
| In-scope unexplained pairs | 83 | 9 |
| In-scope lost paths | 50 | 38 |
218 lost pairs came back and 12 paths left the list whole. No path got worse: the path diff holds removals only. The twelve are the five BondTRS total-return paths and the seven ForwardBond paths, which is every path this wave set out to carry.
The unexplained class fell by 74, in two parts. The first is the 39
pairs of the two hidden paths: the source states true in a required
string and the committed mapper left the whole forward block unset apart
from BondData, so our writer emitted LongInForward empty against a
document that states it. The second is the remaining 35, and the cause
is the same one: the settlement block is required, so the writer marked
it with an empty forward maturity date on every forward bond in the
corpus.
Under the rule the gate used before this wave, the A.3a tree read 6,761 and 44. The A.3b tree reads below both, so the wave clears the two hidden paths and carries the product residue as well.
5.5.3. What remains in scope
The 6,582 lost pairs and the 38 lost paths fall into two groups, and between them they account for every one of the 6,582.
- The option block, 72 pairs over 24 paths, shared by BondOptionData and
AscotData.
OptionData/LongShortis nine of them, six on BondOptionData and three on AscotData: the source statesLongand our output states the element empty, so each is a lost pair as well as an unexplained one. The rest isStyle,Settlement,Redemption,PriceType,KnocksOut, the strike group (StrikePricewith its value and currency,StrikeYieldwith its yield and compounding) and the Ascot premium list. This is wave A.3c. - The trade envelope, 6,510 pairs over 14 paths, from
Envelope/CounterPartydown to theAdditionalFieldsextras. This is wave A.4.
5.5.4. The full suite, and one failure that is not this wave's
compass build is clean and compass test run reads 71 of 71 passing.
The first run of the suite failed ores.reporting.core.tests on the
temporal exclusion constraint for report definitions. The second run
passed the same test with no code change between them.
The cause sits in that component's own test.
report_definition_eventing_integration_tests.cpp waits for a NATS
notification after a write, and when the notification does not arrive it
re-drives repo.write(party_ctx, v) up to four times, passing the same
version object each time. The comment on that loop says the re-drive
makes a new version row. It does not:
ores.database/repository/bitemporal_operations.hpp implements
execute_write_query as a plain insert, with no version bump and no
upsert, so the second insert repeats the first row and the exclusion
constraint rejects it.
The retry therefore cannot work on any path it is taken, and load is
what decides whether it is taken. That is why it reads as a flake. It is
a defect in ores.reporting and this wave does not fix it. It is
recorded here because it makes every later full-suite gate in this work
unreliable until someone does.
5.5.5. No generated code was touched
The four files this wave changes are the two container headers, the
mapper and the mapper's test suite. domain.hpp and domain.cpp are
untouched.
5.6. Wave A.3c, the option block
The option block was 72 pairs over 24 paths, shared by BondOptionData
and AscotData, and it was the last bond-product residue in scope.
This wave closes it: the in-scope reading now holds no bond path at all.
5.6.1. The schema's bool type is not the XML boolean
The generated header spells the schema's own bool domain::bool_, and
the type enumerates thirteen spellings: Y, YES, TRUE, True,
true, 1, N, NO, FALSE, False, false, 0 and the empty
string. The generated writer emits whichever spelling the reader
stored.
The container held a C++ bool instead, so the mapper could reach two
of the thirteen and eleven could not survive a round trip. The corpus
states six <KnocksOut>false</KnocksOut> and two
<AutomaticExercise>true</AutomaticExercise>, so the loss was real and
not theoretical, and the gate counted each one twice: once as a lost
pair, because our output dropped the value, and once as unexplained,
because our output stated the element empty against a document that
states it.
Two of the block's flags were already immune. MidCouponExercise and
PayOffAtExpiry are xsd::string types in the generated header, so
the container had always carried their text verbatim. This wave makes
the other five match those two. That is the schema's own idiom rather
than a new one.
5.6.2. The empty spelling is unreachable, and that is worth recording
The obvious reading of the generated table is wrong, so it goes on the
record. _bool__Values carries a null terminator after the thirteen,
and entry 12 is the empty string, so toNumeric would map "" to
index 12 if it were ever called with an empty string. It never is.
The element reader runs the text setter only when the element held
text. The guard is if (context.pos.pos ! start && …addText)=, and
skipText consumes nothing for an empty element, so pos is unchanged
and addText does not run. The getter then engages the optional with a
default-constructed value, parent->EndOfMonth = domain::bool_(),
which is index 0, Y.
So a stated-empty bool element reads as Y and writes back as Y, and
bool_::_ cannot be reached by parsing at all. The corpus states the
element empty 3,753 times, as <EndOfMonth/> 3,734 times and as
<EndOfMonth /> with a space 19 times, and the gate's boolean rule
accepts empty against any true spelling, which is why none of those
pairs appeared as a loss.
5.6.3. The rest of the block
The bool type was the largest part but not all of it. The block's
nested groups had no container type, so the mapper reached only the
scalars it already knew. bond_schedule_data.hpp gained seven types:
bond_option_settlement, the pay currency, the FX index and the fixing date. The schema spells the same three members twice, under the option and under each premium, so one type serves both.bond_option_premium, an amount, a currency, a pay date and an optional settlement block.bond_option_exercise_fee, an amount with three attributes on the same element.bond_option_exercise, a date and an optional price.bond_option_payment_rules, a lag, a calendar, a convention and an optional relative-to.bond_option_payment_data, the payment dates as a list or as a rule.bond_option_data, the block whole, at 25 members, andbond_strike_data, the strike as a price, as a yield or as a bare value.
bond_instrument_data already carried option_data and strike_data
from wave A.2b's container growth, so this wave fills them rather than
adding them.
5.6.4. Numbers the corpus states as text
The strike values and the premium amount are xs:float in the schema,
and the container holds them as double. Each direction needs its own
rule. On the way in the mapper parses the text. On the way out it
writes the shortest text that reads back as the same double, which is
what std::to_chars produces and std::to_string does not: the latter
writes six fixed decimals, so a corpus value below 1e-6 would come
back as 0.000000.
The strike suite's expectation moved from a double comparison to a check on the text, because the value now has to survive as text.
5.6.5. The measured effect
Both trees were read with the same classifier, over
external/ore/examples.
| Measure | A.3b | A.3c |
|---|---|---|
| In-scope lost pairs | 6,582 | 6,510 |
| In-scope unexplained pairs | 9 | 0 |
| In-scope lost paths | 38 | 14 |
The 72 pairs and the 24 paths are the option block exactly, which is what this wave set out to carry. No path got worse: the path diff holds removals only.
The unexplained class is now empty. Every in-scope difference the classifier reports is a pair the source stated and our output did not, with nothing left over that the classifier cannot account for.
5.6.6. What remains in scope
The 6,510 lost pairs and the 14 lost paths all sit under
/Portfolio/Trade/Envelope, and they are the enumeration wave A.4
already carried.
| Path | Pairs |
|---|---|
Envelope/CounterParty |
2,628 |
Envelope/AdditionalFields |
2,591 |
Envelope/AdditionalFields/valuation_date |
318 |
Envelope/AdditionalFields/party_id |
318 |
Envelope/PortfolioIds |
204 |
Envelope/PortfolioIds/PortfolioId |
160 |
Envelope/NettingSetId |
137 |
Envelope |
137 |
| the six AdditionalFields extras | 17 |
No bond product appears in that list. On the product paths the mapper is now lossless across the whole corpus.
5.6.7. No generated code was touched
The five files this wave changes are the two container headers, the
mapper and two test suites. domain.hpp and domain.cpp are
untouched.
5.7. Wave A.4, the trade envelope
5.7.1. The envelope is keyed by the trade, not by the product
envelope is an xs:all of four members: CounterParty,
nettingSetGroup, PortfolioIds and AdditionalFields. The story's
fourth decision assigns it to the trade entity and not to the
instrument family, and the schema agrees: every product carries one,
and nothing inside it is product-specific.
AdditionalFields is the fourth member and it is open content. The
schema states it as xs:any processContents"lax"= with
maxOccurs"unbounded"=, so it names no field and no generated type can
enumerate one. The binding carries the block as a list of name and
value pairs. This wave had to carry that list whole.
The wave was 6,510 pairs over 14 paths, from Envelope/CounterParty
down to the six additional fields the corpus states.
5.7.2. The carrier and the mapper pair
trade_envelope_data is new, in ores.trading.api, and it holds four
optionals, one per member. trade_envelope_field holds one additional
field as a name and a value. Every member is an optional for the reason
wave A.1 set: an element the document states empty must stay distinct
from one it omits. CounterParty and nettingSetGroup are already
optionals in the binding. PortfolioIds and AdditionalFields are
not, so their optionals carry presence alone.
trade_mapper::map_envelope returns nothing when the trade states no
Envelope, and a carrier when it does, holding whatever the members
hold. reverse_envelope is its inverse and emits every engaged member,
so a held-empty element comes back as an empty element.
One bound stays open. The schema heads the netting set group with an
abstract element and gives it two substitution-group members:
NettingSetId, a string, and NettingSetDetails, which holds a
NettingSetId of its own plus AgreementType, CallType,
InitialMarginType and LegalEntityId. The carrier holds the first
form only, so a document that stated the second would lose four
members. No corpus document states it, and the one mention of the name
in external/ore is inside a Python script and not an XML document, so
nothing exercises the bound.
The carrier rides beside the trade and not inside it.
trade_import_item in ores.ore.core and trade_export_item in
ores.trading.api both gained a
std::optional<trade_envelope_data> envelope member, and
export_portfolio takes the envelope from the item rather than
rebuilding one from the trade's netting_set_id column. That column
stays a projection: it holds the text of NettingSetId and none of the
presence, so the envelope is the carrier of record.
The new member replaces ore_counterparty_name in trade_import_item,
which held one string of the envelope and dropped the other three
members whole. The qt import dialog reads the counterparty through the
envelope now.
5.7.3. The shell writes the trades no reverse mapper can rebuild
The 6,510 pairs fell to 310 once the carrier was in place, and all 310
sat on 63 trades the exporter dropped whole. The exporter visits the
instrument variant and returns without writing anything in two cases: a
std::monostate, and a type that reaches a family but no branch inside
it. InflationSwap is the second kind. It dispatches to the swap family
and falls into that family's else, so it counts as mapped and is then
dropped.
The schema makes the drop unnecessary. trade is a sequence of
TradeType, an optional Envelope, an optional TradeActions and an
optional oreTradeData group, over a required id attribute. A trade
that states only its type and its envelope is therefore a valid
document. The writer now falls back to exactly that: it parses the type
string back to the generated enumeration and writes the trade with its
envelope and no product data. The envelope is keyed by the trade and not
by the product, so dropping the trade whole would lose envelope pairs
that no other trade states.
A type the schema does not name leaves no valid document to write, and that trade is still dropped, with a warning.
5.7.4. The count that has to be maintained by hand
domain.hpp exports to_string for every enumeration and no parse for
any of them. The generated _oreTradeType_Values table and
xsdcpp::toNumeric are of internal linkage, so the exporter cannot
reach them.
The new parse_ore_trade_type scans the spellings through to_string,
which keeps one source of truth: a spelling the generated table adds is
parsed with no table here to keep in step. That needs an upper bound,
and ore_trade_type_count = 115 is it. The number is not a guess. The
generated to_string(oreTradeType) passes 115 to
xsdcpp::to_string as its own count, and _oreTradeType_Values holds
115 spellings, so the constant is the same number the generated code
states and a regeneration can be checked against it.
This is still the hazard the bond_instrument_mapper *_count
constants already carry: the count is correct for the generated code of
today, and a regeneration that adds a type would silently stop finding
it. It is recorded here rather than left implicit.
5.7.5. The measured effect
Both trees were read with the same classifier, over
external/ore/examples. The carrier alone moved 6,510 to 310, and the
shell emission moved 310 to nothing.
| Measure | A.3c | A.4, carrier | A.4, shell |
|---|---|---|---|
| In-scope lost pairs | 6,510 | 310 | 0 |
| In-scope unexplained pairs | 0 | 0 | 0 |
Every path under /Portfolio/Trade/Envelope left the list whole, and
no bond product path returned to it.
The 310 decomposed exactly, and every pair of it sat on the 63 trades the exporter dropped:
| Path | Pairs |
|---|---|
Envelope |
63 |
Envelope/AdditionalFields |
63 |
Envelope/CounterParty |
63 |
Envelope/NettingSetId |
63 |
Envelope/PortfolioIds |
38 |
Envelope/AdditionalFields/party_id |
10 |
Envelope/AdditionalFields/valuation_date |
10 |
Two tests guard the change.
export_portfolio_unmapped_trade_keeps_its_type_and_envelope writes a
monostate item and checks the type and the envelope reach the output and
no product block does.
export_portfolio_unknown_type_is_skipped checks that a type the schema
does not name is still dropped. The test the first one replaces asserted
the opposite, that a monostate item is skipped, and it was deleted.
5.7.6. The trade count now matches the written documents
The roundtrip writes 543 documents and skips 1,023, all 1,566 read.
Across the 543, the counters read 2,590 trades mapped and 40
passthrough, and the output tree holds 2,630 <Trade> elements. The
tree before this wave held 2,567, so 63 is exactly the difference and
every trade in a written document now reaches the output.
The 40 is the monostate class. The 23 trades that reach a family and no branch inside it were never counted anywhere, because the counter asks only whether the variant is monostate. That is a defect in the counter and not in the writer, since it measures dispatch and not output, and after this wave both classes are written anyway.
5.7.7. The literal diff is ordering, tag spelling and number formatting
The classifier normalises values before it compares them, so a separate
literal text diff was run on Cash_Bonds.xml to see what a byte-level
comparison reports. Four classes appear, and none of them is a loss.
- Element order inside
LegData. The schema declareslegDataasxs:all, so any order is valid. Our writer emits in the order the container declares and the document states its own. - An element the document states empty comes back as
<NettingSetId/>or as<NettingSetId></NettingSetId>, whichever the writer picks. Both are the same infoset. - Numbers.
10000000comes back as10000000.000000, and0.05as0.0500000007. Both stay inside the1e-6tolerance, and the worst relative error over the whole corpus is 5.660e-8. - Booleans.
<EndOfMonth/>comes back as<EndOfMonth>Y</EndOfMonth>. The generated reader holds the two as the same value and the classifier compares them as such.
This is the allowance the user set: a diff is acceptable when it is ordering or the like, and not when it is a loss.
5.7.8. The database still has no home for the envelope
The carrier closes the mapper path. It does not close the database path.
trade_export_item is built from the trade row alone at the service
boundary, so the envelope stays absent on the way out of the database.
ores.trading.core/messaging/trade_handler.hpp builds its items with
designated initialisers in two places, one for a single trade and one
paged over the trades of a book, and then populates the instrument from
the trade's facts. Neither reads an envelope, because no table holds
one.
So the full XML to database to XML round trip loses the envelope until unit B gives it a home and the export handler reads it back. That is the acceptance line "every member the container carries has a column or a keyed child row", and the carrier is now its complete specification: four members, one of them an open list of name and value pairs.
The designated initialisers are also why the new member broke nothing.
Both call sites set trade by name, so instrument and envelope
default and the existing code compiles unchanged.
5.7.9. The full suite is green, and the flake did not recur
compass build is clean and compass test run reads 71 of 71 passing,
0 failed, in 910 seconds. ores.ore.core.tests passed in 4.13 seconds,
which carries the new envelope suite and the two exporter tests.
The ores.reporting temporal exclusion flake A.3b recorded did not
recur. That is luck and not a fix: the retry path it describes still
cannot work, and the suite stays unreliable until someone repairs it.
5.7.10. No generated code was touched
The twelve files this wave changes are four headers, three sources,
two test sources, two build files and the qt dialog. domain.hpp and
domain.cpp are untouched, and the two component_files.cmake files
were regenerated rather than edited.
5.7.11. What the gate's exit code counts, and why it is not zero
main() returns 1 if any document is MISSING, UNPARSED, or DIFFERENT
with an in-scope loss. On the A.4 output tree that is 1023 MISSING, 0
UNPARSED and 0 DIFFERENT, so the script exits 1 while the in-scope
counters read zero. The two statements are both true and they measure
different things.
The 1023 are the documents roundtrip() never writes, because their
header matches none of its four markers. They are pricing engines,
market data configs and the like. Not one of them contains a <Trade>
element: counting <Trade> across all 1023 source documents returns
zero. A trade-bearing document is a <Portfolio> document, and every
Portfolio document is written and compared.
So no trade escapes the comparison by going missing, and the in-scope lost and unexplained counters are the predicate. A reader who wants a green exit code has to decide what the tool should do with documents it does not handle at all, which is the same decision the sniff finding below raises.
5.7.12. Finding: the puml generator cannot reach the nested components
build/scripts/generate_component_puml.py discovers components by
scanning projects/*/include, so it sees the flat layout only. The
regroup commit 164255805d moved most components to
projects/<group>/<component>, and their puml files have been frozen
since 2026-06-02. A dry run today finds 18 projects, reports 13 would
change, and refuses ores.ore.core with "No include/ directory found".
The thirteen changes are drift unrelated to this wave, so no
regeneration was run. The consequence for this wave is that
ores.ore.core.puml still names ore_counterparty_name, a member this
wave deleted. The file was left alone rather than hand-patched: the
sentinel in it declares everything above the manual line generated, and
waves A.1 to A.3c set the same precedent by leaving the puml files of
the components they changed untouched. The repair belongs to whoever
teaches the generator the nested layout, and it will correct this puml
and the other twelve in one pass.
5.7.13. Finding: the roundtrip classifies a document by substring, and rewrites what it misfiles
read_header in exporter.cpp peeks the first 4096 bytes of a
document, and roundtrip() picks the importer by looking for one of
four markers in that window: <Portfolio>, <CurrencyConfig>,
<CalendarAdjustments>, <Conventions>. A <CurveConfiguration>
document is none of the four, so it falls through to the conventions
branch whenever the word <Conventions> appears inside the window,
which it does in every curve configuration that names a convention.
The result is data loss under a name that does not announce it.
Academy/FC003_Reporting_Currency/Input/curveconfig.xml is 21,505
bytes and holds a curve configuration; its <Conventions> element sits
at byte offset 457. The roundtrip reads it as a conventions document
and writes 15 bytes of <Conventions/> to the same relative path. 91
output documents are under 64 bytes where their source is more than ten
times larger, and every one of them is a curve configuration.
This is not this wave's doing. The sniff is untouched by the wave's
diff, and the pretree /tmp/ore_rt_a4 holds a byte-identical 15-byte
file. The conventions path is also neither the bond products nor the
envelope, so it sits outside the declared scope and is recorded rather
than repaired here.
It does bound one thing this wave wanted. A second pass over the output
tree cannot serve as an idempotence check, because the tree holds
documents whose content contradicts their filename: pass 2 reads 543
files, skips 120, and writes 423, and diff -rq against its own output
reports 125 differences. The check is contaminated at the source, not
by the mapper, and the mapper path was measured instead against
external/ore/examples, which is the input the directive names.
The repair is to classify by root element and to refuse a document whose root is none of the four, rather than to trust a substring. That change belongs to a task of its own, since it decides what the tool does with the documents it cannot currently name at all.
5.8. Wave B.1 and B.2, the tables that hold what the container carries
Unit A grew the container until it held every member a bond document states. Unit B builds the homes. This wave is the first half: nine tables and five columns.
5.8.1. Three families, and each one is keyed for any instrument
The nine tables are not nine unrelated shapes. They fall into three families, and every key is written so that a family other than the bond can use the table without a migration.
| Family | Tables | Key beyond the instrument or trade |
|---|---|---|
| Leg | bond_legs, bond_leg_amounts, bond_leg_amortizations, bond_leg_rates |
leg_role, leg_number |
| Schedule | instrument_schedules, instrument_schedule_dates |
leg_role, leg_number, schedule_role |
| Envelope | trade_envelopes, trade_envelope_portfolio_ids, trade_envelope_additional_fields |
the trade, not the instrument |
The leg role is one of bond, trs_funding, repo or ascot_swap,
stated as a set in the check rather than as a single value. Those four
are the four the container carries: the bond_legs list and the three
single legs trs_funding_leg, repo_leg and ascot_swap_leg.
The amount table is one table and not six. amount_role names which of
the container's six lists a row belongs to, and the six are notional,
rate, spread, cap, floor and gearing. The check on that
column says only that it is not empty, because the list is the
container's shape and not the schema's.
The envelope family is keyed by trade_id and not by instrument_id.
The story's fourth decision gives the envelope to the trade, and the
key follows the decision.
5.8.2. The five columns on the issue row
BondData states five things at the bond level that no row held:
Calendar, CreditCurveId, ReferenceCurveId, IncomeCurveId and
BondNotional. They are five nullable columns on
ores_trading_bond_issues_tbl.
Four of them name a thing the document spelled. The fifth is text for
the same reason as the other four: export re-emits the document's own
spelling, and a numeric column would give 10000000 back as
10000000.000000.
5.8.3. Finding: the schedule tables cannot yet hold what they were built for
scheduleData is a choice of Rules and Dates, unbounded and
interleaved. One bond_schedule_data can hold several rules entries
and several dates entries, and the generated key allows one row per
(instrument_id, leg_role, leg_number, schedule_role). The dates arm
also states five members (Calendar, Convention, Tenor,
EndOfMonth, IncludeDuplicateDates) that no column anywhere holds.
So the schedule tables ship in the shape the leg family needs, and not
yet in the shape the schedule data needs. The reshape is small and it
belongs to wave B.3: rename the owner columns to owner_role and
owner_number, put sequence_number in the parent key, add the five
flags to the parent, and put schedule_sequence_number in the child
key ahead of the child's own ordinal.
The tables still ship now because nothing writes a schedule row until B.5, so the rename costs no data and no caller.
5.8.4. No generated file was edited by hand
Every C++ file this wave changes is generated. The eight under
projects/ores.trading/ are a domain class, an entity header, two
mappers, a history field mapper and three component_files.cmake
files. The three cmake files came from
regenerate_cmake_component_files.py; the rest came from the entity
generator reading the modeling org.
Written by hand: the ten modeling orgs and the two ores.sql aggregate
scripts that list the new files in create and drop order. Nothing
generated was opened in an editor.
5.8.5. The tables are empty until B.5
This wave gives the nine tables a home in the schema. It does not fill
them. trade_export_item is still built from the trade row alone, and
no code reads or writes a row of the nine. That switch-over is wave
B.5, and the database path keeps losing what the mapper path keeps
until it lands.
5.8.6. Wave B.1 and B.2 landed
The nine tables and the five columns on the issue row shipped, and the component builds and tests clean.
| Check | Result |
|---|---|
compass build --preset linux-clang-debug-make |
green, reached 100% |
compass test run --preset linux-clang-debug-make |
70 of 71 suites passed |
ores.trading.api.tests, ores.trading.core.tests, ores.trading.service.tests |
passed |
ores.ore.core.tests, ores.ore.api.tests, ores.ore.service.tests |
passed |
The one failing suite is ores.refdata.core.tests, and it is not this
wave's. The test write_book_publishes_nats_changed_event raises
Invalid currency: X-0 from ores_refdata_validate_currency_fn. The
book generator hardcodes functional_currency = "X-0", a choice that
landed in commit 38a60de6c9 and is already in main. The validator
lists the writing tenant's active currencies, and that list held X-1
through X-17 but not X-0.
The currency is not missing. X-0 spans twelve tenants and twenty
seven rows. The rows written during the failing run open and close
inside 0.2 seconds and leave no open version for that tenant, which is
what the validator saw. The suite passes in isolation with zero
failures, so the defect is a temporal-validity race in the refdata
eventing fixtures that test order and accumulated database state
expose. It is recorded as its own task and does not gate this wave.
5.9. Wave B.3, the schedule reshape and the option block
Wave B.1 and B.2 shipped the tables in the shape the leg family needs. This wave gives the schedule tables the shape the schedule data needs, and gives the option block the homes no bond table had. Eight entities are generated: two reshaped schedule tables, one grown fact table, and five new ones.
5.9.1. The reshape is three changes in the key
The parent gains sequence_number. The schema states a schedule as an
unbounded choice of Rules and Dates, and a document interleaves
the two arms, so one owner can state several entries under one role.
The role alone did not identify a row.
The pair leg_role and leg_number becomes owner_role and
owner_number. Two of the six values name an owner that is not a leg.
option is the exercise schedule an option block states, and trs is
the return schedule a total return swap states. The other four,
bond, trs_funding, repo and ascot_swap, do name a leg.
The child gains schedule_sequence_number, which is the parent's
sequence_number under its own name. A date stays bound to the entry
that holds it when one owner states several entries under one role.
The parent key is now (tenant_id, instrument_id, owner_role,
owner_number, schedule_role, sequence_number, valid_from, valid_to).
The child's key is that key with schedule_sequence_number ahead of
the child's own ordinal.
No code reads either column yet. Nothing writes a schedule row until
B.5, so the rename costs no data and no caller outside the generated
stack. A search of the repository found no consumer of the renamed
columns outside ores.trading.
5.9.2. Finding: the five dates-arm columns already existed
The B.1 section above records that the dates arm states five members
(Calendar, Convention, Tenor, EndOfMonth and
IncludeDuplicateDates) that no column holds. That text was written
before the tables shipped, and B.3 adds none of the five.
B.1 and B.2 added all five. The parent already carries tenor,
calendar, convention, end_of_month and
include_duplicate_dates. The earlier text stands as the record of
what B.1 measured, and this paragraph corrects it.
5.9.3. Six columns typed text that hold a date
Comparing every column of the reshaped and the new tables against the
generated schema member types found six columns typed text whose
member the schema types as a date.
| Table | Columns |
|---|---|
instrument_schedules |
start_date, end_date, first_date, last_date |
instrument_options |
exercise_date |
instrument_option_premiums |
pay_date |
All six become date. The generated reader parses the member before
the mapper sees it, and the writer re-emits a canonical form, so a
date column carries the member whole.
Five columns that look the same stay text, and the schema says why.
premium_pay_date is an xsd::string. Both settlement_fixing_date
columns are xsd::string. The exercise fee's start_date is an
optional string attribute on a numeric element. Their documents spell
dates the schema never typed as dates.
5.9.4. The option block, and where the container's members land
bondOptionData states three members beside its option block rather
than inside it: the redemption code, the price type and the knock-out
flag. The shared optionData element states none of the three, so an
Ascot never writes them and no other row can hold them. They ride on
bond_options, the one row that is a bond option.
knocks_out is text. The schema types the member as its own bool,
which enumerates thirteen spellings, and the corpus states false. A
decoded boolean would lose the spelling on every document that
carries one.
The block itself is the element every product states, so it gets the shared treatment. Five tables carry it.
| Table | What it holds |
|---|---|
instrument_options |
the block's own members, and a flag per optional sub-block |
instrument_option_premiums |
the Premium list |
instrument_option_exercise_fees |
the ExerciseFees list |
instrument_option_payment_dates |
the PaymentData/Dates list |
instrument_strikes |
the strike group |
bond_option_data carries twenty five members. Every one of them has
a column or a keyed child row once the option row is read with its
three child tables, so the container is the specification for the
tables and the count closes.
Three members are themselves groups whose every member is optional. A
set of null columns could not say whether the document stated the
group and left it bare or omitted it, so each group carries a flag on
the option row: has_exercise_data, has_payment_data and
has_settlement_data.
Every other member is text, and the text is the document's own spelling. The schema types the premium amount, the exercise price list and the several flags as text or as its own bool, so a decoded form would not re-emit what the document held.
5.9.5. The tables stay empty until B.5
This wave gives the option block and the schedule data a home in the
schema. It does not fill them. trade_export_item is still built from
the trade row alone, and no code reads or writes a row of the fourteen
shared tables. That switch-over is B.5, and the database path keeps
losing what the mapper path keeps until it lands.
5.9.6. No generated file was edited by hand
Every C++ file this wave changes is generated. The modeling orgs are
hand written: three edited for the reshape and the residue columns,
and five new for the option tables and the strike table. The two
ores.sql aggregate scripts are hand written too, and they list the
new files in create order and in reverse drop order.
The three component_files.cmake files came from
regenerate_cmake_component_files.py --all, run with the codegen
venv. Nothing generated was opened in an editor.
5.10. Wave B.4, the forward, the future basket and the TRS residue
B.3 gave the option block and the schedule data their homes. This wave closes the last three gaps a document in the corpus can reach: the return side of a total return swap, the forward block a forward bond states, and the delivery basket a bond future names. One entity is grown and two are new.
5.10.1. The return side states three members no row held
totalReturnData states Payer, PriceType and InitialPrice
beside the return schedule and the funding leg. The schedule lands in
the shared schedule table under the trs owner role B.3 opened, and
the funding leg lands in the shared leg family. The three remaining
members had no home, so bond_trs grows them.
| Column | Type | Why |
|---|---|---|
payer |
text |
the schema types it xs:string |
price_type |
text |
the schema types it xs:string |
initial_price |
numeric(28, 10) |
the schema types it xs:float |
Neither text column is a decoded boolean or a decoded enumeration. The
document states its own spelling and export re-emits that spelling.
payer is also not return_type. The row already carried
return_type, which names the return side of the swap; price_type
names how the price is quoted.
The return side states nine more members that no table holds:
ObservationLag, ObservationConvention, ObservationCalendar,
PaymentLag, PaymentConvention, PaymentCalendar, PaymentDates,
FXConversion and FXTerms. The corpus states none of them, so the
round trip is whole without them. They stay a recorded scope limit.
5.10.2. Finding: the forward's two date columns were typed as dates
The forward org was first authored with forward_maturity_date and
forward_settlement_date as :type: date. The schema says otherwise:
settlementData declares ForwardMaturityDate as xs:string, and
ForwardSettlementDate likewise.
The corpus settles it. ForwardMaturityDate appears thirty seven times:
thirty three spelled 20160808, 20160909, 20251220, 20160304 and
20160205 in basic form, and four spelled 2025-07-16 and 2025-08-22
in ISO 8601. A date column returns the basic form as 2026-08-08 and
breaks the round trip on the thirty three documents that state one. The
settlement date is stated nowhere in the corpus, and follows the schema
alone.
Both columns are text now. The C++ member type was
std::optional<std::string> either way, so the change reaches the SQL
and not the API. The defect was found by reading the schema and
surveying the corpus before the entity was generated, so no generated
file had to be corrected.
5.10.3. The forward block, member by member
bond_forward is keyed to the instrument, one row per forward bond. It
carries the settlement block's seven members, the premium block's two,
and the long-in-forward flag, because no other row holds them. The bond
block is the issue, and the nine bond tables hold it.
A block is present exactly when its required member is not null. The
schema declares ForwardMaturityDate the settlement block's one
required member, and the premium's Amount and Date required
together, so a null required member means the document stated no block.
The premium amount is text because the schema types it xs:string.
The settlement amount, the lock rate and the dv01 are numeric(28, 10)
because the schema types those three as xs:float. The settlement
type and the lock rate's day counter are text.
KnockOut is stated on forwardBondData and the mapper does not carry
it. The corpus states none, so the round trip is whole without it. It
is a recorded scope limit, not fixed in this wave.
5.10.4. The delivery basket is a keyed child table
deliveryBasket is an unbounded list of Id elements. A bond future
names several deliverable identifiers and the order is the document's,
so bond_future_delivery_baskets carries one row per identifier, keyed
to the instrument and the identifier's ordinal. The shape is the one
the shared schedule tables use.
The identifier is text because the document states a name, not a reference to a bond row.
5.10.5. Finding: the new tables had no row-level security
projects/ores.sql/utility/validate_schemas.sh --strict checks that
every table carrying tenant_id has an alter table ... enable row
level security statement in some create file, and that every file
under create/ is reachable from its aggregator.
The check reported sixteen tables with no policy: the three envelope tables, the four leg tables, the two schedule tables, the five option tables, and this wave's two. Fourteen of the sixteen came from B.1 and B.2 and from B.3, so the gate has been red since B.1 and no wave recorded it. The two from this wave would have made it seventeen.
The cause is that trading_rls_policies_create.sql is hand maintained
and lists its tables one at a time. The entity generator does not emit
it, so a table arrives without a policy and nothing reports the gap
until the validator runs with --strict.
All sixteen now carry the tenant isolation policy their neighbours
carry, and the matching drops are in
trading_rls_policies_drop.sql. The validator reads two hundred and
ninety nine tables, zero warnings, and exits 0. Every new entity that
carries tenant_id still has to be added to the file by hand.
5.10.6. The tables stay empty until B.5
This wave gives the forward block, the delivery basket and the three
return-side members a home in the schema. It does not fill them.
trade_export_item is still built from the trade row alone, and no
code reads or writes a row of any table this program added. That
switch-over is B.5, and the database path keeps losing what the mapper
path keeps until it lands.
5.10.7. No generated file was edited by hand
Every C++ file this wave changes is generated. The three modeling orgs
are hand written: one edited for the three columns, and two new. The
two ores.sql aggregate scripts are hand written too, and they carry
the new files in the create block and in the drop block.
The three component_files.cmake files came from
regenerate_cmake_component_files.py, run with the codegen venv.
Nothing generated was opened in an editor.
5.11. Wave B.5, the mapper switch-over to the tables
Waves B.1 to B.4 gave every member the container carries a column or a keyed child row. This wave points the database path at them, so import writes what the container holds and export rebuilds it.
Two surfaces change, and both are hand written. The write side is
ore_import_execute_handler.cpp, which saves the bond header row and
nothing else. The read side is populate_instruments_for_trades in
trade_handler.hpp, which rebuilds the header, the issue and the
option, TRS and repo fact rows, and rebuilds neither the envelope nor
any child list.
5.11.1. Finding: the sixteen new tables have no nats surface
Each of the sixteen entities has a generated protocol, handler and
registrar. None of the sixteen registrars is called. registrar.cpp
wires eight product aggregators, and registrar_bond.cpp carries the
eight bond tables of the pilot. The sixteen new ones are absent from
both, so no subject reaches any of the sixteen tables and a running
service cannot read or write one of them. The history providers are
absent from the dispatch registry for the same reason.
The import handler writes over nats, so the write path needs the registrars wired before it can save a single child row.
5.11.2. Finding: the read path needs a query shape the generator does not emit
A child table is keyed by its parent and by the child's own ordinal, and export holds the parent keys. It needs every child row of a set of parents.
The generated repository offers an exact primary key read, a paged read of the whole table, and a batch read over the full primary key. None of the three answers the question. The batch read takes one vector per key column and filters exact key tuples in C++, so a caller that does not already know the ordinals cannot use it.
The trading component already meets this gap once:
fra_instrument_service::get_swap_legs_batch takes instrument ids and
returns legs. That method is not in the regenerated output, so it was
written into a generated file by hand. This wave does not repeat that.
The reads go in a new hand-written file that reuses the generated
entity and mapper types, so no entity file is edited.
5.11.3. Finding: trade_handler.hpp is a generated path kept by hand
The generator renders messaging/<entity>_handler.hpp from
cpp_nats_handler.hpp.mustache for each domain entity, so
trade_handler.hpp is in its write list. The committed file is not what
the template emits. It carries ORES_TRADING_CORE_EXPORT on the class
and the activity-type, instrument and two export methods the template
has no source for, and the regenerated form carries none of them and a
different handler set. The trading component sits outside the drift
checker's known-drift-free registry for reasons of this kind.
The file is therefore a generated path whose content the team maintains by hand and does not regenerate. The switch-over needs one call site in it, because the export paths and the read helper they call all live there and no hand-written seam exists. A new hand-written helper called from the generated file would edit the same file at the same place. The wave takes the call-site edit and records it here.
The durable fix is the template's paste mechanism. The mustache carries
<<paste:UUID>> markers, and a hand-written fragment pasted at a marker
survives regeneration. Moving the hand-written body behind a marker
would let the file regenerate from its model. That is a wave of its own.
5.11.4. Finding: the sixteen tables are absent from the development database
A create script writes a table only when the database is built or
rebuilt from the scripts. This database was last restored on 2026-09-10
from an earlier commit, so it carried none of the sixteen tables B.1 to
B.5a added. compass db status reports the schema a day behind head.
Nothing had exercised them, so nothing had noticed. The write path this
wave wires would have failed at the first save_trade_envelope
request, and the read path at the first query, with the table missing
rather than the code wrong.
The tables were applied in place rather than by a database rebuild,
because a rebuild drops the loaded data and the connections of a running
environment. The trading create aggregate is idempotent, so re-running
it created the sixteen and skipped the forty nine already present, and
the row-level security blocks for the sixteen were applied after it.
information_schema now reports all sixty five trading tables, each of
the sixteen with row-level security enabled and one tenant-isolation
policy.
5.11.5. The switch-over lands in four parts
** B.5a takes the trade envelope, because it is the second half of the
scope and because it is the smallest part that exercises the whole
shape: three tables, one of them the parent and two keyed children, on
the trade side rather than the instrument side.
** B.5b takes the issue row, its call dates and its conversion
targets, and the five product fact rows.
** B.5c takes the leg tables and the shared schedule, amount, rate and
amortization tables below them.
** B.5d takes the option block, the strike, the exercise schedule, the
forward, the delivery basket and the three return-side members.
5.11.6. Wave B.5b, the identity on the write path
The import handler saves the two halves of a bond trade that no earlier
wave wrote: the issue row, with its call dates and its conversion
targets, and the product's fact row. The read path in
trade_handler.hpp rebuilds the same three things, so a stored bond
instrument comes back as the container the mapper expects.
5.11.7. Decision: an issue is found by security id, never minted per trade
bond_issues_security_idx is unique on (tenant_id, security_id)
among the current rows. The mapper mints a fresh identifier inside
resolve_issue, and trade_mapper.cpp passes no lookup, so two trades
of one ISIN arrive at the handler holding two different issue
identifiers. Saving both rows would violate the index on the second,
and a re-import would violate it on the first.
The handler pages the stored issues once per run into a map keyed by
security id, two hundred rows to a page and five hundred pages at most.
A hit adopts the stored identifier and rewrites the instrument's
issue_id to match. A miss saves the issue row and records the minted
identifier, but only after the save returns success. A failed save
therefore leaves the security id out of the map, and the next trade of
it tries the issue again rather than opening an instrument against a
row that is not there.
The read is lazy. It runs at the first bond instrument of the run, and a failure stops the run with the response's own message.
5.11.8. The container holds no second copy of a bond-level member
Five members the issue row holds as columns were also on
bond_instrument_data: calendar, credit_curve_id,
reference_curve_id, income_curve_id and bond_notional. Both
mapper directions now point at the issue row, and the five were
deleted. The container and the tables hold one list rather than two
views of it kept in step by hand.
5.11.9. Finding: no case covered the two new child readers
The wave adds no test case. parent_scoped_queries gained
read_call_dates_by_issue_ids and
read_conversion_targets_by_issue_ids, and the header and the handler
were their only callers. Compilation was the whole of their coverage,
and it stayed so only until the extraction below.
The envelope path of B.5a took the other shape:
trade_envelope_reader is a service, and
service_trade_envelope_reader_tests.cpp drives it against a database
with four cases. The bond read assembly sat inline in
populate_instruments_for_trades instead. The two readers wanted the
same treatment, and the assembly wanted the same home, before Unit C
depended on either.
5.11.10. Finding: the fact row is chosen by the instrument's type code
save_bond_instrument writes one fact row, chosen by
trade_type_code and gated on the matching member being present. A
document whose type code and payload disagree therefore writes no fact
row, and the block is lost on export. The mapper produces both from one
source, so a round trip cannot reach that state, but a container built
by hand can.
5.11.11. What B.5b does not carry
The leg tables and the shared schedule, amount, rate and amortization tables below them stay empty, which is B.5c. The option block, the strike, the exercise schedule, the forward, the delivery basket and the three return-side members stay as they were, which is B.5d. The two readers of the finding above are the debt this wave records against Unit C, and the extraction below paid it before B.5c began.
5.11.12. No generated file was edited by hand
Six of the seven files this wave changes are hand written. The seventh,
trade_handler.hpp, is the generated path already recorded as kept by
hand; this wave deepens that debt rather than opening it. No file is
added or removed, so no component_files.cmake list changes.
5.11.13. The bond read assembly becomes a reader service
The finding above left two pieces of debt: the two child readers had no
case, and the bond assembly sat inline in trade_handler.hpp, the
generated path kept by hand. B.5c and B.5d both grow that assembly, so
it was settled before either landed rather than after.
populate_instruments_for_trades now calls bond_instrument_reader,
which mirrors trade_envelope_reader: a service under
ores.trading.core/service/ that takes instrument identifiers and
returns the containers keyed by them. It reads the headers in one
batch, each distinct issue once rather than once per instrument, the
two child lists in one query each, and the fact row of each instrument
from that instrument's type code. The inline block and the nine
includes it needed are gone from the handler, which keeps one call site
in the generated file where it held sixty lines.
service_bond_instrument_reader_tests.cpp drives it against a database
with four cases: children read back in ordinal order after being
written out of it, one issue serving two instruments, an instrument
with no header row absent from the result, and a fact row read only for
the instrument whose type code names it.
No generated file was edited by hand in this pass. The handler edit is a call site on the path already recorded as kept by hand, and it narrows that debt rather than widening it.
5.11.14. Finding: the case for two instruments of one issue caught a real defect
The handler built one container per instrument and never shared anything between them. The reader does: the issue-keyed child rows are read once and held in a cache that every instrument of that issue reads from. The first version moved the two child lists out of the cache and into the container, so the second instrument of an issue found a cache entry that was present and empty. Its call dates and conversion targets vanished with no error raised.
read_instruments_gives_two_instruments_the_one_issue fails against that
version, and it is the case written for exactly this shape. The lists are
now copied. trade_envelope_reader has the same read-one-batch shape and
never had the bug, because it pushes each child straight into the
container named by that child's own trade id, so no container is ever
built from a cache another container also reads.
5.11.15. Finding: the in-place refresh cannot add a column to an existing table
B.5 recorded that the sixteen new tables were absent from this database
and applied the trading create aggregate in place to add them. That
procedure cannot add a column: create table if not exists skips a table
that is already there, and every column in it.
Eleven columns were in that state. calendar, credit_curve_id,
reference_curve_id, income_curve_id and bond_notional on
bond_issue; knocks_out, price_type and redemption on
bond_option; initial_price, payer and price_type on bond_trs.
Every one was added by A.2 to B.4 to a table that predated it. The
database reported all sixty five trading tables present, and the B.5
finding's own check passed, while all eleven were missing.
Nothing failed until the reader tests wrote an issue row, which is the first write to that table since the columns were modelled. The first error named a missing column and the second a violated check constraint, and neither names drift.
scripts/ore_schema_column_drift.py reads the column list out of every
create script and diffs it against information_schema, over all two
hundred and seventy six declared tables rather than the trading family
alone. It exits 1 on a gap and prints the statements that close it under
--emit-alters, with each column's definition taken verbatim from its
create script rather than retyped. The eleven were applied and the check
now reads clean.
5.12. Wave B.5c, the leg family and the shared schedule tables
B.5b wrote the identity of a bond trade: the issue row and the product's fact row. This wave writes the instrument's own terms. Seven files change, and six of the seven are hand written.
5.12.1. The six tables get a surface
The sixteen tables B.1 to B.4 added have generated protocols, handlers
and registrars, and no registrar was called, so no subject reached any
of them. Six of the sixteen are this wave's: the four leg tables and the
two schedule tables. registrar_bond.cpp wires all six, so a running
service can read and write them.
The other ten stay unwired. B.5a wired the three envelope tables, and B.5d takes the seven that remain.
5.12.2. The six reads the generated repository does not offer
parent_scoped_queries gains one batch read per table, on the pattern
B.5b set for the issue's two child lists: legs, amounts, rates,
amortizations, schedules and schedule dates.
Each read filters on the instrument identifiers and the current
bitemporal version, and orders by the columns the container's own member
order needs, so a reader walks the rows in document order without
sorting again. The orders are (instrument_id, leg_role, leg_number)
for the legs and for the rates, that key plus sequence_number for the
amortizations, that key plus amount_role and sequence_number for the
amounts, and (instrument_id, owner_role, owner_number, schedule_role,
sequence_number) for the schedules, with the child adding
schedule_sequence_number ahead of its own ordinal.
read_schedules_by_instrument_ids reads every owner rather than the legs
alone. A schedule row's owner is a leg, an option block or a return
block, and one query serves all three, so B.5d reuses it as it stands.
5.12.3. The reader rebuilds the legs
bond_instrument_reader now returns the leg family. The six row sets
are read once for the whole batch and grouped by instrument, and each
header row whose instrument appears in the group becomes a
bond_leg_data.
The role decides the destination. A bond row appends to the leg list,
because the schema declares LegData unbounded. The other three fill the
single leg the schema declares, which is trs_funding, repo or
ascot_swap. A row whose role is none of the four is read and dropped,
because the container has no member for it.
A leg's schedule is found by its role. schedule_for selects the rows
whose owner matches the leg and whose schedule role names the member, so
one lookup serves the leg schedule, the payment schedule, the two inner
schedules of a floating rate and the synthesized payment dates block.
bond_leg_rate_data is a choice of three alternatives, and the rate row
carries the discriminator. to_rate_data builds the alternative the
rate_kind column names, and falls through to the fixed case, which is
the only one with no rate row members of its own: its numbers live in the
amount table under the rate role.
5.12.4. The writer saves the legs
save_leg writes one leg whole. It returns immediately when
bond_leg_data::is_empty() holds, which is the shape a container built
from a row set has, so an absent leg writes no row and reads back as an
absent leg.
Otherwise it writes the leg row, the amount lists, the rate row, the amortizations and the schedules in that order, and stops at the first failure with the response's own message.
The amount lists are written under the role that names them, numbered
from one within each role, so the reader's own ordering reassembles the
lists. A fixed leg writes its rates under the rate role, and a floating
leg writes spread, cap, floor and gearing.
save_bond_instrument calls save_leg once per bond leg in list order,
then once for each of the three single legs. The leg number is the
ordinal, counted from one.
5.12.5. Finding: payment_dates is a fifth schedule role
bond_leg_data carries a payment_dates list of dates. No table holds a
bare date list: the shared schedule table holds an entry, and an entry of
the dates kind holds a child row per date. So the writer synthesizes one
dates entry holding the list, under the schedule role payment_dates.
instrument_schedule.org names four schedule roles in its prose and
states the rule that binds them: "The container's member names are the
values". The container's member is payment_dates, so the fifth value
follows from the model's own rule. The column carries no check
constraint, so no schema change is needed.
The prose is now shorter than the rule it states, which is a defect in
the org and not in this wave. Correcting it means regenerating the
instrument_schedule entity, and the text is what a reader consults
rather than what the code executes, so it is recorded here rather than
changed in a wave whose subject is the switch-over.
5.12.6. Two type wrinkles at the column boundary
The fixing_days column is signed and the container member is not. A
negative column value would wrap rather than fail, so the reader engages
the member only when the value is zero or more. The writer casts back.
indexings_from_asset_leg is one optional flag in the container, and the
schema states Indexings as an unbounded list of a choice. The container
holds the FromAssetLeg member alone, which is the bound wave A.2b
recorded. The leg row carries the flag as a column, so a leg writes one
entry and reads one back.
5.12.7. What B.5c does not carry
The option block, the strike, the exercise schedule, the forward, the delivery basket and the three return-side members stay unwired, which is B.5d.
Two behaviours of the write path stay as B.5b recorded them, and this wave extends both to the legs. A re-import writes new rows rather than updating the existing ones, because the generated save handler is a bitemporal insert. And a leg the second import of a document no longer states keeps its row from the first, because nothing deletes a child row the new container omits. Neither is reachable from a document the mapper produced, and both are the same shape across every child table this program added.
5.12.8. No generated file was edited by hand
Six of the seven files are hand written: the queries header and source,
the reader's header and source, the import handler and the test file.
The seventh, trade_handler.hpp, is the generated path already recorded
as kept by hand, and this wave changes no call site in it.
No file is added or removed, so no component_files.cmake list changes.
5.13. Wave B.5d, the option block, the forward and the residue
B.5c wrote the legs and the shared schedules below them. This wave writes the last of what the container carries, and closes the two defects the gate surfaced while it was measured. The predicate this task was opened for is met: the in-scope counters read zero.
5.13.1. The seven tables get a surface
The sixteen tables B.1 to B.4 added have generated protocols, handlers
and registrars, and no registrar was called, so no subject reached any
of them. B.5a wired the three envelope tables and B.5c the six leg and
schedule tables. This wave wires the seven that remain:
instrument_options, instrument_option_premiums,
instrument_option_exercise_fees, instrument_option_payment_dates,
instrument_strikes, bond_forwards and
bond_future_delivery_baskets.
All sixteen now have a subject, so a running service can read and write every table this program added.
5.13.2. The seven reads the generated repository does not offer
parent_scoped_queries gains one batch read per table, on the pattern
B.5b and B.5c set. Each filters on the instrument identifiers and the
current bitemporal version, and orders by the columns the container's
own member order needs, so the reader walks the rows in document order
without sorting again.
Three of the seven are one row per instrument rather than a list: the option row, the strike and the forward. Those read into a single optional, so a second row for one instrument would be dropped rather than appended. No key allows one.
5.13.3. The reader rebuilds the option block and the residue
bond_instrument_reader now returns the option block, its three child
lists, the two spellings of its exercise schedule, the strike, the
forward and the delivery basket.
schedule_for no longer takes a leg. B.5c gave it the leg because the
legs were its only caller. The option block states an exercise schedule
and a list of exercise dates, and both are owned by option and not by
a leg, so the lookup now takes an owner role and an owner number. The
schedule role names the member in both cases, which is the rule
B.5c recorded.
The payment rule block is present exactly when the row states one of its three required members, because a document that stated the rule stated all three. The two arms of the payment data exclude each other, so a row with none of the three stated the date list instead.
Real members and optional blocks share the option row, and the two cannot be told apart by a null test. A flag per optional sub-block says whether the document stated the block and left it bare or omitted it, which is the decision B.3 recorded on the row.
5.13.4. Three members live on the fact row and nowhere else
The mapper writes the option type and the strike to the product's fact row and leaves the redemption, the price type and the knock-out flag nowhere, so a stored row carries them only because the writer put them there. The same is true of the return side's payer, price type and initial price.
Those six are copied back from the fact row rather than read from a column of their own table, which is why the reader applies them after the fact row is read rather than beside the option block. The option block itself is written for every product that states one, and not only for the product whose fact row carries the type and the strike.
5.13.5. The writer saves them
save_option_block writes the option row, the two child lists that
carry an ordinal, the payment dates and the two spellings of the
exercise dates. It writes nothing when the container holds no option
block, which is the shape a container built from a row set has.
save_strike, save_forward and save_delivery_basket follow it.
The forward returns without a request when the container states none of
its three members, so an absent forward writes no row and reads back as
an absent forward.
Each stops at the first failure with the response's own message, which is the shape B.5c set for the legs.
5.13.6. The gate reads zero, and it exits zero
Both trees were read with the same classifier, over
external/ore/examples, after the two repairs below.
Documents: 1566 Failures: 0 Skipped: 1113 Classified: numeric 2330, boolean 313 In scope, bond products and the trade envelope: lost 0, unexplained 0 Out of scope, the other instrument families: lost 81432, unexplained 9917 Worst relative error on a numeric pair: 5.660e-8 (tolerance 0.000001) EXIT=0
The in-scope counters are the acceptance line, and both read zero over every document in the corpus. The out-of-scope counters are the other instrument families, which this programme does not own, and they are counted rather than failed, which is the second acceptance line.
The exit code is zero now as well. The A.4 section records why it was not: the script counted every source document with no output as a failure, and 1,023 of them are configuration documents the exporter skips by design. The script now applies the exporter's own rule.
5.13.7. Finding: the check counted a document the exporter never writes
is_exported reads the source's root element and fails only when the
exporter is meant to write that document. The four roots are the ones
the exporter handles: Portfolio, CurrencyConfig,
CalendarAdjustments and Conventions. The rule is the exporter's own,
expressed again in Python, so the two agree on which documents have an
output to compare.
A skipped document is counted and not failed. A document the exporter does write, and which produced no output, is still a failure: the write was attempted and did not happen.
5.13.8. Finding: the currency maps were missing two codes
Applying that rule left exactly one genuine MISSING:
ORE-Python/Notebooks/Dependencies/conventions.xml. Bisection found the
child that causes it, the FX pair AUD-SKK, whose target currency is
SKK.
parse_currency_code is a hand-written lookup in every *_mapper.cpp,
and the generated currencyCode enumeration has 191 values. Five of the
six maps held 189: SKK and SSP were absent from the commodity,
composite, equity, fx and conventions mappers. The sixth, the swap
instrument mapper, already held all 191.
A currency the map does not name parses to nothing, so the pair is dropped and the document it sits in produces no output. Both codes are now in all six, in alphabetical position.
This is a defect in hand-written code that mirrors generated code, and nothing enforces the mirror. The map is a lookup table duplicated six ways against an enumeration that is generated once, so a regeneration that adds a currency silently stops six files from parsing it. It is recorded as debt rather than repaired here.
5.13.9. Finding: the exporter classified a document by substring
With the maps complete, 91 documents were still written wrong. A
CurveConfiguration document came back as an empty <Conventions/>.
read_header peeks the first 4,096 bytes and roundtrip() chose an
importer by looking for one of four markers in that window. A curve
configuration is none of the four, so it fell through to the
conventions branch whenever the word <Conventions> appeared in the
window, which it does in every curve configuration that names a
convention.
Academy/FC003_Reporting_Currency/Input/curveconfig.xml is 21,505
bytes and holds a curve configuration; its <Conventions> element sits
at byte offset 457. The roundtrip read it as a conventions document and
wrote 15 bytes to the same relative path. 89 curve configurations and 2
sensitivity analyses were rewritten as empty conventions files.
The A.4 section found this and recorded it as belonging to a task of its
own, because the repair decides what the tool does with the documents it
cannot name. The repair is in this wave, because the gate cannot read
zero over the corpus while the exporter rewrites 91 documents the corpus
contains. read_root_element skips comments, processing instructions
and declarations, and returns the name of the first element. The reader
is then chosen by that name alone, and a document whose root is none of
the four is refused rather than filed under a branch it does not belong
to.
The drift check in the diff script mirrors the same parse, so the two agree by construction and not by coincidence.
5.13.10. Finding: the exporter's classification has no test
exporter::roundtrip has one caller, the ores.cli roundtrip command,
and no test. read_root_element is in an anonymous namespace inside
exporter.cpp, so no unit test can reach it, and the four-way choice it
feeds is reachable only by walking a directory.
The defect above therefore had two lives: one before this wave, when the substring rule rewrote 91 documents with no error raised, and one after, where it is repaired but still unguarded. What caught it was the corpus gate, because a misrouted document produces no output and the gate counts that as a failure once the document's root says the exporter should have written it.
The gate is the system-level coverage and it does hold the behaviour.
The gap is a case at the unit level, and it is recorded here rather than
written now: roundtrip() writes to a directory, so the case needs a
temporary corpus, an output tree and an assertion about which documents
were written. That is a small test but not a free one, and the wave
already carries the change it would guard.
5.13.11. What the two repairs moved, and what they did not
The repairs changed which documents are written and not what any document holds, so every pair count is unchanged. Only the file counts moved.
| Measure | Before | After |
|---|---|---|
| Outputs written | 544 | 453 |
| Documents skipped | 1,022 | 1,113 |
| Convention files | 157 | 66 |
Every column closes: 544 - 91 = 453 written, 1,022 + 91 = 1,113 skipped, 157 - 91 = 66 convention files. The 91 documents moved from written to skipped, and no other document moved.
5.13.12. What B.5d does not carry
Two bounds stay open on the read path, and both are recorded rather than repaired.
A re-import writes new rows rather than updating the existing ones, because the generated save handler is a bitemporal insert. And a child row the second import no longer states keeps its row from the first, because nothing deletes a child row the new container omits. Neither is reachable from a document the mapper produced, and both are the shape B.5b and B.5c recorded across every child table this program added.
The full XML to database to XML path is not yet exercised. This wave wires the write and read sides and covers the read side with cases, but no test drives a document through the database and back out. That is unit C, and it is the next unit of this task.
5.13.13. No generated file was edited by hand
Every file this wave changes is hand written: the queries header and
source, the registrar, the reader's source, the import handler, the test
file, the five currency maps, the exporter, and the diff script. Three
of them sit under projects/ores.ore/core/src/domain/, which is
hand-written code and not xsdcpp output; domain.cpp and domain.hpp
are untouched.
No file is added or removed, so no component_files.cmake list changes.
5.14. Verification
Each wave records its build and suite result here. A wave that models a
table or a column also runs scripts/ore_schema_column_drift.py first,
because the database is restored at a commit and the create aggregate
cannot add a column to a table that is already there.
5.14.1. Wave B.5d, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with exit code 0, and the log ends at
[100%] Built target ores.qt.exe. The seven registrars, the seven batch
reads, the reader rebuild, the handler and the four new cases all
compiled and linked on that pass.
The sweep reads 70 of 71 suites passing, in 3456.50 seconds. The one
failure is ores.refdata.core.tests, and the section below records why
it is not this wave's. The host carried a build in another worktree for
the length of the sweep, which is why the total is the largest this task
records.
| suite | result | wall time |
|---|---|---|
| ores.trading.api.tests | passed | 0.10 s |
| ores.trading.core.tests | passed | 15.90 s |
| ores.trading.service.tests | passed | 0.58 s |
| ores.ore.core.tests | passed | 4.51 s |
| ores.ore.api.tests | passed | 0.51 s |
| ores.ore.service.tests | passed | 0.58 s |
ores.trading.core.tests carries the wave's four new cases. It reads
15.90 seconds against the 19.57 of the B.5c sweep and the 15.20 of the
B.5c re-run.
The six readings come from a re-run of those suites on their own, and
not from the sweep. A compass test run writes one host-wide slot log,
so the next run overwrites the last, and the sweep's per-suite lines
were gone before they were read. The sweep total, its result and the
failing suite are read from the sweep's own summary, which was captured
first.
ores.database.tests passed in the sweep at 10.82 seconds. The wave's
own earlier sweep failed it at 163.52 seconds, while another worktree
ran compass db recreate against the shared database. The two runs
together settle that failure as environmental.
No modeling file changed, so no schema drift applies and no codegen
drift is expected. The codegen drift check was run anyway and reads "No
drift: regenerated output matches the checked-in tree", and
regenerate_cmake_component_files.py --all --check reports every
component_files.cmake up to date, which follows from the wave adding
and removing no file.
5.14.2. Finding: the refdata core suite dies on its own failure path
ores.refdata.core.tests failed in the sweep at 271.51 seconds. It is
not this wave's. The failing case includes ores.database,
ores.eventing, ores.nats, ores.refdata, ores.testing and
ores.utility, and nothing from ores.trading or ores.ore, which is
all this wave changes.
It is the flake this task already records. B.1 and B.2 found
write_book_publishes_nats_changed_event failing on a temporal-validity
race in the refdata eventing fixtures, and recorded it as its own task.
The case that took the suite down here is a sibling of it.
The Catch2 XML carries the evidence. It holds 152 case elements. The
last, write_calendar_event_publishes_nats_changed_event, opens and
never closes, and the file has no OverallResults element. The suite
died inside its final eventing case and Catch2 never wrote a summary.
The suite's own per-case report stops at the case before it, which is
regenerate_is_extend_only_and_does_not_rewrite_existing_watermark at
43.004 seconds.
That case writes a calendar event, waits for its NATS notification, and reports a lost notification when its budget runs out. The budget is four attempts of twenty five polls at one hundred milliseconds, or ten seconds. The suite cost about nine seconds more than its completed cases and their per-case overhead explain, so the case spent its budget and the process ended as the case reported.
The suite passes on its own, 152 cases in 263.90 seconds against the 262.85 this task measured before. The flake is load sensitive, and a build ran in another worktree through both the sweep and the re-run, which is the load it needs.
Two defects in the harness were read from source while this failure was
investigated. Neither explains it: no case in this suite's run came near
the sixty second watchdog default, so the watchdog did not fire. They are
recorded here because they belong to ores.testing and not to this
task. The watchdog's test_running_ flag is a plain bool that the
main thread writes and the watchdog thread reads. The watchdog also logs
through Boost.Log before it reaches its std::cerr banner and its
std::_Exit, so a thread that holds the logging lock can stall the exit
path the timeout depends on.
5.14.3. Wave B.5c, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with exit code 0, and the log ends at
[100%] Built target ores.qt.exe. The reader, the writer and the three
new cases compiled and linked on that pass.
The sweep reads 70 of 71 suites passing. The one failure is not this wave's, and the section below records why.
| suite | result | wall time |
|---|---|---|
| ores.trading.api.tests | passed | 0.09 s |
| ores.trading.core.tests | passed | 19.57 s |
| ores.trading.service.tests | passed | 0.55 s |
| ores.ore.core.tests | passed | 4.08 s |
| ores.ore.api.tests | passed | 0.45 s |
| ores.ore.service.tests | passed | 0.73 s |
ores.trading.core.tests carries the wave's three new cases and reads
19.57 seconds against the 13.38 of the wave before.
The whole sweep took 2179.50 seconds, which is longer than the 884.80 of B.5b and is the one second per test case the watchdog join costs.
scripts/ore_schema_column_drift.py reads clean against this database:
two hundred and seventy six declared tables, every declared column
present. No modeling file changed, so no codegen drift check applies.
regenerate_cmake_component_files.py --all --check reports every
component_files.cmake up to date, which follows from the wave adding
and removing no file.
5.14.4. Finding: the listener loss-window test fails under load
ores.database.tests failed in the sweep, on
postgres_listener_service_reconnect_surfaces_loss_window at
postgres_listener_service_tests.cpp:367.
The case terminates the listener's backend, sends a notification into the window it believes is open, and then requires that the only notification to arrive is the one it sends after the listener is back. It received the one sent during the window instead.
The test's own comment states the assumption the failure breaks:
"PostgreSQL delivers NOTIFY only to live LISTEN sessions, so everything
sent from here until the reconnect is a lost window." The listener
reconnects on its own schedule. expect_gone proves the old backend has
gone and nothing more, so the reconnect can complete and reissue LISTEN
before the test sends the notification it intends to lose. That
notification is then delivered, and the follow-up wait for exactly one
received message is satisfied by the very message the test meant to lose.
The suite is a timing race over a reconnect, and it is not this wave's.
ores.database.tests links ores.database.lib, ores.testing.lib and
ores.logging.lib, so the seven files this wave changes are outside its
link closure and nothing in the component depends on ores.trading or
ores.ore. The suite passes alone in 20.60 seconds against the 159.70 it
took in the sweep, which is the load sensitivity the race predicts.
The reconnect path the test leans on was given an exponential backoff
policy in four ores.database commits that are all in this branch's
history, so the window the test measures has moved recently. The repair
is to close the race rather than to widen the sleeps: establish that the
reissued LISTEN has landed before sending, and wait for the notification
the test wants rather than for a count of one.
This is the third suite defect this task records. A.3b found the
ores.reporting retry that cannot work, and B.3 the ores.refdata
currency race. Each belongs to the suite it sits in, and together they
are why a green sweep is not on its own evidence that a wave is sound.
5.14.5. The reader extraction, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with exit 0, and the pass ends at
[100%] Built target ores.trading.core.tests with no error line. The
three new files, the handler, and the two regenerated lists all compiled
and linked on that pass.
The suite did not pass first time, and what it caught is the value of
this wave. The first sweep failed four cases in the new file, all on
column "calendar" does not exist, which is the drift finding above.
With the eleven columns applied, the second failed two: a second fixture
that left conversion_ratio at its zero default and so broke the
conversion_ratio > 0 check, and the shared-cache defect. The reader's
mock was the only mock in the file that was wrong; the defect was real.
The final sweep is 100% tests passed, 0 failed out of 71, in 913.40
seconds. ores.trading.core.tests is 90 cases, all passing, and carries
the four reader cases.
| suite | result | wall time |
|---|---|---|
| ores.trading.api.tests | passed | 0.09 s |
| ores.trading.core.tests | passed | 15.20 s |
| ores.trading.service.tests | passed | 0.59 s |
| ores.ore.core.tests | passed | 4.14 s |
| ores.ore.api.tests | passed | 0.52 s |
| ores.ore.service.tests | passed | 0.56 s |
scripts/ore_schema_column_drift.py reads clean against this database:
two hundred and seventy six declared tables, every declared column
present.
5.14.6. Wave B.5b, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with exit 0, and the log ends at
[100%] Built target ores.qt.exe with no error line. The three
translation units this wave edited that carry logic compiled and linked
on that pass: bond_instrument_mapper.cpp in ores.ore.core,
parent_scoped_queries.cpp in ores.trading.core, and
ore_import_execute_handler.cpp in ores.ore.service.
All seventy one suites pass, in one ctest run rather than the ranges
the earlier waves used, in 884.80 seconds. ctest reports 100% tests
passed, 0 tests failed out of 71.
| suite | result | wall time |
|---|---|---|
| ores.trading.api.tests | passed | 0.09 s |
| ores.trading.core.tests | passed | 13.38 s |
| ores.trading.service.tests | passed | 0.57 s |
| ores.ore.core.tests | passed | 4.45 s |
| ores.ore.api.tests | passed | 0.46 s |
| ores.ore.service.tests | passed | 0.58 s |
ores.trading.core.tests carries the four envelope reader cases of
B.5a and reads 13.38 seconds against the 14.35 of the wave before.
ores.ore.core.tests carries the mapper round trip suite whose
assertions this wave repointed, and reads 4.45 seconds against 4.64.
Both suites passed on the first run, so the not-yet-covered readers of
the finding above did not fail anything.
The build took about an hour of wall time because two builds ran at once on this host, this worktree's and another's, each on a different worktree. The suite was unaffected and ran alone.
git status lists seven modified files and no new file, so no
component_files.cmake list changes and no codegen drift check applies
to this wave. Six of the seven are hand written; the seventh,
trade_handler.hpp, is the generated path already recorded as kept by
hand.
5.14.7. Wave B.5a, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with exit 0, and the log ends at
[100%] Built target ores.qt.exe with no error. The five files this
wave added compiled and linked on that pass.
All seventy one suites pass over those binaries, in eight ctest ranges.
| range | result | wall time |
|---|---|---|
| 1-24 | 24/24 | 237.08 s |
| 25-27 | 3/3 | 246.13 s |
| 28-31 | 4/4 | 262.63 s |
| 32-34 | 3/3 | 14.92 s |
| 35-37 | 3/3 | 30.14 s |
| 38-50 | 13/13 | 20.02 s |
| 51-56 | 6/6 | 10.68 s |
| 57-71 | 15/15 | 88.32 s |
ctest reports 100% tests passed, 0 tests failed out of 71, in 909.97
seconds.
The three trading suites are the wave's own, at 0.09, 14.35 and 0.48
seconds. The three ore suites carry the mapper path, at 4.64, 0.55 and
0.71 seconds. ores.trading.core.tests carries the wave's four new
envelope-reader cases, which is why it reads 14.35 seconds against the
14.50 of the wave before.
The first run of that suite failed, and the failure was the database
rather than the code. Four cases read permission denied for table
ores_trading_trade_envelopes_tbl, because the sixteen tables had been
created in place rather than through setup_schema.sql and so carried
no grants. Once the grants were copied from a sibling trading table the
suite passed 86 of 86 in 13.32 seconds, and the sweep above followed.
projects/ores.sql/utility/validate_schemas.sh --strict reads two
hundred and ninety nine tables and exits 0 with no warning.
regenerate_cmake_component_files.py --all --check reports all
component_files.cmake up to date, so the five new files are registered.
No generated file was edited by hand. git status lists five new
hand-written files, the two headers and two sources plus one test,
three hand-written files edited, and the two component_files.cmake
lists the registrar script rewrote.
5.14.8. Wave B.4, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with exit 0, and the log ends at
[100%] Built target ores.qt.exe with no error. The two new entities'
generated files compiled and linked on that pass.
All seventy one suites pass over those binaries, in eight ctest ranges.
| range | result | wall time |
|---|---|---|
| 1-24 | 24/24 | 203.40 s |
| 25-27 | 3/3 | 222.41 s |
| 28-31 | 4/4 | 263.49 s |
| 32-34 | 3/3 | 15.35 s |
| 35-37 | 3/3 | 31.09 s |
| 38-50 | 13/13 | 19.60 s |
| 51-56 | 6/6 | 9.59 s |
| 57-71 | 15/15 | 80.57 s |
The three trading suites are the wave's own, at 0.21, 14.50 and 0.62 seconds. The three ore suites carry the mapper path, at 4.58, 0.70 and 1.22 seconds.
projects/ores.sql/utility/validate_schemas.sh --strict reads two
hundred and ninety nine tables and exits 0 with no warning, after the
row-level security fix recorded above.
5.14.9. Wave B.3, the build and the sweep
The build ran clean: ./compass.sh build --preset
linux-clang-debug-make finished with no error, and the log ends at
[100%] Built target ores.qt.exe.
Two findings came out of the verification. Both are older than this wave.
5.14.10. The trading component is not drift free
check_component_drift.py holds a registry of the components whose
regeneration leaves the tree clean. That registry carries refdata,
reporting, marketdata, compute-cpp, iam, iam-cpp and synthetic. It does
not carry trading-cpp, and the script's own header explains why a
component outside the registry cannot pass: a committed tree that
predates a template takes the newer per-entity families as untracked
files, and git diff cannot see untracked files.
Regenerating trading-cpp did exactly that. It rewrote sixty eight
tracked files that no part of this wave touches: the whole
projects/ores.qt/trading set, the trade entity's generated family,
nine equity instrument services, seven fx instrument services,
nineteen eventing integration tests, and trading_trades_create.sql.
Those files went back to a shape HEAD has moved past. The trade table
alone lost seven indexes.
The run also materialised a hundred and eight untracked files.
Every one of the sixty eight was reverted before this wave's commit. The wave's own files regenerate unchanged, so the verdict is about the component and not about this wave.
The drift gate cannot read zero for trading until someone brings the whole component to the templates. That is a separate pass, sized well past one wave, so this task records the position and leaves it.
5.14.11. The new files had to be registered, and the component overshoots
src/component_files.cmake is the explicit source list CMakeLists.txt
includes, so a generated file that is not in the list is never
compiled. The api and service lists took the wave's new files. The core
list did not, because the run that produced the other two happened
before the core files existed.
Re-running the generator for the core list exposed a second effect of the same drift. The generator scans the directory, so it also picked up the hundred and eight files the drift run had just materialised, and wrote them into the list. Committing that list would point the build at files that are in no commit.
So the hundred and eight came out first. They are generated files, the
orgs that produce them are on disk, and a copy went to
/tmp/drift_byproducts_1429.tar.gz before the removal. The untracked
count returned to its pre-drift figure of a hundred and sixty seven,
which is the check that the removal took exactly what the drift run had
created.
The three lists were then regenerated. They carry the wave's files, and they carry no entry whose file is absent.
The regeneration also registered files the earlier waves had left out:
the trade_envelope family, which B.1+B.2 committed but never added to
the api, core or service lists. Eighty four generated files were
tracked and never compiled. One hand-written header, the container,
was the family's only registered member.
The lists changed, so the build had to prove them. The rebuild compiled
seventy five core files, fifty five api files and ten service files
that had never been compiled, plus the eighty four envelope files, and
it finished at [100%] Built target ores.qt.exe with no error.
5.14.12. A test case costs one second of dead time
ores.testing/test_timeout_listener.hpp starts a watchdog thread in
testCaseStarting and joins it in testCaseEnded. The watchdog sleeps
in one second steps, so a join waits out the rest of the current
second, and every test case pays that second whether it needs it or
not.
The cost is one second per test case, on every suite that registers the
listener. Measured: ores.iam.core.tests has ninety six test cases and
took 97.69 seconds, ores.shell.tests has twenty six and took 26.66,
and ores.cli.tests has a hundred and twenty five and took 125.72. The
whole sweep therefore costs about one second per case, which for this
tree is tens of minutes.
It also makes a normal per-test timeout look like a hang. ores.cli.tests
failed at a 120 second limit and passed at 480, and a stack sample
taken while it "hung" showed the main thread inside
std::thread::join in testCaseEnded and the watchdog inside
sleep_for. No test was running at the time.
This is a defect in the harness and not in any test. It is not fixed here, because the fix is to wake the watchdog on a condition variable instead of polling, and that change belongs with the harness and not with the bond tables.
5.14.13. The sweep is green, all seventy one suites
The whole suite ran over the rebuilt binaries, in eight ctest ranges. The rebuild matters to the verdict: the trading libraries now carry the files the source lists had left out, and every suite that links them ran against the larger build.
| range | wall time |
|---|---|
| 1-24 | 197.58 s |
| 25-27 | 249.02 s |
| 28-31 | 263.52 s |
| 32-34 | 14.25 s |
| 35-37 | 30.07 s |
| 38-50 | 18.95 s |
| 51-56 | 9.33 s |
| 57-71 | 79.05 s |
The three trading suites are the wave's own: ores.trading.api.tests
at 0.09 seconds, ores.trading.core.tests at 13.63 and
ores.trading.service.tests at 0.51.
ores.refdata.core.tests passed at 262.85 seconds. The flake recorded
against it did not recur, which is the second clean run since the fix.
Most of the fourteen minutes is the one second per test case the watchdog join costs, and not test work.
6. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
7. PRs
| PR | Title |
|---|---|
8. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | A stated element whose value equals its type's default is dropped on export: SettlementDirty N, FairPrice 0 and ExpiryLag 0 all vanished from a crafted bond future probe. |
bond_instrument_mapper.cpp (reverse_bond_future) |
Decline in this branch | The fix is a presence flag per member in the generated bond_future container, so it is a modeling and codegen change, not a mapper edit. Filed as Keep stated but default-valued elements in the bond export. The corpus cannot measure the class: no document in external/ore/examples is a BondFuture trade. |
| 2 | format_number writes 1e+05 for 100000.0, because std::to_chars defaults to the shortest round-trip form. Reaches ContractNotional and FairPrice on the future, and option_strike on the option. |
bond_instrument_mapper.cpp (format_number) |
Decline | The value reparses to the same double, and the acceptance predicate counts a numeric spelling difference as classified rather than lost. Recorded debt. A fix must not trade 1e+05 for 100000.000000, which the same schema reads back as a different string. |
| 3 | Two call sites read a schema-declared string with a bare std::stoi and std::stod while the same file defines count_of and number_of for that job. The bare calls throw; the importer catches per trade, so one malformed value leaves the trade with an empty instrument and no product block. |
bond_instrument_mapper.cpp (map_bond_data, map_bond_option) |
Accept | Fixed in 611dcb81b3. Both call sites now go through the helpers, which is the policy the file already documents for a string-typed numeric field. |
9. Result
The bond products and the trade envelope round trip at zero loss on the mapper path, and every member the container carries has a column or a keyed child row, so the database holds what the container holds.
The gate reads zero over the whole corpus:
Documents: 1566 Failures: 0 Skipped: 1113 Classified: numeric 2330, boolean 313 In scope, bond products and the trade envelope: lost 0, unexplained 0 Out of scope, the other instrument families: lost 81432, unexplained 9917 Worst relative error on a numeric pair: 5.660e-8 (tolerance 0.000001) EXIT=0
9.1. The reading was retaken, and the later reading is the one that stands
The gate above was first read when wave B.5d landed. It was read again
after the untagged-variant codec was replaced by instrument_payload,
and the two readings agree on every number.
The second reading is the one this Result rests on, because the payload
sits on the mapper path. exporter::roundtrip encodes the instrument on
the way in and decodes it on the way out, so the B.5d reading verified a
codec that is no longer in the tree: it was taken with variant tagging
present, and tagging is what made every instrument decode as
std::monostate.
9.2. The four acceptance lines, and what answered each
- Zero lost and zero unexplained in scope, over every document in
external/ore/examples. The reading above is that, over all 1,566 documents. - The other families counted and not failed. The out-of-scope line carries the 81,432 lost and the 9,917 unexplained pairs of the other instrument families, and the run exits zero.
- The container carries every member the mapper needs, and the generated
binding stays untouched. Waves A.1 to A.4 grew the container, and each
one records that
domain.hppanddomain.cppwere not opened. - Every member the container carries has a column or a keyed child row. Waves B.1 to B.4 built the sixteen tables and the columns, and B.5a to B.5d pointed the read and write paths at them.
9.3. What closed the losses
Four classes, in the order they were closed. The container flattened
xsd::optional<T> into a plain member and tested .empty(), so an
element the document stated empty collapsed into an absent one; the
container now holds std::optional and the mappers emit on presence. The
container never held most of legData, and it held one leg where the
schema declares LegData unbounded. Five bondData members had no home
at all. And the whole trade envelope had no carrier, which wave A.4 gave
it, keyed by the trade rather than by the product.
The gate itself needed two repairs before its exit code meant anything:
it counted a document the exporter never writes as a failure, and its
boolean rule swallowed a real loss whenever a string element held the
text true.
9.4. The bounds that stay open
Each is recorded in its own wave and none is exercised by the corpus, so
none shows in the reading above. The fifteen legDataType alternatives
other than fixed, floating and formula-based. The FXReset and
Exchanges members of Notionals. The Indexing member of Indexings,
where the container carries FromAssetLeg alone. HistoricalFixings,
whose date member is named fixingDate rather than startDate. An
element the document states bare, as <ScheduleData/> with no Rules
and no Dates. The NettingSetDetails substitution group, which the
envelope carries in its NettingSetId form alone. Nine
totalReturnData members. KnockOut on a forward bond. And the eight
bondData elements with no column anywhere, which
Align bond_issue with the ORE
data model records.
Two hazards are worth carrying rather than closing. parse_ore_trade_type
scans 115 spellings through a hand-held count, so a regeneration that
adds a type would silently stop finding it. And a re-import writes new
rows rather than updating the existing ones, because the generated save
handler is a bitemporal insert.