diff --git a/AGENTS.md b/AGENTS.md index 58db05d..09c0b2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -276,6 +276,9 @@ output**)。單筆修正只治標;規則修正才治本。 語料要有跨頁共用的核心詞。便利值會變成盲點的形狀——ASCII 檔名讓 `git status` 的路徑轉義 bug 溜過,互不重疊的語料讓 BM25 的 idf 退化 bug 溜過。 新增檢查時先問:**這組測資和真實輸入差在哪?差異處就是沒被測到的地方。** +- **斷言要驗「結果可用」,不是驗「字串存在」**:`assert "![img](" in md` 只證明字串被 + 組出來,證明不了連結解得開——真正該做的是用解析器 render 後確認產出 `` + 且目標檔存在。測資照現實建了、斷言卻停在表面,bug 一樣會溜過去。 - 把任務轉成可驗證的目標,迭代到通過: - 「加驗證」→ 先寫無效輸入的測試,再讓它通過。 - 「修 bug」→ 先寫重現 bug 的測試,再讓它通過。 diff --git a/requirements.txt b/requirements.txt index a27987f..cb9040f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,5 @@ python-pptx # PowerPoint → Markdown trafilatura # 網頁快照 → Markdown 正文萃取 rank-bm25 # tools/search.py BM25(第一版不用向量庫) fastmcp # mcp/server.py 薄殼 +markdown-it-py # 自檢用:以 CommonMark 解析驗證產出的圖片連結真的能 render + # (原為 fastmcp 的傳遞依賴,明確宣告以免上游調整後自檢失效) diff --git a/tools/convert/convert.py b/tools/convert/convert.py index dcf556f..38143d5 100644 --- a/tools/convert/convert.py +++ b/tools/convert/convert.py @@ -125,7 +125,8 @@ def _vision_pdf(root, pdf_path, assets, conv_name): blocks = [] for i, (img, text) in enumerate(zip(images, texts), 1): rel = img.relative_to(md_dir).as_posix() - blocks.append(f"\n\n![page {i}]({rel})\n\n{text}") + # <> 包住:檔名含空格或不平衡括號時,未包住的連結會失效 + blocks.append(f"\n\n![page {i}](<{rel}>)\n\n{text}") return "\n\n".join(blocks), "ok", "from_vision" diff --git a/tools/convert/from_docx.py b/tools/convert/from_docx.py index c02ecb5..d284865 100644 --- a/tools/convert/from_docx.py +++ b/tools/convert/from_docx.py @@ -50,7 +50,9 @@ def _par_md(par, doc, assets_dir, md_dir, counter): rel = (assets_dir / name).relative_to(md_dir).as_posix() else: rel = name # dry-run:僅佔位 - parts.append(f"![{name}]({rel})") + # 連結目標以 <> 包住:來源檔名可能含空格或不平衡括號,兩者都會讓 + # Markdown 連結失效(圖片存在卻整行退化成純文字) + parts.append(f"![{name}](<{rel}>)") parts.append(run.text) text = "".join(parts).strip() lvl = _heading_level(par) diff --git a/tools/convert/from_pdf.py b/tools/convert/from_pdf.py index c450ff0..ffc375a 100644 --- a/tools/convert/from_pdf.py +++ b/tools/convert/from_pdf.py @@ -42,7 +42,8 @@ def convert(src, assets_dir=None, md_dir=None): assets_dir.mkdir(parents=True, exist_ok=True) pix.save(str(assets_dir / name)) rel = (assets_dir / name).relative_to(md_dir).as_posix() - blocks.append(f"![{name}]({rel})") + # <> 包住:檔名含空格或不平衡括號時,未包住的連結會失效 + blocks.append(f"![{name}](<{rel}>)") doc.close() return "\n\n".join(blocks), "ok" diff --git a/tools/convert/from_pptx.py b/tools/convert/from_pptx.py index 28ac585..ad43374 100644 --- a/tools/convert/from_pptx.py +++ b/tools/convert/from_pptx.py @@ -45,7 +45,8 @@ def convert(src, assets_dir=None, md_dir=None): rel = (assets_dir / name).relative_to(md_dir).as_posix() else: rel = name - blocks.append(f"![{name}]({rel})") + # <> 包住:檔名含空格或不平衡括號時,未包住的連結會失效 + blocks.append(f"![{name}](<{rel}>)") if slide.has_notes_slide: notes = slide.notes_slide.notes_text_frame.text.strip() if notes: diff --git a/tools/convert/selfcheck.py b/tools/convert/selfcheck.py index ad84361..bddd5fc 100644 --- a/tools/convert/selfcheck.py +++ b/tools/convert/selfcheck.py @@ -26,13 +26,39 @@ import to_image # 測資檔名一律用「中文 + 空格」——真實來源檔就長這樣,而 git status --porcelain # 對這兩者都會加引號轉義。ASCII 檔名的測資曾讓 ingest 的 preflight bug 溜過(§12)。 -DOCX, XLSX, PDF, SCAN = ("壓測 報告.docx", "測試案例 清單.xlsx", - "規格 說明.pdf", "掃描件 無文字層.pdf") +DOCX, XLSX, PDF, SCAN = ("壓測 報告(1).docx", "測試案例 清單.xlsx", + "規格 說明.pdf", "掃描件(2 期.pdf") PPTX, HTML, TXT, DOC = ("結案 簡報.pptx", "指引 快照.html", "不支援 筆記.txt", "舊版 報告.doc") +def assert_links_ok(md, base): + """每個圖片連結都要真的 render 成 、且目標檔存在。 + + 只斷言 "![" 在不在會讓損毀連結矇混過關——含空格/不平衡括號的檔名會產出 + 語法有效但解析失敗的連結,圖檔明明在磁碟上卻整行退化成純文字(§13.4)。 + """ + import re + import urllib.parse + from markdown_it import MarkdownIt + + want = md.count("![") + html = MarkdownIt("commonmark").render(md) + got = html.count("= 3, f"應至少驗到 docx/pdf/pptx 各一個圖片連結,實際 {linked}" + print(f"PASS: 圖片連結可 render 且目標存在({linked} 個,檔名含空格/括號)") + # 冪等:同檔再跑一次 → skip,manifest 不變 before = (root / "raw/manifest.json").read_text(encoding="utf-8") convert.main([str(fx / DOCX), "--root", str(root)]) @@ -228,6 +264,7 @@ def main(): assert len(conv_rel) == 1 and mv["files"][conv_rel[0]]["converter"] == "from_vision", mv md_v = (root_v / conv_rel[0]).read_text(encoding="utf-8") assert "可疑交易監控" in md_v and "(vision" in md_v and "![page 1]" in md_v, md_v + assert_links_ok(md_v, root_v / "raw/converted") # vision 頁的連結同樣要能 render print("PASS: convert.py --vision 端到端(needs_ocr → 本地 vision 轉錄)") print("ALL PASS")