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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
+1
-1
@@ -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"}
|
||||
|
||||
@@ -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()
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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}`)}
|
||||
});
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bv_nodepack",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BV Titlebar Provider-Port Canary</title>
|
||||
<style>
|
||||
:root{color-scheme:dark;font:14px/1.45 Inter,Segoe UI,sans-serif;background:#111214;color:#e9e7ee}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 50% 0,#24222c 0,#111214 55%);min-height:100vh}main{max-width:1180px;margin:auto;padding:34px 30px 110px}header{display:grid;gap:7px;margin-bottom:20px}h1,h2,p{margin:0}.prototype-badge{width:max-content;padding:4px 8px;border:1px solid #e4a35e;border-radius:999px;color:#ffd5a7;background:#3a2919;font-size:11px;font-weight:800;letter-spacing:.08em}svg{display:block;width:100%;border:1px solid #35343b;border-radius:12px;background:#17181b;box-shadow:0 18px 60px #0008}.grid{fill:url(#grid)}#grid path{fill:none;stroke:#25272a;stroke-width:1}.node-title{fill:#39383d;stroke:#4b4a50}.node-body{fill:#303035;stroke:#4b4a50}.node-label{fill:#d8d5dd;font-size:15px}.widget{fill:#222327;stroke:#59585e}.provider-link{fill:none;stroke:#a9bdb0;stroke-width:4;stroke-dasharray:10 7}.port{stroke:#151618;stroke-width:2}.port-output{fill:#a7b6cd}.port-input{fill:#a8c0ae}.hitbox{fill:#8ecaa522;stroke:#8ecaa588;stroke-dasharray:3 3}.evidence{margin-top:18px;padding:16px;border:1px solid #35343b;border-radius:10px;background:#18191d}.evidence h2{font-size:14px}.evidence pre{overflow:auto;max-height:330px;margin:10px 0 0;color:#b9c6bd;font:12px/1.45 Consolas,monospace}.switcher{position:fixed;left:50%;bottom:24px;display:flex;align-items:center;gap:14px;transform:translateX(-50%);padding:8px 10px;border:1px solid #5a5961;border-radius:999px;background:#24252aee;box-shadow:0 12px 38px #000a;backdrop-filter:blur(10px)}.switcher button{width:34px;height:34px;border:0;border-radius:50%;background:#3a3b43;color:#fff;font-size:20px;cursor:pointer}.switcher button:hover{background:#565864}.switcher strong{min-width:180px;text-align:center}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
import {installTitlebarProviderPortCanary} from "../src/prototypes/titlebarProviderPortCanary.ts";
|
||||
installTitlebarProviderPortCanary(document.querySelector("#app"));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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.
|
||||
@@ -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<TSlot>=Readonly<{slot:TSlot;canonicalSlotIndex:number;visualRowIndex:number}>;
|
||||
type CanaryTitlebarSlot<TSlot>=Readonly<{slot:TSlot;canonicalSlotIndex:number;titlebarVisualOrdinal:number}>;
|
||||
type CanaryProjectedSlot<TSlot>=Readonly<{slot:TSlot;canonicalSlotIndex:number;placement:"body"|"title";visualOrdinal:number|null}>;
|
||||
|
||||
function projectCanonicalDirection<TSlot extends{titlebarProvider?:boolean}>(slots:readonly TSlot[]){
|
||||
let nextVisualOrdinal=0,nextTitlebarVisualOrdinal=0;
|
||||
const projectedSlots:CanaryProjectedSlot<TSlot>[]=slots.map((slot,canonicalSlotIndex)=>slot.titlebarProvider
|
||||
?{slot,canonicalSlotIndex,placement:"title",visualOrdinal:null}
|
||||
:{slot,canonicalSlotIndex,placement:"body",visualOrdinal:nextVisualOrdinal++});
|
||||
const bodyRows:CanaryBodyRow<TSlot>[]=projectedSlots.filter(item=>item.placement==="body").map(item=>({slot:item.slot,canonicalSlotIndex:item.canonicalSlotIndex,visualRowIndex:item.visualOrdinal!}));
|
||||
const titlebarSlots:CanaryTitlebarSlot<TSlot>[]=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<T>(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<Readonly<{key:CanaryVariant;name:string;description:string}>>=[
|
||||
{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<CanaryNode,"width"|"titleHeight">,slot:CanarySlot,variant:CanaryVariant):Point{
|
||||
return{x:slot.direction==="input"?0:node.width,y:variantLocalY(node.titleHeight,variant)};
|
||||
}
|
||||
|
||||
export function toWorldPoint(node:Pick<CanaryNode,"x"|"y">,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<typeof createTitlebarProviderPortEvidence>){
|
||||
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=`<circle class="hitbox" cx="${anchor.x}" cy="${anchor.y}" r="10"/><circle class="port port-${direction}" cx="${anchor.x}" cy="${anchor.y}" r="6"/>`;
|
||||
return`<g><rect class="node-title" x="${node.x}" y="${titleY}" width="${node.width}" height="${node.titleHeight}" rx="10"/><rect class="node-body" x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}"/><text class="node-label" x="${node.x+18}" y="${titleY+20}">${label}</text><rect class="widget" x="${node.x+16}" y="${node.y+34}" width="${node.width-32}" height="28"/><rect class="widget" x="${node.x+16}" y="${node.y+74}" width="${node.width-32}" height="28"/>${port}</g>`;
|
||||
}
|
||||
|
||||
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=`<main><header><span class="prototype-badge">THROWAWAY PROTOTYPE</span><h1>Titlebar provider-port canary</h1><p><strong>${variant} — ${meta.name}</strong> · ${meta.description}</p></header><svg viewBox="0 75 920 300" role="img" aria-label="Registry and consumer connected through projected titlebar ports"><defs><pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse"><path d="M 20 0 L 0 0 0 20"/></pattern></defs><rect class="grid" x="0" y="75" width="920" height="300"/><path class="provider-link" d="${linkPath(evidence.output.anchor,evidence.input.anchor)}"/>${svgNode(fixture.registry,"BV LUT Registry",evidence.output.anchor,"output")}${svgNode(fixture.consumer,"BV Regional LUT Plan",evidence.input.anchor,"input")}</svg><section class="evidence"><h2>Full relevant state</h2><pre>${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)}</pre></section></main>`;
|
||||
}
|
||||
|
||||
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=`<button type="button" data-step="-1" aria-label="Previous variant">←</button><strong>${selected.key} — ${selected.name}</strong><button type="button" data-step="1" aria-label="Next variant">→</button>`;
|
||||
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<HTMLButtonElement>("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();
|
||||
}
|
||||
Reference in New Issue
Block a user