package plan

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

func (config *ArtifactNodeConfig) UnmarshalJSON(raw []byte) error {
	type wire ArtifactNodeConfig
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var decoded wire
	if err := decoder.Decode(&decoded); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return fmt.Errorf("multiple node config values")
		}
		return err
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		return err
	}
	*config = ArtifactNodeConfig(decoded)
	_, config.fanOutPresent = fields["fanOut"]
	return nil
}

func (scope *ArtifactScope) UnmarshalJSON(raw []byte) error {
	type wire ArtifactScope
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var decoded wire
	if err := decoder.Decode(&decoded); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return fmt.Errorf("multiple scope values")
		}
		return err
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		return err
	}
	*scope = ArtifactScope(decoded)
	_, scope.fanOutPresent = fields["fanOut"]
	return nil
}

func (config *ArtifactRequestApprovalConfig) UnmarshalJSON(raw []byte) error {
	type wire ArtifactRequestApprovalConfig
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var decoded wire
	if err := decoder.Decode(&decoded); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return fmt.Errorf("multiple requestApproval config values")
		}
		return err
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		return err
	}
	*config = ArtifactRequestApprovalConfig(decoded)
	_, config.feedbackScopePresent = fields["feedbackScope"]
	return nil
}

func (config *ArtifactSendMessageConfig) UnmarshalJSON(raw []byte) error {
	type wire ArtifactSendMessageConfig
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var decoded wire
	if err := decoder.Decode(&decoded); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return fmt.Errorf("multiple sendMessage config values")
		}
		return err
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		return err
	}
	*config = ArtifactSendMessageConfig(decoded)
	_, config.workScopeModePresent = fields["workScopeMode"]
	_, config.workScopePresent = fields["workScope"]
	_, config.relatedScopesPresent = fields["relatedScopes"]
	_, config.responderPresent = fields["responder"]
	_, config.selectOnePresent = fields["selectOne"]
	_, config.selectOneStrategyPresent = fields["selectOneStrategy"]
	_, config.relatedScopesArrayPresent = fields["relatedScopesArray"]
	_, config.workItemsPresent = fields["workItems"]
	return nil
}

func (config *ArtifactSendMessageConfig) usesV4Feature() bool {
	return config != nil &&
		(config.responderPresent || config.selectOnePresent || config.relatedScopesArrayPresent || config.workItemsPresent ||
			config.Responder != nil || config.SelectOne != nil || config.RelatedScopesArray != nil || config.WorkItems != nil)
}

func (config *ArtifactSendMessageConfig) usesAgentScopeFeature() bool {
	return config != nil &&
		(config.workScopeModePresent || config.workScopePresent || config.relatedScopesPresent ||
			config.relatedScopesArrayPresent || config.WorkScopeMode != "" || config.WorkScope != nil ||
			config.RelatedScopes != nil || config.RelatedScopesArray != nil)
}

// DecodeArtifact strictly decodes one normalized IR envelope and verifies its
// supported versions, structural invariants, and semantic digest.
func DecodeArtifact(raw []byte) (*Artifact, error) {
	if len(raw) > MaxArtifactBytes {
		return nil, fmt.Errorf("Plan artifact must not exceed %d bytes", MaxArtifactBytes)
	}
	if nesting, err := artifactJSONNesting(raw); err == nil && nesting > MaxArtifactJSONNesting {
		return nil, fmt.Errorf("Plan artifact JSON nesting must not exceed %d", MaxArtifactJSONNesting)
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var artifact Artifact
	if err := decoder.Decode(&artifact); err != nil {
		return nil, fmt.Errorf("decode Plan artifact: %w", err)
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return nil, fmt.Errorf("decode Plan artifact: multiple JSON values")
		}
		return nil, fmt.Errorf("decode Plan artifact trailing value: %w", err)
	}
	if err := ValidateEnvelope(&artifact); err != nil {
		return nil, err
	}
	return &artifact, nil
}

// artifactJSONNesting is an iterative preflight before encoding/json reflects
// into recursive ArtifactExpression/ArtifactSchema values.
func artifactJSONNesting(raw []byte) (int, error) {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	depth, maximum := 0, 0
	for {
		token, err := decoder.Token()
		if errors.Is(err, io.EOF) {
			return maximum, nil
		}
		if err != nil {
			return maximum, err
		}
		delimiter, ok := token.(json.Delim)
		if !ok {
			continue
		}
		switch delimiter {
		case '{', '[':
			depth++
			maximum = max(maximum, depth)
		case '}', ']':
			if depth > 0 {
				depth--
			}
		}
	}
}

// ValidateEnvelope validates the normalized IR boundary accepted by future
// runtime/store consumers. It intentionally performs no source parsing.
func ValidateEnvelope(artifact *Artifact) error {
	if artifact == nil {
		return fmt.Errorf("Plan artifact is nil")
	}
	switch artifact.ArtifactVersion {
	case ArtifactVersionV1:
		if artifactUsesV5Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v5 Workflow features", ArtifactVersionV1)
		}
		if artifactUsesV4Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v4 Workflow features", ArtifactVersionV1)
		}
		if artifactUsesScopeFeature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support scope values", ArtifactVersionV1)
		}
	case ArtifactVersionV2:
		// Scope values and scope message destinations were added in v2.
		if artifactUsesV5Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v5 Workflow features", ArtifactVersionV2)
		}
		if artifactUsesAgentScopeFeature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support Agent workScope or relatedScopes", ArtifactVersionV2)
		}
		if artifactUsesV4Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v4 Workflow features", ArtifactVersionV2)
		}
	case ArtifactVersionV3:
		// Agent workScope and relatedScopes were added in v3.
		if artifactUsesV5Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v5 Workflow features", ArtifactVersionV3)
		}
		if artifactUsesV4Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v4 Workflow features", ArtifactVersionV3)
		}
	case ArtifactVersionV4:
		if artifactUsesV5Feature(artifact) {
			return fmt.Errorf("artifactVersion %q does not support v5 Workflow features", ArtifactVersionV4)
		}
		if artifact.CompilerVersion != CompilerVersionV4 {
			return fmt.Errorf("artifactVersion %q requires compilerVersion %q", ArtifactVersionV4, CompilerVersionV4)
		}
		if !artifactUsesV4Feature(artifact) {
			return fmt.Errorf("artifactVersion %q requires at least one v4 Workflow feature", ArtifactVersionV4)
		}
	case ArtifactVersionV5:
		if artifact.CompilerVersion != CompilerVersionV5 {
			return fmt.Errorf("artifactVersion %q requires compilerVersion %q", ArtifactVersionV5, CompilerVersionV5)
		}
		if !artifactUsesV5Feature(artifact) {
			return fmt.Errorf("artifactVersion %q requires at least one v5 Workflow feature", ArtifactVersionV5)
		}
	default:
		return fmt.Errorf("unsupported artifactVersion %q", artifact.ArtifactVersion)
	}
	if artifact.APIVersion != APIVersion {
		return fmt.Errorf("artifactVersion %q requires apiVersion %q", artifact.ArtifactVersion, APIVersion)
	}
	if !validCompilerVersion(artifact.CompilerVersion) {
		return fmt.Errorf("invalid compilerVersion %q", artifact.CompilerVersion)
	}
	if artifact.Nodes == nil || artifact.Edges == nil || artifact.Scopes == nil || artifact.Entry.Bindings == nil {
		return fmt.Errorf("artifact collections must be normalized non-null arrays")
	}
	if len(artifact.Nodes) > MaxGraphNodes {
		return fmt.Errorf("artifact must not contain more than %d nodes", MaxGraphNodes)
	}
	if len(artifact.Edges) > MaxGraphEdges {
		return fmt.Errorf("artifact must not contain more than %d edges", MaxGraphEdges)
	}
	for index := range artifact.Nodes {
		config := artifact.Nodes[index].Config.SendMessage
		if config != nil && len(config.RelatedScopes) > MaxSendMessageRelatedScopes {
			return fmt.Errorf(
				"node %q: sendMessage.relatedScopes must not contain more than %d scopes",
				artifact.Nodes[index].ID,
				MaxSendMessageRelatedScopes,
			)
		}
	}
	artifactSchemaFields, artifactSchemaDepth := artifactSchemaBudget(artifact, MaxArtifactSchemaFields+1)
	if artifactSchemaFields > MaxArtifactSchemaFields {
		return fmt.Errorf("artifact must not contain more than %d schema fields", MaxArtifactSchemaFields)
	}
	if artifactSchemaDepth > maxSchemaArrayDepth {
		return fmt.Errorf("artifact schema nesting must not exceed %d", maxSchemaArrayDepth)
	}
	expressionCount, expressionNesting := artifactExpressionBudget(artifact, MaxArtifactExpressions+1)
	if expressionCount > MaxArtifactExpressions {
		return fmt.Errorf("artifact must not contain more than %d expression instructions", MaxArtifactExpressions)
	}
	if expressionNesting > MaxArtifactExprNesting {
		return fmt.Errorf("artifact expression nesting must not exceed %d", MaxArtifactExprNesting)
	}
	if !strings.HasPrefix(artifact.Digest, "sha256:") || !artifact.VerifyDigest() {
		return fmt.Errorf("artifact semantic digest is invalid")
	}
	if err := validateArtifactContract("contracts.input", artifact.Contracts.Input); err != nil {
		return err
	}
	if err := validateArtifactContract("contracts.output", artifact.Contracts.Output); err != nil {
		return err
	}
	if err := validateArtifactContract("contracts.state", artifact.Contracts.State); err != nil {
		return err
	}

	nodes := make(map[string]*ArtifactNode, len(artifact.Nodes))
	lastNode := ""
	for i := range artifact.Nodes {
		node := &artifact.Nodes[i]
		if node.ID == "" || i > 0 && node.ID <= lastNode {
			return fmt.Errorf("nodes must have unique IDs in ascending order")
		}
		lastNode = node.ID
		if !idPattern.MatchString(node.ID) {
			return fmt.Errorf("node ID %q is invalid", node.ID)
		}
		if (artifact.ArtifactVersion == ArtifactVersionV3 || artifact.ArtifactVersion == ArtifactVersionV4) && node.Config.SendMessage != nil &&
			node.Config.SendMessage.WorkScopeMode != "" &&
			artifactContractContainsField(node.Config.SendMessage.CallbackOutput, sendMessageOutputWorkScope) {
			return fmt.Errorf("node %q: sendMessage.callbackOutput field %q is reserved for compiler-owned Agent work-scope output", node.ID, sendMessageOutputWorkScope)
		}
		if artifact.ArtifactVersion == ArtifactVersionV4 && node.Config.SendMessage != nil &&
			artifactContractContainsField(node.Config.SendMessage.CallbackOutput, sendMessageOutputWorkItems) {
			return fmt.Errorf("node %q: sendMessage.callbackOutput field %q is reserved for compiler-owned WorkItem output", node.ID, sendMessageOutputWorkItems)
		}
		if err := validateArtifactNode(node); err != nil {
			return fmt.Errorf("node %q: %w", node.ID, err)
		}
		nodes[node.ID] = node
	}
	entryNode := nodes[artifact.Entry.NodeID]
	if entryNode == nil || !artifactPortExists(entryNode.InputPorts, artifact.Entry.Port) {
		return fmt.Errorf("entry references unknown node/port %q/%q", artifact.Entry.NodeID, artifact.Entry.Port)
	}
	if err := validateArtifactBindings("entry.bindings", artifact.Entry.Bindings); err != nil {
		return err
	}

	lastEdge := ""
	for i := range artifact.Edges {
		edge := &artifact.Edges[i]
		if edge.ID == "" || i > 0 && edge.ID <= lastEdge {
			return fmt.Errorf("edges must have unique IDs in ascending order")
		}
		if !idPattern.MatchString(edge.ID) {
			return fmt.Errorf("edge ID %q is invalid", edge.ID)
		}
		lastEdge = edge.ID
		source, target := nodes[edge.Source.NodeID], nodes[edge.Target.NodeID]
		if source == nil || !artifactPortExists(source.OutputPorts, edge.Source.Port) {
			return fmt.Errorf("edge %q has unknown source node/port", edge.ID)
		}
		if target == nil || !artifactPortExists(target.InputPorts, edge.Target.Port) {
			return fmt.Errorf("edge %q has unknown target node/port", edge.ID)
		}
		if err := validateArtifactBindings("edge "+edge.ID+".bindings", edge.Bindings); err != nil {
			return err
		}
		if edge.ScopeTransitions == nil {
			return fmt.Errorf("edge %q scopeTransitions must be a normalized array", edge.ID)
		}
	}
	edges := make(map[string]*ArtifactEdge, len(artifact.Edges))
	for i := range artifact.Edges {
		edges[artifact.Edges[i].ID] = &artifact.Edges[i]
	}
	if err := validateArtifactImplicitFailures(artifact, nodes, edges); err != nil {
		return err
	}
	if err := validateArtifactNodePorts(artifact, nodes); err != nil {
		return err
	}
	if err := validateArtifactBindingContracts(artifact, nodes); err != nil {
		return err
	}
	if err := validateArtifactScopes(artifact.Scopes, nodes, edges); err != nil {
		return err
	}
	if err := validateArtifactExpressionReferences(artifact, nodes); err != nil {
		return err
	}
	if err := validateArtifactWorkItemsProvenance(artifact, nodes); err != nil {
		return err
	}
	if err := validateArtifactStateEffects(artifact); err != nil {
		return err
	}
	return nil
}

func validCompilerVersion(value string) bool {
	if value == "" || len(value) > 128 {
		return false
	}
	for _, character := range value {
		if character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' ||
			character >= '0' && character <= '9' || character == '.' || character == '_' || character == '-' || character == '/' {
			continue
		}
		return false
	}
	return true
}

// ValidateEnvelope is also available as a method for decoded callers.
func (artifact *Artifact) ValidateEnvelope() error { return ValidateEnvelope(artifact) }

func artifactSchemaRoots(artifact *Artifact) []ArtifactSchema {
	if artifact == nil {
		return nil
	}
	contracts := []ArtifactContract{
		artifact.Contracts.Input,
		artifact.Contracts.Output,
		artifact.Contracts.State,
	}
	for index := range artifact.Nodes {
		node := &artifact.Nodes[index]
		for _, port := range node.InputPorts {
			contracts = append(contracts, port.Contract)
		}
		for _, port := range node.OutputPorts {
			contracts = append(contracts, port.Contract)
		}
		if node.Config.SendMessage != nil {
			contracts = append(contracts, node.Config.SendMessage.CallbackOutput)
		}
		if node.Config.Join != nil {
			contracts = append(contracts, node.Config.Join.Result)
		}
		if node.Config.Loop != nil {
			contracts = append(contracts, node.Config.Loop.State)
		}
	}
	roots := make([]ArtifactSchema, 0)
	for _, contract := range contracts {
		for _, field := range contract {
			roots = append(roots, field.Schema)
		}
	}
	return roots
}

func artifactSchemaBudget(artifact *Artifact, stopAt int) (count int, maximumDepth int) {
	if artifact == nil || stopAt <= 0 {
		return 0, 0
	}
	type pendingSchema struct {
		schema ArtifactSchema
		depth  int
	}
	roots := artifactSchemaRoots(artifact)
	queue := make([]pendingSchema, 0, len(roots))
	for _, schema := range roots {
		queue = append(queue, pendingSchema{schema: schema})
	}
	for len(queue) != 0 {
		last := len(queue) - 1
		item := queue[last]
		queue = queue[:last]
		count++
		maximumDepth = max(maximumDepth, item.depth)
		if count >= stopAt {
			return count, maximumDepth
		}
		if item.schema.Items != nil {
			queue = append(queue, pendingSchema{schema: *item.schema.Items, depth: item.depth + 1})
		}
	}
	return count, maximumDepth
}

func artifactExpressionRoots(artifact *Artifact) []ArtifactExpression {
	if artifact == nil {
		return nil
	}
	roots := make([]ArtifactExpression, 0)
	appendExpression := func(expression ArtifactExpression) {
		roots = append(roots, expression)
	}
	appendBindings := func(bindings []ArtifactBinding) {
		for _, binding := range bindings {
			appendExpression(binding.Value)
		}
	}
	appendTemplate := func(template ArtifactTemplate) {
		for _, segment := range template.Segments {
			if segment.Expression != nil {
				appendExpression(*segment.Expression)
			}
		}
	}
	appendBindings(artifact.Entry.Bindings)
	for index := range artifact.Nodes {
		config := &artifact.Nodes[index].Config
		if config.SendMessage != nil {
			if config.SendMessage.Target != nil {
				appendTemplate(*config.SendMessage.Target)
			}
			if config.SendMessage.Scope != nil {
				appendExpression(*config.SendMessage.Scope)
			}
			if config.SendMessage.Responder != nil {
				appendExpression(*config.SendMessage.Responder)
			}
			if config.SendMessage.SelectOne != nil {
				appendExpression(*config.SendMessage.SelectOne)
			}
			if config.SendMessage.WorkScope != nil {
				appendExpression(*config.SendMessage.WorkScope)
			}
			for _, relatedScope := range config.SendMessage.RelatedScopes {
				appendExpression(relatedScope)
			}
			if config.SendMessage.RelatedScopesArray != nil {
				appendExpression(*config.SendMessage.RelatedScopesArray)
			}
			if config.SendMessage.WorkItems != nil {
				appendExpression(config.SendMessage.WorkItems.AllowedResponders)
			}
			appendTemplate(config.SendMessage.Text)
		}
		if config.SetVariable != nil {
			for _, assignment := range config.SetVariable.Assignments {
				appendExpression(assignment.Value)
			}
		}
		if config.Wait != nil {
			appendExpression(config.Wait.Until)
		}
		if config.RequestApproval != nil {
			appendTemplate(config.RequestApproval.Assignee)
			appendTemplate(config.RequestApproval.Summary)
			if config.RequestApproval.FeedbackScope != nil {
				appendExpression(*config.RequestApproval.FeedbackScope)
			}
		}
		if config.Switch != nil {
			for _, switchCase := range config.Switch.Cases {
				if switchCase.Condition != nil {
					appendExpression(*switchCase.Condition)
				}
			}
		}
		if config.Join != nil {
			appendBindings(config.Join.Merge)
		}
		if config.FanOut != nil {
			appendExpression(config.FanOut.Items)
		}
	}
	for _, edge := range artifact.Edges {
		appendBindings(edge.Bindings)
	}
	return roots
}

func artifactExpressionBudget(artifact *Artifact, stopAt int) (count int, maximumDepth int) {
	if artifact == nil || stopAt <= 0 {
		return 0, 0
	}
	type pendingExpression struct {
		expression ArtifactExpression
		depth      int
	}
	roots := artifactExpressionRoots(artifact)
	queue := make([]pendingExpression, 0, len(roots))
	for _, expression := range roots {
		queue = append(queue, pendingExpression{expression: expression, depth: 1})
	}
	for len(queue) != 0 {
		last := len(queue) - 1
		item := queue[last]
		queue = queue[:last]
		count++
		maximumDepth = max(maximumDepth, item.depth)
		if count >= stopAt {
			return count, maximumDepth
		}
		for _, argument := range item.expression.Args {
			queue = append(queue, pendingExpression{expression: argument, depth: item.depth + 1})
		}
	}
	return count, maximumDepth
}

// artifactUsesScopeFeature detects every scope occurrence before the shared
// v1/v2/v3 structural validators run. Keep this independent from the
// authoring catalog: it protects already-published artifact/v1 envelopes.
func artifactUsesScopeFeature(artifact *Artifact) bool {
	if artifact == nil {
		return false
	}
	for index := range artifact.Nodes {
		if config := artifact.Nodes[index].Config.SendMessage; config != nil &&
			(config.Scope != nil || config.StartThread || config.usesAgentScopeFeature()) {
			return true
		}
	}

	schemas := artifactSchemaRoots(artifact)
	for len(schemas) != 0 {
		last := len(schemas) - 1
		schema := schemas[last]
		schemas = schemas[:last]
		if schema.Type == TypeScope {
			return true
		}
		if schema.Items != nil {
			schemas = append(schemas, *schema.Items)
		}
	}
	expressions := artifactExpressionRoots(artifact)
	for len(expressions) != 0 {
		last := len(expressions) - 1
		expression := expressions[last]
		expressions = expressions[:last]
		if expression.ResultType == TypeScope {
			return true
		}
		expressions = append(expressions, expression.Args...)
	}
	return false
}

func artifactUsesAgentScopeFeature(artifact *Artifact) bool {
	if artifact == nil {
		return false
	}
	for index := range artifact.Nodes {
		config := artifact.Nodes[index].Config.SendMessage
		if config.usesAgentScopeFeature() {
			return true
		}
	}
	return false
}

func artifactUsesV5Feature(artifact *Artifact) bool {
	if artifact == nil {
		return false
	}
	for index := range artifact.Nodes {
		config := artifact.Nodes[index].Config.RequestApproval
		if config != nil && (config.feedbackScopePresent || config.FeedbackScope != nil) {
			return true
		}
	}
	return false
}

func artifactUsesV4Feature(artifact *Artifact) bool {
	if artifact == nil {
		return false
	}
	for index := range artifact.Nodes {
		node := &artifact.Nodes[index]
		if node.Opcode == OpcodeFanOut || node.Config.fanOutPresent || node.Config.FanOut != nil || node.Config.SendMessage.usesV4Feature() {
			return true
		}
		for _, port := range node.InputPorts {
			if artifactContractUsesV4WorkItemVocabulary(port.Contract, true) {
				return true
			}
		}
		for _, port := range node.OutputPorts {
			allowLegacyWorkScope := port.Name == "completed" &&
				(node.Opcode == OpcodeSendMessageSent || node.Opcode == OpcodeSendMessageCallback)
			if artifactContractUsesV4WorkItemVocabulary(port.Contract, !allowLegacyWorkScope) {
				return true
			}
		}
		if node.Config.SendMessage != nil &&
			artifactContractUsesV4WorkItemVocabulary(node.Config.SendMessage.CallbackOutput, false) {
			return true
		}
	}
	for _, contract := range []ArtifactContract{artifact.Contracts.Input, artifact.Contracts.Output, artifact.Contracts.State} {
		if artifactContractUsesV4WorkItemVocabulary(contract, true) {
			return true
		}
	}
	for _, scope := range artifact.Scopes {
		if scope.Kind == ArtifactScopeFanOut || scope.fanOutPresent || scope.FanOut != nil {
			return true
		}
	}
	for _, schema := range artifactSchemaRoots(artifact) {
		for current := &schema; current != nil; current = current.Items {
			if current.Type == TypeWorkItems {
				return true
			}
		}
	}
	for _, root := range artifactExpressionRoots(artifact) {
		pending := []ArtifactExpression{root}
		for len(pending) != 0 {
			last := len(pending) - 1
			expression := pending[last]
			pending = pending[:last]
			if expression.ResultType == TypeWorkItems ||
				expression.Load != nil && (expression.Load.Space == ArtifactLoadFanOutItem || expression.Load.Space == ArtifactLoadJoinItems) {
				return true
			}
			pending = append(pending, expression.Args...)
		}
	}
	return false
}

func artifactContractUsesV4WorkItemVocabulary(contract ArtifactContract, includeWorkScope bool) bool {
	for _, field := range contract {
		if field.Name == sendMessageOutputWorkItems ||
			field.Name == WorkItemFieldDependsOn ||
			field.Name == WorkItemFieldRelatedScopes ||
			includeWorkScope && field.Name == WorkItemFieldWorkScope {
			return true
		}
	}
	return false
}

func validateArtifactContract(path string, contract ArtifactContract) error {
	if contract == nil {
		return fmt.Errorf("%s must be a normalized array", path)
	}
	last := ""
	for i, field := range contract {
		if field.Name == "" || i > 0 && field.Name <= last {
			return fmt.Errorf("%s fields must be unique and ascending", path)
		}
		if !idPattern.MatchString(field.Name) &&
			!(path == "outputPorts.completed" && isSendMessageSystemOutput(field.Name)) &&
			!isWorkItemContractField(field.Name) {
			return fmt.Errorf("%s field %q is invalid", path, field.Name)
		}
		last = field.Name
		if err := validateArtifactSchema(path+"."+field.Name, field.Schema, 0); err != nil {
			return err
		}
	}
	return nil
}

func isSendMessageSystemOutput(name string) bool {
	return name == sendMessageOutputMessageID || name == sendMessageOutputThreadScope ||
		name == sendMessageOutputWorkScope || name == sendMessageOutputWorkItems
}

func artifactContractContainsField(contract ArtifactContract, name string) bool {
	for _, field := range contract {
		if field.Name == name {
			return true
		}
	}
	return false
}

func validateArtifactSchema(path string, schema ArtifactSchema, depth int) error {
	if depth > maxSchemaArrayDepth || !knownArtifactValueType(schema.Type) {
		return fmt.Errorf("%s has unsupported type/depth", path)
	}
	if schema.Values == nil || !strictlySortedUnique(schema.Values) {
		return fmt.Errorf("%s.values must be a normalized sorted array", path)
	}
	if schema.Type == TypeEnum && len(schema.Values) == 0 {
		return fmt.Errorf("%s enum has no values", path)
	}
	if schema.Type != TypeEnum && len(schema.Values) != 0 {
		return fmt.Errorf("%s values require enum", path)
	}
	if schema.Required && schema.Default != nil {
		return fmt.Errorf("%s required must be false when a default is present", path)
	}
	if schema.Type == TypeArray {
		if schema.Items == nil {
			return fmt.Errorf("%s array has no items", path)
		}
		if err := validateArtifactSchema(path+".items", *schema.Items, depth+1); err != nil {
			return err
		}
	} else if schema.Items != nil {
		return fmt.Errorf("%s items require array", path)
	}
	if schema.Default != nil {
		value, decodeErr := decodeArtifactLiteral(*schema.Default)
		normalized, err := compileArtifactLiteral(value, nil)
		if decodeErr != nil || err != nil || !bytes.Equal(normalized, *schema.Default) {
			return fmt.Errorf("%s default is not canonical", path)
		}
		if err := validateArtifactLiteralType(value, schema.Type); err != nil {
			return fmt.Errorf("%s default: %w", path, err)
		}
		if schema.Type == TypeEnum && !contains(schema.Values, value.(string)) {
			return fmt.Errorf("%s default is not an allowed enum value", path)
		}
		if schema.Type == TypeArray {
			if err := validateArtifactArrayLiteral(value, *schema.Items); err != nil {
				return fmt.Errorf("%s default: %w", path, err)
			}
		}
	}
	return nil
}

// knownArtifactValueType is the union understood by all published readers.
// ValidateEnvelope rejects v2-only scope features in v1 before reaching this
// helper, preserving the immutable artifact/v1 contract.
func knownArtifactValueType(value ValueType) bool {
	switch value {
	case TypeChannel, TypeString, TypeEnum, TypeNumber, TypeBoolean, TypeMember,
		TypeDuration, TypeDatetime, TypeObject, TypeArray, TypeAgent, TypeResponder, TypeScope, TypeWorkItems:
		return true
	default:
		return false
	}
}

func knownArtifactExpressionType(value ValueType) bool {
	return value == ArtifactTypeNull || knownArtifactValueType(value)
}

// validateArtifactLiteralType validates the runtime representation, not the
// authoring representation. In particular duration is positive int64
// nanoseconds and datetime is a normalized UTC RFC3339Nano string.
func validateArtifactLiteralType(value any, valueType ValueType) error {
	switch valueType {
	case ArtifactTypeNull:
		if value != nil {
			return fmt.Errorf("literal is not null")
		}
	case TypeChannel, TypeString, TypeEnum, TypeMember, TypeAgent:
		if _, ok := value.(string); !ok {
			return fmt.Errorf("literal is not a %s string", valueType)
		}
	case TypeResponder:
		// The responder union literal is either a fixed agent string or a
		// `{selectOne: [ordered agents]}` pool. Both forms must be structurally
		// valid; the same bounds the runtime normalize enforces.
		if _, ok := value.(string); ok {
			break
		}
		object, ok := value.(map[string]any)
		if !ok {
			return fmt.Errorf("responder literal is not an agent string or selectOne pool")
		}
		raw, present := object["selectOne"]
		if !present || len(object) != 1 {
			return fmt.Errorf("responder literal selectOne is the only permitted pool field")
		}
		values, ok := raw.([]any)
		if !ok {
			return fmt.Errorf("responder.selectOne literal must be an array")
		}
		if len(values) == 0 {
			return fmt.Errorf("responder.selectOne must not be empty")
		}
		if len(values) > MaxAllowedResponders {
			return fmt.Errorf("responder.selectOne must not contain more than %d candidates", MaxAllowedResponders)
		}
		for index, rawCandidate := range values {
			candidate, candidateOK := rawCandidate.(string)
			if !candidateOK || strings.TrimSpace(candidate) == "" {
				return fmt.Errorf("responder.selectOne candidate %d must be a non-blank agent", index)
			}
		}
	case TypeScope:
		if _, err := domain.NormalizeMessageScopeValue(value); err != nil {
			return fmt.Errorf("literal is not a canonical scope value: %w", err)
		}
	case TypeNumber:
		if _, ok := value.(json.Number); !ok {
			return fmt.Errorf("literal is not a number")
		}
	case TypeBoolean:
		if _, ok := value.(bool); !ok {
			return fmt.Errorf("literal is not a boolean")
		}
	case TypeObject:
		if _, ok := value.(map[string]any); !ok {
			return fmt.Errorf("literal is not an object")
		}
	case TypeArray:
		if _, ok := value.([]any); !ok {
			return fmt.Errorf("literal is not an array")
		}
	case TypeDuration:
		number, ok := value.(json.Number)
		if !ok {
			return fmt.Errorf("duration literal is not integer nanoseconds")
		}
		nanos, err := strconv.ParseInt(string(number), 10, 64)
		if err != nil || nanos <= 0 {
			return fmt.Errorf("duration literal is not positive int64 nanoseconds")
		}
	case TypeDatetime:
		text, ok := value.(string)
		if !ok {
			return fmt.Errorf("datetime literal is not a string")
		}
		instant, err := time.Parse(time.RFC3339, text)
		if err != nil || text != instant.UTC().Format(time.RFC3339Nano) {
			return fmt.Errorf("datetime literal is not canonical UTC RFC3339Nano")
		}
	default:
		return fmt.Errorf("literal has unknown result type %q", valueType)
	}
	return nil
}

func validateArtifactArrayLiteral(value any, items ArtifactSchema) error {
	values, ok := value.([]any)
	if !ok {
		return fmt.Errorf("literal is not an array")
	}
	for index, item := range values {
		if err := validateArtifactLiteralType(item, items.Type); err != nil {
			return fmt.Errorf("array item %d: %w", index, err)
		}
		if items.Type == TypeEnum && !contains(items.Values, item.(string)) {
			return fmt.Errorf("array item %d is not an allowed enum value", index)
		}
		if items.Type == TypeArray {
			if items.Items == nil {
				return fmt.Errorf("array item schema is missing nested items")
			}
			if err := validateArtifactArrayLiteral(item, *items.Items); err != nil {
				return fmt.Errorf("array item %d: %w", index, err)
			}
		}
	}
	return nil
}

func validateArtifactNode(node *ArtifactNode) error {
	if node.InputPorts == nil || node.OutputPorts == nil || node.ScopePath == nil || node.StateEffects.Reads == nil || node.StateEffects.Writes == nil {
		return fmt.Errorf("ports, scopePath, and stateEffects must be normalized arrays")
	}
	if err := validateArtifactPorts("inputPorts", node.InputPorts); err != nil {
		return err
	}
	if err := validateArtifactPorts("outputPorts", node.OutputPorts); err != nil {
		return err
	}
	if !strictlySortedUnique(node.StateEffects.Reads) || !strictlySortedUnique(node.StateEffects.Writes) {
		return fmt.Errorf("state effects must be sorted and unique")
	}
	return validateArtifactNodeConfig(node)
}

func validateArtifactPorts(path string, ports []ArtifactPort) error {
	last := ""
	for i, port := range ports {
		if port.Name == "" || i > 0 && port.Name <= last {
			return fmt.Errorf("%s must be sorted and unique", path)
		}
		last = port.Name
		if err := validateArtifactContract(path+"."+port.Name, port.Contract); err != nil {
			return err
		}
	}
	return nil
}

func validateArtifactNodeConfig(node *ArtifactNode) error {
	if node.Config.fanOutPresent && node.Config.FanOut == nil {
		return fmt.Errorf("fanOut config cannot be null in artifact IR")
	}
	configCount := 0
	for _, present := range []bool{
		node.Config.SendMessage != nil, node.Config.SetVariable != nil, node.Config.Wait != nil,
		node.Config.RequestApproval != nil, node.Config.Switch != nil, node.Config.Fork != nil,
		node.Config.FanOut != nil, node.Config.Join != nil, node.Config.Loop != nil, node.Config.End != nil, node.Config.FailPlan != nil,
	} {
		if present {
			configCount++
		}
	}
	if configCount != 1 {
		return fmt.Errorf("config must contain exactly one typed member")
	}
	expected := false
	switch node.Opcode {
	case OpcodeSendMessageSent, OpcodeSendMessageCallback:
		expected = node.Config.SendMessage != nil
		if expected {
			config := node.Config.SendMessage
			hasTarget, hasScope := config.Target != nil, config.Scope != nil
			if hasTarget == hasScope {
				return fmt.Errorf("send_message requires exactly one of target or scope")
			}
			if hasTarget {
				if err := validateArtifactTemplate(*config.Target); err != nil {
					return err
				}
				if err := validateArtifactMessageTargetTemplate(*config.Target); err != nil {
					return err
				}
			}
			if hasScope {
				if err := validateArtifactExpression(*config.Scope); err != nil {
					return err
				}
				if config.Scope.ResultType != TypeScope {
					return fmt.Errorf("send_message scope must resolve to scope")
				}
			}
			if config.Responder != nil {
				if node.Opcode != OpcodeSendMessageCallback {
					return fmt.Errorf("send_message responder requires callback completion")
				}
				if err := validateArtifactExpression(*config.Responder); err != nil {
					return err
				}
				if config.Responder.ResultType != TypeAgent && config.Responder.ResultType != TypeResponder {
					return fmt.Errorf("send_message responder must resolve to agent")
				}
			}
			if config.SelectOne != nil {
				if node.Opcode != OpcodeSendMessageCallback {
					return fmt.Errorf("send_message responder.selectOne requires callback completion")
				}
				if err := validateArtifactSelectOneExpression(*config.SelectOne); err != nil {
					return fmt.Errorf("send_message responder.selectOne: %w", err)
				}
			}
			if err := validateArtifactSendMessageCollaborationScopes(config, node.Opcode); err != nil {
				return err
			}
			if err := validateArtifactTemplate(config.Text); err != nil {
				return err
			}
			if err := validateArtifactContract("sendMessage.callbackOutput", config.CallbackOutput); err != nil {
				return err
			}
			if node.Opcode == OpcodeSendMessageSent && len(config.CallbackOutput) != 0 {
				return fmt.Errorf("send_message.sent cannot declare callbackOutput")
			}
			if node.Opcode == OpcodeSendMessageCallback && len(config.CallbackOutput) == 0 && config.WorkScopeMode == "" && config.WorkItems == nil {
				return fmt.Errorf("send_message.callback requires callbackOutput, workScope, or workItems")
			}
			if config.WorkItems != nil {
				if node.Opcode != OpcodeSendMessageCallback {
					return fmt.Errorf("send_message workItems requires callback completion")
				}
				if err := validateArtifactExpression(config.WorkItems.AllowedResponders); err != nil {
					return err
				}
				if config.WorkItems.AllowedResponders.ResultType != TypeArray {
					return fmt.Errorf("send_message workItems.allowedResponders must resolve to array")
				}
				if config.WorkItems.AllowedResponders.Op == ArtifactOpConst {
					value, err := decodeArtifactLiteral(config.WorkItems.AllowedResponders.Value)
					if err != nil {
						return err
					}
					if values, ok := value.([]any); !ok || len(values) == 0 {
						return fmt.Errorf("send_message workItems.allowedResponders must not be empty")
					}
				}
			}
			if config.StartThread && node.Opcode != OpcodeSendMessageSent {
				return fmt.Errorf("send_message startThread requires sent completion")
			}
			if err := validateArtifactRetry(config.Retry); err != nil {
				return fmt.Errorf("send_message retry: %w", err)
			}
		}
	case OpcodeSetVariable:
		expected = node.Config.SetVariable != nil
		if expected {
			assignments := node.Config.SetVariable.Assignments
			if assignments == nil || len(assignments) == 0 {
				return fmt.Errorf("set_variable assignments must be a non-empty normalized array")
			}
			seen := map[string]bool{}
			for _, assignment := range assignments {
				if !idPattern.MatchString(assignment.State) || seen[assignment.State] {
					return fmt.Errorf("set_variable assignments must name unique valid state fields")
				}
				seen[assignment.State] = true
				if err := validateArtifactExpression(assignment.Value); err != nil {
					return err
				}
			}
		}
	case OpcodeWait:
		expected = node.Config.Wait != nil
		if expected {
			config := node.Config.Wait
			if err := validateArtifactExpression(config.Until); err != nil {
				return err
			}
			if config.Until.ResultType != TypeDuration && config.Until.ResultType != TypeDatetime {
				return fmt.Errorf("wait until must resolve to duration or datetime")
			}
			if config.TimeoutNanos <= 0 {
				return fmt.Errorf("wait timeout must be positive")
			}
			if err := validateArtifactRetry(config.Retry); err != nil {
				return fmt.Errorf("wait retry: %w", err)
			}
		}
	case OpcodeRequestApproval:
		expected = node.Config.RequestApproval != nil
		if expected {
			config := node.Config.RequestApproval
			if err := validateArtifactTemplate(config.Assignee); err != nil {
				return err
			}
			if err := validateArtifactTemplate(config.Summary); err != nil {
				return err
			}
			if config.feedbackScopePresent && config.FeedbackScope == nil {
				return fmt.Errorf("request_approval feedbackScope expression cannot be null in artifact IR")
			}
			if config.FeedbackScope != nil {
				if err := validateArtifactExpression(*config.FeedbackScope); err != nil {
					return fmt.Errorf("request_approval feedbackScope: %w", err)
				}
				if config.FeedbackScope.ResultType != TypeScope {
					return fmt.Errorf("request_approval feedbackScope must resolve to scope")
				}
			}
			if err := validateArtifactRetry(config.Retry); err != nil {
				return fmt.Errorf("request_approval retry: %w", err)
			}
		}
	case OpcodeSwitchFirstMatch, OpcodeSwitchAllMatch:
		expected = node.Config.Switch != nil
		if expected {
			cases := node.Config.Switch.Cases
			if cases == nil || len(cases) < 2 {
				return fmt.Errorf("switch cases must be a normalized array with at least two entries")
			}
			seen, defaults := map[string]bool{}, 0
			lastPort := ""
			for index, switchCase := range cases {
				if !idPattern.MatchString(switchCase.Port) || seen[switchCase.Port] {
					return fmt.Errorf("switch case ports must be valid and unique")
				}
				seen[switchCase.Port] = true
				if node.Opcode == OpcodeSwitchAllMatch && index > 0 && switchCase.Port <= lastPort {
					return fmt.Errorf("switch.all_match cases must be sorted by port")
				}
				lastPort = switchCase.Port
				if switchCase.Default != (switchCase.Condition == nil) {
					return fmt.Errorf("switch case default/condition union is invalid")
				}
				if switchCase.Default {
					defaults++
					if node.Opcode == OpcodeSwitchFirstMatch && index != len(cases)-1 {
						return fmt.Errorf("switch.first_match default must be last")
					}
				}
				if switchCase.Condition != nil {
					if err := validateArtifactExpression(*switchCase.Condition); err != nil {
						return err
					}
					if switchCase.Condition.ResultType != TypeBoolean {
						return fmt.Errorf("switch condition must be boolean")
					}
				}
			}
			if defaults != 1 {
				return fmt.Errorf("switch requires exactly one default case")
			}
		}
	case OpcodeFork:
		expected = node.Config.Fork != nil
		if expected {
			branches := node.Config.Fork.Branches
			if branches == nil || len(branches) < 2 || !strictlySortedUnique(branches) {
				return fmt.Errorf("fork branches must be a sorted unique array with at least two entries")
			}
			for _, branch := range branches {
				if !idPattern.MatchString(branch) {
					return fmt.Errorf("fork branch %q is invalid", branch)
				}
			}
		}
	case OpcodeFanOut:
		expected = node.Config.FanOut != nil
		if expected {
			config := node.Config.FanOut
			if err := validateArtifactExpression(config.Items); err != nil {
				return err
			}
			if config.Items.ResultType != TypeWorkItems {
				return fmt.Errorf("fan_out items must resolve to work_items")
			}
			if config.MaxConcurrency < 1 || config.MaxConcurrency > MaxFanOutConcurrency {
				return fmt.Errorf("fan_out maxConcurrency is invalid")
			}
			if config.FailureMode != FanOutFailureCollect {
				return fmt.Errorf("fan_out failureMode must be collect")
			}
		}
	case OpcodeJoinAll, OpcodeJoinAny:
		expected = node.Config.Join != nil
		if expected {
			config := node.Config.Join
			if !idPattern.MatchString(config.OwnerID) {
				return fmt.Errorf("join owner is invalid")
			}
			if err := validateArtifactContract("join.result", config.Result); err != nil {
				return err
			}
			if err := validateArtifactBindings("join.merge", config.Merge); err != nil {
				return err
			}
			if err := validateArtifactBindingsAgainstContract("join.merge", config.Merge, config.Result); err != nil {
				return err
			}
		}
	case OpcodeLoop:
		expected = node.Config.Loop != nil
		if expected {
			config := node.Config.Loop
			// Artifact loop modes are versioned runtime data, not the current
			// set of modes accepted by the authoring compiler.
			if config.Mode != LoopCounted {
				return fmt.Errorf("loop mode %q is invalid", config.Mode)
			}
			if config.Iterations < 1 || config.Iterations > MaxLoopIterations || len(config.State) == 0 {
				return fmt.Errorf("counted loop iterations/state is invalid")
			}
			if err := validateArtifactContract("loop.state", config.State); err != nil {
				return err
			}
		}
	case OpcodeEndSucceeded, OpcodeEndFailed:
		expected = node.Config.End != nil
		if expected {
			code := node.Config.End.Code
			if node.Opcode == OpcodeEndSucceeded && code != "" {
				return fmt.Errorf("end.succeeded cannot carry an error code")
			}
			if node.Opcode == OpcodeEndFailed && (code == "" || len(code) > 128 || !errorCodePattern.MatchString(code)) {
				return fmt.Errorf("end.failed requires a valid stable error code")
			}
		}
	case OpcodeFailPlan:
		expected = node.Config.FailPlan != nil
		if expected {
			config := node.Config.FailPlan
			sinkID, portOK := implicitFailureSinkID(config.SourcePort)
			code, codeOK := implicitFailureCode(config.SourcePort)
			if !portOK || !codeOK || node.ID != sinkID || config.FallbackCode != code {
				return fmt.Errorf("implicit fail-plan identity/config is not canonical")
			}
		}
	default:
		return fmt.Errorf("unsupported opcode %q", node.Opcode)
	}
	if !expected {
		return fmt.Errorf("opcode %q does not match config", node.Opcode)
	}
	return nil
}

func validateArtifactSendMessageCollaborationScopes(config *ArtifactSendMessageConfig, opcode ArtifactOpcode) error {
	if config == nil {
		return fmt.Errorf("send_message config is nil")
	}
	if config.usesAgentScopeFeature() && opcode != OpcodeSendMessageCallback {
		return fmt.Errorf("send_message workScope and relatedScopes require callback completion")
	}
	if config.workScopeModePresent && config.WorkScopeMode == "" {
		return fmt.Errorf("send_message workScopeMode cannot be empty when present")
	}
	if config.workScopePresent && config.WorkScope == nil {
		return fmt.Errorf("send_message workScope expression cannot be null in artifact IR")
	}
	if config.relatedScopesPresent && config.RelatedScopes == nil {
		return fmt.Errorf("send_message relatedScopes must be a normalized array when present")
	}
	if config.responderPresent && config.Responder == nil {
		return fmt.Errorf("send_message responder expression cannot be null in artifact IR")
	}
	if config.selectOnePresent && config.SelectOne == nil {
		return fmt.Errorf("send_message responder.selectOne expression cannot be null in artifact IR")
	}
	if config.selectOneStrategyPresent {
		if _, err := ParseArtifactSelectOneStrategyPresent(string(config.SelectOneStrategy)); err != nil {
			return err
		}
		if config.SelectOne == nil {
			return fmt.Errorf("send_message selectOneStrategy is only valid with a selectOne pool")
		}
		if config.SelectOneStrategy.Normalized() == ArtifactSelectOneStrategyRandom {
			count, known, err := artifactSelectOneDedupCount(*config.SelectOne)
			if err != nil {
				return err
			}
			if known && count < 2 {
				return fmt.Errorf("send_message selectOne strategy %q requires at least two deduplicated candidates", config.SelectOneStrategy)
			}
		}
	}
	if config.Responder != nil && config.SelectOne != nil {
		return fmt.Errorf("send_message responder and responder.selectOne are mutually exclusive")
	}
	if config.relatedScopesArrayPresent && config.RelatedScopesArray == nil {
		return fmt.Errorf("send_message relatedScopesArray expression cannot be null in artifact IR")
	}
	if config.workItemsPresent && config.WorkItems == nil {
		return fmt.Errorf("send_message workItems cannot be null in artifact IR")
	}
	if config.RelatedScopes != nil && config.RelatedScopesArray != nil {
		return fmt.Errorf("send_message relatedScopes list and array expression are mutually exclusive")
	}
	switch config.WorkScopeMode {
	case "":
		if config.WorkScope != nil {
			return fmt.Errorf("send_message workScope requires a workScopeMode")
		}
		if config.RelatedScopes != nil || config.RelatedScopesArray != nil {
			return fmt.Errorf("send_message relatedScopes requires workScope")
		}
	case ArtifactWorkScopeAgent:
		if config.WorkScope != nil {
			return fmt.Errorf("send_message agent workScopeMode cannot carry a workScope expression")
		}
	case ArtifactWorkScopeFixed:
		if config.WorkScope == nil {
			return fmt.Errorf("send_message fixed workScopeMode requires a workScope expression")
		}
		if err := validateArtifactExpression(*config.WorkScope); err != nil {
			return err
		}
		if config.WorkScope.ResultType != TypeScope {
			return fmt.Errorf("send_message workScope must resolve to scope")
		}
	default:
		return fmt.Errorf("send_message has unsupported workScopeMode %q", config.WorkScopeMode)
	}
	for index, relatedScope := range config.RelatedScopes {
		if err := validateArtifactExpression(relatedScope); err != nil {
			return fmt.Errorf("send_message relatedScopes[%d]: %w", index, err)
		}
		if relatedScope.ResultType != TypeScope {
			return fmt.Errorf("send_message relatedScopes[%d] must resolve to scope", index)
		}
	}
	if config.RelatedScopesArray != nil {
		if err := validateArtifactExpression(*config.RelatedScopesArray); err != nil {
			return fmt.Errorf("send_message relatedScopesArray: %w", err)
		}
		if config.RelatedScopesArray.ResultType != TypeArray {
			return fmt.Errorf("send_message relatedScopesArray must resolve to array")
		}
	}
	return nil
}

// validateArtifactSelectOneExpression validates the ordered candidate pool
// expression declared by `sendMessage.responder.selectOne`. The pool must
// resolve to a required array<agent> and, when it is a statically known const
// array, must be non-empty and within MaxAllowedResponders. A dynamic pool is
// validated against the same bound by runtime normalize, so a decoded ref
// cannot bypass the upper bound that an authored literal enforces. Old
// fixed-responder artifacts (Responder only, no SelectOne) decode unchanged.
func validateArtifactSelectOneExpression(expression ArtifactExpression) error {
	if err := validateArtifactExpression(expression); err != nil {
		return err
	}
	if expression.ResultType != TypeArray {
		return fmt.Errorf("responder.selectOne must resolve to array<agent>")
	}
	if expression.Op != ArtifactOpConst {
		return nil
	}
	value, err := decodeArtifactLiteral(expression.Value)
	if err != nil {
		return fmt.Errorf("responder.selectOne literal: %w", err)
	}
	values, ok := value.([]any)
	if !ok {
		return fmt.Errorf("responder.selectOne literal must be an array")
	}
	if len(values) == 0 {
		return fmt.Errorf("responder.selectOne must not be empty")
	}
	if len(values) > MaxAllowedResponders {
		return fmt.Errorf("responder.selectOne must not contain more than %d candidates", MaxAllowedResponders)
	}
	for index, raw := range values {
		agent, ok := raw.(string)
		if !ok || strings.TrimSpace(agent) == "" {
			return fmt.Errorf("responder.selectOne candidate %d must be a non-blank agent", index)
		}
	}
	return nil
}

// artifactSelectOneDedupCount returns the number of deduplicated non-blank
// candidates of a statically-known (const) selectOne pool, or known=false when
// the pool is dynamic (runtime normalize enforces the same random lower bound).
func artifactSelectOneDedupCount(expression ArtifactExpression) (count int, known bool, err error) {
	if expression.Op != ArtifactOpConst {
		return 0, false, nil
	}
	value, err := decodeArtifactLiteral(expression.Value)
	if err != nil {
		return 0, true, fmt.Errorf("responder.selectOne literal: %w", err)
	}
	values, ok := value.([]any)
	if !ok {
		return 0, true, fmt.Errorf("responder.selectOne literal must be an array")
	}
	return len(uniqueNonBlankCandidates(values)), true, nil
}

func validateArtifactRetry(retry ArtifactRetryPolicy) error {
	if retry.MaxAttempts < 1 {
		return fmt.Errorf("maxAttempts must be at least 1")
	}
	if retry.MaxAttempts > MaxRetryAttempts {
		return fmt.Errorf("maxAttempts must not exceed %d", MaxRetryAttempts)
	}
	if retry.BackoffNanos <= 0 {
		return fmt.Errorf("backoffNanos must be positive")
	}
	return nil
}

func validateArtifactBindings(path string, bindings []ArtifactBinding) error {
	if bindings == nil {
		return fmt.Errorf("%s must be a normalized array", path)
	}
	last := ""
	for i, binding := range bindings {
		if binding.Target == "" || i > 0 && binding.Target <= last {
			return fmt.Errorf("%s must be sorted and unique", path)
		}
		last = binding.Target
		if err := validateArtifactExpression(binding.Value); err != nil {
			return fmt.Errorf("%s.%s: %w", path, binding.Target, err)
		}
	}
	return nil
}

func validateArtifactBindingsAgainstContract(path string, bindings []ArtifactBinding, contract ArtifactContract) error {
	fields := make(map[string]ArtifactSchema, len(contract))
	for _, field := range contract {
		fields[field.Name] = field.Schema
	}
	bound := make(map[string]bool, len(bindings))
	for _, binding := range bindings {
		schema, ok := fields[binding.Target]
		if !ok {
			return fmt.Errorf("%s targets undeclared field %q", path, binding.Target)
		}
		bound[binding.Target] = true
		if !artifactExpressionAssignableToType(binding.Value.ResultType, schema.Type) {
			return fmt.Errorf("%s.%s resultType %q is not assignable to %q", path, binding.Target, binding.Value.ResultType, schema.Type)
		}
		if binding.Value.Op == ArtifactOpConst {
			value, err := decodeArtifactLiteral(binding.Value.Value)
			if err != nil {
				return fmt.Errorf("%s.%s: %w", path, binding.Target, err)
			}
			if schema.Type == TypeEnum && !contains(schema.Values, value.(string)) {
				return fmt.Errorf("%s.%s is not an allowed enum value", path, binding.Target)
			}
			if schema.Type == TypeArray {
				if schema.Items == nil {
					return fmt.Errorf("%s.%s array schema has no items", path, binding.Target)
				}
				if err := validateArtifactArrayLiteral(value, *schema.Items); err != nil {
					return fmt.Errorf("%s.%s: %w", path, binding.Target, err)
				}
			}
		}
	}
	for _, field := range contract {
		if field.Schema.Required && !bound[field.Name] {
			return fmt.Errorf("%s omits required field %q", path, field.Name)
		}
	}
	return nil
}

func artifactExpressionAssignableToType(source, target ValueType) bool {
	return source == target || source == TypeEnum && target == TypeString
}

func validateArtifactExpression(expression ArtifactExpression) error {
	if !knownArtifactExpressionType(expression.ResultType) {
		return fmt.Errorf("expression resultType %q is invalid", expression.ResultType)
	}
	arity := len(expression.Args)
	switch expression.Op {
	case ArtifactOpConst:
		if len(expression.Value) == 0 || expression.Load != nil || arity != 0 {
			return fmt.Errorf("const expression union is invalid")
		}
		value, decodeErr := decodeArtifactLiteral(expression.Value)
		normalized, normalizedErr := compileArtifactLiteral(value, nil)
		if decodeErr != nil || normalizedErr != nil || !bytes.Equal(normalized, expression.Value) {
			return fmt.Errorf("const expression literal is not canonical")
		}
		if err := validateArtifactLiteralType(value, expression.ResultType); err != nil {
			return fmt.Errorf("const expression: %w", err)
		}
		if expression.ResultType == TypeArray {
			if _, ok := inferArtifactLiteralSchema(value, TypeArray); !ok {
				return fmt.Errorf("const array expression elements must be homogeneous")
			}
		}
	case ArtifactOpLoad:
		if expression.Load == nil || len(expression.Value) != 0 || arity != 0 {
			return fmt.Errorf("load expression union is invalid")
		}
		if expression.ResultType == ArtifactTypeNull {
			return fmt.Errorf("load expression cannot have null resultType")
		}
		if err := validateArtifactLoad(*expression.Load); err != nil {
			return err
		}
	case ArtifactOpArray:
		if expression.ResultType != TypeArray || expression.Load != nil || len(expression.Value) != 0 || arity == 0 {
			return fmt.Errorf("array expression union is invalid")
		}
		for index := 1; index < len(expression.Args); index++ {
			if !artifactExpressionTypesComparable(expression.Args[0].ResultType, expression.Args[index].ResultType) {
				return fmt.Errorf("array expression elements must have compatible types")
			}
		}
	case ArtifactOpNot:
		if arity != 1 || expression.Load != nil || len(expression.Value) != 0 {
			return fmt.Errorf("unary expression arity is invalid")
		}
		if expression.ResultType != TypeBoolean || expression.Args[0].ResultType != TypeBoolean {
			return fmt.Errorf("not expression types are invalid")
		}
	case ArtifactOpLength:
		if arity != 1 || expression.Load != nil || len(expression.Value) != 0 || expression.ResultType != TypeNumber {
			return fmt.Errorf("length expression arity/type is invalid")
		}
		if !artifactStringLikeType(expression.Args[0].ResultType) && expression.Args[0].ResultType != TypeArray {
			return fmt.Errorf("length expression requires a string-backed or array operand")
		}
	case ArtifactOpAnd, ArtifactOpOr:
		if arity != 2 || expression.Load != nil || len(expression.Value) != 0 || expression.ResultType != TypeBoolean ||
			expression.Args[0].ResultType != TypeBoolean || expression.Args[1].ResultType != TypeBoolean {
			return fmt.Errorf("logical expression arity/type is invalid")
		}
	case ArtifactOpEqual, ArtifactOpNotEqual:
		if arity != 2 || expression.Load != nil || len(expression.Value) != 0 || expression.ResultType != TypeBoolean {
			return fmt.Errorf("equality expression arity/type is invalid")
		}
		if !artifactExpressionTypesComparable(expression.Args[0].ResultType, expression.Args[1].ResultType) {
			return fmt.Errorf("equality expression operand types are incompatible")
		}
	case ArtifactOpLess, ArtifactOpLessEqual, ArtifactOpGreater, ArtifactOpGreaterEq:
		if arity != 2 || expression.Load != nil || len(expression.Value) != 0 || expression.ResultType != TypeBoolean ||
			expression.Args[0].ResultType != TypeNumber || expression.Args[1].ResultType != TypeNumber {
			return fmt.Errorf("relational expression arity/type is invalid")
		}
	case ArtifactOpIn:
		if arity != 2 || expression.Load != nil || len(expression.Value) != 0 || expression.ResultType != TypeBoolean || expression.Args[1].ResultType != TypeArray {
			return fmt.Errorf("in expression arity/type is invalid")
		}
		if itemType, ok := artifactExpressionArrayItemType(expression.Args[1]); ok && !artifactExpressionTypesComparable(expression.Args[0].ResultType, itemType) {
			return fmt.Errorf("in expression item type is incompatible with its array")
		}
	case ArtifactOpContains:
		if arity != 2 || expression.Load != nil || len(expression.Value) != 0 {
			return fmt.Errorf("binary expression arity is invalid")
		}
		if expression.ResultType != TypeBoolean {
			return fmt.Errorf("function expression resultType is invalid")
		}
		switch expression.Args[0].ResultType {
		case TypeArray:
			if itemType, ok := artifactExpressionArrayItemType(expression.Args[0]); ok && !artifactExpressionTypesComparable(expression.Args[1].ResultType, itemType) {
				return fmt.Errorf("contains needle type is incompatible with array items")
			}
		default:
			if !artifactStringLikeType(expression.Args[0].ResultType) || !artifactStringLikeType(expression.Args[1].ResultType) {
				return fmt.Errorf("contains string form requires string-backed operands")
			}
		}
	case ArtifactOpStartsWith, ArtifactOpEndsWith:
		if arity != 2 || expression.Load != nil || len(expression.Value) != 0 || expression.ResultType != TypeBoolean ||
			!artifactStringLikeType(expression.Args[0].ResultType) || !artifactStringLikeType(expression.Args[1].ResultType) {
			return fmt.Errorf("prefix/suffix expression requires two string-backed operands")
		}
	default:
		return fmt.Errorf("unknown expression opcode %q", expression.Op)
	}
	for _, argument := range expression.Args {
		if err := validateArtifactExpression(argument); err != nil {
			return err
		}
	}
	return nil
}

func artifactExpressionTypesComparable(left, right ValueType) bool {
	if left == ArtifactTypeNull || right == ArtifactTypeNull || left == right {
		return true
	}
	return left == TypeEnum && right == TypeString || left == TypeString && right == TypeEnum
}

func artifactStringLikeType(value ValueType) bool {
	switch value {
	case TypeChannel, TypeString, TypeEnum, TypeMember, TypeDuration, TypeDatetime, TypeAgent:
		return true
	default:
		return false
	}
}

func artifactExpressionArrayItemType(expression ArtifactExpression) (ValueType, bool) {
	switch expression.Op {
	case ArtifactOpArray:
		if len(expression.Args) == 0 {
			return "", false
		}
		itemType := expression.Args[0].ResultType
		for _, argument := range expression.Args[1:] {
			if !artifactExpressionTypesComparable(itemType, argument.ResultType) {
				return "", false
			}
			if itemType == TypeEnum && argument.ResultType == TypeString || itemType == TypeString && argument.ResultType == TypeEnum {
				itemType = TypeString
			}
		}
		return itemType, true
	case ArtifactOpConst:
		value, err := decodeArtifactLiteral(expression.Value)
		if err != nil {
			return "", false
		}
		values, ok := value.([]any)
		if !ok || len(values) == 0 {
			return "", false
		}
		itemType, ok := artifactLiteralValueType(values[0])
		if !ok {
			return "", false
		}
		for _, item := range values[1:] {
			other, known := artifactLiteralValueType(item)
			if !known || !artifactExpressionTypesComparable(itemType, other) {
				return "", false
			}
			if itemType == TypeEnum && other == TypeString || itemType == TypeString && other == TypeEnum {
				itemType = TypeString
			}
		}
		return itemType, true
	default:
		return "", false
	}
}

func artifactLiteralValueType(value any) (ValueType, bool) {
	switch value.(type) {
	case string:
		return TypeEnum, true
	case json.Number:
		return TypeNumber, true
	case bool:
		return TypeBoolean, true
	case map[string]any:
		return TypeObject, true
	case []any:
		return TypeArray, true
	case nil:
		return ArtifactTypeNull, true
	default:
		return "", false
	}
}

func validateArtifactLoad(load ArtifactLoad) error {
	if load.Path == nil {
		return fmt.Errorf("load path must be a normalized array")
	}
	switch load.Space {
	case ArtifactLoadPlanInput, ArtifactLoadActivationInput, ArtifactLoadActivationOutput, ArtifactLoadRunState:
		if load.ScopeOwnerID != "" || load.Branch != "" || len(load.Path) != 1 || load.Path[0] == "" {
			return fmt.Errorf("field load %q has invalid scope/path", load.Space)
		}
	case ArtifactLoadLoopState:
		if load.ScopeOwnerID == "" || load.Branch != "" || len(load.Path) != 1 || load.Path[0] == "" {
			return fmt.Errorf("loop load %q has invalid owner/path", load.Space)
		}
	case ArtifactLoadLoopIteration, ArtifactLoadLoopIsLast:
		if load.ScopeOwnerID == "" || load.Branch != "" || len(load.Path) != 0 {
			return fmt.Errorf("loop metadata load %q has invalid owner/path", load.Space)
		}
	case ArtifactLoadJoinBranch:
		if load.ScopeOwnerID == "" || len(load.Path) > 1 || len(load.Path) == 1 && (load.Branch == "" || load.Path[0] == "") || load.Branch == "" && len(load.Path) != 0 {
			return fmt.Errorf("join branch load has invalid owner/branch/path")
		}
	case ArtifactLoadJoinWinnerBranch:
		if load.ScopeOwnerID == "" || load.Branch != "" || len(load.Path) != 0 {
			return fmt.Errorf("join winner branch load has invalid owner/path")
		}
	case ArtifactLoadJoinWinnerValue:
		if load.ScopeOwnerID == "" || load.Branch != "" || len(load.Path) > 1 || len(load.Path) == 1 && load.Path[0] == "" {
			return fmt.Errorf("join winner value load has invalid owner/path")
		}
	case ArtifactLoadFanOutItem:
		if load.ScopeOwnerID == "" || load.Branch != "" || len(load.Path) != 1 || load.Path[0] == "" {
			return fmt.Errorf("fan_out item load has invalid owner/path")
		}
	case ArtifactLoadJoinItems:
		if load.ScopeOwnerID == "" || load.Branch != "" || len(load.Path) != 0 {
			return fmt.Errorf("fan_out join items load has invalid owner/path")
		}
	default:
		return fmt.Errorf("unknown load space %q", load.Space)
	}
	return nil
}

func validateArtifactTemplate(template ArtifactTemplate) error {
	if template.Segments == nil || len(template.Segments) == 0 {
		return fmt.Errorf("template segments must be a non-empty normalized array")
	}
	for index, segment := range template.Segments {
		switch segment.Kind {
		case ArtifactTemplateSegmentText:
			if segment.Expression != nil || segment.Text == "" {
				return fmt.Errorf("text template segment union is invalid")
			}
			if index > 0 && template.Segments[index-1].Kind == ArtifactTemplateSegmentText {
				return fmt.Errorf("adjacent text template segments are not canonical")
			}
		case ArtifactTemplateSegmentExpr:
			if segment.Expression == nil || segment.Text != "" {
				return fmt.Errorf("expression template segment union is invalid")
			}
			if err := validateArtifactExpression(*segment.Expression); err != nil {
				return err
			}
		default:
			return fmt.Errorf("unknown template segment kind %q", segment.Kind)
		}
	}
	if len(template.Segments) == 1 && template.Segments[0].Kind == ArtifactTemplateSegmentText && strings.TrimSpace(template.Segments[0].Text) == "" {
		return fmt.Errorf("template cannot be blank")
	}
	return nil
}

func validateArtifactMessageTargetTemplate(template ArtifactTemplate) error {
	for _, segment := range template.Segments {
		if segment.Expression != nil && segment.Expression.ResultType == TypeScope {
			return fmt.Errorf("send_message target template cannot interpolate a scope value")
		}
	}
	return nil
}

func validateArtifactNodePorts(artifact *Artifact, nodes map[string]*ArtifactNode) error {
	for i := range artifact.Nodes {
		node := &artifact.Nodes[i]
		if err := requireArtifactPortNames(node.InputPorts, artifactExpectedInputPortNames(node)); err != nil {
			return fmt.Errorf("node %q input ports: %w", node.ID, err)
		}
		if err := requireArtifactPortNames(node.OutputPorts, artifactExpectedOutputPortNames(node)); err != nil {
			return fmt.Errorf("node %q output ports: %w", node.ID, err)
		}
		switch node.Opcode {
		case OpcodeSendMessageSent, OpcodeSendMessageCallback:
			completed := artifactPortContract(node.OutputPorts, "completed")
			expected := ArtifactContract{}
			if node.Opcode == OpcodeSendMessageCallback {
				expected = append(expected, node.Config.SendMessage.CallbackOutput...)
			}
			if node.Config.SendMessage.WorkScopeMode != "" {
				expected = append(expected, ArtifactField{
					Name:   sendMessageOutputWorkScope,
					Schema: ArtifactSchema{Type: TypeScope, Required: true, Values: []string{}},
				})
			}
			if node.Config.SendMessage.WorkItems != nil {
				expected = append(expected, ArtifactField{
					Name:   sendMessageOutputWorkItems,
					Schema: ArtifactSchema{Type: TypeWorkItems, Required: true, Values: []string{}},
				})
			}
			if node.Config.SendMessage.StartThread {
				expected = append(expected,
					ArtifactField{Name: sendMessageOutputMessageID, Schema: ArtifactSchema{Type: TypeString, Required: true, Values: []string{}}},
					ArtifactField{Name: sendMessageOutputThreadScope, Schema: ArtifactSchema{Type: TypeScope, Required: true, Values: []string{}}},
				)
			}
			sort.Slice(expected, func(i, j int) bool { return expected[i].Name < expected[j].Name })
			if !reflect.DeepEqual(completed, expected) {
				return fmt.Errorf("node %q completed port does not match callback/system output", node.ID)
			}
		case OpcodeSwitchFirstMatch, OpcodeSwitchAllMatch, OpcodeFork:
			input := artifactPortContract(node.InputPorts, "in")
			for _, port := range node.OutputPorts {
				if !reflect.DeepEqual(port.Contract, input) {
					return fmt.Errorf("node %q structural output %q does not preserve input contract", node.ID, port.Name)
				}
			}
		case OpcodeFanOut:
			expected, err := lowerArtifactContract(FanOutItemContract())
			if err != nil {
				return fmt.Errorf("derive fan_out item contract: %w", err)
			}
			if !reflect.DeepEqual(artifactPortContract(node.OutputPorts, "item"), expected) {
				return fmt.Errorf("node %q item port does not match the closed fan_out item contract", node.ID)
			}
		case OpcodeJoinAll, OpcodeJoinAny:
			for index := 1; index < len(node.InputPorts); index++ {
				if !reflect.DeepEqual(node.InputPorts[index].Contract, node.InputPorts[0].Contract) {
					return fmt.Errorf("node %q join branch contracts differ", node.ID)
				}
			}
			if !reflect.DeepEqual(artifactPortContract(node.OutputPorts, "completed"), node.Config.Join.Result) {
				return fmt.Errorf("node %q completed port does not match join result", node.ID)
			}
		case OpcodeLoop:
			config := node.Config.Loop
			if !reflect.DeepEqual(artifactPortContract(node.InputPorts, "enter"), config.State) ||
				!reflect.DeepEqual(artifactPortContract(node.InputPorts, "continue"), config.State) ||
				!reflect.DeepEqual(artifactPortContract(node.InputPorts, "break"), config.State) ||
				!reflect.DeepEqual(artifactPortContract(node.OutputPorts, "body"), config.State) ||
				!reflect.DeepEqual(artifactPortContract(node.OutputPorts, "done"), config.State) {
				return fmt.Errorf("node %q loop port contracts do not match state", node.ID)
			}
		case OpcodeEndSucceeded:
			if !reflect.DeepEqual(artifactPortContract(node.InputPorts, "in"), artifact.Contracts.Output) {
				return fmt.Errorf("node %q success input does not match Plan output", node.ID)
			}
		case OpcodeEndFailed, OpcodeFailPlan:
			if len(artifactPortContract(node.InputPorts, "in")) != 0 {
				return fmt.Errorf("node %q failure input must have an empty contract", node.ID)
			}
		case OpcodeSetVariable:
			stateFields := artifactContractSchemas(artifact.Contracts.State)
			for _, assignment := range node.Config.SetVariable.Assignments {
				schema, ok := stateFields[assignment.State]
				if !ok || !artifactExpressionAssignableToType(assignment.Value.ResultType, schema.Type) {
					return fmt.Errorf("node %q assignment %q does not match declared Run state", node.ID, assignment.State)
				}
				if err := validateArtifactConstAgainstSchema("node "+node.ID+" assignment "+assignment.State, assignment.Value, schema); err != nil {
					return err
				}
			}
		}
		for _, port := range node.OutputPorts {
			if port.Name != "completed" || node.Opcode != OpcodeSendMessageCallback && node.Opcode != OpcodeJoinAll && node.Opcode != OpcodeJoinAny && node.Opcode != OpcodeLoop && node.Opcode != OpcodeSwitchFirstMatch && node.Opcode != OpcodeSwitchAllMatch && node.Opcode != OpcodeFork {
				if artifactOutputPortMustBeEmpty(node, port.Name) && len(port.Contract) != 0 {
					return fmt.Errorf("node %q output %q must have an empty contract", node.ID, port.Name)
				}
			}
		}
	}
	_ = nodes
	return nil
}

func artifactExpectedInputPortNames(node *ArtifactNode) []string {
	switch node.Opcode {
	case OpcodeLoop:
		return []string{"break", "continue", "enter"}
	case OpcodeJoinAll, OpcodeJoinAny:
		names := make([]string, len(node.InputPorts))
		for i := range node.InputPorts {
			names[i] = node.InputPorts[i].Name
		}
		return names
	default:
		return []string{"in"}
	}
}

func artifactExpectedOutputPortNames(node *ArtifactNode) []string {
	switch node.Opcode {
	case OpcodeSendMessageSent, OpcodeSendMessageCallback:
		return []string{"completed", "failed"}
	case OpcodeWait:
		return []string{"completed", "failed", "timed_out"}
	case OpcodeSetVariable:
		return []string{"completed", "failed"}
	case OpcodeRequestApproval:
		return []string{"approved", "failed", "rejected"}
	case OpcodeSwitchFirstMatch, OpcodeSwitchAllMatch:
		ports := make([]string, len(node.Config.Switch.Cases))
		for i := range node.Config.Switch.Cases {
			ports[i] = node.Config.Switch.Cases[i].Port
		}
		sort.Strings(ports)
		return ports
	case OpcodeFork:
		return append([]string{}, node.Config.Fork.Branches...)
	case OpcodeFanOut:
		return []string{"item"}
	case OpcodeJoinAll, OpcodeJoinAny:
		return []string{"completed", "failed"}
	case OpcodeLoop:
		return []string{"body", "done", "failed"}
	case OpcodeEndSucceeded, OpcodeEndFailed:
		return []string{}
	case OpcodeFailPlan:
		return []string{}
	default:
		return nil
	}
}

func requireArtifactPortNames(ports []ArtifactPort, expected []string) error {
	if len(ports) != len(expected) {
		return fmt.Errorf("got %d ports, want %v", len(ports), expected)
	}
	for index := range ports {
		if ports[index].Name != expected[index] {
			return fmt.Errorf("got port %q at %d, want %q", ports[index].Name, index, expected[index])
		}
	}
	return nil
}

func artifactPortContract(ports []ArtifactPort, name string) ArtifactContract {
	for _, port := range ports {
		if port.Name == name {
			return port.Contract
		}
	}
	return nil
}

func artifactContractSchemas(contract ArtifactContract) map[string]ArtifactSchema {
	result := make(map[string]ArtifactSchema, len(contract))
	for _, field := range contract {
		result[field.Name] = field.Schema
	}
	return result
}

func artifactOutputPortMustBeEmpty(node *ArtifactNode, port string) bool {
	switch node.Opcode {
	case OpcodeSendMessageSent:
		return port != "completed" || !node.Config.SendMessage.StartThread
	case OpcodeSendMessageCallback:
		return port != "completed"
	case OpcodeJoinAll, OpcodeJoinAny:
		return port != "completed"
	case OpcodeLoop:
		return port == "failed"
	case OpcodeSwitchFirstMatch, OpcodeSwitchAllMatch, OpcodeFork, OpcodeFanOut:
		return false
	default:
		return true
	}
}

func validateArtifactConstAgainstSchema(path string, expression ArtifactExpression, schema ArtifactSchema) error {
	if expression.Op != ArtifactOpConst {
		return nil
	}
	value, err := decodeArtifactLiteral(expression.Value)
	if err != nil {
		return fmt.Errorf("%s: %w", path, err)
	}
	if schema.Type == TypeEnum && !contains(schema.Values, value.(string)) {
		return fmt.Errorf("%s is not an allowed enum value", path)
	}
	if schema.Type == TypeArray {
		if schema.Items == nil {
			return fmt.Errorf("%s array schema has no items", path)
		}
		if err := validateArtifactArrayLiteral(value, *schema.Items); err != nil {
			return fmt.Errorf("%s: %w", path, err)
		}
	}
	return nil
}

func validateArtifactBindingContracts(artifact *Artifact, nodes map[string]*ArtifactNode) error {
	entry := nodes[artifact.Entry.NodeID]
	if err := validateArtifactBindingsAgainstContract("entry.bindings", artifact.Entry.Bindings, artifactPortContract(entry.InputPorts, artifact.Entry.Port)); err != nil {
		return err
	}
	outgoing := map[string]map[string]int{}
	for i := range artifact.Edges {
		edge := &artifact.Edges[i]
		target := nodes[edge.Target.NodeID]
		if err := validateArtifactBindingsAgainstContract("edge "+edge.ID+".bindings", edge.Bindings, artifactPortContract(target.InputPorts, edge.Target.Port)); err != nil {
			return err
		}
		if outgoing[edge.Source.NodeID] == nil {
			outgoing[edge.Source.NodeID] = map[string]int{}
		}
		outgoing[edge.Source.NodeID][edge.Source.Port]++
	}
	for _, node := range artifact.Nodes {
		for _, port := range node.OutputPorts {
			if outgoing[node.ID][port.Name] != 1 {
				return fmt.Errorf("node %q output %q must have exactly one edge", node.ID, port.Name)
			}
		}
	}
	return nil
}

func validateArtifactImplicitFailures(artifact *Artifact, nodes map[string]*ArtifactNode, edges map[string]*ArtifactEdge) error {
	incoming := map[string]int{}
	if entry := nodes[artifact.Entry.NodeID]; entry != nil && entry.Opcode == OpcodeFailPlan {
		return fmt.Errorf("implicit fail-plan cannot be the graph entry")
	}
	for _, node := range nodes {
		if strings.HasPrefix(node.ID, implicitFailureIDPrefix) && node.Opcode != OpcodeFailPlan {
			return fmt.Errorf("node %q uses the compiler-reserved implicit failure prefix", node.ID)
		}
	}
	for _, edge := range edges {
		target := nodes[edge.Target.NodeID]
		isImplicit := target != nil && target.Opcode == OpcodeFailPlan
		if strings.HasPrefix(edge.ID, implicitFailureIDPrefix) != isImplicit {
			return fmt.Errorf("edge %q does not use the compiler-reserved implicit failure identity canonically", edge.ID)
		}
		if !isImplicit {
			continue
		}
		config := target.Config.FailPlan
		if config == nil || edge.Source.Port != config.SourcePort || edge.Target.Port != "in" || len(edge.Bindings) != 0 {
			return fmt.Errorf("edge %q has an invalid implicit failure route", edge.ID)
		}
		if expected := implicitFailureEdgeID(edge.Source.NodeID, edge.Source.Port); edge.ID != expected {
			return fmt.Errorf("edge %q has a non-canonical implicit failure identity", edge.ID)
		}
		incoming[target.ID]++
	}
	for _, node := range nodes {
		if node.Opcode == OpcodeFailPlan && incoming[node.ID] == 0 {
			return fmt.Errorf("implicit fail-plan %q has no incoming route", node.ID)
		}
	}
	return nil
}

type artifactExpressionValidationContext struct {
	input  ArtifactContract
	output ArtifactContract
	loop   *ArtifactNode
	join   *ArtifactNode
	fanOut *ArtifactNode
}

func validateArtifactExpressionReferences(artifact *Artifact, nodes map[string]*ArtifactNode) error {
	planInput := artifactContractSchemas(artifact.Contracts.Input)
	runState := artifactContractSchemas(artifact.Contracts.State)
	validate := func(path string, expression ArtifactExpression, context artifactExpressionValidationContext) error {
		return validateArtifactExpressionReference(path, expression, context, planInput, runState, nodes)
	}
	validateDestination := func(path string, expression ArtifactExpression, context artifactExpressionValidationContext, target ArtifactSchema) error {
		if err := validate(path, expression, context); err != nil {
			return err
		}
		return validateArtifactExpressionDestination(path, expression, context, target, planInput, runState)
	}
	entryContract := artifactContractSchemas(artifactPortContract(nodes[artifact.Entry.NodeID].InputPorts, artifact.Entry.Port))
	for _, binding := range artifact.Entry.Bindings {
		if err := validateDestination("entry.bindings."+binding.Target, binding.Value, artifactExpressionValidationContext{}, entryContract[binding.Target]); err != nil {
			return err
		}
	}
	for i := range artifact.Nodes {
		node := &artifact.Nodes[i]
		context := artifactExpressionValidationContext{input: artifactActivationInputContract(node)}
		config := node.Config
		switch node.Opcode {
		case OpcodeSendMessageSent, OpcodeSendMessageCallback:
			if err := validateArtifactSendMessageExpressionReferences(
				artifact, node, context, validate, validateDestination,
			); err != nil {
				return err
			}
		case OpcodeSetVariable:
			state := artifactContractSchemas(artifact.Contracts.State)
			for _, assignment := range config.SetVariable.Assignments {
				if err := validateDestination("node "+node.ID+".assignment."+assignment.State, assignment.Value, context, state[assignment.State]); err != nil {
					return err
				}
			}
		case OpcodeWait:
			if err := validate("node "+node.ID+".until", config.Wait.Until, context); err != nil {
				return err
			}
		case OpcodeRequestApproval:
			if err := validateArtifactTemplateReferences("node "+node.ID+".assignee", config.RequestApproval.Assignee, context, validate); err != nil {
				return err
			}
			if err := validateArtifactTemplateReferences("node "+node.ID+".summary", config.RequestApproval.Summary, context, validate); err != nil {
				return err
			}
			if config.RequestApproval.FeedbackScope != nil {
				if err := validate("node "+node.ID+".feedbackScope", *config.RequestApproval.FeedbackScope, context); err != nil {
					return err
				}
			}
		case OpcodeSwitchFirstMatch, OpcodeSwitchAllMatch:
			for _, switchCase := range config.Switch.Cases {
				if switchCase.Condition != nil {
					if err := validate("node "+node.ID+".case."+switchCase.Port, *switchCase.Condition, context); err != nil {
						return err
					}
				}
			}
		case OpcodeJoinAll, OpcodeJoinAny:
			context.join = node
			result := artifactContractSchemas(config.Join.Result)
			for _, binding := range config.Join.Merge {
				if err := validateDestination("node "+node.ID+".merge."+binding.Target, binding.Value, context, result[binding.Target]); err != nil {
					return err
				}
			}
		case OpcodeFanOut:
			workItemsTarget := ArtifactSchema{Type: TypeWorkItems, Required: true, Values: []string{}}
			if err := validateDestination("node "+node.ID+".items", config.FanOut.Items, context, workItemsTarget); err != nil {
				return err
			}
		}
	}
	for _, edge := range artifact.Edges {
		source := nodes[edge.Source.NodeID]
		target := nodes[edge.Target.NodeID]
		context := artifactExpressionValidationContext{
			input:  artifactActivationInputContract(source),
			output: artifactPortContract(source.OutputPorts, edge.Source.Port),
		}
		if source.Opcode == OpcodeLoop {
			context.loop = source
		}
		if source.Opcode == OpcodeFanOut && edge.Source.Port == "item" {
			context.fanOut = source
		}
		targetContract := artifactContractSchemas(artifactPortContract(target.InputPorts, edge.Target.Port))
		for _, binding := range edge.Bindings {
			if err := validateDestination("edge "+edge.ID+".bindings."+binding.Target, binding.Value, context, targetContract[binding.Target]); err != nil {
				return err
			}
		}
	}
	return nil
}

func validateArtifactSendMessageExpressionReferences(
	artifact *Artifact,
	node *ArtifactNode,
	context artifactExpressionValidationContext,
	validate func(string, ArtifactExpression, artifactExpressionValidationContext) error,
	validateDestination func(string, ArtifactExpression, artifactExpressionValidationContext, ArtifactSchema) error,
) error {
	config := node.Config.SendMessage
	if config.Target != nil {
		if err := validateArtifactTemplateReferences("node "+node.ID+".target", *config.Target, context, validate); err != nil {
			return err
		}
		if err := validateArtifactMessageTargetTemplateReferences(
			"node "+node.ID+".target",
			*config.Target,
			context,
			artifact.Contracts.Input,
			artifact.Contracts.State,
		); err != nil {
			return err
		}
	}
	scopeTarget := ArtifactSchema{Type: TypeScope, Required: true, Values: []string{}}
	if config.Scope != nil {
		if err := validateDestination("node "+node.ID+".scope", *config.Scope, context, scopeTarget); err != nil {
			return err
		}
	}
	// The send_message responder accepts the fixed-agent | selectOne-pool
	// union: a fixed resolved agent string or a reference that resolves to a
	// `responder` (fan_out item) input carrying either form. The strict
	// TypeAgent check would reject a selectOne-pool ref from a fan_out item
	// body, so the destination is the responder union.
	responderTarget := ArtifactSchema{Type: TypeResponder, Required: true, Values: []string{}}
	if config.Responder != nil {
		if err := validateDestination("node "+node.ID+".responder", *config.Responder, context, responderTarget); err != nil {
			return err
		}
	}
	if config.SelectOne != nil {
		agentItems := ArtifactSchema{Type: TypeAgent, Values: []string{}}
		selectOneTarget := ArtifactSchema{Type: TypeArray, Required: true, Values: []string{}, Items: &agentItems}
		if err := validateDestination("node "+node.ID+".responder.selectOne", *config.SelectOne, context, selectOneTarget); err != nil {
			return err
		}
	}
	if config.WorkScope != nil {
		if err := validateDestination("node "+node.ID+".workScope", *config.WorkScope, context, scopeTarget); err != nil {
			return err
		}
	}
	for index, relatedScope := range config.RelatedScopes {
		if err := validateDestination(
			fmt.Sprintf("node %s.relatedScopes[%d]", node.ID, index),
			relatedScope,
			context,
			scopeTarget,
		); err != nil {
			return err
		}
	}
	if config.RelatedScopesArray != nil {
		scopeItems := ArtifactSchema{Type: TypeScope, Values: []string{}}
		scopesTarget := ArtifactSchema{Type: TypeArray, Required: true, Values: []string{}, Items: &scopeItems}
		if err := validateDestination("node "+node.ID+".relatedScopesArray", *config.RelatedScopesArray, context, scopesTarget); err != nil {
			return err
		}
	}
	if config.WorkItems != nil {
		agentItems := ArtifactSchema{Type: TypeAgent, Values: []string{}}
		agentsTarget := ArtifactSchema{Type: TypeArray, Required: true, Values: []string{}, Items: &agentItems}
		if err := validateDestination("node "+node.ID+".workItems.allowedResponders", config.WorkItems.AllowedResponders, context, agentsTarget); err != nil {
			return err
		}
	}
	return validateArtifactTemplateReferences("node "+node.ID+".text", config.Text, context, validate)
}

func validateArtifactExpressionDestination(
	path string,
	expression ArtifactExpression,
	context artifactExpressionValidationContext,
	target ArtifactSchema,
	planInput, runState map[string]ArtifactSchema,
) error {
	if expression.Op == ArtifactOpConst {
		return validateArtifactConstAgainstSchema(path, expression, target)
	}
	source, ok := resolveArtifactExpressionSchema(expression, context, planInput, runState)
	if !ok || !artifactResolvedSchemaAssignable(source, target) {
		return fmt.Errorf("%s resolved schema %q is not assignable to destination %q", path, source.Type, target.Type)
	}
	if target.Required && expression.Op == ArtifactOpLoad && !artifactSchemaGuaranteed(source) {
		return fmt.Errorf("%s optional load is not assignable to a required destination", path)
	}
	return nil
}

func artifactSchemaGuaranteed(schema ArtifactSchema) bool {
	return schema.Required || schema.Default != nil
}

func artifactActivationInputContract(node *ArtifactNode) ArtifactContract {
	if node == nil || node.Opcode == OpcodeLoop || len(node.InputPorts) == 0 {
		return ArtifactContract{}
	}
	if contract := artifactPortContract(node.InputPorts, "in"); contract != nil {
		return contract
	}
	return node.InputPorts[0].Contract
}

func validateArtifactTemplateReferences(
	path string,
	template ArtifactTemplate,
	context artifactExpressionValidationContext,
	validate func(string, ArtifactExpression, artifactExpressionValidationContext) error,
) error {
	for index, segment := range template.Segments {
		if segment.Expression != nil {
			if err := validate(fmt.Sprintf("%s.segments[%d]", path, index), *segment.Expression, context); err != nil {
				return err
			}
		}
	}
	return nil
}

func validateArtifactMessageTargetTemplateReferences(
	path string,
	template ArtifactTemplate,
	context artifactExpressionValidationContext,
	planInputContract ArtifactContract,
	runStateContract ArtifactContract,
) error {
	planInput := artifactContractSchemas(planInputContract)
	runState := artifactContractSchemas(runStateContract)
	for index, segment := range template.Segments {
		if segment.Expression == nil {
			continue
		}
		schema, ok := resolveArtifactExpressionSchema(
			*segment.Expression,
			context,
			planInput,
			runState,
		)
		if ok && artifactSchemaContainsScope(schema) {
			return fmt.Errorf(
				"%s.segments[%d] cannot interpolate a scope value",
				path,
				index,
			)
		}
	}
	return nil
}

func artifactSchemaContainsScope(schema ArtifactSchema) bool {
	return schema.Type == TypeScope ||
		schema.Type == TypeArray && schema.Items != nil && artifactSchemaContainsScope(*schema.Items)
}

func validateArtifactExpressionReference(
	path string,
	expression ArtifactExpression,
	context artifactExpressionValidationContext,
	planInput, runState map[string]ArtifactSchema,
	nodes map[string]*ArtifactNode,
) error {
	if expression.Op == ArtifactOpLoad && expression.Load != nil {
		expected, ok := resolveArtifactLoadSchema(*expression.Load, context, planInput, runState)
		if !ok || expression.ResultType != expected.Type {
			return fmt.Errorf("%s load %q does not resolve to declared resultType %q", path, expression.Load.Space, expression.ResultType)
		}
		expectedOptional := !artifactSchemaGuaranteed(expected)
		if expression.Load.Optional != expectedOptional {
			return fmt.Errorf("%s load %q has optional=%t, want %t", path, expression.Load.Space, expression.Load.Optional, expectedOptional)
		}
	}
	for index, argument := range expression.Args {
		if err := validateArtifactExpressionReference(fmt.Sprintf("%s.args[%d]", path, index), argument, context, planInput, runState, nodes); err != nil {
			return err
		}
	}
	_ = nodes
	return validateArtifactResolvedOperandTypes(path, expression, context, planInput, runState)
}

func resolveArtifactLoadSchema(
	load ArtifactLoad,
	context artifactExpressionValidationContext,
	planInput, runState map[string]ArtifactSchema,
) (ArtifactSchema, bool) {
	switch load.Space {
	case ArtifactLoadPlanInput:
		return artifactFieldSchema(planInput, load.Path[0])
	case ArtifactLoadRunState:
		return artifactFieldSchema(runState, load.Path[0])
	case ArtifactLoadActivationInput:
		return artifactContractFieldSchema(context.input, load.Path[0])
	case ArtifactLoadActivationOutput:
		return artifactContractFieldSchema(context.output, load.Path[0])
	case ArtifactLoadLoopState:
		if context.loop != nil && load.ScopeOwnerID == context.loop.ID && context.loop.Config.Loop != nil {
			return artifactContractFieldSchema(context.loop.Config.Loop.State, load.Path[0])
		}
	case ArtifactLoadLoopIteration:
		if context.loop != nil && load.ScopeOwnerID == context.loop.ID && context.loop.Config.Loop != nil && context.loop.Config.Loop.Mode == LoopCounted {
			return ArtifactSchema{Type: TypeNumber, Required: true, Values: []string{}}, true
		}
	case ArtifactLoadLoopIsLast:
		if context.loop != nil && load.ScopeOwnerID == context.loop.ID && context.loop.Config.Loop != nil && context.loop.Config.Loop.Mode == LoopCounted {
			return ArtifactSchema{Type: TypeBoolean, Required: true, Values: []string{}}, true
		}
	case ArtifactLoadJoinBranch:
		if context.join != nil && context.join.Opcode == OpcodeJoinAll && load.ScopeOwnerID == context.join.Config.Join.OwnerID {
			switch {
			case load.Branch == "" && len(load.Path) == 0:
				return ArtifactSchema{Type: TypeObject, Required: true}, true
			case load.Branch != "" && artifactPortContract(context.join.InputPorts, load.Branch) != nil && len(load.Path) == 0:
				return ArtifactSchema{Type: TypeObject, Required: true}, true
			case load.Branch != "" && len(load.Path) == 1:
				return artifactContractFieldSchema(artifactPortContract(context.join.InputPorts, load.Branch), load.Path[0])
			}
		}
	case ArtifactLoadJoinWinnerBranch:
		if context.join != nil && context.join.Opcode == OpcodeJoinAny && load.ScopeOwnerID == context.join.Config.Join.OwnerID {
			values := make([]string, 0, len(context.join.InputPorts))
			for _, port := range context.join.InputPorts {
				values = append(values, port.Name)
			}
			sort.Strings(values)
			return ArtifactSchema{Type: TypeEnum, Required: true, Values: values}, len(values) != 0
		}
	case ArtifactLoadJoinWinnerValue:
		if context.join != nil && context.join.Opcode == OpcodeJoinAny && load.ScopeOwnerID == context.join.Config.Join.OwnerID {
			if len(load.Path) == 0 {
				return ArtifactSchema{Type: TypeObject, Required: true}, true
			}
			if len(context.join.InputPorts) != 0 {
				return artifactContractFieldSchema(context.join.InputPorts[0].Contract, load.Path[0])
			}
		}
	case ArtifactLoadFanOutItem:
		if context.fanOut != nil && context.fanOut.Opcode == OpcodeFanOut && load.ScopeOwnerID == context.fanOut.ID {
			return artifactContractFieldSchema(artifactPortContract(context.fanOut.OutputPorts, "item"), load.Path[0])
		}
	case ArtifactLoadJoinItems:
		if context.join != nil && context.join.Opcode == OpcodeJoinAll && load.ScopeOwnerID == context.join.Config.Join.OwnerID &&
			len(context.join.InputPorts) == 1 && context.join.InputPorts[0].Name == "item" {
			item := ArtifactSchema{Type: TypeObject, Values: []string{}}
			return ArtifactSchema{Type: TypeArray, Required: true, Values: []string{}, Items: &item}, true
		}
	}
	return ArtifactSchema{}, false
}

func validateArtifactResolvedOperandTypes(
	path string,
	expression ArtifactExpression,
	context artifactExpressionValidationContext,
	planInput, runState map[string]ArtifactSchema,
) error {
	resolved := func(argument ArtifactExpression) (ArtifactSchema, bool) {
		return resolveArtifactExpressionSchema(argument, context, planInput, runState)
	}
	switch expression.Op {
	case ArtifactOpArray:
		if _, ok := resolveArtifactExpressionSchema(expression, context, planInput, runState); !ok {
			return fmt.Errorf("%s dynamic array elements are not homogeneous", path)
		}
	case ArtifactOpLength:
		argument, ok := resolved(expression.Args[0])
		if !ok || !artifactSchemaStringLike(argument) && argument.Type != TypeArray {
			return fmt.Errorf("%s length requires a string-backed or array operand", path)
		}
	case ArtifactOpStartsWith, ArtifactOpEndsWith:
		left, leftOK := resolved(expression.Args[0])
		right, rightOK := resolved(expression.Args[1])
		if !leftOK || !rightOK || !artifactSchemaStringLike(left) || !artifactSchemaStringLike(right) {
			return fmt.Errorf("%s prefix/suffix function requires string-backed operands", path)
		}
	case ArtifactOpIn:
		item, itemOK := resolved(expression.Args[0])
		array, arrayOK := resolved(expression.Args[1])
		if !itemOK || !arrayOK || array.Type != TypeArray || array.Items != nil && !artifactResolvedSchemasComparable(item, *array.Items) {
			return fmt.Errorf("%s in item is incompatible with array items", path)
		}
	case ArtifactOpContains:
		haystack, haystackOK := resolved(expression.Args[0])
		needle, needleOK := resolved(expression.Args[1])
		if !haystackOK || !needleOK {
			return fmt.Errorf("%s contains operands cannot be resolved", path)
		}
		if haystack.Type == TypeArray {
			if haystack.Items != nil && !artifactResolvedSchemasComparable(needle, *haystack.Items) {
				return fmt.Errorf("%s contains needle is incompatible with array items", path)
			}
		} else if !artifactSchemaStringLike(haystack) || !artifactSchemaStringLike(needle) {
			return fmt.Errorf("%s contains string form requires string-backed operands", path)
		}
	}
	return nil
}

func resolveArtifactExpressionSchema(
	expression ArtifactExpression,
	context artifactExpressionValidationContext,
	planInput, runState map[string]ArtifactSchema,
) (ArtifactSchema, bool) {
	switch expression.Op {
	case ArtifactOpLoad:
		if expression.Load == nil {
			return ArtifactSchema{}, false
		}
		return resolveArtifactLoadSchema(*expression.Load, context, planInput, runState)
	case ArtifactOpConst:
		value, err := decodeArtifactLiteral(expression.Value)
		if err != nil {
			return ArtifactSchema{}, false
		}
		return inferArtifactLiteralSchema(value, expression.ResultType)
	case ArtifactOpArray:
		if len(expression.Args) == 0 {
			return ArtifactSchema{}, false
		}
		item, ok := resolveArtifactExpressionSchema(expression.Args[0], context, planInput, runState)
		if !ok {
			return ArtifactSchema{}, false
		}
		for _, argument := range expression.Args[1:] {
			other, known := resolveArtifactExpressionSchema(argument, context, planInput, runState)
			if !known {
				return ArtifactSchema{}, false
			}
			item, ok = commonArtifactResolvedSchema(item, other)
			if !ok {
				return ArtifactSchema{}, false
			}
		}
		return ArtifactSchema{Type: TypeArray, Values: []string{}, Items: &item}, true
	default:
		return ArtifactSchema{Type: expression.ResultType, Values: []string{}}, true
	}
}

func inferArtifactLiteralSchema(value any, resultType ValueType) (ArtifactSchema, bool) {
	if resultType != TypeArray {
		if text, ok := value.(string); ok && (resultType == TypeString || resultType == TypeEnum) {
			return ArtifactSchema{Type: TypeEnum, Values: []string{text}}, true
		}
		return ArtifactSchema{Type: resultType, Values: []string{}}, true
	}
	values, ok := value.([]any)
	if !ok {
		return ArtifactSchema{}, false
	}
	array := ArtifactSchema{Type: TypeArray, Values: []string{}}
	if len(values) == 0 {
		return array, true
	}
	item, ok := inferArtifactUntypedLiteralSchema(values[0])
	if !ok {
		return ArtifactSchema{}, false
	}
	for _, value := range values[1:] {
		other, known := inferArtifactUntypedLiteralSchema(value)
		if !known {
			return ArtifactSchema{}, false
		}
		item, ok = commonArtifactResolvedSchema(item, other)
		if !ok {
			return ArtifactSchema{}, false
		}
	}
	array.Items = &item
	return array, true
}

func inferArtifactUntypedLiteralSchema(value any) (ArtifactSchema, bool) {
	switch typed := value.(type) {
	case string:
		return ArtifactSchema{Type: TypeEnum, Values: []string{typed}}, true
	case json.Number:
		return ArtifactSchema{Type: TypeNumber, Values: []string{}}, true
	case bool:
		return ArtifactSchema{Type: TypeBoolean, Values: []string{}}, true
	case map[string]any:
		return ArtifactSchema{Type: TypeObject, Values: []string{}}, true
	case []any:
		return inferArtifactLiteralSchema(typed, TypeArray)
	default:
		return ArtifactSchema{}, false
	}
}

func commonArtifactResolvedSchema(left, right ArtifactSchema) (ArtifactSchema, bool) {
	if left.Type == TypeEnum && right.Type == TypeString || left.Type == TypeString && right.Type == TypeEnum {
		return ArtifactSchema{Type: TypeString, Values: []string{}}, true
	}
	if left.Type != right.Type {
		return ArtifactSchema{}, false
	}
	switch left.Type {
	case TypeEnum:
		values := append(append([]string{}, left.Values...), right.Values...)
		sort.Strings(values)
		return ArtifactSchema{Type: TypeEnum, Values: uniqueStrings(values)}, true
	case TypeArray:
		result := ArtifactSchema{Type: TypeArray, Values: []string{}}
		if left.Items == nil || right.Items == nil {
			return result, true
		}
		items, ok := commonArtifactResolvedSchema(*left.Items, *right.Items)
		if !ok {
			return ArtifactSchema{}, false
		}
		result.Items = &items
		return result, true
	default:
		return ArtifactSchema{Type: left.Type, Values: []string{}}, true
	}
}

func artifactResolvedSchemasComparable(left, right ArtifactSchema) bool {
	if left.Type == TypeEnum && right.Type == TypeString || left.Type == TypeString && right.Type == TypeEnum {
		return true
	}
	if left.Type != right.Type {
		return false
	}
	return left.Type != TypeArray || left.Items == nil || right.Items == nil || artifactResolvedSchemasComparable(*left.Items, *right.Items)
}

func artifactResolvedSchemaAssignable(source, target ArtifactSchema) bool {
	if source.Type == TypeEnum && target.Type == TypeString {
		return true
	}
	// a fixed resolved agent is a valid responder; a responder union is never
	// assignable back to a strict agent target (a selectOne map must not be
	// mistaken for a plain agent string).
	if source.Type == TypeAgent && target.Type == TypeResponder {
		return true
	}
	if source.Type != target.Type {
		return false
	}
	switch source.Type {
	case TypeEnum:
		for _, value := range source.Values {
			if !contains(target.Values, value) {
				return false
			}
		}
		return true
	case TypeArray:
		if source.Items == nil {
			return true
		}
		return target.Items != nil && artifactResolvedSchemaAssignable(*source.Items, *target.Items)
	default:
		return true
	}
}

func artifactSchemaStringLike(schema ArtifactSchema) bool { return artifactStringLikeType(schema.Type) }

func artifactFieldType(fields map[string]ArtifactSchema, name string) (ValueType, bool) {
	schema, ok := fields[name]
	return schema.Type, ok
}

func artifactFieldSchema(fields map[string]ArtifactSchema, name string) (ArtifactSchema, bool) {
	schema, ok := fields[name]
	return schema, ok
}

func artifactContractFieldType(contract ArtifactContract, name string) (ValueType, bool) {
	for _, field := range contract {
		if field.Name == name {
			return field.Schema.Type, true
		}
	}
	return "", false
}

func artifactContractFieldSchema(contract ArtifactContract, name string) (ArtifactSchema, bool) {
	for _, field := range contract {
		if field.Name == name {
			return field.Schema, true
		}
	}
	return ArtifactSchema{}, false
}

type artifactScopeKey struct {
	kind    ArtifactScopeKind
	ownerID string
}

func validateArtifactScopes(scopes []ArtifactScope, nodes map[string]*ArtifactNode, edges map[string]*ArtifactEdge) error {
	last := ""
	scopeByKey := make(map[artifactScopeKey]*ArtifactScope, len(scopes))
	frameMembers := map[ArtifactScopeFrame]map[string]bool{}
	expectedTransitions := map[string][]ArtifactScopeTransition{}
	parallelOwners, loopOwners, fanOutOwners, closingJoins := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
	addTransition := func(edgeID string, transition ArtifactScopeTransition) {
		expectedTransitions[edgeID] = append(expectedTransitions[edgeID], transition)
	}

	for index := range scopes {
		scope := &scopes[index]
		if scope.fanOutPresent && scope.FanOut == nil {
			return fmt.Errorf("scope %q fanOut config cannot be null in artifact IR", scope.OwnerID)
		}
		keyText := scope.OwnerID + "\x00" + string(scope.Kind)
		owner := nodes[scope.OwnerID]
		if scope.OwnerID == "" || keyText <= last || owner == nil {
			return fmt.Errorf("scopes must reference owners and be sorted uniquely")
		}
		last = keyText
		if scope.ParentPath == nil {
			return fmt.Errorf("scope %q parentPath must be normalized", scope.OwnerID)
		}
		if (scope.Kind == ArtifactScopeParallel) != (scope.Parallel != nil) ||
			(scope.Kind == ArtifactScopeLoop) != (scope.Loop != nil) ||
			(scope.Kind == ArtifactScopeFanOut) != (scope.FanOut != nil) {
			return fmt.Errorf("scope %q kind/config union is invalid", scope.OwnerID)
		}
		key := artifactScopeKey{kind: scope.Kind, ownerID: scope.OwnerID}
		scopeByKey[key] = scope

		switch scope.Kind {
		case ArtifactScopeParallel:
			if owner.Opcode != OpcodeFork && owner.Opcode != OpcodeSwitchAllMatch {
				return fmt.Errorf("parallel scope %q owner has opcode %q", scope.OwnerID, owner.Opcode)
			}
			parallel := scope.Parallel
			if parallel.ActivationSet != "all" && parallel.ActivationSet != "matched" || parallel.Mode != JoinAll && parallel.Mode != JoinAny {
				return fmt.Errorf("parallel scope %q activation/mode is invalid", scope.OwnerID)
			}
			if owner.Opcode == OpcodeFork && parallel.ActivationSet != "all" || owner.Opcode == OpcodeSwitchAllMatch && parallel.ActivationSet != "matched" {
				return fmt.Errorf("parallel scope %q activationSet does not match owner opcode", scope.OwnerID)
			}
			if parallel.Branches == nil || len(parallel.Branches) < 2 {
				return fmt.Errorf("parallel scope %q branches must be a normalized array", scope.OwnerID)
			}
			join := nodes[parallel.JoinID]
			if join == nil || join.Config.Join == nil || join.Config.Join.OwnerID != scope.OwnerID ||
				parallel.Mode == JoinAll && join.Opcode != OpcodeJoinAll || parallel.Mode == JoinAny && join.Opcode != OpcodeJoinAny {
				return fmt.Errorf("parallel scope %q closing join is inconsistent", scope.OwnerID)
			}
			if closingJoins[parallel.JoinID] {
				return fmt.Errorf("join %q closes more than one parallel scope", parallel.JoinID)
			}
			closingJoins[parallel.JoinID] = true
			parallelOwners[scope.OwnerID] = true

			failedCounts := map[string]int{}
			ports := make([]string, 0, len(parallel.Branches))
			for branchIndex, branch := range parallel.Branches {
				if !idPattern.MatchString(branch.Port) || branchIndex > 0 && branch.Port <= parallel.Branches[branchIndex-1].Port {
					return fmt.Errorf("parallel scope %q branch ports must be valid, sorted, and unique", scope.OwnerID)
				}
				if err := validateArtifactIDList("parallel branch members", branch.Members, nodes, nil); err != nil {
					return err
				}
				if err := validateArtifactIDList("parallel branch entryEdges", branch.EntryEdges, nil, edges); err != nil || len(branch.EntryEdges) == 0 {
					return fmt.Errorf("parallel scope %q branch %q entryEdges: %w", scope.OwnerID, branch.Port, nonNilArtifactError(err, "must not be empty"))
				}
				if err := validateArtifactIDList("parallel branch successArrivals", branch.SuccessArrivals, nil, edges); err != nil || len(branch.SuccessArrivals) == 0 {
					return fmt.Errorf("parallel scope %q branch %q successArrivals: %w", scope.OwnerID, branch.Port, nonNilArtifactError(err, "must not be empty"))
				}
				if err := validateArtifactIDList("parallel branch failedTerminals", branch.FailedTerminals, nodes, nil); err != nil {
					return err
				}
				ports = append(ports, branch.Port)
				for _, nodeID := range branch.Members {
					if nodeID == scope.OwnerID || nodeID == parallel.JoinID {
						return fmt.Errorf("parallel scope %q branch %q contains its boundary node", scope.OwnerID, branch.Port)
					}
				}
				for _, edgeID := range branch.EntryEdges {
					edge := edges[edgeID]
					if edge.Source.NodeID != scope.OwnerID || edge.Source.Port != branch.Port {
						return fmt.Errorf("parallel scope %q entry edge %q has inconsistent source", scope.OwnerID, edgeID)
					}
					addTransition(edgeID, ArtifactScopeTransition{ScopeKind: ArtifactScopeParallel, OwnerID: scope.OwnerID, Action: "enter", Branch: branch.Port})
				}
				for _, edgeID := range branch.SuccessArrivals {
					edge := edges[edgeID]
					if edge.Target.NodeID != parallel.JoinID || edge.Target.Port != branch.Port {
						return fmt.Errorf("parallel scope %q arrival edge %q has inconsistent target", scope.OwnerID, edgeID)
					}
					addTransition(edgeID, ArtifactScopeTransition{ScopeKind: ArtifactScopeParallel, OwnerID: scope.OwnerID, Action: "arrive", Branch: branch.Port})
				}
				for _, terminalID := range branch.FailedTerminals {
					if nodes[terminalID].Opcode != OpcodeEndFailed {
						return fmt.Errorf("parallel scope %q failed terminal %q is not end.failed", scope.OwnerID, terminalID)
					}
					failedCounts[terminalID]++
				}
			}
			ownerPorts := artifactParallelOwnerPorts(owner)
			if !reflect.DeepEqual(ports, ownerPorts) || !reflect.DeepEqual(ports, artifactPortNames(join.InputPorts)) {
				return fmt.Errorf("parallel scope %q branch ports disagree with owner/join", scope.OwnerID)
			}
			for _, branch := range parallel.Branches {
				frame := ArtifactScopeFrame{Kind: ArtifactScopeParallel, OwnerID: scope.OwnerID, Branch: branch.Port}
				members := map[string]bool{}
				for _, nodeID := range branch.Members {
					members[nodeID] = true
				}
				for _, terminalID := range branch.FailedTerminals {
					if failedCounts[terminalID] == 1 {
						members[terminalID] = true
					}
				}
				frameMembers[frame] = members
			}

		case ArtifactScopeLoop:
			if owner.Opcode != OpcodeLoop {
				return fmt.Errorf("loop scope %q owner has opcode %q", scope.OwnerID, owner.Opcode)
			}
			loop := scope.Loop
			for path, values := range map[string][]string{
				"members": loop.Members, "bodyEntryEdges": loop.BodyEntryEdges, "continueEdges": loop.ContinueEdges,
				"breakEdges": loop.BreakEdges, "doneEdges": loop.DoneEdges, "failedEdges": loop.FailedEdges,
			} {
				var nodeRefs, edgeRefs map[string]any
				if path == "members" {
					nodeRefs = nodesToAny(nodes)
				} else {
					edgeRefs = edgesToAny(edges)
				}
				if err := validateArtifactIDListAny("loop "+path, values, nodeRefs, edgeRefs); err != nil {
					return err
				}
			}
			if len(loop.BodyEntryEdges) == 0 || len(loop.ContinueEdges) == 0 || len(loop.DoneEdges) == 0 || len(loop.FailedEdges) == 0 {
				return fmt.Errorf("loop scope %q has an empty boundary collection", scope.OwnerID)
			}
			for _, member := range loop.Members {
				if member == scope.OwnerID {
					return fmt.Errorf("loop scope %q contains its owner", scope.OwnerID)
				}
			}
			for _, item := range []struct {
				ids         []string
				action      string
				sourcePort  string
				targetPort  string
				sourceOwner bool
				targetOwner bool
			}{{loop.BodyEntryEdges, "body", "body", "", true, false}, {loop.ContinueEdges, "continue", "", "continue", false, true},
				{loop.BreakEdges, "break", "", "break", false, true}, {loop.DoneEdges, "done", "done", "", true, false},
				{loop.FailedEdges, "failed", "failed", "", true, false}} {
				for _, edgeID := range item.ids {
					edge := edges[edgeID]
					if item.sourceOwner && (edge.Source.NodeID != scope.OwnerID || edge.Source.Port != item.sourcePort) ||
						item.targetOwner && (edge.Target.NodeID != scope.OwnerID || edge.Target.Port != item.targetPort) {
						return fmt.Errorf("loop scope %q %s edge %q has inconsistent endpoint", scope.OwnerID, item.action, edgeID)
					}
					addTransition(edgeID, ArtifactScopeTransition{ScopeKind: ArtifactScopeLoop, OwnerID: scope.OwnerID, Action: item.action})
				}
			}
			members := map[string]bool{}
			for _, member := range loop.Members {
				members[member] = true
			}
			frameMembers[ArtifactScopeFrame{Kind: ArtifactScopeLoop, OwnerID: scope.OwnerID}] = members
			loopOwners[scope.OwnerID] = true
		case ArtifactScopeFanOut:
			if owner.Opcode != OpcodeFanOut {
				return fmt.Errorf("fan_out scope %q owner has opcode %q", scope.OwnerID, owner.Opcode)
			}
			for _, frame := range owner.ScopePath {
				if frame.Kind == ArtifactScopeFanOut {
					return fmt.Errorf("nested fan_out scope %q is not supported", scope.OwnerID)
				}
			}
			fanOut := scope.FanOut
			for path, values := range map[string][]string{
				"members": fanOut.Members, "bodyEntryEdges": fanOut.BodyEntryEdges,
				"successArrivals": fanOut.SuccessArrivals, "failedTerminals": fanOut.FailedTerminals,
			} {
				var nodeRefs, edgeRefs map[string]any
				if path == "members" || path == "failedTerminals" {
					nodeRefs = nodesToAny(nodes)
				} else {
					edgeRefs = edgesToAny(edges)
				}
				if err := validateArtifactIDListAny("fan_out "+path, values, nodeRefs, edgeRefs); err != nil {
					return err
				}
			}
			if len(fanOut.BodyEntryEdges) == 0 || len(fanOut.SuccessArrivals) == 0 {
				return fmt.Errorf("fan_out scope %q has an empty success boundary collection", scope.OwnerID)
			}
			join := nodes[fanOut.JoinID]
			if join == nil || join.Opcode != OpcodeJoinAll || join.Config.Join == nil ||
				join.Config.Join.OwnerID != scope.OwnerID || len(join.InputPorts) != 1 || join.InputPorts[0].Name != "item" {
				return fmt.Errorf("fan_out scope %q closing join is inconsistent", scope.OwnerID)
			}
			if closingJoins[fanOut.JoinID] {
				return fmt.Errorf("join %q closes more than one scope", fanOut.JoinID)
			}
			closingJoins[fanOut.JoinID] = true
			for _, member := range fanOut.Members {
				if member == scope.OwnerID || member == fanOut.JoinID {
					return fmt.Errorf("fan_out scope %q contains a boundary node", scope.OwnerID)
				}
			}
			for _, terminalID := range fanOut.FailedTerminals {
				if nodes[terminalID].Opcode != OpcodeEndFailed {
					return fmt.Errorf("fan_out scope %q failed terminal %q is not end.failed", scope.OwnerID, terminalID)
				}
			}
			for _, edgeID := range fanOut.BodyEntryEdges {
				edge := edges[edgeID]
				if edge.Source.NodeID != scope.OwnerID || edge.Source.Port != "item" {
					return fmt.Errorf("fan_out scope %q item edge %q has inconsistent source", scope.OwnerID, edgeID)
				}
				addTransition(edgeID, ArtifactScopeTransition{ScopeKind: ArtifactScopeFanOut, OwnerID: scope.OwnerID, Action: "item"})
			}
			for _, edgeID := range fanOut.SuccessArrivals {
				edge := edges[edgeID]
				if edge.Target.NodeID != fanOut.JoinID || edge.Target.Port != "item" {
					return fmt.Errorf("fan_out scope %q arrival edge %q has inconsistent target", scope.OwnerID, edgeID)
				}
				addTransition(edgeID, ArtifactScopeTransition{ScopeKind: ArtifactScopeFanOut, OwnerID: scope.OwnerID, Action: "arrive"})
			}
			members := map[string]bool{}
			for _, member := range fanOut.Members {
				members[member] = true
			}
			for _, terminal := range fanOut.FailedTerminals {
				members[terminal] = true
			}
			frameMembers[ArtifactScopeFrame{Kind: ArtifactScopeFanOut, OwnerID: scope.OwnerID}] = members
			fanOutOwners[scope.OwnerID] = true
		default:
			return fmt.Errorf("scope %q has unknown kind %q", scope.OwnerID, scope.Kind)
		}
	}

	for _, node := range nodes {
		if node.Opcode == OpcodeFork || node.Opcode == OpcodeSwitchAllMatch {
			if !parallelOwners[node.ID] {
				return fmt.Errorf("parallel owner %q has no scope", node.ID)
			}
		}
		if node.Opcode == OpcodeLoop && !loopOwners[node.ID] {
			return fmt.Errorf("loop owner %q has no scope", node.ID)
		}
		if node.Opcode == OpcodeFanOut && !fanOutOwners[node.ID] {
			return fmt.Errorf("fan_out owner %q has no scope", node.ID)
		}
		if (node.Opcode == OpcodeJoinAll || node.Opcode == OpcodeJoinAny) && !closingJoins[node.ID] {
			return fmt.Errorf("join %q does not close a scope", node.ID)
		}
	}

	for _, scope := range scopes {
		if !reflect.DeepEqual(scope.ParentPath, nodes[scope.OwnerID].ScopePath) {
			return fmt.Errorf("scope %q parentPath does not match owner scopePath", scope.OwnerID)
		}
	}
	for _, node := range nodes {
		if err := validateArtifactNodeScopePath(node, scopeByKey, frameMembers); err != nil {
			return err
		}
	}
	for edgeID, edge := range edges {
		target := nodes[edge.Target.NodeID]
		if target == nil || target.Opcode != OpcodeFailPlan || edge.Source.Port != "failed" && edge.Source.Port != "timed_out" {
			continue
		}
		source := nodes[edge.Source.NodeID]
		if source == nil {
			continue
		}
		for frameIndex := len(source.ScopePath) - 1; frameIndex >= 0; frameIndex-- {
			frame := source.ScopePath[frameIndex]
			if frame.Kind != ArtifactScopeFanOut {
				continue
			}
			action := "fail"
			if edge.Source.Port == "timed_out" {
				action = "timeout"
			}
			addTransition(edgeID, ArtifactScopeTransition{ScopeKind: ArtifactScopeFanOut, OwnerID: frame.OwnerID, Action: action})
			break
		}
	}
	for edgeID, edge := range edges {
		expected := expectedTransitions[edgeID]
		if expected == nil {
			expected = []ArtifactScopeTransition{}
		}
		sortArtifactScopeTransitions(expected)
		if !reflect.DeepEqual(edge.ScopeTransitions, expected) {
			return fmt.Errorf("edge %q scopeTransitions do not match scope metadata", edgeID)
		}
	}
	return nil
}

func validateArtifactNodeScopePath(node *ArtifactNode, scopes map[artifactScopeKey]*ArtifactScope, frameMembers map[ArtifactScopeFrame]map[string]bool) error {
	expectedCount := 0
	for _, members := range frameMembers {
		if members[node.ID] {
			expectedCount++
		}
	}
	if len(node.ScopePath) != expectedCount {
		return fmt.Errorf("node %q scopePath has %d frames, want %d", node.ID, len(node.ScopePath), expectedCount)
	}
	seen := map[ArtifactScopeFrame]bool{}
	for index, frame := range node.ScopePath {
		if frame.OwnerID == "" || frame.Kind != ArtifactScopeParallel && frame.Kind != ArtifactScopeLoop && frame.Kind != ArtifactScopeFanOut ||
			frame.Kind == ArtifactScopeParallel && frame.Branch == "" ||
			(frame.Kind == ArtifactScopeLoop || frame.Kind == ArtifactScopeFanOut) && frame.Branch != "" || seen[frame] {
			return fmt.Errorf("node %q has an invalid scope frame", node.ID)
		}
		seen[frame] = true
		if !frameMembers[frame][node.ID] {
			return fmt.Errorf("node %q scope frame is not backed by scope membership", node.ID)
		}
		scope := scopes[artifactScopeKey{kind: frame.Kind, ownerID: frame.OwnerID}]
		if scope == nil || !reflect.DeepEqual(scope.ParentPath, node.ScopePath[:index]) {
			return fmt.Errorf("node %q scopePath nesting is inconsistent", node.ID)
		}
	}
	return nil
}

func artifactParallelOwnerPorts(owner *ArtifactNode) []string {
	if owner.Opcode == OpcodeFork {
		return append([]string{}, owner.Config.Fork.Branches...)
	}
	ports := make([]string, len(owner.Config.Switch.Cases))
	for i := range owner.Config.Switch.Cases {
		ports[i] = owner.Config.Switch.Cases[i].Port
	}
	sort.Strings(ports)
	return ports
}

func artifactPortNames(ports []ArtifactPort) []string {
	result := make([]string, len(ports))
	for i := range ports {
		result[i] = ports[i].Name
	}
	return result
}

func validateArtifactIDList(path string, values []string, nodes map[string]*ArtifactNode, edges map[string]*ArtifactEdge) error {
	if values == nil || !strictlySortedUnique(values) {
		return fmt.Errorf("%s must be a normalized sorted unique array", path)
	}
	for _, value := range values {
		if !idPattern.MatchString(value) || nodes != nil && nodes[value] == nil || edges != nil && edges[value] == nil {
			return fmt.Errorf("%s references invalid or unknown ID %q", path, value)
		}
	}
	return nil
}

func nodesToAny(nodes map[string]*ArtifactNode) map[string]any {
	result := make(map[string]any, len(nodes))
	for key := range nodes {
		result[key] = true
	}
	return result
}

func edgesToAny(edges map[string]*ArtifactEdge) map[string]any {
	result := make(map[string]any, len(edges))
	for key := range edges {
		result[key] = true
	}
	return result
}

func validateArtifactIDListAny(path string, values []string, nodes, edges map[string]any) error {
	if values == nil || !strictlySortedUnique(values) {
		return fmt.Errorf("%s must be a normalized sorted unique array", path)
	}
	for _, value := range values {
		if !idPattern.MatchString(value) || nodes != nil && nodes[value] == nil || edges != nil && edges[value] == nil {
			return fmt.Errorf("%s references invalid or unknown ID %q", path, value)
		}
	}
	return nil
}

func nonNilArtifactError(err error, fallback string) error {
	if err != nil {
		return err
	}
	return errors.New(fallback)
}

func sortArtifactScopeTransitions(values []ArtifactScopeTransition) {
	sort.Slice(values, func(i, j int) bool {
		if values[i].OwnerID != values[j].OwnerID {
			return values[i].OwnerID < values[j].OwnerID
		}
		if values[i].ScopeKind != values[j].ScopeKind {
			return values[i].ScopeKind < values[j].ScopeKind
		}
		if values[i].Action != values[j].Action {
			return values[i].Action < values[j].Action
		}
		return values[i].Branch < values[j].Branch
	})
}

func validateArtifactStateEffects(artifact *Artifact) error {
	sets := make(map[string]*artifactStateEffectSet, len(artifact.Nodes))
	for i := range artifact.Nodes {
		set := newArtifactStateEffectSet()
		sets[artifact.Nodes[i].ID] = set
		collectArtifactNodeEffects(&artifact.Nodes[i], set)
	}
	for _, edge := range artifact.Edges {
		collectArtifactBindingsReads(edge.Bindings, sets[edge.Source.NodeID].reads)
	}
	collectArtifactBindingsReads(artifact.Entry.Bindings, sets[artifact.Entry.NodeID].reads)
	for _, node := range artifact.Nodes {
		expected := ArtifactStateEffects{Reads: sortedArtifactEffectKeys(sets[node.ID].reads), Writes: sortedArtifactEffectKeys(sets[node.ID].writes)}
		if !reflect.DeepEqual(node.StateEffects, expected) {
			return fmt.Errorf("node %q stateEffects do not match compiled AST/bindings", node.ID)
		}
	}
	return nil
}

func artifactPortExists(ports []ArtifactPort, name string) bool {
	for _, port := range ports {
		if port.Name == name {
			return true
		}
	}
	return false
}

func strictlySortedUnique(values []string) bool {
	for i := range values {
		if i > 0 && values[i] <= values[i-1] {
			return false
		}
	}
	return true
}
