How CORA's seams work¶
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.
A seam is where a protocol lives — the named interface that adapters conform to. CORA has three. The CLI is a thin registry over them; each adapter is one file. Adding a new format or a new validator or a new docs page type is one new adapter at the existing seam, not a new code path through the CLI.
%%{init: {'themeCSS': '.seam .nodeLabel p, .seam .nodeLabel span, .seam .nodeLabel { color: #fafaf8 !important; } .flowchart-link, .edgePath .path { stroke: var(--cora-text) !important; } marker, .arrowheadPath { fill: var(--cora-text) !important; } .cluster rect { fill: transparent !important; stroke: transparent !important; }'}}%%
flowchart TB
classDef seam fill:#2b4257,stroke:#1f3243,color:#fafaf8
classDef adapter fill:#f5f3ee,stroke:#d4d0c7,color:#1a1a1a
classDef cli fill:#eef1eb,stroke:#8faa88,color:#1a1a1a
cli(["cora CLI<br/>thin registry"]):::cli
subgraph val[" "]
direction TB
validator[["Validator.check<br/>(repo_root) → ValidationResult"]]:::seam
vschema[inventory_schema]:::adapter
vcount[field_count]:::adapter
vpaths[crosswalk_paths]:::adapter
validator --- vschema
vschema --- vcount
vcount --> vpaths
end
subgraph gen[" "]
direction TB
generator[["Generator.generate<br/>(repo_root, out_dir) → list[Path]"]]:::seam
gcov[coverage_matrix]:::adapter
gconc[concept_pages]:::adapter
ggraph[concept_graphs]:::adapter
ginv[inventory_pages]:::adapter
gindex[index]:::adapter
generator --- gcov
gcov --- gconc
gconc --- ggraph
ggraph --- ginv
ginv --> gindex
end
subgraph ext[" "]
direction TB
extractor[["Extractor.extract<br/>(source, config) → Inventory"]]:::seam
xsd[xsd]:::adapter
json[json_catalog]:::adapter
cdm[cdm_json]:::adapter
xls[excel_dictionary]:::adapter
xlms[excel_multisheet]:::adapter
extractor --- xsd
xsd --- json
json --- cdm
cdm --- xls
xls --> xlms
end
cli --> validator
cli --> generator
cli --> extractor
The three protocols¶
Extractor¶
@runtime_checkable
class Extractor(Protocol):
name: str
def extract(
self,
source: Path,
config: ExtractorConfig | None = None,
*,
module: str | None = None,
) -> Inventory: ...
Lives at cora_extractors.extractor.
Five adapters today:
| Adapter | Reads | Config |
|---|---|---|
xsd |
XML Schema files (resolves xs:include chains) |
XsdConfig (optional remap dict for unresolvable URLs) |
json_catalog |
Flat JSON catalogs (JSONPath-described) | JsonCatalogConfig (required — describes the shape) |
cdm_json |
Microsoft Common Data Model JSON manifests | CdmJsonConfig (optional — inheritance root marker) |
excel_dictionary |
Single-sheet Excel data dictionaries (REDI shape) | ExcelDictionaryConfig (required — column mapping) |
excel_multisheet |
MITS multi-sheet workbooks (one sheet per type) | ExcelMultiSheetDictionaryConfig (required — sheet→type mapping) |
Every adapter sets Inventory.source_label at construction time ("xsd", "excel", "cdm-json", …) so the same data wears the same label everywhere it appears.
Validator¶
@runtime_checkable
class Validator(Protocol):
name: str
def check(self, repo_root: Path) -> ValidationResult: ...
Lives at cora_extractors.validator.
Three adapters today:
| Adapter | Checks |
|---|---|
inventory_schema |
Every committed inventory validates against the JSON Schema and passes structural invariants. |
field_count |
Every inventory clears its per-module minimum field count (catches silently empty extractions). |
crosswalk_paths |
Every crosswalk's mappings.<std>.field resolves against the standard's inventories; not_present requires field: null + notes; divergent requires notes. |
cora validate runs all three; cora validate <name> runs one.
Generator¶
@runtime_checkable
class Generator(Protocol):
name: str
def generate(self, repo_root: Path, output_dir: Path) -> list[Path]: ...
Lives at cora_extractors.generator.
Five adapters today:
| Adapter | Output |
|---|---|
coverage_matrix |
One Markdown table: concepts × standards with confidence badges. |
concept_pages |
One Markdown per crosswalk: mappings table + per-concept Mermaid graph. |
concept_graphs |
One overview Mermaid flowchart of every concept and its mappings. |
inventory_pages |
One Markdown per inventory: types table + fields table sorted by path. |
index |
docs/generated/README.md linking everything with per-standard counts. |
cora docs build runs every Generator in registry order; cora docs check regenerates into a temp directory and diffs against docs/generated/ (the CI drift gate).
The adapter pattern, end to end¶
Concretely, registering a new adapter is four edits:
- Write the adapter — one file under
cora_extractors/{format}.py(Extractor),cora_extractors/validators/{name}.py(Validator), orcora_extractors/generators/{name}.py(Generator). Implement the protocol's method. - Add a config class if needed — a
pydanticsubclass ofExtractorConfigincora_extractors/config.py. Extractor only. - Register in the CLI — add the adapter to the appropriate dict/list in
cora_extractors/cli.py. One line. - Add tests + a fixture — at least one positive round-trip test under
tools/extractors/tests/. Two fixture sources where reasonable (the "two-source genericity audit" — see Onboarding a format).
The CLI dispatches by name; new adapters show up automatically. There's no central switch statement to update.
Why three seams and not one¶
Earlier drafts of the toolkit had a single "process this artifact" entry point. It conflated three different jobs with different signatures: extract reads, validate checks, generate writes. Naming them separately lets each one have an honest interface — and lets cora validate run all three of its adapters in parallel without confusing them with extractors.
The discipline that keeps the design clean: two adapters at a seam means the seam is real; one adapter is just a hypothesis. Each seam crossed three before being stabilized.
Next: merge vs enrich — the two named operations on Inventory and why they're separate methods.