wavescope-mcp 1.0.2

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.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # WaveScope MCP
2
+
3
+ Wavelet-based multi-resolution context management for LLMs via MCP.
4
+
5
+ Provides a **zoomable view** of code files — the model can look at high-level
6
+ structure, then drill down to specific regions as needed, without loading
7
+ entire files into context.
8
+
9
+ ## How it works
10
+
11
+ 1. **Structural signal** — each line gets an importance score based on
12
+ indentation, keywords (`class`, `def`, `export`, etc.), and comment status.
13
+
14
+ 2. **Ricker wavelet transform** — the signal is convolved with Ricker (Mexican
15
+ hat) wavelets at 8 scales (1–128), detecting structural boundaries at
16
+ multiple resolutions.
17
+
18
+ 3. **Multi-scale peaks** — coefficient maxima are extracted and sorted by
19
+ magnitude, identifying the most important structural transitions.
20
+
21
+ 4. **Band assembly** — three zoom levels, sized as a fraction of the query
22
+ `radius` (default 300):
23
+ - **Fine** (scales 1–2): raw lines in ±`radius/5` (≥10 lines)
24
+ - **Medium** (scales 4–16): function/class signatures in ±`radius/2`
25
+ - **Coarse** (scales 32–128): section-level structural summary across ±`radius`
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pnpm install
31
+ pnpm build
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Running as MCP server
37
+
38
+ Add to your MCP client configuration:
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "wavescope": {
44
+ "command": "node",
45
+ "args": ["/path/to/wavescope-mcp/dist/index.js"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Tools
52
+
53
+ #### `query_wavelet_context`
54
+
55
+ Multi-resolution view around a position.
56
+
57
+ - `file` — absolute path to the file
58
+ - `center` — line number (0-indexed)
59
+ - `radius` — total range to consider, 10–2000 (default 300)
60
+
61
+ Returns `center`, `clamped` (and `clampedFrom` when out of range was
62
+ clamped), three `fine`/`medium`/`coarse` bands, and detected wavelet peaks.
63
+
64
+ #### `get_important_positions`
65
+
66
+ Find structural boundaries (class/function defs, imports, etc.).
67
+
68
+ - exactly one of `file` (single file path) or `directory` (project-wide search)
69
+ - `min_coefficient` — raw wavelet coefficient threshold, 0–10 (default 0.3).
70
+ Same semantics in both single-file and project mode.
71
+ - `limit` — max results, 1–100 (default 20)
72
+
73
+ #### `get_wavelet_coefficients`
74
+
75
+ Raw wavelet coefficients for custom analysis.
76
+
77
+ - `file`, `start`, `end`, `scale` (required, 1–128)
78
+
79
+ Returned object includes `scale` (the actual scale used) and
80
+ `requestedScale` (the original scale you asked for) — they differ when
81
+ the requested scale isn't in the index and the nearest available one is
82
+ substituted.
83
+
84
+ #### `get_summary_at_scale`
85
+
86
+ Compressed view of a region using specified scale peaks.
87
+
88
+ - `file`, `start`, `end`, `scale` (optional — auto-selected based on
89
+ region size if omitted: ≤50 lines → 2, ≤200 → 8, ≤800 → 32, >800 → 128)
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ pnpm install # Install dependencies
95
+ pnpm build # TypeScript compile
96
+ pnpm dev # Run with tsx
97
+ pnpm test # Watch mode
98
+ pnpm test:run # Run tests once
99
+ pnpm test:coverage # Run with coverage
100
+ pnpm typecheck # Type check only
101
+ ```
102
+
103
+ ## Architecture
104
+
105
+ ```
106
+ src/
107
+ ├── index.ts # MCP server entry, tool handlers
108
+ ├── signal.ts # Per-line structural importance signal
109
+ ├── wavelet.ts # Ricker CWT + peak detection
110
+ ├── context.ts # FileContext: query_wavelet_context, get_important_positions, etc.
111
+ ├── project.ts # ProjectIndex: multi-file discovery and indexing
112
+ ├── language.ts # Language-specific keyword weights
113
+ └── *.test.ts # Tests
114
+ ```
115
+
116
+ ## Supported languages
117
+
118
+ Python (`.py`, `.pyi`, `.pyx`), TypeScript (`.ts`, `.tsx`, `.mts`, `.cts`),
119
+ JavaScript (`.js`, `.jsx`, `.mjs`, `.cjs`), Go, Rust, Java, Ruby (incl.
120
+ `Rakefile`, `Gemfile`), PHP, Swift, Kotlin, Scala, Clojure (`.clj`, `.cljs`,
121
+ `.cljc`, `.edn`). Non-recognized extensions use a minimal generic
122
+ configuration.
123
+
124
+ ## Project-wide indexing limits
125
+
126
+ When `get_important_positions` is called with `directory`, the indexer:
127
+
128
+ - honors a root-level `.gitignore` (plain patterns; no negations);
129
+ - skips files larger than 2 MB and binary files (NUL byte sniff in first 4 KB);
130
+ - caps discovery at 5000 files (a `truncated` flag is set if exceeded);
131
+ - follows symlinks once via `realpath`, refusing any that escape the project root.
@@ -0,0 +1,97 @@
1
+ import { LanguageConfig } from "./language.js";
2
+ import { WaveletCoefficients } from "./wavelet.js";
3
+ export interface ImportantPosition {
4
+ position: number;
5
+ coefficient: number;
6
+ scale: number;
7
+ label: string;
8
+ filename?: string;
9
+ }
10
+ export interface BandResult {
11
+ range: [number, number];
12
+ content: string;
13
+ }
14
+ export interface WaveletContextResult {
15
+ center: number;
16
+ clamped: boolean;
17
+ /** Original `center` requested by caller, present only when clamped. */
18
+ clampedFrom?: number;
19
+ bands: {
20
+ fine: BandResult;
21
+ medium: BandResult;
22
+ coarse: BandResult;
23
+ };
24
+ waveletPeaks: ImportantPosition[];
25
+ }
26
+ export interface WaveletCoefficientsResult {
27
+ /** The actual scale used (may differ from `requestedScale`). */
28
+ scale: number;
29
+ /** The scale originally requested by the caller. */
30
+ requestedScale: number;
31
+ /** Raw coefficient slice. */
32
+ coefficients: number[];
33
+ }
34
+ /**
35
+ * FileContext holds the wavelet index for a single file and provides
36
+ * multi-resolution query methods.
37
+ */
38
+ export declare class FileContext {
39
+ readonly filename: string;
40
+ readonly lines: string[];
41
+ readonly language: LanguageConfig;
42
+ readonly signal: number[];
43
+ readonly coefficients: WaveletCoefficients;
44
+ get lineCount(): number;
45
+ private _allPeaks;
46
+ constructor(filename: string, content: string);
47
+ /**
48
+ * Returns all peaks (lazy, cached). Multi-scale peaks at the same
49
+ * position are preserved so that band assembly can filter by scale range.
50
+ */
51
+ private getAllPeaks;
52
+ /**
53
+ * Find important structural positions (class/function boundaries, etc.)
54
+ * sorted by wavelet coefficient magnitude.
55
+ */
56
+ getImportantPositions(minCoefficient?: number, limit?: number): ImportantPosition[];
57
+ /**
58
+ * Multi-resolution context centered at a position.
59
+ *
60
+ * Returns three bands:
61
+ * - fine: raw lines in a narrow window (~radius/5)
62
+ * - medium: peak-based summary in a medium window (~radius/2)
63
+ * - coarse: section-level overview across the full radius
64
+ */
65
+ queryWaveletContext(center: number, radius: number): WaveletContextResult;
66
+ /**
67
+ * Pick a representative scale for a region of the given size, matched
68
+ * to the wavelength most useful for summarizing structure at that
69
+ * resolution. Snaps to the closest available scale.
70
+ *
71
+ * Heuristic (region size in lines → target scale):
72
+ * ≤ 50 → 2
73
+ * ≤ 200 → 8
74
+ * ≤ 800 → 32
75
+ * > 800 → 128
76
+ */
77
+ autoScale(start: number, end: number): number;
78
+ /**
79
+ * Compressed/summarized view of a region using wavelet peaks at a given scale.
80
+ */
81
+ getSummaryAtScale(start: number, end: number, scale?: number): string;
82
+ getWaveletCoefficients(start: number, end: number, scale: number): WaveletCoefficientsResult;
83
+ private dedupPeaks;
84
+ /**
85
+ * Snap `scale` to the nearest scale present in the wavelet index.
86
+ * Ties (e.g. 3 → equidistant from 2 and 4) resolve to the lower scale
87
+ * (insertion-order stable on Array.reduce).
88
+ */
89
+ private findClosestScale;
90
+ private inferLabel;
91
+ private buildMediumBand;
92
+ private buildCoarseBand;
93
+ private buildRangeSummary;
94
+ private buildPeakSummary;
95
+ private buildSectionSummary;
96
+ }
97
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,EAGL,mBAAmB,EAEpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE;QACL,IAAI,EAAE,UAAU,CAAC;QACjB,MAAM,EAAE,UAAU,CAAC;QACnB,MAAM,EAAE,UAAU,CAAC;KACpB,CAAC;IACF,YAAY,EAAE,iBAAiB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,yBAAyB;IACxC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,6BAA6B;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AASD;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAE3C,IAAI,SAAS,IAAI,MAAM,CAA8B;IAGrD,OAAO,CAAC,SAAS,CAAuB;gBAE5B,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAqB7C;;;OAGG;IACH,OAAO,CAAC,WAAW;IAQnB;;;OAGG;IACH,qBAAqB,CACnB,cAAc,GAAE,MAAY,EAC5B,KAAK,GAAE,MAAW,GACjB,iBAAiB,EAAE;IAsBtB;;;;;;;OAOG;IACH,mBAAmB,CACjB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,oBAAoB;IAmEvB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAU7C;;OAEG;IACH,iBAAiB,CACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,GACb,MAAM;IA2BT,sBAAsB,CACpB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GACZ,yBAAyB;IAyB5B,OAAO,CAAC,UAAU;IAWlB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,UAAU;IA0GlB,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,eAAe;IA6BvB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,gBAAgB;IAyBxB,OAAO,CAAC,mBAAmB;CAiC5B"}
@@ -0,0 +1,429 @@
1
+ import { detectLanguage } from "./language.js";
2
+ import { computeSignal } from "./signal.js";
3
+ import { computeCWT, detectPeaks, } from "./wavelet.js";
4
+ /** Band scale ranges used by buildMediumBand / buildCoarseBand. */
5
+ const BAND_SCALES = {
6
+ fine: [1, 2],
7
+ medium: [4, 16],
8
+ coarse: [32, 128],
9
+ };
10
+ /**
11
+ * FileContext holds the wavelet index for a single file and provides
12
+ * multi-resolution query methods.
13
+ */
14
+ export class FileContext {
15
+ filename;
16
+ lines;
17
+ language;
18
+ signal;
19
+ coefficients;
20
+ get lineCount() { return this.lines.length; }
21
+ // Cached peak set — lazily computed once
22
+ _allPeaks = null;
23
+ constructor(filename, content) {
24
+ this.filename = filename;
25
+ this.lines = content.split("\n");
26
+ // Preserve trailing newline: if content ends with \n, ignore the empty last line
27
+ if (content.endsWith("\n") && this.lines[this.lines.length - 1] === "") {
28
+ this.lines.pop();
29
+ }
30
+ // Handle truly empty content
31
+ if (this.lines.length === 1 && this.lines[0] === "") {
32
+ this.lines = [];
33
+ }
34
+ this.language = detectLanguage(filename);
35
+ this.signal = computeSignal(this.lines, this.language);
36
+ this.coefficients = computeCWT(this.signal);
37
+ }
38
+ // ─── Cached peak access ──────────────────────────────────
39
+ /**
40
+ * Returns all peaks (lazy, cached). Multi-scale peaks at the same
41
+ * position are preserved so that band assembly can filter by scale range.
42
+ */
43
+ getAllPeaks() {
44
+ if (this._allPeaks)
45
+ return this._allPeaks;
46
+ this._allPeaks = detectPeaks(this.coefficients, 0.0, 1000);
47
+ return this._allPeaks;
48
+ }
49
+ // ─── Public API ──────────────────────────────────────────
50
+ /**
51
+ * Find important structural positions (class/function boundaries, etc.)
52
+ * sorted by wavelet coefficient magnitude.
53
+ */
54
+ getImportantPositions(minCoefficient = 0.3, limit = 20) {
55
+ const allPeaks = this.getAllPeaks();
56
+ // Deduplicate by position: keep the peak with the largest |coefficient|
57
+ const bestMap = new Map();
58
+ for (const p of allPeaks) {
59
+ if (Math.abs(p.coefficient) < minCoefficient)
60
+ continue;
61
+ const existing = bestMap.get(p.position);
62
+ if (!existing || Math.abs(p.coefficient) > Math.abs(existing.coefficient)) {
63
+ bestMap.set(p.position, p);
64
+ }
65
+ }
66
+ return [...bestMap.values()]
67
+ .sort((a, b) => Math.abs(b.coefficient) - Math.abs(a.coefficient))
68
+ .slice(0, limit)
69
+ .map((p) => ({
70
+ position: p.position,
71
+ coefficient: p.coefficient,
72
+ scale: p.scale,
73
+ label: this.inferLabel(p.position),
74
+ }));
75
+ }
76
+ /**
77
+ * Multi-resolution context centered at a position.
78
+ *
79
+ * Returns three bands:
80
+ * - fine: raw lines in a narrow window (~radius/5)
81
+ * - medium: peak-based summary in a medium window (~radius/2)
82
+ * - coarse: section-level overview across the full radius
83
+ */
84
+ queryWaveletContext(center, radius) {
85
+ if (this.lineCount === 0) {
86
+ const empty = {
87
+ center: 0,
88
+ clamped: center !== 0,
89
+ bands: {
90
+ fine: { range: [0, 0], content: "" },
91
+ medium: { range: [0, 0], content: "" },
92
+ coarse: { range: [0, 0], content: "" },
93
+ },
94
+ waveletPeaks: [],
95
+ };
96
+ if (empty.clamped)
97
+ empty.clampedFrom = center;
98
+ return empty;
99
+ }
100
+ const cl = Math.max(0, Math.min(center, this.lineCount - 1));
101
+ const clamped = center !== cl;
102
+ const total = this.lineCount;
103
+ // Fine band: ±radius/5, minimum 10 lines
104
+ const fineRadius = Math.max(10, Math.floor(radius / 5));
105
+ const fineStart = Math.max(0, cl - fineRadius);
106
+ const fineEnd = Math.min(total - 1, cl + fineRadius);
107
+ // Medium band: ±radius/2
108
+ const medRadius = Math.floor(radius / 2);
109
+ const medStart = Math.max(0, cl - medRadius);
110
+ const medEnd = Math.min(total - 1, cl + medRadius);
111
+ // Coarse band: full radius
112
+ const coarseStart = Math.max(0, cl - radius);
113
+ const coarseEnd = Math.min(total - 1, cl + radius);
114
+ // Get peaks in the full radius
115
+ const allPeaks = this.getAllPeaks();
116
+ const nearbyPeaks = allPeaks.filter((p) => p.position >= coarseStart && p.position <= coarseEnd);
117
+ const result = {
118
+ center: cl,
119
+ clamped,
120
+ bands: {
121
+ fine: {
122
+ range: [fineStart, fineEnd],
123
+ content: this.lines.slice(fineStart, fineEnd + 1).join("\n"),
124
+ },
125
+ medium: {
126
+ range: [medStart, medEnd],
127
+ content: this.buildMediumBand(medStart, medEnd, nearbyPeaks),
128
+ },
129
+ coarse: {
130
+ range: [coarseStart, coarseEnd],
131
+ content: this.buildCoarseBand(coarseStart, coarseEnd, nearbyPeaks),
132
+ },
133
+ },
134
+ waveletPeaks: this.dedupPeaks(nearbyPeaks).slice(0, 10).map((p) => ({
135
+ position: p.position,
136
+ coefficient: p.coefficient,
137
+ scale: p.scale,
138
+ label: this.inferLabel(p.position),
139
+ })),
140
+ };
141
+ if (clamped)
142
+ result.clampedFrom = center;
143
+ return result;
144
+ }
145
+ /**
146
+ * Pick a representative scale for a region of the given size, matched
147
+ * to the wavelength most useful for summarizing structure at that
148
+ * resolution. Snaps to the closest available scale.
149
+ *
150
+ * Heuristic (region size in lines → target scale):
151
+ * ≤ 50 → 2
152
+ * ≤ 200 → 8
153
+ * ≤ 800 → 32
154
+ * > 800 → 128
155
+ */
156
+ autoScale(start, end) {
157
+ const size = Math.max(1, end - start + 1);
158
+ let target;
159
+ if (size <= 50)
160
+ target = 2;
161
+ else if (size <= 200)
162
+ target = 8;
163
+ else if (size <= 800)
164
+ target = 32;
165
+ else
166
+ target = 128;
167
+ return this.findClosestScale(target);
168
+ }
169
+ /**
170
+ * Compressed/summarized view of a region using wavelet peaks at a given scale.
171
+ */
172
+ getSummaryAtScale(start, end, scale) {
173
+ if (this.lines.length === 0)
174
+ return "";
175
+ let s = Math.max(0, start);
176
+ let e = Math.min(this.lines.length - 1, end);
177
+ if (s > e)
178
+ [s, e] = [e, s];
179
+ const allPeaks = this.getAllPeaks();
180
+ // Auto-select a representative scale when caller doesn't pin one.
181
+ const resolvedScale = scale !== undefined
182
+ ? this.findClosestScale(scale)
183
+ : this.autoScale(s, e);
184
+ const peaksInRange = allPeaks.filter((p) => p.position >= s &&
185
+ p.position <= e &&
186
+ p.scale === resolvedScale);
187
+ if (peaksInRange.length === 0) {
188
+ return this.buildRangeSummary(s, e);
189
+ }
190
+ return this.buildPeakSummary(peaksInRange, s, e);
191
+ }
192
+ getWaveletCoefficients(start, end, scale) {
193
+ if (this.coefficients.coefficients.length === 0) {
194
+ return { scale, requestedScale: scale, coefficients: [] };
195
+ }
196
+ const resolvedScale = this.findClosestScale(scale);
197
+ const scaleIdx = this.coefficients.scales.indexOf(resolvedScale);
198
+ const coeffs = this.coefficients.coefficients[scaleIdx];
199
+ if (!coeffs) {
200
+ return { scale: resolvedScale, requestedScale: scale, coefficients: [] };
201
+ }
202
+ let s = Math.max(0, start);
203
+ let e = Math.min(coeffs.length - 1, end);
204
+ if (s > e)
205
+ [s, e] = [e, s];
206
+ return {
207
+ scale: resolvedScale,
208
+ requestedScale: scale,
209
+ coefficients: coeffs.slice(s, e + 1),
210
+ };
211
+ }
212
+ // ─── private helpers ──────────────────────────────────────
213
+ dedupPeaks(peaks) {
214
+ const bestMap = new Map();
215
+ for (const p of peaks) {
216
+ const existing = bestMap.get(p.position);
217
+ if (!existing || Math.abs(p.coefficient) > Math.abs(existing.coefficient)) {
218
+ bestMap.set(p.position, p);
219
+ }
220
+ }
221
+ return [...bestMap.values()];
222
+ }
223
+ /**
224
+ * Snap `scale` to the nearest scale present in the wavelet index.
225
+ * Ties (e.g. 3 → equidistant from 2 and 4) resolve to the lower scale
226
+ * (insertion-order stable on Array.reduce).
227
+ */
228
+ findClosestScale(scale) {
229
+ const scales = this.coefficients.scales;
230
+ if (scales.length === 0)
231
+ return scale;
232
+ return scales.reduce((prev, curr) => Math.abs(curr - scale) < Math.abs(prev - scale) ? curr : prev);
233
+ }
234
+ inferLabel(pos) {
235
+ if (pos < 0 || pos >= this.lines.length)
236
+ return "unknown";
237
+ const line = this.lines[pos].trim();
238
+ if (!line)
239
+ return `line ${pos}`;
240
+ // Tokenize on code delimiters (same regex as signal.ts) so that
241
+ // forms like "(defn foo" correctly produce token "defn"
242
+ const tokens = line.split(/[\s()[\]{},;:'".=!<>+\-*/&|^~%@#`]+/).filter(Boolean);
243
+ // Also keep whitespace-split tokens as fallback for label reading
244
+ const wsTokens = line.split(/\s+/);
245
+ if (this.language.name === "python") {
246
+ if (wsTokens[0] === "class")
247
+ return `class ${wsTokens[1]?.replace(":", "")}`;
248
+ if (wsTokens[0] === "def")
249
+ return `def ${wsTokens[1]?.split("(")[0]}`;
250
+ if (wsTokens[0] === "import")
251
+ return `import ${wsTokens.slice(1).join(" ")}`;
252
+ if (wsTokens[0] === "from")
253
+ return `from ${wsTokens.slice(1).join(" ")}`;
254
+ if (line.startsWith("@"))
255
+ return `decorator ${line.split(/[\s()]+/)[0].slice(1)}`;
256
+ if (line.startsWith("if __name__"))
257
+ return "main guard";
258
+ }
259
+ else {
260
+ // Decorator detection (before keyword checks so "export @foo class" works)
261
+ if (line.startsWith("@")) {
262
+ const decorator = line.split(/[\s()]+/)[0];
263
+ return `decorator ${decorator.slice(1)}`;
264
+ }
265
+ // Import/export at top (before keyword checks to capture "export class Foo")
266
+ if (wsTokens[0] === "import")
267
+ return `import ${wsTokens.slice(1).join(" ")}`;
268
+ if (wsTokens[0] === "export") {
269
+ const rest = wsTokens.slice(1).join(" ");
270
+ return `export ${rest.substring(0, 40)}`;
271
+ }
272
+ // Use code-delimiter tokens for keyword matching
273
+ if (tokens.includes("class")) {
274
+ const idx = tokens.indexOf("class");
275
+ return `class ${tokens[idx + 1]?.replace(/\{.*/, "").replace(/extends|implements/g, "").trim()}`;
276
+ }
277
+ if (tokens.includes("interface")) {
278
+ const idx = tokens.indexOf("interface");
279
+ return `interface ${tokens[idx + 1]?.replace(/\{.*/, "").trim()}`;
280
+ }
281
+ if (tokens.includes("enum")) {
282
+ const idx = tokens.indexOf("enum");
283
+ return `enum ${tokens[idx + 1]?.replace(/\{.*/, "").trim()}`;
284
+ }
285
+ if (tokens.includes("trait")) {
286
+ const idx = tokens.indexOf("trait");
287
+ return `trait ${tokens[idx + 1]?.replace(/\{.*/, "").trim()}`;
288
+ }
289
+ if (tokens.includes("struct")) {
290
+ const idx = tokens.indexOf("struct");
291
+ return `struct ${tokens[idx + 1]?.replace(/\{.*/, "").trim()}`;
292
+ }
293
+ if (tokens.includes("object")) {
294
+ const idx = tokens.indexOf("object");
295
+ return `object ${tokens[idx + 1]?.replace(/\{.*/, "").trim()}`;
296
+ }
297
+ if (tokens.includes("function")) {
298
+ const idx = tokens.indexOf("function");
299
+ return `function ${tokens[idx + 1]?.split("(")[0]}`;
300
+ }
301
+ if (tokens.includes("fn") && !tokens.includes("defn")) {
302
+ // Rust — check defn first to avoid false match on Clojure
303
+ return `fn ${tokens[tokens.indexOf("fn") + 1]?.split("(")[0]}`;
304
+ }
305
+ if (tokens.includes("fun")) {
306
+ // Kotlin
307
+ return `fun ${tokens[tokens.indexOf("fun") + 1]?.split("(")[0]}`;
308
+ }
309
+ if (tokens.includes("func")) {
310
+ // Go
311
+ return `func ${tokens[tokens.indexOf("func") + 1]?.split("(")[0]}`;
312
+ }
313
+ if (tokens.includes("def")) {
314
+ // Scala
315
+ return `def ${tokens[tokens.indexOf("def") + 1]?.split("(")[0]}`;
316
+ }
317
+ if (tokens.includes("defn")) {
318
+ // Clojure
319
+ return `defn ${tokens[tokens.indexOf("defn") + 1]}`;
320
+ }
321
+ if (tokens.includes("defmacro")) {
322
+ return `defmacro ${tokens[tokens.indexOf("defmacro") + 1]}`;
323
+ }
324
+ if (tokens.includes("defprotocol")) {
325
+ return `defprotocol ${tokens[tokens.indexOf("defprotocol") + 1]}`;
326
+ }
327
+ if (tokens.includes("defrecord")) {
328
+ return `defrecord ${tokens[tokens.indexOf("defrecord") + 1]}`;
329
+ }
330
+ if (tokens.includes("deftype")) {
331
+ return `deftype ${tokens[tokens.indexOf("deftype") + 1]}`;
332
+ }
333
+ if (tokens.includes("impl")) {
334
+ return `impl ${tokens[tokens.indexOf("impl") + 1]?.split("(")[0]}`;
335
+ }
336
+ if (tokens.includes("protocol")) {
337
+ return `protocol ${tokens[tokens.indexOf("protocol") + 1]}`;
338
+ }
339
+ if (tokens.includes("extension")) {
340
+ return `extension ${tokens[tokens.indexOf("extension") + 1]}`;
341
+ }
342
+ }
343
+ return line.substring(0, 50);
344
+ }
345
+ buildMediumBand(start, end, peaks) {
346
+ const medPeaks = peaks
347
+ .filter((p) => p.position >= start &&
348
+ p.position <= end &&
349
+ p.scale >= BAND_SCALES.medium[0] &&
350
+ p.scale <= BAND_SCALES.medium[1])
351
+ .sort((a, b) => a.position - b.position);
352
+ if (medPeaks.length === 0) {
353
+ return this.buildRangeSummary(start, end);
354
+ }
355
+ return this.buildPeakSummary(medPeaks, start, end);
356
+ }
357
+ buildCoarseBand(start, end, peaks) {
358
+ const coarsePeaks = peaks
359
+ .filter((p) => p.position >= start &&
360
+ p.position <= end &&
361
+ p.scale >= BAND_SCALES.coarse[0] &&
362
+ p.scale <= BAND_SCALES.coarse[1])
363
+ .sort((a, b) => a.position - b.position);
364
+ if (coarsePeaks.length === 0) {
365
+ const allInRange = peaks
366
+ .filter((p) => p.position >= start && p.position <= end)
367
+ .sort((a, b) => a.position - b.position);
368
+ if (allInRange.length === 0) {
369
+ return this.buildRangeSummary(start, end);
370
+ }
371
+ return this.buildSectionSummary(allInRange, start, end);
372
+ }
373
+ return this.buildSectionSummary(coarsePeaks, start, end);
374
+ }
375
+ buildRangeSummary(start, end) {
376
+ if (start > end)
377
+ return "";
378
+ const previewLines = Math.min(5, end - start + 1);
379
+ const parts = [];
380
+ const step = Math.ceil((end - start + 1) / previewLines);
381
+ for (let i = start; i <= end; i += step) {
382
+ const line = this.lines[i].trim();
383
+ if (line) {
384
+ parts.push(`[${i}] ${line.substring(0, 80)}`);
385
+ }
386
+ }
387
+ return parts.join("\n");
388
+ }
389
+ buildPeakSummary(peaks, rangeStart, rangeEnd) {
390
+ const parts = [];
391
+ let prevEnd = rangeStart - 1;
392
+ for (const peak of peaks) {
393
+ if (peak.position > prevEnd + 1) {
394
+ parts.push(`[${prevEnd + 1}-${peak.position - 1}] ...`);
395
+ }
396
+ parts.push(`[${peak.position}] ${this.lines[peak.position].trim().substring(0, 80)}`);
397
+ prevEnd = peak.position;
398
+ }
399
+ if (prevEnd < rangeEnd) {
400
+ parts.push(`[${prevEnd + 1}-${rangeEnd}] ...`);
401
+ }
402
+ return parts.join("\n");
403
+ }
404
+ buildSectionSummary(peaks, rangeStart, rangeEnd) {
405
+ const parts = [];
406
+ let prevPos = rangeStart;
407
+ let currentSection = "";
408
+ // Set the first section label BEFORE the loop so the initial
409
+ // region [rangeStart, firstPeak-1] gets a label
410
+ if (peaks.length > 0) {
411
+ currentSection = this.inferLabel(peaks[0].position);
412
+ }
413
+ for (const peak of peaks) {
414
+ if (currentSection && prevPos < peak.position) {
415
+ parts.push(`[${prevPos}-${peak.position - 1}] ${currentSection}`);
416
+ }
417
+ currentSection = this.inferLabel(peak.position);
418
+ prevPos = peak.position;
419
+ }
420
+ if (currentSection && prevPos <= rangeEnd) {
421
+ parts.push(`[${prevPos}-${rangeEnd}] ${currentSection}`);
422
+ }
423
+ if (parts.length === 0) {
424
+ parts.push(`[${rangeStart}-${rangeEnd}] (code region)`);
425
+ }
426
+ return parts.join("\n");
427
+ }
428
+ }
429
+ //# sourceMappingURL=context.js.map