package manifest // Package manifest writes per-directory .ai.json manifests: a compact, // deterministic enumeration of a knowledge directory's immediate entries for // human audit and AI orientation. The deterministic walk fills paths and kinds // only; descriptions are left empty and an opt-in AI pass (the Slice-D // `eeco refresh-manifest` verb, using the ManifestSummary prompt) fills them. import ( "encoding/json" "errors" "io/fs" "os" "path/filepath" "sort" ) // FileName is the per-directory manifest file name. const FileName = ".ai.json" // vcsDir is the version-control directory the manifest walk never descends // into or enumerates: the private workspace-history repo (and any nested VCS // dir) is engine plumbing, not knowledge. Pairs with the engine-workspace // exclusion so a refresh after `init` never writes into ajhahnde/.git. const vcsDir = ".git" // Item is one entry in a directory manifest. Desc and FindWhen are populated by // the opt-in AI enrichment pass, never by the deterministic walk. type Item struct { Path string `json:"path"` Kind string `json:"kind"` // "file" or "dir" Desc string `json:"desc,omitempty"` FindWhen string `json:"find_when,omitempty"` } // Manifest is the .ai.json document for one directory. type Manifest struct { Dir string `json:"dir"` Purpose string `json:"purpose,omitempty"` Items []Item `json:"items"` } // Build walks the immediate children of / and returns a // deterministic skeleton manifest (paths + kinds; descriptions empty), sorted // by path. The manifest file itself is never listed, so re-running over a // directory that already holds an .ai.json is idempotent. func Build(root, dir string) (Manifest, error) { target := filepath.Join(root, dir) entries, err := os.ReadDir(target) if err != nil { return Manifest{}, err } items := make([]Item, 0, len(entries)) for _, e := range entries { name := e.Name() if name == FileName || name == vcsDir { continue } if e.IsDir() { items = append(items, Item{Path: name + "/", Kind: "dir"}) continue } items = append(items, Item{Path: name, Kind: "file"}) } sort.Slice(items, func(i, j int) bool { return items[i].Path < items[j].Path }) return Manifest{Dir: dir, Items: items}, nil } // KnowledgeDirs walks the whole tree under userDir and returns every directory // in it — top-level knowledge dirs and their nested subdirectories alike — as // paths relative to userDir, sorted. userDir itself is never returned, and the // engine workspace (engineName, e.g. ".eeco") is excluded along with its entire // subtree. Separators in the returned paths are OS-native. A userDir that does // not exist yet yields an empty list and no error, so a refresh on an un-inited // repo is a clean no-op. func KnowledgeDirs(userDir, engineName string) ([]string, error) { var out []string err := filepath.WalkDir(userDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { return nil } if path == userDir { return nil } if d.Name() == engineName || d.Name() == vcsDir { return fs.SkipDir } rel, err := filepath.Rel(userDir, path) if err != nil { return err } out = append(out, rel) return nil }) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, nil } return nil, err } sort.Strings(out) return out, nil } // Subtree walks / and returns dir plus all of its nested // subdirectories as paths relative to userDir, sorted — so each is directly // usable with Build(userDir, x) and Write(userDir, x, m). No engine exclusion // applies, as the engine workspace is never passed as dir. func Subtree(userDir, dir string) ([]string, error) { root := filepath.Join(userDir, dir) var out []string err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { return nil } if d.Name() == vcsDir { return fs.SkipDir } rel, err := filepath.Rel(userDir, path) if err != nil { return err } out = append(out, rel) return nil }) if err != nil { return nil, err } sort.Strings(out) return out, nil } // Write marshals m to //.ai.json with stable indentation and a // trailing newline. func Write(root, dir string, m Manifest) error { data, err := json.MarshalIndent(m, "", " ") if err != nil { return err } data = append(data, '\n') return os.WriteFile(filepath.Join(root, dir, FileName), data, 0o644) }