* feat(history): add bounded chronological read projections (#209) * fix(history): validate internal continuation positions (#209) * feat(history): expose fenced history windows and run details (#209) * fix(history): release retired projection connections (#209) * feat(history): resolve reported outputs through current authorization (#209) * fix(history): reject invalid inspection targets (#209) * feat(history): add scoped history state and current-file adapter (#209) * fix(history): preserve slideshow and clear stale reentry actions (#209) * feat(history): render recorded executions and frozen run details (#209) * fix(history): recover receipts and refresh capture evidence (#209) * test(history): verify chronological history across restore and host modes (#209) * fix(history): enforce chronological marker visual parity (#209) * fix(history): resolve final chronological review findings Address #209 recovery, bounded visit restoration, exact receipt availability, and portable regression coverage. * test(history): repair Windows fixtures and review feedback Address #209 and PR #227 native path, cleanup, media and review-comment expectations. Document Windows portability preflight without changing production behavior. * ci: raise the viewerjs job budget for the larger browser suite The cross-browser workflows suite grew from 396 to 496 tests on this branch, which pushed the whole viewerjs job to about 25.5 minutes against the 25 minute cap set in #181. Two exact-head runs on this branch timed out without any test failing: run 39173019283 was killed on test 474 of 496, and run 39175125474 printed "496 passed (12.8m)" and was then cancelled during Playwright shutdown. Raise only the job budget. Browser projects, workers and per-test timeouts are unchanged. Splitting the browsers into parallel jobs is tracked separately.
323 lines
10 KiB
Python
323 lines
10 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
|
|
|
|
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"})
|
|
|
|
|
|
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
|
|
|
|
|
|
def _nonfinite(value: str) -> None:
|
|
raise ValueError("Nonfinite JSON number")
|
|
|
|
|
|
def _normalize(
|
|
value: object, depth: int = 1, *, retain: bool = True, carrier: bool = False
|
|
) -> object:
|
|
"""Walk bounded source once; unrelated carrier fields are only validated."""
|
|
if isinstance(value, (dict, list)):
|
|
if depth > 64:
|
|
raise _ProjectionLimit()
|
|
if isinstance(value, dict):
|
|
result = {} if retain else None
|
|
for key, child in value.items():
|
|
keep = retain and (not carrier or key in ("prompt", "workflow"))
|
|
normalized = _normalize(child, depth + 1, retain=keep)
|
|
if keep:
|
|
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 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 retain else None
|
|
if type(value) is int and abs(value) > _MAX_SAFE_INTEGER:
|
|
return str(value) if retain else None
|
|
return value if retain else None
|
|
|
|
|
|
def _strict_projection(text: str, *, carrier: bool = False) -> object:
|
|
try:
|
|
tree = json.loads(
|
|
text,
|
|
parse_float=Decimal,
|
|
object_pairs_hook=_pairs,
|
|
parse_constant=_nonfinite,
|
|
)
|
|
if carrier and not isinstance(tree, dict):
|
|
raise ValueError("Malformed carrier")
|
|
return _normalize(tree, carrier=carrier)
|
|
except RecursionError:
|
|
raise _ProjectionLimit() from None
|
|
except InvalidOperation:
|
|
raise ValueError("JSON decimal exceeds supported range") from None
|
|
|
|
|
|
def _graph(value: object, *, workflow: bool = False) -> dict:
|
|
if not isinstance(value, dict):
|
|
return {"status": "malformed", "value": None}
|
|
nodes = value.get("nodes") if workflow else value
|
|
if isinstance(nodes, (dict, list)) and len(nodes) > 500:
|
|
return {"status": "limited", "value": None}
|
|
return {"status": "parsed", "value": value}
|
|
|
|
|
|
def _metadata(inspection: "InspectionRead | None") -> dict:
|
|
raw = (
|
|
{}
|
|
if inspection is None
|
|
else {
|
|
key: value
|
|
for key, value in inspection.metadata.items()
|
|
if key in _RECORD_KEYS and isinstance(value, str)
|
|
}
|
|
)
|
|
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
|
|
}
|
|
)
|
|
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, carrier=True)
|
|
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:
|
|
graph["status"] = failed
|
|
elif formats.get(key) == "invalid":
|
|
graph["status"] = "malformed"
|
|
else:
|
|
try:
|
|
if carrier is not None:
|
|
value = carrier.get(key)
|
|
if isinstance(value, str):
|
|
value = _strict_projection(value)
|
|
elif formats.get(key) == "reserialized-json":
|
|
raise _ProjectionLimit()
|
|
else:
|
|
value = _strict_projection(raw[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 _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": prompts,
|
|
"resolution": resolution,
|
|
"metadata": metadata,
|
|
"library": _library_projection(library),
|
|
}
|
|
body = _bytes(response)
|
|
if len(body) > MAX_RESPONSE_BYTES:
|
|
for field in ("prompt_graph", "workflow_graph"):
|
|
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,
|
|
)
|