From 63552c353e61b8250beb9332446971dfc04f1b82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 01:44:40 +0000 Subject: [PATCH 1/2] Add theme-aware figures: restyle fig-catalog-status, add row-basis figure Brings the README's figures into the open-coder-ai shared visual language (docs/figures/palette.py, copied byte-identical from the family's shared source): fig-catalog-status is restyled from its existing matplotlib generator into a light/dark SVG pair over the same 18-plugin catalog sample data, and a new fig-row-basis figure shows the distribution of row bases (recomputed / claimed / not-measured) in the report.json the README's own quickstart block produces, so the figure can never disagree with the JSON. Also carries the family diagram and a social-preview card, per BRIEF.md, and a CI job that regenerates every figure and fails the build on drift. Co-Authored-By: Claude Opus 5 Signed-off-by: Claude --- .github/workflows/ci.yml | 24 +++ README.md | 18 +- docs/figures/family-dark.svg | 46 +++++ docs/figures/family-light.svg | 46 +++++ docs/figures/fig-catalog-status-dark.svg | 186 ++++++++++++++++++++ docs/figures/fig-catalog-status-light.svg | 186 ++++++++++++++++++++ docs/figures/fig-row-basis-dark.svg | 23 +++ docs/figures/fig-row-basis-light.svg | 23 +++ docs/figures/make_catalog_status.py | 171 +++++++++++++++++++ docs/figures/make_family.py | 100 +++++++++++ docs/figures/make_row_basis.py | 198 ++++++++++++++++++++++ docs/figures/make_social_card.py | 66 ++++++++ docs/figures/palette.py | 131 ++++++++++++++ docs/figures/social-card.png | Bin 0 -> 29421 bytes docs/figures/social-card.svg | 10 ++ pyproject.toml | 14 ++ 16 files changed, 1241 insertions(+), 1 deletion(-) create mode 100644 docs/figures/family-dark.svg create mode 100644 docs/figures/family-light.svg create mode 100644 docs/figures/fig-catalog-status-dark.svg create mode 100644 docs/figures/fig-catalog-status-light.svg create mode 100644 docs/figures/fig-row-basis-dark.svg create mode 100644 docs/figures/fig-row-basis-light.svg create mode 100644 docs/figures/make_catalog_status.py create mode 100644 docs/figures/make_family.py create mode 100644 docs/figures/make_row_basis.py create mode 100644 docs/figures/make_social_card.py create mode 100644 docs/figures/palette.py create mode 100644 docs/figures/social-card.png create mode 100644 docs/figures/social-card.svg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8118bf1..2ce3a56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,3 +127,27 @@ jobs: run: | chock check chock sync --repo . --check + + # A stale generated figure fails the build: regenerate every docs/figures/*.svg and diff against + # what is committed. make_row_basis.py and make_social_card.py both shell out to the README's + # own quickstart block (see tools/quickstart_block.py) to draw their numbers from a real, + # freshly produced report.json, so context-report itself must be installed first. + figures: + name: Generated figures are not stale + runs-on: ubuntu-latest + # Reads the checkout; writes nothing (the regenerated files are diffed, never pushed). + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: "3.12" + - run: pip install --require-hashes -r requirements/ci.txt + - run: pip install --no-deps -e . + - name: Regenerate figures + run: cd docs/figures && for g in make_*.py; do python "$g"; done + - name: Fail on drift + run: git diff --exit-code -- docs/figures diff --git a/README.md b/README.md index 04e9b14..e5bf4b4 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,14 @@ it). Three real rows from the `report.json` this exact block just produced: relative path `./my-plugin` you passed, so the hook only starts from the one cwd where that path still points at the plugin — the exact failure mode the measurement below calls out. +The full statement has 11 rows, not 3 — every row's basis, from this exact quickstart block run +fresh: + + + + Of 11 rows in the report.json this quickstart block produces, 3 are recomputed from the artifact, 1 is the author's unverified claim, and 7 have not been measured this run — shown as absence of evidence, not a weak score. + + Beyond one statement at a time, `context-report run` drives a whole manifest — subjects × models × tasks — and `context-report compare` puts every run of that manifest side by side; see [`docs/cli.md`](docs/cli.md) for the manifest schema, `--dry-run`/`--n`/`--resume`, and which @@ -143,7 +151,10 @@ skills. Three findings from that run: `FAILED, 0 of 4`, exit 126. Every hook that runs, across both samples, allows on malformed input. See [§5.2](paper/context-report.md#52-top-n-catalog-plugins). -![Eighteen plugins by four measured attributes](paper/figures/fig-catalog-status.svg) + + + Grid of 18 catalog plugins by 4 measured attributes: three plugins fail reachability outright, every plugin that measures malformed-input handling passes it, and not-applicable cells are shown as a neutral dash, never a low score. + **Cost spans two orders of magnitude.** Hooks that shell out to `npx` cost 916.8–941.5 ms p50; a local script costs 7.3–53.9 ms. Context weight varies about a hundredfold across the sample, @@ -244,6 +255,11 @@ merge either way. ## Part of open-coder-ai + + + The open-coder-ai family: agentseam is the foundation, chock sits on it, chock-catalog feeds chock and generates the four plugin repositories, chock-threat-intel feeds the catalog, and context-report runs as a verification arm measuring all four. + + | | | |---|---| | [agentseam](https://github.com/open-coder-ai/agentseam) | the primitives — one handler API and a verified capability matrix across 16 agents | diff --git a/docs/figures/family-dark.svg b/docs/figures/family-dark.svg new file mode 100644 index 0000000..af55724 --- /dev/null +++ b/docs/figures/family-dark.svg @@ -0,0 +1,46 @@ + + The open-coder-ai family + A layered diagram. agentseam is the foundation across the bottom; chock sits on it; chock-catalog feeds chock and generates the four plugin repositories; chock-threat-intel feeds the catalog; context-report runs as a verification arm beside all of them. + + + chock-threat-intel + the threat ledger the catalog's + policies answer to + + plugin repositories + the catalog, packaged for each + agent's plugin format (generated) + + + + + + chock-catalog + the policies — 39, each labelled enforced or advisory, with replayed evals + + + + chock + the compiler — one policy into git hooks, CI gates and native pre-tool + hooks + + + + agentseam + the primitives — one handler API and a verified capability matrix across + 16 agents + + context-report + the evidence — a + signed report of + whether an agent + artifact actually + works + measures all four + + + + + + + diff --git a/docs/figures/family-light.svg b/docs/figures/family-light.svg new file mode 100644 index 0000000..cb95f50 --- /dev/null +++ b/docs/figures/family-light.svg @@ -0,0 +1,46 @@ + + The open-coder-ai family + A layered diagram. agentseam is the foundation across the bottom; chock sits on it; chock-catalog feeds chock and generates the four plugin repositories; chock-threat-intel feeds the catalog; context-report runs as a verification arm beside all of them. + + + chock-threat-intel + the threat ledger the catalog's + policies answer to + + plugin repositories + the catalog, packaged for each + agent's plugin format (generated) + + + + + + chock-catalog + the policies — 39, each labelled enforced or advisory, with replayed evals + + + + chock + the compiler — one policy into git hooks, CI gates and native pre-tool + hooks + + + + agentseam + the primitives — one handler API and a verified capability matrix across + 16 agents + + context-report + the evidence — a + signed report of + whether an agent + artifact actually + works + measures all four + + + + + + + diff --git a/docs/figures/fig-catalog-status-dark.svg b/docs/figures/fig-catalog-status-dark.svg new file mode 100644 index 0000000..6588b6a --- /dev/null +++ b/docs/figures/fig-catalog-status-dark.svg @@ -0,0 +1,186 @@ + + Catalog plugin status: 18 plugins by 4 measured attributes + A grid of 18 public Claude Code plugins by reachability, malformed-input handling, latency and context tokens, one cell per statement's result. Three plugins fail reachability outright; every plugin that measures malformed-input handling passes it; cells marked not-applicable (dash, neutral grey) mean the row does not apply to that plugin, not that it scored low. + + one cell per statement's result, from the 18-plugin catalog sample + reachability + malformed + input + latency + context + tokens + agentforce-adlc + + P + + P + + P + + P + ai-plugins + + P + + P + + P + + P + altimate-code + + - + + - + + - + + P + aws-core + + P + + P + + P + + P + aws-serverless + + - + + - + + - + + P + aws-startup-advisor + + - + + - + + - + + P + azure + + - + + - + + - + + P + carta-cap-table + + F + + P + + E + + P + carta-crm + + F + + P + + E + + P + carta-investors + + F + + P + + E + + P + protect-mcp + + P + + P + + P + + P + review-agent-governance + + P + + P + + P + + P + planning-with-files + + P + + P + + P + + P + 42crunch-api-security-testing + + - + + - + + - + + P + activecampaign + + - + + - + + - + + P + adobe-for-creativity + + - + + - + + - + + P + agent-sdk-dev + + - + + - + + - + + - + aikido + + - + + - + + - + + P + + P + PASSED + + E + Error + + F + FAILED + + - + n/a (not applicable) + diff --git a/docs/figures/fig-catalog-status-light.svg b/docs/figures/fig-catalog-status-light.svg new file mode 100644 index 0000000..c75b3b1 --- /dev/null +++ b/docs/figures/fig-catalog-status-light.svg @@ -0,0 +1,186 @@ + + Catalog plugin status: 18 plugins by 4 measured attributes + A grid of 18 public Claude Code plugins by reachability, malformed-input handling, latency and context tokens, one cell per statement's result. Three plugins fail reachability outright; every plugin that measures malformed-input handling passes it; cells marked not-applicable (dash, neutral grey) mean the row does not apply to that plugin, not that it scored low. + + one cell per statement's result, from the 18-plugin catalog sample + reachability + malformed + input + latency + context + tokens + agentforce-adlc + + P + + P + + P + + P + ai-plugins + + P + + P + + P + + P + altimate-code + + - + + - + + - + + P + aws-core + + P + + P + + P + + P + aws-serverless + + - + + - + + - + + P + aws-startup-advisor + + - + + - + + - + + P + azure + + - + + - + + - + + P + carta-cap-table + + F + + P + + E + + P + carta-crm + + F + + P + + E + + P + carta-investors + + F + + P + + E + + P + protect-mcp + + P + + P + + P + + P + review-agent-governance + + P + + P + + P + + P + planning-with-files + + P + + P + + P + + P + 42crunch-api-security-testing + + - + + - + + - + + P + activecampaign + + - + + - + + - + + P + adobe-for-creativity + + - + + - + + - + + P + agent-sdk-dev + + - + + - + + - + + - + aikido + + - + + - + + - + + P + + P + PASSED + + E + Error + + F + FAILED + + - + n/a (not applicable) + diff --git a/docs/figures/fig-row-basis-dark.svg b/docs/figures/fig-row-basis-dark.svg new file mode 100644 index 0000000..9ab7e02 --- /dev/null +++ b/docs/figures/fig-row-basis-dark.svg @@ -0,0 +1,23 @@ + + Row bases in one context-report statement + Of 11 rows in the report.json the README's own quickstart block produces fresh, 3 are recomputed from the artifact, 1 is the author's unverified claim, and 7 have not been measured this run -- shown as absence of evidence, not a weak score. + + + + + + report.json from the README's quickstart, run fresh -- 11 rows total + recomputed from the artifact + re-derivable, and a real result was measured + + 3 + the author's claim + basis: claimed -- not independently checked this run + + 1 + not measured this run + re-derivable in principle; NotAvailable/Error here + + 7 + ordered by strength of evidence: recomputed > claimed > not measured (absence, not a lower grade) + diff --git a/docs/figures/fig-row-basis-light.svg b/docs/figures/fig-row-basis-light.svg new file mode 100644 index 0000000..1207823 --- /dev/null +++ b/docs/figures/fig-row-basis-light.svg @@ -0,0 +1,23 @@ + + Row bases in one context-report statement + Of 11 rows in the report.json the README's own quickstart block produces fresh, 3 are recomputed from the artifact, 1 is the author's unverified claim, and 7 have not been measured this run -- shown as absence of evidence, not a weak score. + + + + + + report.json from the README's quickstart, run fresh -- 11 rows total + recomputed from the artifact + re-derivable, and a real result was measured + + 3 + the author's claim + basis: claimed -- not independently checked this run + + 1 + not measured this run + re-derivable in principle; NotAvailable/Error here + + 7 + ordered by strength of evidence: recomputed > claimed > not measured (absence, not a lower grade) + diff --git a/docs/figures/make_catalog_status.py b/docs/figures/make_catalog_status.py new file mode 100644 index 0000000..d58de6b --- /dev/null +++ b/docs/figures/make_catalog_status.py @@ -0,0 +1,171 @@ +"""Restyles fig-catalog-status into the shared open-coder-ai visual language. + +Same measurement as `paper/figures/make_figures.py`'s `fig_catalog_status` (18 public catalog +plugins x 4 measured attributes, one cell per statement's `result`) and the same data files; +only the rendering changes, from matplotlib to a light/dark SVG pair drawn with `palette.py`. + +Run from this directory: `python make_catalog_status.py`. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import palette as p + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +CATALOG_DIR = REPO / "paper" / "measurements" / "catalog-sample" +ORDER_FILE = REPO / "paper" / "figures" / "data" / "catalog_plugin_order.json" + +# The same four attributes paper/figures/make_figures.py's STATUS_ATTRS puts in this grid. +ATTRS = [ + ("reachability", "reachability"), + ("fault.malformedOutput", "malformed\ninput"), + ("cost.latency_ms", "latency"), + ("cost.context_tokens", "context\ntokens"), +] + +# Every result this grid actually contains (verified against the committed statements): PASSED, +# FAILED and Error are three distinct, evidence-bearing outcomes -- the ENFORCEMENT ramp's three +# steps, ordered here from least to most cause for concern. NotApplicable is not a fourth grade of +# that ramp: it means the row does not apply (e.g. "declares no hooks"), which is absence of +# evidence, so it is always NEUTRAL, never a ramp colour. +GLYPHS = {"PASSED": "P", "Error": "E", "FAILED": "F", "NotApplicable": "-"} +RESULT_ORDER = ["PASSED", "Error", "FAILED", "NotApplicable"] + +W = 700 +MARGIN = 16 +NAME_COL_W = 220 +COL_W = 108 +HEADER_TOP = 40 +HEADER_H = 34 +ROW_H = 22 +GRID_X0 = MARGIN + NAME_COL_W +GRID_TOP = HEADER_TOP + HEADER_H + 6 + + +def _load_plugins() -> list[dict]: + """One entry per plugin, in `catalog_plugin_order.json`'s order (mirrors make_figures.py's + `_load_catalog_plugins`, reimplemented here without the matplotlib import that module carries, + since this generator must stay stdlib-only).""" + order = json.loads(ORDER_FILE.read_text(encoding="utf-8")) + plugins = [] + for entry in order: + stmt = json.loads( + (CATALOG_DIR / entry["marketplace"] / f"{entry['plugin']}.json").read_text( + encoding="utf-8" + ) + ) + rows = {row["attribute"]: row for row in stmt["predicate"]["attributes"]} + plugins.append({"plugin": entry["plugin"], "rows": rows}) + return plugins + + +def _cell_colour(result: str, t: dict) -> str: + if result == "NotApplicable": + return t["neutral"] + return t["enforcement"][RESULT_ORDER.index(result)] + + +def _cell_text_colour(result: str, t: dict) -> str: + """Dark glyph on a light fill, light glyph on a dark fill; NEUTRAL always reads as text.""" + if result in ("Error", "FAILED"): + return t["surface"] + return t["text"] + + +def render(t: dict, name: str) -> str: # noqa: ARG001 -- write_pair's render(t, name) contract + plugins = _load_plugins() + n = len(plugins) + grid_bottom = GRID_TOP + n * ROW_H + legend_y = grid_bottom + 28 + height = legend_y + 24 + + svg = p.open_svg( + W, + height, + t, + "Catalog plugin status: 18 plugins by 4 measured attributes", + "A grid of 18 public Claude Code plugins by reachability, malformed-input handling, " + "latency and context tokens, one cell per statement's result. Three plugins fail " + "reachability outright; every plugin that measures malformed-input handling passes it; " + "cells marked not-applicable (dash, neutral grey) mean the row does not apply to that " + "plugin, not that it scored low.", + ) + + svg += p.text( + MARGIN, + 26, + "one cell per statement's result, from the 18-plugin catalog sample", + t["secondary"], + 12, + ) + + for col, (_attr, label) in enumerate(ATTRS): + cx = GRID_X0 + col * COL_W + COL_W / 2 + lines = label.split("\n") + y0 = HEADER_TOP + 14 if len(lines) == 1 else HEADER_TOP + 6 + for i, line in enumerate(lines): + svg += p.text(cx, y0 + i * 14, line, t["text"], 11, weight="600", anchor="middle") + + for row, plugin in enumerate(plugins): + y = GRID_TOP + row * ROW_H + svg += p.text( + MARGIN, + y + ROW_H / 2 + 4, + plugin["plugin"], + t["text"], + 10, + p.MONO, + ) + for col, (attr, _label) in enumerate(ATTRS): + result = plugin["rows"][attr]["result"] + x = GRID_X0 + col * COL_W + p.GAP + fill = _cell_colour(result, t) + svg += p.box(x, y + p.GAP, COL_W - 2 * p.GAP, ROW_H - 2 * p.GAP, fill, rx=3) + svg += p.text( + x + (COL_W - 2 * p.GAP) / 2, + y + ROW_H / 2 + 4, + GLYPHS[result], + _cell_text_colour(result, t), + 10, + weight="600", + anchor="middle", + ) + + entries = [ + ( + r, + { + "PASSED": "PASSED", + "Error": "Error", + "FAILED": "FAILED", + "NotApplicable": "n/a (not applicable)", + }[r], + ) + for r in RESULT_ORDER + ] + entry_w = (W - 2 * MARGIN) / len(entries) + for i, (result, label) in enumerate(entries): + x = MARGIN + i * entry_w + fill = _cell_colour(result, t) + svg += p.box(x, legend_y, 16, 16, fill, rx=3) + svg += p.text( + x + 8, + legend_y + 12, + GLYPHS[result], + _cell_text_colour(result, t), + 9, + weight="600", + anchor="middle", + ) + svg += p.text(x + 22, legend_y + 12, label, t["secondary"], 11) + + return svg + p.close_svg() + + +if __name__ == "__main__": + for path in p.write_pair("fig-catalog-status", render): + print("wrote", path) diff --git a/docs/figures/make_family.py b/docs/figures/make_family.py new file mode 100644 index 0000000..3b699ce --- /dev/null +++ b/docs/figures/make_family.py @@ -0,0 +1,100 @@ +"""The open-coder-ai family: what each repository is, and what feeds what. + +Carried byte-identically by four repositories; see palette.py on why every +statement stays on one line under 88 characters. +""" + +import palette as p + +W, H = 800, 420 +NAME, ROLE = 13, 11 +WIDE, NARROW, ARM = 74, 34, 22 + +# Split into fragments joined with a space: a magic trailing comma keeps each list +# exploded, so `ruff format` leaves this alone at 88, 100 and 120 alike. +ROLES = { + "agentseam": [ + "the primitives — one handler API and a verified capability", + "matrix across 16 agents", + ], + "chock": [ + "the compiler — one policy into git hooks, CI gates and", + "native pre-tool hooks", + ], + "chock-catalog": [ + "the policies — 39, each labelled enforced or advisory,", + "with replayed evals", + ], + "context-report": [ + "the evidence — a signed report of whether an agent", + "artifact actually works", + ], + "chock-threat-intel": ["the threat ledger the catalog's policies answer to"], + "plugins": ["the catalog, packaged for each agent's plugin format (generated)"], +} + + +def role(key): + """One repository's one-line description, reassembled from its fragments.""" + return " ".join(ROLES[key]) + + +def _block(rect, name, role, t, filled, chars): + """A repository: monospace identifier, wrapped role beneath it.""" + x, y, w, h = rect + accent = t["enforcement"][1] + fill = accent if filled else t["surface"] + out = p.box(x, y, w, h, fill, None if filled else accent) + label = t["surface"] if filled else t["text"] + body = t["surface"] if filled else t["secondary"] + out += p.text(x + 12, y + 22, name, label, NAME, p.MONO, "600") + for i, line in enumerate(p.wrap(role, chars)): + out += p.text(x + 12, y + 40 + i * 15, line, body, ROLE) + return out + + +DESC = ( + "A layered diagram. agentseam is the foundation across the bottom; chock sits " + "on it; chock-catalog feeds chock and generates the four plugin repositories; " + "chock-threat-intel feeds the catalog; context-report runs as a verification " + "arm beside all of them." +) + + +def render(t, name): + """One theme's copy of the family diagram.""" + a = t["enforcement"][1] + svg = p.open_svg(W, H, t, "The open-coder-ai family", DESC) + + intel = role("chock-threat-intel") + svg += _block((24, 24, 262, 72), "chock-threat-intel", intel, t, False, NARROW) + + svg += p.box(314, 24, 262, 72, t["surface"], t["neutral"]) + svg += p.text(326, 44, "plugin repositories", t["text"], NAME, p.MONO, "600") + for i, line in enumerate(p.wrap(role("plugins"), NARROW)): + svg += p.text(326, 62 + i * 15, line, t["secondary"], ROLE) + + svg += p.arrow(155, 96, 155, 122, a) + svg += p.arrow(445, 122, 445, 96, a) + + cat = role("chock-catalog") + svg += _block((24, 124, 552, 74), "chock-catalog", cat, t, True, WIDE) + svg += p.arrow(300, 198, 300, 224, a) + svg += _block((24, 226, 552, 74), "chock", role("chock"), t, True, WIDE) + svg += p.arrow(300, 328, 300, 302, a) + svg += _block((24, 330, 552, 74), "agentseam", role("agentseam"), t, True, WIDE) + + svg += p.box(596, 24, 180, 380, t["surface"], a) + svg += p.text(608, 46, "context-report", t["text"], NAME, p.MONO, "600") + for i, line in enumerate(p.wrap(role("context-report"), ARM)): + svg += p.text(608, 68 + i * 15, line, t["secondary"], ROLE) + svg += p.text(608, 390, "measures all four", t["secondary"], ROLE) + for y in (140, 242, 346): + svg += p.arrow(592, y, 580, y, a) + + return svg + p.close_svg() + + +if __name__ == "__main__": + for path in p.write_pair("family", render): + print("wrote", path) diff --git a/docs/figures/make_row_basis.py b/docs/figures/make_row_basis.py new file mode 100644 index 0000000..abfafc0 --- /dev/null +++ b/docs/figures/make_row_basis.py @@ -0,0 +1,198 @@ +"""The row-basis figure: what each row of a real report.json was concluded from. + +Runs the README's own 30-second quickstart block fresh (the same mechanism the README's "Three +real rows" JSON block is pulled from -- see tools/quickstart_block.py and +tests/test_quickstart_block.py) and counts every row's basis in the resulting report.json, so this +figure can never disagree with the README: regenerating either regenerates from the same command. + +Run from this directory: `python make_row_basis.py`. Needs `context-report` importable/installed +(as the repo's own CI already ensures before this job) so the extracted quickstart block's +`context-report produce` call resolves. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +import palette as p + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +README = REPO / "README.md" + +sys.path.insert(0, str(REPO / "tools")) +from quickstart_block import extract_quickstart_block # noqa: E402 -- needs REPO on sys.path first + +CLAIMED = "claimed" +# Mirrors context_report.rows.NOT_MEASURED: a result of NotAvailable, Error or NotApplicable +# means nothing was actually recomputed this run. Kept as a literal (not imported) so this +# generator stays stdlib-only -- importing context_report here would pull in its third-party +# dependencies (jsonschema, pyyaml) at figure-generation time. +NOT_MEASURED = frozenset({"NotAvailable", "Error", "NotApplicable"}) + +W, H = 700, 320 +MARGIN = 16 +BAR_MAX_W = W - 2 * MARGIN +MIN_INSIDE_LABEL_W = 24 # narrower than this, the count goes outside the bar instead + + +def _produce_fresh_report() -> dict: + """Run the README's quickstart block in a scratch dir and return the report.json it writes. + + PYTHONPATH is pinned to this checkout's src/ so the block's `context-report` invocations + import *this* commit's code even if another context-report checkout is also installed + somewhere on this machine -- the figure must describe the commit it ships with, never a + stray sibling checkout. + """ + block = extract_quickstart_block(README.read_text(encoding="utf-8")) + env = dict(os.environ) + repo_src = str(REPO / "src") + env["PYTHONPATH"] = ( + repo_src + os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else repo_src + ) + with tempfile.TemporaryDirectory() as workdir: + subprocess.run( # noqa: S602 -- fixed, repo-owned script; same command the README documents + block, + shell=True, + cwd=workdir, + check=True, + capture_output=True, + text=True, + executable="/bin/bash", + env=env, + ) + return json.loads((Path(workdir) / "report.json").read_text(encoding="utf-8")) + + +def _row_bases(stmt: dict) -> list[tuple[str, str, str]]: + """(attribute, basis, result) for every row, in statement order.""" + return [ + (row["attribute"], row["basis"], row["result"]) for row in stmt["predicate"]["attributes"] + ] + + +def _categorise(rows: list[tuple[str, str, str]]) -> dict[str, list[str]]: + """Partition every row into exactly one of three mutually exclusive evidentiary bases. + + `claimed` (an author's assertion, schema basis "claimed") and `recomputed` (schema basis + "re-derivable" AND a real, non-absent result) are the two evidence-bearing bases here -- only + two, because that is what this real report actually contains; a third ramp colour is not used + for a distinction the data does not carry. `absence` (re-derivable in principle, but this run's + result is NotAvailable/Error/NotApplicable -- nothing was actually recomputed) is NEUTRAL: not + a weaker grade of "recomputed", a different thing. + """ + buckets: dict[str, list[str]] = {"recomputed": [], "claimed": [], "absence": []} + for attribute, basis, result in rows: + if basis == CLAIMED: + buckets["claimed"].append(attribute) + elif result in NOT_MEASURED: + buckets["absence"].append(attribute) + else: + buckets["recomputed"].append(attribute) + return buckets + + +BARS = [ + ("recomputed", "recomputed from the artifact", "re-derivable, and a real result was measured"), + ("claimed", "the author's claim", "basis: claimed -- not independently checked this run"), + ("absence", "not measured this run", "re-derivable in principle; NotAvailable/Error here"), +] + + +def _bar_colour(key: str, t: dict) -> str: + return {"recomputed": t["enforcement"][2], "claimed": t["enforcement"][0]}.get( + key, t["neutral"] + ) + + +def _hatch_id(name: str) -> str: + return f"hatch-{name}" + + +def render(t: dict, name: str, buckets: dict[str, list[str]]) -> str: + total = sum(len(v) for v in buckets.values()) + max_n = max(len(v) for v in buckets.values()) + row_block = 72 + bar_h = 20 + + svg = p.open_svg( + W, + H, + t, + "Row bases in one context-report statement", + f"Of {total} rows in the report.json the README's own quickstart block produces fresh, " + f"{len(buckets['recomputed'])} are recomputed from the artifact, " + f"{len(buckets['claimed'])} is the author's unverified claim, and " + f"{len(buckets['absence'])} have not been measured this run -- shown as absence of " + "evidence, not a weak score.", + ) + + svg += ( + f' \n' + f' \n' + f' \n' + " \n" + ) + + svg += p.text( + MARGIN, + 26, + f"report.json from the README's quickstart, run fresh -- {total} rows total", + t["secondary"], + 12, + ) + + y = 48 + for key, label, sublabel in BARS: + n = len(buckets[key]) + bar_w = (n / max_n) * BAR_MAX_W if max_n else 0 + svg += p.text(MARGIN, y, label, t["text"], 12, weight="600") + svg += p.text(MARGIN, y + 15, sublabel, t["secondary"], 10) + by = y + 22 + fill = f"url(#{_hatch_id(name)})" if key == "absence" else _bar_colour(key, t) + if bar_w > 0: + svg += p.box(MARGIN, by, bar_w, bar_h, fill, rx=3) + inside = bar_w > MIN_INSIDE_LABEL_W + if inside: + text_colour = t["surface"] if key == "recomputed" else t["text"] + svg += p.text( + MARGIN + bar_w - 8, + by + bar_h / 2 + 4, + str(n), + text_colour, + 12, + weight="600", + anchor="end", + ) + else: + svg += p.text( + MARGIN + bar_w + 8, by + bar_h / 2 + 4, str(n), t["text"], 12, weight="600" + ) + y += row_block + + svg += p.text( + MARGIN, + H - 14, + "ordered by strength of evidence: recomputed > claimed > not measured " + "(absence, not a lower grade)", + t["secondary"], + 10, + ) + + return svg + p.close_svg() + + +if __name__ == "__main__": + statement = _produce_fresh_report() + rows = _row_bases(statement) + row_buckets = _categorise(rows) + for k, v in row_buckets.items(): + print(k, len(v), v) + for path in p.write_pair("fig-row-basis", lambda t, n: render(t, n, row_buckets)): + print("wrote", path) diff --git a/docs/figures/make_social_card.py b/docs/figures/make_social_card.py new file mode 100644 index 0000000..edac589 --- /dev/null +++ b/docs/figures/make_social_card.py @@ -0,0 +1,66 @@ +"""GitHub social-preview card: repo name, one line of what it is, one real number. Light only -- +GitHub's social preview has no dark variant. No logo: this repo has none in docs/assets/. + +Run from this directory: `python make_social_card.py`. Rendering the PNG needs `cairosvg`; if it +is not installed (as in CI, which never needs it -- see docs/figures/make_row_basis.py) the SVG is +still written and the PNG step is skipped rather than failing the regeneration. +""" + +from __future__ import annotations + +from pathlib import Path + +import palette as p +from make_row_basis import _categorise, _produce_fresh_report, _row_bases + +HERE = Path(__file__).resolve().parent +W, H = 1280, 640 + +TAGLINE = "an open, signed report of whether an agent context artifact actually works" + + +def _headline_number() -> str: + """The same real report make_row_basis.py draws from, reduced to one number for the card.""" + buckets = _categorise(_row_bases(_produce_fresh_report())) + total = sum(len(v) for v in buckets.values()) + return f"{len(buckets['recomputed'])} of {total} rows recomputed from the artifact, fresh" + + +def render(number: str) -> str: + t = p.theme("light") + svg = p.open_svg( + W, + H, + t, + "context-report", + "context-report: an open, signed report of whether an agent context artifact actually " + f"works. {number}.", + ) + accent = t["enforcement"][2] + svg += p.box(0, 0, 14, H, accent, rx=0) + svg += p.text(72, 260, "context-report", t["text"], 64, p.MONO, "600") + svg += p.text(72, 320, TAGLINE, t["secondary"], 26) + svg += p.box(72, 380, 760, 3, t["neutral"], rx=0) + svg += p.text(72, 440, number, accent, 30, p.MONO, "600") + return svg + p.close_svg() + + +def main() -> None: + number = _headline_number() + print(number) + svg_path = HERE / "social-card.svg" + svg_path.write_text(render(number).strip() + "\n", encoding="utf-8") + print("wrote", svg_path) + + try: + import cairosvg + except ImportError: + print("cairosvg not installed -- skipping PNG render (not needed in CI)") + return + png_path = HERE / "social-card.png" + cairosvg.svg2png(url=str(svg_path), write_to=str(png_path), output_width=W, output_height=H) + print("wrote", png_path) + + +if __name__ == "__main__": + main() diff --git a/docs/figures/palette.py b/docs/figures/palette.py new file mode 100644 index 0000000..e5ff671 --- /dev/null +++ b/docs/figures/palette.py @@ -0,0 +1,131 @@ +"""The one visual language every open-coder-ai figure is drawn in. + +Four repositories carry this file byte-identically while configuring `ruff format` at +three different line lengths (88, 100 and 120), so no formatted output could satisfy all +of them. Every statement here therefore fits on a single line under 88 characters and +nothing is split across lines: a formatter at any width finds nothing to join or wrap. +Keep it that way when editing. +""" + +# Enforcement is ordinal: advisory < in-agent < enforced. Never reorder these. +ENFORCEMENT = { + "light": ["#86b6ef", "#2a78d6", "#104281"], + "dark": ["#9ec5f4", "#3987e5", "#184f95"], +} + +# agentseam grades five levels; the ramp is the same hue walked further. +LEVELS = { + "light": ["#86b6ef", "#5598e7", "#2a78d6", "#1c5cab", "#104281"], + "dark": ["#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95"], +} + +# Absence is not a low score, so it is never a ramp colour. +NEUTRAL = {"light": "#d8d7d2", "dark": "#383835"} + +SURFACE = {"light": "#fcfcfb", "dark": "#1a1a19"} +TEXT = {"light": "#0b0b0b", "dark": "#ffffff"} +TEXT_SECONDARY = {"light": "#52514e", "dark": "#c3c2b7"} + +STROKE_WIDTH = 2 +CORNER = 4 +GAP = 2 # surface showing between adjacent fills +GRID_OPACITY = 0.3 + +SANS = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" +MONO = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" + +THEMES = ("light", "dark") + +_XML = (("&", "&"), ("<", "<"), (">", ">"), ('"', """)) + + +def theme(name): + """Every colour of one theme, resolved.""" + return { + "enforcement": ENFORCEMENT[name], + "levels": LEVELS[name], + "neutral": NEUTRAL[name], + "surface": SURFACE[name], + "text": TEXT[name], + "secondary": TEXT_SECONDARY[name], + } + + +def esc(s): + """XML-escape a label.""" + out = str(s) + for old, new in _XML: + out = out.replace(old, new) + return out + + +def open_svg(width, height, t, title, desc): + """An accessible root element: the title and description are the alt text.""" + ns = 'xmlns="http://www.w3.org/2000/svg"' + size = f'width="{width:d}" height="{height:d}"' + view = f'viewBox="0 0 {width:d} {height:d}"' + root = f'\n' + alt = f' {esc(title)}\n' + alt += f' {esc(desc)}\n' + return root + alt + f' \n' + + +def close_svg(): + return "\n" + + +def box(x, y, w, h, fill, stroke=None, rx=CORNER): + """A rounded rectangle, filled and optionally outlined.""" + edge = f' stroke="{stroke}" stroke-width="{STROKE_WIDTH:d}"' if stroke else "" + geom = f'x="{x:g}" y="{y:g}" width="{w:g}" height="{h:g}"' + return f' \n' + + +def text(x, y, s, fill, size=13, family=SANS, weight="400", anchor="start"): + """One line of type, anchored at (x, y).""" + font = f'font-family="{family}" font-size="{size:g}" font-weight="{weight}"' + place = f'x="{x:g}" y="{y:g}"' + paint = f'fill="{fill}" text-anchor="{anchor}"' + return f" {esc(s)}\n" + + +def arrow(x1, y1, x2, y2, colour, head=6): + """A straight connector with a solid head, drawn direction-aware.""" + ends = f'x1="{x1:g}" y1="{y1:g}" x2="{x2:g}" y2="{y2:g}"' + paint = f'stroke="{colour}" stroke-width="{STROKE_WIDTH:d}"' + line = f' \n' + if x1 == x2: # vertical: the head's base sits back along the travel + base = y2 - head if y2 > y1 else y2 + head + wings = ((x2 - head, base), (x2 + head, base)) + else: + base = x2 - head if x2 > x1 else x2 + head + wings = ((base, y2 - head), (base, y2 + head)) + corners = ((x2, y2), *wings) + pts = " ".join(f"{px:g},{py:g}" for px, py in corners) + return line + f' \n' + + +def write_pair(stem, render): + """`render(theme_colours, theme_name) -> svg string`, once per theme.""" + written = [] + for name in THEMES: + path = f"{stem}-{name}.svg" + with open(path, "w", encoding="utf-8", newline="\n") as fh: + fh.write(render(theme(name), name)) + written.append(path) + return written + + +def wrap(s, width): + """Greedy wrap to `width` characters, so a label fits without a font metric.""" + words, lines, line = s.split(), [], "" + for w in words: + candidate = (line + " " + w).strip() + if len(candidate) > width and line: + lines.append(line) + line = w + else: + line = candidate + if line: + lines.append(line) + return lines diff --git a/docs/figures/social-card.png b/docs/figures/social-card.png new file mode 100644 index 0000000000000000000000000000000000000000..a22543c0d44bb8ee3d1567b3e8bd8ed607642692 GIT binary patch literal 29421 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 + context-report + context-report: an open, signed report of whether an agent context artifact actually works. 3 of 11 rows recomputed from the artifact, fresh. + + + context-report + an open, signed report of whether an agent context artifact actually works + + 3 of 11 rows recomputed from the artifact, fresh + diff --git a/pyproject.toml b/pyproject.toml index 0e90fb0..7179a46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,14 @@ extend-exclude = [".chock/bin/"] [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C90", "S", "BLE", "FBT", "T20", "SIM", "RET", "ARG", "ERA", "PIE", "PL", "TRY", "RUF"] ignore = ["TRY003", "S101"] +# docs/figures/palette.py and make_family.py are the open-coder-ai family's shared visual +# language, copied byte-identical from every sibling repo (block 2, BRIEF.md) and never edited +# here. They are `ruff format`-clean at every sibling's line length (88/100/120) by construction, +# but this repo's own PL/FBT/ARG selections still flag their helpers' multi-positional-argument +# shape (box/text/arrow take up to 8 positional args by design; _block's boolean `filled` flag is +# likewise deliberate) -- rules a line-width fix cannot satisfy without changing the shared API, +# which BRIEF.md forbids doing here. Lint-only exclude; format checking stays on for both files. +exclude = ["docs/figures/palette.py", "docs/figures/make_family.py"] [tool.ruff.lint.per-file-ignores] "tests/*" = ["PLR2004"] @@ -96,3 +104,9 @@ ignore = ["TRY003", "S101"] # Chock policy-authoring skill template: a runnable stub scaffolded into new policies, # printing is its whole point. Matches chock's own per-file-ignore for the same file. ".agents/skills/policy-init/assets/templates/scripts-stub.py" = ["T201"] +# Figure generators (block 2, BRIEF.md): run by hand or in CI's figures job, never imported; +# each prints what it wrote/computed, same pattern as the shared make_family.py above (excluded +# rather than ignored, since that file cannot be edited here). make_social_card.py's cairosvg +# import is deferred so the CI figures job -- which never installs cairosvg, see its docstring -- +# can still regenerate the SVG and skip only the PNG step. +"docs/figures/make_*.py" = ["T201", "PLC0415"] From e0c2d509958bb6f8c516322907abd73cbe6b24c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 01:49:29 +0000 Subject: [PATCH 2/2] Lint the shared figure files by name, not by excluding them This repo's ruff select is stricter than its siblings' (PL, FBT, ARG), and it flagged 14 findings in the shared docs/figures/palette.py and make_family.py. Excluding both files from lint was the wrong answer: it would also hide an undefined name or an unused import in a file nobody edits here and four repositories depend on staying identical. Two of the findings were real and are fixed at the shared source instead: _block's `filled` flag is keyword-only (FBT003 at four call sites) and render's unused `name` parameter is now `_name` (ARG001). The rest are PLR0913/PLR0917 on the drawing primitives, which take many arguments because they take coordinates, so they are ignored by name for exactly those two paths. Verified the files are still linted by appending an unused import and watching F401 fire. make_family.py re-copied from the corrected shared source; every generated SVG is byte-identical. 408 tests pass, ruff check and ruff format --check are clean. Co-Authored-By: Claude Opus 5 Signed-off-by: Claude --- docs/figures/make_family.py | 13 +++++++------ pyproject.toml | 15 ++++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/figures/make_family.py b/docs/figures/make_family.py index 3b699ce..6a70d5e 100644 --- a/docs/figures/make_family.py +++ b/docs/figures/make_family.py @@ -39,7 +39,7 @@ def role(key): return " ".join(ROLES[key]) -def _block(rect, name, role, t, filled, chars): +def _block(rect, name, role, t, chars, *, filled=False): """A repository: monospace identifier, wrapped role beneath it.""" x, y, w, h = rect accent = t["enforcement"][1] @@ -61,13 +61,13 @@ def _block(rect, name, role, t, filled, chars): ) -def render(t, name): +def render(t, _name): """One theme's copy of the family diagram.""" a = t["enforcement"][1] svg = p.open_svg(W, H, t, "The open-coder-ai family", DESC) intel = role("chock-threat-intel") - svg += _block((24, 24, 262, 72), "chock-threat-intel", intel, t, False, NARROW) + svg += _block((24, 24, 262, 72), "chock-threat-intel", intel, t, NARROW) svg += p.box(314, 24, 262, 72, t["surface"], t["neutral"]) svg += p.text(326, 44, "plugin repositories", t["text"], NAME, p.MONO, "600") @@ -78,11 +78,12 @@ def render(t, name): svg += p.arrow(445, 122, 445, 96, a) cat = role("chock-catalog") - svg += _block((24, 124, 552, 74), "chock-catalog", cat, t, True, WIDE) + svg += _block((24, 124, 552, 74), "chock-catalog", cat, t, WIDE, filled=True) svg += p.arrow(300, 198, 300, 224, a) - svg += _block((24, 226, 552, 74), "chock", role("chock"), t, True, WIDE) + svg += _block((24, 226, 552, 74), "chock", role("chock"), t, WIDE, filled=True) svg += p.arrow(300, 328, 300, 302, a) - svg += _block((24, 330, 552, 74), "agentseam", role("agentseam"), t, True, WIDE) + seam = role("agentseam") + svg += _block((24, 330, 552, 74), "agentseam", seam, t, WIDE, filled=True) svg += p.box(596, 24, 180, 380, t["surface"], a) svg += p.text(608, 46, "context-report", t["text"], NAME, p.MONO, "600") diff --git a/pyproject.toml b/pyproject.toml index 7179a46..455fe8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,16 +80,13 @@ extend-exclude = [".chock/bin/"] [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C90", "S", "BLE", "FBT", "T20", "SIM", "RET", "ARG", "ERA", "PIE", "PL", "TRY", "RUF"] ignore = ["TRY003", "S101"] -# docs/figures/palette.py and make_family.py are the open-coder-ai family's shared visual -# language, copied byte-identical from every sibling repo (block 2, BRIEF.md) and never edited -# here. They are `ruff format`-clean at every sibling's line length (88/100/120) by construction, -# but this repo's own PL/FBT/ARG selections still flag their helpers' multi-positional-argument -# shape (box/text/arrow take up to 8 positional args by design; _block's boolean `filled` flag is -# likewise deliberate) -- rules a line-width fix cannot satisfy without changing the shared API, -# which BRIEF.md forbids doing here. Lint-only exclude; format checking stays on for both files. -exclude = ["docs/figures/palette.py", "docs/figures/make_family.py"] - [tool.ruff.lint.per-file-ignores] +# The open-coder-ai family's shared visual language, carried byte-identical by four +# repositories and never edited here. Its drawing primitives take many arguments because +# they take coordinates, and a generator prints what it wrote. Ignored by name rather than +# excluded: a blanket exclude would also hide an undefined name in a file nobody edits here. +"docs/figures/palette.py" = ["PLR0913", "PLR0917"] +"docs/figures/make_family.py" = ["PLR0913"] "tests/*" = ["PLR2004"] # Test doubles implement Runner/Judge Protocols by contract; unused args and terse asserts are fine. "tests/efficacy/*" = ["ARG002", "ARG005", "E702", "FBT002"]