Merge branch 'main' into fc3/traceable-block-decomposition

This commit is contained in:
Cathryn Lavery
2026-09-06 16:46:50 -05:00
committed by GitHub
20 changed files with 173 additions and 41 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "diagram-design",
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, polar chart (polar/radial lollipop), loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
"version": "2.6.12",
"version": "2.6.15",
"author": {
"name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "diagram-design",
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, polar chart (polar/radial lollipop), loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
"version": "2.6.12",
"version": "2.6.15",
"author": {
"name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "diagram-design",
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, polar chart (polar/radial lollipop), loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
"version": "2.6.12",
"version": "2.6.15",
"author": {
"name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery"
+16 -6
View File
@@ -125,14 +125,19 @@ jobs:
set -euo pipefail
python3 scripts/verify-plugin-package.py "$SOURCE_SHA"
expected=$(printf '%s\n' \
expected_without_skill=$(printf '%s\n' \
.claude-plugin/plugin.json \
.codex-plugin/plugin.json \
.factory-plugin/plugin.json | LC_ALL=C sort)
expected_with_skill=$(printf '%s\n' \
.claude-plugin/plugin.json \
.codex-plugin/plugin.json \
.factory-plugin/plugin.json \
skills/diagram-design/SKILL.md | LC_ALL=C sort)
actual=$(git diff --name-only | LC_ALL=C sort)
if [ "$actual" != "$expected" ]; then
echo "Bump changed files outside the release allowlist:" >&2
if [ "$actual" != "$expected_without_skill" ] \
&& [ "$actual" != "$expected_with_skill" ]; then
echo "Bump changed unexpected files or omitted a required manifest:" >&2
git diff --name-only >&2
exit 1
fi
@@ -199,13 +204,18 @@ jobs:
run: |
set -euo pipefail
patch=_version-bump/version-bump.patch
expected=$(printf '%s\n' \
expected_without_skill=$(printf '%s\n' \
.claude-plugin/plugin.json \
.codex-plugin/plugin.json \
.factory-plugin/plugin.json | LC_ALL=C sort)
expected_with_skill=$(printf '%s\n' \
.claude-plugin/plugin.json \
.codex-plugin/plugin.json \
.factory-plugin/plugin.json \
skills/diagram-design/SKILL.md | LC_ALL=C sort)
patch_paths=$(git apply --numstat "$patch" | cut -f3- | LC_ALL=C sort)
if [ "$patch_paths" != "$expected" ]; then
if [ "$patch_paths" != "$expected_without_skill" ] \
&& [ "$patch_paths" != "$expected_with_skill" ]; then
echo "Artifact contains paths outside the release allowlist" >&2
exit 1
fi
@@ -213,7 +223,7 @@ jobs:
git apply --check --whitespace=error "$patch"
git apply --index --whitespace=error "$patch"
staged_paths=$(git diff --cached --name-only | LC_ALL=C sort)
if [ "$staged_paths" != "$expected" ] \
if [ "$staged_paths" != "$patch_paths" ] \
|| [ -n "$(git diff --cached --summary)" ]; then
echo "Artifact changed unexpected paths or file modes" >&2
exit 1
+1 -1
View File
@@ -572,7 +572,7 @@ One accent color, 12 focal elements per diagram. Three font families: Instrum
- **Annotation callout** — italic Instrument Serif + dashed Bézier leader, for editorial asides that sit in the margins. See [`skills/diagram-design/references/primitive-annotation.md`](skills/diagram-design/references/primitive-annotation.md).
- **Sketchy filter** — SVG turbulence + displacement map for a hand-drawn variant. Good for essays, not for technical docs. See [`skills/diagram-design/references/primitive-sketchy.md`](skills/diagram-design/references/primitive-sketchy.md).
- **Icon set** — 55 monochrome IT/cloud icons (laptop, phone, user, server, database, Docker, Kubernetes, AWS, Azure, GitHub, Postgres…) for richer architecture and sequence diagrams. Stroked icons from [Tabler Icons](https://tabler.io/icons) (MIT); brand silhouettes from [Simple Icons](https://simpleicons.org) (CC0). Each icon uses `currentColor` so it inherits the editorial skin or your onboarded brand. See [`skills/diagram-design/references/primitive-icons.md`](skills/diagram-design/references/primitive-icons.md); browse the [gallery](skills/diagram-design/assets/icons.html). Regenerate with `python scripts/build-icons.py`.
- **Icon set** — 87 monochrome IT/cloud icons (laptop, phone, user, server, database, Docker, Kubernetes, AWS, Azure, GitHub, Postgres…) for richer architecture and sequence diagrams. Stroked icons from [Tabler Icons](https://tabler.io/icons) (MIT); brand silhouettes from [Simple Icons](https://simpleicons.org) (CC0). Each icon uses `currentColor` so it inherits the editorial skin or your onboarded brand. See [`skills/diagram-design/references/primitive-icons.md`](skills/diagram-design/references/primitive-icons.md); browse the [gallery](skills/diagram-design/assets/icons.html). Regenerate with `python scripts/build-icons.py`.
---
+1
View File
@@ -71,6 +71,7 @@ ALLOWED_FONTS = {
"noto sans mono cjk sc",
"pingfang tc",
"noto sans tc",
"noto serif tc",
"microsoft jhenghei",
"noto sans mono cjk tc",
"system-ui",
+61
View File
@@ -16,6 +16,7 @@ from typing import Iterator, Optional
ROOT = Path(__file__).resolve().parent.parent
VERIFY_SCRIPT = ROOT / "scripts/verify-plugin-package.py"
BUMP_SCRIPT = ROOT / "scripts/bump-plugin-version.py"
AUTO_BUMP_WORKFLOW = ROOT / ".github/workflows/auto-bump.yml"
PLUGIN_NAME = "diagram-design"
@@ -479,6 +480,11 @@ def test_bumper() -> None:
with tempfile.TemporaryDirectory() as scratch:
root = Path(scratch)
seed_package(root)
version_paths = (*BUMP.MANIFEST_PATHS, BUMP.SKILL_PATH)
before = {
relative: (root / relative).read_text(encoding="utf-8")
for relative in version_paths
}
actual = BUMP.bump(root, part)
versions = {
json.loads((root / relative).read_text(encoding="utf-8"))["version"]
@@ -495,6 +501,19 @@ def test_bumper() -> None:
f"{part} bump left SKILL.md metadata.version off "
f"{expected_minor!r}: {skill_text!r}"
)
changed = {
relative
for relative in version_paths
if (root / relative).read_text(encoding="utf-8") != before[relative]
}
expected_changed = set(BUMP.MANIFEST_PATHS)
if part != "patch":
expected_changed.add(BUMP.SKILL_PATH)
if changed != expected_changed:
raise AssertionError(
f"{part} bump changed {sorted(map(str, changed))}; expected "
f"{sorted(map(str, expected_changed))}"
)
print(f"OK: {part} bump produced {expected} and synced SKILL.md")
with tempfile.TemporaryDirectory() as scratch:
@@ -535,9 +554,51 @@ def test_bumper() -> None:
print("OK: version bumper fails closed on SKILL.md drift, manifests untouched")
def test_auto_bump_workflow_allowlists() -> None:
workflow = AUTO_BUMP_WORKFLOW.read_text(encoding="utf-8")
expected = {
"expected_without_skill": tuple(
sorted(relative.as_posix() for relative in BUMP.MANIFEST_PATHS)
),
"expected_with_skill": tuple(
sorted(
relative.as_posix()
for relative in (*BUMP.MANIFEST_PATHS, BUMP.SKILL_PATH)
)
),
}
for variable, expected_paths in expected.items():
marker = f"{variable}=$(printf '%s" + "\\n' \\" + "\n"
chunks = workflow.split(marker)
if len(chunks) != 3:
raise AssertionError(
f"expected prepare and publish assignments for {variable}; "
f"found {len(chunks) - 1}"
)
for job, chunk in zip(("prepare", "publish"), chunks[1:]):
body, separator, _ = chunk.partition("| LC_ALL=C sort)")
if not separator:
raise AssertionError(f"could not parse {job} {variable} allowlist")
actual_paths = tuple(
sorted(
line.strip().removesuffix("\\").strip()
for line in body.splitlines()
if line.strip()
)
)
if actual_paths != expected_paths:
raise AssertionError(
f"{job} {variable} allowlist is {actual_paths}; "
f"expected {expected_paths}"
)
print("OK: prepare and publish workflow allowlists match bumper paths")
def main() -> int:
test_verifier()
test_bumper()
test_auto_bump_workflow_allowlists()
print("All plugin package tests passed")
return 0
+28 -2
View File
@@ -596,9 +596,35 @@ diagram-design/
raise AssertionError(f"variant with missing parent not caught: {errs}")
print("OK gallery: variant with missing parent caught")
with tempfile.TemporaryDirectory(prefix="verify-docs-sync-assets-") as asset_tmp:
tmp_skill_dir = Path(asset_tmp)
tmp_asset_dir = tmp_skill_dir / "assets"
tmp_ref_dir = tmp_skill_dir / "references"
tmp_asset_dir.mkdir(parents=True)
tmp_ref_dir.mkdir(parents=True)
(tmp_asset_dir / "example-valid.html").write_text("", encoding="utf-8")
(tmp_ref_dir / "type-sample.md").write_text(
"- `assets/example-valid.html`\n- `assets/example-missing.html`\n",
encoding="utf-8",
)
errs: list[str] = []
verify.check_reference_asset_links(errs, tmp_skill_dir)
if not any("type-sample.md" in e and "example-missing.html" in e for e in errs):
raise AssertionError(f"missing asset citation was not caught: {errs}")
print("OK reference assets: missing asset citation caught")
(tmp_asset_dir / "example-missing.html").write_text("", encoding="utf-8")
errs = []
verify.check_reference_asset_links(errs, tmp_skill_dir)
if errs:
raise AssertionError(f"valid asset citations produced unexpected error: {errs}")
print("OK reference assets: valid asset citations produce no error")
print(
"PASS: docs sync checks references, strict-bundler packaging, routing surfaces, "
"Factory install contract, type-count routing, High-Level invariants, "
"PASS: docs sync checks references, asset citations, strict-bundler packaging, "
"routing surfaces, Factory install contract, type-count routing, High-Level invariants, "
"and gallery guards (parent/variant model)"
)
return 0
+26 -3
View File
@@ -286,6 +286,28 @@ def check_skill_reference_links(
errors.append(f"SKILL.md links to missing reference {target!r}")
def check_reference_asset_links(
errors: list[str], skill_directory: Path
) -> None:
"""Require every asset cited across skill documentation to exist on disk."""
asset_dir = skill_directory / "assets"
ref_dir = skill_directory / "references"
md_paths = [skill_directory / "SKILL.md", *sorted(ref_dir.glob("*.md"))]
asset_pattern = re.compile(r"assets/([A-Za-z0-9_.-]+\.html)")
for path in md_paths:
if not path.is_file():
continue
content = path.read_text(encoding="utf-8")
for match in asset_pattern.finditer(content):
asset_name = match.group(1)
target = asset_dir / asset_name
if not target.is_file():
errors.append(
f"{path.name} cites missing asset 'assets/{asset_name}'"
)
def scanner_visible_support_references(markdown: str) -> list[str]:
"""Return the local support paths a strict skill bundler will request."""
normalized_markdown = markdown.replace("\\", "/")
@@ -546,6 +568,7 @@ def main() -> int:
SKILL.read_text(encoding="utf-8"),
SKILL.parent,
)
check_reference_asset_links(errors, SKILL.parent)
check_packaged_support_references(
errors,
SKILL.read_text(encoding="utf-8"),
@@ -565,9 +588,9 @@ def main() -> int:
return 1
print(
"OK docs sync: description hooks, gallery reachability, README tree, "
"reference links, packaged support files, routing surfaces, manifest descriptions, "
"Factory install contract, type-count routing, High-Level invariants, "
"onboarding trust boundary, Line dark-skin contract"
"reference links, asset citations, packaged support files, routing surfaces, "
"manifest descriptions, Factory install contract, type-count routing, "
"High-Level invariants, onboarding trust boundary, Line dark-skin contract"
)
return 0
+2 -2
View File
@@ -200,12 +200,12 @@ Type-specific anti-patterns live in each type reference linked in the guide.
- **Arrow label** — Geist Mono, 8px — annotation on arrows
- **Editorial aside** — Instrument Serif *italic*, 14px — callouts only
**Korean labels** — Geist and Instrument Serif carry no Hangul. Extend the family on that `<text>`, budget 1em per Unicode wide or full-width character and the Latin advance for every other, and never set Hangul below 12px. Full rules in [`style-guide.md`](references/style-guide.md#korean-labels).
**CJK labels** — Geist and Instrument Serif carry no Hangul or Han; extend the family and keep CJK at 12px+. Rules: [Korean](references/style-guide.md#korean-labels), [Chinese](references/style-guide.md#traditional-chinese-labels).
**Mono is for technical content only** — never as a blanket "dev" font, and never JetBrains Mono.
```html
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap" rel="stylesheet">
```
---
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagram</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[PROJECT NAME] Architecture</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Motion diagram template</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; }
:root {
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagram</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
@@ -144,10 +144,10 @@ Geist has no CJK coverage. When labels contain Japanese, Chinese, or Korean text
<text font-family="'Geist', 'Hiragino Sans', 'Noto Sans JP', 'Yu Gothic', sans-serif">認証サービス</text>
<text font-family="'Geist', 'Noto Sans KR', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif">인증 서비스</text>
<text font-family="'Geist', 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif">认证服务</text>
<text font-family="'Geist', 'PingFang TC', 'Noto Sans TC', 'Microsoft JhengHei', sans-serif">認證服務</text>
<text font-family="'Geist', 'Noto Sans TC', 'PingFang TC', 'Microsoft JhengHei', sans-serif">認證服務</text>
```
The Hiragino/Yu Gothic stack carries no Hangul glyphs, so Korean labels need the Korean stack — don't reuse the Japanese one. Noto Sans KR ships in the skin's font link, so it leads that stack and the local families follow it; the register, floor, and title rules Korean needs beyond the font live in [`style-guide.md`](style-guide.md#korean-labels). Japanese fonts also cover only a subset of the Chinese character set and render Simplified forms with Japanese glyph variants, so Chinese labels need a Chinese stack; Simplified and Traditional are separate stacks for the same reason. For mono sublabels use `'Geist Mono', 'Noto Sans Mono CJK JP', monospace` (Japanese), `'Geist Mono', 'Noto Sans Mono CJK KR', monospace` (Korean), or `'Geist Mono', 'Noto Sans Mono CJK SC', monospace` / `'Geist Mono', 'Noto Sans Mono CJK TC', monospace` (Chinese). Budget **1em per full-width CJK glyph**, not a small percentage over the average Latin glyph; `verify-treemap.py` uses that conservative contract for Unicode wide/full-width characters and treats combining marks as non-advancing. Prefer 12px names over 8px sublabels for CJK, which goes muddy below 10px. Actual width still varies by fallback font, so run the relevant geometry verifier after translating labels.
The Hiragino/Yu Gothic stack carries no Hangul glyphs, so Korean labels need the Korean stack — don't reuse the Japanese one. Noto Sans KR ships in the skin's font link, so it leads that stack and the local families follow it; the register, floor, and title rules Korean needs beyond the font live in [`style-guide.md`](style-guide.md#korean-labels). Japanese fonts also cover only a subset of the Chinese character set and render Simplified forms with Japanese glyph variants, so Chinese labels need a Chinese stack; Simplified and Traditional are separate stacks for the same reason. Noto Sans TC now ships in the link too, so it leads the Traditional stack and the local families follow; the register, floor, and title rules Traditional Chinese needs beyond the font live in [`style-guide.md`](style-guide.md#traditional-chinese-labels). For mono sublabels use `'Geist Mono', 'Noto Sans Mono CJK JP', monospace` (Japanese), `'Geist Mono', 'Noto Sans Mono CJK KR', monospace` (Korean), or `'Geist Mono', 'Noto Sans Mono CJK SC', monospace` / `'Geist Mono', 'Noto Sans Mono CJK TC', monospace` (Chinese). Budget **1em per full-width CJK glyph**, not a small percentage over the average Latin glyph; `verify-treemap.py` uses that conservative contract for Unicode wide/full-width characters and treats combining marks as non-advancing. Prefer 12px names over 8px sublabels for CJK; Hangul and Han go muddy below 12px, so treat 12px as the floor rather than 10px. Actual width still varies by fallback font, so run the relevant geometry verifier after translating labels.
---
@@ -83,7 +83,7 @@ A self-contained palette for the terminal-window primitive (see [primitive-termi
### Font stack
```html
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap" rel="stylesheet">
```
### Korean labels
@@ -108,6 +108,28 @@ Three rules follow from Hangul metrics:
**Load-bearing rule:** Mono is for *technical* content (ports, commands, URLs, field types). Names go in Geist sans. Page title is Instrument Serif. Italic Instrument Serif is reserved for annotation callouts (see [primitive-annotation.md](primitive-annotation.md)). **Never JetBrains Mono** as a blanket "dev" font.
### Traditional Chinese labels
Geist and Instrument Serif carry no Han. A Traditional Chinese `<text>` element extends its own family — never swap the skin:
```svg
<text font-family="'Geist', 'Noto Sans TC', 'PingFang TC', 'Microsoft JhengHei', sans-serif">請求項比對</text>
```
Both Noto TC faces ship in the font link above, so the web font resolves before any locally installed one and the same file renders identically on macOS, Windows, and a reviewer's browser. The local families follow it for offline viewing. Page titles need the serif equivalent — `'Instrument Serif', 'Noto Serif TC', serif` — or a mixed Latin/Han title resolves Han through the platform's generic serif and the two halves disagree. Google's `css2` endpoint slices Chinese by unicode-range, so a diagram with a handful of Chinese labels downloads only the slices it touches.
**Width budget.** The per-character contract above is unchanged: every Unicode wide or full-width character costs 1em, every other character costs its face's Latin advance, and nonspacing marks cost nothing. Full-width punctuation — `()「」,。:` — is wide and costs 1em as well, which is the part most often dropped.
Counting by script is the trap. `請求項 v2.1` is three full-width characters and five narrow ones; a formula that tallies Han and Latin letters silently drops `2`, `.`, and `1` and sizes the box for six of its nine characters.
Three rules follow from Han metrics, mirroring the Hangul ones:
- **Sublabels stay Latin.** Ports, protocols, field types, and URLs are Latin anyway — keep `Geist Mono` there and don't translate them. Han in a 9px mono sublabel is unreadable and has no mono face to fall back to. A sublabel that is prose rather than a value may be Chinese, but it then switches register by the third rule below.
- **Floor of 12px.** Han packs more strokes than Hangul into the same em box, so the 12px floor binds at least as hard here. If a Chinese name doesn't fit at 12px, cut the name — don't shrink the type.
- **Arrow labels, eyebrows, and legend text switch register.** Those slots are 78px Geist Mono, uppercase and tracked, which Han has neither a face nor legibility for. A Chinese label in one of those slots becomes 12px sans at weight 500 with no tracking and no uppercase transform, and its mask rect grows to match (16px tall, width from the budget above, still rounded to a multiple of 4). Latin labels in the same diagram keep the mono treatment.
Simplified Chinese takes the same three rules with the Simplified stack (`'Noto Sans SC'`, `'PingFang SC'`, `'Microsoft YaHei'`). That face does not ship in the link, so Simplified labels still resolve through whatever the viewer has locally.
---
## Stroke, radius, spacing
@@ -369,6 +369,3 @@ Above 4 lanes or 6 steps: split into two diagrams (e.g., ingestion pipeline / an
- `assets/example-data-flow.html` — minimal light (the platform, 4-role × 5-step: Admin, Engineers, Scientists, Consumers). Gallery default.
- `assets/example-data-flow-dark.html` — same, dark skin.
- `assets/example-data-flow-full.html` — same, editorial-card frame.
- `assets/example-data-flow-extended.html` — exercises §4 color override: Access Control node in rust-red (governance), Clean & Stage node in slate-blue (data quality). Focal accent on Analyze step + Explore & Model node + anon-data arrow unchanged.
- `assets/example-data-flow-extended-dark.html` — extended pattern, dark skin.
- `assets/example-data-flow-extended-full.html` — extended pattern, editorial-card frame.
@@ -405,6 +405,3 @@ When this gets unwieldy:
- `assets/example-dp-integration.html` — minimal light (1 footer = AD). Gallery default.
- `assets/example-dp-integration-dark.html` — same, dark skin.
- `assets/example-dp-integration-full.html` — same, editorial-card frame.
- `assets/example-dp-integration-extended.html` — exercises §4 color override + multi-footer: AD in rust-red, Observability (Prometheus/Grafana/Loki) in slate-blue. Canvas height grown to fit 2 footer rows.
- `assets/example-dp-integration-extended-dark.html` — extended pattern, dark skin.
- `assets/example-dp-integration-extended-full.html` — extended pattern, editorial-card frame.
@@ -396,8 +396,6 @@ Before emitting SVG, verify **every** item:
- `assets/example-it-state.html` — minimal light (NatStat canonical: 3 zones, 9 components, 8 connectors, 0 footer bars, SQL Server tinted olive). Gallery default.
- `assets/example-it-state-dark.html` — same, dark skin.
- `assets/example-it-state-full.html` — same, editorial-card frame with summary cards.
- `assets/example-it-state-extended.html` — exercises §4 color override + footer bars: 2 footer bars (Identity Manager + Observability) below the zones, third custom color on Analyst Machines (slate-blue, data-quality concern).
- `assets/example-it-state-extended-dark.html` — extended pattern, dark skin.
---
@@ -467,4 +465,4 @@ dark: false
When the source row matches the destination row's y range (e.g., Survey at y=108 with Shared Drive at y=80148), prefer **side-edge** entry — a single horizontal path with a fully visible arrow. When the source row is offset, detour through the destination's nearest zone background to enter a side edge rather than approaching a top/bottom edge from the wrong side.
The extended example (§9 line 4) demonstrates footer bars + a third custom color and proves `viewBox_h` grows correctly when `N_footer > 0`.
When footer bars are added (`N_footer > 0`), `viewBox_h` grows according to the canvas formula in §2 to accommodate the footer rows and custom colors.
@@ -388,9 +388,9 @@ Above 6 lanes or 12 steps: split into two diagrams (overview + detail).
---
## 12. Worked example — full YAML for `example-process-extended.html`
## 12. Worked example — full YAML for an extended process variant
The extended example diagram is fully described by the following inputs. Every coordinate in the rendered SVG is derivable from this block via §2 + §3 + §4. This is the canonical proof that the parametric contract works end-to-end.
The following input block describes an extended process workflow with custom colors and lanes. Every coordinate in the rendered SVG is derivable from this block via §2 + §3 + §4, demonstrating the parametric layout contract end-to-end.
```yaml
# Quarterly survey — end-to-end workflow (extended variant)
@@ -461,15 +461,15 @@ dark: false
Run §2 of this reference with these inputs:
- `n_lanes = 6`, `n_steps = 11`, `has_color_row = true` (3 nodes carry `color`).
- `viewBox_w = 140 + 11 * 112 + 28 = 1400`. ✓ matches rendered SVG.
- `viewBox_w = 140 + 11 * 112 + 28 = 1400`. ✓
- `legend_h = 100`, `viewBox_h = 36 + 6 * 80 + 100 = 616`. ✓
- Lane y_top = [36, 116, 196, 276, 356, 436]; lane mid = [76, 156, 236, 316, 396, 476]. ✓
- Step cx = [198, 310, 422, 534, 646, 758, 870, 982, 1094, 1208, 1320] (the 8-px content-area gutter shifts every value by 8 from `140 + j*112 + 50`). ✓
- Node 4 (HQ Review): step=3, lane="SVY" (k=3) → x = 534-50 = 484, y = 276+8 = 284. ✓
- Node 5 (Error Checks): step=4, lane="IT" (k=1) → x = 646-50 = 596, y = 116+8 = 124. ✓
- Node 10 (Public Release): step=9, lane="CMM" (k=5) → x = 1208-50 = 1158, y = 436+8 = 444. ✓ *(Rendered uses x=1156 — 2-px tolerance from chip-width rounding on the step "10" label.)*
- Node 10 (Public Release): step=9, lane="CMM" (k=5) → x = 1208-50 = 1158, y = 436+8 = 444. ✓
The two coord drifts on the rightmost two nodes (chip width=20 for two-digit step numbers shifts the chip but not the node center math) are an artifact of the existing hand-tuned example, not a formula failure — a fresh generation from this YAML would produce x=1158 and the diagram would be visually indistinguishable from the shipped version.
A fresh generation from this YAML produces deterministic coordinates directly from the formula without manual tweaking.
### 12.2 Adapting this YAML to a different process
@@ -490,6 +490,3 @@ Everything else — viewBox sizing, chip positions, legend layout, dark-mode tok
- `assets/example-process.html` — minimal light (quarterly survey: 11 steps, 6 divisions, data-type chips). Gallery default.
- `assets/example-process-dark.html` — same, dark skin.
- `assets/example-process-full.html` — same, editorial-card frame.
- `assets/example-process-extended.html` — exercises §4 color override: Build app in slate-blue (data quality), Train enumerators in rust-red (governance), Publish results in olive-green (data products). Focal accent on Pilot test step + node unchanged.
- `assets/example-process-extended-dark.html` — extended pattern, dark skin.
- `assets/example-process-extended-full.html` — extended pattern, editorial-card frame.