fix(collector): daemon 不再改用戶版控中的檔案,接 repo 只讀整理好的 wiki(#105/#104)

## #105 daemon 會改用戶的檔案(不可逆,先修)

2026-08-14 21:45 實撞:InkStoneCo 在看守清單裡,daemon 回報「已把 1 張舊卡片歸位」,
實際把 system-dev/wiki/cards/autonomy/ 整個子目錄壓平改名,16 個版控中的檔案變成刪除。

真兇是 MigrateCardNames 的一句假設:「那兩個目錄從頭到尾只有 daemon 會寫」。
那句話在 vault 上成立,在 repo 上不成立——system-dev/wiki/cards/ 是 template 的規約
路徑,而 template 就是要裝進開發者自己的 repo,那裡本來就有人家自己的檔案。

- 新增 repoguard.go:`.git` 判準(含 linked worktree),與 #104 共用同一個判準源
- 版控中的資料夾一個檔都不自動動;改成記帳+講給使用者聽,出口是 collector tidy --apply
- Blocked 只算「確定是我們寫的」(帶 arcrun- 標記)——報使用者自己的卡等於發假訊息
- 落卡與工作區改走 .arcrun-rag/(與 vault 同待遇),自帶 .gitignore(*) 讓它對 git 隱形
- 非版控資料夾(含 vault)行為與 #60 第三輪完全一致,不推翻前兩輪的成果

## #104 接上開發 repo 會把上萬個原始檔排進佇列

leo 的規格:「它要辨識這個庫已經有 wiki,那就直接 ingest 了」「只有文件要讀,程式碼不用讀」。

- 新增 ingestplan.go:掃描前先問「這個資料夾是什麼」——all/curated-wiki/docs-only
- 排除靠路徑身分不靠副檔名:依賴、建置產物、templatefs 範本、linked worktree、巢狀子 repo
- 策略與擋掉的數字經 TriggerPayload.Plan 走進 status.json,CLI 走 stderr(排除規則要看得見)
- 子專案自己的 wiki 刻意不收,但一定列出來讓使用者知道去哪裡找
- 與 TemplateOwns(把 system-dev/ 整棵當開發用的)的對撞用身分化解,不拿掉任一條:
  我們代裝的資料夾沒有 .git(舊規則照舊),他自己的 repo 有(收那份 wiki)

## 實測

#105:舊版 daemon 對真 repo 跑一輪 → git status 32 行(16 個 D + 16 個 ??,與 21:45
      撞到的一字不差);新版跑同一輪 → 0 行。另有真 git init 的端到端測試。
#104:造出 leo 那棵樹的形狀(InkStoneCo + products/arcrun-rag + 3 份出貨 worktree,
      967 個文件檔)→ 舊版送 91 個檔(多收的全是 -wt*ship/ 裡的 templatefs 範本、
      benchmark 結果、docs-site 產物),新版送 32 個,全部落在 system-dev/wiki/ 底下。

MachineMark 規約新增唯一例外 IsMachineOwnedRel:.arcrun-rag/ 底下的檔以目錄名為標記
(.gitignore 的檔名是 git 定的,改不得)。迴歸網判準同步換過去。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUwsLkFEGN8496bZTqhjFq
This commit is contained in:
Claude
2026-08-14 14:41:08 +00:00
parent 64cb20a1aa
commit 129fe79ecb
13 changed files with 1104 additions and 36 deletions
+49 -11
View File
@@ -192,18 +192,56 @@ func collectTemplateItems(absRoot string, isVault bool) []TidyItem {
return items
}
// CardMigrationMigrateCardNames 一輪的帳目。
//
// Blocked 不是「失敗」,是**刻意沒動**:那個資料夾在版控裡,我們沒有資格自己決定
// 改別人版控中的檔案。它要被講出來(direct.go 收進 results ⇒ status.json ⇒ App),
// 不是靜默跳過——#105 的紅線與 #104 同一條:「不要讓用戶猜」。
type CardMigration struct {
Moved int // 真的改名/搬移了幾個
Blocked int // 認出是舊卡、但因為在版控裡而沒動的
RepoRoot string // Blocked > 0 時:那個版控工作目錄的根
}
// MigrateCardNames 把卡片產物區裡「位置不對或沒帶標記」的舊卡歸位。daemon 每輪自動呼叫。
// 回傳實際動到的筆數。**只碰卡片產物區**,其餘一概不動。
// **只碰卡片產物區**,其餘一概不動。
//
// 為什麼可以自動:那兩個目錄從頭到尾只有 daemon 會寫(見 cardsRelDirFor 的註解),
// 不存在「誤把使用者的檔案改名」的可能。目標已存在就跳過,不覆蓋。
// 🔴 第四輪=arcrun-rag#1052026-08-14 實撞):**版控中的資料夾一個檔都不動。**
//
// 🔴 第三輪起也負責**搬移**(不只改名):監看根在 vault 裡時,舊版寫在可見目錄的卡
// 會被搬進 `.arcrun-rag/wiki/cards/`。這是自動的,因為紅線寫著「不准要使用者去設定
// 什麼開關才能保護自己的筆記」——他不該為了收拾機器留下的東西去學一個新指令。
func MigrateCardNames(absRoot string) int {
n := 0
for _, it := range collectCardItems(absRoot) {
// 前三輪(#60f71e5a2/578ac2b)修的都是同一個場景的深度——「卡片會不會汙染
// 使用者的**筆記庫**」,方向一路往「更自動」走,因為那條紅線是「不准要使用者
// 去設定什麼開關才能保護自己的筆記」。**本輪不推翻那個方向**:非版控資料夾
// (含 vault)的行為與 578ac2b 完全一致。
//
// 本輪修的是另一個場景——「監看的是他的 **git repo**」。上一版這裡的理由是
// 「那兩個目錄從頭到尾只有 daemon 會寫,不存在誤把使用者的檔案改名的可能」。
// 那句話在 repo 上錯了,而且錯得很貴:`system-dev/wiki/cards/` 是
// system-dev-template 的規約路徑,而 template 就是要裝進**開發者自己的 repo**——
// leo 的 `InkStoneCo` 裡那個目錄底下是他自己寫的知識卡(還有 `autonomy/` 這種
// 他自己開的子目錄)。daemon 把它們壓平改名,**16 個版控中的檔案當場變成刪除**。
//
// ⇒ 判準加一問「這裡有沒有 `.git`」(見 repoguard.go 對「這夠不夠」的討論)。
// 有=停手、記帳、講給使用者聽,要整理請他自己跑 `collector tidy --apply`
// (那條路預設 dry-run,會先把要動什麼列給他看)。
// 兩條紅線不衝突:「不要他學新選項才能保護筆記」管的是筆記庫;
// repo 那邊 leo 的原話是「**預設應該是不動,要動要有出口**」。
func MigrateCardNames(absRoot string) CardMigration {
items := collectCardItems(absRoot)
if repoRoot := DetectRepoRoot(absRoot); repoRoot != "" {
// 只把**確定是我們寫的**(帶 arcrun- 標記、只是位置不對)算進 Blocked。
// 沒帶標記的在 repo 裡預設就是使用者自己的檔案——把它們算進來,訊息就會變成
// 「有 40 個舊卡片沒整理」,而那 40 張是 leo 自己寫的知識卡,
// 報一個全錯的數字比不報更糟(#105 要的是誠實,不是熱心)。
blocked := 0
for _, it := range items {
if IsMarked(filepath.Base(it.Rel)) {
blocked++
}
}
return CardMigration{Blocked: blocked, RepoRoot: repoRoot}
}
out := CardMigration{}
for _, it := range items {
from := filepath.Join(absRoot, filepath.FromSlash(it.Rel))
to := filepath.Join(absRoot, filepath.FromSlash(it.To))
if _, err := os.Stat(to); err == nil {
@@ -213,10 +251,10 @@ func MigrateCardNames(absRoot string) int {
continue
}
if err := os.Rename(from, to); err == nil {
n++
out.Moved++
}
}
return n
return out
}
// Tidy 掃出所有舊產物;apply=false 只報告,apply=true 才真的改名/搬移。