129fe79ecb
## #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
99 lines
5.3 KiB
Go
99 lines
5.3 KiB
Go
// machinemark.go — daemon 寫進使用者資料夾的每一個檔案,檔名都帶同一個標記
|
||
// (arcrun-rag#60 第二輪,2026-08-12 leo 實撞)。
|
||
//
|
||
// 🔴 為什麼上一輪不夠:
|
||
//
|
||
// 第一輪(1e36bb1)只改了「卡片**落在哪個目錄**」——vault 就改落隱藏的
|
||
// `.arcrun-rag/wiki/cards/`。但 Logseq/Obsidian 的**頁名是從 basename 來的**,
|
||
// 而卡片的 basename 一直是「原稿的 basename」:使用者的 `journals/2026_08_10.md`
|
||
// 萃出來的卡就叫 `2026_08_10.md`,跟他自己的日誌頁**同名**。
|
||
// ⇒ 只要那個目錄有任何一刻被看見(他自己翻資料夾、備份工具攤平、之後改成非隱藏、
|
||
// 或是**非 vault 的一般資料夾**——那裡卡片本來就是可見的),撞名就回來了。
|
||
// leo 原話:「不只是加上 journal,**可能所有的檔案都加一個前後綴,比如「wiki」**。」
|
||
//
|
||
// 🔴 規約(本檔是唯一真相源,不准有第二種標記):
|
||
//
|
||
// **daemon 在使用者的監看根底下產生的每一個檔案,basename 一律以 MachineMark 開頭。**
|
||
//
|
||
// 一種就是一種——「一部分加一部分不加」比全不加更難分辨(leo 的紅線)。
|
||
// 所以任何新的寫檔點都必須經過 MarkName(),不得自己拼檔名。
|
||
// 迴歸網=vault_footprint_test.go:它 walk 整個監看根,只要有任何新檔沒帶標記就紅,
|
||
// **與寫檔點的數量無關**——這正是上一輪缺的那張網(上一輪只點測了一個常數)。
|
||
//
|
||
// 為什麼是「前綴」不是「後綴」:`ls`/Finder/Logseq 的頁面清單都是按名字排序的,
|
||
// 前綴會讓機器寫的東西**全部聚成一叢**,一眼就掃得完;後綴則散落在他自己的檔案之間,
|
||
// 等於還是要一個一個看。leo 要的是「打開資料夾一眼分得出」,那就是前綴。
|
||
//
|
||
// 為什麼標記字串是 `arcrun-` 而不是 leo 舉例的 `wiki`:`wiki` 是他自己筆記裡會用的
|
||
// 普通詞(`wiki-整理術.md` 是他的、不是我們的),拿它當標記等於製造新的誤認;
|
||
// `arcrun-` 直接說出「這是 Arcrun 寫的」,且與既有的 `.arcrun-rag/` 目錄同一組字。
|
||
// 這是一行常數,leo 覺得該用別的字就改這裡一個地方(連舊產物一起 tidy 重跑即可)。
|
||
package collector
|
||
|
||
import "strings"
|
||
|
||
// MachineMark 是 daemon 產物的**唯一**標記。改這裡=改全部(含 tidy 的認人判準)。
|
||
const MachineMark = "arcrun-"
|
||
|
||
// MarkName 把一個 basename 加上標記。冪等:已經帶標記的原樣回傳,
|
||
// 不會疊成 `arcrun-arcrun-x.md`(tidy 重跑、daemon 重跑都會走到這條)。
|
||
func MarkName(base string) string {
|
||
if IsMarked(base) {
|
||
return base
|
||
}
|
||
return MachineMark + base
|
||
}
|
||
|
||
// IsMarked 回答「這個 basename 是不是機器寫的」。
|
||
// ⚠️ 只吃 basename,不吃路徑——路徑中間的目錄名不算數(目錄是不是機器的,
|
||
// 由 cardsRelDirFor 那組常數決定,不由這裡猜)。
|
||
func IsMarked(base string) bool {
|
||
return strings.HasPrefix(base, MachineMark)
|
||
}
|
||
|
||
// UnmarkName 去掉標記,回傳原本的名字。tidy 拿來對照「這張卡對應哪一份原稿」。
|
||
func UnmarkName(base string) string {
|
||
return strings.TrimPrefix(base, MachineMark)
|
||
}
|
||
|
||
// IsMachineOwnedRel 回答「這個相對路徑是不是我們寫的東西」——**規約的完整判準**,
|
||
// 迴歸網(vault_footprint_test.go/vault_subdir_test.go)用的就是這一支。
|
||
//
|
||
// 兩條路,第二條是規約唯一認可的例外:
|
||
//
|
||
// ① basename 帶 MachineMark ⇒ 走在使用者的檔案之間也一眼分得出(規約主體)
|
||
// ② 路徑上**任何一層**是 `.arcrun-rag/` ⇒ **那個目錄名自己就是標記**,且它是隱藏目錄
|
||
//
|
||
// ②「任何一層」而不是「開頭」:呼叫端給的相對起點不一定是監看根——迴歸網比對整個
|
||
// 筆記庫時,同一個檔的相對路徑是 `docs/.arcrun-rag/…`(監看根是 vault 底下的 docs/)。
|
||
// 判「誰擁有這個檔」不該受呼叫端從哪裡起算影響。
|
||
//
|
||
// 🔴 為什麼需要 ②(arcrun-rag#105):我們得在使用者的 repo 裡放一份 `.gitignore`
|
||
// (讓整個工作區對 git 隱形,他跑一輪 `git status` 才會是乾淨的)。那個檔名是
|
||
// **git 定的,我們改不得**——叫 `arcrun-.gitignore` 的話 git 根本不認,功能等於沒做。
|
||
//
|
||
// 這不是替規約開後門:規約的目的是「打開資料夾一眼分得出哪些是機器寫的」,
|
||
// 而 `.arcrun-rag/` 這個目錄名把整叢東西一次講完,比逐檔前綴**更強**。
|
||
// ⚠️ 例外只有這一個。監看根底下**其他任何位置**的新檔,一律得帶前綴——
|
||
// 想在別處寫一個「名字不能改」的檔時,先想清楚那個檔為什麼不能住進 `.arcrun-rag/`。
|
||
func IsMachineOwnedRel(relSlash string) bool {
|
||
if IsMarked(pathBase(relSlash)) {
|
||
return true
|
||
}
|
||
for _, seg := range strings.Split(relSlash, "/") {
|
||
if seg == workspaceRelDir {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// pathBase 取斜線路徑的最後一段(不用 filepath.Base:這裡的輸入一律是斜線分隔的
|
||
// 相對路徑,不該受執行平台的分隔符影響)。
|
||
func pathBase(relSlash string) string {
|
||
if i := strings.LastIndex(relSlash, "/"); i >= 0 {
|
||
return relSlash[i+1:]
|
||
}
|
||
return relSlash
|
||
}
|