@memberjunction/db-auto-doc 5.36.0 → 5.38.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 +31 -0
- package/dist/core/AnalysisOrchestrator.d.ts.map +1 -1
- package/dist/core/AnalysisOrchestrator.js +32 -2
- package/dist/core/AnalysisOrchestrator.js.map +1 -1
- package/dist/discovery/BridgeViewSQLGenerator.d.ts +67 -0
- package/dist/discovery/BridgeViewSQLGenerator.d.ts.map +1 -0
- package/dist/discovery/BridgeViewSQLGenerator.js +99 -0
- package/dist/discovery/BridgeViewSQLGenerator.js.map +1 -0
- package/dist/discovery/ColumnClusterer.d.ts +63 -0
- package/dist/discovery/ColumnClusterer.d.ts.map +1 -0
- package/dist/discovery/ColumnClusterer.js +205 -0
- package/dist/discovery/ColumnClusterer.js.map +1 -0
- package/dist/discovery/ColumnNormalizer.d.ts +106 -0
- package/dist/discovery/ColumnNormalizer.d.ts.map +1 -0
- package/dist/discovery/ColumnNormalizer.js +376 -0
- package/dist/discovery/ColumnNormalizer.js.map +1 -0
- package/dist/discovery/Composer.d.ts +59 -0
- package/dist/discovery/Composer.d.ts.map +1 -0
- package/dist/discovery/Composer.js +95 -0
- package/dist/discovery/Composer.js.map +1 -0
- package/dist/discovery/EmbeddingProvider.d.ts +27 -0
- package/dist/discovery/EmbeddingProvider.d.ts.map +1 -0
- package/dist/discovery/EmbeddingProvider.js +87 -0
- package/dist/discovery/EmbeddingProvider.js.map +1 -0
- package/dist/discovery/FKGraphWalker.d.ts +108 -0
- package/dist/discovery/FKGraphWalker.d.ts.map +1 -0
- package/dist/discovery/FKGraphWalker.js +169 -0
- package/dist/discovery/FKGraphWalker.js.map +1 -0
- package/dist/discovery/OrganicKeyDetector.d.ts +51 -0
- package/dist/discovery/OrganicKeyDetector.d.ts.map +1 -0
- package/dist/discovery/OrganicKeyDetector.js +78 -0
- package/dist/discovery/OrganicKeyDetector.js.map +1 -0
- package/dist/discovery/OrganicKeyTranslator.d.ts +78 -0
- package/dist/discovery/OrganicKeyTranslator.d.ts.map +1 -0
- package/dist/discovery/OrganicKeyTranslator.js +166 -0
- package/dist/discovery/OrganicKeyTranslator.js.map +1 -0
- package/dist/discovery/SemanticPhase.d.ts +70 -0
- package/dist/discovery/SemanticPhase.d.ts.map +1 -0
- package/dist/discovery/SemanticPhase.js +423 -0
- package/dist/discovery/SemanticPhase.js.map +1 -0
- package/dist/discovery/StructuralPhase.d.ts +24 -0
- package/dist/discovery/StructuralPhase.d.ts.map +1 -0
- package/dist/discovery/StructuralPhase.js +23 -0
- package/dist/discovery/StructuralPhase.js.map +1 -0
- package/dist/discovery/TransitiveBridgeDetector.d.ts +65 -0
- package/dist/discovery/TransitiveBridgeDetector.d.ts.map +1 -0
- package/dist/discovery/TransitiveBridgeDetector.js +244 -0
- package/dist/discovery/TransitiveBridgeDetector.js.map +1 -0
- package/dist/generators/AdditionalSchemaInfoGenerator.d.ts +12 -0
- package/dist/generators/AdditionalSchemaInfoGenerator.d.ts.map +1 -1
- package/dist/generators/AdditionalSchemaInfoGenerator.js +31 -0
- package/dist/generators/AdditionalSchemaInfoGenerator.js.map +1 -1
- package/dist/types/config.d.ts +71 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/organic-keys.d.ts +141 -0
- package/dist/types/organic-keys.d.ts.map +1 -0
- package/dist/types/organic-keys.js +27 -0
- package/dist/types/organic-keys.js.map +1 -0
- package/dist/types/state.d.ts +7 -0
- package/dist/types/state.d.ts.map +1 -1
- package/dist/utils/json.d.ts +40 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +141 -0
- package/dist/utils/json.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmbeddingProvider — thin wrapper over MemberJunction's `BaseEmbeddings` infrastructure.
|
|
3
|
+
*
|
|
4
|
+
* Embeddings are produced through the same MJ ClassFactory + driver pattern that
|
|
5
|
+
* `llm-factory` uses for LLMs (so DBAutoDoc stays coupled to MJ's AI stack rather
|
|
6
|
+
* than talking to provider REST endpoints directly). The concrete driver class is
|
|
7
|
+
* resolved from the provider name and instantiated with the supplied API key.
|
|
8
|
+
*
|
|
9
|
+
* Vectors are unit-normalized so the clustering step can use cosine distance
|
|
10
|
+
* directly regardless of whether the underlying model returns normalized output.
|
|
11
|
+
*/
|
|
12
|
+
import { BaseEmbeddings } from '@memberjunction/ai';
|
|
13
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
14
|
+
/** Provider name → registered MJ embedding driver class (see `@RegisterClass(BaseEmbeddings, ...)`). */
|
|
15
|
+
const PROVIDER_TO_DRIVER_CLASS = {
|
|
16
|
+
openai: 'OpenAIEmbedding',
|
|
17
|
+
mistral: 'MistralEmbedding',
|
|
18
|
+
azure: 'AzureEmbedding',
|
|
19
|
+
bedrock: 'BedrockEmbedding',
|
|
20
|
+
ollama: 'OllamaEmbedding',
|
|
21
|
+
local: 'LocalEmbedding',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Sensible default embedding model per provider when the config doesn't specify one.
|
|
25
|
+
* `local` uses a small HuggingFace sentence-transformer (downloaded on first use) so
|
|
26
|
+
* organic-key clustering can run with no API key. `azure`/`bedrock` are intentionally
|
|
27
|
+
* absent — their models are deployment-specific and must be set explicitly.
|
|
28
|
+
*/
|
|
29
|
+
const PROVIDER_DEFAULT_MODEL = {
|
|
30
|
+
openai: 'text-embedding-3-small',
|
|
31
|
+
mistral: 'mistral-embed',
|
|
32
|
+
local: 'Xenova/all-MiniLM-L6-v2',
|
|
33
|
+
ollama: 'nomic-embed-text',
|
|
34
|
+
};
|
|
35
|
+
export function createEmbeddingProvider(config) {
|
|
36
|
+
const driverClass = PROVIDER_TO_DRIVER_CLASS[config.provider];
|
|
37
|
+
if (!driverClass) {
|
|
38
|
+
const supported = Object.keys(PROVIDER_TO_DRIVER_CLASS).join(', ');
|
|
39
|
+
throw new Error(`Embedding provider not supported: ${config.provider}. Supported: ${supported}`);
|
|
40
|
+
}
|
|
41
|
+
return new MJEmbeddingProvider(config, driverClass);
|
|
42
|
+
}
|
|
43
|
+
/** Wraps a registered `BaseEmbeddings` driver behind the simple `embed(texts)` contract. */
|
|
44
|
+
class MJEmbeddingProvider {
|
|
45
|
+
constructor(config, driverClass) {
|
|
46
|
+
this.provider = config.provider;
|
|
47
|
+
this.model = config.model || PROVIDER_DEFAULT_MODEL[config.provider] || '';
|
|
48
|
+
this.batchSize = config.batchSize ?? 100;
|
|
49
|
+
const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseEmbeddings, driverClass, config.apiKey);
|
|
50
|
+
if (!instance) {
|
|
51
|
+
throw new Error(`Failed to create embedding instance for provider '${config.provider}' (driver class: ${driverClass}). ` +
|
|
52
|
+
`Check that the provider package is installed and registered via server-bootstrap.`);
|
|
53
|
+
}
|
|
54
|
+
this.embeddings = instance;
|
|
55
|
+
}
|
|
56
|
+
async embed(texts) {
|
|
57
|
+
const out = new Array(texts.length);
|
|
58
|
+
for (let i = 0; i < texts.length; i += this.batchSize) {
|
|
59
|
+
const batch = texts.slice(i, i + this.batchSize);
|
|
60
|
+
const vectors = await this.embedBatch(batch);
|
|
61
|
+
for (let j = 0; j < vectors.length; j++)
|
|
62
|
+
out[i + j] = vectors[j];
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
async embedBatch(texts) {
|
|
67
|
+
const result = await this.embeddings.EmbedTexts({ texts, model: this.model });
|
|
68
|
+
const vectors = result?.vectors;
|
|
69
|
+
if (!vectors || vectors.length !== texts.length) {
|
|
70
|
+
throw new Error(`Embedding driver returned ${vectors?.length ?? 0} vectors for ${texts.length} inputs (provider: ${this.provider}).`);
|
|
71
|
+
}
|
|
72
|
+
return vectors.map((v) => normalizeVec(Float32Array.from(v)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Unit-normalize a vector in place so the clustering step can use cosine distance. */
|
|
76
|
+
function normalizeVec(v) {
|
|
77
|
+
let sum = 0;
|
|
78
|
+
for (let i = 0; i < v.length; i++)
|
|
79
|
+
sum += v[i] * v[i];
|
|
80
|
+
const norm = Math.sqrt(sum);
|
|
81
|
+
if (norm === 0)
|
|
82
|
+
return v;
|
|
83
|
+
for (let i = 0; i < v.length; i++)
|
|
84
|
+
v[i] = v[i] / norm;
|
|
85
|
+
return v;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=EmbeddingProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmbeddingProvider.js","sourceRoot":"","sources":["../../src/discovery/EmbeddingProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FKGraphWalker — Pattern 3 graph traversal.
|
|
3
|
+
*
|
|
4
|
+
* Given the foreign-key relationships of a database (hard declared + DBAutoDoc-
|
|
5
|
+
* discovered soft FKs), build an undirected join graph and find all paths of
|
|
6
|
+
* length ≤ maxHops from each "spoke" table to each "hub" table carrying an
|
|
7
|
+
* organic key. A path of length 2-3 means the spoke can reach the hub through
|
|
8
|
+
* intermediate tables — that's a transitive bridge candidate.
|
|
9
|
+
*
|
|
10
|
+
* Why a graph, not just FK chains:
|
|
11
|
+
* Many natural bridges traverse FKs in both directions. Example:
|
|
12
|
+
* Subscriber ←─ CampaignSend (CampaignSend.SubscriberID → Subscriber.ID)
|
|
13
|
+
* Contact (hub) — Email column
|
|
14
|
+
* To reach CampaignSend from Contact via "email", the path goes:
|
|
15
|
+
* Contact.Email ↔ Subscriber.Email (organic key match)
|
|
16
|
+
* Subscriber.ID ← CampaignSend.SubscriberID (FK navigation, reverse direction)
|
|
17
|
+
*
|
|
18
|
+
* The FK arrow points CampaignSend → Subscriber, but the bridge traversal
|
|
19
|
+
* goes Subscriber → CampaignSend. Modeling the graph as undirected handles
|
|
20
|
+
* this naturally; the edge metadata still tells us which side is the FK
|
|
21
|
+
* source vs target.
|
|
22
|
+
*
|
|
23
|
+
* Output: structural paths the BridgeViewSQLGenerator turns into CREATE VIEW
|
|
24
|
+
* SQL conformant with PR #2193's TransitiveView contract.
|
|
25
|
+
*/
|
|
26
|
+
/** One foreign-key relationship between two tables. */
|
|
27
|
+
export interface FKEdge {
|
|
28
|
+
sourceSchema: string;
|
|
29
|
+
sourceTable: string;
|
|
30
|
+
sourceColumn: string;
|
|
31
|
+
targetSchema: string;
|
|
32
|
+
targetTable: string;
|
|
33
|
+
targetColumn: string;
|
|
34
|
+
/** Hard (declared) vs soft (DBAutoDoc-detected). Affects ranking only. */
|
|
35
|
+
kind: 'hard' | 'soft';
|
|
36
|
+
/** Soft-FK confidence (0-1). Hard FKs are always 1. */
|
|
37
|
+
confidence: number;
|
|
38
|
+
}
|
|
39
|
+
/** A discovered transitive path from spoke to hub. */
|
|
40
|
+
export interface BridgePath {
|
|
41
|
+
/** The table reachable through the chain (the "spoke" in organic-key terms). */
|
|
42
|
+
spokeSchema: string;
|
|
43
|
+
spokeTable: string;
|
|
44
|
+
/** The table carrying the organic key (the "hub"). */
|
|
45
|
+
hubSchema: string;
|
|
46
|
+
hubTable: string;
|
|
47
|
+
/** Field name on the hub being projected (the organic-key match field). */
|
|
48
|
+
hubKeyField: string;
|
|
49
|
+
/**
|
|
50
|
+
* Ordered list of join hops. hops[0].fromTable === spokeTable;
|
|
51
|
+
* hops[last].toTable === hubTable. For a length-2 path:
|
|
52
|
+
* hops = [ { fromTable: spoke, toTable: intermediate, ... },
|
|
53
|
+
* { fromTable: intermediate, toTable: hub, ... } ]
|
|
54
|
+
*/
|
|
55
|
+
hops: BridgeHop[];
|
|
56
|
+
/** Total path length (number of FK joins). */
|
|
57
|
+
pathLength: number;
|
|
58
|
+
/**
|
|
59
|
+
* Path confidence = product of edge confidences. Hard-only paths = 1.
|
|
60
|
+
* Soft FKs on the path drag the confidence down (1 × 0.85 = 0.85).
|
|
61
|
+
*/
|
|
62
|
+
pathConfidence: number;
|
|
63
|
+
}
|
|
64
|
+
/** One join hop in a bridge path. */
|
|
65
|
+
export interface BridgeHop {
|
|
66
|
+
fromSchema: string;
|
|
67
|
+
fromTable: string;
|
|
68
|
+
fromColumn: string;
|
|
69
|
+
toSchema: string;
|
|
70
|
+
toTable: string;
|
|
71
|
+
toColumn: string;
|
|
72
|
+
/** Edge kind for ranking; same as FKEdge.kind. */
|
|
73
|
+
kind: 'hard' | 'soft';
|
|
74
|
+
}
|
|
75
|
+
export interface FKGraphWalkerOptions {
|
|
76
|
+
/** Maximum path length to explore. PR #2193 examples cap at 3. Default 3. */
|
|
77
|
+
maxHops?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Minimum confidence threshold for soft FKs to be included in the graph.
|
|
80
|
+
* Hard FKs are always included. Default 0.6.
|
|
81
|
+
*/
|
|
82
|
+
minSoftFKConfidence?: number;
|
|
83
|
+
/**
|
|
84
|
+
* When true, paths through the same table twice are pruned (no cycles).
|
|
85
|
+
* Default true.
|
|
86
|
+
*/
|
|
87
|
+
pruneCycles?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build the join graph and find all bridge paths from each spoke candidate to
|
|
91
|
+
* each hub candidate.
|
|
92
|
+
*
|
|
93
|
+
* @param edges - all FK relationships in the database (hard + soft).
|
|
94
|
+
* @param hubs - tables that carry an organic key (output of Pattern 1/2). Each
|
|
95
|
+
* entry names the hub's match field — this becomes the projected
|
|
96
|
+
* column in the bridge view.
|
|
97
|
+
* @param spokes - tables to attempt to reach from each hub. Typically every
|
|
98
|
+
* table in the database except the hub itself.
|
|
99
|
+
*/
|
|
100
|
+
export declare function findBridgePaths(edges: FKEdge[], hubs: Array<{
|
|
101
|
+
schema: string;
|
|
102
|
+
table: string;
|
|
103
|
+
keyField: string;
|
|
104
|
+
}>, spokes: Array<{
|
|
105
|
+
schema: string;
|
|
106
|
+
table: string;
|
|
107
|
+
}>, opts?: FKGraphWalkerOptions): BridgePath[];
|
|
108
|
+
//# sourceMappingURL=FKGraphWalker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FKGraphWalker.d.ts","sourceRoot":"","sources":["../../src/discovery/FKGraphWalker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,uDAAuD;AACvD,MAAM,WAAW,MAAM;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,sDAAsD;AACtD,MAAM,WAAW,UAAU;IACvB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,qCAAqC;AACrC,MAAM,WAAW,SAAS;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACjC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAQD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,EAChE,MAAM,EAAE,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAChD,IAAI,GAAE,oBAAyB,GAChC,UAAU,EAAE,CA2Bd"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FKGraphWalker — Pattern 3 graph traversal.
|
|
3
|
+
*
|
|
4
|
+
* Given the foreign-key relationships of a database (hard declared + DBAutoDoc-
|
|
5
|
+
* discovered soft FKs), build an undirected join graph and find all paths of
|
|
6
|
+
* length ≤ maxHops from each "spoke" table to each "hub" table carrying an
|
|
7
|
+
* organic key. A path of length 2-3 means the spoke can reach the hub through
|
|
8
|
+
* intermediate tables — that's a transitive bridge candidate.
|
|
9
|
+
*
|
|
10
|
+
* Why a graph, not just FK chains:
|
|
11
|
+
* Many natural bridges traverse FKs in both directions. Example:
|
|
12
|
+
* Subscriber ←─ CampaignSend (CampaignSend.SubscriberID → Subscriber.ID)
|
|
13
|
+
* Contact (hub) — Email column
|
|
14
|
+
* To reach CampaignSend from Contact via "email", the path goes:
|
|
15
|
+
* Contact.Email ↔ Subscriber.Email (organic key match)
|
|
16
|
+
* Subscriber.ID ← CampaignSend.SubscriberID (FK navigation, reverse direction)
|
|
17
|
+
*
|
|
18
|
+
* The FK arrow points CampaignSend → Subscriber, but the bridge traversal
|
|
19
|
+
* goes Subscriber → CampaignSend. Modeling the graph as undirected handles
|
|
20
|
+
* this naturally; the edge metadata still tells us which side is the FK
|
|
21
|
+
* source vs target.
|
|
22
|
+
*
|
|
23
|
+
* Output: structural paths the BridgeViewSQLGenerator turns into CREATE VIEW
|
|
24
|
+
* SQL conformant with PR #2193's TransitiveView contract.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULTS = {
|
|
27
|
+
maxHops: 3,
|
|
28
|
+
minSoftFKConfidence: 0.6,
|
|
29
|
+
pruneCycles: true,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Build the join graph and find all bridge paths from each spoke candidate to
|
|
33
|
+
* each hub candidate.
|
|
34
|
+
*
|
|
35
|
+
* @param edges - all FK relationships in the database (hard + soft).
|
|
36
|
+
* @param hubs - tables that carry an organic key (output of Pattern 1/2). Each
|
|
37
|
+
* entry names the hub's match field — this becomes the projected
|
|
38
|
+
* column in the bridge view.
|
|
39
|
+
* @param spokes - tables to attempt to reach from each hub. Typically every
|
|
40
|
+
* table in the database except the hub itself.
|
|
41
|
+
*/
|
|
42
|
+
export function findBridgePaths(edges, hubs, spokes, opts = {}) {
|
|
43
|
+
const o = { ...DEFAULTS, ...opts };
|
|
44
|
+
// Build the adjacency map keyed by "schema.table".
|
|
45
|
+
const adjacency = buildAdjacency(edges, o.minSoftFKConfidence);
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const hub of hubs) {
|
|
48
|
+
const hubKey = `${hub.schema}.${hub.table}`;
|
|
49
|
+
for (const spoke of spokes) {
|
|
50
|
+
const spokeKey = `${spoke.schema}.${spoke.table}`;
|
|
51
|
+
if (spokeKey === hubKey)
|
|
52
|
+
continue;
|
|
53
|
+
// BFS from spoke → hub.
|
|
54
|
+
const paths = bfsPaths(adjacency, spokeKey, hubKey, o.maxHops, o.pruneCycles);
|
|
55
|
+
for (const p of paths) {
|
|
56
|
+
if (p.length === 0)
|
|
57
|
+
continue; // self
|
|
58
|
+
if (p.length === 1)
|
|
59
|
+
continue; // direct FK already handled by existing relationship system
|
|
60
|
+
out.push(materializeBridgePath(p, hub, spoke));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Sort: shortest paths first, then highest confidence.
|
|
65
|
+
out.sort((a, b) => {
|
|
66
|
+
if (a.pathLength !== b.pathLength)
|
|
67
|
+
return a.pathLength - b.pathLength;
|
|
68
|
+
return b.pathConfidence - a.pathConfidence;
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function buildAdjacency(edges, minSoftFKConfidence) {
|
|
73
|
+
const out = new Map();
|
|
74
|
+
for (const e of edges) {
|
|
75
|
+
if (e.kind === 'soft' && e.confidence < minSoftFKConfidence)
|
|
76
|
+
continue;
|
|
77
|
+
const aKey = `${e.sourceSchema}.${e.sourceTable}`;
|
|
78
|
+
const bKey = `${e.targetSchema}.${e.targetTable}`;
|
|
79
|
+
const forward = {
|
|
80
|
+
fromKey: aKey,
|
|
81
|
+
toKey: bKey,
|
|
82
|
+
fkSourceKey: aKey,
|
|
83
|
+
fkSourceColumn: e.sourceColumn,
|
|
84
|
+
fkTargetColumn: e.targetColumn,
|
|
85
|
+
kind: e.kind,
|
|
86
|
+
confidence: e.confidence,
|
|
87
|
+
};
|
|
88
|
+
const reverse = {
|
|
89
|
+
fromKey: bKey,
|
|
90
|
+
toKey: aKey,
|
|
91
|
+
fkSourceKey: aKey,
|
|
92
|
+
fkSourceColumn: e.sourceColumn,
|
|
93
|
+
fkTargetColumn: e.targetColumn,
|
|
94
|
+
kind: e.kind,
|
|
95
|
+
confidence: e.confidence,
|
|
96
|
+
};
|
|
97
|
+
push(out, aKey, forward);
|
|
98
|
+
push(out, bKey, reverse);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function push(map, key, value) {
|
|
103
|
+
const list = map.get(key);
|
|
104
|
+
if (list)
|
|
105
|
+
list.push(value);
|
|
106
|
+
else
|
|
107
|
+
map.set(key, [value]);
|
|
108
|
+
}
|
|
109
|
+
// ─── BFS ────────────────────────────────────────────────────────────────────
|
|
110
|
+
/** Path of adjacency edges from `start` to `goal`. Returns empty if none found. */
|
|
111
|
+
function bfsPaths(adjacency, start, goal, maxHops, pruneCycles) {
|
|
112
|
+
if (start === goal)
|
|
113
|
+
return [[]];
|
|
114
|
+
const queue = [
|
|
115
|
+
{ node: start, pathEdges: [], visited: new Set([start]) },
|
|
116
|
+
];
|
|
117
|
+
const found = [];
|
|
118
|
+
while (queue.length > 0) {
|
|
119
|
+
const { node, pathEdges, visited } = queue.shift();
|
|
120
|
+
if (pathEdges.length >= maxHops)
|
|
121
|
+
continue;
|
|
122
|
+
const neighbors = adjacency.get(node) ?? [];
|
|
123
|
+
for (const edge of neighbors) {
|
|
124
|
+
const nextNode = edge.toKey;
|
|
125
|
+
if (pruneCycles && visited.has(nextNode))
|
|
126
|
+
continue;
|
|
127
|
+
const newPath = [...pathEdges, edge];
|
|
128
|
+
if (nextNode === goal) {
|
|
129
|
+
found.push(newPath);
|
|
130
|
+
continue; // don't extend past the goal
|
|
131
|
+
}
|
|
132
|
+
const nextVisited = new Set(visited);
|
|
133
|
+
nextVisited.add(nextNode);
|
|
134
|
+
queue.push({ node: nextNode, pathEdges: newPath, visited: nextVisited });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
139
|
+
// ─── Bridge path materialization ────────────────────────────────────────────
|
|
140
|
+
function materializeBridgePath(edgePath, hub, spoke) {
|
|
141
|
+
const hops = edgePath.map((e) => {
|
|
142
|
+
const [fromSchema, fromTable] = e.fromKey.split('.');
|
|
143
|
+
const [toSchema, toTable] = e.toKey.split('.');
|
|
144
|
+
// The column on the "from" side is the FK-source column if from is the FK source;
|
|
145
|
+
// otherwise it's the FK-target column.
|
|
146
|
+
const fromIsFKSource = e.fromKey === e.fkSourceKey;
|
|
147
|
+
return {
|
|
148
|
+
fromSchema,
|
|
149
|
+
fromTable,
|
|
150
|
+
fromColumn: fromIsFKSource ? e.fkSourceColumn : e.fkTargetColumn,
|
|
151
|
+
toSchema,
|
|
152
|
+
toTable,
|
|
153
|
+
toColumn: fromIsFKSource ? e.fkTargetColumn : e.fkSourceColumn,
|
|
154
|
+
kind: e.kind,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
const pathConfidence = edgePath.reduce((acc, e) => acc * e.confidence, 1);
|
|
158
|
+
return {
|
|
159
|
+
spokeSchema: spoke.schema,
|
|
160
|
+
spokeTable: spoke.table,
|
|
161
|
+
hubSchema: hub.schema,
|
|
162
|
+
hubTable: hub.table,
|
|
163
|
+
hubKeyField: hub.keyField,
|
|
164
|
+
hops,
|
|
165
|
+
pathLength: edgePath.length,
|
|
166
|
+
pathConfidence,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=FKGraphWalker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FKGraphWalker.js","sourceRoot":"","sources":["../../src/discovery/FKGraphWalker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrganicKeyDetector — the thin orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* 1. SemanticPhase LLM identifies organic-key concept per column,
|
|
5
|
+
* groups columns by canonical concept name into clusters
|
|
6
|
+
* spanning ≥2 distinct tables.
|
|
7
|
+
*
|
|
8
|
+
* 2. StructuralPhase Walks the FK graph from each cluster's hubs to find
|
|
9
|
+
* reachable tables 2-3 hops away → bridge view SQL.
|
|
10
|
+
*
|
|
11
|
+
* 3. Composer Drops fk-redundant clusters (already navigable via
|
|
12
|
+
* declared FK per PR #2193), attaches matching bridges,
|
|
13
|
+
* emits PR #2193 JSON.
|
|
14
|
+
*
|
|
15
|
+
* No knobs. The LLM is the algorithm; the graph is deterministic; the filter
|
|
16
|
+
* is the one PR #2193 explicitly defines.
|
|
17
|
+
*/
|
|
18
|
+
import { AIConfig, OrganicKeyDetectionConfig } from '../types/config.js';
|
|
19
|
+
import { DatabaseDocumentation } from '../types/state.js';
|
|
20
|
+
import { OrganicKeyCluster, OrganicKeyDetectionPhase } from '../types/organic-keys.js';
|
|
21
|
+
import { ProgressCallback } from './SemanticPhase.js';
|
|
22
|
+
import { DetectedOrganicKeysOutput } from './OrganicKeyTranslator.js';
|
|
23
|
+
export interface OrganicKeyDetectionResult {
|
|
24
|
+
clusters: OrganicKeyCluster[];
|
|
25
|
+
output: DetectedOrganicKeysOutput;
|
|
26
|
+
phase: OrganicKeyDetectionPhase;
|
|
27
|
+
summary: {
|
|
28
|
+
columnsInScope: number;
|
|
29
|
+
columnsNormalized: number;
|
|
30
|
+
columnsRejectedByNormalizer: number;
|
|
31
|
+
clustersFound: number;
|
|
32
|
+
clustersEmitted: number;
|
|
33
|
+
clustersDropped: number;
|
|
34
|
+
outputSchemas: number;
|
|
35
|
+
outputTables: number;
|
|
36
|
+
outputKeys: number;
|
|
37
|
+
outputSpokes: number;
|
|
38
|
+
transitiveBridges: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface DetectorRunOptions {
|
|
42
|
+
onProgress?: ProgressCallback;
|
|
43
|
+
}
|
|
44
|
+
export declare class OrganicKeyDetector {
|
|
45
|
+
private readonly config;
|
|
46
|
+
private readonly aiConfig;
|
|
47
|
+
constructor(config: OrganicKeyDetectionConfig, aiConfig: AIConfig);
|
|
48
|
+
detect(state: DatabaseDocumentation, opts?: DetectorRunOptions): Promise<OrganicKeyDetectionResult>;
|
|
49
|
+
private estimateCost;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=OrganicKeyDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganicKeyDetector.d.ts","sourceRoot":"","sources":["../../src/discovery/OrganicKeyDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,QAAQ,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,EAAoB,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,MAAM,WAAW,yBAAyB;IACtC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,yBAAyB,CAAC;IAClC,KAAK,EAAE,wBAAwB,CAAC;IAChC,OAAO,EAAE;QACL,cAAc,EAAE,MAAM,CAAC;QACvB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,2BAA2B,EAAE,MAAM,CAAC;QACpC,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;QACxB,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,iBAAiB,EAAE,MAAM,CAAC;KAC7B,CAAC;CACL;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAED,qBAAa,kBAAkB;IAEvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,MAAM,EAAE,yBAAyB,EACjC,QAAQ,EAAE,QAAQ;IAG1B,MAAM,CACf,KAAK,EAAE,qBAAqB,EAC5B,IAAI,GAAE,kBAAuB,GAC9B,OAAO,CAAC,yBAAyB,CAAC;IAmDrC,OAAO,CAAC,YAAY;CAMvB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrganicKeyDetector — the thin orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* 1. SemanticPhase LLM identifies organic-key concept per column,
|
|
5
|
+
* groups columns by canonical concept name into clusters
|
|
6
|
+
* spanning ≥2 distinct tables.
|
|
7
|
+
*
|
|
8
|
+
* 2. StructuralPhase Walks the FK graph from each cluster's hubs to find
|
|
9
|
+
* reachable tables 2-3 hops away → bridge view SQL.
|
|
10
|
+
*
|
|
11
|
+
* 3. Composer Drops fk-redundant clusters (already navigable via
|
|
12
|
+
* declared FK per PR #2193), attaches matching bridges,
|
|
13
|
+
* emits PR #2193 JSON.
|
|
14
|
+
*
|
|
15
|
+
* No knobs. The LLM is the algorithm; the graph is deterministic; the filter
|
|
16
|
+
* is the one PR #2193 explicitly defines.
|
|
17
|
+
*/
|
|
18
|
+
import { runSemanticPhase } from './SemanticPhase.js';
|
|
19
|
+
import { runStructuralPhase } from './StructuralPhase.js';
|
|
20
|
+
import { compose } from './Composer.js';
|
|
21
|
+
export class OrganicKeyDetector {
|
|
22
|
+
constructor(config, aiConfig) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.aiConfig = aiConfig;
|
|
25
|
+
}
|
|
26
|
+
async detect(state, opts = {}) {
|
|
27
|
+
const progress = opts.onProgress ?? (() => { });
|
|
28
|
+
const startedAt = new Date().toISOString();
|
|
29
|
+
const a = await runSemanticPhase(state, this.config, this.aiConfig, progress);
|
|
30
|
+
const b = runStructuralPhase(state, a.clusters);
|
|
31
|
+
progress(`structural: ${b.summary.transitiveBridgesFound} bridges`);
|
|
32
|
+
const c = compose(a.clusters, b.bridges);
|
|
33
|
+
progress(`compose: emitted ${c.emitted}/${a.clusters.length} clusters (${c.summary.outputKeys} keys, ${c.summary.outputSpokes} spokes)`);
|
|
34
|
+
// Net additional clusters produced by the concept-name split (sub-clusters created
|
|
35
|
+
// beyond the raw clusterer output, counting both kept and dropped sub-clusters).
|
|
36
|
+
const splitClusterCount = Math.max(0, a.summary.clustersFound + a.summary.clustersDropped - a.summary.clustersBeforeSplit);
|
|
37
|
+
return {
|
|
38
|
+
clusters: c.annotatedClusters,
|
|
39
|
+
output: c.output,
|
|
40
|
+
phase: {
|
|
41
|
+
triggered: true,
|
|
42
|
+
startedAt,
|
|
43
|
+
completedAt: new Date().toISOString(),
|
|
44
|
+
status: 'completed',
|
|
45
|
+
candidateClusterCount: a.clusters.length,
|
|
46
|
+
confirmedClusterCount: c.emitted,
|
|
47
|
+
rejectedClusterCount: a.summary.columnsRejectedByNormalizer,
|
|
48
|
+
splitClusterCount,
|
|
49
|
+
tokensUsed: a.tokens.total,
|
|
50
|
+
inputTokens: a.tokens.input,
|
|
51
|
+
outputTokens: a.tokens.output,
|
|
52
|
+
estimatedCost: this.estimateCost(a.tokens.input, a.tokens.output),
|
|
53
|
+
refinementModelUsed: this.aiConfig.model,
|
|
54
|
+
},
|
|
55
|
+
summary: {
|
|
56
|
+
columnsInScope: a.summary.columnsInScope,
|
|
57
|
+
columnsNormalized: a.summary.columnsNormalized,
|
|
58
|
+
columnsRejectedByNormalizer: a.summary.columnsRejectedByNormalizer,
|
|
59
|
+
clustersFound: a.clusters.length,
|
|
60
|
+
clustersEmitted: c.emitted,
|
|
61
|
+
clustersDropped: a.summary.clustersDropped,
|
|
62
|
+
outputSchemas: c.summary.outputSchemas,
|
|
63
|
+
outputTables: c.summary.outputTables,
|
|
64
|
+
outputKeys: c.summary.outputKeys,
|
|
65
|
+
outputSpokes: c.summary.outputSpokes,
|
|
66
|
+
transitiveBridges: b.summary.transitiveBridgesFound,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
estimateCost(inputTokens, outputTokens) {
|
|
71
|
+
const pricing = this.aiConfig.pricing;
|
|
72
|
+
if (!pricing)
|
|
73
|
+
return 0;
|
|
74
|
+
return (inputTokens / 1_000_000) * (pricing.inputCostPer1MTokens ?? 0)
|
|
75
|
+
+ (outputTokens / 1_000_000) * (pricing.outputCostPer1MTokens ?? 0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=OrganicKeyDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganicKeyDetector.js","sourceRoot":"","sources":["../../src/discovery/OrganicKeyDetector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrganicKeyTranslator — PURE EMISSION.
|
|
3
|
+
*
|
|
4
|
+
* Takes a list of clusters that have ALREADY been gated by the Composer, plus
|
|
5
|
+
* the transitive spokes for those that survived, and fans them out into PR
|
|
6
|
+
* #2193's per-schema/per-table JSON.
|
|
7
|
+
*
|
|
8
|
+
* No filtering, no thresholds, no scoring logic in here. The Composer is the
|
|
9
|
+
* single emission gate; this file is its render-to-JSON helper.
|
|
10
|
+
*
|
|
11
|
+
* Same-concept consolidation: clusters that the LLM normalizer assigned the
|
|
12
|
+
* same canonical concept name (e.g. four separate geometric clusters all
|
|
13
|
+
* named `product_id`) are MERGED here into a single conceptual cluster before
|
|
14
|
+
* fan-out — deterministic equivalent of an LLM concept-merge pass.
|
|
15
|
+
*/
|
|
16
|
+
import { OrganicKeyCluster, OrganicKeyNormalizationStrategy } from '../types/organic-keys.js';
|
|
17
|
+
/** A generated SQL bridge view backing a transitive (multi-hop) spoke. */
|
|
18
|
+
export interface TransitiveViewConfig {
|
|
19
|
+
Name: string;
|
|
20
|
+
SchemaName?: string;
|
|
21
|
+
SQL: string;
|
|
22
|
+
}
|
|
23
|
+
/** One related entity (spoke) reachable from an organic key — either a direct field match or a transitive (bridge-view) match. */
|
|
24
|
+
export interface OrganicKeyRelatedEntityConfig {
|
|
25
|
+
SchemaName: string;
|
|
26
|
+
TableName: string;
|
|
27
|
+
RelatedFieldNames?: string[];
|
|
28
|
+
TransitiveView?: TransitiveViewConfig;
|
|
29
|
+
TransitiveMatchFieldNames?: string[];
|
|
30
|
+
TransitiveOutputFieldName?: string;
|
|
31
|
+
RelatedEntityJoinFieldName?: string;
|
|
32
|
+
DisplayName?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A single organic key anchored on one table's column(s), with its own normalization and its spokes. */
|
|
35
|
+
export interface OrganicKeyConfig {
|
|
36
|
+
Name: string;
|
|
37
|
+
Description?: string;
|
|
38
|
+
MatchFieldNames: string[];
|
|
39
|
+
NormalizationStrategy: OrganicKeyNormalizationStrategy;
|
|
40
|
+
CustomNormalizationExpression?: string;
|
|
41
|
+
AutoCreateRelatedViewOnForm?: boolean;
|
|
42
|
+
RelatedEntities: OrganicKeyRelatedEntityConfig[];
|
|
43
|
+
}
|
|
44
|
+
/** All organic keys anchored on a given table. */
|
|
45
|
+
export interface TableOrganicKeyConfig {
|
|
46
|
+
TableName: string;
|
|
47
|
+
OrganicKeys: OrganicKeyConfig[];
|
|
48
|
+
}
|
|
49
|
+
/** The full emit payload: schema name → its tables' organic-key configs. */
|
|
50
|
+
export type DetectedOrganicKeysOutput = Record<string, TableOrganicKeyConfig[]>;
|
|
51
|
+
export interface TransitiveSpokeInput {
|
|
52
|
+
hubSchema: string;
|
|
53
|
+
hubTable: string;
|
|
54
|
+
hubKeyFields: string[];
|
|
55
|
+
spokeSchema: string;
|
|
56
|
+
spokeTable: string;
|
|
57
|
+
transitiveView: TransitiveViewConfig;
|
|
58
|
+
transitiveMatchFieldNames: string[];
|
|
59
|
+
transitiveOutputFieldName: string;
|
|
60
|
+
relatedEntityJoinFieldName: string;
|
|
61
|
+
hubConcept?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Render gated clusters into PR #2193 JSON. Same-concept consolidation +
|
|
65
|
+
* per-table fan-out + transitive spoke attachment. No filters, no thresholds.
|
|
66
|
+
*
|
|
67
|
+
* The caller (Composer) is responsible for filtering. Anything passed in
|
|
68
|
+
* gets emitted.
|
|
69
|
+
*/
|
|
70
|
+
export declare function translateClusters(clusters: OrganicKeyCluster[], transitiveSpokes?: TransitiveSpokeInput[]): DetectedOrganicKeysOutput;
|
|
71
|
+
/** Tally the emit payload: number of schemas, tables, organic keys, and spokes it contains. */
|
|
72
|
+
export declare function countOutputEntries(out: DetectedOrganicKeysOutput): {
|
|
73
|
+
schemas: number;
|
|
74
|
+
tables: number;
|
|
75
|
+
keys: number;
|
|
76
|
+
spokes: number;
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=OrganicKeyTranslator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganicKeyTranslator.d.ts","sourceRoot":"","sources":["../../src/discovery/OrganicKeyTranslator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACH,iBAAiB,EAEjB,+BAA+B,EAGlC,MAAM,0BAA0B,CAAC;AAOlC,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACf;AAED,kIAAkI;AAClI,MAAM,WAAW,6BAA6B;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAC;IACrC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,yGAAyG;AACzG,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,qBAAqB,EAAE,+BAA+B,CAAC;IACvD,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,eAAe,EAAE,6BAA6B,EAAE,CAAC;CACpD;AAED,kDAAkD;AAClD,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,gBAAgB,EAAE,CAAC;CACnC;AAED,4EAA4E;AAC5E,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;AAIhF,MAAM,WAAW,oBAAoB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,oBAAoB,CAAC;IACrC,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC,yBAAyB,EAAE,MAAM,CAAC;IAClC,0BAA0B,EAAE,MAAM,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,gBAAgB,GAAE,oBAAoB,EAAO,GAC9C,yBAAyB,CAmG3B;AAwCD,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,yBAAyB,GAAG;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAClB,CAaA"}
|