// Package inventory implements the Gaze query engine. // // Gaze is the "all-seeing eye" of the Coven. It answers: // // gaze install — list every file owned by an Essence // gaze tablet — show the y/n answers stored in the Tablet // gaze depends — print the live DAG of a spell's build tree // gaze essence — show which spell and config produced this blob // gaze whereis — reverse lookup: which spell owns /usr/bin/wget // gaze sbom [essence_id] — produce a CycloneDX SBOM on stdout package inventory import ( "fmt" "sort" "dcos.net/sorcery-go/pkg/dag" "dcos.net/sorcery-go/pkg/legal" "dcos.net/sorcery-go/pkg/state" "dcos.net/sorcery-go/pkg/tomb" ) // Inventory wraps the state and tomb so Gaze can answer queries fast. type Inventory struct { State *state.Manager Tomb *tomb.Tomb Graph *dag.Graph } // New returns an Inventory. func New(s *state.Manager, t *tomb.Tomb, g *dag.Graph) *Inventory { return &Inventory{State: s, Tomb: t, Graph: g} } // InstallList returns every file the spell variant currently owns. func (i *Inventory) InstallList(spell, variant string) ([]string, error) { return i.State.GetManifest(spell, variant) } // Depends walks the Graph from `spell` and returns a flat list of // dependencies (transitive). Optional+disabled edges are skipped. func (i *Inventory) Depends(spell string) ([]string, error) { if i.Graph == nil { return nil, fmt.Errorf("gaze: no graph loaded") } root, ok := i.Graph.Nodes[spell] if !ok { return nil, fmt.Errorf("gaze: %s not in grimoire", spell) } seen := make(map[string]bool) var walk func(*dag.Node) walk = func(n *dag.Node) { if seen[n.Name] { return } seen[n.Name] = true for _, e := range n.Edges { if e.Type == dag.OptionalDep && !e.Enabled { continue } walk(e.Target) } } walk(root) delete(seen, spell) out := make([]string, 0, len(seen)) for k := range seen { out = append(out, k) } sort.Strings(out) return out, nil } // EssenceInfo is the reverse lookup: given a file hash, find which spell // produced it. func (i *Inventory) EssenceInfo(hash string) (string, error) { if i.Tomb == nil { return "", fmt.Errorf("gaze: no tomb loaded") } all, err := i.Tomb.List() if err != nil { return "", err } for _, s := range all { for _, h := range s.Files { if h == hash { return s.SpellName + " (" + s.Version + ")", nil } } } return "", fmt.Errorf("gaze: no essence contains hash %s", hash) } // WhereIs reverse-lookup: given a filesystem path, find which spell owns it. // Uses the state DB's reverse index for O(1) lookup. func (i *Inventory) WhereIs(path string) (string, error) { if i.State == nil { return "", fmt.Errorf("gaze: no state loaded") } spell, variant, err := i.State.WhoOwns(path) if err != nil { return "", fmt.Errorf("gaze: %s: %w", path, err) } return spell + " (" + variant + ")", nil } // Sbom collects every component in the Tomb and returns it as a // legal.Component slice ready for CycloneDX / SPDX export. func (i *Inventory) Sbom() []legal.Component { if i.Tomb == nil { return nil } all, _ := i.Tomb.List() out := make([]legal.Component, 0, len(all)) for _, s := range all { out = append(out, legal.Component{ Name: s.SpellName, Version: s.Version, License: s.License, Hash: s.EssenceID, Purl: fmt.Sprintf("pkg:sorcery/%s@%s", s.SpellName, s.Version), }) } return out }