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 - keep `tools/prompt_map_audit.py` passing; it now checks referenced
expression/composition/scene pools, item-template axes, object-template expression/composition/scene pools, item-template axes, object-template
metadata values for both string and object templates, registered formatter metadata values for both string and object templates, category/subcategory
policy coverage for route families, and critical route documentation plus identity uniqueness, registered formatter policy coverage for route
expected smoke coverage. families, and critical route documentation plus expected smoke coverage.
### Node / UI Path ### 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 - JSON reference validation for every `scene_pools`, `expression_pools`, and
`composition_pools` reference; `composition_pools` reference;
- item template validation so `{placeholder}` names resolve to `item_axes`. - 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 - effective category route coverage so each normalized category path has
usable item, scene, expression, composition, and hardcore route metadata usable item, scene, expression, composition, and hardcore route metadata
before runtime fallbacks can hide a gap. 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 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, critical script exits nonzero when JSON pool references, item template axes, category
route docs, critical route smoke registrations, or registered node display identities, critical route docs, critical route smoke registrations, or
names do not resolve. registered node display names do not resolve.
## Behavioral Smoke Helper ## Behavioral Smoke Helper
+131
View File
@@ -118,6 +118,10 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = (
"docs/prompt-pool-routing-map.md", "docs/prompt-pool-routing-map.md",
"multi-seed route sweeps", "multi-seed route sweeps",
), ),
(
"docs/prompt-pool-routing-map.md",
"category identity validation",
),
( (
"docs/prompt-pool-routing-map.md", "docs/prompt-pool-routing-map.md",
"node documentation validation", "node documentation validation",
@@ -141,6 +145,15 @@ ALLOWED_PROMPT_ROW_READS: set[tuple[str, str]] = {
ROUTE_POLICY_ACTION_EXCLUSIONS = {"default"} ROUTE_POLICY_ACTION_EXCLUSIONS = {"default"}
ROUTE_POLICY_POSITION_EXCLUSIONS = {"any"} ROUTE_POLICY_POSITION_EXCLUSIONS = {"any"}
RESERVED_CATEGORY_CHOICES = {
"auto_weighted",
"auto_full",
"woman",
"man",
"couple",
"group_or_layout",
"custom_random",
}
NODE_DOC_PATHS = ( NODE_DOC_PATHS = (
"docs/prompt-pool-routing-map.md", "docs/prompt-pool-routing-map.md",
"README.md", "README.md",
@@ -418,6 +431,117 @@ def _json_reference_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
return errors 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]]: def _hardcore_template_metadata_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
errors: list[tuple[str, str, str]] = [] errors: list[tuple[str, str, str]] = []
for path in paths: for path in paths:
@@ -1064,6 +1188,13 @@ def main() -> int:
return 1 return 1
print("OK: all JSON pool references and item template axes resolve.") 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") print("\n# Hardcore Template Metadata Validation")
hardcore_metadata_errors = _hardcore_template_metadata_errors(category_paths) hardcore_metadata_errors = _hardcore_template_metadata_errors(category_paths)
if hardcore_metadata_errors: if hardcore_metadata_errors: