op

package
v0.1.0-dev.20260827023101 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Overview

Package op owns the concrete graph data model shared by the execution engine, Starlark layer, and CLI tools.

Core types

  • Graph: a directed graph of nodes and edges representing work to be done.
  • Node: a single unit of work with an action to execute.
  • Edge: a dependency relationship between nodes.

Graph lifecycle

Graph is immutable: a re-executable plan that carries no per-execution state. RuntimeEnvironment is the mutable counterpart, scoped to one execution; it owns every per-run mutation (catalog state, results, variable resolution, recovery stack, status). A run produces a receipt (*RecoveryStack) — the audit trail of dispatches and their compensations — that, paired with the graph, suffices to restart execution where it left off.

Index

Constants

View Source
const (
	// GraphKind is the canonical artifact-type identifier stamped onto every [Graph].
	GraphKind = "com.noblefactor.DevLore.Graph"

	// GraphSchemaVersion is the current graph serialization-format version.
	GraphSchemaVersion = 1
)

Variables

View Source
var (
	// ErrNilGraph is the sentinel error returned by [NewGraphExecutor] and [ResumeExecutor] when the caller
	// passes a nil *Graph. Surfaces through the assert.NonZero precondition; declared here so callers can
	// match the specific shape via errors.Is when they need to distinguish nil-graph from other errors.
	ErrNilGraph = errors.New("expected non-nil Graph")

	// ErrPaused is the sentinel error returned by [GraphExecutor.Run] when the run halted because
	// [GraphExecutor.Pause] was called. The executor's [RunStatus.Phase] is [PhasePaused] on this exit;
	// callers can take a [*Trace] and resume later via [ResumeExecutor].
	ErrPaused = errors.New("execution paused")

	// ErrStopped is the sentinel error returned by [GraphExecutor.Run] when the run halted because
	// [GraphExecutor.Stop] was called. Unlike a pause, Run unwinds (compensating completed work) and lands
	// [PhaseStopped] — a terminal, non-resumable exit; the deliberate-halt terminal is `stopped × healthy × stopped`
	// (or `stopped × compensation_failed` when the unwind itself fails).
	ErrStopped = errors.New("execution stopped")
)
View Source
var ErrClaimKindMismatch = errors.New("claim asserts a different kind than the path holds")

ErrClaimKindMismatch marks an unmet claim whose path holds an entry of the WRONG KIND rather than no entry at all (ruled 2026-08-23).

The distinction decides tolerance. MissingResourcePolicyIgnore means "the goal already holds" — a coherent thing to say about absence, and an incoherent one about a surprise: if a claim asserted a symbolic link and the path holds a hand-written regular file, the goal plainly does not hold and something unexpected is there. **A kind mismatch therefore stops under every policy.** A regular file is not a directory is not a symbolic link.

The concrete case this was ruled from: `writ decommission` removes what its own runs deployed, under `on_missing=ignore` so that a hand-removed target decommissions as recorded history. A deployed link the user replaced with a real file must be REFUSED rather than deleted — and without the distinction, tolerance would have deleted it.

View Source
var ErrNotCompensable = errors.New("action is not compensable")

ErrNotCompensable signals that "Do" acknowledges rollback but cannot undo its effect.

The executor logs a warning and continues unwinding.

View Source
var ErrRecoverySourceNotFound = errors.New("recovery source not found")

ErrRecoverySourceNotFound is returned by RecoverySite.RestoreFile when no archive exists for the supplied recoveryID.

Compensation paths use this sentinel to distinguish "the receipt was committed but no archive was made" (a forward action that created new state without displacing existing state) from genuine restore failures. In the former case the caller silently treats the missing archive as a no-op; in the latter it propagates.

View Source
var (
	// ErrUnimplemented is returned by [op.ResourceBase.Digest] as a default. Concrete Resource types that need a
	// working Digest (every type save sentinels) must override [Resource.Digest] — content hashing is type-specific
	// (full file sha256, HEAD commit composition, last-observed body hash, projected from the URI for CAS, etc.).
	ErrUnimplemented = errors.New("op: unimplemented")
)
View Source
var ReceiverRegistry = sync.OnceValue(newReceiverRegistry)

ReceiverRegistry is the process-wide registry used for environment-free type resolution during starlark projection. It is built once from the announced set on first use; every Announce* runs at package init, so the snapshot is complete before any projection occurs.

Functions

func AnnounceProvider

func AnnounceProvider(providerType reflect.Type, roles ProviderRole, construct ProviderConstructor, methods map[string]MethodMetadata)

AnnounceProvider registers a provider with its roles and per-method metadata.

Called in init(). Roles are declared via ProviderRole flags: RoleModule for immediate-mode starlark globals, RoleAction for plan-mode graph node creation.

Companion methods on the provider type — [Method.Plan] via <Name>Planned, Method.Undo via Compensate<Name> — are discovered automatically by reflection in NewProviderReceiverType. No registration is required.

Parameters:

  • `providerType`: the provider's reflect.Type.
  • `roles`: the provider's declared roles.
  • `construct`: creates a provider instance from RuntimeEnvironment.
  • `methods`: codegen-emitted MethodMetadata per Go method, keyed by the method's Go name.

func AnnounceResource

func AnnounceResource(
	resourceType reflect.Type,
	construct ResourceConstructor,
	methodParameters map[string][]string,
	sourceTypes ...reflect.Type,
)

AnnounceResource registers a resource type.

Called in init(). Resources are always RoleResource — they cannot be actions or modules. They are data types constructed by coercing a raw value (e.g., a string path becomes a file.Resource).

Parameters:

  • `resourceType`: the resource's reflect.Type.
  • `construct`: coerces a raw value into the typed resource.
  • `methodParameters`: starlark parameter names per Go method (for attribute access).
  • `sourceTypes`: Go source types the resource is constructed from (e.g. `*starlark.Function`); each is registered as a `byType` key so [receiverRegistry.ConstructorForSource] resolves the constructor from a source value.

func AnnounceType

func AnnounceType(goType reflect.Type, methods map[string]MethodMetadata)

AnnounceType registers a bare receiver type for an arbitrary Go struct.

Called in init(). This is for Go types that need method dispatch in starlark but are neither providers nor resources (e.g., Go AST types returned by the goast provider). The receiver type has no constructor and no roles — it exists solely so marshalReflect can wrap instances with method dispatch.

Parameters:

  • `goType`: the Go struct's reflect.Type.
  • `methods`: codegen-emitted MethodMetadata per Go method, keyed by the method's Go name.

func CamelToSnake

func CamelToSnake(s string) string

CamelToSnake converts a CamelCase Go identifier to snake_case.

Parameters:

  • `s`: the CamelCase string (e.g., "WriteText", "ReadBytes").

Returns:

  • `string`: the snake_case equivalent (e.g., "write_text", "read_bytes").

func CanonicalResourceTypeID

func CanonicalResourceTypeID(t reflect.Type) string

CanonicalResourceTypeID returns the id a receipt records for a resource of type `t`.

The announced identity — [typeIDOf], which drops any pointer — rather than the Go type's canonical id, which carries one. op.ResourceBase stores exactly this at construction, and [canonicalIDOf] reads it back off the value, so a test or a tool that needs the id WITHOUT a value in hand derives it here rather than reimplementing the rule.

Parameters:

  • `t`: a resource type, pointer or interface.

Returns:

  • `string`: the canonical type id.

func ContentAddressedPath

func ContentAddressedPath(resource Resource) fsroot.Path

ContentAddressedPath returns the sharded on-disk location of a content-addressed resource's stored bytes.

The layout is `.devlore/<package>/<type>/<algo>/<first-two-hex>/<hex>`, derived entirely from the resource's own base: the run's root, the `<algo>:<hex>` reachability URI, and the type id. It therefore computes the path for WHATEVER resource it is handed — a function pack lands under `.devlore/function/resource/` because function's type id says so, not because any particular provider placed it there.

That generality is why this lives here rather than in the provider that happened to need it first. `.devlore` is the framework's directory — RecoverySite already owns `.devlore/recovery` — and the parameter is Resource, so no provider has to depend on another to find its own content.

Composition goes through fsroot, which owns path and root questions; nothing here joins strings by hand.

Parameters:

  • `resource`: any resource whose reachability URI is `<algo>:<hex>`.

Returns:

  • `fsroot.Path`: the content-addressed path, or the zero Path when there is no root or the URI is not in `<algo>:<hex>` form. A zero Path is the caller's signal that this resource is not content-addressed.

func ContentAddressedReader

func ContentAddressedReader(resource Resource) (io.ReadCloser, error)

ContentAddressedReader opens a content-addressed resource's stored bytes, memory-mapped.

Each call opens a new mmap; the caller must Close the returned reader, which unmaps the file. The content is never held in the Go heap.

Parameters:

Returns:

  • `io.ReadCloser`: a reader over the full archived content.
  • `error`: no content-addressed path (nothing archived), or the mapping failed.

func Convert

func Convert(runtimeEnvironment *RuntimeEnvironment, value any, target reflect.Type) (any, error)

Convert projects a Go value into the target type via the type-matching cascade.

Convert is the single source of truth for Go↔Go projection in the framework. Every starlark-bridge entry point (wrapper extraction, plan-mode slot fill, immediate-mode dispatch) and method-dispatch site (Method.Invoke) routes through here so type-matching semantics stay in one place. Convert itself is context-blind: graph dispatch precedes it with identity resolution at Method.Invoke's seam ([resolveDispatchResource], 4-resource-management.md §5.6) — a resource-typed slot value there resolves through the run catalog and never reaches the construction steps below.

The cascade:

  1. Identity — value's type is the target type. Return as-is.
  2. Assignability — value's underlying type is assignable to target (reflect.Type.AssignableTo).
  3. Slice element conversion — both source and target are slices; recurse element-wise.
  4. Map element conversion — both source and target are maps; recurse key-and-value-wise.
  5. Source-side opt-in — value implements SourceConverter and advertises the target type.
  6. Registered Resource construction — target implements Resource, and a constructor is registered in [RuntimeEnvironment.ReceiverRegistry]; the constructor is run with (runtimeEnvironment, value).
  7. Target-side opt-in — fresh target probe implements TargetConverter and advertises the source type.
  8. Text unmarshal — string source into a target (or its pointer) implementing encoding.TextUnmarshaler, e.g. time.Time; the target reconstructs itself from the text bytes.
  9. Struct hydration — map source into a struct target; each exported field is filled from the map by its `json`/`yaml` tag (or field name), recursing through Convert.
  10. Error — no path through the cascade succeeds.

Parameters:

  • `runtimeEnvironment`: the ambient RuntimeEnvironment. Step 6 uses its [Registry] for registered Resource construction.
  • `value`: the source value to project. `nil` yields the zero value of `target`.
  • `target`: the [`reflect.Type`] of the desired result.

Returns:

  • `any`: the projected value, ready to assign to a target of type `target`.
  • `error`: non-nil if no path through the cascade succeeds.

func Defer

func Defer[R any, PR interface {
	*R
	Resource
}](runtimeEnvironment *RuntimeEnvironment) PR

Defer constructs a placeholder instance of *R with a deferred tag URI — empty <specific>, typeID set to *R's canonical Go type id.

Use at plan time when a Resource's identity is not known until the producing node has executed. The returned value is a freshly allocated *R whose embedded ResourceBase has been seeded by NewResourceBase against the deferred identity.

Type parameters:

  • R: the struct type of the Resource (e.g., yaml.Resource).
  • PR: the pointer type *R that satisfies Resource. The "*R; Resource" constraint is statically enforced at the call site; invalid combinations fail to compile.

Call sites must spell both parameters:

r := op.Defer[yaml.Resource, *yaml.Resource](runtimeEnvironment)

func ExtractTagSpecific

func ExtractTagSpecific(value string) (specific, typeID string, err error)

ExtractTagSpecific parses a canonical tag URI and returns its scheme-specific payload and fragment.

Returns an error when s lacks the tag URI prefix, is missing the '#' delimiter, or has an empty fragment. An empty specific is valid and denotes the deferred ("known-at-execution") form.

Parameters:

  • `value`: the URI to parse.

Returns:

  • `specific`: the scheme-specific payload (this may be empty and indicates that it's unknown at the moment).
  • `typeID`: the fragment — the canonical Go type id of the Resource type.
  • `err`: non-nil on any syntactic defect.

func GenerateNodeID

func GenerateNodeID(prefix string, components ...string) string

GenerateNodeID creates a unique node ID with the given prefix and components.

func GitStyleChecksum

func GitStyleChecksum(objectType string, content []byte) string

GitStyleChecksum computes a checksum modeled on git's object hashing.

Git hashes objects as HASH("<type> <length>\0<content>"). See Pro Git, Chapter 10 — Git Objects. The default hash is SHA-1, with SHA-256 opt-in per repository since git 2.29 via `git init --object-format=sha256`. Both variants share the same header format and differ only in the hash function.

This function uses SHA-256. When `objectType` is a real git object type (`blob`, `tree`, `commit`, `tag`) and `content` is in git's canonical form for that type, output matches `git hash-object` against a SHA-256 repository. For custom `objectType` values (e.g., `graph`), output is a stable, content-derived identifier in the git tradition without a git-compatible counterpart.

Parameters:

  • `objectType`: the object type label embedded in the header.
  • `content`: the content to hash.

Returns:

  • `string`: the canonical "sha256:<hex>" checksum string.

func IsTruthy

func IsTruthy(value any) bool

IsTruthy reports whether `value` is truthy under Python / Starlark truth semantics.

Mirrors starlark.Value.Truth() over the Go-native values the converter produces, so every truthiness test — a decision node's GuardResult, a flow.wait_until poll, an [OnRetry] / [OnError] handler verdict — evaluates the same way whether the tested value was projected from a Starlark value or produced as a resolved Go value:

  • nil — and any typed-nil pointer, function, or channel — is falsy.
  • `bool`: false is falsy; true is truthy.
  • numbers (every integer width, `float32`, `float64`): zero is falsy; non-zero is truthy.
  • `string`, slices, arrays, maps: empty is falsy; non-empty is truthy.
  • structs: the zero value is falsy; anything else is truthy.
  • anything else (Resource, non-nil pointers): truthy.

Parameters:

  • `value`: the value whose truthiness routes the caller — a decision node's result, a poll result, or a handler's return.

Returns:

  • `bool`: true if `value` is truthy under the rules above.

func RegisterDefaultFunc

func RegisterDefaultFunc(name string, fn DefaultFunc)

RegisterDefaultFunc adds a function to the package-level deferred-default registry under the given name.

Intended caller is package init; the registry is conceptually static for the process lifetime. Re-registration of the same name panics — registration is a one-time act, and accidental duplicate registration almost always indicates an init-order bug or a copy-paste error.

Parameters:

  • name: the identifier as it appears in directive expressions (`{{ name args }}`). Conventionally lowercase ASCII; the parser is case-sensitive.
  • fn: the function to invoke at slot-fill time. Must be non-nil.

Panics:

  • If name is empty.
  • If fn is nil.
  • If name is already registered.

func RegisterPlanPathNormalizer

func RegisterPlanPathNormalizer(t reflect.Type, normalize PlanPathNormalizer)

RegisterPlanPathNormalizer registers `normalize` as the plan-space grammar for resource type `t`.

Called from provider init alongside the type's announcement. Registering the value type covers the pointer spelling and vice versa — lookup tries both forms.

Parameters:

  • `t`: the resource's reflect.Type (value or pointer form).
  • `normalize`: the scheme's plan-space normalizer.

func RegisterResourceImplementation

func RegisterResourceImplementation(resourceInterface, implementation reflect.Type)

RegisterResourceImplementation designates `implementation` as the concrete struct behind the sealed resource interface `resourceInterface`.

A sealed resource is an exported interface over an unexported struct, so nothing outside the provider's package can name the struct — including the generated announcement, which lives in a sibling package and can reach only exported identifiers. This is how the struct crosses that boundary: the provider package registers it from its own init, and AnnounceResource resolves it when the interface is announced.

**Ordering is guaranteed by the language, not by convention.** The generated package imports the provider package, and Go initializes imported packages first, so this registration always precedes the announcement that consumes it.

The two types serve different roles and cannot be collapsed. The interface supplies the canonical type id — the URI fragment a saved document carries — while the struct supplies everything reflection needs: the method set, the dispatch target, and the key `marshalReflect` looks up when it wraps a returned value.

Distinct from RegisterResourceMint, which answers a different question: what a bare authored string claims as. The two coincide only for an interface with exactly one implementation. `file` designates `*file.AnyKind` as its mint while having four implementations, so conflating them would be wrong there.

Parameters:

  • `resourceInterface`: the provider's sealed resource interface type.
  • `implementation`: the concrete pointer type implementing it.

func RegisterResourceMint

func RegisterResourceMint(resourceInterface, mint reflect.Type)

RegisterResourceMint designates `mint` as the concrete type an authored string claims as when it is bound to a parameter typed by the resource interface `resourceInterface` (docs/architecture/4-resource-management.md §5.7 rule 6, amended 2026-08-23).

A claim asserts a kind — "claims are true when made" needs a kind to be true about — and an interface asserts none, which is why an authored string bound to one is refused by default. A scheme with a kind axis resolves that by naming the claim that deliberately asserts nothing: `file` designates `*file.AnyKind`, whose assertion is existence alone and which resolves to the observed kind at activation.

**The designation lives on the interface, once.** Not per parameter: two methods taking the same slot type must claim the same way, or the same authored path would mean different intent depending on which method received it. An interface with no designation keeps the refusal — the author states a kind or feeds a discovery.

Called from provider init alongside the type's announcement.

Parameters:

  • `resourceInterface`: the provider's resource interface type.
  • `mint`: the concrete resource type an authored string claims as.

func RenderError

func RenderError(format string, args []any, kwargs map[string]any) error

RenderError formats a Go template string with positional args and keyword args, returning the result as an error.

Template data available to the format string:

{{ .key }}          — kwargs value
{{ index .Args 0 }} — positional arg by index

If the format string contains no template directives, it passes through as a plain string.

func SaveGraph

func SaveGraph(dst fsroot.Dir, path fsroot.Path, graph *Graph, format string) error

SaveGraph encodes a graph and writes it through `dst`.

The write-side complement of LoadGraph, and the reason the pair exists: a graph knows how to render itself (Graph.MarshalJSON, Graph.MarshalYAML) and LoadGraph knows how to rebuild itself, so persistence has no business inferring a rendering from a filename suffix. The format is stated here, as LoadGraph states it.

The root is received, never constructed (#405, phase 2b): whoever owns the store owns the tree it is written into. Mode 0600 is the artifact policy — a graph is signed material, and its confidentiality does not vary by caller.

Parameters:

  • `dst`: the tree the document is written into, opened by the caller.
  • `path`: the destination within `dst`.
  • `graph`: the graph to persist. Must not be nil.
  • `format`: "json" or "yaml" (or "yml") — case-insensitive, matching LoadGraph.

Returns:

  • `error`: non-nil if the format is unsupported, encoding fails, or the write fails.

func SaveTrace

func SaveTrace(dst fsroot.Dir, path fsroot.Path, trace *Trace) error

SaveTrace stamps a trace's checksum, encodes it, and writes it through `dst`.

The write-side complement of LoadTrace, and it stamps deliberately: LoadTrace refuses a document carrying no checksum, so a save that left stamping to the caller could write a document nothing can read — a failure that surfaces at the next load rather than at the write. Stamping is idempotent, because Trace.CanonicalContent excludes the checksum field.

YAML only, matching LoadTrace. The root is received, never constructed (#405, phase 2b), and mode 0600 is the artifact policy: a trace is signed material and its confidentiality does not vary by caller.

Signing remains the caller's, because the signer is: the checksum belongs to the artifact, the key belongs to whoever is publishing it.

Parameters:

  • `dst`: the tree the document is written into, opened by the caller.
  • `path`: the destination within `dst`.
  • `trace`: the trace to persist. Must not be nil.

Returns:

  • `error`: non-nil if stamping, encoding, or the write fails.

func SerializeGraphs

func SerializeGraphs(w io.Writer, graphs []*Graph) (err error)

SerializeGraphs serializes the graphs to w as one YAML document stream.

This is the write-side complement of LoadGraph: the framework owns every aspect of the rendering — the two-space indentation, the multi-document framing, and the folding of an encoder-close failure into the returned error. Callers choose only the destination writer.

Parameters:

  • `w`: the destination writer.
  • `graphs`: the graphs to serialize, in order.

Returns:

  • `err`: a serialization or encoder-close failure, or nil on success.

func ValidateGraph

func ValidateGraph(g *Graph) error

ValidateGraph asserts the assembled graph satisfies the plan-time invariants every executable unit must hold before execution.

Checks performed:

  • Required-parameter coverage: for each *Node, and each *Subgraph whose Action is non-nil, every required parameter of the bound Method has a slot entry. Optional, Variadic, and Kwargs parameters are exempt — Optional may be supplied or omitted; Variadic and Kwargs absorb whatever is or is not supplied.
  • Bubble-up consistency: triggers Graph.Parameters to drive [Subgraph.mergeBubbled] across every level. Any same-named variable declared with incompatible types across child slots surfaces here as one or more violations joined into the returned error.
  • Plan-time type check: for every slot bound to a PromiseBinding, walks producer → consumer in the graph, looks up the producer's declared output type (Method.ResultType) and the consumer's slot type (Method.ParameterByName.Type), then consults [typesAreInterconvertible] to decide whether Convert would succeed at dispatch. Mismatches surface here as plan-time errors so ill-typed promise bindings never reach execution.

ValidateGraph is the single source of truth for both boundary checks:

  • The planning path calls it as the final step of plan.Provider.Assemble.
  • The document-form load path calls it after [Graph.Rebind]'s linkActions resolves pending action references through the registry. The loader (e.g., plan.Provider.Load) orders Unmarshal -> Rebind -> ValidateGraph.

Action-binding is a prerequisite. A loaded graph in its post-Unmarshal, pre-Rebind state carries unresolved action references in `pendingAction` and has no methods to validate against; calling ValidateGraph in that state reports every unit as having a nil action. Callers must Rebind first.

Parameters:

  • `g`: the graph to validate. A nil graph or a graph with a nil Root is treated as empty (no error).

Returns:

  • `error`: an errors.Join of all violations found, or nil when the graph is valid. Each joined entry is a single human-readable string identifying the unit and the violation; callers that want structured handling can Unwrap the join.

Types

type Action

type Action interface {
	FullName() string
	Name() ActionName
	Method() *Method
	Params() []Parameter
	Do(activationRecord *ActivationRecord) (Result, Compensator, error)
}

Action is an infallible unit of work: Do returns (result, nil, nil).

**Infallible, not effect-free.** The distinction matters because this doc comment used to claim the latter, and ten of the twenty-two methods reaching this tier contradict it — `platform`'s five report the host, and `ui`'s six write to a terminal. They arrive here because reading a supplied value cannot fail and neither can narrating, not because they compute in a vacuum.

What a method promises about its effects is a separate axis it states for itself: see MethodClaims and [3.6-method-classification.md]. This interface classifies a RETURN SIGNATURE, and a signature cannot see effects.

[3.6-method-classification.md]: ../../docs/architecture/3.6-method-classification.md

func NewAction

func NewAction(rt ProviderReceiverType, method *Method, name ActionName) Action

NewAction creates the appropriate concrete Action from a receiver type, method, and short label.

Plan-time callers (planners, writ / lore graph builders, migration plan builders) that hold the ProviderReceiverType and *Method directly use this to bind an Action onto a fresh Node without re-walking the registry. Callers that only know the action's short name use [ReceiverRegistry.BuildAction] instead.

Parameters:

  • rt: the provider receiver type.
  • method: the method descriptor.
  • name: the action's short label (e.g., "file.copy").

Returns:

  • Action: the concrete action (one of [action], [fallibleAction], [compensableAction] per Method.Kind).

type ActionName

type ActionName string

ActionName is the short dotted action label — `<receiver>.<snake_method>`, e.g. "file.write_text".

It is the script-facing vocabulary: the name a `.star` line invokes, the value Action.Name reports, and the key [receiverRegistry.BuildAction] / RuntimeEnvironment.ActionByName / plan.Provider.Plan resolve. Codegen emits one typed constant per action method into the provider package root (`file.WriteText`), so Go callers get completion and a typo is a compile error instead of a runtime lookup failure.

The fully-qualified type identity — Method.ActionName / Action.FullName, the `<pkg-path>.<receiver>.<method>` receipt-stamp form — is a different concept (reflect's package-path identity) and deliberately stays `string`.

type ActionPlanner

type ActionPlanner struct{}

ActionPlanner is the default vanilla planner — one starlark call produces one leaf *Node.

func (ActionPlanner) Plan

func (ActionPlanner) Plan(
	invocator PlanInvocator,
	receiverType ProviderReceiverType,
	method *Method,
	args []any,
	kwargs map[string]any,
	annotations map[string]any,
	onError *Subgraph,
	onRetry *Subgraph,
	retryPolicy *RetryPolicy,
	transitionPolicy *TransitionPolicy,
) (ExecutableUnit, error)

Plan builds the leaf *Node for one vanilla `plan.<provider>.<method>(...)` call.

The action name is `<receiverType.Name>.<snake(method.Name)>`; the node binds a resolved Action built from `receiverType` + `method` directly (the planner holds both, so no by-name deferral). Every field is gathered into a NodeSpec and the node is constructed once via NewNode — no post-construction mutation (the graph-immutability seal).

Slot fill walks the method's declared parameters in order, taking each value positionally from `args` first, then by name from `kwargs`. A parameter the call omits takes its declared default when one exists; a required parameter (non-optional, no default) with no value is an error. Each value is projected to the Binding variant matching the argument kind:

Parameters:

  • `invocator`: the planning host; supplies the session *InvocationRegistry and the *RuntimeEnvironment for plan-time Convert calls.
  • `receiverType`: the planning provider whose method is being called; must be non-nil.
  • `method`: the registered method descriptor; must be non-nil.
  • `args`: positional arguments, already converted starlark → Go, in call order.
  • `kwargs`: keyword arguments by parameter name, already converted (reserved entries removed).
  • `annotations`: tool-specific annotations stamped onto the unit; nil for none.
  • `onError`: the failure-handler *Subgraph stamped onto the unit, or nil.
  • `onRetry`: the retry-handler *Subgraph stamped onto the unit, or nil.
  • `retryPolicy`: the *RetryPolicy stamped onto the unit, or nil.
  • `transitionPolicy`: the *TransitionPolicy stamped onto the unit, or nil.

Returns:

  • `ExecutableUnit`: the sealed *Node with `onError` / `onRetry` / `retryPolicy` / `transitionPolicy` applied and Label unset.
  • `error`: non-nil on nil `receiverType` / `method`, a missing required parameter, or a slot-value conversion failure.

type ActionSummary

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

ActionSummary is the per-action slice of a Summary.

func (ActionSummary) Completed

func (a ActionSummary) Completed() int

Completed returns the number of successful dispatches tallied for this action.

type ActivationRecord

type ActivationRecord struct {

	// RuntimeEnvironment is the session-scope execution environment. Always set during dispatch. Shared across every
	// concurrent dispatch in the same session; never mutated mid-execution.
	RuntimeEnvironment *RuntimeEnvironment

	// Context is the cancellation-aware context for this dispatch. Defaults to `RuntimeEnvironment.Context`;
	// combinators may assign a scoped child context to tighten the cancellation boundary for their nested dispatches.
	Context context.Context

	// Graph is the operation graph this activation belongs to. Non-nil during graph dispatch; nil for non-graph
	// dispatchers. Providers that traverse the graph (e.g., [flow.Provider] for `choose` / `gather` / `wait_until` /
	// `subgraph`) read this field; when `nil` they have no graph to walk.
	Graph *Graph

	// Stack is the recovery stack the current dispatch's receipt pushes onto and that [PromiseBinding.Resolve] queries
	// via [RecoveryStack.ResultByUnitID] for upstream unit results. Stamped by the executor when constructing the
	// activation; nil during non-graph dispatch.
	Stack *RecoveryStack

	// CallerID identifies the caller of the dispatched method (step 30). Graph dispatch: the dispatching unit's
	// id (a unit is a graph-encoded call to a provider method). Starlark dispatch: the script call-site as
	// `file:line:col` (a .star line is a script-encoded call to the same method). Empty when no caller identity
	// exists.
	//
	// [ResourceCatalog.GetOrCreate] takes it as the producer stamp on interned Resources, so a .star-produced
	// resource's ProducerID() reads like "mkfile.star:42:8" — its origin, visible in a debugger. Method bodies
	// that need the dispatching unit OBJECT (the flow combinators walking their subgraphs) resolve it via
	// `activation.Graph.ResolveExecutable(activation.CallerID)` — graph dispatch always has the graph in scope.
	CallerID string

	// Variables is the per-call variable frame in scope for this dispatch. Stamped by the executor just before
	// [Action.Do] is invoked. Carries the session-resolved variables ([VariableResolver] output) at top-level; per-call
	// frames (e.g., gather's per-iteration `item` binding) supersede it on nested dispatches.
	//
	// Concurrent dispatches each hold their own [ActivationRecord], so per-iteration frames built by combinators
	// (gather, future map / fold) are race-free by construction — each goroutine owns its activation and the variables
	// map referenced from it.
	Variables map[string]Variable

	// Slots holds this dispatch's resolved slot values — the output of [ExecutableUnit.ResolveSlots] keyed by the
	// parameter name. Stamped by the executor (or non-graph dispatcher) just before [Action.Do] is invoked, consumed by
	// [Method.Invoke] when mapping slot entries to typed Go arguments via reflection, then implicitly discarded when
	// the activation goes out of scope at dispatch tail.
	//
	// Conceptually transient: a binding-to-argument transform that lives only between resolve and call. It rides on
	// the activation rather than as a separate parameter, so the dispatch context is one bundle (alongside Variables,
	// Stack, Context, CallerID, Graph) rather than half-on-the-activation, half-in-a-parameter.
	Slots map[string]any
	// contains filtered or unexported fields
}

ActivationRecord serves as the data record specific to action invocations.

It is passed as the initial argument injected by the framework into provider methods during every Action.Do and CompensableAction.Undo call. The framework constructs one ActivationRecord per dispatch and passes it to the provider method as the first parameter.

Provider methods read shared session state via ActivationRecord.RuntimeEnvironment, the dispatching unit via ActivationRecord.CallerID, the graph via ActivationRecord.Graph, and a `stdlib` `context.Context` for cancellation-aware operations via ActivationRecord.Context.

Each goroutine-driven dispatch holds its own ActivationRecord; pointer fields on `RuntimeEnvironment` (Catalog, Status, RecoverySite, Registry, etc.) share underlying instances with their own internal synchronization. Concurrent dispatches cannot race on per-call fields because they hold different records.

CallerID identifies the caller in every dispatch mode (step 30): the dispatching unit's id under graph dispatch, a deterministic `file:line:col` call-site under starlark immediate-mode dispatch, and "" when no caller identity exists (test fixtures, CLI runners). Graph stays optional independently — the old Graph/Unit both-nil-or-both-set pairing invariant dissolved with the rename.

Context is the per-dispatch cancellation context. It defaults to `RuntimeEnvironment.Context` at construction. Combinators (subgraph, choose, gather, and wait_until) derive a scoped child context with `context.WithCancel( activation.Context)` and assign it back so per-iteration cancellation reaches the nested provider methods. Provider methods don't act on the context for their own logic. They thread it into the stdlib / 3rd-party dependencies they call (e.g., `exec.CommandContext`, `http.NewRequestWithContext`), which use Go's standard context convention to abort on cancellation. To signal cancellation from a provider's own body, return an error wrapping `context.Context.Err()`.

Lifecycle: created by the executor (or a non-graph dispatcher) before dispatch; consumed during the dispatch; discarded afterward. No persistent identity, no registry — each record is unique to one invocation.

func NewActivationRecord

func NewActivationRecord(graph *Graph, callerID string, runtimeEnvironment *RuntimeEnvironment) *ActivationRecord

NewActivationRecord constructs an *ActivationRecord for one dispatch.

The caller id is "" for non-graph, non-starlark dispatch; Graph is independently optional; the old pairing states are not legal under this design. [Context] is initialized to `runtimeEnvironment.Context`. Combinator-scoped callers (gather and similar) assign a derived child context to ActivationRecord.Context after construction to narrow the cancellation boundary for their nested dispatches.

Parameters:

  • `graph`: the graph this dispatch belongs to, or nil for non-graph dispatch.
  • `unit`: the executable unit being dispatched, or nil for non-graph dispatch. Must be non-nil iff `graph` is non-nil.
  • `runtimeEnvironment`: the session-scope execution environment.

Returns:

  • *ActivationRecord: the constructed activation.

func (*ActivationRecord) DispatchChild

func (a *ActivationRecord) DispatchChild(
	ctx context.Context,
	child ExecutableUnit,
	stack *RecoveryStack,
	variables map[string]Variable,
) (any, error)

DispatchChild dispatches a child through the owning GraphExecutor, retrying per the child's RetryPolicy.

Available only from a bound subgraph's flow-method body — the executor stamps itself on the activation when it dispatches the bound subgraph via Action.Do. Calling DispatchChild outside that context (non-graph dispatch) returns an error.

A thin forwarder to [GraphExecutor.dispatchWithPolicy] — the shared per-unit dispatch primitive that also drives the root from GraphExecutor.Run — so retry (and, in the failure-protocol seam, the OnRetry / OnError handlers) lives in one place, uniform for every unit and invisible to the flow method. See [GraphExecutor.dispatchWithPolicy] for the retry-budget / backoff / pause-cancel semantics.

The caller supplies the RecoveryStack so compensations from this child dispatch land in the caller's saga boundary, and the `variables` frame for the child dispatch — typically `a.Variables` to inherit the current frame, or a per-iteration frame for combinators that rebind variables (gather binds `item` per iteration).

Parameters:

  • `ctx`: the cancellation context for the child dispatch and its backoff waits — typically `a.Context` or a scoped child derived via `context.WithCancel`.
  • `child`: the unit to dispatch (with retry).
  • `stack`: the recovery stack child compensations push onto.
  • `variables`: the variable frame in scope for the child dispatch.

Returns:

  • `any`: the child's terminal result on the succeeding attempt; nil when every attempt failed.
  • `error`: non-nil if the child fails its retry budget, is paused / canceled, or DispatchChild is invoked outside a bound-subgraph dispatch.

func (*ActivationRecord) RunStatus

func (a *ActivationRecord) RunStatus() RunStatus

RunStatus returns a copy of the owning boundary's current RunStatus triplet.

Read-only: the returned value is a copy, so a caller cannot change the run status through it — the only mutator is ActivationRecord.Transition. During non-graph dispatch (no executor) the zero triplet (preparing × healthy) is returned.

Returns:

  • `RunStatus`: the boundary executor's current status, or the zero value when there is no executor.

func (*ActivationRecord) Transition

func (a *ActivationRecord) Transition(condition Condition, reason Reason, message string) error

Transition submits a condition flip to the owning boundary's run status through the executor's single choke point.

The one path by which a dispatched provider (a flow terminal driver) changes the run status; the executor is never exposed, so this and ActivationRecord.RunStatus are the entire run-status surface a provider sees. The submission is arbitrated: a request that would de-escalate the Condition is rejected with a non-nil error (monotonicity), while a worsening or same-condition request is applied or is a no-op. Phase is not an argument — the executor owns phase moves. A no-op returning nil during non-graph dispatch.

Parameters:

  • `condition`: the Condition being entered — must be at or above the current condition.
  • `reason`: the Reason token classifying the flip.
  • `message`: free-text detail, typically an err.Error().

Returns:

  • `error`: non-nil when the request would de-escalate the condition (rejected); nil when applied or a no-op.

type AddressingMode

type AddressingMode int

AddressingMode classifies a Resource by what its identity is grounded in.

Two real modes: AddressingLocation for Resources whose identity is the place they live (file path, URL, repo path, service name) — bytes at the location are mutable, and the catalog uses shadow semantics to track changes. AddressingContent for Resources whose identity is their content digest — same URI implies same bytes by construction, so content "changes" mint new URIs rather than shadowing existing ones.

AddressingUnknown is the zero-value sentinel. It is not a valid runtime classification — every concrete Resource type self-declares its mode by overriding Resource.Addressing. The boot-discipline test in pkg/op/addressing_test.go (added in 13.0(k) sub-step k.12) walks every announced Resource type and asserts none returns AddressingUnknown. The catalog's branch logic panics if it ever encounters AddressingUnknown at runtime.

const (
	// AddressingUnknown is the zero-value sentinel. Concrete Resource types must override [Resource.Addressing]
	// to return one of the two real modes.
	AddressingUnknown AddressingMode = iota

	// AddressingLocation is for Resources whose identity is a location — file paths, URLs, repo paths, service names.
	// Used by file/git/appnet/pkg/service Resources. Catalog applies shadow semantics on content changes.
	AddressingLocation

	// AddressingContent is for Resources whose identity is their content digest. Used by mem/stream/function/json/yaml
	// Resources. URI takes the form tag:devlore.noblefactor.com,2026-01-01:<algo>:<hex>#<go-type-id>. Same URI implies
	// same content by construction; content changes mint new URIs.
	AddressingContent
)

func (AddressingMode) String

func (m AddressingMode) String() string

String returns the lowercase name of the addressing mode.

Panics via assert.Unreachable when m holds an integer outside the declared constants — that state is a programming error (e.g., AddressingMode cast from an arbitrary int) and surfaces loudly rather than silently falling through.

type AnnotationMap

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

AnnotationMap is a read-only wrapper around tool-specific unit metadata.

It encapsulates the raw map to ensure the immutability of a unit's annotations after construction. Serializes to the same JSON/YAML shape as the underlying map.

func NewAnnotationMap

func NewAnnotationMap(values map[string]any) AnnotationMap

NewAnnotationMap returns an AnnotationMap holding a detached, read-only copy of `values`.

Later mutations of the source map do not bleed into the constructed map, and callers read through AnnotationMap.Get but cannot mutate it. An empty or nil `values` yields the zero AnnotationMap.

Parameters:

  • `values`: the name → value annotations to capture.

Returns:

  • `AnnotationMap`: an immutable wrapper over a fresh copy of `values`.

func (AnnotationMap) Get

func (a AnnotationMap) Get(name string) (any, bool)

Get returns the annotation value for the given name and a boolean indicating if it was present.

Returns:

  • `any`: the value associated with the name, or nil if the annotation is missing.
  • `bool`: true if the annotation was present (even if the value is nil).

func (AnnotationMap) MarshalJSON

func (a AnnotationMap) MarshalJSON() ([]byte, error)

MarshalJSON ensures the wrapper serializes as a plain map.

Returns:

  • `[]byte`: the JSON encoding of the underlying annotation map.
  • `error`: any error returned by json.Marshal over the underlying map.

func (AnnotationMap) MarshalYAML

func (a AnnotationMap) MarshalYAML() (any, error)

MarshalYAML ensures the wrapper serializes as a plain map.

Returns:

  • `any`: the underlying annotation map, emitted in place of the wrapper.
  • `error`: always nil; the signature satisfies the yaml.Marshaler contract.

type Attempt

type Attempt struct {

	// Number is the 1-based attempt number.
	Number int `json:"number" yaml:"number"`

	// Status is "completed" or "failed".
	Status string `json:"status" yaml:"status"`

	// Error is the error message if the attempt failed.
	Error string `json:"error,omitempty" yaml:"error,omitempty"`

	// Timestamp is when this attempt completed (RFC3339).
	Timestamp string `json:"timestamp" yaml:"timestamp"`
}

Attempt records one execution attempt of an ExecutableUnit.

type AttributeResolver

type AttributeResolver interface {
	ResolveAttr(name string) any
}

AttributeResolver is implemented by providers that expose dynamic attributes beyond their own methods.

When the bridge encounters an unknown attribute, it checks if the provider implements AttributeResolver and delegates to it. The returned value is marshaled to a starlark.Value.

type BackoffStrategy

type BackoffStrategy string

BackoffStrategy defines how delays increase between retries.

const (
	// BackoffNone applies no delay between retries.
	BackoffNone BackoffStrategy = "none"
	// BackoffLinear increases delay linearly between retries.
	BackoffLinear BackoffStrategy = "linear"
	// BackoffExponential doubles the delay between each retry.
	BackoffExponential BackoffStrategy = "exponential"
)

BackoffStrategy constants define the available retry backoff strategies.

type Binding

type Binding interface {
	Edge(consumer string) *Edge
	Resolve(variables map[string]Variable, stack *RecoveryStack) any
	// contains filtered or unexported methods
}

Binding is the value bound to a slot.

It is sealed at three variants and the set is closed: ImmediateBinding, PromiseBinding, and VariableBinding. Callers cannot extend it because the marker method [Binding.isBinding] is unexported.

Resolve returns the slot's resolved Go value at execution time. The variables map carries the binding layer's resolved variables (one entry per VariableBinding slot the graph references). The recovery stack is queried by PromiseBinding to look up an upstream unit's output via RecoveryStack.ResultByUnitID. Variants ignore the parameters they do not need.

Edge returns the producer→consumer dependency edge the binding induces, or nil when it induces none. Only PromiseBinding yields an edge: its producer is a unit in the graph. An immediate value has no producing unit, and a variable is injected from the RuntimeEnvironment, so both return nil.

type Collision

type Collision struct {
	Loser             string `json:"loser" yaml:"loser"`
	LoserLayer        string `json:"loser_layer,omitempty" yaml:"loser_layer,omitempty"`
	LoserSpecificity  int    `json:"loser_specificity,omitempty" yaml:"loser_specificity,omitempty"`
	Target            string `json:"target" yaml:"target"`
	Winner            string `json:"winner" yaml:"winner"`
	WinnerLayer       string `json:"winner_layer,omitempty" yaml:"winner_layer,omitempty"`
	WinnerSpecificity int    `json:"winner_specificity,omitempty" yaml:"winner_specificity,omitempty"`
}

Collision records a source conflict resolved during tree building (writ-specific).

type Comparer

type Comparer interface {
	Equal(other any) bool
}

Comparer is implemented by types that define domain-specific equality.

Go's `==` works on built-in types and pointer identity, but neither captures the equality semantics most domain types need — two Resource values with the same URI represent the same resource even if they are distinct pointers, and two configuration structs that differ only in cached metadata are equivalent for routing decisions. Types that implement Comparer take ownership of their own equality rule; callers compare via Comparer.Equal rather than `==` whenever both sides advertise it.

type CompensableAction

type CompensableAction interface {
	Action
	Undo(activationRecord *ActivationRecord, compensator Compensator) error
}

CompensableAction is a unit of work that can fail and can be undone: Do returns (result, compensator, error).

This is the one tier whose name does describe effects, and legitimately: a compensator exists to reverse something, so producing one is evidence that something was changed. Method.Mutates reads exactly that, which is why mutation is the single property derived from a signature rather than claimed.

type Compensator

type Compensator interface {

	// Compensate reverses the effects captured when this compensator was produced, using `runtimeEnvironment` to
	// resolve and dispatch the reversal.
	Compensate(runtimeEnvironment *RuntimeEnvironment) error
}

Compensator reverses its own effects during saga rollback — the Composite "component" of the recovery tree.

A leaf receipt (via ReceiptBase) compensates by invoking its compensating action; a *RecoveryStack compensates by unwinding its children LIFO. Every compensable Do returns a Compensator as its second result (nil for non-compensable actions), which the executor invokes during rollback.

type Condition

type Condition int

Condition is the worst trouble a run has met — the severity dimension of RunStatus, orthogonal to Phase.

The four values are ordered by severity (ConditionHealthy < ConditionDegraded < ConditionExecutionFailed < ConditionCompensationFailed), and a run's condition only worsens: it climbs when a unit degrades or fails and never falls within a run. A ConditionDegraded is reached when a flow.Degraded gate executes; ConditionExecutionFailed when an unhandled failure or flow.Failed reaches a saga boundary; ConditionCompensationFailed when a compensation action itself fails, leaving the system dirty. The severity order encodes the bubble-up rule directly: a parent takes the worst of its children's reported conditions.

Serialized over [conditionNames] in both document formats — Condition.MarshalText for JSON, Condition.MarshalYAML for gopkg.in/yaml.v3, which does not honor encoding.TextMarshaler.

const (

	// ConditionHealthy is the no degradations or failures condition; the zero value.
	ConditionHealthy Condition = iota

	// ConditionDegraded marks a run that met a failure a flow.Degraded gate handled: the failure is recorded and
	// execution continues.
	ConditionDegraded

	// ConditionExecutionFailed marks an unhandled forward failure — a saga boundary exhausted its retries, or
	// flow.Failed executed.
	ConditionExecutionFailed

	// ConditionCompensationFailed marks a failed unwind: a forward action failed and at least one Compensate also
	// failed, so the system is dirty. The worst condition; pairs only with [PhaseStopped].
	ConditionCompensationFailed
)

func (Condition) MarshalText

func (c Condition) MarshalText() ([]byte, error)

MarshalText encodes this condition as its serialized name.

Satisfies encoding.TextMarshaler, so JSON documents carry "degraded" / "execution_failed" rather than a bare integer.

Returns:

  • `[]byte`: the name from [conditionNames].
  • `error`: non-nil when the value is out of range.

func (Condition) MarshalYAML

func (c Condition) MarshalYAML() (any, error)

MarshalYAML encodes this condition as its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextMarshaler, so YAML documents need this companion to carry "degraded" / "execution_failed" rather than a bare integer.

Returns:

  • `any`: the name from [conditionNames], as a string.
  • `error`: non-nil when the value is out of range.

func (Condition) String

func (c Condition) String() string

String returns this condition's serialized name.

Returns:

  • `string`: the name from [conditionNames], or "Condition(<n>)" for an out-of-range value.

func (*Condition) UnmarshalText

func (c *Condition) UnmarshalText(text []byte) error

UnmarshalText decodes a condition from its serialized name.

Satisfies encoding.TextUnmarshaler for JSON documents.

Parameters:

  • `text`: one of the [conditionNames] entries.

Returns:

  • `error`: non-nil when `text` names no condition.

func (*Condition) UnmarshalYAML

func (c *Condition) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes a condition from its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextUnmarshaler, so YAML documents need this companion.

Parameters:

  • `unmarshal`: the YAML node decoder supplied by the `yaml` package.

Returns:

  • `error`: non-nil when the node is not a string or names no condition.

type ConflictPolicy

type ConflictPolicy int

ConflictPolicy specifies how an occupied write target is handled at the file provider's write seam.

Phase-8 step 49. Exactly three values (the former Backup/Overwrite pair collapsed into Replace: a replace ALWAYS archives the occupant to the recovery site — the receipt's pre-archive digest is what compensation restores from, so an unarchived overwrite would break the SAGA contract). The policy travels the interim application.Application flag channel (`Flags["conflict"]`, the dry-run precedent) until the config loader delivers the cli source.

The floor is ConflictReplace via NewRuntimeEnvironmentConfig: at the write seam, updating a known target in place (a lint fix rewriting a file, an archive displacing, a re-render) is indistinguishable from deploying over a foreign occupant, and in-place updates are not conflicts — so the seam's default keeps the archive-and-overwrite semantics every consumer depends on. The cautious `stop` default belongs to the layer that can tell the cases apart: writ deploy's pre-flight classifies occupants through its readback and passes the resolved policy per run (the phase-8 step-49 layered-enforcement ruling).

const (
	// ConflictStop refuses to touch an occupied target: the write errors, the node fails, the run unwinds.
	ConflictStop ConflictPolicy = iota
	// ConflictSkip leaves the occupant untouched and reports the write as a no-op success.
	ConflictSkip
	// ConflictReplace archives the occupant to the recovery site and replaces it (restorable on unwind).
	ConflictReplace
)

func ParseConflictPolicy

func ParseConflictPolicy(value string) (ConflictPolicy, error)

ParseConflictPolicy parses a flag/serialized policy name.

Parameters:

  • `value`: "stop", "skip", or "replace"; "" parses as the ConflictStop floor.

Returns:

  • `ConflictPolicy`: the parsed policy.
  • `error`: non-nil for any other value.

func (ConflictPolicy) String

func (p ConflictPolicy) String() string

String returns the policy's flag/serialized name.

Returns:

  • `string`: "stop", "skip", or "replace".

type ControlCommand

type ControlCommand int32

ControlCommand is a command a consumer issues to steer a run.

const (
	// ControlPause halts at the next control-point and preserves the recovery stack as the resume point
	// ([PhasePaused], resumable via [ResumeExecutor]).
	ControlPause ControlCommand = iota

	// ControlStop halts at the next control-point, unwinds (compensating completed work), and terminates
	// ([PhaseStopped]); not resumable.
	ControlStop

	// ControlStep advances one unit then re-pauses. Reserved; not yet served.
	ControlStep
)

ControlCommand values.

func (ControlCommand) String

func (c ControlCommand) String() string

String returns the lowercase command name.

Returns:

  • `string`: "pause" / "stop" / "step", or "control(<n>)" for an unknown value.

type ControlEvent

type ControlEvent struct {

	// Seq is the per-run monotonic sequence number, stamped by [ControlPlane.emit]; a gap reveals a dropped event.
	Seq uint64

	// Kind classifies the event.
	Kind ControlEventKind

	// Status is the run status at the moment of the event.
	Status RunStatus

	// UnitID is the unit involved, when relevant (empty for run-level events).
	UnitID string

	// Err is set for [EventError].
	Err error
}

ControlEvent is one pushed observation of a run — the event half of the plane.

type ControlEventKind

type ControlEventKind int

ControlEventKind classifies a pushed ControlEvent.

const (
	// EventPhaseChanged reports a [RunStatus] transition (including per-unit progress within [PhaseRunning]).
	EventPhaseChanged ControlEventKind = iota

	// EventError reports a unit or run error.
	EventError

	// EventResult reports that a terminal result is available.
	EventResult
)

ControlEventKind values.

type ControlPlane

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

ControlPlane is a run's bidirectional command / event channel — the in-process core of the control plane (architecture 2.7).

Two directions cross one plane, both fully async (nothing here blocks the run or the consumer):

  • Commands in — a consumer calls ControlPlane.Request with a ControlCommand and gets back a response channel (the future / stream-id correlation). The executor drains pending requests at each control-point via [ControlPlane.poll] and answers each on its own channel, so a slow response never blocks another (no response head-of-line blocking).
  • Events out — a consumer calls ControlPlane.Subscribe to receive pushed [ControlEvent]s; the executor emits via [ControlPlane.emit], a non-blocking fan-out that drops rather than stall the run when a subscriber can't keep up.

One plane is shared by a whole run — every child executor holds the same pointer (see [GraphExecutor.newChildExecutor]) — so a command issued anywhere is observed at the next control-point wherever it falls in the tree, and an event emitted anywhere reaches every subscriber. The wire listener (HTTP/2) that bridges a remote consumer to a plane is a separate layer; this type is transport-agnostic.

func NewControlPlane

func NewControlPlane() *ControlPlane

NewControlPlane returns an empty control plane ready for ControlPlane.Request and ControlPlane.Subscribe.

Returns:

  • *ControlPlane: the constructed plane.

func (*ControlPlane) Request

func (p *ControlPlane) Request(cmd ControlCommand) <-chan ControlResponse

Request submits `cmd` and returns its response channel — the consumer side of commands-in. It never blocks.

The channel yields exactly one ControlResponse, sent when the executor serves the command at its next control-point (or immediately, with an error, when the inbound queue is somehow full). The caller selects on the channel when ready; a caller that wants to block just receives from it.

Parameters:

Returns:

  • `<-chan ControlResponse`: a single-response channel (the future / stream-id correlation).

func (*ControlPlane) Subscribe

func (p *ControlPlane) Subscribe() (events <-chan ControlEvent, cancel func())

Subscribe registers for pushed events and returns the stream plus an unsubscribe func — the consumer side of events-out. It never blocks.

The returned channel receives every ControlEvent emitted after the call, in order, until `cancel` is invoked (which removes the subscription and closes the channel). The buffer is bounded; a subscriber that falls behind drops events rather than stall the run — the `Seq` gap on the next delivered event reveals the loss.

Returns:

  • `<-chan ControlEvent`: the pushed event stream.
  • `func()`: the unsubscribe; idempotent.

type ControlResponse

type ControlResponse struct {

	// Status is the run status the command produced (e.g. [PhasePaused] for a served pause).
	Status RunStatus

	// Err is non-nil when the command was rejected (an unsupported command, or a full inbound queue).
	Err error
}

ControlResponse is the acknowledgement of a ControlCommand — the response half of a request/response pair.

type DefaultFunc

type DefaultFunc func(env *RuntimeEnvironment, siblings map[string]any, args []reflect.Value) (reflect.Value, error)

DefaultFunc is the signature every entry in the deferred-default registry conforms to.

Each `{{ funcname arg1 arg2 ... }}` command in a directive expression resolves at slot-fill time to exactly one DefaultFunc call. The evaluator gathers the command's evaluated arguments into args, looks up funcname in `env.DefaultFuncs` (a snapshot of the package-level announcements), and invokes the matched DefaultFunc.

Parameters:

  • env: live runtime environment from the dispatching call. Always non-nil at slot-fill; registered functions may rely on env.Status, env.Root, env.Catalog, etc. without a nil check.
  • siblings: already-filled slot values from the same dispatch, keyed by parameter name. Nil-safe lookups via the standard `v, ok := siblings[name]` form. Functions that don't consult siblings (umask, mode, env) ignore this argument.
  • args: argument values produced by recursively evaluating each child node of the CommandNode in announce-declared order. Functions validate arity and per-argument Kind before extracting concrete values. Empty slice for a zero-arg call (e.g., `{{ umask }}`).

Returns:

  • reflect.Value: the function's natural Go result. The evaluator carries this up the pipeline; the last command's reflect.Value is what treeDefault.Resolve hands to op.Convert.
  • error: non-nil on argument-arity mismatch, argument-type mismatch, or function-internal failure.

type DeferredDefault

type DeferredDefault interface {

	// Resolve evaluates the deferred default and returns a Go value at target's exact type.
	//
	// Parameters:
	//   - env:      live runtime environment; passed to every registered DefaultFunc.
	//   - siblings: already-filled slot values from the same dispatch, keyed by parameter name.
	//   - target:   the parameter's reflect.Type — Resolve widens its result to this type via [Convert].
	//
	// Returns:
	//   - any:   the resolved value, dynamic type matches target exactly.
	//   - error: non-nil if any function call errors, an identifier doesn't resolve in the funcmap, or a
	//     sibling-slot reference points at an unfilled slot.
	Resolve(env *RuntimeEnvironment, siblings map[string]any, target reflect.Type) (any, error)
}

DeferredDefault is a Parameter.Default that resolves at slot-fill time against the live runtime environment instead of being a typed Go value parsed at announce time.

[parseDefaultExpression] returns a DeferredDefault for any directive value wrapped in `{{ ... }}` outer braces. Slot-fill checks Parameter.Default with a type assertion; if the assertion succeeds, slot-fill calls Resolve and writes the returned value onto the slot via ImmediateBinding. Plain literal defaults (Parameter.Default holding os.FileMode, int, string, etc.) bypass this path entirely.

The siblings map carries already-filled slot values from the same dispatch — keys are parameter names, values are the natural Go values that landed in those slots. Sibling references in the directive expression — text/template `.fieldname` syntax — read from this map at evaluation time.

type Digest

type Digest struct {
	Algorithm string
	Bytes     []byte
}

Digest is the honest content hash of a Resource. It is one of two change-detection signals every Resource exposes; the other is the cheap Resource.Etag. The catalog consults Digest only when Etag mismatches — touch-style drift (mtime updates without content change) is caught at the Etag tier and never reaches Digest.

Algorithm names use the OCI convention: a lowercase identifier such as "sha256". Bytes is the raw digest payload; render the canonical "<algo>:<hex>" form via Digest.String.

func ParseDigest

func ParseDigest(s string) (Digest, error)

ParseDigest parses the canonical "<algo>:<hex>" digest form into a Digest.

Strict per the locked decision in 13.0(k) F5: algorithm must be a lowercase identifier in the supported allowlist (currently sha256 only); hex must be lowercase; sha256 payloads must be exactly 32 bytes. Uppercase hex, malformed shape, unknown algorithm, and wrong-length payloads all fail with an explicit error.

Parameters:

  • s: the canonical digest string.

Returns:

  • Digest: the parsed digest with Algorithm and Bytes populated.
  • error: non-nil on any syntactic or semantic defect.

func (Digest) Equal

func (d Digest) Equal(other Digest) bool

Equal reports whether d and other represent the same content under the same algorithm.

Two Digests with different algorithms are never equal even when the bytes coincidentally match — they encode hashes from different functions and represent unrelated identities. Reflexivity, symmetry, and consistency follow trivially from byte-for-byte comparison.

Parameters:

  • other: the digest to compare against.

Returns:

  • bool: true iff Algorithm and Bytes match exactly.

func (Digest) String

func (d Digest) String() string

String returns the canonical "<algo>:<hex>" form. Hex is lowercase; round-trips through ParseDigest.

type Edge

type Edge struct {
	From  string      `json:"from" yaml:"from"`
	To    string      `json:"to" yaml:"to"`
	Guard GuardResult `json:"guard,omitempty" yaml:"guard,omitempty"`
}

Edge represents a dependency relationship between two nodes.

From must complete before To can begin execution. An unguarded edge (`Guard == GuardNone`, the zero value) is a pure ordering constraint consumed by [topologicallySorted]. A guarded edge additionally routes execution: a decision node's out-edges each carry the GuardResult they are followed on (phase-8 step 10, the choose decision tree).

type ElevationLifespan

type ElevationLifespan struct {

	// Ephemeral, if true, means privileges drop immediately after the action completes.
	Ephemeral bool `json:"ephemeral" yaml:"ephemeral"`

	// CacheDuration defines how long the elevated token/session remains valid before expiring.
	CacheDuration time.Duration `json:"cache_duration" yaml:"cache_duration"`
}

ElevationLifespan defines the duration and caching semantics of the elevated state.

type ElevationOffer

type ElevationOffer struct {

	// Strategy is the mechanism used to acquire elevation (host escalation, interactive challenge, role assumption,
	// or mandated approval).
	Strategy ElevationStrategy `json:"strategy" yaml:"strategy"`

	// Scope is the security domain and the explicit privileges the elevation must grant.
	Scope ElevationScope `json:"scope" yaml:"scope"`

	// Lifespan is the duration and caching semantics of the elevated state.
	Lifespan ElevationLifespan `json:"lifespan" yaml:"lifespan"`

	// Fallback is an optional chainable alternative, attempted when this policy cannot be satisfied.
	Fallback *ElevationOffer `json:"fallback" yaml:"fallback"`
}

ElevationOffer acts as the complete metadata block governing elevation rules.

type ElevationScope

type ElevationScope struct {

	// Domain specifies the security subsystem (e.g., "OS", "GoogleOAuth", "AWS-IAM").
	Domain string `json:"domain" yaml:"domain"`

	// RequiredPrivileges lists the explicit capabilities needed (e.g., ["root", "repo:write"]).
	RequiredPrivileges []string `json:"required_privileges" yaml:"required_privileges"`
}

ElevationScope defines the boundaries of the required privilege.

type ElevationStrategy

type ElevationStrategy string

ElevationStrategy defines the mechanical approach used to achieve elevation.

const (
	HostEscalation       ElevationStrategy = "host_escalation"       // OS-level escalation (e.g., sudo, runas)
	InteractiveChallenge ElevationStrategy = "interactive_challenge" // Prompting user for password/OTP
	IdentityAssumption   ElevationStrategy = "identity_assumption"   // Assuming a role dynamically (AWS STS, JWT minting)
	MandatedApproval     ElevationStrategy = "mandated_approval"     // Awaiting third-party admin gatekeeper approval
)

The elevation strategies.

type Encoder

type Encoder interface {
	Encode(v any) error
}

Encoder is the interface for graph serialization.

Both *json.Encoder and *yaml.Encoder satisfy this interface.

type ExecutableUnit

type ExecutableUnit interface {

	// Dispatch state: The bound action (or its registry name), plan-time annotations, the input surface, and slots.
	Action() Action
	ActionName() ActionName
	Annotations() AnnotationMap
	Parameters() ([]Parameter, error)
	Slots() map[string]Binding

	// Identity — the unit's id and its parent unit's id.
	ID() string
	ParentID() string

	// Per-unit policies: elevation, retry, the error / retry handler subgraphs, and the transition policy (each nil-able).
	ElevationOffer() *ElevationOffer
	RetryPolicy() *RetryPolicy
	OnError() *Subgraph
	OnRetry() *Subgraph
	TransitionPolicy() *TransitionPolicy

	Execute(
		ctx context.Context,
		executor *GraphExecutor,
		stack *RecoveryStack,
		variables map[string]Variable,
	) (any, error)
	// contains filtered or unexported methods
}

ExecutableUnit is anything the executor can dispatch: a Node or a Subgraph.

Every unit carries an Action (the dispatch surface), an annotation map (extensible plan-time metadata), a slot map (parameter-name → Binding bindings), and the per-unit policy triplet — an optional elevation policy, retry policy, and error-handler *Subgraph. Both Node and Subgraph dispatch through the same path: `unit.Action() → action.Do(activationRecord)`. Parameters reports the unit's input surface (the method's parameters for Node; the bubble-up variable surface for Subgraph).

The interface exposes read-only accessors and the dispatch entry point only. Mutation is package-internal: the lowercase setters on the embedded [executableUnit] are visible to in-package builders (NewSubgraph, NewNode, [Subgraph.addChild]'s parent-stamp, the planner's slot fill, the promise resolver's slot fill, the load path's child linkage) but invisible across the package boundary. The construction surface (NewGraph / NewSubgraph / NewNode) is the only public path for producing a fully-formed unit.

stampParentID is also package-internal — exposed on the interface so the in-package mutators can stamp ownership without a *Node / *Subgraph type-switch. Because both setters and stampParentID are unexported, the interface is closed to same-package implementations — only *Node and *Subgraph satisfy it.

Parameters on the executableUnit base is intentionally not implemented.

Both *Node and *Subgraph override Parameters to return their own bubble-up variable surface; the embedded base has no usable default — leaf vs. composite need different walks. The ExecutableUnit interface declares Parameters, and both concrete types satisfy it via their overrides.

type ExecutableUnitSpec

type ExecutableUnitSpec struct {
	Action           Action
	ActionName       ActionName
	Annotations      map[string]any
	ElevationOffer   *ElevationOffer
	OnError          *Subgraph
	OnRetry          *Subgraph
	ID               string
	RetryPolicy      *RetryPolicy
	Slots            map[string]Binding
	TransitionPolicy *TransitionPolicy
}

ExecutableUnitSpec is the construction payload shared by NodeSpec and SubgraphSpec.

It carries the fields common to every ExecutableUnit — identity, action, annotations, slot bindings, and the per-unit policy triplet (optional elevation policy, retry policy, and error-action subgraph) — and exposes one fluent `With*` setter per field. NodeSpec and SubgraphSpec embed it and re-declare each setter to return their own type; a populated spec feeds NewNode / NewSubgraph, which produce the sealed unit. The setters mutate the builder, never a constructed unit — the seal forbids post-construction mutation.

func (*ExecutableUnitSpec) WithAction

func (s *ExecutableUnitSpec) WithAction(action Action) *ExecutableUnitSpec

WithAction sets the dispatch Action for the unit.

Use this when the caller holds a resolved Action; a caller that holds only a name binds via [WithActionNamed] instead. Every unit must end up bound one way or the other — there is no structural (action-less) unit.

Parameters:

  • `action`: the Action to bind.

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithActionNamed

func (s *ExecutableUnitSpec) WithActionNamed(name ActionName) *ExecutableUnitSpec

WithActionNamed binds the dispatch action by its registry name, for callers that hold a name but no resolved Action.

The name is validated against the global receiver registry (ReceiverRegistry, populated by AnnounceProvider at package init): an un-resolvable name is a programming/configuration error — the named provider was never announced — so this panics rather than returning an error, matching the fluent `With*` contract ([WithAction] returns the spec with no error). The concrete Action is NOT stored; the validated name is, to be resolved lazily at dispatch via RuntimeEnvironment.ActionByName. Use [WithAction] when you already hold the resolved action.

Parameters:

  • `name`: the dotted registry name (e.g. "flow.subgraph"); must resolve via the global registry.

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithAnnotations

func (s *ExecutableUnitSpec) WithAnnotations(annotations map[string]any) *ExecutableUnitSpec

WithAnnotations sets the tool-specific annotations stamped on the unit at construction.

Parameters:

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithElevationOffer

func (s *ExecutableUnitSpec) WithElevationOffer(elevationOffer *ElevationOffer) *ExecutableUnitSpec

WithElevationOffer sets the ElevationOffer for the unit.

Parameters:

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithID

WithID sets the unit identifier.

Parameters:

  • `id`: the unit identifier; immutable once the unit is constructed.

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithOnError

func (s *ExecutableUnitSpec) WithOnError(onError *Subgraph) *ExecutableUnitSpec

WithOnError sets the failure-handler Subgraph for the unit.

Parameters:

  • `onError`: the handler Subgraph, or nil for no error action.

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithOnRetry

func (s *ExecutableUnitSpec) WithOnRetry(onRetry *Subgraph) *ExecutableUnitSpec

WithOnRetry sets the per-attempt retry-handler Subgraph for the unit.

Parameters:

  • `onRetry`: the retry-handler Subgraph, or nil for no retry handler.

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithRetryPolicy

func (s *ExecutableUnitSpec) WithRetryPolicy(retryPolicy *RetryPolicy) *ExecutableUnitSpec

WithRetryPolicy sets the RetryPolicy for the unit.

Parameters:

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithSlot

func (s *ExecutableUnitSpec) WithSlot(name string, value Binding) *ExecutableUnitSpec

WithSlot binds one slot value by parameter name, allocating the slot map on first use.

Parameters:

  • `name`: the parameter name (or frame-binding name) the slot fills.
  • `value`: the Binding to bind.

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

func (*ExecutableUnitSpec) WithTransitionPolicy

func (s *ExecutableUnitSpec) WithTransitionPolicy(transitionPolicy *TransitionPolicy) *ExecutableUnitSpec

WithTransitionPolicy sets the TransitionPolicy for the unit.

Parameters:

Returns:

  • `*ExecutableUnitSpec`: the receiver, for chaining.

type FallibleAction

type FallibleAction interface {
	Action
}

FallibleAction is a unit of work that can fail: Do returns (result, nil, error).

**Fallible, not necessarily effectful** — the mirror of Action's correction. `regex`'s eight methods and `json` and `yaml`'s seven reach this tier and touch nothing outside their arguments; they can fail because a pattern or a document can be malformed, which is a statement about inputs rather than about effects.

type Graph

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

Graph represents an execution graph containing nodes and edges.

This is THE graph used by both writ and lore — they differ only in content. Graph is immutable: the plan is re-executable any number of times against any number of fresh [RuntimeEnvironment]s without carrying execution state across runs.

func LoadGraph

func LoadGraph(env *RuntimeEnvironment, data []byte, format string) (*Graph, error)

LoadGraph decodes a serialized-form graph (JSON or YAML) into a fully action-bound in-memory *Graph.

The decode path is registry-aware end-to-end: payload bytes are first decoded into the serialized-form payload structs ([graphData], [nodeData], [subgraphData]); LoadGraph then hands the payload to [assembleGraph], which resolves each unit's action by short name through `env.Registry` and constructs each *Node / *Subgraph via NewNode / NewSubgraph with the resolved action — so no unit ever exists in a transient action-less state outside the load internals.

After unit construction the load path rebuilds containment (child IDs → child pointers, topological order per subgraph edges) and validates edge endpoints. The returned graph holds no reference to the supplied env; pass it to NewGraphExecutor to execute.

Parameters:

  • `env`: the runtime environment whose registry resolves action names. Must be non-nil; the registry must contain every action referenced in the serialized form.
  • `data`: the encoded bytes.
  • `format`: "json" or "yaml" (or "yml") — case-insensitive.

Returns:

  • `*Graph`: the constructed graph with every unit's action bound.
  • `error`: non-nil if decoding fails, the format is unsupported, any action name is unknown to the registry, any child ID is dangling, or any edge endpoint fails to resolve.

func NewGraph

func NewGraph(spec *GraphSpec) (*Graph, error)

NewGraph constructs a sealed *Graph from a populated *GraphSpec.

Structural state is supplied at construction time; the returned Graph carries no public setters that mutate its fields. Per the phase-8 immutability invariant, every later session-owner (a GraphExecutor.Run, a serializer, an inspector) reads from this Graph without changing it.

Pipeline: build the root *Subgraph from the spec's units, slots, retry policy, and error action (which materializes edges and topologically sorts the children); assemble fresh [graphMetadata] (a now timestamp, the current schema version, the spec's origin and resource catalog — defaulting to a fresh empty *ResourceCatalog when nil); hand the root and metadata to the shared [buildGraph], which walks the unit table and computes the integrity checksum from Graph.CanonicalContent. Graph signing is not done at construction — the [Graph.signature] is set externally: the load path preserves the document's signature, and a fresh graph is signed through Graph.SignWith (signing proper lives in pkg/signing).

Parameters:

  • `spec`: the populated graph spec. A zero `Origin` is permitted (graphs built outside a tooling context); a nil `ResourceCatalog` defaults to a fresh empty catalog.

Returns:

  • `*Graph`: the sealed graph, with checksum populated and signature populated when applicable.
  • `error`: non-nil when canonical-content serialization or signing fails.

func Plan

func Plan(
	ctx context.Context,
	spec *RuntimeEnvironmentSpec,
	fn func(*RuntimeEnvironment) (*Graph, error),
) (graph *Graph, err error)

Plan runs a planning session bounded by spec and fn.

The session-shape is:

  1. Build a planning RuntimeEnvironment from spec, minting its fsroot.Dir from the spec's anchor.
  2. Call fn with the runtime environment; the caller drives planning (loading a starlark script, calling plan.assemble_definition, etc.) and returns the assembled *Graph (or nil if the script did not assemble a graph).
  3. Close the planning runtime environment.

Step 3 fires via defer, so a panic inside fn still leaves the runtime environment closed. The returned *Graph is immutable and holds no reference to the planning environment; the next session-owner (typically a GraphExecutor) executes it under a fresh environment of its own.

Parameters:

  • `ctx`: the parent context whose cancellation / values flow into the planning runtime environment.
  • `spec`: the planning-environment configuration.
  • `fn`: the caller-supplied planning routine; receives the runtime environment and returns the assembled graph.

Returns:

  • *Graph: the assembled graph (nil if fn did not assemble one).
  • `error`: non-nil if the planning runtime environment cannot be built, fn returned an error, or the planning runtime environment's RuntimeEnvironment.Close failed.

func (*Graph) CanonicalContent

func (g *Graph) CanonicalContent() ([]byte, error)

CanonicalContent returns the graph serialized as YAML without checksum and signature.

Used for computing checksums and verifying signatures. The output mirrors the symbol-table serialized form: top-level `children` (root's children IDs in topological order), `subgraphs` (every non-root Subgraph sorted by ID), and `nodes` (every Node sorted by ID).

Returns:

  • `[]byte`: the canonical YAML bytes.
  • `error`: non-nil if YAML marshaling fails.

func (*Graph) Checksum

func (g *Graph) Checksum() string

Checksum returns the git-style integrity hash.

Returns:

  • `string`: the canonical "sha256:<hex>" form, or empty when unset.

func (*Graph) Edges

func (g *Graph) Edges() []Edge

Edges returns the ordering edges at the root level.

A copy: the graph's construction checksum is computed over its contents, so a caller that could rewrite the recorded edges could leave the checksum describing a graph that no longer exists. The Edge values inside are shared, which is exactly the sharing the rule permits.

Returns:

  • `[]Edge`: a copy of the root-level dependency edges, in insertion order.

func (*Graph) Filename

func (g *Graph) Filename() string

Filename returns the standard filename for this graph.

Format: "<timestamp>.yaml", or "<scope>-<timestamp>.yaml" when Origin.Scope is set.

Returns:

  • `string`: the formatted filename.

func (*Graph) Kind

func (g *Graph) Kind() string

Kind returns the canonical identifier of this graph's artifact type.

Stamped at construction from GraphKind. Paired with Graph.SerialVersion (the numeric schema version), it serves as the serialization-format discriminator that distinguishes a Devlore Graph from other YAML/JSON artifacts that might share a stream or path, and lets readers reject payloads of the wrong shape before attempting to decode them.

Returns:

  • `string`: the value of GraphKind at the time the graph was constructed.

func (*Graph) MarshalJSON

func (g *Graph) MarshalJSON() ([]byte, error)

MarshalJSON projects the graph to its [graphData] serialized shape and JSON-encodes it.

Returns:

  • `[]byte`: the JSON encoding of the graph's serialized form.
  • `error`: non-nil if packing a content resource or JSON marshaling fails.

func (*Graph) MarshalYAML

func (g *Graph) MarshalYAML() (any, error)

MarshalYAML returns the graph's [graphData] serialized shape for the YAML encoder to serialize.

Returns:

  • `any`: the [graphData] serialized-form value.
  • `error`: non-nil if packing a content resource fails.

func (*Graph) Nodes

func (g *Graph) Nodes() []*Node

Nodes returns all nodes in the graph by walking the tree recursively.

The returned slice is in tree-walk order (depth-first, declaration order).

Returns:

  • `[]*Node`: the flat node list in tree-walk order; nil when no nodes are present.

func (*Graph) Origin

func (g *Graph) Origin() Origin

Origin returns the tool-stamped graph metadata as a shallow value copy.

The struct's scalar fields (Scope, SourceRoot, TargetPlatform, Tool, TargetRoot) are copy-safe. Its map and slice fields (CommitHashes, DirtyLayers, Features, Layers, Packages, Projects, Segments, Settings) share underlying storage with the original — mutations to those reference-typed children would reach back. Callers must treat the returned value as read-only.

Never nil: the graph stores the concrete OriginBase carrier by value ([graphMetadata.origin]), so a graph built without an origin — or loaded from a document carrying none — reports the zero origin, whose Tool and Scope are empty strings. Callers test those, never the interface against nil.

Returns:

  • `Origin`: the tool-stamped metadata; the zero origin when none was supplied.

func (*Graph) Parameters

func (g *Graph) Parameters() ([]Parameter, error)

Parameters returns the bubble-up variable surface of the graph.

It is the deduplicated, type-checked set of VariableBinding references walked across the root subgraph's children (plan-doc D3). It is consumed by the executor's preflight pass to drive VariableResolver.Resolve.

Returns:

  • `[]Parameter`: the bubble-up surface, stable-sorted by Name. Returned even when `error` is non-nil, so callers can render a best-effort surface alongside the diagnostic.
  • `error`: an errors.Join of any same-name-different-type collisions detected during the walk; nil when the walk succeeded without violations.

func (*Graph) ResolveExecutable

func (g *Graph) ResolveExecutable(id string) (ExecutableUnit, error)

ResolveExecutable returns the executable unit with the given ID, or an error if no such unit exists.

Nodes and subgraphs share one ID space (Phase 7 invariant); ResolveExecutable is the single lookup gather, choose, and other combinators use to resolve a body reference.

Parameters:

  • `id`: the executable unit identifier to resolve.

Returns:

  • `ExecutableUnit`: the resolved unit (Root, a Subgraph descendant, or a Node).
  • `error`: non-nil when no descendant or root matches `id`.

func (*Graph) ResourceCatalog

func (g *Graph) ResourceCatalog() *ResourceCatalog

ResourceCatalog returns the ResourceCatalog carried by the graph from planning into execution.

Returns:

  • `*ResourceCatalog`: the catalog pointer; callers must not mutate the catalog after graph construction.

func (*Graph) Root

func (g *Graph) Root() *Subgraph

Root returns the graph's root subgraph.

Returns:

  • `*Subgraph`: the root subgraph pointer; callers must not mutate the subgraph after graph construction.

func (*Graph) SerialVersion

func (g *Graph) SerialVersion() uint32

SerialVersion returns the graph format version stamped at construction.

Returns:

func (*Graph) Serialize

func (g *Graph) Serialize(encoder Encoder) error

Serialize writes this graph through `encoder`, selecting JSON or YAML by the encoder's concrete type.

Dispatches to Graph.MarshalJSON or Graph.MarshalYAML. The result is the symbol-table serialized form: top-level `children` IDs from Root, plus the flat `subgraphs` and `nodes` lists sorted by ID.

Whatever value is currently in Graph.Checksum is emitted as-is; this method does not (re)compute it. Callers that want a fresh checksum compute it from Graph.CanonicalContent and assign before calling.

Usage:

encoder := yaml.NewEncoder(file)
encoder.SetIndent(2)
defer encoder.Close()
g.Serialize(encoder)

Parameters:

  • `encoder`: the destination encoder; both *json.Encoder and *yaml.Encoder satisfy Encoder.

Returns:

  • `error`: the encoder's error, or nil on success.

func (*Graph) SignWith

func (g *Graph) SignWith(sign func(canonical []byte) (*Signature, error)) error

SignWith signs the graph through `sign`, setting the signature exactly once.

The seam keeps pkg/op crypto-free: this method supplies the canonical bytes and stores the result; the signer (pkg/signing) owns the ciphersuite and key custody. The checksum is unaffected — [CanonicalContent] excludes both checksum and signature — so signing does not change the graph's identity. A graph signs at most once; re-signing an already-signed graph is refused.

Parameters:

  • `sign`: computes the *Signature over the canonical bytes (the signer prefixes its namespace).

Returns:

  • `error`: non-nil when the graph is already signed, canonicalization fails, or `sign` fails.

func (*Graph) Signature

func (g *Graph) Signature() *Signature

Signature returns the graph's publisher signature, or nil when the graph is unsigned.

Returns:

  • `*Signature`: the signature pointer, or nil.

func (*Graph) SubgraphByID

func (g *Graph) SubgraphByID(id string) *Subgraph

SubgraphByID returns the descendant subgraph with the given ID, or nil if no descendant has that ID.

Searches the tree recursively; the graph root is never returned.

Parameters:

  • `id`: the Subgraph ID to find.

Returns:

  • `*Subgraph`: the matching descendant, or nil.

func (*Graph) Subgraphs

func (g *Graph) Subgraphs() []*Subgraph

Subgraphs returns every *Subgraph descendant of the graph's root.

The result does NOT include the root subgraph itself — it lists only authored / planner-emitted container units below it. Used by Graph.UnitCount and by harness assertions that want to count or inspect every executable unit produced by `plan.assemble_definition`.

Returns:

  • `[]*Subgraph`: the descendant subgraphs in tree-walk order.

func (*Graph) Timestamp

func (g *Graph) Timestamp() time.Time

Timestamp returns when the graph was created.

Returns:

  • `time.Time`: the construction timestamp set at NewGraph.

func (*Graph) UnitCount

func (g *Graph) UnitCount() int

UnitCount returns the total count of ExecutableUnit descendants of the graph's root.

Both *Node and *Subgraph are children. The count excludes the root itself.

This is the count the harness asserts against via `ctx.assert_equal(graph.unit_count(), n)`: a `plan.choose` container materializes as a Subgraph that holds its branch's children, so a script with `write_text` + `exists` + `choose(then=remove)` produces unit count 4 (3 Nodes + 1 Subgraph), not 3.

Returns:

  • `int`: the total descendant-unit count.

type GraphExecutor

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

GraphExecutor executes a planned *Graph under a *RuntimeEnvironmentSpec.

One executor drives one execution. GraphExecutor.Run builds a per-run *RuntimeEnvironment from the spec, clones the graph's planning catalog onto that environment, dispatches the graph, then tears the environment down. Each Run gets an independent working catalog while the graph's planning catalog stays pristine — but each Run requires a fresh executor; a second GraphExecutor.Run call on the same executor returns an error. Reexecution = `NewGraphExecutor(graph, spec)` again; resuming from a paused execution rebuilds the executor from a serialized *Trace.

func NewGraphExecutor

func NewGraphExecutor(graph *Graph, spec *RuntimeEnvironmentSpec) *GraphExecutor

NewGraphExecutor returns an executor bound to `graph` and `spec`, in the preparing phase (PhasePreparing).

The executor drives a single execution. GraphExecutor.Run builds a fresh *RuntimeEnvironment from `spec`, clones the graph's planning catalog onto it, dispatches the graph, and tears the environment down — so the executor itself is cheap. Re-running the same graph means constructing a new executor; GraphExecutor.Run rejects a second call against the same executor.

Parameters:

  • `graph`: the planned graph. Must be non-nil.
  • `spec`: the session configuration. Must be non-nil.

Returns:

  • *GraphExecutor: the configured executor.

func ResumeExecutor

func ResumeExecutor(graph *Graph, spec *RuntimeEnvironmentSpec, trace *Trace) (*GraphExecutor, error)

ResumeExecutor constructs a *GraphExecutor ready to continue dispatch from a *Trace's state.

The trace's Trace.GraphChecksum must match `graph.Checksum()` — a mismatch indicates the graph has changed since the pause and the trace is incompatible. On success the returned executor has its RunStatus, *RecoveryStack, and resolved variables restored from the trace; a subsequent GraphExecutor.Run continues dispatch from that point, skipping units whose UnitID already appears in the recovery stack with a successful receipt.

Parameters:

  • `graph`: the planned graph the trace was taken against. Must be non-nil.
  • `spec`: the session configuration for the resumed execution. Must be non-nil.
  • `trace`: the captured execution state. Must be non-nil and graph-compatible.

Returns:

  • *GraphExecutor: the executor ready to resume.
  • `error`: non-nil on nil arguments or checksum mismatch.

func (*GraphExecutor) Control

func (e *GraphExecutor) Control() *ControlPlane

Control returns the run's *ControlPlane.

The plane is the surface a consumer uses to issue commands (ControlPlane.Request) and subscribe to events (ControlPlane.Subscribe).

Returns:

  • `*ControlPlane`: the run's control plane; never nil.

func (*GraphExecutor) LastVariables

func (e *GraphExecutor) LastVariables() map[string]Variable

LastVariables returns a snapshot of the resolved variable map from the most recent GraphExecutor.Run.

Empty before the first Run; preserved across the Run teardown. Cleared and re-populated by each Run.

Returns:

  • map[string]Variable: the resolved variables; never nil, may be empty when no parameters bubbled up.

func (*GraphExecutor) Pause

func (e *GraphExecutor) Pause() error

Pause requests PhaseRunningPhasePaused at the next control-point.

A thin convenience over ControlPlane.Request(ControlPause).

Pause returns immediately (fire-and-forget); the transition happens on the goroutine driving GraphExecutor.Run when it next drains the command, at which point Run returns ErrPaused with GraphExecutor.RunStatus reporting PhasePaused. If the run terminates before the control-point is reached, the request is silently dropped. A caller that wants the acknowledgement issues ControlPlane.Request directly and reads the response channel.

Safe to call from any goroutine.

Returns:

  • `error`: non-nil when the executor is not in PhaseRunning (nothing to pause).

func (*GraphExecutor) ResumeUnwind

func (e *GraphExecutor) ResumeUnwind(ctx context.Context) error

ResumeUnwind drives the resumed state-checked unwind of a `stopped × compensation_failed` trace.

The Restart half of the step-21 contract, and the one sanctioned downward condition move.

Resume is an unwind, NOT a forward retry: the retained recovery journal names the candidate set, and every entry's Compensate re-runs against the live filesystem — the framework does not assume the operator unwound while clearing the blocker, it observes (an already-cleared state no-ops through the compensators' own tolerance; a still-failing compensation fails again). A clean unwind clears the journal and de-escalates the run to `stopped × execution_failed` (ReasonUnwound, journaled) — the clean baseline from which re-running forward is a fresh, explicit run. A dirty unwind leaves the run at `stopped × compensation_failed` with the fresh diagnostics retained.

The ledger rehydrate and stack re-arm mirror GraphExecutor.Run's resumed branch; the two paths stay separate because Run resumes FORWARD execution from a paused trace, which this contract explicitly forbids for a compensation-failed one.

Parameters:

  • `ctx`: the context for the unwind's runtime environment.

Returns:

  • `error`: non-nil when the executor is not at `stopped × compensation_failed`, when restoring the resumed state fails, or when any compensation fails again (the joined errors).

func (*GraphExecutor) Run

func (e *GraphExecutor) Run(ctx context.Context, variables map[string]Variable) (any, error)

Run dispatches the executor's graph under a fresh per-run *RuntimeEnvironment.

At every Run:

  1. Build a fresh *RuntimeEnvironment from the stored spec, bound to `ctx` — minting the spec's Root from its anchor path and mode; a mint failure lands the preflight-failed terminal without dispatching.
  2. Clone `graph.Catalog` onto the new environment's Catalog. The clone is independent — Resources written by this Run cannot reach back into the graph's planning catalog.
  3. Rebind the graph onto the per-run environment.
  4. Preflight: [GraphExecutor.bindVariables] resolves the graph's parameter surface against the spec's application.Application source maps; caller-supplied `variables` layer on top as the highest-priority source.
  5. Dispatch through [Graph.dispatch].
  6. On error, unwind the recovery stack so every successfully-completed Action gets its Compensate companion called.
  7. Close the env (deferred); clear the transient `e.environment` and `e.variables` fields.

The stop contract: the result and error come through the return; the terminal run status (phase × condition × reason) is read separately via GraphExecutor.RunStatus (or GraphExecutor.Trace). The error reflects whether the run HALTED — a stop, an unhandled or asserted failure, or a pause — while the condition reflects the run's HEALTH, so a run whose TransitionPolicy continues past a failure returns `(result, nil)` yet reports `completed × execution_failed` ("ran to the end despite a failure").

Parameters:

  • `ctx`: the per-run cancellation context. Its values flow through `RuntimeEnvironment.Context` into providers and subprocesses.
  • `variables`: caller-supplied variable bindings layered on top of the resolver's output. Pass nil or an empty map for the common case where the resolver alone produces the variable surface.

Returns:

  • `any`: the final dispatch's result — the terminal node's output value; nil on a failure / pause, or when no node produced output.
  • `error`: non-nil when the run halts (preflight failure, an unhandled or asserted failure that stopped the run, or a pause via ErrPaused); nil when the run completes — including a `completed × degraded` or `completed × execution_failed` outcome the policy continued through.

func (*GraphExecutor) RunStatus

func (e *GraphExecutor) RunStatus() RunStatus

RunStatus returns the executor's current RunStatus triplet.

Concurrent-safe to read at any point; the field is mutated only by the goroutine driving GraphExecutor.Run (and by GraphExecutor.Pause's observation of the pause flag at the next pause-point).

Returns:

  • `RunStatus`: the current phase × condition × reason triplet.

func (*GraphExecutor) SetHooks

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

SetHooks installs `hooks` as the lifecycle hook registry for every Run.

Parameters:

  • `hooks`: the hook registry to install.

func (*GraphExecutor) Stop

func (e *GraphExecutor) Stop() error

Stop requests a halt-and-terminate at the next control-point.

A thin convenience over ControlPlane.Request(ControlStop).

Stop returns immediately (fire-and-forget); at the next control-point Run unwinds (compensating completed work) and lands PhaseStopped, returning ErrStopped. Unlike GraphExecutor.Pause, a stopped run is **not** resumable — the recovery stack is spent by the unwind. A caller that wants the acknowledgement issues ControlPlane.Request directly and reads the response channel.

Safe to call from any goroutine.

Returns:

  • `error`: non-nil when the executor is not in PhaseRunning (nothing to stop).

func (*GraphExecutor) Trace

func (e *GraphExecutor) Trace() *Trace

Trace projects the executor's current per-run mutable state into a serializable *Trace.

Pairs with the executor's bound *Graph (loaded separately via LoadGraph) to fully describe the execution. The graph identity is captured as Trace.GraphChecksum for resume-time verification. Safe to call at any point — before, during, or after GraphExecutor.Run — the stack and variables fields are nil-safe.

Returns:

  • *Trace: the captured state.

func (*GraphExecutor) Transition

func (e *GraphExecutor) Transition(unitID string, condition Condition, reason Reason, message string) error

Transition records a condition flip on this executor's run status and reports whether it was accepted.

The single mutator of the run-status machine and its sole choke point: every condition flip goes through here, so none escapes the journal. Condition only worsens *within a run* — a request below the current condition is rejected (returns a non-nil error) rather than silently clamped, which is how monotonicity is enforced under the submission model. The one legal downward move is the resume de-escalation — a resumed run whose state-checked unwind clears a `compensation_failed` trace back to `execution_failed` — and it enters through resume (ResumeExecutor sets the status directly), not through Transition; that full resumed-unwind is step 21. A request equal to the current condition is a no-op (flips-only: a repeat driver, e.g. a second flow.Degraded while already degraded, is a receipt, not a transition). Phase is not an argument — the executor owns phase moves (lifecycle events and the policy reaction). `At` is stamped internally. An aberrant flip also records the effective TransitionPolicy reaction (unit ?? graph ?? floor) as this executor's pending reaction — so the flip and its policy reaction are one atomic act at the one choke point.

Parameters:

  • `unitID`: the unit whose outcome drove the flip; empty for run-level events.
  • `condition`: the Condition being entered — must be at or above the current condition.
  • `reason`: the Reason token classifying the flip.
  • `message`: free-text detail (typically an err.Error()), carried on the status and the journal entry.

Returns:

  • `error`: non-nil when the request would de-escalate the condition (rejected); nil when applied or a no-op.

type GraphSpec

type GraphSpec struct {
	Root            SubgraphSpec
	Origin          Origin
	ResourceCatalog *ResourceCatalog
	// contains filtered or unexported fields
}

GraphSpec is the fluent builder for a *Graph. A Graph is a document container, not an ExecutableUnit, so the spec has no ID / action / annotations of its own; instead it carries the root subgraph's spec (GraphSpec.Root) plus graph-level metadata (origin, resource catalog, SOPS client). The root-shaped `With*` setters delegate to Root, and NewGraph hands `&spec.Root` to NewSubgraph. The root spec is seeded by NewGraphSpec (ID "root", binding "flow.subgraph" by name). Hand a populated spec to NewGraph.

func NewGraphSpec

func NewGraphSpec() *GraphSpec

NewGraphSpec returns a *GraphSpec whose root is seeded with the canonical root spec and is ready for fluent population via its With* setters.

Seeding the root means every graph's root has ID "root" and binds "flow.subgraph" by name (resolved at dispatch) — the root runs through the same bound-action path as every other subgraph. This is the single root call site: inlining the spec here (rather than a shared factory) guarantees no other site can produce a divergent root. Because SubgraphSpec.WithActionNamed validates the action name against the global registry, NewGraphSpec requires the flow provider to be announced.

Returns:

  • `*GraphSpec`: a graph spec with its root pre-seeded.

func (*GraphSpec) WithElevationOffer

func (s *GraphSpec) WithElevationOffer(elevationOffer *ElevationOffer) *GraphSpec

WithElevationOffer sets the root subgraph's ElevationOffer and returns the spec for chaining.

Parameters:

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithOnError

func (s *GraphSpec) WithOnError(onError *Subgraph) *GraphSpec

WithOnError sets the root subgraph's failure-handler and returns the spec for chaining.

Parameters:

  • `onError`: the handler Subgraph, or nil for no error action.

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithOnRetry

func (s *GraphSpec) WithOnRetry(onRetry *Subgraph) *GraphSpec

WithOnRetry sets the root subgraph's per-attempt retry-handler and returns the spec for chaining.

Parameters:

  • `onRetry`: the retry-handler Subgraph, or nil for no retry handler.

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithOrigin

func (s *GraphSpec) WithOrigin(origin Origin) *GraphSpec

WithOrigin sets the tool-stamp Origin and returns the spec for chaining.

Parameters:

  • `origin`: the graph's Origin; the zero value is permitted.

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithResourceCatalog

func (s *GraphSpec) WithResourceCatalog(catalog *ResourceCatalog) *GraphSpec

WithResourceCatalog sets the *ResourceCatalog the graph carries from planning into execution.

Parameters:

  • `catalog`: the *ResourceCatalog; nil defaults to a fresh empty catalog at construction.

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithRetryPolicy

func (s *GraphSpec) WithRetryPolicy(retryPolicy *RetryPolicy) *GraphSpec

WithRetryPolicy sets the root subgraph's RetryPolicy and returns the spec for chaining.

Parameters:

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithSlot

func (s *GraphSpec) WithSlot(name string, value Binding) *GraphSpec

WithSlot binds one root-subgraph slot value by name and returns the spec for chaining.

Parameters:

  • `name`: the slot (frame-binding) name.
  • `value`: the Binding to bind.

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithTimestamp

func (s *GraphSpec) WithTimestamp(timestamp time.Time) *GraphSpec

WithTimestamp records when the planning session that produced this graph began.

Supplied, never read: graph construction is a pure function of its spec, so two runs of the same plan build byte-identical graphs (#690). The value is RuntimeEnvironment.ConceivedAt -- time of conception, not time of birth -- so every graph a session assembles shares it and they stay comparable.

An unset timestamp is the zero time, which serializes as such. That is a provenance gap, never a correctness one: identity does not depend on it (Graph.CanonicalContent omits it, as it always has omitted the signature).

Parameters:

  • `timestamp`: when the session began.

Returns:

  • `*GraphSpec`: the spec, for chaining.

func (*GraphSpec) WithTransitionPolicy

func (s *GraphSpec) WithTransitionPolicy(transitionPolicy *TransitionPolicy) *GraphSpec

WithTransitionPolicy sets the root subgraph's TransitionPolicy and returns the spec for chaining.

Parameters:

Returns:

  • `*GraphSpec`: the receiver, for chaining.

func (*GraphSpec) WithUnits

func (s *GraphSpec) WithUnits(units ...ExecutableUnit) *GraphSpec

WithUnits sets the top-level ExecutableUnit children of the graph's root subgraph.

Parameters:

  • `units`: the units, in planned order; replaces any prior set.

Returns:

  • `*GraphSpec`: the receiver, for chaining.

type GuardResult

type GuardResult uint8

GuardResult keys a conditional edge to an outcome of the guard.

The guard is the truthiness evaluation of the From node's result: a decision node's out-edges each carry the GuardResult they are followed on, so the graph's topology — not any method body — routes execution (phase-8 step 10, the choose decision tree). Serialized over [guardResultNames] ("none" / "truthy" / "falsy") in both document formats — GuardResult.MarshalText for JSON, GuardResult.MarshalYAML for gopkg.in/yaml.v3, which does not honor encoding.TextMarshaler.

const (
	// GuardNone means the edge is unguarded: a plain ordering edge, always followed. As the zero value it is the
	// default, and with omitempty it never appears in serialized output, so existing traces are unchanged.
	GuardNone GuardResult = iota
	// GuardTruthy is followed when the From node's result is truthy in the Python sense: non-nil, non-false,
	// non-zero, non-empty. See [IsTruthy] for the exact rules.
	GuardTruthy
	// GuardFalsy is followed when the From node's result is falsy.
	GuardFalsy
)

func (GuardResult) MarshalText

func (g GuardResult) MarshalText() ([]byte, error)

MarshalText encodes this guard result as its serialized name.

Satisfies encoding.TextMarshaler, so JSON documents carry "truthy" / "falsy" rather than a bare integer.

Returns:

  • `[]byte`: the name from [guardResultNames].
  • `error`: non-nil when the value is out of range.

func (GuardResult) MarshalYAML

func (g GuardResult) MarshalYAML() (any, error)

MarshalYAML encodes this guard result as its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextMarshaler, so YAML documents need this companion to carry "truthy" / "falsy" rather than a bare integer.

Returns:

  • `any`: the name from [guardResultNames], as a string.
  • `error`: non-nil when the value is out of range.

func (GuardResult) String

func (g GuardResult) String() string

String returns this guard result's serialized name.

Returns:

  • `string`: the name from [guardResultNames], or "GuardResult(<n>)" for an out-of-range value.

func (*GuardResult) UnmarshalText

func (g *GuardResult) UnmarshalText(text []byte) error

UnmarshalText decodes a guard result from its serialized name.

Satisfies encoding.TextUnmarshaler for JSON documents.

Parameters:

  • `text`: one of the [guardResultNames] entries.

Returns:

  • `error`: non-nil when `text` names no guard result.

func (*GuardResult) UnmarshalYAML

func (g *GuardResult) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes a guard result from its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextUnmarshaler, so YAML documents need this companion.

Parameters:

  • `unmarshal`: the YAML node decoder supplied by the yaml package.

Returns:

  • `error`: non-nil when the node is not a string or names no guard result.

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(runtimeEnvironment *RuntimeEnvironment, nodeID string, result Result, err error)

FireNodeComplete notifies all hooks that a node has finished.

func (*HookRegistry) FireNodeStart

func (r *HookRegistry) FireNodeStart(runtimeEnvironment *RuntimeEnvironment, nodeID string, slots map[string]any)

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

func (*HookRegistry) FireSubgraphComplete

func (r *HookRegistry) FireSubgraphComplete(runtimeEnvironment *RuntimeEnvironment, subgraphID string, err error)

FireSubgraphComplete notifies all hooks that a subgraph has finished.

func (*HookRegistry) FireSubgraphStart

func (r *HookRegistry) FireSubgraphStart(runtimeEnvironment *RuntimeEnvironment, subgraphID string)

FireSubgraphStart notifies all hooks that a subgraph is about to execute.

func (*HookRegistry) Register

func (r *HookRegistry) Register(hook LifecycleHook)

Register adds a lifecycle hook to the registry.

type ImmediateBinding

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

ImmediateBinding is a Go value known at plan time.

func NewImmediateBinding

func NewImmediateBinding(value any) ImmediateBinding

NewImmediateBinding returns an ImmediateBinding wrapping a Go value known at plan time.

Parameters:

  • `value`: the plan-time value to bind (any Go value, including a Resource).

Returns:

  • `ImmediateBinding`: the binding.

func (ImmediateBinding) Edge

func (b ImmediateBinding) Edge(_ string) *Edge

Edge returns nil: an immediate value has no producing unit, so it induces no dependency edge.

A Resource carried as an immediate value contributes its dependency through its own stamped producer (Resource.ProducerID), discovered wherever the resource flows — not through this binding.

Parameters:

  • `consumer`: the id of the consuming unit (ignored).

Returns:

  • `*Edge`: always nil.

func (ImmediateBinding) Resolve

func (b ImmediateBinding) Resolve(_ map[string]Variable, _ *RecoveryStack) any

Resolve returns the wrapped Go value verbatim.

Both inputs are ignored.

Parameters:

  • `variables`: the resolved variable map (ignored).
  • `stack`: the recovery stack (ignored).

Returns:

  • `any`: the wrapped value.

type IntentEntry

type IntentEntry struct {

	// ID is the catalog id (`res-N`) the claim was minted under; restored verbatim on load.
	ID string `json:"id" yaml:"id"`

	// URI is the resource's identity, from which the concrete Resource object is rebuilt on load.
	URI string `json:"uri" yaml:"uri"`
}

IntentEntry is one row of the graph document's resource catalog — input intent, nothing else.

`{id, uri}` and no more (ruled 2026-08-21, 4-resource-management.md §5.4): presence in the section IS the pending claim — pending is definitional, not recorded — and state, producer, Etag, and Digest are trace vocabulary (LedgerEntrySnapshot), where observation genuinely varies. The intent row is its own type precisely so the graph document cannot say more than intent.

type Invocation

type Invocation struct {
	Target ExecutableUnit // workflow-level unit that will dispatch when executed
	Label  string         // registered (user-supplied or auto-generated)
}

Invocation is the handle dispatch constructs for every plan.* call and the starlark value every plan.* call returns to the author. Target is the op-level unit the invocation will dispatch (a *Node or *Subgraph).

Per phase-8 D2, the binding layer picks how to bind an invocation from the target parameter's type at the binding site: slots typed ExecutableUnit consume Target directly; value-typed slots consume the invocation's PromiseBinding (via Invocation.Binding), which carries the producer's ID for plan.run to materialize into an edge.

func (*Invocation) Binding

func (i *Invocation) Binding() Binding

Binding returns the PromiseBinding that binds a consumer slot to this invocation's producer.

Preserves the detachment contract (phase-8 D5): the returned PromiseBinding carries the producer's ID and plan.run materializes the producer→consumer edge at graph construction. The caller places it into a spec slot via WithSlot — no node is mutated.

Returns:

type InvocationRegistry

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

InvocationRegistry is the session-scoped ledger of every Invocation constructed during plan-time evaluation.

Entries are appended to ordered in creation order and indexed in byLabel by the label under which they were registered. Auto-labeling uses a per-provider.method counter: InvocationRegistry.AutoLabel formats a monotonic label of the form "<providerMethod>#<N>" whose ordinal is unique for that providerMethod within the registry's lifetime. Every call is protected by a single mutex; the registry is written only during plan-time evaluation and frozen after plan.run is invoked (Phase 8 invariant I3).

func NewInvocationRegistry

func NewInvocationRegistry() *InvocationRegistry

NewInvocationRegistry creates an empty registry.

Returns:

  • *InvocationRegistry: the empty registry.

func (*InvocationRegistry) All

func (r *InvocationRegistry) All() []*Invocation

All returns every registered invocation in creation order.

The returned slice is a shallow copy safe for the caller to iterate without holding the registry lock. It is used by the plan-end orphan walk (D4) and the plan-time type-check pass (D8).

Returns:

  • []*Invocation: the registered invocations in creation order.

func (*InvocationRegistry) AutoLabel

func (r *InvocationRegistry) AutoLabel(providerMethod string) string

AutoLabel returns the next auto-generated label for `providerMethod`.

The label has the form "<providerMethod>#<N>" where N is a 1-based ordinal that increments monotonically per `providerMethod` across the registry's lifetime. Callers use this when the author did not supply an explicit label via [Options.Label].

Parameters:

  • providerMethod: the "<provider>.<method>" identifier (e.g., "file.write_text", "plan.choose").

Returns:

  • string: the formatted auto-label.

func (*InvocationRegistry) ByLabel

func (r *InvocationRegistry) ByLabel(label string) *Invocation

ByLabel returns the invocation registered under label, or nil if no such label exists.

Parameters:

  • `label`: the label to look up.

Returns:

  • `*Invocation`: the registered invocation, or nil if not found.

func (*InvocationRegistry) Register

func (r *InvocationRegistry) Register(label string, invocation *Invocation) error

Register appends invocation to the ordered list and inserts it into byLabel under the given label.

Duplicate labels return an error without modifying either structure. Callers are expected to either supply a user-provided label (from [Options.Label]) or an auto-generated one from InvocationRegistry.AutoLabel.

Parameters:

  • `label`: the unique label for this invocation.
  • `invocation`: the invocation to register.

Returns:

  • `error`: non-nil if label is already registered.

func (*InvocationRegistry) Reset

func (r *InvocationRegistry) Reset()

Reset clears every registered invocation and the auto-label counters.

Called by [plan.Provider.Clear] to reset the registry between distinct planning passes. Previously- assembled Graphs that hold their own references to *Invocation values are unaffected — Reset only drops the registry's view; the invocations themselves remain valid wherever they are still held.

type KindMismatcher

type KindMismatcher interface {

	// MismatchesKind reports whether the path holds an entry this claim's kind does not admit.
	//
	// Consulted only when the claim has already failed to verify, so the answer distinguishes "nothing
	// is there" (false) from "something else is there" (true).
	MismatchesKind() bool
}

KindMismatcher reports whether a claim's path holds an entry of a different kind than the claim asserts — the seam that separates absence from mismatch when a claim fails to verify.

Optional, like RootBinder and KindResolver: a scheme with no kind axis never implements it, and its claims fail only ever as absence.

type KindResolver

type KindResolver interface {

	// ResolveKind returns the typed resource the observation names, freshly built and uninterned.
	//
	// Called only on the Active branch of a transition, so the entry is known to exist. A nil resource
	// with a nil error means "nothing to resolve" and leaves the entry as it is.
	ResolveKind() (Resource, error)
}

KindResolver is the kind-resolution seam (docs/plans/any-entry-claims.md, ruled 2026-08-23): a claim that asserts existence without asserting kind becomes the kind the world actually holds, at the moment the model first looks.

An unasserted claim is in effect a promise to observe. Pre-flight's PendingActive transition is where the model first consults the world, so it is where the promise comes due: the entry is replaced with the typed resource the observation names, once, at the consuming scope's starting line. The Gone branch resolves nothing — nothing was observed, so there is nothing to resolve to, and the entry honestly records an unmet unasserted claim.

The catalog drives it and carries identity across the swap, because identity is the catalog's business: an implementation returns a freshly typed resource and does NOT stamp the catalog id or producer onto it. Resources whose scheme has no kind axis — every scheme but `file` today — simply do not implement the interface, and their transitions are untouched.

type LedgerEntrySnapshot

type LedgerEntrySnapshot struct {

	// ID is the catalog id (`res-N`) — the stable identity the recovery stack references.
	ID string `json:"id" yaml:"id"`

	// URI is the resource's URI, from which the concrete Resource object is rebuilt on rehydration.
	URI string `json:"uri" yaml:"uri"`

	// ProducerID is the producing unit's id, or empty for a discovery entry.
	ProducerID string `json:"producer_id,omitempty" yaml:"producer_id,omitempty"`

	// State is the entry's lifecycle state at capture time.
	State ResourceState `json:"state" yaml:"state"`

	// Etag is the entry's cheap change-detection token at capture time (phase-8 step 48). Drift consumers
	// compare a live Etag against this first and compute a Digest only on mismatch — the catalog's own cascade.
	// A recorded Etag equal to the entry's URI is the uninformative [ResourceBase] default; consumers bypass
	// the screen and compare digests directly. Captured best effort for Active entries only; reporting
	// metadata — [ResourceLedgerSnapshot.Rehydrate] ignores it.
	Etag string `json:"etag,omitempty" yaml:"etag,omitempty"`

	// Digest is the entry's honest content identity at capture time, in the canonical "<algo>:<hex>" form
	// (phase-8 step 48) — the as-deployed record drift attribution compares against (source-changed vs.
	// target-modified). Captured best effort for Active entries only (a digest error — e.g. the directory case,
	// deferred to step 23's Merkle deliverable — leaves it empty); reporting metadata —
	// [ResourceLedgerSnapshot.Rehydrate] ignores it.
	Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`

	// DestroyedBy is the unit that destroyed this resource — the destroyer stamp on a mutator-side [Gone]
	// transition (ruled 2026-08-20), symmetric with ProducerID. Empty for reactive Gone transitions and
	// every non-Gone state; reporting metadata — [ResourceLedgerSnapshot.Rehydrate] ignores it.
	DestroyedBy string `json:"destroyed_by,omitempty" yaml:"destroyed_by,omitempty"`
}

LedgerEntrySnapshot is one ledger generation's serializable identity and lifecycle state.

type LifecycleHook

type LifecycleHook interface {
	OnNodeStart(runtimeEnvironment *RuntimeEnvironment, nodeID string, slots map[string]any)
	OnNodeComplete(runtimeEnvironment *RuntimeEnvironment, nodeID string, result Result, err error)
	OnSubgraphStart(runtimeEnvironment *RuntimeEnvironment, subgraphID string)
	OnSubgraphComplete(runtimeEnvironment *RuntimeEnvironment, subgraphID string, err error)
}

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

type Method

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

Method describes a callable method on a provider or resource.

It is shared metadata used by both action receiverTypes and starlark receivers. Actions wrap a Method for graph dispatch. Starlark receivers wrap a Method for immediate dispatch. Method itself is neither — it is the callable they both delegate to.

Any method of a provider may have a plan companion; no method need have one. Companions are discovered by reflection on the receiver type, using a name-prefix convention:

  • `plan (Plan<Name>)`: plan-time output spec, computes the identity of the resource the method will produce from the same inputs. Pure — no I/O.
  • `undo (Compensate<Name>)`: compensation companion for compensable methods, takes the compensator returned by the forward method and reverses its effect.

func NewMethod

func NewMethod(
	do *reflect.Method,
	parameters []Parameter,
	plan *reflect.Method,
	undo *reflect.Method,
	enforceCompanions bool,
) (*Method, error)

NewMethod creates a Method from a reflected method, its parameter names, and its optional plan and undo companions.

Classification rules:

Returns an error if:

  • paramNames length doesn't match method parameter count (excluding receiver)
  • return signature does not match any known method kind
  • plan companion provided for a method that produces no result
  • plan companion parameter list differs from do
  • plan companion return signature is not (T, error) where T matches `do`'s first result
  • compensable method has no Compensate companion (if enforceCompanions is true)
  • Compensate companion signature is invalid

Parameters:

  • `do`: the reflected Go method to wrap.
  • `parameters`: parsed Parameter values matching the method's non-receiver parameters. Token-form parsing happens upstream in parseParameters at the announcement boundary; NewMethod consumes typed Parameters only.
  • `plan`: the Plan<Name> companion method, or nil if the method has no plan companion.
  • `undo`: the Compensate companion method, or nil for non-compensable methods.
  • `enforceCompanions`: true if this method belongs to a provider; enables companion requirements.

Returns:

  • `*Method`: the classified method.
  • `error`: non-nil if validation fails.

func (*Method) ActionName

func (m *Method) ActionName() string

ActionName returns the canonical action name for this method.

Returns:

  • `string`: the canonical `<pkg-path>.<receiver>.<method>` action name computed at construction.

func (*Method) Claims

func (m *Method) Claims() MethodClaims

Claims returns the guarantees this method asserts.

A claim is an author's assertion checked by codegen, never a fact derived from the signature — that is what separates it from Method.Kind and from Method.Mutates. The zero value asserts nothing, which admits the method nowhere that requires a guarantee.

Returns:

  • `MethodClaims`: the claim set, empty when the method declared none.

func (*Method) Do

func (m *Method) Do(receiver any, args []any) (result, compensator reflect.Value, err error)

Do dispatches a method call directly with Go arguments, returning reflected values.

Parameters:

  • `receiver`: the provider or resource value the method is called on; auto-addressed when passed by value.
  • `args`: the Go arguments in declaration order, excluding the receiver.

Returns:

  • `reflect.Value`: the method's first result, or the zero Value for actions.
  • `reflect.Value`: the method's compensator (compensable third return), or the zero Value.
  • `error`: non-nil if the argument count is wrong or the method returned a non-nil error.

func (*Method) Invoke

func (m *Method) Invoke(activation *ActivationRecord, receiver any) (Result, Compensator, error)

Invoke coerces slot values into Go arguments via Convert and dispatches to the wrapped method.

Reads the resolved slot values from `activation.Slots` (stamped by the executor before the dispatch). Each parameter's value is looked up by name and converted to the parameter's declared Go type.

Parameters:

  • `activation`: the per-dispatch record carrying resolved slot values, runtime environment, and unit identity.
  • `receiver`: the provider or resource value the wrapped method is called on.

Returns:

  • `Result`: the method's unwrapped return value, or nil for actions.
  • `Compensator`: the committed Receipt or spliced *RecoveryStack, or nil when there is no compensator.
  • `error`: non-nil if slot conversion, dispatch, or receipt commit failed.

func (*Method) Kind

func (m *Method) Kind() MethodKind

Kind returns the classification of this method's signature.

Returns:

  • `MethodKind`: the signature classification computed at construction.

func (*Method) Modifiers

func (m *Method) Modifiers() MethodModifiers

Modifiers returns the surface modifiers stamped on this method.

Returns:

  • `MethodModifiers`: the modifier set, or [ModifierNone] when none were declared.

func (*Method) Mutates

func (m *Method) Mutates() bool

Mutates reports whether this method changes state outside the process.

Derived, never claimed: a method returning a receipt or a recovery stack is a mutator by construction, and the signature cannot drift without the classification following it. The converse does not hold — `shell.exec` and `git.pull` mutate without returning either — so a false result means "no receipt", not "no mutation".

Returns:

func (*Method) Name

func (m *Method) Name() string

Name returns the short name of the method.

Returns:

  • `string`: the method's short Go name.

func (*Method) ParameterByName

func (m *Method) ParameterByName(name string) (Parameter, bool)

ParameterByName returns the Parameter with the given name, if any.

Parameters:

  • `name`: the parameter name to look up.

Returns:

  • `Parameter`: the matching parameter, or the zero `Parameter` when none matches.
  • `bool`: true when a parameter with `name` exists.

func (*Method) Parameters

func (m *Method) Parameters() []Parameter

Parameters returns the named parameters of the method, excluding the receiver and any leading context.Context.

Returns:

  • `[]Parameter`: the named parameters, excluding the receiver and any leading *ActivationRecord.

func (*Method) Planner

func (m *Method) Planner() Planner

Planner returns the plan-mode dispatch strategy for this method.

Nil for resource methods (resources are not plan-dispatchable). Provider methods carry the planner declared at announcement; absent declaration means ActionPlanner.

Returns:

  • `Planner`: the dispatch strategy, or nil for resource methods.

func (*Method) ReceiverType

func (m *Method) ReceiverType() reflect.Type

ReceiverType returns the reflect.Type of the method's receiver.

Returns:

  • `reflect.Type`: the receiver's type, pointer or value as declared.

func (*Method) ResultType

func (m *Method) ResultType() reflect.Type

ResultType returns the reflect.Type of the method's first non-error result, or nil.

Returns:

  • `reflect.Type`: the first non-error result's type, or nil when the method returns nothing or only an error.

func (*Method) String

func (m *Method) String() string

String returns the full Go method signature in human-readable form.

Returns:

  • `string`: the full Go method signature in human-readable form.

func (*Method) Undo

func (m *Method) Undo(activation *ActivationRecord, receiver any, compensator Compensator) error

Undo calls the compensation companion on the receiver with the given activation and compensator.

The call goes through the registration-baked undo adapter (step 43): the companion's mandated activation-first shape (step 27's floor) was validated and closed over at NewMethod, so this is a plain call — no per-call reflection decisions.

Parameters:

  • `activation`: the per-dispatch record forwarded to the companion.
  • `receiver`: the provider value the companion is called on.
  • `compensator`: the compensator the forward method returned, reversed by the companion.

Returns:

  • `error`: the companion's error, or non-nil when the method has no compensation companion.

type MethodClaims

type MethodClaims uint

MethodClaims is a bit set of the guarantees a method asserts about itself.

It is orthogonal to MethodKind and to MethodModifiers: MethodKind classifies a return signature, a modifier records how a method is projected onto a starlark surface, and a claim states what the method promises. The set is codegen-emitted onto MethodMetadata and threaded onto the constructed Method.

Claims are asserted, not derived. The generator checks each one and fails the build on a false claim, so the check enforces a contract rather than discovering a value — a derived property would flip silently when a helper three calls down grew a new dependency. Mutation is the deliberate exception (Method.Mutates): a receipt return is a signature-level fact that cannot drift without changing the signature.

The zero value claims nothing, which is the fail-closed default: over-restrictive, never unsafe.

const (

	// ClaimDeterministic asserts that the method's output is a function of its declared inputs alone.
	//
	// Declared inputs are its arguments, values supplied to the [RuntimeEnvironment] at construction, and the
	// contents of the [fsroot.Dir] it was handed. Verified against the call graph: any reach into the capability
	// packages -- crypto/rand, math/rand, net, net/http, os, os/exec, os/signal, os/user, runtime, syscall, time --
	// fails the claim unless it routes through a sandboxed root. Subsequent flags double from here (2, 4, 8, …).
	ClaimDeterministic MethodClaims = 0x0001

	// ClaimIdempotent asserts that re-applying the method converges to the same state.
	//
	// Nothing static can check this, so it carries a test obligation instead: a method claiming it must have a
	// test that applies it twice and asserts convergence. Unlike the other two it applies to mutators, which is
	// why these are a claim namespace rather than a single classification.
	ClaimIdempotent MethodClaims = 0x0002

	// ClaimSandboxed asserts that every filesystem operation routes through an [fsroot.Dir].
	//
	// The confinement is the kernel's, via os.OpenRoot, so the claim is about which API the method calls rather
	// than about what it computes. Verified statically: a direct os.* filesystem call fails it unless the call
	// carries an `// Unsandboxed:` comment stating why the root cannot serve it.
	ClaimSandboxed MethodClaims = 0x0004
)

type MethodKind

type MethodKind int

MethodKind identifies the signature and capabilities of a method.

const (
	// MethodAction produces no result and cannot fail. Return: ().
	MethodAction MethodKind = iota

	// MethodFallibleAction produces no result but may fail. Return: (error).
	MethodFallibleAction

	// MethodFunction produces a result and cannot fail. Return: (T).
	MethodFunction

	// MethodFallibleFunction produces a result but may fail. Return: (T, error).
	MethodFallibleFunction

	// MethodCompensableFunction produces a result and compensator or an error. Return: (T, U, error).
	MethodCompensableFunction
)

type MethodMetadata

type MethodMetadata struct {
	Claims         MethodClaims    // verified guarantees (e.g. ClaimDeterministic); the empty set is the default
	ParameterNames []string        // starlark parameter name tokens, ordered to match the Go method's parameter slots
	Modifiers      MethodModifiers // surface modifiers (e.g. ModifierProperty); ModifierNone is the default
	Planner        reflect.Type    // optional; nil means default ActionPlanner
}

MethodMetadata is the codegen-emitted record describing one method on a registered provider.

Carries source-level information that Go reflection can't see: the starlark parameter spelling, any surface modifiers (e.g. eager property projection via ModifierProperty), and, optionally, the planner type that materializes the method's calls into an ExecutableUnit. Absent Planner means the method uses ActionPlanner — the default vanilla leaf-node dispatcher.

type MethodModifiers

type MethodModifiers uint

MethodModifiers is a bit set of per-method surface modifiers.

It is orthogonal to MethodKind: where MethodKind classifies a method's return signature (action vs. function), a modifier records how the method is projected onto a starlark surface. The set is codegen-emitted onto MethodMetadata and threaded onto the constructed Method; the zero value [ModifierNone] is the default callable projection.

const (

	// ModifierProperty marks a zero-arg getter ([MethodFunction] or [MethodFallibleFunction]) for property projection.
	//
	// A starlark attribute access calls the method and yields its result instead of returning the builtin. The codegen
	// sets it from a `+devlore:property` directive; it is valid only on zero-arg, value-returning methods (an action
	// has no value to project). Subsequent flags double from here (2, 4, 8, …).
	ModifierProperty MethodModifiers = 1 << 0
)

type MissingResourcePolicy

type MissingResourcePolicy int

MissingResourcePolicy is a consumer's declared response to a missing resource (ruled 2026-08-22; docs/architecture/4-resource-management.md §3, the claims taxonomy).

The parameter's TYPE is the declaration — no directive: a method with a MissingResourcePolicy-typed parameter and exactly one consumed (resource-typed) parameter links the two at announcement. A warning is produced whenever a missing resource is detected, under every policy. Aggregation across the consumers of one entry: Stop wins. A Skip variant ("do not dispatch") was considered and DROPPED (ruled 2026-08-22): its undo story is trivially clean — nothing ran, nothing to undo — but its forward side (nil-valued promises to downstream consumers; a trace that cannot tell "skipped" from "ran and produced nothing") buys machinery that Ignore never needs. Re-adding it later is purely additive.

const (
	// MissingResourcePolicyStop is the zero value and the default — fail-safe: a missing resource fails
	// the consuming scope as unmet intent. An unset policy can never accidentally tolerate.
	MissingResourcePolicyStop MissingResourcePolicy = 0

	// MissingResourcePolicyIgnore makes the call anyway: the provider sees the absence and handles it
	// (a remove no-ops), and the receipt records that the target was already absent.
	MissingResourcePolicyIgnore MissingResourcePolicy = 1
)

func (MissingResourcePolicy) MarshalJSON

func (p MissingResourcePolicy) MarshalJSON() ([]byte, error)

MarshalJSON serializes the policy as its canonical lowercase string — a document carries "stop", never a bare ordinal (the typed-value rule: no value degrades to its least-typed rendering in an artifact).

Returns:

  • `[]byte`: the JSON string form.
  • `error`: any error from the underlying marshal.

func (MissingResourcePolicy) MarshalYAML

func (p MissingResourcePolicy) MarshalYAML() (any, error)

MarshalYAML serializes the policy as its canonical lowercase string, mirroring MissingResourcePolicy.MarshalJSON.

Returns:

  • `any`: the string form for the YAML encoder.
  • `error`: always nil; present to satisfy the yaml.Marshaler interface.

func (MissingResourcePolicy) String

func (p MissingResourcePolicy) String() string

String returns the canonical lowercase rendering of the policy.

Returns:

  • `string`: "stop" or "ignore".

func (*MissingResourcePolicy) UnmarshalJSON

func (p *MissingResourcePolicy) UnmarshalJSON(data []byte) error

UnmarshalJSON deserializes the canonical string form.

Parameters:

  • `data`: the JSON bytes to decode.

Returns:

  • `error`: a malformed JSON string, or an unknown policy name.

func (*MissingResourcePolicy) UnmarshalText

func (p *MissingResourcePolicy) UnmarshalText(text []byte) error

UnmarshalText deserializes the canonical string form — the seam Convert's text-unmarshal step uses to turn an authored "skip" into the typed policy at slot fill.

Parameters:

  • `text`: the policy name as UTF-8 bytes.

Returns:

  • `error`: non-nil for an unknown name.

func (*MissingResourcePolicy) UnmarshalYAML

func (p *MissingResourcePolicy) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML deserializes the canonical string form, mirroring MissingResourcePolicy.UnmarshalJSON.

Parameters:

  • `value`: the YAML node to decode.

Returns:

  • `error`: a malformed YAML scalar, or an unknown policy name.

type Node

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

Node represents a single unit of work in an execution graph.

func NewNode

func NewNode(spec *NodeSpec) (*Node, error)

NewNode constructs a sealed *Node from a populated *NodeSpec.

Every Node dispatches to a method, bound by a resolved Action (`spec.Action`) OR by name (`spec.ActionName`, resolved lazily at dispatch) — a spec with neither is a program-construction error and panics via the assert package. The spec's ID, action, annotations, slots, elevation offer, error action, and retry policy are applied here; the returned Node exposes no public setters and is immutable thereafter (the graph-immutability seal).

Document deserialization reaches the same result through LoadGraph: it decodes the stream into [nodeData] values and rebuilds each Node with its registry-resolved Action, never leaving a Node in an action-less transient state.

Parameters:

  • `spec`: the populated node spec; must be non-nil and carry a non-nil action.

Returns:

  • `*Node`: the constructed node.
  • `error`: reserved for future validation; nil today.

func (*Node) Action

func (e *Node) Action() Action

Action returns the bound dispatch Action, or nil when this unit has not been bound.

Returns:

  • `Action`: the bound action, or nil.

func (*Node) ActionName

func (e *Node) ActionName() ActionName

ActionName returns the registry name of the action bound by name, or the empty string when this unit binds a resolved Action directly (or binds nothing).

A non-empty name is resolved lazily at dispatch via RuntimeEnvironment.ActionByName; it is the binding path for callers that hold only a name (no *ReceiverRegistry or RuntimeEnvironment in scope), e.g. the graph root naming "flow.subgraph".

Returns:

  • `string`: the bound action name, or "".

func (*Node) Annotations

func (e *Node) Annotations() AnnotationMap

Annotations returns this unit's annotation map.

Returns:

  • `AnnotationMap`: the annotation map wrapper.

func (*Node) ElevationOffer

func (e *Node) ElevationOffer() *ElevationOffer

ElevationOffer returns the privilege-elevation offer for this unit, or nil when no elevation is required.

Returns:

  • `*ElevationOffer`: the configured elevation offer, or nil.

func (*Node) Execute

func (n *Node) Execute(
	ctx context.Context,
	executor *GraphExecutor,
	stack *RecoveryStack,
	variables map[string]Variable,
) (any, error)

Execute resolves slots, dispatches the action, and pushes a receipt at every exit.

Entry checks are ordered: cancellation first (hard signal — `ctx.Err()` catches root/external cancel and any ancestor combinator's scoped cancel), then pause (soft signal — GraphExecutor.Pause sets a flag observed at this pause-point). A canceled or paused check pushes its audit receipt and returns before the action runs.

On a clean entry path, slots are resolved against the active stack (via RecoveryStack.ResultByUnitID for PromiseBinding entries), the node-start hook fires, an *ActivationRecord is built, and the action's Action.Do is invoked. The audit trail — per-attempt history, outcome, captured slots, recovery state — lives on the receipt pushed onto `stack` at every exit. The return value is just control flow.

Parameters:

  • `ctx`: the cancellation context threaded from the parent dispatch.
  • `executor`: the executor driving the run; provides hooks, the runtime environment, the audit-receipt helper, and the pause-point hook.
  • `stack`: the recovery stack the node's receipt pushes onto and that PromiseBinding.Resolve queries via RecoveryStack.ResultByUnitID for upstream unit results.
  • `variables`: the per-call variable frame; resolves VariableBinding slots and is stamped onto the activation for the dispatched method.

Returns:

  • `any`: the dispatch's terminal result; nil on failure, cancellation, pause, or void return.
  • `error`: non-nil on cancellation, pause (ErrPaused), missing action, Action.Do error, or a failed audit-receipt commit.

func (*Node) ID

func (e *Node) ID() string

ID returns the identifier.

Returns:

  • `string`: the unit identifier.

func (*Node) MarshalJSON

func (n *Node) MarshalJSON() ([]byte, error)

MarshalJSON projects the node to its [nodeData] document shape and JSON-encodes it.

Returns:

  • []byte: the JSON encoding of the node's document form.
  • `error`: non-nil if JSON marshaling fails.

func (*Node) MarshalYAML

func (n *Node) MarshalYAML() (any, error)

MarshalYAML returns the node's [nodeData] document shape for the YAML encoder to serialize.

Returns:

  • `any`: the [nodeData] document-form value.
  • `error`: always nil; present only to satisfy the yaml.Marshaler signature.

func (*Node) OnError

func (e *Node) OnError() *Subgraph

OnError returns the failure-handler subgraph for this unit, or nil when no error action is configured.

Returns:

  • `*Subgraph`: the configured failure-handler subgraph, or nil. Nil defaults to the flow.Provider.Failed sentinel at dispatch time.

func (*Node) OnRetry

func (e *Node) OnRetry() *Subgraph

OnRetry returns the per-attempt retry-handler subgraph for this unit, or nil when none is configured.

Returns:

  • `*Subgraph`: the configured retry-handler subgraph, or nil.

func (*Node) Parameters

func (n *Node) Parameters() ([]Parameter, error)

Parameters returns this node's variable bubble-up surface — one Parameter per slot whose value is a VariableBinding. Each returned entry carries the value-side variable name (the variable a caller of this node's containing Subgraph must supply) and the type / default sourced from the bound action's method signature via Method.ParameterByName on the slot name.

Implements ExecutableUnit.Parameters so that Subgraph.Parameters composes its bubble-up surface uniformly via ExecutableUnit.Parameters across both Node and Subgraph children, without a per-child type switch. Callers that want the method's declared parameter list (the slot names / types the method expects to receive) read Action.Method.Parameters() directly.

Node never produces a non-nil error — there's no merging at the leaf — so the second return value exists purely for ExecutableUnit.Parameters signature alignment with Subgraph.Parameters.

Returns:

  • []Parameter: the variable bubble-up surface; nil when no slot carries a VariableBinding.
  • `error`: always nil for Node.

func (*Node) ParentID

func (e *Node) ParentID() string

ParentID returns the ID of the enclosing Subgraph, or the empty string when this unit has no parent.

A unit has no parent when it is the graph root or has not yet been added to any Subgraph.

Returns:

  • `string`: the parent Subgraph's ID, or "".

func (*Node) ResolveSlots

func (e *Node) ResolveSlots(variables map[string]Variable, stack *RecoveryStack) map[string]any

ResolveSlots returns all slot values resolved against the per-dispatch `variables` frame.

Each slot's Binding.Resolve is called with the supplied `variables` map and `stack`: VariableBinding entries look up `variables[name]`; PromiseBinding entries look up the producer's result via RecoveryStack.ResultByUnitID; ImmediateBinding entries return their stored value.

Shared by *Node and *Subgraph dispatch paths in GraphExecutor. The `variables` map is the per-call frame threaded through dispatch — at top level it's the session-resolved variables; for combinator-driven sub-dispatches (gather's per-iteration body) it's a per-iteration frame the combinator built.

Parameters:

  • `variables`: the variable frame in scope for this dispatch.
  • `stack`: the recovery stack; PromiseBinding.Resolve queries it for upstream unit results.

Returns:

  • `map[string]any`: the resolved slot values, keyed by slot name.

func (*Node) RetryPolicy

func (e *Node) RetryPolicy() *RetryPolicy

RetryPolicy returns this unit's retry policy, or nil when no policy is configured.

Returns:

  • `*RetryPolicy`: the configured retry policy, or nil.

func (*Node) Slots

func (e *Node) Slots() map[string]Binding

Slots returns this unit's slot map, keyed by parameter name.

The map aliases the unit's storage; callers must not mutate it directly — use [executableUnit.setSlot] instead.

Returns:

  • `map[string]Binding`: the slot map (may be nil).

func (*Node) TransitionPolicy

func (e *Node) TransitionPolicy() *TransitionPolicy

TransitionPolicy returns this unit's transition policy, or nil when no policy is configured.

Returns:

  • `*TransitionPolicy`: the configured transition policy, or nil.

type NodeSpec

type NodeSpec struct {
	ExecutableUnitSpec
}

NodeSpec is the fluent builder for a *Node. It embeds ExecutableUnitSpec and adds nothing — a node is a leaf unit — re-declaring each inherited With* to return `*NodeSpec` so the builder chain stays on the concrete type. Hand a populated spec to NewNode.

func NewNodeSpec

func NewNodeSpec() *NodeSpec

NewNodeSpec returns an empty *NodeSpec ready for fluent population via its With* setters.

Returns:

  • `*NodeSpec`: a zero-valued node spec.

func (*NodeSpec) WithAction

func (s *NodeSpec) WithAction(action Action) *NodeSpec

WithAction sets the dispatch Action and returns the spec for chaining.

Callers that hold only a name bind via NodeSpec.WithActionNamed instead; every node must end up bound one way or the other.

Parameters:

  • `action`: the Action to bind.

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithActionNamed

func (s *NodeSpec) WithActionNamed(name ActionName) *NodeSpec

WithActionNamed binds the dispatch action by its registry name and returns the spec for chaining.

Validates the name against the global receiver registry and panics on an un-resolvable name (see ExecutableUnitSpec.WithActionNamed).

Parameters:

  • `name`: the dotted registry name (e.g. "flow.complete").

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithAnnotations

func (s *NodeSpec) WithAnnotations(annotations map[string]any) *NodeSpec

WithAnnotations sets the tool-specific annotations and returns the spec for chaining.

Parameters:

  • `annotations`: the raw `map[string]any` to stamp; nil for none.

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithElevationOffer

func (s *NodeSpec) WithElevationOffer(elevationOffer *ElevationOffer) *NodeSpec

WithElevationOffer sets the ElevationOffer and returns the spec for chaining.

Parameters:

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithID

func (s *NodeSpec) WithID(id string) *NodeSpec

WithID sets the unit identifier and returns the spec for chaining.

Parameters:

  • `id`: the unit identifier.

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithOnError

func (s *NodeSpec) WithOnError(onError *Subgraph) *NodeSpec

WithOnError sets the failure-handler Subgraph and returns the spec for chaining.

Parameters:

  • `onError`: the handler Subgraph, or nil for no error action.

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithOnRetry

func (s *NodeSpec) WithOnRetry(onRetry *Subgraph) *NodeSpec

WithOnRetry sets the per-attempt retry-handler Subgraph and returns the spec for chaining.

Parameters:

  • `onRetry`: the retry-handler Subgraph, or nil for no retry handler.

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithRetryPolicy

func (s *NodeSpec) WithRetryPolicy(retryPolicy *RetryPolicy) *NodeSpec

WithRetryPolicy sets the RetryPolicy and returns the spec for chaining.

Parameters:

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithSlot

func (s *NodeSpec) WithSlot(name string, value Binding) *NodeSpec

WithSlot binds one slot value by parameter name and returns the spec for chaining.

Parameters:

  • `name`: the parameter name (or frame-binding name) the slot fills.
  • `value`: the Binding to bind.

Returns:

  • `*NodeSpec`: the receiver, for chaining.

func (*NodeSpec) WithTransitionPolicy

func (s *NodeSpec) WithTransitionPolicy(transitionPolicy *TransitionPolicy) *NodeSpec

WithTransitionPolicy sets the TransitionPolicy and returns the spec for chaining.

Parameters:

Returns:

  • `*NodeSpec`: the receiver, for chaining.

type ObservationBase

type ObservationBase struct {

	// OfResource is the [Resource] this observation is of — the record's identity anchor. Set at construction; non-nil
	// invariant asserted by [NewObservationBase]. Access the observed identity as `o.OfResource.URI()`.
	OfResource Resource

	// Exists is true when the observed thing was present at observation time. When false, the concrete observation's
	// measurement fields carry zero values.
	Exists bool
}

ObservationBase is the back-link-plus-existence surface shared by every concrete observation record.

An observation is a point-in-time metadata snapshot of a Resource — a fact *about* a thing in the world, not a thing whose existence is in question — so it is not a Resource and never enters the ResourceCatalog (ruled 2026-07-14; see docs/architecture/4-resource-management.md §6.1). Its identity comes from the resource it references by pointer value: an observation mints no URI and carries no content hash of its own. Runtime tracking and verification ride the execution record — an observation produced by an observe action is that node's result, carried on its receipt and serialized in the trace; resume re-observes rather than reconstructs.

Concrete observation types embed ObservationBase and contribute their own per-provider measurement fields plus an optional String() debug helper.

func NewObservationBase

func NewObservationBase(ofResource Resource, exists bool) ObservationBase

NewObservationBase constructs an ObservationBase anchored to the resource it observes.

Parameters:

  • `ofResource`: the Resource this observation is of. Must be non-nil (asserted).
  • `exists`: true when the observed thing was present at observation time.

Returns:

  • ObservationBase: the constructed base.

type OrderingEdge

type OrderingEdge struct{}

OrderingEdge is the pure ordering edge's parameter type (ruled at phase 4 PR 3/#611): a slot of this type consumes an upstream invocation's promise solely for sequencing — the promise edge orders the consumer after its producer, and the delivered value is discarded BY TYPE: every source converts to the empty OrderingEdge through the TargetConverter contract. The parameter's type is the declaration (4-resource-management.md §3): no directive, no value semantics, no nil-promise machinery.

The type exists because `any` cannot carry the contract: ExecutableUnit is assignable to `any`, so an invocation bound to an any-typed parameter captures the flow-combinator convention (the unit itself) instead of its promise — an OrderingEdge-typed parameter takes the promise, which is the edge.

Authoring: `after=<invocation>`; nil (the default) means no edge.

func (*OrderingEdge) CanConvertFrom

func (*OrderingEdge) CanConvertFrom(_ reflect.Type) bool

CanConvertFrom reports that every source converts — the discard-by-type half of the contract.

Cheap-probe contract: safe on a zero-value receiver.

Parameters:

  • `_`: the candidate source type; every type is absorbable.

Returns:

  • `bool`: always true.

func (*OrderingEdge) ConvertFrom

func (*OrderingEdge) ConvertFrom(_ any) (any, error)

ConvertFrom discards `value` and returns the empty edge — the delivered promise value carries no meaning here; only the edge it rode in on does.

Parameters:

  • `_`: the delivered value, discarded.

Returns:

type Origin

type Origin interface {

	// Tool identifies which program produced the graph ("lore", "writ") — the trace-identity and filename key.
	Tool() string

	// Scope identifies the planning scope (writ: "system"/"home"; lore: package cache scope).
	//
	// The only field the framework reads; it derives the graph filename from it.
	Scope() string

	// Annotations is the open bag of tool-specific metadata the framework round-trips but never inspects.
	Annotations() AnnotationMap
}

Origin is tool-stamped graph metadata: the contract the framework reads and round-trips.

The framework consults exactly one thing — Origin.Scope — to derive the graph filename. Origin.Tool is the trace-identity, and Origin.Annotations is an open bag of tool-specific metadata the framework stores and round-trips but never inspects. Produced at plan-time, immutable thereafter (matches the graph seal).

OriginBase is the single concrete carrier: tools build one via NewOriginBase, and read graph.Origin() back as this interface, wrapping it in their own typed view (e.g. lore.Origin / writ.Origin) projected over the annotations.

type OriginBase

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

OriginBase is the single concrete Origin carrier and the consumer-facing embedded base.

Its fields are unexported and set once at construction (via NewOriginBase); there are no mutators, matching the graph seal. It (un)marshals to a flat {tool, scope, annotations} document through the unexported [originData] DTO. Tools do not extend OriginBase with typed fields — they project typed read-only views over OriginBase.Annotations — so every concrete Origin serializes to the same shape and decodes back into this one type.

func NewOriginBase

func NewOriginBase(tool, scope string, annotations AnnotationMap) OriginBase

NewOriginBase returns an OriginBase stamped with the given tool, scope, and annotations.

Parameters:

  • `tool`: the producing program's name ("lore", "writ"); becomes the trace-identity.
  • `scope`: the planning scope; the framework derives the graph filename from it.
  • `annotations`: the tool-specific metadata bag; tools project typed views over it on the read side.

Returns:

  • `OriginBase`: the stamped origin.

func (OriginBase) Annotations

func (o OriginBase) Annotations() AnnotationMap

Annotations returns the tool-specific metadata bag.

Returns:

  • `AnnotationMap`: the annotation map; the zero value when none were stamped.

func (OriginBase) MarshalJSON

func (o OriginBase) MarshalJSON() ([]byte, error)

MarshalJSON encodes the origin to its flat {tool, scope, annotations} JSON document via [originData].

Returns:

  • `[]byte`: the JSON encoding.
  • `error`: any error from json.Marshal.

func (OriginBase) MarshalYAML

func (o OriginBase) MarshalYAML() (any, error)

MarshalYAML returns the origin's flat [originData] shape for the YAML encoder.

Returns:

  • `any`: the [originData] value.
  • `error`: always nil; present to satisfy the yaml.Marshaler contract.

func (OriginBase) Scope

func (o OriginBase) Scope() string

Scope returns the planning scope the framework uses to derive the graph filename.

Returns:

  • `string`: the scope; "" when unset.

func (OriginBase) Tool

func (o OriginBase) Tool() string

Tool returns the producing program's name.

Returns:

  • `string`: the tool name; "" when unset.

func (*OriginBase) UnmarshalJSON

func (o *OriginBase) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a flat {tool, scope, annotations} JSON document into the receiver via [originData].

Parameters:

  • `data`: the JSON document.

Returns:

func (*OriginBase) UnmarshalYAML

func (o *OriginBase) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes a flat {tool, scope, annotations} YAML node into the receiver via [originData].

Parameters:

  • `unmarshal`: the yaml.v3 node-decoding callback.

Returns:

  • `error`: any error from `unmarshal`.

type Packer

type Packer interface {

	// Pack returns the resource's transportable content bytes.
	//
	// Returns:
	//   - `[]byte`: the content in the form [Unpacker.Unpack] reconstructs the resource from.
	//   - `error`: non-nil when the content is not materialized (for example a URI-only rehydrated resource) or
	//     cannot be read.
	Pack() ([]byte, error)
}

Packer is implemented by content-addressed resources whose bytes travel with a serialized graph document.

A graph must be immutable and portable across machine boundaries. Reference resources (AddressingLocation) are named by URI in slots and recreate on the target host, but a content resource (AddressingContent) IS its bytes — a target host cannot reconstruct it from the URI alone, so the bytes cross the boundary in the document's content section. Pack produces those bytes; Unpacker.Unpack is the inverse. Pack must be deterministic — the same resource state yields the same bytes — because the document round-trips through pack → unpack → pack and the re-packed section must match what was written.

The invariant: `Addressing() == AddressingContent` ⟹ the type implements Packer and Unpacker. A content-addressable resource that cannot pack its bytes could not cross a machine boundary and could not run there — it is an illegal resource, not a degraded one. The resource-enumeration discipline test enforces this.

type Parameter

type Parameter struct {
	Name     string
	Type     reflect.Type
	Optional bool
	Variadic bool
	Kwargs   bool
	Default  any
}

Parameter describes a single parameter accepted by an Action's Do method.

Parameter is the runtime-typed form of a parameter token produced by codegen. The parameter token (e.g., "destination_path", "mode?", "mode?=0o666", "*parts", "**kwargs") is cracked at the announce boundary by parseParameterToken, which populates every field below. Downstream consumers — Method.Invoke, slot-fill in the starlark bridge, error reporting — read these fields directly and never re-parse the token.

Field invariants:

  • Name is the bare parameter name with no decoration (no leading "*"/"**", no trailing "?", no "=value" suffix). It is the canonical key for slots[Name] lookups and for kwarg matching.
  • Type is the Go reflect.Type the dispatch site projects values into via op.Convert.
  • Optional is true when the caller may omit this slot. Set by parseParameterToken in three cases: the parameter token carries "?", or the parameter is Variadic, or the parameter is Kwargs. Variadic and Kwargs are inherently "zero or more" — the caller may always omit positional overflow or extra keyword args — so Optional being true for them lets consumers ask one question ("may caller omit this slot?") without special-casing the Variadic / Kwargs flags. If Default is non-nil, slot-fill substitutes it.
  • Variadic is true for tokens with a leading single "*". The Go method declares the parameter as a slice; the dispatch site collects positional overflow into it.
  • Kwargs is true for tokens with a leading "**". The Go method declares the parameter as map[string]any; the dispatch site collects unknown keyword arguments into it.
  • Default holds a Go-native value assignable to Type (or nil iff the parameter has no default). The dynamic type inside the any box always matches Type exactly — parseDefaultExpression widens the parsed primitive to Type's named form (e.g., os.FileMode(0o666), not uint32(0o666)). Default is never a starlark.Value and never a raw string at the runtime layer.

Variadic and Kwargs cannot carry an explicit "?" or "=value" at the token level — the grammar rejects "*parts?" and "**kwargs?=foo" at parse time. Their Optional bit is set by parseParameterToken on the basis of the marker alone, and Default is always nil for them.

type Phase

type Phase int

Phase is where a run is in its lifecycle — the control dimension of RunStatus.

A run is constructed in PhasePreparing, enters PhaseRunning when dispatch begins, and may pass through the resumable PhasePausing/PhasePaused rest states. It ends in one of two terminal phases: PhaseCompleted (the natural end — the final unit or flow.Complete executes) or PhaseStopped (the commanded or policy-driven end). The transitional forms PhasePausing and PhaseStopping carry the requested-but-not-yet-observed gap that the control plane reads.

Serialized over [phaseNames] in both document formats — Phase.MarshalText for JSON, Phase.MarshalYAML for gopkg.in/yaml.v3, which does not honor encoding.TextMarshaler.

const (

	// PhasePreparing is the pre-flight phase: variable binding, environment build, and catalog clone. The zero
	// value; entered at construction and exited when the first unit dispatches.
	PhasePreparing Phase = iota

	// PhaseRunning is the active-dispatch phase, from the first unit onward.
	PhaseRunning

	// PhasePausing is the requested-but-not-yet-observed pause: [GraphExecutor.Pause] has been called, and the run
	// will suspend at the next pause-point.
	PhasePausing

	// PhasePaused is the suspended, resumable rest state; a future executor built from a serialized trace resumes it
	// to [PhaseRunning].
	PhasePaused

	// PhaseStopping is the requested-but-not-yet-observed stop: a stop command or a Stop transition reaction is
	// unwinding the boundary toward [PhaseStopped].
	PhaseStopping

	// PhaseStopped is the terminal commanded-or-policy-driven end: a stop command, a cancellation, or a
	// TransitionPolicy Stop reaction. Pairs with [Condition] to name the stop's cause.
	PhaseStopped

	// PhaseCompleted is the natural end: the final unit executes, or flow.Complete executes. The [Condition]
	// stays exactly as set.
	PhaseCompleted
)

func (Phase) MarshalText

func (p Phase) MarshalText() ([]byte, error)

MarshalText encodes this phase as its serialized name.

Satisfies encoding.TextMarshaler, so JSON documents carry "running" / "completed" rather than a bare integer.

Returns:

  • `[]byte`: the name from [phaseNames].
  • `error`: non-nil when the value is out of range.

func (Phase) MarshalYAML

func (p Phase) MarshalYAML() (any, error)

MarshalYAML encodes this phase as its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextMarshaler, so YAML documents need this companion to carry "running" / "completed" rather than a bare integer.

Returns:

  • `any`: the name from [phaseNames], as a string.
  • `error`: non-nil when the value is out of range.

func (Phase) String

func (p Phase) String() string

String returns this phase's serialized name.

Returns:

  • `string`: the name from [phaseNames], or "Phase(<n>)" for an out-of-range value.

func (*Phase) UnmarshalText

func (p *Phase) UnmarshalText(text []byte) error

UnmarshalText decodes a phase from its serialized name.

Satisfies encoding.TextUnmarshaler for JSON documents.

Parameters:

  • `text`: one of the [phaseNames] entries.

Returns:

  • `error`: non-nil when `text` names no phase.

func (*Phase) UnmarshalYAML

func (p *Phase) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes a phase from its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextUnmarshaler, so YAML documents need this companion.

Parameters:

  • `unmarshal`: the YAML node decoder supplied by the `yaml` package.

Returns:

  • `error`: non-nil when the node is not a string or names no phase.

type PlanInvocator

type PlanInvocator interface {

	// InvocationRegistry returns the session-scoped ledger of constructed invocations.
	//
	// Returns:
	//   - *InvocationRegistry: the session ledger; never nil during planning.
	InvocationRegistry() *InvocationRegistry

	// RuntimeEnvironment returns the session environment the planner uses for plan-time [Convert] calls that
	// resolve immediate arguments to their parameter types (e.g. a string to a *file.Resource).
	//
	// Returns:
	//   - *RuntimeEnvironment: the session environment; never nil during planning.
	RuntimeEnvironment() *RuntimeEnvironment
}

PlanInvocator is the contract a Planner consumes to reach plan-time session state.

plan.Provider satisfies this interface.

type PlanPathNormalizer

type PlanPathNormalizer func(path string) (string, error)

PlanPathNormalizer renders an authored plan-space path into its canonical rel form, or refuses it.

The plan-space grammar belongs to the scheme that owns the resource type (the file scheme's git model: docs/architecture/4-resource-management.md §5.2), so the framework carries only this seam: a resource type registers its normalizer, and ActionPlanner applies it to string values bound to that type's parameters at plan time. Immediate-mode construction and programmatic Go callers are untouched — the little language governs what a PLAN may say, not what a session may do.

type Planner

type Planner interface {

	// Plan builds the [ExecutableUnit] for one plan-mode method call.
	//
	// The unit's slots are filled from `args` / `kwargs` against the method's declared parameters; declared defaults
	// fill any parameter the call omits; `onError` / `onRetry` / `retryPolicy` / `transitionPolicy` are stamped at
	// construction. A required parameter (non-optional, no default) with no value is an error. Implementations leave
	// Label unset — the caller stamps it when wrapping the unit in an [Invocation] and registering it. [ActionPlanner]
	// is the default implementation.
	//
	// Parameters:
	//   - `invocator`: the planning host; supplies the session [*InvocationRegistry] and the [*RuntimeEnvironment] for
	//     plan-time [Convert] calls.
	//   - `receiverType`: the planning provider whose method is being called; must be non-nil.
	//   - `method`: the registered method descriptor; must be non-nil.
	//   - `args`: positional arguments, already converted starlark → Go, in call order.
	//   - `kwargs`: keyword arguments by parameter name, already converted (reserved entries removed).
	//   - `annotations`: tool-specific annotations stamped onto the unit; nil for none.
	//   - `onError`: the failure-handler [*Subgraph] stamped onto the unit, or nil.
	//   - `onRetry`: the retry-handler [*Subgraph] stamped onto the unit, or nil.
	//   - `retryPolicy`: the [*RetryPolicy] stamped onto the unit, or nil.
	//   - `transitionPolicy`: the [*TransitionPolicy] stamped onto the unit, or nil.
	//
	// Returns:
	//   - `ExecutableUnit`: the assembled unit with `onError` / `onRetry` / `retryPolicy` / `transitionPolicy`
	//     applied and Label unset.
	//   - `error`: non-nil on a missing required parameter, a slot-value projection failure, or unit construction error.
	Plan(
		invocator PlanInvocator,
		receiverType ProviderReceiverType,
		method *Method,
		args []any,
		kwargs map[string]any,
		annotations map[string]any,
		onError *Subgraph,
		onRetry *Subgraph,
		retryPolicy *RetryPolicy,
		transitionPolicy *TransitionPolicy,
	) (ExecutableUnit, error)
}

Planner builds an ExecutableUnit for one plan-mode method call.

Each *Method in the receiver registry carries a Planner — either the default ActionPlanner or a specialized planner named by reflect.Type in the method's announcement. plan.Provider.Invocation delegates the structural shape of the call to the method's planner; plan.Provider then stamps Label / RetryPolicy / OnError on the returned unit, wraps it in an Invocation, and registers it.

Planners are stateless and constructed once per planner type at announcement time.

type PoliciesConfig

type PoliciesConfig struct {
	devconfig.SectionBase

	// Retry is the DEFAULT retry policy for structural nested subgraphs — a saga boundary bound to flow.subgraph.
	// The step-35 tri-state resolves an unset policy here for such a subgraph other than the graph root; a node, the
	// graph root, the flow combinators (gather / choose / wait_until), and every non-subgraph unit resolve to none.
	Retry RetryPolicy `json:"retry" yaml:"retry"`

	// Transition is the reaction policy consulted at each aberrant [Condition] flip.
	Transition TransitionPolicy `json:"transition" yaml:"transition"`
}

PoliciesConfig is the op-owned "policies" section — the home of every executor-enforced run policy.

It follows the RuntimeEnvironmentConfig precedent: announced at init() with its builtin floor and read live from application.Application.Config (the floor now, enriched with file / env / cli once the loader resolves those sources). It carries the two policies the graph executor consults: PoliciesConfig.Retry — the default RetryPolicy for subgraph combinators (step 35's tri-state) — and PoliciesConfig.Transition — the TransitionPolicy that decides continue / pause / stop on each aberrant condition flip.

func NewPoliciesConfig

func NewPoliciesConfig() *PoliciesConfig

NewPoliciesConfig returns the policies section at its builtin floor.

Floor: TransitionPolicy degraded → continue, execution_failed → stop, compensation_failed → stop (the unattended-execution baseline — stop delivers the consistent pre-run state); RetryPolicy `MaxAttempts:3` with exponential backoff (1s → 30s cap) and full jitter — the step-35 default that an unset structural-subgraph policy resolves to (a node, the graph root, and the flow combinators resolve to none).

Returns:

  • `*PoliciesConfig`: the policies section at its builtin floor.

func PoliciesFrom

func PoliciesFrom(config *devconfig.Config) (*PoliciesConfig, bool)

PoliciesFrom fetches the *PoliciesConfig section from a resolved devconfig.Config.

The typed wrapper over devconfig.SectionOf the design prescribes so consumers never type-assert by hand. PoliciesConfig is announced at init(), so a config that snapshotted the registry carries it.

Parameters:

  • `config`: the resolved configuration container.

Returns:

  • `*PoliciesConfig`: the policies section, or nil when absent.
  • `bool`: true when the section was present.

func (*PoliciesConfig) Validate

func (c *PoliciesConfig) Validate() error

Validate reports whether the policies section is internally consistent.

Delegates to TransitionPolicy.Validate — the loader calls each section's Validate() as it walks the resolved tree. The sole invariant today: `continue` is illegal for `compensation_failed`.

Returns:

  • `error`: non-nil when the transition policy is invalid.

type PromiseBinding

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

PromiseBinding references the output of another executable unit (node or subgraph)

It is resolved to a Go value at execution time via RecoveryStack.ResultByUnitID against the active recovery stack.

func NewPromiseBinding

func NewPromiseBinding(unitID string) PromiseBinding

NewPromiseBinding returns a PromiseBinding that resolves, at execution time, to the output of the producer identified by `unitID`.

Parameters:

Returns:

  • `PromiseBinding`: the binding.

func (PromiseBinding) Edge

func (b PromiseBinding) Edge(consumer string) *Edge

Edge returns the dependency edge from this promise's producer unit to the consuming unit.

Parameters:

  • `consumer`: the id of the unit that consumes this binding — the edge's Edge.To.

Returns:

  • `*Edge`: the producer→consumer dependency edge; the producer is this promise's referenced ExecutableUnit.

func (PromiseBinding) Resolve

func (b PromiseBinding) Resolve(_ map[string]Variable, stack *RecoveryStack) any

Resolve returns the referenced producer's result by querying the recovery stack.

Parameters:

  • `variables`: the resolved variable map (ignored).
  • `stack`: the recovery stack carrying per-dispatch receipts.

Returns:

  • `any`: the referenced producer's stored result, or nil when `stack` is nil or no matching receipt exists.

type Provider

type Provider interface {
	RuntimeEnvironment() *RuntimeEnvironment
	// contains filtered or unexported methods
}

Provider allows a provider to access its scoped execution RuntimeEnvironment.

Actions that need access to the execution environment implement this interface to receive the RuntimeEnvironment during graph execution. The RuntimeEnvironment includes execution parameters, platform abstractions, and runtime state.

Types should satisfy this interface by embedding ProviderBase.

type ProviderBase

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

ProviderBase provides a standardized implementation of the Provider interface.

It must be embedded in all domain-specific providers to ensure they adhere to the execution graph's strictly enforced lifetime.

All providers constructed from the same RuntimeEnvironment share a pointer to it. Per-invocation state changes (DryRun, Data) propagate to all providers without reconstruction.

func NewProviderBase

func NewProviderBase(runtimeEnvironment *RuntimeEnvironment) ProviderBase

NewProviderBase returns a new ProviderBase provider instance with the given RuntimeEnvironment.

func (*ProviderBase) RuntimeEnvironment

func (p *ProviderBase) RuntimeEnvironment() *RuntimeEnvironment

RuntimeEnvironment returns the shared context associated with this provider's lifetime.

type ProviderConstructor

type ProviderConstructor func(runtimeEnvironment *RuntimeEnvironment) (any, error)

ProviderConstructor creates a provider instance bound to the given RuntimeEnvironment.

type ProviderReceiverType

type ProviderReceiverType interface {
	ReceiverType
	Roles() ProviderRole
	Construct() ProviderConstructor
}

ProviderReceiverType extends ReceiverType with provider-specific capabilities.

func NewProviderReceiverType

func NewProviderReceiverType(
	providerType reflect.Type,
	construct ProviderConstructor,
	roles ProviderRole,
	methodParameters map[string][]Parameter,
	planners map[string]Planner,
) (ProviderReceiverType, error)

NewProviderReceiverType creates a ProviderReceiverType from a provider's reflect.Type and its capabilities.

Parameters:

  • providerType: the provider's reflect.Type.
  • construct: creates a provider instance from RuntimeEnvironment.
  • roles: the provider's declared roles (RoleModule, RoleAction, or both).
  • methodParameters: parsed Parameter values per Go method. The parameter tokens are cracked into Parameter values upstream by parseParameters at the announce boundary.

Returns:

  • ProviderReceiverType: the descriptor.
  • error: non-nil if method classification fails.

type ProviderRole

type ProviderRole uint

ProviderRole declares what roles a provider supports.

ProviderRole is a bitflag partitioned into two zones:

  • Dispatch zone (bits 0–7) declares how the provider's methods are invoked. Providers must set at least one bit in this zone; AnnounceProvider panics otherwise.
  • Placement zone (bits 8–15) modifies where the provider's methods surface in starlark. Orthogonal to the dispatch zone; optional.

ProviderRole.Dispatch and ProviderRole.Placement project a role value onto its respective zone.

const (
	// RoleModule declares a provider as an immediate-mode starlark global.
	RoleModule ProviderRole = 1 << iota

	// RoleAction declares a provider as a plan-mode graph node creator.
	RoleAction
)

Dispatch zone — bits 0–7. Declares how the provider's methods are invoked.

const (
	// RoleRoot declares that the provider's methods surface flat at their access-defined namespace root, rather than
	// nested under the provider's own name. For a RoleAction provider, this means the methods appear directly under
	// plan.* (e.g., plan.choose) rather than plan.<provider>.* (e.g., plan.flow.choose). For a RoleModule provider,
	// this means the methods appear as top-level starlark globals (e.g., note()) rather than under the provider name
	// (e.g., ui.note()).
	RoleRoot ProviderRole = 1 << (iota + 8)
)

Placement zone — bits 8–15. Modifies where the provider's methods surface in starlark.

func (ProviderRole) Dispatch

func (r ProviderRole) Dispatch() ProviderRole

Dispatch returns the dispatch-zone bits of r — which execution modes the provider supports.

Returns:

  • ProviderRole: the role value masked to the dispatch zone.

func (ProviderRole) Placement

func (r ProviderRole) Placement() ProviderRole

Placement returns the placement-zone bits of r — how the provider's methods are placed in the namespace.

Returns:

  • ProviderRole: the role value masked to the placement zone.

type Reaction

type Reaction int

Reaction is a TransitionPolicy's response to an aberrant Condition flip: continue, pause, or stop.

Serialized over [reactionNames] in both document formats — Reaction.MarshalText for JSON, Reaction.MarshalYAML for gopkg.in/yaml.v3, which does not honor encoding.TextMarshaler.

const (

	// ReactionContinue keeps the run walking in its (now aberrant) condition; the zero value.
	ReactionContinue Reaction = iota

	// ReactionPause parks the whole run, resumable — the attended-mode override that preserves the failure scene.
	ReactionPause

	// ReactionStop unwinds the boundary's stack and lands it stopped × condition, returning to the parent for
	// bubble-up adjudication.
	ReactionStop
)

func (Reaction) MarshalText

func (r Reaction) MarshalText() ([]byte, error)

MarshalText encodes this reaction as its serialized name.

Satisfies encoding.TextMarshaler, so JSON documents carry "continue" / "pause" / "stop" rather than a bare integer.

Returns:

  • `[]byte`: the name from [reactionNames].
  • `error`: non-nil when the value is out of range.

func (Reaction) MarshalYAML

func (r Reaction) MarshalYAML() (any, error)

MarshalYAML encodes this reaction as its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextMarshaler, so YAML documents need this companion to carry "continue" / "pause" / "stop" rather than a bare integer.

Returns:

  • `any`: the name from [reactionNames], as a string.
  • `error`: non-nil when the value is out of range.

func (Reaction) String

func (r Reaction) String() string

String returns this reaction's serialized name.

Returns:

  • `string`: the name from [reactionNames], or "Reaction(<n>)" for an out-of-range value.

func (*Reaction) UnmarshalText

func (r *Reaction) UnmarshalText(text []byte) error

UnmarshalText decodes a reaction from its serialized name.

Satisfies encoding.TextUnmarshaler for JSON documents.

Parameters:

  • `text`: one of the [reactionNames] entries.

Returns:

  • `error`: non-nil when `text` names no reaction.

func (*Reaction) UnmarshalYAML

func (r *Reaction) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes a reaction from its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextUnmarshaler, so YAML documents need this companion.

Parameters:

  • `unmarshal`: the YAML node decoder supplied by the yaml package.

Returns:

  • `error`: non-nil when the node is not a string or names no reaction.

type Reason

type Reason int

Reason names the class of event that drove a RunStatus to its latest phase or condition — a closed, coarse vocabulary for machine dispatch and diagnostics, distinct from the free-text RunStatus.Message. Two families: health reasons name a condition's cause (ReasonActionFailed, ReasonCompensationFailed, ReasonRetryVetoed, ReasonHandlerFailed, ReasonAbsorbed, ReasonDegraded, ReasonFailed, ReasonPreflightFailed, ReasonFrameworkFailed); lifecycle reasons name a phase move (ReasonStarted, ReasonCompleted, ReasonStopped, ReasonPaused). The zero value ReasonUnspecified serializes to the empty string.

Serialized over [reasonNames] in both document formats — Reason.MarshalText for JSON, Reason.MarshalYAML for gopkg.in/yaml.v3, which does not honor encoding.TextMarshaler.

const (
	// ReasonUnspecified is the zero value — no reason recorded, a run at its healthy default.
	ReasonUnspecified Reason = iota

	// ReasonActionFailed marks an execution failure from an action's error return — the objective default.
	ReasonActionFailed

	// ReasonCompensationFailed marks a compensation failure from a compensating action's error return.
	ReasonCompensationFailed

	// ReasonRetryVetoed marks a retry loop ended by an OnRetry veto rather than by exhaustion.
	ReasonRetryVetoed

	// ReasonHandlerFailed marks an OnError or OnRetry handler that itself errored or broke.
	ReasonHandlerFailed

	// ReasonAbsorbed marks a failure an OnError handler recovered — the pending flip was rejected.
	ReasonAbsorbed

	// ReasonDegraded marks a subjective degrade asserted by flow.Degraded.
	ReasonDegraded

	// ReasonFailed marks an execution failure asserted by flow.Failed — subjective, distinct from an action's error.
	ReasonFailed

	// ReasonPreflightFailed marks a failure during the preparing phase (ledger rehydrate, stack re-arm, variable bind).
	ReasonPreflightFailed

	// ReasonFrameworkFailed marks a framework dispatch failure that is not an action's error return — no action bound,
	// action-name resolution failure, or malformed decision topology at runtime. A structural error, so it bypasses
	// OnError rather than being absorbed as an incidental failure.
	ReasonFrameworkFailed

	// ReasonStarted marks the move into the running phase.
	ReasonStarted

	// ReasonCompleted marks the move into the completed phase.
	ReasonCompleted

	// ReasonStopped marks the move into the stopped phase.
	ReasonStopped

	// ReasonPaused marks the move into the paused phase.
	ReasonPaused

	// ReasonUnwound marks the resume de-escalation: a resumed state-checked unwind cleared a compensation_failed
	// trace back to execution_failed — the one sanctioned downward condition move (step 21's Restart contract).
	ReasonUnwound
)

func (Reason) MarshalText

func (r Reason) MarshalText() ([]byte, error)

MarshalText encodes this reason as its serialized name.

Satisfies encoding.TextMarshaler, so JSON documents carry "action_failed" / "paused" rather than a bare integer.

Returns:

  • `[]byte`: the name from [reasonNames].
  • `error`: non-nil when the value is out of range.

func (Reason) MarshalYAML

func (r Reason) MarshalYAML() (any, error)

MarshalYAML encodes this reason as its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextMarshaler, so YAML documents need this companion to carry "action_failed" / "paused" rather than a bare integer.

Returns:

  • `any`: the name from [reasonNames], as a string.
  • `error`: non-nil when the value is out of range.

func (Reason) String

func (r Reason) String() string

String returns this reason's serialized name.

Returns:

  • `string`: the name from [reasonNames], or "Reason(<n>)" for an out-of-range value.

func (*Reason) UnmarshalText

func (r *Reason) UnmarshalText(text []byte) error

UnmarshalText decodes a reason from its serialized name.

Satisfies encoding.TextUnmarshaler for JSON documents.

Parameters:

  • `text`: one of the [reasonNames] entries.

Returns:

  • `error`: non-nil when `text` names no reason.

func (*Reason) UnmarshalYAML

func (r *Reason) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes a reason from its serialized name.

gopkg.in/yaml.v3 does not honor encoding.TextUnmarshaler, so YAML documents need this companion.

Parameters:

  • `unmarshal`: the YAML node decoder supplied by the `yaml` package.

Returns:

  • `error`: non-nil when the node is not a string or names no reason.

type Receipt

type Receipt interface {
	Compensator

	// ForwardAction returns the short, human-facing name of the provider method that dispatched (e.g. "file.link"),
	// captured at dispatch.
	//
	// Empty for immediate-mode receipts that had no issuing unit. [Trace.Summarize] keys its per-action tally on
	// this label, so a trace can be summarized without consulting the graph.
	ForwardAction() string

	// CompensatingAction identifies the compensating action that undoes this receipt — the dotted name the
	// compensation lookup resolves to the receipt's undo.
	CompensatingAction() string

	// Attempts returns the per-attempt history for retried dispatches. Empty when the dispatch completed on the first
	// attempt.
	Attempts() []Attempt

	// Compensator returns the per-call recovery state captured by a compensable forward method, or nil for
	// non-compensable dispatches and for compensable dispatches with no undo state.
	Compensator() Compensator

	// Err returns the dispatch error, or nil on success.
	Err() error

	// CompensationError returns the error this receipt's Compensate returned during a failed unwind, or nil on success.
	//
	// Distinct from Err (the forward dispatch error): on a retained failed-unwind stack a receipt can carry both — a nil
	// Err (the forward call succeeded) and a set CompensationError (its undo failed), the dirty half of a
	// stopped × ConditionCompensationFailed journal.
	CompensationError() error

	// IsCommitted reports whether this receipt has been finalized with a TransactionID.
	//
	// A committed receipt is ready for archival and reversal. Receipts returned from forward methods are uncommitted;
	// they are committed by the orchestration engine (via [RecoveryStack.Push]) once the forward call succeeds.
	IsCommitted() bool

	// Annotations returns the dispatching unit's annotation map, captured whole at [Commit]. The framework is
	// key-agnostic; tools read their own keys (e.g. writ "project"/"layer", lore "package").
	Annotations() AnnotationMap

	// Resource returns the resource affected by the compensable forward method call, or nil for non-resource-producing
	// dispatches.
	Resource() Resource

	// Result returns the dispatch's return value, or nil for void methods, action-error methods, and failed dispatches.
	Result() any

	// ResultType returns the canonical type id of the produced result, captured at [Commit], or "" for a nil result.
	//
	// Restore reads it to retype the reloaded (untyped) result to its concrete Go type — the produced type is
	// authoritative even when a combinator's static return is `any`.
	ResultType() string

	// Slots returns the resolved slot values at dispatch time — the audit snapshot of "what inputs did this dispatch
	// see."
	Slots() map[string]any

	// Timestamp returns the moment the call was issued as a [uuid.Time].
	//
	// The timestamp is encoded within the [TransactionID] and becomes available once the receipt is committed.
	Timestamp() uuid.Time

	// TransactionID returns the unique identifier for correlating the forward call with its reversal.
	//
	// The identifier is a UUIDv7 minted at [Commit].
	TransactionID() string

	// UnitID returns the [ExecutableUnit.ID] of the unit that dispatched.
	UnitID() string

	SetAttempts(attempts []Attempt)
	SetSlots(slots map[string]any)

	// Commit finalizes this receipt by minting its TransactionID and stamping the supplied action name.
	//
	// Idempotent: if the receipt is already committed, Commit is a no-op.
	Commit(activation *ActivationRecord, result any, compensator Compensator, err error) error

	// RestoreEncoded reconstructs this receipt from its codec-decoded envelope, resolving any resource id references
	// against the runtime environment's rehydrated catalog.
	//
	// Reconstruction consumes decoded values, never format-specific bytes, so one path serves a trace loaded from JSON,
	// YAML, or (later) Protobuf: the base execution state arrives as a [ReceiptData] the codec already decoded, and the
	// receipt's id-reference sub-field as a format-neutral `map[string]any`. [ReceiptBase] supplies the default (base
	// state plus any [*RecoveryStack] compensator); a concrete receipt overrides it to additionally resolve its
	// provider-specific references (e.g. file.Receipt's resource/boundary/source ids) via [ResourceCatalog.Lookup]. The
	// env is passed explicitly rather than read off a pre-seeded receiver, matching how the rest of [op] injects it.
	RestoreEncoded(runtimeEnvironment *RuntimeEnvironment, base ReceiptData, fields map[string]any) error
	// contains filtered or unexported methods
}

Receipt acknowledges a compensable forward method call and carries the minimum state a reversal needs.

A Receipt is the evidence returned by a compensable action, sufficient for Compensate to counteract that action's effects. Every compensable forward method returns a Receipt alongside its [Product]. The Receipt carries the affected Resource (Resource()), the moment the call was issued (Timestamp()), and an opaque identifier for correlating the forward call with its eventual reversal (TransactionID()). Provider-specific receipts (e.g., file.Receipt) must embed ReceiptBase to satisfy this interface. The unexported receiptBase method seals the interface to receiverTypes that embed ReceiptBase.

type ReceiptBase

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

ReceiptBase holds the resource affected by a compensable forward method call.

The transactionID both correlates the forward call with its reversal and encodes the moment the call was issued.

ReceiverType-specific receipts (e.g., file.Receipt) must embed it by value. The embedded Resource preserves its true identity — its fields are never modified by the recovery system. The transactionID is a UUIDv7: its first 48 bits are the Unix-millisecond timestamp, making it both unique and time-sortable, and making ReceiptBase.Timestamp a pure bit-extract over the stored ID — no parsing, no heap allocation. The transactionID is stored in its 16-byte binary form to avoid the ~60 bytes and per-access parse cost of the string form; ReceiptBase.TransactionID formats on demand at serialization or display boundaries.

For providers that also need an archive-storage key (e.g., file.Receipt, which archives displaced bytes to RecoverySite), the transactionID doubles as the recovery key — RecoverySite interprets the receipt's TransactionID directly; no per-domain alias is needed.

func NewReceiptBase

func NewReceiptBase(resource Resource) ReceiptBase

NewReceiptBase creates an uninflated ReceiptBase anchored to the given resource.

The transactionID and action remain zero-valued until [ReceiptBase.Inflate] is called. This split lets a provider method bind the affected resource at construction and defer the per-call reflection + UUID work until inflation, when the issuing method is known.

Parameters:

  • `resource`: the resource affected by the compensable forward method call.

Returns:

  • ReceiptBase: the constructed base with only resource populated.

func NewReceiptBaseWithCompensator

func NewReceiptBaseWithCompensator(resource Resource, compensatingAction string) ReceiptBase

NewReceiptBaseWithCompensator creates a ReceiptBase anchored to resource and naming its compensator.

compensatingAction is the dotted name of the Compensate* method that undoes this receipt, resolved through the registry's compensating-action index (e.g. "file.compensate_file_mutation"). It is fixed at construction — the receipt's type knows its undo — and never mutated afterward. Receipts built without a type-intrinsic compensating action use NewReceiptBase and let ReceiptBase.Commit fill compensatingAction from the dispatching unit.

Parameters:

  • `resource`: the resource affected by the compensable forward method call.
  • `compensatingAction`: the dotted compensator name that undoes this receipt.

Returns:

  • ReceiptBase: the constructed base with resource and compensatingAction populated.

func (*ReceiptBase) Annotations

func (b *ReceiptBase) Annotations() AnnotationMap

Annotations returns the dispatching unit's annotation map, captured whole at ReceiptBase.Commit.

The framework is key-agnostic: it carries the map without interpreting it. Tools read their own keys (writ "project"/"layer", lore "package") via AnnotationMap.Get.

Returns:

  • AnnotationMap: the captured annotations; the zero value for units with none.

func (*ReceiptBase) Attempts

func (b *ReceiptBase) Attempts() []Attempt

Attempts returns the per-attempt history for retried dispatches.

Empty when the dispatch completed on the first attempt.

Returns:

  • []Attempt: the per-attempt history, or nil when no retries occurred.

func (*ReceiptBase) Commit

func (b *ReceiptBase) Commit(activation *ActivationRecord, result any, compensator Compensator, err error) error

Commit finalizes the receipt by minting its TransactionID and recording info on the action that committed the result.

Idempotent: if the transactionID is already set, Commit is a no-op and returns nil. Commit fails only if uuid.NewV7 fails; no resource or context lookup is required.

A nil `unit` is valid: immediate-mode dispatch has no graph and no unit to stamp, so the unit-identity fields are left zero (an honest "no issuing unit") while the transactionID, result, compensator, and error are still recorded.

Parameters:

  • `unit`: the executable unit whose dispatch produced the result; nil in immediate mode.
  • `result`: the unit's return value.
  • `compensator`: the reversal artifact (Receipt or RecoveryStack) paired with the forward call.
  • `err`: the error returned by the forward call, if any.

Returns:

func (*ReceiptBase) Compensate

func (b *ReceiptBase) Compensate(runtimeEnvironment *RuntimeEnvironment) error

Compensate reverses this receipt by resolving its compensating action and invoking it with the receipt's compensator artifact — the leaf Compensator.

Delegates to the registry-resolving invoke path; the concrete artifact rides through ReceiptBase.Compensator (the self-reference stamped at ReceiptBase.Commit), so the base method needs no concrete-type recovery.

Parameters:

  • `runtimeEnvironment`: the executor's environment; resolves the provider and dispatches the compensating action.

Returns:

  • `error`: non-nil when resolution or the compensating action fails; ErrNotCompensable is treated as success.

func (*ReceiptBase) CompensatingAction

func (b *ReceiptBase) CompensatingAction() string

CompensatingAction identifies the compensating action that undoes this receipt — the dotted name the compensation lookup resolves to the receipt's undo (via the registry, with the RuntimeEnvironment.ActionByName fallback when [ReceiverRegistry.ActionByPath] misses).

Returns:

  • `string`: the compensating-action identity; empty until the receipt is stamped.

func (*ReceiptBase) CompensationError

func (b *ReceiptBase) CompensationError() error

CompensationError returns the error this receipt's Compensate returned during a failed unwind, or nil on success.

Distinct from ReceiptBase.Err (the forward dispatch error): on a retained failed-unwind stack a receipt can carry both — a nil ReceiptBase.Err (the forward call succeeded) and a set CompensationError (its undo failed), the dirty half of a stopped × ConditionCompensationFailed journal. RecoveryStack.Unwind records it when the receipt's ReceiptBase.Compensate fails; it round-trips as `compensation_error` on ReceiptData.

Returns:

  • `error`: the compensation error, or nil when the undo succeeded or was never attempted.

func (*ReceiptBase) Compensator

func (b *ReceiptBase) Compensator() Compensator

Compensator returns the per-call recovery state captured by a compensable forward method.

Returns nil for non-compensable dispatches and for compensable dispatches with no undo state.

Returns:

  • `any`: the recovery state, or nil when none was captured.

func (*ReceiptBase) Err

func (b *ReceiptBase) Err() error

Err returns the dispatch error, or nil on success.

Returns:

  • `error`: the dispatch error, or nil when the dispatch succeeded.

func (*ReceiptBase) ForwardAction

func (b *ReceiptBase) ForwardAction() string

ForwardAction returns the short name of the provider method that dispatched (e.g. "file.link"), captured at dispatch, or empty for immediate-mode receipts that had no issuing unit.

Returns:

  • `string`: the short forward-action name; empty when no issuing unit stamped this receipt.

func (*ReceiptBase) IsCommitted

func (b *ReceiptBase) IsCommitted() bool

IsCommitted reports whether this receipt has been finalized with a TransactionID.

Returns:

  • `bool`: true if the transactionID is not the nil UUID.

func (*ReceiptBase) MarshalJSON

func (b *ReceiptBase) MarshalJSON() ([]byte, error)

MarshalJSON encodes the receipt's base state as JSON via the ReceiptData shape.

Delegates to ReceiptBase.MarshalYAML for the encoded value, then runs json.Marshal over it. ReceiptData carries both `json:` and `yaml:` field tags so the JSON encoder reads its tags directly. Concrete Receipt types with no provider-specific fields inherit this method unchanged via embedding; types that carry extra fields override both ReceiptBase.MarshalJSON and ReceiptBase.MarshalYAML together because Go method dispatch on an embedded receiver does not see the outer type's overrides.

Returns:

func (*ReceiptBase) MarshalYAML

func (b *ReceiptBase) MarshalYAML() (any, error)

MarshalYAML returns the receipt's base state as a ReceiptData value the YAML encoder serializes.

Per phase-8 13.0(d), Resource is projected to its Resource.URI string in the document — not embedded as a full Resource document — so the envelope stays flat and the Unmarshal side can rehydrate the concrete Resource via each derivative's NewResource without nested-decoder context plumbing. TransactionID serializes as the canonical 36-char UUIDv7 string already produced by ReceiptBase.TransactionID. Err round-trips as a `status` string (the error message); empty restores as nil. ReceiptData is the single source of truth for the document shape: its `json:` and `yaml:` tags drive both encoders, and ReceiptBase.MarshalJSON delegates here for the value before running json.Marshal.

Returns:

  • `any`: the populated ReceiptData for the YAML encoder to walk.
  • `error`: nil under normal conditions.

func (*ReceiptBase) Resource

func (b *ReceiptBase) Resource() Resource

Resource returns the resource affected by the compensable forward method call, or nil for non-resource-producing dispatches.

Returns:

  • `Resource`: the affected resource set at NewReceiptBase (or nil when none).

func (*ReceiptBase) Restore

func (b *ReceiptBase) Restore(snapshot ReceiptData) error

Restore rebuilds this receipt's base state from a ReceiptData.

[Snapshot] and Restore form the encapsulation-respecting path to read or write the embedded base state from outside op. Concrete receipt types in other packages embed ReceiptData in their own document-shape struct, extract the embedded value during unmarshal, and pass it here. The boundary conversions (Resource -> URI, UUID -> 36-char string, error -> status string) run at the Snapshot / Restore boundary so downstream encoders see plain field values and skip reflection-driven method dispatch on the embedded base.

The receiver MUST be pre-seeded with a Resource before Restore is called — typically by reconstructing the receipt's base via NewReceiptBase with a freshly-built concrete Resource. Restore validates that the pre-seeded resource's URI matches snapshot.ResourceURI (sanity check against malformed document input), parses the transaction ID, then writes every base field from the snapshot. The Resource itself is not mutated — its identity was fixed at construction.

Restore is one-shot: it errors if the receipt has already been committed or restored. Callers that need to re-bind a receipt construct a fresh one.

Parameters:

  • `snapshot`: the base-state ReceiptData, identical in shape to the value returned by [Snapshot].

Returns:

  • `error`: non-nil when the receipt's transactionID is already set, the resource is missing, the resource URI does not match the snapshot, or the transaction_id string is malformed.

func (*ReceiptBase) RestoreEncoded

func (b *ReceiptBase) RestoreEncoded(_ *RuntimeEnvironment, base ReceiptData, _ map[string]any) error

RestoreEncoded restores the base execution state and any *RecoveryStack compensator from a codec-decoded envelope.

It is the default restore for every receipt. The recovery stack already decoded the envelope — through whichever codec read the trace — into a ReceiptData, so the base only copies the fields across: no byte parsing, so the same method serves a trace stored as JSON, YAML, or Protobuf. The decoded `*RecoveryStack` compensator (a subgraph's child stack) rides through as `base.Compensator`. A concrete receipt type overrides this to additionally resolve its own provider-specific id references (`fields`) against the catalog; the base needs neither the environment nor `fields`, so both are ignored here.

Parameters:

  • `_`: the runtime environment, unused by the base restore.
  • `base`: the codec-decoded base execution state.
  • `_`: the receipt's id-reference sub-field, unused by the base restore.

Returns:

  • `error`: always nil; the signature satisfies the Receipt interface.

func (*ReceiptBase) Result

func (b *ReceiptBase) Result() any

Result returns the dispatch's return value.

Returns nil for void methods, action-error methods, and failed dispatches.

Returns:

  • `any`: the dispatch's return value, or nil when the method returned nothing or failed.

func (*ReceiptBase) ResultType

func (b *ReceiptBase) ResultType() string

ResultType returns the canonical type id of the produced result, captured at ReceiptBase.Commit, or "" for a nil result.

Returns:

  • `string`: the canonical type id, or "" when no typed result was produced.

func (*ReceiptBase) SetAttempts

func (b *ReceiptBase) SetAttempts(attempts []Attempt)

SetAttempts replaces the per-attempt history with `attempts`.

Parameters:

  • `attempts`: the per-attempt history to stamp on the receipt.

func (*ReceiptBase) SetSlots

func (b *ReceiptBase) SetSlots(slots map[string]any)

SetSlots stamps the resolved slot snapshot `slots` on the receipt.

Parameters:

  • `slots`: the resolved slot values keyed by parameter name.

func (*ReceiptBase) Slots

func (b *ReceiptBase) Slots() map[string]any

Slots returns the resolved slot values at dispatch time — the audit snapshot of "what inputs did this dispatch see."

Returns:

  • map[string]any: the resolved slot snapshot keyed by parameter name.

func (*ReceiptBase) Snapshot

func (b *ReceiptBase) Snapshot() ReceiptData

Snapshot returns this receipt's base state as a ReceiptData.

Snapshot is the read side of the encapsulation boundary. Marshalers can return Snapshot's value directly or embed it alongside derivative-specific fields — concrete receipt types in other packages compose ReceiptData with their provider fields in a single document-shape struct. The boundary conversions (Resource -> URI, UUID -> 36-char string, error -> status string) run once here, so downstream encoders see plain field values and skip reflection-driven method dispatch on the embedded base.

Returns:

  • ReceiptData: the receipt's base state with ResourceURI empty when no resource is attached, TransactionID the canonical 36-char UUID string (the all-zeros UUID until Commit runs), Status the dispatch error's message (empty when Err is nil), and CompensationError the failed-unwind error's message (empty when the undo succeeded or never ran).

func (*ReceiptBase) Timestamp

func (b *ReceiptBase) Timestamp() uuid.Time

Timestamp returns the timestamp encoded in this receipt's transactionID as a uuid.Time.

This is a count of 100-nanosecond intervals since the UUID epoch (1582-10-15 UTC). The value corresponds to the 48-bit Unix-millisecond timestamp encoded in the first 48 bits. Use uuid.Time.UnixTime to project to seconds plus nanoseconds suitable for time.Unix.

Returns:

func (*ReceiptBase) TransactionID

func (b *ReceiptBase) TransactionID() string

TransactionID returns the receipt's transactionID as a canonical 36-char UUID string.

The transactionID is a UUIDv7 minted at [ReceiptBase.Inflate]; it correlates the forward call with its reversal and encodes the call's issue time (see ReceiptBase.Timestamp). The string is produced on demand via uuid.UUID.String — the receipt stores only the 16-byte binary form.

Returns:

  • `string`: the canonical UUID string; the all-zeros UUID until Inflate runs.

func (*ReceiptBase) UnitID

func (b *ReceiptBase) UnitID() string

UnitID returns the ExecutableUnit.ID of the unit that dispatched.

Returns:

  • `string`: the dispatching unit's ID.

type ReceiptData

type ReceiptData struct {
	ForwardAction      string         `json:"forward_action"      yaml:"forward_action"`
	CompensatingAction string         `json:"compensating_action" yaml:"compensating_action"`
	Annotations        map[string]any `json:"annotations,omitempty"  yaml:"annotations,omitempty"`
	Attempts           []Attempt      `json:"attempts,omitempty"     yaml:"attempts,omitempty"`
	Compensator        any            `json:"compensator,omitempty"   yaml:"compensator,omitempty"`
	ResourceURI        string         `json:"resource_uri,omitempty" yaml:"resource_uri,omitempty"`
	Result             any            `json:"result,omitempty"       yaml:"result,omitempty"`
	ResultType         string         `json:"result_type,omitempty"  yaml:"result_type,omitempty"`
	Slots              map[string]any `json:"slots,omitempty"        yaml:"slots,omitempty"`
	Status             string         `json:"status,omitempty"       yaml:"status,omitempty"`
	CompensationError  string         `json:"compensation_error,omitempty" yaml:"compensation_error,omitempty"`
	TransactionID      string         `json:"transaction_id"         yaml:"transaction_id"`
	UnitID             string         `json:"unit_id"                yaml:"unit_id"`
}

ReceiptData is the canonical document shape for ReceiptBase.

ReceiptBase.Snapshot and ReceiptBase.Restore form the encapsulation-respecting path to read or write the base state from outside op. Concrete receipt types in other packages embed ReceiptData in their own document-shape struct (combining base and provider-specific fields) and pass the embedded value to ReceiptBase.Restore during unmarshal. The named type avoids the verbose anonymous-struct repetition a 12-field shape would otherwise demand at every call site.

Field-level encoding choices:

  • Status holds the dispatch error's message; non-empty restores as errors.New(status) so Err()-presence and the human-readable reason survive the round trip (typed/joined errors collapse into a single error on reload).
  • CompensationError holds the message of the error this receipt's Compensate returned on a failed unwind (empty when the undo succeeded or never ran); like Status it restores as errors.New(...). Distinct from Status, which is the forward dispatch error — a receipt on a failed-unwind journal can carry both.
  • Slots / Result / Compensator serialize as their natural YAML; on reload they are untyped (map[string]any or primitive) and the framework's Convert cascade retypes them where a typed value is needed (compensation, promise resolution).
  • ResourceURI carries the resource's identity; the receiver must be pre-seeded with the concrete Resource before Restore is called (Restore validates that URIs match).

type ReceiverType

type ReceiverType interface {
	Name() string
	ProviderType() reflect.Type
	TypeID() string
	Methods() iter.Seq[*Method]
	MethodByName(name string) (*Method, bool)
	Do(method string, receiver any, args []any) (reflect.Value, reflect.Value, error)
}

ReceiverType is the base interface for all receiver descriptors.

Callers that need provider-specific or resource-specific behavior assert to ProviderReceiverType or ResourceReceiverType.

func NewReceiverType

func NewReceiverType(providerType reflect.Type, methodParameters map[string][]Parameter) (ReceiverType, error)

NewReceiverType creates a ReceiverType for an arbitrary Go type via reflection.

Parameters:

  • providerType: the Go type (pointer or struct).
  • methodParameters: parsed Parameter values per Go method, or nil for positional names. The raw parameter tokens are cracked into Parameter values upstream by parseParameters at the announce boundary.

Returns:

  • ReceiverType: the receiver type descriptor.
  • error: non-nil if the type cannot be introspected.

func SnapshotReceiverTypes

func SnapshotReceiverTypes() []ReceiverType

SnapshotReceiverTypes returns a freshly-allocated slice of every announced receiver type.

Intended for boot-discipline tests, code-generation tools, and introspection callers that need to enumerate the package-level registry from outside pkg/op. Iteration order is unspecified — callers that need a stable order must sort the result themselves.

Returns:

  • `[]ReceiverType`: snapshot of every receiver type currently in the registry.

type RecoverySite

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

RecoverySite manages archival and restoration of resources within the authority boundary.

All operations use zero-copy renames for files and byte serialization for data. The recovery directory is .devlore/recovery/ within the fsroot.Dir authority boundary. All I/O goes through RuntimeEnvironment.Root.

func NewRecoverySite

func NewRecoverySite(runtimeEnvironment *RuntimeEnvironment) *RecoverySite

NewRecoverySite creates a RecoverySite with the given RuntimeEnvironment.

The RuntimeEnvironment must have a non-nil Root.

func (*RecoverySite) ArchiveData

func (s *RecoverySite) ArchiveData(data []byte) (string, error)

ArchiveData writes bytes to a file in the recovery directory.

Parameters:

  • data: Bytes to archive

Returns:

  • string: opaque recovery ID for tombstone storage
  • error: any write error

func (*RecoverySite) ArchiveFile

func (s *RecoverySite) ArchiveFile(p fsroot.Path) (string, error)

ArchiveFile moves a file to recovery via zero-copy rename.

No data is copied — the file's directory entry is relocated. Takes [Path] for the user-facing location. Returns an opaque recovery ID for tombstone storage.

Parameters:

  • p: Path of the file to archive

Returns:

  • string: opaque recovery ID for tombstone storage
  • error: any rename error

func (*RecoverySite) ArchiveStream

func (s *RecoverySite) ArchiveStream(r io.Reader) (_ string, err error)

ArchiveStream copies a reader into the recovery directory chunk-by-chunk.

Use when the content source is an io.Reader — e.g., an HTTP response body — and should not be buffered fully in memory. The reader is drained via io.Copy into a freshly created file at .devlore/recovery/<uuid>. Returns the opaque recovery ID for tombstone storage or later consumption via memory-mapped access.

Parameters:

  • r: source reader; drained until EOF.

Returns:

  • string: opaque recovery ID for tombstone storage.
  • error: any error from recovery-directory creation, file creation, or the copy.

func (*RecoverySite) RestoreData

func (s *RecoverySite) RestoreData(recoveryID string) ([]byte, error)

RestoreData reads bytes back from a file in the recovery directory.

Parameters:

  • recoveryID: Opaque recovery ID returned by ArchiveData

Returns:

  • []byte: archived data
  • error: any read error

func (*RecoverySite) RestoreFile

func (s *RecoverySite) RestoreFile(original fsroot.Path, recoveryID string) error

RestoreFile moves a file back from recovery via zero-copy rename.

No data is copied — the directory entry is relocated back. The parent directory of original is recreated if it was pruned after archival.

Parameters:

  • original: Path where the file should be restored
  • recoveryID: Opaque recovery ID returned by ArchiveFile

Returns:

  • error: any rename error

type RecoveryStack

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

RecoveryStack accumulates compensable operations in LIFO order.

On Unwind, each entry is compensated in reverse order. All entries are attempted regardless of individual failures; errors are joined via errors.Join.

Per-subgraph executors own one stack each, chained into a tree (phase-8 step 31). A child stack is nested *down* into its parent via RecoveryStack.PushNested, so Unwind cascades compensation through the tree; and it points *up* at its parent via the `parent` field, so RecoveryStack.ResultByUnitID walks the chain to resolve a promise against an ancestor stack's receipt. The nesting is durable (serialized in a Trace); the parent pointer is transient — re-derived from the nesting on a load operation, never serialized.

A stack may also be *stamped* (RecoveryStack.Stamp) as one combinator body-run's resumption record: it then carries the receipt's identity/outcome subset — `unitID`, `result`, `resultType`, `err` — directly, so a nested stamped stack is self-describing without a wrapper receipt. A stack undoes itself via RecoveryStack.Unwind, so it needs identity, not a named compensator; the stamp is why it does *not* embed ReceiptBase. An unstamped stack (a subgraph's child stack, a root) leaves those fields zero.

func NewChildRecoveryStack

func NewChildRecoveryStack(parent *RecoveryStack) *RecoveryStack

NewChildRecoveryStack creates an empty RecoveryStack chained to `parent`.

A combinator body-run (a Gather iteration, a Choose branch) runs on a child stack so its promise lookups resolve up the chain into `parent` and its ancestors (RecoveryStack.ResultByUnitID), while its compensation nests *down* into `parent` once the run's stamped stack is pushed. Mirrors how Subgraph.Execute mints a subgraph's child stack.

Parameters:

  • `parent`: the enclosing stack to chain up to; must be non-nil (use NewRecoveryStack for a root).

Returns:

  • `*RecoveryStack`: the new chained stack.

func NewRecoveryStack

func NewRecoveryStack() *RecoveryStack

NewRecoveryStack creates an empty RecoveryStack at the root of a chain (no parent).

Returns:

  • `*RecoveryStack`: the new root stack.

func (*RecoveryStack) Compensate

func (s *RecoveryStack) Compensate(runtimeEnvironment *RuntimeEnvironment) error

Compensate reverses this recovery stack by unwinding its children LIFO, the composite Compensator.

It is RecoveryStack.Unwind under the Compensator interface's name.

Parameters:

  • `runtimeEnvironment`: the executor's environment, threaded into each entry's compensation.

Returns:

  • `error`: the joined errors from every entry that failed to compensate, or nil when all succeed.

func (*RecoveryStack) Discard

func (s *RecoveryStack) Discard()

Discard drops all entries without unwinding.

func (*RecoveryStack) Err

func (s *RecoveryStack) Err() error

Err returns this stack's stamped body-run status, or nil when the stack is unstamped or the run completed.

Returns:

  • `error`: the stamped status, or nil.

func (*RecoveryStack) Len

func (s *RecoveryStack) Len() int

Len returns the number of entries on the stack.

Returns:

  • `int`: the entry count.

func (*RecoveryStack) MarshalJSON

func (s *RecoveryStack) MarshalJSON() ([]byte, error)

MarshalJSON encodes the stack as a JSON object via RecoveryStack.MarshalYAML.

Encoded form: `{stamp, "entries": [...]}` where each element serializes itself — a nested `*RecoveryStack` recurses (carrying its own `entries`), and a receipt emits its own flat encoding (ReceiptData base + the concrete receipt's id references). No `kind` tag and no stack-owned envelope: decode discriminates structurally on the presence of `entries` (phase-8 step 42 slice 3b).

Returns:

  • `[]byte`: the encoded JSON object.
  • `error`: non-nil when a receipt or nested stack fails to encode.

func (*RecoveryStack) MarshalYAML

func (s *RecoveryStack) MarshalYAML() (any, error)

MarshalYAML returns the stack's stamp and entries as a struct value the encoder walks.

Source of truth for the encoded shape; RecoveryStack.MarshalJSON delegates here. Each entry's compensator serializes itself: a nested `*RecoveryStack` recurses through this method, a receipt through its own [Receipt.MarshalJSON] (ReceiptData base plus the concrete receipt's id references). The recovery tree owns no receipt envelope — a receipt is responsible for its whole encoding (phase-8 step 42 slice 3b).

Returns:

  • `any`: the encodable struct value carrying the stamp fields and entries.
  • `error`: reserved; currently always nil.

func (*RecoveryStack) NestedStackByUnitID

func (s *RecoveryStack) NestedStackByUnitID(unitID string) (*RecoveryStack, bool)

NestedStackByUnitID returns the nested stamped substack for `unitID` from this stack's own entries, searched LIFO.

The stamped-stack analog of [RecoveryStack.receiptByUnitID]: a combinator reads it against its own stack to decide a body-run's fate on resume — a stack with a nil RecoveryStack.Err is a completed run to replay (RecoveryStack.Result is its cached output); a stack with a non-nil err is an in-progress run to adopt and re-enter. Only stamped nested entries (`unitID != ""`) match; audit-only nested stacks are skipped.

Parameters:

  • `unitID`: the stamped body-run identity to look up (e.g. "<gatherID>#<i>").

Returns:

  • `*RecoveryStack`: the matching stamped substack, or nil when none is present on this stack.
  • `bool`: true when a stamped substack for `unitID` is present.

func (*RecoveryStack) Push

func (s *RecoveryStack) Push(receipt Receipt)

Push appends a Receipt onto the stack as an audit-trail entry.

Step 12 broadens RecoveryStack from a compensable-only ledger to an every-dispatch ledger: the executor calls Push at every dispatch exit (cancellation, Do-error, success). When the receipt carries a non-nil compensator, the entry is also wired for compensation — RecoveryStack.Unwind invokes the action's Compensate companion at rollback, reached through the *RuntimeEnvironment Unwind supplies (not the receipt's resource, so a resource-less compensator still compensates). Otherwise, the entry is audit-only and RecoveryStack.Unwind skips it.

The receipt must already be committed by its caller; Push does not commit. A nil receipt is a programming error and panics via assert.NonZero.

Parameters:

  • `receipt`: the receipt to push. Must be non-nil and already committed.

func (*RecoveryStack) PushNested

func (s *RecoveryStack) PushNested(recoveryStack *RecoveryStack)

PushNested appends a substack as a single transactional entry on this stack.

The nested entry preserves the saga boundary: at unwind time the substack is unwound as a unit (its own LIFO walk, its own error aggregation) before the outer stack continues, driven by the *RuntimeEnvironment the enclosing RecoveryStack.Unwind threads down.

A nil substack is a programming error and panics via assert.NonZero.

Parameters:

  • `recoveryStack`: the substack to nest. Must be non-nil.

func (*RecoveryStack) Receipts

func (s *RecoveryStack) Receipts() []Receipt

Receipts returns all receipt-bearing entries on the stack, descending into nested substacks, in FIFO order.

Unlike RecoveryStack.ResultByUnitID, which searches only this stack's top level, Receipts flattens nested substacks so callers that summarize a whole execution (see Trace.Summarize) observe every dispatched unit's receipt, including per-iteration combinator children. Nested-stack marker entries contribute their contained receipts, not themselves; and a receipt whose compensator is itself a RecoveryStack — a subgraph or file.WalkTree dispatch — also contributes that child stack's receipts, since the child no longer rides a separate nested entry.

Returns:

  • `[]Receipt`: the flattened receipts in push order; empty when the stack holds none.

func (*RecoveryStack) Result

func (s *RecoveryStack) Result() any

Result returns this stack's stamped body-run result, or nil when the stack is unstamped or the run produced nothing.

Returns:

  • `any`: the stamped result, or nil.

func (*RecoveryStack) ResultByUnitID

func (s *RecoveryStack) ResultByUnitID(unitID string) (any, bool)

ResultByUnitID returns the most recent receipt's Receipt.Result for the unit identified by `unitID`.

The search covers this stack and then walks up the parent chain. The stack tree is the source of truth for per-dispatch results: every dispatch exit pushes a receipt with the// producing unit's ID and result, so promise-style "look up an upstream unit's output" queries walk the stacks instead of a separate results map. Each stack is searched LIFO so a retried unit returns its latest outcome; when the unit is not found, the search continues into the stack's `parent`, so a promise to an upstream producer in an ancestor subgraph resolves against that ancestor's stack.

The walk only ever goes *up* the chain, never *down* into nested substacks (a producer always runs before its consumer, and so lives in this stack or an ancestor, never in a child).

Parameters:

Returns:

  • `any`: the matched receipt's result, or nil when no match is found in this stack or any ancestor.
  • `bool`: true when a matching receipt was found, false otherwise.

func (*RecoveryStack) Stamp

func (s *RecoveryStack) Stamp(unitID string, result any, err error)

Stamp finalizes this stack as one combinator body-run's resumption record.

A combinator (Gather, Choose, WaitUntil) runs its body once on a child stack, then stamps that stack with the run's identity and outcome before nesting it — the stamped-stack analog of ReceiptBase.Commit. It captures `unitID` (the resumption key), `result` and its canonical type id (for replay + retype-on-resume), and `err` (the run's status). It is a one-shot finalize, not a piecemeal setter; re-stamping an already-adopted paused substack in place updates its outcome once the re-entered run completes.

Parameters:

  • `unitID`: the body-run identity (e.g. "<gatherID>#<i>").
  • `result`: the body-run's return value.
  • `err`: the body-run's status — nil on completion, non-nil (a failure or ErrPaused) otherwise.

func (*RecoveryStack) UnitID

func (s *RecoveryStack) UnitID() string

UnitID returns this stack's stamped body-run identity, or "" when the stack is unstamped.

Returns:

  • `string`: the stamped identity, or "" for an unstamped stack.

func (*RecoveryStack) UnmarshalJSON

func (s *RecoveryStack) UnmarshalJSON(data []byte) error

UnmarshalJSON reconstructs the stack tree from the JSON form encoded by RecoveryStack.MarshalJSON.

It decodes the entries into [recoveryEntryData] and delegates to [RecoveryStack.fromEntries], which the YAML reader (RecoveryStack.UnmarshalYAML) shares — so JSON and YAML reconstruct identically. Reconstruction consumes the decoded values, never reparsed bytes, per the format-neutral requirement (a trace must reload and verify across JSON/YAML/Protobuf — see the step doc's "Format-neutral trace reconstruction" section).

Parameters:

Returns:

  • `error`: non-nil on malformed input.

func (*RecoveryStack) UnmarshalYAML

func (s *RecoveryStack) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML reconstructs the stack tree from the YAML form encoded by RecoveryStack.MarshalYAML.

The YAML mirror of RecoveryStack.UnmarshalJSON: it decodes the entries into [recoveryEntryData] through the YAML codec and delegates to the shared [RecoveryStack.fromEntries] builder, so a YAML-stored trace reconstructs through the same format-neutral path as a JSON one.

Parameters:

  • `unmarshal`: the YAML node decoder supplied by the yaml package.

Returns:

  • `error`: non-nil on malformed input.

func (*RecoveryStack) Unwind

func (s *RecoveryStack) Unwind(runtimeEnvironment *RuntimeEnvironment) error

Unwind rolls back all stack entries in LIFO order.

All entries are attempted regardless of individual failures; errors are joined via errors.Join. Each entry's undo closure is run with `runtimeEnvironment` — supplied here rather than captured at RecoveryStack.Push — so the env is bound once, at rollback, and threaded down into every nested substack's own Unwind.

The stack is the compensation-failure journal (phase-8 step 21): a *clean* unwind clears the entries — the system is back at its pre-run baseline, nothing to journal — but a *failed* unwind RETAINS them, recording the error each leaf receipt's [Receipt.Compensate] returned on that receipt as its Receipt.CompensationError (a nested substack has no receipt of its own — its dirtiness rides its own retained failed children). The retained tree is what GraphExecutor.Trace reports on a stopped × ConditionCompensationFailed terminal, so a client can persist and present the source (the failing node's receipt) and the diagnostics (which Compensate failed and why).

Parameters:

  • `runtimeEnvironment`: the executor's environment, used to resolve and invoke each entry's Compensate companion.

Returns:

  • `error`: the joined errors from every entry that failed to compensate, or nil when all succeed.

type Resource

type Resource interface {

	// RuntimeEnvironment returns the execution environment this resource was constructed against — needed for the
	// off-dispatch surfaces (Digest/Etag/Resolve, the fixed-signature unmarshalers) where no activation exists.
	// A resource is a resource and a provider is a provider: the two are uncoupled (step 29, 2026-07-19).
	RuntimeEnvironment() *RuntimeEnvironment

	ID() string
	URI() string

	// ResourceType returns the canonical Go type id of the concrete Resource type — the fragment component of the
	// canonical tag URI, and the key the framework dispatches on (rehydration constructors, the pre-flight resolve
	// pass's staging gate). Satisfied by [ResourceBase.ResourceType]; minted at construction.
	ResourceType() string

	Addressing() AddressingMode
	Digest() (Digest, error)
	Etag() (string, error)

	// Resolve locates the resource by URI and verifies reachability; Exists reports whether it currently exists. The
	// catalog reads Exists (via [ResourceCatalog.VerifyExistence]) to drive the Pending → Active / Gone transition
	// (phase-8 step 22). [ResourceBase] supplies loud unimplemented defaults; concrete types that participate in the
	// discovery-side lifecycle override them (file does today).
	Resolve() error
	Exists() bool

	ProducerID() string
	// contains filtered or unexported methods
}

Resource is the interface for all resource receiverTypes.

Every provider-specific resource (e.g., file.Resource) must embed ResourceBase to satisfy it. The unexported resourceBase method seals the interface to package op. Only receiverTypes embedding ResourceBase can implement Resource.

URI() returns an immutable string computed at construction time. Each concrete type's NewResource constructor formulates the URI from the value descriptor and execution context. The URI is the resource's identity — it does not change after construction. [Resolve] enriches metadata (stat, version) but does not alter identity.

type ResourceBase

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

ResourceBase holds the identity fields common to all resources.

ReceiverType-specific resource receiverTypes must embed it by value. The uri, specific, and typeID fields are set at construction via NewResourceBase: uri is the minted canonical tag URI, specific is the scheme-specific identity payload, typeID is the canonical Go type id of the concrete Resource type. The id and producerID fields are stamped by the ResourceCatalog when the resource is cataloged; they are not a concern of the resource itself.

func NewResourceBase

func NewResourceBase(runtimeEnvironment *RuntimeEnvironment, specific string, goType reflect.Type) (ResourceBase, error)

NewResourceBase constructs a ResourceBase whose identity is the canonical tag URI.

tag:devlore.noblefactor.com,2026-01-01:<specific>#<typeID>, where <typeID> is goType's canonical Go type id (PkgPath() + "." + Name()). Pointer types are normalized to their element.

An empty <specific> is valid and produces the deferred ("known-at-execution") form — the shape constructed by op.Defer when a resource's identity is not known until the producing node has executed.

Parameters:

  • `runtimeEnvironment`: the execution context; embedded via ProviderBase.
  • `specific`: the scheme-specific identity payload. Must not contain '#', reserved as the fragment delimiter.
  • `goType`: the concrete Go type whose identity is placed in the fragment.

Returns:

  • `ResourceBase`: the constructed base with uri, specific, and typeID all populated.
  • `error`: non-nil when specific contains '#' or goType has empty PkgPath and Name.

func (*ResourceBase) Addressing

func (b *ResourceBase) Addressing() AddressingMode

Addressing returns AddressingUnknown as a sentinel default.

Every concrete Resource type must override to return one of AddressingLocation or AddressingContent. The boot-discipline test in pkg/op/addressing_test.go (added in 13.0(k) sub-step k.12) walks every announced Resource type and asserts none returns AddressingUnknown.

Returns:

func (*ResourceBase) CanConvertTo

func (b *ResourceBase) CanConvertTo(target reflect.Type) bool

CanConvertTo reports whether this resource can project itself into the given target Go type.

The baseline projection is URI → string: any ResourceBase knows how to produce its URI as a Go string. Concrete Resource types extend this by overriding [ResourceBase.CanConvert] to accept additional targets (e.g., a [file.Resource] that projects to an fsroot.Path) and delegating to this method for the string case.

Parameters:

  • `target`: the destination Go type the caller wants to project the resource into.

Returns:

  • `bool`: true if `target` is the Go string type; false otherwise.

func (*ResourceBase) ConvertTo

func (b *ResourceBase) ConvertTo(target reflect.Type) (any, error)

ConvertTo projects this resource into the given target Go type.

The baseline projection is URI → string, matching [ResourceBase.CanConvert]. Concrete Resource types that recognize additional targets override [ResourceBase.Convert] and delegate to this method for the string case.

Parameters:

  • `target`: the destination Go type the caller wants to project the resource into.

Returns:

  • `any`: the resource's URI (as a Go string) when `target` is string.
  • `error`: non-nil if `target` is not a conversion this base recognizes.

func (*ResourceBase) Digest

func (b *ResourceBase) Digest() (Digest, error)

Digest returns ErrUnimplemented.

Concrete Resource types must override — content hashing is type-specific (full file sha256, HEAD commit composition, last-observed body hash, projected from the URI for CAS, etc.).

Returns:

func (*ResourceBase) Equal

func (b *ResourceBase) Equal(other any) bool

Equal reports whether b and other identify the same resource.

Equality is URI-based and loose with respect to the concrete Go type: any two values implementing Resource whose URIs match are equal. A URI collision across concrete types (e.g., a file URI embedded in an appnet.Resource) is treated as a caller-side construction error, not a case Equal needs to disambiguate — the URI is the sole identity.

Contract (mirroring the [java.lang.Object.equals] properties):

  • Reflexive: b.Equal(b) returns true.
  • Symmetric: b.Equal(x) returns true iff x.Equal(b) returns true.
  • Transitive: if b.Equal(x) and x.Equal(y), then b.Equal(y).
  • Consistent: repeated calls return the same result while URIs are stable.
  • Nil-safe: b.Equal(nil) returns false.

Parameters:

  • `other`: the value to compare against; may be any, including nil or a non-Resource.

Returns:

  • `bool`: true if other is a Resource with the same URI as b.

func (*ResourceBase) Etag

func (b *ResourceBase) Etag() (string, error)

Etag returns the URI as the inexpensive change-detection token.

Suggestive of change but not authoritative; the catalog computes Resource.Digest only when Etag mismatches what's stored. This default is correct for resources with AddressingContent by definition. The same URI implies the contents are immutable, so the URI itself doubles as the etag at no I/O cost. AddressingLocation subtypes override with their own stamp (size + mtime + inode for files; HTTP ETag header for appnet; etc.).

Returns:

  • `string`: the URI.
  • `error`: nil.

func (*ResourceBase) Exists

func (b *ResourceBase) Exists() bool

Exists reports whether the resource currently exists.

The ResourceBase default is unimplemented — a loud stub via assert.Unimplemented. The catalog reads Exists (via ResourceCatalog.VerifyExistence) to drive the Pending → Active / Gone transition, so concrete types that participate in the discovery-side lifecycle override it (file does today; phase-8 step 22).

Returns:

func (*ResourceBase) Format

func (b *ResourceBase) Format(value any) string

Format marshals value as compact JSON.

Concrete resource receiverTypes call this from their String() method: func (r Resource) String() string { return r.Format(r) }

func (*ResourceBase) ID

func (b *ResourceBase) ID() string

ID returns the catalog-stamped identity of this resource.

func (*ResourceBase) MarshalJSON

func (b *ResourceBase) MarshalJSON() ([]byte, error)

MarshalJSON marshals the resource to its JSON form, which is the URI as a JSON-encoded string.

The URI is the resource's identity and the only field required for a round trip through JSON: catalog rehydration reconstructs the resource via [NewResource] from the stored URI. Concrete Resource types that need to persist additional fields (cached metadata, domain-specific state) override ResourceBase.MarshalJSON with their own serialization.

Returns:

  • `[]byte`: the JSON-encoded URI string.
  • `error`: any error from json.Marshal; none under normal conditions.

func (*ResourceBase) MarshalText

func (b *ResourceBase) MarshalText() ([]byte, error)

MarshalText marshals the resource to its text form, which is the URI as raw UTF-8 bytes.

The text form is consumed by stdlib encoders (encoding/json for map keys, encoding/xml for attributes), YAML scalar emission via [yaml.v3], CLI flag ingestion via flag.TextVar, and most env/config parsers. Round trip through [UnmarshalText] (implemented per concrete Resource type) reconstructs an equivalent resource.

Returns:

  • `[]byte`: the URI as UTF-8 bytes.
  • `error`: nil under normal conditions; included to satisfy the encoding.TextMarshaler interface.

func (*ResourceBase) MarshalYAML

func (b *ResourceBase) MarshalYAML() (any, error)

MarshalYAML marshals the resource for YAML encoding as a bare string scalar — the URI.

Returning a plain string (rather than a struct) yields a clean YAML scalar in serialized form, avoiding the nested-object shape that reflection-based YAML marshaling would produce. Concrete Resource types that need to persist additional fields override ResourceBase.MarshalYAML with their own representation.

Returns:

  • `any`: the URI string.
  • `error`: nil under normal conditions; included to satisfy the yaml.Marshaler interface.

func (*ResourceBase) ProducerID

func (b *ResourceBase) ProducerID() string

ProducerID returns the catalog-stamped producer node ID.

func (*ResourceBase) ReachabilityURI

func (b *ResourceBase) ReachabilityURI() string

ReachabilityURI returns the scheme-specific identity payload — the <specific> portion of the canonical tag URI.

Empty for resources constructed via Defer or otherwise in the deferred ("known-at-execution") form.

func (*ResourceBase) Resolve

func (b *ResourceBase) Resolve() error

Resolve locates the resource by URI and verifies reachability.

The ResourceBase default is unimplemented — a loud stub via assert.Unimplemented; concrete types that participate in the discovery-side lifecycle override it (file does today; phase-8 step 22). It populates no metadata — that is the observe action's role.

Returns:

func (*ResourceBase) ResourceType

func (b *ResourceBase) ResourceType() string

ResourceType returns the canonical Go type id of the concrete Resource type — the fragment portion of the canonical tag URI.

func (*ResourceBase) RuntimeEnvironment

func (b *ResourceBase) RuntimeEnvironment() *RuntimeEnvironment

RuntimeEnvironment returns the execution environment this resource was constructed against.

The resource's OWN environment (step 29): resources need it off the dispatch path — catalog verification, digesting, rehydration through the fixed-signature unmarshalers — where no activation is in scope. It is not inherited from any provider; a resource is a resource and a provider is a provider.

Returns:

  • `*RuntimeEnvironment`: the held environment; nil on an unlinked candidate built without one.

func (*ResourceBase) URI

func (b *ResourceBase) URI() string

URI returns the cached canonical tag URI of this resource.

type ResourceCatalog

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

ResourceCatalog is the graph-level owner of the append-only Resource ledger and the URI→ID addressing namespace.

One catalog per Graph. Created at plan time by the planner, consumed at execution time by the executor's preflight pass and post-dispatch transition. See docs/architecture/4-resource-management.md §6.1-§6.5, §6.8.

The catalog holds Resource interface values, which are pointers to concrete resource structs (e.g., [*file.Resource]). Preflight and node execution populate metadata fields on those structs in place; all holders of the pointer see the updated fields. The ledger's append-only property refers to the sequence of distinct resources, not to the mutability of their metadata.

Two entry classes, derived from an entry's producer:

  • Discovery: producerID == "". The entry was registered without a production claim — by ResourceCatalog.Discover, by a discovery-style provider call, or by reference handles in CLI tools. The catalog tracks the URI but no dispatch claims to have created it.
  • Production: producerID != "". The entry was created by ResourceCatalog.GetOrCreate from a producer dispatch context. The producerID is the dispatching ExecutableUnit's ID (typically a graph node ID, occasionally a subgraph ID) and is the answer to "who created this URI?" for downstream producer→consumer edge derivation.

Orthogonal to that split, every entry carries a per-run lifecycle state (ResourceState: PendingActive / Gone), owned by the catalog — read via ResourceCatalog.State, driven by ResourceCatalog.VerifyExistence on the discovery side (the executor's pre-flight resolve pass) and by [GetOrCreate] on the production side.

func NewResourceCatalog

func NewResourceCatalog() *ResourceCatalog

NewResourceCatalog creates an empty catalog.

Returns:

  • `*ResourceCatalog`: the empty catalog.

func (*ResourceCatalog) Clone

func (c *ResourceCatalog) Clone() *ResourceCatalog

Clone returns a shallow copy of this catalog with a fresh mutex.

The returned catalog has its own `entries`, `byID`, `ns`, and `nextID` — distinct from the receiver's — so subsequent appends, namespace updates, and producer-stamp changes on either catalog do not affect the other. The Resource values themselves are shared by pointer: each Resource's identity-bearing fields (URI, the `producerID` stamped by [GetOrCreate] / [Shadow]) are plan-time-fixed and effectively immutable, but mutable metadata fields populated by `Resource.Resolve` (size, mod-time, checksum, etc.) are not deep-copied. Concurrent runs that share Resource instances would race on those metadata writes — single-run cloning is the supported usage (the planning catalog handed off via Graph.ResourceCatalog and cloned into RuntimeEnvironment.ResourceCatalog at each GraphExecutor.Run invocation).

Locks the receiver's mutex for the duration of the copy so the snapshot is internally consistent; the cloned catalog gets a fresh zero-value mutex.

Returns:

  • `*ResourceCatalog`: a new catalog with the receiver's ledger structure shallow-copied. Returns nil when the receiver is nil so callers can chain Clone on optional catalogs without a nil-guard.

func (*ResourceCatalog) ContentResources

func (c *ResourceCatalog) ContentResources() []Resource

ContentResources returns the current-generation content-addressed entries, sorted by URI.

This is the list that travels with a serialized graph document: reference resources (AddressingLocation) are named by URI in slots and recreate on the target host, but a content resource's bytes must cross the boundary in the document's content section (see Packer). Only current generations qualify — a superseded generation is reachable solely by id and never feeds a slot. The URI sort keeps the packed section deterministic.

Returns:

  • `[]Resource`: the current AddressingContent entries in URI order; empty when none exist.

func (*ResourceCatalog) Current

func (c *ResourceCatalog) Current(uri string) string

Current returns the catalog ID authoritative for the given URI, or the empty string if the URI is unknown.

Two keying regimes coexist behind this lookup (see [namespaceKey]): location-addressed entries key on the fragment-stripped URI, content-addressed entries on the full URI. Callers pass whichever form they hold — the exact key is tried first, then the fragment-stripped form, so a full canonical tag URI finds a location entry keyed without its fragment.

Parameters:

  • `uri`: the URI to look up (full or fragment-stripped form).

Returns:

  • `string`: the current catalog ID for `uri`, or "" if not found.

func (*ResourceCatalog) DestroyerOf

func (c *ResourceCatalog) DestroyerOf(id string) string

DestroyerOf returns the unit that destroyed the entry `id`, or "" when the Gone transition was reactive (a failed existence check) or unattributed.

Parameters:

  • `id`: the catalog id to look up.

Returns:

  • `string`: the destroying unit's id, or "".

func (*ResourceCatalog) Discover

func (c *ResourceCatalog) Discover(uri string, factory func() (Resource, error)) (Resource, error)

Discover returns the canonical catalog entry for uri, introducing it as Pending when unseen.

Discover is the consumption-side counterpart to [GetOrCreate]. Use it from non-production callsites: plan-time slot coercion (a string path becoming a typed resource), receipt rehydration during unmarshal, and any other path where there is no producing node. The returned entry has no producerID stamped (or carries whatever stamp a previous GetOrCreate already applied) — discovery records identity, not authorship.

Discover is introduction-only — it never verifies existence (phase-8 step 22, Ruling 2): a plan-time resource is deliberately unresolved and is expected to resolve at runtime, when the executor's pre-flight resolve pass drives the PendingActive/Gone transition through ResourceCatalog.VerifyExistence. Cache-hit behavior branches on the entry's ResourceState: Active and Pending return the existing entry as-is; Gone returns an error (Gone is terminal — reviving a URI is a production act, via [GetOrCreate]'s shadow path). Cache-miss constructs a fresh candidate via factory and links it as Pending.

Parameters:

  • `uri`: the URI to look up. Must not be empty (asserted).
  • `factory`: closure invoked on cache miss to construct a fresh Resource. Must be non-nil (asserted).

Returns:

  • `Resource`: the canonical catalog entry for `uri` (Active or Pending); nil when the entry is known-Gone.
  • `error`: any factory error (returned untouched), or a known-gone error when the URI's existing entry is Gone.

Panics with an *assert.AssertionError when any precondition is violated — these are programming errors at the call site, not runtime conditions.

func (*ResourceCatalog) GetOrCreate

func (c *ResourceCatalog) GetOrCreate(producerID, uri string, factory func() (Resource, error)) (Resource, error)

GetOrCreate returns the canonical catalog entry for uri after recording the producer's claim.

GetOrCreate is the production-claim hook. Forward-method outputs flow through it via each provider's `NewResource(env, unit, ...)` constructor. The catalog stays type-neutral; the factory closure resolves the concrete-type-to-construct decision at the call site, where the type is statically known. The producerID stamp on the resulting entry is `unit.ID()` when `unit` is non-nil; non-graph dispatches (the starlark immediate-mode bridge, test fixtures, CLI runners) pass a nil `unit` and the resulting entry carries an empty producer stamp — see the discovery-vs-production split documented on ResourceCatalog.

Cache-hit behavior branches on the existing entry's [Addressing] × ResourceState per docs/architecture/4-resource-management.md §3's behavior matrix. The factory is invoked on cache miss, on location-based hits (any state), and on Gone hits (either addressing — Gone is terminal, so revival appends a new ledger entry via [Shadow]). Content-addressable hits on Pending or Active return the existing entry without invoking the factory (singleton). The current generation transitions to Active via [markActive] before returning.

A non-nil factory error short-circuits without touching the catalog. A different producer at an occupied URI appends a generation (§4, revised 2026-08-20) — run-time versioning, never a conflict.

Parameters:

  • `producerID`: the producing caller's id (`activation.CallerID` — a unit id or a starlark call-site), or "" carries `unit.ID()` as its producer stamp; when nil the stamp is empty. Discovery call sites that need to query existence without claiming production use ResourceCatalog.Discover instead.
  • `uri`: the URI to look up. Must not be empty (asserted).
  • `factory`: closure invoked on cache miss (or location/Gone shadow path) to construct a fresh Resource. Must be non-nil (asserted).

Returns:

  • `Resource`: the canonical catalog entry for `uri`, in state Active.
  • `error`: any factory error (returned untouched).

Panics with an *assert.AssertionError when any precondition is violated — these are programming errors at the call site, not runtime conditions.

func (*ResourceCatalog) IntentEntries

func (c *ResourceCatalog) IntentEntries() []IntentEntry

IntentEntries returns the graph document's intent rows: every current-generation entry, in ledger append order, rendered Pending — the stored catalog is what must exist when the graph runs, never what planning observed, so no producer stamps and no content identity travel here (4-resource-management.md §5.4, ruled 2026-08-20).

Returns:

  • `[]LedgerEntrySnapshot`: id, URI, and Pending per current generation; empty (never nil) when the ledger holds nothing — the document's section serializes even then, mandatorily.

func (*ResourceCatalog) Len

func (c *ResourceCatalog) Len() int

Len returns the number of entries in the ledger.

Returns:

  • `int`: the entry count.
func (c *ResourceCatalog) Link(resource Resource) Resource

Link interns the given resource and returns the canonical catalog entry, discarding the catalog ID.

Link is a thin convenience over ResourceCatalog.Resolve for callers that only need the linked Resource — notably the slot-fill path in the plan provider's dispatch and the rehydration path in plan.load_definition. Behavior matches Resolve exactly: first sighting of a URI catalogs the input as a discovery entry; subsequent sightings discard the input in favor of the canonical entry, which may already carry a producerID stamped by a producer node's Planned companion. The producerID stays on the returned Resource for downstream consumers to observe.

Parameters:

  • `resource`: the resource to intern. URI must be set.

Returns:

  • `Resource`: the canonical entry for `resource`'s URI.

func (*ResourceCatalog) Lookup

func (c *ResourceCatalog) Lookup(id string) (Resource, bool)

Lookup returns the resource with the given catalog ID, or false if no entry exists for that ID.

Parameters:

  • `id`: the catalog ID to look up.

Returns:

  • `Resource`: the resource at that ID.
  • `bool`: true if the ID is known.

func (*ResourceCatalog) MarkGone

func (c *ResourceCatalog) MarkGone(r Resource, destroyerID string)

MarkGone records a successful deletion: the entry's lifecycle state transitions to Gone.

The mutator-side counterpart of ResourceCatalog.VerifyExistence's discovery-side transition (phase-8 step 23, ruling 3): a provider that has removed the disk entry behind an interned resource reports the termination, so the catalog reflects what the run DID, not only what it observed. Any state may transition in — deleting a Pending entry is legal (the delete itself just observed the disk) — and re-marking a Gone entry is idempotent. Gone is terminal: no catalog operation transitions out of it, and reviving the URI is a production act that appends a fresh generation via ResourceCatalog.GetOrCreate's shadow path.

Parameters:

Panics with an *assert.AssertionError when `r` is nil or not cataloged — a programming error at the call site, not a runtime condition.

func (*ResourceCatalog) Resolve

func (c *ResourceCatalog) Resolve(r Resource) (canonical Resource, id string)

Resolve returns the canonical resource for the given resource's URI, along with its catalog ID.

If the URI has never been seen, r is cataloged as a discovery entry (no origin) and returned as-is. If the URI was previously cataloged — either as a discovery or shadowed by a producer — the canonical entry is returned and r is discarded. Callers should always use the returned Resource, not the one they passed in, so downstream consumers observe the authoritative version.

The caller is responsible for type-tagging the input: a raw string path becomes a *file.Resource via the resource type's registered constructor before reaching the catalog. The catalog never fabricates a concrete Resource type itself — the concrete type flows in from the caller.

Resolve is the link-time lookup operation: planner dispatches use it to convert typed-but-unresolved inputs into the catalog's canonical entries, picking up any `producerID` that a producer has already stamped and so creating implicit edges via URI matching.

Freshness cascade on cache hit (per ResourceBase.Etag's contract): the catalog branches on `r.Addressing()`. For AddressingContent, the URI carries the digest, so URI lookup is the complete identity check — no Etag or Digest call is needed. For AddressingLocation, the canonical entry's freshness is verified via the Etag-mismatch-then-Digest cascade: compare the input's Etag to the canonical's Etag; on match, fast-pass; on mismatch, compute Digest on both sides; on Digest match, the mismatch is metadata drift only and the canonical is returned unchanged; on Digest mismatch, the canonical is still returned (Resolve preserves the cached identity), but the drift will be visible to a future reconciliation pass. Etag and Digest calls happen outside the catalog mutex so they cannot block other namespace operations.

Parameters:

  • `r`: a typed resource with its URI set.

Returns:

  • `Resource`: the canonical entry for `r`'s URI.
  • `string`: the canonical entry's catalog ID.

func (*ResourceCatalog) Shadow

func (c *ResourceCatalog) Shadow(r Resource, producerID string) string

Shadow appends a new generation for r's URI and repoints the namespace at it — run-time versioning (4-resource-management.md §4, revised 2026-08-20).

The prior generation survives in the ledger as history; the trace tells the story "this URI was version N, and the run made it version N+1." Two producers writing one URI are generations, not a conflict — legal versioning when the plan ordered them, an authoring race when it did not (the former write-write conflict error was the superseded plan-time output model's residue). Shadowing is how production lands at an occupied URI (§3's production matrix, via ResourceCatalog.GetOrCreate) and how a Gone URI revives.

One deference: an empty `producerID` — a non-graph caller — over a produced, non-Gone entry adopts the existing generation rather than minting an anonymous one; a Gone entry is never adopted, so revival always appends.

Parameters:

  • `r`: the resource to catalog as the URI's new current generation. URI must be set.
  • `producerID`: the producing caller's stamp, or empty for a non-graph caller.

Returns:

  • `string`: the catalog id of the URI's current generation — the appended one, or the adopted existing one on the producerless deference.

func (*ResourceCatalog) Snapshot

func (c *ResourceCatalog) Snapshot() *ResourceLedgerSnapshot

Snapshot projects the catalog into a serializable *ResourceLedgerSnapshot — every generation, keyed by id.

The recovery stack references ledger entries by id; a resource URI is not a unique identity, because [Shadow] re-catalogs an existing URI as a fresh generation and the URI→id namespace tracks only the current one. Snapshot therefore captures every entry in append order (each as id, URI, producerID, and lifecycle state) plus the observation index and the id counter, so the live ledger can be rebuilt on resume with ids preserved.

Active entries additionally record both content-identity tiers — Resource.Etag and Resource.Digest — best effort (phase-8 step 48): an error leaves the field empty; Pending has nothing on disk and Gone cannot be read, so both record neither. The tier calls do I/O, so they run after the catalog mutex is released (mirroring [verifyLocationFreshness]'s discipline).

Returns:

  • `*ResourceLedgerSnapshot`: the serializable ledger projection.

func (*ResourceCatalog) State

func (c *ResourceCatalog) State(id string) ResourceState

State returns the lifecycle state for the catalog entry with the given id.

The state is per-catalog (per-run): a Clone starts with its own fresh state map, so a run's transitions never leak back to the source catalog. Unknown ids (never cataloged here, or cataloged in a sibling catalog) return the zero-value Pending.

Parameters:

  • `id`: the catalog id stamped on the resource by [GetOrCreate] / [Shadow] (read via ResourceBase.ID).

Returns:

  • `ResourceState`: the current lifecycle state — `Pending` (zero value, newly cataloged), `Active` (observed or produced), or `Gone` (Resolve failed; terminal).

func (*ResourceCatalog) Supersede

func (c *ResourceCatalog) Supersede(standing, stricter Resource) Resource

Supersede replaces the ledger entry standing for `standing`'s identity with `stricter`, carrying the catalog id and producer stamp across the swap.

**Not ResourceCatalog.Shadow.** Shadowing appends a new *generation* because the world changed — the prior version survives as history, and the trace tells that story. Superseding says nothing about the world: the same entry is simply described better, so it keeps its id, its state, and its place in the ledger, and no history accrues. Two callers need it, both cases of a claim that asserted less giving way to one that asserts more:

  • kind resolution at activation, where an unasserted claim becomes the kind the disk showed; and
  • claim time, where an unasserted claim meets a kinded one on the same identity — one rel is one identity, and the stricter assertion wins, because it is the one that can fail.

Identity is stamped here rather than by the caller: it is the catalog's business, and a caller that stamped its own id would be claiming an authority it does not have.

Parameters:

  • `standing`: the entry currently in the ledger.
  • `stricter`: the resource that takes its place; freshly built and uninterned.

Returns:

  • `Resource`: `stricter`, now carrying `standing`'s identity.

func (*ResourceCatalog) VerifyExistence

func (c *ResourceCatalog) VerifyExistence(resource Resource) error

VerifyExistence resolves a cataloged resource's lifecycle from its Resource.Exists verdict — the discovery-side counterpart to production's [ResourceCatalog.markActive] (phase-8 step 22).

An entry already resolved to Active is left as-is (no re-check); otherwise the resource's Resource.Exists predicate drives the catalog-owned transition — [markActive] when the resource exists, [markGone] plus an error when it does not — so a Pending entry becomes Active or Gone per §3 of docs/architecture/4-resource-management.md. The caller owns the reaction to a missing resource: the executor's pre-flight resolve pass records the Gone mark and decides independently whether the run proceeds (a `Gone` resource is a recorded fact; consumers of it fail on their own).

Only resources whose type implements a real Resource.Exists participate: the ResourceBase default is a loud assert.Unimplemented stub, so a not-yet-migrated type must not be routed here (step 22 wires file only).

Parameters:

  • `resource`: the cataloged resource to verify; its Resource.ID must be stamped (already interned).

Returns:

  • `error`: non-nil (and the entry marked Gone) when the resource does not exist.

type ResourceConstructor

type ResourceConstructor func(runtimeEnvironment *RuntimeEnvironment, value any) (Resource, error)

ResourceConstructor coerces a value into a typed resource.

Parameters:

  • `runtimeEnvironment`: the active RuntimeEnvironment.
  • `value`: type-specific input (e.g., a string path for file, or []byte / io.Reader / URI string for mem).

Returns:

  • `Resource`: the constructed resource.
  • `error`: non-nil if construction fails.

type ResourceLedgerSnapshot

type ResourceLedgerSnapshot struct {

	// Root is the run's bound fsroot, stamped by the executor at capture time (empty on a snapshot taken
	// outside a run). File identities are root-relative (#584), so the trace must record which root the run
	// bound — consumers derive an entry's native path by joining Root with the URI's rel payload, never by
	// parsing a native form out of identity.
	Root string `json:"root" yaml:"root"`

	// Entries is every ledger generation in append order. Replaying that order reproduces the URI→id namespace's
	// current-generation pointer (last writer for a URI wins).
	Entries []LedgerEntrySnapshot `json:"entries" yaml:"entries"`

	// NextID is the monotonic id counter, restored so post-resume production continues the id sequence.
	NextID int `json:"next_id" yaml:"next_id"`
}

ResourceLedgerSnapshot is the serializable projection of a ResourceCatalog — every generation, keyed by id.

It is the Trace field that lets a paused run's resource ledger survive save → load → resume. A resource URI is not a unique identity (see ResourceCatalog.Snapshot), so entries are keyed and referenced by id; the recovery stack's receipt references resolve against the rehydrated ledger by id.

func (*ResourceLedgerSnapshot) Rehydrate

func (s *ResourceLedgerSnapshot) Rehydrate(runtimeEnvironment *RuntimeEnvironment) (*ResourceCatalog, error)

Rehydrate rebuilds a live *ResourceCatalog from the snapshot, preserving every generation's id.

Each entry's Resource object is reconstructed from its URI: ExtractTagSpecific splits the URI into its `specific` identity and the type id, the type id resolves the registered ResourceConstructor (see [receiverRegistry.ResourceConstructorByTypeID]), and the constructor rebuilds the object from the `specific` part. Interning is disabled during reconstruction (a nil catalog makes the constructor return the bare candidate), so [ResourceCatalog.restoreEntry] stamps the saved id rather than minting a fresh one. Reconstruction in append order reproduces the namespace's current-generation pointer.

Location-addressed resources (e.g. file) round-trip exactly when the resume root resolves paths as the capture root did. A content-addressed resource whose `specific` is a digest can be rebuilt only if its constructor accepts that `specific` as a value; otherwise it surfaces here as a reconstruction error.

Parameters:

  • `runtimeEnvironment`: the resume environment; its catalog is detached during reconstruction and restored before return, so the caller installs the returned catalog.

Returns:

  • `*ResourceCatalog`: the rebuilt ledger, ids preserved.
  • `error`: a malformed URI, an unregistered type id, or a constructor failure.

type ResourceReceiverType

type ResourceReceiverType interface {
	ReceiverType
	Construct() ResourceConstructor
	SourceTypes() []reflect.Type
}

ResourceReceiverType extends ReceiverType with resource-specific capabilities.

Resources are data types that flow through starlark code or an execution graph. They are constructed by coercing a raw value (e.g., a string path becomes a file.Resource).

func NewResourceReceiverType

func NewResourceReceiverType(
	resourceType reflect.Type,
	implementation reflect.Type,
	construct ResourceConstructor,
	methodParameters map[string][]Parameter,
	sourceTypes ...reflect.Type,
) (ResourceReceiverType, error)

NewResourceReceiverType creates a ResourceReceiverType from a resource's reflect.Type.

Parameters:

  • `resourceType`: the resource's reflect.Type.
  • `construct`: coerces a raw value into the typed resource.
  • `methodParameters`: starlark parameter names per Go method.
  • `sourceTypes`: Go source types the resource is constructed from (e.g. `*starlark.Function`); each becomes a `byType` key so [ReceiverRegistry.ConstructorForSource] can resolve the constructor from a source value.

Returns:

  • `ResourceReceiverType`: the descriptor.
  • `error`: non-nil if method classification fails.

type ResourceState

type ResourceState int

ResourceState is the lifecycle state of a catalog entry.

Three states: Pending (initial — entry exists in the namespace but the underlying resource has not yet been observed or produced), Active (observation succeeded or the producer created the resource; metadata is populated), Gone (the resource is no longer there — an existence check failed, or a mutating consumer destroyed the resource and reported it; a later consumer of a Gone entry sees the state rather than rediscovering the loss).

The state field is mutated by catalog code only; provider implementations have no setter. See docs/architecture/4-resource-management.md §3 (states + the behavior matrix) for the full lifecycle spec.

const (
	// Pending is the zero value; every new catalog entry is born here.
	Pending ResourceState = iota

	// Active means the resource has been observed (discovery path) or freshly created (production path).
	Active

	// Gone means the resource is no longer there: an existence check failed, or a mutating consumer
	// destroyed the resource and reported it. A later consumer sees Gone from the catalog rather than
	// rediscovering the loss.
	Gone
)

func (ResourceState) MarshalJSON

func (s ResourceState) MarshalJSON() ([]byte, error)

MarshalJSON serializes the state as its canonical lowercase string — a document carries "pending", never a bare ordinal (the typed-value rule: no value degrades to its least-typed rendering in an artifact).

Returns:

  • `[]byte`: the JSON string form.
  • `error`: any error from the underlying marshal.

func (ResourceState) MarshalYAML

func (s ResourceState) MarshalYAML() (any, error)

MarshalYAML serializes the state as its canonical lowercase string, mirroring ResourceState.MarshalJSON.

Returns:

  • `any`: the string form for the YAML encoder.
  • `error`: always nil; present to satisfy the yaml.Marshaler interface.

func (ResourceState) String

func (s ResourceState) String() string

String returns the canonical lowercase rendering of the state.

Returns:

  • `string`: "pending", "active", or "gone".

func (*ResourceState) UnmarshalJSON

func (s *ResourceState) UnmarshalJSON(data []byte) error

UnmarshalJSON deserializes the canonical string form.

Parameters:

  • `data`: the JSON bytes to decode.

Returns:

  • `error`: a malformed JSON string, or an unknown state name.

func (*ResourceState) UnmarshalYAML

func (s *ResourceState) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML deserializes the canonical string form, mirroring ResourceState.UnmarshalJSON.

Parameters:

  • `value`: the YAML node to decode.

Returns:

  • `error`: a malformed YAML scalar, or an unknown state name.

type Result

type Result = any

Result is data that flows to downstream nodes via edges (e.g., file content, a rendered template, a query result).

The executor stores this keyed by node ID and resolves promise slots from stored Results before calling downstream Do.

type RetryPolicy

type RetryPolicy struct {

	// MaxAttempts is the maximum number of retries (0 = no retry, fail immediately).
	MaxAttempts int `json:"max_attempts" yaml:"max_attempts"`

	// Backoff is the delay strategy: none, linear, exponential.
	Backoff BackoffStrategy `json:"backoff" yaml:"backoff"`

	// InitialDelay is the delay before the first retry (Go duration string, e.g. "1s").
	InitialDelay string `json:"initial_delay,omitempty" yaml:"initial_delay,omitempty"`

	// MaxDelay caps the delay between retries (Go duration string, e.g. "30s").
	MaxDelay string `json:"max_delay,omitempty" yaml:"max_delay,omitempty"`

	// Jitter enables full jitter: the backoff curve becomes a ceiling, and [RetryPolicy.ComputeDelay] draws the
	// actual wait uniformly from [0, ceiling]. It spreads a correlated retry herd across the whole window instead
	// of releasing it as a synchronized spike (the anti-thundering-herd default for concurrent combinators such as
	// gather). Off by default; the graph-default policy (policies.retry) turns it on.
	Jitter bool `json:"jitter,omitempty" yaml:"jitter,omitempty"`
}

RetryPolicy configures retry behavior for an executable unit.

func (RetryPolicy) ComputeDelay

func (r RetryPolicy) ComputeDelay(attempt int) time.Duration

ComputeDelay returns the backoff delay before the given attempt.

Combines RetryPolicy.InitialDelay with RetryPolicy.Backoff (none / linear / exponential) and caps the result at RetryPolicy.MaxDelay when MaxDelay is non-zero. When RetryPolicy.Jitter is set, the capped value is treated as a ceiling and the returned delay is drawn uniformly from [0, ceiling] (full jitter) — so this is non-deterministic only on the jitter path; the plain backoff paths stay deterministic. Returns 0 when InitialDelay is empty or unparseable.

Parameters:

  • `attempt`: the 0-based attempt number for which the delay applies.

Returns:

  • `time.Duration`: the computed delay; 0 when no delay should be applied.

func (RetryPolicy) ParseInitialDelay

func (r RetryPolicy) ParseInitialDelay() time.Duration

ParseInitialDelay parses RetryPolicy.InitialDelay into a time.Duration.

Returns:

  • `time.Duration`: the parsed duration, or 0 when InitialDelay is empty or unparseable.

func (RetryPolicy) ParseMaxDelay

func (r RetryPolicy) ParseMaxDelay() time.Duration

ParseMaxDelay parses RetryPolicy.MaxDelay into a time.Duration.

Returns:

  • `time.Duration`: the parsed duration, or 0 when MaxDelay is empty or unparseable.

type RootBinder

type RootBinder interface {

	// BindRoot re-binds the resource's location to `root`, rel-first: identity (the rel) is unchanged;
	// the root is the run's; the native form derives.
	BindRoot(root fsroot.Dir)
}

RootBinder is the activation-binding seam (4-resource-management.md §5.5): a resource whose identity is root-relative re-binds its location to the run's root at the executor's pre-flight resolve pass.

The run, not the construction session, owns location: a resource is constructed (or rehydrated) in whatever environment planned or loaded the graph, but every location fact about it — existence, content identity, I/O — belongs to the root the run binds. A scheme whose identity is a rel implements BindRoot to re-derive its path rel-first against the run's root; Abs derives from that binding and never survives from construction. The inverse — verifying against the construction environment's root — was the run-from-elsewhere defect: a graph saved on one machine verified against the environment that loaded it, never the root that ran it. Schemes without location (the content-addressed types) simply do not implement the interface.

type RunStatus

type RunStatus struct {

	// Phase is where the run is in its lifecycle — the control dimension.
	Phase Phase `json:"phase" yaml:"phase"`

	// Condition is the worst trouble the run has met — the severity dimension, orthogonal to [RunStatus.Phase].
	Condition Condition `json:"condition" yaml:"condition"`

	// Reason names the class of event that drove the latest phase or condition move — a closed [Reason] vocabulary
	// for machine dispatch and diagnostics. [ReasonUnspecified] (the zero value) when the run has not left its
	// healthy default.
	Reason Reason `json:"reason,omitempty" yaml:"reason,omitempty"`

	// Message is the free-text detail behind [RunStatus.Reason] (e.g. "flow.degraded executed: disk 90% full",
	// typically an err.Error()), carried on the status for informative logging. Empty when there is nothing to add.
	Message string `json:"message,omitempty" yaml:"message,omitempty"`
}

RunStatus reports where a run is (Phase), the worst trouble it has met (Condition), and why it last changed.

Phase and Condition are orthogonal. Phase moves forward through the run's lifecycle — preparing, running, the pausing/paused rest states, and the two terminal phases (completed, stopped). Condition only worsens — it climbs by severity as the run meets trouble (healthy, degraded, execution-failed, compensation-failed) and never improves within a run. Completion is a Phase event, not a Condition change: a run that reached its end lands with Condition exactly as it stood (completed × healthy for a clean run, completed × degraded for one that degraded along the way). RunStatus.Reason is the prose driver of the latest move — carried on the status so one logged on its own reads informatively ("stopped/compensation_failed: unwind failed").

Terminals are derived rather than enumerated — the grid is {PhaseCompleted, PhaseStopped} × Condition. Notable cells: completed × healthy is the clean run; completed × execution_failed is a run that continued past a failure; stopped × healthy is a clean cancel; stopped × execution_failed is the default stop-on-failure end; stopped × compensation_failed is a failed unwind. The status is the executor's O(1) answer to "where is the run, how did it end, and why"; per-event detail lives on the receipts and the trace's transition journal.

Serializes as a nested object of its dimensions ({"phase": …, "condition": …, "reason": …}); each enum dimension carries its snake name through Phase and Condition's own text/YAML marshalers, and an empty reason is omitted.

func (RunStatus) String

func (r RunStatus) String() string

String renders the status as "<phase>/<condition>", appending ": <message>" when a message is present.

Returns:

  • `string`: e.g. "running/healthy", "completed/degraded", or "stopped/compensation_failed: unwind failed".

type RunStatusTransition

type RunStatusTransition struct {

	// Phase is the [Phase] the run is in after this transition.
	Phase Phase `json:"phase" yaml:"phase"`

	// Condition is the [Condition] the run is in after this transition.
	Condition Condition `json:"condition" yaml:"condition"`

	// At is when the flip was recorded, stamped by [GraphExecutor.Transition].
	At time.Time `json:"at" yaml:"at"`

	// UnitID is the unit whose outcome drove the flip; empty for run-level events (a pause command, pre-flight).
	UnitID string `json:"unit_id,omitempty" yaml:"unit_id,omitempty"`

	// Reason names the class of event that drove the flip — a [Reason] token for machine dispatch and diagnostics.
	Reason Reason `json:"reason,omitempty" yaml:"reason,omitempty"`

	// Message is the free-text detail behind [RunStatusTransition.Reason] (e.g. "flow.degraded executed: disk full").
	Message string `json:"message,omitempty" yaml:"message,omitempty"`
}

RunStatusTransition is one entry in a Trace's transition journal — a recorded flip of the run's status.

Flips-only: the journal records actual changes to the run's Phase or Condition, with when each happened, which unit drove it, and why. A repeat driver that does not change the status (a second flow.Degraded while already degraded) is a receipt, not a transition. The executor's RunStatus stays the O(1) answer; the journal answers "when did the run flip to degraded?" and "where did it flip to execution_failed?" directly, cross-referenced to per-event detail on the receipts by RunStatusTransition.UnitID.

type RuntimeEnvironment

type RuntimeEnvironment struct {

	// Application is the tool-side handle carrying the variable-resolver source maps (flags / config / overrides) and
	// the tool's program name (formerly a ProgramName field — now [application.Application.Name] is the single source
	// of truth). Framework code reads system flags such as "dry-run" directly from `Application.Flags` via
	// [application.Application.DryRun].
	Application *application.Application

	// BackupSuffix is appended to back up filenames during conflict resolution.
	BackupSuffix string

	// Context carries a deadline, a cancellation signal, and other values across API boundaries.
	//
	// See https://pkg.go.dev/context.
	Context context.Context

	// Modules are the provider receiver types exposed as Starlark globals. Populated by [NewRuntimeEnvironment] from
	// the spec's selected modules, defaulting to every announced module-provider ([ReceiverRegistry.Modules]) when the
	// spec selected none.
	Modules []ProviderReceiverType

	// Platform provides platform abstractions (package manager, service manager) to do providers.
	//
	// Nil when running in environments where host access is not needed (e.g., pure data transforms).
	Platform platform.Platform

	// Result is the primary output pipeline carried from the [RuntimeEnvironmentSpec].
	//
	// Populated by [RuntimeEnvironmentSpec.Build]. Defaults to a [result.Pipeline] writing JSON to
	// [sink.Stdout] when the spec field is nil.
	Result *result.Pipeline

	// Status is the user-facing side-channel narrator carried from the [RuntimeEnvironmentSpec].
	//
	// Same instance that flows to `cli.UI()` and through every status emission point. Populated by
	// [RuntimeEnvironmentSpec.Build] (defaults to a [status.Narrator] writing through [sink.Stderr] when the spec field
	// is nil; pass a Narrator wrapping [sink.Discard] to suppress).
	// ConceivedAt is when this session began — the one clock read a planning run makes.
	//
	// Every graph the session assembles carries it, so graphs from one planning run share a timestamp and are
	// comparable, where a per-graph construction instant made each one unique for no reason. Time of conception,
	// not time of birth.
	//
	// It also puts the clock read somewhere visible. Graph construction is now a pure function of its spec, which
	// is what lets a method assembling one claim `+devlore:claim=deterministic` with a straight face (#690).
	ConceivedAt time.Time

	Status *status.Narrator

	// RecoverySite is the shared recovery service for archiving and restoring resources during compensation.
	//
	// Instantiated by the executor from Root.
	RecoverySite *RecoverySite

	// ResourceCatalog is the resource catalog for the current execution session.
	//
	// The do layer uses it to shadow Resource results after dispatch. Nil when running without catalog integration
	// (e.g., tests).
	ResourceCatalog *ResourceCatalog
	// contains filtered or unexported fields
}

RuntimeEnvironment is the session-scoped execution context for providers, resources, and graphs.

One runtime environment per session. A session is bounded by a single CLI command invocation, a single test in the test harness, or any other unit of work that owns a graph's plan-then-execute lifecycle. Long-running processes (test runners, daemons, repls) construct a fresh runtime environment per session; one process may produce many runtime environments over its lifetime.

The runtime environment owns the root handle and the scratch tree, and is the single point of Close responsibility. See RuntimeEnvironment.Close. Every other type in this package that holds a *RuntimeEnvironment ([starlarkbridge.Runtime], GraphExecutor, Graph) is a co-user of the session, not an owner; callers construct the runtime environment, defer Close once, and pass it by pointer to whatever needs it.

Session-shared state (ResourceCatalog, variable map, provider cache, RecoverySite, …) lives on this struct, so plan-time and execute-time machinery operate on the same instances.

func NewRuntimeEnvironment

func NewRuntimeEnvironment(ctx context.Context, spec *RuntimeEnvironmentSpec) (*RuntimeEnvironment, error)

NewRuntimeEnvironment constructs a fully populated RuntimeEnvironment from this spec.

It defaults the absent optionals (Status → status.Narrator over sink.Stderr, Result → result.Pipeline writing JSON to sink.Stdout, a fresh ResourceCatalog, the detected platform.Platform, the registry's module set), mints the environment's fsroot.Dir via fsroot.OpenExisting when RuntimeEnvironmentSpec.RootPath is non-empty, and wires the RecoverySite when a Root was minted. The environment owns the minted Root — a spec never carries a live handle (issue #393) — and RuntimeEnvironment.Close releases it.

Parameters:

  • `ctx`: the context whose cancellation and values flow into providers and subprocesses.
  • `spec`: the environment configuration; must be non-nil with a non-nil Application.

Returns:

  • `*RuntimeEnvironment`: the constructed runtime environment; nil when the mint fails.
  • `error`: non-nil when fsroot.OpenExisting fails at the spec's anchor path.

func (*RuntimeEnvironment) ActionByName

func (re *RuntimeEnvironment) ActionByName(name ActionName) (Action, error)

ActionByName returns a resolved Action for the given dotted action name (e.g., "file.write_text").

The name is split into provider name and method name. The provider must play the action role. The provider instance is cached. Subsequent calls for the same provider reuse the instance.

Parameters:

  • `name`: the dotted action name (e.g., "file.write_text").

Returns:

  • `Action`: the resolved action wrapping the provider instance and method.
  • `error`: non-nil if the provider is not a registered action, the method doesn't exist, or construction fails.

func (*RuntimeEnvironment) Capture

func (re *RuntimeEnvironment) Capture(cmd *exec.Cmd) ([]byte, error)

Capture executes cmd, returning stdout bytes verbatim and streaming stderr through the environment's status UI.

In dry-run, the command is narrated and nil bytes are returned with a nil error.

Parameters:

  • `cmd`: the prepared exec.Cmd; its Stdout, Stderr, and Cancel fields are overwritten by the runner.

Returns:

  • `[]byte`: the captured stdout (nil in dry-run).
  • `error`: a wrapped exit-error including the stderr tail on non-zero exit.

func (*RuntimeEnvironment) Close

func (re *RuntimeEnvironment) Close() error

Close releases the session's owned resources — the root handle, and the scratch tree along with its contents.

Idempotent: The close path runs exactly once per runtime environment regardless of how many times Close is called. The first call performs the close and stores any joined error; later calls return the stored error without re-closing.

Callers construct the runtime environment, defer Close once, then hand the runtime environment by pointer to whatever uses it ([starlarkbridge.Runtime], GraphExecutor, providers, …). Holders do not implement their own Close. The RuntimeEnvironment is the single owner.

Returns:

  • `error`: the joined error from closing the runtime environment's owned resources, or nil on success.

func (*RuntimeEnvironment) Emit

func (re *RuntimeEnvironment) Emit(cmd *exec.Cmd, parse func([]byte) (any, error)) error

Emit captures stdout, applies parse to produce a typed value, and emits that value to the environment's result sink.

Stderr streams through the status UI. In dry-run, the command is narrated and nil is returned without invoking parse.

Parameters:

  • `cmd`: the prepared exec.Cmd.
  • `parse`: converts captured stdout bytes into the typed value forwarded to the result sink.

Returns:

  • `error`: a wrapped exit-error, parse error, or sink error; nil on success.

func (*RuntimeEnvironment) HasRoot

func (re *RuntimeEnvironment) HasRoot() bool

HasRoot reports whether the session was built with a filesystem root.

It answers from the spec's anchor, not from whether the handle has been minted yet, so the answer never changes over a session's life. A session with no anchor is legal and advertised: NewRuntimeEnvironment mints no root when RuntimeEnvironmentSpec.RootPath is empty, and `cmd/lore` builds exactly such a session for work that touches no files.

If HasRoot returns false, RuntimeEnvironment.Root panics.

Returns:

func (*RuntimeEnvironment) ModuleByName

func (re *RuntimeEnvironment) ModuleByName(name string) (any, error)

ModuleByName returns a cached provider instance for the named module, constructing it on first access.

Parameters:

  • `name`: the module name (e.g., "file", "ui").

Returns:

  • `any`: the provider instance.
  • `error`: non-nil if the name is not a registered module or construction fails.

func (*RuntimeEnvironment) ProviderByType

func (re *RuntimeEnvironment) ProviderByType(reflectType reflect.Type) (any, error)

ProviderByType returns a cached provider instance for the given Go type, constructing it on first access.

Resolves `reflectType` to its ProviderReceiverType via [ReceiverRegistry.TypeByReflection], then delegates to the shared provider cache. Use this when one provider needs to invoke another's methods (e.g., archive.Provider.CompensateExtract delegating to file.Provider.CompensateWriteText) — the returned instance is the same one [action_types.go] resolves on the dispatch path for the same `(runtimeEnvironment, type)` pair, so the GC-amortization invariant from D16(c) in `docs/plans/extract-starlark-from-op/phase-8.md` holds across this access path too.

`reflectType` must match the form passed to AnnounceProvider at registration time — the struct type (e.g., `reflect.TypeFor[file.Provider]()`), not the pointer type. The returned `any` is the constructor's return value, which is conventionally `*Provider`; callers type-assert as `provider.(*file.Provider)`.

Parameters:

  • `reflectType`: the provider's Go type, in the struct form used at registration.

Returns:

  • `any`: the provider instance.
  • `error`: non-nil if the type is not registered, the registered receiver is not a provider, or construction fails.

func (*RuntimeEnvironment) RegisterParameter

func (re *RuntimeEnvironment) RegisterParameter(p Parameter) error

RegisterParameter declares interest in a binding-layer variable.

The cascade resolves the parameter immediately against the [Application]'s source maps (override → flag → environment variable → config → default) and stores the resolved Variable in the runtime environment's `variables` map. Subsequent reads via RuntimeEnvironment.VariableByName return the typed value with VariableSource provenance.

Reregistration of the same name is idempotent when the declared Parameter.Type matches the prior declaration; a type mismatch returns an error without overwriting state.

Type checking on source-supplied values: every non-environment-variable source's raw value is asserted against the declared Type via reflect.Type.AssignableTo. Mismatches return an error before any storage. The environment variable source (which always yields strings) currently skips parameters whose Type is not string-parsable — full environment-variable-string parsing lands with the binding-resolver real implementation in a later phase.

Parameters:

  • `p`: the parameter declaration. Name and Type must be set; Default is optional.

Returns:

  • `error`: non-nil on re-registration type-mismatch or source-value type-mismatch.

func (*RuntimeEnvironment) Root

func (re *RuntimeEnvironment) Root() fsroot.Dir

Root returns the session's filesystem root, minting it on first use.

Panics when the session has no root (RuntimeEnvironment.HasRoot is false), and panics when minting fails despite the anchor having passed validation at construction. Both are invariant violations rather than conditions to handle: the first is a caller that never asked, the second is an environment that changed under a validated session. This mirrors the repository's checksum trust boundary — an error before verification, an assert after it — and keeps the ~75 call sites that structurally have a root as one-liners.

Returns:

  • `fsroot.Dir`: the session's root, the same instance on every call.

func (*RuntimeEnvironment) Run

func (re *RuntimeEnvironment) Run(cmd *exec.Cmd) error

Run executes cmd, streaming stdout and stderr line-by-line through the runtime environment's status UI.

In dry-run, the command is narrated and nil is returned without launching it.

Parameters:

  • `cmd`: the prepared exec.Cmd; its Stdout, Stderr, and Cancel fields are overwritten by the runner.

Returns:

  • `error`: a wrapped exit-error including the stderr tail on non-zero exit.

func (*RuntimeEnvironment) Scratch

func (re *RuntimeEnvironment) Scratch() fsroot.Dir

Scratch returns the session's scratch directory, minting it on first use.

The directory is the session's own private tree inside the OS temp directory, created 0700 — never a root over the temp directory itself, which is shared with every other process, cannot be mode-controlled, and could not be removed on Close. RuntimeEnvironment.Close removes the tree and everything left in it.

Takes no predicate: unlike the root, scratch has nothing to configure, so there is no session that lacks one and a HasScratch could only ever return true. Panics when minting fails, on the same grounds as RuntimeEnvironment.Root: NewRuntimeEnvironment proves the OS temp directory usable at construction.

Use scratch unless the bytes must end up in the root's tree atomically — a rename out of scratch can cross a device boundary and degrade to a copy, so a stage-then-rename stages with fsroot.Dir.CreateTemp on RuntimeEnvironment.Root instead.

Returns:

  • `fsroot.Dir`: the session's scratch directory, the same instance on every call.

func (*RuntimeEnvironment) VariableByName

func (re *RuntimeEnvironment) VariableByName(name string) (Variable, bool)

VariableByName returns the binding-layer Variable resolved for the named parameter.

It reads the eagerly-resolved `variables` map first (populated by [RegisterParameter] when a source was present at registration, and by the executor's preflight pass). On a miss it falls back to the parameter's lazy resolver (installed by [RegisterParameter], memoized with sync.OnceValues), which resolves on first read and caches — so a source value set after registration is still found, independent of call order. Returns the zero Variable and false only when the name was never declared or no source supplies a value.

Parameters:

  • `name`: the parameter name.

Returns:

  • `Variable`: the resolved variable, or the zero value when absent.
  • `bool`: true if a variable was resolved for this name; false otherwise.

type RuntimeEnvironmentConfig

type RuntimeEnvironmentConfig struct {
	devconfig.SectionBase

	// BackupSuffix is appended to back up filenames during conflict resolution.
	BackupSuffix string

	// ConflictPolicy chooses how preflight conflicts are handled.
	ConflictPolicy ConflictPolicy

	// DryRun narrates actions instead of performing them when true.
	DryRun bool
}

RuntimeEnvironmentConfig is pkg/op's own configuration section: the execution-runtime settings the framework reads.

It carries dry-run, the conflict policy, and the backup suffix — the settings RuntimeEnvironment applies during execution. As the framework's own owner it is announced at init(), and consumers read it live from application.Application.Config: the builtin floor now, the same lookup enriched with file / env / cli once the loader resolves those sources.

# TODO(david-noble): migrate the consumers (file.Provider.Backup, the dry-run readers) off the spec and Application flags onto Application.Config.

func NewRuntimeEnvironmentConfig

func NewRuntimeEnvironmentConfig() *RuntimeEnvironmentConfig

NewRuntimeEnvironmentConfig returns the runtime section at its builtin floor.

Floor: dry-run off, ConflictReplace (see the ConflictPolicy doc — in-place updates are not conflicts; the cautious stop default is writ deploy's, enforced by its pre-flight), and `BackupSuffix` ".devlore-backup".

Returns:

  • `*RuntimeEnvironmentConfig`: the runtime section at its builtin floor.

type RuntimeEnvironmentSpec

type RuntimeEnvironmentSpec struct {

	// ProgramName identifies the running tool (e.g., "lore", "writ").
	// ConceivedAt is when this session began, stamped once by [NewRuntimeEnvironmentSpec].
	//
	// Carried to [RuntimeEnvironment.ConceivedAt] and from there onto every graph the session assembles. Time of
	// conception, not time of birth.
	ConceivedAt time.Time

	ProgramName string

	// Modules lists the selected modules to expose as Starlark globals.
	Modules []ProviderReceiverType

	// Application is the tool-side handle that carries the variable-resolver source maps (flags / config /
	// overrides) and the tool's program name. Tools set this via [WithApplication]; pkg/op builds the
	// [VariableResolver] from it at [NewRuntimeEnvironment] time.
	Application *application.Application

	// ResourceCatalog is the resource catalog the constructed runtime environment will hold.
	//
	// When nil, [NewRuntimeEnvironment] creates a fresh empty [ResourceCatalog]. Callers that need to seed the
	// environment with a pre-built catalog — typically [GraphExecutor.Run] cloning the graph's planning catalog
	// onto the per-run environment — set this via [WithResourceCatalog].
	ResourceCatalog *ResourceCatalog

	// Platform classifies the host (OS, arch, distro, version) and gives access to the managers available to providers.
	//
	// Construct via [platform.Linux] / [platform.Darwin] / [platform.Windows] for explicit fixtures or via
	// [platform.Detect] for host detection.
	Platform platform.Platform

	// Result is the primary output sink.
	//
	// Carries structured data destined for the user or downstream tooling (JSON / YAML / CSV / template). The same
	// instance flows from the client's bootstrap into the runtime environment. When nil, [RuntimeEnvironmentSpec.Build]
	// defaults to a [result.Pipeline] writing JSON to [sink.Stdout].
	Result *result.Pipeline

	// RootPath is the anchor directory the constructed runtime environment's [fsroot.Dir] is minted at.
	//
	// Empty means no root: the environment's Root stays nil and no [RecoverySite] is wired. The spec carries only
	// this serializable anchor — never a live handle; [NewRuntimeEnvironment] mints and
	// [RuntimeEnvironment.Close] releases (issue #393).
	RootPath string

	// Status is the user-facing side-channel narrator.
	//
	// It Carries categorized status messages and starlark `print()` output. The same instance flows from the client's
	// bootstrap into the runtime environment. When nil, [RuntimeEnvironmentSpec.Build] defaults to a [status.Narrator]
	// writing through [sink.Stderr]; pass a Narrator wrapping [sink.Discard] to suppress.
	Status *status.Narrator
}

RuntimeEnvironmentSpec holds configuration for constructing Starlark bindings.

Use NewRuntimeEnvironmentSpec to create, then chain With* methods:

cfg := op.NewRuntimeEnvironmentSpec("lore").
    WithModules(op.ReceiverRegistry().ModuleByName("file"), op.ReceiverRegistry().ModuleByName("json")).
    WithRoot(wd).
    WithApplication(app)

func NewRuntimeEnvironmentSpec

func NewRuntimeEnvironmentSpec(programName string) *RuntimeEnvironmentSpec

NewRuntimeEnvironmentSpec creates a RuntimeEnvironmentSpec with the given program name.

Status and Result stay nil so NewRuntimeEnvironment's documented defaulting applies (a status.Narrator over sink.Stderr; a result.Pipeline writing JSON to sink.Stdout). Suppression is an explicit caller choice: pass RuntimeEnvironmentSpec.WithStatus / RuntimeEnvironmentSpec.WithResult wrapping sink.Discard.

Parameters:

  • `programName`: the name of the running tool (e.g., "lore", "writ").

Returns:

  • *RuntimeEnvironmentSpec: the initialized config.

func (*RuntimeEnvironmentSpec) WithApplication

WithApplication sets the tool-side application.Application handle.

The constructed runtime environment builds its VariableResolver from the Application's Name / Flags / Config / Overrides; framework code also reads system flags (e.g., "dry_run") directly from `Application.Flags`.

Parameters:

Returns:

  • *RuntimeEnvironmentSpec: the config for method chaining.

func (*RuntimeEnvironmentSpec) WithModules

WithModules sets the modules to expose as Starlark globals.

Parameters:

  • `modules`: the selected provider receiver types.

Returns:

  • *RuntimeEnvironmentSpec: the config for method chaining.

func (*RuntimeEnvironmentSpec) WithPlatform

WithPlatform sets the interface-typed platform capability for the constructed runtime environment.

Parameters:

Returns:

  • *RuntimeEnvironmentSpec: the config for method chaining.

func (*RuntimeEnvironmentSpec) WithResourceCatalog

func (c *RuntimeEnvironmentSpec) WithResourceCatalog(catalog *ResourceCatalog) *RuntimeEnvironmentSpec

WithResourceCatalog seeds the constructed runtime environment with the supplied *ResourceCatalog instead of a fresh one.

Used by GraphExecutor.Run to clone the planning graph's catalog onto the per-run environment, so the per-run env is born with the right catalog instead of having one created and immediately replaced.

Parameters:

  • `catalog`: the catalog to seed the environment with. Nil means "fall back to the default of a fresh empty catalog created at NewRuntimeEnvironment time."

Returns:

  • *RuntimeEnvironmentSpec: the config for method chaining.

func (*RuntimeEnvironmentSpec) WithResult

WithResult sets the primary output sink for the constructed runtime environment.

Parameters:

Returns:

  • *RuntimeEnvironmentSpec: the config for method chaining.

func (*RuntimeEnvironmentSpec) WithRoot

WithRoot sets the anchor path the constructed runtime environment mints its fsroot.Dir from.

The spec never carries a live Root: NewRuntimeEnvironment mints from this value and RuntimeEnvironment.Close releases what it minted (issue #393).

Parameters:

  • `path`: the anchor directory; empty means the constructed environment gets no root.

Returns:

  • `*RuntimeEnvironmentSpec`: the config for method chaining.

func (*RuntimeEnvironmentSpec) WithStatus

WithStatus sets the side-channel narrator for the constructed runtime environment.

Parameters:

  • `narrator`: the status.Narrator instance — typically the same one held by the cli facade via [cli.SetUI].

Returns:

  • *RuntimeEnvironmentSpec: the config for method chaining.

type Signature

type Signature struct {

	// Algorithm is the full ciphersuite as an OpenSSH key-type name: "ssh-ed25519" (the default);
	// "ecdsa-sha2-nistp256"/"-384" and "rsa-sha2-256"/"-512" are acceptable suites for later backends.
	Algorithm string `json:"algorithm" yaml:"algorithm"`

	// PublicKey is the publisher's verifying key in OpenSSH wire format.
	PublicKey []byte `json:"public_key,omitempty" yaml:"public_key,omitempty"`

	// Value is the raw signature over `namespace ‖ CanonicalContent` (domain-separated per artifact kind:
	// devlore.graph.v1 / devlore.trace.v1 — the pkg/signing namespace constants).
	Value []byte `json:"value" yaml:"value"`
}

Signature is the publisher signature carried by a signable artifact: a Graph or a Trace.

The three-field record is settled in the signing design (docs/plans/.../signing-options.md): the algorithm names the whole ciphersuite, the value is a RAW signature (no envelope) over the artifact's namespace-prefixed canonical bytes, and the public key is the publisher's key in OpenSSH wire format — what the verifier's `allowed_signers` trust list keys on. Deliberately absent: a hash field (intrinsic to the ciphersuite), an envelope, the trust list, and any identity string (identity comes from the verifier's mapping, never from the document).

type SourceConverter

type SourceConverter interface {
	CanConvertTo(target reflect.Type) bool
	ConvertTo(target reflect.Type) (any, error)
}

SourceConverter is implemented by values that know how to convert themselves into specific target Go types.

Type conversion in op runs through a small cascade: identity, reflect.Type.AssignableTo, then opt-in interfaces. SourceConverter is the source-side opt-in — the value being converted advertises which targets it can produce. SourceConverter.CanConvertTo is a cheap probe the cascade calls before committing; SourceConverter.ConvertTo does the work and may fail with a domain-specific error. A type that returns true from CanConvertTo for a given target must succeed in ConvertTo for the same target on well-formed input — the probe is a contract, not a hint.

Plan-time obligation: SourceConverter.CanConvertTo is consulted by [typesAreInterconvertible] at plan time to validate the bubble-up parameter-consistency surface ([Subgraph.mergeBubbled]). The probe is therefore called against zero-value or fresh-allocated probes of the source type — never against a populated value. Implementations MUST treat the receiver as opaque: do not dereference fields, call methods that touch state, or assume the receiver was constructed via a normal constructor. The contract is "given just my type, can I convert to `target`?" — the answer must be a pure function of the type pair, computable from the receiver's type identity alone.

type Subgraph

type Subgraph struct {

	// Name is the name of the subgraph (e.g., "install").
	Name string
	// contains filtered or unexported fields
}

Subgraph is a subsystem of the graph — a functional, structural, and transactional boundary.

Subgraphs are recursive: a subgraph contains nodes and child subgraphs, forming a tree. The graph is the root of the tree. All subgraphs participate in the saga pattern: retry, compensation. Nodes and subgraphs are peers at any level — both are vertices in the same topological sort.

executableUnits is the in-memory containment list; each entry is an ExecutableUnit (a *Node or a nested *Subgraph). The field is unexported to make [Subgraph.AddChild] the only mutator — that's where parent-ID stamping and ordering invariants are enforced. Serialization emits child IDs (plus inline child data) via custom marshalers in `marshalers.go`; deserialization rebuilds the slice through [Subgraph.linkChildren] once the surrounding Graph's unit table is populated.

func NewSubgraph

func NewSubgraph(spec *SubgraphSpec) (*Subgraph, error)

NewSubgraph constructs a sealed *Subgraph from a populated *SubgraphSpec.

Every Subgraph binds a method, by a resolved Action (`spec.Action`) OR by name (`spec.ActionName`, resolved lazily at dispatch). At least one must be present — a spec with neither is a program-construction error and panics via the assert package, because there is no structural child-walk: every subgraph (the root included) dispatches through its bound action. The returned Subgraph carries no public setters: the spec's children, slots, retry policy, and error-action subgraph are applied here, edges are materialized from the children's promise / resource references, and the children are topologically sorted. Immutable thereafter (the graph-immutability seal). Mirrors the NewGraph shape one level down.

Parameters:

  • `spec`: the populated subgraph spec; must be non-nil and carry a non-nil action OR a non-empty action name.

Returns:

  • `*Subgraph`: the sealed subgraph.
  • `error`: reserved for future validation; nil today.

func (*Subgraph) Action

func (e *Subgraph) Action() Action

Action returns the bound dispatch Action, or nil when this unit has not been bound.

Returns:

  • `Action`: the bound action, or nil.

func (*Subgraph) ActionName

func (e *Subgraph) ActionName() ActionName

ActionName returns the registry name of the action bound by name, or the empty string when this unit binds a resolved Action directly (or binds nothing).

A non-empty name is resolved lazily at dispatch via RuntimeEnvironment.ActionByName; it is the binding path for callers that hold only a name (no *ReceiverRegistry or RuntimeEnvironment in scope), e.g. the graph root naming "flow.subgraph".

Returns:

  • `string`: the bound action name, or "".

func (*Subgraph) Annotations

func (e *Subgraph) Annotations() AnnotationMap

Annotations returns this unit's annotation map.

Returns:

  • `AnnotationMap`: the annotation map wrapper.

func (*Subgraph) ChildByID

func (s *Subgraph) ChildByID(id string) ExecutableUnit

ChildByID returns this subgraph's direct child with the given ID, or nil when no direct child carries that ID.

The lookup is local — it does not recurse into nested subgraphs.

Parameters:

  • `id`: the child unit's ID.

Returns:

  • `ExecutableUnit`: the matching direct child, or nil.

func (*Subgraph) Children

func (s *Subgraph) Children() []ExecutableUnit

Children returns this subgraph's direct children in their assembled order.

Topological order once [Subgraph.sortChildren] has run; otherwise declaration order. A copy: writing through the returned slice would re-parent a subtree and leave the construction checksum describing contents that no longer exist. The units themselves are shared — that is the sharing the rule permits.

Returns:

  • `[]ExecutableUnit`: a copy of the direct children.

func (*Subgraph) Edges

func (s *Subgraph) Edges() []Edge

Edges returns this subgraph's edges.

A copy, for the reason Subgraph.Children gives: external mutation becomes impossible rather than discouraged.

Returns:

  • `[]Edge`: a copy of the edges (nil when there are none).

func (*Subgraph) ElevationOffer

func (e *Subgraph) ElevationOffer() *ElevationOffer

ElevationOffer returns the privilege-elevation offer for this unit, or nil when no elevation is required.

Returns:

  • `*ElevationOffer`: the configured elevation offer, or nil.

func (*Subgraph) Execute

func (s *Subgraph) Execute(
	ctx context.Context,
	executor *GraphExecutor,
	stack *RecoveryStack,
	variables map[string]Variable,
) (any, error)

Execute dispatches this subgraph through its bound action.

Every subgraph — including the graph root — binds an action: a resolved Action (`subgraph.Action() != nil`, the planner-built shape) or a name (`subgraph.ActionName() != ""`, the root's shape) resolved lazily at dispatch via RuntimeEnvironment.ActionByName. flow.Subgraph / flow.Gather / flow.Choose / flow.WaitUntil all reach this path: the subgraph's own slots are resolved (matching the bound method's parameter list); the activation is built with the subgraph as `Unit`; the action's Action.Do is invoked. The flow method's body orchestrates the children walk + any per-iteration semantics (retry, onError, frame minting). There is no structural child-walk — a subgraph that binds neither shape is a construction error (NewSubgraph rejects it) and surfaces as a no-Action-bound failure.

Entry checks mirror Node.Execute: cancellation first (hard signal), then pause (soft signal via GraphExecutor.Pause). The audit-receipt push happens at every exit (canceled, paused, action error, success), stamped with the subgraph's ID. Subgraph hooks (HookRegistry.FireSubgraphStart / HookRegistry.FireSubgraphComplete) fire around the dispatch.

Parameters:

  • `ctx`: the cancellation context threaded from the parent dispatch.
  • `executor`: the executor driving the run; provides hooks, the runtime environment, the audit-receipt helper, and the pause-point hook.
  • `stack`: the recovery stack child compensations push onto and that PromiseBinding.Resolve query via RecoveryStack.ResultByUnitID for upstream unit results.
  • `variables`: the per-call variable frame; passed through to child dispatches and stamped onto the activation for the bound flow method.

Returns:

  • `any`: the subgraph's terminal result, or nil for dispatches whose action produces no output, or on pause/failure.
  • `error`: non-nil on cancellation, pause (ErrPaused), an unbound subgraph, or a bound action's failure.

func (*Subgraph) ID

func (e *Subgraph) ID() string

ID returns the identifier.

Returns:

  • `string`: the unit identifier.

func (*Subgraph) MarshalJSON

func (s *Subgraph) MarshalJSON() ([]byte, error)

MarshalJSON encodes this subgraph to its canonical JSON document via [Subgraph.marshalData].

Returns:

  • `[]byte`: the encoded JSON payload.
  • `error`: any error reported by json.Marshal.

func (*Subgraph) MarshalYAML

func (s *Subgraph) MarshalYAML() (any, error)

MarshalYAML projects this subgraph to its canonical YAML value via [Subgraph.marshalData].

Returns:

  • `any`: the [subgraphData] projection handed to the YAML encoder.
  • `error`: always nil; present to satisfy the yaml.Marshaler interface.

func (*Subgraph) OnError

func (e *Subgraph) OnError() *Subgraph

OnError returns the failure-handler subgraph for this unit, or nil when no error action is configured.

Returns:

  • `*Subgraph`: the configured failure-handler subgraph, or nil. Nil defaults to the flow.Provider.Failed sentinel at dispatch time.

func (*Subgraph) OnRetry

func (e *Subgraph) OnRetry() *Subgraph

OnRetry returns the per-attempt retry-handler subgraph for this unit, or nil when none is configured.

Returns:

  • `*Subgraph`: the configured retry-handler subgraph, or nil.

func (*Subgraph) Parameters

func (s *Subgraph) Parameters() ([]Parameter, error)

Parameters are the exposed bubble-up variable surface of this subgraph.

The deduplicated set of VariableBinding references walked across every child's slots, recursing into nested subgraphs (plan-doc D3), MINUS the variables this subgraph binds locally as frame bindings. The exposed surface is what a parent caller must supply when invoking this subgraph: variables already bound locally are resolved within this subgraph's frame at dispatch time and do not propagate up. `*Subgraph` supplies this as its own implementation of ExecutableUnit.Parameters — the embedded [executableUnit] base provides none — so it is the surface seen by both direct `*Subgraph` callers and interface dispatch through ExecutableUnit.

Discovery is a graph-walk: for each child node, iterate its slots; for each slot whose value is a VariableBinding, contribute a Parameter under the variable's Name, sourcing Type and Default from the child's bound method via Method.ParameterByName keyed on the slot name. For each child subgraph, recurse — its Subgraph.Parameters already returns its own deduped, locally-filtered exposed surface; merge those entries into the parent's working set. ImmediateBinding and PromiseBinding slot fills do not contribute (they are intrinsically resolved at execution time).

Parameters with the same name and same type collapse to one entry. Parameters with the same name and different types are reported as plan-time errors via [Subgraph.mergeBubbled] and joined into the returned error, because the variable map at runtime is keyed by name and carries one value.

Method-signature-driven frame-binding filter: a Subgraph's own slot entries split into method parameters and frame bindings according to whether the slot name matches a parameter of the Subgraph's bound flow method. Any slot whose name is NOT a method parameter is a frame binding — its name is removed from the bubble-up result so callers of this Subgraph never need to supply it. When the Subgraph has no bound Action, every slot is treated as a frame binding (filter applies). An empty or nil slot map produces no filtering.

Returns:

  • `[]Parameter`: the exposed, deduplicated bubble-up surface, in stable order by Name. Returned even when error is non-nil, so callers can render a best-effort surface alongside the diagnostic.
  • `error`: an errors.Join of every same-name-different-type collision detected during the walk plus any errors returned by child ExecutableUnit.Parameters calls; nil when the walk succeeded without violations.

func (*Subgraph) ParentID

func (e *Subgraph) ParentID() string

ParentID returns the ID of the enclosing Subgraph, or the empty string when this unit has no parent.

A unit has no parent when it is the graph root or has not yet been added to any Subgraph.

Returns:

  • `string`: the parent Subgraph's ID, or "".

func (*Subgraph) ResolveSlots

func (e *Subgraph) ResolveSlots(variables map[string]Variable, stack *RecoveryStack) map[string]any

ResolveSlots returns all slot values resolved against the per-dispatch `variables` frame.

Each slot's Binding.Resolve is called with the supplied `variables` map and `stack`: VariableBinding entries look up `variables[name]`; PromiseBinding entries look up the producer's result via RecoveryStack.ResultByUnitID; ImmediateBinding entries return their stored value.

Shared by *Node and *Subgraph dispatch paths in GraphExecutor. The `variables` map is the per-call frame threaded through dispatch — at top level it's the session-resolved variables; for combinator-driven sub-dispatches (gather's per-iteration body) it's a per-iteration frame the combinator built.

Parameters:

  • `variables`: the variable frame in scope for this dispatch.
  • `stack`: the recovery stack; PromiseBinding.Resolve queries it for upstream unit results.

Returns:

  • `map[string]any`: the resolved slot values, keyed by slot name.

func (*Subgraph) RetryPolicy

func (e *Subgraph) RetryPolicy() *RetryPolicy

RetryPolicy returns this unit's retry policy, or nil when no policy is configured.

Returns:

  • `*RetryPolicy`: the configured retry policy, or nil.

func (*Subgraph) Slots

func (e *Subgraph) Slots() map[string]Binding

Slots returns this unit's slot map, keyed by parameter name.

The map aliases the unit's storage; callers must not mutate it directly — use [executableUnit.setSlot] instead.

Returns:

  • `map[string]Binding`: the slot map (may be nil).

func (*Subgraph) TransitionPolicy

func (e *Subgraph) TransitionPolicy() *TransitionPolicy

TransitionPolicy returns this unit's transition policy, or nil when no policy is configured.

Returns:

  • `*TransitionPolicy`: the configured transition policy, or nil.

type SubgraphSpec

type SubgraphSpec struct {
	ExecutableUnitSpec
	Children []ExecutableUnit
	Edges    []Edge
	Name     string
}

SubgraphSpec is the fluent builder for a *Subgraph.

It embeds ExecutableUnitSpec and adds a child list, re-declaring each inherited With* to return `*SubgraphSpec` so the chain — including its own `WithChildren` — stays on the concrete type. Hand a populated spec to NewSubgraph.

func NewSubgraphSpec

func NewSubgraphSpec() *SubgraphSpec

NewSubgraphSpec returns an empty *SubgraphSpec ready for fluent population via its With* setters.

Returns:

  • `*SubgraphSpec`: a zero-valued subgraph spec.

func (*SubgraphSpec) WithAction

func (s *SubgraphSpec) WithAction(action Action) *SubgraphSpec

WithAction sets the dispatch Action and returns the spec for chaining.

Callers that hold only a name bind via SubgraphSpec.WithActionNamed instead; every subgraph must end up bound one way or the other (there is no structural, action-less subgraph).

Parameters:

  • `action`: the Action to bind.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithActionNamed

func (s *SubgraphSpec) WithActionNamed(name ActionName) *SubgraphSpec

WithActionNamed binds the dispatch action by its registry name and returns the spec for chaining.

Validates the name against the global receiver registry and panics on an un-resolvable name (see ExecutableUnitSpec.WithActionNamed).

Parameters:

  • `name`: the dotted registry name (e.g. "flow.subgraph").

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithAnnotations

func (s *SubgraphSpec) WithAnnotations(annotations map[string]any) *SubgraphSpec

WithAnnotations sets the tool-specific annotations and returns the spec for chaining.

Parameters:

  • `annotations`: the raw `map[string]any` to stamp; nil for none.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithChildren

func (s *SubgraphSpec) WithChildren(children ...ExecutableUnit) *SubgraphSpec

WithChildren sets the subgraph's child units and returns the spec for chaining.

Parameters:

  • `children`: the ExecutableUnit children, in planned order; replaces any prior set.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithEdges

func (s *SubgraphSpec) WithEdges(edges ...Edge) *SubgraphSpec

WithEdges sets the subgraph's explicit edge list and returns the spec for chaining.

Explicit edges are applied ahead of the producer→consumer edges NewSubgraph materializes from the children's promise references; a guarded edge set (a choose decision tree, phase-8 step 10) arrives here from its planner.

Parameters:

  • `edges`: the explicit Edge list, in insertion order; replaces any prior set.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithElevationOffer

func (s *SubgraphSpec) WithElevationOffer(elevationOffer *ElevationOffer) *SubgraphSpec

WithElevationOffer sets the ElevationOffer and returns the spec for chaining.

Parameters:

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithID

func (s *SubgraphSpec) WithID(id string) *SubgraphSpec

WithID sets the unit identifier and returns the spec for chaining.

Parameters:

  • `id`: the unit identifier.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithName

func (s *SubgraphSpec) WithName(name string) *SubgraphSpec

WithName sets the subgraph's display name and returns the spec for chaining.

Parameters:

  • `name`: the subgraph name (e.g. a lore phase name like "install").

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithOnError

func (s *SubgraphSpec) WithOnError(onError *Subgraph) *SubgraphSpec

WithOnError sets the failure-handler Subgraph and returns the spec for chaining.

Parameters:

  • `onError`: the handler Subgraph, or nil for no error action.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithOnRetry

func (s *SubgraphSpec) WithOnRetry(onRetry *Subgraph) *SubgraphSpec

WithOnRetry sets the per-attempt retry-handler Subgraph and returns the spec for chaining.

Parameters:

  • `onRetry`: the retry-handler Subgraph, or nil for no retry handler.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithRetryPolicy

func (s *SubgraphSpec) WithRetryPolicy(retryPolicy *RetryPolicy) *SubgraphSpec

WithRetryPolicy sets the RetryPolicy and returns the spec for chaining.

Parameters:

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithSlot

func (s *SubgraphSpec) WithSlot(name string, value Binding) *SubgraphSpec

WithSlot binds one slot value by parameter name and returns the spec for chaining.

Parameters:

  • `name`: the parameter name (or frame-binding name) the slot fills.
  • `value`: the Binding to bind.

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

func (*SubgraphSpec) WithTransitionPolicy

func (s *SubgraphSpec) WithTransitionPolicy(transitionPolicy *TransitionPolicy) *SubgraphSpec

WithTransitionPolicy sets the TransitionPolicy and returns the spec for chaining.

Parameters:

Returns:

  • `*SubgraphSpec`: the receiver, for chaining.

type Summary

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

Summary is the per-action tally of an execution, reconstructed from a Trace by Trace.Summarize.

It replaces the execution summary the mutable Graph carried before the graph-immutability seal: the counts now derive from the trace's receipt stack rather than from per-node state on the graph.

func (Summary) ByAction

func (s Summary) ByAction() map[string]ActionSummary

ByAction returns the per-action tallies keyed by short action name (e.g. "file.link").

func (Summary) Failed

func (s Summary) Failed() int

Failed returns the number of node dispatches that returned an error.

func (Summary) Skipped

func (s Summary) Skipped() int

Skipped returns the number of planned nodes that never dispatched.

type TargetConverter

type TargetConverter interface {
	CanConvertFrom(source reflect.Type) bool
	ConvertFrom(value any) (any, error)
}

TargetConverter is implemented by types that know how to absorb specific source Go values into themselves.

TargetConverter is the target-side opt-in counterpart to SourceConverter — useful when the source is a stdlib or third-party type that cannot be retrofitted with methods (`[]any`, `map[string]any`, primitive slices) but the target can opt in. The Convert cascade consults sources first, then registered Resource constructors (when the target is a Resource type and a [RuntimeEnvironment.ReceiverRegistry] is available), then this target-side opt-in as the final reflective path. TargetConverter.CanConvertFrom takes a reflect.Type (not a value) so the probe stays cheap and value-free; TargetConverter.ConvertFrom commits with the actual source value and returns the constructed target instance.

Plan-time obligation: TargetConverter.CanConvertFrom is consulted by [typesAreInterconvertible] at plan time to validate the bubble-up parameter-consistency surface ([Subgraph.mergeBubbled]). The probe is therefore called against a freshly-allocated probe of the target type (via reflect.New) — its receiver is non-nil but otherwise zero. Implementations MUST be safe on a zero receiver: do not dereference receiver fields, call methods that touch state, or assume the receiver was constructed via a normal constructor. The contract is "given just my type, can I absorb `source`?" — the answer must be a pure function of the type pair, computable from the receiver's type identity alone.

Resource implementers: opting into TargetConverter advertises that a CLI flag, env var, or config value of the declared source type can fill a slot typed as the Resource. The framework wires both halves uniformly: [Subgraph.mergeBubbled] honors the convertibility relation at plan time (no false collisions between, say, a `string` slot and a *Resource slot bound to the same variable name); Convert step 6 (registered constructor) produces the canonical, env-aware Resource at dispatch when [RuntimeEnvironment.ReceiverRegistry] is available; Convert step 7 (TargetConverter.ConvertFrom) is the env-less fallback for library callers or test fixtures that exercise Convert outside a runtime session. Resource implementers' TargetConverter.ConvertFrom typically returns a minimal unlinked Resource (identity field set, catalog interning deferred to the receiving provider method's own [NewResource]/[DiscoverResource] call). Content-addressed Resources (mem / function / json / yaml) commonly omit TargetConverter when their natural source is content bytes or a parsed structure — not a CLI string — so the cascade falls back to the registered constructor unconditionally.

type Trace

type Trace struct {

	// GraphChecksum is the canonical "sha256:<hex>" identity of the graph this trace was taken
	// against. Required for resume to refuse mismatched graphs.
	GraphChecksum string `json:"graph_checksum" yaml:"graph_checksum"`

	// RunStatus is the executor's [RunStatus] (phase × condition × reason) at the moment the trace was taken.
	RunStatus RunStatus `json:"run_status" yaml:"run_status"`

	// Transitions is the run's flips-only transition journal — one [RunStatusTransition] per recorded change of the
	// [Phase] or [Condition] dimension, in order, written by [GraphExecutor.Transition]. [Trace.RunStatus]
	// is the O(1) answer; this journal answers when and where each flip happened.
	Transitions []RunStatusTransition `json:"transitions,omitempty" yaml:"transitions,omitempty"`

	// Stack is the recovery stack of per-dispatch receipts (audit + compensation entries).
	Stack *RecoveryStack `json:"stack"           yaml:"stack"`

	// Variables is the resolved variable map at the time of the trace.
	Variables map[string]Variable `json:"variables,omitempty" yaml:"variables,omitempty"`

	// Catalog is the serialized resource ledger — every generation keyed by id — captured at Run teardown for
	// every outcome. Resume rehydrates a paused run's ledger into the live [ResourceCatalog] and resolves the
	// recovery stack's receipt id references against it; a completed run's ledger carries the recorded
	// content-identity pair (Etag/Digest, phase-8 step 48) drift attribution reads back.
	Catalog *ResourceLedgerSnapshot `json:"catalog,omitempty" yaml:"catalog,omitempty"`

	// Checksum is the trace's own tier-1 integrity hash — [GitStyleChecksum]("trace", canonical) over
	// [Trace.CanonicalContent] — stamped at persist by [SaveTrace] and recomputed and compared by
	// [LoadTrace]. Excluded (with Signature) from the canonical bytes, so integrity and authenticity verify
	// independently. See docs/architecture/5-graph-trace-integrity.md § Document Integrity.
	Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"`

	// Signature is the trace's publisher signature, or nil when unsigned (phase-8 step 46). The raw signature
	// covers `devlore.trace.v1 ‖ CanonicalContent`; the store's [WriteTrace] signs at persist, and
	// [Trace.CanonicalContent] excludes this field so verification round-trips.
	Signature *Signature `json:"signature,omitempty" yaml:"signature,omitempty"`
}

Trace is the serializable projection of a *GraphExecutor's per-run mutable state.

Trace pairs with a *Graph (loaded separately via LoadGraph) to fully describe an execution that can be resumed: the graph carries the immutable plan; the trace carries the RunStatus triplet at the moment of capture, the *RecoveryStack of dispatch receipts (audit + compensation), and the resolved variable map. The Trace.GraphChecksum identifies which graph the trace was taken against; a future resume constructor compares it against the loaded graph's checksum to refuse stale traces.

A trace whose RunStatus.Phase is PhasePaused is resumable. A trace in a terminal phase (PhaseCompleted or PhaseStopped) is for archival — restoring such a trace reconstructs the same terminal triplet, not a runnable executor. The compensation-failure contract (phase-8 step 21) makes the stopped × ConditionCompensationFailed trace a restartable journal: the framework retains the failed unwind's stack (the source plus each receipt's `compensation_error`) so the journal survives (landed 2026-07-13); the state-checked resume *from* such a trace — a re-query-and-unwind, not a forward retry — is not yet built.

func LoadTrace

func LoadTrace(data []byte) (*Trace, error)

LoadTrace decodes a persisted trace document and verifies its tier-1 checksum.

The verification is the trust boundary for every downstream trace consumer: an unreadable document, a missing checksum, or a mismatch is an error — expected external corruption. Past this gate, decode failures are bugs and panic (docs/architecture/5-graph-trace-integrity.md § The Checksum Trust Boundary). There is no unverified read path: a trace written before checksums existed is refused.

Parameters:

  • `data`: the trace document's YAML bytes.

Returns:

  • `*Trace`: the decoded, integrity-verified trace.
  • `error`: non-nil on decode failure, a missing checksum, or a checksum mismatch.

func (*Trace) CanonicalContent

func (t *Trace) CanonicalContent() ([]byte, error)

CanonicalContent returns the trace's canonical bytes: its YAML document form without the integrity fields.

The canonical bytes are what the trace's checksum and signature both cover (the signer prefixes the devlore.trace.v1 namespace — phase-8 step 46, mirroring Graph.CanonicalContent). Unlike the graph — whose canonical serialization is hand-built and round-trip-stable — the trace's live form holds typed values (receipt results, catalog resources) that are not a marshal fixed point with their decoded document forms. Canonical is therefore defined over the DOCUMENT form: marshal, decode generically, strip `checksum` and `signature`, re-marshal — which produces identical bytes from a live trace and from a decoded document (yaml key ordering is stable), so one checksum and one signature verify on both sides.

Returns:

  • `[]byte`: the canonical YAML bytes.
  • `error`: non-nil if a marshaling step fails.

func (*Trace) SignWith

func (t *Trace) SignWith(sign func(canonical []byte) (*Signature, error)) error

SignWith signs the trace through `sign`, setting the signature exactly once.

The seam keeps pkg/op crypto-free, mirroring Graph.SignWith: this method supplies the canonical bytes and stores the result; the signer (pkg/signing) owns the ciphersuite and key custody.

Parameters:

  • `sign`: computes the *Signature over the canonical bytes (the signer prefixes its namespace).

Returns:

  • `error`: non-nil when the trace is already signed, canonicalization fails, or `sign` fails.

func (*Trace) StampChecksum

func (t *Trace) StampChecksum() error

StampChecksum computes and sets the trace's tier-1 checksum over its canonical bytes.

Idempotent: the canonical bytes exclude the checksum field, so restamping recomputes the same value. SaveTrace stamps at persist — which is why it can do so unconditionally — and LoadTrace recomputes and compares.

Returns:

  • `error`: non-nil when canonicalization fails.

func (*Trace) Summarize

func (t *Trace) Summarize(graph *Graph) Summary

Summarize reconstructs a Summary of this trace's execution.

Walks the trace's receipt stack (RecoveryStack.Receipts) and tallies, per dispatched action, the dispatches that completed (keyed by the receipt's short [Receipt.ActionLabel], e.g. "file.link") versus those that failed. Receipts with an empty label — audit-only entries pushed at a non-dispatching exit (cancellation, pause, or a unit whose action never resolved) — are skipped, so a failure is not double-counted against both a failing node and its propagating parent.

`graph` is optional and consulted only for the skipped count: nodes in `graph` with no receipt are counted as skipped (planned but never reached; the executor unwinds on first failure). A nil `graph` yields a Summary with no skipped count — the per-action and failed tallies come from the trace alone.

Parameters:

  • `graph`: the executed graph, or nil. When supplied, its Graph.Nodes provide the planned set for the skipped count.

Returns:

  • Summary: the reconstructed per-action / skipped / failed tally.

type TransitionPolicy

type TransitionPolicy struct {

	// Degraded is the reaction when the run's condition flips to [ConditionDegraded]. Floor: [ReactionContinue].
	Degraded Reaction `json:"degraded" yaml:"degraded"`

	// ExecutionFailed is the reaction when the run's condition flips to [ConditionExecutionFailed]. Floor:
	// [ReactionStop]. The Go field reads subject-verb; the serialized key keeps the settled `execution_failed` form.
	ExecutionFailed Reaction `json:"execution_failed" yaml:"execution_failed"`

	// CompensationFailed is the reaction when the run's condition flips to [ConditionCompensationFailed]. Floor:
	// [ReactionStop]; [ReactionContinue] is rejected by [TransitionPolicy.Validate].
	CompensationFailed Reaction `json:"compensation_failed" yaml:"compensation_failed"`
}

TransitionPolicy maps each aberrant Condition to the Reaction the executor takes when the run's condition flips to it.

The floor (from NewPoliciesConfig) is degraded → continue, execution_failed → stop, compensation_failed → stop: the author chose to degrade, so degradation continues; a failure stops at its saga boundary with the consistent pre-run state. Pause is the attended-mode override for the two failure conditions, layered in via profile / app config. `continue` is never legal for `compensation_failed` — you cannot walk on past a dirty unwind — TransitionPolicy.Validate enforces it.

func (TransitionPolicy) ReactionFor

func (p TransitionPolicy) ReactionFor(condition Condition) Reaction

ReactionFor reports the configured Reaction for a condition flip.

The healthy condition is not aberrant and never flips through a policy consultation, so it maps to ReactionContinue alongside any unrecognized value.

Parameters:

  • `condition`: the Condition the run flipped to.

Returns:

  • `Reaction`: the reaction configured for `condition`.

func (TransitionPolicy) Validate

func (p TransitionPolicy) Validate() error

Validate reports whether the policy is internally consistent.

The sole invariant: `continue` is never legal for `compensation_failed` — walking on past a dirty unwind is forbidden. Pause and stop are both legal there (pause holds the dirty residue for inspection after the best-effort unwind completes).

Returns:

type Unpacker

type Unpacker interface {

	// Unpack reconstructs the resource from packed content bytes.
	//
	// Parameters:
	//   - `runtimeEnvironment`: the session runtime environment; supplies the local content-addressed store the
	//     bytes materialize into.
	//   - `uri`: the resource's canonical tag URI as recorded in the document; the reconstructed resource must
	//     reproduce it exactly.
	//   - `content`: the packed bytes produced by [Packer.Pack].
	//
	// Returns:
	//   - `Resource`: the reconstructed resource, not interned in any catalog — the caller decides the catalog.
	//   - `error`: non-nil on malformed content, a URI mismatch (integrity failure), or a store write failure.
	Unpack(runtimeEnvironment *RuntimeEnvironment, uri string, content []byte) (Resource, error)
}

Unpacker is the inverse of Packer: it reconstructs a content resource from a document's content section.

Unpack is dispatched on a zero value of the resource type — the receiver carries no state; the resource type is resolved from the URI fragment's canonical Go type id through the announced inventory ([receiverRegistry.UnpackerByTypeID]), so no new registry exists for transport. Implementations MUST verify that the reconstructed resource's URI equals `uri`: the URI carries the content digest and is covered by the graph checksum and signature, so the equality check is what extends that integrity guarantee over the (unsigned) content section — tampered bytes fail here.

type Value

type Value interface {
	Provider
	Unwrap() reflect.Value
}

Value wraps an arbitrary Go object with an execution context.

Unlike Provider (which has side effects and lifecycle) and Resource (which has identity via URI), a Value is a plain Go object returned from a provider method. It carries the execution context for registry access and holds the underlying Go value as a reflect.Value to avoid round-trips through any.

Types should satisfy this interface by embedding ValueBase.

type ValueBase

type ValueBase struct {
	ProviderBase
	// contains filtered or unexported fields
}

ValueBase provides a standardized implementation of the Value interface.

It embeds ProviderBase for execution context access and stores the wrapped Go value as a reflect.Value.

func NewValueBase

func NewValueBase(runtimeEnvironment *RuntimeEnvironment, v reflect.Value) *ValueBase

NewValueBase creates a ValueBase wrapping the given Go value.

Parameters:

  • ctx: the execution context.
  • v: the Go value to wrap.

Returns:

  • ValueBase: the initialized base.

func (*ValueBase) Unwrap

func (v *ValueBase) Unwrap() reflect.Value

Unwrap returns the underlying Go value.

type Variable

type Variable struct {

	// Name is the parameter name the variable satisfies. Matches the parameter declared via plan.variable(name).
	Name string `json:"name" yaml:"name"`

	// Field optionally projects one field out of a record-valued variable at resolve time — authored via
	// plan.item(field) or plan.variable(name, field=...). Empty means the whole value (phase-8 step 45).
	Field string `json:"field,omitempty" yaml:"field,omitempty"`

	// Value is the resolved value, already parsed to the parameter's declared Go type by the resolver.
	// Env-sourced strings are parsed; other sources supply already-typed values.
	Value any `json:"value" yaml:"value"`

	// Source records the source kind and lookup key that produced this value.
	Source VariableSource `json:"source" yaml:"source"`
}

Variable pairs a resolved value with its name and source. Produced by VariableResolver.Resolve and consumed by the executor at slot-fill time for VariableBinding slots.

func (Variable) String

func (v Variable) String() string

String formats as "<name> = <value> [<source>]". The bracketed source keeps the boundary between value and source unambiguous even when the value contains spaces.

Returns:

  • `string`: the canonical "<name> = <value> [<source>]" form.

type VariableBinding

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

VariableBinding references a Variable by name, optionally projecting one field of a record-valued variable.

It is authored at plan time via plan.variable("name") — or, projected, via plan.variable("name", field=...) / plan.item(field) (phase-8 step 45) — and resolved at execution time via the variable map passed to GraphExecutor.Run. It is assembled by VariableResolver from layered sources: default => config => env => flag => override.

func NewVariableBinding

func NewVariableBinding(name string) VariableBinding

NewVariableBinding returns a VariableBinding referencing the Variable named `name`.

Parameters:

  • `name`: the variable name, resolved at execution time from the layered variable sources.

Returns:

  • `VariableBinding`: the binding.

func NewVariableBindingWithField

func NewVariableBindingWithField(name, field string) VariableBinding

NewVariableBindingWithField returns a VariableBinding that resolves the Variable named `name` and then projects `field` from the record it holds (phase-8 step 45).

Parameters:

  • `name`: the variable name, resolved at execution time from the layered variable sources.
  • `field`: the record field to project; "" resolves the whole value (equivalent to NewVariableBinding).

Returns:

  • `VariableBinding`: the binding.

func (VariableBinding) Edge

func (b VariableBinding) Edge(_ string) *Edge

Edge returns nil: a variable is injected from the RuntimeEnvironment at execution time, not produced by a unit, so it induces no dependency edge.

Parameters:

  • `consumer`: the id of the consuming unit (ignored).

Returns:

  • `*Edge`: always nil.

func (VariableBinding) Field

func (b VariableBinding) Field() string

Field returns the projected field name, or "" when the binding resolves the whole value.

Returns:

  • `string`: the projected field, or "".

func (VariableBinding) Name

func (b VariableBinding) Name() string

Name returns the referenced variable's name.

Returns:

  • `string`: the variable name.

func (VariableBinding) Resolve

func (b VariableBinding) Resolve(variables map[string]Variable, _ *RecoveryStack) any

Resolve returns the value of the named variable from the supplied variable map, projected to the binding's field when one is set.

The record arrives as the converted natural form (`map[string]any` for a .star dict). A projection against an absent field or a non-record value resolves nil — plan-time validation makes that unreachable for immediate gather items ([flow.GatherPlanner]); elsewhere the preflight required-parameter check surfaces the miss.

Parameters:

  • `variables`: the resolved variable map keyed by parameter name.
  • `stack`: the recovery stack (ignored).

Returns:

  • `any`: the named variable's value (or its projected field), or nil if absent or the map is nil.

type VariableResolver

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

VariableResolver assembles variable values from layered sources with explicit precedence. Construction captures a reference to the application.Application whose source maps (Flags / Config / Overrides) and Name drive the cascade. The resolver is read-only after construction except for the internal resolved map populated by VariableResolver.Resolve.

Precedence per parameter (descending; first hit wins):

  1. Override — programmatic force.
  2. Flag — command-line argument (snake-case keys; see application.NewApplication for the kebab→snake normalization).
  3. Env — process environment, key = `strings.ToUpper(app.Name) + "_" + strings.ToUpper(CamelToSnake(name))`. Env strings are parsed via [envValue] routed through Convert step 5. Resource-typed parameters short-circuit envValue and feed Convert step 7 (registered Resource construction) directly with the raw string.
  4. Config — config map.
  5. Default — the parameter's declared default (only when `p.Optional` and `p.Default != nil`).

Missing required parameters (`Optional == false` with no source hit) produce aggregated errors naming the parameter and the literal lookup keys the cascade tried.

func NewVariableResolver

func NewVariableResolver(app *application.Application) *VariableResolver

NewVariableResolver constructs a VariableResolver from a tool's application.Application.

Parameters:

  • `app`: the application handle whose source maps drive the cascade. Must be non-nil.

Returns:

  • *VariableResolver: the constructed resolver.

func (*VariableResolver) EnvPrefix

func (r *VariableResolver) EnvPrefix() string

EnvPrefix returns the env-var lookup prefix derived from `app.Name`.

The prefix is uppercased, with hyphens converted to underscores so multi-word program names (`devlore-test`, `noble-factor`) produce POSIX-valid env keys (`DEVLORE_TEST_*`, `NOBLE_FACTOR_*`). Returns the empty string when the underlying application is nil or its Name is empty — in that case the env step of the cascade is skipped (parameter names alone are too generic to safely shadow process env).

Returns:

  • `string`: the env-var prefix (e.g., "WRIT_" or "DEVLORE_TEST_"), or "" when app or app.Name is empty.

func (*VariableResolver) Get

func (r *VariableResolver) Get(name string) (Variable, bool)

Get returns the Variable resolved for the named parameter.

Panics if called before VariableResolver.Resolve.

Parameters:

  • `name`: the parameter name.

Returns:

  • `Variable`: the resolved variable.
  • `bool`: true if a variable was resolved for this name; false otherwise.

func (*VariableResolver) Resolve

func (r *VariableResolver) Resolve(runtimeEnvironment *RuntimeEnvironment, parameters []Parameter) []error

Resolve walks each parameter through the source precedence chain and populates the resolver's map.

Aggregates errors rather than failing fast — callers (the executor's preflight pass in Phase 4) fold the returned slice into the D5 envelope.

Parameters:

  • `runtimeEnvironment`: the runtime environment carried into Convert step 7 for env-sourced Resource targets; may be nil for resolver paths that never reach a Resource target.
  • `parameters`: the parameter specs to resolve.

Returns:

  • []error: aggregated errors (missing required, type mismatch, default-type mismatch). Nil on success.

func (*VariableResolver) Variables

func (r *VariableResolver) Variables() map[string]Variable

Variables returns the full resolved variable map. Panics if called before VariableResolver.Resolve.

Returns:

  • map[string]Variable: the resolved variable map, keyed by parameter name.

type VariableSource

type VariableSource struct {

	// Kind identifies the source category. See [VariableSourceKind] for the enum.
	Kind VariableSourceKind `json:"kind" yaml:"kind"`

	// Name is the literal lookup key that matched. Examples: "WRIT_TARGET_ROOT" for an env hit;
	// "target_root" for a flag/config/default hit.
	Name string `json:"name" yaml:"name"`
}

VariableSource records where a resolved Variable's value came from.

func (VariableSource) String

func (s VariableSource) String() string

String formats as "<kind>:<name>". VariableSourceKindUnknown renders as "unknown" alone since no name is meaningful in that case.

Returns:

  • `string`: the canonical "<kind>:<name>" form, or "unknown" for the zero value.

type VariableSourceKind

type VariableSourceKind int

VariableSourceKind identifies a variable-value source category. Numeric values ascend with precedence — higher beats lower. Callers can compare kinds directly to determine which source would win in a cascade.

const (
	// VariableSourceKindUnknown is the zero value; should not appear on a resolved Variable.
	VariableSourceKindUnknown VariableSourceKind = iota

	// VariableSourceKindDefault — the parameter's declared default; lowest non-unknown precedence.
	VariableSourceKindDefault

	// VariableSourceKindConfig — starlark or YAML config files.
	VariableSourceKindConfig

	// VariableSourceKindEnv — process environment variables, derived prefix from ProgramName.
	VariableSourceKindEnv

	// VariableSourceKindFlag — command-line arguments parsed by the program's flag layer.
	VariableSourceKindFlag

	// VariableSourceKindOverride — programmatic force; highest precedence.
	VariableSourceKindOverride
)

func (VariableSourceKind) String

func (k VariableSourceKind) String() string

String returns the canonical lowercase name of the VariableSourceKind.

Returns:

  • `string`: the canonical name ("unknown", "default", "config", "env", "flag", or "override").

Directories

Path Synopsis
Package claimcheck verifies that a provider method does what its `+devlore:claim=` directive says.
Package claimcheck verifies that a provider method does what its `+devlore:claim=` directive says.
Package provider holds the shared provider-registration surface.
Package provider holds the shared provider-registration surface.
appnet
Package appnet provides network actions for the operation graph.
Package appnet provides network actions for the operation graph.
archive
Package archive provides archive extraction actions for the operation graph.
Package archive provides archive extraction actions for the operation graph.
elevator
Package elevator is a STUB / PLACEHOLDER for the privilege-elevation provider — the mechanism that fulfills the elevation policy ([6.1-privilege-elevation.md]).
Package elevator is a STUB / PLACEHOLDER for the privilege-elevation provider — the mechanism that fulfills the elevation policy ([6.1-privilege-elevation.md]).
encryption
Package encryption provides encryption and decryption actions for the operation graph.
Package encryption provides encryption and decryption actions for the operation graph.
file
Package file provides file system actions for the operation graph.
Package file provides file system actions for the operation graph.
flow
Package flow implements flow-control methods for execution graphs.
Package flow implements flow-control methods for execution graphs.
function
Package function is the function provider: session-scoped Starlark function resources.
Package function is the function provider: session-scoped Starlark function resources.
git
json
Package json provides JSON encoding and decoding for the operation graph.
Package json provides JSON encoding and decoding for the operation graph.
mem
Package mem is the in-memory resource provider.
Package mem is the in-memory resource provider.
pkg
Package pkg provides package management actions for the operation graph.
Package pkg provides package management actions for the operation graph.
plan
Package plan provides graph-construction actions for the plan namespace.
Package plan provides graph-construction actions for the plan namespace.
platform
Package platform provides access to platform information by graph actions and executing receivers.
Package platform provides access to platform information by graph actions and executing receivers.
powershell
Package powershell provides PowerShell 7+ command execution actions for the operation graph.
Package powershell provides PowerShell 7+ command execution actions for the operation graph.
regex
Package regex provides regular expression operations for the operation graph.
Package regex provides regular expression operations for the operation graph.
service
Package service provides platform-agnostic service management actions.
Package service provides platform-agnostic service management actions.
shell
Package shell provides POSIX shell command execution actions for the operation graph.
Package shell provides POSIX shell command execution actions for the operation graph.
template
Package template provides template expansion actions for the operation graph.
Package template provides template expansion actions for the operation graph.
ui
Package ui exposes the runtime environment's [status.Narrator] capability to starlark.
Package ui exposes the runtime environment's [status.Narrator] capability to starlark.
yaml
Package yaml provides YAML encoding and decoding for the operation graph.
Package yaml provides YAML encoding and decoding for the operation graph.
Package server is the HTTP/2 wire listener for op runs (architecture 2.7) — it bridges a remote consumer to a run's in-process op.ControlPlane over REST commands and Server-Sent Events, so the whole bidirectional channel is drivable with curl.
Package server is the HTTP/2 wire listener for op runs (architecture 2.7) — it bridges a remote consumer to a run's in-process op.ControlPlane over REST commands and Server-Sent Events, so the whole bidirectional channel is drivable with curl.
Package starlarkbridge bridges the Go framework and Starlark scripts.
Package starlarkbridge bridges the Go framework and Starlark scripts.

Jump to

Keyboard shortcuts

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