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:
2026-08-15 21:03:45 +08:00
parent f233eec9c2
commit 12c2d41886
13 changed files with 1442 additions and 134 deletions
+23 -38
View File
@@ -47,12 +47,12 @@ import (
"testing"
)
// gemmaEchoStub=會**看請求是哪一頁**、回傳對應那一頁合格卡片的 Gemini 替身。
// gemmaEchoStub=會**看請求是哪一頁**、回傳對應那一頁合格萃取 JSON 的 Gemini 替身。
//
// 為什麼不能沿用 gemmaCardStub(固定回同一張卡):這一支測試的 fixture 是「真的有
// 既有頁面的 vault」,一輪會萃好幾份(journals、pages、丟進去的原稿)。固定回同一張卡
// 會讓 pageName 對不上 cleanGemmaCard 的「# <頁名>」契約 ⇒ 被品質 lint 擋下 ⇒ 測試
// 綠不了,而那是替身的問題不是產品的問題。頁名從 gemmaPrompt 的「# <頁名>」裡取回來。
// 為什麼不能沿用 gemmaCardStub(固定回同一):這一支測試的 fixture 是「真的有
// 既有頁面的 vault」,一輪會萃好幾份(journals、pages、丟進去的原稿)。固定回同一
// 會讓多份文件的概念卡同名互撞(wikishape 的佔用保護會擋下⇒ 測試綠不了,
// 而那是替身的問題不是產品的問題。頁名從 gemmaPrompt 的「檔名:<頁名>」裡取回來。
func gemmaEchoStub(t *testing.T) func() {
t.Helper()
return gemmaStub(t, func(w http.ResponseWriter, r *http.Request) {
@@ -69,9 +69,9 @@ func gemmaEchoStub(t *testing.T) func() {
prompt = req.Contents[0].Parts[0].Text
}
page := "未知頁"
if i := strings.Index(prompt, "「# "); i >= 0 {
rest := prompt[i+len("「# "):]
if j := strings.Index(rest, ""); j >= 0 {
if i := strings.Index(prompt, "檔名:"); i >= 0 {
rest := prompt[i+len("檔名:"):]
if j := strings.Index(rest, ""); j >= 0 {
page = rest[:j]
}
}
@@ -324,46 +324,31 @@ func TestPlainFolderFootprint_EveryNewFileIsMarked(t *testing.T) {
}
}
// 備份檔也在網內:既有卡片被改寫時產生的 .bak-<ts>,同樣必須帶標記。
// 佔用保護也在網內:`.wiki/` 裡「不是本文件產的」同名檔案,改寫必須被擋下、
// 原檔原封不動(InkStoneCo#44 ④:塑形層以 manifest 認擁有權,佔用=報錯不覆蓋)。
//
// 這一條是「足跡測試比點測強」的最好例子——沒有人特地為備份檔寫過測試,
// 但它確確實實是 daemon 寫進使用者資料夾的檔案
func TestVaultFootprint_BackupFilesAreMarkedToo(t *testing.T) {
// 這一條是「足跡測試比點測強」的最好例子——沒有人特地為既有檔寫過測試,
// 但那正是 daemon 最不准碰的東西
func TestVaultFootprint_ForeignWikiFileNotClobbered(t *testing.T) {
root, _ := newVaultWithRealJournals(t)
srcRel := "會議記錄.md"
if err := os.WriteFile(filepath.Join(root, srcRel), []byte("# 原稿 v1"), 0o644); err != nil {
if err := os.WriteFile(filepath.Join(root, srcRel), []byte("# 會議記錄\n內文 v1"), 0o644); err != nil {
t.Fatal(err)
}
// 先放一同名舊卡(內容不同)→ 這一輪落卡會先備份它
cardDir := filepath.Join(root, filepath.FromSlash(vaultCardsRelDir))
// 先放一同名檔(不是 manifest 記載的產物)→ 這一輪落卡必須被擋
cardDir := filepath.Join(root, wikiRelDir)
mustMkdir(t, cardDir)
cardName := MarkName("會議記錄.md")
if err := os.WriteFile(filepath.Join(cardDir, cardName), []byte("# 會議記錄\n舊的內容"), 0o644); err != nil {
preexisting := "# 會議記錄\n這是先前就存在的內容"
if err := os.WriteFile(filepath.Join(cardDir, "會議記錄.md"), []byte(preexisting), 0o644); err != nil {
t.Fatal(err)
}
defer gemmaCardStub(t, cardFixture("會議記錄", "專案"))()
if _, err := ExtractWithGemma("k-test", "gemma-test", root, srcRel); err != nil {
t.Fatal(err)
if _, err := ExtractWithGemma("k-test", "gemma-test", root, srcRel); err == nil {
t.Fatal("目標被佔用時應報錯,不得無聲覆蓋")
}
entries, err := os.ReadDir(cardDir)
if err != nil {
t.Fatal(err)
}
var names []string
foundBackup := false
for _, e := range entries {
names = append(names, e.Name())
if !IsMarked(e.Name()) {
t.Fatalf("卡片目錄裡出現沒帶標記的檔案:%s(目錄內容:%v)", e.Name(), names)
}
if bakSuffix.MatchString(e.Name()) {
foundBackup = true
}
}
t.Logf("卡片目錄內容:%v", names)
if !foundBackup {
t.Fatalf("既有卡片沒有被備份就被改寫了,目錄內容:%v", names)
data, _ := os.ReadFile(filepath.Join(cardDir, "會議記錄.md"))
if string(data) != preexisting {
t.Fatalf("既有檔案被動過:%q", data)
}
}