mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
refactor(subgraph): drop disambiguatingSourceNodeId from canonical promoted widget shape
Treat each SubgraphNode as opaque: canonical PromotedWidgetView and PromotedWidgetSource link parent levels to immediate child SubgraphNode inputs rather than carrying deepest-leaf widget identity. The disambiguator is retained only as legacy lookup metadata in migration code via a dedicated LegacyProxyEntrySource shape. Amp-Thread-ID: https://ampcode.com/threads/T-019e0df9-abbb-73df-88d9-379128728306 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -83,8 +83,7 @@ function isWidgetShownOnParents(
|
||||
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: interiorNodeId,
|
||||
sourceWidgetName: widget.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: widget.disambiguatingSourceNodeId
|
||||
sourceWidgetName: widget.sourceWidgetName
|
||||
})
|
||||
}
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
|
||||
@@ -63,8 +63,7 @@ function isSamePromotedWidget(a: IBaseWidget, b: IBaseWidget): boolean {
|
||||
isPromotedWidgetView(a) &&
|
||||
isPromotedWidgetView(b) &&
|
||||
a.sourceNodeId === b.sourceNodeId &&
|
||||
a.sourceWidgetName === b.sourceWidgetName &&
|
||||
a.disambiguatingSourceNodeId === b.disambiguatingSourceNodeId
|
||||
a.sourceWidgetName === b.sourceWidgetName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,10 +123,7 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
|
||||
({ node: interiorNode, widget }) =>
|
||||
!isWidgetPromotedOnSubgraphNode(node, {
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: getWidgetName(widget),
|
||||
disambiguatingSourceNodeId: isPromotedWidgetView(widget)
|
||||
? widget.disambiguatingSourceNodeId
|
||||
: undefined
|
||||
sourceWidgetName: getWidgetName(widget)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -46,12 +46,7 @@ const { t } = useI18n()
|
||||
const hasParents = computed(() => parents?.length > 0)
|
||||
const isLinked = computed(() => {
|
||||
if (!node.isSubgraphNode() || !isPromotedWidgetView(widget)) return false
|
||||
return isLinkedPromotion(
|
||||
node,
|
||||
widget.sourceNodeId,
|
||||
widget.sourceWidgetName,
|
||||
widget.disambiguatingSourceNodeId
|
||||
)
|
||||
return isLinkedPromotion(node, widget.sourceNodeId, widget.sourceWidgetName)
|
||||
})
|
||||
const canToggleVisibility = computed(() => hasParents.value && !isLinked.value)
|
||||
const favoriteNode = computed(() =>
|
||||
|
||||
@@ -206,7 +206,6 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
'10',
|
||||
'prompt',
|
||||
'value',
|
||||
undefined,
|
||||
'value'
|
||||
)
|
||||
|
||||
|
||||
@@ -227,18 +227,15 @@ function safeWidgetMapper(
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePromotedSourceByInputName(inputName: string): {
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
} | null {
|
||||
function resolvePromotedSourceByInputName(
|
||||
inputName: string
|
||||
): PromotedWidgetSource | null {
|
||||
const resolvedTarget = resolveSubgraphInputTarget(node, inputName)
|
||||
if (!resolvedTarget) return null
|
||||
|
||||
return {
|
||||
sourceNodeId: resolvedTarget.nodeId,
|
||||
sourceWidgetName: resolvedTarget.widgetName,
|
||||
disambiguatingSourceNodeId: resolvedTarget.sourceNodeId
|
||||
sourceWidgetName: resolvedTarget.widgetName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,10 +253,9 @@ function safeWidgetMapper(
|
||||
const matchedInput = matchPromotedInput(node.inputs, widget)
|
||||
const promotedInputName = matchedInput?.name
|
||||
const displayName = promotedInputName ?? widget.name
|
||||
const directSource = {
|
||||
const directSource: PromotedWidgetSource = {
|
||||
sourceNodeId: widget.sourceNodeId,
|
||||
sourceWidgetName: widget.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: widget.disambiguatingSourceNodeId
|
||||
sourceWidgetName: widget.sourceWidgetName
|
||||
}
|
||||
const promotedSource =
|
||||
matchedInput?._widget === widget
|
||||
@@ -306,8 +302,7 @@ function safeWidgetMapper(
|
||||
? resolveConcretePromotedWidget(
|
||||
node,
|
||||
promotedSource.sourceNodeId,
|
||||
promotedSource.sourceWidgetName,
|
||||
promotedSource.disambiguatingSourceNodeId
|
||||
promotedSource.sourceWidgetName
|
||||
)
|
||||
: null
|
||||
const resolvedSource =
|
||||
@@ -320,11 +315,7 @@ function safeWidgetMapper(
|
||||
const effectiveWidget = sourceWidget ?? widget
|
||||
|
||||
const localId = isPromotedWidgetView(widget)
|
||||
? String(
|
||||
sourceNode?.id ??
|
||||
promotedSource?.disambiguatingSourceNodeId ??
|
||||
promotedSource?.sourceNodeId
|
||||
)
|
||||
? String(sourceNode?.id ?? promotedSource?.sourceNodeId)
|
||||
: undefined
|
||||
const nodeId =
|
||||
subgraphId && localId ? `${subgraphId}:${localId}` : undefined
|
||||
|
||||
@@ -105,7 +105,11 @@ describe('normalizeLegacyProxyWidgetEntry', () => {
|
||||
expect(result.disambiguatingSourceNodeId).toBe(String(samplerNode.id))
|
||||
})
|
||||
|
||||
it('returns original entry when prefix cannot be resolved', () => {
|
||||
it('strips legacy prefix and surfaces it as disambiguator even when the bare name does not resolve', () => {
|
||||
// ADR 0009: each SubgraphNode is opaque, so legacy nested
|
||||
// disambiguator-based lookup no longer reaches deep widgets. The
|
||||
// prefix is preserved as `disambiguatingSourceNodeId` lookup metadata
|
||||
// for migration tooling.
|
||||
const { hostNode, innerNode } = createHostWithInnerWidget('seed')
|
||||
|
||||
const result = normalizeLegacyProxyWidgetEntry(
|
||||
@@ -116,7 +120,8 @@ describe('normalizeLegacyProxyWidgetEntry', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: '999: nonexistent_widget'
|
||||
sourceWidgetName: 'nonexistent_widget',
|
||||
disambiguatingSourceNodeId: '999'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,89 +1,54 @@
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LegacyProxyEntrySource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { resolveConcretePromotedWidget } from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
|
||||
const LEGACY_PROXY_WIDGET_PREFIX_PATTERN = /^\s*(\d+)\s*:\s*(.+)$/
|
||||
|
||||
type PromotedWidgetPatch = Omit<PromotedWidgetSource, 'sourceNodeId'>
|
||||
|
||||
function canResolve(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
widgetName: string,
|
||||
disambiguator?: string
|
||||
widgetName: string
|
||||
): boolean {
|
||||
return (
|
||||
resolveConcretePromotedWidget(
|
||||
hostNode,
|
||||
sourceNodeId,
|
||||
widgetName,
|
||||
disambiguator
|
||||
).status === 'resolved'
|
||||
resolveConcretePromotedWidget(hostNode, sourceNodeId, widgetName).status ===
|
||||
'resolved'
|
||||
)
|
||||
}
|
||||
|
||||
function tryResolveCandidate(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
widgetName: string,
|
||||
disambiguator?: string
|
||||
): PromotedWidgetPatch | undefined {
|
||||
if (!canResolve(hostNode, sourceNodeId, widgetName, disambiguator))
|
||||
return undefined
|
||||
|
||||
return {
|
||||
sourceWidgetName: widgetName,
|
||||
...(disambiguator && { disambiguatingSourceNodeId: disambiguator })
|
||||
}
|
||||
interface StrippedPrefix {
|
||||
sourceWidgetName: string
|
||||
/** Deepest legacy `n: ` prefix removed from the original widget name. */
|
||||
deepestPrefixId?: string
|
||||
}
|
||||
|
||||
function resolveLegacyPrefixedEntry(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): PromotedWidgetPatch | undefined {
|
||||
function stripLegacyPrefixes(sourceWidgetName: string): StrippedPrefix {
|
||||
let remaining = sourceWidgetName
|
||||
|
||||
let deepestPrefixId: string | undefined
|
||||
while (true) {
|
||||
const match = LEGACY_PROXY_WIDGET_PREFIX_PATTERN.exec(remaining)
|
||||
if (!match) return undefined
|
||||
|
||||
const [, legacySourceNodeId, unprefixed] = match
|
||||
remaining = unprefixed
|
||||
|
||||
const disambiguators = [
|
||||
legacySourceNodeId,
|
||||
...(disambiguatingSourceNodeId ? [disambiguatingSourceNodeId] : []),
|
||||
undefined
|
||||
]
|
||||
|
||||
for (const disambiguator of disambiguators) {
|
||||
const resolved = tryResolveCandidate(
|
||||
hostNode,
|
||||
sourceNodeId,
|
||||
remaining,
|
||||
disambiguator
|
||||
)
|
||||
if (resolved) return resolved
|
||||
}
|
||||
if (!match) return { sourceWidgetName: remaining, deepestPrefixId }
|
||||
deepestPrefixId = match[1]
|
||||
remaining = match[2]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a legacy `proxyWidgets` entry.
|
||||
*
|
||||
* Under ADR 0009 each `SubgraphNode` is opaque, so the canonical state never
|
||||
* resolves through deep nested identities. This helper still recognizes the
|
||||
* legacy `"<id>: <name>"` prefix encoding and surfaces the deepest prefix as
|
||||
* `disambiguatingSourceNodeId` so migration tooling can preserve it as
|
||||
* lookup metadata. The bare entry is returned unchanged when it already
|
||||
* resolves at the immediate level.
|
||||
*/
|
||||
export function normalizeLegacyProxyWidgetEntry(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): PromotedWidgetSource {
|
||||
if (
|
||||
canResolve(
|
||||
hostNode,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
)
|
||||
) {
|
||||
): LegacyProxyEntrySource {
|
||||
if (canResolve(hostNode, sourceNodeId, sourceWidgetName)) {
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
@@ -91,19 +56,13 @@ export function normalizeLegacyProxyWidgetEntry(
|
||||
}
|
||||
}
|
||||
|
||||
const patch = resolveLegacyPrefixedEntry(
|
||||
hostNode,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
)
|
||||
|
||||
const stripped = stripLegacyPrefixes(sourceWidgetName)
|
||||
const patchDisambiguatingSourceNodeId =
|
||||
patch?.disambiguatingSourceNodeId ?? disambiguatingSourceNodeId
|
||||
stripped.deepestPrefixId ?? disambiguatingSourceNodeId
|
||||
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceWidgetName: patch?.sourceWidgetName ?? sourceWidgetName,
|
||||
sourceWidgetName: stripped.sourceWidgetName,
|
||||
...(patchDisambiguatingSourceNodeId && {
|
||||
disambiguatingSourceNodeId: patchDisambiguatingSourceNodeId
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
import { classifyProxyEntry } from '@/core/graph/subgraph/migration/classifyProxyEntry'
|
||||
import type {
|
||||
PromotedWidgetSource,
|
||||
LegacyProxyEntrySource,
|
||||
PromotedWidgetView
|
||||
} from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
|
||||
@@ -41,7 +41,7 @@ function makeSource(
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): PromotedWidgetSource {
|
||||
): LegacyProxyEntrySource {
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
@@ -79,28 +79,26 @@ describe(classifyProxyEntry, () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('matches already-linked inputs by disambiguatingSourceNodeId when provided', () => {
|
||||
it('quarantines as ambiguous when canonical inputs share the same identity, even if the legacy entry has a disambiguator', () => {
|
||||
// ADR 0009: canonical PromotedWidgetView no longer carries a
|
||||
// `disambiguatingSourceNodeId`, so two inputs sharing the same
|
||||
// (sourceNodeId, sourceWidgetName) cannot be told apart by the
|
||||
// classifier. The legacy entry's disambiguator is metadata only and
|
||||
// does not break the tie.
|
||||
const host = buildHost()
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
innerNode.addWidget('number', 'seed', 0, () => {})
|
||||
host.subgraph.add(innerNode)
|
||||
|
||||
const firstInput = host.addInput('first_seed', '*')
|
||||
firstInput._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'first'
|
||||
})
|
||||
const secondInput = host.addInput('second_seed', '*')
|
||||
secondInput._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'second'
|
||||
})
|
||||
for (const inputName of ['first_seed', 'second_seed']) {
|
||||
const input = host.addInput(inputName, '*')
|
||||
input._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed'
|
||||
})
|
||||
}
|
||||
|
||||
const normalized = makeSource(String(innerNode.id), 'seed', 'second')
|
||||
const result = classifyProxyEntry({
|
||||
@@ -109,9 +107,9 @@ describe(classifyProxyEntry, () => {
|
||||
cohort: [normalized]
|
||||
})
|
||||
|
||||
expect(result.plan).toEqual({
|
||||
kind: 'alreadyLinked',
|
||||
subgraphInputName: 'second_seed'
|
||||
expect(result).toEqual({
|
||||
classification: 'unknown',
|
||||
plan: { kind: 'quarantine', reason: 'ambiguousSubgraphInput' }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
PrimitiveBypassTargetRef,
|
||||
ProxyEntryClassification
|
||||
} from '@/core/graph/subgraph/migration/proxyWidgetMigrationPlanTypes'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LegacyProxyEntrySource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import {
|
||||
getPromotableWidgets,
|
||||
@@ -19,9 +19,9 @@ interface ClassificationResult {
|
||||
|
||||
interface ClassifyProxyEntryArgs {
|
||||
hostNode: SubgraphNode
|
||||
normalized: PromotedWidgetSource
|
||||
normalized: LegacyProxyEntrySource
|
||||
/** All proxy entries this planner pass is considering — needed to detect primitive fan-out. */
|
||||
cohort: readonly PromotedWidgetSource[]
|
||||
cohort: readonly LegacyProxyEntrySource[]
|
||||
}
|
||||
|
||||
const PRIMITIVE_NODE_TYPE = 'PrimitiveNode'
|
||||
@@ -33,7 +33,7 @@ type LinkedInputMatch =
|
||||
|
||||
function findLinkedSubgraphInputMatch(
|
||||
hostNode: SubgraphNode,
|
||||
normalized: PromotedWidgetSource
|
||||
normalized: LegacyProxyEntrySource
|
||||
): LinkedInputMatch {
|
||||
const matches: string[] = []
|
||||
for (const input of hostNode.inputs) {
|
||||
@@ -41,10 +41,7 @@ function findLinkedSubgraphInputMatch(
|
||||
if (!widget || !isPromotedWidgetView(widget)) continue
|
||||
if (
|
||||
widget.sourceNodeId === normalized.sourceNodeId &&
|
||||
widget.sourceWidgetName === normalized.sourceWidgetName &&
|
||||
(!normalized.disambiguatingSourceNodeId ||
|
||||
widget.disambiguatingSourceNodeId ===
|
||||
normalized.disambiguatingSourceNodeId)
|
||||
widget.sourceWidgetName === normalized.sourceWidgetName
|
||||
) {
|
||||
matches.push(input.name)
|
||||
}
|
||||
@@ -74,7 +71,7 @@ function collectPrimitiveTargets(
|
||||
}
|
||||
|
||||
function cohortReferencesPrimitive(
|
||||
cohort: readonly PromotedWidgetSource[],
|
||||
cohort: readonly LegacyProxyEntrySource[],
|
||||
primitiveNodeId: string
|
||||
): boolean {
|
||||
let count = 0
|
||||
|
||||
@@ -16,7 +16,6 @@ import { flushProxyWidgetMigration } from '@/core/graph/subgraph/migration/proxy
|
||||
import { readHostQuarantine } from '@/core/graph/subgraph/migration/quarantineEntry'
|
||||
import type { PromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({})
|
||||
@@ -69,7 +68,7 @@ describe(flushProxyWidgetMigration, () => {
|
||||
|
||||
const exposures = usePreviewExposureStore().getExposures(
|
||||
host.rootGraph.id,
|
||||
createNodeLocatorId(host.rootGraph.id, host.id)
|
||||
String(host.id)
|
||||
)
|
||||
expect(exposures).toHaveLength(1)
|
||||
expect(exposures[0].sourcePreviewName).toBe('$$canvas-image-preview')
|
||||
@@ -152,10 +151,7 @@ describe(flushProxyWidgetMigration, () => {
|
||||
expect(first.previewMigrated).toBe(1)
|
||||
|
||||
const exposuresAfterFirst = usePreviewExposureStore()
|
||||
.getExposures(
|
||||
host.rootGraph.id,
|
||||
createNodeLocatorId(host.rootGraph.id, host.id)
|
||||
)
|
||||
.getExposures(host.rootGraph.id, String(host.id))
|
||||
.map((e) => ({ ...e }))
|
||||
|
||||
const second = flushProxyWidgetMigration({ hostNode: host })
|
||||
@@ -169,7 +165,7 @@ describe(flushProxyWidgetMigration, () => {
|
||||
expect(
|
||||
usePreviewExposureStore().getExposures(
|
||||
host.rootGraph.id,
|
||||
createNodeLocatorId(host.rootGraph.id, host.id)
|
||||
String(host.id)
|
||||
)
|
||||
).toEqual(exposuresAfterFirst)
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/core/graph/subgraph/migration/quarantineEntry'
|
||||
import { repairPrimitiveFanout } from '@/core/graph/subgraph/migration/repairPrimitiveFanout'
|
||||
import { repairValueWidget } from '@/core/graph/subgraph/migration/repairValueWidget'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LegacyProxyEntrySource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { SerializedProxyWidgetTuple } from '@/core/schemas/promotionSchema'
|
||||
import type { ProxyWidgetErrorQuarantineEntry } from '@/core/schemas/proxyWidgetQuarantineSchema'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
@@ -37,7 +37,7 @@ const EMPTY_RESULT: FlushProxyWidgetMigrationResult = {
|
||||
}
|
||||
|
||||
function toLegacyTuple(
|
||||
source: PromotedWidgetSource
|
||||
source: LegacyProxyEntrySource
|
||||
): SerializedProxyWidgetTuple {
|
||||
return source.disambiguatingSourceNodeId
|
||||
? [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LegacyProxyEntrySource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { ProxyWidgetQuarantineReason } from '@/core/schemas/proxyWidgetQuarantineSchema'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
@@ -56,7 +56,7 @@ type MigrationPlanKind = MigrationPlan['kind']
|
||||
* applying mutations.
|
||||
*/
|
||||
export interface PendingMigrationEntry {
|
||||
normalized: PromotedWidgetSource
|
||||
normalized: LegacyProxyEntrySource
|
||||
legacyOrderIndex: number
|
||||
hostValue: HostValue
|
||||
classification: ProxyEntryClassification
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@/core/graph/subgraph/migration/proxyWidgetMigrationPlanTypes'
|
||||
import { HOST_VALUE_HOLE } from '@/core/graph/subgraph/migration/proxyWidgetMigrationPlanTypes'
|
||||
import { normalizeLegacyProxyWidgetEntry } from '@/core/graph/subgraph/legacyProxyWidgetNormalization'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LegacyProxyEntrySource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { parseProxyWidgets } from '@/core/schemas/promotionSchema'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
@@ -37,7 +37,7 @@ export function planProxyWidgetMigration(
|
||||
const tuples = parseProxyWidgets(hostNode.properties.proxyWidgets)
|
||||
if (tuples.length === 0) return { entries: [] }
|
||||
|
||||
const normalized: PromotedWidgetSource[] = tuples.map(
|
||||
const normalized: LegacyProxyEntrySource[] = tuples.map(
|
||||
([sourceNodeId, sourceWidgetName, disambiguator]) =>
|
||||
normalizeLegacyProxyWidgetEntry(
|
||||
hostNode,
|
||||
|
||||
@@ -117,7 +117,12 @@ describe(repairValueWidget, () => {
|
||||
expect(inputSlot._widget?.value).toBe(7)
|
||||
})
|
||||
|
||||
it('applies host value to the linked input with the matching disambiguator', () => {
|
||||
it('routes by subgraphInputName, ignoring legacy disambiguator metadata', () => {
|
||||
// ADR 0009: canonical PromotedWidgetView no longer carries a
|
||||
// `disambiguatingSourceNodeId`. Repair routes the host value to the
|
||||
// input named by `subgraphInputName`; any disambiguator carried on the
|
||||
// legacy entry is metadata only and does not affect the canonical
|
||||
// match.
|
||||
const host = buildHost()
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
innerNode.addWidget('number', 'seed', 0, () => {})
|
||||
@@ -129,7 +134,6 @@ describe(repairValueWidget, () => {
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'first',
|
||||
value: 1
|
||||
})
|
||||
const secondInput = host.addInput('second_seed', '*')
|
||||
@@ -138,7 +142,6 @@ describe(repairValueWidget, () => {
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'second',
|
||||
value: 2
|
||||
})
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ function findHostInputForLinkedSource(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
subgraphInputName: string | undefined,
|
||||
disambiguatingSourceNodeId?: string
|
||||
subgraphInputName: string | undefined
|
||||
):
|
||||
| { kind: 'none' }
|
||||
| { kind: 'one'; input: INodeInputSlot }
|
||||
@@ -40,9 +39,7 @@ function findHostInputForLinkedSource(
|
||||
if (!widget || !isPromotedWidgetView(widget)) return false
|
||||
return (
|
||||
widget.sourceNodeId === sourceNodeId &&
|
||||
widget.sourceWidgetName === sourceWidgetName &&
|
||||
(!disambiguatingSourceNodeId ||
|
||||
widget.disambiguatingSourceNodeId === disambiguatingSourceNodeId)
|
||||
widget.sourceWidgetName === sourceWidgetName
|
||||
)
|
||||
})
|
||||
if (matches.length === 0) return { kind: 'none' }
|
||||
@@ -68,8 +65,7 @@ function repairAlreadyLinked(
|
||||
entry.normalized.sourceWidgetName,
|
||||
entry.plan.kind === 'alreadyLinked'
|
||||
? entry.plan.subgraphInputName
|
||||
: undefined,
|
||||
entry.normalized.disambiguatingSourceNodeId
|
||||
: undefined
|
||||
)
|
||||
if (hostInput.kind === 'ambiguous') {
|
||||
return { ok: false, reason: 'ambiguousSubgraphInput' }
|
||||
|
||||
@@ -10,20 +10,27 @@ export interface ResolvedPromotedWidget {
|
||||
export interface PromotedWidgetSource {
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy proxyWidget tuple shape carried through migration. The optional
|
||||
* `disambiguatingSourceNodeId` is read from legacy `properties.proxyWidgets`
|
||||
* payloads only — canonical runtime state never sets it. See ADR 0009.
|
||||
*/
|
||||
export interface LegacyProxyEntrySource extends PromotedWidgetSource {
|
||||
disambiguatingSourceNodeId?: string
|
||||
}
|
||||
|
||||
export interface PromotedWidgetView extends IBaseWidget {
|
||||
readonly node: SubgraphNode
|
||||
/**
|
||||
* Identity of the immediate interior child whose widget (or input slot, for
|
||||
* nested SubgraphNode children) this view exposes. Per ADR 0009 each
|
||||
* SubgraphNode is opaque: the parent's promoted view references the
|
||||
* immediate child only and does not flatten to deeper origins.
|
||||
*/
|
||||
readonly sourceNodeId: string
|
||||
readonly sourceWidgetName: string
|
||||
/**
|
||||
* The original leaf-level source node ID, used to distinguish promoted
|
||||
* widgets with the same name on the same intermediate node. Unlike
|
||||
* `sourceNodeId` (the direct interior node), this traces to the deepest
|
||||
* origin.
|
||||
*/
|
||||
readonly disambiguatingSourceNodeId?: string
|
||||
}
|
||||
|
||||
export function isPromotedWidgetView(
|
||||
|
||||
@@ -31,12 +31,7 @@ export { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
export function getPromotedWidgetHostStateName(
|
||||
widget: IPromotedWidgetView
|
||||
): string {
|
||||
return [
|
||||
widget.name,
|
||||
widget.sourceNodeId,
|
||||
widget.sourceWidgetName,
|
||||
widget.disambiguatingSourceNodeId ?? ''
|
||||
].join(':')
|
||||
return [widget.name, widget.sourceNodeId, widget.sourceWidgetName].join(':')
|
||||
}
|
||||
|
||||
interface SubgraphSlotRef {
|
||||
@@ -76,7 +71,6 @@ export function createPromotedWidgetView(
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
displayName?: string,
|
||||
disambiguatingSourceNodeId?: string,
|
||||
identityName?: string
|
||||
): IPromotedWidgetView {
|
||||
return new PromotedWidgetView(
|
||||
@@ -84,7 +78,6 @@ export function createPromotedWidgetView(
|
||||
nodeId,
|
||||
widgetName,
|
||||
displayName,
|
||||
disambiguatingSourceNodeId,
|
||||
identityName
|
||||
)
|
||||
}
|
||||
@@ -120,7 +113,6 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
private readonly displayName?: string,
|
||||
readonly disambiguatingSourceNodeId?: string,
|
||||
private readonly identityName?: string
|
||||
) {
|
||||
this.sourceNodeId = nodeId
|
||||
@@ -453,8 +445,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
return resolvePromotedWidgetAtHost(
|
||||
this.subgraphNode,
|
||||
this.sourceNodeId,
|
||||
this.sourceWidgetName,
|
||||
this.disambiguatingSourceNodeId
|
||||
this.sourceWidgetName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -468,8 +459,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
const result = resolveConcretePromotedWidget(
|
||||
this.subgraphNode,
|
||||
this.sourceNodeId,
|
||||
this.sourceWidgetName,
|
||||
this.disambiguatingSourceNodeId
|
||||
this.sourceWidgetName
|
||||
)
|
||||
const resolved = result.status === 'resolved' ? result.resolved : undefined
|
||||
|
||||
@@ -509,9 +499,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
if (boundWidget && isPromotedWidgetView(boundWidget)) {
|
||||
return (
|
||||
boundWidget.sourceNodeId === this.sourceNodeId &&
|
||||
boundWidget.sourceWidgetName === this.sourceWidgetName &&
|
||||
boundWidget.disambiguatingSourceNodeId ===
|
||||
this.disambiguatingSourceNodeId
|
||||
boundWidget.sourceWidgetName === this.sourceWidgetName
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,7 @@ export function getWidgetName(w: IBaseWidget): string {
|
||||
export function isLinkedPromotion(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
sourceWidgetName: string
|
||||
): boolean {
|
||||
return subgraphNode.inputs.some((input) => {
|
||||
const w = input._widget
|
||||
@@ -45,9 +44,7 @@ export function isLinkedPromotion(
|
||||
w &&
|
||||
isPromotedWidgetView(w) &&
|
||||
w.sourceNodeId === sourceNodeId &&
|
||||
w.sourceWidgetName === sourceWidgetName &&
|
||||
(!disambiguatingSourceNodeId ||
|
||||
w.disambiguatingSourceNodeId === disambiguatingSourceNodeId)
|
||||
w.sourceWidgetName === sourceWidgetName
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -155,14 +152,13 @@ function isSamePromotedWidget(left: IBaseWidget, right: IBaseWidget): boolean {
|
||||
isPromotedWidgetView(left) &&
|
||||
isPromotedWidgetView(right) &&
|
||||
left.sourceNodeId === right.sourceNodeId &&
|
||||
left.sourceWidgetName === right.sourceWidgetName &&
|
||||
left.disambiguatingSourceNodeId === right.disambiguatingSourceNodeId
|
||||
left.sourceWidgetName === right.sourceWidgetName
|
||||
)
|
||||
}
|
||||
|
||||
export function getSourceNodeId(w: IBaseWidget): string | undefined {
|
||||
if (!isPromotedWidgetView(w)) return undefined
|
||||
return w.disambiguatingSourceNodeId ?? w.sourceNodeId
|
||||
return w.sourceNodeId
|
||||
}
|
||||
|
||||
function isPreviewExposed(
|
||||
@@ -201,8 +197,7 @@ export function isWidgetPromotedOnSubgraphNode(
|
||||
isLinkedPromotion(
|
||||
subgraphNode,
|
||||
source.sourceNodeId,
|
||||
source.sourceWidgetName,
|
||||
source.disambiguatingSourceNodeId
|
||||
source.sourceWidgetName
|
||||
) || isPreviewExposed(subgraphNode, source)
|
||||
)
|
||||
}
|
||||
@@ -213,10 +208,7 @@ function toPromotionSource(
|
||||
): PromotedWidgetSource {
|
||||
return {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceWidgetName: getWidgetName(widget),
|
||||
disambiguatingSourceNodeId: isPromotedWidgetView(widget)
|
||||
? widget.disambiguatingSourceNodeId
|
||||
: undefined
|
||||
sourceWidgetName: getWidgetName(widget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,9 +338,7 @@ export function demoteWidget(
|
||||
linkedWidget &&
|
||||
isPromotedWidgetView(linkedWidget) &&
|
||||
linkedWidget.sourceNodeId === source.sourceNodeId &&
|
||||
linkedWidget.sourceWidgetName === source.sourceWidgetName &&
|
||||
linkedWidget.disambiguatingSourceNodeId ===
|
||||
source.disambiguatingSourceNodeId
|
||||
linkedWidget.sourceWidgetName === source.sourceWidgetName
|
||||
)
|
||||
})
|
||||
if (linkedInput) {
|
||||
|
||||
@@ -30,7 +30,6 @@ type PromotedWidgetStub = Pick<
|
||||
> & {
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
node?: SubgraphNode
|
||||
}
|
||||
|
||||
@@ -52,8 +51,7 @@ function createPromotedWidget(
|
||||
name: string,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
node?: SubgraphNode,
|
||||
disambiguatingSourceNodeId?: string
|
||||
node?: SubgraphNode
|
||||
): IBaseWidget {
|
||||
const promotedWidget: PromotedWidgetStub = {
|
||||
name,
|
||||
@@ -63,7 +61,6 @@ function createPromotedWidget(
|
||||
value: undefined,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId,
|
||||
node
|
||||
}
|
||||
return promotedWidget as IBaseWidget
|
||||
@@ -97,27 +94,6 @@ describe('resolvePromotedWidgetAtHost', () => {
|
||||
|
||||
expect(resolved).toBeUndefined()
|
||||
})
|
||||
|
||||
test('resolves duplicate-name promoted host widgets by disambiguating source node id', () => {
|
||||
const host = createHostNode(100)
|
||||
const sourceNode = addNodeToHost(host, 'source')
|
||||
sourceNode.widgets = [
|
||||
createPromotedWidget('text', String(sourceNode.id), 'text', host, '1'),
|
||||
createPromotedWidget('text', String(sourceNode.id), 'text', host, '2')
|
||||
]
|
||||
|
||||
const resolved = resolvePromotedWidgetAtHost(
|
||||
host,
|
||||
String(sourceNode.id),
|
||||
'text',
|
||||
'2'
|
||||
)
|
||||
|
||||
expect(resolved).toBeDefined()
|
||||
expect(
|
||||
(resolved!.widget as PromotedWidgetStub).disambiguatingSourceNodeId
|
||||
).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveConcretePromotedWidget', () => {
|
||||
|
||||
@@ -20,8 +20,7 @@ const MAX_PROMOTED_WIDGET_CHAIN_DEPTH = 100
|
||||
function traversePromotedWidgetChain(
|
||||
hostNode: SubgraphNode,
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
sourceNodeId?: string
|
||||
widgetName: string
|
||||
): PromotedWidgetResolutionResult {
|
||||
const visited = new Set<string>()
|
||||
const hostUidByObject = new WeakMap<SubgraphNode, number>()
|
||||
@@ -29,7 +28,6 @@ function traversePromotedWidgetChain(
|
||||
let currentHost = hostNode
|
||||
let currentNodeId = nodeId
|
||||
let currentWidgetName = widgetName
|
||||
let currentSourceNodeId = sourceNodeId
|
||||
|
||||
for (let depth = 0; depth < MAX_PROMOTED_WIDGET_CHAIN_DEPTH; depth++) {
|
||||
let hostUid = hostUidByObject.get(currentHost)
|
||||
@@ -39,7 +37,7 @@ function traversePromotedWidgetChain(
|
||||
hostUidByObject.set(currentHost, hostUid)
|
||||
}
|
||||
|
||||
const key = `${hostUid}:${currentNodeId}:${currentWidgetName}:${currentSourceNodeId ?? ''}`
|
||||
const key = `${hostUid}:${currentNodeId}:${currentWidgetName}`
|
||||
if (visited.has(key)) {
|
||||
return { status: 'failure', failure: 'cycle' }
|
||||
}
|
||||
@@ -52,8 +50,7 @@ function traversePromotedWidgetChain(
|
||||
|
||||
const sourceWidget = findWidgetByIdentity(
|
||||
sourceNode.widgets,
|
||||
currentWidgetName,
|
||||
currentSourceNodeId
|
||||
currentWidgetName
|
||||
)
|
||||
if (!sourceWidget) {
|
||||
return { status: 'failure', failure: 'missing-widget' }
|
||||
@@ -73,7 +70,6 @@ function traversePromotedWidgetChain(
|
||||
currentHost = sourceWidget.node
|
||||
currentNodeId = sourceWidget.sourceNodeId
|
||||
currentWidgetName = sourceWidget.sourceWidgetName
|
||||
currentSourceNodeId = undefined
|
||||
}
|
||||
|
||||
return { status: 'failure', failure: 'max-depth-exceeded' }
|
||||
@@ -81,34 +77,20 @@ function traversePromotedWidgetChain(
|
||||
|
||||
function findWidgetByIdentity(
|
||||
widgets: IBaseWidget[] | undefined,
|
||||
widgetName: string,
|
||||
sourceNodeId?: string
|
||||
widgetName: string
|
||||
): IBaseWidget | undefined {
|
||||
if (!widgets) return undefined
|
||||
|
||||
if (sourceNodeId) {
|
||||
return widgets.find(
|
||||
(entry) =>
|
||||
isPromotedWidgetView(entry) &&
|
||||
(entry.disambiguatingSourceNodeId ?? entry.sourceNodeId) ===
|
||||
sourceNodeId &&
|
||||
(entry.sourceWidgetName === widgetName || entry.name === widgetName)
|
||||
)
|
||||
}
|
||||
|
||||
return widgets.find((entry) => entry.name === widgetName)
|
||||
return widgets?.find((entry) => entry.name === widgetName)
|
||||
}
|
||||
|
||||
export function resolvePromotedWidgetAtHost(
|
||||
hostNode: SubgraphNode,
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
sourceNodeId?: string
|
||||
widgetName: string
|
||||
): ResolvedPromotedWidget | undefined {
|
||||
const node = hostNode.subgraph.getNodeById(nodeId)
|
||||
if (!node) return undefined
|
||||
|
||||
const widget = findWidgetByIdentity(node.widgets, widgetName, sourceNodeId)
|
||||
const widget = findWidgetByIdentity(node.widgets, widgetName)
|
||||
if (!widget) return undefined
|
||||
|
||||
return { node, widget }
|
||||
@@ -117,11 +99,10 @@ export function resolvePromotedWidgetAtHost(
|
||||
export function resolveConcretePromotedWidget(
|
||||
hostNode: LGraphNode,
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
sourceNodeId?: string
|
||||
widgetName: string
|
||||
): PromotedWidgetResolutionResult {
|
||||
if (!hostNode.isSubgraphNode()) {
|
||||
return { status: 'failure', failure: 'invalid-host' }
|
||||
}
|
||||
return traversePromotedWidgetChain(hostNode, nodeId, widgetName, sourceNodeId)
|
||||
return traversePromotedWidgetChain(hostNode, nodeId, widgetName)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ export function resolvePromotedWidgetSource(
|
||||
const result = resolveConcretePromotedWidget(
|
||||
hostNode,
|
||||
widget.sourceNodeId,
|
||||
widget.sourceWidgetName,
|
||||
widget.disambiguatingSourceNodeId
|
||||
widget.sourceWidgetName
|
||||
)
|
||||
if (result.status === 'resolved') return result.resolved
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
import { resolveSubgraphInputLink } from './resolveSubgraphInputLink'
|
||||
|
||||
type ResolvedSubgraphInputTarget = {
|
||||
nodeId: string
|
||||
widgetName: string
|
||||
sourceNodeId?: string
|
||||
}
|
||||
|
||||
export function resolveSubgraphInputTarget(
|
||||
@@ -17,29 +15,18 @@ export function resolveSubgraphInputTarget(
|
||||
node,
|
||||
inputName,
|
||||
({ inputNode, targetInput, getTargetWidget }) => {
|
||||
const targetWidget = getTargetWidget()
|
||||
if (!targetWidget) return undefined
|
||||
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
const targetWidget = getTargetWidget()
|
||||
if (!targetWidget) return undefined
|
||||
|
||||
if (isPromotedWidgetView(targetWidget)) {
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
widgetName: targetWidget.sourceWidgetName,
|
||||
sourceNodeId:
|
||||
targetWidget.disambiguatingSourceNodeId ??
|
||||
targetWidget.sourceNodeId
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0009: each SubgraphNode is opaque. The promoted target is the
|
||||
// child SubgraphNode's input slot, not a deeper leaf widget.
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
widgetName: targetInput.name
|
||||
}
|
||||
}
|
||||
|
||||
const targetWidget = getTargetWidget()
|
||||
if (!targetWidget) return undefined
|
||||
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
widgetName: targetWidget.name
|
||||
|
||||
@@ -147,15 +147,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
if (!targetWidget) continue
|
||||
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
if (isPromotedWidgetView(targetWidget)) {
|
||||
return {
|
||||
sourceNodeId: String(inputNode.id),
|
||||
sourceWidgetName: targetWidget.sourceWidgetName,
|
||||
disambiguatingSourceNodeId:
|
||||
targetWidget.disambiguatingSourceNodeId ??
|
||||
targetWidget.sourceNodeId
|
||||
}
|
||||
}
|
||||
// ADR 0009: each SubgraphNode is opaque. The promoted source on the
|
||||
// parent host always references the immediate child's input slot, not
|
||||
// the deeper leaf widget identity that the child internally exposes.
|
||||
return {
|
||||
sourceNodeId: String(inputNode.id),
|
||||
sourceWidgetName: targetInput.name
|
||||
@@ -227,8 +221,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
entry.inputKey,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.inputName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
entry.inputName
|
||||
)
|
||||
if (seenEntryKeys.has(entryKey)) return false
|
||||
|
||||
@@ -287,7 +280,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.viewKey ? displayNameByViewKey.get(entry.viewKey) : undefined,
|
||||
entry.disambiguatingSourceNodeId,
|
||||
entry.slotName
|
||||
)
|
||||
)
|
||||
@@ -318,28 +310,18 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
viewKey: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
slotName: string
|
||||
}> {
|
||||
return linkedEntries.map(
|
||||
({
|
||||
inputKey,
|
||||
inputName,
|
||||
slotName,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
}) => ({
|
||||
({ inputKey, inputName, slotName, sourceNodeId, sourceWidgetName }) => ({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
slotName,
|
||||
disambiguatingSourceNodeId,
|
||||
viewKey: this._makePromotionViewKey(
|
||||
inputKey,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
inputName,
|
||||
disambiguatingSourceNodeId
|
||||
inputName
|
||||
)
|
||||
})
|
||||
)
|
||||
@@ -354,8 +336,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
entry.inputKey,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.inputName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
entry.inputName
|
||||
),
|
||||
entry.inputName
|
||||
])
|
||||
@@ -366,18 +347,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
inputKey: string,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
inputName = '',
|
||||
disambiguatingSourceNodeId?: string
|
||||
inputName = ''
|
||||
): string {
|
||||
return disambiguatingSourceNodeId
|
||||
? JSON.stringify([
|
||||
inputKey,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
inputName,
|
||||
disambiguatingSourceNodeId
|
||||
])
|
||||
: JSON.stringify([inputKey, sourceNodeId, sourceWidgetName, inputName])
|
||||
return JSON.stringify([inputKey, sourceNodeId, sourceWidgetName, inputName])
|
||||
}
|
||||
|
||||
/** Manages lifecycle of all subgraph event listeners */
|
||||
@@ -882,10 +854,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
const nodeId = String(interiorNode.id)
|
||||
const widgetName = interiorWidget.name
|
||||
const sourceNodeId =
|
||||
interiorNode.isSubgraphNode() && isPromotedWidgetView(interiorWidget)
|
||||
? interiorWidget.sourceNodeId
|
||||
: undefined
|
||||
|
||||
const previousView = input._widget
|
||||
|
||||
@@ -911,15 +879,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
nodeId,
|
||||
widgetName,
|
||||
input.label ?? subgraphInput.name,
|
||||
sourceNodeId,
|
||||
subgraphInput.name
|
||||
),
|
||||
this._makePromotionViewKey(
|
||||
String(subgraphInput.id),
|
||||
nodeId,
|
||||
widgetName,
|
||||
input.label ?? input.name,
|
||||
sourceNodeId
|
||||
input.label ?? input.name
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1094,8 +1060,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const resolved = resolveConcretePromotedWidget(
|
||||
this,
|
||||
view.sourceNodeId,
|
||||
view.sourceWidgetName,
|
||||
view.disambiguatingSourceNodeId
|
||||
view.sourceWidgetName
|
||||
)
|
||||
if (resolved.status !== 'resolved') return
|
||||
|
||||
@@ -1124,8 +1089,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
String(input._subgraphSlot.id),
|
||||
view.sourceNodeId,
|
||||
view.sourceWidgetName,
|
||||
inputName,
|
||||
view.disambiguatingSourceNodeId
|
||||
inputName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
TWidgetType
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { BaseWidget, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { flushProxyWidgetMigration } from '@/core/graph/subgraph/migration/proxyWidgetMigrationFlush'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
|
||||
import {
|
||||
@@ -356,7 +357,14 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
expect(widgetSourceIds).toContain(keptSamplerNodeId)
|
||||
})
|
||||
|
||||
it('should normalize legacy prefixed proxyWidgets on configure', () => {
|
||||
it('quarantines legacy prefixed proxyWidgets that target a deep leaf widget', () => {
|
||||
// ADR 0009: each SubgraphNode is opaque. The legacy
|
||||
// "<nestedId>: <leafId>: <leafWidgetName>" encoding referenced a deep
|
||||
// leaf widget through nested chain traversal. Under the opaque model
|
||||
// the migration cannot resolve that identity at the immediate level,
|
||||
// so the entry is quarantined rather than reconstructed as a
|
||||
// canonical promoted view. Users with this legacy state must
|
||||
// re-promote through each subgraph level explicitly.
|
||||
const rootGraph = createTestRootGraph()
|
||||
|
||||
const innerSubgraph = createTestSubgraph({
|
||||
@@ -396,20 +404,16 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
}
|
||||
|
||||
hostNode.configure(serializedHostNode)
|
||||
flushProxyWidgetMigration({ hostNode })
|
||||
|
||||
const promotedWidgets = hostNode.widgets
|
||||
.filter(isPromotedWidgetView)
|
||||
.filter((widget) => !widget.name.startsWith('$$'))
|
||||
|
||||
expect(promotedWidgets).toHaveLength(1)
|
||||
expect(promotedWidgets[0].type).toBe('number')
|
||||
expect(promotedWidgets[0].value).toBe(123)
|
||||
expect(promotedWidgets[0].sourceWidgetName).toBe('noise_seed')
|
||||
expect(promotedWidgets[0].disambiguatingSourceNodeId).toBe(
|
||||
String(samplerNode.id)
|
||||
)
|
||||
// ADR 0009: configure() no longer rewrites properties.proxyWidgets.
|
||||
// Normalization is observable on the synthetic widget surface above.
|
||||
expect(promotedWidgets).toHaveLength(0)
|
||||
expect(hostNode.properties.proxyWidgets).toBeUndefined()
|
||||
const quarantine = hostNode.properties.proxyWidgetErrorQuarantine
|
||||
expect(Array.isArray(quarantine) && quarantine.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should preserve promoted widget entries after cloning', () => {
|
||||
|
||||
Reference in New Issue
Block a user