feat(ingest-hash-trigger): 凍結 collector/ingest schema+Go collector 骨架(issue #5, SDD task 1+2)
- schemas/collector-trigger.v1.schema.json:collector→named-webhook 觸發 payload(source_hash/r2_key/renamed/R6 warnings) - schemas/kbdb-ingest-request.v1.schema.json:source_hash 必填缺=400;儲存映射走 metadata_json.$.source_hash(守 kbdb 表不變鐵律);同 hash 重送=200 already_ingested - schemas/MIGRATION-webhook-payload.md:舊 Gitea push payload 逐欄對照(給 task 4 改寫 workflow) - collector/:Go module(純 stdlib)——manifest 讀寫+mtime fast-path 掃描+renamed hash 配對+40% 大量刪除防呆;CLI collector scan - go test ./... 全綠 7/7(added/modified/removed/renamed/防呆五情境+fast-path+manifest 往返) - Node 版 collector 標 legacy 並存(SDD task 4 拆除) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
// collector — hash 差異偵測 collector(SDD ingest-hash-trigger,Go 版骨架)。
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// collector scan --root <知識資料夾> --manifest <manifest.json 路徑> \
|
||||
// [--max-removed-ratio 0.4] [--dry-run]
|
||||
//
|
||||
// 一次掃描:走訪 root、對照 manifest、把事件(符合
|
||||
// schemas/collector-trigger.v1.schema.json)以 JSON 輸出到 stdout,並更新 manifest
|
||||
// (--dry-run 不寫)。daemon 常駐/launchd/R2 上傳/打 named-webhook=之後的 task,
|
||||
// 本階段不接網路。
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "scan" {
|
||||
fmt.Fprintln(os.Stderr, "用法: collector scan --root <dir> --manifest <file> [--max-removed-ratio 0.4] [--dry-run]")
|
||||
os.Exit(2)
|
||||
}
|
||||
fs := flag.NewFlagSet("scan", flag.ExitOnError)
|
||||
root := fs.String("root", "", "知識資料夾根路徑(必填)")
|
||||
manifestPath := fs.String("manifest", "", "manifest JSON 檔路徑(必填;不存在會建新)")
|
||||
ratio := fs.Float64("max-removed-ratio", DefaultMaxRemovedRatio, "大量刪除防呆門檻(removed 數 > manifest 條目 × 本值 → 全部不下架、只發警告)")
|
||||
dryRun := fs.Bool("dry-run", false, "只輸出事件,不更新 manifest")
|
||||
if err := fs.Parse(os.Args[2:]); err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
if *root == "" || *manifestPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "錯誤:--root 與 --manifest 皆為必填")
|
||||
os.Exit(2)
|
||||
}
|
||||
absRoot, err := filepath.Abs(*root)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
absManifest, err := filepath.Abs(*manifestPath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
m, err := LoadManifest(absManifest, absRoot)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
payload, err := Scan(absRoot, m, ScanOptions{
|
||||
MaxRemovedRatio: *ratio,
|
||||
SkipPaths: map[string]bool{absManifest: true}, // manifest 若住在 root 底下,不掃自己
|
||||
})
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if !*dryRun {
|
||||
if err := m.Save(absManifest); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
out, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, "collector:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user