Vocabulary¶
The PyCharter Vocabulary pillar attaches business definitions to
contract fields so two teams' notional mean the same thing: bind a
field to a governed concept once, and every contract, validator,
lineage query, and AI agent that touches that field sees the same
definition. Concretely it adds field bindings, governed business
concepts, lineage tracking, and collaborative governance workflows on
top of data contracts — and serves the result to agents over
MCP.
What the vocabulary provides¶
| Sub-module | Purpose |
|---|---|
| Concepts | Concept schemes, governed concepts, concept types, tiers, field bindings, relationship types, lineage |
| Concept schemes | Group concepts into named schemes (e.g. trading, CRM) with versioning and base URI |
| RDF / JSON-LD | Export vocabulary artifacts as RDF for interoperability |
| Knowledge graph | Graph-based discovery and lineage traversal across contracts |
| Collaboration | Proposals, reviews, conflict resolution, and permissions for governed changes |
| Versioning | Diff, history, and merge of vocabulary artifacts over time |
| Ingestion | Import external concept schemes or vocabulary files |
| REST API | HTTP routes for schemes, concepts, relationships, and semantic health at /api/v1/semantic/ |
| Web UI | Vocabulary section: concepts browser, diagrammatic workspace (ReactFlow), scheme selector, property drawer |
Core concepts¶
Concept tiers¶
Governance is structured around three tiers:
| Tier | Enum value | Description |
|---|---|---|
| Core | ConceptTier.CORE |
Governed by the central data team; changes require approval |
| Domain | ConceptTier.DOMAIN |
Governed by individual domain teams |
| Free | ConceptTier.FREE |
No governance; used for experimental or team-local concepts |
Concept types (node kinds)¶
Concepts in the knowledge graph are classified by concept type (also referred to as node kind in some APIs):
| Type | Example |
|---|---|
entity |
Customer, Product, Order |
process |
Checkout flow, Onboarding |
event |
Payment received, Item shipped |
attribute |
Price, Weight, Status |
rule |
Discount eligibility, Age restriction |
system |
Payment gateway, CRM |
concept |
Revenue, Churn |
metric |
Monthly active users, NPS |
Relationship types¶
| Type | Meaning |
|---|---|
references |
Foreign key or pointer to another field |
derived_from |
Computed from another field |
same_as |
Two fields represent the same concept |
related_to |
General association |
is_a |
Taxonomic parent (X is a kind of Y) |
has |
Composition (X contains Y) |
depends_on |
X requires Y |
precedes |
X comes before Y (temporal) |
causes |
X triggers Y |
implements |
X is a concrete form of Y |
governs |
X constrains Y |
describes |
X is a property of Y |
equivalent_to |
Semantic equivalence |
Field bindings in contracts¶
Bind contract fields to vocabulary concepts with a top-level field_bindings block (not inline ontology: keys under JSON Schema — those are removed):
# orders.contract.yaml
id: trading/orders
version: "1.0.0"
json_schema:
type: object
properties:
order_id:
type: string
customer_id:
type: string
total_amount:
type: number
field_bindings:
order_id:
primary:
concept: order_identifier
concept_scheme: trading:1.0.0
tags: [core, immutable]
customer_id:
primary:
concept: customer_identifier
concept_scheme: trading:1.0.0
total_amount:
primary:
concept: monetary_value
concept_scheme: trading:1.0.0
LinkML-first contract authoring (recommended)¶
When a contract's fields map cleanly onto a LinkML class/slot vocabulary,
prefer the LinkML-first authoring pattern: declare the contract's
primary vocabulary once via domain_schema, then bind each field to a
concrete LinkML class:slot pair. Optional concept annotations layer
SKOS-style cross-references on top.
# orders.contract.yaml
id: trading/orders
version: "1.0.0"
json_schema:
type: object
properties:
order_id:
type: string
customer_id:
type: string
metadata:
title: Order events
version: "1.0.0"
domain_schema: trading:1.0.0 # primary LinkML vocabulary for this contract
field_bindings:
order_id:
primary:
linkml_slot: { class: Order, slot: id }
concept_scheme: trading:1.0.0
tags: ["identifier"]
annotations:
- concept: TradeEntity
concept_scheme: financial-core:2.1.0
relationship: exact_match
customer_id:
primary:
linkml_slot: { class: Order, slot: customer_id }
concept_scheme: trading:1.0.0
domain_schema names the contract's primary vocabulary so the editor
and downstream tooling can auto-load it. Each primary.linkml_slot
binding then targets a specific class+slot pair within a declared concept
scheme (usually equal to domain_schema).
Shape vs. meaning¶
Two distinct layers live side-by-side here:
- Shape — what fields exist, their types, structural constraints —
is the LinkML schema's job (
trading:1.0.0in the example). - Meaning — what concept a field refers to in the real world — is declared on the binding's annotations or on concept-only bindings.
Don't try to encode meaning inside LinkML classes or shape inside the
concept registry. The bridge between the two is the binding's
concept_scheme and slot.meaning: URIs on the LinkML schema itself.
Enforcing LinkML bindings at load time¶
Field bindings that target a LinkML class+slot can be verified
against the actual LinkML schema at contract load time. Pass an
:class:~pycharter.semantic.linkml.LinkMLSchemaRegistry to
load_contract, load_contract_from_string, or
load_contract_from_dict:
from pycharter.semantic.contracts import load_contract
from pycharter.semantic.linkml import LinkMLSchemaRegistry
registry = LinkMLSchemaRegistry() # default: data/vocabulary/versions/
contract = load_contract(
"orders.contract.yaml",
linkml_schema_registry=registry,
)
For each binding whose primary.linkml_slot is set, the loader will:
- Resolve the binding's concept scheme (e.g.
trading:1.0.0) via the registry. - Confirm the named class exists in that schema.
- Confirm the slot is reachable on the class — directly, via
slot_usage, or via theis_a/mixinsinheritance chain.
Typos and stale references fail the load with an actionable message:
ContractLoadError: LinkML field-binding validation failed: 1 issue.
- field 'order_id' → class 'Custmer', slot 'id' in schema
'trading:1.0.0': class 'Custmer' does not exist in schema
'trading:1.0.0'
The check is opt-in. Omit the LinkML registry argument and contract loading
preserves structural validation only. Contracts without domain_schema, or
with concept-only bindings (primary.concept), are unaffected.
Parsing field bindings¶
from pycharter.semantic import parse_field_bindings, parse_field_bindings_file
mapping = parse_field_bindings(
{
"domain_schema": "trading:1.0.0",
"field_bindings": {
"order_id": {
"primary": {
"linkml_slot": {"class": "Order", "slot": "id"},
"concept_scheme": "trading:1.0.0",
}
}
},
}
)
mapping = parse_field_bindings_file("demo_field_bindings.yaml")
print(mapping.bound_field_count)
Key data models¶
FieldBinding¶
Per-field link from a contract field to a concept or LinkML slot:
| Attribute | Description |
|---|---|
primary |
Primary binding target (concept and/or LinkML slot) |
annotations |
Optional additional concept cross-references |
FieldConceptBinding¶
Concept-only binding shape (no LinkML class/slot target).
Concept / ConceptScheme¶
Governed vocabulary entries and their versioned collections. See Semantic Store.
Seed vocabulary¶
Load example concept schemes and demo bindings with:
Example manifests reference concept_scheme.yaml files under
src/pycharter/data/seed/examples/. Import additional schemes via
ConceptSchemeImporter or the ingestion API routes under
/api/v1/semantic/ingestion/.
RDF / JSON-LD export¶
Export vocabulary artifacts as RDF for interoperability with SPARQL endpoints and JSON-LD tooling:
from pycharter.semantic.rdf.emitter import emit_concepts_jsonld
# Requires: pip install pycharter[semantic]
doc = emit_concepts_jsonld(concepts, scheme_key="trading", version="1.0.0")
Wiring automatic structural extraction into a pipeline run¶
When a pipeline step's effective extraction_policy.trigger is automatic,
the records passing through the step can be tee'd into the structural
extractor (HyperLogLog / Count-Min Sketch / reservoir sampling) and
materialised as suggestions in the governed queue. The plumbing is
opt-in at the caller — run_dag accepts a extraction_sink_factory that
constructs an :class:AutoExtractionSink per load step.
A minimal CLI / API / worker wiring looks like:
from pycharter.pipeline_generator import run_dag
from pycharter.pipeline_generator.steps._base import LoadStepConfig
from pycharter.semantic.extraction.auto_trigger import AutoExtractionSink
from pycharter.semantic.extraction_policy import resolve_effective_policy
def make_extraction_sink_factory(session, run_id, contracts_by_ref, scheme_id):
"""Build a per-step sink factory bound to one run's session."""
def factory(step: LoadStepConfig) -> AutoExtractionSink | None:
block = step.contract
if block is None:
return None
contract = contracts_by_ref.get(_ref_key(block))
effective = resolve_effective_policy(
contract.extraction_policy if contract else None,
block.extraction_policy,
)
if effective.trigger != "automatic":
return None
return AutoExtractionSink(
session=session,
scheme_id=scheme_id,
source_uri=f"pipeline_run://{run_id}#{step.id}",
policy=effective,
concept_key_prefix=(contract.id if contract else "records"),
)
return factory
result = await run_dag(
steps,
pipeline_name="orders",
run_id="abc123",
contract_store=store,
extraction_sink_factory=make_extraction_sink_factory(
session=session,
run_id="abc123",
contracts_by_ref=contracts_by_ref,
scheme_id=scheme_id,
),
)
The factory has three responsibilities the runner deliberately
delegates: (a) resolving each step's effective extraction_policy (the
canonical resolver is resolve_effective_policy from
pycharter.semantic.extraction_policy); (b) returning None for steps that
should not fire (off / manual policies, or steps with no contract);
and © owning the database session lifecycle. Factory exceptions are
logged and swallowed by the runner — auto-extraction is opportunistic
and must never cause a real load to fail.
When extraction_sink_factory is omitted, no extraction occurs and run_dag
behaves exactly as before. The default-off posture means every
existing CLI / API / worker caller of the runner stays unchanged
until it explicitly opts in.
Collaborative governance¶
The vocabulary layer includes a collaboration layer for teams that need structured change management on concept schemes and bindings:
- Proposals — submit a proposed change to a concept definition
- Reviews — accept, reject, or request changes on a proposal
- Conflict resolution — detect and resolve competing proposals
- Permissions — control who can propose, review, and approve changes per concept tier
These are primarily used through the REST API (see the semantic API routes at /api/v1/semantic/).
Install¶
Vocabulary support is included in the core package. RDF features require an extra:
See also¶
- Domain Models and Lifecycle — lifecycle contracts and FSM binding
- Data Contracts & Validation tutorial
- API Reference — Vocabulary