Task: Consolidate the ORE element maps

Table of Contents

This page documents a task in the Normalise every ORE parameter representation story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

One table states which ORE element a leaf column is and which value set states its values, with the projection columns attached to it, and one link shape joins a parameter or a leaf to its set. Nothing else in the story has to guess which of three maps wins.

2. Status

Field Value
State DONE
Parent story Normalise every ORE parameter representation
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-31

3. Acceptance

  • One table keys a leaf by its address, (schema_name, xpath), and it is the only statement of which value set governs a leaf.
  • Every projection column that carries a leaf is attached to that map, so both guards read the map and the parameter contract is no longer a prerequisite of enforcement. Done: both guards join the child and the map.
  • Every row of the three tables is represented in the new map, and a test counts the leaves and the columns before and after. Done: reporting_ore_element_map_test.sql states the 480 leaves, the 482 columns, and that neither side holds a row the other does not.
  • Both guards' pgTAP suites pass unchanged.
  • The leaves the maps name and the contract does not describe are listed by the map, each with a decision: a value set, or text. Done: every one of the 480 names the authority that decided it, and none is left unaccounted.

4. Discovery

Measured from the seed scripts rather than from a database, so the numbers do not depend on which schema a checkout's database holds: evidence/leaf_maps_overlap.py, with its output in leaf_maps_overlap.txt beside it.

Source Rows Leaves Schema
curve configuration columns 212 123 curveconfig.xsd
convention kind terms 270 176 conventions.xsd
vocabularies 4 4 curveconfig.xsd
the parameter contract 2339 293 both, and ore.xsd
setting links to a value set 760    

4.1. The three maps do not overlap

The curve columns name only curveconfig.xsd leaves and the convention terms only conventions.xsd ones, so their intersection is empty. The vocabularies table names four curveconfig.xsd leaves, and for each of the four the contract path resolves the same set to the same code:

Leaf Set, both ways
BusinessDayConvention curve.business_day_convention
Currency curve.currency
DayCounter curve.day_counter
YieldCurveDayCounter curve.day_counter

So the tree holds two column maps, one per schema, and a four-row duplicate of what the contract already states. It does not hold three statements of one leaf.

4.2. The real gap is coverage

Of the 299 distinct leaves the two guards check, 245 resolve to a value set and 54 resolve to none, because the contract holds no row for them at all. A leaf the map names and the contract does not is silently unguarded: the guard's join finds nothing and admits every value. The 54 are mostly lists (AtmTenors, CapStrikes, DetachmentPoints, Index1, OptionTenors, SwapTenors, and others) plus a few leaves whose spelling differs from the contract's (Accuracy, Guess, Qualifier).

Both findings are in leaf_maps_overlap.txt, leaf by leaf.

4.3. What that changes

The consolidation is not the de-duplication of three equal statements. It is one map keyed by the ORE element that absorbs the two column maps, absorbs or removes the four-row duplicate, and shows the 54 leaves that no set governs. Enforcement then reads the map, so a tenant that has not published the parameter contract still meets the guard; today it meets nothing.

5. The map is keyed by the leaf's address

The first cut keyed the map by (schema_name, element_name). That key is wrong, and a test found it: SpotDays is xs:integer in two conventions and xs:string that ORE reads as a period in a third, so a rule keyed by the name gave the commodity forward's integer leaf the CMS spread option's period rule, and a count of days was refused.

The key is the leaf's address in its schema. element_name stays beside it, so a person can read and search the map, and a vocabulary that holds for every leaf of a name (Currency, DayCounter, BusinessDayConvention) is stated once in ore_parameter_vocabularies.json and applied to every address that declares it. That is a rule, not a fallback: the declaration says the name is a vocabulary, and the map says which addresses declare it.

Measured with that key: 480 addresses over 482 columns and 288 element names, of which 181 addresses take text, and 31 carry a curated vocabulary.

6. Design

The map is one table keyed by the ORE element, with the projection columns as a child. Both guards read it directly, so enforcement stops depending on the parameter contract, and the seeding stops depending on a column being noticed.

6.1. The two tables

ores_reporting_ore_parameter_vocabularies_tbl already keys a leaf by (schema_name, element_name) and carries value_set_code, name, source, source_ref and reason. It becomes the map: no rename in this task, and a rename to ore_element_map is its own small task, because the name is carried by generated C++, TypeScript, shell recipes and their tests.

Table Keys States
ores_reporting_ore_parameter_vocabularies_tbl (the map) (schema_name, element_name) the value set that governs the element, and where its provenance came from
ores_reporting_ore_element_map_columns_tbl (new child) (element_map_code, entity_name, column_name) the projection column that carries the element, its table, and the xpath the column's leaf sits at

The child is what the two existing column maps become: the curve configuration columns (212 rows) and the convention kind terms (270). The xpath stays on the child because it is the leaf's address in the document, which is what the import and the convention screens show.

6.2. What the guards join

Today a guard walks three tables: its column map, then the contract on (schema_name, xpath), then the setting link on (scope, name). After this task it walks two: its child rows for the entity the trigger names, then the map, then the value set. The contract keeps its own job, which is documentation for the screens, and stops being a prerequisite of enforcement; a tenant that has not published the contract still meets the guard.

6.3. Why every column with a leaf is mapped

The seeding decides what is guarded. generate_curve_column_guards.py already knows every column of every curve family table and the ORE leaf it carries (it writes the xpath today), so it also writes the child rows and the map rows, with text where no set exists. The convention terms are generated the same way. A column whose leaf is known but has no set therefore lands in the map as a decision to be made, instead of vanishing as the 54 do today.

6.4. The slices, each one verifiable on its own

  1. The map gains the child table, and the seed writes both from the three sources. Both guards read the map; their pgTAP suites pass unchanged, which is the proof that the same column still resolves the same set.
  2. The convention CRUD and the ORE import read the child instead of ores_refdata_convention_kind_terms_tbl, then the two old column maps and their populate scripts go.
  3. The 54 leaves the contract does not describe get their decision: a set where one exists, text where the document takes a list. The generator emits them, so the count of unmapped leaves goes to zero and stays there.
  4. value_set_code gains a foreign key to the value sets, which closes the misspelt-code gap the knowledge cluster records.

Slices 1 and 2 are this task. Slice 3 is value-set curation and belongs to the story's canonical-form tasks. Slice 4 rides slice 1, because it is the same migration.

6.5. Out of scope

The canonical rendering of a value (next task), the screen that shows a leaf, and any change to what a value set states. This task moves where a leaf's set is found, not what the set says.

7. What slice 1a found

The map and its child are seeded, and the switches are built, but two things had to be settled before the guards could read them.

7.1. The curve column map on the trunk was stale

Regenerating it added six business_day_convention columns that the projection had gained and the committed seed had not. They were unguarded on the trunk, and generate_curve_column_guards.py --check failed there before this task touched it. The regeneration is in this slice, so the trunk's check goes green.

7.2. An element-keyed map guards leaves that were silent, and two of them fail

The old guards matched a column's xpath against the contract, so a column whose path the contract did not hold was never checked. The map is keyed by the element, so it guards every column that carries a leaf, including those. That is the point of the change, and it exposed two refusals on paths that today pass.

The first was the currency catalogue, and it was a fixture, not a defect. The catalogue read is fine: ores_reporting_value_in_set_fn reads ores_refdata_currency_catalogue_v for the caller's tenant, which is the tenant the publish writes as. The catalogue was empty because nothing had published iso.currencies for that tenant. The curve column suite already publishes them for the same reason; the publish test now does too, and its seven assertions pass with the switch in place.

The second is a wrong rule the map inherited. With the currencies present, the conventions publish refuses 2 for commodity_forward_convention.spot_days: SpotDays resolves to convention.period, whose pattern requires a unit, so a count of days is refused. The old guard never applied that rule to this leaf because the contract held no row at that path. The element map applies it to every SpotDays column, which is what an element key means.

So the blocker for slice 1b is not the catalogue. It is that the rules the map inherits are stated per parameter and were never exercised on the leaves the map now applies them to. That is slice 3, the curation of the map's rules, and it has to come before the guards read the map: SpotDays is a whole number of days (convention.integer), and the link that says otherwise is the thing to fix.

The switch itself is written and was exercised. Under it the curve column suite passed unchanged and the convention suite failed only on its expected error text, which the map's key changes from Swap.FixedFrequency to swap_convention.fixed_frequency. Both are recorded here rather than lost.

8. The addresses the map leaves unconstrained

181 of the 480 addresses take the shared text set, which admits every value. They are not one problem. evidence/text_addresses.py splits them, and its output is beside it:

Group Addresses What it needs
the contract states text, and no other address of that element has a rule 76 nothing: a person decided the leaf is free text
the contract states text, but the same element has a rule elsewhere 26 a review: the statement is probably wrong
the contract holds no row, and a sibling address has a rule 12 a proposal to accept or reject
the contract holds no row, and no sibling has a rule 67 a decision from the schema type and the ORE parser

The 26 and the 12 are the cheap end, because a rule already exists for the same element somewhere. The 67 are the real curation: mostly lists of tenors, strikes, dates and indices, where the honest answer is often a stated decision that the leaf is a list rather than a single value.

Two examples of the 26, and why they matter: SettlementDays, FixingDays and SpotDays state text at some addresses while another address of the same name takes a rule, and DayCounter and BusinessDayConvention state text at two addresses while the map elsewhere states a catalogue. Each is either a wrong statement or a name that is two leaves, which is the SpotDays problem again.

Five of the 26 are now corrected. The extractor states no parser for those members, so it typed them text; the ORE schema states xs:integer for every declaration of SettlementDays and FixingDays and for the commodity forward's SpotDays, and convention_value_sets.py now states that type with the reason. The unconstrained count falls from 181 to 176.

Twenty-one are left, and they are not one job either. The Index, Conventions and Calendar addresses state text where a sibling states curve.text, which is the same rule under another code, so the question there is whether two text sets should exist, not whether the leaf is text. The DayCounter and BusinessDayConvention addresses at IborIndex and OvernightIndex state text where a catalogue exists.

Those three are now stated as the catalogue, and the ORE source is why. The extractor typed them text and it was faithful: IborIndexConvention::fromXML reads DayCounter and BusinessDayConvention with getChildValue into std::string and never parses them (OREData/ored/configuration/conventions.cpp:428 and :430), and OvernightIndexConvention reads its day counter the same way at :470. So the engine accepts any string there and a typo in one changes nothing, silently.

The field means the same vocabulary the other twenty conventions state, so the rule is the catalogue and the engine's permissiveness is not a licence. A value the catalogue does not hold is refused with a sentence that names the set, which is a better answer than a typo the engine ignores. The unconstrained count falls to 173.

What was left of the 26 was eight addresses of Index, Conventions and Calendar that state text where a sibling states curve.text. Those two are one rule under two codes: both are the text kind, both are named Text, and neither constrains anything. They are now one set, text, and curve.text is gone.

The collapse corrects a number this page carried. Counting only addresses that name text understated the gap, because curve.text was counted as a rule when it constrains nothing. The honest statement of the map is: 274 of the 480 addresses are free text, and 206 state a real rule. 195 of the 274 are a decision the contract makes; 79 have no statement at all.

So the 26 contradictions are closed, and the remaining work is the 79 addresses no contract row describes, plus a review of the 195 that say text where the schema declares something narrower.

9. The map states the rule from the ORE parser

The 79 were not a curation backlog. They were the signature of a wrong source.

A leaf's set was resolved through the parameter contract: the leaf's xpath joined a contract row, and the row's (scope, name) joined the setting link. The contract holds a row only for a leaf a person wrote text for, so a leaf ORE reads and the guide does not describe had no rule and took text by omission. Measured with the extractor as it stood, 79 of 480.

The rule now comes from the parser the ORE source reads the leaf with, through the curation the value set seed is written from. A leaf's rule and the rule of the setting that shares its name are one fact, so the map reads the resolved curation (generate_value_sets.resolved()) instead of deriving the answer a second way, and the contract keeps only its own job, which is documentation for the screens.

That changes what a row says. source is ore_code, source_ref is the ORE source line that reads the leaf, and reason is generated from the parser and the set's kind, so a reviewer checks the rule instead of trusting it. The 232 leaves that stay text state why they stay text: ORE never parses the value, or it does and this tree holds no catalogue of the names it accepts.

Three numbers moved, and only one of them by curation:

  Before After
leaves that state a rule 206 220
text, and the ORE source says why 195 232
text, because no ORE source read names the leaf 79 28

The 14 new rules are leaves the extractor already read and the contract did not describe: BMAPaymentConvention, BMAPaymentLag, OvernightLockouDays, Accuracy, Guess, MaxEvaluations, the two NthWeekday/Weekday leaves, BalanceOfTheMonth, OffPeakHours, Deposit/SettlementDays, LongTermAtmType, LongTermDeltaType and UseLastFixingDate. The 51 that fell out of the "no contract row" group did so because they now have a term: 37 of them state text with a reason, and 14 state a rule.

The 28 that remain are the extractor's gap, not the model's: extract_curve_terms.py reads getChildValue, getChildNode, getNodeValue and their typed forms, and these leaves are read with getChildrenValuesAsStrings, getChildrenValuesAsDoublesCompact or a name built from a variable. UNREAD_ALLOWED in the generator holds the number so it can fall and not grow.

9.1. A rule from the parser is enforced, and a test says so

The 14 leaves that gained a rule are enforced by the existing guards, which read the map, so the proof is a refusal. refdata_convention_terms_validate_test.sql now writes a BMA basis swap with bma_payment_convention = 'Follwing' and expects the refusal to name convention.business_day_convention. Before this slice the leaf took text, so the same typo was written and nothing refused it: the test fails on the previous commit and passes on this one.

9.2. The collapse left a red suite

Collapsing curve.text into text in the previous slice changed a link's set code from curve.text to text, and one assertion read the code's prefix: reporting_ore_parameter_curve_value_sets_test.sql required every set a curve leaf uses to start with curve., so it failed on the next run. The assertion was a proxy for "a curve leaf does not take a convention or pricing vocabulary", and the shared free-text set is not a family vocabulary: it is the one set that says no vocabulary governs the value. The assertion now reads the set's kind and states that rule, so it can still catch a curve leaf pointing at another family's catalogue.

10. The extractor learns the list forms

The 28 leaves the map could not rule were mostly lists, and the reason was the extractor. `READ` knew `getChildValue`, `getChildNode`, `getNodeValue` and their typed forms; ORE reads a list with `getChildrenValuesAsStrings`, `getChildrenValuesAsDoublesCompact`, `getChildrenValuesAsPeriods` and `getChildValueAsPeriod`, and none of them matched. A leaf read that way produced no term, so no parser stated a rule and the leaf took text.

Two changes, both in extract_curve_terms.py:

  • the four accessors are read, and each records the type of one item: a template that names its item (parseListOfValues<Real>) states a list of numbers, a template that names none states a list of strings, and the compact forms state what their name says;
  • a leaf read with getNodeValue takes the type of the call that wraps the read, so parseListOfValues<Real>(getNodeValue(node)) is a list of numbers and parseBool(getNodeValue(node)) is a flag.

The second change is why the count moved so far. A report grid's leaves were all typed text because the read is a getNodeValue, and the parse sits around it; reading the wrapper types the grid's flags as flags and its expiry and strike lists as lists. The parser reuses one variable for every child, so a read is matched to the child that precedes it, bounded by the next read into the same variable: without the bound the first child's parser typed every later child and a grid's strikes became the flag beside them.

  Before After
leaves that state a rule 220 253
text, and the ORE source says why 232 227
text, because no ORE source read names the leaf 28 7
leaves the ORE source reads as terms 483 508

The 7 that remain: SwapTenors, BondTenors and SmileSwapTenors are named by a string the code builds (underlyingLabel_ + "Tenors"), so no read spells them; UseEffeciveVolatility is spelled that way in ORE and read elsewhere; DefaultCurve/Type, IndexInterpolated and Qualifier the extractor still does not reach.

11. A set says how many values it governs

A value set stated a kind and nothing else, so it could say what one period is and could not say whether the document writes one period or a list of them. ORE writes a list into a single element, and a numeric set already split on commas to admit one, with a comment saying why. That was a rule in the wrong place: it applied to every numeric parameter, whether or not its element holds a list.

ores_reporting_ore_parameter_value_sets_tbl gains cardinality, one or many. The kind states what one value may be; the cardinality states how many. ores_reporting_value_in_set_fn splits on the comma when the set says many and holds every item to the kind, and refuses an empty item, which is a trailing or doubled comma. A set of one refuses the comma, because a comma there is a document the engine cannot parse. The numeric split left the function with the comment, because curve.number_list now says the same rule as data.

Keeping the two facts apart keeps the kinds at nine. The alternative is a kind per combination, period_list beside period and so on, and then a screen reads a name to know whether its control repeats; with a cardinality it reads the kind for the control and the cardinality for whether it repeats.

11.1. The link cannot hold two leaves of one name

The map resolves a leaf's rule by the leaf. The setting link still keys a rule by (scope, name), and two leaves of one section can share a name while ORE reads them differently: the equity volatility curve's Expiries is a list of strings, and the report grid's beside it is a list of periods. The link holds one, so a name whose drawn leaves disagree is not linked, and curve_value_sets.py returns it in conflicted and generate_value_sets.py prints it. Today no drawn leaf disagrees: the link follows the leaf the projection draws, and only that leaf.

This is the same lesson as the contract one level down, and it is the next slice: a setting keyed by scope and name cannot state two rules, so either the link gains a subtype or the curve screens read the map by address.

11.2. A list is checked item by item, and a test says so

reporting_ore_parameter_list_value_set_test.sql states both directions: a list of numbers is in curve.number_list, an item that is not a number is not, an empty item is not, a list of periods is in curve.period_list, a value that is not a period is not, and curve.period, which is one value, refuses a comma.

12. The ORE schema is the second authority

The parser says what the engine accepts; it is not the only authority. Where the parser reads a leaf as a plain string, the schema still declares a type, and a leaf declared xs:integer is not free text: a document that writes two there is one the engine cannot parse, even though the parser hands the string on.

ore_schema_types.py walks the schema from the document's root element to the leaf, following a named complex type and an inline one, and states the leaf's type when that type is narrower than xs:string. A type the schema enumerates is left alone for now, because the values are the schema's and the set needs labels of its own.

The schema speaks in two places. A leaf the parser reads and states no more of takes the schema's type, after the parser's own answer. A leaf the extractor cannot reach at all takes the schema's type too, because nothing else speaks, and the row says so. The parser stays first in both, so a leaf the parser reads as a number keeps the number rule even where the schema is looser.

evidence/schema_types.py prints the two authorities side by side. Before this slice 16 leaves stated text where the schema declared a narrower type: six xs:integer, four xs:nonNegativeInteger, five bool and one xs:double. Now none does.

  Before After
leaves that state a rule 253 273
text, and the ORE source says why 227 207
text, because no ORE source read names the leaf 7 5
leaves where the schema is narrower than the map 16 0
leaves whose schema type the reader resolves 462 467

The 5 that remain are the three tenor leaves a variable names, DefaultCurve/Type and Qualifier. The schema types none of the five, so text is the honest answer rather than a gap. The 13 leaves whose schema type the reader does not resolve are listed by the evidence report; they all state a rule from another authority.

13. A list states what one item is

A set of cardinality many stated how many values a leaf holds and not what one may be, so 16 leaves said "a list of anything". The ORE user guide states the item for most of them in the sentence it describes the leaf with: Tenors and AtmTenors are "a comma separated list of valid tenor strings", Terms is "a comma separated list of tenors", SmileOptionTenors are "option expiries, in period form", CapStrikes and FloorStrikes are the strikes, DetachmentPoints are "any positive number less than one", and SmileSpreads are strike spreads.

curve_value_sets.py now states that per name, and each entry carries the reason and the guide or code line it came from, so the map's row for a tenor list reads ore_guide and cites the line. Twelve leaves gained an item rule: eight take curve.period_list and four curve.number_list.

Four keep curve.list, and their reason says why rather than leaving the gap unstated. A delta is written 25D, which is neither a number nor a period; an expiry may be a tenor, a date or a wildcard *, which the guide states; and a pillar date is a date, for which this tree has no kind yet. A date set is the missing piece, and it is the next one.

  Before After
leaves that take a period list 0 8
leaves that take a number list 5 9
leaves that take a list with no item rule 16 4

The seeded corpus holds real tenor and strike lists, and it publishes under the new rules unchanged, which is what proves the item rule is not too narrow.

14. The schema states the values it enumerates

An enumerated simple type in the schema names its values, so the rule for a leaf of that type is a set of them and not a shape one value must have. The schema states 142 such types, and 33 mapped leaves had text where one of them applied: Dimension with 4 values, VolatilityType with 3, Extrapolation with 4, the commodity future weekday with 7, the default curve's Type with 8, and the segment types of a yield curve.

ore_schema_types.py now reads them, and curve_value_sets.py and convention_value_sets.py generate one enumerated set per type, named curve.<type> or convention.<type>, with the values in the schema's order and the file and line the schema states them at.

Two rules keep the sets honest. A set the schema states is generated only for a leaf a column carries, because a set nothing names and no row carries is dead data; that is what the curve value-set test's orphan assertion catches. And the schema lists the empty string where a leaf may be left unset, which the guard treats as absent rather than as a choice, so the set states the choices and the unset case stays an empty field.

  Before After
leaves that state a rule 273 306
text, and the ORE source says why 207 174
text, because no ORE source read names the leaf 5 4
sets 229 245

The scheme also closed one of the five untyped leaves: DefaultCurve/Type is declared defaultCurveType in the schema, which enumerates eight values, so the map states them without any ORE source reading the leaf.

15. No leaf is left unread

Four leaves were still unread, and all four were the same shape: ORE names the element from a label the class passes to its base constructor, so no read spells the name. The swaption class passes ("Swap", "SwaptionVolatility", "SWAPTION", "") and the yield class ("Bond", "YieldVolatility", "BOND_OPTION", "Qualifier"), and GenericYieldVolatilityCurveConfig::fromXML reads underlyingLabel_ + "Tenors" and qualifierLabel_.

extract_curve_terms.py now reads those two forms and expands them once the class's labels are known: one term per label, named as the document spells it. Swaptions gain SwapTenors and SmileSwapTenors, yield curves gain BondTenors, SmileBondTenors and Qualifier, and the empty qualifier label states no leaf, because the swaption class never reads one.

The three tenor leaves take curve.period_list, citing the guide where it states the leaf in period form and the class constructor where it does not. Qualifier states text with the reason the parser gives: the engine reads it and never parses it.

  Before After
leaves that state a rule 306 309
text, and the authority says why 174 171
leaves that name no authority at all 4 0
leaves the ORE schema types alone 2 3
leaves the ORE source reads as terms 508 513

UNREAD_ALLOWED is now zero, so the check fails the moment a projection column carries a leaf no authority can state a rule for. Coverage is complete in the sense the task set out to prove: every leaf the map carries names the authority that decided it.

Three leaves take their rule from the ORE schema because no ORE source read names them at all: UseEffeciveVolatility, which ORE spells that way and reads elsewhere, DefaultCurve/Type, whose projection path the schema types but no read matches, and IndexInterpolated. Their rows say the schema is the only authority that speaks, so the difference between "the parser read it" and "the schema typed it" is read rather than assumed.

16. The schema states a pattern and a range too

A schema simple type states more than an enumeration. It restricts its base to a pattern, or to a range of values, and both are rules the model can hold:

  • date states the pattern ORE's date parser accepts, and its own comment says it "is not super strict, e.g. 2016-17-39 would pass, but it catches most errors";
  • calendar states a pattern naming every calendar ORE resolves, including the joint form US,MWK that ORE writes by repeating the group;
  • dayOfMonth restricts xs:integer to 1 through 31.

So ore_schema_types.py now reads the restriction facets: base, pattern, minInclusive and maxInclusive. A built-in type keeps the family's static set, because a flag is a flag and the static set accepts the Y and N that ORE also writes; a named type the family has no static set for, or one that narrows a kind with bounds, generates a set of its own.

Sixteen leaves gained a rule: the thirteen calendars the projection carries, the base correlation's StartDate, and the two commodity future day-of-month leaves. PillarDates gained an item rule too, curve.date_list, so a list of dates is stated rather than left as a list of anything.

  Before After
leaves that state a rule 309 325
text, and the authority says why 171 155
leaves that take a list of dates 0 1
sets 245 249

16.1. A pattern is anchored

The guard matches a pattern anywhere in the value, so a seeded pattern is wrapped in ^(...)$. Without the anchor date would admit x2024-01-31y, and a pattern that means "the whole value" would be read as "somewhere in the value". The test states both directions: a date the pattern allows is in, one it refuses is out.

The schema's date pattern admits month 13, and that is the schema's own statement about its strictness rather than a gap in the model. The test says so, so a reader does not mistake the rule for calendar validation.

17. Plan

17.1. Read the three maps

ores_refdata_curve_configuration_columns_tbl (212 rows) maps a projection column to an entity, a table and an xpath. ores_refdata_convention_kind_terms_tbl (270) maps a convention term to a schema and an xpath. ores_reporting_ore_parameter_vocabularies_tbl (4) maps a schema and an element to a value set. The first two carry the element implicitly in the xpath, the third names it, and only the third names a set. The Discovery section above holds what each one actually names.

Only the first two are read at runtime. Both guards resolve a set by joining the map to the parameter contract on (schema_name, xpath) and the contract to the setting links on (scope, name) (projects/ores.sql/create/refdata/refdata_curve_configuration_columns_validate_create.sql:43, refdata_convention_terms_validate_create.sql:51). Nothing reads the vocabularies table: grep -rln "ore_parameter_vocabularies" --include=*.cpp --include=*.hpp --include=*.ts --include=*.tsx projects/ apps/ returns nothing. So the third is not a rival authority, it is a statement with no enforcer. The consolidation either makes it the map the guards read, or removes it; leaving it as a row nobody consults is the one outcome that does not finish the task.

17.2. Write the one map

Key it by the ORE element: (schema, element). Attach the projection columns that carry it (entity, table, column), the value set, and the provenance the vocabulary rows already state. One row per element, the columns in a child table or an array, and the seeds read from the three sources already in the tree.

17.3. Collapse the links

ores_reporting_ore_parameter_setting_value_sets_tbl keys a parameter by (scope, name); the element map keys a leaf by (schema, element). One shape with a kind column, or two tables with one reader, is a decision for the task's first commit; what must not survive is a caller that knows which table to ask.

17.4. Carry the data and prove it

Every row of the three tables moves, and a test counts them before and after. The curve column guard's pgTAP suite must pass unchanged, which is the proof that the same column resolves the same set through the new map.

18. 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
leaves in the map 480 over 482 columns generate_ore_element_map.py --check 337d74dbfc plus this branch
leaves that state a rule 220 the same, first line of its report 337d74dbfc plus this branch
leaves that state text, with the ORE source's reason 232 the same 337d74dbfc plus this branch
leaves no ORE source read names 28 the same, and evidence/text_addresses.txt 337d74dbfc plus this branch
setting links to a value set 972 generate_value_sets.py --check 337d74dbfc plus this branch
value sets 226 the same 337d74dbfc plus this branch
leaves that state a rule 253 the same, first line of its report 337d74dbfc plus this branch
leaves no ORE source read names 7 the same, and evidence/text_addresses.txt 337d74dbfc plus this branch
setting links to a value set 991 generate_value_sets.py --check 337d74dbfc plus this branch
pgTAP suites 72 files, 669 tests, one failure that also fails on main projects/ores.sql/test/run_tests.sh 337d74dbfc plus this branch

19. Notes

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

21. PRs

PR Title
#4167 Measure the leaf maps and design the one that replaces them
#4170 Seed the element map and the columns that carry it
#4172 Publish the currencies a fresh database's publish test reads
#4175 Key the element map by the leaf's address, and read it from both guards
#4186 Census the leaf rules the map inherits
#4188 State the type three convention modifiers are read as
#4190 State the catalogue on the index conventions' vocabulary fields
#4194 Collapse the two free-text sets into one
#4199 Take a leaf's rule from the ORE parser, not the contract
#4207 State how many values a set governs, and read ORE's list forms
#4208 Let the ORE schema state the rule where the parser states none
#4213 State what one item of a list may be
#4217 Take the values the ORE schema enumerates
#4219 Read the leaves ORE names after a class label
#4225 Take the pattern and the range the ORE schema states
#4226 State the map's shape as a test

22. Review

Comment summary File Decision Notes
       

23. Result

The map is one table keyed by a leaf's address, its child names the projection column that carries the leaf, and both leaf guards join the pair, so the parameter contract is no longer a prerequisite of enforcement.

Every one of the 480 leaves states a rule or the reason it has none, and each row names the authority that decided it: the ORE parser for 207 rules and 171 text decisions, the ORE user guide for 14, and the ORE schema for 88. No leaf is left unaccounted, and UNREAD_ALLOWED is zero so one cannot arrive quietly.

The value set states what one value may be by its kind, how many the document writes by its cardinality, and the values, pattern, bounds or catalogue it draws on. A set is generated from the parser's map or the schema's restriction wherever a source states it, and curated with its reason where a person does.

What the work found that the plan did not expect: the contract was the wrong source for a leaf's rule, a set could not say how many values it governed, the extractor could not read a list or a name a class builds, and the schema was an authority nobody consulted. Each is a case of a rule that lived in prose, in a name, or in another table rather than in the model.

What is left belongs to the next task. A setting link is keyed by (scope, name) and so cannot state two rules for two leaves of one name: the equity volatility curve's Expiries is a list of dates and the report grid beside it is a list of periods, and only the drawn leaf is linked. No drawn leaf disagrees today, so no data is wrong; the key is what has to change before a screen can draw both.

Emacs 29.3 (Org mode 9.6.15)