Files
ARCADIA-Studio/harness/experiments/wrapped-label-20261001/evidence/scripts/check-wrapped-svg-initial.py
T

158 lines
8.6 KiB
Python

"""Read-only native SVG conservation check. Python 3 stdlib; never overwrite a receipt.
Usage: check-wrapped-svg.py BASELINE.svg CANDIDATE.svg E|F NEW-OUTPUT.json
Both native export directories must retain their original eight-card.json input.
"""
from pathlib import Path
import argparse
import hashlib
import json
import math
import re
import sys
import xml.etree.ElementTree as ET
TARGET = "63009b5f-539e-5914-b630-ec6b7fefb4d5"
NAME = "Technician service conversation"
FIXTURE_SHA = "9db1015369f450d5810c96a01fddb8a009d57f6aafbda5182703f15ded23d9c8"
NS = "{http://www.w3.org/2000/svg}"
# Independent expected logical metrics printed by the pinned Rust/Iced tests at
# ebb6a9980b11c4d4df2f7e413c6b135a337f6f6d. Never infer these from candidate SVG.
# Lines: canonical display text, left x, top y, shaped baseline y, glyph advance.
METRICS = {
"E": {"rect": [1102.4126, 254.5, 119.174805, 40.0], "lines": [
["Technician service", 1110.4126, 258.5, 271.8828, 103.174805],
["conversation", 1125.4089, 274.5, 287.8828, 73.18213],
]},
"F": {"rect": [1180.0, 226.2, 89.18213, 56.0], "lines": [
["Technician", 1194.2905, 230.2, 243.58281, 60.601074],
["service", 1205.0847, 246.2, 259.58282, 39.012695],
["conversation", 1188.0, 262.2, 275.58282, 73.18213],
]},
}
def require(condition, message):
if not condition:
raise ValueError(message)
def digest(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def close(actual, expected, label):
value = float(actual)
require(math.isfinite(value) and abs(value - expected) <= 0.0002,
f"{label}: expected {expected}, got {actual}")
def element_attrs(element, tag, fixed, numeric):
require(element.tag == NS + tag, f"Expected {tag}, got {element.tag}")
require(set(element.attrib) == set(fixed) | set(numeric), f"Unexpected attributes on {tag}")
for key, value in fixed.items():
require(element.get(key) == value, f"{tag}.{key} changed")
for key, value in numeric.items():
close(element.get(key), value, tag + "." + key)
def strip_immediate_label(raw, candidate):
edge = re.compile(rb'<path\b[^>]*\bdata-edge="' + TARGET.encode() + rb'"[^>]*>.*?</path>', re.DOTALL)
matches = list(edge.finditer(raw))
require(len(matches) == 1, "Target edge must occur exactly once")
start = matches[0].end()
pattern = (rb'<g data-label-experiment="wrapped">.*?</g>' if candidate
else rb'<rect\b[^>]*/><text\b[^>]*>[^<]*</text>')
label = re.compile(pattern, re.DOTALL).match(raw, start)
require(label is not None, "Target label must immediately follow its edge")
return raw[:start] + raw[label.end():], raw[start:label.end()]
def check(args, report):
baseline, candidate = Path(args.baseline), Path(args.candidate)
before, after = baseline.read_bytes(), candidate.read_bytes()
report["inputs"] = {"baseline_svg": {"path": str(baseline), **digest(before)},
"candidate_svg": {"path": str(candidate), **digest(after)}}
fixture_bytes = []
for key, svg in [("baseline_project", baseline), ("candidate_project", candidate)]:
path = svg.parent / "eight-card.json"
data = path.read_bytes()
report["inputs"][key] = {"path": str(path), **digest(data)}
require(digest(data)["sha256"] == FIXTURE_SHA, key + " differs from pinned full Project")
fixture_bytes.append(data)
require(fixture_bytes[0] == fixture_bytes[1], "Full input Projects differ")
roots = [ET.fromstring(data) for data in [before, after]]
for root in roots:
require(root.tag == NS + "svg", "Expected SVG root")
require(roots[0].attrib == roots[1].attrib, "Root attributes/viewBox changed")
require(roots[0].get("viewBox") == "24 -12 1566 874", "Pinned full scene viewBox changed")
edges = [[e for e in root.iter() if e.get("data-edge") is not None] for root in roots]
for found in edges:
require(len(found) == 12 and len({e.get("data-edge") for e in found}) == 12,
"Expected all 12 distinct edge UUIDs")
require([ET.tostring(e) for e in edges[0]] == [ET.tostring(e) for e in edges[1]],
"Edge order, UUID, path, orientation/marker, drawing attributes or title changed")
for root in roots:
children = list(root)
target_index = next(i for i, e in enumerate(children) if e.get("data-edge") == TARGET)
require(children[target_index].find(NS + "title").text == NAME, "Canonical edge title changed")
bchildren = list(roots[0])
bi = next(i for i, e in enumerate(bchildren) if e.get("data-edge") == TARGET)
element_attrs(bchildren[bi + 1], "rect", {"fill": "white"},
{"x": 1180., "y": 262.5, "width": 233., "height": 24.})
element_attrs(bchildren[bi + 2], "text", {"text-anchor": "middle", "dominant-baseline": "central",
"font-family": "sans-serif", "fill": "#637487"}, {"x": 1296.5, "y": 274.5, "font-size": 13.})
require(bchildren[bi + 2].text == NAME, "Baseline target is not canonical one-line label")
groups = [e for e in roots[1].iter() if e.get("data-label-experiment") is not None]
require(len(groups) == 1, "Exactly one experimental label group is allowed")
group = groups[0]
element_attrs(group, "g", {"data-label-experiment": "wrapped"}, {})
cchildren = list(roots[1])
ci = next(i for i, e in enumerate(cchildren) if e.get("data-edge") == TARGET)
require(cchildren[ci + 1] is group, "Wrapped group belongs immediately to target edge")
expected = METRICS[args.profile]
parts = list(group)
require(len(parts) == 2 + len(expected["lines"]), "Unexpected wrapped group contents")
element_attrs(parts[0], "title", {}, {})
require(parts[0].text == NAME and not list(parts[0]), "Canonical group title changed")
element_attrs(parts[1], "rect", {"fill": "white"}, dict(zip(["x", "y", "width", "height"], expected["rect"])))
for actual, (name, x, top, baseline_y, width) in zip(parts[2:], expected["lines"]):
element_attrs(actual, "text", {"font-family": "Segoe UI", "fill": "#637487"},
{"x": x, "y": baseline_y, "font-size": 13.})
require(actual.text == name and not list(actual), "Ordered complete display line changed")
require(" ".join(e.text for e in parts[2:]) == NAME, "Wrapping altered the complete canonical text")
close(expected["rect"][2], max(line[4] for line in expected["lines"]) + 16., "Expected advance plus padding")
close(expected["rect"][3], 8. + 16. * len(expected["lines"]), "Expected line height plus padding")
bremaining, bremoved = strip_immediate_label(before, False)
cremaining, cremoved = strip_immediate_label(after, True)
require(bremaining == cremaining,
"Raw SVG bytes outside ONLY target's immediate label presentation changed")
report.update({
"status": "PASS", "full_project_bytes_equal": True,
"edge_uuids": [e.get("data-edge") for e in edges[0]],
"all_12_edge_records_exact": True, "root_and_viewBox_exact": True,
"all_remaining_svg_bytes_exact": True, "remaining_svg": digest(bremaining),
"removed_baseline_presentation": digest(bremoved), "removed_candidate_presentation": digest(cremoved),
"expected_logical_metrics": expected, "numeric_absolute_tolerance": 0.0002,
"conservation_scope": "All edge/path/orientation/drawing, card, port and unrelated label bytes preserved; only target immediate label presentation removed for comparison.",
"limits": "Logical SVG conservation and declared typography coordinates only. No browser/font availability, glyph/raster parity, screenshot reading, native input-event history, save/reopen or usability claim.",
})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("baseline")
parser.add_argument("candidate")
parser.add_argument("profile", choices=["E", "F"])
parser.add_argument("output")
args = parser.parse_args()
report = {"status": "FAIL", "profile": args.profile,
"script": digest(Path(__file__).read_bytes()),
"metrics_source": "ebb6a9980b11c4d4df2f7e413c6b135a337f6f6d Rust/Iced test stdout; wrapped-label-regression-first.log"}
# Exclusive creation precedes reads; a failed prior run is never overwritten.
with Path(args.output).open("x", encoding="utf-8") as output:
try:
check(args, report)
except Exception as error:
report["failure"] = {"type": type(error).__name__, "message": str(error)}
json.dump(report, output, indent=2)
output.write("\n")
print(json.dumps(report, indent=2))
return 0 if report["status"] == "PASS" else 1
if __name__ == "__main__":
sys.exit(main())