Skip to content

feat(context): Resolve bounded lexical file mentions - #167

Merged
JordanCoin merged 3 commits into
JordanCoin:mainfrom
reneleonhardt:feat/context-lexical-routing
Sep 2, 2026
Merged

feat(context): Resolve bounded lexical file mentions#167
JordanCoin merged 3 commits into
JordanCoin:mainfrom
reneleonhardt:feat/context-lexical-routing

Conversation

@reneleonhardt

Copy link
Copy Markdown
Contributor

What does this PR do?

  • Resolves exact paths, unique extension basenames, unique stems, and configured subsystem paths.
  • Normalizes repository-relative paths across slash styles and rejects traversal, absolute, volume, or ambiguous case-folded matches.
  • Feeds resolved files into the existing evidence-backed intent risk model.

This makes common agentic coding prompts such as refactor final_build useful without fuzzy dependency false positives.

Type of change

  • Bug fix
  • New feature
  • New language support
  • Documentation
  • Other

Checklist

  • Tested locally with Go 1.27.0: go test ./...
  • Verified traversal, ambiguity, case, duplicate subsystem, and top-k boundaries
  • Verified with go vet ./...

Additional notes

Routing is inventory-only and performs no semantic search or unbounded expansion.

Resolve normalized repository-relative paths and bounded subsystem routes before intent risk analysis. Reject unsafe and ambiguous mentions.
@reneleonhardt
reneleonhardt force-pushed the feat/context-lexical-routing branch from f54fc94 to c030bb9 Compare September 2, 2026 06:17

Copy link
Copy Markdown
Owner

Reviewed this closely and built binaries from both f9ed58d (main) and c030bb9 to compare. The exact-path and basename passes look right to me, and the len(matches) == 1 ambiguity guards are genuinely load-bearing — I mutated each to >= 1 and the corresponding test failed every time. Nice.

The problem is what passes 3 and 4 do to TaskIntent.

Blocking: passes 3 and 4 manufacture a risk verdict from prompts that mention no file

TaskIntent.Files is documented in cmd/intent.go:15 as // mentioned files, and it is the sole gate on analyzeRiskclassifyIntent only computes a risk level when len(files) > 0. So anything that puts a file in that list converts an honest risk: unknown into a confident one.

Pass 4 (subsystem paths). With {id: scanner, keywords: [parsing], paths: [scanner]} configured and the prompt "why is parsing slow?":

main : files=[]                                             scope=unknown  risk=unknown
#167 : files=[scanner/alpha.go beta.go delta.go epsilon.go] scope=package  risk=low

Four files chosen by alphabetical position and truncated at topK — gamma.go and zeta.go lost a coin flip, not a relevance contest. This is the same shape as #149, where blast-radius truncates alphabetically and presents the subset as the whole.

Pass 3 (stem matching) is worse, because it needs no config at all. contextRoutingTokens tokenizes the entire prompt with no stopword filter, so any ordinary English word that happens to be a unique file stem resolves. Run against codemap's own repo:

"I need a doctor's note before I can travel"
  main : files=[]                    scope=unknown      risk=unknown
  #167 : files=['cmd/doctor.go']     scope=single-file  risk=low

"please install a new espresso machine in the kitchen"
  main : files=[]                    scope=unknown      risk=unknown
  #167 : files=['plugins/install.go'] scope=single-file risk=low

"let's root out the flaky tests in CI"
  main : files=[]                    scope=unknown      risk=unknown
  #167 : files=['cmd/root.go']       scope=single-file  risk=low

"the build was slow again today"
  main : files=[]                    scope=unknown      risk=unknown
  #167 : files=['handoff/build.go']  scope=single-file  risk=low

root, setup, serve, drift, doctor, install, build and release all have unique stems in this repo. An agent reading risk: low on the espresso prompt has been told something false, and nothing in TaskIntent distinguishes "the user named this file" from "the resolver guessed it from a common word" — same field, same shape, no provenance marker. That's the distinction #138 and #111 are both circling.

The fix I'd suggest, smallest first: drop pass 3 (require an extension, keeping the basename pass), and thread a fuzzy/confidence marker through to TaskIntent so computeScope and analyzeRisk return unknown when guessed files are the only evidence. A guessed file is fine as a suggestion; it is not fine as the basis of a risk verdict.

Blocking: the two cross-cutting guarantees in the description have no test coverage

Both verified by mutation, -count=1 so nothing is cached:

  • Volume-path rejection. I replaced || isContextVolumePath(file) with || (false && isContextVolumePath(file)) — neutering the new guard entirely — and the whole TestContextLexicalRouting suite still passes, including the subtest named absolute and volume paths stay unresolved. That test's assertions hold because path.Clean collapses //server/... to a single leading / (so pathpkg.IsAbs short-circuits first) and because contextRoutingTokenPattern excludes :, so C:\tmp\target.go tokenizes to ["C", "/tmp/target.go"] and the path half is IsAbs-rejected on its own. The guard's "//" branch is unreachable dead code, and its drive-letter branch is unreachable from prompt tokens — it only does real work on subsystem.Paths and on scanner paths in newContextFileIndex.
  • Pass ordering. Swapping pass 3 above pass 1 in the function body leaves all 13 subtests green, including exact path wins before stem — its fixture has the stem and the exact path resolving to the same file, so it cannot detect a priority regression. The shipped code does implement the priority correctly (I checked with topK=1 and reversed token order), but nothing protects it.

Worth adding: a fixture where pass 1 and pass 3 resolve to different files, and a direct normalizeContextPath call that actually reaches the drive-letter branch.

Smaller things

  • contextSubsystemMatches is a near-verbatim copy of matchSubsystemRoutes (cmd/hooks.go:1275) — same scoring, same tie-break. Two copies will drift. It's also called with len(cfg.Routing.Subsystems) instead of RoutingTopKOrDefault(), so file routing considers more subsystems than intent.Subsystems reports, and a file can appear with no visible reason.
  • contextRequestInputs.fileSet (cmd/context_evidence.go:47) is still built on every call but no longer read by any non-test code.
  • Because newContextFileIndex runs real scanner paths through the volume guard, a legitimate POSIX file like x:helper.go is dropped from exact, basenames, stems and paths — unfindable even by a verbatim mention. Narrow, and fail-closed rather than wrong, but easy to avoid by applying the guard only to prompt tokens and config-authored paths.
  • Pass 4 iterates every indexed path per prefix per matched subsystem. Measured ~89ms for 40 subsystems × 5 prefixes × 20k files, on top of ~21ms index construction. context is on the editor-hook hot path; the topK mismatch above compounds it.

The exact and basename passes are a real improvement and I'd take those on their own. It's the confidence that passes 3 and 4 manufacture that I can't merge — risk: low on a prompt about an espresso machine is the failure mode this project exists to avoid. Happy to be argued out of any of it if I've misread the intent.

Baseline note: go test ./cmd/... on this branch is clean apart from TestRunSetupCreatesConfigAndHooks, which fails for anyone running as root.


Generated by Claude Code

Keep inferred candidates as suggestions without letting them drive scope or risk. Reuse the shared subsystem matcher and keep route ordering deterministic.
Preserve significant whitespace in inventory paths; trim only prompt and configuration input.
@reneleonhardt

Copy link
Copy Markdown
Contributor Author

Complied

  • Removed extensionless stem inference.
  • Added file-confidence provenance.
  • Prevented inferred candidates from driving scope or risk.
  • Added the requested volume-path and ordering coverage.

Other changes

  • Fixed mixed explicit/inferred evidence handling found during our adversarial loop.
  • Made duplicate subsystem ordering deterministic.
  • Removed dead fileSet state.
  • Fixed significant-whitespace inventory paths and added regression coverage.

Copy link
Copy Markdown
Owner

Re-verified everything at 8cd19c2 with fresh binaries. All of it holds up.

The fabrication is gone. Same prompts as before, against codemap's own repo:

"please install a new espresso machine in the kitchen"  files=[]  scope=unknown  risk=unknown
"I need a doctor's note before I can travel"            files=[]  scope=unknown  risk=unknown
"let's root out the flaky tests in CI"                  files=[]  scope=unknown  risk=unknown
"the build was slow again today"                        files=[]  scope=unknown  risk=unknown
"inspect cmd/context.go for the bug"   files=['cmd/context.go']  scope=single-file  risk=low

The file_confidence design is better than what I suggested. Inferred candidates are still surfaced — which is the useful half — but honestly labelled and firewalled from the verdict. The subsystem case now reads:

"files": ["scanner/alpha.go", "scanner/beta.go", "scanner/delta.go", "scanner/epsilon.go"],
"file_confidence": "inferred",
"scope": "unknown",
"risk": "unknown",
"suggestions": [{"reason": "risk is unknown without an explicit file mention"}]

and the mixed case computes scope and risk from the explicit file alone while still listing the inferred ones as "file_confidence": "mixed". That's the right shape — I'd only floated dropping the candidates entirely, and keeping them with provenance is more useful.

The new tests are load-bearing. I re-ran the mutations that previously passed:

  • Neutering the volume guard (rejectVolume && volumePathfalse && ...) now fails absolute_and_volume_paths_stay_unresolved. Hoisting isContextVolumePath above pathpkg.Clean also un-deadens the "//" branch, which was the underlying reason the old test couldn't see it.
  • Mislabelling exact matches as inferred fails inferred_candidates_do_not_affect_explicit_scope_or_risk and TestContextExactPathResolution/separator_normalized.
  • Labelling subsystem candidates as explicit fails both inferred_*_do_not_set_scope_or_risk tests.

Also confirmed fixed: the RoutingTopKOrDefault() mismatch, the dead fileSet, and the real-scanner-path rejection (normalizeContextInventoryPath keeps files like x:helper.go and whitespace-significant names in the index).

go vet ./... clean; go test ./cmd/... ./config/... at baseline (only TestRunSetupCreatesConfigAndHooks, which fails for anyone running as root). CI 12/12.

Two things I'm deliberately not holding this for, noting them for later: contextSubsystemMatches still duplicates matchSubsystemRoutes scoring, and the subsystem pass is still O(subsystems × prefixes × files). Neither affects correctness.

Merging. Thanks for the quick turnaround on this one — the provenance field is a genuine improvement to the contract, not just a patch over my complaint.


Generated by Claude Code

@JordanCoin
JordanCoin merged commit 5852e72 into JordanCoin:main Sep 2, 2026
12 checks passed
@reneleonhardt
reneleonhardt deleted the feat/context-lexical-routing branch September 3, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants