villebro opened a new issue, #44875:
URL: https://github.com/apache/superset/issues/44875

   ## [SIP] Proposal for a Widget Framework
   
   > **Authors:** @villebro (co-authors: @michael-s-molina, @EnxDev, @rusackas, 
@msyavuz)
   >
   > **Status:** Draft for discussion. A proof of concept on the `dashboard-v2` 
feature branch (#43262, #43400, #43588) evaluated the approach end to end. None 
of its code is assumed to carry over; this SIP specifies the design.
   
   ### The vision
   
   Widgets are the building blocks of **Canvas** (the next generation of 
Superset dashboards): charts, tables, metric tiles, filters, markdown blocks 
and containers. This SIP proposes the framework behind them. Its main use case 
is the chart, and it changes what charts can do:
   
   - **Build and refine charts by talking to an agent.** "Show monthly revenue 
by region, and make EMEA red." The agent reads only the parts of the control 
panel it needs, sends small edits, and previews the result before anything is 
saved.
   - **Agents can't break a chart.** Every edit is validated against the 
widget's schema. A bad edit is rejected with an error that says exactly how to 
fix it.
   - **Control panels of any depth, with no hand-written UI.** A widget 
describes its properties once, as a schema. Superset generates the property 
panel from it, and agents read the same schema.
   - **Any visualization, delivered as an extension.** A widget is a React 
component plus Python logic, so it can use ECharts, D3, deck.gl, WebGL or plain 
HTML. The extensions framework already loads frontend and backend code, so a 
new chart type ships as one package and registers exactly like the built-ins.
   - **Charts that work without a browser.** Queries are built on the server, 
so alerts, reports, thumbnails, cache warm-up and charts created through the 
API all work without running JavaScript.
   - **Fast, compact data.** Results travel as Apache Arrow, and small results 
arrive in a single request.
   - **Reusable or self-contained widgets.** Save a widget instance once and 
use it on its own, or place it on as many canvases as you like. Alternatively, 
keep it inline in a single Canvas. The same chart can even appear twice on one 
Canvas, with differently scoped filters.
   - **Immutable snapshots.** Export a Canvas as one self-contained document 
that later edits can't change.
   - **Data access that follows the chart.** The query is defined by the stored 
widget instance, never by the caller. This lays the groundwork for sharing an 
aggregate without sharing the underlying table.
   - **Cross-filtering and drill-down for every widget,** built-in or 
extension, through public hooks.
   - **Upgrades that never lose work.** Schemas are versioned and migrated 
automatically, and anything the server can't load is kept intact until it can.
   
   v1 charts, Explore and existing dashboards keep working alongside the new 
framework. v1 charts are deprecated in phases, each gated on a milestone rather 
than a date.
   
   ### Motivation
   
   Superset's chart system was designed for a human working in a browser, one 
chart at a time. AI agents, server-side features and finer-grained sharing all 
strain that design:
   
   1. **AI agents can't operate charts.** A v1 control panel 
(`controlPanel.tsx`) is imperative TypeScript, and its behavior is expressed as 
functions (`visibility`, `mapStateToProps`, `shouldMapStateToProps`). None of 
that can be serialized and handed to an agent. As a result, every agent-facing 
surface describes chart types again by hand. For example, the MCP 
`generate_chart` tool keeps its own per-type models (`XYChartConfig`, 
`PieChartConfig`, `TableChartConfig` and others in 
`superset/mcp_service/chart/schemas.py`), and the chat extension keeps its own 
tool schemas. Each new chart type multiplies this work, and the copies drift 
apart. Being serializable is also not enough on its own: a rich control panel 
is far too large to put into an agent's context on every turn.
   
   2. **Queries are built in the browser.** In v1, a chart's frontend plugin 
builds its query (`buildQuery`), and the result is stored as `query_context`. 
Several features need a chart's query without a browser: alerts and reports, 
thumbnails, cache warm-up, and charts created through the API. They must either 
rely on a previously saved `query_context` or run JavaScript. SIP-211 proposes 
a Node.js sidecar to close this gap. We think the lasting fix is to move query 
building to the backend.
   
   3. **Viewing a chart effectively requires dataset access.** 
`/api/v1/chart/data` accepts a complete `query_context` from the caller. 
Because the caller shapes the query (metrics, group-bys, columns), the endpoint 
has to authorize against the dataset. So anyone who can view a chart can also 
query its dataset in other ways. There is no way to share an aggregate without 
sharing the underlying table. This SIP lays the architectural groundwork to fix 
that (section 8), but its first milestone deliberately keeps today's data 
entitlement.
   
   4. **The v1 rendering and interactivity pipeline is heavy.**
      - `transformProps` is an imperative step that reshapes props before 
rendering, and it fits poorly with React hooks.
      - Native filters and cross-filters go through a central Redux `dataMask` 
store. The dashboard computes which charts each filter applies to and pushes 
merged form data down to each chart (`getFormDataWithExtraFilters`).
      - A chart can't take part in interactivity on its own terms, because the 
dashboard orchestrates everything.
   
   ### Proposed Change
   
   #### 1. Concepts and overview
   
   - **Widget:** a kind of thing that can be placed on a Canvas, such as the 
Timeseries Bar chart, the markdown block or the filter bar. A widget is a 
Python class (a subclass of `Widget`) plus a React renderer, provided by core 
or by an extension.
   - **Widget instance:** one configured occurrence of a widget, meaning props 
that are valid against the widget's schema. An instance is either 
**persisted**, a saved entity with its own UUID, or **inline**, stored inside a 
Canvas document.
   - **Placement:** an instance's spot on a Canvas, with its position, size and 
filter scope.
     - Persisted instances and canvases have a many-to-many relationship 
through placements. A persisted instance can exist on its own with no placement 
at all, as many charts in large deployments do today, or be placed any number 
of times on one or several canvases.
     - An inline instance always has exactly one placement.
   - **Chart:** a widget that queries data and visualizes it, by mixing in 
`QueryMixin`. Charts are the main use case and get most of the attention below, 
but nothing in the base contract is specific to charts.
   - **Canvas:** the next generation of Superset dashboards, made of structure 
and placements. A companion Canvas SIP specifies the Canvas document.
   
   People will call widget instances "widgets" in everyday speech; this 
document says "widget instance" wherever the difference matters.
   
   This gives a clean split. The Canvas grammar is small and fixed: a tree of 
placements. Each instance's content is open-ended, but it is validated against 
its widget's schema, wherever it is stored.
   
   **How the pieces fit.** The sections below follow the life of a chart:
   1. A widget registers its contract with the backend (section 2), including a 
schema that describes its properties (section 3).
   2. A user or an agent creates a widget instance. The property panel renders 
the schema, while an agent reads it step by step (section 4).
   3. The instance is stored either on its own or inside a Canvas (section 5). 
A stored-on-its-own instance can be placed on any number of canvases, or none. 
Every edit happens in an isolated, validated draft (section 6).
   4. When the instance is shown, the backend builds, checks and runs its 
queries (section 7) and returns the data through an endpoint keyed by the 
instance (section 8).
   5. Canvas filters and cross-filters reach each placement as runtime filters 
(section 9), and the widget's React renderer draws the result (section 10).
   6. Widgets evolve without breaking saved instances (section 11).
   
   Agents reach every step through MCP (section 12).
   
   <details>
   <summary><b>2. Defining a widget: the widget contract</b></summary>
   
   Widgets register into a backend registry in `superset-core`, exactly as 
built-ins do. The following sketch is illustrative, and its names are not final:
   
   ```python
   from superset_core.widgets import QueryMixin, Widget
   from superset_core.widgets.decorators import widget
   
   @widget(
       id="timeseries-bar",
       name="Timeseries Bar",
       description="Bars over time. Use for period-over-period comparisons; 
each series is a value of the grouping dimension.",
   )
   class TimeseriesBar(QueryMixin, Widget):
       controls_class = TimeseriesBarControls  # Pydantic model; its JSON 
Schema is the property panel
       schema_version = 1
       behavior = WidgetBehavior(emits_filters=True, drill=True)
       ui = WidgetUi(default_size=(6, 4))
   
       # From QueryMixin: only widgets that query data implement these
       @classmethod
       def build_queries(cls, view: SemanticView, props: TimeseriesBarControls) 
-> list[WidgetQuery]: ...
   
       @classmethod
       def transform_data(cls, view: SemanticView, props: 
TimeseriesBarControls, results: list[SemanticResult]) -> WidgetPayload: ...
   ```
   
   The decorator gives the widget its identity and registers it with the host, 
which applies extension namespacing; the class carries its behavior. Semantic 
layers (`@semantic_layer`) and MCP tools (`@tool`) register the same way.
   
   Every widget declares:
   
   - **Description:** what the widget is for, when to use it, and how to read 
it. Humans see it in the palette, and agents use it to choose a widget. Every 
widget instance also carries its own `description` field: what that instance 
shows, its business meaning and any caveats.
   - **Properties schema:** a Pydantic model whose JSON Schema is the property 
panel (section 3), together with its `schema_version` (section 11).
   - **Behavior:**
     - For containers: which child widgets they accept, and how many.
     - For charts: which interactivity they support, such as emitting filters 
or drilling down. These replace v1's `Behavior` flags.
   - **UI metadata:** label, default size and header controls.
   
   A widget that queries data mixes in **`QueryMixin`**, which adds two methods:
   
   - **`build_queries(view, props) -> list[WidgetQuery]`** replaces the 
frontend `buildQuery`.
     - It returns query *descriptions*, and the framework validates and 
executes them (section 7).
     - Each `WidgetQuery` has a name, a `SemanticQuery`, and a declared 
runtime-filter policy.
     - It returns a list from day one, so charts that need several queries (for 
example a comparison period, or overlaid series) won't require a breaking 
change later. Most charts return one query.
   - **`transform_data(view, props, results) -> WidgetPayload`** replaces 
`transformProps` for reshaping data on the server, such as pivoting, deriving 
series, or joining the results of several queries.
     - It returns named datasets plus a `context` object, for example which 
dimensions were pivoted, that the renderer can use.
     - It defaults to passing results through, and it is a pure function with 
bounded, typed output (section 7).
   
   **The backend is the single authority for widgets.**
   
   - The palette, the Inspector and agents all read widget metadata from the 
backend. The frontend only registers a renderer for a given widget id.
   - Extension widgets are namespaced as `extensions.<publisher>.<name>.<widget 
id>`.
   - Registering an id that is already taken fails loudly on both backend and 
frontend.
   - A widget is checked thoroughly when it registers (Appendix B), and the 
frontend and backend confirm they agree through a handshake (section 11).
   
   As a result, every widget has a backend part, including purely 
presentational ones. That is what lets an agent work with any widget without a 
browser in the loop.
   
   </details>
   
   <details>
   <summary><b>3. Describing properties: the control schema</b></summary>
   
   Property panels are JSON Schema generated from Pydantic models, plus a small 
set of `x-*` extension keywords, building on the Semantic Layer's progressive 
JSON Schema pattern (`x-dynamic`, `x-dependsOn`, `x-enumNames`). This SIP makes 
the vocabulary normative, so the backend, the frontend and agents can't drift 
apart.
   
   - **A supported subset of JSON Schema.** Schemas stay within a documented 
subset of JSON Schema 2020-12 (Appendix A), so the property panel, the 
disclosure engine, validators and MCP all interpret them the same way. 
Constructs outside the subset are rejected at registration, and a shared 
conformance suite checks every consumer.
   - **A closed, documented list of keywords.** Every `x-*` keyword has a 
defined meaning and at least one consumer. Unknown keywords are rejected when a 
widget registers.
   - **Renderers are chosen only by `x-control`, never by field name.** 
Composite controls (dataset, metric, dimension, ad-hoc filter, time range, 
color, code, and so on) live in a registry that extensions can add to, on both 
the backend and the frontend.
   - **Dynamic fields:**
     - `x-dynamic` marks a field whose options the backend fills in from the 
current props.
     - An enricher declares the fields it reads separately from the rules that 
show or hide a field.
     - Enrichers run in dependency order, and cycles are detected.
     - Options derived from data, such as the distinct series values behind 
per-series styling, are computed on the backend from cached queries. The client 
does not supply them.
   - **Paths are JSON Pointers (RFC 6901).** This is the same addressing JSON 
Patch uses, so keys derived from data, such as series names containing `.` or 
`/`, can always be addressed.
   - **Strict props, enforced by the framework.** Every props model inherits a 
framework base model that forbids undeclared fields and enables Pydantic's 
strict mode, checked recursively at registration (Appendix B). Undeclared props 
are rejected rather than silently dropped, which is Pydantic's default, and 
values are never coerced: `"5"` is not an integer and `"true"` is not a 
boolean. Validation follows JSON semantics, so UUIDs, dates and timestamps are 
accepted only in their canonical string format.
   - **Explicit values only.** Stored props contain only the values that were 
set explicitly; defaults come from the schema (section 11).
   - **Translation.** Labels and descriptions are translated on the backend, 
because that is where the schema lives.
   - **Arbitrarily nested.** JSON Schema is recursive, so a customization 
object can contain arrays of objects to any depth, and the same tree drives 
progressive disclosure (section 4). Rendering any depth is a requirement the 
conformance suite proves, not a given.
   
   **How enrichment runs.**
   - **Inputs.** Enrichers run on the backend from the instance's current 
props. For an instance being edited, those are the props stored in its draft, 
at its current revision (section 6). Runtime filters never affect the schema.
   - **Data-derived options** come from cached queries. They are capped and 
sorted, and flagged when truncated. Maps keyed by data values still accept keys 
that aren't listed, so a value beyond the cap can still be styled.
   - **Canonical keys.** NULL and non-string values have a documented canonical 
key form, so they can be addressed by JSON Pointer.
   - **Authorization.** Option lookups need the same datasource access as data 
queries, and row-level security applies. Superset has no column-level 
permissions, so dataset access covers every column.
   - **Limits.** Enrichment queries are cached and time-limited.
   - **Status per field.** Every dynamic field reports a status: ready, waiting 
on inputs, empty, forbidden, or failed (with a message). The UI and agents can 
tell "no options" apart from "not loaded yet" or "not allowed".
   
   The first milestone's composite controls are:
   - the four that mirror the `SemanticQuery` vocabulary: metrics, dimensions, 
filters and time range;
   - the presentational composites needed by the first widgets: color, code and 
per-series overrides.
   
   </details>
   
   <details>
   <summary><b>4. Reading the schema: progressive disclosure</b></summary>
   
   Control panels must be able to scale to arbitrary depth. A chart's settings 
nest naturally: per-series overrides, axes, legend, tooltip and conditional 
formatting, each with its own sub-options. An AI agent, however, works within a 
limited context window. Sending a widget's full schema on every turn would use 
up a large share of it, and the cost would grow with every option a widget adds.
   
   The framework therefore never makes an agent load a whole schema. It serves 
a minimal root, and the agent expands only the branches the task needs. The 
cost depends on what the agent touches, not on how deep the panel is.
   
   **The minimum viable object.** When a schema is requested without paths, the 
backend returns a pruned view built by fixed rules:
   - **Required fields** are expanded down to their leaves, including inside 
required nested objects. The root alone is enough to build a valid widget 
instance.
   - **Optional cheap fields** (scalars, bounded enums, arrays of scalars) are 
shown inline. They are small and there is nothing to drill into.
   - **Optional nested objects and arrays of objects** are replaced by a 
drill-in marker. The marker holds the field's type, its short description (so 
the agent can judge whether the branch is relevant), and the JSON Pointer to 
expand. It never includes the child fields.
   - **Partial objects are flagged.** An object that withheld children is 
marked partial, and the root is marked as a minimal view.
   - **Descriptions** of the widget and of the instance are always included. 
They let an agent decide what a widget is for, or what an instance shows, 
without reading its full configuration.
   
   **Expanding branches.**
   - **Several branches per call.** An agent expands one or more markers in a 
single call by passing their paths.
   - **Same rules at every level.** Each expanded branch is disclosed by the 
same rules: its own required fields expanded, its deeper optional branches 
collapsed again. Expanding a top-level branch of a deep panel therefore doesn't 
return everything beneath it. A consumer can still ask for a complete branch 
when it knows the branch is small.
   - **Shared addressing.** Paths are the same JSON Pointers that JSON Patch 
uses. An agent can expand `/customization/series` and then patch 
`/customization/series/EMEA/color` using the same addresses. Validation errors 
also point at paths the agent can expand directly.
   - **Data-keyed maps.** Maps keyed by data values, such as per-series 
settings, can be expanded by key.
   
   **Dynamic branches.**
   - **Flagged when data-dependent.** A marker is flagged `x-dynamic` when the 
branch's shape depends on live data, for example a map keyed by a dimension's 
distinct values. Branches without the flag never change, so an agent can expand 
them once and reuse the result.
   - **Works against a draft.** The agent passes its draft token (section 6), 
and the backend enriches the branch from the draft's stored props. The agent 
doesn't have to resend the props on every call.
   
   **Large option lists.** Bounded enums are inlined. Option lists that can 
grow without limit (datasets, columns, distinct values) are never inlined into 
a schema view; the field points instead to a searchable, paginated options 
lookup.
   
   **One mechanism for every consumer.**
   - **REST and MCP match.** Both serve the same minimal view and the same 
format for expanded branches, and the property panel can use the same rules to 
render collapsed sections lazily.
   - **Views are deterministic for given inputs:** the schema and contract 
version, the draft revision, the user's locale and permissions, and the 
datasource's metadata. The same inputs give the same view, so views can be 
cached and tested. Data-derived options can change with the data; dynamic 
markers flag where.
   
   </details>
   
   <details>
   <summary><b>5. Storing instances: persisted, inline and placed</b></summary>
   
   Once created, a widget instance has to live somewhere. The two storage modes 
differ only in where the props live and how access is checked:
   
   | | Persisted instance | Inline instance |
   |---|---|---|
   | Stored in | A new `WidgetInstance` table | The Canvas document, on its 
placement |
   | Identity | Its own UUID | The Canvas UUID plus the placement id |
   | Permissions | Its own editors and viewers | Taken from the Canvas |
   | Placements | Zero or more, on any number of canvases | Exactly one |
   | Addressed by | Agents, reports, links and canvases, directly | Through its 
Canvas |
   | Typical use | Shared charts, instances that need their own access rules | 
Text, layout containers, one-off charts, exported snapshots |
   
   **Persisted instances** live in a **new model**, not in the `Slice` table:
   
   | Field | Purpose |
   |---|---|
   | `uuid` (primary key) | Stable public identifier |
   | `widget_id` | Namespaced id of the widget this is an instance of |
   | `schema_version` | Version of the widget's schema that the props conform 
to |
   | `props` | JSON, valid against the widget's schema; explicitly set values 
only |
   | `revision` | Incremented on every change, exposed as an ETag; updates must 
state the revision they're based on |
   | datasource reference(s) | For chart instances; used for access checks and 
lineage |
   | editors / viewers | Subject-based access, as charts and dashboards have |
   | audit fields | Created and changed by, created and changed on |
   
   **Inline instances** store the same `widget_id`, `schema_version` and 
`props` on their placement in the Canvas document. Datasource references are 
derived from the props for access checks and lineage, exactly as for persisted 
instances. The Canvas document's own revision protects inline instances against 
concurrent changes.
   
   **Placements.** A Canvas shows widget instances through placements. A 
placement holds the instance's position, size and filter scope, all specified 
in the companion Canvas SIP. It either stores an inline instance or references 
a persisted instance by UUID.
   - **Persisted instances and canvases are many-to-many.**
     - A persisted instance stands on its own: it can exist without any 
placement, for example a chart that is only shared by link or used in a report.
     - It can also be placed any number of times, on the same Canvas or on 
several. For example, the same chart can appear twice with differently scoped 
filters (section 9).
     - Removing its last placement doesn't delete it.
   - **An inline instance has exactly one placement.** To show it again, 
promote it.
   
   **Converting between modes.**
   - An inline instance can be **promoted** to a persisted instance, to place 
it again, reuse it elsewhere or give it its own permissions.
   - A persisted instance can be **inlined** as a copy, which detaches it from 
the shared original.
   - Both are operations on the Canvas document, specified in the companion 
Canvas SIP.
   
   **Export snapshot.** Inline storage is what makes an "export snapshot" 
feature straightforward:
   - Exporting a snapshot produces one self-contained, immutable document: the 
Canvas with every instance inline.
   - Persisted instances the Canvas references are copied in at export time. 
Later edits to a shared instance therefore don't change the snapshot.
   - The snapshot can be archived, shared, compared, or imported again as a new 
Canvas.
   
   The snapshot format and lifecycle, including whether query results are 
included, belong to the companion Canvas SIP.
   
   **Common to both modes:**
   - **Permissions:** a new `Widget` permission resource (`can_read`, 
`can_write`) governs the widget and widget instance APIs.
   - **Feature flag:** the whole surface sits behind a flag while it incubates. 
With the flag off, the API routes and MCP tools aren't registered at all, 
rather than only being hidden in the UI.
   - **Import/export:** covers persisted instances alongside canvases. Inline 
instances travel inside their Canvas.
   - **`Slice` migration:** existing `Slice` rows are expected to move into 
persisted widget instances in a later phase (see the migration plan).
   
   </details>
   
   <details>
   <summary><b>6. Editing instances: drafts and validation</b></summary>
   
   Wherever an instance is stored, it is never edited in place. Edits are made 
to **drafts stored on the server**. Drafts reuse the key-value store that 
already holds Explore's temporary state, and like that state they expire after 
a period of inactivity.
   
   **Every edit happens in an isolated draft, whatever the storage mode.**
   - **Where a draft can start:** from a persisted instance, from an inline 
instance (Canvas UUID plus placement id), or from a widget for a brand-new 
instance.
   - **What it holds:** only that instance's state, under its own token.
   - **Why isolation matters:**
     - An agent can iterate on the instance for as many turns as it needs: 
patching it, validating it, expanding its schema and previewing its data.
     - The agent never has to load or modify the whole Canvas document.
     - Other users don't see the intermediate states.
   - **Only an explicit commit writes the result back.**
   
   This matters most for inline instances. Their state still has to live 
somewhere on its own while it is being worked on, and the draft is that place.
   
   **Token rules.** These apply to draft tokens and to the data chunk tokens of 
section 8:
   - **Unguessable, expiring, and never logged.** Draft tokens are also 
invalidated on commit or delete.
   - **A locator, never an authorization.** Access is re-checked on every use.
   - **Bound to the authenticated principal and its authorization context, not 
to a browser session.** The principal may be a user, an embedded guest token, 
or a service identity acting for a user. A user's browser tab and an agent 
acting as that user are the same principal, so they can work on the same draft.
   
   **Versioning and concurrency.** A draft records the `schema_version` its 
props conform to and a `revision` incremented on every change. Every write 
states the revision it is based on, as an `If-Match` ETag or in the body. On a 
mismatch the write is rejected with a conflict that includes the current 
revision, so a concurrent edit from another tab or an agent is never silently 
overwritten. At commit, a draft of a persisted instance checks the instance's 
revision, and a draft of an inline instance checks the revision of the Canvas 
it started from.
   
   **How changes are sent.** The UI replaces the whole props object on each 
edit. Agents send **JSON Patch (RFC 6902)** operations, which are compact and 
express inserting, removing and reordering array items precisely.
   
   **Patch mechanics.**
   - All six RFC 6902 operations are supported. `move` and `copy` are validated 
like an `add` at the destination, and a `move` also counts its source as a 
written path. A failing `test` rejects the whole patch.
   - Patches are atomic: they apply completely or not at all.
   - Configurable limits cap document size, patch size and operations per patch.
   
   **Validation is strict about what a write touches, and tolerant of what 
isn't finished yet.**
   - **A write can't introduce an error.** A write is rejected, leaving the 
draft unchanged, if it would leave an error at any path it writes: a wrong 
type, an unknown key, a value outside an enum, a reference that doesn't exist 
(such as a metric the dataset doesn't have), or a malformed path. For a 
whole-props replacement, the written paths are the ones whose values changed.
   - **A draft may be unfinished.** Missing required fields never block a 
write, so an instance can be built up one field at a time. References made 
stale by a change elsewhere (switching the dataset makes the selected metrics 
stale) are kept and reported, not silently cleared. An agent can also change 
dependent fields together in one atomic patch.
   - **Two gates.** Preview requires the fields that affect the query to be 
valid. Commit requires full validation: the complete schema, datasource access, 
existence of every referenced column and metric, migrations, and validation of 
the queries the instance would build (section 7).
   - **Visible state.** The whole draft is revalidated after every accepted 
write, and exposes whether it can be previewed, whether it can be committed, 
and its open errors.
   
   **Errors are written for an agent to act on without guessing.** Each carries 
a JSON Pointer path, a stable code, a plain-language message, what was expected 
and received, permission-filtered suggestions (such as valid values or the 
metrics available on the dataset), a next step (such as a path to expand or an 
options lookup to call), and what it blocks. The format is in Appendix C.
   
   **Committing.** A draft writes its props back to where the instance lives:
   - **Persisted instance:** to the `WidgetInstance` table.
   - **Inline instance:** to the Canvas document, through the Canvas's own 
update operation (companion SIP).
   - **New instance:** created in either mode.
   
   **There is one write path.** The property panel, a raw JSON editor, an 
extension calling the public Canvas API, and an MCP agent all go through the 
same validation, whichever storage mode the instance uses.
   
   </details>
   
   <details>
   <summary><b>7. Querying: <code>QueryMixin</code> and 
<code>SemanticQuery</code></b></summary>
   
   A chart turns an instance's props into queries on the backend. 
`SemanticQuery`, introduced by the Semantic Layer work (SIP-182), is the only 
query format widget authors see. It is a small, typed model: metrics, 
dimensions, filters, order, limit and group limit. The four core composite 
controls map onto it one-to-one, which keeps most `build_queries` 
implementations simple.
   
   **Datasets get an adapter.**
   - Today a `SemanticQuery` can only be executed against an external semantic 
view.
   - This SIP adds a **`SemanticView` adapter for regular datasets 
(`SqlaTable`)**. It runs a `SemanticQuery` through the existing query engine by 
translating it into a `QueryObject`. That is the reverse of the mapping that 
already exists for semantic views (`superset/semantic_layers/mapper.py`).
   - Widget authors then write a single query path that works for both datasets 
and semantic views.
   - Charts backed by datasets keep every engine behavior they have today: 
row-level security, Jinja templating, caching and time grains.
   
   **Query boundaries.** Widgets are written by core and by extensions. 
Extensions are trusted by the operator (see `SECURITY.md`), so the concern 
isn't malicious code. The concern is a widget accidentally producing a query 
that a Gamma user, an embedded guest or a custom role isn't entitled to run. 
The framework therefore enforces the boundaries itself instead of relying on 
each widget:
   - **Widgets describe, the framework executes.** `build_queries` returns 
query descriptions, and the framework binds them to the instance's datasource 
and runs them. A widget never executes a query or opens a connection itself.
   - **Every query is checked.**
     - Metrics and dimensions are re-resolved by id against the datasource's 
catalog.
     - This matters because metrics and dimensions in a `SemanticQuery` carry 
SQL `definition` strings. Any expression not found in the catalog is treated as 
user-authored ad-hoc SQL and goes through the same validation as ad-hoc SQL in 
v1.
     - Filter, ordering and grouping fields are checked the same way.
   - **Only declared datasources.** An instance queries the datasources its 
props declare, after authorization. A query can't add an undeclared secondary 
datasource.
   - **Runtime filters**, meaning Canvas filters and cross-filters (section 9), 
**are applied centrally** after `build_queries`. Widgets never add or remove 
filters in code.
     - **Declared policy per query.** Each `WidgetQuery` declares how runtime 
filters apply to it:
       - `apply`: the default;
       - `ignore`: for example the denominator of a share-of-total;
       - possibly a constrained subset later.
     - **Visible to users.** The policy is reported in the data manifest 
(section 8) and shown in the Inspector, so it's clear why a result didn't react 
to a filter.
     - **Not a security boundary.** Runtime filters only narrow data the user 
is already entitled to, so the policy is a consistency rule.
   - **Server limits win.** Row limits, query counts, timeouts and result sizes 
configured on the server clamp whatever a widget requests.
   - **Row-level security and user context are applied at execution**, in the 
existing engine through the `SqlaTable` adapter, including cache keys that 
depend on the user.
   
   These rules uphold the Gamma ("only granted datasets"), embedded guest token 
and custom-role rows of the role and capability matrix in `SECURITY.md`.
   
   **Transforms.** `transform_data` is a pure function of the props and the 
query results:
   - **Behavior:**
     - it issues no additional queries and does no I/O;
     - it is deterministic, so its output can be cached and reports are 
reproducible;
     - it runs in the same execution context as its queries: synchronously in 
M1, and later asynchronously through the Global Task Framework (SIP-227).
   - **Output is bounded and typed:**
     - `context` uses JSON types only (strings, numbers, booleans, null, lists 
and string-keyed objects) and has a size cap;
     - datasets are uniquely named, their Arrow types are normalized to a 
documented supported set, and their size is capped.
   - **Errors and warnings** are structured and returned in the manifest.
   - **Multi-query failure:** if any of an instance's queries fails, the 
instance fails. This is the M1 behavior; partial results are an open question.
   
   **Caching.**
   - Raw query results from `build_queries` and from schema enrichment are 
cached through the existing cache layer and shared across instances.
   - Transformed output is cached under a key made of:
     - the identities of every raw result it was computed from. Those are the 
cache keys of the underlying queries, which already capture every query input: 
runtime filters, row-level security and other user context, and the 
datasource's revision;
     - the instance's revision;
     - the implementation version (section 11);
     - the locale, when the output depends on it.
   
     Two executions of the same instance with different filters or different 
user context therefore never share transformed output, and deploying new 
transform code invalidates it.
   - The server can warm caches, and alerts, reports and thumbnails can get a 
chart's data, without running any JavaScript.
   
   </details>
   
   <details>
   <summary><b>8. Delivering data: access by widget instance</b></summary>
   
   Those queries run when a chart is displayed, and their results are fetched 
by widget instance, not by query:
   
   ```
   POST /api/v1/widget_instance/{uuid}/data                     (persisted 
instance)
   POST /api/v1/canvas/{uuid}/placement/{placement_id}/data     (inline 
instance)
     body:  {"runtime_filters": [...]}
     → 200  JSON manifest: {context, warnings, datasets: [{name, arrow_schema, 
row_count, inline_data | chunks}]}
   GET  /api/v1/widget_instance/data/{token}/{dataset}/{chunk}  (large results 
only)
     → 200  application/vnd.apache.arrow.stream
   ```
   
   - **The query definition always comes from storage.**
     - The server loads the instance's props and datasource, from the 
`WidgetInstance` table or from the Canvas document.
     - It then validates and runs the instance's queries (section 7), runs 
`transform_data`, and returns the result.
     - The caller's only input is `runtime_filters`, meaning Canvas filters and 
cross-filters. These are validated to be strictly additive constraints on 
columns of the instance's own datasource.
     - A caller cannot add metrics, remove group-bys, or select raw columns.
   - **Unsaved instances** (drafts, see section 6) are previewed through their 
draft token, using the same pipeline.
   - **The transport is a JSON manifest plus Arrow IPC.**
     - Query results are already Arrow tables on the backend, and the browser 
decodes them with `apache-arrow`.
     - **Small results** come back inline in the manifest response, so the 
common case needs a single request per placement.
     - **Large results** are split into Arrow IPC chunks fetched over plain 
HTTP. This avoids page loads that need one manifest request plus many chunk 
requests for every placement.
     - The manifest itself stays JSON, which makes it easy to log, cache and 
hand to an agent.
     - Arrow Flight does not work in browsers and is not part of this proposal.
   - **Chunk tokens** follow the token rules in section 6, and are also bound 
to the instance revision and the resolved query, including its runtime filters. 
Chunk responses carry `Cache-Control: private`, chunk size and count are 
limited, and if a chunk fetch fails the renderer requests the manifest again.
   - **Shared caches are safe.** Results are shared across users only when the 
cache key captures everything that affects them: row-level security, 
impersonation and templating that depends on the user. One Gamma user or 
embedded guest can therefore never receive another principal's result.
   - **Access checks:**
     - **Persisted instance:** reading its data requires access to the instance 
*and* to its datasource.
     - **Inline instance:** reading its data requires access to the Canvas 
*and* to the instance's datasource.
     - **Either way, today's data entitlement is unchanged.**
     - **Later:** because the stored definition owns the query's shape, the 
design makes it possible to grant "read this instance's output" without 
granting the dataset, giving out an aggregate without its granular rows. That 
would add a row to the role and capability matrix in `SECURITY.md`, so it is 
left for a follow-up proposal rather than enabled here.
   
   </details>
   
   <details>
   <summary><b>9. Reacting to filters: the interactivity contract</b></summary>
   
   Filters, cross-filters and drill-down move off the central Redux `dataMask` 
store. This SIP defines only what a widget needs in order to take part. The 
companion Canvas SIP defines the listener bus that connects placements on a 
Canvas: where it lives, how filter scope is configured, how filter state is 
held and serialized into URLs and permalinks, and how cascading filters work.
   
   **Filter scope belongs to placements, not instances.** Each placement 
subscribes to the filters in its own scope and sends them as `runtime_filters`. 
Two placements of the same instance can therefore show different results. 
Runtime filters are part of the cache key, so the two never share cached 
results.
   
   What a widget needs:
   - **Runtime filters as input.** The data endpoints accept `runtime_filters` 
in the same vocabulary as the chart's own filters: `{column, operator, value, 
datasource?}`. That gives one filter concept throughout. The framework applies 
them to the instance's queries (section 7).
   - **Hooks to subscribe and emit.**
     - `useRuntimeFilters()` returns the filters currently applying to the 
placement, and refetching follows automatically.
     - `emitFilter()` publishes a filter: clicking a bar emits `region = EMEA`. 
Drill-down works the same way.
     - The hooks are public API in `@apache-superset/core`, so built-in and 
extension widgets take part in exactly the same way.
   - **Declared interactivity.** The widget's behavior declaration says whether 
it emits filters or supports drill-down, so a Canvas can offer the right 
affordances.
   
   For v2 widgets, the Redux `dataMask` / `getFormDataWithExtraFilters` 
pipeline is replaced, not wrapped.
   
   </details>
   
   <details>
   <summary><b>10. Rendering: the frontend contract</b></summary>
   
   A widget's frontend is **a React component and nothing else**: no 
`buildQuery`, no `transformProps`, and no control-panel code. Inside that 
component the renderer can use any library, such as ECharts, D3, deck.gl, SVG, 
the HTML canvas element, WebGL or plain HTML/CSS, or any mix of them. The 
renderer doesn't need to know whether its instance is persisted or inline, or 
how many times it is placed.
   
   Authors compose ordinary hooks from `@apache-superset/core`: 
`useWidgetData()` fetches the instance's data and refetches when runtime 
filters change, and `useRuntimeFilters()` and `emitFilter()` take part in 
interactivity (section 9).
   
   The property panel is rendered generically from the served schema with 
JSONForms, which Superset already uses for Semantic Layer configuration, plus 
the composite renderer registry from section 3. Widget renderers are loaded 
lazily, and each declares the schema versions it supports (section 11).
   
   </details>
   
   <details>
   <summary><b>11. Evolving: versioning, compatibility and 
lifecycle</b></summary>
   
   Widgets change over time, through new releases and as extensions are 
installed, upgraded or removed. Saved instances have to survive all of that.
   
   **Schema versions and stored values.** Each widget's schema has an integer 
`schema_version` that only increases. Only explicitly set values are stored, 
which keeps props small for agents and diffs. Any change to what stored props 
mean requires a version bump and a migrator, including changing a default: the 
migrator writes the old default into existing instances, so they never change 
behavior by accident.
   
   **Migrations** are applied in memory whenever an instance is read, and the 
upgraded props are saved on the next commit, so read paths never write. An 
optional offline command upgrades every stored instance in bulk. Migrators must 
form an unbroken chain to the current version, and there are no downgrades.
   
   **When an instance can't be loaded.** An instance becomes a durable 
placeholder in three cases:
   - its migration fails;
   - it was saved by a newer version of its widget than the server has;
   - its widget is unavailable, for example because the extension that provides 
it is disabled or removed.
   
   The placeholder keeps the raw props, widget id, schema version and placement 
exactly as stored, and never normalizes or overwrites them, so re-enabling the 
extension or upgrading the server restores the instance. The rest of the Canvas 
keeps working, and the instance's data endpoint returns a structured "widget 
unavailable" error.
   
   **Implementation version.** A widget's implementation version is the version 
of the package that provides it: the Superset release for built-in widgets, and 
the extension's manifest version for extension widgets. It is the same across 
identical deployments (unlike a build hash) and changes with every release, so 
any change to query or transform semantics is covered. That is conservative for 
caching, but never stale. Transformed-output caches and exports use it.
   
   **Exports and snapshots.** Exports record the widget id, the 
`schema_version` and the implementation version. Old renderer and backend code 
isn't kept around for snapshots, so a snapshot that contains an unavailable 
widget shows placeholders.
   
   **Frontend and backend handshake.** The backend decides which widgets exist, 
but the browser also needs a matching renderer, and during rolling deploys or 
extension updates the two can briefly disagree. Both sides declare a widget 
contract version, each renderer declares the schema versions it can render, and 
the frontend reports mismatches with the backend's widget list in diagnostics.
   - **Only the renderer is missing or incompatible** (missing, failed to load, 
or not supporting the instance's schema version): the visualization shows a 
placeholder with a retry option. The property panel keeps working, because the 
schema comes from the backend, as long as the backend understands the stored 
schema version and the client supports the contract version and every control 
the schema requires.
   - **Otherwise, nothing can be edited.** An instance saved by a newer version 
than the server knows, or a schema whose contract version the client doesn't 
support, becomes a read-only placeholder for both rendering and editing.
   - Schema responses carry an ETag derived from the contract version, schema 
version, locale and draft revision, so caches are invalidated when any of them 
changes.
   
   </details>
   
   <details>
   <summary><b>12. Agent access: the MCP surface</b></summary>
   
   Every step above is exposed to agents as MCP tools, through the existing MCP 
service and its permission-aware `@tool` decorator. The tools wrap the same 
commands the UI uses, so they share validation, permissions and behavior:
   
   | Tool | Purpose |
   |---|---|
   | `list_widgets` | Discover registered widgets (id, name, description) |
   | `get_widget_info` | A widget's metadata, including its behavior |
   | `get_widget_control_schema` | The minimal view, or expanded branches by 
path; takes a draft token so the draft's props drive enrichment (section 4) |
   | `get_widget_field_options` | Search and paginate a field's options, such 
as datasets, columns or distinct values |
   | `create_widget_instance_draft` / `get_widget_instance_draft` | Start a 
draft from a persisted instance, an inline instance or a widget; read a draft 
and its validation state |
   | `patch_widget_instance_draft` | Apply a JSON Patch under the rules of 
section 6 |
   | `preview_widget_instance_draft` | Run the draft's queries, optionally with 
runtime filters; return a manifest or a small sample |
   | `commit_widget_instance_draft` | Fully validate the draft and write it 
back, or create a new instance |
   
   </details>
   
   ### New or Changed Public Interfaces
   
   <details>
   <summary>Full list</summary>
   
   **Backend (`superset-core`)**
   - `superset_core.widgets`:
     - the `Widget` base class, `QueryMixin`, and the `@widget` decorator (in 
`superset_core.widgets.decorators`);
     - the base props model, which forbids undeclared fields;
     - behavior and UI metadata types;
     - `WidgetQuery` (a named query with its runtime-filter policy) and 
`WidgetPayload`;
     - the enrichment API;
     - the migrator API;
     - the composite-control registry;
     - the widget contract version.
   - A `SemanticView` adapter for `SqlaTable` datasets.
   
   **REST: widgets (`/api/v1/widget`)**
   - `GET /`, `GET /{id}`;
   - `POST /{id}/control-schema` (minimal or full view, optional `paths`, 
optional draft token);
   - `POST /{id}/field-options` (search and paginate a field's options);
   - `POST /{id}/validate`.
   
   **REST: widget instances (`/api/v1/widget_instance`)**
   - **Persisted instances:** CRUD on `/{uuid}` with revision checks, plus 
import and export.
   - **Drafts:** `POST /draft` (from a persisted instance, an inline instance 
or a widget), `GET|PUT|PATCH|DELETE /draft/{token}` (responses include the 
validation state), `POST /draft/{token}/data`, `POST /draft/{token}/commit`.
   - **Data:** `POST /{uuid}/data`, `GET /data/{token}/{dataset}/{chunk}`.
   
   **REST: Canvas resource:** `POST 
/api/v1/canvas/{uuid}/placement/{placement_id}/data` for inline instances. The 
rest of the Canvas resource, including placements, promoting and inlining 
instances, and exporting snapshots, is defined in the companion Canvas SIP.
   
   **MCP:** the tools in section 12.
   
   **Frontend (`@apache-superset/core`)**
   - registration of widget renderers, keyed by namespaced widget id, with the 
schema-version range each one supports;
   - the data and interactivity hooks (`useWidgetData`, `useRuntimeFilters`, 
`emitFilter`);
   - runtime-filter types;
   - the composite renderer registry;
   - the widget contract version.
   
   **Security:** a new `Widget` permission resource (`can_read`, `can_write`), 
plus the data access rules (section 8), query boundaries (section 7) and token 
rules (section 6).
   
   **Configuration:** a feature flag gating the API routes, the MCP tools and 
the Canvas builder (section 5).
   
   **Models:** a `WidgetInstance` table for persisted instances, plus its 
editors/viewers association tables. Inline instances live in the Canvas 
document (companion SIP).
   
   **Unchanged:** v1 chart plugins, Explore, existing dashboards, 
`/api/v1/chart/data` and `query_context`.
   
   </details>
   
   ### New dependencies
   
   - **`apache-arrow` (npm):** decodes Arrow IPC in the browser. Apache License 
2.0, maintained by the Apache Arrow project. `pyarrow` is already a backend 
dependency.
   - **A JSON Patch implementation for Python,** for example `jsonpatch` 
(BSD-3-Clause). It is small and stable. Given the limited set of operations, it 
could also be implemented in-house.
   - **No new frontend form library:** `@jsonforms/*` is already a dependency.
   
   ### Migration Plan and Compatibility
   
   Nothing changes for existing deployments until they opt in. v1 charts, 
Explore and existing dashboards are untouched, and the new surface sits behind 
a feature flag. Phases are gated on milestones rather than release dates; each 
phase's release number is set when its milestone is reached.
   
   1. **M1: Framework.** Ships behind the feature flag:
      - the widget contract, the schema vocabulary and its conformance suite;
      - the `SqlaTable` adapter and framework-enforced query boundaries;
      - persisted instances (the table and its migration), inline instances and 
placements;
      - drafts, the data endpoints and the MCP tools;
      - the interactivity hooks;
      - the core widgets Canvas needs.
   2. **M2: Parity.** The most-used visualization types are available as v2 
widgets, with an authoring experience equivalent to Explore. The feature flag 
turns on by default.
   3. **M3: Convergence.** A migration converts saved `Slice` rows into 
persisted widget instances wherever a v2 widget exists for them. Charts that 
aren't on any dashboard become persisted instances without placements. v1 chart 
plugins and the v1 chart-data path are formally deprecated, and the deprecation 
is recorded in `UPDATING.md`.
   4. **M4: Removal.** v1 chart plugins and their supporting code are removed 
in the next major release.
   
   **Database migrations:** M1 adds the widget instance tables, and M3 adds the 
data migration from `Slice`. Upgrades of props between schema versions are 
handled by each widget's own migrators (section 11), not by database migrations.
   
   **Stored URLs:** v1 chart and dashboard URLs keep working through M3. Canvas 
permalinks are defined in the companion Canvas SIP.
   
   ### Rejected Alternatives
   
   <details>
   <summary>Alternatives considered</summary>
   
   - **A Node.js sidecar that builds queries on the server (SIP-211).** It 
takes the browser out of the loop, but keeps query logic in JavaScript and adds 
a runtime to deploy and operate. Query building in Python needs no extra 
service, and is inspectable and testable on the backend.
   - **Letting widgets execute their own queries.** Every widget, including 
extension-provided ones, would have to get authorization, row-level security, 
limits and caching right on its own.
   - **Schemas owned by the frontend, or a serializable subset of 
`controlPanel.tsx`.** An agent would need a browser, or a copy of the frontend 
registry, to know what a widget accepts, and backend validation would need a 
second copy of the rules.
   - **Two independent registries (frontend and backend) joined by an id.** The 
palette, the Inspector and agents could then disagree about which widgets exist.
   - **Registering widgets by subclassing alone.** Abstract base classes and 
mixins would register by accident, and there would be no natural point for the 
host to apply extension namespacing.
   - **Reusing the `Slice` table.** It would inherit ownership, tags and 
dashboard relationships for free, but every v1 code path assumes `params` and 
`query_context`, and the new table holds instances of every kind of widget, not 
just charts. `Slice` rows can migrate into it later.
   - **A single storage mode.** Persisted-only would make every text block and 
container a separate row with its own permissions, and snapshots would have no 
natural self-contained form. Inline-only would leave no stable id for reuse, 
multiple placements, instances that live outside any Canvas, separate 
permissions, or direct addressing by agents, reports and links.
   - **Requiring every draft revision to be fully valid.** Building a new 
instance, or switching its dataset, would need one large atomic replacement for 
every change.
   - **Shallow merges or JSON Merge Patch for writes.** Neither can precisely 
express inserting, removing or reordering array items, which nested controls 
need.
   - **Storing defaults in props.** Every stored instance would freeze every 
default, adding noise for agents and diffs.
   - **Arrow Flight to the browser.** Browsers can't speak gRPC-based Flight 
without gRPC-web and a proxy. Arrow IPC over HTTP gives the same columnar 
benefits, and Flight can still be adopted later between services.
   - **Wrapping the Redux `dataMask` pipeline for v2.** This would keep the 
Canvas as the central orchestrator, and extensions could not take part on equal 
terms.
   
   </details>
   
   ### Prototype
   
   A proof of concept on the `dashboard-v2` feature branch evaluated the 
approach end to end. It confirmed that:
   
   - core and extensions can register widgets through one contract in 
`superset_core.widgets` (#43262);
   - schemas generated from Pydantic models, with dynamic fields enriched in 
dependency order, can drive data-aware controls for dataset, column, metric 
(including custom SQL) and per-series styling (#43588);
   - an agent can build and restyle charts through progressive disclosure, 
without loading whole schemas (#43262);
   - specific validation errors let an agent correct its own requests (#43262);
   - an event bus lets filters and cross-filters reach widgets, scoped to the 
same datasource by default (#43400);
   - chat extensions can register client-side tools that drive the same widgets.
   
   ### Related proposals
   
   - **SIP-182, Semantic Layer support:** the source of `SemanticQuery`, 
`SemanticView`, and the progressive JSON Schema pattern this SIP builds on.
   - **SIP-151, plugins architecture:** the extensions framework that delivers 
widgets.
   - **SIP-211, server-side query context generation:** the Node.js sidecar 
alternative (see Rejected Alternatives).
   - **SIP-219, a uniform model for built-in extensions:** built-in and 
extension widgets share one registration model.
   - **SIP-207, replacing Redux with Zustand and TanStack Query:** 
complementary; v2 interactivity doesn't use Redux.
   - **SIP-227, async and realtime by default:** the natural home for running 
widget queries and transforms asynchronously.
   - **Canvas SIP (companion, forthcoming):** the Canvas document, placements 
and their filter scope, promoting and inlining instances, the snapshot export, 
and the listener bus (filter state, URL serialization, cascading filters).
   
   ### Open questions
   
   - **Default storage mode.** When a widget instance is created on a Canvas, 
should it be inline or persisted by default? Should the default depend on the 
widget (for example inline for text and containers, persisted for charts)?
   - **Access through a Canvas.** Can Canvas viewers read a persisted instance 
they reach through the Canvas without explicit access to the instance? This is 
the same question `VIEWER_PROMISCUOUS_MODE` answers for dashboards and charts 
today.
   - **Inline instances beyond snapshots.** Are inline instances a general 
storage mode on live canvases, or only the form instances take inside an 
exported snapshot? Isolated drafts support either choice.
   - **Snapshot data.** Does an exported snapshot include only instance 
definitions, or also their query results? Arrow results make bundling the data 
feasible.
   - **Standard `context` keys.** Which parts of the result `context` (pivot 
metadata, column roles, series descriptors) should be standardized first?
   - **Descriptions.** Should widget descriptions be free text or lightly 
structured (purpose, when to use, how to read), and should agents propose an 
instance's description when creating it?
   - **Running several queries.** Parallel or sequential execution, per-query 
caching and `force`, partial results instead of failing the whole instance, and 
reusable join helpers.
   - **Batch data requests.** Should one request fetch data for several 
placements, beyond inlining small results?
   - **Remaining composite controls.** Time grain, conditional formatting, 
annotation layers and the other v1 composites beyond the first milestone.
   - **Pruning heuristics.** Should branches collapse by kind (objects 
collapse, scalars stay inline, as in section 4) or by size (inline any branch 
below a token budget)? And should expanding a branch accept a depth or budget 
hint?
   - **Recursive schemas.** Should self-referencing models, such as nested 
filter groups, be supported in a later version of the schema subset?
   - **Embedded canvases.** How guest tokens authorize reading data, for both 
inline and persisted instances.
   - **Reports and thumbnails.** How alerts, reports and thumbnails adopt 
`build_queries` for v2 charts.
   
   ### Appendices
   
   <details>
   <summary><b>Appendix A: Supported JSON Schema subset and conformance 
suite</b></summary>
   
   **Dialect.** JSON Schema draft 2020-12, as emitted by Pydantic v2.
   
   **Supported constructs:**
   - **Types:** `object`, `array`, `string`, `number`, `integer`, `boolean`, 
`null`.
   - **Objects:**
     - `properties` and `required`;
     - `additionalProperties` set to a schema, for typed maps such as 
per-series settings;
     - `additionalProperties: false`, which is implied by strict props.
   - **Arrays:** `items` with a single schema (homogeneous arrays), `minItems`, 
`maxItems`, `uniqueItems`.
   - **Values:** `enum`, `const`, `default`.
   - **Scalar constraints:** `minimum`, `maximum`, `exclusiveMinimum`, 
`exclusiveMaximum`, `multipleOf`, `minLength`, `maxLength`, `pattern`, `format`.
   - **Annotations:** `title`, `description`, and the documented `x-*` keywords.
   - **References:** local `$defs` and `$ref`. A `$ref` may also be wrapped in 
a single-entry `allOf` to attach annotations.
   
   **Canonical encodings:**
   - **Nullable field:** `anyOf: [<schema>, {"type": "null"}]`.
   - **Discriminated union:** `oneOf` plus `discriminator` (`propertyName` and 
`mapping`). Each branch is an object whose discriminator property is a `const`.
   - **Fixed set of choices:** `enum`, with labels in `x-enumNames`.
   
   **Not supported, and rejected at registration:**
   - tuples (`prefixItems`);
   - conditional schemas (`if`/`then`/`else`, `dependentRequired`, 
`dependentSchemas`);
   - `patternProperties`;
   - `not`;
   - untagged `anyOf`/`oneOf` (other than the nullable encoding);
   - `allOf` beyond the single-`$ref` wrapper;
   - remote references;
   - recursive references (deferred; see open questions).
   
   **Pydantic constructs widget authors may use:**
   - **Models:** subclasses of the framework's base props model.
   - **Scalars:** `str`, `int`, `float`, `bool`, `Literal`, `Enum`, `UUID`, 
`date`, `datetime`. These are validated strictly in JSON mode (section 3).
   - **Containers:** `list[T]`, `set[T]` (as `uniqueItems`), `dict[str, T]`.
   - **Optional and union types:** `Optional[T]`, and discriminated unions 
(`Annotated[Union[...], Field(discriminator=...)]`).
   - **Constraints:** declared with `Field`.
   
   Anything that emits an unsupported construct fails registration with a 
message naming the field and the construct.
   
   **Unknown `x-control` values.**
   - **Backend:** registration fails if a schema uses an `x-control` that isn't 
registered.
   - **Frontend:** a control it doesn't know, for example during version skew, 
falls back to a generic renderer chosen by JSON type. If there is none, it 
shows a read-only JSON view of the value. Unknown controls never silently drop 
data.
   
   **Conformance suite.** A shared set of fixtures, each made of:
   - a schema;
   - props;
   - the expected minimal view and expansions;
   - the expected validation errors, in the format of Appendix C;
   - the expected rendering outcome.
   
   Fixtures cover strict validation of booleans, integers, numbers, UUIDs, 
dates, enums and nested models. Backend CI runs the suite for disclosure and 
validation, frontend CI checks that every field renders and values round-trip 
unchanged, and the MCP tests run it too. Widgets can contribute fixtures, and 
extension authors can run the suite against their own widgets.
   
   </details>
   
   <details>
   <summary><b>Appendix B: Registration checks</b></summary>
   
   When a widget registers, the framework checks that:
   
   1. **Strict props.** Its props model, and every nested model (including 
inside lists, maps and unions), inherits the framework base model that forbids 
undeclared fields and enables strict mode.
   2. **Supported schema.** The generated schema stays within the supported 
subset (Appendix A).
   3. **Known keywords.** Every `x-*` keyword is in the documented list, and 
every `x-control` names a registered composite control.
   4. **Enrichers wired.** Every `x-dynamic` field has an enricher, and every 
enricher input is a valid JSON Pointer into the schema.
   5. **No cycles.** The enrichment dependency graph has no cycles.
   6. **Valid defaults.** Every default value validates against its own field.
   7. **Complete minimal view.** The minimal view (section 4) contains every 
required path, so the root alone is enough to build a valid instance.
   8. **Unbroken migrations.** Migrators form an unbroken chain from version 1 
to the current `schema_version`.
   9. **Consistent metadata.** Behavior declarations and UI metadata are 
consistent, for example a container declares which child widgets it accepts.
   10. **Compatible contract.** The widget declares a widget contract version 
that the server supports.
   
   If a built-in widget fails, startup stops, which CI catches. If an extension 
widget fails, it isn't registered, the failure shows up in diagnostics, and 
existing instances of that widget show placeholders (section 11).
   
   Whether a matching frontend renderer exists can't be checked here, because 
the backend can't see frontend bundles. That check is part of the handshake 
(section 11).
   
   </details>
   
   <details>
   <summary><b>Appendix C: Validation error format</b></summary>
   
   Every validation error, whether returned for a rejected write, listed in a 
draft's validation state, or returned at commit, has this shape:
   
   ```json
   {
     "path": "/dataBinding/metrics/0",
     "code": "unknown_metric",
     "message": "Metric \"sum__sales\" doesn't exist on dataset \"orders\".",
     "expected": "a metric defined on dataset 42",
     "received": "sum__sales",
     "suggestions": ["sum__amount", "count"],
     "next_step": {"options_lookup": "/dataBinding/metrics"},
     "blocks": "write"
   }
   ```
   
   - **`path`:** a JSON Pointer to the offending value, usable directly for 
disclosure and patching.
   - **`code`:** a stable, documented identifier, for example `type_mismatch`, 
`unknown_key`, `invalid_enum_value`, `unknown_metric`, `missing_required`, 
`stale_reference`, `forbidden_datasource`, `test_failed`, `revision_conflict` 
or `limit_exceeded`.
   - **`message`:** plain language that names the field and the problem, 
without internal jargon.
   - **`expected` and `received`:** what the schema accepts, and what was sent.
   - **`suggestions`:**
     - **For enums and other bounded choices:** the valid values, nearest 
matches first.
     - **For unbounded references:** the closest matches, capped.
     - **Always permission-filtered:** suggestions never include datasets, 
columns, metrics or values the principal can't access.
   - **`next_step`:** where to go next. That is a path to expand, an options 
lookup to search, or another field to set first.
   - **`blocks`:** what the error prevents:
     - `write`: the write was rejected and the draft is unchanged;
     - `preview`;
     - `commit`.
   
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to