@mrclrchtr/supi-code-intelligence 0.2.0 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +31 -187
  2. package/node_modules/@mrclrchtr/supi-core/package.json +8 -4
  3. package/node_modules/@mrclrchtr/supi-lsp/README.md +40 -86
  4. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +8 -4
  5. package/node_modules/@mrclrchtr/supi-lsp/package.json +15 -5
  6. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +11 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-stale-resync.ts +47 -0
  9. package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +1 -1
  10. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +38 -70
  11. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +20 -29
  12. package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +9 -0
  13. package/package.json +14 -8
  14. package/src/actions/affected-action.ts +153 -24
  15. package/src/actions/callees-action.ts +17 -0
  16. package/src/actions/callers-action.ts +178 -111
  17. package/src/actions/implementations-action.ts +18 -0
  18. package/src/actions/pattern-action.ts +167 -7
  19. package/src/brief-focused.ts +189 -9
  20. package/src/code-intelligence.ts +10 -2
  21. package/src/git-context.ts +11 -0
  22. package/src/guidance.ts +11 -8
  23. package/src/pattern-structured.ts +196 -0
  24. package/src/prioritization-signals.ts +188 -0
  25. package/src/resolve-target.ts +11 -3
  26. package/src/semantic-action-helpers.ts +28 -0
  27. package/src/target-resolution.ts +215 -0
  28. package/src/tool-actions.ts +8 -0
  29. package/src/types.ts +4 -0
@@ -0,0 +1,188 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getSessionLspService } from "@mrclrchtr/supi-lsp";
4
+
5
+ export interface PrioritySignalsSummary {
6
+ diagnosticsCount: number;
7
+ lowCoverageCount: number;
8
+ unusedCount: number;
9
+ warnings: string[];
10
+ }
11
+
12
+ interface LoadedSignals {
13
+ diagnostics: Array<{ file: string; total: number; errors: number; warnings: number }>;
14
+ coverageByFile: Map<string, number>;
15
+ unusedFiles: Set<string>;
16
+ unusedExports: Array<{ file: string; name: string }>;
17
+ }
18
+
19
+ export function summarizePrioritySignalsForFiles(
20
+ cwd: string,
21
+ files: Iterable<string>,
22
+ ): PrioritySignalsSummary | null {
23
+ const loaded = loadPrioritizationSignals(cwd);
24
+ const relevantFiles = new Set([...files].map((file) => path.resolve(cwd, file)));
25
+ if (relevantFiles.size === 0) return null;
26
+
27
+ const warnings: string[] = [];
28
+
29
+ const matchingDiagnostics = loaded.diagnostics.filter((entry) =>
30
+ relevantFiles.has(path.resolve(entry.file)),
31
+ );
32
+ const diagnosticsCount = matchingDiagnostics.reduce((sum, entry) => sum + entry.total, 0);
33
+ for (const entry of matchingDiagnostics.slice(0, 3)) {
34
+ warnings.push(
35
+ `Diagnostics: \`${path.relative(cwd, entry.file)}\` (${entry.total} total${entry.errors > 0 ? `, ${entry.errors} errors` : ""}${entry.warnings > 0 ? `, ${entry.warnings} warnings` : ""})`,
36
+ );
37
+ }
38
+
39
+ const lowCoverage = [...loaded.coverageByFile.entries()]
40
+ .filter(([file, pct]) => relevantFiles.has(path.resolve(file)) && pct < 50)
41
+ .sort((a, b) => a[1] - b[1]);
42
+ for (const [file, pct] of lowCoverage.slice(0, 3)) {
43
+ warnings.push(`Low coverage: \`${path.relative(cwd, file)}\` (${pct.toFixed(0)}%)`);
44
+ }
45
+
46
+ const unusedFileMatches = [...loaded.unusedFiles]
47
+ .filter((file) => relevantFiles.has(path.resolve(file)))
48
+ .sort((a, b) => a.localeCompare(b));
49
+ for (const file of unusedFileMatches.slice(0, 3)) {
50
+ warnings.push(`Unused file: \`${path.relative(cwd, file)}\``);
51
+ }
52
+
53
+ const unusedExportMatches = loaded.unusedExports
54
+ .filter((entry) => relevantFiles.has(path.resolve(entry.file)))
55
+ .sort((a, b) => a.name.localeCompare(b.name));
56
+ for (const entry of unusedExportMatches.slice(0, 3)) {
57
+ warnings.push(`Unused export: \`${entry.name}\` in \`${path.relative(cwd, entry.file)}\``);
58
+ }
59
+
60
+ const summary: PrioritySignalsSummary = {
61
+ diagnosticsCount,
62
+ lowCoverageCount: lowCoverage.length,
63
+ unusedCount: unusedFileMatches.length + unusedExportMatches.length,
64
+ warnings,
65
+ };
66
+
67
+ return summary.diagnosticsCount > 0 || summary.lowCoverageCount > 0 || summary.unusedCount > 0
68
+ ? summary
69
+ : null;
70
+ }
71
+
72
+ export function loadPrioritizationSignals(cwd: string): LoadedSignals {
73
+ return {
74
+ diagnostics: loadDiagnostics(cwd),
75
+ coverageByFile: loadCoverageSummary(cwd),
76
+ unusedFiles: loadUnusedFiles(cwd),
77
+ unusedExports: loadUnusedExports(cwd),
78
+ };
79
+ }
80
+
81
+ export function appendPrioritySignalsSection(
82
+ lines: string[],
83
+ summary: PrioritySignalsSummary | null,
84
+ ): void {
85
+ if (!summary || summary.warnings.length === 0) return;
86
+ lines.push("## Priority Signals");
87
+ for (const warning of summary.warnings.slice(0, 6)) {
88
+ lines.push(`- ${warning}`);
89
+ }
90
+ lines.push("");
91
+ }
92
+
93
+ function loadDiagnostics(
94
+ cwd: string,
95
+ ): Array<{ file: string; total: number; errors: number; warnings: number }> {
96
+ const lspState = getSessionLspService(cwd);
97
+ if (lspState.kind !== "ready") return [];
98
+
99
+ return lspState.service.getOutstandingDiagnosticSummary(2).map((entry) => ({
100
+ file: path.resolve(cwd, entry.file),
101
+ total: entry.total,
102
+ errors: entry.errors,
103
+ warnings: entry.warnings,
104
+ }));
105
+ }
106
+
107
+ function loadCoverageSummary(cwd: string): Map<string, number> {
108
+ const coveragePath = path.join(cwd, "coverage", "coverage-summary.json");
109
+ if (!fs.existsSync(coveragePath)) return new Map();
110
+
111
+ try {
112
+ const parsed = JSON.parse(fs.readFileSync(coveragePath, "utf-8")) as Record<string, unknown>;
113
+ const map = new Map<string, number>();
114
+ for (const [file, value] of Object.entries(parsed)) {
115
+ if (file === "total" || typeof value !== "object" || value === null) continue;
116
+ const linesPct = getPct(value, "lines");
117
+ const statementsPct = getPct(value, "statements");
118
+ const pct = Math.min(linesPct ?? 100, statementsPct ?? 100);
119
+ map.set(path.resolve(cwd, file), pct);
120
+ }
121
+ return map;
122
+ } catch {
123
+ return new Map();
124
+ }
125
+ }
126
+
127
+ function loadUnusedFiles(cwd: string): Set<string> {
128
+ const parsed = loadKnipJson(cwd);
129
+ const values = Array.isArray(parsed?.files) ? parsed.files : [];
130
+ const files = values
131
+ .map((value) => toPathString(value))
132
+ .filter((value): value is string => Boolean(value))
133
+ .map((file) => path.resolve(cwd, file));
134
+ return new Set(files);
135
+ }
136
+
137
+ function loadUnusedExports(cwd: string): Array<{ file: string; name: string }> {
138
+ const parsed = loadKnipJson(cwd);
139
+ const values = Array.isArray(parsed?.exports) ? parsed.exports : [];
140
+ const exports: Array<{ file: string; name: string }> = [];
141
+
142
+ for (const value of values) {
143
+ if (typeof value === "string") {
144
+ exports.push({ file: path.resolve(cwd, value), name: path.basename(value) });
145
+ continue;
146
+ }
147
+ if (typeof value !== "object" || value === null) continue;
148
+ const record = value as Record<string, unknown>;
149
+ const file = toPathString(record.file) ?? toPathString(record.path);
150
+ const name = typeof record.name === "string" ? record.name : null;
151
+ if (file && name) {
152
+ exports.push({ file: path.resolve(cwd, file), name });
153
+ }
154
+ }
155
+
156
+ return exports;
157
+ }
158
+
159
+ function loadKnipJson(cwd: string): Record<string, unknown> | null {
160
+ const knipPath = path.join(cwd, "knip.json");
161
+ if (!fs.existsSync(knipPath)) return null;
162
+ try {
163
+ const parsed = JSON.parse(fs.readFileSync(knipPath, "utf-8"));
164
+ return typeof parsed === "object" && parsed !== null
165
+ ? (parsed as Record<string, unknown>)
166
+ : null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ function getPct(value: object, key: string): number | null {
173
+ const candidate = (value as Record<string, unknown>)[key];
174
+ if (typeof candidate !== "object" || candidate === null) return null;
175
+ const pct = (candidate as Record<string, unknown>).pct;
176
+ return typeof pct === "number" ? pct : null;
177
+ }
178
+
179
+ function toPathString(value: unknown): string | null {
180
+ if (typeof value === "string") return value;
181
+ if (typeof value !== "object" || value === null) return null;
182
+ const record = value as Record<string, unknown>;
183
+ return typeof record.file === "string"
184
+ ? record.file
185
+ : typeof record.path === "string"
186
+ ? record.path
187
+ : null;
188
+ }
@@ -1,18 +1,20 @@
1
1
  import {
2
2
  type ResolvedTarget,
3
+ type ResolvedTargetGroup,
3
4
  resolveAnchoredTarget,
5
+ resolveFileTargetGroup,
4
6
  resolveSymbolTarget,
5
7
  } from "./target-resolution.ts";
6
8
  import type { ActionParams } from "./tool-actions.ts";
7
9
 
8
10
  /**
9
- * Resolve a target from action params. Returns either a ResolvedTarget
11
+ * Resolve a target from action params. Returns either a target, a file-level target group,
10
12
  * or a string error/disambiguation message to return directly.
11
13
  */
12
14
  export async function resolveTarget(
13
15
  params: ActionParams,
14
16
  cwd: string,
15
- ): Promise<ResolvedTarget | string> {
17
+ ): Promise<ResolvedTarget | ResolvedTargetGroup | string> {
16
18
  if (!params.file && !params.symbol) {
17
19
  return "**Error:** Semantic actions require either anchored coordinates (`file`, `line`, `character`) or a `symbol` for discovery.";
18
20
  }
@@ -22,7 +24,7 @@ export async function resolveTarget(
22
24
  }
23
25
 
24
26
  if (params.file && !params.symbol) {
25
- return "**Error:** Semantic actions with `file` require `line` and `character`, or provide `symbol` for discovery.";
27
+ return resolveByFile(params.file, cwd);
26
28
  }
27
29
 
28
30
  if (params.symbol) {
@@ -44,6 +46,12 @@ function resolveAnchored(
44
46
  return "**Error:** Unexpected disambiguation for anchored target.";
45
47
  }
46
48
 
49
+ async function resolveByFile(file: string, cwd: string): Promise<ResolvedTargetGroup | string> {
50
+ const result = await resolveFileTargetGroup(file, cwd);
51
+ if (result.kind === "error") return result.message;
52
+ return result.group;
53
+ }
54
+
47
55
  async function resolveBySymbol(
48
56
  params: ActionParams,
49
57
  cwd: string,
@@ -0,0 +1,28 @@
1
+ import type { ResolvedTarget, ResolvedTargetGroup } from "./target-resolution.ts";
2
+ import type { ConfidenceMode } from "./types.ts";
3
+
4
+ interface FileLineRef {
5
+ file: string;
6
+ line: number;
7
+ }
8
+
9
+ export function isResolvedTargetGroup(
10
+ target: ResolvedTarget | ResolvedTargetGroup,
11
+ ): target is ResolvedTargetGroup {
12
+ return "targets" in target;
13
+ }
14
+
15
+ export function highestConfidence(confidences: ConfidenceMode[]): ConfidenceMode {
16
+ if (confidences.includes("semantic")) return "semantic";
17
+ if (confidences.includes("structural")) return "structural";
18
+ if (confidences.includes("heuristic")) return "heuristic";
19
+ return "unavailable";
20
+ }
21
+
22
+ export function dedupeFileLineRefs<T extends FileLineRef>(refs: T[]): T[] {
23
+ const deduped = new Map<string, T>();
24
+ for (const ref of refs) {
25
+ deduped.set(`${ref.file}:${ref.line}`, ref);
26
+ }
27
+ return [...deduped.values()];
28
+ }
@@ -1,11 +1,14 @@
1
1
  // Target resolution — resolve symbol references to concrete file positions
2
2
  // for semantic actions (callers, callees, implementations, affected).
3
+ // biome-ignore-all lint/nursery/noExcessiveLinesPerFile: anchored, symbol, and file-surface target resolution intentionally live together to share resolution helpers
3
4
 
4
5
  import * as fs from "node:fs";
5
6
  import * as path from "node:path";
6
7
  import { isWithinOrEqual } from "@mrclrchtr/supi-core";
7
8
  import { getSessionLspService, type Position, type SessionLspService } from "@mrclrchtr/supi-lsp";
9
+ import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
8
10
  import { escapeRegex, normalizePath } from "./search-helpers.ts";
11
+ import { highestConfidence } from "./semantic-action-helpers.ts";
9
12
  import type { ConfidenceMode, DisambiguationCandidate } from "./types.ts";
10
13
 
11
14
  export interface ResolvedTarget {
@@ -20,6 +23,13 @@ export interface ResolvedTarget {
20
23
  confidence: ConfidenceMode;
21
24
  }
22
25
 
26
+ export interface ResolvedTargetGroup {
27
+ file: string;
28
+ displayName: string;
29
+ targets: ResolvedTarget[];
30
+ confidence: ConfidenceMode;
31
+ }
32
+
23
33
  export type TargetResolutionResult =
24
34
  | { kind: "resolved"; target: ResolvedTarget }
25
35
  | { kind: "disambiguation"; candidates: DisambiguationCandidate[]; omittedCount: number }
@@ -73,6 +83,52 @@ export function resolveAnchoredTarget(
73
83
  };
74
84
  }
75
85
 
86
+ /**
87
+ * Resolve a file-only request into a group of actionable targets.
88
+ * Prefers LSP document symbols when available and falls back to Tree-sitter export discovery.
89
+ */
90
+ export async function resolveFileTargetGroup(
91
+ file: string,
92
+ cwd: string,
93
+ ): Promise<{ kind: "resolved"; group: ResolvedTargetGroup } | { kind: "error"; message: string }> {
94
+ const resolvedFile = normalizePath(file, cwd);
95
+
96
+ if (!fs.existsSync(resolvedFile)) {
97
+ return { kind: "error", message: `File not found: \`${file}\`` };
98
+ }
99
+
100
+ if (isBinaryFile(resolvedFile)) {
101
+ return {
102
+ kind: "error",
103
+ message: `File type not supported for semantic analysis: \`${file}\`. Try \`code_intel pattern\` for text search.`,
104
+ };
105
+ }
106
+
107
+ const relPath = path.relative(cwd, resolvedFile);
108
+ const structuralTargets = await resolveFileTargetsViaTreeSitter(relPath, resolvedFile, cwd);
109
+ const lspTargets = await resolveFileTargetsViaLsp(resolvedFile, cwd, structuralTargets);
110
+ const targets = lspTargets ?? structuralTargets;
111
+
112
+ if (!targets || targets.length === 0) {
113
+ return {
114
+ kind: "error",
115
+ message:
116
+ `**Error:** File-level semantic exploration is not available for \`${file}\`. ` +
117
+ "Provide `line` and `character`, or a `symbol` for discovery.",
118
+ };
119
+ }
120
+
121
+ return {
122
+ kind: "resolved",
123
+ group: {
124
+ file: resolvedFile,
125
+ displayName: relPath,
126
+ targets,
127
+ confidence: highestConfidence(targets.map((target) => target.confidence)),
128
+ },
129
+ };
130
+ }
131
+
76
132
  /**
77
133
  * Resolve a target from symbol discovery — finds matching declarations.
78
134
  * Uses LSP workspace symbols when available, falls back to Tree-sitter/text search.
@@ -271,6 +327,165 @@ async function resolveSymbolViaSearch(
271
327
  }
272
328
  }
273
329
 
330
+ async function resolveFileTargetsViaLsp(
331
+ resolvedFile: string,
332
+ cwd: string,
333
+ structuralTargets: ResolvedTarget[] | null,
334
+ ): Promise<ResolvedTarget[] | null> {
335
+ const lspState = getSessionLspService(cwd);
336
+ if (lspState.kind !== "ready") return null;
337
+
338
+ const symbols = await lspState.service.documentSymbols(resolvedFile);
339
+ if (!symbols || symbols.length === 0) {
340
+ return structuralTargets;
341
+ }
342
+
343
+ const topLevel = flattenDocumentSymbols(symbols)
344
+ .filter((symbol) => !symbol.container)
345
+ .map((symbol) =>
346
+ createResolvedTarget({
347
+ file: resolvedFile,
348
+ line: symbol.line,
349
+ character: symbol.character,
350
+ name: symbol.name,
351
+ kind: symbol.kind,
352
+ confidence: "semantic",
353
+ }),
354
+ );
355
+
356
+ if (topLevel.length === 0) {
357
+ return structuralTargets;
358
+ }
359
+
360
+ if (!structuralTargets || structuralTargets.length === 0) {
361
+ return dedupeTargets(topLevel);
362
+ }
363
+
364
+ const matched = structuralTargets.map((target) => {
365
+ const byName = topLevel.find((candidate) => candidate.name === target.name);
366
+ return byName ?? target;
367
+ });
368
+
369
+ return dedupeTargets(matched);
370
+ }
371
+
372
+ async function resolveFileTargetsViaTreeSitter(
373
+ relPath: string,
374
+ resolvedFile: string,
375
+ cwd: string,
376
+ ): Promise<ResolvedTarget[] | null> {
377
+ let tsSession: ReturnType<typeof createTreeSitterSession> | null = null;
378
+ try {
379
+ tsSession = createTreeSitterSession(cwd);
380
+ const exportsResult = await tsSession.exports(relPath);
381
+ if (exportsResult.kind !== "success" || exportsResult.data.length === 0) {
382
+ return null;
383
+ }
384
+
385
+ return dedupeTargets(
386
+ exportsResult.data.map((record) =>
387
+ createResolvedTarget({
388
+ file: resolvedFile,
389
+ line: record.range.startLine,
390
+ character: record.range.startCharacter,
391
+ name: record.name,
392
+ kind: record.kind,
393
+ confidence: "structural",
394
+ }),
395
+ ),
396
+ );
397
+ } catch {
398
+ return null;
399
+ } finally {
400
+ tsSession?.dispose();
401
+ }
402
+ }
403
+
404
+ function flattenDocumentSymbols(
405
+ symbols: Array<{
406
+ name: string;
407
+ kind: number;
408
+ selectionRange?: { start: { line: number; character: number } };
409
+ location?: { range: { start: { line: number; character: number } } };
410
+ children?: Array<unknown>;
411
+ }>,
412
+ container: string | null = null,
413
+ ): Array<{
414
+ name: string;
415
+ kind: string;
416
+ line: number;
417
+ character: number;
418
+ container: string | null;
419
+ }> {
420
+ const flattened: Array<{
421
+ name: string;
422
+ kind: string;
423
+ line: number;
424
+ character: number;
425
+ container: string | null;
426
+ }> = [];
427
+
428
+ for (const symbol of symbols) {
429
+ const start = symbol.selectionRange?.start ?? symbol.location?.range.start;
430
+ if (!start) continue;
431
+
432
+ flattened.push({
433
+ name: symbol.name,
434
+ kind: symbolKindName(symbol.kind),
435
+ line: start.line + 1,
436
+ character: start.character + 1,
437
+ container,
438
+ });
439
+
440
+ if (Array.isArray(symbol.children) && symbol.children.length > 0) {
441
+ flattened.push(
442
+ ...flattenDocumentSymbols(
443
+ symbol.children as Array<{
444
+ name: string;
445
+ kind: number;
446
+ selectionRange?: { start: { line: number; character: number } };
447
+ location?: { range: { start: { line: number; character: number } } };
448
+ children?: Array<unknown>;
449
+ }>,
450
+ symbol.name,
451
+ ),
452
+ );
453
+ }
454
+ }
455
+
456
+ return flattened;
457
+ }
458
+
459
+ function createResolvedTarget(input: {
460
+ file: string;
461
+ line: number;
462
+ character: number;
463
+ name: string;
464
+ kind: string | null;
465
+ confidence: ConfidenceMode;
466
+ }): ResolvedTarget {
467
+ return {
468
+ file: input.file,
469
+ position: toZeroBased(input.line, input.character),
470
+ displayLine: input.line,
471
+ displayCharacter: input.character,
472
+ name: input.name,
473
+ kind: input.kind,
474
+ confidence: input.confidence,
475
+ };
476
+ }
477
+
478
+ function dedupeTargets(targets: ResolvedTarget[]): ResolvedTarget[] {
479
+ const deduped = new Map<string, ResolvedTarget>();
480
+ for (const target of targets) {
481
+ const key = `${target.name ?? ""}:${target.displayLine}:${target.displayCharacter}`;
482
+ if (!deduped.has(key)) {
483
+ deduped.set(key, target);
484
+ }
485
+ }
486
+ return [...deduped.values()];
487
+ }
488
+
274
489
  // ── Helpers ───────────────────────────────────────────────────────────
275
490
 
276
491
  function mapCandidateToDisambiguation(
@@ -105,5 +105,13 @@ function validateParams(params: ActionParams, cwd: string): string | null {
105
105
  return "**Error:** `line` and `character` require `file`.";
106
106
  }
107
107
 
108
+ if (
109
+ params.action === "pattern" &&
110
+ params.kind &&
111
+ !new Set(["definition", "export", "import"]).has(params.kind)
112
+ ) {
113
+ return "**Error:** `pattern` action `kind` must be one of `definition`, `export`, or `import`.";
114
+ }
115
+
108
116
  return null;
109
117
  }
package/src/types.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  // Shared types for code intelligence tool results and metadata.
2
2
 
3
+ import type { PrioritySignalsSummary } from "./prioritization-signals.ts";
4
+
3
5
  /** Confidence vocabulary for result labeling. */
4
6
  export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavailable";
5
7
 
@@ -12,6 +14,7 @@ export interface BriefDetails {
12
14
  dependencySummary: { moduleCount: number; edgeCount: number } | null;
13
15
  omittedCount: number;
14
16
  nextQueries: string[];
17
+ prioritySignals?: PrioritySignalsSummary | null;
15
18
  }
16
19
 
17
20
  /** Structured details metadata for relationship and pattern results. */
@@ -33,6 +36,7 @@ export interface AffectedDetails {
33
36
  likelyTests: string[];
34
37
  omittedCount: number;
35
38
  nextQueries: string[];
39
+ prioritySignals?: PrioritySignalsSummary | null;
36
40
  }
37
41
 
38
42
  /** Disambiguation candidate for ambiguous symbol resolution. */