Day 03 · W1 · AI 線 · 難度 ★★★☆☆
本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01。
Day 1 說「AAA 自評的 20 題我們全部做出了程式判斷」,Day 2 說「工具有規則不等於抓得到」。
今天回答中間那段:那些規則是怎麼長出來的。
先看一段真的 git log。這是 2026 年 4 月 30 日晚上,同一台電腦上發生的事:
18:43 chore: initial commit
20:39 feat(rules): automate 5 AAA self-eval rules (Q2/Q6/Q7/Q8/Q13)
Coverage of MODA AAA self-eval form: 12/20 → 17/20
21:49 feat(rules): automate final 3 AAA self-eval rules (Q3/Q19/Q20)
Coverage of MODA AAA self-eval form: 17/20 → 20/20 (100%)
22:07 feat(form_probe): click likely modal triggers when no usable form found
23:00 feat(form_probe): two-stage triggers + Q19 modal-aware
23:07 fix(GN1210101E): exempt WAI-ARIA roving-tabindex roles
23:48 docs(README): document AAA self-eval coverage with per-rule mechanism
5 小時 5 分鐘,8 個 commit,自評表覆蓋率從 12/20 到 20/20。
我不寫 Python。這段時間我做的事是讀規範、判斷規則對不對、跑掃描、看結果、決定哪條要修。程式碼是 AI 寫的。
先講清楚一件事,免得這篇讀起來像「一個晚上生出一套工具」。
18:43 那個 initial commit 推上去的時候,專案裡已經有 121 條規則檔。那些是更早之前用同一套流程長出來的,只是還沒進版控。
那晚在做的是一件範圍明確的事:把 AAA 自評表那 20 題補完。 送件在即,自評表上還有 8 題沒有對應的程式檢查,得靠人一頁一頁看。

那晚的實際節奏。階梯是自評表的覆蓋數,橫軸是時間。注意 23:07 那一格是往回修,不是往前加。
所以正確的說法是:這套流程已經在轉了,那晚只是轉得比較快而已。 因為目標具體(補完 20 題)、驗收明確(自評表填得出來就算過),迴圈就跑得順。目標模糊的時候,同一套流程一樣會卡。
把這件事拆開,是一個三段迴圈:
聽起來理所當然。但關鍵在於:這三段都必須是機器讀得懂的形式,整組齒輪才會轉。
規範如果只存在於某個人腦袋裡,第一段就斷。規則如果散在一個三千行的大檔案裡,AI 每次都要重讀全部才敢改,第二段就慢到不能用。掃描結果如果只印在畫面上、沒有結構化輸出,第三段就等於沒有。

迴圈要能封閉,四個接點都得是機器讀得懂的東西。任何一段需要人手動搬運,整圈就停在那裡。
下面三節,一節一個齒輪。
MODA 把規範公開成文件,每一條檢核項目都有固定欄位:檢核碼、對應的 WCAG 成功準則、等級(A/AA/AAA)、還有一句中文描述。
這四個欄位剛好就是規則檔開頭那個 RuleMeta 要填的東西。等於規範本身已經幫你把 metadata 結構化了,這一段幾乎是機械搬運。
真正需要人介入的是第五個欄位:這條規則實際上要檢查什麼。
規範寫的是「應該達成什麼」,程式要寫的是「怎麼判斷有沒有達成」。規範說「使用語意組件標記強調文字」,它不會告訴你「超過 5 個 <b> 且沒有任何 <strong> 就算違規」。那個 5 是我們定的閾值,可以被質疑,而且改一次整站結果就變。
這中間沒有機械對應,是整套流程唯一無法外包的部分。
先看一條真的規則。這是專案裡最短的一個檔,完整內容 23 行:
"""HM1130105E rule."""
from __future__ import annotations
from ....models import Level
from ...base import Rule, RuleMeta, register
@register
class SemanticEmphasisMarkup(Rule):
"""HM1130105/106E — 強調文字應使用語意元件,不只是 b/i。"""
meta = RuleMeta(rule_id="HM1130105E", guideline="1.3.1", level=Level.A,
desc="使用語意組件來標記強調的文字或特殊文字",
source="extension")
def _check(self, soup, report, *, html, url, ctx) -> None:
b_count = len(soup.find_all(["b", "i"]))
sem_count = len(soup.find_all(["strong", "em"]))
if b_count >= 5 and sem_count == 0:
report.add(self._issue(
message=f"頁面使用 {b_count} 個 <b>/<i> 但無 <strong>/<em> 語意元件。",
status="info"))
檔案路徑是 rules/codes/structure/HM1130105E.py。一個檢核碼,一個檔,檔名就是檢核碼。
新增規則不需要去任何清單裡登記。套件啟動時會把 codes/ 底下每個資料夾裡的每個檔都 import 一遍,檔案被 import 時,@register 這個裝飾器就把規則登記進去。所以加一條規則 = 在資料夾裡丟一個檔。
這個設計有它的代價,而且我們踩過。
這個專案的巢狀結構是 rules/codes/<主題>/<檔>.py,所以檔案開頭那行 from ....models import Level 是四個點。寫成三個點,這個檔會 import 失敗,自動掛載直接跳過它,然後什麼事都不會發生。
沒有紅字,沒有警告。掃描照跑,規則總數少一條。除非你剛好記得應該有幾條,否則不會察覺。
對掃描器來說,一個 import 錯誤的檔案跟一個不存在的檔案,是同一件事。
這種失敗模式跟 Day 2 那 7 條「規則存在卻沒抓到」剛好互補:一個是規則在但判錯,一個是規則根本沒被載入。兩種都不會有東西跳出來提醒你。後來我們把規則總數當成一個要盯的數字,就是為了這個。
所以「一個檔一條規則」不只是整潔問題,是用一種可以被自動化的失敗模式,換掉另一種更難查的失敗模式。這個取捨值得整篇講,第 10 天會回來。
回頭看時間軸裡的 23:07 那筆:
23:07 fix(GN1210101E): exempt WAI-ARIA roving-tabindex roles
commit 訊息裡寫得很清楚:這條規則把任何帶 tabindex="-1" 的可聚焦元素都標為鍵盤無法到達,而這對複合元件是錯的。在 WAI-ARIA 的 roving tabindex 模式裡,一組元件中只有一個持有 tabindex="0"(讓你 Tab 進去),其他都是 tabindex="-1"(用方向鍵在其中移動)。
發現方式也寫在同一則訊息裡:由一個真實的偽陽性發現,自家網站 Tabs 元件裡的 <button role="tab" tabindex="-1"> 被誤判。
這就是齒輪三在轉。規則寫出來,跑自家網站,跳出一個看起來很可疑的 fail,人去看,發現是規則錯了不是網站錯了,回頭改規則。
注意這裡人做了什麼:判斷那個 fail 是真的還假的。 這件事 AI 目前做不好,因為它需要你知道 roving tabindex 是什麼、也知道那個元件實際用鍵盤操作起來是什麼感覺。AI 負責的是後半段:理解那個模式之後,把豁免清單寫成程式碼。那部分它很快。
這一環能成立,靠的是掃描結果有結構化輸出。報告可以出成 JSON,每一筆都帶著檢核碼、狀態、出問題的元素、所在網址。這代表 AI 可以直接讀掃描結果,不需要人把畫面上的東西轉述一遍。
如果工具只會在終端機印彩色文字給人看,這段就得由人念給 AI 聽。那不是迴圈,那是人在中間當傳聲筒。工具要能被 AI 使用,輸出格式是門檻,不是加分項。
我原本以為這套流程最難的地方是 prompt。實際跑下來,prompt 只排第三。
排第一的是齒輪一那個「第五欄位」,規範到程式碼中間的轉譯。排第二的是AI 會很有信心地寫錯方向 —— Day 2 講的那條 title 規則就是實例,它寫出了一個跑得很順、方向完全相反的檢查。程式碼品質沒問題,題目理解錯了。
那規範是 PDF,AI 讀 PDF 會不會出錯?會,但那不是主要風險。表格抽出來偶爾錯行,這種錯很明顯,一眼看得出來。真正危險的是抽得很順、格式完全正確、但漏了一整個章節。所以我們的做法是先數:規範說有幾條,抽出來就該有幾條,對不上就回去找。
明天 Day 4,把工具真的跑一次給你看:從 pip install 到看見第一個 issue,30 秒。