feat: consolidate reviewed Diagram Design 2.6.0 release (#134)

* fix(commands): stop hardcoding the visual-type count

Integrates the final reviewed diff from PR #112.

* docs(high-level): correct effective width checklist

Integrates the final reviewed diff from PR #105.

* fix(commands): quote profile argument hint

Integrates the final reviewed diff from PR #126.

* test(sankey): gate conservation and ribbon geometry

Integrates the final reviewed diff from PR #103.

* feat(tooling): add doctor diagnostics gate

Integrates the final reviewed diff from PR #132.

* feat(scripts): lint diagrams as rendered

Integrates the final reviewed diff from PR #18.

* feat(types): add quantitative polar chart

Integrates the final reviewed diff from PR #101.

* chore(release): consolidate reviewed changes in 2.6.0

Documents every combined gate and synchronizes Claude, Codex, and Factory metadata for the 39-type release.

* fix(doctor): support installed-skill projects

---------

Co-authored-by: mysoniw <soniw1@gmail.com>
Co-authored-by: thang <76550342+thangldw@users.noreply.github.com>
Co-authored-by: YinShaokun <yinshaokun@didiglobal.com>
Co-authored-by: 0xDarkMatter <0xDarkMatter@users.noreply.github.com>
Co-authored-by: Rupam Pal <rupam.pal@euromonitor.com>
Co-authored-by: Yuvraj3905 <yuvraj202001@gmail.com>
This commit is contained in:
Cathryn Lavery
2026-08-20 12:21:43 -07:00
committed by GitHub
co-authored by mysoniw thang YinShaokun 0xDarkMatter Rupam Pal Yuvraj3905
parent 4b573616cb
commit b827385a17
42 changed files with 7090 additions and 130 deletions
@@ -1,6 +1,6 @@
# ADR 0002 — Semantic patterns never expand the visual-type taxonomy
**Status:** accepted (v2.3)
**Status:** accepted (v2.3; amended v2.6)
## Context
@@ -8,7 +8,7 @@ Auditing behavior-rich figures (queues, policy traces, trust boundaries) showed
## Decision
Behavior is a separate axis. The seven semantic patterns in `references/semantic-patterns.md` each route to the **nearest existing visual type** for layout; a pattern owns semantic primitives and a tighter budget, never a second layout grammar. The visual-type count only moves when a genuinely new *layout* grammar appears (it did in v2.5.10 — see [ADR 0007](0007-new-layout-grammars.md), 28 → 38).
Behavior is a separate axis. The seven semantic patterns in `references/semantic-patterns.md` each route to the **nearest existing visual type** for layout; a pattern owns semantic primitives and a tighter budget, never a second layout grammar. The visual-type count only moves when a genuinely new *layout* grammar appears (it did in v2.5.10 — see [ADR 0007](0007-new-layout-grammars.md), 28 → 38 — and again for Polar, 38 → 39).
## Consequences
@@ -20,6 +20,8 @@ Behavior is a separate axis. The seven semantic patterns in `references/semantic
**2026-08-18 — the count is 28.** Treemap was admitted under the escape clause above: recursive area subdivision is a layout grammar no existing type provides (bar encodes with length, nested with containment and no quantity, pyramid with rank). It shipped the full §10 set, and the counters named above moved 27 → 28 together with the prose.
The decision itself is unchanged — semantic patterns still never add a type, and the count still moves only for a new *layout* grammar. What this amendment records is the procedure: the two counters are this ADR's enforcement, so a PR that edits them without amending this file has quietly made itself the authority. Amend here in the same PR, or the number in the test is just whatever the last contributor typed.
**2026-08-19 — the count is 38.** Ten additional visual grammars were admitted under the same escape clause: Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, and database schema. The per-type argument is in [ADR 0007](0007-new-layout-grammars.md); each ships the full §10 set, and the two counters move 28 → 38 together with the prose.
**2026-08-20 — the count is 39.** Polar was admitted under the same escape clause: angle encodes ordered cyclic categories and linear radius encodes one quantitative series, a layout grammar no existing type provides. It shipped the full §10 set, and the counters named above moved 38 → 39 together with the prose.
The decision itself is unchanged — semantic patterns still never add a type, and the count still moves only for a new *layout* grammar. What this amendment records is the procedure: the two counters are this ADR's enforcement, so a PR that edits them without amending this file has quietly made itself the authority. Amend here in the same PR, or the number in the test is just whatever the last contributor typed.
+5 -1
View File
@@ -61,4 +61,8 @@ Kept out on purpose, so the next audit does not relitigate them.
- **ER's scope was narrowed, not duplicated.** `type-er.md` now states that it is entity-level and points at `type-db-schema.md` for the physical schema. Without that edit the two types would have overlapping "Best for" claims and the router would be ambiguous.
- The ADR 0004 byte cap became binding. Adding these types required trimming SKILL.md body prose: the §11 import consequences were compressed, the terminal-variant and typography paragraphs were tightened, and §4's six connector anti-pattern rows collapsed into one row pointing at §6 — that table was the third statement of rules already given in full in §6 and checked one by one in §9. `SKILL.md` sits at ~39.2 KB against the 40 KB cap. The next type must be paid for the same way, never by trimming the frontmatter description.
- Per-type budget rows stayed in SKILL.md §7 rather than moving into the type references, because several older references (venn, pyramid, layers, ER, swimlane, timeline) do not state their own limits. Moving the rows out would have lost those numbers.
- `docs/screenshots/` has no PNGs for the new types yet; the README lists them in a text table instead of the image grid until the screenshots are generated.
- At acceptance time, `docs/screenshots/` had no PNGs for the new types; README listed them in a text table pending generation.
## Amendment — canonical screenshots shipped
The ten types now have canonical PNGs, image-grid entries in README, and source/screenshot digests in `docs/screenshots/manifest.json`. Polar was admitted separately afterward and moved the repository-wide count from 38 to 39; this ADR still records the ten-type 28 → 38 decision.
+9
View File
@@ -101,6 +101,15 @@
"width": 2400,
"height": 1202
},
{
"slug": "polar",
"source": "skills/diagram-design/assets/example-polar.html",
"screenshot": "docs/screenshots/polar.png",
"source_sha256": "596e2042f18044ef1fa2c2eb849f39fa5c7767b2a17e272617ebd27e10bc86b1",
"screenshot_sha256": "a1dd1d4cb38db2217759c5d256cc56d832acc4f3077f090cc8691d6e4f82ef31",
"width": 2400,
"height": 1202
},
{
"slug": "loop",
"source": "skills/diagram-design/assets/example-loop.html",
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

@@ -0,0 +1,592 @@
# Quantitative Polar Chart Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a verified 28th visual type that renders one cyclic quantitative series as radial lollipops with angle=category and linear radius=magnitude.
**Architecture:** The public contract lives in `references/type-polar.md`; three static HTML examples carry machine-readable `data-polar-*` metadata. A dependency-free Python verifier recomputes all endpoints and rejects hubs, nonlinear radius, visible zero rays, invalid values, and variant drift. Existing routing, gallery, README, CI, and package manifests are updated as one minor release.
**Tech Stack:** Python standard library 3.10+, static HTML/CSS, inline SVG, existing repository validation scripts, headless Google Chrome for the README screenshot.
**Spec:** `docs/superpowers/specs/2026-08-18-polar-chart-design.md`
## Global Constraints
- Angle encodes category; radius encodes magnitude with `radius = R * value / max`.
- The shared scale starts at exactly zero; `max` is finite and greater than zero.
- Zero renders with no value ray and no endpoint marker; its numeric label remains visible.
- One series, 4–8 unique ordered categories, and at most one focal category.
- No filled quantitative wedges, donut hub, gradients, negative values, logarithmic scale, interaction, animation, or external assets.
- All three examples use the same values, `viewBox="0 0 1000 520"`, center `(500,230)`, radius `160`, max `100`, start angle `-90`, and clockwise order.
- Static examples must satisfy the accessible SVG contract and existing skin rules.
- No new runtime or test dependency.
- Add a minor plugin release: `2.4.4 → 2.5.0` unless `origin/main` advances first; after a rebase, rerun `python3 scripts/bump-plugin-version.py --minor` from the new base.
---
### Task 1: Build the quantitative polar verifier with adversarial tests
**Files:**
- Create: `scripts/test-verify-polar.py`
- Create: `scripts/verify-polar.py`
**Interfaces:**
- Produces: `check(path: Path) -> list[str]`, `check_variants(paths: Sequence[Path]) -> list[str]`, and CLI `python3 scripts/verify-polar.py [file ...]`.
- Consumes later: Task 2 uses the verifier as the RED/GREEN gate for the three examples; Task 4 adds both commands to CI.
- [ ] **Step 1: Write the failing verifier tests**
Create `scripts/test-verify-polar.py` with a fixture builder for a four-category chart. The valid fixture has center `(100,100)`, `R=100`, max `100`, angles `-90/0/90/180`, and values `0/25/50/100`:
```python
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
VERIFIER = ROOT / "scripts/verify-polar.py"
def load_verifier():
spec = importlib.util.spec_from_file_location("verify_polar", VERIFIER)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def category(index: int, value: int, endpoint: tuple[float, float] | None) -> str:
body = '<text data-polar-value-label="">{}</text>'.format(value)
if endpoint is not None:
x, y = endpoint
body = (
f'<line data-polar-ray="" x1="100" y1="100" x2="{x}" y2="{y}"/>'
f'<circle data-polar-marker="" cx="{x}" cy="{y}" r="4"/>' + body
)
return (
f'<g data-polar-category="c{index}" data-polar-index="{index}" '
f'data-polar-value="{value}">{body}</g>'
)
def document(categories: str, extra_svg: str = "") -> str:
return (
'<!DOCTYPE html><html><body>'
'<svg data-polar-chart="" data-polar-cx="100" data-polar-cy="100" '
'data-polar-radius="100" data-polar-min="0" data-polar-max="100" '
'data-polar-start-angle="-90" data-polar-clockwise="true" '
'data-polar-radius-encoding="linear" data-polar-inner-radius="0">'
f'{extra_svg}{categories}</svg></body></html>'
)
VALID = document(
category(0, 0, None)
+ category(1, 25, (125, 100))
+ category(2, 50, (100, 150))
+ category(3, 100, (0, 100))
)
```
In `main()`, write each fixture to a temporary file and assert these results:
```python
cases = [
("25 percent hub", VALID.replace('data-polar-inner-radius="0"', 'data-polar-inner-radius="25"'), "inner radius"),
("sqrt radius", VALID.replace('x2="125"', 'x2="150"').replace('cx="125"', 'cx="150"'), "linear endpoint"),
("visible zero ray", VALID.replace('<text data-polar-value-label="">0</text>', '<line data-polar-ray="" x1="100" y1="100" x2="100" y2="100"/><text data-polar-value-label="">0</text>'), "zero category"),
("out of range", VALID.replace('data-polar-value="100"', 'data-polar-value="101"'), "outside 0..100"),
("duplicate index", VALID.replace('data-polar-index="3"', 'data-polar-index="2"'), "indices"),
("quantitative wedge", VALID.replace('</svg>', '<path data-polar-wedge="" d="M0 0"/></svg>'), "wedge"),
]
```
Assert `check(valid_path) == []`. For every invalid case, assert at least one finding exists and one finding contains the named substring; do not assert the total count because a malformed value may correctly violate more than one invariant.
Also create three valid files and change the dark fixture's `data-polar-value="25"` to `26`; assert `check_variants()` reports `variant data drift`.
- [ ] **Step 2: Run the test to verify RED**
Run:
```bash
python3 scripts/test-verify-polar.py
```
Expected: fail while importing missing `scripts/verify-polar.py`.
- [ ] **Step 3: Implement the minimal verifier**
Create `scripts/verify-polar.py` using only `dataclasses`, `html.parser`, `math`, `pathlib`, `sys`, and `typing`.
Define these data structures and parser output:
```python
@dataclass
class Category:
name: str
index: int
value: float
focal: bool
rays: list[dict[str, str]]
markers: list[dict[str, str]]
value_labels: int
@dataclass
class Chart:
attrs: dict[str, str]
categories: list[Category]
wedge_count: int
```
`PolarParser(HTMLParser)` must capture exactly one element carrying `data-polar-chart`; capture `g[data-polar-category]`, its `line[data-polar-ray]`, `circle[data-polar-marker]`, and `text[data-polar-value-label]`; count any `path[data-polar-wedge]`.
Implement `check()` with these exact invariants:
```python
TOLERANCE = 0.75
required = (
"data-polar-cx", "data-polar-cy", "data-polar-radius",
"data-polar-min", "data-polar-max", "data-polar-start-angle",
"data-polar-clockwise", "data-polar-radius-encoding",
"data-polar-inner-radius",
)
expected_angle = radians(start_angle) + (1 if clockwise else -1) * 2 * pi * index / count
expected_radius = radius * value / maximum
expected_x = cx + expected_radius * cos(expected_angle)
expected_y = cy + expected_radius * sin(expected_angle)
```
Return findings for: missing/duplicate chart; missing or non-numeric metadata; min not zero; max/radius not positive; non-linear encoding; non-zero inner radius; category count outside 4–8; non-contiguous indices; duplicate trimmed names; more than one focal; value outside range; value-label count not one; zero carrying a ray or marker; positive value not carrying exactly one ray and marker; ray start not at center; ray/marker endpoint outside tolerance; any wedge.
Implement `check_variants()` by comparing this signature for all paths:
```python
signature = (
cx, cy, radius, minimum, maximum, start_angle, clockwise,
tuple((c.index, c.name, c.value, c.focal) for c in sorted(categories, key=lambda c: c.index)),
)
```
With no CLI arguments, validate the exact shipped paths `example-polar.html`, `example-polar-dark.html`, and `example-polar-full.html`; with arguments, validate those files. Print each finding as `FAIL <path>: <message>` and return 1 if any finding exists; otherwise print `OK <path>` and return 0.
- [ ] **Step 4: Run GREEN and confirm the shipped-file gate is still RED**
Run:
```bash
python3 scripts/test-verify-polar.py
python3 scripts/verify-polar.py
```
Expected: adversarial tests pass; shipped verifier fails because the three example files do not exist yet.
- [ ] **Step 5: Commit the verifier slice**
```bash
git add scripts/test-verify-polar.py scripts/verify-polar.py
git commit -m "test(polar): verify linear radius encoding"
```
---
### Task 2: Add the type reference and three verified static examples
**Files:**
- Create: `skills/diagram-design/references/type-polar.md`
- Create: `skills/diagram-design/assets/example-polar.html`
- Create: `skills/diagram-design/assets/example-polar-dark.html`
- Create: `skills/diagram-design/assets/example-polar-full.html`
**Interfaces:**
- Consumes: `scripts/verify-polar.py` and the approved spec.
- Produces: three identical `data-polar-*` datasets consumed by Task 3 gallery/docs wiring.
- [ ] **Step 1: Preserve the missing-example RED result**
Run:
```bash
python3 scripts/verify-polar.py
```
Expected: fail naming all three missing example files.
- [ ] **Step 2: Write `type-polar.md` from the approved contract**
Create these sections in order:
```markdown
# Polar Chart
**Best for:** one quantitative series across 4–8 categories whose clockwise order is meaningful.
## Input contract
## Quantitative encoding
### Zero
## Layout conventions
## Geometry
## Visual treatment
## Complexity budget
## When not to use
## Anti-patterns
## Examples
```
Copy the formulas, zero behavior, exclusions, scale validation, label-anchor rules, and `1000×520` reference geometry verbatim from the spec. The anti-pattern list must explicitly reject donut hubs, filled wedges, tangent labels, multiple series, truncated scales, sorted-by-value categories, and treating missing as zero. Route multiple series to Radar, non-cyclic categories to Bar, and more than eight time buckets to Line.
- [ ] **Step 3: Build the minimal-light example with exact geometry**
Start from `skills/diagram-design/assets/template.html`. Use this dataset and endpoint table; retain three decimals where shown so the verifier tolerance is meaningful:
| i | label | value | spoke end | value end | label point (`R+28`) |
|---|---|---:|---|---|---|
| 0 | 00–03 | 32 | `500,70` | `500,178.8` | `500,42` |
| 1 | 03–06 | 18 | `613.137,116.863` | `520.365,209.635` | `632.936,97.064` |
| 2 | 06–09 | 24 | `660,230` | `538.4,230` | `688,230` |
| 3 | 09–12 | 58 | `613.137,343.137` | `565.62,295.62` | `632.936,362.936` |
| 4 | 12–15 | 100 focal | `500,390` | `500,390` | `500,418` |
| 5 | 15–18 | 82 | `386.863,343.137` | `407.227,322.773` | `367.064,362.936` |
| 6 | 18–21 | 76 | `340,230` | `378.4,230` | `312,230` |
| 7 | 21–24 | 45 | `386.863,116.863` | `449.088,179.088` | `367.064,97.064` |
The primary SVG opening must be:
```html
<svg viewBox="0 0 1000 520" xmlns="http://www.w3.org/2000/svg"
role="img" aria-labelledby="polar-title polar-desc"
data-polar-chart data-polar-cx="500" data-polar-cy="230"
data-polar-radius="160" data-polar-min="0" data-polar-max="100"
data-polar-start-angle="-90" data-polar-clockwise="true"
data-polar-radius-encoding="linear" data-polar-inner-radius="0">
```
Use `<circle>` grid rings at `r=32/64/96/128/160`, eight faint full-radius spokes, then one group per category:
```html
<g data-polar-category="00–03" data-polar-index="0" data-polar-value="32">
<line data-polar-ray x1="500" y1="230" x2="500" y2="178.8"
stroke="#4f5d75" stroke-width="2"/>
<circle data-polar-marker cx="500" cy="178.8" r="4"
fill="#f5f5f5" stroke="#4f5d75" stroke-width="1.2"/>
<text data-polar-value-label x="500" y="26" ...>32</text>
</g>
```
Set `data-polar-focal="true"` only on `12–15`; use accent stroke and a 5 px endpoint marker there. Every other group uses muted. Put the scale values `20/40/60/80/100` on the top axis and a single `0` beside the center. Add `Illustrative normalized workload profile` below a hairline at the bottom. The `<desc>` must identify `12–15 UTC` as the 100% peak and state that the other seven windows proceed clockwise.
- [ ] **Step 4: Create dark and full variants without dataset drift**
Create `example-polar-dark.html` from `template-dark.html` and `example-polar-full.html` from `template-full.html`. Keep every `data-polar-*` attribute and all chart coordinates byte-equivalent to the light variant; only skin tokens and surrounding editorial cards differ.
The full variant has three unequal cards:
- `Peak · 12–15 UTC · 100%`
- `Quietest · 03–06 UTC · 18%`
- `Evening tail · 18–21 UTC · 76%`
Use prefixed accessible IDs `polar-dark-title/desc` and `polar-full-title/desc`.
- [ ] **Step 5: Run quantitative, skin, accessibility, and visual gates**
Run:
```bash
python3 scripts/test-verify-polar.py
python3 scripts/verify-polar.py
python3 scripts/lint-skin.py \
skills/diagram-design/assets/example-polar.html \
skills/diagram-design/assets/example-polar-dark.html \
skills/diagram-design/assets/example-polar-full.html
python3 scripts/verify-geometry.py \
skills/diagram-design/assets/example-polar.html \
skills/diagram-design/assets/example-polar-dark.html \
skills/diagram-design/assets/example-polar-full.html
```
Expected: all commands exit 0 and report zero findings.
Open each HTML file in a fresh browser tab. Verify: the focal ray is the only accent; rings do not read as filled wedges; labels are upright and unclipped; `03–06` visibly lands at 18%; the full card layout does not change SVG geometry.
- [ ] **Step 6: Commit the reference and examples**
```bash
git add \
skills/diagram-design/references/type-polar.md \
skills/diagram-design/assets/example-polar.html \
skills/diagram-design/assets/example-polar-dark.html \
skills/diagram-design/assets/example-polar-full.html
git commit -m "feat(types): add quantitative polar chart"
```
---
### Task 3: Wire the 28th type through routing, gallery, documentation, and count gates
**Files:**
- Modify: `skills/diagram-design/SKILL.md:1-13,52,81-112,356-385`
- Modify: `skills/diagram-design/assets/index.html:197-309`
- Modify: `README.md:15-88,188,209,373,431`
- Create: `docs/screenshots/polar.png`
- Modify: `commands/import-drawio.md:25`
- Modify: `commands/import-mermaid.md:25`
- Modify: `skills/diagram-design/references/onboarding.md:162`
- Modify: `skills/diagram-design/references/semantic-patterns.md:3`
- Modify: `docs/adr/0002-semantic-patterns-do-not-expand-the-taxonomy.md:1-17`
- Modify: `CONTRIBUTING.md:11,130`
- Modify: `scripts/verify-docs-sync.py:22-79`
- Modify: `scripts/verify-semantic-motion.py:190-207,422`
- Modify: `scripts/test-verify-motion.py:401-415`
**Interfaces:**
- Consumes: `type-polar.md` and the three examples from Task 2.
- Produces: discoverable routing vocabulary, 28-row taxonomy, gallery reachability, and count assertions consumed by Task 4 CI.
- [ ] **Step 1: Raise the count gates first and verify RED**
In `scripts/verify-docs-sync.py`, add `VISUAL_TYPE_COUNT = 28` beside `VARIANTS` and replace both hardcoded `27` values in `check_description()` with the constant.
In `scripts/verify-semantic-motion.py`, add `VISUAL_TYPE_COUNT = 28`, derive `guide_heading = f"### Visual-type guide ({VISUAL_TYPE_COUNT})"`, require 28 rows, and update the success message. In `scripts/test-verify-motion.py`, replace the missing-guide fixture and expected diagnostic with `28`.
Run:
```bash
python3 scripts/verify-docs-sync.py
python3 scripts/verify-semantic-motion.py --markdown-only
```
Expected: both fail because SKILL.md still has 27 rows and the old heading.
- [ ] **Step 2: Add Polar to SKILL.md and the complexity budget**
Make these exact routing changes:
- Add `polar/radial lollipop` to the frontmatter description.
- Change `metadata.version` from `2.4` to `2.5`.
- Change `Twenty-seven` and both numeric `27` references to `Twenty-eight`/`28`.
- Rename the heading to `### Visual-type guide (28)`.
- Add this row immediately after Radar:
```markdown
| One quantitative series across cyclic categories; angle=category, radius=magnitude | **Polar chart** | [type-polar.md](references/type-polar.md) |
```
- Add complexity rows `Max polar categories | 8`, `Max polar series | 1`, and `Max focal polar categories | 1` after the radar rows.
Run `python3 scripts/verify-docs-sync.py`; expect the type-count and description/reference checks to be repaired, with the command still failing only because the new examples are not gallery-reachable yet.
- [ ] **Step 3: Register all three examples in the gallery**
Insert a non-`data-single` tab immediately after Radar:
```html
<button class="tab new" data-type="polar">
<span class="eyebrow">16</span>Polar
</button>
```
Increment every later numeric eyebrow so gallery numbers remain unique and ascending. Do not change `data-type`, `data-single`, variant behavior, or keyboard navigation.
Run:
```bash
python3 scripts/verify-docs-sync.py
```
Expected: gallery reachability passes for `example-polar{,-dark,-full}.html`.
- [ ] **Step 4: Update every current count surface while preserving historical text**
Change active product claims from 27 to 28 in README, CONTRIBUTING, both import commands, onboarding, semantic-patterns, and ADR 0002. In ADR 0002, explain that Polar is the first admitted new grammar and the invariant is now 28. Do not alter ADR 0004's historical statement that v2.3 once carried 27 type names.
Add this README gallery cell in the final row:
```html
<td align="center"><img src="docs/screenshots/polar.png" alt="Polar chart"><br><b>Polar chart</b><br><sub>Cyclic magnitude · linear radius</sub></td>
```
Keep the third cell empty unless another type lands during rebase.
- [ ] **Step 5: Generate and inspect the README screenshot**
Run from the repository root:
```bash
chrome='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
page="file://$(pwd)/skills/diagram-design/assets/example-polar.html"
"$chrome" --headless=new --hide-scrollbars --force-device-scale-factor=2 \
--window-size=1200,601 --screenshot=docs/screenshots/polar.png "$page"
sips -g pixelWidth -g pixelHeight docs/screenshots/polar.png
```
Expected dimensions: `2400 × 1202`. Inspect the PNG and confirm the entire chart, title, labels, and source note are visible with no browser chrome.
- [ ] **Step 6: Run routing/count tests and the SKILL.md byte cap**
```bash
python3 scripts/verify-docs-sync.py
python3 scripts/test-verify-docs-sync.py
python3 scripts/verify-semantic-motion.py --markdown-only
python3 scripts/test-verify-motion.py
```
Expected: all pass; semantic verification reports 28 visual types and SKILL.md remains below 40,000 bytes.
- [ ] **Step 7: Commit the routing and documentation slice**
```bash
git add \
skills/diagram-design/SKILL.md \
skills/diagram-design/assets/index.html \
README.md docs/screenshots/polar.png \
commands/import-drawio.md commands/import-mermaid.md \
skills/diagram-design/references/onboarding.md \
skills/diagram-design/references/semantic-patterns.md \
docs/adr/0002-semantic-patterns-do-not-expand-the-taxonomy.md \
CONTRIBUTING.md \
scripts/verify-docs-sync.py scripts/verify-semantic-motion.py \
scripts/test-verify-motion.py
git commit -m "docs(polar): register the 28th visual type"
```
---
### Task 4: Add CI coverage and publish the minor package version
**Files:**
- Modify: `.github/workflows/ci.yml:140-222`
- Modify: `CONTRIBUTING.md:40-82`
- Modify: `.claude-plugin/plugin.json`
- Modify: `.codex-plugin/plugin.json`
**Interfaces:**
- Consumes: verifier/tests from Task 1 and examples from Task 2.
- Produces: cross-platform CI enforcement and synchronized release version `2.5.0`.
- [ ] **Step 1: Add the polar gate to CI**
Insert after the skin linter:
```yaml
- name: Verify quantitative polar chart
if: always()
id: polar
shell: bash
run: |
python scripts/test-verify-polar.py
python scripts/verify-polar.py
```
Add this row to the execution summary after Color Palette & Skin Linter:
```yaml
echo "| Quantitative Polar Chart | ${{ steps.polar.outcome == 'success' && '✅ Passed' || (steps.polar.outcome == 'skipped' && '⏭️ Skipped' || '❌ Failed') }} |" >> $GITHUB_STEP_SUMMARY
```
- [ ] **Step 2: Add the local gate to CONTRIBUTING**
Add table row:
```markdown
| Quantitative polar encoding and variant parity | `python3 scripts/test-verify-polar.py && python3 scripts/verify-polar.py` |
```
Add both commands after `lint-skin.py --all --baseline` in the combined validation command.
- [ ] **Step 3: Bump both package manifests together**
Run:
```bash
python3 scripts/bump-plugin-version.py --minor
jq -r '.version' .claude-plugin/plugin.json .codex-plugin/plugin.json
```
Expected: both print `2.5.0`. If main advanced, the expected value is the next minor derived from the rebased synchronized version.
- [ ] **Step 4: Verify package and CI YAML**
```bash
python3 scripts/test-plugin-package.py
python3 scripts/verify-plugin-package.py origin/main
claude plugin validate . --strict
python3 -c 'import yaml, pathlib; yaml.safe_load(pathlib.Path(".github/workflows/ci.yml").read_text())' 2>/dev/null \
|| ruby -e 'require "yaml"; YAML.load_file(".github/workflows/ci.yml"); puts "CI YAML OK"'
```
Expected: package checks pass, both versions are synchronized and increasing, plugin validation passes, and one available YAML parser accepts the workflow.
- [ ] **Step 5: Commit CI and release metadata**
```bash
git add \
.github/workflows/ci.yml CONTRIBUTING.md \
.claude-plugin/plugin.json .codex-plugin/plugin.json
git commit -m "ci(polar): enforce quantitative chart contract"
```
---
### Task 5: Run the complete release gate on the exact committed tree
**Files:**
- Verify only; modify only files named by a failing, reproducible gate.
**Interfaces:**
- Consumes: every artifact from Tasks 1–4.
- Produces: evidence that the exact branch tip is PR-ready.
- [ ] **Step 1: Confirm scope and branch provenance**
```bash
git status --short --branch
git diff --stat origin/main...HEAD
git log --oneline --decorate origin/main..HEAD
```
Expected: branch `feat/polar-chart`; no uncommitted files; only spec, plan, polar feature, count/docs, CI, screenshot, and version files.
- [ ] **Step 2: Run the complete CONTRIBUTING suite plus the new gate**
```bash
python3 scripts/test-plugin-package.py \
&& python3 scripts/verify-plugin-package.py origin/main \
&& claude plugin validate . --strict \
&& python3 scripts/test-lint-a11y.py \
&& python3 scripts/verify-semantic-motion.py --markdown-only \
&& python3 scripts/verify-semantic-motion.py --example-only \
&& python3 scripts/verify-motion.py --shipped \
&& python3 scripts/lint-skin.py --all --baseline \
&& python3 scripts/test-verify-polar.py \
&& python3 scripts/verify-polar.py \
&& python3 scripts/verify-sequence-oauth.py \
&& python3 scripts/verify-drawio-import.py \
&& python3 scripts/verify-mermaid-import.py \
&& python3 scripts/test-verify-motion.py \
&& python3 scripts/verify-docs-sync.py \
&& python3 scripts/test-verify-docs-sync.py \
&& python3 scripts/test-self-check.py \
&& python3 scripts/verify-geometry.py --all \
&& python3 scripts/test-verify-geometry.py \
&& python3 scripts/build-icons.py \
&& git diff --ignore-space-at-eol --exit-code -- \
skills/diagram-design/assets/icons.html \
skills/diagram-design/references/primitive-icons.md
```
Expected: exit 0, no failures, no warnings from plugin validation, zero lint/geometry findings, and no generated icon drift.
- [ ] **Step 3: Reconfirm a clean committed snapshot**
```bash
git diff --check origin/main...HEAD
git status --short --branch
```
Expected: no whitespace findings and no working-tree changes. If `build-icons.py` created only line-ending changes, confirm `git diff --ignore-space-at-eol --exit-code` returns 0, then restore only the two generated icon files before reporting completion.
@@ -0,0 +1,167 @@
# Quantitative Polar Chart Design
## Purpose
Add a 28th visual type, **Polar chart**, for one quantitative series whose categories have a meaningful circular order. The type fills the layout gap accepted in issue #60 while narrowing the earlier wheel proposal to a truthful quantitative contract:
- angle encodes category;
- radius encodes magnitude linearly from an explicit zero baseline;
- wedge area encodes nothing;
- static HTML/SVG remains the only shipped output.
The type is a radial lollipop chart, not a profile wheel. Full-radius grid spokes establish the category axes; a value ray from the center and a small endpoint marker encode each observation.
## Scope
Version 1 supports:
- one quantitative series;
- 4–8 equally spaced categories in a meaningful clockwise order;
- a shared linear scale with `min = 0` and `max > 0`;
- five circular grid rings at 20%, 40%, 60%, 80%, and 100%;
- one optional focal category rendered with the accent token;
- minimal light, minimal dark, and full-editorial examples;
- horizontal, upright category labels outside the outer ring;
- one numeric value label per category.
Version 1 excludes:
- DISC, personality, competency, and role-profile wheels whose sectors are qualitative;
- arbitrary point placement within sectors;
- filled quantitative wedges, gradient bands, or donut hubs;
- multiple series, negative values, logarithmic scales, unequal sector angles, and more than eight categories;
- animation or interaction.
Use Radar for multiple entities scored across common criteria, Line for more than eight ordered time buckets, and Bar when circular order adds no meaning.
## Input Contract
```yaml
title: "Request demand by UTC window"
unit: "% of daily peak"
scale:
min: 0
max: 100
categories:
- { label: "00–03", value: 32 }
- { label: "03–06", value: 18 }
- { label: "06–09", value: 24 }
- { label: "09–12", value: 58 }
- { label: "12–15", value: 100, focal: true }
- { label: "15–18", value: 82 }
- { label: "18–21", value: 76 }
- { label: "21–24", value: 45 }
start_angle: -90
clockwise: true
source_note: "Illustrative normalized workload profile"
```
Validation rules:
1. `scale.min` is exactly `0`; truncated radial scales are invalid.
2. `scale.max` is finite and greater than `0`.
3. Every value is finite and satisfies `0 <= value <= scale.max`.
4. Category labels are unique after trimming and category count is 4–8.
5. At most one category is focal.
6. Categories retain input order; sorting by value would destroy circular meaning.
## Quantitative Encoding
For category `i` of `N`, value `v`, center `(cx, cy)`, and outer radius `R`:
```text
theta_i = start_angle + clockwise_sign * 2π * i / N
radius_i = R * v / scale.max
x_i = cx + radius_i * cos(theta_i)
y_i = cy + radius_i * sin(theta_i)
```
Angles are expressed in radians for calculation; `start_angle` is supplied in degrees and defaults to `-90`, placing the first category at twelve o'clock. `clockwise_sign` is `+1` for the default clockwise order and `-1` otherwise.
The value ray is the line segment from `(cx, cy)` to `(x_i, y_i)`. Its visible length is exactly proportional to `v`. Endpoint circles have constant radius and encode no additional quantity. No filled sector may sit behind or beneath a value ray, because its area would compete with the declared radius encoding.
The element carrying `data-polar-chart` must be the chart's root `<svg>`. Its descendants are limited to `<g>`, `<line>`, `<circle>`, `<rect>`, `<text>`, `<title>`, and `<desc>`; nested SVGs, paths, polygons, external references, images, and every other element are rejected. Duplicate attributes, mismatched markup, URL-valued SVG references, transforms, CSS loading through `@import`/`url()`, and geometry-affecting CSS are forbidden. Chart elements carry neither `class` nor inline `style`; document styles may not use selectors capable of matching the chart, except the canonical root `svg` layout rule. The only external stylesheet allowed is Google Fonts. Every line carries exactly one of `data-polar-spoke`, `data-polar-ray`, or `data-polar-rule` and uses the role's declared stroke width; spoke and ray geometry is verified within `min(0.75 px, 5% of R)`. The five grid circles carry `data-polar-ring`, use `fill="none"`, a 0.8 px stroke, the chart center, and the 20% through 100% radius intervals exactly once. Every endpoint circle carries `data-polar-marker` and uses the declared 4 px radius, or 5 px for the focal category, with a 1.2 px stroke. The optional full-canvas background is the only rectangle and carries `data-polar-background`. This fail-closed allowlist prevents contributors from bypassing the radius-only encoding with an unannotated or falsely annotated filled shape.
### Zero
For `v = 0`, `radius_i = 0`. Render no value ray and no endpoint marker. Keep the faint full-radius category spoke, and render the category's numeric label as `0`. A single scale label at the center also marks the shared zero baseline. Do not add a minimum hub, minimum ray length, displaced marker, or other visible magnitude.
### Missing values
Missing values are out of scope for the first version. The agent must stop and ask whether to omit the category or supply a value; it must not coerce missing data to zero.
## Geometry and Layout
All three examples use `viewBox="0 0 1000 520"`, center `(500, 230)`, and `R = 160`.
- Grid rings: `r = 32, 64, 96, 128, 160`.
- Category spokes: center to `R`, 0.8 px rule stroke, no arrowhead.
- Non-focal value rays: 2 px muted stroke; endpoint marker radius 4.
- Focal value ray: 2.4 px accent stroke; endpoint marker radius 5.
- Ring labels: `0.2 × max` through `1.0 × max` on the first axis only, Geist Mono 8 px; the example therefore shows `20, 40, 60, 80, 100`.
- Category labels: at `R + 28`, Geist Sans 11 px semibold, horizontal and upright.
- Numeric labels: at `R + 44`, Geist Mono 8 px, including the unit in the chart subtitle rather than repeating it eight times.
- Label anchor: `middle` within 15 degrees of vertical, `start` on the right half, and `end` on the left half.
- Drawing order: background, rings, spokes, scale labels, non-focal rays, focal ray, endpoint markers, category labels, numeric labels, legend/source note.
The minimal examples use the full 1000×520 canvas. The full-editorial example places the same chart in the existing editorial frame and adds summary cards without changing chart geometry.
## Visual Semantics
- One accent category maximum. All other value rays use the muted token.
- Circular rings and spokes are structural grid marks, never category colors.
- Endpoint markers are constant-size lollipop heads, not bubbles.
- Category labels remain horizontal; tangent rotation is intentionally excluded because it lowers scanability and belongs to the rejected qualitative wheel direction.
- The chart must remain understandable in grayscale: position and numeric labels carry the data; accent only directs attention.
- The SVG accessible description names the peak category, the scale, and the clockwise category order without narrating geometry.
## Example Dataset
All three variants use the input contract above. The values are explicitly illustrative, normalized as a percentage of the daily peak, and make no external factual claim. `12–15` is the focal category at `100`; `03–06` is the minimum at `18`.
The examples intentionally do not include zero because the zero rule is validated by an adversarial verifier fixture rather than by changing the primary editorial story.
## Taxonomy and Documentation
The implementation will:
1. Add `references/type-polar.md` and route “polar chart”, “radial lollipop”, “cyclical magnitude”, and equivalent requests to it.
2. Add **Polar chart** to the SKILL.md visual-type guide and frontmatter description.
3. Change the shipped visual-type count from 27 to 28 across README, SKILL.md, onboarding/reference prose, validation scripts, and ADR 0002.
4. Add the three examples to the gallery as `example-polar.html`, `example-polar-dark.html`, and `example-polar-full.html`.
5. Add a README gallery cell and screenshot for the new type.
6. Use a minor plugin version bump because this adds a public visual type.
ADR 0002 remains valid: semantic patterns do not expand the taxonomy. Its count changes because Polar supplies a genuinely new quantitative circular layout grammar, the exception already allowed by the ADR.
## Machine Verification
Each example's primary SVG carries:
```html
<svg data-polar-chart data-polar-cx="500" data-polar-cy="230"
data-polar-radius="160" data-polar-max="100" ...>
```
Each category is a group with `data-polar-category`, `data-polar-index`, and `data-polar-value`. Positive categories contain one `line[data-polar-ray]` and one `circle[data-polar-marker]`; zero categories contain neither.
A new `scripts/verify-polar.py` gate will:
- require the exact three shipped variants;
- enforce category count, unique indices, shared zero baseline, and at most one focal category;
- recompute every positive endpoint from angle and linearly scaled radius within `min(0.75 px, 5% of R)`;
- reject a ray or endpoint marker for zero;
- allowlist the chart's SVG elements and semantic area-bearing roles, rejecting unannotated or falsely annotated filled shapes, transforms, and non-zero inner baselines;
- confirm all three variants carry identical values and geometry metadata.
`scripts/test-verify-polar.py` will cover a valid chart plus adversarial cases for a 25% hub, square-root radius encoding, a visible zero ray, out-of-range values, duplicate indices, and variant data drift. CI and CONTRIBUTING will run both scripts.
## Acceptance Criteria
- Radius, not area, is the only graphical magnitude encoding.
- A zero-valued category produces zero ray length and remains legible through its numeric label.
- All three examples are static, self-contained, accessible, gallery-reachable, and skin-clean.
- The verifier fails on the known #64 baseline defect and passes the shipped examples.
- The visual-type count and routing vocabulary are consistent across every documented surface.
- Both plugin manifests receive the same minor version.
- The complete CONTRIBUTING validation suite passes on Python 3.11 and 3.12 without adding dependencies.