code-auditor-mcp 2.6.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -147
- package/dist/analyzers/analyzerUtils.d.ts +13 -11
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +22 -56
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/dataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/dataAccessAnalyzer.js +50 -24
- package/dist/analyzers/dataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +4 -1
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +210 -111
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/dryAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/dryAnalyzer.js +190 -58
- package/dist/analyzers/dryAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts +20 -0
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/invariantsAnalyzer.js +148 -0
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -0
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +2 -2
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/schemaAnalyzer.d.ts +2 -0
- package/dist/analyzers/schemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/schemaAnalyzer.js +306 -147
- package/dist/analyzers/schemaAnalyzer.js.map +1 -1
- package/dist/analyzers/solidAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/solidAnalyzer.js +145 -119
- package/dist/analyzers/solidAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +3 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +41 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +8 -8
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +6 -6
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +367 -24
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +740 -18
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB-enhanced.d.ts +35 -13
- package/dist/codeIndexDB-enhanced.d.ts.map +1 -1
- package/dist/codeIndexDB-enhanced.js +311 -214
- package/dist/codeIndexDB-enhanced.js.map +1 -1
- package/dist/{codeIndexDb.d.ts → codeIndexDB.d.ts} +51 -224
- package/dist/codeIndexDB.d.ts.map +1 -0
- package/dist/codeIndexDB.js +2042 -0
- package/dist/codeIndexDB.js.map +1 -0
- package/dist/codeIndexService.d.ts.map +1 -1
- package/dist/codeIndexService.js.map +1 -1
- package/dist/componentScanner.d.ts +14 -8
- package/dist/componentScanner.d.ts.map +1 -1
- package/dist/componentScanner.js +136 -113
- package/dist/componentScanner.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +10 -7
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/fingerprint.d.ts +15 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +32 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/functionScanner.d.ts +5 -0
- package/dist/functionScanner.d.ts.map +1 -1
- package/dist/functionScanner.js +417 -341
- package/dist/functionScanner.js.map +1 -1
- package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.js.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
- package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
- package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/invariants/index.d.ts +9 -0
- package/dist/invariants/index.d.ts.map +1 -0
- package/dist/invariants/index.js +4 -0
- package/dist/invariants/index.js.map +1 -0
- package/dist/invariants/invariant-rules.schema.json +137 -0
- package/dist/invariants/ruleEngine.d.ts +32 -0
- package/dist/invariants/ruleEngine.d.ts.map +1 -0
- package/dist/invariants/ruleEngine.js +536 -0
- package/dist/invariants/ruleEngine.js.map +1 -0
- package/dist/invariants/ruleValidator.d.ts +17 -0
- package/dist/invariants/ruleValidator.d.ts.map +1 -0
- package/dist/invariants/ruleValidator.js +264 -0
- package/dist/invariants/ruleValidator.js.map +1 -0
- package/dist/invariants/types.d.ts +78 -0
- package/dist/invariants/types.d.ts.map +1 -0
- package/dist/invariants/types.js +5 -0
- package/dist/invariants/types.js.map +1 -0
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/LanguageRegistry.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +1 -1
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.d.ts +102 -0
- package/dist/languages/adapterBridge.d.ts.map +1 -0
- package/dist/languages/adapterBridge.js +382 -0
- package/dist/languages/adapterBridge.js.map +1 -0
- package/dist/languages/go/GoAdapter.d.ts +37 -26
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +560 -281
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/index.d.ts +5 -2
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +11 -9
- package/dist/languages/index.js.map +1 -1
- package/dist/languages/tree-sitter/converter.d.ts +43 -0
- package/dist/languages/tree-sitter/converter.d.ts.map +1 -0
- package/dist/languages/tree-sitter/converter.js +150 -0
- package/dist/languages/tree-sitter/converter.js.map +1 -0
- package/dist/languages/tree-sitter/parser.d.ts +36 -0
- package/dist/languages/tree-sitter/parser.d.ts.map +1 -0
- package/dist/languages/tree-sitter/parser.js +114 -0
- package/dist/languages/tree-sitter/parser.js.map +1 -0
- package/dist/languages/types.d.ts +11 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +92 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +951 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -0
- package/dist/mcp-tools/projectTasks.d.ts +58 -22
- package/dist/mcp-tools/projectTasks.d.ts.map +1 -1
- package/dist/mcp-tools/projectTasks.js +172 -10
- package/dist/mcp-tools/projectTasks.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +21 -21
- package/dist/mcp-tools/whitelistTools.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +130 -126
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp-ui-simple.js +1 -1
- package/dist/mcp-ui-simple.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +1376 -1181
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +4 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +49 -2
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/mcpToolErrors.d.ts.map +1 -1
- package/dist/mcpToolErrors.js.map +1 -1
- package/dist/native/ast-grep-napi.darwin-arm64.node +0 -0
- package/dist/native/ast-grep-napi.darwin-x64.node +0 -0
- package/dist/native/ast-grep-napi.linux-arm64-gnu.node +0 -0
- package/dist/native/ast-grep-napi.linux-arm64-musl.node +0 -0
- package/dist/native/ast-grep-napi.linux-x64-gnu.node +0 -0
- package/dist/native/ast-grep-napi.linux-x64-musl.node +0 -0
- package/dist/native/ast-grep-napi.win32-x64-msvc.node +0 -0
- package/dist/native-bootstrap.d.ts +15 -0
- package/dist/native-bootstrap.d.ts.map +1 -0
- package/dist/native-bootstrap.js +75 -0
- package/dist/native-bootstrap.js.map +1 -0
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/reportGenerator.d.ts.map +1 -1
- package/dist/reporting/reportGenerator.js +4 -0
- package/dist/reporting/reportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts +39 -0
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -0
- package/dist/reporting/sarifReportGenerator.js +229 -0
- package/dist/reporting/sarifReportGenerator.js.map +1 -0
- package/dist/search/QueryParser.d.ts +41 -0
- package/dist/search/QueryParser.d.ts.map +1 -1
- package/dist/search/QueryParser.js +200 -19
- package/dist/search/QueryParser.js.map +1 -1
- package/dist/services/CodeMapGenerator.d.ts.map +1 -1
- package/dist/services/CodeMapGenerator.js +4 -4
- package/dist/services/CodeMapGenerator.js.map +1 -1
- package/dist/services/ProjectTaskRepository.d.ts +15 -2
- package/dist/services/ProjectTaskRepository.d.ts.map +1 -1
- package/dist/services/ProjectTaskRepository.js +16 -1
- package/dist/services/ProjectTaskRepository.js.map +1 -1
- package/dist/services/SchemaParser.d.ts.map +1 -1
- package/dist/services/SchemaParser.js +5 -8
- package/dist/services/SchemaParser.js.map +1 -1
- package/dist/services/auditJobService.d.ts +1 -1
- package/dist/services/projectTaskHelpers.js.map +1 -1
- package/dist/services/whitelistService.d.ts.map +1 -1
- package/dist/services/whitelistService.js +1 -1
- package/dist/services/whitelistService.js.map +1 -1
- package/dist/tool-registry.d.ts +79 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +139 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/types/projectTask.d.ts +4 -0
- package/dist/types/projectTask.d.ts.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/InteractivePrompts.d.ts.map +1 -1
- package/dist/ui/InteractivePrompts.js.map +1 -1
- package/dist/utils/astParser.d.ts +31 -38
- package/dist/utils/astParser.d.ts.map +1 -1
- package/dist/utils/astParser.js +138 -196
- package/dist/utils/astParser.js.map +1 -1
- package/dist/utils/astUtils.d.ts +87 -58
- package/dist/utils/astUtils.d.ts.map +1 -1
- package/dist/utils/astUtils.js +852 -581
- package/dist/utils/astUtils.js.map +1 -1
- package/dist/utils/componentPatterns.js.map +1 -1
- package/dist/utils/componentResponsibility.d.ts +13 -9
- package/dist/utils/componentResponsibility.d.ts.map +1 -1
- package/dist/utils/componentResponsibility.js +236 -206
- package/dist/utils/componentResponsibility.js.map +1 -1
- package/dist/utils/dependencyExtractor.d.ts +11 -6
- package/dist/utils/dependencyExtractor.d.ts.map +1 -1
- package/dist/utils/dependencyExtractor.js +200 -119
- package/dist/utils/dependencyExtractor.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +1 -1
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/jsDocParser.d.ts +27 -30
- package/dist/utils/jsDocParser.d.ts.map +1 -1
- package/dist/utils/jsDocParser.js +271 -207
- package/dist/utils/jsDocParser.js.map +1 -1
- package/dist/utils/reactDetection.d.ts +27 -12
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +331 -219
- package/dist/utils/reactDetection.js.map +1 -1
- package/dist/workers/auditWorker.js +17 -3
- package/dist/workers/auditWorker.js.map +1 -1
- package/package.json +54 -51
- package/dist/adapters/GoAdapter.d.ts +0 -53
- package/dist/adapters/GoAdapter.d.ts.map +0 -1
- package/dist/adapters/GoAdapter.js +0 -611
- package/dist/adapters/GoAdapter.js.map +0 -1
- package/dist/adapters/GoConfig.d.ts +0 -35
- package/dist/adapters/GoConfig.d.ts.map +0 -1
- package/dist/adapters/GoConfig.js +0 -155
- package/dist/adapters/GoConfig.js.map +0 -1
- package/dist/adapters/LanguageAdapter.d.ts +0 -135
- package/dist/adapters/LanguageAdapter.d.ts.map +0 -1
- package/dist/adapters/LanguageAdapter.js +0 -47
- package/dist/adapters/LanguageAdapter.js.map +0 -1
- package/dist/adapters/TypeScriptAdapter.d.ts +0 -43
- package/dist/adapters/TypeScriptAdapter.d.ts.map +0 -1
- package/dist/adapters/TypeScriptAdapter.js +0 -386
- package/dist/adapters/TypeScriptAdapter.js.map +0 -1
- package/dist/adapters/UniversalAnalyzer.d.ts +0 -42
- package/dist/adapters/UniversalAnalyzer.d.ts.map +0 -1
- package/dist/adapters/UniversalAnalyzer.js +0 -103
- package/dist/adapters/UniversalAnalyzer.js.map +0 -1
- package/dist/adapters/UniversalSOLIDAnalyzer.d.ts +0 -48
- package/dist/adapters/UniversalSOLIDAnalyzer.d.ts.map +0 -1
- package/dist/adapters/UniversalSOLIDAnalyzer.js +0 -178
- package/dist/adapters/UniversalSOLIDAnalyzer.js.map +0 -1
- package/dist/adapters/solidAnalyzerCompat.d.ts +0 -21
- package/dist/adapters/solidAnalyzerCompat.d.ts.map +0 -1
- package/dist/adapters/solidAnalyzerCompat.js +0 -78
- package/dist/adapters/solidAnalyzerCompat.js.map +0 -1
- package/dist/analyzers/dataAccessAnalyzerCompat.d.ts +0 -20
- package/dist/analyzers/dataAccessAnalyzerCompat.d.ts.map +0 -1
- package/dist/analyzers/dataAccessAnalyzerCompat.js +0 -59
- package/dist/analyzers/dataAccessAnalyzerCompat.js.map +0 -1
- package/dist/analyzers/documentationAnalyzerCompat.d.ts +0 -20
- package/dist/analyzers/documentationAnalyzerCompat.d.ts.map +0 -1
- package/dist/analyzers/documentationAnalyzerCompat.js +0 -52
- package/dist/analyzers/documentationAnalyzerCompat.js.map +0 -1
- package/dist/analyzers/dryAnalyzerCompat.d.ts +0 -20
- package/dist/analyzers/dryAnalyzerCompat.d.ts.map +0 -1
- package/dist/analyzers/dryAnalyzerCompat.js +0 -44
- package/dist/analyzers/dryAnalyzerCompat.js.map +0 -1
- package/dist/analyzers/schemaAnalyzerCompat.d.ts +0 -20
- package/dist/analyzers/schemaAnalyzerCompat.d.ts.map +0 -1
- package/dist/analyzers/schemaAnalyzerCompat.js +0 -69
- package/dist/analyzers/schemaAnalyzerCompat.js.map +0 -1
- package/dist/analyzers/solidAnalyzerCompat.d.ts +0 -20
- package/dist/analyzers/solidAnalyzerCompat.d.ts.map +0 -1
- package/dist/analyzers/solidAnalyzerCompat.js +0 -78
- package/dist/analyzers/solidAnalyzerCompat.js.map +0 -1
- package/dist/codeIndexDb.d.ts.map +0 -1
- package/dist/codeIndexDb.js +0 -2102
- package/dist/codeIndexDb.js.map +0 -1
- package/dist/languages/typescript/TypeScriptAdapter.d.ts +0 -108
- package/dist/languages/typescript/TypeScriptAdapter.d.ts.map +0 -1
- package/dist/languages/typescript/TypeScriptAdapter.js +0 -567
- package/dist/languages/typescript/TypeScriptAdapter.js.map +0 -1
- package/dist/languages/typescript/test-extraction.d.ts +0 -2
- package/dist/languages/typescript/test-extraction.d.ts.map +0 -1
- package/dist/languages/typescript/test-extraction.js +0 -36
- package/dist/languages/typescript/test-extraction.js.map +0 -1
- package/dist/mcp-hybrid.d.ts +0 -10
- package/dist/mcp-hybrid.d.ts.map +0 -1
- package/dist/mcp-hybrid.js +0 -41
- package/dist/mcp-hybrid.js.map +0 -1
- package/dist/mcp-index.d.ts +0 -13
- package/dist/mcp-index.d.ts.map +0 -1
- package/dist/mcp-index.js +0 -136
- package/dist/mcp-index.js.map +0 -1
- package/dist/mcp-standalone.d.ts +0 -3
- package/dist/mcp-standalone.d.ts.map +0 -1
- package/dist/mcp-standalone.js +0 -1324
- package/dist/mcp-standalone.js.map +0 -1
- package/dist/mcp-ui-server.d.ts +0 -27
- package/dist/mcp-ui-server.d.ts.map +0 -1
- package/dist/mcp-ui-server.js +0 -484
- package/dist/mcp-ui-server.js.map +0 -1
- package/dist/test-multi-strategy-search.d.ts +0 -3
- package/dist/test-multi-strategy-search.d.ts.map +0 -1
- package/dist/test-multi-strategy-search.js +0 -170
- package/dist/test-multi-strategy-search.js.map +0 -1
|
@@ -0,0 +1,2042 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Index Database using SQLite (better-sqlite3) + FTS5
|
|
3
|
+
* Replaces LokiJS + FlexSearch with durable, transactional storage.
|
|
4
|
+
*/
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { ProjectTaskRepository } from './services/ProjectTaskRepository.js';
|
|
10
|
+
import { QueryParser, compileToSQL } from './search/QueryParser.js';
|
|
11
|
+
import { getPersistedStorageRoot, resolvePersistedIndexPath } from './dataPaths.js';
|
|
12
|
+
import { ContextualError, getErrnoCode } from './mcpToolErrors.js';
|
|
13
|
+
import { WhitelistType, WhitelistStatus } from './types/whitelist.js';
|
|
14
|
+
// ── Content hash ────────────────────────────────────────────────────────
|
|
15
|
+
function computeContentHash(body, signature) {
|
|
16
|
+
const normalized = (body ?? '').replace(/\s+/g, ' ').trim() + '|' + (signature ?? '').trim();
|
|
17
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
class SqliteCollectionAdapter {
|
|
20
|
+
db;
|
|
21
|
+
tableName;
|
|
22
|
+
constructor(db, tableName) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.tableName = tableName;
|
|
25
|
+
}
|
|
26
|
+
/** Return all rows, or rows matching the query. */
|
|
27
|
+
find(query) {
|
|
28
|
+
if (!query) {
|
|
29
|
+
return this.db.prepare(`SELECT *, rowid as "$loki" FROM "${this.tableName}"`).all()
|
|
30
|
+
.map(r => this.unbindRow(r));
|
|
31
|
+
}
|
|
32
|
+
const clauses = [];
|
|
33
|
+
const params = {};
|
|
34
|
+
for (const [key, value] of Object.entries(query)) {
|
|
35
|
+
if (value === null || value === undefined) {
|
|
36
|
+
clauses.push(`"${key}" IS NULL`);
|
|
37
|
+
}
|
|
38
|
+
else if (key === '$loki' && typeof value === 'object' && value.$in) {
|
|
39
|
+
// Handle $loki: { $in: [...] }
|
|
40
|
+
const placeholders = value.$in.map((_, i) => `@in_${i}`);
|
|
41
|
+
clauses.push(`rowid IN (${placeholders.join(', ')})`);
|
|
42
|
+
value.$in.forEach((v, i) => { params[`in_${i}`] = this.bindable(v); });
|
|
43
|
+
}
|
|
44
|
+
else if (key === 'expiresAt' && typeof value === 'object' && value.$lt) {
|
|
45
|
+
clauses.push(`"expiresAt" < @expiresAt`);
|
|
46
|
+
params['expiresAt'] = value.$lt instanceof Date ? value.$lt.toISOString() : String(value.$lt);
|
|
47
|
+
}
|
|
48
|
+
else if (key === 'timestamp' && typeof value === 'object' && value.$lt) {
|
|
49
|
+
clauses.push(`"timestamp" < @timestamp`);
|
|
50
|
+
params['timestamp'] = value.$lt instanceof Date ? value.$lt.toISOString() : String(value.$lt);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
clauses.push(`"${key}" = @${key}`);
|
|
54
|
+
params[key] = this.bindable(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const sql = `SELECT *, rowid as "$loki" FROM "${this.tableName}"${clauses.length ? ' WHERE ' + clauses.join(' AND ') : ''}`;
|
|
58
|
+
return this.db.prepare(sql).all(params).map(r => this.unbindRow(r));
|
|
59
|
+
}
|
|
60
|
+
findOne(query) {
|
|
61
|
+
const clauses = [];
|
|
62
|
+
const params = {};
|
|
63
|
+
for (const [key, value] of Object.entries(query)) {
|
|
64
|
+
if (value === null || value === undefined) {
|
|
65
|
+
clauses.push(`"${key}" IS NULL`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
clauses.push(`"${key}" = @${key}`);
|
|
69
|
+
params[key] = this.bindable(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const sql = `SELECT *, rowid as "$loki" FROM "${this.tableName}"${clauses.length ? ' WHERE ' + clauses.join(' AND ') : ''} LIMIT 1`;
|
|
73
|
+
const row = this.db.prepare(sql).get(params) ?? null;
|
|
74
|
+
return row ? this.unbindRow(row) : null;
|
|
75
|
+
}
|
|
76
|
+
/** Convert a doc value to something SQLite can bind (primitives + Buffer, null). */
|
|
77
|
+
bindable(v) {
|
|
78
|
+
if (v === null || v === undefined)
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof v === 'object' && !Buffer.isBuffer(v))
|
|
81
|
+
return JSON.stringify(v);
|
|
82
|
+
return v;
|
|
83
|
+
}
|
|
84
|
+
/** Try to parse a value that looks like serialized JSON back to its native form. */
|
|
85
|
+
unbindable(v) {
|
|
86
|
+
if (typeof v === 'string' && (v.startsWith('[') || v.startsWith('{'))) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(v);
|
|
89
|
+
}
|
|
90
|
+
catch { /* not JSON, leave as string */ }
|
|
91
|
+
}
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
unbindRow(row) {
|
|
95
|
+
if (!row)
|
|
96
|
+
return row;
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(row)) {
|
|
99
|
+
out[k] = this.unbindable(v);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
insert(doc) {
|
|
104
|
+
const keys = Object.keys(doc);
|
|
105
|
+
const vals = keys.map(k => `@${k}`);
|
|
106
|
+
const sql = `INSERT INTO "${this.tableName}" ("${keys.join('", "')}") VALUES (${vals.join(', ')})`;
|
|
107
|
+
const params = {};
|
|
108
|
+
for (const k of keys)
|
|
109
|
+
params[k] = this.bindable(doc[k]);
|
|
110
|
+
const info = this.db.prepare(sql).run(params);
|
|
111
|
+
return { ...doc, $loki: Number(info.lastInsertRowid) };
|
|
112
|
+
}
|
|
113
|
+
update(doc) {
|
|
114
|
+
const keys = Object.keys(doc).filter(k => k !== '$loki' && k !== 'meta');
|
|
115
|
+
const sets = keys.map(k => `"${k}" = @${k}`);
|
|
116
|
+
const params = {};
|
|
117
|
+
for (const k of keys)
|
|
118
|
+
params[k] = this.bindable(doc[k]);
|
|
119
|
+
params['_rowid'] = doc.$loki;
|
|
120
|
+
this.db.prepare(`UPDATE "${this.tableName}" SET ${sets.join(', ')} WHERE rowid = @_rowid`).run(params);
|
|
121
|
+
}
|
|
122
|
+
remove(doc) {
|
|
123
|
+
if (doc.$loki !== undefined) {
|
|
124
|
+
this.db.prepare(`DELETE FROM "${this.tableName}" WHERE rowid = @_rowid`).run({ _rowid: doc.$loki });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Remove all rows matching a query. */
|
|
128
|
+
findAndRemove(query) {
|
|
129
|
+
const clauses = [];
|
|
130
|
+
const params = {};
|
|
131
|
+
for (const [key, value] of Object.entries(query)) {
|
|
132
|
+
if (value === null || value === undefined) {
|
|
133
|
+
clauses.push(`"${key}" IS NULL`);
|
|
134
|
+
}
|
|
135
|
+
else if (key === 'timestamp' && typeof value === 'object' && value.$lt) {
|
|
136
|
+
clauses.push(`"timestamp" < @timestamp`);
|
|
137
|
+
params['timestamp'] = value.$lt instanceof Date ? value.$lt.toISOString() : String(value.$lt);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
clauses.push(`"${key}" = @${key}`);
|
|
141
|
+
params[key] = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.db.prepare(`DELETE FROM "${this.tableName}"${clauses.length ? ' WHERE ' + clauses.join(' AND ') : ''}`).run(params);
|
|
145
|
+
}
|
|
146
|
+
clear() {
|
|
147
|
+
this.db.prepare(`DELETE FROM "${this.tableName}"`).run();
|
|
148
|
+
}
|
|
149
|
+
chain() {
|
|
150
|
+
return {
|
|
151
|
+
_table: this.tableName,
|
|
152
|
+
_db: this.db,
|
|
153
|
+
_query: null,
|
|
154
|
+
_result: null,
|
|
155
|
+
find(query) {
|
|
156
|
+
this._query = query ?? null;
|
|
157
|
+
return this;
|
|
158
|
+
},
|
|
159
|
+
where(fn) {
|
|
160
|
+
if (this._result === null) {
|
|
161
|
+
this._result = this._query !== null
|
|
162
|
+
? new SqliteCollectionAdapter(this._db, this._table).find(this._query)
|
|
163
|
+
: new SqliteCollectionAdapter(this._db, this._table).find();
|
|
164
|
+
}
|
|
165
|
+
this._result = this._result.filter(fn);
|
|
166
|
+
return this;
|
|
167
|
+
},
|
|
168
|
+
simplesort(field, opts = {}) {
|
|
169
|
+
if (this._result === null) {
|
|
170
|
+
this._result = this._query !== null
|
|
171
|
+
? new SqliteCollectionAdapter(this._db, this._table).find(this._query)
|
|
172
|
+
: new SqliteCollectionAdapter(this._db, this._table).find();
|
|
173
|
+
}
|
|
174
|
+
this._result.sort((a, b) => {
|
|
175
|
+
const av = a[field] ?? '';
|
|
176
|
+
const bv = b[field] ?? '';
|
|
177
|
+
if (av < bv)
|
|
178
|
+
return opts.desc ? 1 : -1;
|
|
179
|
+
if (av > bv)
|
|
180
|
+
return opts.desc ? -1 : 1;
|
|
181
|
+
return 0;
|
|
182
|
+
});
|
|
183
|
+
return this;
|
|
184
|
+
},
|
|
185
|
+
limit(n) {
|
|
186
|
+
if (this._result === null) {
|
|
187
|
+
this._result = this._query !== null
|
|
188
|
+
? new SqliteCollectionAdapter(this._db, this._table).find(this._query)
|
|
189
|
+
: new SqliteCollectionAdapter(this._db, this._table).find();
|
|
190
|
+
}
|
|
191
|
+
this._result = this._result.slice(0, n);
|
|
192
|
+
return this;
|
|
193
|
+
},
|
|
194
|
+
data() {
|
|
195
|
+
return this._result ?? [];
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ── Main class ──────────────────────────────────────────────────────────
|
|
201
|
+
export class CodeIndexDB {
|
|
202
|
+
static instance;
|
|
203
|
+
/** Subclasses (e.g. EnhancedCodeIndexDB) need access for extra tables without `as any`. */
|
|
204
|
+
db;
|
|
205
|
+
dbPath;
|
|
206
|
+
isInitialized = false;
|
|
207
|
+
initializePromise = null;
|
|
208
|
+
// Collection adapters (preserve naming for internal clarity)
|
|
209
|
+
functionsAdapter;
|
|
210
|
+
whitelistAdapter;
|
|
211
|
+
auditResultsAdapter;
|
|
212
|
+
analyzerConfigAdapter;
|
|
213
|
+
codeMapAdapter;
|
|
214
|
+
schemaAdapter;
|
|
215
|
+
schemaUsageAdapter;
|
|
216
|
+
tasksAdapter;
|
|
217
|
+
taskRepository = null;
|
|
218
|
+
stmts = new Map();
|
|
219
|
+
// ── Schema version ──────────────────────────────────────────────────
|
|
220
|
+
static SCHEMA_VERSION = 1;
|
|
221
|
+
constructor(dbPath = ':memory:') {
|
|
222
|
+
this.dbPath = dbPath === ':memory:' ? dbPath : path.resolve(dbPath);
|
|
223
|
+
}
|
|
224
|
+
// ── Singleton ───────────────────────────────────────────────────────
|
|
225
|
+
static getInstance(dbPath) {
|
|
226
|
+
if (!CodeIndexDB.instance) {
|
|
227
|
+
const resolved = dbPath !== undefined && dbPath !== ''
|
|
228
|
+
? path.resolve(dbPath)
|
|
229
|
+
: resolvePersistedIndexPath();
|
|
230
|
+
CodeIndexDB.instance = new CodeIndexDB(resolved);
|
|
231
|
+
}
|
|
232
|
+
return CodeIndexDB.instance;
|
|
233
|
+
}
|
|
234
|
+
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
235
|
+
async initialize() {
|
|
236
|
+
if (this.isInitialized)
|
|
237
|
+
return;
|
|
238
|
+
if (this.initializePromise) {
|
|
239
|
+
await this.initializePromise;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.initializePromise = this.initializeInternal();
|
|
243
|
+
try {
|
|
244
|
+
await this.initializePromise;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
this.initializePromise = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async initializeInternal() {
|
|
251
|
+
if (this.isInitialized)
|
|
252
|
+
return;
|
|
253
|
+
// Ensure parent directory exists
|
|
254
|
+
if (this.dbPath !== ':memory:') {
|
|
255
|
+
const dir = path.dirname(this.dbPath);
|
|
256
|
+
const storageRoot = getPersistedStorageRoot();
|
|
257
|
+
try {
|
|
258
|
+
let st;
|
|
259
|
+
try {
|
|
260
|
+
st = await fs.stat(dir);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
const code = getErrnoCode(e);
|
|
264
|
+
if (code && code !== 'ENOENT') {
|
|
265
|
+
throw new ContextualError(`Cannot access code index storage directory (${code}): ${dir}`, { errnoCode: code, storageRoot, dbPath: this.dbPath,
|
|
266
|
+
hint: 'Fix permissions or set CODE_AUDITOR_DATA_DIR / --data-dir to a writable directory.' }, e instanceof Error ? e : undefined);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (st && !st.isDirectory()) {
|
|
270
|
+
throw new ContextualError(`Code index storage path exists but is not a directory: ${dir}`, { storageRoot, dbPath: this.dbPath,
|
|
271
|
+
hint: 'Remove the conflicting file/path or choose a different CODE_AUDITOR_DATA_DIR / --data-dir.' });
|
|
272
|
+
}
|
|
273
|
+
await fs.mkdir(dir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
if (e instanceof ContextualError)
|
|
277
|
+
throw e;
|
|
278
|
+
const code = getErrnoCode(e);
|
|
279
|
+
throw new ContextualError(`Failed to prepare code index storage: ${e instanceof Error ? e.message : String(e)}`, { ...(code && { errnoCode: code }), storageRoot, dbPath: this.dbPath,
|
|
280
|
+
hint: 'Ensure the storage directory is writable (default: <cwd>/.code-index when CODE_AUDITOR_DATA_DIR is unset).' }, e instanceof Error ? e : undefined);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Check for LokiJS migration
|
|
284
|
+
const migrationResult = this.maybeMigrateFromLokiJS();
|
|
285
|
+
// Open SQLite database
|
|
286
|
+
try {
|
|
287
|
+
this.db = new Database(this.dbPath);
|
|
288
|
+
this.db.pragma('journal_mode = WAL');
|
|
289
|
+
this.db.pragma('foreign_keys = ON');
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
const code = getErrnoCode(e);
|
|
293
|
+
throw new ContextualError(`Failed to open code index database: ${e instanceof Error ? e.message : String(e)}`, { ...(code && { errnoCode: code }), dbPath: this.dbPath,
|
|
294
|
+
hint: 'The index file may be corrupted, locked, or on a read-only volume. Try a different CODE_AUDITOR_DATA_DIR.' }, e instanceof Error ? e : undefined);
|
|
295
|
+
}
|
|
296
|
+
// Create schema
|
|
297
|
+
this.createSchema();
|
|
298
|
+
// Initialize collection adapters
|
|
299
|
+
this.functionsAdapter = new SqliteCollectionAdapter(this.db, 'functions');
|
|
300
|
+
this.whitelistAdapter = new SqliteCollectionAdapter(this.db, 'whitelist');
|
|
301
|
+
this.auditResultsAdapter = new SqliteCollectionAdapter(this.db, 'audit_results');
|
|
302
|
+
this.analyzerConfigAdapter = new SqliteCollectionAdapter(this.db, 'analyzer_configs');
|
|
303
|
+
this.codeMapAdapter = new SqliteCollectionAdapter(this.db, 'code_maps');
|
|
304
|
+
this.schemaAdapter = new SqliteCollectionAdapter(this.db, 'schema_definitions');
|
|
305
|
+
this.schemaUsageAdapter = new SqliteCollectionAdapter(this.db, 'schema_usage');
|
|
306
|
+
this.tasksAdapter = new SqliteCollectionAdapter(this.db, 'project_tasks');
|
|
307
|
+
// Init defaults
|
|
308
|
+
const whitelistCount = this.db.prepare('SELECT COUNT(*) as cnt FROM whitelist').get().cnt;
|
|
309
|
+
if (whitelistCount === 0) {
|
|
310
|
+
await this.initializeDefaultWhitelists();
|
|
311
|
+
}
|
|
312
|
+
// Auto-sync if we just migrated
|
|
313
|
+
if (migrationResult.migrated) {
|
|
314
|
+
const funcCount = this.db.prepare('SELECT COUNT(*) as cnt FROM functions').get().cnt;
|
|
315
|
+
if (funcCount === 0) {
|
|
316
|
+
try {
|
|
317
|
+
await this.deepSync();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Silently skip — index will be rebuilt on next sync
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.isInitialized = true;
|
|
325
|
+
}
|
|
326
|
+
// ── SQLite schema ───────────────────────────────────────────────────
|
|
327
|
+
createSchema() {
|
|
328
|
+
this.db.exec(`
|
|
329
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
330
|
+
key TEXT PRIMARY KEY,
|
|
331
|
+
value TEXT NOT NULL
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
CREATE TABLE IF NOT EXISTS functions (
|
|
335
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
336
|
+
name TEXT NOT NULL,
|
|
337
|
+
file_path TEXT NOT NULL,
|
|
338
|
+
line_number INTEGER,
|
|
339
|
+
start_line INTEGER,
|
|
340
|
+
end_line INTEGER,
|
|
341
|
+
language TEXT DEFAULT 'typescript',
|
|
342
|
+
entity_type TEXT DEFAULT 'function',
|
|
343
|
+
component_type TEXT,
|
|
344
|
+
signature TEXT,
|
|
345
|
+
return_type TEXT,
|
|
346
|
+
complexity INTEGER DEFAULT 0,
|
|
347
|
+
is_exported INTEGER DEFAULT 0,
|
|
348
|
+
has_jsdoc INTEGER DEFAULT 0,
|
|
349
|
+
jsdoc_description TEXT,
|
|
350
|
+
jsdoc_tags TEXT,
|
|
351
|
+
parameters TEXT,
|
|
352
|
+
type_info TEXT,
|
|
353
|
+
hooks TEXT,
|
|
354
|
+
props TEXT,
|
|
355
|
+
used_imports TEXT,
|
|
356
|
+
unused_imports TEXT,
|
|
357
|
+
import_usage TEXT,
|
|
358
|
+
has_unused_imports INTEGER DEFAULT 0,
|
|
359
|
+
dependency_depth INTEGER DEFAULT 0,
|
|
360
|
+
purpose TEXT DEFAULT '',
|
|
361
|
+
context TEXT DEFAULT '',
|
|
362
|
+
body TEXT,
|
|
363
|
+
content_hash TEXT,
|
|
364
|
+
last_modified TEXT,
|
|
365
|
+
metadata_json TEXT,
|
|
366
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
367
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
CREATE INDEX IF NOT EXISTS idx_functions_name ON functions(name);
|
|
371
|
+
CREATE INDEX IF NOT EXISTS idx_functions_file_path ON functions(file_path);
|
|
372
|
+
CREATE INDEX IF NOT EXISTS idx_functions_language ON functions(language);
|
|
373
|
+
CREATE INDEX IF NOT EXISTS idx_functions_entity_type ON functions(entity_type);
|
|
374
|
+
CREATE INDEX IF NOT EXISTS idx_functions_complexity ON functions(complexity);
|
|
375
|
+
CREATE INDEX IF NOT EXISTS idx_functions_content_hash ON functions(content_hash);
|
|
376
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_functions_name_file ON functions(name, file_path);
|
|
377
|
+
|
|
378
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS functions_fts USING fts5(
|
|
379
|
+
name, signature, jsdoc_description, purpose, context, body,
|
|
380
|
+
content='functions', content_rowid='id',
|
|
381
|
+
tokenize='porter unicode61'
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
CREATE TRIGGER IF NOT EXISTS functions_ai AFTER INSERT ON functions BEGIN
|
|
385
|
+
INSERT INTO functions_fts(rowid, name, signature, jsdoc_description, purpose, context, body)
|
|
386
|
+
VALUES (new.id, new.name, new.signature, new.jsdoc_description, new.purpose, new.context, new.body);
|
|
387
|
+
END;
|
|
388
|
+
CREATE TRIGGER IF NOT EXISTS functions_ad AFTER DELETE ON functions BEGIN
|
|
389
|
+
INSERT INTO functions_fts(functions_fts, rowid, name, signature, jsdoc_description, purpose, context, body)
|
|
390
|
+
VALUES ('delete', old.id, old.name, old.signature, old.jsdoc_description, old.purpose, old.context, old.body);
|
|
391
|
+
END;
|
|
392
|
+
CREATE TRIGGER IF NOT EXISTS functions_au AFTER UPDATE ON functions BEGIN
|
|
393
|
+
INSERT INTO functions_fts(functions_fts, rowid, name, signature, jsdoc_description, purpose, context, body)
|
|
394
|
+
VALUES ('delete', old.id, old.name, old.signature, old.jsdoc_description, old.purpose, old.context, old.body);
|
|
395
|
+
INSERT INTO functions_fts(rowid, name, signature, jsdoc_description, purpose, context, body)
|
|
396
|
+
VALUES (new.id, new.name, new.signature, new.jsdoc_description, new.purpose, new.context, new.body);
|
|
397
|
+
END;
|
|
398
|
+
|
|
399
|
+
CREATE TABLE IF NOT EXISTS function_calls (
|
|
400
|
+
caller_id INTEGER NOT NULL REFERENCES functions(id) ON DELETE CASCADE,
|
|
401
|
+
callee_name TEXT NOT NULL,
|
|
402
|
+
PRIMARY KEY (caller_id, callee_name)
|
|
403
|
+
);
|
|
404
|
+
CREATE INDEX IF NOT EXISTS idx_function_calls_callee ON function_calls(callee_name);
|
|
405
|
+
|
|
406
|
+
CREATE TABLE IF NOT EXISTS function_dependencies (
|
|
407
|
+
function_id INTEGER NOT NULL REFERENCES functions(id) ON DELETE CASCADE,
|
|
408
|
+
dependency TEXT NOT NULL,
|
|
409
|
+
PRIMARY KEY (function_id, dependency)
|
|
410
|
+
);
|
|
411
|
+
CREATE INDEX IF NOT EXISTS idx_function_dependencies_dep ON function_dependencies(dependency);
|
|
412
|
+
|
|
413
|
+
CREATE TABLE IF NOT EXISTS whitelist (
|
|
414
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
415
|
+
name TEXT NOT NULL,
|
|
416
|
+
type TEXT NOT NULL,
|
|
417
|
+
status TEXT DEFAULT 'Active',
|
|
418
|
+
category TEXT,
|
|
419
|
+
description TEXT,
|
|
420
|
+
patterns TEXT,
|
|
421
|
+
added_by TEXT DEFAULT 'system',
|
|
422
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
423
|
+
updated_at TEXT,
|
|
424
|
+
metadata_json TEXT
|
|
425
|
+
);
|
|
426
|
+
CREATE INDEX IF NOT EXISTS idx_whitelist_name ON whitelist(name);
|
|
427
|
+
CREATE INDEX IF NOT EXISTS idx_whitelist_type ON whitelist(type);
|
|
428
|
+
CREATE INDEX IF NOT EXISTS idx_whitelist_status ON whitelist(status);
|
|
429
|
+
|
|
430
|
+
CREATE TABLE IF NOT EXISTS audit_results (
|
|
431
|
+
audit_id TEXT PRIMARY KEY,
|
|
432
|
+
timestamp TEXT NOT NULL,
|
|
433
|
+
project_path TEXT NOT NULL,
|
|
434
|
+
summary_json TEXT NOT NULL,
|
|
435
|
+
analyzer_results_json TEXT NOT NULL,
|
|
436
|
+
violations_json TEXT,
|
|
437
|
+
recommendations_json TEXT,
|
|
438
|
+
metadata_json TEXT,
|
|
439
|
+
expires_at TEXT NOT NULL
|
|
440
|
+
);
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_audit_results_timestamp ON audit_results(timestamp);
|
|
442
|
+
CREATE INDEX IF NOT EXISTS idx_audit_results_project_path ON audit_results(project_path);
|
|
443
|
+
|
|
444
|
+
CREATE TABLE IF NOT EXISTS analyzer_configs (
|
|
445
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
446
|
+
analyzer_name TEXT NOT NULL,
|
|
447
|
+
project_path TEXT,
|
|
448
|
+
is_global INTEGER DEFAULT 0,
|
|
449
|
+
config_json TEXT NOT NULL DEFAULT '{}',
|
|
450
|
+
version TEXT,
|
|
451
|
+
created_by TEXT DEFAULT 'system',
|
|
452
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
453
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
454
|
+
metadata_json TEXT
|
|
455
|
+
);
|
|
456
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_analyzer_configs_key
|
|
457
|
+
ON analyzer_configs(analyzer_name, COALESCE(project_path, '__global__'), is_global);
|
|
458
|
+
|
|
459
|
+
CREATE TABLE IF NOT EXISTS code_maps (
|
|
460
|
+
map_id TEXT NOT NULL,
|
|
461
|
+
section_type TEXT NOT NULL,
|
|
462
|
+
content TEXT NOT NULL,
|
|
463
|
+
metadata_json TEXT DEFAULT '{}',
|
|
464
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
465
|
+
size INTEGER DEFAULT 0,
|
|
466
|
+
PRIMARY KEY (map_id, section_type)
|
|
467
|
+
);
|
|
468
|
+
CREATE INDEX IF NOT EXISTS idx_code_maps_timestamp ON code_maps(timestamp);
|
|
469
|
+
|
|
470
|
+
CREATE TABLE IF NOT EXISTS schema_definitions (
|
|
471
|
+
schema_id TEXT PRIMARY KEY,
|
|
472
|
+
schema_name TEXT,
|
|
473
|
+
schema_json TEXT NOT NULL,
|
|
474
|
+
metadata_json TEXT,
|
|
475
|
+
indexed_at TEXT DEFAULT (datetime('now'))
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
CREATE TABLE IF NOT EXISTS schema_usage (
|
|
479
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
480
|
+
schema_id TEXT,
|
|
481
|
+
table_name TEXT NOT NULL,
|
|
482
|
+
file_path TEXT NOT NULL,
|
|
483
|
+
function_name TEXT NOT NULL,
|
|
484
|
+
usage_type TEXT NOT NULL,
|
|
485
|
+
line INTEGER,
|
|
486
|
+
"column" INTEGER,
|
|
487
|
+
raw_query TEXT,
|
|
488
|
+
parameters TEXT,
|
|
489
|
+
recorded_at TEXT DEFAULT (datetime('now'))
|
|
490
|
+
);
|
|
491
|
+
CREATE INDEX IF NOT EXISTS idx_schema_usage_table ON schema_usage(table_name);
|
|
492
|
+
CREATE INDEX IF NOT EXISTS idx_schema_usage_file ON schema_usage(file_path);
|
|
493
|
+
CREATE INDEX IF NOT EXISTS idx_schema_usage_function ON schema_usage(function_name);
|
|
494
|
+
|
|
495
|
+
CREATE TABLE IF NOT EXISTS project_tasks (
|
|
496
|
+
taskId TEXT PRIMARY KEY,
|
|
497
|
+
projectPath TEXT NOT NULL,
|
|
498
|
+
title TEXT NOT NULL,
|
|
499
|
+
description TEXT DEFAULT '',
|
|
500
|
+
status TEXT DEFAULT 'pending',
|
|
501
|
+
priority TEXT DEFAULT 'medium',
|
|
502
|
+
labels TEXT DEFAULT '[]',
|
|
503
|
+
source TEXT DEFAULT 'manual',
|
|
504
|
+
parentTaskId TEXT,
|
|
505
|
+
blockedBy TEXT DEFAULT '[]',
|
|
506
|
+
dueAt TEXT,
|
|
507
|
+
sortOrder INTEGER DEFAULT 0,
|
|
508
|
+
relatedFiles TEXT DEFAULT '[]',
|
|
509
|
+
relatedSymbols TEXT DEFAULT '[]',
|
|
510
|
+
fingerprint TEXT,
|
|
511
|
+
metadata TEXT DEFAULT '{}',
|
|
512
|
+
createdAt TEXT DEFAULT (datetime('now')),
|
|
513
|
+
updatedAt TEXT DEFAULT (datetime('now')),
|
|
514
|
+
completedAt TEXT
|
|
515
|
+
);
|
|
516
|
+
CREATE INDEX IF NOT EXISTS idx_project_tasks_project ON project_tasks(projectPath);
|
|
517
|
+
CREATE INDEX IF NOT EXISTS idx_project_tasks_status ON project_tasks(status);
|
|
518
|
+
CREATE INDEX IF NOT EXISTS idx_project_tasks_source ON project_tasks(source);
|
|
519
|
+
CREATE INDEX IF NOT EXISTS idx_project_tasks_fingerprint ON project_tasks(fingerprint);
|
|
520
|
+
CREATE INDEX IF NOT EXISTS idx_project_tasks_sort ON project_tasks(sortOrder);
|
|
521
|
+
`);
|
|
522
|
+
// Record schema version
|
|
523
|
+
this.db.prepare(`INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)`).run(String(CodeIndexDB.SCHEMA_VERSION));
|
|
524
|
+
}
|
|
525
|
+
// ── LokiJS migration ────────────────────────────────────────────────
|
|
526
|
+
maybeMigrateFromLokiJS() {
|
|
527
|
+
if (this.dbPath === ':memory:')
|
|
528
|
+
return { migrated: false };
|
|
529
|
+
const bakPath = this.dbPath + '.loki.bak';
|
|
530
|
+
// If backup already exists alongside a valid SQLite db, skip
|
|
531
|
+
try {
|
|
532
|
+
const st = require('fs').statSync(bakPath);
|
|
533
|
+
if (st.isFile()) {
|
|
534
|
+
// Check if SQLite db exists
|
|
535
|
+
try {
|
|
536
|
+
require('fs').statSync(this.dbPath);
|
|
537
|
+
return { migrated: false };
|
|
538
|
+
}
|
|
539
|
+
catch { /* fall through */ }
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch { /* no backup */ }
|
|
543
|
+
// Check if dbPath is a LokiJS file
|
|
544
|
+
try {
|
|
545
|
+
const raw = require('fs').readFileSync(this.dbPath, 'utf-8');
|
|
546
|
+
if (!raw.startsWith('{"filename":') && !raw.includes('"collections":')) {
|
|
547
|
+
return { migrated: false };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return { migrated: false };
|
|
552
|
+
}
|
|
553
|
+
// It's LokiJS — migrate
|
|
554
|
+
try {
|
|
555
|
+
const content = require('fs').readFileSync(this.dbPath, 'utf-8');
|
|
556
|
+
const data = JSON.parse(content);
|
|
557
|
+
// LokiJS stores collections as an array of {name, data, ...}
|
|
558
|
+
const collArray = Array.isArray(data.collections)
|
|
559
|
+
? data.collections
|
|
560
|
+
: [];
|
|
561
|
+
const collections = {};
|
|
562
|
+
for (const c of collArray) {
|
|
563
|
+
collections[c.name] = c.data ?? [];
|
|
564
|
+
}
|
|
565
|
+
// Rename old LokiJS file FIRST, then create fresh SQLite DB
|
|
566
|
+
require('fs').renameSync(this.dbPath, bakPath);
|
|
567
|
+
const migDb = new Database(this.dbPath);
|
|
568
|
+
migDb.pragma('journal_mode = WAL');
|
|
569
|
+
migDb.pragma('foreign_keys = ON');
|
|
570
|
+
// User-authored tables only (function index gets rebuilt by sync).
|
|
571
|
+
// whitelist + analyzer_configs use snake_case (matching raw SQL queries in the main code).
|
|
572
|
+
// project_tasks uses camelCase (matching ProjectTaskRepository document fields).
|
|
573
|
+
migDb.exec(`
|
|
574
|
+
CREATE TABLE IF NOT EXISTS whitelist (
|
|
575
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
576
|
+
name TEXT NOT NULL,
|
|
577
|
+
type TEXT NOT NULL,
|
|
578
|
+
status TEXT DEFAULT 'Active',
|
|
579
|
+
category TEXT,
|
|
580
|
+
description TEXT,
|
|
581
|
+
patterns TEXT,
|
|
582
|
+
added_by TEXT DEFAULT 'system',
|
|
583
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
584
|
+
updated_at TEXT,
|
|
585
|
+
metadata_json TEXT
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
CREATE TABLE IF NOT EXISTS analyzer_configs (
|
|
589
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
590
|
+
analyzer_name TEXT NOT NULL,
|
|
591
|
+
project_path TEXT,
|
|
592
|
+
is_global INTEGER DEFAULT 0,
|
|
593
|
+
config_json TEXT NOT NULL DEFAULT '{}',
|
|
594
|
+
version TEXT,
|
|
595
|
+
created_by TEXT DEFAULT 'system',
|
|
596
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
597
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
598
|
+
metadata_json TEXT
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
CREATE TABLE IF NOT EXISTS project_tasks (
|
|
602
|
+
taskId TEXT PRIMARY KEY,
|
|
603
|
+
projectPath TEXT NOT NULL,
|
|
604
|
+
title TEXT NOT NULL,
|
|
605
|
+
description TEXT DEFAULT '',
|
|
606
|
+
status TEXT DEFAULT 'pending',
|
|
607
|
+
priority TEXT DEFAULT 'medium',
|
|
608
|
+
labels TEXT DEFAULT '[]',
|
|
609
|
+
source TEXT DEFAULT 'manual',
|
|
610
|
+
parentTaskId TEXT,
|
|
611
|
+
blockedBy TEXT DEFAULT '[]',
|
|
612
|
+
dueAt TEXT,
|
|
613
|
+
sortOrder INTEGER DEFAULT 0,
|
|
614
|
+
relatedFiles TEXT DEFAULT '[]',
|
|
615
|
+
relatedSymbols TEXT DEFAULT '[]',
|
|
616
|
+
fingerprint TEXT,
|
|
617
|
+
metadata TEXT DEFAULT '{}',
|
|
618
|
+
createdAt TEXT DEFAULT (datetime('now')),
|
|
619
|
+
updatedAt TEXT DEFAULT (datetime('now')),
|
|
620
|
+
completedAt TEXT
|
|
621
|
+
);
|
|
622
|
+
CREATE INDEX IF NOT EXISTS idx_project_tasks_fingerprint ON project_tasks(fingerprint);
|
|
623
|
+
`);
|
|
624
|
+
let taskCount = 0;
|
|
625
|
+
let configCount = 0;
|
|
626
|
+
let whitelistCount = 0;
|
|
627
|
+
// Migrate tasks
|
|
628
|
+
const tasksData = collections['projectTasks'] ?? [];
|
|
629
|
+
const insertTask = migDb.prepare(`INSERT OR IGNORE INTO project_tasks
|
|
630
|
+
(taskId, projectPath, title, description, status, priority, labels, source,
|
|
631
|
+
parentTaskId, blockedBy, dueAt, sortOrder, relatedFiles, relatedSymbols,
|
|
632
|
+
fingerprint, metadata, createdAt, updatedAt, completedAt)
|
|
633
|
+
VALUES (@taskId, @projectPath, @title, @description, @status, @priority, @labels, @source,
|
|
634
|
+
@parentTaskId, @blockedBy, @dueAt, @sortOrder, @relatedFiles, @relatedSymbols,
|
|
635
|
+
@fingerprint, @metadata, @createdAt, @updatedAt, @completedAt)`);
|
|
636
|
+
for (const t of tasksData) {
|
|
637
|
+
insertTask.run({
|
|
638
|
+
taskId: t.taskId ?? '',
|
|
639
|
+
projectPath: t.projectPath ?? '',
|
|
640
|
+
title: t.title ?? '',
|
|
641
|
+
description: t.description ?? '',
|
|
642
|
+
status: t.status ?? 'pending',
|
|
643
|
+
priority: t.priority ?? 'medium',
|
|
644
|
+
labels: JSON.stringify(t.labels ?? []),
|
|
645
|
+
source: t.source ?? 'manual',
|
|
646
|
+
parentTaskId: t.parentTaskId ?? null,
|
|
647
|
+
blockedBy: JSON.stringify(t.blockedBy ?? []),
|
|
648
|
+
dueAt: t.dueAt ?? null,
|
|
649
|
+
sortOrder: t.sortOrder ?? 0,
|
|
650
|
+
relatedFiles: JSON.stringify(t.relatedFiles ?? []),
|
|
651
|
+
relatedSymbols: JSON.stringify(t.relatedSymbols ?? []),
|
|
652
|
+
fingerprint: t.fingerprint ?? null,
|
|
653
|
+
metadata: JSON.stringify(t.metadata ?? t.metadata_json ?? {}),
|
|
654
|
+
createdAt: t.createdAt ?? t.created_at ?? new Date().toISOString(),
|
|
655
|
+
updatedAt: t.updatedAt ?? t.updated_at ?? new Date().toISOString(),
|
|
656
|
+
completedAt: t.completedAt ?? t.completed_at ?? null,
|
|
657
|
+
});
|
|
658
|
+
taskCount++;
|
|
659
|
+
}
|
|
660
|
+
// Migrate analyzer configs (snake_case columns matching createSchema)
|
|
661
|
+
const configsData = collections['analyzerConfigs'] ?? [];
|
|
662
|
+
const insertConfig = migDb.prepare(`INSERT OR IGNORE INTO analyzer_configs
|
|
663
|
+
(analyzer_name, project_path, is_global, config_json, version, created_by, created_at, updated_at, metadata_json)
|
|
664
|
+
VALUES (@analyzerName, @projectPath, @isGlobal, @configJson, @version, @createdBy, @createdAt, @updatedAt, @metadataJson)`);
|
|
665
|
+
for (const c of configsData) {
|
|
666
|
+
insertConfig.run({
|
|
667
|
+
analyzerName: c.analyzerName ?? '',
|
|
668
|
+
projectPath: c.projectPath ?? null,
|
|
669
|
+
isGlobal: c.isGlobal ? 1 : 0,
|
|
670
|
+
configJson: typeof c.config_json === 'string'
|
|
671
|
+
? c.config_json
|
|
672
|
+
: JSON.stringify(c.config ?? c.config_json ?? {}),
|
|
673
|
+
version: c.version ?? null,
|
|
674
|
+
createdBy: c.createdBy ?? c.created_by ?? 'system',
|
|
675
|
+
createdAt: c.createdAt ?? c.created_at ?? new Date().toISOString(),
|
|
676
|
+
updatedAt: c.updatedAt ?? c.updated_at ?? new Date().toISOString(),
|
|
677
|
+
metadataJson: JSON.stringify(c.metadata ?? c.metadata_json ?? {}),
|
|
678
|
+
});
|
|
679
|
+
configCount++;
|
|
680
|
+
}
|
|
681
|
+
// Migrate whitelist (snake_case columns matching createSchema)
|
|
682
|
+
const whitelistData = collections['whitelist'] ?? [];
|
|
683
|
+
const insertWl = migDb.prepare(`INSERT OR IGNORE INTO whitelist
|
|
684
|
+
(name, type, status, category, description, patterns, added_by, added_at, updated_at, metadata_json)
|
|
685
|
+
VALUES (@name, @type, @status, @category, @description, @patterns, @addedBy, @addedAt, @updatedAt, @metadataJson)`);
|
|
686
|
+
for (const w of whitelistData) {
|
|
687
|
+
insertWl.run({
|
|
688
|
+
name: w.name ?? '',
|
|
689
|
+
type: w.type ?? 'PlatformAPI',
|
|
690
|
+
status: w.status ?? 'Active',
|
|
691
|
+
category: w.category ?? null,
|
|
692
|
+
description: w.description ?? null,
|
|
693
|
+
patterns: JSON.stringify(w.patterns ?? []),
|
|
694
|
+
addedBy: w.addedBy ?? w.added_by ?? 'system',
|
|
695
|
+
addedAt: w.addedAt ?? w.added_at ?? new Date().toISOString(),
|
|
696
|
+
updatedAt: w.updatedAt ?? w.updated_at ?? null,
|
|
697
|
+
metadataJson: JSON.stringify(w.metadata ?? w.metadata_json ?? {}),
|
|
698
|
+
});
|
|
699
|
+
whitelistCount++;
|
|
700
|
+
}
|
|
701
|
+
migDb.close();
|
|
702
|
+
const countLine = [
|
|
703
|
+
taskCount && `${taskCount} tasks`,
|
|
704
|
+
configCount && `${configCount} analyzer configs`,
|
|
705
|
+
whitelistCount && `${whitelistCount} whitelist entries`,
|
|
706
|
+
].filter(Boolean).join(', ') || '0 entries';
|
|
707
|
+
console.log(`[code-auditor] Migrated ${countLine} from LokiJS to SQLite. Old file saved as ${path.basename(bakPath)}`);
|
|
708
|
+
return { migrated: true, counts: { tasks: taskCount, configs: configCount, whitelist: whitelistCount } };
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
// Migration failed — try to restore the backup
|
|
712
|
+
console.error('[code-auditor] LokiJS migration failed:', err instanceof Error ? err.message : String(err));
|
|
713
|
+
try {
|
|
714
|
+
if (require('fs').existsSync(bakPath) && !require('fs').existsSync(this.dbPath)) {
|
|
715
|
+
require('fs').renameSync(bakPath, this.dbPath);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch { /* best effort */ }
|
|
719
|
+
return { migrated: false };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// ── Init guard ──────────────────────────────────────────────────────
|
|
723
|
+
ensureInitialized() {
|
|
724
|
+
if (!this.isInitialized) {
|
|
725
|
+
throw new Error('Database not initialized. Call initialize() first.');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// ── Helpers: row → EnhancedFunctionMetadata ─────────────────────────
|
|
729
|
+
rowToFunction(row) {
|
|
730
|
+
return {
|
|
731
|
+
name: row.name,
|
|
732
|
+
filePath: row.file_path,
|
|
733
|
+
lineNumber: row.line_number,
|
|
734
|
+
startLine: row.start_line,
|
|
735
|
+
endLine: row.end_line,
|
|
736
|
+
language: row.language,
|
|
737
|
+
dependencies: [],
|
|
738
|
+
purpose: row.purpose ?? '',
|
|
739
|
+
context: row.context ?? '',
|
|
740
|
+
signature: row.signature ?? '',
|
|
741
|
+
parameters: tryParseJson(row.parameters) ?? [],
|
|
742
|
+
jsDoc: { description: row.jsdoc_description ?? '' },
|
|
743
|
+
imports: [],
|
|
744
|
+
body: row.body ?? '',
|
|
745
|
+
comments: [],
|
|
746
|
+
isAsync: false,
|
|
747
|
+
isGenerator: false,
|
|
748
|
+
returnType: row.return_type,
|
|
749
|
+
visibility: 'public',
|
|
750
|
+
complexity: row.complexity ?? 0,
|
|
751
|
+
content_hash: row.content_hash,
|
|
752
|
+
metadata: tryParseJson(row.metadata_json) ?? {},
|
|
753
|
+
hooks: tryParseJson(row.hooks),
|
|
754
|
+
props: tryParseJson(row.props),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
functionToRow(func) {
|
|
758
|
+
const enhanced = func;
|
|
759
|
+
const jsDoc = func.jsDoc;
|
|
760
|
+
return {
|
|
761
|
+
name: func.name,
|
|
762
|
+
file_path: func.filePath,
|
|
763
|
+
line_number: func.lineNumber ?? null,
|
|
764
|
+
start_line: func.startLine ?? null,
|
|
765
|
+
end_line: func.endLine ?? null,
|
|
766
|
+
language: func.language ?? 'typescript',
|
|
767
|
+
entity_type: func.metadata?.entityType ?? 'function',
|
|
768
|
+
component_type: func.metadata?.componentType ?? null,
|
|
769
|
+
signature: enhanced.signature ?? '',
|
|
770
|
+
return_type: enhanced.returnType ?? null,
|
|
771
|
+
complexity: enhanced.complexity ?? func.metadata?.complexity ?? 0,
|
|
772
|
+
is_exported: func.metadata?.isExported ? 1 : 0,
|
|
773
|
+
has_jsdoc: (jsDoc && jsDoc.description) ? 1 : 0,
|
|
774
|
+
jsdoc_description: typeof jsDoc === 'string' ? jsDoc : (jsDoc?.description ?? ''),
|
|
775
|
+
jsdoc_tags: jsDoc?.tags ? JSON.stringify(jsDoc.tags) : null,
|
|
776
|
+
parameters: enhanced.parameters ? JSON.stringify(enhanced.parameters) : null,
|
|
777
|
+
type_info: func.metadata?.typeInfo ? JSON.stringify(func.metadata.typeInfo) : null,
|
|
778
|
+
hooks: enhanced.hooks ? JSON.stringify(enhanced.hooks) : null,
|
|
779
|
+
props: enhanced.props ? JSON.stringify(enhanced.props) : null,
|
|
780
|
+
used_imports: func.metadata?.usedImports ? JSON.stringify(func.metadata.usedImports) : null,
|
|
781
|
+
unused_imports: func.metadata?.unusedImports ? JSON.stringify(func.metadata.unusedImports) : null,
|
|
782
|
+
import_usage: func.metadata?.importUsage ? JSON.stringify(func.metadata.importUsage) : null,
|
|
783
|
+
has_unused_imports: func.metadata?.unusedImports?.length > 0 ? 1 : 0,
|
|
784
|
+
dependency_depth: func.metadata?.dependencyDepth ?? 0,
|
|
785
|
+
purpose: func.purpose ?? '',
|
|
786
|
+
context: func.context ?? '',
|
|
787
|
+
body: func.body ?? func.metadata?.body ?? null,
|
|
788
|
+
content_hash: enhanced.content_hash ?? computeContentHash(func.body ?? func.metadata?.body, enhanced.signature),
|
|
789
|
+
last_modified: new Date().toISOString(),
|
|
790
|
+
metadata_json: func.metadata ? JSON.stringify(func.metadata) : '{}',
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
// ── Function CRUD ───────────────────────────────────────────────────
|
|
794
|
+
async registerFunction(func) {
|
|
795
|
+
this.ensureInitialized();
|
|
796
|
+
const row = this.functionToRow(func);
|
|
797
|
+
try {
|
|
798
|
+
this.db.prepare(`
|
|
799
|
+
INSERT INTO functions (name, file_path, line_number, start_line, end_line, language,
|
|
800
|
+
entity_type, component_type, signature, return_type, complexity, is_exported, has_jsdoc,
|
|
801
|
+
jsdoc_description, jsdoc_tags, parameters, type_info, hooks, props,
|
|
802
|
+
used_imports, unused_imports, import_usage, has_unused_imports, dependency_depth,
|
|
803
|
+
purpose, context, body, content_hash, last_modified, metadata_json)
|
|
804
|
+
VALUES (@name, @file_path, @line_number, @start_line, @end_line, @language,
|
|
805
|
+
@entity_type, @component_type, @signature, @return_type, @complexity, @is_exported, @has_jsdoc,
|
|
806
|
+
@jsdoc_description, @jsdoc_tags, @parameters, @type_info, @hooks, @props,
|
|
807
|
+
@used_imports, @unused_imports, @import_usage, @has_unused_imports, @dependency_depth,
|
|
808
|
+
@purpose, @context, @body, @content_hash, @last_modified, @metadata_json)
|
|
809
|
+
ON CONFLICT(name, file_path) DO UPDATE SET
|
|
810
|
+
line_number=excluded.line_number, start_line=excluded.start_line, end_line=excluded.end_line,
|
|
811
|
+
language=excluded.language, entity_type=excluded.entity_type, component_type=excluded.component_type,
|
|
812
|
+
signature=excluded.signature, return_type=excluded.return_type, complexity=excluded.complexity,
|
|
813
|
+
is_exported=excluded.is_exported, has_jsdoc=excluded.has_jsdoc,
|
|
814
|
+
jsdoc_description=excluded.jsdoc_description, jsdoc_tags=excluded.jsdoc_tags,
|
|
815
|
+
parameters=excluded.parameters, type_info=excluded.type_info, hooks=excluded.hooks, props=excluded.props,
|
|
816
|
+
used_imports=excluded.used_imports, unused_imports=excluded.unused_imports,
|
|
817
|
+
import_usage=excluded.import_usage, has_unused_imports=excluded.has_unused_imports,
|
|
818
|
+
dependency_depth=excluded.dependency_depth,
|
|
819
|
+
purpose=excluded.purpose, context=excluded.context, body=excluded.body,
|
|
820
|
+
content_hash=excluded.content_hash, last_modified=excluded.last_modified,
|
|
821
|
+
metadata_json=excluded.metadata_json
|
|
822
|
+
`).run(row);
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
throw new Error(`Failed to register function: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async registerFunctions(functions) {
|
|
829
|
+
this.ensureInitialized();
|
|
830
|
+
let registered = 0;
|
|
831
|
+
let failed = 0;
|
|
832
|
+
const errors = [];
|
|
833
|
+
const insertAll = this.db.transaction((funcs) => {
|
|
834
|
+
for (const func of funcs) {
|
|
835
|
+
try {
|
|
836
|
+
this.registerFunction(func);
|
|
837
|
+
registered++;
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
failed++;
|
|
841
|
+
errors.push({
|
|
842
|
+
function: func.name || 'unknown',
|
|
843
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
insertAll(functions);
|
|
849
|
+
return {
|
|
850
|
+
success: failed === 0,
|
|
851
|
+
registered,
|
|
852
|
+
failed,
|
|
853
|
+
errors: errors
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
async syncFileIndex(filePath, currentFunctions) {
|
|
857
|
+
this.ensureInitialized();
|
|
858
|
+
const stats = { added: 0, updated: 0, removed: 0 };
|
|
859
|
+
const txn = this.db.transaction(() => {
|
|
860
|
+
const existing = this.db.prepare('SELECT id, name, file_path, line_number FROM functions WHERE file_path = ?').all(filePath);
|
|
861
|
+
const createKey = (f) => `${f.name ?? f.name}:${f.filePath ?? f.file_path}:${f.lineNumber ?? f.line_number}`;
|
|
862
|
+
const currentMap = new Map(currentFunctions.map(f => [createKey(f), f]));
|
|
863
|
+
// Insert/update current functions
|
|
864
|
+
for (const func of currentFunctions) {
|
|
865
|
+
const exists = existing.find(e => e.name === func.name);
|
|
866
|
+
if (exists) {
|
|
867
|
+
const row = this.functionToRow(func);
|
|
868
|
+
const keys = Object.keys(row);
|
|
869
|
+
const sets = keys.filter(k => k !== 'name' && k !== 'file_path').map(k => `"${k}" = @${k}`);
|
|
870
|
+
const params = { ...row, _id: exists.id };
|
|
871
|
+
this.db.prepare(`UPDATE functions SET ${sets.join(', ')} WHERE id = @_id`).run(params);
|
|
872
|
+
stats.updated++;
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
const row = this.functionToRow(func);
|
|
876
|
+
const keys = Object.keys(row);
|
|
877
|
+
const sql = `INSERT INTO functions ("${keys.join('", "')}") VALUES (${keys.map(k => '@' + k).join(', ')})`;
|
|
878
|
+
this.db.prepare(sql).run(row);
|
|
879
|
+
stats.added++;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Remove stale functions
|
|
883
|
+
for (const e of existing) {
|
|
884
|
+
if (!currentMap.has(createKey(e))) {
|
|
885
|
+
this.db.prepare('DELETE FROM functions WHERE id = ?').run(e.id);
|
|
886
|
+
stats.removed++;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
txn();
|
|
891
|
+
await this.updateDependencyGraph(filePath);
|
|
892
|
+
return stats;
|
|
893
|
+
}
|
|
894
|
+
// ── Dependency graph ────────────────────────────────────────────────
|
|
895
|
+
async updateDependencyGraph(filePath) {
|
|
896
|
+
this.ensureInitialized();
|
|
897
|
+
const txn = this.db.transaction(() => {
|
|
898
|
+
// For functions in scope, rebuild their call edges
|
|
899
|
+
const functions = filePath
|
|
900
|
+
? this.db.prepare('SELECT id, name, file_path FROM functions WHERE file_path = ?').all(filePath)
|
|
901
|
+
: this.db.prepare('SELECT id, name, file_path FROM functions').all();
|
|
902
|
+
// Clear existing call edges for scoped functions
|
|
903
|
+
if (filePath) {
|
|
904
|
+
this.db.prepare(`DELETE FROM function_calls WHERE caller_id IN (SELECT id FROM functions WHERE file_path = ?)`).run(filePath);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
this.db.prepare('DELETE FROM function_calls').run();
|
|
908
|
+
}
|
|
909
|
+
// Clear existing dependency edges for scoped functions
|
|
910
|
+
if (filePath) {
|
|
911
|
+
this.db.prepare(`DELETE FROM function_dependencies WHERE function_id IN (SELECT id FROM functions WHERE file_path = ?)`).run(filePath);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
this.db.prepare('DELETE FROM function_dependencies').run();
|
|
915
|
+
}
|
|
916
|
+
// Rebuild from metadata
|
|
917
|
+
const allFns = filePath
|
|
918
|
+
? this.db.prepare('SELECT id, name, file_path, metadata_json FROM functions').all()
|
|
919
|
+
: this.db.prepare('SELECT id, name, file_path, metadata_json FROM functions').all();
|
|
920
|
+
const insertCall = this.db.prepare('INSERT OR IGNORE INTO function_calls (caller_id, callee_name) VALUES (?, ?)');
|
|
921
|
+
const insertDep = this.db.prepare('INSERT OR IGNORE INTO function_dependencies (function_id, dependency) VALUES (?, ?)');
|
|
922
|
+
for (const fn of allFns) {
|
|
923
|
+
const meta = tryParseJson(fn.metadata_json) ?? {};
|
|
924
|
+
if (meta.functionCalls) {
|
|
925
|
+
for (const callee of meta.functionCalls) {
|
|
926
|
+
insertCall.run(fn.id, callee);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// Also add deps from the function's own file-level dependencies if we can extract them
|
|
930
|
+
// (dependencies are file-level, stored in function row context)
|
|
931
|
+
}
|
|
932
|
+
// Also rebuild dependency edges from the functions' import data
|
|
933
|
+
const insertFnDep = this.db.prepare('INSERT OR IGNORE INTO function_dependencies (function_id, dependency) VALUES (?, ?)');
|
|
934
|
+
for (const fn of allFns) {
|
|
935
|
+
const meta = tryParseJson(fn.metadata_json) ?? {};
|
|
936
|
+
const usedImports = meta.usedImports ?? [];
|
|
937
|
+
for (const imp of usedImports) {
|
|
938
|
+
insertFnDep.run(fn.id, imp);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
txn();
|
|
943
|
+
}
|
|
944
|
+
async getTransitiveDependencies(functionName, maxDepth = 10) {
|
|
945
|
+
this.ensureInitialized();
|
|
946
|
+
const result = [];
|
|
947
|
+
const visited = new Set();
|
|
948
|
+
// Find starting function(s)
|
|
949
|
+
const startFns = this.db.prepare('SELECT id FROM functions WHERE name = ?').all(functionName);
|
|
950
|
+
if (startFns.length === 0)
|
|
951
|
+
return result;
|
|
952
|
+
// Use recursive CTE for transitive closure
|
|
953
|
+
const rows = this.db.prepare(`
|
|
954
|
+
WITH RECURSIVE deps(id, callee_name, depth) AS (
|
|
955
|
+
SELECT fc.caller_id, fc.callee_name, 1
|
|
956
|
+
FROM function_calls fc
|
|
957
|
+
WHERE fc.caller_id IN (SELECT id FROM functions WHERE name = ?)
|
|
958
|
+
UNION
|
|
959
|
+
SELECT fc.caller_id, fc.callee_name, deps.depth + 1
|
|
960
|
+
FROM function_calls fc
|
|
961
|
+
JOIN deps ON fc.caller_id IN (SELECT id FROM functions WHERE name = deps.callee_name)
|
|
962
|
+
WHERE deps.depth < ?
|
|
963
|
+
)
|
|
964
|
+
SELECT DISTINCT callee_name as name, depth FROM deps ORDER BY depth, name
|
|
965
|
+
`).all(functionName, maxDepth);
|
|
966
|
+
return rows;
|
|
967
|
+
}
|
|
968
|
+
async getTransitiveCallers(functionName, maxDepth = 10) {
|
|
969
|
+
this.ensureInitialized();
|
|
970
|
+
const rows = this.db.prepare(`
|
|
971
|
+
WITH RECURSIVE callers(id, caller_name, depth) AS (
|
|
972
|
+
SELECT fc.caller_id, f.name, 1
|
|
973
|
+
FROM function_calls fc
|
|
974
|
+
JOIN functions f ON f.id = fc.caller_id
|
|
975
|
+
WHERE fc.callee_name = ?
|
|
976
|
+
UNION
|
|
977
|
+
SELECT fc.caller_id, f2.name, callers.depth + 1
|
|
978
|
+
FROM function_calls fc
|
|
979
|
+
JOIN functions f2 ON f2.id = fc.caller_id
|
|
980
|
+
JOIN functions f3 ON f3.name = callers.caller_name AND fc.callee_name = f3.name
|
|
981
|
+
WHERE callers.depth < ?
|
|
982
|
+
)
|
|
983
|
+
SELECT DISTINCT caller_name as name, depth FROM callers ORDER BY depth, name
|
|
984
|
+
`).all(functionName, maxDepth);
|
|
985
|
+
return rows;
|
|
986
|
+
}
|
|
987
|
+
async detectCircularDependencies() {
|
|
988
|
+
this.ensureInitialized();
|
|
989
|
+
// SQLite recursive CTE for cycle detection
|
|
990
|
+
const rows = this.db.prepare(`
|
|
991
|
+
WITH RECURSIVE paths(start_name, path, current_name, depth) AS (
|
|
992
|
+
SELECT f.name, f.name, fc.callee_name, 1
|
|
993
|
+
FROM functions f
|
|
994
|
+
JOIN function_calls fc ON fc.caller_id = f.id
|
|
995
|
+
UNION
|
|
996
|
+
SELECT paths.start_name,
|
|
997
|
+
paths.path || '→' || fc.callee_name,
|
|
998
|
+
fc.callee_name,
|
|
999
|
+
paths.depth + 1
|
|
1000
|
+
FROM paths
|
|
1001
|
+
JOIN functions f ON f.name = paths.current_name
|
|
1002
|
+
JOIN function_calls fc ON fc.caller_id = f.id
|
|
1003
|
+
WHERE paths.depth < 20
|
|
1004
|
+
AND instr(paths.path, fc.callee_name) = 0
|
|
1005
|
+
)
|
|
1006
|
+
SELECT DISTINCT path FROM paths
|
|
1007
|
+
WHERE current_name = start_name AND depth > 1
|
|
1008
|
+
`).all();
|
|
1009
|
+
return rows.map(r => r.path.split('→'));
|
|
1010
|
+
}
|
|
1011
|
+
async calculateDependencyDepths() {
|
|
1012
|
+
this.ensureInitialized();
|
|
1013
|
+
const functions = this.db.prepare('SELECT id, name FROM functions').all();
|
|
1014
|
+
const update = this.db.prepare('UPDATE functions SET dependency_depth = ? WHERE id = ?');
|
|
1015
|
+
// Calculate depths outside transaction since getTransitiveDependencies is async
|
|
1016
|
+
const depths = [];
|
|
1017
|
+
for (const fn of functions) {
|
|
1018
|
+
const deps = await this.getTransitiveDependencies(fn.name);
|
|
1019
|
+
const maxDepth = deps.length > 0 ? Math.max(...deps.map(d => d.depth)) : 0;
|
|
1020
|
+
depths.push({ id: fn.id, maxDepth });
|
|
1021
|
+
}
|
|
1022
|
+
const txn = this.db.transaction(() => {
|
|
1023
|
+
for (const { id, maxDepth } of depths) {
|
|
1024
|
+
update.run(maxDepth, id);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
txn();
|
|
1028
|
+
}
|
|
1029
|
+
// ── Search ──────────────────────────────────────────────────────────
|
|
1030
|
+
async searchFunctions(options) {
|
|
1031
|
+
this.ensureInitialized();
|
|
1032
|
+
const startTime = Date.now();
|
|
1033
|
+
const queryParser = new QueryParser();
|
|
1034
|
+
let parsedQuery;
|
|
1035
|
+
if (options.query) {
|
|
1036
|
+
parsedQuery = options.parsedQuery || queryParser.parse(options.query);
|
|
1037
|
+
}
|
|
1038
|
+
else if (options.parsedQuery) {
|
|
1039
|
+
parsedQuery = options.parsedQuery;
|
|
1040
|
+
}
|
|
1041
|
+
// If there's a parsed query, use the SQL path (handles both FTS5 and operator-only queries)
|
|
1042
|
+
if (parsedQuery) {
|
|
1043
|
+
const compiled = compileToSQL(parsedQuery, { defaultLimit: options.limit || 50, offset: options.offset || 0 });
|
|
1044
|
+
return this.executeCompiledSearch(compiled, parsedQuery, options, startTime);
|
|
1045
|
+
}
|
|
1046
|
+
// Otherwise, get all functions and apply filters in-memory (backward compatible)
|
|
1047
|
+
const rows = this.db.prepare('SELECT *, rowid as "$loki" FROM functions').all();
|
|
1048
|
+
let results = rows.map((r) => this.rowToFunctionDoc(r));
|
|
1049
|
+
// Apply filters (parsedQuery is undefined here — the if-parsedQuery branch returned early)
|
|
1050
|
+
const combinedFilters = this.mergeFilters(options.filters, undefined);
|
|
1051
|
+
results = this.applyFilters(results, combinedFilters);
|
|
1052
|
+
const totalCount = results.length;
|
|
1053
|
+
const limit = options.limit || 50;
|
|
1054
|
+
const offset = options.offset || 0;
|
|
1055
|
+
results = results.slice(offset, offset + limit);
|
|
1056
|
+
const functions = results.map(doc => ({
|
|
1057
|
+
...doc,
|
|
1058
|
+
score: 50
|
|
1059
|
+
}));
|
|
1060
|
+
return {
|
|
1061
|
+
functions,
|
|
1062
|
+
totalCount,
|
|
1063
|
+
query: options.query,
|
|
1064
|
+
parsedQuery,
|
|
1065
|
+
executionTime: Date.now() - startTime
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
executeCompiledSearch(compiled, parsedQuery, options, startTime) {
|
|
1069
|
+
let sql;
|
|
1070
|
+
const params = { ...compiled.params };
|
|
1071
|
+
let afterFrom = '';
|
|
1072
|
+
// Collect JOINs first — they must precede WHERE in SQL
|
|
1073
|
+
for (const join of compiled.joinClauses) {
|
|
1074
|
+
afterFrom += ` ${join}`;
|
|
1075
|
+
}
|
|
1076
|
+
if (compiled.ftsMatch) {
|
|
1077
|
+
// FTS5 path
|
|
1078
|
+
sql = `SELECT f.*, f.rowid as "$loki", bm25(functions_fts) as score
|
|
1079
|
+
FROM functions f
|
|
1080
|
+
JOIN functions_fts ON functions_fts.rowid = f.id${afterFrom}
|
|
1081
|
+
WHERE functions_fts MATCH @_ftsMatch`;
|
|
1082
|
+
params['_ftsMatch'] = compiled.ftsMatch;
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
sql = `SELECT f.*, f.rowid as "$loki", 0 as score FROM functions f${afterFrom} WHERE 1=1`;
|
|
1086
|
+
}
|
|
1087
|
+
// Add WHERE clauses
|
|
1088
|
+
for (const clause of compiled.whereClauses) {
|
|
1089
|
+
sql += ` AND (${clause})`;
|
|
1090
|
+
}
|
|
1091
|
+
// Ordering
|
|
1092
|
+
sql += ` ORDER BY ${compiled.orderBy}`;
|
|
1093
|
+
// Limit/offset
|
|
1094
|
+
sql += ` LIMIT ${compiled.limit} OFFSET ${compiled.offset}`;
|
|
1095
|
+
const rows = this.db.prepare(sql).all(params);
|
|
1096
|
+
// Get total count (without limit)
|
|
1097
|
+
let totalCount = rows.length;
|
|
1098
|
+
try {
|
|
1099
|
+
let countSql;
|
|
1100
|
+
let countAfterFrom = '';
|
|
1101
|
+
for (const join of compiled.joinClauses) {
|
|
1102
|
+
countAfterFrom += ` ${join}`;
|
|
1103
|
+
}
|
|
1104
|
+
if (compiled.ftsMatch) {
|
|
1105
|
+
countSql = `SELECT COUNT(*) as cnt FROM functions f
|
|
1106
|
+
JOIN functions_fts ON functions_fts.rowid = f.id${countAfterFrom}
|
|
1107
|
+
WHERE functions_fts MATCH @_ftsMatch`;
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
countSql = `SELECT COUNT(*) as cnt FROM functions f${countAfterFrom} WHERE 1=1`;
|
|
1111
|
+
}
|
|
1112
|
+
for (const clause of compiled.whereClauses) {
|
|
1113
|
+
countSql += ` AND (${clause})`;
|
|
1114
|
+
}
|
|
1115
|
+
const countParams = { ...params };
|
|
1116
|
+
delete countParams['_ftsMatch'];
|
|
1117
|
+
if (compiled.ftsMatch) {
|
|
1118
|
+
countParams['_ftsMatch'] = compiled.ftsMatch;
|
|
1119
|
+
}
|
|
1120
|
+
const countResult = this.db.prepare(countSql).get(countParams);
|
|
1121
|
+
totalCount = countResult?.cnt ?? rows.length;
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
// Fall back to results length
|
|
1125
|
+
}
|
|
1126
|
+
// Convert rows to FunctionDocument, applying excluded terms
|
|
1127
|
+
let docs = rows.map((r) => this.rowToFunctionDoc(r));
|
|
1128
|
+
if (parsedQuery.excludedTerms.length > 0) {
|
|
1129
|
+
docs = this.excludeTermsFromResults(docs, parsedQuery.excludedTerms);
|
|
1130
|
+
}
|
|
1131
|
+
// Apply in-memory filters (for filters not handled by compileToSQL)
|
|
1132
|
+
const combinedFilters = this.mergeFilters(options.filters, parsedQuery.filters);
|
|
1133
|
+
docs = this.applyFilters(docs, combinedFilters);
|
|
1134
|
+
const functions = docs.map(doc => ({
|
|
1135
|
+
...doc,
|
|
1136
|
+
score: doc.score ?? 0
|
|
1137
|
+
}));
|
|
1138
|
+
return {
|
|
1139
|
+
functions,
|
|
1140
|
+
totalCount,
|
|
1141
|
+
query: options.query,
|
|
1142
|
+
parsedQuery,
|
|
1143
|
+
executionTime: Date.now() - startTime
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
rowToFunctionDoc(row) {
|
|
1147
|
+
const func = this.rowToFunction(row);
|
|
1148
|
+
const doc = {
|
|
1149
|
+
...func,
|
|
1150
|
+
$loki: row['$loki'] ?? row.id,
|
|
1151
|
+
dependencies: [],
|
|
1152
|
+
};
|
|
1153
|
+
// Populate dependencies from metadata
|
|
1154
|
+
const meta = tryParseJson(row.metadata_json);
|
|
1155
|
+
if (meta?.usedImports) {
|
|
1156
|
+
doc.dependencies = meta.usedImports;
|
|
1157
|
+
}
|
|
1158
|
+
return doc;
|
|
1159
|
+
}
|
|
1160
|
+
async findDefinition(name, filePath) {
|
|
1161
|
+
this.ensureInitialized();
|
|
1162
|
+
let row;
|
|
1163
|
+
if (filePath) {
|
|
1164
|
+
row = this.db.prepare('SELECT * FROM functions WHERE name = ? AND file_path = ?').get(name, filePath);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
row = this.db.prepare('SELECT * FROM functions WHERE name = ?').get(name);
|
|
1168
|
+
}
|
|
1169
|
+
if (!row)
|
|
1170
|
+
return null;
|
|
1171
|
+
const func = this.rowToFunction(row);
|
|
1172
|
+
return func;
|
|
1173
|
+
}
|
|
1174
|
+
// ── Stats ───────────────────────────────────────────────────────────
|
|
1175
|
+
async getAllFunctions() {
|
|
1176
|
+
this.ensureInitialized();
|
|
1177
|
+
const rows = this.db.prepare('SELECT * FROM functions').all();
|
|
1178
|
+
return rows.map((r) => this.rowToFunction(r));
|
|
1179
|
+
}
|
|
1180
|
+
async getStats() {
|
|
1181
|
+
this.ensureInitialized();
|
|
1182
|
+
const totalFunctions = this.db.prepare('SELECT COUNT(*) as cnt FROM functions').get().cnt;
|
|
1183
|
+
const languages = {};
|
|
1184
|
+
const langRows = this.db.prepare('SELECT language, COUNT(*) as cnt FROM functions GROUP BY language').all();
|
|
1185
|
+
for (const r of langRows) {
|
|
1186
|
+
languages[r.language] = r.cnt;
|
|
1187
|
+
}
|
|
1188
|
+
const depRows = this.db.prepare('SELECT dependency as name, COUNT(*) as cnt FROM function_dependencies GROUP BY dependency ORDER BY cnt DESC LIMIT 10').all();
|
|
1189
|
+
const topDependencies = depRows.map((r) => ({ name: r.name, count: r.cnt }));
|
|
1190
|
+
const filesIndexed = this.db.prepare('SELECT COUNT(DISTINCT file_path) as cnt FROM functions').get().cnt;
|
|
1191
|
+
return {
|
|
1192
|
+
totalFunctions,
|
|
1193
|
+
languages,
|
|
1194
|
+
topDependencies,
|
|
1195
|
+
filesIndexed,
|
|
1196
|
+
lastUpdated: new Date()
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
// ── Lifecycle: clear & close ────────────────────────────────────────
|
|
1200
|
+
async clearIndex() {
|
|
1201
|
+
this.ensureInitialized();
|
|
1202
|
+
this.db.transaction(() => {
|
|
1203
|
+
this.db.prepare('DELETE FROM functions').run();
|
|
1204
|
+
// FTS5 triggers handle cleanup
|
|
1205
|
+
this.db.prepare('DELETE FROM audit_results').run();
|
|
1206
|
+
this.db.prepare('DELETE FROM code_maps').run();
|
|
1207
|
+
this.db.prepare('DELETE FROM schema_definitions').run();
|
|
1208
|
+
this.db.prepare('DELETE FROM schema_usage').run();
|
|
1209
|
+
// Preserve: project_tasks, analyzer_configs, whitelist
|
|
1210
|
+
})();
|
|
1211
|
+
}
|
|
1212
|
+
async close() {
|
|
1213
|
+
if (this.isInitialized) {
|
|
1214
|
+
this.taskRepository = null;
|
|
1215
|
+
this.stmts.clear();
|
|
1216
|
+
this.db.close();
|
|
1217
|
+
this.isInitialized = false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
// ── File sync & bulk cleanup ────────────────────────────────────────
|
|
1221
|
+
async synchronizeFile(filePath) {
|
|
1222
|
+
this.ensureInitialized();
|
|
1223
|
+
try {
|
|
1224
|
+
await fs.access(filePath);
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
// File doesn't exist — remove all functions for it
|
|
1228
|
+
const result = this.db.prepare('DELETE FROM functions WHERE file_path = ?').run(filePath);
|
|
1229
|
+
return { added: 0, updated: 0, removed: result.changes };
|
|
1230
|
+
}
|
|
1231
|
+
try {
|
|
1232
|
+
const { FunctionScanner } = await import('./functionScanner.js');
|
|
1233
|
+
const scanner = new FunctionScanner();
|
|
1234
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
1235
|
+
const language = filePath.endsWith('.ts') || filePath.endsWith('.tsx') ? 'typescript' : 'javascript';
|
|
1236
|
+
const parsedFunctions = await scanner.scanFunctions(fileContent, filePath, language);
|
|
1237
|
+
return await this.syncFileIndex(filePath, parsedFunctions);
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
throw new Error(`Failed to sync file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
async bulkCleanup() {
|
|
1244
|
+
this.ensureInitialized();
|
|
1245
|
+
const files = this.db.prepare('SELECT DISTINCT file_path FROM functions').all();
|
|
1246
|
+
const removedFiles = [];
|
|
1247
|
+
const errors = [];
|
|
1248
|
+
let removedCount = 0;
|
|
1249
|
+
let scannedCount = 0;
|
|
1250
|
+
for (const { file_path: fp } of files) {
|
|
1251
|
+
scannedCount++;
|
|
1252
|
+
try {
|
|
1253
|
+
await fs.access(fp);
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
const result = this.db.prepare('DELETE FROM functions WHERE file_path = ?').run(fp);
|
|
1257
|
+
removedCount += result.changes;
|
|
1258
|
+
removedFiles.push(fp);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return { scannedCount, removedCount, removedFiles, errors };
|
|
1262
|
+
}
|
|
1263
|
+
async deepSync(progressCallback) {
|
|
1264
|
+
this.ensureInitialized();
|
|
1265
|
+
const files = this.db.prepare('SELECT DISTINCT file_path FROM functions').all();
|
|
1266
|
+
let syncedFiles = 0;
|
|
1267
|
+
let totalAdded = 0;
|
|
1268
|
+
let totalUpdated = 0;
|
|
1269
|
+
let totalRemoved = 0;
|
|
1270
|
+
const errors = [];
|
|
1271
|
+
let current = 0;
|
|
1272
|
+
for (const { file_path: fp } of files) {
|
|
1273
|
+
current++;
|
|
1274
|
+
if (progressCallback) {
|
|
1275
|
+
progressCallback({ current, total: files.length, file: fp });
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
const result = await this.synchronizeFile(fp);
|
|
1279
|
+
if (result) {
|
|
1280
|
+
syncedFiles++;
|
|
1281
|
+
totalAdded += result.added;
|
|
1282
|
+
totalUpdated += result.updated;
|
|
1283
|
+
totalRemoved += result.removed;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
catch (error) {
|
|
1287
|
+
errors.push({
|
|
1288
|
+
file: fp,
|
|
1289
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
syncedFiles,
|
|
1295
|
+
addedFunctions: totalAdded,
|
|
1296
|
+
updatedFunctions: totalUpdated,
|
|
1297
|
+
removedFunctions: totalRemoved,
|
|
1298
|
+
errors
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
// ── Diff-scoped audit detection (Spec 04) ────────────────────────────
|
|
1302
|
+
/**
|
|
1303
|
+
* For a set of file paths, re-parse each file and return only functions
|
|
1304
|
+
* whose content_hash differs from the stored value (plus new functions).
|
|
1305
|
+
* Deleted functions are removed from the index.
|
|
1306
|
+
*
|
|
1307
|
+
* Returns the list of changed/new function metadata for scoped analysis,
|
|
1308
|
+
* and the set of file paths that were actually touched.
|
|
1309
|
+
*/
|
|
1310
|
+
async detectChangedFunctions(filePaths) {
|
|
1311
|
+
this.ensureInitialized();
|
|
1312
|
+
const changedFunctions = [];
|
|
1313
|
+
const deletedFunctions = [];
|
|
1314
|
+
const changedFilePaths = [];
|
|
1315
|
+
const errors = [];
|
|
1316
|
+
const { FunctionScanner } = await import('./functionScanner.js');
|
|
1317
|
+
for (const filePath of filePaths) {
|
|
1318
|
+
try {
|
|
1319
|
+
await fs.access(filePath);
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
// File doesn't exist — remove all its functions
|
|
1323
|
+
const removed = this.db.prepare('SELECT * FROM functions WHERE file_path = ?').all(filePath);
|
|
1324
|
+
if (removed.length > 0) {
|
|
1325
|
+
const parsed = removed.map((r) => this.rowToFunction(r));
|
|
1326
|
+
deletedFunctions.push(...parsed);
|
|
1327
|
+
this.db.prepare('DELETE FROM functions WHERE file_path = ?').run(filePath);
|
|
1328
|
+
changedFilePaths.push(filePath);
|
|
1329
|
+
}
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
const scanner = new FunctionScanner();
|
|
1334
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
1335
|
+
const language = (filePath.endsWith('.ts') || filePath.endsWith('.tsx'))
|
|
1336
|
+
? 'typescript' : 'javascript';
|
|
1337
|
+
const currentFunctions = await scanner.scanFunctions(fileContent, filePath, language);
|
|
1338
|
+
// Get existing functions for this file from DB
|
|
1339
|
+
const existing = this.db.prepare('SELECT * FROM functions WHERE file_path = ?').all(filePath);
|
|
1340
|
+
// Build a name → existing-row map
|
|
1341
|
+
const existingByName = new Map();
|
|
1342
|
+
for (const e of existing) {
|
|
1343
|
+
existingByName.set(e.name, e);
|
|
1344
|
+
}
|
|
1345
|
+
const currentNames = new Set(currentFunctions.map((f) => f.name));
|
|
1346
|
+
let fileChanged = false;
|
|
1347
|
+
for (const func of currentFunctions) {
|
|
1348
|
+
const existingRow = existingByName.get(func.name);
|
|
1349
|
+
// Convert scanner output to EnhancedFunctionMetadata shape
|
|
1350
|
+
const funcMeta = {
|
|
1351
|
+
...func,
|
|
1352
|
+
complexity: func.complexity,
|
|
1353
|
+
content_hash: computeContentHash(func.body ?? func.metadata?.body, func.signature),
|
|
1354
|
+
};
|
|
1355
|
+
const newHash = funcMeta.content_hash;
|
|
1356
|
+
if (!existingRow) {
|
|
1357
|
+
// New function
|
|
1358
|
+
changedFunctions.push(funcMeta);
|
|
1359
|
+
fileChanged = true;
|
|
1360
|
+
}
|
|
1361
|
+
else if (existingRow.content_hash !== newHash) {
|
|
1362
|
+
// Changed function
|
|
1363
|
+
changedFunctions.push(funcMeta);
|
|
1364
|
+
fileChanged = true;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
// Detect deleted functions
|
|
1368
|
+
for (const e of existing) {
|
|
1369
|
+
if (!currentNames.has(e.name)) {
|
|
1370
|
+
deletedFunctions.push(this.rowToFunction(e));
|
|
1371
|
+
fileChanged = true;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (fileChanged) {
|
|
1375
|
+
changedFilePaths.push(filePath);
|
|
1376
|
+
}
|
|
1377
|
+
// Sync the file into the index (upsert + deletes)
|
|
1378
|
+
await this.syncFileIndex(filePath, currentFunctions);
|
|
1379
|
+
}
|
|
1380
|
+
catch (error) {
|
|
1381
|
+
errors.push({
|
|
1382
|
+
file: filePath,
|
|
1383
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return { changedFunctions, deletedFunctions, changedFilePaths, errors };
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Given a project root, detect all indexed files whose mtime is newer than
|
|
1391
|
+
* the stored last_modified timestamp. Returns file paths for further processing.
|
|
1392
|
+
*/
|
|
1393
|
+
async detectModifiedFiles(projectRoot) {
|
|
1394
|
+
this.ensureInitialized();
|
|
1395
|
+
const files = this.db.prepare('SELECT DISTINCT file_path, last_modified FROM functions').all();
|
|
1396
|
+
const modifiedFiles = [];
|
|
1397
|
+
const uniqueFiles = new Map();
|
|
1398
|
+
for (const row of files) {
|
|
1399
|
+
if (!uniqueFiles.has(row.file_path)) {
|
|
1400
|
+
uniqueFiles.set(row.file_path, row.last_modified);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
for (const [filePath, storedMtime] of uniqueFiles) {
|
|
1404
|
+
try {
|
|
1405
|
+
const stat = await fs.stat(filePath);
|
|
1406
|
+
// Normalize to ISO string for comparison
|
|
1407
|
+
const currentMtime = stat.mtime.toISOString();
|
|
1408
|
+
if (!storedMtime || currentMtime > storedMtime) {
|
|
1409
|
+
modifiedFiles.push(filePath);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
// File doesn't exist — it will be handled as a deletion
|
|
1414
|
+
modifiedFiles.push(filePath);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return modifiedFiles;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Get content hashes for a set of file paths. Returns a map:
|
|
1421
|
+
* file_path → { name → content_hash }
|
|
1422
|
+
*/
|
|
1423
|
+
getContentHashesForFiles(filePaths) {
|
|
1424
|
+
this.ensureInitialized();
|
|
1425
|
+
const result = new Map();
|
|
1426
|
+
if (filePaths.length === 0)
|
|
1427
|
+
return result;
|
|
1428
|
+
const placeholders = filePaths.map(() => '?').join(', ');
|
|
1429
|
+
const rows = this.db.prepare(`SELECT file_path, name, content_hash FROM functions WHERE file_path IN (${placeholders})`).all(...filePaths);
|
|
1430
|
+
for (const row of rows) {
|
|
1431
|
+
if (!result.has(row.file_path)) {
|
|
1432
|
+
result.set(row.file_path, new Map());
|
|
1433
|
+
}
|
|
1434
|
+
result.get(row.file_path).set(row.name, row.content_hash ?? '');
|
|
1435
|
+
}
|
|
1436
|
+
return result;
|
|
1437
|
+
}
|
|
1438
|
+
// ── Legacy search helpers (for in-memory filtering) ─────────────────
|
|
1439
|
+
mergeFilters(optionsFilters, queryFilters) {
|
|
1440
|
+
const merged = {};
|
|
1441
|
+
if (optionsFilters)
|
|
1442
|
+
Object.assign(merged, optionsFilters);
|
|
1443
|
+
if (queryFilters) {
|
|
1444
|
+
if (queryFilters.language)
|
|
1445
|
+
merged.language = queryFilters.language;
|
|
1446
|
+
if (queryFilters.filePath)
|
|
1447
|
+
merged.filePath = queryFilters.filePath;
|
|
1448
|
+
if (queryFilters.fileType)
|
|
1449
|
+
merged.fileType = queryFilters.fileType;
|
|
1450
|
+
if (queryFilters.hasJsDoc !== undefined)
|
|
1451
|
+
merged.hasJsDoc = queryFilters.hasJsDoc;
|
|
1452
|
+
if (queryFilters.complexity)
|
|
1453
|
+
merged.complexity = queryFilters.complexity;
|
|
1454
|
+
if (queryFilters.dateRange)
|
|
1455
|
+
merged.dateRange = queryFilters.dateRange;
|
|
1456
|
+
if (queryFilters.metadata)
|
|
1457
|
+
merged.metadata = queryFilters.metadata;
|
|
1458
|
+
}
|
|
1459
|
+
return merged;
|
|
1460
|
+
}
|
|
1461
|
+
applyFilters(results, filters) {
|
|
1462
|
+
if (!filters)
|
|
1463
|
+
return results;
|
|
1464
|
+
let filtered = results;
|
|
1465
|
+
if (filters.language) {
|
|
1466
|
+
filtered = filtered.filter(doc => doc.language === filters.language);
|
|
1467
|
+
}
|
|
1468
|
+
if (filters.filePath) {
|
|
1469
|
+
if (filters.filePath.includes('*') || filters.filePath.includes('?')) {
|
|
1470
|
+
const pattern = filters.filePath.replace(/\*/g, '.*').replace(/\?/g, '.').replace(/\//g, '\\/');
|
|
1471
|
+
const regex = new RegExp(pattern);
|
|
1472
|
+
filtered = filtered.filter(doc => regex.test(doc.filePath));
|
|
1473
|
+
}
|
|
1474
|
+
else if (filters.filePath.endsWith('.ts') || filters.filePath.endsWith('.tsx') ||
|
|
1475
|
+
filters.filePath.endsWith('.js') || filters.filePath.endsWith('.jsx')) {
|
|
1476
|
+
filtered = filtered.filter(doc => doc.filePath.endsWith(filters.filePath));
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
filtered = filtered.filter(doc => doc.filePath.includes(filters.filePath));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (filters.fileType) {
|
|
1483
|
+
filtered = filtered.filter(doc => doc.filePath.endsWith(filters.fileType));
|
|
1484
|
+
}
|
|
1485
|
+
if (filters.hasJsDoc !== undefined) {
|
|
1486
|
+
filtered = filtered.filter(doc => {
|
|
1487
|
+
const hasJsDoc = doc.jsDoc && doc.jsDoc.description && doc.jsDoc.description.length > 0;
|
|
1488
|
+
return filters.hasJsDoc ? hasJsDoc : !hasJsDoc;
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
if (filters.complexity) {
|
|
1492
|
+
filtered = filtered.filter(doc => {
|
|
1493
|
+
if (!doc.complexity)
|
|
1494
|
+
return false;
|
|
1495
|
+
const min = filters.complexity.min || 0;
|
|
1496
|
+
const max = filters.complexity.max || Infinity;
|
|
1497
|
+
return doc.complexity >= min && doc.complexity <= max;
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
if (filters.hasAnyDependency && filters.hasAnyDependency.length > 0) {
|
|
1501
|
+
filtered = filtered.filter(doc => filters.hasAnyDependency.some(dep => doc.dependencies.includes(dep)));
|
|
1502
|
+
}
|
|
1503
|
+
if (filters.metadata) {
|
|
1504
|
+
filtered = filtered.filter(doc => {
|
|
1505
|
+
if (!doc.metadata)
|
|
1506
|
+
return false;
|
|
1507
|
+
const m = filters.metadata;
|
|
1508
|
+
if (m.entityType && doc.metadata.entityType !== m.entityType)
|
|
1509
|
+
return false;
|
|
1510
|
+
if (m.componentType && doc.metadata.componentType !== m.componentType)
|
|
1511
|
+
return false;
|
|
1512
|
+
if (m.hasHook) {
|
|
1513
|
+
if (!doc.metadata.hooks)
|
|
1514
|
+
return false;
|
|
1515
|
+
const found = doc.metadata.hooks.some((h) => h.name?.toLowerCase().includes(m.hasHook.toLowerCase()));
|
|
1516
|
+
if (!found)
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
if (m.hasProp) {
|
|
1520
|
+
if (!doc.metadata.props)
|
|
1521
|
+
return false;
|
|
1522
|
+
const found = doc.metadata.props.some((p) => p.name?.toLowerCase().includes(m.hasProp.toLowerCase()));
|
|
1523
|
+
if (!found)
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
if (m.usesDependency) {
|
|
1527
|
+
const dep = m.usesDependency.toLowerCase();
|
|
1528
|
+
const inFile = doc.dependencies.some(d => d.toLowerCase().includes(dep));
|
|
1529
|
+
const inFunc = doc.metadata.usedImports?.some(i => i.toLowerCase().includes(dep)) ?? false;
|
|
1530
|
+
if (!inFile && !inFunc)
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
if (m.callsFunction) {
|
|
1534
|
+
const target = m.callsFunction.toLowerCase();
|
|
1535
|
+
const calls = doc.metadata.functionCalls;
|
|
1536
|
+
if (!calls || !calls.some(c => c.toLowerCase().includes(target)))
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
if (m.calledByFunction) {
|
|
1540
|
+
const caller = m.calledByFunction.toLowerCase();
|
|
1541
|
+
const calledBy = doc.metadata.calledBy;
|
|
1542
|
+
if (!calledBy || !calledBy.some(c => c.toLowerCase().includes(caller)))
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
if (m.dependsOnModule) {
|
|
1546
|
+
const mod = m.dependsOnModule.toLowerCase();
|
|
1547
|
+
const inFile2 = doc.filePath.toLowerCase().includes(mod);
|
|
1548
|
+
const inDep = doc.dependencies.some(d => d.toLowerCase().includes(mod));
|
|
1549
|
+
const inCall = doc.metadata.functionCalls?.some(c => c.toLowerCase().includes(mod)) ?? false;
|
|
1550
|
+
if (!inFile2 && !inDep && !inCall)
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
if (m.hasUnusedImports) {
|
|
1554
|
+
const unused = doc.metadata.unusedImports;
|
|
1555
|
+
if (!unused || unused.length === 0)
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
return true;
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
return filtered;
|
|
1562
|
+
}
|
|
1563
|
+
excludeTermsFromResults(results, excludedTerms) {
|
|
1564
|
+
return results.filter(doc => {
|
|
1565
|
+
const searchText = [
|
|
1566
|
+
doc.name, doc.signature, doc.purpose, doc.context,
|
|
1567
|
+
doc.jsDoc?.description, doc.returnType,
|
|
1568
|
+
...(doc.parameters || []).map((p) => `${p.name} ${p.description || ''}`),
|
|
1569
|
+
...doc.dependencies
|
|
1570
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
1571
|
+
return !excludedTerms.some(term => searchText.includes(term.toLowerCase()));
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
// ── Whitelist ───────────────────────────────────────────────────────
|
|
1575
|
+
async initializeDefaultWhitelists() {
|
|
1576
|
+
const defaults = [
|
|
1577
|
+
{ name: 'Date', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1578
|
+
{ name: 'Error', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1579
|
+
{ name: 'Array', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1580
|
+
{ name: 'Map', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1581
|
+
{ name: 'Set', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1582
|
+
{ name: 'Promise', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1583
|
+
{ name: 'RegExp', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'javascript', addedBy: 'system', addedAt: new Date() },
|
|
1584
|
+
{ name: 'URL', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'dom', addedBy: 'system', addedAt: new Date() },
|
|
1585
|
+
{ name: 'URLSearchParams', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'dom', addedBy: 'system', addedAt: new Date() },
|
|
1586
|
+
{ name: 'FormData', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'dom', addedBy: 'system', addedAt: new Date() },
|
|
1587
|
+
{ name: 'Headers', type: WhitelistType.PlatformAPI, status: WhitelistStatus.Active, category: 'dom', addedBy: 'system', addedAt: new Date() },
|
|
1588
|
+
{ name: 'fs', type: WhitelistType.NodeBuiltin, status: WhitelistStatus.Active, patterns: ['fs', 'node:fs', 'fs/promises'], addedBy: 'system', addedAt: new Date() },
|
|
1589
|
+
{ name: 'path', type: WhitelistType.NodeBuiltin, status: WhitelistStatus.Active, patterns: ['path', 'node:path'], addedBy: 'system', addedAt: new Date() },
|
|
1590
|
+
{ name: 'crypto', type: WhitelistType.NodeBuiltin, status: WhitelistStatus.Active, patterns: ['crypto', 'node:crypto'], addedBy: 'system', addedAt: new Date() },
|
|
1591
|
+
{ name: 'NextResponse', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nextjs', addedBy: 'system', addedAt: new Date() },
|
|
1592
|
+
{ name: 'NextRequest', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nextjs', addedBy: 'system', addedAt: new Date() },
|
|
1593
|
+
{ name: 'Response', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'web-api', addedBy: 'system', addedAt: new Date() },
|
|
1594
|
+
{ name: 'Request', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'web-api', addedBy: 'system', addedAt: new Date() },
|
|
1595
|
+
{ name: 'Component', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'react', addedBy: 'system', addedAt: new Date() },
|
|
1596
|
+
{ name: 'PureComponent', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'react', addedBy: 'system', addedAt: new Date() },
|
|
1597
|
+
{ name: 'Fragment', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'react', addedBy: 'system', addedAt: new Date() },
|
|
1598
|
+
{ name: 'StrictMode', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'react', addedBy: 'system', addedAt: new Date() },
|
|
1599
|
+
{ name: 'Suspense', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'react', addedBy: 'system', addedAt: new Date() },
|
|
1600
|
+
{ name: 'Pool', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'database', patterns: ['pg'], addedBy: 'system', addedAt: new Date() },
|
|
1601
|
+
{ name: 'Client', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'database', patterns: ['pg'], addedBy: 'system', addedAt: new Date() },
|
|
1602
|
+
{ name: 'MongoClient', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'database', patterns: ['mongodb'], addedBy: 'system', addedAt: new Date() },
|
|
1603
|
+
{ name: 'PrismaClient', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'database', patterns: ['@prisma/client'], addedBy: 'system', addedAt: new Date() },
|
|
1604
|
+
{ name: 'StackServerApp', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'auth', patterns: ['@stackframe/stack'], addedBy: 'system', addedAt: new Date() },
|
|
1605
|
+
{ name: 'StackClient', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'auth', patterns: ['@stackframe/stack'], addedBy: 'system', addedAt: new Date() },
|
|
1606
|
+
{ name: 'ClerkProvider', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'auth', patterns: ['@clerk/nextjs'], addedBy: 'system', addedAt: new Date() },
|
|
1607
|
+
{ name: 'Auth0Provider', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'auth', patterns: ['@auth0/nextjs-auth0'], addedBy: 'system', addedAt: new Date() },
|
|
1608
|
+
{ name: 'Axios', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'http', patterns: ['axios'], addedBy: 'system', addedAt: new Date() },
|
|
1609
|
+
{ name: 'HttpClient', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'http', addedBy: 'system', addedAt: new Date() },
|
|
1610
|
+
{ name: 'Router', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'express', patterns: ['express'], addedBy: 'system', addedAt: new Date() },
|
|
1611
|
+
{ name: 'Application', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'express', patterns: ['express'], addedBy: 'system', addedAt: new Date() },
|
|
1612
|
+
{ name: 'TestingModule', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'testing', patterns: ['@nestjs/testing'], addedBy: 'system', addedAt: new Date() },
|
|
1613
|
+
{ name: 'MockedProvider', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'testing', patterns: ['@apollo/client/testing'], addedBy: 'system', addedAt: new Date() },
|
|
1614
|
+
{ name: 'EventEmitter', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nodejs', patterns: ['events', 'node:events'], addedBy: 'system', addedAt: new Date() },
|
|
1615
|
+
{ name: 'Readable', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nodejs', patterns: ['stream', 'node:stream'], addedBy: 'system', addedAt: new Date() },
|
|
1616
|
+
{ name: 'Writable', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nodejs', patterns: ['stream', 'node:stream'], addedBy: 'system', addedAt: new Date() },
|
|
1617
|
+
{ name: 'Transform', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nodejs', patterns: ['stream', 'node:stream'], addedBy: 'system', addedAt: new Date() },
|
|
1618
|
+
{ name: 'Buffer', type: WhitelistType.FrameworkClass, status: WhitelistStatus.Active, category: 'nodejs', addedBy: 'system', addedAt: new Date() },
|
|
1619
|
+
];
|
|
1620
|
+
const insert = this.db.prepare(`INSERT INTO whitelist (name, type, status, category, description, patterns, added_by, added_at, metadata_json)
|
|
1621
|
+
VALUES (@name, @type, @status, @category, @description, @patterns, @added_by, @added_at, @metadata_json)`);
|
|
1622
|
+
for (const entry of defaults) {
|
|
1623
|
+
insert.run({
|
|
1624
|
+
name: entry.name,
|
|
1625
|
+
type: entry.type,
|
|
1626
|
+
status: entry.status,
|
|
1627
|
+
category: entry.category ?? null,
|
|
1628
|
+
description: entry.description ?? null,
|
|
1629
|
+
patterns: JSON.stringify(entry.patterns ?? []),
|
|
1630
|
+
added_by: entry.addedBy ?? 'system',
|
|
1631
|
+
added_at: (entry.addedAt ?? new Date()).toISOString(),
|
|
1632
|
+
metadata_json: '{}',
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
async getWhitelist(type, status) {
|
|
1637
|
+
this.ensureInitialized();
|
|
1638
|
+
const rows = this.whitelistAdapter.find({
|
|
1639
|
+
...(type ? { type } : {}),
|
|
1640
|
+
...(status ? { status } : {}),
|
|
1641
|
+
});
|
|
1642
|
+
return rows.map((r) => ({
|
|
1643
|
+
name: r.name,
|
|
1644
|
+
type: r.type,
|
|
1645
|
+
status: r.status,
|
|
1646
|
+
category: r.category,
|
|
1647
|
+
description: r.description,
|
|
1648
|
+
patterns: tryParseJson(r.patterns),
|
|
1649
|
+
addedBy: r.added_by ?? r.addedBy,
|
|
1650
|
+
addedAt: new Date(r.added_at ?? r.addedAt),
|
|
1651
|
+
updatedAt: r.updated_at ? new Date(r.updated_at) : undefined,
|
|
1652
|
+
metadata: tryParseJson(r.metadata_json),
|
|
1653
|
+
}));
|
|
1654
|
+
}
|
|
1655
|
+
async addWhitelistEntry(entry) {
|
|
1656
|
+
this.ensureInitialized();
|
|
1657
|
+
const row = this.whitelistAdapter.insert({
|
|
1658
|
+
name: entry.name,
|
|
1659
|
+
type: entry.type,
|
|
1660
|
+
status: entry.status ?? WhitelistStatus.Active,
|
|
1661
|
+
category: entry.category ?? null,
|
|
1662
|
+
description: entry.description ?? null,
|
|
1663
|
+
patterns: JSON.stringify(entry.patterns ?? []),
|
|
1664
|
+
added_by: entry.addedBy ?? 'user',
|
|
1665
|
+
added_at: new Date().toISOString(),
|
|
1666
|
+
metadata_json: '{}',
|
|
1667
|
+
});
|
|
1668
|
+
return {
|
|
1669
|
+
name: row.name,
|
|
1670
|
+
type: row.type,
|
|
1671
|
+
status: row.status,
|
|
1672
|
+
category: row.category,
|
|
1673
|
+
description: row.description,
|
|
1674
|
+
patterns: tryParseJson(row.patterns),
|
|
1675
|
+
addedBy: row.added_by,
|
|
1676
|
+
addedAt: new Date(row.added_at),
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
async updateWhitelistStatus(name, status) {
|
|
1680
|
+
this.ensureInitialized();
|
|
1681
|
+
this.db.prepare('UPDATE whitelist SET status = ?, updated_at = ? WHERE name = ?')
|
|
1682
|
+
.run(status, new Date().toISOString(), name);
|
|
1683
|
+
}
|
|
1684
|
+
isWhitelisted(name, type) {
|
|
1685
|
+
if (!this.isInitialized)
|
|
1686
|
+
return false;
|
|
1687
|
+
const rows = this.db.prepare('SELECT name, patterns FROM whitelist WHERE type = ? AND status = ?').all(type, WhitelistStatus.Active);
|
|
1688
|
+
return rows.some(entry => {
|
|
1689
|
+
if (entry.name === name)
|
|
1690
|
+
return true;
|
|
1691
|
+
const patterns = tryParseJson(entry.patterns);
|
|
1692
|
+
if (patterns && Array.isArray(patterns)) {
|
|
1693
|
+
return patterns.some((p) => {
|
|
1694
|
+
if (p.includes('*')) {
|
|
1695
|
+
const regex = new RegExp(p.replace(/\*/g, '.*'));
|
|
1696
|
+
return regex.test(name);
|
|
1697
|
+
}
|
|
1698
|
+
return p === name;
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return false;
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
async detectWhitelistCandidates() {
|
|
1705
|
+
return [];
|
|
1706
|
+
}
|
|
1707
|
+
// ── Audit results ───────────────────────────────────────────────────
|
|
1708
|
+
async storeAuditResults(auditResult, projectPath) {
|
|
1709
|
+
this.ensureInitialized();
|
|
1710
|
+
const auditId = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1711
|
+
this.db.prepare(`INSERT INTO audit_results (audit_id, timestamp, project_path, summary_json, analyzer_results_json, violations_json, recommendations_json, metadata_json, expires_at)
|
|
1712
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(auditId, new Date().toISOString(), projectPath, JSON.stringify(auditResult.summary ?? {}), JSON.stringify(auditResult.analyzerResults ?? auditResult.results ?? {}), JSON.stringify(auditResult.violations ?? null), JSON.stringify(auditResult.recommendations ?? null), JSON.stringify(auditResult.metadata ?? {}), new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString());
|
|
1713
|
+
this.cleanupExpiredAudits();
|
|
1714
|
+
return auditId;
|
|
1715
|
+
}
|
|
1716
|
+
async getAuditResults(auditId) {
|
|
1717
|
+
this.ensureInitialized();
|
|
1718
|
+
const row = this.db.prepare('SELECT * FROM audit_results WHERE audit_id = ?').get(auditId);
|
|
1719
|
+
if (!row)
|
|
1720
|
+
return null;
|
|
1721
|
+
if (new Date(row.expires_at) <= new Date()) {
|
|
1722
|
+
this.db.prepare('DELETE FROM audit_results WHERE audit_id = ?').run(auditId);
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
return {
|
|
1726
|
+
auditId: row.audit_id,
|
|
1727
|
+
timestamp: new Date(row.timestamp),
|
|
1728
|
+
projectPath: row.project_path,
|
|
1729
|
+
summary: tryParseJson(row.summary_json),
|
|
1730
|
+
analyzerResults: tryParseJson(row.analyzer_results_json),
|
|
1731
|
+
violations: tryParseJson(row.violations_json),
|
|
1732
|
+
recommendations: tryParseJson(row.recommendations_json),
|
|
1733
|
+
metadata: tryParseJson(row.metadata_json),
|
|
1734
|
+
expiresAt: new Date(row.expires_at),
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
async getMostRecentAuditResults(projectPath, resultScope) {
|
|
1738
|
+
this.ensureInitialized();
|
|
1739
|
+
const now = new Date().toISOString();
|
|
1740
|
+
let row;
|
|
1741
|
+
const scopeFilter = resultScope
|
|
1742
|
+
? "AND json_extract(metadata_json, '$.scope') = ?"
|
|
1743
|
+
: '';
|
|
1744
|
+
if (projectPath) {
|
|
1745
|
+
const params = [projectPath, now];
|
|
1746
|
+
if (resultScope)
|
|
1747
|
+
params.push(resultScope);
|
|
1748
|
+
row = this.db.prepare(`SELECT * FROM audit_results WHERE project_path = ? AND expires_at > ? ${scopeFilter} ORDER BY timestamp DESC, rowid DESC LIMIT 1`).get(...params);
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
const params = [now];
|
|
1752
|
+
if (resultScope)
|
|
1753
|
+
params.push(resultScope);
|
|
1754
|
+
row = this.db.prepare(`SELECT * FROM audit_results WHERE expires_at > ? ${scopeFilter} ORDER BY timestamp DESC, rowid DESC LIMIT 1`).get(...params);
|
|
1755
|
+
}
|
|
1756
|
+
if (!row)
|
|
1757
|
+
return null;
|
|
1758
|
+
return {
|
|
1759
|
+
auditId: row.audit_id,
|
|
1760
|
+
timestamp: new Date(row.timestamp),
|
|
1761
|
+
projectPath: row.project_path,
|
|
1762
|
+
summary: tryParseJson(row.summary_json),
|
|
1763
|
+
analyzerResults: tryParseJson(row.analyzer_results_json),
|
|
1764
|
+
violations: tryParseJson(row.violations_json),
|
|
1765
|
+
recommendations: tryParseJson(row.recommendations_json),
|
|
1766
|
+
metadata: tryParseJson(row.metadata_json),
|
|
1767
|
+
expiresAt: new Date(row.expires_at),
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
hasOpenTaskByFingerprint(fingerprint) {
|
|
1771
|
+
if (!fingerprint)
|
|
1772
|
+
return false;
|
|
1773
|
+
return this.getTaskRepository().findOpenByFingerprint(fingerprint).length > 0;
|
|
1774
|
+
}
|
|
1775
|
+
cleanupExpiredAudits() {
|
|
1776
|
+
this.db.prepare('DELETE FROM audit_results WHERE expires_at < ?').run(new Date().toISOString());
|
|
1777
|
+
}
|
|
1778
|
+
// ── Analyzer configs ────────────────────────────────────────────────
|
|
1779
|
+
async storeAnalyzerConfig(analyzerName, config, options) {
|
|
1780
|
+
this.ensureInitialized();
|
|
1781
|
+
const projectPath = options?.projectPath ?? null;
|
|
1782
|
+
const isGlobal = options?.isGlobal ?? true;
|
|
1783
|
+
const existing = this.db.prepare('SELECT id FROM analyzer_configs WHERE analyzer_name = ? AND COALESCE(project_path, \'__global__\') = ? AND is_global = ?').get(analyzerName, projectPath ?? '__global__', isGlobal ? 1 : 0);
|
|
1784
|
+
if (existing) {
|
|
1785
|
+
this.db.prepare('UPDATE analyzer_configs SET config_json = ?, updated_at = ?, metadata_json = ? WHERE id = ?').run(JSON.stringify(config), new Date().toISOString(), JSON.stringify(options?.metadata ?? {}), existing.id);
|
|
1786
|
+
}
|
|
1787
|
+
else {
|
|
1788
|
+
this.db.prepare(`INSERT INTO analyzer_configs (analyzer_name, project_path, is_global, config_json, created_by, created_at, updated_at, metadata_json)
|
|
1789
|
+
VALUES (?, ?, ?, ?, 'user', ?, ?, ?)`).run(analyzerName, projectPath, isGlobal ? 1 : 0, JSON.stringify(config), new Date().toISOString(), new Date().toISOString(), JSON.stringify(options?.metadata ?? {}));
|
|
1790
|
+
}
|
|
1791
|
+
return analyzerName;
|
|
1792
|
+
}
|
|
1793
|
+
async getAnalyzerConfig(analyzerName, projectPath) {
|
|
1794
|
+
this.ensureInitialized();
|
|
1795
|
+
if (projectPath) {
|
|
1796
|
+
const row = this.db.prepare('SELECT config_json FROM analyzer_configs WHERE analyzer_name = ? AND project_path = ? AND is_global = 0').get(analyzerName, projectPath);
|
|
1797
|
+
if (row)
|
|
1798
|
+
return tryParseJson(row.config_json);
|
|
1799
|
+
}
|
|
1800
|
+
const globalRow = this.db.prepare('SELECT config_json FROM analyzer_configs WHERE analyzer_name = ? AND is_global = 1').get(analyzerName);
|
|
1801
|
+
return globalRow ? tryParseJson(globalRow.config_json) : null;
|
|
1802
|
+
}
|
|
1803
|
+
async getAllAnalyzerConfigs(projectPath) {
|
|
1804
|
+
this.ensureInitialized();
|
|
1805
|
+
const configs = {};
|
|
1806
|
+
const globals = this.db.prepare('SELECT analyzer_name, config_json FROM analyzer_configs WHERE is_global = 1').all();
|
|
1807
|
+
for (const c of globals) {
|
|
1808
|
+
configs[c.analyzer_name] = tryParseJson(c.config_json);
|
|
1809
|
+
}
|
|
1810
|
+
if (projectPath) {
|
|
1811
|
+
const locals = this.db.prepare('SELECT analyzer_name, config_json FROM analyzer_configs WHERE project_path = ? AND is_global = 0').all(projectPath);
|
|
1812
|
+
for (const c of locals) {
|
|
1813
|
+
configs[c.analyzer_name] = tryParseJson(c.config_json);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return configs;
|
|
1817
|
+
}
|
|
1818
|
+
async deleteAnalyzerConfig(analyzerName, options) {
|
|
1819
|
+
this.ensureInitialized();
|
|
1820
|
+
const result = this.db.prepare('DELETE FROM analyzer_configs WHERE analyzer_name = ? AND COALESCE(project_path, \'__global__\') = ? AND is_global = ?').run(analyzerName, options?.projectPath ?? '__global__', (options?.isGlobal ?? !options?.projectPath) ? 1 : 0);
|
|
1821
|
+
return result.changes > 0;
|
|
1822
|
+
}
|
|
1823
|
+
async resetAnalyzerConfigs(projectPath) {
|
|
1824
|
+
this.ensureInitialized();
|
|
1825
|
+
if (projectPath) {
|
|
1826
|
+
this.db.prepare('DELETE FROM analyzer_configs WHERE project_path = ? AND is_global = 0').run(projectPath);
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
this.db.prepare('DELETE FROM analyzer_configs').run();
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
// ── Code maps ───────────────────────────────────────────────────────
|
|
1833
|
+
async storeCodeMapSection(mapId, sectionType, content, metadata) {
|
|
1834
|
+
this.ensureInitialized();
|
|
1835
|
+
this.db.prepare(`INSERT OR REPLACE INTO code_maps (map_id, section_type, content, metadata_json, timestamp, size)
|
|
1836
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(mapId, sectionType, content, JSON.stringify(metadata ?? {}), new Date().toISOString(), content.length);
|
|
1837
|
+
}
|
|
1838
|
+
async getCodeMapSection(mapId, sectionType) {
|
|
1839
|
+
this.ensureInitialized();
|
|
1840
|
+
const row = this.db.prepare('SELECT content, metadata_json FROM code_maps WHERE map_id = ? AND section_type = ?')
|
|
1841
|
+
.get(mapId, sectionType);
|
|
1842
|
+
return row ? { content: row.content, metadata: tryParseJson(row.metadata_json) ?? {} } : null;
|
|
1843
|
+
}
|
|
1844
|
+
async listCodeMapSections(mapId) {
|
|
1845
|
+
this.ensureInitialized();
|
|
1846
|
+
const rows = this.db.prepare('SELECT section_type, size, timestamp FROM code_maps WHERE map_id = ?')
|
|
1847
|
+
.all(mapId);
|
|
1848
|
+
return rows.map((r) => ({ sectionType: r.section_type, size: r.size ?? 0, timestamp: new Date(r.timestamp) }));
|
|
1849
|
+
}
|
|
1850
|
+
async clearOldCodeMaps(olderThanHours = 24) {
|
|
1851
|
+
this.ensureInitialized();
|
|
1852
|
+
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000).toISOString();
|
|
1853
|
+
const result = this.db.prepare('DELETE FROM code_maps WHERE timestamp < ?').run(cutoff);
|
|
1854
|
+
return result.changes;
|
|
1855
|
+
}
|
|
1856
|
+
async deleteCodeMap(mapId) {
|
|
1857
|
+
this.ensureInitialized();
|
|
1858
|
+
const result = this.db.prepare('DELETE FROM code_maps WHERE map_id = ?').run(mapId);
|
|
1859
|
+
return result.changes;
|
|
1860
|
+
}
|
|
1861
|
+
// ── Schema management ───────────────────────────────────────────────
|
|
1862
|
+
async storeSchema(schema) {
|
|
1863
|
+
this.ensureInitialized();
|
|
1864
|
+
const schemaId = `schema_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1865
|
+
const metadata = {
|
|
1866
|
+
schemaId,
|
|
1867
|
+
schemaName: schema.name,
|
|
1868
|
+
indexedAt: new Date(),
|
|
1869
|
+
tableCount: schema.databases.reduce((acc, db) => acc + db.tables.length, 0),
|
|
1870
|
+
relationshipCount: schema.databases.reduce((acc, db) => acc + (db.relationships?.length || 0), 0),
|
|
1871
|
+
usagePatterns: [],
|
|
1872
|
+
discoveredPatterns: [],
|
|
1873
|
+
violations: []
|
|
1874
|
+
};
|
|
1875
|
+
this.db.prepare('INSERT INTO schema_definitions (schema_id, schema_name, schema_json, metadata_json, indexed_at) VALUES (?, ?, ?, ?, ?)').run(schemaId, schema.name, JSON.stringify(schema), JSON.stringify(metadata), new Date().toISOString());
|
|
1876
|
+
return schemaId;
|
|
1877
|
+
}
|
|
1878
|
+
async getSchema(schemaId) {
|
|
1879
|
+
this.ensureInitialized();
|
|
1880
|
+
const row = this.db.prepare('SELECT schema_json FROM schema_definitions WHERE schema_id = ?').get(schemaId);
|
|
1881
|
+
return row ? tryParseJson(row.schema_json) : null;
|
|
1882
|
+
}
|
|
1883
|
+
async getAllSchemas() {
|
|
1884
|
+
this.ensureInitialized();
|
|
1885
|
+
const rows = this.db.prepare('SELECT schema_id, schema_json, metadata_json FROM schema_definitions').all();
|
|
1886
|
+
return rows.map((r) => ({
|
|
1887
|
+
schemaId: r.schema_id,
|
|
1888
|
+
metadata: tryParseJson(r.metadata_json) ?? {},
|
|
1889
|
+
schema: tryParseJson(r.schema_json),
|
|
1890
|
+
}));
|
|
1891
|
+
}
|
|
1892
|
+
async deleteSchema(schemaId) {
|
|
1893
|
+
this.ensureInitialized();
|
|
1894
|
+
this.db.prepare('DELETE FROM schema_usage WHERE schema_id = ?').run(schemaId);
|
|
1895
|
+
const result = this.db.prepare('DELETE FROM schema_definitions WHERE schema_id = ?').run(schemaId);
|
|
1896
|
+
return result.changes > 0;
|
|
1897
|
+
}
|
|
1898
|
+
async recordSchemaUsage(usage, schemaId) {
|
|
1899
|
+
this.ensureInitialized();
|
|
1900
|
+
const existing = this.db.prepare('SELECT id FROM schema_usage WHERE table_name = ? AND file_path = ? AND function_name = ? AND line = ?').get(usage.tableName, usage.filePath, usage.functionName, usage.line);
|
|
1901
|
+
if (existing) {
|
|
1902
|
+
this.db.prepare('UPDATE schema_usage SET schema_id = ?, usage_type = ?, "column" = ?, raw_query = ?, parameters = ?, recorded_at = ? WHERE id = ?').run(schemaId ?? 'default', usage.usageType, usage.column ?? null, usage.rawQuery ?? null, JSON.stringify(usage.parameters ?? []), new Date().toISOString(), existing.id);
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
this.db.prepare('INSERT INTO schema_usage (schema_id, table_name, file_path, function_name, usage_type, line, "column", raw_query, parameters, recorded_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(schemaId ?? 'default', usage.tableName, usage.filePath, usage.functionName, usage.usageType, usage.line ?? null, usage.column ?? null, usage.rawQuery ?? null, JSON.stringify(usage.parameters ?? []), new Date().toISOString());
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
async getSchemaUsage(options = {}) {
|
|
1909
|
+
this.ensureInitialized();
|
|
1910
|
+
const clauses = [];
|
|
1911
|
+
const params = [];
|
|
1912
|
+
if (options.schemaId) {
|
|
1913
|
+
clauses.push('schema_id = ?');
|
|
1914
|
+
params.push(options.schemaId);
|
|
1915
|
+
}
|
|
1916
|
+
if (options.tableName) {
|
|
1917
|
+
clauses.push('table_name = ?');
|
|
1918
|
+
params.push(options.tableName);
|
|
1919
|
+
}
|
|
1920
|
+
if (options.filePath) {
|
|
1921
|
+
clauses.push('file_path = ?');
|
|
1922
|
+
params.push(options.filePath);
|
|
1923
|
+
}
|
|
1924
|
+
if (options.functionName) {
|
|
1925
|
+
clauses.push('function_name = ?');
|
|
1926
|
+
params.push(options.functionName);
|
|
1927
|
+
}
|
|
1928
|
+
if (options.usageType) {
|
|
1929
|
+
clauses.push('usage_type = ?');
|
|
1930
|
+
params.push(options.usageType);
|
|
1931
|
+
}
|
|
1932
|
+
const sql = 'SELECT * FROM schema_usage' + (clauses.length ? ' WHERE ' + clauses.join(' AND ') : '');
|
|
1933
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1934
|
+
return rows.map((r) => ({
|
|
1935
|
+
tableName: r.table_name,
|
|
1936
|
+
filePath: r.file_path,
|
|
1937
|
+
functionName: r.function_name,
|
|
1938
|
+
usageType: r.usage_type,
|
|
1939
|
+
line: r.line,
|
|
1940
|
+
column: r.column,
|
|
1941
|
+
rawQuery: r.raw_query,
|
|
1942
|
+
parameters: tryParseJson(r.parameters) ?? [],
|
|
1943
|
+
}));
|
|
1944
|
+
}
|
|
1945
|
+
async findFunctionsUsingTable(tableName) {
|
|
1946
|
+
this.ensureInitialized();
|
|
1947
|
+
const rows = this.db.prepare('SELECT function_name, file_path, usage_type, line FROM schema_usage WHERE table_name = ?').all(tableName);
|
|
1948
|
+
return rows.map((r) => ({
|
|
1949
|
+
functionName: r.function_name,
|
|
1950
|
+
filePath: r.file_path,
|
|
1951
|
+
usageType: r.usage_type,
|
|
1952
|
+
line: r.line,
|
|
1953
|
+
}));
|
|
1954
|
+
}
|
|
1955
|
+
async getSchemaStats() {
|
|
1956
|
+
this.ensureInitialized();
|
|
1957
|
+
const totalSchemas = this.db.prepare('SELECT COUNT(*) as cnt FROM schema_definitions').get().cnt;
|
|
1958
|
+
const totalUsagePatterns = this.db.prepare('SELECT COUNT(*) as cnt FROM schema_usage').get().cnt;
|
|
1959
|
+
const schemas = this.db.prepare('SELECT schema_json FROM schema_definitions').all();
|
|
1960
|
+
const totalTables = schemas.reduce((acc, s) => {
|
|
1961
|
+
const schema = tryParseJson(s.schema_json);
|
|
1962
|
+
return acc + (schema?.databases?.reduce((dbAcc, db) => dbAcc + (db.tables?.length ?? 0), 0) ?? 0);
|
|
1963
|
+
}, 0);
|
|
1964
|
+
const mostUsedTables = this.db.prepare('SELECT table_name as tableName, COUNT(*) as usageCount FROM schema_usage GROUP BY table_name ORDER BY usageCount DESC LIMIT 10').all();
|
|
1965
|
+
const usageByType = {};
|
|
1966
|
+
const typeRows = this.db.prepare('SELECT usage_type, COUNT(*) as cnt FROM schema_usage GROUP BY usage_type').all();
|
|
1967
|
+
for (const r of typeRows) {
|
|
1968
|
+
usageByType[r.usage_type] = r.cnt;
|
|
1969
|
+
}
|
|
1970
|
+
return { totalSchemas, totalTables, totalUsagePatterns, mostUsedTables, usageByType };
|
|
1971
|
+
}
|
|
1972
|
+
async searchWithSchemaContext(query, options = {}) {
|
|
1973
|
+
const searchResult = await this.searchFunctions(options);
|
|
1974
|
+
if (!options.includeSchemaUsage)
|
|
1975
|
+
return searchResult;
|
|
1976
|
+
const enhancedFunctions = await Promise.all(searchResult.functions.map(async (func) => {
|
|
1977
|
+
const schemaUsage = await this.getSchemaUsage({
|
|
1978
|
+
filePath: func.filePath,
|
|
1979
|
+
functionName: func.name
|
|
1980
|
+
});
|
|
1981
|
+
return {
|
|
1982
|
+
...func,
|
|
1983
|
+
schemaUsage,
|
|
1984
|
+
affectedTables: [...new Set(schemaUsage.map(u => u.tableName))],
|
|
1985
|
+
schemaPatterns: [...new Set(schemaUsage.map(u => u.usageType))]
|
|
1986
|
+
};
|
|
1987
|
+
}));
|
|
1988
|
+
const allSchemaUsage = enhancedFunctions.flatMap(f => f.schemaUsage || []);
|
|
1989
|
+
const schemaContext = [...new Set(allSchemaUsage.map((u) => ({
|
|
1990
|
+
tableName: u.tableName, usageType: u.usageType
|
|
1991
|
+
})))];
|
|
1992
|
+
return { ...searchResult, functions: enhancedFunctions, schemaContext };
|
|
1993
|
+
}
|
|
1994
|
+
// ── Project tasks ───────────────────────────────────────────────────
|
|
1995
|
+
getTaskRepository() {
|
|
1996
|
+
this.ensureInitialized();
|
|
1997
|
+
if (!this.taskRepository) {
|
|
1998
|
+
this.taskRepository = new ProjectTaskRepository(() => this.tasksAdapter, () => { });
|
|
1999
|
+
}
|
|
2000
|
+
return this.taskRepository;
|
|
2001
|
+
}
|
|
2002
|
+
async createProjectTask(input) {
|
|
2003
|
+
return this.getTaskRepository().create(input);
|
|
2004
|
+
}
|
|
2005
|
+
async getProjectTask(taskId) {
|
|
2006
|
+
return this.getTaskRepository().getById(taskId);
|
|
2007
|
+
}
|
|
2008
|
+
async listProjectTasks(projectPath, options) {
|
|
2009
|
+
return this.getTaskRepository().list(projectPath, options);
|
|
2010
|
+
}
|
|
2011
|
+
async listProjectTasksTree(projectPath, options) {
|
|
2012
|
+
return this.getTaskRepository().listTree(projectPath, options);
|
|
2013
|
+
}
|
|
2014
|
+
async listActionableProjectTasks(projectPath, options) {
|
|
2015
|
+
return this.getTaskRepository().listActionable(projectPath, options);
|
|
2016
|
+
}
|
|
2017
|
+
async completeProjectTask(taskId) {
|
|
2018
|
+
return this.getTaskRepository().complete(taskId);
|
|
2019
|
+
}
|
|
2020
|
+
async updateProjectTask(taskId, patch) {
|
|
2021
|
+
return this.getTaskRepository().update(taskId, patch);
|
|
2022
|
+
}
|
|
2023
|
+
async deleteProjectTask(taskId, mode) {
|
|
2024
|
+
return this.getTaskRepository().delete(taskId, mode);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
2028
|
+
function tryParseJson(val) {
|
|
2029
|
+
if (val === null || val === undefined)
|
|
2030
|
+
return null;
|
|
2031
|
+
if (typeof val === 'object')
|
|
2032
|
+
return val;
|
|
2033
|
+
if (typeof val !== 'string')
|
|
2034
|
+
return val;
|
|
2035
|
+
try {
|
|
2036
|
+
return JSON.parse(val);
|
|
2037
|
+
}
|
|
2038
|
+
catch {
|
|
2039
|
+
return val;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
//# sourceMappingURL=codeIndexDB.js.map
|