@memberjunction/db-auto-doc 6.2.0-edge.4 → 6.2.0-edge.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +105 -0
  2. package/dist/api/DBAutoDocAPI.d.ts +2 -0
  3. package/dist/api/DBAutoDocAPI.d.ts.map +1 -1
  4. package/dist/api/DBAutoDocAPI.js +2 -1
  5. package/dist/api/DBAutoDocAPI.js.map +1 -1
  6. package/dist/commands/analyze.d.ts +1 -0
  7. package/dist/commands/analyze.d.ts.map +1 -1
  8. package/dist/commands/analyze.js +6 -0
  9. package/dist/commands/analyze.js.map +1 -1
  10. package/dist/commands/export.d.ts +28 -19
  11. package/dist/commands/export.d.ts.map +1 -1
  12. package/dist/commands/export.js +91 -12
  13. package/dist/commands/export.js.map +1 -1
  14. package/dist/core/AnalysisEngine.d.ts +39 -6
  15. package/dist/core/AnalysisEngine.d.ts.map +1 -1
  16. package/dist/core/AnalysisEngine.js +144 -50
  17. package/dist/core/AnalysisEngine.js.map +1 -1
  18. package/dist/core/AnalysisOrchestrator.d.ts +20 -1
  19. package/dist/core/AnalysisOrchestrator.d.ts.map +1 -1
  20. package/dist/core/AnalysisOrchestrator.js +144 -28
  21. package/dist/core/AnalysisOrchestrator.js.map +1 -1
  22. package/dist/core/PKProposalGate.d.ts +22 -0
  23. package/dist/core/PKProposalGate.d.ts.map +1 -0
  24. package/dist/core/PKProposalGate.js +52 -0
  25. package/dist/core/PKProposalGate.js.map +1 -0
  26. package/dist/database/Database.d.ts +8 -0
  27. package/dist/database/Database.d.ts.map +1 -1
  28. package/dist/database/Database.js +23 -1
  29. package/dist/database/Database.js.map +1 -1
  30. package/dist/discovery/ColumnNormalizer.d.ts +31 -0
  31. package/dist/discovery/ColumnNormalizer.d.ts.map +1 -1
  32. package/dist/discovery/ColumnNormalizer.js +68 -2
  33. package/dist/discovery/ColumnNormalizer.js.map +1 -1
  34. package/dist/discovery/Composer.d.ts +33 -2
  35. package/dist/discovery/Composer.d.ts.map +1 -1
  36. package/dist/discovery/Composer.js +126 -5
  37. package/dist/discovery/Composer.js.map +1 -1
  38. package/dist/discovery/DiscoveryEngine.d.ts +3 -0
  39. package/dist/discovery/DiscoveryEngine.d.ts.map +1 -1
  40. package/dist/discovery/DiscoveryEngine.js +1 -1
  41. package/dist/discovery/DiscoveryEngine.js.map +1 -1
  42. package/dist/discovery/EmbeddingProvider.d.ts +6 -1
  43. package/dist/discovery/EmbeddingProvider.d.ts.map +1 -1
  44. package/dist/discovery/EmbeddingProvider.js +11 -1
  45. package/dist/discovery/EmbeddingProvider.js.map +1 -1
  46. package/dist/discovery/EnumCandidateGate.d.ts +19 -0
  47. package/dist/discovery/EnumCandidateGate.d.ts.map +1 -1
  48. package/dist/discovery/EnumCandidateGate.js +47 -5
  49. package/dist/discovery/EnumCandidateGate.js.map +1 -1
  50. package/dist/discovery/FKGraphWalker.d.ts +35 -0
  51. package/dist/discovery/FKGraphWalker.d.ts.map +1 -1
  52. package/dist/discovery/FKGraphWalker.js +113 -12
  53. package/dist/discovery/FKGraphWalker.js.map +1 -1
  54. package/dist/discovery/IndexKeyEvidence.d.ts +36 -0
  55. package/dist/discovery/IndexKeyEvidence.d.ts.map +1 -0
  56. package/dist/discovery/IndexKeyEvidence.js +42 -0
  57. package/dist/discovery/IndexKeyEvidence.js.map +1 -0
  58. package/dist/discovery/JoinProbe.d.ts +148 -0
  59. package/dist/discovery/JoinProbe.d.ts.map +1 -0
  60. package/dist/discovery/JoinProbe.js +233 -0
  61. package/dist/discovery/JoinProbe.js.map +1 -0
  62. package/dist/discovery/LLMDiscoveryValidator.d.ts +10 -1
  63. package/dist/discovery/LLMDiscoveryValidator.d.ts.map +1 -1
  64. package/dist/discovery/LLMDiscoveryValidator.js +44 -12
  65. package/dist/discovery/LLMDiscoveryValidator.js.map +1 -1
  66. package/dist/discovery/LLMSanityChecker.d.ts.map +1 -1
  67. package/dist/discovery/LLMSanityChecker.js +10 -1
  68. package/dist/discovery/LLMSanityChecker.js.map +1 -1
  69. package/dist/discovery/OrganicKeyDetector.d.ts +29 -1
  70. package/dist/discovery/OrganicKeyDetector.d.ts.map +1 -1
  71. package/dist/discovery/OrganicKeyDetector.js +34 -5
  72. package/dist/discovery/OrganicKeyDetector.js.map +1 -1
  73. package/dist/discovery/OrganicKeyTranslator.d.ts +24 -5
  74. package/dist/discovery/OrganicKeyTranslator.d.ts.map +1 -1
  75. package/dist/discovery/OrganicKeyTranslator.js +32 -14
  76. package/dist/discovery/OrganicKeyTranslator.js.map +1 -1
  77. package/dist/discovery/PKDetector.d.ts +9 -0
  78. package/dist/discovery/PKDetector.d.ts.map +1 -1
  79. package/dist/discovery/PKDetector.js +86 -8
  80. package/dist/discovery/PKDetector.js.map +1 -1
  81. package/dist/discovery/SemanticPhase.d.ts +25 -0
  82. package/dist/discovery/SemanticPhase.d.ts.map +1 -1
  83. package/dist/discovery/SemanticPhase.js +55 -11
  84. package/dist/discovery/SemanticPhase.js.map +1 -1
  85. package/dist/discovery/StructuralPhase.d.ts +27 -5
  86. package/dist/discovery/StructuralPhase.d.ts.map +1 -1
  87. package/dist/discovery/StructuralPhase.js +42 -9
  88. package/dist/discovery/StructuralPhase.js.map +1 -1
  89. package/dist/discovery/TransitiveBridgeDetector.d.ts +5 -1
  90. package/dist/discovery/TransitiveBridgeDetector.d.ts.map +1 -1
  91. package/dist/discovery/TransitiveBridgeDetector.js +25 -3
  92. package/dist/discovery/TransitiveBridgeDetector.js.map +1 -1
  93. package/dist/discovery/embedding-provider-default.d.ts +25 -0
  94. package/dist/discovery/embedding-provider-default.d.ts.map +1 -0
  95. package/dist/discovery/embedding-provider-default.js +47 -0
  96. package/dist/discovery/embedding-provider-default.js.map +1 -0
  97. package/dist/drivers/BaseAutoDocDriver.d.ts +63 -0
  98. package/dist/drivers/BaseAutoDocDriver.d.ts.map +1 -1
  99. package/dist/drivers/BaseAutoDocDriver.js +3 -5
  100. package/dist/drivers/BaseAutoDocDriver.js.map +1 -1
  101. package/dist/drivers/MySQLDriver.d.ts +21 -1
  102. package/dist/drivers/MySQLDriver.d.ts.map +1 -1
  103. package/dist/drivers/MySQLDriver.js +64 -0
  104. package/dist/drivers/MySQLDriver.js.map +1 -1
  105. package/dist/drivers/PostgreSQLDriver.d.ts +43 -2
  106. package/dist/drivers/PostgreSQLDriver.d.ts.map +1 -1
  107. package/dist/drivers/PostgreSQLDriver.js +132 -19
  108. package/dist/drivers/PostgreSQLDriver.js.map +1 -1
  109. package/dist/drivers/SQLServerDriver.d.ts +28 -1
  110. package/dist/drivers/SQLServerDriver.d.ts.map +1 -1
  111. package/dist/drivers/SQLServerDriver.js +108 -1
  112. package/dist/drivers/SQLServerDriver.js.map +1 -1
  113. package/dist/drivers/probeErrors.d.ts +30 -0
  114. package/dist/drivers/probeErrors.d.ts.map +1 -0
  115. package/dist/drivers/probeErrors.js +94 -0
  116. package/dist/drivers/probeErrors.js.map +1 -0
  117. package/dist/generators/AdditionalSchemaInfoGenerator.d.ts +34 -18
  118. package/dist/generators/AdditionalSchemaInfoGenerator.d.ts.map +1 -1
  119. package/dist/generators/AdditionalSchemaInfoGenerator.js +130 -8
  120. package/dist/generators/AdditionalSchemaInfoGenerator.js.map +1 -1
  121. package/dist/generators/ErdClusterGenerator.d.ts +44 -0
  122. package/dist/generators/ErdClusterGenerator.d.ts.map +1 -0
  123. package/dist/generators/ErdClusterGenerator.js +216 -0
  124. package/dist/generators/ErdClusterGenerator.js.map +1 -0
  125. package/dist/generators/ErdClusterer.d.ts +130 -0
  126. package/dist/generators/ErdClusterer.d.ts.map +1 -0
  127. package/dist/generators/ErdClusterer.js +308 -0
  128. package/dist/generators/ErdClusterer.js.map +1 -0
  129. package/dist/generators/HTMLGenerator.d.ts +6 -0
  130. package/dist/generators/HTMLGenerator.d.ts.map +1 -1
  131. package/dist/generators/HTMLGenerator.js +41 -6
  132. package/dist/generators/HTMLGenerator.js.map +1 -1
  133. package/dist/generators/MermaidGenerator.d.ts +3 -0
  134. package/dist/generators/MermaidGenerator.d.ts.map +1 -1
  135. package/dist/generators/MermaidGenerator.js +4 -0
  136. package/dist/generators/MermaidGenerator.js.map +1 -1
  137. package/dist/generators/index.d.ts +2 -0
  138. package/dist/generators/index.d.ts.map +1 -1
  139. package/dist/generators/index.js +2 -0
  140. package/dist/generators/index.js.map +1 -1
  141. package/dist/prompts/PromptEngine.d.ts.map +1 -1
  142. package/dist/prompts/PromptEngine.js +10 -1
  143. package/dist/prompts/PromptEngine.js.map +1 -1
  144. package/dist/types/analysis.d.ts +2 -0
  145. package/dist/types/analysis.d.ts.map +1 -1
  146. package/dist/types/config.d.ts +128 -4
  147. package/dist/types/config.d.ts.map +1 -1
  148. package/dist/types/config.js +7 -1
  149. package/dist/types/config.js.map +1 -1
  150. package/dist/types/discovery.d.ts +41 -0
  151. package/dist/types/discovery.d.ts.map +1 -1
  152. package/dist/types/state.d.ts +29 -0
  153. package/dist/types/state.d.ts.map +1 -1
  154. package/dist/utils/call-deadline.d.ts +43 -0
  155. package/dist/utils/call-deadline.d.ts.map +1 -0
  156. package/dist/utils/call-deadline.js +81 -0
  157. package/dist/utils/call-deadline.js.map +1 -0
  158. package/dist/utils/config-loader.d.ts +5 -0
  159. package/dist/utils/config-loader.d.ts.map +1 -1
  160. package/dist/utils/config-loader.js +37 -0
  161. package/dist/utils/config-loader.js.map +1 -1
  162. package/dist/utils/index-hints.d.ts +43 -0
  163. package/dist/utils/index-hints.d.ts.map +1 -0
  164. package/dist/utils/index-hints.js +80 -0
  165. package/dist/utils/index-hints.js.map +1 -0
  166. package/dist/utils/index.d.ts +2 -0
  167. package/dist/utils/index.d.ts.map +1 -1
  168. package/dist/utils/index.js +2 -0
  169. package/dist/utils/index.js.map +1 -1
  170. package/dist/utils/llm-factory.d.ts +1 -0
  171. package/dist/utils/llm-factory.d.ts.map +1 -1
  172. package/dist/utils/llm-factory.js +6 -1
  173. package/dist/utils/llm-factory.js.map +1 -1
  174. package/dist/utils/seed-context.d.ts +23 -0
  175. package/dist/utils/seed-context.d.ts.map +1 -0
  176. package/dist/utils/seed-context.js +38 -0
  177. package/dist/utils/seed-context.js.map +1 -0
  178. package/package.json +5 -5
  179. package/prompts/fk-pruning-holistic.md +7 -0
  180. package/prompts/fk-pruning-table.md +20 -1
  181. package/prompts/pk-pruning-holistic.md +7 -0
  182. package/prompts/pk-pruning-table.md +17 -0
  183. package/prompts/table-analysis.md +11 -0
@@ -2,8 +2,11 @@
2
2
  * Main analysis orchestrator
3
3
  * Coordinates the entire documentation generation workflow
4
4
  */
5
- import { DatabaseDocumentation, AnalysisRun, SchemaDefinition } from '../types/state.js';
5
+ import { DatabaseDocumentation, AnalysisRun, SchemaDefinition, TableDefinition } from '../types/state.js';
6
+ import type { BaseAutoDocDriver } from "../drivers/BaseAutoDocDriver.js";
6
7
  import { TableNode, BackpropagationTrigger } from '../types/analysis.js';
8
+ import { KeyVerifier } from '../discovery/JoinProbe.js';
9
+ import { ColumnDescriptionPromptResult } from '../types/prompts.js';
7
10
  import { DBAutoDocConfig } from '../types/config.js';
8
11
  import { PromptEngine } from '../prompts/PromptEngine.js';
9
12
  import { StateManager } from '../state/StateManager.js';
@@ -13,6 +16,8 @@ export declare class AnalysisEngine {
13
16
  private promptEngine;
14
17
  private stateManager;
15
18
  private iterationTracker;
19
+ /** Database access for checks the LLM's proposals must pass (composite PK uniqueness). Optional: without it, composite PK proposals are rejected. */
20
+ private driver?;
16
21
  private backpropagationEngine;
17
22
  private convergenceDetector;
18
23
  private guardrailsManager;
@@ -20,7 +25,22 @@ export declare class AnalysisEngine {
20
25
  private startTime;
21
26
  private currentRun?;
22
27
  private onProgress;
23
- constructor(config: DBAutoDocConfig, promptEngine: PromptEngine, stateManager: StateManager, iterationTracker: IterationTracker, onProgress?: (message: string, data?: Record<string, unknown>) => void);
28
+ /**
29
+ * Verifies a proposed key against the data before it is confirmed. Optional: when
30
+ * absent, LLM-proposed FKs are still emitted but stamped `Unprobed` rather than
31
+ * silently presented as though someone had checked them.
32
+ */
33
+ private keyVerifier;
34
+ /**
35
+ * Supply the probe used to verify LLM-proposed foreign keys. Injected rather than
36
+ * constructed here because the engine has no driver of its own; the orchestrator owns
37
+ * the connection and passes ONE verifier so the per-run probe budget is shared with
38
+ * the organic-key path instead of each path getting its own allowance.
39
+ */
40
+ SetKeyVerifier(verifier: KeyVerifier | null): void;
41
+ constructor(config: DBAutoDocConfig, promptEngine: PromptEngine, stateManager: StateManager, iterationTracker: IterationTracker, onProgress?: (message: string, data?: Record<string, unknown>) => void,
42
+ /** Database access for checks the LLM's proposals must pass (composite PK uniqueness). Optional: without it, composite PK proposals are rejected. */
43
+ driver?: BaseAutoDocDriver);
24
44
  /**
25
45
  * Initialize timing for guardrails and set current run
26
46
  */
@@ -99,6 +119,8 @@ export declare class AnalysisEngine {
99
119
  * Build context for table analysis
100
120
  */
101
121
  private buildTableContext;
122
+ /** The table's existing indexes as prompt lines, or undefined when there are none to show. */
123
+ private indexHintLines;
102
124
  /**
103
125
  * Build FK candidate stats from the discovery phase for this table.
104
126
  * Provides the LLM with cross-table relationship evidence (value overlap,
@@ -109,7 +131,7 @@ export declare class AnalysisEngine {
109
131
  * Persist enum/value-list verdicts from LLM response onto ColumnDefinition.
110
132
  * Keeps the highest-confidence verdict across multiple iterations.
111
133
  */
112
- private persistEnumVerdicts;
134
+ protected persistEnumVerdicts(table: TableDefinition, columnDescriptions: ColumnDescriptionPromptResult[] | undefined, modelUsed: string): void;
113
135
  /**
114
136
  * Sanitize LLM-returned enum values: trim whitespace, drop empties and null sentinels.
115
137
  */
@@ -177,9 +199,10 @@ export declare class AnalysisEngine {
177
199
  */
178
200
  private extractAndFeedbackFKInsights;
179
201
  /**
180
- * Process PK proposal from LLM. The LLM can propose a PK, but ALL proposed columns
181
- * must pass deterministic eligibility: zero nulls, zero blanks, 100% unique values.
182
- * If any column fails, the entire proposal is rejected.
202
+ * Process PK proposal from LLM. The LLM can propose a PK, but it must pass deterministic
203
+ * eligibility: every column exists and has no nulls, and the key is unique — a single column
204
+ * by its own statistics, a composite key as a combination (checked in the database).
205
+ * If any check fails, the entire proposal is rejected.
183
206
  */
184
207
  private processPKInsightFromLLM;
185
208
  /**
@@ -188,6 +211,16 @@ export declare class AnalysisEngine {
188
211
  * Uses the foreignKeys array from table-analysis prompt response instead of brittle regex parsing.
189
212
  * Per architectural decision: use LLM for language understanding, deterministic code for processing.
190
213
  */
214
+ /**
215
+ * Probe an LLM-proposed foreign key.
216
+ *
217
+ * Returns `Unprobed` rather than throwing for every reason a probe can fail to run —
218
+ * no verifier injected, no such table or column in the analysed state, or the driver
219
+ * reporting that it could not evaluate the join. `Refuted` is reserved for a probe
220
+ * that RAN and measured containment below the floor, because only that is evidence
221
+ * the key is wrong.
222
+ */
223
+ private verifyProposedFK;
191
224
  private processFKInsightsFromLLM;
192
225
  /**
193
226
  * Find a column in the state schemas
@@ -1 +1 @@
1
- {"version":3,"file":"AnalysisEngine.d.ts","sourceRoot":"","sources":["../../src/core/AnalysisEngine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AnalysisEngine.d.ts","sourceRoot":"","sources":["../../src/core/AnalysisEngine.ts"],"names":[],"mappings":"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"}
@@ -3,17 +3,39 @@
3
3
  * Coordinates the entire documentation generation workflow
4
4
  */
5
5
  import { EnsureArray } from "../utils/ensureArray.js";
6
+ import { CheckPKProposal } from "./PKProposalGate.js";
7
+ import { BuildIndexHints, FormatIndexHint, IsUniquelyIndexed } from "../utils/index-hints.js";
8
+ import { SeedContextExcerpt } from "../utils/seed-context.js";
6
9
  import { EnumCandidateGate } from '../discovery/EnumCandidateGate.js';
10
+ import { StampFor } from '../discovery/JoinProbe.js';
7
11
  import { BackpropagationEngine } from './BackpropagationEngine.js';
8
12
  import { ConvergenceDetector } from './ConvergenceDetector.js';
9
13
  import { GuardrailsManager } from './GuardrailsManager.js';
10
14
  export class AnalysisEngine {
11
- constructor(config, promptEngine, stateManager, iterationTracker, onProgress) {
15
+ /**
16
+ * Supply the probe used to verify LLM-proposed foreign keys. Injected rather than
17
+ * constructed here because the engine has no driver of its own; the orchestrator owns
18
+ * the connection and passes ONE verifier so the per-run probe budget is shared with
19
+ * the organic-key path instead of each path getting its own allowance.
20
+ */
21
+ SetKeyVerifier(verifier) {
22
+ this.keyVerifier = verifier;
23
+ }
24
+ constructor(config, promptEngine, stateManager, iterationTracker, onProgress,
25
+ /** Database access for checks the LLM's proposals must pass (composite PK uniqueness). Optional: without it, composite PK proposals are rejected. */
26
+ driver) {
12
27
  this.config = config;
13
28
  this.promptEngine = promptEngine;
14
29
  this.stateManager = stateManager;
15
30
  this.iterationTracker = iterationTracker;
31
+ this.driver = driver;
16
32
  this.startTime = 0;
33
+ /**
34
+ * Verifies a proposed key against the data before it is confirmed. Optional: when
35
+ * absent, LLM-proposed FKs are still emitted but stamped `Unprobed` rather than
36
+ * silently presented as though someone had checked them.
37
+ */
38
+ this.keyVerifier = null;
17
39
  this.onProgress = onProgress || (() => { });
18
40
  this.backpropagationEngine = new BackpropagationEngine(promptEngine, stateManager, iterationTracker, config.analysis.backpropagation.maxDepth, config);
19
41
  this.convergenceDetector = new ConvergenceDetector(config.analysis.convergence, stateManager, iterationTracker);
@@ -128,7 +150,7 @@ export class AnalysisEngine {
128
150
  const [schemaName, tableName] = tableKey.split('.');
129
151
  const table = this.stateManager.findTable(state, schemaName, tableName);
130
152
  const candidates = tablePKs.map(pk => ({ columns: pk.columnNames, confidence: pk.confidence, locked: pk.status === 'confirmed' }));
131
- const context = { sourceSchema: schemaName, sourceTable: tableName, tableDescription: table?.description || '', allTables, candidates, seedContext: state.seedContext ?? this.config.seedContext };
153
+ const context = { sourceSchema: schemaName, sourceTable: tableName, tableDescription: table?.description || '', allTables, candidates, existingIndexes: this.indexHintLines(table), seedContext: state.seedContext ?? this.config.seedContext, seedExcerpt: SeedContextExcerpt(state.seedContext ?? this.config.seedContext, [tableName]) };
132
154
  const result = await this.promptEngine.executePrompt('pk-pruning-table', context, { responseFormat: 'JSON', temperature: override?.temperature ?? 0.05, maxTokens: override?.maxTokens ?? this.config.ai.maxTokens, modelOverride: override?.model, effortLevelOverride: override?.effortLevel });
133
155
  if (!result.success || !result.result) {
134
156
  console.log(`[AnalysisEngine] PK pruning failed for ${tableKey}: ${result.errorMessage}`);
@@ -154,7 +176,7 @@ export class AnalysisEngine {
154
176
  if (allProposals.length === 0)
155
177
  return { removed: 0, kept: allPKs.length };
156
178
  this.onProgress('PK pruning pass 2: holistic review', { proposals: allProposals.length, model: effectiveModel });
157
- const holisticContext = { allTables, proposals: allProposals.map(p => ({ sourceSchema: p.sourceSchema, sourceTable: p.sourceTable, columns: p.columns, confidence: p.confidence, reasoning: p.reasoning })), seedContext: state.seedContext ?? this.config.seedContext };
179
+ const holisticContext = { allTables, proposals: allProposals.map(p => ({ sourceSchema: p.sourceSchema, sourceTable: p.sourceTable, columns: p.columns, confidence: p.confidence, reasoning: p.reasoning })), seedContext: state.seedContext ?? this.config.seedContext, seedExcerpt: SeedContextExcerpt(state.seedContext ?? this.config.seedContext, []) };
158
180
  const holisticResult = await this.promptEngine.executePrompt('pk-pruning-holistic', holisticContext, { responseFormat: 'JSON', temperature: override?.temperature ?? 0.05, maxTokens: override?.maxTokens ?? this.config.ai.maxTokens, modelOverride: override?.model, effortLevelOverride: override?.effortLevel });
159
181
  let removed = 0;
160
182
  if (holisticResult.success && holisticResult.result) {
@@ -237,7 +259,8 @@ export class AnalysisEngine {
237
259
  targetTable: fk.targetTable,
238
260
  targetColumn: fk.targetColumn,
239
261
  confidence: fk.confidence,
240
- locked: fk.status === 'confirmed'
262
+ locked: fk.status === 'confirmed',
263
+ targetUniquelyIndexed: IsUniquelyIndexed(this.stateManager.findTable(state, fk.targetSchema, fk.targetTable), fk.targetColumn)
241
264
  }));
242
265
  const context = {
243
266
  sourceSchema: schemaName,
@@ -245,7 +268,9 @@ export class AnalysisEngine {
245
268
  tableDescription: table?.description || '',
246
269
  allTables,
247
270
  candidates,
248
- seedContext: state.seedContext ?? this.config.seedContext
271
+ existingIndexes: this.indexHintLines(table),
272
+ seedContext: state.seedContext ?? this.config.seedContext,
273
+ seedExcerpt: SeedContextExcerpt(state.seedContext ?? this.config.seedContext, [tableName])
249
274
  };
250
275
  const result = await this.promptEngine.executePrompt('fk-pruning-table', context, {
251
276
  responseFormat: 'JSON',
@@ -304,7 +329,8 @@ export class AnalysisEngine {
304
329
  confidence: p.confidence,
305
330
  reasoning: p.reasoning
306
331
  })),
307
- seedContext: state.seedContext ?? this.config.seedContext
332
+ seedContext: state.seedContext ?? this.config.seedContext,
333
+ seedExcerpt: SeedContextExcerpt(state.seedContext ?? this.config.seedContext, [])
308
334
  };
309
335
  const holisticResult = await this.promptEngine.executePrompt('fk-pruning-holistic', holisticContext, {
310
336
  responseFormat: 'JSON',
@@ -438,11 +464,11 @@ export class AnalysisEngine {
438
464
  this.persistEnumVerdicts(table, result.result.columnDescriptions, run.modelUsed);
439
465
  // Process structured FK insights from LLM and feed back to discovery phase
440
466
  if (state.phases.keyDetection && result.result.foreignKeys) {
441
- this.processFKInsightsFromLLM(state, tableNode.schema, tableNode.table, result.result.foreignKeys);
467
+ await this.processFKInsightsFromLLM(state, tableNode.schema, tableNode.table, result.result.foreignKeys);
442
468
  }
443
469
  // Process PK proposal from LLM — verify eligibility deterministically
444
470
  if (state.phases.keyDetection && result.result.primaryKey) {
445
- this.processPKInsightFromLLM(state, tableNode.schema, tableNode.table, result.result.primaryKey);
471
+ await this.processPKInsightFromLLM(state, tableNode.schema, tableNode.table, result.result.primaryKey);
446
472
  }
447
473
  // Update inferred business domain
448
474
  if (result.result.inferredBusinessDomain) {
@@ -535,9 +561,15 @@ export class AnalysisEngine {
535
561
  allTables,
536
562
  groundTruth: groundTruthContext,
537
563
  fkCandidateStats,
538
- enumCandidates: enumCandidates.length > 0 ? enumCandidates : undefined
564
+ enumCandidates: enumCandidates.length > 0 ? enumCandidates : undefined,
565
+ existingIndexes: this.indexHintLines(table)
539
566
  };
540
567
  }
568
+ /** The table's existing indexes as prompt lines, or undefined when there are none to show. */
569
+ indexHintLines(table) {
570
+ const lines = BuildIndexHints(table).map(FormatIndexHint);
571
+ return lines.length > 0 ? lines : undefined;
572
+ }
541
573
  /**
542
574
  * Build FK candidate stats from the discovery phase for this table.
543
575
  * Provides the LLM with cross-table relationship evidence (value overlap,
@@ -580,11 +612,16 @@ export class AnalysisEngine {
580
612
  // If sanitization drops all values below 2, skip the verdict
581
613
  if (sanitizedValues.length < 2 && vl.isEnum)
582
614
  continue;
615
+ // The prompt asks for a verdict only on enum candidates; enforce that here, and keep only
616
+ // values observed in the column, so a verdict on a non-candidate or an invented value is dropped.
617
+ const keptValues = this.enumCandidateGate.FilterVerdictValues(column, table.rowCount, sanitizedValues);
618
+ if (keptValues === null && vl.isEnum)
619
+ continue;
583
620
  const newVerdict = {
584
621
  isEnum: vl.isEnum,
585
622
  type: vl.type,
586
623
  confidence: vl.confidence,
587
- values: sanitizedValues,
624
+ values: keptValues ?? sanitizedValues,
588
625
  reasoning: vl.reasoning,
589
626
  source: 'llm',
590
627
  cardinalityAtDecision: column.statistics
@@ -967,11 +1004,12 @@ export class AnalysisEngine {
967
1004
  return;
968
1005
  }
969
1006
  /**
970
- * Process PK proposal from LLM. The LLM can propose a PK, but ALL proposed columns
971
- * must pass deterministic eligibility: zero nulls, zero blanks, 100% unique values.
972
- * If any column fails, the entire proposal is rejected.
1007
+ * Process PK proposal from LLM. The LLM can propose a PK, but it must pass deterministic
1008
+ * eligibility: every column exists and has no nulls, and the key is unique — a single column
1009
+ * by its own statistics, a composite key as a combination (checked in the database).
1010
+ * If any check fails, the entire proposal is rejected.
973
1011
  */
974
- processPKInsightFromLLM(state, schemaName, tableName, pkProposal) {
1012
+ async processPKInsightFromLLM(state, schemaName, tableName, pkProposal) {
975
1013
  const discoveryPhase = state.phases.keyDetection;
976
1014
  if (!discoveryPhase || !pkProposal || !pkProposal.columns || pkProposal.columns.length === 0)
977
1015
  return;
@@ -1016,47 +1054,29 @@ export class AnalysisEngine {
1016
1054
  }
1017
1055
  return;
1018
1056
  }
1019
- // New PK proposal — verify ALL columns are PK-eligible deterministically
1057
+ // New PK proposal — verify it deterministically
1020
1058
  const table = this.stateManager.findTable(state, schemaName, tableName);
1021
1059
  if (!table)
1022
1060
  return;
1023
- for (const colName of columns) {
1024
- const column = table.columns.find(c => c.name.toLowerCase() === colName.toLowerCase());
1025
- if (!column) {
1026
- console.log(`[AnalysisEngine] LLM PK rejected: ${schemaName}.${tableName} — column "${colName}" not found`);
1027
- return;
1028
- }
1029
- // Check PK eligibility from stats
1030
- const stats = column.statistics;
1031
- if (!stats) {
1032
- console.log(`[AnalysisEngine] LLM PK rejected: ${schemaName}.${tableName}.${colName} — no statistics available`);
1033
- return;
1034
- }
1035
- // Safety: prefer pre-computed uniquenessRatio if available (totalRows can be 0 due to field naming bug)
1036
- const uniqueness = stats.uniquenessRatio != null && stats.uniquenessRatio > 0
1037
- ? stats.uniquenessRatio
1038
- : (stats.totalRows > 0 ? stats.distinctCount / stats.totalRows : 0);
1039
- const hasNulls = (stats.nullCount || 0) > 0;
1040
- if (hasNulls) {
1041
- console.log(`[AnalysisEngine] LLM PK rejected: ${schemaName}.${tableName}.${colName} — has ${stats.nullCount} nulls`);
1042
- return;
1043
- }
1044
- if (uniqueness < 1.0) {
1045
- console.log(`[AnalysisEngine] LLM PK rejected: ${schemaName}.${tableName}.${colName} — uniqueness ${(uniqueness * 100).toFixed(1)}% (must be 100%)`);
1046
- return;
1047
- }
1061
+ const sampleSize = this.config.analysis.relationshipDiscovery?.sampling?.maxRowsPerTable ?? 1000;
1062
+ const driver = this.driver;
1063
+ const verdict = await CheckPKProposal(table, columns, driver ? (cols) => driver.checkColumnCombinationUniqueness(schemaName, tableName, cols, sampleSize) : undefined);
1064
+ if (!verdict.Eligible) {
1065
+ console.log(`[AnalysisEngine] LLM PK rejected: ${schemaName}.${tableName} ${verdict.Reason}`);
1066
+ return;
1048
1067
  }
1068
+ const pkColumns = verdict.Columns ?? columns;
1049
1069
  // All columns pass — create new PK candidate
1050
1070
  const newPK = {
1051
1071
  schemaName,
1052
1072
  tableName,
1053
- columnNames: columns,
1073
+ columnNames: pkColumns,
1054
1074
  confidence,
1055
1075
  evidence: {
1056
1076
  uniqueness: 1.0,
1057
1077
  nullCount: 0,
1058
1078
  totalRows: table.rowCount || 0,
1059
- dataPattern: columns.length > 1 ? 'composite' : 'unknown',
1079
+ dataPattern: pkColumns.length > 1 ? 'composite' : 'unknown',
1060
1080
  namingScore: 0.5,
1061
1081
  dataTypeScore: 0.8,
1062
1082
  warnings: ['Created from LLM proposal — passed deterministic eligibility']
@@ -1067,12 +1087,12 @@ export class AnalysisEngine {
1067
1087
  };
1068
1088
  discoveryPhase.discovered.primaryKeys.push(newPK);
1069
1089
  // Update column flags
1070
- for (const colName of columns) {
1090
+ for (const colName of pkColumns) {
1071
1091
  const column = this.findColumnInState(state, schemaName, tableName, colName);
1072
1092
  if (column)
1073
1093
  column.isPrimaryKey = true;
1074
1094
  }
1075
- console.log(`[AnalysisEngine] Created PK from LLM: ${schemaName}.${tableName} (${columns.join(', ')}) confidence: ${confidence}`);
1095
+ console.log(`[AnalysisEngine] Created PK from LLM: ${schemaName}.${tableName} (${pkColumns.join(', ')}) confidence: ${confidence}`);
1076
1096
  }
1077
1097
  /**
1078
1098
  * Process structured FK insights from LLM and create feedback to discovery phase
@@ -1080,7 +1100,39 @@ export class AnalysisEngine {
1080
1100
  * Uses the foreignKeys array from table-analysis prompt response instead of brittle regex parsing.
1081
1101
  * Per architectural decision: use LLM for language understanding, deterministic code for processing.
1082
1102
  */
1083
- processFKInsightsFromLLM(state, schemaName, tableName, foreignKeys) {
1103
+ /**
1104
+ * Probe an LLM-proposed foreign key.
1105
+ *
1106
+ * Returns `Unprobed` rather than throwing for every reason a probe can fail to run —
1107
+ * no verifier injected, no such table or column in the analysed state, or the driver
1108
+ * reporting that it could not evaluate the join. `Refuted` is reserved for a probe
1109
+ * that RAN and measured containment below the floor, because only that is evidence
1110
+ * the key is wrong.
1111
+ */
1112
+ async verifyProposedFK(childSchema, childTable, childColumn, parentSchema, parentTable, parentColumn) {
1113
+ const now = new Date().toISOString();
1114
+ if (!this.keyVerifier) {
1115
+ return {
1116
+ Status: 'Unprobed',
1117
+ Containment: null,
1118
+ Reason: 'no key verifier configured for this run',
1119
+ ProbedAt: now
1120
+ };
1121
+ }
1122
+ if (!parentColumn || !parentTable || !parentSchema) {
1123
+ return {
1124
+ Status: 'Unprobed',
1125
+ Containment: null,
1126
+ Reason: 'LLM did not name a complete target column',
1127
+ ProbedAt: now
1128
+ };
1129
+ }
1130
+ return this.keyVerifier.Verify({
1131
+ Child: { Schema: childSchema, Table: childTable, Column: childColumn },
1132
+ Parent: { Schema: parentSchema, Table: parentTable, Column: parentColumn }
1133
+ });
1134
+ }
1135
+ async processFKInsightsFromLLM(state, schemaName, tableName, foreignKeys) {
1084
1136
  const discoveryPhase = state.phases.keyDetection;
1085
1137
  if (!discoveryPhase || !foreignKeys || foreignKeys.length === 0)
1086
1138
  return;
@@ -1122,6 +1174,38 @@ export class AnalysisEngine {
1122
1174
  console.log(`[AnalysisEngine] Rejecting LLM FK suggestion for confirmed PK: ${schemaName}.${tableName}.${columnName} -> ${referencesSchema}.${referencesTable}.${referencesColumn}`);
1123
1175
  continue;
1124
1176
  }
1177
+ // ── Verify the proposed join before anything is confirmed ────────────────
1178
+ //
1179
+ // This is the gate that was missing. The statistical path (FKDetector) probes
1180
+ // and rejects below 75% containment at its GATE 6; this path never probed at
1181
+ // all — it wrote a literal `valueOverlap: 0` placeholder, set
1182
+ // `status: 'confirmed'`, and stamped `column.isForeignKey = true`. The
1183
+ // additionalSchemaInfo generator then emits from BOTH
1184
+ // `discovered.foreignKeys` (gated on confidence + status, never on overlap)
1185
+ // and `column.foreignKeyReferences` (gated on nothing at all), so an
1186
+ // LLM-proposed key reached the output through two independent unchecked doors.
1187
+ //
1188
+ // The shape this catches: a connector schema whose parent key is namespaced
1189
+ // `<product>:<id>` while children store the bare id. "A foreign key points at
1190
+ // the parent's primary key" is correct almost everywhere and unfalsifiable
1191
+ // from schema shape, so the LLM proposes it for every child table and every
1192
+ // one of them matches zero rows.
1193
+ const probe = await this.verifyProposedFK(schemaName, tableName, columnName, referencesSchema, referencesTable, referencesColumn);
1194
+ if (probe.Status === 'Refuted') {
1195
+ // Measured, not assumed: the probe ran and the join matches (near) nothing.
1196
+ // Dropping it here is the only place it can be dropped — once the column is
1197
+ // stamped, the introspected-FK path emits it with no gate to fail. It runs
1198
+ // before the PK block below, so a refuted proposal is not evidence against a
1199
+ // PK candidate either.
1200
+ console.log(`[AnalysisEngine] Rejecting LLM FK — probe refuted: ${schemaName}.${tableName}.${columnName} -> ${referencesSchema}.${referencesTable}.${referencesColumn} (${probe.Reason})`);
1201
+ this.onProgress(`Rejected LLM-proposed FK ${schemaName}.${tableName}.${columnName} → ${referencesTable}.${referencesColumn}: ${probe.Reason}`, { refutedKey: `${schemaName}.${tableName}.${columnName}` });
1202
+ continue;
1203
+ }
1204
+ // Verified or Unprobed both proceed. Unprobed proceeds deliberately: refusing
1205
+ // whenever the probe cannot run turns a permissions or timeout problem into
1206
+ // total key loss with no signal. It proceeds STAMPED, so a consumer can tell a
1207
+ // checked key from an unchecked one.
1208
+ const verificationStamp = StampFor('LLM', probe);
1125
1209
  if (existingPK && existingPK.status !== 'confirmed') {
1126
1210
  // PK exists but isn't confirmed — LLM suggesting it's an FK is evidence
1127
1211
  // that the PK detection was wrong. Reject the PK.
@@ -1140,6 +1224,7 @@ export class AnalysisEngine {
1140
1224
  existingFK.validatedByLLM = true;
1141
1225
  existingFK.status = 'confirmed';
1142
1226
  existingFK.confidence = Math.min(existingFK.confidence + 20, 100);
1227
+ existingFK.verification = verificationStamp;
1143
1228
  feedback.type = 'confidence_change';
1144
1229
  feedback.newConfidence = existingFK.confidence;
1145
1230
  feedback.affectedCandidates.push(`FK:${schemaName}.${tableName}.${columnName}`);
@@ -1169,17 +1254,26 @@ export class AnalysisEngine {
1169
1254
  confidence: Math.round(confidence * 100),
1170
1255
  evidence: {
1171
1256
  namingMatch: 0.9,
1172
- valueOverlap: 0,
1257
+ // Real measurements where the probe ran. These were hardcoded 0 / 0 / 0,
1258
+ // which is indistinguishable from a probe that ran and found nothing —
1259
+ // the exact conflation that let a schema of zero-match keys be emitted
1260
+ // with the same apparent confidence as a correct one.
1261
+ valueOverlap: probe.Containment ? probe.Containment.Containment : 0,
1173
1262
  cardinalityRatio: 0,
1174
1263
  dataTypeMatch: true,
1175
1264
  nullPercentage: 0,
1176
- sampleSize: 0,
1177
- orphanCount: 0,
1178
- warnings: ['Created from structured LLM output']
1265
+ sampleSize: probe.Containment ? probe.Containment.SampledValues : 0,
1266
+ orphanCount: probe.Containment
1267
+ ? probe.Containment.SampledValues - probe.Containment.MatchedValues
1268
+ : 0,
1269
+ warnings: probe.Status === 'Unprobed'
1270
+ ? ['Created from structured LLM output', `Join NOT verified: ${probe.Reason}`]
1271
+ : ['Created from structured LLM output', `Join verified: ${probe.Reason}`]
1179
1272
  },
1180
1273
  discoveredInIteration: 1,
1181
1274
  validatedByLLM: true,
1182
- status: 'confirmed'
1275
+ status: 'confirmed',
1276
+ verification: verificationStamp
1183
1277
  };
1184
1278
  discoveryPhase.discovered.foreignKeys.push(newFK);
1185
1279
  feedback.newConfidence = newFK.confidence;