package prompt import ( "context" "sort" "text/template" "text/template/parse" "gitea.maximumdirect.net/eric/promptkit/internal/domain" ) type requirementReachability uint8 const ( requirementDefinite requirementReachability = iota requirementPossible requirementInactive ) type dotIdentity uint8 const ( dotRoot dotIdentity = iota dotNonRoot dotUnknown ) type inputEvidence struct { name string kind domain.RequirementKind declared bool applicable bool contentType *string description *string explanation string } type variableEvidence struct { name string kind domain.RequirementKind applicable bool explanation string } type requirementIssueKey struct { code domain.RequirementIssueCode inputName string variableName string } type conditionEvaluation struct { value any known bool unsupported bool inputNameUnknown bool missingVariables map[string]struct{} contentInputs map[string]struct{} } func (evaluation conditionEvaluation) truth() bool { truth, ok := template.IsTrue(evaluation.value) return evaluation.known && ok && truth } func (walker *requirementWalker) evaluateCondition(pipe *parse.PipeNode, reach requirementReachability, dot dotIdentity) conditionEvaluation { if pipe == nil { return conditionEvaluation{unsupported: true} } if len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign { return walker.evaluateCommand(pipe.Cmds[0], reach, dot) } if name, ok := directInputPipeline(pipe); ok { return walker.evaluateInputName(name, reach, dot) } before := walker.dependencies visitReach := requirementPossible if reach == requirementInactive { visitReach = requirementInactive } if err := walker.visitPipe(pipe, visitReach, dot); err != nil { return conditionEvaluation{unsupported: true} } return conditionEvaluation{unsupported: walker.dependencies != before} } func (walker *requirementWalker) evaluateCommand(command *parse.CommandNode, reach requirementReachability, dot dotIdentity) conditionEvaluation { if command == nil || len(command.Args) == 0 { return conditionEvaluation{unsupported: true} } identifier, isIdentifier := command.Args[0].(*parse.IdentifierNode) if isIdentifier { switch identifier.Ident { case "input": if reach == requirementInactive { if len(command.Args) == 2 { if name, ok := literalStringValue(command.Args[1]); ok { walker.recordInput(name, requirementInactive) return conditionEvaluation{} } } for _, argument := range command.Args[1:] { _ = walker.visit(argument, requirementInactive, dot) } return conditionEvaluation{} } if len(command.Args) == 2 { if name, ok := command.Args[1].(*parse.StringNode); ok { walker.recordInput(name.Text, reach) return conditionEvaluation{contentInputs: map[string]struct{}{name.Text: {}}} } return walker.evaluateInputName(command.Args[1], reach, dot) } for _, argument := range command.Args[1:] { _ = walker.visit(argument, requirementPossible, dot) } return walker.unresolvedInputName(conditionEvaluation{unsupported: true}) case "index": if len(command.Args) == 3 { if key, ok := command.Args[2].(*parse.StringNode); ok && isRootMap(command.Args[1], dot, walker.rootKnown) { walker.recordOptionalVariable(key.Text, reach) value, supplied := walker.vars[key.Text] if !supplied { value = "" } return conditionEvaluation{value: value, known: true} } } case "not": if len(command.Args) == 2 { evaluation := walker.evaluateValue(command.Args[1], reach, dot) if evaluation.known { truth, ok := template.IsTrue(evaluation.value) if ok { evaluation.value = !truth } else { evaluation.known = false evaluation.unsupported = true } } return evaluation } case "eq", "ne": if len(command.Args) == 3 { left := walker.evaluateValue(command.Args[1], reach, dot) right := walker.evaluateValue(command.Args[2], reach, dot) result := mergeConditionEvaluations(left, right) if left.known && right.known { leftString, leftOK := left.value.(string) rightString, rightOK := right.value.(string) if leftOK && rightOK { equal := leftString == rightString if identifier.Ident == "ne" { equal = !equal } result.value = equal result.known = true return result } result.unsupported = true } return result } } } if !isIdentifier && len(command.Args) == 1 { return walker.evaluateValue(command.Args[0], reach, dot) } if reach == requirementInactive { _ = walker.visitCommand(command, reach, dot) return conditionEvaluation{} } before := walker.dependencies _ = walker.visitCommand(command, requirementPossible, dot) return conditionEvaluation{unsupported: walker.dependencies != before} } func (walker *requirementWalker) evaluateValue(node parse.Node, reach requirementReachability, dot dotIdentity) conditionEvaluation { switch node := node.(type) { case *parse.DotNode: if dot == dotRoot { return conditionEvaluation{value: walker.vars, known: true} } if dot == dotUnknown { walker.unresolvedVariableScope(reach) } return conditionEvaluation{unsupported: true} case *parse.StringNode: return conditionEvaluation{value: node.Text, known: true} case *parse.BoolNode: return conditionEvaluation{value: node.True, known: true} case *parse.NilNode: return conditionEvaluation{value: nil, known: true} case *parse.FieldNode: if dot != dotRoot || len(node.Ident) == 0 { if dot == dotUnknown { walker.unresolvedVariableScope(reach) } return conditionEvaluation{unsupported: true} } name := node.Ident[0] walker.recordVariable(name, reach) if len(node.Ident) > 1 { walker.addIssue("", name, "A chained field access is outside the direct-key scope.") return conditionEvaluation{unsupported: true} } value, supplied := walker.vars[name] if !supplied { return conditionEvaluation{missingVariables: map[string]struct{}{name: {}}} } return conditionEvaluation{value: value, known: true} case *parse.VariableNode: if len(node.Ident) == 1 && node.Ident[0] == "$" && walker.rootKnown { return conditionEvaluation{value: walker.vars, known: true} } if len(node.Ident) < 2 || node.Ident[0] != "$" || !walker.rootKnown { walker.visitVariable(node, reach) return conditionEvaluation{unsupported: true} } name := node.Ident[1] walker.recordVariable(name, reach) if len(node.Ident) > 2 { walker.addIssue("", name, "A chained field access is outside the direct-key scope.") return conditionEvaluation{unsupported: true} } value, supplied := walker.vars[name] if !supplied { return conditionEvaluation{missingVariables: map[string]struct{}{name: {}}} } return conditionEvaluation{value: value, known: true} case *parse.CommandNode: return walker.evaluateCommand(node, reach, dot) case *parse.PipeNode: return walker.evaluateCondition(node, reach, dot) case *parse.ChainNode: before := walker.dependencies visitReach := requirementPossible if reach == requirementInactive { visitReach = requirementInactive } _ = walker.visit(node.Node, visitReach, dot) return conditionEvaluation{unsupported: walker.dependencies != before} default: return conditionEvaluation{unsupported: true} } } func (walker *requirementWalker) evaluateInputName(node parse.Node, reach requirementReachability, dot dotIdentity) conditionEvaluation { evaluation := walker.evaluateValue(node, reach, dot) if evaluation.known { if name, ok := evaluation.value.(string); ok { walker.recordInput(name, reach) return conditionEvaluation{contentInputs: map[string]struct{}{name: {}}} } evaluation.unsupported = true } // Value analysis has already visited recognizable references, including // the conservative fallback for unsupported expressions. Do not rescan // the same subtree: nested computed names would multiply that work. return walker.unresolvedInputName(evaluation) } func (walker *requirementWalker) unresolvedInputName(evaluation conditionEvaluation) conditionEvaluation { for name := range evaluation.missingVariables { walker.addIssueCode(domain.RequirementIssueMissingVariable, "", name, "A missing variable prevents resolving an input name.") } for name := range evaluation.contentInputs { walker.addIssueCode(domain.RequirementIssueInputContent, name, "", "Captured input content affects an input name.") } walker.addIssueCode(domain.RequirementIssueDynamicInputName, "", "", "An input helper name is not statically available.") walker.unknownInputEffect = true evaluation.inputNameUnknown = true return evaluation } func directInputPipeline(pipe *parse.PipeNode) (parse.Node, bool) { if pipe == nil || len(pipe.Cmds) != 2 || len(pipe.Decl) != 0 || pipe.IsAssign { return nil, false } input := pipe.Cmds[1] if len(input.Args) != 1 { return nil, false } identifier, ok := input.Args[0].(*parse.IdentifierNode) if !ok || identifier.Ident != "input" { return nil, false } value := pipe.Cmds[0] if len(value.Args) > 0 { valueIdentifier, isValueIdentifier := value.Args[0].(*parse.IdentifierNode) if isValueIdentifier && valueIdentifier.Ident == "input" && len(value.Args) == 2 { return value, true } } if len(value.Args) == 1 { return value.Args[0], true } if len(value.Args) == 3 { function, ok := value.Args[0].(*parse.IdentifierNode) if ok && function.Ident == "index" { return value, true } } return nil, false } func literalStringValue(node parse.Node) (string, bool) { switch node := node.(type) { case *parse.StringNode: return node.Text, true case *parse.PipeNode: if len(node.Cmds) == 1 && len(node.Cmds[0].Args) == 1 && len(node.Decl) == 0 && !node.IsAssign { return literalStringValue(node.Cmds[0].Args[0]) } } return "", false } func mergeConditionEvaluations(left, right conditionEvaluation) conditionEvaluation { result := conditionEvaluation{ unsupported: left.unsupported || right.unsupported, inputNameUnknown: left.inputNameUnknown || right.inputNameUnknown, missingVariables: make(map[string]struct{}, len(left.missingVariables)+len(right.missingVariables)), contentInputs: make(map[string]struct{}, len(left.contentInputs)+len(right.contentInputs)), } for name := range left.missingVariables { result.missingVariables[name] = struct{}{} } for name := range right.missingVariables { result.missingVariables[name] = struct{}{} } for name := range left.contentInputs { result.contentInputs[name] = struct{}{} } for name := range right.contentInputs { result.contentInputs[name] = struct{}{} } return result } func branchReach(parent requirementReachability, known, truth, selected bool) requirementReachability { if parent == requirementInactive { return requirementInactive } if !known { return requirementPossible } if truth == selected { if parent == requirementPossible { return requirementPossible } return requirementDefinite } return requirementInactive } type requirementWalker struct { ctx context.Context vars map[string]string inputs map[string]*inputEvidence variables map[string]*variableEvidence issues map[requirementIssueKey]domain.RequirementIssue complete bool rootKnown bool unknownInputEffect bool unknownVariableEffect bool dependencies int templateSet *template.Template visitedTrees map[*parse.Tree]bool } // AnalyzeRequirements collects the prompt dependencies recognizable from its // captured syntax. Branch bodies remain conditional until their conditions are // classified; unsupported dependency-bearing syntax makes the result partial. func AnalyzeRequirements( ctx context.Context, definition *domain.PromptDefinition, snapshot RequirementSnapshot, vars map[string]string, sessionBypassed bool, ) (domain.RequirementInspection, error) { if err := ctx.Err(); err != nil { return domain.RequirementInspection{}, err } result := domain.RequirementInspection{Complete: true} if definition != nil { result.PromptID = definition.ID result.PromptVersion = copyString(definition.Version) } walker := &requirementWalker{ ctx: ctx, vars: vars, inputs: make(map[string]*inputEvidence, len(snapshot.Inputs)), variables: make(map[string]*variableEvidence), issues: make(map[requirementIssueKey]domain.RequirementIssue), complete: true, rootKnown: true, } for _, input := range snapshot.Inputs { if err := ctx.Err(); err != nil { return domain.RequirementInspection{}, err } kind := domain.RequirementOptional if input.Required { kind = domain.RequirementRequired } walker.inputs[input.Name] = &inputEvidence{ name: input.Name, kind: kind, declared: true, contentType: copyString(input.ContentType), description: copyString(input.Description), } if kind == domain.RequirementOptional { walker.inputs[input.Name].explanation = "Declared as an optional input." } } for _, message := range snapshot.Templates.Messages { if err := walker.walkTemplate(message); err != nil { return domain.RequirementInspection{}, err } } if !sessionBypassed && snapshot.Templates.Session != nil { if err := walker.walkTemplate(snapshot.Templates.Session); err != nil { return domain.RequirementInspection{}, err } } if err := ctx.Err(); err != nil { return domain.RequirementInspection{}, err } return walker.result(result), nil } func (walker *requirementWalker) walkTemplate(compiled *template.Template) error { if err := walker.ctx.Err(); err != nil { return err } if compiled == nil || compiled.Tree == nil || compiled.Tree.Root == nil { walker.addIssue("", "", "A captured template has no parsed root.") return nil } previousSet := walker.templateSet previousVisited := walker.visitedTrees walker.templateSet = compiled walker.visitedTrees = map[*parse.Tree]bool{compiled.Tree: true} walker.rootKnown = true err := walker.visit(compiled.Tree.Root, requirementDefinite, dotRoot) walker.templateSet = previousSet walker.visitedTrees = previousVisited return err } func (walker *requirementWalker) visit(node parse.Node, reach requirementReachability, dot dotIdentity) error { if err := walker.ctx.Err(); err != nil { return err } if node == nil { return nil } switch node := node.(type) { case *parse.ListNode: if node == nil { return nil } for _, child := range node.Nodes { if err := walker.visit(child, reach, dot); err != nil { return err } } case *parse.ActionNode: return walker.visit(node.Pipe, reach, dot) case *parse.IfNode: if reach == requirementInactive { if err := walker.visit(node.Pipe, requirementInactive, dot); err != nil { return err } if err := walker.visit(node.List, requirementInactive, dot); err != nil { return err } return walker.visit(node.ElseList, requirementInactive, dot) } condition := walker.evaluateCondition(node.Pipe, reach, dot) before := walker.dependencies thenReach := branchReach(reach, condition.known, condition.truth(), true) elseReach := branchReach(reach, condition.known, condition.truth(), false) if err := walker.visit(node.List, thenReach, dot); err != nil { return err } if err := walker.visit(node.ElseList, elseReach, dot); err != nil { return err } if !condition.known && walker.dependencies != before { walker.recordConditionLimitation(condition) } return nil case *parse.WithNode: if reach == requirementInactive { if err := walker.visit(node.Pipe, requirementInactive, dot); err != nil { return err } bodyDot := walker.withDotIdentity(node.Pipe, dot, conditionEvaluation{}) if err := walker.visit(node.List, requirementInactive, bodyDot); err != nil { return err } return walker.visit(node.ElseList, requirementInactive, dot) } condition := walker.evaluateCondition(node.Pipe, reach, dot) before := walker.dependencies thenReach := branchReach(reach, condition.known, condition.truth(), true) elseReach := branchReach(reach, condition.known, condition.truth(), false) bodyDot := walker.withDotIdentity(node.Pipe, dot, condition) if err := walker.visit(node.List, thenReach, bodyDot); err != nil { return err } if err := walker.visit(node.ElseList, elseReach, dot); err != nil { return err } if !condition.known && walker.dependencies != before { walker.recordConditionLimitation(condition) } return nil case *parse.RangeNode: if err := walker.visit(node.Pipe, reach, dot); err != nil { return err } before := walker.dependencies bodyReach := requirementPossible if reach == requirementInactive { bodyReach = requirementInactive } if err := walker.visit(node.List, bodyReach, dotNonRoot); err != nil { return err } if err := walker.visit(node.ElseList, bodyReach, dot); err != nil { return err } if reach != requirementInactive && walker.dependencies != before { walker.addIssue("", "", "Dependencies inside a range are only conservatively enumerated.") } case *parse.TemplateNode: pipeReach := requirementPossible if reach == requirementInactive { pipeReach = requirementInactive } if err := walker.visit(node.Pipe, pipeReach, dot); err != nil { return err } if reach == requirementInactive { return nil } called := walker.templateSet.Lookup(node.Name) if called == nil || called.Tree == nil || called.Tree.Root == nil { walker.addIssue("", "", "An invoked named template is unavailable for syntax inspection.") return nil } if walker.visitedTrees[called.Tree] { walker.addIssue("", "", "An invoked named template is scanned conservatively.") return nil } walker.visitedTrees[called.Tree] = true rootKnown := walker.rootKnown walker.rootKnown = false walker.unknownVariableEffect = true err := walker.visit(called.Tree.Root, requirementPossible, dotNonRoot) walker.rootKnown = rootKnown if err != nil { return err } walker.addIssue("", "", "An invoked named template is scanned conservatively.") case *parse.PipeNode: return walker.visitPipe(node, reach, dot) case *parse.CommandNode: return walker.visitCommand(node, reach, dot) case *parse.FieldNode: if dot == dotRoot && len(node.Ident) > 0 { walker.recordVariable(node.Ident[0], reach) if len(node.Ident) > 1 && reach != requirementInactive { walker.addIssue("", node.Ident[0], "A chained field access is outside the direct-key scope.") } } if dot == dotUnknown { walker.unresolvedVariableScope(reach) } case *parse.DotNode: if dot == dotUnknown { walker.unresolvedVariableScope(reach) } case *parse.VariableNode: walker.visitVariable(node, reach) case *parse.ChainNode: before := walker.dependencies chainReach := requirementPossible if reach == requirementInactive { chainReach = requirementInactive } if err := walker.visit(node.Node, chainReach, dot); err != nil { return err } if reach != requirementInactive && walker.dependencies != before { walker.addIssue("", "", "A chained expression is not interpreted.") } case *parse.BreakNode, *parse.ContinueNode: if reach != requirementInactive { walker.addIssue("", "", "Loop control is outside the supported syntax scope.") } } return nil } func (walker *requirementWalker) visitPipe(pipe *parse.PipeNode, reach requirementReachability, dot dotIdentity) error { if pipe == nil { return nil } if len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign { return walker.visitCommand(pipe.Cmds[0], reach, dot) } if name, ok := directInputPipeline(pipe); ok { if reach == requirementInactive { if literal, known := literalStringValue(name); known { walker.recordInput(literal, requirementInactive) return nil } return walker.visit(name, requirementInactive, dot) } walker.evaluateInputName(name, reach, dot) return nil } before := walker.dependencies commandReach := requirementPossible if reach == requirementInactive { commandReach = requirementInactive } for _, command := range pipe.Cmds { if err := walker.visitCommand(command, commandReach, dot); err != nil { return err } } if reach != requirementInactive && walker.dependencies != before { walker.addIssue("", "", "A pipeline or variable assignment is not interpreted.") } if reach != requirementInactive { for _, declaration := range pipe.Decl { if len(declaration.Ident) > 0 && declaration.Ident[0] == "$" { walker.rootKnown = false walker.unknownVariableEffect = true walker.addIssue("", "", "The root template binding is reassigned.") } } } return nil } func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach requirementReachability, dot dotIdentity) error { if command == nil || len(command.Args) == 0 { return nil } identifier, isIdentifier := command.Args[0].(*parse.IdentifierNode) if isIdentifier && identifier.Ident == "input" { if reach == requirementInactive { if len(command.Args) == 2 { if name, ok := literalStringValue(command.Args[1]); ok { walker.recordInput(name, reach) return nil } } for _, argument := range command.Args[1:] { if err := walker.visit(argument, requirementInactive, dot); err != nil { return err } } return nil } if len(command.Args) == 2 { if name, ok := command.Args[1].(*parse.StringNode); ok { walker.recordInput(name.Text, reach) return nil } walker.evaluateInputName(command.Args[1], reach, dot) return nil } for _, argument := range command.Args[1:] { if err := walker.visit(argument, requirementPossible, dot); err != nil { return err } } walker.unresolvedInputName(conditionEvaluation{unsupported: true}) return nil } if isIdentifier && identifier.Ident == "index" && len(command.Args) == 3 { if _, ok := command.Args[2].(*parse.StringNode); ok && isRootMap(command.Args[1], dot, walker.rootKnown) { key := command.Args[2].(*parse.StringNode).Text walker.recordOptionalVariable(key, reach) return nil } // A local map may alias the caller root even though its value is not // tracked. A lookup on it must not vanish from complete discovery. if variable, ok := unwrapSingleValue(command.Args[1]).(*parse.VariableNode); ok && len(variable.Ident) == 1 { walker.unresolvedVariableScope(reach) } } if !isIdentifier && len(command.Args) == 1 { return walker.visit(command.Args[0], reach, dot) } before := walker.dependencies for index, argument := range command.Args { if isIdentifier && index == 0 { continue } argumentReach := requirementPossible if reach == requirementInactive { argumentReach = requirementInactive } if err := walker.visit(argument, argumentReach, dot); err != nil { return err } } if reach != requirementInactive && walker.dependencies != before { walker.addIssue("", "", "A function call or computed expression is not interpreted.") } return nil } func (walker *requirementWalker) visitVariable(variable *parse.VariableNode, reach requirementReachability) { if len(variable.Ident) < 2 { return } if variable.Ident[0] != "$" { walker.unresolvedVariableScope(reach) return } if !walker.rootKnown { if reach != requirementInactive { walker.unknownVariableEffect = true walker.addIssue("", "", "The root template binding is not known.") } return } walker.recordVariable(variable.Ident[1], reach) if len(variable.Ident) > 2 && reach != requirementInactive { walker.addIssue("", variable.Ident[1], "A chained field access is outside the direct-key scope.") } } func (walker *requirementWalker) unresolvedVariableScope(reach requirementReachability) { if reach != requirementInactive { walker.unknownVariableEffect = true walker.addIssue("", "", "A caller-variable scope is not statically known.") } } func (walker *requirementWalker) withDotIdentity(pipe *parse.PipeNode, dot dotIdentity, condition conditionEvaluation) dotIdentity { if pipe != nil && len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign && len(pipe.Cmds[0].Args) == 1 && isRootMap(pipe.Cmds[0].Args[0], dot, walker.rootKnown) { return dotRoot } if condition.known { if _, root := condition.value.(map[string]string); root { return dotRoot } return dotNonRoot } if condition.inputNameUnknown || !condition.unsupported && (len(condition.missingVariables) > 0 || len(condition.contentInputs) > 0) { // Supported caller values and input bodies are strings; supported // comparisons and negation return booleans, never the root map. return dotNonRoot } return dotUnknown } func isRootMap(node parse.Node, dot dotIdentity, rootKnown bool) bool { switch node := unwrapSingleValue(node).(type) { case *parse.DotNode: return dot == dotRoot case *parse.VariableNode: return rootKnown && len(node.Ident) == 1 && node.Ident[0] == "$" default: return false } } func unwrapSingleValue(node parse.Node) parse.Node { for { pipe, ok := node.(*parse.PipeNode) if !ok || len(pipe.Cmds) != 1 || len(pipe.Decl) != 0 || pipe.IsAssign || len(pipe.Cmds[0].Args) != 1 { return node } node = pipe.Cmds[0].Args[0] } } func (walker *requirementWalker) recordInput(name string, reach requirementReachability) { evidence := walker.inputs[name] if evidence == nil { evidence = &inputEvidence{ name: name, kind: domain.RequirementOptional, explanation: "Declared as an optional input.", } walker.inputs[name] = evidence } if reach == requirementInactive { if !evidence.applicable && evidence.kind != domain.RequirementRequired { evidence.explanation = "This input is referenced only in a skipped template path." } } else { evidence.applicable = true walker.promoteInput(evidence, kindForReach(reach)) if evidence.kind == domain.RequirementRequired { evidence.explanation = "" } else if reach == requirementPossible { evidence.explanation = "This reference is not known to be reached on every execution path." } } if reach != requirementInactive { walker.dependencies++ } } func (walker *requirementWalker) recordVariable(name string, reach requirementReachability) { evidence := walker.variables[name] if evidence == nil { evidence = &variableEvidence{name: name, kind: domain.RequirementOptional} walker.variables[name] = evidence } if reach == requirementInactive { if !evidence.applicable { evidence.explanation = "This variable is referenced only in a skipped template path." } } else { evidence.applicable = true walker.promoteVariable(evidence, kindForReach(reach)) if evidence.kind == domain.RequirementRequired { evidence.explanation = "" } else if reach == requirementPossible { evidence.explanation = "This reference is not known to be reached on every execution path." } } if reach != requirementInactive { walker.dependencies++ } } func (walker *requirementWalker) recordOptionalVariable(name string, reach requirementReachability) { evidence := walker.variables[name] if evidence == nil { evidence = &variableEvidence{name: name, kind: domain.RequirementOptional} walker.variables[name] = evidence } if reach == requirementInactive { if !evidence.applicable { evidence.explanation = "This variable is referenced only in a skipped template path." } } else { evidence.applicable = true } if evidence.kind == domain.RequirementOptional && reach != requirementInactive { evidence.explanation = "This key is read through an optional map lookup." } if reach != requirementInactive { walker.dependencies++ } } func (walker *requirementWalker) promoteInput(evidence *inputEvidence, kind domain.RequirementKind) { if requirementRank(kind) > requirementRank(evidence.kind) { evidence.kind = kind } if evidence.kind == domain.RequirementRequired { evidence.explanation = "" } } func (walker *requirementWalker) promoteVariable(evidence *variableEvidence, kind domain.RequirementKind) { if requirementRank(kind) > requirementRank(evidence.kind) { evidence.kind = kind } if evidence.kind == domain.RequirementRequired { evidence.explanation = "" } } func kindForReach(reach requirementReachability) domain.RequirementKind { if reach == requirementPossible { return domain.RequirementConditional } return domain.RequirementRequired } func requirementRank(kind domain.RequirementKind) int { switch kind { case domain.RequirementRequired: return 3 case domain.RequirementConditional: return 2 default: return 1 } } func (walker *requirementWalker) addIssue(inputName, variableName, description string) { walker.addIssueCode(domain.RequirementIssueUnsupportedConstruct, inputName, variableName, description) } func (walker *requirementWalker) addIssueCode(code domain.RequirementIssueCode, inputName, variableName, description string) { walker.complete = false issue := domain.RequirementIssue{ Code: code, InputName: inputName, VariableName: variableName, Description: description, } key := requirementIssueKey{code: issue.Code, inputName: inputName, variableName: variableName} walker.issues[key] = issue } func (walker *requirementWalker) recordConditionLimitation(condition conditionEvaluation) { for name := range condition.missingVariables { walker.addIssueCode(domain.RequirementIssueMissingVariable, "", name, "A missing variable prevents selecting a dependency-bearing branch.") } for name := range condition.contentInputs { walker.addIssueCode(domain.RequirementIssueInputContent, name, "", "Captured input content affects branch selection.") } if condition.unsupported && !condition.inputNameUnknown || (len(condition.missingVariables) == 0 && len(condition.contentInputs) == 0 && !condition.inputNameUnknown) { walker.addIssue("", "", "A dependency-bearing branch condition is not resolved by the supported syntax analysis.") } } func (walker *requirementWalker) result(result domain.RequirementInspection) domain.RequirementInspection { result.Complete = walker.complete result.Inputs = make([]domain.InputRequirement, 0, len(walker.inputs)) for _, evidence := range walker.inputs { if walker.unknownInputEffect && evidence.kind != domain.RequirementRequired { evidence.kind = domain.RequirementConditional evidence.explanation = "An unresolved input name could refer to this input." } result.Inputs = append(result.Inputs, domain.InputRequirement{ Name: evidence.name, Kind: evidence.kind, Declared: evidence.declared, ContentType: copyString(evidence.contentType), Description: copyString(evidence.description), Explanation: evidence.explanation, }) } sort.Slice(result.Inputs, func(i, j int) bool { return result.Inputs[i].Name < result.Inputs[j].Name }) result.Variables = make([]domain.VariableRequirement, 0, len(walker.variables)) for _, evidence := range walker.variables { if walker.unknownVariableEffect && evidence.kind != domain.RequirementRequired { evidence.kind = domain.RequirementConditional evidence.explanation = "An unresolved caller-variable scope could refer to this variable." } _, supplied := walker.vars[evidence.name] result.Variables = append(result.Variables, domain.VariableRequirement{ Name: evidence.name, Kind: evidence.kind, Supplied: supplied, Explanation: evidence.explanation, }) } sort.Slice(result.Variables, func(i, j int) bool { return result.Variables[i].Name < result.Variables[j].Name }) result.Issues = make([]domain.RequirementIssue, 0, len(walker.issues)) for _, issue := range walker.issues { result.Issues = append(result.Issues, issue) } sort.Slice(result.Issues, func(i, j int) bool { left, right := result.Issues[i], result.Issues[j] if left.Code != right.Code { return left.Code < right.Code } if left.InputName != right.InputName { return left.InputName < right.InputName } return left.VariableName < right.VariableName }) return result } func copyString(value *string) *string { if value == nil { return nil } copy := *value return © }