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.