feat(collector): daemon 產出改成規範形 wiki 卡——1 文件卡+N 原子卡落 .wiki/(InkStoneCo#44 ④)
LLM 只回 JSON 判斷,格式/落點/連結閉合/索引/manifest 全由 wikishape.go 機械組裝: - 卡形=frontmatter(tags/gloss/created/updated)+← 上層+摘要/重點/實體(帶類型)/ 關聯(內文知識關係/卡片關係/出處)——差距表 #6 - 落點 <節點>/.wiki/、檔名=H1(.wiki 隱藏目錄自身即機器標記,machinemark 例外③)——#7 - 00-INDEX 機械維護:每行五樣照抄 frontmatter;每份原稿必列——#8 - 萃取端就切 1 hub+N 原子卡(gemma 路 prompt 改 JSON 契約)——#9 - 無可萃概念標「空」+理由,上索引不產卡——#10 機械保證對著 wiki-lint.py 寫:斷連結拆殼、index 式句改寫、三元組恰三項、 雙向邊自動補反向、佔用不覆蓋、原稿永不動;同名概念跨文件先消歧(merge 歸第⑤環)。 scan/convert 白名單補 .feature/.yaml/.yml/.org/.rst(規範洞 6); lint.go 新舊格式雙軌(workers-ai 雲端 prompt 屬 matrix/arcrun 核心,待開票); direct.go 只送文件卡上雲(takedown 配對鍵不變)、下架收走 .wiki 產物。 實測:真資料夾+真 Gemini → 2 節點 12 卡,wiki-lint 17/17 exit 0, 原稿 sha256 前後不變;go test collector+supervisor+arcrun-app 全綠(新增 9 案)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+28
-28
@@ -12,30 +12,26 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// cardFixture 組出一張 B2 合格四段卡(H1 段名齊全且順序正確+H2 一句話定義恰一行+
|
||||
// H3 3–12 條要點+H4 3–8 條端點閉合三元組——2026-08-09 併入品質 lint 後,minimal
|
||||
// 兩段卡會被 H1 硬擋,這裡改成全段合格卡,讓所有沿用 cardFixture 的既有測試自動過關)。
|
||||
// 三元組的分隔符在原始碼裡用組字串的方式產生,避免被 arcrun 意圖 guard 誤判成工作流的邊。
|
||||
// cardFixture 組出一份合格的萃取 JSON(InkStoneCo#44 ④:gemmaPrompt 的契約改為
|
||||
// 「文件總覽+N 個原子概念」的 JSON,卡片格式由 wikishape.go 機械組裝)。
|
||||
// 概念名固定為「<subject>·概念」——避免與各測試原稿的 H1/頁名撞名。
|
||||
func cardFixture(subject, object string) string {
|
||||
sep := strings.Repeat(">", 2)
|
||||
return "# " + subject + "\n" +
|
||||
"## 一句話定義\n" + subject + "是一張測試用的自包含知識卡。\n" +
|
||||
"## 要點\n" +
|
||||
"- 第一個要點含具體條件\n" +
|
||||
"- 第二個要點含具體條件\n" +
|
||||
"- 第三個要點含具體條件\n" +
|
||||
"## 關鍵實體\n" +
|
||||
"- **" + subject + "** — 測試主體\n" +
|
||||
"- **" + object + "** — 測試客體\n" +
|
||||
"## 關聯\n" +
|
||||
"- " + subject + " " + sep + " 屬於 " + sep + " " + object + "\n" +
|
||||
"- " + subject + " " + sep + " 關聯到 " + sep + " " + object + "\n" +
|
||||
"- " + object + " " + sep + " 對應 " + sep + " " + subject + "\n"
|
||||
concept := subject + "·概念"
|
||||
return `{"gloss":"` + subject + `的測試用一句話","tags":["測試"],` +
|
||||
`"summary":"這是測試用的文件摘要,交代 ` + subject + ` 與 ` + object + ` 的關係。",` +
|
||||
`"points":["本文的核心判斷落在 [[` + concept + `]] 上,其餘是背景"],` +
|
||||
`"no_concept":false,"reason":"",` +
|
||||
`"concepts":[{"name":"` + concept + `","gloss":"一句話說明這個概念",` +
|
||||
`"tags":["測試"],"summary":"概念層的摘要,說明它離開原稿也能獨立成立。",` +
|
||||
`"points":["第一個判斷句含具體條件"],` +
|
||||
`"entities":[{"name":"` + subject + `","type":"概念","desc":"測試主體"},` +
|
||||
`{"name":"` + object + `","type":"組織","desc":"測試客體"}],` +
|
||||
`"facts":[["` + subject + `","屬於","` + object + `"]],` +
|
||||
`"relations":[]}]}`
|
||||
}
|
||||
|
||||
// gemmaCardStub 讓 Gemini 替身回傳一張以 pageName 命名的卡片(t176 起產品只走 gemma 路,
|
||||
// gemmaCardStub 讓 Gemini 替身回傳一份萃取 JSON(t176 起產品只走 gemma 路,
|
||||
// 測試也跟著走真實路徑——不再用 claude stub,否則測的是產品走不到的分支=假綠)。
|
||||
// 卡片內容照 gemmaPrompt 的契約:含「## 一句話定義」與「## 關聯」。
|
||||
func gemmaCardStub(t *testing.T, cardBody string) func() {
|
||||
t.Helper()
|
||||
return gemmaStub(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -51,7 +47,8 @@ func gemmaCardStub(t *testing.T, cardBody string) func() {
|
||||
// → 原文從未離開本機 → manifest 標 ingested(下一輪不重送)。
|
||||
func TestDirectExtractorModeE2E(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "報銷規則.md"), []byte("# 原稿機密內容 XYZZY"), 0o644); err != nil {
|
||||
// H1=卡名(規範洞 1);機密哨兵放內文,驗「原文不出機」看的是內容不是標題。
|
||||
if err := os.WriteFile(filepath.Join(root, "報銷規則.md"), []byte("# 報銷規則\n\n機密內容 XYZZY"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -92,9 +89,11 @@ func TestDirectExtractorModeE2E(t *testing.T) {
|
||||
if len(fileResults) != 1 || fileResults[0].Status != "ingested" {
|
||||
t.Fatalf("results=%+v", fileResults)
|
||||
}
|
||||
// 卡片落地本地(用戶看得到自己的 wiki)
|
||||
if _, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "cards", "arcrun-報銷規則.md")); err != nil {
|
||||
t.Fatalf("卡片未落地:%v", err)
|
||||
// 卡片落地本地 `.wiki/`(InkStoneCo#44 ④:檔名=H1、文件卡+概念卡+索引齊備)
|
||||
for _, rel := range []string{".wiki/報銷規則.md", ".wiki/報銷規則·概念.md", ".wiki/00-INDEX.md", ".wiki/manifest.json"} {
|
||||
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(rel))); err != nil {
|
||||
t.Fatalf("wiki 產物未落地 %s:%v", rel, err)
|
||||
}
|
||||
}
|
||||
// 上雲的是「總覽卡+內容卡」兩張,都不含原文
|
||||
if len(posted) != 2 {
|
||||
@@ -115,8 +114,8 @@ func TestDirectExtractorModeE2E(t *testing.T) {
|
||||
t.Fatal("找不到內容卡")
|
||||
}
|
||||
cc, _ := contentCard["card_content"].(string)
|
||||
if !strings.Contains(cc, "## 一句話定義") {
|
||||
t.Fatalf("card_content 不是卡片:%.80s", cc)
|
||||
if !strings.Contains(cc, "## 摘要") || !strings.Contains(cc, "gloss:") {
|
||||
t.Fatalf("card_content 不是規範形卡片:%.80s", cc)
|
||||
}
|
||||
// path 必須是「原檔路徑」(takedown 比對鍵+B4 溯源)——不是卡片路徑(07-24 第五枚坑)
|
||||
if p, _ := contentCard["path"].(string); p != "報銷規則.md" {
|
||||
@@ -173,7 +172,7 @@ func TestDirectExtractorRemovedClearsLocalCard(t *testing.T) {
|
||||
if _, exit, _ := RunDirectOnce(cfg, false); exit != 0 {
|
||||
t.Fatalf("第一輪 ingest 失敗 exit=%d", exit)
|
||||
}
|
||||
cardPath := filepath.Join(root, "system-dev", "wiki", "cards", "arcrun-報銷規則.md")
|
||||
cardPath := filepath.Join(root, ".wiki", "原稿.md") // 原稿內容「# 原稿」⇒ H1=卡名
|
||||
if _, err := os.Stat(cardPath); err != nil {
|
||||
t.Fatalf("前置失敗:卡片未落地 %v", err)
|
||||
}
|
||||
@@ -225,7 +224,8 @@ func TestDirectExtractorRemovedNoLocalCardOK(t *testing.T) {
|
||||
t.Fatal("第一輪失敗")
|
||||
}
|
||||
// 模擬用戶已手動清走本地卡 → removed 分支「存在才刪」不應報錯或多出 warning
|
||||
if err := os.Remove(filepath.Join(root, "system-dev", "wiki", "cards", "arcrun-a.md")); err != nil {
|
||||
// (原稿內容 "x" 無 H1 ⇒ 文件卡名 fallback=檔名 "a")
|
||||
if err := os.Remove(filepath.Join(root, ".wiki", "a.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(root, "a.md")); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user