@duckcodeailabs/dql-cli 1.6.4 → 1.6.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.
- package/dist/apps-api.d.ts +19 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +335 -15
- package/dist/apps-api.js.map +1 -1
- package/dist/args.d.ts +11 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +21 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/index-D_tpetmE.js +3790 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-studio-import.js +23 -4
- package/dist/block-studio-import.js.map +1 -1
- package/dist/commands/agent.d.ts +2 -2
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +78 -13
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +3 -2
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/compile.d.ts +2 -0
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +33 -1
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/import.js +6 -6
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +17 -3
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +113 -10
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/local-runtime.d.ts +8 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +439 -37
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/promote-from-draft.d.ts +4 -4
- package/dist/promote-from-draft.js +8 -8
- package/dist/promote-from-draft.js.map +1 -1
- package/package.json +11 -11
- package/dist/assets/dql-notebook/assets/index-BFUUTIWF.js +0 -3618
package/dist/apps-api.d.ts
CHANGED
|
@@ -143,6 +143,19 @@ export declare function createAppPackage(projectRoot: string, input: AppCreateRe
|
|
|
143
143
|
ok: false;
|
|
144
144
|
error: string;
|
|
145
145
|
};
|
|
146
|
+
declare function selectedBlockContext(context: unknown): Record<string, unknown> | null;
|
|
147
|
+
declare function buildPreviewMetricSnapshot(preview: Record<string, unknown>, fallbackTitle?: string): Record<string, unknown>;
|
|
148
|
+
declare function buildPreviewDriverCards(preview: Record<string, unknown>, intent: LocalAppInvestigationIntent): Array<Record<string, unknown>>;
|
|
149
|
+
declare function buildDeterministicInvestigationSql(projectRoot: string, input: {
|
|
150
|
+
question: string;
|
|
151
|
+
intent: LocalAppInvestigationIntent;
|
|
152
|
+
selected: Record<string, unknown> | null;
|
|
153
|
+
sourceBlockId?: string;
|
|
154
|
+
}): {
|
|
155
|
+
sql: string;
|
|
156
|
+
sourceBlockPath: string;
|
|
157
|
+
sourceBlockName: string;
|
|
158
|
+
} | undefined;
|
|
146
159
|
declare function loadAppById(projectRoot: string, id: string): {
|
|
147
160
|
app: AppDocument;
|
|
148
161
|
dashboards: Array<{
|
|
@@ -187,5 +200,11 @@ export declare function previewNotebookForApp(projectRoot: string, appId: string
|
|
|
187
200
|
status: number;
|
|
188
201
|
error: string;
|
|
189
202
|
};
|
|
203
|
+
export declare const __test__: {
|
|
204
|
+
buildPreviewDriverCards: typeof buildPreviewDriverCards;
|
|
205
|
+
buildPreviewMetricSnapshot: typeof buildPreviewMetricSnapshot;
|
|
206
|
+
buildDeterministicInvestigationSql: typeof buildDeterministicInvestigationSql;
|
|
207
|
+
selectedBlockContext: typeof selectedBlockContext;
|
|
208
|
+
};
|
|
190
209
|
export {};
|
|
191
210
|
//# sourceMappingURL=apps-api.d.ts.map
|
package/dist/apps-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps-api.d.ts","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAQL,KAAK,WAAW,EAGjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAOL,KAAK,2BAA2B,EACjC,MAAM,6BAA6B,CAAC;AAErC,UAAU,GAAG;IACX,GAAG,EAAE,eAAe,CAAC;IACrB,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,iCAAiC,KAAK,OAAO,CAAC,gCAAgC,CAAC,CAAC;IACnH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"apps-api.d.ts","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"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"}
|
package/dist/apps-api.js
CHANGED
|
@@ -48,6 +48,7 @@ export async function handleAppsApi(ctx) {
|
|
|
48
48
|
sendJson(res, 400, { error: result.error });
|
|
49
49
|
return true;
|
|
50
50
|
}
|
|
51
|
+
await refreshGeneratedMetadata(projectRoot);
|
|
51
52
|
sendJson(res, 201, result);
|
|
52
53
|
}
|
|
53
54
|
catch (err) {
|
|
@@ -689,7 +690,7 @@ export async function generateAppPackage(projectRoot, input) {
|
|
|
689
690
|
if (!prompt)
|
|
690
691
|
return { ok: false, error: 'prompt is required' };
|
|
691
692
|
const selectedBlockIds = unique((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean));
|
|
692
|
-
const { KGStore, defaultKgPath, generateAppFromPlan, planAppFromPrompt, reindexProject, validateAppPlan, } = await import('@duckcodeailabs/dql-agent');
|
|
693
|
+
const { KGStore, defaultKgPath, generateAppFromPlan, ensureMetadataCatalogFresh, planAppFromPrompt, reindexProject, validateAppPlan, } = await import('@duckcodeailabs/dql-agent');
|
|
693
694
|
const kgPath = defaultKgPath(projectRoot);
|
|
694
695
|
await reindexProject(projectRoot, { kgPath });
|
|
695
696
|
const kg = new KGStore(kgPath);
|
|
@@ -705,6 +706,7 @@ export async function generateAppPackage(projectRoot, input) {
|
|
|
705
706
|
const generated = generateAppFromPlan(projectRoot, plan, kg, {
|
|
706
707
|
overwrite: Boolean(input.force),
|
|
707
708
|
});
|
|
709
|
+
await ensureMetadataCatalogFresh(projectRoot, { force: true });
|
|
708
710
|
const app = collectAppsList(projectRoot).find((entry) => entry.id === plan.appId) ?? null;
|
|
709
711
|
return {
|
|
710
712
|
ok: true,
|
|
@@ -1063,13 +1065,28 @@ async function runAppInvestigation(ctx, storage, investigation, input = {}) {
|
|
|
1063
1065
|
const previews = buildContextPreviews(selected);
|
|
1064
1066
|
let metricSnapshot = buildMetricSnapshot(selected);
|
|
1065
1067
|
let driverCards = buildDriverCards(selected, intent);
|
|
1066
|
-
const
|
|
1068
|
+
const baselineGap = intent === 'diagnose_change' && hasSelectedRows(selected) && !hasComparableTimeBaseline(selected);
|
|
1069
|
+
const metadataOnly = intent === 'trust_gap_review';
|
|
1070
|
+
if (metadataOnly)
|
|
1071
|
+
generatedSql = '';
|
|
1072
|
+
const sourceTileId = investigation.sourceTileId ?? selectedContextString(context, 'tileId');
|
|
1073
|
+
const sourceBlockId = investigation.sourceBlockId ?? selectedContextString(context, 'blockId');
|
|
1074
|
+
const deterministicGeneration = generatedSql || baselineGap || metadataOnly
|
|
1075
|
+
? undefined
|
|
1076
|
+
: buildDeterministicInvestigationSql(ctx.projectRoot, {
|
|
1077
|
+
question,
|
|
1078
|
+
intent,
|
|
1079
|
+
selected,
|
|
1080
|
+
sourceBlockId,
|
|
1081
|
+
});
|
|
1082
|
+
generatedSql = generatedSql || deterministicGeneration?.sql;
|
|
1083
|
+
const agentGeneration = generatedSql || baselineGap || metadataOnly
|
|
1067
1084
|
? undefined
|
|
1068
1085
|
: await generateInvestigationSql(ctx, {
|
|
1069
1086
|
appId: investigation.appId,
|
|
1070
1087
|
dashboardId: investigation.dashboardId ?? selectedString(context, 'dashboardId'),
|
|
1071
|
-
sourceTileId
|
|
1072
|
-
sourceBlockId
|
|
1088
|
+
sourceTileId,
|
|
1089
|
+
sourceBlockId,
|
|
1073
1090
|
title: investigation.title,
|
|
1074
1091
|
question,
|
|
1075
1092
|
intent,
|
|
@@ -1079,7 +1096,7 @@ async function runAppInvestigation(ctx, storage, investigation, input = {}) {
|
|
|
1079
1096
|
const generationError = cleanString(agentGeneration?.executionError);
|
|
1080
1097
|
const sqlEvidence = agentGeneration?.result
|
|
1081
1098
|
? { preview: buildGeneratedSqlPreview(agentGeneration.result, generatedSql), error: generationError || undefined }
|
|
1082
|
-
: await runGeneratedSqlPreview(ctx, generatedSql);
|
|
1099
|
+
: metadataOnly ? {} : await runGeneratedSqlPreview(ctx, generatedSql);
|
|
1083
1100
|
const sqlError = sqlEvidence.error ?? generationError;
|
|
1084
1101
|
if (sqlEvidence.preview) {
|
|
1085
1102
|
previews.unshift(sqlEvidence.preview);
|
|
@@ -1095,6 +1112,10 @@ async function runAppInvestigation(ctx, storage, investigation, input = {}) {
|
|
|
1095
1112
|
generatedSql: generatedSql || undefined,
|
|
1096
1113
|
sqlExecuted: Boolean(sqlEvidence.preview),
|
|
1097
1114
|
sqlError,
|
|
1115
|
+
generationSource: baselineGap ? 'missing_baseline' : deterministicGeneration ? 'selected_block_metadata' : agentGeneration?.providerUsed ? 'ai_provider' : generatedSql ? 'provided_sql' : 'context_only',
|
|
1116
|
+
baselineGap,
|
|
1117
|
+
sourceBlockPath: deterministicGeneration?.sourceBlockPath,
|
|
1118
|
+
sourceBlockName: deterministicGeneration?.sourceBlockName,
|
|
1098
1119
|
providerUsed: agentGeneration?.providerUsed,
|
|
1099
1120
|
},
|
|
1100
1121
|
certifiedContext: {
|
|
@@ -1102,18 +1123,26 @@ async function runAppInvestigation(ctx, storage, investigation, input = {}) {
|
|
|
1102
1123
|
appName: appInfo?.app.name,
|
|
1103
1124
|
dashboardId: investigation.dashboardId,
|
|
1104
1125
|
dashboardTitle: selectedString(context, 'dashboardTitle'),
|
|
1105
|
-
sourceTileId
|
|
1106
|
-
sourceBlockId
|
|
1126
|
+
sourceTileId,
|
|
1127
|
+
sourceBlockId,
|
|
1128
|
+
sourceBlockPath: deterministicGeneration?.sourceBlockPath ?? selectedString(selected, 'blockPath'),
|
|
1107
1129
|
certificationStatus: selectedString(selected, 'certificationStatus'),
|
|
1108
1130
|
},
|
|
1109
|
-
assumptions:
|
|
1131
|
+
assumptions: [
|
|
1132
|
+
...investigationAssumptions(intent, selected, generatedSql, sqlError),
|
|
1133
|
+
...(baselineGap ? ['The selected tile sample does not include at least two comparable time values, so DQL did not invent a change query from an unrelated table.'] : []),
|
|
1134
|
+
],
|
|
1110
1135
|
context,
|
|
1111
1136
|
agentEvidence: agentGeneration?.evidence,
|
|
1112
1137
|
analysisPlan: agentGeneration?.analysisPlan,
|
|
1113
1138
|
citations: agentGeneration?.citations,
|
|
1114
1139
|
};
|
|
1115
|
-
const summary = cleanString(agentGeneration?.answer) ||
|
|
1116
|
-
|
|
1140
|
+
const summary = cleanString(agentGeneration?.answer) || (baselineGap
|
|
1141
|
+
? buildMissingBaselineSummary(question, selected)
|
|
1142
|
+
: buildInvestigationSummary(intent, question, selected, metricSnapshot, driverCards));
|
|
1143
|
+
const recommendation = baselineGap
|
|
1144
|
+
? buildMissingBaselineRecommendation(selected)
|
|
1145
|
+
: buildInvestigationRecommendation(intent, selected, sqlError);
|
|
1117
1146
|
return storage.updateAppInvestigation(investigation.id, {
|
|
1118
1147
|
title: cleanString(input.question) ? titleFromInvestigation(question, selected) : investigation.title,
|
|
1119
1148
|
question,
|
|
@@ -1174,7 +1203,14 @@ function safeIntentContext(context) {
|
|
|
1174
1203
|
}
|
|
1175
1204
|
function selectedBlockContext(context) {
|
|
1176
1205
|
const root = asRecord(context);
|
|
1177
|
-
|
|
1206
|
+
const selected = asRecord(root?.selectedBlock);
|
|
1207
|
+
if (selected)
|
|
1208
|
+
return selected;
|
|
1209
|
+
if (!root)
|
|
1210
|
+
return null;
|
|
1211
|
+
const hasSelectedTileContext = ['blockId', 'blockPath', 'tileId', 'certificationStatus', 'resultSample', 'rowCount']
|
|
1212
|
+
.some((key) => root[key] !== undefined && root[key] !== null);
|
|
1213
|
+
return hasSelectedTileContext ? root : null;
|
|
1178
1214
|
}
|
|
1179
1215
|
function selectedContextString(context, key) {
|
|
1180
1216
|
return selectedString(selectedBlockContext(context), key);
|
|
@@ -1226,6 +1262,24 @@ function buildMetricSnapshot(selected) {
|
|
|
1226
1262
|
context: selectedString(selected, 'title') ?? 'Selected dashboard tile',
|
|
1227
1263
|
};
|
|
1228
1264
|
}
|
|
1265
|
+
function hasSelectedRows(selected) {
|
|
1266
|
+
return selectedRows(selected).length > 0;
|
|
1267
|
+
}
|
|
1268
|
+
function hasComparableTimeBaseline(selected) {
|
|
1269
|
+
const rows = selectedRows(selected);
|
|
1270
|
+
if (rows.length < 2)
|
|
1271
|
+
return false;
|
|
1272
|
+
const columns = selectedColumns(selected, rows);
|
|
1273
|
+
const profile = profileResultColumns(columns, rows);
|
|
1274
|
+
const timeDimension = chooseTimeDimension(profile);
|
|
1275
|
+
if (!timeDimension)
|
|
1276
|
+
return false;
|
|
1277
|
+
const values = new Set(rows
|
|
1278
|
+
.map((row) => row[timeDimension.name])
|
|
1279
|
+
.filter((value) => value !== null && value !== undefined && String(value).trim())
|
|
1280
|
+
.map((value) => String(value)));
|
|
1281
|
+
return values.size >= 2;
|
|
1282
|
+
}
|
|
1229
1283
|
function buildDriverCards(selected, intent) {
|
|
1230
1284
|
const rows = selectedRows(selected);
|
|
1231
1285
|
const columns = selectedColumns(selected, rows);
|
|
@@ -1274,7 +1328,8 @@ function buildPreviewMetricSnapshot(preview, fallbackTitle) {
|
|
|
1274
1328
|
};
|
|
1275
1329
|
}
|
|
1276
1330
|
const currentColumn = pickColumn(numericColumns, [/^current_/i, /current.*(revenue|value|amount|total|orders?)/i])
|
|
1277
|
-
?? pickColumn(numericColumns, [/(revenue|value|amount|total|orders?|
|
|
1331
|
+
?? pickColumn(numericColumns, [/^total_/i, /(revenue|value|amount|total|orders?|points?|goals?|assists?|rebounds?|score|games_played)$/i])
|
|
1332
|
+
?? pickColumn(numericColumns, [/(count|row_count)$/i])
|
|
1278
1333
|
?? numericColumns[0];
|
|
1279
1334
|
const baselineColumn = pickColumn(numericColumns, [/^baseline_/i, /baseline.*(revenue|value|amount|total|orders?)/i]);
|
|
1280
1335
|
const deltaColumn = pickColumn(numericColumns, [/(delta|change|variance|diff|contribution)/i]);
|
|
@@ -1302,7 +1357,8 @@ function buildPreviewDriverCards(preview, intent) {
|
|
|
1302
1357
|
}
|
|
1303
1358
|
const numericColumns = columns.filter((column) => rows.some((row) => typeofNumber(row[column]) !== null));
|
|
1304
1359
|
const contributionColumn = pickColumn(numericColumns, [/(delta|change|variance|diff|contribution)/i])
|
|
1305
|
-
?? pickColumn(numericColumns, [/^current_/i, /(revenue|value|amount|total|orders?|
|
|
1360
|
+
?? pickColumn(numericColumns, [/^current_/i, /^total_/i, /(revenue|value|amount|total|orders?|points?|goals?|assists?|rebounds?|score|games_played)$/i])
|
|
1361
|
+
?? pickColumn(numericColumns, [/(count|row_count)$/i])
|
|
1306
1362
|
?? numericColumns[0];
|
|
1307
1363
|
const dimensionColumn = columns.find((column) => column !== contributionColumn && rows.some((row) => typeof row[column] === 'string'));
|
|
1308
1364
|
if (!contributionColumn) {
|
|
@@ -1355,6 +1411,229 @@ function sumNumericRows(rows, column) {
|
|
|
1355
1411
|
}
|
|
1356
1412
|
return found ? total : null;
|
|
1357
1413
|
}
|
|
1414
|
+
function buildDeterministicInvestigationSql(projectRoot, input) {
|
|
1415
|
+
if (input.intent === 'trust_gap_review')
|
|
1416
|
+
return undefined;
|
|
1417
|
+
const block = resolveSelectedBlock(projectRoot, input.selected, input.sourceBlockId);
|
|
1418
|
+
if (!block)
|
|
1419
|
+
return undefined;
|
|
1420
|
+
const source = readFileSync(join(projectRoot, block.path), 'utf-8');
|
|
1421
|
+
const blockSql = extractDqlQuery(source);
|
|
1422
|
+
if (!blockSql || /\{\{/.test(blockSql) || !isReadOnlySql(blockSql))
|
|
1423
|
+
return undefined;
|
|
1424
|
+
const rows = selectedRows(input.selected);
|
|
1425
|
+
const columns = selectedColumns(input.selected, rows);
|
|
1426
|
+
const sourceSql = stripTopLevelOrderAndLimit(blockSql);
|
|
1427
|
+
const profile = profileResultColumns(columns, rows);
|
|
1428
|
+
const measure = chooseMeasureColumn(profile);
|
|
1429
|
+
if (!measure)
|
|
1430
|
+
return undefined;
|
|
1431
|
+
const dimension = chooseDimensionColumn(input.question, profile, input.intent);
|
|
1432
|
+
const sourceCte = `WITH dql_source AS (\n${sourceSql}\n)`;
|
|
1433
|
+
if (input.intent === 'entity_drilldown') {
|
|
1434
|
+
const entity = inferEntityFilter(input.question, profile, rows);
|
|
1435
|
+
const orderBy = `ORDER BY ${quoteSqlIdentifier(measure.name)} DESC`;
|
|
1436
|
+
const where = entity ? `\nWHERE ${quoteSqlIdentifier(entity.column)} IS NOT NULL AND LOWER(CAST(${quoteSqlIdentifier(entity.column)} AS VARCHAR)) LIKE ${sqlStringLiteral(`%${entity.value.toLowerCase()}%`)}` : '';
|
|
1437
|
+
return {
|
|
1438
|
+
sql: `${sourceCte}\nSELECT *\nFROM dql_source${where}\n${orderBy}\nLIMIT 100`,
|
|
1439
|
+
sourceBlockPath: block.path,
|
|
1440
|
+
sourceBlockName: block.name,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
if (input.intent === 'anomaly_investigation' || input.intent === 'diagnose_change') {
|
|
1444
|
+
const timeDimension = chooseTimeDimension(profile) ?? dimension;
|
|
1445
|
+
const rankExpr = `${measureAgg(measure)}(${quoteSqlIdentifier(measure.name)})`;
|
|
1446
|
+
if (timeDimension) {
|
|
1447
|
+
return {
|
|
1448
|
+
sql: [
|
|
1449
|
+
sourceCte,
|
|
1450
|
+
', dql_trend AS (',
|
|
1451
|
+
` SELECT ${quoteSqlIdentifier(timeDimension.name)} AS ${quoteSqlIdentifier(timeDimension.name)}, ${rankExpr} AS ${quoteSqlIdentifier(measure.name)}`,
|
|
1452
|
+
' FROM dql_source',
|
|
1453
|
+
` GROUP BY ${quoteSqlIdentifier(timeDimension.name)}`,
|
|
1454
|
+
'), dql_deltas AS (',
|
|
1455
|
+
` SELECT ${quoteSqlIdentifier(timeDimension.name)}, ${quoteSqlIdentifier(measure.name)}, LAG(${quoteSqlIdentifier(measure.name)}) OVER (ORDER BY ${quoteSqlIdentifier(timeDimension.name)}) AS baseline_${safeAlias(measure.name)}`,
|
|
1456
|
+
' FROM dql_trend',
|
|
1457
|
+
')',
|
|
1458
|
+
`SELECT *, ${quoteSqlIdentifier(measure.name)} - baseline_${safeAlias(measure.name)} AS delta_${safeAlias(measure.name)}`,
|
|
1459
|
+
'FROM dql_deltas',
|
|
1460
|
+
`ORDER BY ABS(COALESCE(delta_${safeAlias(measure.name)}, 0)) DESC`,
|
|
1461
|
+
'LIMIT 20',
|
|
1462
|
+
].join('\n'),
|
|
1463
|
+
sourceBlockPath: block.path,
|
|
1464
|
+
sourceBlockName: block.name,
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (!dimension)
|
|
1469
|
+
return undefined;
|
|
1470
|
+
const aggregate = `${measureAgg(measure)}(${quoteSqlIdentifier(measure.name)})`;
|
|
1471
|
+
const label = quoteSqlIdentifier(dimension.name);
|
|
1472
|
+
return {
|
|
1473
|
+
sql: [
|
|
1474
|
+
sourceCte,
|
|
1475
|
+
`SELECT ${label} AS ${label}, ${aggregate} AS ${quoteSqlIdentifier(measure.name)}, COUNT(*) AS ${quoteSqlIdentifier('row_count')}`,
|
|
1476
|
+
'FROM dql_source',
|
|
1477
|
+
`GROUP BY ${label}`,
|
|
1478
|
+
`ORDER BY ABS(COALESCE(${quoteSqlIdentifier(measure.name)}, 0)) DESC`,
|
|
1479
|
+
'LIMIT 20',
|
|
1480
|
+
].join('\n'),
|
|
1481
|
+
sourceBlockPath: block.path,
|
|
1482
|
+
sourceBlockName: block.name,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
function resolveSelectedBlock(projectRoot, selected, sourceBlockId) {
|
|
1486
|
+
const selectedPath = selectedString(selected, 'blockPath');
|
|
1487
|
+
const candidates = collectBlockCandidates(projectRoot);
|
|
1488
|
+
if (selectedPath) {
|
|
1489
|
+
const normalizedPath = selectedPath.replace(/^\/+/, '');
|
|
1490
|
+
const found = candidates.find((block) => block.path === normalizedPath);
|
|
1491
|
+
if (found)
|
|
1492
|
+
return found;
|
|
1493
|
+
if (normalizedPath.startsWith('blocks/') && existsSync(join(projectRoot, normalizedPath))) {
|
|
1494
|
+
const source = readFileSync(join(projectRoot, normalizedPath), 'utf-8');
|
|
1495
|
+
const name = matchString(source, /block\s+"([^"]+)"/) ?? titleFromPath(normalizedPath);
|
|
1496
|
+
return {
|
|
1497
|
+
id: name,
|
|
1498
|
+
name,
|
|
1499
|
+
domain: matchString(source, /domain\s*=\s*"([^"]+)"/) ?? 'uncategorized',
|
|
1500
|
+
status: matchString(source, /status\s*=\s*"([^"]+)"/) ?? 'draft',
|
|
1501
|
+
owner: matchString(source, /owner\s*=\s*"([^"]+)"/),
|
|
1502
|
+
tags: matchArray(source, /tags\s*=\s*\[([^\]]*)\]/),
|
|
1503
|
+
path: normalizedPath,
|
|
1504
|
+
lastModified: statSyncSafe(join(projectRoot, normalizedPath))?.mtime.toISOString() ?? new Date(0).toISOString(),
|
|
1505
|
+
description: matchString(source, /description\s*=\s*"((?:[^"\\]|\\.)*)"/) ?? '',
|
|
1506
|
+
llmContext: matchString(source, /llmContext\s*=\s*"((?:[^"\\]|\\.)*)"/),
|
|
1507
|
+
chartType: matchString(source, /chart\s*=\s*"([^"]+)"/) ?? undefined,
|
|
1508
|
+
score: 0,
|
|
1509
|
+
reasons: [],
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const id = cleanString(sourceBlockId) || selectedString(selected, 'blockId');
|
|
1514
|
+
if (!id)
|
|
1515
|
+
return undefined;
|
|
1516
|
+
return candidates.find((block) => block.id === id || block.name === id || block.path === id);
|
|
1517
|
+
}
|
|
1518
|
+
function profileResultColumns(columns, rows) {
|
|
1519
|
+
return columns.map((name) => {
|
|
1520
|
+
const lower = name.toLowerCase();
|
|
1521
|
+
const numeric = rows.length === 0 ? !isLikelyTextColumn(lower) : rows.some((row) => typeofNumber(row[name]) !== null);
|
|
1522
|
+
const text = rows.some((row) => typeof row[name] === 'string' && String(row[name]).trim().length > 0);
|
|
1523
|
+
const time = /\b(season|year|month|week|quarter|date|day)\b/i.test(lower);
|
|
1524
|
+
const identifier = /\b(id|key|uuid|number)\b/i.test(lower) && !time;
|
|
1525
|
+
const measureName = /\b(total|sum|amount|revenue|sales|points?|goals?|assists?|rebounds?|count|avg|average|rate|pct|percent|score|value|delta|change|variance)\b/i.test(lower);
|
|
1526
|
+
const dimensionName = /\b(name|type|segment|region|market|category|status|player|customer|account|team|season|year|month|week|quarter|date)\b/i.test(lower);
|
|
1527
|
+
const measure = numeric && measureName && !identifier && !time;
|
|
1528
|
+
const dimension = !measure && (text || time || dimensionName || !numeric);
|
|
1529
|
+
return { name, lower, numeric, text, dimension, measure, time };
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
function chooseMeasureColumn(columns) {
|
|
1533
|
+
const candidates = columns.filter((column) => column.measure);
|
|
1534
|
+
return candidates.find((column) => /\b(delta|change|variance|contribution)\b/i.test(column.lower))
|
|
1535
|
+
?? candidates.find((column) => /\b(total_points|total_revenue|total_amount|total|revenue|amount|sales|points|goals)\b/i.test(column.lower))
|
|
1536
|
+
?? candidates.find((column) => /\b(count|value|score|avg|average|rate|pct|percent)\b/i.test(column.lower))
|
|
1537
|
+
?? columns.find((column) => column.numeric && !column.dimension);
|
|
1538
|
+
}
|
|
1539
|
+
function chooseDimensionColumn(question, columns, intent) {
|
|
1540
|
+
const dimensions = columns.filter((column) => column.dimension);
|
|
1541
|
+
const questionTokens = new Set(question.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
1542
|
+
const mentioned = dimensions.find((column) => column.lower.split(/[^a-z0-9]+/).some((token) => questionTokens.has(token)));
|
|
1543
|
+
if (mentioned)
|
|
1544
|
+
return mentioned;
|
|
1545
|
+
const timeDimension = chooseTimeDimension(columns);
|
|
1546
|
+
if ((intent === 'diagnose_change' || intent === 'segment_compare' || intent === 'anomaly_investigation') && timeDimension)
|
|
1547
|
+
return timeDimension;
|
|
1548
|
+
return dimensions.find((column) => column.text && !column.time)
|
|
1549
|
+
?? timeDimension
|
|
1550
|
+
?? dimensions[0];
|
|
1551
|
+
}
|
|
1552
|
+
function chooseTimeDimension(columns) {
|
|
1553
|
+
return columns.find((column) => column.time);
|
|
1554
|
+
}
|
|
1555
|
+
function inferEntityFilter(question, columns, rows) {
|
|
1556
|
+
const textDimensions = columns.filter((column) => column.dimension && (column.text || /\b(name|player|customer|account|team)\b/i.test(column.lower)));
|
|
1557
|
+
const lowerQuestion = question.toLowerCase();
|
|
1558
|
+
for (const column of textDimensions) {
|
|
1559
|
+
for (const row of rows) {
|
|
1560
|
+
const value = cleanString(row[column.name]);
|
|
1561
|
+
if (value && lowerQuestion.includes(value.toLowerCase()))
|
|
1562
|
+
return { column: column.name, value };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const named = question.match(/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/);
|
|
1566
|
+
const value = named?.[1]?.trim();
|
|
1567
|
+
const column = textDimensions[0];
|
|
1568
|
+
return value && column ? { column: column.name, value } : undefined;
|
|
1569
|
+
}
|
|
1570
|
+
function measureAgg(column) {
|
|
1571
|
+
return /\b(avg|average|rate|pct|percent|per_)\b/i.test(column.lower) ? 'AVG' : 'SUM';
|
|
1572
|
+
}
|
|
1573
|
+
function extractDqlQuery(source) {
|
|
1574
|
+
const tripleQuoteMatch = source.match(/query\s*=\s*"""([\s\S]*?)"""/i);
|
|
1575
|
+
if (tripleQuoteMatch)
|
|
1576
|
+
return tripleQuoteMatch[1].trim() || null;
|
|
1577
|
+
const singleQuoteMatch = source.match(/query\s*=\s*"((?:[^"\\]|\\.)*)"/i);
|
|
1578
|
+
if (singleQuoteMatch)
|
|
1579
|
+
return singleQuoteMatch[1].replace(/\\"/g, '"').trim() || null;
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
function stripTopLevelOrderAndLimit(sql) {
|
|
1583
|
+
let next = sql.trim().replace(/;+\s*$/g, '');
|
|
1584
|
+
const limitIndex = findLastTopLevelKeyword(next, 'limit');
|
|
1585
|
+
if (limitIndex >= 0 && /^\s+limit\s+\d+\s*$/i.test(next.slice(limitIndex))) {
|
|
1586
|
+
next = next.slice(0, limitIndex).trim();
|
|
1587
|
+
}
|
|
1588
|
+
const orderIndex = findLastTopLevelKeyword(next, 'order by');
|
|
1589
|
+
if (orderIndex >= 0)
|
|
1590
|
+
next = next.slice(0, orderIndex).trim();
|
|
1591
|
+
return next;
|
|
1592
|
+
}
|
|
1593
|
+
function findLastTopLevelKeyword(sql, keyword) {
|
|
1594
|
+
const lower = sql.toLowerCase();
|
|
1595
|
+
const target = keyword.toLowerCase();
|
|
1596
|
+
let depth = 0;
|
|
1597
|
+
let quote = null;
|
|
1598
|
+
let last = -1;
|
|
1599
|
+
for (let i = 0; i < lower.length; i += 1) {
|
|
1600
|
+
const char = lower[i];
|
|
1601
|
+
if (quote) {
|
|
1602
|
+
if (char === quote && lower[i - 1] !== '\\')
|
|
1603
|
+
quote = null;
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1607
|
+
quote = char;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (char === '(')
|
|
1611
|
+
depth += 1;
|
|
1612
|
+
if (char === ')')
|
|
1613
|
+
depth = Math.max(0, depth - 1);
|
|
1614
|
+
if (depth === 0 && lower.startsWith(target, i) && isKeywordBoundary(lower, i - 1) && isKeywordBoundary(lower, i + target.length)) {
|
|
1615
|
+
last = i;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return last;
|
|
1619
|
+
}
|
|
1620
|
+
function isKeywordBoundary(value, index) {
|
|
1621
|
+
if (index < 0 || index >= value.length)
|
|
1622
|
+
return true;
|
|
1623
|
+
return /[^a-z0-9_]/i.test(value[index]);
|
|
1624
|
+
}
|
|
1625
|
+
function quoteSqlIdentifier(identifier) {
|
|
1626
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1627
|
+
}
|
|
1628
|
+
function sqlStringLiteral(value) {
|
|
1629
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1630
|
+
}
|
|
1631
|
+
function safeAlias(identifier) {
|
|
1632
|
+
return identifier.replace(/[^a-z0-9_]+/gi, '_').replace(/^_+|_+$/g, '') || 'value';
|
|
1633
|
+
}
|
|
1634
|
+
function isLikelyTextColumn(value) {
|
|
1635
|
+
return /\b(name|type|segment|region|market|category|status|player|customer|account|team)\b/i.test(value);
|
|
1636
|
+
}
|
|
1358
1637
|
async function generateInvestigationSql(ctx, input) {
|
|
1359
1638
|
if (!ctx.generateInvestigationSql)
|
|
1360
1639
|
return undefined;
|
|
@@ -1432,13 +1711,26 @@ function buildGeneratedSqlPreview(result, generatedSql) {
|
|
|
1432
1711
|
};
|
|
1433
1712
|
}
|
|
1434
1713
|
function isReadOnlySql(sql) {
|
|
1435
|
-
const trimmed = sql
|
|
1714
|
+
const trimmed = stripLeadingSqlComments(sql).replace(/;+\s*$/g, '');
|
|
1436
1715
|
if (!/^(select|with)\b/i.test(trimmed))
|
|
1437
1716
|
return false;
|
|
1438
1717
|
if (/;\s*\S/.test(trimmed))
|
|
1439
1718
|
return false;
|
|
1440
1719
|
return !/\b(insert|update|delete|merge|drop|alter|create|truncate|copy|grant|revoke|call|execute|attach|detach)\b/i.test(trimmed);
|
|
1441
1720
|
}
|
|
1721
|
+
function stripLeadingSqlComments(sql) {
|
|
1722
|
+
let next = sql.trim();
|
|
1723
|
+
while (next.startsWith('--') || next.startsWith('/*')) {
|
|
1724
|
+
if (next.startsWith('--')) {
|
|
1725
|
+
const lineEnd = next.indexOf('\n');
|
|
1726
|
+
next = lineEnd >= 0 ? next.slice(lineEnd + 1).trimStart() : '';
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const blockEnd = next.indexOf('*/');
|
|
1730
|
+
next = blockEnd >= 0 ? next.slice(blockEnd + 2).trimStart() : '';
|
|
1731
|
+
}
|
|
1732
|
+
return next;
|
|
1733
|
+
}
|
|
1442
1734
|
function boundedPreviewSql(sql) {
|
|
1443
1735
|
return `SELECT * FROM (${sql.trim().replace(/;+\s*$/g, '')}) AS dql_research_preview LIMIT 100`;
|
|
1444
1736
|
}
|
|
@@ -1487,6 +1779,14 @@ function buildInvestigationSummary(intent, question, selected, metrics, drivers)
|
|
|
1487
1779
|
const driver = drivers[0]?.title ? ` Top visible driver in the current evidence is ${drivers[0].title}.` : '';
|
|
1488
1780
|
return `DQL opened a review-required investigation for ${target}: ${question}.${delta}${driver}`;
|
|
1489
1781
|
}
|
|
1782
|
+
function buildMissingBaselineSummary(question, selected) {
|
|
1783
|
+
const target = selectedString(selected, 'title') ?? 'the selected tile';
|
|
1784
|
+
return `DQL opened a review-required investigation for ${target}: ${question}. The selected tile shows the current certified result, but its sample does not include a comparable prior period or historical snapshot, so DQL cannot calculate what changed without guessing.`;
|
|
1785
|
+
}
|
|
1786
|
+
function buildMissingBaselineRecommendation(selected) {
|
|
1787
|
+
const target = selectedString(selected, 'title') ?? 'this tile';
|
|
1788
|
+
return `Use ${target} as current-state evidence. To explain change, add or select a block with a time grain, snapshot date, or prior-period baseline, then rerun the investigation.`;
|
|
1789
|
+
}
|
|
1490
1790
|
function buildInvestigationRecommendation(intent, selected, sqlError) {
|
|
1491
1791
|
if (sqlError)
|
|
1492
1792
|
return 'Review the generated SQL or add a certified drilldown block before promoting this result.';
|
|
@@ -1525,7 +1825,11 @@ function titleFromInvestigation(question, selected) {
|
|
|
1525
1825
|
return base.replace(/\s+/g, ' ').slice(0, 90);
|
|
1526
1826
|
}
|
|
1527
1827
|
function selectedRows(selected) {
|
|
1528
|
-
const rows = Array.isArray(selected?.sampleRows)
|
|
1828
|
+
const rows = Array.isArray(selected?.sampleRows)
|
|
1829
|
+
? selected?.sampleRows
|
|
1830
|
+
: Array.isArray(selected?.resultSample)
|
|
1831
|
+
? selected?.resultSample
|
|
1832
|
+
: selected?.rows;
|
|
1529
1833
|
if (!Array.isArray(rows))
|
|
1530
1834
|
return [];
|
|
1531
1835
|
return rows.map(asRecord).filter((row) => Boolean(row)).slice(0, 100);
|
|
@@ -2187,6 +2491,16 @@ function activatePersona(projectRoot, userId, appId) {
|
|
|
2187
2491
|
return null;
|
|
2188
2492
|
}
|
|
2189
2493
|
// ---- IO utilities ----
|
|
2494
|
+
async function refreshGeneratedMetadata(projectRoot) {
|
|
2495
|
+
try {
|
|
2496
|
+
const { ensureMetadataCatalogFresh } = await import('@duckcodeailabs/dql-agent');
|
|
2497
|
+
await ensureMetadataCatalogFresh(projectRoot, { force: true });
|
|
2498
|
+
}
|
|
2499
|
+
catch {
|
|
2500
|
+
// App files remain the source of truth; the local catalog refreshes again
|
|
2501
|
+
// on the next agent/MCP call if this best-effort update fails.
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2190
2504
|
function sendJson(res, status, body) {
|
|
2191
2505
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
2192
2506
|
res.end(JSON.stringify(body));
|
|
@@ -2209,6 +2523,12 @@ async function readJson(req) {
|
|
|
2209
2523
|
req.on('error', reject);
|
|
2210
2524
|
});
|
|
2211
2525
|
}
|
|
2526
|
+
export const __test__ = {
|
|
2527
|
+
buildPreviewDriverCards,
|
|
2528
|
+
buildPreviewMetricSnapshot,
|
|
2529
|
+
buildDeterministicInvestigationSql,
|
|
2530
|
+
selectedBlockContext,
|
|
2531
|
+
};
|
|
2212
2532
|
// reference unused parseAppDocument/readFileSync to keep import stable for forward use
|
|
2213
2533
|
void parseAppDocument;
|
|
2214
2534
|
void readFileSync;
|