九份 ADR:一個畫圖技能包怎麼把設計品味變成 CI 關卡
補寫於 2026 年 9 月 4 日,日期掛回文章原本該發的那天。文中對 diagram-design 的描述以撰寫當日的官方文件與 skill 原始碼為準。
2026 年 4 月,GitHub 上多了一個叫 diagram-design 的 repo。作者 Cathryn Lavery 做它的理由很生活化:她自己寫部落格要配圖,每次不是跟 Figma 耗三十分鐘,就是乾脆不放圖。
不到五個月,這個 repo 衝到三萬顆星。翻進去會發現它沒有函式庫、沒有 CLI、也沒有 web app,就是一包寫給 AI coding agent 讀的畫圖規格。你對 Claude Code 說「幫我畫一張架構圖:前端、後端、資料庫、Redis 快取」,它照著規格手寫 inline SVG,吐一個雙擊就能開、離線也看得懂的單檔 HTML 給你。
在它出現以前
那之前,工程師要在文章裡塞一張圖,路只有三條。開 Figma 手拖,品質可控但時間吃到飽。寫 Mermaid,語法五分鐘搞定,可是排版邏輯完全插不上手,你只能接受它給的那個結果。第三條是直接叫 AI 畫,然後拿到那種東西:圓角方框、深色底、青紫色光暈,每個節點長得一模一樣,看起來很「科技」但你講不出它到底表達了什麼。
repo 的一句話介紹就是衝著第三條路來的:No shadows. No Mermaid slop.
它跟 Mermaid 的分工剛好是反的。Mermaid 是你寫語法、它幫你排版;diagram-design 把排版規則寫成文件,動筆的是 LLM。真正的引擎不在某個 renderer 裡,就是模型本身照一份很硬的規格手工擺座標。所以它匯入 Mermaid 的那個動作叫 redraw 不叫 convert,來源只提供內容(有哪些節點、誰連誰),原本的座標、配色、字體、形狀一律丟掉重來。
把「好看」改寫成對錯題
整包東西第一個值得偷的動作,是它沒有叫模型「畫漂亮一點」。那種指令對 LLM 等於沒說。它把審美拆成一條一條非對即錯的條件:
| 規則 | 內容 |
|---|---|
| 4px 網格 | 所有座標、尺寸、間距都要被 4 整除。自檢法則好記到誇張:座標尾數出現 1/2/3/5/6/7/9 就是錯的。字級只准 8/12/16/20/24/28/32/40 |
| 複雜度預算 | 節點上限 9 個、箭頭 12 條、accent 色元素 2 個。超過就拆成 overview 加 detail,不准縮小字級硬塞 |
| 連接器 | 不共軸的節點之間禁止斜線,每個轉角必須是 r=8 的四分之一圓弧。斜線連接器是 automatic fail |
| 陰影 | 一律禁止。原文一句話:Shadows are out. Borders are in. |
| 字族分工 | serif 給標題、sans 給節點名、mono 只給技術內容。而且明文永遠不准用 JetBrains Mono,理由是「一種假裝很技術的偷懶」 |
連接器那組總共六條,其中一條我看了直接想抄進自己的 prompt:同一邊有 N 條線時,第 k 條的附著點固定落在 L * k / (N + 1),相鄰附著點間距至少 12px。這條把「線全部擠在方框中央」這個 AI 老毛病用一個算式解掉了。另一條是連接器不准從「非起點也非終點」的方框後面穿過去,真的繞不開時線要改成虛線(stroke-dasharray="4,3")表示過境、非互動,箭頭只能落在真正的終點。
再往下還有 12 條通用 anti-pattern,每條都附一句「為什麼失敗」。Dark mode + cyan/purple glow 的判詞是 *”Looks ‘technical’ without design decisions”*;Coral on every important node 的判詞是 “Coral is 1-2 editorial accents, not a signaling system”。地基則是 README 引的那句:The highest-quality move is usually deletion. 目標密度直接寫死在文件裡,4/10。
寫到這裡,它做完的事其實只有一件:把品味寫成了文字。文字會不會生效,是完全另一個問題。
九個壞掉的範例,全部通過檢查
答案寫在 ADR 0005 裡。
有 9 個已經出貨的範例犯了同一個錯:label 底下那塊遮罩矩形被放進後畫的節點裡,於是被節點填色蓋掉,文字變成貼在邊框上的碎片。這種東西人眼一看就知道壞了。
問題在於,當時所有既有的 gate 全部通過。lint-skin.py 只看顏色跟字體,self_check.py 只看 DOM,沒有任何一個 gate 會去讀座標。規則本身白紙黑字寫在規格裡,寫得非常清楚,然後九個範例就這樣一路走到使用者手上。
ADR 的結論寫得很硬:只活在文字裡的規則,就是會出貨壞範例的規則。
這句話值得停一下。我們平常寫 coding style、寫 code review checklist、寫「這個專案的 commit message 要長這樣」,寫完就當作事情辦完了。這個專案的態度是相反的,規則寫進文件只是草稿狀態,要有一支腳本真的去讀它、真的會讓 CI 紅燈,那條規則才算存在。於是他們補了 verify-geometry.py 去解析矩形座標,判準取的是文件順序。SVG 裡誰蓋住誰由畫的先後決定,光看幾何重疊判不出來。
從這一刻開始,這個 repo 的性格就定了。後面幾份 ADR 都在做同一件事:找出一條「大家都同意但沒人在檢查」的規則,然後把它變成一支會擋你的腳本。這篇挑的是這個形狀的四份,其餘五份我沒挑進來,不展開。
先講清楚我站在哪裡。 這篇是讀文件跟原始碼寫的,我沒有實際裝起來畫過任何一張圖,所以整篇的觀察都是紙上得來的,實際手感如何我說不準。唯一有實測的是文末那組星數與 fork 數,那是當場去打 API 取回來的。另外有兩件事文件自己承認了:assets/ 裡附的範例 HTML 是舊 skin 產的,別拿它當現行色票參考;官方文件還有兩處數字過期,repo description 寫 38 種型別(實際 39)、README 寫 55 個內建圖示(實際 87),抄之前自己數一下。
還有一個台灣使用者會直接撞到的洞:CJK 段落只處理日文、韓文、簡體中文(Noto Sans JP、Noto Sans KR),Noto Sans TC 從頭到尾沒出現過。繁體中文大概得自己補 family,但這是推論不是官方支援,動手前自己驗一次。
skill 的 description 是一個介面
SKILL.md 有位元組上限。有人為了塞進去,把 27 個型別名從 frontmatter 的 description 裡刪掉了。單看那次改動完全合理,description 本來就是一句話介紹,型別清單搬到正文裡不是很正常嗎。
不正常。description 是 agent 決定要不要載入這個 skill 之前,唯一看得到的文字。刪掉「flowchart」這個字,等於「make me a flowchart」這句話再也叫不動這個 skill。skill 本體一行沒少、規格一條沒改,功能就這樣消失了一半,而且你在任何測試裡都看不到它壞掉,只會覺得「今天 Claude 怎麼不太聽話」。
0004 就是為了這件事寫的。決議是把上限提到 40,000 bytes,要瘦身只准砍正文,另外寫一支 verify-docs-sync.py,只要 description 掉了某個型別的關鍵字就讓 CI 紅燈。把 skill 的 description 當成路由介面來版控,這個做法我在別的 skill repo 沒看過。它跟「API 的公開簽章不能亂改」是同一件事,只是那個簽章長得像行銷文案,所以大家都以為它可以隨便改。
沒有 golden image 的視覺回歸
整個 repo 裡一張比對用的 PNG 都沒有。
要抓的東西是「圖被裁切」。直覺做法是量幾何,但 getBoundingClientRect() 會忽略 stroke 寬度、marker、filter 溢出,也完全不懂 clip-path 跟 overflow: visible。結論是它既會漏掉真的裁切,也會捏造不存在的裁切,兩種錯都會犯。
lint-render.py 的做法是渲染後比對像素:照原樣截一張,把 overflow 放開再截一張,兩張相減,跑到框外面的墨水就是被切掉的部分。沒有 golden image,就沒有東西要重錄。
做過視覺回歸測試的人應該懂這句話的份量,golden image 那套東西最貴的不是建立,是每次改版之後那輪「這 40 張差異哪些是預期的」。
連文件裡的一個整數都有腳本盯著
0002 這份表面上在講架構,實際上把同一個習慣又推了一層。
它的設計決定是語意跟版面分離:semantic pattern 描述行為(佇列、政策追蹤、信任邊界),visual type 描述版面(flowchart、quadrant、swimlane、treemap),兩邊正交組合,7 種語意模式各自路由到最接近的版面型別。好處是「新增一種行為」不會讓圖型數量爆炸。
聽起來有點神經質:一個專案連自己文件裡的一個整數都要派人看著。
有趣的是 ADR 裡連數字史都留著:27 到 28(加了 treemap)、到 38(一次加十種)、到 39(polar)。而 repo 裡有兩支腳本把 39 這個數字硬寫死,改了型別數量卻沒同步改 ADR,CI 就紅燈。
星星變多之後撞上的牆
舊規則是每個 PR 都要自己遞增三份 manifest 的版號。這在只有作者一個人的時候完全沒問題,甚至還算嚴謹。
等到社群進來,同時開著的 PR 大約 17 個,所有分支都在改同樣那三行,於是一次 squash-merge 就讓那 17 個 PR 全部撞衝突。大家乖乖 rebase 完,下一次 merge 又全滅一輪。0009 記的就是這件事。
修法是把方向反過來:CI 直接擋住 PR 動版號,merge 進 main 之後才由 workflow 自動 bump。
原本靠「每個貢獻者記得做一件事」維持的紀律,換成機器單點負責。差別在於前面幾條是防錯,這條是防塞車,成本從一個人的注意力變成整個社群的 rebase 時間。
五個月後,它長成什麼樣子
現在的目錄有 474 個檔案、約 10.5 MB。SKILL.md 是 40 KB 的主入口,放哲學、選型決策、設計系統跟出貨前 checklist;references/ 53 檔,39 個型別規格加 14 份機制文件;assets/ 155 檔範例;docs/adr/ 就是上面那九份。
scripts/ 底下 52 支 Python,其中 20 支是 verify-*.py 驗證器,另外 17 支是 test-verify-*.py。後者的職責是驗證那些驗證器,也就是拿故意壞掉的輸入去確認 gate 真的攔得住。ADR 0005 那個教訓被貫徹得比我想像的徹底:規則要有腳本看著,腳本本身也要有東西看著。
規格總量 58 萬位元組,不可能整包塞進 context。它的解法叫漸進揭露,39 種圖型各自拆成獨立的 type-*.md,agent 一次只讀主檔加上真正用到的那一個型別檔。這件事講起來像 context 預算的技術細節,但它其實跟前面那條「刪除是最高品質的動作」是同一個信念的兩種寫法。
同樣的信念也長在輸出裡。來源太複雜塞不進複雜度預算時,它照固定順序砍:裝飾格、完全重複的節點(六個 worker 併成 Worker x6)、全是葉子的容器整包收起來、不影響故事的匯點、橫切基礎設施(logging、metrics、CI),砍到剛好進預算就停。然後砍掉的東西要交一份 fidelity ledger:
1 | Detail: balanced - 18 source nodes -> 9 drawn |
理由寫在文件裡:*”The reader of the diagram can’t see what’s missing. The person who asked for it needs to.”* 看圖的人看不出少了什麼,要圖的那個人必須知道。這句話拿去套 AI 寫的任何東西都成立,摘要、翻譯、code review 都一樣。
近兩週有一批外部 PR 進來:scatter 的 bubble 變體、line 的 ridgeline 變體、Mermaid 多方向 label 解析。其中有人是專門來替驗證器補對抗性測試的。
至於現況,MIT 授權、未封存、9 月 3 日當天還在 push。寫這段的時候去 API 點了一次:30,635 顆星、1,966 個 fork、28 位 contributor、main 上 135 個 commit。有意思的是它沒有 GitHub Release、沒有 tag,版號 2.6.12 只活在三份 plugin manifest 裡,你去找「最新 release」是找不到的。一個把文件裡的整數都寫成 CI 關卡的專案,自己的版本卻沒有任何一個對外的錨點,這個對比我到現在還沒想明白該怎麼解讀。































































































































































































