package infra import ( "fmt" "os" "path/filepath" "strings" "time" ) // VaultDoctorEntry holds the health report for a single vault. type VaultDoctorEntry struct { VaultName string `json:"vault_name"` VaultPath string `json:"vault_path"` ProjectID string `json:"project_id"` Issues []string `json:"issues"` // human-readable issues; empty = healthy IndexedFiles int `json:"indexed_files"` // 0 if no vault_index.db LastIndexedAt int64 `json:"last_indexed_at"` // unix seconds; 0 if N/A DiskFiles int `json:"disk_files"` // count via WalkDir (no hashing) Status string `json:"status"` // "ok" | "warning" | "error" } // VaultDoctor audits every vault declared in projects/*/vaults/vault.yaml under // repoRoot. For each vault it performs a series of checks (disk presence, layout, // index existence, staleness, drift) and returns a slice of VaultDoctorEntry. // // The function is read-only: it never writes to disk or any database. // Returns an error only if VaultManifestRead fails (manifest parse error). func VaultDoctor(repoRoot string) ([]VaultDoctorEntry, error) { entries, err := VaultManifestRead(repoRoot) if err != nil { return nil, fmt.Errorf("vault_doctor: read manifests: %w", err) } results := make([]VaultDoctorEntry, 0, len(entries)) for _, e := range entries { result := auditVault(e) results = append(results, result) } return results, nil } func auditVault(e VaultManifestEntry) VaultDoctorEntry { entry := VaultDoctorEntry{ VaultName: e.Name, VaultPath: e.Path, ProjectID: e.ProjectID, } // Resolve symlinks for disk checks realPath, err := filepath.EvalSymlinks(e.Path) if err != nil || realPath == "" { realPath = e.Path } // CHECK 1: directory_missing info, statErr := os.Stat(realPath) if statErr != nil || !info.IsDir() { entry.Issues = append(entry.Issues, "directory_missing") entry.Status = "error" return entry } // COUNT disk files (cheap walk — no hashing, no mime detection) diskCount := countDiskFiles(realPath) entry.DiskFiles = diskCount // CHECK 2: layout_missing / non_standard_layout hasData := dirExists(filepath.Join(realPath, "data")) hasKnowledge := dirExists(filepath.Join(realPath, "knowledge")) if !hasData && !hasKnowledge { // Check if it looks like a non-standard but intentional layout if hasNonStandardLayout(realPath) { entry.Issues = append(entry.Issues, "non_standard_layout") } else { entry.Issues = append(entry.Issues, "layout_missing") } } // CHECK 3: index_missing indexPath := filepath.Join(realPath, "vault_index.db") _, indexStatErr := os.Stat(indexPath) if indexStatErr != nil { entry.Issues = append(entry.Issues, "index_missing") entry.setWarningStatus() entry.setFinalStatus() return entry } // Open vault index (read-only) for checks 4 and 5 vdb, openErr := VaultIndexOpen(realPath) if openErr != nil { entry.Issues = append(entry.Issues, fmt.Sprintf("index_open_error: %v", openErr)) entry.setWarningStatus() return entry } defer vdb.Close() // Query indexed file count and max indexed_at var indexedCount int var maxIndexedAt int64 row := vdb.QueryRow(`SELECT COUNT(*), COALESCE(MAX(indexed_at), 0) FROM files`) if scanErr := row.Scan(&indexedCount, &maxIndexedAt); scanErr != nil { entry.Issues = append(entry.Issues, fmt.Sprintf("index_query_error: %v", scanErr)) } else { entry.IndexedFiles = indexedCount entry.LastIndexedAt = maxIndexedAt } // CHECK 4: index_stale — any file on disk newer than MAX(indexed_at) if maxIndexedAt > 0 { maxTime := time.Unix(maxIndexedAt, 0) if isIndexStale(realPath, maxTime) { entry.Issues = append(entry.Issues, "index_stale") } } // CHECK 5: index_drift — disk file count != indexed count if indexedCount != diskCount { entry.Issues = append(entry.Issues, fmt.Sprintf("index_drift: disk=%d indexed=%d", diskCount, indexedCount)) } // CHECK 6: empty_vault if diskCount == 0 { entry.Issues = append(entry.Issues, "empty_vault") } entry.setFinalStatus() return entry } // setWarningStatus sets status to warning if not already error. func (e *VaultDoctorEntry) setWarningStatus() { if e.Status != "error" { e.Status = "warning" } } // setFinalStatus derives the final Status from Issues. func (e *VaultDoctorEntry) setFinalStatus() { if e.Status == "error" { return } if len(e.Issues) == 0 { e.Status = "ok" } else { e.Status = "warning" } } // countDiskFiles walks realPath and counts regular files, excluding: // vault_index.db*, .git/, hidden files/dirs at any depth. func countDiskFiles(realPath string) int { count := 0 _ = filepath.WalkDir(realPath, func(path string, d os.DirEntry, err error) error { if err != nil { return nil } name := d.Name() // Skip hidden entries if strings.HasPrefix(name, ".") { if d.IsDir() { return filepath.SkipDir } return nil } // Skip .git if d.IsDir() && name == ".git" { return filepath.SkipDir } // Skip vault_index.db files if !d.IsDir() && (name == "vault_index.db" || name == "vault_index.db-shm" || name == "vault_index.db-wal") { return nil } if !d.IsDir() { count++ } return nil }) return count } // isIndexStale returns true if any regular file under realPath has an mtime // strictly after maxTime (excluding vault_index.db* and hidden files). func isIndexStale(realPath string, maxTime time.Time) bool { stale := false _ = filepath.WalkDir(realPath, func(path string, d os.DirEntry, err error) error { if err != nil || stale { return nil } name := d.Name() if strings.HasPrefix(name, ".") { if d.IsDir() { return filepath.SkipDir } return nil } if d.IsDir() && name == ".git" { return filepath.SkipDir } if !d.IsDir() { if name == "vault_index.db" || name == "vault_index.db-shm" || name == "vault_index.db-wal" { return nil } fi, statErr := d.Info() if statErr == nil && fi.ModTime().After(maxTime) { stale = true } } return nil }) return stale } // hasNonStandardLayout returns true when a vault directory contains // subdirectories that are clearly intentional but not data/knowledge. // Heuristic: any subdir at the vault root that is not data/knowledge. func hasNonStandardLayout(realPath string) bool { entries, err := os.ReadDir(realPath) if err != nil { return false } standardDirs := map[string]bool{"data": true, "knowledge": true, ".git": true} for _, e := range entries { if e.IsDir() && !standardDirs[e.Name()] && !strings.HasPrefix(e.Name(), ".") { return true } } return false }