Skip to content

Binding any resource to a concept

PyCharter's binding lifecycle lets you tie a real-world thing to an ontology concept and drive that relationship through a governed state machine (UNMAPPED → SUGGESTED → MAPPED → APPROVED, plus a reversible TECHNICAL_ONLY). Historically the "thing" was always a contract field. The generic resource-binding API removes that restriction: any consumer can bind any external reference — a knowledge-base chunk, a file URI, a CRM record, a product SKU — to a concept through the same lifecycle, provenance, and audit trail.

Contract-field bindings are now simply the source="contract_field" specialisation of this one machine.

The binding convention

A binding's identity is (source, external_ref, concept_id), with an optional contract_id scope:

Part Meaning
source The kind of thing being bound — and how to read external_ref (think URI scheme). A small controlled vocabulary: "contract_field" is built in; consumers add their own ("kb_chunk", "product", …).
external_ref Which specific resource, within that source's namespace. Opaque to pycharter (never parsed); ideally hierarchical/path-like.
concept_id The meaning — a concept in an ontology scheme.
contract_id Optional scope. Participates in identity (the same external_ref can exist under two contracts); None for sources that don't need it.

Granularity has two orthogonal axes: a different kind of resource uses a different source; a different depth within the same kind uses a different external_ref (e.g. line_items.price vs line_items.price.currency, both source="contract_field").

Python API

from pycharter import ResourceBindingLifecycle, BindingState

lifecycle = ResourceBindingLifecycle()  # in-memory by default

# Bind a knowledge-base chunk to the "ReturnRate" concept.
lifecycle.bind(
    source="kb_chunk",
    external_ref="returns-policy.md#electronics",
    concept_id="ReturnRate",
    actor="ingest-bot",
    reason="auto-linked during KB ingest",
)

# Advance it through the governed lifecycle.
lifecycle.transition(
    source="kb_chunk",
    external_ref="returns-policy.md#electronics",
    concept_id="ReturnRate",
    to_state=BindingState.APPROVED,
    actor="steward@example.com",
    reason="reviewed",
)

# Query current bindings by meaning, across every source.
for binding in lifecycle.query(concept_id="ReturnRate"):
    print(binding.source, binding.external_ref, binding.state)

For a durable store, pass a SQLAlchemy-backed event store:

from pycharter import make_resource_binding_store, ResourceBindingLifecycle

store = make_resource_binding_store(backend="sqlalchemy", session_factory=Session)
lifecycle = ResourceBindingLifecycle(store=store)

See the runnable example at examples/resource_bindings/bind_kb_chunk.py.

HTTP API

Mounted at /api/v1/semantic/resource-bindings:

Method Path Purpose
POST /api/v1/semantic/resource-bindings Create or advance a binding (body carries the full identity + to_state). Requires the reviewer role.
GET /api/v1/semantic/resource-bindings?source=&concept_id=&state= List current-state bindings.
GET /api/v1/semantic/resource-bindings/events?source=&external_ref=&concept_id=&contract_id= List the event history, oldest-first.
curl -X POST http://localhost:8000/api/v1/semantic/resource-bindings \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"source":"kb_chunk","external_ref":"returns-policy.md#electronics",
       "concept_id":"ReturnRate","to_state":"mapped","actor":"ingest-bot"}'

Storage

All bindings — field and otherwise — are appended to the unified resource_binding_events table (in-memory by default, SQLAlchemy when a session factory is supplied). Contract-field events carry source="contract_field" and external_ref=<field_path>; the contract-field routes and the audit/lineage surfaces read the same table filtered to that source, so their behaviour is unchanged.