fix(treemap): verify every cell share (#124)

* fix(treemap): verify every cell's area, including cells too small to label

Closes #89.

The area check derived its basis from in-cell labels, so a cell with no
label was excluded from it. That is precisely the sliver -- the cell a
4px grid distorts most, and the one that shipped 24% undersized during
review of #87. On current main, widening Oceania from 16px to 24px, a
50% area distortion, still reports OK.

Every cell now declares `data-share`, its percentage of the whole, and
the verifier checks all of them against it. Cells without it are
reported rather than skipped: a treemap whose cells cannot be parsed is
not a passing treemap, it is an unchecked one, and answering OK to it is
the defect. The label and the metadata are also cross-checked, because
two statements of one fact is a new way for the picture to lie while the
gate stays green.

Adversarial tests gain the oversized and undersized unlabelled sliver
(both pass on current main and fail here), a fail-closed case for
missing metadata, and a label/metadata divergence case.

SKILL.md `metadata.version` said 2.4 against 2.5.0 manifests. The
manifests are bumped by a script and that field is hand-maintained, so
nothing read both numbers and nothing could notice. verify-plugin-package
now asserts it tracks the manifest MAJOR.MINOR. The package test's
fixture gains the field with it -- a SKILL.md without one is not a valid
package, and a fixture missing it would only prove the gate can be
tripped by its own test data.

The examples regained the `PARTS ROUNDED, MAY NOT SUM` disclosure the
type reference requires: displayed parts round up to 8.10B against a
stated 8.09B. It was dropped when the examples were rebuilt around the
legend block. The anti-pattern list, which still forbade outright what
the honest-data rule permits once disclosed, now agrees with it.

Also nudges the info mark onto the label row of its neighbours rather
than the vertical centre of its own cell, so the bottom row reads as one
row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(treemap): reject ambiguous share metadata

---------

Co-authored-by: 0xDarkMatter <0xDarkMatter@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cathryn Lavery
2026-08-19 22:02:02 -05:00
committed by GitHub
co-authored by 0xDarkMatter Claude Fable 5
parent 91fb4f243b
commit e011b04af8
13 changed files with 442 additions and 85 deletions
+140 -48
View File
@@ -38,6 +38,7 @@ from __future__ import annotations
import argparse
import html
import math
import re
import sys
import unicodedata
@@ -46,13 +47,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ASSET_DIR = ROOT / "skills/diagram-design/assets"
CELL_RE = re.compile(
r"<rect\b[^>]*?"
r'\bx="(?P<x>-?[\d.]+)"\s+y="(?P<y>-?[\d.]+)"\s+'
r'width="(?P<w>[\d.]+)"\s+height="(?P<h>[\d.]+)"[^>]*?'
r'rx="2"',
re.IGNORECASE,
)
CELL_RE = re.compile(r'<rect\b(?P<attrs>[^>]*\brx="2"[^>]*?)/?>', re.IGNORECASE)
TEXT_RE = re.compile(r"<text\b(?P<attrs>[^>]*)>(?P<body>.*?)</text>", re.IGNORECASE | re.DOTALL)
CIRCLE_RE = re.compile(r"<circle\b(?P<attrs>[^>]*)/?>", re.IGNORECASE)
ATTR_RE = re.compile(r'(?P<name>[\w:-]+)="(?P<value>[^"]*)"')
@@ -87,10 +82,26 @@ MARKER_EPSILON = 0.01
class Box:
__slots__ = ("x", "y", "w", "h", "offset")
__slots__ = (
"x",
"y",
"w",
"h",
"offset",
"share",
"share_errors",
"rect_count",
)
def __init__(self, x: float, y: float, w: float, h: float, offset: int = 0) -> None:
def __init__(
self, x: float, y: float, w: float, h: float, offset: int = 0, share: float | None = None
) -> None:
self.x, self.y, self.w, self.h, self.offset = x, y, w, h, offset
# Declared share of the whole, from data-share. None means the cell
# never stated one, which for a treemap is itself a finding.
self.share = share
self.share_errors: list[str] = []
self.rect_count = 1
@property
def right(self) -> float:
@@ -137,25 +148,64 @@ def estimated_advance(text: str, mono: bool) -> float:
def parse_cells(source: str) -> list[Box]:
"""Deduped cell rects. Each cell is painted twice: paper mask, then body."""
"""Deduped cell rects. Each cell is painted twice: paper mask, then body.
`data-share` may sit on either rect of the pair, so the declared share is
merged across duplicates rather than read off whichever came first.
"""
seen: list[Box] = []
for m in CELL_RE.finditer(source):
box = Box(
float(m.group("x")),
float(m.group("y")),
float(m.group("w")),
float(m.group("h")),
m.start(),
)
attrs = {a.group("name"): a.group("value") for a in ATTR_RE.finditer(m.group("attrs"))}
try:
box = Box(
float(attrs["x"]),
float(attrs["y"]),
float(attrs["width"]),
float(attrs["height"]),
m.start(),
)
except (KeyError, ValueError):
continue
if "data-share" in attrs:
raw_share = attrs["data-share"]
try:
parsed_share = float(raw_share)
except ValueError:
box.share_errors.append(f"data-share {raw_share!r} is not numeric")
else:
if not math.isfinite(parsed_share):
box.share_errors.append(f"data-share {raw_share!r} is not finite")
elif not 0 < parsed_share <= 100:
box.share_errors.append(
f"data-share {raw_share!r} must be greater than 0 and at most 100"
)
else:
box.share = parsed_share
if box.area < CELL_MIN_AREA or box.w > CELL_MAX_W or box.h > CELL_MAX_H:
continue
if any(
abs(s.x - box.x) < 0.01
and abs(s.y - box.y) < 0.01
and abs(s.w - box.w) < 0.01
and abs(s.h - box.h) < 0.01
for s in seen
):
twin = next(
(
s
for s in seen
if abs(s.x - box.x) < 0.01
and abs(s.y - box.y) < 0.01
and abs(s.w - box.w) < 0.01
and abs(s.h - box.h) < 0.01
),
None,
)
if twin is not None:
twin.rect_count += 1
twin.share_errors.extend(box.share_errors)
if twin.share is not None and box.share is not None and not math.isclose(
twin.share, box.share, rel_tol=0.0, abs_tol=1e-9
):
twin.share_errors.append(
f"duplicate rects declare conflicting data-share values "
f"{twin.share:g} and {box.share:g}"
)
if twin.share is None:
twin.share = box.share
continue
seen.append(box)
return seen
@@ -223,7 +273,10 @@ def check(path: Path) -> list[str]:
source = path.read_text(encoding="utf-8")
cells = parse_cells(source)
if len(cells) < 3:
return []
return [
f"{path.name}: expected at least 3 parseable treemap cells, found {len(cells)} — "
"the area contract cannot be verified"
]
findings: list[str] = []
labels: dict[int, list[str]] = {index: [] for index in range(len(cells))}
@@ -306,31 +359,70 @@ def check(path: Path) -> list[str]:
"drop the marker and identify the sliver in the legend"
)
claims = {index: parse_claim(" ".join(texts)) for index, texts in labels.items()}
# Only compare on a basis every cell states, so the shares share a
# denominator. Percentages win when present: they are the claim itself.
percentages = {i: p for i, (p, _) in claims.items() if p}
magnitudes = {i: v for i, (_, v) in claims.items() if v}
known = percentages if len(percentages) >= len(magnitudes) else magnitudes
# Every cell is checked against its OWN declared share, so the cell most
# likely to be wrong is not the one exempt from checking. Deriving the
# basis from in-cell labels instead leaves an unlabelled sliver - exactly
# the cell a 4px grid distorts most - verified by nothing at all.
for cell in cells:
for error in cell.share_errors:
findings.append(
f"{path.name}:{line_of(source, cell.offset)}: cell {cell.w:g}x{cell.h:g} "
f"has invalid share metadata: {error}"
)
if cell.rect_count > 2:
findings.append(
f"{path.name}:{line_of(source, cell.offset)}: cell {cell.w:g}x{cell.h:g} "
f"is painted by {cell.rect_count} duplicate rects — expected at most a mask/body pair"
)
# Normalise both sides over the labelled cells only. A sliver too small to
# label is legal - it keeps its honest area and is named in the source line
# - and it must not switch this check off for the cells that do carry a
# claim, which is what comparing against the full drawn area would do.
if len(known) >= 3:
drawn_total = sum(cells[index].area for index in known)
value_total = sum(known.values())
for index, value in sorted(known.items()):
cell = cells[index]
area_share = cell.area / drawn_total * 100
value_share = value / value_total * 100
relative = (area_share - value_share) / value_share * 100
if abs(relative) > AREA_TOLERANCE:
undeclared = [cell for cell in cells if cell.share is None and not cell.share_errors]
if undeclared:
# Fail closed. A treemap whose cells cannot be parsed is not a passing
# treemap; it is an unchecked one, and saying "OK" to it is the bug.
for cell in undeclared:
findings.append(
f"{path.name}:{line_of(source, cell.offset)}: cell {cell.w:g}x{cell.h:g} "
f"has no data-share, so its area is unverifiable — every treemap cell "
f"must declare its share of the whole, including cells too small to label"
)
return findings
drawn_total = sum(cell.area for cell in cells)
if any(cell.share_errors for cell in cells):
return findings
share_total = sum(cell.share for cell in cells if cell.share is not None)
if not 99.0 <= share_total <= 101.0:
findings.append(
f"{path.name}: data-share values total {share_total:g}%, not approximately 100% — "
"every cell must declare its share of the same whole"
)
return findings
for index, cell in enumerate(cells):
assert cell.share is not None
area_share = cell.area / drawn_total * 100
declared = cell.share / share_total * 100
relative = (area_share - declared) / declared * 100
if abs(relative) > AREA_TOLERANCE:
findings.append(
f"{path.name}:{line_of(source, cell.offset)}: cell {cell.w:g}x{cell.h:g} "
f"draws {area_share:.2f}% of the area but declares {declared:.2f}% — "
f"{relative:+.1f}% relative. Area is the only encoding; resize the cell"
)
continue
# A label and the metadata are two statements of one fact. Let them
# disagree and the picture lies while the gate stays green.
percentage, _ = parse_claim(" ".join(labels[index]))
if percentage is not None and declared > 0:
drift = abs(percentage - declared)
# 1pp covers honest rounding of a displayed integer percentage.
if drift > 1.0:
findings.append(
f"{path.name}:{line_of(source, cell.offset)}: cell "
f"{cell.w:g}x{cell.h:g} labelled {' '.join(labels[index])[:30]!r} draws "
f"{area_share:.2f}% of the area for a {value_share:.2f}% value — "
f"{relative:+.1f}% relative. Area is the only encoding; resize the cell"
f"{path.name}:{line_of(source, cell.offset)}: cell {cell.w:g}x{cell.h:g} "
f"is labelled {percentage:g}% but declares {declared:.2f}% — "
f"the label and data-share must state the same fact"
)
return findings