kibi-plugin-builtin 0.2.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 (44) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/index.d.ts +34 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +72 -0
  5. package/dist/ontology/builtin-ontology-pack.d.ts +11 -0
  6. package/dist/ontology/builtin-ontology-pack.d.ts.map +1 -0
  7. package/dist/ontology/builtin-ontology-pack.js +78 -0
  8. package/dist/ontology/normalize.d.ts +7 -0
  9. package/dist/ontology/normalize.d.ts.map +1 -0
  10. package/dist/ontology/normalize.js +55 -0
  11. package/dist/ontology/predicate-rule.d.ts +26 -0
  12. package/dist/ontology/predicate-rule.d.ts.map +1 -0
  13. package/dist/ontology/predicate-rule.js +55 -0
  14. package/dist/ontology/predicate-rules-core.d.ts +94 -0
  15. package/dist/ontology/predicate-rules-core.d.ts.map +1 -0
  16. package/dist/ontology/predicate-rules-core.js +156 -0
  17. package/dist/ontology/predicate-rules-launcher.d.ts +58 -0
  18. package/dist/ontology/predicate-rules-launcher.d.ts.map +1 -0
  19. package/dist/ontology/predicate-rules-launcher.js +105 -0
  20. package/dist/ontology/predicate-rules-policy.d.ts +99 -0
  21. package/dist/ontology/predicate-rules-policy.d.ts.map +1 -0
  22. package/dist/ontology/predicate-rules-policy.js +167 -0
  23. package/dist/ontology/predicate-rules-product-tail.d.ts +57 -0
  24. package/dist/ontology/predicate-rules-product-tail.d.ts.map +1 -0
  25. package/dist/ontology/predicate-rules-product-tail.js +112 -0
  26. package/dist/ontology/predicate-rules-product.d.ts +84 -0
  27. package/dist/ontology/predicate-rules-product.d.ts.map +1 -0
  28. package/dist/ontology/predicate-rules-product.js +147 -0
  29. package/dist/semantic/builtin-classifier.d.ts +22 -0
  30. package/dist/semantic/builtin-classifier.d.ts.map +1 -0
  31. package/dist/semantic/builtin-classifier.js +134 -0
  32. package/dist/symbols/ts-morph-enrichment.d.ts +21 -0
  33. package/dist/symbols/ts-morph-enrichment.d.ts.map +1 -0
  34. package/dist/symbols/ts-morph-enrichment.js +371 -0
  35. package/dist/symbols/ts-morph-extractor.d.ts +8 -0
  36. package/dist/symbols/ts-morph-extractor.d.ts.map +1 -0
  37. package/dist/symbols/ts-morph-extractor.js +229 -0
  38. package/dist/symbols/ts-morph-granularity.d.ts +11 -0
  39. package/dist/symbols/ts-morph-granularity.d.ts.map +1 -0
  40. package/dist/symbols/ts-morph-granularity.js +98 -0
  41. package/dist/symbols/ts-morph-shared.d.ts +15 -0
  42. package/dist/symbols/ts-morph-shared.d.ts.map +1 -0
  43. package/dist/symbols/ts-morph-shared.js +71 -0
  44. package/package.json +42 -0
@@ -0,0 +1,371 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ import { access, readFile } from "node:fs/promises";
19
+ import * as path from "node:path";
20
+ import { Project, SyntaxKind, } from "ts-morph";
21
+ import { SUPPORTED_SOURCE_EXTENSIONS, onlyCandidate, } from "./ts-morph-shared.js";
22
+ // implements REQ-capability-plugin-builtin-parity-v1
23
+ export async function enrichSymbolCoordinatesWithTsMorph(entries, workspaceRoot) {
24
+ const project = new Project({
25
+ skipAddingFilesFromTsConfig: true,
26
+ });
27
+ const sourceFileCache = new Map();
28
+ const enriched = [];
29
+ for (const entry of entries) {
30
+ try {
31
+ const absolutePath = await resolveSourcePath(entry.sourceFile, workspaceRoot);
32
+ if (!absolutePath) {
33
+ enriched.push(entry);
34
+ continue;
35
+ }
36
+ const sourceFile = getOrAddSourceFile(project, sourceFileCache, absolutePath);
37
+ if (!sourceFile) {
38
+ enriched.push(await enrichWithTextFallback(entry, absolutePath));
39
+ continue;
40
+ }
41
+ const match = findNamedDeclaration(sourceFile, entry.title);
42
+ if (!match) {
43
+ enriched.push(entry);
44
+ continue;
45
+ }
46
+ const nameStart = match.getNameNode().getStart();
47
+ const end = match.node.getEnd();
48
+ const startLc = sourceFile.getLineAndColumnAtPos(nameStart);
49
+ const endLc = sourceFile.getLineAndColumnAtPos(end);
50
+ const coordinates = {
51
+ sourceLine: startLc.line,
52
+ sourceColumn: Math.max(0, startLc.column - 1),
53
+ sourceEndLine: endLc.line,
54
+ sourceEndColumn: Math.max(0, endLc.column - 1),
55
+ coordinatesGeneratedAt: new Date().toISOString(),
56
+ };
57
+ enriched.push({
58
+ ...entry,
59
+ ...coordinates,
60
+ });
61
+ }
62
+ catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ console.warn(`[kibi] Failed to enrich symbol coordinates for ${entry.id}: ${message}`);
65
+ const absolutePath = await resolveSourcePath(entry.sourceFile, workspaceRoot);
66
+ if (!absolutePath) {
67
+ enriched.push(entry);
68
+ continue;
69
+ }
70
+ enriched.push(await enrichWithTextFallback(entry, absolutePath));
71
+ }
72
+ }
73
+ return enriched;
74
+ }
75
+ async function resolveSourcePath(sourceFile, workspaceRoot) {
76
+ if (!sourceFile)
77
+ return null;
78
+ const absolute = path.isAbsolute(sourceFile)
79
+ ? sourceFile
80
+ : path.resolve(workspaceRoot, sourceFile);
81
+ const ext = path.extname(absolute).toLowerCase();
82
+ if (!SUPPORTED_SOURCE_EXTENSIONS.has(ext))
83
+ return null;
84
+ try {
85
+ await access(absolute);
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ return absolute;
91
+ }
92
+ function getOrAddSourceFile(project, cache, absolutePath) {
93
+ const cached = cache.get(absolutePath);
94
+ if (cached)
95
+ return cached;
96
+ try {
97
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
98
+ cache.set(absolutePath, sourceFile);
99
+ return sourceFile;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ async function enrichWithTextFallback(entry, absolutePath) {
106
+ try {
107
+ const content = await readFile(absolutePath, "utf8");
108
+ const lines = content.split(/\r?\n/);
109
+ const escapedTitle = entry.title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
+ const pattern = new RegExp(`\\b${escapedTitle}\\b`);
111
+ for (let index = 0; index < lines.length; index++) {
112
+ const line = lines[index];
113
+ if (!line)
114
+ continue;
115
+ const match = pattern.exec(line);
116
+ if (!match || match.index < 0)
117
+ continue;
118
+ return {
119
+ ...entry,
120
+ sourceLine: index + 1,
121
+ sourceColumn: match.index,
122
+ sourceEndLine: index + 1,
123
+ sourceEndColumn: match.index + entry.title.length,
124
+ coordinatesGeneratedAt: new Date().toISOString(),
125
+ };
126
+ }
127
+ return entry;
128
+ }
129
+ catch {
130
+ return entry;
131
+ }
132
+ }
133
+ function findNamedDeclaration(sourceFile, title) {
134
+ const qualifiedMethod = parseQualifiedMethodTitle(title);
135
+ if (qualifiedMethod) {
136
+ for (const cls of sourceFile.getClasses()) {
137
+ if (cls.getName() !== qualifiedMethod.className)
138
+ continue;
139
+ const match = findClassMember(cls, qualifiedMethod.methodName);
140
+ if (match)
141
+ return match;
142
+ }
143
+ for (const statement of sourceFile.getVariableStatements()) {
144
+ if (!statement.isExported())
145
+ continue;
146
+ for (const declaration of statement.getDeclarations()) {
147
+ if (declaration.getName() !== qualifiedMethod.className)
148
+ continue;
149
+ const classExpression = declaration.getInitializerIfKind(SyntaxKind.ClassExpression);
150
+ const match = classExpression
151
+ ? findClassMember(classExpression, qualifiedMethod.methodName)
152
+ : null;
153
+ if (match)
154
+ return match;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ const candidates = [];
160
+ for (const decl of sourceFile.getFunctions()) {
161
+ if (!decl.isExported())
162
+ continue;
163
+ if (decl.getName() !== title)
164
+ continue;
165
+ const nameNode = decl.getNameNode();
166
+ if (!nameNode)
167
+ continue;
168
+ candidates.push({ node: decl, getNameNode: () => nameNode });
169
+ }
170
+ for (const decl of sourceFile.getClasses()) {
171
+ if (!decl.isExported())
172
+ continue;
173
+ if (decl.getName() !== title)
174
+ continue;
175
+ const nameNode = decl.getNameNode();
176
+ if (!nameNode)
177
+ continue;
178
+ candidates.push({ node: decl, getNameNode: () => nameNode });
179
+ }
180
+ for (const decl of sourceFile.getInterfaces()) {
181
+ if (!decl.isExported())
182
+ continue;
183
+ if (decl.getName() !== title)
184
+ continue;
185
+ const nameNode = decl.getNameNode();
186
+ if (!nameNode)
187
+ continue;
188
+ candidates.push({ node: decl, getNameNode: () => nameNode });
189
+ }
190
+ for (const decl of sourceFile.getTypeAliases()) {
191
+ if (!decl.isExported())
192
+ continue;
193
+ if (decl.getName() !== title)
194
+ continue;
195
+ const nameNode = decl.getNameNode();
196
+ if (!nameNode)
197
+ continue;
198
+ candidates.push({ node: decl, getNameNode: () => nameNode });
199
+ }
200
+ for (const decl of sourceFile.getEnums()) {
201
+ if (!decl.isExported())
202
+ continue;
203
+ if (decl.getName() !== title)
204
+ continue;
205
+ const nameNode = decl.getNameNode();
206
+ if (!nameNode)
207
+ continue;
208
+ candidates.push({ node: decl, getNameNode: () => nameNode });
209
+ }
210
+ for (const statement of sourceFile.getVariableStatements()) {
211
+ if (!statement.isExported())
212
+ continue;
213
+ for (const declaration of statement.getDeclarations()) {
214
+ if (declaration.getName() !== title)
215
+ continue;
216
+ const nameNode = declaration.getNameNode();
217
+ candidates.push({ node: declaration, getNameNode: () => nameNode });
218
+ }
219
+ }
220
+ if (candidates.length === 0) {
221
+ const internalCandidates = [];
222
+ for (const decl of sourceFile.getFunctions()) {
223
+ if (decl.isExported())
224
+ continue;
225
+ if (decl.getName() !== title)
226
+ continue;
227
+ const nameNode = decl.getNameNode();
228
+ if (!nameNode)
229
+ continue;
230
+ internalCandidates.push({ node: decl, getNameNode: () => nameNode });
231
+ }
232
+ const uniqueInternal = onlyCandidate(internalCandidates);
233
+ if (uniqueInternal) {
234
+ return uniqueInternal;
235
+ }
236
+ const methodCandidates = [];
237
+ for (const cls of sourceFile.getClasses()) {
238
+ for (const method of cls.getMethods()) {
239
+ if (method.getName() !== title)
240
+ continue;
241
+ const nameNode = method.getNameNode();
242
+ if (!nameNode)
243
+ continue;
244
+ methodCandidates.push({ node: method, getNameNode: () => nameNode });
245
+ }
246
+ }
247
+ for (const statement of sourceFile.getVariableStatements()) {
248
+ if (!statement.isExported())
249
+ continue;
250
+ for (const declaration of statement.getDeclarations()) {
251
+ const classExpression = declaration.getInitializerIfKind(SyntaxKind.ClassExpression);
252
+ if (!classExpression)
253
+ continue;
254
+ for (const method of classExpression.getMethods()) {
255
+ if (method.getName() !== title)
256
+ continue;
257
+ const nameNode = method.getNameNode();
258
+ if (!nameNode)
259
+ continue;
260
+ methodCandidates.push({
261
+ node: method,
262
+ getNameNode: () => nameNode,
263
+ });
264
+ }
265
+ }
266
+ }
267
+ const uniqueMethod = onlyCandidate(methodCandidates);
268
+ if (uniqueMethod) {
269
+ return uniqueMethod;
270
+ }
271
+ const memberCandidates = [];
272
+ for (const cls of sourceFile.getClasses()) {
273
+ for (const property of cls.getProperties()) {
274
+ if (property.getName() !== title)
275
+ continue;
276
+ const nameNode = property.getNameNode();
277
+ if (!nameNode)
278
+ continue;
279
+ memberCandidates.push({ node: property, getNameNode: () => nameNode });
280
+ }
281
+ for (const accessor of [
282
+ ...cls.getGetAccessors(),
283
+ ...cls.getSetAccessors(),
284
+ ]) {
285
+ if (accessor.getName() !== title)
286
+ continue;
287
+ const nameNode = accessor.getNameNode();
288
+ if (!nameNode)
289
+ continue;
290
+ memberCandidates.push({ node: accessor, getNameNode: () => nameNode });
291
+ }
292
+ }
293
+ for (const statement of sourceFile.getVariableStatements()) {
294
+ if (!statement.isExported())
295
+ continue;
296
+ for (const declaration of statement.getDeclarations()) {
297
+ const classExpression = declaration.getInitializerIfKind(SyntaxKind.ClassExpression);
298
+ if (!classExpression)
299
+ continue;
300
+ for (const property of classExpression.getProperties()) {
301
+ if (property.getName() !== title)
302
+ continue;
303
+ const nameNode = property.getNameNode();
304
+ if (!nameNode)
305
+ continue;
306
+ memberCandidates.push({
307
+ node: property,
308
+ getNameNode: () => nameNode,
309
+ });
310
+ }
311
+ for (const accessor of [
312
+ ...classExpression.getGetAccessors(),
313
+ ...classExpression.getSetAccessors(),
314
+ ]) {
315
+ if (accessor.getName() !== title)
316
+ continue;
317
+ const nameNode = accessor.getNameNode();
318
+ if (!nameNode)
319
+ continue;
320
+ memberCandidates.push({
321
+ node: accessor,
322
+ getNameNode: () => nameNode,
323
+ });
324
+ }
325
+ }
326
+ }
327
+ const uniqueMember = onlyCandidate(memberCandidates);
328
+ if (uniqueMember) {
329
+ return uniqueMember;
330
+ }
331
+ return null;
332
+ }
333
+ candidates.sort((a, b) => a.getNameNode().getStart() - b.getNameNode().getStart());
334
+ return candidates[0] ?? null;
335
+ }
336
+ function findClassMember(declaration, name) {
337
+ for (const method of declaration.getMethods()) {
338
+ if (method.getName() !== name)
339
+ continue;
340
+ const nameNode = method.getNameNode();
341
+ if (nameNode)
342
+ return { node: method, getNameNode: () => nameNode };
343
+ }
344
+ for (const property of declaration.getProperties()) {
345
+ if (property.getName() !== name)
346
+ continue;
347
+ const nameNode = property.getNameNode();
348
+ if (nameNode)
349
+ return { node: property, getNameNode: () => nameNode };
350
+ }
351
+ for (const accessor of [
352
+ ...declaration.getGetAccessors(),
353
+ ...declaration.getSetAccessors(),
354
+ ]) {
355
+ if (accessor.getName() !== name)
356
+ continue;
357
+ const nameNode = accessor.getNameNode();
358
+ if (nameNode)
359
+ return { node: accessor, getNameNode: () => nameNode };
360
+ }
361
+ return null;
362
+ }
363
+ function parseQualifiedMethodTitle(title) {
364
+ const separatorIndex = title.lastIndexOf(".");
365
+ if (separatorIndex <= 0 || separatorIndex === title.length - 1)
366
+ return null;
367
+ return {
368
+ className: title.slice(0, separatorIndex),
369
+ methodName: title.slice(separatorIndex + 1),
370
+ };
371
+ }
@@ -0,0 +1,8 @@
1
+ import type { SourceAnalysisProvider, SymbolExtractorV1 } from "kibi-plugin-sdk";
2
+ /**
3
+ * Built-in ts-morph symbol extractor. Behavior matches the historical CLI
4
+ * `createTsMorphSourceAnalysisProvider` analysis path.
5
+ */
6
+ export declare function createBuiltinTsMorphSymbolExtractor(): SymbolExtractorV1;
7
+ export declare function createBuiltinTsMorphSourceAnalysisProvider(): SourceAnalysisProvider;
8
+ //# sourceMappingURL=ts-morph-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-morph-extractor.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-extractor.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,sBAAsB,EAItB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAmBzB;;;GAGG;AAEH,wBAAgB,mCAAmC,IAAI,iBAAiB,CA8BvE;AAGD,wBAAgB,0CAA0C,IAAI,sBAAsB,CAEnF"}
@@ -0,0 +1,229 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ import * as path from "node:path";
19
+ import { toSourceAnalysisProvider } from "kibi-plugin-sdk";
20
+ import { Project, SyntaxKind, } from "ts-morph";
21
+ import { SUPPORTED_SOURCE_EXTENSIONS, chooseScriptKind, formatMethodSymbolName, isPrivateClassMember, } from "./ts-morph-shared.js";
22
+ const EXTRACTOR_ID = "kibi-plugin-builtin.ts-morph";
23
+ /**
24
+ * Built-in ts-morph symbol extractor. Behavior matches the historical CLI
25
+ * `createTsMorphSourceAnalysisProvider` analysis path.
26
+ */
27
+ // implements REQ-capability-plugin-builtin-parity-v1
28
+ export function createBuiltinTsMorphSymbolExtractor() {
29
+ const project = new Project({
30
+ skipAddingFilesFromTsConfig: true,
31
+ });
32
+ return {
33
+ id: EXTRACTOR_ID,
34
+ supports(input) {
35
+ return SUPPORTED_SOURCE_EXTENSIONS.has(path.extname(input.path).toLowerCase());
36
+ },
37
+ analyze(input) {
38
+ const sourceFile = project.createSourceFile(input.path, input.content, {
39
+ overwrite: true,
40
+ scriptKind: chooseScriptKind(input.path),
41
+ });
42
+ return {
43
+ sourceFile: input.path,
44
+ language: inferSourceLanguage(input.path),
45
+ module: {
46
+ title: inferModuleTitle(input.path),
47
+ language: inferSourceLanguage(input.path),
48
+ analysisMode: "parser",
49
+ },
50
+ symbols: collectSourceSymbols(sourceFile),
51
+ };
52
+ },
53
+ };
54
+ }
55
+ // implements REQ-capability-plugin-builtin-parity-v1
56
+ export function createBuiltinTsMorphSourceAnalysisProvider() {
57
+ return toSourceAnalysisProvider(createBuiltinTsMorphSymbolExtractor());
58
+ }
59
+ function collectSourceSymbols(sourceFile) {
60
+ const symbols = [];
61
+ for (const decl of sourceFile.getFunctions()) {
62
+ if (!decl.isExported())
63
+ continue;
64
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "function", decl.getNameNode() ?? decl, decl,
65
+ // Functions: leading trivia via getFullText + JSDoc (historical CLI).
66
+ fullTextWithJsDocs(decl)));
67
+ }
68
+ for (const decl of sourceFile.getClasses()) {
69
+ if (!decl.isExported())
70
+ continue;
71
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "class", decl.getNameNode() ?? decl, decl,
72
+ // Leading comments + JSDoc only — never the class body (method
73
+ // `// implements` must stay on the method symbol).
74
+ leadingCommentsAndJsDocs(sourceFile, decl)));
75
+ try {
76
+ appendClassMembers(sourceFile, decl, decl.getName(), symbols);
77
+ }
78
+ catch {
79
+ // Skip malformed class member walks; keep the class symbol.
80
+ }
81
+ }
82
+ for (const decl of sourceFile.getInterfaces()) {
83
+ if (!decl.isExported())
84
+ continue;
85
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "interface", decl.getNameNode() ?? decl, decl,
86
+ // Historical CLI used getText(); keep calling it so characterization
87
+ // mocks that throw from getText still isolate the failure.
88
+ `${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}`));
89
+ }
90
+ for (const decl of sourceFile.getTypeAliases()) {
91
+ if (!decl.isExported())
92
+ continue;
93
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "type", decl.getNameNode() ?? decl, decl, `${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}`));
94
+ }
95
+ for (const decl of sourceFile.getEnums()) {
96
+ if (!decl.isExported())
97
+ continue;
98
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, decl.getName() ?? "<anonymous>", "enum", decl.getNameNode() ?? decl, decl,
99
+ // Enums historically used getText(); prefer leading comments when the
100
+ // real AST is available, else getText for test doubles.
101
+ `${leadingCommentsAndJsDocs(sourceFile, decl)}\n${safeGetText(decl)}`));
102
+ }
103
+ for (const statement of sourceFile.getVariableStatements()) {
104
+ if (!statement.isExported())
105
+ continue;
106
+ for (const declaration of statement.getDeclarations()) {
107
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, declaration.getName(), "variable", declaration.getNameNode() ?? declaration, declaration,
108
+ // Statement carries leading `// implements`; declaration text is the
109
+ // historical fallback when only getText is stubbed in tests.
110
+ `${safeFullText(statement)}\n${safeGetText(declaration)}`));
111
+ try {
112
+ const classExpression = typeof declaration.getInitializerIfKind === "function"
113
+ ? declaration.getInitializerIfKind(SyntaxKind.ClassExpression)
114
+ : undefined;
115
+ if (classExpression) {
116
+ appendClassMembers(sourceFile, classExpression, declaration.getName(), symbols);
117
+ }
118
+ }
119
+ catch {
120
+ // Skip malformed class-expression members; keep the variable symbol.
121
+ }
122
+ }
123
+ }
124
+ return symbols;
125
+ }
126
+ function jsDocsOnly(node) {
127
+ if (typeof node.getJsDocs !== "function")
128
+ return "";
129
+ return node
130
+ .getJsDocs()
131
+ .map((doc) => doc.getFullText())
132
+ .join("\n");
133
+ }
134
+ function leadingCommentsAndJsDocs(sourceFile, node) {
135
+ let leading = "";
136
+ try {
137
+ if (typeof node.getFullStart === "function" &&
138
+ typeof node.getStart === "function") {
139
+ const fullStart = node.getFullStart();
140
+ // Exclude JSDoc from the range so we don't double-count with jsDocsOnly.
141
+ const start = node.getStart(false);
142
+ if (Number.isFinite(fullStart) &&
143
+ Number.isFinite(start) &&
144
+ start > fullStart) {
145
+ leading = sourceFile.getFullText().slice(fullStart, start);
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ leading = "";
151
+ }
152
+ return `${leading}\n${jsDocsOnly(node)}`;
153
+ }
154
+ function fullTextWithJsDocs(node) {
155
+ return `${safeFullText(node)}\n${jsDocsOnly(node)}`;
156
+ }
157
+ function safeFullText(node) {
158
+ return typeof node.getFullText === "function" ? node.getFullText() : "";
159
+ }
160
+ function safeGetText(node) {
161
+ return typeof node.getText === "function" ? node.getText() : "";
162
+ }
163
+ function appendClassMembers(sourceFile, declaration, className, symbols) {
164
+ // Inherit class *leading* ownership comments onto members (not the class
165
+ // body). Method-local `// implements` stay on the method via getFullText.
166
+ const classLeading = leadingCommentsAndJsDocs(sourceFile, declaration);
167
+ const methods = typeof declaration.getMethods === "function"
168
+ ? declaration.getMethods()
169
+ : [];
170
+ for (const method of methods) {
171
+ if (isPrivateClassMember(method))
172
+ continue;
173
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, method.getName()), "method", method.getNameNode() ?? method, method, `${classLeading}\n${fullTextWithJsDocs(method)}`));
174
+ }
175
+ const properties = typeof declaration.getProperties === "function"
176
+ ? declaration.getProperties()
177
+ : [];
178
+ for (const property of properties) {
179
+ if (isPrivateClassMember(property))
180
+ continue;
181
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, property.getName()), "property", property.getNameNode() ?? property, property, `${classLeading}\n${fullTextWithJsDocs(property)}`));
182
+ }
183
+ const accessors = [
184
+ ...(typeof declaration.getGetAccessors === "function"
185
+ ? declaration.getGetAccessors()
186
+ : []),
187
+ ...(typeof declaration.getSetAccessors === "function"
188
+ ? declaration.getSetAccessors()
189
+ : []),
190
+ ];
191
+ for (const accessor of accessors) {
192
+ if (isPrivateClassMember(accessor))
193
+ continue;
194
+ pushSymbol(symbols, () => toSourceSymbolAnalysis(sourceFile, formatMethodSymbolName(className, accessor.getName()), "accessor", accessor.getNameNode() ?? accessor, accessor, `${classLeading}\n${fullTextWithJsDocs(accessor)}`));
195
+ }
196
+ }
197
+ function pushSymbol(symbols, build) {
198
+ try {
199
+ symbols.push(build());
200
+ }
201
+ catch {
202
+ // Skip malformed declarations; keep sibling symbols.
203
+ }
204
+ }
205
+ function toSourceSymbolAnalysis(sourceFile, name, kind, startNode, endNode, directiveText) {
206
+ const start = sourceFile.getLineAndColumnAtPos(startNode.getStart());
207
+ const end = sourceFile.getLineAndColumnAtPos(endNode.getEnd());
208
+ return {
209
+ name,
210
+ kind,
211
+ startLine: start.line,
212
+ startColumn: Math.max(0, start.column - 1),
213
+ endLine: end.line,
214
+ endColumn: Math.max(0, end.column - 1),
215
+ directiveText,
216
+ };
217
+ }
218
+ function inferSourceLanguage(filePath) {
219
+ const extension = path.extname(filePath).toLowerCase();
220
+ if ([".ts", ".tsx", ".mts", ".cts"].includes(extension)) {
221
+ return "typescript";
222
+ }
223
+ return "javascript";
224
+ }
225
+ function inferModuleTitle(filePath) {
226
+ const extension = path.extname(filePath);
227
+ const basename = path.basename(filePath, extension);
228
+ return basename.length > 0 ? basename : path.basename(filePath);
229
+ }
@@ -0,0 +1,11 @@
1
+ import type { SourceSymbolKind } from "kibi-plugin-sdk";
2
+ export interface GranularitySymbolCandidate {
3
+ readonly name: string;
4
+ readonly kind: SourceSymbolKind;
5
+ }
6
+ /**
7
+ * Collect exported (and unique bare method) symbol candidates used by
8
+ * coarse-vs-granular symbol validation.
9
+ */
10
+ export declare function collectGranularityCandidates(filePath: string, content: string): GranularitySymbolCandidate[];
11
+ //# sourceMappingURL=ts-morph-granularity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-morph-granularity.d.ts","sourceRoot":"","sources":["../../src/symbols/ts-morph-granularity.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKxD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC;AAED;;;GAGG;AAEH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,0BAA0B,EAAE,CA+D9B"}