fix(collector): #60 監看的是筆記庫底下的子資料夾時,保護整個失效
真正的根因不是「vault 判斷漏了子庫」,是**判斷的方向搞反了**: 前兩輪問的都是「監看根**這一層**是不是 vault」,但 daemon 的產物一律落在 監看根底下——這兩件事只有在「監看根 == 庫根」時才等價,而那正好是前兩輪 唯一測過、也唯一不會出事的擺法。 使用者只要把庫底下的某一層加進監看(`KB/docs`、`KB/pages`、Obsidian 庫裡的 某個專案夾——很自然的用法),DetectVaultType 就回 VaultNone,整套保護退回 一般資料夾模式,卡片落在 `<監看根>/system-dev/wiki/cards/`:那個路徑就在 使用者的 graph 裡面,而且看得見,Logseq/Obsidian 每一張卡都收編成一頁。 前綴(第二輪)只擋得住撞名,擋不住「多出一堆機器頁」。 改法:把「這一層是不是庫」與「我寫的東西會不會落進誰的庫」拆成兩個判準。 - vault.go:新增 DetectVaultContext(往上找到最近的庫根)與 VaultDirUnder (往下擋:寫入目標會不會踩進子庫)。DetectVaultType 一字未改,繼續與 install.sh 對齊——往上找用較嚴的判準(logseq/ 要有 config.edn 或 journals//pages/ 佐證),因為那是替使用者猜、而且一次猜好幾層。 停在家目錄與檔案系統根,避免 `~/logseq` 這種常見資料夾把整個家目錄判成庫。 - extract.go:cardsRelDirFor 改用 DetectVaultContext。 - safewrite.go:落卡前過 ensureWritable 機械閘——目標踩進子庫就中止, 不靜靜寫進去。今天不會觸發,它防的是以後新增的寫檔點。 - tidy.go:收拾判準從「有沒有帶標記」擴充成「位置對不對 + 有沒有帶標記」, 舊版留在看得見位置的卡會被搬進隱藏目錄;MigrateCardNames 每輪自動做, 使用者不必下任何指令。報告多一個 VaultRoot,說清楚是誰的庫。 leo 派工單上的線索(庫在監看根**底下**)實測不成立:產物一律錨在監看根, 不會落進子庫。但那個「本來就沒破」原本沒有任何機制保證,所以照樣把兩種 格式的子庫情境永久寫進測試,加上 ensureWritable 當第二道保險。 驗證缺口(票上第 6 條):第二輪的足跡測試方向是對的,漏的是**觀測窗**—— snapshotTree 只拍監看根,而災情發生在監看根外面、庫裡面;且 fixture 只有 `root := vault` 一種擺法,測試與被測程式犯了同一個假設,所以永遠是綠的。 vault_subdir_test.go 把快照邊界改成筆記庫,並把「監看根與庫根的關係」升格 成測試維度(庫在上/庫在下/庫就是它/沒有庫 × Logseq/Obsidian)。 全程只用 t.TempDir() 與 mktemp -d;沒碰任何真實筆記庫、沒重啟任何 daemon。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,8 +70,11 @@ type TidyItem struct {
|
||||
|
||||
// TidyReport=一次收拾的完整帳目。
|
||||
type TidyReport struct {
|
||||
Root string `json:"root"`
|
||||
VaultType VaultType `json:"vault_type"`
|
||||
Root string `json:"root"`
|
||||
VaultType VaultType `json:"vault_type"`
|
||||
// VaultRoot=筆記庫的根。與 Root 不同時代表「監看的是筆記庫底下的一層子資料夾」
|
||||
// ——第三輪修的正是這一種(arcrun-rag#60)。空字串=不在任何筆記庫裡。
|
||||
VaultRoot string `json:"vault_root,omitempty"`
|
||||
Applied bool `json:"applied"` // false=只看不動(dry-run)
|
||||
Items []TidyItem `json:"items"`
|
||||
}
|
||||
@@ -102,9 +105,16 @@ func isUnderCardDir(relSlash string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// collectCardItems 掃出卡片產物區裡「還沒帶標記」的檔案,算出它們該改成什麼名字。
|
||||
// 已經帶標記的不列入(冪等:跑第二次是空的)。
|
||||
// collectCardItems 掃出卡片產物區裡「不在它現在該在的位置/還沒帶標記」的檔案,
|
||||
// 算出它們該搬去哪、該叫什麼名字。已經到位又帶標記的不列入(冪等:跑第二次是空的)。
|
||||
//
|
||||
// 🔴 arcrun-rag#60 第三輪:判準從「有沒有帶標記」擴充成「**位置對不對** + 有沒有帶標記」。
|
||||
// 只看標記在第三輪不夠——監看根是 vault 子資料夾時,前兩輪的 daemon 已經把一批
|
||||
// **帶標記但落在可見目錄**(`system-dev/wiki/cards/arcrun-*.md`)的卡寫進使用者的
|
||||
// 筆記庫了。那些卡在 Logseq 眼裡照樣是一頁一頁的機器頁面,改名救不了,要搬進隱藏目錄。
|
||||
// 少了這一段,票上「已經寫進去的那一批要能收拾」對這一類就等於沒做。
|
||||
func collectCardItems(absRoot string) []TidyItem {
|
||||
want := cardsRelDirFor(absRoot)
|
||||
var items []TidyItem
|
||||
for _, relDir := range cardDirsToScan() {
|
||||
base := filepath.Join(absRoot, filepath.FromSlash(relDir))
|
||||
@@ -113,20 +123,21 @@ func collectCardItems(absRoot string) []TidyItem {
|
||||
return nil
|
||||
}
|
||||
name := d.Name()
|
||||
if IsMarked(name) {
|
||||
return nil // 已經是新的了
|
||||
}
|
||||
rel, rerr := filepath.Rel(absRoot, p)
|
||||
if rerr != nil {
|
||||
return nil
|
||||
}
|
||||
relSlash := filepath.ToSlash(rel)
|
||||
if IsMarked(name) && filepath.ToSlash(filepath.Dir(relSlash)) == want {
|
||||
return nil // 位置對、標記也有,已經是新的了
|
||||
}
|
||||
kind := TidyKindCard
|
||||
if bakSuffix.MatchString(name) {
|
||||
kind = TidyKindCardBak
|
||||
}
|
||||
items = append(items, TidyItem{
|
||||
Rel: filepath.ToSlash(rel),
|
||||
To: filepath.ToSlash(filepath.Join(filepath.Dir(rel), MarkName(name))),
|
||||
Rel: relSlash,
|
||||
To: filepath.ToSlash(filepath.Join(want, MarkName(name))),
|
||||
Kind: kind,
|
||||
})
|
||||
return nil
|
||||
@@ -181,11 +192,15 @@ func collectTemplateItems(absRoot string, isVault bool) []TidyItem {
|
||||
return items
|
||||
}
|
||||
|
||||
// MigrateCardNames 把卡片產物區裡沒帶標記的舊卡就地改名。daemon 每輪自動呼叫。
|
||||
// 回傳實際改名的筆數。**只碰卡片產物區**,其餘一概不動。
|
||||
// MigrateCardNames 把卡片產物區裡「位置不對或沒帶標記」的舊卡歸位。daemon 每輪自動呼叫。
|
||||
// 回傳實際動到的筆數。**只碰卡片產物區**,其餘一概不動。
|
||||
//
|
||||
// 為什麼可以自動:那兩個目錄從頭到尾只有 daemon 會寫(見 cardsRelDirFor 的註解),
|
||||
// 不存在「誤把使用者的檔案改名」的可能。目標已存在就跳過,不覆蓋。
|
||||
//
|
||||
// 🔴 第三輪起也負責**搬移**(不只改名):監看根在 vault 裡時,舊版寫在可見目錄的卡
|
||||
// 會被搬進 `.arcrun-rag/wiki/cards/`。這是自動的,因為紅線寫著「不准要使用者去設定
|
||||
// 什麼開關才能保護自己的筆記」——他不該為了收拾機器留下的東西去學一個新指令。
|
||||
func MigrateCardNames(absRoot string) int {
|
||||
n := 0
|
||||
for _, it := range collectCardItems(absRoot) {
|
||||
@@ -194,6 +209,9 @@ func MigrateCardNames(absRoot string) int {
|
||||
if _, err := os.Stat(to); err == nil {
|
||||
continue // 新名字已經有東西了,不覆蓋
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(to), 0o755); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := os.Rename(from, to); err == nil {
|
||||
n++
|
||||
}
|
||||
@@ -215,11 +233,19 @@ func Tidy(root string, apply bool) (*TidyReport, error) {
|
||||
return nil, fmt.Errorf("%s 不是資料夾", absRoot)
|
||||
}
|
||||
|
||||
vt := DetectVaultType(absRoot)
|
||||
rep := &TidyReport{Root: absRoot, VaultType: vt, Applied: apply}
|
||||
// 🔴 兩個判準,回答兩個不同的問題,不要合併(arcrun-rag#60 第三輪):
|
||||
//
|
||||
// · ctx(含往上找)=「我寫東西會不會落進誰的筆記庫」——決定卡片該在哪,也是報告顯示的身分。
|
||||
// · DetectVaultType(absRoot)(只看這一層)=「這個資料夾**本身**是不是筆記庫」——
|
||||
// 只有它才夠格授權「把 template 殘留搬走」。理由:一個住在筆記庫底下的
|
||||
// **使用者自己的開發 repo**(`KB/some-repo/system-dev/…`)在 ctx 眼中也「在 vault 裡」,
|
||||
// 但那些 template 檔是他的真檔案,不是 daemon 鋪的。搬了就是弄壞他的 repo。
|
||||
// 分不出來的時候只列不動——這條在第二輪就寫了,第三輪擴大偵測範圍時更要守住。
|
||||
ctx := DetectVaultContext(absRoot)
|
||||
rep := &TidyReport{Root: absRoot, VaultType: ctx.Type, VaultRoot: ctx.Root, Applied: apply}
|
||||
|
||||
items := collectCardItems(absRoot)
|
||||
items = append(items, collectTemplateItems(absRoot, vt != VaultNone)...)
|
||||
items = append(items, collectTemplateItems(absRoot, DetectVaultType(absRoot) != VaultNone)...)
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Rel < items[j].Rel })
|
||||
|
||||
for _, it := range items {
|
||||
@@ -227,7 +253,10 @@ func Tidy(root string, apply bool) (*TidyReport, error) {
|
||||
rep.Items = append(rep.Items, it)
|
||||
continue
|
||||
}
|
||||
willMove := it.Kind == TidyKindTemplate
|
||||
// 換了目錄就是「搬走」,只換名字才是「改名」——報告要說實話,
|
||||
// 使用者才知道去哪裡找他的東西(第三輪起卡片也會換目錄)。
|
||||
willMove := it.Kind == TidyKindTemplate ||
|
||||
filepath.ToSlash(filepath.Dir(it.Rel)) != filepath.ToSlash(filepath.Dir(it.To))
|
||||
from := filepath.Join(absRoot, filepath.FromSlash(it.Rel))
|
||||
to := filepath.Join(absRoot, filepath.FromSlash(it.To))
|
||||
|
||||
@@ -294,7 +323,11 @@ func runTidy(args []string) int {
|
||||
return 0
|
||||
}
|
||||
kind := "一般資料夾"
|
||||
if rep.VaultType != VaultNone {
|
||||
switch {
|
||||
case rep.VaultType != VaultNone && rep.VaultRoot != rep.Root:
|
||||
// 第三輪的那一種擺法——講清楚是誰的庫,不然使用者看不懂為什麼要搬。
|
||||
kind = fmt.Sprintf("%s 筆記庫「%s」底下的子資料夾", rep.VaultType, rep.VaultRoot)
|
||||
case rep.VaultType != VaultNone:
|
||||
kind = string(rep.VaultType) + " 筆記庫"
|
||||
}
|
||||
fmt.Printf("%s(%s)\n", rep.Root, kind)
|
||||
@@ -306,7 +339,11 @@ func runTidy(args []string) int {
|
||||
case TidyActionRenamed, TidyActionWillRename:
|
||||
fmt.Printf(" 改名 %s → %s\n", it.Rel, it.To)
|
||||
case TidyActionMoved, TidyActionWillMove:
|
||||
fmt.Printf(" 搬走 %s → %s(舊版 daemon 鋪的開發用檔案,不是你的筆記)\n", it.Rel, it.To)
|
||||
why := "舊版 daemon 鋪的開發用檔案,不是你的筆記"
|
||||
if it.Kind != TidyKindTemplate {
|
||||
why = "舊版寫在看得見的位置,你的筆記軟體會把它當成一頁——搬進隱藏目錄"
|
||||
}
|
||||
fmt.Printf(" 搬走 %s → %s(%s)\n", it.Rel, it.To, why)
|
||||
case TidyActionReport:
|
||||
fmt.Printf(" 略過 %s(%s)\n", it.Rel, it.Note)
|
||||
case TidyActionSkipped:
|
||||
|
||||
Reference in New Issue
Block a user