d365fo-mcp 1.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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/bridge/bridgeAdapter.d.ts +319 -0
- package/dist/bridge/bridgeAdapter.d.ts.map +1 -0
- package/dist/bridge/bridgeAdapter.js +2003 -0
- package/dist/bridge/bridgeAdapter.js.map +1 -0
- package/dist/bridge/bridgeClient.d.ts +255 -0
- package/dist/bridge/bridgeClient.d.ts.map +1 -0
- package/dist/bridge/bridgeClient.js +741 -0
- package/dist/bridge/bridgeClient.js.map +1 -0
- package/dist/bridge/bridgeTypes.d.ts +561 -0
- package/dist/bridge/bridgeTypes.d.ts.map +1 -0
- package/dist/bridge/bridgeTypes.js +6 -0
- package/dist/bridge/bridgeTypes.js.map +1 -0
- package/dist/bridge/debouncedRefresh.d.ts +16 -0
- package/dist/bridge/debouncedRefresh.d.ts.map +1 -0
- package/dist/bridge/debouncedRefresh.js +77 -0
- package/dist/bridge/debouncedRefresh.js.map +1 -0
- package/dist/bridge/index.d.ts +21 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +20 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +177 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/indexCmd.d.ts +8 -0
- package/dist/cli/commands/indexCmd.d.ts.map +1 -0
- package/dist/cli/commands/indexCmd.js +78 -0
- package/dist/cli/commands/indexCmd.js.map +1 -0
- package/dist/cli/commands/instance.d.ts +4 -0
- package/dist/cli/commands/instance.d.ts.map +1 -0
- package/dist/cli/commands/instance.js +183 -0
- package/dist/cli/commands/instance.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +4 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +246 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +49 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/update.d.ts +4 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +55 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/context.d.ts +26 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +38 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/envFile.d.ts +23 -0
- package/dist/cli/envFile.d.ts.map +1 -0
- package/dist/cli/envFile.js +79 -0
- package/dist/cli/envFile.js.map +1 -0
- package/dist/cli/exec.d.ts +13 -0
- package/dist/cli/exec.d.ts.map +1 -0
- package/dist/cli/exec.js +47 -0
- package/dist/cli/exec.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +122 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/instances.d.ts +19 -0
- package/dist/cli/instances.d.ts.map +1 -0
- package/dist/cli/instances.js +60 -0
- package/dist/cli/instances.js.map +1 -0
- package/dist/cli/mcpJson.d.ts +15 -0
- package/dist/cli/mcpJson.d.ts.map +1 -0
- package/dist/cli/mcpJson.js +44 -0
- package/dist/cli/mcpJson.js.map +1 -0
- package/dist/cli/settingsPrompt.d.ts +20 -0
- package/dist/cli/settingsPrompt.d.ts.map +1 -0
- package/dist/cli/settingsPrompt.js +150 -0
- package/dist/cli/settingsPrompt.js.map +1 -0
- package/dist/cli/settingsStore.d.ts +47 -0
- package/dist/cli/settingsStore.d.ts.map +1 -0
- package/dist/cli/settingsStore.js +144 -0
- package/dist/cli/settingsStore.js.map +1 -0
- package/dist/cli/target.d.ts +23 -0
- package/dist/cli/target.d.ts.map +1 -0
- package/dist/cli/target.js +62 -0
- package/dist/cli/target.js.map +1 -0
- package/dist/cli/ui.d.ts +26 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +48 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli/xppConfig.d.ts +35 -0
- package/dist/cli/xppConfig.d.ts.map +1 -0
- package/dist/cli/xppConfig.js +97 -0
- package/dist/cli/xppConfig.js.map +1 -0
- package/dist/config/configFile.d.ts +50 -0
- package/dist/config/configFile.d.ts.map +1 -0
- package/dist/config/configFile.js +152 -0
- package/dist/config/configFile.js.map +1 -0
- package/dist/config/settings.d.ts +58 -0
- package/dist/config/settings.d.ts.map +1 -0
- package/dist/config/settings.js +641 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/database/download.d.ts +27 -0
- package/dist/database/download.d.ts.map +1 -0
- package/dist/database/download.js +241 -0
- package/dist/database/download.js.map +1 -0
- package/dist/eval/audit/knowledgeAudit.d.ts +66 -0
- package/dist/eval/audit/knowledgeAudit.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeAudit.js +117 -0
- package/dist/eval/audit/knowledgeAudit.js.map +1 -0
- package/dist/eval/audit/knowledgeAuditCli.d.ts +17 -0
- package/dist/eval/audit/knowledgeAuditCli.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeAuditCli.js +83 -0
- package/dist/eval/audit/knowledgeAuditCli.js.map +1 -0
- package/dist/eval/audit/knowledgeRefs.d.ts +37 -0
- package/dist/eval/audit/knowledgeRefs.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeRefs.js +150 -0
- package/dist/eval/audit/knowledgeRefs.js.map +1 -0
- package/dist/eval/audit/symbolLookup.d.ts +23 -0
- package/dist/eval/audit/symbolLookup.d.ts.map +1 -0
- package/dist/eval/audit/symbolLookup.js +80 -0
- package/dist/eval/audit/symbolLookup.js.map +1 -0
- package/dist/eval/coverage/coverage.d.ts +65 -0
- package/dist/eval/coverage/coverage.d.ts.map +1 -0
- package/dist/eval/coverage/coverage.js +136 -0
- package/dist/eval/coverage/coverage.js.map +1 -0
- package/dist/eval/coverage/coverageCli.d.ts +13 -0
- package/dist/eval/coverage/coverageCli.d.ts.map +1 -0
- package/dist/eval/coverage/coverageCli.js +106 -0
- package/dist/eval/coverage/coverageCli.js.map +1 -0
- package/dist/eval/coverage/sources.d.ts +17 -0
- package/dist/eval/coverage/sources.d.ts.map +1 -0
- package/dist/eval/coverage/sources.js +40 -0
- package/dist/eval/coverage/sources.js.map +1 -0
- package/dist/eval/coverage/taxonomy.d.ts +59 -0
- package/dist/eval/coverage/taxonomy.d.ts.map +1 -0
- package/dist/eval/coverage/taxonomy.js +400 -0
- package/dist/eval/coverage/taxonomy.js.map +1 -0
- package/dist/eval/improver/caseMining.d.ts +55 -0
- package/dist/eval/improver/caseMining.d.ts.map +1 -0
- package/dist/eval/improver/caseMining.js +64 -0
- package/dist/eval/improver/caseMining.js.map +1 -0
- package/dist/eval/improver/caseMiningCli.d.ts +10 -0
- package/dist/eval/improver/caseMiningCli.d.ts.map +1 -0
- package/dist/eval/improver/caseMiningCli.js +50 -0
- package/dist/eval/improver/caseMiningCli.js.map +1 -0
- package/dist/eval/improver/cli.d.ts +11 -0
- package/dist/eval/improver/cli.d.ts.map +1 -0
- package/dist/eval/improver/cli.js +73 -0
- package/dist/eval/improver/cli.js.map +1 -0
- package/dist/eval/improver/cluster.d.ts +47 -0
- package/dist/eval/improver/cluster.d.ts.map +1 -0
- package/dist/eval/improver/cluster.js +0 -0
- package/dist/eval/improver/cluster.js.map +1 -0
- package/dist/eval/improver/corpusIO.d.ts +26 -0
- package/dist/eval/improver/corpusIO.d.ts.map +1 -0
- package/dist/eval/improver/corpusIO.js +50 -0
- package/dist/eval/improver/corpusIO.js.map +1 -0
- package/dist/eval/improver/fixBrief.d.ts +34 -0
- package/dist/eval/improver/fixBrief.d.ts.map +1 -0
- package/dist/eval/improver/fixBrief.js +69 -0
- package/dist/eval/improver/fixBrief.js.map +1 -0
- package/dist/eval/improver/fixBriefCli.d.ts +11 -0
- package/dist/eval/improver/fixBriefCli.d.ts.map +1 -0
- package/dist/eval/improver/fixBriefCli.js +46 -0
- package/dist/eval/improver/fixBriefCli.js.map +1 -0
- package/dist/eval/improver/flakeDetection.d.ts +50 -0
- package/dist/eval/improver/flakeDetection.d.ts.map +1 -0
- package/dist/eval/improver/flakeDetection.js +0 -0
- package/dist/eval/improver/flakeDetection.js.map +1 -0
- package/dist/eval/improver/flakeDetectionCli.d.ts +8 -0
- package/dist/eval/improver/flakeDetectionCli.d.ts.map +1 -0
- package/dist/eval/improver/flakeDetectionCli.js +27 -0
- package/dist/eval/improver/flakeDetectionCli.js.map +1 -0
- package/dist/eval/improver/heldout.d.ts +44 -0
- package/dist/eval/improver/heldout.d.ts.map +1 -0
- package/dist/eval/improver/heldout.js +52 -0
- package/dist/eval/improver/heldout.js.map +1 -0
- package/dist/eval/improver/knowledgeFeedback.d.ts +56 -0
- package/dist/eval/improver/knowledgeFeedback.d.ts.map +1 -0
- package/dist/eval/improver/knowledgeFeedback.js +88 -0
- package/dist/eval/improver/knowledgeFeedback.js.map +1 -0
- package/dist/eval/improver/knowledgeFeedbackCli.d.ts +8 -0
- package/dist/eval/improver/knowledgeFeedbackCli.d.ts.map +1 -0
- package/dist/eval/improver/knowledgeFeedbackCli.js +28 -0
- package/dist/eval/improver/knowledgeFeedbackCli.js.map +1 -0
- package/dist/eval/improver/report.d.ts +36 -0
- package/dist/eval/improver/report.d.ts.map +1 -0
- package/dist/eval/improver/report.js +61 -0
- package/dist/eval/improver/report.js.map +1 -0
- package/dist/eval/improver/reportCli.d.ts +7 -0
- package/dist/eval/improver/reportCli.d.ts.map +1 -0
- package/dist/eval/improver/reportCli.js +27 -0
- package/dist/eval/improver/reportCli.js.map +1 -0
- package/dist/eval/oracle/actualArtifactResolution.d.ts +49 -0
- package/dist/eval/oracle/actualArtifactResolution.d.ts.map +1 -0
- package/dist/eval/oracle/actualArtifactResolution.js +73 -0
- package/dist/eval/oracle/actualArtifactResolution.js.map +1 -0
- package/dist/eval/oracle/cli.d.ts +30 -0
- package/dist/eval/oracle/cli.d.ts.map +1 -0
- package/dist/eval/oracle/cli.js +179 -0
- package/dist/eval/oracle/cli.js.map +1 -0
- package/dist/eval/oracle/diff.d.ts +26 -0
- package/dist/eval/oracle/diff.d.ts.map +1 -0
- package/dist/eval/oracle/diff.js +51 -0
- package/dist/eval/oracle/diff.js.map +1 -0
- package/dist/eval/oracle/index.d.ts +81 -0
- package/dist/eval/oracle/index.d.ts.map +1 -0
- package/dist/eval/oracle/index.js +55 -0
- package/dist/eval/oracle/index.js.map +1 -0
- package/dist/eval/oracle/normalize.d.ts +85 -0
- package/dist/eval/oracle/normalize.d.ts.map +1 -0
- package/dist/eval/oracle/normalize.js +321 -0
- package/dist/eval/oracle/normalize.js.map +1 -0
- package/dist/eval/oracle/score.d.ts +26 -0
- package/dist/eval/oracle/score.d.ts.map +1 -0
- package/dist/eval/oracle/score.js +15 -0
- package/dist/eval/oracle/score.js.map +1 -0
- package/dist/eval/oracle/systest.d.ts +30 -0
- package/dist/eval/oracle/systest.d.ts.map +1 -0
- package/dist/eval/oracle/systest.js +98 -0
- package/dist/eval/oracle/systest.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +753 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.js +38 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.d.ts +21 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.js +168 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.js +47 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.d.ts +16 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.js +124 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.d.ts +12 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.js +36 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.d.ts +11 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.js +37 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.d.ts +8 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.js +86 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.js +69 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.d.ts +14 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.js +187 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.js +54 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.js +33 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.js +95 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.d.ts +16 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.js +158 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.js +34 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.js +99 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.d.ts +8 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.js +95 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.js +42 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.js +42 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.d.ts +17 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.js +157 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.js.map +1 -0
- package/dist/knowledge/formPatterns/crossCheck.d.ts +44 -0
- package/dist/knowledge/formPatterns/crossCheck.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/crossCheck.js +109 -0
- package/dist/knowledge/formPatterns/crossCheck.js.map +1 -0
- package/dist/knowledge/formPatterns/index.d.ts +30 -0
- package/dist/knowledge/formPatterns/index.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/index.js +158 -0
- package/dist/knowledge/formPatterns/index.js.map +1 -0
- package/dist/knowledge/formPatterns/methodStubs.d.ts +45 -0
- package/dist/knowledge/formPatterns/methodStubs.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/methodStubs.js +257 -0
- package/dist/knowledge/formPatterns/methodStubs.js.map +1 -0
- package/dist/knowledge/formPatterns/types.d.ts +115 -0
- package/dist/knowledge/formPatterns/types.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/types.js +44 -0
- package/dist/knowledge/formPatterns/types.js.map +1 -0
- package/dist/metadata/enhancedParser.d.ts +91 -0
- package/dist/metadata/enhancedParser.d.ts.map +1 -0
- package/dist/metadata/enhancedParser.js +379 -0
- package/dist/metadata/enhancedParser.js.map +1 -0
- package/dist/metadata/formPatternMiner.d.ts +78 -0
- package/dist/metadata/formPatternMiner.d.ts.map +1 -0
- package/dist/metadata/formPatternMiner.js +220 -0
- package/dist/metadata/formPatternMiner.js.map +1 -0
- package/dist/metadata/labelParser.d.ts +71 -0
- package/dist/metadata/labelParser.d.ts.map +1 -0
- package/dist/metadata/labelParser.js +258 -0
- package/dist/metadata/labelParser.js.map +1 -0
- package/dist/metadata/symbolCountsWorker.d.ts +16 -0
- package/dist/metadata/symbolCountsWorker.d.ts.map +1 -0
- package/dist/metadata/symbolCountsWorker.js +42 -0
- package/dist/metadata/symbolCountsWorker.js.map +1 -0
- package/dist/metadata/symbolIndex.d.ts +474 -0
- package/dist/metadata/symbolIndex.d.ts.map +1 -0
- package/dist/metadata/symbolIndex.js +3152 -0
- package/dist/metadata/symbolIndex.js.map +1 -0
- package/dist/metadata/types.d.ts +267 -0
- package/dist/metadata/types.d.ts.map +1 -0
- package/dist/metadata/types.js +5 -0
- package/dist/metadata/types.js.map +1 -0
- package/dist/metadata/xmlParser.d.ts +232 -0
- package/dist/metadata/xmlParser.d.ts.map +1 -0
- package/dist/metadata/xmlParser.js +1021 -0
- package/dist/metadata/xmlParser.js.map +1 -0
- package/dist/metadata/xppDeclaration.d.ts +116 -0
- package/dist/metadata/xppDeclaration.d.ts.map +1 -0
- package/dist/metadata/xppDeclaration.js +350 -0
- package/dist/metadata/xppDeclaration.js.map +1 -0
- package/dist/middleware/apiKeyAuth.d.ts +7 -0
- package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
- package/dist/middleware/apiKeyAuth.js +70 -0
- package/dist/middleware/apiKeyAuth.js.map +1 -0
- package/dist/middleware/rateLimiter.d.ts +7 -0
- package/dist/middleware/rateLimiter.d.ts.map +1 -0
- package/dist/middleware/rateLimiter.js +78 -0
- package/dist/middleware/rateLimiter.js.map +1 -0
- package/dist/prompts/codeReview.d.ts +8 -0
- package/dist/prompts/codeReview.d.ts.map +1 -0
- package/dist/prompts/codeReview.js +619 -0
- package/dist/prompts/codeReview.js.map +1 -0
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +6 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/systemInstructions.d.ts +33 -0
- package/dist/prompts/systemInstructions.d.ts.map +1 -0
- package/dist/prompts/systemInstructions.js +154 -0
- package/dist/prompts/systemInstructions.js.map +1 -0
- package/dist/resources/classResource.d.ts +17 -0
- package/dist/resources/classResource.d.ts.map +1 -0
- package/dist/resources/classResource.js +33 -0
- package/dist/resources/classResource.js.map +1 -0
- package/dist/resources/index.d.ts +20 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +154 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server/mcpServer.d.ts +11 -0
- package/dist/server/mcpServer.d.ts.map +1 -0
- package/dist/server/mcpServer.js +185 -0
- package/dist/server/mcpServer.js.map +1 -0
- package/dist/server/serverMode.d.ts +53 -0
- package/dist/server/serverMode.d.ts.map +1 -0
- package/dist/server/serverMode.js +72 -0
- package/dist/server/serverMode.js.map +1 -0
- package/dist/server/toolAnnotations.d.ts +28 -0
- package/dist/server/toolAnnotations.d.ts.map +1 -0
- package/dist/server/toolAnnotations.js +70 -0
- package/dist/server/toolAnnotations.js.map +1 -0
- package/dist/server/toolSchemas/analyzeCode.d.ts +71 -0
- package/dist/server/toolSchemas/analyzeCode.d.ts.map +1 -0
- package/dist/server/toolSchemas/analyzeCode.js +50 -0
- package/dist/server/toolSchemas/analyzeCode.js.map +1 -0
- package/dist/server/toolSchemas/batchGetInfo.d.ts +32 -0
- package/dist/server/toolSchemas/batchGetInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/batchGetInfo.js +35 -0
- package/dist/server/toolSchemas/batchGetInfo.js.map +1 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts +44 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts.map +1 -0
- package/dist/server/toolSchemas/buildD365foProject.js +47 -0
- package/dist/server/toolSchemas/buildD365foProject.js.map +1 -0
- package/dist/server/toolSchemas/d365foFile.d.ts +99 -0
- package/dist/server/toolSchemas/d365foFile.d.ts.map +1 -0
- package/dist/server/toolSchemas/d365foFile.js +163 -0
- package/dist/server/toolSchemas/d365foFile.js.map +1 -0
- package/dist/server/toolSchemas/extensionInfo.d.ts +65 -0
- package/dist/server/toolSchemas/extensionInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/extensionInfo.js +73 -0
- package/dist/server/toolSchemas/extensionInfo.js.map +1 -0
- package/dist/server/toolSchemas/findReferences.d.ts +35 -0
- package/dist/server/toolSchemas/findReferences.d.ts.map +1 -0
- package/dist/server/toolSchemas/findReferences.js +35 -0
- package/dist/server/toolSchemas/findReferences.js.map +1 -0
- package/dist/server/toolSchemas/generateObject.d.ts +246 -0
- package/dist/server/toolSchemas/generateObject.d.ts.map +1 -0
- package/dist/server/toolSchemas/generateObject.js +158 -0
- package/dist/server/toolSchemas/generateObject.js.map +1 -0
- package/dist/server/toolSchemas/getKnowledge.d.ts +39 -0
- package/dist/server/toolSchemas/getKnowledge.d.ts.map +1 -0
- package/dist/server/toolSchemas/getKnowledge.js +46 -0
- package/dist/server/toolSchemas/getKnowledge.js.map +1 -0
- package/dist/server/toolSchemas/getMethod.d.ts +30 -0
- package/dist/server/toolSchemas/getMethod.d.ts.map +1 -0
- package/dist/server/toolSchemas/getMethod.js +34 -0
- package/dist/server/toolSchemas/getMethod.js.map +1 -0
- package/dist/server/toolSchemas/getObjectInfo.d.ts +24 -0
- package/dist/server/toolSchemas/getObjectInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/getObjectInfo.js +30 -0
- package/dist/server/toolSchemas/getObjectInfo.js.map +1 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts +29 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js +29 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js.map +1 -0
- package/dist/server/toolSchemas/index.d.ts +1142 -0
- package/dist/server/toolSchemas/index.d.ts.map +1 -0
- package/dist/server/toolSchemas/index.js +60 -0
- package/dist/server/toolSchemas/index.js.map +1 -0
- package/dist/server/toolSchemas/labels.d.ts +172 -0
- package/dist/server/toolSchemas/labels.d.ts.map +1 -0
- package/dist/server/toolSchemas/labels.js +159 -0
- package/dist/server/toolSchemas/labels.js.map +1 -0
- package/dist/server/toolSchemas/objectPatterns.d.ts +93 -0
- package/dist/server/toolSchemas/objectPatterns.d.ts.map +1 -0
- package/dist/server/toolSchemas/objectPatterns.js +104 -0
- package/dist/server/toolSchemas/objectPatterns.js.map +1 -0
- package/dist/server/toolSchemas/prepare.d.ts +49 -0
- package/dist/server/toolSchemas/prepare.d.ts.map +1 -0
- package/dist/server/toolSchemas/prepare.js +57 -0
- package/dist/server/toolSchemas/prepare.js.map +1 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts +20 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts.map +1 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js +19 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js.map +1 -0
- package/dist/server/toolSchemas/runBpCheck.d.ts +36 -0
- package/dist/server/toolSchemas/runBpCheck.d.ts.map +1 -0
- package/dist/server/toolSchemas/runBpCheck.js +21 -0
- package/dist/server/toolSchemas/runBpCheck.js.map +1 -0
- package/dist/server/toolSchemas/runSystestClass.d.ts +32 -0
- package/dist/server/toolSchemas/runSystestClass.d.ts.map +1 -0
- package/dist/server/toolSchemas/runSystestClass.js +20 -0
- package/dist/server/toolSchemas/runSystestClass.js.map +1 -0
- package/dist/server/toolSchemas/search.d.ts +109 -0
- package/dist/server/toolSchemas/search.d.ts.map +1 -0
- package/dist/server/toolSchemas/search.js +123 -0
- package/dist/server/toolSchemas/search.js.map +1 -0
- package/dist/server/toolSchemas/securityInfo.d.ts +45 -0
- package/dist/server/toolSchemas/securityInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/securityInfo.js +39 -0
- package/dist/server/toolSchemas/securityInfo.js.map +1 -0
- package/dist/server/toolSchemas/suggestEdt.d.ts +29 -0
- package/dist/server/toolSchemas/suggestEdt.d.ts.map +1 -0
- package/dist/server/toolSchemas/suggestEdt.js +29 -0
- package/dist/server/toolSchemas/suggestEdt.js.map +1 -0
- package/dist/server/toolSchemas/triggerDbSync.d.ts +47 -0
- package/dist/server/toolSchemas/triggerDbSync.d.ts.map +1 -0
- package/dist/server/toolSchemas/triggerDbSync.js +31 -0
- package/dist/server/toolSchemas/triggerDbSync.js.map +1 -0
- package/dist/server/toolSchemas/undoLastModification.d.ts +20 -0
- package/dist/server/toolSchemas/undoLastModification.d.ts.map +1 -0
- package/dist/server/toolSchemas/undoLastModification.js +17 -0
- package/dist/server/toolSchemas/undoLastModification.js.map +1 -0
- package/dist/server/toolSchemas/updateSymbolIndex.d.ts +19 -0
- package/dist/server/toolSchemas/updateSymbolIndex.d.ts.map +1 -0
- package/dist/server/toolSchemas/updateSymbolIndex.js +18 -0
- package/dist/server/toolSchemas/updateSymbolIndex.js.map +1 -0
- package/dist/server/toolSchemas/validateCode.d.ts +35 -0
- package/dist/server/toolSchemas/validateCode.d.ts.map +1 -0
- package/dist/server/toolSchemas/validateCode.js +38 -0
- package/dist/server/toolSchemas/validateCode.js.map +1 -0
- package/dist/server/toolSchemas/validateObjectNaming.d.ts +33 -0
- package/dist/server/toolSchemas/validateObjectNaming.d.ts.map +1 -0
- package/dist/server/toolSchemas/validateObjectNaming.js +32 -0
- package/dist/server/toolSchemas/validateObjectNaming.js.map +1 -0
- package/dist/server/toolSchemas/verifyD365foProject.d.ts +50 -0
- package/dist/server/toolSchemas/verifyD365foProject.d.ts.map +1 -0
- package/dist/server/toolSchemas/verifyD365foProject.js +46 -0
- package/dist/server/toolSchemas/verifyD365foProject.js.map +1 -0
- package/dist/server/transport.d.ts +30 -0
- package/dist/server/transport.d.ts.map +1 -0
- package/dist/server/transport.js +391 -0
- package/dist/server/transport.js.map +1 -0
- package/dist/tools/analyzeCode.d.ts +17 -0
- package/dist/tools/analyzeCode.d.ts.map +1 -0
- package/dist/tools/analyzeCode.js +63 -0
- package/dist/tools/analyzeCode.js.map +1 -0
- package/dist/tools/analyzeCompleteness.d.ts +20 -0
- package/dist/tools/analyzeCompleteness.d.ts.map +1 -0
- package/dist/tools/analyzeCompleteness.js +103 -0
- package/dist/tools/analyzeCompleteness.js.map +1 -0
- package/dist/tools/analyzeExtensionPoints.d.ts +21 -0
- package/dist/tools/analyzeExtensionPoints.d.ts.map +1 -0
- package/dist/tools/analyzeExtensionPoints.js +343 -0
- package/dist/tools/analyzeExtensionPoints.js.map +1 -0
- package/dist/tools/analyzePatterns.d.ts +20 -0
- package/dist/tools/analyzePatterns.d.ts.map +1 -0
- package/dist/tools/analyzePatterns.js +71 -0
- package/dist/tools/analyzePatterns.js.map +1 -0
- package/dist/tools/apiUsagePatterns.d.ts +20 -0
- package/dist/tools/apiUsagePatterns.d.ts.map +1 -0
- package/dist/tools/apiUsagePatterns.js +136 -0
- package/dist/tools/apiUsagePatterns.js.map +1 -0
- package/dist/tools/batchGetInfo.d.ts +50 -0
- package/dist/tools/batchGetInfo.d.ts.map +1 -0
- package/dist/tools/batchGetInfo.js +52 -0
- package/dist/tools/batchGetInfo.js.map +1 -0
- package/dist/tools/batchSearch.d.ts +64 -0
- package/dist/tools/batchSearch.d.ts.map +1 -0
- package/dist/tools/batchSearch.js +265 -0
- package/dist/tools/batchSearch.js.map +1 -0
- package/dist/tools/buildProject.d.ts +28 -0
- package/dist/tools/buildProject.d.ts.map +1 -0
- package/dist/tools/buildProject.js +929 -0
- package/dist/tools/buildProject.js.map +1 -0
- package/dist/tools/classInfo.d.ts +8 -0
- package/dist/tools/classInfo.d.ts.map +1 -0
- package/dist/tools/classInfo.js +235 -0
- package/dist/tools/classInfo.js.map +1 -0
- package/dist/tools/codeGen.d.ts +19 -0
- package/dist/tools/codeGen.d.ts.map +1 -0
- package/dist/tools/codeGen.js +1956 -0
- package/dist/tools/codeGen.js.map +1 -0
- package/dist/tools/completion.d.ts +8 -0
- package/dist/tools/completion.d.ts.map +1 -0
- package/dist/tools/completion.js +272 -0
- package/dist/tools/completion.js.map +1 -0
- package/dist/tools/configKeyInfo.d.ts +22 -0
- package/dist/tools/configKeyInfo.d.ts.map +1 -0
- package/dist/tools/configKeyInfo.js +74 -0
- package/dist/tools/configKeyInfo.js.map +1 -0
- package/dist/tools/createD365File.d.ts +479 -0
- package/dist/tools/createD365File.d.ts.map +1 -0
- package/dist/tools/createD365File.js +4185 -0
- package/dist/tools/createD365File.js.map +1 -0
- package/dist/tools/createLabel.d.ts +51 -0
- package/dist/tools/createLabel.d.ts.map +1 -0
- package/dist/tools/createLabel.js +761 -0
- package/dist/tools/createLabel.js.map +1 -0
- package/dist/tools/d365foErrorHelp.d.ts +29 -0
- package/dist/tools/d365foErrorHelp.d.ts.map +1 -0
- package/dist/tools/d365foErrorHelp.js +389 -0
- package/dist/tools/d365foErrorHelp.js.map +1 -0
- package/dist/tools/d365foFile.d.ts +20 -0
- package/dist/tools/d365foFile.d.ts.map +1 -0
- package/dist/tools/d365foFile.js +58 -0
- package/dist/tools/d365foFile.js.map +1 -0
- package/dist/tools/d365foFileOpSpecs.d.ts +41 -0
- package/dist/tools/d365foFileOpSpecs.d.ts.map +1 -0
- package/dist/tools/d365foFileOpSpecs.js +336 -0
- package/dist/tools/d365foFileOpSpecs.js.map +1 -0
- package/dist/tools/dataEntityInfo.d.ts +23 -0
- package/dist/tools/dataEntityInfo.d.ts.map +1 -0
- package/dist/tools/dataEntityInfo.js +45 -0
- package/dist/tools/dataEntityInfo.js.map +1 -0
- package/dist/tools/dataEntityXml.d.ts +31 -0
- package/dist/tools/dataEntityXml.d.ts.map +1 -0
- package/dist/tools/dataEntityXml.js +131 -0
- package/dist/tools/dataEntityXml.js.map +1 -0
- package/dist/tools/dbSync.d.ts +8 -0
- package/dist/tools/dbSync.d.ts.map +1 -0
- package/dist/tools/dbSync.js +285 -0
- package/dist/tools/dbSync.js.map +1 -0
- package/dist/tools/edtInfo.d.ts +23 -0
- package/dist/tools/edtInfo.d.ts.map +1 -0
- package/dist/tools/edtInfo.js +122 -0
- package/dist/tools/edtInfo.js.map +1 -0
- package/dist/tools/enumInfo.d.ts +17 -0
- package/dist/tools/enumInfo.d.ts.map +1 -0
- package/dist/tools/enumInfo.js +44 -0
- package/dist/tools/enumInfo.js.map +1 -0
- package/dist/tools/extensionInfo.d.ts +26 -0
- package/dist/tools/extensionInfo.d.ts.map +1 -0
- package/dist/tools/extensionInfo.js +73 -0
- package/dist/tools/extensionInfo.js.map +1 -0
- package/dist/tools/extensionSearch.d.ts +20 -0
- package/dist/tools/extensionSearch.d.ts.map +1 -0
- package/dist/tools/extensionSearch.js +72 -0
- package/dist/tools/extensionSearch.js.map +1 -0
- package/dist/tools/extensionStrategyAdvisor.d.ts +15 -0
- package/dist/tools/extensionStrategyAdvisor.d.ts.map +1 -0
- package/dist/tools/extensionStrategyAdvisor.js +519 -0
- package/dist/tools/extensionStrategyAdvisor.js.map +1 -0
- package/dist/tools/findCocExtensions.d.ts +20 -0
- package/dist/tools/findCocExtensions.d.ts.map +1 -0
- package/dist/tools/findCocExtensions.js +285 -0
- package/dist/tools/findCocExtensions.js.map +1 -0
- package/dist/tools/findEventHandlers.d.ts +21 -0
- package/dist/tools/findEventHandlers.d.ts.map +1 -0
- package/dist/tools/findEventHandlers.js +196 -0
- package/dist/tools/findEventHandlers.js.map +1 -0
- package/dist/tools/findReferences.d.ts +30 -0
- package/dist/tools/findReferences.d.ts.map +1 -0
- package/dist/tools/findReferences.js +551 -0
- package/dist/tools/findReferences.js.map +1 -0
- package/dist/tools/formInfo.d.ts +13 -0
- package/dist/tools/formInfo.d.ts.map +1 -0
- package/dist/tools/formInfo.js +407 -0
- package/dist/tools/formInfo.js.map +1 -0
- package/dist/tools/formPattern.d.ts +19 -0
- package/dist/tools/formPattern.d.ts.map +1 -0
- package/dist/tools/formPattern.js +52 -0
- package/dist/tools/formPattern.js.map +1 -0
- package/dist/tools/generateD365Xml.d.ts +12 -0
- package/dist/tools/generateD365Xml.d.ts.map +1 -0
- package/dist/tools/generateD365Xml.js +1454 -0
- package/dist/tools/generateD365Xml.js.map +1 -0
- package/dist/tools/generateFindMethods.d.ts +69 -0
- package/dist/tools/generateFindMethods.d.ts.map +1 -0
- package/dist/tools/generateFindMethods.js +207 -0
- package/dist/tools/generateFindMethods.js.map +1 -0
- package/dist/tools/generateMetadata.d.ts +37 -0
- package/dist/tools/generateMetadata.d.ts.map +1 -0
- package/dist/tools/generateMetadata.js +213 -0
- package/dist/tools/generateMetadata.js.map +1 -0
- package/dist/tools/generateObject.d.ts +34 -0
- package/dist/tools/generateObject.d.ts.map +1 -0
- package/dist/tools/generateObject.js +47 -0
- package/dist/tools/generateObject.js.map +1 -0
- package/dist/tools/generateRelationXpp.d.ts +45 -0
- package/dist/tools/generateRelationXpp.d.ts.map +1 -0
- package/dist/tools/generateRelationXpp.js +129 -0
- package/dist/tools/generateRelationXpp.js.map +1 -0
- package/dist/tools/generateSmart.d.ts +32 -0
- package/dist/tools/generateSmart.d.ts.map +1 -0
- package/dist/tools/generateSmart.js +52 -0
- package/dist/tools/generateSmart.js.map +1 -0
- package/dist/tools/generateSmartForm.d.ts +82 -0
- package/dist/tools/generateSmartForm.d.ts.map +1 -0
- package/dist/tools/generateSmartForm.js +996 -0
- package/dist/tools/generateSmartForm.js.map +1 -0
- package/dist/tools/generateSmartReport.d.ts +90 -0
- package/dist/tools/generateSmartReport.d.ts.map +1 -0
- package/dist/tools/generateSmartReport.js +1254 -0
- package/dist/tools/generateSmartReport.js.map +1 -0
- package/dist/tools/generateSmartTable.d.ts +138 -0
- package/dist/tools/generateSmartTable.d.ts.map +1 -0
- package/dist/tools/generateSmartTable.js +1305 -0
- package/dist/tools/generateSmartTable.js.map +1 -0
- package/dist/tools/generateTableFields.d.ts +48 -0
- package/dist/tools/generateTableFields.d.ts.map +1 -0
- package/dist/tools/generateTableFields.js +186 -0
- package/dist/tools/generateTableFields.js.map +1 -0
- package/dist/tools/generateTableRelation.d.ts +35 -0
- package/dist/tools/generateTableRelation.d.ts.map +1 -0
- package/dist/tools/generateTableRelation.js +161 -0
- package/dist/tools/generateTableRelation.js.map +1 -0
- package/dist/tools/getFormPatternSpec.d.ts +13 -0
- package/dist/tools/getFormPatternSpec.d.ts.map +1 -0
- package/dist/tools/getFormPatternSpec.js +290 -0
- package/dist/tools/getFormPatternSpec.js.map +1 -0
- package/dist/tools/getFormPatterns.d.ts +68 -0
- package/dist/tools/getFormPatterns.d.ts.map +1 -0
- package/dist/tools/getFormPatterns.js +471 -0
- package/dist/tools/getFormPatterns.js.map +1 -0
- package/dist/tools/getKnowledge.d.ts +27 -0
- package/dist/tools/getKnowledge.d.ts.map +1 -0
- package/dist/tools/getKnowledge.js +50 -0
- package/dist/tools/getKnowledge.js.map +1 -0
- package/dist/tools/getLabelInfo.d.ts +21 -0
- package/dist/tools/getLabelInfo.d.ts.map +1 -0
- package/dist/tools/getLabelInfo.js +138 -0
- package/dist/tools/getLabelInfo.js.map +1 -0
- package/dist/tools/getMethod.d.ts +19 -0
- package/dist/tools/getMethod.d.ts.map +1 -0
- package/dist/tools/getMethod.js +77 -0
- package/dist/tools/getMethod.js.map +1 -0
- package/dist/tools/getMethodSource.d.ts +17 -0
- package/dist/tools/getMethodSource.d.ts.map +1 -0
- package/dist/tools/getMethodSource.js +161 -0
- package/dist/tools/getMethodSource.js.map +1 -0
- package/dist/tools/getObjectInfo.d.ts +17 -0
- package/dist/tools/getObjectInfo.d.ts.map +1 -0
- package/dist/tools/getObjectInfo.js +88 -0
- package/dist/tools/getObjectInfo.js.map +1 -0
- package/dist/tools/getTablePatterns.d.ts +45 -0
- package/dist/tools/getTablePatterns.d.ts.map +1 -0
- package/dist/tools/getTablePatterns.js +235 -0
- package/dist/tools/getTablePatterns.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +11 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/labels.d.ts +25 -0
- package/dist/tools/labels.d.ts.map +1 -0
- package/dist/tools/labels.js +111 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/macroInfo.d.ts +23 -0
- package/dist/tools/macroInfo.d.ts.map +1 -0
- package/dist/tools/macroInfo.js +62 -0
- package/dist/tools/macroInfo.js.map +1 -0
- package/dist/tools/mapInfo.d.ts +21 -0
- package/dist/tools/mapInfo.d.ts.map +1 -0
- package/dist/tools/mapInfo.js +54 -0
- package/dist/tools/mapInfo.js.map +1 -0
- package/dist/tools/mapXml.d.ts +26 -0
- package/dist/tools/mapXml.d.ts.map +1 -0
- package/dist/tools/mapXml.js +88 -0
- package/dist/tools/mapXml.js.map +1 -0
- package/dist/tools/menuItemInfo.d.ts +20 -0
- package/dist/tools/menuItemInfo.d.ts.map +1 -0
- package/dist/tools/menuItemInfo.js +185 -0
- package/dist/tools/menuItemInfo.js.map +1 -0
- package/dist/tools/methodSignature.d.ts +33 -0
- package/dist/tools/methodSignature.d.ts.map +1 -0
- package/dist/tools/methodSignature.js +348 -0
- package/dist/tools/methodSignature.js.map +1 -0
- package/dist/tools/modifyD365File.d.ts +130 -0
- package/dist/tools/modifyD365File.d.ts.map +1 -0
- package/dist/tools/modifyD365File.js +2179 -0
- package/dist/tools/modifyD365File.js.map +1 -0
- package/dist/tools/objectInfoRegistry.d.ts +36 -0
- package/dist/tools/objectInfoRegistry.d.ts.map +1 -0
- package/dist/tools/objectInfoRegistry.js +98 -0
- package/dist/tools/objectInfoRegistry.js.map +1 -0
- package/dist/tools/objectPatterns.d.ts +16 -0
- package/dist/tools/objectPatterns.d.ts.map +1 -0
- package/dist/tools/objectPatterns.js +84 -0
- package/dist/tools/objectPatterns.js.map +1 -0
- package/dist/tools/prepare.d.ts +18 -0
- package/dist/tools/prepare.d.ts.map +1 -0
- package/dist/tools/prepare.js +47 -0
- package/dist/tools/prepare.js.map +1 -0
- package/dist/tools/prepareChange.d.ts +43 -0
- package/dist/tools/prepareChange.d.ts.map +1 -0
- package/dist/tools/prepareChange.js +327 -0
- package/dist/tools/prepareChange.js.map +1 -0
- package/dist/tools/prepareCreate.d.ts +45 -0
- package/dist/tools/prepareCreate.d.ts.map +1 -0
- package/dist/tools/prepareCreate.js +263 -0
- package/dist/tools/prepareCreate.js.map +1 -0
- package/dist/tools/queryInfo.d.ts +17 -0
- package/dist/tools/queryInfo.d.ts.map +1 -0
- package/dist/tools/queryInfo.js +46 -0
- package/dist/tools/queryInfo.js.map +1 -0
- package/dist/tools/queryViewXml.d.ts +36 -0
- package/dist/tools/queryViewXml.d.ts.map +1 -0
- package/dist/tools/queryViewXml.js +129 -0
- package/dist/tools/queryViewXml.js.map +1 -0
- package/dist/tools/renameLabel.d.ts +29 -0
- package/dist/tools/renameLabel.d.ts.map +1 -0
- package/dist/tools/renameLabel.js +377 -0
- package/dist/tools/renameLabel.js.map +1 -0
- package/dist/tools/repairFormControls.d.ts +21 -0
- package/dist/tools/repairFormControls.d.ts.map +1 -0
- package/dist/tools/repairFormControls.js +151 -0
- package/dist/tools/repairFormControls.js.map +1 -0
- package/dist/tools/reportInfo.d.ts +14 -0
- package/dist/tools/reportInfo.d.ts.map +1 -0
- package/dist/tools/reportInfo.js +302 -0
- package/dist/tools/reportInfo.js.map +1 -0
- package/dist/tools/resolveReferences.d.ts +89 -0
- package/dist/tools/resolveReferences.d.ts.map +1 -0
- package/dist/tools/resolveReferences.js +791 -0
- package/dist/tools/resolveReferences.js.map +1 -0
- package/dist/tools/reviewWorkspaceChanges.d.ts +14 -0
- package/dist/tools/reviewWorkspaceChanges.d.ts.map +1 -0
- package/dist/tools/reviewWorkspaceChanges.js +73 -0
- package/dist/tools/reviewWorkspaceChanges.js.map +1 -0
- package/dist/tools/runBpCheck.d.ts +14 -0
- package/dist/tools/runBpCheck.d.ts.map +1 -0
- package/dist/tools/runBpCheck.js +241 -0
- package/dist/tools/runBpCheck.js.map +1 -0
- package/dist/tools/search.d.ts +20 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +246 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/searchLabels.d.ts +26 -0
- package/dist/tools/searchLabels.d.ts.map +1 -0
- package/dist/tools/searchLabels.js +91 -0
- package/dist/tools/searchLabels.js.map +1 -0
- package/dist/tools/searchUnified.d.ts +28 -0
- package/dist/tools/searchUnified.d.ts.map +1 -0
- package/dist/tools/searchUnified.js +48 -0
- package/dist/tools/searchUnified.js.map +1 -0
- package/dist/tools/securityArtifactInfo.d.ts +14 -0
- package/dist/tools/securityArtifactInfo.d.ts.map +1 -0
- package/dist/tools/securityArtifactInfo.js +192 -0
- package/dist/tools/securityArtifactInfo.js.map +1 -0
- package/dist/tools/securityCoverageInfo.d.ts +21 -0
- package/dist/tools/securityCoverageInfo.d.ts.map +1 -0
- package/dist/tools/securityCoverageInfo.js +248 -0
- package/dist/tools/securityCoverageInfo.js.map +1 -0
- package/dist/tools/securityInfo.d.ts +22 -0
- package/dist/tools/securityInfo.d.ts.map +1 -0
- package/dist/tools/securityInfo.js +43 -0
- package/dist/tools/securityInfo.js.map +1 -0
- package/dist/tools/securityPolicyInfo.d.ts +22 -0
- package/dist/tools/securityPolicyInfo.d.ts.map +1 -0
- package/dist/tools/securityPolicyInfo.js +49 -0
- package/dist/tools/securityPolicyInfo.js.map +1 -0
- package/dist/tools/securityPrivilegeXml.d.ts +21 -0
- package/dist/tools/securityPrivilegeXml.d.ts.map +1 -0
- package/dist/tools/securityPrivilegeXml.js +61 -0
- package/dist/tools/securityPrivilegeXml.js.map +1 -0
- package/dist/tools/serviceInfo.d.ts +25 -0
- package/dist/tools/serviceInfo.d.ts.map +1 -0
- package/dist/tools/serviceInfo.js +93 -0
- package/dist/tools/serviceInfo.js.map +1 -0
- package/dist/tools/suggestEdt.d.ts +15 -0
- package/dist/tools/suggestEdt.d.ts.map +1 -0
- package/dist/tools/suggestEdt.js +348 -0
- package/dist/tools/suggestEdt.js.map +1 -0
- package/dist/tools/suggestImplementation.d.ts +20 -0
- package/dist/tools/suggestImplementation.d.ts.map +1 -0
- package/dist/tools/suggestImplementation.js +144 -0
- package/dist/tools/suggestImplementation.js.map +1 -0
- package/dist/tools/sysTestRunner.d.ts +14 -0
- package/dist/tools/sysTestRunner.d.ts.map +1 -0
- package/dist/tools/sysTestRunner.js +145 -0
- package/dist/tools/sysTestRunner.js.map +1 -0
- package/dist/tools/tableExtensionInfo.d.ts +91 -0
- package/dist/tools/tableExtensionInfo.d.ts.map +1 -0
- package/dist/tools/tableExtensionInfo.js +305 -0
- package/dist/tools/tableExtensionInfo.js.map +1 -0
- package/dist/tools/tableInfo.d.ts +23 -0
- package/dist/tools/tableInfo.d.ts.map +1 -0
- package/dist/tools/tableInfo.js +125 -0
- package/dist/tools/tableInfo.js.map +1 -0
- package/dist/tools/toolHandler.d.ts +4 -0
- package/dist/tools/toolHandler.d.ts.map +1 -0
- package/dist/tools/toolHandler.js +508 -0
- package/dist/tools/toolHandler.js.map +1 -0
- package/dist/tools/undoLastModification.d.ts +15 -0
- package/dist/tools/undoLastModification.d.ts.map +1 -0
- package/dist/tools/undoLastModification.js +147 -0
- package/dist/tools/undoLastModification.js.map +1 -0
- package/dist/tools/updateSymbolIndex.d.ts +15 -0
- package/dist/tools/updateSymbolIndex.d.ts.map +1 -0
- package/dist/tools/updateSymbolIndex.js +520 -0
- package/dist/tools/updateSymbolIndex.js.map +1 -0
- package/dist/tools/validateCode.d.ts +24 -0
- package/dist/tools/validateCode.d.ts.map +1 -0
- package/dist/tools/validateCode.js +201 -0
- package/dist/tools/validateCode.js.map +1 -0
- package/dist/tools/validateFormPattern.d.ts +51 -0
- package/dist/tools/validateFormPattern.d.ts.map +1 -0
- package/dist/tools/validateFormPattern.js +201 -0
- package/dist/tools/validateFormPattern.js.map +1 -0
- package/dist/tools/validateObjectNaming.d.ts +21 -0
- package/dist/tools/validateObjectNaming.d.ts.map +1 -0
- package/dist/tools/validateObjectNaming.js +319 -0
- package/dist/tools/validateObjectNaming.js.map +1 -0
- package/dist/tools/validateXpp.d.ts +79 -0
- package/dist/tools/validateXpp.d.ts.map +1 -0
- package/dist/tools/validateXpp.js +705 -0
- package/dist/tools/validateXpp.js.map +1 -0
- package/dist/tools/verifyD365Project.d.ts +21 -0
- package/dist/tools/verifyD365Project.d.ts.map +1 -0
- package/dist/tools/verifyD365Project.js +299 -0
- package/dist/tools/verifyD365Project.js.map +1 -0
- package/dist/tools/viewInfo.d.ts +17 -0
- package/dist/tools/viewInfo.d.ts.map +1 -0
- package/dist/tools/viewInfo.js +46 -0
- package/dist/tools/viewInfo.js.map +1 -0
- package/dist/tools/xppKnowledge.d.ts +45 -0
- package/dist/tools/xppKnowledge.d.ts.map +1 -0
- package/dist/tools/xppKnowledge.js +2815 -0
- package/dist/tools/xppKnowledge.js.map +1 -0
- package/dist/types/context.d.ts +56 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +5 -0
- package/dist/types/context.js.map +1 -0
- package/dist/utils/callDedup.d.ts +29 -0
- package/dist/utils/callDedup.d.ts.map +1 -0
- package/dist/utils/callDedup.js +87 -0
- package/dist/utils/callDedup.js.map +1 -0
- package/dist/utils/configManager.d.ts +307 -0
- package/dist/utils/configManager.d.ts.map +1 -0
- package/dist/utils/configManager.js +1169 -0
- package/dist/utils/configManager.js.map +1 -0
- package/dist/utils/d365XmlNormalizer.d.ts +9 -0
- package/dist/utils/d365XmlNormalizer.d.ts.map +1 -0
- package/dist/utils/d365XmlNormalizer.js +24 -0
- package/dist/utils/d365XmlNormalizer.js.map +1 -0
- package/dist/utils/edtExtensionValidator.d.ts +110 -0
- package/dist/utils/edtExtensionValidator.d.ts.map +1 -0
- package/dist/utils/edtExtensionValidator.js +367 -0
- package/dist/utils/edtExtensionValidator.js.map +1 -0
- package/dist/utils/eolUtils.d.ts +15 -0
- package/dist/utils/eolUtils.d.ts.map +1 -0
- package/dist/utils/eolUtils.js +21 -0
- package/dist/utils/eolUtils.js.map +1 -0
- package/dist/utils/extractManifest.d.ts +34 -0
- package/dist/utils/extractManifest.d.ts.map +1 -0
- package/dist/utils/extractManifest.js +43 -0
- package/dist/utils/extractManifest.js.map +1 -0
- package/dist/utils/fieldControlTypes.d.ts +44 -0
- package/dist/utils/fieldControlTypes.d.ts.map +1 -0
- package/dist/utils/fieldControlTypes.js +97 -0
- package/dist/utils/fieldControlTypes.js.map +1 -0
- package/dist/utils/formCloner.d.ts +77 -0
- package/dist/utils/formCloner.d.ts.map +1 -0
- package/dist/utils/formCloner.js +364 -0
- package/dist/utils/formCloner.js.map +1 -0
- package/dist/utils/formControlExpander.d.ts +68 -0
- package/dist/utils/formControlExpander.d.ts.map +1 -0
- package/dist/utils/formControlExpander.js +207 -0
- package/dist/utils/formControlExpander.js.map +1 -0
- package/dist/utils/formControlRepair.d.ts +81 -0
- package/dist/utils/formControlRepair.d.ts.map +1 -0
- package/dist/utils/formControlRepair.js +203 -0
- package/dist/utils/formControlRepair.js.map +1 -0
- package/dist/utils/formExtensionShapeValidator.d.ts +44 -0
- package/dist/utils/formExtensionShapeValidator.d.ts.map +1 -0
- package/dist/utils/formExtensionShapeValidator.js +99 -0
- package/dist/utils/formExtensionShapeValidator.js.map +1 -0
- package/dist/utils/formPatternTemplates.d.ts +77 -0
- package/dist/utils/formPatternTemplates.d.ts.map +1 -0
- package/dist/utils/formPatternTemplates.js +1917 -0
- package/dist/utils/formPatternTemplates.js.map +1 -0
- package/dist/utils/fsExtensionScanner.d.ts +76 -0
- package/dist/utils/fsExtensionScanner.d.ts.map +1 -0
- package/dist/utils/fsExtensionScanner.js +271 -0
- package/dist/utils/fsExtensionScanner.js.map +1 -0
- package/dist/utils/fuzzyMatching.d.ts +41 -0
- package/dist/utils/fuzzyMatching.d.ts.map +1 -0
- package/dist/utils/fuzzyMatching.js +186 -0
- package/dist/utils/fuzzyMatching.js.map +1 -0
- package/dist/utils/indexStaleness.d.ts +30 -0
- package/dist/utils/indexStaleness.d.ts.map +1 -0
- package/dist/utils/indexStaleness.js +104 -0
- package/dist/utils/indexStaleness.js.map +1 -0
- package/dist/utils/loadEnv.d.ts +32 -0
- package/dist/utils/loadEnv.d.ts.map +1 -0
- package/dist/utils/loadEnv.js +83 -0
- package/dist/utils/loadEnv.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/metadataResolver.d.ts +155 -0
- package/dist/utils/metadataResolver.d.ts.map +1 -0
- package/dist/utils/metadataResolver.js +277 -0
- package/dist/utils/metadataResolver.js.map +1 -0
- package/dist/utils/modelClassifier.d.ts +180 -0
- package/dist/utils/modelClassifier.d.ts.map +1 -0
- package/dist/utils/modelClassifier.js +373 -0
- package/dist/utils/modelClassifier.js.map +1 -0
- package/dist/utils/operationLocks.d.ts +26 -0
- package/dist/utils/operationLocks.d.ts.map +1 -0
- package/dist/utils/operationLocks.js +182 -0
- package/dist/utils/operationLocks.js.map +1 -0
- package/dist/utils/packageResolver.d.ts +52 -0
- package/dist/utils/packageResolver.d.ts.map +1 -0
- package/dist/utils/packageResolver.js +174 -0
- package/dist/utils/packageResolver.js.map +1 -0
- package/dist/utils/pathContainment.d.ts +60 -0
- package/dist/utils/pathContainment.d.ts.map +1 -0
- package/dist/utils/pathContainment.js +270 -0
- package/dist/utils/pathContainment.js.map +1 -0
- package/dist/utils/projectUtils.d.ts +14 -0
- package/dist/utils/projectUtils.d.ts.map +1 -0
- package/dist/utils/projectUtils.js +48 -0
- package/dist/utils/projectUtils.js.map +1 -0
- package/dist/utils/provenanceStore.d.ts +54 -0
- package/dist/utils/provenanceStore.d.ts.map +1 -0
- package/dist/utils/provenanceStore.js +203 -0
- package/dist/utils/provenanceStore.js.map +1 -0
- package/dist/utils/richContext.d.ts +45 -0
- package/dist/utils/richContext.d.ts.map +1 -0
- package/dist/utils/richContext.js +316 -0
- package/dist/utils/richContext.js.map +1 -0
- package/dist/utils/smartXmlBuilder.d.ts +187 -0
- package/dist/utils/smartXmlBuilder.d.ts.map +1 -0
- package/dist/utils/smartXmlBuilder.js +504 -0
- package/dist/utils/smartXmlBuilder.js.map +1 -0
- package/dist/utils/stdioSessionInfo.d.ts +53 -0
- package/dist/utils/stdioSessionInfo.d.ts.map +1 -0
- package/dist/utils/stdioSessionInfo.js +47 -0
- package/dist/utils/stdioSessionInfo.js.map +1 -0
- package/dist/utils/suggestionEngine.d.ts +20 -0
- package/dist/utils/suggestionEngine.d.ts.map +1 -0
- package/dist/utils/suggestionEngine.js +145 -0
- package/dist/utils/suggestionEngine.js.map +1 -0
- package/dist/utils/symbolLookup.d.ts +60 -0
- package/dist/utils/symbolLookup.d.ts.map +1 -0
- package/dist/utils/symbolLookup.js +0 -0
- package/dist/utils/symbolLookup.js.map +1 -0
- package/dist/utils/terminalUi.d.ts +85 -0
- package/dist/utils/terminalUi.d.ts.map +1 -0
- package/dist/utils/terminalUi.js +182 -0
- package/dist/utils/terminalUi.js.map +1 -0
- package/dist/utils/toolMetrics.d.ts +32 -0
- package/dist/utils/toolMetrics.d.ts.map +1 -0
- package/dist/utils/toolMetrics.js +89 -0
- package/dist/utils/toolMetrics.js.map +1 -0
- package/dist/utils/toolProgressMessage.d.ts +8 -0
- package/dist/utils/toolProgressMessage.d.ts.map +1 -0
- package/dist/utils/toolProgressMessage.js +182 -0
- package/dist/utils/toolProgressMessage.js.map +1 -0
- package/dist/utils/workspaceDetector.d.ts +51 -0
- package/dist/utils/workspaceDetector.d.ts.map +1 -0
- package/dist/utils/workspaceDetector.js +328 -0
- package/dist/utils/workspaceDetector.js.map +1 -0
- package/dist/utils/xppConfigProvider.d.ts +48 -0
- package/dist/utils/xppConfigProvider.d.ts.map +1 -0
- package/dist/utils/xppConfigProvider.js +121 -0
- package/dist/utils/xppConfigProvider.js.map +1 -0
- package/dist/utils/xppDocGen.d.ts +39 -0
- package/dist/utils/xppDocGen.d.ts.map +1 -0
- package/dist/utils/xppDocGen.js +518 -0
- package/dist/utils/xppDocGen.js.map +1 -0
- package/dist/utils/xppFormat.d.ts +21 -0
- package/dist/utils/xppFormat.d.ts.map +1 -0
- package/dist/utils/xppFormat.js +102 -0
- package/dist/utils/xppFormat.js.map +1 -0
- package/dist/utils/xppSelectLint.d.ts +18 -0
- package/dist/utils/xppSelectLint.d.ts.map +1 -0
- package/dist/utils/xppSelectLint.js +80 -0
- package/dist/utils/xppSelectLint.js.map +1 -0
- package/dist/validation/formPatternValidator.d.ts +53 -0
- package/dist/validation/formPatternValidator.d.ts.map +1 -0
- package/dist/validation/formPatternValidator.js +454 -0
- package/dist/validation/formPatternValidator.js.map +1 -0
- package/dist/workspace/contextRanker.d.ts +58 -0
- package/dist/workspace/contextRanker.d.ts.map +1 -0
- package/dist/workspace/contextRanker.js +216 -0
- package/dist/workspace/contextRanker.js.map +1 -0
- package/dist/workspace/contextSnapshot.d.ts +68 -0
- package/dist/workspace/contextSnapshot.d.ts.map +1 -0
- package/dist/workspace/contextSnapshot.js +193 -0
- package/dist/workspace/contextSnapshot.js.map +1 -0
- package/dist/workspace/hybridSearch.d.ts +39 -0
- package/dist/workspace/hybridSearch.d.ts.map +1 -0
- package/dist/workspace/hybridSearch.js +96 -0
- package/dist/workspace/hybridSearch.js.map +1 -0
- package/dist/workspace/workspaceScanner.d.ts +95 -0
- package/dist/workspace/workspaceScanner.d.ts.map +1 -0
- package/dist/workspace/workspaceScanner.js +247 -0
- package/dist/workspace/workspaceScanner.js.map +1 -0
- package/dist/workspace/workspaceUtils.d.ts +23 -0
- package/dist/workspace/workspaceUtils.d.ts.map +1 -0
- package/dist/workspace/workspaceUtils.js +77 -0
- package/dist/workspace/workspaceUtils.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,3152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X++ Symbol Index
|
|
3
|
+
* SQLite-based symbol indexing with FTS5 full-text search
|
|
4
|
+
*/
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { Worker } from 'node:worker_threads';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { isStandardModel } from '../utils/modelClassifier.js';
|
|
10
|
+
import { c, log } from '../utils/terminalUi.js';
|
|
11
|
+
/**
|
|
12
|
+
* Detect if running in CI environment
|
|
13
|
+
* Supports: Azure Pipelines (TF_BUILD), GitHub Actions (CI), GitLab CI (CI)
|
|
14
|
+
*/
|
|
15
|
+
const isCI = () => {
|
|
16
|
+
return !!(process.env.CI || process.env.TF_BUILD || process.env.GITHUB_ACTIONS);
|
|
17
|
+
};
|
|
18
|
+
export class XppSymbolIndex {
|
|
19
|
+
db; // Public for direct pragma access in build scripts
|
|
20
|
+
labelsDb; // Separate DB for labels (performance optimization)
|
|
21
|
+
standardModels = [];
|
|
22
|
+
stmtCache = new Map();
|
|
23
|
+
labelsStmtCache = new Map();
|
|
24
|
+
// Buffer for property_stats observations — flushed once per model (batch INSERT)
|
|
25
|
+
// Key: "nodeType|property|value|model", Value: accumulated count
|
|
26
|
+
propStatBuffer = new Map();
|
|
27
|
+
// Read-only connection pool: WAL mode allows N readers + 1 writer without
|
|
28
|
+
// blocking each other. Pool size: READ_POOL_SIZE env var (default 3, clamped 1-8).
|
|
29
|
+
// Not used for :memory: databases (each connection would be a separate empty DB).
|
|
30
|
+
readPool = [];
|
|
31
|
+
labelsReadPool = [];
|
|
32
|
+
readPoolRR = 0;
|
|
33
|
+
// Symbol-count scans are expensive (full index scan of 1M+ rows, 30-60 s
|
|
34
|
+
// cold) — memoize the result and compute it off-thread (see getSymbolCounts).
|
|
35
|
+
dbPath;
|
|
36
|
+
symbolCountsCache = null;
|
|
37
|
+
symbolCountsPromise = null;
|
|
38
|
+
// Per-connection prepared-statement cache. Prepared statements are bound to
|
|
39
|
+
// their originating connection and cannot be shared across connections.
|
|
40
|
+
perConnStmtCache = new WeakMap();
|
|
41
|
+
constructor(dbPath, labelsDbPath) {
|
|
42
|
+
this.dbPath = dbPath;
|
|
43
|
+
// Ensure database directory exists
|
|
44
|
+
const dbDir = path.dirname(dbPath);
|
|
45
|
+
if (!fs.existsSync(dbDir)) {
|
|
46
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
this.db = new Database(dbPath);
|
|
49
|
+
// Labels live in a separate DB so the main symbol DB stays small and fast;
|
|
50
|
+
// labels can be huge (20M+ rows) without affecting search performance.
|
|
51
|
+
const labelPath = labelsDbPath || dbPath.replace('.db', '-labels.db');
|
|
52
|
+
this.labelsDb = new Database(labelPath);
|
|
53
|
+
// journal_mode should be set by caller (MEMORY for build, WAL for production).
|
|
54
|
+
// pragma() returns a string like "wal"/"delete" — always truthy, so compare the value, not !pragma(...).
|
|
55
|
+
const currentJournalMode = this.db.pragma('journal_mode', { simple: true });
|
|
56
|
+
if (currentJournalMode !== 'wal') {
|
|
57
|
+
this.db.pragma('journal_mode = WAL'); // Write-Ahead Logging for better concurrency
|
|
58
|
+
}
|
|
59
|
+
this.db.pragma('synchronous = NORMAL'); // Faster writes, still crash-safe
|
|
60
|
+
this.db.pragma('cache_size = -64000'); // 64MB cache (negative = kibibytes)
|
|
61
|
+
this.db.pragma('temp_store = MEMORY'); // Store temp tables in memory
|
|
62
|
+
this.db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O
|
|
63
|
+
// Without a busy_timeout the writer fails immediately (SQLITE_BUSY) whenever
|
|
64
|
+
// a WAL checkpoint races with an active reader; retry for up to 5s instead.
|
|
65
|
+
this.db.pragma('busy_timeout = 5000');
|
|
66
|
+
// Raise checkpoint threshold (default 1000 WAL frames) to reduce checkpoint
|
|
67
|
+
// frequency and reader/checkpoint contention; production DB is read-mostly.
|
|
68
|
+
this.db.pragma('wal_autocheckpoint = 4000');
|
|
69
|
+
// Configure labels DB similarly
|
|
70
|
+
const labelsJournalMode = this.labelsDb.pragma('journal_mode', { simple: true });
|
|
71
|
+
if (labelsJournalMode !== 'wal') {
|
|
72
|
+
this.labelsDb.pragma('journal_mode = WAL');
|
|
73
|
+
}
|
|
74
|
+
this.labelsDb.pragma('synchronous = NORMAL');
|
|
75
|
+
this.labelsDb.pragma('cache_size = -32000'); // 32MB cache for labels
|
|
76
|
+
this.labelsDb.pragma('temp_store = MEMORY');
|
|
77
|
+
this.labelsDb.pragma('mmap_size = 134217728'); // 128MB memory-mapped I/O
|
|
78
|
+
this.labelsDb.pragma('busy_timeout = 5000');
|
|
79
|
+
this.labelsDb.pragma('wal_autocheckpoint = 4000');
|
|
80
|
+
// page_size is a no-op on an existing database (only applies to new DBs).
|
|
81
|
+
// optimize/ANALYZE intentionally NOT run here (slow on 500K+ rows) — the
|
|
82
|
+
// pre-built DB already has persisted stats; use runPostBuildTasks() instead.
|
|
83
|
+
this.loadStandardModels();
|
|
84
|
+
this.initializeDatabase();
|
|
85
|
+
// Skip pool for :memory: — each new connection would be a separate empty DB.
|
|
86
|
+
if (dbPath !== ':memory:') {
|
|
87
|
+
const poolSize = Math.min(8, Math.max(1, parseInt(process.env.READ_POOL_SIZE || '3', 10) || 3));
|
|
88
|
+
for (let i = 0; i < poolSize; i++) {
|
|
89
|
+
const rConn = new Database(dbPath, { readonly: true });
|
|
90
|
+
rConn.pragma('busy_timeout = 5000');
|
|
91
|
+
rConn.pragma('cache_size = -32000'); // 32 MB page cache per connection
|
|
92
|
+
rConn.pragma('temp_store = MEMORY');
|
|
93
|
+
rConn.pragma('mmap_size = 268435456');
|
|
94
|
+
this.readPool.push(rConn);
|
|
95
|
+
if (labelPath !== ':memory:') {
|
|
96
|
+
const rLabels = new Database(labelPath, { readonly: true });
|
|
97
|
+
rLabels.pragma('busy_timeout = 5000');
|
|
98
|
+
rLabels.pragma('cache_size = -16000'); // 16 MB per labels connection
|
|
99
|
+
rLabels.pragma('temp_store = MEMORY');
|
|
100
|
+
rLabels.pragma('mmap_size = 134217728');
|
|
101
|
+
this.labelsReadPool.push(rLabels);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns the next read-only connection from the pool (round-robin).
|
|
108
|
+
* Falls back to the main writer connection when the pool is empty
|
|
109
|
+
* (e.g. :memory: databases used in write-only mode).
|
|
110
|
+
*
|
|
111
|
+
* Tool handlers should use this instead of accessing `db` directly
|
|
112
|
+
* to benefit from read-pool parallelism and per-connection stmt caching.
|
|
113
|
+
*/
|
|
114
|
+
getReadDb() {
|
|
115
|
+
if (this.readPool.length === 0)
|
|
116
|
+
return this.db;
|
|
117
|
+
return this.readPool[this.readPoolRR++ % this.readPool.length];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Close and drain all read-pool connections.
|
|
121
|
+
* Must be called before setting locking_mode = EXCLUSIVE on the writer
|
|
122
|
+
* connection (e.g. in build scripts) — SQLite cannot grant EXCLUSIVE while
|
|
123
|
+
* any other connection (even read-only, even in-process) holds a shared lock.
|
|
124
|
+
*/
|
|
125
|
+
closeReadPool() {
|
|
126
|
+
for (const conn of this.readPool) {
|
|
127
|
+
try {
|
|
128
|
+
conn.close();
|
|
129
|
+
}
|
|
130
|
+
catch { /* ignore */ }
|
|
131
|
+
}
|
|
132
|
+
this.readPool = [];
|
|
133
|
+
for (const conn of this.labelsReadPool) {
|
|
134
|
+
try {
|
|
135
|
+
conn.close();
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
}
|
|
139
|
+
this.labelsReadPool = [];
|
|
140
|
+
this.readPoolRR = 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get (or lazily prepare) a statement on a specific connection.
|
|
144
|
+
* Uses the per-connection WeakMap cache so statements are never shared
|
|
145
|
+
* across connections.
|
|
146
|
+
*
|
|
147
|
+
* Tool handlers should use `getReadStmt(index.getReadDb(), key, () => sql)`
|
|
148
|
+
* for repeated queries — avoids re-preparing the same SQL on every call.
|
|
149
|
+
*/
|
|
150
|
+
getReadStmt(db, key, buildSql) {
|
|
151
|
+
let cache = this.perConnStmtCache.get(db);
|
|
152
|
+
if (!cache) {
|
|
153
|
+
cache = new Map();
|
|
154
|
+
this.perConnStmtCache.set(db, cache);
|
|
155
|
+
}
|
|
156
|
+
let stmt = cache.get(key);
|
|
157
|
+
if (!stmt) {
|
|
158
|
+
stmt = db.prepare(buildSql());
|
|
159
|
+
cache.set(key, stmt);
|
|
160
|
+
}
|
|
161
|
+
return stmt;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run post-build maintenance tasks: ANALYZE + optimize.
|
|
165
|
+
* Call this at the END of build scripts (after all data is loaded and WAL mode is set).
|
|
166
|
+
* Do NOT call from the production server startup — the pre-built DB already has stats.
|
|
167
|
+
*/
|
|
168
|
+
runPostBuildTasks() {
|
|
169
|
+
log.step('Running post-build database optimization (ANALYZE + optimize)...');
|
|
170
|
+
const start = Date.now();
|
|
171
|
+
try {
|
|
172
|
+
// Optimize main symbol database
|
|
173
|
+
this.db.pragma('analysis_limit = 1000');
|
|
174
|
+
this.db.exec('ANALYZE');
|
|
175
|
+
this.db.pragma('optimize');
|
|
176
|
+
// Optimize labels database
|
|
177
|
+
this.labelsDb.pragma('analysis_limit = 1000');
|
|
178
|
+
this.labelsDb.exec('ANALYZE');
|
|
179
|
+
this.labelsDb.pragma('optimize');
|
|
180
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
|
181
|
+
log.ok(`Post-build optimization complete in ${elapsed}s`);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
log.warn(`Post-build optimization failed (non-fatal): ${e instanceof Error ? e.message : e}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Convert database row to XppSymbol with enhanced metadata
|
|
189
|
+
*/
|
|
190
|
+
rowToSymbol(row) {
|
|
191
|
+
return {
|
|
192
|
+
name: row.name,
|
|
193
|
+
type: row.type,
|
|
194
|
+
parentName: row.parent_name || undefined,
|
|
195
|
+
signature: row.signature || undefined,
|
|
196
|
+
filePath: row.file_path,
|
|
197
|
+
model: row.model,
|
|
198
|
+
packageName: row.package_name || row.model,
|
|
199
|
+
description: row.description || undefined,
|
|
200
|
+
tags: row.tags || undefined,
|
|
201
|
+
sourceSnippet: row.source_snippet || undefined,
|
|
202
|
+
complexity: row.complexity || undefined,
|
|
203
|
+
usedTypes: row.used_types || undefined,
|
|
204
|
+
methodCalls: row.method_calls || undefined,
|
|
205
|
+
inlineComments: row.inline_comments || undefined,
|
|
206
|
+
extendsClass: row.extends_class || undefined,
|
|
207
|
+
implementsInterfaces: row.implements_interfaces || undefined,
|
|
208
|
+
usageExample: row.usage_example || undefined,
|
|
209
|
+
usageFrequency: row.usage_frequency || undefined,
|
|
210
|
+
patternType: row.pattern_type || undefined,
|
|
211
|
+
typicalUsages: row.typical_usages || undefined,
|
|
212
|
+
calledByCount: row.called_by_count || undefined,
|
|
213
|
+
relatedMethods: row.related_methods || undefined,
|
|
214
|
+
apiPatterns: row.api_patterns || undefined,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Kept for compatibility; standard-model determination now lives in
|
|
219
|
+
* isStandardModel() from modelClassifier (standard = not in CUSTOM_MODELS).
|
|
220
|
+
*/
|
|
221
|
+
loadStandardModels() {
|
|
222
|
+
this.standardModels = [];
|
|
223
|
+
}
|
|
224
|
+
initializeDatabase() {
|
|
225
|
+
// Create symbols table with enhanced metadata fields
|
|
226
|
+
this.db.exec(`
|
|
227
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
228
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
229
|
+
name TEXT NOT NULL,
|
|
230
|
+
type TEXT NOT NULL,
|
|
231
|
+
parent_name TEXT,
|
|
232
|
+
signature TEXT,
|
|
233
|
+
file_path TEXT NOT NULL,
|
|
234
|
+
model TEXT NOT NULL,
|
|
235
|
+
package_name TEXT,
|
|
236
|
+
description TEXT,
|
|
237
|
+
tags TEXT,
|
|
238
|
+
source_snippet TEXT,
|
|
239
|
+
source TEXT,
|
|
240
|
+
complexity INTEGER,
|
|
241
|
+
used_types TEXT,
|
|
242
|
+
method_calls TEXT,
|
|
243
|
+
inline_comments TEXT,
|
|
244
|
+
extends_class TEXT,
|
|
245
|
+
implements_interfaces TEXT,
|
|
246
|
+
usage_example TEXT,
|
|
247
|
+
usage_frequency INTEGER DEFAULT 0,
|
|
248
|
+
pattern_type TEXT,
|
|
249
|
+
typical_usages TEXT,
|
|
250
|
+
called_by_count INTEGER DEFAULT 0,
|
|
251
|
+
related_methods TEXT,
|
|
252
|
+
api_patterns TEXT
|
|
253
|
+
);
|
|
254
|
+
`);
|
|
255
|
+
// Migrate existing symbols table: add any columns that may be missing
|
|
256
|
+
// (needed when opening a DB built with an older schema)
|
|
257
|
+
{
|
|
258
|
+
const existingCols = new Set(this.db.pragma('table_info(symbols)').map(r => r.name));
|
|
259
|
+
const newCols = [
|
|
260
|
+
['package_name', 'TEXT'],
|
|
261
|
+
['description', 'TEXT'],
|
|
262
|
+
['tags', 'TEXT'],
|
|
263
|
+
['source_snippet', 'TEXT'],
|
|
264
|
+
['source', 'TEXT'],
|
|
265
|
+
['complexity', 'INTEGER'],
|
|
266
|
+
['used_types', 'TEXT'],
|
|
267
|
+
['method_calls', 'TEXT'],
|
|
268
|
+
['inline_comments', 'TEXT'],
|
|
269
|
+
['extends_class', 'TEXT'],
|
|
270
|
+
['implements_interfaces', 'TEXT'],
|
|
271
|
+
['usage_example', 'TEXT'],
|
|
272
|
+
['usage_frequency', 'INTEGER DEFAULT 0'],
|
|
273
|
+
['pattern_type', 'TEXT'],
|
|
274
|
+
['typical_usages', 'TEXT'],
|
|
275
|
+
['called_by_count', 'INTEGER DEFAULT 0'],
|
|
276
|
+
['related_methods', 'TEXT'],
|
|
277
|
+
['api_patterns', 'TEXT'],
|
|
278
|
+
];
|
|
279
|
+
// Validate column names and types before using them in DDL.
|
|
280
|
+
// exec() does not support parameters for DDL, so we guard against
|
|
281
|
+
// any future accidental mutation of newCols with external data.
|
|
282
|
+
const allowedColNames = new Set(newCols.map(([col]) => col));
|
|
283
|
+
const allowedColTypePat = /^(TEXT|INTEGER|INTEGER DEFAULT \d+)$/;
|
|
284
|
+
for (const [col, def] of newCols) {
|
|
285
|
+
if (!existingCols.has(col)) {
|
|
286
|
+
if (!allowedColNames.has(col) || !allowedColTypePat.test(def)) {
|
|
287
|
+
throw new Error(`symbolIndex: unexpected column definition: "${col} ${def}"`);
|
|
288
|
+
}
|
|
289
|
+
this.db.exec(`ALTER TABLE symbols ADD COLUMN ${col} ${def};`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Create FTS5 virtual table for full-text search with enhanced fields
|
|
294
|
+
this.db.exec(`
|
|
295
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(
|
|
296
|
+
name,
|
|
297
|
+
type,
|
|
298
|
+
parent_name,
|
|
299
|
+
signature,
|
|
300
|
+
description,
|
|
301
|
+
tags,
|
|
302
|
+
source_snippet,
|
|
303
|
+
inline_comments,
|
|
304
|
+
content='symbols',
|
|
305
|
+
content_rowid='id'
|
|
306
|
+
);
|
|
307
|
+
`);
|
|
308
|
+
// Create triggers to keep FTS table in sync
|
|
309
|
+
this.createFTSTriggers();
|
|
310
|
+
// Create indexes - optimized with composite indexes for common query patterns
|
|
311
|
+
this.db.exec(`
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
313
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_type ON symbols(type);
|
|
314
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_model ON symbols(model);
|
|
315
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_pattern_type ON symbols(pattern_type);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_parent_name ON symbols(parent_name);
|
|
317
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_symbols_unique
|
|
318
|
+
ON symbols(name, type, COALESCE(parent_name, ''), model);
|
|
319
|
+
|
|
320
|
+
-- Composite indexes for common query patterns (major speed boost)
|
|
321
|
+
CREATE INDEX IF NOT EXISTS idx_type_parent ON symbols(type, parent_name) WHERE parent_name IS NOT NULL;
|
|
322
|
+
CREATE INDEX IF NOT EXISTS idx_type_name ON symbols(type, name);
|
|
323
|
+
CREATE INDEX IF NOT EXISTS idx_parent_type ON symbols(parent_name, type) WHERE parent_name IS NOT NULL;
|
|
324
|
+
CREATE INDEX IF NOT EXISTS idx_name_type ON symbols(name, type);
|
|
325
|
+
-- Covering index for field/method lookups by parent (avoids table access)
|
|
326
|
+
CREATE INDEX IF NOT EXISTS idx_parent_type_name ON symbols(parent_name, type, name) WHERE parent_name IS NOT NULL;
|
|
327
|
+
-- Index for extends_class lookups (CoC extension discovery)
|
|
328
|
+
CREATE INDEX IF NOT EXISTS idx_extends_class ON symbols(extends_class) WHERE extends_class IS NOT NULL;
|
|
329
|
+
`);
|
|
330
|
+
// Create code_patterns table for pattern analysis
|
|
331
|
+
this.db.exec(`
|
|
332
|
+
CREATE TABLE IF NOT EXISTS code_patterns (
|
|
333
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
334
|
+
pattern_name TEXT NOT NULL UNIQUE,
|
|
335
|
+
pattern_type TEXT NOT NULL,
|
|
336
|
+
common_methods TEXT,
|
|
337
|
+
dependencies TEXT,
|
|
338
|
+
usage_examples TEXT,
|
|
339
|
+
frequency INTEGER DEFAULT 0,
|
|
340
|
+
domain TEXT,
|
|
341
|
+
characteristics TEXT
|
|
342
|
+
);
|
|
343
|
+
`);
|
|
344
|
+
this.db.exec(`
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_type ON code_patterns(pattern_type);
|
|
346
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_domain ON code_patterns(domain);
|
|
347
|
+
`);
|
|
348
|
+
// Labels live in the separate labelsDb (keeps the main symbol DB fast for search).
|
|
349
|
+
this.labelsDb.exec(`
|
|
350
|
+
CREATE TABLE IF NOT EXISTS labels (
|
|
351
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
352
|
+
label_id TEXT NOT NULL,
|
|
353
|
+
label_file_id TEXT NOT NULL,
|
|
354
|
+
model TEXT NOT NULL,
|
|
355
|
+
language TEXT NOT NULL,
|
|
356
|
+
text TEXT NOT NULL,
|
|
357
|
+
comment TEXT,
|
|
358
|
+
file_path TEXT NOT NULL
|
|
359
|
+
);
|
|
360
|
+
`);
|
|
361
|
+
this.labelsDb.exec(`
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_labels_id ON labels(label_id);
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_labels_file_id ON labels(label_file_id);
|
|
364
|
+
CREATE INDEX IF NOT EXISTS idx_labels_model ON labels(model);
|
|
365
|
+
CREATE INDEX IF NOT EXISTS idx_labels_language ON labels(language);
|
|
366
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_labels_unique
|
|
367
|
+
ON labels(label_id, label_file_id, model, language);
|
|
368
|
+
`);
|
|
369
|
+
// FTS5 full-text search for labels (en-US text only – primary search language)
|
|
370
|
+
this.labelsDb.exec(`
|
|
371
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS labels_fts USING fts5(
|
|
372
|
+
label_id,
|
|
373
|
+
text,
|
|
374
|
+
comment,
|
|
375
|
+
content='labels',
|
|
376
|
+
content_rowid='id'
|
|
377
|
+
);
|
|
378
|
+
`);
|
|
379
|
+
// Only index en-US rows to keep FTS compact (~5x smaller on typical installs)
|
|
380
|
+
// Case-insensitive: Microsoft packages store language as 'en-us' from Linux directory names
|
|
381
|
+
this.labelsDb.exec(`
|
|
382
|
+
CREATE TRIGGER IF NOT EXISTS labels_ai AFTER INSERT ON labels WHEN LOWER(new.language) = 'en-us' BEGIN
|
|
383
|
+
INSERT INTO labels_fts(rowid, label_id, text, comment)
|
|
384
|
+
VALUES (new.id, new.label_id, new.text, new.comment);
|
|
385
|
+
END;
|
|
386
|
+
|
|
387
|
+
CREATE TRIGGER IF NOT EXISTS labels_ad AFTER DELETE ON labels WHEN LOWER(old.language) = 'en-us' BEGIN
|
|
388
|
+
INSERT INTO labels_fts(labels_fts, rowid, label_id, text, comment)
|
|
389
|
+
VALUES ('delete', old.id, old.label_id, old.text, old.comment);
|
|
390
|
+
END;
|
|
391
|
+
|
|
392
|
+
CREATE TRIGGER IF NOT EXISTS labels_au AFTER UPDATE ON labels WHEN LOWER(old.language) = 'en-us' OR LOWER(new.language) = 'en-us' BEGIN
|
|
393
|
+
INSERT INTO labels_fts(labels_fts, rowid, label_id, text, comment)
|
|
394
|
+
VALUES ('delete', old.id, old.label_id, old.text, old.comment);
|
|
395
|
+
INSERT INTO labels_fts(rowid, label_id, text, comment)
|
|
396
|
+
VALUES (new.id, new.label_id, new.text, new.comment);
|
|
397
|
+
END;
|
|
398
|
+
`);
|
|
399
|
+
// Extended metadata tables for smart generation
|
|
400
|
+
// Table Relations - for analyzing table relationships
|
|
401
|
+
this.db.exec(`
|
|
402
|
+
CREATE TABLE IF NOT EXISTS table_relations (
|
|
403
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
404
|
+
source_table TEXT NOT NULL,
|
|
405
|
+
target_table TEXT NOT NULL,
|
|
406
|
+
relation_name TEXT NOT NULL,
|
|
407
|
+
constraint_fields TEXT,
|
|
408
|
+
model TEXT NOT NULL
|
|
409
|
+
);
|
|
410
|
+
`);
|
|
411
|
+
this.db.exec(`
|
|
412
|
+
CREATE INDEX IF NOT EXISTS idx_table_relations_source ON table_relations(source_table);
|
|
413
|
+
CREATE INDEX IF NOT EXISTS idx_table_relations_target ON table_relations(target_table);
|
|
414
|
+
CREATE INDEX IF NOT EXISTS idx_table_relations_model ON table_relations(model);
|
|
415
|
+
`);
|
|
416
|
+
// Form DataSources - for analyzing form patterns
|
|
417
|
+
this.db.exec(`
|
|
418
|
+
CREATE TABLE IF NOT EXISTS form_datasources (
|
|
419
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
420
|
+
form_name TEXT NOT NULL,
|
|
421
|
+
datasource_name TEXT NOT NULL,
|
|
422
|
+
table_name TEXT NOT NULL,
|
|
423
|
+
allow_edit INTEGER DEFAULT 1,
|
|
424
|
+
allow_create INTEGER DEFAULT 1,
|
|
425
|
+
allow_delete INTEGER DEFAULT 1,
|
|
426
|
+
model TEXT NOT NULL
|
|
427
|
+
);
|
|
428
|
+
`);
|
|
429
|
+
this.db.exec(`
|
|
430
|
+
CREATE INDEX IF NOT EXISTS idx_form_datasources_form ON form_datasources(form_name);
|
|
431
|
+
CREATE INDEX IF NOT EXISTS idx_form_datasources_table ON form_datasources(table_name);
|
|
432
|
+
CREATE INDEX IF NOT EXISTS idx_form_datasources_model ON form_datasources(model);
|
|
433
|
+
`);
|
|
434
|
+
// Form Patterns — mined Pattern/PatternVersion per Design node and
|
|
435
|
+
// sub-patterned container (node_path 'Design' = the form's top-level pattern).
|
|
436
|
+
// Grounds pattern recommendations ("real forms using pattern X") and
|
|
437
|
+
// cross-checks the curated catalog against actual metadata.
|
|
438
|
+
this.db.exec(`
|
|
439
|
+
CREATE TABLE IF NOT EXISTS form_patterns (
|
|
440
|
+
form_name TEXT NOT NULL,
|
|
441
|
+
model TEXT NOT NULL,
|
|
442
|
+
node_path TEXT NOT NULL,
|
|
443
|
+
control_name TEXT NOT NULL DEFAULT '',
|
|
444
|
+
control_type TEXT NOT NULL DEFAULT '',
|
|
445
|
+
pattern TEXT NOT NULL,
|
|
446
|
+
pattern_version TEXT,
|
|
447
|
+
child_sequence TEXT,
|
|
448
|
+
PRIMARY KEY (form_name, node_path)
|
|
449
|
+
);
|
|
450
|
+
`);
|
|
451
|
+
this.db.exec(`
|
|
452
|
+
CREATE INDEX IF NOT EXISTS idx_form_patterns_pattern ON form_patterns(pattern, pattern_version);
|
|
453
|
+
CREATE INDEX IF NOT EXISTS idx_form_patterns_model ON form_patterns(model);
|
|
454
|
+
`);
|
|
455
|
+
// EDT Metadata - for EDT suggestion and validation
|
|
456
|
+
this.db.exec(`
|
|
457
|
+
CREATE TABLE IF NOT EXISTS edt_metadata (
|
|
458
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
459
|
+
edt_name TEXT NOT NULL,
|
|
460
|
+
extends TEXT,
|
|
461
|
+
enum_type TEXT,
|
|
462
|
+
reference_table TEXT,
|
|
463
|
+
relation_type TEXT,
|
|
464
|
+
string_size TEXT,
|
|
465
|
+
database_string_size TEXT,
|
|
466
|
+
display_length TEXT,
|
|
467
|
+
label TEXT,
|
|
468
|
+
model TEXT NOT NULL
|
|
469
|
+
);
|
|
470
|
+
`);
|
|
471
|
+
// Migrate existing edt_metadata table: add database_string_size column when missing
|
|
472
|
+
{
|
|
473
|
+
const existingCols = new Set(this.db.pragma('table_info(edt_metadata)').map(r => r.name));
|
|
474
|
+
if (!existingCols.has('database_string_size')) {
|
|
475
|
+
this.db.exec(`ALTER TABLE edt_metadata ADD COLUMN database_string_size TEXT;`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
this.db.exec(`
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_edt_metadata_name ON edt_metadata(edt_name);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_edt_metadata_extends ON edt_metadata(extends);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS idx_edt_metadata_enum ON edt_metadata(enum_type);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_edt_metadata_ref_table ON edt_metadata(reference_table);
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_edt_metadata_model ON edt_metadata(model);
|
|
484
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_edt_metadata_unique ON edt_metadata(edt_name, model);
|
|
485
|
+
`);
|
|
486
|
+
// Security tables
|
|
487
|
+
// Security Privilege Entry Points
|
|
488
|
+
this.db.exec(`
|
|
489
|
+
CREATE TABLE IF NOT EXISTS security_privilege_entries (
|
|
490
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
491
|
+
privilege_name TEXT NOT NULL,
|
|
492
|
+
entry_point_name TEXT NOT NULL,
|
|
493
|
+
object_type TEXT NOT NULL,
|
|
494
|
+
access_level TEXT NOT NULL,
|
|
495
|
+
model TEXT NOT NULL
|
|
496
|
+
);
|
|
497
|
+
CREATE INDEX IF NOT EXISTS idx_spe_privilege ON security_privilege_entries(privilege_name);
|
|
498
|
+
CREATE INDEX IF NOT EXISTS idx_spe_entry ON security_privilege_entries(entry_point_name);
|
|
499
|
+
CREATE INDEX IF NOT EXISTS idx_spe_model ON security_privilege_entries(model);
|
|
500
|
+
`);
|
|
501
|
+
// Security Duty → Privilege references
|
|
502
|
+
this.db.exec(`
|
|
503
|
+
CREATE TABLE IF NOT EXISTS security_duty_privileges (
|
|
504
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
505
|
+
duty_name TEXT NOT NULL,
|
|
506
|
+
privilege_name TEXT NOT NULL,
|
|
507
|
+
model TEXT NOT NULL
|
|
508
|
+
);
|
|
509
|
+
CREATE INDEX IF NOT EXISTS idx_sdp_duty ON security_duty_privileges(duty_name);
|
|
510
|
+
CREATE INDEX IF NOT EXISTS idx_sdp_privilege ON security_duty_privileges(privilege_name);
|
|
511
|
+
CREATE INDEX IF NOT EXISTS idx_sdp_model ON security_duty_privileges(model);
|
|
512
|
+
`);
|
|
513
|
+
// Security Role → Duty references
|
|
514
|
+
this.db.exec(`
|
|
515
|
+
CREATE TABLE IF NOT EXISTS security_role_duties (
|
|
516
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
517
|
+
role_name TEXT NOT NULL,
|
|
518
|
+
duty_name TEXT NOT NULL,
|
|
519
|
+
model TEXT NOT NULL
|
|
520
|
+
);
|
|
521
|
+
CREATE INDEX IF NOT EXISTS idx_srd_role ON security_role_duties(role_name);
|
|
522
|
+
CREATE INDEX IF NOT EXISTS idx_srd_duty ON security_role_duties(duty_name);
|
|
523
|
+
CREATE INDEX IF NOT EXISTS idx_srd_model ON security_role_duties(model);
|
|
524
|
+
`);
|
|
525
|
+
// Menu item targets
|
|
526
|
+
this.db.exec(`
|
|
527
|
+
CREATE TABLE IF NOT EXISTS menu_item_targets (
|
|
528
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
529
|
+
menu_item_name TEXT NOT NULL,
|
|
530
|
+
menu_item_type TEXT NOT NULL,
|
|
531
|
+
target_object TEXT,
|
|
532
|
+
target_type TEXT,
|
|
533
|
+
security_privilege TEXT,
|
|
534
|
+
label TEXT,
|
|
535
|
+
model TEXT NOT NULL
|
|
536
|
+
);
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_mit_name ON menu_item_targets(menu_item_name);
|
|
538
|
+
CREATE INDEX IF NOT EXISTS idx_mit_target ON menu_item_targets(target_object);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_mit_model ON menu_item_targets(model);
|
|
540
|
+
`);
|
|
541
|
+
// Index metadata (key-value); e.g. last_indexed_at drives staleness detection in get_workspace_info.
|
|
542
|
+
this.db.exec(`
|
|
543
|
+
CREATE TABLE IF NOT EXISTS _index_meta (
|
|
544
|
+
key TEXT PRIMARY KEY,
|
|
545
|
+
value TEXT NOT NULL
|
|
546
|
+
);
|
|
547
|
+
`);
|
|
548
|
+
// Property stats: distribution of metadata property values across STANDARD models,
|
|
549
|
+
// used for data-driven BP rules in validate_xpp instead of hardcoded tables.
|
|
550
|
+
// Presence is encoded via special values '(present)' / '(absent)'.
|
|
551
|
+
this.db.exec(`
|
|
552
|
+
CREATE TABLE IF NOT EXISTS property_stats (
|
|
553
|
+
node_type TEXT NOT NULL,
|
|
554
|
+
property TEXT NOT NULL,
|
|
555
|
+
value TEXT NOT NULL,
|
|
556
|
+
model TEXT NOT NULL,
|
|
557
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
558
|
+
PRIMARY KEY (node_type, property, value, model)
|
|
559
|
+
);
|
|
560
|
+
CREATE INDEX IF NOT EXISTS idx_ps_node_prop ON property_stats(node_type, property);
|
|
561
|
+
`);
|
|
562
|
+
// Extension metadata
|
|
563
|
+
this.db.exec(`
|
|
564
|
+
CREATE TABLE IF NOT EXISTS extension_metadata (
|
|
565
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
566
|
+
extension_name TEXT NOT NULL,
|
|
567
|
+
extension_type TEXT NOT NULL,
|
|
568
|
+
base_object_name TEXT NOT NULL,
|
|
569
|
+
added_fields TEXT,
|
|
570
|
+
added_methods TEXT,
|
|
571
|
+
added_indexes TEXT,
|
|
572
|
+
coc_methods TEXT,
|
|
573
|
+
event_subscriptions TEXT,
|
|
574
|
+
model TEXT NOT NULL
|
|
575
|
+
);
|
|
576
|
+
CREATE INDEX IF NOT EXISTS idx_em_base ON extension_metadata(base_object_name);
|
|
577
|
+
CREATE INDEX IF NOT EXISTS idx_em_type ON extension_metadata(extension_type);
|
|
578
|
+
CREATE INDEX IF NOT EXISTS idx_em_name ON extension_metadata(extension_name);
|
|
579
|
+
CREATE INDEX IF NOT EXISTS idx_em_model ON extension_metadata(model);
|
|
580
|
+
`);
|
|
581
|
+
// Service operations & service group membership
|
|
582
|
+
// AxService → exposed operations (each maps to a public method on the class).
|
|
583
|
+
this.db.exec(`
|
|
584
|
+
CREATE TABLE IF NOT EXISTS service_operations (
|
|
585
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
586
|
+
service_name TEXT NOT NULL,
|
|
587
|
+
operation_name TEXT NOT NULL,
|
|
588
|
+
method_name TEXT NOT NULL,
|
|
589
|
+
idempotent INTEGER NOT NULL DEFAULT 0,
|
|
590
|
+
model TEXT NOT NULL
|
|
591
|
+
);
|
|
592
|
+
CREATE INDEX IF NOT EXISTS idx_so_service ON service_operations(service_name);
|
|
593
|
+
CREATE INDEX IF NOT EXISTS idx_so_model ON service_operations(model);
|
|
594
|
+
`);
|
|
595
|
+
// AxServiceGroup → member services. Enables the service→group reverse lookup
|
|
596
|
+
// used to compute the /api/services/<group>/<service>/<operation> endpoint.
|
|
597
|
+
this.db.exec(`
|
|
598
|
+
CREATE TABLE IF NOT EXISTS service_group_members (
|
|
599
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
600
|
+
group_name TEXT NOT NULL,
|
|
601
|
+
service_name TEXT NOT NULL,
|
|
602
|
+
model TEXT NOT NULL
|
|
603
|
+
);
|
|
604
|
+
CREATE INDEX IF NOT EXISTS idx_sgm_group ON service_group_members(group_name);
|
|
605
|
+
CREATE INDEX IF NOT EXISTS idx_sgm_service ON service_group_members(service_name);
|
|
606
|
+
CREATE INDEX IF NOT EXISTS idx_sgm_model ON service_group_members(model);
|
|
607
|
+
`);
|
|
608
|
+
// Map mappings
|
|
609
|
+
// AxMap → tables it maps onto (with field-connection counts).
|
|
610
|
+
this.db.exec(`
|
|
611
|
+
CREATE TABLE IF NOT EXISTS map_mappings (
|
|
612
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
613
|
+
map_name TEXT NOT NULL,
|
|
614
|
+
mapping_table TEXT NOT NULL,
|
|
615
|
+
field_connections INTEGER NOT NULL DEFAULT 0,
|
|
616
|
+
model TEXT NOT NULL
|
|
617
|
+
);
|
|
618
|
+
CREATE INDEX IF NOT EXISTS idx_mm_map ON map_mappings(map_name);
|
|
619
|
+
CREATE INDEX IF NOT EXISTS idx_mm_table ON map_mappings(mapping_table);
|
|
620
|
+
CREATE INDEX IF NOT EXISTS idx_mm_model ON map_mappings(model);
|
|
621
|
+
`);
|
|
622
|
+
// Security policies (row-level / OLS)
|
|
623
|
+
// Indexed by primary table so get_security_coverage_for_object can report
|
|
624
|
+
// which OLS policies constrain a given table.
|
|
625
|
+
this.db.exec(`
|
|
626
|
+
CREATE TABLE IF NOT EXISTS security_policies (
|
|
627
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
628
|
+
policy_name TEXT NOT NULL,
|
|
629
|
+
primary_table TEXT,
|
|
630
|
+
query_name TEXT,
|
|
631
|
+
operation TEXT,
|
|
632
|
+
constrained_table INTEGER NOT NULL DEFAULT 0,
|
|
633
|
+
label TEXT,
|
|
634
|
+
model TEXT NOT NULL
|
|
635
|
+
);
|
|
636
|
+
CREATE INDEX IF NOT EXISTS idx_sp_policy ON security_policies(policy_name);
|
|
637
|
+
CREATE INDEX IF NOT EXISTS idx_sp_table ON security_policies(primary_table);
|
|
638
|
+
CREATE INDEX IF NOT EXISTS idx_sp_model ON security_policies(model);
|
|
639
|
+
`);
|
|
640
|
+
// Macro defines
|
|
641
|
+
// AxMacroDictionary → its #define entries.
|
|
642
|
+
this.db.exec(`
|
|
643
|
+
CREATE TABLE IF NOT EXISTS macro_defines (
|
|
644
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
645
|
+
macro_name TEXT NOT NULL,
|
|
646
|
+
define_name TEXT NOT NULL,
|
|
647
|
+
define_value TEXT,
|
|
648
|
+
model TEXT NOT NULL
|
|
649
|
+
);
|
|
650
|
+
CREATE INDEX IF NOT EXISTS idx_md_macro ON macro_defines(macro_name);
|
|
651
|
+
CREATE INDEX IF NOT EXISTS idx_md_define ON macro_defines(define_name);
|
|
652
|
+
CREATE INDEX IF NOT EXISTS idx_md_model ON macro_defines(model);
|
|
653
|
+
`);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Create FTS triggers for keeping symbols_fts in sync
|
|
657
|
+
* Extracted to allow disabling during bulk inserts and re-enabling after
|
|
658
|
+
*/
|
|
659
|
+
createFTSTriggers() {
|
|
660
|
+
this.db.exec(`
|
|
661
|
+
CREATE TRIGGER IF NOT EXISTS symbols_ai AFTER INSERT ON symbols BEGIN
|
|
662
|
+
INSERT INTO symbols_fts(rowid, name, type, parent_name, signature, description, tags, source_snippet, inline_comments)
|
|
663
|
+
VALUES (new.id, new.name, new.type, new.parent_name, new.signature, new.description, new.tags, new.source_snippet, new.inline_comments);
|
|
664
|
+
END;
|
|
665
|
+
`);
|
|
666
|
+
this.db.exec(`
|
|
667
|
+
CREATE TRIGGER IF NOT EXISTS symbols_ad AFTER DELETE ON symbols BEGIN
|
|
668
|
+
DELETE FROM symbols_fts WHERE rowid = old.id;
|
|
669
|
+
END;
|
|
670
|
+
`);
|
|
671
|
+
this.db.exec(`
|
|
672
|
+
CREATE TRIGGER IF NOT EXISTS symbols_au AFTER UPDATE ON symbols BEGIN
|
|
673
|
+
UPDATE symbols_fts SET
|
|
674
|
+
name = new.name,
|
|
675
|
+
type = new.type,
|
|
676
|
+
parent_name = new.parent_name,
|
|
677
|
+
signature = new.signature,
|
|
678
|
+
description = new.description,
|
|
679
|
+
tags = new.tags,
|
|
680
|
+
source_snippet = new.source_snippet,
|
|
681
|
+
inline_comments = new.inline_comments
|
|
682
|
+
WHERE rowid = new.id;
|
|
683
|
+
END;
|
|
684
|
+
`);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Add a symbol to the index with enhanced metadata
|
|
688
|
+
*/
|
|
689
|
+
addSymbol(symbol) {
|
|
690
|
+
this.invalidateSymbolCounts();
|
|
691
|
+
let stmt = this.stmtCache.get('addSymbol');
|
|
692
|
+
if (!stmt) {
|
|
693
|
+
stmt = this.db.prepare(`
|
|
694
|
+
INSERT OR REPLACE INTO symbols (
|
|
695
|
+
name, type, parent_name, signature, file_path, model, package_name,
|
|
696
|
+
description, tags, source_snippet, source, complexity, used_types, method_calls,
|
|
697
|
+
inline_comments, extends_class, implements_interfaces, usage_example,
|
|
698
|
+
usage_frequency, pattern_type, typical_usages, called_by_count, related_methods, api_patterns
|
|
699
|
+
)
|
|
700
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
701
|
+
`);
|
|
702
|
+
this.stmtCache.set('addSymbol', stmt);
|
|
703
|
+
}
|
|
704
|
+
stmt.run(symbol.name, symbol.type, symbol.parentName || null, symbol.signature || null, symbol.filePath, symbol.model, symbol.packageName || symbol.model, symbol.description || null, symbol.tags || null, symbol.sourceSnippet || null, symbol.source || null, symbol.complexity || null, symbol.usedTypes || null, symbol.methodCalls || null, symbol.inlineComments || null, symbol.extendsClass || null, symbol.implementsInterfaces || null, symbol.usageExample || null, symbol.usageFrequency || 0, symbol.patternType || null, symbol.typicalUsages || null, symbol.calledByCount || 0, symbol.relatedMethods || null, symbol.apiPatterns || null);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* All stored forms a file path may take in the index. Full builds store the
|
|
708
|
+
* JSON's sourcePath: CI-extracted custom models normalize it to a
|
|
709
|
+
* PackagesLocalDirectory-relative forward-slash path (see normalizeSourcePath
|
|
710
|
+
* in scripts/extract-metadata.ts), while locally built DBs keep the absolute
|
|
711
|
+
* Windows path with backslashes. Matching every form keeps stale-row cleanup
|
|
712
|
+
* working regardless of which build produced the DB.
|
|
713
|
+
*/
|
|
714
|
+
filePathForms(filePath) {
|
|
715
|
+
const forms = new Set([
|
|
716
|
+
filePath,
|
|
717
|
+
filePath.replace(/\//g, '\\'),
|
|
718
|
+
filePath.replace(/\\/g, '/'),
|
|
719
|
+
]);
|
|
720
|
+
const m = /[/\\]PackagesLocalDirectory[/\\](.+)$/.exec(filePath);
|
|
721
|
+
if (m) {
|
|
722
|
+
const tail = m[1].replace(/\\/g, '/');
|
|
723
|
+
forms.add(tail);
|
|
724
|
+
forms.add(tail.replace(/\//g, '\\'));
|
|
725
|
+
}
|
|
726
|
+
return [...forms];
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Remove all symbols for a given file path from both the main table and FTS index.
|
|
730
|
+
* Matches every stored path form (see filePathForms) so stale rows are removed
|
|
731
|
+
* even when the DB stores a different path form than the caller passed.
|
|
732
|
+
* Returns the names of top-level objects that were removed (for cache invalidation).
|
|
733
|
+
*/
|
|
734
|
+
removeSymbolsByFile(filePath) {
|
|
735
|
+
const forms = this.filePathForms(filePath);
|
|
736
|
+
const placeholders = forms.map(() => '?').join(', ');
|
|
737
|
+
// Collect object names BEFORE deletion (for cache invalidation)
|
|
738
|
+
const rows = this.db.prepare(`SELECT DISTINCT name FROM symbols WHERE file_path IN (${placeholders}) AND parent_name IS NULL`).all(...forms);
|
|
739
|
+
const objectNames = rows.map(r => r.name);
|
|
740
|
+
// The FTS trigger (symbols_fts AFTER DELETE) handles FTS cleanup automatically
|
|
741
|
+
const result = this.db.prepare(`DELETE FROM symbols WHERE file_path IN (${placeholders})`).run(...forms);
|
|
742
|
+
this.invalidateSymbolCounts();
|
|
743
|
+
return { deletedCount: result.changes, objectNames };
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Remove all labels for a given file path from the labels DB.
|
|
747
|
+
* Matches every stored path form (see filePathForms), like removeSymbolsByFile.
|
|
748
|
+
* Also cleans up the labels FTS index.
|
|
749
|
+
* Returns the count of deleted label rows.
|
|
750
|
+
*/
|
|
751
|
+
removeLabelsByFile(filePath) {
|
|
752
|
+
const forms = this.filePathForms(filePath);
|
|
753
|
+
const placeholders = forms.map(() => '?').join(', ');
|
|
754
|
+
// The labels_ad trigger handles FTS cleanup for en-US rows
|
|
755
|
+
const result = this.labelsDb.prepare(`DELETE FROM labels WHERE file_path IN (${placeholders})`).run(...forms);
|
|
756
|
+
return result.changes;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Remove all labels matching a specific label_id + model combination.
|
|
760
|
+
* Used when a label is known to have been deleted/reverted.
|
|
761
|
+
*/
|
|
762
|
+
removeLabelById(labelId, model) {
|
|
763
|
+
const result = this.labelsDb.prepare(`DELETE FROM labels WHERE label_id = ? AND model = ?`).run(labelId, model);
|
|
764
|
+
return result.changes;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Sanitize a user query for FTS5 to prevent syntax errors.
|
|
768
|
+
* FTS5 operators (AND, OR, NOT, NEAR, quotes, parens, *) can crash the engine
|
|
769
|
+
* when they appear in raw user input. Wraps each token as a quoted prefix term.
|
|
770
|
+
*
|
|
771
|
+
* Performance: restricts the MATCH to the small/fast columns only.
|
|
772
|
+
* source_snippet and inline_comments hold full X++ source code (100-2000 chars per
|
|
773
|
+
* method × 300K+ methods) — including them in every FTS scan is the single biggest
|
|
774
|
+
* cause of slow symbol searches after table-method indexing was added.
|
|
775
|
+
*/
|
|
776
|
+
sanitizeFtsQuery(query) {
|
|
777
|
+
const trimmed = query.trim();
|
|
778
|
+
if (!trimmed)
|
|
779
|
+
return '""';
|
|
780
|
+
// Minimal stop words - only the most common query keywords
|
|
781
|
+
const stopWords = new Set([
|
|
782
|
+
// Common query verbs (Czech)
|
|
783
|
+
'vyhledej', 'najdi', 'zobraz', 'ukaž', 'související', 'proces', 'procesy',
|
|
784
|
+
// Common query verbs (English)
|
|
785
|
+
'find', 'search', 'show', 'get', 'list', 'related', 'process', 'processes',
|
|
786
|
+
// Object type keywords (already in type parameter)
|
|
787
|
+
'method', 'methods', 'class', 'classes', 'table', 'tables', 'třídy', 'třída'
|
|
788
|
+
]);
|
|
789
|
+
// Strip FTS5 special characters – keep alphanumeric, underscore and spaces
|
|
790
|
+
const cleaned = trimmed.replace(/[^\w\s]/g, ' ').trim();
|
|
791
|
+
const allTokens = cleaned
|
|
792
|
+
.split(/\s+/)
|
|
793
|
+
.map(t => t.toLowerCase())
|
|
794
|
+
.filter(t => t.length > 1); // Filter single chars
|
|
795
|
+
// Drop stop words only while a real token remains — a query may legitimately
|
|
796
|
+
// BE a stop word (e.g. a method literally named "find"), and falling through
|
|
797
|
+
// to the phrase-search fallback would behave differently for it.
|
|
798
|
+
const withoutStopWords = allTokens.filter(t => !stopWords.has(t));
|
|
799
|
+
const tokens = withoutStopWords.length > 0 ? withoutStopWords : allTokens;
|
|
800
|
+
// If no tokens remain after filtering, use original query in quotes
|
|
801
|
+
if (tokens.length === 0) {
|
|
802
|
+
return `{name type parent_name signature description tags} : "${trimmed}"`;
|
|
803
|
+
}
|
|
804
|
+
// Create FTS query with prefix matching
|
|
805
|
+
const baseQuery = tokens.map(t => `"${t}"*`).join(' ');
|
|
806
|
+
// Column-set filter: FTS5 searches only these columns, skipping source_snippet
|
|
807
|
+
// and inline_comments. This is valid FTS5 syntax and uses the same index.
|
|
808
|
+
return `{name type parent_name signature description tags} : ${baseQuery}`;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Search symbols by query with full-text search
|
|
812
|
+
* PERFORMANCE: Only select essential columns (name, type, parent_name, signature, model, file_path)
|
|
813
|
+
* Uses prepared statement caching for common queries
|
|
814
|
+
*/
|
|
815
|
+
searchSymbols(query, limit = 20, types) {
|
|
816
|
+
const ftsQuery = this.sanitizeFtsQuery(query);
|
|
817
|
+
const cacheKey = types?.length ? `search_typed_${types.join('_')}` : 'search_all';
|
|
818
|
+
// PERFORMANCE: Select only essential columns, not s.* (avoids loading large text fields)
|
|
819
|
+
let sql = `
|
|
820
|
+
SELECT s.id, s.name, s.type, s.parent_name, s.signature, s.file_path, s.model, s.description
|
|
821
|
+
FROM symbols_fts fts
|
|
822
|
+
JOIN symbols s ON s.id = fts.rowid
|
|
823
|
+
WHERE symbols_fts MATCH ?
|
|
824
|
+
`;
|
|
825
|
+
const params = [ftsQuery];
|
|
826
|
+
if (types && types.length > 0) {
|
|
827
|
+
sql += ` AND s.type IN (${types.map(() => '?').join(',')})`;
|
|
828
|
+
params.push(...types);
|
|
829
|
+
}
|
|
830
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
831
|
+
params.push(limit);
|
|
832
|
+
const db = this.getReadDb();
|
|
833
|
+
try {
|
|
834
|
+
const stmt = this.getReadStmt(db, cacheKey, () => sql);
|
|
835
|
+
return stmt.all(...params).map(row => this.rowToSymbol(row));
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
// FTS5 syntax error (e.g. user typed *, ", (, ), -) — fall back to LIKE contains search
|
|
839
|
+
// PERFORMANCE: Also select only essential columns in fallback
|
|
840
|
+
const fallbackCacheKey = types?.length ? `fallback_typed_${types.join('_')}` : 'fallback_all';
|
|
841
|
+
// ESCAPE '\' is required — without it the backslashes produced by
|
|
842
|
+
// escapeLikePattern are literal characters and any query containing
|
|
843
|
+
// '_' or '%' (e.g. "SalesLine_MyExt") silently matches nothing.
|
|
844
|
+
let fallbackSql = `SELECT s.id, s.name, s.type, s.parent_name, s.signature, s.file_path, s.model, s.description FROM symbols s WHERE s.name LIKE ? ESCAPE '\\'`;
|
|
845
|
+
const escapeLikePattern = (value) => {
|
|
846
|
+
// First escape backslashes, then escape SQL LIKE wildcards % and _
|
|
847
|
+
return value
|
|
848
|
+
.replace(/\\/g, '\\\\')
|
|
849
|
+
.replace(/[%_]/g, '\\$&');
|
|
850
|
+
};
|
|
851
|
+
const fallbackParams = [`%${escapeLikePattern(query)}%`];
|
|
852
|
+
if (types && types.length > 0) {
|
|
853
|
+
fallbackSql += ` AND s.type IN (${types.map(() => '?').join(',')})`;
|
|
854
|
+
fallbackParams.push(...types);
|
|
855
|
+
}
|
|
856
|
+
fallbackSql += ` ORDER BY s.name LIMIT ?`;
|
|
857
|
+
fallbackParams.push(limit);
|
|
858
|
+
const fallbackStmt = this.getReadStmt(db, fallbackCacheKey, () => fallbackSql);
|
|
859
|
+
return fallbackStmt.all(...fallbackParams).map(r => this.rowToSymbol(r));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Search symbols by prefix (for autocomplete)
|
|
864
|
+
* PERFORMANCE: Only select essential columns
|
|
865
|
+
*/
|
|
866
|
+
searchByPrefix(prefix, types, limit = 20) {
|
|
867
|
+
let sql = `
|
|
868
|
+
SELECT id, name, type, parent_name, signature, file_path, model, description
|
|
869
|
+
FROM symbols
|
|
870
|
+
WHERE name LIKE ?
|
|
871
|
+
`;
|
|
872
|
+
const params = [`${prefix}%`];
|
|
873
|
+
if (types && types.length > 0) {
|
|
874
|
+
sql += ` AND type IN (${types.map(() => '?').join(',')})`;
|
|
875
|
+
params.push(...types);
|
|
876
|
+
}
|
|
877
|
+
sql += ` ORDER BY name LIMIT ?`;
|
|
878
|
+
params.push(limit);
|
|
879
|
+
const cacheKey = types?.length ? `prefix_typed_${types.join('_')}` : 'prefix_all';
|
|
880
|
+
const db = this.getReadDb();
|
|
881
|
+
const stmt = this.getReadStmt(db, cacheKey, () => sql);
|
|
882
|
+
return stmt.all(...params).map(row => this.rowToSymbol(row));
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Get a specific symbol by name and type
|
|
886
|
+
*/
|
|
887
|
+
getSymbolByName(name, type) {
|
|
888
|
+
const db = this.getReadDb();
|
|
889
|
+
const stmt = this.getReadStmt(db, 'getSymbolByName', () => `SELECT * FROM symbols WHERE name = ? AND type = ? LIMIT 1`);
|
|
890
|
+
const row = stmt.get(name, type);
|
|
891
|
+
return row ? this.rowToSymbol(row) : null;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Get all classes (for resource listing)
|
|
895
|
+
*/
|
|
896
|
+
getAllClasses() {
|
|
897
|
+
const stmt = this.getReadDb().prepare(`SELECT * FROM symbols WHERE type = 'class' ORDER BY name`);
|
|
898
|
+
return stmt.all().map(row => this.rowToSymbol(row));
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Get symbol count.
|
|
902
|
+
*
|
|
903
|
+
* WARNING: without a warm cache this is a full index scan — 30-60 s on a
|
|
904
|
+
* large production DB with a cold file cache, and better-sqlite3 blocks the
|
|
905
|
+
* event loop for the whole scan. Server request paths must use
|
|
906
|
+
* getSymbolCounts() (off-thread) or getCachedSymbolCounts() instead; the
|
|
907
|
+
* synchronous form is for build scripts and post-indexing logging where the
|
|
908
|
+
* DB is small or already hot.
|
|
909
|
+
*/
|
|
910
|
+
getSymbolCount() {
|
|
911
|
+
if (this.symbolCountsCache)
|
|
912
|
+
return this.symbolCountsCache.total;
|
|
913
|
+
const stmt = this.getReadDb().prepare('SELECT COUNT(*) as count FROM symbols');
|
|
914
|
+
return stmt.get().count;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Get symbol count by type. Same event-loop-blocking caveat as getSymbolCount().
|
|
918
|
+
*/
|
|
919
|
+
getSymbolCountByType() {
|
|
920
|
+
if (this.symbolCountsCache)
|
|
921
|
+
return this.symbolCountsCache.byType;
|
|
922
|
+
return this.computeSymbolCountsSync().byType;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Cheap emptiness probe — O(1) regardless of table size. Use this instead of
|
|
926
|
+
* getSymbolCount() === 0 on startup paths.
|
|
927
|
+
*/
|
|
928
|
+
hasAnySymbols() {
|
|
929
|
+
const row = this.getReadDb()
|
|
930
|
+
.prepare('SELECT EXISTS(SELECT 1 FROM symbols) as present')
|
|
931
|
+
.get();
|
|
932
|
+
return row.present === 1;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Memoized counts if already computed this session, else null. Never scans —
|
|
936
|
+
* safe on any request path (health endpoints, status displays).
|
|
937
|
+
*/
|
|
938
|
+
getCachedSymbolCounts() {
|
|
939
|
+
return this.symbolCountsCache;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Total + per-type symbol counts without blocking the event loop.
|
|
943
|
+
*
|
|
944
|
+
* The scan runs in a worker thread with its own read-only connection (WAL
|
|
945
|
+
* allows concurrent readers), so the MCP server keeps answering protocol
|
|
946
|
+
* requests while it runs. The result is memoized; concurrent callers share
|
|
947
|
+
* one in-flight computation. Falls back to a synchronous scan when the
|
|
948
|
+
* worker cannot start (:memory: DBs are per-connection and invisible to
|
|
949
|
+
* another thread; under tsx/vitest the compiled worker .js does not exist).
|
|
950
|
+
*/
|
|
951
|
+
async getSymbolCounts() {
|
|
952
|
+
if (this.symbolCountsCache)
|
|
953
|
+
return this.symbolCountsCache;
|
|
954
|
+
if (this.symbolCountsPromise)
|
|
955
|
+
return this.symbolCountsPromise;
|
|
956
|
+
const promise = this.computeSymbolCountsInWorker()
|
|
957
|
+
.catch(() => this.computeSymbolCountsSync())
|
|
958
|
+
.then(counts => {
|
|
959
|
+
// Only memoize if no write invalidated the computation while it ran.
|
|
960
|
+
if (this.symbolCountsPromise === promise) {
|
|
961
|
+
this.symbolCountsCache = counts;
|
|
962
|
+
}
|
|
963
|
+
return counts;
|
|
964
|
+
});
|
|
965
|
+
this.symbolCountsPromise = promise;
|
|
966
|
+
return promise;
|
|
967
|
+
}
|
|
968
|
+
/** Drop memoized counts — call after any write that changes symbol rows. */
|
|
969
|
+
invalidateSymbolCounts() {
|
|
970
|
+
this.symbolCountsCache = null;
|
|
971
|
+
this.symbolCountsPromise = null;
|
|
972
|
+
}
|
|
973
|
+
computeSymbolCountsSync() {
|
|
974
|
+
const rows = this.getReadDb()
|
|
975
|
+
.prepare(`SELECT type, COUNT(*) as count FROM symbols GROUP BY type`)
|
|
976
|
+
.all();
|
|
977
|
+
const byType = {};
|
|
978
|
+
let total = 0;
|
|
979
|
+
for (const row of rows) {
|
|
980
|
+
byType[row.type] = row.count;
|
|
981
|
+
total += row.count;
|
|
982
|
+
}
|
|
983
|
+
return { total, byType };
|
|
984
|
+
}
|
|
985
|
+
computeSymbolCountsInWorker() {
|
|
986
|
+
if (this.dbPath === ':memory:') {
|
|
987
|
+
return Promise.reject(new Error('in-memory DB is not visible to worker threads'));
|
|
988
|
+
}
|
|
989
|
+
return new Promise((resolve, reject) => {
|
|
990
|
+
const worker = new Worker(new URL('./symbolCountsWorker.js', import.meta.url), {
|
|
991
|
+
workerData: { dbPath: this.dbPath },
|
|
992
|
+
});
|
|
993
|
+
worker.once('message', (msg) => {
|
|
994
|
+
if (msg.ok) {
|
|
995
|
+
resolve({ total: msg.total, byType: msg.byType });
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
reject(new Error(msg.error));
|
|
999
|
+
}
|
|
1000
|
+
void worker.terminate();
|
|
1001
|
+
});
|
|
1002
|
+
worker.once('error', reject);
|
|
1003
|
+
// A promise settles only once — this covers "exited before messaging".
|
|
1004
|
+
worker.once('exit', code => reject(new Error(`counts worker exited with code ${code}`)));
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Compute usage statistics (usage_frequency and called_by_count) for all methods
|
|
1009
|
+
* Should be called after initial indexing is complete
|
|
1010
|
+
* Optimized for 300k+ methods with minimal memory usage
|
|
1011
|
+
*/
|
|
1012
|
+
computeUsageStatistics() {
|
|
1013
|
+
log.step('Computing usage statistics...');
|
|
1014
|
+
const startTime = Date.now();
|
|
1015
|
+
const inCI = isCI();
|
|
1016
|
+
// Temporarily disable synchronous writes for speed during statistics computation
|
|
1017
|
+
const originalSync = this.db.pragma('synchronous', { simple: true });
|
|
1018
|
+
this.db.pragma('synchronous = OFF');
|
|
1019
|
+
this.db.exec(`
|
|
1020
|
+
CREATE TEMP TABLE IF NOT EXISTS temp_method_calls (
|
|
1021
|
+
caller_method TEXT,
|
|
1022
|
+
called_method TEXT
|
|
1023
|
+
);
|
|
1024
|
+
DELETE FROM temp_method_calls;
|
|
1025
|
+
`);
|
|
1026
|
+
const allMethods = this.db.prepare(`
|
|
1027
|
+
SELECT name, method_calls
|
|
1028
|
+
FROM symbols
|
|
1029
|
+
WHERE type = 'method' AND method_calls IS NOT NULL AND method_calls != ''
|
|
1030
|
+
`).all();
|
|
1031
|
+
log.detail(`${allMethods.length.toLocaleString('en-US')} methods with call references`);
|
|
1032
|
+
if (allMethods.length === 0) {
|
|
1033
|
+
log.detail('No method calls to process, skipping statistics');
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
log.detail('Parsing and inserting method calls...');
|
|
1037
|
+
const insertStmt = this.db.prepare('INSERT INTO temp_method_calls (caller_method, called_method) VALUES (?, ?)');
|
|
1038
|
+
// Process in batches of 1000 methods to show progress and allow GC
|
|
1039
|
+
const BATCH_SIZE = 1000;
|
|
1040
|
+
const totalBatches = Math.ceil(allMethods.length / BATCH_SIZE);
|
|
1041
|
+
for (let batchIdx = 0; batchIdx < totalBatches; batchIdx++) {
|
|
1042
|
+
const batchStart = batchIdx * BATCH_SIZE;
|
|
1043
|
+
const batchEnd = Math.min(batchStart + BATCH_SIZE, allMethods.length);
|
|
1044
|
+
const batchMethods = allMethods.slice(batchStart, batchEnd);
|
|
1045
|
+
const insertBatch = this.db.transaction(() => {
|
|
1046
|
+
for (const method of batchMethods) {
|
|
1047
|
+
const calls = method.method_calls.split(',');
|
|
1048
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1049
|
+
const calledMethod = calls[i].trim();
|
|
1050
|
+
if (calledMethod) {
|
|
1051
|
+
insertStmt.run(method.name, calledMethod);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
insertBatch();
|
|
1057
|
+
// Progress every 10% — a single overwriting line in interactive terminals
|
|
1058
|
+
// (matches the model-indexing progress style below), plain periodic lines
|
|
1059
|
+
// in CI logs where \r overwriting doesn't render.
|
|
1060
|
+
if ((batchIdx + 1) % Math.ceil(totalBatches / 10) === 0 || batchIdx === totalBatches - 1) {
|
|
1061
|
+
const percent = Math.round(((batchIdx + 1) / totalBatches) * 100);
|
|
1062
|
+
const text = `${percent}% (${batchEnd.toLocaleString('en-US')}/${allMethods.length.toLocaleString('en-US')} methods)`;
|
|
1063
|
+
if (inCI) {
|
|
1064
|
+
log.detail(text);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
process.stdout.write(`\r ${c.dim(text.padEnd(44))}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// Force GC in CI after each batch to prevent memory buildup
|
|
1071
|
+
if (isCI() && global.gc && batchIdx % 10 === 0) {
|
|
1072
|
+
global.gc();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (!inCI)
|
|
1076
|
+
console.log(''); // newline after the overwriting progress line
|
|
1077
|
+
log.detail('Computing aggregated statistics...');
|
|
1078
|
+
const updateTransaction = this.db.transaction(() => {
|
|
1079
|
+
this.db.exec(`
|
|
1080
|
+
CREATE TEMP TABLE temp_call_stats AS
|
|
1081
|
+
SELECT
|
|
1082
|
+
called_method,
|
|
1083
|
+
COUNT(*) as total_calls,
|
|
1084
|
+
COUNT(DISTINCT caller_method) as unique_callers
|
|
1085
|
+
FROM temp_method_calls
|
|
1086
|
+
GROUP BY called_method;
|
|
1087
|
+
|
|
1088
|
+
CREATE INDEX idx_temp_call_stats ON temp_call_stats(called_method);
|
|
1089
|
+
`);
|
|
1090
|
+
log.detail('Applying statistics to symbols...');
|
|
1091
|
+
// LEFT JOIN UPDATE (SQLite 3.33+); falls back to correlated subquery if unsupported.
|
|
1092
|
+
try {
|
|
1093
|
+
if (inCI) {
|
|
1094
|
+
log.detail('Updating usage_frequency and called_by_count (this may take 1-2 minutes)...');
|
|
1095
|
+
}
|
|
1096
|
+
this.db.exec(`
|
|
1097
|
+
UPDATE symbols
|
|
1098
|
+
SET
|
|
1099
|
+
usage_frequency = COALESCE((
|
|
1100
|
+
SELECT total_calls
|
|
1101
|
+
FROM temp_call_stats
|
|
1102
|
+
WHERE temp_call_stats.called_method = symbols.name
|
|
1103
|
+
), 0),
|
|
1104
|
+
called_by_count = COALESCE((
|
|
1105
|
+
SELECT unique_callers
|
|
1106
|
+
FROM temp_call_stats
|
|
1107
|
+
WHERE temp_call_stats.called_method = symbols.name
|
|
1108
|
+
), 0)
|
|
1109
|
+
WHERE type = 'method'
|
|
1110
|
+
AND EXISTS (SELECT 1 FROM temp_call_stats WHERE temp_call_stats.called_method = symbols.name);
|
|
1111
|
+
`);
|
|
1112
|
+
if (inCI) {
|
|
1113
|
+
log.detail('Setting zero counts for unused methods...');
|
|
1114
|
+
}
|
|
1115
|
+
// Set to 0 for methods not in temp_call_stats
|
|
1116
|
+
this.db.exec(`
|
|
1117
|
+
UPDATE symbols
|
|
1118
|
+
SET usage_frequency = 0, called_by_count = 0
|
|
1119
|
+
WHERE type = 'method'
|
|
1120
|
+
AND NOT EXISTS (SELECT 1 FROM temp_call_stats WHERE temp_call_stats.called_method = symbols.name);
|
|
1121
|
+
`);
|
|
1122
|
+
}
|
|
1123
|
+
catch (e) {
|
|
1124
|
+
log.warn('Optimized UPDATE failed, using fallback method');
|
|
1125
|
+
// Fallback to correlated subquery (slower but compatible)
|
|
1126
|
+
this.db.exec(`
|
|
1127
|
+
UPDATE symbols
|
|
1128
|
+
SET
|
|
1129
|
+
usage_frequency = COALESCE((SELECT total_calls FROM temp_call_stats WHERE called_method = symbols.name), 0),
|
|
1130
|
+
called_by_count = COALESCE((SELECT unique_callers FROM temp_call_stats WHERE called_method = symbols.name), 0)
|
|
1131
|
+
WHERE type = 'method';
|
|
1132
|
+
`);
|
|
1133
|
+
}
|
|
1134
|
+
// Cleanup
|
|
1135
|
+
this.db.exec('DROP TABLE IF EXISTS temp_call_stats;');
|
|
1136
|
+
});
|
|
1137
|
+
updateTransaction();
|
|
1138
|
+
// Cleanup
|
|
1139
|
+
this.db.exec('DROP TABLE temp_method_calls;');
|
|
1140
|
+
// Restore original synchronous setting
|
|
1141
|
+
this.db.pragma(`synchronous = ${originalSync}`);
|
|
1142
|
+
const endTime = Date.now();
|
|
1143
|
+
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
1144
|
+
log.ok(`Usage statistics computed in ${duration}s`);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Index metadata from a directory.
|
|
1148
|
+
*
|
|
1149
|
+
* `modelNames` scopes the pass:
|
|
1150
|
+
* - omitted → index every model directory found under `metadataPath`
|
|
1151
|
+
* - a model name → index just that one model
|
|
1152
|
+
* - an array → index exactly those models in a SINGLE pass
|
|
1153
|
+
*
|
|
1154
|
+
* Pass an array rather than calling this once per model: the FTS index is rebuilt from
|
|
1155
|
+
* scratch ONCE at the end of the call (and the FTS triggers dropped/recreated once), both
|
|
1156
|
+
* O(all symbols in the DB), so a per-model loop turns a scoped rebuild into N full-table
|
|
1157
|
+
* rebuilds.
|
|
1158
|
+
*/
|
|
1159
|
+
async indexMetadataDirectory(metadataPath, modelNames) {
|
|
1160
|
+
const skipFts = process.env.SKIP_FTS === 'true';
|
|
1161
|
+
const resumable = process.env.RESUME === 'true';
|
|
1162
|
+
const requested = modelNames === undefined
|
|
1163
|
+
? undefined
|
|
1164
|
+
: (Array.isArray(modelNames) ? modelNames : [modelNames]);
|
|
1165
|
+
const allModels = requested ?? await this.getModelDirectories(metadataPath);
|
|
1166
|
+
// Sort largest models first — ensures Foundation (56K files) is indexed before any CI timeout.
|
|
1167
|
+
// Skip the size scan only when a single model was requested (nothing to order).
|
|
1168
|
+
let models = allModels;
|
|
1169
|
+
if (allModels.length > 1) {
|
|
1170
|
+
log.detail(`Scanning ${allModels.length} model(s) to determine build order...`);
|
|
1171
|
+
models = this.sortModelsBySize(metadataPath, allModels);
|
|
1172
|
+
log.detail(`Build order determined (largest model first: ${models[0] ?? '—'})`);
|
|
1173
|
+
}
|
|
1174
|
+
// Skip already-indexed models when resuming (RESUME=true)
|
|
1175
|
+
if (resumable) {
|
|
1176
|
+
const done = this.getIndexedModels();
|
|
1177
|
+
const skipped = models.filter(m => done.has(m));
|
|
1178
|
+
models = models.filter(m => !done.has(m));
|
|
1179
|
+
if (skipped.length > 0) {
|
|
1180
|
+
log.detail(`Resuming build: skipping ${skipped.length} already-indexed model(s)`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const startTime = Date.now();
|
|
1184
|
+
// Disable FTS triggers during bulk insert — we rebuild FTS once at the end
|
|
1185
|
+
this.db.exec('DROP TRIGGER IF EXISTS symbols_ai;');
|
|
1186
|
+
this.db.exec('DROP TRIGGER IF EXISTS symbols_au;');
|
|
1187
|
+
this.db.exec('DROP TRIGGER IF EXISTS symbols_ad;');
|
|
1188
|
+
// Prepare progress statement (executes inside each model's transaction)
|
|
1189
|
+
const markProgress = resumable
|
|
1190
|
+
? this.db.prepare(`INSERT OR REPLACE INTO _build_progress (model, indexed_at) VALUES (?, ?)`)
|
|
1191
|
+
: null;
|
|
1192
|
+
// Per-model transactions (not one giant transaction): bounds peak memory to
|
|
1193
|
+
// one model's inserts and commits progress to disk after each model, so a
|
|
1194
|
+
// CI timeout can resume instead of losing all progress.
|
|
1195
|
+
let modelIndex = 0;
|
|
1196
|
+
for (const model of models) {
|
|
1197
|
+
modelIndex++;
|
|
1198
|
+
const modelPath = path.join(metadataPath, model);
|
|
1199
|
+
const modelStartTime = Date.now();
|
|
1200
|
+
const progressPercent = ((modelIndex / models.length) * 100).toFixed(0);
|
|
1201
|
+
// Show which model is being processed RIGHT NOW (before the slow transaction)
|
|
1202
|
+
if (isCI()) {
|
|
1203
|
+
log.detail(`[${progressPercent}%] indexing ${model}...`);
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
process.stdout.write(`\r ${c.dim(`[${progressPercent}%]`)} ${model.padEnd(40)} ${c.dim('indexing...')}`);
|
|
1207
|
+
}
|
|
1208
|
+
const tx = this.db.transaction(() => {
|
|
1209
|
+
const classesPath = path.join(modelPath, 'classes');
|
|
1210
|
+
if (fs.existsSync(classesPath))
|
|
1211
|
+
this.indexClasses(classesPath, model);
|
|
1212
|
+
const tablesPath = path.join(modelPath, 'tables');
|
|
1213
|
+
if (fs.existsSync(tablesPath))
|
|
1214
|
+
this.indexTables(tablesPath, model);
|
|
1215
|
+
const formsPath = path.join(modelPath, 'forms');
|
|
1216
|
+
if (fs.existsSync(formsPath))
|
|
1217
|
+
this.indexForms(formsPath, model);
|
|
1218
|
+
const queriesPath = path.join(modelPath, 'queries');
|
|
1219
|
+
if (fs.existsSync(queriesPath))
|
|
1220
|
+
this.indexQueries(queriesPath, model);
|
|
1221
|
+
const viewsPath = path.join(modelPath, 'views');
|
|
1222
|
+
if (fs.existsSync(viewsPath))
|
|
1223
|
+
this.indexViews(viewsPath, model);
|
|
1224
|
+
const enumsPath = path.join(modelPath, 'enums');
|
|
1225
|
+
if (fs.existsSync(enumsPath))
|
|
1226
|
+
this.indexEnums(enumsPath, model);
|
|
1227
|
+
const edtsPath = path.join(modelPath, 'edts');
|
|
1228
|
+
if (fs.existsSync(edtsPath))
|
|
1229
|
+
this.indexEdts(edtsPath, model);
|
|
1230
|
+
const reportsPath = path.join(modelPath, 'reports');
|
|
1231
|
+
if (fs.existsSync(reportsPath))
|
|
1232
|
+
this.indexReports(reportsPath, model);
|
|
1233
|
+
// Security artifacts
|
|
1234
|
+
const secPrivPath = path.join(modelPath, 'security-privileges');
|
|
1235
|
+
if (fs.existsSync(secPrivPath))
|
|
1236
|
+
this.indexSecurityPrivileges(secPrivPath, model);
|
|
1237
|
+
const secDutyPath = path.join(modelPath, 'security-duties');
|
|
1238
|
+
if (fs.existsSync(secDutyPath))
|
|
1239
|
+
this.indexSecurityDuties(secDutyPath, model);
|
|
1240
|
+
const secRolePath = path.join(modelPath, 'security-roles');
|
|
1241
|
+
if (fs.existsSync(secRolePath))
|
|
1242
|
+
this.indexSecurityRoles(secRolePath, model);
|
|
1243
|
+
// Menu items
|
|
1244
|
+
const menuDisplayPath = path.join(modelPath, 'menu-item-displays');
|
|
1245
|
+
if (fs.existsSync(menuDisplayPath))
|
|
1246
|
+
this.indexMenuItems(menuDisplayPath, model, 'display');
|
|
1247
|
+
const menuActionPath = path.join(modelPath, 'menu-item-actions');
|
|
1248
|
+
if (fs.existsSync(menuActionPath))
|
|
1249
|
+
this.indexMenuItems(menuActionPath, model, 'action');
|
|
1250
|
+
const menuOutputPath = path.join(modelPath, 'menu-item-outputs');
|
|
1251
|
+
if (fs.existsSync(menuOutputPath))
|
|
1252
|
+
this.indexMenuItems(menuOutputPath, model, 'output');
|
|
1253
|
+
// Extensions
|
|
1254
|
+
const tableExtPath = path.join(modelPath, 'table-extensions');
|
|
1255
|
+
if (fs.existsSync(tableExtPath))
|
|
1256
|
+
this.indexExtensions(tableExtPath, model, 'table-extension');
|
|
1257
|
+
const classExtPath = path.join(modelPath, 'class-extensions');
|
|
1258
|
+
if (fs.existsSync(classExtPath))
|
|
1259
|
+
this.indexExtensions(classExtPath, model, 'class-extension');
|
|
1260
|
+
const formExtPath = path.join(modelPath, 'form-extensions');
|
|
1261
|
+
if (fs.existsSync(formExtPath))
|
|
1262
|
+
this.indexExtensions(formExtPath, model, 'form-extension');
|
|
1263
|
+
const enumExtPath = path.join(modelPath, 'enum-extensions');
|
|
1264
|
+
if (fs.existsSync(enumExtPath))
|
|
1265
|
+
this.indexExtensions(enumExtPath, model, 'enum-extension');
|
|
1266
|
+
const edtExtPath = path.join(modelPath, 'edt-extensions');
|
|
1267
|
+
if (fs.existsSync(edtExtPath))
|
|
1268
|
+
this.indexExtensions(edtExtPath, model, 'edt-extension');
|
|
1269
|
+
const deExtPath = path.join(modelPath, 'data-entity-extensions');
|
|
1270
|
+
if (fs.existsSync(deExtPath))
|
|
1271
|
+
this.indexExtensions(deExtPath, model, 'data-entity-extension');
|
|
1272
|
+
const viewExtPath = path.join(modelPath, 'view-extensions');
|
|
1273
|
+
if (fs.existsSync(viewExtPath))
|
|
1274
|
+
this.indexExtensions(viewExtPath, model, 'view-extension');
|
|
1275
|
+
const queryExtPath = path.join(modelPath, 'query-extensions');
|
|
1276
|
+
if (fs.existsSync(queryExtPath))
|
|
1277
|
+
this.indexExtensions(queryExtPath, model, 'query-extension');
|
|
1278
|
+
const mapExtPath = path.join(modelPath, 'map-extensions');
|
|
1279
|
+
if (fs.existsSync(mapExtPath))
|
|
1280
|
+
this.indexExtensions(mapExtPath, model, 'map-extension');
|
|
1281
|
+
const menuExtPath = path.join(modelPath, 'menu-extensions');
|
|
1282
|
+
if (fs.existsSync(menuExtPath))
|
|
1283
|
+
this.indexExtensions(menuExtPath, model, 'menu-extension');
|
|
1284
|
+
const secDutyExtPath = path.join(modelPath, 'security-duty-extensions');
|
|
1285
|
+
if (fs.existsSync(secDutyExtPath))
|
|
1286
|
+
this.indexExtensions(secDutyExtPath, model, 'security-duty-extension');
|
|
1287
|
+
const secRoleExtPath = path.join(modelPath, 'security-role-extensions');
|
|
1288
|
+
if (fs.existsSync(secRoleExtPath))
|
|
1289
|
+
this.indexExtensions(secRoleExtPath, model, 'security-role-extension');
|
|
1290
|
+
const miDisplayExtPath = path.join(modelPath, 'menu-item-display-extensions');
|
|
1291
|
+
if (fs.existsSync(miDisplayExtPath))
|
|
1292
|
+
this.indexExtensions(miDisplayExtPath, model, 'menu-item-display-extension');
|
|
1293
|
+
const miActionExtPath = path.join(modelPath, 'menu-item-action-extensions');
|
|
1294
|
+
if (fs.existsSync(miActionExtPath))
|
|
1295
|
+
this.indexExtensions(miActionExtPath, model, 'menu-item-action-extension');
|
|
1296
|
+
const miOutputExtPath = path.join(modelPath, 'menu-item-output-extensions');
|
|
1297
|
+
if (fs.existsSync(miOutputExtPath))
|
|
1298
|
+
this.indexExtensions(miOutputExtPath, model, 'menu-item-output-extension');
|
|
1299
|
+
// Services + service groups
|
|
1300
|
+
const servicesPath = path.join(modelPath, 'services');
|
|
1301
|
+
if (fs.existsSync(servicesPath))
|
|
1302
|
+
this.indexServices(servicesPath, model);
|
|
1303
|
+
const serviceGroupsPath = path.join(modelPath, 'service-groups');
|
|
1304
|
+
if (fs.existsSync(serviceGroupsPath))
|
|
1305
|
+
this.indexServiceGroups(serviceGroupsPath, model);
|
|
1306
|
+
// Maps, feature gating, security policies, macros
|
|
1307
|
+
const mapsPath = path.join(modelPath, 'maps');
|
|
1308
|
+
if (fs.existsSync(mapsPath))
|
|
1309
|
+
this.indexMaps(mapsPath, model);
|
|
1310
|
+
const configKeysPath = path.join(modelPath, 'configuration-keys');
|
|
1311
|
+
if (fs.existsSync(configKeysPath))
|
|
1312
|
+
this.indexConfigurationKeys(configKeysPath, model);
|
|
1313
|
+
const licenseCodesPath = path.join(modelPath, 'license-codes');
|
|
1314
|
+
if (fs.existsSync(licenseCodesPath))
|
|
1315
|
+
this.indexLicenseCodes(licenseCodesPath, model);
|
|
1316
|
+
const securityPoliciesPath = path.join(modelPath, 'security-policies');
|
|
1317
|
+
if (fs.existsSync(securityPoliciesPath))
|
|
1318
|
+
this.indexSecurityPolicies(securityPoliciesPath, model);
|
|
1319
|
+
const macrosPath = path.join(modelPath, 'macros');
|
|
1320
|
+
if (fs.existsSync(macrosPath))
|
|
1321
|
+
this.indexMacros(macrosPath, model);
|
|
1322
|
+
// Flush buffered property_stats observations (batch write — much faster than
|
|
1323
|
+
// per-field upserts scattered across the transaction)
|
|
1324
|
+
this.flushPropertyStats();
|
|
1325
|
+
// Mark model as done atomically with its data (same transaction)
|
|
1326
|
+
markProgress?.run(model, Date.now());
|
|
1327
|
+
});
|
|
1328
|
+
tx();
|
|
1329
|
+
const modelDuration = ((Date.now() - modelStartTime) / 1000).toFixed(1);
|
|
1330
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
1331
|
+
if (isCI()) {
|
|
1332
|
+
// CI environment: overwrite the "indexing..." line with the completed summary
|
|
1333
|
+
log.detail(`[${progressPercent}%] ${model} - ${modelDuration}s (${elapsed}s total)`);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
// Interactive terminal: overwrite the "indexing..." line with completed summary
|
|
1337
|
+
process.stdout.write(`\r ${c.dim(`[${progressPercent}%]`)} ${model.padEnd(40)} ${c.dim(`${modelDuration}s (${elapsed}s total)`)}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1341
|
+
// Add newline only if we were overwriting (interactive mode)
|
|
1342
|
+
if (!isCI()) {
|
|
1343
|
+
console.log(''); // New line after progress
|
|
1344
|
+
}
|
|
1345
|
+
if (skipFts) {
|
|
1346
|
+
// Phase 1 of two-phase CI build: symbols only, FTS deferred to build-fts step
|
|
1347
|
+
log.info(`Skipping FTS rebuild (SKIP_FTS=true) - run 'npm run build-fts' to finish`);
|
|
1348
|
+
this.createFTSTriggers();
|
|
1349
|
+
log.ok(`Indexed ${models.length} model(s) in ${duration}s`);
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
// Rebuild FTS index from scratch (much faster than per-insert triggers)
|
|
1353
|
+
const ftsStartTime = Date.now();
|
|
1354
|
+
this.db.exec("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild');");
|
|
1355
|
+
const ftsDuration = ((Date.now() - ftsStartTime) / 1000).toFixed(1);
|
|
1356
|
+
this.createFTSTriggers();
|
|
1357
|
+
log.ok(`Indexed ${models.length} model(s) in ${duration}s (FTS rebuilt in ${ftsDuration}s)`);
|
|
1358
|
+
}
|
|
1359
|
+
this.touchLastIndexed();
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Sort models by JSON file count descending.
|
|
1363
|
+
* Ensures the largest models (e.g. Foundation with 56K files) are indexed first,
|
|
1364
|
+
* so the most data is committed to disk before any CI pipeline timeout.
|
|
1365
|
+
*
|
|
1366
|
+
* Uses a single recursive readdirSync per model (Node 18.17+) instead of
|
|
1367
|
+
* 20 separate readdirSync calls per subdirectory — ~20× fewer syscalls.
|
|
1368
|
+
*/
|
|
1369
|
+
sortModelsBySize(metadataPath, models) {
|
|
1370
|
+
const sized = models.map(model => {
|
|
1371
|
+
const modelPath = path.join(metadataPath, model);
|
|
1372
|
+
let count = 0;
|
|
1373
|
+
try {
|
|
1374
|
+
// readdirSync with recursive:true returns all entries in one call (Node 18.17+)
|
|
1375
|
+
const entries = fs.readdirSync(modelPath, { recursive: true });
|
|
1376
|
+
count = entries.filter(f => f.endsWith('.json')).length;
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
// Unreadable model directory — treat as empty (will be sorted last)
|
|
1380
|
+
}
|
|
1381
|
+
return { model, count };
|
|
1382
|
+
});
|
|
1383
|
+
return sized.sort((a, b) => b.count - a.count).map(s => s.model);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get the set of models already indexed (for RESUME=true builds).
|
|
1387
|
+
*/
|
|
1388
|
+
getIndexedModels() {
|
|
1389
|
+
try {
|
|
1390
|
+
const rows = this.db.prepare(`SELECT model FROM _build_progress`).all();
|
|
1391
|
+
return new Set(rows.map(r => r.model));
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
return new Set();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Clear progress tracking checkpoint (call before a fresh full rebuild).
|
|
1399
|
+
*/
|
|
1400
|
+
clearProgressTracking() {
|
|
1401
|
+
try {
|
|
1402
|
+
this.db.exec(`DELETE FROM _build_progress`);
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
// Table may not exist yet
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Rebuild the FTS index for symbols from scratch.
|
|
1410
|
+
* Use this as a standalone step after a SKIP_FTS=true build (Phase 2 of two-phase CI).
|
|
1411
|
+
*/
|
|
1412
|
+
rebuildFTS() {
|
|
1413
|
+
log.step('Rebuilding symbols FTS index...');
|
|
1414
|
+
this.db.exec('DROP TRIGGER IF EXISTS symbols_ai;');
|
|
1415
|
+
this.db.exec('DROP TRIGGER IF EXISTS symbols_au;');
|
|
1416
|
+
this.db.exec('DROP TRIGGER IF EXISTS symbols_ad;');
|
|
1417
|
+
const start = Date.now();
|
|
1418
|
+
this.db.exec("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild');");
|
|
1419
|
+
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
|
1420
|
+
this.createFTSTriggers();
|
|
1421
|
+
log.ok(`Symbols FTS index rebuilt in ${duration}s`);
|
|
1422
|
+
}
|
|
1423
|
+
async getModelDirectories(metadataPath) {
|
|
1424
|
+
const entries = fs.readdirSync(metadataPath, { withFileTypes: true });
|
|
1425
|
+
return entries
|
|
1426
|
+
.filter(entry => entry.isDirectory())
|
|
1427
|
+
.map(entry => entry.name);
|
|
1428
|
+
}
|
|
1429
|
+
indexClasses(classesPath, model) {
|
|
1430
|
+
const files = fs.readdirSync(classesPath).filter(f => f.endsWith('.json'));
|
|
1431
|
+
let processedCount = 0;
|
|
1432
|
+
for (const file of files) {
|
|
1433
|
+
try {
|
|
1434
|
+
const filePath = path.join(classesPath, file);
|
|
1435
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1436
|
+
const classData = JSON.parse(content);
|
|
1437
|
+
// Use sourcePath from metadata (original XML file) instead of JSON file path
|
|
1438
|
+
const sourceFilePath = classData.sourcePath || filePath;
|
|
1439
|
+
// Add class symbol with enhanced metadata
|
|
1440
|
+
this.addSymbol({
|
|
1441
|
+
name: classData.name,
|
|
1442
|
+
type: 'class',
|
|
1443
|
+
signature: classData.extends ? `extends ${classData.extends}` : undefined,
|
|
1444
|
+
filePath: sourceFilePath,
|
|
1445
|
+
model,
|
|
1446
|
+
description: classData.description || classData.documentation,
|
|
1447
|
+
tags: classData.tags?.join(', '),
|
|
1448
|
+
extendsClass: classData.extends,
|
|
1449
|
+
implementsInterfaces: classData.implements?.join(', '),
|
|
1450
|
+
usedTypes: classData.usedTypes?.join(', '),
|
|
1451
|
+
// Pattern analysis fields
|
|
1452
|
+
patternType: classData.patternType,
|
|
1453
|
+
typicalUsages: classData.typicalUsages ? JSON.stringify(classData.typicalUsages) : undefined,
|
|
1454
|
+
relatedMethods: classData.relatedMethods ? JSON.stringify(classData.relatedMethods) : undefined,
|
|
1455
|
+
apiPatterns: classData.apiPatterns ? JSON.stringify(classData.apiPatterns) : undefined,
|
|
1456
|
+
});
|
|
1457
|
+
// Add method symbols with enhanced metadata
|
|
1458
|
+
if (classData.methods && Array.isArray(classData.methods)) {
|
|
1459
|
+
for (const method of classData.methods) {
|
|
1460
|
+
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
|
|
1461
|
+
this.addSymbol({
|
|
1462
|
+
name: method.name,
|
|
1463
|
+
type: 'method',
|
|
1464
|
+
parentName: classData.name,
|
|
1465
|
+
signature: `${method.returnType} ${method.name}(${params})`,
|
|
1466
|
+
filePath: sourceFilePath,
|
|
1467
|
+
model,
|
|
1468
|
+
description: method.documentation,
|
|
1469
|
+
tags: method.tags?.join(', '),
|
|
1470
|
+
sourceSnippet: method.sourceSnippet,
|
|
1471
|
+
source: method.source,
|
|
1472
|
+
complexity: method.complexity,
|
|
1473
|
+
usedTypes: method.usedTypes?.join(', '),
|
|
1474
|
+
methodCalls: method.methodCalls?.join(', '),
|
|
1475
|
+
inlineComments: method.inlineComments,
|
|
1476
|
+
usageExample: method.usageExample,
|
|
1477
|
+
// Pattern analysis fields
|
|
1478
|
+
typicalUsages: method.typicalUsages ? JSON.stringify(method.typicalUsages) : undefined,
|
|
1479
|
+
relatedMethods: method.relatedMethods ? JSON.stringify(method.relatedMethods) : undefined,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
processedCount++;
|
|
1484
|
+
}
|
|
1485
|
+
catch (error) {
|
|
1486
|
+
log.warn(`Skipped ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
indexTables(tablesPath, model) {
|
|
1491
|
+
const files = fs.readdirSync(tablesPath).filter(f => f.endsWith('.json'));
|
|
1492
|
+
for (const file of files) {
|
|
1493
|
+
try {
|
|
1494
|
+
const filePath = path.join(tablesPath, file);
|
|
1495
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1496
|
+
const tableData = JSON.parse(content);
|
|
1497
|
+
// Use sourcePath from metadata (original XML file) instead of JSON file path
|
|
1498
|
+
const sourceFilePath = tableData.sourcePath || filePath;
|
|
1499
|
+
this.addSymbol({
|
|
1500
|
+
name: tableData.name,
|
|
1501
|
+
type: 'table',
|
|
1502
|
+
signature: tableData.label || undefined,
|
|
1503
|
+
filePath: sourceFilePath,
|
|
1504
|
+
model,
|
|
1505
|
+
});
|
|
1506
|
+
// Mine property distribution for data-driven BP rules (standard models only)
|
|
1507
|
+
this.recordTablePropertyStats(tableData, model);
|
|
1508
|
+
// Add field symbols
|
|
1509
|
+
if (tableData.fields && Array.isArray(tableData.fields)) {
|
|
1510
|
+
for (const field of tableData.fields) {
|
|
1511
|
+
this.addSymbol({
|
|
1512
|
+
name: field.name,
|
|
1513
|
+
type: 'field',
|
|
1514
|
+
parentName: tableData.name,
|
|
1515
|
+
signature: field.type,
|
|
1516
|
+
filePath: sourceFilePath,
|
|
1517
|
+
model,
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Add method symbols (parallel to indexClasses)
|
|
1522
|
+
if (tableData.methods && Array.isArray(tableData.methods)) {
|
|
1523
|
+
for (const method of tableData.methods) {
|
|
1524
|
+
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
|
|
1525
|
+
this.addSymbol({
|
|
1526
|
+
name: method.name,
|
|
1527
|
+
type: 'method',
|
|
1528
|
+
parentName: tableData.name,
|
|
1529
|
+
signature: `${method.returnType} ${method.name}(${params})`,
|
|
1530
|
+
filePath: sourceFilePath,
|
|
1531
|
+
model,
|
|
1532
|
+
description: method.documentation,
|
|
1533
|
+
tags: method.tags?.join(', '),
|
|
1534
|
+
sourceSnippet: method.sourceSnippet,
|
|
1535
|
+
source: method.source,
|
|
1536
|
+
complexity: method.complexity,
|
|
1537
|
+
usedTypes: method.usedTypes?.join(', '),
|
|
1538
|
+
methodCalls: method.methodCalls?.join(', '),
|
|
1539
|
+
inlineComments: method.inlineComments,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
// Index table relations to new table
|
|
1544
|
+
if (tableData.relations && Array.isArray(tableData.relations)) {
|
|
1545
|
+
const stmt = this.db.prepare(`
|
|
1546
|
+
INSERT OR IGNORE INTO table_relations (
|
|
1547
|
+
source_table, target_table, relation_name, constraint_fields, model
|
|
1548
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
1549
|
+
`);
|
|
1550
|
+
for (const relation of tableData.relations) {
|
|
1551
|
+
if (relation.name && relation.relatedTable) {
|
|
1552
|
+
// Serialize constraints as JSON
|
|
1553
|
+
const constraintFields = relation.constraints
|
|
1554
|
+
? JSON.stringify(relation.constraints)
|
|
1555
|
+
: null;
|
|
1556
|
+
stmt.run(tableData.name, relation.relatedTable, relation.name, constraintFields, model);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
catch (error) {
|
|
1562
|
+
log.warn(`Skipped table ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
indexEnums(enumsPath, model) {
|
|
1567
|
+
const files = fs.readdirSync(enumsPath).filter(f => f.endsWith('.json'));
|
|
1568
|
+
for (const file of files) {
|
|
1569
|
+
try {
|
|
1570
|
+
const filePath = path.join(enumsPath, file);
|
|
1571
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1572
|
+
const enumData = JSON.parse(content);
|
|
1573
|
+
// Use sourcePath from metadata (original XML file) instead of JSON file path
|
|
1574
|
+
const sourceFilePath = enumData.sourcePath || filePath;
|
|
1575
|
+
const enumName = enumData.name || path.basename(file, '.json');
|
|
1576
|
+
this.addSymbol({
|
|
1577
|
+
name: enumName,
|
|
1578
|
+
type: 'enum',
|
|
1579
|
+
filePath: sourceFilePath,
|
|
1580
|
+
model,
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
catch (error) {
|
|
1584
|
+
log.warn(`Skipped enum ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
indexEdts(edtsPath, model) {
|
|
1589
|
+
const files = fs.readdirSync(edtsPath).filter(f => f.endsWith('.json'));
|
|
1590
|
+
for (const file of files) {
|
|
1591
|
+
try {
|
|
1592
|
+
const filePath = path.join(edtsPath, file);
|
|
1593
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1594
|
+
const edtData = JSON.parse(content);
|
|
1595
|
+
const sourceFilePath = edtData.sourcePath || filePath;
|
|
1596
|
+
const edtName = edtData.name || path.basename(file, '.json');
|
|
1597
|
+
let signature;
|
|
1598
|
+
// Enhanced metadata extraction from parsed EDT info
|
|
1599
|
+
if (edtData.extends) {
|
|
1600
|
+
signature = edtData.extends;
|
|
1601
|
+
}
|
|
1602
|
+
else if (edtData.enumType) {
|
|
1603
|
+
signature = edtData.enumType;
|
|
1604
|
+
}
|
|
1605
|
+
else if (typeof edtData.raw === 'string') {
|
|
1606
|
+
// Fallback to raw XML parsing for old metadata
|
|
1607
|
+
const extendsMatch = edtData.raw.match(/<Extends>([^<]+)<\/Extends>/i);
|
|
1608
|
+
const enumTypeMatch = edtData.raw.match(/<EnumType>([^<]+)<\/EnumType>/i);
|
|
1609
|
+
signature = extendsMatch?.[1]?.trim() || enumTypeMatch?.[1]?.trim();
|
|
1610
|
+
}
|
|
1611
|
+
// Add symbol
|
|
1612
|
+
this.addSymbol({
|
|
1613
|
+
name: edtName,
|
|
1614
|
+
type: 'edt',
|
|
1615
|
+
signature,
|
|
1616
|
+
filePath: sourceFilePath,
|
|
1617
|
+
model,
|
|
1618
|
+
});
|
|
1619
|
+
// Add extended EDT metadata to new table — always insert when any property is present
|
|
1620
|
+
if (edtData.extends || edtData.enumType || edtData.referenceTable ||
|
|
1621
|
+
edtData.stringSize || edtData.displayLength || edtData.label ||
|
|
1622
|
+
edtData.databaseStringSize) {
|
|
1623
|
+
const stmt = this.db.prepare(`
|
|
1624
|
+
INSERT OR REPLACE INTO edt_metadata (
|
|
1625
|
+
edt_name, extends, enum_type, reference_table, relation_type,
|
|
1626
|
+
string_size, database_string_size, display_length, label, model
|
|
1627
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1628
|
+
`);
|
|
1629
|
+
stmt.run(edtName, edtData.extends || null, edtData.enumType || null, edtData.referenceTable || null, edtData.relationType || null, edtData.stringSize || null, edtData.databaseStringSize || null, edtData.displayLength || null, edtData.label || null, model);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
log.warn(`Skipped edt ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
indexReports(reportsPath, model) {
|
|
1638
|
+
const files = fs.readdirSync(reportsPath).filter(f => f.endsWith('.json'));
|
|
1639
|
+
for (const file of files) {
|
|
1640
|
+
try {
|
|
1641
|
+
const filePath = path.join(reportsPath, file);
|
|
1642
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1643
|
+
const reportData = JSON.parse(content);
|
|
1644
|
+
// sourcePath points to the live AxReport XML on disk (set by extractReports)
|
|
1645
|
+
const sourceFilePath = reportData.sourcePath || filePath;
|
|
1646
|
+
const reportName = reportData.name || path.basename(file, '.json');
|
|
1647
|
+
this.addSymbol({
|
|
1648
|
+
name: reportName,
|
|
1649
|
+
type: 'report',
|
|
1650
|
+
filePath: sourceFilePath,
|
|
1651
|
+
model,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
catch (error) {
|
|
1655
|
+
log.warn(`Skipped report ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
indexForms(formsPath, model) {
|
|
1660
|
+
const files = fs.readdirSync(formsPath).filter(f => f.endsWith('.json'));
|
|
1661
|
+
for (const file of files) {
|
|
1662
|
+
try {
|
|
1663
|
+
const filePath = path.join(formsPath, file);
|
|
1664
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1665
|
+
const formData = JSON.parse(content);
|
|
1666
|
+
// Use sourcePath from metadata (original XML file) instead of JSON file path
|
|
1667
|
+
const sourceFilePath = formData.sourcePath || filePath;
|
|
1668
|
+
const formName = formData.name || path.basename(file, '.json');
|
|
1669
|
+
this.addSymbol({
|
|
1670
|
+
name: formName,
|
|
1671
|
+
type: 'form',
|
|
1672
|
+
filePath: sourceFilePath,
|
|
1673
|
+
model,
|
|
1674
|
+
description: formData.caption || formData.label,
|
|
1675
|
+
});
|
|
1676
|
+
// Index mined pattern nodes (Design + sub-patterned containers) and
|
|
1677
|
+
// record pattern distribution stats for the advisor/cross-check.
|
|
1678
|
+
if (formData.patternNodes && Array.isArray(formData.patternNodes)) {
|
|
1679
|
+
const patternStmt = this.db.prepare(`
|
|
1680
|
+
INSERT OR REPLACE INTO form_patterns (
|
|
1681
|
+
form_name, model, node_path, control_name, control_type,
|
|
1682
|
+
pattern, pattern_version, child_sequence
|
|
1683
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1684
|
+
`);
|
|
1685
|
+
for (const node of formData.patternNodes) {
|
|
1686
|
+
if (!node?.pattern || !node?.nodePath)
|
|
1687
|
+
continue;
|
|
1688
|
+
patternStmt.run(formName, model, node.nodePath, node.controlName ?? '', node.controlType ?? '', node.pattern, node.patternVersion ?? null, JSON.stringify(node.childSequence ?? []));
|
|
1689
|
+
if (node.nodePath === 'Design') {
|
|
1690
|
+
this.recordPropertyStat('AxFormDesign', 'Pattern', node.pattern, model);
|
|
1691
|
+
this.recordPropertyStat('AxFormDesign', `PatternVersion:${node.pattern}`, node.patternVersion ?? '(absent)', model);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
// Index form datasources to new table
|
|
1696
|
+
if (formData.dataSources && Array.isArray(formData.dataSources)) {
|
|
1697
|
+
const stmt = this.db.prepare(`
|
|
1698
|
+
INSERT OR REPLACE INTO form_datasources (
|
|
1699
|
+
form_name, datasource_name, table_name,
|
|
1700
|
+
allow_edit, allow_create, allow_delete, model
|
|
1701
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1702
|
+
`);
|
|
1703
|
+
for (const ds of formData.dataSources) {
|
|
1704
|
+
if (ds.name && ds.table) {
|
|
1705
|
+
stmt.run(formName, ds.name, ds.table, ds.allowEdit ? 1 : 0, ds.allowCreate ? 1 : 0, ds.allowDelete ? 1 : 0, model);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
catch (error) {
|
|
1711
|
+
log.warn(`Skipped form ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
indexQueries(queriesPath, model) {
|
|
1716
|
+
const files = fs.readdirSync(queriesPath).filter(f => f.endsWith('.json'));
|
|
1717
|
+
for (const file of files) {
|
|
1718
|
+
try {
|
|
1719
|
+
const filePath = path.join(queriesPath, file);
|
|
1720
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1721
|
+
const queryData = JSON.parse(content);
|
|
1722
|
+
// Use sourcePath from metadata (original XML file) instead of JSON file path
|
|
1723
|
+
const sourceFilePath = queryData.sourcePath || filePath;
|
|
1724
|
+
const queryName = queryData.name || path.basename(file, '.json');
|
|
1725
|
+
this.addSymbol({
|
|
1726
|
+
name: queryName,
|
|
1727
|
+
type: 'query',
|
|
1728
|
+
filePath: sourceFilePath,
|
|
1729
|
+
model,
|
|
1730
|
+
description: queryData.title || queryData.label,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
catch (error) {
|
|
1734
|
+
log.warn(`Skipped query ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
indexViews(viewsPath, model) {
|
|
1739
|
+
const files = fs.readdirSync(viewsPath).filter(f => f.endsWith('.json'));
|
|
1740
|
+
for (const file of files) {
|
|
1741
|
+
try {
|
|
1742
|
+
const filePath = path.join(viewsPath, file);
|
|
1743
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1744
|
+
const viewData = JSON.parse(content);
|
|
1745
|
+
// Use sourcePath from metadata (original XML file) instead of JSON file path
|
|
1746
|
+
const sourceFilePath = viewData.sourcePath || filePath;
|
|
1747
|
+
const viewName = viewData.name || path.basename(file, '.json');
|
|
1748
|
+
this.addSymbol({
|
|
1749
|
+
name: viewName,
|
|
1750
|
+
type: 'view',
|
|
1751
|
+
signature: viewData.type || undefined,
|
|
1752
|
+
filePath: sourceFilePath,
|
|
1753
|
+
model,
|
|
1754
|
+
description: viewData.label || viewData.type,
|
|
1755
|
+
});
|
|
1756
|
+
// Add field symbols (same pattern as tables)
|
|
1757
|
+
if (viewData.fields && Array.isArray(viewData.fields)) {
|
|
1758
|
+
for (const field of viewData.fields) {
|
|
1759
|
+
this.addSymbol({
|
|
1760
|
+
name: field.name,
|
|
1761
|
+
type: 'field',
|
|
1762
|
+
parentName: viewName,
|
|
1763
|
+
signature: field.dataMethod || field.dataField || undefined,
|
|
1764
|
+
filePath: sourceFilePath,
|
|
1765
|
+
model,
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
// Add method symbols (views and data-entities can have display/computed methods)
|
|
1770
|
+
if (viewData.methods && Array.isArray(viewData.methods)) {
|
|
1771
|
+
for (const method of viewData.methods) {
|
|
1772
|
+
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
|
|
1773
|
+
this.addSymbol({
|
|
1774
|
+
name: method.name,
|
|
1775
|
+
type: 'method',
|
|
1776
|
+
parentName: viewName,
|
|
1777
|
+
signature: `${method.returnType} ${method.name}(${params})`,
|
|
1778
|
+
filePath: sourceFilePath,
|
|
1779
|
+
model,
|
|
1780
|
+
description: method.documentation,
|
|
1781
|
+
tags: method.tags?.join(', '),
|
|
1782
|
+
sourceSnippet: method.sourceSnippet,
|
|
1783
|
+
source: method.source,
|
|
1784
|
+
complexity: method.complexity,
|
|
1785
|
+
usedTypes: method.usedTypes?.join(', '),
|
|
1786
|
+
methodCalls: method.methodCalls?.join(', '),
|
|
1787
|
+
inlineComments: method.inlineComments,
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
catch (error) {
|
|
1793
|
+
log.warn(`Skipped view ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
indexSecurityPrivileges(dirPath, model) {
|
|
1798
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1799
|
+
const insertEntry = this.db.prepare(`
|
|
1800
|
+
INSERT OR IGNORE INTO security_privilege_entries
|
|
1801
|
+
(privilege_name, entry_point_name, object_type, access_level, model)
|
|
1802
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1803
|
+
`);
|
|
1804
|
+
for (const file of files) {
|
|
1805
|
+
try {
|
|
1806
|
+
const filePath = path.join(dirPath, file);
|
|
1807
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1808
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
1809
|
+
const name = data.name || path.basename(file, '.json');
|
|
1810
|
+
const entryPoints = data.entryPoints || [];
|
|
1811
|
+
this.addSymbol({
|
|
1812
|
+
name,
|
|
1813
|
+
type: 'security-privilege',
|
|
1814
|
+
filePath: sourceFilePath,
|
|
1815
|
+
model,
|
|
1816
|
+
description: data.label || undefined,
|
|
1817
|
+
signature: entryPoints.length > 0 ? `${entryPoints.length} entry point(s)` : undefined,
|
|
1818
|
+
});
|
|
1819
|
+
for (const ep of entryPoints) {
|
|
1820
|
+
// Skip malformed entry points — missing name causes "Too few parameter values" in better-sqlite3
|
|
1821
|
+
if (!ep.name)
|
|
1822
|
+
continue;
|
|
1823
|
+
// Safeguard: accessLevel may be an object in older JSONs extracted before
|
|
1824
|
+
// xmlParser normalisation was added. Serialize to string.
|
|
1825
|
+
const accessLevelStr = ep.accessLevel == null
|
|
1826
|
+
? null
|
|
1827
|
+
: typeof ep.accessLevel === 'object'
|
|
1828
|
+
? Object.entries(ep.accessLevel).map(([k, v]) => `${k}:${v}`).join(',')
|
|
1829
|
+
: String(ep.accessLevel);
|
|
1830
|
+
insertEntry.run(name, ep.name, ep.objectType ?? null, accessLevelStr, model);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
catch (error) {
|
|
1834
|
+
log.warn(`Skipped security-privilege ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
indexSecurityDuties(dirPath, model) {
|
|
1839
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1840
|
+
const insertPriv = this.db.prepare(`
|
|
1841
|
+
INSERT OR IGNORE INTO security_duty_privileges (duty_name, privilege_name, model)
|
|
1842
|
+
VALUES (?, ?, ?)
|
|
1843
|
+
`);
|
|
1844
|
+
for (const file of files) {
|
|
1845
|
+
try {
|
|
1846
|
+
const filePath = path.join(dirPath, file);
|
|
1847
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1848
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
1849
|
+
const name = data.name || path.basename(file, '.json');
|
|
1850
|
+
const privileges = data.privileges || [];
|
|
1851
|
+
this.addSymbol({
|
|
1852
|
+
name,
|
|
1853
|
+
type: 'security-duty',
|
|
1854
|
+
filePath: sourceFilePath,
|
|
1855
|
+
model,
|
|
1856
|
+
description: data.label || undefined,
|
|
1857
|
+
signature: privileges.length > 0 ? `${privileges.length} privilege(s)` : undefined,
|
|
1858
|
+
});
|
|
1859
|
+
for (const priv of privileges) {
|
|
1860
|
+
insertPriv.run(name, priv, model);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
catch (error) {
|
|
1864
|
+
log.warn(`Skipped security-duty ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
indexSecurityRoles(dirPath, model) {
|
|
1869
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1870
|
+
const insertDuty = this.db.prepare(`
|
|
1871
|
+
INSERT OR IGNORE INTO security_role_duties (role_name, duty_name, model)
|
|
1872
|
+
VALUES (?, ?, ?)
|
|
1873
|
+
`);
|
|
1874
|
+
for (const file of files) {
|
|
1875
|
+
try {
|
|
1876
|
+
const filePath = path.join(dirPath, file);
|
|
1877
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1878
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
1879
|
+
const name = data.name || path.basename(file, '.json');
|
|
1880
|
+
const duties = data.duties || [];
|
|
1881
|
+
this.addSymbol({
|
|
1882
|
+
name,
|
|
1883
|
+
type: 'security-role',
|
|
1884
|
+
filePath: sourceFilePath,
|
|
1885
|
+
model,
|
|
1886
|
+
description: data.description || data.label || undefined,
|
|
1887
|
+
signature: duties.length > 0 ? `${duties.length} duty(ies)` : undefined,
|
|
1888
|
+
});
|
|
1889
|
+
for (const duty of duties) {
|
|
1890
|
+
insertDuty.run(name, duty, model);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
catch (error) {
|
|
1894
|
+
log.warn(`Skipped security-role ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
indexMenuItems(dirPath, model, menuItemType) {
|
|
1899
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1900
|
+
const symbolType = `menu-item-${menuItemType}`;
|
|
1901
|
+
const insertTarget = this.db.prepare(`
|
|
1902
|
+
INSERT OR REPLACE INTO menu_item_targets
|
|
1903
|
+
(menu_item_name, menu_item_type, target_object, target_type, security_privilege, label, model)
|
|
1904
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1905
|
+
`);
|
|
1906
|
+
for (const file of files) {
|
|
1907
|
+
try {
|
|
1908
|
+
const filePath = path.join(dirPath, file);
|
|
1909
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1910
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
1911
|
+
const name = data.name || path.basename(file, '.json');
|
|
1912
|
+
this.addSymbol({
|
|
1913
|
+
name,
|
|
1914
|
+
type: symbolType,
|
|
1915
|
+
filePath: sourceFilePath,
|
|
1916
|
+
model,
|
|
1917
|
+
description: data.label || undefined,
|
|
1918
|
+
signature: data.targetObject || data.object || undefined,
|
|
1919
|
+
});
|
|
1920
|
+
insertTarget.run(name, menuItemType, data.targetObject || data.object || null, data.targetType || data.objectType || null, data.securityPrivilege || null, data.label || null, model);
|
|
1921
|
+
}
|
|
1922
|
+
catch (error) {
|
|
1923
|
+
log.warn(`Skipped menu-item-${menuItemType} ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
indexServices(dirPath, model) {
|
|
1928
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1929
|
+
const insertOp = this.db.prepare(`
|
|
1930
|
+
INSERT INTO service_operations
|
|
1931
|
+
(service_name, operation_name, method_name, idempotent, model)
|
|
1932
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1933
|
+
`);
|
|
1934
|
+
for (const file of files) {
|
|
1935
|
+
try {
|
|
1936
|
+
const filePath = path.join(dirPath, file);
|
|
1937
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1938
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
1939
|
+
const name = data.name || path.basename(file, '.json');
|
|
1940
|
+
// signature carries the backing class; description carries the external name
|
|
1941
|
+
// so both surface in search/FTS without an extra lookup.
|
|
1942
|
+
this.addSymbol({
|
|
1943
|
+
name,
|
|
1944
|
+
type: 'service',
|
|
1945
|
+
filePath: sourceFilePath,
|
|
1946
|
+
model,
|
|
1947
|
+
signature: data.serviceClass || undefined,
|
|
1948
|
+
description: data.externalName || undefined,
|
|
1949
|
+
});
|
|
1950
|
+
if (Array.isArray(data.operations)) {
|
|
1951
|
+
for (const op of data.operations) {
|
|
1952
|
+
if (!op?.name)
|
|
1953
|
+
continue;
|
|
1954
|
+
insertOp.run(name, op.name, op.method || op.name, op.idempotent ? 1 : 0, model);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
catch (error) {
|
|
1959
|
+
log.warn(`Skipped service ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
indexServiceGroups(dirPath, model) {
|
|
1964
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1965
|
+
const insertMember = this.db.prepare(`
|
|
1966
|
+
INSERT INTO service_group_members (group_name, service_name, model)
|
|
1967
|
+
VALUES (?, ?, ?)
|
|
1968
|
+
`);
|
|
1969
|
+
for (const file of files) {
|
|
1970
|
+
try {
|
|
1971
|
+
const filePath = path.join(dirPath, file);
|
|
1972
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1973
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
1974
|
+
const name = data.name || path.basename(file, '.json');
|
|
1975
|
+
this.addSymbol({
|
|
1976
|
+
name,
|
|
1977
|
+
type: 'service-group',
|
|
1978
|
+
filePath: sourceFilePath,
|
|
1979
|
+
model,
|
|
1980
|
+
description: data.description || undefined,
|
|
1981
|
+
});
|
|
1982
|
+
if (Array.isArray(data.services)) {
|
|
1983
|
+
for (const svc of data.services) {
|
|
1984
|
+
if (!svc)
|
|
1985
|
+
continue;
|
|
1986
|
+
insertMember.run(name, svc, model);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
catch (error) {
|
|
1991
|
+
log.warn(`Skipped service-group ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
indexMaps(dirPath, model) {
|
|
1996
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
1997
|
+
const insertMapping = this.db.prepare(`
|
|
1998
|
+
INSERT INTO map_mappings (map_name, mapping_table, field_connections, model)
|
|
1999
|
+
VALUES (?, ?, ?, ?)
|
|
2000
|
+
`);
|
|
2001
|
+
for (const file of files) {
|
|
2002
|
+
try {
|
|
2003
|
+
const filePath = path.join(dirPath, file);
|
|
2004
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2005
|
+
const name = data.name || path.basename(file, '.json');
|
|
2006
|
+
this.addSymbol({
|
|
2007
|
+
name,
|
|
2008
|
+
type: 'map',
|
|
2009
|
+
filePath: data.sourcePath || filePath,
|
|
2010
|
+
model,
|
|
2011
|
+
extendsClass: data.extends || undefined,
|
|
2012
|
+
});
|
|
2013
|
+
if (Array.isArray(data.mappings)) {
|
|
2014
|
+
for (const m of data.mappings) {
|
|
2015
|
+
if (!m?.table)
|
|
2016
|
+
continue;
|
|
2017
|
+
insertMapping.run(name, m.table, m.fieldConnections || 0, model);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
catch (error) {
|
|
2022
|
+
log.warn(`Skipped map ${file}: ${error instanceof Error ? error.message : error}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
indexConfigurationKeys(dirPath, model) {
|
|
2027
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
2028
|
+
for (const file of files) {
|
|
2029
|
+
try {
|
|
2030
|
+
const filePath = path.join(dirPath, file);
|
|
2031
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2032
|
+
const name = data.name || path.basename(file, '.json');
|
|
2033
|
+
// signature carries the parent key so the gating tree is queryable from search.
|
|
2034
|
+
this.addSymbol({
|
|
2035
|
+
name,
|
|
2036
|
+
type: 'configuration-key',
|
|
2037
|
+
filePath: data.sourcePath || filePath,
|
|
2038
|
+
model,
|
|
2039
|
+
description: data.label || undefined,
|
|
2040
|
+
signature: data.parentKey || undefined,
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
catch (error) {
|
|
2044
|
+
log.warn(`Skipped configuration-key ${file}: ${error instanceof Error ? error.message : error}`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
indexLicenseCodes(dirPath, model) {
|
|
2049
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
2050
|
+
for (const file of files) {
|
|
2051
|
+
try {
|
|
2052
|
+
const filePath = path.join(dirPath, file);
|
|
2053
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2054
|
+
const name = data.name || path.basename(file, '.json');
|
|
2055
|
+
// signature: "Group/Type" compact descriptor for search hits.
|
|
2056
|
+
const sig = [data.group, data.type].filter(Boolean).join(' / ') || undefined;
|
|
2057
|
+
this.addSymbol({
|
|
2058
|
+
name,
|
|
2059
|
+
type: 'license-code',
|
|
2060
|
+
filePath: data.sourcePath || filePath,
|
|
2061
|
+
model,
|
|
2062
|
+
description: data.label || undefined,
|
|
2063
|
+
signature: sig,
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
catch (error) {
|
|
2067
|
+
log.warn(`Skipped license-code ${file}: ${error instanceof Error ? error.message : error}`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
indexSecurityPolicies(dirPath, model) {
|
|
2072
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
2073
|
+
const insertPolicy = this.db.prepare(`
|
|
2074
|
+
INSERT INTO security_policies
|
|
2075
|
+
(policy_name, primary_table, query_name, operation, constrained_table, label, model)
|
|
2076
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2077
|
+
`);
|
|
2078
|
+
for (const file of files) {
|
|
2079
|
+
try {
|
|
2080
|
+
const filePath = path.join(dirPath, file);
|
|
2081
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2082
|
+
const name = data.name || path.basename(file, '.json');
|
|
2083
|
+
this.addSymbol({
|
|
2084
|
+
name,
|
|
2085
|
+
type: 'security-policy',
|
|
2086
|
+
filePath: data.sourcePath || filePath,
|
|
2087
|
+
model,
|
|
2088
|
+
description: data.label || undefined,
|
|
2089
|
+
signature: data.primaryTable || undefined,
|
|
2090
|
+
});
|
|
2091
|
+
insertPolicy.run(name, data.primaryTable || null, data.query || null, data.operation || null, data.constrainedTable ? 1 : 0, data.label || null, model);
|
|
2092
|
+
}
|
|
2093
|
+
catch (error) {
|
|
2094
|
+
log.warn(`Skipped security-policy ${file}: ${error instanceof Error ? error.message : error}`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
indexMacros(dirPath, model) {
|
|
2099
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
2100
|
+
const insertDefine = this.db.prepare(`
|
|
2101
|
+
INSERT INTO macro_defines (macro_name, define_name, define_value, model)
|
|
2102
|
+
VALUES (?, ?, ?, ?)
|
|
2103
|
+
`);
|
|
2104
|
+
for (const file of files) {
|
|
2105
|
+
try {
|
|
2106
|
+
const filePath = path.join(dirPath, file);
|
|
2107
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2108
|
+
const name = data.name || path.basename(file, '.json');
|
|
2109
|
+
this.addSymbol({
|
|
2110
|
+
name,
|
|
2111
|
+
type: 'macro',
|
|
2112
|
+
filePath: data.sourcePath || filePath,
|
|
2113
|
+
model,
|
|
2114
|
+
});
|
|
2115
|
+
if (Array.isArray(data.defines)) {
|
|
2116
|
+
for (const d of data.defines) {
|
|
2117
|
+
if (!d?.name)
|
|
2118
|
+
continue;
|
|
2119
|
+
insertDefine.run(name, d.name, d.value ?? '', model);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
catch (error) {
|
|
2124
|
+
log.warn(`Skipped macro ${file}: ${error instanceof Error ? error.message : error}`);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
indexExtensions(dirPath, model, extensionType) {
|
|
2129
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
2130
|
+
const insertMeta = this.db.prepare(`
|
|
2131
|
+
INSERT OR REPLACE INTO extension_metadata
|
|
2132
|
+
(extension_name, extension_type, base_object_name, added_fields, added_methods,
|
|
2133
|
+
added_indexes, coc_methods, event_subscriptions, model)
|
|
2134
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2135
|
+
`);
|
|
2136
|
+
for (const file of files) {
|
|
2137
|
+
try {
|
|
2138
|
+
const filePath = path.join(dirPath, file);
|
|
2139
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2140
|
+
const sourceFilePath = data.sourcePath || filePath;
|
|
2141
|
+
const name = data.name || path.basename(file, '.json');
|
|
2142
|
+
const baseObjectName = data.baseObjectName || data.extends || '';
|
|
2143
|
+
this.addSymbol({
|
|
2144
|
+
name,
|
|
2145
|
+
type: extensionType,
|
|
2146
|
+
filePath: sourceFilePath,
|
|
2147
|
+
model,
|
|
2148
|
+
parentName: baseObjectName || undefined,
|
|
2149
|
+
extendsClass: baseObjectName || undefined,
|
|
2150
|
+
signature: baseObjectName || undefined,
|
|
2151
|
+
});
|
|
2152
|
+
insertMeta.run(name, extensionType, baseObjectName, data.addedFields ? JSON.stringify(data.addedFields) : null, data.addedMethods ? JSON.stringify(data.addedMethods) : null, data.addedIndexes ? JSON.stringify(data.addedIndexes) : null, data.cocMethods ? JSON.stringify(data.cocMethods) : null, data.eventSubscriptions ? JSON.stringify(data.eventSubscriptions) : null, model);
|
|
2153
|
+
}
|
|
2154
|
+
catch (error) {
|
|
2155
|
+
log.warn(`Skipped ${extensionType} ${file}: ${error instanceof Error ? error.message : error}`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
// Index freshness bookkeeping
|
|
2160
|
+
/** Record "the index was (re)built/updated now" — drives staleness detection. */
|
|
2161
|
+
touchLastIndexed() {
|
|
2162
|
+
try {
|
|
2163
|
+
this.db.prepare(`INSERT OR REPLACE INTO _index_meta (key, value) VALUES ('last_indexed_at', ?)`).run(new Date().toISOString());
|
|
2164
|
+
}
|
|
2165
|
+
catch {
|
|
2166
|
+
// Bookkeeping is best-effort
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
/** ISO timestamp of the last full or incremental index update, or null. */
|
|
2170
|
+
getLastIndexedAt() {
|
|
2171
|
+
try {
|
|
2172
|
+
const row = this.getReadDb().prepare(`SELECT value FROM _index_meta WHERE key = 'last_indexed_at'`).get();
|
|
2173
|
+
return row?.value ?? null;
|
|
2174
|
+
}
|
|
2175
|
+
catch {
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// Property statistics (data-driven BP rules)
|
|
2180
|
+
/**
|
|
2181
|
+
* Record one observation of a metadata property value.
|
|
2182
|
+
* Presence checks use the special values '(present)' / '(absent)'.
|
|
2183
|
+
*/
|
|
2184
|
+
recordPropertyStat(nodeType, property, value, model) {
|
|
2185
|
+
// Buffer observations in memory; flushed to DB in batch by flushPropertyStats()
|
|
2186
|
+
const key = `${nodeType}|${property}|${value}|${model}`;
|
|
2187
|
+
this.propStatBuffer.set(key, (this.propStatBuffer.get(key) ?? 0) + 1);
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Flush all buffered property_stats observations to the database in a single
|
|
2191
|
+
* batch. Call once at the end of each model's transaction. The buffer is
|
|
2192
|
+
* cleared after flushing so repeated calls are safe.
|
|
2193
|
+
*/
|
|
2194
|
+
flushPropertyStats() {
|
|
2195
|
+
if (this.propStatBuffer.size === 0)
|
|
2196
|
+
return;
|
|
2197
|
+
let stmt = this.stmtCache.get('flushPropertyStat');
|
|
2198
|
+
if (!stmt) {
|
|
2199
|
+
stmt = this.db.prepare(`
|
|
2200
|
+
INSERT INTO property_stats (node_type, property, value, model, count)
|
|
2201
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2202
|
+
ON CONFLICT(node_type, property, value, model) DO UPDATE SET count = count + excluded.count
|
|
2203
|
+
`);
|
|
2204
|
+
this.stmtCache.set('flushPropertyStat', stmt);
|
|
2205
|
+
}
|
|
2206
|
+
for (const [key, count] of this.propStatBuffer) {
|
|
2207
|
+
const [nodeType, property, value, model] = key.split('|');
|
|
2208
|
+
stmt.run(nodeType, property, value, model, count);
|
|
2209
|
+
}
|
|
2210
|
+
this.propStatBuffer.clear();
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Ratio of '(present)' observations for a property across all mined models.
|
|
2214
|
+
* Returns total=0 when no statistics exist (validate_xpp falls back to
|
|
2215
|
+
* static defaults in that case).
|
|
2216
|
+
*/
|
|
2217
|
+
getPropertyPresenceRatio(nodeType, property) {
|
|
2218
|
+
const rows = this.getReadDb().prepare(`SELECT value, SUM(count) AS c FROM property_stats
|
|
2219
|
+
WHERE node_type = ? AND property = ? GROUP BY value`).all(nodeType, property);
|
|
2220
|
+
let present = 0;
|
|
2221
|
+
let total = 0;
|
|
2222
|
+
for (const row of rows) {
|
|
2223
|
+
total += row.c;
|
|
2224
|
+
if (row.value === '(present)')
|
|
2225
|
+
present += row.c;
|
|
2226
|
+
}
|
|
2227
|
+
return { present, total, ratio: total > 0 ? present / total : 0 };
|
|
2228
|
+
}
|
|
2229
|
+
/** Most common values for a property, ordered by observation count. */
|
|
2230
|
+
getPropertyValueDistribution(nodeType, property, limit = 10) {
|
|
2231
|
+
return this.getReadDb().prepare(`SELECT value, SUM(count) AS count FROM property_stats
|
|
2232
|
+
WHERE node_type = ? AND property = ? AND value NOT IN ('(present)', '(absent)')
|
|
2233
|
+
GROUP BY value ORDER BY count DESC LIMIT ?`).all(nodeType, property, limit);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Mine property statistics from one parsed table JSON. Only standard
|
|
2237
|
+
* (Microsoft) models are mined — the stats answer "what does the standard
|
|
2238
|
+
* platform do", not "what did our customizations do".
|
|
2239
|
+
*/
|
|
2240
|
+
recordTablePropertyStats(tableData, model) {
|
|
2241
|
+
if (!isStandardModel(model))
|
|
2242
|
+
return;
|
|
2243
|
+
const presence = (v) => (v ? '(present)' : '(absent)');
|
|
2244
|
+
try {
|
|
2245
|
+
// xmlParser defaults label to the table name — same value means no real label
|
|
2246
|
+
const hasLabel = !!tableData.label && tableData.label !== tableData.name;
|
|
2247
|
+
this.recordPropertyStat('AxTable', 'Label', presence(hasLabel), model);
|
|
2248
|
+
this.recordPropertyStat('AxTable', 'TableGroup', tableData.tableGroup || '(absent)', model);
|
|
2249
|
+
this.recordPropertyStat('AxTable', 'PrimaryIndex', presence(tableData.primaryIndex), model);
|
|
2250
|
+
this.recordPropertyStat('AxTable', 'ClusteredIndex', presence(tableData.clusteredIndex), model);
|
|
2251
|
+
const indexes = Array.isArray(tableData.indexes) ? tableData.indexes : [];
|
|
2252
|
+
this.recordPropertyStat('AxTable', 'AlternateKeyIndex', presence(indexes.some((i) => i?.unique)), model);
|
|
2253
|
+
const fields = Array.isArray(tableData.fields) ? tableData.fields : [];
|
|
2254
|
+
for (const field of fields) {
|
|
2255
|
+
this.recordPropertyStat('AxTableField', 'ExtendedDataType', presence(field?.extendedDataType || field?.enumType), model);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
// Statistics are best-effort — never fail the indexing pass
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Get class methods for autocomplete
|
|
2264
|
+
*/
|
|
2265
|
+
getClassMethods(className) {
|
|
2266
|
+
let stmt = this.stmtCache.get('getClassMethods');
|
|
2267
|
+
if (!stmt) {
|
|
2268
|
+
stmt = this.db.prepare(`SELECT * FROM symbols WHERE parent_name = ? AND type = 'method' ORDER BY name`);
|
|
2269
|
+
this.stmtCache.set('getClassMethods', stmt);
|
|
2270
|
+
}
|
|
2271
|
+
return stmt.all(className).map(row => this.rowToSymbol(row));
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Get table fields for autocomplete
|
|
2275
|
+
*/
|
|
2276
|
+
getTableFields(tableName) {
|
|
2277
|
+
let stmt = this.stmtCache.get('getTableFields');
|
|
2278
|
+
if (!stmt) {
|
|
2279
|
+
stmt = this.db.prepare(`SELECT * FROM symbols WHERE parent_name = ? AND type = 'field' ORDER BY name`);
|
|
2280
|
+
this.stmtCache.set('getTableFields', stmt);
|
|
2281
|
+
}
|
|
2282
|
+
return stmt.all(tableName).map(row => this.rowToSymbol(row));
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Get completions for a class or table
|
|
2286
|
+
*/
|
|
2287
|
+
getCompletions(objectName, prefix) {
|
|
2288
|
+
// Single query instead of two separate calls for methods + fields
|
|
2289
|
+
let stmt = this.stmtCache.get('getCompletions');
|
|
2290
|
+
if (!stmt) {
|
|
2291
|
+
stmt = this.db.prepare(`SELECT name, type, signature FROM symbols
|
|
2292
|
+
WHERE parent_name = ? AND type IN ('method', 'field')
|
|
2293
|
+
ORDER BY type DESC, name` // methods before fields
|
|
2294
|
+
);
|
|
2295
|
+
this.stmtCache.set('getCompletions', stmt);
|
|
2296
|
+
}
|
|
2297
|
+
const allMembers = stmt.all(objectName);
|
|
2298
|
+
const filtered = prefix
|
|
2299
|
+
? allMembers.filter(m => m.name.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
2300
|
+
: allMembers;
|
|
2301
|
+
return filtered.map(m => ({
|
|
2302
|
+
label: m.name,
|
|
2303
|
+
kind: m.type === 'method' ? 'Method' : 'Field',
|
|
2304
|
+
detail: m.signature ?? undefined,
|
|
2305
|
+
documentation: undefined,
|
|
2306
|
+
}));
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Search custom extensions by prefix.
|
|
2310
|
+
*
|
|
2311
|
+
* Restricts results to symbol types whose names carry the `*_Extension` /
|
|
2312
|
+
* `*.<model>Extension` convention (class-extension, table-extension, etc.)
|
|
2313
|
+
* so that unrelated symbols sharing a substring don't leak into extension UI.
|
|
2314
|
+
*/
|
|
2315
|
+
searchCustomExtensions(query, prefix, limit = 20) {
|
|
2316
|
+
// Allow extension-shaped rows (regardless of whether the indexer set a
|
|
2317
|
+
// dedicated *-extension type) while also including explicit extension types.
|
|
2318
|
+
let sql = `
|
|
2319
|
+
SELECT *
|
|
2320
|
+
FROM symbols
|
|
2321
|
+
WHERE name LIKE ?
|
|
2322
|
+
AND (
|
|
2323
|
+
type IN (
|
|
2324
|
+
'class-extension','table-extension','form-extension','enum-extension',
|
|
2325
|
+
'edt-extension','view-extension','query-extension','data-entity-extension',
|
|
2326
|
+
'map-extension','menu-extension','security-role-extension','security-duty-extension',
|
|
2327
|
+
'menu-item-display-extension','menu-item-action-extension','menu-item-output-extension'
|
|
2328
|
+
)
|
|
2329
|
+
OR name LIKE '%_Extension'
|
|
2330
|
+
OR name LIKE '%.%Extension'
|
|
2331
|
+
)
|
|
2332
|
+
`;
|
|
2333
|
+
const params = [`%${query}%`];
|
|
2334
|
+
if (prefix) {
|
|
2335
|
+
sql += ` AND model LIKE ?`;
|
|
2336
|
+
params.push(`${prefix}%`);
|
|
2337
|
+
}
|
|
2338
|
+
sql += ` ORDER BY name LIMIT ?`;
|
|
2339
|
+
params.push(limit);
|
|
2340
|
+
const stmt = this.db.prepare(sql);
|
|
2341
|
+
const rows = stmt.all(...params);
|
|
2342
|
+
return rows.map(row => this.rowToSymbol(row));
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Get list of custom models (non-standard models)
|
|
2346
|
+
* Filters out Microsoft's standard D365 F&O models loaded from config
|
|
2347
|
+
*/
|
|
2348
|
+
getCustomModels() {
|
|
2349
|
+
const stmt = this.db.prepare(`
|
|
2350
|
+
SELECT DISTINCT model
|
|
2351
|
+
FROM symbols
|
|
2352
|
+
ORDER BY model
|
|
2353
|
+
`);
|
|
2354
|
+
const rows = stmt.all();
|
|
2355
|
+
return rows
|
|
2356
|
+
.map(row => row.model)
|
|
2357
|
+
.filter(model => !this.standardModels.includes(model));
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Analyze code patterns for a given scenario/domain
|
|
2361
|
+
*/
|
|
2362
|
+
analyzeCodePatterns(scenario, classPattern, limit = 20) {
|
|
2363
|
+
// Extract keywords from scenario for better search
|
|
2364
|
+
const keywords = scenario.toLowerCase()
|
|
2365
|
+
.split(/\s+/)
|
|
2366
|
+
.filter(w => w.length > 3 && !['with', 'which', 'will', 'that', 'this', 'from', 'have'].includes(w));
|
|
2367
|
+
// Build LIKE-only fallback SQL (always safe)
|
|
2368
|
+
const buildLikeSql = () => {
|
|
2369
|
+
const likeSql = keywords.length > 0
|
|
2370
|
+
? `SELECT DISTINCT s.* FROM symbols s WHERE s.type = 'class' AND (${keywords.map(() => 's.name LIKE ? OR s.tags LIKE ? OR s.description LIKE ?').join(' OR ')})`
|
|
2371
|
+
: `SELECT DISTINCT s.* FROM symbols s WHERE s.type = 'class' AND (s.name LIKE ? OR s.tags LIKE ? OR s.description LIKE ?)`;
|
|
2372
|
+
const likeParams = keywords.length > 0
|
|
2373
|
+
? keywords.flatMap(kw => [`%${kw}%`, `%${kw}%`, `%${kw}%`])
|
|
2374
|
+
: [`%${scenario}%`, `%${scenario}%`, `%${scenario}%`];
|
|
2375
|
+
return { likeSql, likeParams };
|
|
2376
|
+
};
|
|
2377
|
+
let classes;
|
|
2378
|
+
if (keywords.length > 0) {
|
|
2379
|
+
// Use FTS5 for better text search; double-quote terms to prevent FTS5 syntax errors
|
|
2380
|
+
// (e.g. keywords like "select" or ones containing ":" would break MATCH otherwise)
|
|
2381
|
+
const safeFtsTerms = keywords.map(kw => `"${kw.replace(/"/g, '')}"`);
|
|
2382
|
+
let sql = `
|
|
2383
|
+
SELECT DISTINCT s.*
|
|
2384
|
+
FROM symbols s
|
|
2385
|
+
WHERE s.type = 'class'
|
|
2386
|
+
AND (
|
|
2387
|
+
s.id IN (
|
|
2388
|
+
SELECT rowid FROM symbols_fts WHERE symbols_fts MATCH ?
|
|
2389
|
+
)
|
|
2390
|
+
${safeFtsTerms.slice(1).map(() => `
|
|
2391
|
+
OR s.id IN (
|
|
2392
|
+
SELECT rowid FROM symbols_fts WHERE symbols_fts MATCH ?
|
|
2393
|
+
)`).join('')}
|
|
2394
|
+
${keywords.map(() => 'OR s.name LIKE ? OR s.tags LIKE ?').join(' ')}
|
|
2395
|
+
)
|
|
2396
|
+
`;
|
|
2397
|
+
const params = [];
|
|
2398
|
+
params.push(...safeFtsTerms);
|
|
2399
|
+
for (const keyword of keywords) {
|
|
2400
|
+
params.push(`%${keyword}%`, `%${keyword}%`);
|
|
2401
|
+
}
|
|
2402
|
+
if (classPattern) {
|
|
2403
|
+
sql += ` AND s.name LIKE ?`;
|
|
2404
|
+
params.push(`%${classPattern}%`);
|
|
2405
|
+
}
|
|
2406
|
+
sql += ` LIMIT ?`;
|
|
2407
|
+
params.push(limit);
|
|
2408
|
+
try {
|
|
2409
|
+
classes = this.db.prepare(sql).all(...params);
|
|
2410
|
+
}
|
|
2411
|
+
catch {
|
|
2412
|
+
// FTS5 query failed (e.g. reserved word, missing virtual table) — fall back to LIKE
|
|
2413
|
+
const { likeSql, likeParams } = buildLikeSql();
|
|
2414
|
+
let fallback = likeSql;
|
|
2415
|
+
const fallbackParams = [...likeParams];
|
|
2416
|
+
if (classPattern) {
|
|
2417
|
+
fallback += ` AND s.name LIKE ?`;
|
|
2418
|
+
fallbackParams.push(`%${classPattern}%`);
|
|
2419
|
+
}
|
|
2420
|
+
fallback += ` LIMIT ?`;
|
|
2421
|
+
fallbackParams.push(limit);
|
|
2422
|
+
classes = this.db.prepare(fallback).all(...fallbackParams);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
else {
|
|
2426
|
+
// Fallback to simple search
|
|
2427
|
+
const { likeSql, likeParams } = buildLikeSql();
|
|
2428
|
+
let fallback = likeSql;
|
|
2429
|
+
const fallbackParams = [...likeParams];
|
|
2430
|
+
if (classPattern) {
|
|
2431
|
+
fallback += ` AND s.name LIKE ?`;
|
|
2432
|
+
fallbackParams.push(`%${classPattern}%`);
|
|
2433
|
+
}
|
|
2434
|
+
fallback += ` LIMIT ?`;
|
|
2435
|
+
fallbackParams.push(limit);
|
|
2436
|
+
classes = this.db.prepare(fallback).all(...fallbackParams);
|
|
2437
|
+
}
|
|
2438
|
+
// Analyze common patterns
|
|
2439
|
+
const methodFrequency = {};
|
|
2440
|
+
const dependencyFrequency = {};
|
|
2441
|
+
const exampleClasses = [];
|
|
2442
|
+
// Collect example class names and count dependency frequencies (data already in classes)
|
|
2443
|
+
for (const cls of classes) {
|
|
2444
|
+
exampleClasses.push(cls.name);
|
|
2445
|
+
if (cls.used_types) {
|
|
2446
|
+
for (const rawType of cls.used_types.split(',')) {
|
|
2447
|
+
const cleaned = rawType.trim();
|
|
2448
|
+
if (cleaned)
|
|
2449
|
+
dependencyFrequency[cleaned] = (dependencyFrequency[cleaned] || 0) + 1;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
// Single bulk query instead of N+1 (one getClassMethods() call per class)
|
|
2454
|
+
if (classes.length > 0) {
|
|
2455
|
+
const classNames = classes.map((c) => c.name);
|
|
2456
|
+
const placeholders = classNames.map(() => '?').join(',');
|
|
2457
|
+
const allMethods = this.db.prepare(`SELECT name FROM symbols WHERE type = 'method' AND parent_name IN (${placeholders})`).all(...classNames);
|
|
2458
|
+
for (const method of allMethods) {
|
|
2459
|
+
methodFrequency[method.name] = (methodFrequency[method.name] || 0) + 1;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
// Get top methods and dependencies
|
|
2463
|
+
const commonMethods = Object.entries(methodFrequency)
|
|
2464
|
+
.sort(([, a], [, b]) => b - a)
|
|
2465
|
+
.slice(0, 20)
|
|
2466
|
+
.map(([name, count]) => ({ name, frequency: count }));
|
|
2467
|
+
const commonDependencies = Object.entries(dependencyFrequency)
|
|
2468
|
+
.sort(([, a], [, b]) => b - a)
|
|
2469
|
+
.slice(0, 15)
|
|
2470
|
+
.map(([name, count]) => ({ name, frequency: count }));
|
|
2471
|
+
return {
|
|
2472
|
+
scenario,
|
|
2473
|
+
totalMatches: classes.length,
|
|
2474
|
+
commonMethods,
|
|
2475
|
+
commonDependencies,
|
|
2476
|
+
exampleClasses: exampleClasses.slice(0, 10),
|
|
2477
|
+
patterns: this.detectPatternTypes(classes)
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Detect pattern types from set of classes
|
|
2482
|
+
*/
|
|
2483
|
+
detectPatternTypes(classes) {
|
|
2484
|
+
const patterns = {};
|
|
2485
|
+
for (const cls of classes) {
|
|
2486
|
+
const name = cls.name;
|
|
2487
|
+
let patternType = 'Unknown';
|
|
2488
|
+
if (name.endsWith('Helper'))
|
|
2489
|
+
patternType = 'Helper';
|
|
2490
|
+
else if (name.endsWith('Service'))
|
|
2491
|
+
patternType = 'Service';
|
|
2492
|
+
else if (name.endsWith('Controller'))
|
|
2493
|
+
patternType = 'Controller';
|
|
2494
|
+
else if (name.endsWith('Handler'))
|
|
2495
|
+
patternType = 'Handler';
|
|
2496
|
+
else if (name.endsWith('Repository') || name.endsWith('Repo'))
|
|
2497
|
+
patternType = 'Repository';
|
|
2498
|
+
else if (name.endsWith('Manager'))
|
|
2499
|
+
patternType = 'Manager';
|
|
2500
|
+
else if (name.endsWith('Factory'))
|
|
2501
|
+
patternType = 'Factory';
|
|
2502
|
+
else if (name.endsWith('Builder'))
|
|
2503
|
+
patternType = 'Builder';
|
|
2504
|
+
else if (name.endsWith('Processor'))
|
|
2505
|
+
patternType = 'Processor';
|
|
2506
|
+
else if (name.endsWith('Validator'))
|
|
2507
|
+
patternType = 'Validator';
|
|
2508
|
+
if (!patterns[patternType]) {
|
|
2509
|
+
patterns[patternType] = { count: 0, examples: [] };
|
|
2510
|
+
}
|
|
2511
|
+
patterns[patternType].count++;
|
|
2512
|
+
if (patterns[patternType].examples.length < 5) {
|
|
2513
|
+
patterns[patternType].examples.push(name);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
return Object.entries(patterns).map(([type, data]) => ({
|
|
2517
|
+
patternType: type,
|
|
2518
|
+
count: data.count,
|
|
2519
|
+
examples: data.examples
|
|
2520
|
+
}));
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Find similar methods based on name and context
|
|
2524
|
+
*/
|
|
2525
|
+
findSimilarMethods(methodName, _contextClass, limit = 10) {
|
|
2526
|
+
// Try exact-name match first (fast) — falls back to LIKE only when nothing found
|
|
2527
|
+
const stmtKeyExact = 'findSimilarMethods:exact';
|
|
2528
|
+
let stmtExact = this.stmtCache.get(stmtKeyExact);
|
|
2529
|
+
if (!stmtExact) {
|
|
2530
|
+
stmtExact = this.db.prepare(`
|
|
2531
|
+
SELECT s.name, s.parent_name, s.signature, s.source_snippet, s.complexity, s.tags,
|
|
2532
|
+
parent.pattern_type
|
|
2533
|
+
FROM symbols s
|
|
2534
|
+
LEFT JOIN symbols parent ON s.parent_name = parent.name AND parent.type = 'class'
|
|
2535
|
+
WHERE s.type = 'method' AND s.name = ?
|
|
2536
|
+
ORDER BY s.complexity ASC
|
|
2537
|
+
LIMIT ?
|
|
2538
|
+
`);
|
|
2539
|
+
this.stmtCache.set(stmtKeyExact, stmtExact);
|
|
2540
|
+
}
|
|
2541
|
+
let methods = stmtExact.all(methodName, limit);
|
|
2542
|
+
// Fall back to suffix/prefix LIKE only when exact produced nothing, but cap at limit
|
|
2543
|
+
if (methods.length === 0) {
|
|
2544
|
+
let stmtLike = this.stmtCache.get('findSimilarMethods:like');
|
|
2545
|
+
if (!stmtLike) {
|
|
2546
|
+
stmtLike = this.db.prepare(`
|
|
2547
|
+
SELECT s.name, s.parent_name, s.signature, s.source_snippet, s.complexity, s.tags,
|
|
2548
|
+
parent.pattern_type
|
|
2549
|
+
FROM symbols s
|
|
2550
|
+
LEFT JOIN symbols parent ON s.parent_name = parent.name AND parent.type = 'class'
|
|
2551
|
+
WHERE s.type = 'method' AND s.name LIKE ?
|
|
2552
|
+
ORDER BY s.complexity ASC, s.name
|
|
2553
|
+
LIMIT ?
|
|
2554
|
+
`);
|
|
2555
|
+
this.stmtCache.set('findSimilarMethods:like', stmtLike);
|
|
2556
|
+
}
|
|
2557
|
+
methods = stmtLike.all(`%${methodName}%`, limit);
|
|
2558
|
+
}
|
|
2559
|
+
return methods.map(m => ({
|
|
2560
|
+
className: m.class_name || m.parent_name,
|
|
2561
|
+
methodName: m.name,
|
|
2562
|
+
signature: m.signature,
|
|
2563
|
+
sourceSnippet: m.source_snippet,
|
|
2564
|
+
complexity: m.complexity,
|
|
2565
|
+
tags: m.tags?.split(',').filter(Boolean) || [],
|
|
2566
|
+
patternType: m.pattern_type
|
|
2567
|
+
}));
|
|
2568
|
+
}
|
|
2569
|
+
getApiUsagePatterns(className) {
|
|
2570
|
+
// Find all methods that reference this class in their used_types.
|
|
2571
|
+
// Cap at 20 rows — fetching source_snippet for 50+ rows on a 584K-row table causes timeout.
|
|
2572
|
+
let stmt = this.stmtCache.get('getApiUsagePatterns');
|
|
2573
|
+
if (!stmt) {
|
|
2574
|
+
stmt = this.db.prepare(`SELECT name, parent_name, method_calls, source_snippet
|
|
2575
|
+
FROM symbols
|
|
2576
|
+
WHERE type = 'method' AND used_types LIKE ?
|
|
2577
|
+
LIMIT 20`);
|
|
2578
|
+
this.stmtCache.set('getApiUsagePatterns', stmt);
|
|
2579
|
+
}
|
|
2580
|
+
const methods = stmt.all(`%${className}%`);
|
|
2581
|
+
if (methods.length === 0) {
|
|
2582
|
+
return [];
|
|
2583
|
+
}
|
|
2584
|
+
const methodCallPatterns = {};
|
|
2585
|
+
const initPatterns = [];
|
|
2586
|
+
for (const method of methods) {
|
|
2587
|
+
if (method.method_calls) {
|
|
2588
|
+
for (const call of method.method_calls.split(',')) {
|
|
2589
|
+
const c = call.trim();
|
|
2590
|
+
if (c)
|
|
2591
|
+
methodCallPatterns[c] = (methodCallPatterns[c] || 0) + 1;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
// Collect initialization snippets
|
|
2595
|
+
if (method.source_snippet && method.source_snippet.includes('new ' + className)) {
|
|
2596
|
+
const snippet = method.source_snippet.split('\n').slice(0, 5).join('\n');
|
|
2597
|
+
if (!initPatterns.includes(snippet))
|
|
2598
|
+
initPatterns.push(snippet);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
const commonMethodCalls = Object.entries(methodCallPatterns)
|
|
2602
|
+
.sort(([, a], [, b]) => b - a)
|
|
2603
|
+
.slice(0, 10);
|
|
2604
|
+
// Return an array so formatPatterns() can iterate with .length and index access
|
|
2605
|
+
return [{
|
|
2606
|
+
patternType: 'General Usage',
|
|
2607
|
+
usageCount: methods.length,
|
|
2608
|
+
classes: methods.map((m) => m.parent_name).filter(Boolean).slice(0, 10),
|
|
2609
|
+
initialization: initPatterns.slice(0, 3),
|
|
2610
|
+
methodSequence: commonMethodCalls.map(([name, count]) => `${name} // called ${count}×`),
|
|
2611
|
+
relatedApis: commonMethodCalls.slice(0, 5).map(([name]) => name),
|
|
2612
|
+
}];
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Suggest missing methods for a class based on pattern analysis
|
|
2616
|
+
*/
|
|
2617
|
+
suggestMissingMethods(className) {
|
|
2618
|
+
const classSymbol = this.getSymbolByName(className, 'class');
|
|
2619
|
+
if (!classSymbol)
|
|
2620
|
+
return [];
|
|
2621
|
+
// Get existing methods
|
|
2622
|
+
const existingMethods = this.getClassMethods(className);
|
|
2623
|
+
const existingMethodNames = new Set(existingMethods.map(m => m.name));
|
|
2624
|
+
// Detect pattern type
|
|
2625
|
+
let patternType = classSymbol.patternType || 'Unknown';
|
|
2626
|
+
if (!patternType || patternType === 'Unknown') {
|
|
2627
|
+
if (className.endsWith('Helper'))
|
|
2628
|
+
patternType = 'Helper';
|
|
2629
|
+
else if (className.endsWith('Service'))
|
|
2630
|
+
patternType = 'Service';
|
|
2631
|
+
else if (className.endsWith('Controller'))
|
|
2632
|
+
patternType = 'Controller';
|
|
2633
|
+
}
|
|
2634
|
+
// Find similar classes with same pattern
|
|
2635
|
+
const sql = `
|
|
2636
|
+
SELECT DISTINCT parent_name
|
|
2637
|
+
FROM symbols
|
|
2638
|
+
WHERE type = 'method'
|
|
2639
|
+
AND parent_name LIKE ?
|
|
2640
|
+
AND parent_name != ?
|
|
2641
|
+
LIMIT 20
|
|
2642
|
+
`;
|
|
2643
|
+
const stmt = this.db.prepare(sql);
|
|
2644
|
+
const similarClasses = stmt.all(`%${patternType}`, className);
|
|
2645
|
+
// Single GROUP BY query instead of N+1 getClassMethods() calls per similar class
|
|
2646
|
+
const methodFrequency = {};
|
|
2647
|
+
if (similarClasses.length > 0) {
|
|
2648
|
+
const classNames = similarClasses.map((r) => r.parent_name);
|
|
2649
|
+
const placeholders = classNames.map(() => '?').join(',');
|
|
2650
|
+
const methodCounts = this.db.prepare(`SELECT name, COUNT(DISTINCT parent_name) AS class_count
|
|
2651
|
+
FROM symbols
|
|
2652
|
+
WHERE type = 'method' AND parent_name IN (${placeholders})
|
|
2653
|
+
GROUP BY name
|
|
2654
|
+
ORDER BY class_count DESC
|
|
2655
|
+
LIMIT 50`).all(...classNames);
|
|
2656
|
+
for (const row of methodCounts) {
|
|
2657
|
+
if (!existingMethodNames.has(row.name)) {
|
|
2658
|
+
methodFrequency[row.name] = row.class_count;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
// Return top missing methods
|
|
2663
|
+
return Object.entries(methodFrequency)
|
|
2664
|
+
.sort(([, a], [, b]) => b - a)
|
|
2665
|
+
.slice(0, 10)
|
|
2666
|
+
.map(([name, count]) => ({
|
|
2667
|
+
methodName: name,
|
|
2668
|
+
frequency: count,
|
|
2669
|
+
totalClasses: similarClasses.length,
|
|
2670
|
+
percentage: Math.round((count / similarClasses.length) * 100)
|
|
2671
|
+
}));
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Clear all symbols
|
|
2675
|
+
*/
|
|
2676
|
+
clear() {
|
|
2677
|
+
this.invalidateSymbolCounts();
|
|
2678
|
+
this.db.exec('DELETE FROM symbols');
|
|
2679
|
+
this.db.exec('DELETE FROM table_relations');
|
|
2680
|
+
this.db.exec('DELETE FROM form_datasources');
|
|
2681
|
+
this.db.exec('DELETE FROM form_patterns');
|
|
2682
|
+
this.db.exec('DELETE FROM edt_metadata');
|
|
2683
|
+
this.db.exec('DELETE FROM security_privilege_entries');
|
|
2684
|
+
this.db.exec('DELETE FROM security_duty_privileges');
|
|
2685
|
+
this.db.exec('DELETE FROM security_role_duties');
|
|
2686
|
+
this.db.exec('DELETE FROM menu_item_targets');
|
|
2687
|
+
this.db.exec('DELETE FROM extension_metadata');
|
|
2688
|
+
this.db.exec('DELETE FROM service_operations');
|
|
2689
|
+
this.db.exec('DELETE FROM service_group_members');
|
|
2690
|
+
this.db.exec('DELETE FROM map_mappings');
|
|
2691
|
+
this.db.exec('DELETE FROM security_policies');
|
|
2692
|
+
this.db.exec('DELETE FROM macro_defines');
|
|
2693
|
+
this.db.exec('DELETE FROM property_stats');
|
|
2694
|
+
this.vacuum();
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Clear symbols for specific models
|
|
2698
|
+
* @param modelNames - Array of model names to clear
|
|
2699
|
+
* @param shouldVacuum - Whether to run VACUUM after deletion (default: false for better incremental build performance)
|
|
2700
|
+
*/
|
|
2701
|
+
clearModels(modelNames, shouldVacuum = false) {
|
|
2702
|
+
if (modelNames.length === 0)
|
|
2703
|
+
return;
|
|
2704
|
+
// NOTE: `placeholders` is built from '?' characters only — it never contains
|
|
2705
|
+
// user-supplied data. The actual model name values flow through SQLite's
|
|
2706
|
+
// parameterized binding via .run(...modelNames), so there is no injection risk.
|
|
2707
|
+
const placeholders = modelNames.map(() => '?').join(',');
|
|
2708
|
+
this.invalidateSymbolCounts();
|
|
2709
|
+
// Wrap all deletes in a single transaction so the database never ends up
|
|
2710
|
+
// in a partially-cleared state if one statement fails mid-way.
|
|
2711
|
+
const deleteAll = this.db.transaction(() => {
|
|
2712
|
+
this.db.prepare(`DELETE FROM symbols WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2713
|
+
this.db.prepare(`DELETE FROM table_relations WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2714
|
+
this.db.prepare(`DELETE FROM form_datasources WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2715
|
+
this.db.prepare(`DELETE FROM form_patterns WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2716
|
+
this.db.prepare(`DELETE FROM edt_metadata WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2717
|
+
this.db.prepare(`DELETE FROM security_privilege_entries WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2718
|
+
this.db.prepare(`DELETE FROM security_duty_privileges WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2719
|
+
this.db.prepare(`DELETE FROM security_role_duties WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2720
|
+
this.db.prepare(`DELETE FROM menu_item_targets WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2721
|
+
this.db.prepare(`DELETE FROM extension_metadata WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2722
|
+
this.db.prepare(`DELETE FROM service_operations WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2723
|
+
this.db.prepare(`DELETE FROM service_group_members WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2724
|
+
this.db.prepare(`DELETE FROM map_mappings WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2725
|
+
this.db.prepare(`DELETE FROM security_policies WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2726
|
+
this.db.prepare(`DELETE FROM macro_defines WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2727
|
+
this.db.prepare(`DELETE FROM property_stats WHERE model IN (${placeholders})`).run(...modelNames);
|
|
2728
|
+
});
|
|
2729
|
+
deleteAll();
|
|
2730
|
+
log.step(`Cleared symbols for models: ${modelNames.join(', ')}`);
|
|
2731
|
+
if (shouldVacuum) {
|
|
2732
|
+
log.step('Running VACUUM to optimize database...');
|
|
2733
|
+
this.vacuum();
|
|
2734
|
+
log.ok('VACUUM completed');
|
|
2735
|
+
}
|
|
2736
|
+
else {
|
|
2737
|
+
log.info('Skipping VACUUM for faster incremental build');
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Vacuum the database to reclaim space after deletions
|
|
2742
|
+
*/
|
|
2743
|
+
vacuum() {
|
|
2744
|
+
this.db.exec('VACUUM');
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Get candidate symbol names for fuzzy matching ("did you mean" suggestions).
|
|
2748
|
+
*
|
|
2749
|
+
* When a query is given, candidates are anchored to it: names sharing the
|
|
2750
|
+
* query's leading characters plus names containing its root term (avoids
|
|
2751
|
+
* always sampling the same alphabetical slice of a 580K-symbol index).
|
|
2752
|
+
* Without a query, falls back to the first 5000 names alphabetically.
|
|
2753
|
+
*/
|
|
2754
|
+
getAllSymbolNames(query, limit = 2000) {
|
|
2755
|
+
const trimmed = query?.trim();
|
|
2756
|
+
if (!trimmed) {
|
|
2757
|
+
const stmt = this.db.prepare(`
|
|
2758
|
+
SELECT DISTINCT name
|
|
2759
|
+
FROM symbols
|
|
2760
|
+
ORDER BY name
|
|
2761
|
+
LIMIT 5000
|
|
2762
|
+
`);
|
|
2763
|
+
const names = [];
|
|
2764
|
+
for (const row of stmt.iterate()) {
|
|
2765
|
+
names.push(row.name);
|
|
2766
|
+
}
|
|
2767
|
+
return names;
|
|
2768
|
+
}
|
|
2769
|
+
const escapeLike = (value) => value.replace(/\\/g, '\\\\').replace(/[%_]/g, '\\$&');
|
|
2770
|
+
// Typo distance is usually in the tail of the name, so same-prefix names
|
|
2771
|
+
// are the best candidate pool; contains-matches on the root term catch
|
|
2772
|
+
// wrong-prefix typos (e.g. "CusTable" → "CustTable").
|
|
2773
|
+
const half = Math.max(1, Math.floor(limit / 2));
|
|
2774
|
+
const prefix = escapeLike(trimmed.slice(0, 2));
|
|
2775
|
+
const root = escapeLike(trimmed.slice(0, Math.max(3, Math.ceil(trimmed.length / 2))));
|
|
2776
|
+
const names = new Set();
|
|
2777
|
+
const db = this.getReadDb();
|
|
2778
|
+
try {
|
|
2779
|
+
const prefixStmt = this.getReadStmt(db, 'suggest_prefix', () => `SELECT DISTINCT name FROM symbols WHERE name LIKE ? ESCAPE '\\' LIMIT ?`);
|
|
2780
|
+
for (const row of prefixStmt.iterate(`${prefix}%`, half)) {
|
|
2781
|
+
names.add(row.name);
|
|
2782
|
+
}
|
|
2783
|
+
const containsStmt = this.getReadStmt(db, 'suggest_contains', () => `SELECT DISTINCT name FROM symbols WHERE name LIKE ? ESCAPE '\\' LIMIT ?`);
|
|
2784
|
+
for (const row of containsStmt.iterate(`%${root}%`, half)) {
|
|
2785
|
+
names.add(row.name);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
catch {
|
|
2789
|
+
// Best-effort — suggestions are advisory, never fail the search
|
|
2790
|
+
}
|
|
2791
|
+
return [...names];
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Get symbols grouped by term (for relationship analysis)
|
|
2795
|
+
* Returns a map of term -> symbols with that term
|
|
2796
|
+
* Uses iterator to avoid loading all symbols into memory at once
|
|
2797
|
+
*/
|
|
2798
|
+
getSymbolsByTerm() {
|
|
2799
|
+
const stmt = this.db.prepare(`
|
|
2800
|
+
SELECT *
|
|
2801
|
+
FROM symbols
|
|
2802
|
+
WHERE used_types IS NOT NULL
|
|
2803
|
+
OR method_calls IS NOT NULL
|
|
2804
|
+
OR related_methods IS NOT NULL
|
|
2805
|
+
ORDER BY name
|
|
2806
|
+
LIMIT 3000
|
|
2807
|
+
`);
|
|
2808
|
+
const symbolsByTerm = new Map();
|
|
2809
|
+
for (const row of stmt.iterate()) {
|
|
2810
|
+
const symbol = this.rowToSymbol(row);
|
|
2811
|
+
const termLower = symbol.name.toLowerCase();
|
|
2812
|
+
if (!symbolsByTerm.has(termLower)) {
|
|
2813
|
+
symbolsByTerm.set(termLower, []);
|
|
2814
|
+
}
|
|
2815
|
+
symbolsByTerm.get(termLower).push(symbol);
|
|
2816
|
+
}
|
|
2817
|
+
return symbolsByTerm;
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Get all symbols for relationship analysis
|
|
2821
|
+
* Used to build term relationship graph
|
|
2822
|
+
* Uses iterator to avoid memory exhaustion on large datasets
|
|
2823
|
+
*/
|
|
2824
|
+
getAllSymbolsForAnalysis() {
|
|
2825
|
+
const stmt = this.db.prepare(`
|
|
2826
|
+
SELECT *
|
|
2827
|
+
FROM symbols
|
|
2828
|
+
WHERE used_types IS NOT NULL
|
|
2829
|
+
OR method_calls IS NOT NULL
|
|
2830
|
+
OR related_methods IS NOT NULL
|
|
2831
|
+
OR parent_name IS NOT NULL
|
|
2832
|
+
OR extends_class IS NOT NULL
|
|
2833
|
+
LIMIT 2000
|
|
2834
|
+
`);
|
|
2835
|
+
const symbols = [];
|
|
2836
|
+
for (const row of stmt.iterate()) {
|
|
2837
|
+
symbols.push(this.rowToSymbol(row));
|
|
2838
|
+
}
|
|
2839
|
+
return symbols;
|
|
2840
|
+
}
|
|
2841
|
+
/**
|
|
2842
|
+
* Close the database connection and release all pooled resources: the
|
|
2843
|
+
* prepared-statement cache, writer + read pool, labels DB + its read pool,
|
|
2844
|
+
* and any pending debounced labels FTS rebuild timer.
|
|
2845
|
+
*/
|
|
2846
|
+
close() {
|
|
2847
|
+
// Flush any debounced labels FTS rebuild to avoid losing writes on shutdown.
|
|
2848
|
+
if (this._labelsFtsTimer) {
|
|
2849
|
+
clearTimeout(this._labelsFtsTimer);
|
|
2850
|
+
this._labelsFtsTimer = null;
|
|
2851
|
+
}
|
|
2852
|
+
// Drain read pools first — writer close will fail on WAL if readers hold a lock.
|
|
2853
|
+
this.closeReadPool();
|
|
2854
|
+
this.stmtCache.clear();
|
|
2855
|
+
try {
|
|
2856
|
+
this.db.close();
|
|
2857
|
+
}
|
|
2858
|
+
catch { /* ignore */ }
|
|
2859
|
+
try {
|
|
2860
|
+
this.labelsDb.close();
|
|
2861
|
+
}
|
|
2862
|
+
catch { /* ignore */ }
|
|
2863
|
+
}
|
|
2864
|
+
// Label methods
|
|
2865
|
+
/**
|
|
2866
|
+
* Add (or replace) a label entry in the index.
|
|
2867
|
+
* Labels live in the separate `labelsDb` connection — NOT in the main symbols DB.
|
|
2868
|
+
* The stmtCache is shared across connections so the cache key is namespaced to avoid
|
|
2869
|
+
* accidentally reusing a statement prepared against a different DB handle.
|
|
2870
|
+
*/
|
|
2871
|
+
addLabel(entry) {
|
|
2872
|
+
let stmt = this.stmtCache.get('labels::addLabel');
|
|
2873
|
+
if (!stmt) {
|
|
2874
|
+
stmt = this.labelsDb.prepare(`
|
|
2875
|
+
INSERT OR REPLACE INTO labels (label_id, label_file_id, model, language, text, comment, file_path)
|
|
2876
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2877
|
+
`);
|
|
2878
|
+
this.stmtCache.set('labels::addLabel', stmt);
|
|
2879
|
+
}
|
|
2880
|
+
stmt.run(entry.labelId, entry.labelFileId, entry.model, entry.language, entry.text, entry.comment ?? null, entry.filePath);
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Bulk-insert labels (drops FTS triggers for speed).
|
|
2884
|
+
* Pass `{ skipFtsRebuild: true }` when indexing many models sequentially;
|
|
2885
|
+
* the caller must then invoke `rebuildLabelsFts()` once after all models are done.
|
|
2886
|
+
*/
|
|
2887
|
+
bulkAddLabels(entries, opts) {
|
|
2888
|
+
// Disable FTS triggers during bulk insert
|
|
2889
|
+
this.labelsDb.exec(`DROP TRIGGER IF EXISTS labels_ai`);
|
|
2890
|
+
this.labelsDb.exec(`DROP TRIGGER IF EXISTS labels_ad`);
|
|
2891
|
+
this.labelsDb.exec(`DROP TRIGGER IF EXISTS labels_au`);
|
|
2892
|
+
const insert = this.labelsDb.prepare(`
|
|
2893
|
+
INSERT OR REPLACE INTO labels (label_id, label_file_id, model, language, text, comment, file_path)
|
|
2894
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2895
|
+
`);
|
|
2896
|
+
const insertMany = this.labelsDb.transaction((rows) => {
|
|
2897
|
+
for (const e of rows) {
|
|
2898
|
+
insert.run(e.labelId, e.labelFileId, e.model, e.language, e.text, e.comment ?? null, e.filePath);
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
insertMany(entries);
|
|
2902
|
+
// Rebuild FTS unless the caller will do a single rebuild after all batches
|
|
2903
|
+
if (!opts?.skipFtsRebuild) {
|
|
2904
|
+
this.rebuildLabelsFts();
|
|
2905
|
+
}
|
|
2906
|
+
// Re-create triggers (en-US only to keep FTS compact)
|
|
2907
|
+
this.labelsDb.exec(`
|
|
2908
|
+
CREATE TRIGGER IF NOT EXISTS labels_ai AFTER INSERT ON labels WHEN new.language = 'en-US' BEGIN
|
|
2909
|
+
INSERT INTO labels_fts(rowid, label_id, text, comment)
|
|
2910
|
+
VALUES (new.id, new.label_id, new.text, new.comment);
|
|
2911
|
+
END;
|
|
2912
|
+
CREATE TRIGGER IF NOT EXISTS labels_ad AFTER DELETE ON labels WHEN old.language = 'en-US' BEGIN
|
|
2913
|
+
INSERT INTO labels_fts(labels_fts, rowid, label_id, text, comment)
|
|
2914
|
+
VALUES ('delete', old.id, old.label_id, old.text, old.comment);
|
|
2915
|
+
END;
|
|
2916
|
+
CREATE TRIGGER IF NOT EXISTS labels_au AFTER UPDATE ON labels WHEN old.language = 'en-US' OR new.language = 'en-US' BEGIN
|
|
2917
|
+
INSERT INTO labels_fts(labels_fts, rowid, label_id, text, comment)
|
|
2918
|
+
VALUES ('delete', old.id, old.label_id, old.text, old.comment);
|
|
2919
|
+
INSERT INTO labels_fts(rowid, label_id, text, comment)
|
|
2920
|
+
VALUES (new.id, new.label_id, new.text, new.comment);
|
|
2921
|
+
END;
|
|
2922
|
+
`);
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Rebuild the FTS index for labels from scratch.
|
|
2926
|
+
* Only indexes en-US rows — the primary search language — keeping the
|
|
2927
|
+
* index ~(N_languages)x smaller compared to indexing all translations.
|
|
2928
|
+
*/
|
|
2929
|
+
rebuildLabelsFts() {
|
|
2930
|
+
// Clear existing FTS index
|
|
2931
|
+
this.labelsDb.exec(`INSERT INTO labels_fts(labels_fts) VALUES('delete-all')`);
|
|
2932
|
+
// Re-populate with en-US rows only (case-insensitive: Microsoft packages store
|
|
2933
|
+
// locale as 'en-us' from Linux-unzipped directory names, custom packages use 'en-US')
|
|
2934
|
+
this.labelsDb.exec(`
|
|
2935
|
+
INSERT INTO labels_fts(rowid, label_id, text, comment)
|
|
2936
|
+
SELECT id, label_id, text, comment FROM labels WHERE LOWER(language) = 'en-us'
|
|
2937
|
+
`);
|
|
2938
|
+
}
|
|
2939
|
+
// Debounced labels FTS rebuild
|
|
2940
|
+
_labelsFtsTimer = null;
|
|
2941
|
+
static LABELS_FTS_SETTLE_MS = 300;
|
|
2942
|
+
/**
|
|
2943
|
+
* Schedule a debounced labels FTS rebuild.
|
|
2944
|
+
* Multiple rapid create_label calls defer the expensive rebuild to ~300ms
|
|
2945
|
+
* after the last insertion, so a batch of 5 labels triggers only 1 rebuild.
|
|
2946
|
+
*/
|
|
2947
|
+
scheduleLabelsFtsRebuild() {
|
|
2948
|
+
if (this._labelsFtsTimer)
|
|
2949
|
+
clearTimeout(this._labelsFtsTimer);
|
|
2950
|
+
this._labelsFtsTimer = setTimeout(() => {
|
|
2951
|
+
this._labelsFtsTimer = null;
|
|
2952
|
+
try {
|
|
2953
|
+
this.rebuildLabelsFts();
|
|
2954
|
+
console.error('[SymbolIndex] Debounced labels FTS rebuild complete');
|
|
2955
|
+
}
|
|
2956
|
+
catch (e) {
|
|
2957
|
+
console.error(`[SymbolIndex] Debounced labels FTS rebuild failed: ${e}`);
|
|
2958
|
+
}
|
|
2959
|
+
}, XppSymbolIndex.LABELS_FTS_SETTLE_MS);
|
|
2960
|
+
}
|
|
2961
|
+
/** Flush any pending labels FTS rebuild immediately (for tests / shutdown). */
|
|
2962
|
+
flushLabelsFtsRebuild() {
|
|
2963
|
+
if (this._labelsFtsTimer) {
|
|
2964
|
+
clearTimeout(this._labelsFtsTimer);
|
|
2965
|
+
this._labelsFtsTimer = null;
|
|
2966
|
+
this.rebuildLabelsFts();
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Full-text search labels (default language: en-US, falls back to any)
|
|
2971
|
+
*/
|
|
2972
|
+
searchLabels(query, opts = {}) {
|
|
2973
|
+
const { language = 'en-US', model, labelFileId, limit = 30 } = opts;
|
|
2974
|
+
// labels_fts only indexes en-US rows. For any other language, skip straight to
|
|
2975
|
+
// LIKE-based search — attempting FTS would always produce 0 results and then
|
|
2976
|
+
// fall through to LIKE anyway, wasting two round-trips.
|
|
2977
|
+
// Compare case-insensitively: callers pass variants like 'en-us', and a
|
|
2978
|
+
// false mismatch here degrades to a LIKE full scan of the labels table
|
|
2979
|
+
// (200+ s on a production DB, synchronously blocking the event loop).
|
|
2980
|
+
if (language.toLowerCase() !== 'en-us') {
|
|
2981
|
+
return this.searchLabelsLike(query, opts);
|
|
2982
|
+
}
|
|
2983
|
+
// Sanitize query for FTS5 (strip chars that would cause a syntax error)
|
|
2984
|
+
const ftsQuery = query.replace(/['"*()]/g, ' ').trim();
|
|
2985
|
+
// Route to LIKE when FTS5 would silently return 0 results:
|
|
2986
|
+
// • '_' and '%' — word separators in the unicode61 tokenizer (also LIKE wildcards);
|
|
2987
|
+
// literal underscore/percent searches must go through LIKE with proper escaping.
|
|
2988
|
+
// • Any query whose alphanumeric content disappears after sanitization (e.g. '-',
|
|
2989
|
+
// '.', '@', ':', '@SYS:') produces zero FTS5 tokens and no exception to trigger
|
|
2990
|
+
// the catch-based fallback below.
|
|
2991
|
+
if (/[_%]/.test(query) || !/[a-zA-Z0-9]/.test(ftsQuery)) {
|
|
2992
|
+
return this.searchLabelsLike(query, opts);
|
|
2993
|
+
}
|
|
2994
|
+
// Cache statement keyed by which optional filters are active (4 variants)
|
|
2995
|
+
const stmtKey = `searchLabels_${model ? 'model' : 'nomodel'}_${labelFileId ? 'lfid' : 'nolfid'}`;
|
|
2996
|
+
let stmt = this.labelsStmtCache.get(stmtKey);
|
|
2997
|
+
if (!stmt) {
|
|
2998
|
+
let sql = `
|
|
2999
|
+
SELECT l.label_id AS labelId, l.label_file_id AS labelFileId, l.model, l.language,
|
|
3000
|
+
l.text, l.comment, l.file_path AS filePath,
|
|
3001
|
+
f.rank
|
|
3002
|
+
FROM labels_fts f
|
|
3003
|
+
JOIN labels l ON l.id = f.rowid
|
|
3004
|
+
WHERE labels_fts MATCH ?`;
|
|
3005
|
+
if (model)
|
|
3006
|
+
sql += `\n AND l.model = ?`;
|
|
3007
|
+
if (labelFileId)
|
|
3008
|
+
sql += `\n AND l.label_file_id = ?`;
|
|
3009
|
+
sql += `\n ORDER BY f.rank\n LIMIT ?`;
|
|
3010
|
+
stmt = this.labelsDb.prepare(sql);
|
|
3011
|
+
this.labelsStmtCache.set(stmtKey, stmt);
|
|
3012
|
+
}
|
|
3013
|
+
const params = [ftsQuery];
|
|
3014
|
+
if (model)
|
|
3015
|
+
params.push(model);
|
|
3016
|
+
if (labelFileId)
|
|
3017
|
+
params.push(labelFileId);
|
|
3018
|
+
params.push(limit);
|
|
3019
|
+
try {
|
|
3020
|
+
return stmt.all(...params);
|
|
3021
|
+
}
|
|
3022
|
+
catch {
|
|
3023
|
+
// FTS query syntax error — fallback to LIKE
|
|
3024
|
+
return this.searchLabelsLike(query, opts);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* LIKE-based fallback label search (for queries with special characters)
|
|
3029
|
+
*/
|
|
3030
|
+
searchLabelsLike(query, opts = {}) {
|
|
3031
|
+
const { language = 'en-US', model, labelFileId, limit = 30 } = opts;
|
|
3032
|
+
// Escape LIKE special characters so the query is treated as a literal substring.
|
|
3033
|
+
// '\' is the escape character declared in the SQL ESCAPE clause below.
|
|
3034
|
+
const escaped = query.replace(/[\\%_]/g, '\\$&');
|
|
3035
|
+
const pattern = `%${escaped}%`;
|
|
3036
|
+
const stmtKey = `searchLabelsLike_${model ? 'model' : 'nomodel'}_${labelFileId ? 'lfid' : 'nolfid'}`;
|
|
3037
|
+
let stmt = this.labelsStmtCache.get(stmtKey);
|
|
3038
|
+
if (!stmt) {
|
|
3039
|
+
let sql = `
|
|
3040
|
+
SELECT label_id AS labelId, label_file_id AS labelFileId, model, language,
|
|
3041
|
+
text, comment, file_path AS filePath, 0 as rank
|
|
3042
|
+
FROM labels
|
|
3043
|
+
WHERE (text LIKE ? ESCAPE '\\' OR label_id LIKE ? ESCAPE '\\')
|
|
3044
|
+
AND LOWER(language) = LOWER(?)`;
|
|
3045
|
+
if (model)
|
|
3046
|
+
sql += `\n AND model = ?`;
|
|
3047
|
+
if (labelFileId)
|
|
3048
|
+
sql += `\n AND label_file_id = ?`;
|
|
3049
|
+
sql += `\n LIMIT ?`;
|
|
3050
|
+
stmt = this.labelsDb.prepare(sql);
|
|
3051
|
+
this.labelsStmtCache.set(stmtKey, stmt);
|
|
3052
|
+
}
|
|
3053
|
+
const params = [pattern, pattern, language];
|
|
3054
|
+
if (model)
|
|
3055
|
+
params.push(model);
|
|
3056
|
+
if (labelFileId)
|
|
3057
|
+
params.push(labelFileId);
|
|
3058
|
+
params.push(limit);
|
|
3059
|
+
return stmt.all(...params);
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Get a single label by exact ID (returns all languages)
|
|
3063
|
+
*/
|
|
3064
|
+
getLabelById(labelId, labelFileId, model) {
|
|
3065
|
+
const params = [labelId];
|
|
3066
|
+
let sql = `
|
|
3067
|
+
SELECT label_id AS labelId, label_file_id AS labelFileId, model, language, text, comment, file_path AS filePath
|
|
3068
|
+
FROM labels
|
|
3069
|
+
WHERE label_id = ?
|
|
3070
|
+
`;
|
|
3071
|
+
if (labelFileId) {
|
|
3072
|
+
sql += ` AND label_file_id = ?`;
|
|
3073
|
+
params.push(labelFileId);
|
|
3074
|
+
}
|
|
3075
|
+
if (model) {
|
|
3076
|
+
sql += ` AND model = ?`;
|
|
3077
|
+
params.push(model);
|
|
3078
|
+
}
|
|
3079
|
+
sql += ` ORDER BY language`;
|
|
3080
|
+
return this.labelsDb.prepare(sql).all(...params);
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Get all label file IDs for a model (i.e. which AxLabelFiles exist)
|
|
3084
|
+
*/
|
|
3085
|
+
getLabelFileIds(model) {
|
|
3086
|
+
if (model) {
|
|
3087
|
+
return this.labelsDb.prepare(`
|
|
3088
|
+
SELECT label_file_id AS labelFileId, model, GROUP_CONCAT(DISTINCT language) AS languages
|
|
3089
|
+
FROM labels
|
|
3090
|
+
WHERE model = ?
|
|
3091
|
+
GROUP BY label_file_id, model
|
|
3092
|
+
ORDER BY label_file_id
|
|
3093
|
+
`).all(model);
|
|
3094
|
+
}
|
|
3095
|
+
return this.labelsDb.prepare(`
|
|
3096
|
+
SELECT label_file_id AS labelFileId, model, GROUP_CONCAT(DISTINCT language) AS languages
|
|
3097
|
+
FROM labels
|
|
3098
|
+
GROUP BY label_file_id, model
|
|
3099
|
+
ORDER BY label_file_id
|
|
3100
|
+
`).all();
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Get the physical .label.txt file path for each language of a label file.
|
|
3104
|
+
* Used by labels(action="info", labelFileId=…) so callers get the on-disk
|
|
3105
|
+
* location per language instead of having to shell out to find it.
|
|
3106
|
+
*/
|
|
3107
|
+
getLabelFilePaths(labelFileId, model) {
|
|
3108
|
+
const params = [labelFileId];
|
|
3109
|
+
let sql = `
|
|
3110
|
+
SELECT DISTINCT language, file_path AS filePath, model
|
|
3111
|
+
FROM labels
|
|
3112
|
+
WHERE label_file_id = ? AND file_path IS NOT NULL AND file_path != ''
|
|
3113
|
+
`;
|
|
3114
|
+
if (model) {
|
|
3115
|
+
sql += ` AND model = ?`;
|
|
3116
|
+
params.push(model);
|
|
3117
|
+
}
|
|
3118
|
+
sql += ` ORDER BY language`;
|
|
3119
|
+
return this.labelsDb.prepare(sql).all(...params);
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Remove all labels for the given models (used during incremental rebuild)
|
|
3123
|
+
*/
|
|
3124
|
+
clearLabelsForModels(models) {
|
|
3125
|
+
const placeholders = models.map(() => '?').join(',');
|
|
3126
|
+
this.labelsDb.prepare(`DELETE FROM labels WHERE model IN (${placeholders})`).run(...models);
|
|
3127
|
+
this.rebuildLabelsFts();
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Total label count
|
|
3131
|
+
*/
|
|
3132
|
+
getLabelCount() {
|
|
3133
|
+
const row = this.labelsDb.prepare(`SELECT COUNT(*) AS cnt FROM labels`).get();
|
|
3134
|
+
return row?.cnt ?? 0;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Rename a label ID in the index (used by rename_label tool).
|
|
3138
|
+
* Updates all rows for the given labelId + labelFileId + model combination
|
|
3139
|
+
* and rebuilds the FTS index.
|
|
3140
|
+
*/
|
|
3141
|
+
renameLabelInIndex(oldLabelId, newLabelId, labelFileId, model) {
|
|
3142
|
+
this.labelsDb.prepare(`
|
|
3143
|
+
UPDATE labels
|
|
3144
|
+
SET label_id = ?
|
|
3145
|
+
WHERE label_id = ?
|
|
3146
|
+
AND label_file_id = ?
|
|
3147
|
+
AND model = ?
|
|
3148
|
+
`).run(newLabelId, oldLabelId, labelFileId, model);
|
|
3149
|
+
this.rebuildLabelsFts();
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
//# sourceMappingURL=symbolIndex.js.map
|