iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Modern Web

前端不寫 Python,照樣 ship 一把網頁無障礙 CLI系列 第 23

Day 23:我沒教 AI 怎麼修,我教它怎麼證明修好了

  • 分享至 

  • xImage
  •  

Day 23 · W4 · AI 線 · 難度 ★★★☆☆

本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01

Day 22 發出去那天下午,站上就動手了。我原本以為是一行 CSS:搜尋框的焦點外框換個顏色。晚上看檔案的修改時間,14:43 到 23:32,動了 27 個檔。

改了 27 個檔之後,那 33 個 fail 修好了嗎?有沒有順手弄壞別的?「應該沒有」不算答案。

一句話主軸:我出貨給 agent 的不是修法,是一份證明修好了的協定:修之前掃一次、修之後掃一次,兩份報告用三元組比對,分成 fixed、still、new 三桶。

前面四篇我都手動做過同一件事

Day 04 把門檻從 16 改成 20 再掃一次;Day 11 修完偽陽性回頭確認 fail 數沒變;Day 13 補完規則重掃;Day 19 修完對六頁重跑。四次都是同一個動作:改之前留一份,改之後再跑一份,看差在哪。

那是我的習慣。agent 沒有習慣,只有指令。你跟它說「修好了,再檢查一次」,它會重掃,然後把新報告唸給你聽,跟舊的沒有對照。30 頁的報告有兩百多筆,兩份並排用眼睛對,對不出來。

所以那個動作被寫成協定,放在隨 package 出貨的 skill 檔裡。Day 20 講過那份檔案的六條「不准」,今天講它的另一節。

協定原文

1. Baseline    scan --format json -o .a11y-moda/reports/<TS>-baseline.json
2. 使用者改     改 code、部署、重啟 dev server
3. Re-scan     同一道指令,存成 <NEW_TS>-after.json
4. 讀兩份       都用 Read,不靠 jq / diff(Windows 使用者通常兩個都沒有)
5. 比對         從 .issues 抽 (rule_id, snippet, status) 三元組,排序
               before − after = fixed;after − before = new;兩邊都在 = still
6. 呈現三桶     fixed / still / new,有 new 要放最前面

第二步只寫「改」,沒寫怎麼改、也沒規定誰改。skill 對修法只管兩句:fail 可以建議一句修法,caveat 連建議都不准(Day 20 那條)。剩下的它不管,agent 自己動手改也行,人改也行。協定管的是第一步跟第三步之後:修之前留一份、修之後再跑一份,把「修好了」從一句話變成一份可以逐條核對的清單。

第三步的「同一道指令」不是靠記憶。每次掃描完,工具會把這次的範圍、網址、等級、旗標寫進一個小檔案,重掃時 agent 讀那個檔案還原參數,而不是自己重新猜一組。skill 裡另外規定那個檔案只能當純文字讀、每個值要對格式,不准直接丟給 shell 執行,因為它是上一次跑出來的產物,不是使用者剛打的字。

第四步「不靠 jq / diff」是替 Windows 使用者留的。兩份 JSON 由 agent 自己讀進來、在腦子裡比,機器上不用多裝任何東西。

這像搬家前後各拍一張照片。少了什麼、多了什麼,照片說了算,不是搬家工人說了算,不管搬的是人還是機器。

身分是三元組,不是行號

diff 要先決定「同一個問題」怎麼認。最直覺的是行號,而行號最不能用:改了一行 CSS,整份 HTML 的行號都可能位移,diff 會把每一條都當成新的。

協定用的是 (rule_id, snippet, status):哪一條規則、報告裡那段原始 HTML 片段、判定是 fail 還是 info。三個都一樣,就是同一個問題還在;只出現在修之前,是修掉了;只出現在修之後,是新的。

status 也在鍵裡,是刻意的。同一個元素從 fail 降成 info,會同時出現在 fixed 跟 new 兩桶,看起來像多報了一次,但那正是你想看到的:它沒有消失,是換了一種判定,值得知道。

拿昨天的例子,baseline 裡有 30 筆長這樣:

("CS3241300E", "input#site-search-input outline=rgba(10, 71, 176, 0.25)", "fail")

外框換成實色之後,after 裡找不到這個三元組,它就進了 fixed。不需要知道是哪一行 CSS 改的,也不需要知道改成什麼顏色。

協定流程圖:左邊是修之前的 baseline 報告,中間是改 code(誰改都行,協定不管這一步),右邊是修之後的 after 報告;兩份報告各抽出 (rule_id, snippet, status) 三元組後比對,分成三桶:只在 baseline 的是 fixed,只在 after 的是 new,兩邊都有的是 still;new 那一桶標示要放最前面;下方註明 fail 才建議修法、caveat 不准

身分用三元組,不用行號。位移不會製造假的差異,但 snippet 本身變了會。

這個選擇有代價。snippet 是原始 HTML 片段,修法如果改到那段 HTML 本身,同一個問題會同時出現在 fixed 跟 new 兩桶。所以三桶不是判決,是給人看的清單,new 那一桶尤其要人看。

一定要報 new

只報「修好幾條」,讀的人會以為進度是單向的。5 月 21 日那天的紀錄就是反例。

那天工具剛加了兩條規則,本機掃 30 頁掉出 39 個 fail,其中 8 個是文字對比。修法是動色票。20 分鐘後再掃:fixed 39、new 4、still 192。新的那 4 個是兩頁裡的 codepre 區塊,色票一動,它們的前景跟背景變成 230 對 245,原本沒問題的元素變成不合格。再修一輪,23:19 那份:fixed 4、new 0。

如果那晚只看「fail 從 39 變 0」,是看不到中間那一步的,因為總數確實變 0 了。三桶把它攤開:修掉 39,弄壞 4,再修掉 4。

隔天 5 月 22 日再來一輪,26 個焦點可見的 fail,修完 fixed 26、new 0。乾淨的 diff 長這樣,沒有故事,一行就講完。

昨天的 33 個

回到開頭。9 月 6 日早上那份 30 頁報告是 baseline,33 個 fail。站上改了 27 個檔:搜尋框外框換實色、全域焦點改成雙色環、輪播圓點放大到 24px、sitemap 頁、頁尾、頁首選單、外部連結元件、聯絡表單的 API。今天早上用同一道指令再掃 30 頁,跑三桶:

三桶結果表:五次真實比對各一列,欄位是 fixed、new、still 其中仍為 fail 的數量與備註。5 月 21 日 22:20 到 22:40:fixed 39、new 4、still 192、fail 0,備註色票動了 code 與 pre 變新 fail;5 月 21 日 22:40 到 23:19:fixed 4、new 0、still 192;5 月 22 日:fixed 26、new 0、still 217;8 月 17 日單頁:fixed 1、new 0、still 8、fail 仍在 1,備註 nextjs-portal 是 dev server 塞的;9 月 6 日到 9 月 7 日正式站 30 頁:fixed 36、new 0、still 216、fail 0,備註 33 個 fail 加 3 個 info,改了 27 個檔

動了 27 個檔,new 是 0。這句話是 diff 說的,不是我猜的。

總數這邊:fail 33 變 0,info 179 變 176,caveat 40 還是 40。逐頁看,30 頁每一頁至少 fixed 1;首頁 3 個(搜尋框、輪播圓點、標題跳級),部落格列表 3 個(搜尋框加被頁首蓋住的分頁面板,兩條規則各一)。

fixed 是 36,不是 33。多出來的 3 個是沒人要求修的 info:首頁標題從 h2 直接跳到 h6 那一條(Day 22 提過),還有兩頁「同一個連結在不同頁面用不同文字」。改頁尾跟 sitemap 頁的時候順手收掉了。沒有三桶,這 3 個會安靜地消失,沒人知道它們是被誰修好的。

33 個 fail 也有一個細節是這次才看清楚的:29 頁是頁首搜尋框的外框,剩下 1 頁是搜尋結果頁的送出鍵,外框是另一個顏色。同一條規則、同樣 30 頁每頁一個,元素卻不是同一個。逐頁的三元組把它分開了,總數看不出來。

三桶也有它不告訴我的事:那 27 個檔裡,哪幾個跟這 36 筆有關,它不知道。它只認報告,不認 code。要對回去,還是得人開檔案。

沒收斂的那一次

8 月 17 日,系列開賽當天,我對本機的第一篇文章頁跑過一輪:baseline、改、after。結果 fixed 1、new 0、still 8,而 still 裡有一個 fail 沒掉。

那個元素叫 nextjs-portal,是開發伺服器自己塞進頁面的除錯層,正式站上沒有這個東西。修了兩輪它都在,因為它不在我的 code 裡。這是三桶會誠實報出來、但需要人判讀的情況:still 不一定是沒修好,也可能是量的環境本身帶了東西。處理方式是換環境再量,昨天那輪就是直接掃正式站,這個元素不存在,也就不會出現。

協定裡還有一條類似的提醒:baseline 跟 after 要用同一個 --spec。115.11 退場的檢測碼在新基準下不會出現,如果一份用舊基準、一份用新基準,那些碼會被算進 fixed,而其實什麼都沒修。diff 不會說謊,但兩份輸入的條件不同時,它會很誠實地算出一個錯的答案。

自己跑一遍

兩道指令加一段比對:

a11y-moda site https://example.com --level AA --render --format json -o .a11y-moda/reports/20260906-baseline.json
# 改 code、部署
a11y-moda site https://example.com --level AA --render --format json -o .a11y-moda/reports/20260907-after.json
import json
def keys(path):
    r = json.load(open(path, encoding="utf-8"))
    return {(p["url"], i["rule_id"], i.get("snippet") or "", i["status"])
            for p in r["pages"] for i in p["issues"]}
a, b = keys("20260906-baseline.json"), keys("20260907-after.json")
print("fixed", len(a - b), "new", len(b - a), "still", len(a & b))

site 模式多了一個頁面維度,所以鍵裡加了 url;單頁掃描不用。輸出檔固定放在 .a11y-moda/reports/,是 Day 20 第六條「不准在別人的 repo 留一堆檔案」的延伸,也讓下一次跑的 agent 找得到上一次的 baseline。

讀輸出的順序跟協定一樣:先看 new 有沒有東西,有就一條一條對 snippet;再看 still 裡有沒有 fail,有就分清楚是沒修好還是環境帶的;最後才看 fixed,那一桶通常是最長的,也最不需要看。

今天的重點

  • 協定不教修,管修的前後:誰改 code 都行,agent 自己改也行;它規定的是修之前一份、修之後一份,三元組比對。
  • 身分用 (rule_id, snippet, status),不用行號:行號會位移;代價是 snippet 一改會同時進 fixed 跟 new,所以清單要人看。
  • new 那一桶不能省:5 月 21 日修對比動了色票,修掉 39 個、弄壞 4 個,總數看不出來。
  • 動 27 個檔、new 是 0,是 diff 說的:fixed 36 裡有 3 個是沒人要求修的 info,順手收掉了。
  • still 不一定是沒修好:8 月 17 日那個沒掉的 fail 是開發伺服器塞的元素;兩份報告條件要一樣,不然 diff 會誠實地算錯。

明天 Day 24:修好了,換一把尺。把瀏覽器縮到 320px,我的網站有幾頁會橫著捲。


上一篇
Day 22:同一份 CSS,五月的工具沒話說,九月的工具報了一整排 fail
下一篇
Day 24:把瀏覽器縮到 320px,我的網站有幾頁會橫著捲
系列文
前端不寫 Python,照樣 ship 一把網頁無障礙 CLI24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言