Task: Import a pricing engine configuration as ORE XML

Table of Contents

This page documents a task in the Make every configuration screen follow one standard story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

A person brings an ORE pricing engine document into a pricing engine configuration, through the standard five-step import, with the server doing the parse and the review naming every addition, change, removal, unknown type and dropped element before anything is written.

2. Status

Field Value
State DONE
Parent story Make every configuration screen follow one standard
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-28

3. Acceptance

  • The server parses the document and answers what it holds, what the vocabulary does not know and what the target does not hold.
  • The preview states added, changed and removed rows, with Keep it as the default for a removal.
  • The import writes the configuration through the same save path the screen uses, and the run is the person's, with a reason and a commentary.
  • The screen draws Import on the list header and the shell exposes the same two operations.

4. Plan

4.1. The pair, and where each part lives

Part Where Why there
The two operations ores.ore.modeling, generated to C++ and TypeScript RS-ORE-06: every ore.v1.ops.* operation lives in the ORE component
The parser, the document and the difference ores.ore.core, domain/pricing_engines_import The one place that knows how PricingEngines maps onto the analytics rows, beside pricing_engine_projection which maps them the other way
The handler ores.ore.service, pricing_engines_handler It reads and writes the configuration through analytics' own messages under the caller's delegated session
The routes the BFF's pricing-engines.ts POST /api/reporting/pricing-engines/ore-import/preview and /ore-import, carrying the file's text and the choices
The steps the web's shared OreExchange module RS-ORE-07: one module carries the buttons, the Export dialog and the Import steps
The command ores.shell, pricing_engines import The same two operations, with --dry-run as the preview

4.2. The protocol

Two requests and two responses, in the shape the currencies' pair established:

  • preview_pricing_engines_import_request carries the file's text and the target: the reporting configuration to compare against, or nothing for a configuration the file would create. The response states the root element it found, the products and settings the file holds, the names of the root-level settings, the difference in counts and in rows, and three lists of what the system does not know.
  • import_pricing_engines_request carries the same file, the same target or a new configuration's name and variant, the keys of the removed rows the person kept, and the reason and commentary. The response states the configuration it wrote, what it added, changed, deleted and kept, and the rows it could not write.

A row of the difference is named by a key: a product by its type and which product of that type it is (Swap#0), a setting by its product's key, its scope and its name (Swap#0/engine/DiscountingCurve). The key is what a choice names, and it is what makes two products of the same type, or a setting repeated twice in one scope, tellable apart.

4.3. The parse

pricing_engines_import::parse reads the text with pugixml and answers the same analytics pricing_engines_document the exporter writes: the products in their order, each carrying Model, ModelParameters, Engine and EngineParameters, then one GlobalParameters block, with a position per scope so the export comes back in the order the file wrote. A text that is not well formed, or whose root element is another document's, answers a refusal that names the root element it found and writes nothing.

Three things the tables cannot hold are named rather than dropped:

What Kind What happens to the row
an attribute or element with no column dropped written without it
a product with no type, a setting with no name, a product with no model or engine missing the product is left out, or the row is named and the save refuses it
a setting stated twice in one owner and scope duplicate the first value stands, because analytics refuses a repeated setting
a product type the vocabulary does not hold unknown_type written as the file states it, and named

The vocabulary is analytics': the handler reads the pricing engine types and passes the codes to the difference, so the parser decides nothing about what a screen may offer.

4.4. The difference, and the person's choices

A product is matched by its type and by which product of that type it is; a setting by its product, its scope and its name, and then by which repeat of that name it is. Nothing is matched by identity, because the file states no identifiers.

Only removed rows ask a question, and the review starts every one of them at Keep it: a file that omits a row usually omits it by accident. pricing_engines_import::kept_by_default states that default once, so the screen and the command cannot default differently, and the import request carries the keys of the removed rows the person kept.

4.5. The write

The import re-reads the target and re-parses the file, so the write acts on the configuration as it stands rather than on the preview's reading. It assembles the document with pricing_engines_import::merge: the file's rows laid over the target's, each row keeping the identity and the version of the row the target already holds, so a write corrects a row rather than replacing it and the history stays one line per setting. A kept removal is carried from the target with the settings it owns; a removal the person dropped is absent from the document, which is how the analytics save removes it.

It then saves through analytics.v1.pricing_engines_documents.put, the same operation the configuration's own screen uses, so the validation, the history and the party rules are the screen's, and the person's reason and commentary travel on the rows, which is where that save keeps them.

4.6. A configuration the file creates

The screen's create writes the reporting configuration the record fills and then the document, because analytics finds a document by that reporting configuration. Both the browser and the shell import, so neither may carry that order: the ORE service's handler registers the reporting configuration through reporting's own put_configuration, saves the document, and removes the header again if the save is refused, so a refused import leaves nothing behind.

4.7. The official configuration, and what is not built

The configuration screen standard makes a file put into an official configuration a proposal, with a sign-off. Sign-off is not built. The import writes directly, with the person's reason and commentary, and the review states that in as many words before the press. It is the one place this build does not reach the standard, and it is recorded as the known gap rather than dressed as an approval.

4.8. The screen

Import stands on the list header between Refresh and Workflows (RS-ORE-02), at /reporting/pricing-engines/import, and follows the standard's five steps on the steps UI: the file, what it holds, what the system does not know, the target (new or existing), and the difference with Keep it as the default for a removed row. The preview is read twice: once on leaving the file step, which states what the file holds and what the system does not know without a target, and again on leaving the target step, which states the difference against the configuration the person chose.

The five steps are the shared module's, not the kind's: OreImportPage takes a kind that declares its document, its target and its wording, so the currencies and the pricing engines draw the same dialog. The kind's own file states what a row is called and what the review offers for it, and nothing else.

4.9. The command

pricing_engines import <id or --new-name> --file <path> [--dry-run] sends the same two operations, prints what the file holds, what the system does not know and the difference, and then writes, keeping every removed row, because a command line states no per-row choice. --dry-run stops after the preview.

4.10. The order**

  1. The protocol, generated to C++ and TypeScript.
  2. The parser and the difference in ores.ore.core, with its tests.
  3. The handler: the preview, then the import, with the reporting configuration's registration.
  4. The BFF's two routes, with their tests.
  5. The shared steps module, generalised for a kind with a target, and the pricing engines' Import page and list action, with their tests.
  6. The shell command and its tests.
  7. The build, the suites, the local checks, and the live round trip.

5. 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.

5.1. The build

What Value Command Commit
The tree builds bare ./compass.sh build passes ./compass.sh build a74fd129ed
Import cases in the ORE core suite 10 grep -c 'TEST_CASE(' projects/ores.ore/core/tests/pricing_engines_import_tests.cpp a74fd129ed
Cases in the ORE core suite 416 (475709 assertions) publish/bin/ores.ore.core.tests a74fd129ed
Cases in the shell reporting suite 577 (1599 assertions) publish/bin/ores.shell.reporting.tests a74fd129ed
Pricing engines route tests in the BFF 33 grep -c 'it(' projects/ores.web/packages/bff/src/pricing-engines.test.ts a74fd129ed
Import step tests in the web package 11 grep -c 'it(' projects/ores.web/packages/web/src/ore/OreExchange.test.tsx a74fd129ed

5.2. The live round trip

The environment was recreated and provisioned with compass db recreate -y -k and compass smoke ticks --leave-running, which left 24 services running, the acme_corporation tenant provisioned and 44214 ticks on 10 routes.

What Value Command Commit
Pricing engine configurations the seed holds 23 across 4 parties select count(*) from ores_analytics_pricing_model_configs_tbl where valid_to > now() a74fd129ed
Products and settings the export wrote for the UK party's XVA Overnight 8 products, 19 settings, 1 configuration setting grep -c '<Product ' / '<Parameter ' tmp/live/xva-overnight.xml a74fd129ed
The exported document, previewed against the configuration it came from 0 added, 0 changed, 0 removed, 27 held, no issues POST /api/reporting/pricing-engines/ore-import/preview a74fd129ed
The same document previewed against Pre-Trade Quick Price 17 added, 1 changed, 0 removed, 7 held POST .../ore-import/preview a74fd129ed
A wrong root element success: false, root_element: CurrencyConfig, and the sentence naming both POST .../ore-import/preview with a currency document a74fd129ed
Configurations in the table around a preview 26 before, 26 after select count(*) ... before and after a74fd129ed
The shell import into a new configuration 27 added, 0 changed, 0 deleted, 0 kept pricing_engines import --new-name ... --file ... a74fd129ed
The API import into a new configuration 27 added, configuration 01a17fd2-6bc3-7310-9d13-b33fcf99f76e POST .../ore-import a74fd129ed
The document exported, imported and exported again byte identical, twice (once by the shell, once by the API) diff tmp/live/xva-overnight.xml tmp/live/imported2.xml a74fd129ed
The review of a file that omits rows 2 removed rows first, 4 choice buttons, 2 checked — Keep ours on each Playwright, tmp/live/shots/step8-removals-review.png a74fd129ed

6. Notes

  • The parse is pugixml's, and the root element is refused with the element it found named, as the currencies' import does. The difference from that import is that nothing here is matched by identity: the file states no identifiers, so a product is matched by its type and by which product of that type it is, and a setting within its product by its scope and its name.
  • The position an import writes is the place a setting takes in its own scope. The live round trip is what found that the first cut numbered it by which repeat of its name it was, which put every first occurrence at zero and let the export break the ties by id: the values were right and the order was not. The round-trip case now carries three settings of one scope in an order no sort would produce.
  • The vocabulary that decides whether a product type is known is analytics', and the service reads it: a type it does not hold is reported, and the row is still written, because the tables hold any type code.
  • A target the caller names and analytics refuses is a refusal of the whole preview, in analytics' own words. One case follows from that: a configuration whose header exists but which holds no document cannot be a target, because analytics' read finds nothing. A person who wants to fill such a configuration has to start a new one, or fill it from the screen.
  • A change reason is required, and the analytics save keeps the one a row states. The import states the person's reason on every row it writes, which is how the screen's own save carries one.

7. What the standard asks that this build cannot express

The configuration screen standard's Import section, step 4, says that a file put into an official configuration is a proposal, drawn for sign-off. Nothing in the system is a proposal and no sign-off is built, so the import writes directly, with the person's reason and commentary, and the target step says so before the last press rather than promising an approval it cannot make. Making it a real proposal needs the lifecycle the standard's own States section names for official configurations — a state, a reviewer and a transition — and that is a task of its own. This is the one place where the build does not reach the standard, and it is recorded as a gap rather than dressed as an approval.

Two smaller ones follow from the same shape:

  • The standard says the review "claims the version read". The analytics save takes the document whole and stamps it, so the import carries the version of every row it touches and the save's own conflict rules apply; there is no separate claim the screen makes and the service checks.
  • The standard's third step names three kinds of unknown. This document has a fourth: a product with no model, with no engine, or with no type at all, which analytics refuses as a whole document. The preview names each as missing rather than leaving the person to meet the refusal after the press.

8. 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
Import a pricing engine configuration as ORE XML PENDING Runs on 21002 after provisioning.

9. PRs

PR Title
#3999 [ore] Import a pricing engine configuration as ORE XML

10. Review

The review of #3999 found five things. All five are taken, on fix/import-pricing-engines-review, with the tests the review asked for.

# Comment summary File Decision Notes
1 A new configuration's header can be orphaned: it is registered before the vocabulary read, and the final catch never removes it pricing_engines_handler.cpp Taken The order and the rollback are now run_pricing_engines_import in pricing_engines_import_run.cpp. Everything that can refuse is read before anything is registered, the registration is the last step before the save, and the catch removes it however the run fails. The regression test makes the vocabulary read fail on a new-configuration import and asserts no configuration remains, and a second case makes the save refuse so the rollback itself is driven
2 read_target reads read->message before it tests !read pricing_engines_handler.cpp Taken The missing reply is tested first and answers its own sentence; the service refusal is read only from a reply that came back
3 Merged rows may keep the file's random pricing_model_config_id pricing_engines_import.cpp Taken Confirmed in the code: the analytics save writes the rows it is given as they stand (`pricing_engines_document_service::save` stamps the tenant, the party and the audit fields and nothing else), so a row would have been written under an id no configuration holds. merge now takes the configuration the document fills and stamps every row with it. The core case reads each row's id back for an existing target and for a new configuration, the run case does the same through the save, and the live run shows no product or parameter row pointing at a configuration that does not exist
4 No size cap on the request's content pricing_engines_import.cpp, pricing-engines.ts Taken pricing_engines_import::maximum_content is four megabytes, refused by parse with the size and the limit named, and the BFF states the same limit and holds its body to twice it (a body is the document and its escaping)
5 Log a failed rollback; build the type#n key once; share the removal walk between the difference and the merge pricing_engines_handler.cpp, pricing_engines_import.cpp Taken The rollback that fails is a warning; pricing_engines_import::product_keys is the one builder of a product key, which the review's rows and a refused row's name both ask; the rows the file omits come from one walk that the difference states and the merge carries the kept ones from

One suggestion did not fit the code as it stands: the review's regression test is asked for at the handler, and this component's handlers talk to analytics and reporting over subjects their own test server cannot stand in for without racing the running services. The orchestration was therefore made a function with each step handed in, so the test drives the order and the rollback with fakes instead of a fake broker — the property under test is the same one.

11. Result

11.1. The build

Committed on feature/import-a-pricing-engine-configuration-as-ore-xml:

Commit What
762199c75f The protocol: ore.v1.ops.preview_pricing_engines_import and ore.v1.ops.import_pricing_engines, generated to C++ and TypeScript
05ed11f12d pricing_engines_import in ores.ore.core: the pugixml parse, the difference, the default for a removed row and the merge, with its tests
706aaf3586 The handler in ores.ore.service, the reporting configuration it registers for a new one, and its registration
fe2409e8c3 pricing_engines import <id or --new-name> --file <path> [--dry-run] in ores.shell
be1c136a82 POST /api/reporting/pricing-engines/ore-import/preview and /ore-import, and their tests
1518407e81 The shared steps generalised for a kind with a target, the pricing engines' Import page and list action, and the words in English, French and Portuguese
612c8892b6 The story's row and this task
55747a26a0 Current main merged in, which the bare build needed: the branch was cut before the market data work landed
b757d735a6 The equity underlyings regenerated from their model. Main's committed output no longer matched the generator and broke the bare build twice, in a unity collision and in a generated test
1e8b3e3b55 a74fd129ed Formatting, the regenerated list request descriptors, the ordering fix and the route walk
cefca27ab4 The import's trail starts from the kind's own area, which the live run found reading "Reference data" over a Reporting screen

11.2. What was proved

  • ./compass.sh build (bare) passes, and passes again after every commit that followed it.
  • The ORE core suite passes (416 cases, 475709 assertions) and holds the 10 import cases; the shell reporting suite passes (577 cases); the analytics suites pass; the BFF suite passes (1100 tests in 81 files); the web suite passes (2232 tests in 179 files).
  • The live round trip, on a database recreated with compass db recreate -y -k and provisioned with compass smoke ticks --leave-running (24 services, 44214 ticks on 10 routes):
    • The UK party's XVA Overnight exported to tmp/live/xva-overnight.xml: 8 products, 19 settings, one configuration setting, well formed under xmllint --noout.
    • Previewed through the BFF against the configuration it came from: 0 added, 0 changed, 0 removed, 27 held, no issues — the export and the importer agree.
    • Previewed against Pre-Trade Quick Price: 17 added, 1 changed, 0 removed, 7 held, the changed row stating "Model DiscountedCashflows → CrossAssetModel; Engine DiscountingSwapEngine → AMC".
    • A currency document offered to the preview: success: false, the root element CurrencyConfig named, and the sentence "This file is not a pricing engine configuration: its root element is CurrencyConfig. Choose a PricingEngines file."
    • The configurations in the table around a preview: 26 before, 26 after.
    • The shell import into a new configuration: 27 added, and the configuration 01a17fd0-9cf9-77e2-9aec-e4c5311dd467 exported again is the same document, byte for byte.
    • The API import into a new configuration: 27 added, the configuration 01a17fd2-6bc3-7310-9d13-b33fcf99f76e, and its ore-document read back with content-type: application/xml and content-disposition: attachment; filename"pricingengine.xml"= is the same document, byte for byte.
    • The reason and commentary the person states reach the rows: the configurations made by the shell and the API carry system.external_data_import, and the one made in the browser carries the reason the review started on.
    • Playwright drove the five steps on the running environment (tmp/live/drive_import.mjs, screenshots in tmp/live/shots/): the counts and the product list, "everything in the file is known", the target step, the review, the reason and commentary last, and the press. A second run with a file holding one product against the seeded XVA Overnight drew the two removed rows first, each with four choice buttons of which the two Keep ours were checked, and the sentence "Writes over XVA Overnight, in place. Every row the file omits is kept unless you say otherwise, and the history records the change."
  • The live round trip is what found the one defect the unit tests could not: the ordering of the settings within a scope. It is fixed, and the round-trip case now carries three settings of one scope in an order no sort would produce.

11.3. The review branch

The five things the review of #3999 found are taken on fix/import-pricing-engines-review (see * Review). What that branch proved again, on a database recreated with compass db recreate -y -k and provisioned with compass smoke ticks --leave-running:

  • The UK party's XVA Overnight exported as 8 products and 19 settings.
  • Imported through the shell into a configuration that exists — the case the review's third finding was about — 13 rows were added and the export of that configuration is the file's document, byte for byte. No product and no parameter row in the database points at a configuration that does not exist, which is what the finding would have caused.
  • Imported through the API into a new configuration: 27 added, and the export is the file's document, byte for byte.
  • A file whose product states no model is refused by the save, the refusal names the row by its key (Swap#0), and no configuration with that name is left behind — the rollback, live.
  • A body of four megabytes and one byte is refused over HTTP with "An ORE pricing engines document may be at most 4194304 bytes."
  • The suites: ORE core 444 cases (478894 assertions), ORE service 6 cases (the first this target has held), BFF 1139 tests in 83 files, and ./compass.sh build passes bare.

11.4. The checks

python3 build/scripts/local_checks.py --exclude-heavy runs 41 checks and passes

  1. The eight that fail carry main's inputs, not this branch's:
Check Why it fails on this tree
web Prettier refuses eight files main merged unformatted (inbox/, marketdata/Published*). The typecheck passes, and this branch's files are formatted
er-diagram The committed ER diagrams are stale against the equity underlyings main added
money-decimal-columns ores.trading.equity_basket_constituent::initial_price, from main
protocol-twins ores.reporting.report_result_row has no TypeScript twin, from main
handler-permissions refdata::equity_underlyings:* is checked and never seeded, from main
event-registrars The equity_underlying registrar is not composed, from main
entity-writes A SQL function main added writes an entity table
codegen-tests Two cases fail on the equity_underlyings permission seed, above

format, bff-list-requests and unity-collisions were red when this task started and are green now: the first two because of this branch's own files, the third because the regeneration of the equity underlyings repaired main's collision.

Emacs 29.3 (Org mode 9.6.15)