phase 1: Go foundation — module, migrations, domain, seed ingest

Core deliverables:
- Go module github.com/dtoro/oikos (Go 1.26.3)
- cmd/oikos: single binary with role subcommands (migrate, seed, export)
- 6 SQL migrations: ontology meta-schema, entity instances (UUID+slug,
  blast_radius recursive function), operations (signals/checks/approvals),
  cognition (classifications/executions/feedback/patterns/skills), policy,
  observability (TimescaleDB hypertables + CAGGs + retention)
- Domain layer: entity, signal, execution, classification, pattern, skill,
  approval, check types + 11 sentinel errors + lifecycle state machines
- DB layer: pgx pool, SQL splitter (handles 94436 and -- comments), migration
  runner, seed ingest (ontology+inventory+policy) with content-hash dedup
- Config: env-based with defaults, secrets redaction
- Observability: slog JSON logger with debug mode
- Infrastructure: Makefile, docker-compose.yml, multi-stage Dockerfile
  (distroless, CGO_ENABLED=0)

Verified end-to-end against timescale/timescaledb:2.17.2-pg16:
- 6 migrations applied (65 SQL statements)
- Seeds ingested: 6 lifecycles, 59 entity types, 46 relationship types,
  111 entities, 144 relationships, 4 risk classes, 27 approval rules,
  9 autonomy settings
- Idempotent: second seed run is a no-op (content hash matches)

Bugs fixed during implementation:
- TimescaleDB CAGGs can't run in a transaction -> splitSQL() executes
  statements individually
- Semicolons in -- comments treated as separators -> comment handling
- YAML keys source/target didn't match code's source_type/target_type
- yaml.Marshal produced YAML for JSONB columns -> json.Marshal
This commit is contained in:
2026-07-07 01:07:26 +02:00
parent 55710bd254
commit aa2ca0ae6f
23 changed files with 1964 additions and 0 deletions

76
internal/domain/entity.go Normal file
View File

@@ -0,0 +1,76 @@
package domain
import (
"time"
)
// Entity is the core graph node — every object in the OS is an entity.
// Typed tables (signals, executions, etc.) reference entities(id) for
// indexed querying; graph edges live in the relationships table.
type Entity struct {
ID UUID
Slug string
Type string
Name string
State string
Attributes map[string]any
MaintenanceUntil *time.Time
Version int
CreatedAt time.Time
UpdatedAt time.Time
}
// EntityType is the meta-schema entry defining what entities can exist.
type EntityType struct {
Name string
ParentType string
IsAbstract bool
Domain string
Layer string
Description string
LifecycleID string
AttributeSchema map[string]any
SchemaVersion int
Status string
}
// RelationshipType defines a typed edge between entity types.
type RelationshipType struct {
Name string
Inverse string
SourceType string
TargetType string
Cardinality string
Description string
}
// LifecycleDef is the state machine for an entity type.
type LifecycleDef struct {
ID string
States []string
DefaultState string
TerminalStates []string
Transitions map[string]map[string]TransitionReq
}
// TransitionReq holds the named preconditions for a lifecycle transition.
type TransitionReq struct {
Requires []string `json:"requires"`
}
// Relationship is a typed edge between two entities.
type Relationship struct {
SourceID UUID
TargetID UUID
Type string
Attributes map[string]any
ValidFrom time.Time
ValidTo *time.Time
}
// UUID is a type alias for UUID values. Using string for simplicity;
// the DB layer uses pgx's UUID type. Conversion happens at the boundary.
type UUID string
// IsNil returns true if the UUID is empty.
func (u UUID) IsNil() bool { return u == "" }