fix(ingestion): the switch walk skips negative inputs by #90's own rule

The upstream walk treated only names containing "negative" as negative, so
an off node on a neg, neg_text or text_neg input blocked an enabled
positive. prompt_graph gains is_negative_input (the _NEGATIVE_KEY rule, also
used by follows_input) and the walk uses it. The guards accept a node with
null inputs and a side whose provenance is not an object.
This commit is contained in:
Vito Sansevero
2026-10-28 13:07:40 -07:00
parent 982464b7f2
commit dc0df5760f
3 changed files with 56 additions and 4 deletions
+43
View File
@@ -271,6 +271,49 @@ class TestSaveToLibrarySteps(unittest.TestCase):
self.assertEqual(decision.kind, "positive")
self.assertEqual(decision.text, "SECRET OFF TEXT")
def test_an_off_node_on_a_negative_input_name_never_blocks(self):
"""The walk skips negative inputs by #90's own rule (``neg``,
``neg_text``, ``text_neg``, ``negative_text``)."""
for key in ("neg", "neg_text", "text_neg", "negative_text"):
with self.subTest(key=key):
graph = {
"1": {
"class_type": "PromptManagerText",
"inputs": {"text": "bad hands", "save_to_library": False},
},
"9": {
"class_type": "PromptManagerText",
"inputs": {"text": "a cat", "save_to_library": True},
},
"7": {
"class_type": "PipeEncode",
"inputs": {"pos_text": ["9", 0], key: ["1", 0]},
},
"4": {"class_type": "CheckpointLoaderSimple", "inputs": {}},
"3": {
"class_type": "KSampler",
"inputs": {
"model": ["4", 0],
"positive": ["7", 0],
"negative": ["7", 1],
},
},
}
self.assertIsNone(file_prompt._switched_off_upstream(graph))
self.assertNotIn(
_decide(_graph(graph)).kind, ("not_saved", "not_saved_wired")
)
def test_the_guards_accept_malformed_nodes(self):
graph = {
"1": {"class_type": "PromptManagerText", "inputs": None},
"2": {"class_type": "PromptManagerText"},
}
self.assertIsNone(file_prompt._off_text(graph, "x"))
side = {"provenance": ["not", "a", "dict"]}
with mock.patch.object(file_prompt, "side_opted_out", return_value=True):
self.assertIsNone(file_prompt._side_switch(graph, side))
def test_the_off_text_guard_reads_a_nodes_resolved_text(self):
graph = {
"1": {
+7 -3
View File
@@ -31,6 +31,7 @@ from .generation_params import (
from .prompt_graph import (
PROMPT_MANAGER_TYPES,
_positive_roots,
is_negative_input,
metadata_opted_out,
resolve_text,
save_setting,
@@ -281,7 +282,9 @@ def _side_switch(graph: Any, side: Any) -> Optional[str]:
"""The switch outcome of the PromptManager nodes on a side's path."""
if not isinstance(graph, dict) or not side_opted_out(graph, side):
return None
path = (side.get("provenance") or {}).get("path") or []
provenance = side.get("provenance")
path = provenance.get("path") if isinstance(provenance, dict) else None
path = path if isinstance(path, list) else []
nodes = {str(step.get("node_id")) for step in path if isinstance(step, dict)} & set(
_prompt_manager_ids(graph)
)
@@ -337,7 +340,7 @@ def _switched_off_upstream(graph: Any) -> Optional[str]:
found.add(node_id)
inputs = node.get("inputs")
for key, value in (inputs.items() if isinstance(inputs, dict) else ()):
if key in _NOT_TEXT_INPUTS or "negative" in str(key).lower():
if key in _NOT_TEXT_INPUTS or is_negative_input(key):
continue
if isinstance(value, (list, tuple)) and len(value) == 2:
if isinstance(value[0], (str, int)) and isinstance(value[1], int):
@@ -353,7 +356,8 @@ def _off_text(graph: Any, text: str) -> Optional[str]:
for node_id in _prompt_manager_ids(graph):
node_text = resolve_text(graph, node_id)
if node_text is None:
literal = graph[node_id].get("inputs", {}).get("text")
inputs = graph[node_id].get("inputs")
literal = inputs.get("text") if isinstance(inputs, dict) else None
node_text = literal if isinstance(literal, str) else None
if node_text is not None and node_text.strip() == wanted:
matched.append(save_setting(graph, node_id))
+6 -1
View File
@@ -41,9 +41,14 @@ _TEXT_KEY = re.compile(r"^(text|string|prompt)(_?[a-z0-9]+)?$", re.IGNORECASE)
_NEGATIVE_KEY = re.compile(r"(^|_)neg(ative)?($|_)", re.IGNORECASE)
def is_negative_input(key: Any) -> bool:
"""Whether an input name carries a negative prompt (#90's one rule)."""
return isinstance(key, str) and bool(_NEGATIVE_KEY.search(key))
def follows_input(key: Any) -> bool:
"""Whether an input name carries positive conditioning or prompt text."""
if not isinstance(key, str) or _NEGATIVE_KEY.search(key):
if not isinstance(key, str) or is_negative_input(key):
return False
return key == "positive" or bool(
_CONDITIONING_KEY.match(key) or _TEXT_KEY.match(key)