From 55ff5df61115e99f18ac18a1a3e6f464f3c51b1c Mon Sep 17 00:00:00 2001 From: Zain Dana Harper <17142659+HarperZ9@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:10:34 -0700 Subject: [PATCH] feat(art): draw what a wiring edge settles and what it only repeats The README says an edge is declared, never probed, and then asks the reader to carry that sentence through every JSON key the tool returns. docs/art/ edge-evidence.svg draws the six keys plexus discover hands back for one edge and says, per key, whether discovery settled it or copied it out of a manifest. Four are computed: producer, consumer, capability, self_loop. One is a constant: evidence, always the word declared. One is copied and carries the hot mark: via, the producer's own pointer at the code behind the port, which plexus never imports or resolves. That is the field a reader would take for a citation, so it is the field the accent sits on. The rows are held to the code rather than to the picture. tests/test_repo_art.py now drives a real mesh: the drawn keys are compared to the keys discover actually returns, every edge's evidence is checked to be the constant, self_loop is checked to follow from the two ends and to survive into the output, and via is proven copied by pointing a manifest at a path that is not in the repository and watching the edge form with that string intact. The footnote's claim that the edge holds a second pointer it never returns is checked on both halves. The card class comes in with it: repo_card.py, check_repo_card.py, and the flow budgets split out to check_repo_flow.py so no file outgrows what a reader can hold. art.card_alt_reaches_the_readme comes too, binding the README's alt attribute to the spec's, because GitHub draws the card as an img and an img hides whatever description the SVG carries inside it. Measured against the 916px rule with the canon face loaded: widest drawn text 913.4, zero of thirty-two elements over. As a standalone document, where no webfont loads and the fallback face is narrower: 909.0, zero over. Co-Authored-By: Claude Opus 5 --- README.md | 2 + docs/art/edge-evidence.svg | 17 ++++ docs/art/plexus.art.json | 48 ++++++++++ tests/test_repo_art.py | 89 +++++++++++++++++++ tools/check_repo_art.py | 77 ++-------------- tools/check_repo_card.py | 177 +++++++++++++++++++++++++++++++++++++ tools/check_repo_flow.py | 83 +++++++++++++++++ tools/render_repo_art.py | 3 + tools/repo_card.py | 176 ++++++++++++++++++++++++++++++++++++ 9 files changed, 604 insertions(+), 68 deletions(-) create mode 100644 docs/art/edge-evidence.svg create mode 100644 tools/check_repo_card.py create mode 100644 tools/check_repo_flow.py create mode 100644 tools/repo_card.py diff --git a/README.md b/README.md index cdf35ab..4838b3e 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,8 @@ for r in results: ## Declared, not probed +

The six keys plexus discover returns for one wiring edge, one to a row, each with what settled it. Four are computed by discovery: the producing organ, the consuming organ, the capability that matched, and whether both ends are the same organ. One is copied out of a manifest without being read: via, the producer's own pointer at the code behind the port. One is a constant: evidence, always the word declared. The via row is accented, because it is the field that looks like a citation and is the one plexus never follows.

+ Every edge is tagged `evidence: "declared"` and cites the **module** its producer names as the source (`file:function`). plexus does not import, resolve, or run that pointer, so the citation is a self-reported claim to check, not a verified diff --git a/docs/art/edge-evidence.svg b/docs/art/edge-evidence.svg new file mode 100644 index 0000000..fd13942 --- /dev/null +++ b/docs/art/edge-evidence.svg @@ -0,0 +1,17 @@ +DECLARED, NEVER PROBEDWhat one wiring edge settles, and what it only repeats$ plexus discover --dir manifestsKEYWHAT SETTLED ITWHAT IT DOES NOT SETTLEproducerCOMPUTEDThe organ whose emitted port satisfied the name. Only organs in themanifest set this run loaded can appear.consumerCOMPUTEDThe organ that named this capability among its own inputs.capabilityCOMPUTEDThe consumer's input name. A port satisfies it by equality, or bynaming it in consumable_as.self_loopCOMPUTEDTrue when both ends are the same organ. Reported rather than dropped,so a cycle stays visible in the plan.viaCOPIEDThe producer's own pointer at the code behind the port. plexus neverresolves it, so a path at nothing reads as a real one.evidenceCONSTANTAlways declared. The field is here to say the edge was matched onstrings and that no tool was run.An edge forms by matching capability strings the manifests report about themselves. That is enough to settle who names what, and it is not enough to settle that thecited module exists or that the two sides compose. The edge carries a second pointer, the consumer's own module, which discover holds and never serializes, so it isnot drawn here either. diff --git a/docs/art/plexus.art.json b/docs/art/plexus.art.json index a3ebe5a..6b0f3ff 100644 --- a/docs/art/plexus.art.json +++ b/docs/art/plexus.art.json @@ -137,5 +137,53 @@ } ] } + ], + "cards": [ + { + "file": "edge-evidence.svg", + "kicker": "declared, never probed", + "title": "What one wiring edge settles, and what it only repeats", + "source": "plexus discover --dir manifests", + "heads": [ + "key", + "what settled it", + "what it does not settle" + ], + "fields": [ + { + "key": "producer", + "value": "COMPUTED", + "note": "The organ whose emitted port satisfied the name. Only organs in the manifest set this run loaded can appear." + }, + { + "key": "consumer", + "value": "COMPUTED", + "note": "The organ that named this capability among its own inputs." + }, + { + "key": "capability", + "value": "COMPUTED", + "note": "The consumer's input name. A port satisfies it by equality, or by naming it in consumable_as." + }, + { + "key": "self_loop", + "value": "COMPUTED", + "note": "True when both ends are the same organ. Reported rather than dropped, so a cycle stays visible in the plan." + }, + { + "key": "via", + "value": "COPIED", + "tone": "drift", + "note": "The producer's own pointer at the code behind the port. plexus never resolves it, so a path at nothing reads as a real one." + }, + { + "key": "evidence", + "value": "CONSTANT", + "note": "Always declared. The field is here to say the edge was matched on strings and that no tool was run." + } + ], + "alt": "The six keys plexus discover returns for one wiring edge, one to a row, each with what settled it. Four are computed by discovery: the producing organ, the consuming organ, the capability that matched, and whether both ends are the same organ. One is copied out of a manifest without being read: via, the producer's own pointer at the code behind the port. One is a constant: evidence, always the word declared. The via row is accented, because it is the field that looks like a citation and is the one plexus never follows.", + "footnote": "An edge forms by matching capability strings the manifests report about themselves. That is enough to settle who names what, and it is not enough to settle that the cited module exists or that the two sides compose. The edge carries a second pointer, the consumer's own module, which discover holds and never serializes, so it is not drawn here either." + } ] } diff --git a/tests/test_repo_art.py b/tests/test_repo_art.py index d8e0360..ebefef0 100644 --- a/tests/test_repo_art.py +++ b/tests/test_repo_art.py @@ -4,6 +4,7 @@ the spec and compares bytes. This runs the gate under pytest and asserts on its receipt, so a drifted drawing fails the suite instead of quietly shipping.""" +import dataclasses import json import subprocess import sys @@ -25,6 +26,10 @@ "art.every_illustration_is_shown", "art.tagline_stays_inside_its_rule", "art.outcome_fits_its_box", + "art.card_draws_shapes_not_digits", + "art.card_text_fits_its_column", + "art.card_carries_one_mark", + "art.card_alt_reaches_the_readme", "art.the_gate_can_fail", ) @@ -32,6 +37,7 @@ "docs/art/plexus-header.svg", "docs/art/wiring-lane.svg", "docs/art/honesty-lane.svg", + "docs/art/edge-evidence.svg", ) @@ -70,3 +76,86 @@ def test_a_gate_that_cannot_fail_is_not_a_gate(tmp_path, monkeypatch): (tmp_path / "plexus.art.json").write_text(json.dumps(spec), encoding="utf-8") monkeypatch.setattr(gate, "ART", tmp_path) assert len(gate.check_outcome_fits_its_box([])) == 1 + + +# docs/art/edge-evidence.svg draws the six keys plexus discover returns for one +# wiring edge and says, per key, whether discovery settled it or copied it. That +# is a claim about mesh.py and cli.py, not about the picture, so nothing in +# tools/ can settle it. Each row below is driven against a real mesh. +sys.path.insert(0, str(_REPO / "src")) + +from plexus.cli import _mesh_json # noqa: E402 +from plexus.manifest import Manifest, Port # noqa: E402 +from plexus.mesh import Edge, discover # noqa: E402 +from plexus.registry import builtin_manifests # noqa: E402 + + +def _card() -> dict: + spec = json.loads( + (_REPO / "docs" / "art" / "plexus.art.json").read_text("utf-8")) + return next(c for c in spec["cards"] if c["file"] == "edge-evidence.svg") + + +def _live() -> dict: + return _mesh_json(discover(builtin_manifests())) + + +def test_the_card_draws_the_keys_an_edge_actually_comes_back_with(): + """A key drawn that discover does not return, or one returned and not + drawn, makes the picture a description of a different tool.""" + drawn = [f["key"] for f in _card()["fields"]] + returned = list(_live()["edges"][0]) + assert drawn == returned + + +def test_evidence_is_the_constant_the_card_says_it_is(): + assert {e["evidence"] for e in _live()["edges"]} == {"declared"} + + +def test_self_loop_is_computed_from_the_two_ends_and_kept(): + """Drawn COMPUTED, and drawn as reported rather than dropped, so both + halves of that row need something that reaches them.""" + edges = _live()["edges"] + assert all(e["self_loop"] == (e["producer"] == e["consumer"]) + for e in edges) + assert any(e["self_loop"] for e in edges), "no self loop survived to check" + + +def test_via_is_carried_through_without_being_read(): + """Drawn COPIED. Point a producer's module at a path that is not in the + repository at all and the edge still forms, carrying that string as it was + written. Nothing between the manifest and the receipt opens it.""" + nowhere = "src/does_not_exist.py:never_defined" + assert not (_REPO / "src" / "does_not_exist.py").exists() + pair = [ + Manifest(organ="a", emits=[Port(capability="c/1", module=nowhere)]), + Manifest(organ="b", consumes=[Port(capability="c/1", module="b.py:f")]), + ] + edge = _mesh_json(discover(pair))["edges"][0] + assert edge["via"] == nowhere + assert edge["evidence"] == "declared" + + +def test_the_marked_row_is_the_one_that_looks_like_a_citation(): + """The accent claims via is the field a reader would take for proof. The + other five are drawn as settled, so the mark has to sit on via and the + accented row has to be the one the footnote is about.""" + marked = [f["key"] for f in _card()["fields"] + if f.get("tone", "none") != "none"] + assert marked == ["via"] + assert [f["value"] for f in _card()["fields"] if f["key"] == "via"] \ + == ["COPIED"] + + +def test_the_footnote_second_pointer_is_held_and_never_returned(): + """The footnote says the edge carries the consumer's own module too, and + that discover holds it without serializing it. Both halves, because a field + quietly added to the receipt would make the footnote wrong.""" + fields = {f.name for f in dataclasses.fields(Edge)} + assert "consumer_module" in fields + assert "consumer_module" not in _live()["edges"][0] + pair = [ + Manifest(organ="a", emits=[Port(capability="c/1", module="a.py:g")]), + Manifest(organ="b", consumes=[Port(capability="c/1", module="b.py:f")]), + ] + assert discover(pair).edges[0].consumer_module == "b.py:f" diff --git a/tools/check_repo_art.py b/tools/check_repo_art.py index 1cd4559..d0577b3 100644 --- a/tools/check_repo_art.py +++ b/tools/check_repo_art.py @@ -25,17 +25,14 @@ import render_repo_art as RENDER # noqa: E402 import repo_art as ART_LIB # noqa: E402 +import check_repo_card as CARD_GATE # noqa: E402 +import check_repo_flow as FLOW_GATE # noqa: E402 import repo_flow as FLOW # noqa: E402 ROOT = Path(__file__).resolve().parents[1] ART = ROOT / "docs" / "art" SCHEMA = "plexus.repo-art/v1" -# The widest tagline that has been looked at on a rendered page. It counts -# characters rather than measuring glyphs, so it cannot tell "mmmm" from -# "iiii": a guardrail, not a typographic fact. -TAGLINE_BUDGET = 70 - # Where an illustration lives. .github/assets/ and docs/brand/ are deliberately # outside this set: they hold the social-preview source and the flagship heroes, # which other gates already cover. @@ -130,18 +127,7 @@ def check_note_survives_the_wrapper(_unused: list[Path]) -> list[str]: """Card notes wrap to three lines and the wrapper drops the rest, so an edited sentence can lose its ending in the drawing while reading fine in the spec.""" - return _notes_the_wrapper_cuts(_loaded()) - - -def _notes_the_wrapper_cuts(specs: list[dict]) -> list[str]: - bad = [] - for spec in specs: - for flow in spec.get("flows", []): - for stage in flow["stages"]: - drawn = " ".join(FLOW._wrap(stage["note"])) - if drawn != " ".join(stage["note"].split()): - bad.append(f'{stage["title"]}: the drawing cuts off at "{drawn}"') - return bad + return FLOW_GATE.notes_the_wrapper_cuts(_loaded()) def check_return_edge_stays_on_its_row(_unused: list[Path]) -> list[str]: @@ -171,65 +157,19 @@ def check_every_illustration_is_shown(_unused: list[Path]) -> list[str]: def check_tagline_stays_inside_its_rule(_unused: list[Path]) -> list[str]: """The tagline is one unwrapped line under a rule that ends at x=700. Past that it runs on toward the aperture and nothing about the render fails.""" - return _taglines_that_overrun(_loaded()) - - -def _taglines_that_overrun(specs: list[dict]) -> list[str]: - bad = [] - for spec in specs: - tagline = spec["header"]["tagline"] - if len(tagline) > TAGLINE_BUDGET: - bad.append(f"{len(tagline)} characters runs past the rule: {tagline!r}") - return bad - - -def _outcome_budgets(count: int) -> tuple[int, int]: - """Label and note budgets for one box in a band of `count` boxes.""" - span = (FLOW.W - FLOW.PAD * 2 - FLOW.GAP * (count - 1)) / count - usable = span - 14 - 10 - return int(usable / 7.0), int(usable / 5.4) + return FLOW_GATE.taglines_that_overrun(_loaded()) def check_outcome_fits_its_box(_unused: list[Path]) -> list[str]: """An outcome box is one unwrapped label over one unwrapped note, and neither is clipped, so an over-long note runs into the next box.""" - return _outcomes_that_overflow(_loaded()) - - -def _outcomes_that_overflow(specs: list[dict]) -> list[str]: - bad = [] - for spec in specs: - for flow in spec.get("flows", []): - label_budget, note_budget = _outcome_budgets(len(flow["outcomes"])) - for item in flow["outcomes"]: - if len(item["label"]) > label_budget: - bad.append(f'{item["label"]!r} is wider than its box') - if len(item["note"]) > note_budget: - bad.append(f'the note under {item["label"]} is wider than ' - f'its box: {item["note"]!r}') - return bad - - -# A spec built to break all three geometry budgets at once. Every other check -# here reports clean, which says it ran and not that it works. -CONTROL = [{ - "header": {"tagline": "x" * (TAGLINE_BUDGET + 1)}, - "flows": [{ - "stages": [{"title": "CARD", "note": "word " * 60}], - "outcomes": [{"label": "OK", "note": "x" * 200}, - {"label": "y" * 200, "note": "short"}], - }], -}] + return FLOW_GATE.outcomes_that_overflow(_loaded()) def check_the_gate_can_fail(_unused: list[Path]) -> list[str]: - """Feed the three geometry checks input they have to reject.""" - return [f"the gate missed {what}" for caught, what in ( - (len(_notes_the_wrapper_cuts(CONTROL)) == 1, "a truncated note"), - (len(_taglines_that_overrun(CONTROL)) == 1, "a tagline past its rule"), - (len(_outcomes_that_overflow(CONTROL)) == 2, - "an over-wide label and an over-long note"), - ) if not caught] + """A gate that cannot fail is not a gate. Every check with a budget in it + gets handed input it has to reject, and anything that passes is named.""" + return FLOW_GATE.control_failures() + CARD_GATE.control_failures() CHECKS = [ @@ -245,6 +185,7 @@ def check_the_gate_can_fail(_unused: list[Path]) -> list[str]: ("art.every_illustration_is_shown", check_every_illustration_is_shown), ("art.tagline_stays_inside_its_rule", check_tagline_stays_inside_its_rule), ("art.outcome_fits_its_box", check_outcome_fits_its_box), +] + CARD_GATE.checks() + [ ("art.the_gate_can_fail", check_the_gate_can_fail), ] diff --git a/tools/check_repo_card.py b/tools/check_repo_card.py new file mode 100644 index 0000000..ca9c73d --- /dev/null +++ b/tools/check_repo_card.py @@ -0,0 +1,177 @@ +"""check_repo_card.py -- gates for the drawing of a record. + +The card draws a record this tool hands back, one field to a row. These guard +the drawing: text that fits the column it is drawn into, colour that still +says one thing, and values that carry the shape of a field rather than one +run's worth of digits. A picture with a hash in it is wrong by the next +commit, so a hash may not be drawn at all. + +Whether the drawn fields are TRUE of the record is a different question, and +it is asked where the record lives rather than here. + +Kept beside the art gates rather than inside them so neither file outgrows +what one person can hold at once. +""" +from __future__ import annotations + +import json +import re +from pathlib import Path + +import repo_card as CARD + +ROOT = Path(__file__).resolve().parents[1] +ART = ROOT / "docs" / "art" + +# A run of hex long enough to be a digest, and a number long enough to be a +# byte count. Either one in a value column dates the picture to one checkout. +DIGEST = re.compile(r"[0-9a-f]{12,}") +BIG_NUMBER = re.compile(r"\d{5,}") + +# The two mono columns, in characters. A monospace advance is about 0.6em, so +# these count characters against the width each column actually has. +KEY_BUDGET = int((CARD.KEY_W + CARD.GUTTER - 16) / 7.8) +VAL_BUDGET = int(CARD.VAL_W / 7.2) + +# The three column heads, in characters. They are set at 11px with a sixth of +# an em of tracking, so they run wider per character than the columns under +# them and get their own count. +HEAD_BUDGETS = (int((CARD.KEY_W + CARD.GUTTER - 16) / 8.4), + int((CARD.VAL_W + CARD.GUTTER) / 8.4), + int(CARD.NOTE_W / 8.4)) + + +def _cards() -> list[dict]: + return [card for path in sorted(ART.glob("*.art.json")) + for card in json.loads(path.read_text(encoding="utf-8")) + .get("cards", [])] + + +def values_that_are_not_shapes(cards: list[dict]) -> list[str]: + """A value column holds the shape of a field, never a run of its digits.""" + bad = [] + for card in cards: + for field in card["fields"]: + value = field["value"] + if DIGEST.search(value): + bad.append(f'{card["file"]}: {field["key"]} draws a digest, ' + f"and a digest is true for one checkout: {value!r}") + if BIG_NUMBER.search(value): + bad.append(f'{card["file"]}: {field["key"]} draws a number ' + f"that moves with the commit: {value!r}") + return bad + + +def text_that_overflows(cards: list[dict]) -> list[str]: + """Nothing is drawn wider than the column it is drawn into. The key and + the value are single unwrapped lines, so they run into their neighbour + rather than being clipped; the note and the footnote wrap by measured + width and then drop what will not fit instead of growing the drawing.""" + bad = [] + for card in cards: + for field in card["fields"]: + if len(field["key"]) > KEY_BUDGET: + bad.append(f'{card["file"]}: the {field["key"]} name runs ' + f"into the value column") + if len(field["value"]) > VAL_BUDGET: + bad.append(f'{card["file"]}: the value on {field["key"]} runs ' + f"into the note column") + drawn = " ".join(CARD._wrap(field["note"])) + if drawn != " ".join(field["note"].split()): + bad.append(f'{card["file"]}: the note on {field["key"]} cuts ' + f'off at "{drawn}"') + heads = card.get("heads", CARD.HEADS) + if len(heads) != 3: + bad.append(f'{card["file"]} names {len(heads)} columns, and the ' + f"drawing has three") + for head, budget in zip(heads, HEAD_BUDGETS): + if len(head) > budget: + bad.append(f'{card["file"]}: the {head!r} column head runs ' + f"into the column beside it") + foot = " ".join(CARD._wrap(card["footnote"], CARD.FOOT_BUDGET, + CARD.FOOT_LINES)) + if foot != " ".join(card["footnote"].split()): + bad.append(f'{card["file"]}: the footnote cuts off at "{foot}"') + return bad + + +def alt_text_that_drifted(cards: list[dict]) -> list[str]: + """The README alt attribute is the whole of what a reader who cannot see + the card gets. GitHub draws it as an , and an hides whatever + description the SVG carries inside it, so the long one in the spec has to + reach the README as it is written. Without this, a row can be re-worded + and the sentence describing it to a screen reader still says what the card + used to say.""" + shown = (ROOT / "README.md").read_text(encoding="utf-8") + return [f'{card["file"]}: the README describes it as something it is no ' + f"longer, because the spec alt is not the alt in the README" + for card in cards if card["alt"] not in shown] + + +def wrong_number_of_marks(cards: list[dict]) -> list[str]: + """Colour says one thing here. Two accents and it says nothing.""" + bad = [] + for card in cards: + hot = [f["key"] for f in card["fields"] + if f.get("tone", "none") != "none"] + if len(hot) != 1: + bad.append(f'{card["file"]} accents {len(hot)} rows, and one hot ' + f"mark per view is the whole of the colour rule") + return bad + + +def checks() -> list[tuple]: + """The card gates, in the order the receipt reports them.""" + return [ + ("art.card_draws_shapes_not_digits", + lambda _unused: values_that_are_not_shapes(_cards())), + ("art.card_text_fits_its_column", + lambda _unused: text_that_overflows(_cards())), + ("art.card_carries_one_mark", + lambda _unused: wrong_number_of_marks(_cards())), + ("art.card_alt_reaches_the_readme", + lambda _unused: alt_text_that_drifted(_cards())), + ] + + +# A card built to break every one of those at once: a digest and a byte count +# in the value column, a name and a value too wide for their columns, a +# clipped note, a clipped footnote, and two hot marks where the rule allows +# one. +# +# The fourth row is the shape that got past an earlier version of this file. A +# budget counted in characters read that note as two comfortable lines and let +# it through, and it drew forty pixels past the edge of the page, because +# capitals are wider than the lowercase prose the count was calibrated on. It +# stays here so a return to counting characters fails rather than ships. +CONTROL = [{ + "file": "control.svg", + "alt": "a description of a drawing that is in no README anywhere", + "footnote": "word " * 200, + "heads": ["z" * (HEAD_BUDGETS[0] + 1), "ok", "ok", "one column too many"], + "fields": [ + {"key": "head", "value": "9f2c4ab71de0", "note": "ok", + "tone": "verified"}, + {"key": "bytes", "value": "104857 bytes", "note": "ok", + "tone": "drift"}, + {"key": "z" * (KEY_BUDGET + 1), "value": "z" * (VAL_BUDGET + 1), + "note": "word " * 40}, + {"key": "caps", "value": "ok", "note": " ".join(["UNVERIFIABLE"] * 10)}, + ], +}] + + +def control_failures() -> list[str]: + """Feed each card gate input it has to reject, and say what got past.""" + return [f"the gate missed {what}" for caught, what in ( + (len(values_that_are_not_shapes(CONTROL)) == 2, + "a digest and a byte count drawn as values"), + (len(text_that_overflows(CONTROL)) == 7, + "an over-wide name, an over-wide value, a clipped note, a row of " + "capitals that fits a character count and not the column, a fourth " + "column, an over-wide column head and a clipped footnote"), + (len(wrong_number_of_marks(CONTROL)) == 1, + "a card wearing two hot marks"), + (len(alt_text_that_drifted(CONTROL)) == 1, + "a description that reaches no README at all"), + ) if not caught] diff --git a/tools/check_repo_flow.py b/tools/check_repo_flow.py new file mode 100644 index 0000000..fffc164 --- /dev/null +++ b/tools/check_repo_flow.py @@ -0,0 +1,83 @@ +"""check_repo_flow.py -- the budget checks behind the lane diagram. + +Every one of these guards the same failure: text that reads fine in the spec +and comes out clipped, overrun, or on top of its neighbour in the drawing. +Each takes the specs it judges as an argument, so the control below can hand +it input it has to reject. + +Kept beside the art gates rather than inside them so neither file outgrows +what one person can hold at once. +""" +from __future__ import annotations + +import repo_flow as FLOW + +# The widest tagline that has been looked at on a rendered page. It counts +# characters rather than measuring glyphs, so it cannot tell "mmmm" from +# "iiii": a guardrail, not a typographic fact. +TAGLINE_BUDGET = 70 + + +def notes_the_wrapper_cuts(specs: list[dict]) -> list[str]: + bad = [] + for spec in specs: + for flow in spec.get("flows", []): + for stage in flow["stages"]: + drawn = " ".join(FLOW._wrap(stage["note"])) + if drawn != " ".join(stage["note"].split()): + bad.append(f'{stage["title"]}: the drawing cuts off at ' + f'"{drawn}"') + return bad + + +def taglines_that_overrun(specs: list[dict]) -> list[str]: + bad = [] + for spec in specs: + tagline = spec["header"]["tagline"] + if len(tagline) > TAGLINE_BUDGET: + bad.append(f"{len(tagline)} characters runs past the rule: " + f"{tagline!r}") + return bad + + +def outcome_budgets(count: int) -> tuple[int, int]: + """Label and note budgets for one box in a band of `count` boxes.""" + span = (FLOW.W - FLOW.PAD * 2 - FLOW.GAP * (count - 1)) / count + usable = span - 14 - 10 + return int(usable / 7.0), int(usable / 5.4) + + +def outcomes_that_overflow(specs: list[dict]) -> list[str]: + bad = [] + for spec in specs: + for flow in spec.get("flows", []): + label_budget, note_budget = outcome_budgets(len(flow["outcomes"])) + for item in flow["outcomes"]: + if len(item["label"]) > label_budget: + bad.append(f'{item["label"]!r} is wider than its box') + if len(item["note"]) > note_budget: + bad.append(f'the note under {item["label"]} is wider than ' + f'its box: {item["note"]!r}') + return bad + + +# A spec built to break all three budgets at once. Every other check reports +# clean against it, which says the checks ran and not that they work. +CONTROL = [{ + "header": {"tagline": "x" * (TAGLINE_BUDGET + 1)}, + "flows": [{ + "stages": [{"title": "CARD", "note": "word " * 60}], + "outcomes": [{"label": "OK", "note": "x" * 200}, + {"label": "y" * 200, "note": "short"}], + }], +}] + + +def control_failures() -> list[str]: + """Feed each budget check input it has to reject, and say what got past.""" + return [f"the gate missed {what}" for caught, what in ( + (len(notes_the_wrapper_cuts(CONTROL)) == 1, "a truncated note"), + (len(taglines_that_overrun(CONTROL)) == 1, "a tagline past its rule"), + (len(outcomes_that_overflow(CONTROL)) == 2, + "an over-wide label and an over-long note"), + ) if not caught] diff --git a/tools/render_repo_art.py b/tools/render_repo_art.py index 8ca1a3b..e7dab49 100644 --- a/tools/render_repo_art.py +++ b/tools/render_repo_art.py @@ -19,6 +19,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from repo_art import header_svg # noqa: E402 +from repo_card import card_svg # noqa: E402 from repo_flow import flow_svg # noqa: E402 ART = Path(__file__).resolve().parents[1] / "docs" / "art" @@ -31,6 +32,8 @@ def rendered(spec_path: Path) -> dict[Path, str]: out = {spec_path.parent / f"{stem}-header.svg": header_svg(spec["header"])} for flow in spec.get("flows", []): out[spec_path.parent / flow["file"]] = flow_svg(flow) + for card in spec.get("cards", []): + out[spec_path.parent / card["file"]] = card_svg(card) return out diff --git a/tools/repo_card.py b/tools/repo_card.py new file mode 100644 index 0000000..620ac95 --- /dev/null +++ b/tools/repo_card.py @@ -0,0 +1,176 @@ +"""repo_card.py -- the artifact a tool hands back, drawn field by field. + +Every command here writes a receipt, and until now the README said so in a +sentence. A reader deciding whether to trust the tool wants to see the thing: +what fields come back, which one carries the verdict, and how they would check +each field themselves. This draws that from a spec, so the picture is data in +the repository and a gate can hold it against a receipt the tool actually +emits. + +Color still says one thing. Exactly one row carries the verdict and takes the +verified green; a row that reports drift takes the drift iris. Every other row +is ink and a hairline, because a field is structure and structure is not news. + +The value column shows a literal only where the literal is stable. A hash or a +byte count changes with the checkout, so those rows carry the shape of the +value instead: how many entries, how many keys. A picture that shows a hash is +a picture that is wrong by the next commit. +""" +from __future__ import annotations + +from repo_art import GROTESK, MONO, _esc, _num + +W = 960 +PAD = 44 +ROW_H = 46 +TOP = 142 +KEY_W = 186 +VAL_W = 258 +GUTTER = 26 +NOTE_X = PAD + KEY_W + GUTTER + VAL_W + GUTTER +NOTE_W = W - PAD - NOTE_X + +# The same two palettes the schematics use, so the whole set reads as one hand. +STYLE = """ + :root{ --void:#f4f3ef; --bone:#0b0c0e; --muted:#43474e; + --hairline:rgba(11,12,14,.16); --card:rgba(255,255,255,.66); + --verified:#1f7a52; --drift:#3a2bd6; } + @media (prefers-color-scheme: dark){ + :root{ --void:#0b0e0f; --bone:#eef1ee; --muted:#9aa39c; + --hairline:rgba(238,241,238,.18); --card:rgba(255,255,255,.05); + --verified:#5fae93; --drift:#a99cf5; } } + .bg{ fill:var(--void); } + .row{ fill:var(--card); stroke:var(--hairline); stroke-width:1.2; } + .key{ fill:var(--bone); font-size:13px; font-weight:650; } + .val{ fill:var(--muted); font-size:12px; } + .s{ fill:var(--muted); font-size:11.5px; } + .k{ fill:var(--muted); font-size:11px; letter-spacing:.16em; } + .h{ fill:var(--bone); font-size:21px; font-weight:700; } + .thin{ stroke:var(--hairline); stroke-width:1.2; fill:none; } +""" + +TONE = {"verified": "var(--verified)", "drift": "var(--drift)", + "none": "var(--hairline)"} + +# The column heads say what a row of this drawing is. A receipt reads as a +# field and what comes back in it; something else in the repository reads as +# something else, so a spec may name its own three and these are the default. +HEADS = ("field", "what comes back", "how you check it") + +# What one character draws in the note and footnote columns, in pixels at +# 11.5px. These are measured off a rendered probe rather than assumed, because +# a budget counted in characters cannot tell an uppercase line from a +# lowercase one: capitals run about a quarter wider, so a row of verdict +# tokens fits a character count and still draws off the edge of the page. +# +# The weights round up. The drawing ships to readers whose machine resolves a +# different face than the one measured, so a line that stops a little short is +# a smaller defect than one that runs past the rule. +UPPER, LOWER, DIGIT, SPACE, NARROW = 7.1, 5.75, 6.3, 3.2, 2.7 +_NARROW = frozenset(".,;:'!|") + + +def _advance(char: str) -> float: + if char == " ": + return SPACE + if char.isupper(): + return UPPER + if char.islower(): + return LOWER + if char.isdigit(): + return DIGIT + return NARROW if char in _NARROW else LOWER + + +def text_width(text: str) -> float: + """What a line of note or footnote prose draws, in pixels.""" + return sum(_advance(char) for char in text) + + +# One line of the note column, in pixels. Two lines fit the row. +NOTE_BUDGET = NOTE_W +NOTE_LINES = 2 + +# The footnote runs the width of the page at the same size, so it holds more. +FOOT_BUDGET = W - PAD * 2 +FOOT_LINES = 3 + + +def _wrap(text: str, width: float = NOTE_BUDGET, + limit: int = NOTE_LINES) -> list[str]: + """Greedy wrap by drawn width, cut to the lines the caller has room for.""" + lines: list[str] = [] + line = "" + for word in text.split(): + candidate = f"{line} {word}".strip() + if text_width(candidate) > width and line: + lines.append(line) + line = word + else: + line = candidate + if line: + lines.append(line) + return lines[:limit] + + +def _row_y(index: int) -> float: + return TOP + index * ROW_H + + +def _row(index: int, field: dict) -> str: + """One field: its name, what comes back in it, and how to check it.""" + y = _row_y(index) + tone = TONE[field.get("tone", "none")] + accent = field.get("tone", "none") != "none" + notes = "".join( + f'' + f"{_esc(line)}" + for i, line in enumerate(_wrap(field["note"]))) + rule = (f'') if accent else "" + return (f'{rule}' + f'{_esc(field["key"])}' + f'' + f'{_esc(field["value"])}{notes}') + + +def _column_heads(labels: tuple[str, str, str] = HEADS) -> str: + columns = (PAD + 16, PAD + KEY_W + GUTTER, NOTE_X) + return "".join( + f'{_esc(label.upper())}' + for label, x in zip(labels, columns)) + + +def _footnote(text: str, top: float) -> str: + return "".join( + f'{_esc(line)}' + for i, line in enumerate(_wrap(text, FOOT_BUDGET, FOOT_LINES))) + + +def card_svg(spec: dict) -> str: + """A receipt drawn field by field, with the source that produced it.""" + fields = spec["fields"] + foot = _wrap(spec["footnote"], FOOT_BUDGET, FOOT_LINES) + rule = _row_y(len(fields)) + 12 + height = rule + 22 + len(foot) * 16 + rows = "".join(_row(i, f) for i, f in enumerate(fields)) + return ( + f'' + f"" + f'' + f'' + f'{_esc(spec["kicker"].upper())}' + f'{_esc(spec["title"])}' + f'$ {_esc(spec["source"])}' + f'{_column_heads(tuple(spec.get("heads", HEADS)))}{rows}' + f'' + f'{_footnote(spec["footnote"], rule + 22)}' + "")