Skip to content

fix: [ source-risk ] 緊鄰的編號不再讓卡號整筆消失 - #108

Merged
CarlLee1983 merged 1 commit into
mainfrom
agent/issue-103-card-candidate-rescan
Aug 17, 2026
Merged

fix: [ source-risk ] 緊鄰的編號不再讓卡號整筆消失#108
CarlLee1983 merged 1 commit into
mainfrom
agent/issue-103-card-candidate-rescan

Conversation

@CarlLee1983

Copy link
Copy Markdown
Owner

Closes #103(MEDIUM-2 與 MEDIUM-3)。兩項都不是 #101 引入的,但 #101AGENTS.mdREADME.md 新增了「card candidates are Luhn-validated」的公開宣稱,宣稱的邊界該與實作對齊。

問題

PAYMENT_CARD_CANDIDATE 是貪婪的。同一行左邊緊鄰另一段數字時,兩者被吃成一段候選,Luhn 對整串失敗,finditer 從候選結尾繼續掃,裡面的卡號不會有第二次機會:

'1 4539148803436004'       候選 ['1 4539148803436004']    偵測到:否
'ID 88 4539148803436004'   候選 ['88 4539148803436004']   偵測到:否
'| 1 | 4539148803436004 |' 候選 ['4539148803436004']      偵測到:是

Markdown 表格因為有 | 分隔而倖存,純文字編號清單整筆漏掉。漏的不是誤報而是真卡號,而且兩個閘都漏:inspect-source-riskSR-PAYMENT-CARD,以及 feedback 治理持久化的 privacy gate。

做法

候選不再等於一個卡號。iter_payment_card_numbers 逐段候選再切窗,切點只落在數字群邊界,取最長的合格窗並回報那個窗的位置。切點不進數字群內部是刻意的——從一串數字中間切開會憑空造出卡號,而不是找出寫在來源裡的那一個;代價是 9994539148803436004 這種沒有分隔符直接黏上去的形狀仍然掃不到,寫進了註解與 AGENTS.md,免得下一個人當成 bug 重報。

Luhn 改成兩條奇偶前綴和:一個數字要不要加倍只取決於它自己的位置與窗尾的奇偶,不取決於窗從哪裡開始,所以每個窗變成一次減法。5 MiB 純數字對抗輸入從 6 秒級降到 1.3 秒(原本 0.3 秒);候選最多 19 位數,所以每段候選的窗數是常數。is_payment_card_number 一併刪除而非保留——它的語意正是「一段候選一個卡號」,留著就是留一個好用錯的入口,現在唯一入口是 iter_payment_card_numbers

測試卡號豁免改成比對窗內含有公告號碼。切窗取最長的合格窗,88 4111… 這種形狀約十分之一會湊出一個更長、不在清單上的合格窗;只認相等的話,一份只是引用公告號碼的付款文件會拿到警告,而那正是豁免存在的理由。

MEDIUM-3:rule_id 字串特例換成資料

_RULES 每一條帶一個掃描函式,BOM 豁免(_is_not_leading_bom)與卡號政策(_payment_card_scanner)各自是具名函式。rule_id 打錯一個字母原本會靜默變成不過濾,而同一 rule_id 已有對應多個樣式的先例。BOM 豁免原本沒有任何測試,一併補上。掃描函式回傳位移而非 re.Match,因為卡號的命中在候選內部,沒有對應的 match 物件。

遷移

RULESET_VERSION 3 → 4。偵測結果變了,不升版的話舊 audit 會以「does not match deterministic inspection」失敗,升了才講得出成因與下一步。兩份 README 補上那個下一步(重跑 inspect-source-riskassess-sources 重建 source-quality 包)。十三份 gitignored 的 benchmark source-quality 包已重建(stripe-basic-rest--max-bytes 20971520,它原本就是這樣產的)。

接受的代價

多段數字的來源上誤報率大約加倍(一段 9.9% → 9.9%、四段 10.0% → 20.7%),那是嘗試多個窗的必然結果。可見後果有兩個:高密度訂單編號文件會更快用掉 500 筆的 warning 額度(SR-WARNINGS-TRUNCATED),治理端也會更常拒絕含分段參考編號的 case body。兩者都是 fail-closed 方向。

Test plan

  • uv run pytest → 2303 passed
  • uv run ruff check . → clean
  • npm run docs:check → clean

新增 3 個 CLI 測試:緊鄰編號的卡號(含 locator 欄位)、緊鄰編號的公告測試卡號不報、BOM 只豁免檔案開頭而非文件中間;tests/foundry/test_feedback.py 的 free-text PII 參數化加上同一形狀,釘住治理端。切窗的正確性另外用兩萬筆隨機數字串的全窗暴力對照驗過(0 不一致),偵測結果對舊實作是嚴格超集。

文件

AGENTS.mdprivacy.pysource_risk/ 兩列)、README.mdREADME.en.md、兩份 operator manual 都改了對應敘述。

`PAYMENT_CARD_CANDIDATE` 是貪婪的:同一行左邊緊鄰另一段數字時,兩者被吃成
一段候選,Luhn 對整串失敗,`finditer` 從候選結尾繼續掃,裡面的卡號不會有
第二次機會。`| 1 | 4539… |` 因為表格分隔而倖存,純文字編號清單則整筆漏掉。
漏的不是誤報而是真卡號,兩個閘都漏:`inspect-source-risk` 的 SR-PAYMENT-CARD
與治理持久化的 privacy gate。#101 在 AGENTS.md 與 README 新增了「card
candidates are Luhn-validated」的公開宣稱,宣稱的邊界要與實作對齊。

候選不再等於一個卡號。`iter_payment_card_numbers` 逐段候選再切窗,切點只落
在數字群邊界,取最長的合格窗並回報那個窗的位置。切點不進數字群內部是刻意
的:從一串數字中間切開會憑空造出卡號,而不是找出寫在來源裡的那一個——代價
是沒有分隔符直接黏上去的形狀仍然掃不到,寫進了註解與 AGENTS.md。

Luhn 改成兩條奇偶前綴和。一個數字要不要加倍只取決於它自己的位置與窗尾的
奇偶,不取決於窗從哪裡開始,所以每個窗變成一次減法;沒有這個等式,切窗在
5 MiB 的純數字對抗輸入上是 6 秒級,有了是 1.3 秒(原本 0.3 秒)。整段候選
上限 19 位數,所以每段候選的窗數是常數。`is_payment_card_number` 隨之刪除
而非保留:它的語意正是「一段候選一個卡號」,留著就是留一個好用錯的入口。

測試卡號豁免改成比對窗內含有公告號碼。切窗取最長的合格窗,`88 4111…` 這種
形狀約十分之一會湊出一個更長、不在清單上的合格窗,只認相等的話,一份只是
引用公告號碼的付款文件會拿到警告——豁免存在的理由正是不產生那種警告。

規則的判準從通用迴圈裡的 rule_id 字串分支變成資料(#103 MEDIUM-3)。`_RULES`
每一條帶一個掃描函式,BOM 豁免與卡號政策各自是具名函式:rule_id 打錯一個
字母原本會靜默變成不過濾,而同一 rule_id 已有對應多個樣式的先例。BOM 豁免
原本沒有任何測試,一併補上。函式回傳位移而非 `re.Match`,因為卡號的命中在
候選內部,沒有對應的 match 物件。

`RULESET_VERSION` 3 → 4。偵測結果變了,不升版的話舊 audit 會以「does not
match deterministic inspection」失敗,升了才講得出成因與下一步;README 兩份
補上那個下一步(重跑 inspect-source-risk 與 assess-sources)。十三份 gitignored
的 benchmark source-quality 包已重建。

多段數字的來源上誤報率大約加倍(四段 10% → 21%),那是嘗試多個窗的必然代價:
高密度訂單編號文件會更快用掉 500 筆的 warning 額度,治理端也會更常拒絕含
分段參考編號的 case body。兩者都是 fail-closed 方向。

Closes #103
@CarlLee1983
CarlLee1983 merged commit d315a3f into main Aug 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

來源受理 #101 後續:source-risk 的兩項既有行為

1 participant