kirograph 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/bin/commands/data.js +52 -0
- package/dist/bin/commands/data.js.map +2 -2
- package/dist/bin/commands/docs.js +2 -1
- package/dist/bin/commands/docs.js.map +2 -2
- package/dist/bin/commands/gain.js +21 -1
- package/dist/bin/commands/gain.js.map +2 -2
- package/dist/bin/commands/status.js +23 -1
- package/dist/bin/commands/status.js.map +2 -2
- package/dist/bin/installer/config-prompt.js +30 -1
- package/dist/bin/installer/config-prompt.js.map +2 -2
- package/dist/bin/installer/index.js +137 -0
- package/dist/bin/installer/index.js.map +2 -2
- package/dist/bin/kirograph.js +1 -1
- package/dist/config.js +19 -2
- package/dist/config.js.map +2 -2
- package/dist/core/pipeline.js +3 -1
- package/dist/core/pipeline.js.map +2 -2
- package/dist/data/indexer.js +4 -4
- package/dist/data/indexer.js.map +2 -2
- package/dist/data/linker.js +9 -5
- package/dist/data/linker.js.map +2 -2
- package/dist/data/linker.test.js +60 -0
- package/dist/data/linker.test.js.map +7 -0
- package/dist/data/parsers/__fixtures__/pdf-inspector-stub.js +41 -0
- package/dist/data/parsers/__fixtures__/pdf-inspector-stub.js.map +7 -0
- package/dist/data/parsers/index.js +3 -1
- package/dist/data/parsers/index.js.map +2 -2
- package/dist/data/parsers/pdf.js +95 -0
- package/dist/data/parsers/pdf.js.map +7 -0
- package/dist/data/parsers/pdf.test.js +125 -0
- package/dist/data/parsers/pdf.test.js.map +7 -0
- package/dist/data/queries.js +39 -2
- package/dist/data/queries.js.map +2 -2
- package/dist/data/types.js.map +1 -1
- package/dist/db/data-schema.sql +2 -1
- package/dist/db/database.js +7 -0
- package/dist/db/database.js.map +2 -2
- package/dist/docs/indexer.js +2 -1
- package/dist/docs/indexer.js.map +2 -2
- package/dist/docs/queries.js +2 -1
- package/dist/docs/queries.js.map +2 -2
- package/dist/docs/vectors.js +46 -5
- package/dist/docs/vectors.js.map +2 -2
- package/dist/extraction/extractor.js +26 -3
- package/dist/extraction/extractor.js.map +2 -2
- package/dist/extraction/grammars.js +1 -0
- package/dist/extraction/grammars.js.map +2 -2
- package/dist/extraction/languages.js +5 -1
- package/dist/extraction/languages.js.map +2 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +2 -2
- package/dist/mcp/tools.js +1 -1
- package/dist/mcp/tools.js.map +2 -2
- package/dist/memory/index.js +2 -1
- package/dist/memory/index.js.map +2 -2
- package/dist/memory/vectors.js +52 -8
- package/dist/memory/vectors.js.map +3 -3
- package/dist/patterns/library/code-eval-elixir.yaml +11 -0
- package/dist/patterns/library/command-injection-c.yaml +11 -0
- package/dist/patterns/library/command-injection-go.yaml +21 -3
- package/dist/patterns/library/command-injection-kotlin.yaml +10 -0
- package/dist/patterns/library/command-injection-php.yaml +11 -0
- package/dist/patterns/library/command-injection-ruby.yaml +11 -0
- package/dist/patterns/library/command-injection-rust.yaml +11 -0
- package/dist/patterns/library/dangerous-eval-bash.yaml +11 -0
- package/dist/patterns/library/dangerous-eval-php.yaml +11 -0
- package/dist/patterns/library/dangerous-eval-ruby.yaml +11 -0
- package/dist/patterns/library/dynamic-load-lua.yaml +10 -0
- package/dist/patterns/library/os-cmd-elixir.yaml +16 -0
- package/dist/patterns/library/os-execute-lua.yaml +10 -0
- package/dist/patterns/library/path-traversal-go.yaml +4 -4
- package/dist/patterns/library/process-launch-swift.yaml +11 -0
- package/dist/patterns/library/process-run-dart.yaml +15 -0
- package/dist/patterns/library/source-injection-bash.yaml +11 -0
- package/dist/patterns/library/sql-injection-dart.yaml +9 -0
- package/dist/patterns/library/sql-injection-kotlin.yaml +11 -0
- package/dist/patterns/library/sql-injection-php.yaml +9 -0
- package/dist/patterns/library/sql-injection-swift.yaml +11 -0
- package/dist/patterns/library/subshell-injection-ruby.yaml +8 -0
- package/dist/patterns/library/unsafe-block-rust.yaml +8 -0
- package/dist/patterns/library/unsafe-string-c.yaml +11 -0
- package/dist/patterns/runner.js +45 -27
- package/dist/patterns/runner.js.map +2 -2
- package/dist/security/integrator.js +8 -8
- package/dist/security/integrator.js.map +2 -2
- package/dist/types.js.map +2 -2
- package/dist/vectors/index.js +66 -3
- package/dist/vectors/index.js.map +2 -2
- package/dist/vectors/turboquant-index.js +261 -0
- package/dist/vectors/turboquant-index.js.map +7 -0
- package/dist/vectors/turbovec-index.js +189 -0
- package/dist/vectors/turbovec-index.js.map +7 -0
- package/package.json +26 -1
package/dist/patterns/runner.js
CHANGED
|
@@ -33,43 +33,56 @@ __export(runner_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(runner_exports);
|
|
34
34
|
var import_types = require("./types");
|
|
35
35
|
var import_errors = require("../errors");
|
|
36
|
-
const
|
|
36
|
+
const NAPI_BUILTIN_LANGUAGES = /* @__PURE__ */ new Set([
|
|
37
|
+
"html",
|
|
37
38
|
"javascript",
|
|
39
|
+
"jsx",
|
|
38
40
|
"typescript",
|
|
39
41
|
"tsx",
|
|
40
|
-
"
|
|
41
|
-
"python",
|
|
42
|
-
"go",
|
|
43
|
-
"rust",
|
|
44
|
-
"java",
|
|
45
|
-
"c",
|
|
46
|
-
"cpp",
|
|
47
|
-
"cs",
|
|
48
|
-
"swift",
|
|
49
|
-
"kotlin",
|
|
50
|
-
"html",
|
|
51
|
-
"css",
|
|
52
|
-
"json",
|
|
53
|
-
"yaml",
|
|
54
|
-
"bash",
|
|
55
|
-
"ruby",
|
|
56
|
-
"php",
|
|
57
|
-
"scala",
|
|
58
|
-
"haskell",
|
|
59
|
-
"lua",
|
|
60
|
-
"r",
|
|
61
|
-
"dart",
|
|
62
|
-
"elixir",
|
|
63
|
-
"erlang",
|
|
64
|
-
"ocaml"
|
|
42
|
+
"css"
|
|
65
43
|
]);
|
|
66
44
|
let _napi = void 0;
|
|
67
45
|
let _napiWarnLogged = false;
|
|
46
|
+
let _napiRuntimeLangs = null;
|
|
47
|
+
const DYNAMIC_LANG_PACKAGES = {
|
|
48
|
+
go: "@ast-grep/lang-go",
|
|
49
|
+
python: "@ast-grep/lang-python",
|
|
50
|
+
java: "@ast-grep/lang-java",
|
|
51
|
+
rust: "@ast-grep/lang-rust",
|
|
52
|
+
c: "@ast-grep/lang-c",
|
|
53
|
+
cpp: "@ast-grep/lang-cpp",
|
|
54
|
+
cs: "@ast-grep/lang-csharp",
|
|
55
|
+
kotlin: "@ast-grep/lang-kotlin",
|
|
56
|
+
swift: "@ast-grep/lang-swift",
|
|
57
|
+
ruby: "@ast-grep/lang-ruby",
|
|
58
|
+
php: "@ast-grep/lang-php",
|
|
59
|
+
bash: "@ast-grep/lang-bash",
|
|
60
|
+
scala: "@ast-grep/lang-scala",
|
|
61
|
+
dart: "@ast-grep/lang-dart",
|
|
62
|
+
lua: "@ast-grep/lang-lua",
|
|
63
|
+
elixir: "@ast-grep/lang-elixir",
|
|
64
|
+
haskell: "@ast-grep/lang-haskell"
|
|
65
|
+
};
|
|
68
66
|
async function getNapi() {
|
|
69
67
|
if (_napi !== void 0) return _napi;
|
|
70
68
|
try {
|
|
71
69
|
require.resolve("@ast-grep/napi");
|
|
72
70
|
_napi = require("@ast-grep/napi");
|
|
71
|
+
const registrations = {};
|
|
72
|
+
for (const [langName, pkgName] of Object.entries(DYNAMIC_LANG_PACKAGES)) {
|
|
73
|
+
try {
|
|
74
|
+
require.resolve(pkgName);
|
|
75
|
+
registrations[langName] = require(pkgName);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (Object.keys(registrations).length > 0 && typeof _napi.registerDynamicLanguage === "function") {
|
|
80
|
+
_napi.registerDynamicLanguage(registrations);
|
|
81
|
+
}
|
|
82
|
+
_napiRuntimeLangs = /* @__PURE__ */ new Set([
|
|
83
|
+
...NAPI_BUILTIN_LANGUAGES,
|
|
84
|
+
...Object.keys(registrations)
|
|
85
|
+
]);
|
|
73
86
|
return _napi;
|
|
74
87
|
} catch {
|
|
75
88
|
_napi = null;
|
|
@@ -108,7 +121,9 @@ class PatternRunner {
|
|
|
108
121
|
* native C++ exception that bypasses JS try/catch and corrupts internal mutexes.
|
|
109
122
|
*/
|
|
110
123
|
isSupportedLanguage(language) {
|
|
111
|
-
|
|
124
|
+
const lang = language.toLowerCase();
|
|
125
|
+
if (_napiRuntimeLangs !== null) return _napiRuntimeLangs.has(lang);
|
|
126
|
+
return NAPI_BUILTIN_LANGUAGES.has(lang);
|
|
112
127
|
}
|
|
113
128
|
/**
|
|
114
129
|
* Lazy async loader — returns the napi module or null if missing.
|
|
@@ -125,6 +140,7 @@ class PatternRunner {
|
|
|
125
140
|
if (!this.isSupportedLanguage(language)) return [];
|
|
126
141
|
const napi = await getNapi();
|
|
127
142
|
if (!napi) return [];
|
|
143
|
+
if (!this.isSupportedLanguage(language)) return [];
|
|
128
144
|
try {
|
|
129
145
|
const { parse } = napi;
|
|
130
146
|
const root = parse(language, fileContent);
|
|
@@ -163,6 +179,7 @@ class PatternRunner {
|
|
|
163
179
|
if (!this.isSupportedLanguage(language)) return [];
|
|
164
180
|
const napi = await getNapi();
|
|
165
181
|
if (!napi) return [];
|
|
182
|
+
if (!this.isSupportedLanguage(language)) return [];
|
|
166
183
|
try {
|
|
167
184
|
const { parse } = napi;
|
|
168
185
|
const root = parse(language, fileContent);
|
|
@@ -218,6 +235,7 @@ class PatternRunner {
|
|
|
218
235
|
async applyFix(filePath, fileContent, language, rule) {
|
|
219
236
|
const napi = await this.getNapi();
|
|
220
237
|
if (!napi || !rule.fix) return null;
|
|
238
|
+
if (!this.isSupportedLanguage(language)) return null;
|
|
221
239
|
try {
|
|
222
240
|
const { parse } = napi;
|
|
223
241
|
const root = parse(language, fileContent);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/patterns/runner.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph PatternRunner \u2014 lazy-loads @ast-grep/napi and executes pattern rules.\n */\n\nimport { PatternRule, PatternMatch, SEVERITY_ORDER } from './types';\nimport { logWarn } from '../errors';\n\n// Languages supported by @ast-grep/napi tree-sitter grammars.\n// Calling parse() with an unsupported language throws a native C++ exception\n// that bypasses JS try/catch and corrupts the mutex \u2014 skip unsupported languages.\nconst NAPI_SUPPORTED_LANGUAGES = new Set([\n 'javascript', 'typescript', 'tsx', 'jsx',\n 'python', 'go', 'rust', 'java', 'c', 'cpp', 'cs', 'swift', 'kotlin',\n 'html', 'css', 'json', 'yaml', 'bash', 'ruby', 'php', 'scala',\n 'haskell', 'lua', 'r', 'dart', 'elixir', 'erlang', 'ocaml',\n]);\n\n// Module-level cache: undefined = not yet attempted, null = missing, module = loaded\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _napi: any | null | undefined = undefined;\nlet _napiWarnLogged = false;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function getNapi(): Promise<any | null> {\n if (_napi !== undefined) return _napi;\n try {\n // Verify it can be resolved synchronously before async import\n require.resolve('@ast-grep/napi');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _napi = require('@ast-grep/napi');\n return _napi;\n } catch {\n _napi = null;\n if (!_napiWarnLogged) {\n logWarn('PatternRunner: @ast-grep/napi is not installed. Pattern matching is disabled. Run: npm install @ast-grep/napi');\n _napiWarnLogged = true;\n }\n return null;\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction renderFix(fixTemplate: string, node: any): string | undefined {\n if (!fixTemplate) return undefined;\n // Replace $VAR metavariables with matched text\n return fixTemplate.replace(/\\$([A-Z_]+)/g, (_, varName) => {\n try {\n return node.getMatch(varName)?.text() ?? `$${varName}`;\n } catch {\n return `$${varName}`;\n }\n });\n}\n\nexport class PatternRunner {\n /**\n * Synchronously check whether @ast-grep/napi is available.\n */\n isAvailable(): boolean {\n try {\n require.resolve('@ast-grep/napi');\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Returns true if the language is supported by @ast-grep/napi.\n * Unsupported languages must be skipped \u2014 calling parse() on them throws a\n * native C++ exception that bypasses JS try/catch and corrupts internal mutexes.\n */\n isSupportedLanguage(language: string): boolean {\n return NAPI_SUPPORTED_LANGUAGES.has(language.toLowerCase());\n }\n\n /**\n * Lazy async loader \u2014 returns the napi module or null if missing.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async getNapi(): Promise<any | null> {\n return getNapi();\n }\n\n /**\n * Run a single library rule against a file's content.\n * Returns an empty array and logs a warning on per-file errors.\n */\n async runRule(rule: PatternRule, fileContent: string, language: string): Promise<PatternMatch[]> {\n if (!this.isSupportedLanguage(language)) return [];\n const napi = await getNapi();\n if (!napi) return [];\n\n try {\n const { parse } = napi;\n const root = parse(language as any, fileContent);\n const nodes = root.root().findAll({ rule: rule.rule as any });\n const lines = fileContent.split('\\n');\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return nodes.map((node: any) => {\n const range = node.range();\n const line = range.start.line + 1; // 1-based\n const col = range.start.column; // 0-based\n const matchText = node.text().slice(0, 500);\n const context = _buildContext(lines, range.start.line);\n const fixSuggestion = rule.fix ? renderFix(rule.fix, node) : undefined;\n\n return {\n patternId: rule.id,\n filePath: '',\n line,\n col,\n matchText,\n context,\n severity: rule.severity,\n owaspCategory: rule.owaspCategory,\n language,\n ...(fixSuggestion !== undefined ? { fixSuggestion } : {}),\n };\n });\n } catch (err) {\n logWarn(`PatternRunner: error running rule \"${rule.id}\" on file (language: ${language}): ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n }\n\n /**\n * Run an inline pattern expression (not from library) against file content.\n * Returns an empty array and logs a warning on per-file errors.\n */\n async runInline(pattern: string, language: string, fileContent: string): Promise<PatternMatch[]> {\n if (!this.isSupportedLanguage(language)) return [];\n const napi = await getNapi();\n if (!napi) return [];\n\n try {\n const { parse } = napi;\n const root = parse(language as any, fileContent);\n const nodes = root.root().findAll(pattern);\n const lines = fileContent.split('\\n');\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return nodes.map((node: any) => {\n const range = node.range();\n const line = range.start.line + 1;\n const col = range.start.column;\n const matchText = node.text().slice(0, 500);\n const context = _buildContext(lines, range.start.line);\n\n return {\n patternId: '',\n filePath: '',\n line,\n col,\n matchText,\n context,\n severity: 'low' as const,\n owaspCategory: '',\n language,\n };\n });\n } catch (err) {\n logWarn(`PatternRunner: error running inline pattern on file (language: ${language}): ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n }\n\n /**\n * Run all rules against a file's content, filtered by severity threshold.\n */\n async runAllRules(\n rules: PatternRule[],\n fileContent: string,\n language: string,\n threshold: PatternRule['severity'],\n ): Promise<PatternMatch[]> {\n const napi = await getNapi();\n if (!napi) return [];\n\n const thresholdOrder = SEVERITY_ORDER[threshold];\n const applicableRules = rules.filter(r => {\n const langs = Array.isArray(r.language) ? r.language : [r.language];\n return langs.includes(language) && SEVERITY_ORDER[r.severity] >= thresholdOrder;\n });\n\n const results: PatternMatch[] = [];\n for (const rule of applicableRules) {\n const matches = await this.runRule(rule, fileContent, language);\n results.push(...matches);\n }\n return results;\n }\n\n /**\n * Apply the rule's fix template to all matches in the file content.\n * Returns the transformed source string, or null if no fix was applied.\n * Uses a range-based text substitution approach since commitEdits may not\n * be available in all versions of @ast-grep/napi.\n */\n async applyFix(filePath: string, fileContent: string, language: string, rule: PatternRule): Promise<string | null> {\n const napi = await this.getNapi();\n if (!napi || !rule.fix) return null;\n try {\n const { parse } = napi;\n const root = parse(language as any, fileContent);\n const nodes = root.root().findAll({ rule: rule.rule as any });\n if (nodes.length === 0) return null;\n\n // Try the native commitEdits API first\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n for (const node of nodes as any[]) {\n const fixText = renderFix(rule.fix, node);\n if (fixText) node.replace(fixText);\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const edited: string | null | undefined = (root.root() as any).commitEdits?.();\n if (typeof edited === 'string') return edited;\n } catch {\n // commitEdits not available \u2014 fall through to text substitution\n }\n\n // Fallback: apply replacements using byte offsets, processing from end to start\n // so earlier replacements don't shift the offsets of later ones.\n interface Edit { start: number; end: number; replacement: string }\n const edits: Edit[] = [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n for (const node of nodes as any[]) {\n const fixText = renderFix(rule.fix, node);\n if (fixText === undefined) continue;\n const range = node.range();\n // Convert line/col to byte offset\n const lines = fileContent.split('\\n');\n let startOffset = 0;\n for (let i = 0; i < range.start.line; i++) {\n startOffset += (lines[i]?.length ?? 0) + 1; // +1 for \\n\n }\n startOffset += range.start.column;\n\n let endOffset = 0;\n for (let i = 0; i < range.end.line; i++) {\n endOffset += (lines[i]?.length ?? 0) + 1;\n }\n endOffset += range.end.column;\n\n edits.push({ start: startOffset, end: endOffset, replacement: fixText });\n }\n\n if (edits.length === 0) return null;\n\n // Sort descending by start offset so we can splice without re-indexing\n edits.sort((a, b) => b.start - a.start);\n\n let result = fileContent;\n for (const edit of edits) {\n result = result.slice(0, edit.start) + edit.replacement + result.slice(edit.end);\n }\n\n return result !== fileContent ? result : null;\n } catch (err) {\n logWarn(`PatternRunner.applyFix: error applying fix for rule \"${rule.id}\" on \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`);\n return null;\n }\n }\n}\n\nfunction _buildContext(lines: string[], zeroBasedLine: number): string {\n const contextLines: string[] = [];\n if (zeroBasedLine > 0) contextLines.push(lines[zeroBasedLine - 1] ?? '');\n contextLines.push(lines[zeroBasedLine] ?? '');\n if (zeroBasedLine + 1 < lines.length) contextLines.push(lines[zeroBasedLine + 1] ?? '');\n return contextLines.join('\\n');\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,mBAA0D;AAC1D,oBAAwB;AAKxB,MAAM,
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph PatternRunner \u2014 lazy-loads @ast-grep/napi and executes pattern rules.\n */\n\nimport { PatternRule, PatternMatch, SEVERITY_ORDER } from './types';\nimport { logWarn } from '../errors';\n\n// Languages the @ast-grep/napi binary bundles natively (its built-in Lang enum).\n// napi.Lang is an empty object at runtime (values are type-only), so we enumerate\n// these explicitly rather than reading from the export.\nconst NAPI_BUILTIN_LANGUAGES = new Set([\n 'html', 'javascript', 'jsx', 'typescript', 'tsx', 'css',\n]);\n\n// Module-level cache: undefined = not yet attempted, null = missing, module = loaded\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _napi: any | null | undefined = undefined;\nlet _napiWarnLogged = false;\n// Runtime-detected set of languages the installed @ast-grep/napi actually supports,\n// derived from its Lang enum on first load. Null until napi has been loaded.\nlet _napiRuntimeLangs: Set<string> | null = null;\n\n// Optional language packages that extend napi beyond its built-in JS/TS/HTML/CSS set.\n// registerDynamicLanguage() loads a tree-sitter grammar .so for each entry.\n// Keys must match the language strings used in pattern rule files and the files table.\nconst DYNAMIC_LANG_PACKAGES: Record<string, string> = {\n go: '@ast-grep/lang-go',\n python: '@ast-grep/lang-python',\n java: '@ast-grep/lang-java',\n rust: '@ast-grep/lang-rust',\n c: '@ast-grep/lang-c',\n cpp: '@ast-grep/lang-cpp',\n cs: '@ast-grep/lang-csharp',\n kotlin: '@ast-grep/lang-kotlin',\n swift: '@ast-grep/lang-swift',\n ruby: '@ast-grep/lang-ruby',\n php: '@ast-grep/lang-php',\n bash: '@ast-grep/lang-bash',\n scala: '@ast-grep/lang-scala',\n dart: '@ast-grep/lang-dart',\n lua: '@ast-grep/lang-lua',\n elixir: '@ast-grep/lang-elixir',\n haskell:'@ast-grep/lang-haskell',\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function getNapi(): Promise<any | null> {\n if (_napi !== undefined) return _napi;\n try {\n // Verify it can be resolved synchronously before async import\n require.resolve('@ast-grep/napi');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _napi = require('@ast-grep/napi');\n\n // Register any installed dynamic language grammars. Must happen before the\n // first parse() call and exactly once per process.\n const registrations: Record<string, unknown> = {};\n for (const [langName, pkgName] of Object.entries(DYNAMIC_LANG_PACKAGES)) {\n try {\n require.resolve(pkgName);\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n registrations[langName] = require(pkgName);\n } catch {\n // Package not installed \u2014 skip silently; language simply won't be supported.\n }\n }\n if (Object.keys(registrations).length > 0 && typeof _napi.registerDynamicLanguage === 'function') {\n _napi.registerDynamicLanguage(registrations);\n }\n\n // Build the runtime language set: built-in grammars + successfully registered\n // dynamic ones. napi.Lang is {} at runtime (type-only enum), so we use the\n // explicit NAPI_BUILTIN_LANGUAGES constant as the base.\n _napiRuntimeLangs = new Set([\n ...NAPI_BUILTIN_LANGUAGES,\n ...Object.keys(registrations),\n ]);\n return _napi;\n } catch {\n _napi = null;\n if (!_napiWarnLogged) {\n logWarn('PatternRunner: @ast-grep/napi is not installed. Pattern matching is disabled. Run: npm install @ast-grep/napi');\n _napiWarnLogged = true;\n }\n return null;\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction renderFix(fixTemplate: string, node: any): string | undefined {\n if (!fixTemplate) return undefined;\n // Replace $VAR metavariables with matched text\n return fixTemplate.replace(/\\$([A-Z_]+)/g, (_, varName) => {\n try {\n return node.getMatch(varName)?.text() ?? `$${varName}`;\n } catch {\n return `$${varName}`;\n }\n });\n}\n\nexport class PatternRunner {\n /**\n * Synchronously check whether @ast-grep/napi is available.\n */\n isAvailable(): boolean {\n try {\n require.resolve('@ast-grep/napi');\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Returns true if the language is supported by @ast-grep/napi.\n * Unsupported languages must be skipped \u2014 calling parse() on them throws a\n * native C++ exception that bypasses JS try/catch and corrupts internal mutexes.\n */\n isSupportedLanguage(language: string): boolean {\n const lang = language.toLowerCase();\n // After napi loads, _napiRuntimeLangs reflects exactly what this binary\n // supports (built-ins + registered dynamic langs). Before that, we conservatively\n // allow only the known built-in set so we don't accidentally call parse() on an\n // unsupported language before registration has happened.\n if (_napiRuntimeLangs !== null) return _napiRuntimeLangs.has(lang);\n return NAPI_BUILTIN_LANGUAGES.has(lang);\n }\n\n /**\n * Lazy async loader \u2014 returns the napi module or null if missing.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async getNapi(): Promise<any | null> {\n return getNapi();\n }\n\n /**\n * Run a single library rule against a file's content.\n * Returns an empty array and logs a warning on per-file errors.\n */\n async runRule(rule: PatternRule, fileContent: string, language: string): Promise<PatternMatch[]> {\n if (!this.isSupportedLanguage(language)) return [];\n const napi = await getNapi();\n if (!napi) return [];\n // Re-check after napi loads \u2014 _napiRuntimeLangs is now populated and may\n // exclude languages the static list includes (e.g. Go in a JS-only build).\n if (!this.isSupportedLanguage(language)) return [];\n\n try {\n const { parse } = napi;\n const root = parse(language as any, fileContent);\n const nodes = root.root().findAll({ rule: rule.rule as any });\n const lines = fileContent.split('\\n');\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return nodes.map((node: any) => {\n const range = node.range();\n const line = range.start.line + 1; // 1-based\n const col = range.start.column; // 0-based\n const matchText = node.text().slice(0, 500);\n const context = _buildContext(lines, range.start.line);\n const fixSuggestion = rule.fix ? renderFix(rule.fix, node) : undefined;\n\n return {\n patternId: rule.id,\n filePath: '',\n line,\n col,\n matchText,\n context,\n severity: rule.severity,\n owaspCategory: rule.owaspCategory,\n language,\n ...(fixSuggestion !== undefined ? { fixSuggestion } : {}),\n };\n });\n } catch (err) {\n logWarn(`PatternRunner: error running rule \"${rule.id}\" on file (language: ${language}): ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n }\n\n /**\n * Run an inline pattern expression (not from library) against file content.\n * Returns an empty array and logs a warning on per-file errors.\n */\n async runInline(pattern: string, language: string, fileContent: string): Promise<PatternMatch[]> {\n if (!this.isSupportedLanguage(language)) return [];\n const napi = await getNapi();\n if (!napi) return [];\n if (!this.isSupportedLanguage(language)) return [];\n\n try {\n const { parse } = napi;\n const root = parse(language as any, fileContent);\n const nodes = root.root().findAll(pattern);\n const lines = fileContent.split('\\n');\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return nodes.map((node: any) => {\n const range = node.range();\n const line = range.start.line + 1;\n const col = range.start.column;\n const matchText = node.text().slice(0, 500);\n const context = _buildContext(lines, range.start.line);\n\n return {\n patternId: '',\n filePath: '',\n line,\n col,\n matchText,\n context,\n severity: 'low' as const,\n owaspCategory: '',\n language,\n };\n });\n } catch (err) {\n logWarn(`PatternRunner: error running inline pattern on file (language: ${language}): ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n }\n\n /**\n * Run all rules against a file's content, filtered by severity threshold.\n */\n async runAllRules(\n rules: PatternRule[],\n fileContent: string,\n language: string,\n threshold: PatternRule['severity'],\n ): Promise<PatternMatch[]> {\n const napi = await getNapi();\n if (!napi) return [];\n\n const thresholdOrder = SEVERITY_ORDER[threshold];\n const applicableRules = rules.filter(r => {\n const langs = Array.isArray(r.language) ? r.language : [r.language];\n return langs.includes(language) && SEVERITY_ORDER[r.severity] >= thresholdOrder;\n });\n\n const results: PatternMatch[] = [];\n for (const rule of applicableRules) {\n const matches = await this.runRule(rule, fileContent, language);\n results.push(...matches);\n }\n return results;\n }\n\n /**\n * Apply the rule's fix template to all matches in the file content.\n * Returns the transformed source string, or null if no fix was applied.\n * Uses a range-based text substitution approach since commitEdits may not\n * be available in all versions of @ast-grep/napi.\n */\n async applyFix(filePath: string, fileContent: string, language: string, rule: PatternRule): Promise<string | null> {\n const napi = await this.getNapi();\n if (!napi || !rule.fix) return null;\n if (!this.isSupportedLanguage(language)) return null;\n try {\n const { parse } = napi;\n const root = parse(language as any, fileContent);\n const nodes = root.root().findAll({ rule: rule.rule as any });\n if (nodes.length === 0) return null;\n\n // Try the native commitEdits API first\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n for (const node of nodes as any[]) {\n const fixText = renderFix(rule.fix, node);\n if (fixText) node.replace(fixText);\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const edited: string | null | undefined = (root.root() as any).commitEdits?.();\n if (typeof edited === 'string') return edited;\n } catch {\n // commitEdits not available \u2014 fall through to text substitution\n }\n\n // Fallback: apply replacements using byte offsets, processing from end to start\n // so earlier replacements don't shift the offsets of later ones.\n interface Edit { start: number; end: number; replacement: string }\n const edits: Edit[] = [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n for (const node of nodes as any[]) {\n const fixText = renderFix(rule.fix, node);\n if (fixText === undefined) continue;\n const range = node.range();\n // Convert line/col to byte offset\n const lines = fileContent.split('\\n');\n let startOffset = 0;\n for (let i = 0; i < range.start.line; i++) {\n startOffset += (lines[i]?.length ?? 0) + 1; // +1 for \\n\n }\n startOffset += range.start.column;\n\n let endOffset = 0;\n for (let i = 0; i < range.end.line; i++) {\n endOffset += (lines[i]?.length ?? 0) + 1;\n }\n endOffset += range.end.column;\n\n edits.push({ start: startOffset, end: endOffset, replacement: fixText });\n }\n\n if (edits.length === 0) return null;\n\n // Sort descending by start offset so we can splice without re-indexing\n edits.sort((a, b) => b.start - a.start);\n\n let result = fileContent;\n for (const edit of edits) {\n result = result.slice(0, edit.start) + edit.replacement + result.slice(edit.end);\n }\n\n return result !== fileContent ? result : null;\n } catch (err) {\n logWarn(`PatternRunner.applyFix: error applying fix for rule \"${rule.id}\" on \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`);\n return null;\n }\n }\n}\n\nfunction _buildContext(lines: string[], zeroBasedLine: number): string {\n const contextLines: string[] = [];\n if (zeroBasedLine > 0) contextLines.push(lines[zeroBasedLine - 1] ?? '');\n contextLines.push(lines[zeroBasedLine] ?? '');\n if (zeroBasedLine + 1 < lines.length) contextLines.push(lines[zeroBasedLine + 1] ?? '');\n return contextLines.join('\\n');\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,mBAA0D;AAC1D,oBAAwB;AAKxB,MAAM,yBAAyB,oBAAI,IAAI;AAAA,EACrC;AAAA,EAAQ;AAAA,EAAc;AAAA,EAAO;AAAA,EAAc;AAAA,EAAO;AACpD,CAAC;AAID,IAAI,QAAgC;AACpC,IAAI,kBAAkB;AAGtB,IAAI,oBAAwC;AAK5C,MAAM,wBAAgD;AAAA,EACpD,IAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAQ;AAAA,EACR,MAAQ;AAAA,EACR,GAAQ;AAAA,EACR,KAAQ;AAAA,EACR,IAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAQ;AAAA,EACR,MAAQ;AAAA,EACR,KAAQ;AAAA,EACR,MAAQ;AAAA,EACR,OAAQ;AAAA,EACR,MAAQ;AAAA,EACR,KAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAQ;AACV;AAGA,eAAe,UAA+B;AAC5C,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AAEF,oBAAgB,gBAAgB;AAEhC,YAAQ,QAAQ,gBAAgB;AAIhC,UAAM,gBAAyC,CAAC;AAChD,eAAW,CAAC,UAAU,OAAO,KAAK,OAAO,QAAQ,qBAAqB,GAAG;AACvE,UAAI;AACF,gBAAQ,QAAQ,OAAO;AAEvB,sBAAc,QAAQ,IAAI,QAAQ,OAAO;AAAA,MAC3C,QAAQ;AAAA,MAER;AAAA,IACF;AACA,QAAI,OAAO,KAAK,aAAa,EAAE,SAAS,KAAK,OAAO,MAAM,4BAA4B,YAAY;AAChG,YAAM,wBAAwB,aAAa;AAAA,IAC7C;AAKA,wBAAoB,oBAAI,IAAI;AAAA,MAC1B,GAAG;AAAA,MACH,GAAG,OAAO,KAAK,aAAa;AAAA,IAC9B,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AACN,YAAQ;AACR,QAAI,CAAC,iBAAiB;AACpB,iCAAQ,+GAA+G;AACvH,wBAAkB;AAAA,IACpB;AACA,WAAO;AAAA,EACT;AACF;AAGA,SAAS,UAAU,aAAqB,MAA+B;AACrE,MAAI,CAAC,YAAa,QAAO;AAEzB,SAAO,YAAY,QAAQ,gBAAgB,CAAC,GAAG,YAAY;AACzD,QAAI;AACF,aAAO,KAAK,SAAS,OAAO,GAAG,KAAK,KAAK,IAAI,OAAO;AAAA,IACtD,QAAQ;AACN,aAAO,IAAI,OAAO;AAAA,IACpB;AAAA,EACF,CAAC;AACH;AAEO,MAAM,cAAc;AAAA;AAAA;AAAA;AAAA,EAIzB,cAAuB;AACrB,QAAI;AACF,sBAAgB,gBAAgB;AAChC,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,UAA2B;AAC7C,UAAM,OAAO,SAAS,YAAY;AAKlC,QAAI,sBAAsB,KAAM,QAAO,kBAAkB,IAAI,IAAI;AACjE,WAAO,uBAAuB,IAAI,IAAI;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAA+B;AACnC,WAAO,QAAQ;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQ,MAAmB,aAAqB,UAA2C;AAC/F,QAAI,CAAC,KAAK,oBAAoB,QAAQ,EAAG,QAAO,CAAC;AACjD,UAAM,OAAO,MAAM,QAAQ;AAC3B,QAAI,CAAC,KAAM,QAAO,CAAC;AAGnB,QAAI,CAAC,KAAK,oBAAoB,QAAQ,EAAG,QAAO,CAAC;AAEjD,QAAI;AACF,YAAM,EAAE,MAAM,IAAI;AAClB,YAAM,OAAO,MAAM,UAAiB,WAAW;AAC/C,YAAM,QAAQ,KAAK,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAY,CAAC;AAC5D,YAAM,QAAQ,YAAY,MAAM,IAAI;AAGpC,aAAO,MAAM,IAAI,CAAC,SAAc;AAC9B,cAAM,QAAQ,KAAK,MAAM;AACzB,cAAM,OAAO,MAAM,MAAM,OAAO;AAChC,cAAM,MAAM,MAAM,MAAM;AACxB,cAAM,YAAY,KAAK,KAAK,EAAE,MAAM,GAAG,GAAG;AAC1C,cAAM,UAAU,cAAc,OAAO,MAAM,MAAM,IAAI;AACrD,cAAM,gBAAgB,KAAK,MAAM,UAAU,KAAK,KAAK,IAAI,IAAI;AAE7D,eAAO;AAAA,UACL,WAAW,KAAK;AAAA,UAChB,UAAU;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU,KAAK;AAAA,UACf,eAAe,KAAK;AAAA,UACpB;AAAA,UACA,GAAI,kBAAkB,SAAY,EAAE,cAAc,IAAI,CAAC;AAAA,QACzD;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,iCAAQ,sCAAsC,KAAK,EAAE,wBAAwB,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC7I,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAU,SAAiB,UAAkB,aAA8C;AAC/F,QAAI,CAAC,KAAK,oBAAoB,QAAQ,EAAG,QAAO,CAAC;AACjD,UAAM,OAAO,MAAM,QAAQ;AAC3B,QAAI,CAAC,KAAM,QAAO,CAAC;AACnB,QAAI,CAAC,KAAK,oBAAoB,QAAQ,EAAG,QAAO,CAAC;AAEjD,QAAI;AACF,YAAM,EAAE,MAAM,IAAI;AAClB,YAAM,OAAO,MAAM,UAAiB,WAAW;AAC/C,YAAM,QAAQ,KAAK,KAAK,EAAE,QAAQ,OAAO;AACzC,YAAM,QAAQ,YAAY,MAAM,IAAI;AAGpC,aAAO,MAAM,IAAI,CAAC,SAAc;AAC9B,cAAM,QAAQ,KAAK,MAAM;AACzB,cAAM,OAAO,MAAM,MAAM,OAAO;AAChC,cAAM,MAAM,MAAM,MAAM;AACxB,cAAM,YAAY,KAAK,KAAK,EAAE,MAAM,GAAG,GAAG;AAC1C,cAAM,UAAU,cAAc,OAAO,MAAM,MAAM,IAAI;AAErD,eAAO;AAAA,UACL,WAAW;AAAA,UACX,UAAU;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU;AAAA,UACV,eAAe;AAAA,UACf;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,iCAAQ,kEAAkE,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC1I,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YACJ,OACA,aACA,UACA,WACyB;AACzB,UAAM,OAAO,MAAM,QAAQ;AAC3B,QAAI,CAAC,KAAM,QAAO,CAAC;AAEnB,UAAM,iBAAiB,4BAAe,SAAS;AAC/C,UAAM,kBAAkB,MAAM,OAAO,OAAK;AACxC,YAAM,QAAQ,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,WAAW,CAAC,EAAE,QAAQ;AAClE,aAAO,MAAM,SAAS,QAAQ,KAAK,4BAAe,EAAE,QAAQ,KAAK;AAAA,IACnE,CAAC;AAED,UAAM,UAA0B,CAAC;AACjC,eAAW,QAAQ,iBAAiB;AAClC,YAAM,UAAU,MAAM,KAAK,QAAQ,MAAM,aAAa,QAAQ;AAC9D,cAAQ,KAAK,GAAG,OAAO;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,UAAkB,aAAqB,UAAkB,MAA2C;AACjH,UAAM,OAAO,MAAM,KAAK,QAAQ;AAChC,QAAI,CAAC,QAAQ,CAAC,KAAK,IAAK,QAAO;AAC/B,QAAI,CAAC,KAAK,oBAAoB,QAAQ,EAAG,QAAO;AAChD,QAAI;AACF,YAAM,EAAE,MAAM,IAAI;AAClB,YAAM,OAAO,MAAM,UAAiB,WAAW;AAC/C,YAAM,QAAQ,KAAK,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAY,CAAC;AAC5D,UAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,UAAI;AAEF,mBAAW,QAAQ,OAAgB;AACjC,gBAAM,UAAU,UAAU,KAAK,KAAK,IAAI;AACxC,cAAI,QAAS,MAAK,QAAQ,OAAO;AAAA,QACnC;AAEA,cAAM,SAAqC,KAAK,KAAK,EAAU,cAAc;AAC7E,YAAI,OAAO,WAAW,SAAU,QAAO;AAAA,MACzC,QAAQ;AAAA,MAER;AAKA,YAAM,QAAgB,CAAC;AAEvB,iBAAW,QAAQ,OAAgB;AACjC,cAAM,UAAU,UAAU,KAAK,KAAK,IAAI;AACxC,YAAI,YAAY,OAAW;AAC3B,cAAM,QAAQ,KAAK,MAAM;AAEzB,cAAM,QAAQ,YAAY,MAAM,IAAI;AACpC,YAAI,cAAc;AAClB,iBAAS,IAAI,GAAG,IAAI,MAAM,MAAM,MAAM,KAAK;AACzC,0BAAgB,MAAM,CAAC,GAAG,UAAU,KAAK;AAAA,QAC3C;AACA,uBAAe,MAAM,MAAM;AAE3B,YAAI,YAAY;AAChB,iBAAS,IAAI,GAAG,IAAI,MAAM,IAAI,MAAM,KAAK;AACvC,wBAAc,MAAM,CAAC,GAAG,UAAU,KAAK;AAAA,QACzC;AACA,qBAAa,MAAM,IAAI;AAEvB,cAAM,KAAK,EAAE,OAAO,aAAa,KAAK,WAAW,aAAa,QAAQ,CAAC;AAAA,MACzE;AAEA,UAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,YAAM,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEtC,UAAI,SAAS;AACb,iBAAW,QAAQ,OAAO;AACxB,iBAAS,OAAO,MAAM,GAAG,KAAK,KAAK,IAAI,KAAK,cAAc,OAAO,MAAM,KAAK,GAAG;AAAA,MACjF;AAEA,aAAO,WAAW,cAAc,SAAS;AAAA,IAC3C,SAAS,KAAK;AACZ,iCAAQ,wDAAwD,KAAK,EAAE,SAAS,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAChJ,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,cAAc,OAAiB,eAA+B;AACrE,QAAM,eAAyB,CAAC;AAChC,MAAI,gBAAgB,EAAG,cAAa,KAAK,MAAM,gBAAgB,CAAC,KAAK,EAAE;AACvE,eAAa,KAAK,MAAM,aAAa,KAAK,EAAE;AAC5C,MAAI,gBAAgB,IAAI,MAAM,OAAQ,cAAa,KAAK,MAAM,gBAAgB,CAAC,KAAK,EAAE;AACtF,SAAO,aAAa,KAAK,IAAI;AAC/B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -322,16 +322,16 @@ class DependencyGraphIntegrator {
|
|
|
322
322
|
const ecosystemsWithLockFile = new Set(lockFiles.map((lf) => lf.ecosystem));
|
|
323
323
|
for (const [ecosystem, depsMap] of depsByEcosystem) {
|
|
324
324
|
if (!ecosystemsWithLockFile.has(ecosystem)) {
|
|
325
|
-
for (const [
|
|
325
|
+
for (const [, nodeId] of depsMap) {
|
|
326
326
|
rawDb.run(
|
|
327
327
|
`UPDATE sec_dependencies SET transitive_status = 'incomplete' WHERE node_id = ?`,
|
|
328
328
|
[nodeId]
|
|
329
329
|
);
|
|
330
330
|
incompleteNodes.push(nodeId);
|
|
331
|
-
(0, import_errors.logWarn)(
|
|
332
|
-
`[sec:integrator] Transitive resolution incomplete for "${pkgName}" (${ecosystem}): no lock file found`
|
|
333
|
-
);
|
|
334
331
|
}
|
|
332
|
+
(0, import_errors.logDebug)(
|
|
333
|
+
`[sec:integrator] No lock file parser for ${ecosystem} \u2014 transitive depth unavailable for ${depsMap.size} package(s). Direct vulnerability scanning is unaffected.`
|
|
334
|
+
);
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
for (const lockFile of lockFiles) {
|
|
@@ -340,16 +340,16 @@ class DependencyGraphIntegrator {
|
|
|
340
340
|
if (!ecosystemDeps) continue;
|
|
341
341
|
if (transitives.length === 0 && ecosystemDeps.size > 0) {
|
|
342
342
|
if (lockFile.ecosystem === "go" || lockFile.ecosystem === "python") {
|
|
343
|
-
for (const [
|
|
343
|
+
for (const [, nodeId] of ecosystemDeps) {
|
|
344
344
|
rawDb.run(
|
|
345
345
|
`UPDATE sec_dependencies SET transitive_status = 'incomplete' WHERE node_id = ?`,
|
|
346
346
|
[nodeId]
|
|
347
347
|
);
|
|
348
348
|
incompleteNodes.push(nodeId);
|
|
349
|
-
(0, import_errors.logWarn)(
|
|
350
|
-
`[sec:integrator] Transitive resolution incomplete for "${pkgName}" (${lockFile.ecosystem}): dependency tree not resolvable from lock file`
|
|
351
|
-
);
|
|
352
349
|
}
|
|
350
|
+
(0, import_errors.logDebug)(
|
|
351
|
+
`[sec:integrator] ${lockFile.ecosystem === "go" ? "go.sum" : "requirements.txt"} does not encode a dependency tree \u2014 transitive depth unavailable for ${ecosystemDeps.size} ${lockFile.ecosystem} package(s). Direct vulnerability scanning is unaffected.`
|
|
352
|
+
);
|
|
353
353
|
}
|
|
354
354
|
continue;
|
|
355
355
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/security/integrator.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * DependencyGraphIntegrator\n *\n * Links dependency nodes to code symbols via import and reference edges,\n * resolves transitive dependencies from lock files, and cleans up orphaned nodes.\n *\n * Integration flow:\n * 1. integrate() \u2014 Scan file nodes for import statements resolving to dependencies,\n * create `imports` and `references` edges.\n * 2. resolveTransitives(maxDepth) \u2014 Parse lock files to discover transitive dependency\n * relationships, create `depends_on` edges between Dependency_Nodes.\n * 3. cleanup() \u2014 Remove orphaned Dependency_Nodes no longer declared in any manifest.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { GraphDatabase } from '../db/database';\nimport type { Edge } from '../types';\nimport type { IntegrationResult, TransitiveResult, CleanupResult } from './types';\nimport { logWarn } from '../errors';\n\n// \u2500\u2500 Lock File Parsers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Transitive dependency relationship: parent depends on child.\n */\ninterface TransitiveDep {\n parent: string;\n child: string;\n}\n\n/**\n * Parse package-lock.json (v2/v3) to extract transitive dependency relationships.\n * Returns pairs of (parent, child) where parent depends on child.\n */\nfunction parseNpmLockTransitives(lockPath: string): TransitiveDep[] {\n const deps: TransitiveDep[] = [];\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n const lockData = JSON.parse(content);\n\n if (typeof lockData !== 'object' || lockData === null) return deps;\n\n const packages = lockData.packages;\n if (typeof packages !== 'object' || packages === null) {\n // Try lockfileVersion 1 format\n return parseNpmLockV1Transitives(lockData);\n }\n\n // lockfileVersion 2/3: packages field with nested node_modules paths\n for (const [key, value] of Object.entries(packages as Record<string, unknown>)) {\n if (key === '') continue; // skip root\n if (typeof value !== 'object' || value === null) continue;\n const pkg = value as Record<string, unknown>;\n\n // Extract the package name from the key\n const parentName = key.replace(/^.*node_modules\\//, '');\n if (!parentName) continue;\n\n // Extract this package's dependencies\n const allDeps: Record<string, unknown> = {};\n for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {\n const fieldDeps = pkg[field];\n if (typeof fieldDeps === 'object' && fieldDeps !== null) {\n Object.assign(allDeps, fieldDeps);\n }\n }\n\n for (const childName of Object.keys(allDeps)) {\n deps.push({ parent: parentName, child: childName });\n }\n }\n } catch {\n // Lock file parse failure \u2014 return empty\n }\n return deps;\n}\n\n/**\n * Parse lockfileVersion 1 format (nested dependencies object).\n */\nfunction parseNpmLockV1Transitives(lockData: Record<string, unknown>): TransitiveDep[] {\n const deps: TransitiveDep[] = [];\n const topDeps = lockData.dependencies;\n if (typeof topDeps !== 'object' || topDeps === null) return deps;\n\n function recurse(parentDeps: Record<string, unknown>): void {\n for (const [name, value] of Object.entries(parentDeps)) {\n if (typeof value !== 'object' || value === null) continue;\n const pkg = value as Record<string, unknown>;\n\n // Check for requires (direct dependencies of this package)\n const requires = pkg.requires;\n if (typeof requires === 'object' && requires !== null) {\n for (const childName of Object.keys(requires as Record<string, unknown>)) {\n deps.push({ parent: name, child: childName });\n }\n }\n\n // Recurse into nested dependencies\n const nested = pkg.dependencies;\n if (typeof nested === 'object' && nested !== null) {\n recurse(nested as Record<string, unknown>);\n }\n }\n }\n\n recurse(topDeps as Record<string, unknown>);\n return deps;\n}\n\n/**\n * Parse Cargo.lock to extract transitive dependency relationships.\n * Cargo.lock [[package]] blocks have a `dependencies` field listing direct deps.\n */\nfunction parseCargoLockTransitives(lockPath: string): TransitiveDep[] {\n const deps: TransitiveDep[] = [];\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n const lines = content.split('\\n');\n\n let currentName: string | null = null;\n let inDeps = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (trimmed === '[[package]]') {\n currentName = null;\n inDeps = false;\n continue;\n }\n\n if (currentName === null) {\n const nameMatch = trimmed.match(/^name\\s*=\\s*\"([^\"]+)\"$/);\n if (nameMatch) {\n currentName = nameMatch[1];\n continue;\n }\n }\n\n if (currentName !== null && trimmed === 'dependencies = [') {\n inDeps = true;\n continue;\n }\n\n if (inDeps) {\n if (trimmed === ']') {\n inDeps = false;\n continue;\n }\n // Lines like: \"serde_derive 1.0.193 (registry+...)\",\n // or simply: \"serde_derive\",\n const depMatch = trimmed.match(/^\\s*\"([^\"\\s]+)/);\n if (depMatch && currentName) {\n deps.push({ parent: currentName, child: depMatch[1] });\n }\n }\n }\n } catch {\n // Lock file parse failure \u2014 return empty\n }\n return deps;\n}\n\n/**\n * Parse go.sum to extract module dependency relationships.\n * go.sum doesn't directly encode dependency trees, so we parse go.mod\n * for the require directives. For transitive resolution, we'd need\n * `go mod graph` output, but for the MVP we mark Go deps as incomplete\n * if no explicit dependency tree is available.\n *\n * Returns empty \u2014 Go transitive resolution requires `go mod graph` which\n * is not available at index time. Nodes will be marked incomplete.\n */\nfunction parseGoTransitives(_lockPath: string): TransitiveDep[] {\n // Go modules don't have a lock file that encodes the dependency tree.\n // go.sum only provides checksums. Full transitive resolution requires\n // running `go mod graph` which is out of scope for static analysis.\n return [];\n}\n\n/**\n * Parse pip's requirements files for transitive dependencies.\n * pip doesn't have a standard lock file with dependency trees.\n * Returns empty \u2014 nodes will be marked incomplete.\n */\nfunction parsePipTransitives(_lockPath: string): TransitiveDep[] {\n return [];\n}\n\n// \u2500\u2500 Lock File Discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface LockFileInfo {\n ecosystem: string;\n lockPath: string;\n parser: (lockPath: string) => TransitiveDep[];\n}\n\n/**\n * Discover lock files in the project root for each ecosystem.\n */\nfunction discoverLockFiles(projectRoot: string): LockFileInfo[] {\n const lockFiles: LockFileInfo[] = [];\n\n const candidates: Array<{ ecosystem: string; filename: string; parser: (p: string) => TransitiveDep[] }> = [\n { ecosystem: 'npm', filename: 'package-lock.json', parser: parseNpmLockTransitives },\n { ecosystem: 'npm', filename: 'yarn.lock', parser: parseNpmLockTransitives },\n { ecosystem: 'cargo', filename: 'Cargo.lock', parser: parseCargoLockTransitives },\n { ecosystem: 'go', filename: 'go.sum', parser: parseGoTransitives },\n { ecosystem: 'python', filename: 'requirements.txt', parser: parsePipTransitives },\n ];\n\n for (const candidate of candidates) {\n const lockPath = path.join(projectRoot, candidate.filename);\n if (fs.existsSync(lockPath)) {\n lockFiles.push({\n ecosystem: candidate.ecosystem,\n lockPath,\n parser: candidate.parser,\n });\n }\n }\n\n return lockFiles;\n}\n\n// \u2500\u2500 DependencyGraphIntegrator Class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class DependencyGraphIntegrator {\n private readonly db: GraphDatabase;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, projectRoot: string) {\n this.db = db;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Create import/reference edges between code symbols and Dependency_Nodes.\n *\n * Per Requirement 2.1: creates `imports` edges from file nodes that contain\n * import statements resolving to a dependency \u2192 the Dependency_Node.\n *\n * Per Requirement 2.2: creates `references` edges from code symbols that\n * reference classes/functions from a dependency \u2192 the Dependency_Node.\n */\n async integrate(): Promise<IntegrationResult> {\n let importsEdgesCreated = 0;\n let referencesEdgesCreated = 0;\n\n const rawDb = this.db.getRawDb();\n\n // Get all dependency nodes with their package names and ecosystems\n const depRows: Array<{ node_id: string; package_name: string; ecosystem: string }> =\n rawDb.all('SELECT node_id, package_name, ecosystem FROM sec_dependencies');\n\n if (depRows.length === 0) {\n return { importsEdgesCreated, referencesEdgesCreated };\n }\n\n // Build a lookup map: package name \u2192 dependency node ID\n const depByName = new Map<string, string>();\n for (const row of depRows) {\n depByName.set(row.package_name, row.node_id);\n }\n\n // \u2500\u2500 Step 1: Create `imports` edges (file \u2192 dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Scan import nodes. The import node's `name` field contains the module\n // specifier (e.g., 'express', '@nestjs/core', 'github.com/gin-gonic/gin').\n // For each match, create an edge from the FILE node to the Dependency_Node.\n const importNodes = this.db.getNodesByKind('import');\n\n // Track (file, dep) pairs to avoid duplicate edges\n const createdImportEdges = new Set<string>();\n\n for (const importNode of importNodes) {\n // Use the import node's `name` field which is the raw module specifier\n const specifier = importNode.name;\n\n const matchedDepId = this.matchImportToDependency(specifier, depByName);\n if (!matchedDepId) continue;\n\n // Find the file node for this import's file path\n const fileNodeId = this.findFileNodeId(rawDb, importNode.filePath);\n if (!fileNodeId) continue;\n\n const edgeKey = `${fileNodeId}\u2192${matchedDepId}`;\n if (createdImportEdges.has(edgeKey)) continue;\n\n const edge: Edge = {\n source: fileNodeId,\n target: matchedDepId,\n kind: 'imports',\n line: importNode.startLine,\n confidence: 'inferred',\n confidenceScore: 0.9,\n };\n this.db.insertEdge(edge);\n createdImportEdges.add(edgeKey);\n importsEdgesCreated++;\n }\n\n // \u2500\u2500 Step 2: Create `references` edges (code symbol \u2192 dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // For each import node that matched a dependency, find code symbols in the\n // same file that call or reference that import node. Those symbols effectively\n // reference the dependency.\n const codeKinds = new Set(['function', 'method', 'class', 'variable', 'constant', 'property']);\n const createdRefEdges = new Set<string>();\n\n for (const importNode of importNodes) {\n const specifier = importNode.name;\n const matchedDepId = this.matchImportToDependency(specifier, depByName);\n if (!matchedDepId) continue;\n\n // Get all nodes in the same file\n const fileNodes = this.db.getNodesByFile(importNode.filePath);\n const codeSymbols = fileNodes.filter(n => codeKinds.has(n.kind));\n if (codeSymbols.length === 0) continue;\n\n // Check edges from code symbols to this import node\n const symbolIds = codeSymbols.map(n => n.id);\n const edges = this.db.getEdgesForNodes(symbolIds);\n\n for (const edge of edges) {\n // A code symbol calls or references the import node \u2192 it references the dependency\n if (\n (edge.kind === 'calls' || edge.kind === 'references') &&\n edge.target === importNode.id &&\n symbolIds.includes(edge.source)\n ) {\n const refKey = `${edge.source}\u2192${matchedDepId}`;\n if (createdRefEdges.has(refKey)) continue;\n\n const refEdge: Edge = {\n source: edge.source,\n target: matchedDepId,\n kind: 'references',\n confidence: 'inferred',\n confidenceScore: 0.8,\n };\n this.db.insertEdge(refEdge);\n createdRefEdges.add(refKey);\n referencesEdgesCreated++;\n }\n }\n }\n\n return { importsEdgesCreated, referencesEdgesCreated };\n }\n\n /**\n * Find the file node ID for a given file path.\n * File nodes have kind='file' and their file_path matches.\n */\n private findFileNodeId(rawDb: any, filePath: string): string | null {\n const row = rawDb.get(\n 'SELECT id FROM nodes WHERE kind = ? AND file_path = ? LIMIT 1',\n ['file', filePath],\n );\n return row ? row.id : null;\n }\n\n /**\n * Match an import specifier to a dependency node ID.\n *\n * Handles:\n * - Exact match: `express` \u2192 dep:npm:express\n * - Scoped packages: `@scope/pkg` \u2192 dep:npm:@scope/pkg\n * - Subpath imports: `express/Router` \u2192 dep:npm:express\n * - Scoped subpath: `@angular/core/testing` \u2192 dep:npm:@angular/core\n * - Go modules: `github.com/gin-gonic/gin` \u2192 dep:go:github.com/gin-gonic/gin\n * - Go subpath: `github.com/gin-gonic/gin/internal` \u2192 dep:go:github.com/gin-gonic/gin\n */\n private matchImportToDependency(\n specifier: string,\n depByName: Map<string, string>,\n ): string | null {\n if (!specifier) return null;\n\n // Skip relative imports\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null;\n\n // Direct match\n if (depByName.has(specifier)) {\n return depByName.get(specifier)!;\n }\n\n // For scoped packages: @scope/name/subpath \u2192 @scope/name\n if (specifier.startsWith('@')) {\n const parts = specifier.split('/');\n if (parts.length >= 2) {\n const scopedName = `${parts[0]}/${parts[1]}`;\n if (depByName.has(scopedName)) {\n return depByName.get(scopedName)!;\n }\n }\n }\n\n // For unscoped packages: name/subpath \u2192 name\n if (!specifier.startsWith('@')) {\n const slashIdx = specifier.indexOf('/');\n if (slashIdx > 0) {\n const baseName = specifier.slice(0, slashIdx);\n if (depByName.has(baseName)) {\n return depByName.get(baseName)!;\n }\n }\n }\n\n // Go module path matching: try progressively shorter prefixes\n // e.g. github.com/gin-gonic/gin/internal \u2192 github.com/gin-gonic/gin\n if (specifier.includes('.') && specifier.includes('/')) {\n const parts = specifier.split('/');\n for (let i = parts.length - 1; i >= 2; i--) {\n const prefix = parts.slice(0, i).join('/');\n if (depByName.has(prefix)) {\n return depByName.get(prefix)!;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Resolve transitive dependencies up to maxDepth levels.\n *\n * For each ecosystem with a lock file:\n * 1. Parse the lock file to extract dependency relationships\n * 2. Create `depends_on` edges between Dependency_Nodes\n * 3. Traverse up to maxDepth levels of transitive depth\n *\n * If a lock file is missing for an ecosystem, mark all dependencies of that\n * ecosystem with `transitive_status: 'incomplete'` and log a warning.\n *\n * @param maxDepth - Maximum levels of transitive depth to resolve (default: 10)\n */\n async resolveTransitives(maxDepth: number = 10): Promise<TransitiveResult> {\n let dependsOnEdgesCreated = 0;\n const incompleteNodes: string[] = [];\n const rawDb = this.db.getRawDb();\n\n // Get all dependency nodes grouped by ecosystem\n const depRows: Array<{ node_id: string; package_name: string; ecosystem: string }> =\n rawDb.all('SELECT node_id, package_name, ecosystem FROM sec_dependencies');\n\n if (depRows.length === 0) {\n return { dependsOnEdgesCreated, incompleteNodes };\n }\n\n // Group dependencies by ecosystem\n const depsByEcosystem = new Map<string, Map<string, string>>();\n for (const row of depRows) {\n if (!depsByEcosystem.has(row.ecosystem)) {\n depsByEcosystem.set(row.ecosystem, new Map());\n }\n depsByEcosystem.get(row.ecosystem)!.set(row.package_name, row.node_id);\n }\n\n // Discover available lock files\n const lockFiles = discoverLockFiles(this.projectRoot);\n const ecosystemsWithLockFile = new Set(lockFiles.map(lf => lf.ecosystem));\n\n // Mark ecosystems without lock files as incomplete\n for (const [ecosystem, depsMap] of depsByEcosystem) {\n if (!ecosystemsWithLockFile.has(ecosystem)) {\n // No lock file for this ecosystem \u2014 mark all deps as incomplete\n for (const [pkgName, nodeId] of depsMap) {\n rawDb.run(\n `UPDATE sec_dependencies SET transitive_status = 'incomplete' WHERE node_id = ?`,\n [nodeId],\n );\n incompleteNodes.push(nodeId);\n logWarn(\n `[sec:integrator] Transitive resolution incomplete for \"${pkgName}\" (${ecosystem}): ` +\n `no lock file found`,\n );\n }\n }\n }\n\n // Parse each lock file and create depends_on edges\n for (const lockFile of lockFiles) {\n const transitives = lockFile.parser(lockFile.lockPath);\n const ecosystemDeps = depsByEcosystem.get(lockFile.ecosystem);\n if (!ecosystemDeps) continue;\n\n if (transitives.length === 0 && ecosystemDeps.size > 0) {\n // Lock file exists but yielded no transitive info (e.g., go.sum)\n // Mark deps as incomplete if the parser couldn't extract tree info\n if (lockFile.ecosystem === 'go' || lockFile.ecosystem === 'python') {\n for (const [pkgName, nodeId] of ecosystemDeps) {\n rawDb.run(\n `UPDATE sec_dependencies SET transitive_status = 'incomplete' WHERE node_id = ?`,\n [nodeId],\n );\n incompleteNodes.push(nodeId);\n logWarn(\n `[sec:integrator] Transitive resolution incomplete for \"${pkgName}\" (${lockFile.ecosystem}): ` +\n `dependency tree not resolvable from lock file`,\n );\n }\n }\n continue;\n }\n\n // Build adjacency list from transitive relationships\n const adjacency = new Map<string, Set<string>>();\n for (const { parent, child } of transitives) {\n if (!adjacency.has(parent)) {\n adjacency.set(parent, new Set());\n }\n adjacency.get(parent)!.add(child);\n }\n\n // BFS from each direct dependency up to maxDepth levels\n // Create depends_on edges only between nodes that exist in our dep graph\n const createdEdges = new Set<string>();\n\n for (const [pkgName, nodeId] of ecosystemDeps) {\n const visited = new Set<string>([pkgName]);\n let frontier = [pkgName];\n let depth = 0;\n\n while (frontier.length > 0 && depth < maxDepth) {\n const nextFrontier: string[] = [];\n depth++;\n\n for (const current of frontier) {\n const children = adjacency.get(current);\n if (!children) continue;\n\n for (const child of children) {\n if (visited.has(child)) continue;\n visited.add(child);\n\n // Create depends_on edge if the child is a known dependency node\n const childNodeId = ecosystemDeps.get(child);\n const parentNodeId = current === pkgName ? nodeId : ecosystemDeps.get(current);\n\n if (parentNodeId && childNodeId) {\n const edgeKey = `${parentNodeId}->${childNodeId}`;\n if (!createdEdges.has(edgeKey)) {\n const edge: Edge = {\n source: parentNodeId,\n target: childNodeId,\n kind: 'depends_on',\n confidence: 'inferred',\n confidenceScore: 1.0,\n };\n this.db.insertEdge(edge);\n createdEdges.add(edgeKey);\n dependsOnEdgesCreated++;\n }\n }\n\n nextFrontier.push(child);\n }\n }\n\n frontier = nextFrontier;\n }\n }\n }\n\n return { dependsOnEdgesCreated, incompleteNodes };\n }\n\n /**\n * Remove orphaned Dependency_Nodes and their edges.\n *\n * A Dependency_Node is orphaned if:\n * 1. It has no `declared_in` edges (manifest no longer declares it), OR\n * 2. None of its source manifests still exist on disk\n *\n * Also removes transitive Dependency_Nodes that are no longer reachable from\n * any remaining direct dependency via `depends_on` edges.\n *\n * @param manifestExists - Optional function to check if a manifest path exists.\n * Defaults to fs.existsSync. Useful for testing.\n */\n async cleanup(manifestExists?: (absolutePath: string) => boolean): Promise<CleanupResult> {\n let nodesRemoved = 0;\n let edgesRemoved = 0;\n const rawDb = this.db.getRawDb();\n const checkExists = manifestExists ?? ((p: string) => fs.existsSync(p));\n\n // Step 1: Find dependency nodes that are orphaned because they have no\n // declared_in edges (manifest no longer declares them)\n const orphanedByEdge: Array<{ node_id: string }> = rawDb.all(\n `SELECT sd.node_id FROM sec_dependencies sd\n WHERE NOT EXISTS (\n SELECT 1 FROM edges e\n WHERE e.source = sd.node_id AND e.kind = 'declared_in'\n )`,\n );\n\n const orphanedNodeIds = new Set<string>(orphanedByEdge.map(r => r.node_id));\n\n // Step 2: Also check if source manifests still exist on disk\n const allDeps: Array<{ node_id: string; source_manifests: string }> = rawDb.all(\n `SELECT node_id, source_manifests FROM sec_dependencies`,\n );\n\n for (const dep of allDeps) {\n if (orphanedNodeIds.has(dep.node_id)) continue; // already marked\n\n const sourceManifests: string[] = JSON.parse(dep.source_manifests);\n const hasValidManifest = sourceManifests.some(manifest => {\n const absolutePath = path.isAbsolute(manifest)\n ? manifest\n : path.join(this.projectRoot, manifest);\n return checkExists(absolutePath);\n });\n\n if (!hasValidManifest) {\n orphanedNodeIds.add(dep.node_id);\n }\n }\n\n // Step 3: Find transitive Dependency_Nodes no longer reachable from any\n // remaining direct dependency via `depends_on` edges\n const remainingNodeIds = new Set<string>(\n allDeps\n .filter(d => !orphanedNodeIds.has(d.node_id))\n .map(d => d.node_id),\n );\n\n // BFS from remaining nodes following `depends_on` edges to find all reachable transitives\n const reachableFromDirect = new Set<string>(remainingNodeIds);\n let frontier = [...remainingNodeIds];\n\n while (frontier.length > 0) {\n const placeholders = frontier.map(() => '?').join(',');\n const nextRows: Array<{ target: string }> = rawDb.all(\n `SELECT DISTINCT target FROM edges WHERE source IN (${placeholders}) AND kind = 'depends_on'`,\n frontier,\n );\n\n frontier = [];\n for (const row of nextRows) {\n if (!reachableFromDirect.has(row.target)) {\n reachableFromDirect.add(row.target);\n frontier.push(row.target);\n }\n }\n }\n\n // Any dependency node that is NOT reachable from a remaining direct dependency is also orphaned\n for (const dep of allDeps) {\n if (!orphanedNodeIds.has(dep.node_id) && !reachableFromDirect.has(dep.node_id)) {\n orphanedNodeIds.add(dep.node_id);\n }\n }\n\n if (orphanedNodeIds.size === 0) {\n return { nodesRemoved: 0, edgesRemoved: 0 };\n }\n\n // Step 4: Remove orphaned nodes and their edges\n const orphanedIds = [...orphanedNodeIds];\n\n for (const nodeId of orphanedIds) {\n // Count edges being removed\n const edgeCount = rawDb.get(\n `SELECT COUNT(*) as c FROM edges\n WHERE source = ? OR target = ?`,\n [nodeId, nodeId],\n );\n edgesRemoved += edgeCount?.c ?? 0;\n\n // Delete edges connected to this node\n rawDb.run('DELETE FROM edges WHERE source = ? OR target = ?', [nodeId, nodeId]);\n\n // Remove from sec_dependencies (explicit delete before nodes for clarity)\n rawDb.run('DELETE FROM sec_dependencies WHERE node_id = ?', [nodeId]);\n\n // Remove from nodes table\n rawDb.run('DELETE FROM nodes WHERE id = ?', [nodeId]);\n\n // Clean up FTS entries\n rawDb.run('DELETE FROM nodes_fts WHERE id = ?', [nodeId]);\n\n nodesRemoved++;\n }\n\n return { nodesRemoved, edgesRemoved };\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,SAAoB;AACpB,WAAsB;AAItB,
|
|
4
|
+
"sourcesContent": ["/**\n * DependencyGraphIntegrator\n *\n * Links dependency nodes to code symbols via import and reference edges,\n * resolves transitive dependencies from lock files, and cleans up orphaned nodes.\n *\n * Integration flow:\n * 1. integrate() \u2014 Scan file nodes for import statements resolving to dependencies,\n * create `imports` and `references` edges.\n * 2. resolveTransitives(maxDepth) \u2014 Parse lock files to discover transitive dependency\n * relationships, create `depends_on` edges between Dependency_Nodes.\n * 3. cleanup() \u2014 Remove orphaned Dependency_Nodes no longer declared in any manifest.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { GraphDatabase } from '../db/database';\nimport type { Edge } from '../types';\nimport type { IntegrationResult, TransitiveResult, CleanupResult } from './types';\nimport { logWarn, logDebug } from '../errors';\n\n// \u2500\u2500 Lock File Parsers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Transitive dependency relationship: parent depends on child.\n */\ninterface TransitiveDep {\n parent: string;\n child: string;\n}\n\n/**\n * Parse package-lock.json (v2/v3) to extract transitive dependency relationships.\n * Returns pairs of (parent, child) where parent depends on child.\n */\nfunction parseNpmLockTransitives(lockPath: string): TransitiveDep[] {\n const deps: TransitiveDep[] = [];\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n const lockData = JSON.parse(content);\n\n if (typeof lockData !== 'object' || lockData === null) return deps;\n\n const packages = lockData.packages;\n if (typeof packages !== 'object' || packages === null) {\n // Try lockfileVersion 1 format\n return parseNpmLockV1Transitives(lockData);\n }\n\n // lockfileVersion 2/3: packages field with nested node_modules paths\n for (const [key, value] of Object.entries(packages as Record<string, unknown>)) {\n if (key === '') continue; // skip root\n if (typeof value !== 'object' || value === null) continue;\n const pkg = value as Record<string, unknown>;\n\n // Extract the package name from the key\n const parentName = key.replace(/^.*node_modules\\//, '');\n if (!parentName) continue;\n\n // Extract this package's dependencies\n const allDeps: Record<string, unknown> = {};\n for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {\n const fieldDeps = pkg[field];\n if (typeof fieldDeps === 'object' && fieldDeps !== null) {\n Object.assign(allDeps, fieldDeps);\n }\n }\n\n for (const childName of Object.keys(allDeps)) {\n deps.push({ parent: parentName, child: childName });\n }\n }\n } catch {\n // Lock file parse failure \u2014 return empty\n }\n return deps;\n}\n\n/**\n * Parse lockfileVersion 1 format (nested dependencies object).\n */\nfunction parseNpmLockV1Transitives(lockData: Record<string, unknown>): TransitiveDep[] {\n const deps: TransitiveDep[] = [];\n const topDeps = lockData.dependencies;\n if (typeof topDeps !== 'object' || topDeps === null) return deps;\n\n function recurse(parentDeps: Record<string, unknown>): void {\n for (const [name, value] of Object.entries(parentDeps)) {\n if (typeof value !== 'object' || value === null) continue;\n const pkg = value as Record<string, unknown>;\n\n // Check for requires (direct dependencies of this package)\n const requires = pkg.requires;\n if (typeof requires === 'object' && requires !== null) {\n for (const childName of Object.keys(requires as Record<string, unknown>)) {\n deps.push({ parent: name, child: childName });\n }\n }\n\n // Recurse into nested dependencies\n const nested = pkg.dependencies;\n if (typeof nested === 'object' && nested !== null) {\n recurse(nested as Record<string, unknown>);\n }\n }\n }\n\n recurse(topDeps as Record<string, unknown>);\n return deps;\n}\n\n/**\n * Parse Cargo.lock to extract transitive dependency relationships.\n * Cargo.lock [[package]] blocks have a `dependencies` field listing direct deps.\n */\nfunction parseCargoLockTransitives(lockPath: string): TransitiveDep[] {\n const deps: TransitiveDep[] = [];\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n const lines = content.split('\\n');\n\n let currentName: string | null = null;\n let inDeps = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (trimmed === '[[package]]') {\n currentName = null;\n inDeps = false;\n continue;\n }\n\n if (currentName === null) {\n const nameMatch = trimmed.match(/^name\\s*=\\s*\"([^\"]+)\"$/);\n if (nameMatch) {\n currentName = nameMatch[1];\n continue;\n }\n }\n\n if (currentName !== null && trimmed === 'dependencies = [') {\n inDeps = true;\n continue;\n }\n\n if (inDeps) {\n if (trimmed === ']') {\n inDeps = false;\n continue;\n }\n // Lines like: \"serde_derive 1.0.193 (registry+...)\",\n // or simply: \"serde_derive\",\n const depMatch = trimmed.match(/^\\s*\"([^\"\\s]+)/);\n if (depMatch && currentName) {\n deps.push({ parent: currentName, child: depMatch[1] });\n }\n }\n }\n } catch {\n // Lock file parse failure \u2014 return empty\n }\n return deps;\n}\n\n/**\n * Parse go.sum to extract module dependency relationships.\n * go.sum doesn't directly encode dependency trees, so we parse go.mod\n * for the require directives. For transitive resolution, we'd need\n * `go mod graph` output, but for the MVP we mark Go deps as incomplete\n * if no explicit dependency tree is available.\n *\n * Returns empty \u2014 Go transitive resolution requires `go mod graph` which\n * is not available at index time. Nodes will be marked incomplete.\n */\nfunction parseGoTransitives(_lockPath: string): TransitiveDep[] {\n // Go modules don't have a lock file that encodes the dependency tree.\n // go.sum only provides checksums. Full transitive resolution requires\n // running `go mod graph` which is out of scope for static analysis.\n return [];\n}\n\n/**\n * Parse pip's requirements files for transitive dependencies.\n * pip doesn't have a standard lock file with dependency trees.\n * Returns empty \u2014 nodes will be marked incomplete.\n */\nfunction parsePipTransitives(_lockPath: string): TransitiveDep[] {\n return [];\n}\n\n// \u2500\u2500 Lock File Discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface LockFileInfo {\n ecosystem: string;\n lockPath: string;\n parser: (lockPath: string) => TransitiveDep[];\n}\n\n/**\n * Discover lock files in the project root for each ecosystem.\n */\nfunction discoverLockFiles(projectRoot: string): LockFileInfo[] {\n const lockFiles: LockFileInfo[] = [];\n\n const candidates: Array<{ ecosystem: string; filename: string; parser: (p: string) => TransitiveDep[] }> = [\n { ecosystem: 'npm', filename: 'package-lock.json', parser: parseNpmLockTransitives },\n { ecosystem: 'npm', filename: 'yarn.lock', parser: parseNpmLockTransitives },\n { ecosystem: 'cargo', filename: 'Cargo.lock', parser: parseCargoLockTransitives },\n { ecosystem: 'go', filename: 'go.sum', parser: parseGoTransitives },\n { ecosystem: 'python', filename: 'requirements.txt', parser: parsePipTransitives },\n ];\n\n for (const candidate of candidates) {\n const lockPath = path.join(projectRoot, candidate.filename);\n if (fs.existsSync(lockPath)) {\n lockFiles.push({\n ecosystem: candidate.ecosystem,\n lockPath,\n parser: candidate.parser,\n });\n }\n }\n\n return lockFiles;\n}\n\n// \u2500\u2500 DependencyGraphIntegrator Class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class DependencyGraphIntegrator {\n private readonly db: GraphDatabase;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, projectRoot: string) {\n this.db = db;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Create import/reference edges between code symbols and Dependency_Nodes.\n *\n * Per Requirement 2.1: creates `imports` edges from file nodes that contain\n * import statements resolving to a dependency \u2192 the Dependency_Node.\n *\n * Per Requirement 2.2: creates `references` edges from code symbols that\n * reference classes/functions from a dependency \u2192 the Dependency_Node.\n */\n async integrate(): Promise<IntegrationResult> {\n let importsEdgesCreated = 0;\n let referencesEdgesCreated = 0;\n\n const rawDb = this.db.getRawDb();\n\n // Get all dependency nodes with their package names and ecosystems\n const depRows: Array<{ node_id: string; package_name: string; ecosystem: string }> =\n rawDb.all('SELECT node_id, package_name, ecosystem FROM sec_dependencies');\n\n if (depRows.length === 0) {\n return { importsEdgesCreated, referencesEdgesCreated };\n }\n\n // Build a lookup map: package name \u2192 dependency node ID\n const depByName = new Map<string, string>();\n for (const row of depRows) {\n depByName.set(row.package_name, row.node_id);\n }\n\n // \u2500\u2500 Step 1: Create `imports` edges (file \u2192 dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Scan import nodes. The import node's `name` field contains the module\n // specifier (e.g., 'express', '@nestjs/core', 'github.com/gin-gonic/gin').\n // For each match, create an edge from the FILE node to the Dependency_Node.\n const importNodes = this.db.getNodesByKind('import');\n\n // Track (file, dep) pairs to avoid duplicate edges\n const createdImportEdges = new Set<string>();\n\n for (const importNode of importNodes) {\n // Use the import node's `name` field which is the raw module specifier\n const specifier = importNode.name;\n\n const matchedDepId = this.matchImportToDependency(specifier, depByName);\n if (!matchedDepId) continue;\n\n // Find the file node for this import's file path\n const fileNodeId = this.findFileNodeId(rawDb, importNode.filePath);\n if (!fileNodeId) continue;\n\n const edgeKey = `${fileNodeId}\u2192${matchedDepId}`;\n if (createdImportEdges.has(edgeKey)) continue;\n\n const edge: Edge = {\n source: fileNodeId,\n target: matchedDepId,\n kind: 'imports',\n line: importNode.startLine,\n confidence: 'inferred',\n confidenceScore: 0.9,\n };\n this.db.insertEdge(edge);\n createdImportEdges.add(edgeKey);\n importsEdgesCreated++;\n }\n\n // \u2500\u2500 Step 2: Create `references` edges (code symbol \u2192 dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // For each import node that matched a dependency, find code symbols in the\n // same file that call or reference that import node. Those symbols effectively\n // reference the dependency.\n const codeKinds = new Set(['function', 'method', 'class', 'variable', 'constant', 'property']);\n const createdRefEdges = new Set<string>();\n\n for (const importNode of importNodes) {\n const specifier = importNode.name;\n const matchedDepId = this.matchImportToDependency(specifier, depByName);\n if (!matchedDepId) continue;\n\n // Get all nodes in the same file\n const fileNodes = this.db.getNodesByFile(importNode.filePath);\n const codeSymbols = fileNodes.filter(n => codeKinds.has(n.kind));\n if (codeSymbols.length === 0) continue;\n\n // Check edges from code symbols to this import node\n const symbolIds = codeSymbols.map(n => n.id);\n const edges = this.db.getEdgesForNodes(symbolIds);\n\n for (const edge of edges) {\n // A code symbol calls or references the import node \u2192 it references the dependency\n if (\n (edge.kind === 'calls' || edge.kind === 'references') &&\n edge.target === importNode.id &&\n symbolIds.includes(edge.source)\n ) {\n const refKey = `${edge.source}\u2192${matchedDepId}`;\n if (createdRefEdges.has(refKey)) continue;\n\n const refEdge: Edge = {\n source: edge.source,\n target: matchedDepId,\n kind: 'references',\n confidence: 'inferred',\n confidenceScore: 0.8,\n };\n this.db.insertEdge(refEdge);\n createdRefEdges.add(refKey);\n referencesEdgesCreated++;\n }\n }\n }\n\n return { importsEdgesCreated, referencesEdgesCreated };\n }\n\n /**\n * Find the file node ID for a given file path.\n * File nodes have kind='file' and their file_path matches.\n */\n private findFileNodeId(rawDb: any, filePath: string): string | null {\n const row = rawDb.get(\n 'SELECT id FROM nodes WHERE kind = ? AND file_path = ? LIMIT 1',\n ['file', filePath],\n );\n return row ? row.id : null;\n }\n\n /**\n * Match an import specifier to a dependency node ID.\n *\n * Handles:\n * - Exact match: `express` \u2192 dep:npm:express\n * - Scoped packages: `@scope/pkg` \u2192 dep:npm:@scope/pkg\n * - Subpath imports: `express/Router` \u2192 dep:npm:express\n * - Scoped subpath: `@angular/core/testing` \u2192 dep:npm:@angular/core\n * - Go modules: `github.com/gin-gonic/gin` \u2192 dep:go:github.com/gin-gonic/gin\n * - Go subpath: `github.com/gin-gonic/gin/internal` \u2192 dep:go:github.com/gin-gonic/gin\n */\n private matchImportToDependency(\n specifier: string,\n depByName: Map<string, string>,\n ): string | null {\n if (!specifier) return null;\n\n // Skip relative imports\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null;\n\n // Direct match\n if (depByName.has(specifier)) {\n return depByName.get(specifier)!;\n }\n\n // For scoped packages: @scope/name/subpath \u2192 @scope/name\n if (specifier.startsWith('@')) {\n const parts = specifier.split('/');\n if (parts.length >= 2) {\n const scopedName = `${parts[0]}/${parts[1]}`;\n if (depByName.has(scopedName)) {\n return depByName.get(scopedName)!;\n }\n }\n }\n\n // For unscoped packages: name/subpath \u2192 name\n if (!specifier.startsWith('@')) {\n const slashIdx = specifier.indexOf('/');\n if (slashIdx > 0) {\n const baseName = specifier.slice(0, slashIdx);\n if (depByName.has(baseName)) {\n return depByName.get(baseName)!;\n }\n }\n }\n\n // Go module path matching: try progressively shorter prefixes\n // e.g. github.com/gin-gonic/gin/internal \u2192 github.com/gin-gonic/gin\n if (specifier.includes('.') && specifier.includes('/')) {\n const parts = specifier.split('/');\n for (let i = parts.length - 1; i >= 2; i--) {\n const prefix = parts.slice(0, i).join('/');\n if (depByName.has(prefix)) {\n return depByName.get(prefix)!;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Resolve transitive dependencies up to maxDepth levels.\n *\n * For each ecosystem with a lock file:\n * 1. Parse the lock file to extract dependency relationships\n * 2. Create `depends_on` edges between Dependency_Nodes\n * 3. Traverse up to maxDepth levels of transitive depth\n *\n * If a lock file is missing for an ecosystem, mark all dependencies of that\n * ecosystem with `transitive_status: 'incomplete'` and log a warning.\n *\n * @param maxDepth - Maximum levels of transitive depth to resolve (default: 10)\n */\n async resolveTransitives(maxDepth: number = 10): Promise<TransitiveResult> {\n let dependsOnEdgesCreated = 0;\n const incompleteNodes: string[] = [];\n const rawDb = this.db.getRawDb();\n\n // Get all dependency nodes grouped by ecosystem\n const depRows: Array<{ node_id: string; package_name: string; ecosystem: string }> =\n rawDb.all('SELECT node_id, package_name, ecosystem FROM sec_dependencies');\n\n if (depRows.length === 0) {\n return { dependsOnEdgesCreated, incompleteNodes };\n }\n\n // Group dependencies by ecosystem\n const depsByEcosystem = new Map<string, Map<string, string>>();\n for (const row of depRows) {\n if (!depsByEcosystem.has(row.ecosystem)) {\n depsByEcosystem.set(row.ecosystem, new Map());\n }\n depsByEcosystem.get(row.ecosystem)!.set(row.package_name, row.node_id);\n }\n\n // Discover available lock files\n const lockFiles = discoverLockFiles(this.projectRoot);\n const ecosystemsWithLockFile = new Set(lockFiles.map(lf => lf.ecosystem));\n\n // Mark ecosystems without lock files as incomplete\n for (const [ecosystem, depsMap] of depsByEcosystem) {\n if (!ecosystemsWithLockFile.has(ecosystem)) {\n // No lock file for this ecosystem \u2014 mark all deps as incomplete and log once per ecosystem.\n for (const [, nodeId] of depsMap) {\n rawDb.run(\n `UPDATE sec_dependencies SET transitive_status = 'incomplete' WHERE node_id = ?`,\n [nodeId],\n );\n incompleteNodes.push(nodeId);\n }\n logDebug(\n `[sec:integrator] No lock file parser for ${ecosystem} \u2014 transitive depth unavailable for ` +\n `${depsMap.size} package(s). Direct vulnerability scanning is unaffected.`,\n );\n }\n }\n\n // Parse each lock file and create depends_on edges\n for (const lockFile of lockFiles) {\n const transitives = lockFile.parser(lockFile.lockPath);\n const ecosystemDeps = depsByEcosystem.get(lockFile.ecosystem);\n if (!ecosystemDeps) continue;\n\n if (transitives.length === 0 && ecosystemDeps.size > 0) {\n // Lock file exists but yielded no transitive info (e.g., go.sum has only checksums,\n // pip requirements.txt has no tree). Mark all deps incomplete and log once per ecosystem.\n if (lockFile.ecosystem === 'go' || lockFile.ecosystem === 'python') {\n for (const [, nodeId] of ecosystemDeps) {\n rawDb.run(\n `UPDATE sec_dependencies SET transitive_status = 'incomplete' WHERE node_id = ?`,\n [nodeId],\n );\n incompleteNodes.push(nodeId);\n }\n // Single debug note per ecosystem \u2014 known limitation, not an error.\n logDebug(\n `[sec:integrator] ${lockFile.ecosystem === 'go' ? 'go.sum' : 'requirements.txt'} does not encode a ` +\n `dependency tree \u2014 transitive depth unavailable for ${ecosystemDeps.size} ${lockFile.ecosystem} ` +\n `package(s). Direct vulnerability scanning is unaffected.`,\n );\n }\n continue;\n }\n\n // Build adjacency list from transitive relationships\n const adjacency = new Map<string, Set<string>>();\n for (const { parent, child } of transitives) {\n if (!adjacency.has(parent)) {\n adjacency.set(parent, new Set());\n }\n adjacency.get(parent)!.add(child);\n }\n\n // BFS from each direct dependency up to maxDepth levels\n // Create depends_on edges only between nodes that exist in our dep graph\n const createdEdges = new Set<string>();\n\n for (const [pkgName, nodeId] of ecosystemDeps) {\n const visited = new Set<string>([pkgName]);\n let frontier = [pkgName];\n let depth = 0;\n\n while (frontier.length > 0 && depth < maxDepth) {\n const nextFrontier: string[] = [];\n depth++;\n\n for (const current of frontier) {\n const children = adjacency.get(current);\n if (!children) continue;\n\n for (const child of children) {\n if (visited.has(child)) continue;\n visited.add(child);\n\n // Create depends_on edge if the child is a known dependency node\n const childNodeId = ecosystemDeps.get(child);\n const parentNodeId = current === pkgName ? nodeId : ecosystemDeps.get(current);\n\n if (parentNodeId && childNodeId) {\n const edgeKey = `${parentNodeId}->${childNodeId}`;\n if (!createdEdges.has(edgeKey)) {\n const edge: Edge = {\n source: parentNodeId,\n target: childNodeId,\n kind: 'depends_on',\n confidence: 'inferred',\n confidenceScore: 1.0,\n };\n this.db.insertEdge(edge);\n createdEdges.add(edgeKey);\n dependsOnEdgesCreated++;\n }\n }\n\n nextFrontier.push(child);\n }\n }\n\n frontier = nextFrontier;\n }\n }\n }\n\n return { dependsOnEdgesCreated, incompleteNodes };\n }\n\n /**\n * Remove orphaned Dependency_Nodes and their edges.\n *\n * A Dependency_Node is orphaned if:\n * 1. It has no `declared_in` edges (manifest no longer declares it), OR\n * 2. None of its source manifests still exist on disk\n *\n * Also removes transitive Dependency_Nodes that are no longer reachable from\n * any remaining direct dependency via `depends_on` edges.\n *\n * @param manifestExists - Optional function to check if a manifest path exists.\n * Defaults to fs.existsSync. Useful for testing.\n */\n async cleanup(manifestExists?: (absolutePath: string) => boolean): Promise<CleanupResult> {\n let nodesRemoved = 0;\n let edgesRemoved = 0;\n const rawDb = this.db.getRawDb();\n const checkExists = manifestExists ?? ((p: string) => fs.existsSync(p));\n\n // Step 1: Find dependency nodes that are orphaned because they have no\n // declared_in edges (manifest no longer declares them)\n const orphanedByEdge: Array<{ node_id: string }> = rawDb.all(\n `SELECT sd.node_id FROM sec_dependencies sd\n WHERE NOT EXISTS (\n SELECT 1 FROM edges e\n WHERE e.source = sd.node_id AND e.kind = 'declared_in'\n )`,\n );\n\n const orphanedNodeIds = new Set<string>(orphanedByEdge.map(r => r.node_id));\n\n // Step 2: Also check if source manifests still exist on disk\n const allDeps: Array<{ node_id: string; source_manifests: string }> = rawDb.all(\n `SELECT node_id, source_manifests FROM sec_dependencies`,\n );\n\n for (const dep of allDeps) {\n if (orphanedNodeIds.has(dep.node_id)) continue; // already marked\n\n const sourceManifests: string[] = JSON.parse(dep.source_manifests);\n const hasValidManifest = sourceManifests.some(manifest => {\n const absolutePath = path.isAbsolute(manifest)\n ? manifest\n : path.join(this.projectRoot, manifest);\n return checkExists(absolutePath);\n });\n\n if (!hasValidManifest) {\n orphanedNodeIds.add(dep.node_id);\n }\n }\n\n // Step 3: Find transitive Dependency_Nodes no longer reachable from any\n // remaining direct dependency via `depends_on` edges\n const remainingNodeIds = new Set<string>(\n allDeps\n .filter(d => !orphanedNodeIds.has(d.node_id))\n .map(d => d.node_id),\n );\n\n // BFS from remaining nodes following `depends_on` edges to find all reachable transitives\n const reachableFromDirect = new Set<string>(remainingNodeIds);\n let frontier = [...remainingNodeIds];\n\n while (frontier.length > 0) {\n const placeholders = frontier.map(() => '?').join(',');\n const nextRows: Array<{ target: string }> = rawDb.all(\n `SELECT DISTINCT target FROM edges WHERE source IN (${placeholders}) AND kind = 'depends_on'`,\n frontier,\n );\n\n frontier = [];\n for (const row of nextRows) {\n if (!reachableFromDirect.has(row.target)) {\n reachableFromDirect.add(row.target);\n frontier.push(row.target);\n }\n }\n }\n\n // Any dependency node that is NOT reachable from a remaining direct dependency is also orphaned\n for (const dep of allDeps) {\n if (!orphanedNodeIds.has(dep.node_id) && !reachableFromDirect.has(dep.node_id)) {\n orphanedNodeIds.add(dep.node_id);\n }\n }\n\n if (orphanedNodeIds.size === 0) {\n return { nodesRemoved: 0, edgesRemoved: 0 };\n }\n\n // Step 4: Remove orphaned nodes and their edges\n const orphanedIds = [...orphanedNodeIds];\n\n for (const nodeId of orphanedIds) {\n // Count edges being removed\n const edgeCount = rawDb.get(\n `SELECT COUNT(*) as c FROM edges\n WHERE source = ? OR target = ?`,\n [nodeId, nodeId],\n );\n edgesRemoved += edgeCount?.c ?? 0;\n\n // Delete edges connected to this node\n rawDb.run('DELETE FROM edges WHERE source = ? OR target = ?', [nodeId, nodeId]);\n\n // Remove from sec_dependencies (explicit delete before nodes for clarity)\n rawDb.run('DELETE FROM sec_dependencies WHERE node_id = ?', [nodeId]);\n\n // Remove from nodes table\n rawDb.run('DELETE FROM nodes WHERE id = ?', [nodeId]);\n\n // Clean up FTS entries\n rawDb.run('DELETE FROM nodes_fts WHERE id = ?', [nodeId]);\n\n nodesRemoved++;\n }\n\n return { nodesRemoved, edgesRemoved };\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,SAAoB;AACpB,WAAsB;AAItB,oBAAkC;AAgBlC,SAAS,wBAAwB,UAAmC;AAClE,QAAM,OAAwB,CAAC;AAC/B,MAAI;AACF,UAAM,UAAU,GAAG,aAAa,UAAU,MAAM;AAChD,UAAM,WAAW,KAAK,MAAM,OAAO;AAEnC,QAAI,OAAO,aAAa,YAAY,aAAa,KAAM,QAAO;AAE9D,UAAM,WAAW,SAAS;AAC1B,QAAI,OAAO,aAAa,YAAY,aAAa,MAAM;AAErD,aAAO,0BAA0B,QAAQ;AAAA,IAC3C;AAGA,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAmC,GAAG;AAC9E,UAAI,QAAQ,GAAI;AAChB,UAAI,OAAO,UAAU,YAAY,UAAU,KAAM;AACjD,YAAM,MAAM;AAGZ,YAAM,aAAa,IAAI,QAAQ,qBAAqB,EAAE;AACtD,UAAI,CAAC,WAAY;AAGjB,YAAM,UAAmC,CAAC;AAC1C,iBAAW,SAAS,CAAC,gBAAgB,wBAAwB,kBAAkB,GAAG;AAChF,cAAM,YAAY,IAAI,KAAK;AAC3B,YAAI,OAAO,cAAc,YAAY,cAAc,MAAM;AACvD,iBAAO,OAAO,SAAS,SAAS;AAAA,QAClC;AAAA,MACF;AAEA,iBAAW,aAAa,OAAO,KAAK,OAAO,GAAG;AAC5C,aAAK,KAAK,EAAE,QAAQ,YAAY,OAAO,UAAU,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAKA,SAAS,0BAA0B,UAAoD;AACrF,QAAM,OAAwB,CAAC;AAC/B,QAAM,UAAU,SAAS;AACzB,MAAI,OAAO,YAAY,YAAY,YAAY,KAAM,QAAO;AAE5D,WAAS,QAAQ,YAA2C;AAC1D,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACtD,UAAI,OAAO,UAAU,YAAY,UAAU,KAAM;AACjD,YAAM,MAAM;AAGZ,YAAM,WAAW,IAAI;AACrB,UAAI,OAAO,aAAa,YAAY,aAAa,MAAM;AACrD,mBAAW,aAAa,OAAO,KAAK,QAAmC,GAAG;AACxE,eAAK,KAAK,EAAE,QAAQ,MAAM,OAAO,UAAU,CAAC;AAAA,QAC9C;AAAA,MACF;AAGA,YAAM,SAAS,IAAI;AACnB,UAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,gBAAQ,MAAiC;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAkC;AAC1C,SAAO;AACT;AAMA,SAAS,0BAA0B,UAAmC;AACpE,QAAM,OAAwB,CAAC;AAC/B,MAAI;AACF,UAAM,UAAU,GAAG,aAAa,UAAU,MAAM;AAChD,UAAM,QAAQ,QAAQ,MAAM,IAAI;AAEhC,QAAI,cAA6B;AACjC,QAAI,SAAS;AAEb,eAAW,QAAQ,OAAO;AACxB,YAAM,UAAU,KAAK,KAAK;AAE1B,UAAI,YAAY,eAAe;AAC7B,sBAAc;AACd,iBAAS;AACT;AAAA,MACF;AAEA,UAAI,gBAAgB,MAAM;AACxB,cAAM,YAAY,QAAQ,MAAM,wBAAwB;AACxD,YAAI,WAAW;AACb,wBAAc,UAAU,CAAC;AACzB;AAAA,QACF;AAAA,MACF;AAEA,UAAI,gBAAgB,QAAQ,YAAY,oBAAoB;AAC1D,iBAAS;AACT;AAAA,MACF;AAEA,UAAI,QAAQ;AACV,YAAI,YAAY,KAAK;AACnB,mBAAS;AACT;AAAA,QACF;AAGA,cAAM,WAAW,QAAQ,MAAM,gBAAgB;AAC/C,YAAI,YAAY,aAAa;AAC3B,eAAK,KAAK,EAAE,QAAQ,aAAa,OAAO,SAAS,CAAC,EAAE,CAAC;AAAA,QACvD;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAYA,SAAS,mBAAmB,WAAoC;AAI9D,SAAO,CAAC;AACV;AAOA,SAAS,oBAAoB,WAAoC;AAC/D,SAAO,CAAC;AACV;AAaA,SAAS,kBAAkB,aAAqC;AAC9D,QAAM,YAA4B,CAAC;AAEnC,QAAM,aAAqG;AAAA,IACzG,EAAE,WAAW,OAAO,UAAU,qBAAqB,QAAQ,wBAAwB;AAAA,IACnF,EAAE,WAAW,OAAO,UAAU,aAAa,QAAQ,wBAAwB;AAAA,IAC3E,EAAE,WAAW,SAAS,UAAU,cAAc,QAAQ,0BAA0B;AAAA,IAChF,EAAE,WAAW,MAAM,UAAU,UAAU,QAAQ,mBAAmB;AAAA,IAClE,EAAE,WAAW,UAAU,UAAU,oBAAoB,QAAQ,oBAAoB;AAAA,EACnF;AAEA,aAAW,aAAa,YAAY;AAClC,UAAM,WAAW,KAAK,KAAK,aAAa,UAAU,QAAQ;AAC1D,QAAI,GAAG,WAAW,QAAQ,GAAG;AAC3B,gBAAU,KAAK;AAAA,QACb,WAAW,UAAU;AAAA,QACrB;AAAA,QACA,QAAQ,UAAU;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAIO,MAAM,0BAA0B;AAAA,EAIrC,YAAY,IAAmB,aAAqB;AAClD,SAAK,KAAK;AACV,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,YAAwC;AAC5C,QAAI,sBAAsB;AAC1B,QAAI,yBAAyB;AAE7B,UAAM,QAAQ,KAAK,GAAG,SAAS;AAG/B,UAAM,UACJ,MAAM,IAAI,+DAA+D;AAE3E,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO,EAAE,qBAAqB,uBAAuB;AAAA,IACvD;AAGA,UAAM,YAAY,oBAAI,IAAoB;AAC1C,eAAW,OAAO,SAAS;AACzB,gBAAU,IAAI,IAAI,cAAc,IAAI,OAAO;AAAA,IAC7C;AAMA,UAAM,cAAc,KAAK,GAAG,eAAe,QAAQ;AAGnD,UAAM,qBAAqB,oBAAI,IAAY;AAE3C,eAAW,cAAc,aAAa;AAEpC,YAAM,YAAY,WAAW;AAE7B,YAAM,eAAe,KAAK,wBAAwB,WAAW,SAAS;AACtE,UAAI,CAAC,aAAc;AAGnB,YAAM,aAAa,KAAK,eAAe,OAAO,WAAW,QAAQ;AACjE,UAAI,CAAC,WAAY;AAEjB,YAAM,UAAU,GAAG,UAAU,SAAI,YAAY;AAC7C,UAAI,mBAAmB,IAAI,OAAO,EAAG;AAErC,YAAM,OAAa;AAAA,QACjB,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,MAAM,WAAW;AAAA,QACjB,YAAY;AAAA,QACZ,iBAAiB;AAAA,MACnB;AACA,WAAK,GAAG,WAAW,IAAI;AACvB,yBAAmB,IAAI,OAAO;AAC9B;AAAA,IACF;AAMA,UAAM,YAAY,oBAAI,IAAI,CAAC,YAAY,UAAU,SAAS,YAAY,YAAY,UAAU,CAAC;AAC7F,UAAM,kBAAkB,oBAAI,IAAY;AAExC,eAAW,cAAc,aAAa;AACpC,YAAM,YAAY,WAAW;AAC7B,YAAM,eAAe,KAAK,wBAAwB,WAAW,SAAS;AACtE,UAAI,CAAC,aAAc;AAGnB,YAAM,YAAY,KAAK,GAAG,eAAe,WAAW,QAAQ;AAC5D,YAAM,cAAc,UAAU,OAAO,OAAK,UAAU,IAAI,EAAE,IAAI,CAAC;AAC/D,UAAI,YAAY,WAAW,EAAG;AAG9B,YAAM,YAAY,YAAY,IAAI,OAAK,EAAE,EAAE;AAC3C,YAAM,QAAQ,KAAK,GAAG,iBAAiB,SAAS;AAEhD,iBAAW,QAAQ,OAAO;AAExB,aACG,KAAK,SAAS,WAAW,KAAK,SAAS,iBACxC,KAAK,WAAW,WAAW,MAC3B,UAAU,SAAS,KAAK,MAAM,GAC9B;AACA,gBAAM,SAAS,GAAG,KAAK,MAAM,SAAI,YAAY;AAC7C,cAAI,gBAAgB,IAAI,MAAM,EAAG;AAEjC,gBAAM,UAAgB;AAAA,YACpB,QAAQ,KAAK;AAAA,YACb,QAAQ;AAAA,YACR,MAAM;AAAA,YACN,YAAY;AAAA,YACZ,iBAAiB;AAAA,UACnB;AACA,eAAK,GAAG,WAAW,OAAO;AAC1B,0BAAgB,IAAI,MAAM;AAC1B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,qBAAqB,uBAAuB;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,eAAe,OAAY,UAAiC;AAClE,UAAM,MAAM,MAAM;AAAA,MAChB;AAAA,MACA,CAAC,QAAQ,QAAQ;AAAA,IACnB;AACA,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,wBACN,WACA,WACe;AACf,QAAI,CAAC,UAAW,QAAO;AAGvB,QAAI,UAAU,WAAW,GAAG,KAAK,UAAU,WAAW,GAAG,EAAG,QAAO;AAGnE,QAAI,UAAU,IAAI,SAAS,GAAG;AAC5B,aAAO,UAAU,IAAI,SAAS;AAAA,IAChC;AAGA,QAAI,UAAU,WAAW,GAAG,GAAG;AAC7B,YAAM,QAAQ,UAAU,MAAM,GAAG;AACjC,UAAI,MAAM,UAAU,GAAG;AACrB,cAAM,aAAa,GAAG,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AAC1C,YAAI,UAAU,IAAI,UAAU,GAAG;AAC7B,iBAAO,UAAU,IAAI,UAAU;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAGA,QAAI,CAAC,UAAU,WAAW,GAAG,GAAG;AAC9B,YAAM,WAAW,UAAU,QAAQ,GAAG;AACtC,UAAI,WAAW,GAAG;AAChB,cAAM,WAAW,UAAU,MAAM,GAAG,QAAQ;AAC5C,YAAI,UAAU,IAAI,QAAQ,GAAG;AAC3B,iBAAO,UAAU,IAAI,QAAQ;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAIA,QAAI,UAAU,SAAS,GAAG,KAAK,UAAU,SAAS,GAAG,GAAG;AACtD,YAAM,QAAQ,UAAU,MAAM,GAAG;AACjC,eAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,cAAM,SAAS,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACzC,YAAI,UAAU,IAAI,MAAM,GAAG;AACzB,iBAAO,UAAU,IAAI,MAAM;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,mBAAmB,WAAmB,IAA+B;AACzE,QAAI,wBAAwB;AAC5B,UAAM,kBAA4B,CAAC;AACnC,UAAM,QAAQ,KAAK,GAAG,SAAS;AAG/B,UAAM,UACJ,MAAM,IAAI,+DAA+D;AAE3E,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO,EAAE,uBAAuB,gBAAgB;AAAA,IAClD;AAGA,UAAM,kBAAkB,oBAAI,IAAiC;AAC7D,eAAW,OAAO,SAAS;AACzB,UAAI,CAAC,gBAAgB,IAAI,IAAI,SAAS,GAAG;AACvC,wBAAgB,IAAI,IAAI,WAAW,oBAAI,IAAI,CAAC;AAAA,MAC9C;AACA,sBAAgB,IAAI,IAAI,SAAS,EAAG,IAAI,IAAI,cAAc,IAAI,OAAO;AAAA,IACvE;AAGA,UAAM,YAAY,kBAAkB,KAAK,WAAW;AACpD,UAAM,yBAAyB,IAAI,IAAI,UAAU,IAAI,QAAM,GAAG,SAAS,CAAC;AAGxE,eAAW,CAAC,WAAW,OAAO,KAAK,iBAAiB;AAClD,UAAI,CAAC,uBAAuB,IAAI,SAAS,GAAG;AAE1C,mBAAW,CAAC,EAAE,MAAM,KAAK,SAAS;AAChC,gBAAM;AAAA,YACJ;AAAA,YACA,CAAC,MAAM;AAAA,UACT;AACA,0BAAgB,KAAK,MAAM;AAAA,QAC7B;AACA;AAAA,UACE,4CAA4C,SAAS,4CAClD,QAAQ,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAGA,eAAW,YAAY,WAAW;AAChC,YAAM,cAAc,SAAS,OAAO,SAAS,QAAQ;AACrD,YAAM,gBAAgB,gBAAgB,IAAI,SAAS,SAAS;AAC5D,UAAI,CAAC,cAAe;AAEpB,UAAI,YAAY,WAAW,KAAK,cAAc,OAAO,GAAG;AAGtD,YAAI,SAAS,cAAc,QAAQ,SAAS,cAAc,UAAU;AAClE,qBAAW,CAAC,EAAE,MAAM,KAAK,eAAe;AACtC,kBAAM;AAAA,cACJ;AAAA,cACA,CAAC,MAAM;AAAA,YACT;AACA,4BAAgB,KAAK,MAAM;AAAA,UAC7B;AAEA;AAAA,YACE,oBAAoB,SAAS,cAAc,OAAO,WAAW,kBAAkB,8EACzB,cAAc,IAAI,IAAI,SAAS,SAAS;AAAA,UAEhG;AAAA,QACF;AACA;AAAA,MACF;AAGA,YAAM,YAAY,oBAAI,IAAyB;AAC/C,iBAAW,EAAE,QAAQ,MAAM,KAAK,aAAa;AAC3C,YAAI,CAAC,UAAU,IAAI,MAAM,GAAG;AAC1B,oBAAU,IAAI,QAAQ,oBAAI,IAAI,CAAC;AAAA,QACjC;AACA,kBAAU,IAAI,MAAM,EAAG,IAAI,KAAK;AAAA,MAClC;AAIA,YAAM,eAAe,oBAAI,IAAY;AAErC,iBAAW,CAAC,SAAS,MAAM,KAAK,eAAe;AAC7C,cAAM,UAAU,oBAAI,IAAY,CAAC,OAAO,CAAC;AACzC,YAAI,WAAW,CAAC,OAAO;AACvB,YAAI,QAAQ;AAEZ,eAAO,SAAS,SAAS,KAAK,QAAQ,UAAU;AAC9C,gBAAM,eAAyB,CAAC;AAChC;AAEA,qBAAW,WAAW,UAAU;AAC9B,kBAAM,WAAW,UAAU,IAAI,OAAO;AACtC,gBAAI,CAAC,SAAU;AAEf,uBAAW,SAAS,UAAU;AAC5B,kBAAI,QAAQ,IAAI,KAAK,EAAG;AACxB,sBAAQ,IAAI,KAAK;AAGjB,oBAAM,cAAc,cAAc,IAAI,KAAK;AAC3C,oBAAM,eAAe,YAAY,UAAU,SAAS,cAAc,IAAI,OAAO;AAE7E,kBAAI,gBAAgB,aAAa;AAC/B,sBAAM,UAAU,GAAG,YAAY,KAAK,WAAW;AAC/C,oBAAI,CAAC,aAAa,IAAI,OAAO,GAAG;AAC9B,wBAAM,OAAa;AAAA,oBACjB,QAAQ;AAAA,oBACR,QAAQ;AAAA,oBACR,MAAM;AAAA,oBACN,YAAY;AAAA,oBACZ,iBAAiB;AAAA,kBACnB;AACA,uBAAK,GAAG,WAAW,IAAI;AACvB,+BAAa,IAAI,OAAO;AACxB;AAAA,gBACF;AAAA,cACF;AAEA,2BAAa,KAAK,KAAK;AAAA,YACzB;AAAA,UACF;AAEA,qBAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,uBAAuB,gBAAgB;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,QAAQ,gBAA4E;AACxF,QAAI,eAAe;AACnB,QAAI,eAAe;AACnB,UAAM,QAAQ,KAAK,GAAG,SAAS;AAC/B,UAAM,cAAc,mBAAmB,CAAC,MAAc,GAAG,WAAW,CAAC;AAIrE,UAAM,iBAA6C,MAAM;AAAA,MACvD;AAAA;AAAA;AAAA;AAAA;AAAA,IAKF;AAEA,UAAM,kBAAkB,IAAI,IAAY,eAAe,IAAI,OAAK,EAAE,OAAO,CAAC;AAG1E,UAAM,UAAgE,MAAM;AAAA,MAC1E;AAAA,IACF;AAEA,eAAW,OAAO,SAAS;AACzB,UAAI,gBAAgB,IAAI,IAAI,OAAO,EAAG;AAEtC,YAAM,kBAA4B,KAAK,MAAM,IAAI,gBAAgB;AACjE,YAAM,mBAAmB,gBAAgB,KAAK,cAAY;AACxD,cAAM,eAAe,KAAK,WAAW,QAAQ,IACzC,WACA,KAAK,KAAK,KAAK,aAAa,QAAQ;AACxC,eAAO,YAAY,YAAY;AAAA,MACjC,CAAC;AAED,UAAI,CAAC,kBAAkB;AACrB,wBAAgB,IAAI,IAAI,OAAO;AAAA,MACjC;AAAA,IACF;AAIA,UAAM,mBAAmB,IAAI;AAAA,MAC3B,QACG,OAAO,OAAK,CAAC,gBAAgB,IAAI,EAAE,OAAO,CAAC,EAC3C,IAAI,OAAK,EAAE,OAAO;AAAA,IACvB;AAGA,UAAM,sBAAsB,IAAI,IAAY,gBAAgB;AAC5D,QAAI,WAAW,CAAC,GAAG,gBAAgB;AAEnC,WAAO,SAAS,SAAS,GAAG;AAC1B,YAAM,eAAe,SAAS,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AACrD,YAAM,WAAsC,MAAM;AAAA,QAChD,sDAAsD,YAAY;AAAA,QAClE;AAAA,MACF;AAEA,iBAAW,CAAC;AACZ,iBAAW,OAAO,UAAU;AAC1B,YAAI,CAAC,oBAAoB,IAAI,IAAI,MAAM,GAAG;AACxC,8BAAoB,IAAI,IAAI,MAAM;AAClC,mBAAS,KAAK,IAAI,MAAM;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAGA,eAAW,OAAO,SAAS;AACzB,UAAI,CAAC,gBAAgB,IAAI,IAAI,OAAO,KAAK,CAAC,oBAAoB,IAAI,IAAI,OAAO,GAAG;AAC9E,wBAAgB,IAAI,IAAI,OAAO;AAAA,MACjC;AAAA,IACF;AAEA,QAAI,gBAAgB,SAAS,GAAG;AAC9B,aAAO,EAAE,cAAc,GAAG,cAAc,EAAE;AAAA,IAC5C;AAGA,UAAM,cAAc,CAAC,GAAG,eAAe;AAEvC,eAAW,UAAU,aAAa;AAEhC,YAAM,YAAY,MAAM;AAAA,QACtB;AAAA;AAAA,QAEA,CAAC,QAAQ,MAAM;AAAA,MACjB;AACA,sBAAgB,WAAW,KAAK;AAGhC,YAAM,IAAI,oDAAoD,CAAC,QAAQ,MAAM,CAAC;AAG9E,YAAM,IAAI,kDAAkD,CAAC,MAAM,CAAC;AAGpE,YAAM,IAAI,kCAAkC,CAAC,MAAM,CAAC;AAGpD,YAAM,IAAI,sCAAsC,CAAC,MAAM,CAAC;AAExD;AAAA,IACF;AAEA,WAAO,EAAE,cAAc,aAAa;AAAA,EACtC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/types.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph Type Definitions\n */\n\nexport type NodeKind =\n | 'file' | 'module' | 'class' | 'struct' | 'interface' | 'trait' | 'protocol'\n | 'function' | 'method' | 'property' | 'field' | 'variable' | 'constant'\n | 'enum' | 'enum_member' | 'type_alias' | 'namespace' | 'parameter'\n | 'import' | 'export' | 'route' | 'component'\n | 'dependency' | 'vulnerability';\n\nexport type EdgeKind =\n | 'contains' | 'calls' | 'imports' | 'exports' | 'extends' | 'implements'\n | 'references' | 'type_of' | 'returns' | 'instantiates' | 'overrides' | 'decorates'\n | 'has_vulnerability' | 'depends_on' | 'declared_in';\n\nexport type Language =\n | 'typescript' | 'javascript' | 'tsx' | 'jsx' | 'python' | 'go' | 'rust'\n | 'java' | 'c' | 'cpp' | 'csharp' | 'php' | 'ruby' | 'swift' | 'kotlin'\n | 'dart' | 'svelte' | 'pascal' | 'liquid' | 'elixir'\n | 'scala' | 'lua' | 'zig' | 'bash' | 'ocaml' | 'elm' | 'solidity' | 'vue' | 'objc'\n | 'yaml' | 'hcl' | 'css' | 'scss' | 'html'\n | 'jupyter'\n | 'rescript' | 'sql' | 'r' | 'julia' | 'powershell' | 'perl' | 'astro' | 'gdscript' | 'nix' | 'verilog'\n | 'unknown';\n\nexport interface Node {\n id: string;\n kind: NodeKind;\n name: string;\n qualifiedName: string;\n filePath: string;\n language: Language;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n docstring?: string;\n signature?: string;\n visibility?: 'public' | 'private' | 'protected' | 'internal';\n isExported?: boolean;\n isAsync?: boolean;\n isStatic?: boolean;\n isAbstract?: boolean;\n decorators?: string[];\n typeParameters?: string[];\n updatedAt: number;\n}\n\nexport interface Edge {\n source: string;\n target: string;\n kind: EdgeKind;\n metadata?: Record<string, unknown>;\n line?: number;\n column?: number;\n confidence?: 'extracted' | 'inferred' | 'ambiguous';\n confidenceScore?: number;\n}\n\nexport interface FileRecord {\n path: string;\n contentHash: string;\n language: Language;\n fileSize: number;\n symbolCount: number;\n indexedAt: number;\n}\n\nexport interface SearchResult {\n node: Node;\n score: number;\n matchType: 'exact' | 'prefix' | 'fuzzy' | 'semantic';\n}\n\nexport interface SearchOptions {\n kinds?: NodeKind[];\n languages?: Language[];\n limit?: number;\n minScore?: number;\n}\n\nexport interface Subgraph {\n nodes: Node[];\n edges: Edge[];\n entryPoints: string[];\n}\n\nexport interface NodeContext {\n node: Node;\n ancestors: Node[];\n children: Node[];\n callers: Node[];\n callees: Node[];\n}\n\nexport interface NodeMetrics {\n incomingEdgeCount: number;\n outgoingEdgeCount: number;\n callCount: number;\n callerCount: number;\n childCount: number;\n}\n\nexport interface GraphStats {\n files: number;\n nodes: number;\n edges: number;\n nodesByKind: Record<string, number>;\n filesByLanguage: Record<string, number>;\n dbSizeBytes: number;\n embeddingCount: number;\n embeddableNodeCount: number;\n embeddingsEnabled: boolean;\n embeddingModel: string;\n useVecIndex: boolean;\n semanticEngine: 'cosine' | 'sqlite-vec' | 'orama' | 'pglite' | 'lancedb' | 'qdrant' | 'typesense';\n vecIndexCount: number;\n engineFallback: string | null;\n frameworks: string[];\n architectureEnabled: boolean;\n architectureStats?: { packages: number; layers: number; packageDeps: number };\n}\n\nexport interface TaskContext {\n task: string;\n entryPoints: Node[];\n relatedNodes: Node[];\n edges: Edge[];\n codeSnippets: Map<string, string>;\n summary: string;\n}\n\nexport interface IndexProgress {\n phase: 'scanning' | 'parsing' | 'storing' | 'resolving' | 'detecting frameworks' | 'embeddings' | 'architecture' | 'security' | 'docs' | 'data' | 'patterns' | 'retrying';\n current: number;\n total: number;\n currentFile?: string;\n meta?: Record<string, unknown>;\n}\n\nexport interface IndexResult {\n success: boolean;\n filesIndexed: number;\n nodesCreated: number;\n edgesCreated: number;\n errors: string[];\n duration: number;\n}\n\nexport interface SyncResult {\n added: string[];\n modified: string[];\n removed: string[];\n nodesCreated: number;\n nodesUpdated: number;\n nodesRemoved: number;\n edgesCreated: number;\n edgesRemoved: number;\n filesScanned: number;\n errors: string[];\n duration: number;\n}\n\n// \u2500\u2500 Error Classes (re-exported from src/errors.ts for backward compatibility) \u2500\u2500\n\nexport {\n KiroGraphError,\n FileError,\n ParseError,\n DatabaseError,\n ConfigError,\n} from './errors';\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph Type Definitions\n */\n\nexport type NodeKind =\n | 'file' | 'module' | 'class' | 'struct' | 'interface' | 'trait' | 'protocol'\n | 'function' | 'method' | 'property' | 'field' | 'variable' | 'constant'\n | 'enum' | 'enum_member' | 'type_alias' | 'namespace' | 'parameter'\n | 'import' | 'export' | 'route' | 'component'\n | 'dependency' | 'vulnerability';\n\nexport type EdgeKind =\n | 'contains' | 'calls' | 'imports' | 'exports' | 'extends' | 'implements'\n | 'references' | 'type_of' | 'returns' | 'instantiates' | 'overrides' | 'decorates'\n | 'has_vulnerability' | 'depends_on' | 'declared_in';\n\nexport type Language =\n | 'typescript' | 'javascript' | 'tsx' | 'jsx' | 'python' | 'go' | 'rust'\n | 'java' | 'c' | 'cpp' | 'csharp' | 'php' | 'ruby' | 'swift' | 'kotlin'\n | 'dart' | 'svelte' | 'pascal' | 'liquid' | 'elixir'\n | 'scala' | 'lua' | 'zig' | 'bash' | 'ocaml' | 'elm' | 'solidity' | 'vue' | 'objc'\n | 'yaml' | 'hcl' | 'css' | 'scss' | 'html'\n | 'jupyter'\n | 'rescript' | 'sql' | 'r' | 'julia' | 'powershell' | 'perl' | 'astro' | 'gdscript' | 'nix' | 'verilog'\n | 'haskell'\n | 'unknown';\n\nexport interface Node {\n id: string;\n kind: NodeKind;\n name: string;\n qualifiedName: string;\n filePath: string;\n language: Language;\n startLine: number;\n endLine: number;\n startColumn: number;\n endColumn: number;\n docstring?: string;\n signature?: string;\n visibility?: 'public' | 'private' | 'protected' | 'internal';\n isExported?: boolean;\n isAsync?: boolean;\n isStatic?: boolean;\n isAbstract?: boolean;\n decorators?: string[];\n typeParameters?: string[];\n updatedAt: number;\n}\n\nexport interface Edge {\n source: string;\n target: string;\n kind: EdgeKind;\n metadata?: Record<string, unknown>;\n line?: number;\n column?: number;\n confidence?: 'extracted' | 'inferred' | 'ambiguous';\n confidenceScore?: number;\n}\n\nexport interface FileRecord {\n path: string;\n contentHash: string;\n language: Language;\n fileSize: number;\n symbolCount: number;\n indexedAt: number;\n}\n\nexport interface SearchResult {\n node: Node;\n score: number;\n matchType: 'exact' | 'prefix' | 'fuzzy' | 'semantic';\n}\n\nexport interface SearchOptions {\n kinds?: NodeKind[];\n languages?: Language[];\n limit?: number;\n minScore?: number;\n}\n\nexport interface Subgraph {\n nodes: Node[];\n edges: Edge[];\n entryPoints: string[];\n}\n\nexport interface NodeContext {\n node: Node;\n ancestors: Node[];\n children: Node[];\n callers: Node[];\n callees: Node[];\n}\n\nexport interface NodeMetrics {\n incomingEdgeCount: number;\n outgoingEdgeCount: number;\n callCount: number;\n callerCount: number;\n childCount: number;\n}\n\nexport interface GraphStats {\n files: number;\n nodes: number;\n edges: number;\n nodesByKind: Record<string, number>;\n filesByLanguage: Record<string, number>;\n dbSizeBytes: number;\n embeddingCount: number;\n embeddableNodeCount: number;\n embeddingsEnabled: boolean;\n embeddingModel: string;\n useVecIndex: boolean;\n semanticEngine: 'cosine' | 'turboquant' | 'turbovec' | 'sqlite-vec' | 'orama' | 'pglite' | 'lancedb' | 'qdrant' | 'typesense';\n turbovecBits?: number;\n vecIndexCount: number;\n engineFallback: string | null;\n frameworks: string[];\n architectureEnabled: boolean;\n architectureStats?: { packages: number; layers: number; packageDeps: number };\n}\n\nexport interface TaskContext {\n task: string;\n entryPoints: Node[];\n relatedNodes: Node[];\n edges: Edge[];\n codeSnippets: Map<string, string>;\n summary: string;\n}\n\nexport interface IndexProgress {\n phase: 'scanning' | 'parsing' | 'storing' | 'resolving' | 'detecting frameworks' | 'embeddings' | 'architecture' | 'security' | 'docs' | 'data' | 'patterns' | 'retrying';\n current: number;\n total: number;\n currentFile?: string;\n meta?: Record<string, unknown>;\n}\n\nexport interface IndexResult {\n success: boolean;\n filesIndexed: number;\n nodesCreated: number;\n edgesCreated: number;\n errors: string[];\n duration: number;\n}\n\nexport interface SyncResult {\n added: string[];\n modified: string[];\n removed: string[];\n nodesCreated: number;\n nodesUpdated: number;\n nodesRemoved: number;\n edgesCreated: number;\n edgesRemoved: number;\n filesScanned: number;\n errors: string[];\n duration: number;\n}\n\n// \u2500\u2500 Error Classes (re-exported from src/errors.ts for backward compatibility) \u2500\u2500\n\nexport {\n KiroGraphError,\n FileError,\n ParseError,\n DatabaseError,\n ConfigError,\n} from './errors';\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwKA,oBAMO;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/vectors/index.js
CHANGED
|
@@ -41,6 +41,8 @@ var import_pglite_index = require("./pglite-index");
|
|
|
41
41
|
var import_lancedb_index = require("./lancedb-index");
|
|
42
42
|
var import_qdrant_index = require("./qdrant-index");
|
|
43
43
|
var import_typesense_index = require("./typesense-index");
|
|
44
|
+
var import_turboquant_index = require("./turboquant-index");
|
|
45
|
+
var import_turbovec_index = require("./turbovec-index");
|
|
44
46
|
const DEFAULT_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
45
47
|
const DEFAULT_EMBEDDING_DIM = 768;
|
|
46
48
|
const GLOBAL_MODELS_DIR = path.join((0, import_os.homedir)(), ".kirograph", "models");
|
|
@@ -109,6 +111,8 @@ class VectorManager {
|
|
|
109
111
|
this.lancedbIndex = null;
|
|
110
112
|
this.qdrantIndex = null;
|
|
111
113
|
this.typesenseIndex = null;
|
|
114
|
+
this.turboquantIndex = null;
|
|
115
|
+
this.turbovecIndex = null;
|
|
112
116
|
this._engineFallback = null;
|
|
113
117
|
}
|
|
114
118
|
isInitialized() {
|
|
@@ -240,6 +244,26 @@ class VectorManager {
|
|
|
240
244
|
this._engineFallback = reason ? `typesense initialization failed: ${reason}` : "typesense unavailable \u2014 run: npm install typesense";
|
|
241
245
|
(0, import_errors.logDebug)("VectorManager: Typesense unavailable, falling back to in-process cosine");
|
|
242
246
|
}
|
|
247
|
+
} else if (engine === "turboquant") {
|
|
248
|
+
const bits = this.config.turboquantBits ?? 3;
|
|
249
|
+
this.turboquantIndex = new import_turboquant_index.TurboQuantIndex(kirographDir, "turboquant.bin", embeddingDim, bits);
|
|
250
|
+
await this.turboquantIndex.initialize();
|
|
251
|
+
if (this.turboquantIndex.isAvailable()) {
|
|
252
|
+
(0, import_errors.logDebug)("VectorManager: TurboQuant ANN index ready", { bits });
|
|
253
|
+
} else {
|
|
254
|
+
this._engineFallback = "turboquant unavailable \u2014 run: npm install turboquant-js";
|
|
255
|
+
(0, import_errors.logDebug)("VectorManager: TurboQuant unavailable, falling back to in-process cosine");
|
|
256
|
+
}
|
|
257
|
+
} else if (engine === "turbovec") {
|
|
258
|
+
const bits = this.config.turbovecBits ?? 4;
|
|
259
|
+
this.turbovecIndex = new import_turbovec_index.TurboVecIndex(kirographDir, "turbovec.tvim", embeddingDim, bits);
|
|
260
|
+
await this.turbovecIndex.initialize();
|
|
261
|
+
if (this.turbovecIndex.isAvailable()) {
|
|
262
|
+
(0, import_errors.logDebug)("VectorManager: TurboVec ANN index ready", { bits });
|
|
263
|
+
} else {
|
|
264
|
+
this._engineFallback = "turbovec unavailable \u2014 build it: cd native/turbovec-node && npm install && npm run build";
|
|
265
|
+
(0, import_errors.logDebug)("VectorManager: TurboVec unavailable, falling back to in-process cosine");
|
|
266
|
+
}
|
|
243
267
|
}
|
|
244
268
|
}
|
|
245
269
|
}
|
|
@@ -258,7 +282,7 @@ class VectorManager {
|
|
|
258
282
|
const text = `search_document: ${nodeToText(node)}`;
|
|
259
283
|
const output = await this.pipeline(text, { pooling: "mean", normalize: true });
|
|
260
284
|
const embedding = toFloat32Array(output.data);
|
|
261
|
-
if (!this.vecIndex?.isAvailable() && !this.oramaIndex?.isAvailable() && !this.pgliteIndex?.isAvailable() && !this.lancedbIndex?.isAvailable() && !this.qdrantIndex?.isAvailable() && !this.typesenseIndex?.isAvailable()) {
|
|
285
|
+
if (!this.vecIndex?.isAvailable() && !this.oramaIndex?.isAvailable() && !this.pgliteIndex?.isAvailable() && !this.lancedbIndex?.isAvailable() && !this.qdrantIndex?.isAvailable() && !this.typesenseIndex?.isAvailable() && !this.turboquantIndex?.isAvailable() && !this.turbovecIndex?.isAvailable()) {
|
|
262
286
|
this.db.storeEmbedding(node.id, embedding, this.config.embeddingModel || DEFAULT_MODEL);
|
|
263
287
|
}
|
|
264
288
|
this.vecIndex?.upsert(node.id, embedding);
|
|
@@ -267,12 +291,16 @@ class VectorManager {
|
|
|
267
291
|
if (this.lancedbIndex?.isAvailable()) await this.lancedbIndex.upsert(node, embedding);
|
|
268
292
|
if (this.qdrantIndex?.isAvailable()) await this.qdrantIndex.upsert(node, embedding);
|
|
269
293
|
if (this.typesenseIndex?.isAvailable()) await this.typesenseIndex.upsert(node, embedding);
|
|
294
|
+
this.turboquantIndex?.upsert(node.id, embedding);
|
|
295
|
+
this.turbovecIndex?.upsert(node.id, embedding);
|
|
270
296
|
} catch (err) {
|
|
271
297
|
(0, import_errors.logWarn)("Failed to embed node", { nodeId: node.id, error: String(err) });
|
|
272
298
|
}
|
|
273
299
|
}
|
|
274
300
|
/** Number of entries currently in the active ANN/hybrid index (0 when not in use). */
|
|
275
301
|
async vecIndexCount() {
|
|
302
|
+
if (this.turbovecIndex?.isAvailable()) return this.turbovecIndex.count();
|
|
303
|
+
if (this.turboquantIndex?.isAvailable()) return this.turboquantIndex.count();
|
|
276
304
|
if (this.vecIndex?.isAvailable()) return this.vecIndex.count();
|
|
277
305
|
if (this.oramaIndex?.isAvailable()) return this.oramaIndex.count();
|
|
278
306
|
if (this.pgliteIndex?.isAvailable()) return this.pgliteIndex.count();
|
|
@@ -295,6 +323,8 @@ class VectorManager {
|
|
|
295
323
|
if (this.lancedbIndex?.isAvailable()) await this.lancedbIndex.delete(id);
|
|
296
324
|
if (this.qdrantIndex?.isAvailable()) await this.qdrantIndex.delete(id);
|
|
297
325
|
if (this.typesenseIndex?.isAvailable()) await this.typesenseIndex.delete(id);
|
|
326
|
+
this.turboquantIndex?.delete(id);
|
|
327
|
+
this.turbovecIndex?.delete(id);
|
|
298
328
|
}
|
|
299
329
|
}
|
|
300
330
|
/**
|
|
@@ -319,7 +349,7 @@ class VectorManager {
|
|
|
319
349
|
onProgress?.(-1, totalEmbeddable);
|
|
320
350
|
}
|
|
321
351
|
const existingIds = new Set(
|
|
322
|
-
this.typesenseIndex?.isAvailable() ? await this.typesenseIndex.getEmbeddedNodeIds() : this.qdrantIndex?.isAvailable() ? await this.qdrantIndex.getEmbeddedNodeIds() : this.lancedbIndex?.isAvailable() ? await this.lancedbIndex.getEmbeddedNodeIds() : this.pgliteIndex?.isAvailable() ? await this.pgliteIndex.getEmbeddedNodeIds() : this.oramaIndex?.isAvailable() ? await this.oramaIndex.getEmbeddedNodeIds() : this.vecIndex?.isAvailable() ? this.vecIndex.getEmbeddedNodeIds() : this.db.getEmbeddedNodeIds()
|
|
352
|
+
this.typesenseIndex?.isAvailable() ? await this.typesenseIndex.getEmbeddedNodeIds() : this.qdrantIndex?.isAvailable() ? await this.qdrantIndex.getEmbeddedNodeIds() : this.lancedbIndex?.isAvailable() ? await this.lancedbIndex.getEmbeddedNodeIds() : this.pgliteIndex?.isAvailable() ? await this.pgliteIndex.getEmbeddedNodeIds() : this.oramaIndex?.isAvailable() ? await this.oramaIndex.getEmbeddedNodeIds() : this.turbovecIndex?.isAvailable() ? this.turbovecIndex.getEmbeddedIds() : this.turboquantIndex?.isAvailable() ? this.turboquantIndex.getEmbeddedIds() : this.vecIndex?.isAvailable() ? this.vecIndex.getEmbeddedNodeIds() : this.db.getEmbeddedNodeIds()
|
|
323
353
|
);
|
|
324
354
|
const PAGE_SIZE = 2e3;
|
|
325
355
|
let pageOffset = 0;
|
|
@@ -364,7 +394,7 @@ class VectorManager {
|
|
|
364
394
|
for (let j = 0; j < batch.length; j++) {
|
|
365
395
|
const node = batch[j];
|
|
366
396
|
const embedding = flat.slice(j * dim, (j + 1) * dim);
|
|
367
|
-
if (!this.vecIndex?.isAvailable() && !this.oramaIndex?.isAvailable() && !this.pgliteIndex?.isAvailable() && !this.lancedbIndex?.isAvailable() && !this.qdrantIndex?.isAvailable() && !this.typesenseIndex?.isAvailable()) {
|
|
397
|
+
if (!this.vecIndex?.isAvailable() && !this.oramaIndex?.isAvailable() && !this.pgliteIndex?.isAvailable() && !this.lancedbIndex?.isAvailable() && !this.qdrantIndex?.isAvailable() && !this.typesenseIndex?.isAvailable() && !this.turboquantIndex?.isAvailable() && !this.turbovecIndex?.isAvailable()) {
|
|
368
398
|
this.db.storeEmbedding(node.id, embedding, modelId);
|
|
369
399
|
}
|
|
370
400
|
this.vecIndex?.upsert(node.id, embedding);
|
|
@@ -379,6 +409,8 @@ class VectorManager {
|
|
|
379
409
|
tsNodes.push(node);
|
|
380
410
|
tsEmbeddings.push(embedding);
|
|
381
411
|
}
|
|
412
|
+
this.turboquantIndex?.upsert(node.id, embedding);
|
|
413
|
+
this.turbovecIndex?.upsert(node.id, embedding);
|
|
382
414
|
}
|
|
383
415
|
if (qdNodes.length > 0) await this.qdrantIndex.bulkUpsert(qdNodes, qdEmbeddings);
|
|
384
416
|
if (tsNodes.length > 0) await this.typesenseIndex.bulkUpsert(tsNodes, tsEmbeddings);
|
|
@@ -421,6 +453,32 @@ class VectorManager {
|
|
|
421
453
|
onProgress?.(processed, totalPending);
|
|
422
454
|
}
|
|
423
455
|
if (this.oramaIndex?.isAvailable()) await this.oramaIndex.save();
|
|
456
|
+
if (this.turboquantIndex?.isAvailable()) {
|
|
457
|
+
await this.turboquantIndex.save();
|
|
458
|
+
if (this.projectRoot) {
|
|
459
|
+
const kirographDir = path.join(this.projectRoot, ".kirograph");
|
|
460
|
+
const stats = this.turboquantIndex.memoryStats();
|
|
461
|
+
const count = this.turboquantIndex.count();
|
|
462
|
+
const dim = this.config.embeddingDim ?? DEFAULT_EMBEDDING_DIM;
|
|
463
|
+
const rawBytes = count * dim * 4;
|
|
464
|
+
(0, import_turboquant_index.writeTurboQuantStats)(kirographDir, {
|
|
465
|
+
count,
|
|
466
|
+
dim,
|
|
467
|
+
bits: this.config.turboquantBits ?? 3,
|
|
468
|
+
compressionRatio: stats.compressionRatio,
|
|
469
|
+
actualBytes: stats.actualBytes,
|
|
470
|
+
rawBytes,
|
|
471
|
+
savedBytes: rawBytes - stats.actualBytes
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (this.turbovecIndex?.isAvailable()) {
|
|
476
|
+
await this.turbovecIndex.save();
|
|
477
|
+
(0, import_errors.logDebug)("VectorManager: TurboVec index saved", {
|
|
478
|
+
count: this.turbovecIndex.count(),
|
|
479
|
+
bits: this.config.turbovecBits ?? 4
|
|
480
|
+
});
|
|
481
|
+
}
|
|
424
482
|
(0, import_errors.logDebug)(`VectorManager: embedding complete \u2014 ${processed} processed, final batch size: ${currentBatchSize}`);
|
|
425
483
|
return processed;
|
|
426
484
|
}
|
|
@@ -450,6 +508,10 @@ class VectorManager {
|
|
|
450
508
|
nodeIds = await this.qdrantIndex.search(queryVec, topN);
|
|
451
509
|
} else if (this.typesenseIndex?.isAvailable()) {
|
|
452
510
|
nodeIds = await this.typesenseIndex.search(queryVec, topN);
|
|
511
|
+
} else if (this.turbovecIndex?.isAvailable()) {
|
|
512
|
+
nodeIds = this.turbovecIndex.search(queryVec, topN);
|
|
513
|
+
} else if (this.turboquantIndex?.isAvailable()) {
|
|
514
|
+
nodeIds = this.turboquantIndex.search(queryVec, topN);
|
|
453
515
|
} else if (this.vecIndex?.isAvailable()) {
|
|
454
516
|
nodeIds = this.vecIndex.search(queryVec, topN);
|
|
455
517
|
} else {
|
|
@@ -471,6 +533,7 @@ class VectorManager {
|
|
|
471
533
|
close() {
|
|
472
534
|
this.qdrantIndex?.close();
|
|
473
535
|
this.typesenseIndex?.close();
|
|
536
|
+
this.turbovecIndex?.close();
|
|
474
537
|
}
|
|
475
538
|
}
|
|
476
539
|
// Annotate the CommonJS export names for ESM import in node:
|