Per D9 strangler-bridge taxonomy, every direct globalThis.app access in
v2 conversion files must carry a 'strangler-bridge:Phase-B' marker so
the bridge audit can enumerate remaining touch-points before Phase-B
removes the global.
Tag the three sites in rerouteNode.v2.ts (configuringGraph guard in
onConnectionsChange, configuringGraph guard in updateLink, and the
canvas.setDirty call from the context-menu callback).
Known-stubbed channels (executed/connected/disconnected/configured)
dispatch to a Phase-A stub bus that does not deliver events. Previously
these registrations were silent — extensions saw no warning that their
handlers were dead until #11939 lands the dispatch substrate.
Add a DEV console.warn (eslint-disable-next-line no-console, matching
the established pattern in this file) for each known-stubbed channel
registration so authors know to wait on #11939.
Restratified i-ext. Adds v2 conversions for 6 core extensions:
- dynamicPrompts.v2.ts
- imageCrop.v2.ts
- previewAny.v2.ts
- noteNode.v2.ts
- rerouteNode.v2.ts
- slotDefaults.v2.ts
And registers the first 3 in src/extensions/core/index.ts.
Note: noteNode.v2, rerouteNode.v2, slotDefaults.v2 are NOT yet
registered in index.ts (pre-existing issue from original i-ext branch).
Filed as a follow-up TODO.
Original (pre-restratify) branch tip backed up at
refs/backup/restratify-20260511/ext-api-i-ext.
Hand-patched mirror of foundation hygiene cleanup per AGENTS.md Rule 8
(Node 20 vs vite blocks `pnpm build` in this worktree):
- Strip HR-style section dividers
- Strip parenthetical decision archaeology
((per D5), (D-immutability-enforcement, Hybrid C),
D-bootstrap-hooks (W6.P6.C), D-shell-ui-entrypoints (W6.P5.C), etc.)
from per-export JSDoc — preserve AXIOMS.md §A1/A14/A15/A16/A12 refs
and PHASE_A_EXCLUDED axiom blocks (functional content).
- Strip "W6.P8.UNMIGRATABLE / D-input-output-shape" todo refs.
- One stale `defineWidgetExtension` JSDoc example fixed (was already
on foundation; rebase carried the fix through).
Mirrors foundation commit `ee0537fdb5`.
ADR: decisions/D-design-review-hygiene-cleanup.md
Zero published-surface change (no types added/removed/renamed).
Hand-patched packages/extension-api/build/index.d.ts to match the
foundation surface after A16 closure:
- Drop `isSerializeEnabled` / `setSerializeEnabled` from WidgetHandle
- Drop `WidgetBeforeSerializeEvent.context` 4-way discriminator
- Drop `WidgetBeforeSerializeEvent.skip()`
- Drop `on('propertyChange', handler)` overload
- Drop entire `WidgetPropertyChangeEvent` interface
- Drop `WidgetOptions.serialize?: boolean` key
- Update JSDoc on `WidgetBeforeSerializeEvent` / `options` / `serializeValue`
to reference A16 / drop transport-discriminator examples
`NodeBeforeSerializeEvent.context` is intentionally retained — the
node-level surface stays `@deprecated` + runtime-warned per ADR-0010
(not banned by D-widget-serialization-simplification).
Hand-edit pattern because pnpm build is blocked by Node 20 vs vite
requirement per AGENTS.md Rule 8. Surface verified to match foundation
HEAD e56187adf3.
Refs: decisions/D-widget-serialization-simplification.md, AXIOMS.md §A15+A16
Picks up the canvas-units / escape-hatch / A13 JSDoc on NodeHandle.{getPosition,setPosition,getSize,setSize} from foundation fa3229b402 so the published .d.ts and the docgen pipeline see the policy text. No symbol changes.
Cascade of D20 (decisions/D20-id-type-convergence.md) from foundation:
- Rebuild packages/extension-api/build/index.d.ts via vite build so the
published .d.ts ships the new id/equals surface and drops the three
demoted brand re-exports.
- src/types/extensionV2.ts: stop re-exporting NodeEntityId, WidgetEntityId,
SlotEntityId — they are no longer in the @/extension-api barrel per D20.
Use node.id/widget.id (string) and equals(other) for the public surface.
The packages/extension-api/build/ directory contains TypeDoc/tsc-generated
.d.ts snapshots committed for reviewability (per D17). These should not
be format-checked since they're machine-generated.
Adds packages/extension-api/build/** to oxfmt ignorePatterns so that
'pnpm format:check' passes cleanly post-restratify reconciliation.
Part of RECONCILE-PKG (PR #12143).
Remove index.js and index.js.map from git tracking. The .d.ts file is
committed for npm package visibility and reviewer inspection, while JS
files are generated at build time and don't need to be in version control.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove build/ from .gitignore so types are visible in PR
- Update vite config to use rollupTypes for bundled declarations
- Update package.json types path to ./build/index.d.ts
- Exclude packages/*/build/ from lint-staged
- Include index.d.ts (37KB bundled types), index.js (2MB), index.js.map
The bundled index.d.ts contains all public API types inline.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Resolves CodeRabbit findings F-12143-1, F-12143-3, F-12143-7.
Per D17-pkg2-build-strategy ADR: package now builds via Vite library
mode + vite-plugin-dts, dropping the inconsistent rootDir restriction
and the --emitDeclarationOnly flag. Vite resolves @/* aliases against
the canonical surface in main app src/extension-api/, externalizes vue
as a peer dep, and bundles to a single 3.95 kB build/index.js.
vite-plugin-dts emits the per-file .d.ts closure under build/ with
@/* imports rewritten to relative paths.
The original tsc-only plan in #12143 was unworkable because tsc does
not rewrite path aliases — emitted JS would have unresolvable @/...
imports at runtime. Vite library mode is the smallest deviation that
satisfies PKG3 acceptance ('emits BOTH .d.ts + .js') while preserving
'the barrel is the source of truth in main app src/extension-api/'
intent in packages/extension-api/AGENTS.md.
Verified:
pnpm build emits build/index.js (3.95 kB) + build/.../*.d.ts tree
node -e "import('./build/index.js')" returns real functions for
defineNodeExtension, defineExtension, onNodeMounted, onNodeRemoved
synthetic downstream consumer importing NodeHandle/WidgetHandle
types and runtime fns type-checks under tsc --noEmit
pnpm lint && pnpm format:check && pnpm knip pass
Also adds packages/extension-api/vite.config.mts to the eslint
allowDefaultProject list so the typed lint rules can resolve it.
See decisions/D17-pkg2-build-strategy.md in the cross-repo workspace
for the full options analysis (A: types-only — rejected; B: vendor
sources — deferred to post-#11939; C: build from main app sources —
chosen).
The dry_run input was declared as type: boolean but the default was the
string 'true' and the conditional comparisons used '== "true"' / '== "false"'.
GitHub Actions coerces typed boolean inputs to actual booleans, so the
string comparisons would never match a workflow_dispatch invocation.
- default: 'true' → default: true (match declared type)
- inputs.dry_run == 'false' → !inputs.dry_run
- inputs.dry_run == 'true' → inputs.dry_run
Validated locally with actionlint (clean).
Refs CodeRabbit findings F-12143-8, F-12143-9, F-12143-10 in
research/reviews/coderabbit-batch-2026-05-12.md.
oxlint flags console.log in packages/extension-api/scripts/build-docs.ts
(intentional CLI progress output). The pre-commit lint-staged hook in
ci-lint-format then aborts before the [automated] format commit can be
pushed back, blocking the entire lint-and-format job.
- Add file-level eslint-disable for no-console (CLI build script)
- Whitelist build-docs.ts in eslint.config.ts allowDefaultProject so
typed-lint does not error on the file outside the tsconfig project graph
Discovered while rebasing #12143 onto the foundation-v2 knip fix
(5b05f2b793).
The package barrel at packages/extension-api/src/index.ts duplicated the
canonical surface in src/extension-api/index.ts. Two hand-maintained
exports lists drift out of sync — Architect #4's removal of
startExtensionSystem from the public surface, for example, only landed in
the canonical barrel.
Collapse the package barrel to a single 'export * from
@/extension-api/index' re-export so the canonical file is the one source of
truth. Future surface additions/removals flow through automatically; no
risk of asymmetric export sets between the in-tree consumer
(@/extension-api) and the npm package consumer
(@comfyorg/extension-api).
Restratified i-pkg. Adds the @comfyorg/extension-api npm package
(packages/extension-api/) — package.json, tsconfig.{json,build,docs},
typedoc.json, scripts/build-docs.ts, README, .npmignore — plus three
CI workflows (extension-api-typecheck.yml, extension-api-publish.yml,
ci-tests-extension-api.yaml) and the docgen helper script.
Modifies foundation's src/extension-api/index.ts and
src/services/extension-api-service.ts with the package-aware tweaks
required for npm consumption.
Lockfile regenerated to include packages/extension-api workspace deps.
Original (pre-restratify) branch tip backed up at
refs/backup/restratify-20260511/ext-api-i-pkg.
typedoc renders re-exported type pages from the original-declaration
site (src/types/extensionTypes.ts), not from the @comfyorg/extension-api
barrel re-export. The shell-UI type aliases (ToastMessageOptions,
HotkeyExtension, AboutBadgeExtension, ToolbarButtonExtension) therefore
shipped to the dashboard without any @example blocks even though their
matching defineX entry points carry full examples in registrations.ts /
imperatives.ts.
Lift the canonical @example from each defineX onto the source
declaration so the dashboard type page renders with a working snippet.
Pure JSDoc — zero exported-type diff. Phase A freeze respected.
Wave-18 W18.J. Closes W17.N.2.
- W17.C: refresh 5 stale @example blocks (types.ts, lifecycle.ts, node.ts,
imperatives.ts ×2) — replace deprecated `async setup()` with
`setup() { onMounted(...) }`; remove A1-removed `node.getWidget` from
defineNode + NodeHandle.on migration examples; align imperatives examples
with W17.B notify-toast verdict.
- W17.D: author ~37 new @example blocks across 9 files for sparse exports
surfaced by W17.A audit. Pure JSDoc — zero exported-type diff.
- W17.E: add @deprecated JSDoc to notify() + NotifyOptions per
D-notify-toast-consolidation (W17.B verdict (a) soft-deprecate).
Pure JSDoc — no signature, type, or export changes.
Phase A surface remains FROZEN at ee0537fdb5 (foundation surface SHA
unchanged; only @example / @deprecated text differs).
See research/dashboard-snippet-audit-2026-05-21.md (W17.A) for full
per-edit rationale.
Address PR #12142 review thread PRRT_kwDOMIrOxs6BQjVr —
remove '(I-SR.3 / MIG1.E5)' internal task-tracker reference from the
startExtensionSystem() comment. Wave-11 hygiene cleanup did the same
across src/extension-api/**; this completes the same hygiene for the
single call site in src/scripts/app.ts.
Review: https://github.com/Comfy-Org/ComfyUI_frontend/pull/12142#discussion_r3222947433
Wave-10 follow-up to D-widget-serialization-simplification (wave-9). Closes
the v1 enforcement loop on AXIOMS.md A15 (Widget Declarativity).
User direction 2026-05-20: "we dont need soft deprecate for beforeSerialize
OR addWidget — they just simply shouldnt be present in the new API. for the
old api surfaces, we can just add deprecations with the suggested
remediation."
## v2 surface — absent (no soft-deprecate)
- Delete dead `addWidget` + `addDOMWidget` shims in
`src/services/extension-api-service.ts` (~30 lines), along with the
`domWidgetElements` side-table and `getDOMWidgetElement` accessor.
With the public NodeHandle no longer exposing these methods, all of
this is unreachable.
- Drop `DOMWidgetOptions` import from the service.
- Scrub 5 v2 JSDoc `@example` blocks that still referenced
`node.addWidget(...)` as if it were a valid call:
node.ts (NodeBeforeSerialize migration example), widget.ts (3 spots:
name doc, label doc, WidgetOptions doc), types.ts (defineWidget doc),
lifecycle.ts (defineNode example).
- Replace each example with one of A15's three remediation paths
(boxed widget / non-widget UI primitive / schema input).
## v1 surface — @deprecated + once-per-session console.warn
- Add @deprecated JSDoc + `warnDeprecated` (session-deduped via the
existing `./utils/feedback` mechanism) on:
- `LGraphNode.addWidget` (LGraphNode.ts)
- `LGraphNode.addCustomWidget` (same file)
- `LGraphNode.prototype.addDOMWidget` (src/scripts/domWidget.ts)
- Shared message constant `ADD_WIDGET_DEPRECATION_MESSAGE` so all three
warn paths dedupe to one console line per session. Message text cites
AXIOMS.md A15 + decisions/D-ban-runtime-addwidget.md and enumerates
the three migration paths.
## First-party core call sites — silenced via sentinel
- New `__suppressDeprecationWarning?: boolean` field on
`IWidgetOptions` (marked @internal, NOT part of public surface).
- Added to all first-party call sites that haven't yet migrated:
src/scripts/widgets.ts (valueControl, comboFilter),
src/extensions/core/uploadAudio.ts (4 sites),
src/extensions/core/load3d.ts (3 sites),
src/extensions/core/webcamCapture.ts (2 sites),
src/extensions/core/customWidgets.ts (1 site).
- `src/scripts/domWidget.ts addWidget()` helper (internal renderer
registration path) auto-injects the sentinel before delegating to
`addCustomWidget` — third-party callers of LGraphNode.addCustomWidget
still see the warning.
- Migration of these first-party sites tracked as wave-9 follow-up
#2 (preview/progress/audio widgets migration audit).
## Axiom-Excluded Test Annotation Policy — new, codified
- AXIOMS.md: new "Axiom-Excluded Test Annotation Policy" section after
A14, retiring the "compat-floor blast_radius ≥ N MUST pass" doctrine.
- AGENTS.md Rule 11: tests asserting on axiom-excluded surfaces MUST
use `axiomExcluded({...})` (vitest test.fails + task.meta.annotations)
rather than deletion. Deletion only when the *scenario* is no longer
meaningful.
- decisions/D-ban-runtime-addwidget.md: "Testing policy" section
documenting concrete application to bc-05 / bc-11 BC tests in tf
branch (deferred to tf cascade step).
## Decision propagation
- (a) commit on PR branches — this commit (foundation) + cascade
- (b) ADR — `decisions/D-ban-runtime-addwidget.md` ACCEPTED 2026-05-20
- (c) dashboard MDX — refresh via scripts/refresh-api-docs.sh after
cascade
## Validation
- scripts/check-axiom-compliance.sh: all 7 checks pass (no new strict
patterns added; A15 already strict-fails on v2 re-introduction per
wave-9, and the v1 deprecation is enforced at the call-site level
via the warnDeprecated mechanism)
- oxlint: 0 warnings/errors on all 13 touched files
- oxfmt: applied to all 13 touched files
- tsc: no new errors introduced (pre-existing TS2305/TS2353/TS2307 errors
from prior waves remain under AGENTS.md Rule 8 carve-out)
- Bundle size shrinks by ~30 lines (dead v2 shims removed)
## Compatibility (D6 parallel-paths)
- v1 path remains fully functional. Deprecation is a soft signal — no
throw, no exception, no behavior change beyond one console.warn per
session.
- Existing custom-node packs (R7 sweep: 5+ evidenced) continue to work;
authors see migration text on first addWidget call.
Refs: AXIOMS.md A15, A16; D-widget-serialization-simplification (wave-9);
ADR-0010 (node-level beforeSerialize deprecation pattern); D6
parallel-paths.
Codify A15 (Widget Declarativity) + A16 (Unified Serialization Target) by
collapsing widget-level serialization to its irreducible core.
Per D-widget-serialization-simplification (wave-9, 2026-05-20) — comment out
in src/extension-api/widget.ts (A14 pattern):
- WidgetHandle.setSerializeEnabled(enabled) / .isSerializeEnabled()
- WidgetBeforeSerializeEvent.context: 'workflow'|'prompt'|'clone'|'subgraph-promote'
- WidgetBeforeSerializeEvent.skip()
- WidgetPropertyChangeEvent (vacuous union after 'serialize' removed)
- WidgetHandle.on('propertyChange', handler) overload
- WidgetOptions.serialize?: boolean key
Per A16, the sole extension-author interface to serialization is
`widget.on('beforeSerialize', handler)`. The framework writes one payload
to every transport (workflow JSON, API prompt, clone, subgraph-promote);
authors do not branch on transport and cannot disable serialization. Per
A15, runtime widget addition was the underlying reason for the disable
matrix — declarativity removes the cause, A16 removes the escape hatch.
Removed framework-side adapters in src/services/extension-api-service.ts:
isSerializeEnabled / setSerializeEnabled blocks (lines 302-315) and the
propertyChange branch of the event dispatcher (line 362). Dropped
WidgetComponentSerialize import (now unused).
Removed WidgetPropertyChangeEvent from src/extension-api/index.ts barrel.
Updated JSDoc: stripped 4-context narrative + skip() examples from
WidgetBeforeSerializeEvent, dropped widget.options.serialize references
from the options accessor, dropped e.context === 'prompt' from the
serializeValue accessor example.
node.on('beforeSerialize') (separate surface) remains `@deprecated` +
runtime-warned per ADR-0010 — NOT banned by this commit (per user
direction 2026-05-20).
Blast radius: 0 v2 ext call sites for any removed surface (verified via
rg sweep across restack-ext-v2/src and foundation extensions/core).
v1 path untouched per D6 parallel-paths migration.
Refs:
- decisions/D-widget-serialization-simplification.md (ACCEPTED 2026-05-20)
- AXIOMS.md §A15 (Widget Declarativity), §A16 (Unified Serialization Target)
- AXIOMS.md §A14 (7 new rows in COMMENTED OUT table)
- D14-serialization-convergence.md (predecessor — promised this end-state)
- D-no-node-widget-access.md (wave-8, structural sibling)
- design-review-12142.md row #11
Verified: scripts/check-axiom-compliance.sh ✅ all checks pass
(A1, A2, A3, A4, A6, A15, A16 — strict-fail on re-introduction)
Comments out NodeHandle.getWidget(name) + NodeHandle.getWidgets() per
D-no-node-widget-access (ACCEPTED 2026-05-19, ADR landed). Closes the
node→widget direction so that the v2 public surface enforces A1 as
written: 'nodes cannot enumerate widgets' is now bilateral with
'nodes cannot reference widgets' — the widget→node direction
(widget.parentNode) remains the sole node↔widget channel.
Origin: a contradiction was surfaced during handoff-14 (wave-7
post-cascade). AXIOMS.md §A1 is NON-NEGOTIABLE and says 'Node hooks
cannot reference/modify widgets' + 'Widgets can know their parent
node, but nodes cannot enumerate widgets'. However design-review-12142
decision #13 (2026-05-12, 'getWidget seems good') had renamed
widget() → getWidget() without re-checking A1, and a codified
read-only carve-out in scripts/check-axiom-compliance.sh:22 papered
over the gap. The carve-out is removed; the axiom checker is now
strict-fail on any node→widget surface.
Changes:
- node.ts: getWidget(name) + getWidgets() commented out with A1+A2
rationale block (A2 pattern: 'comment out if not 100% sure')
- widget.ts: 2 JSDoc examples rewritten to use
defineWidget({mount}, ctx.widget) — the only legal v2 path to a
WidgetHandle
Migration cost (v2 surface):
- 0 call sites in the tf test suite
- 1 call site in ext-v2 (extensions/core/dynamicPrompts.v2.ts) —
stubbed pending follow-up 'defineWidgetAugmenter' API; v1 path
(Comfy.DynamicPrompts) continues to work
- TypeScript-only signature change; no runtime change
Restoration criteria: see AXIOMS.md §A14 entry — a documented use
case unexpressible via widget.parentNode + defineWidget({mount}) +
provide/inject, A1 re-audit pass, and bc-XX test triplet coverage
across promotion / app-mode / linked-instance scenarios.
Refs:
- ADR: decisions/D-no-node-widget-access.md (ACCEPTED 2026-05-19)
- AXIOMS.md §A1 + §A14
- scripts/check-axiom-compliance.sh (strict bilateral check)
Adopt per-surface defineX entries plus inline-imperative carve-out for
toast + notify, per D-shell-ui-entrypoints ADR (ACCEPTED 2026-05-18,
foundation impl handoff-13).
## What lands
New shell-UI registration entries (registrations.ts, NEW):
- defineSidebarTab / defineBottomPanelTab — wire to workspace stores
- defineCommand — wires to commandStore.registerCommand
- defineHotkey — parses key combo, wires to keybindingStore
- defineSetting — wires to settingStore.addSetting
- defineAboutBadge / defineToolbarButton — module-level registries
(consumed by aboutPanelStore / action-bar component via P5.E follow-up)
All defineX return DisposableHandle { dispose(): void }. Lazy-mount
queue (pendingEntries[]) flushed by startExtensionSystem() via the new
_flushShellRegistrations() hook so defineX is safe to call at
module-eval time (before Pinia is ready). Async store imports inside
each mount-fn keep the registration module itself dependency-free.
Inline imperatives (imperatives.ts, NEW):
- toast.show / toast.remove / toast.removeAll
- notify({ kind, message, detail, life }) — thin convenience wrapper
- Fire-and-forget; no DisposableHandle, no defineX wrapper
- 100% imperative across 166 ecosystem hits / 16 repos (R1+R2+R3)
Net-new public arg types (extensionTypes.ts):
- CommandDefinition (alias of v1 ComfyCommand)
- HotkeyExtension (keys + commandId + optional targetElementId)
- AboutBadgeExtension (label/url/icon/severity)
- SettingDefinition<TValue> (alias of SettingParams<TValue>)
- ToolbarButtonExtension (id/icon/label/tooltip/class/onClick)
Barrel reshape (shell.ts + index.ts):
- Re-export the 5 new arg types from shell.ts
- DROP ExtensionManager + CommandManager from public re-export per
W6.P5.B reconciliation — v2 uses per-surface disposables, not
umbrella handles. Internal callers still import from
@/types/extensionTypes directly.
- Export 7 new defineX + DisposableHandle + toast/notify/NotifyOptions
from index.ts
v1 deprecation (comfy.ts):
- ComfyExtension.{commands, keybindings, settings, bottomPanelTabs,
aboutPageBadges, actionBarButtons} gain @deprecated JSDoc pointing at
the v2 defineX equivalent. Slots remain functional for back-compat
during the deprecation window (v1 extensionService still loads them).
- menuCommands + topbarBadges marked @deprecated with scope-note that
they're not part of W6.P5 (follow-on ADRs).
Runtime wiring (extension-api-service.ts):
- startExtensionSystem() dynamic-imports registrations and calls
_flushShellRegistrations() so pending defineX calls land into stores
exactly once at bootstrap.
Verification:
- tsc --noEmit: zero new errors in modified files (pre-existing node.ts
/ widget.ts errors are uncommitted working-tree state from prior
agent, not from this work)
- oxfmt: clean
- oxlint: 0 warnings / 0 errors on touched files
- vitest/knip blocked by Node 20-vs-24 env requirement (env, not code)
ADR: decisions/D-shell-ui-entrypoints.md §Implementation plan Steps 1-5.
Steps 6 (codemod) lands in PKG; Step 7 (.d.ts regen) cascades via
restack-after-foundation.sh; Steps 8-13 land in EXT/TF/docs/dashboard.
Per AGENTS.md §Decision propagation: subject contains
'D-shell-ui-entrypoints' so check-decision-propagation.sh picks it up.
Per D-coord-space ACCEPTED 2026-05-18 (option iii, Christian PICK) + new Axiom A13 (Single Coordinate Space — Canvas) in workspace AXIOMS.md.
Expands JSDoc on NodeHandle.{getPosition,setPosition,getSize,setSize} to: (1) explicitly state 'canvas units', (2) name client/CSS spaces in prose so the distinction is visible at the API boundary, (3) document the escape-hatch (window.app.canvas.ds.{scale,offset} + window.app.canvas.canvas.getBoundingClientRect() + window.devicePixelRatio) for legitimate screen-space cases, (4) point AI agents and human authors at A13 + ADR D-coord-space, (5) carry @stability stable. Also adds a SPATIAL STATE section header comment block stating the single-space policy once at the top.
No runtime surface change — getPosition/getSize/setPosition/setSize already returned/accepted canvas units post W6.P8.C (df921f3512). The PICK ships as documentation + axiom because the runtime contract was already in the right shape. R1+R2+R3 evidence (8,424 EXT hits / 103 repos / ★41,621; bug-class 2,617 hits / 71 repos / ★40,040) backs the choice.
Phase A gates: format clean / lint 0 errors 3 pre-existing warnings / knip 6 pre-existing tag hints 0 failures.
Per D-widget-converge ACCEPTED 2026-05-18 (W6.P3.B PICK: option iii) and
new Axiom A12 (Mount-Lifecycle as the Sole DOM Seam):
- Add WidgetCleanup, WidgetMountContext, WidgetMountFn to widget.ts. The
mount context provides widget/node handles plus onUnmount /
onBeforeRemount / onAfterRemount lifecycle hooks. Cleanup fires on
destruction only; host remount uses the remount hooks.
- Replace WidgetExtensionOptions.created({render, destroy}) with optional
mount: WidgetMountFn. Authors capture host element via closure inside
mount() — there is no widget.element accessor on the handle.
- Delete DOMWidgetOptions interface from node.ts.
- Delete NodeHandle.addDOMWidget() method (replaced by defineWidget + the
same addWidget call as native widgets).
- Update WidgetHandle.setHeight() JSDoc — applies to widgets registered
with a mount() body, not "DOM widgets" as a category.
- Drop DOMWidgetOptions from public barrel; add WidgetCleanup,
WidgetMountContext, WidgetMountFn to the barrel.
Phase A gates (lint + format:check + knip) pass. typecheck/test on v2
stack continue to fail per AGENTS.md Rule #8 (parallel-paths stack).
Migration path: v1 patterns (addDOMWidget, widget.element, widget.inputEl,
addWidget('unregistered-type', …)) keep working through the v1 surface
during the D6 parallel-paths window and break deliberately at D6 Phase D
sunset. See decisions/D-widget-converge.md §Migration-guide rows.
Implements the W6.P8.C foundation slice of D-immutability-enforcement
(ACCEPTED 2026-05-14, Hybrid C):
* Point/Size are now readonly tuples — getPosition()[0] = X and
getSize()[0] = X raise TS-ERR at compile time.
* getWidgets() now returns ReadonlyArray<Readonly<WidgetHandle>>
— node.getWidgets().push(...) and []= raise TS-ERR.
* inputs()/outputs() are renamed to getInputs()/getOutputs() returning
ReadonlyArray<Readonly<SlotInfo>>. Old names kept as @deprecated
aliases for one minor release; SlotInfo fields were already readonly.
* New WidgetHandle.options: Readonly<WidgetOptions> accessor — bulk read
of the options bag; widget.options.min = 0 and widget.options = {...}
raise TS-ERR. Mutate via setOption(key, value).
* New WidgetHandle.serializeValue accessor-only — direct assignment
raises TS-ERR; the v2 path is on('beforeSerialize') per D5.
* widget.value (setValue/getValue pair) is unchanged — already ZERO row
in R3 per D14.
Zero runtime cost — no Object.freeze, no Proxy. The 'as any' /
'@ts-ignore' / JS-not-TS escape gap is the explicit trade per ADR; the
follow-up W6.P8.FREEZE closes that gap when prioritized.
Refs: decisions/D-immutability-enforcement.md (ACCEPTED 2026-05-14)
research/architecture/D-immutability-enforcement-blast-radius.md
W6.P8.B PICK (handoff-6, captured in tasks/W6-P8-B.lock)
Phase A — gates: lint OK / format:check OK / knip OK.
typecheck/test failures on the v2 stack are EXPECTED per AGENTS.md
Rule #8 until rebased onto Alex's ECS branch (PR #11939).
Per decisions/D20-id-type-convergence.md (closes design-review rows #3 and #8):
- NodeHandle.entityId → NodeHandle.id (string), add equals(other)
- WidgetHandle.entityId → WidgetHandle.id (string), add equals(other)
- SlotInfo.entityId → SlotInfo.id (string), add equals(other)
- SlotInfo.nodeEntityId → SlotInfo.nodeId (string)
- Drop NodeEntityId/WidgetEntityId/SlotEntityId re-exports from src/extension-api/index.ts
- Mark the three brand re-exports in node.ts/widget.ts as @internal — still
available to internal package modules but absent from the published barrel
and TypeDoc output
- knip.config.ts: add -internal tag so the @internal demoted brands stop
failing the unused-export check
- Update scope-registry.test.ts to use handle.id instead of handle.entityId
- Update createNodeHandle/createWidgetHandle/inputs/outputs in
extension-api-service.ts to expose id + equals
- Drop unused PublicSlotEntityId import alias
Identity is now opaque on the public surface (90% case). Branded
NodeLocatorId / NodeExecutionId remain public for the protocol-boundary
10% case (workflow JSON, websocket frames) per D20 Tier 2.
Per AUDIT-LG.4 finding: the previous doc-comments had numeric values
mapped to wrong names (claimed 1=Never, 2=Bypass, 3=once, 4=trigger,
but the actual LiteGraph runtime enum is 0=ALWAYS, 1=ON_EVENT,
2=NEVER, 3=ON_TRIGGER, 4=BYPASS). An extension following the old
docs and writing setMode(3) for 'execute once' would actually wire
the dead ON_TRIGGER plumbing.
Also notes that slots 1 and 3 are legacy ABI-reserved positions for
the dead trigger/action subsystem flagged for removal by AUDIT-LG.5.
Cross-ref: research/architecture/audit-litegraph-pruning.md
\xc2\xa7AUDIT-LG.4 \xc2\xa7AUDIT-LG.5
Folds the foundation-file edits that were previously living in PRs #12143
and #12144 into the foundation PR (#12142) so each downstream PR's diff
contains only its own concerns.
From #12143 (pkg):
- src/extension-api/index.ts: drop startExtensionSystem from public barrel
(it is the internal boot fn; app.ts imports directly from the service)
- src/extension-api/types.ts: fix bad escape in widgetCreated JSDoc example
- src/services/extension-api-service.ts: add @internal tag to
startExtensionSystem; widen widget.on() Phase-A stub warnings; harden
addWidget name parse with explanatory TODO
- src/services/__tests__/scope-registry.test.ts: drop unused removedCb in
no-dispose-on-subgraph-promotion test
From #12144 (ext):
- src/extension-api/lifecycle.ts: add JSDoc @example to defineWidgetExtension
- src/extension-api/node.ts: hoist DOMWidgetOptions interface above NodeHandle
- src/extension-api/shell.ts: clarify shell.ts is a re-export, not a move
- src/services/extension-api-service.ts: more informative Phase-A stub
warning text on node.on() (replaces pkg's terser version per ext review)
These are pure refinements (JSDoc, dev-only warnings, structural tidying);
no runtime behavior change. After this commit, rebasing pkg/ext onto the
new foundation will drop the corresponding source-touching commits as
empty no-ops and leave each PR scoped to its own concerns.
D18 Phase 1 (decisions/D18-pure-functions-loader-registration.md):
- Add EXTENSION_BRAND symbol + isBrandedExtension type-guard in
src/extension-api/brand.ts. Phase 2 loader will use these to identify
define* outputs without grepping module exports.
- Stamp brand inside defineNode / defineWidget / defineExtension. Side-
effect registration remains for Phase 1; Phase 2 removes it.
- Scaffold src/services/registries/{node,widget,app}ExtensionRegistry.ts
with the {register,getAll,_clearForTesting} shape the loader will call.
D19 (decisions/D19-vueextension-disposition.md):
- Remove VueExtension and CustomExtension from the @comfyorg/extension-api
barrel and from src/extension-api/shell.ts. They remain exported from
src/types/extensionTypes.ts for ExtensionSlot.vue (the only internal
consumer).
knip.config.ts: ignore the new Phase-1-scaffolding modules until Phase 2
wires them in. All three Phase-A quality gates (lint, format:check, knip)
pass.
Add test:extension-api script that skips gracefully when the vitest
config doesn't exist. This allows CI to pass on stacked PRs that
precede the tf branch (which adds the actual tests).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The compat-floor gate should only enforce when extension-api-v2 tests
exist. On stacked PRs that precede the tf branch (which adds tests),
gracefully skip instead of failing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add scripts/check-compat-floor.py and research/touch-points data files
to foundation branch so CI compat-floor job passes on all stacked PRs.
Per PLAN.md §Compat-floor, all blast_radius ≥ 2.0 categories must have
complete test triples before v2 ships.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove startExtensionSystem from public exports (internal-only)
- Remove unused World interface export (make module-local)
- Make addDOMWidget command serializable (store element in side table)
- Wrap event values in proper event objects ({pos}, {size}, {mode})
- defineExtension/defineNode/defineWidget now return options object
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive JSDoc explaining the implicit-context pattern for
onNodeMounted and onNodeRemoved hooks:
- Document how the runtime sets a global scope slot before nodeCreated()
- Explain why hooks must be called synchronously (Vue-style constraint)
- Add code examples showing correct vs incorrect async usage
- Document automatic cleanup and memory-safety benefits
Per design review decision #7.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Per design review decision #1: strip internal decision references
(D10c, D12) from JSDoc comments in the public API surface.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Design review cleanup:
- Remove all HR section header comments per design-review-12142.md Decision #1
- SlotEntityId changed from number to string brand (per D13 Slack resolution)
LayoutStore integration (D13 §4):
- NodeHandle.getPosition/setPosition now read/write via layoutStore
- NodeHandle.getSize/setSize now read/write via layoutStore
- Position/size are Yjs CRDT-backed with operation logging (undo/redo ready)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change status from "scaffolded" to "implemented (Phase A)"
- Update file structure to match actual files
- Replace stale decision doc refs with ADR links
- Add related research document links
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add console.warn when node.on('beforeSerialize') is used (ADR-0010)
- Add console.warn if extensions registered but startExtensionSystem()
never called (ADR-0012 mitigation)
- Enhance JSDoc with migration examples for deprecated beforeSerialize
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents the 5 categories of widget state (identity, value,
properties, options bag, DOM-specific) and their constraints
for agents working with the extension API.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>