feat(images): match image tags in the Tags filter, search and inspector

The workspace Images listing now admits a file under a Tags filter when
every requested tag is carried by its linked positive or its current image
tag set, and q also matches image tag names (case-folded substring). Image
tags count only while a byte record is verified at the file's present
(size, mtime_ns) (spec amendment S1): the tagged records are streamed
once, only listed files carrying a filter tag or q hit are stat'ed, and
their linked positives are read through the file_path index so tags,
category, rating and the positive count combine.

The rooted inspector envelope gains a validated image_tags facet
(current / none / unavailable) read at the inspection's own stable stat.
A failed image-tag read fails a Tags filter like a failed link read and
degrades a q search to no image tags; plain listings read none. Legacy
gallery and search responses are unchanged.
This commit is contained in:
Vito Sansevero
2026-10-22 09:31:58 -07:00
parent 489d449ec8
commit 66f61de0db
9 changed files with 759 additions and 8 deletions
+42 -1
View File
@@ -6,12 +6,16 @@ from decimal import Decimal, InvalidOperation
from typing import TYPE_CHECKING, Any, Literal, Optional
from .inspection_outcomes import MESSAGE_LENGTH, project_resolution
from .validators import validate_tags
if TYPE_CHECKING:
from .prompt_inspector import InspectionRead
MAX_RESPONSE_BYTES = 8 * 1024 * 1024
MAX_LIBRARY_BYTES = 32 * 1024
# The image tag set cap (database/annotations.py MAX_IMAGE_TAGS).
MAX_IMAGE_TAGS = 100
_IMAGE_TAG_STATES = frozenset({"current", "none", "unavailable"})
_MAX_SAFE_INTEGER = 2**53 - 1
_RECORD_KEYS = frozenset(
{"prompt", "workflow", "parameters", "comfyassets_prompt_pairs"}
@@ -363,6 +367,35 @@ def _library_projection(library: dict) -> dict:
return result
def _image_tag_name(value: object) -> bool:
if not isinstance(value, str) or value != value.strip() or "," in value:
return False
try:
return validate_tags([value])
except ValueError:
return False
def project_image_tags(facet: object) -> dict:
"""The ``image_tags`` facet as the client accepts it, else unavailable.
``current`` carries the byte record's names (at most 100 valid vocabulary
names); ``none`` and ``unavailable`` carry no names. Anything else, extra
keys included, is reported as ``unavailable`` rather than passed on.
"""
unavailable = {"state": "unavailable", "tags": []}
if not isinstance(facet, dict) or set(facet) != {"state", "tags"}:
return unavailable
state, tags = facet["state"], facet["tags"]
if state not in _IMAGE_TAG_STATES or not isinstance(tags, list):
return unavailable
if state != "current" and tags:
return unavailable
if len(tags) > MAX_IMAGE_TAGS or not all(_image_tag_name(tag) for tag in tags):
return unavailable
return {"state": state, "tags": list(tags)}
def _bounded_message(value: Any) -> Optional[str]:
"""A role line as the client accepts it: 1 to 512 UTF-16 code units."""
if not isinstance(value, str) or not value:
@@ -394,6 +427,7 @@ def _encode_inspection(
resolution: dict,
inspection: "InspectionRead | None",
library: dict,
image_tags: dict | None = None,
) -> bytes:
"""Serialize exactly once per budget step, preserving identity and roles."""
metadata = _metadata(inspection)
@@ -407,6 +441,8 @@ def _encode_inspection(
"metadata": metadata,
"library": _library_projection(library),
}
if image_tags is not None:
response["image_tags"] = project_image_tags(image_tags)
body = _bytes(response)
# Omission order keeps prompt evidence longest: the workflow graph first,
# then the prompt graph, then all raw records atomically.
@@ -438,8 +474,12 @@ def encode_inspection_response(
resolution: dict,
inspection: "InspectionRead | None",
library: dict,
image_tags: dict | None = None,
) -> bytes:
"""Preserve the existing rooted wire and its bounded metadata projection."""
"""Preserve the existing rooted wire and its bounded metadata projection.
*image_tags*, when given, adds the validated ``image_tags`` facet (B3).
"""
return _encode_inspection(
identity={"root_scope": root_scope, "image": image},
file=file,
@@ -447,6 +487,7 @@ def encode_inspection_response(
resolution=resolution,
inspection=inspection,
library=library,
image_tags=image_tags,
)