diff --git a/docs/internal/sources.md b/docs/internal/sources.md index 569538d..73f43a0 100644 --- a/docs/internal/sources.md +++ b/docs/internal/sources.md @@ -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 diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 57ffefe..66b9177 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -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`. diff --git a/internal/prompt/requirements.go b/internal/prompt/requirements.go index c73fc98..f918231 100644 --- a/internal/prompt/requirements.go +++ b/internal/prompt/requirements.go @@ -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) } diff --git a/internal/prompt/requirements_test.go b/internal/prompt/requirements_test.go index e898243..77e9506 100644 --- a/internal/prompt/requirements_test.go +++ b/internal/prompt/requirements_test.go @@ -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"}