codebase-onboarder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/bin/onboarder.js +14 -0
  4. package/bin/postinstall.js +6 -0
  5. package/cli/commands.js +436 -0
  6. package/cli/main.js +129 -0
  7. package/cli/prompt.js +101 -0
  8. package/cli/ui.js +55 -0
  9. package/cli/wizard.js +325 -0
  10. package/package.json +48 -0
  11. package/public/app.js +1339 -0
  12. package/public/index.html +455 -0
  13. package/public/js/about.js +284 -0
  14. package/public/js/aiDraft.js +163 -0
  15. package/public/js/analysisPanel.js +270 -0
  16. package/public/js/analysisReport.js +154 -0
  17. package/public/js/api.js +264 -0
  18. package/public/js/atlas.js +98 -0
  19. package/public/js/blameView.js +41 -0
  20. package/public/js/codeTab.js +256 -0
  21. package/public/js/codeViewer.js +184 -0
  22. package/public/js/components/FileChip.js +102 -0
  23. package/public/js/components/MetricSparkline.js +89 -0
  24. package/public/js/components/RiskBadge.js +105 -0
  25. package/public/js/deepAnalysisView.js +496 -0
  26. package/public/js/diagramPane.js +154 -0
  27. package/public/js/diffView.js +352 -0
  28. package/public/js/docsView.js +566 -0
  29. package/public/js/fileSourceBrowser.js +47 -0
  30. package/public/js/flameGraph.js +75 -0
  31. package/public/js/forceGraph.js +592 -0
  32. package/public/js/heatmap.js +185 -0
  33. package/public/js/highlight.js +122 -0
  34. package/public/js/html.js +50 -0
  35. package/public/js/insightsView.js +236 -0
  36. package/public/js/inspector.js +535 -0
  37. package/public/js/llm.js +345 -0
  38. package/public/js/mapView.js +159 -0
  39. package/public/js/markdown.js +119 -0
  40. package/public/js/mindmap.js +289 -0
  41. package/public/js/repoFiles.js +44 -0
  42. package/public/js/sbomView.js +142 -0
  43. package/public/js/scanCache.js +76 -0
  44. package/public/js/search.js +874 -0
  45. package/public/js/serverSettings.js +169 -0
  46. package/public/js/state.js +192 -0
  47. package/public/js/tour.js +17 -0
  48. package/public/js/transitions.js +15 -0
  49. package/public/js/tree.js +200 -0
  50. package/public/js/workflowsView.js +95 -0
  51. package/public/styles.css +2767 -0
  52. package/public/vendor/mermaid.min.js +3587 -0
  53. package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  54. package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  55. package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  56. package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  57. package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  58. package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  59. package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  60. package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  61. package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  62. package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  63. package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  64. package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  65. package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  66. package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  67. package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  68. package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  69. package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  70. package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  71. package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  72. package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  73. package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  74. package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  75. package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  76. package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  77. package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  78. package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  79. package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  80. package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  81. package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  82. package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  83. package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  84. package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  85. package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  86. package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  87. package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  88. package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  89. package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  90. package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  91. package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  92. package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  93. package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  94. package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  95. package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  96. package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  97. package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  98. package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  99. package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  100. package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  101. package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  102. package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  103. package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  104. package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  105. package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  106. package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  107. package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  108. package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  109. package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  110. package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  111. package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  112. package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  113. package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  114. package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  115. package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  116. package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  117. package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  118. package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  119. package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  120. package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  121. package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  122. package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  123. package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  124. package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  125. package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  126. package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  127. package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  128. package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  129. package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  130. package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  131. package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  132. package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  133. package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  134. package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  135. package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  136. package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  137. package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  138. package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  139. package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  140. package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  141. package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  142. package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  143. package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  144. package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  145. package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  146. package/public/vendor/monaco/vs/loader.js +11 -0
  147. package/public/vendor/monaco/worker-boot.js +9 -0
  148. package/server/.fuse_hidden0000000800000001 +36 -0
  149. package/server/apiDiff.js +25 -0
  150. package/server/apiDocs.js +67 -0
  151. package/server/apiFile.js +36 -0
  152. package/server/apiGitBlame.js +63 -0
  153. package/server/apiMcp.js +110 -0
  154. package/server/apiScan.js +119 -0
  155. package/server/apiSearch.js +253 -0
  156. package/server/apiSettings.js +118 -0
  157. package/server/apiTools.js +90 -0
  158. package/server/config.js +227 -0
  159. package/server/fileSourceNode.js +44 -0
  160. package/server/gitClone.js +95 -0
  161. package/server/gitDiff.js +184 -0
  162. package/server/gitHistory.js +110 -0
  163. package/server/htmlText.js +44 -0
  164. package/server/http.js +55 -0
  165. package/server/httpGuards.js +87 -0
  166. package/server/index.js +156 -0
  167. package/server/llmProxy.js +162 -0
  168. package/server/logger.js +29 -0
  169. package/server/mcp/analysis.js +209 -0
  170. package/server/mcp/http.js +213 -0
  171. package/server/mcp/runner.js +278 -0
  172. package/server/mcp/server.js +241 -0
  173. package/server/mcp/standalone.js +42 -0
  174. package/server/mcp/tools.js +683 -0
  175. package/server/paths.js +39 -0
  176. package/server/router.js +218 -0
  177. package/server/searchIndex.js +118 -0
  178. package/server/sessions.js +163 -0
  179. package/server/static.js +59 -0
  180. package/server/tools/install.js +246 -0
  181. package/server/tools/parse.js +170 -0
  182. package/server/tools/platform.js +91 -0
  183. package/server/tools/registry.js +212 -0
  184. package/server/tools/scan.js +136 -0
  185. package/server/tools.js +212 -0
  186. package/server/tunnel.js +92 -0
  187. package/shared/analyzer/docs.js +135 -0
  188. package/shared/analyzer/explainLocal.js +163 -0
  189. package/shared/analyzer/graph.js +461 -0
  190. package/shared/analyzer/health.js +215 -0
  191. package/shared/analyzer/history.js +146 -0
  192. package/shared/analyzer/languages/csharp.js +39 -0
  193. package/shared/analyzer/languages/generic.js +131 -0
  194. package/shared/analyzer/languages/go.js +70 -0
  195. package/shared/analyzer/languages/index.js +43 -0
  196. package/shared/analyzer/languages/java.js +39 -0
  197. package/shared/analyzer/languages/javascript.js +240 -0
  198. package/shared/analyzer/languages/python.js +120 -0
  199. package/shared/analyzer/languages/rust.js +42 -0
  200. package/shared/analyzer/languages/typescript.js +138 -0
  201. package/shared/analyzer/licenses.js +151 -0
  202. package/shared/analyzer/metrics.js +120 -0
  203. package/shared/analyzer/pathUtil.js +69 -0
  204. package/shared/analyzer/patterns.js +272 -0
  205. package/shared/analyzer/scan.js +473 -0
  206. package/shared/analyzer/security.js +187 -0
  207. package/shared/analyzer/services.js +157 -0
  208. package/shared/analyzer/stack.js +173 -0
  209. package/shared/analyzer/tour.js +69 -0
  210. package/shared/analyzer/util.js +123 -0
  211. package/shared/analyzer/workflows.js +143 -0
  212. package/shared/diagram/aiFacts.js +145 -0
  213. package/shared/diagram/aiMermaid.js +59 -0
  214. package/shared/diagram/atlas.js +93 -0
  215. package/shared/diagram/mermaid.js +459 -0
  216. package/shared/search/query.js +518 -0
@@ -0,0 +1,518 @@
1
+ // One query language, two places it runs.
2
+ //
3
+ // The palette in the browser and `POST /api/search` on the server have to agree
4
+ // about what a query means. If they didn't, `ext:js` would filter the file list
5
+ // and be treated as literal text by the code search — the same keystrokes
6
+ // answering two different questions. So the parser lives here, in the
7
+ // isomorphic layer, and both sides import it: the tab for the file and symbol
8
+ // pass it can do without a round trip, the server for the TF-IDF pass over file
9
+ // contents. That is the same reasoning that put the analyzer in `shared/`, and
10
+ // it is why this module may not touch the DOM or Node (see Agent.md).
11
+ //
12
+ // The grammar is small on purpose. Every clause is optional, order does not
13
+ // matter, and a query made only of filters — `ext:js is:test` — is legal and
14
+ // useful: it means "the tests written in this language".
15
+ //
16
+ // render a bare word. Fuzzy-matched against paths and
17
+ // symbol names, substring-matched against code.
18
+ // "exact phrase" a phrase that must appear verbatim.
19
+ // /createS.*?/i a regular expression, flags optional.
20
+ // ext:js,ts only these extensions (no dot needed).
21
+ // path:server/api only paths containing this substring.
22
+ // kind:class only symbols of this kind.
23
+ // is:test, is:source the tests toggle the filter row already has.
24
+ // -vendor must NOT appear in the path.
25
+ //
26
+ // Filter clauses are strict — they are a decision the person already made.
27
+ // Bare words stay forgiving, because a half-remembered name should still find
28
+ // the file. That split is the whole design: `matchesPathFilters` is a gate,
29
+ // `scorePath`/`scoreContent` are a ranking.
30
+
31
+ import { baseName, extOf, isTestPath } from '../analyzer/pathUtil.js';
32
+
33
+ // A regex longer than this is either a paste accident or an attempt to make the
34
+ // tab hang. Real patterns people type at a search box are two orders of
35
+ // magnitude shorter than the cap.
36
+ const MAX_REGEX_LENGTH = 200;
37
+
38
+ // Quoted phrase | /regex/flags | bare token. Order matters: the quote and the
39
+ // slash have to win before `\S+` gets a chance to swallow them whole.
40
+ const TOKEN_RE = /"([^"]*)"|\/([^\s/]+)\/([gimsuy]*)|\S+/g;
41
+
42
+ // Ranges are capped as they are collected. A one-letter query against a
43
+ // minified bundle can match tens of thousands of times, and the UI only ever
44
+ // paints the first screenful.
45
+ const MAX_RANGES = 400;
46
+
47
+ function fold(text, caseSensitive) {
48
+ return caseSensitive ? text : text.toLowerCase();
49
+ }
50
+
51
+ function splitList(value) {
52
+ return value
53
+ .split(',')
54
+ .map((part) => part.trim().toLowerCase().replace(/^\./, ''))
55
+ .filter(Boolean);
56
+ }
57
+
58
+
59
+ export function parseQuery(raw, options = {}) {
60
+ const caseSensitive = !!options.caseSensitive;
61
+ const text = String(raw ?? '');
62
+ const parsed = {
63
+ raw: text,
64
+ clauses: [], // { kind, raw, start, end } — what the input was made of
65
+ terms: [], // bare words — fuzzy against paths, substring in content
66
+ phrases: [], // "quoted" — verbatim substrings
67
+ negatives: [], // -foo — must not appear in the path
68
+ exts: [], // ext:js,ts — extensions without the dot
69
+ paths: [], // path:src/api — path substrings
70
+ kinds: [], // kind:function — symbol kinds
71
+ isTest: false, // is:test
72
+ isSource: false, // is:source
73
+ regex: null, // the pattern as authored
74
+ regexSource: '',
75
+ regexFlags: '',
76
+ regexGlobal: null, // a 'g' copy used for scanning, never for matching
77
+ caseSensitive,
78
+ error: null,
79
+ };
80
+
81
+ if (!text.trim()) return parsed;
82
+
83
+ const lower = (s) => fold(s, caseSensitive);
84
+
85
+ for (const m of text.matchAll(TOKEN_RE)) {
86
+ // The span each clause came from, so the palette can turn a filter into a
87
+ // chip and take it back out of the input when the chip is dismissed.
88
+ const note = (kind) => parsed.clauses.push({
89
+ kind, raw: m[0], start: m.index, end: m.index + m[0].length,
90
+ });
91
+
92
+ if (m[1] !== undefined) {
93
+ const phrase = m[1].trim();
94
+ if (phrase) { parsed.phrases.push(lower(phrase)); note('phrase'); }
95
+ continue;
96
+ }
97
+
98
+ if (m[2] !== undefined) {
99
+ const source = m[2];
100
+ parsed.regexSource = source;
101
+ // Flags the person typed are respected exactly; with no flags at all the
102
+ // case toggle decides, which is what makes `/Foo/` and the `Aa` button
103
+ // behave the way a reader expects.
104
+ const flags = m[3] || (caseSensitive ? '' : 'i');
105
+ parsed.regexFlags = flags;
106
+ if (source.length > MAX_REGEX_LENGTH) {
107
+ parsed.error = `Regex is too long (${source.length} > ${MAX_REGEX_LENGTH}).`;
108
+ continue;
109
+ }
110
+ try {
111
+ parsed.regex = new RegExp(source, flags.replace('g', ''));
112
+ parsed.regexGlobal = new RegExp(source, flags.includes('g') ? flags : flags + 'g');
113
+ note('regex');
114
+ } catch (err) {
115
+ parsed.error = 'Bad regex: ' + err.message;
116
+ }
117
+ continue;
118
+ }
119
+
120
+ const token = m[0];
121
+ if (!token) continue;
122
+
123
+ if (token.length > 1 && token.startsWith('-')) {
124
+ parsed.negatives.push(lower(token.slice(1)));
125
+ note('neg');
126
+ continue;
127
+ }
128
+
129
+ const colon = token.indexOf(':');
130
+ if (colon > 0) {
131
+ const field = token.slice(0, colon).toLowerCase();
132
+ const value = token.slice(colon + 1);
133
+ if (!value) continue;
134
+ if (field === 'ext' || field === 'exts' || field === 'type') {
135
+ parsed.exts.push(...splitList(value));
136
+ note('ext');
137
+ continue;
138
+ }
139
+ if (field === 'path' || field === 'in') {
140
+ parsed.paths.push(lower(value));
141
+ note('path');
142
+ continue;
143
+ }
144
+ if (field === 'kind') {
145
+ parsed.kinds.push(value.toLowerCase());
146
+ note('kind');
147
+ continue;
148
+ }
149
+ if (field === 'is') {
150
+ const v = value.toLowerCase();
151
+ if (v === 'test' || v === 'tests') { parsed.isTest = true; note('is'); }
152
+ else if (v === 'source' || v === 'src') { parsed.isSource = true; note('is'); }
153
+ else { parsed.terms.push(lower(token)); note('term'); }
154
+ continue;
155
+ }
156
+ // An unrecognized `foo:bar` is more likely a real word (a URL, a
157
+ // protocol, `Read:`) than a typo'd clause. Search for it as text.
158
+ }
159
+
160
+ parsed.terms.push(lower(token));
161
+ note('term');
162
+ }
163
+
164
+ return parsed;
165
+ }
166
+
167
+ // Take one clause back out of the raw query, leaving the rest of the text
168
+ // exactly as it was typed. The palette uses this when a filter chip is
169
+ // dismissed — hand-editing a query that contains a quoted phrase is otherwise
170
+ // a counting exercise.
171
+ export function removeClause(raw, clause) {
172
+ const text = String(raw ?? '');
173
+ if (!clause) return text;
174
+ const start = Math.max(0, Math.min(Number(clause.start) || 0, text.length));
175
+ const end = Math.max(start, Math.min(Number(clause.end) || start, text.length));
176
+ const head = text.slice(0, start).replace(/\s+$/, '');
177
+ const tail = text.slice(end).replace(/^\s+/, '');
178
+ if (head && tail) return `${head} ${tail}`;
179
+ return head || tail;
180
+ }
181
+
182
+ // Is there anything to search *for*? A query of pure filters still filters —
183
+ // see `hasFilters` — but it has no text to rank with, and that difference is
184
+ // what tells the palette to stop asking the server for content matches.
185
+ export function queryIsEmpty(parsed) {
186
+ return !parsed || (
187
+ !parsed.terms.length && !parsed.phrases.length && !parsed.regex
188
+ && !parsed.negatives.length && !parsed.exts.length && !parsed.paths.length
189
+ && !parsed.kinds.length && !parsed.isTest && !parsed.isSource
190
+ );
191
+ }
192
+
193
+ export function hasFilters(parsed) {
194
+ return !!(parsed && (
195
+ parsed.negatives.length || parsed.exts.length || parsed.paths.length
196
+ || parsed.kinds.length || parsed.isTest || parsed.isSource
197
+ ));
198
+ }
199
+
200
+ // Advanced means "the strict clauses are in play": filters, a phrase, a regex,
201
+ // or the case toggle. A query of bare words alone keeps the forgiving ranked
202
+ // behaviour the palette always had — partial matches included — which is what a
203
+ // half-remembered name deserves. Anything else is a decision, and decisions are
204
+ // enforced rather than ranked.
205
+ export function isAdvanced(parsed) {
206
+ if (!parsed) return false;
207
+ return hasFilters(parsed)
208
+ || parsed.phrases.length > 0
209
+ || parsed.regex !== null
210
+ || parsed.caseSensitive;
211
+ }
212
+
213
+ // ---- the gate -------------------------------------------------------------
214
+
215
+ // Everything that can be decided from the path alone. Cheap, so it runs first
216
+ // and lets the expensive content pass skip the file entirely.
217
+ export function matchesPathFilters(parsed, path) {
218
+ if (!parsed) return true;
219
+
220
+ if (parsed.exts.length) {
221
+ // `extOf` hands back the dot (`'.js'`); the filter is stored without it,
222
+ // because `ext:js` is what people type.
223
+ const want = extOf(path).replace(/^\./, '');
224
+ if (!parsed.exts.includes(want)) return false;
225
+ }
226
+
227
+ if (parsed.paths.length) {
228
+ const folded = fold(path, parsed.caseSensitive);
229
+ for (const needle of parsed.paths) {
230
+ if (!folded.includes(needle)) return false;
231
+ }
232
+ }
233
+
234
+ if (parsed.isTest && !isTestPath(path)) return false;
235
+ if (parsed.isSource && isTestPath(path)) return false;
236
+
237
+ if (parsed.negatives.length) {
238
+ const folded = fold(path, parsed.caseSensitive);
239
+ for (const needle of parsed.negatives) {
240
+ if (folded.includes(needle)) return false;
241
+ }
242
+ }
243
+
244
+ if (parsed.kinds.length) {
245
+ // A kind filter is a statement about symbols, not paths: the caller has to
246
+ // know the symbol to answer it. Answer "yes" here and let the symbol pass
247
+ // apply it, so a kind filter never silently hides every file.
248
+ return true;
249
+ }
250
+
251
+ return true;
252
+ }
253
+
254
+ // Decimals matter only for ordering, and this keeps the ordering stable: exact
255
+ // matches win, then a prefix, then a word boundary, then anywhere.
256
+ function occurrenceScore(hay, needle, weight) {
257
+ const at = hay.indexOf(needle);
258
+ if (at === -1) return 0;
259
+ let score = weight - at;
260
+ if (at === 0) score += weight * 0.6;
261
+ else if (/[^a-z0-9]/.test(hay[at - 1])) score += weight * 0.35;
262
+ if (hay === needle) score += weight * 1.2;
263
+ return score;
264
+ }
265
+
266
+ // ---- the ranking ----------------------------------------------------------
267
+
268
+ // Subsequence match, the fzf idea: `rt` finds `router.js` because the letters
269
+ // appear in order. Adjacent runs, word boundaries and camel humps are worth
270
+ // more than letters sprinkled through a long path, which is what stops `rt`
271
+ // from ranking `reduction-table.js` above `router.js`.
272
+ //
273
+ // `raw` is kept alongside the folded haystack so the camel hump can be seen —
274
+ // folding throws away the case that carries the signal.
275
+ function fuzzyScore(needle, hay, raw) {
276
+ let score = 0;
277
+ let at = 0;
278
+ let streak = 0;
279
+ let first = -1;
280
+
281
+ for (let i = 0; i < needle.length; i++) {
282
+ const found = hay.indexOf(needle[i], at);
283
+ if (found === -1) return 0;
284
+ if (first === -1) first = found;
285
+
286
+ streak = found === at && i > 0 ? streak + 1 : 0;
287
+ score += 10 + streak * 6;
288
+
289
+ if (found === 0) score += 14;
290
+ else if (/[^a-z0-9]/.test(hay[found - 1])) score += 12;
291
+ else if (raw && /[a-z0-9]/.test(raw[found - 1] || '') && /[A-Z]/.test(raw[found] || '')) score += 10;
292
+
293
+ at = found + 1;
294
+ }
295
+
296
+ score -= first; // earlier is better
297
+ score -= Math.max(0, hay.length - needle.length) * 0.4; // shorter is better
298
+ return Math.max(1, score);
299
+ }
300
+
301
+ // How well a path answers the free-text part of the query. Filters are not
302
+ // consulted here — `matchesPathFilters` owns those.
303
+ export function scorePath(parsed, path) {
304
+ if (!parsed || queryIsEmpty(parsed)) return 1;
305
+ const hay = fold(path, parsed.caseSensitive);
306
+ const raw = path;
307
+ const base = baseName(path);
308
+ const baseHay = fold(base, parsed.caseSensitive);
309
+ let score = 0;
310
+
311
+ for (const phrase of parsed.phrases) {
312
+ if (!hay.includes(phrase)) return 0;
313
+ score += occurrenceScore(hay, phrase, 90);
314
+ }
315
+
316
+ if (parsed.regex) {
317
+ parsed.regex.lastIndex = 0;
318
+ if (!parsed.regex.test(path)) return 0;
319
+ score += 80;
320
+ }
321
+
322
+ for (const term of parsed.terms) {
323
+ const exact = baseHay === term ? 320 : 0;
324
+ if (exact) { score += exact; continue; }
325
+ const inBase = occurrenceScore(baseHay, term, 150);
326
+ if (inBase) { score += inBase; continue; }
327
+ const inPath = occurrenceScore(hay, term, 70);
328
+ if (inPath) { score += inPath; continue; }
329
+ const fuzzy = fuzzyScore(term, baseHay, base) || fuzzyScore(term, hay, raw);
330
+ if (!fuzzy) return 0;
331
+ score += fuzzy;
332
+ }
333
+
334
+ // A path with no holder for a bare term is not a match at all. (Filter-only
335
+ // queries have no terms and are already satisfied by the gate.)
336
+ if (!score && parsed.terms.length) return 0;
337
+
338
+ // Nothing to rank by: a query of pure filters is a set, not an ordering, so
339
+ // it answers with a flat 1 and skips the tie-breaking below.
340
+ if (!parsed.terms.length) return score || 1;
341
+
342
+ // Shallower files win ties. When the same name appears at three depths — a
343
+ // page, a feature folder and a barrel — the one nearest the top of the tree
344
+ // is usually the one meant. The clamp matters: a match must stay strictly
345
+ // positive, because zero is how the callers spell "no match at all".
346
+ const adjusted = score - path.split('/').length * 0.5;
347
+ return adjusted > 0 ? adjusted : 0.5;
348
+ }
349
+
350
+ // A symbol is a name plus the kind it was declared with. `kind:` and the bare
351
+ // terms both apply here, and nothing about extensions or paths does.
352
+ export function scoreSymbol(parsed, name, kind) {
353
+ if (!parsed) return 0;
354
+ const foldedName = fold(name || '', parsed.caseSensitive);
355
+ const foldedKind = (kind || '').toLowerCase();
356
+
357
+ if (parsed.kinds.length) {
358
+ const kindHit = parsed.kinds.some((k) => foldedKind.includes(k) || foldedName.endsWith(k));
359
+ if (!kindHit) return 0;
360
+ }
361
+ if (parsed.regex) {
362
+ parsed.regex.lastIndex = 0;
363
+ if (!parsed.regex.test(name || '')) return 0;
364
+ }
365
+ for (const negative of parsed.negatives) {
366
+ if (foldedName.includes(negative)) return 0;
367
+ }
368
+ for (const phrase of parsed.phrases) {
369
+ if (!foldedName.includes(phrase)) return 0;
370
+ }
371
+ if (!parsed.terms.length) return parsed.regex || parsed.phrases.length || parsed.kinds.length ? 60 : 0;
372
+
373
+ let score = 0;
374
+ for (const term of parsed.terms) {
375
+ if (foldedName === term) score += 240;
376
+ else {
377
+ const hit = occurrenceScore(foldedName, term, 130);
378
+ if (hit) score += hit;
379
+ else {
380
+ const fuzzy = fuzzyScore(term, foldedName, name);
381
+ if (!fuzzy) return 0;
382
+ score += fuzzy;
383
+ }
384
+ }
385
+ }
386
+ return score;
387
+ }
388
+
389
+ // ---- content --------------------------------------------------------------
390
+
391
+ // How well a file's text answers the query, plus the offset of the best hit so
392
+ // the caller can turn it into a line number and a snippet without searching
393
+ // again. Returns `{ score: 0, index: -1 }` when the file does not answer it.
394
+ //
395
+ // Strict here, unlike `scorePath`: text search is a deliberate act — you typed
396
+ // the words because you expect them to be there, so every term must appear.
397
+ // Negations are deliberately *not* checked here: `-vendor` is a statement about
398
+ // paths (see `matchesPathFilters`), and reading it as "this file must not
399
+ // mention the word vendor" would hide exactly the code you were looking for.
400
+ export function scoreContent(parsed, text) {
401
+ if (!parsed || !text || queryIsEmpty(parsed)) return { score: 0, index: -1 };
402
+ const hay = fold(text, parsed.caseSensitive);
403
+ let score = 0;
404
+ let best = -1;
405
+
406
+ const note = (at) => {
407
+ if (at !== -1 && (best === -1 || at < best)) best = at;
408
+ };
409
+
410
+ for (const phrase of parsed.phrases) {
411
+ const at = hay.indexOf(phrase);
412
+ if (at === -1) return { score: 0, index: -1 };
413
+ score += occurrenceScore(hay, phrase, 120);
414
+ note(at);
415
+ }
416
+
417
+ if (parsed.regex) {
418
+ parsed.regex.lastIndex = 0;
419
+ const at = hay.search(parsed.regex);
420
+ if (at === -1) return { score: 0, index: -1 };
421
+ score += 100;
422
+ note(at);
423
+ }
424
+
425
+ for (const term of parsed.terms) {
426
+ const at = hay.indexOf(term);
427
+ if (at === -1) return { score: 0, index: -1 };
428
+ score += occurrenceScore(hay, term, 60);
429
+ note(at);
430
+ }
431
+
432
+ return { score, index: best };
433
+ }
434
+
435
+ // ---- highlighting ---------------------------------------------------------
436
+
437
+ function mergeRanges(ranges) {
438
+ if (!ranges.length) return [];
439
+ const sorted = ranges.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
440
+ const out = [sorted[0]];
441
+ for (let i = 1; i < sorted.length; i++) {
442
+ const last = out[out.length - 1];
443
+ if (sorted[i][0] <= last[1]) last[1] = Math.max(last[1], sorted[i][1]);
444
+ else out.push(sorted[i]);
445
+ }
446
+ return out;
447
+ }
448
+
449
+ // Every span of `text` the query matches, as `[start, end)` offsets, merged so
450
+ // overlapping hits paint once. This is what lets a snippet mark *all* the words
451
+ // instead of only the first — the old highlighter stopped at one, which made a
452
+ // multi-word query look like it had only matched half of itself.
453
+ export function highlightRanges(text, parsed) {
454
+ if (!text || !parsed) return [];
455
+ const ranges = [];
456
+ const add = (start, length) => {
457
+ if (length > 0 && start >= 0 && ranges.length < MAX_RANGES) ranges.push([start, start + length]);
458
+ };
459
+
460
+ if (parsed.regexGlobal) {
461
+ parsed.regexGlobal.lastIndex = 0;
462
+ let m;
463
+ let guard = 0;
464
+ while ((m = parsed.regexGlobal.exec(text)) !== null && guard++ < MAX_RANGES) {
465
+ add(m.index, m[0].length);
466
+ // A pattern that can match the empty string would spin forever here.
467
+ if (m[0].length === 0) parsed.regexGlobal.lastIndex++;
468
+ }
469
+ }
470
+
471
+ const needles = parsed.phrases.concat(parsed.terms);
472
+ if (needles.length) {
473
+ const hay = fold(text, parsed.caseSensitive);
474
+ for (const needle of needles) {
475
+ if (!needle) continue;
476
+ let from = 0;
477
+ let at;
478
+ while ((at = hay.indexOf(needle, from)) !== -1) {
479
+ add(at, needle.length);
480
+ from = at + needle.length;
481
+ if (ranges.length >= MAX_RANGES) break;
482
+ }
483
+ }
484
+ }
485
+
486
+ return mergeRanges(ranges);
487
+ }
488
+
489
+ // ---- turning an offset into a place to stand ------------------------------
490
+
491
+ // 1-based line number for an offset. Used for the "Line 42" label on a result
492
+ // and, more importantly, to put the cursor there when the result is opened.
493
+ export function lineOfIndex(text, index) {
494
+ if (!text || index <= 0) return 1;
495
+ let line = 1;
496
+ for (let i = 0; i < index && i < text.length; i++) {
497
+ if (text.charCodeAt(i) === 10) line++;
498
+ }
499
+ return line;
500
+ }
501
+
502
+ // The trimmed text of the line containing `index`, shortened around the match
503
+ // so the interesting part is not the part that gets cut off.
504
+ export function snippetAt(text, index, maxLength = 120) {
505
+ if (!text) return '';
506
+ const start = index < 0 ? 0 : index;
507
+ let lineStart = text.lastIndexOf('\n', start - 1) + 1;
508
+ let lineEnd = text.indexOf('\n', start);
509
+ if (lineEnd === -1) lineEnd = text.length;
510
+ let line = text.slice(lineStart, lineEnd).replace(/\r$/, '');
511
+
512
+ if (line.length > maxLength) {
513
+ const column = Math.max(0, start - lineStart);
514
+ const from = Math.max(0, column - Math.floor(maxLength / 3));
515
+ line = (from > 0 ? '…' : '') + line.slice(from, from + maxLength - 2);
516
+ }
517
+ return line.trim();
518
+ }