execution

package
v0.1.0-dev.20260223223859 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 23, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package execution orchestrates graph-based action execution.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyResults

func ApplyResults(g *op.Graph, results []*NodeResult)

ApplyResults updates node states from execution results.

func FillSlotsFromData

func FillSlotsFromData(slots, data map[string]any)

FillSlotsFromData fills unfilled slots from Context.Data. Slots already set by the caller or resolved from promises are not overwritten.

func NewHostProvider

func NewHostProvider(h host.Host) op.HostProvider

NewHostProvider wraps a host.Host in the op.HostProvider interface. Returns nil if h is nil.

func OrderNodes

func OrderNodes(nodes []*op.Node, edges []op.Edge) []*op.Node

OrderNodes returns nodes in execution order. Nodes with edges are topologically sorted; nodes without edges are sorted by path depth.

Types

type ActivationState

type ActivationState struct {
	Status    op.NodeStatus
	Timestamp string
	Error     string
}

ActivationState captures per-execution mutable state for a node.

For non-gather execution, activation state is inlined on the Node struct (Status). For gather's concurrent execution, ActivationState lives in a per-iteration map so that shared nodes are never mutated.

ActivationState is transient — it is discarded after results and undo state are captured.

type Conflict

type Conflict struct {
	Node         *op.Node
	Type         ConflictType
	ExistingPath string // For symlinks, where it points
	Message      string
}

Conflict represents a pre-flight detected conflict.

type ConflictResolution

type ConflictResolution int

ConflictResolution specifies how to handle conflicts during execution.

const (
	// ResolutionStop aborts execution on first conflict.
	ResolutionStop ConflictResolution = iota
	// ResolutionBackup moves conflicting files to timestamped backups.
	ResolutionBackup
	// ResolutionOverwrite removes conflicting files without backup.
	ResolutionOverwrite
	// ResolutionSkip skips conflicting files and continues.
	ResolutionSkip
)

type ConflictType

type ConflictType int

ConflictType describes the kind of conflict at a target path.

const (
	// ConflictNone indicates no conflict exists.
	ConflictNone ConflictType = iota
	// ConflictRegularFile indicates a regular file exists at target.
	ConflictRegularFile
	// ConflictDirectory indicates a directory exists at target.
	ConflictDirectory
	// ConflictForeignSymlink indicates a symlink pointing elsewhere exists.
	ConflictForeignSymlink
	// ConflictOurSymlink indicates our symlink already exists (no action needed).
	ConflictOurSymlink
)

type DependencyView

type DependencyView struct {
	// contains filtered or unexported fields
}

DependencyView provides dependency analysis for a single execution graph. It indexes the graph's edges to enable efficient dependency queries.

func NewDependencyView

func NewDependencyView(g *op.Graph) *DependencyView

NewDependencyView creates a DependencyView for the given graph.

func (*DependencyView) AllDependencies

func (v *DependencyView) AllDependencies(nodeID string) []string

AllDependencies returns the transitive closure of dependencies for a node. This includes all nodes that must complete before this node can start.

func (*DependencyView) AllDependents

func (v *DependencyView) AllDependents(nodeID string) []string

AllDependents returns the transitive closure of dependents for a node. This includes all nodes that directly or indirectly depend on this node.

func (*DependencyView) CriticalPath

func (v *DependencyView) CriticalPath() []string

CriticalPath returns the longest dependency chain in the graph. This represents the minimum sequential execution path.

func (*DependencyView) Dependents

func (v *DependencyView) Dependents(nodeID string) []string

Dependents returns the direct dependents of a node (nodes that wait for this one).

func (*DependencyView) DependsOn

func (v *DependencyView) DependsOn(nodeID string) []string

DependsOn returns the direct dependencies of a node (nodes that must complete first).

func (*DependencyView) EdgeCount

func (v *DependencyView) EdgeCount() int

EdgeCount returns the number of edges in the graph.

func (*DependencyView) Graph

func (v *DependencyView) Graph() *op.Graph

Graph returns the underlying graph.

func (*DependencyView) HasCycle

func (v *DependencyView) HasCycle() bool

HasCycle returns true if the graph contains a cycle.

func (*DependencyView) IndependentSets

func (v *DependencyView) IndependentSets() [][]string

IndependentSets returns groups of nodes that have no dependencies between them. Each set can be executed fully in parallel with other sets.

func (*DependencyView) Leaves

func (v *DependencyView) Leaves() []string

Leaves returns nodes with no dependents (final nodes in the graph).

func (*DependencyView) Node

func (v *DependencyView) Node(id string) *op.Node

Node returns the node with the given ID, or nil if not found.

func (*DependencyView) NodeCount

func (v *DependencyView) NodeCount() int

NodeCount returns the number of nodes in the graph.

func (*DependencyView) ParallelLevels

func (v *DependencyView) ParallelLevels() [][]string

ParallelLevels returns nodes grouped by execution level. Level 0 contains roots (can execute immediately). Level N contains nodes whose dependencies are all in levels < N. Within each level, nodes can execute in parallel.

func (*DependencyView) PathBetween

func (v *DependencyView) PathBetween(source, target string) []string

PathBetween returns the shortest path from source to target, or nil if none exists.

func (*DependencyView) Roots

func (v *DependencyView) Roots() []string

Roots returns nodes with no dependencies (can execute immediately).

func (*DependencyView) Subgraph

func (v *DependencyView) Subgraph(nodeIDs []string) *DependencyView

Subgraph returns a new DependencyView containing only the specified nodes and the edges between them.

func (*DependencyView) TopologicalOrder

func (v *DependencyView) TopologicalOrder() []string

TopologicalOrder returns nodes in a valid execution order. Nodes appear after all their dependencies. Returns nil if the graph has a cycle.

type EntryType

type EntryType string

EntryType distinguishes between package and file entries.

const (
	// EntryPackage represents a lore package lifecycle entry.
	EntryPackage EntryType = "package"
	// EntryFile represents a project file entry (link/copy/expand/decrypt).
	EntryFile EntryType = "file"
)

type ExecutorOptions

type ExecutorOptions struct {
	// DryRun prevents filesystem modifications.
	DryRun bool

	// Writer receives user-facing output.
	Writer io.Writer

	// Data holds tool-provided context (template vars, SOPS config, etc.).
	Data map[string]any

	// Host provides platform abstractions (package manager, service manager)
	// to action providers. Create with NewHostProvider(host.NewHost()).
	Host op.HostProvider

	// ConflictResolution specifies how to handle conflicts detected during preflight.
	ConflictResolution ConflictResolution

	// BackupSuffix is appended to backup filenames (default: ".writ-backup").
	BackupSuffix string
}

ExecutorOptions configures GraphExecutor behavior.

type FileEntry

type FileEntry struct {
	// Target is the relative target path (e.g., ".bashrc").
	Target string `json:"target" yaml:"target"`

	// Source is the absolute source path.
	Source string `json:"source" yaml:"source"`

	// Project this file belongs to.
	Project string `json:"project" yaml:"project"`

	// Layer is the repository layer (base, team, personal).
	Layer string `json:"layer,omitempty" yaml:"layer,omitempty"`

	// History of deployment actions, ordered by time.
	History []HistoryRecord `json:"history" yaml:"history"`
}

FileEntry represents a project file's deployment history.

func (*FileEntry) IsCopied

func (e *FileEntry) IsCopied() bool

IsCopied returns true if the latest action was a copy (not symlink).

func (*FileEntry) IsLinked

func (e *FileEntry) IsLinked() bool

IsLinked returns true if the latest action was a symlink.

func (*FileEntry) LastAction

func (e *FileEntry) LastAction() *HistoryRecord

LastAction returns the most recent action, or nil if no history.

func (*FileEntry) LastActionName

func (e *FileEntry) LastActionName() string

LastActionName returns the action name from the latest deployment.

type FileTree

type FileTree struct {
	// Root is the target root path (e.g., $HOME).
	Root string `json:"root" yaml:"root"`

	// Entries provides flat lookup by relative target path.
	Entries map[string]*FileEntry `json:"entries" yaml:"entries"`

	// Tree is the hierarchical view.
	Tree *FileTreeNode `json:"tree" yaml:"tree"`
}

FileTree provides both flat and hierarchical access to files.

func (*FileTree) CopiedFiles

func (t *FileTree) CopiedFiles() map[string]*FileEntry

CopiedFiles returns all entries that were copied (not symlinked).

func (*FileTree) ForProject

func (t *FileTree) ForProject(project string) map[string]*FileEntry

ForProject returns all file entries for a specific project.

func (*FileTree) LinkedFiles

func (t *FileTree) LinkedFiles() map[string]*FileEntry

LinkedFiles returns all entries that are symlinks.

func (*FileTree) Projects

func (t *FileTree) Projects() []string

Projects returns a list of all projects with files in the tree.

type FileTreeNode

type FileTreeNode struct {
	// Name is the filename or directory name.
	Name string `json:"name" yaml:"name"`

	// IsDir is true if this is a directory.
	IsDir bool `json:"is_dir" yaml:"is_dir"`

	// Entry is the file entry (nil for directories).
	Entry *FileEntry `json:"entry,omitempty" yaml:"entry,omitempty"`

	// Children are the child nodes (nil for files).
	Children map[string]*FileTreeNode `json:"children,omitempty" yaml:"children,omitempty"`
}

FileTreeNode represents a node in the target filesystem tree.

type GraphBuilder

type GraphBuilder interface {
	// Build creates an execution graph.
	// Implementations hold their configuration internally (set at construction).
	Build(ctx context.Context) (*op.Graph, error)
}

GraphBuilder is the interface for building execution graphs. Implementations are provided by tools (writ, lore) and create graphs from their respective inputs (file trees, package manifests, etc.).

type GraphExecutor

type GraphExecutor struct {
	// contains filtered or unexported fields
}

GraphExecutor executes action graphs.

func NewGraphExecutor

func NewGraphExecutor(opts ExecutorOptions) *GraphExecutor

NewGraphExecutor creates an executor with the given options.

func (*GraphExecutor) ExecutePhaseInner

func (e *GraphExecutor) ExecutePhaseInner(ctx *op.Context, g *op.Graph, phase *op.Phase, results map[string]any, stack *RecoveryStack) error

ExecutePhaseInner runs the inner nodes of a phase.

func (*GraphExecutor) Run

func (e *GraphExecutor) Run(ctx context.Context, g *op.Graph) error

Run executes all nodes in the graph, respecting ordering constraints. When the graph has phases, execution is delegated to RunPhased which implements the saga pattern with retry and rollback. Otherwise, nodes are processed in topological order (flat execution).

func (*GraphExecutor) RunNodes

func (e *GraphExecutor) RunNodes(ctx context.Context, nodes []*op.Node, edges []op.Edge) ([]*NodeResult, error)

RunNodes executes a slice of nodes with the given edges. This is a lower-level API for callers that don't have a full Graph.

func (*GraphExecutor) RunPhased

func (e *GraphExecutor) RunPhased(ctx context.Context, g *op.Graph) error

RunPhased executes a phased graph using the saga pattern. Phases are executed in order. Each phase runs its inner nodes via topological sort. On failure, completed phases are compensated in LIFO order (rollback).

Phases referenced as compensating actions (via Compensate fields) are skipped during the forward pass — they execute only during rollback.

func (*GraphExecutor) SetHooks

func (e *GraphExecutor) SetHooks(hooks *HookRegistry)

SetHooks sets the lifecycle hook registry for this executor.

type HistoryRecord

type HistoryRecord struct {
	// Timestamp is when this action occurred.
	Timestamp time.Time `json:"timestamp" yaml:"timestamp"`

	// Receipt is the filename of the receipt that recorded this.
	Receipt string `json:"receipt" yaml:"receipt"`

	// Tool is which tool created this record ("lore" or "writ").
	Tool string `json:"tool" yaml:"tool"`

	// Action performed: link, copy, render, decrypt, install, etc.
	Action string `json:"action" yaml:"action"`

	// Status of this action: completed, skipped, failed.
	Status op.NodeStatus `json:"status" yaml:"status"`
}

HistoryRecord represents a single action on an entry from a receipt.

type HookRegistry

type HookRegistry struct {
	// contains filtered or unexported fields
}

HookRegistry holds registered lifecycle hooks and provides fire methods. A nil *HookRegistry is safe to use — all fire methods are no-ops.

func NewHookRegistry

func NewHookRegistry() *HookRegistry

NewHookRegistry creates an empty hook registry.

func (*HookRegistry) FireNodeComplete

func (r *HookRegistry) FireNodeComplete(ctx *op.Context, nodeID string, result op.Result, err error)

FireNodeComplete notifies all hooks that a node has finished.

func (*HookRegistry) FireNodeStart

func (r *HookRegistry) FireNodeStart(ctx *op.Context, nodeID string, slots map[string]any)

FireNodeStart notifies all hooks that a node is about to execute.

func (*HookRegistry) FirePhaseComplete

func (r *HookRegistry) FirePhaseComplete(ctx *op.Context, phaseID string, err error)

FirePhaseComplete notifies all hooks that a phase has finished.

func (*HookRegistry) FirePhaseStart

func (r *HookRegistry) FirePhaseStart(ctx *op.Context, phaseID string)

FirePhaseStart notifies all hooks that a phase is about to execute.

func (*HookRegistry) Register

func (r *HookRegistry) Register(hook LifecycleHook)

Register adds a lifecycle hook to the registry.

type LifecycleHook

type LifecycleHook interface {
	OnNodeStart(ctx *op.Context, nodeID string, slots map[string]any)
	OnNodeComplete(ctx *op.Context, nodeID string, result op.Result, err error)
	OnPhaseStart(ctx *op.Context, phaseID string)
	OnPhaseComplete(ctx *op.Context, phaseID string, err error)
}

LifecycleHook receives events at phase and node boundaries during execution. Hooks are fire-and-forget — a hook panic is recovered and logged but does not fail the node or phase. Hooks run synchronously and must not block.

type NodeResult

type NodeResult struct {
	NodeID  string
	Status  ResultStatus
	Error   error
	Message string
}

NodeResult represents the outcome of executing a single node.

type PackageEntry

type PackageEntry struct {
	// Name is the package name (e.g., "docker").
	Name string `json:"name" yaml:"name"`

	// History of lifecycle actions, ordered by time.
	History []HistoryRecord `json:"history" yaml:"history"`
}

PackageEntry represents a lore package's lifecycle history.

func (*PackageEntry) LastAction

func (e *PackageEntry) LastAction() *HistoryRecord

LastAction returns the most recent action, or nil if no history.

type Plan

type Plan struct {
	// contains filtered or unexported fields
}

Plan provides binding functions for building an execution graph. Graph producers (writ tree builder, lore pipeline executor, LLM graph builder) use Plan to add actions to the graph. Each method returns the created node for edge construction.

In Starlark scripts, plan is a global:

def install(package, phase):
    plan.file.mkdir("/usr/local/bin")
    plan.file.link("/usr/local/bin/foo", source="/path/to/foo")

func NewPlan

func NewPlan(reg *op.ActionRegistry, project string) *Plan

NewPlan creates a new plan for building an execution graph.

func (*Plan) Backup

func (p *Plan) Backup(path string) *op.Node

Backup adds a backup action for an existing file.

func (*Plan) Copy

func (p *Plan) Copy(source, path string) *op.Node

Copy adds a file copy action.

func (*Plan) CopyWithMode

func (p *Plan) CopyWithMode(source, path string, mode os.FileMode) *op.Node

CopyWithMode adds a file copy action with explicit permissions.

func (*Plan) Decrypt

func (p *Plan) Decrypt(source string) *op.Node

Decrypt adds a decryption action.

func (*Plan) DependsOn

func (p *Plan) DependsOn(from, to *op.Node)

DependsOn adds an ordering edge: from must complete before to begins.

func (*Plan) Graph

func (p *Plan) Graph() *op.Graph

Graph returns the built execution graph.

func (p *Plan) Link(source, path string) *op.Node

Link adds a symlink creation action.

func (*Plan) Mkdir

func (p *Plan) Mkdir(path string) *op.Node

Mkdir adds a directory creation action.

func (*Plan) Remove

func (p *Plan) Remove(path string) *op.Node

Remove adds a file/directory removal action.

func (*Plan) Rename

func (p *Plan) Rename(source, path string) *op.Node

Rename adds a file/directory move action (git mv when possible).

func (*Plan) Render

func (p *Plan) Render(source string) *op.Node

Render adds a template rendering action.

func (p *Plan) Unlink(path string) *op.Node

Unlink adds a symlink removal action.

type PreflightResult

type PreflightResult struct {
	Conflicts   []Conflict
	AlreadyDone []Conflict // Symlinks that already point correctly
	Ready       []*op.Node // Nodes ready to deploy (no conflict)
}

PreflightResult contains the results of pre-flight conflict detection.

func Preflight

func Preflight(graph *op.Graph) *PreflightResult

Preflight performs pre-flight conflict detection without modifying anything. Only applies to nodes with file actions (link, copy).

func (*PreflightResult) HasConflicts

func (p *PreflightResult) HasConflicts() bool

HasConflicts returns true if any conflicts were detected.

type RecoveryEntry

type RecoveryEntry struct {
	// Node is the executed node (carries the CompensableAction action for Undo).
	Node *op.Node

	// UndoState is the state captured by Do, passed to Undo during rollback.
	UndoState op.UndoState
}

RecoveryEntry represents a successfully executed undoable node and the state needed to undo it. The executor pushes one entry per completed node that implements CompensableAction.

type RecoveryStack

type RecoveryStack struct {
	// contains filtered or unexported fields
}

RecoveryStack is a LIFO stack of recovery entries. The executor pushes entries as nodes complete and unwinds (pops + executes Undo) on failure.

func (*RecoveryStack) Entries

func (s *RecoveryStack) Entries() []RecoveryEntry

Entries returns a copy of the stack entries (bottom to top). Used for inspection and serialization.

func (*RecoveryStack) Len

func (s *RecoveryStack) Len() int

Len returns the number of entries on the stack.

func (*RecoveryStack) Push

func (s *RecoveryStack) Push(entry RecoveryEntry)

Push adds a recovery entry to the top of the stack.

func (*RecoveryStack) Unwind

func (s *RecoveryStack) Unwind(ctx *op.Context) []error

Unwind executes Undo on all entries in LIFO order. Undo failures do not stop the unwind — all entries are processed. Non-compensable actions (ErrNotCompensable) are logged and skipped.

type ResultStatus

type ResultStatus int

ResultStatus represents the execution status of a node.

const (
	// ResultPending means the node has not been processed yet.
	ResultPending ResultStatus = iota
	// ResultRunning means the node is currently executing.
	ResultRunning
	// ResultCompleted means the node executed successfully.
	ResultCompleted
	// ResultFailed means the node encountered an error.
	ResultFailed
	// ResultSkipped means the node was skipped (conflict, already deployed, etc.).
	ResultSkipped
)

func (ResultStatus) String

func (s ResultStatus) String() string

String returns a human-readable status label.

type StateView

type StateView struct {
	// Since is the start of the time window (inclusive).
	Since time.Time `json:"since" yaml:"since"`

	// Until is the end of the time window (inclusive).
	Until time.Time `json:"until" yaml:"until"`

	// ReceiptCount is the number of receipts included in this view.
	ReceiptCount int `json:"receipt_count" yaml:"receipt_count"`

	// Packages maps package names to their lifecycle history.
	Packages map[string]*PackageEntry `json:"packages" yaml:"packages"`

	// Files provides file entry access (flat and tree).
	Files *FileTree `json:"files" yaml:"files"`
}

StateView is a read-only view over multiple execution graphs. It represents "what we believe happened" over a time interval.

func (*StateView) Summary

func (v *StateView) Summary() (packages, links, copied int)

Summary returns counts of packages, linked files, and copied files.

type StateViewBuilder

type StateViewBuilder struct {
	// contains filtered or unexported fields
}

StateViewBuilder creates StateViews from receipts.

func NewStateViewBuilder

func NewStateViewBuilder(opts ViewOptions) *StateViewBuilder

NewStateViewBuilder creates a new builder with the given options.

func (*StateViewBuilder) Build

func (b *StateViewBuilder) Build(receiptsDir string) (*StateView, error)

Build loads all receipts from the directory and builds a StateView.

func (*StateViewBuilder) BuildFrom

func (b *StateViewBuilder) BuildFrom(graphs []*op.Graph) *StateView

BuildFrom creates a StateView from the given graphs.

type ViewOptions

type ViewOptions struct {
	// Since filters to receipts after this time (zero = no lower bound).
	Since time.Time

	// Until filters to receipts before this time (zero = no upper bound).
	Until time.Time

	// Tools filters to specific tools (empty = all tools).
	Tools []string
}

ViewOptions configures how the view is built.

Directories

Path Synopsis
Package flow implements flow-control actions for execution graphs.
Package flow implements flow-control actions for execution graphs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL