kirograph 0.20.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/bin/installer/index.js +29 -0
  2. package/dist/bin/installer/index.js.map +2 -2
  3. package/dist/bin/kirograph.js +1 -1
  4. package/dist/extraction/extractor.js +26 -3
  5. package/dist/extraction/extractor.js.map +2 -2
  6. package/dist/extraction/grammars.js +1 -0
  7. package/dist/extraction/grammars.js.map +2 -2
  8. package/dist/extraction/languages.js +5 -1
  9. package/dist/extraction/languages.js.map +2 -2
  10. package/dist/patterns/library/code-eval-elixir.yaml +11 -0
  11. package/dist/patterns/library/command-injection-c.yaml +11 -0
  12. package/dist/patterns/library/command-injection-go.yaml +21 -3
  13. package/dist/patterns/library/command-injection-kotlin.yaml +10 -0
  14. package/dist/patterns/library/command-injection-php.yaml +11 -0
  15. package/dist/patterns/library/command-injection-ruby.yaml +11 -0
  16. package/dist/patterns/library/command-injection-rust.yaml +11 -0
  17. package/dist/patterns/library/dangerous-eval-bash.yaml +11 -0
  18. package/dist/patterns/library/dangerous-eval-php.yaml +11 -0
  19. package/dist/patterns/library/dangerous-eval-ruby.yaml +11 -0
  20. package/dist/patterns/library/dynamic-load-lua.yaml +10 -0
  21. package/dist/patterns/library/os-cmd-elixir.yaml +16 -0
  22. package/dist/patterns/library/os-execute-lua.yaml +10 -0
  23. package/dist/patterns/library/path-traversal-go.yaml +4 -4
  24. package/dist/patterns/library/process-launch-swift.yaml +11 -0
  25. package/dist/patterns/library/process-run-dart.yaml +15 -0
  26. package/dist/patterns/library/source-injection-bash.yaml +11 -0
  27. package/dist/patterns/library/sql-injection-dart.yaml +9 -0
  28. package/dist/patterns/library/sql-injection-kotlin.yaml +11 -0
  29. package/dist/patterns/library/sql-injection-php.yaml +9 -0
  30. package/dist/patterns/library/sql-injection-swift.yaml +11 -0
  31. package/dist/patterns/library/subshell-injection-ruby.yaml +8 -0
  32. package/dist/patterns/library/unsafe-block-rust.yaml +8 -0
  33. package/dist/patterns/library/unsafe-string-c.yaml +11 -0
  34. package/dist/patterns/runner.js +45 -27
  35. package/dist/patterns/runner.js.map +2 -2
  36. package/dist/security/integrator.js +8 -8
  37. package/dist/security/integrator.js.map +2 -2
  38. package/dist/types.js.map +2 -2
  39. package/dist/vectors/turboquant-index.js +261 -0
  40. package/dist/vectors/turboquant-index.js.map +7 -0
  41. package/package.json +20 -1
@@ -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,2BAA2B,oBAAI,IAAI;AAAA,EACvC;AAAA,EAAc;AAAA,EAAc;AAAA,EAAO;AAAA,EACnC;AAAA,EAAU;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAK;AAAA,EAAO;AAAA,EAAM;AAAA,EAAS;AAAA,EAC3D;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAO;AAAA,EACtD;AAAA,EAAW;AAAA,EAAO;AAAA,EAAK;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAU;AACrD,CAAC;AAID,IAAI,QAAgC;AACpC,IAAI,kBAAkB;AAGtB,eAAe,UAA+B;AAC5C,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AAEF,oBAAgB,gBAAgB;AAEhC,YAAQ,QAAQ,gBAAgB;AAChC,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,WAAO,yBAAyB,IAAI,SAAS,YAAY,CAAC;AAAA,EAC5D;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;AAEnB,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;AAEnB,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;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;",
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": "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",
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 [pkgName, nodeId] of depsMap) {
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 [pkgName, nodeId] of ecosystemDeps) {
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": "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",
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": "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",
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;AAsKA,oBAMO;",
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' | '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;AAuKA,oBAMO;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var turboquant_index_exports = {};
30
+ __export(turboquant_index_exports, {
31
+ TurboQuantIndex: () => TurboQuantIndex,
32
+ readTurboQuantStats: () => readTurboQuantStats,
33
+ writeTurboQuantStats: () => writeTurboQuantStats
34
+ });
35
+ module.exports = __toCommonJS(turboquant_index_exports);
36
+ var path = __toESM(require("path"));
37
+ var fs = __toESM(require("fs"));
38
+ var import_errors = require("../errors");
39
+ const STATS_FILE = "turboquant-stats.json";
40
+ function readTurboQuantStats(kirographDir) {
41
+ try {
42
+ const p = path.join(kirographDir, STATS_FILE);
43
+ if (!fs.existsSync(p)) return null;
44
+ return JSON.parse(fs.readFileSync(p, "utf8"));
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ function writeTurboQuantStats(kirographDir, patch) {
50
+ const p = path.join(kirographDir, STATS_FILE);
51
+ const tmp = p + ".tmp";
52
+ try {
53
+ const existing = readTurboQuantStats(kirographDir) ?? {
54
+ count: 0,
55
+ dim: 768,
56
+ bits: 3,
57
+ compressionRatio: 1,
58
+ actualBytes: 0,
59
+ rawBytes: 0,
60
+ savedBytes: 0,
61
+ memEnabled: false,
62
+ memCount: 0,
63
+ memActualBytes: 0,
64
+ memRawBytes: 0,
65
+ docsEnabled: false,
66
+ docsCount: 0,
67
+ docsActualBytes: 0,
68
+ docsRawBytes: 0,
69
+ updatedAt: 0
70
+ };
71
+ const updated = { ...existing, ...patch, updatedAt: Math.floor(Date.now() / 1e3) };
72
+ fs.writeFileSync(tmp, JSON.stringify(updated, null, 2));
73
+ fs.renameSync(tmp, p);
74
+ } catch (err) {
75
+ (0, import_errors.logWarn)("TurboQuantIndex: failed to write stats file", { error: String(err) });
76
+ try {
77
+ fs.unlinkSync(tmp);
78
+ } catch {
79
+ }
80
+ }
81
+ }
82
+ class TurboQuantIndex {
83
+ constructor(kirographDir, binName, dim, bits = 3) {
84
+ this.kirographDir = kirographDir;
85
+ this.binName = binName;
86
+ this.dim = dim;
87
+ this.bits = bits;
88
+ this.index = null;
89
+ this._available = false;
90
+ }
91
+ isAvailable() {
92
+ return this._available;
93
+ }
94
+ /**
95
+ * Load turboquant-js and restore index from disk if the .bin file exists.
96
+ * Silent no-op when the optional dependency is missing.
97
+ */
98
+ async initialize() {
99
+ if (this._available) return;
100
+ let TQModule;
101
+ try {
102
+ TQModule = require("turboquant-js");
103
+ } catch {
104
+ (0, import_errors.logDebug)("TurboQuantIndex: turboquant-js not installed \u2014 turboquant engine unavailable");
105
+ return;
106
+ }
107
+ const VectorIndex = TQModule?.VectorIndex ?? TQModule?.default?.VectorIndex ?? (typeof TQModule === "function" ? TQModule : null);
108
+ if (!VectorIndex || typeof VectorIndex !== "function") {
109
+ (0, import_errors.logWarn)("TurboQuantIndex: unrecognised turboquant-js module shape \u2014 turboquant engine unavailable");
110
+ return;
111
+ }
112
+ const binPath = path.join(this.kirographDir, this.binName);
113
+ try {
114
+ if (fs.existsSync(binPath) && typeof VectorIndex.fromBuffer === "function") {
115
+ const nodeBuf = fs.readFileSync(binPath);
116
+ const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
117
+ this.index = VectorIndex.fromBuffer(ab, { dimension: this.dim, bits: this.bits });
118
+ (0, import_errors.logDebug)("TurboQuantIndex: restored from disk", { binPath });
119
+ } else {
120
+ this.index = new VectorIndex({ dimension: this.dim, bits: this.bits });
121
+ (0, import_errors.logDebug)("TurboQuantIndex: new index", { dim: this.dim, bits: this.bits });
122
+ }
123
+ this._available = true;
124
+ } catch (err) {
125
+ (0, import_errors.logError)("TurboQuantIndex: initialization failed", { error: String(err) });
126
+ this._available = false;
127
+ this.index = null;
128
+ }
129
+ }
130
+ /** Add or update an embedding (turboquant-js compresses at index time). */
131
+ upsert(id, vec) {
132
+ if (!this._available || !this.index) return;
133
+ try {
134
+ this.index.add(id, vec);
135
+ } catch (err) {
136
+ (0, import_errors.logWarn)("TurboQuantIndex: upsert failed", { id, error: String(err) });
137
+ }
138
+ }
139
+ /** Remove an entry from the index (permanent via turboquant-js remove()). */
140
+ delete(id) {
141
+ if (!this._available || !this.index) return;
142
+ try {
143
+ this.index.remove(id);
144
+ } catch (err) {
145
+ (0, import_errors.logWarn)("TurboQuantIndex: delete failed", { id, error: String(err) });
146
+ }
147
+ }
148
+ /** ANN search: returns IDs sorted by descending similarity. */
149
+ search(queryVec, topN = 10) {
150
+ return this.searchWithScores(queryVec, topN).map((r) => r.id);
151
+ }
152
+ /**
153
+ * ANN search: returns [{id, score}] sorted by descending similarity.
154
+ * Used by MemoryVectorManager and DocsVectorManager to preserve scores for
155
+ * hybrid merge.
156
+ */
157
+ searchWithScores(queryVec, topN = 10) {
158
+ if (!this._available || !this.index) return [];
159
+ try {
160
+ const raw = this.index.search(queryVec, topN);
161
+ return this._normaliseSearchResults(raw).slice(0, topN);
162
+ } catch (err) {
163
+ (0, import_errors.logWarn)("TurboQuantIndex: search failed", { error: String(err) });
164
+ return [];
165
+ }
166
+ }
167
+ /**
168
+ * Persist the compressed index to disk atomically.
169
+ * Called once at the end of `kirograph index` — not on every upsert.
170
+ */
171
+ async save() {
172
+ if (!this._available || !this.index) return;
173
+ if (typeof this.index.toBuffer !== "function") {
174
+ (0, import_errors.logDebug)("TurboQuantIndex: toBuffer() not available \u2014 index not persisted");
175
+ return;
176
+ }
177
+ const binPath = path.join(this.kirographDir, this.binName);
178
+ const tmp = binPath + ".tmp";
179
+ try {
180
+ const raw = this.index.toBuffer();
181
+ const buf = raw instanceof ArrayBuffer ? Buffer.from(raw) : raw;
182
+ fs.writeFileSync(tmp, buf);
183
+ fs.renameSync(tmp, binPath);
184
+ (0, import_errors.logDebug)("TurboQuantIndex: saved", { binPath, bytes: buf.length });
185
+ } catch (err) {
186
+ (0, import_errors.logWarn)("TurboQuantIndex: save failed", { error: String(err) });
187
+ try {
188
+ fs.unlinkSync(tmp);
189
+ } catch {
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Memory usage stats from the compressed index.
195
+ * Falls back to a formula-based estimate when memoryUsage is unavailable.
196
+ */
197
+ memoryStats() {
198
+ if (this._available && this.index?.memoryUsage) {
199
+ return this.index.memoryUsage;
200
+ }
201
+ const n = this.count();
202
+ const rawBytes = n * this.dim * 4;
203
+ const actualBytes = Math.ceil(n * this.dim * this.bits / 8);
204
+ return {
205
+ totalBits: actualBytes * 8,
206
+ bitsPerVector: this.bits * this.dim,
207
+ compressionRatio: actualBytes > 0 ? rawBytes / actualBytes : 1,
208
+ actualBytes
209
+ };
210
+ }
211
+ count() {
212
+ if (!this._available || !this.index) return 0;
213
+ try {
214
+ if (this.index.idToIndex instanceof Map) return this.index.idToIndex.size;
215
+ if (typeof this.index.size === "number") return this.index.size;
216
+ } catch {
217
+ }
218
+ return 0;
219
+ }
220
+ /** All IDs currently in the index. */
221
+ getEmbeddedIds() {
222
+ if (!this._available || !this.index) return [];
223
+ try {
224
+ if (this.index.idToIndex instanceof Map) {
225
+ return [...this.index.idToIndex.keys()];
226
+ }
227
+ } catch {
228
+ }
229
+ return [];
230
+ }
231
+ close() {
232
+ this.index = null;
233
+ this._available = false;
234
+ }
235
+ // ── Private ────────────────────────────────────────────────────────────────
236
+ _normaliseSearchResults(raw) {
237
+ if (!Array.isArray(raw)) return [];
238
+ return raw.map((item, rank) => {
239
+ if (item && typeof item === "object" && !Array.isArray(item)) {
240
+ const obj = item;
241
+ const id = obj["id"] ?? obj["key"] ?? "";
242
+ const score = obj["score"] ?? obj["similarity"] ?? obj["distance"] ?? 1 - rank / raw.length;
243
+ return { id, score: typeof score === "number" ? score : 1 - rank / raw.length };
244
+ }
245
+ if (Array.isArray(item) && item.length >= 2) {
246
+ return { id: String(item[0]), score: typeof item[1] === "number" ? item[1] : 1 - rank / raw.length };
247
+ }
248
+ if (typeof item === "string") {
249
+ return { id: item, score: 1 - rank / raw.length };
250
+ }
251
+ return null;
252
+ }).filter((r) => r !== null && r.id.length > 0);
253
+ }
254
+ }
255
+ // Annotate the CommonJS export names for ESM import in node:
256
+ 0 && (module.exports = {
257
+ TurboQuantIndex,
258
+ readTurboQuantStats,
259
+ writeTurboQuantStats
260
+ });
261
+ //# sourceMappingURL=turboquant-index.js.map