Inventory operations: merge vs enrich¶
Contributing
This page documents the implementation toolkit used to maintain CORA's published artifacts. Most consumers won't need it. If you're a data team integrating CORA into a pipeline, start with Quickstart or Consuming inventories.
Two named operations on the Inventory type. Picking the right one is a load-bearing call; the design rationale lives in ADR-0001.
%%{init: {'themeCSS': '.op .nodeLabel p, .op .nodeLabel span, .op .nodeLabel { color: #fafaf8 !important; } .flowchart-link, .edgePath .path { stroke: var(--cora-text) !important; } marker, .arrowheadPath { fill: var(--cora-text) !important; }'}}%%
flowchart TB
classDef inv fill:#f5f3ee,stroke:#d4d0c7,color:#1a1a1a
classDef op fill:#2b4257,stroke:#1f3243,color:#fafaf8
classDef out fill:#eef1eb,stroke:#8faa88,color:#1a1a1a
classDef bad fill:#fbe9e5,stroke:#a85c4a,color:#1a1a1a
subgraph S1[Inventory.merge — symmetric]
direction LR
a1["Inventory A"]:::inv
b1["Inventory B"]:::inv
m1(["merge<br/>match_by: name | path"]):::op
ok1["Combined inventory"]:::out
bad1["MergeConflict<br/>(any attested disagreement)"]:::bad
a1 --> m1
b1 --> m1
m1 -- agree --> ok1
m1 -- disagree --> bad1
end
subgraph S2[Inventory.enrich — asymmetric, type-scoped]
direction LR
xsd["XSD inventory<br/>(primary, self)"]:::inv
excel["Excel inventory<br/>(secondary, other)"]:::inv
e2(["enrich<br/>attributes={definition, ...}"]):::op
out2["Enriched inventory<br/>+ provenance + unmatched_enrichments"]:::out
xsd --> e2
excel --> e2
e2 --> out2
end
The distinction in one paragraph¶
merge is symmetric: two inventories of equal authority, combined into one. Any attested disagreement is a real problem, and the operation raises MergeConflict to surface it. enrich is asymmetric: a primary inventory (self) gets filled in by a secondary source (other) for a specific set of attributes the caller trusts. Disagreements outside that trust list are silently dropped from the top level but recorded in provenance for review. The operation never raises.
merge |
enrich |
|
|---|---|---|
| Caller's relationship to the data | Two equal halves of one source | One primary, one fill-in |
| Match key | Caller picks 'name' or 'path' |
Hardcoded: (field.domain, leaf-name) |
| Trust filter | None (all attributes flow) | Required attributes: set[str] |
| Disagreement behavior | raise MergeConflict |
Silently picks per trust list; records provenance |
Unmatched other.fields |
Appended to result | Recorded in unmatched_enrichments[] |
| When to use | Combining XSD-domain + XSD-extends views | XSD enriched by Excel; future: PDF, SQL DDL |
Why two methods and not parameters¶
A single merge(other, *, strict, attributes, ...) would do both jobs, but the interface would lie about what it does. A reader seeing inv.enrich(other, attributes={"definition"}) knows immediately this is asymmetric and won't raise. A reader seeing inv.merge(other, match_by="name", attributes={"definition"}, append_unmatched=False) has to study the parameters to understand the semantics.
The deletion test: if enrich didn't exist, every enrichment-source caller (Excel today, PDF tomorrow, SQL DDL after that) would have to compose merge with the same pre-filter logic. Adding enrich concentrates that pattern in one place. See ADR-0001 for the eight sub-decisions that constrain enrich's shape.
The MITS XSD-plus-Excel-dictionary flow¶
from cora_extractors.inventory import Inventory
xsd_inv = Inventory.from_yaml("standards/mits/current/inventory/lead-management.yaml")
excel_inv = Inventory.from_yaml("/tmp/lead-management-excel.yaml")
enriched = xsd_inv.enrich(excel_inv, attributes={"definition", "enumeration"})
enriched.to_yaml("standards/mits/current/inventory/lead-management.yaml")
The trust list {"definition", "enumeration"} says: take Excel's prose definitions and Excel's enumeration lists, but ignore Excel's claims about range (Excel's range="Complex" would otherwise overwrite XSD's range="AddressType").
Same thing via the CLI:
tools/extractors/.venv/bin/cora inventory merge \
--into standards/mits/current/inventory/lead-management.yaml \
--from /tmp/lead-management-excel.yaml \
--attribute definition --attribute enumeration \
--output standards/mits/current/inventory/lead-management.yaml
Reading provenance¶
When enrich finds ≥2 sources attested for an attribute, the merged inventory carries the full lineage:
- path: EventType/Description
domain: EventType
definition: "An event recorded in the lead lifecycle."
source_location: lead-management.xsd:146
provenance:
- attribute: definition
claims:
- source: xsd
value: "EventType"
location: lead-management.xsd:146
- source: excel
value: "An event recorded in the lead lifecycle."
location: lead-management.xls!Lead Management 4.0!23
chosen: excel
claims[] lists every source that attested. Always ≥2 entries (single-source attestations don't get provenance — the top-level value carries them implicitly).
chosen is present only when claims disagree. When all claims agree, the field is omitted — there was no decision to record.
Untrusted disagreements are still recorded. If Excel says range="Complex" and XSD says range="AddressType", the top-level range stays as XSD's (because range isn't in the trust list), but a provenance entry is written so the YAML diff between regenerations shows that Excel changed its claim. Future PDF or SQL DDL enrichment adds more claims entries to the same field.
Reading unmatched_enrichments¶
Top-level on the Inventory:
unmatched_enrichments:
- source: excel
domain: PersonType
field: IDValue
location: lead-management.xls!Person Type!7
These are other.fields rows whose (domain, leaf-name) found no match in self. In MITS, most unmatched rows reflect the docs-vs-defs mismatch: Excel documents fields by usage context (Person Type sheet listing IDValue), XSD models them by definition (IDValue lives on Identification). The audit catches typos and sheet-mapping errors without polluting fields[].
Auditing what a merge changed¶
The repo ships a script that spotlights multi-source disagreements:
tools/extractors/.venv/bin/python tools/extractors/scripts/show_disagreements.py \
standards/mits/current/inventory/accounts-payable.yaml \
--attribute definition
mits/accounts-payable — 124 fields total
source_label: xsd
disagreements: 4
[definition] InvoiceDetailType/FreightAmount (chosen: excel)
xsd: 'Freight amount for each line item'
* excel: 'Per line item freight amount'
...
Useful for spot-checking before merging a re-extraction PR.
Next: CLI reference — every cora subcommand.