Audit node registration coverage
This commit is contained in:
@@ -625,6 +625,10 @@ Already isolated:
|
|||||||
- node input tooltip inventory, node-specific tooltip overrides, dynamic input
|
- node input tooltip inventory, node-specific tooltip overrides, dynamic input
|
||||||
fallback tooltip rules, and tooltip injection live in `node_tooltips.py`;
|
fallback tooltip rules, and tooltip injection live in `node_tooltips.py`;
|
||||||
`__init__.py` only applies the installer to the assembled node registry.
|
`__init__.py` only applies the installer to the assembled node registry.
|
||||||
|
- node registration drift is checked by `tools/prompt_map_audit.py`: concrete
|
||||||
|
`SxCP...` node classes in node modules must be present in their module class
|
||||||
|
mappings and matching display-name mappings before they can silently
|
||||||
|
disappear from ComfyUI.
|
||||||
- profile-save and accumulator server payload handling lives in
|
- profile-save and accumulator server payload handling lives in
|
||||||
`server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
|
`server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
|
||||||
JSON responses, and `tools/prompt_smoke.py` covers the handlers without
|
JSON responses, and `tools/prompt_smoke.py` covers the handlers without
|
||||||
|
|||||||
@@ -920,12 +920,15 @@ The script does not import ComfyUI. It parses the repo and prints:
|
|||||||
- node documentation validation so every registered ComfyUI display name appears
|
- node documentation validation so every registered ComfyUI display name appears
|
||||||
in this route map or the README before the node can silently drift out of
|
in this route map or the README before the node can silently drift out of
|
||||||
user-facing docs.
|
user-facing docs.
|
||||||
|
- node registration validation so every concrete `SxCP...` node class in a
|
||||||
|
node module is present in that module's class mapping, has a matching display
|
||||||
|
mapping, and uses the same mapping key as the class name.
|
||||||
|
|
||||||
Use its output to spot doc drift after adding a new node or pool. If a new node
|
Use its output to spot doc drift after adding a new node or pool. If a new node
|
||||||
or pool appears there but not in this map, update the relevant route table. The
|
or pool appears there but not in this map, update the relevant route table. The
|
||||||
script exits nonzero when JSON pool references, item template axes, category
|
script exits nonzero when JSON pool references, item template axes, category
|
||||||
identities, critical route docs, critical route smoke registrations, or
|
identities, critical route docs, critical route smoke registrations, or
|
||||||
registered node display names do not resolve.
|
registered node classes/display names do not resolve.
|
||||||
|
|
||||||
## Behavioral Smoke Helper
|
## Behavioral Smoke Helper
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = (
|
|||||||
"docs/prompt-pool-routing-map.md",
|
"docs/prompt-pool-routing-map.md",
|
||||||
"node documentation validation",
|
"node documentation validation",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"docs/prompt-pool-routing-map.md",
|
||||||
|
"node registration validation",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
PROMPT_ROW_READ_SCAN_GLOBS: tuple[str, ...] = (
|
PROMPT_ROW_READ_SCAN_GLOBS: tuple[str, ...] = (
|
||||||
@@ -220,6 +224,80 @@ def _class_return_names(path: Path) -> dict[str, tuple[str, ...]]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _sxcp_class_names(path: Path) -> set[str]:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
return {
|
||||||
|
node.name
|
||||||
|
for node in tree.body
|
||||||
|
if isinstance(node, ast.ClassDef) and node.name.startswith("SxCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _class_mapping_entries(path: Path, mapping_names: tuple[str, ...]) -> dict[str, str]:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
entries: dict[str, str] = {}
|
||||||
|
for node in tree.body:
|
||||||
|
if not isinstance(node, ast.Assign):
|
||||||
|
continue
|
||||||
|
if not any(isinstance(target, ast.Name) and target.id in mapping_names for target in node.targets):
|
||||||
|
continue
|
||||||
|
if not isinstance(node.value, ast.Dict):
|
||||||
|
continue
|
||||||
|
for key_node, value_node in zip(node.value.keys, node.value.values):
|
||||||
|
key = _literal_or_none(key_node) if key_node is not None else None
|
||||||
|
if not isinstance(key, str):
|
||||||
|
continue
|
||||||
|
if isinstance(value_node, ast.Name):
|
||||||
|
entries[key] = value_node.id
|
||||||
|
else:
|
||||||
|
entries[key] = ""
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _display_mapping_keys(path: Path, mapping_names: tuple[str, ...]) -> set[str]:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
keys: set[str] = set()
|
||||||
|
for node in tree.body:
|
||||||
|
if not isinstance(node, ast.Assign):
|
||||||
|
continue
|
||||||
|
if not any(isinstance(target, ast.Name) and target.id in mapping_names for target in node.targets):
|
||||||
|
continue
|
||||||
|
if not isinstance(node.value, ast.Dict):
|
||||||
|
continue
|
||||||
|
for key_node in node.value.keys:
|
||||||
|
key = _literal_or_none(key_node) if key_node is not None else None
|
||||||
|
if isinstance(key, str):
|
||||||
|
keys.add(key)
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def _node_registration_errors() -> list[tuple[str, str, str]]:
|
||||||
|
errors: list[tuple[str, str, str]] = []
|
||||||
|
for path in _node_module_python_paths():
|
||||||
|
class_names = _sxcp_class_names(path)
|
||||||
|
if not class_names:
|
||||||
|
continue
|
||||||
|
class_map = _class_mapping_entries(path, ("NODE_CLASS_MAPPINGS", "LOOP_NODE_CLASS_MAPPINGS"))
|
||||||
|
display_keys = _display_mapping_keys(path, ("NODE_DISPLAY_NAME_MAPPINGS", "LOOP_NODE_DISPLAY_NAME_MAPPINGS"))
|
||||||
|
registered_classes = {class_name for class_name in class_map.values() if class_name}
|
||||||
|
|
||||||
|
for class_name in sorted(class_names - registered_classes):
|
||||||
|
errors.append((path.name, class_name, "SxCP node class is not registered in the module class mapping"))
|
||||||
|
for mapping_key, class_name in sorted(class_map.items()):
|
||||||
|
if not class_name:
|
||||||
|
errors.append((path.name, mapping_key, "node class mapping value should be a class name"))
|
||||||
|
continue
|
||||||
|
if mapping_key != class_name:
|
||||||
|
errors.append((path.name, mapping_key, f"node class mapping key should match class name {class_name}"))
|
||||||
|
if class_name not in class_names:
|
||||||
|
errors.append((path.name, mapping_key, f"node class mapping references unknown class {class_name}"))
|
||||||
|
for mapping_key in sorted(set(class_map) - display_keys):
|
||||||
|
errors.append((path.name, mapping_key, "registered node is missing a display-name mapping"))
|
||||||
|
for mapping_key in sorted(display_keys - set(class_map)):
|
||||||
|
errors.append((path.name, mapping_key, "display-name mapping has no matching registered node"))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def _category_summary(path: Path) -> dict[str, Any]:
|
def _category_summary(path: Path) -> dict[str, Any]:
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
categories = data.get("categories") or []
|
categories = data.get("categories") or []
|
||||||
@@ -260,6 +338,12 @@ def _node_python_paths() -> list[Path]:
|
|||||||
return [path for path in paths if path.exists()]
|
return [path for path in paths if path.exists()]
|
||||||
|
|
||||||
|
|
||||||
|
def _node_module_python_paths() -> list[Path]:
|
||||||
|
paths = [ROOT / "loop_nodes.py"]
|
||||||
|
paths.extend(sorted(ROOT.glob("node_*.py")))
|
||||||
|
return [path for path in paths if path.exists()]
|
||||||
|
|
||||||
|
|
||||||
def _load_category_json(path: Path) -> dict[str, Any]:
|
def _load_category_json(path: Path) -> dict[str, Any]:
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
return data if isinstance(data, dict) else {}
|
return data if isinstance(data, dict) else {}
|
||||||
@@ -1237,6 +1321,13 @@ def main() -> int:
|
|||||||
return 1
|
return 1
|
||||||
print("OK: registered node display names are documented in the route map or README.")
|
print("OK: registered node display names are documented in the route map or README.")
|
||||||
|
|
||||||
|
print("\n# Node Registration Validation")
|
||||||
|
node_registration_errors = _node_registration_errors()
|
||||||
|
if node_registration_errors:
|
||||||
|
print_table(("Module", "Node", "Issue"), node_registration_errors)
|
||||||
|
return 1
|
||||||
|
print("OK: concrete SxCP node classes are registered with matching display names.")
|
||||||
|
|
||||||
print("\n# Metadata Prompt Fallback Validation")
|
print("\n# Metadata Prompt Fallback Validation")
|
||||||
prompt_row_read_errors = _prompt_row_read_errors()
|
prompt_row_read_errors = _prompt_row_read_errors()
|
||||||
if prompt_row_read_errors:
|
if prompt_row_read_errors:
|
||||||
|
|||||||
Reference in New Issue
Block a user