Drop internal plan references from code comments
The pack cited internal planning-doc items (plan P3-7, decision D1, plan risk 9.2) that aren't public, leaving outside contributors with pointers they can't follow. Each comment already states its reason; only the dangling citation is removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cca9661dd1
commit
f8e5548b46
+15
-16
@@ -7,12 +7,12 @@ link ids, and slot indices across all of them — the kind of bookkeeping that i
|
||||
silently wrong until someone drags the file onto a canvas. This builds them from
|
||||
a description instead, so the ids are correct by construction.
|
||||
|
||||
Layout policy (plan P2-1): node positions are never guessed. Every node's height
|
||||
Layout policy: node positions are never guessed. Every node's height
|
||||
is estimated from its widget count, and nodes stack per column with a fixed
|
||||
vertical gap — so no template can ship with overlapping nodes at default sizes.
|
||||
A test asserts that invariant on the committed files.
|
||||
|
||||
Instruction policy (plan P2-2): user-facing instructions live in visible core
|
||||
Instruction policy: user-facing instructions live in visible core
|
||||
`Note` nodes on the canvas. `extra.styleref.notes` carries only a one-line
|
||||
provenance string — ComfyUI never renders it, so it must not carry anything a
|
||||
user needs to read.
|
||||
@@ -35,7 +35,7 @@ _PACK_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _PACK_ROOT not in sys.path:
|
||||
sys.path.insert(0, _PACK_ROOT)
|
||||
|
||||
# Single-sourced from the node itself (D4: the Facets output list is data-driven
|
||||
# Single-sourced from the node itself (the Facets output list is data-driven
|
||||
# and long — duplicating it here would guarantee drift).
|
||||
from styleref_nodes.facets import StyleRefFacets # noqa: E402
|
||||
|
||||
@@ -89,7 +89,7 @@ INPUTS = {
|
||||
"PreviewAny": [("source", "*")],
|
||||
}
|
||||
|
||||
# ── layout constants (plan P2-1) ─────────────────────────────────────────────
|
||||
# ── layout constants ─────────────────────────────────────────────────────────
|
||||
|
||||
NODE_WIDTH = 320
|
||||
HEADER_H = 60 # title bar
|
||||
@@ -106,10 +106,10 @@ MULTILINE_WIDGETS = {
|
||||
"Note": {0},
|
||||
}
|
||||
|
||||
# Node titles longer than this truncate in both frontends (plan P2-2).
|
||||
# Node titles longer than this truncate in both frontends.
|
||||
MAX_TITLE = 28
|
||||
|
||||
# Frontend model-download metadata (plan P2-7): a missing checkpoint becomes a
|
||||
# Frontend model-download metadata: a missing checkpoint becomes a
|
||||
# guided download instead of a red error.
|
||||
# Placeholder ref for the "your own style" templates. Refs are opaque ids now
|
||||
# (no name lookup), so the templates ship an obvious placeholder the user swaps
|
||||
@@ -159,7 +159,7 @@ MODELS_ZIMAGE = [MODEL_ZIMAGE_UNET, MODEL_ZIMAGE_CLIP, MODEL_ZIMAGE_VAE]
|
||||
|
||||
def node_height(node_type: str, widgets: list[Any]) -> int:
|
||||
"""
|
||||
Estimated on-canvas height (plan P2-1): header + one row per input/output
|
||||
Estimated on-canvas height: header + one row per input/output
|
||||
slot + one row per widget. The slot term matters for wide-output nodes —
|
||||
Facets alone has dozens of outputs.
|
||||
"""
|
||||
@@ -231,7 +231,7 @@ class Graph:
|
||||
return self._node_id
|
||||
|
||||
def note(self, col: int, text: str, title: str = "Read me") -> int:
|
||||
"""A visible core Note node — the instruction channel (plan P2-2)."""
|
||||
"""A visible core Note node — the instruction channel."""
|
||||
# Height scales with the text so long notes don't clip at default size.
|
||||
h = HEADER_H + 30 + 18 * (text.count("\n") + len(text) // 55)
|
||||
return self.place("Note", col, [text], title=title, height=h)
|
||||
@@ -280,7 +280,7 @@ def apply_widgets(subject: str, target: str) -> list[Any]:
|
||||
|
||||
|
||||
def sampler_widgets(cfg: float) -> list[Any]:
|
||||
# Seeds randomize (plan P2-4): a fixed seed makes the second queue a silent
|
||||
# Seeds randomize: a fixed seed makes the second queue a silent
|
||||
# cache no-op — deadly in a demo.
|
||||
return [0, "randomize", 25, cfg, "euler", "normal", 1.0]
|
||||
|
||||
@@ -332,10 +332,9 @@ def _facet_board(graph: Graph, load: int, col: int) -> int:
|
||||
|
||||
def _preview(graph: Graph, apply_node: int, col: int, suffix: str = "") -> None:
|
||||
"""
|
||||
Compiled prompts made visible (plan P2-3, extended to every template by
|
||||
owner decision): Preview Any on Apply's positive and negative kills the
|
||||
"empty text box on the encoder looks broken" perception and teaches what
|
||||
StyleRef actually produces.
|
||||
Compiled prompts made visible, in every template: Preview Any on Apply's
|
||||
positive and negative kills the "empty text box on the encoder looks
|
||||
broken" perception and teaches what StyleRef actually produces.
|
||||
"""
|
||||
pos = graph.place("PreviewAny", col, [], title=f"Compiled positive{suffix}")
|
||||
neg = graph.place("PreviewAny", col, [], title=f"Compiled negative{suffix}")
|
||||
@@ -442,7 +441,7 @@ def workflow_zimage_01_quickstart() -> dict[str, Any]:
|
||||
|
||||
|
||||
def workflow_zimage_02_consistency() -> dict[str, Any]:
|
||||
"""One style across three subjects on Z-Image (plan P2-5)."""
|
||||
"""One style across three subjects on Z-Image."""
|
||||
g = Graph()
|
||||
g.note(
|
||||
0,
|
||||
@@ -567,7 +566,7 @@ def workflow_01_quickstart() -> dict[str, Any]:
|
||||
|
||||
def workflow_02_consistency() -> dict[str, Any]:
|
||||
"""
|
||||
One style across three subjects, side by side (plan P2-5).
|
||||
One style across three subjects, side by side.
|
||||
|
||||
Consistency is the claim this demonstrates, and it is scoped precisely: the
|
||||
same style applied within this one model produces a coherent set. It is not
|
||||
@@ -603,7 +602,7 @@ def workflow_02_consistency() -> dict[str, Any]:
|
||||
|
||||
|
||||
def workflow_03_your_own_style() -> dict[str, Any]:
|
||||
"""Your own style, made on the web, loaded here (plan P0-6's template 03)."""
|
||||
"""Your own style, made on the web, loaded here (template 03)."""
|
||||
g = Graph()
|
||||
g.note(
|
||||
0,
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ Everything here is optional. If ComfyUI's server module isn't importable — a
|
||||
headless test run, an API-only embed, a future frontend rewrite — importing this
|
||||
fails, `__init__` catches it, and the nodes carry on with the plain text field.
|
||||
The JS side probes /styleref/search once at startup and hides its button when
|
||||
these routes are absent (plan P3-7), so both halves degrade together.
|
||||
these routes are absent, so both halves degrade together.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,7 +27,7 @@ from styleref_nodes.load import bump_refresh
|
||||
|
||||
|
||||
def _card(entry: dict) -> dict:
|
||||
"""The picker-row shape. heroImage feeds the row thumbnail (plan P3-1)."""
|
||||
"""The picker-row shape. heroImage feeds the row thumbnail."""
|
||||
return {
|
||||
"slug": entry.get("slug"),
|
||||
"name": entry.get("name"),
|
||||
@@ -143,7 +143,7 @@ async def styleref_my_styles(request: web.Request) -> web.Response:
|
||||
}
|
||||
for s in (payload.get("styles") or [])
|
||||
],
|
||||
# Passed back as ?cursor= for the dialog's Next button (plan P3-6).
|
||||
# Passed back as ?cursor= for the dialog's Next button.
|
||||
"nextCursor": payload.get("nextCursor"),
|
||||
}
|
||||
)
|
||||
|
||||
+3
-3
@@ -418,6 +418,6 @@ def list_saved_styles(
|
||||
|
||||
|
||||
# Note: this client deliberately has no extraction endpoints. Extraction is a
|
||||
# web-app experience (preview, per-block editing, history, free re-apply) —
|
||||
# plugin decision D1. The REST /extractions tier itself remains available for
|
||||
# CLI and agent surfaces.
|
||||
# web-app experience (preview, per-block editing, history, free re-apply), so
|
||||
# the pack loads styles rather than making them. The REST /extractions tier
|
||||
# itself remains available for CLI and agent surfaces.
|
||||
|
||||
+3
-3
@@ -143,7 +143,7 @@ def is_headless(env: dict[str, str] | None = None) -> bool:
|
||||
"""
|
||||
True when we almost certainly cannot open a browser on this machine.
|
||||
|
||||
Plan risk 9.2: a large share of ComfyUI installs are remote GPU boxes where
|
||||
a large share of ComfyUI installs are remote GPU boxes where
|
||||
the loopback flow cannot work *at all* — localhost is the wrong machine.
|
||||
Detecting that up front lets the Login node print the STYLEREF_TOKEN
|
||||
instructions instead of hanging on a callback that will never arrive.
|
||||
@@ -231,7 +231,7 @@ class _CallbackHandler(BaseHTTPRequestHandler):
|
||||
code = (params.get("code") or [None])[0]
|
||||
state = (params.get("state") or [None])[0]
|
||||
|
||||
# Validate BEFORE choosing the page (plan P0-4): the browser must never
|
||||
# Validate BEFORE choosing the page: the browser must never
|
||||
# claim success while the node reports a failed sign-in.
|
||||
if not code or state != type(self).expected_state:
|
||||
# A state mismatch is the CSRF case: discard the code entirely.
|
||||
@@ -387,7 +387,7 @@ def bearer_token() -> str | None:
|
||||
The token to send, or None for anonymous.
|
||||
|
||||
STYLEREF_TOKEN wins over stored credentials — it is the documented path for
|
||||
headless installs (plan risk 9.2), so it must not be second-guessed.
|
||||
headless installs, so it must not be second-guessed.
|
||||
"""
|
||||
env_token = (os.environ.get("STYLEREF_TOKEN") or "").strip()
|
||||
if env_token:
|
||||
|
||||
@@ -22,7 +22,7 @@ CATEGORY = "StyleRef"
|
||||
# Node-facing target → the server's `format` parameter.
|
||||
#
|
||||
# The list is EXACTLY the web app's copy-box format list (owner decision
|
||||
# 2026-07-22, superseding D2's midjourney removal), in the same order:
|
||||
# 2026-07-22, superseding the earlier midjourney removal), in the same order:
|
||||
# AI Tools, STYLE.md, Flux, Midjourney, Diffusion, JSON. The style means the
|
||||
# same thing here as on styleref.io, format for format.
|
||||
#
|
||||
@@ -158,7 +158,7 @@ def estimate_tokens(text: str) -> int:
|
||||
|
||||
|
||||
def preview_text(positive: str, target: str, compact: bool) -> str:
|
||||
"""The node-body summary (P0-2/P1-8): prompt preview + token estimate."""
|
||||
"""The node-body summary: prompt preview + token estimate."""
|
||||
shown = positive if len(positive) <= PREVIEW_CHARS else positive[:PREVIEW_CHARS] + "…"
|
||||
tokens = estimate_tokens(positive)
|
||||
line = f"≈{tokens} tokens ({target})"
|
||||
@@ -279,7 +279,7 @@ class StyleRefApply:
|
||||
positive = compose_prompt(subject, style_text, subject_first)
|
||||
negative = compose_negative(style, extra_negative)
|
||||
|
||||
# ui.text (P0-2): show what was actually composed, in the node body —
|
||||
# ui.text: show what was actually composed, in the node body —
|
||||
# otherwise the first visible evidence of the prompt is the final image.
|
||||
return {
|
||||
"ui": {"text": [preview_text(positive, target, compact)]},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""
|
||||
StyleRef Load — fetch a style into the graph.
|
||||
|
||||
Widget policy (plan risk 9.1): every widget here is vanilla — a text field, a
|
||||
Widget policy: every widget here is vanilla — a text field, a
|
||||
combo, a toggle. The live search dialog is a *progressive enhancement* shipped
|
||||
in web/styleref.js. When that JS fails to load, or a ComfyUI frontend release
|
||||
breaks it, this node still works exactly as written: type or paste a ref. The
|
||||
search path is additionally available headlessly via the `search` widget, whose
|
||||
results land on the `search_results` output and in the node body.
|
||||
|
||||
Guidance policy (plan P1-2): Nodes 2.0 does not render STRING placeholders, so
|
||||
Guidance policy: Nodes 2.0 does not render STRING placeholders, so
|
||||
every input's contract must survive on tooltip + default alone. Placeholders are
|
||||
kept (harmless where supported) but never as the only channel.
|
||||
|
||||
@@ -64,7 +64,7 @@ SEARCH_CATEGORIES = [
|
||||
# ComfyUI re-runs a node whenever anything upstream changes, and re-fetching an
|
||||
# unchanged style on every queue would burn the anonymous rate limit for no
|
||||
# benefit. When the node does re-execute, a stored ETag turns the re-fetch into
|
||||
# a cheap conditional GET (P5-8): the server answers 304 and the cached style —
|
||||
# a cheap conditional GET: the server answers 304 and the cached style —
|
||||
# including its compiled-prompt cache — is kept.
|
||||
_CACHE: dict[str, dict[str, Any]] = {}
|
||||
|
||||
@@ -110,7 +110,7 @@ def fetch_style(ref: str, use_cache: bool = True) -> dict[str, Any]:
|
||||
|
||||
# With use_cache off (or after a refresh) we still send the stored ETag:
|
||||
# "re-download every run" becomes "revalidate every run" — a bodyless 304
|
||||
# when nothing changed, the full spec when it did (P5-8).
|
||||
# when nothing changed, the full spec when it did.
|
||||
etag = cached.get("etag") if cached else None
|
||||
spec, canonical_url, new_etag = api.get_style_spec(ref, etag=etag)
|
||||
if spec is None and cached is not None:
|
||||
@@ -265,8 +265,7 @@ class StyleRefLoad:
|
||||
|
||||
summary = f"Loaded {summary_line(style)}"
|
||||
# The search card carries no palette data, so the node body is where the
|
||||
# palette shows up (plan P3-2's fallback) — as text, the only channel
|
||||
# ui.text has.
|
||||
# palette shows up — as text, the only channel ui.text has.
|
||||
hexes = palette_hexes(style)[:5]
|
||||
if hexes:
|
||||
summary += f"\nPalette: {' '.join(hexes)}"
|
||||
@@ -274,7 +273,7 @@ class StyleRefLoad:
|
||||
if save_to_library:
|
||||
summary += f"\n{self._save_to_library(style)}"
|
||||
elif style.get("url"):
|
||||
# Passive contribution nudge (plan P6-3): the style's public page is
|
||||
# Passive contribution nudge: the style's public page is
|
||||
# where renders made with it can be shared.
|
||||
summary += f"\nMade something good with it? Share it on {style['url']}"
|
||||
|
||||
@@ -302,7 +301,7 @@ class StyleRefLoad:
|
||||
|
||||
@staticmethod
|
||||
def _result(style: dict[str, Any], results: str, body: str):
|
||||
# ui.text renders in the node body (P0-2): the load confirmation and any
|
||||
# ui.text renders in the node body: the load confirmation and any
|
||||
# search results are visible without wiring the STRING output anywhere.
|
||||
return {"ui": {"text": [body]}, "result": (style, results)}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ without a frontend goes through `npx styleref login` (the CLI shares this
|
||||
pack's credentials file) or STYLEREF_TOKEN; sign-out without a frontend is
|
||||
deleting the credentials file, which the status text names.
|
||||
|
||||
Remote installs (plan risk 9.2): the loopback flow cannot work when ComfyUI
|
||||
Remote installs: the loopback flow cannot work when ComfyUI
|
||||
runs on a rented GPU box — there is no browser to open and `localhost` is the
|
||||
wrong machine. The sign-in path detects that case up front and returns the
|
||||
STYLEREF_TOKEN instructions instead of hanging on a callback that will never
|
||||
|
||||
+4
-4
@@ -32,10 +32,10 @@ from typing import Any
|
||||
PALETTE = ("colors", "color_palette")
|
||||
MOOD = ("mood_personality", "mood_personality")
|
||||
|
||||
# Every guardrail field that belongs in an image model's NEGATIVE channel
|
||||
# (plan P1-9 + the 2026-07-23 negation pass), in render order. avoided_words is
|
||||
# deliberately absent: it constrains text output (buzzwords, brand voice) and
|
||||
# stays in the text formats — the same split as voice_language.
|
||||
# Every guardrail field that belongs in an image model's NEGATIVE channel, in
|
||||
# render order. avoided_words is deliberately absent: it constrains text output
|
||||
# (buzzwords, brand voice) and stays in the text formats — the same split as
|
||||
# voice_language.
|
||||
GUARDRAIL_ARTIFACTS = ("guardrails", "common_artifacts_to_avoid")
|
||||
GUARDRAILS = ("guardrails", "avoided_visuals")
|
||||
GUARDRAIL_COLORS = ("guardrails", "avoided_colors")
|
||||
|
||||
+4
-4
@@ -160,7 +160,7 @@ def test_401_flags_needs_login(monkeypatch):
|
||||
|
||||
def test_401_with_env_token_hints_at_expiry(monkeypatch):
|
||||
"""
|
||||
Plan P0-5: the env token never refreshes itself, so a headless install's
|
||||
The env token never refreshes itself, so a headless install's
|
||||
401 hours later needs to say the likely cause — an expired STYLEREF_TOKEN.
|
||||
"""
|
||||
monkeypatch.setenv("STYLEREF_TOKEN", "stale-token")
|
||||
@@ -213,7 +213,7 @@ def test_get_style_spec_reads_the_canonical_url_header(monkeypatch):
|
||||
assert url == "https://styleref.io/share/real-slug"
|
||||
|
||||
|
||||
# ── extraction stays out of this client (plan P0-6 / decision D1) ────────────
|
||||
# ── extraction stays out of this client ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_client_has_no_extraction_endpoints():
|
||||
@@ -306,7 +306,7 @@ def test_client_tag_header_is_sent(monkeypatch):
|
||||
assert captured.get("X-styleref-client") == "comfyui"
|
||||
|
||||
|
||||
# ── save (P5-6) ──────────────────────────────────────────────────────────────
|
||||
# ── save ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_save_style_posts_to_the_save_endpoint(monkeypatch):
|
||||
@@ -322,7 +322,7 @@ def test_save_style_posts_to_the_save_endpoint(monkeypatch):
|
||||
assert seen["url"].endswith("/styles/%40ada%2FWarm%20Editorial/save")
|
||||
|
||||
|
||||
# ── conditional GET (P5-8) ───────────────────────────────────────────────────
|
||||
# ── conditional GET ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_304_is_success_not_error(monkeypatch):
|
||||
|
||||
+2
-2
@@ -138,7 +138,7 @@ def test_dead_session_degrades_to_anonymous(tmp_path, monkeypatch):
|
||||
assert auth.bearer_token() is None
|
||||
|
||||
|
||||
# ── headless detection (plan risk 9.2) ───────────────────────────────────────
|
||||
# ── headless detection ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ssh_session_is_headless():
|
||||
@@ -174,7 +174,7 @@ def test_headless_help_leads_with_the_credentials_file_then_the_env_var():
|
||||
assert help_text.index("credentials.json") < help_text.index("STYLEREF_TOKEN")
|
||||
|
||||
|
||||
# ── OAuth callback page (plan P0-4) ──────────────────────────────────────────
|
||||
# ── OAuth callback page ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class _FakeWFile:
|
||||
|
||||
+14
-14
@@ -36,12 +36,12 @@ def clear_cache():
|
||||
load_node._GENERATION.clear()
|
||||
|
||||
|
||||
# ── pack registration (plan P0-6) ────────────────────────────────────────────
|
||||
# ── pack registration ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_pack_registers_exactly_the_five_nodes():
|
||||
"""
|
||||
Extraction moved to the web app (decision D1): the pack registers
|
||||
Extraction moved to the web app: the pack registers
|
||||
Load/Apply/Facets/ReferenceImages/Login and nothing else — an Extract node
|
||||
here means the removal regressed.
|
||||
"""
|
||||
@@ -65,7 +65,7 @@ def test_pack_registers_exactly_the_five_nodes():
|
||||
|
||||
def test_targets_are_exactly_the_web_apps_copy_list_in_order():
|
||||
"""
|
||||
Owner decision 2026-07-22 (supersedes D2): the target list mirrors the web
|
||||
Owner decision 2026-07-22: the target list mirrors the web
|
||||
app's copy-box format list exactly — same entries, same order — so the
|
||||
style means the same thing here as on styleref.io. Source of truth:
|
||||
PROMPT_FORMAT_OPTIONS in src/components/ui/PromptFormatDropdown.tsx.
|
||||
@@ -246,7 +246,7 @@ def test_apply_rejects_an_unloaded_style():
|
||||
node.apply(make_style(ref="", name="(none)"), "subject", "flux")
|
||||
|
||||
|
||||
# ── Apply: node-body feedback (plan P0-2 / P1-8) ─────────────────────────────
|
||||
# ── Apply: node-body feedback ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_apply_returns_ui_text_and_unchanged_result_tuple(monkeypatch):
|
||||
@@ -370,7 +370,7 @@ def test_private_refs_are_tagged_as_library(monkeypatch):
|
||||
|
||||
def test_ungenerated_style_error_reaches_the_user_verbatim(monkeypatch):
|
||||
"""
|
||||
Plan P0-7: when the server refuses a never-generated own-style ref (409),
|
||||
When the server refuses a never-generated own-style ref (409),
|
||||
its message — which tells the user to Generate on styleref.io — must pass
|
||||
through unreworded.
|
||||
"""
|
||||
@@ -390,7 +390,7 @@ def test_ungenerated_style_error_reaches_the_user_verbatim(monkeypatch):
|
||||
|
||||
|
||||
def test_load_returns_ui_text_and_result_tuple(monkeypatch):
|
||||
"""P0-2: the load confirmation renders in the node body."""
|
||||
"""The load confirmation renders in the node body."""
|
||||
monkeypatch.setattr(
|
||||
api, "get_style_spec", lambda ref, etag=None: ({"name": "Warm", "sections": {}}, None, None)
|
||||
)
|
||||
@@ -422,7 +422,7 @@ def test_load_never_fetches_style_text(monkeypatch):
|
||||
|
||||
|
||||
def test_load_is_an_output_node():
|
||||
"""A search-only Load must be queueable alone and still display (P0-2)."""
|
||||
"""A search-only Load must be queueable alone and still display."""
|
||||
assert load_node.StyleRefLoad.OUTPUT_NODE is True
|
||||
|
||||
|
||||
@@ -456,7 +456,7 @@ def test_search_failure_does_not_sink_the_run(monkeypatch):
|
||||
|
||||
|
||||
def test_search_category_is_forwarded(monkeypatch):
|
||||
"""The vanilla category combo (P3-4) must reach the API; `any` must not."""
|
||||
"""The vanilla category combo must reach the API; `any` must not."""
|
||||
seen = {}
|
||||
|
||||
def fake_search(query, category=None, sort=None, limit=12):
|
||||
@@ -474,7 +474,7 @@ def test_search_category_is_forwarded(monkeypatch):
|
||||
assert seen["category"] is None
|
||||
|
||||
|
||||
# ── IS_CHANGED contracts (plan P0-1's regression class) ──────────────────────
|
||||
# ── IS_CHANGED contracts ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_load_is_changed_is_stable_when_cached():
|
||||
@@ -497,13 +497,13 @@ def test_login_is_always_dirty():
|
||||
def test_apply_has_no_is_changed_override():
|
||||
"""
|
||||
Apply's dirtiness must come from its inputs alone. An IS_CHANGED override
|
||||
returning NaN here is the credit-burn/api-hammer class of bug (plan P0-1) —
|
||||
returning NaN here is the credit-burn/api-hammer class of bug —
|
||||
the node would re-run on every queue.
|
||||
"""
|
||||
assert "IS_CHANGED" not in vars(apply_node.StyleRefApply)
|
||||
|
||||
|
||||
# ── Apply: sections filter (P5-3) ────────────────────────────────────────────
|
||||
# ── Apply: sections filter ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_sections_filter_reaches_the_api_and_the_cache_key(monkeypatch):
|
||||
@@ -527,7 +527,7 @@ def test_sections_filter_reaches_the_api_and_the_cache_key(monkeypatch):
|
||||
assert calls[-1] is None
|
||||
|
||||
|
||||
# ── Facets: full schema coverage (P5-2 / D4) ─────────────────────────────────
|
||||
# ── Facets: full schema coverage ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_facets_outputs_are_exactly_the_schema_sections_plus_custom_items():
|
||||
@@ -636,7 +636,7 @@ def test_reference_images_errors_clearly_without_images():
|
||||
ri.StyleRefReferenceImages().fetch(make_style(ref="x", name="Bare"))
|
||||
|
||||
|
||||
# ── Load: save_to_library (P5-6) ─────────────────────────────────────────────
|
||||
# ── Load: save_to_library ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_save_to_library_saves_once_per_session(monkeypatch):
|
||||
@@ -690,7 +690,7 @@ def test_own_styles_are_not_re_saved(monkeypatch):
|
||||
assert "Already your own style" in out["ui"]["text"][0]
|
||||
|
||||
|
||||
# ── Load: ETag revalidation (P5-8) ───────────────────────────────────────────
|
||||
# ── Load: ETag revalidation ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_cache_off_revalidates_with_the_stored_etag(monkeypatch):
|
||||
|
||||
+2
-2
@@ -128,7 +128,7 @@ def test_negatives_are_normalized_comma_terms(style):
|
||||
|
||||
def test_negatives_cover_artifacts_and_avoided_colors():
|
||||
"""
|
||||
Plan P1-9: guardrails carry more than avoided_visuals — artifacts to avoid
|
||||
Guardrails carry more than avoided_visuals — artifacts to avoid
|
||||
and avoided colors are user-authored constraints and belong in the negative.
|
||||
"""
|
||||
style = make_style(
|
||||
@@ -241,7 +241,7 @@ def test_is_style_rejects_arbitrary_dicts():
|
||||
assert is_style(make_style(ref="x", name="ok")) is True
|
||||
|
||||
|
||||
# ── dimensions (P5-2: width/height from the style) ───────────────────────────
|
||||
# ── dimensions (width/height from the style) ─────────────────────────────────
|
||||
|
||||
|
||||
def _fmt_style(aspect: str | None = None, resolution: str | None = None):
|
||||
|
||||
+12
-12
@@ -4,8 +4,8 @@ Workflow template tests.
|
||||
A broken template is only discovered when a user drags it onto a canvas and it
|
||||
fails silently, so the structural invariants are checked here instead: every
|
||||
link points at a node that exists, at a slot that exists, carrying a type both
|
||||
ends agree on — and the layout invariants of plan P2-1/P2-2: no overlapping
|
||||
nodes, instructions in visible Note nodes, titles short enough to render.
|
||||
ends agree on — plus the layout invariants: no overlapping nodes, instructions
|
||||
in visible Note nodes, titles short enough to render.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -36,7 +36,7 @@ def _load(path: str) -> dict:
|
||||
|
||||
|
||||
def _note_text(graph: dict) -> str:
|
||||
"""All visible Note-node text — the instruction channel (plan P2-2)."""
|
||||
"""All visible Note-node text — the instruction channel."""
|
||||
return "\n".join(
|
||||
str(n["widgets_values"][0])
|
||||
for n in graph["nodes"]
|
||||
@@ -50,7 +50,7 @@ def test_every_declared_workflow_is_committed():
|
||||
|
||||
|
||||
def test_extraction_template_is_gone():
|
||||
"""Plan P0-6: extraction lives on the web; template 03 tells that story."""
|
||||
"""Extraction lives on the web; template 03 tells that story."""
|
||||
assert "03-extract-and-reuse.json" not in {os.path.basename(p) for p in FILES}
|
||||
assert "03-your-own-style.json" in {os.path.basename(p) for p in FILES}
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_committed_file_matches_the_generator(path):
|
||||
|
||||
@pytest.mark.parametrize("path", FILES, ids=[_relpath(p) for p in FILES])
|
||||
def test_only_the_shipped_styleref_nodes_appear(path):
|
||||
"""Plan P0-6: a template referencing a removed node fails on the canvas."""
|
||||
"""A template referencing a removed node fails on the canvas."""
|
||||
allowed = {
|
||||
"StyleRefLoad",
|
||||
"StyleRefApply",
|
||||
@@ -128,7 +128,7 @@ def test_link_ids_are_unique_and_registered_on_both_ends(path):
|
||||
|
||||
@pytest.mark.parametrize("path", FILES, ids=[_relpath(p) for p in FILES])
|
||||
def test_no_nodes_overlap_at_default_sizes(path):
|
||||
"""Plan P2-1: overlapping nodes read as a broken template — layout is math,
|
||||
"""Overlapping nodes read as a broken template — layout is math,
|
||||
not guesses, so this must hold for every template."""
|
||||
nodes = _load(path)["nodes"]
|
||||
boxes = [(n["id"], *n["pos"], *n["size"]) for n in nodes]
|
||||
@@ -142,7 +142,7 @@ def test_no_nodes_overlap_at_default_sizes(path):
|
||||
def test_every_workflow_explains_itself_in_a_visible_note(path):
|
||||
"""
|
||||
Templates are documentation — and ComfyUI never renders extra.styleref, so
|
||||
the instructions must live in a visible core Note node (plan P2-2).
|
||||
the instructions must live in a visible core Note node.
|
||||
"""
|
||||
graph = _load(path)
|
||||
assert len(_note_text(graph)) > 60, "no substantial Note node found"
|
||||
@@ -168,14 +168,14 @@ def test_auth_requirements_are_stated(path):
|
||||
needs_auth = any(n["type"] == "StyleRefLogin" for n in graph["nodes"])
|
||||
if needs_auth:
|
||||
assert "sign-in" in notes or "sign in" in notes
|
||||
# The sign-in path ships in the template (plan P2-6), not just in prose.
|
||||
# The sign-in path ships in the template, not just in prose.
|
||||
assert any(n["type"] == "StyleRefLogin" for n in graph["nodes"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", FILES, ids=[_relpath(p) for p in FILES])
|
||||
def test_seeds_randomize(path):
|
||||
"""
|
||||
Plan P2-4: a fixed seed makes the second queue a silent cache no-op —
|
||||
A fixed seed makes the second queue a silent cache no-op —
|
||||
deadly in a demo.
|
||||
"""
|
||||
for node in _load(path)["nodes"]:
|
||||
@@ -190,7 +190,7 @@ _MODEL_DIRECTORIES = {"checkpoints", "diffusion_models", "text_encoders", "vae"}
|
||||
|
||||
@pytest.mark.parametrize("path", FILES, ids=[_relpath(p) for p in FILES])
|
||||
def test_model_loaders_carry_download_metadata(path):
|
||||
"""Plan P2-7: a missing model should be a guided download, not a red error —
|
||||
"""A missing model should be a guided download, not a red error —
|
||||
for every loader type (checkpoints and the Z-Image UNET/CLIP/VAE stack)."""
|
||||
graph = _load(path)
|
||||
loader_files = {
|
||||
@@ -288,7 +288,7 @@ def test_zimage_templates_have_no_negative_encoder(name):
|
||||
|
||||
|
||||
def test_consistency_template_shows_one_style_many_subjects():
|
||||
"""Plan P2-5: three Apply nodes with three different subjects, one Load."""
|
||||
"""Three Apply nodes with three different subjects, one Load."""
|
||||
graph = _load(os.path.join(WORKFLOW_DIR, "02-consistency-grid.json"))
|
||||
applies = [n for n in graph["nodes"] if n["type"] == "StyleRefApply"]
|
||||
loads = [n for n in graph["nodes"] if n["type"] == "StyleRefLoad"]
|
||||
@@ -299,7 +299,7 @@ def test_consistency_template_shows_one_style_many_subjects():
|
||||
|
||||
|
||||
def test_prompt_previews_exist_where_promised():
|
||||
"""Plan P2-3: templates 01 and 04 make the compiled prompts visible."""
|
||||
"""Templates 01 and 04 make the compiled prompts visible."""
|
||||
for name in ("01-quickstart.json", "04-facets.json"):
|
||||
graph = _load(os.path.join(WORKFLOW_DIR, name))
|
||||
assert any(n["type"] == "PreviewAny" for n in graph["nodes"]), name
|
||||
|
||||
+9
-9
@@ -53,7 +53,7 @@ const CATEGORIES = [
|
||||
// ‹ Prev / Next ›, so the dialog stays a fixed height instead of growing.
|
||||
const PAGE_SIZE = 8;
|
||||
|
||||
// Resolved by the availability probe (plan P3-7). `null` = probe still running.
|
||||
// Resolved by the availability probe. `null` = probe still running.
|
||||
let routesAvailable = null;
|
||||
|
||||
async function probeRoutes() {
|
||||
@@ -101,7 +101,7 @@ function el(tag, css, text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The picker dialog (plan P3-3/P3-5): owns its query, filters, and result
|
||||
* The picker dialog: owns its query, filters, and result
|
||||
* list. Resolves with the chosen slug/ref, or null when dismissed.
|
||||
*/
|
||||
function openPicker(initialQuery) {
|
||||
@@ -124,7 +124,7 @@ function openPicker(initialQuery) {
|
||||
resolve(value);
|
||||
};
|
||||
const onKey = (e) => {
|
||||
// Esc-to-close (plan P3-5), capturing so the canvas never sees it.
|
||||
// Esc-to-close, capturing so the canvas never sees it.
|
||||
if (e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
close(null);
|
||||
@@ -213,7 +213,7 @@ function openPicker(initialQuery) {
|
||||
button.onmouseenter = () => (button.style.background = "#32323a");
|
||||
button.onmouseleave = () => (button.style.background = "#26262a");
|
||||
|
||||
// Thumbnail (plan P3-1) — a fixed box so rows align with or without one.
|
||||
// Thumbnail — a fixed box so rows align with or without one.
|
||||
const thumb = el(
|
||||
"div",
|
||||
"width:44px;height:44px;border-radius:6px;background:#1a1a1d;flex:none;overflow:hidden;",
|
||||
@@ -410,7 +410,7 @@ function openPicker(initialQuery) {
|
||||
return;
|
||||
}
|
||||
if (gallery.error) {
|
||||
// Inline message row, never alert() (plan P3-5).
|
||||
// Inline message row, never alert().
|
||||
out.append(message(gallery.error));
|
||||
return;
|
||||
}
|
||||
@@ -593,7 +593,7 @@ async function refreshLoginStatus(node, statusWidget) {
|
||||
}
|
||||
}
|
||||
|
||||
/** True when Apply's `positive` output feeds a CLIPTextEncode (plan P1-3). */
|
||||
/** True when Apply's `positive` output feeds a CLIPTextEncode. */
|
||||
function positiveFeedsClipEncode(node) {
|
||||
const graph = node.graph;
|
||||
const output = node.outputs?.find((o) => o.name === "positive");
|
||||
@@ -619,7 +619,7 @@ app.registerExtension({
|
||||
const result = onCreated?.apply(this, arguments);
|
||||
|
||||
// Only add the search button when the backend routes exist
|
||||
// (plan P3-7) — otherwise it could only 404.
|
||||
// — otherwise it could only 404.
|
||||
if (routesAvailable !== false) {
|
||||
this.addWidget("button", "Search styles…", null, async () => {
|
||||
const refWidget = this.widgets?.find((w) => w.name === "style_ref");
|
||||
@@ -635,7 +635,7 @@ app.registerExtension({
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh affordance (plan P1-6): the backend drops its cached
|
||||
// Refresh affordance: the backend drops its cached
|
||||
// copy and dirties the node, so the next queue re-downloads —
|
||||
// no need to toggle use_cache off and back on.
|
||||
if (routesAvailable !== false) {
|
||||
@@ -662,7 +662,7 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
if (nodeData?.name === APPLY_NODE) {
|
||||
// Warning badge (plan P1-3): `natural`/`style_md` are copy-out
|
||||
// Warning badge: `natural`/`style_md` are copy-out
|
||||
// documents; wiring one into a CLIP encoder is almost certainly a
|
||||
// mistake. Additive drawing only — nothing else is touched.
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
|
||||
Reference in New Issue
Block a user