Classify simple prompt requirement branches

This commit is contained in:
2026-10-08 20:31:45 +00:00
parent cc0b6183bf
commit 93e8874813
4 changed files with 570 additions and 51 deletions

View File

@@ -14,6 +14,7 @@ type requirementReachability uint8
const (
requirementDefinite requirementReachability = iota
requirementPossible
requirementInactive
)
type dotIdentity uint8
@@ -27,6 +28,7 @@ type inputEvidence struct {
name string
kind domain.RequirementKind
declared bool
applicable bool
contentType *string
description *string
explanation string
@@ -35,6 +37,7 @@ type inputEvidence struct {
type variableEvidence struct {
name string
kind domain.RequirementKind
applicable bool
explanation string
}
@@ -44,6 +47,210 @@ type requirementIssueKey struct {
variableName string
}
type conditionEvaluation struct {
value any
known bool
unsupported 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)
}
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 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: {}}}
}
}
if reach == requirementInactive {
_ = walker.visitCommand(command, reach, dot)
return conditionEvaluation{}
}
before := walker.dependencies
_ = walker.visitCommand(command, requirementPossible, dot)
return conditionEvaluation{unsupported: walker.dependencies != before}
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.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 {
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) < 2 || node.Ident[0] != "$" || !walker.rootKnown {
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 mergeConditionEvaluations(left, right conditionEvaluation) conditionEvaluation {
result := conditionEvaluation{
unsupported: left.unsupported || right.unsupported,
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
@@ -158,39 +365,82 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
case *parse.ActionNode:
return walker.visit(node.Pipe, reach, dot)
case *parse.IfNode:
if err := walker.visit(node.Pipe, reach, dot); err != nil {
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.List, requirementPossible, dot); err != nil {
if err := walker.visit(node.ElseList, elseReach, dot); err != nil {
return err
}
return walker.visit(node.ElseList, requirementPossible, dot)
if !condition.known && walker.dependencies != before {
walker.recordConditionLimitation(condition)
}
return nil
case *parse.WithNode:
if err := walker.visit(node.Pipe, reach, dot); err != nil {
if reach == requirementInactive {
if err := walker.visit(node.Pipe, requirementInactive, dot); err != nil {
return err
}
if err := walker.visit(node.List, requirementInactive, dotNonRoot); 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, dotNonRoot); err != nil {
return err
}
if err := walker.visit(node.List, requirementPossible, dotNonRoot); err != nil {
if err := walker.visit(node.ElseList, elseReach, dot); err != nil {
return err
}
return walker.visit(node.ElseList, requirementPossible, dot)
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
if err := walker.visit(node.List, requirementPossible, dotNonRoot); err != nil {
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, requirementPossible, dot); err != nil {
if err := walker.visit(node.ElseList, bodyReach, dot); err != nil {
return err
}
if walker.dependencies != before {
if reach != requirementInactive && walker.dependencies != before {
walker.addIssue("", "", "Dependencies inside a range are only conservatively enumerated.")
}
case *parse.TemplateNode:
if err := walker.visit(node.Pipe, requirementPossible, dot); err != nil {
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.")
@@ -216,7 +466,7 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
case *parse.FieldNode:
if dot == dotRoot && len(node.Ident) > 0 {
walker.recordVariable(node.Ident[0], reach)
if len(node.Ident) > 1 {
if len(node.Ident) > 1 && reach != requirementInactive {
walker.addIssue("", node.Ident[0], "A chained field access is outside the direct-key scope.")
}
}
@@ -224,14 +474,20 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
walker.visitVariable(node, reach)
case *parse.ChainNode:
before := walker.dependencies
if err := walker.visit(node.Node, requirementPossible, dot); err != nil {
chainReach := requirementPossible
if reach == requirementInactive {
chainReach = requirementInactive
}
if err := walker.visit(node.Node, chainReach, dot); err != nil {
return err
}
if walker.dependencies != before {
if reach != requirementInactive && walker.dependencies != before {
walker.addIssue("", "", "A chained expression is not interpreted.")
}
case *parse.BreakNode, *parse.ContinueNode:
walker.addIssue("", "", "Loop control is outside the supported syntax scope.")
if reach != requirementInactive {
walker.addIssue("", "", "Loop control is outside the supported syntax scope.")
}
}
return nil
}
@@ -244,18 +500,24 @@ func (walker *requirementWalker) visitPipe(pipe *parse.PipeNode, reach requireme
return walker.visitCommand(pipe.Cmds[0], reach, dot)
}
before := walker.dependencies
commandReach := requirementPossible
if reach == requirementInactive {
commandReach = requirementInactive
}
for _, command := range pipe.Cmds {
if err := walker.visitCommand(command, requirementPossible, dot); err != nil {
if err := walker.visitCommand(command, commandReach, dot); err != nil {
return err
}
}
if walker.dependencies != before {
if reach != requirementInactive && walker.dependencies != before {
walker.addIssue("", "", "A pipeline or variable assignment is not interpreted.")
}
for _, declaration := range pipe.Decl {
if len(declaration.Ident) > 0 && declaration.Ident[0] == "$" {
walker.rootKnown = false
walker.addIssue("", "", "The root template binding is reassigned.")
if reach != requirementInactive {
for _, declaration := range pipe.Decl {
if len(declaration.Ident) > 0 && declaration.Ident[0] == "$" {
walker.rootKnown = false
walker.addIssue("", "", "The root template binding is reassigned.")
}
}
}
return nil
@@ -274,11 +536,17 @@ func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach
}
}
for _, argument := range command.Args[1:] {
if err := walker.visit(argument, requirementPossible, dot); err != nil {
argumentReach := requirementPossible
if reach == requirementInactive {
argumentReach = requirementInactive
}
if err := walker.visit(argument, argumentReach, dot); err != nil {
return err
}
}
walker.addIssue("", "", "An input name is not a direct string literal.")
if reach != requirementInactive {
walker.addIssue("", "", "An input name is not a direct string literal.")
}
return nil
}
if isIdentifier && identifier.Ident == "index" && len(command.Args) == 3 {
@@ -296,11 +564,15 @@ func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach
if isIdentifier && index == 0 {
continue
}
if err := walker.visit(argument, requirementPossible, dot); err != nil {
argumentReach := requirementPossible
if reach == requirementInactive {
argumentReach = requirementInactive
}
if err := walker.visit(argument, argumentReach, dot); err != nil {
return err
}
}
if walker.dependencies != before {
if reach != requirementInactive && walker.dependencies != before {
walker.addIssue("", "", "A function call or computed expression is not interpreted.")
}
return nil
@@ -311,11 +583,13 @@ func (walker *requirementWalker) visitVariable(variable *parse.VariableNode, rea
return
}
if !walker.rootKnown {
walker.addIssue("", "", "The root template binding is not known.")
if reach != requirementInactive {
walker.addIssue("", "", "The root template binding is not known.")
}
return
}
walker.recordVariable(variable.Ident[1], reach)
if len(variable.Ident) > 2 {
if len(variable.Ident) > 2 && reach != requirementInactive {
walker.addIssue("", variable.Ident[1], "A chained field access is outside the direct-key scope.")
}
}
@@ -340,14 +614,22 @@ func (walker *requirementWalker) recordInput(name string, reach requirementReach
}
walker.inputs[name] = evidence
}
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."
walker.addIssue(name, "", "A dependency is conservatively classified.")
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++
}
walker.dependencies++
}
func (walker *requirementWalker) recordVariable(name string, reach requirementReachability) {
@@ -356,26 +638,43 @@ func (walker *requirementWalker) recordVariable(name string, reach requirementRe
evidence = &variableEvidence{name: name, kind: domain.RequirementOptional}
walker.variables[name] = evidence
}
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."
walker.addIssue("", name, "A dependency is conservatively classified.")
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++
}
walker.dependencies++
}
func (walker *requirementWalker) recordOptionalVariable(name string, _ requirementReachability) {
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 evidence.kind == domain.RequirementOptional {
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."
}
walker.dependencies++
if reach != requirementInactive {
walker.dependencies++
}
}
func (walker *requirementWalker) promoteInput(evidence *inputEvidence, kind domain.RequirementKind) {
@@ -415,9 +714,13 @@ func requirementRank(kind domain.RequirementKind) int {
}
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: domain.RequirementIssueUnsupportedConstruct,
Code: code,
InputName: inputName,
VariableName: variableName,
Description: description,
@@ -426,6 +729,18 @@ func (walker *requirementWalker) addIssue(inputName, variableName, description s
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 || (len(condition.missingVariables) == 0 && len(condition.contentInputs) == 0) {
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))