Skip to content

feat(source-facts): name the unclosed fence instead of failing silently (closes #82) - #88

Merged
CarlLee1983 merged 2 commits into
mainfrom
agent/issue-82-unclosed-fence
Aug 16, 2026
Merged

feat(source-facts): name the unclosed fence instead of failing silently (closes #82)#88
CarlLee1983 merged 2 commits into
mainfrom
agent/issue-82-unclosed-fence

Conversation

@CarlLee1983

Copy link
Copy Markdown
Owner

Closes #82。基於 main(不是 #87 的分支)——兩者檔案不重疊。

決定:維持嚴格判定,把失效變可見

#82 列了兩個方向。選方向 2(偵測而非猜測),理由記在新的 ADR 0008

放寬「關閉行不得帶 info string」會在最貴的那個案例上出錯:兩個相鄰的開啟圍籬(JSON request 範例接 JSON response 範例,其中一個關閉行多帶了 info string)會被讀成一開一關,夾在中間的範例因此落在圍籬之外、變成來源事實。假事實在 fail-closed 閘門下會擋掉正確的擷取——這個專案一貫拒絕承擔的那種傷害(ADR 0007)。

「只在嚴格掃描掃到檔尾仍在圍籬內時才寬容重掃」這個折衷也寫進 ADR 的 Considered options 並被否決:來源真的被截斷時同樣會掃到檔尾仍在圍籬內,而那正是寬容重掃會把半截範例讀成散文的時候。

改了什麼

  • SourceFacts.unclosed_fence_line 記下未關閉的圍籬開在第幾行。
  • 涵蓋投影(FactCoverage)帶著這個純量——validate 仍然不認識來源事實的內部結構。
  • SOURCE_FACTS_UNSCANNED 的訊息在成因已知時直接點名行號與常見寫法(以 ```json 結尾),不再列出三種可能讓 operator 自己猜;verify-extraction 的預告同步。

量測

十三個 benchmark case、320 個圍籬行:零個 info-string 關閉、零份文件掃到檔尾仍在圍籬內。這是把便宜的揭露做在前面,不是在滅火——也因此 benchmark 期望值沒有任何變動。

兩套掃描器的分歧

markdown_drafts/markdown.py 是寬容的(任何以標記開頭的行都算關閉),刻意不動:它的產物是給人審的非權威草稿,外洩一段範例的代價是審閱者多看一眼;source_facts 餵的是 fail-closed 閘門,同樣的外洩代價是一份正確的擷取。分歧記在 ADR 0008 的 Consequences,收斂與否留給 #83

Test plan

  • 掃描器:未關閉時記下行號、全部關閉時為 None(tests/source_facts/test_markdown.py)
  • 警告:成因已知時點名行號、未知時仍列出三種可能(tests/validate/test_fact_coverage.py,打 validate_outputs)
  • CLI:預告點名行號且 exit code 不變(tests/test_cli_verify_extraction_fact_coverage.py)
  • uv run pytest 2180 passed(連跑三次,含兩次隨機順序)
  • uv run python scripts/quality_gate.py --strict-local PASS、npm run docs:check PASS

…ailing silently

關閉行帶 info string(```json)依 CommonMark 不算關閉,所以掃描器從那一行起把整份
文件當成圍籬內容:掃出零筆、沒有任何錯誤,與「這份來源本來就沒結構」完全無法區分。

判定維持嚴格(ADR 0008)。放寬會在最貴的那個案例上出錯——兩個相鄰的開啟圍籬會被
讀成一開一關,夾在中間的程式碼範例因此變成來源事實,而假事實在 fail-closed 閘門下
會擋掉正確的擷取。改的是可見性:SourceFacts 記下未關閉的圍籬開在第幾行,涵蓋投影
帶著這個純量,SOURCE_FACTS_UNSCANNED 的訊息與 verify-extraction 的預告直接點名行號
——成因已知時就不該叫 operator 從三種可能裡自己猜。

量測:十三個 benchmark case 掃過 320 個圍籬行,零個 info-string 關閉、零份文件掃到
檔尾仍在圍籬內。這是把便宜的揭露做在前面,不是在滅火。

markdown_drafts 那套掃描器是寬容的(任何以標記開頭的行都算關閉),刻意不動:它的
產物是給人審的非權威草稿,外洩一段範例的代價是審閱者多看一眼;source_facts 餵的是
fail-closed 閘門,同樣的外洩代價是一份正確的擷取。分歧記在 ADR 0008,收斂與否留給
掃描器分歧那張票。
…m real loss

code review 的 HIGH 成立:揭露只在事實數為零時才發,所以「前半讀得好好的、圍籬之後
全部沒讀到」這種來源完全不會浮現——比全篇未讀更危險,因為閘門判了前半、報告是乾淨的。
而它一旦開口(零匹配那條分支)還會叫 operator 去查一個並不存在的 extraction 缺陷。
改法:成因已知時壓過其他措辭,判準也從「零匹配」放寬成「零匹配或有未讀的尾巴」。

回報條件同時收緊成更誠實的一條:只有在掃描器看見「長得像關閉、卻不算關閉」的行時
才主張有內容沒被讀到。圍籬單純沒有收尾不算——CommonMark 在檔尾關閉未終止的圍籬,
那份文件在每個讀者眼中都一樣,沒有任何內容因為我們的判定而遺失,回報只會叫人去修
一個不存在的缺陷,而「修好」之後事實數仍然是零。

其餘 review 項目:ADR 0008 改寫成程式碼實際實作的規則(含 EOF 圍籬與半讀來源兩段);
兩份 operator manual 的新段落原本插進了兩項並列的中間、把句子切斷,改成自己的段落;
AGENTS.md 補上 validate 那列與 correction 分類那列(ADR 0008 的 falsification 點名
fact_coverage.py,那列正是它的邊界文件);fence_line 的 0 哨兵改成 None;
測試補上 tilde 圍籬、先關後開、EOF 圍籬、半讀來源與非 Markdown 來源的投影分支,
並拿掉一個用 or 串起來、少了行號也會通過的斷言。
@CarlLee1983

Copy link
Copy Markdown
Owner Author

Code review 的 HIGH 與兩個 MEDIUM 都成立,已修正並推上 6533ede

HIGH — 半讀的來源仍然沉默,開口時還指錯方向。 揭露原本掛在 facts == 0 下,而 unscanned_sources 只列 matched == 0,所以「前半讀得好好的、圍籬之後全部沒讀到」這種來源完全不會浮現——它比全篇未讀更危險,因為閘門判了前半、報告是乾淨的。而它一旦落到零匹配那條分支,訊息還會叫 operator 去查一個並不存在的 extraction 缺陷。改法:成因已知時壓過其他措辭,判準放寬成「零匹配有未讀的尾巴」,_forecast 用同一個述詞,兩邊不可能漂移。

MEDIUM — EOF 圍籬被當成有損失的缺陷。 這條讓我把回報條件整個換掉,換成比 review 建議的更誠實的一條:只有在掃描器看見「長得像關閉、卻不算關閉」的那一行時才回報。圍籬單純沒有收尾不算——CommonMark 在檔尾關閉未終止的圍籬,那份文件在每個讀者眼中都一樣,沒有任何內容因為我們的嚴格判定而遺失;回報只會叫人去修一個不存在的缺陷,而且「修好」之後事實數仍然是零。review 建議的「其後沒有非空白行才不報」會漏掉這一點:未終止的最後一段範例後面通常還有範例內容。

MEDIUM — 手冊插句切斷了並列句。 兩份都改成自己的段落,並補上「即使前半有事實對上也照樣回報」與「單純沒收尾不在此列」兩件事。

LOW 全部採納:AGENTS.md 補上 validate 那列與 correction 分類那列(ADR 0008 的 falsification 點名 fact_coverage.py,那列正是它的邊界文件);fence_line0 哨兵改成 None;拿掉那個用 or 串起來、少了行號也會通過的斷言;測試補上 tilde 圍籬、先關後開、EOF 圍籬、半讀來源,以及 build_fact_coverage 的非 Markdown 來源分支。

ADR 0008 改寫成程式碼實際實作的規則(新增「只在有被拒絕的關閉行時才記錄」與「半讀來源同樣回報」兩段,falsification 條件同步)。

uv run pytest 2186 passed、--strict-local PASS、docs:check PASS。

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.

圍籬關閉行帶 info string 會讓整份文件從該點起被當成在圍籬內

1 participant