@duckcodeailabs/dql-agent 1.6.0 → 1.6.2
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 +74 -10
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +856 -84
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +85 -0
- package/dist/app-builder.d.ts.map +1 -0
- package/dist/app-builder.js +677 -0
- package/dist/app-builder.js.map +1 -0
- package/dist/index.d.ts +14 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -19
- package/dist/index.js.map +1 -1
- package/dist/kg/build.d.ts +2 -3
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +115 -4
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.d.ts.map +1 -1
- package/dist/kg/sqlite-fts.js +8 -1
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +7 -6
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/kg/types.js +5 -4
- package/dist/kg/types.js.map +1 -1
- package/dist/memory/sqlite-memory.d.ts.map +1 -1
- package/dist/memory/sqlite-memory.js +8 -1
- package/dist/memory/sqlite-memory.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { parseAppDocument, parseDashboardDocument, suggestAppId, } from "@duckcodeailabs/dql-core";
|
|
4
|
+
export const APP_BUILDER_SKILLS = [
|
|
5
|
+
{
|
|
6
|
+
id: "match_certified_context",
|
|
7
|
+
title: "Match certified context",
|
|
8
|
+
description: "Search the local DQL ledger for certified blocks, terms, business views, and lineage that fit the prompt.",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "shape_business_story",
|
|
12
|
+
title: "Shape the story",
|
|
13
|
+
description: "Turn matched blocks into a stakeholder flow with filters, page title, tile order, and decision framing.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "draft_missing_sections",
|
|
17
|
+
title: "Draft missing sections",
|
|
18
|
+
description: "Create clearly marked draft tiles when a needed explanation, drilldown, or metric is not certified yet.",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "route_review",
|
|
22
|
+
title: "Route review",
|
|
23
|
+
description: "Keep generated sections uncertified and attach concrete review tasks before stakeholder use.",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
export function planAppFromPrompt(input) {
|
|
27
|
+
const prompt = input.prompt.trim();
|
|
28
|
+
if (!prompt)
|
|
29
|
+
throw new Error("prompt is required");
|
|
30
|
+
const domain = normalizeToken(input.domain) ?? inferDomain(prompt) ?? "general";
|
|
31
|
+
const audience = inferAudience(prompt) ?? inferDefaultAudience(prompt);
|
|
32
|
+
const appName = titleForPrompt(prompt, inferFallbackName(prompt, domain));
|
|
33
|
+
const appId = suggestAppId(appName);
|
|
34
|
+
const maxCertifiedTiles = input.maxCertifiedTiles ?? 4;
|
|
35
|
+
const preferredNodes = findPreferredCertifiedBlockNodes(input.kg, input.preferredBlockIds ?? []);
|
|
36
|
+
const matchedNodes = findCertifiedBlockNodes(input.kg, prompt, domain, Math.max(maxCertifiedTiles, preferredNodes.length));
|
|
37
|
+
const certifiedNodes = mergeCertifiedBlockNodes(preferredNodes, matchedNodes).slice(0, Math.max(maxCertifiedTiles, preferredNodes.length));
|
|
38
|
+
const filters = inferFilters(prompt);
|
|
39
|
+
const certifiedTiles = certifiedNodes.map((node, index) => tileFromCertifiedNode(node, index));
|
|
40
|
+
const draftTiles = inferDraftTiles(prompt, domain, certifiedNodes).map((tile, index) => ({
|
|
41
|
+
id: slugify(tile.title) || `draft-${index + 1}`,
|
|
42
|
+
title: tile.title,
|
|
43
|
+
kind: "draft_placeholder",
|
|
44
|
+
description: tile.description,
|
|
45
|
+
viz: tile.viz,
|
|
46
|
+
certification: "uncertified",
|
|
47
|
+
reviewStatus: "draft_ready",
|
|
48
|
+
rationale: "No certified block was selected for this generated app section.",
|
|
49
|
+
reviewTasks: [
|
|
50
|
+
"Validate metric definition, grain, filters, and source tables.",
|
|
51
|
+
"Promote to a certified block before treating this tile as governed.",
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
const narrativeTile = {
|
|
55
|
+
id: "app-context",
|
|
56
|
+
title: "App context",
|
|
57
|
+
kind: "narrative",
|
|
58
|
+
description: `Generated from prompt: ${prompt}`,
|
|
59
|
+
viz: "text",
|
|
60
|
+
certification: "uncertified",
|
|
61
|
+
reviewStatus: "review_required",
|
|
62
|
+
rationale: "Narrative text is generated scaffolding and should be reviewed with the app.",
|
|
63
|
+
reviewTasks: [
|
|
64
|
+
"Confirm the audience, business goal, caveats, and review cadence.",
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
appId,
|
|
70
|
+
name: appName,
|
|
71
|
+
prompt,
|
|
72
|
+
skills: APP_BUILDER_SKILLS,
|
|
73
|
+
domain,
|
|
74
|
+
audience,
|
|
75
|
+
businessGoal: prompt,
|
|
76
|
+
owner: input.owner?.trim() || `${process.env.USER ?? "owner"}@local`,
|
|
77
|
+
lifecycle: "draft",
|
|
78
|
+
tags: Array.from(new Set([
|
|
79
|
+
"ai-generated-app",
|
|
80
|
+
...inferTags(prompt, domain),
|
|
81
|
+
`audience:${slugify(audience)}`,
|
|
82
|
+
])),
|
|
83
|
+
pages: [
|
|
84
|
+
{
|
|
85
|
+
id: "overview",
|
|
86
|
+
title: inferDashboardTitle(prompt, domain, appName),
|
|
87
|
+
description: `Generated app story for ${audience}.`,
|
|
88
|
+
filters,
|
|
89
|
+
tiles: [narrativeTile, ...certifiedTiles, ...draftTiles],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
caveats: [
|
|
93
|
+
"Generated app plans are local draft artifacts until reviewed.",
|
|
94
|
+
"Certified tiles stay governed; draft and narrative tiles require analyst review.",
|
|
95
|
+
],
|
|
96
|
+
reviewTasks: [
|
|
97
|
+
"Review every uncertified tile before stakeholder use.",
|
|
98
|
+
"Run dql app build after accepting the generated files.",
|
|
99
|
+
"Promote repeated draft sections to certified blocks when the review is complete.",
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function validateAppPlan(plan, kg) {
|
|
104
|
+
const issues = [];
|
|
105
|
+
let certifiedTiles = 0;
|
|
106
|
+
let draftTiles = 0;
|
|
107
|
+
if (plan.version !== 1)
|
|
108
|
+
issues.push(error("version", "unsupported app plan version"));
|
|
109
|
+
if (!plan.appId || !/^[a-z0-9][a-z0-9_-]*$/i.test(plan.appId)) {
|
|
110
|
+
issues.push(error("appId", "appId must be folder-safe"));
|
|
111
|
+
}
|
|
112
|
+
if (!plan.name.trim())
|
|
113
|
+
issues.push(error("name", "name is required"));
|
|
114
|
+
if (!plan.domain.trim())
|
|
115
|
+
issues.push(error("domain", "domain is required"));
|
|
116
|
+
if (!plan.owner.trim())
|
|
117
|
+
issues.push(error("owner", "owner is required"));
|
|
118
|
+
if (plan.pages.length === 0)
|
|
119
|
+
issues.push(error("pages", "at least one page is required"));
|
|
120
|
+
for (const [pageIndex, page] of plan.pages.entries()) {
|
|
121
|
+
if (!page.id.trim())
|
|
122
|
+
issues.push(error(`pages[${pageIndex}].id`, "page id is required"));
|
|
123
|
+
if (page.tiles.length === 0)
|
|
124
|
+
issues.push(warn(`pages[${pageIndex}].tiles`, "page has no tiles"));
|
|
125
|
+
for (const [tileIndex, tile] of page.tiles.entries()) {
|
|
126
|
+
const path = `pages[${pageIndex}].tiles[${tileIndex}]`;
|
|
127
|
+
if (!tile.id.trim())
|
|
128
|
+
issues.push(error(`${path}.id`, "tile id is required"));
|
|
129
|
+
if (!tile.title.trim())
|
|
130
|
+
issues.push(error(`${path}.title`, "tile title is required"));
|
|
131
|
+
if (tile.kind === "certified_block") {
|
|
132
|
+
certifiedTiles += 1;
|
|
133
|
+
if (!tile.blockId) {
|
|
134
|
+
issues.push(error(`${path}.blockId`, "certified tile requires blockId"));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const node = kg.getNode(`block:${tile.blockId}`);
|
|
138
|
+
if (!node) {
|
|
139
|
+
issues.push(error(`${path}.blockId`, `certified block not found: ${tile.blockId}`));
|
|
140
|
+
}
|
|
141
|
+
else if (node.kind !== "block" || node.status !== "certified") {
|
|
142
|
+
issues.push(error(`${path}.blockId`, `block is not certified: ${tile.blockId}`));
|
|
143
|
+
}
|
|
144
|
+
if (tile.certification !== "certified" ||
|
|
145
|
+
tile.reviewStatus !== "certified") {
|
|
146
|
+
issues.push(error(path, "certified block tiles must be visibly certified"));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
draftTiles += 1;
|
|
151
|
+
if (tile.certification === "certified") {
|
|
152
|
+
issues.push(error(path, "non-block generated tiles cannot be marked certified"));
|
|
153
|
+
}
|
|
154
|
+
if (!tile.reviewTasks || tile.reviewTasks.length === 0) {
|
|
155
|
+
issues.push(warn(`${path}.reviewTasks`, "generated tile should include review tasks"));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (certifiedTiles === 0) {
|
|
161
|
+
issues.push(warn("pages", "no certified blocks matched; generated app will contain only draft/review tiles"));
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
ok: issues.every((issue) => issue.level !== "error"),
|
|
165
|
+
issues,
|
|
166
|
+
certifiedTiles,
|
|
167
|
+
draftTiles,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function generateAppFromPlan(projectRoot, plan, kg, options = {}) {
|
|
171
|
+
const validation = validateAppPlan(plan, kg);
|
|
172
|
+
const errors = validation.issues.filter((issue) => issue.level === "error");
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
throw new Error(`AppPlan is invalid: ${errors.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
175
|
+
}
|
|
176
|
+
const appDir = join(projectRoot, "apps", plan.appId);
|
|
177
|
+
if (existsSync(appDir) && !options.overwrite) {
|
|
178
|
+
throw new Error(`App already exists: apps/${plan.appId}`);
|
|
179
|
+
}
|
|
180
|
+
const dashboardId = plan.pages[0]?.id || "overview";
|
|
181
|
+
const app = {
|
|
182
|
+
version: 1,
|
|
183
|
+
id: plan.appId,
|
|
184
|
+
name: plan.name,
|
|
185
|
+
description: plan.businessGoal,
|
|
186
|
+
businessOutcome: plan.businessGoal,
|
|
187
|
+
businessOwner: plan.owner,
|
|
188
|
+
decisionUse: `${plan.audience} review`,
|
|
189
|
+
reviewCadence: inferReviewCadence(plan.prompt),
|
|
190
|
+
businessRules: [
|
|
191
|
+
"Certified tiles must reference existing certified blocks.",
|
|
192
|
+
],
|
|
193
|
+
caveats: plan.caveats,
|
|
194
|
+
visibility: "shared",
|
|
195
|
+
domain: plan.domain,
|
|
196
|
+
audience: plan.audience,
|
|
197
|
+
lifecycle: plan.lifecycle,
|
|
198
|
+
owners: [plan.owner],
|
|
199
|
+
tags: plan.tags,
|
|
200
|
+
members: [
|
|
201
|
+
{
|
|
202
|
+
userId: plan.owner,
|
|
203
|
+
displayName: plan.owner,
|
|
204
|
+
roles: ["owner", "analyst"],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
roles: [
|
|
208
|
+
{
|
|
209
|
+
id: "owner",
|
|
210
|
+
displayName: "Owner",
|
|
211
|
+
description: "Full access to app configuration and review.",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "analyst",
|
|
215
|
+
displayName: "Analyst",
|
|
216
|
+
description: "Can review generated drafts and run dashboards.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "viewer",
|
|
220
|
+
displayName: "Viewer",
|
|
221
|
+
description: "Read-only access to reviewed app content.",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
policies: [
|
|
225
|
+
{
|
|
226
|
+
id: "viewers-read",
|
|
227
|
+
domain: plan.domain,
|
|
228
|
+
minClassification: "internal",
|
|
229
|
+
allowedRoles: ["viewer", "analyst", "owner"],
|
|
230
|
+
accessLevel: "read",
|
|
231
|
+
enabled: true,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "analyst-execute",
|
|
235
|
+
domain: plan.domain,
|
|
236
|
+
minClassification: "internal",
|
|
237
|
+
allowedRoles: ["analyst", "owner"],
|
|
238
|
+
accessLevel: "execute",
|
|
239
|
+
enabled: true,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
rlsBindings: [],
|
|
243
|
+
schedules: [],
|
|
244
|
+
homepage: { type: "dashboard", id: dashboardId },
|
|
245
|
+
};
|
|
246
|
+
const dashboards = plan.pages.map((page) => ({
|
|
247
|
+
version: 1,
|
|
248
|
+
id: page.id,
|
|
249
|
+
metadata: {
|
|
250
|
+
title: page.title,
|
|
251
|
+
description: page.description,
|
|
252
|
+
domain: plan.domain,
|
|
253
|
+
audience: plan.audience,
|
|
254
|
+
visibility: "shared",
|
|
255
|
+
lifecycle: "draft",
|
|
256
|
+
tags: plan.tags,
|
|
257
|
+
businessOutcome: plan.businessGoal,
|
|
258
|
+
businessOwner: plan.owner,
|
|
259
|
+
decisionUse: `${plan.audience} review`,
|
|
260
|
+
reviewCadence: inferReviewCadence(plan.prompt),
|
|
261
|
+
caveats: plan.caveats,
|
|
262
|
+
},
|
|
263
|
+
filters: page.filters.map((filter) => ({
|
|
264
|
+
id: filter.id,
|
|
265
|
+
type: filter.type,
|
|
266
|
+
default: filter.default,
|
|
267
|
+
bindsTo: filter.bindsTo,
|
|
268
|
+
})),
|
|
269
|
+
layout: {
|
|
270
|
+
kind: "grid",
|
|
271
|
+
cols: 12,
|
|
272
|
+
rowHeight: 80,
|
|
273
|
+
items: buildLayoutItems(page.tiles),
|
|
274
|
+
},
|
|
275
|
+
}));
|
|
276
|
+
const appValidation = parseAppDocument(JSON.stringify(app), join(appDir, "dql.app.json"));
|
|
277
|
+
if (appValidation.errors.length > 0 || !appValidation.document) {
|
|
278
|
+
throw new Error(`Generated app is invalid: ${appValidation.errors.map((e) => e.message).join("; ")}`);
|
|
279
|
+
}
|
|
280
|
+
for (const dashboard of dashboards) {
|
|
281
|
+
const parsed = parseDashboardDocument(JSON.stringify(dashboard), join(appDir, "dashboards", `${dashboard.id}.dqld`));
|
|
282
|
+
if (parsed.errors.length > 0 || !parsed.document) {
|
|
283
|
+
throw new Error(`Generated dashboard is invalid: ${parsed.errors.map((e) => e.message).join("; ")}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
mkdirSync(join(appDir, "dashboards"), { recursive: true });
|
|
287
|
+
mkdirSync(join(appDir, "notebooks"), { recursive: true });
|
|
288
|
+
mkdirSync(join(appDir, "drafts"), { recursive: true });
|
|
289
|
+
writeFileSync(join(appDir, "dql.app.json"), JSON.stringify(appValidation.document, null, 2) + "\n", "utf-8");
|
|
290
|
+
for (const dashboard of dashboards) {
|
|
291
|
+
writeFileSync(join(appDir, "dashboards", `${dashboard.id}.dqld`), JSON.stringify(dashboard, null, 2) + "\n", "utf-8");
|
|
292
|
+
}
|
|
293
|
+
writeFileSync(join(appDir, "README.md"), appPlanReadme(plan, validation), "utf-8");
|
|
294
|
+
return {
|
|
295
|
+
app: appValidation.document,
|
|
296
|
+
dashboards,
|
|
297
|
+
paths: [
|
|
298
|
+
join(appDir, "dql.app.json"),
|
|
299
|
+
...dashboards.map((dashboard) => join(appDir, "dashboards", `${dashboard.id}.dqld`)),
|
|
300
|
+
join(appDir, "README.md"),
|
|
301
|
+
].map((path) => relative(projectRoot, path)),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function findCertifiedBlockNodes(kg, prompt, domain, limit) {
|
|
305
|
+
const hits = kg.search({
|
|
306
|
+
query: prompt,
|
|
307
|
+
domain,
|
|
308
|
+
kinds: ["block"],
|
|
309
|
+
limit: limit * 3,
|
|
310
|
+
});
|
|
311
|
+
let nodes = hits
|
|
312
|
+
.map((hit) => hit.node)
|
|
313
|
+
.filter((node) => node.status === "certified");
|
|
314
|
+
if (nodes.length === 0 && domain !== "general") {
|
|
315
|
+
nodes = kg
|
|
316
|
+
.search({ query: prompt, kinds: ["block"], limit: limit * 3 })
|
|
317
|
+
.map((hit) => hit.node)
|
|
318
|
+
.filter((node) => node.status === "certified");
|
|
319
|
+
}
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
return nodes
|
|
322
|
+
.filter((node) => {
|
|
323
|
+
if (seen.has(node.nodeId))
|
|
324
|
+
return false;
|
|
325
|
+
seen.add(node.nodeId);
|
|
326
|
+
return true;
|
|
327
|
+
})
|
|
328
|
+
.slice(0, limit);
|
|
329
|
+
}
|
|
330
|
+
function findPreferredCertifiedBlockNodes(kg, blockIds) {
|
|
331
|
+
const seen = new Set();
|
|
332
|
+
const nodes = [];
|
|
333
|
+
for (const id of blockIds) {
|
|
334
|
+
const clean = id.trim();
|
|
335
|
+
if (!clean)
|
|
336
|
+
continue;
|
|
337
|
+
const nodeId = clean.startsWith("block:") ? clean : `block:${clean}`;
|
|
338
|
+
if (seen.has(nodeId))
|
|
339
|
+
continue;
|
|
340
|
+
seen.add(nodeId);
|
|
341
|
+
const node = kg.getNode(nodeId);
|
|
342
|
+
if (node?.kind === "block" && node.status === "certified") {
|
|
343
|
+
nodes.push(node);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return nodes;
|
|
347
|
+
}
|
|
348
|
+
function mergeCertifiedBlockNodes(preferredNodes, matchedNodes) {
|
|
349
|
+
const seen = new Set();
|
|
350
|
+
const merged = [];
|
|
351
|
+
for (const node of [...preferredNodes, ...matchedNodes]) {
|
|
352
|
+
if (seen.has(node.nodeId))
|
|
353
|
+
continue;
|
|
354
|
+
seen.add(node.nodeId);
|
|
355
|
+
merged.push(node);
|
|
356
|
+
}
|
|
357
|
+
return merged;
|
|
358
|
+
}
|
|
359
|
+
function tileFromCertifiedNode(node, index) {
|
|
360
|
+
const blockId = node.name;
|
|
361
|
+
return {
|
|
362
|
+
id: slugify(blockId) || `certified-${index + 1}`,
|
|
363
|
+
title: node.name,
|
|
364
|
+
kind: "certified_block",
|
|
365
|
+
description: node.description,
|
|
366
|
+
blockId,
|
|
367
|
+
sourceNodeId: node.nodeId,
|
|
368
|
+
viz: inferVizForNode(node, index),
|
|
369
|
+
certification: "certified",
|
|
370
|
+
reviewStatus: "certified",
|
|
371
|
+
rationale: node.decisionUse ??
|
|
372
|
+
node.businessOutcome ??
|
|
373
|
+
"Certified DQL block matched the app prompt.",
|
|
374
|
+
caveats: node.caveats,
|
|
375
|
+
reviewTasks: [
|
|
376
|
+
"Confirm the block is the intended stakeholder-facing metric for this app.",
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function buildLayoutItems(tiles) {
|
|
381
|
+
let x = 0;
|
|
382
|
+
let y = 0;
|
|
383
|
+
let rowH = 0;
|
|
384
|
+
return tiles.map((tile, index) => {
|
|
385
|
+
const size = tileSize(tile.viz, tile.kind);
|
|
386
|
+
if (x + size.w > 12) {
|
|
387
|
+
x = 0;
|
|
388
|
+
y += rowH || size.h;
|
|
389
|
+
rowH = 0;
|
|
390
|
+
}
|
|
391
|
+
const item = {
|
|
392
|
+
i: tile.id || `tile-${index + 1}`,
|
|
393
|
+
x,
|
|
394
|
+
y,
|
|
395
|
+
w: size.w,
|
|
396
|
+
h: size.h,
|
|
397
|
+
title: tile.title,
|
|
398
|
+
viz: { type: tile.viz },
|
|
399
|
+
...(tile.kind === "certified_block" && tile.blockId
|
|
400
|
+
? { block: { blockId: tile.blockId } }
|
|
401
|
+
: { text: { markdown: markdownForGeneratedTile(tile) } }),
|
|
402
|
+
};
|
|
403
|
+
x += size.w;
|
|
404
|
+
rowH = Math.max(rowH, size.h);
|
|
405
|
+
return item;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function markdownForGeneratedTile(tile) {
|
|
409
|
+
const lines = [
|
|
410
|
+
`### ${tile.title}`,
|
|
411
|
+
"",
|
|
412
|
+
tile.description ?? "Generated app section pending analyst review.",
|
|
413
|
+
"",
|
|
414
|
+
`Certification: ${tile.certification}`,
|
|
415
|
+
`Review status: ${tile.reviewStatus}`,
|
|
416
|
+
];
|
|
417
|
+
if (tile.reviewTasks?.length) {
|
|
418
|
+
lines.push("", "Review tasks:", ...tile.reviewTasks.map((task) => `- ${task}`));
|
|
419
|
+
}
|
|
420
|
+
return lines.join("\n");
|
|
421
|
+
}
|
|
422
|
+
function appPlanReadme(plan, validation) {
|
|
423
|
+
return [
|
|
424
|
+
`# ${plan.name}`,
|
|
425
|
+
"",
|
|
426
|
+
plan.businessGoal,
|
|
427
|
+
"",
|
|
428
|
+
`- Generated from prompt: ${plan.prompt}`,
|
|
429
|
+
`- Domain: ${plan.domain}`,
|
|
430
|
+
`- Audience: ${plan.audience}`,
|
|
431
|
+
`- Lifecycle: ${plan.lifecycle}`,
|
|
432
|
+
`- Certified tiles: ${validation.certifiedTiles}`,
|
|
433
|
+
`- Draft/review tiles: ${validation.draftTiles}`,
|
|
434
|
+
"",
|
|
435
|
+
"## Agent skills applied",
|
|
436
|
+
"",
|
|
437
|
+
...plan.skills.map((skill) => `- ${skill.title}: ${skill.description}`),
|
|
438
|
+
"",
|
|
439
|
+
"## Review tasks",
|
|
440
|
+
"",
|
|
441
|
+
...plan.reviewTasks.map((task) => `- ${task}`),
|
|
442
|
+
"",
|
|
443
|
+
"## Caveats",
|
|
444
|
+
"",
|
|
445
|
+
...plan.caveats.map((caveat) => `- ${caveat}`),
|
|
446
|
+
"",
|
|
447
|
+
].join("\n");
|
|
448
|
+
}
|
|
449
|
+
function inferFallbackName(prompt, domain) {
|
|
450
|
+
const lower = prompt.toLowerCase();
|
|
451
|
+
if (/\brevenue|arr|sales|pipeline|growth\b/.test(lower))
|
|
452
|
+
return "Revenue Story";
|
|
453
|
+
if (/\bcustomer|account|churn|retention\b/.test(lower))
|
|
454
|
+
return "Customer Story";
|
|
455
|
+
if (/\bquality|freshness|anomal|monitor\b/.test(lower))
|
|
456
|
+
return "Data Quality Story";
|
|
457
|
+
if (/\bexperiment|ab test|a\/b|readout\b/.test(lower))
|
|
458
|
+
return "Experiment Story";
|
|
459
|
+
return `${titleCase(domain)} App`;
|
|
460
|
+
}
|
|
461
|
+
function inferDashboardTitle(prompt, domain, appName) {
|
|
462
|
+
const lower = prompt.toLowerCase();
|
|
463
|
+
if (/\boverview|summary|360|review|readout|monitor\b/.test(lower)) {
|
|
464
|
+
return titleCase(prompt
|
|
465
|
+
.split(/[.:;]/)[0]
|
|
466
|
+
.replace(/\b(build|create|generate|make|app|dashboard)\b/gi, "")
|
|
467
|
+
.trim()).slice(0, 64) || "Overview";
|
|
468
|
+
}
|
|
469
|
+
if (domain !== "general")
|
|
470
|
+
return `${titleCase(domain)} Overview`;
|
|
471
|
+
return appName || "Overview";
|
|
472
|
+
}
|
|
473
|
+
function inferDefaultAudience(prompt) {
|
|
474
|
+
const lower = prompt.toLowerCase();
|
|
475
|
+
if (lower.includes("executive") || /\bcxo|ceo|cfo|coo|cro\b/.test(lower))
|
|
476
|
+
return "executive team";
|
|
477
|
+
if (lower.includes("sales") || lower.includes("revenue"))
|
|
478
|
+
return "revenue leadership";
|
|
479
|
+
if (lower.includes("customer"))
|
|
480
|
+
return "customer team";
|
|
481
|
+
if (lower.includes("quality") || lower.includes("platform"))
|
|
482
|
+
return "data team";
|
|
483
|
+
if (lower.includes("product") || lower.includes("experiment"))
|
|
484
|
+
return "product team";
|
|
485
|
+
return "stakeholders";
|
|
486
|
+
}
|
|
487
|
+
function inferTags(prompt, domain) {
|
|
488
|
+
const lower = prompt.toLowerCase();
|
|
489
|
+
const tags = new Set(["agent-built", "reviewable", domain]);
|
|
490
|
+
if (/\brevenue|arr|sales|pipeline|growth\b/.test(lower))
|
|
491
|
+
tags.add("revenue");
|
|
492
|
+
if (/\bcustomer|account|churn|retention\b/.test(lower))
|
|
493
|
+
tags.add("customer");
|
|
494
|
+
if (/\bquality|freshness|anomal|monitor\b/.test(lower))
|
|
495
|
+
tags.add("quality");
|
|
496
|
+
if (/\bexperiment|ab test|a\/b|readout\b/.test(lower))
|
|
497
|
+
tags.add("experiment");
|
|
498
|
+
if (/\bweekly|week\b/.test(lower))
|
|
499
|
+
tags.add("weekly-review");
|
|
500
|
+
if (/\bmonthly|month\b/.test(lower))
|
|
501
|
+
tags.add("monthly-review");
|
|
502
|
+
return Array.from(tags).filter(Boolean);
|
|
503
|
+
}
|
|
504
|
+
function inferDraftTiles(prompt, domain, certifiedNodes) {
|
|
505
|
+
const lower = prompt.toLowerCase();
|
|
506
|
+
const domainLabel = titleCase(domain);
|
|
507
|
+
const tiles = [
|
|
508
|
+
{
|
|
509
|
+
title: `${domainLabel} decision narrative`,
|
|
510
|
+
description: "Draft explanation of the business story, decision context, and caveats inferred from the prompt.",
|
|
511
|
+
viz: "text",
|
|
512
|
+
},
|
|
513
|
+
];
|
|
514
|
+
if (/\brisk|caveat|issue|anomal|quality\b/.test(lower)) {
|
|
515
|
+
tiles.push({
|
|
516
|
+
title: "Open risks and caveats",
|
|
517
|
+
description: "Generated review checklist for risks that need certified evidence before stakeholder use.",
|
|
518
|
+
viz: "table",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
else if (/\bsegment|cohort|region|location|product|channel|customer\b/.test(lower)) {
|
|
522
|
+
tiles.push({
|
|
523
|
+
title: "Missing drilldowns to certify",
|
|
524
|
+
description: "Candidate slices and drilldowns the agent could not fully back with certified blocks yet.",
|
|
525
|
+
viz: "table",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
tiles.push({
|
|
530
|
+
title: "Evidence gaps to certify",
|
|
531
|
+
description: "Open sections where new or extended DQL blocks should be created before this app is governed.",
|
|
532
|
+
viz: "table",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (certifiedNodes.length === 0) {
|
|
536
|
+
tiles.push({
|
|
537
|
+
title: "Certified block search",
|
|
538
|
+
description: "No certified blocks matched strongly enough; review suggested sources and create certified blocks.",
|
|
539
|
+
viz: "table",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return tiles;
|
|
543
|
+
}
|
|
544
|
+
function inferDomain(prompt) {
|
|
545
|
+
const lower = prompt.toLowerCase();
|
|
546
|
+
if (/\brevenue|arr|sales|pipeline|growth\b/.test(lower))
|
|
547
|
+
return "growth";
|
|
548
|
+
if (/\bcustomer|account|churn|retention\b/.test(lower))
|
|
549
|
+
return "customer";
|
|
550
|
+
if (/\bquality|freshness|anomal|platform\b/.test(lower))
|
|
551
|
+
return "data";
|
|
552
|
+
if (/\bexperiment|product\b/.test(lower))
|
|
553
|
+
return "product";
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
function inferAudience(prompt) {
|
|
557
|
+
const match = prompt.match(/\b(?:for|to)\s+(?:the\s+)?([A-Z][A-Za-z0-9 &-]{1,40}|COO|CEO|CFO|CRO|VP[^,.]*)/);
|
|
558
|
+
if (match?.[1])
|
|
559
|
+
return match[1].trim();
|
|
560
|
+
const lower = prompt.toLowerCase();
|
|
561
|
+
if (lower.includes("coo"))
|
|
562
|
+
return "COO";
|
|
563
|
+
if (lower.includes("cfo"))
|
|
564
|
+
return "CFO";
|
|
565
|
+
if (lower.includes("cro"))
|
|
566
|
+
return "CRO";
|
|
567
|
+
if (lower.includes("executive"))
|
|
568
|
+
return "executive team";
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
function inferFilters(prompt) {
|
|
572
|
+
const filters = [];
|
|
573
|
+
const lower = prompt.toLowerCase();
|
|
574
|
+
if (/\bweekly|week|last week|this week\b/.test(lower)) {
|
|
575
|
+
filters.push({
|
|
576
|
+
id: "week",
|
|
577
|
+
label: "Week",
|
|
578
|
+
type: "daterange",
|
|
579
|
+
bindsTo: "date",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else if (/\bmonthly|month|quarter|year\b/.test(lower)) {
|
|
583
|
+
filters.push({
|
|
584
|
+
id: "period",
|
|
585
|
+
label: "Period",
|
|
586
|
+
type: "daterange",
|
|
587
|
+
bindsTo: "date",
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (/\benterprise|segment|smb|mid-market\b/.test(lower)) {
|
|
591
|
+
filters.push({
|
|
592
|
+
id: "segment",
|
|
593
|
+
label: "Segment",
|
|
594
|
+
type: "select",
|
|
595
|
+
bindsTo: "segment",
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return filters;
|
|
599
|
+
}
|
|
600
|
+
function inferReviewCadence(prompt) {
|
|
601
|
+
const lower = prompt.toLowerCase();
|
|
602
|
+
if (lower.includes("daily"))
|
|
603
|
+
return "daily";
|
|
604
|
+
if (lower.includes("weekly") || lower.includes("week"))
|
|
605
|
+
return "weekly";
|
|
606
|
+
if (lower.includes("monthly") || lower.includes("month"))
|
|
607
|
+
return "monthly";
|
|
608
|
+
if (lower.includes("quarterly") || lower.includes("quarter"))
|
|
609
|
+
return "quarterly";
|
|
610
|
+
return "ad hoc";
|
|
611
|
+
}
|
|
612
|
+
function titleForPrompt(prompt, fallback) {
|
|
613
|
+
const cleaned = prompt
|
|
614
|
+
.replace(/\bfor\s+(?:the\s+)?[A-Z][A-Za-z0-9 &-]{1,40}$/i, "")
|
|
615
|
+
.replace(/\b(build|create|generate|make)\b/gi, "")
|
|
616
|
+
.replace(/\b(a|an|app|dashboard|for|the)\b/gi, "")
|
|
617
|
+
.replace(/\s+/g, " ")
|
|
618
|
+
.trim();
|
|
619
|
+
const words = cleaned.split(" ").filter(Boolean).slice(0, 6);
|
|
620
|
+
if (words.length === 0)
|
|
621
|
+
return fallback;
|
|
622
|
+
return titleCase(words.join(" "));
|
|
623
|
+
}
|
|
624
|
+
function inferVizForNode(node, index) {
|
|
625
|
+
const text = [
|
|
626
|
+
node.name,
|
|
627
|
+
node.description,
|
|
628
|
+
node.llmContext,
|
|
629
|
+
...(node.tags ?? []),
|
|
630
|
+
]
|
|
631
|
+
.join(" ")
|
|
632
|
+
.toLowerCase();
|
|
633
|
+
if (/\btrend|time|week|month|quarter|daily|weekly|monthly\b/.test(text))
|
|
634
|
+
return "line";
|
|
635
|
+
if (/\btotal|count|rate|score|kpi|arr|revenue\b/.test(text) && index === 0)
|
|
636
|
+
return "single_value";
|
|
637
|
+
if (/\bsegment|region|channel|category|breakdown|split\b/.test(text))
|
|
638
|
+
return "bar";
|
|
639
|
+
return "table";
|
|
640
|
+
}
|
|
641
|
+
function tileSize(viz, kind) {
|
|
642
|
+
if (kind === "narrative")
|
|
643
|
+
return { w: 12, h: 2 };
|
|
644
|
+
if (viz === "single_value" || viz === "kpi")
|
|
645
|
+
return { w: 3, h: 2 };
|
|
646
|
+
if (viz === "text")
|
|
647
|
+
return { w: 6, h: 3 };
|
|
648
|
+
if (viz === "table" || viz === "pivot")
|
|
649
|
+
return { w: 6, h: 4 };
|
|
650
|
+
return { w: 6, h: 3 };
|
|
651
|
+
}
|
|
652
|
+
function titleCase(value) {
|
|
653
|
+
return value
|
|
654
|
+
.split(/\s+/)
|
|
655
|
+
.map((word) => word.length <= 3 && word === word.toUpperCase()
|
|
656
|
+
? word
|
|
657
|
+
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
658
|
+
.join(" ");
|
|
659
|
+
}
|
|
660
|
+
function slugify(input) {
|
|
661
|
+
return input
|
|
662
|
+
.toLowerCase()
|
|
663
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
664
|
+
.replace(/^-+|-+$/g, "")
|
|
665
|
+
.replace(/-{2,}/g, "-");
|
|
666
|
+
}
|
|
667
|
+
function normalizeToken(input) {
|
|
668
|
+
const value = input?.trim().toLowerCase();
|
|
669
|
+
return value || undefined;
|
|
670
|
+
}
|
|
671
|
+
function error(path, message) {
|
|
672
|
+
return { level: "error", path, message };
|
|
673
|
+
}
|
|
674
|
+
function warn(path, message) {
|
|
675
|
+
return { level: "warning", path, message };
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=app-builder.js.map
|