Investigation: what ores.ore should own

Table of Contents

A point-in-time record, written on 2026-09-26, of a question the design and documentation survey raised but could not settle. The question is not how to tidy ores.ore; it is what ores.ore is for.

The measurements below are of the component as it stood that day, and one of the four jobs it counts has since been cut back. Read it for the reasoning about scope and ownership; read the component models for what the component is.

1. The stated intent

The owner's model, given while reviewing the survey:

ores.ore should not be exposing NATS. It should just be a library to provide mappings from and to the ORE XML representation.

That is a narrow, testable definition. A library that maps ORE representations to ORES domain types and back needs: the ORE bindings, the mappers, the readers that turn ORE bytes into those bindings, and the writers that go the other way. It does not need a database, a message bus, an executable, or a shell command surface.

The rest of this page measures how far the component is from that definition, so the gap can be closed deliberately rather than by attrition.

2. What the component owns today

ores.ore is a composite of three parts, api, core and service, plus a root modeling/ directory that holds the one entity model. Every C++ line in the component falls into one of four jobs:

Job Facets Lines Is it ORE representation?
A. ORE representation core/domain minus the bindings, core/xml, core/log 11,318 hand-written Yes
— the xsdcpp bindings core/domain/domain.{hpp,cpp}, domain_xsd.hpp 56,645 generated Yes
B. ORE market data readers core/market 1,105, of which 639 read and 466 do not Mostly
C. Import orchestration core/planner, core/hierarchy, core/scanner, the import protocols in api, service/app, service/config, the import handlers in service/messaging 4,461 No
D. The series_key_shape entity api/domain, core/repository ~1,400 plus SQL No, but read in process

Job A is the component's reason to exist and is in scope by the stated intent. The other three are what the component does besides map ORE representations.

Job B, the market data readers, in more detail. This facet holds two different things. market_data_parser (free functions parse_market_data and parse_fixings) and fixing read ORE's -market*.txt and fixings*.txt text formats. That is representation work and belongs in scope. series_key_registry and fx_quote_convention_checker are not: the first decomposes an oresmd key using rows from a database table, and the second validates a currency pair against ores.refdata conventions. They are ORES logic that happens to be about ORE files.

Job C, import orchestration. ore_import_planner decides what an import will do before anything is written: which currencies to import and how (create, update, skip), whether a re-import replaces or merges, and what defaults a trade gets. ore_hierarchy_builder turns a flat scan into a portfolio and book tree. ore_directory_scanner classifies files. Around them sit the ore.import protocol, the object-storage tarball helper, and the workflow registration. None of this maps a representation; all of it decides policy and drives a saga. It is a service.

Job D, the series_key_shape entity. This was the component's largest unjustified surface when this page was written: a full CRUD stack for one entity, with a 35-class NATS protocol, change events, a sample generator, a presentation renderer, a service layer and a shell command unit. It has since been cut back to what is used. The entity's model page justifies the table belonging to ores.ore, because the key grammar is ORE's and a table lets ORE add a type later without a rebuild, and that argument covers the data and the repository that reads it. It never covered the surface, and nothing served the surface: the registrars were composed nowhere and the shell unit was never in the build. The protocol, the events, the generator, the presentation mapper, the service layer, the shell command and the TypeScript twins are now disabled in the model, leaving an entity, a repository and their SQL. What remains is still not ORE representation, and the ownership question below is unchanged: a reference table one component reads in process could live anywhere.

Job D was unwired. Of the four registrar families the regeneration produced, three were declared, defined, compiled and called from nowhere, so the nine ore.v1.series_key_shapes* subjects answered nothing and the generated shell command unit was not compiled because it landed in a directory no CMake target owned. All of it is now removed rather than wired: the surface served nobody, and a reference table an operator does not edit needs no commands.

3. The three external use cases

Fourteen includes reach ores.ore.core from outside and three reach ores.ore.api. They fall into three use cases, and they are not equally defensible.

1. ores.marketdata — seven files, the substantive case.

File What it reaches for
core/src/service/import_service.cpp parse_market_data, parse_fixings, fixing, series_key_registry, fx_quote_convention_checker, series_key_shape_repository
core/include/…/oresmd/oresmd_projections.hpp fx_quote_convention_checker
service/src/app/feed_ingest_loop.cpp series_key_registry, series_key_shape_repository
two test files the parser and the fx checker

This is the honest use of the component: ores.marketdata imports ORE market data files, and the only code that knows ORE's text formats is ores.ore. Two of the six things it reaches for are the leak. It builds the key registry from series_key_shape_repository{}.read_latest(ctx), a direct read of another component's table, and it checks FX quotes against ores.refdata through a type that lives in ores.ore.

2. ores.compute — one file, and clean. wrapper/src/app/log_publisher.cpp calls parse_ore_log_line(line) and gets an ore_log_line struct. That is 226 lines of pure format parsing with no state, no database and no bus. It is exactly what the stated intent describes, and it should stay.

3. ores.shell — two files, and both are avoidable. application/…/trading/ore_commands.cpp serialises planner::import_choices to JSON for an ore.import request, and calls exporter::export_portfolio on the items that come back from a trading NATS reply. The shell is a client: it should ask over the protocol, not link a library. The second file, ore/…/series_key_shape_commands.cpp, does use the generated protocol, and is not compiled.

Note what the shell does not do: it drives the import through workflow.v1.ore.import. Job C is already server-side. Only the small type that parameterises the request, and the export step on the reply, are on the client.

4. What follows from the stated intent

Three consequences, in order of confidence.

The entity (Job D) does not belong in this component. The owner intent and the entity's own justification point the same way: the grammar is ORE's, but the entity is an oresmd series key shape, and oresmd is ores.marketdata's vocabulary. ores.marketdata already models market_series, market_observation, feed_binding and the oresmd/quote_type models, and it is the only real consumer of the table. Cutting the surface back has already removed the component's NATS exposure, its shell unit and its events; what a move would still buy is the entity and its repository, its SQL schema, and its key registry. It would also cost a full ores.marketdata regeneration: that component is not in COMPONENTS_UNDER_TEST, so its committed output predates the current templates, and adding one model to it materialises 135 files, of which about thirty are the entity's.

Import orchestration (Job C) is a service and should be named as one. Either a slimmed =ores.ore.service keeps it, in which case the component is a library plus a service and the api holds only the import contract, or it moves to its own component and ores.ore becomes a pure library with no executable at all. The first is a smaller change and keeps the ores.ore.service registry entry, database role and account. The second matches the stated intent exactly.

Two pieces of Job B are in the wrong place within the component. series_key_registry should follow the entity to ores.marketdata, since it exists to decompose keys using that entity's rows. fx_quote_convention_checker either follows it or moves to the shared contract, because its input is ores.refdata conventions rather than ORE.

5. Options

Option What moves Effect on the component
0. Remove the unwired surface (done) The entity's protocol, events, generator, presentation mapper, service layer, shell command, recipe and TypeScript twins Already landed. The component exposes six subjects and serves all six; the api is a third smaller. Does not move the entity, so the ownership question stays open.
1. Record the remaining shape Nothing further ores.ore stays a library, a service and the entity's table. Cheapest, and the survey's finding 2 is answered with a reason rather than a change.
2. Move the entity to ores.marketdata The entity and its repository and SQL, plus series_key_registry Puts the table next to the only code that reads it. Costs that component's whole regenerated backlog in the same branch: 135 files, about thirty of them the entity's.
3. Move the entity, and move the saga out Jobs C and D ores.ore becomes core only: a pure library with bindings, mappers and readers. The strongest match to the stated intent, and the largest change: a new component, a new service registry entry, and the shell's export step must move onto a protocol.

Options 2 and 3 both require the shell to stop calling exporter::export_portfolio directly. That is a small, independent piece of work and can be done first.

6. Recommendation

Option 0 is taken: the surface was the part of this component that no evidence supported, and removing it closes the survey's P03, W01 and D13 findings outright. The remaining question is smaller than it was and no longer blocks the clean standard, which is why it is recorded rather than forced.

Between options 1 and 2, the argument for 2 is that the entity is ores.marketdata's vocabulary and it is the only consumer. The argument for 1 is the cost: 135 files of another component's backlog would ride along in the same branch, none of it this component's to regenerate, and the entity's own contribution to that number is about a quarter of it. That coupling is the real obstacle, and it is worth clearing first: bring ores.marketdata to a regenerable state in its own task, then move the entity in one clean step. Until then the table is documented as living here deliberately.

Option 3 is unaffected by any of this and remains the target if ores.ore is to stop hosting a service at all.

7. See also

Emacs 29.3 (Org mode 9.6.15)