@finos/legend-lego 2.0.196 → 2.0.198
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/lib/code-editor/CodeEditor.d.ts.map +1 -1
- package/lib/code-editor/CodeEditor.js +14 -1
- package/lib/code-editor/CodeEditor.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +60 -0
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js +429 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +127 -1
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +111 -2
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +14 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +2 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +37 -2
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +11 -12
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +7 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +106 -41
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts +1 -5
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +168 -109
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js +85 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js +78 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts +25 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js +85 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -0
- package/lib/legend-ai/index.d.ts +8 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +8 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +105 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +1482 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -35
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +114 -949
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/code-editor/CodeEditor.tsx +19 -0
- package/src/legend-ai/LegendAIDocEnrichment.ts +572 -0
- package/src/legend-ai/LegendAITypes.ts +213 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +25 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +55 -1
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +14 -34
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +157 -47
- package/src/legend-ai/components/LegendAIChat.tsx +389 -206
- package/src/legend-ai/components/LegendAIChatHelpers.ts +117 -0
- package/src/legend-ai/components/LegendAIChatInput.tsx +209 -0
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +199 -0
- package/src/legend-ai/index.ts +31 -4
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +2563 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +161 -1697
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Multiplicity } from '@finos/legend-graph';
|
|
18
|
+
import {
|
|
19
|
+
ClassDocumentationEntry,
|
|
20
|
+
AssociationDocumentationEntry,
|
|
21
|
+
PropertyDocumentationEntry,
|
|
22
|
+
type NormalizedDocumentationEntry,
|
|
23
|
+
} from '../model-documentation/index.js';
|
|
24
|
+
import type {
|
|
25
|
+
TDSColumnSchema,
|
|
26
|
+
TDSServiceSchema,
|
|
27
|
+
TDSServicePreFilter,
|
|
28
|
+
LegendAIServiceRelationship,
|
|
29
|
+
} from './LegendAITypes.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds a lookup map from lowercase property name to its documentation entry.
|
|
33
|
+
* Used to enrich TDS columns with business descriptions and nullability
|
|
34
|
+
* from the PURE model's class properties.
|
|
35
|
+
*/
|
|
36
|
+
export function buildPropertyDocIndex(
|
|
37
|
+
elementDocs: NormalizedDocumentationEntry[],
|
|
38
|
+
): Map<string, PropertyDocumentationEntry> {
|
|
39
|
+
const index = new Map<string, PropertyDocumentationEntry>();
|
|
40
|
+
for (const entry of elementDocs) {
|
|
41
|
+
if (
|
|
42
|
+
entry.elementEntry instanceof ClassDocumentationEntry &&
|
|
43
|
+
entry.entry instanceof PropertyDocumentationEntry
|
|
44
|
+
) {
|
|
45
|
+
if (entry.entry.name) {
|
|
46
|
+
index.set(entry.entry.name.toLowerCase(), entry.entry);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enriches TDS columns with documentation and nullability from model
|
|
55
|
+
* class property docs. Matches columns to properties case-insensitively.
|
|
56
|
+
*/
|
|
57
|
+
export function enrichColumnsFromElementDocs(
|
|
58
|
+
columns: TDSColumnSchema[],
|
|
59
|
+
propIndex: Map<string, PropertyDocumentationEntry>,
|
|
60
|
+
): void {
|
|
61
|
+
for (const col of columns) {
|
|
62
|
+
const prop = propIndex.get(col.name.toLowerCase());
|
|
63
|
+
if (prop) {
|
|
64
|
+
if (!col.documentation && prop.docs.length > 0) {
|
|
65
|
+
col.documentation = prop.docs.join('; ');
|
|
66
|
+
}
|
|
67
|
+
if (col.nullable === undefined && prop.multiplicity) {
|
|
68
|
+
col.nullable = prop.multiplicity.lowerBound === 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractClassNamesFromPattern(pattern: string): string[] {
|
|
75
|
+
return pattern
|
|
76
|
+
.split('/')
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.flatMap((part) => {
|
|
79
|
+
const match = /^get(?<name>.+)$/i.exec(part);
|
|
80
|
+
return match?.groups?.name ? [match.groups.name] : [part];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatMultiplicity(multiplicity: Multiplicity | undefined): string {
|
|
85
|
+
if (!multiplicity) {
|
|
86
|
+
return '*';
|
|
87
|
+
}
|
|
88
|
+
const upper =
|
|
89
|
+
multiplicity.upperBound === undefined ? '*' : `${multiplicity.upperBound}`;
|
|
90
|
+
return `${multiplicity.lowerBound}..${upper}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractUniqueAssociations(
|
|
94
|
+
elementDocs: NormalizedDocumentationEntry[],
|
|
95
|
+
): Map<string, AssociationDocumentationEntry> {
|
|
96
|
+
const assocEntries = elementDocs
|
|
97
|
+
.map((e) => e.elementEntry)
|
|
98
|
+
.filter(
|
|
99
|
+
(e): e is AssociationDocumentationEntry =>
|
|
100
|
+
e instanceof AssociationDocumentationEntry,
|
|
101
|
+
);
|
|
102
|
+
const uniqueAssocs = new Map<string, AssociationDocumentationEntry>();
|
|
103
|
+
for (const a of assocEntries) {
|
|
104
|
+
if (!uniqueAssocs.has(a.path)) {
|
|
105
|
+
uniqueAssocs.set(a.path, a);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return uniqueAssocs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addAdjacencyEdge(
|
|
112
|
+
adjacency: Map<string, { target: string; multiplicity: string }[]>,
|
|
113
|
+
from: string,
|
|
114
|
+
to: string,
|
|
115
|
+
multiplicity: string,
|
|
116
|
+
): void {
|
|
117
|
+
let edges = adjacency.get(from);
|
|
118
|
+
if (!edges) {
|
|
119
|
+
edges = [];
|
|
120
|
+
adjacency.set(from, edges);
|
|
121
|
+
}
|
|
122
|
+
edges.push({ target: to, multiplicity });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildAssociationAdjacency(
|
|
126
|
+
uniqueAssocs: Map<string, AssociationDocumentationEntry>,
|
|
127
|
+
): Map<string, { target: string; multiplicity: string }[]> {
|
|
128
|
+
const adjacency = new Map<
|
|
129
|
+
string,
|
|
130
|
+
{ target: string; multiplicity: string }[]
|
|
131
|
+
>();
|
|
132
|
+
for (const assoc of uniqueAssocs.values()) {
|
|
133
|
+
if (assoc.properties.length === 2) {
|
|
134
|
+
const [propA, propB] = assoc.properties;
|
|
135
|
+
if (propA && propB) {
|
|
136
|
+
const nameA = propA.name.toLowerCase();
|
|
137
|
+
const nameB = propB.name.toLowerCase();
|
|
138
|
+
addAdjacencyEdge(
|
|
139
|
+
adjacency,
|
|
140
|
+
nameA,
|
|
141
|
+
nameB,
|
|
142
|
+
formatMultiplicity(propB.multiplicity),
|
|
143
|
+
);
|
|
144
|
+
addAdjacencyEdge(
|
|
145
|
+
adjacency,
|
|
146
|
+
nameB,
|
|
147
|
+
nameA,
|
|
148
|
+
formatMultiplicity(propA.multiplicity),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return adjacency;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function findSharedColumns(
|
|
157
|
+
svcA: TDSServiceSchema,
|
|
158
|
+
svcB: TDSServiceSchema,
|
|
159
|
+
): string[] {
|
|
160
|
+
const colsA = new Set(svcA.columns.map((c) => c.name.toLowerCase()));
|
|
161
|
+
return svcB.columns
|
|
162
|
+
.filter((c) => colsA.has(c.name.toLowerCase()))
|
|
163
|
+
.map((c) => c.name);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function findViaConnectionFromClass(
|
|
167
|
+
clsA: string,
|
|
168
|
+
classNamesB: string[],
|
|
169
|
+
adjacency: Map<string, { target: string; multiplicity: string }[]>,
|
|
170
|
+
): { viaEntity: string; leftMult: string; rightMult: string } | undefined {
|
|
171
|
+
const edgesA = adjacency.get(clsA.toLowerCase());
|
|
172
|
+
if (!edgesA) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
for (const edgeA of edgesA) {
|
|
176
|
+
const edgesFromVia = adjacency.get(edgeA.target);
|
|
177
|
+
if (!edgesFromVia) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const clsB of classNamesB) {
|
|
181
|
+
const edgeB = edgesFromVia.find((e) => e.target === clsB.toLowerCase());
|
|
182
|
+
if (edgeB) {
|
|
183
|
+
return {
|
|
184
|
+
viaEntity: edgeA.target,
|
|
185
|
+
leftMult: edgeA.multiplicity,
|
|
186
|
+
rightMult: edgeB.multiplicity,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findViaRelationship(
|
|
195
|
+
svcA: TDSServiceSchema,
|
|
196
|
+
svcB: TDSServiceSchema,
|
|
197
|
+
adjacency: Map<string, { target: string; multiplicity: string }[]>,
|
|
198
|
+
): LegendAIServiceRelationship | undefined {
|
|
199
|
+
const classNamesA = extractClassNamesFromPattern(svcA.pattern);
|
|
200
|
+
const classNamesB = extractClassNamesFromPattern(svcB.pattern);
|
|
201
|
+
for (const clsA of classNamesA) {
|
|
202
|
+
const via = findViaConnectionFromClass(clsA, classNamesB, adjacency);
|
|
203
|
+
if (via) {
|
|
204
|
+
return {
|
|
205
|
+
leftService: svcA.title,
|
|
206
|
+
rightService: svcB.title,
|
|
207
|
+
joinColumns: findSharedColumns(svcA, svcB),
|
|
208
|
+
viaEntity: via.viaEntity,
|
|
209
|
+
leftCardinality: via.leftMult,
|
|
210
|
+
rightCardinality: via.rightMult,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findDirectRelationship(
|
|
218
|
+
svcA: TDSServiceSchema,
|
|
219
|
+
svcB: TDSServiceSchema,
|
|
220
|
+
adjacency: Map<string, { target: string; multiplicity: string }[]>,
|
|
221
|
+
): LegendAIServiceRelationship | undefined {
|
|
222
|
+
const classNamesA = extractClassNamesFromPattern(svcA.pattern);
|
|
223
|
+
const classNamesB = extractClassNamesFromPattern(svcB.pattern);
|
|
224
|
+
for (const clsA of classNamesA) {
|
|
225
|
+
const edges = adjacency.get(clsA.toLowerCase());
|
|
226
|
+
if (!edges) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
for (const clsB of classNamesB) {
|
|
230
|
+
const edge = edges.find((e) => e.target === clsB.toLowerCase());
|
|
231
|
+
if (edge) {
|
|
232
|
+
return {
|
|
233
|
+
leftService: svcA.title,
|
|
234
|
+
rightService: svcB.title,
|
|
235
|
+
joinColumns: findSharedColumns(svcA, svcB),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Infers cross-service relationships from association documentation entries.
|
|
245
|
+
*
|
|
246
|
+
* Associations in the PURE model define how classes connect (e.g.,
|
|
247
|
+
* `Trade ──(1:*)──→ Instrument`). By finding associations that link
|
|
248
|
+
* classes backing different services, we determine:
|
|
249
|
+
* - Which services share a common parent entity (the via entity)
|
|
250
|
+
* - The cardinality of each side (1:1, 1:*, etc.)
|
|
251
|
+
* - Which columns to JOIN on (shared column names)
|
|
252
|
+
*
|
|
253
|
+
* Used by both Data Space and Data Product AI integrations.
|
|
254
|
+
*/
|
|
255
|
+
export function inferServiceRelationshipsFromAssociations(
|
|
256
|
+
services: TDSServiceSchema[],
|
|
257
|
+
elementDocs: NormalizedDocumentationEntry[],
|
|
258
|
+
): LegendAIServiceRelationship[] {
|
|
259
|
+
const uniqueAssocs = extractUniqueAssociations(elementDocs);
|
|
260
|
+
const adjacency = buildAssociationAdjacency(uniqueAssocs);
|
|
261
|
+
const relationships: LegendAIServiceRelationship[] = [];
|
|
262
|
+
const seen = new Set<string>();
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < services.length; i++) {
|
|
265
|
+
for (let j = i + 1; j < services.length; j++) {
|
|
266
|
+
const svcA = services[i];
|
|
267
|
+
const svcB = services[j];
|
|
268
|
+
if (!svcA || !svcB) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const key = `${svcA.title}|${svcB.title}`;
|
|
272
|
+
if (seen.has(key)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const relationship =
|
|
276
|
+
findViaRelationship(svcA, svcB, adjacency) ??
|
|
277
|
+
findDirectRelationship(svcA, svcB, adjacency);
|
|
278
|
+
if (relationship) {
|
|
279
|
+
seen.add(key);
|
|
280
|
+
relationships.push(relationship);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return relationships;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
288
|
+
// Lambda pre-filter extraction
|
|
289
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
interface LambdaNode {
|
|
292
|
+
_type?: string;
|
|
293
|
+
function?: string;
|
|
294
|
+
parameters?: LambdaNode[];
|
|
295
|
+
property?: string;
|
|
296
|
+
name?: string;
|
|
297
|
+
value?: string | number | boolean;
|
|
298
|
+
body?: LambdaNode[];
|
|
299
|
+
values?: LambdaNode[];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isLambdaNode(value: unknown): value is LambdaNode {
|
|
303
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function collectPropertyPath(node: LambdaNode): string | undefined {
|
|
307
|
+
if (node._type !== 'property' || !node.property) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
const params = node.parameters;
|
|
311
|
+
if (!params || params.length === 0) {
|
|
312
|
+
return node.property;
|
|
313
|
+
}
|
|
314
|
+
const firstParam = params[0];
|
|
315
|
+
if (!firstParam) {
|
|
316
|
+
return node.property;
|
|
317
|
+
}
|
|
318
|
+
if (firstParam._type === 'var') {
|
|
319
|
+
return node.property;
|
|
320
|
+
}
|
|
321
|
+
const parentPath = collectPropertyPath(firstParam);
|
|
322
|
+
if (parentPath) {
|
|
323
|
+
return `${parentPath}.${node.property}`;
|
|
324
|
+
}
|
|
325
|
+
return node.property;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function extractLiteralValue(
|
|
329
|
+
node: LambdaNode,
|
|
330
|
+
): string | number | boolean | undefined {
|
|
331
|
+
if (
|
|
332
|
+
node._type === 'string' &&
|
|
333
|
+
(typeof node.value === 'string' || typeof node.value === 'number')
|
|
334
|
+
) {
|
|
335
|
+
return node.value;
|
|
336
|
+
}
|
|
337
|
+
if (node._type === 'integer' && typeof node.value === 'number') {
|
|
338
|
+
return node.value;
|
|
339
|
+
}
|
|
340
|
+
if (node._type === 'float' && typeof node.value === 'number') {
|
|
341
|
+
return node.value;
|
|
342
|
+
}
|
|
343
|
+
if (node._type === 'boolean' && typeof node.value === 'boolean') {
|
|
344
|
+
return node.value;
|
|
345
|
+
}
|
|
346
|
+
if (node._type === 'strictDate' && typeof node.value === 'string') {
|
|
347
|
+
return node.value;
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function tryExtractEqualFilter(
|
|
353
|
+
node: LambdaNode,
|
|
354
|
+
): TDSServicePreFilter | undefined {
|
|
355
|
+
if (node.parameters?.length !== 2) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const [left, right] = node.parameters;
|
|
359
|
+
if (!left || !right) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
const propPath = collectPropertyPath(left);
|
|
363
|
+
const literal = extractLiteralValue(right);
|
|
364
|
+
if (propPath && literal !== undefined) {
|
|
365
|
+
return { property: propPath, operator: 'equal', value: literal };
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function tryExtractUnaryFilter(
|
|
371
|
+
node: LambdaNode,
|
|
372
|
+
operator: 'isEmpty' | 'isNotEmpty',
|
|
373
|
+
): TDSServicePreFilter | undefined {
|
|
374
|
+
if (node.parameters?.length !== 1) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const [arg] = node.parameters;
|
|
378
|
+
const propPath = arg ? collectPropertyPath(arg) : undefined;
|
|
379
|
+
if (propPath) {
|
|
380
|
+
return { property: propPath, operator };
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function extractFiltersFromExpression(
|
|
386
|
+
node: LambdaNode,
|
|
387
|
+
results: TDSServicePreFilter[],
|
|
388
|
+
): void {
|
|
389
|
+
if (node._type !== 'func' || !node.parameters) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (node.function === 'equal') {
|
|
394
|
+
const filter = tryExtractEqualFilter(node);
|
|
395
|
+
if (filter) {
|
|
396
|
+
results.push(filter);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (node.function === 'isEmpty' || node.function === 'isNotEmpty') {
|
|
402
|
+
const filter = tryExtractUnaryFilter(node, node.function);
|
|
403
|
+
if (filter) {
|
|
404
|
+
results.push(filter);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const param of node.parameters) {
|
|
410
|
+
extractFiltersFromExpression(param, results);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function extractFiltersFromFilterCall(
|
|
415
|
+
filterLambda: LambdaNode,
|
|
416
|
+
results: TDSServicePreFilter[],
|
|
417
|
+
): void {
|
|
418
|
+
const body = filterLambda.body;
|
|
419
|
+
if (!body || !Array.isArray(body)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
for (const expr of body) {
|
|
423
|
+
extractFiltersFromExpression(expr, results);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function processFilterNode(
|
|
428
|
+
node: LambdaNode,
|
|
429
|
+
results: TDSServicePreFilter[],
|
|
430
|
+
): void {
|
|
431
|
+
const params = node.parameters;
|
|
432
|
+
if (params?.length === 2) {
|
|
433
|
+
const filterLambdaParam = params[1];
|
|
434
|
+
if (filterLambdaParam?._type === 'lambda') {
|
|
435
|
+
extractFiltersFromFilterCall(filterLambdaParam, results);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (params) {
|
|
439
|
+
walkLambdaBody(params, results);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function walkLambdaBody(
|
|
444
|
+
nodes: unknown[],
|
|
445
|
+
results: TDSServicePreFilter[],
|
|
446
|
+
): void {
|
|
447
|
+
for (const rawNode of nodes) {
|
|
448
|
+
if (!isLambdaNode(rawNode)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (rawNode._type === 'func' && rawNode.function === 'filter') {
|
|
452
|
+
processFilterNode(rawNode, results);
|
|
453
|
+
} else if (rawNode._type === 'func' && rawNode.parameters) {
|
|
454
|
+
walkLambdaBody(rawNode.parameters, results);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function processPostFilterNode(
|
|
460
|
+
rawNode: LambdaNode,
|
|
461
|
+
results: TDSServicePreFilter[],
|
|
462
|
+
): void {
|
|
463
|
+
if (rawNode.function === 'filter' && rawNode.parameters?.length === 2) {
|
|
464
|
+
const innerLambda = rawNode.parameters[1];
|
|
465
|
+
if (innerLambda?._type === 'lambda' && innerLambda.body) {
|
|
466
|
+
for (const bodyNode of innerLambda.body) {
|
|
467
|
+
collectIsNotNullChecks(bodyNode, results);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
extractIsNotNullPostFilters(rawNode.parameters, results);
|
|
471
|
+
} else if (rawNode.parameters) {
|
|
472
|
+
extractIsNotNullPostFilters(rawNode.parameters, results);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function extractIsNotNullPostFilters(
|
|
477
|
+
nodes: unknown[],
|
|
478
|
+
results: TDSServicePreFilter[],
|
|
479
|
+
): void {
|
|
480
|
+
for (const rawNode of nodes) {
|
|
481
|
+
if (!isLambdaNode(rawNode)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (rawNode._type !== 'func') {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
processPostFilterNode(rawNode, results);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function collectIsNotNullChecks(
|
|
492
|
+
node: LambdaNode,
|
|
493
|
+
results: TDSServicePreFilter[],
|
|
494
|
+
): void {
|
|
495
|
+
if (node._type === 'property' && node.property === 'isNotNull') {
|
|
496
|
+
const colNameNode = node.parameters?.[1];
|
|
497
|
+
if (
|
|
498
|
+
colNameNode?._type === 'string' &&
|
|
499
|
+
typeof colNameNode.value === 'string'
|
|
500
|
+
) {
|
|
501
|
+
results.push({
|
|
502
|
+
property: colNameNode.value,
|
|
503
|
+
operator: 'isNotNull',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (node._type === 'func' && node.function === 'and' && node.parameters) {
|
|
509
|
+
for (const param of node.parameters) {
|
|
510
|
+
collectIsNotNullChecks(param, results);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Extracts hardcoded pre-filter constraints from a raw lambda body.
|
|
517
|
+
*
|
|
518
|
+
* Walks the lambda JSON tree to find:
|
|
519
|
+
* - `equal` comparisons with literal values (e.g. `fsymId == 'D7HG0X-S'`)
|
|
520
|
+
* - `isEmpty` checks (e.g. `consEndDate->isEmpty()`)
|
|
521
|
+
* - `isNotNull` post-projection TDS row checks (e.g. `row.isNotNull('Fe Mean')`)
|
|
522
|
+
*
|
|
523
|
+
* The `rawLambdaBody` parameter is `RawLambda.body` — the raw JSON array
|
|
524
|
+
* from the PURE protocol.
|
|
525
|
+
*/
|
|
526
|
+
export function extractLambdaPreFilters(
|
|
527
|
+
rawLambdaBody: object | undefined,
|
|
528
|
+
): TDSServicePreFilter[] {
|
|
529
|
+
if (!rawLambdaBody || !Array.isArray(rawLambdaBody)) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const results: TDSServicePreFilter[] = [];
|
|
533
|
+
walkLambdaBody(rawLambdaBody, results);
|
|
534
|
+
extractIsNotNullPostFilters(rawLambdaBody, results);
|
|
535
|
+
return results;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Formats pre-filter constraints into a human-readable summary for LLM context.
|
|
540
|
+
* This generates a concise description of what's hardcoded in the service lambda
|
|
541
|
+
* so the AI avoids generating contradictory WHERE clauses.
|
|
542
|
+
*/
|
|
543
|
+
export function formatPreFiltersForContext(
|
|
544
|
+
preFilters: TDSServicePreFilter[],
|
|
545
|
+
): string {
|
|
546
|
+
if (preFilters.length === 0) {
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
const parts: string[] = [];
|
|
550
|
+
for (const pf of preFilters) {
|
|
551
|
+
const shortProp = pf.property.includes('.')
|
|
552
|
+
? (pf.property.split('.').pop() ?? pf.property)
|
|
553
|
+
: pf.property;
|
|
554
|
+
switch (pf.operator) {
|
|
555
|
+
case 'equal':
|
|
556
|
+
parts.push(`${shortProp} = '${String(pf.value)}'`);
|
|
557
|
+
break;
|
|
558
|
+
case 'isEmpty':
|
|
559
|
+
parts.push(`${shortProp} IS NULL (always)`);
|
|
560
|
+
break;
|
|
561
|
+
case 'isNotEmpty':
|
|
562
|
+
parts.push(`${shortProp} IS NOT NULL (always)`);
|
|
563
|
+
break;
|
|
564
|
+
case 'isNotNull':
|
|
565
|
+
parts.push(`${shortProp} IS NOT NULL (post-filter)`);
|
|
566
|
+
break;
|
|
567
|
+
default:
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return `Pre-applied filters: ${parts.join('; ')}`;
|
|
572
|
+
}
|