返回 DeepSeek-Reasonix
named_paths.go
根目录 / internal / evidence / named_paths.go
1 package evidence
2
3 import (
4 "path/filepath"
5 "slices"
6 "strings"
7 )
8
9 // namedPathMaxExt bounds what counts as a file extension so ordinary prose
10 // such as "i.e." is never scored as a location handed to a child.
11 const namedPathMaxExt = 5
12
13 // NamedPaths returns the distinct locations a delegation's own text names.
14 // Recognition leans inclusive on purpose: a spurious token inflates the hint
15 // count and matches no receipt, while a miss would flatter the parent twice,
16 // once as a smaller hint and once as evidence credited to the child.
17 func NamedPaths(text string) []string {
18 seen := map[string]bool{}
19 var out []string
20 for _, token := range strings.FieldsFunc(text, namedPathBreak) {
21 p := namedPathToken(token)
22 if p == "" || seen[p] {
23 continue
24 }
25 seen[p] = true
26 out = append(out, p)
27 }
28 slices.Sort(out)
29 return out
30 }
31
32 // SplitNamedPaths separates a delegation's locations into directory-level
33 // scope and file-level naming. A parent must narrow the search to hand off
34 // work at all, so scope is the cost of delegating; naming the file says where
35 // the answer is. Counting them as one number would report a minimal, honest
36 // scope hint as though it were the whole conclusion handed over.
37 func SplitNamedPaths(named []string) (scope, files []string) {
38 for _, p := range named {
39 if hasFileExtension(filepath.Base(p)) {
40 files = append(files, p)
41 continue
42 }
43 scope = append(scope, p)
44 }
45 return scope, files
46 }
47
48 // UnderNamedPath reports whether a path the child produced evidence for was
49 // already named by the delegation. Matching is on whole path segments: a
50 // delegation writes workspace-relative prose while a receipt records the
51 // absolute path the tool actually received, so equality would never hold.
52 func UnderNamedPath(named []string, path string) bool {
53 p := namedPathSegments(path)
54 if p == "" {
55 return false
56 }
57 for _, n := range named {
58 seg := namedPathSegments(n)
59 if seg != "" && strings.Contains(p, seg) {
60 return true
61 }
62 }
63 return false
64 }
65
66 func namedPathBreak(r rune) bool {
67 switch r {
68 case '`', '"', '\'', '(', ')', '[', ']', '{', '}', '<', '>', ',', ';', '|', '*':
69 return true
70 }
71 return r == ' ' || r == '\t' || r == '\n' || r == '\r'
72 }
73
74 // namedPathToken reduces one whitespace-delimited token to the path it refers
75 // to, or "" when it refers to none.
76 func namedPathToken(token string) string {
77 token = strings.TrimPrefix(token, "@")
78 token = strings.TrimRight(token, ".!?")
79 token = trimLineRef(token)
80 token = strings.TrimRight(token, ":")
81 if token == "" || strings.Contains(token, "://") {
82 return ""
83 }
84 if !strings.Contains(token, "/") && !hasFileExtension(token) {
85 return ""
86 }
87 return normalizePath(token)
88 }
89
90 // trimLineRef drops a ":184" or ":181-207" citation so a cited range and a
91 // plain mention of the same file count as one location.
92 func trimLineRef(token string) string {
93 for {
94 i := strings.LastIndexByte(token, ':')
95 if i < 0 || i == len(token)-1 || !isLineRef(token[i+1:]) {
96 return token
97 }
98 token = token[:i]
99 }
100 }
101
102 func isLineRef(s string) bool {
103 digits := false
104 for _, r := range s {
105 switch {
106 case r >= '0' && r <= '9':
107 digits = true
108 case r == '-':
109 default:
110 return false
111 }
112 }
113 return digits
114 }
115
116 // hasFileExtension accepts a dotted token only when the stem is at least two
117 // characters, which is what separates "parser.go" from "i.e." and "e.g.".
118 func hasFileExtension(token string) bool {
119 dot := strings.LastIndexByte(token, '.')
120 if dot < 2 || dot == len(token)-1 {
121 return false
122 }
123 ext := token[dot+1:]
124 if len(ext) > namedPathMaxExt {
125 return false
126 }
127 for _, r := range ext {
128 alnum := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')
129 if !alnum {
130 return false
131 }
132 }
133 return true
134 }
135
136 // namedPathSegments wraps a path in separators so a substring test can only
137 // match on whole segments: "/parser.go/" never matches "/myparser.go/".
138 func namedPathSegments(p string) string {
139 p = strings.Trim(filepath.ToSlash(normalizePath(p)), "/")
140 if p == "" || p == "." {
141 return ""
142 }
143 return "/" + p + "/"
144 }
145
145 lines GO