Avoid rescanning unsupported input names

This commit is contained in:
2026-10-08 21:18:45 +00:00
parent e69a847cf2
commit c7e22c0dbe
4 changed files with 39 additions and 5 deletions

View File

@@ -80,6 +80,8 @@ entries are reported as conditional rather than inactive-only or definitively
optional. Direct root-map `with` forms retain caller-map identity. Known scalar
values remain separate from unknown scopes; accesses through untracked local
maps report incomplete discovery rather than silently losing caller keys.
Unsupported name expressions reuse value analysis's conservative syntax scan
instead of recursively rescanning the same subtree.
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

View File

@@ -56,6 +56,11 @@ discovery. Retain known-string dot isolation and inactive-path behavior.
### Finding 2 — Avoid Recursive Rescanning Of Unsupported Names (P2)
**Status: Resolved.** The duplicate fallback traversal was removed. A focused
source-relative work regression failed before the fix (4,096 accesses for a
277-byte expression) and passed afterward, preserving partial-result metadata.
No production limits or caches were added; prompt/use-case race tests passed.
Location: [requirement walker](../../internal/prompt/requirements.go),
`evaluateInputName` (lines 231–244), through `evaluateValue`,
`evaluateCommand`, and `visitCommand`.

View File

@@ -253,11 +253,9 @@ func (walker *requirementWalker) evaluateInputName(node parse.Node, reach requir
}
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)
}
// 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)
}

View File

@@ -77,6 +77,35 @@ func TestAnalyzeRequirementsUnknownScopesAffectOptionalEntriesOnlyWhenActive(t *
}
}
func TestRequirementWalkerDoesNotRescanNestedUnsupportedNames(t *testing.T) {
for _, depth := range []int{6, 12} {
expression := ".selector"
for level := 0; level < depth; level++ {
expression = `input (printf "%s" (` + expression + `))`
}
source := "{{" + expression + "}}"
compiled := compileRequirementTemplate(t, "computed-name", source)
walker := &requirementWalker{
ctx: context.Background(), vars: map[string]string{"selector": "binding"},
inputs: make(map[string]*inputEvidence), variables: make(map[string]*variableEvidence),
issues: make(map[requirementIssueKey]domain.RequirementIssue), complete: true, rootKnown: true,
}
if err := walker.walkTemplate(compiled); err != nil {
t.Fatal(err)
}
// A generous source-relative bound detects exponential rescanning, not
// an exact visit count or a production analysis limit.
if walker.dependencies > len(source)*8 {
t.Fatalf("depth=%d: %d accesses for %d bytes of source", depth, walker.dependencies, len(source))
}
result := walker.result(domain.RequirementInspection{})
if result.Complete || len(result.Variables) != 1 || result.Variables[0].Name != "selector" ||
result.Variables[0].Kind != domain.RequirementConditional || len(result.Issues) == 0 {
t.Fatalf("nested unsupported names lost discovery metadata: %+v", result)
}
}
}
func TestAnalyzeRequirementsCollectsDeclaredAndLiteralInputs(t *testing.T) {
description := "retained metadata"
definition := &domain.PromptDefinition{ID: "capture", Version: "v1"}