// Package modules provides the module registry for Silo. // Each module groups API endpoints, UI views, and configuration. // Modules can be required (always on) or optional (admin-toggleable). package modules import ( "fmt" "sort" "sync" ) // Module IDs. const ( Core = "core" Schemas = "schemas" Storage = "storage" Auth = "auth" Projects = "projects" Audit = "audit" Odoo = "odoo" FreeCAD = "freecad" Jobs = "jobs" DAG = "dag" Solver = "solver" Sessions = "sessions" ) // ModuleInfo describes a module's metadata. type ModuleInfo struct { ID string Name string Description string Required bool // cannot be disabled DefaultEnabled bool // initial state for optional modules DependsOn []string // module IDs this module requires Version string } // registry entries with their runtime enabled state. type moduleState struct { ModuleInfo enabled bool } // Registry holds all module definitions and their enabled state. type Registry struct { mu sync.RWMutex modules map[string]*moduleState } // builtinModules defines the complete set of Silo modules. var builtinModules = []ModuleInfo{ {ID: Core, Name: "Core PDM", Description: "Items, revisions, files, BOM, search, import/export", Required: true, Version: "0.2"}, {ID: Schemas, Name: "Schemas", Description: "Part numbering schema parsing and segment management", Required: true}, {ID: Storage, Name: "Storage", Description: "Filesystem storage", Required: true}, {ID: Auth, Name: "Authentication", Description: "Local, LDAP, OIDC authentication and RBAC", DefaultEnabled: true}, {ID: Projects, Name: "Projects", Description: "Project management and item tagging", DefaultEnabled: true}, {ID: Audit, Name: "Audit", Description: "Audit logging, completeness scoring", DefaultEnabled: true}, {ID: Odoo, Name: "Odoo ERP", Description: "Odoo integration (config, sync-log, push/pull)", DependsOn: []string{Auth}}, {ID: FreeCAD, Name: "Create Integration", Description: "URI scheme, executable path, client settings", DefaultEnabled: true}, {ID: Jobs, Name: "Job Queue", Description: "Async compute jobs, runner management", DependsOn: []string{Auth}}, {ID: DAG, Name: "Dependency DAG", Description: "Feature DAG sync, validation states, interference detection", DependsOn: []string{Jobs}}, {ID: Solver, Name: "Solver", Description: "Assembly constraint solving via server-side runners", DependsOn: []string{Jobs}}, {ID: Sessions, Name: "Sessions", Description: "Workstation registration, edit sessions, and presence tracking", DependsOn: []string{Auth}, DefaultEnabled: true}, } // NewRegistry creates a registry with all builtin modules set to their default state. func NewRegistry() *Registry { r := &Registry{modules: make(map[string]*moduleState, len(builtinModules))} for _, m := range builtinModules { enabled := m.Required || m.DefaultEnabled r.modules[m.ID] = &moduleState{ModuleInfo: m, enabled: enabled} } return r } // IsEnabled returns whether a module is currently enabled. func (r *Registry) IsEnabled(id string) bool { r.mu.RLock() defer r.mu.RUnlock() if m, ok := r.modules[id]; ok { return m.enabled } return false } // SetEnabled changes a module's enabled state with dependency validation. func (r *Registry) SetEnabled(id string, enabled bool) error { r.mu.Lock() defer r.mu.Unlock() m, ok := r.modules[id] if !ok { return fmt.Errorf("unknown module %q", id) } if m.Required { return fmt.Errorf("module %q is required and cannot be disabled", id) } if enabled { // Check that all dependencies are enabled. for _, dep := range m.DependsOn { if dm, ok := r.modules[dep]; ok && !dm.enabled { return fmt.Errorf("cannot enable %q: dependency %q is disabled", id, dep) } } } else { // Check that no enabled module depends on this one. for _, other := range r.modules { if !other.enabled || other.ID == id { continue } for _, dep := range other.DependsOn { if dep == id { return fmt.Errorf("cannot disable %q: module %q depends on it", id, other.ID) } } } } m.enabled = enabled return nil } // All returns info for every module, sorted by ID. func (r *Registry) All() []ModuleInfo { r.mu.RLock() defer r.mu.RUnlock() out := make([]ModuleInfo, 0, len(r.modules)) for _, m := range r.modules { out = append(out, m.ModuleInfo) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } // Get returns info for a single module, or nil if not found. func (r *Registry) Get(id string) *ModuleInfo { r.mu.RLock() defer r.mu.RUnlock() if m, ok := r.modules[id]; ok { info := m.ModuleInfo return &info } return nil } // ValidateDependencies checks that every enabled module's dependencies // are also enabled. Returns the first violation found. func (r *Registry) ValidateDependencies() error { r.mu.RLock() defer r.mu.RUnlock() for _, m := range r.modules { if !m.enabled { continue } for _, dep := range m.DependsOn { if dm, ok := r.modules[dep]; ok && !dm.enabled { return fmt.Errorf("module %q is enabled but its dependency %q is disabled", m.ID, dep) } } } return nil }