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=` ${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();
-}