@duckcodeailabs/dql-agent 1.6.20 → 1.6.22
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/agent-run-engine.d.ts +287 -0
- package/dist/agent-run-engine.d.ts.map +1 -0
- package/dist/agent-run-engine.js +916 -0
- package/dist/agent-run-engine.js.map +1 -0
- package/dist/agent-run-gates.d.ts +26 -0
- package/dist/agent-run-gates.d.ts.map +1 -0
- package/dist/agent-run-gates.js +170 -0
- package/dist/agent-run-gates.js.map +1 -0
- package/dist/agent-run-planner.d.ts +32 -0
- package/dist/agent-run-planner.d.ts.map +1 -0
- package/dist/agent-run-planner.js +280 -0
- package/dist/agent-run-planner.js.map +1 -0
- package/dist/answer-loop.d.ts +49 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +292 -17
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +44 -2
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +558 -123
- package/dist/app-builder.js.map +1 -1
- package/dist/app-planner.d.ts +78 -0
- package/dist/app-planner.d.ts.map +1 -0
- package/dist/app-planner.js +145 -0
- package/dist/app-planner.js.map +1 -0
- package/dist/embeddings/provider.d.ts +66 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +105 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/hints/git-store.d.ts +69 -0
- package/dist/hints/git-store.d.ts.map +1 -0
- package/dist/hints/git-store.js +244 -0
- package/dist/hints/git-store.js.map +1 -0
- package/dist/hints/retrieval.d.ts +49 -0
- package/dist/hints/retrieval.d.ts.map +1 -0
- package/dist/hints/retrieval.js +60 -0
- package/dist/hints/retrieval.js.map +1 -0
- package/dist/hints/store.d.ts +39 -0
- package/dist/hints/store.d.ts.map +1 -0
- package/dist/hints/store.js +279 -0
- package/dist/hints/store.js.map +1 -0
- package/dist/hints/types.d.ts +147 -0
- package/dist/hints/types.d.ts.map +1 -0
- package/dist/hints/types.js +92 -0
- package/dist/hints/types.js.map +1 -0
- package/dist/index.d.ts +43 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +63 -0
- package/dist/intent-controller.d.ts.map +1 -0
- package/dist/intent-controller.js +150 -0
- package/dist/intent-controller.js.map +1 -0
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +2 -0
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.js +4 -0
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +10 -0
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/metadata/catalog.d.ts +81 -4
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +208 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/drafts.d.ts +22 -0
- package/dist/metadata/drafts.d.ts.map +1 -1
- package/dist/metadata/drafts.js +126 -2
- package/dist/metadata/drafts.js.map +1 -1
- package/dist/metadata/grain-gate.d.ts +61 -0
- package/dist/metadata/grain-gate.d.ts.map +1 -0
- package/dist/metadata/grain-gate.js +265 -0
- package/dist/metadata/grain-gate.js.map +1 -0
- package/dist/metadata/identity.d.ts +47 -0
- package/dist/metadata/identity.d.ts.map +1 -0
- package/dist/metadata/identity.js +147 -0
- package/dist/metadata/identity.js.map +1 -0
- package/dist/metadata/metric-match.d.ts +93 -0
- package/dist/metadata/metric-match.d.ts.map +1 -0
- package/dist/metadata/metric-match.js +233 -0
- package/dist/metadata/metric-match.js.map +1 -0
- package/dist/metadata/sql-grounding.d.ts +149 -0
- package/dist/metadata/sql-grounding.d.ts.map +1 -0
- package/dist/metadata/sql-grounding.js +397 -0
- package/dist/metadata/sql-grounding.js.map +1 -0
- package/dist/metadata/sql-retrieval.d.ts +29 -0
- package/dist/metadata/sql-retrieval.d.ts.map +1 -0
- package/dist/metadata/sql-retrieval.js +79 -0
- package/dist/metadata/sql-retrieval.js.map +1 -0
- package/dist/narrate.d.ts +52 -0
- package/dist/narrate.d.ts.map +1 -0
- package/dist/narrate.js +263 -0
- package/dist/narrate.js.map +1 -0
- package/dist/propose/build-from-prompt.d.ts +160 -0
- package/dist/propose/build-from-prompt.d.ts.map +1 -0
- package/dist/propose/build-from-prompt.js +815 -0
- package/dist/propose/build-from-prompt.js.map +1 -0
- package/dist/propose/classify.d.ts +55 -0
- package/dist/propose/classify.d.ts.map +1 -0
- package/dist/propose/classify.js +167 -0
- package/dist/propose/classify.js.map +1 -0
- package/dist/propose/config.d.ts +39 -0
- package/dist/propose/config.d.ts.map +1 -0
- package/dist/propose/config.js +52 -0
- package/dist/propose/config.js.map +1 -0
- package/dist/propose/dbt-artifacts.d.ts +134 -0
- package/dist/propose/dbt-artifacts.d.ts.map +1 -0
- package/dist/propose/dbt-artifacts.js +320 -0
- package/dist/propose/dbt-artifacts.js.map +1 -0
- package/dist/propose/edit-block.d.ts +74 -0
- package/dist/propose/edit-block.d.ts.map +1 -0
- package/dist/propose/edit-block.js +133 -0
- package/dist/propose/edit-block.js.map +1 -0
- package/dist/propose/enrich.d.ts +48 -0
- package/dist/propose/enrich.d.ts.map +1 -0
- package/dist/propose/enrich.js +125 -0
- package/dist/propose/enrich.js.map +1 -0
- package/dist/propose/generate-sql.d.ts +61 -0
- package/dist/propose/generate-sql.d.ts.map +1 -0
- package/dist/propose/generate-sql.js +180 -0
- package/dist/propose/generate-sql.js.map +1 -0
- package/dist/propose/index.d.ts +24 -0
- package/dist/propose/index.d.ts.map +1 -0
- package/dist/propose/index.js +15 -0
- package/dist/propose/index.js.map +1 -0
- package/dist/propose/propose.d.ts +226 -0
- package/dist/propose/propose.d.ts.map +1 -0
- package/dist/propose/propose.js +668 -0
- package/dist/propose/propose.js.map +1 -0
- package/dist/propose/reflect-block.d.ts +99 -0
- package/dist/propose/reflect-block.d.ts.map +1 -0
- package/dist/propose/reflect-block.js +220 -0
- package/dist/propose/reflect-block.js.map +1 -0
- package/dist/propose/write-draft.d.ts +90 -0
- package/dist/propose/write-draft.d.ts.map +1 -0
- package/dist/propose/write-draft.js +185 -0
- package/dist/propose/write-draft.js.map +1 -0
- package/dist/providers/claude.d.ts +13 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +20 -2
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/gemini.d.ts +13 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +24 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/research-loop.d.ts +62 -0
- package/dist/research-loop.d.ts.map +1 -0
- package/dist/research-loop.js +210 -0
- package/dist/research-loop.js.map +1 -0
- package/dist/skills/defaults.d.ts +37 -0
- package/dist/skills/defaults.d.ts.map +1 -0
- package/dist/skills/defaults.js +152 -0
- package/dist/skills/defaults.js.map +1 -0
- package/dist/skills/loader.d.ts +60 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +164 -2
- package/dist/skills/loader.js.map +1 -1
- package/package.json +7 -5
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,43 @@ 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
};
|
|
51
|
+
// Bind the global filter bar to what the certified tiles actually accept (their
|
|
52
|
+
// declared allowedFilters), not prompt words — falling back to prompt inference
|
|
53
|
+
// only when no tile declares any filter. Keeps the dashboard's filters dynamic.
|
|
54
|
+
const promptFilters = inferFilters(prompt);
|
|
55
|
+
const blockFilters = filtersFromCertifiedNodes(certifiedNodes);
|
|
56
|
+
const filters = blockFilters.length > 0 ? mergeAppFilters(blockFilters, promptFilters) : promptFilters;
|
|
57
|
+
const certifiedTiles = certifiedNodes.map((node, index) => tileFromCertifiedNode(node, index, analysisIntent, filters));
|
|
58
|
+
const scopedReports = inferScopedReports(prompt, domain, analysisIntent, certifiedNodes);
|
|
59
|
+
const certifiedContextSummary = summarizeCertifiedContext([
|
|
60
|
+
...certifiedNodes,
|
|
61
|
+
...contextNodes,
|
|
62
|
+
]);
|
|
63
|
+
const missingEvidence = Array.from(new Set([
|
|
64
|
+
...missingEvidenceForPlan(prompt, analysisIntent, certifiedNodes, contextNodes),
|
|
65
|
+
...scopedReports.map((report) => `${report.title}: ${report.description}`),
|
|
66
|
+
]));
|
|
67
|
+
const stakeholderSummary = stakeholderSummaryForPlan({
|
|
68
|
+
prompt,
|
|
69
|
+
appName,
|
|
70
|
+
audience,
|
|
71
|
+
domain,
|
|
72
|
+
analysisIntent,
|
|
73
|
+
certifiedTiles: certifiedTiles.length,
|
|
74
|
+
scopedReports: scopedReports.length,
|
|
75
|
+
certifiedNames: certifiedNodes.map((node) => node.name),
|
|
76
|
+
filterLabels: filters.map((filter) => filter.label),
|
|
77
|
+
});
|
|
99
78
|
return {
|
|
100
79
|
version: 1,
|
|
101
80
|
appId,
|
|
@@ -107,23 +86,22 @@ export function planAppFromPrompt(input) {
|
|
|
107
86
|
analysisIntent,
|
|
108
87
|
audience,
|
|
109
88
|
domain,
|
|
110
|
-
certifiedContext:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
]),
|
|
114
|
-
missingEvidence: missingEvidenceForPlan(prompt, analysisIntent, certifiedNodes, contextNodes),
|
|
89
|
+
certifiedContext: certifiedContextSummary,
|
|
90
|
+
missingEvidence,
|
|
91
|
+
scopedReports,
|
|
115
92
|
displayStrategy: displayStrategyForIntent(analysisIntent),
|
|
116
|
-
layoutRationale: "Business-first layout: certified KPIs, trends, breakdowns, and
|
|
93
|
+
layoutRationale: "Business-first layout: certified KPIs, trends, breakdowns, and proof-backed tiles only; generated gaps stay as scoped analysis memos until promoted.",
|
|
117
94
|
handoffPlan: [
|
|
118
95
|
"Use certified block tiles as the governed dashboard surface.",
|
|
119
|
-
"Keep generated narrative, trust gaps, and drilldown ideas
|
|
120
|
-
"Use app
|
|
96
|
+
"Keep generated narrative, trust gaps, and drilldown ideas as scoped analysis memos until reviewed.",
|
|
97
|
+
"Use app Copilot analysis to run additional SQL, inspect previews, then pin or promote reviewed results into the app.",
|
|
121
98
|
],
|
|
122
99
|
},
|
|
123
100
|
skills: APP_BUILDER_SKILLS,
|
|
124
101
|
domain,
|
|
125
102
|
audience,
|
|
126
103
|
businessGoal: prompt,
|
|
104
|
+
stakeholderSummary,
|
|
127
105
|
owner: input.owner?.trim() || `${process.env.USER ?? "owner"}@local`,
|
|
128
106
|
lifecycle: "draft",
|
|
129
107
|
tags: Array.from(new Set([
|
|
@@ -131,24 +109,56 @@ export function planAppFromPrompt(input) {
|
|
|
131
109
|
...inferTags(prompt, domain),
|
|
132
110
|
`audience:${slugify(audience)}`,
|
|
133
111
|
])),
|
|
112
|
+
appSections: [
|
|
113
|
+
{
|
|
114
|
+
id: "dashboard",
|
|
115
|
+
title: "Stakeholder view",
|
|
116
|
+
purpose: "Clean dashboard pages backed by certified blocks and active filters.",
|
|
117
|
+
reviewStatus: certifiedTiles.length > 0 ? "certified" : "draft_ready",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "research",
|
|
121
|
+
title: "Analysis",
|
|
122
|
+
purpose: "Review-required analyst memos for follow-up questions that need new SQL or deeper analysis.",
|
|
123
|
+
reviewStatus: "draft_ready",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
globalFilters: filters,
|
|
127
|
+
selectedEvidence: certifiedContextSummary.map((item) => ({
|
|
128
|
+
source: `${item.kind}:${item.name}`,
|
|
129
|
+
reason: item.reason,
|
|
130
|
+
kind: item.kind,
|
|
131
|
+
nodeId: item.nodeId,
|
|
132
|
+
trustState: "certified",
|
|
133
|
+
})),
|
|
134
|
+
missingEvidence,
|
|
135
|
+
scopedReports,
|
|
134
136
|
pages: [
|
|
135
137
|
{
|
|
136
138
|
id: "overview",
|
|
137
139
|
title: inferDashboardTitle(prompt, domain, appName),
|
|
138
|
-
description: `Certified app surface for ${audience}. Draft gaps stay in
|
|
140
|
+
description: `Certified app surface for ${audience}. Draft gaps stay in reports until reviewed.`,
|
|
139
141
|
filters,
|
|
140
|
-
tiles:
|
|
142
|
+
tiles: certifiedTiles,
|
|
141
143
|
},
|
|
142
144
|
],
|
|
143
145
|
caveats: [
|
|
144
146
|
"Generated app plans are local draft artifacts until reviewed.",
|
|
145
|
-
"Generated dashboards render certified block tiles only; draft and
|
|
147
|
+
"Generated dashboards render certified block tiles only; draft narrative, proof, and analysis suggestions stay as scoped memos, Copilot answers, or promoted reviewed insights.",
|
|
146
148
|
],
|
|
147
149
|
reviewTasks: [
|
|
148
|
-
"Review every
|
|
150
|
+
"Review every scoped analysis memo before stakeholder use.",
|
|
151
|
+
...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
152
|
"Run dql app build after accepting the generated files.",
|
|
150
|
-
"Use app
|
|
153
|
+
"Use app Copilot analysis for additional questions, then promote reviewed SQL results to draft or certified blocks.",
|
|
151
154
|
],
|
|
155
|
+
coverage: {
|
|
156
|
+
certifiedTiles: certifiedTiles.length,
|
|
157
|
+
gaps: scopedReports.length,
|
|
158
|
+
ratio: certifiedTiles.length + scopedReports.length > 0
|
|
159
|
+
? certifiedTiles.length / (certifiedTiles.length + scopedReports.length)
|
|
160
|
+
: 0,
|
|
161
|
+
},
|
|
152
162
|
};
|
|
153
163
|
}
|
|
154
164
|
export function validateAppPlan(plan, kg) {
|
|
@@ -199,6 +209,7 @@ export function validateAppPlan(plan, kg) {
|
|
|
199
209
|
}
|
|
200
210
|
else {
|
|
201
211
|
draftTiles += 1;
|
|
212
|
+
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
213
|
if (tile.certification === "certified") {
|
|
203
214
|
issues.push(error(path, "non-block generated tiles cannot be marked certified"));
|
|
204
215
|
}
|
|
@@ -209,7 +220,7 @@ export function validateAppPlan(plan, kg) {
|
|
|
209
220
|
}
|
|
210
221
|
}
|
|
211
222
|
if (certifiedTiles === 0) {
|
|
212
|
-
issues.push(warn("pages", "no certified blocks matched; generated
|
|
223
|
+
issues.push(warn("pages", "no certified blocks matched; generated dashboard has no governed result tiles yet"));
|
|
213
224
|
}
|
|
214
225
|
return {
|
|
215
226
|
ok: issues.every((issue) => issue.level !== "error"),
|
|
@@ -315,6 +326,7 @@ export function generateAppFromPlan(projectRoot, plan, kg, options = {}) {
|
|
|
315
326
|
id: filter.id,
|
|
316
327
|
type: filter.type,
|
|
317
328
|
default: filter.default,
|
|
329
|
+
...(filter.options?.length ? { options: filter.options } : {}),
|
|
318
330
|
bindsTo: filter.bindsTo,
|
|
319
331
|
})),
|
|
320
332
|
layout: {
|
|
@@ -407,6 +419,133 @@ function mergeCertifiedBlockNodes(preferredNodes, matchedNodes) {
|
|
|
407
419
|
}
|
|
408
420
|
return merged;
|
|
409
421
|
}
|
|
422
|
+
function dedupeCertifiedBlockNodes(nodes, preferredNodeIds = new Set()) {
|
|
423
|
+
const selected = [];
|
|
424
|
+
const signatureIndex = new Map();
|
|
425
|
+
for (const node of nodes) {
|
|
426
|
+
const signatures = certifiedBlockDuplicateSignatures(node);
|
|
427
|
+
let duplicateIndex = -1;
|
|
428
|
+
for (const signature of signatures) {
|
|
429
|
+
const existing = signatureIndex.get(signature);
|
|
430
|
+
if (existing !== undefined) {
|
|
431
|
+
duplicateIndex = existing;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (duplicateIndex < 0) {
|
|
436
|
+
signatureIndex.set(node.nodeId, selected.length);
|
|
437
|
+
for (const signature of signatures)
|
|
438
|
+
signatureIndex.set(signature, selected.length);
|
|
439
|
+
selected.push(node);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const current = selected[duplicateIndex];
|
|
443
|
+
if (certifiedBlockSelectionScore(node, preferredNodeIds) > certifiedBlockSelectionScore(current, preferredNodeIds)) {
|
|
444
|
+
selected[duplicateIndex] = node;
|
|
445
|
+
signatureIndex.set(node.nodeId, duplicateIndex);
|
|
446
|
+
for (const signature of signatures)
|
|
447
|
+
signatureIndex.set(signature, duplicateIndex);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return selected;
|
|
451
|
+
}
|
|
452
|
+
function certifiedBlockDuplicateSignatures(node) {
|
|
453
|
+
const signatures = new Set([node.nodeId]);
|
|
454
|
+
if (node.sqlFingerprints?.exact)
|
|
455
|
+
signatures.add(`sql:${node.sqlFingerprints.exact}`);
|
|
456
|
+
if (node.sqlFingerprints?.parameterized)
|
|
457
|
+
signatures.add(`sqlp:${node.sqlFingerprints.parameterized}`);
|
|
458
|
+
if (node.businessFingerprint?.hash)
|
|
459
|
+
signatures.add(`business:${node.businessFingerprint.hash}`);
|
|
460
|
+
const topic = certifiedBlockTopicSignature(node);
|
|
461
|
+
if (topic)
|
|
462
|
+
signatures.add(`topic:${topic}`);
|
|
463
|
+
return Array.from(signatures);
|
|
464
|
+
}
|
|
465
|
+
function certifiedBlockTopicSignature(node) {
|
|
466
|
+
const text = [
|
|
467
|
+
node.name,
|
|
468
|
+
node.description,
|
|
469
|
+
node.llmContext,
|
|
470
|
+
node.businessOutcome,
|
|
471
|
+
node.decisionUse,
|
|
472
|
+
node.grain,
|
|
473
|
+
...(node.tags ?? []),
|
|
474
|
+
...(node.entities ?? []),
|
|
475
|
+
...(node.declaredOutputs ?? []),
|
|
476
|
+
...(node.dimensions ?? []),
|
|
477
|
+
...(node.allowedFilters ?? []),
|
|
478
|
+
...(node.businessFingerprint?.tokens ?? []),
|
|
479
|
+
].join(" ").toLowerCase();
|
|
480
|
+
const sources = normalizedTopicTokens([
|
|
481
|
+
...(node.sourceSystems ?? []),
|
|
482
|
+
...(node.businessFingerprint?.tokens ?? [])
|
|
483
|
+
.filter((token) => /^(source|system):/i.test(token))
|
|
484
|
+
.map((token) => token.replace(/^(source|system):/i, "")),
|
|
485
|
+
...Array.from(text.matchAll(/\b(?:from|join)\s+([a-z0-9_.]+)/gi)).map((match) => match[1]),
|
|
486
|
+
...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]),
|
|
487
|
+
]);
|
|
488
|
+
const entity = firstTopicFamily(text, [
|
|
489
|
+
["player", /\b(player|players|scorer|scorers|athlete)\b/],
|
|
490
|
+
["customer", /\b(customer|account|user|subscriber)\b/],
|
|
491
|
+
["order", /\b(order|orders|purchase)\b/],
|
|
492
|
+
["product", /\b(product|sku|item)\b/],
|
|
493
|
+
["team", /\b(team|teams)\b/],
|
|
494
|
+
]);
|
|
495
|
+
const intent = firstTopicFamily(text, [
|
|
496
|
+
["availability", /\b(availability|freshness|record count|records|quality|coverage)\b/],
|
|
497
|
+
["ranking", /\b(top|bottom|rank|ranking|leader|leaderboard|scorer|scorers)\b/],
|
|
498
|
+
["trend", /\b(trend|weekly|monthly|daily|time series|over time)\b/],
|
|
499
|
+
["segment", /\b(segment|breakdown|split|by segment|cohort|region|channel)\b/],
|
|
500
|
+
["profile", /\b(profile|360|detail|drilldown)\b/],
|
|
501
|
+
["kpi", /\b(kpi|metric|total|summary|scorecard)\b/],
|
|
502
|
+
]);
|
|
503
|
+
const metric = firstTopicFamily(text, [
|
|
504
|
+
["points", /\b(point|points|pts|score|scoring|scorer|scorers)\b/],
|
|
505
|
+
["revenue", /\b(revenue|arr|sales|amount|bookings)\b/],
|
|
506
|
+
["count", /\b(count|records|games played|orders|users)\b/],
|
|
507
|
+
["quality", /\b(availability|freshness|quality|coverage)\b/],
|
|
508
|
+
["conversion", /\b(conversion|rate|pct|percent|ratio)\b/],
|
|
509
|
+
]);
|
|
510
|
+
if (!sources.length || !intent || !metric)
|
|
511
|
+
return "";
|
|
512
|
+
return [sources.slice(0, 3).join("+"), entity, intent, metric].filter(Boolean).join("|");
|
|
513
|
+
}
|
|
514
|
+
function normalizedTopicTokens(values) {
|
|
515
|
+
return Array.from(new Set(values
|
|
516
|
+
.map((value) => value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, ""))
|
|
517
|
+
.map((value) => value
|
|
518
|
+
.replace(/^.*\./, "")
|
|
519
|
+
.replace(/^transformed_/, "")
|
|
520
|
+
.replace(/^nba_analytics_/, ""))
|
|
521
|
+
.filter(Boolean)))
|
|
522
|
+
.sort();
|
|
523
|
+
}
|
|
524
|
+
function firstTopicFamily(text, patterns) {
|
|
525
|
+
return patterns.find(([, pattern]) => pattern.test(text))?.[0] ?? "";
|
|
526
|
+
}
|
|
527
|
+
function certifiedBlockSelectionScore(node, preferredNodeIds) {
|
|
528
|
+
let score = preferredNodeIds.has(node.nodeId) ? 10000 : 0;
|
|
529
|
+
score += (node.declaredOutputs?.length ?? 0) * 70;
|
|
530
|
+
score += (node.allowedFilters?.length ?? 0) * 45;
|
|
531
|
+
score += (node.parameterPolicy?.length ?? 0) * 45;
|
|
532
|
+
score += (node.entities?.length ?? 0) * 35;
|
|
533
|
+
score += (node.businessFingerprint?.tokens?.length ?? 0) * 8;
|
|
534
|
+
if (node.grain)
|
|
535
|
+
score += 90;
|
|
536
|
+
if (node.pattern)
|
|
537
|
+
score += 60;
|
|
538
|
+
if (node.sqlFingerprints?.parameterized)
|
|
539
|
+
score += 40;
|
|
540
|
+
if (node.description && node.description.length > 80)
|
|
541
|
+
score += 25;
|
|
542
|
+
const text = [node.name, node.description, node.llmContext, ...(node.tags ?? [])].join(" ").toLowerCase();
|
|
543
|
+
if (/\b(raw-sql|imported|codex-e2e|test)\b/.test(text))
|
|
544
|
+
score -= 80;
|
|
545
|
+
if (/\breview required\b/.test(text))
|
|
546
|
+
score -= 10;
|
|
547
|
+
return score;
|
|
548
|
+
}
|
|
410
549
|
function findCertifiedContextNodes(kg, prompt, domain, limit) {
|
|
411
550
|
const kinds = [
|
|
412
551
|
"business_view",
|
|
@@ -458,13 +597,22 @@ function normalizeGoal(prompt) {
|
|
|
458
597
|
.trim()
|
|
459
598
|
.replace(/\.$/, "");
|
|
460
599
|
}
|
|
461
|
-
function
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
`
|
|
466
|
-
"
|
|
467
|
-
|
|
600
|
+
function stakeholderSummaryForPlan(input) {
|
|
601
|
+
// Ground the narrative in the actual certified blocks + filter bar — the assets the
|
|
602
|
+
// app is built on — not just the prompt echo.
|
|
603
|
+
const built = input.certifiedNames && input.certifiedNames.length > 0
|
|
604
|
+
? ` Built on ${input.certifiedNames.slice(0, 3).join(", ")}${input.certifiedNames.length > 3 ? ", …" : ""}.`
|
|
605
|
+
: "";
|
|
606
|
+
const coverage = input.certifiedTiles > 0
|
|
607
|
+
? `${input.certifiedTiles} certified tile${input.certifiedTiles === 1 ? "" : "s"} anchor the app.`
|
|
608
|
+
: "No certified tiles matched strongly enough yet.";
|
|
609
|
+
const filterLine = input.filterLabels && input.filterLabels.length > 0
|
|
610
|
+
? ` One filter set — ${input.filterLabels.slice(0, 4).join(", ")} — refreshes every tile that shares it.`
|
|
611
|
+
: "";
|
|
612
|
+
const review = input.scopedReports > 0
|
|
613
|
+
? `${input.scopedReports} scoped analysis memo${input.scopedReports === 1 ? "" : "s"} capture questions that need deeper review.`
|
|
614
|
+
: "No scoped analysis memos were needed.";
|
|
615
|
+
return `${input.appName} is a ${titleCase(input.domain)} decision room for ${input.audience}.${built} ${coverage}${filterLine} ${review} Intent: ${titleCase(input.analysisIntent.replace(/_/g, " "))}.`;
|
|
468
616
|
}
|
|
469
617
|
function summarizeCertifiedContext(nodes) {
|
|
470
618
|
const seen = new Set();
|
|
@@ -490,13 +638,13 @@ function missingEvidenceForPlan(prompt, intent, certifiedNodes, contextNodes) {
|
|
|
490
638
|
const lower = prompt.toLowerCase();
|
|
491
639
|
const missing = new Set();
|
|
492
640
|
if (certifiedNodes.length === 0) {
|
|
493
|
-
missing.add("No certified block matched strongly enough for the primary dashboard
|
|
641
|
+
missing.add("No certified block matched strongly enough for the primary dashboard proof.");
|
|
494
642
|
}
|
|
495
643
|
if (contextNodes.length === 0) {
|
|
496
|
-
missing.add("No certified business view, term, semantic object, or dbt context was matched for
|
|
644
|
+
missing.add("No certified business view, term, semantic object, or dbt context was matched for supporting explanation.");
|
|
497
645
|
}
|
|
498
646
|
if (intent === "driver_analysis" || /\bwhy|driver|break\s*down/.test(lower)) {
|
|
499
|
-
missing.add("Driver analysis should be opened as review-required
|
|
647
|
+
missing.add("Driver analysis should be opened as a review-required report until a certified drilldown block exists.");
|
|
500
648
|
}
|
|
501
649
|
if (intent === "trust_review" || /\btrust|rely|lineage/.test(lower)) {
|
|
502
650
|
missing.add("Leadership trust checks need lineage, owner, caveats, and review cadence confirmation.");
|
|
@@ -509,19 +657,19 @@ function missingEvidenceForPlan(prompt, intent, certifiedNodes, contextNodes) {
|
|
|
509
657
|
function displayStrategyForIntent(intent) {
|
|
510
658
|
switch (intent) {
|
|
511
659
|
case "driver_analysis":
|
|
512
|
-
return "Lead with certified summary
|
|
660
|
+
return "Lead with certified summary proof, then route root-cause questions into Copilot reports.";
|
|
513
661
|
case "entity_drilldown":
|
|
514
662
|
return "Lead with entity context and certified performance blocks, then provide drilldown and trust review paths.";
|
|
515
663
|
case "trust_review":
|
|
516
664
|
return "Lead with certification, lineage, caveats, and review tasks before any generated interpretation.";
|
|
517
665
|
case "data_quality":
|
|
518
|
-
return "Lead with availability
|
|
666
|
+
return "Lead with availability and freshness proof, then list gaps and review actions.";
|
|
519
667
|
case "experiment_readout":
|
|
520
668
|
return "Lead with KPI impact, guardrails, segment readout, and decision caveats.";
|
|
521
669
|
case "metric_monitoring":
|
|
522
670
|
return "Lead with KPIs, trends, and breakdowns for recurring operating review.";
|
|
523
671
|
default:
|
|
524
|
-
return "Lead with the business brief, certified metrics, supporting breakdowns, then
|
|
672
|
+
return "Lead with the business brief, certified metrics, supporting breakdowns, then proof and report paths.";
|
|
525
673
|
}
|
|
526
674
|
}
|
|
527
675
|
function displayForCertifiedNode(node, viz, index, intent) {
|
|
@@ -541,7 +689,7 @@ function displayForCertifiedNode(node, viz, index, intent) {
|
|
|
541
689
|
? "evidence"
|
|
542
690
|
: /\btop\s*\d+|rank|ranking|leader|leaderboard|scorer|score|goal|segment|breakdown|split|driver|player|customer|account|region|channel\b/.test(text)
|
|
543
691
|
? "breakdown"
|
|
544
|
-
: "
|
|
692
|
+
: "breakdown";
|
|
545
693
|
const followUpActions = [
|
|
546
694
|
"ask_follow_up",
|
|
547
695
|
"open_research",
|
|
@@ -585,7 +733,7 @@ function displayForDraftTile(title, viz, index) {
|
|
|
585
733
|
"review_trust",
|
|
586
734
|
"create_draft_block",
|
|
587
735
|
];
|
|
588
|
-
const rationale = "Generated section captures missing
|
|
736
|
+
const rationale = "Generated section captures missing proof or review work without implying certification.";
|
|
589
737
|
return {
|
|
590
738
|
role,
|
|
591
739
|
recommendedDisplayType: viz,
|
|
@@ -709,9 +857,9 @@ function insightTitleForPresentation(title, role, component) {
|
|
|
709
857
|
if (component === "BusinessBrief")
|
|
710
858
|
return "Business context";
|
|
711
859
|
if (component === "TrustCallout")
|
|
712
|
-
return "
|
|
860
|
+
return "Proof and lineage";
|
|
713
861
|
if (component === "ResearchActions")
|
|
714
|
-
return "Follow-up
|
|
862
|
+
return "Follow-up analysis";
|
|
715
863
|
if (role === "kpi")
|
|
716
864
|
return title;
|
|
717
865
|
if (component === "RankingPanel")
|
|
@@ -735,9 +883,11 @@ function inferExpectedGrain(text, intent) {
|
|
|
735
883
|
return "metric";
|
|
736
884
|
return "dashboard";
|
|
737
885
|
}
|
|
738
|
-
function tileFromCertifiedNode(node, index, intent) {
|
|
886
|
+
function tileFromCertifiedNode(node, index, intent, globalFilters = []) {
|
|
739
887
|
const blockId = node.name;
|
|
740
888
|
const viz = inferVizForNode(node, index);
|
|
889
|
+
const filterBindings = filterBindingsForCertifiedNode(node, globalFilters);
|
|
890
|
+
const parameterBindings = parameterBindingsForCertifiedNode(node, filterBindings);
|
|
741
891
|
return {
|
|
742
892
|
id: slugify(blockId) || `certified-${index + 1}`,
|
|
743
893
|
title: node.name,
|
|
@@ -748,6 +898,20 @@ function tileFromCertifiedNode(node, index, intent) {
|
|
|
748
898
|
viz,
|
|
749
899
|
certification: "certified",
|
|
750
900
|
reviewStatus: "certified",
|
|
901
|
+
trustState: "certified",
|
|
902
|
+
...(filterBindings.length ? { filterBindings } : {}),
|
|
903
|
+
...(parameterBindings.length ? { parameterBindings } : {}),
|
|
904
|
+
sourceEvidence: [{
|
|
905
|
+
source: node.nodeId,
|
|
906
|
+
reason: node.decisionUse ??
|
|
907
|
+
node.businessOutcome ??
|
|
908
|
+
node.description ??
|
|
909
|
+
"Certified DQL block matched the app prompt.",
|
|
910
|
+
kind: node.kind,
|
|
911
|
+
nodeId: node.nodeId,
|
|
912
|
+
path: node.sourcePath,
|
|
913
|
+
trustState: "certified",
|
|
914
|
+
}],
|
|
751
915
|
display: displayForCertifiedNode(node, viz, index, intent),
|
|
752
916
|
rationale: node.decisionUse ??
|
|
753
917
|
node.businessOutcome ??
|
|
@@ -758,11 +922,108 @@ function tileFromCertifiedNode(node, index, intent) {
|
|
|
758
922
|
],
|
|
759
923
|
};
|
|
760
924
|
}
|
|
925
|
+
function filterBindingsForCertifiedNode(node, globalFilters) {
|
|
926
|
+
const bindings = (node.filterBindings ?? []).map((entry) => ({
|
|
927
|
+
filter: entry.filter,
|
|
928
|
+
binding: entry.binding,
|
|
929
|
+
mode: "predicate",
|
|
930
|
+
}));
|
|
931
|
+
const dynamicParams = new Set((node.parameterPolicy ?? [])
|
|
932
|
+
.filter((entry) => entry.policy === "dynamic")
|
|
933
|
+
.map((entry) => entry.name));
|
|
934
|
+
const allowedFilters = new Set((node.allowedFilters ?? []).map((entry) => entry.toLowerCase()));
|
|
935
|
+
for (const filter of globalFilters) {
|
|
936
|
+
if (bindings.some((entry) => entry.filter === filter.id))
|
|
937
|
+
continue;
|
|
938
|
+
if (dynamicParams.has(filter.id)) {
|
|
939
|
+
bindings.push({
|
|
940
|
+
filter: filter.id,
|
|
941
|
+
binding: filter.bindsTo ?? filter.id,
|
|
942
|
+
mode: "parameter",
|
|
943
|
+
paramNames: [filter.id],
|
|
944
|
+
});
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const matchingParam = parameterForFilter(filter, dynamicParams);
|
|
948
|
+
if (matchingParam) {
|
|
949
|
+
bindings.push({
|
|
950
|
+
filter: filter.id,
|
|
951
|
+
binding: filter.bindsTo ?? matchingParam,
|
|
952
|
+
mode: "parameter",
|
|
953
|
+
paramNames: [matchingParam],
|
|
954
|
+
});
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (filter.bindsTo && allowedFilters.has(filter.bindsTo.toLowerCase())) {
|
|
958
|
+
bindings.push({
|
|
959
|
+
filter: filter.id,
|
|
960
|
+
binding: filter.bindsTo,
|
|
961
|
+
mode: "predicate",
|
|
962
|
+
});
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (filter.id && allowedFilters.has(filter.id.toLowerCase())) {
|
|
966
|
+
bindings.push({
|
|
967
|
+
filter: filter.id,
|
|
968
|
+
binding: filter.bindsTo ?? filter.id,
|
|
969
|
+
mode: "predicate",
|
|
970
|
+
});
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (filter.id) {
|
|
974
|
+
bindings.push({
|
|
975
|
+
filter: filter.id,
|
|
976
|
+
binding: filter.bindsTo,
|
|
977
|
+
unsupportedReason: "No matching certified block parameter or allowed filter was declared.",
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return uniqueTileFilterBindings(bindings);
|
|
982
|
+
}
|
|
983
|
+
function parameterBindingsForCertifiedNode(node, filterBindings) {
|
|
984
|
+
const dynamicParams = (node.parameterPolicy ?? [])
|
|
985
|
+
.filter((entry) => entry.policy === "dynamic")
|
|
986
|
+
.map((entry) => entry.name);
|
|
987
|
+
return dynamicParams.map((param) => {
|
|
988
|
+
const filterBinding = filterBindings.find((entry) => entry.paramNames?.includes(param) || entry.filter === param);
|
|
989
|
+
return {
|
|
990
|
+
param,
|
|
991
|
+
source: filterBinding ? "dashboard_filter" : "variable",
|
|
992
|
+
...(filterBinding?.filter ? { filter: filterBinding.filter } : {}),
|
|
993
|
+
field: filterBinding?.binding ?? param,
|
|
994
|
+
};
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function parameterForFilter(filter, dynamicParams) {
|
|
998
|
+
const candidates = [];
|
|
999
|
+
if (filter.id === "season")
|
|
1000
|
+
candidates.push("season", "season_year", "year");
|
|
1001
|
+
if (filter.id === "season_start")
|
|
1002
|
+
candidates.push("season_start", "start_year", "from_year");
|
|
1003
|
+
if (filter.id === "season_end")
|
|
1004
|
+
candidates.push("season_end", "end_year", "to_year");
|
|
1005
|
+
if (filter.id === "top_n")
|
|
1006
|
+
candidates.push("top_n", "limit", "n");
|
|
1007
|
+
candidates.push(filter.id);
|
|
1008
|
+
return candidates.find((candidate) => dynamicParams.has(candidate)) ?? null;
|
|
1009
|
+
}
|
|
1010
|
+
function uniqueTileFilterBindings(bindings) {
|
|
1011
|
+
const seen = new Set();
|
|
1012
|
+
const out = [];
|
|
1013
|
+
for (const binding of bindings) {
|
|
1014
|
+
const key = `${binding.filter}:${binding.binding ?? ""}:${binding.mode ?? ""}:${(binding.paramNames ?? []).join(",")}:${binding.unsupportedReason ?? ""}`;
|
|
1015
|
+
if (seen.has(key))
|
|
1016
|
+
continue;
|
|
1017
|
+
seen.add(key);
|
|
1018
|
+
out.push(binding);
|
|
1019
|
+
}
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
761
1022
|
function buildLayoutItems(tiles) {
|
|
762
1023
|
let x = 0;
|
|
763
1024
|
let y = 0;
|
|
764
1025
|
let rowH = 0;
|
|
765
|
-
const orderedTiles = [...tiles].sort((a, b) => {
|
|
1026
|
+
const orderedTiles = [...tiles].filter(isDashboardTile).sort((a, b) => {
|
|
766
1027
|
const priorityA = a.display?.layoutPriority ?? 50;
|
|
767
1028
|
const priorityB = b.display?.layoutPriority ?? 50;
|
|
768
1029
|
return priorityA - priorityB;
|
|
@@ -799,6 +1060,11 @@ function buildLayoutItems(tiles) {
|
|
|
799
1060
|
},
|
|
800
1061
|
},
|
|
801
1062
|
display: displayMetadataForTile(tile, genUi),
|
|
1063
|
+
...(tile.filterBindings?.length ? { filterBindings: tile.filterBindings } : {}),
|
|
1064
|
+
...(tile.parameterBindings?.length ? { parameterBindings: tile.parameterBindings } : {}),
|
|
1065
|
+
...(tile.sourceEvidence?.length ? { sourceEvidence: tile.sourceEvidence } : {}),
|
|
1066
|
+
trustState: tile.trustState ?? tile.display?.trustState ?? (tile.certification === "certified" ? "certified" : "draft_ready"),
|
|
1067
|
+
reviewStatus: tile.reviewStatus,
|
|
802
1068
|
};
|
|
803
1069
|
if (isDashboardTile(tile)) {
|
|
804
1070
|
item.block = { blockId: tile.blockId };
|
|
@@ -835,9 +1101,25 @@ function isDashboardTile(tile) {
|
|
|
835
1101
|
return tile.kind === "certified_block" && tile.certification === "certified" && Boolean(tile.blockId);
|
|
836
1102
|
}
|
|
837
1103
|
function appPlanReadme(plan, validation) {
|
|
1104
|
+
const reviewDashboardItems = plan.pages.flatMap((page) => page.tiles
|
|
1105
|
+
.filter((tile) => !isDashboardTile(tile))
|
|
1106
|
+
.map((tile) => `- ${tile.title}: ${tile.description ?? tile.rationale ?? "Review before adding to the app."}`));
|
|
1107
|
+
const scopedReports = plan.scopedReports.length > 0
|
|
1108
|
+
? plan.scopedReports.map((report) => [
|
|
1109
|
+
`- ${report.title}: ${report.description}`,
|
|
1110
|
+
` - Question: ${report.question}`,
|
|
1111
|
+
` - Evidence needed: ${report.evidenceNeeded.join(", ")}`,
|
|
1112
|
+
].join("\n"))
|
|
1113
|
+
: reviewDashboardItems.length > 0
|
|
1114
|
+
? reviewDashboardItems
|
|
1115
|
+
: plan.missingEvidence.length > 0
|
|
1116
|
+
? plan.missingEvidence.map((item) => `- ${item}`)
|
|
1117
|
+
: ["- No scoped analysis memos were suggested. Use app Copilot for follow-up questions."];
|
|
838
1118
|
return [
|
|
839
1119
|
`# ${plan.name}`,
|
|
840
1120
|
"",
|
|
1121
|
+
plan.stakeholderSummary,
|
|
1122
|
+
"",
|
|
841
1123
|
plan.businessGoal,
|
|
842
1124
|
"",
|
|
843
1125
|
`- Generated from prompt: ${plan.prompt}`,
|
|
@@ -846,8 +1128,10 @@ function appPlanReadme(plan, validation) {
|
|
|
846
1128
|
`- Domain: ${plan.domain}`,
|
|
847
1129
|
`- Audience: ${plan.audience}`,
|
|
848
1130
|
`- Lifecycle: ${plan.lifecycle}`,
|
|
849
|
-
`- Certified tiles: ${validation.certifiedTiles}`,
|
|
850
|
-
`- Review
|
|
1131
|
+
`- Certified dashboard tiles: ${validation.certifiedTiles}`,
|
|
1132
|
+
`- Review-required dashboard tiles: ${validation.draftTiles}`,
|
|
1133
|
+
`- Scoped analysis memos: ${plan.scopedReports.length}`,
|
|
1134
|
+
`- Global filters: ${plan.globalFilters.map((filter) => filter.id).join(", ") || "none"}`,
|
|
851
1135
|
"",
|
|
852
1136
|
"## Planner brief",
|
|
853
1137
|
"",
|
|
@@ -861,17 +1145,15 @@ function appPlanReadme(plan, validation) {
|
|
|
861
1145
|
? plan.planning.certifiedContext.map((item) => `- ${item.kind}:${item.name} — ${item.reason}`)
|
|
862
1146
|
: ["- No certified context matched strongly enough."]),
|
|
863
1147
|
"",
|
|
864
|
-
"## Missing
|
|
1148
|
+
"## Missing proof",
|
|
865
1149
|
"",
|
|
866
1150
|
...(plan.planning.missingEvidence.length
|
|
867
1151
|
? plan.planning.missingEvidence.map((item) => `- ${item}`)
|
|
868
|
-
: ["- No missing
|
|
1152
|
+
: ["- No missing proof was identified by the deterministic planner."]),
|
|
869
1153
|
"",
|
|
870
|
-
"##
|
|
1154
|
+
"## Scoped analysis memos",
|
|
871
1155
|
"",
|
|
872
|
-
...
|
|
873
|
-
.filter((tile) => !isDashboardTile(tile))
|
|
874
|
-
.map((tile) => `- ${tile.title}: ${tile.description ?? tile.rationale ?? "Review before adding to the app."}`)),
|
|
1156
|
+
...scopedReports,
|
|
875
1157
|
"",
|
|
876
1158
|
"## Agent skills applied",
|
|
877
1159
|
"",
|
|
@@ -942,59 +1224,83 @@ function inferTags(prompt, domain) {
|
|
|
942
1224
|
tags.add("monthly-review");
|
|
943
1225
|
return Array.from(tags).filter(Boolean);
|
|
944
1226
|
}
|
|
945
|
-
function
|
|
1227
|
+
function inferScopedReports(prompt, domain, intent, certifiedNodes) {
|
|
946
1228
|
const lower = prompt.toLowerCase();
|
|
947
1229
|
const domainLabel = titleCase(domain);
|
|
948
|
-
const
|
|
1230
|
+
const reports = [
|
|
949
1231
|
{
|
|
950
|
-
title: `${domainLabel} decision story`,
|
|
951
|
-
|
|
952
|
-
|
|
1232
|
+
title: `${domainLabel} decision story report`,
|
|
1233
|
+
question: `What is the decision story behind "${normalizeGoal(prompt)}"?`,
|
|
1234
|
+
description: "Analyst narrative that ties certified results to the business decision, caveats, and recommended next action.",
|
|
1235
|
+
intent,
|
|
1236
|
+
evidenceNeeded: ["certified block results", "active filters", "lineage and owner context"],
|
|
953
1237
|
},
|
|
954
1238
|
];
|
|
955
1239
|
if (intent === "driver_analysis" || /\bwhy|driver|break\s*down|root cause\b/.test(lower)) {
|
|
956
|
-
|
|
957
|
-
title: "
|
|
958
|
-
|
|
959
|
-
|
|
1240
|
+
reports.push({
|
|
1241
|
+
title: "Driver analysis report",
|
|
1242
|
+
question: "Which drivers, segments, or entities explain the movement?",
|
|
1243
|
+
description: "Review-required report for drivers, exceptions, segment comparison, and entity drilldown.",
|
|
1244
|
+
intent: "driver_analysis",
|
|
1245
|
+
evidenceNeeded: ["comparison period", "segment fields", "metric definition", "previewed SQL"],
|
|
960
1246
|
});
|
|
961
1247
|
}
|
|
962
1248
|
else if (/\brisk|caveat|issue|anomal|quality\b/.test(lower)) {
|
|
963
|
-
|
|
964
|
-
title: "
|
|
965
|
-
|
|
966
|
-
|
|
1249
|
+
reports.push({
|
|
1250
|
+
title: "Risk and caveat report",
|
|
1251
|
+
question: "Which caveats or quality risks should stakeholders understand before using this app?",
|
|
1252
|
+
description: "Review checklist for risks that need certified evidence before stakeholder use.",
|
|
1253
|
+
intent: "data_quality",
|
|
1254
|
+
evidenceNeeded: ["freshness checks", "row counts", "known caveats", "owner review"],
|
|
967
1255
|
});
|
|
968
1256
|
}
|
|
969
1257
|
else if (/\bsegment|cohort|region|location|product|channel|customer\b/.test(lower)) {
|
|
970
|
-
|
|
971
|
-
title: "
|
|
1258
|
+
reports.push({
|
|
1259
|
+
title: "Drilldown certification report",
|
|
1260
|
+
question: "Which slices or drilldowns should become reusable certified blocks?",
|
|
972
1261
|
description: "Candidate slices and drilldowns the agent could not fully back with certified blocks yet.",
|
|
973
|
-
|
|
1262
|
+
intent: "entity_drilldown",
|
|
1263
|
+
evidenceNeeded: ["slice dimensions", "grain", "allowed filters", "previewed SQL"],
|
|
974
1264
|
});
|
|
975
1265
|
}
|
|
976
1266
|
else {
|
|
977
|
-
|
|
978
|
-
title: "
|
|
979
|
-
|
|
980
|
-
|
|
1267
|
+
reports.push({
|
|
1268
|
+
title: "Proof and lineage gaps",
|
|
1269
|
+
question: "What proof is still missing before this app can be treated as governed?",
|
|
1270
|
+
description: "Open proof, lineage, and review tasks to complete before this app is governed.",
|
|
1271
|
+
intent: "proof_review",
|
|
1272
|
+
evidenceNeeded: ["lineage", "certification state", "tests", "review cadence"],
|
|
981
1273
|
});
|
|
982
1274
|
}
|
|
983
1275
|
if (certifiedNodes.length === 0) {
|
|
984
|
-
|
|
1276
|
+
reports.push({
|
|
985
1277
|
title: "Certified block search",
|
|
1278
|
+
question: "Which existing or new DQL blocks should support this app?",
|
|
986
1279
|
description: "No certified blocks matched strongly enough; review suggested sources and create certified blocks.",
|
|
987
|
-
|
|
1280
|
+
intent: "proof_review",
|
|
1281
|
+
evidenceNeeded: ["candidate dbt models", "semantic metrics", "business terms", "draft block plan"],
|
|
988
1282
|
});
|
|
989
1283
|
}
|
|
990
|
-
if (!
|
|
991
|
-
|
|
992
|
-
title: "
|
|
1284
|
+
if (!reports.some((report) => /\btrust|evidence|risk|caveat|gap|proof/.test(report.title.toLowerCase()))) {
|
|
1285
|
+
reports.push({
|
|
1286
|
+
title: "Proof and lineage gaps",
|
|
1287
|
+
question: "What lineage and certification proof should be reviewed?",
|
|
993
1288
|
description: "Certification, lineage, caveats, and review actions that must be confirmed before stakeholder use.",
|
|
994
|
-
|
|
1289
|
+
intent: "proof_review",
|
|
1290
|
+
evidenceNeeded: ["lineage", "certification state", "owner", "tests"],
|
|
995
1291
|
});
|
|
996
1292
|
}
|
|
997
|
-
return
|
|
1293
|
+
return reports.map((report, index) => ({
|
|
1294
|
+
id: slugify(report.title) || `scoped-report-${index + 1}`,
|
|
1295
|
+
title: report.title,
|
|
1296
|
+
question: report.question,
|
|
1297
|
+
description: report.description,
|
|
1298
|
+
intent: report.intent,
|
|
1299
|
+
reviewStatus: "draft_ready",
|
|
1300
|
+
source: "app_builder",
|
|
1301
|
+
evidenceNeeded: report.evidenceNeeded,
|
|
1302
|
+
suggestedActions: ["open_research", "review_trust", "create_draft_block"],
|
|
1303
|
+
}));
|
|
998
1304
|
}
|
|
999
1305
|
function inferDomain(prompt) {
|
|
1000
1306
|
const lower = prompt.toLowerCase();
|
|
@@ -1026,6 +1332,12 @@ function inferAudience(prompt) {
|
|
|
1026
1332
|
function inferFilters(prompt) {
|
|
1027
1333
|
const filters = [];
|
|
1028
1334
|
const lower = prompt.toLowerCase();
|
|
1335
|
+
const wantsSeasonStart = /\bseason[_\s-]*start\b/.test(lower);
|
|
1336
|
+
const wantsSeasonEnd = /\bseason[_\s-]*end\b/.test(lower);
|
|
1337
|
+
const wantsTopN = /\btop[_\s-]*n\b/.test(lower);
|
|
1338
|
+
const years = Array.from(new Set(Array.from(prompt.matchAll(/\b(20\d{2}|19\d{2})\b/g)).map((match) => Number(match[1]))))
|
|
1339
|
+
.filter((year) => Number.isFinite(year))
|
|
1340
|
+
.sort((a, b) => a - b);
|
|
1029
1341
|
if (/\bweekly|week|last week|this week\b/.test(lower)) {
|
|
1030
1342
|
filters.push({
|
|
1031
1343
|
id: "week",
|
|
@@ -1042,6 +1354,47 @@ function inferFilters(prompt) {
|
|
|
1042
1354
|
bindsTo: "date",
|
|
1043
1355
|
});
|
|
1044
1356
|
}
|
|
1357
|
+
if (wantsSeasonStart || wantsSeasonEnd || /\bseason|year|annual|nba\b/.test(lower) || years.length > 0) {
|
|
1358
|
+
if (years.length >= 2 || wantsSeasonStart || wantsSeasonEnd) {
|
|
1359
|
+
filters.push({
|
|
1360
|
+
id: "season_start",
|
|
1361
|
+
label: "Season start",
|
|
1362
|
+
type: "select",
|
|
1363
|
+
...(years[0] ? { default: years[0] } : {}),
|
|
1364
|
+
...(years.length ? { options: years.map(String) } : {}),
|
|
1365
|
+
bindsTo: "game_date_est.year",
|
|
1366
|
+
});
|
|
1367
|
+
filters.push({
|
|
1368
|
+
id: "season_end",
|
|
1369
|
+
label: "Season end",
|
|
1370
|
+
type: "select",
|
|
1371
|
+
...(years[years.length - 1] ? { default: years[years.length - 1] } : {}),
|
|
1372
|
+
...(years.length ? { options: years.map(String) } : {}),
|
|
1373
|
+
bindsTo: "game_date_est.year",
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
filters.push({
|
|
1378
|
+
id: "season",
|
|
1379
|
+
label: "Season",
|
|
1380
|
+
type: "select",
|
|
1381
|
+
...(years[0] ? { default: years[0] } : {}),
|
|
1382
|
+
...(years.length ? { options: years.map(String) } : {}),
|
|
1383
|
+
bindsTo: "game_date_est.year",
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const topN = /\btop(?:[_\s-]*n)?\s+(\d+)\b/i.exec(prompt)?.[1]
|
|
1388
|
+
?? /\btop[_\s-]*n\s*[=:]?\s*(\d+)\b/i.exec(prompt)?.[1];
|
|
1389
|
+
if (topN || wantsTopN) {
|
|
1390
|
+
filters.push({
|
|
1391
|
+
id: "top_n",
|
|
1392
|
+
label: "Top N",
|
|
1393
|
+
type: "number",
|
|
1394
|
+
...(topN ? { default: Number(topN) } : {}),
|
|
1395
|
+
bindsTo: "limit",
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1045
1398
|
if (/\benterprise|segment|smb|mid-market\b/.test(lower)) {
|
|
1046
1399
|
filters.push({
|
|
1047
1400
|
id: "segment",
|
|
@@ -1050,8 +1403,90 @@ function inferFilters(prompt) {
|
|
|
1050
1403
|
bindsTo: "segment",
|
|
1051
1404
|
});
|
|
1052
1405
|
}
|
|
1406
|
+
return uniquePlanFilters(filters);
|
|
1407
|
+
}
|
|
1408
|
+
function uniquePlanFilters(filters) {
|
|
1409
|
+
const seen = new Set();
|
|
1410
|
+
const out = [];
|
|
1411
|
+
for (const filter of filters) {
|
|
1412
|
+
if (seen.has(filter.id))
|
|
1413
|
+
continue;
|
|
1414
|
+
seen.add(filter.id);
|
|
1415
|
+
out.push(filter);
|
|
1416
|
+
}
|
|
1417
|
+
return out;
|
|
1418
|
+
}
|
|
1419
|
+
const APP_FILTER_TIME_RE = /(_at$|_date$|_time$|_ts$|^date$|^month$|^week$|^day$|ordered_at|created)/i;
|
|
1420
|
+
const APP_FILTER_NUMBER_RE = /(top[_-]?n|limit|count|amount|year|score|rank)/i;
|
|
1421
|
+
/** Map a block filter column to the dashboard control the runtime filter engine renders. */
|
|
1422
|
+
function controlTypeForColumn(column) {
|
|
1423
|
+
if (APP_FILTER_TIME_RE.test(column))
|
|
1424
|
+
return "daterange";
|
|
1425
|
+
if (APP_FILTER_NUMBER_RE.test(column))
|
|
1426
|
+
return "number";
|
|
1427
|
+
return "select"; // categorical → dropdown (options filled by /api/dashboard/filter-options)
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Derive the global filter bar from what the certified tiles ACTUALLY accept — the
|
|
1431
|
+
* union of their declared `allowedFilters` (a block's governed, app-safe filters) —
|
|
1432
|
+
* instead of guessing from prompt words. Each control binds to a real column, so the
|
|
1433
|
+
* existing tile-binding logic refreshes exactly the tiles that declare it.
|
|
1434
|
+
*/
|
|
1435
|
+
function filtersFromCertifiedNodes(nodes) {
|
|
1436
|
+
const seen = new Set();
|
|
1437
|
+
const filters = [];
|
|
1438
|
+
for (const node of nodes) {
|
|
1439
|
+
for (const column of node.allowedFilters ?? []) {
|
|
1440
|
+
const key = column.toLowerCase();
|
|
1441
|
+
if (!column.trim() || seen.has(key))
|
|
1442
|
+
continue;
|
|
1443
|
+
seen.add(key);
|
|
1444
|
+
filters.push({
|
|
1445
|
+
id: column,
|
|
1446
|
+
label: titleCase(column.replace(/[_.]+/g, " ")),
|
|
1447
|
+
type: controlTypeForColumn(column),
|
|
1448
|
+
bindsTo: column,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1053
1452
|
return filters;
|
|
1054
1453
|
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Merge the block-derived bar (authoritative) with prompt-inferred filters: copy a
|
|
1456
|
+
* prompt default/options onto a matching column control, keep prompt-only PARAM
|
|
1457
|
+
* controls (e.g. top_n → LIMIT), and DROP prompt column filters no tile supports.
|
|
1458
|
+
*/
|
|
1459
|
+
function mergeAppFilters(blockFilters, promptFilters) {
|
|
1460
|
+
const merged = blockFilters.map((f) => ({ ...f }));
|
|
1461
|
+
// Match by exact id first (a prompt filter for the same governed filter), else by a
|
|
1462
|
+
// prompt filter that binds to a column which IS a block filter. Avoid matching on a
|
|
1463
|
+
// shared physical bindsTo — that collides once two filters resolve to the same column.
|
|
1464
|
+
const matchOf = (pf) => merged.find((bf) => bf.id.toLowerCase() === pf.id.toLowerCase()) ??
|
|
1465
|
+
(pf.bindsTo
|
|
1466
|
+
? merged.find((bf) => bf.id.toLowerCase() === pf.bindsTo.toLowerCase())
|
|
1467
|
+
: undefined);
|
|
1468
|
+
for (const pf of promptFilters) {
|
|
1469
|
+
const match = matchOf(pf);
|
|
1470
|
+
if (match) {
|
|
1471
|
+
if (pf.default !== undefined && match.default === undefined)
|
|
1472
|
+
match.default = pf.default;
|
|
1473
|
+
if (pf.options && !match.options)
|
|
1474
|
+
match.options = pf.options;
|
|
1475
|
+
// Prefer the prompt's more specific physical binding when the block filter only
|
|
1476
|
+
// self-binds (bindsTo === id) and the prompt resolved an actual column/expression.
|
|
1477
|
+
if (pf.bindsTo && (!match.bindsTo || match.bindsTo.toLowerCase() === match.id.toLowerCase())) {
|
|
1478
|
+
match.bindsTo = pf.bindsTo;
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
// A param-style control (LIMIT, not a column predicate) is always useful — keep it.
|
|
1483
|
+
if (pf.bindsTo === "limit" || /^(top_n|limit)$/i.test(pf.id)) {
|
|
1484
|
+
merged.push({ ...pf });
|
|
1485
|
+
}
|
|
1486
|
+
// else: a prompt column filter that no certified tile accepts — drop it (no orphans).
|
|
1487
|
+
}
|
|
1488
|
+
return uniquePlanFilters(merged);
|
|
1489
|
+
}
|
|
1055
1490
|
function inferReviewCadence(prompt) {
|
|
1056
1491
|
const lower = prompt.toLowerCase();
|
|
1057
1492
|
if (lower.includes("daily"))
|