@mrclrchtr/supi-code-intelligence 4.8.0 → 4.9.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.
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +3 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +107 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +107 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +3 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +107 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/package.json +5 -5
- package/src/analysis/search/ast-scan-timing.ts +59 -0
- package/src/analysis/search/pattern-analysis.ts +230 -0
- package/src/analysis/search/pattern.ts +22 -224
|
@@ -48,7 +48,7 @@ Config file locations:
|
|
|
48
48
|
### Shared registries
|
|
49
49
|
|
|
50
50
|
- context-provider registry for `/supi-context`
|
|
51
|
-
- debug-event registry for producers that want shared debug capture
|
|
51
|
+
- debug-event registry and monotonic phase timers for producers that want shared debug capture
|
|
52
52
|
- settings registry used by `/supi-settings`
|
|
53
53
|
|
|
54
54
|
### Project and session helpers
|
|
@@ -108,5 +108,7 @@ export default function myExtension(pi: ExtensionAPI) {
|
|
|
108
108
|
|
|
109
109
|
- `src/api.ts` — exported library surface
|
|
110
110
|
- `src/config.ts` — shared config loading and writing
|
|
111
|
+
- `src/debug-registry.ts` — Debug domain surface, event state, retention, redaction, listeners, and queries
|
|
112
|
+
- `src/debug-timing.ts` — monotonic total and phase timers for Debug Event Producers
|
|
111
113
|
- `src/settings/` — settings registry, schema, scope resolution, and persistence
|
|
112
114
|
- `src/report.ts` — shared text/report rendering helpers
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// supi-debug extension owns policy/configuration and exposes events through a
|
|
5
5
|
// command/tool while this module stays dependency-free for producers.
|
|
6
6
|
|
|
7
|
+
// biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
|
|
8
|
+
export * from "./debug-timing.ts";
|
|
9
|
+
|
|
7
10
|
export type DebugLevel = "debug" | "info" | "warning" | "error";
|
|
8
11
|
export type DebugAgentAccess = "off" | "sanitized" | "raw";
|
|
9
12
|
export interface DebugRegistryConfig {
|
|
@@ -180,6 +183,11 @@ export function getDebugRegistryConfig(): DebugRegistryConfig {
|
|
|
180
183
|
return cloneConfig(getState().config);
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
/** Return whether the Debug Registry currently retains producer events. */
|
|
187
|
+
export function isDebugRegistryEnabled(): boolean {
|
|
188
|
+
return getState().config.enabled;
|
|
189
|
+
}
|
|
190
|
+
|
|
183
191
|
/** Best-effort redaction helper for data exposed through sanitized debug views. */
|
|
184
192
|
export function redactDebugData<T>(value: T): T {
|
|
185
193
|
return redactValue(value, 8) as T;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import {
|
|
3
|
+
type DebugEvent,
|
|
4
|
+
type DebugEventInput,
|
|
5
|
+
isDebugRegistryEnabled,
|
|
6
|
+
recordDebugEvent,
|
|
7
|
+
} from "./debug-registry.ts";
|
|
8
|
+
|
|
9
|
+
/** Monotonic duration data added to a timed debug event. */
|
|
10
|
+
export interface DebugTiming {
|
|
11
|
+
readonly durationMs: number;
|
|
12
|
+
readonly phasesMs: Readonly<Record<string, number>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Debug event input whose data can receive the reserved `timing` field. */
|
|
16
|
+
export interface TimedDebugEventInput extends Omit<DebugEventInput, "data"> {
|
|
17
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Test seam for the monotonic clock used by a debug timer. */
|
|
21
|
+
export interface DebugTimerOptions {
|
|
22
|
+
readonly now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Lazy timed-event input that is not evaluated when Debug is disabled. */
|
|
26
|
+
export type TimedDebugEventFactory = () => TimedDebugEventInput;
|
|
27
|
+
|
|
28
|
+
/** One-shot debug timer with optional sequential phase measurements. */
|
|
29
|
+
export interface DebugTimer {
|
|
30
|
+
/** Whether this timer sampled Debug as enabled when it started. */
|
|
31
|
+
readonly enabled: boolean;
|
|
32
|
+
/** Finish the current phase and start the next unnamed interval. */
|
|
33
|
+
mark(phase: string): void;
|
|
34
|
+
/** Record one event. A second call returns `null` without recording another event. */
|
|
35
|
+
finish(
|
|
36
|
+
input: TimedDebugEventInput | TimedDebugEventFactory,
|
|
37
|
+
finalPhase?: string,
|
|
38
|
+
): DebugEvent | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Start a monotonic timer for one debug event.
|
|
43
|
+
*
|
|
44
|
+
* Each `mark(name)` stores the interval since the previous mark. `finish()`
|
|
45
|
+
* stores the total duration and can name the final interval. Repeated phase
|
|
46
|
+
* names are accumulated. Event data reserves the `timing` field. When Debug is
|
|
47
|
+
* disabled at start, this returns a no-op timer and does not read the clock.
|
|
48
|
+
* Pass a factory to `finish()` to avoid event-data construction when disabled.
|
|
49
|
+
*/
|
|
50
|
+
export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
51
|
+
if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
|
|
52
|
+
const now = options.now ?? performance.now.bind(performance);
|
|
53
|
+
const startedAt = now();
|
|
54
|
+
let previousAt = startedAt;
|
|
55
|
+
let finished = false;
|
|
56
|
+
const phases = new Map<string, number>();
|
|
57
|
+
|
|
58
|
+
const markAt = (phase: string, current: number): void => {
|
|
59
|
+
const name = phase.trim();
|
|
60
|
+
if (!name) return;
|
|
61
|
+
phases.set(name, (phases.get(name) ?? 0) + Math.max(0, current - previousAt));
|
|
62
|
+
previousAt = current;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
enabled: true,
|
|
67
|
+
mark(phase) {
|
|
68
|
+
if (finished) return;
|
|
69
|
+
markAt(phase, now());
|
|
70
|
+
},
|
|
71
|
+
finish(input, finalPhase) {
|
|
72
|
+
if (finished) return null;
|
|
73
|
+
if (!isDebugRegistryEnabled()) {
|
|
74
|
+
finished = true;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const completedAt = now();
|
|
78
|
+
if (finalPhase) markAt(finalPhase, completedAt);
|
|
79
|
+
finished = true;
|
|
80
|
+
const phasesMs = Object.fromEntries(
|
|
81
|
+
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
82
|
+
);
|
|
83
|
+
const timing: DebugTiming = {
|
|
84
|
+
durationMs: duration(completedAt - startedAt),
|
|
85
|
+
phasesMs,
|
|
86
|
+
};
|
|
87
|
+
const eventInput = typeof input === "function" ? input() : input;
|
|
88
|
+
return recordDebugEvent({
|
|
89
|
+
...eventInput,
|
|
90
|
+
data: { ...eventInput.data, timing },
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DISABLED_DEBUG_TIMER: DebugTimer = Object.freeze({
|
|
97
|
+
enabled: false,
|
|
98
|
+
mark() {},
|
|
99
|
+
finish() {
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function duration(value: number): number {
|
|
105
|
+
if (!Number.isFinite(value)) return 0;
|
|
106
|
+
return Math.round(Math.max(0, value) * 10) / 10;
|
|
107
|
+
}
|
|
@@ -48,7 +48,7 @@ Config file locations:
|
|
|
48
48
|
### Shared registries
|
|
49
49
|
|
|
50
50
|
- context-provider registry for `/supi-context`
|
|
51
|
-
- debug-event registry for producers that want shared debug capture
|
|
51
|
+
- debug-event registry and monotonic phase timers for producers that want shared debug capture
|
|
52
52
|
- settings registry used by `/supi-settings`
|
|
53
53
|
|
|
54
54
|
### Project and session helpers
|
|
@@ -108,5 +108,7 @@ export default function myExtension(pi: ExtensionAPI) {
|
|
|
108
108
|
|
|
109
109
|
- `src/api.ts` — exported library surface
|
|
110
110
|
- `src/config.ts` — shared config loading and writing
|
|
111
|
+
- `src/debug-registry.ts` — Debug domain surface, event state, retention, redaction, listeners, and queries
|
|
112
|
+
- `src/debug-timing.ts` — monotonic total and phase timers for Debug Event Producers
|
|
111
113
|
- `src/settings/` — settings registry, schema, scope resolution, and persistence
|
|
112
114
|
- `src/report.ts` — shared text/report rendering helpers
|
package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// supi-debug extension owns policy/configuration and exposes events through a
|
|
5
5
|
// command/tool while this module stays dependency-free for producers.
|
|
6
6
|
|
|
7
|
+
// biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
|
|
8
|
+
export * from "./debug-timing.ts";
|
|
9
|
+
|
|
7
10
|
export type DebugLevel = "debug" | "info" | "warning" | "error";
|
|
8
11
|
export type DebugAgentAccess = "off" | "sanitized" | "raw";
|
|
9
12
|
export interface DebugRegistryConfig {
|
|
@@ -180,6 +183,11 @@ export function getDebugRegistryConfig(): DebugRegistryConfig {
|
|
|
180
183
|
return cloneConfig(getState().config);
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
/** Return whether the Debug Registry currently retains producer events. */
|
|
187
|
+
export function isDebugRegistryEnabled(): boolean {
|
|
188
|
+
return getState().config.enabled;
|
|
189
|
+
}
|
|
190
|
+
|
|
183
191
|
/** Best-effort redaction helper for data exposed through sanitized debug views. */
|
|
184
192
|
export function redactDebugData<T>(value: T): T {
|
|
185
193
|
return redactValue(value, 8) as T;
|
package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import {
|
|
3
|
+
type DebugEvent,
|
|
4
|
+
type DebugEventInput,
|
|
5
|
+
isDebugRegistryEnabled,
|
|
6
|
+
recordDebugEvent,
|
|
7
|
+
} from "./debug-registry.ts";
|
|
8
|
+
|
|
9
|
+
/** Monotonic duration data added to a timed debug event. */
|
|
10
|
+
export interface DebugTiming {
|
|
11
|
+
readonly durationMs: number;
|
|
12
|
+
readonly phasesMs: Readonly<Record<string, number>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Debug event input whose data can receive the reserved `timing` field. */
|
|
16
|
+
export interface TimedDebugEventInput extends Omit<DebugEventInput, "data"> {
|
|
17
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Test seam for the monotonic clock used by a debug timer. */
|
|
21
|
+
export interface DebugTimerOptions {
|
|
22
|
+
readonly now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Lazy timed-event input that is not evaluated when Debug is disabled. */
|
|
26
|
+
export type TimedDebugEventFactory = () => TimedDebugEventInput;
|
|
27
|
+
|
|
28
|
+
/** One-shot debug timer with optional sequential phase measurements. */
|
|
29
|
+
export interface DebugTimer {
|
|
30
|
+
/** Whether this timer sampled Debug as enabled when it started. */
|
|
31
|
+
readonly enabled: boolean;
|
|
32
|
+
/** Finish the current phase and start the next unnamed interval. */
|
|
33
|
+
mark(phase: string): void;
|
|
34
|
+
/** Record one event. A second call returns `null` without recording another event. */
|
|
35
|
+
finish(
|
|
36
|
+
input: TimedDebugEventInput | TimedDebugEventFactory,
|
|
37
|
+
finalPhase?: string,
|
|
38
|
+
): DebugEvent | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Start a monotonic timer for one debug event.
|
|
43
|
+
*
|
|
44
|
+
* Each `mark(name)` stores the interval since the previous mark. `finish()`
|
|
45
|
+
* stores the total duration and can name the final interval. Repeated phase
|
|
46
|
+
* names are accumulated. Event data reserves the `timing` field. When Debug is
|
|
47
|
+
* disabled at start, this returns a no-op timer and does not read the clock.
|
|
48
|
+
* Pass a factory to `finish()` to avoid event-data construction when disabled.
|
|
49
|
+
*/
|
|
50
|
+
export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
51
|
+
if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
|
|
52
|
+
const now = options.now ?? performance.now.bind(performance);
|
|
53
|
+
const startedAt = now();
|
|
54
|
+
let previousAt = startedAt;
|
|
55
|
+
let finished = false;
|
|
56
|
+
const phases = new Map<string, number>();
|
|
57
|
+
|
|
58
|
+
const markAt = (phase: string, current: number): void => {
|
|
59
|
+
const name = phase.trim();
|
|
60
|
+
if (!name) return;
|
|
61
|
+
phases.set(name, (phases.get(name) ?? 0) + Math.max(0, current - previousAt));
|
|
62
|
+
previousAt = current;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
enabled: true,
|
|
67
|
+
mark(phase) {
|
|
68
|
+
if (finished) return;
|
|
69
|
+
markAt(phase, now());
|
|
70
|
+
},
|
|
71
|
+
finish(input, finalPhase) {
|
|
72
|
+
if (finished) return null;
|
|
73
|
+
if (!isDebugRegistryEnabled()) {
|
|
74
|
+
finished = true;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const completedAt = now();
|
|
78
|
+
if (finalPhase) markAt(finalPhase, completedAt);
|
|
79
|
+
finished = true;
|
|
80
|
+
const phasesMs = Object.fromEntries(
|
|
81
|
+
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
82
|
+
);
|
|
83
|
+
const timing: DebugTiming = {
|
|
84
|
+
durationMs: duration(completedAt - startedAt),
|
|
85
|
+
phasesMs,
|
|
86
|
+
};
|
|
87
|
+
const eventInput = typeof input === "function" ? input() : input;
|
|
88
|
+
return recordDebugEvent({
|
|
89
|
+
...eventInput,
|
|
90
|
+
data: { ...eventInput.data, timing },
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DISABLED_DEBUG_TIMER: DebugTimer = Object.freeze({
|
|
97
|
+
enabled: false,
|
|
98
|
+
mark() {},
|
|
99
|
+
finish() {
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function duration(value: number): number {
|
|
105
|
+
if (!Number.isFinite(value)) return 0;
|
|
106
|
+
return Math.round(Math.max(0, value) * 10) / 10;
|
|
107
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-lsp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Language Server Protocol runtime for SuPi code intelligence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"vscode-jsonrpc": "^9.0.0",
|
|
38
38
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
39
39
|
"vscode-languageserver-types": "^3.17.5",
|
|
40
|
-
"@mrclrchtr/supi-core": "4.
|
|
41
|
-
"@mrclrchtr/supi-code-runtime": "4.
|
|
40
|
+
"@mrclrchtr/supi-core": "4.9.0",
|
|
41
|
+
"@mrclrchtr/supi-code-runtime": "4.9.0"
|
|
42
42
|
},
|
|
43
43
|
"bundledDependencies": [
|
|
44
44
|
"@mrclrchtr/supi-code-runtime",
|
package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Config file locations:
|
|
|
48
48
|
### Shared registries
|
|
49
49
|
|
|
50
50
|
- context-provider registry for `/supi-context`
|
|
51
|
-
- debug-event registry for producers that want shared debug capture
|
|
51
|
+
- debug-event registry and monotonic phase timers for producers that want shared debug capture
|
|
52
52
|
- settings registry used by `/supi-settings`
|
|
53
53
|
|
|
54
54
|
### Project and session helpers
|
|
@@ -108,5 +108,7 @@ export default function myExtension(pi: ExtensionAPI) {
|
|
|
108
108
|
|
|
109
109
|
- `src/api.ts` — exported library surface
|
|
110
110
|
- `src/config.ts` — shared config loading and writing
|
|
111
|
+
- `src/debug-registry.ts` — Debug domain surface, event state, retention, redaction, listeners, and queries
|
|
112
|
+
- `src/debug-timing.ts` — monotonic total and phase timers for Debug Event Producers
|
|
111
113
|
- `src/settings/` — settings registry, schema, scope resolution, and persistence
|
|
112
114
|
- `src/report.ts` — shared text/report rendering helpers
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// supi-debug extension owns policy/configuration and exposes events through a
|
|
5
5
|
// command/tool while this module stays dependency-free for producers.
|
|
6
6
|
|
|
7
|
+
// biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
|
|
8
|
+
export * from "./debug-timing.ts";
|
|
9
|
+
|
|
7
10
|
export type DebugLevel = "debug" | "info" | "warning" | "error";
|
|
8
11
|
export type DebugAgentAccess = "off" | "sanitized" | "raw";
|
|
9
12
|
export interface DebugRegistryConfig {
|
|
@@ -180,6 +183,11 @@ export function getDebugRegistryConfig(): DebugRegistryConfig {
|
|
|
180
183
|
return cloneConfig(getState().config);
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
/** Return whether the Debug Registry currently retains producer events. */
|
|
187
|
+
export function isDebugRegistryEnabled(): boolean {
|
|
188
|
+
return getState().config.enabled;
|
|
189
|
+
}
|
|
190
|
+
|
|
183
191
|
/** Best-effort redaction helper for data exposed through sanitized debug views. */
|
|
184
192
|
export function redactDebugData<T>(value: T): T {
|
|
185
193
|
return redactValue(value, 8) as T;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import {
|
|
3
|
+
type DebugEvent,
|
|
4
|
+
type DebugEventInput,
|
|
5
|
+
isDebugRegistryEnabled,
|
|
6
|
+
recordDebugEvent,
|
|
7
|
+
} from "./debug-registry.ts";
|
|
8
|
+
|
|
9
|
+
/** Monotonic duration data added to a timed debug event. */
|
|
10
|
+
export interface DebugTiming {
|
|
11
|
+
readonly durationMs: number;
|
|
12
|
+
readonly phasesMs: Readonly<Record<string, number>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Debug event input whose data can receive the reserved `timing` field. */
|
|
16
|
+
export interface TimedDebugEventInput extends Omit<DebugEventInput, "data"> {
|
|
17
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Test seam for the monotonic clock used by a debug timer. */
|
|
21
|
+
export interface DebugTimerOptions {
|
|
22
|
+
readonly now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Lazy timed-event input that is not evaluated when Debug is disabled. */
|
|
26
|
+
export type TimedDebugEventFactory = () => TimedDebugEventInput;
|
|
27
|
+
|
|
28
|
+
/** One-shot debug timer with optional sequential phase measurements. */
|
|
29
|
+
export interface DebugTimer {
|
|
30
|
+
/** Whether this timer sampled Debug as enabled when it started. */
|
|
31
|
+
readonly enabled: boolean;
|
|
32
|
+
/** Finish the current phase and start the next unnamed interval. */
|
|
33
|
+
mark(phase: string): void;
|
|
34
|
+
/** Record one event. A second call returns `null` without recording another event. */
|
|
35
|
+
finish(
|
|
36
|
+
input: TimedDebugEventInput | TimedDebugEventFactory,
|
|
37
|
+
finalPhase?: string,
|
|
38
|
+
): DebugEvent | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Start a monotonic timer for one debug event.
|
|
43
|
+
*
|
|
44
|
+
* Each `mark(name)` stores the interval since the previous mark. `finish()`
|
|
45
|
+
* stores the total duration and can name the final interval. Repeated phase
|
|
46
|
+
* names are accumulated. Event data reserves the `timing` field. When Debug is
|
|
47
|
+
* disabled at start, this returns a no-op timer and does not read the clock.
|
|
48
|
+
* Pass a factory to `finish()` to avoid event-data construction when disabled.
|
|
49
|
+
*/
|
|
50
|
+
export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
51
|
+
if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
|
|
52
|
+
const now = options.now ?? performance.now.bind(performance);
|
|
53
|
+
const startedAt = now();
|
|
54
|
+
let previousAt = startedAt;
|
|
55
|
+
let finished = false;
|
|
56
|
+
const phases = new Map<string, number>();
|
|
57
|
+
|
|
58
|
+
const markAt = (phase: string, current: number): void => {
|
|
59
|
+
const name = phase.trim();
|
|
60
|
+
if (!name) return;
|
|
61
|
+
phases.set(name, (phases.get(name) ?? 0) + Math.max(0, current - previousAt));
|
|
62
|
+
previousAt = current;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
enabled: true,
|
|
67
|
+
mark(phase) {
|
|
68
|
+
if (finished) return;
|
|
69
|
+
markAt(phase, now());
|
|
70
|
+
},
|
|
71
|
+
finish(input, finalPhase) {
|
|
72
|
+
if (finished) return null;
|
|
73
|
+
if (!isDebugRegistryEnabled()) {
|
|
74
|
+
finished = true;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const completedAt = now();
|
|
78
|
+
if (finalPhase) markAt(finalPhase, completedAt);
|
|
79
|
+
finished = true;
|
|
80
|
+
const phasesMs = Object.fromEntries(
|
|
81
|
+
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
82
|
+
);
|
|
83
|
+
const timing: DebugTiming = {
|
|
84
|
+
durationMs: duration(completedAt - startedAt),
|
|
85
|
+
phasesMs,
|
|
86
|
+
};
|
|
87
|
+
const eventInput = typeof input === "function" ? input() : input;
|
|
88
|
+
return recordDebugEvent({
|
|
89
|
+
...eventInput,
|
|
90
|
+
data: { ...eventInput.data, timing },
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DISABLED_DEBUG_TIMER: DebugTimer = Object.freeze({
|
|
97
|
+
enabled: false,
|
|
98
|
+
mark() {},
|
|
99
|
+
finish() {
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function duration(value: number): number {
|
|
105
|
+
if (!Number.isFinite(value)) return 0;
|
|
106
|
+
return Math.round(Math.max(0, value) * 10) / 10;
|
|
107
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-tree-sitter",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Structural AST analysis for SuPi code intelligence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"web-tree-sitter": "^0.26.8",
|
|
37
|
-
"@mrclrchtr/supi-
|
|
38
|
-
"@mrclrchtr/supi-
|
|
37
|
+
"@mrclrchtr/supi-code-runtime": "4.9.0",
|
|
38
|
+
"@mrclrchtr/supi-core": "4.9.0"
|
|
39
39
|
},
|
|
40
40
|
"bundledDependencies": [
|
|
41
41
|
"@mrclrchtr/supi-code-runtime",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-code-intelligence",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "LSP and AST navigation, search, diagnostics, and refactoring",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"yaml": "^2.9.0",
|
|
38
|
-
"@mrclrchtr/supi-code-runtime": "4.
|
|
39
|
-
"@mrclrchtr/supi-core": "4.
|
|
40
|
-
"@mrclrchtr/supi-
|
|
41
|
-
"@mrclrchtr/supi-
|
|
38
|
+
"@mrclrchtr/supi-code-runtime": "4.9.0",
|
|
39
|
+
"@mrclrchtr/supi-core": "4.9.0",
|
|
40
|
+
"@mrclrchtr/supi-tree-sitter": "4.9.0",
|
|
41
|
+
"@mrclrchtr/supi-lsp": "4.9.0"
|
|
42
42
|
},
|
|
43
43
|
"bundledDependencies": [
|
|
44
44
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
|
|
2
|
+
import type { StructuralSearchOperation } from "@mrclrchtr/supi-tree-sitter/api";
|
|
3
|
+
import type { StructuredFileAnalysis, StructuredPatternParams } from "./pattern-analysis.ts";
|
|
4
|
+
|
|
5
|
+
interface ScanContext {
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly params: StructuredPatternParams;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface EnumerationMetrics {
|
|
11
|
+
readonly eligibleFileCount: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AstScanTimingInput {
|
|
15
|
+
readonly context: ScanContext;
|
|
16
|
+
readonly operation: StructuralSearchOperation;
|
|
17
|
+
readonly roots: readonly string[];
|
|
18
|
+
readonly enumeration: EnumerationMetrics;
|
|
19
|
+
readonly analysis: StructuredFileAnalysis;
|
|
20
|
+
readonly complete: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One-shot timer that records aggregate AST scan phases. */
|
|
24
|
+
export interface AstScanTimer {
|
|
25
|
+
enumerationCompleted(): void;
|
|
26
|
+
record(input: AstScanTimingInput): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Start one AST scan timer without adding paths to debug-event data. */
|
|
30
|
+
export function startAstScanTimer(): AstScanTimer {
|
|
31
|
+
const timer = startDebugTimer();
|
|
32
|
+
return {
|
|
33
|
+
enumerationCompleted() {
|
|
34
|
+
timer.mark("enumeration");
|
|
35
|
+
},
|
|
36
|
+
record(input) {
|
|
37
|
+
timer.finish(
|
|
38
|
+
() => ({
|
|
39
|
+
source: "code-intelligence",
|
|
40
|
+
level: "debug",
|
|
41
|
+
category: "ast-scan.timing",
|
|
42
|
+
message: `AST ${input.context.params.kind} scan analyzed ${input.analysis.analyzedFileCount} files`,
|
|
43
|
+
cwd: input.context.cwd,
|
|
44
|
+
data: {
|
|
45
|
+
kind: input.context.params.kind,
|
|
46
|
+
operation: input.operation,
|
|
47
|
+
rootCount: input.roots.length,
|
|
48
|
+
eligibleFileCount: input.enumeration.eligibleFileCount,
|
|
49
|
+
analyzedFileCount: input.analysis.analyzedFileCount,
|
|
50
|
+
matchCount: input.analysis.matches.length,
|
|
51
|
+
failureCount: input.analysis.failures.length,
|
|
52
|
+
complete: input.complete,
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
"analysis",
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodeResult,
|
|
3
|
+
OutlineData,
|
|
4
|
+
StructuralProvider as StructuralSubstrate,
|
|
5
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
6
|
+
import type { CodeFindAstKind } from "../../tool/find/ast-kinds.ts";
|
|
7
|
+
import type { AstScanLimitation } from "./ast-scan.ts";
|
|
8
|
+
import { callableExpressionForMatching } from "./call-name.ts";
|
|
9
|
+
import { settleByDeadline } from "./deadline.ts";
|
|
10
|
+
import { relativeDisplayPath } from "./paths.ts";
|
|
11
|
+
|
|
12
|
+
export interface StructuredPatternParams {
|
|
13
|
+
readonly pattern: string;
|
|
14
|
+
readonly kind: CodeFindAstKind;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StructuredMatch {
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly kind: string;
|
|
21
|
+
readonly line: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type StructuredFailureKind = Exclude<CodeResult<never>, { kind: "success" }>["kind"];
|
|
25
|
+
|
|
26
|
+
export interface StructuredFailure {
|
|
27
|
+
readonly file: string;
|
|
28
|
+
readonly kind: StructuredFailureKind;
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type StructuredScanLimitation = AstScanLimitation | ProviderScanLimitation;
|
|
33
|
+
|
|
34
|
+
export interface ProviderScanLimitation {
|
|
35
|
+
readonly reason: "provider-failure";
|
|
36
|
+
readonly pathCount: number;
|
|
37
|
+
readonly examples: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface StructuredFileAnalysis {
|
|
41
|
+
readonly matches: StructuredMatch[];
|
|
42
|
+
readonly failures: StructuredFailure[];
|
|
43
|
+
readonly limitations: StructuredScanLimitation[];
|
|
44
|
+
readonly analyzedFileCount: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface AnalyzeStructuredFilesOptions {
|
|
48
|
+
readonly files: readonly string[];
|
|
49
|
+
readonly displayBase: string;
|
|
50
|
+
readonly params: StructuredPatternParams;
|
|
51
|
+
readonly structural: StructuralSubstrate;
|
|
52
|
+
readonly deadline: number;
|
|
53
|
+
readonly now: () => number;
|
|
54
|
+
readonly signal?: AbortSignal;
|
|
55
|
+
readonly initialLimitations: readonly AstScanLimitation[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Analyze enumerated files through the operation-specific structural provider method. */
|
|
59
|
+
export async function analyzeStructuredFiles(
|
|
60
|
+
options: AnalyzeStructuredFilesOptions,
|
|
61
|
+
): Promise<StructuredFileAnalysis> {
|
|
62
|
+
const matches: StructuredMatch[] = [];
|
|
63
|
+
const failures: StructuredFailure[] = [];
|
|
64
|
+
const limitations: StructuredScanLimitation[] = [...options.initialLimitations];
|
|
65
|
+
const matcher = createStructuredMatcher(options.params.pattern);
|
|
66
|
+
let analyzedFileCount = 0;
|
|
67
|
+
|
|
68
|
+
for (const [index, absoluteFile] of options.files.entries()) {
|
|
69
|
+
options.signal?.throwIfAborted();
|
|
70
|
+
if (options.now() > options.deadline) {
|
|
71
|
+
addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const relativeFile = relativeDisplayPath(options.displayBase, absoluteFile);
|
|
75
|
+
const fileMatches: StructuredMatch[] = [];
|
|
76
|
+
const fileFailures: StructuredFailure[] = [];
|
|
77
|
+
const outcome = await settleByDeadline(
|
|
78
|
+
async () => {
|
|
79
|
+
try {
|
|
80
|
+
await collectMatchesForFile(
|
|
81
|
+
fileMatches,
|
|
82
|
+
fileFailures,
|
|
83
|
+
options.structural,
|
|
84
|
+
relativeFile,
|
|
85
|
+
options.params.kind,
|
|
86
|
+
matcher,
|
|
87
|
+
);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
fileFailures.push({
|
|
90
|
+
file: relativeFile,
|
|
91
|
+
kind: "runtime-error",
|
|
92
|
+
reason: errorMessage(error),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{ deadline: options.deadline, now: options.now, signal: options.signal },
|
|
97
|
+
);
|
|
98
|
+
if (outcome.kind === "timeout") {
|
|
99
|
+
addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
options.signal?.throwIfAborted();
|
|
103
|
+
matches.push(...fileMatches);
|
|
104
|
+
failures.push(...fileFailures);
|
|
105
|
+
if (fileFailures.length === 0) analyzedFileCount += 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (failures.length > 0) {
|
|
109
|
+
limitations.push({
|
|
110
|
+
reason: "provider-failure",
|
|
111
|
+
pathCount: failures.length,
|
|
112
|
+
examples: failures.slice(0, 5).map((failure) => failure.file),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return { matches, failures, limitations, analyzedFileCount };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addAnalysisTimeout(
|
|
119
|
+
limitations: StructuredScanLimitation[],
|
|
120
|
+
remainingFiles: readonly string[],
|
|
121
|
+
cwd: string,
|
|
122
|
+
): void {
|
|
123
|
+
if (limitations.some((limitation) => limitation.reason === "timeout")) return;
|
|
124
|
+
limitations.push({
|
|
125
|
+
reason: "timeout",
|
|
126
|
+
pathCount: remainingFiles.length,
|
|
127
|
+
examples: remainingFiles.slice(0, 5).map((file) => relativeDisplayPath(cwd, file)),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// biome-ignore lint/complexity/useMaxParams: helper takes explicit collection inputs to avoid intermediate objects in the hot path
|
|
132
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: kind-specific tree-sitter matching is clearest as one helper
|
|
133
|
+
async function collectMatchesForFile(
|
|
134
|
+
matches: StructuredMatch[],
|
|
135
|
+
failures: StructuredFailure[],
|
|
136
|
+
structural: StructuralSubstrate,
|
|
137
|
+
relFile: string,
|
|
138
|
+
kind: CodeFindAstKind,
|
|
139
|
+
matcher: (value: string) => boolean,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const recordFailure = (kind: StructuredFailureKind, reason: string) => {
|
|
142
|
+
failures.push({ file: relFile, kind, reason });
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (kind === "definition") {
|
|
146
|
+
const outline = await structural.outline(relFile);
|
|
147
|
+
if (!handleStructuralResult(outline, recordFailure)) return;
|
|
148
|
+
for (const item of flattenOutlineItems(outline.data)) {
|
|
149
|
+
if (!matcher(item.name)) continue;
|
|
150
|
+
matches.push({ file: relFile, name: item.name, kind: item.kind, line: item.startLine });
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (kind === "export") {
|
|
156
|
+
const exportsResult = await structural.exports(relFile);
|
|
157
|
+
if (!handleStructuralResult(exportsResult, recordFailure)) return;
|
|
158
|
+
for (const item of exportsResult.data) {
|
|
159
|
+
if (!matcher(item.name)) continue;
|
|
160
|
+
matches.push({ file: relFile, name: item.name, kind: item.kind, line: item.startLine });
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (kind === "import") {
|
|
166
|
+
const importsResult = await structural.imports(relFile);
|
|
167
|
+
if (!handleStructuralResult(importsResult, recordFailure)) return;
|
|
168
|
+
for (const item of importsResult.data) {
|
|
169
|
+
if (!matcher(item.moduleSpecifier)) continue;
|
|
170
|
+
matches.push({
|
|
171
|
+
file: relFile,
|
|
172
|
+
name: item.moduleSpecifier,
|
|
173
|
+
kind: "import",
|
|
174
|
+
line: item.startLine,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (kind === "call") {
|
|
181
|
+
const callResult = await structural.callSites(relFile);
|
|
182
|
+
if (!handleStructuralResult(callResult, recordFailure)) return;
|
|
183
|
+
for (const call of callResult.data) {
|
|
184
|
+
if (!matcher(callableExpressionForMatching(call.name))) continue;
|
|
185
|
+
matches.push({ file: relFile, name: call.name, kind: "call", line: call.startLine });
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const outline = await structural.outline(relFile);
|
|
191
|
+
if (!handleStructuralResult(outline, recordFailure)) return;
|
|
192
|
+
for (const item of flattenOutlineItems(outline.data)) {
|
|
193
|
+
if (kind === "type" && !TYPE_KIND.test(item.kind.toLowerCase())) continue;
|
|
194
|
+
if (kind === "interface" && item.kind.toLowerCase() !== "interface") continue;
|
|
195
|
+
if (kind === "class" && item.kind.toLowerCase() !== "class") continue;
|
|
196
|
+
if (kind === "method" && item.kind.toLowerCase() !== "method") continue;
|
|
197
|
+
if (kind === "enum" && item.kind.toLowerCase() !== "enum") continue;
|
|
198
|
+
if (!matcher(item.name)) continue;
|
|
199
|
+
matches.push({ file: relFile, name: item.name, kind: item.kind, line: item.startLine });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Flatten provider outlines so nested class/interface/enum declarations remain searchable. */
|
|
204
|
+
function flattenOutlineItems(items: readonly OutlineData[]): OutlineData[] {
|
|
205
|
+
return items.flatMap((item) => [item, ...flattenOutlineItems(item.children ?? [])]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleStructuralResult<T>(
|
|
209
|
+
result: CodeResult<T>,
|
|
210
|
+
recordFailure: (kind: StructuredFailureKind, reason: string) => void,
|
|
211
|
+
): result is { kind: "success"; data: T } {
|
|
212
|
+
if (result.kind === "success") return true;
|
|
213
|
+
recordFailure(result.kind, result.message);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function createStructuredMatcher(pattern: string): (value: string) => boolean {
|
|
218
|
+
const ignoreCase = !/[A-Z]/.test(pattern);
|
|
219
|
+
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
|
|
220
|
+
return (value: string) => {
|
|
221
|
+
const haystack = ignoreCase ? value.toLowerCase() : value;
|
|
222
|
+
return haystack.includes(needle);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function errorMessage(error: unknown): string {
|
|
227
|
+
return error instanceof Error ? error.message : "Structural provider failed.";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const TYPE_KIND = /^(?:class|interface|type|enum|struct|union|record|object|concept)$/;
|
|
@@ -1,51 +1,33 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CodeResult,
|
|
3
|
-
OutlineData,
|
|
4
|
-
StructuralProvider as StructuralSubstrate,
|
|
5
|
-
} from "@mrclrchtr/supi-code-runtime/api";
|
|
1
|
+
import type { StructuralProvider as StructuralSubstrate } from "@mrclrchtr/supi-code-runtime/api";
|
|
6
2
|
import type { StructuralSearchOperation } from "@mrclrchtr/supi-tree-sitter/api";
|
|
7
3
|
import type { CodeFindAstKind } from "../../tool/find/ast-kinds.ts";
|
|
8
4
|
import type { EvidencePartialReason } from "../evidence.ts";
|
|
9
5
|
import {
|
|
10
6
|
type AstScanExclusion,
|
|
11
|
-
type AstScanLimitation,
|
|
12
7
|
type AstScanOperations,
|
|
13
8
|
type AstScanPolicy,
|
|
14
9
|
DEFAULT_AST_SCAN_MAX_FILES,
|
|
15
10
|
DEFAULT_AST_SCAN_TIMEOUT_MS,
|
|
16
11
|
enumerateAstFiles,
|
|
17
12
|
} from "./ast-scan.ts";
|
|
18
|
-
import {
|
|
19
|
-
import { settleByDeadline } from "./deadline.ts";
|
|
13
|
+
import { startAstScanTimer } from "./ast-scan-timing.ts";
|
|
20
14
|
import { relativeDisplayPath } from "./paths.ts";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
readonly file: string;
|
|
38
|
-
readonly kind: StructuredFailureKind;
|
|
39
|
-
readonly reason: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type StructuredScanLimitation = AstScanLimitation | ProviderScanLimitation;
|
|
43
|
-
|
|
44
|
-
export interface ProviderScanLimitation {
|
|
45
|
-
readonly reason: "provider-failure";
|
|
46
|
-
readonly pathCount: number;
|
|
47
|
-
readonly examples: readonly string[];
|
|
48
|
-
}
|
|
15
|
+
import {
|
|
16
|
+
analyzeStructuredFiles,
|
|
17
|
+
type StructuredFailure,
|
|
18
|
+
type StructuredMatch,
|
|
19
|
+
type StructuredPatternParams,
|
|
20
|
+
type StructuredScanLimitation,
|
|
21
|
+
} from "./pattern-analysis.ts";
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
ProviderScanLimitation,
|
|
25
|
+
StructuredFailure,
|
|
26
|
+
StructuredFailureKind,
|
|
27
|
+
StructuredMatch,
|
|
28
|
+
StructuredPatternParams,
|
|
29
|
+
StructuredScanLimitation,
|
|
30
|
+
} from "./pattern-analysis.ts";
|
|
49
31
|
|
|
50
32
|
/** Structured completeness state for the AST source-file scan. */
|
|
51
33
|
export interface StructuredScanSummary {
|
|
@@ -98,6 +80,7 @@ export interface StructuredPatternSearchOptions {
|
|
|
98
80
|
export async function getStructuredPatternMatches(
|
|
99
81
|
options: StructuredPatternSearchOptions,
|
|
100
82
|
): Promise<StructuredPatternOutcome> {
|
|
83
|
+
const scanTimer = startAstScanTimer();
|
|
101
84
|
const now = options.control?.now ?? Date.now;
|
|
102
85
|
const maxFiles = options.control?.maxFiles ?? DEFAULT_AST_SCAN_MAX_FILES;
|
|
103
86
|
const timeoutMs = options.control?.timeoutMs ?? DEFAULT_AST_SCAN_TIMEOUT_MS;
|
|
@@ -143,6 +126,7 @@ export async function getStructuredPatternMatches(
|
|
|
143
126
|
};
|
|
144
127
|
}
|
|
145
128
|
|
|
129
|
+
scanTimer.enumerationCompleted();
|
|
146
130
|
const analysis = await analyzeStructuredFiles({
|
|
147
131
|
files: enumeration.files,
|
|
148
132
|
displayBase: enumeration.displayBase,
|
|
@@ -156,13 +140,14 @@ export async function getStructuredPatternMatches(
|
|
|
156
140
|
const capabilityMismatches = analysis.failures.filter(
|
|
157
141
|
(failure) => failure.kind === "unsupported-language",
|
|
158
142
|
);
|
|
143
|
+
const complete = enumeration.complete && analysis.limitations.length === 0;
|
|
144
|
+
scanTimer.record({ context: options, operation, roots, enumeration, analysis, complete });
|
|
159
145
|
if (capabilityMismatches.length > 0) {
|
|
160
146
|
return {
|
|
161
147
|
kind: "unavailable",
|
|
162
148
|
reason: `Structural provider rejected ${capabilityMismatches.length} file${capabilityMismatches.length === 1 ? "" : "s"} declared eligible for ${operation} analysis.`,
|
|
163
149
|
};
|
|
164
150
|
}
|
|
165
|
-
const complete = enumeration.complete && analysis.limitations.length === 0;
|
|
166
151
|
return {
|
|
167
152
|
kind: "completed",
|
|
168
153
|
result: {
|
|
@@ -183,92 +168,6 @@ export async function getStructuredPatternMatches(
|
|
|
183
168
|
};
|
|
184
169
|
}
|
|
185
170
|
|
|
186
|
-
interface AnalyzeStructuredFilesOptions {
|
|
187
|
-
readonly files: readonly string[];
|
|
188
|
-
readonly displayBase: string;
|
|
189
|
-
readonly params: StructuredPatternParams;
|
|
190
|
-
readonly structural: StructuralSubstrate;
|
|
191
|
-
readonly deadline: number;
|
|
192
|
-
readonly now: () => number;
|
|
193
|
-
readonly signal?: AbortSignal;
|
|
194
|
-
readonly initialLimitations: readonly AstScanLimitation[];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function analyzeStructuredFiles(options: AnalyzeStructuredFilesOptions): Promise<{
|
|
198
|
-
matches: StructuredMatch[];
|
|
199
|
-
failures: StructuredFailure[];
|
|
200
|
-
limitations: StructuredScanLimitation[];
|
|
201
|
-
analyzedFileCount: number;
|
|
202
|
-
}> {
|
|
203
|
-
const matches: StructuredMatch[] = [];
|
|
204
|
-
const failures: StructuredFailure[] = [];
|
|
205
|
-
const limitations: StructuredScanLimitation[] = [...options.initialLimitations];
|
|
206
|
-
const matcher = createStructuredMatcher(options.params.pattern);
|
|
207
|
-
let analyzedFileCount = 0;
|
|
208
|
-
|
|
209
|
-
for (const [index, absoluteFile] of options.files.entries()) {
|
|
210
|
-
options.signal?.throwIfAborted();
|
|
211
|
-
if (options.now() > options.deadline) {
|
|
212
|
-
addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
const relativeFile = relativeDisplayPath(options.displayBase, absoluteFile);
|
|
216
|
-
const fileMatches: StructuredMatch[] = [];
|
|
217
|
-
const fileFailures: StructuredFailure[] = [];
|
|
218
|
-
const outcome = await settleByDeadline(
|
|
219
|
-
async () => {
|
|
220
|
-
try {
|
|
221
|
-
await collectMatchesForFile(
|
|
222
|
-
fileMatches,
|
|
223
|
-
fileFailures,
|
|
224
|
-
options.structural,
|
|
225
|
-
relativeFile,
|
|
226
|
-
options.params.kind,
|
|
227
|
-
matcher,
|
|
228
|
-
);
|
|
229
|
-
} catch (error) {
|
|
230
|
-
fileFailures.push({
|
|
231
|
-
file: relativeFile,
|
|
232
|
-
kind: "runtime-error",
|
|
233
|
-
reason: errorMessage(error),
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
{ deadline: options.deadline, now: options.now, signal: options.signal },
|
|
238
|
-
);
|
|
239
|
-
if (outcome.kind === "timeout") {
|
|
240
|
-
addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
options.signal?.throwIfAborted();
|
|
244
|
-
matches.push(...fileMatches);
|
|
245
|
-
failures.push(...fileFailures);
|
|
246
|
-
if (fileFailures.length === 0) analyzedFileCount += 1;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (failures.length > 0) {
|
|
250
|
-
limitations.push({
|
|
251
|
-
reason: "provider-failure",
|
|
252
|
-
pathCount: failures.length,
|
|
253
|
-
examples: failures.slice(0, 5).map((failure) => failure.file),
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
return { matches, failures, limitations, analyzedFileCount };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function addAnalysisTimeout(
|
|
260
|
-
limitations: StructuredScanLimitation[],
|
|
261
|
-
remainingFiles: readonly string[],
|
|
262
|
-
cwd: string,
|
|
263
|
-
): void {
|
|
264
|
-
if (limitations.some((limitation) => limitation.reason === "timeout")) return;
|
|
265
|
-
limitations.push({
|
|
266
|
-
reason: "timeout",
|
|
267
|
-
pathCount: remainingFiles.length,
|
|
268
|
-
examples: remainingFiles.slice(0, 5).map((file) => relativeDisplayPath(cwd, file)),
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
171
|
function primaryPartialReason(
|
|
273
172
|
limitations: readonly StructuredScanLimitation[],
|
|
274
173
|
): EvidencePartialReason {
|
|
@@ -282,94 +181,6 @@ function primaryPartialReason(
|
|
|
282
181
|
return "provider-limited";
|
|
283
182
|
}
|
|
284
183
|
|
|
285
|
-
// biome-ignore lint/complexity/useMaxParams: helper takes explicit collection inputs to avoid intermediate objects in the hot path
|
|
286
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: kind-specific tree-sitter matching is clearest as one helper
|
|
287
|
-
async function collectMatchesForFile(
|
|
288
|
-
matches: StructuredMatch[],
|
|
289
|
-
failures: StructuredFailure[],
|
|
290
|
-
structural: StructuralSubstrate,
|
|
291
|
-
relFile: string,
|
|
292
|
-
kind: CodeFindAstKind,
|
|
293
|
-
matcher: (value: string) => boolean,
|
|
294
|
-
): Promise<void> {
|
|
295
|
-
const recordFailure = (kind: StructuredFailureKind, reason: string) => {
|
|
296
|
-
failures.push({ file: relFile, kind, reason });
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
if (kind === "definition") {
|
|
300
|
-
const outline = await structural.outline(relFile);
|
|
301
|
-
if (!handleStructuralResult(outline, recordFailure)) return;
|
|
302
|
-
for (const item of flattenOutlineItems(outline.data)) {
|
|
303
|
-
if (!matcher(item.name)) continue;
|
|
304
|
-
matches.push({ file: relFile, name: item.name, kind: item.kind, line: item.startLine });
|
|
305
|
-
}
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (kind === "export") {
|
|
310
|
-
const exportsResult = await structural.exports(relFile);
|
|
311
|
-
if (!handleStructuralResult(exportsResult, recordFailure)) return;
|
|
312
|
-
for (const item of exportsResult.data) {
|
|
313
|
-
if (!matcher(item.name)) continue;
|
|
314
|
-
matches.push({ file: relFile, name: item.name, kind: item.kind, line: item.startLine });
|
|
315
|
-
}
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (kind === "import") {
|
|
320
|
-
const importsResult = await structural.imports(relFile);
|
|
321
|
-
if (!handleStructuralResult(importsResult, recordFailure)) return;
|
|
322
|
-
for (const item of importsResult.data) {
|
|
323
|
-
if (!matcher(item.moduleSpecifier)) continue;
|
|
324
|
-
matches.push({
|
|
325
|
-
file: relFile,
|
|
326
|
-
name: item.moduleSpecifier,
|
|
327
|
-
kind: "import",
|
|
328
|
-
line: item.startLine,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (kind === "call") {
|
|
335
|
-
const callResult = await structural.callSites(relFile);
|
|
336
|
-
if (!handleStructuralResult(callResult, recordFailure)) return;
|
|
337
|
-
for (const call of callResult.data) {
|
|
338
|
-
if (!matcher(callableExpressionForMatching(call.name))) continue;
|
|
339
|
-
matches.push({ file: relFile, name: call.name, kind: "call", line: call.startLine });
|
|
340
|
-
}
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const outline = await structural.outline(relFile);
|
|
345
|
-
if (!handleStructuralResult(outline, recordFailure)) return;
|
|
346
|
-
for (const item of flattenOutlineItems(outline.data)) {
|
|
347
|
-
if (kind === "type" && !TYPE_KIND.test(item.kind.toLowerCase())) continue;
|
|
348
|
-
if (kind === "interface" && item.kind.toLowerCase() !== "interface") continue;
|
|
349
|
-
if (kind === "class" && item.kind.toLowerCase() !== "class") continue;
|
|
350
|
-
if (kind === "method" && item.kind.toLowerCase() !== "method") continue;
|
|
351
|
-
if (kind === "enum" && item.kind.toLowerCase() !== "enum") continue;
|
|
352
|
-
if (!matcher(item.name)) continue;
|
|
353
|
-
matches.push({ file: relFile, name: item.name, kind: item.kind, line: item.startLine });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Flatten provider outlines so nested class/interface/enum declarations remain searchable. */
|
|
358
|
-
function flattenOutlineItems(items: readonly OutlineData[]): OutlineData[] {
|
|
359
|
-
return items.flatMap((item) => [item, ...flattenOutlineItems(item.children ?? [])]);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function handleStructuralResult<T>(
|
|
363
|
-
result: CodeResult<T>,
|
|
364
|
-
recordFailure: (kind: StructuredFailureKind, reason: string) => void,
|
|
365
|
-
): result is { kind: "success"; data: T } {
|
|
366
|
-
if (result.kind === "success") return true;
|
|
367
|
-
recordFailure(result.kind, result.message);
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const TYPE_KIND = /^(?:class|interface|type|enum|struct|union|record|object|concept)$/;
|
|
372
|
-
|
|
373
184
|
const AST_KIND_OPERATIONS = {
|
|
374
185
|
definition: "outline",
|
|
375
186
|
import: "imports",
|
|
@@ -385,16 +196,3 @@ const AST_KIND_OPERATIONS = {
|
|
|
385
196
|
function structuralOperationForKind(kind: CodeFindAstKind): StructuralSearchOperation {
|
|
386
197
|
return AST_KIND_OPERATIONS[kind];
|
|
387
198
|
}
|
|
388
|
-
|
|
389
|
-
function createStructuredMatcher(pattern: string): (value: string) => boolean {
|
|
390
|
-
const ignoreCase = !/[A-Z]/.test(pattern);
|
|
391
|
-
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
|
|
392
|
-
return (value: string) => {
|
|
393
|
-
const haystack = ignoreCase ? value.toLowerCase() : value;
|
|
394
|
-
return haystack.includes(needle);
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function errorMessage(error: unknown): string {
|
|
399
|
-
return error instanceof Error ? error.message : "Structural provider failed.";
|
|
400
|
-
}
|