@mrclrchtr/supi-code-intelligence 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +212 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/README.md +112 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/package.json +45 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/capabilities.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +229 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +545 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +192 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/config.ts +143 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/defaults.json +82 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +82 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +68 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +73 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +98 -0
  46. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +47 -0
  47. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +58 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +359 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +163 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +17 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/lsp-state.ts +82 -0
  52. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +470 -0
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +34 -0
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +139 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +39 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +46 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-types.ts +39 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +83 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +18 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +550 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/overrides.ts +173 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +197 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/renderer.ts +120 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/scanner.ts +153 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +98 -0
  66. package/node_modules/@mrclrchtr/supi-lsp/src/service-registry.ts +153 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +292 -0
  68. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +153 -0
  69. package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +430 -0
  70. package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +48 -0
  71. package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +156 -0
  72. package/node_modules/@mrclrchtr/supi-lsp/src/types.ts +409 -0
  73. package/node_modules/@mrclrchtr/supi-lsp/src/ui.ts +358 -0
  74. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +122 -0
  75. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-sentinels.ts +114 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +97 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +67 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/.gitkeep +0 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm +0 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm +0 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm +0 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm +0 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm +0 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm +0 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +126 -0
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +144 -0
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +151 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/callees.ts +343 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +108 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/exports.ts +315 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/formatting.ts +104 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/imports.ts +42 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +16 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +116 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/node-at.ts +96 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/outline.ts +287 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +237 -0
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session.ts +112 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/structure.ts +7 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +13 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +306 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +146 -0
  127. package/package.json +47 -0
  128. package/src/actions/affected-action.ts +310 -0
  129. package/src/actions/brief-action.ts +242 -0
  130. package/src/actions/callees-action.ts +134 -0
  131. package/src/actions/callers-action.ts +215 -0
  132. package/src/actions/implementations-action.ts +190 -0
  133. package/src/actions/index-action.ts +187 -0
  134. package/src/actions/pattern-action.ts +232 -0
  135. package/src/architecture.ts +367 -0
  136. package/src/brief-focused.ts +383 -0
  137. package/src/brief.ts +228 -0
  138. package/src/code-intelligence.ts +122 -0
  139. package/src/git-context.ts +65 -0
  140. package/src/guidance.ts +39 -0
  141. package/src/index.ts +28 -0
  142. package/src/resolve-target.ts +104 -0
  143. package/src/search-helpers.ts +283 -0
  144. package/src/target-resolution.ts +368 -0
  145. package/src/tool-actions.ts +109 -0
  146. package/src/types.ts +57 -0
@@ -0,0 +1,232 @@
1
+ // Pattern action — bounded, scope-aware text search.
2
+
3
+ import type { RgMatch } from "../search-helpers.ts";
4
+ import {
5
+ escapeRegex,
6
+ groupByFile,
7
+ normalizePath,
8
+ runRipgrep,
9
+ runRipgrepDetailed,
10
+ } from "../search-helpers.ts";
11
+ import type { ActionParams } from "../tool-actions.ts";
12
+ import type { CodeIntelResult, SearchDetails } from "../types.ts";
13
+
14
+ /**
15
+ * Execute the bounded text-search action.
16
+ *
17
+ * Public `pattern` input is treated as a literal string by default. Callers can
18
+ * opt into raw ripgrep regex semantics with `regex: true`; malformed regex
19
+ * patterns are surfaced as explicit user-facing errors instead of being
20
+ * collapsed into a misleading no-match response.
21
+ */
22
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: function has multiple distinct paths (validation, regex vs literal, summary vs detailed, zero vs results) that are clearer when explicit than when split; 18 is acceptable for the use case
23
+ export async function executePatternAction(
24
+ params: ActionParams,
25
+ cwd: string,
26
+ ): Promise<CodeIntelResult> {
27
+ if (!params.pattern) {
28
+ return {
29
+ content: "**Error:** `pattern` action requires a `pattern` parameter.",
30
+ details: undefined,
31
+ };
32
+ }
33
+
34
+ const maxResults = params.maxResults ?? 8;
35
+ const contextLines = params.contextLines ?? 1;
36
+ const scopePath = params.path ? normalizePath(params.path, cwd) : cwd;
37
+ const relScope = params.path ?? ".";
38
+
39
+ const matches = params.regex
40
+ ? getRegexMatches({
41
+ pattern: params.pattern,
42
+ scopePath,
43
+ cwd,
44
+ maxResults,
45
+ contextLines,
46
+ summary: params.summary,
47
+ })
48
+ : runRipgrep(escapeRegex(params.pattern), scopePath, cwd, {
49
+ maxMatches: params.summary ? undefined : maxResults * 3,
50
+ contextLines,
51
+ filterLowSignal: true,
52
+ });
53
+
54
+ if (typeof matches === "string") {
55
+ const errorDetails: SearchDetails = {
56
+ confidence: "unavailable",
57
+ scope: params.path ?? null,
58
+ candidateCount: 0,
59
+ omittedCount: 0,
60
+ nextQueries: ["Fix the regex pattern and retry"],
61
+ };
62
+ return { content: matches, details: { type: "search", data: errorDetails } };
63
+ }
64
+
65
+ if (matches.length === 0) {
66
+ const emptyDetails: SearchDetails = {
67
+ confidence: "heuristic",
68
+ scope: params.path ?? null,
69
+ candidateCount: 0,
70
+ omittedCount: 0,
71
+ nextQueries: params.regex
72
+ ? ["Set `regex: false` for literal matching"]
73
+ : ["Set `regex: true` for regex matching"],
74
+ };
75
+ return {
76
+ content: `No matches found for \`${params.pattern}\` in \`${relScope}\`.`,
77
+ details: { type: "search", data: emptyDetails },
78
+ };
79
+ }
80
+
81
+ let content: string;
82
+ if (params.summary) {
83
+ content = formatPatternSummary(params.pattern, relScope, matches, maxResults);
84
+ } else {
85
+ content = formatPatternResults(params.pattern, relScope, matches, maxResults);
86
+ }
87
+
88
+ const details: SearchDetails = {
89
+ confidence: "heuristic",
90
+ scope: params.path ?? null,
91
+ candidateCount: matches.length,
92
+ omittedCount: 0,
93
+ nextQueries: params.regex
94
+ ? ["Set `regex: false` for literal matching"]
95
+ : ["Set `regex: true` for regex matching"],
96
+ };
97
+ return { content, details: { type: "search" as const, data: details } };
98
+ }
99
+
100
+ function getRegexMatches(options: {
101
+ pattern: string;
102
+ scopePath: string;
103
+ cwd: string;
104
+ maxResults: number;
105
+ contextLines: number;
106
+ summary?: boolean;
107
+ }): RgMatch[] | string {
108
+ const result = runRipgrepDetailed(options.pattern, options.scopePath, options.cwd, {
109
+ maxMatches: options.summary ? undefined : options.maxResults * 3,
110
+ contextLines: options.contextLines,
111
+ filterLowSignal: true,
112
+ });
113
+
114
+ if (result.error) {
115
+ return formatRegexError(options.pattern, result.error);
116
+ }
117
+
118
+ return result.matches;
119
+ }
120
+
121
+ function formatRegexError(pattern: string, error: string): string {
122
+ const lines = error
123
+ .split("\n")
124
+ .map((line) => line.trim())
125
+ .filter(Boolean);
126
+ const detail =
127
+ [...lines]
128
+ .reverse()
129
+ .find((line) => line.startsWith("error:"))
130
+ ?.replace(/^error:\s*/, "") ??
131
+ lines.at(-1) ??
132
+ "ripgrep rejected the regex.";
133
+ return `**Error:** Invalid regex pattern \`${pattern}\`: ${detail}`;
134
+ }
135
+
136
+ function formatPatternResults(
137
+ pattern: string,
138
+ relScope: string,
139
+ matches: RgMatch[],
140
+ maxResults: number,
141
+ ): string {
142
+ const lines: string[] = [];
143
+ lines.push(`# Pattern: \`${pattern}\``);
144
+ lines.push("");
145
+ lines.push(`**${matches.length} match${matches.length > 1 ? "es" : ""}** in \`${relScope}\``);
146
+ lines.push("");
147
+
148
+ const byFile = groupByFile(matches);
149
+ let shown = 0;
150
+ for (const [file, fileMatches] of byFile) {
151
+ if (shown >= maxResults) break;
152
+ lines.push(`### ${file}`);
153
+ const renderedLines = new Set<number>();
154
+ const matchLines = new Set(fileMatches.map((match) => match.line));
155
+ for (const m of fileMatches.slice(0, 5)) {
156
+ renderMatchWithContext(lines, m, renderedLines, matchLines);
157
+ }
158
+ if (fileMatches.length > 5) {
159
+ lines.push(`- _+${fileMatches.length - 5} more in this file_`);
160
+ }
161
+ lines.push("");
162
+ shown++;
163
+ }
164
+
165
+ if (byFile.size > maxResults) {
166
+ lines.push(
167
+ `_+${byFile.size - maxResults} more files omitted. Narrow with \`path\` or increase \`maxResults\`._`,
168
+ );
169
+ }
170
+
171
+ return lines.join("\n");
172
+ }
173
+
174
+ function formatPatternSummary(
175
+ pattern: string,
176
+ relScope: string,
177
+ matches: RgMatch[],
178
+ maxResults: number,
179
+ ): string {
180
+ const byFile = groupByFile(matches);
181
+ const byDir = new Map<string, number>();
182
+ for (const [file] of byFile) {
183
+ const dir = file.includes("/") ? file.split("/").slice(0, -1).join("/") : ".";
184
+ byDir.set(dir, (byDir.get(dir) ?? 0) + 1);
185
+ }
186
+
187
+ const lines: string[] = [];
188
+ lines.push(`# Pattern Summary: \`${pattern}\``);
189
+ lines.push("");
190
+ lines.push(
191
+ `**${matches.length} match${matches.length > 1 ? "es" : ""}** across **${byFile.size} file${byFile.size !== 1 ? "s" : ""}** in \`${relScope}\``,
192
+ );
193
+ lines.push("");
194
+
195
+ const sortedDirs = [...byDir.entries()].sort((a, b) => b[1] - a[1]).slice(0, maxResults);
196
+ for (const [dir, count] of sortedDirs) {
197
+ lines.push(`- \`${dir}/\` — ${count} file${count !== 1 ? "s" : ""}`);
198
+ }
199
+
200
+ if (byDir.size > maxResults) {
201
+ lines.push(`- _+${byDir.size - maxResults} more directories_`);
202
+ }
203
+
204
+ lines.push("");
205
+ return lines.join("\n");
206
+ }
207
+
208
+ function renderMatchWithContext(
209
+ lines: string[],
210
+ m: RgMatch,
211
+ renderedLines: Set<number>,
212
+ matchLines: Set<number>,
213
+ ): void {
214
+ if (m.context) {
215
+ for (const c of m.context.filter((cx) => cx.line < m.line)) {
216
+ if (renderedLines.has(c.line) || matchLines.has(c.line)) continue;
217
+ lines.push(` L${c.line}: ${c.text.slice(0, 120)}`);
218
+ renderedLines.add(c.line);
219
+ }
220
+ }
221
+ if (!renderedLines.has(m.line)) {
222
+ lines.push(`- L${m.line}: \`${m.text.slice(0, 120)}\``);
223
+ renderedLines.add(m.line);
224
+ }
225
+ if (m.context) {
226
+ for (const c of m.context.filter((cx) => cx.line > m.line)) {
227
+ if (renderedLines.has(c.line) || matchLines.has(c.line)) continue;
228
+ lines.push(` L${c.line}: ${c.text.slice(0, 120)}`);
229
+ renderedLines.add(c.line);
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,367 @@
1
+ // Shared architecture model — scans project metadata to build a structural model
2
+ // that powers both auto-injected overviews and on-demand briefs.
3
+
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { findProjectRoot, isWithinOrEqual, walkProject } from "@mrclrchtr/supi-core";
7
+
8
+ // ── Types ─────────────────────────────────────────────────────────────
9
+
10
+ export interface ArchitectureModel {
11
+ /** Absolute root directory of the project */
12
+ root: string;
13
+ /** Project name from root manifest */
14
+ name: string | null;
15
+ /** Project description from root manifest */
16
+ description: string | null;
17
+ /** Detected modules/packages */
18
+ modules: ModuleInfo[];
19
+ /** Internal dependency edges between modules */
20
+ edges: DependencyEdge[];
21
+ }
22
+
23
+ export interface ModuleInfo {
24
+ /** Package/module name */
25
+ name: string;
26
+ /** Short description from manifest */
27
+ description: string | null;
28
+ /** Absolute path to the module root */
29
+ root: string;
30
+ /** Relative path from project root */
31
+ relativePath: string;
32
+ /** Main entrypoint(s) from manifest */
33
+ entrypoints: string[];
34
+ /** Whether this is a leaf module (no internal dependents) */
35
+ isLeaf: boolean;
36
+ /** Internal dependency names */
37
+ internalDeps: string[];
38
+ /** External dependency names */
39
+ externalDeps: string[];
40
+ }
41
+
42
+ export interface DependencyEdge {
43
+ /** Source module name */
44
+ from: string;
45
+ /** Target module name */
46
+ to: string;
47
+ }
48
+
49
+ // ── Model building ────────────────────────────────────────────────────
50
+
51
+ const PROJECT_MARKERS = [
52
+ "package.json",
53
+ "pnpm-workspace.yaml",
54
+ "deno.json",
55
+ "deno.jsonc",
56
+ "Cargo.toml",
57
+ "go.mod",
58
+ "pyproject.toml",
59
+ ];
60
+
61
+ /**
62
+ * Build an architecture model from project metadata.
63
+ * Starts from cheap manifest scanning — no deep AST or LSP analysis.
64
+ */
65
+ export async function buildArchitectureModel(cwd: string): Promise<ArchitectureModel | null> {
66
+ const root = findProjectRoot(cwd, PROJECT_MARKERS, cwd);
67
+
68
+ // Read root manifest
69
+ const rootManifest = readPackageJson(root);
70
+ if (!rootManifest) {
71
+ // Try to detect at least some source structure
72
+ return buildMinimalModel(root);
73
+ }
74
+
75
+ const projectName = rootManifest.name ?? null;
76
+ const projectDescription = rootManifest.description ?? null;
77
+
78
+ // Detect workspace packages
79
+ const workspaceModules = await detectWorkspaceModules(root, rootManifest);
80
+
81
+ if (workspaceModules.length === 0) {
82
+ // Single-package project
83
+ return buildSinglePackageModel(root, rootManifest);
84
+ }
85
+
86
+ // Build dependency edges between workspace modules
87
+ const moduleNames = new Set(workspaceModules.map((m) => m.name));
88
+ const edges: DependencyEdge[] = [];
89
+
90
+ for (const mod of workspaceModules) {
91
+ for (const dep of mod.internalDeps) {
92
+ if (moduleNames.has(dep)) {
93
+ edges.push({ from: mod.name, to: dep });
94
+ }
95
+ }
96
+ }
97
+
98
+ // Mark leaf modules (no other module depends on them internally)
99
+ const dependedOn = new Set(edges.map((e) => e.to));
100
+ for (const mod of workspaceModules) {
101
+ mod.isLeaf = !dependedOn.has(mod.name);
102
+ }
103
+
104
+ return {
105
+ root,
106
+ name: projectName,
107
+ description: projectDescription,
108
+ modules: workspaceModules,
109
+ edges,
110
+ };
111
+ }
112
+
113
+ // ── Workspace detection ───────────────────────────────────────────────
114
+
115
+ interface PackageJson {
116
+ name?: string;
117
+ description?: string;
118
+ main?: string;
119
+ module?: string;
120
+ exports?: unknown;
121
+ dependencies?: Record<string, string>;
122
+ devDependencies?: Record<string, string>;
123
+ peerDependencies?: Record<string, string>;
124
+ workspaces?: string[] | { packages?: string[] };
125
+ pi?: { extensions?: string[] };
126
+ }
127
+
128
+ function readPackageJson(dir: string): PackageJson | null {
129
+ try {
130
+ const raw = fs.readFileSync(path.join(dir, "package.json"), "utf-8");
131
+ return JSON.parse(raw);
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: workspace package detection with manifest parsing
138
+ async function detectWorkspaceModules(
139
+ root: string,
140
+ rootManifest: PackageJson,
141
+ ): Promise<ModuleInfo[]> {
142
+ // Check for pnpm-workspace.yaml
143
+ let workspaceGlobs: string[] = [];
144
+
145
+ try {
146
+ const pnpmWs = fs.readFileSync(path.join(root, "pnpm-workspace.yaml"), "utf-8");
147
+ // Simple YAML parsing for packages array
148
+ const packagesMatch = pnpmWs.match(/packages:\s*\n((?:\s*-\s*.+\n?)*)/);
149
+ if (packagesMatch) {
150
+ workspaceGlobs = packagesMatch[1]
151
+ .split("\n")
152
+ .map((line) => line.replace(/^\s*-\s*/, "").replace(/['"\s]/g, ""))
153
+ .filter(Boolean);
154
+ }
155
+ } catch {
156
+ // Try npm/yarn workspaces from package.json
157
+ const ws = rootManifest.workspaces;
158
+ if (Array.isArray(ws)) {
159
+ workspaceGlobs = ws;
160
+ } else if (ws && Array.isArray(ws.packages)) {
161
+ workspaceGlobs = ws.packages;
162
+ }
163
+ }
164
+
165
+ if (workspaceGlobs.length === 0) return [];
166
+
167
+ // Resolve workspace globs to actual package directories
168
+ const modules: ModuleInfo[] = [];
169
+ const resolvedDirs = resolveWorkspaceGlobs(root, workspaceGlobs);
170
+
171
+ for (const dir of resolvedDirs) {
172
+ const manifest = readPackageJson(dir);
173
+ if (!manifest?.name) continue;
174
+
175
+ const relativePath = path.relative(root, dir);
176
+ const allDeps = {
177
+ ...manifest.dependencies,
178
+ ...manifest.peerDependencies,
179
+ };
180
+
181
+ // Separate internal vs external deps
182
+ const internalDeps: string[] = [];
183
+ const externalDeps: string[] = [];
184
+ for (const depName of Object.keys(allDeps)) {
185
+ // Internal if it matches a workspace package pattern
186
+ const depVersion = allDeps[depName];
187
+ if (depVersion?.startsWith("workspace:")) {
188
+ internalDeps.push(depName);
189
+ } else {
190
+ externalDeps.push(depName);
191
+ }
192
+ }
193
+
194
+ // Detect entrypoints
195
+ const entrypoints: string[] = [];
196
+ if (manifest.pi?.extensions) {
197
+ entrypoints.push(...manifest.pi.extensions);
198
+ } else if (manifest.main) {
199
+ entrypoints.push(manifest.main);
200
+ } else if (manifest.module) {
201
+ entrypoints.push(manifest.module);
202
+ }
203
+
204
+ modules.push({
205
+ name: manifest.name,
206
+ description: manifest.description ?? null,
207
+ root: dir,
208
+ relativePath,
209
+ entrypoints,
210
+ isLeaf: false, // Will be computed after edges
211
+ internalDeps,
212
+ externalDeps,
213
+ });
214
+ }
215
+
216
+ return modules;
217
+ }
218
+
219
+ function resolveWorkspaceGlobs(root: string, globs: string[]): string[] {
220
+ const dirs: string[] = [];
221
+
222
+ for (const glob of globs) {
223
+ if (glob.includes("*")) {
224
+ // Expand glob: "packages/*" -> list all dirs under packages/
225
+ const prefix = glob.replace(/\/?\*.*$/, "");
226
+ const baseDir = path.join(root, prefix);
227
+ const recursive = glob.includes("**");
228
+ try {
229
+ collectPackageDirs(baseDir, dirs, recursive ? 5 : 0);
230
+ } catch {
231
+ // Glob base directory doesn't exist
232
+ }
233
+ } else {
234
+ // Exact path
235
+ const fullPath = path.join(root, glob);
236
+ if (fs.existsSync(path.join(fullPath, "package.json"))) {
237
+ dirs.push(fullPath);
238
+ }
239
+ }
240
+ }
241
+
242
+ return dirs;
243
+ }
244
+
245
+ /**
246
+ * Collect directories containing a package.json.
247
+ * When depth > 0, recurse into subdirectories (for `**` globs).
248
+ */
249
+ function collectPackageDirs(baseDir: string, dirs: string[], depth: number): void {
250
+ let entries: fs.Dirent[];
251
+ try {
252
+ entries = fs.readdirSync(baseDir, { withFileTypes: true });
253
+ } catch {
254
+ return;
255
+ }
256
+ for (const entry of entries) {
257
+ if (!entry.isDirectory()) continue;
258
+ if (entry.name.startsWith(".")) continue;
259
+ if (entry.name === "node_modules") continue;
260
+ const fullPath = path.join(baseDir, entry.name);
261
+ if (fs.existsSync(path.join(fullPath, "package.json"))) {
262
+ dirs.push(fullPath);
263
+ } else if (depth > 0) {
264
+ collectPackageDirs(fullPath, dirs, depth - 1);
265
+ }
266
+ }
267
+ }
268
+
269
+ // ── Fallback models ───────────────────────────────────────────────────
270
+
271
+ function buildSinglePackageModel(root: string, manifest: PackageJson): ArchitectureModel {
272
+ const entrypoints: string[] = [];
273
+ if (manifest.pi?.extensions) {
274
+ entrypoints.push(...manifest.pi.extensions);
275
+ } else if (manifest.main) {
276
+ entrypoints.push(manifest.main);
277
+ }
278
+
279
+ const mod: ModuleInfo = {
280
+ name: manifest.name ?? path.basename(root),
281
+ description: manifest.description ?? null,
282
+ root,
283
+ relativePath: ".",
284
+ entrypoints,
285
+ isLeaf: true,
286
+ internalDeps: [],
287
+ externalDeps: Object.keys({
288
+ ...manifest.dependencies,
289
+ ...manifest.peerDependencies,
290
+ }),
291
+ };
292
+
293
+ return {
294
+ root,
295
+ name: manifest.name ?? null,
296
+ description: manifest.description ?? null,
297
+ modules: [mod],
298
+ edges: [],
299
+ };
300
+ }
301
+
302
+ function buildMinimalModel(root: string): ArchitectureModel | null {
303
+ // Check if there are any recognizable source files
304
+ let hasSource = false;
305
+ walkProject(root, 2, (_dir, entries) => {
306
+ for (const name of entries) {
307
+ if (
308
+ name.endsWith(".ts") ||
309
+ name.endsWith(".js") ||
310
+ name.endsWith(".tsx") ||
311
+ name.endsWith(".jsx") ||
312
+ name.endsWith(".py") ||
313
+ name.endsWith(".rs") ||
314
+ name.endsWith(".go")
315
+ ) {
316
+ hasSource = true;
317
+ }
318
+ }
319
+ });
320
+
321
+ if (!hasSource) return null;
322
+
323
+ return {
324
+ root,
325
+ name: path.basename(root),
326
+ description: null,
327
+ modules: [],
328
+ edges: [],
329
+ };
330
+ }
331
+
332
+ // ── Query helpers ─────────────────────────────────────────────────────
333
+
334
+ /**
335
+ * Find the module containing a given file path.
336
+ * Returns the most specific (deepest) matching module.
337
+ */
338
+ export function findModuleForPath(model: ArchitectureModel, filePath: string): ModuleInfo | null {
339
+ const resolved = path.resolve(filePath);
340
+ let best: ModuleInfo | null = null;
341
+
342
+ for (const mod of model.modules) {
343
+ if (isWithinOrEqual(mod.root, resolved)) {
344
+ if (!best || mod.root.length > best.root.length) {
345
+ best = mod;
346
+ }
347
+ }
348
+ }
349
+
350
+ return best;
351
+ }
352
+
353
+ /**
354
+ * Get modules that depend on a given module (reverse dependencies / dependents).
355
+ */
356
+ export function getDependents(model: ArchitectureModel, moduleName: string): ModuleInfo[] {
357
+ return model.modules.filter((m) => m.internalDeps.includes(moduleName));
358
+ }
359
+
360
+ /**
361
+ * Get the internal dependencies of a module as ModuleInfo objects.
362
+ */
363
+ export function getDependencies(model: ArchitectureModel, moduleName: string): ModuleInfo[] {
364
+ const mod = model.modules.find((m) => m.name === moduleName);
365
+ if (!mod) return [];
366
+ return model.modules.filter((m) => mod.internalDeps.includes(m.name));
367
+ }