package main import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "time" "chat-agent-system/internal/docgov" "chat-agent-system/internal/docrevision" "github.com/jackc/pgx/v5/pgxpool" ) // document-content-reconcile classifies output.md vs ledger head. // Default is dry-run against an isolated DB. Live apply to dev requires // --live --environment dev --doc-id --confirm REPAIR_SINGLE_DEV_DOC. func main() { cfg := cliConfig{} flag.StringVar(&cfg.DSN, "dsn", os.Getenv("TEST_DATABASE_URL"), "postgres DSN") flag.StringVar(&cfg.RepoRoot, "repo-root", "", "directory used to resolve relative work_dir") flag.BoolVar(&cfg.Apply, "apply", false, "apply LEGACY_CANONICALIZATION_DRIFT repairs (default dry-run)") flag.StringVar(&cfg.DocID, "doc-id", "", "single document id (required for --live)") flag.StringVar(&cfg.BackupDir, "backup-dir", "", "required when --apply: write file + head metadata backups") flag.BoolVar(&cfg.Live, "live", false, "explicit live/dev mode; default still refuses shared env") flag.StringVar(&cfg.Environment, "environment", "", "live environment; only 'dev' is accepted") flag.StringVar(&cfg.Confirm, "confirm", "", "live apply hard gate; must be REPAIR_SINGLE_DEV_DOC") flag.StringVar(&cfg.StoredDataRoot, "stored-data-root", "", "stored/container data root, e.g. /app/data") flag.StringVar(&cfg.HostDataRoot, "host-data-root", "", "host data root mapped from stored-data-root, e.g. $REPO/data/dev") outPath := flag.String("report", "", "write JSON report path") flag.Parse() if err := validateCLI(cfg); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(2) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() pool, err := pgxpool.New(ctx, cfg.DSN) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer pool.Close() svc := docrevision.NewService(pool) if cfg.HostDataRoot != "" { svc.DataDir = cfg.HostDataRoot } else { svc.DataDir = cfg.RepoRoot } svc.StoredDataRoot = cfg.StoredDataRoot svc.HostDataRoot = cfg.HostDataRoot gov := docgov.NewGovernanceEngine(pool) q := `SELECT doc_id FROM document_records WHERE task_key <> '' ORDER BY doc_id` args := []any{} if strings.TrimSpace(cfg.DocID) != "" { q = `SELECT doc_id FROM document_records WHERE doc_id=$1` args = append(args, cfg.DocID) } rows, err := pool.Query(ctx, q, args...) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer rows.Close() report := struct { DryRun bool `json:"dry_run"` Live bool `json:"live"` Items []*docrevision.ReconcileResult `json:"items"` Legacy int `json:"legacy_drift"` Fork int `json:"content_fork"` Align int `json:"aligned"` }{DryRun: !cfg.Apply, Live: cfg.Live} for rows.Next() { var id string if err := rows.Scan(&id); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if _, gerr := gov.GetDocument(ctx, id); gerr != nil && cfg.Apply { fmt.Fprintf(os.Stderr, "preload document %s: %v\n", id, gerr) } res, rerr := svc.ReconcileDocument(ctx, docrevision.ReconcileRequest{ DocID: id, Apply: cfg.Apply, BackupDir: cfg.BackupDir, HashUpdater: gov, }) if res == nil { res = &docrevision.ReconcileResult{DocID: id, Detail: rerr.Error()} } else if rerr != nil && res.Detail == "" { res.Detail = rerr.Error() } switch res.Class { case docrevision.ProjectionLegacyCanonicalizationDrift: report.Legacy++ case docrevision.ProjectionContentFork: report.Fork++ case docrevision.ProjectionAligned: report.Align++ } report.Items = append(report.Items, res) if rerr != nil && cfg.Apply { enc, _ := json.MarshalIndent(report, "", " ") fmt.Fprintln(os.Stderr, string(enc)) os.Exit(1) } } 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 !cfg.Apply && report.Legacy+report.Fork > 0 { os.Exit(3) } }