Resolve direct prompt input names

This commit is contained in:
2026-10-08 20:36:17 +00:00
parent 93e8874813
commit 089e824a92
4 changed files with 336 additions and 29 deletions

View File

@@ -70,10 +70,17 @@ dependencies do not by themselves make discovery incomplete. Recognizable
references on known skipped paths remain optional inventory entries with an references on known skipped paths remain optional inventory entries with an
inactive-use explanation, unless another applicable use establishes a inactive-use explanation, unless another applicable use establishes a
stronger classification. Skipped expressions are scanned only for stronger classification. Skipped expressions are scanned only for
syntactically recognizable names and are not evaluated. Invoked named syntactically recognizable names and are not evaluated. A direct caller value
templates are scanned conservatively, and dependency-bearing range scopes make or literal-key lookup can also supply an input name, including through simple
discovery incomplete. Uncalled definitions and bypassed session templates are parentheses or a direct pipeline to `input`; names are used exactly as
not traversed. This internal walker does not change rendering behavior. supplied. Unresolved names and unsupported dependency-bearing expressions
produce structured limitations while retaining recognizable references.
Where an unresolved name or scope could affect existing optional entries, those
entries are reported as conditional rather than inactive-only or definitively
optional. Invoked named templates are scanned conservatively, and
dependency-bearing range scopes make discovery incomplete. Uncalled definitions
and bypassed session templates are not traversed. This internal walker does not
change rendering behavior.
## Profiles And Built-Ins ## Profiles And Built-Ins

View File

@@ -305,6 +305,11 @@ Run `go test ./internal/prompt`; commit.
## Stage 4 — Resolve Direct Names And Finish Limitation Reporting ## Stage 4 — Resolve Direct Names And Finish Limitation Reporting
**Status: Complete (2026-10-08).** Supported caller-derived input names resolve
exactly; unresolved names preserve recognizable dependencies and report
structured limitations, with affected optional entries conservatively marked
conditional.
**Outcome:** Simple supplied names resolve exactly; complicated names and **Outcome:** Simple supplied names resolve exactly; complicated names and
constructs return useful partial inventories without speculative analysis. constructs return useful partial inventories without speculative analysis.

View File

@@ -51,6 +51,7 @@ type conditionEvaluation struct {
value any value any
known bool known bool
unsupported bool unsupported bool
inputNameUnknown bool
missingVariables map[string]struct{} missingVariables map[string]struct{}
contentInputs map[string]struct{} contentInputs map[string]struct{}
} }
@@ -67,6 +68,9 @@ func (walker *requirementWalker) evaluateCondition(pipe *parse.PipeNode, reach r
if len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign { if len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign {
return walker.evaluateCommand(pipe.Cmds[0], reach, dot) return walker.evaluateCommand(pipe.Cmds[0], reach, dot)
} }
if name, ok := directInputPipeline(pipe); ok {
return walker.evaluateInputName(name, reach, dot)
}
before := walker.dependencies before := walker.dependencies
visitReach := requirementPossible visitReach := requirementPossible
if reach == requirementInactive { if reach == requirementInactive {
@@ -86,19 +90,29 @@ func (walker *requirementWalker) evaluateCommand(command *parse.CommandNode, rea
if isIdentifier { if isIdentifier {
switch identifier.Ident { switch identifier.Ident {
case "input": 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 len(command.Args) == 2 {
if name, ok := command.Args[1].(*parse.StringNode); ok { if name, ok := command.Args[1].(*parse.StringNode); ok {
walker.recordInput(name.Text, reach) walker.recordInput(name.Text, reach)
return conditionEvaluation{contentInputs: map[string]struct{}{name.Text: {}}} return conditionEvaluation{contentInputs: map[string]struct{}{name.Text: {}}}
} }
return walker.evaluateInputName(command.Args[1], reach, dot)
} }
if reach == requirementInactive { for _, argument := range command.Args[1:] {
_ = walker.visitCommand(command, reach, dot) _ = walker.visit(argument, requirementPossible, dot)
return conditionEvaluation{}
} }
before := walker.dependencies return walker.unresolvedInputName(conditionEvaluation{unsupported: true})
_ = walker.visitCommand(command, requirementPossible, dot)
return conditionEvaluation{unsupported: walker.dependencies != before}
case "index": case "index":
if len(command.Args) == 3 { if len(command.Args) == 3 {
if key, ok := command.Args[2].(*parse.StringNode); ok && isRootMap(command.Args[1], dot, walker.rootKnown) { if key, ok := command.Args[2].(*parse.StringNode); ok && isRootMap(command.Args[1], dot, walker.rootKnown) {
@@ -214,9 +228,83 @@ func (walker *requirementWalker) evaluateValue(node parse.Node, reach requiremen
} }
} }
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
}
if evaluation.unsupported {
// Preserve directly recognizable references from a computed name without
// treating the computation itself as an evaluated dependency.
_ = walker.visit(node, requirementPossible, dot)
}
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 { func mergeConditionEvaluations(left, right conditionEvaluation) conditionEvaluation {
result := conditionEvaluation{ result := conditionEvaluation{
unsupported: left.unsupported || right.unsupported, unsupported: left.unsupported || right.unsupported,
inputNameUnknown: left.inputNameUnknown || right.inputNameUnknown,
missingVariables: make(map[string]struct{}, len(left.missingVariables)+len(right.missingVariables)), missingVariables: make(map[string]struct{}, len(left.missingVariables)+len(right.missingVariables)),
contentInputs: make(map[string]struct{}, len(left.contentInputs)+len(right.contentInputs)), contentInputs: make(map[string]struct{}, len(left.contentInputs)+len(right.contentInputs)),
} }
@@ -252,16 +340,18 @@ func branchReach(parent requirementReachability, known, truth, selected bool) re
} }
type requirementWalker struct { type requirementWalker struct {
ctx context.Context ctx context.Context
vars map[string]string vars map[string]string
inputs map[string]*inputEvidence inputs map[string]*inputEvidence
variables map[string]*variableEvidence variables map[string]*variableEvidence
issues map[requirementIssueKey]domain.RequirementIssue issues map[requirementIssueKey]domain.RequirementIssue
complete bool complete bool
rootKnown bool rootKnown bool
dependencies int unknownInputEffect bool
templateSet *template.Template unknownVariableEffect bool
visitedTrees map[*parse.Tree]bool dependencies int
templateSet *template.Template
visitedTrees map[*parse.Tree]bool
} }
// AnalyzeRequirements collects the prompt dependencies recognizable from its // AnalyzeRequirements collects the prompt dependencies recognizable from its
@@ -453,6 +543,7 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
walker.visitedTrees[called.Tree] = true walker.visitedTrees[called.Tree] = true
rootKnown := walker.rootKnown rootKnown := walker.rootKnown
walker.rootKnown = false walker.rootKnown = false
walker.unknownVariableEffect = true
err := walker.visit(called.Tree.Root, requirementPossible, dotNonRoot) err := walker.visit(called.Tree.Root, requirementPossible, dotNonRoot)
walker.rootKnown = rootKnown walker.rootKnown = rootKnown
if err != nil { if err != nil {
@@ -499,6 +590,17 @@ func (walker *requirementWalker) visitPipe(pipe *parse.PipeNode, reach requireme
if len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign { if len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign {
return walker.visitCommand(pipe.Cmds[0], reach, dot) 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 before := walker.dependencies
commandReach := requirementPossible commandReach := requirementPossible
if reach == requirementInactive { if reach == requirementInactive {
@@ -516,6 +618,7 @@ func (walker *requirementWalker) visitPipe(pipe *parse.PipeNode, reach requireme
for _, declaration := range pipe.Decl { for _, declaration := range pipe.Decl {
if len(declaration.Ident) > 0 && declaration.Ident[0] == "$" { if len(declaration.Ident) > 0 && declaration.Ident[0] == "$" {
walker.rootKnown = false walker.rootKnown = false
walker.unknownVariableEffect = true
walker.addIssue("", "", "The root template binding is reassigned.") walker.addIssue("", "", "The root template binding is reassigned.")
} }
} }
@@ -529,24 +632,34 @@ func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach
} }
identifier, isIdentifier := command.Args[0].(*parse.IdentifierNode) identifier, isIdentifier := command.Args[0].(*parse.IdentifierNode)
if isIdentifier && identifier.Ident == "input" { 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 len(command.Args) == 2 {
if name, ok := command.Args[1].(*parse.StringNode); ok { if name, ok := command.Args[1].(*parse.StringNode); ok {
walker.recordInput(name.Text, reach) walker.recordInput(name.Text, reach)
return nil return nil
} }
walker.evaluateInputName(command.Args[1], reach, dot)
return nil
} }
for _, argument := range command.Args[1:] { for _, argument := range command.Args[1:] {
argumentReach := requirementPossible if err := walker.visit(argument, requirementPossible, dot); err != nil {
if reach == requirementInactive {
argumentReach = requirementInactive
}
if err := walker.visit(argument, argumentReach, dot); err != nil {
return err return err
} }
} }
if reach != requirementInactive { walker.unresolvedInputName(conditionEvaluation{unsupported: true})
walker.addIssue("", "", "An input name is not a direct string literal.")
}
return nil return nil
} }
if isIdentifier && identifier.Ident == "index" && len(command.Args) == 3 { if isIdentifier && identifier.Ident == "index" && len(command.Args) == 3 {
@@ -584,6 +697,7 @@ func (walker *requirementWalker) visitVariable(variable *parse.VariableNode, rea
} }
if !walker.rootKnown { if !walker.rootKnown {
if reach != requirementInactive { if reach != requirementInactive {
walker.unknownVariableEffect = true
walker.addIssue("", "", "The root template binding is not known.") walker.addIssue("", "", "The root template binding is not known.")
} }
return return
@@ -736,7 +850,8 @@ func (walker *requirementWalker) recordConditionLimitation(condition conditionEv
for name := range condition.contentInputs { for name := range condition.contentInputs {
walker.addIssueCode(domain.RequirementIssueInputContent, name, "", "Captured input content affects branch selection.") walker.addIssueCode(domain.RequirementIssueInputContent, name, "", "Captured input content affects branch selection.")
} }
if condition.unsupported || (len(condition.missingVariables) == 0 && len(condition.contentInputs) == 0) { 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.") walker.addIssue("", "", "A dependency-bearing branch condition is not resolved by the supported syntax analysis.")
} }
} }
@@ -745,6 +860,10 @@ func (walker *requirementWalker) result(result domain.RequirementInspection) dom
result.Complete = walker.complete result.Complete = walker.complete
result.Inputs = make([]domain.InputRequirement, 0, len(walker.inputs)) result.Inputs = make([]domain.InputRequirement, 0, len(walker.inputs))
for _, evidence := range 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{ result.Inputs = append(result.Inputs, domain.InputRequirement{
Name: evidence.name, Kind: evidence.kind, Declared: evidence.declared, Name: evidence.name, Kind: evidence.kind, Declared: evidence.declared,
ContentType: copyString(evidence.contentType), Description: copyString(evidence.description), ContentType: copyString(evidence.contentType), Description: copyString(evidence.description),
@@ -754,6 +873,10 @@ func (walker *requirementWalker) result(result domain.RequirementInspection) dom
sort.Slice(result.Inputs, func(i, j int) bool { return result.Inputs[i].Name < result.Inputs[j].Name }) 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)) result.Variables = make([]domain.VariableRequirement, 0, len(walker.variables))
for _, evidence := range 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] _, supplied := walker.vars[evidence.name]
result.Variables = append(result.Variables, domain.VariableRequirement{ result.Variables = append(result.Variables, domain.VariableRequirement{
Name: evidence.name, Kind: evidence.kind, Supplied: supplied, Explanation: evidence.explanation, Name: evidence.name, Kind: evidence.kind, Supplied: supplied, Explanation: evidence.explanation,

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"reflect" "reflect"
"strings"
"testing" "testing"
"text/template" "text/template"
@@ -360,6 +361,177 @@ func TestAnalyzeRequirementsRetainsInactiveEntriesAndPromotesActiveUses(t *testi
} }
} }
func TestAnalyzeRequirementsResolvesSimpleInputNames(t *testing.T) {
tests := []struct {
name string
expression string
vars map[string]string
kind domain.RequirementKind
supplied bool
}{
{name: "direct dot", expression: `{{input .name}}`, vars: map[string]string{"name": " chosen "}, kind: domain.RequirementRequired, supplied: true},
{name: "explicit root", expression: `{{input $.name}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementRequired, supplied: true},
{name: "parenthesized direct", expression: `{{input (.name)}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementRequired, supplied: true},
{name: "literal lookup", expression: `{{input (index . "name")}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementOptional, supplied: true},
{name: "direct pipeline", expression: `{{.name | input}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementRequired, supplied: true},
{name: "empty supplied value", expression: `{{input .name}}`, vars: map[string]string{"name": ""}, kind: domain.RequirementRequired, supplied: true},
{name: "missing lookup has empty zero value", expression: `{{input (index . "name")}}`, vars: nil, kind: domain.RequirementOptional, supplied: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
definition := &domain.PromptDefinition{Templates: []domain.PromptMessageTemplate{{Role: "user", Content: test.expression}}}
snapshot := RequirementSnapshot{Templates: CompiledTemplates{Messages: []*template.Template{
compileRequirementTemplate(t, "message", test.expression),
}}}
result, err := AnalyzeRequirements(context.Background(), definition, snapshot, test.vars, false)
if err != nil {
t.Fatal(err)
}
inputName := "chosen"
if test.name == "direct dot" {
inputName = " chosen "
}
if test.name == "empty supplied value" || test.name == "missing lookup has empty zero value" {
inputName = ""
}
if !result.Complete || len(result.Issues) != 0 || len(result.Inputs) != 1 || result.Inputs[0].Name != inputName ||
result.Inputs[0].Kind != domain.RequirementRequired {
t.Fatalf("simple input name was not resolved exactly: %+v", result)
}
if len(result.Variables) != 1 || result.Variables[0].Name != "name" || result.Variables[0].Kind != test.kind || result.Variables[0].Supplied != test.supplied {
t.Fatalf("name-source caller key classification is incorrect: %+v", result.Variables)
}
artifacts := map[string]*domain.Artifact{inputName: {Name: inputName, Body: []byte("resolved")}}
rendered, err := NewGoRenderer().Render(context.Background(), definition, artifacts, test.vars)
if err != nil {
t.Fatal(err)
}
if len(rendered.Messages) != 1 || rendered.Messages[0].Content != "resolved" {
t.Fatalf("analyzed input name differs from renderer: %+v", rendered)
}
})
}
}
func TestAnalyzeRequirementsReportsUnresolvedInputNamesAndPromotesUnknownEffects(t *testing.T) {
definition := &domain.PromptDefinition{Templates: []domain.PromptMessageTemplate{{
Role: "user", Content: `{{input .missing}}`,
}}}
result, err := AnalyzeRequirements(context.Background(), definition, RequirementSnapshot{
Inputs: []InputMetadata{{Name: "optional"}, {Name: "required", Required: true}},
Templates: CompiledTemplates{Messages: []*template.Template{
compileRequirementTemplate(t, "message", definition.Templates[0].Content),
}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if result.Complete || len(result.Issues) != 2 || result.Issues[0].Code != domain.RequirementIssueDynamicInputName ||
result.Issues[1].Code != domain.RequirementIssueMissingVariable {
t.Fatalf("missing dynamic name did not report its limitations: %+v", result)
}
if result.Inputs[0].Name != "optional" || result.Inputs[0].Kind != domain.RequirementConditional || result.Inputs[0].Explanation == "" ||
result.Inputs[1].Name != "required" || result.Inputs[1].Kind != domain.RequirementRequired || len(result.Variables) != 1 ||
result.Variables[0].Name != "missing" || result.Variables[0].Kind != domain.RequirementRequired {
t.Fatalf("unknown name effects lost known classifications: %+v", result)
}
computed := compileRequirementTemplate(t, "computed", `{{input (printf "%s" .prefix)}}`)
computedResult, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Inputs: []InputMetadata{{Name: "declared"}}, Templates: CompiledTemplates{Messages: []*template.Template{computed}},
}, map[string]string{"prefix": "SENSITIVE-VALUE"}, false)
if err != nil {
t.Fatal(err)
}
if computedResult.Complete || len(computedResult.Inputs) != 1 || computedResult.Inputs[0].Name != "declared" ||
computedResult.Inputs[0].Kind != domain.RequirementConditional || len(computedResult.Variables) != 1 ||
computedResult.Variables[0].Name != "prefix" || computedResult.Variables[0].Kind != domain.RequirementConditional {
t.Fatalf("computed-name uncertainty did not affect optional entries: %+v", computedResult)
}
for _, issue := range computedResult.Issues {
if strings.Contains(issue.Description, "SENSITIVE-VALUE") || issue.InputName == "SENSITIVE-VALUE" || issue.VariableName == "SENSITIVE-VALUE" {
t.Fatalf("input-name source value leaked into diagnostics: %+v", issue)
}
}
for _, input := range computedResult.Inputs {
if strings.Contains(input.Explanation, "SENSITIVE-VALUE") {
t.Fatalf("input-name source value leaked into an explanation: %+v", input)
}
}
for _, variable := range computedResult.Variables {
if strings.Contains(variable.Explanation, "SENSITIVE-VALUE") {
t.Fatalf("input-name source value leaked into an explanation: %+v", variable)
}
}
for _, expression := range []string{`{{input (input "selector")}}`, `{{input "selector" | input}}`} {
t.Run(expression, func(t *testing.T) {
contentName := compileRequirementTemplate(t, "content-name", expression)
contentResult, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{contentName}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if contentResult.Complete || len(contentResult.Issues) != 2 ||
contentResult.Issues[0].Code != domain.RequirementIssueDynamicInputName ||
contentResult.Issues[1].Code != domain.RequirementIssueInputContent || contentResult.Issues[1].InputName != "selector" || len(contentResult.Inputs) != 1 ||
contentResult.Inputs[0].Name != "selector" || contentResult.Inputs[0].Kind != domain.RequirementRequired {
t.Fatalf("input-body-derived name did not preserve the known source dependency: %+v", contentResult)
}
})
}
}
func TestAnalyzeRequirementsDoesNotResolveSkippedComputedInputNames(t *testing.T) {
compiled := compileRequirementTemplate(t, "inactive-name", `{{if false}}{{input .name}}{{input (printf "%s" .other)}}{{"piped-literal" | input}}{{template "missing" .arg}}{{end}}`)
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{compiled}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if !result.Complete || len(result.Issues) != 0 || len(result.Inputs) != 1 || result.Inputs[0].Name != "piped-literal" ||
result.Inputs[0].Kind != domain.RequirementOptional || result.Inputs[0].Explanation == "" || len(result.Variables) != 3 {
t.Fatalf("skipped computed expressions introduced uncertainty or names: %+v", result)
}
for _, variable := range result.Variables {
if variable.Kind != domain.RequirementOptional || variable.Explanation == "" {
t.Errorf("skipped variable %q was not retained as inactive optional: %+v", variable.Name, variable)
}
}
}
func TestAnalyzeRequirementsPromotesOptionalVariablesWhenRootScopeIsUnknown(t *testing.T) {
compiled := compileRequirementTemplate(t, "scope", `{{index . "optional"}} {{.required}} {{template "called" .}}{{define "called"}}{{$.hidden}}{{end}}`)
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{compiled}},
}, map[string]string{"required": "value"}, false)
if err != nil {
t.Fatal(err)
}
if result.Complete || len(result.Variables) != 2 || result.Variables[0].Name != "optional" ||
result.Variables[0].Kind != domain.RequirementConditional || result.Variables[0].Explanation == "" ||
result.Variables[1].Name != "required" || result.Variables[1].Kind != domain.RequirementRequired {
t.Fatalf("unknown caller scope did not affect only optional known entries: %+v", result)
}
}
func TestAnalyzeRequirementsLeavesAssignmentDerivedNamesPartial(t *testing.T) {
compiled := compileRequirementTemplate(t, "assignment", `{{$alias := .source}}{{input $alias}}`)
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Inputs: []InputMetadata{{Name: "known-optional"}}, Templates: CompiledTemplates{Messages: []*template.Template{compiled}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if result.Complete || len(result.Issues) == 0 || len(result.Inputs) != 1 ||
result.Inputs[0].Name != "known-optional" || result.Inputs[0].Kind != domain.RequirementConditional ||
len(result.Variables) != 1 || result.Variables[0].Name != "source" || result.Variables[0].Kind != domain.RequirementConditional {
t.Fatalf("assignment-derived input name was interpreted or lost: %+v", result)
}
}
func TestAnalyzeRequirementsSupportsEmptyTemplatesAndCancellation(t *testing.T) { func TestAnalyzeRequirementsSupportsEmptyTemplatesAndCancellation(t *testing.T) {
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{ result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{compileRequirementTemplate(t, "message", "plain text")}}, Templates: CompiledTemplates{Messages: []*template.Template{compileRequirementTemplate(t, "message", "plain text")}},