Audit category selector identities

This commit is contained in:
2026-06-27 20:59:37 +02:00
parent de6615c024
commit 1f9544233e
3 changed files with 140 additions and 6 deletions
+3 -3
View File
@@ -561,9 +561,9 @@ Improve later:
- keep `tools/prompt_map_audit.py` passing; it now checks referenced
expression/composition/scene pools, item-template axes, object-template
metadata values for both string and object templates, registered formatter
policy coverage for route families, and critical route documentation plus
expected smoke coverage.
metadata values for both string and object templates, category/subcategory
identity uniqueness, registered formatter policy coverage for route
families, and critical route documentation plus expected smoke coverage.
### Node / UI Path
+6 -3
View File
@@ -902,6 +902,9 @@ The script does not import ComfyUI. It parses the repo and prints:
- JSON reference validation for every `scene_pools`, `expression_pools`, and
`composition_pools` reference;
- item template validation so `{placeholder}` names resolve to `item_axes`.
- category identity validation so custom category names/slugs do not collide
with built-in selectors, category identities stay unique, and exact
subcategory selectors cannot become ambiguous.
- effective category route coverage so each normalized category path has
usable item, scene, expression, composition, and hardcore route metadata
before runtime fallbacks can hide a gap.
@@ -920,9 +923,9 @@ The script does not import ComfyUI. It parses the repo and prints:
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
script exits nonzero when JSON pool references, item template axes, critical
route docs, critical route smoke registrations, or registered node display
names do not resolve.
script exits nonzero when JSON pool references, item template axes, category
identities, critical route docs, critical route smoke registrations, or
registered node display names do not resolve.
## Behavioral Smoke Helper
+131
View File
@@ -118,6 +118,10 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = (
"docs/prompt-pool-routing-map.md",
"multi-seed route sweeps",
),
(
"docs/prompt-pool-routing-map.md",
"category identity validation",
),
(
"docs/prompt-pool-routing-map.md",
"node documentation validation",
@@ -141,6 +145,15 @@ ALLOWED_PROMPT_ROW_READS: set[tuple[str, str]] = {
ROUTE_POLICY_ACTION_EXCLUSIONS = {"default"}
ROUTE_POLICY_POSITION_EXCLUSIONS = {"any"}
RESERVED_CATEGORY_CHOICES = {
"auto_weighted",
"auto_full",
"woman",
"man",
"couple",
"group_or_layout",
"custom_random",
}
NODE_DOC_PATHS = (
"docs/prompt-pool-routing-map.md",
"README.md",
@@ -418,6 +431,117 @@ def _json_reference_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
return errors
def _identity_key(value: Any) -> str:
return re.sub(r"\s+", " ", str(value or "").strip()).lower()
def _identity_location(category: dict[str, Any], subcategory: dict[str, Any] | None = None) -> str:
category_slug = str(category.get("slug") or category.get("name") or "category").strip()
location = f"categories.{category_slug}"
if subcategory is not None:
sub_slug = str(subcategory.get("slug") or subcategory.get("name") or "subcategory").strip()
location = f"{location}.subcategories.{sub_slug}"
return location
def _add_duplicate_identity_error(
seen: dict[str, str],
*,
key: str,
location: str,
label: str,
errors: list[tuple[str, str, str]],
) -> None:
if not key:
return
previous = seen.get(key)
if previous is not None:
errors.append(("(category library)", location, f"duplicate {label}: {key!r} also used at {previous}"))
return
seen[key] = location
def _category_identity_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
# Use the normalized library because that is the selector surface exposed by
# category/preset nodes and exact subcategory routing.
_ = paths
try:
categories = category_policy.load_category_library()
except Exception as exc:
return [("(category library)", "categories", f"cannot load categories: {exc}")]
errors: list[tuple[str, str, str]] = []
category_names: dict[str, str] = {}
category_slugs: dict[str, str] = {}
exact_selectors: dict[str, str] = {}
exact_slug_selectors: dict[str, str] = {}
for category in categories:
location = _identity_location(category)
name = _identity_key(category.get("name"))
slug = _identity_key(category.get("slug"))
if not name:
errors.append(("(category library)", f"{location}.name", "missing normalized category name"))
if not slug:
errors.append(("(category library)", f"{location}.slug", "missing normalized category slug"))
if name in RESERVED_CATEGORY_CHOICES or slug in RESERVED_CATEGORY_CHOICES:
errors.append(
(
"(category library)",
location,
"category identity collides with reserved built-in selector",
)
)
_add_duplicate_identity_error(category_names, key=name, location=location, label="category name", errors=errors)
_add_duplicate_identity_error(category_slugs, key=slug, location=location, label="category slug", errors=errors)
subcategory_names: dict[str, str] = {}
subcategory_slugs: dict[str, str] = {}
for subcategory in category.get("subcategories") or []:
if not isinstance(subcategory, dict):
continue
sub_location = _identity_location(category, subcategory)
sub_name = _identity_key(subcategory.get("name"))
sub_slug = _identity_key(subcategory.get("slug"))
if not sub_name:
errors.append(("(category library)", f"{sub_location}.name", "missing normalized subcategory name"))
if not sub_slug:
errors.append(("(category library)", f"{sub_location}.slug", "missing normalized subcategory slug"))
_add_duplicate_identity_error(
subcategory_names,
key=sub_name,
location=sub_location,
label=f"subcategory name in {category.get('name')}",
errors=errors,
)
_add_duplicate_identity_error(
subcategory_slugs,
key=sub_slug,
location=sub_location,
label=f"subcategory slug in {category.get('name')}",
errors=errors,
)
exact_selector = _identity_key(category_policy.exact_subcategory_selector(category, subcategory))
slug_selector = f"{slug} / {sub_slug}" if slug and sub_slug else ""
_add_duplicate_identity_error(
exact_selectors,
key=exact_selector,
location=sub_location,
label="exact subcategory selector",
errors=errors,
)
_add_duplicate_identity_error(
exact_slug_selectors,
key=slug_selector,
location=sub_location,
label="category/subcategory slug selector",
errors=errors,
)
return errors
def _hardcore_template_metadata_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
errors: list[tuple[str, str, str]] = []
for path in paths:
@@ -1064,6 +1188,13 @@ def main() -> int:
return 1
print("OK: all JSON pool references and item template axes resolve.")
print("\n# Category Identity Validation")
category_identity_errors = _category_identity_errors(category_paths)
if category_identity_errors:
print_table(("Source", "Path", "Issue"), category_identity_errors)
return 1
print("OK: category and subcategory identities are unique and selector-safe.")
print("\n# Hardcore Template Metadata Validation")
hardcore_metadata_errors = _hardcore_template_metadata_errors(category_paths)
if hardcore_metadata_errors: