package main import ( "context" "encoding/json" "flag" "fmt" "os" "path/filepath" "strings" "time" "chat-agent-system/internal/docrevision" "github.com/jackc/pgx/v5/pgxpool" ) // document-revision-backfill dry-runs or applies controlled document output.md into the ledger. func main() { dsn := flag.String("dsn", os.Getenv("TEST_DATABASE_URL"), "postgres DSN") repoRoot := flag.String("repo-root", ".", "repository root for relative work_dir") apply := flag.Bool("apply", false, "apply changes (default dry-run)") outPath := flag.String("report", "", "write JSON report path") flag.Parse() if *dsn == "" { fmt.Fprintln(os.Stderr, "dsn or TEST_DATABASE_URL required") os.Exit(2) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() pool, err := pgxpool.New(ctx, *dsn) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer pool.Close() rows, err := pool.Query(ctx, ` SELECT doc_id, COALESCE(work_dir,''), COALESCE(content_sha256,''), COALESCE(current_state,'drafting'), project_id FROM document_records ORDER BY doc_id`) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer rows.Close() type item struct { DocID string `json:"doc_id"` Status string `json:"status"` Detail string `json:"detail,omitempty"` SHA string `json:"sha,omitempty"` FileSHA string `json:"file_sha,omitempty"` } report := struct { DryRun bool `json:"dry_run"` Items []item `json:"items"` Success int `json:"success"` Missing int `json:"missing"` Mismatch int `json:"hash_mismatch"` }{DryRun: !*apply} svc := docrevision.NewService(pool) for rows.Next() { var docID, workDir, dbSHA, state string var projectID int64 if err := rows.Scan(&docID, &workDir, &dbSHA, &state, &projectID); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } it := item{DocID: docID, SHA: dbSHA} if workDir == "" { it.Status = "missing" it.Detail = "empty work_dir" report.Missing++ report.Items = append(report.Items, it) continue } path := workDir if !filepath.IsAbs(path) { path = filepath.Join(*repoRoot, workDir) } body, err := os.ReadFile(filepath.Join(path, "output.md")) if err != nil { it.Status = "missing" it.Detail = err.Error() report.Missing++ report.Items = append(report.Items, it) continue } canon := docrevision.CanonicalizeMarkdown(string(body)) fileSHA := docrevision.SHA256Hex(canon) it.FileSHA = fileSHA if dbSHA != "" && !strings.EqualFold(dbSHA, fileSHA) { legacy := docrevision.SHA256Hex(docrevision.LegacyDestructiveCanonicalizeMarkdown(string(body))) if strings.EqualFold(dbSHA, legacy) { it.Status = "legacy_canonicalization_drift" it.Detail = "LEGACY_CANONICALIZATION_DRIFT: refuse silent overwrite; use document-content-reconcile" } else { it.Status = "hash_mismatch" it.Detail = "CONTENT_FORK: document_records.content_sha256 != file" } report.Mismatch++ report.Items = append(report.Items, it) continue } if *apply { if _, err := svc.OpenTemplateVersion(ctx, docID, canon, docrevision.ActorSnapshot{DisplayName: "backfill"}, projectID, workDir); err != nil { it.Status = "error" it.Detail = err.Error() report.Items = append(report.Items, it) continue } } it.Status = "success" report.Success++ report.Items = append(report.Items, it) } if err := rows.Err(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } enc, _ := json.MarshalIndent(report, "", " ") if *outPath != "" { _ = os.WriteFile(*outPath, enc, 0o644) } fmt.Println(string(enc)) if report.Mismatch > 0 { os.Exit(3) } }