package programchecker

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/formula/spec"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type FlowCheckOptions struct {
	Metadata      ProgramMetadataReader
	Formula       FormulaCatalogReader
	Actions       ActionSignatureReader
	Connectors    ConnectorMappingReader
	ProcessType   string
	ComponentPath string
	OwnerPack     string
}

type formulaSpecRegistryProvider interface {
	SpecRegistry() *spec.Registry
}

func CheckFlowMetadata(ctx context.Context, meta models.FlowMetadata, opts FlowCheckOptions) Diagnostics {
	c := &flowChecker{
		ctx:       ctx,
		meta:      meta,
		opts:      opts,
		variables: map[string]TypeRef{},
	}
	c.checkFlowFormulaResourceProgram()
	c.checkStart()
	c.checkRecordCreates()
	c.checkRecordUpdates()
	c.checkRecordDeletes()
	c.checkRecordLookups()
	c.checkAtomicParentPrelocks()
	c.checkAtomicHistoryPrelocks()
	c.checkActionCalls()
	c.checkSubflows()
	c.checkLoops()
	c.checkAssignments()
	return c.diagnostics
}

// CheckFlowFormulaResources is the runtime-independent admission validator for
// Flow Formula Resources. It owns the resource namespace, dependency graph,
// declaration-order-independent type scope, expression validation, and return
// type contract. Metadata may be omitted by authoring surfaces that do not yet
// have an effective schema snapshot; source-pack preflight supplies it.
func CheckFlowFormulaResources(ctx context.Context, meta models.FlowMetadata, opts FlowCheckOptions) Diagnostics {
	c := &flowChecker{
		ctx:                  ctx,
		meta:                 meta,
		opts:                 opts,
		formulaResourcesOnly: true,
		variables:            map[string]TypeRef{},
	}
	c.checkFlowFormulaResourceProgram()
	return c.diagnostics
}

// checkFlowFormulaResourceProgram is the single validator implementation used
// by both complete Flow program checking and write-before-authoring admission.
func (c *flowChecker) checkFlowFormulaResourceProgram() {
	c.checkResourceNamespace()
	c.checkVariables()
	c.collectImplicitVariables()
	if c.meta.Start != nil {
		c.startObject = strings.TrimSpace(c.meta.Start.Object)
	}
	c.preloadFormulaExpressionScopeTypes()
	c.checkFormulas()
}

type flowChecker struct {
	ctx                  context.Context
	meta                 models.FlowMetadata
	opts                 FlowCheckOptions
	formulaResourcesOnly bool
	variables            map[string]TypeRef
	startObject          string
	diagnostics          Diagnostics
}

func (c *flowChecker) hasConnectorSourceRecord() bool {
	return c.meta.Start != nil && c.meta.Start.ConnectorSource != nil
}

func (c *flowChecker) checkVariables() {
	for i, v := range c.meta.Variables {
		name := strings.TrimSpace(v.Name)
		if name == "" {
			continue
		}
		typ := typeRefFromFlowVariable(v)
		c.variables[name] = typ
		if !c.formulaResourcesOnly && typ.Scalar == spec.TypeObject && strings.TrimSpace(typ.ObjectAPIName) != "" {
			c.requireObject(typ.ObjectAPIName, fmt.Sprintf("/variables/%d/objectType", i), name)
		}
		if v.Value != nil {
			if ref, ok := flowRuntimeGlobalReferenceFromValue(v.Value); ok {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.unsupported_variable_default",
					Severity:    SeverityError,
					JSONPointer: fmt.Sprintf("/variables/%d/value", i),
					ElementName: name,
					Reference:   ref,
					Message:     fmt.Sprintf("flow variable default %q references runtime global %q, but variable defaults are not resolved through the runtime global read path", name, ref),
				})
				continue
			}
			actual, ok := c.typeRefFromStaticValue(v.Value)
			if ok {
				c.checkTypeCompatible(typ, actual, fmt.Sprintf("/variables/%d/value", i), name, name)
			}
		}
	}
}

func (c *flowChecker) collectImplicitVariables() {
	for _, create := range c.meta.RecordCreates {
		c.declareVariableRoot(create.AssignRecordIdToReference, TypeRef{Scalar: spec.TypeString, Nullable: true, Source: ValueSourceVariable})
		if create.StoreOutputAutomatically {
			c.declareVariableRoot(create.Name, TypeRef{Scalar: spec.TypeString, Nullable: true, Source: ValueSourceVariable})
		}
	}
	for _, lookup := range c.meta.RecordLookups {
		lookupType := recordLookupResultType(lookup)
		c.declareVariableRoot(lookup.Name, lookupType)
		c.declareOutputReferenceRoot(lookup.OutputReference, lookupType)
		if lookup.GetFirstRecordOnly {
			for _, out := range lookup.OutputAssignments {
				c.declareVariableRoot(out.AssignToReference, TypeRef{Scalar: spec.TypeUnknown, Nullable: true, Source: ValueSourceVariable})
			}
		}
	}
	for _, assignment := range c.meta.Assignments {
		for _, item := range assignment.Items {
			c.declareVariableRoot(item.AssignToReference, TypeRef{Scalar: spec.TypeUnknown, Nullable: true, Source: ValueSourceVariable})
		}
	}
	for _, f := range c.meta.Formulas {
		c.declareVariableRoot(f.Name, TypeRef{Scalar: spec.TypeUnknown, Nullable: true, Source: ValueSourceFormulaExpression})
	}
	for _, action := range c.meta.ActionCalls {
		if target, objectName := flowPreviewRecordUpdateOutputTarget(action); target != "" {
			c.declareOutputReferenceRoot(target, TypeRef{Scalar: spec.TypeObject, ObjectAPIName: objectName, Nullable: true, Source: ValueSourceActionOutput})
		}
		if !flowActionWritesOutputParameters(action.ActionType) {
			continue
		}
		for _, out := range action.OutputParameters {
			target := flowActionOutputTarget(out.Value)
			if target == "" || strings.Contains(normalizeFlowReference(target), ".") {
				continue
			}
			typ := TypeRef{Scalar: spec.TypeUnknown, Nullable: true, Source: ValueSourceActionOutput}
			root, ok := flowOutputReferenceRoot(target)
			if ok && strings.HasPrefix(root, "$") {
				builtinTyp, found := flowBuiltinActionOutputTypeForAction(action, out.Name)
				if !found {
					continue
				}
				typ = builtinTyp
			} else if builtinTyp, found := flowBuiltinActionOutputTypeForAction(action, out.Name); found {
				typ = builtinTyp
			}
			c.declareOutputReferenceRoot(target, typ)
		}
	}
	for _, subflow := range c.meta.Subflows {
		for _, out := range subflow.OutputAssignments {
			c.declareVariableRoot(out.AssignToReference, TypeRef{Scalar: spec.TypeAny, Nullable: true, Source: ValueSourceVariable})
		}
	}
	for _, decision := range c.meta.Decisions {
		for _, rule := range decision.Rules {
			c.declareVariableRoot(rule.Name, TypeRef{Scalar: spec.TypeBoolean, Nullable: true, Source: ValueSourceVariable})
		}
	}
}

func (c *flowChecker) declareVariableRoot(ref string, typ TypeRef) {
	root := flowReferenceRoot(ref)
	if !flowReferenceRootIsDeclarable(root) {
		return
	}
	if _, exists := c.variables[root]; exists {
		return
	}
	c.variables[root] = typ
}

func (c *flowChecker) declareOutputReferenceRoot(ref string, typ TypeRef) {
	root, ok := flowOutputReferenceRoot(ref)
	if !ok || !flowOutputReferenceRootIsDeclarable(root) {
		return
	}
	existing, exists := c.variables[root]
	if !exists {
		c.variables[root] = typ
		return
	}
	if c.flowVariableDeclared(root) {
		return
	}
	if flowPreservesConcreteOutputRoot(existing, typ) {
		return
	}
	if flowOutputReferenceTypesCompatible(existing, typ) {
		c.variables[root] = typ
	}
}

func (c *flowChecker) checkStart() {
	if c.meta.Start == nil {
		return
	}
	objectName := strings.TrimSpace(c.meta.Start.Object)
	if objectName != "" {
		if _, ok := c.requireObject(objectName, "/start/object", "start"); ok {
			c.startObject = objectName
			c.checkFilters(objectName, c.meta.Start.Filters, "/start/filters", "start", flowFilterCheckOptions{
				allowUnaryOperators: flowStartHasPriorRecordSemantics(c.opts.ProcessType, c.meta.Start),
			})
		}
	}
	if expr := strings.TrimSpace(c.meta.Start.FilterFormula); expr != "" {
		c.checkFilterFormula(expr, objectName)
	}
}

func (c *flowChecker) checkRecordCreates() {
	for i, create := range c.meta.RecordCreates {
		c.withElementScope(create.Name, func() {
			base := fmt.Sprintf("/recordCreates/%d", i)
			objectName := strings.TrimSpace(create.Object)
			if _, ok := c.requireObject(objectName, base+"/object", create.Name); !ok {
				return
			}
			c.checkFieldMappingRef(create.FieldMapping, objectName, "", base+"/fieldMapping", create.Name)
			c.checkInputAssignments(objectName, create.InputAssignments, base+"/inputAssignments", create.Name)
			if create.StoreOutputAutomatically {
				c.checkScopedLoopIteratorRootWrite(create.Name, base+"/name", create.Name, "record create auto-store output")
			}
			if create.AssignRecordIdToReference != "" {
				if c.checkScopedLoopIteratorRootWrite(create.AssignRecordIdToReference, base+"/assignRecordIdToReference", create.Name, "record create assignRecordIdToReference") {
					return
				}
				c.checkAssignableReference(create.AssignRecordIdToReference, TypeRef{Scalar: spec.TypeString, Nullable: false, Source: ValueSourceFieldReference}, base+"/assignRecordIdToReference", create.Name)
			}
		})
	}
}

func (c *flowChecker) checkRecordUpdates() {
	for i, update := range c.meta.RecordUpdates {
		c.withElementScope(update.Name, func() {
			base := fmt.Sprintf("/recordUpdates/%d", i)
			if update.RecheckFiltersAtWrite {
				if len(update.Filters) == 0 || strings.TrimSpace(update.InputReference) != "" || strings.TrimSpace(update.Object) == "" {
					c.addDiagnostic(Diagnostic{
						Code:        "metadata_program.invalid_record_update_guard",
						Severity:    SeverityError,
						JSONPointer: base + "/recheckFiltersAtWrite",
						ElementName: update.Name,
						Message:     "recheckFiltersAtWrite requires an explicit object, at least one filter, and no inputReference",
					})
				}
				if err := models.ValidateFlowFilterLogic(update.FilterLogic, len(update.Filters)); err != nil {
					c.addDiagnostic(Diagnostic{
						Code:        "metadata_program.invalid_record_update_guard_logic",
						Severity:    SeverityError,
						JSONPointer: base + "/filterLogic",
						ElementName: update.Name,
						Message:     fmt.Sprintf("recheckFiltersAtWrite requires complete filterLogic: %v", err),
					})
				}
				for filterIndex, filter := range update.Filters {
					if err := validateGuardedRecordUpdateAuthoredFilter(filter); err != nil {
						c.addDiagnostic(Diagnostic{
							Code:        "metadata_program.invalid_record_update_guard_filter",
							Severity:    SeverityError,
							JSONPointer: fmt.Sprintf("%s/filters/%d", base, filterIndex),
							ElementName: update.Name,
							Message:     fmt.Sprintf("recheckFiltersAtWrite filter %d is not query-recheckable: %v", filterIndex+1, err),
						})
					}
				}
			}
			objectName := strings.TrimSpace(update.Object)
			relatedObjectName, filtersUseRelatedTarget := c.recordUpdateFiltersDerivedRelatedObject(update.Filters)
			filtersUseRuntimeRecord := c.recordUpdateFiltersUseRuntimeRecord(update, filtersUseRelatedTarget)
			if objectName == "" && strings.TrimSpace(update.InputReference) != "" {
				if typ, ok := c.resolveReference(update.InputReference, base+"/inputReference", update.Name); ok && typ.Scalar == spec.TypeObject {
					objectName = typ.ObjectAPIName
				}
			}
			if objectName == "" && strings.TrimSpace(update.InputReference) == "" && filtersUseRelatedTarget && relatedObjectName != "" {
				objectName = relatedObjectName
			}
			if objectName == "" && filtersUseRuntimeRecord {
				objectName = c.startObject
			}
			if _, ok := c.requireObject(objectName, base+"/object", update.Name); !ok {
				return
			}
			c.checkFieldMappingRef(update.FieldMapping, objectName, update.InputReference, base+"/fieldMapping", update.Name)
			recordFiltersHavePrior := filtersUseRuntimeRecord && flowStartHasPriorRecordSemantics(c.opts.ProcessType, c.meta.Start)
			c.checkFilters(objectName, update.Filters, base+"/filters", update.Name, flowFilterCheckOptions{
				allowUnaryOperators: recordFiltersHavePrior,
				allowRuntimeGlobals: filtersUseRuntimeRecord || recordUpdateFiltersUseQueryRuntimeValues(update),
			})
			c.checkInputAssignments(objectName, update.InputAssignments, base+"/inputAssignments", update.Name)
		})
	}
}

func validateGuardedRecordUpdateAuthoredFilter(filter models.FlowRecordFilter) error {
	_, err := models.ValidateGuardedFlowRecordFilter(filter)
	return err
}

func (c *flowChecker) recordUpdateFiltersUseRuntimeRecord(update models.FlowRecordUpdate, filtersUseRelatedTarget bool) bool {
	inputRef := strings.TrimSpace(update.InputReference)
	if inputRef == "$Record" {
		return true
	}
	if inputRef != "" || strings.TrimSpace(update.Object) != "" || c.startObject == "" {
		return false
	}
	return !filtersUseRelatedTarget
}

func recordUpdateFiltersUseQueryRuntimeValues(update models.FlowRecordUpdate) bool {
	return strings.TrimSpace(update.InputReference) == "" && len(update.Filters) > 0
}

func (c *flowChecker) recordUpdateFiltersDerivedRelatedObject(filters []models.FlowRecordFilter) (string, bool) {
	for _, filter := range filters {
		if !strings.EqualFold(filter.Field, "Id") || !strings.EqualFold(filter.Operator, "EqualTo") {
			continue
		}
		ref, ok := flowFilterElementReference(filter.Value)
		if !ok || !strings.HasPrefix(ref, "$Record.") {
			continue
		}
		fieldName := strings.TrimPrefix(ref, "$Record.")
		if fieldName == "" || strings.Contains(fieldName, ".") || strings.EqualFold(fieldName, "Id") {
			continue
		}
		field, ok := c.lookupFieldSilently(c.startObject, fieldName)
		if !ok {
			return "", true
		}
		if len(field.ReferenceTo) == 1 {
			return strings.TrimSpace(field.ReferenceTo[0]), true
		}
		return "", true
	}
	return "", false
}

func flowFilterElementReference(value any) (string, bool) {
	m, ok := value.(map[string]interface{})
	if !ok {
		return "", false
	}
	ref, ok := m["elementReference"].(string)
	if !ok {
		return "", false
	}
	return strings.TrimSpace(ref), ref != ""
}

func flowStartHasPriorRecordSemantics(processType string, start *models.FlowStart) bool {
	if start == nil || strings.TrimSpace(start.Object) == "" {
		return false
	}
	if start.Schedule != nil {
		return false
	}
	if flowProcessTypeIsScheduled(processType) || flowTriggerTypeIsScheduled(start.TriggerType) {
		return false
	}
	return flowTriggerTypeIsRecord(start.TriggerType)
}

func flowProcessTypeIsScheduled(processType string) bool {
	switch strings.ToLower(strings.TrimSpace(processType)) {
	case "scheduledflow", "scheduletriggeredflow":
		return true
	default:
		return false
	}
}

func flowTriggerTypeIsScheduled(triggerType string) bool {
	switch strings.ToLower(strings.TrimSpace(triggerType)) {
	case "scheduled", constants.TriggerTypeSchedule:
		return true
	default:
		return false
	}
}

func flowTriggerTypeIsRecord(triggerType string) bool {
	switch strings.ToLower(strings.TrimSpace(triggerType)) {
	case "recordbeforesave", "recordaftersave", "onallchanges",
		constants.TriggerTypeRecordCreated, constants.TriggerTypeRecordUpdated:
		return true
	default:
		return false
	}
}

func (c *flowChecker) checkRecordDeletes() {
	for i, del := range c.meta.RecordDeletes {
		c.withElementScope(del.Name, func() {
			base := fmt.Sprintf("/recordDeletes/%d", i)
			objectName := strings.TrimSpace(del.Object)
			if objectName == "" && strings.TrimSpace(del.InputReference) != "" {
				if typ, ok := c.resolveReference(del.InputReference, base+"/inputReference", del.Name); ok && typ.Scalar == spec.TypeObject {
					objectName = typ.ObjectAPIName
				}
			}
			if _, ok := c.requireObject(objectName, base+"/object", del.Name); !ok {
				return
			}
			c.checkFilters(objectName, del.Filters, base+"/filters", del.Name, flowFilterCheckOptions{
				allowRuntimeGlobals: strings.TrimSpace(del.InputReference) == "" && len(del.Filters) > 0,
			})
		})
	}
}

func (c *flowChecker) checkRecordLookups() {
	for i, lookup := range c.meta.RecordLookups {
		c.withElementScope(lookup.Name, func() {
			base := fmt.Sprintf("/recordLookups/%d", i)
			c.checkScopedLoopIteratorRootWrite(lookup.Name, base+"/name", lookup.Name, "record lookup name")
			objectName := strings.TrimSpace(lookup.Object)
			if _, ok := c.requireObject(objectName, base+"/object", lookup.Name); !ok {
				return
			}
			c.checkFilters(objectName, lookup.Filters, base+"/filters", lookup.Name, flowFilterCheckOptions{
				allowRuntimeGlobals: len(lookup.Filters) > 0,
			})
			for j, out := range lookup.OutputAssignments {
				fieldPath := fmt.Sprintf("%s/outputAssignments/%d/field", base, j)
				field, ok := c.requireField(objectName, out.Field, fieldPath, lookup.Name)
				if !ok {
					continue
				}
				assignPointer := fmt.Sprintf("%s/outputAssignments/%d/assignToReference", base, j)
				if c.checkScopedLoopIteratorRootWrite(out.AssignToReference, assignPointer, lookup.Name, "record lookup output assignment") {
					continue
				}
				c.checkAssignableReference(out.AssignToReference, typeRefFromField(field), assignPointer, lookup.Name)
			}
			if ref := strings.TrimSpace(lookup.OutputReference); ref != "" {
				c.checkLookupOutputReference(ref, recordLookupResultType(lookup), base+"/outputReference", lookup.Name)
			}
		})
	}
}

func (c *flowChecker) checkAtomicParentPrelocks() {
	if len(c.meta.AtomicParentPrelocks) == 0 {
		return
	}
	if err := models.ValidateFlowAtomicParentPrelocks(c.meta); err != nil {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_parent_prelock",
			Severity:    SeverityError,
			JSONPointer: "/atomicParentPrelocks",
			Message:     err.Error(),
		})
		return
	}
	if !c.meta.RunAtomically {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_parent_prelock",
			Severity:    SeverityError,
			JSONPointer: "/atomicParentPrelocks",
			Message:     "atomicParentPrelocks requires runAtomically=true",
		})
	}
	seenNames := make(map[string]struct{}, len(c.meta.AtomicParentPrelocks))
	for i, prelock := range c.meta.AtomicParentPrelocks {
		base := fmt.Sprintf("/atomicParentPrelocks/%d", i)
		name := strings.TrimSpace(prelock.Name)
		if name == "" {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.invalid_parent_prelock",
				Severity:    SeverityError,
				JSONPointer: base + "/name",
				Message:     "atomic parent prelock name is blank",
			})
		} else if _, exists := seenNames[name]; exists {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.invalid_parent_prelock",
				Severity:    SeverityError,
				JSONPointer: base + "/name",
				ElementName: name,
				Message:     fmt.Sprintf("duplicate atomic parent prelock name %q", name),
			})
		}
		seenNames[name] = struct{}{}

		parentObject := strings.TrimSpace(prelock.ParentObject)
		if _, ok := c.requireObject(parentObject, base+"/parentObject", name); !ok {
			continue
		}
		for j, value := range prelock.RecordIDs {
			c.checkAtomicParentPrelockRecordIDValue(value, fmt.Sprintf("%s/recordIds/%d", base, j), name)
		}
		sourceObject := strings.TrimSpace(prelock.SourceObject)
		if sourceObject == "" {
			if len(prelock.RecordIDs) == 0 {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_parent_prelock",
					Severity:    SeverityError,
					JSONPointer: base,
					ElementName: name,
					Message:     "atomic parent prelock requires recordIds or a source query",
				})
			}
			continue
		}
		if _, ok := c.requireObject(sourceObject, base+"/sourceObject", name); !ok {
			continue
		}
		field, ok := c.requireField(sourceObject, prelock.ParentReferenceField, base+"/parentReferenceField", name)
		if ok && !(strings.EqualFold(sourceObject, parentObject) && strings.EqualFold(prelock.ParentReferenceField, constants.FieldID)) {
			referencesParent := false
			for _, referenceTo := range field.ReferenceTo {
				if strings.EqualFold(referenceTo, parentObject) {
					referencesParent = true
					break
				}
			}
			if !referencesParent {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_parent_prelock",
					Severity:    SeverityError,
					JSONPointer: base + "/parentReferenceField",
					ElementName: name,
					Reference:   sourceObject + "." + prelock.ParentReferenceField,
					Message:     fmt.Sprintf("field %q does not reference parent object %q", prelock.ParentReferenceField, parentObject),
				})
			}
		}
		if len(prelock.Filters) == 0 {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.invalid_parent_prelock",
				Severity:    SeverityError,
				JSONPointer: base + "/filters",
				ElementName: name,
				Message:     "atomic parent prelock source query requires at least one filter",
			})
			continue
		}
		c.checkFilters(sourceObject, prelock.Filters, base+"/filters", name, flowFilterCheckOptions{
			allowRuntimeGlobals: true,
		})
	}
}

func (c *flowChecker) checkAtomicHistoryPrelocks() {
	if len(c.meta.AtomicHistoryPrelocks) == 0 {
		return
	}
	if err := models.ValidateFlowAtomicHistoryPrelocks(c.meta); err != nil {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_history_prelock",
			Severity:    SeverityError,
			JSONPointer: "/atomicHistoryPrelocks",
			Message:     err.Error(),
		})
		return
	}
	for i, prelock := range c.meta.AtomicHistoryPrelocks {
		base := fmt.Sprintf("/atomicHistoryPrelocks/%d", i)
		objectAPIName := strings.TrimSpace(prelock.Object)
		if _, ok := c.requireObject(objectAPIName, base+"/object", objectAPIName); !ok {
			continue
		}
		for j, fieldAPIName := range prelock.Fields {
			pointer := fmt.Sprintf("%s/fields/%d", base, j)
			field, ok := c.requireField(objectAPIName, fieldAPIName, pointer, objectAPIName)
			if ok && strings.TrimSpace(field.ID) == "" {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_history_prelock",
					Severity:    SeverityError,
					JSONPointer: pointer,
					ElementName: objectAPIName,
					Reference:   objectAPIName + "." + fieldAPIName,
					Message:     fmt.Sprintf("field %q on object %q does not have a stable FieldId", fieldAPIName, objectAPIName),
				})
			}
		}
	}
}

func (c *flowChecker) checkAtomicParentPrelockRecordIDValue(value any, pointer, elementName string) {
	switch typed := value.(type) {
	case []interface{}:
		for i, item := range typed {
			c.checkAtomicParentPrelockRecordIDValue(item, fmt.Sprintf("%s/%d", pointer, i), elementName)
		}
		return
	case map[string]interface{}:
		if raw, ok := typed["elementReference"]; ok {
			ref, _ := raw.(string)
			actual, resolved := c.resolveReference(ref, pointer, elementName)
			if !resolved {
				return
			}
			expected := TypeRef{Scalar: spec.TypeString, Collection: actual.Collection, Nullable: true, Source: ValueSourceFieldReference}
			c.checkTypeCompatible(expected, actual, pointer, elementName, ref)
			return
		}
	}

	actual, ok := c.typeRefFromValue(value, pointer, elementName)
	if !ok {
		return
	}
	expected := TypeRef{Scalar: spec.TypeString, Collection: actual.Collection, Nullable: true, Source: ValueSourceLiteral}
	c.checkTypeCompatible(expected, actual, pointer, elementName, "atomic parent prelock record id")
}

func (c *flowChecker) checkLookupOutputReference(ref string, typ TypeRef, pointer, elementName string) {
	root, ok := flowOutputReferenceRoot(ref)
	if !ok {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_reference",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("record lookup outputReference %q must be a single variable symbol; dotted or merge-wrapped references are not resolvable by the Flow runtime context", ref),
		})
		return
	}
	if c.checkScopedLoopIteratorRootWrite(ref, pointer, elementName, "record lookup outputReference") {
		return
	}
	if !flowOutputReferenceRootIsDeclarable(root) {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_reference",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("record lookup outputReference %q collides with a runtime-owned Flow reference root", ref),
		})
		return
	}
	if existing, exists := c.variables[root]; exists && !flowOutputReferenceTypesCompatible(existing, typ) {
		c.addDiagnostic(Diagnostic{
			Code:         "metadata_program.type_mismatch",
			Severity:     SeverityError,
			JSONPointer:  pointer,
			ElementName:  elementName,
			Reference:    ref,
			ExpectedType: typ.String(),
			ActualType:   existing.String(),
			Message:      fmt.Sprintf("record lookup outputReference %q writes %s but existing variable %q is %s", ref, typ.String(), root, existing.String()),
		})
		return
	}
	c.refineOutputReferenceRoot(ref, typ)
}

func (c *flowChecker) checkLoops() {
	for i, loop := range c.meta.Loops {
		c.withElementScope(loop.Name, func() {
			base := fmt.Sprintf("/loops/%d", i)
			collection, ok := c.resolveLoopCollectionReference(loop.CollectionReference, base+"/collectionReference", loop.Name)
			if ok && !collection.Collection && !flowLooseType(collection) {
				c.addDiagnostic(Diagnostic{
					Code:         "metadata_program.type_mismatch",
					Severity:     SeverityError,
					JSONPointer:  base + "/collectionReference",
					ElementName:  loop.Name,
					Reference:    loop.CollectionReference,
					ExpectedType: "collection",
					ActualType:   collection.String(),
					Message:      fmt.Sprintf("loop %q collectionReference %q must resolve to a collection, got %s", loop.Name, loop.CollectionReference, collection.String()),
				})
			}

			iterVar := flowLoopIteratorReference(loop)
			if iterVar == "" {
				return
			}
			pointer := base + "/assignNextValueToReference"
			if strings.TrimSpace(loop.AssignNextValueToReference) == "" {
				pointer = base + "/name"
			}
			root, valid := flowOutputReferenceRoot(iterVar)
			if !valid {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_reference",
					Severity:    SeverityError,
					JSONPointer: pointer,
					ElementName: loop.Name,
					Reference:   iterVar,
					Message:     fmt.Sprintf("loop iterator reference %q must be a single variable symbol; dotted or merge-wrapped references are not resolvable by the Flow runtime context", iterVar),
				})
				return
			}
			if !flowOutputReferenceRootIsDeclarable(root) {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_reference",
					Severity:    SeverityError,
					JSONPointer: pointer,
					ElementName: loop.Name,
					Reference:   iterVar,
					Message:     fmt.Sprintf("loop iterator reference %q collides with a runtime-owned Flow reference root", iterVar),
				})
				return
			}
			if itemType, ok := c.loopIteratorType(loop); ok {
				c.checkLoopIteratorWriterCollision(root, itemType, pointer, loop.Name, iterVar)
				c.checkNestedLoopIteratorCollision(loop, root, itemType, pointer, iterVar)
			}
		})
	}
}

func (c *flowChecker) checkLoopIteratorWriterCollision(root string, typ TypeRef, pointer, elementName, reference string) {
	existing, exists := c.variables[root]
	if !exists || existing.Source == ValueSourceLoopIterator || c.flowVariableDeclared(root) || existing.Source == ValueSourceFormulaExpression {
		return
	}
	c.addDiagnostic(Diagnostic{
		Code:         "metadata_program.type_mismatch",
		Severity:     SeverityError,
		JSONPointer:  pointer,
		ElementName:  elementName,
		Reference:    reference,
		ExpectedType: typ.String(),
		ActualType:   existing.String(),
		Message:      fmt.Sprintf("loop iterator reference %q writes %s but existing variable %q is %s", reference, typ.String(), root, existing.String()),
	})
}

func (c *flowChecker) checkNestedLoopIteratorCollision(loop models.FlowLoop, root string, typ TypeRef, pointer, reference string) {
	for _, other := range c.meta.Loops {
		if strings.TrimSpace(other.Name) == strings.TrimSpace(loop.Name) {
			continue
		}
		otherRoot, ok := flowOutputReferenceRoot(flowLoopIteratorReference(other))
		if !ok || otherRoot != root {
			continue
		}
		if !c.loopBodyContainsElement(loop, other.Name) && !c.loopBodyContainsElement(other, loop.Name) {
			continue
		}
		c.addDiagnostic(Diagnostic{
			Code:         "metadata_program.type_mismatch",
			Severity:     SeverityError,
			JSONPointer:  pointer,
			ElementName:  loop.Name,
			Reference:    reference,
			ExpectedType: typ.String(),
			ActualType:   "nested loop iterator",
			Message:      fmt.Sprintf("loop iterator reference %q reuses nested loop iterator root %q; the Flow runtime deletes that key when the nested loop exits", reference, root),
		})
		return
	}
}

func (c *flowChecker) checkScopedLoopIteratorRootWrite(ref, pointer, elementName, writerKind string) bool {
	root, typ, ok := c.scopedLoopIteratorRootWrite(ref, elementName)
	if !ok {
		return false
	}
	c.addDiagnostic(Diagnostic{
		Code:         "metadata_program.type_mismatch",
		Severity:     SeverityError,
		JSONPointer:  pointer,
		ElementName:  elementName,
		Reference:    normalizeFlowReference(ref),
		ExpectedType: "non-loop-iterator write target",
		ActualType:   typ.String(),
		Message:      fmt.Sprintf("%s %q writes loop iterator root %q inside a loop body; the Flow runtime owns that key and overwrites or deletes it across loop iterations", writerKind, elementName, root),
	})
	return true
}

func (c *flowChecker) scopedLoopIteratorRootWrite(ref, elementName string) (string, TypeRef, bool) {
	root, ok := flowWriteReferenceRoot(ref)
	if !ok {
		return "", TypeRef{}, false
	}
	scoped := c.loopIteratorTypesForElement(elementName)
	if len(scoped) == 0 {
		return "", TypeRef{}, false
	}
	typ, ok := scoped[root]
	return root, typ, ok
}

func (c *flowChecker) checkSubflows() {
	for i, subflow := range c.meta.Subflows {
		c.withElementScope(subflow.Name, func() {
			for j, out := range subflow.OutputAssignments {
				pointer := fmt.Sprintf("/subflows/%d/outputAssignments/%d/assignToReference", i, j)
				c.checkScopedLoopIteratorRootWrite(out.AssignToReference, pointer, subflow.Name, "subflow output assignment")
			}
		})
	}
}

func (c *flowChecker) checkAssignments() {
	for i, assignment := range c.meta.Assignments {
		c.withElementScope(assignment.Name, func() {
			for j, item := range assignment.Items {
				base := fmt.Sprintf("/assignments/%d/assignmentItems/%d", i, j)
				if c.checkScopedLoopIteratorRootWrite(item.AssignToReference, base+"/assignToReference", assignment.Name, "assignment") {
					continue
				}
				expected, ok := c.resolveAssignableReference(item.AssignToReference, base+"/assignToReference", assignment.Name)
				if !ok {
					continue
				}
				actual, ok := c.typeRefFromRuntimeValue(item.Value, base+"/value", assignment.Name)
				if !ok {
					continue
				}
				c.checkAssignmentItemTypeCompatible(expected, actual, item.Operator, base+"/value", assignment.Name, item.AssignToReference)
			}
		})
	}
}

func (c *flowChecker) checkAssignmentItemTypeCompatible(expected, actual TypeRef, operator, pointer, elementName, reference string) {
	switch strings.TrimSpace(operator) {
	case "Add":
		if expected.Collection {
			c.checkCollectionItemTypeCompatible(expected, actual, pointer, elementName, reference)
			return
		}
		c.checkTypeCompatible(expected, actual, pointer, elementName, reference)
	case "AddAtStart", "RemoveFirst", "RemoveAll", "RemoveAfterFirst", "RemoveBeforeFirst":
		c.checkCollectionItemTypeCompatible(expected, actual, pointer, elementName, reference)
	default:
		c.checkTypeCompatible(expected, actual, pointer, elementName, reference)
	}
}

func (c *flowChecker) checkCollectionItemTypeCompatible(collection, item TypeRef, pointer, elementName, reference string) {
	if !collection.Collection {
		scalar := collection.normalizedScalar()
		if scalar == spec.TypeUnknown || scalar == spec.TypeAny {
			expectedItem := collection
			expectedItem.Collection = false
			c.checkTypeCompatible(expectedItem, item, pointer, elementName, reference)
			return
		}
		c.addDiagnostic(Diagnostic{
			Code:         "metadata_program.type_mismatch",
			Severity:     SeverityError,
			JSONPointer:  pointer,
			ElementName:  elementName,
			Reference:    reference,
			ExpectedType: "collection",
			ActualType:   collection.String(),
			Message:      fmt.Sprintf("%s expects a collection target but got %s", reference, collection.String()),
		})
		return
	}
	expectedItem := collection
	expectedItem.Collection = false
	c.checkTypeCompatible(expectedItem, item, pointer, elementName, reference)
}

func (c *flowChecker) checkInputAssignments(objectName string, inputs []models.FlowInputAssignment, pointerBase, elementName string) {
	for i, input := range inputs {
		field, ok := c.requireField(objectName, input.Field, fmt.Sprintf("%s/%d/field", pointerBase, i), elementName)
		if !ok {
			continue
		}
		actual, ok := c.typeRefFromRuntimeValue(input.Value, fmt.Sprintf("%s/%d/value", pointerBase, i), elementName)
		if !ok {
			continue
		}
		c.checkFieldValueCompatible(field, actual, fmt.Sprintf("%s/%d/value", pointerBase, i), elementName, objectName+"."+input.Field)
	}
}

type flowFilterCheckOptions struct {
	allowUnaryOperators bool
	allowRuntimeGlobals bool
}

func (c *flowChecker) checkFilters(objectName string, filters []models.FlowRecordFilter, pointerBase, elementName string, opts flowFilterCheckOptions) {
	for i, filter := range filters {
		field, ok := c.requireField(objectName, filter.Field, fmt.Sprintf("%s/%d/field", pointerBase, i), elementName)
		if !ok {
			continue
		}
		if flowFilterOperatorIgnoresValue(filter.Operator) {
			if !opts.allowUnaryOperators {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.unsupported_operator",
					Severity:    SeverityError,
					JSONPointer: fmt.Sprintf("%s/%d/operator", pointerBase, i),
					ElementName: elementName,
					Reference:   objectName + "." + filter.Field,
					Message:     fmt.Sprintf("%s.%s operator %q is only supported in Flow start filters or RecordUpdate filters targeting $Record", objectName, filter.Field, strings.TrimSpace(filter.Operator)),
				})
			}
			continue
		}
		actual, ok := c.typeRefFromValueWithRuntimeGlobals(filter.Value, fmt.Sprintf("%s/%d/value", pointerBase, i), elementName, opts.allowRuntimeGlobals)
		if !ok {
			continue
		}
		c.checkFilterValueCompatible(field, actual, filter.Value, filter.Operator, fmt.Sprintf("%s/%d/value", pointerBase, i), elementName, objectName+"."+filter.Field)
	}
}

func (c *flowChecker) checkFilterValueCompatible(field *models.FieldMetadata, actual TypeRef, value any, operator, pointer, elementName, reference string) {
	if flowFilterOperatorTakesBooleanOperand(operator) {
		// IsNull/IsBlank/IsChanged-family operators inspect the FIELD's
		// state; their right-hand operand is a boolean switch by definition
		// (runtime: executor_operator_human.go, evaluateFlowBinaryOperatorCore),
		// never a field value. Typing the operand against the field's own
		// type would reject every legitimate change/null guard on a
		// non-boolean field — e.g. {field: Website, operator: IsChanged,
		// value: {booleanValue: true}} on a string field. Nullable because
		// the flow contract lets these operators omit the operand entirely
		// (FlowRecordFilter.Value is omitempty; the runtime evaluators ignore
		// a missing RHS for the fixed-positive forms) — an omitted operand
		// types as nil and must stay accepted.
		c.checkTypeCompatible(TypeRef{Scalar: spec.TypeBoolean, Nullable: true, Source: ValueSourceLiteral}, actual, pointer, elementName, reference)
		return
	}
	expected := typeRefFromField(field)
	if flowFilterOperatorExpectsCollection(operator) || (isMultiPicklistField(field) && actual.Collection) {
		expected.Collection = true
	}
	c.checkTypeCompatible(expected, actual, pointer, elementName, reference)
	if !flowFilterOperatorRequiresTwoBounds(operator) {
		return
	}
	if count, ok := literalListLength(value); ok && count != 2 {
		c.addDiagnostic(Diagnostic{
			Code:         "metadata_program.type_mismatch",
			Severity:     SeverityError,
			JSONPointer:  pointer,
			ElementName:  elementName,
			Reference:    reference,
			ExpectedType: "2-value collection",
			ActualType:   fmt.Sprintf("%d-value collection", count),
			Message:      fmt.Sprintf("%s %s expects exactly 2 values but got %d", reference, strings.TrimSpace(operator), count),
		})
	}
}

func (c *flowChecker) checkFieldValueCompatible(field *models.FieldMetadata, actual TypeRef, pointer, elementName, reference string) {
	expected := typeRefFromField(field)
	if isMultiPicklistField(field) && actual.Collection {
		expected.Collection = true
	}
	c.checkTypeCompatible(expected, actual, pointer, elementName, reference)
}

// flowFilterOperatorTakesBooleanOperand reports whether the operator's
// right-hand value is a boolean presence/change switch rather than a field
// value. Mirrors the runtime operator semantics (IsNull/IsBlank honor a
// boolean right-hand inversion; IsNotNull/IsNotBlank/IsChanged carry a fixed
// positive form with a boolean operand) — see
// nexuscrm-automation/executor_operator_human.go.
func flowFilterOperatorTakesBooleanOperand(operator string) bool {
	switch strings.TrimSpace(operator) {
	case "IsNull", "IsNotNull", "IsBlank", "IsNotBlank", "IsChanged":
		return true
	default:
		return false
	}
}

func flowFilterOperatorExpectsCollection(operator string) bool {
	switch strings.TrimSpace(operator) {
	case "Between", "NotBetween", "In", "NotIn":
		return true
	default:
		return false
	}
}

func flowFilterOperatorRequiresTwoBounds(operator string) bool {
	switch strings.TrimSpace(operator) {
	case "Between", "NotBetween":
		return true
	default:
		return false
	}
}

func flowFilterOperatorIgnoresValue(operator string) bool {
	switch strings.TrimSpace(operator) {
	case "IsChanged":
		return true
	default:
		return false
	}
}

func literalListLength(value any) (int, bool) {
	switch v := value.(type) {
	case []interface{}:
		return len(v), true
	default:
		return 0, false
	}
}

func (c *flowChecker) checkFilterFormula(expr, objectName string) {
	provider, ok := c.opts.Formula.(formulaSpecRegistryProvider)
	if !ok {
		return
	}
	var schema *models.ObjectMetadata
	if strings.TrimSpace(objectName) != "" {
		if resolved, found := c.requireObject(objectName, "/start/object", "start"); found {
			schema = resolved
		}
	}
	extraSchemas, ok := c.effectiveSchemasByObject()
	if !ok {
		return
	}
	eng := formula.NewEngine(formula.WithFunctionRegistry(provider.SpecRegistry()))
	if err := eng.ValidateFilterFormulaExpressionWithSchemas(expr, schema, extraSchemas); err != nil {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_formula",
			Severity:    SeverityError,
			JSONPointer: "/start/filterFormula",
			ElementName: "start",
			Reference:   "start.filterFormula",
			Message:     fmt.Sprintf("start.filterFormula is invalid: %v", err),
		})
	}
}

func (c *flowChecker) checkFormulas() {
	if len(c.meta.Formulas) == 0 {
		return
	}
	provider, ok := c.opts.Formula.(formulaSpecRegistryProvider)
	if !ok {
		return
	}
	var schema *models.ObjectMetadata
	if c.opts.Metadata != nil && strings.TrimSpace(c.startObject) != "" {
		if resolved, found := c.requireObject(c.startObject, "/start/object", "start"); found {
			schema = resolved
		}
	}
	var extraSchemas map[string]*models.ObjectMetadata
	if c.opts.Metadata != nil {
		var ok bool
		extraSchemas, ok = c.effectiveSchemasByObject()
		if !ok {
			return
		}
	}
	eng := formula.NewEngine(formula.WithFunctionRegistry(provider.SpecRegistry()))
	scope := c.formulaExpressionScope()
	// Formula resources are declarations, not sequential assignments. Seed the
	// complete declared type scope before checking any expression so forward
	// references have the same semantics as backward references.
	for _, f := range c.meta.Formulas {
		name := strings.TrimSpace(f.Name)
		if name == "" {
			continue
		}
		declared := typeRefFromFlowFormula(f)
		c.variables[name] = declared
		scope[name] = expressionVariableTypeFromTypeRef(declared)
	}
	c.checkFormulaDependencyGraph()
	for i, f := range c.meta.Formulas {
		name := strings.TrimSpace(f.Name)
		if name == "" {
			continue
		}
		declared := typeRefFromFlowFormula(f)
		c.variables[name] = declared
		expr := strings.TrimSpace(f.Expression)
		pointer := fmt.Sprintf("/formulas/%d/expression", i)
		if expr == "" {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.invalid_formula",
				Severity:    SeverityError,
				JSONPointer: pointer,
				ElementName: name,
				Reference:   name,
				Message:     fmt.Sprintf("formula %q has a blank expression", name),
			})
			scope[name] = expressionVariableTypeFromTypeRef(declared)
			continue
		}
		actualScalar, err := eng.InferFormulaExpressionTypeWithSchemas(expr, schema, extraSchemas, scope)
		if err != nil {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.invalid_formula",
				Severity:    SeverityError,
				JSONPointer: pointer,
				ElementName: name,
				Reference:   name,
				Message:     fmt.Sprintf("formula %q is invalid: %v", name, err),
			})
			scope[name] = expressionVariableTypeFromTypeRef(declared)
			continue
		}
		actual := TypeRef{Scalar: actualScalar, Nullable: true, Source: ValueSourceFormulaExpression}
		c.checkTypeCompatible(declared, actual, pointer, name, name)
		if declared.normalizedScalar() == spec.TypeUnknown || declared.normalizedScalar() == spec.TypeAny {
			declared.Scalar = actualScalar
			c.variables[name] = declared
		}
		scope[name] = expressionVariableTypeFromTypeRef(c.variables[name])
	}
}

func (c *flowChecker) checkFormulaDependencyGraph() {
	formulaNames := make(map[string]struct{}, len(c.meta.Formulas))
	formulaIndex := make(map[string]int, len(c.meta.Formulas))
	for i, resource := range c.meta.Formulas {
		name := strings.TrimSpace(resource.Name)
		if name == "" {
			continue
		}
		formulaNames[name] = struct{}{}
		if _, exists := formulaIndex[name]; !exists {
			formulaIndex[name] = i
		}
	}
	edges := make(map[string][]string, len(formulaNames))
	for _, resource := range c.meta.Formulas {
		name := strings.TrimSpace(resource.Name)
		if name == "" {
			continue
		}
		identifiers, err := formula.CollectExpressionIdentifiers(resource.Expression)
		if err != nil {
			// checkFormulas owns the syntax diagnostic.
			continue
		}
		for _, identifier := range identifiers {
			if _, ok := formulaNames[identifier]; ok {
				edges[name] = append(edges[name], identifier)
			}
		}
	}
	const (
		unvisited = iota
		visiting
		visited
	)
	state := map[string]int{}
	stack := make([]string, 0, len(formulaNames))
	reported := map[string]struct{}{}
	var visit func(string)
	visit = func(name string) {
		state[name] = visiting
		stack = append(stack, name)
		for _, dependency := range edges[name] {
			switch state[dependency] {
			case unvisited:
				visit(dependency)
			case visiting:
				start := 0
				for i := range stack {
					if stack[i] == dependency {
						start = i
						break
					}
				}
				cycle := append(append([]string(nil), stack[start:]...), dependency)
				key := strings.Join(cycle, " -> ")
				if _, ok := reported[key]; ok {
					continue
				}
				reported[key] = struct{}{}
				index := formulaIndex[name]
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.circular_formula_resource",
					Severity:    SeverityError,
					JSONPointer: fmt.Sprintf("/formulas/%d/expression", index),
					ElementName: name,
					Reference:   dependency,
					Message:     fmt.Sprintf("formula resource dependency is circular: %s", key),
				})
			}
		}
		stack = stack[:len(stack)-1]
		state[name] = visited
	}
	for name := range formulaNames {
		if state[name] == unvisited {
			visit(name)
		}
	}
}

func (c *flowChecker) preloadFormulaExpressionScopeTypes() {
	c.preloadRecordLookupOutputTypes()
	c.preloadActionOutputTypes()
	c.preloadLoopIteratorTypes()
	c.preloadAssignmentTargetTypes()
	c.preloadLoopIteratorTypes()
}

func (c *flowChecker) preloadRecordLookupOutputTypes() {
	for _, lookup := range c.meta.RecordLookups {
		objectName := strings.TrimSpace(lookup.Object)
		if objectName == "" {
			continue
		}
		lookupType := recordLookupResultType(lookup)
		c.refineImplicitVariableRoot(lookup.Name, lookupType)
		c.refineOutputReferenceRoot(lookup.OutputReference, lookupType)
		if !lookup.GetFirstRecordOnly {
			continue
		}
		for _, out := range lookup.OutputAssignments {
			target := normalizeFlowReference(out.AssignToReference)
			if target == "" || strings.Contains(target, ".") {
				continue
			}
			field, ok := c.lookupFieldSilently(objectName, out.Field)
			if !ok {
				continue
			}
			c.refineImplicitVariableRoot(target, typeRefFromField(field))
		}
	}
}

func recordLookupResultType(lookup models.FlowRecordLookup) TypeRef {
	return TypeRef{
		Scalar:        spec.TypeObject,
		Collection:    !lookup.GetFirstRecordOnly,
		ObjectAPIName: strings.TrimSpace(lookup.Object),
		Nullable:      true,
		Source:        ValueSourceVariable,
	}
}

func (c *flowChecker) preloadActionOutputTypes() {
	for _, action := range c.meta.ActionCalls {
		if target, objectName := flowPreviewRecordUpdateOutputTarget(action); target != "" {
			c.refineOutputReferenceRoot(target, TypeRef{Scalar: spec.TypeObject, ObjectAPIName: objectName, Nullable: true, Source: ValueSourceActionOutput})
		}
		if c.opts.Actions == nil || !flowActionWritesOutputParameters(action.ActionType) {
			continue
		}
		_, sig, ok, err := c.resolveActionSignature(action)
		if err != nil || !ok || sig.SkipParameterValidation {
			continue
		}
		outputs := actionSchemaFieldsByName(sig.Outputs)
		for _, param := range action.OutputParameters {
			name := strings.TrimSpace(param.Name)
			if name == "" {
				continue
			}
			field, ok := outputs[actionSchemaFieldKey(name)]
			if !ok {
				continue
			}
			target := flowActionOutputTarget(param.Value)
			if target == "" || strings.Contains(normalizeFlowReference(target), ".") {
				continue
			}
			if _, _, collides := c.scopedLoopIteratorRootWrite(target, action.Name); collides {
				continue
			}
			c.refineOutputReferenceRoot(target, typeRefFromActionSchemaField(field, ValueSourceActionOutput))
		}
	}
}

func (c *flowChecker) preloadAssignmentTargetTypes() {
	for i, assignment := range c.meta.Assignments {
		c.withElementScope(assignment.Name, func() {
			for j, item := range assignment.Items {
				target := normalizeFlowReference(item.AssignToReference)
				if target == "" || strings.HasPrefix(target, "$") || strings.HasPrefix(target, "__") || strings.Contains(target, ".") {
					continue
				}
				if _, _, collides := c.scopedLoopIteratorRootWrite(target, assignment.Name); collides {
					continue
				}
				actual, ok := c.typeRefFromRuntimeValue(item.Value, fmt.Sprintf("/assignments/%d/items/%d/value", i, j), assignment.Name)
				if !ok {
					continue
				}
				switch item.Operator {
				case "", "Assign":
					c.refineImplicitVariableRoot(target, actual)
				case "Add", "AddAtStart":
					actual.Collection = true
					c.refineImplicitVariableRoot(target, actual)
				case "RemoveFirst", "RemoveAll", "RemoveAfterFirst", "RemoveBeforeFirst":
					actual.Collection = true
					c.refineImplicitVariableRoot(target, actual)
				}
			}
		})
	}
}

func (c *flowChecker) preloadLoopIteratorTypes() {
	for _, loop := range c.meta.Loops {
		iterVar := flowLoopIteratorReference(loop)
		if iterVar == "" {
			continue
		}
		root, ok := flowOutputReferenceRoot(iterVar)
		if !ok || strings.HasPrefix(root, "$") {
			continue
		}
		itemType, ok := c.loopIteratorType(loop)
		if !ok {
			continue
		}
		if flowLooseType(itemType) {
			continue
		}
		c.refineLoopIteratorReferenceRoot(iterVar, itemType)
	}
}

func flowLoopIteratorReference(loop models.FlowLoop) string {
	iterVar := strings.TrimSpace(loop.AssignNextValueToReference)
	if iterVar == "" {
		iterVar = strings.TrimSpace(loop.Name)
	}
	return iterVar
}

func (c *flowChecker) resolveLoopCollectionReference(ref, pointer, elementName string) (TypeRef, bool) {
	if flowReferenceWasMergeWrapped(ref) {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.invalid_reference",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("loop collectionReference %q must be a runtime reference without merge syntax", ref),
		})
		return TypeRef{}, false
	}
	if typ, ok := c.resolveLooseDottedLoopCollectionReference(ref); ok {
		return typ, true
	}
	return c.resolveReference(ref, pointer, elementName)
}

func (c *flowChecker) resolveLoopCollectionReferenceSilently(ref string) (TypeRef, bool) {
	if flowReferenceWasMergeWrapped(ref) {
		return TypeRef{}, false
	}
	if typ, ok := c.resolveLooseDottedLoopCollectionReference(ref); ok {
		return typ, true
	}
	return c.resolveReferenceSilently(ref)
}

func (c *flowChecker) resolveLooseDottedLoopCollectionReference(ref string) (TypeRef, bool) {
	ref = normalizeFlowReference(ref)
	if ref == "" || strings.Count(ref, ".") != 1 {
		return TypeRef{}, false
	}
	root, leaf, ok := strings.Cut(ref, ".")
	if !ok || strings.TrimSpace(root) == "" || strings.TrimSpace(leaf) == "" {
		return TypeRef{}, false
	}
	root = strings.TrimSpace(root)
	typ, ok := c.variables[root]
	if !ok || typ.Collection {
		return TypeRef{}, false
	}
	scalar := typ.normalizedScalar()
	if scalar == spec.TypeAny || scalar == spec.TypeUnknown || (scalar == spec.TypeObject && strings.TrimSpace(typ.ObjectAPIName) == "") {
		return TypeRef{Scalar: spec.TypeAny, Nullable: true, Source: typ.Source}, true
	}
	return TypeRef{}, false
}

func (c *flowChecker) loopIteratorType(loop models.FlowLoop) (TypeRef, bool) {
	collection, ok := c.resolveLoopCollectionReferenceSilently(loop.CollectionReference)
	if !ok {
		return TypeRef{}, false
	}
	itemType := TypeRef{Scalar: spec.TypeAny, Nullable: true, Source: ValueSourceLoopIterator}
	if collection.Collection {
		if collection.normalizedScalar() == spec.TypeObject && strings.TrimSpace(collection.ObjectAPIName) == "" {
			return itemType, true
		}
		itemType = collection
		itemType.Collection = false
		itemType.Source = ValueSourceLoopIterator
		return itemType, true
	}
	if flowLooseType(collection) {
		return itemType, true
	}
	return TypeRef{}, false
}

func (c *flowChecker) withElementScope(elementName string, fn func()) {
	scoped := c.loopIteratorTypesForElement(elementName)
	if len(scoped) == 0 {
		fn()
		return
	}
	c.withLoopIteratorTypeOverlay(scoped, fn)
}

func (c *flowChecker) withLoopIteratorTypeOverlay(scoped map[string]TypeRef, fn func()) {
	type savedType struct {
		typ    TypeRef
		exists bool
	}
	saved := map[string]savedType{}
	for root, typ := range scoped {
		existing, exists := c.variables[root]
		if exists && (c.flowVariableDeclared(root) || existing.Source == ValueSourceFormulaExpression) {
			continue
		}
		saved[root] = savedType{typ: existing, exists: exists}
		c.variables[root] = typ
	}
	fn()
	for root, prev := range saved {
		if prev.exists {
			c.variables[root] = prev.typ
			continue
		}
		delete(c.variables, root)
	}
}

func (c *flowChecker) loopIteratorTypesForElement(elementName string) map[string]TypeRef {
	elementName = strings.TrimSpace(elementName)
	if elementName == "" {
		return nil
	}
	out := map[string]TypeRef{}
	for {
		changed := false
		for _, loop := range c.meta.Loops {
			if !c.loopBodyContainsElement(loop, elementName) {
				continue
			}
			iterVar := flowLoopIteratorReference(loop)
			root, ok := flowOutputReferenceRoot(iterVar)
			if !ok || !flowOutputReferenceRootIsDeclarable(root) {
				continue
			}
			if _, exists := out[root]; exists {
				continue
			}
			var itemType TypeRef
			c.withLoopIteratorTypeOverlay(out, func() {
				itemType, ok = c.loopIteratorType(loop)
			})
			if !ok {
				continue
			}
			out[root] = itemType
			changed = true
		}
		if !changed {
			break
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func (c *flowChecker) loopBodyContainsElement(loop models.FlowLoop, elementName string) bool {
	elementName = strings.TrimSpace(elementName)
	loopName := strings.TrimSpace(loop.Name)
	start := flowConnectorTarget(loop.NextValueConnector)
	if elementName == "" || loopName == "" || start == "" || elementName == loopName {
		return false
	}
	exitTarget := flowConnectorTarget(loop.NoMoreValuesConnector)
	graph := c.flowElementOutgoingConnectors()
	visited := map[string]struct{}{}
	body := map[string]struct{}{}
	queue := []string{start}
	for len(queue) > 0 {
		current := strings.TrimSpace(queue[0])
		queue = queue[1:]
		if current == "" {
			continue
		}
		if _, ok := visited[current]; ok {
			continue
		}
		visited[current] = struct{}{}
		if current == loopName || (exitTarget != "" && current == exitTarget) {
			continue
		}
		body[current] = struct{}{}
		queue = append(queue, graph[current]...)
	}
	if _, ok := body[elementName]; !ok {
		return false
	}
	return c.loopBodyExclusiveElements(loop, body)[elementName]
}

func (c *flowChecker) loopBodyExclusiveElements(loop models.FlowLoop, body map[string]struct{}) map[string]bool {
	loopName := strings.TrimSpace(loop.Name)
	nextTarget := flowConnectorTarget(loop.NextValueConnector)
	incoming := c.flowElementIncomingConnectors()
	exclusive := map[string]bool{}
	for element := range body {
		exclusive[element] = true
	}
	changed := true
	for changed {
		changed = false
		for element := range body {
			if !exclusive[element] {
				continue
			}
			for _, source := range incoming[element] {
				if source == loopName && element == nextTarget {
					continue
				}
				if exclusive[source] {
					continue
				}
				exclusive[element] = false
				changed = true
				break
			}
		}
	}
	return exclusive
}

func (c *flowChecker) flowElementOutgoingConnectors() map[string][]string {
	graph := map[string][]string{}
	add := func(name string, connectors ...*models.FlowConnector) {
		name = strings.TrimSpace(name)
		if name == "" {
			return
		}
		for _, connector := range connectors {
			if target := flowConnectorTarget(connector); target != "" {
				graph[name] = append(graph[name], target)
			}
		}
	}
	for _, assignment := range c.meta.Assignments {
		add(assignment.Name, assignment.Connector)
	}
	for _, create := range c.meta.RecordCreates {
		add(create.Name, create.Connector, create.FaultConnector)
	}
	for _, update := range c.meta.RecordUpdates {
		add(update.Name, update.Connector, update.FaultConnector)
	}
	for _, del := range c.meta.RecordDeletes {
		add(del.Name, del.Connector, del.FaultConnector)
	}
	for _, lookup := range c.meta.RecordLookups {
		add(lookup.Name, lookup.Connector, lookup.FaultConnector)
	}
	for _, action := range c.meta.ActionCalls {
		add(action.Name, action.Connector, action.FaultConnector)
	}
	for _, subflow := range c.meta.Subflows {
		add(subflow.Name, subflow.Connector, subflow.FaultConnector)
	}
	for _, loop := range c.meta.Loops {
		add(loop.Name, loop.NextValueConnector, loop.NoMoreValuesConnector)
	}
	for _, decision := range c.meta.Decisions {
		connectors := []*models.FlowConnector{decision.DefaultConnector}
		for _, rule := range decision.Rules {
			connectors = append(connectors, rule.Connector)
		}
		add(decision.Name, connectors...)
	}
	for _, screen := range c.meta.Screens {
		add(screen.Name, screen.Connector)
	}
	for _, customError := range c.meta.CustomErrors {
		add(customError.Name, customError.Connector)
	}
	for _, wait := range c.meta.Waits {
		connectors := []*models.FlowConnector{wait.DefaultConnector, wait.FaultConnector}
		for _, event := range wait.WaitEvents {
			connectors = append(connectors, event.Connector)
		}
		add(wait.Name, connectors...)
	}
	return graph
}

func (c *flowChecker) flowElementIncomingConnectors() map[string][]string {
	incoming := map[string][]string{}
	add := func(source string, connector *models.FlowConnector) {
		source = strings.TrimSpace(source)
		target := flowConnectorTarget(connector)
		if source == "" || target == "" {
			return
		}
		incoming[target] = append(incoming[target], source)
	}
	for source, targets := range c.flowElementOutgoingConnectors() {
		for _, target := range targets {
			if target != "" {
				incoming[target] = append(incoming[target], source)
			}
		}
	}
	if c.meta.Start != nil && flowConnectorTarget(c.meta.Start.Connector) != "" {
		add("__start", c.meta.Start.Connector)
		return incoming
	}
	if target := strings.TrimSpace(c.meta.StartElementReference); target != "" {
		incoming[target] = append(incoming[target], "__start")
		return incoming
	}
	if c.meta.Start != nil {
		for _, path := range c.meta.Start.ScheduledPaths {
			add("__start", path.Connector)
			if flowConnectorTarget(path.Connector) != "" {
				break
			}
		}
	}
	return incoming
}

func flowConnectorTarget(connector *models.FlowConnector) string {
	if connector == nil {
		return ""
	}
	return strings.TrimSpace(connector.TargetReference)
}

func (c *flowChecker) refineLoopIteratorReferenceRoot(ref string, typ TypeRef) {
	root, ok := flowOutputReferenceRoot(ref)
	if !ok || !flowOutputReferenceRootIsDeclarable(root) {
		return
	}
	existing, exists := c.variables[root]
	if !exists {
		c.variables[root] = typ
		return
	}
	if c.flowVariableDeclared(root) || existing.Source == ValueSourceFormulaExpression {
		return
	}
	c.variables[root] = typ
}

func flowLooseType(typ TypeRef) bool {
	scalar := typ.normalizedScalar()
	return scalar == spec.TypeAny || scalar == spec.TypeUnknown
}

func (c *flowChecker) typeRefFromStaticValue(value any) (TypeRef, bool) {
	switch v := value.(type) {
	case nil:
		return TypeRef{Scalar: spec.TypeNil, Nullable: true, Source: ValueSourceLiteral}, true
	case bool:
		return TypeRef{Scalar: spec.TypeBoolean, Source: ValueSourceLiteral}, true
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, json.Number:
		return TypeRef{Scalar: spec.TypeNumber, Source: ValueSourceLiteral}, true
	case string:
		if looksLikeFlowReference(v) {
			return c.resolveReferenceSilently(v)
		}
		return TypeRef{Scalar: spec.TypeString, Source: ValueSourceLiteral}, true
	case []interface{}:
		return c.typeRefFromStaticValueList(v)
	case map[string]interface{}:
		return c.typeRefFromStaticValueMap(v)
	default:
		return TypeRef{}, false
	}
}

func (c *flowChecker) typeRefFromStaticValueList(values []interface{}) (TypeRef, bool) {
	return c.typeRefFromValueList(values, "", "", false, false)
}

func (c *flowChecker) typeRefFromStaticValueMap(value map[string]any) (TypeRef, bool) {
	if raw, ok := value["elementReference"]; ok {
		ref, _ := raw.(string)
		return c.resolveReferenceSilently(ref)
	}
	if _, ok := value["stringValue"]; ok {
		return TypeRef{Scalar: spec.TypeString, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["numberValue"]; ok {
		return TypeRef{Scalar: spec.TypeNumber, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["booleanValue"]; ok {
		return TypeRef{Scalar: spec.TypeBoolean, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["dateValue"]; ok {
		return TypeRef{Scalar: spec.TypeDate, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["dateTimeValue"]; ok {
		return TypeRef{Scalar: spec.TypeDatetime, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["datetimeValue"]; ok {
		return TypeRef{Scalar: spec.TypeDatetime, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["nullValue"]; ok {
		return TypeRef{Scalar: spec.TypeNil, Nullable: true, Source: ValueSourceLiteral}, true
	}
	return TypeRef{}, false
}

func (c *flowChecker) resolveReferenceSilently(ref string) (TypeRef, bool) {
	ref = normalizeFlowReference(ref)
	if ref == "" {
		return TypeRef{}, false
	}
	parts := strings.Split(ref, ".")
	root := parts[0]
	switch strings.ToLower(root) {
	case "$record", "__record", "$record__old", "__record__old", "$record__prior", "__record__prior":
		if c.startObject == "" {
			if c.hasConnectorSourceRecord() && strings.EqualFold(root, "$record") {
				return TypeRef{Scalar: spec.TypeAny, Nullable: true, Source: ValueSourceFieldReference}, true
			}
			return TypeRef{}, false
		}
		if len(parts) == 1 {
			return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: c.startObject, Nullable: true, Source: ValueSourceFieldReference}, true
		}
		return c.resolveObjectPathSilently(c.startObject, parts[1:])
	}
	if typ, ok := c.variables[root]; ok {
		if len(parts) == 1 {
			return typ, true
		}
		if typ.Scalar == spec.TypeUnknown || typ.Scalar == spec.TypeAny {
			return typ, true
		}
		if typ.Scalar != spec.TypeObject || typ.Collection || strings.TrimSpace(typ.ObjectAPIName) == "" {
			return TypeRef{}, false
		}
		return c.resolveObjectPathSilently(typ.ObjectAPIName, parts[1:])
	}
	return TypeRef{}, false
}

func (c *flowChecker) resolveObjectPathSilently(objectName string, path []string) (TypeRef, bool) {
	if len(path) == 0 {
		return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: objectName, Nullable: true, Source: ValueSourceFieldReference}, true
	}
	currentObject := strings.TrimSpace(objectName)
	for i, segment := range path {
		segment = strings.TrimSpace(segment)
		if segment == "" {
			return TypeRef{}, false
		}
		if i == len(path)-1 {
			field, ok := c.lookupFieldSilently(currentObject, segment)
			if !ok {
				return TypeRef{}, false
			}
			return typeRefFromField(field), true
		}
		nextObject, ok := c.resolveRelationshipTargetSilently(currentObject, segment)
		if !ok {
			return TypeRef{}, false
		}
		currentObject = nextObject
	}
	return TypeRef{}, false
}

func (c *flowChecker) resolveRelationshipTargetSilently(objectName, relationshipName string) (string, bool) {
	if c.opts.Metadata == nil {
		return "", false
	}
	fk, ok, err := c.opts.Metadata.ForeignKeyForRelationship(c.ctx, objectName, relationshipName)
	if err != nil || !ok {
		return "", false
	}
	targets, ok, err := c.opts.Metadata.TargetObjectsForForeignKey(c.ctx, objectName, fk)
	if err != nil || !ok || len(targets) != 1 {
		return "", false
	}
	return targets[0], true
}

func (c *flowChecker) lookupFieldSilently(objectName, fieldName string) (*models.FieldMetadata, bool) {
	objectName = strings.TrimSpace(objectName)
	fieldName = strings.TrimSpace(fieldName)
	if objectName == "" || fieldName == "" || c.opts.Metadata == nil {
		return nil, false
	}
	field, ok, err := c.opts.Metadata.Field(c.ctx, objectName, fieldName)
	if err != nil || !ok || field == nil {
		return nil, false
	}
	return field, true
}

func (c *flowChecker) refineImplicitVariableRoot(ref string, typ TypeRef) {
	root := flowReferenceRoot(ref)
	if !flowReferenceRootIsDeclarable(root) {
		return
	}
	existing, exists := c.variables[root]
	if !exists {
		c.variables[root] = typ
		return
	}
	if c.flowVariableDeclared(root) || existing.Source == ValueSourceFormulaExpression {
		return
	}
	scalar := existing.normalizedScalar()
	if scalar == spec.TypeUnknown || scalar == spec.TypeAny || existing.Source == typ.Source {
		c.variables[root] = typ
	}
}

func (c *flowChecker) refineOutputReferenceRoot(ref string, typ TypeRef) {
	root, ok := flowOutputReferenceRoot(ref)
	if !ok || !flowOutputReferenceRootIsDeclarable(root) {
		return
	}
	existing, exists := c.variables[root]
	if !exists {
		c.variables[root] = typ
		return
	}
	if c.flowVariableDeclared(root) {
		return
	}
	if flowPreservesConcreteOutputRoot(existing, typ) {
		return
	}
	if flowOutputReferenceTypesCompatible(existing, typ) {
		c.variables[root] = typ
	}
}

func flowPreservesConcreteOutputRoot(existing, output TypeRef) bool {
	return flowLooseType(output) && !flowLooseType(existing)
}

func flowReferenceRootIsDeclarable(root string) bool {
	root = strings.TrimSpace(root)
	if root == "" || strings.HasPrefix(root, "$") || strings.HasPrefix(root, "__") {
		return false
	}
	return true
}

func flowOutputReferenceRootIsDeclarable(root string) bool {
	root = strings.TrimSpace(root)
	if root == "" || strings.HasPrefix(root, "__") {
		return false
	}
	switch strings.ToLower(root) {
	case "$record", "$record__old", "$record__prior":
		return false
	}
	if _, handled, _ := flowRuntimeGlobalType([]string{root}); handled {
		return false
	}
	return true
}

func flowOutputReferenceRoot(ref string) (string, bool) {
	if flowReferenceWasMergeWrapped(ref) {
		return "", false
	}
	ref = normalizeFlowReference(ref)
	if ref == "" || strings.Contains(ref, ".") {
		return "", false
	}
	return ref, true
}

func flowWriteReferenceRoot(ref string) (string, bool) {
	if flowReferenceWasMergeWrapped(ref) {
		return "", false
	}
	ref = normalizeFlowReference(ref)
	if ref == "" {
		return "", false
	}
	return strings.Split(ref, ".")[0], true
}

func flowOutputReferenceTypesCompatible(existing, output TypeRef) bool {
	return typeCompatible(existing, output) && typeCompatible(output, existing)
}

func (c *flowChecker) flowVariableDeclared(name string) bool {
	name = strings.TrimSpace(name)
	if name == "" {
		return false
	}
	for _, v := range c.meta.Variables {
		if strings.TrimSpace(v.Name) == name {
			return true
		}
	}
	return false
}

func (c *flowChecker) formulaExpressionScope() map[string]formula.ExpressionVariableType {
	scope := map[string]formula.ExpressionVariableType{}
	if c.hasConnectorSourceRecord() {
		scope["__Record"] = expressionVariableTypeFromTypeRefDynamic(TypeRef{
			Scalar:   spec.TypeAny,
			Nullable: true,
			Source:   ValueSourceFieldReference,
		}, true)
	}
	for _, v := range c.meta.Variables {
		name := strings.TrimSpace(v.Name)
		if name == "" {
			continue
		}
		typ := typeRefFromFlowVariable(v)
		dynamic := !flowVariableAvailableForInitialFormula(v, c.opts.ProcessType)
		scope[name] = expressionVariableTypeFromTypeRefDynamic(typ, dynamic)
	}
	for name, typ := range c.variables {
		if _, exists := scope[name]; exists {
			continue
		}
		if typ.Collection {
			scope[name] = expressionVariableTypeFromTypeRefDynamic(typ, true)
			continue
		}
		if typ.normalizedScalar() == spec.TypeUnknown {
			typ.Scalar = spec.TypeAny
		}
		scope[name] = expressionVariableTypeFromTypeRefDynamic(typ, true)
	}
	return scope
}

func flowVariableAvailableForInitialFormula(v models.FlowVariable, processType string) bool {
	if v.IsInput || v.Value != nil {
		return true
	}
	if !strings.EqualFold(strings.TrimSpace(processType), "Workflow") {
		return false
	}
	return strings.EqualFold(v.Name, "myVariable_current") || strings.EqualFold(v.Name, "myVariable_old")
}

func (c *flowChecker) effectiveSchemasByObject() (map[string]*models.ObjectMetadata, bool) {
	if c.opts.Metadata == nil {
		return nil, false
	}
	schemas, err := c.opts.Metadata.Schemas(c.ctx)
	if err != nil {
		c.readerError("/schemas", "schemas", err)
		return nil, false
	}
	out := make(map[string]*models.ObjectMetadata, len(schemas))
	for _, schema := range schemas {
		if schema == nil {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(schema.APIName))
		if key != "" {
			out[key] = schema
		}
	}
	return out, true
}

func (c *flowChecker) checkAssignableReference(ref string, actual TypeRef, pointer, elementName string) {
	expected, ok := c.resolveAssignableReference(ref, pointer, elementName)
	if !ok {
		return
	}
	c.checkTypeCompatible(expected, actual, pointer, elementName, ref)
}

func (c *flowChecker) resolveAssignableReference(ref, pointer, elementName string) (TypeRef, bool) {
	ref = normalizeFlowReference(ref)
	if ref == "" {
		return c.resolveReference(ref, pointer, elementName)
	}
	parts := strings.Split(ref, ".")
	if _, handled, _ := flowRuntimeGlobalType(parts); handled {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.read_only_reference",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("flow runtime global reference %q is read-only and cannot be assigned", ref),
		})
		return TypeRef{}, false
	}
	return c.resolveReference(ref, pointer, elementName)
}

func (c *flowChecker) resolveRuntimeValueReference(ref, pointer, elementName string) (TypeRef, bool) {
	rawRef := strings.TrimSpace(ref)
	ref = normalizeFlowReference(rawRef)
	if ref == "" {
		return c.resolveReference(ref, pointer, elementName)
	}
	parts := strings.Split(ref, ".")
	if flowReferenceWasMergeWrapped(rawRef) {
		if _, handled, _ := flowRuntimeGlobalType(parts); handled {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.missing_variable",
				Severity:    SeverityError,
				JSONPointer: pointer,
				ElementName: elementName,
				Reference:   rawRef,
				Message:     fmt.Sprintf("flow runtime global reference %q uses merge syntax, but the runtime global read path requires an unwrapped reference", rawRef),
			})
			return TypeRef{}, false
		}
	}
	if typ, handled, ok := flowRuntimeGlobalType(parts); handled {
		if !ok {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.missing_variable",
				Severity:    SeverityError,
				JSONPointer: pointer,
				ElementName: elementName,
				Reference:   ref,
				Message:     fmt.Sprintf("flow runtime global reference %q is not declared", ref),
			})
		}
		return typ, ok
	}
	return c.resolveReference(ref, pointer, elementName)
}

func (c *flowChecker) checkTypeCompatible(expected, actual TypeRef, pointer, elementName, reference string) {
	if typeCompatible(expected, actual) {
		return
	}
	c.addDiagnostic(Diagnostic{
		Code:         "metadata_program.type_mismatch",
		Severity:     SeverityError,
		JSONPointer:  pointer,
		ElementName:  elementName,
		Reference:    reference,
		ExpectedType: expected.String(),
		ActualType:   actual.String(),
		Message:      fmt.Sprintf("%s expects %s but got %s", reference, expected.String(), actual.String()),
	})
}

func (c *flowChecker) requireObject(apiName, pointer, elementName string) (*models.ObjectMetadata, bool) {
	apiName = strings.TrimSpace(apiName)
	if apiName == "" {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_object",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   apiName,
			Message:     "object reference is blank",
		})
		return nil, false
	}
	if c.opts.Metadata == nil {
		c.readerError(pointer, elementName, ErrReaderUnavailable)
		return nil, false
	}
	schema, ok, err := c.opts.Metadata.Object(c.ctx, apiName)
	if err != nil {
		c.readerError(pointer, elementName, err)
		return nil, false
	}
	if !ok || schema == nil {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_object",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   apiName,
			Message:     fmt.Sprintf("object %q does not exist", apiName),
		})
		return nil, false
	}
	return schema, true
}

func (c *flowChecker) requireField(objectName, fieldName, pointer, elementName string) (*models.FieldMetadata, bool) {
	objectName = strings.TrimSpace(objectName)
	fieldName = strings.TrimSpace(fieldName)
	if objectName == "" || fieldName == "" {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_field",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   objectName + "." + fieldName,
			Message:     "field reference is blank",
		})
		return nil, false
	}
	if c.opts.Metadata == nil {
		c.readerError(pointer, elementName, ErrReaderUnavailable)
		return nil, false
	}
	field, ok, err := c.opts.Metadata.Field(c.ctx, objectName, fieldName)
	if err != nil {
		c.readerError(pointer, elementName, err)
		return nil, false
	}
	if !ok || field == nil {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_field",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   objectName + "." + fieldName,
			Message:     fmt.Sprintf("field %q does not exist on object %q", fieldName, objectName),
		})
		return nil, false
	}
	return field, true
}

func (c *flowChecker) resolveReference(ref, pointer, elementName string) (TypeRef, bool) {
	ref = normalizeFlowReference(ref)
	if ref == "" {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_variable",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     "reference is blank",
		})
		return TypeRef{}, false
	}
	parts := strings.Split(ref, ".")
	root := parts[0]
	switch strings.ToLower(root) {
	case "$record", "__record", "$record__old", "__record__old", "$record__prior", "__record__prior":
		if c.startObject == "" {
			if c.hasConnectorSourceRecord() && strings.EqualFold(root, "$record") {
				return TypeRef{Scalar: spec.TypeAny, Nullable: true, Source: ValueSourceFieldReference}, true
			}
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.missing_object",
				Severity:    SeverityError,
				JSONPointer: pointer,
				ElementName: elementName,
				Reference:   ref,
				Message:     fmt.Sprintf("%s requires a Flow start object", ref),
			})
			return TypeRef{}, false
		}
		if len(parts) == 1 {
			return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: c.startObject, Nullable: true, Source: ValueSourceFieldReference}, true
		}
		return c.resolveObjectPath(c.startObject, parts[1:], pointer, elementName, ref)
	}
	if typ, ok := c.variables[root]; ok {
		if len(parts) == 1 {
			return typ, true
		}
		if typ.Scalar == spec.TypeUnknown || typ.Scalar == spec.TypeAny {
			return typ, true
		}
		if typ.Collection {
			c.addDiagnostic(Diagnostic{
				Code:         "metadata_program.type_mismatch",
				Severity:     SeverityError,
				JSONPointer:  pointer,
				ElementName:  elementName,
				Reference:    ref,
				ExpectedType: "object",
				ActualType:   typ.String(),
				Message:      fmt.Sprintf("reference %q dereferences collection variable %q as a scalar object", ref, root),
			})
			return TypeRef{}, false
		}
		if typ.Scalar != spec.TypeObject || strings.TrimSpace(typ.ObjectAPIName) == "" {
			c.addDiagnostic(Diagnostic{
				Code:         "metadata_program.type_mismatch",
				Severity:     SeverityError,
				JSONPointer:  pointer,
				ElementName:  elementName,
				Reference:    ref,
				ExpectedType: "object",
				ActualType:   typ.String(),
				Message:      fmt.Sprintf("reference %q dereferences non-object variable %q", ref, root),
			})
			return TypeRef{}, false
		}
		return c.resolveObjectPath(typ.ObjectAPIName, parts[1:], pointer, elementName, ref)
	}
	if c.startObject != "" {
		return c.resolveObjectPath(c.startObject, parts, pointer, elementName, ref)
	}
	c.addDiagnostic(Diagnostic{
		Code:        "metadata_program.missing_variable",
		Severity:    SeverityError,
		JSONPointer: pointer,
		ElementName: elementName,
		Reference:   ref,
		Message:     fmt.Sprintf("variable or field reference %q is not declared", ref),
	})
	return TypeRef{}, false
}

func flowRuntimeGlobalType(parts []string) (TypeRef, bool, bool) {
	if len(parts) == 0 {
		return TypeRef{}, false, false
	}
	root := strings.TrimSpace(parts[0])
	switch root {
	case "$Flow":
		return flowSystemVariableType(parts)
	case "$User":
		return flowSessionGlobalType("__User", parts)
	case "$Profile":
		return flowSessionGlobalType("__Profile", parts)
	case "$UserRole":
		return flowSessionGlobalType("__UserRole", parts)
	case "$GlobalConstant":
		return flowGlobalConstantType(parts)
	case "$SystemContext", constants.FormulaContextSystemContext:
		return flowSystemContextType(parts)
	default:
		return TypeRef{}, false, false
	}
}

func flowSystemVariableType(parts []string) (TypeRef, bool, bool) {
	if len(parts) != 2 {
		return TypeRef{}, true, false
	}
	switch parts[1] {
	case "CurrentDate":
		return TypeRef{Scalar: spec.TypeDate, Nullable: true, Source: ValueSourceVariable}, true, true
	case "CurrentDateTime":
		return TypeRef{Scalar: spec.TypeDatetime, Nullable: true, Source: ValueSourceVariable}, true, true
	default:
		return TypeRef{}, true, false
	}
}

func flowSessionGlobalType(globalName string, parts []string) (TypeRef, bool, bool) {
	if len(parts) != 2 {
		return TypeRef{}, true, false
	}
	if !flowSessionGlobalFieldAllowed(globalName, parts[1]) {
		return TypeRef{}, true, false
	}
	return TypeRef{Scalar: spec.TypeString, Nullable: true, Source: ValueSourceVariable}, true, true
}

func flowSessionGlobalFieldAllowed(globalName, fieldName string) bool {
	switch globalName {
	case "__User":
		switch fieldName {
		case "Id", "ID", "Name", "Email", "ProfileId", "UserRoleId":
			return true
		}
	case "__Profile", "__UserRole":
		switch fieldName {
		case "Id", "Name":
			return true
		}
	}
	return false
}

func flowGlobalConstantType(parts []string) (TypeRef, bool, bool) {
	if len(parts) != 2 {
		return TypeRef{}, true, false
	}
	switch parts[1] {
	case "True", "False":
		return TypeRef{Scalar: spec.TypeBoolean, Source: ValueSourceVariable}, true, true
	case "EmptyString":
		return TypeRef{Scalar: spec.TypeString, Source: ValueSourceVariable}, true, true
	default:
		return TypeRef{}, true, false
	}
}

func flowSystemContextType(parts []string) (TypeRef, bool, bool) {
	if len(parts) == 1 {
		return TypeRef{}, true, false
	}
	if len(parts) != 2 {
		return TypeRef{}, true, false
	}
	switch parts[1] {
	case constants.FormulaSystemContextIsDerivedRecompute:
		return TypeRef{Scalar: spec.TypeBoolean, Source: ValueSourceVariable}, true, true
	case constants.FormulaSystemContextConnectorSyncOrigin:
		return TypeRef{Scalar: spec.TypeString, Nullable: true, Source: ValueSourceVariable}, true, true
	default:
		return TypeRef{}, true, false
	}
}

func flowRuntimeGlobalReferenceFromValue(value any) (string, bool) {
	switch v := value.(type) {
	case string:
		ref := normalizeFlowReference(v)
		if ref == "" {
			return "", false
		}
		if _, handled, _ := flowRuntimeGlobalType(strings.Split(ref, ".")); handled {
			return ref, true
		}
	case []interface{}:
		for _, item := range v {
			if ref, ok := flowRuntimeGlobalReferenceFromValue(item); ok {
				return ref, true
			}
		}
	case map[string]interface{}:
		if raw, ok := v["elementReference"]; ok {
			ref, _ := raw.(string)
			ref = normalizeFlowReference(ref)
			if ref != "" {
				if _, handled, _ := flowRuntimeGlobalType(strings.Split(ref, ".")); handled {
					return ref, true
				}
			}
		}
	}
	return "", false
}

func (c *flowChecker) resolveObjectPath(objectName string, path []string, pointer, elementName, ref string) (TypeRef, bool) {
	if len(path) == 0 {
		return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: objectName, Nullable: true, Source: ValueSourceFieldReference}, true
	}
	currentObject := objectName
	for i, segment := range path {
		segment = strings.TrimSpace(segment)
		if segment == "" {
			return TypeRef{}, false
		}
		if i == len(path)-1 {
			field, ok := c.requireField(currentObject, segment, pointer, elementName)
			if !ok {
				return TypeRef{}, false
			}
			return typeRefFromField(field), true
		}
		nextObject, ok := c.resolveRelationshipTarget(currentObject, segment, pointer, elementName, ref)
		if !ok {
			return TypeRef{}, false
		}
		currentObject = nextObject
	}
	return TypeRef{}, false
}

func (c *flowChecker) resolveRelationshipTarget(objectName, relationshipName, pointer, elementName, ref string) (string, bool) {
	if c.opts.Metadata == nil {
		c.readerError(pointer, elementName, ErrReaderUnavailable)
		return "", false
	}
	fk, ok, err := c.opts.Metadata.ForeignKeyForRelationship(c.ctx, objectName, relationshipName)
	if err != nil {
		c.readerError(pointer, elementName, err)
		return "", false
	}
	if !ok {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_relationship",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("relationship %q does not exist on object %q", relationshipName, objectName),
		})
		return "", false
	}
	targets, ok, err := c.opts.Metadata.TargetObjectsForForeignKey(c.ctx, objectName, fk)
	if err != nil {
		c.readerError(pointer, elementName, err)
		return "", false
	}
	if !ok || len(targets) == 0 {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_relationship",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("relationship %q on object %q has no target object", relationshipName, objectName),
		})
		return "", false
	}
	if len(targets) > 1 {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.polymorphic_relationship",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("relationship %q on object %q is polymorphic and cannot be statically resolved", relationshipName, objectName),
		})
		return "", false
	}
	return targets[0], true
}

func (c *flowChecker) typeRefFromValue(value any, pointer, elementName string) (TypeRef, bool) {
	return c.typeRefFromValueWithRuntimeGlobals(value, pointer, elementName, false)
}

func (c *flowChecker) typeRefFromRuntimeValue(value any, pointer, elementName string) (TypeRef, bool) {
	return c.typeRefFromValueWithRuntimeGlobals(value, pointer, elementName, true)
}

func (c *flowChecker) typeRefFromValueWithRuntimeGlobals(value any, pointer, elementName string, allowRuntimeGlobals bool) (TypeRef, bool) {
	switch v := value.(type) {
	case nil:
		return TypeRef{Scalar: spec.TypeNil, Nullable: true, Source: ValueSourceLiteral}, true
	case bool:
		return TypeRef{Scalar: spec.TypeBoolean, Source: ValueSourceLiteral}, true
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, json.Number:
		return TypeRef{Scalar: spec.TypeNumber, Source: ValueSourceLiteral}, true
	case string:
		if looksLikeFlowReference(v) {
			if allowRuntimeGlobals {
				return c.resolveRuntimeValueReference(v, pointer, elementName)
			}
			return c.resolveReference(v, pointer, elementName)
		}
		return TypeRef{Scalar: spec.TypeString, Source: ValueSourceLiteral}, true
	case []interface{}:
		return c.typeRefFromValueList(v, pointer, elementName, true, false)
	case map[string]interface{}:
		return c.typeRefFromValueMap(v, pointer, elementName, allowRuntimeGlobals)
	default:
		return TypeRef{Scalar: spec.TypeAny, Source: ValueSourceLiteral}, true
	}
}

func (c *flowChecker) typeRefFromValueList(values []interface{}, pointer, elementName string, diagnose, allowRuntimeGlobals bool) (TypeRef, bool) {
	out := TypeRef{Scalar: spec.TypeAny, Collection: true, Nullable: true, Source: ValueSourceLiteral}
	var element TypeRef
	haveElement := false
	for i, value := range values {
		typ, ok := c.typeRefFromListElement(value, fmt.Sprintf("%s/%d", pointer, i), elementName, diagnose, allowRuntimeGlobals)
		if !ok {
			continue
		}
		if typ.Collection {
			out.NestedCollection = true
		}
		if !haveElement {
			element = typ
			haveElement = true
			continue
		}
		if !typeCompatible(element, typ) || !typeCompatible(typ, element) {
			element = TypeRef{Scalar: spec.TypeAny, Source: ValueSourceLiteral}
		}
	}
	if haveElement {
		out.Scalar = element.normalizedScalar()
		out.ObjectAPIName = element.ObjectAPIName
	}
	return out, true
}

func (c *flowChecker) typeRefFromListElement(value any, pointer, elementName string, diagnose, allowRuntimeGlobals bool) (TypeRef, bool) {
	switch v := value.(type) {
	case string:
		return TypeRef{Scalar: spec.TypeString, Source: ValueSourceLiteral}, true
	case []interface{}:
		return c.typeRefFromValueList(v, pointer, elementName, diagnose, false)
	case map[string]interface{}:
		if diagnose {
			return c.typeRefFromValueMap(v, pointer, elementName, false)
		}
		return c.typeRefFromStaticValueMap(v)
	default:
		if diagnose {
			return c.typeRefFromValueWithRuntimeGlobals(value, pointer, elementName, allowRuntimeGlobals)
		}
		return c.typeRefFromStaticValue(value)
	}
}

func (c *flowChecker) typeRefFromValueMap(value map[string]any, pointer, elementName string, allowRuntimeGlobals bool) (TypeRef, bool) {
	if raw, ok := value["elementReference"]; ok {
		ref, _ := raw.(string)
		if allowRuntimeGlobals {
			return c.resolveRuntimeValueReference(ref, pointer, elementName)
		}
		return c.resolveReference(ref, pointer, elementName)
	}
	if _, ok := value["stringValue"]; ok {
		return TypeRef{Scalar: spec.TypeString, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["numberValue"]; ok {
		return TypeRef{Scalar: spec.TypeNumber, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["booleanValue"]; ok {
		return TypeRef{Scalar: spec.TypeBoolean, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["dateValue"]; ok {
		return TypeRef{Scalar: spec.TypeDate, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["dateTimeValue"]; ok {
		return TypeRef{Scalar: spec.TypeDatetime, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["datetimeValue"]; ok {
		return TypeRef{Scalar: spec.TypeDatetime, Source: ValueSourceLiteral}, true
	}
	if _, ok := value["nullValue"]; ok {
		return TypeRef{Scalar: spec.TypeNil, Nullable: true, Source: ValueSourceLiteral}, true
	}
	return TypeRef{Scalar: spec.TypeAny, Source: ValueSourceLiteral}, true
}

func (c *flowChecker) readerError(pointer, elementName string, err error) {
	c.addDiagnostic(Diagnostic{
		Code:        DiagnosticCodeReaderError,
		Severity:    SeverityError,
		JSONPointer: pointer,
		ElementName: elementName,
		Message:     err.Error(),
	})
}

func (c *flowChecker) addDiagnostic(d Diagnostic) {
	d.ComponentPath = c.opts.ComponentPath
	d.OwnerPack = c.opts.OwnerPack
	c.diagnostics = append(c.diagnostics, d)
}

func normalizeFlowReference(ref string) string {
	ref = strings.TrimSpace(ref)
	if strings.HasPrefix(ref, "{!") && strings.HasSuffix(ref, "}") {
		ref = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(ref, "{!"), "}"))
	}
	return ref
}

func flowReferenceWasMergeWrapped(ref string) bool {
	ref = strings.TrimSpace(ref)
	return strings.HasPrefix(ref, "{!") && strings.HasSuffix(ref, "}")
}

func flowReferenceRoot(ref string) string {
	ref = normalizeFlowReference(ref)
	if ref == "" {
		return ""
	}
	return strings.Split(ref, ".")[0]
}

func looksLikeFlowReference(value string) bool {
	value = normalizeFlowReference(value)
	if value == "" {
		return false
	}
	if strings.HasPrefix(value, "$") || strings.HasPrefix(value, "__") {
		return true
	}
	return strings.Contains(value, ".")
}

func typeRefFromFlowVariable(v models.FlowVariable) TypeRef {
	ref := typeRefFromFlowDataType(v.DataType)
	ref.Collection = v.IsCollection
	ref.ObjectAPIName = strings.TrimSpace(v.ObjectType)
	ref.Nullable = true
	ref.Source = ValueSourceVariable
	return ref
}

func typeRefFromFlowFormula(f models.FlowFormula) TypeRef {
	ref := typeRefFromFlowDataType(f.DataType)
	ref.Nullable = true
	ref.Source = ValueSourceFormulaExpression
	return ref
}

func typeRefFromFlowDataType(dataType string) TypeRef {
	scalar := spec.TypeUnknown
	switch strings.ToLower(strings.TrimSpace(dataType)) {
	case "string", "text", "id", "picklist", "textarea", "email", "phone", "url":
		scalar = spec.TypeString
	case "number", "double", "currency", "percent", "integer":
		scalar = spec.TypeNumber
	case "boolean":
		scalar = spec.TypeBoolean
	case "date":
		scalar = spec.TypeDate
	case "datetime", "date/time":
		scalar = spec.TypeDatetime
	case "object":
		scalar = spec.TypeObject
	case "any":
		scalar = spec.TypeAny
	}
	return TypeRef{Scalar: scalar}
}

func expressionVariableTypeFromTypeRef(ref TypeRef) formula.ExpressionVariableType {
	return expressionVariableTypeFromTypeRefDynamic(ref, false)
}

func expressionVariableTypeFromTypeRefDynamic(ref TypeRef, dynamic bool) formula.ExpressionVariableType {
	return formula.ExpressionVariableType{
		Type:          ref.normalizedScalar(),
		Collection:    ref.Collection,
		Dynamic:       dynamic,
		ObjectAPIName: strings.TrimSpace(ref.ObjectAPIName),
	}
}

func typeRefFromField(field *models.FieldMetadata) TypeRef {
	if field == nil {
		return TypeRef{Scalar: spec.TypeUnknown, Source: ValueSourceFieldReference}
	}
	fieldType := field.Type
	if field.Type == constants.FieldTypeFormula && field.ReturnType != nil {
		fieldType = *field.ReturnType
	}
	return TypeRef{
		Scalar:   valueTypeFromFieldType(fieldType),
		Nullable: !field.Required,
		Source:   ValueSourceFieldReference,
	}
}

func isMultiPicklistField(field *models.FieldMetadata) bool {
	if field == nil {
		return false
	}
	fieldType := field.Type
	if field.Type == constants.FieldTypeFormula && field.ReturnType != nil {
		fieldType = *field.ReturnType
	}
	return constants.NormalizeFieldType(string(fieldType)) == constants.FieldTypeMultiPicklist
}

func valueTypeFromFieldType(fieldType constants.SchemaFieldType) spec.ValueType {
	switch constants.NormalizeFieldType(string(fieldType)) {
	case constants.FieldTypeString,
		constants.FieldTypeEmail,
		constants.FieldTypePhone,
		constants.FieldTypeTextarea,
		constants.FieldTypeUrl,
		constants.FieldTypePicklist,
		constants.FieldTypeMultiPicklist,
		constants.FieldTypeEncryptedString,
		constants.FieldTypePassword,
		constants.FieldTypeAutonumber,
		constants.FieldTypeReference,
		constants.FieldTypeMasterDetail:
		return spec.TypeString
	case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
		return spec.TypeNumber
	case constants.FieldTypeBoolean:
		return spec.TypeBoolean
	case constants.FieldTypeDate:
		return spec.TypeDate
	case constants.FieldTypeDatetime:
		return spec.TypeDatetime
	case constants.FieldTypeJson:
		return spec.TypeAny
	default:
		return spec.TypeUnknown
	}
}

func typeCompatible(expected, actual TypeRef) bool {
	expectedScalar := expected.normalizedScalar()
	actualScalar := actual.normalizedScalar()
	expectedLoose := expectedScalar == spec.TypeAny || expectedScalar == spec.TypeUnknown
	actualLoose := actualScalar == spec.TypeAny || actualScalar == spec.TypeUnknown
	if !expectedLoose && expected.Collection != actual.Collection && (actual.Collection || (actualScalar != spec.TypeNil && !actualLoose)) {
		return false
	}
	if !expectedLoose && expected.NestedCollection != actual.NestedCollection && (actual.NestedCollection || !actualLoose) {
		return false
	}
	if actualScalar == spec.TypeNil {
		return expected.Nullable
	}
	if expectedLoose || actualLoose {
		return true
	}
	if expectedScalar == spec.TypeString && (actualScalar == spec.TypeDate || actualScalar == spec.TypeDatetime) {
		return true
	}
	if expectedScalar != actualScalar {
		return false
	}
	if expectedScalar == spec.TypeObject && expected.ObjectAPIName != "" && actual.ObjectAPIName != "" {
		return strings.EqualFold(expected.ObjectAPIName, actual.ObjectAPIName)
	}
	return true
}
