Skip to content

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_scheme to name: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 (both json-schema and sql-table display 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 SchemaProperty per LinkML SlotDefinition — name, data_type → JSON-Schema type, required, pattern, multivalued → array;
  • a pre-populated PrimaryBinding per field (linkml_slot target = class + slot, concept_scheme pinned to the picked scheme's name:version) written into the field_bindings working copy;
  • domain_schema set 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, slot integer),
  • missing or weaker pattern,
  • required mismatch,
  • multivalued vs 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_bindings or 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 MappingWorkbench or ContractVocabularyPanel. 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.