package execution import ( "encoding/json" "fmt" "time" "github.com/kami/hexis/internal/domain" "github.com/kami/hexis/internal/provider" "github.com/kami/hexis/internal/storage" ) type Engine struct { store storage.Interface registry *provider.Registry } func New(store storage.Interface, registry *provider.Registry) *Engine { return &Engine{store: store, registry: registry} } type ExecuteResult struct { Execution *domain.Execution `json:"execution"` } func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) { if req.IdempotencyKey != "" { existing, err := e.store.GetExecutionByIdempotencyKey(req.IdempotencyKey) if err == nil { return &ExecuteResult{Execution: existing}, nil } } capability, err := e.store.GetCapability(req.CapabilityID) if err != nil { return nil, fmt.Errorf("capability: %w", err) } if capability.TargetEntityID != "" && capability.TargetEntityID != req.TargetEntityID { return nil, domain.ErrCapabilityNotBound } prov, err := e.registry.Get(capability.Provider) if err != nil { return nil, fmt.Errorf("provider: %w", err) } now := time.Now().UTC() exec := &domain.Execution{ ID: domain.NewExecutionID(), CapabilityID: req.CapabilityID, TargetEntityID: req.TargetEntityID, EntityVersion: req.EntityVersion, Arguments: req.Arguments, RequestedBy: req.RequestedBy, Origin: req.Origin, IdempotencyKey: req.IdempotencyKey, Status: domain.ExecutionStarted, CorrelationID: req.CorrelationID, ResolutionEvidence: req.ResolutionEvidence, CreatedAt: now, UpdatedAt: now, } if exec.Arguments == nil { exec.Arguments = map[string]any{} } if exec.RequestedBy == nil { exec.RequestedBy = map[string]string{} } if exec.Origin == nil { exec.Origin = map[string]string{} } if err := e.store.CreateExecution(exec); err != nil { return nil, err } e.emitEvent(domain.EventExecutionStarted, exec.ID, map[string]any{ "capability_id": req.CapabilityID, "target_entity_id": req.TargetEntityID, "provider": capability.Provider, "correlation_id": req.CorrelationID, }) result, execErr := prov.Execute(capability, req) exec.UpdatedAt = time.Now().UTC() if execErr != nil { exec.Status = domain.ExecutionFailed exec.Error = execErr.Error() exec.Result = map[string]any{"error": execErr.Error()} e.emitEvent(domain.EventExecutionFailed, exec.ID, map[string]any{ "capability_id": req.CapabilityID, "error": execErr.Error(), }) } else { exec.Status = domain.ExecutionSucceeded exec.Result = result e.emitEvent(domain.EventExecutionSucceeded, exec.ID, map[string]any{ "capability_id": req.CapabilityID, }) } if err := e.store.UpdateExecution(exec); err != nil { return nil, err } return &ExecuteResult{Execution: exec}, nil } func (e *Engine) ValidateTarget(capabilityID, entityID string) error { cap, err := e.store.GetCapability(capabilityID) if err != nil { return err } if cap.TargetEntityID != "" && cap.TargetEntityID != entityID { return domain.ErrCapabilityNotBound } return nil } func (e *Engine) emitEvent(evtType domain.HexisEventType, entityID string, payload map[string]any) { e.store.AppendEvent(&domain.Event{ ID: domain.NewEventID(), Type: evtType, Timestamp: time.Now().UTC(), Payload: payload, }) } type ResolveRequest struct { Query string `json:"query"` Types []string `json:"types,omitempty"` } type ResolveResult struct { Status string `json:"status"` Candidates []map[string]any `json:"candidates,omitempty"` EntityID string `json:"entity_id,omitempty"` } var _ json.Marshaler = (*ResolveResult)(nil) func (r *ResolveResult) MarshalJSON() ([]byte, error) { m := map[string]any{"status": r.Status} if r.Candidates != nil { m["candidates"] = r.Candidates } if r.EntityID != "" { m["entity_id"] = r.EntityID } return json.Marshal(m) }