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:
@@ -118,6 +118,13 @@ type TriggerPayload struct {
|
||||
// DuplicateFormats=本輪偵測到、同檔名主幹的多格式重複(2026-08-07,見 FormatDuplicate)。
|
||||
// 同 Skipped:只給本機使用者看,不隨 payload 送雲端(schema additionalProperties:false 會擋)。
|
||||
DuplicateFormats []FormatDuplicate `json:"-"`
|
||||
|
||||
// Plan/ExcludedByPlan=這一輪用了什麼收檔策略、據此擋掉幾個檔(arcrun-rag#104)。
|
||||
// 同上,`json:"-"`:給本機使用者看的,不送雲端。
|
||||
// 🔴 這兩個欄位就是 #104 那條紅線的載體——「排除規則要看得見」。少了它們,
|
||||
// 使用者接上一個一萬檔的 repo 只看到 32 個進度,會以為系統壞了。
|
||||
Plan IngestPlan `json:"-"`
|
||||
ExcludedByPlan int `json:"-"`
|
||||
}
|
||||
|
||||
// FormatDuplicate=同一份內容被偵測到有多種格式並存(同檔名主幹、不同副檔名)。
|
||||
@@ -217,6 +224,9 @@ type ScanOptions struct {
|
||||
// SkipDirNames:目錄名黑名單(任一層命中整棵跳過)。daemon-beta task 2:
|
||||
// template 代裝後 `system-dev/`(wiki 產物區)不得被當成原稿掃進 ingest。
|
||||
SkipDirNames map[string]bool
|
||||
// Plan:收檔策略(arcrun-rag#104)。零值=IngestAll,行為與加這個欄位之前**完全一致**
|
||||
// ——既有呼叫端與測試不必全部改。要拿到 #104 的效果就傳 PlanIngest(root)。
|
||||
Plan IngestPlan
|
||||
}
|
||||
|
||||
const DefaultMaxRemovedRatio = 0.4
|
||||
@@ -259,11 +269,21 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
||||
var skipped []SkippedFile
|
||||
skippedOther := 0
|
||||
var skippedOtherNames []string
|
||||
// arcrun-rag#104:被策略擋掉的檔案數。**一定要數出來**——票上的紅線是
|
||||
// 「用戶要知道有 8,000 個檔沒被收,因為它們是程式碼」,不是安靜地少收。
|
||||
excludedByPlan := 0
|
||||
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, werr error) error {
|
||||
if werr != nil {
|
||||
return werr
|
||||
}
|
||||
name := d.Name()
|
||||
relOf := func() (string, bool) {
|
||||
rel, rerr := filepath.Rel(root, p)
|
||||
if rerr != nil {
|
||||
return "", false
|
||||
}
|
||||
return filepath.ToSlash(rel), true
|
||||
}
|
||||
if d.IsDir() {
|
||||
if p != root && strings.HasPrefix(name, ".") {
|
||||
return filepath.SkipDir // 隱藏目錄(.git、.obsidian…)整棵跳過
|
||||
@@ -271,11 +291,23 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
||||
if p != root && opts.SkipDirNames[name] {
|
||||
return filepath.SkipDir // 名單目錄(system-dev…)整棵跳過
|
||||
}
|
||||
// #104:依策略整棵跳過(依賴/建置產物/範本/worktree/巢狀 repo/
|
||||
// 以及非本次策略要收的區域)。整棵跳掉的檔不逐一計數——
|
||||
// 那個數字對使用者沒有意義,Plan.Reason 那句話才是他要的解釋。
|
||||
if p != root {
|
||||
if rel, ok := relOf(); ok && opts.Plan.SkipsDir(rel, p) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(name, ".") {
|
||||
return nil
|
||||
}
|
||||
if rel, ok := relOf(); ok && !opts.Plan.KeepsFile(rel) {
|
||||
excludedByPlan++
|
||||
return nil
|
||||
}
|
||||
if abs, aerr := filepath.Abs(p); aerr == nil && opts.SkipPaths[abs] {
|
||||
return nil
|
||||
}
|
||||
@@ -284,8 +316,17 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
||||
// template 本來就不是隱藏的,靠隱藏判斷會漏掉一大半。
|
||||
// 也不計進「有 N 個檔案沒有被整理」——那是給使用者看他自己的檔案的,
|
||||
// 我們自己鋪的東西不該佔用他的注意力。
|
||||
if rel, rerr := filepath.Rel(root, p); rerr == nil && TemplateOwns(filepath.ToSlash(rel)) {
|
||||
return nil
|
||||
//
|
||||
// 🔴 arcrun-rag#104 例外:curated-wiki 模式下,`system-dev/wiki/` **正是要收的那一份**。
|
||||
// 上面那條規則(2026-08-06)與 leo 2026-08-14 的規格直接對撞,衝突由身分化解:
|
||||
// · 我們代裝 template 的資料夾(沒有 `.git`)⇒ 那些檔是**我們鋪的**,照舊不收
|
||||
// · 使用者自己的 repo(有 `.git`)⇒ 那份 wiki 是**他寫的**,正是他要我們讀的
|
||||
// 判準只有 PlanIngest 一個地方,見 ingestplan.go。
|
||||
if rel, rerr := filepath.Rel(root, p); rerr == nil {
|
||||
relSlash := filepath.ToSlash(rel)
|
||||
if TemplateOwns(relSlash) && !opts.Plan.OverridesTemplateOwned(relSlash) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if !allowedExt[ext] {
|
||||
@@ -505,5 +546,7 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
||||
SkippedOther: skippedOther,
|
||||
SkippedOtherNames: skippedOtherNames,
|
||||
DuplicateFormats: duplicateFormats,
|
||||
Plan: opts.Plan,
|
||||
ExcludedByPlan: excludedByPlan,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user