Resolve direct prompt input names
This commit is contained in:
@@ -70,10 +70,17 @@ dependencies do not by themselves make discovery incomplete. Recognizable
|
||||
references on known skipped paths remain optional inventory entries with an
|
||||
inactive-use explanation, unless another applicable use establishes a
|
||||
stronger classification. Skipped expressions are scanned only for
|
||||
syntactically recognizable names and are not evaluated. 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.
|
||||
syntactically recognizable names and are not evaluated. A direct caller value
|
||||
or literal-key lookup can also supply an input name, including through simple
|
||||
parentheses or a direct pipeline to `input`; names are used exactly as
|
||||
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
|
||||
|
||||
|
||||
@@ -305,6 +305,11 @@ Run `go test ./internal/prompt`; commit.
|
||||
|
||||
## 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
|
||||
constructs return useful partial inventories without speculative analysis.
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ type conditionEvaluation struct {
|
||||
value any
|
||||
known bool
|
||||
unsupported bool
|
||||
inputNameUnknown bool
|
||||
missingVariables 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 {
|
||||
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 {
|
||||
@@ -86,19 +90,29 @@ func (walker *requirementWalker) evaluateCommand(command *parse.CommandNode, rea
|
||||
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)
|
||||
}
|
||||
if reach == requirementInactive {
|
||||
_ = walker.visitCommand(command, reach, dot)
|
||||
return conditionEvaluation{}
|
||||
for _, argument := range command.Args[1:] {
|
||||
_ = walker.visit(argument, requirementPossible, dot)
|
||||
}
|
||||
before := walker.dependencies
|
||||
_ = walker.visitCommand(command, requirementPossible, dot)
|
||||
return conditionEvaluation{unsupported: walker.dependencies != before}
|
||||
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) {
|
||||
@@ -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 {
|
||||
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)),
|
||||
}
|
||||
@@ -252,16 +340,18 @@ func branchReach(parent requirementReachability, known, truth, selected bool) re
|
||||
}
|
||||
|
||||
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
|
||||
dependencies int
|
||||
templateSet *template.Template
|
||||
visitedTrees map[*parse.Tree]bool
|
||||
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
|
||||
@@ -453,6 +543,7 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
|
||||
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 {
|
||||
@@ -499,6 +590,17 @@ func (walker *requirementWalker) visitPipe(pipe *parse.PipeNode, reach requireme
|
||||
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 {
|
||||
@@ -516,6 +618,7 @@ func (walker *requirementWalker) visitPipe(pipe *parse.PipeNode, reach requireme
|
||||
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.")
|
||||
}
|
||||
}
|
||||
@@ -529,24 +632,34 @@ func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach
|
||||
}
|
||||
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:] {
|
||||
argumentReach := requirementPossible
|
||||
if reach == requirementInactive {
|
||||
argumentReach = requirementInactive
|
||||
}
|
||||
if err := walker.visit(argument, argumentReach, dot); err != nil {
|
||||
if err := walker.visit(argument, requirementPossible, dot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if reach != requirementInactive {
|
||||
walker.addIssue("", "", "An input name is not a direct string literal.")
|
||||
}
|
||||
walker.unresolvedInputName(conditionEvaluation{unsupported: true})
|
||||
return nil
|
||||
}
|
||||
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 reach != requirementInactive {
|
||||
walker.unknownVariableEffect = true
|
||||
walker.addIssue("", "", "The root template binding is not known.")
|
||||
}
|
||||
return
|
||||
@@ -736,7 +850,8 @@ func (walker *requirementWalker) recordConditionLimitation(condition conditionEv
|
||||
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) {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
@@ -745,6 +860,10 @@ func (walker *requirementWalker) result(result domain.RequirementInspection) dom
|
||||
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),
|
||||
@@ -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 })
|
||||
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,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"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) {
|
||||
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
|
||||
Templates: CompiledTemplates{Messages: []*template.Template{compileRequirementTemplate(t, "message", "plain text")}},
|
||||
|
||||
Reference in New Issue
Block a user