feat(bar): add dumbbell as a Bar variant (#107)
Documents the dumbbell in type-bar.md's existing Variants section, alongside grouped and stacked bars. Reference-only, matching the precedent those two set and the one #100 landed for datalake: no example assets, no gallery entries. Not a new visual type. The count stays at 28 and SKILL.md is untouched -- no selection-table row, no frontmatter hook, no manifest description change. Covers layout on the shared 1000x500 composition, an element pattern, and five honesty rules: no truncated axis, one scale with both endpoints labelled, the connector is a gap and not a trajectory, a stated row order, and no imputed or silently dropped endpoints. Two of those rules live in a formula rather than a drawing, so they ship as a checker with adversarial fixtures per ADR 0005: - Domain resolution is exhaustive over sign. A rule phrased as "all-positive / all-negative" silently drops data that only touches zero, and drops all-zero data entirely -- where floor == ceil == 0 and the position formula divides by zero. resolve_domain() partitions the space, guarantees ceil > floor, and returns a finite fallback span for all-zero input so every dot lands on the floor. - Non-text contrast is carried by the boundary, not the fill. Accent on paper is 2.86:1 skin-wide, and shape redundancy does not waive WCAG 1.4.11, so the solid endpoint takes a 1px ink stroke (11.82:1) and the connector 55% ink on light / 40% on dark (3.19:1 and 3.24:1). The checker reads these tokens out of the reference, so prose and thresholds cannot drift apart. scripts/test-verify-dumbbell.py covers both polarities: all-zero, zero-touching, mixed-sign, empty and non-finite input, observed-extremes truncation, and the sub-3:1 treatments this replaces. Wired into CI and CONTRIBUTING alongside the treemap and geometry gates. Other things the geometry and house style forced: the focal bar pattern (12% tint plus accent stroke) does not survive at a 6px dot, so the pair separates by fill weight; a 64px row pitch from y=96 overflows the y=420 plot floor at seven rows, so pitch and origin are tabulated per row count; rgb(45,49,66) IS the dark paper, so light-theme hairlines composite to 1.000:1 and must invert; label offsets keyed to start/end put both labels inside the pair on a decreasing row; a value on the domain floor collided with its category label; and the value axis was missing the Geist Mono 7px title every other cartesian type carries. Co-authored-by: 0xDarkMatter <0xDarkMatter@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
0xDarkMatter
Claude Opus 5
parent
0947fdb6e5
commit
29917724f8
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Adversarial tests for verify-dumbbell.py - both polarities.
|
||||
|
||||
A checker that only proves the good cases pass is half a checker: it cannot
|
||||
tell you it would have caught the bug. Every rule below is tested twice, once
|
||||
for the case that must pass and once for the case that must be rejected.
|
||||
|
||||
The zero cases are the reason this file exists. A sign-based domain rule reads
|
||||
naturally and silently omits two inputs - data that only touches zero, and data
|
||||
that is entirely zero - and the second one divides by zero in the position
|
||||
formula. Both are exercised through the real scaling path here, not asserted in
|
||||
prose.
|
||||
|
||||
Usage:
|
||||
python3 scripts/test-verify-dumbbell.py
|
||||
|
||||
Exit: 0 all passed, 1 a case behaved wrongly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CHECKER = ROOT / "scripts/verify-dumbbell.py"
|
||||
|
||||
|
||||
def _load_checker():
|
||||
"""Import the checker despite its hyphenated filename."""
|
||||
spec = importlib.util.spec_from_file_location("verify_dumbbell", CHECKER)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("cannot load %s" % CHECKER)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
verify_dumbbell = _load_checker()
|
||||
|
||||
resolve_domain = verify_dumbbell.resolve_domain
|
||||
scale = verify_dumbbell.scale
|
||||
contrast = verify_dumbbell.contrast
|
||||
composite = verify_dumbbell.composite
|
||||
is_truncated = verify_dumbbell.is_truncated
|
||||
DomainError = verify_dumbbell.DomainError
|
||||
|
||||
FAILURES = []
|
||||
|
||||
|
||||
def ok(condition, label):
|
||||
if condition:
|
||||
sys.stdout.write("OK: %s\n" % label)
|
||||
else:
|
||||
FAILURES.append(label)
|
||||
sys.stdout.write("FAIL: %s\n" % label)
|
||||
|
||||
|
||||
# === DOMAIN - the cases a sign-based rule drops =============================
|
||||
|
||||
def test_all_zero_is_finite():
|
||||
"""The case that divides by zero if the rule stops at 'positive/negative'."""
|
||||
floor, ceiling = resolve_domain([0, 0, 0, 0])
|
||||
ok(ceiling > floor, "all-zero data yields a non-degenerate domain")
|
||||
coordinates = [scale(v, floor, ceiling) for v in (0, 0, 0, 0)]
|
||||
ok(all(math.isfinite(x) for x in coordinates),
|
||||
"all-zero data produces finite coordinates")
|
||||
ok(all(x == verify_dumbbell.PLOT_X0 for x in coordinates),
|
||||
"all-zero data puts every dot on the floor, which is the truth")
|
||||
|
||||
|
||||
def test_zero_touching_domains():
|
||||
floor, ceiling = resolve_domain([0, 5, 12])
|
||||
ok((floor, ceiling) == (0.0, 20.0),
|
||||
"positive data touching zero anchors the floor at zero")
|
||||
floor, ceiling = resolve_domain([-12, -5, 0])
|
||||
ok((floor, ceiling) == (-20.0, 0.0),
|
||||
"negative data touching zero anchors the ceiling at zero")
|
||||
for values in ([0, 5, 12], [-12, -5, 0]):
|
||||
floor, ceiling = resolve_domain(values)
|
||||
ok(all(math.isfinite(scale(v, floor, ceiling)) for v in values),
|
||||
"zero-touching %s scales finitely" % ("positive" if values[-1] else "negative"))
|
||||
|
||||
|
||||
def test_sign_cases_resolve():
|
||||
ok(resolve_domain([18, 66, 71]) == (0.0, 100.0),
|
||||
"all-positive data anchors the floor at zero")
|
||||
ok(resolve_domain([-71, -18]) == (-100.0, 0.0),
|
||||
"all-negative data anchors the ceiling at zero")
|
||||
floor, ceiling = resolve_domain([-20, 5, 40])
|
||||
ok(floor < 0 < ceiling, "data crossing zero brackets both sides")
|
||||
|
||||
|
||||
def test_identical_and_tiny_values():
|
||||
floor, ceiling = resolve_domain([7, 7])
|
||||
ok(ceiling > floor and math.isfinite(scale(7, floor, ceiling)),
|
||||
"a pair of identical non-zero values still scales")
|
||||
floor, ceiling = resolve_domain([0.0004, 0.0009])
|
||||
ok(ceiling > floor, "sub-unit magnitudes still yield a usable domain")
|
||||
|
||||
|
||||
def test_empty_and_non_finite_rejected():
|
||||
"""The other polarity: garbage in must raise, not return a silent domain."""
|
||||
for label, values in (("empty input", []),
|
||||
("NaN in input", [1.0, float("nan")]),
|
||||
("infinity in input", [1.0, float("inf")])):
|
||||
try:
|
||||
resolve_domain(values)
|
||||
except DomainError:
|
||||
ok(True, "%s is rejected" % label)
|
||||
else:
|
||||
ok(False, "%s was NOT rejected" % label)
|
||||
|
||||
|
||||
def test_truncated_bounds_are_detected():
|
||||
"""The honesty rule, executable: observed min/max must read as truncation."""
|
||||
values = [18, 66, 34, 71]
|
||||
ok(is_truncated(values, 18.0, 71.0),
|
||||
"bounds taken from the observed extremes are reported as truncation")
|
||||
resolved = resolve_domain(values)
|
||||
ok(not is_truncated(values, *resolved),
|
||||
"correctly resolved bounds are NOT reported as truncation")
|
||||
|
||||
|
||||
def test_scaling_preserves_gap_ratios():
|
||||
"""Two rows on one scale: drawn gaps must stay proportional to real ones."""
|
||||
values = [10, 30, 40, 80]
|
||||
floor, ceiling = resolve_domain(values)
|
||||
gap_a = scale(30, floor, ceiling) - scale(10, floor, ceiling)
|
||||
gap_b = scale(80, floor, ceiling) - scale(40, floor, ceiling)
|
||||
ok(abs(gap_b / gap_a - 2.0) < 1e-9,
|
||||
"a doubled data gap draws exactly twice as wide")
|
||||
|
||||
|
||||
# === CONTRAST - both polarities =============================================
|
||||
|
||||
def test_documented_marks_clear_the_threshold():
|
||||
for label, ink, alpha, paper in verify_dumbbell.CONNECTORS:
|
||||
ratio = contrast(composite(ink, alpha, paper), paper)
|
||||
ok(ratio >= 3.0, "%s clears 3:1 (%.3f:1)" % (label, ratio))
|
||||
for label, stroke, paper in verify_dumbbell.ENDPOINT_STROKES:
|
||||
ratio = contrast(stroke, paper)
|
||||
ok(ratio >= 3.0, "%s clears 3:1 (%.3f:1)" % (label, ratio))
|
||||
|
||||
|
||||
def test_rejected_treatments_are_caught():
|
||||
"""The other polarity: the treatments this PR replaced must still fail."""
|
||||
hairline = composite("#2d3142", 0.25, "#f5f5f5")
|
||||
ok(contrast(hairline, "#f5f5f5") < 3.0,
|
||||
"the original 25%% hairline connector is correctly under 3:1")
|
||||
ok(contrast("#eb6c36", "#f5f5f5") < 3.0,
|
||||
"an unstroked accent endpoint is correctly under 3:1 on light paper")
|
||||
ok(contrast("#7a8399", "#f5f5f5") < 4.5,
|
||||
"the soft token is correctly under 4.5:1 and unusable for text")
|
||||
|
||||
|
||||
def test_checker_fails_closed_on_a_stripped_reference(tmp_path: Path):
|
||||
empty = tmp_path / "type-bar.md"
|
||||
empty.write_text("# Bar\n\nNo variants here.\n", encoding="utf-8")
|
||||
findings = verify_dumbbell.check_reference(empty)
|
||||
ok(bool(findings), "a reference with no dumbbell section is a finding, not a pass")
|
||||
|
||||
missing = tmp_path / "partial.md"
|
||||
missing.write_text("# Bar\n\n## Variants\n\n- Dumbbell: floor and ceil.\n",
|
||||
encoding="utf-8")
|
||||
findings = verify_dumbbell.check_reference(missing)
|
||||
ok(bool(findings),
|
||||
"a dumbbell section missing the connector tokens is a finding")
|
||||
|
||||
absent = tmp_path / "nope.md"
|
||||
ok(bool(verify_dumbbell.check_reference(absent)),
|
||||
"a missing reference file is a finding, not a pass")
|
||||
|
||||
|
||||
def test_shipped_reference_passes():
|
||||
findings = verify_dumbbell.check_reference(verify_dumbbell.REFERENCE)
|
||||
ok(not findings,
|
||||
"the shipped reference documents every token the checker verifies")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import tempfile
|
||||
|
||||
test_all_zero_is_finite()
|
||||
test_zero_touching_domains()
|
||||
test_sign_cases_resolve()
|
||||
test_identical_and_tiny_values()
|
||||
test_empty_and_non_finite_rejected()
|
||||
test_truncated_bounds_are_detected()
|
||||
test_scaling_preserves_gap_ratios()
|
||||
test_documented_marks_clear_the_threshold()
|
||||
test_rejected_treatments_are_caught()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
test_checker_fails_closed_on_a_stripped_reference(Path(directory))
|
||||
test_shipped_reference_passes()
|
||||
|
||||
if FAILURES:
|
||||
sys.stderr.write("\n%d case(s) behaved wrongly:\n" % len(FAILURES))
|
||||
for failure in FAILURES:
|
||||
sys.stderr.write(" - %s\n" % failure)
|
||||
return 1
|
||||
sys.stdout.write("\nAll dumbbell checker tests passed\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the two dumbbell rules that live in a formula rather than in a drawing.
|
||||
|
||||
Both rules in `references/type-bar.md` are formula-level: a generator implements
|
||||
them arithmetically, and both fail in ways that render perfectly. Prose alone
|
||||
cannot defend either, so both are executable here and the reference is checked
|
||||
against this module rather than the other way round.
|
||||
|
||||
1. DOMAIN RESOLUTION - the axis bounds are a function of the data's range, never
|
||||
of its observed extremes. Taking `min`/`max` as the bounds IS the truncation
|
||||
the reference forbids: the plot width is fixed, so a narrowed domain raises
|
||||
px-per-unit and draws every gap wider. The subtle case is zero. A sign-based
|
||||
rule that says "all-positive" and "all-negative" silently omits data that
|
||||
only touches zero, and omits the all-zero dataset entirely - where the naive
|
||||
reading yields `floor == ceil == 0` and the position formula divides by zero.
|
||||
`resolve_domain` partitions the space exhaustively and guarantees
|
||||
`ceil > floor`, so coordinates are always finite.
|
||||
|
||||
2. NON-TEXT CONTRAST - the solid endpoint and the connector are the marks that
|
||||
carry "which series" and "which pair". WCAG 1.4.11 asks 3:1 of a graphical
|
||||
object required to understand the content, and shape redundancy does not
|
||||
waive it: a reader still has to see the solid mark's boundary and the line
|
||||
joining the pair. Accent-on-paper is 2.86:1 skin-wide and cannot carry that,
|
||||
so the boundary is carried by a stroke and the connector by an alpha that
|
||||
clears 3:1 in both themes. The thresholds are checked here against the tokens
|
||||
the reference actually documents, so the two cannot drift apart.
|
||||
|
||||
The check FAILS CLOSED. A reference this cannot parse, or a token it cannot
|
||||
find, is a finding - never a silent pass.
|
||||
|
||||
Usage:
|
||||
python3 scripts/verify-dumbbell.py
|
||||
python3 scripts/verify-dumbbell.py --reference path/to/type-bar.md
|
||||
|
||||
Exit: 0 clean, 1 findings, 2 usage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
REFERENCE = ROOT / "skills/diagram-design/references/type-bar.md"
|
||||
|
||||
# --- composition constants, quoted from the reference -----------------------
|
||||
PLOT_X0 = 200 # left edge of the plot area
|
||||
PLOT_WIDTH = 760 # 200 -> 960
|
||||
MIN_GAP_PX = 16 # below this two r=6 marks read as one blob
|
||||
|
||||
# All-zero data has no range to scale against. A finite fallback span keeps the
|
||||
# position formula defined; every dot lands on the floor, which is the truth.
|
||||
ALL_ZERO_SPAN = 1.0
|
||||
|
||||
# --- skin tokens ------------------------------------------------------------
|
||||
PAPER_LIGHT, PAPER_DARK = "#f5f5f5", "#2d3142"
|
||||
WCAG_NON_TEXT = 3.0 # 1.4.11, graphical objects
|
||||
|
||||
# (label, ink hex, alpha, paper) - the connector in each theme
|
||||
CONNECTORS = [
|
||||
("connector (light)", "#2d3142", 0.55, PAPER_LIGHT),
|
||||
("connector (dark)", "#f5f5f5", 0.40, PAPER_DARK),
|
||||
]
|
||||
# (label, stroke hex, paper) - the boundary of the solid endpoint
|
||||
ENDPOINT_STROKES = [
|
||||
("solid endpoint stroke (light)", "#2d3142", PAPER_LIGHT),
|
||||
("solid endpoint stroke (dark)", "#f5f5f5", PAPER_DARK),
|
||||
]
|
||||
# Tokens the reference must actually name, so prose and code cannot drift.
|
||||
REQUIRED_TOKENS = [
|
||||
"rgba(45,49,66,0.55)",
|
||||
"rgba(245,245,245,0.40)",
|
||||
]
|
||||
|
||||
|
||||
class DomainError(ValueError):
|
||||
"""Raised when a domain cannot be resolved at all."""
|
||||
|
||||
|
||||
# === DOMAIN =================================================================
|
||||
|
||||
def _nice_magnitude(value: float) -> float:
|
||||
"""Smallest round number >= |value|, from the 1 / 2 / 2.5 / 5 / 10 ladder."""
|
||||
magnitude = abs(value)
|
||||
if magnitude == 0:
|
||||
return 0.0
|
||||
exponent = math.floor(math.log10(magnitude))
|
||||
base = 10.0 ** exponent
|
||||
for step in (1.0, 2.0, 2.5, 5.0, 10.0):
|
||||
candidate = step * base
|
||||
if candidate >= magnitude - 1e-12:
|
||||
return candidate
|
||||
return 10.0 * base
|
||||
|
||||
|
||||
def resolve_domain(values):
|
||||
"""Return (floor, ceil) for a dumbbell's value axis.
|
||||
|
||||
Exhaustive over the sign of the data, including the two cases a sign-based
|
||||
rule drops: data that only touches zero, and data that is entirely zero.
|
||||
Guarantees ceil > floor, so the position formula never divides by zero.
|
||||
"""
|
||||
numbers = [float(v) for v in values]
|
||||
if not numbers:
|
||||
raise DomainError("no values: a dumbbell needs at least one pair")
|
||||
if any(math.isnan(v) or math.isinf(v) for v in numbers):
|
||||
raise DomainError("non-finite value in input")
|
||||
|
||||
low, high = min(numbers), max(numbers)
|
||||
|
||||
if low == 0.0 and high == 0.0:
|
||||
floor, ceiling = 0.0, ALL_ZERO_SPAN # every dot sits on the floor
|
||||
elif low >= 0.0:
|
||||
floor, ceiling = 0.0, _nice_magnitude(high)
|
||||
elif high <= 0.0:
|
||||
floor, ceiling = -_nice_magnitude(low), 0.0
|
||||
else:
|
||||
floor, ceiling = -_nice_magnitude(low), _nice_magnitude(high)
|
||||
|
||||
if not ceiling > floor:
|
||||
raise DomainError(
|
||||
"degenerate domain %r..%r from values %r" % (floor, ceiling, numbers)
|
||||
)
|
||||
return floor, ceiling
|
||||
|
||||
|
||||
def scale(value: float, floor: float, ceiling: float) -> float:
|
||||
"""Map a value onto the plot's x axis. Mirrors the reference's formula."""
|
||||
span = ceiling - floor
|
||||
if span <= 0:
|
||||
raise DomainError("non-positive span %r" % (span,))
|
||||
return PLOT_X0 + (float(value) - floor) / span * PLOT_WIDTH
|
||||
|
||||
|
||||
def is_truncated(values, floor: float, ceiling: float) -> bool:
|
||||
"""True when the bounds hug the data instead of following its sign.
|
||||
|
||||
This is the failure the honesty rule exists to prevent: bounds taken from
|
||||
the observed extremes rather than resolved from the data's range.
|
||||
"""
|
||||
resolved_floor, resolved_ceiling = resolve_domain(values)
|
||||
return (floor, ceiling) != (resolved_floor, resolved_ceiling)
|
||||
|
||||
|
||||
# === CONTRAST ===============================================================
|
||||
|
||||
def _channel(component: int) -> float:
|
||||
fraction = component / 255.0
|
||||
if fraction <= 0.04045:
|
||||
return fraction / 12.92
|
||||
return ((fraction + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def relative_luminance(color: str) -> float:
|
||||
value = color.lstrip("#")
|
||||
red, green, blue = (int(value[i:i + 2], 16) for i in (0, 2, 4))
|
||||
return 0.2126 * _channel(red) + 0.7152 * _channel(green) + 0.0722 * _channel(blue)
|
||||
|
||||
|
||||
def contrast(foreground: str, background: str) -> float:
|
||||
first, second = relative_luminance(foreground), relative_luminance(background)
|
||||
lighter, darker = max(first, second), min(first, second)
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
|
||||
|
||||
def composite(foreground: str, alpha: float, background: str) -> str:
|
||||
"""Flatten a translucent stroke onto its background."""
|
||||
fore = [int(foreground.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4)]
|
||||
back = [int(background.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4)]
|
||||
blended = [round(fore[i] * alpha + back[i] * (1 - alpha)) for i in range(3)]
|
||||
return "#%02x%02x%02x" % tuple(blended)
|
||||
|
||||
|
||||
# === CHECKS =================================================================
|
||||
|
||||
def check_domain_rules():
|
||||
"""Exercise the scaling path over every sign case, including the zero ones."""
|
||||
findings = []
|
||||
cases = [
|
||||
("all zero", [0, 0, 0, 0]),
|
||||
("zero-touching positive", [0, 0, 5, 12]),
|
||||
("zero-touching negative", [-12, -5, 0, 0]),
|
||||
("all positive", [18, 66, 34, 71]),
|
||||
("all negative", [-71, -34, -66, -18]),
|
||||
("crossing zero", [-20, 5, 40, -3]),
|
||||
("single identical pair", [7, 7]),
|
||||
("tiny magnitudes", [0.0004, 0.0009]),
|
||||
]
|
||||
for label, values in cases:
|
||||
try:
|
||||
floor, ceiling = resolve_domain(values)
|
||||
except DomainError as error:
|
||||
findings.append("%s: domain unresolved (%s)" % (label, error))
|
||||
continue
|
||||
if not ceiling > floor:
|
||||
findings.append("%s: ceil %r not above floor %r" % (label, ceiling, floor))
|
||||
continue
|
||||
for value in values:
|
||||
x = scale(value, floor, ceiling)
|
||||
if not math.isfinite(x):
|
||||
findings.append("%s: non-finite coordinate for %r" % (label, value))
|
||||
elif not (PLOT_X0 - 1e-6 <= x <= PLOT_X0 + PLOT_WIDTH + 1e-6):
|
||||
findings.append(
|
||||
"%s: %r maps to %.3f, outside the plot area" % (label, value, x)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def check_contrast_rules():
|
||||
findings = []
|
||||
for label, ink, alpha, paper in CONNECTORS:
|
||||
ratio = contrast(composite(ink, alpha, paper), paper)
|
||||
if ratio < WCAG_NON_TEXT:
|
||||
findings.append(
|
||||
"%s: %.3f:1 against paper, under the %.1f:1 WCAG 1.4.11 asks"
|
||||
% (label, ratio, WCAG_NON_TEXT)
|
||||
)
|
||||
for label, stroke, paper in ENDPOINT_STROKES:
|
||||
ratio = contrast(stroke, paper)
|
||||
if ratio < WCAG_NON_TEXT:
|
||||
findings.append(
|
||||
"%s: %.3f:1 against paper, under the %.1f:1 WCAG 1.4.11 asks"
|
||||
% (label, ratio, WCAG_NON_TEXT)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def check_reference(path: Path):
|
||||
"""Fail closed: the reference must name the tokens this module verifies."""
|
||||
findings = []
|
||||
if not path.is_file():
|
||||
return ["reference not found: %s" % path]
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "Dumbbell" not in text:
|
||||
return ["reference %s carries no dumbbell section to check" % path.name]
|
||||
for token in REQUIRED_TOKENS:
|
||||
if token not in text:
|
||||
findings.append(
|
||||
"reference does not document %s, so the checked value is not the "
|
||||
"shipped one" % token
|
||||
)
|
||||
if not re.search(r"\bfloor\b", text) or not re.search(r"\bceil\b", text):
|
||||
findings.append("reference does not name the domain bounds the formula uses")
|
||||
return findings
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Verify the dumbbell's domain and non-text-contrast rules."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference", type=Path, default=REFERENCE,
|
||||
help="path to type-bar.md (default: the shipped reference)",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
findings = []
|
||||
findings += check_domain_rules()
|
||||
findings += check_contrast_rules()
|
||||
findings += check_reference(args.reference)
|
||||
|
||||
if findings:
|
||||
for finding in findings:
|
||||
sys.stderr.write("FAIL dumbbell: %s\n" % finding)
|
||||
sys.stderr.write("Summary: %d finding(s).\n" % len(findings))
|
||||
return 1
|
||||
sys.stdout.write(
|
||||
"OK dumbbell: domain resolves finitely over every sign case "
|
||||
"(including all-zero), and the documented connector and endpoint "
|
||||
"boundary clear 3:1 in both themes\n"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user