From 9228c7c0b4455bbf210eae83fac6ea02454e0dc3 Mon Sep 17 00:00:00 2001 From: BlackVortexAI Date: Mon, 31 Aug 2026 21:14:37 +0200 Subject: [PATCH] Prepare BV Node Pack 1.2.2 - synchronize Python and private UI package metadata on version 1.2.2 - document the prepared, not-yet-released presentation, Regional Canvas, and LoRA changes - remove the completed titlebar-port Canary experiment and its tests Validated with npm ci, 455 Python tests, 634 Node tests, compileall, TypeScript typecheck, production build, bundle comparison, and secret scan. --- README.md | 36 +- js/bv_titlebar_port_canary.js | 136 ------- js/bv_titlebar_port_canary_core.js | 334 ------------------ py/nodes/bv_titlebar_port_canary.py | 106 ------ pyproject.toml | 2 +- ...t_titlebar_provider_port_runtime_canary.py | 62 ---- tests/titlebar_provider_port_canary.test.mjs | 105 ------ ...ebar_provider_port_runtime_canary.test.mjs | 169 --------- ui/package-lock.json | 4 +- ui/package.json | 2 +- .../titlebar-provider-port-canary.html | 18 - .../titlebar-provider-port-runtime-canary.md | 65 ---- .../prototypes/titlebarProviderPortCanary.ts | 203 ----------- 13 files changed, 18 insertions(+), 1224 deletions(-) delete mode 100644 js/bv_titlebar_port_canary.js delete mode 100644 js/bv_titlebar_port_canary_core.js delete mode 100644 py/nodes/bv_titlebar_port_canary.py delete mode 100644 tests/test_titlebar_provider_port_runtime_canary.py delete mode 100644 tests/titlebar_provider_port_canary.test.mjs delete mode 100644 tests/titlebar_provider_port_runtime_canary.test.mjs delete mode 100644 ui/prototypes/titlebar-provider-port-canary.html delete mode 100644 ui/prototypes/titlebar-provider-port-runtime-canary.md delete mode 100644 ui/src/prototypes/titlebarProviderPortCanary.ts diff --git a/README.md b/README.md index eebd6fd..79d5e5f 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ tools, Subgraph controls and deterministic workflow utilities to ComfyUI. > direction, architecture, review and real-workflow validation remain human-led. > [!IMPORTANT] -> **BV Node Pack 1.2.1 includes Regional V3, workflow-local LoRA and LUT -> library tooling, ComfyUI-aware widget themes and expanded output metadata.** +> **BV Node Pack 1.2.2 is prepared but not yet released.** This source +> centralizes native node presentation, adds Regional Canvas image sources and +> aligns LoRA Registry interaction and outputs. > The accompanying > [Wiki](https://blackvortexai.github.io/bv_nodepack_wiki/) is online but still > under construction, so missing screenshots or incomplete pages do not indicate @@ -127,6 +128,17 @@ provenance are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). The README keeps the four most recent version entries. The complete history is maintained in the [Wiki changelog](https://blackvortexai.github.io/bv_nodepack_wiki/reference/changelog). +### 1.2.2 — prepared 2026-08-31, not released + +- Centralize native node, port and widget presentation across Classic and Nodes + 2.0, including titlebar routing for hidden infrastructure connections. +- Add Regional Canvas image selection from connected inputs and sender sources, + including a compact thumbnail picker and automatic latest-image selection. +- Align LoRA Registry count and summary outputs and use stable 0.05 increments + for strength scrubbing. +- Remove the temporary titlebar-port Canary after the shared production path + passed the complete regression suite. + ### 1.2.1 — 2026-08-30 - Add `BV Inspect Any` for readable in-node previews and string pass-through of @@ -169,26 +181,6 @@ download. - Normalize Control Center, Seed, prompt-debug and text-preview presentation and add regression coverage for the shared UI rules. -### 1.0.1 — 2026-08-26 - -- Add deterministic PNG export for the active graph, selected nodes and groups, - and open BV-managed windows. -- Add transparent, BV Documentation Grid and current ComfyUI background modes - with configurable scale and padding. -- Add optional complete-workflow PNG metadata and the versioned - `window.bvNodepack.export.v1` automation API. -- Preserve native Subgraph capture compatibility with guarded offscreen-canvas - attachment and cleanup. - -### 1.0.0 — 2026-08-25 - -- Release Regional V3 with workflow-scoped documents, stable region identity, - resource providers and model-specific execution paths. -- Add migration support and temporary Legacy Debug visibility for compatible - pre-1.0 Regional workflows. -- Publish the task-oriented documentation site and runtime-derived node - reference. - --- ## Continue with the BV Node Pack Wiki diff --git a/js/bv_titlebar_port_canary.js b/js/bv_titlebar_port_canary.js deleted file mode 100644 index e6c07aa..0000000 --- a/js/bv_titlebar_port_canary.js +++ /dev/null @@ -1,136 +0,0 @@ -// THROW AWAY - DO NOT MERGE OR RELEASE. -// Loaded by WEB_DIRECTORY even when the Python env gate is off, but completely -// inert unless one of the two exact Canary node definitions exists. - -import { app } from "../../scripts/app.js"; -import { - CANARY_MODES, - CANARY_RECEIVER, - CANARY_SENDER, - applyCanaryMode, - captureCanaryBaseline, - inspectCanaryNode, - restoreCanaryNode, - scheduleCanaryInspection, -} from "./bv_titlebar_port_canary_core.js"; - -const NAMES = new Set([CANARY_SENDER, CANARY_RECEIVER]); -const INSTALLED = Symbol("bvTitlebarPortCanaryInstalled"); - -function reportText(report) { - if (!report?.ok) return `${report?.status ?? "BLOCKED"}: ${report?.reason ?? "unknown"}`; - const invariant = report.comparison?.ok ? "PASS" : "NOT CAPTURED / FAIL"; - const links = JSON.stringify(report.links ?? []); - const bodyY = (report.measuredSlots ?? []).map(slot => `${slot.index}:${slot.y}`).join(" "); - const legacyY = (report.legacyMeasuredSlots ?? []).map(slot => `${slot.index}:${slot.y}`).join(" "); - const widgetY = (report.widgetMetrics ?? []).map(widget => `${widget.name}:${widget.y ?? "?"}`).join(" "); - const serializedPos = report.serialization?.providerHasOwnPos ? JSON.stringify(report.serialization.providerPos) : "absent"; - return [ - `${report.status} | ${report.direction}[${report.canonicalProviderIndex}] of ${report.canonicalSlotCount}`, - `structure/link invariant: ${invariant}`, - `links: ${links}`, - `provider.pos: ${JSON.stringify(report.providerPosition)} | serialized pos: ${serializedPos}`, - `slot world-y: ${bodyY}`, - `legacy getConnectionPos y: ${legacyY}`, - `widget y: ${widgetY}`, - `node size: ${JSON.stringify(report.nodeSize)} | computeSize: ${JSON.stringify(report.computedSize)}`, - ].join("\n"); -} - -function modeWidget(node) { - return node?.widgets?.find(widget => widget?.name === "presentation_mode"); -} - -function refresh(node, nodeName, requestedMode) { - const mode = requestedMode ?? modeWidget(node)?.value ?? "Native"; - const applied = applyCanaryMode(node, nodeName, mode, { nodes2Active: globalThis.LiteGraph?.vueNodesMode === true }); - const report = applied.ok ? inspectCanaryNode(node, nodeName) : applied; - if (node.__bvTitlebarCanaryStatusWidget) node.__bvTitlebarCanaryStatusWidget.value = reportText(report); - if (applied.ok) scheduleCanaryInspection(node, nodeName, postFrame => { - if (node.__bvTitlebarCanaryStatusWidget) node.__bvTitlebarCanaryStatusWidget.value = reportText(postFrame); - node.setDirtyCanvas?.(true, true); - }); - node.setDirtyCanvas?.(true, true); - node.graph?.setDirtyCanvas?.(true, true); - return report; -} - -function install(node, nodeName, recapture = false) { - if (!NAMES.has(nodeName)) return; - const mode = modeWidget(node); - if (!mode) return; - - if (!node[INSTALLED]) { - node[INSTALLED] = true; - mode.serialize = false; - const originalCallback = mode.callback; - mode.callback = function(value) { - const result = typeof originalCallback === "function" ? originalCallback.apply(this, arguments) : undefined; - queueMicrotask(() => refresh(node, nodeName, value)); - return result; - }; - const capture = node.addWidget?.("button", "Capture Native baseline", null, () => { - const result = captureCanaryBaseline(node, nodeName); - node.__bvTitlebarCanaryStatusWidget.value = result.ok ? reportText(inspectCanaryNode(node, nodeName)) : reportText(result); - }); - if (capture) capture.serialize = false; - const status = node.addWidget?.("text", "canary_status", "Waiting for Native baseline", null, { multiline: true }); - if (status) { - status.serialize = false; - node.__bvTitlebarCanaryStatusWidget = status; - } - } - - if (recapture || !node.__bvTitlebarCanaryBaselineReady) { - applyCanaryMode(node, nodeName, "Native"); - const captured = captureCanaryBaseline(node, nodeName); - node.__bvTitlebarCanaryBaselineReady = captured.ok; - } - refresh(node, nodeName, mode.value); -} - -function wrapNodeType(nodeType, nodeName) { - if (nodeType.prototype.__bvTitlebarPortCanaryWrapped) return; - nodeType.prototype.__bvTitlebarPortCanaryWrapped = true; - const created = nodeType.prototype.onNodeCreated; - const configured = nodeType.prototype.onConfigure; - const connectionsChanged = nodeType.prototype.onConnectionsChange; - const resized = nodeType.prototype.onResize; - const removed = nodeType.prototype.onRemoved; - - nodeType.prototype.onNodeCreated = function() { - const result = created?.apply(this, arguments); - queueMicrotask(() => install(this, nodeName, true)); - return result; - }; - nodeType.prototype.onConfigure = function() { - const result = configured?.apply(this, arguments); - queueMicrotask(() => install(this, nodeName, true)); - return result; - }; - nodeType.prototype.onConnectionsChange = function() { - const result = connectionsChanged?.apply(this, arguments); - queueMicrotask(() => { - if (modeWidget(this)?.value === "Native") captureCanaryBaseline(this, nodeName); - refresh(this, nodeName); - }); - return result; - }; - nodeType.prototype.onResize = function() { - const result = resized?.apply(this, arguments); - if (modeWidget(this)?.value !== "Native") queueMicrotask(() => refresh(this, nodeName)); - return result; - }; - nodeType.prototype.onRemoved = function() { - restoreCanaryNode(this); - return removed?.apply(this, arguments); - }; -} - -app.registerExtension({ - name: "bv.titlebar-provider-port-canary.throw-away", - beforeRegisterNodeDef(nodeType, nodeData) { - if (!NAMES.has(nodeData?.name)) return; - wrapNodeType(nodeType, nodeData.name); - }, -}); diff --git a/js/bv_titlebar_port_canary_core.js b/js/bv_titlebar_port_canary_core.js deleted file mode 100644 index a71c909..0000000 --- a/js/bv_titlebar_port_canary_core.js +++ /dev/null @@ -1,334 +0,0 @@ -// THROW AWAY - DO NOT MERGE OR RELEASE. -// Pure branch-local capability model for one Classic provider slot. It never -// changes graph arrays, links, normal slots, widgets, labels, visibility or size. - -export const CANARY_SENDER = "BV Titlebar Port Canary Sender (THROW AWAY)"; -export const CANARY_RECEIVER = "BV Titlebar Port Canary Receiver (THROW AWAY)"; -export const CANARY_PROVIDER_TYPE = "BV_RUNTIME_RESOURCE_PROVIDER"; -export const CANARY_MODES = ["Native", "A - Title midline", "B - Body seam", "C - Top rail"]; - -const CONTRACTS = new Map([ - [CANARY_SENDER, { direction: "output", count: 6, providerIndex: 2, providerName: "resource_provider" }], - [CANARY_RECEIVER, { direction: "input", count: 6, providerIndex: 3, providerName: "resource_provider" }], -]); -const MODE_Y = new Map([ - ["A - Title midline", -15], - ["B - Body seam", 0], - ["C - Top rail", -30], -]); -const nodeStates = new WeakMap(); -const inspectionRevisions = new WeakMap(); - -function ownDescriptor(target, key) { - return Object.getOwnPropertyDescriptor(target, key); -} - -function capturePositionOrigin(slot) { - const descriptor = ownDescriptor(slot, "pos"); - if (descriptor) return { kind: "own", descriptor }; - if ("pos" in slot) return { kind: "inherited", value: slot.pos }; - return { kind: "absent" }; -} - -function restorePosition(slot, origin) { - if (!slot || !origin) return; - if (origin.kind === "own") Object.defineProperty(slot, "pos", origin.descriptor); - else delete slot.pos; -} - -function contractSlots(node, contract) { - return contract.direction === "input" ? node?.inputs : node?.outputs; -} - -function isWidgetBackedInput(slot) { - return Boolean(slot?.widget && typeof slot.widget === "object"); -} - -function contractPortEntries(node, contract) { - const slots = contractSlots(node, contract); - if (!Array.isArray(slots)) return null; - return slots - .map((slot, index) => ({ slot, index })) - .filter(({ slot }) => contract.direction !== "input" || !isWidgetBackedInput(slot)); -} - -export function validateCanaryContract(node, nodeName) { - const contract = CONTRACTS.get(nodeName); - if (!contract) return { ok: false, status: "IGNORED", reason: "not a canary node" }; - const entries = contractPortEntries(node, contract); - if (!entries || entries.length !== contract.count) { - return { ok: false, status: "BLOCKED", reason: `${contract.direction}s must contain exactly ${contract.count} connectable slots` }; - } - const providerEntry = entries[contract.providerIndex]; - const provider = providerEntry?.slot; - if (providerEntry?.index !== contract.providerIndex) { - return { ok: false, status: "BLOCKED", reason: `provider graph index mismatch at ${contract.direction}[${providerEntry?.index}]` }; - } - if (provider?.name !== contract.providerName || provider?.type !== CANARY_PROVIDER_TYPE) { - return { ok: false, status: "BLOCKED", reason: `provider contract mismatch at ${contract.direction}[${contract.providerIndex}]` }; - } - const slots = entries.map(({ slot }) => slot); - const slotIndexes = entries.map(({ index }) => index); - const matches = slots.filter(slot => slot?.name === contract.providerName && slot?.type === CANARY_PROVIDER_TYPE); - if (matches.length !== 1) return { ok: false, status: "BLOCKED", reason: "provider must occur exactly once" }; - return { ok: true, status: "READY", contract, slots, slotIndexes, provider, providerArrayIndex: providerEntry.index }; -} - -function graphLink(graph, linkId) { - if (linkId == null) return null; - if (graph?._links?.get) return graph._links.get(linkId) ?? null; - if (graph?.links?.get) return graph.links.get(linkId) ?? null; - return graph?.links?.[linkId] ?? graph?._links?.[linkId] ?? null; -} - -function providerLinkTuples(node, validated) { - if (!validated.ok) return []; - const linkIds = validated.contract.direction === "input" - ? (validated.provider.link == null ? [] : [validated.provider.link]) - : [...(validated.provider.links ?? [])]; - return linkIds.map(id => { - const link = graphLink(node?.graph, id); - return link ? { - id, - origin_id: link.origin_id, - origin_slot: Number(link.origin_slot), - target_id: link.target_id, - target_slot: Number(link.target_slot), - type: String(link.type ?? ""), - } : { id, missing: true }; - }); -} - -function captureStructure(node, validated) { - return { - inputs: node.inputs, - outputs: node.outputs, - widgets: node.widgets, - inputSlots: [...(node.inputs ?? [])], - outputSlots: [...(node.outputs ?? [])], - widgetObjects: [...(node.widgets ?? [])], - inputShape: (node.inputs ?? []).map(slot => [slot?.name, slot?.type]), - outputShape: (node.outputs ?? []).map(slot => [slot?.name, slot?.type]), - providerIndex: validated.contract.providerIndex, - links: providerLinkTuples(node, validated), - }; -} - -function sameReferences(left, right) { - return left.length === right.length && left.every((value, index) => value === right[index]); -} - -function sameJson(left, right) { - return JSON.stringify(left) === JSON.stringify(right); -} - -function compareStructure(node, validated, baseline) { - if (!baseline) return { ok: false, reason: "baseline not captured" }; - const current = captureStructure(node, validated); - const checks = { - inputArray: current.inputs === baseline.inputs, - outputArray: current.outputs === baseline.outputs, - widgetArray: current.widgets === baseline.widgets, - inputSlots: sameReferences(current.inputSlots, baseline.inputSlots), - outputSlots: sameReferences(current.outputSlots, baseline.outputSlots), - widgets: sameReferences(current.widgetObjects, baseline.widgetObjects), - inputShape: sameJson(current.inputShape, baseline.inputShape), - outputShape: sameJson(current.outputShape, baseline.outputShape), - providerIndex: current.providerIndex === baseline.providerIndex, - linkTuples: sameJson(current.links, baseline.links), - }; - return { ok: Object.values(checks).every(Boolean), checks }; -} - -function ensureState(node, validated) { - let state = nodeStates.get(node); - if (!state) { - state = { slot: validated.provider, positionOrigin: capturePositionOrigin(validated.provider), baseline: null, mode: "Native" }; - nodeStates.set(node, state); - } else if (state.slot !== validated.provider) { - restorePosition(state.slot, state.positionOrigin); - state.slot = validated.provider; - state.positionOrigin = capturePositionOrigin(validated.provider); - state.baseline = null; - state.mode = "Native"; - } - return state; -} - -export function captureCanaryBaseline(node, nodeName) { - const validated = validateCanaryContract(node, nodeName); - if (!validated.ok) return validated; - const state = ensureState(node, validated); - if (state.mode !== "Native") return { ok: false, status: "BLOCKED", reason: "capture baseline in Native mode" }; - state.baseline = captureStructure(node, validated); - return { ok: true, status: "BASELINE_CAPTURED", comparison: compareStructure(node, validated, state.baseline) }; -} - -function expectedAnchor(node, validated, localPosition) { - return [Number(node?.pos?.[0] ?? 0) + localPosition[0], Number(node?.pos?.[1] ?? 0) + localPosition[1]]; -} - -function currentAnchor(node, validated) { - if (typeof node?.getSlotPosition !== "function") return null; - const point = node.getSlotPosition(validated.providerArrayIndex, validated.contract.direction === "input"); - return point ? [Number(point[0]), Number(point[1])] : null; -} - -function legacyCurrentAnchor(node, validated) { - if (typeof node?.getConnectionPos !== "function") return null; - const point = node.getConnectionPos(validated.contract.direction === "input", validated.providerArrayIndex); - return point ? [Number(point[0]), Number(point[1])] : null; -} - -function pointsMatch(left, right) { - return Boolean(left && right && Math.abs(left[0] - right[0]) <= 0.5 && Math.abs(left[1] - right[1]) <= 0.5); -} - -function dirty(node) { - node?.setDirtyCanvas?.(true, true); - node?.graph?.setDirtyCanvas?.(true, true); -} - -export function applyCanaryMode(node, nodeName, requestedMode, options = {}) { - const validated = validateCanaryContract(node, nodeName); - if (!validated.ok) { - restoreCanaryNode(node); - return validated; - } - const state = ensureState(node, validated); - const mode = CANARY_MODES.includes(requestedMode) ? requestedMode : "Native"; - if (mode === "Native") { - restorePosition(state.slot, state.positionOrigin); - state.mode = "Native"; - dirty(node); - return { ok: true, status: "NATIVE", mode, comparison: compareStructure(node, validated, state.baseline) }; - } - if (options.nodes2Active === true) { - restorePosition(state.slot, state.positionOrigin); - state.mode = "Native"; - dirty(node); - return { ok: false, status: "UNSUPPORTED_SURFACE", reason: "Nodes 2.0 is outside this Classic-only Canary" }; - } - - const original = ownDescriptor(state.slot, "pos"); - if (original && original.configurable === false && original !== state.positionOrigin.descriptor) { - return { ok: false, status: "BLOCKED", reason: "provider pos is non-configurable" }; - } - const y = MODE_Y.get(mode); - const localPosition = [validated.contract.direction === "input" ? 0 : Number(node?.size?.[0] ?? 0), y]; - try { - Object.defineProperty(state.slot, "pos", { - value: localPosition, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (error) { - restorePosition(state.slot, state.positionOrigin); - state.mode = "Native"; - return { ok: false, status: "BLOCKED", reason: `provider pos cannot be projected: ${String(error)}` }; - } - dirty(node); - const expected = expectedAnchor(node, validated, localPosition); - const actual = currentAnchor(node, validated); - const legacyActual = legacyCurrentAnchor(node, validated); - if (!pointsMatch(expected, actual)) { - restorePosition(state.slot, state.positionOrigin); - state.mode = "Native"; - dirty(node); - return { ok: false, status: "UNSUPPORTED_SURFACE", reason: "unified slot renderer did not use provider slot.pos", expected, actual, legacyActual }; - } - state.mode = mode; - return { ok: true, status: "PROJECTED", mode, expected, actual, legacyActual, comparison: compareStructure(node, validated, state.baseline) }; -} - -function slotPositionSummary(slot) { - const descriptor = ownDescriptor(slot, "pos"); - return { - own: Boolean(descriptor), - enumerable: descriptor?.enumerable ?? null, - value: slot?.pos == null ? null : [...slot.pos], - }; -} - -function serializeProbe(node, validated) { - try { - const serialized = node?.serialize?.(); - const slots = validated.contract.direction === "input" ? serialized?.inputs : serialized?.outputs; - const provider = slots?.[validated.providerArrayIndex]; - return { - available: Boolean(serialized), - providerHasOwnPos: Boolean(provider && Object.hasOwn(provider, "pos")), - providerPos: provider?.pos ?? null, - }; - } catch (error) { - return { available: false, error: String(error) }; - } -} - -function measuredSlots(node, validated) { - if (typeof node?.getSlotPosition !== "function") return []; - return validated.slots.map((slot, ordinal) => { - const index = validated.slotIndexes[ordinal]; - const point = node.getSlotPosition(index, validated.contract.direction === "input"); - return { index, name: slot?.name, x: Number(point?.[0]), y: Number(point?.[1]) }; - }); -} - -function legacyMeasuredSlots(node, validated) { - if (typeof node?.getConnectionPos !== "function") return []; - return validated.slots.map((slot, ordinal) => { - const index = validated.slotIndexes[ordinal]; - const point = node.getConnectionPos(validated.contract.direction === "input", index); - return { index, name: slot?.name, x: Number(point?.[0]), y: Number(point?.[1]) }; - }); -} - -export function inspectCanaryNode(node, nodeName) { - const validated = validateCanaryContract(node, nodeName); - if (!validated.ok) { - restoreCanaryNode(node); - return validated; - } - const state = ensureState(node, validated); - let computedSize = null; - try { computedSize = node?.computeSize?.() ?? null; } catch { computedSize = null; } - return { - ok: true, - status: state.mode === "Native" ? "NATIVE" : "PROJECTED", - mode: state.mode, - direction: validated.contract.direction, - canonicalProviderIndex: validated.contract.providerIndex, - canonicalSlotCount: validated.slots.length, - providerPosition: slotPositionSummary(validated.provider), - links: providerLinkTuples(node, validated), - comparison: compareStructure(node, validated, state.baseline), - measuredSlots: measuredSlots(node, validated), - legacyMeasuredSlots: legacyMeasuredSlots(node, validated), - widgetMetrics: (node.widgets ?? []).map(widget => ({ name: widget?.name, y: widget?.y ?? null, last_y: widget?.last_y ?? null })), - nodeSize: node?.size ? [...node.size] : null, - computedSize: computedSize ? [...computedSize] : null, - serialization: serializeProbe(node, validated), - }; -} - -export function scheduleCanaryInspection(node, nodeName, publish, scheduleFrame = globalThis.requestAnimationFrame) { - if (typeof scheduleFrame !== "function") return 0; - const revision = (inspectionRevisions.get(node) ?? 0) + 1; - inspectionRevisions.set(node, revision); - scheduleFrame(() => scheduleFrame(() => { - if (inspectionRevisions.get(node) !== revision) return; - publish(inspectCanaryNode(node, nodeName)); - })); - return revision; -} - -export function restoreCanaryNode(node) { - const state = nodeStates.get(node); - if (!state) return false; - restorePosition(state.slot, state.positionOrigin); - nodeStates.delete(node); - inspectionRevisions.set(node, (inspectionRevisions.get(node) ?? 0) + 1); - dirty(node); - return true; -} diff --git a/py/nodes/bv_titlebar_port_canary.py b/py/nodes/bv_titlebar_port_canary.py deleted file mode 100644 index 97da36e..0000000 --- a/py/nodes/bv_titlebar_port_canary.py +++ /dev/null @@ -1,106 +0,0 @@ -"""THROW AWAY - DO NOT MERGE OR RELEASE. - -Branch-only ComfyUI nodes for the titlebar provider-port capability canary. -The module itself is the opt-in boundary: production branches must not contain -this throw-away file. -""" - -from __future__ import annotations - -CATEGORY = "BV Node Pack/Canary (THROW AWAY)" -PROVIDER_TYPE = "BV_RUNTIME_RESOURCE_PROVIDER" -SENDER_NAME = "BV Titlebar Port Canary Sender (THROW AWAY)" -RECEIVER_NAME = "BV Titlebar Port Canary Receiver (THROW AWAY)" -PRESENTATION_MODES = ( - "Native", - "A - Title midline", - "B - Body seam", - "C - Top rail", -) - - -class BVTitlebarPortCanarySender: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "presentation_mode": (PRESENTATION_MODES, {"default": "Native"}), - "canary_note": ( - "STRING", - {"default": "Connect provider output 2 to receiver input 3", "multiline": False}, - ), - } - } - - RETURN_TYPES = ( - "BV_CANARY_ALPHA", - "BV_CANARY_BETA", - PROVIDER_TYPE, - "BV_CANARY_GAMMA", - "BV_CANARY_DELTA", - "BV_CANARY_EPSILON", - ) - RETURN_NAMES = ("alpha", "beta", "resource_provider", "gamma", "delta", "epsilon") - FUNCTION = "send" - CATEGORY = CATEGORY - DESCRIPTION = "THROW AWAY sender: provider is canonical output index 2 of 6." - - def send(self, presentation_mode="Native", canary_note=""): - shared = {"mode": presentation_mode, "note": canary_note} - return tuple({**shared, "channel": name} for name in self.RETURN_NAMES) - - -class BVTitlebarPortCanaryReceiver: - @classmethod - def INPUT_TYPES(cls): - socket = {"forceInput": True} - return { - "required": { - "alpha": ("BV_CANARY_ALPHA", socket), - "beta": ("BV_CANARY_BETA", socket), - "gamma": ("BV_CANARY_GAMMA", socket), - "resource_provider": (PROVIDER_TYPE, socket), - "delta": ("BV_CANARY_DELTA", socket), - "epsilon": ("BV_CANARY_EPSILON", socket), - "presentation_mode": (PRESENTATION_MODES, {"default": "Native"}), - "canary_note": ( - "STRING", - {"default": "Provider is canonical input index 3", "multiline": False}, - ), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("canary_result",) - FUNCTION = "receive" - CATEGORY = CATEGORY - DESCRIPTION = "THROW AWAY receiver: provider is canonical input index 3 of 6." - - def receive( - self, - alpha, - beta, - gamma, - resource_provider, - delta, - epsilon, - presentation_mode="Native", - canary_note="", - ): - channels = (alpha, beta, gamma, resource_provider, delta, epsilon) - return ( - f"mode={presentation_mode}; note={canary_note}; channels={len(channels)}; " - "provider=sender[2]->receiver[3]", - ) - - -_CANARY_CLASS_MAPPINGS = { - SENDER_NAME: BVTitlebarPortCanarySender, - RECEIVER_NAME: BVTitlebarPortCanaryReceiver, -} -_CANARY_DISPLAY_NAME_MAPPINGS = { - SENDER_NAME: SENDER_NAME, - RECEIVER_NAME: RECEIVER_NAME, -} -NODE_CLASS_MAPPINGS = dict(_CANARY_CLASS_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS = dict(_CANARY_DISPLAY_NAME_MAPPINGS) diff --git a/pyproject.toml b/pyproject.toml index d3e5286..4c14d89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bv_nodepack" -version = "1.2.1" +version = "1.2.2" description = "Visual regional prompting with sequential regional detailer plans, detector-aware Impact integration, regional LoRA hooks, native composition modes and model-specific attention backends, plus Smart Pipes and ComfyUI workflow tools." authors = [ {name = "BlackVortex", email = "blackvortexx@proton.me"} diff --git a/tests/test_titlebar_provider_port_runtime_canary.py b/tests/test_titlebar_provider_port_runtime_canary.py deleted file mode 100644 index 48fa5db..0000000 --- a/tests/test_titlebar_provider_port_runtime_canary.py +++ /dev/null @@ -1,62 +0,0 @@ -import importlib.util -import os -from pathlib import Path -import unittest -from unittest import mock - - -ROOT = Path(__file__).parents[1] -MODULE_PATH = ROOT / "py" / "nodes" / "bv_titlebar_port_canary.py" - - -def load_canary(value): - name = f"bv_titlebar_port_canary_test_{value!r}".replace(" ", "_") - spec = importlib.util.spec_from_file_location(name, MODULE_PATH) - module = importlib.util.module_from_spec(spec) - environment = {} if value is None else {"BV_TITLEBAR_PORT_CANARY": value} - with mock.patch.dict(os.environ, environment, clear=True): - spec.loader.exec_module(module) - return module - - -class TitlebarProviderPortRuntimeCanaryTests(unittest.TestCase): - def test_branch_module_registers_without_hidden_environment_gate(self): - for value in (None, "", "0", "true", "TRUE", " 1", "1 ", "1"): - with self.subTest(value=value): - module = load_canary(value) - self.assertEqual( - set(module.NODE_CLASS_MAPPINGS), - {module.SENDER_NAME, module.RECEIVER_NAME}, - ) - self.assertEqual(set(module.NODE_DISPLAY_NAME_MAPPINGS), set(module.NODE_CLASS_MAPPINGS)) - - def test_sender_and_receiver_keep_provider_at_distinct_middle_indices(self): - module = load_canary("1") - sender = module.BVTitlebarPortCanarySender - receiver = module.BVTitlebarPortCanaryReceiver - - self.assertEqual(len(sender.RETURN_TYPES), 6) - self.assertEqual(sender.RETURN_TYPES[2], module.PROVIDER_TYPE) - self.assertEqual(sender.RETURN_NAMES[2], "resource_provider") - - required = receiver.INPUT_TYPES()["required"] - socket_names = list(required)[:6] - self.assertEqual(socket_names, ["alpha", "beta", "gamma", "resource_provider", "delta", "epsilon"]) - self.assertEqual(required["resource_provider"][0], module.PROVIDER_TYPE) - self.assertEqual(required["resource_provider"][1]["forceInput"], True) - self.assertEqual(list(required)[3], "resource_provider") - self.assertEqual(list(required)[6:], ["presentation_mode", "canary_note"]) - - def test_nodes_are_executable_without_reordering_channel_contract(self): - module = load_canary("1") - payloads = module.BVTitlebarPortCanarySender().send("Native", "baseline") - self.assertEqual(len(payloads), 6) - self.assertEqual(payloads[2]["channel"], "resource_provider") - result = module.BVTitlebarPortCanaryReceiver().receive( - payloads[0], payloads[1], payloads[3], payloads[2], payloads[4], payloads[5], "Native", "baseline" - ) - self.assertIn("sender[2]->receiver[3]", result[0]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/titlebar_provider_port_canary.test.mjs b/tests/titlebar_provider_port_canary.test.mjs deleted file mode 100644 index a0322fb..0000000 --- a/tests/titlebar_provider_port_canary.test.mjs +++ /dev/null @@ -1,105 +0,0 @@ -import assert from"node:assert/strict"; -import{readFileSync}from"node:fs"; -import test from"node:test"; -import{CANARY_VARIANTS,createSlotOrderFixture,createTitlebarProviderPortEvidence,createTitlebarProviderPortFixture,projectCanonicalNodeRows,projectTitlebarProviderPort,verifyTitlebarProviderPortEvidence}from"../ui/src/prototypes/titlebarProviderPortCanary.ts"; - -test("three variants project deterministic anchors while leaving fixture sizes untouched",()=>{ - const fixture=createTitlebarProviderPortFixture(); - const expectedY={A:[165,130],B:[180,145],C:[150,115]}; - assert.deepEqual(CANARY_VARIANTS.map(item=>item.key),["A","B","C"]); - for(const variant of CANARY_VARIANTS.map(item=>item.key)){ - const evidence=createTitlebarProviderPortEvidence(variant,fixture); - assert.deepEqual(evidence.output.anchor,{x:320,y:expectedY[variant][0]}); - assert.deepEqual(evidence.input.anchor,{x:560,y:expectedY[variant][1]}); - assert.deepEqual(evidence.output.localAnchor,{x:240,y:expectedY[variant][0]-180}); - assert.deepEqual(evidence.input.localAnchor,{x:0,y:expectedY[variant][1]-145}); - assert.deepEqual(evidence.nodeSizes,{registry:[240,150],consumer:[280,190]}); - assert.equal(evidence.output.canonicalSlotIndex,2);assert.equal(evidence.input.canonicalSlotIndex,1); - assert.deepEqual(verifyTitlebarProviderPortEvidence(evidence),{endpointMatchesAnchor:true,expectedHitboxCentersAnchor:true,canonicalSlotIndicesPreserved:true}); - } -}); - -test("projection is inspect-only and preserves slot, link and object identity",()=>{ - const fixture=createTitlebarProviderPortFixture(),before=JSON.stringify(fixture),registry=fixture.registry,output=fixture.registryOutput,consumer=fixture.consumer,input=fixture.consumerInput,link=fixture.link; - for(const variant of CANARY_VARIANTS.map(item=>item.key)){ - projectTitlebarProviderPort(registry,output,variant); - projectTitlebarProviderPort(consumer,input,variant); - } - assert.equal(JSON.stringify(fixture),before); - assert.equal(fixture.registry,registry);assert.equal(fixture.registryOutput,output);assert.equal(fixture.consumer,consumer);assert.equal(fixture.consumerInput,input);assert.equal(fixture.link,link); - assert.deepEqual(fixture.registryOutput,{name:"resource_provider",type:"BV_RUNTIME_RESOURCE_PROVIDER",direction:"output",index:2,links:[9001]}); - assert.deepEqual(fixture.consumerInput,{name:"resource_provider_1",type:"BV_RUNTIME_RESOURCE_PROVIDER",direction:"input",index:1,link:9001}); - assert.deepEqual(fixture.link,{id:9001,type:"BV_RUNTIME_RESOURCE_PROVIDER",origin_id:101,origin_slot:2,target_id:202,target_slot:1}); -}); - -test("unlinked provider slots use the same pure geometry without inventing a link",()=>{ - const fixture=createTitlebarProviderPortFixture(false),before=JSON.stringify(fixture); - assert.deepEqual(fixture.registryOutput.links,[]);assert.equal(fixture.consumerInput.link,null);assert.equal(fixture.link,null); - for(const variant of CANARY_VARIANTS.map(item=>item.key)){ - const evidence=createTitlebarProviderPortEvidence(variant,fixture); - assert.equal(evidence.link,null); - assert.deepEqual(verifyTitlebarProviderPortEvidence(evidence),{endpointMatchesAnchor:true,expectedHitboxCentersAnchor:true,canonicalSlotIndicesPreserved:true}); - } - assert.equal(JSON.stringify(fixture),before); -}); - -test("provider slots keep canonical first middle or last indices while visual body rows compact",()=>{ - for(const direction of["input","output"]){ - for(const position of["first","middle","last"]){ - for(const linked of[false,true]){ - const fixture=createSlotOrderFixture(direction,position,linked),before=JSON.stringify(fixture),inputs=fixture.inputs,outputs=fixture.outputs,widgets=fixture.widgets,links=fixture.links,inputRefs=[...inputs],outputRefs=[...outputs],widgetRefs=[...widgets],linkRefs=[...links]; - const projection=projectCanonicalNodeRows(fixture),selected=direction==="input"?projection.inputs:projection.outputs,other=direction==="input"?projection.outputs:projection.inputs,canonical=direction==="input"?inputs:outputs,providerIndex={first:0,middle:1,last:2}[position],provider=canonical[providerIndex]; - - assert.equal(JSON.stringify(fixture),before,`${direction}/${position}/${linked}: serializable fixture changed`); - assert.equal(fixture.inputs,inputs);assert.equal(fixture.outputs,outputs);assert.equal(fixture.widgets,widgets);assert.equal(fixture.links,links); - inputRefs.forEach((slot,index)=>assert.equal(fixture.inputs[index],slot));outputRefs.forEach((slot,index)=>assert.equal(fixture.outputs[index],slot));widgetRefs.forEach((widget,index)=>assert.equal(fixture.widgets[index],widget));linkRefs.forEach((link,index)=>assert.equal(fixture.links[index],link)); - - assert.equal(selected.titlebarSlots.length,1);assert.equal(selected.titlebarSlots[0].slot,provider);assert.equal(selected.titlebarSlots[0].canonicalSlotIndex,providerIndex);assert.equal(selected.titlebarSlots[0].titlebarVisualOrdinal,0); - assert.deepEqual(selected.bodyRows.map(row=>row.visualRowIndex),[0,1]); - assert.deepEqual(selected.bodyRows.map(row=>row.canonicalSlotIndex),[0,1,2].filter(index=>index!==providerIndex)); - assert.deepEqual(selected.projectedSlots.map(item=>item.placement),[0,1,2].map(index=>index===providerIndex?"title":"body")); - assert.deepEqual(selected.projectedSlots.map(item=>item.visualOrdinal),[0,1,2].map(index=>index===providerIndex?null:index-(index>providerIndex?1:0))); - assert.equal(new Set(selected.bodyRows.map(row=>row.slot)).size,2);assert.equal(selected.bodyRows.some(row=>row.slot===provider),false); - assert.equal(other.titlebarSlots.length,0);assert.deepEqual(other.bodyRows.map(row=>row.visualRowIndex),[0,1]); - assert.equal(projection.widgetStartRow,2);assert.equal(projection.widgets.length,1);assert.equal(projection.widgets[0].widget,widgets[0]);assert.equal(projection.widgets[0].visualRowIndex,2); - - assert.equal(Object.hasOwn(provider,"hidden"),false);assert.equal(Object.hasOwn(provider,"label"),false);assert.equal(Object.hasOwn(provider,"localized_name"),false);assert.equal(Object.hasOwn(provider,"pos"),false); - if(linked){assert.equal(links.length,1);assert.equal(direction==="input"?links[0].target_slot:links[0].origin_slot,providerIndex)}else assert.equal(links.length,0); - } - } - } -}); - -test("multiple interleaved providers compact independently on both sides without touching link indices",()=>{ - const inputs=[{name:"image",type:"IMAGE",link:null},{name:"resource_provider_1",type:"BV_RUNTIME_RESOURCE_PROVIDER",link:8101,titlebarProvider:true},{name:"resource_provider_2",type:"BV_RUNTIME_RESOURCE_PROVIDER",link:null,titlebarProvider:true},{name:"mask",type:"MASK",link:null}],outputs=[{name:"image",type:"IMAGE",links:null},{name:"resource_provider_a",type:"BV_RUNTIME_RESOURCE_PROVIDER",links:null,titlebarProvider:true},{name:"resource_provider_b",type:"BV_RUNTIME_RESOURCE_PROVIDER",links:[8102],titlebarProvider:true},{name:"summary",type:"STRING",links:null}],widgets=[{name:"config_json",type:"text",value:"{}"}],links=[{id:8101,type:"BV_RUNTIME_RESOURCE_PROVIDER",origin_id:1,origin_slot:0,target_id:2,target_slot:1},{id:8102,type:"BV_RUNTIME_RESOURCE_PROVIDER",origin_id:2,origin_slot:2,target_id:3,target_slot:0}],fixture={inputs,outputs,widgets,links},before=JSON.stringify(fixture),inputRefs=[...inputs],outputRefs=[...outputs],linkRefs=[...links]; - const projection=projectCanonicalNodeRows(fixture); - assert.deepEqual(projection.inputs.bodyRows.map(row=>[row.visualRowIndex,row.canonicalSlotIndex,row.slot.name]),[[0,0,"image"],[1,3,"mask"]]); - assert.deepEqual(projection.outputs.bodyRows.map(row=>[row.visualRowIndex,row.canonicalSlotIndex,row.slot.name]),[[0,0,"image"],[1,3,"summary"]]); - assert.deepEqual(projection.inputs.projectedSlots.map(item=>[item.placement,item.visualOrdinal]),[["body",0],["title",null],["title",null],["body",1]]); - assert.deepEqual(projection.outputs.projectedSlots.map(item=>[item.placement,item.visualOrdinal]),[["body",0],["title",null],["title",null],["body",1]]); - assert.deepEqual(projection.inputs.titlebarSlots.map(item=>[item.canonicalSlotIndex,item.titlebarVisualOrdinal]),[[1,0],[2,1]]);assert.deepEqual(projection.outputs.titlebarSlots.map(item=>[item.canonicalSlotIndex,item.titlebarVisualOrdinal]),[[1,0],[2,1]]); - assert.equal(JSON.stringify(fixture),before);inputRefs.forEach((slot,index)=>assert.equal(inputs[index],slot));outputRefs.forEach((slot,index)=>assert.equal(outputs[index],slot));linkRefs.forEach((link,index)=>assert.equal(links[index],link)); - assert.deepEqual(links.map(link=>[link.origin_slot,link.target_slot]),[[0,1],[2,0]]); -}); - -test("asymmetric sides place widgets after the larger compacted body count",()=>{ - const inputs=[{name:"a",type:"A",link:null},{name:"resource_provider",type:"BV_RUNTIME_RESOURCE_PROVIDER",link:null,titlebarProvider:true},{name:"b",type:"B",link:null},{name:"c",type:"C",link:null}],outputs=[{name:"d",type:"D",links:null},{name:"resource_provider",type:"BV_RUNTIME_RESOURCE_PROVIDER",links:[],titlebarProvider:true}],widgets=[{name:"w1",type:"text",value:"1"},{name:"w2",type:"text",value:"2"}],fixture={inputs,outputs,widgets,links:[]},before=JSON.stringify(fixture),widgetRefs=[...widgets]; - const projection=projectCanonicalNodeRows(fixture); - assert.equal(projection.inputs.bodyRowCount,3);assert.equal(projection.outputs.bodyRowCount,1);assert.equal(projection.widgetStartRow,3); - assert.deepEqual(projection.widgets.map(item=>[item.visualRowIndex,item.canonicalWidgetIndex,item.widget.name]),[[3,0,"w1"],[4,1,"w2"]]); - widgetRefs.forEach((widget,index)=>assert.equal(widgets[index],widget));assert.equal(JSON.stringify(fixture),before); -}); - -test("unlinked output null and empty-array forms survive projection exactly",()=>{ - for(const linksValue of[null,[]]){ - const provider={name:"resource_provider",type:"BV_RUNTIME_RESOURCE_PROVIDER",links:linksValue,titlebarProvider:true},fixture={inputs:[],outputs:[{name:"before",type:"A",links:null},provider,{name:"after",type:"B",links:null}],widgets:[],links:[]},before=JSON.stringify(fixture); - const projection=projectCanonicalNodeRows(fixture); - assert.equal(projection.outputs.titlebarSlots[0].slot,provider);assert.equal(provider.links,linksValue);assert.equal(JSON.stringify(fixture),before); - } -}); - -test("canary stays outside production entries and graph mutation APIs",()=>{ - const source=readFileSync(new URL("../ui/src/prototypes/titlebarProviderPortCanary.ts",import.meta.url),"utf8"),index=readFileSync(new URL("../ui/src/index.tsx",import.meta.url),"utf8"),showcase=readFileSync(new URL("../ui/src/showcase.tsx",import.meta.url),"utf8"),vite=readFileSync(new URL("../ui/vite.config.js",import.meta.url),"utf8"); - assert.doesNotMatch(index,/titlebarProviderPortCanary/);assert.doesNotMatch(showcase,/titlebarProviderPortCanary/);assert.doesNotMatch(vite,/titlebar-provider-port|titlebarProviderPortCanary/); - for(const forbidden of["addInput(","addOutput(","removeInput(","removeOutput(","connect(","disconnectInput(","disconnectOutput(","graphToPrompt","onSerialize","onConfigure",".pos=",".splice(",".sort("])assert.equal(source.includes(forbidden),false,`forbidden prototype API: ${forbidden}`); -}); diff --git a/tests/titlebar_provider_port_runtime_canary.test.mjs b/tests/titlebar_provider_port_runtime_canary.test.mjs deleted file mode 100644 index 724a645..0000000 --- a/tests/titlebar_provider_port_runtime_canary.test.mjs +++ /dev/null @@ -1,169 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import test from "node:test"; -import { - CANARY_RECEIVER, - CANARY_SENDER, - applyCanaryMode, - captureCanaryBaseline, - inspectCanaryNode, - restoreCanaryNode, - scheduleCanaryInspection, - validateCanaryContract, -} from "../js/bv_titlebar_port_canary_core.js"; - -const PROVIDER="BV_RUNTIME_RESOURCE_PROVIDER"; - -function graphFixture(){ - const link={id:42,origin_id:101,origin_slot:2,target_id:202,target_slot:3,type:PROVIDER}; - const graph={_links:new Map([[42,link]]),dirty:0,setDirtyCanvas(){this.dirty++}}; - return{graph,link}; -} - -function connectionPosition(node,input,index,ignorePos=false){ - const slots=input?node.inputs:node.outputs,slot=slots[index]; - if(!ignorePos&&slot.pos)return[node.pos[0]+slot.pos[0],node.pos[1]+slot.pos[1]]; - const ordinal=slots.slice(0,index).filter(candidate=>!candidate.pos).length; - return[node.pos[0]+(input?10:node.size[0]-10),node.pos[1]+(ordinal+0.7)*20]; -} - -function senderNode(graph,{provider=undefined,unifiedIgnorePos=false,legacyIgnorePos=false}={}){ - const outputs=[ - {name:"alpha",type:"BV_CANARY_ALPHA",links:[]}, - {name:"beta",type:"BV_CANARY_BETA",links:[]}, - provider??{name:"resource_provider",type:PROVIDER,links:[42]}, - {name:"gamma",type:"BV_CANARY_GAMMA",links:[]}, - {name:"delta",type:"BV_CANARY_DELTA",links:[]}, - {name:"epsilon",type:"BV_CANARY_EPSILON",links:[]}, - ]; - return fakeNode({id:101,graph,inputs:[],outputs,direction:"output",unifiedIgnorePos,legacyIgnorePos}); -} - -function receiverNode(graph,{provider=undefined,unifiedIgnorePos=false,legacyIgnorePos=false}={}){ - const inputs=[ - {name:"alpha",type:"BV_CANARY_ALPHA",link:null}, - {name:"beta",type:"BV_CANARY_BETA",link:null}, - {name:"gamma",type:"BV_CANARY_GAMMA",link:null}, - provider??{name:"resource_provider",type:PROVIDER,link:42}, - {name:"delta",type:"BV_CANARY_DELTA",link:null}, - {name:"epsilon",type:"BV_CANARY_EPSILON",link:null}, - {name:"presentation_mode",type:"COMBO",widget:{name:"presentation_mode"},link:null}, - {name:"canary_note",type:"STRING",widget:{name:"canary_note"},link:null}, - ]; - return fakeNode({id:202,graph,inputs,outputs:[{name:"canary_result",type:"STRING",links:[]}],direction:"input",unifiedIgnorePos,legacyIgnorePos}); -} - -function fakeNode({id,graph,inputs,outputs,direction,unifiedIgnorePos,legacyIgnorePos}){ - const widgets=[{name:"presentation_mode",value:"Native",y:140,last_y:160},{name:"canary_note",value:"baseline",y:164,last_y:184}]; - return{ - id,graph,inputs,outputs,widgets,pos:[100,200],size:[260,210],properties:{stable:true},dirty:0, - getSlotPosition(index,input){return connectionPosition(this,input,index,unifiedIgnorePos)}, - getConnectionPos(input,index){return connectionPosition(this,input,index,legacyIgnorePos)}, - setDirtyCanvas(){this.dirty++}, - computeSize(){return[260,210]}, - serialize(){return JSON.parse(JSON.stringify({id:this.id,inputs:this.inputs,outputs:this.outputs,properties:this.properties}))}, - }; -} - -function refs(node){return{inputs:node.inputs,outputs:node.outputs,widgets:node.widgets,inputSlots:[...node.inputs],outputSlots:[...node.outputs],widgetObjects:[...node.widgets],properties:node.properties}} -function assertRefs(node,before){assert.equal(node.inputs,before.inputs);assert.equal(node.outputs,before.outputs);assert.equal(node.widgets,before.widgets);assert.equal(node.properties,before.properties);before.inputSlots.forEach((slot,index)=>assert.equal(node.inputs[index],slot));before.outputSlots.forEach((slot,index)=>assert.equal(node.outputs[index],slot));before.widgetObjects.forEach((widget,index)=>assert.equal(node.widgets[index],widget))} - -test("exact contracts place sender provider at output 2 and receiver provider at input 3",()=>{ - const{graph}=graphFixture(),sender=senderNode(graph),receiver=receiverNode(graph); - assert.deepEqual(validateCanaryContract(sender,CANARY_SENDER).contract,{direction:"output",count:6,providerIndex:2,providerName:"resource_provider"}); - assert.deepEqual(validateCanaryContract(receiver,CANARY_RECEIVER).contract,{direction:"input",count:6,providerIndex:3,providerName:"resource_provider"}); - assert.equal(validateCanaryContract(sender,"BV LUT Registry").status,"IGNORED"); -}); - -test("receiver contract counts connectable ports and retains widget-backed input entries",()=>{ - const{graph}=graphFixture(),receiver=receiverNode(graph),before=refs(receiver),validated=validateCanaryContract(receiver,CANARY_RECEIVER); - assert.equal(receiver.inputs.length,8); - assert.equal(validated.ok,true); - assert.deepEqual(validated.slots.map(slot=>slot.name),["alpha","beta","gamma","resource_provider","delta","epsilon"]); - assert.deepEqual(validated.slotIndexes,[0,1,2,3,4,5]); - assert.equal(validated.providerArrayIndex,3); - assert.equal(captureCanaryBaseline(receiver,CANARY_RECEIVER).ok,true); - assert.equal(applyCanaryMode(receiver,CANARY_RECEIVER,"A - Title midline").ok,true); - assertRefs(receiver,before); - assert.deepEqual(receiver.inputs.slice(6).map(slot=>slot.widget?.name),["presentation_mode","canary_note"]); - assert.deepEqual(inspectCanaryNode(receiver,CANARY_RECEIVER).measuredSlots.map(slot=>slot.index),[0,1,2,3,4,5]); -}); - -test("receiver contract fails closed when widgets shift graph indexes or an extra port appears",()=>{ - const{graph}=graphFixture(),shifted=receiverNode(graph),extra=receiverNode(graph); - shifted.inputs.splice(2,0,{name:"early_widget",type:"STRING",widget:{name:"early_widget"},link:null}); - extra.inputs.splice(6,0,{name:"zeta",type:"BV_CANARY_ZETA",link:null}); - assert.match(validateCanaryContract(shifted,CANARY_RECEIVER).reason,/graph index mismatch/); - assert.match(validateCanaryContract(extra,CANARY_RECEIVER).reason,/exactly 6 connectable slots/); - assert.equal(shifted.inputs.some(slot=>Object.hasOwn(slot,"pos")),false); - assert.equal(extra.inputs.some(slot=>Object.hasOwn(slot,"pos")),false); -}); - -test("linked middle slots survive repeated Native A B C cycles while body ordinals compact",()=>{ - const{graph}=graphFixture(),sender=senderNode(graph),receiver=receiverNode(graph),senderRefs=refs(sender),receiverRefs=refs(receiver),senderBefore=JSON.stringify(sender.serialize()),receiverBefore=JSON.stringify(receiver.serialize()),senderSize=[...sender.size],receiverSize=[...receiver.size],senderWidgetMetrics=sender.widgets.map(widget=>[widget.y,widget.last_y]),receiverWidgetMetrics=receiver.widgets.map(widget=>[widget.y,widget.last_y]); - assert.equal(captureCanaryBaseline(sender,CANARY_SENDER).ok,true);assert.equal(captureCanaryBaseline(receiver,CANARY_RECEIVER).ok,true); - for(let cycle=0;cycle<3;cycle++)for(const mode of["A - Title midline","B - Body seam","C - Top rail","Native"]){ - const sendResult=applyCanaryMode(sender,CANARY_SENDER,mode),receiveResult=applyCanaryMode(receiver,CANARY_RECEIVER,mode); - assert.equal(sendResult.ok,true,`${cycle}/${mode}/sender`);assert.equal(receiveResult.ok,true,`${cycle}/${mode}/receiver`); - const send=inspectCanaryNode(sender,CANARY_SENDER),receive=inspectCanaryNode(receiver,CANARY_RECEIVER); - assert.equal(send.comparison.ok,true);assert.equal(receive.comparison.ok,true); - assert.deepEqual(send.links,[{id:42,origin_id:101,origin_slot:2,target_id:202,target_slot:3,type:PROVIDER}]); - assert.deepEqual(receive.links,send.links);assert.equal(send.serialization.providerHasOwnPos,false);assert.equal(receive.serialization.providerHasOwnPos,false); - assertRefs(sender,senderRefs);assertRefs(receiver,receiverRefs);assert.equal(JSON.stringify(sender.serialize()),senderBefore);assert.equal(JSON.stringify(receiver.serialize()),receiverBefore);assert.deepEqual(sender.size,senderSize);assert.deepEqual(receiver.size,receiverSize);assert.deepEqual(sender.widgets.map(widget=>[widget.y,widget.last_y]),senderWidgetMetrics);assert.deepEqual(receiver.widgets.map(widget=>[widget.y,widget.last_y]),receiverWidgetMetrics); - if(mode==="Native"){ - assert.equal(Object.hasOwn(sender.outputs[2],"pos"),false);assert.equal(Object.hasOwn(receiver.inputs[3],"pos"),false); - assert.deepEqual(send.measuredSlots.map(slot=>slot.y),[214,234,254,274,294,314]); - assert.deepEqual(receive.measuredSlots.map(slot=>slot.y),[214,234,254,274,294,314]); - }else{ - assert.equal(Object.getOwnPropertyDescriptor(sender.outputs[2],"pos").enumerable,false);assert.equal(Object.getOwnPropertyDescriptor(receiver.inputs[3],"pos").enumerable,false); - assert.deepEqual(send.measuredSlots.filter(slot=>slot.index!==2).map(slot=>slot.y),[214,234,254,274,294]); - assert.deepEqual(receive.measuredSlots.filter(slot=>slot.index!==3).map(slot=>slot.y),[214,234,254,274,294]); - } - } -}); - -test("unlinked providers retain null and empty-array forms",()=>{ - const{graph}=graphFixture(),sender=senderNode(graph),receiver=receiverNode(graph);sender.outputs[2].links=[];receiver.inputs[3].link=null;graph._links.clear(); - captureCanaryBaseline(sender,CANARY_SENDER);captureCanaryBaseline(receiver,CANARY_RECEIVER); - for(const mode of["A - Title midline","Native"]){applyCanaryMode(sender,CANARY_SENDER,mode);applyCanaryMode(receiver,CANARY_RECEIVER,mode);assert.deepEqual(inspectCanaryNode(sender,CANARY_SENDER).links,[]);assert.deepEqual(inspectCanaryNode(receiver,CANARY_RECEIVER).links,[]);assert.deepEqual(sender.outputs[2].links,[]);assert.equal(receiver.inputs[3].link,null)} -}); - -test("contract mismatches fail closed without touching slot positions",()=>{ - const{graph}=graphFixture(),wrongCount=senderNode(graph),wrongName=receiverNode(graph);wrongCount.outputs.pop();wrongName.inputs[3].name="wrong"; - assert.equal(applyCanaryMode(wrongCount,CANARY_SENDER,"A - Title midline").status,"BLOCKED");assert.equal(applyCanaryMode(wrongName,CANARY_RECEIVER,"A - Title midline").status,"BLOCKED"); - assert.equal(wrongCount.outputs.some(slot=>Object.hasOwn(slot,"pos")),false);assert.equal(wrongName.inputs.some(slot=>Object.hasOwn(slot,"pos")),false); -}); - -test("contract mismatch after projection restores the old slot and clears state",()=>{ - const{graph}=graphFixture(),node=senderNode(graph),provider=node.outputs[2];captureCanaryBaseline(node,CANARY_SENDER);assert.equal(applyCanaryMode(node,CANARY_SENDER,"A - Title midline").ok,true);assert.equal(Object.hasOwn(provider,"pos"),true);node.outputs=node.outputs.slice(0,5);const result=applyCanaryMode(node,CANARY_SENDER,"B - Body seam");assert.equal(result.status,"BLOCKED");assert.equal(Object.hasOwn(provider,"pos"),false);assert.equal(restoreCanaryNode(node),false);assert.equal(node.outputs.some(slot=>Object.hasOwn(slot,"pos")),false); -}); - -test("Native restores absent own and inherited pos states exactly",()=>{ - const{graph}=graphFixture(); - const ownProvider={name:"resource_provider",type:PROVIDER,links:[]};Object.defineProperty(ownProvider,"pos",{value:null,writable:false,configurable:true,enumerable:true}); - const own=senderNode(graph,{provider:ownProvider}),ownBefore=Object.getOwnPropertyDescriptor(ownProvider,"pos");captureCanaryBaseline(own,CANARY_SENDER);applyCanaryMode(own,CANARY_SENDER,"A - Title midline");applyCanaryMode(own,CANARY_SENDER,"Native");assert.deepEqual(Object.getOwnPropertyDescriptor(ownProvider,"pos"),ownBefore); - const inheritedValue=[7,8],inheritedProvider=Object.assign(Object.create({pos:inheritedValue}),{name:"resource_provider",type:PROVIDER,link:null}),inherited=receiverNode(graph,{provider:inheritedProvider});captureCanaryBaseline(inherited,CANARY_RECEIVER);applyCanaryMode(inherited,CANARY_RECEIVER,"B - Body seam");applyCanaryMode(inherited,CANARY_RECEIVER,"Native");assert.equal(Object.hasOwn(inheritedProvider,"pos"),false);assert.equal(inheritedProvider.pos,inheritedValue); - restoreCanaryNode(own);restoreCanaryNode(inherited); -}); - -test("surface that ignores slot pos fails closed and restores Native",()=>{ - const{graph}=graphFixture(),node=senderNode(graph,{unifiedIgnorePos:true,legacyIgnorePos:false});captureCanaryBaseline(node,CANARY_SENDER);const result=applyCanaryMode(node,CANARY_SENDER,"A - Title midline");assert.equal(result.ok,false);assert.equal(result.status,"UNSUPPORTED_SURFACE");assert.equal(Object.hasOwn(node.outputs[2],"pos"),false);assert.deepEqual(result.expected,[360,185]);assert.deepEqual(result.legacyActual,result.expected);assert.notDeepEqual(result.actual,result.expected); -}); - -test("explicit Nodes 2 mode fails before any pos mutation",()=>{ - const{graph}=graphFixture(),node=senderNode(graph);captureCanaryBaseline(node,CANARY_SENDER);const result=applyCanaryMode(node,CANARY_SENDER,"A - Title midline",{nodes2Active:true});assert.equal(result.ok,false);assert.equal(result.status,"UNSUPPORTED_SURFACE");assert.match(result.reason,/Nodes 2\.0/);assert.equal(Object.hasOwn(node.outputs[2],"pos"),false);assert.equal(inspectCanaryNode(node,CANARY_SENDER).mode,"Native"); -}); - -test("non-configurable provider pos fails closed without replacement",()=>{ - const{graph}=graphFixture(),provider={name:"resource_provider",type:PROVIDER,links:[]};Object.defineProperty(provider,"pos",{value:[9,10],writable:false,configurable:false,enumerable:true});const node=senderNode(graph,{provider}),before=Object.getOwnPropertyDescriptor(provider,"pos");captureCanaryBaseline(node,CANARY_SENDER);const result=applyCanaryMode(node,CANARY_SENDER,"A - Title midline");assert.equal(result.status,"BLOCKED");assert.deepEqual(Object.getOwnPropertyDescriptor(provider,"pos"),before); -}); - -test("post-frame inspection publishes native layout metrics and drops stale revisions",()=>{ - const{graph}=graphFixture(),node=senderNode(graph),frames=[],published=[];captureCanaryBaseline(node,CANARY_SENDER);applyCanaryMode(node,CANARY_SENDER,"A - Title midline");const schedule=callback=>frames.push(callback);scheduleCanaryInspection(node,CANARY_SENDER,report=>published.push(["stale",report]),schedule);node.widgets[0].y=90;node.size[1]=170;scheduleCanaryInspection(node,CANARY_SENDER,report=>published.push(["latest",report]),schedule);while(frames.length)frames.shift()();assert.equal(published.length,1);assert.equal(published[0][0],"latest");assert.equal(published[0][1].widgetMetrics[0].y,90);assert.deepEqual(published[0][1].nodeSize,[260,170]); -}); - -test("adapter is exact-name only and contains no structural graph mutation",()=>{ - const source=readFileSync(new URL("../js/bv_titlebar_port_canary.js",import.meta.url),"utf8"),core=readFileSync(new URL("../js/bv_titlebar_port_canary_core.js",import.meta.url),"utf8"); - assert.match(source,/NAMES\.has\(nodeData\?\.name\)/);assert.match(source,/THROW AWAY - DO NOT MERGE OR RELEASE/);assert.match(source,/mode\.serialize = false/);assert.match(source,/if \(!node\[INSTALLED\]\)/);assert.match(source,/globalThis\.LiteGraph\?\.vueNodesMode === true/); - for(const forbidden of["addInput(","addOutput(","removeInput(","removeOutput(","connect(","disconnectInput(","disconnectOutput(",".splice(",".sort(","onSerialize","graphToPrompt","slot.hidden","slot.label","setSize("]){assert.equal(source.includes(forbidden),false,`adapter forbidden: ${forbidden}`);assert.equal(core.includes(forbidden),false,`core forbidden: ${forbidden}`)} -}); diff --git a/ui/package-lock.json b/ui/package-lock.json index bd525d8..4b8510c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "bv_nodepack", - "version": "1.1.0", + "version": "1.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bv_nodepack", - "version": "1.1.0", + "version": "1.2.2", "dependencies": { "flexlayout-react": "0.10.5", "modern-screenshot": "4.7.0", diff --git a/ui/package.json b/ui/package.json index ac348a2..416268b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "bv_nodepack", "private": true, - "version": "1.1.0", + "version": "1.2.2", "type": "module", "scripts": { "build": "vite build", diff --git a/ui/prototypes/titlebar-provider-port-canary.html b/ui/prototypes/titlebar-provider-port-canary.html deleted file mode 100644 index 66b53aa..0000000 --- a/ui/prototypes/titlebar-provider-port-canary.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - BV Titlebar Provider-Port Canary - - - -
- - - diff --git a/ui/prototypes/titlebar-provider-port-runtime-canary.md b/ui/prototypes/titlebar-provider-port-runtime-canary.md deleted file mode 100644 index 84e8f25..0000000 --- a/ui/prototypes/titlebar-provider-port-runtime-canary.md +++ /dev/null @@ -1,65 +0,0 @@ -# Titlebar provider-port runtime canary — THROW AWAY - -**DO NOT MERGE OR RELEASE.** This branch-only prototype asks one narrow question: can current Classic ComfyUI use a provider slot's `pos` as a titlebar anchor while leaving the canonical slot arrays and link indices untouched and natively compacting the remaining body rows? - -## Activation boundary - -- While this branch-only module is present, Python always registers exactly two - node types; there is no environment-variable activation gate: - - `BV Titlebar Port Canary Sender (THROW AWAY)` - - `BV Titlebar Port Canary Receiver (THROW AWAY)` -- The JS extension targets only those two exact names and remains inert for every - production node. -- Use only a disposable, task-owned ComfyUI runtime and workflow. Do not save a user workflow with a projected slot until the serializer probe has been reviewed. - -## Surface boundary - -- The central product presentation supports Classic titlebar anchors and - suspends/restores them when the node transitions to or from Nodes 2.0. -- The throw-away Canary's own `A`/`B`/`C` adapter is deliberately Classic-only. - In Nodes 2.0 it must return fail-closed `UNSUPPORTED_SURFACE` before mutating - `pos`. This is an adapter capability check, not a claim that the central - product presentation lacks Nodes 2.0 lifecycle support. - -## Fixed graph contract - -- Sender: six outputs; `resource_provider` is canonical output index **2**. -- Receiver: six real socket inputs; `resource_provider` is canonical input index **3**. -- Connect all matching normal socket types if desired, then connect provider **2 → 3** in `Native` mode. -- Both nodes contain `presentation_mode` (`Native`, `A`, `B`, `C`), a note widget, a nonserialized baseline button and a nonserialized status widget. This instrumentation is installed before baseline capture and stays identity/order-stable; mode switches update only the status value, not widget geometry or objects. - -## Test sequence - -1. Start in `Native`, connect sender output 2 to receiver input 3, then press **Capture Native baseline** on both nodes. -2. Toggle both nodes through A → B → C → Native at least three times. -3. For each state record the status widget after its bounded two-frame refresh: canonical count/index, exact link tuple, structure identity, provider `pos`, serialized provider `pos`, unified `getSlotPosition` world-Y, separately labelled legacy `getConnectionPos` Y, widget Y, `node.size` and `computeSize()`. -4. Confirm draw endpoint, hitbox and drag at each title anchor. Connect/disconnect in every projected mode only after the baseline toggle sequence. -5. Save/reload only the disposable Canary workflow and compare node/workflow serialization. A persisted `pos` is a risk finding, not a success. -6. Repeat the throw-away adapter in Nodes 2.0 to confirm its explicit - `LiteGraph.vueNodesMode` guard returns fail-closed `UNSUPPORTED_SURFACE` - before any `pos` mutation. Separately verify that the central product - presentation suspends Classic anchors in Nodes 2.0 and restores them after - returning to Classic when debug remains enabled. - -The adapter deliberately does not change node size, `computeSize`, widget coordinates, labels, visibility, normal slot positions, arrays or links. Port compaction, widget movement and height changes must be reported as three separate observations. - -## Pre-runtime checks - -Run from the repository root: - -```powershell -python -m unittest discover -s tests -p 'test_titlebar_provider_port_runtime_canary.py' -v -& ui\node_modules\.bin\tsx.cmd --test tests\titlebar_provider_port_canary.test.mjs tests\titlebar_provider_port_runtime_canary.test.mjs -``` - -The isolated runtime export, activation, ComfyUI restart and browser workflow mutation are separate authorization gates and are intentionally not performed by these commands. - -## Removal checklist before any production implementation - -- Remove `py/nodes/bv_titlebar_port_canary.py`. -- Remove `js/bv_titlebar_port_canary.js` and `js/bv_titlebar_port_canary_core.js`. -- Remove both focused runtime-canary test files and this note. -- Remove or archive the earlier static Canary files on the throwaway branch. -- Rebuild the validated decision centrally in the shared presentation policy/adapters; do not promote this lifecycle code. -- Preserve the proven central Classic/Nodes 2.0 suspend-and-restore lifecycle - when removing the throw-away adapter. diff --git a/ui/src/prototypes/titlebarProviderPortCanary.ts b/ui/src/prototypes/titlebarProviderPortCanary.ts deleted file mode 100644 index f265733..0000000 --- a/ui/src/prototypes/titlebarProviderPortCanary.ts +++ /dev/null @@ -1,203 +0,0 @@ -// PROTOTYPE — THROW AWAY. Tests code-defined titlebar anchor geometry without touching ComfyUI graph state. - -export type CanaryVariant="A"|"B"|"C"; -export type CanaryDirection="input"|"output"; -export type Point=Readonly<{x:number;y:number}>; -export type Rect=Readonly<{x:number;y:number;width:number;height:number}>; - -export type CanaryNode=Readonly<{ - id:number; - type:"BV LUT Registry"|"BV Regional LUT Plan"; - x:number; - y:number; - width:number; - height:number; - titleHeight:number; -}>; - -type CanarySlotBase=Readonly<{ - name:"resource_provider"|"resource_provider_1"; - type:"BV_RUNTIME_RESOURCE_PROVIDER"; - direction:CanaryDirection; - index:number; -}>; - -export type CanaryOutputSlot=CanarySlotBase&Readonly<{ - direction:"output"; - links:readonly number[]|null; -}>; - -export type CanaryInputSlot=CanarySlotBase&Readonly<{ - direction:"input"; - link:number|null; -}>; - -export type CanarySlot=CanaryOutputSlot|CanaryInputSlot; - -export type CanaryLink=Readonly<{ - id:number; - type:"BV_RUNTIME_RESOURCE_PROVIDER"; - origin_id:number; - origin_slot:number; - target_id:number; - target_slot:number; -}>; - -export type CanaryFixture=Readonly<{ - registry:CanaryNode; - registryOutput:CanaryOutputSlot; - consumer:CanaryNode; - consumerInput:CanaryInputSlot; - link:CanaryLink|null; -}>; - -export type CanaryProviderPosition="first"|"middle"|"last"; -export type CanaryCanonicalInput=Readonly<{name:string;type:string;link:number|null;titlebarProvider?:boolean}>; -export type CanaryCanonicalOutput=Readonly<{name:string;type:string;links:readonly number[]|null;titlebarProvider?:boolean}>; -export type CanaryCanonicalWidget=Readonly<{name:string;type:string;value:string}>; -export type CanarySlotOrderFixture=Readonly<{ - inputs:readonly CanaryCanonicalInput[]; - outputs:readonly CanaryCanonicalOutput[]; - widgets:readonly CanaryCanonicalWidget[]; - links:readonly CanaryLink[]; -}>; - -type CanaryBodyRow=Readonly<{slot:TSlot;canonicalSlotIndex:number;visualRowIndex:number}>; -type CanaryTitlebarSlot=Readonly<{slot:TSlot;canonicalSlotIndex:number;titlebarVisualOrdinal:number}>; -type CanaryProjectedSlot=Readonly<{slot:TSlot;canonicalSlotIndex:number;placement:"body"|"title";visualOrdinal:number|null}>; - -function projectCanonicalDirection(slots:readonly TSlot[]){ - let nextVisualOrdinal=0,nextTitlebarVisualOrdinal=0; - const projectedSlots:CanaryProjectedSlot[]=slots.map((slot,canonicalSlotIndex)=>slot.titlebarProvider - ?{slot,canonicalSlotIndex,placement:"title",visualOrdinal:null} - :{slot,canonicalSlotIndex,placement:"body",visualOrdinal:nextVisualOrdinal++}); - const bodyRows:CanaryBodyRow[]=projectedSlots.filter(item=>item.placement==="body").map(item=>({slot:item.slot,canonicalSlotIndex:item.canonicalSlotIndex,visualRowIndex:item.visualOrdinal!})); - const titlebarSlots:CanaryTitlebarSlot[]=projectedSlots.filter(item=>item.placement==="title").map(item=>({slot:item.slot,canonicalSlotIndex:item.canonicalSlotIndex,titlebarVisualOrdinal:nextTitlebarVisualOrdinal++})); - return{projectedSlots,bodyRows,titlebarSlots,bodyRowCount:bodyRows.length} as const; -} - -export function projectCanonicalNodeRows(fixture:CanarySlotOrderFixture){ - const inputs=projectCanonicalDirection(fixture.inputs),outputs=projectCanonicalDirection(fixture.outputs),widgetStartRow=Math.max(inputs.bodyRowCount,outputs.bodyRowCount); - const widgets=fixture.widgets.map((widget,canonicalWidgetIndex)=>({widget,canonicalWidgetIndex,visualRowIndex:widgetStartRow+canonicalWidgetIndex})); - return{inputs,outputs,widgets,widgetStartRow} as const; -} - -function insertProvider(publicSlots:readonly T[],provider:T,position:CanaryProviderPosition):readonly T[]{ - if(position==="first")return[provider,...publicSlots]; - if(position==="middle")return[publicSlots[0],provider,...publicSlots.slice(1)]; - return[...publicSlots,provider]; -} - -export function createSlotOrderFixture(direction:CanaryDirection,position:CanaryProviderPosition,linked:boolean):CanarySlotOrderFixture{ - const providerIndex=position==="first"?0:position==="middle"?1:2,linkId=7301; - const publicInputs:readonly CanaryCanonicalInput[]=[{name:"regional_prompt",type:"BV_REGIONAL",link:null},{name:"mask",type:"MASK",link:null}]; - const publicOutputs:readonly CanaryCanonicalOutput[]=[{name:"lut_count",type:"INT",links:null},{name:"registry_summary",type:"STRING",links:null}]; - const providerInput:CanaryCanonicalInput={name:"resource_provider_1",type:"BV_RUNTIME_RESOURCE_PROVIDER",link:linked?linkId:null,titlebarProvider:true}; - const providerOutput:CanaryCanonicalOutput={name:"resource_provider",type:"BV_RUNTIME_RESOURCE_PROVIDER",links:linked?[linkId]:[],titlebarProvider:true}; - const inputs=direction==="input"?insertProvider(publicInputs,providerInput,position):publicInputs; - const outputs=direction==="output"?insertProvider(publicOutputs,providerOutput,position):publicOutputs; - const links:CanaryLink[]=linked?[{id:linkId,type:"BV_RUNTIME_RESOURCE_PROVIDER",origin_id:301,origin_slot:direction==="output"?providerIndex:0,target_id:302,target_slot:direction==="input"?providerIndex:0}]:[]; - return{inputs,outputs,widgets:[{name:"config_json",type:"text",value:"{}"}],links}; -} - -export type CanaryProjection=Readonly<{ - variant:CanaryVariant; - direction:CanaryDirection; - canonicalSlotIndex:number; - localAnchor:Point; - anchor:Point; - renderedEndpoint:Point; - expectedHitbox:Rect; -}>; - -export const CANARY_VARIANTS:ReadonlyArray>=[ - {key:"A",name:"Title midline",description:"Ports sit on the left and right edges at the visual centre of the titlebar."}, - {key:"B",name:"Body seam",description:"Ports sit where the titlebar meets the node body."}, - {key:"C",name:"Top rail",description:"Ports sit on the upper titlebar edge, furthest away from body rows."}, -]; - -export function createTitlebarProviderPortFixture(linked=true):CanaryFixture{ - const link:CanaryLink|null=linked?{id:9001,type:"BV_RUNTIME_RESOURCE_PROVIDER",origin_id:101,origin_slot:2,target_id:202,target_slot:1}:null; - return{ - registry:{id:101,type:"BV LUT Registry",x:80,y:180,width:240,height:150,titleHeight:30}, - registryOutput:{name:"resource_provider",type:"BV_RUNTIME_RESOURCE_PROVIDER",direction:"output",index:2,links:linked?[9001]:[]}, - consumer:{id:202,type:"BV Regional LUT Plan",x:560,y:145,width:280,height:190,titleHeight:30}, - consumerInput:{name:"resource_provider_1",type:"BV_RUNTIME_RESOURCE_PROVIDER",direction:"input",index:1,link:linked?9001:null}, - link, - }; -} - -function variantLocalY(titleHeight:number,variant:CanaryVariant){ - if(variant==="A")return-titleHeight/2; - if(variant==="B")return 0; - return-titleHeight; -} - -export function projectLocalTitlebarProviderPort(node:Pick,slot:CanarySlot,variant:CanaryVariant):Point{ - return{x:slot.direction==="input"?0:node.width,y:variantLocalY(node.titleHeight,variant)}; -} - -export function toWorldPoint(node:Pick,point:Point):Point{ - return{x:node.x+point.x,y:node.y+point.y}; -} - -export function projectTitlebarProviderPort(node:CanaryNode,slot:CanarySlot,variant:CanaryVariant):CanaryProjection{ - const localAnchor=projectLocalTitlebarProviderPort(node,slot,variant),anchor=toWorldPoint(node,localAnchor); - const hitSize=Math.min(20,node.titleHeight); - return{ - variant, - direction:slot.direction, - canonicalSlotIndex:slot.index, - localAnchor, - anchor, - renderedEndpoint:{...anchor}, - expectedHitbox:{x:anchor.x-hitSize/2,y:anchor.y-hitSize/2,width:hitSize,height:hitSize}, - }; -} - -export function createTitlebarProviderPortEvidence(variant:CanaryVariant,fixture=createTitlebarProviderPortFixture()){ - const output=projectTitlebarProviderPort(fixture.registry,fixture.registryOutput,variant); - const input=projectTitlebarProviderPort(fixture.consumer,fixture.consumerInput,variant); - return{variant,output,input,link:fixture.link?{...fixture.link}:null,nodeSizes:{registry:[fixture.registry.width,fixture.registry.height],consumer:[fixture.consumer.width,fixture.consumer.height]}} as const; -} - -function center(rect:Rect):Point{return{x:rect.x+rect.width/2,y:rect.y+rect.height/2}} -function equalPoint(a:Point,b:Point){return a.x===b.x&&a.y===b.y} - -export function verifyTitlebarProviderPortEvidence(evidence:ReturnType){ - const projections=[evidence.output,evidence.input]; - return{ - endpointMatchesAnchor:projections.every(item=>equalPoint(item.anchor,item.renderedEndpoint)), - expectedHitboxCentersAnchor:projections.every(item=>equalPoint(item.anchor,center(item.expectedHitbox))), - canonicalSlotIndicesPreserved:!evidence.link||(evidence.output.canonicalSlotIndex===evidence.link.origin_slot&&evidence.input.canonicalSlotIndex===evidence.link.target_slot), - } as const; -} - -function svgNode(node:CanaryNode,label:string,anchor:Point,direction:CanaryDirection){ - const titleY=node.y-node.titleHeight; - const port=``; - return`${label}${port}`; -} - -function linkPath(from:Point,to:Point){const span=Math.max(80,(to.x-from.x)*.45);return`M ${from.x} ${from.y} C ${from.x+span} ${from.y}, ${to.x-span} ${to.y}, ${to.x} ${to.y}`} - -export function renderTitlebarProviderPortCanary(root:HTMLElement,variant:CanaryVariant){ - const fixture=createTitlebarProviderPortFixture(),evidence=createTitlebarProviderPortEvidence(variant,fixture),verification=verifyTitlebarProviderPortEvidence(evidence),meta=CANARY_VARIANTS.find(item=>item.key===variant)!; - root.innerHTML=`
THROWAWAY PROTOTYPE

Titlebar provider-port canary

${variant} — ${meta.name} · ${meta.description}

${svgNode(fixture.registry,"BV LUT Registry",evidence.output.anchor,"output")}${svgNode(fixture.consumer,"BV Regional LUT Plan",evidence.input.anchor,"input")}

Full relevant state

${JSON.stringify({question:"Can code-defined provider anchors leave and enter through node titlebars without reserving body rows?",scope:"Pure geometry only; not native ComfyUI hit-testing, drag, Nodes 2.0, serialization or runtime evidence.",evidence,verification},null,2)}
`; -} - -export function installTitlebarProviderPortCanary(root:HTMLElement){ - const readVariant=()=>{const candidate=new URLSearchParams(location.search).get("variant")?.toUpperCase();return CANARY_VARIANTS.some(item=>item.key===candidate)?candidate as CanaryVariant:"A"}; - let variant=readVariant(); - const render=()=>{ - renderTitlebarProviderPortCanary(root,variant); - const selected=CANARY_VARIANTS.find(item=>item.key===variant)!; - const nav=document.createElement("nav");nav.className="switcher";nav.setAttribute("aria-label","Prototype variants"); - nav.innerHTML=`${selected.key} — ${selected.name}`; - const cycle=(step:number)=>{const index=CANARY_VARIANTS.findIndex(item=>item.key===variant);variant=CANARY_VARIANTS[(index+step+CANARY_VARIANTS.length)%CANARY_VARIANTS.length].key;const url=new URL(location.href);url.searchParams.set("variant",variant);history.replaceState({},"",url);render()}; - nav.querySelectorAll("button").forEach(button=>button.addEventListener("click",()=>cycle(Number(button.dataset.step)))); - root.append(nav); - }; - window.addEventListener("keydown",event=>{if(event.target instanceof HTMLElement&&event.target.matches("input,textarea,[contenteditable=true]"))return;if(event.key==="ArrowLeft"||event.key==="ArrowRight"){event.preventDefault();const index=CANARY_VARIANTS.findIndex(item=>item.key===variant),step=event.key==="ArrowRight"?1:-1;variant=CANARY_VARIANTS[(index+step+CANARY_VARIANTS.length)%CANARY_VARIANTS.length].key;const url=new URL(location.href);url.searchParams.set("variant",variant);history.replaceState({},"",url);render()}}); - render(); -}