Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cmd/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -147,9 +148,9 @@ func buildContextEnvelopeWithDeps(parent context.Context, root, prompt string, c
// Intent classification (if prompt provided)
if prompt != "" {
topK := projCfg.RoutingTopKOrDefault()
mentions := extractMentionedFiles(strings.ReplaceAll(prompt, `\`, "/"), topK)
files := resolveExactConfiguredFiles(mentions, inputs.fileSet)
intent := classifyIntent(prompt, files, inputs.info, projCfg)
resolution := resolveContextFileResolutionWithCase(prompt, inputs.files, projCfg, topK, runtime.GOOS == "windows")
files := resolution.files
intent := classifyIntentWithResolution(prompt, resolution, inputs.info, projCfg)
envelope.Intent = &intent

// Match skills against intent
Expand Down
40 changes: 14 additions & 26 deletions cmd/context_evidence.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ type contextEnvelopeDeps struct {
type contextRequestInputs struct {
state *watch.State
files []scanner.FileInfo
fileSet map[string]string
info *hubInfo
evidence GraphEvidence
fileScanOK bool
Expand All @@ -67,7 +66,6 @@ func defaultContextEnvelopeDeps() contextEnvelopeDeps {
func loadContextRequestInputs(ctx context.Context, root string, deps contextEnvelopeDeps) contextRequestInputs {
inputs := contextRequestInputs{
state: deps.readState(root),
fileSet: make(map[string]string),
evidence: unavailableGraphEvidence(graphEvidenceScanFailed),
}

Expand All @@ -78,12 +76,6 @@ func loadContextRequestInputs(ctx context.Context, root string, deps contextEnve
}
inputs.files = files
inputs.fileScanOK = true
for _, file := range files {
normalized := normalizeContextPath(file.Path)
if normalized != "" {
inputs.fileSet[normalized] = file.Path
}
}

if len(files) == 0 {
inputs.info = &hubInfo{Importers: map[string][]string{}, Imports: map[string][]string{}}
Expand Down Expand Up @@ -157,28 +149,24 @@ func sortedGraphHubs(importers map[string][]string) []string {
}

func normalizeContextPath(file string) string {
file = strings.TrimSpace(strings.ReplaceAll(file, `\`, "/"))
return normalizeContextPathWithVolumeGuard(strings.TrimSpace(file), true)
}

func normalizeContextInventoryPath(file string) string {
return normalizeContextPathWithVolumeGuard(file, false)
}

func normalizeContextPathWithVolumeGuard(file string, rejectVolume bool) string {
file = strings.ReplaceAll(file, `\`, "/")
volumePath := isContextVolumePath(file)
file = strings.TrimPrefix(pathpkg.Clean(file), "./")
if file == "." || file == "" || strings.HasPrefix(file, "../") || file == ".." || pathpkg.IsAbs(file) {
if file == "." || file == "" || strings.HasPrefix(file, "../") || file == ".." || pathpkg.IsAbs(file) || (rejectVolume && volumePath) {
return ""
}
return file
}

func resolveExactConfiguredFiles(mentions []string, fileSet map[string]string) []string {
resolved := make([]string, 0, len(mentions))
seen := make(map[string]struct{})
for _, mention := range mentions {
normalized := normalizeContextPath(mention)
actual, ok := fileSet[normalized]
if !ok {
continue
}
if _, duplicate := seen[actual]; duplicate {
continue
}
seen[actual] = struct{}{}
resolved = append(resolved, actual)
}
return resolved
func isContextVolumePath(file string) bool {
return strings.HasPrefix(file, "//") ||
(len(file) >= 2 && file[1] == ':' && ((file[0] >= 'a' && file[0] <= 'z') || (file[0] >= 'A' && file[0] <= 'Z')))
}
165 changes: 165 additions & 0 deletions cmd/context_routing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package cmd

import (
pathpkg "path"
"regexp"
"sort"
"strings"

"codemap/config"
"codemap/scanner"
)

var contextRoutingTokenPattern = regexp.MustCompile(`[A-Za-z0-9_@./\\-]*[A-Za-z0-9_@]`)

type contextFileIndex struct {
caseInsensitive bool
paths []string
exact map[string][]string
basenames map[string][]string
}

type contextFileResolution struct {
files []string
explicitFiles []string
inferredFiles []string
}

func resolveContextFilesWithCase(prompt string, files []scanner.FileInfo, cfg config.ProjectConfig, topK int, caseInsensitive bool) []string {
return resolveContextFileResolutionWithCase(prompt, files, cfg, topK, caseInsensitive).files
}

func resolveContextFileResolutionWithCase(prompt string, files []scanner.FileInfo, cfg config.ProjectConfig, topK int, caseInsensitive bool) contextFileResolution {
if topK <= 0 || len(files) == 0 {
return contextFileResolution{}
}
index := newContextFileIndex(files, caseInsensitive)
tokens := contextRoutingTokens(prompt)
resolution := contextFileResolution{files: make([]string, 0, topK)}
seen := make(map[string]struct{})
add := func(path string, inferred bool) bool {
if path == "" {
return false
}
if _, duplicate := seen[path]; duplicate {
return false
}
seen[path] = struct{}{}
resolution.files = append(resolution.files, path)
if inferred {
resolution.inferredFiles = append(resolution.inferredFiles, path)
} else {
resolution.explicitFiles = append(resolution.explicitFiles, path)
}
return len(resolution.files) >= topK
}

// Exact normalized repository-relative paths always win.
for _, token := range tokens {
normalized := normalizeContextPath(token)
matches := index.exact[index.key(normalized)]
if normalized != "" && len(matches) == 1 && add(matches[0], false) {
return resolution
}
}

// Then accept a unique basename that includes its extension.
for _, token := range tokens {
normalized := normalizeContextPath(token)
if strings.Contains(normalized, "/") {
continue
}
base := pathpkg.Base(normalized)
if pathpkg.Ext(base) == "" {
continue
}
matches := index.basenames[index.key(base)]
if len(matches) == 1 && add(matches[0], false) {
return resolution
}
}

// Configured subsystem routes are bounded last-resort file candidates.
for _, subsystemIndex := range contextSubsystemMatches(prompt, cfg, cfg.RoutingTopKOrDefault()) {
subsystem := cfg.Routing.Subsystems[subsystemIndex]
for _, prefix := range subsystem.Paths {
prefix = normalizeContextPath(prefix)
if prefix == "" {
continue
}
prefixKey := index.key(prefix)
for _, path := range index.paths {
pathKey := index.key(path)
if (pathKey == prefixKey || strings.HasPrefix(pathKey, prefixKey+"/")) && add(path, true) {
return resolution
}
}
}
}
return resolution
}

func newContextFileIndex(files []scanner.FileInfo, caseInsensitive bool) contextFileIndex {
index := contextFileIndex{
caseInsensitive: caseInsensitive,
exact: make(map[string][]string, len(files)),
basenames: make(map[string][]string),
}
for _, file := range files {
path := normalizeContextInventoryPath(file.Path)
if path == "" {
continue
}
if _, duplicate := index.exact[path]; duplicate && !caseInsensitive {
continue
}
pathKey := index.key(path)
duplicate := false
for _, existing := range index.exact[pathKey] {
if existing == path {
duplicate = true
break
}
}
if duplicate {
continue
}
index.paths = append(index.paths, path)
index.exact[pathKey] = append(index.exact[pathKey], path)
base := pathpkg.Base(path)
index.basenames[index.key(base)] = append(index.basenames[index.key(base)], path)
}
sort.Strings(index.paths)
return index
}

func (i contextFileIndex) key(value string) string {
if i.caseInsensitive {
return strings.ToLower(value)
}
return value
}

func contextRoutingTokens(prompt string) []string {
matches := contextRoutingTokenPattern.FindAllString(strings.ReplaceAll(prompt, `\`, "/"), -1)
tokens := make([]string, 0, len(matches))
for _, match := range matches {
if match != "" {
tokens = append(tokens, match)
}
}
return tokens
}

func contextSubsystemMatches(prompt string, cfg config.ProjectConfig, topK int) []int {
matches := matchSubsystemRoutes(prompt, cfg, topK)
if len(matches) == 0 {
return nil
}

indices := make([]int, len(matches))
for i, match := range matches {
indices[i] = match.Index
}
return indices
}
Loading
Loading