* feat(images): link summary on workspace image items
* fix(images): carry title source and cut first-line titles like the Library
Summaries gain title_source (stored or text). The inspector always shows a
stored title and confirms only a first-line title against the file, so a
stored title equal to the first line is no longer withheld. A link-title read
failure no longer marks a readable link unavailable. First-line titles over
80 characters end with an ellipsis, as the Library's title() does. The card
gate for unconfirmed first-line titles is pinned as an expected failure for B2.
* feat(images): unambiguous seed on workspace items
Workspace items gain `seed`, read with the shared generation-parameter
parser from the prompt/workflow records the header open already loaded
(each at most 2 MiB; no extra file reads, videos give None). A
placeholder or non-integer seed gives None. JSON integers beyond 2**53
are recovered exactly, so the card prints the digits the inspector shows.
The same parse confirms first-line card titles: a `text` title shows only
when the file's parsed positive gives the same title. One helper applies
that rule on the card and in the inspector, which now reads the positive
from its own inspection records with the same parser.
* fix(images): a deeply nested record no longer fails the workspace listing
The Python parser recurses over nested arrays where JavaScript String()
does not, so a crafted 2 KB PNG turned every workspace page holding it
(and its inspect route) into a 500. Card facts now treat RecursionError
like any record the parser cannot use: no seed and no confirmed title.
Also: Library previews skip the card parse they never show; the parse
runs after the file handle closes; the exact-seed read uses the one JSON
reader (parse_js_json(..., exact_integers=True)) and the public
number_string; record size is decided from the text length unless only
the UTF-8 byte count can tell.
* feat(images): search and filter workspace images by linked prompts
The workspace listing now takes category (exact), tags (comma list, all
required) and sort=rating, and q also matches the linked positive's text,
Library title, pair title and tag names. One bounded, parameterized SELECT
(get_image_link_matches / get_image_paths_matching) returns normalized
paths of unambiguously linked images; the listing is intersected with it.
Unlinked and ambiguous files are excluded by a category or tag filter and
sort last by rating, with a newest-first tie-break. The cursor scope covers
the new filters and sort; unknown values are still rejected.
The derived listing values and sort orders are cached per gallery scan so a
50,000-file listing with 20,000 links pages well under 150 ms at p95.
* test(images): count the filter budget everywhere, time it in one job
The 150 ms wall-clock budget failed under the CI coverage tracer, so it
is split. test_filter_work_is_bounded runs on every job and under
coverage: one filter SELECT per request, the ambiguity probe is an index
search (EXPLAIN QUERY PLAN), the per-scan values are built on the cold
request only, and a cursor page calls the sort key at most
ceil(log2 n)+3 times. test_filter_budget runs only with
PROMPTMANAGER_PERF_BUDGET=1 and no tracer, asserts the best of three
rounds' p95, and has its own non-required "perf budget" job. TESTING.md
records the convention.
* fix(images): rating sort works however many link rows the database holds
The filter SELECT counted every generated_images row against a 500,000
row cap and materialized all of them first, so a large history made
sort=rating answer 400 with nothing to narrow. The rows are now streamed
and only paths in the listing's normalized set are kept, so memory is
bounded by the listing; the cap and LinkFilterTooBroad are gone.
* fix(images): rebuild workspace listing values only where the scan changed
A TTL rescan that finds the same files reuses the previous values and
sort orders; one that finds a few changes rebuilds only those files.
Gallery invalidation and a gallery-roots change drop the derived values,
so old roots no longer hold memory. category is trimmed like tags.
* fix(images): a monitor invalidation rebuilds only the new files' values
The image monitor invalidates the gallery whenever it sees a new file,
and that dropped every derived workspace value, so the next request paid
a full cold build. Invalidation now keeps the per-file values and drops
only the sort orders; the rescan reuses every unchanged file and builds
only new or changed ones. A gallery-roots change still drops everything.
* feat(library): expose run count, last use and variant totals
* fix(library): an out-of-range last use reads as unknown, not a failed list
* perf(images): read positive counts only on a plain listing's first page
* feat(images): persist image favorites
Favorites are stored in image_favorites keyed by the gallery root label
and the relative path, set and cleared through PUT/DELETE
/prompt_manager/images/favorite with a {root_index, path} body validated
against the rooted listing. Workspace image cards carry favorite: bool.
The table is created by migration, travels with backups, is optional in
old backups and required after a restore's migration.
* fix(images): key favorites by the real root and case-normalized path
Folder names are not unique across gallery roots, so two roots shown as
"output" shared each other's stars. Favorites are now keyed by the
root's real, case-normalized path (server-side only) and a
case-normalized relative path, so a Windows case-only rename keeps its
star while distinct Linux files stay distinct. Toggles look the file up
in a per-scan dictionary instead of walking the listing.
* fix(images): favorites lookup follows monitor invalidation and the stored key bound
A monitor invalidation now also drops the favorites lookup, which held the
previous scan list until the next toggle. PUT refuses a favorite whose root
or relative key would exceed the schema's 4096-character bound, so the live
database keeps passing startup and restore validation.
* docs(testing): workspace data fields for the concept pages
* fix(images): card facts read the inspector's projected graphs
The inspector hands its browser parser only graphs the inspection
projection parsed (strict JSON, depth 64, node ceiling); a deeper record
is "limited" there, so the legacy fill never runs and no seed shows. The
card parsed the raw records instead and showed a seed the inspector did
not, e.g. for a 900-deep class_type beside a normal sampler.
project_graph() is now the one projection step, used by the inspection
response and by the card. The card runs the shared parser on the
projected graphs, the same values the browser receives: big integers
arrive as their exact digits, so the separate exact-integer re-parse and
the parse_js_json flag it needed are gone.
* fix(images): send the card seed as a decimal string
JSON.parse rounds a number past 2**53, so a listing seed such as
9223372036854788153 reached the page as 9223372036854788000 while the
inspector showed the file's digits. Every non-null card seed is now its
decimal string.
The Node parity harness now JSON.parses the real listing response and
compares String(seed) with the seed the inspector shows, including a
seed past 2**53. TESTING.md describes the string contract and drops the
removed exact re-read.
* fix(images): bound digit-string seeds and isolate each card's parse
A seed recorded as a 5000-digit string reached int(), which raises
ValueError past Python's integer string limit, and the listing answered
500. A digit-string seed now has at most 20 digits (2**64 fits), and the
conversion falls back to no seed on ValueError.
Each card's parse is also guarded in the page builder: an unexpected
error in one file's records logs a warning and gives that card no seed
and no first-line title, and the rest of the page still lists.
* fix(images): filename search survives a failed prompt-link read
On main a workspace q matched only file paths, so search worked with a
broken database. Since search also matches linked prompts, any q turned
a failed link read into a 500. A q without category, tags or the rating
sort now falls back to filename matches with a null positive_count and
logs the failure with its traceback; those linked filters still fail.
* fix(images): card reads PNG records with the inspector's reader
The card read PNG text through Pillow's header info, where a later
duplicate prompt chunk silently wins and text after IDAT is missed. The
inspector's bounded reader refuses conflicting duplicates and reads text
after IDAT, so a file with two different prompt chunks showed a card
seed the inspector refused.
PNG cards now read their records with read_inspection_file, the reader
the inspector uses. When it refuses a file the card keeps its
dimensions from Pillow and shows no seed. Other formats still use
Pillow's header info, as the inspector's header read does.
* perf(images): read each card's header facts once per gallery scan
Every page parsed each card's prompt and workflow records again, about
3.4 ms per 50 KB record set and 25 ms per 400 KB workflow, inside
ComfyUI's server process, on every scroll-back and refresh.
Dimensions, the seed and the parsed positive's display title are now
cached per root by (path, mtime), beside the scan's per-file values and
invalidated the same way: a rescan keeps unchanged files and prunes the
rest, a roots change drops them all, and only files of the current scan
are stored. An unreadable header is not stored. Links and favorites
are still read on every page.
* ci: run the perf budget on main pushes and on demand only
The wall-clock budget has under 2x margin on shared ubuntu runners, and
a red exact-head check blocks a merge that must never be rerun to green.
The job now runs on each push to main and on workflow_dispatch, not on
pull requests. TESTING.md says a red run on main means fix the code.
* fix(library): cut display titles at 80 UTF-16 units, never inside a pair
The server counted code points while the Library's JavaScript counts UTF-16
units, so a first line of emoji kept 80 emoji on cards but showed 40 in the
Library. Both now cut at 80 UTF-16 units and drop a character that would
straddle the limit instead of leaving half a surrogate pair.
440 lines
15 KiB
Python
440 lines
15 KiB
Python
"""Exact inspection-only graph projection and bounded JSON wire encoding."""
|
|
|
|
import json
|
|
import math
|
|
from decimal import Decimal, InvalidOperation
|
|
from typing import TYPE_CHECKING, Any, Literal, Optional
|
|
|
|
from .inspection_outcomes import MESSAGE_LENGTH, project_resolution
|
|
|
|
if TYPE_CHECKING:
|
|
from .prompt_inspector import InspectionRead
|
|
|
|
MAX_RESPONSE_BYTES = 8 * 1024 * 1024
|
|
MAX_LIBRARY_BYTES = 32 * 1024
|
|
_MAX_SAFE_INTEGER = 2**53 - 1
|
|
_RECORD_KEYS = frozenset(
|
|
{"prompt", "workflow", "parameters", "comfyassets_prompt_pairs"}
|
|
)
|
|
_FORMATS = frozenset({"original-text", "reserialized-json", "invalid", "limited"})
|
|
_PROMPT_NODE_KEYS = frozenset({"class_type", "inputs"})
|
|
MAX_PROJECTION_DEPTH = 64
|
|
# Backstop, not an expected outcome: executable API nodes for a prompt; UI nodes
|
|
# including subgraph definitions for a workflow.
|
|
GRAPH_NODE_CEILING = 20_000
|
|
ProjectionKind = Literal["prompt", "workflow", "carrier"]
|
|
|
|
|
|
class _ProjectionLimit(ValueError):
|
|
"""A present graph cannot be projected within inspection limits."""
|
|
|
|
|
|
def _pairs(items: list[tuple]) -> dict:
|
|
result = {}
|
|
for key, value in items:
|
|
if key in result:
|
|
raise ValueError("Duplicate JSON key")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
class _Nonfinite:
|
|
"""A parsed ``NaN``/``Infinity`` literal: never a fact, projected as null."""
|
|
|
|
|
|
_NONFINITE = _Nonfinite()
|
|
|
|
|
|
def _nonfinite(value: str) -> _Nonfinite:
|
|
return _NONFINITE
|
|
|
|
|
|
def _normalize(value: object, depth: int = 1, *, retain: bool = True) -> object:
|
|
"""Walk bounded source once; unretained values are only validated as JSON."""
|
|
if isinstance(value, (dict, list)):
|
|
if depth > MAX_PROJECTION_DEPTH:
|
|
raise _ProjectionLimit()
|
|
if isinstance(value, dict):
|
|
result = {} if retain else None
|
|
for key, child in value.items():
|
|
normalized = _normalize(child, depth + 1, retain=retain)
|
|
if retain:
|
|
result[key] = normalized
|
|
else:
|
|
result = [] if retain else None
|
|
for child in value:
|
|
normalized = _normalize(child, depth + 1, retain=retain)
|
|
if retain:
|
|
result.append(normalized)
|
|
return result
|
|
if not retain or value is _NONFINITE:
|
|
return None
|
|
if isinstance(value, Decimal):
|
|
if not value.is_finite():
|
|
raise ValueError("Nonfinite JSON number")
|
|
# str keeps large exponents compact and never rounds through a float.
|
|
return str(value)
|
|
if type(value) is int and abs(value) > _MAX_SAFE_INTEGER:
|
|
return str(value)
|
|
return value
|
|
|
|
|
|
def _project_prompt(tree: object, depth: int = 1) -> dict:
|
|
"""Each API node keeps exactly ``class_type`` and ``inputs``.
|
|
|
|
Other node keys (``_meta``, ``is_changed``, future keys) are validated and
|
|
dropped, so a nonfinite value there never reaches the projection.
|
|
"""
|
|
if not isinstance(tree, dict):
|
|
raise ValueError("Prompt graph must be an object of nodes")
|
|
if depth + 1 > MAX_PROJECTION_DEPTH: # the root and its node objects
|
|
raise _ProjectionLimit()
|
|
result = {}
|
|
for node_id, node in tree.items():
|
|
if not isinstance(node, dict):
|
|
result[node_id] = _normalize(node, depth + 1)
|
|
continue
|
|
projected = {}
|
|
for key, child in node.items():
|
|
keep = key in _PROMPT_NODE_KEYS
|
|
normalized = _normalize(child, depth + 2, retain=keep)
|
|
if keep:
|
|
projected[key] = normalized
|
|
result[node_id] = projected
|
|
return result
|
|
|
|
|
|
def _project(tree: object, kind: str, depth: int = 1) -> object:
|
|
if kind == "prompt":
|
|
return _project_prompt(tree, depth)
|
|
if kind == "workflow":
|
|
return _normalize(tree, depth)
|
|
if not isinstance(tree, dict):
|
|
raise ValueError("Malformed carrier")
|
|
result = {}
|
|
for key, member in tree.items():
|
|
if key not in ("prompt", "workflow"):
|
|
_normalize(member, depth + 1, retain=False)
|
|
elif isinstance(member, str):
|
|
result[key] = member # projected by its own kind when read
|
|
elif key == "prompt" and not isinstance(member, dict):
|
|
result[key] = _normalize(member, depth + 1)
|
|
else:
|
|
result[key] = _project(member, key, depth + 1)
|
|
return result
|
|
|
|
|
|
def _strict_projection(text: str, *, kind: ProjectionKind) -> object:
|
|
"""Parse exactly (Decimal, no duplicate keys) and project by record kind."""
|
|
try:
|
|
tree = json.loads(
|
|
text,
|
|
parse_float=Decimal,
|
|
object_pairs_hook=_pairs,
|
|
parse_constant=_nonfinite,
|
|
)
|
|
return _project(tree, kind)
|
|
except RecursionError:
|
|
raise _ProjectionLimit() from None
|
|
except InvalidOperation:
|
|
raise ValueError("JSON decimal exceeds supported range") from None
|
|
|
|
|
|
def _node_count(value: dict, *, workflow: bool) -> int:
|
|
if not workflow:
|
|
return len(value)
|
|
nodes = value.get("nodes")
|
|
count = len(nodes) if isinstance(nodes, list) else 0
|
|
definitions = value.get("definitions")
|
|
subgraphs = definitions.get("subgraphs") if isinstance(definitions, dict) else None
|
|
for subgraph in subgraphs if isinstance(subgraphs, list) else ():
|
|
inner = subgraph.get("nodes") if isinstance(subgraph, dict) else None
|
|
count += len(inner) if isinstance(inner, list) else 0
|
|
return count
|
|
|
|
|
|
def _graph(value: object, *, workflow: bool = False) -> dict:
|
|
if not isinstance(value, dict):
|
|
return {"status": "malformed", "value": None}
|
|
if _node_count(value, workflow=workflow) > GRAPH_NODE_CEILING:
|
|
return {"status": "limited", "value": None}
|
|
return {"status": "parsed", "value": value}
|
|
|
|
|
|
def project_graph(text: str, kind: Literal["prompt", "workflow"]) -> dict:
|
|
"""One record's graph as the inspector projects it: ``{status, value}``.
|
|
|
|
``status`` is ``parsed`` (``value`` is the projected graph the browser
|
|
parser receives), ``limited`` (past the depth or node bound) or
|
|
``malformed``. Shared with the workspace card so both read one bound.
|
|
"""
|
|
try:
|
|
return _graph(_strict_projection(text, kind=kind), workflow=kind == "workflow")
|
|
except _ProjectionLimit:
|
|
return {"status": "limited", "value": None}
|
|
except (ValueError, TypeError, KeyError, OverflowError):
|
|
return {"status": "malformed", "value": None}
|
|
|
|
|
|
def _metadata(inspection: "InspectionRead | None") -> dict:
|
|
formats = (
|
|
{}
|
|
if inspection is None
|
|
else {
|
|
key: value
|
|
for key, value in inspection.record_formats.items()
|
|
if key in _RECORD_KEYS and value in _FORMATS
|
|
}
|
|
)
|
|
if inspection is not None:
|
|
# A record over its read budget has no text: like ``invalid``, raw omits
|
|
# it, and its graph is ``limited`` rather than parsed from nothing.
|
|
for key in inspection.limited_records & _RECORD_KEYS:
|
|
formats[key] = "limited"
|
|
raw = (
|
|
{}
|
|
if inspection is None
|
|
else {
|
|
key: value
|
|
for key, value in inspection.metadata.items()
|
|
if key in _RECORD_KEYS
|
|
and isinstance(value, str)
|
|
and formats.get(key) != "limited"
|
|
}
|
|
)
|
|
result = {
|
|
"status": (
|
|
"unreadable"
|
|
if inspection is None
|
|
else ("available" if raw or formats else "absent")
|
|
),
|
|
"omitted_reason": None,
|
|
"raw": raw,
|
|
"record_formats": formats,
|
|
"parameters": raw.get("parameters"),
|
|
}
|
|
carrier, failed = None, None
|
|
if inspection is not None and inspection.video_carrier_text is not None:
|
|
try:
|
|
carrier = _strict_projection(inspection.video_carrier_text, kind="carrier")
|
|
except _ProjectionLimit:
|
|
failed = "limited"
|
|
except (ValueError, TypeError, OverflowError):
|
|
failed = "malformed"
|
|
for key, field in (("prompt", "prompt_graph"), ("workflow", "workflow_graph")):
|
|
graph = {"status": "absent", "value": None}
|
|
if key in raw or key in formats:
|
|
if failed or formats.get(key) == "limited":
|
|
graph["status"] = failed or "limited"
|
|
elif formats.get(key) == "invalid":
|
|
graph["status"] = "malformed"
|
|
elif carrier is None and formats.get(key) == "reserialized-json":
|
|
graph["status"] = "limited"
|
|
elif carrier is None:
|
|
graph = project_graph(raw.get(key), key)
|
|
else:
|
|
try:
|
|
value = carrier.get(key)
|
|
if isinstance(value, str):
|
|
value = _strict_projection(value, kind=key)
|
|
graph = _graph(value, workflow=key == "workflow")
|
|
except _ProjectionLimit:
|
|
graph["status"] = "limited"
|
|
except (ValueError, TypeError, KeyError, OverflowError):
|
|
graph["status"] = "malformed"
|
|
result[field] = graph
|
|
return result
|
|
|
|
|
|
def _bytes(value: dict) -> bytes:
|
|
# JSON preserves isolated surrogate evidence through escaped code units.
|
|
return json.dumps(
|
|
value, ensure_ascii=False, allow_nan=False, separators=(",", ":")
|
|
).encode("utf-8", "backslashreplace")
|
|
|
|
|
|
def _finite(value: object, *, positive: bool = False) -> bool:
|
|
try:
|
|
return (
|
|
type(value) in (int, float)
|
|
and math.isfinite(value)
|
|
and (value > 0 if positive else value >= 0)
|
|
)
|
|
except OverflowError:
|
|
return False
|
|
|
|
|
|
def _file_projection(file: dict, inspection: "InspectionRead | None") -> dict:
|
|
source = dict(file)
|
|
if inspection is not None:
|
|
source.update(
|
|
format=inspection.format, width=inspection.width, height=inspection.height
|
|
)
|
|
result = {"observation": "current-file"}
|
|
for key in ("source_label", "extension", "format"):
|
|
value = source.get(key)
|
|
if value is None and key == "format":
|
|
result[key] = None
|
|
elif isinstance(value, str):
|
|
result[key] = value
|
|
if source.get("media_type") in ("image", "video"):
|
|
result["media_type"] = source["media_type"]
|
|
for key in ("size", "modified_time", "width", "height"):
|
|
if key not in source:
|
|
continue
|
|
value = source[key]
|
|
if value is None or _finite(value, positive=key in ("width", "height")):
|
|
if key not in ("width", "height") or value is None or type(value) is int:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _library_projection(library: dict) -> dict:
|
|
status = library.get("status")
|
|
result = {
|
|
"status": (
|
|
status
|
|
if status in ("linked", "none", "ambiguous", "unavailable", "limited")
|
|
else "unavailable"
|
|
),
|
|
"scope": "saved-library",
|
|
}
|
|
for key in ("positive_id", "pair_id"):
|
|
value = library.get(key)
|
|
if value is None or type(value) is int and 0 < value <= _MAX_SAFE_INTEGER:
|
|
result[key] = value
|
|
for key in ("title", "category"):
|
|
value = library.get(key)
|
|
if value is None or isinstance(value, str):
|
|
result[key] = value
|
|
scope = library.get("metadata_scope")
|
|
if scope in ("pair", "positive", None):
|
|
result["metadata_scope"] = scope
|
|
tags = library.get("tags")
|
|
result["tags"] = (
|
|
[tag for tag in tags if isinstance(tag, str)] if isinstance(tags, list) else []
|
|
)
|
|
if len(_bytes(result)) > MAX_LIBRARY_BYTES:
|
|
return {
|
|
"status": "limited",
|
|
"scope": "saved-library",
|
|
"positive_id": None,
|
|
"pair_id": None,
|
|
"title": None,
|
|
"category": None,
|
|
"tags": [],
|
|
"metadata_scope": None,
|
|
}
|
|
return result
|
|
|
|
|
|
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:
|
|
return None
|
|
value = value[:MESSAGE_LENGTH]
|
|
while len(value.encode("utf-16-le", "surrogatepass")) > 2 * MESSAGE_LENGTH:
|
|
value = value[:-1]
|
|
return value
|
|
|
|
|
|
def _bounded_messages(prompts: dict) -> dict:
|
|
"""Keep a role ``message`` only as bounded text on a non-resolved role."""
|
|
bounded = {}
|
|
for role, side in prompts.items():
|
|
if isinstance(side, dict) and "message" in side:
|
|
side = {key: item for key, item in side.items() if key != "message"}
|
|
message = _bounded_message(prompts[role]["message"])
|
|
if message is not None and side.get("status") != "resolved":
|
|
side["message"] = message
|
|
bounded[role] = side
|
|
return bounded
|
|
|
|
|
|
def _encode_inspection(
|
|
*,
|
|
identity: dict,
|
|
file: dict,
|
|
prompts: dict,
|
|
resolution: dict,
|
|
inspection: "InspectionRead | None",
|
|
library: dict,
|
|
) -> bytes:
|
|
"""Serialize exactly once per budget step, preserving identity and roles."""
|
|
metadata = _metadata(inspection)
|
|
response = {
|
|
"success": True,
|
|
"inspection_version": 1,
|
|
**identity,
|
|
"file": _file_projection(file, inspection),
|
|
"prompts": _bounded_messages(prompts),
|
|
"resolution": project_resolution(resolution),
|
|
"metadata": metadata,
|
|
"library": _library_projection(library),
|
|
}
|
|
body = _bytes(response)
|
|
# Omission order keeps prompt evidence longest: the workflow graph first,
|
|
# then the prompt graph, then all raw records atomically.
|
|
for field in ("workflow_graph", "prompt_graph"):
|
|
if len(body) <= MAX_RESPONSE_BYTES:
|
|
break
|
|
if metadata[field]["status"] != "absent":
|
|
metadata[field] = {"status": "limited", "value": None}
|
|
body = _bytes(response)
|
|
if len(body) > MAX_RESPONSE_BYTES:
|
|
metadata.update(
|
|
raw={},
|
|
parameters=None,
|
|
status="limited",
|
|
omitted_reason="response-size-limit",
|
|
)
|
|
body = _bytes(response)
|
|
if len(body) > MAX_RESPONSE_BYTES:
|
|
raise ValueError("Inspection response exceeds limits")
|
|
return body
|
|
|
|
|
|
def encode_inspection_response(
|
|
*,
|
|
image: dict,
|
|
root_scope: str,
|
|
file: dict,
|
|
prompts: dict,
|
|
resolution: dict,
|
|
inspection: "InspectionRead | None",
|
|
library: dict,
|
|
) -> bytes:
|
|
"""Preserve the existing rooted wire and its bounded metadata projection."""
|
|
return _encode_inspection(
|
|
identity={"root_scope": root_scope, "image": image},
|
|
file=file,
|
|
prompts=prompts,
|
|
resolution=resolution,
|
|
inspection=inspection,
|
|
library=library,
|
|
)
|
|
|
|
|
|
def encode_history_inspection_response(
|
|
*,
|
|
current_output: dict,
|
|
file: dict,
|
|
prompts: dict,
|
|
resolution: dict,
|
|
inspection: "InspectionRead | None",
|
|
library: dict,
|
|
) -> bytes:
|
|
"""Use occurrence identity with the same current-file evidence projection."""
|
|
return _encode_inspection(
|
|
identity={
|
|
"history_version": 1,
|
|
"history_epoch": current_output["history_epoch"],
|
|
"current_output": current_output,
|
|
},
|
|
file=file,
|
|
prompts=prompts,
|
|
resolution=resolution,
|
|
inspection=inspection,
|
|
library=library,
|
|
)
|