diff --git a/src/extension-api/index.ts b/src/extension-api/index.ts index 542724a4f6..115d97f14f 100644 --- a/src/extension-api/index.ts +++ b/src/extension-api/index.ts @@ -120,7 +120,7 @@ export type { WidgetOptions, WidgetValueChangeEvent, WidgetOptionChangeEvent, - WidgetPropertyChangeEvent, + // WidgetPropertyChangeEvent removed per A16 (D-widget-serialization-simplification, wave-9) WidgetBeforeSerializeEvent, WidgetBeforeQueueEvent, // Mount-lifecycle surface per D-widget-converge / Axiom A12 ──────────── diff --git a/src/extension-api/widget.ts b/src/extension-api/widget.ts index 18a11e4b27..bc659368e8 100644 --- a/src/extension-api/widget.ts +++ b/src/extension-api/widget.ts @@ -74,85 +74,66 @@ export interface WidgetOptionChangeEvent { readonly newValue: unknown } -/** - * Payload for `widget.on('propertyChange', handler)`. - * - * Fires when a first-class every-widget property is mutated — specifically - * `hidden`, `disabled`, and `serialize`. Does NOT fire for `value` changes - * (use `valueChange`) or for options-bag mutations (use `optionChange`). - * - * @stability experimental - * @example - * ```ts - * widget.on('propertyChange', (e) => { - * if (e.property === 'hidden') updateLayout(e.newValue as boolean) - * }) - * ``` - */ -export interface WidgetPropertyChangeEvent { - /** - * Which first-class property changed. - * - `'serialize'` — serialization opt-in/out via `setSerializeEnabled()` - * - * PHASE_A_EXCLUDED per AXIOMS.md A14: - * - `'hidden'` — visibility toggled via `setHidden()` (deferred) - * - `'disabled'` — enabled/disabled via `setDisabled()` (deferred) - */ - readonly property: 'serialize' // Phase A: 'hidden' | 'disabled' deferred - /** Value before the change. */ - readonly oldValue: boolean - /** Value after the change. */ - readonly newValue: boolean -} +// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): +// `WidgetPropertyChangeEvent` is vacuous after `'serialize'` was removed from +// the property union (A16: authors cannot disable serialization). The other +// historical members ('hidden', 'disabled') were already A14-deferred. +// Restoration requires a new first-class property to surface that satisfies +// A1–A16 and an axiom amendment. +// +// export interface WidgetPropertyChangeEvent { +// readonly property: 'serialize' +// readonly oldValue: boolean +// readonly newValue: boolean +// } /** * Payload for `widget.on('beforeSerialize', handler)`. * - * This is the **only async-allowed event** in the API. - * Replaces `widget.serializeValue`, `widget.options.serialize = false`, and - * the v1 `widget.serializeValue = (workflowNode, widgetIndex) => ...` pattern. + * This is the **only async-allowed event** in the API and, per AXIOMS.md + * §A16 (Unified Serialization Target), the **sole** extension-author + * interface to serialization. Replaces every v1 serialization hook: + * `widget.serializeValue = fn`, `widget.options.serialize = false`, + * `nodeType.prototype.serialize`. * - * Call `event.setSerializedValue(v)` to override what is written to - * `widgets_values[i]` and the API prompt. Call `event.skip()` to exclude this - * widget from the prompt entirely. Do not call either to pass through the - * widget's current `getValue()` unchanged. + * The hook fires **once per serialization**. The framework writes the + * resulting payload to every transport (workflow JSON `widgets_values[i]`, + * API prompt, clone target, subgraph promotion). Extensions do not see and + * cannot branch on the transport — that is a framework concern (A16). + * + * Call `event.setSerializedValue(v)` to override what is written. Do not + * call it to pass through the widget's current `getValue()` unchanged. + * + * Per A16 there is no `skip()` and no `context` discriminator. Per A15 + * (Widget Declarativity) there is no way to exclude a widget from + * serialization — if a widget should not contribute to the payload, it + * should not be a widget (use a boxed widget, a non-widget UI primitive, + * or a schema input). * * @typeParam T - The widget's value type. * @example * ```ts * // Dynamic prompts: replace value at serialize time * widget.on('beforeSerialize', (e) => { - * if (e.context === 'prompt') { - * e.setSerializedValue(processDynamicPrompt(widget.getValue())) - * } + * e.setSerializedValue(processDynamicPrompt(widget.getValue())) * }) * - * // Preview widget: exclude from prompt - * widget.on('beforeSerialize', (e) => { - * if (e.context === 'prompt') e.skip() - * }) - * - * // Async: webcam capture — materialize frame before prompt builds + * // Async: webcam capture — materialize frame before serialization * widget.on('beforeSerialize', async (e) => { - * if (e.context === 'prompt') { - * const frame = await captureFrame() - * e.setSerializedValue(frame) - * } + * const frame = await captureFrame() + * e.setSerializedValue(frame) * }) * ``` */ export interface WidgetBeforeSerializeEvent { - /** - * Which serialization path triggered this handler. - * - * - `'workflow'` — user is saving the workflow to disk (full round-trip). - * - `'prompt'` — user is queueing a run (only prompt-relevant data sent to backend). - * - `'clone'` — a copy/paste is happening; the framework already populated the - * cloned entity's widget value from the source. Override only if the clone should - * differ from the source. - * - `'subgraph-promote'` — the widget is being promoted to a subgraph IO slot. - */ - readonly context: 'workflow' | 'prompt' | 'clone' | 'subgraph-promote' + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // The 4-way transport discriminator inverted the direction of knowledge + // flow — framework owns transport, extensions own value. Workflow JSON + // and API prompt converge to a single serialized payload; clone and + // subgraph-promote are framework concerns. Restoration requires an + // axiom amendment to A16. + // + // readonly context: 'workflow' | 'prompt' | 'clone' | 'subgraph-promote' /** * The widget's current value at the time of serialization (before any override). @@ -161,20 +142,21 @@ export interface WidgetBeforeSerializeEvent { readonly value: T /** - * Override the serialized value. The provided value is written to - * `widgets_values[i]` (and to the API prompt for `context='prompt'`). - * Calling this multiple times keeps the last call's value. + * Override the serialized value. The provided value is written to every + * transport (workflow JSON `widgets_values[i]`, API prompt, clone target, + * subgraph promotion). Calling this multiple times keeps the last call's + * value. * * @param v - The value to serialize. Must be JSON-serializable. */ setSerializedValue(v: unknown): void - /** - * Exclude this widget from the API prompt entirely. - * Only meaningful for `context='prompt'`; no-ops on other contexts. - * Replaces `widget.options.serialize = false` and `() => undefined` patterns. - */ - skip(): void + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // `skip()` IS a per-call disable — authors cannot disable serialization + // (A16). If a widget should not contribute to the payload, it should not + // be a widget (A15). Restoration requires axiom amendments to A15 + A16. + // + // skip(): void } /** @@ -349,23 +331,17 @@ export interface WidgetHandle { */ setHeight(px: number): void - // ── SERIALIZATION OPT-OUT — first-class, every-widget ──────────────────── - - /** - * Returns `true` if this widget is included in workflow and prompt - * serialization. Defaults to `true` for all widget types. - * - */ - isSerializeEnabled(): boolean - - /** - * Enable or disable serialization for this widget. When disabled, the widget - * is excluded from both `widgets_values` in the workflow JSON and the API - * prompt payload. Equivalent to the v1 `widget.options.serialize = false` - * pattern. - * - */ - setSerializeEnabled(enabled: boolean): void + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // Authors cannot disable serialization at the widget level (A16). If a + // widget should not contribute to the serialized payload, it should not + // be a widget (A15) — use a boxed/composed widget (BBOX-style), a + // non-widget UI primitive, or a schema input. The sole serialization + // interface is `widget.on('beforeSerialize', handler)`. Restoration + // requires axiom amendments to A15 + A16 + a validated ecosystem use + // case that no boxed/composed pattern can serve. + // + // isSerializeEnabled(): boolean + // setSerializeEnabled(enabled: boolean): void // ── OPTIONS BAG — type-specific overrides ───────────────────────────────── @@ -379,25 +355,23 @@ export interface WidgetHandle { * {@link WidgetHandle.setOption} per-key. * * Note: this is an accessor pair on the v2 surface. Reading is free; the - * setter intentionally does not exist on the public type. v1 patterns like - * `widget.options.serialize = false` should migrate to - * {@link WidgetHandle.setSerializeEnabled}; `widget.options.values = [...]` - * (combo refresh) migrates to a future `setValues` mutator (tracked - * under W6.P8.UNMIGRATABLE). + * setter intentionally does not exist on the public type. Per AXIOMS.md + * §A16, `serialize` is no longer a writable option (and no longer a key + * on this bag) — there is no widget-level serialization disable. + * `widget.options.values = [...]` (combo refresh) migrates to a future + * `setValues` mutator (tracked under W6.P8.UNMIGRATABLE). * * @example * ```ts * // ❌ TS-ERR — every option write raises a compile-time error * widget.options.min = 0 * widget.options = { min: 0, max: 100 } - * widget.options.serialize = false * * // ✅ Read freely * const min = widget.options.min ?? 0 * * // ✅ Mutate via typed setters * widget.setOption('min', 0) - * widget.setSerializeEnabled(false) * ``` */ readonly options: Readonly @@ -456,9 +430,11 @@ export interface WidgetHandle { * // ❌ TS-ERR — direct assignment no longer compiles * widget.serializeValue = () => 'static value' * - * // ✅ Subscribe to the typed event (D5) + * // ✅ Subscribe to the typed event (D5). Per A16 the hook fires once + * // and the framework writes the resulting payload to every transport; + * // there is no transport discriminator. * widget.on('beforeSerialize', (e) => { - * if (e.context === 'prompt') e.setSerializedValue('static value') + * e.setSerializedValue('static value') * }) * ``` * @@ -495,28 +471,28 @@ export interface WidgetHandle { handler: Handler ): Unsubscribe - /** - * Subscribe to first-class property mutations (`setSerializeEnabled`). - * - * PHASE_A_EXCLUDED per AXIOMS.md A14: `setHidden`, `setDisabled` deferred - * pending serialization convergence. - * - * Does NOT fire for `setValue` (use `valueChange`) or options-bag mutations - * (use `optionChange`). - * - * @returns A cleanup function to remove the listener. - * @stability experimental - */ - on( - event: 'propertyChange', - handler: Handler - ): Unsubscribe + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // `WidgetPropertyChangeEvent` is vacuous — the only property the event + // ever surfaced was `'serialize'`, which is gone per A16. `setHidden` / + // `setDisabled` were already A14-deferred. Restoration requires a new + // first-class property to surface that satisfies A1–A16. + // + // on( + // event: 'propertyChange', + // handler: Handler + // ): Unsubscribe /** * Subscribe to widget serialization. The only async-allowed event. * - * Replaces `widget.serializeValue = fn` and the v1 `widget.options.serialize` - * flag. The handler may be sync or async; async handlers are awaited before + * Per AXIOMS.md §A16 this is the **sole** extension-author interface + * to serialization. The hook fires once per serialization and the + * framework writes the resulting payload to every transport (workflow + * JSON `widgets_values`, API prompt, clone target, subgraph promotion). + * Replaces the v1 `widget.serializeValue = fn` / + * `widget.options.serialize` patterns. + * + * The handler may be sync or async; async handlers are awaited before * the serialization payload is sent. * * @returns A cleanup function to remove the listener. @@ -630,8 +606,12 @@ export interface WidgetOptions { hidden?: boolean /** If `true`, the widget is rendered read-only (no user editing). */ readonly?: boolean - /** If `false`, this widget is excluded from workflow/prompt serialization. */ - serialize?: boolean + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // `serialize` contradicted A16 even as a read-only key — there is no + // widget-level serialization disable. Already write-blocked by + // D-immutability-enforcement; now removed from the type entirely. + // + // serialize?: boolean /** Display label override. Defaults to the widget `name`. */ label?: string /** Toggle label shown when value is `true` (BOOLEAN widgets). */ diff --git a/src/services/extension-api-service.ts b/src/services/extension-api-service.ts index 6441a43cfb..a4bc019f6b 100644 --- a/src/services/extension-api-service.ts +++ b/src/services/extension-api-service.ts @@ -42,7 +42,8 @@ import { WidgetComponentContainer, WidgetComponentDisplay, WidgetComponentSchema, - WidgetComponentSerialize, + // WidgetComponentSerialize import removed per A16 + // (D-widget-serialization-simplification, wave-9) — adapter blocks gone. WidgetComponentValue } from '@/world/widgets/widgetComponents' import type { NodeEntityId, WidgetEntityId } from '@/world/entityIds' @@ -299,20 +300,12 @@ function createWidgetHandle(widgetId: WidgetEntityId): WidgetHandle { }) }, - isSerializeEnabled() { - return ( - world.getComponent(widgetId, WidgetComponentSerialize)?.serialize ?? - true - ) - }, - setSerializeEnabled(enabled: boolean) { - dispatch({ - type: 'SetWidgetOption', - widgetId, - key: 'serialize', - value: enabled - }) - }, + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // Authors cannot disable serialization at the widget level (A16). + // Restoration requires axiom amendments to A15 + A16. + // + // isSerializeEnabled(): boolean { ... } + // setSerializeEnabled(enabled: boolean): void { ... } // D-immutability-enforcement (Hybrid C): read-only snapshot of options // bag. Public type is Readonly — TS-ERR on any assignment. @@ -359,7 +352,10 @@ function createWidgetHandle(widgetId: WidgetEntityId): WidgetHandle { () => world.getComponent(widgetId, WidgetComponentValue)?.value, (newValue, oldValue) => fn({ newValue, oldValue }) ) - } else if (event === 'optionChange' || event === 'propertyChange') { + } else if (event === 'optionChange') { + // PHASE_A_EXCLUDED per AXIOMS.md A14 + A16 (D-widget-serialization-simplification, wave-9): + // `propertyChange` event removed alongside `setSerializeEnabled` + // (vacuous union after `'serialize'` was the sole member). // TODO(#11939): wire through ECS event bus when available if (import.meta.env.DEV) { console.warn(