PyCharter — Concept-Backed Contracts in the Editor¶
Status: implemented (CB-1…CB-8, #441–#451) · Created: 2026-07-13 · Audience: maintainers ·
Extends: terminology.md (T-series),
vocabulary-consolidation.md (VC-series),
sem-2-publish-validation.md ·
Related: linkml-as-schema-substrate.md
The Vocabulary pillar's field-binding primitives are complete: contracts
carry a portable field_bindings section (PrimaryBinding /
AnnotationBinding → concept key + pinned scheme version, see
src/pycharter/semantic/contracts/field_bindings.py), the DB has a
field_concepts join table, and the editor already ships a
BindingsSection, ConceptTypeahead, MappingWorkbench,
ContractVocabularyPanel (coverage %), and canvas MappingEdges built
by buildMappingEdges at contract load. The primitives are done; the
product isn't. A user building a contract sees two palette cards
("JSON Schema", "SQL Table" — one TableAsset with two displayMode
skins), never encounters the vocabulary unless they open a field
inspector, and even a fully bound contract validates exactly as an
unbound one does.
This proposal makes concepts a visible, useful part of building a contract, in three moves:
See — every field row shows its binding at rest; the association graph appears on selection. Start from — a "From Concept" palette card scaffolds a schema, pre-bound, from a concept's LinkML slots. Mean something — bound concepts lint the schema at publish, and (opt-in) their slot constraints merge into the effective
json_schema. The runtime validator stays stateless and untouched.
Eight CB items across five phases, one-PR-one-story.
Why¶
Symptom → root cause → fix:
| # | Symptom | Root cause | CB |
|---|---|---|---|
| 1 | Bindings are invisible while editing | Binding UI lives only in FieldInspector / MappingWorkbench; nothing on the field rows users actually look at |
CB-1 |
| 2 | Vocabulary coverage grows only when a steward goes hunting | Suggestions surface in panels (SuggestionPanel, ContractVocabularyPanel), not at the moment a field is being edited |
CB-2 |
| 3 | The field↔concept graph exists but nobody sees it | buildMappingEdges runs at contract load, but edges render only when the knowledge layer is visible and concepts were manually placed on the canvas |
CB-3, CB-4 |
| 4 | Contract building starts from a blank schema | The Components palette offers two skins of one empty TableAsset; concepts (which already carry LinkML SlotDefinitions) cannot seed a schema |
CB-5, CB-6 |
| 5 | A field can contradict its own concept silently | Nothing compares a bound field's schema with the concept's slot (type, pattern, required) | CB-7 |
| 6 | Bindings never affect validation | runtime_validator/_loaders.py consumes only json_schema + validation_rules + coercion_rules; field_bindings / domain_schema are search/KG-only |
CB-8 |
Design constraints this proposal holds to:
- The contract stays a self-contained, portable artifact. Anything that affects validation lands inside the artifact at save/publish time. The runtime validator gains no semantic-store dependency (runtime concept resolution is an explicit non-goal).
- No drift by construction. Bindings pin
concept_schemetoname:version; vocabulary updates affect a contract only when it is re-published against a new scheme version. - The suggestion queue remains the sole write path into the concept registry (VC invariant). CB-2 accepts bindings, never creates concepts.
- Edge count scales with bound fields, not concepts. Concepts
dedupe (many fields → one
User), but a 100-field contract still means ~100 mapping edges. Always-on edges are spaghetti; the graph is therefore selection-scoped (CB-3), with an explicit show-all toggle for small contracts (CB-4).
Phase 1 — Binding chips (always-on visibility)¶
CB-1: Binding chip on every field row¶
A compact chip showing the field's primary binding (concept name, color-keyed by scheme) on all three surfaces that render field rows:
- canvas schema node —
components/editor/canvas/nodes/JsonSchemaNode.tsx(bothjson-schemaandsql-tabledisplay modes; in the SQL view the chip reads as a "semantic type" column), - form view —
components/editor/form-view/SchemaFormCard.tsx, - (inspector already has it via
BindingsSection).
Clicking a chip selects the field and opens the existing
FieldInspector bindings section. Reuses
components/editor/bindings/BindingChip.tsx and
useContractFieldBindings.ts; no new data flow — chips read the same
contractArtifacts.field_bindings working copy the inspector writes.
Unbound fields render a muted "+ concept" affordance (same click target). Annotation bindings are not shown on the chip (primary only); the inspector remains the place to see the full binding set.
CB-2: Inline binding suggestions on unbound fields¶
When the suggestion backend (/api/v1/vocabulary/suggestions) has a
confident match for an unbound field, the "+ concept" affordance shows
the top suggestion inline — one click to accept (writes a
PrimaryBinding via the existing binding write path), or open the
ConceptTypeahead to pick manually. Reuses the fetch/accept logic
behind components/editor/bindings/BindingSuggestionBanner.tsx.
This is the contribution flywheel (VC-11..13) surfaced at the exact moment of editing: coverage grows as a side effect of building the contract, and every accept/reject feeds calibration.
Phase 2 — Selection-scoped edges (on-demand graph)¶
CB-3: Materialize concepts + edges on selection¶
At rest, contract mode shows only schema/table nodes — zero standing clutter. On selection, the association graph appears:
- Select a field → its bound concept node materializes beside the
schema node with one
MappingEdge; dismissed on deselect. - Select a concept (once materialized) → edges fan out to every
field bound to it, across all tables on the canvas. This direction is
the payoff chips cannot provide: impact analysis ("click
UserEmail, see every PII touchpoint").
Reuses buildMappingEdges (lib/unified-editor/contract-loader.ts)
and components/editor/canvas/edges/MappingEdge.tsx; the new part is
ephemeral placement (auto-position near the schema node, not persisted
to layout) and lifecycle (materialize/dismiss driven by
selection / selection.fieldId in the workspace store).
CB-4: Show-all toggle¶
VocabularyToggleButton gains a "show all bindings" mode that
materializes every bound concept and edge at once — useful for small
contracts where density is not a problem. Off by default; state lives
in the editor UI store, not the persisted layout.
Phase 3 — "From Concept" palette card¶
CB-5: Scaffold a schema from a concept¶
A third card in CONTRACT_ITEMS
(components/editor/shell/ContractPaletteTab.tsx). Dropping it on the
canvas (new branch in the application/reactflow MIME handling in
components/editor/canvas/ConceptCanvas.tsx handleDrop) opens a
concept picker (reuse ConceptTypeahead + ConceptSchemePicker).
Picking a concept generates, via a new add-mutator in
stores/editor/workspace-canvas-add-mutators.ts following the
addJsonSchemaNodeOnWorkspace pattern:
- one
SchemaPropertyper LinkMLSlotDefinition—name,data_type→ JSON-Schema type,required,pattern,multivalued→ array; - a pre-populated
PrimaryBindingper field (linkml_slottarget = class + slot,concept_schemepinned to the picked scheme'sname:version) written into thefield_bindingsworking copy; domain_schemaset on the contract if not already set.
The result is a schema that is born fully bound — Phase 1 chips light up green immediately, and Phase 4 lint passes by construction.
Slots whose range is another concept (e.g. Order.customer → User)
become plain fields bound to that concept — no auto-generated nested
objects or FK tables in v1 (the no-surprises option; see CB-6).
CB-6 (backlog): richer scaffolding gestures¶
Deferred, in priority order: (a) drag a concept instance from the
Browse tab into contract mode as a scaffolding shortcut (cross-mode
drag); (b) optional expansion of concept-range slots into a second
table + foreign-key edge. Neither blocks CB-5.
Phase 4 — Semantic lint¶
CB-7: Field-vs-slot consistency checks¶
For every field with a primary binding that resolves to a slot (either
a linkml_slot target, or a concept target whose concept defines a
matching slot), compare the field's schema against the
SlotDefinition:
- type mismatch (field
string, slotinteger), - missing or weaker
pattern, requiredmismatch,multivaluedvs array mismatch.
Findings are warnings only — validation behavior does not change. Two surfaces, same check implementation (shared service, not duplicated):
- publish time — a check alongside the SEM-2 machinery in
src/pycharter/api/services/_contract_publish_validation.py; - editor time — the same findings shown in
ContractVocabularyPanel/ValidationCompanionTab, and as a warning tint on the affected field's chip (CB-1).
Lint also serves as the data-quality gate for Phase 5: inheritance only ships once lint has shown that existing bindings and slots agree in practice.
Phase 5 — Opt-in constraint inheritance¶
CB-8: Merge slot constraints into the effective json_schema¶
A per-contract (default) or per-binding flag, inherit_constraints.
When set, save/publish merges the bound slots' constraints (pattern,
required, type, multivalued) into the contract's json_schema —
tightening only, never loosening, with CB-7 lint escalated to an error
on direct conflicts.
Because the merge happens at save/publish and lands inside the artifact:
- the runtime validator (
runtime_validator/_loaders.py) is untouched and offline validation keeps working; - the artifact remains self-contained and portable;
- scheme pinning means re-publishing is the only moment vocabulary changes propagate.
A scaffolded schema (CB-5) and a hand-built schema bound to the same concept thereby converge on the same effective constraints — the "concepts mean something" end state.
Non-goals¶
- Runtime concept resolution. The validator never reads
field_bindingsor fetches concepts at validation time; that would add a semantic-store dependency to every validation path and make results depend on registry state. - Auto-generation of nested objects / FK tables from concept-range slots in v1 (CB-6 backlog).
- Replacing
MappingWorkbenchorContractVocabularyPanel. They remain the aggregate/bulk views; this proposal adds the in-flow surfaces, not substitutes. - New palette formats unrelated to the vocabulary (ODCS import cards, template cards) — worth doing, separate proposal.
Sequencing¶
CB-1 → CB-2 → CB-3 → CB-4 → CB-5 → CB-7 → CB-8 (CB-6 unscheduled). Phases 1–3 are UI-only; Phase 4 adds a backend check service; Phase 5 is the only item that changes what a published artifact contains, and it is opt-in.