@duckcodeailabs/dql-agent 1.6.19 → 1.6.21
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/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +6 -2
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +38 -2
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +465 -123
- package/dist/app-builder.js.map +1 -1
- package/package.json +4 -4
package/dist/app-builder.js
CHANGED
|
@@ -24,13 +24,13 @@ export const APP_BUILDER_SKILLS = [
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
id: "draft_missing_sections",
|
|
27
|
-
title: "
|
|
28
|
-
description: "
|
|
27
|
+
title: "Scope report gaps",
|
|
28
|
+
description: "Turn missing explanations, drilldowns, and metrics into scoped analyst reports instead of dashboard placeholders.",
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
id: "route_review",
|
|
32
32
|
title: "Route review",
|
|
33
|
-
description: "Keep generated
|
|
33
|
+
description: "Keep generated analysis review-required and attach concrete next steps before stakeholder use.",
|
|
34
34
|
},
|
|
35
35
|
];
|
|
36
36
|
export function planAppFromPrompt(input) {
|
|
@@ -38,64 +38,36 @@ export function planAppFromPrompt(input) {
|
|
|
38
38
|
if (!prompt)
|
|
39
39
|
throw new Error("prompt is required");
|
|
40
40
|
const domain = normalizeToken(input.domain) ?? inferDomain(prompt) ?? "general";
|
|
41
|
-
const audience = inferAudience(prompt)
|
|
41
|
+
const audience = input.audience?.trim() || inferAudience(prompt) || inferDefaultAudience(prompt);
|
|
42
42
|
const analysisIntent = inferAnalysisIntent(prompt);
|
|
43
43
|
const appName = titleForPrompt(prompt, inferFallbackName(prompt, domain));
|
|
44
44
|
const appId = suggestAppId(appName);
|
|
45
45
|
const maxCertifiedTiles = input.maxCertifiedTiles ?? 4;
|
|
46
|
+
const targetCertifiedTiles = Math.max(maxCertifiedTiles, input.preferredBlockIds?.length ?? 0);
|
|
46
47
|
const preferredNodes = findPreferredCertifiedBlockNodes(input.kg, input.preferredBlockIds ?? []);
|
|
47
|
-
const matchedNodes = findCertifiedBlockNodes(input.kg, prompt, domain, Math.max(
|
|
48
|
-
const certifiedNodes = mergeCertifiedBlockNodes(preferredNodes, matchedNodes).
|
|
48
|
+
const matchedNodes = findCertifiedBlockNodes(input.kg, prompt, domain, Math.max(targetCertifiedTiles * 3, preferredNodes.length));
|
|
49
|
+
const certifiedNodes = dedupeCertifiedBlockNodes(mergeCertifiedBlockNodes(preferredNodes, matchedNodes), new Set(preferredNodes.map((node) => node.nodeId))).slice(0, targetCertifiedTiles);
|
|
49
50
|
const contextNodes = findCertifiedContextNodes(input.kg, prompt, domain, 6);
|
|
50
51
|
const filters = inferFilters(prompt);
|
|
51
|
-
const certifiedTiles = certifiedNodes.map((node, index) => tileFromCertifiedNode(node, index, analysisIntent));
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
kind: "narrative",
|
|
71
|
-
description: businessBriefDescription(prompt, audience, analysisIntent),
|
|
72
|
-
viz: "text",
|
|
73
|
-
certification: "uncertified",
|
|
74
|
-
reviewStatus: "review_required",
|
|
75
|
-
display: {
|
|
76
|
-
role: "business_summary",
|
|
77
|
-
recommendedDisplayType: "text",
|
|
78
|
-
layoutPriority: 0,
|
|
79
|
-
expectedGrain: "app",
|
|
80
|
-
trustState: "review_required",
|
|
81
|
-
followUpActions: ["ask_follow_up", "review_trust"],
|
|
82
|
-
rationale: "Compact business brief keeps the app goal visible without dominating the dashboard.",
|
|
83
|
-
genUi: buildGenUiContract({
|
|
84
|
-
title: "Business brief",
|
|
85
|
-
role: "business_summary",
|
|
86
|
-
viz: "text",
|
|
87
|
-
text: prompt,
|
|
88
|
-
trustState: "review_required",
|
|
89
|
-
reviewStatus: "review_required",
|
|
90
|
-
followUpActions: ["ask_follow_up", "review_trust"],
|
|
91
|
-
rationale: "Compact business brief keeps the app goal visible without dominating the dashboard.",
|
|
92
|
-
}),
|
|
93
|
-
},
|
|
94
|
-
rationale: "Narrative text is generated scaffolding and should be reviewed with the app.",
|
|
95
|
-
reviewTasks: [
|
|
96
|
-
"Confirm the audience, business goal, caveats, and review cadence.",
|
|
97
|
-
],
|
|
98
|
-
};
|
|
52
|
+
const certifiedTiles = certifiedNodes.map((node, index) => tileFromCertifiedNode(node, index, analysisIntent, filters));
|
|
53
|
+
const scopedReports = inferScopedReports(prompt, domain, analysisIntent, certifiedNodes);
|
|
54
|
+
const certifiedContextSummary = summarizeCertifiedContext([
|
|
55
|
+
...certifiedNodes,
|
|
56
|
+
...contextNodes,
|
|
57
|
+
]);
|
|
58
|
+
const missingEvidence = Array.from(new Set([
|
|
59
|
+
...missingEvidenceForPlan(prompt, analysisIntent, certifiedNodes, contextNodes),
|
|
60
|
+
...scopedReports.map((report) => `${report.title}: ${report.description}`),
|
|
61
|
+
]));
|
|
62
|
+
const stakeholderSummary = stakeholderSummaryForPlan({
|
|
63
|
+
prompt,
|
|
64
|
+
appName,
|
|
65
|
+
audience,
|
|
66
|
+
domain,
|
|
67
|
+
analysisIntent,
|
|
68
|
+
certifiedTiles: certifiedTiles.length,
|
|
69
|
+
scopedReports: scopedReports.length,
|
|
70
|
+
});
|
|
99
71
|
return {
|
|
100
72
|
version: 1,
|
|
101
73
|
appId,
|
|
@@ -107,23 +79,22 @@ export function planAppFromPrompt(input) {
|
|
|
107
79
|
analysisIntent,
|
|
108
80
|
audience,
|
|
109
81
|
domain,
|
|
110
|
-
certifiedContext:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
]),
|
|
114
|
-
missingEvidence: missingEvidenceForPlan(prompt, analysisIntent, certifiedNodes, contextNodes),
|
|
82
|
+
certifiedContext: certifiedContextSummary,
|
|
83
|
+
missingEvidence,
|
|
84
|
+
scopedReports,
|
|
115
85
|
displayStrategy: displayStrategyForIntent(analysisIntent),
|
|
116
|
-
layoutRationale: "Business-first layout: certified KPIs, trends, breakdowns, and
|
|
86
|
+
layoutRationale: "Business-first layout: certified KPIs, trends, breakdowns, and proof-backed tiles only; generated gaps stay as scoped analysis memos until promoted.",
|
|
117
87
|
handoffPlan: [
|
|
118
88
|
"Use certified block tiles as the governed dashboard surface.",
|
|
119
|
-
"Keep generated narrative, trust gaps, and drilldown ideas
|
|
120
|
-
"Use app
|
|
89
|
+
"Keep generated narrative, trust gaps, and drilldown ideas as scoped analysis memos until reviewed.",
|
|
90
|
+
"Use app Copilot analysis to run additional SQL, inspect previews, then pin or promote reviewed results into the app.",
|
|
121
91
|
],
|
|
122
92
|
},
|
|
123
93
|
skills: APP_BUILDER_SKILLS,
|
|
124
94
|
domain,
|
|
125
95
|
audience,
|
|
126
96
|
businessGoal: prompt,
|
|
97
|
+
stakeholderSummary,
|
|
127
98
|
owner: input.owner?.trim() || `${process.env.USER ?? "owner"}@local`,
|
|
128
99
|
lifecycle: "draft",
|
|
129
100
|
tags: Array.from(new Set([
|
|
@@ -131,23 +102,48 @@ export function planAppFromPrompt(input) {
|
|
|
131
102
|
...inferTags(prompt, domain),
|
|
132
103
|
`audience:${slugify(audience)}`,
|
|
133
104
|
])),
|
|
105
|
+
appSections: [
|
|
106
|
+
{
|
|
107
|
+
id: "dashboard",
|
|
108
|
+
title: "Stakeholder view",
|
|
109
|
+
purpose: "Clean dashboard pages backed by certified blocks and active filters.",
|
|
110
|
+
reviewStatus: certifiedTiles.length > 0 ? "certified" : "draft_ready",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "research",
|
|
114
|
+
title: "Analysis",
|
|
115
|
+
purpose: "Review-required analyst memos for follow-up questions that need new SQL or deeper analysis.",
|
|
116
|
+
reviewStatus: "draft_ready",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
globalFilters: filters,
|
|
120
|
+
selectedEvidence: certifiedContextSummary.map((item) => ({
|
|
121
|
+
source: `${item.kind}:${item.name}`,
|
|
122
|
+
reason: item.reason,
|
|
123
|
+
kind: item.kind,
|
|
124
|
+
nodeId: item.nodeId,
|
|
125
|
+
trustState: "certified",
|
|
126
|
+
})),
|
|
127
|
+
missingEvidence,
|
|
128
|
+
scopedReports,
|
|
134
129
|
pages: [
|
|
135
130
|
{
|
|
136
131
|
id: "overview",
|
|
137
132
|
title: inferDashboardTitle(prompt, domain, appName),
|
|
138
|
-
description: `Certified app surface for ${audience}. Draft gaps stay in
|
|
133
|
+
description: `Certified app surface for ${audience}. Draft gaps stay in reports until reviewed.`,
|
|
139
134
|
filters,
|
|
140
|
-
tiles:
|
|
135
|
+
tiles: certifiedTiles,
|
|
141
136
|
},
|
|
142
137
|
],
|
|
143
138
|
caveats: [
|
|
144
139
|
"Generated app plans are local draft artifacts until reviewed.",
|
|
145
|
-
"Generated dashboards render certified block tiles only; draft and
|
|
140
|
+
"Generated dashboards render certified block tiles only; draft narrative, proof, and analysis suggestions stay as scoped memos, Copilot answers, or promoted reviewed insights.",
|
|
146
141
|
],
|
|
147
142
|
reviewTasks: [
|
|
148
|
-
"Review every
|
|
143
|
+
"Review every scoped analysis memo before stakeholder use.",
|
|
144
|
+
...scopedReports.map((report) => `Run scoped analysis "${report.title}" from app Copilot with the stakeholder question and active filters, then pin or promote only after review.`),
|
|
149
145
|
"Run dql app build after accepting the generated files.",
|
|
150
|
-
"Use app
|
|
146
|
+
"Use app Copilot analysis for additional questions, then promote reviewed SQL results to draft or certified blocks.",
|
|
151
147
|
],
|
|
152
148
|
};
|
|
153
149
|
}
|
|
@@ -199,6 +195,7 @@ export function validateAppPlan(plan, kg) {
|
|
|
199
195
|
}
|
|
200
196
|
else {
|
|
201
197
|
draftTiles += 1;
|
|
198
|
+
issues.push(error(path, "stakeholder dashboard tiles must be certified blocks; route generated story, trust, and analysis work through scoped reports or Copilot investigations"));
|
|
202
199
|
if (tile.certification === "certified") {
|
|
203
200
|
issues.push(error(path, "non-block generated tiles cannot be marked certified"));
|
|
204
201
|
}
|
|
@@ -209,7 +206,7 @@ export function validateAppPlan(plan, kg) {
|
|
|
209
206
|
}
|
|
210
207
|
}
|
|
211
208
|
if (certifiedTiles === 0) {
|
|
212
|
-
issues.push(warn("pages", "no certified blocks matched; generated
|
|
209
|
+
issues.push(warn("pages", "no certified blocks matched; generated dashboard has no governed result tiles yet"));
|
|
213
210
|
}
|
|
214
211
|
return {
|
|
215
212
|
ok: issues.every((issue) => issue.level !== "error"),
|
|
@@ -315,6 +312,7 @@ export function generateAppFromPlan(projectRoot, plan, kg, options = {}) {
|
|
|
315
312
|
id: filter.id,
|
|
316
313
|
type: filter.type,
|
|
317
314
|
default: filter.default,
|
|
315
|
+
...(filter.options?.length ? { options: filter.options } : {}),
|
|
318
316
|
bindsTo: filter.bindsTo,
|
|
319
317
|
})),
|
|
320
318
|
layout: {
|
|
@@ -407,6 +405,133 @@ function mergeCertifiedBlockNodes(preferredNodes, matchedNodes) {
|
|
|
407
405
|
}
|
|
408
406
|
return merged;
|
|
409
407
|
}
|
|
408
|
+
function dedupeCertifiedBlockNodes(nodes, preferredNodeIds = new Set()) {
|
|
409
|
+
const selected = [];
|
|
410
|
+
const signatureIndex = new Map();
|
|
411
|
+
for (const node of nodes) {
|
|
412
|
+
const signatures = certifiedBlockDuplicateSignatures(node);
|
|
413
|
+
let duplicateIndex = -1;
|
|
414
|
+
for (const signature of signatures) {
|
|
415
|
+
const existing = signatureIndex.get(signature);
|
|
416
|
+
if (existing !== undefined) {
|
|
417
|
+
duplicateIndex = existing;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (duplicateIndex < 0) {
|
|
422
|
+
signatureIndex.set(node.nodeId, selected.length);
|
|
423
|
+
for (const signature of signatures)
|
|
424
|
+
signatureIndex.set(signature, selected.length);
|
|
425
|
+
selected.push(node);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const current = selected[duplicateIndex];
|
|
429
|
+
if (certifiedBlockSelectionScore(node, preferredNodeIds) > certifiedBlockSelectionScore(current, preferredNodeIds)) {
|
|
430
|
+
selected[duplicateIndex] = node;
|
|
431
|
+
signatureIndex.set(node.nodeId, duplicateIndex);
|
|
432
|
+
for (const signature of signatures)
|
|
433
|
+
signatureIndex.set(signature, duplicateIndex);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return selected;
|
|
437
|
+
}
|
|
438
|
+
function certifiedBlockDuplicateSignatures(node) {
|
|
439
|
+
const signatures = new Set([node.nodeId]);
|
|
440
|
+
if (node.sqlFingerprints?.exact)
|
|
441
|
+
signatures.add(`sql:${node.sqlFingerprints.exact}`);
|
|
442
|
+
if (node.sqlFingerprints?.parameterized)
|
|
443
|
+
signatures.add(`sqlp:${node.sqlFingerprints.parameterized}`);
|
|
444
|
+
if (node.businessFingerprint?.hash)
|
|
445
|
+
signatures.add(`business:${node.businessFingerprint.hash}`);
|
|
446
|
+
const topic = certifiedBlockTopicSignature(node);
|
|
447
|
+
if (topic)
|
|
448
|
+
signatures.add(`topic:${topic}`);
|
|
449
|
+
return Array.from(signatures);
|
|
450
|
+
}
|
|
451
|
+
function certifiedBlockTopicSignature(node) {
|
|
452
|
+
const text = [
|
|
453
|
+
node.name,
|
|
454
|
+
node.description,
|
|
455
|
+
node.llmContext,
|
|
456
|
+
node.businessOutcome,
|
|
457
|
+
node.decisionUse,
|
|
458
|
+
node.grain,
|
|
459
|
+
...(node.tags ?? []),
|
|
460
|
+
...(node.entities ?? []),
|
|
461
|
+
...(node.declaredOutputs ?? []),
|
|
462
|
+
...(node.dimensions ?? []),
|
|
463
|
+
...(node.allowedFilters ?? []),
|
|
464
|
+
...(node.businessFingerprint?.tokens ?? []),
|
|
465
|
+
].join(" ").toLowerCase();
|
|
466
|
+
const sources = normalizedTopicTokens([
|
|
467
|
+
...(node.sourceSystems ?? []),
|
|
468
|
+
...(node.businessFingerprint?.tokens ?? [])
|
|
469
|
+
.filter((token) => /^(source|system):/i.test(token))
|
|
470
|
+
.map((token) => token.replace(/^(source|system):/i, "")),
|
|
471
|
+
...Array.from(text.matchAll(/\b(?:from|join)\s+([a-z0-9_.]+)/gi)).map((match) => match[1]),
|
|
472
|
+
...Array.from(text.matchAll(/\b([a-z0-9_]*int_player_stats|[a-z0-9_]*fct_[a-z0-9_]+|[a-z0-9_]*dim_[a-z0-9_]+)\b/gi)).map((match) => match[1]),
|
|
473
|
+
]);
|
|
474
|
+
const entity = firstTopicFamily(text, [
|
|
475
|
+
["player", /\b(player|players|scorer|scorers|athlete)\b/],
|
|
476
|
+
["customer", /\b(customer|account|user|subscriber)\b/],
|
|
477
|
+
["order", /\b(order|orders|purchase)\b/],
|
|
478
|
+
["product", /\b(product|sku|item)\b/],
|
|
479
|
+
["team", /\b(team|teams)\b/],
|
|
480
|
+
]);
|
|
481
|
+
const intent = firstTopicFamily(text, [
|
|
482
|
+
["availability", /\b(availability|freshness|record count|records|quality|coverage)\b/],
|
|
483
|
+
["ranking", /\b(top|bottom|rank|ranking|leader|leaderboard|scorer|scorers)\b/],
|
|
484
|
+
["trend", /\b(trend|weekly|monthly|daily|time series|over time)\b/],
|
|
485
|
+
["segment", /\b(segment|breakdown|split|by segment|cohort|region|channel)\b/],
|
|
486
|
+
["profile", /\b(profile|360|detail|drilldown)\b/],
|
|
487
|
+
["kpi", /\b(kpi|metric|total|summary|scorecard)\b/],
|
|
488
|
+
]);
|
|
489
|
+
const metric = firstTopicFamily(text, [
|
|
490
|
+
["points", /\b(point|points|pts|score|scoring|scorer|scorers)\b/],
|
|
491
|
+
["revenue", /\b(revenue|arr|sales|amount|bookings)\b/],
|
|
492
|
+
["count", /\b(count|records|games played|orders|users)\b/],
|
|
493
|
+
["quality", /\b(availability|freshness|quality|coverage)\b/],
|
|
494
|
+
["conversion", /\b(conversion|rate|pct|percent|ratio)\b/],
|
|
495
|
+
]);
|
|
496
|
+
if (!sources.length || !intent || !metric)
|
|
497
|
+
return "";
|
|
498
|
+
return [sources.slice(0, 3).join("+"), entity, intent, metric].filter(Boolean).join("|");
|
|
499
|
+
}
|
|
500
|
+
function normalizedTopicTokens(values) {
|
|
501
|
+
return Array.from(new Set(values
|
|
502
|
+
.map((value) => value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, ""))
|
|
503
|
+
.map((value) => value
|
|
504
|
+
.replace(/^.*\./, "")
|
|
505
|
+
.replace(/^transformed_/, "")
|
|
506
|
+
.replace(/^nba_analytics_/, ""))
|
|
507
|
+
.filter(Boolean)))
|
|
508
|
+
.sort();
|
|
509
|
+
}
|
|
510
|
+
function firstTopicFamily(text, patterns) {
|
|
511
|
+
return patterns.find(([, pattern]) => pattern.test(text))?.[0] ?? "";
|
|
512
|
+
}
|
|
513
|
+
function certifiedBlockSelectionScore(node, preferredNodeIds) {
|
|
514
|
+
let score = preferredNodeIds.has(node.nodeId) ? 10000 : 0;
|
|
515
|
+
score += (node.declaredOutputs?.length ?? 0) * 70;
|
|
516
|
+
score += (node.allowedFilters?.length ?? 0) * 45;
|
|
517
|
+
score += (node.parameterPolicy?.length ?? 0) * 45;
|
|
518
|
+
score += (node.entities?.length ?? 0) * 35;
|
|
519
|
+
score += (node.businessFingerprint?.tokens?.length ?? 0) * 8;
|
|
520
|
+
if (node.grain)
|
|
521
|
+
score += 90;
|
|
522
|
+
if (node.pattern)
|
|
523
|
+
score += 60;
|
|
524
|
+
if (node.sqlFingerprints?.parameterized)
|
|
525
|
+
score += 40;
|
|
526
|
+
if (node.description && node.description.length > 80)
|
|
527
|
+
score += 25;
|
|
528
|
+
const text = [node.name, node.description, node.llmContext, ...(node.tags ?? [])].join(" ").toLowerCase();
|
|
529
|
+
if (/\b(raw-sql|imported|codex-e2e|test)\b/.test(text))
|
|
530
|
+
score -= 80;
|
|
531
|
+
if (/\breview required\b/.test(text))
|
|
532
|
+
score -= 10;
|
|
533
|
+
return score;
|
|
534
|
+
}
|
|
410
535
|
function findCertifiedContextNodes(kg, prompt, domain, limit) {
|
|
411
536
|
const kinds = [
|
|
412
537
|
"business_view",
|
|
@@ -458,13 +583,14 @@ function normalizeGoal(prompt) {
|
|
|
458
583
|
.trim()
|
|
459
584
|
.replace(/\.$/, "");
|
|
460
585
|
}
|
|
461
|
-
function
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
586
|
+
function stakeholderSummaryForPlan(input) {
|
|
587
|
+
const coverage = input.certifiedTiles > 0
|
|
588
|
+
? `${input.certifiedTiles} certified tile${input.certifiedTiles === 1 ? "" : "s"} anchor the app.`
|
|
589
|
+
: "No certified tiles matched strongly enough yet.";
|
|
590
|
+
const review = input.scopedReports > 0
|
|
591
|
+
? `${input.scopedReports} scoped analysis memo${input.scopedReports === 1 ? "" : "s"} capture questions that need deeper review.`
|
|
592
|
+
: "No scoped analysis memos were needed.";
|
|
593
|
+
return `${input.appName} is a ${titleCase(input.domain)} decision room for ${input.audience}. ${coverage} ${review} Intent: ${titleCase(input.analysisIntent.replace(/_/g, " "))}.`;
|
|
468
594
|
}
|
|
469
595
|
function summarizeCertifiedContext(nodes) {
|
|
470
596
|
const seen = new Set();
|
|
@@ -490,13 +616,13 @@ function missingEvidenceForPlan(prompt, intent, certifiedNodes, contextNodes) {
|
|
|
490
616
|
const lower = prompt.toLowerCase();
|
|
491
617
|
const missing = new Set();
|
|
492
618
|
if (certifiedNodes.length === 0) {
|
|
493
|
-
missing.add("No certified block matched strongly enough for the primary dashboard
|
|
619
|
+
missing.add("No certified block matched strongly enough for the primary dashboard proof.");
|
|
494
620
|
}
|
|
495
621
|
if (contextNodes.length === 0) {
|
|
496
|
-
missing.add("No certified business view, term, semantic object, or dbt context was matched for
|
|
622
|
+
missing.add("No certified business view, term, semantic object, or dbt context was matched for supporting explanation.");
|
|
497
623
|
}
|
|
498
624
|
if (intent === "driver_analysis" || /\bwhy|driver|break\s*down/.test(lower)) {
|
|
499
|
-
missing.add("Driver analysis should be opened as review-required
|
|
625
|
+
missing.add("Driver analysis should be opened as a review-required report until a certified drilldown block exists.");
|
|
500
626
|
}
|
|
501
627
|
if (intent === "trust_review" || /\btrust|rely|lineage/.test(lower)) {
|
|
502
628
|
missing.add("Leadership trust checks need lineage, owner, caveats, and review cadence confirmation.");
|
|
@@ -509,19 +635,19 @@ function missingEvidenceForPlan(prompt, intent, certifiedNodes, contextNodes) {
|
|
|
509
635
|
function displayStrategyForIntent(intent) {
|
|
510
636
|
switch (intent) {
|
|
511
637
|
case "driver_analysis":
|
|
512
|
-
return "Lead with certified summary
|
|
638
|
+
return "Lead with certified summary proof, then route root-cause questions into Copilot reports.";
|
|
513
639
|
case "entity_drilldown":
|
|
514
640
|
return "Lead with entity context and certified performance blocks, then provide drilldown and trust review paths.";
|
|
515
641
|
case "trust_review":
|
|
516
642
|
return "Lead with certification, lineage, caveats, and review tasks before any generated interpretation.";
|
|
517
643
|
case "data_quality":
|
|
518
|
-
return "Lead with availability
|
|
644
|
+
return "Lead with availability and freshness proof, then list gaps and review actions.";
|
|
519
645
|
case "experiment_readout":
|
|
520
646
|
return "Lead with KPI impact, guardrails, segment readout, and decision caveats.";
|
|
521
647
|
case "metric_monitoring":
|
|
522
648
|
return "Lead with KPIs, trends, and breakdowns for recurring operating review.";
|
|
523
649
|
default:
|
|
524
|
-
return "Lead with the business brief, certified metrics, supporting breakdowns, then
|
|
650
|
+
return "Lead with the business brief, certified metrics, supporting breakdowns, then proof and report paths.";
|
|
525
651
|
}
|
|
526
652
|
}
|
|
527
653
|
function displayForCertifiedNode(node, viz, index, intent) {
|
|
@@ -541,7 +667,7 @@ function displayForCertifiedNode(node, viz, index, intent) {
|
|
|
541
667
|
? "evidence"
|
|
542
668
|
: /\btop\s*\d+|rank|ranking|leader|leaderboard|scorer|score|goal|segment|breakdown|split|driver|player|customer|account|region|channel\b/.test(text)
|
|
543
669
|
? "breakdown"
|
|
544
|
-
: "
|
|
670
|
+
: "breakdown";
|
|
545
671
|
const followUpActions = [
|
|
546
672
|
"ask_follow_up",
|
|
547
673
|
"open_research",
|
|
@@ -585,7 +711,7 @@ function displayForDraftTile(title, viz, index) {
|
|
|
585
711
|
"review_trust",
|
|
586
712
|
"create_draft_block",
|
|
587
713
|
];
|
|
588
|
-
const rationale = "Generated section captures missing
|
|
714
|
+
const rationale = "Generated section captures missing proof or review work without implying certification.";
|
|
589
715
|
return {
|
|
590
716
|
role,
|
|
591
717
|
recommendedDisplayType: viz,
|
|
@@ -709,9 +835,9 @@ function insightTitleForPresentation(title, role, component) {
|
|
|
709
835
|
if (component === "BusinessBrief")
|
|
710
836
|
return "Business context";
|
|
711
837
|
if (component === "TrustCallout")
|
|
712
|
-
return "
|
|
838
|
+
return "Proof and lineage";
|
|
713
839
|
if (component === "ResearchActions")
|
|
714
|
-
return "Follow-up
|
|
840
|
+
return "Follow-up analysis";
|
|
715
841
|
if (role === "kpi")
|
|
716
842
|
return title;
|
|
717
843
|
if (component === "RankingPanel")
|
|
@@ -735,9 +861,11 @@ function inferExpectedGrain(text, intent) {
|
|
|
735
861
|
return "metric";
|
|
736
862
|
return "dashboard";
|
|
737
863
|
}
|
|
738
|
-
function tileFromCertifiedNode(node, index, intent) {
|
|
864
|
+
function tileFromCertifiedNode(node, index, intent, globalFilters = []) {
|
|
739
865
|
const blockId = node.name;
|
|
740
866
|
const viz = inferVizForNode(node, index);
|
|
867
|
+
const filterBindings = filterBindingsForCertifiedNode(node, globalFilters);
|
|
868
|
+
const parameterBindings = parameterBindingsForCertifiedNode(node, filterBindings);
|
|
741
869
|
return {
|
|
742
870
|
id: slugify(blockId) || `certified-${index + 1}`,
|
|
743
871
|
title: node.name,
|
|
@@ -748,6 +876,20 @@ function tileFromCertifiedNode(node, index, intent) {
|
|
|
748
876
|
viz,
|
|
749
877
|
certification: "certified",
|
|
750
878
|
reviewStatus: "certified",
|
|
879
|
+
trustState: "certified",
|
|
880
|
+
...(filterBindings.length ? { filterBindings } : {}),
|
|
881
|
+
...(parameterBindings.length ? { parameterBindings } : {}),
|
|
882
|
+
sourceEvidence: [{
|
|
883
|
+
source: node.nodeId,
|
|
884
|
+
reason: node.decisionUse ??
|
|
885
|
+
node.businessOutcome ??
|
|
886
|
+
node.description ??
|
|
887
|
+
"Certified DQL block matched the app prompt.",
|
|
888
|
+
kind: node.kind,
|
|
889
|
+
nodeId: node.nodeId,
|
|
890
|
+
path: node.sourcePath,
|
|
891
|
+
trustState: "certified",
|
|
892
|
+
}],
|
|
751
893
|
display: displayForCertifiedNode(node, viz, index, intent),
|
|
752
894
|
rationale: node.decisionUse ??
|
|
753
895
|
node.businessOutcome ??
|
|
@@ -758,11 +900,108 @@ function tileFromCertifiedNode(node, index, intent) {
|
|
|
758
900
|
],
|
|
759
901
|
};
|
|
760
902
|
}
|
|
903
|
+
function filterBindingsForCertifiedNode(node, globalFilters) {
|
|
904
|
+
const bindings = (node.filterBindings ?? []).map((entry) => ({
|
|
905
|
+
filter: entry.filter,
|
|
906
|
+
binding: entry.binding,
|
|
907
|
+
mode: "predicate",
|
|
908
|
+
}));
|
|
909
|
+
const dynamicParams = new Set((node.parameterPolicy ?? [])
|
|
910
|
+
.filter((entry) => entry.policy === "dynamic")
|
|
911
|
+
.map((entry) => entry.name));
|
|
912
|
+
const allowedFilters = new Set((node.allowedFilters ?? []).map((entry) => entry.toLowerCase()));
|
|
913
|
+
for (const filter of globalFilters) {
|
|
914
|
+
if (bindings.some((entry) => entry.filter === filter.id))
|
|
915
|
+
continue;
|
|
916
|
+
if (dynamicParams.has(filter.id)) {
|
|
917
|
+
bindings.push({
|
|
918
|
+
filter: filter.id,
|
|
919
|
+
binding: filter.bindsTo ?? filter.id,
|
|
920
|
+
mode: "parameter",
|
|
921
|
+
paramNames: [filter.id],
|
|
922
|
+
});
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const matchingParam = parameterForFilter(filter, dynamicParams);
|
|
926
|
+
if (matchingParam) {
|
|
927
|
+
bindings.push({
|
|
928
|
+
filter: filter.id,
|
|
929
|
+
binding: filter.bindsTo ?? matchingParam,
|
|
930
|
+
mode: "parameter",
|
|
931
|
+
paramNames: [matchingParam],
|
|
932
|
+
});
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if (filter.bindsTo && allowedFilters.has(filter.bindsTo.toLowerCase())) {
|
|
936
|
+
bindings.push({
|
|
937
|
+
filter: filter.id,
|
|
938
|
+
binding: filter.bindsTo,
|
|
939
|
+
mode: "predicate",
|
|
940
|
+
});
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (filter.id && allowedFilters.has(filter.id.toLowerCase())) {
|
|
944
|
+
bindings.push({
|
|
945
|
+
filter: filter.id,
|
|
946
|
+
binding: filter.bindsTo ?? filter.id,
|
|
947
|
+
mode: "predicate",
|
|
948
|
+
});
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (filter.id) {
|
|
952
|
+
bindings.push({
|
|
953
|
+
filter: filter.id,
|
|
954
|
+
binding: filter.bindsTo,
|
|
955
|
+
unsupportedReason: "No matching certified block parameter or allowed filter was declared.",
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return uniqueTileFilterBindings(bindings);
|
|
960
|
+
}
|
|
961
|
+
function parameterBindingsForCertifiedNode(node, filterBindings) {
|
|
962
|
+
const dynamicParams = (node.parameterPolicy ?? [])
|
|
963
|
+
.filter((entry) => entry.policy === "dynamic")
|
|
964
|
+
.map((entry) => entry.name);
|
|
965
|
+
return dynamicParams.map((param) => {
|
|
966
|
+
const filterBinding = filterBindings.find((entry) => entry.paramNames?.includes(param) || entry.filter === param);
|
|
967
|
+
return {
|
|
968
|
+
param,
|
|
969
|
+
source: filterBinding ? "dashboard_filter" : "variable",
|
|
970
|
+
...(filterBinding?.filter ? { filter: filterBinding.filter } : {}),
|
|
971
|
+
field: filterBinding?.binding ?? param,
|
|
972
|
+
};
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function parameterForFilter(filter, dynamicParams) {
|
|
976
|
+
const candidates = [];
|
|
977
|
+
if (filter.id === "season")
|
|
978
|
+
candidates.push("season", "season_year", "year");
|
|
979
|
+
if (filter.id === "season_start")
|
|
980
|
+
candidates.push("season_start", "start_year", "from_year");
|
|
981
|
+
if (filter.id === "season_end")
|
|
982
|
+
candidates.push("season_end", "end_year", "to_year");
|
|
983
|
+
if (filter.id === "top_n")
|
|
984
|
+
candidates.push("top_n", "limit", "n");
|
|
985
|
+
candidates.push(filter.id);
|
|
986
|
+
return candidates.find((candidate) => dynamicParams.has(candidate)) ?? null;
|
|
987
|
+
}
|
|
988
|
+
function uniqueTileFilterBindings(bindings) {
|
|
989
|
+
const seen = new Set();
|
|
990
|
+
const out = [];
|
|
991
|
+
for (const binding of bindings) {
|
|
992
|
+
const key = `${binding.filter}:${binding.binding ?? ""}:${binding.mode ?? ""}:${(binding.paramNames ?? []).join(",")}:${binding.unsupportedReason ?? ""}`;
|
|
993
|
+
if (seen.has(key))
|
|
994
|
+
continue;
|
|
995
|
+
seen.add(key);
|
|
996
|
+
out.push(binding);
|
|
997
|
+
}
|
|
998
|
+
return out;
|
|
999
|
+
}
|
|
761
1000
|
function buildLayoutItems(tiles) {
|
|
762
1001
|
let x = 0;
|
|
763
1002
|
let y = 0;
|
|
764
1003
|
let rowH = 0;
|
|
765
|
-
const orderedTiles = [...tiles].sort((a, b) => {
|
|
1004
|
+
const orderedTiles = [...tiles].filter(isDashboardTile).sort((a, b) => {
|
|
766
1005
|
const priorityA = a.display?.layoutPriority ?? 50;
|
|
767
1006
|
const priorityB = b.display?.layoutPriority ?? 50;
|
|
768
1007
|
return priorityA - priorityB;
|
|
@@ -799,6 +1038,11 @@ function buildLayoutItems(tiles) {
|
|
|
799
1038
|
},
|
|
800
1039
|
},
|
|
801
1040
|
display: displayMetadataForTile(tile, genUi),
|
|
1041
|
+
...(tile.filterBindings?.length ? { filterBindings: tile.filterBindings } : {}),
|
|
1042
|
+
...(tile.parameterBindings?.length ? { parameterBindings: tile.parameterBindings } : {}),
|
|
1043
|
+
...(tile.sourceEvidence?.length ? { sourceEvidence: tile.sourceEvidence } : {}),
|
|
1044
|
+
trustState: tile.trustState ?? tile.display?.trustState ?? (tile.certification === "certified" ? "certified" : "draft_ready"),
|
|
1045
|
+
reviewStatus: tile.reviewStatus,
|
|
802
1046
|
};
|
|
803
1047
|
if (isDashboardTile(tile)) {
|
|
804
1048
|
item.block = { blockId: tile.blockId };
|
|
@@ -835,9 +1079,25 @@ function isDashboardTile(tile) {
|
|
|
835
1079
|
return tile.kind === "certified_block" && tile.certification === "certified" && Boolean(tile.blockId);
|
|
836
1080
|
}
|
|
837
1081
|
function appPlanReadme(plan, validation) {
|
|
1082
|
+
const reviewDashboardItems = plan.pages.flatMap((page) => page.tiles
|
|
1083
|
+
.filter((tile) => !isDashboardTile(tile))
|
|
1084
|
+
.map((tile) => `- ${tile.title}: ${tile.description ?? tile.rationale ?? "Review before adding to the app."}`));
|
|
1085
|
+
const scopedReports = plan.scopedReports.length > 0
|
|
1086
|
+
? plan.scopedReports.map((report) => [
|
|
1087
|
+
`- ${report.title}: ${report.description}`,
|
|
1088
|
+
` - Question: ${report.question}`,
|
|
1089
|
+
` - Evidence needed: ${report.evidenceNeeded.join(", ")}`,
|
|
1090
|
+
].join("\n"))
|
|
1091
|
+
: reviewDashboardItems.length > 0
|
|
1092
|
+
? reviewDashboardItems
|
|
1093
|
+
: plan.missingEvidence.length > 0
|
|
1094
|
+
? plan.missingEvidence.map((item) => `- ${item}`)
|
|
1095
|
+
: ["- No scoped analysis memos were suggested. Use app Copilot for follow-up questions."];
|
|
838
1096
|
return [
|
|
839
1097
|
`# ${plan.name}`,
|
|
840
1098
|
"",
|
|
1099
|
+
plan.stakeholderSummary,
|
|
1100
|
+
"",
|
|
841
1101
|
plan.businessGoal,
|
|
842
1102
|
"",
|
|
843
1103
|
`- Generated from prompt: ${plan.prompt}`,
|
|
@@ -846,8 +1106,10 @@ function appPlanReadme(plan, validation) {
|
|
|
846
1106
|
`- Domain: ${plan.domain}`,
|
|
847
1107
|
`- Audience: ${plan.audience}`,
|
|
848
1108
|
`- Lifecycle: ${plan.lifecycle}`,
|
|
849
|
-
`- Certified tiles: ${validation.certifiedTiles}`,
|
|
850
|
-
`- Review
|
|
1109
|
+
`- Certified dashboard tiles: ${validation.certifiedTiles}`,
|
|
1110
|
+
`- Review-required dashboard tiles: ${validation.draftTiles}`,
|
|
1111
|
+
`- Scoped analysis memos: ${plan.scopedReports.length}`,
|
|
1112
|
+
`- Global filters: ${plan.globalFilters.map((filter) => filter.id).join(", ") || "none"}`,
|
|
851
1113
|
"",
|
|
852
1114
|
"## Planner brief",
|
|
853
1115
|
"",
|
|
@@ -861,17 +1123,15 @@ function appPlanReadme(plan, validation) {
|
|
|
861
1123
|
? plan.planning.certifiedContext.map((item) => `- ${item.kind}:${item.name} — ${item.reason}`)
|
|
862
1124
|
: ["- No certified context matched strongly enough."]),
|
|
863
1125
|
"",
|
|
864
|
-
"## Missing
|
|
1126
|
+
"## Missing proof",
|
|
865
1127
|
"",
|
|
866
1128
|
...(plan.planning.missingEvidence.length
|
|
867
1129
|
? plan.planning.missingEvidence.map((item) => `- ${item}`)
|
|
868
|
-
: ["- No missing
|
|
1130
|
+
: ["- No missing proof was identified by the deterministic planner."]),
|
|
869
1131
|
"",
|
|
870
|
-
"##
|
|
1132
|
+
"## Scoped analysis memos",
|
|
871
1133
|
"",
|
|
872
|
-
...
|
|
873
|
-
.filter((tile) => !isDashboardTile(tile))
|
|
874
|
-
.map((tile) => `- ${tile.title}: ${tile.description ?? tile.rationale ?? "Review before adding to the app."}`)),
|
|
1134
|
+
...scopedReports,
|
|
875
1135
|
"",
|
|
876
1136
|
"## Agent skills applied",
|
|
877
1137
|
"",
|
|
@@ -942,59 +1202,83 @@ function inferTags(prompt, domain) {
|
|
|
942
1202
|
tags.add("monthly-review");
|
|
943
1203
|
return Array.from(tags).filter(Boolean);
|
|
944
1204
|
}
|
|
945
|
-
function
|
|
1205
|
+
function inferScopedReports(prompt, domain, intent, certifiedNodes) {
|
|
946
1206
|
const lower = prompt.toLowerCase();
|
|
947
1207
|
const domainLabel = titleCase(domain);
|
|
948
|
-
const
|
|
1208
|
+
const reports = [
|
|
949
1209
|
{
|
|
950
|
-
title: `${domainLabel} decision story`,
|
|
951
|
-
|
|
952
|
-
|
|
1210
|
+
title: `${domainLabel} decision story report`,
|
|
1211
|
+
question: `What is the decision story behind "${normalizeGoal(prompt)}"?`,
|
|
1212
|
+
description: "Analyst narrative that ties certified results to the business decision, caveats, and recommended next action.",
|
|
1213
|
+
intent,
|
|
1214
|
+
evidenceNeeded: ["certified block results", "active filters", "lineage and owner context"],
|
|
953
1215
|
},
|
|
954
1216
|
];
|
|
955
1217
|
if (intent === "driver_analysis" || /\bwhy|driver|break\s*down|root cause\b/.test(lower)) {
|
|
956
|
-
|
|
957
|
-
title: "
|
|
958
|
-
|
|
959
|
-
|
|
1218
|
+
reports.push({
|
|
1219
|
+
title: "Driver analysis report",
|
|
1220
|
+
question: "Which drivers, segments, or entities explain the movement?",
|
|
1221
|
+
description: "Review-required report for drivers, exceptions, segment comparison, and entity drilldown.",
|
|
1222
|
+
intent: "driver_analysis",
|
|
1223
|
+
evidenceNeeded: ["comparison period", "segment fields", "metric definition", "previewed SQL"],
|
|
960
1224
|
});
|
|
961
1225
|
}
|
|
962
1226
|
else if (/\brisk|caveat|issue|anomal|quality\b/.test(lower)) {
|
|
963
|
-
|
|
964
|
-
title: "
|
|
965
|
-
|
|
966
|
-
|
|
1227
|
+
reports.push({
|
|
1228
|
+
title: "Risk and caveat report",
|
|
1229
|
+
question: "Which caveats or quality risks should stakeholders understand before using this app?",
|
|
1230
|
+
description: "Review checklist for risks that need certified evidence before stakeholder use.",
|
|
1231
|
+
intent: "data_quality",
|
|
1232
|
+
evidenceNeeded: ["freshness checks", "row counts", "known caveats", "owner review"],
|
|
967
1233
|
});
|
|
968
1234
|
}
|
|
969
1235
|
else if (/\bsegment|cohort|region|location|product|channel|customer\b/.test(lower)) {
|
|
970
|
-
|
|
971
|
-
title: "
|
|
1236
|
+
reports.push({
|
|
1237
|
+
title: "Drilldown certification report",
|
|
1238
|
+
question: "Which slices or drilldowns should become reusable certified blocks?",
|
|
972
1239
|
description: "Candidate slices and drilldowns the agent could not fully back with certified blocks yet.",
|
|
973
|
-
|
|
1240
|
+
intent: "entity_drilldown",
|
|
1241
|
+
evidenceNeeded: ["slice dimensions", "grain", "allowed filters", "previewed SQL"],
|
|
974
1242
|
});
|
|
975
1243
|
}
|
|
976
1244
|
else {
|
|
977
|
-
|
|
978
|
-
title: "
|
|
979
|
-
|
|
980
|
-
|
|
1245
|
+
reports.push({
|
|
1246
|
+
title: "Proof and lineage gaps",
|
|
1247
|
+
question: "What proof is still missing before this app can be treated as governed?",
|
|
1248
|
+
description: "Open proof, lineage, and review tasks to complete before this app is governed.",
|
|
1249
|
+
intent: "proof_review",
|
|
1250
|
+
evidenceNeeded: ["lineage", "certification state", "tests", "review cadence"],
|
|
981
1251
|
});
|
|
982
1252
|
}
|
|
983
1253
|
if (certifiedNodes.length === 0) {
|
|
984
|
-
|
|
1254
|
+
reports.push({
|
|
985
1255
|
title: "Certified block search",
|
|
1256
|
+
question: "Which existing or new DQL blocks should support this app?",
|
|
986
1257
|
description: "No certified blocks matched strongly enough; review suggested sources and create certified blocks.",
|
|
987
|
-
|
|
1258
|
+
intent: "proof_review",
|
|
1259
|
+
evidenceNeeded: ["candidate dbt models", "semantic metrics", "business terms", "draft block plan"],
|
|
988
1260
|
});
|
|
989
1261
|
}
|
|
990
|
-
if (!
|
|
991
|
-
|
|
992
|
-
title: "
|
|
1262
|
+
if (!reports.some((report) => /\btrust|evidence|risk|caveat|gap|proof/.test(report.title.toLowerCase()))) {
|
|
1263
|
+
reports.push({
|
|
1264
|
+
title: "Proof and lineage gaps",
|
|
1265
|
+
question: "What lineage and certification proof should be reviewed?",
|
|
993
1266
|
description: "Certification, lineage, caveats, and review actions that must be confirmed before stakeholder use.",
|
|
994
|
-
|
|
1267
|
+
intent: "proof_review",
|
|
1268
|
+
evidenceNeeded: ["lineage", "certification state", "owner", "tests"],
|
|
995
1269
|
});
|
|
996
1270
|
}
|
|
997
|
-
return
|
|
1271
|
+
return reports.map((report, index) => ({
|
|
1272
|
+
id: slugify(report.title) || `scoped-report-${index + 1}`,
|
|
1273
|
+
title: report.title,
|
|
1274
|
+
question: report.question,
|
|
1275
|
+
description: report.description,
|
|
1276
|
+
intent: report.intent,
|
|
1277
|
+
reviewStatus: "draft_ready",
|
|
1278
|
+
source: "app_builder",
|
|
1279
|
+
evidenceNeeded: report.evidenceNeeded,
|
|
1280
|
+
suggestedActions: ["open_research", "review_trust", "create_draft_block"],
|
|
1281
|
+
}));
|
|
998
1282
|
}
|
|
999
1283
|
function inferDomain(prompt) {
|
|
1000
1284
|
const lower = prompt.toLowerCase();
|
|
@@ -1026,6 +1310,12 @@ function inferAudience(prompt) {
|
|
|
1026
1310
|
function inferFilters(prompt) {
|
|
1027
1311
|
const filters = [];
|
|
1028
1312
|
const lower = prompt.toLowerCase();
|
|
1313
|
+
const wantsSeasonStart = /\bseason[_\s-]*start\b/.test(lower);
|
|
1314
|
+
const wantsSeasonEnd = /\bseason[_\s-]*end\b/.test(lower);
|
|
1315
|
+
const wantsTopN = /\btop[_\s-]*n\b/.test(lower);
|
|
1316
|
+
const years = Array.from(new Set(Array.from(prompt.matchAll(/\b(20\d{2}|19\d{2})\b/g)).map((match) => Number(match[1]))))
|
|
1317
|
+
.filter((year) => Number.isFinite(year))
|
|
1318
|
+
.sort((a, b) => a - b);
|
|
1029
1319
|
if (/\bweekly|week|last week|this week\b/.test(lower)) {
|
|
1030
1320
|
filters.push({
|
|
1031
1321
|
id: "week",
|
|
@@ -1042,6 +1332,47 @@ function inferFilters(prompt) {
|
|
|
1042
1332
|
bindsTo: "date",
|
|
1043
1333
|
});
|
|
1044
1334
|
}
|
|
1335
|
+
if (wantsSeasonStart || wantsSeasonEnd || /\bseason|year|annual|nba\b/.test(lower) || years.length > 0) {
|
|
1336
|
+
if (years.length >= 2 || wantsSeasonStart || wantsSeasonEnd) {
|
|
1337
|
+
filters.push({
|
|
1338
|
+
id: "season_start",
|
|
1339
|
+
label: "Season start",
|
|
1340
|
+
type: "select",
|
|
1341
|
+
...(years[0] ? { default: years[0] } : {}),
|
|
1342
|
+
...(years.length ? { options: years.map(String) } : {}),
|
|
1343
|
+
bindsTo: "game_date_est.year",
|
|
1344
|
+
});
|
|
1345
|
+
filters.push({
|
|
1346
|
+
id: "season_end",
|
|
1347
|
+
label: "Season end",
|
|
1348
|
+
type: "select",
|
|
1349
|
+
...(years[years.length - 1] ? { default: years[years.length - 1] } : {}),
|
|
1350
|
+
...(years.length ? { options: years.map(String) } : {}),
|
|
1351
|
+
bindsTo: "game_date_est.year",
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
else {
|
|
1355
|
+
filters.push({
|
|
1356
|
+
id: "season",
|
|
1357
|
+
label: "Season",
|
|
1358
|
+
type: "select",
|
|
1359
|
+
...(years[0] ? { default: years[0] } : {}),
|
|
1360
|
+
...(years.length ? { options: years.map(String) } : {}),
|
|
1361
|
+
bindsTo: "game_date_est.year",
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const topN = /\btop(?:[_\s-]*n)?\s+(\d+)\b/i.exec(prompt)?.[1]
|
|
1366
|
+
?? /\btop[_\s-]*n\s*[=:]?\s*(\d+)\b/i.exec(prompt)?.[1];
|
|
1367
|
+
if (topN || wantsTopN) {
|
|
1368
|
+
filters.push({
|
|
1369
|
+
id: "top_n",
|
|
1370
|
+
label: "Top N",
|
|
1371
|
+
type: "number",
|
|
1372
|
+
...(topN ? { default: Number(topN) } : {}),
|
|
1373
|
+
bindsTo: "limit",
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1045
1376
|
if (/\benterprise|segment|smb|mid-market\b/.test(lower)) {
|
|
1046
1377
|
filters.push({
|
|
1047
1378
|
id: "segment",
|
|
@@ -1050,7 +1381,18 @@ function inferFilters(prompt) {
|
|
|
1050
1381
|
bindsTo: "segment",
|
|
1051
1382
|
});
|
|
1052
1383
|
}
|
|
1053
|
-
return filters;
|
|
1384
|
+
return uniquePlanFilters(filters);
|
|
1385
|
+
}
|
|
1386
|
+
function uniquePlanFilters(filters) {
|
|
1387
|
+
const seen = new Set();
|
|
1388
|
+
const out = [];
|
|
1389
|
+
for (const filter of filters) {
|
|
1390
|
+
if (seen.has(filter.id))
|
|
1391
|
+
continue;
|
|
1392
|
+
seen.add(filter.id);
|
|
1393
|
+
out.push(filter);
|
|
1394
|
+
}
|
|
1395
|
+
return out;
|
|
1054
1396
|
}
|
|
1055
1397
|
function inferReviewCadence(prompt) {
|
|
1056
1398
|
const lower = prompt.toLowerCase();
|