Audit category selector identities
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user