package cache import ( "sort" "strings" "sync" "time" "git.wntrmute.dev/mc/mcdoc/internal/render" ) // Document represents a single rendered markdown document. type Document struct { Repo string FilePath string // original path with .md extension URLPath string // path without .md extension Title string // first heading or filename HTML string Headings []render.Heading LastUpdated time.Time } // RepoInfo holds metadata about a repository and its documents. type RepoInfo struct { Name string Description string Docs []*Document CommitSHA string FetchedAt time.Time } // Cache stores rendered documents in memory with atomic per-repo swaps. type Cache struct { mu sync.RWMutex repos map[string]*RepoInfo ready bool } // New creates an empty cache. func New() *Cache { return &Cache{ repos: make(map[string]*RepoInfo), } } // SetRepo atomically replaces all documents for a repository. func (c *Cache) SetRepo(info *RepoInfo) { sortDocs(info.Docs) c.mu.Lock() c.repos[info.Name] = info c.mu.Unlock() } // RemoveRepo removes a repository from the cache. func (c *Cache) RemoveRepo(name string) { c.mu.Lock() delete(c.repos, name) c.mu.Unlock() } // SetReady marks the cache as ready to serve requests. func (c *Cache) SetReady() { c.mu.Lock() c.ready = true c.mu.Unlock() } // IsReady returns whether the initial fetch has completed. func (c *Cache) IsReady() bool { c.mu.RLock() defer c.mu.RUnlock() return c.ready } // ListRepos returns all cached repos, sorted by name. func (c *Cache) ListRepos() []*RepoInfo { c.mu.RLock() defer c.mu.RUnlock() repos := make([]*RepoInfo, 0, len(c.repos)) for _, r := range c.repos { repos = append(repos, r) } sort.Slice(repos, func(i, j int) bool { return repos[i].Name < repos[j].Name }) return repos } // GetRepo returns info for a specific repository. func (c *Cache) GetRepo(name string) (*RepoInfo, bool) { c.mu.RLock() defer c.mu.RUnlock() r, ok := c.repos[name] return r, ok } // GetDocument returns a document by repo name and URL path. func (c *Cache) GetDocument(repo, urlPath string) (*Document, bool) { c.mu.RLock() defer c.mu.RUnlock() r, ok := c.repos[repo] if !ok { return nil, false } for _, doc := range r.Docs { if doc.URLPath == urlPath { return doc, true } } return nil, false } // GetCommitSHA returns the cached commit SHA for a repo. func (c *Cache) GetCommitSHA(repo string) string { c.mu.RLock() defer c.mu.RUnlock() r, ok := c.repos[repo] if !ok { return "" } return r.CommitSHA } // docPriority defines the sort order for well-known filenames. var docPriority = map[string]int{ "README": 0, "ARCHITECTURE": 1, "RUNBOOK": 2, "CLAUDE": 3, } func sortDocs(docs []*Document) { sort.Slice(docs, func(i, j int) bool { pi, oki := docPriority[baseNameUpper(docs[i].URLPath)] pj, okj := docPriority[baseNameUpper(docs[j].URLPath)] switch { case oki && okj: return pi < pj case oki: return true case okj: return false default: return strings.ToLower(docs[i].URLPath) < strings.ToLower(docs[j].URLPath) } }) } func baseNameUpper(urlPath string) string { parts := strings.Split(urlPath, "/") base := parts[len(parts)-1] return strings.ToUpper(base) }