Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md) must give the use-cases-to-be their contract surface: driven-port interfaces, test fakes, the secrets interface moved into core, and the postgres package inside the adapters tree — before the first vertical slice (Phase 3) can wire a composition root. Change: - internal/core/ports: full driven-port catalog per plan §3.3 — repositories as transaction-scoped aggregates whose inputs carry derived checks, audit, and events (§3.6), plus CommandExecutor, TargetResolver, Checker, Secrets, EventPublisher, Provisioner. Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry, ExecResult) keep signatures off infrastructure; TypeTree aliases internal/ontology (pure over domain) until checkdefaults is absorbed. ReadModels intentionally not declared yet — it materializes with the Phase 3 slice and grows as report handlers rewire. - secrets.Backend is now an alias of ports.Secrets; implementations (Infisical, SOPS, Manager) unchanged. mcp's local secretBackend subset is deleted; tool constructors take ports.Secrets. - internal/db → internal/adapters/postgres (mechanical import rewrite; package identifier stays db until the Phase 3 repository split). sqlc.yaml, Makefile, golangci exclusions, and docs follow the move; make generate-check verified. - internal/adapters/ssh: Executor implements ports.CommandExecutor over the actuator dial pool + RunStreaming (10-min default timeout carried over from the httpapi path). - internal/adapters/remote: Resolver implements ports.TargetResolver delegating to internal/remote (still pool-based; drops onto ports.EntityRepository when repositories land in Phase 3 — documented transitional import). - internal/core/ports/portstest: importable fakes — in-memory EntityRepo (with check-then-act SetState, side-effect recording), RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction guards; tests. Risk: ports are declared ahead of implementations — signatures firm up per phase as slices land (documented in the package doc); the remote→postgres transitional import is explicit and dissolves in Phase 3. Verification: go vet, make test (race, 19 packages), generate-check, golangci on core+adapters — 0 issues; full-repo baseline down 365→344.
301 lines
7.6 KiB
Go
301 lines
7.6 KiB
Go
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
|
|
}
|