package db import ( "context" "encoding/json" "fmt" "gopkg.in/yaml.v3" ) // ExportToYAML regenerates the three seed YAMLs from the DB (DR / version // control, plan D6). Output is deterministic: maps marshal with sorted keys // (yaml.v3 default for map[string]any) and lists are ordered by slug/name, // so export → ingest → export is byte-stable. // // Cognition-layer entities (signals, executions, patterns, …) are runtime // state, not inventory — they are excluded from the export. func ExportToYAML(ctx context.Context, pool *Pool) (map[string][]byte, error) { result := make(map[string][]byte) for name, fn := range map[string]func(context.Context, *Pool) (map[string]any, error){ "ontology.yaml": exportOntology, "inventory.yaml": exportInventory, "policy.yaml": exportPolicy, } { doc, err := fn(ctx, pool) if err != nil { return nil, fmt.Errorf("export %s: %w", name, err) } out, err := yaml.Marshal(doc) if err != nil { return nil, fmt.Errorf("marshal %s: %w", name, err) } result[name] = out } return result, nil } func exportOntology(ctx context.Context, pool *Pool) (map[string]any, error) { lifecycles := map[string]any{} rows, err := pool.Query(ctx, `SELECT id, states, default_state, terminal_states, transitions FROM lifecycle_defs ORDER BY id`) if err != nil { return nil, err } for rows.Next() { var id, def string var states, terminal []string var transitionsJSON []byte if err := rows.Scan(&id, &states, &def, &terminal, &transitionsJSON); err != nil { rows.Close() return nil, err } var transitions map[string]any if err := json.Unmarshal(transitionsJSON, &transitions); err != nil { rows.Close() return nil, fmt.Errorf("lifecycle %s transitions: %w", id, err) } lifecycles[id] = map[string]any{ "states": states, "default_state": def, "terminal_states": terminal, "transitions": transitions, } } rows.Close() if rows.Err() != nil { return nil, rows.Err() } entityTypes := map[string]any{} rows, err = pool.Query(ctx, `SELECT name, COALESCE(parent_type,''), is_abstract, domain, layer, COALESCE(description,''), COALESCE(lifecycle_id,''), attribute_schema FROM entity_types ORDER BY name`) if err != nil { return nil, err } for rows.Next() { var name, parent, dom, layer, desc, lc string var isAbstract bool var schemaJSON []byte if err := rows.Scan(&name, &parent, &isAbstract, &dom, &layer, &desc, &lc, &schemaJSON); err != nil { rows.Close() return nil, err } et := map[string]any{"domain": dom, "layer": layer} if parent != "" { et["parent"] = parent } if isAbstract { et["abstract"] = true } if desc != "" { et["description"] = desc } if lc != "" { et["lifecycle"] = lc } if len(schemaJSON) > 0 && string(schemaJSON) != "null" { var schema map[string]any if err := json.Unmarshal(schemaJSON, &schema); err == nil && schema != nil { et["attributes"] = schema } } entityTypes[name] = et } rows.Close() if rows.Err() != nil { return nil, rows.Err() } relTypes := map[string]any{} rows, err = pool.Query(ctx, `SELECT name, COALESCE(inverse,''), source_type, target_type, cardinality, COALESCE(description,'') FROM relationship_types ORDER BY name`) if err != nil { return nil, err } for rows.Next() { var name, inverse, src, tgt, card, desc string if err := rows.Scan(&name, &inverse, &src, &tgt, &card, &desc); err != nil { rows.Close() return nil, err } rt := map[string]any{"source": src, "target": tgt, "cardinality": card} if inverse != "" { rt["inverse"] = inverse } if desc != "" { rt["description"] = desc } relTypes[name] = rt } rows.Close() if rows.Err() != nil { return nil, rows.Err() } return map[string]any{ "version": 1, "lifecycles": lifecycles, "entity_types": entityTypes, "relationship_types": relTypes, }, nil } func exportInventory(ctx context.Context, pool *Pool) (map[string]any, error) { var entities []any rows, err := pool.Query(ctx, ` SELECT e.slug, e.type, e.name, COALESCE(e.state,''), e.attributes FROM entities e JOIN entity_types et ON et.name = e.type WHERE et.layer <> 'cognition' ORDER BY e.slug`) if err != nil { return nil, err } for rows.Next() { var slug, typ, name, state string var attrsJSON []byte if err := rows.Scan(&slug, &typ, &name, &state, &attrsJSON); err != nil { rows.Close() return nil, err } e := map[string]any{"slug": slug, "type": typ, "name": name} if state != "" { e["state"] = state } var attrs map[string]any if err := json.Unmarshal(attrsJSON, &attrs); err == nil && len(attrs) > 0 { e["attributes"] = attrs } entities = append(entities, e) } rows.Close() if rows.Err() != nil { return nil, rows.Err() } var rels []any rows, err = pool.Query(ctx, ` SELECT se.slug, te.slug, r.type, r.attributes FROM relationships r JOIN entities se ON se.id = r.source_id JOIN entities te ON te.id = r.target_id WHERE r.valid_to IS NULL ORDER BY r.type, se.slug, te.slug`) if err != nil { return nil, err } for rows.Next() { var src, tgt, typ string var attrsJSON []byte if err := rows.Scan(&src, &tgt, &typ, &attrsJSON); err != nil { rows.Close() return nil, err } rel := map[string]any{"source": src, "target": tgt, "type": typ} var attrs map[string]any if len(attrsJSON) > 0 && json.Unmarshal(attrsJSON, &attrs) == nil && len(attrs) > 0 { rel["attributes"] = attrs } rels = append(rels, rel) } rows.Close() if rows.Err() != nil { return nil, rows.Err() } return map[string]any{ "version": 1, "entities": entities, "relationships": rels, }, nil } func exportPolicy(ctx context.Context, pool *Pool) (map[string]any, error) { riskClasses := map[string]any{} rows, err := pool.Query(ctx, `SELECT name, COALESCE(description,''), approval_required, autonomy_allowed FROM risk_classes ORDER BY name`) if err != nil { return nil, err } for rows.Next() { var name, desc, approval string var autonomy bool if err := rows.Scan(&name, &desc, &approval, &autonomy); err != nil { rows.Close() return nil, err } rc := map[string]any{"approval_required": approval, "autonomy_allowed": autonomy} if desc != "" { rc["description"] = desc } riskClasses[name] = rc } rows.Close() if rows.Err() != nil { return nil, rows.Err() } var rules []any rows, err = pool.Query(ctx, ` SELECT COALESCE(ar.entity_type,''), ar.action, ar.risk_class, ar.autonomy_level, COALESCE(se.slug,'') FROM approval_rules ar LEFT JOIN entities se ON se.id = ar.scope_entity ORDER BY COALESCE(ar.entity_type,''), ar.action, COALESCE(se.slug,'')`) if err != nil { return nil, err } for rows.Next() { var et, action, rc, al, scope string if err := rows.Scan(&et, &action, &rc, &al, &scope); err != nil { rows.Close() return nil, err } rule := map[string]any{"action": action, "risk_class": rc, "autonomy_level": al} if et != "" { rule["entity_type"] = et } if scope != "" { rule["scope_entity"] = scope } rules = append(rules, rule) } rows.Close() if rows.Err() != nil { return nil, rows.Err() } settings := map[string]any{} rows, err = pool.Query(ctx, `SELECT key, value FROM autonomy_settings ORDER BY key`) if err != nil { return nil, err } for rows.Next() { var k, v string if err := rows.Scan(&k, &v); err != nil { rows.Close() return nil, err } settings[k] = v } rows.Close() if rows.Err() != nil { return nil, rows.Err() } return map[string]any{ "version": 1, "risk_classes": riskClasses, "approval_rules": rules, "autonomy_settings": settings, }, nil }