Task: Migrate the reference data documents
Table of Contents
- 1. Goal
- 2. Status
- 3. Acceptance
- 4. Plan
- 5. Conventions moved last
- 6. The curve configuration table came before its projection
- 7. The curve configuration moved once its table was proved
- 8. The stress testing mapper had no reader
- 9. The run document table came before its projection
- 10. The run document moved onto the codec
- 11. The recursive member is an index into an arena
- 12. The portfolio table keeps every value of 374 documents
- 13. The projection follows the instrument families
- 14. Where the work stands
- 15. Measurements
- 16. Today's market moved onto the codec
- 17. Notes
- 18. Test Scenarios
- 19. PRs
- 20. Review
- 21. Result
This page documents a task in the Decide the XML strategy for ORE documents story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Move currency config, calendar adjustments, conventions, netting set definitions and credit simulation off the generated binding. Each family moves completely in one change: reader, writer, and the deletion of its mapper.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Decide the XML strategy for ORE documents |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-19 |
3. Acceptance
- Each family is read and written through the codec, with its mapper deleted in the same change.
- The corpus round-trip harness passes for each family's documents.
- No family has two live implementations at the end of the change.
4. Plan
One family at a time, each complete. A family moves reader, writer and mapper in one change, so no family ever has two live implementations. Calendar adjustments went first because it is the smallest and proves the shape every later migration reuses: its mapper is deleted, not retyped, and the projection that survives sits at the boundary beside the reading and the writing it serves. Currency config followed and needed two more codec helpers, because a currency's rounding type is an enumeration: the record holds an index and a projection speaks the schema's spelling.
The codec grew the vocabulary a projection needs. A projection receives the
record the generated table describes and states which of its members it fills,
because the writer writes only the fields whose presence bit is set. Four small
functions join the codec: find_field, find_child_type, is_present and
mark_present. A projection names a member the way the document does, so it
does not repeat the schema or hardcode bit numbers. Because the lookup accepts
an element or an attribute, the generator now refuses a type that spells both
the same, so the ambiguity cannot arise silently.
A date list is two rows. The element that holds the dates and the Date row
inside the record it points at are both rows, so a projection states both. The
first attempt set only the outer one, and the dates vanished from the writing:
the family's own round-trip test caught it before the change left the branch.
The importer reads text as well as files, so an export can be imported again without a temporary file, which is what the family's round-trip test does.
The netting set mapper had no reader. The ore-side mapper turned a
netting set document into mapped_netting_sets, and only its own tests
called it; the live netting set mapper is the refdata one on the database
side. So the family needed no projection at all: its table was already
written and proved, and the mappers and suites that duplicated it are gone.
Pricing engines moved as a projection, not a mapper. The family maps a
document into analytics product and parameter rows, which is work no table
can carry, so the class survives as pricing_engine_projection with the
codec record as its input. It also owns reading and writing, so its two
callers in the run store and the run configuration service ask it for a
document rather than binding one themselves.
Two families share one projection helper. Currency config followed calendar
adjustments, and its review found the table lookup copy-pasted in the importer
and the exporter, and the writer's error message still naming the calendar
table for a currency field. Both live in src/xml/projection.hpp now, beside
the rule that a projection states every row it fills and throws when the
schema no longer has it.
The remaining families, sized from a probe of the generator. Running the generator against each remaining schema says which need codec work and which need the generator extended first.
| Family | Table | Mapper | Blocker |
|---|---|---|---|
| CreditSimulation | generates | 291 lines, decomposes into five analytics tables | none |
| PricingEngines | generates | 241 lines, read by run_store and the run configuration service | none |
| CurveConfiguration | generates | 3,310 lines | none, but the largest |
| Portfolio | generates | instrument mappers, the largest group | none |
| ORE run document | generates | run document mapper | none; xs:complexContent extension is read as a flattened derivation now, and a restriction is still refused |
The generator reads a derivation. A type written as xs:complexContent
extension now produces the base type's fields followed by its own, which is
what flattening a derivation means for a record: the base's fields keep the
offsets the base gives them. A regression test builds a base and a derived
type and asserts the emitted record holds both, in that order. The run
document generates with it. It is not in the regeneration wrapper yet, so
nothing guards that table: it joins the wrapper in the change that migrates
the family, because a table with no projection would leave the census with a
supported family nothing reads.
One probe result was wrong. The table first said stress testing could not
be generated because the generator lacked its shiftTypeEntry shape. It was
the probe that was wrong: the type lives in sensitivity.xsd, and the family
generates as soon as that schema is read. Only the run document still needs
the generator extended, and that is xs:complexContent.
Credit simulation is not free. Its mapper is reached only by its own suites, which reads as dead code, but the configuration type is seeded and stored, so the family is a product concept with a test-only XML path. It is migrated, not deleted: the projection into the five analytics tables is the work.
5. Conventions moved last
conventions_mapper now takes the codec's records. read and write belong
to it, and split_conventions and join_conventions keep their signatures, so
the refdata side did not change. Enumerations are read as the schema spells
them and collapsed to the canonical code through one table per enumeration,
which a script derived from the old switch statements. Two codec functions were
added for the one repeated enumerated list.
The export bytes changed, because the codec writes the document with its own
serialiser. The recorded digests were taken again. A float the old binding held
as float is now read as the document wrote it.
The generated domain, the xsdcpp scripts and the two regeneration patches went
in the same change, with the 22 suites that only tested the generated binding
and its golden dataset. The seed test lists simulation and sensitivity as
types the codec does not carry.
6. The curve configuration table came before its projection
The family is the largest in the corpus: 89 documents and a mapper of 3,310 lines. Its table is generated and proved over every one of those documents before any of the mapping moves, because the table is the part a schema decides, and proving it first separates "the schema reads differently than we thought" from "the mapping was transcribed wrongly".
It read differently in three places, and each was silent. A choice nested in a
sequence, or whose alternatives are sequences, offered no members at all, so a
document writing an equity volatility's VolatilityConfig or a commodity
curve's SpotQuote did not bind. Two alternatives of one choice named the same
element, which is one member in a flat record. And a member named after a type
declared earlier in the same struct hid that type. The first two are generator
work a test now covers. The third is a struct keyword on the one member that
needs it, rather than a rename of a type whose name is fine everywhere else.
The census calls the family supported while its mapper is still the reader. That is the honest state of a two-step migration, and the evidence line in the census says so: the table reads and writes the document, and the projection replaces the mapper next.
7. The curve configuration moved once its table was proved
The mapper was 3,310 lines and its input was the binding's generated types. The projection is the same mapping with the table's record as its input, so the schema's shape reaches it once instead of twice, and the mapper, its header and its suite are gone. Nothing about the mapping moved: an element it could not hold is still refused, and segments are still restored in the order the document wrote them.
Two conversions the binding used to make silently are now stated where they
happen. The table holds every attribute as text, so a priority attribute is
parsed and formatted; and an element typed xs:boolean is text as well, because
it is not the ORE bool enumeration, so it is read as a flag and written as
true or false. A strike factor is a schema decimal in a double column, and
the boundary converts it.
8. The stress testing mapper had no reader
The family's mapper was 1,230 lines and two suites, and nothing in the product called it: the importer, the run store and the run configuration service never name a stress document, and the file type table says the import cannot yet parse one. Its only callers were its own round-trip suites, which is the shape the netting set mapper had.
So the family did not need a projection. Its table is generated and proved over
all 19 corpus documents, and the mapper and its suites are gone: the analytics
stress test store keeps its own tests, and a later feature that imports a stress
document will start from the table. The schema needs three files: stress.xsd,
sensitivity.xsd for the shared element types, and ore_types.xsd for the
enumerations. One name to know when reading the table: the SwaptionVolatility
Shifts/Shift record is generated as shift_type because its element is named
Shift, and it is not shift_type_entry.
9. The run document table came before its projection
The run document has the widest corpus of any family: 422 files under 235 names,
six of which carry no ore prefix, so the value harness finds it by its root
element. All of them bind, write and keep every value. The table is 269 lines
across the two generated files; the mapper it will replace is the one the run
store and the run configuration service call, and that projection is the next
change.
What the corpus does not state is worth knowing: every document writes Setup
and Analytics, most write Markets, and none writes Logging or Restore.
Those two members are proved by the schema and the table, not by a document, and
the projection that follows has no corpus fixture for them.
10. The run document moved onto the codec
The projection is the mapper's rename table with the table's record as its input, and it owns reading and writing, so the run store and the run configuration service ask it for a document. The mapper, its header and its suite are gone.
Two things do not survive positionally, and the suite states both rather than comparing records blindly: the Setup parameters are written in a fixed order because the entity is a row of columns, and each analytic's active flag is written first because that is where ORE writes it. The suite compares Setup by name and value and each analytic by name and value, and it compares the analytics in order and the markets as a subtree.
11. The recursive member is an index into an arena
The generator reads every shape the portfolio schema uses: a model group, a substitution member that writes its own type, a reference to a global element of an earlier schema file, an element with no type, a presence mask over more than 64 fields, and dependency-ordered types.
A composite trade holds a list of subTradeGroup entries whose SubTrade member
is a componentSubTrade, which carries oreTradeData, which reaches the
composite trade again. A record is a fixed layout and cannot hold that cycle, so
the edge that closes it becomes an index row: the member is a
std::vector<std::uint32_t> of indices beside a std::vector<T> arena in the
same record. The arena belongs to the record that holds the indices, so copying
the record copies its rows and no second object has to agree about their
lifetime.
The binder appends the row to the parent's arena and pushes its index before it descends, so no row of the parent moves while a child frame is open. The emitter follows the indices in order. A row two indices share is written twice, which is the trade-off the database model makes. An index past the end of the arena, a row no index names, a second row where the schema allows one, and nesting deeper than the binder's 32 frames are all refused rather than dropped.
12. The portfolio table keeps every value of 374 documents
The table is generated from input.xsd with the nine schemas it reaches. The
value harness binds, writes and reads back all 374 corpus portfolio documents,
including the 16 that nest a composite, and no value changes.
Three rules the corpus found, each a codec rule now:
- A real is carried as an exact decimal. ORE writes
0.050000000000000003and7342.8000000000002from a double, and no double reproduces that text, so the family states that every real is exact. The value a document states is the text it wrote. - A number may sit on its own indented line. The number's lexical space allows
the space around it and
std::from_charsdoes not, so the reading trims a number's text before it parses it. - The runs of one text are one value. A comment or a CDATA boundary splits an element's text, so the reading joins the runs and stores them once. A number split that way is the number its text states.
- A text element is not a number, whatever its name.
Strikeis a script variable in a scripted trade and a number in an equity digital option, so the exact decimal rule names reals only and leaves every text element as text. An element typedxs:simpleContentkeeps its number as its own text, and an element that states no text leaves that number absent rather than zero.
The census now reports 13 supported families, 0 migrating and 7 unused.
What the table does not check yet. An xs:choice group is flattened, and each
alternative is marked optional because the record has no way to state that one
of them must appear. instruments.xsd makes DeliveryStart with DeliveryEnd,
or PowerLoadProfileData, mandatory, and a portfolio document that states none
of them binds. The values are not lost; the reading is looser than the schema.
Teaching the table a required group is its own change.
13. The projection follows the instrument families
The product still reads a portfolio through the old binding: importer.cpp,
exporter.cpp, trade_mapper and one mapper per instrument family. The
projection is the same mapping with the codec record as its input, so the
schema's shape reaches the product once.
The shell owns the document, the envelope and the trade list. Each family owns the trade types it holds, and a family that does not handle a type returns nothing so the next one is asked. A projection names the elements it takes (the table is asked for the offsets), reads a money value as the exact decimal the table keeps and converts at the boundary, and refuses an element the mapping cannot hold rather than dropping it.
The scripted family came first because it is the smallest. The script's NPV is
a name the schema requires and the instrument's flat table has no column for, so
it rides in the parameters JSON the table does carry. Rates, FX, bonds, credit,
equity, commodities and composites followed, and the importer and the exporter
now ask the projection for the document and the trades. The nine mappers and
their thirteen suites are gone, so the product never runs both paths.
Three things the port changes on purpose. A quantity the old binding read as
float is now the document's exact decimal, so a stored amount no longer
carries float noise. An FX forward or swap settlement is written back, which the
old mapper read and dropped. A required element the trading domain holds no
value for is stated at its structural minimum (one unit of quantity, an absent
date, a zero initial price) rather than left out: the old binding tolerated the
absence, and the document it produced did not bind.
The export is the part the old boundary never proved. Thirty-one of the 132 corpus documents used to export a document the reading refused, because a writer left a required element unstated. Every corpus document now exports and reads back, and the directory round trip asserts it.
14. Where the work stands
Verified at the head of the portfolio projection branch, with the commands that prove each line.
| Claim | Evidence |
|---|---|
| Thirteen families are supported, each with a generated table, and none is migrating | python3 build/scripts/check_xml_family_coverage.py |
| Portfolio documents bind, write and keep every value | publish/bin/ores.ore.core.tests "every_generated_family_keeps_its_values_across_the_writing", 374 of 374 |
| Curve configuration documents bind, write and keep every value | same, 89 of 89 |
| Stress testing documents bind, write and keep every value, and nothing reads the XML | same, 19 of 19 |
| Run documents bind, write and keep every value | same, 422 of 422 |
| Every emitted document validates | ./scripts/validate_ore_examples.sh build/output/linux-clang-release-ninja/xml-roundtrip/emitted external/ore/xsd, 246 of 246 |
| The XML suites pass | publish/bin/ores.ore.core.tests "[ore][xml]", 372 cases and 44,364 assertions |
| The tables match their schemas, deterministically | python3 build/scripts/check_xml_bindings_drift.py |
| Legacy readers the product still calls: conventions and today's market | grep -rn "load_data" projects/ores.ore/*/src |
| Every corpus portfolio exports a document that binds | publish/bin/ores.ore.core.tests "roundtrip mirrors every portfolio in the ORE example corpus", 132 of 132 |
| Five product files still include the generated domain | grep -rc "domain/domain.hpp" projects/*/*/src --include=*.cpp |
What each remaining family needs. Portfolio's table is proved, and its
projection replaces the importer, the exporter, the instrument mappers and
trade_mapper next. Conventions and today's market are supported families whose
legacy readers the importer still calls, and they leave with the binding.
15. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit | |
|---|---|---|---|---|
| Families read and written through the codec | 3 of 5 (calendar adjustments, currency config, pricing engines) | publish/bin/ores.ore.core.tests "[ore][xml]" |
35e8c17b9c | |
| Families whose XML mapper was deleted because nothing read it | 1 (netting sets) | publish/bin/ores.ore.core.tests "[ore][xml]" |
674f35682b | |
| Pricing engine documents that round trip through the projection | every corpus document | publish/bin/ores.ore.core.tests "[ore][xml][roundtrip][pricingengines]" |
35e8c17b9c | |
| Calendar documents that round trip through the importer and exporter | 8 of 8 | publish/bin/ores.ore.core.tests "[ore][xml][roundtrip][calendaradjustment]" |
3260f93169 | |
| Currency configs that round trip through the importer and exporter | 13 of 13 | publish/bin/ores.ore.core.tests "every_currency_config_in_the_corpus_round_trips" |
3260f93169 | |
| XML suites | 509 cases, 79,240 assertions | publish/bin/ores.ore.core.tests "[ore][xml]" |
35e8c17b9c | |
| Curve configurations that bind, write and keep every value | 89 of 89 | publish/bin/ores.ore.core.tests "every_generated_family_keeps_its_values_across_the_writing" |
4e8b75caec | |
| XML suites after the curve configuration table | 497 cases, 82,020 assertions | publish/bin/ores.ore.core.tests "[ore][xml]" |
4e8b75caec | |
| Corpus documents the value harness sweeps | 776 | same | c19354f6b9 | |
| Run documents that bind, write and keep every value | 422 of 422 | publish/bin/ores.ore.core.tests "every_generated_family_keeps_its_values_across_the_writing" |
c19354f6b9 | |
| Stress testing documents that bind, write and keep every value | 19 of 19 | publish/bin/ores.ore.core.tests "every_generated_family_keeps_its_values_across_the_writing" |
488e82f0b8 | |
| Stress mapper .cpp lines deleted, with the 76-line header and the 748 lines of suites | 2,054 | =git show origin/main:projects/ores.ore/core/src/domain/stress_test_mapper.cpp \ | wc -l= | 488e82f0b8 |
| Curve configurations that project and project back | 89 of 89 | publish/bin/ores.ore.core.tests "[curveconfig]" |
97015dc883 | |
| XML suites after the curve configuration migration | 502 cases, 82,086 assertions | publish/bin/ores.ore.core.tests "[ore][xml]" |
97015dc883 | |
| Portfolio documents that bind, write and keep every value | 374 of 374 | publish/bin/ores.ore.core.tests "every_generated_family_keeps_its_values_across_the_writing" |
e39030319c | |
| Corpus documents the value harness sweeps | 1150, with each family's count asserted | same | e39030319c | |
| Families supported after the portfolio table | 13 supported, 0 migrating | python3 build/scripts/check_xml_family_coverage.py |
e39030319c | |
| XML suites after the portfolio table | 508 cases, 45,275 assertions | publish/bin/ores.ore.core.tests "[ore][xml]" |
e39030319c | |
| Portfolio projection suites | 1172 assertions in 28 cases | publish/bin/ores.ore.core.tests "[portfolio][projection]" |
cb494c4e8c | |
| XML suites after the portfolio projection | 372 cases, 44,364 assertions | publish/bin/ores.ore.core.tests "[ore][xml]" |
cb494c4e8c | |
| Corpus portfolios that export and read back | 132 of 132 | publish/bin/ores.ore.core.tests "roundtrip mirrors every portfolio in the ORE example corpus" |
cb494c4e8c | |
| Mapper files and suites deleted with the portfolio path | 9 mappers, 13 suites | git show --stat cb494c4e8c |
cb494c4e8c | |
| Today's market documents that project and project back | 99 of 99 | publish/bin/ores.ore.core.tests "[todaysmarket]" |
see the PR | |
| XML suites after the today's market projection | 370 cases, 44,389 assertions | publish/bin/ores.ore.core.tests "[ore][xml]~[database]" |
see the PR |
16. Today's market moved onto the codec
todays_market_projection replaces todays_market_mapper and owns reading and
writing, so the run store and the run configuration service ask it for a
document. The twenty-four collections are four shapes, so one table of codecs
states each collection once, and one table of binding attributes serves both
directions. The old mapper held the same knowledge twice, once per direction.
The mapper's tests moved with it. The corpus walk now compares records, not
written text, so an attribute written empty and one left out stay different. It
finds 99 documents, not 98: simm_todaysmarket.xml has a root element the old
walk's file-name prefix missed, and it round trips. The two structural suites
that loaded the generated binding duplicated the value harness and are gone.
17. Notes
- A rename now fails instead of writing nothing.
mark_presentreports a name the table does not have, and the projection throws rather than emitting a document missing the field. - The netting set family has a table and no reader. Its XML mapper was reached only by its own tests, so the mapper and those tests are gone and the family is covered by the binding, writing and round-trip suites that walk the corpus. A sprint-26 story records netting sets as reading and writing through the refdata store; that store is live, but the ORE XML path into it never had a caller, and this change makes that explicit.
- The reading is stricter than xsdcpp. The binder refuses an element or an
attribute the schema does not name, and the importer throws. That is the
codec's policy on a family with no
xs:any, and it is the behaviour the tables were chosen for; a document ORE tolerates but the schema does not describe is a document to look at, not to import quietly. - The boundary still throws.
import_calendar_adjustments_textand the exporter report a malformed document withstd::runtime_error, like every sibling in the importer and exporter. Converting the whole boundary tostd::expectedis one change for all families, not one per family.
18. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
19. PRs
| PR | Title |
|---|---|
| #3565 | [ore] Read conventions through the codec and remove the generated domain |
| #3563 | [ore] Read and write today's market through the codec |
| #3456 | [ore] Read and write calendar adjustments through the codec |
| #3470 | [ore] Read and write currency config through the codec |
| #3486 | [ore] Delete the netting set XML mapper nothing reads |
| #3532 | [ore] Generate the curve configuration table |
| #3544 | [ore] Read and write the curve configuration through the codec |
| #3545 | [ore] Generate the stress testing table |
| #3547 | [ore] Generate the run document table |
| #3548 | [ore] Read and write the run document through the codec |
| #3549 | [ore] Read the schema shapes the portfolio document needs |
| #3555 | [ore] Index the member a record cannot hold |
| #3556 | [ore] Read and write the portfolio document's numbers exactly |
| #3560 | [ore] Read and write the portfolio through the projection |
20. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
21. Result
Every reference data document is on the codec, conventions included. The family census reports 13 supported, 0 migrating and 7 unused. The XML suites pass, and every portfolio in the example corpus exports a document that binds.
The generated domain is gone. No product file includes it, and the XML suites, the family census and the drift check pass.