@duckcodeailabs/dql-agent 1.13.0 → 1.13.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/agent-run-engine.d.ts +8 -6
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +34 -2
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/answer-loop.d.ts +9 -1
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +59 -5
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +19 -1
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +261 -51
- package/dist/app-builder.js.map +1 -1
- package/dist/cascade/route-policy.d.ts +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/context-authoring.d.ts +98 -0
- package/dist/context-authoring.d.ts.map +1 -0
- package/dist/context-authoring.js +146 -0
- package/dist/context-authoring.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/metadata/analytical-policy.d.ts +8 -0
- package/dist/metadata/analytical-policy.d.ts.map +1 -1
- package/dist/metadata/analytical-policy.js +5 -2
- package/dist/metadata/analytical-policy.js.map +1 -1
- package/dist/metadata/catalog.d.ts +2 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +106 -14
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +0 -5
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +57 -0
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/propose/build-from-prompt.d.ts.map +1 -1
- package/dist/propose/build-from-prompt.js +27 -0
- package/dist/propose/build-from-prompt.js.map +1 -1
- package/dist/skills/loader.d.ts +24 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +137 -6
- package/dist/skills/loader.js.map +1 -1
- package/package.json +4 -4
package/dist/app-builder.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface AppPlanGenUi {
|
|
|
58
58
|
export interface AppPlanFilter {
|
|
59
59
|
id: string;
|
|
60
60
|
label: string;
|
|
61
|
-
type: "date" | "daterange" | "select" | "string" | "number";
|
|
61
|
+
type: "date" | "daterange" | "select" | "search" | "string" | "number";
|
|
62
62
|
default?: unknown;
|
|
63
63
|
options?: string[];
|
|
64
64
|
bindsTo?: string;
|
|
@@ -223,4 +223,22 @@ export declare const APP_BUILDER_SKILLS: AppBuilderSkill[];
|
|
|
223
223
|
export declare function planAppFromPrompt(input: PlanAppFromPromptInput): AppPlan;
|
|
224
224
|
export declare function validateAppPlan(plan: AppPlan, kg: KGStore): AppPlanValidationResult;
|
|
225
225
|
export declare function generateAppFromPlan(projectRoot: string, plan: AppPlan, kg: KGStore, options?: GenerateAppFromPlanOptions): GeneratedAppPackage;
|
|
226
|
+
/**
|
|
227
|
+
* Compile a validated AppPlan into typed App/dashboard documents without
|
|
228
|
+
* touching the user's project. App Studio uses this for AI proposal diffs;
|
|
229
|
+
* only the later Publish to Project service writes canonical App source.
|
|
230
|
+
*/
|
|
231
|
+
export declare function materializeAppPlanDocuments(plan: AppPlan, kg: KGStore, options?: GenerateAppFromPlanOptions): Pick<GeneratedAppPackage, "app" | "dashboards">;
|
|
232
|
+
/**
|
|
233
|
+
* A tile title a stakeholder can read.
|
|
234
|
+
*
|
|
235
|
+
* A certified block's KG node name is its id — `monthly_revenue` — so using it
|
|
236
|
+
* directly labelled every AI-built tile with an identifier, which also made the
|
|
237
|
+
* titles look like system fields rather than something the author may rename.
|
|
238
|
+
* Prefer the lead clause of the block's own description, which authors already
|
|
239
|
+
* write as a summary ("Monthly gross order revenue and order count."), and fall
|
|
240
|
+
* back to the de-slugged id when that clause is missing or too long to sit in a
|
|
241
|
+
* tile header.
|
|
242
|
+
*/
|
|
243
|
+
export declare function humanTileTitle(name: string, description?: string): string;
|
|
226
244
|
//# sourceMappingURL=app-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-builder.d.ts","sourceRoot":"","sources":["../src/app-builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-builder.d.ts","sourceRoot":"","sources":["../src/app-builder.ts"],"names":[],"mappings":"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"}
|
package/dist/app-builder.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { join, relative } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
4
5
|
import { parseAppDocument, parseDashboardDocument, suggestAppId, } from "@duckcodeailabs/dql-core";
|
|
5
6
|
import { buildAnalysisQuestionPlan } from "./metadata/analysis-planner.js";
|
|
6
7
|
import { certifiedFitAllowsTier1, evaluateCertifiedBlockFit } from "./metadata/block-fit.js";
|
|
@@ -36,13 +37,25 @@ export const APP_BUILDER_SKILLS = [
|
|
|
36
37
|
description: "Keep generated analysis review-required and attach concrete next steps before stakeholder use.",
|
|
37
38
|
},
|
|
38
39
|
];
|
|
39
|
-
function appRequirementsForPrompt(prompt) {
|
|
40
|
+
function appRequirementsForPrompt(prompt, certifiedNodes = []) {
|
|
40
41
|
const analysis = buildAnalysisQuestionPlan(prompt);
|
|
41
42
|
const measures = analysis.requestedShape.measures.length
|
|
42
43
|
? analysis.requestedShape.measures
|
|
43
44
|
: analysis.metricTerms;
|
|
44
|
-
const
|
|
45
|
-
const
|
|
45
|
+
const explicitMembers = explicitMemberFilters(prompt);
|
|
46
|
+
const requestedDimensions = Array.from(new Set([
|
|
47
|
+
...analysis.requestedShape.dimensions,
|
|
48
|
+
...explicitMembers.map((member) => member.dimension),
|
|
49
|
+
]));
|
|
50
|
+
const availableDimensions = Array.from(new Set(certifiedNodes.flatMap((node) => [
|
|
51
|
+
...(node.dimensions ?? []),
|
|
52
|
+
...(node.allowedFilters ?? []),
|
|
53
|
+
]))).filter((dimension) => !/^(top_?n|limit)$/i.test(dimension));
|
|
54
|
+
const dimensions = requestedDimensions.length > 0 ? requestedDimensions : availableDimensions.slice(0, 3);
|
|
55
|
+
const filters = Array.from(new Set([
|
|
56
|
+
...analysis.requestedShape.filters,
|
|
57
|
+
...explicitMembers.map((member) => member.value),
|
|
58
|
+
]));
|
|
46
59
|
const ranking = analysis.requestedShape.topN
|
|
47
60
|
? { direction: analysis.requestedShape.rankingDirection ?? "top", limit: analysis.requestedShape.topN.n }
|
|
48
61
|
: undefined;
|
|
@@ -56,18 +69,31 @@ function appRequirementsForPrompt(prompt) {
|
|
|
56
69
|
grain: analysis.requestedShape.grain,
|
|
57
70
|
...(ranking ? { ranking } : {}),
|
|
58
71
|
}];
|
|
59
|
-
|
|
72
|
+
if (measures.length > 0 && requirements[0]?.role !== "kpi") {
|
|
73
|
+
requirements.unshift({
|
|
74
|
+
id: "headline",
|
|
75
|
+
question: `Headline ${measures.join(" and ")}`,
|
|
76
|
+
role: "kpi",
|
|
77
|
+
measures,
|
|
78
|
+
dimensions: [],
|
|
79
|
+
filters,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const sourceTimeDimension = availableDimensions.find((dimension) => /(_at$|_date$|_time$|^date$|^month$|^week$|^day$|quarter|year)/i.test(dimension));
|
|
83
|
+
const wantsTrend = Boolean(sourceTimeDimension) || analysis.mode === "trend" || /\btrend|over time|weekly|monthly|quarterly\b/i.test(prompt);
|
|
60
84
|
if (wantsTrend && !requirements.some((requirement) => requirement.role === "trend")) {
|
|
61
85
|
requirements.push({
|
|
62
86
|
id: "trend",
|
|
63
87
|
question: `${measures.join(" and ") || "primary metric"} over time`,
|
|
64
88
|
role: "trend",
|
|
65
89
|
measures,
|
|
66
|
-
dimensions: analysis.timeTerms.length ? analysis.timeTerms : ["time"],
|
|
90
|
+
dimensions: analysis.timeTerms.length ? analysis.timeTerms : [sourceTimeDimension ?? "time"],
|
|
67
91
|
filters,
|
|
68
92
|
});
|
|
69
93
|
}
|
|
70
94
|
for (const dimension of dimensions.slice(0, 2)) {
|
|
95
|
+
if (/(_at$|_date$|_time$|^date$|^month$|^week$|^day$|quarter|year)/i.test(dimension))
|
|
96
|
+
continue;
|
|
71
97
|
if (requirements[0]?.dimensions.includes(dimension) && dimensions.length === 1)
|
|
72
98
|
continue;
|
|
73
99
|
requirements.push({
|
|
@@ -79,8 +105,25 @@ function appRequirementsForPrompt(prompt) {
|
|
|
79
105
|
filters,
|
|
80
106
|
});
|
|
81
107
|
}
|
|
108
|
+
const detailDimensions = dimensions.filter((dimension) => !/(_at$|_date$|_time$|^date$|^month$|^week$|^day$|quarter|year)/i.test(dimension));
|
|
109
|
+
if ((ranking || detailDimensions.length > 0) && !requirements.some((requirement) => requirement.role === "detail")) {
|
|
110
|
+
requirements.push({
|
|
111
|
+
id: "detail",
|
|
112
|
+
question: `Detailed evidence for ${measures.join(" and ") || prompt}`,
|
|
113
|
+
role: "detail",
|
|
114
|
+
measures,
|
|
115
|
+
dimensions: detailDimensions.slice(0, 3),
|
|
116
|
+
filters,
|
|
117
|
+
grain: analysis.requestedShape.grain,
|
|
118
|
+
...(ranking ? { ranking } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
82
121
|
return requirements;
|
|
83
122
|
}
|
|
123
|
+
function explicitMemberFilters(prompt) {
|
|
124
|
+
const matches = prompt.matchAll(/\bfor\s+([a-z0-9][a-z0-9_-]*)\s+((?:product\s+)?category|segment|status|region|channel|country|type)\b/gi);
|
|
125
|
+
return Array.from(matches, (match) => ({ value: match[1], dimension: match[2] }));
|
|
126
|
+
}
|
|
84
127
|
function certifiedCoverageForRequirement(requirement, nodes) {
|
|
85
128
|
const plan = buildAnalysisQuestionPlan(requirement.question);
|
|
86
129
|
const ranked = nodes.map((node) => ({
|
|
@@ -151,6 +194,12 @@ function semanticTileForRequirement(kg, requirement, domain, snapshotId) {
|
|
|
151
194
|
if (!metric)
|
|
152
195
|
return null;
|
|
153
196
|
const semanticModelRefs = Array.from(new Set(modelNodes.map((node) => node.name))).slice(0, 4);
|
|
197
|
+
const semanticFilters = semanticFiltersForRequirement(requirement);
|
|
198
|
+
// A semantic fallback may close a filtered requirement only when every
|
|
199
|
+
// requested member is bound to a named dimension. Recording the filter in a
|
|
200
|
+
// fingerprint without applying it would make coverage untruthful.
|
|
201
|
+
if (requirement.filters.length > 0 && semanticFilters.length !== requirement.filters.length)
|
|
202
|
+
return null;
|
|
154
203
|
const id = `semantic-${requirement.id}`;
|
|
155
204
|
const fingerprintPayload = JSON.stringify({
|
|
156
205
|
metric: metric.nodeId,
|
|
@@ -164,6 +213,7 @@ function semanticTileForRequirement(kg, requirement, domain, snapshotId) {
|
|
|
164
213
|
provider: metric.provenance?.toLowerCase().includes("metricflow") ? "metricflow" : "native",
|
|
165
214
|
metrics: [metric.name],
|
|
166
215
|
...(requirement.dimensions.length ? { dimensions: requirement.dimensions } : {}),
|
|
216
|
+
...(semanticFilters.length ? { filters: semanticFilters } : {}),
|
|
167
217
|
...(requirement.ranking ? {
|
|
168
218
|
orderBy: [{ field: metric.name, direction: requirement.ranking.direction === "bottom" ? "asc" : "desc" }],
|
|
169
219
|
limit: requirement.ranking.limit,
|
|
@@ -189,6 +239,15 @@ function semanticTileForRequirement(kg, requirement, domain, snapshotId) {
|
|
|
189
239
|
reviewStatus: "review_required",
|
|
190
240
|
trustState: "review_required",
|
|
191
241
|
rationale: "Governed semantic coverage for a requirement not fully covered by a compatible certified block.",
|
|
242
|
+
...(semanticFilters.length ? {
|
|
243
|
+
filterBindings: semanticFilters.map((filter) => ({
|
|
244
|
+
filter: slugify(filter.field),
|
|
245
|
+
binding: filter.field,
|
|
246
|
+
mode: "semantic",
|
|
247
|
+
capability: "supported",
|
|
248
|
+
required: true,
|
|
249
|
+
})),
|
|
250
|
+
} : {}),
|
|
192
251
|
sourceEvidence: [
|
|
193
252
|
{ source: metric.nodeId, nodeId: metric.nodeId, kind: metric.kind, reason: "Governed semantic metric; result review is still required", trustState: "review_required" },
|
|
194
253
|
...modelNodes.slice(0, 4).map((node) => ({ source: node.nodeId, nodeId: node.nodeId, kind: node.kind, reason: "Semantic model used by the canonical query", trustState: "review_required" })),
|
|
@@ -196,6 +255,39 @@ function semanticTileForRequirement(kg, requirement, domain, snapshotId) {
|
|
|
196
255
|
reviewTasks: ["Review semantic definition freshness and result shape before stakeholder publication."],
|
|
197
256
|
};
|
|
198
257
|
}
|
|
258
|
+
function semanticFiltersForRequirement(requirement) {
|
|
259
|
+
const question = requirement.question.toLowerCase();
|
|
260
|
+
return requirement.filters.flatMap((value) => {
|
|
261
|
+
const valueIndex = question.indexOf(value.toLowerCase());
|
|
262
|
+
if (valueIndex < 0)
|
|
263
|
+
return [];
|
|
264
|
+
const candidates = requirement.dimensions
|
|
265
|
+
.map((dimension) => ({
|
|
266
|
+
dimension,
|
|
267
|
+
index: question.indexOf(dimension.toLowerCase(), valueIndex + value.length),
|
|
268
|
+
}))
|
|
269
|
+
.filter((candidate) => candidate.index >= 0 && candidate.index - valueIndex <= 64)
|
|
270
|
+
.sort((left, right) => left.index - right.index);
|
|
271
|
+
const dimension = inferFilterFieldFromQuestion(requirement.question, value) ?? candidates[0]?.dimension;
|
|
272
|
+
if (!dimension)
|
|
273
|
+
return [];
|
|
274
|
+
return [{
|
|
275
|
+
field: slugify(dimension).replace(/-/g, "_"),
|
|
276
|
+
operator: "=",
|
|
277
|
+
value,
|
|
278
|
+
}];
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function inferFilterFieldFromQuestion(question, value) {
|
|
282
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
283
|
+
const dimensionWord = /(category|type|segment|status|region|country|channel|customer|product|team|season|date|period)$/i;
|
|
284
|
+
const after = new RegExp(`${escaped}\\s+([a-z0-9_ -]{2,40})$`, "i").exec(question.trim())?.[1]?.trim();
|
|
285
|
+
if (after && dimensionWord.test(after))
|
|
286
|
+
return after;
|
|
287
|
+
const before = new RegExp(`([a-z0-9_ -]{2,40})\\s+${escaped}(?:\\s|$)`, "i").exec(question)?.[1]?.trim();
|
|
288
|
+
const beforeTail = before?.split(/\s+/).slice(-2).join(" ");
|
|
289
|
+
return beforeTail && dimensionWord.test(beforeTail) ? beforeTail : undefined;
|
|
290
|
+
}
|
|
199
291
|
export function planAppFromPrompt(input) {
|
|
200
292
|
const prompt = input.prompt.trim();
|
|
201
293
|
if (!prompt)
|
|
@@ -208,17 +300,22 @@ export function planAppFromPrompt(input) {
|
|
|
208
300
|
const maxCertifiedTiles = input.maxCertifiedTiles ?? 4;
|
|
209
301
|
const targetCertifiedTiles = Math.max(maxCertifiedTiles, input.preferredBlockIds?.length ?? 0);
|
|
210
302
|
const preferredNodes = findPreferredCertifiedBlockNodes(input.kg, input.preferredBlockIds ?? []);
|
|
211
|
-
const
|
|
212
|
-
const
|
|
303
|
+
const hasExplicitSelection = (input.preferredBlockIds?.length ?? 0) > 0;
|
|
304
|
+
const matchedNodes = hasExplicitSelection ? [] : findCertifiedBlockNodes(input.kg, prompt, domain, Math.max(targetCertifiedTiles * 3, preferredNodes.length));
|
|
305
|
+
// App Studio source review is an exact allow-list. Once the author chooses
|
|
306
|
+
// blocks, do not silently reintroduce an AI-ranked source they removed.
|
|
307
|
+
const certifiedNodes = dedupeCertifiedBlockNodes(hasExplicitSelection ? preferredNodes : mergeCertifiedBlockNodes(preferredNodes, matchedNodes), new Set(preferredNodes.map((node) => node.nodeId))).slice(0, targetCertifiedTiles);
|
|
213
308
|
const contextNodes = findCertifiedContextNodes(input.kg, prompt, domain, 6);
|
|
214
309
|
// Bind the global filter bar to what the certified tiles actually accept (their
|
|
215
310
|
// declared allowedFilters), not prompt words — falling back to prompt inference
|
|
216
311
|
// only when no tile declares any filter. Keeps the dashboard's filters dynamic.
|
|
217
312
|
const promptFilters = inferFilters(prompt);
|
|
218
313
|
const blockFilters = filtersFromCertifiedNodes(certifiedNodes);
|
|
219
|
-
|
|
314
|
+
let filters = blockFilters.length > 0 ? mergeAppFilters(blockFilters, promptFilters) : promptFilters;
|
|
220
315
|
const candidateCertifiedTiles = certifiedNodes.map((node, index) => tileFromCertifiedNode(node, index, analysisIntent, filters));
|
|
221
|
-
const requirements = appRequirementsForPrompt(prompt);
|
|
316
|
+
const requirements = appRequirementsForPrompt(prompt, certifiedNodes);
|
|
317
|
+
const allowSemanticFallback = input.allowSemanticQueries !== false
|
|
318
|
+
&& !promptExplicitlyUsesNamedCertifiedBlocks(prompt, certifiedNodes);
|
|
222
319
|
const coverage = [];
|
|
223
320
|
const selectedCertifiedIds = new Set();
|
|
224
321
|
const semanticTiles = [];
|
|
@@ -238,7 +335,7 @@ export function planAppFromPrompt(input) {
|
|
|
238
335
|
});
|
|
239
336
|
continue;
|
|
240
337
|
}
|
|
241
|
-
const semanticTile =
|
|
338
|
+
const semanticTile = !allowSemanticFallback
|
|
242
339
|
? null
|
|
243
340
|
: semanticTileForRequirement(input.kg, requirement, domain, input.snapshotId);
|
|
244
341
|
if (semanticTile) {
|
|
@@ -263,6 +360,15 @@ export function planAppFromPrompt(input) {
|
|
|
263
360
|
});
|
|
264
361
|
}
|
|
265
362
|
}
|
|
363
|
+
const semanticFilters = semanticTiles.flatMap((tile) => (tile.semantic?.filters ?? []).map((filter) => ({
|
|
364
|
+
id: slugify(filter.field),
|
|
365
|
+
label: titleCase(filter.field.replace(/[_.]+/g, " ")),
|
|
366
|
+
type: "select",
|
|
367
|
+
default: filter.value,
|
|
368
|
+
options: [String(filter.value)],
|
|
369
|
+
bindsTo: filter.field,
|
|
370
|
+
})));
|
|
371
|
+
filters = uniquePlanFilters([...filters, ...semanticFilters]);
|
|
266
372
|
// Keep compatible certified context only. For legacy unshaped prompts, retain
|
|
267
373
|
// the best certified candidates so existing simple App generation stays useful.
|
|
268
374
|
// Requirement coverage is strict, while explicitly discovered certified
|
|
@@ -398,6 +504,21 @@ export function planAppFromPrompt(input) {
|
|
|
398
504
|
},
|
|
399
505
|
};
|
|
400
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* A user who explicitly names one or more certified blocks has selected the
|
|
509
|
+
* analytical contracts they want to use. Keep uncovered requirements visible
|
|
510
|
+
* instead of silently broadening that request with semantic fallbacks. Generic
|
|
511
|
+
* "governed sources" requests still retain certified-first semantic fallback.
|
|
512
|
+
*/
|
|
513
|
+
function promptExplicitlyUsesNamedCertifiedBlocks(prompt, nodes) {
|
|
514
|
+
if (!/\bcertified\b/i.test(prompt) || !/\bblocks?\b/i.test(prompt))
|
|
515
|
+
return false;
|
|
516
|
+
const normalizedPrompt = canonicalRequirementText(prompt);
|
|
517
|
+
return nodes.some((node) => {
|
|
518
|
+
const normalizedName = canonicalRequirementText(node.name).trim();
|
|
519
|
+
return normalizedName.split(/\s+/).length >= 2 && normalizedPrompt.includes(` ${normalizedName} `);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
401
522
|
export function validateAppPlan(plan, kg) {
|
|
402
523
|
const issues = [];
|
|
403
524
|
let certifiedTiles = 0;
|
|
@@ -575,43 +696,52 @@ export function generateAppFromPlan(projectRoot, plan, kg, options = {}) {
|
|
|
575
696
|
? { copilot: { suggestedQuestions: options.copilotQuestions.slice(0, 8) } }
|
|
576
697
|
: {}),
|
|
577
698
|
};
|
|
578
|
-
const dashboards = plan.pages.map((page, pageIndex) =>
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
:
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
699
|
+
const dashboards = plan.pages.map((page, pageIndex) => {
|
|
700
|
+
const items = options.narration && pageIndex === 0
|
|
701
|
+
? buildStoryLayoutItems(page.tiles, options.narration)
|
|
702
|
+
: buildLayoutItems(page.tiles);
|
|
703
|
+
return {
|
|
704
|
+
version: 2,
|
|
705
|
+
id: page.id,
|
|
706
|
+
...(options.narration && pageIndex === 0
|
|
707
|
+
? { sections: buildStorySections(page.tiles, options.narration) }
|
|
708
|
+
: {}),
|
|
709
|
+
...(pageIndex === 0 ? { story: plan.storyEvidencePlan } : {}),
|
|
710
|
+
metadata: {
|
|
711
|
+
title: page.title,
|
|
712
|
+
description: page.description ?? plan.planning.displayStrategy,
|
|
713
|
+
domain: plan.domain,
|
|
714
|
+
audience: plan.audience,
|
|
715
|
+
visibility: "private",
|
|
716
|
+
lifecycle: "draft",
|
|
717
|
+
tags: plan.tags,
|
|
718
|
+
businessOutcome: plan.planning.normalizedGoal,
|
|
719
|
+
businessOwner: plan.owner,
|
|
720
|
+
decisionUse: plan.planning.displayStrategy,
|
|
721
|
+
reviewCadence: inferReviewCadence(plan.prompt),
|
|
722
|
+
caveats: plan.caveats,
|
|
723
|
+
},
|
|
724
|
+
filters: page.filters.map((filter) => ({
|
|
725
|
+
id: filter.id,
|
|
726
|
+
label: filter.label,
|
|
727
|
+
type: filter.type,
|
|
728
|
+
default: filter.default,
|
|
729
|
+
...(filter.options?.length ? { options: filter.options } : {}),
|
|
730
|
+
bindsTo: filter.bindsTo,
|
|
731
|
+
...(filter.bindsTo ? { field: { name: filter.bindsTo } } : {}),
|
|
732
|
+
...(filter.options?.length
|
|
733
|
+
? { optionSource: { mode: "static" } }
|
|
734
|
+
: {}),
|
|
735
|
+
})),
|
|
736
|
+
layout: {
|
|
737
|
+
kind: "grid",
|
|
738
|
+
cols: 12,
|
|
739
|
+
rowHeight: 80,
|
|
740
|
+
items,
|
|
741
|
+
responsive: responsiveLayoutsForItems(items),
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
});
|
|
615
745
|
const appValidation = parseAppDocument(JSON.stringify(app), join(appDir, "dql.app.json"));
|
|
616
746
|
if (appValidation.errors.length > 0 || !appValidation.document) {
|
|
617
747
|
throw new Error(`Generated app is invalid: ${appValidation.errors.map((e) => e.message).join("; ")}`);
|
|
@@ -640,10 +770,30 @@ export function generateAppFromPlan(projectRoot, plan, kg, options = {}) {
|
|
|
640
770
|
].map((path) => relative(projectRoot, path)),
|
|
641
771
|
};
|
|
642
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* Compile a validated AppPlan into typed App/dashboard documents without
|
|
775
|
+
* touching the user's project. App Studio uses this for AI proposal diffs;
|
|
776
|
+
* only the later Publish to Project service writes canonical App source.
|
|
777
|
+
*/
|
|
778
|
+
export function materializeAppPlanDocuments(plan, kg, options = {}) {
|
|
779
|
+
const scratchRoot = mkdtempSync(join(tmpdir(), "dql-app-plan-"));
|
|
780
|
+
try {
|
|
781
|
+
const generated = generateAppFromPlan(scratchRoot, plan, kg, { ...options, overwrite: true });
|
|
782
|
+
return { app: generated.app, dashboards: generated.dashboards };
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
rmSync(scratchRoot, { recursive: true, force: true });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
643
788
|
function findCertifiedBlockNodes(kg, prompt, domain, limit) {
|
|
644
789
|
const hits = kg.search({
|
|
645
790
|
query: prompt,
|
|
646
|
-
domain
|
|
791
|
+
// `general` is the Studio's initial, unscoped page domain. Treating it as
|
|
792
|
+
// a literal KG domain hid every real project block from AI-created drafts
|
|
793
|
+
// until the author manually chose a domain. An unscoped draft must search
|
|
794
|
+
// the whole governed catalog; a concrete domain remains a hard preference
|
|
795
|
+
// with the existing cross-domain fallback below.
|
|
796
|
+
domain: domain === "general" ? undefined : domain,
|
|
647
797
|
kinds: ["block"],
|
|
648
798
|
limit: limit * 3,
|
|
649
799
|
});
|
|
@@ -716,6 +866,14 @@ function dedupeCertifiedBlockNodes(nodes, preferredNodeIds = new Set()) {
|
|
|
716
866
|
continue;
|
|
717
867
|
}
|
|
718
868
|
const current = selected[duplicateIndex];
|
|
869
|
+
// An explicit App Studio selection is author intent, not a search result.
|
|
870
|
+
// Keep both chosen blocks even when their business/topic fingerprints are
|
|
871
|
+
// similar; automatic discovery may still dedupe everything else.
|
|
872
|
+
if (preferredNodeIds.has(current.nodeId) && preferredNodeIds.has(node.nodeId)) {
|
|
873
|
+
signatureIndex.set(node.nodeId, selected.length);
|
|
874
|
+
selected.push(node);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
719
877
|
if (certifiedBlockSelectionScore(node, preferredNodeIds) > certifiedBlockSelectionScore(current, preferredNodeIds)) {
|
|
720
878
|
selected[duplicateIndex] = node;
|
|
721
879
|
signatureIndex.set(node.nodeId, duplicateIndex);
|
|
@@ -983,7 +1141,7 @@ function displayForCertifiedNode(node, viz, index, intent) {
|
|
|
983
1141
|
followUpActions,
|
|
984
1142
|
rationale,
|
|
985
1143
|
genUi: buildGenUiContract({
|
|
986
|
-
title: node.name,
|
|
1144
|
+
title: humanTileTitle(node.name, node.description),
|
|
987
1145
|
role,
|
|
988
1146
|
viz,
|
|
989
1147
|
text,
|
|
@@ -1166,7 +1324,7 @@ function tileFromCertifiedNode(node, index, intent, globalFilters = []) {
|
|
|
1166
1324
|
const parameterBindings = parameterBindingsForCertifiedNode(node, filterBindings);
|
|
1167
1325
|
return {
|
|
1168
1326
|
id: slugify(blockId) || `certified-${index + 1}`,
|
|
1169
|
-
title: node.
|
|
1327
|
+
title: humanTileTitle(blockId, node.description),
|
|
1170
1328
|
kind: "certified_block",
|
|
1171
1329
|
description: node.description,
|
|
1172
1330
|
blockId,
|
|
@@ -1453,6 +1611,32 @@ function buildLayoutItems(tiles) {
|
|
|
1453
1611
|
return item;
|
|
1454
1612
|
});
|
|
1455
1613
|
}
|
|
1614
|
+
function responsiveLayoutsForItems(items) {
|
|
1615
|
+
return {
|
|
1616
|
+
wide: { kind: "grid", cols: 12, rowHeight: 80, items },
|
|
1617
|
+
medium: { kind: "grid", cols: 6, rowHeight: 80, items: projectItemsToColumns(items, 6) },
|
|
1618
|
+
narrow: { kind: "grid", cols: 1, rowHeight: 80, items: projectItemsToColumns(items, 1) },
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function projectItemsToColumns(items, cols) {
|
|
1622
|
+
let x = 0;
|
|
1623
|
+
let y = 0;
|
|
1624
|
+
let rowHeight = 0;
|
|
1625
|
+
return [...items]
|
|
1626
|
+
.sort((left, right) => left.y - right.y || left.x - right.x)
|
|
1627
|
+
.map((item) => {
|
|
1628
|
+
const width = cols === 1 ? 1 : Math.min(cols, Math.max(2, Math.ceil(item.w / 2)));
|
|
1629
|
+
if (x + width > cols) {
|
|
1630
|
+
x = 0;
|
|
1631
|
+
y += rowHeight || item.h;
|
|
1632
|
+
rowHeight = 0;
|
|
1633
|
+
}
|
|
1634
|
+
const projected = { ...item, x, y, w: width };
|
|
1635
|
+
x += width;
|
|
1636
|
+
rowHeight = Math.max(rowHeight, item.h);
|
|
1637
|
+
return projected;
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1456
1640
|
function markdownForGeneratedPlanTile(tile) {
|
|
1457
1641
|
const reviewTasks = tile.reviewTasks?.length
|
|
1458
1642
|
? tile.reviewTasks.map((task) => `- ${task}`).join("\n")
|
|
@@ -1795,12 +1979,15 @@ function uniquePlanFilters(filters) {
|
|
|
1795
1979
|
}
|
|
1796
1980
|
const APP_FILTER_TIME_RE = /(_at$|_date$|_time$|_ts$|^date$|^month$|^week$|^day$|ordered_at|created)/i;
|
|
1797
1981
|
const APP_FILTER_NUMBER_RE = /(top[_-]?n|limit|count|amount|year|score|rank)/i;
|
|
1982
|
+
const APP_FILTER_SEARCH_RE = /(_name$|_id$|^name$|search|email)/i;
|
|
1798
1983
|
/** Map a block filter column to the dashboard control the runtime filter engine renders. */
|
|
1799
1984
|
function controlTypeForColumn(column) {
|
|
1800
1985
|
if (APP_FILTER_TIME_RE.test(column))
|
|
1801
1986
|
return "daterange";
|
|
1802
1987
|
if (APP_FILTER_NUMBER_RE.test(column))
|
|
1803
1988
|
return "number";
|
|
1989
|
+
if (APP_FILTER_SEARCH_RE.test(column))
|
|
1990
|
+
return "search";
|
|
1804
1991
|
return "select"; // categorical → dropdown (options filled by /api/dashboard/filter-options)
|
|
1805
1992
|
}
|
|
1806
1993
|
/**
|
|
@@ -1961,6 +2148,29 @@ function cleanFieldHints(input) {
|
|
|
1961
2148
|
}
|
|
1962
2149
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
1963
2150
|
}
|
|
2151
|
+
/** Longest tile title we will lift out of a block description. */
|
|
2152
|
+
const MAX_DERIVED_TILE_TITLE = 48;
|
|
2153
|
+
/**
|
|
2154
|
+
* A tile title a stakeholder can read.
|
|
2155
|
+
*
|
|
2156
|
+
* A certified block's KG node name is its id — `monthly_revenue` — so using it
|
|
2157
|
+
* directly labelled every AI-built tile with an identifier, which also made the
|
|
2158
|
+
* titles look like system fields rather than something the author may rename.
|
|
2159
|
+
* Prefer the lead clause of the block's own description, which authors already
|
|
2160
|
+
* write as a summary ("Monthly gross order revenue and order count."), and fall
|
|
2161
|
+
* back to the de-slugged id when that clause is missing or too long to sit in a
|
|
2162
|
+
* tile header.
|
|
2163
|
+
*/
|
|
2164
|
+
export function humanTileTitle(name, description) {
|
|
2165
|
+
const lead = (description ?? "").split(/[.;]/)[0]?.trim() ?? "";
|
|
2166
|
+
if (lead.length >= 3 && lead.length <= MAX_DERIVED_TILE_TITLE && /\s/.test(lead)) {
|
|
2167
|
+
return lead.charAt(0).toUpperCase() + lead.slice(1);
|
|
2168
|
+
}
|
|
2169
|
+
const words = name.replace(/[_-]+/g, " ").trim();
|
|
2170
|
+
if (!words)
|
|
2171
|
+
return name;
|
|
2172
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
2173
|
+
}
|
|
1964
2174
|
function titleCase(value) {
|
|
1965
2175
|
return value
|
|
1966
2176
|
.split(/\s+/)
|