iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Modern Web

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

Day 26:每一件 AI 沒做到的事,都是我以為它會自己做的

  • 分享至 

  • xImage
  •  

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

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

還剩五篇。前面二十五篇講的是工具做對了什麼、量到了什麼,今天反過來,講五件它沒做、而我以為它會自己做的事。

Day 08 說過這個專案的 Python 沒有一行是我打的。那句話的另一面是:程式裡每一個我沒說出口的假設,也沒有人替我說。五件事全在 git 紀錄裡,日期、commit、修法都查得到;每一件後來都變成一句寫給 AI 的規矩。

一句話主軸:AI 做的是你說的,不是你以為你說了的。人沒想到的事,它也不會替你想到。

五件事對照表:每列一件,欄位是我以為、實際發生、留下的規矩、日期。一,以為旗標會接到每一層,實際五個探針都沒收到 dark-mode,規矩是每一層都要驗,5 月 12 日;二,以為 AI 翻條文不會偷改範圍,實際四條規則查得比碼表少、全是假通過,規矩是描述必須逐字等於碼表原文,8 月 26 日;三,以為跳頁連結只有一種長相,實際絕對網址加片段全被判缺,規矩是判定照碼表文字不照慣例,8 月 26 日;四,以為寫死的數字會自己更新,實際版本號五版沒動、規則數四處錯,規矩是數字只能有一個來源加守門測試,8 月 26 日;五,以為 fail 數等於網站品質,實際 27 個 fail 來自第三方元件,規矩是先問數字是誰的、分流成 caveat,5 月 4 日

五件事,五句規矩。每一句都是先付過學費才寫進去的。

我以為加一個旗標,它會接到每一層

5 月初工具加了 --dark-mode,讓掃描在深色模式下量對比。旗標在 CLI 解析到了,抓 HTML 那一步也收到了,我以為事情就結束了。

5 月 12 日,同事拿人工審查回來的一條對比意見對照工具,發現工具在深色模式下應該抓到那個元素,卻什麼都沒報。追下去:單頁掃描的路徑會開五個探針,每個探針自己開一個瀏覽器,而這五個呼叫一個都沒接到旗標,全部在淺色模式下量。整站掃描走的是另一條共用瀏覽器的路,剛好有傳,所以 site 對、scan 錯。

# 修之前:五個探針各開自己的瀏覽器,旗標到不了
ctx.tab_stops = walk_tab_stops(url)
ctx.text_samples = collect_text_samples(url)
# 修之後:每一跳都明講
ctx.tab_stops = walk_tab_stops(url, color_scheme=color_scheme)
ctx.text_samples = collect_text_samples(url, color_scheme=color_scheme)

commit 裡留了一句:API 用對了,是內部的管線沒把狀態傳到下一層。同一週還有另一個同型的錯,一個 API 的第二個參數只收偽元素,傳進去的是偽類別,瀏覽器不報錯,安靜地退回預設值。

留下的規矩:加任何會影響探針畫面狀態的旗標,列一張清單,從 CLI 到掃描函式到探針到瀏覽器分頁,每一層都要驗它真的過去了。單元測試抓不到這種事,它只能對自家站真的跑一次才會露出來。Day 17 那些深色模式的數字,是這條規矩留下來之後才量得出來的。

我以為 AI 翻條文不會偷改範圍

146 條規則每一條都有一句描述,是 AI 讀碼表條文之後寫的。8 月 26 日我把每一句描述對回 115.11 的原文,四條對不起來,而且四條都是同一個方向:實作查的比條文要的少。

最明顯的一條,條文說「在所有情況中,均提供使用者在送出答覆前加以檢查及更正的能力」。實作版本把範圍縮成「法律、財務、個資等表單,而且三個欄位以上」,搜尋、登入、註冊、訂閱全部跳過。那組條件是另一條準則 3.3.4 的,不是 3.3.6 的。

# 修之前:條文沒有的條件
_SKIP_FORM_HINT_RE = re.compile(r"search|login|signup|subscribe|搜尋|登入|註冊|訂閱")
if len(inputs) < 3: continue
# 修之後:照條文,唯一的窄化是單欄搜尋框(沒有東西可以檢查更正)
_SEARCH_ONLY_RE = re.compile(r"search|搜尋|搜索|查詢")

另外三條:驗證碼那條把客服電話跟 mailto 當成「第二種驗證形式」,那是聯絡人的方式,不是另一種驗證;麵包屑那條只認麵包屑,用 aria-current 標目前位置的站直接判缺;表單錯誤那條只驗焦點跳轉,沒驗畫面上有沒有文字,Day 19 講過。

四條全是假通過:報告說沒問題,其實沒查。這是最壞的錯法,因為它不會被任何人看見。我原本以為 AI 翻譯條文最多是翻得生硬,沒想到它會把範圍翻小,而且翻小的理由聽起來都很合理。

留下的規矩:規則的描述必須是碼表原文,不准改寫;實作的範圍不得小於條文;加了一個測試把每一條的描述跟碼表逐字對,對不上就紅。

我以為跳頁連結只有一種長相

「跳到主要內容」的連結,我腦中的樣子是 <a href="#main">。規則寫的時候也是這樣想的:href# 開頭,目標存在,就過。

8 月 26 日掃一個站,53 個 fail 裡 14 個是這一條跟它的兄弟條,而那些頁面其實都有跳頁連結。台灣公部門常見的寫法長這樣:

<a href="https://example.org/#accesskey-c" class="sr-only">跳到主要內容區塊</a>
<a accesskey="C" id="accesskey-c" title="中央內容區塊">:::</a>

絕對網址加片段,不是 # 開頭;而且有些站模板寫死 http,頁面已經是 https,連網址的 scheme 都對不上。規則一個都認不出來。兄弟條更糟,條文寫的是「加入鏈結,連到內容區域的開頭」,實作找的卻是 <main> 元素,根本沒在找連結。

# 修之前
if href.startswith("#") and len(href) > 1: ...
# 修之後:把片段對回本頁網址,比 host 跟 path,不比 scheme
if find_content_skip_link(soup, url) is not None: return

這一則方向要寫對:是我們的規則錯,對方的站沒錯。修完還順手抓到一種真的壞掉的寫法,模板把跳頁連結寫死成另一頁的絕對網址,按下去會離開本頁。

留下的規矩:判定照碼表文字,不照我腦中的慣例。碼表說的是「連到內容區域的鏈結」,沒說它長什麼樣。

我以為寫死的數字會自己更新

__version__ = "0.1.0" 這一行寫在套件的入口檔裡,從第一版到 0.5.0 五個版本沒動過。每一次掃描送出去的 User-Agent 都寫著 0.1.0,站方看 log 看到的版本是錯的。規則數也一樣,四個地方寫著 133 或 129,實際 146。Day 04Day 20 各撞到一次,當時只改了字,沒改機制。

8 月 26 日改機制:

# 修之前
__version__ = "0.1.0"
# 修之後:單一來源是 pyproject.toml,從安裝的中繼資料讀
__version__ = _installed_version("a11y-moda")

規則數同理,從註冊表算,不從字串抄。然後加了兩個守門測試:任何出貨文件裡寫的規則總數必須等於註冊表的長度;版本字串必須等於 pyproject 裡宣告的。之後這兩個數字要是再錯,測試會先紅。

留下的規矩:數字只能有一個來源,其他地方從那個來源推導。這件事的諷刺在於,那份要 AI 別憑印象回答的檔案,自己憑印象寫了一個數字,Day 20 講過。

我以為 fail 數字等於網站品質

5 月 4 日掃一個嵌了第三方搜尋元件的站,30 頁,31 個 fail。第一反應是這個站做得很差。分下去,27 個 fail 全來自那個元件自己載入的 CSS,站方改不了那個檔案。

規範對這種情況有程序:WCAG 的部分符合條款,允許在申請時聲明第三方內容。所以工具該做的不是把它們算成 fail,也不是裝作沒看見,是分流:違規的資源來自不同的根網域,就降成 caveat,訊息前面標上來源,並附上聲明的路徑。

# Rule.check() 之後的一道後處理
if is_third_party(res_url, url):
    issue.status = "caveat"
    issue.message = f"[third-party: {origin}] {issue.message} 註:此違規來自第三方資源,需於申請時備註欄聲明。"

降完之後那個站剩 4 個 fail,其中 3 個是站自己的東西,原本被 27 個第三方的 fail 淹沒,沒人看見。分流不只是少報,是讓真正該修的浮出來。要看全部的人有 --strict-third-party 可以關掉。

留下的規矩:一個數字先問它是誰的。fail 數等於品質這個假設,在有第三方內容的頁面上直接失效。

五件事的共同點

五件裡沒有一件是 AI 寫錯了我說的話。旗標我沒說要傳到探針,條文我沒說不准縮,跳頁連結我沒說有幾種寫法,數字我沒說要從哪裡讀,第三方我沒說要分開算。它做了我說的,我以為我說了更多。

這像交接給一個很強的新人:交接文件沒寫的事他不會做,而且他不會問。差別在於新人做了一個月會自己補上常識,AI 每一次都從交接文件開始。所以那份文件一直在長:每一件事之後都多了一句規矩,寫進專案的 CLAUDE.md 或記憶檔,下一個 agent 開工前會先讀到。

五件事的日期也值得看一眼:兩件在 5 月,三件在 8 月 26 日同一天。那天是 Day 11 的寫稿日,為了寫偽陽性那篇,把 146 條全部對回碼表,一天挖出三件。寫這個系列本身,是這個工具今年最大的一次體檢。

時間軸:4 月 30 日專案開始,5 月 4 日第三方分流,5 月 12 日旗標串接,5 月 26 日標章核發,8 月 16 日系列開賽,8 月 26 日同一天三件(條文範圍、跳頁連結、寫死數字),9 月 10 日今天;系列區間用底色標出,三件落在區間內

三件在系列開賽之後才挖出來。寫出來給人看,比自己跑測試更會逼出問題。

剩下四篇,要收了。

今天的重點

  • 旗標要接到每一層--dark-mode 在 CLI 有、抓 HTML 有、五個探針都沒有;單元測試抓不到,要對真站跑。
  • 條文範圍不准縮:四條規則實作查的比碼表要的少,全是假通過;現在描述必須逐字等於碼表原文。
  • 判定照碼表文字,不照腦中的慣例:跳頁連結不只 #main 一種長相,我們的規則錯,對方沒錯。
  • 數字只能有一個來源:版本號跟規則數改成推導,兩個守門測試。
  • fail 數先問是誰的:第三方資源的違規分流成 caveat,本站自己的 3 個才浮出來。

明天 Day 27:工具的第 20 個版本要出門了,講發版這件事。發版清單九步,我漏的那一步是使用者第一眼看的那頁。


上一篇
Day 25:我讓 AI 先查規範再寫,差別只在它查到的那幾條
系列文
前端不寫 Python,照樣 ship 一把網頁無障礙 CLI26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言