package app import ( "context" "fmt" "log/slog" "os" "github.com/dtoro/oikos/internal/core/ports" ) // SeedService is the bootstrap/DR use-case: ingest the seeds/*.yaml files // into the DB (idempotent, ontology-validated) and regenerate them from // the DB for version control. Absorbed the ingest orchestration that // lived in cmd/oikos runSeed and the export entry point in runExport // (Phase 7); the SQL stays behind ports.SeedRepository in the postgres // adapter. type SeedService struct { repo ports.SeedRepository } // NewSeedService wires the service. func NewSeedService(repo ports.SeedRepository) *SeedService { return &SeedService{repo: repo} } // Ingest reads the seed YAMLs from dir and ingests them in dependency // order: ontology (types) before inventory (entities/edges validated // against them), then policy, then knowledge — which is optional and // skipped silently when absent. Files whose content hash is unchanged // are no-ops at the repository layer. func (s *SeedService) Ingest(ctx context.Context, dir string) error { type step struct { file string ingest func(ctx context.Context, filename string, content []byte) (ports.SeedCounts, bool, error) optional bool } steps := []step{ {"ontology.yaml", s.repo.IngestOntology, false}, {"inventory.yaml", s.repo.IngestInventory, false}, {"policy.yaml", s.repo.IngestPolicy, false}, {"knowledge.yaml", s.repo.IngestKnowledge, true}, } for _, st := range steps { content, err := os.ReadFile(dir + "/" + st.file) if err != nil { if os.IsNotExist(err) && st.optional { slog.Info("knowledge seed not found, skipping") continue } return fmt.Errorf("read %s seed: %w", st.file, err) } counts, applied, err := st.ingest(ctx, st.file, content) if err != nil { return err } if !applied { continue } switch st.file { case "ontology.yaml": slog.Info("ontology ingested", "lifecycles", counts.Lifecycles, "entity_types", counts.EntityTypes, "relationship_types", counts.RelationshipTypes) case "inventory.yaml": slog.Info("inventory ingested", "entities", counts.Entities, "relationships", counts.Relationships) case "policy.yaml": slog.Info("policy ingested", "risk_classes", counts.RiskClasses, "approval_rules", counts.ApprovalRules, "autonomy_settings", counts.AutonomySettings) case "knowledge.yaml": slog.Info("knowledge ingested", "documents", counts.Documents, "investigations", counts.Investigations, "runbooks", counts.Runbooks) } } slog.Info("seed ingest complete") return nil } // Export regenerates the seed YAMLs from the DB (DR / version control). // Deterministic: the repository orders lists and sorts map keys so // export → ingest → export is byte-stable. func (s *SeedService) Export(ctx context.Context) (map[string][]byte, error) { return s.repo.Export(ctx) }