@memberjunction/db-auto-doc 2.116.0 → 2.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +652 -165
- package/bin/run.js +7 -0
- package/dist/api/DBAutoDocAPI.d.ts +252 -0
- package/dist/api/DBAutoDocAPI.d.ts.map +1 -0
- package/dist/api/DBAutoDocAPI.js +530 -0
- package/dist/api/DBAutoDocAPI.js.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +10 -0
- package/dist/api/index.js.map +1 -0
- package/dist/commands/analyze.d.ts +6 -4
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +58 -71
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/export.d.ts +14 -4
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +156 -61
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/init.d.ts +3 -4
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +155 -146
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/reset.d.ts +4 -1
- package/dist/commands/reset.d.ts.map +1 -1
- package/dist/commands/reset.js +33 -19
- package/dist/commands/reset.js.map +1 -1
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +66 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/core/AnalysisEngine.d.ts +108 -0
- package/dist/core/AnalysisEngine.d.ts.map +1 -0
- package/dist/core/AnalysisEngine.js +716 -0
- package/dist/core/AnalysisEngine.js.map +1 -0
- package/dist/core/AnalysisOrchestrator.d.ts +37 -0
- package/dist/core/AnalysisOrchestrator.d.ts.map +1 -0
- package/dist/core/AnalysisOrchestrator.js +294 -0
- package/dist/core/AnalysisOrchestrator.js.map +1 -0
- package/dist/core/BackpropagationEngine.d.ts +32 -0
- package/dist/core/BackpropagationEngine.d.ts.map +1 -0
- package/dist/core/BackpropagationEngine.js +121 -0
- package/dist/core/BackpropagationEngine.js.map +1 -0
- package/dist/core/ConvergenceDetector.d.ts +27 -0
- package/dist/core/ConvergenceDetector.d.ts.map +1 -0
- package/dist/core/ConvergenceDetector.js +92 -0
- package/dist/core/ConvergenceDetector.js.map +1 -0
- package/dist/core/GuardrailsManager.d.ts +78 -0
- package/dist/core/GuardrailsManager.d.ts.map +1 -0
- package/dist/core/GuardrailsManager.js +367 -0
- package/dist/core/GuardrailsManager.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +13 -0
- package/dist/core/index.js.map +1 -0
- package/dist/database/Database.d.ts +56 -0
- package/dist/database/Database.d.ts.map +1 -0
- package/dist/database/Database.js +172 -0
- package/dist/database/Database.js.map +1 -0
- package/dist/database/TopologicalSorter.d.ts +25 -0
- package/dist/database/TopologicalSorter.d.ts.map +1 -0
- package/dist/database/TopologicalSorter.js +150 -0
- package/dist/database/TopologicalSorter.js.map +1 -0
- package/dist/database/index.d.ts +6 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +14 -0
- package/dist/database/index.js.map +1 -0
- package/dist/discovery/ColumnStatsCache.d.ts +91 -0
- package/dist/discovery/ColumnStatsCache.d.ts.map +1 -0
- package/dist/discovery/ColumnStatsCache.js +231 -0
- package/dist/discovery/ColumnStatsCache.js.map +1 -0
- package/dist/discovery/DiscoveryEngine.d.ts +100 -0
- package/dist/discovery/DiscoveryEngine.d.ts.map +1 -0
- package/dist/discovery/DiscoveryEngine.js +726 -0
- package/dist/discovery/DiscoveryEngine.js.map +1 -0
- package/dist/discovery/DiscoveryTriggerAnalyzer.d.ts +57 -0
- package/dist/discovery/DiscoveryTriggerAnalyzer.d.ts.map +1 -0
- package/dist/discovery/DiscoveryTriggerAnalyzer.js +186 -0
- package/dist/discovery/DiscoveryTriggerAnalyzer.js.map +1 -0
- package/dist/discovery/FKDetector.d.ts +47 -0
- package/dist/discovery/FKDetector.d.ts.map +1 -0
- package/dist/discovery/FKDetector.js +317 -0
- package/dist/discovery/FKDetector.js.map +1 -0
- package/dist/discovery/LLMDiscoveryValidator.d.ts +64 -0
- package/dist/discovery/LLMDiscoveryValidator.d.ts.map +1 -0
- package/dist/discovery/LLMDiscoveryValidator.js +431 -0
- package/dist/discovery/LLMDiscoveryValidator.js.map +1 -0
- package/dist/discovery/LLMSanityChecker.d.ts +38 -0
- package/dist/discovery/LLMSanityChecker.d.ts.map +1 -0
- package/dist/discovery/LLMSanityChecker.js +156 -0
- package/dist/discovery/LLMSanityChecker.js.map +1 -0
- package/dist/discovery/PKDetector.d.ts +62 -0
- package/dist/discovery/PKDetector.d.ts.map +1 -0
- package/dist/discovery/PKDetector.js +436 -0
- package/dist/discovery/PKDetector.js.map +1 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +25 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/drivers/BaseAutoDocDriver.d.ts +132 -0
- package/dist/drivers/BaseAutoDocDriver.d.ts.map +1 -0
- package/dist/drivers/BaseAutoDocDriver.js +121 -0
- package/dist/drivers/BaseAutoDocDriver.js.map +1 -0
- package/dist/drivers/MySQLDriver.d.ts +61 -0
- package/dist/drivers/MySQLDriver.d.ts.map +1 -0
- package/dist/drivers/MySQLDriver.js +668 -0
- package/dist/drivers/MySQLDriver.js.map +1 -0
- package/dist/drivers/PostgreSQLDriver.d.ts +65 -0
- package/dist/drivers/PostgreSQLDriver.d.ts.map +1 -0
- package/dist/drivers/PostgreSQLDriver.js +704 -0
- package/dist/drivers/PostgreSQLDriver.js.map +1 -0
- package/dist/drivers/SQLServerDriver.d.ts +61 -0
- package/dist/drivers/SQLServerDriver.d.ts.map +1 -0
- package/dist/drivers/SQLServerDriver.js +667 -0
- package/dist/drivers/SQLServerDriver.js.map +1 -0
- package/dist/generators/CSVGenerator.d.ts +35 -0
- package/dist/generators/CSVGenerator.d.ts.map +1 -0
- package/dist/generators/CSVGenerator.js +154 -0
- package/dist/generators/CSVGenerator.js.map +1 -0
- package/dist/generators/HTMLGenerator.d.ts +29 -0
- package/dist/generators/HTMLGenerator.d.ts.map +1 -0
- package/dist/generators/HTMLGenerator.js +710 -0
- package/dist/generators/HTMLGenerator.js.map +1 -0
- package/dist/generators/MarkdownGenerator.d.ts +27 -0
- package/dist/generators/MarkdownGenerator.d.ts.map +1 -0
- package/dist/generators/MarkdownGenerator.js +361 -0
- package/dist/generators/MarkdownGenerator.js.map +1 -0
- package/dist/generators/MermaidGenerator.d.ts +35 -0
- package/dist/generators/MermaidGenerator.d.ts.map +1 -0
- package/dist/generators/MermaidGenerator.js +321 -0
- package/dist/generators/MermaidGenerator.js.map +1 -0
- package/dist/generators/ReportGenerator.d.ts +22 -0
- package/dist/generators/ReportGenerator.d.ts.map +1 -0
- package/dist/generators/ReportGenerator.js +176 -0
- package/dist/generators/ReportGenerator.js.map +1 -0
- package/dist/generators/SQLGenerator.d.ts +31 -0
- package/dist/generators/SQLGenerator.d.ts.map +1 -0
- package/dist/generators/SQLGenerator.js +168 -0
- package/dist/generators/SQLGenerator.js.map +1 -0
- package/dist/generators/index.d.ts +10 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +19 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/index.d.ts +11 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -20
- package/dist/index.js.map +1 -1
- package/dist/prompts/PromptEngine.d.ts +65 -0
- package/dist/prompts/PromptEngine.d.ts.map +1 -0
- package/dist/prompts/PromptEngine.js +282 -0
- package/dist/prompts/PromptEngine.js.map +1 -0
- package/dist/prompts/PromptFileLoader.d.ts +21 -0
- package/dist/prompts/PromptFileLoader.d.ts.map +1 -0
- package/dist/prompts/PromptFileLoader.js +74 -0
- package/dist/prompts/PromptFileLoader.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 +11 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/state/IterationTracker.d.ts +64 -0
- package/dist/state/IterationTracker.d.ts.map +1 -0
- package/dist/state/IterationTracker.js +136 -0
- package/dist/state/IterationTracker.js.map +1 -0
- package/dist/state/StateManager.d.ts +79 -0
- package/dist/state/StateManager.d.ts.map +1 -0
- package/dist/state/StateManager.js +348 -0
- package/dist/state/StateManager.js.map +1 -0
- package/dist/state/StateValidator.d.ts +24 -0
- package/dist/state/StateValidator.d.ts.map +1 -0
- package/dist/state/StateValidator.js +147 -0
- package/dist/state/StateValidator.js.map +1 -0
- package/dist/state/index.d.ts +7 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +13 -0
- package/dist/state/index.js.map +1 -0
- package/dist/types/analysis.d.ts +76 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +6 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/config.d.ts +132 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +7 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/discovery.d.ts +277 -0
- package/dist/types/discovery.d.ts.map +1 -0
- package/dist/types/discovery.js +7 -0
- package/dist/types/discovery.js.map +1 -0
- package/dist/types/driver.d.ts +148 -0
- package/dist/types/driver.d.ts.map +1 -0
- package/dist/types/driver.js +7 -0
- package/dist/types/driver.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +24 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/prompts.d.ts +158 -0
- package/dist/types/prompts.d.ts.map +1 -0
- package/dist/types/prompts.js +6 -0
- package/dist/types/prompts.js.map +1 -0
- package/dist/types/state.d.ts +278 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +7 -0
- package/dist/types/state.js.map +1 -0
- package/dist/utils/config-loader.d.ts +29 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +163 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +24 -3
- package/dist/ai/simple-ai-client.d.ts +0 -70
- package/dist/ai/simple-ai-client.d.ts.map +0 -1
- package/dist/ai/simple-ai-client.js +0 -181
- package/dist/ai/simple-ai-client.js.map +0 -1
- package/dist/analyzers/analyzer.d.ts +0 -23
- package/dist/analyzers/analyzer.d.ts.map +0 -1
- package/dist/analyzers/analyzer.js +0 -127
- package/dist/analyzers/analyzer.js.map +0 -1
- package/dist/cli-old/cli.d.ts +0 -3
- package/dist/cli-old/cli.d.ts.map +0 -1
- package/dist/cli-old/cli.js +0 -388
- package/dist/cli-old/cli.js.map +0 -1
- package/dist/commands/review.d.ts +0 -11
- package/dist/commands/review.d.ts.map +0 -1
- package/dist/commands/review.js +0 -82
- package/dist/commands/review.js.map +0 -1
- package/dist/database/connection.d.ts +0 -40
- package/dist/database/connection.d.ts.map +0 -1
- package/dist/database/connection.js +0 -136
- package/dist/database/connection.js.map +0 -1
- package/dist/database/introspection.d.ts +0 -59
- package/dist/database/introspection.d.ts.map +0 -1
- package/dist/database/introspection.js +0 -124
- package/dist/database/introspection.js.map +0 -1
- package/dist/generators/markdown-generator.d.ts +0 -8
- package/dist/generators/markdown-generator.d.ts.map +0 -1
- package/dist/generators/markdown-generator.js +0 -106
- package/dist/generators/markdown-generator.js.map +0 -1
- package/dist/generators/sql-generator.d.ts +0 -20
- package/dist/generators/sql-generator.d.ts.map +0 -1
- package/dist/generators/sql-generator.js +0 -83
- package/dist/generators/sql-generator.js.map +0 -1
- package/dist/state/state-manager.d.ts +0 -95
- package/dist/state/state-manager.d.ts.map +0 -1
- package/dist/state/state-manager.js +0 -236
- package/dist/state/state-manager.js.map +0 -1
- package/dist/types/state-file.d.ts +0 -124
- package/dist/types/state-file.d.ts.map +0 -1
- package/dist/types/state-file.js +0 -79
- package/dist/types/state-file.js.map +0 -1
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery Engine
|
|
4
|
+
* Orchestrates relationship discovery with iterative refinement and backpropagation
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DiscoveryEngine = void 0;
|
|
8
|
+
const PKDetector_js_1 = require("./PKDetector.js");
|
|
9
|
+
const FKDetector_js_1 = require("./FKDetector.js");
|
|
10
|
+
const LLMDiscoveryValidator_js_1 = require("./LLMDiscoveryValidator.js");
|
|
11
|
+
const LLMSanityChecker_js_1 = require("./LLMSanityChecker.js");
|
|
12
|
+
const ColumnStatsCache_js_1 = require("./ColumnStatsCache.js");
|
|
13
|
+
class DiscoveryEngine {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.driver = options.driver;
|
|
16
|
+
this.config = options.config;
|
|
17
|
+
this.aiConfig = options.aiConfig;
|
|
18
|
+
this.schemas = options.schemas;
|
|
19
|
+
this.onProgress = options.onProgress || (() => { });
|
|
20
|
+
// Create stats cache and detectors
|
|
21
|
+
this.statsCache = new ColumnStatsCache_js_1.ColumnStatsCache();
|
|
22
|
+
this.pkDetector = new PKDetector_js_1.PKDetector(this.driver, this.config, this.statsCache);
|
|
23
|
+
this.fkDetector = new FKDetector_js_1.FKDetector(this.driver, this.config);
|
|
24
|
+
// Create LLM sanity checker (runs once after statistical detection)
|
|
25
|
+
this.sanityChecker = new LLMSanityChecker_js_1.LLMSanityChecker(this.aiConfig);
|
|
26
|
+
// Create LLM validator if enabled (runs per-table validation)
|
|
27
|
+
if (this.config.llmValidation?.enabled) {
|
|
28
|
+
this.llmValidator = new LLMDiscoveryValidator_js_1.LLMDiscoveryValidator(this.driver, this.config, this.aiConfig, this.statsCache, this.schemas);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the column statistics cache
|
|
33
|
+
*/
|
|
34
|
+
getStatsCache() {
|
|
35
|
+
return this.statsCache;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Analyze if discovery should be triggered based on schema state
|
|
39
|
+
*/
|
|
40
|
+
analyzeTrigger() {
|
|
41
|
+
let totalTables = 0;
|
|
42
|
+
let tablesWithPK = 0;
|
|
43
|
+
let totalFKs = 0;
|
|
44
|
+
for (const schema of this.schemas) {
|
|
45
|
+
totalTables += schema.tables.length;
|
|
46
|
+
for (const table of schema.tables) {
|
|
47
|
+
// Check if table has PK
|
|
48
|
+
const hasPK = table.columns.some(col => col.isPrimaryKey);
|
|
49
|
+
if (hasPK) {
|
|
50
|
+
tablesWithPK++;
|
|
51
|
+
}
|
|
52
|
+
// Count FKs
|
|
53
|
+
totalFKs += table.columns.filter(col => col.isForeignKey).length;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const tablesWithoutPK = totalTables - tablesWithPK;
|
|
57
|
+
// Heuristic: expect at least 0.3 FKs per table on average
|
|
58
|
+
const expectedMinFKs = Math.floor(totalTables * 0.3);
|
|
59
|
+
const fkDeficit = Math.max(0, expectedMinFKs - totalFKs);
|
|
60
|
+
const fkDeficitPercentage = expectedMinFKs > 0 ? fkDeficit / expectedMinFKs : 0;
|
|
61
|
+
// Determine if discovery should run
|
|
62
|
+
let shouldRun = false;
|
|
63
|
+
let reason = '';
|
|
64
|
+
if (tablesWithoutPK > 0 && this.config.triggers.runOnMissingPKs) {
|
|
65
|
+
shouldRun = true;
|
|
66
|
+
reason = `${tablesWithoutPK} table(s) missing primary keys`;
|
|
67
|
+
}
|
|
68
|
+
if (fkDeficitPercentage >= this.config.triggers.fkDeficitThreshold &&
|
|
69
|
+
this.config.triggers.runOnInsufficientFKs) {
|
|
70
|
+
shouldRun = true;
|
|
71
|
+
if (reason) {
|
|
72
|
+
reason += ` and FK deficit of ${(fkDeficitPercentage * 100).toFixed(1)}%`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
reason = `FK deficit of ${(fkDeficitPercentage * 100).toFixed(1)}% (${totalFKs}/${expectedMinFKs})`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
shouldRun,
|
|
80
|
+
reason: shouldRun ? reason : 'Discovery not needed - schema is well-defined',
|
|
81
|
+
details: {
|
|
82
|
+
totalTables,
|
|
83
|
+
tablesWithPK,
|
|
84
|
+
tablesWithoutPK,
|
|
85
|
+
totalFKs,
|
|
86
|
+
expectedMinFKs,
|
|
87
|
+
fkDeficit,
|
|
88
|
+
fkDeficitPercentage
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Execute the discovery process
|
|
94
|
+
*/
|
|
95
|
+
async discover(maxTokens, triggerAnalysis) {
|
|
96
|
+
const phase = this.initializePhase(maxTokens, triggerAnalysis);
|
|
97
|
+
this.onProgress('Starting relationship discovery', {
|
|
98
|
+
maxTokens,
|
|
99
|
+
trigger: triggerAnalysis.reason
|
|
100
|
+
});
|
|
101
|
+
let iteration = 1;
|
|
102
|
+
let tokensUsed = 0;
|
|
103
|
+
let guardrailsReached = false;
|
|
104
|
+
let guardrailReason;
|
|
105
|
+
while (iteration <= this.config.backpropagation.maxIterations) {
|
|
106
|
+
// Check guardrails before starting iteration
|
|
107
|
+
const guardrailCheck = this.checkGuardrails(tokensUsed, maxTokens, iteration);
|
|
108
|
+
if (!guardrailCheck.canContinue) {
|
|
109
|
+
guardrailsReached = true;
|
|
110
|
+
guardrailReason = guardrailCheck.reason;
|
|
111
|
+
this.onProgress('Guardrails reached', { reason: guardrailReason });
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
this.onProgress(`Discovery iteration ${iteration}`, { tokensUsed, maxTokens });
|
|
115
|
+
// Execute iteration
|
|
116
|
+
const iterationResult = await this.executeIteration(iteration, phase, maxTokens - tokensUsed);
|
|
117
|
+
phase.iterations.push(iterationResult);
|
|
118
|
+
tokensUsed += iterationResult.tokensUsed;
|
|
119
|
+
phase.tokenBudget.used = tokensUsed;
|
|
120
|
+
phase.tokenBudget.remaining = maxTokens - tokensUsed;
|
|
121
|
+
// Update phase discoveries
|
|
122
|
+
this.mergeDiscoveries(phase, iterationResult);
|
|
123
|
+
this.onProgress(`Iteration ${iteration} complete`, {
|
|
124
|
+
newPKs: iterationResult.discoveries.newPKs.length,
|
|
125
|
+
newFKs: iterationResult.discoveries.newFKs.length,
|
|
126
|
+
tokensUsed: iterationResult.tokensUsed
|
|
127
|
+
});
|
|
128
|
+
// Check if we should continue
|
|
129
|
+
const shouldContinue = this.shouldContinueDiscovery(iterationResult, phase, iteration);
|
|
130
|
+
if (!shouldContinue) {
|
|
131
|
+
this.onProgress('Discovery converged - no significant changes detected');
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
iteration++;
|
|
135
|
+
}
|
|
136
|
+
// Finalize phase
|
|
137
|
+
phase.completedAt = new Date().toISOString();
|
|
138
|
+
this.calculateSummary(phase);
|
|
139
|
+
this.onProgress('Discovery complete', {
|
|
140
|
+
iterations: phase.iterations.length,
|
|
141
|
+
tokensUsed,
|
|
142
|
+
pksDiscovered: phase.discovered.primaryKeys.length,
|
|
143
|
+
fksDiscovered: phase.discovered.foreignKeys.length
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
phase,
|
|
147
|
+
guardrailsReached,
|
|
148
|
+
guardrailReason,
|
|
149
|
+
statsCache: this.statsCache
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Initialize discovery phase
|
|
154
|
+
*/
|
|
155
|
+
initializePhase(maxTokens, triggerAnalysis) {
|
|
156
|
+
const triggerReason = this.determineTriggerReason(triggerAnalysis);
|
|
157
|
+
return {
|
|
158
|
+
triggered: true,
|
|
159
|
+
triggerReason,
|
|
160
|
+
triggerDetails: {
|
|
161
|
+
tablesWithoutPK: triggerAnalysis.details.tablesWithoutPK,
|
|
162
|
+
expectedFKs: triggerAnalysis.details.expectedMinFKs,
|
|
163
|
+
actualFKs: triggerAnalysis.details.totalFKs,
|
|
164
|
+
fkDeficitPercentage: triggerAnalysis.details.fkDeficitPercentage
|
|
165
|
+
},
|
|
166
|
+
startedAt: new Date().toISOString(),
|
|
167
|
+
tokenBudget: {
|
|
168
|
+
allocated: maxTokens,
|
|
169
|
+
used: 0,
|
|
170
|
+
remaining: maxTokens
|
|
171
|
+
},
|
|
172
|
+
iterations: [],
|
|
173
|
+
discovered: {
|
|
174
|
+
primaryKeys: [],
|
|
175
|
+
foreignKeys: []
|
|
176
|
+
},
|
|
177
|
+
schemaEnhancements: {
|
|
178
|
+
pkeysAdded: 0,
|
|
179
|
+
fkeysAdded: 0,
|
|
180
|
+
overallConfidence: 0
|
|
181
|
+
},
|
|
182
|
+
feedbackFromAnalysis: [],
|
|
183
|
+
summary: {
|
|
184
|
+
totalTablesAnalyzed: 0,
|
|
185
|
+
tablesWithDiscoveredPKs: 0,
|
|
186
|
+
relationshipsDiscovered: 0,
|
|
187
|
+
averageConfidence: 0,
|
|
188
|
+
highConfidenceCount: 0,
|
|
189
|
+
mediumConfidenceCount: 0,
|
|
190
|
+
lowConfidenceCount: 0,
|
|
191
|
+
rejectedCount: 0
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Determine trigger reason from analysis
|
|
197
|
+
*/
|
|
198
|
+
determineTriggerReason(analysis) {
|
|
199
|
+
const hasMissingPKs = analysis.details.tablesWithoutPK > 0;
|
|
200
|
+
const hasInsufficientFKs = analysis.details.fkDeficitPercentage >= this.config.triggers.fkDeficitThreshold;
|
|
201
|
+
if (hasMissingPKs && hasInsufficientFKs) {
|
|
202
|
+
return 'both';
|
|
203
|
+
}
|
|
204
|
+
else if (hasMissingPKs) {
|
|
205
|
+
return 'missing_pks';
|
|
206
|
+
}
|
|
207
|
+
else if (hasInsufficientFKs) {
|
|
208
|
+
return 'insufficient_fks';
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
return 'manual';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Execute a single discovery iteration
|
|
216
|
+
*/
|
|
217
|
+
async executeIteration(iteration, phase, remainingTokens) {
|
|
218
|
+
const startedAt = new Date().toISOString();
|
|
219
|
+
const iterationResult = {
|
|
220
|
+
iteration,
|
|
221
|
+
phase: 'sampling',
|
|
222
|
+
startedAt,
|
|
223
|
+
completedAt: '',
|
|
224
|
+
tokensUsed: 0,
|
|
225
|
+
discoveries: {
|
|
226
|
+
newPKs: [],
|
|
227
|
+
newFKs: [],
|
|
228
|
+
validated: [],
|
|
229
|
+
rejected: [],
|
|
230
|
+
confidenceChanges: []
|
|
231
|
+
},
|
|
232
|
+
backpropTriggered: false
|
|
233
|
+
};
|
|
234
|
+
// Phase 1: PK Detection
|
|
235
|
+
iterationResult.phase = 'pk_detection';
|
|
236
|
+
let newPKs = await this.detectPrimaryKeys(iteration, phase);
|
|
237
|
+
iterationResult.discoveries.newPKs = newPKs;
|
|
238
|
+
this.onProgress(`Detected ${newPKs.length} PK candidates`, {
|
|
239
|
+
iteration,
|
|
240
|
+
candidates: newPKs.map(pk => `${pk.schemaName}.${pk.tableName}.${pk.columnNames.join('+')}`).slice(0, 5)
|
|
241
|
+
});
|
|
242
|
+
// Phase 2: FK Detection
|
|
243
|
+
iterationResult.phase = 'fk_detection';
|
|
244
|
+
const allPKs = [...phase.discovered.primaryKeys, ...newPKs];
|
|
245
|
+
let newFKs = await this.detectForeignKeys(iteration, phase, allPKs);
|
|
246
|
+
this.onProgress(`Detected ${newFKs.length} FK candidates (pre-sanity)`, {
|
|
247
|
+
iteration,
|
|
248
|
+
candidates: newFKs.map(fk => `${fk.schemaName}.${fk.sourceTable}.${fk.sourceColumn} -> ${fk.targetSchema}.${fk.targetTable}.${fk.targetColumn}`).slice(0, 5)
|
|
249
|
+
});
|
|
250
|
+
// Phase 2.5: LLM Sanity Check (FIRST TIME ONLY - reject obviously wrong candidates)
|
|
251
|
+
if (this.sanityChecker && iteration === 1 && (newPKs.length > 0 || newFKs.length > 0)) {
|
|
252
|
+
iterationResult.phase = 'sanity_check';
|
|
253
|
+
this.onProgress('Running LLM sanity check for obvious errors', {
|
|
254
|
+
pkCandidates: newPKs.length,
|
|
255
|
+
fkCandidates: newFKs.length
|
|
256
|
+
});
|
|
257
|
+
const sanityResult = await this.sanityChecker.reviewCandidates(newPKs, newFKs);
|
|
258
|
+
iterationResult.tokensUsed += sanityResult.tokensUsed;
|
|
259
|
+
// Remove invalid PKs
|
|
260
|
+
if (sanityResult.invalidPKs.length > 0) {
|
|
261
|
+
console.log(`[DiscoveryEngine] Sanity check rejecting ${sanityResult.invalidPKs.length} PKs:`);
|
|
262
|
+
for (const invalid of sanityResult.invalidPKs) {
|
|
263
|
+
console.log(` - ${invalid.schema}.${invalid.table}.${invalid.column}: ${invalid.reason}`);
|
|
264
|
+
}
|
|
265
|
+
newPKs = newPKs.filter(pk => {
|
|
266
|
+
return !sanityResult.invalidPKs.some(invalid => invalid.schema === pk.schemaName &&
|
|
267
|
+
invalid.table === pk.tableName &&
|
|
268
|
+
pk.columnNames.includes(invalid.column));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Remove invalid FKs
|
|
272
|
+
if (sanityResult.invalidFKs.length > 0) {
|
|
273
|
+
console.log(`[DiscoveryEngine] Sanity check rejecting ${sanityResult.invalidFKs.length} FKs:`);
|
|
274
|
+
for (const invalid of sanityResult.invalidFKs) {
|
|
275
|
+
console.log(` - ${invalid.schema}.${invalid.table}.${invalid.column}: ${invalid.reason}`);
|
|
276
|
+
}
|
|
277
|
+
newFKs = newFKs.filter(fk => {
|
|
278
|
+
return !sanityResult.invalidFKs.some(invalid => invalid.schema === fk.schemaName &&
|
|
279
|
+
invalid.table === fk.sourceTable &&
|
|
280
|
+
invalid.column === fk.sourceColumn);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Log suggestions
|
|
284
|
+
if (sanityResult.suggestions.length > 0) {
|
|
285
|
+
console.log(`[DiscoveryEngine] Sanity check suggestions:`);
|
|
286
|
+
for (const suggestion of sanityResult.suggestions) {
|
|
287
|
+
console.log(` - ${suggestion}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
this.onProgress(`Sanity check complete`, {
|
|
291
|
+
invalidPKs: sanityResult.invalidPKs.length,
|
|
292
|
+
invalidFKs: sanityResult.invalidFKs.length,
|
|
293
|
+
finalPKs: newPKs.length,
|
|
294
|
+
finalFKs: newFKs.length,
|
|
295
|
+
tokensUsed: sanityResult.tokensUsed
|
|
296
|
+
});
|
|
297
|
+
// Update discoveries
|
|
298
|
+
iterationResult.discoveries.newPKs = newPKs;
|
|
299
|
+
iterationResult.discoveries.newFKs = newFKs;
|
|
300
|
+
}
|
|
301
|
+
// Phase 3: LLM Validation (if enabled and we have candidates or need validation)
|
|
302
|
+
if (this.llmValidator && (newPKs.length > 0 || newFKs.length > 0)) {
|
|
303
|
+
iterationResult.phase = 'llm_validation';
|
|
304
|
+
this.onProgress('Starting LLM validation', {
|
|
305
|
+
pkCandidates: newPKs.length,
|
|
306
|
+
fkCandidates: newFKs.length,
|
|
307
|
+
tokensRemaining: remainingTokens - iterationResult.tokensUsed
|
|
308
|
+
});
|
|
309
|
+
const validationResults = await this.performLLMValidation(newPKs, newFKs, allPKs, iterationResult, remainingTokens - iterationResult.tokensUsed);
|
|
310
|
+
// Update candidates based on LLM feedback
|
|
311
|
+
newPKs = validationResults.updatedPKs;
|
|
312
|
+
newFKs = validationResults.updatedFKs;
|
|
313
|
+
iterationResult.tokensUsed += validationResults.tokensUsed;
|
|
314
|
+
this.onProgress(`LLM validation complete`, {
|
|
315
|
+
tokensUsed: validationResults.tokensUsed,
|
|
316
|
+
validated: validationResults.validated.length,
|
|
317
|
+
rejected: validationResults.rejected.length,
|
|
318
|
+
finalPKs: newPKs.length,
|
|
319
|
+
finalFKs: newFKs.length
|
|
320
|
+
});
|
|
321
|
+
iterationResult.discoveries.validated = validationResults.validated;
|
|
322
|
+
iterationResult.discoveries.rejected = validationResults.rejected;
|
|
323
|
+
iterationResult.discoveries.newPKs = newPKs;
|
|
324
|
+
iterationResult.discoveries.newFKs = newFKs;
|
|
325
|
+
}
|
|
326
|
+
// Phase 4: Backpropagation check
|
|
327
|
+
iterationResult.phase = 'backprop';
|
|
328
|
+
const backpropCheck = this.shouldTriggerBackprop(iterationResult, phase);
|
|
329
|
+
iterationResult.backpropTriggered = backpropCheck.shouldTrigger;
|
|
330
|
+
iterationResult.backpropReason = backpropCheck.reason;
|
|
331
|
+
if (backpropCheck.shouldTrigger && this.config.backpropagation.enabled) {
|
|
332
|
+
this.onProgress('Backpropagation triggered', { reason: backpropCheck.reason });
|
|
333
|
+
// Re-analyze affected tables with new relationship context
|
|
334
|
+
const confidenceChanges = await this.performBackpropagation(phase, newPKs, newFKs);
|
|
335
|
+
iterationResult.discoveries.confidenceChanges = confidenceChanges;
|
|
336
|
+
}
|
|
337
|
+
iterationResult.completedAt = new Date().toISOString();
|
|
338
|
+
return iterationResult;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Detect primary keys in all tables without PKs
|
|
342
|
+
*/
|
|
343
|
+
async detectPrimaryKeys(iteration, phase) {
|
|
344
|
+
const allPKs = [];
|
|
345
|
+
const tablesAnalyzed = new Set();
|
|
346
|
+
for (const schema of this.schemas) {
|
|
347
|
+
for (const table of schema.tables) {
|
|
348
|
+
const tableKey = `${schema.name}.${table.name}`;
|
|
349
|
+
// Skip if already has PK or already analyzed
|
|
350
|
+
const hasExistingPK = table.columns.some(col => col.isPrimaryKey);
|
|
351
|
+
const hasDiscoveredPK = phase.discovered.primaryKeys.some(pk => pk.schemaName === schema.name && pk.tableName === table.name);
|
|
352
|
+
if (hasExistingPK || hasDiscoveredPK || tablesAnalyzed.has(tableKey)) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// Detect PK candidates for this table
|
|
356
|
+
const candidates = await this.pkDetector.detectPKCandidates(schema.name, table, iteration);
|
|
357
|
+
allPKs.push(...candidates);
|
|
358
|
+
tablesAnalyzed.add(tableKey);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return allPKs;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Detect foreign keys across all schemas
|
|
365
|
+
*/
|
|
366
|
+
async detectForeignKeys(iteration, phase, discoveredPKs) {
|
|
367
|
+
const allFKs = [];
|
|
368
|
+
for (const schema of this.schemas) {
|
|
369
|
+
for (const table of schema.tables) {
|
|
370
|
+
// Detect FK candidates for this table
|
|
371
|
+
const candidates = await this.fkDetector.detectFKCandidates(this.schemas, schema.name, table, discoveredPKs, iteration);
|
|
372
|
+
// Filter out duplicates (already discovered FKs)
|
|
373
|
+
const newCandidates = candidates.filter(newFK => !phase.discovered.foreignKeys.some(existingFK => existingFK.schemaName === newFK.schemaName &&
|
|
374
|
+
existingFK.sourceTable === newFK.sourceTable &&
|
|
375
|
+
existingFK.sourceColumn === newFK.sourceColumn &&
|
|
376
|
+
existingFK.targetSchema === newFK.targetSchema &&
|
|
377
|
+
existingFK.targetTable === newFK.targetTable &&
|
|
378
|
+
existingFK.targetColumn === newFK.targetColumn));
|
|
379
|
+
allFKs.push(...newCandidates);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return allFKs;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Perform LLM validation on PK/FK candidates
|
|
386
|
+
*/
|
|
387
|
+
async performLLMValidation(pkCandidates, fkCandidates, allPKs, iteration, remainingTokens) {
|
|
388
|
+
if (!this.llmValidator) {
|
|
389
|
+
return {
|
|
390
|
+
updatedPKs: pkCandidates,
|
|
391
|
+
updatedFKs: fkCandidates,
|
|
392
|
+
validated: [],
|
|
393
|
+
rejected: [],
|
|
394
|
+
tokensUsed: 0
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const updatedPKs = [...pkCandidates];
|
|
398
|
+
const updatedFKs = [...fkCandidates];
|
|
399
|
+
const validated = [];
|
|
400
|
+
const rejected = [];
|
|
401
|
+
let tokensUsed = 0;
|
|
402
|
+
// Group candidates by table for efficient validation
|
|
403
|
+
const tableMap = new Map();
|
|
404
|
+
for (const pk of pkCandidates) {
|
|
405
|
+
const key = `${pk.schemaName}.${pk.tableName}`;
|
|
406
|
+
if (!tableMap.has(key)) {
|
|
407
|
+
tableMap.set(key, { pks: [], fks: [] });
|
|
408
|
+
}
|
|
409
|
+
tableMap.get(key).pks.push(pk);
|
|
410
|
+
}
|
|
411
|
+
for (const fk of fkCandidates) {
|
|
412
|
+
const key = `${fk.schemaName}.${fk.sourceTable}`;
|
|
413
|
+
if (!tableMap.has(key)) {
|
|
414
|
+
tableMap.set(key, { pks: [], fks: [] });
|
|
415
|
+
}
|
|
416
|
+
tableMap.get(key).fks.push(fk);
|
|
417
|
+
}
|
|
418
|
+
// Validate each table's candidates
|
|
419
|
+
for (const [tableKey, candidates] of tableMap.entries()) {
|
|
420
|
+
if (tokensUsed >= remainingTokens) {
|
|
421
|
+
this.onProgress('Token budget exhausted during LLM validation');
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
const [schemaName, tableName] = tableKey.split('.');
|
|
425
|
+
this.onProgress(`Validating ${tableKey}`, {
|
|
426
|
+
pks: candidates.pks.length,
|
|
427
|
+
fks: candidates.fks.length
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
const result = await this.llmValidator.validateTableRelationships(schemaName, tableName, candidates.pks, candidates.fks);
|
|
431
|
+
tokensUsed += result.tokensUsed;
|
|
432
|
+
if (!result.validated) {
|
|
433
|
+
this.onProgress(`LLM validation failed for ${tableKey}: ${result.reasoning}`);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
// Process LLM recommendations
|
|
437
|
+
for (const rec of result.recommendations) {
|
|
438
|
+
const recId = `${rec.target}:${rec.schemaName}.${rec.tableName}.${rec.columnName}`;
|
|
439
|
+
if (rec.type === 'confirm') {
|
|
440
|
+
validated.push(recId);
|
|
441
|
+
// Mark as validated and potentially boost confidence
|
|
442
|
+
if (rec.target === 'pk') {
|
|
443
|
+
const pk = updatedPKs.find(p => p.schemaName === rec.schemaName &&
|
|
444
|
+
p.tableName === rec.tableName &&
|
|
445
|
+
p.columnNames.includes(rec.columnName));
|
|
446
|
+
if (pk) {
|
|
447
|
+
pk.validatedByLLM = true;
|
|
448
|
+
pk.confidence = Math.min(pk.confidence + 15, 100); // Boost confidence
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else if (rec.target === 'fk') {
|
|
452
|
+
const fk = updatedFKs.find(f => f.schemaName === rec.schemaName &&
|
|
453
|
+
f.sourceTable === rec.tableName &&
|
|
454
|
+
f.sourceColumn === rec.columnName);
|
|
455
|
+
if (fk) {
|
|
456
|
+
fk.validatedByLLM = true;
|
|
457
|
+
fk.confidence = Math.min(fk.confidence + 20, 100); // Bigger boost for FKs
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (rec.type === 'reject') {
|
|
462
|
+
rejected.push(recId);
|
|
463
|
+
// Remove or downgrade confidence
|
|
464
|
+
if (rec.target === 'pk') {
|
|
465
|
+
const pkIndex = updatedPKs.findIndex(p => p.schemaName === rec.schemaName &&
|
|
466
|
+
p.tableName === rec.tableName &&
|
|
467
|
+
p.columnNames.includes(rec.columnName));
|
|
468
|
+
if (pkIndex >= 0) {
|
|
469
|
+
updatedPKs[pkIndex].status = 'rejected';
|
|
470
|
+
updatedPKs.splice(pkIndex, 1); // Remove rejected
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (rec.target === 'fk') {
|
|
474
|
+
const fkIndex = updatedFKs.findIndex(f => f.schemaName === rec.schemaName &&
|
|
475
|
+
f.sourceTable === rec.tableName &&
|
|
476
|
+
f.sourceColumn === rec.columnName);
|
|
477
|
+
if (fkIndex >= 0) {
|
|
478
|
+
updatedFKs[fkIndex].status = 'rejected';
|
|
479
|
+
updatedFKs.splice(fkIndex, 1); // Remove rejected
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
this.onProgress(`Validated ${tableKey}`, {
|
|
485
|
+
reasoning: result.reasoning.substring(0, 100) + '...'
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
this.onProgress(`Error validating ${tableKey}: ${error.message}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
updatedPKs,
|
|
494
|
+
updatedFKs,
|
|
495
|
+
validated,
|
|
496
|
+
rejected,
|
|
497
|
+
tokensUsed
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Check if backpropagation should be triggered
|
|
502
|
+
*/
|
|
503
|
+
shouldTriggerBackprop(iteration, phase) {
|
|
504
|
+
// Don't trigger on first iteration
|
|
505
|
+
if (iteration.iteration === 1) {
|
|
506
|
+
return { shouldTrigger: false };
|
|
507
|
+
}
|
|
508
|
+
// Check if significant new discoveries were made
|
|
509
|
+
const significantPKs = iteration.discoveries.newPKs.filter(pk => pk.confidence >= 80).length;
|
|
510
|
+
const significantFKs = iteration.discoveries.newFKs.filter(fk => fk.confidence >= 80).length;
|
|
511
|
+
if (significantPKs > 0 || significantFKs > 0) {
|
|
512
|
+
return {
|
|
513
|
+
shouldTrigger: true,
|
|
514
|
+
reason: `Discovered ${significantPKs} high-confidence PKs and ${significantFKs} high-confidence FKs`
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return { shouldTrigger: false };
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Perform backpropagation to update confidence scores
|
|
521
|
+
*/
|
|
522
|
+
async performBackpropagation(phase, newPKs, newFKs) {
|
|
523
|
+
const changes = [];
|
|
524
|
+
// Update FK confidence if their target is now a confirmed PK
|
|
525
|
+
for (const fk of phase.discovered.foreignKeys) {
|
|
526
|
+
const targetIsNowPK = newPKs.some(pk => pk.schemaName === fk.targetSchema &&
|
|
527
|
+
pk.tableName === fk.targetTable &&
|
|
528
|
+
pk.columnNames.includes(fk.targetColumn) &&
|
|
529
|
+
pk.confidence >= 80);
|
|
530
|
+
if (targetIsNowPK && fk.confidence < 90) {
|
|
531
|
+
const oldConfidence = fk.confidence;
|
|
532
|
+
// Boost FK confidence if pointing to a confirmed PK
|
|
533
|
+
fk.confidence = Math.min(fk.confidence + 15, 95);
|
|
534
|
+
changes.push({
|
|
535
|
+
id: `${fk.schemaName}.${fk.sourceTable}.${fk.sourceColumn}`,
|
|
536
|
+
oldConfidence,
|
|
537
|
+
newConfidence: fk.confidence,
|
|
538
|
+
reason: 'Target column confirmed as PK'
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Update PK confidence if columns have FKs pointing to them
|
|
543
|
+
for (const pk of phase.discovered.primaryKeys) {
|
|
544
|
+
const incomingFKs = newFKs.filter(fk => fk.targetSchema === pk.schemaName &&
|
|
545
|
+
fk.targetTable === pk.tableName &&
|
|
546
|
+
pk.columnNames.includes(fk.targetColumn)).length;
|
|
547
|
+
if (incomingFKs > 0 && pk.confidence < 95) {
|
|
548
|
+
const oldConfidence = pk.confidence;
|
|
549
|
+
// Boost PK confidence if other tables reference it
|
|
550
|
+
pk.confidence = Math.min(pk.confidence + (incomingFKs * 5), 95);
|
|
551
|
+
changes.push({
|
|
552
|
+
id: `${pk.schemaName}.${pk.tableName}.${pk.columnNames.join('+')}`,
|
|
553
|
+
oldConfidence,
|
|
554
|
+
newConfidence: pk.confidence,
|
|
555
|
+
reason: `${incomingFKs} FK(s) now reference this column`
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return changes;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Merge iteration discoveries into phase
|
|
563
|
+
*/
|
|
564
|
+
mergeDiscoveries(phase, iteration) {
|
|
565
|
+
// Add new PKs (no duplicates)
|
|
566
|
+
for (const newPK of iteration.discoveries.newPKs) {
|
|
567
|
+
const exists = phase.discovered.primaryKeys.some(pk => pk.schemaName === newPK.schemaName &&
|
|
568
|
+
pk.tableName === newPK.tableName &&
|
|
569
|
+
pk.columnNames.join(',') === newPK.columnNames.join(','));
|
|
570
|
+
if (!exists) {
|
|
571
|
+
phase.discovered.primaryKeys.push(newPK);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Add new FKs (no duplicates)
|
|
575
|
+
for (const newFK of iteration.discoveries.newFKs) {
|
|
576
|
+
const exists = phase.discovered.foreignKeys.some(fk => fk.schemaName === newFK.schemaName &&
|
|
577
|
+
fk.sourceTable === newFK.sourceTable &&
|
|
578
|
+
fk.sourceColumn === newFK.sourceColumn &&
|
|
579
|
+
fk.targetSchema === newFK.targetSchema &&
|
|
580
|
+
fk.targetTable === newFK.targetTable &&
|
|
581
|
+
fk.targetColumn === newFK.targetColumn);
|
|
582
|
+
if (!exists) {
|
|
583
|
+
phase.discovered.foreignKeys.push(newFK);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Check if discovery should continue
|
|
589
|
+
*/
|
|
590
|
+
shouldContinueDiscovery(iteration, phase, iterationNumber) {
|
|
591
|
+
// Always run at least 2 iterations
|
|
592
|
+
if (iterationNumber < 2) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
// Stop if no new discoveries
|
|
596
|
+
if (iteration.discoveries.newPKs.length === 0 &&
|
|
597
|
+
iteration.discoveries.newFKs.length === 0 &&
|
|
598
|
+
iteration.discoveries.confidenceChanges.length === 0) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
// Stop if only low-confidence discoveries
|
|
602
|
+
const hasHighConfidenceDiscoveries = iteration.discoveries.newPKs.some(pk => pk.confidence >= 70) ||
|
|
603
|
+
iteration.discoveries.newFKs.some(fk => fk.confidence >= 60);
|
|
604
|
+
if (!hasHighConfidenceDiscoveries) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Check guardrails
|
|
611
|
+
*/
|
|
612
|
+
checkGuardrails(tokensUsed, maxTokens, iteration) {
|
|
613
|
+
// Check token budget
|
|
614
|
+
if (tokensUsed >= maxTokens) {
|
|
615
|
+
return {
|
|
616
|
+
canContinue: false,
|
|
617
|
+
reason: `Token budget exhausted (${tokensUsed}/${maxTokens})`
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
// Check max iterations
|
|
621
|
+
if (iteration > this.config.backpropagation.maxIterations) {
|
|
622
|
+
return {
|
|
623
|
+
canContinue: false,
|
|
624
|
+
reason: `Max iterations reached (${this.config.backpropagation.maxIterations})`
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
// Check if we're within warning threshold (80%)
|
|
628
|
+
const tokenPercentage = tokensUsed / maxTokens;
|
|
629
|
+
if (tokenPercentage > 0.8) {
|
|
630
|
+
this.onProgress('Warning: Token budget 80% consumed', {
|
|
631
|
+
used: tokensUsed,
|
|
632
|
+
total: maxTokens
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
return { canContinue: true };
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Calculate final summary statistics
|
|
639
|
+
*/
|
|
640
|
+
calculateSummary(phase) {
|
|
641
|
+
const allPKs = phase.discovered.primaryKeys;
|
|
642
|
+
const allFKs = phase.discovered.foreignKeys;
|
|
643
|
+
const allDiscoveries = [...allPKs, ...allFKs];
|
|
644
|
+
// Count by confidence level
|
|
645
|
+
const highConfidence = allDiscoveries.filter(d => d.confidence >= 80).length;
|
|
646
|
+
const mediumConfidence = allDiscoveries.filter(d => d.confidence >= 50 && d.confidence < 80).length;
|
|
647
|
+
const lowConfidence = allDiscoveries.filter(d => d.confidence < 50).length;
|
|
648
|
+
const rejected = allDiscoveries.filter(d => d.status === 'rejected').length;
|
|
649
|
+
// Calculate average confidence
|
|
650
|
+
const totalConfidence = allDiscoveries.reduce((sum, d) => sum + d.confidence, 0);
|
|
651
|
+
const averageConfidence = allDiscoveries.length > 0
|
|
652
|
+
? totalConfidence / allDiscoveries.length
|
|
653
|
+
: 0;
|
|
654
|
+
// Count unique tables with discovered PKs
|
|
655
|
+
const tablesWithPKs = new Set(allPKs.map(pk => `${pk.schemaName}.${pk.tableName}`)).size;
|
|
656
|
+
// Count total tables analyzed
|
|
657
|
+
const tablesAnalyzed = new Set();
|
|
658
|
+
for (const schema of this.schemas) {
|
|
659
|
+
for (const table of schema.tables) {
|
|
660
|
+
tablesAnalyzed.add(`${schema.name}.${table.name}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
phase.summary = {
|
|
664
|
+
totalTablesAnalyzed: tablesAnalyzed.size,
|
|
665
|
+
tablesWithDiscoveredPKs: tablesWithPKs,
|
|
666
|
+
relationshipsDiscovered: allFKs.length,
|
|
667
|
+
averageConfidence: Math.round(averageConfidence),
|
|
668
|
+
highConfidenceCount: highConfidence,
|
|
669
|
+
mediumConfidenceCount: mediumConfidence,
|
|
670
|
+
lowConfidenceCount: lowConfidence,
|
|
671
|
+
rejectedCount: rejected
|
|
672
|
+
};
|
|
673
|
+
phase.schemaEnhancements = {
|
|
674
|
+
pkeysAdded: allPKs.filter(pk => pk.status === 'confirmed').length,
|
|
675
|
+
fkeysAdded: allFKs.filter(fk => fk.status === 'confirmed').length,
|
|
676
|
+
overallConfidence: Math.round(averageConfidence)
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Apply discovered relationships to state
|
|
681
|
+
*/
|
|
682
|
+
applyDiscoveriesToState(state, phase) {
|
|
683
|
+
// Apply high-confidence PKs
|
|
684
|
+
for (const pk of phase.discovered.primaryKeys) {
|
|
685
|
+
if (pk.confidence >= this.config.confidence.primaryKeyMinimum * 100) {
|
|
686
|
+
const schema = state.schemas.find(s => s.name === pk.schemaName);
|
|
687
|
+
if (!schema)
|
|
688
|
+
continue;
|
|
689
|
+
const table = schema.tables.find(t => t.name === pk.tableName);
|
|
690
|
+
if (!table)
|
|
691
|
+
continue;
|
|
692
|
+
for (const columnName of pk.columnNames) {
|
|
693
|
+
const column = table.columns.find(c => c.name === columnName);
|
|
694
|
+
if (column) {
|
|
695
|
+
column.isPrimaryKey = true;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Apply high-confidence FKs
|
|
701
|
+
for (const fk of phase.discovered.foreignKeys) {
|
|
702
|
+
if (fk.confidence >= this.config.confidence.foreignKeyMinimum * 100) {
|
|
703
|
+
const schema = state.schemas.find(s => s.name === fk.schemaName);
|
|
704
|
+
if (!schema)
|
|
705
|
+
continue;
|
|
706
|
+
const table = schema.tables.find(t => t.name === fk.sourceTable);
|
|
707
|
+
if (!table)
|
|
708
|
+
continue;
|
|
709
|
+
const column = table.columns.find(c => c.name === fk.sourceColumn);
|
|
710
|
+
if (column) {
|
|
711
|
+
column.isForeignKey = true;
|
|
712
|
+
column.foreignKeyReferences = {
|
|
713
|
+
schema: fk.targetSchema,
|
|
714
|
+
table: fk.targetTable,
|
|
715
|
+
column: fk.targetColumn,
|
|
716
|
+
referencedColumn: fk.targetColumn
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Store discovery phase in state (new phases structure)
|
|
722
|
+
state.phases.keyDetection = phase;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
exports.DiscoveryEngine = DiscoveryEngine;
|
|
726
|
+
//# sourceMappingURL=DiscoveryEngine.js.map
|