septum 0.1.4 → 0.1.6

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 (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -105
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +216 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +213 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +104 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +50 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +220 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +50 -38
  17. package/src/cli/templates/skill.ts +65 -69
  18. package/src/core/boundary/evaluator.ts +429 -210
  19. package/src/core/config/loader.ts +117 -97
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -44
  22. package/src/core/database/repositories/dependency.repository.ts +552 -349
  23. package/src/core/database/repositories/domain.repository.ts +275 -198
  24. package/src/core/database/repositories/file.repository.ts +117 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +482 -443
  27. package/src/core/database/repository.ts +281 -260
  28. package/src/core/database/schema.ts +154 -153
  29. package/src/core/discovery/topology-detector.ts +486 -486
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +606 -535
  32. package/src/core/parser/boundary-tracker.ts +433 -288
  33. package/src/core/parser/extractors/base.ts +62 -62
  34. package/src/core/parser/extractors/blade.ts +158 -158
  35. package/src/core/parser/extractors/frontend.ts +147 -194
  36. package/src/core/parser/extractors/go.ts +408 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
  40. package/src/core/parser/extractors/php.ts +415 -153
  41. package/src/core/parser/extractors/python.ts +460 -228
  42. package/src/core/parser/extractors/registry.ts +70 -70
  43. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  44. package/src/core/parser/extractors/typescript.ts +668 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/parser/wasm-loader.ts +271 -0
  47. package/src/core/resolver/call-graph-tracer.ts +698 -668
  48. package/src/core/resolver/module-resolver.ts +479 -342
  49. package/src/core/resolver/path-resolver.ts +147 -147
  50. package/src/core/resolver/symbol-locator.ts +543 -543
  51. package/src/core/resolver/vertical-slice-tracer.ts +323 -285
  52. package/src/core/session/session-manager.ts +56 -56
  53. package/src/core/telemetry/telemetry.ts +86 -86
  54. package/src/core/topology/environment-detector.ts +109 -6
  55. package/src/index.ts +21 -21
  56. package/src/mcp/schemas.ts +695 -286
  57. package/src/mcp/server.ts +927 -716
  58. package/src/mcp/tools/check-boundary.ts +307 -152
  59. package/src/mcp/tools/get-domain-catalog.ts +155 -121
  60. package/src/mcp/tools/get-environment-topology.ts +3 -6
  61. package/src/mcp/tools/get-feature-context.ts +227 -116
  62. package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
  63. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  64. package/src/mcp/tools/get-symbol.ts +65 -63
  65. package/src/mcp/tools/locate-symbol.ts +169 -190
  66. package/src/mcp/tools/register-domain.ts +59 -64
  67. package/src/mcp/tools/register-feature.ts +91 -0
  68. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  69. package/src/types/index.ts +366 -315
  70. package/src/version.ts +3 -3
@@ -1,288 +1,433 @@
1
- /**
2
- * Utility to find the end line (1-based index) of a code block.
3
- * Handles brace matching with string and comment awareness for C-like languages (TS, JS, PHP, Go).
4
- */
5
- export function findBraceBlockEnd(lines: string[], startLineIndex: number): number {
6
- let depth = 0;
7
- let hasOpened = false;
8
- let inMultiLineComment = false;
9
-
10
- for (let i = startLineIndex; i < lines.length; i++) {
11
- const line = lines[i];
12
- let inSingleQuote = false;
13
- let inDoubleQuote = false;
14
- let inTemplateString = false;
15
-
16
- for (let j = 0; j < line.length; j++) {
17
- const char = line[j];
18
- const prevChar = j > 0 ? line[j - 1] : "";
19
- const nextChar = j + 1 < line.length ? line[j + 1] : "";
20
-
21
- // Multi-line comment boundary
22
- if (inMultiLineComment) {
23
- if (char === "*" && nextChar === "/") {
24
- inMultiLineComment = false;
25
- j++; // skip '/'
26
- }
27
- continue;
28
- }
29
-
30
- // Check start of multi-line comment /*
31
- if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
32
- inMultiLineComment = true;
33
- j++;
34
- continue;
35
- }
36
-
37
- // Check single-line comment //
38
- if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
39
- break; // ignore rest of line
40
- }
41
-
42
- // String quote handling (with escape character check)
43
- if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
44
- inSingleQuote = !inSingleQuote;
45
- continue;
46
- }
47
- if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
48
- inDoubleQuote = !inDoubleQuote;
49
- continue;
50
- }
51
- if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
52
- inTemplateString = !inTemplateString;
53
- continue;
54
- }
55
-
56
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
57
- continue;
58
- }
59
-
60
- // Brace tracking
61
- if (char === "{") {
62
- depth++;
63
- hasOpened = true;
64
- } else if (char === "}") {
65
- if (hasOpened) {
66
- depth--;
67
- if (depth === 0) {
68
- return i + 1; // 1-based line number
69
- }
70
- }
71
- }
72
- }
73
-
74
- // Safety guard: If we haven't seen an opening brace within 40 lines or hit a declaration-only statement
75
- if (!hasOpened) {
76
- const trimmed = line.trim();
77
- if (trimmed.endsWith(";") && i > startLineIndex) {
78
- return i + 1;
79
- }
80
- if (i - startLineIndex > 40) {
81
- return startLineIndex + 1;
82
- }
83
- }
84
- }
85
-
86
- // Fallback: If braces were unclosed or not found, return start line
87
- return startLineIndex + 1;
88
- }
89
-
90
- /**
91
- * Utility to find the end line (1-based index) of an indentation-based block (Python).
92
- */
93
- export function findPythonBlockEnd(lines: string[], startLineIndex: number): number {
94
- if (startLineIndex >= lines.length) return startLineIndex + 1;
95
-
96
- const startLine = lines[startLineIndex];
97
- const baseIndentMatch = startLine.match(/^(\s*)/);
98
- const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
99
-
100
- let lastContentLine = startLineIndex + 1;
101
- let bodyStarted = false;
102
-
103
- for (let i = startLineIndex + 1; i < lines.length; i++) {
104
- const line = lines[i];
105
- const trimmed = line.trim();
106
-
107
- // Skip empty lines or pure comment lines
108
- if (!trimmed || trimmed.startsWith("#")) {
109
- continue;
110
- }
111
-
112
- const currentIndentMatch = line.match(/^(\s*)/);
113
- const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
114
-
115
- if (currentIndent > baseIndent) {
116
- bodyStarted = true;
117
- lastContentLine = i + 1;
118
- } else {
119
- // Returned to same or lesser indentation -> block ended
120
- if (bodyStarted) {
121
- return lastContentLine;
122
- }
123
- break;
124
- }
125
- }
126
-
127
- return lastContentLine;
128
- }
129
-
130
- /**
131
- * Calculates the maximum control flow nesting depth within a code block (PHP, TS, JS, Go).
132
- * Base depth is 1 (the function body itself).
133
- * Nested control structures (if, else if, for, foreach, while, do, switch, try, catch)
134
- * that open a block increase the active depth.
135
- */
136
- export function calculateControlFlowNesting(
137
- lines: string[],
138
- startLineIndex: number,
139
- endLineIndex: number
140
- ): number {
141
- if (startLineIndex >= endLineIndex) return 1;
142
-
143
- let maxDepth = 1;
144
- let currentDepth = 1;
145
- let inMultiLineComment = false;
146
- let insideBody = false;
147
-
148
- const controlStack: number[] = [];
149
- let braceDepth = 0;
150
- let pendingControl = false;
151
-
152
- const controlRegex = /\b(if|else\s+if|else|for|foreach|while|do|switch|try|catch)\b/;
153
-
154
- const startIdx = Math.max(0, startLineIndex);
155
- const endIdx = Math.min(lines.length - 1, endLineIndex);
156
-
157
- for (let i = startIdx; i <= endIdx; i++) {
158
- const rawLine = lines[i];
159
-
160
- let cleanLine = "";
161
- let inSingleQuote = false;
162
- let inDoubleQuote = false;
163
- let inTemplateString = false;
164
-
165
- for (let j = 0; j < rawLine.length; j++) {
166
- const char = rawLine[j];
167
- const prevChar = j > 0 ? rawLine[j - 1] : "";
168
- const nextChar = j + 1 < rawLine.length ? rawLine[j + 1] : "";
169
-
170
- if (inMultiLineComment) {
171
- if (char === "*" && nextChar === "/") {
172
- inMultiLineComment = false;
173
- j++;
174
- }
175
- continue;
176
- }
177
-
178
- if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
179
- inMultiLineComment = true;
180
- j++;
181
- continue;
182
- }
183
-
184
- if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
185
- break; // Single line comment: ignore rest of line
186
- }
187
-
188
- if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
189
- inSingleQuote = !inSingleQuote;
190
- continue;
191
- }
192
- if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
193
- inDoubleQuote = !inDoubleQuote;
194
- continue;
195
- }
196
- if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
197
- inTemplateString = !inTemplateString;
198
- continue;
199
- }
200
-
201
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
202
- continue;
203
- }
204
-
205
- cleanLine += char;
206
- }
207
-
208
- const trimmedClean = cleanLine.trim();
209
- if (!trimmedClean) continue;
210
-
211
- if (insideBody && controlRegex.test(trimmedClean)) {
212
- pendingControl = true;
213
- }
214
-
215
- for (let j = 0; j < cleanLine.length; j++) {
216
- const char = cleanLine[j];
217
- if (char === "{") {
218
- braceDepth++;
219
- if (!insideBody) {
220
- insideBody = true;
221
- } else if (pendingControl) {
222
- currentDepth++;
223
- controlStack.push(braceDepth);
224
- if (currentDepth > maxDepth) {
225
- maxDepth = currentDepth;
226
- }
227
- pendingControl = false;
228
- }
229
- } else if (char === "}") {
230
- if (controlStack.length > 0 && controlStack[controlStack.length - 1] === braceDepth) {
231
- controlStack.pop();
232
- currentDepth = Math.max(1, currentDepth - 1);
233
- }
234
- braceDepth = Math.max(0, braceDepth - 1);
235
- }
236
- }
237
-
238
- if (trimmedClean.endsWith(";")) {
239
- pendingControl = false;
240
- }
241
- }
242
-
243
- return maxDepth;
244
- }
245
-
246
- /**
247
- * Calculates the maximum control flow nesting depth in indentation-based languages (Python).
248
- */
249
- export function calculatePythonNesting(
250
- lines: string[],
251
- startLineIndex: number,
252
- endLineIndex: number
253
- ): number {
254
- if (startLineIndex >= endLineIndex) return 1;
255
-
256
- const startLine = lines[startLineIndex];
257
- const baseIndentMatch = startLine.match(/^(\s*)/);
258
- const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
259
-
260
- const controlRegex = /^\s*(if|elif|else|for|while|try|except|finally|match|case)\b/;
261
- const indentStack: number[] = [baseIndent];
262
- let maxDepth = 1;
263
-
264
- const startIdx = Math.max(0, startLineIndex + 1);
265
- const endIdx = Math.min(lines.length - 1, endLineIndex);
266
-
267
- for (let i = startIdx; i <= endIdx; i++) {
268
- const line = lines[i];
269
- const trimmed = line.trim();
270
- if (!trimmed || trimmed.startsWith("#")) continue;
271
-
272
- const currentIndentMatch = line.match(/^(\s*)/);
273
- const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
274
-
275
- while (indentStack.length > 1 && currentIndent <= indentStack[indentStack.length - 1]) {
276
- indentStack.pop();
277
- }
278
-
279
- if (controlRegex.test(line)) {
280
- indentStack.push(currentIndent);
281
- if (indentStack.length > maxDepth) {
282
- maxDepth = indentStack.length;
283
- }
284
- }
285
- }
286
-
287
- return maxDepth;
288
- }
1
+ /**
2
+ * Utility to find the end line (1-based index) of a code block.
3
+ * Handles brace matching with string and comment awareness for C-like languages (TS, JS, PHP, Go).
4
+ */
5
+ export function findBraceBlockEnd(lines: string[], startLineIndex: number): number {
6
+ let depth = 0;
7
+ let hasOpened = false;
8
+ let inMultiLineComment = false;
9
+
10
+ for (let i = startLineIndex; i < lines.length; i++) {
11
+ const line = lines[i];
12
+ let inSingleQuote = false;
13
+ let inDoubleQuote = false;
14
+ let inTemplateString = false;
15
+
16
+ for (let j = 0; j < line.length; j++) {
17
+ const char = line[j];
18
+ const prevChar = j > 0 ? line[j - 1] : "";
19
+ const nextChar = j + 1 < line.length ? line[j + 1] : "";
20
+
21
+ // Multi-line comment boundary
22
+ if (inMultiLineComment) {
23
+ if (char === "*" && nextChar === "/") {
24
+ inMultiLineComment = false;
25
+ j++; // skip '/'
26
+ }
27
+ continue;
28
+ }
29
+
30
+ // Check start of multi-line comment /*
31
+ if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
32
+ inMultiLineComment = true;
33
+ j++;
34
+ continue;
35
+ }
36
+
37
+ // Check single-line comment //
38
+ if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
39
+ break; // ignore rest of line
40
+ }
41
+
42
+ // String quote handling (with escape character check)
43
+ if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
44
+ inSingleQuote = !inSingleQuote;
45
+ continue;
46
+ }
47
+ if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
48
+ inDoubleQuote = !inDoubleQuote;
49
+ continue;
50
+ }
51
+ if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
52
+ inTemplateString = !inTemplateString;
53
+ continue;
54
+ }
55
+
56
+ if (inSingleQuote || inDoubleQuote || inTemplateString) {
57
+ continue;
58
+ }
59
+
60
+ // Brace tracking
61
+ if (char === "{") {
62
+ depth++;
63
+ hasOpened = true;
64
+ } else if (char === "}") {
65
+ if (hasOpened) {
66
+ depth--;
67
+ if (depth === 0) {
68
+ return i + 1; // 1-based line number
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // Safety guard: If we haven't seen an opening brace within 40 lines or hit a declaration-only statement
75
+ if (!hasOpened) {
76
+ const trimmed = line.trim();
77
+ if (trimmed.endsWith(";") && i > startLineIndex) {
78
+ return i + 1;
79
+ }
80
+ if (i - startLineIndex > 40) {
81
+ return startLineIndex + 1;
82
+ }
83
+ }
84
+ }
85
+
86
+ // Fallback: If braces were unclosed or not found, return start line
87
+ return startLineIndex + 1;
88
+ }
89
+
90
+ /**
91
+ * Utility to find the end line (1-based index) of an indentation-based block (Python).
92
+ */
93
+ export function findPythonBlockEnd(lines: string[], startLineIndex: number): number {
94
+ if (startLineIndex >= lines.length) return startLineIndex + 1;
95
+
96
+ const startLine = lines[startLineIndex];
97
+ const baseIndentMatch = startLine.match(/^(\s*)/);
98
+ const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
99
+
100
+ let lastContentLine = startLineIndex + 1;
101
+ let bodyStarted = false;
102
+
103
+ for (let i = startLineIndex + 1; i < lines.length; i++) {
104
+ const line = lines[i];
105
+ const trimmed = line.trim();
106
+
107
+ // Skip empty lines or pure comment lines
108
+ if (!trimmed || trimmed.startsWith("#")) {
109
+ continue;
110
+ }
111
+
112
+ const currentIndentMatch = line.match(/^(\s*)/);
113
+ const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
114
+
115
+ if (currentIndent > baseIndent) {
116
+ bodyStarted = true;
117
+ lastContentLine = i + 1;
118
+ } else {
119
+ // Returned to same or lesser indentation -> block ended
120
+ if (bodyStarted) {
121
+ return lastContentLine;
122
+ }
123
+ break;
124
+ }
125
+ }
126
+
127
+ return lastContentLine;
128
+ }
129
+
130
+ /**
131
+ * Calculates the maximum control flow nesting depth within a code block (PHP, TS, JS, Go).
132
+ * Base depth is 1 (the function body itself).
133
+ * Nested control structures (if, else if, for, foreach, while, do, switch, try, catch)
134
+ * that open a block increase the active depth.
135
+ */
136
+ export function calculateControlFlowNesting(
137
+ lines: string[],
138
+ startLineIndex: number,
139
+ endLineIndex: number
140
+ ): number {
141
+ if (startLineIndex >= endLineIndex) return 1;
142
+
143
+ let maxDepth = 1;
144
+ let currentDepth = 1;
145
+ let inMultiLineComment = false;
146
+ let insideBody = false;
147
+
148
+ const controlStack: number[] = [];
149
+ let braceDepth = 0;
150
+ let pendingControl = false;
151
+
152
+ const controlRegex = /\b(if|else\s+if|else|for|foreach|while|do|switch|try|catch)\b/;
153
+
154
+ const startIdx = Math.max(0, startLineIndex);
155
+ const endIdx = Math.min(lines.length - 1, endLineIndex);
156
+
157
+ for (let i = startIdx; i <= endIdx; i++) {
158
+ const rawLine = lines[i];
159
+
160
+ let cleanLine = "";
161
+ let inSingleQuote = false;
162
+ let inDoubleQuote = false;
163
+ let inTemplateString = false;
164
+
165
+ for (let j = 0; j < rawLine.length; j++) {
166
+ const char = rawLine[j];
167
+ const prevChar = j > 0 ? rawLine[j - 1] : "";
168
+ const nextChar = j + 1 < rawLine.length ? rawLine[j + 1] : "";
169
+
170
+ if (inMultiLineComment) {
171
+ if (char === "*" && nextChar === "/") {
172
+ inMultiLineComment = false;
173
+ j++;
174
+ }
175
+ continue;
176
+ }
177
+
178
+ if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
179
+ inMultiLineComment = true;
180
+ j++;
181
+ continue;
182
+ }
183
+
184
+ if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
185
+ break; // Single line comment: ignore rest of line
186
+ }
187
+
188
+ if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
189
+ inSingleQuote = !inSingleQuote;
190
+ continue;
191
+ }
192
+ if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
193
+ inDoubleQuote = !inDoubleQuote;
194
+ continue;
195
+ }
196
+ if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
197
+ inTemplateString = !inTemplateString;
198
+ continue;
199
+ }
200
+
201
+ if (inSingleQuote || inDoubleQuote || inTemplateString) {
202
+ continue;
203
+ }
204
+
205
+ cleanLine += char;
206
+ }
207
+
208
+ const trimmedClean = cleanLine.trim();
209
+ if (!trimmedClean) continue;
210
+
211
+ if (insideBody && controlRegex.test(trimmedClean)) {
212
+ pendingControl = true;
213
+ }
214
+
215
+ for (let j = 0; j < cleanLine.length; j++) {
216
+ const char = cleanLine[j];
217
+ if (char === "{") {
218
+ braceDepth++;
219
+ if (!insideBody) {
220
+ insideBody = true;
221
+ } else if (pendingControl) {
222
+ currentDepth++;
223
+ controlStack.push(braceDepth);
224
+ if (currentDepth > maxDepth) {
225
+ maxDepth = currentDepth;
226
+ }
227
+ pendingControl = false;
228
+ }
229
+ } else if (char === "}") {
230
+ if (controlStack.length > 0 && controlStack[controlStack.length - 1] === braceDepth) {
231
+ controlStack.pop();
232
+ currentDepth = Math.max(1, currentDepth - 1);
233
+ }
234
+ braceDepth = Math.max(0, braceDepth - 1);
235
+ }
236
+ }
237
+
238
+ if (trimmedClean.endsWith(";")) {
239
+ pendingControl = false;
240
+ }
241
+ }
242
+
243
+ return maxDepth;
244
+ }
245
+
246
+ /**
247
+ * Calculates the maximum control flow nesting depth in indentation-based languages (Python).
248
+ */
249
+ export function calculatePythonNesting(
250
+ lines: string[],
251
+ startLineIndex: number,
252
+ endLineIndex: number
253
+ ): number {
254
+ if (startLineIndex >= endLineIndex) return 1;
255
+
256
+ const startLine = lines[startLineIndex];
257
+ const baseIndentMatch = startLine.match(/^(\s*)/);
258
+ const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
259
+
260
+ const controlRegex = /^\s*(if|elif|else|for|while|try|except|finally|match|case)\b/;
261
+ const indentStack: number[] = [baseIndent];
262
+ let maxDepth = 1;
263
+
264
+ const startIdx = Math.max(0, startLineIndex + 1);
265
+ const endIdx = Math.min(lines.length - 1, endLineIndex);
266
+
267
+ for (let i = startIdx; i <= endIdx; i++) {
268
+ const line = lines[i];
269
+ const trimmed = line.trim();
270
+ if (!trimmed || trimmed.startsWith("#")) continue;
271
+
272
+ const currentIndentMatch = line.match(/^(\s*)/);
273
+ const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
274
+
275
+ while (indentStack.length > 1 && currentIndent <= indentStack[indentStack.length - 1]) {
276
+ indentStack.pop();
277
+ }
278
+
279
+ if (controlRegex.test(line)) {
280
+ indentStack.push(currentIndent);
281
+ if (indentStack.length > maxDepth) {
282
+ maxDepth = indentStack.length;
283
+ }
284
+ }
285
+ }
286
+
287
+ return maxDepth;
288
+ }
289
+
290
+ /**
291
+ * Replaces the contents of comments and string/template literals with spaces while
292
+ * preserving exact character offsets, outer quote delimiters, and line breaks.
293
+ */
294
+ export function blankCommentsAndStrings(
295
+ content: string,
296
+ options?: { hashComments?: boolean }
297
+ ): string {
298
+ const hashComments = options?.hashComments ?? false;
299
+ const out = content.split("");
300
+ const len = out.length;
301
+
302
+ let i = 0;
303
+ let inSingle = false;
304
+ let inDouble = false;
305
+ let inTemplate = false;
306
+ let inLineComment = false;
307
+ let inBlockComment = false;
308
+
309
+ while (i < len) {
310
+ const ch = out[i];
311
+ const next = i + 1 < len ? out[i + 1] : "";
312
+
313
+ if (inLineComment) {
314
+ if (ch === "\n") {
315
+ inLineComment = false;
316
+ } else if (ch !== "\r") {
317
+ out[i] = " ";
318
+ }
319
+ i++;
320
+ continue;
321
+ }
322
+
323
+ if (inBlockComment) {
324
+ if (ch === "*" && next === "/") {
325
+ out[i] = " ";
326
+ out[i + 1] = " ";
327
+ inBlockComment = false;
328
+ i += 2;
329
+ continue;
330
+ }
331
+ if (ch !== "\n" && ch !== "\r") {
332
+ out[i] = " ";
333
+ }
334
+ i++;
335
+ continue;
336
+ }
337
+
338
+ if (inSingle) {
339
+ if (ch === "\\" && i + 1 < len) {
340
+ if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
341
+ if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
342
+ i += 2;
343
+ continue;
344
+ }
345
+ if (ch === "'") {
346
+ inSingle = false;
347
+ } else if (ch !== "\n" && ch !== "\r") {
348
+ out[i] = " ";
349
+ }
350
+ i++;
351
+ continue;
352
+ }
353
+
354
+ if (inDouble) {
355
+ if (ch === "\\" && i + 1 < len) {
356
+ if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
357
+ if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
358
+ i += 2;
359
+ continue;
360
+ }
361
+ if (ch === '"') {
362
+ inDouble = false;
363
+ } else if (ch !== "\n" && ch !== "\r") {
364
+ out[i] = " ";
365
+ }
366
+ i++;
367
+ continue;
368
+ }
369
+
370
+ if (inTemplate) {
371
+ if (ch === "\\" && i + 1 < len) {
372
+ if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
373
+ if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
374
+ i += 2;
375
+ continue;
376
+ }
377
+ if (ch === "`") {
378
+ inTemplate = false;
379
+ } else if (ch !== "\n" && ch !== "\r") {
380
+ out[i] = " ";
381
+ }
382
+ i++;
383
+ continue;
384
+ }
385
+
386
+ // Normal code state
387
+ if (ch === "/" && next === "/") {
388
+ out[i] = " ";
389
+ out[i + 1] = " ";
390
+ inLineComment = true;
391
+ i += 2;
392
+ continue;
393
+ }
394
+
395
+ if (ch === "/" && next === "*") {
396
+ out[i] = " ";
397
+ out[i + 1] = " ";
398
+ inBlockComment = true;
399
+ i += 2;
400
+ continue;
401
+ }
402
+
403
+ if (hashComments && ch === "#") {
404
+ out[i] = " ";
405
+ inLineComment = true;
406
+ i++;
407
+ continue;
408
+ }
409
+
410
+ if (ch === "'") {
411
+ inSingle = true;
412
+ i++;
413
+ continue;
414
+ }
415
+
416
+ if (ch === '"') {
417
+ inDouble = true;
418
+ i++;
419
+ continue;
420
+ }
421
+
422
+ if (ch === "`") {
423
+ inTemplate = true;
424
+ i++;
425
+ continue;
426
+ }
427
+
428
+ i++;
429
+ }
430
+
431
+ return out.join("");
432
+ }
433
+