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 +131 -0
- package/dist/context.d.ts +97 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +429 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +216 -0
- package/dist/index.js.map +1 -0
- package/dist/language.d.ts +41 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +398 -0
- package/dist/language.js.map +1 -0
- package/dist/project.d.ts +34 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +343 -0
- package/dist/project.js.map +1 -0
- package/dist/signal.d.ts +9 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +267 -0
- package/dist/signal.js.map +1 -0
- package/dist/wavelet.d.ts +46 -0
- package/dist/wavelet.d.ts.map +1 -0
- package/dist/wavelet.js +141 -0
- package/dist/wavelet.js.map +1 -0
- package/package.json +43 -0
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"}
|
package/dist/context.js
ADDED
|
@@ -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
|