@duckcodeailabs/dql-cli 1.6.2 → 1.6.4
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 +25 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +682 -0
- package/dist/apps-api.js.map +1 -1
- package/dist/args.d.ts +1 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +4 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{index-60sOoPrg.js → index-BFUUTIWF.js} +657 -638
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-studio-import.d.ts +1 -1
- package/dist/block-studio-import.d.ts.map +1 -1
- package/dist/block-studio-import.js +44 -7
- package/dist/block-studio-import.js.map +1 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +80 -11
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +32 -2
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/local-runtime.d.ts +24 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +493 -62
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +44 -0
- package/package.json +12 -12
- package/dist/apps-api.test.d.ts +0 -2
- package/dist/apps-api.test.d.ts.map +0 -1
- package/dist/apps-api.test.js +0 -196
- package/dist/apps-api.test.js.map +0 -1
- package/dist/args.test.d.ts +0 -2
- package/dist/args.test.d.ts.map +0 -1
- package/dist/args.test.js +0 -41
- package/dist/args.test.js.map +0 -1
- package/dist/block-studio-import.test.d.ts +0 -2
- package/dist/block-studio-import.test.d.ts.map +0 -1
- package/dist/block-studio-import.test.js +0 -168
- package/dist/block-studio-import.test.js.map +0 -1
- package/dist/commands/build.test.d.ts +0 -2
- package/dist/commands/build.test.d.ts.map +0 -1
- package/dist/commands/build.test.js +0 -44
- package/dist/commands/build.test.js.map +0 -1
- package/dist/commands/compile.test.d.ts +0 -2
- package/dist/commands/compile.test.d.ts.map +0 -1
- package/dist/commands/compile.test.js +0 -115
- package/dist/commands/compile.test.js.map +0 -1
- package/dist/commands/doctor.test.d.ts +0 -2
- package/dist/commands/doctor.test.d.ts.map +0 -1
- package/dist/commands/doctor.test.js +0 -44
- package/dist/commands/doctor.test.js.map +0 -1
- package/dist/commands/init.test.d.ts +0 -2
- package/dist/commands/init.test.d.ts.map +0 -1
- package/dist/commands/init.test.js +0 -182
- package/dist/commands/init.test.js.map +0 -1
- package/dist/commands/new.test.d.ts +0 -2
- package/dist/commands/new.test.d.ts.map +0 -1
- package/dist/commands/new.test.js +0 -297
- package/dist/commands/new.test.js.map +0 -1
- package/dist/commands/sync.test.d.ts +0 -2
- package/dist/commands/sync.test.d.ts.map +0 -1
- package/dist/commands/sync.test.js +0 -147
- package/dist/commands/sync.test.js.map +0 -1
- package/dist/commands/validate.test.d.ts +0 -2
- package/dist/commands/validate.test.d.ts.map +0 -1
- package/dist/commands/validate.test.js +0 -140
- package/dist/commands/validate.test.js.map +0 -1
- package/dist/local-runtime.test.d.ts +0 -2
- package/dist/local-runtime.test.d.ts.map +0 -1
- package/dist/local-runtime.test.js +0 -363
- package/dist/local-runtime.test.js.map +0 -1
- package/dist/metricflow.test.d.ts +0 -2
- package/dist/metricflow.test.d.ts.map +0 -1
- package/dist/metricflow.test.js +0 -54
- package/dist/metricflow.test.js.map +0 -1
- package/dist/promote-from-draft.test.d.ts +0 -2
- package/dist/promote-from-draft.test.d.ts.map +0 -1
- package/dist/promote-from-draft.test.js +0 -149
- package/dist/promote-from-draft.test.js.map +0 -1
- package/dist/semantic-import.test.d.ts +0 -2
- package/dist/semantic-import.test.d.ts.map +0 -1
- package/dist/semantic-import.test.js +0 -95
- package/dist/semantic-import.test.js.map +0 -1
- package/dist/template-adoption.test.d.ts +0 -2
- package/dist/template-adoption.test.d.ts.map +0 -1
- package/dist/template-adoption.test.js +0 -105
- package/dist/template-adoption.test.js.map +0 -1
package/dist/apps-api.js
CHANGED
|
@@ -255,6 +255,157 @@ export async function handleAppsApi(ctx) {
|
|
|
255
255
|
storage.close();
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/investigations$/);
|
|
259
|
+
if (m) {
|
|
260
|
+
const appId = decodeURIComponent(m[1]);
|
|
261
|
+
if (!loadAppById(projectRoot, appId)) {
|
|
262
|
+
sendJson(res, 404, { error: `App "${appId}" not found` });
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
266
|
+
try {
|
|
267
|
+
if (req.method === 'GET') {
|
|
268
|
+
const dashboardId = ctx.url.searchParams.get('dashboardId') ?? undefined;
|
|
269
|
+
sendJson(res, 200, { investigations: storage.listAppInvestigations(appId, dashboardId) });
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (req.method === 'POST') {
|
|
273
|
+
const body = await readJson(req);
|
|
274
|
+
const question = cleanString(body.question);
|
|
275
|
+
if (!question) {
|
|
276
|
+
sendJson(res, 400, { error: 'question is required' });
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
let investigation = storage.createAppInvestigation({
|
|
280
|
+
appId,
|
|
281
|
+
dashboardId: body.dashboardId,
|
|
282
|
+
sourceTileId: body.sourceTileId ?? selectedContextString(body.context, 'tileId'),
|
|
283
|
+
sourceBlockId: body.sourceBlockId ?? selectedContextString(body.context, 'blockId'),
|
|
284
|
+
title: body.title,
|
|
285
|
+
question,
|
|
286
|
+
intent: normalizeInvestigationIntent(body.intent, question, body.context),
|
|
287
|
+
context: body.context,
|
|
288
|
+
generatedSql: body.generatedSql,
|
|
289
|
+
});
|
|
290
|
+
if (body.run !== false) {
|
|
291
|
+
investigation = await runAppInvestigation(ctx, storage, investigation, body);
|
|
292
|
+
}
|
|
293
|
+
sendJson(res, 201, { ok: true, investigation });
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
storage.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/investigations\/([^/]+)$/);
|
|
306
|
+
if (m) {
|
|
307
|
+
const appId = decodeURIComponent(m[1]);
|
|
308
|
+
const investigationId = decodeURIComponent(m[2]);
|
|
309
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
310
|
+
try {
|
|
311
|
+
const investigation = storage.getAppInvestigation(investigationId);
|
|
312
|
+
if (!investigation || investigation.appId !== appId) {
|
|
313
|
+
sendJson(res, 404, { error: `Investigation "${investigationId}" not found` });
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
if (req.method === 'GET') {
|
|
317
|
+
sendJson(res, 200, { investigation });
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
storage.close();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/investigations\/([^/]+)\/run$/);
|
|
330
|
+
if (m && req.method === 'POST') {
|
|
331
|
+
const appId = decodeURIComponent(m[1]);
|
|
332
|
+
const investigationId = decodeURIComponent(m[2]);
|
|
333
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
334
|
+
try {
|
|
335
|
+
const investigation = storage.getAppInvestigation(investigationId);
|
|
336
|
+
if (!investigation || investigation.appId !== appId) {
|
|
337
|
+
sendJson(res, 404, { error: `Investigation "${investigationId}" not found` });
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
const body = await readJson(req);
|
|
341
|
+
const updated = await runAppInvestigation(ctx, storage, investigation, body);
|
|
342
|
+
sendJson(res, 200, { ok: true, investigation: updated });
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
storage.close();
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/investigations\/([^/]+)\/pin$/);
|
|
353
|
+
if (m && req.method === 'POST') {
|
|
354
|
+
const appId = decodeURIComponent(m[1]);
|
|
355
|
+
const investigationId = decodeURIComponent(m[2]);
|
|
356
|
+
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
357
|
+
try {
|
|
358
|
+
const investigation = storage.getAppInvestigation(investigationId);
|
|
359
|
+
if (!investigation || investigation.appId !== appId) {
|
|
360
|
+
sendJson(res, 404, { error: `Investigation "${investigationId}" not found` });
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
const body = await readJson(req);
|
|
364
|
+
const appInfo = loadAppById(projectRoot, appId);
|
|
365
|
+
const dashboardId = cleanString(body.dashboardId) || investigation.dashboardId || appInfo?.dashboards[0]?.id;
|
|
366
|
+
if (!dashboardId) {
|
|
367
|
+
sendJson(res, 400, { error: 'No dashboard is available for this investigation.' });
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
const created = createAiPinTile(projectRoot, appId, {
|
|
371
|
+
dashboardId,
|
|
372
|
+
title: cleanString(body.title) || investigation.title,
|
|
373
|
+
answer: investigation.summary ?? investigation.recommendation ?? investigation.title,
|
|
374
|
+
question: investigation.question,
|
|
375
|
+
sql: investigation.generatedSql,
|
|
376
|
+
sourceTier: 'metadata_research',
|
|
377
|
+
certification: 'ai_generated',
|
|
378
|
+
reviewStatus: 'needs_review',
|
|
379
|
+
refreshCadence: body.refreshCadence === 'daily' ? 'daily' : 'none',
|
|
380
|
+
chartConfig: { chart: 'table' },
|
|
381
|
+
result: investigationPreviewResult(investigation),
|
|
382
|
+
citations: investigationCitations(investigation),
|
|
383
|
+
analysisPlan: {
|
|
384
|
+
intent: investigation.intent,
|
|
385
|
+
reviewRequired: true,
|
|
386
|
+
uncertified: true,
|
|
387
|
+
sourceTileId: investigation.sourceTileId,
|
|
388
|
+
sourceBlockId: investigation.sourceBlockId,
|
|
389
|
+
},
|
|
390
|
+
evidence: investigation.evidence,
|
|
391
|
+
followUps: nextResearchFollowUps(investigation),
|
|
392
|
+
});
|
|
393
|
+
if (!created.ok) {
|
|
394
|
+
sendJson(res, 400, { error: created.error });
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
const pinId = typeof created.pin === 'object' && created.pin && 'id' in created.pin ? String(created.pin.id) : '';
|
|
398
|
+
const updated = pinId ? storage.markAppInvestigationPinned(investigationId, pinId) : investigation;
|
|
399
|
+
sendJson(res, 200, { ...created, investigation: updated });
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
storage.close();
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
258
409
|
m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins$/);
|
|
259
410
|
if (m) {
|
|
260
411
|
const appId = decodeURIComponent(m[1]);
|
|
@@ -478,6 +629,7 @@ function collectAppsList(projectRoot) {
|
|
|
478
629
|
notebooks: listAppNotebookRefs(projectRoot, document, appDir),
|
|
479
630
|
drafts: listAppDrafts(projectRoot, appDir),
|
|
480
631
|
aiPins: countAiPins(projectRoot, document.id),
|
|
632
|
+
investigations: countAppInvestigations(projectRoot, document.id),
|
|
481
633
|
homepage: document.homepage,
|
|
482
634
|
});
|
|
483
635
|
}
|
|
@@ -890,6 +1042,515 @@ function normalizeConversationMessages(messages) {
|
|
|
890
1042
|
}))
|
|
891
1043
|
.filter((message) => message.content.length > 0);
|
|
892
1044
|
}
|
|
1045
|
+
async function runAppInvestigation(ctx, storage, investigation, input = {}) {
|
|
1046
|
+
const question = cleanString(input.question) || investigation.question;
|
|
1047
|
+
const context = input.context === undefined ? investigation.context : input.context;
|
|
1048
|
+
const intent = normalizeInvestigationIntent(input.intent, question, context);
|
|
1049
|
+
let generatedSql = cleanString(input.generatedSql) || investigation.generatedSql;
|
|
1050
|
+
const lastRunAt = new Date().toISOString();
|
|
1051
|
+
storage.updateAppInvestigation(investigation.id, {
|
|
1052
|
+
question,
|
|
1053
|
+
intent,
|
|
1054
|
+
context,
|
|
1055
|
+
generatedSql,
|
|
1056
|
+
status: 'running',
|
|
1057
|
+
reviewStatus: 'needs_review',
|
|
1058
|
+
error: '',
|
|
1059
|
+
});
|
|
1060
|
+
try {
|
|
1061
|
+
const selected = selectedBlockContext(context);
|
|
1062
|
+
const appInfo = ctx.path.includes('/api/apps/') ? loadAppById(ctx.projectRoot, investigation.appId) : null;
|
|
1063
|
+
const previews = buildContextPreviews(selected);
|
|
1064
|
+
let metricSnapshot = buildMetricSnapshot(selected);
|
|
1065
|
+
let driverCards = buildDriverCards(selected, intent);
|
|
1066
|
+
const agentGeneration = generatedSql
|
|
1067
|
+
? undefined
|
|
1068
|
+
: await generateInvestigationSql(ctx, {
|
|
1069
|
+
appId: investigation.appId,
|
|
1070
|
+
dashboardId: investigation.dashboardId ?? selectedString(context, 'dashboardId'),
|
|
1071
|
+
sourceTileId: investigation.sourceTileId ?? selectedContextString(context, 'tileId'),
|
|
1072
|
+
sourceBlockId: investigation.sourceBlockId ?? selectedContextString(context, 'blockId'),
|
|
1073
|
+
title: investigation.title,
|
|
1074
|
+
question,
|
|
1075
|
+
intent,
|
|
1076
|
+
context,
|
|
1077
|
+
});
|
|
1078
|
+
generatedSql = generatedSql || cleanString(agentGeneration?.sql);
|
|
1079
|
+
const generationError = cleanString(agentGeneration?.executionError);
|
|
1080
|
+
const sqlEvidence = agentGeneration?.result
|
|
1081
|
+
? { preview: buildGeneratedSqlPreview(agentGeneration.result, generatedSql), error: generationError || undefined }
|
|
1082
|
+
: await runGeneratedSqlPreview(ctx, generatedSql);
|
|
1083
|
+
const sqlError = sqlEvidence.error ?? generationError;
|
|
1084
|
+
if (sqlEvidence.preview) {
|
|
1085
|
+
previews.unshift(sqlEvidence.preview);
|
|
1086
|
+
metricSnapshot = buildPreviewMetricSnapshot(sqlEvidence.preview, selectedString(selected, 'title'));
|
|
1087
|
+
driverCards = buildPreviewDriverCards(sqlEvidence.preview, intent);
|
|
1088
|
+
}
|
|
1089
|
+
const evidence = {
|
|
1090
|
+
trustStatus: buildInvestigationTrust(investigation, selected, sqlError),
|
|
1091
|
+
planner: {
|
|
1092
|
+
intent,
|
|
1093
|
+
steps: investigationSteps(intent),
|
|
1094
|
+
reviewRequired: true,
|
|
1095
|
+
generatedSql: generatedSql || undefined,
|
|
1096
|
+
sqlExecuted: Boolean(sqlEvidence.preview),
|
|
1097
|
+
sqlError,
|
|
1098
|
+
providerUsed: agentGeneration?.providerUsed,
|
|
1099
|
+
},
|
|
1100
|
+
certifiedContext: {
|
|
1101
|
+
appId: investigation.appId,
|
|
1102
|
+
appName: appInfo?.app.name,
|
|
1103
|
+
dashboardId: investigation.dashboardId,
|
|
1104
|
+
dashboardTitle: selectedString(context, 'dashboardTitle'),
|
|
1105
|
+
sourceTileId: investigation.sourceTileId ?? selectedContextString(context, 'tileId'),
|
|
1106
|
+
sourceBlockId: investigation.sourceBlockId ?? selectedContextString(context, 'blockId'),
|
|
1107
|
+
certificationStatus: selectedString(selected, 'certificationStatus'),
|
|
1108
|
+
},
|
|
1109
|
+
assumptions: investigationAssumptions(intent, selected, generatedSql, sqlError),
|
|
1110
|
+
context,
|
|
1111
|
+
agentEvidence: agentGeneration?.evidence,
|
|
1112
|
+
analysisPlan: agentGeneration?.analysisPlan,
|
|
1113
|
+
citations: agentGeneration?.citations,
|
|
1114
|
+
};
|
|
1115
|
+
const summary = cleanString(agentGeneration?.answer) || buildInvestigationSummary(intent, question, selected, metricSnapshot, driverCards);
|
|
1116
|
+
const recommendation = buildInvestigationRecommendation(intent, selected, sqlError);
|
|
1117
|
+
return storage.updateAppInvestigation(investigation.id, {
|
|
1118
|
+
title: cleanString(input.question) ? titleFromInvestigation(question, selected) : investigation.title,
|
|
1119
|
+
question,
|
|
1120
|
+
intent,
|
|
1121
|
+
context,
|
|
1122
|
+
status: sqlEvidence.fatal ? 'error' : 'ready',
|
|
1123
|
+
summary,
|
|
1124
|
+
recommendation,
|
|
1125
|
+
metrics: metricSnapshot,
|
|
1126
|
+
driverCards,
|
|
1127
|
+
resultPreviews: previews,
|
|
1128
|
+
evidence,
|
|
1129
|
+
generatedSql,
|
|
1130
|
+
reviewStatus: 'needs_review',
|
|
1131
|
+
error: sqlError ?? '',
|
|
1132
|
+
lastRunAt,
|
|
1133
|
+
}) ?? investigation;
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
return storage.updateAppInvestigation(investigation.id, {
|
|
1137
|
+
status: 'error',
|
|
1138
|
+
reviewStatus: 'needs_review',
|
|
1139
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1140
|
+
lastRunAt,
|
|
1141
|
+
}) ?? investigation;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function normalizeInvestigationIntent(value, question, context) {
|
|
1145
|
+
if (value === 'diagnose_change'
|
|
1146
|
+
|| value === 'driver_breakdown'
|
|
1147
|
+
|| value === 'segment_compare'
|
|
1148
|
+
|| value === 'entity_drilldown'
|
|
1149
|
+
|| value === 'anomaly_investigation'
|
|
1150
|
+
|| value === 'trust_gap_review')
|
|
1151
|
+
return value;
|
|
1152
|
+
const text = `${question} ${JSON.stringify(safeIntentContext(context)).slice(0, 500)}`.toLowerCase();
|
|
1153
|
+
if (/\b(trust|rely|certif|lineage|owner|caveat|gap)\b/.test(text))
|
|
1154
|
+
return 'trust_gap_review';
|
|
1155
|
+
if (/\b(anomal|exception|outlier|spike|dip)\b/.test(text))
|
|
1156
|
+
return 'anomaly_investigation';
|
|
1157
|
+
if (/\b(compare|versus| vs |segment|cohort)\b/.test(text))
|
|
1158
|
+
return 'segment_compare';
|
|
1159
|
+
if (/\b(why|changed|change|drop|decline|increase|decrease|february|month|week|quarter)\b/.test(text))
|
|
1160
|
+
return 'diagnose_change';
|
|
1161
|
+
if (/\b(driver|drove|break down|breakdown|contribute|top mover|movers)\b/.test(text))
|
|
1162
|
+
return 'driver_breakdown';
|
|
1163
|
+
if (/\b(customer|account|user|client|merchant|product|sku|alice|johnson)\b/.test(text))
|
|
1164
|
+
return 'entity_drilldown';
|
|
1165
|
+
return 'driver_breakdown';
|
|
1166
|
+
}
|
|
1167
|
+
function safeIntentContext(context) {
|
|
1168
|
+
const root = asRecord(context);
|
|
1169
|
+
return {
|
|
1170
|
+
selectedBlock: root ? root.selectedBlock : undefined,
|
|
1171
|
+
dashboardTitle: root ? root.dashboardTitle : undefined,
|
|
1172
|
+
availableBlocks: root ? root.availableBlocks : undefined,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function selectedBlockContext(context) {
|
|
1176
|
+
const root = asRecord(context);
|
|
1177
|
+
return asRecord(root?.selectedBlock);
|
|
1178
|
+
}
|
|
1179
|
+
function selectedContextString(context, key) {
|
|
1180
|
+
return selectedString(selectedBlockContext(context), key);
|
|
1181
|
+
}
|
|
1182
|
+
function selectedString(context, key) {
|
|
1183
|
+
const record = asRecord(context);
|
|
1184
|
+
const value = record?.[key];
|
|
1185
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
1186
|
+
}
|
|
1187
|
+
function buildContextPreviews(selected) {
|
|
1188
|
+
const rows = selectedRows(selected);
|
|
1189
|
+
if (rows.length === 0)
|
|
1190
|
+
return [];
|
|
1191
|
+
const columns = selectedColumns(selected, rows);
|
|
1192
|
+
return [{
|
|
1193
|
+
id: 'selected-tile-sample',
|
|
1194
|
+
title: 'Selected tile evidence',
|
|
1195
|
+
kind: 'table',
|
|
1196
|
+
reviewRequired: true,
|
|
1197
|
+
result: {
|
|
1198
|
+
columns,
|
|
1199
|
+
rows,
|
|
1200
|
+
rowCount: typeof selected?.rowCount === 'number' ? selected.rowCount : rows.length,
|
|
1201
|
+
},
|
|
1202
|
+
}];
|
|
1203
|
+
}
|
|
1204
|
+
function buildMetricSnapshot(selected) {
|
|
1205
|
+
const rows = selectedRows(selected);
|
|
1206
|
+
const columns = selectedColumns(selected, rows);
|
|
1207
|
+
const numericColumns = columns.filter((column) => rows.some((row) => typeofNumber(row[column]) !== null));
|
|
1208
|
+
const metricColumn = numericColumns[0];
|
|
1209
|
+
if (!metricColumn || rows.length === 0) {
|
|
1210
|
+
return {
|
|
1211
|
+
currentValue: undefined,
|
|
1212
|
+
baselineValue: undefined,
|
|
1213
|
+
delta: undefined,
|
|
1214
|
+
context: 'Metric values were not available in the selected tile sample.',
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
const baselineValue = typeofNumber(rows[0]?.[metricColumn]);
|
|
1218
|
+
const currentValue = typeofNumber(rows[rows.length - 1]?.[metricColumn]) ?? baselineValue;
|
|
1219
|
+
const delta = currentValue !== null && baselineValue !== null ? currentValue - baselineValue : undefined;
|
|
1220
|
+
return {
|
|
1221
|
+
metric: metricColumn,
|
|
1222
|
+
currentValue,
|
|
1223
|
+
baselineValue,
|
|
1224
|
+
delta,
|
|
1225
|
+
rowsReviewed: rows.length,
|
|
1226
|
+
context: selectedString(selected, 'title') ?? 'Selected dashboard tile',
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
function buildDriverCards(selected, intent) {
|
|
1230
|
+
const rows = selectedRows(selected);
|
|
1231
|
+
const columns = selectedColumns(selected, rows);
|
|
1232
|
+
if (rows.length === 0) {
|
|
1233
|
+
return [{
|
|
1234
|
+
title: 'Runtime context needed',
|
|
1235
|
+
contribution: 'Needs SQL preview',
|
|
1236
|
+
explanation: 'The selected tile did not include sample rows, so DQL captured the question and review path for a deeper SQL run.',
|
|
1237
|
+
intent,
|
|
1238
|
+
}];
|
|
1239
|
+
}
|
|
1240
|
+
const numericColumn = columns.find((column) => rows.some((row) => typeofNumber(row[column]) !== null));
|
|
1241
|
+
const dimensionColumn = columns.find((column) => column !== numericColumn && rows.some((row) => typeof row[column] === 'string'));
|
|
1242
|
+
if (!numericColumn) {
|
|
1243
|
+
return rows.slice(0, 5).map((row, index) => ({
|
|
1244
|
+
title: String(row[dimensionColumn ?? columns[0]] ?? `Row ${index + 1}`),
|
|
1245
|
+
contribution: 'Context row',
|
|
1246
|
+
explanation: 'This row is part of the tile evidence for the investigation.',
|
|
1247
|
+
}));
|
|
1248
|
+
}
|
|
1249
|
+
return rows
|
|
1250
|
+
.map((row, index) => {
|
|
1251
|
+
const value = typeofNumber(row[numericColumn]) ?? 0;
|
|
1252
|
+
const label = String(row[dimensionColumn ?? columns[0]] ?? `Row ${index + 1}`);
|
|
1253
|
+
return {
|
|
1254
|
+
title: label,
|
|
1255
|
+
value,
|
|
1256
|
+
contribution: formatContribution(value),
|
|
1257
|
+
explanation: `${label} is one of the highest-signal rows in the selected tile sample for ${numericColumn}.`,
|
|
1258
|
+
evidenceLabel: numericColumn,
|
|
1259
|
+
};
|
|
1260
|
+
})
|
|
1261
|
+
.sort((a, b) => Math.abs(Number(b.value ?? 0)) - Math.abs(Number(a.value ?? 0)))
|
|
1262
|
+
.slice(0, 5);
|
|
1263
|
+
}
|
|
1264
|
+
function buildPreviewMetricSnapshot(preview, fallbackTitle) {
|
|
1265
|
+
const rows = previewResultRows(preview);
|
|
1266
|
+
const columns = previewResultColumns(preview, rows);
|
|
1267
|
+
const numericColumns = columns.filter((column) => rows.some((row) => typeofNumber(row[column]) !== null));
|
|
1268
|
+
if (rows.length === 0 || numericColumns.length === 0) {
|
|
1269
|
+
return {
|
|
1270
|
+
currentValue: undefined,
|
|
1271
|
+
baselineValue: undefined,
|
|
1272
|
+
delta: undefined,
|
|
1273
|
+
context: 'Generated SQL preview did not return numeric metric rows.',
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
const currentColumn = pickColumn(numericColumns, [/^current_/i, /current.*(revenue|value|amount|total|orders?)/i])
|
|
1277
|
+
?? pickColumn(numericColumns, [/(revenue|value|amount|total|orders?|count)$/i])
|
|
1278
|
+
?? numericColumns[0];
|
|
1279
|
+
const baselineColumn = pickColumn(numericColumns, [/^baseline_/i, /baseline.*(revenue|value|amount|total|orders?)/i]);
|
|
1280
|
+
const deltaColumn = pickColumn(numericColumns, [/(delta|change|variance|diff|contribution)/i]);
|
|
1281
|
+
const currentValue = sumNumericRows(rows, currentColumn);
|
|
1282
|
+
const baselineValue = baselineColumn ? sumNumericRows(rows, baselineColumn) : typeofNumber(rows[0]?.[currentColumn]);
|
|
1283
|
+
const delta = deltaColumn
|
|
1284
|
+
? sumNumericRows(rows, deltaColumn)
|
|
1285
|
+
: currentValue !== null && baselineValue !== null
|
|
1286
|
+
? currentValue - baselineValue
|
|
1287
|
+
: undefined;
|
|
1288
|
+
return {
|
|
1289
|
+
metric: deltaColumn ?? currentColumn,
|
|
1290
|
+
currentValue,
|
|
1291
|
+
baselineValue,
|
|
1292
|
+
delta,
|
|
1293
|
+
rowsReviewed: rows.length,
|
|
1294
|
+
context: fallbackTitle ?? selectedString(preview, 'title') ?? 'Generated SQL preview',
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function buildPreviewDriverCards(preview, intent) {
|
|
1298
|
+
const rows = previewResultRows(preview);
|
|
1299
|
+
const columns = previewResultColumns(preview, rows);
|
|
1300
|
+
if (rows.length === 0) {
|
|
1301
|
+
return buildDriverCards(null, intent);
|
|
1302
|
+
}
|
|
1303
|
+
const numericColumns = columns.filter((column) => rows.some((row) => typeofNumber(row[column]) !== null));
|
|
1304
|
+
const contributionColumn = pickColumn(numericColumns, [/(delta|change|variance|diff|contribution)/i])
|
|
1305
|
+
?? pickColumn(numericColumns, [/^current_/i, /(revenue|value|amount|total|orders?|count)$/i])
|
|
1306
|
+
?? numericColumns[0];
|
|
1307
|
+
const dimensionColumn = columns.find((column) => column !== contributionColumn && rows.some((row) => typeof row[column] === 'string'));
|
|
1308
|
+
if (!contributionColumn) {
|
|
1309
|
+
return rows.slice(0, 5).map((row, index) => ({
|
|
1310
|
+
title: String(row[dimensionColumn ?? columns[0]] ?? `Row ${index + 1}`),
|
|
1311
|
+
contribution: 'Preview row',
|
|
1312
|
+
explanation: 'This row came from the generated SQL preview and needs analyst review.',
|
|
1313
|
+
intent,
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
return rows
|
|
1317
|
+
.map((row, index) => {
|
|
1318
|
+
const value = typeofNumber(row[contributionColumn]) ?? 0;
|
|
1319
|
+
const label = String(row[dimensionColumn ?? columns[0]] ?? `Row ${index + 1}`);
|
|
1320
|
+
return {
|
|
1321
|
+
title: label,
|
|
1322
|
+
value,
|
|
1323
|
+
contribution: formatContribution(value),
|
|
1324
|
+
explanation: `${label} is ranked by ${contributionColumn} from the generated SQL preview.`,
|
|
1325
|
+
evidenceLabel: contributionColumn,
|
|
1326
|
+
reviewRequired: true,
|
|
1327
|
+
intent,
|
|
1328
|
+
};
|
|
1329
|
+
})
|
|
1330
|
+
.sort((a, b) => Math.abs(Number(b.value ?? 0)) - Math.abs(Number(a.value ?? 0)))
|
|
1331
|
+
.slice(0, 5);
|
|
1332
|
+
}
|
|
1333
|
+
function previewResultRows(preview) {
|
|
1334
|
+
const result = asRecord(preview.result);
|
|
1335
|
+
const rows = Array.isArray(result?.rows) ? result.rows : [];
|
|
1336
|
+
return rows.map(asRecord).filter((row) => Boolean(row)).slice(0, 100);
|
|
1337
|
+
}
|
|
1338
|
+
function previewResultColumns(preview, rows) {
|
|
1339
|
+
const result = asRecord(preview.result);
|
|
1340
|
+
const columns = Array.isArray(result?.columns) ? result.columns.map(String).filter(Boolean) : [];
|
|
1341
|
+
return columns.length > 0 ? columns.slice(0, 20) : Object.keys(rows[0] ?? {}).slice(0, 20);
|
|
1342
|
+
}
|
|
1343
|
+
function pickColumn(columns, patterns) {
|
|
1344
|
+
return columns.find((column) => patterns.some((pattern) => pattern.test(column)));
|
|
1345
|
+
}
|
|
1346
|
+
function sumNumericRows(rows, column) {
|
|
1347
|
+
let total = 0;
|
|
1348
|
+
let found = false;
|
|
1349
|
+
for (const row of rows) {
|
|
1350
|
+
const value = typeofNumber(row[column]);
|
|
1351
|
+
if (value === null)
|
|
1352
|
+
continue;
|
|
1353
|
+
total += value;
|
|
1354
|
+
found = true;
|
|
1355
|
+
}
|
|
1356
|
+
return found ? total : null;
|
|
1357
|
+
}
|
|
1358
|
+
async function generateInvestigationSql(ctx, input) {
|
|
1359
|
+
if (!ctx.generateInvestigationSql)
|
|
1360
|
+
return undefined;
|
|
1361
|
+
try {
|
|
1362
|
+
return await ctx.generateInvestigationSql(input);
|
|
1363
|
+
}
|
|
1364
|
+
catch (err) {
|
|
1365
|
+
return {
|
|
1366
|
+
executionError: err instanceof Error ? err.message : String(err),
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
async function runGeneratedSqlPreview(ctx, generatedSql) {
|
|
1371
|
+
const sql = cleanString(generatedSql);
|
|
1372
|
+
if (!sql)
|
|
1373
|
+
return {};
|
|
1374
|
+
if (!isReadOnlySql(sql)) {
|
|
1375
|
+
return {
|
|
1376
|
+
error: 'Generated SQL was not run because it was not a read-only SELECT or WITH query.',
|
|
1377
|
+
fatal: true,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
if (!ctx.executeSql) {
|
|
1381
|
+
return {
|
|
1382
|
+
error: 'This host cannot execute investigation SQL.',
|
|
1383
|
+
fatal: false,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
const result = await ctx.executeSql(boundedPreviewSql(sql));
|
|
1388
|
+
return {
|
|
1389
|
+
preview: {
|
|
1390
|
+
id: 'generated-sql-preview',
|
|
1391
|
+
title: 'Generated SQL preview',
|
|
1392
|
+
kind: 'table',
|
|
1393
|
+
reviewRequired: true,
|
|
1394
|
+
result,
|
|
1395
|
+
},
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
catch (err) {
|
|
1399
|
+
return {
|
|
1400
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1401
|
+
fatal: true,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function buildGeneratedSqlPreview(result, generatedSql) {
|
|
1406
|
+
const record = asRecord(result);
|
|
1407
|
+
const rawRows = Array.isArray(record?.rows) ? record.rows : [];
|
|
1408
|
+
const rows = rawRows.map(asRecord).filter((row) => Boolean(row)).slice(0, 100);
|
|
1409
|
+
const rawColumns = Array.isArray(record?.columns) ? record.columns : [];
|
|
1410
|
+
const columns = rawColumns.length
|
|
1411
|
+
? rawColumns.map((column) => {
|
|
1412
|
+
const columnRecord = asRecord(column);
|
|
1413
|
+
return typeof column === 'string'
|
|
1414
|
+
? column
|
|
1415
|
+
: typeof columnRecord?.name === 'string'
|
|
1416
|
+
? columnRecord.name
|
|
1417
|
+
: String(column);
|
|
1418
|
+
})
|
|
1419
|
+
: Object.keys(rows[0] ?? {});
|
|
1420
|
+
return {
|
|
1421
|
+
id: 'generated-sql-preview',
|
|
1422
|
+
title: 'Generated SQL preview',
|
|
1423
|
+
kind: 'table',
|
|
1424
|
+
reviewRequired: true,
|
|
1425
|
+
sql: generatedSql,
|
|
1426
|
+
result: {
|
|
1427
|
+
columns,
|
|
1428
|
+
rows,
|
|
1429
|
+
rowCount: typeof record?.rowCount === 'number' ? record.rowCount : rows.length,
|
|
1430
|
+
executionTime: typeof record?.executionTime === 'number' ? record.executionTime : undefined,
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
function isReadOnlySql(sql) {
|
|
1435
|
+
const trimmed = sql.trim().replace(/;+\s*$/g, '');
|
|
1436
|
+
if (!/^(select|with)\b/i.test(trimmed))
|
|
1437
|
+
return false;
|
|
1438
|
+
if (/;\s*\S/.test(trimmed))
|
|
1439
|
+
return false;
|
|
1440
|
+
return !/\b(insert|update|delete|merge|drop|alter|create|truncate|copy|grant|revoke|call|execute|attach|detach)\b/i.test(trimmed);
|
|
1441
|
+
}
|
|
1442
|
+
function boundedPreviewSql(sql) {
|
|
1443
|
+
return `SELECT * FROM (${sql.trim().replace(/;+\s*$/g, '')}) AS dql_research_preview LIMIT 100`;
|
|
1444
|
+
}
|
|
1445
|
+
function buildInvestigationTrust(investigation, selected, sqlError) {
|
|
1446
|
+
return {
|
|
1447
|
+
label: 'AI-generated research',
|
|
1448
|
+
uncertified: true,
|
|
1449
|
+
reviewStatus: 'needs_review',
|
|
1450
|
+
certifiedContext: selectedString(selected, 'certificationStatus') === 'certified' ? 'selected tile is certified' : 'selected tile certification needs review',
|
|
1451
|
+
sourceBlockId: investigation.sourceBlockId ?? selectedString(selected, 'blockId'),
|
|
1452
|
+
sourceTileId: investigation.sourceTileId ?? selectedString(selected, 'tileId'),
|
|
1453
|
+
caveats: [
|
|
1454
|
+
'Investigation output is not certified until a reviewer promotes or certifies the generated block.',
|
|
1455
|
+
...(sqlError ? [`SQL preview caveat: ${sqlError}`] : []),
|
|
1456
|
+
],
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function investigationSteps(intent) {
|
|
1460
|
+
const common = ['trust check', 'evidence capture'];
|
|
1461
|
+
if (intent === 'trust_gap_review')
|
|
1462
|
+
return ['certification review', 'lineage review', 'owner and caveat check', ...common];
|
|
1463
|
+
if (intent === 'entity_drilldown')
|
|
1464
|
+
return ['entity value match', 'metric trend', 'exception rows', ...common];
|
|
1465
|
+
if (intent === 'segment_compare')
|
|
1466
|
+
return ['segment grouping', 'baseline comparison', 'top movers', ...common];
|
|
1467
|
+
if (intent === 'anomaly_investigation')
|
|
1468
|
+
return ['baseline comparison', 'trend check', 'exception rows', 'top movers', ...common];
|
|
1469
|
+
if (intent === 'diagnose_change')
|
|
1470
|
+
return ['baseline comparison', 'trend check', 'top movers', 'segment contribution', ...common];
|
|
1471
|
+
return ['top movers', 'segment contribution', 'exception rows', ...common];
|
|
1472
|
+
}
|
|
1473
|
+
function investigationAssumptions(intent, selected, generatedSql, sqlError) {
|
|
1474
|
+
return [
|
|
1475
|
+
`Intent classified as ${intent}.`,
|
|
1476
|
+
selected ? 'The selected dashboard tile is the starting context.' : 'No selected tile context was provided.',
|
|
1477
|
+
generatedSql ? 'A generated SQL preview was requested and bounded to 100 rows.' : 'No generated SQL was supplied, so the first pass used dashboard result samples and metadata.',
|
|
1478
|
+
sqlError ? `SQL preview needs review: ${sqlError}` : 'The result remains uncertified until reviewed.',
|
|
1479
|
+
];
|
|
1480
|
+
}
|
|
1481
|
+
function buildInvestigationSummary(intent, question, selected, metrics, drivers) {
|
|
1482
|
+
const target = selectedString(selected, 'title') ?? 'this dashboard question';
|
|
1483
|
+
const delta = typeof metrics.delta === 'number' ? ` Delta from the sampled baseline is ${formatContribution(metrics.delta)}.` : '';
|
|
1484
|
+
if (intent === 'trust_gap_review') {
|
|
1485
|
+
return `This tile can be used as certified context only where its source block and lineage are certified. The deeper answer is AI-generated research and needs review before leaders rely on it.${delta}`;
|
|
1486
|
+
}
|
|
1487
|
+
const driver = drivers[0]?.title ? ` Top visible driver in the current evidence is ${drivers[0].title}.` : '';
|
|
1488
|
+
return `DQL opened a review-required investigation for ${target}: ${question}.${delta}${driver}`;
|
|
1489
|
+
}
|
|
1490
|
+
function buildInvestigationRecommendation(intent, selected, sqlError) {
|
|
1491
|
+
if (sqlError)
|
|
1492
|
+
return 'Review the generated SQL or add a certified drilldown block before promoting this result.';
|
|
1493
|
+
if (intent === 'trust_gap_review')
|
|
1494
|
+
return 'Use the certified tile for reporting, and promote only the reviewed gaps into a draft block.';
|
|
1495
|
+
if (!selected)
|
|
1496
|
+
return 'Select a dashboard tile or provide SQL so DQL can rank drivers with stronger evidence.';
|
|
1497
|
+
return 'Review the driver evidence, then pin the useful answer or promote the SQL path into a draft DQL block.';
|
|
1498
|
+
}
|
|
1499
|
+
function investigationPreviewResult(investigation) {
|
|
1500
|
+
const previews = Array.isArray(investigation.resultPreviews) ? investigation.resultPreviews : [];
|
|
1501
|
+
const first = previews.find((preview) => asRecord(preview)?.result);
|
|
1502
|
+
return asRecord(first)?.result;
|
|
1503
|
+
}
|
|
1504
|
+
function investigationCitations(investigation) {
|
|
1505
|
+
return [{
|
|
1506
|
+
kind: 'app_investigation',
|
|
1507
|
+
name: investigation.title,
|
|
1508
|
+
reviewStatus: investigation.reviewStatus,
|
|
1509
|
+
uncertified: true,
|
|
1510
|
+
sourceBlockId: investigation.sourceBlockId,
|
|
1511
|
+
sourceTileId: investigation.sourceTileId,
|
|
1512
|
+
}];
|
|
1513
|
+
}
|
|
1514
|
+
function nextResearchFollowUps(investigation) {
|
|
1515
|
+
const target = investigation.sourceBlockId ?? investigation.sourceTileId ?? 'this result';
|
|
1516
|
+
return [
|
|
1517
|
+
`Break ${target} down by the strongest segment`,
|
|
1518
|
+
`Show exception rows for ${target}`,
|
|
1519
|
+
`What would need review before certifying this answer?`,
|
|
1520
|
+
];
|
|
1521
|
+
}
|
|
1522
|
+
function titleFromInvestigation(question, selected) {
|
|
1523
|
+
const selectedTitle = selectedString(selected, 'title');
|
|
1524
|
+
const base = selectedTitle ? `${selectedTitle}: ${question}` : question;
|
|
1525
|
+
return base.replace(/\s+/g, ' ').slice(0, 90);
|
|
1526
|
+
}
|
|
1527
|
+
function selectedRows(selected) {
|
|
1528
|
+
const rows = Array.isArray(selected?.sampleRows) ? selected?.sampleRows : selected?.rows;
|
|
1529
|
+
if (!Array.isArray(rows))
|
|
1530
|
+
return [];
|
|
1531
|
+
return rows.map(asRecord).filter((row) => Boolean(row)).slice(0, 100);
|
|
1532
|
+
}
|
|
1533
|
+
function selectedColumns(selected, rows) {
|
|
1534
|
+
const columns = selected?.columns;
|
|
1535
|
+
if (Array.isArray(columns) && columns.length > 0) {
|
|
1536
|
+
return columns.map(String).filter(Boolean).slice(0, 20);
|
|
1537
|
+
}
|
|
1538
|
+
return Object.keys(rows[0] ?? {}).slice(0, 20);
|
|
1539
|
+
}
|
|
1540
|
+
function typeofNumber(value) {
|
|
1541
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
1542
|
+
return value;
|
|
1543
|
+
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value)))
|
|
1544
|
+
return Number(value);
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
function formatContribution(value) {
|
|
1548
|
+
const rounded = Math.abs(value) >= 100 ? Math.round(value).toLocaleString() : Number(value.toFixed(2)).toLocaleString();
|
|
1549
|
+
return value >= 0 ? `+${rounded}` : `-${rounded.replace(/^-/, '')}`;
|
|
1550
|
+
}
|
|
1551
|
+
function asRecord(value) {
|
|
1552
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
1553
|
+
}
|
|
893
1554
|
function cleanString(value) {
|
|
894
1555
|
return typeof value === 'string' ? value.trim() : '';
|
|
895
1556
|
}
|
|
@@ -1145,6 +1806,7 @@ function loadAppById(projectRoot, id) {
|
|
|
1145
1806
|
notebooks: listAppNotebookRefs(projectRoot, document, appDir),
|
|
1146
1807
|
drafts: listAppDrafts(projectRoot, appDir),
|
|
1147
1808
|
aiPins: listAiPins(projectRoot, document.id),
|
|
1809
|
+
investigations: listAppInvestigations(projectRoot, document.id),
|
|
1148
1810
|
};
|
|
1149
1811
|
}
|
|
1150
1812
|
return null;
|
|
@@ -1422,6 +2084,9 @@ function scanFiles(root, extension) {
|
|
|
1422
2084
|
function countAiPins(projectRoot, appId) {
|
|
1423
2085
|
return listAiPins(projectRoot, appId).length;
|
|
1424
2086
|
}
|
|
2087
|
+
function countAppInvestigations(projectRoot, appId) {
|
|
2088
|
+
return listAppInvestigations(projectRoot, appId).length;
|
|
2089
|
+
}
|
|
1425
2090
|
function listAiPins(projectRoot, appId) {
|
|
1426
2091
|
const dbPath = defaultLocalAppsDbPath(projectRoot);
|
|
1427
2092
|
if (!existsSync(dbPath))
|
|
@@ -1441,6 +2106,23 @@ function listAiPins(projectRoot, appId) {
|
|
|
1441
2106
|
return [];
|
|
1442
2107
|
}
|
|
1443
2108
|
}
|
|
2109
|
+
function listAppInvestigations(projectRoot, appId) {
|
|
2110
|
+
const dbPath = defaultLocalAppsDbPath(projectRoot);
|
|
2111
|
+
if (!existsSync(dbPath))
|
|
2112
|
+
return [];
|
|
2113
|
+
try {
|
|
2114
|
+
const storage = new LocalAppStorage(dbPath);
|
|
2115
|
+
try {
|
|
2116
|
+
return storage.listAppInvestigations(appId);
|
|
2117
|
+
}
|
|
2118
|
+
finally {
|
|
2119
|
+
storage.close();
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
catch {
|
|
2123
|
+
return [];
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1444
2126
|
function listDashboardsFor(projectRoot, id) {
|
|
1445
2127
|
const result = loadAppById(projectRoot, id);
|
|
1446
2128
|
return result?.dashboards ?? null;
|