@duckcodeailabs/dql-cli 1.12.10 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps-api.d.ts +151 -3
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +675 -97
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/AgentAnswerCard-Cp0eh6zU.js +5 -0
- package/dist/assets/dql-notebook/assets/{AgentLogPage-uvVs284-.js → AgentLogPage-CBGEoGmU.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-DEUS19WF.js → AiBuildDialog-CwAD3tXE.js} +1 -1
- package/dist/assets/dql-notebook/assets/AiBuildResult-CyqTuQe5.js +33 -0
- package/dist/assets/dql-notebook/assets/AiSidePanel-DjB8PWHG.js +6 -0
- package/dist/assets/dql-notebook/assets/AnalyticsHome-B7BkvA-R.js +6 -0
- package/dist/assets/dql-notebook/assets/AppsView-CUrqtEGC.js +4583 -0
- package/dist/assets/dql-notebook/assets/BlockStudio-Bl0aGQJR.js +60 -0
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-D8FIgrkF.js → BusinessArtifactView-C4SZVNW-.js} +1 -1
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-B0uxitnx.js +22 -0
- package/dist/assets/dql-notebook/assets/GitPage-BfVD4tVq.js +7 -0
- package/dist/assets/dql-notebook/assets/GlobalAiRail-DUwdy3fB.js +1 -0
- package/dist/assets/dql-notebook/assets/GovernedContextPage-D9f26ZFb.js +7 -0
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-Bc5pLakt.js → HelpDocsPage-BDPNVot1.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HomePage-DQSo-K4y.js → HomePage-DiOnRhEG.js} +9 -4
- package/dist/assets/dql-notebook/assets/LineageDAG-BiUFX2KL.js +1 -0
- package/dist/assets/dql-notebook/assets/{LineageDetailView-CZEx2r1H.js → LineageDetailView-CfbSvlPJ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-P7ZdOW8-.js → LineageDrawer-CgTScaGB.js} +1 -1
- package/dist/assets/dql-notebook/assets/LineagePathBreadcrumb-D7ZA7k68.js +14 -0
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-Ctp_yFh8.js → MiniLineageGraph-GkD4y17x.js} +1 -1
- package/dist/assets/dql-notebook/assets/NewBlockModal-B9TfRjt7.js +1 -0
- package/dist/assets/dql-notebook/assets/NewNotebookModal-BbO9NKUl.js +46 -0
- package/dist/assets/dql-notebook/assets/NotebookEditor-CuMpvXR9.js +200 -0
- package/dist/assets/dql-notebook/assets/ReadinessPage-BROJ9YBM.js +1 -0
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-DX2f53J2.js → SetupOnboarding-BCcZlpuQ.js} +2 -2
- package/dist/assets/dql-notebook/assets/{TrustBadge-DPnKzRSY.js → TrustBadge-vQQh8d0D.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Dj4AXAhT.js +83 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-Bm9oByWA.js → answer-to-notebook-DuqCcDDl.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-D2FbLWqh.js → arrow-left-CMe1kkOL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-OYItAZ-s.js → arrow-right-DvB_DONr.js} +2 -2
- package/dist/assets/dql-notebook/assets/{book-open-text-BnnirfAc.js → book-open-text-CJ3NAvrs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{bot-BJNxPYFm.js → bot-CV8DYRrt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{chevron-up-DM5LX-26.js → chevron-up-HxU1l85K.js} +2 -2
- package/dist/assets/dql-notebook/assets/{circle-x-Dic_-4tF.js → circle-x-BCgtha1W.js} +1 -1
- package/dist/assets/dql-notebook/assets/{codemirror-1_I9gm6Z.js → codemirror-DXt7RW72.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-C9zxjHHX.js → dagre.esm-GKp7qoUL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-DHxAcoCE.js → external-link-C-PXokWV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-02I-EOjL.js → grip-vertical-B4EHbGXW.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-1mIC_XX4.js +634 -0
- package/dist/assets/dql-notebook/assets/{link-2-Delh-lai.js → link-2-BLx-goet.js} +2 -2
- package/dist/assets/dql-notebook/assets/{list-tree-BrIZg6l6.js → list-tree-BRFpEmvV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{maximize-2-CQOA-bbN.js → maximize-2-CCd5uRIZ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CUDBPkwe.js → minimize-2-CDwSCGlO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-CwhZfkKJ.js → panel-right-open-CunA1xO7.js} +2 -2
- package/dist/assets/dql-notebook/assets/{pencil-Cf6-MgpT.js → pencil-D_I14Wyk.js} +2 -2
- package/dist/assets/dql-notebook/assets/{rotate-ccw-DndbJAiz.js → rotate-ccw-DOrfTZk8.js} +2 -2
- package/dist/assets/dql-notebook/assets/{semantic-fields-DwKPY_mU.js → semantic-fields-AfQvjt5R.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-DvcMC9wZ.js → star-DDhv4e8M.js} +2 -2
- package/dist/assets/dql-notebook/assets/{triangle-alert-DUf4Ook0.js → triangle-alert-2ZXx8wAu.js} +1 -1
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-3rzkZrqc.js +18 -0
- package/dist/assets/dql-notebook/assets/{user-round-DSt0xU3K.js → user-round-BA074kiQ.js} +3 -3
- package/dist/assets/dql-notebook/assets/{wand-sparkles-ro8Cq0ro.js → wand-sparkles-CEYHLt9b.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-DVJXehjU.js → workflow-CZtyrcR-.js} +2 -2
- package/dist/assets/dql-notebook/assets/{wrench-DxUnoHtz.js → wrench-Ong23-wu.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-9Gtb3EAd.js → x-CSo1M3tu.js} +2 -2
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +4 -3
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +13 -10
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/local-operation-coordinator.d.ts +59 -0
- package/dist/local-operation-coordinator.d.ts.map +1 -0
- package/dist/local-operation-coordinator.js +256 -0
- package/dist/local-operation-coordinator.js.map +1 -0
- package/dist/local-runtime.d.ts +89 -2
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +2158 -461
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +12 -11
- package/dist/project-refresh-coordinator.d.ts +19 -0
- package/dist/project-refresh-coordinator.d.ts.map +1 -0
- package/dist/project-refresh-coordinator.js +134 -0
- package/dist/project-refresh-coordinator.js.map +1 -0
- package/dist/project-refresh-worker.d.ts +13 -0
- package/dist/project-refresh-worker.d.ts.map +1 -0
- package/dist/project-refresh-worker.js +58 -0
- package/dist/project-refresh-worker.js.map +1 -0
- package/package.json +13 -11
- package/dist/assets/dql-notebook/assets/AgentAnswerCard-Dp4odOLE.js +0 -5
- package/dist/assets/dql-notebook/assets/AiBuildResult-B9KmBRsh.js +0 -33
- package/dist/assets/dql-notebook/assets/AiSidePanel-C7BZVSkM.js +0 -6
- package/dist/assets/dql-notebook/assets/AnalyticsHome-4TgtH6SB.js +0 -6
- package/dist/assets/dql-notebook/assets/AppsView-CnfNtk5W.js +0 -4466
- package/dist/assets/dql-notebook/assets/BlockStudio-DUXN3MKz.js +0 -60
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-DR8hTZ95.js +0 -22
- package/dist/assets/dql-notebook/assets/GitPage-BS2sm9zh.js +0 -7
- package/dist/assets/dql-notebook/assets/GlobalAiRail-B_MZsB0z.js +0 -1
- package/dist/assets/dql-notebook/assets/GovernedContextPage-CNM5UCAH.js +0 -7
- package/dist/assets/dql-notebook/assets/LineageDAG-RdpWmGmR.js +0 -1
- package/dist/assets/dql-notebook/assets/LineagePathBreadcrumb-DTUMFkJ8.js +0 -14
- package/dist/assets/dql-notebook/assets/NewBlockModal-BLtjuWGy.js +0 -1
- package/dist/assets/dql-notebook/assets/NewNotebookModal-xgaKB2kI.js +0 -46
- package/dist/assets/dql-notebook/assets/NotebookEditor-D6RgLQw-.js +0 -202
- package/dist/assets/dql-notebook/assets/ReadinessPage-C8hATaT_.js +0 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-CwxQ4T19.js +0 -83
- package/dist/assets/dql-notebook/assets/index-Ca3UeUWI.js +0 -644
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-DbdBi0oH.js +0 -18
package/dist/apps-api.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `/api/persona`. Designed to be invoked from `local-runtime.ts`'s request
|
|
4
4
|
* dispatcher — returns `true` if the request was handled, `false` otherwise.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { join, dirname, relative, basename } from 'node:path';
|
|
9
9
|
import { loadAppDocument, findAppDocuments, loadDashboardDocument, findDashboardsForApp, parseAppDocument, parseDashboardDocument, normalizeDqlArtifactReference, suggestAppId, } from '@duckcodeailabs/dql-core';
|
|
@@ -72,6 +72,41 @@ export async function handleAppsApi(ctx) {
|
|
|
72
72
|
return true;
|
|
73
73
|
}
|
|
74
74
|
// Two-phase AI build: propose (plan + confirmable content list, no files) …
|
|
75
|
+
if (req.method === 'POST' && path === '/api/apps/ai-builds/propose-background') {
|
|
76
|
+
try {
|
|
77
|
+
const body = await readJson(req);
|
|
78
|
+
if (!ctx.queueOperation) {
|
|
79
|
+
sendJson(res, 501, { ok: false, error: 'Background App builds are unavailable in this runtime.' });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const sessionId = appAiBuildSessionId(body.sessionId);
|
|
83
|
+
const operation = ctx.queueOperation({
|
|
84
|
+
type: 'app_ai_build',
|
|
85
|
+
scope: `app-build:${sessionId}`,
|
|
86
|
+
resourceRevision: createHash('sha256').update(JSON.stringify(body)).digest('hex'),
|
|
87
|
+
message: body.existingAppId ? 'App page proposal queued.' : 'App proposal queued.',
|
|
88
|
+
cancellable: true,
|
|
89
|
+
}, async (signal, report) => {
|
|
90
|
+
report({ phase: 'planning', progress: 8, message: 'Planning the governed App proposal.' });
|
|
91
|
+
if (signal.aborted)
|
|
92
|
+
throw Object.assign(new Error('App build cancelled.'), { code: 'OPERATION_CANCELLED' });
|
|
93
|
+
const session = await proposeAppAiBuild(projectRoot, { ...body, sessionId }, {
|
|
94
|
+
generateGovernedAnswer: ctx.generateGovernedAnswer,
|
|
95
|
+
});
|
|
96
|
+
if (signal.aborted)
|
|
97
|
+
throw Object.assign(new Error('App build cancelled.'), { code: 'OPERATION_CANCELLED' });
|
|
98
|
+
if (session.status === 'error')
|
|
99
|
+
throw Object.assign(new Error(session.error ?? 'App proposal failed.'), { code: 'APP_BUILD_FAILED' });
|
|
100
|
+
report({ phase: 'review_ready', progress: 96, message: 'Build Brief ready for review.' });
|
|
101
|
+
return { sessionId: session.id, status: session.status, appId: session.appId, dashboardId: session.dashboardId };
|
|
102
|
+
});
|
|
103
|
+
sendJson(res, 202, { ok: true, operation, sessionId });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
75
110
|
if (req.method === 'POST' && path === '/api/apps/ai-builds/propose') {
|
|
76
111
|
try {
|
|
77
112
|
const body = await readJson(req);
|
|
@@ -169,6 +204,37 @@ export async function handleAppsApi(ctx) {
|
|
|
169
204
|
}
|
|
170
205
|
return true;
|
|
171
206
|
}
|
|
207
|
+
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)\/approve-semantic$/);
|
|
208
|
+
if (m && req.method === 'POST') {
|
|
209
|
+
const appId = decodeURIComponent(m[1]);
|
|
210
|
+
const dashboardId = decodeURIComponent(m[2]);
|
|
211
|
+
try {
|
|
212
|
+
const body = await readJson(req);
|
|
213
|
+
const runId = cleanString(body.runId);
|
|
214
|
+
const tileIds = unique((body.tileIds ?? []).map(cleanString).filter(Boolean));
|
|
215
|
+
if (!runId || tileIds.length === 0 || !ctx.verifyDashboardRun) {
|
|
216
|
+
sendJson(res, 400, { ok: false, error: 'A successful dashboard run and semantic tile selection are required.' });
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const verified = ctx.verifyDashboardRun({ runId, appId, dashboardId, tileIds });
|
|
220
|
+
if (!verified.ok) {
|
|
221
|
+
sendJson(res, 409, { ok: false, error: verified.error });
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const result = approveAppSemanticTiles(projectRoot, appId, dashboardId, {
|
|
225
|
+
tileIds,
|
|
226
|
+
runId,
|
|
227
|
+
snapshotId: verified.snapshotId,
|
|
228
|
+
expectedDashboardFingerprint: cleanString(body.expectedDashboardFingerprint),
|
|
229
|
+
reviewer: cleanString(body.reviewer) || 'local-reviewer',
|
|
230
|
+
});
|
|
231
|
+
sendJson(res, result.ok ? 200 : 409, result);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
172
238
|
m = path.match(/^\/api\/apps\/([^/]+)\/editor\/catalog$/);
|
|
173
239
|
if (m && req.method === 'GET') {
|
|
174
240
|
const appId = decodeURIComponent(m[1]);
|
|
@@ -645,6 +711,22 @@ export async function handleAppsApi(ctx) {
|
|
|
645
711
|
}
|
|
646
712
|
// /api/apps/:id — single App with dashboards summary
|
|
647
713
|
m = path.match(/^\/api\/apps\/([^/]+)$/);
|
|
714
|
+
if (m && req.method === 'DELETE') {
|
|
715
|
+
const id = decodeURIComponent(m[1]);
|
|
716
|
+
try {
|
|
717
|
+
const result = deleteAppPackage(projectRoot, id);
|
|
718
|
+
if (!result.ok) {
|
|
719
|
+
sendJson(res, result.status, { ok: false, error: result.error });
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
ctx.scheduleProjectRefresh?.('app-deleted');
|
|
723
|
+
sendJson(res, 200, result);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
727
|
+
}
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
648
730
|
if (m && req.method === 'GET') {
|
|
649
731
|
const id = m[1];
|
|
650
732
|
const result = loadAppById(projectRoot, id);
|
|
@@ -764,6 +846,7 @@ function collectAppsList(projectRoot) {
|
|
|
764
846
|
status: document.lifecycle === 'review' ? 'review' : dashboards.length > 0 ? 'ready' : 'empty',
|
|
765
847
|
storage: document.visibility === 'private' ? 'mine' : document.visibility === 'template' ? 'template' : 'shared',
|
|
766
848
|
visibility: document.visibility ?? 'shared',
|
|
849
|
+
publicationIntent: document.publicationIntent ?? (document.visibility === 'private' ? 'personal' : 'shared_project'),
|
|
767
850
|
owners: document.owners,
|
|
768
851
|
tags: document.tags ?? [],
|
|
769
852
|
members: document.members.length,
|
|
@@ -975,12 +1058,12 @@ export async function generateAppPackage(projectRoot, input) {
|
|
|
975
1058
|
owner: cleanString(input.owner) || undefined,
|
|
976
1059
|
preferredBlockIds: selectedBlockIds,
|
|
977
1060
|
plannerMode: input.plannerMode === 'ai_assisted' ? 'ai_assisted' : 'deterministic',
|
|
1061
|
+
mode: input.mode === 'personal' ? 'personal' : 'stakeholder',
|
|
978
1062
|
});
|
|
979
1063
|
const validation = validateAppPlan(plan, kg);
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
if (certifiedTiles === 0) {
|
|
1064
|
+
const governedTiles = plan.pages.flatMap((page) => page.tiles)
|
|
1065
|
+
.filter((tile) => tile.kind === 'certified_block' || tile.kind === 'semantic_query').length;
|
|
1066
|
+
if (governedTiles === 0) {
|
|
984
1067
|
return {
|
|
985
1068
|
ok: false,
|
|
986
1069
|
error: appBuildBlockedMessage(validation, plan),
|
|
@@ -1009,7 +1092,7 @@ export async function generateAppPackage(projectRoot, input) {
|
|
|
1009
1092
|
}
|
|
1010
1093
|
export async function createAppAiBuildSession(projectRoot, input) {
|
|
1011
1094
|
const now = new Date().toISOString();
|
|
1012
|
-
const id =
|
|
1095
|
+
const id = appAiBuildSessionId(input.sessionId);
|
|
1013
1096
|
const selectedBlockIds = unique((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean));
|
|
1014
1097
|
const base = {
|
|
1015
1098
|
id,
|
|
@@ -1027,6 +1110,7 @@ export async function createAppAiBuildSession(projectRoot, input) {
|
|
|
1027
1110
|
notebookPath: cleanString(input.notebookPath) || undefined,
|
|
1028
1111
|
existingAppId: cleanString(input.existingAppId) || undefined,
|
|
1029
1112
|
mode: input.mode === 'personal' ? 'personal' : 'stakeholder',
|
|
1113
|
+
exploreGaps: input.mode === 'personal' && input.exploreGaps === true,
|
|
1030
1114
|
selectedBlockIds,
|
|
1031
1115
|
},
|
|
1032
1116
|
};
|
|
@@ -1093,7 +1177,18 @@ function buildAppProposal(plan) {
|
|
|
1093
1177
|
description: tile.description,
|
|
1094
1178
|
blockId: tile.blockId,
|
|
1095
1179
|
viz: tile.viz,
|
|
1180
|
+
allowedVisualizations: tile.display?.genUi.allowedVisualizations ?? [tile.viz, 'table'],
|
|
1096
1181
|
certification: 'certified',
|
|
1182
|
+
sourceClass: 'certified_block',
|
|
1183
|
+
reviewStatus: 'not_required',
|
|
1184
|
+
requirementIds: tile.requirementIds,
|
|
1185
|
+
sourceEvidence: tile.sourceEvidence,
|
|
1186
|
+
preflight: {
|
|
1187
|
+
status: 'passed',
|
|
1188
|
+
snapshotId: plan.snapshotId,
|
|
1189
|
+
sourceFingerprint: tile.sourceNodeId,
|
|
1190
|
+
message: 'Certified source and requirement compatibility were validated against this project snapshot.',
|
|
1191
|
+
},
|
|
1097
1192
|
selectedByDefault: true,
|
|
1098
1193
|
});
|
|
1099
1194
|
}
|
|
@@ -1106,7 +1201,18 @@ function buildAppProposal(plan) {
|
|
|
1106
1201
|
question: tile.title,
|
|
1107
1202
|
semantic: tile.semantic,
|
|
1108
1203
|
viz: tile.viz,
|
|
1204
|
+
allowedVisualizations: tile.display?.genUi.allowedVisualizations ?? [tile.viz, 'table'],
|
|
1109
1205
|
certification: 'reviewed_semantic',
|
|
1206
|
+
sourceClass: 'governed_semantic',
|
|
1207
|
+
reviewStatus: 'required',
|
|
1208
|
+
requirementIds: tile.requirementIds,
|
|
1209
|
+
sourceEvidence: tile.sourceEvidence,
|
|
1210
|
+
preflight: {
|
|
1211
|
+
status: 'review_required',
|
|
1212
|
+
snapshotId: plan.snapshotId,
|
|
1213
|
+
sourceFingerprint: tile.semantic.definitionFingerprint,
|
|
1214
|
+
message: 'Semantic identities are resolved; run and review the result before shared publication.',
|
|
1215
|
+
},
|
|
1110
1216
|
selectedByDefault: true,
|
|
1111
1217
|
});
|
|
1112
1218
|
}
|
|
@@ -1137,6 +1243,10 @@ function buildAppProposal(plan) {
|
|
|
1137
1243
|
gaps.push({ id: `gap_${gaps.length + 1}`, question, reason: 'No certified block covers this question.' });
|
|
1138
1244
|
}
|
|
1139
1245
|
return {
|
|
1246
|
+
intent: {
|
|
1247
|
+
target: plan.publicationIntent,
|
|
1248
|
+
initialVisibility: 'private',
|
|
1249
|
+
},
|
|
1140
1250
|
tiles,
|
|
1141
1251
|
gaps,
|
|
1142
1252
|
followUps: gaps.map((gap) => gap.question).slice(0, 4),
|
|
@@ -1213,6 +1323,11 @@ async function fillProposalGaps(proposal, hooks, maxGeneratedTiles) {
|
|
|
1213
1323
|
remaining.push(gap);
|
|
1214
1324
|
continue;
|
|
1215
1325
|
}
|
|
1326
|
+
const preview = previewFromAgentResult(answer.result);
|
|
1327
|
+
const repair = answer.appBuilderRepair;
|
|
1328
|
+
const unresolvedExecutionError = !preview
|
|
1329
|
+
? cleanString(answer.executionError ?? answer.warehouseFailure?.redactedMessage ?? '')
|
|
1330
|
+
: '';
|
|
1216
1331
|
// Gap-fill tiles are AI-generated by definition (they answer questions the
|
|
1217
1332
|
// certified blocks did NOT cover). They are ALWAYS ai_generated / review-
|
|
1218
1333
|
// required — never auto-certified, even if the governed loop happened to
|
|
@@ -1226,8 +1341,28 @@ async function fillProposalGaps(proposal, hooks, maxGeneratedTiles) {
|
|
|
1226
1341
|
answer: cleanString(answer.answer ?? answer.text ?? '') || undefined,
|
|
1227
1342
|
viz: normalizeGeneratedViz(answer.suggestedViz),
|
|
1228
1343
|
certification: 'ai_generated',
|
|
1229
|
-
|
|
1230
|
-
|
|
1344
|
+
sourceClass: 'exploratory_analysis',
|
|
1345
|
+
reviewStatus: 'required',
|
|
1346
|
+
preflight: {
|
|
1347
|
+
status: preview ? 'passed' : unresolvedExecutionError ? 'blocked' : 'review_required',
|
|
1348
|
+
sourceFingerprint: `sha256:${createHash('sha256').update(sql).digest('hex')}`,
|
|
1349
|
+
receiptId: preview
|
|
1350
|
+
? `preview:${createHash('sha256').update(JSON.stringify(preview)).digest('hex')}`
|
|
1351
|
+
: undefined,
|
|
1352
|
+
message: preview
|
|
1353
|
+
? repair?.status === 'repaired'
|
|
1354
|
+
? 'The shared bounded repair produced a successful preview; the query remains review-required.'
|
|
1355
|
+
: 'A bounded preview ran successfully; business review is still required.'
|
|
1356
|
+
: repair?.status === 'failed'
|
|
1357
|
+
? repair.message
|
|
1358
|
+
: unresolvedExecutionError
|
|
1359
|
+
? 'The generated query failed its bounded preview and could not be selected.'
|
|
1360
|
+
: 'SQL was generated but has no successful preview receipt yet.',
|
|
1361
|
+
},
|
|
1362
|
+
preview,
|
|
1363
|
+
...(repair ? { repair } : {}),
|
|
1364
|
+
...(unresolvedExecutionError ? { error: unresolvedExecutionError } : {}),
|
|
1365
|
+
selectedByDefault: false,
|
|
1231
1366
|
});
|
|
1232
1367
|
}
|
|
1233
1368
|
catch (err) {
|
|
@@ -1246,6 +1381,12 @@ async function fillProposalGaps(proposal, hooks, maxGeneratedTiles) {
|
|
|
1246
1381
|
question: gap.question,
|
|
1247
1382
|
viz: 'table',
|
|
1248
1383
|
certification: 'ai_generated',
|
|
1384
|
+
sourceClass: 'exploratory_analysis',
|
|
1385
|
+
reviewStatus: 'required',
|
|
1386
|
+
preflight: {
|
|
1387
|
+
status: 'blocked',
|
|
1388
|
+
message: 'Exploratory analysis generation failed.',
|
|
1389
|
+
},
|
|
1249
1390
|
error: message,
|
|
1250
1391
|
selectedByDefault: false,
|
|
1251
1392
|
});
|
|
@@ -1267,11 +1408,11 @@ async function fillProposalGaps(proposal, hooks, maxGeneratedTiles) {
|
|
|
1267
1408
|
*/
|
|
1268
1409
|
export async function proposeAppAiBuild(projectRoot, input, hooks) {
|
|
1269
1410
|
const now = new Date().toISOString();
|
|
1270
|
-
const id =
|
|
1411
|
+
const id = appAiBuildSessionId(input.sessionId);
|
|
1271
1412
|
const selectedBlockIds = unique((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean));
|
|
1272
1413
|
const base = {
|
|
1273
1414
|
id,
|
|
1274
|
-
status: '
|
|
1415
|
+
status: 'building',
|
|
1275
1416
|
createdAt: now,
|
|
1276
1417
|
updatedAt: now,
|
|
1277
1418
|
prompt: cleanString(input.prompt),
|
|
@@ -1285,6 +1426,7 @@ export async function proposeAppAiBuild(projectRoot, input, hooks) {
|
|
|
1285
1426
|
notebookPath: cleanString(input.notebookPath) || undefined,
|
|
1286
1427
|
existingAppId: cleanString(input.existingAppId) || undefined,
|
|
1287
1428
|
mode: input.mode === 'personal' ? 'personal' : 'stakeholder',
|
|
1429
|
+
exploreGaps: input.mode === 'personal' && input.exploreGaps === true,
|
|
1288
1430
|
selectedBlockIds,
|
|
1289
1431
|
},
|
|
1290
1432
|
};
|
|
@@ -1293,17 +1435,32 @@ export async function proposeAppAiBuild(projectRoot, input, hooks) {
|
|
|
1293
1435
|
writeAppAiBuildSession(projectRoot, session);
|
|
1294
1436
|
return session;
|
|
1295
1437
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1438
|
+
// Persist the durable identity before planning or provider work begins. The
|
|
1439
|
+
// browser can leave this page immediately and recover the same session later.
|
|
1440
|
+
writeAppAiBuildSession(projectRoot, base);
|
|
1441
|
+
const existingApp = base.inputs.existingAppId ? loadAppById(projectRoot, base.inputs.existingAppId) : null;
|
|
1442
|
+
if (base.inputs.existingAppId && !existingApp) {
|
|
1443
|
+
const session = { ...base, status: 'error', error: `App "${base.inputs.existingAppId}" not found.` };
|
|
1298
1444
|
writeAppAiBuildSession(projectRoot, session);
|
|
1299
1445
|
return session;
|
|
1300
1446
|
}
|
|
1447
|
+
if (existingApp) {
|
|
1448
|
+
// Existing-App page generation inherits server-owned App context. The prompt
|
|
1449
|
+
// controls the new page, never the identity or governance boundary of the App.
|
|
1450
|
+
base.inputs.domain = existingApp.app.domain;
|
|
1451
|
+
base.inputs.audience = existingApp.app.audience;
|
|
1452
|
+
base.inputs.owner = existingApp.app.businessOwner ?? existingApp.app.owners[0];
|
|
1453
|
+
const publicationIntent = existingApp.app.publicationIntent
|
|
1454
|
+
?? (existingApp.app.visibility === 'private' ? 'personal' : 'shared_project');
|
|
1455
|
+
base.inputs.mode = publicationIntent === 'personal' ? 'personal' : 'stakeholder';
|
|
1456
|
+
base.inputs.exploreGaps = base.inputs.mode === 'personal' && input.exploreGaps === true;
|
|
1457
|
+
}
|
|
1301
1458
|
const { KGStore, defaultKgPath, planAppFromPrompt, reindexProject, validateAppPlan, } = await import('@duckcodeailabs/dql-agent');
|
|
1302
1459
|
const kgPath = defaultKgPath(projectRoot);
|
|
1303
1460
|
const index = await reindexProject(projectRoot, { kgPath });
|
|
1304
1461
|
const kg = new KGStore(kgPath);
|
|
1305
1462
|
try {
|
|
1306
|
-
const
|
|
1463
|
+
const planned = planAppFromPrompt({
|
|
1307
1464
|
prompt: base.prompt,
|
|
1308
1465
|
kg,
|
|
1309
1466
|
domain: base.inputs.domain,
|
|
@@ -1311,22 +1468,48 @@ export async function proposeAppAiBuild(projectRoot, input, hooks) {
|
|
|
1311
1468
|
owner: base.inputs.owner,
|
|
1312
1469
|
preferredBlockIds: selectedBlockIds,
|
|
1313
1470
|
plannerMode: input.plannerMode === 'ai_assisted' ? 'ai_assisted' : 'deterministic',
|
|
1314
|
-
mode:
|
|
1471
|
+
mode: base.inputs.mode === 'personal' ? 'personal' : 'stakeholder',
|
|
1315
1472
|
snapshotId: index.kgFingerprint,
|
|
1316
1473
|
});
|
|
1474
|
+
const firstPage = planned.pages[0];
|
|
1475
|
+
if (!firstPage)
|
|
1476
|
+
throw new Error('The App planner did not return a page.');
|
|
1477
|
+
const existingDashboardIds = new Set(existingApp?.dashboards.map((dashboard) => dashboard.id) ?? []);
|
|
1478
|
+
const pageId = existingApp
|
|
1479
|
+
? uniqueAppPageId(slugify(firstPage.title) || 'page', existingDashboardIds)
|
|
1480
|
+
: firstPage.id;
|
|
1481
|
+
const plan = existingApp ? {
|
|
1482
|
+
...planned,
|
|
1483
|
+
appId: existingApp.app.id,
|
|
1484
|
+
name: existingApp.app.name,
|
|
1485
|
+
domain: existingApp.app.domain,
|
|
1486
|
+
audience: existingApp.app.audience ?? planned.audience,
|
|
1487
|
+
owner: existingApp.app.businessOwner ?? existingApp.app.owners[0] ?? planned.owner,
|
|
1488
|
+
publicationIntent: existingApp.app.publicationIntent
|
|
1489
|
+
?? (existingApp.app.visibility === 'private' ? 'personal' : 'shared_project'),
|
|
1490
|
+
pages: [{ ...firstPage, id: pageId }],
|
|
1491
|
+
} : planned;
|
|
1317
1492
|
const validation = validateAppPlan(plan, kg);
|
|
1318
1493
|
const proposal = buildAppProposal(plan);
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
|
|
1494
|
+
// Raw SQL exploration is opt-in, personal-only, preview-bounded, and remains
|
|
1495
|
+
// unselected until the user explicitly accepts it in the Build Brief.
|
|
1496
|
+
if (base.inputs.exploreGaps) {
|
|
1497
|
+
await fillProposalGaps(proposal, hooks, input.maxGeneratedTiles);
|
|
1498
|
+
}
|
|
1499
|
+
const existingAppFingerprint = existingApp ? fingerprintAppDirectory(existingApp.appDir) : undefined;
|
|
1500
|
+
const proposalHash = createHash('sha256')
|
|
1501
|
+
.update(JSON.stringify({ plan, proposal, existingAppFingerprint }))
|
|
1502
|
+
.digest('hex');
|
|
1322
1503
|
const session = {
|
|
1323
1504
|
...base,
|
|
1505
|
+
status: 'proposed',
|
|
1324
1506
|
appId: plan.appId,
|
|
1325
1507
|
dashboardId: plan.pages[0]?.id ?? null,
|
|
1326
1508
|
plan,
|
|
1327
1509
|
validation,
|
|
1328
1510
|
proposal,
|
|
1329
1511
|
snapshotId: index.kgFingerprint,
|
|
1512
|
+
...(existingAppFingerprint ? { existingAppFingerprint } : {}),
|
|
1330
1513
|
proposalHash,
|
|
1331
1514
|
warnings: appBuildWarnings(validation, plan),
|
|
1332
1515
|
reviewTasks: reviewTasksFromPlan(plan),
|
|
@@ -1348,12 +1531,11 @@ export async function proposeAppAiBuild(projectRoot, input, hooks) {
|
|
|
1348
1531
|
}
|
|
1349
1532
|
}
|
|
1350
1533
|
/**
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
* this rewrites it with the extra items — the renderer already badges aiPin tiles.
|
|
1534
|
+
* Materialize explicitly selected exploratory candidates as Git-owned, app-scoped
|
|
1535
|
+
* review DQL. They are never promoted into the shared block library and the
|
|
1536
|
+
* dashboard stores a fingerprinted reference instead of inline SQL.
|
|
1355
1537
|
*/
|
|
1356
|
-
function
|
|
1538
|
+
function attachGeneratedDraftAnalyses(projectRoot, plan, generatedPaths, candidates) {
|
|
1357
1539
|
const dashboardId = plan.pages[0]?.id ?? 'overview';
|
|
1358
1540
|
const dashboardPath = generatedPaths.find((path) => path.endsWith(`${dashboardId}.dqld`))
|
|
1359
1541
|
?? generatedPaths.find((path) => path.endsWith('.dqld'));
|
|
@@ -1363,7 +1545,7 @@ function attachGeneratedTiles(projectRoot, plan, generatedPaths, candidates) {
|
|
|
1363
1545
|
if (!existsSync(absolutePath))
|
|
1364
1546
|
return;
|
|
1365
1547
|
const doc = JSON.parse(readFileSync(absolutePath, 'utf-8'));
|
|
1366
|
-
// Story layout:
|
|
1548
|
+
// Story layout: exploratory tiles live in the review appendix. Classic grids
|
|
1367
1549
|
// (no sections) just append at the bottom, untagged.
|
|
1368
1550
|
const storyMode = Array.isArray(doc.sections) && doc.sections.length > 0;
|
|
1369
1551
|
if (storyMode && !doc.sections.some((section) => section.kind === 'appendix')) {
|
|
@@ -1374,52 +1556,80 @@ function attachGeneratedTiles(projectRoot, plan, generatedPaths, candidates) {
|
|
|
1374
1556
|
order: doc.sections.length,
|
|
1375
1557
|
});
|
|
1376
1558
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
})
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1559
|
+
let y = doc.layout.items.reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
1560
|
+
let x = 0;
|
|
1561
|
+
const draftsDir = join(projectRoot, 'apps', plan.appId, 'drafts');
|
|
1562
|
+
mkdirSync(draftsDir, { recursive: true });
|
|
1563
|
+
for (const tile of candidates) {
|
|
1564
|
+
if (!tile.sql)
|
|
1565
|
+
continue;
|
|
1566
|
+
if (tile.sql.includes('"""'))
|
|
1567
|
+
throw new Error(`Exploratory SQL for "${tile.title}" contains an unsupported triple-quote delimiter.`);
|
|
1568
|
+
const draftId = slugify(tile.title) || slugify(tile.id) || `analysis-${Date.now()}`;
|
|
1569
|
+
const draftPath = join(draftsDir, `${draftId}.dql`);
|
|
1570
|
+
const source = [
|
|
1571
|
+
`block "${escapeDqlString(draftId)}" {`,
|
|
1572
|
+
` domain = "${escapeDqlString(plan.domain)}"`,
|
|
1573
|
+
' type = "custom"',
|
|
1574
|
+
' status = "review"',
|
|
1575
|
+
` owner = "${escapeDqlString(plan.owner)}"`,
|
|
1576
|
+
` description = "${escapeDqlString((tile.answer ?? tile.question ?? tile.title).slice(0, 240))}"`,
|
|
1577
|
+
tile.question ? ` llmContext = "${escapeDqlString(`App-scoped exploratory analysis. Question: ${tile.question}`)}"` : '',
|
|
1578
|
+
tile.question ? ` examples = [{ question = "${escapeDqlString(tile.question)}" }]` : '',
|
|
1579
|
+
' caveats = ["AI-generated app draft. Validate joins, filters, grain, and business interpretation before promotion or publication."]',
|
|
1580
|
+
' tags = ["app-scoped", "ai-generated", "needs-review"]',
|
|
1581
|
+
'',
|
|
1582
|
+
' query = """',
|
|
1583
|
+
tile.sql,
|
|
1584
|
+
' """',
|
|
1585
|
+
'',
|
|
1586
|
+
' visualization {',
|
|
1587
|
+
` chart = "${escapeDqlString(normalizeGeneratedViz(tile.viz))}"`,
|
|
1588
|
+
' }',
|
|
1589
|
+
'}',
|
|
1590
|
+
'',
|
|
1591
|
+
].filter(Boolean).join('\n');
|
|
1592
|
+
const fingerprint = `sha256:${createHash('sha256').update(source).digest('hex')}`;
|
|
1593
|
+
writeFileSync(draftPath, source, 'utf-8');
|
|
1594
|
+
generatedPaths.push(relative(projectRoot, draftPath));
|
|
1595
|
+
const item = {
|
|
1596
|
+
i: `draft_${draftId}`,
|
|
1597
|
+
x,
|
|
1598
|
+
y,
|
|
1599
|
+
w: 6,
|
|
1600
|
+
h: 4,
|
|
1601
|
+
draftAnalysis: {
|
|
1602
|
+
ref: `drafts/${draftId}.dql`,
|
|
1603
|
+
artifactFingerprint: fingerprint,
|
|
1604
|
+
...(plan.snapshotId ? { snapshotId: plan.snapshotId } : {}),
|
|
1605
|
+
...(tile.preflight.receiptId ? { executionReceiptId: tile.preflight.receiptId } : {}),
|
|
1606
|
+
},
|
|
1607
|
+
viz: { type: normalizeGeneratedViz(tile.viz) },
|
|
1608
|
+
title: tile.title,
|
|
1609
|
+
sourceClass: 'exploratory_analysis',
|
|
1610
|
+
review: {
|
|
1611
|
+
status: 'required',
|
|
1612
|
+
sourceFingerprint: fingerprint,
|
|
1613
|
+
...(tile.preflight.receiptId ? { preflightReceiptId: tile.preflight.receiptId } : {}),
|
|
1614
|
+
},
|
|
1615
|
+
trustState: 'review_required',
|
|
1616
|
+
reviewStatus: 'review_required',
|
|
1617
|
+
...(storyMode ? { sectionId: 'appendix' } : {}),
|
|
1618
|
+
};
|
|
1619
|
+
doc.layout.items.push(item);
|
|
1620
|
+
if (x === 0) {
|
|
1621
|
+
x = 6;
|
|
1622
|
+
}
|
|
1623
|
+
else {
|
|
1624
|
+
x = 0;
|
|
1625
|
+
y += 4;
|
|
1417
1626
|
}
|
|
1418
|
-
writeFileSync(absolutePath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
|
|
1419
1627
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1628
|
+
const parsed = parseDashboardDocument(JSON.stringify(doc), absolutePath);
|
|
1629
|
+
if (!parsed.document || parsed.errors.length > 0) {
|
|
1630
|
+
throw new Error(`Exploratory App dashboard is invalid: ${parsed.errors.map((error) => error.message).join('; ')}`);
|
|
1422
1631
|
}
|
|
1632
|
+
writeFileSync(absolutePath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
|
|
1423
1633
|
}
|
|
1424
1634
|
/**
|
|
1425
1635
|
* Phase 2 of the two-phase app build: the user confirmed the proposal — filter the
|
|
@@ -1439,7 +1649,29 @@ export async function commitAppAiBuild(projectRoot, sessionId, input = {}, hooks
|
|
|
1439
1649
|
return { ok: false, error: 'The App proposal changed or is stale. Refresh the proposal before creating the App.', status: 409 };
|
|
1440
1650
|
}
|
|
1441
1651
|
const plan = session.plan;
|
|
1652
|
+
const existingAppId = cleanString(session.inputs.existingAppId);
|
|
1653
|
+
const isExistingPageBuild = Boolean(existingAppId);
|
|
1654
|
+
const existingApp = existingAppId ? loadAppById(projectRoot, existingAppId) : null;
|
|
1655
|
+
if (isExistingPageBuild) {
|
|
1656
|
+
if (!existingApp)
|
|
1657
|
+
return { ok: false, error: `App "${existingAppId}" no longer exists.`, status: 409 };
|
|
1658
|
+
if (!session.existingAppFingerprint
|
|
1659
|
+
|| fingerprintAppDirectory(existingApp.appDir) !== session.existingAppFingerprint) {
|
|
1660
|
+
return {
|
|
1661
|
+
ok: false,
|
|
1662
|
+
error: 'The App changed after this page proposal was created. Refresh and review the page proposal again.',
|
|
1663
|
+
status: 409,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1442
1667
|
const proposal = session.proposal;
|
|
1668
|
+
const proposalTileIds = new Set(proposal.tiles.map((tile) => tile.id));
|
|
1669
|
+
const tileOverrides = Object.fromEntries(Object.entries(input.tileOverrides ?? {})
|
|
1670
|
+
.filter(([tileId, value]) => proposalTileIds.has(tileId) && value && typeof value === 'object')
|
|
1671
|
+
.map(([tileId, value]) => [tileId, {
|
|
1672
|
+
...(cleanString(value.title) ? { title: cleanString(value.title).slice(0, 120) } : {}),
|
|
1673
|
+
...(cleanString(value.viz) ? { viz: normalizeGeneratedViz(value.viz) } : {}),
|
|
1674
|
+
}]));
|
|
1443
1675
|
const selectable = new Set(proposal.tiles.filter((tile) => !tile.error).map((tile) => tile.id));
|
|
1444
1676
|
const selected = new Set((input.selectedTileIds ?? Array.from(selectable)).filter((tileId) => selectable.has(tileId)));
|
|
1445
1677
|
if (selected.size === 0) {
|
|
@@ -1449,11 +1681,43 @@ export async function commitAppAiBuild(projectRoot, sessionId, input = {}, hooks
|
|
|
1449
1681
|
// tiles (narrative, draft placeholders) stay — they document the story + gaps.
|
|
1450
1682
|
const committedPlan = {
|
|
1451
1683
|
...plan,
|
|
1452
|
-
|
|
1684
|
+
...(!isExistingPageBuild && cleanString(input.appName) ? { name: cleanString(input.appName).slice(0, 120) } : {}),
|
|
1685
|
+
...(!isExistingPageBuild && cleanString(input.audience) ? { audience: cleanString(input.audience).slice(0, 120) } : {}),
|
|
1686
|
+
pages: plan.pages.map((page, pageIndex) => ({
|
|
1453
1687
|
...page,
|
|
1454
|
-
|
|
1688
|
+
...(isExistingPageBuild && pageIndex === 0 && cleanString(input.pageTitle)
|
|
1689
|
+
? { title: cleanString(input.pageTitle).slice(0, 120) }
|
|
1690
|
+
: {}),
|
|
1691
|
+
tiles: page.tiles
|
|
1692
|
+
.filter((tile) => !['certified_block', 'semantic_query'].includes(tile.kind) || selected.has(tile.id))
|
|
1693
|
+
.map((tile) => {
|
|
1694
|
+
const override = tileOverrides[tile.id];
|
|
1695
|
+
if (!override)
|
|
1696
|
+
return tile;
|
|
1697
|
+
const viz = override.viz;
|
|
1698
|
+
return {
|
|
1699
|
+
...tile,
|
|
1700
|
+
...(override.title ? { title: override.title } : {}),
|
|
1701
|
+
...(viz ? { viz } : {}),
|
|
1702
|
+
...(viz && tile.display ? {
|
|
1703
|
+
display: {
|
|
1704
|
+
...tile.display,
|
|
1705
|
+
recommendedDisplayType: viz,
|
|
1706
|
+
genUi: {
|
|
1707
|
+
...tile.display.genUi,
|
|
1708
|
+
defaultVisualization: viz,
|
|
1709
|
+
allowedVisualizations: Array.from(new Set([viz, ...tile.display.genUi.allowedVisualizations])),
|
|
1710
|
+
},
|
|
1711
|
+
},
|
|
1712
|
+
} : {}),
|
|
1713
|
+
};
|
|
1714
|
+
}),
|
|
1455
1715
|
})),
|
|
1456
1716
|
};
|
|
1717
|
+
const selectedExploratory = proposal.tiles.filter((tile) => (tile.source === 'ai_generated' && selected.has(tile.id) && !tile.error && Boolean(tile.sql))).map((tile) => ({ ...tile, ...tileOverrides[tile.id] }));
|
|
1718
|
+
if (plan.mode !== 'personal' && selectedExploratory.length > 0) {
|
|
1719
|
+
return { ok: false, error: 'Exploratory SQL can only be accepted into a Personal Draft.', status: 400 };
|
|
1720
|
+
}
|
|
1457
1721
|
const { KGStore, defaultKgPath, ensureMetadataCatalogFresh, generateAppFromPlan, reindexProject, validateAppPlan, } = await import('@duckcodeailabs/dql-agent');
|
|
1458
1722
|
const index = await reindexProject(projectRoot, { kgPath: defaultKgPath(projectRoot) });
|
|
1459
1723
|
if (!session.snapshotId || index.kgFingerprint !== session.snapshotId) {
|
|
@@ -1465,8 +1729,8 @@ export async function commitAppAiBuild(projectRoot, sessionId, input = {}, hooks
|
|
|
1465
1729
|
const validation = validateAppPlan(committedPlan, kg);
|
|
1466
1730
|
const governedTileCount = committedPlan.pages.flatMap((page) => page.tiles)
|
|
1467
1731
|
.filter((tile) => tile.kind === 'certified_block' || tile.kind === 'semantic_query').length;
|
|
1468
|
-
if (governedTileCount === 0) {
|
|
1469
|
-
return { ok: false, error: 'Select
|
|
1732
|
+
if (governedTileCount === 0 && selectedExploratory.length === 0) {
|
|
1733
|
+
return { ok: false, error: 'Select a certified block, governed semantic query, or explicitly accepted Personal Draft analysis.', status: 409 };
|
|
1470
1734
|
}
|
|
1471
1735
|
// Copilot follow-ups the created app carries: still-uncovered gaps first, then
|
|
1472
1736
|
// the questions behind any candidates the user chose to leave out.
|
|
@@ -1478,33 +1742,104 @@ export async function commitAppAiBuild(projectRoot, sessionId, input = {}, hooks
|
|
|
1478
1742
|
]).slice(0, 4);
|
|
1479
1743
|
let generated;
|
|
1480
1744
|
const stagingRoot = join(projectRoot, '.dql', 'local', 'app-build-staging', session.id);
|
|
1481
|
-
const
|
|
1745
|
+
const generatedRoot = isExistingPageBuild ? join(stagingRoot, 'generated') : stagingRoot;
|
|
1746
|
+
const generatedAppDir = join(generatedRoot, 'apps', committedPlan.appId);
|
|
1747
|
+
const stagedAppDir = isExistingPageBuild
|
|
1748
|
+
? join(stagingRoot, 'candidate', 'apps', committedPlan.appId)
|
|
1749
|
+
: generatedAppDir;
|
|
1482
1750
|
const targetAppDir = join(projectRoot, 'apps', committedPlan.appId);
|
|
1483
1751
|
const backupAppDir = join(projectRoot, '.dql', 'local', 'app-build-staging', `${session.id}-backup`);
|
|
1484
1752
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
1485
1753
|
try {
|
|
1486
|
-
generated = generateAppFromPlan(
|
|
1754
|
+
generated = generateAppFromPlan(generatedRoot, committedPlan, kg, { overwrite: true, copilotQuestions });
|
|
1755
|
+
if (selectedExploratory.length > 0) {
|
|
1756
|
+
attachGeneratedDraftAnalyses(generatedRoot, committedPlan, generated.paths, selectedExploratory);
|
|
1757
|
+
}
|
|
1487
1758
|
mkdirSync(dirname(targetAppDir), { recursive: true });
|
|
1488
|
-
if (
|
|
1759
|
+
if (isExistingPageBuild) {
|
|
1760
|
+
if (!existingApp || !existsSync(targetAppDir)) {
|
|
1761
|
+
return { ok: false, error: `App "${existingAppId}" no longer exists.`, status: 409 };
|
|
1762
|
+
}
|
|
1763
|
+
if (fingerprintAppDirectory(existingApp.appDir) !== session.existingAppFingerprint) {
|
|
1764
|
+
return {
|
|
1765
|
+
ok: false,
|
|
1766
|
+
error: 'The App changed while this page was being prepared. Refresh and review the page proposal again.',
|
|
1767
|
+
status: 409,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
const page = committedPlan.pages[0];
|
|
1771
|
+
if (!page)
|
|
1772
|
+
return { ok: false, error: 'The approved plan has no page to add.', status: 400 };
|
|
1773
|
+
const generatedDashboardPath = join(generatedAppDir, 'dashboards', `${page.id}.dqld`);
|
|
1774
|
+
const targetDashboardPath = join(stagedAppDir, 'dashboards', `${page.id}.dqld`);
|
|
1775
|
+
if (existingApp.dashboards.some((dashboard) => dashboard.id === page.id)) {
|
|
1776
|
+
return { ok: false, error: `Dashboard page already exists: ${page.id}`, status: 409 };
|
|
1777
|
+
}
|
|
1778
|
+
cpSync(existingApp.appDir, stagedAppDir, { recursive: true });
|
|
1779
|
+
if (existsSync(targetDashboardPath)) {
|
|
1780
|
+
return { ok: false, error: `Dashboard page already exists: ${page.id}`, status: 409 };
|
|
1781
|
+
}
|
|
1782
|
+
mkdirSync(dirname(targetDashboardPath), { recursive: true });
|
|
1783
|
+
cpSync(generatedDashboardPath, targetDashboardPath);
|
|
1784
|
+
const committedPaths = [relative(projectRoot, join(targetAppDir, 'dashboards', `${page.id}.dqld`))];
|
|
1785
|
+
for (const generatedPath of generated.paths.filter((path) => path.includes('/drafts/') && path.endsWith('.dql'))) {
|
|
1786
|
+
const sourcePath = join(generatedRoot, generatedPath);
|
|
1787
|
+
const packageRelativePath = relative(generatedAppDir, sourcePath);
|
|
1788
|
+
const candidatePath = join(stagedAppDir, packageRelativePath);
|
|
1789
|
+
const finalPath = join(targetAppDir, packageRelativePath);
|
|
1790
|
+
if (existsSync(candidatePath)) {
|
|
1791
|
+
return { ok: false, error: `App draft already exists: ${relative(projectRoot, finalPath)}`, status: 409 };
|
|
1792
|
+
}
|
|
1793
|
+
mkdirSync(dirname(candidatePath), { recursive: true });
|
|
1794
|
+
cpSync(sourcePath, candidatePath);
|
|
1795
|
+
committedPaths.push(relative(projectRoot, finalPath));
|
|
1796
|
+
}
|
|
1797
|
+
generated = { ...generated, paths: committedPaths };
|
|
1798
|
+
if (fingerprintAppDirectory(existingApp.appDir) !== session.existingAppFingerprint) {
|
|
1799
|
+
return {
|
|
1800
|
+
ok: false,
|
|
1801
|
+
error: 'The App changed while this page was being staged. Refresh and review the page proposal again.',
|
|
1802
|
+
status: 409,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
rmSync(backupAppDir, { recursive: true, force: true });
|
|
1806
|
+
renameSync(targetAppDir, backupAppDir);
|
|
1807
|
+
try {
|
|
1808
|
+
renameSync(stagedAppDir, targetAppDir);
|
|
1809
|
+
}
|
|
1810
|
+
catch (error) {
|
|
1811
|
+
if (!existsSync(targetAppDir) && existsSync(backupAppDir))
|
|
1812
|
+
renameSync(backupAppDir, targetAppDir);
|
|
1813
|
+
throw error;
|
|
1814
|
+
}
|
|
1815
|
+
rmSync(backupAppDir, { recursive: true, force: true });
|
|
1816
|
+
}
|
|
1817
|
+
else if (existsSync(targetAppDir)) {
|
|
1489
1818
|
if (!input.force)
|
|
1490
1819
|
return { ok: false, error: `App already exists: apps/${committedPlan.appId}`, status: 409 };
|
|
1491
1820
|
rmSync(backupAppDir, { recursive: true, force: true });
|
|
1492
1821
|
renameSync(targetAppDir, backupAppDir);
|
|
1822
|
+
try {
|
|
1823
|
+
renameSync(stagedAppDir, targetAppDir);
|
|
1824
|
+
rmSync(backupAppDir, { recursive: true, force: true });
|
|
1825
|
+
}
|
|
1826
|
+
catch (error) {
|
|
1827
|
+
if (existsSync(backupAppDir) && !existsSync(targetAppDir))
|
|
1828
|
+
renameSync(backupAppDir, targetAppDir);
|
|
1829
|
+
throw error;
|
|
1830
|
+
}
|
|
1493
1831
|
}
|
|
1494
|
-
|
|
1832
|
+
else {
|
|
1495
1833
|
renameSync(stagedAppDir, targetAppDir);
|
|
1496
|
-
rmSync(backupAppDir, { recursive: true, force: true });
|
|
1497
|
-
}
|
|
1498
|
-
catch (error) {
|
|
1499
|
-
if (existsSync(backupAppDir) && !existsSync(targetAppDir))
|
|
1500
|
-
renameSync(backupAppDir, targetAppDir);
|
|
1501
|
-
throw error;
|
|
1502
1834
|
}
|
|
1503
1835
|
}
|
|
1504
1836
|
catch (err) {
|
|
1505
1837
|
const message = err instanceof Error ? err.message : String(err);
|
|
1506
1838
|
return { ok: false, error: message, status: message.includes('already exists') ? 409 : 400 };
|
|
1507
1839
|
}
|
|
1840
|
+
finally {
|
|
1841
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
1842
|
+
}
|
|
1508
1843
|
const attachWarnings = [];
|
|
1509
1844
|
await ensureMetadataCatalogFresh(projectRoot, { force: true });
|
|
1510
1845
|
const app = collectAppsList(projectRoot).find((entry) => entry.id === committedPlan.appId) ?? null;
|
|
@@ -1518,6 +1853,13 @@ export async function commitAppAiBuild(projectRoot, sessionId, input = {}, hooks
|
|
|
1518
1853
|
validation,
|
|
1519
1854
|
generatedPaths: generated.paths,
|
|
1520
1855
|
committedTileIds: Array.from(selected),
|
|
1856
|
+
committedBrief: {
|
|
1857
|
+
...(isExistingPageBuild
|
|
1858
|
+
? { pageTitle: committedPlan.pages[0]?.title }
|
|
1859
|
+
: { appName: committedPlan.name }),
|
|
1860
|
+
audience: committedPlan.audience,
|
|
1861
|
+
tileOverrides,
|
|
1862
|
+
},
|
|
1521
1863
|
warnings: [...session.warnings, ...attachWarnings],
|
|
1522
1864
|
};
|
|
1523
1865
|
writeAppAiBuildSession(projectRoot, next);
|
|
@@ -1578,6 +1920,11 @@ async function routeAppAskQuestion(ctx, appId, input) {
|
|
|
1578
1920
|
const tile = dashboard?.layout.items.find((item) => item.i === input.tileId ||
|
|
1579
1921
|
(input.blockId && item.block && 'blockId' in item.block && item.block.blockId === input.blockId));
|
|
1580
1922
|
const blockId = cleanString(input.blockId) || (tile?.block && 'blockId' in tile.block ? tile.block.blockId : undefined);
|
|
1923
|
+
const appContext = buildAppContextEnvelope(loaded.app, loaded.appDir, {
|
|
1924
|
+
dashboardId,
|
|
1925
|
+
tileId: cleanString(input.tileId) || undefined,
|
|
1926
|
+
blockId,
|
|
1927
|
+
});
|
|
1581
1928
|
const citations = [
|
|
1582
1929
|
{ kind: 'app', name: loaded.app.name, path: loaded.appPath },
|
|
1583
1930
|
...(dashboard ? [{ kind: 'dashboard', name: dashboard.metadata.title, path: `${loaded.appPath}/dashboards/${dashboard.id}.dqld` }] : []),
|
|
@@ -1601,11 +1948,20 @@ async function routeAppAskQuestion(ctx, appId, input) {
|
|
|
1601
1948
|
decision,
|
|
1602
1949
|
proposal: {
|
|
1603
1950
|
type: 'app_change',
|
|
1951
|
+
version: 1,
|
|
1604
1952
|
dashboardId,
|
|
1605
1953
|
tileId: input.tileId,
|
|
1606
1954
|
blockId,
|
|
1607
1955
|
question,
|
|
1608
1956
|
reviewRequired: true,
|
|
1957
|
+
expectedDashboardFingerprint: dashboard
|
|
1958
|
+
? `sha256:${createHash('sha256').update(JSON.stringify(dashboard)).digest('hex')}`
|
|
1959
|
+
: undefined,
|
|
1960
|
+
operations: [{
|
|
1961
|
+
op: 'review_in_build_mode',
|
|
1962
|
+
target: cleanString(input.tileId) || dashboardId || appId,
|
|
1963
|
+
suggestion: appChangeSuggestion(question),
|
|
1964
|
+
}],
|
|
1609
1965
|
},
|
|
1610
1966
|
};
|
|
1611
1967
|
}
|
|
@@ -1635,6 +1991,7 @@ async function routeAppAskQuestion(ctx, appId, input) {
|
|
|
1635
1991
|
variables: input.variables,
|
|
1636
1992
|
selectedTile: tile ? appAskTileContext(tile) : undefined,
|
|
1637
1993
|
userContext: input.context,
|
|
1994
|
+
appContext,
|
|
1638
1995
|
},
|
|
1639
1996
|
};
|
|
1640
1997
|
if (input.runInvestigation === false) {
|
|
@@ -1669,6 +2026,7 @@ async function routeAppAskQuestion(ctx, appId, input) {
|
|
|
1669
2026
|
variables: input.variables,
|
|
1670
2027
|
selectedTile: tile ? appAskTileContext(tile) : undefined,
|
|
1671
2028
|
userContext: input.context,
|
|
2029
|
+
appContext,
|
|
1672
2030
|
routeDecision: decision,
|
|
1673
2031
|
},
|
|
1674
2032
|
});
|
|
@@ -1719,7 +2077,7 @@ async function routeAppAskQuestion(ctx, appId, input) {
|
|
|
1719
2077
|
// surfaces use: question-first retrieval finds the best-matching certified block
|
|
1720
2078
|
// across the app, generates review-required SQL, or digs deeper — instead of
|
|
1721
2079
|
// narrating whichever tile happens to be focused.
|
|
1722
|
-
const governed = await tryGovernedAppAnswer(ctx, question, { blockId, appName: loaded.app.name, citations });
|
|
2080
|
+
const governed = await tryGovernedAppAnswer(ctx, question, { blockId, appName: loaded.app.name, citations, appContext });
|
|
1723
2081
|
if (governed)
|
|
1724
2082
|
return governed;
|
|
1725
2083
|
// Offline / no AI provider: keep the focused tile as a graceful fallback.
|
|
@@ -1847,7 +2205,7 @@ async function tryGovernedAppAnswer(ctx, question, input) {
|
|
|
1847
2205
|
if (!ctx.generateGovernedAnswer)
|
|
1848
2206
|
return null;
|
|
1849
2207
|
try {
|
|
1850
|
-
const governed = await ctx.generateGovernedAnswer(question);
|
|
2208
|
+
const governed = await ctx.generateGovernedAnswer(question, input.appContext);
|
|
1851
2209
|
return governed ? mapGovernedToAppAsk(governed, { question, ...input }) : null;
|
|
1852
2210
|
}
|
|
1853
2211
|
catch {
|
|
@@ -2032,24 +2390,32 @@ export function promoteAppForStakeholders(projectRoot, appId, input = {}) {
|
|
|
2032
2390
|
const loaded = loadAppById(projectRoot, appId);
|
|
2033
2391
|
if (!loaded)
|
|
2034
2392
|
return { ok: false, error: `App "${appId}" not found` };
|
|
2393
|
+
const dashboardSources = findDashboardsForApp(loaded.appDir)
|
|
2394
|
+
.map((path) => ({ path, document: loadDashboardDocument(path).document }))
|
|
2395
|
+
.filter((item) => Boolean(item.document));
|
|
2396
|
+
const readiness = appPublicationReadiness(dashboardSources.map((item) => item.document));
|
|
2397
|
+
if (!readiness.ready) {
|
|
2398
|
+
return {
|
|
2399
|
+
ok: false,
|
|
2400
|
+
error: `App is not ready for shared publication: ${readiness.blockers.map((blocker) => blocker.message).join('; ')}`,
|
|
2401
|
+
readiness,
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2035
2404
|
const lifecycle = input.lifecycle === 'certified' || input.lifecycle === 'deprecated' ? input.lifecycle : 'review';
|
|
2036
2405
|
const app = {
|
|
2037
2406
|
...loaded.app,
|
|
2038
2407
|
visibility: 'shared',
|
|
2408
|
+
publicationIntent: 'shared_project',
|
|
2039
2409
|
lifecycle,
|
|
2040
2410
|
};
|
|
2041
2411
|
const appPath = join(loaded.appDir, 'dql.app.json');
|
|
2042
2412
|
const parsedApp = parseAppDocument(JSON.stringify(app), appPath);
|
|
2043
2413
|
if (!parsedApp.document)
|
|
2044
2414
|
return { ok: false, error: parsedApp.errors.map((err) => err.message).join('; ') };
|
|
2045
|
-
writeFileSync(appPath, JSON.stringify(parsedApp.document, null, 2) + '\n', 'utf-8');
|
|
2046
2415
|
let removedLocalTiles = 0;
|
|
2047
|
-
const
|
|
2048
|
-
for (const
|
|
2049
|
-
const
|
|
2050
|
-
if (!loadedDashboard)
|
|
2051
|
-
continue;
|
|
2052
|
-
const items = loadedDashboard.layout.items
|
|
2416
|
+
const dashboards = [];
|
|
2417
|
+
for (const source of dashboardSources) {
|
|
2418
|
+
const items = source.document.layout.items
|
|
2053
2419
|
.filter((item) => {
|
|
2054
2420
|
if (item.aiPin) {
|
|
2055
2421
|
removedLocalTiles += 1;
|
|
@@ -2059,24 +2425,129 @@ export function promoteAppForStakeholders(projectRoot, appId, input = {}) {
|
|
|
2059
2425
|
})
|
|
2060
2426
|
.map((item) => promoteSharedDashboardItem(item));
|
|
2061
2427
|
const dashboard = {
|
|
2062
|
-
...
|
|
2428
|
+
...source.document,
|
|
2063
2429
|
metadata: {
|
|
2064
|
-
...
|
|
2430
|
+
...source.document.metadata,
|
|
2065
2431
|
visibility: 'shared',
|
|
2066
2432
|
lifecycle,
|
|
2067
2433
|
},
|
|
2068
2434
|
layout: {
|
|
2069
|
-
...
|
|
2435
|
+
...source.document.layout,
|
|
2070
2436
|
items,
|
|
2071
2437
|
},
|
|
2072
2438
|
};
|
|
2073
|
-
const parsed = parseDashboardDocument(JSON.stringify(dashboard),
|
|
2439
|
+
const parsed = parseDashboardDocument(JSON.stringify(dashboard), source.path);
|
|
2074
2440
|
if (!parsed.document)
|
|
2075
2441
|
return { ok: false, error: parsed.errors.map((err) => err.message).join('; ') };
|
|
2076
|
-
|
|
2077
|
-
|
|
2442
|
+
dashboards.push({ sourcePath: source.path, document: parsed.document });
|
|
2443
|
+
}
|
|
2444
|
+
// Stage and swap the whole App directory so validation or write failures do
|
|
2445
|
+
// not leave a partially published manifest/dashboard set.
|
|
2446
|
+
const stagingBase = join(projectRoot, '.dql', 'local', 'app-publish-staging');
|
|
2447
|
+
const stagedAppDir = join(stagingBase, `${appId}-next`);
|
|
2448
|
+
const backupAppDir = join(stagingBase, `${appId}-backup`);
|
|
2449
|
+
rmSync(stagedAppDir, { recursive: true, force: true });
|
|
2450
|
+
rmSync(backupAppDir, { recursive: true, force: true });
|
|
2451
|
+
mkdirSync(stagingBase, { recursive: true });
|
|
2452
|
+
cpSync(loaded.appDir, stagedAppDir, { recursive: true });
|
|
2453
|
+
writeFileSync(join(stagedAppDir, 'dql.app.json'), JSON.stringify(parsedApp.document, null, 2) + '\n', 'utf-8');
|
|
2454
|
+
for (const dashboard of dashboards) {
|
|
2455
|
+
const stagedPath = join(stagedAppDir, relative(loaded.appDir, dashboard.sourcePath));
|
|
2456
|
+
writeFileSync(stagedPath, JSON.stringify(dashboard.document, null, 2) + '\n', 'utf-8');
|
|
2457
|
+
}
|
|
2458
|
+
try {
|
|
2459
|
+
renameSync(loaded.appDir, backupAppDir);
|
|
2460
|
+
renameSync(stagedAppDir, loaded.appDir);
|
|
2461
|
+
rmSync(backupAppDir, { recursive: true, force: true });
|
|
2462
|
+
}
|
|
2463
|
+
catch (error) {
|
|
2464
|
+
if (existsSync(backupAppDir) && !existsSync(loaded.appDir))
|
|
2465
|
+
renameSync(backupAppDir, loaded.appDir);
|
|
2466
|
+
throw error;
|
|
2078
2467
|
}
|
|
2079
|
-
|
|
2468
|
+
const paths = [relative(projectRoot, appPath), ...dashboards.map((dashboard) => relative(projectRoot, dashboard.sourcePath))];
|
|
2469
|
+
return { ok: true, app: parsedApp.document, paths, removedLocalTiles, readiness };
|
|
2470
|
+
}
|
|
2471
|
+
export function approveAppSemanticTiles(projectRoot, appId, dashboardId, input) {
|
|
2472
|
+
const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
|
|
2473
|
+
if (!loaded)
|
|
2474
|
+
return { ok: false, error: `Dashboard "${dashboardId}" not found in App "${appId}".` };
|
|
2475
|
+
const currentFingerprint = `sha256:${createHash('sha256').update(JSON.stringify(loaded.dashboard)).digest('hex')}`;
|
|
2476
|
+
if (!input.expectedDashboardFingerprint || input.expectedDashboardFingerprint !== currentFingerprint) {
|
|
2477
|
+
return { ok: false, error: 'The dashboard changed after the reviewed run. Run it again before approval.' };
|
|
2478
|
+
}
|
|
2479
|
+
const selected = new Set(input.tileIds);
|
|
2480
|
+
const semanticIds = new Set(loaded.dashboard.layout.items.filter((item) => item.semantic).map((item) => item.i));
|
|
2481
|
+
const invalid = input.tileIds.filter((tileId) => !semanticIds.has(tileId));
|
|
2482
|
+
if (invalid.length > 0)
|
|
2483
|
+
return { ok: false, error: `Only semantic tiles can be approved here: ${invalid.join(', ')}` };
|
|
2484
|
+
const reviewedAt = new Date().toISOString();
|
|
2485
|
+
const dashboard = {
|
|
2486
|
+
...loaded.dashboard,
|
|
2487
|
+
layout: {
|
|
2488
|
+
...loaded.dashboard.layout,
|
|
2489
|
+
items: loaded.dashboard.layout.items.map((item) => {
|
|
2490
|
+
if (!selected.has(item.i) || !item.semantic)
|
|
2491
|
+
return item;
|
|
2492
|
+
return {
|
|
2493
|
+
...item,
|
|
2494
|
+
semantic: { ...item.semantic, snapshotId: input.snapshotId },
|
|
2495
|
+
review: {
|
|
2496
|
+
status: 'approved',
|
|
2497
|
+
sourceFingerprint: item.semantic.definitionFingerprint,
|
|
2498
|
+
preflightReceiptId: input.runId,
|
|
2499
|
+
reviewedAt,
|
|
2500
|
+
reviewedBy: input.reviewer,
|
|
2501
|
+
},
|
|
2502
|
+
trustState: 'review_required',
|
|
2503
|
+
reviewStatus: 'review_required',
|
|
2504
|
+
};
|
|
2505
|
+
}),
|
|
2506
|
+
},
|
|
2507
|
+
};
|
|
2508
|
+
const parsed = parseDashboardDocument(JSON.stringify(dashboard), loaded.path);
|
|
2509
|
+
if (!parsed.document)
|
|
2510
|
+
return { ok: false, error: parsed.errors.map((error) => error.message).join('; ') };
|
|
2511
|
+
writeFileSync(loaded.path, JSON.stringify(parsed.document, null, 2) + '\n', 'utf-8');
|
|
2512
|
+
return { ok: true, dashboard: parsed.document, path: relative(projectRoot, loaded.path) };
|
|
2513
|
+
}
|
|
2514
|
+
function appPublicationReadiness(dashboards) {
|
|
2515
|
+
const blockers = [];
|
|
2516
|
+
let governedTiles = 0;
|
|
2517
|
+
for (const dashboard of dashboards) {
|
|
2518
|
+
for (const item of dashboard.layout.items) {
|
|
2519
|
+
if (item.block)
|
|
2520
|
+
governedTiles += 1;
|
|
2521
|
+
if (item.draftAnalysis) {
|
|
2522
|
+
blockers.push({
|
|
2523
|
+
dashboardId: dashboard.id,
|
|
2524
|
+
tileId: item.i,
|
|
2525
|
+
code: 'exploratory_source',
|
|
2526
|
+
message: `${dashboard.id}/${item.i} references app-scoped exploratory DQL; promote it to governed semantic logic or a certified block first.`,
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
if (item.semantic) {
|
|
2530
|
+
governedTiles += 1;
|
|
2531
|
+
if (item.review?.status !== 'approved') {
|
|
2532
|
+
blockers.push({
|
|
2533
|
+
dashboardId: dashboard.id,
|
|
2534
|
+
tileId: item.i,
|
|
2535
|
+
code: 'semantic_review',
|
|
2536
|
+
message: `${dashboard.id}/${item.i} semantic result has not been explicitly approved.`,
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
if (!item.semantic.snapshotId || !item.review?.preflightReceiptId) {
|
|
2540
|
+
blockers.push({
|
|
2541
|
+
dashboardId: dashboard.id,
|
|
2542
|
+
tileId: item.i,
|
|
2543
|
+
code: 'semantic_preflight',
|
|
2544
|
+
message: `${dashboard.id}/${item.i} needs a snapshot-bound semantic preflight receipt.`,
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return { ready: blockers.length === 0, governedTiles, blockers };
|
|
2080
2551
|
}
|
|
2081
2552
|
function appAiBuildSessionDir(projectRoot) {
|
|
2082
2553
|
return join(projectRoot, '.dql', 'local', 'app-ai-builds');
|
|
@@ -2163,6 +2634,10 @@ function appAskTileContext(tile) {
|
|
|
2163
2634
|
tileId: tile.i,
|
|
2164
2635
|
title: tile.title,
|
|
2165
2636
|
blockId: tile.block && 'blockId' in tile.block ? tile.block.blockId : undefined,
|
|
2637
|
+
semantic: tile.semantic,
|
|
2638
|
+
draftAnalysis: tile.draftAnalysis,
|
|
2639
|
+
sourceClass: tile.sourceClass ?? (tile.block ? 'certified_block' : tile.semantic ? 'governed_semantic' : tile.draftAnalysis ? 'exploratory_analysis' : 'narrative'),
|
|
2640
|
+
review: tile.review,
|
|
2166
2641
|
viz: tile.viz.type,
|
|
2167
2642
|
trustState: tile.trustState ?? tile.display?.trustState,
|
|
2168
2643
|
reviewStatus: tile.reviewStatus ?? tile.display?.reviewStatus,
|
|
@@ -2171,6 +2646,34 @@ function appAskTileContext(tile) {
|
|
|
2171
2646
|
sourceEvidence: tile.sourceEvidence,
|
|
2172
2647
|
};
|
|
2173
2648
|
}
|
|
2649
|
+
function buildAppContextEnvelope(app, appDir, focus) {
|
|
2650
|
+
const dashboards = findDashboardsForApp(appDir)
|
|
2651
|
+
.map((path) => loadDashboardDocument(path).document)
|
|
2652
|
+
.filter((document) => Boolean(document))
|
|
2653
|
+
.map((document) => ({
|
|
2654
|
+
id: document.id,
|
|
2655
|
+
title: document.metadata.title,
|
|
2656
|
+
filters: document.filters,
|
|
2657
|
+
story: document.story,
|
|
2658
|
+
tiles: document.layout.items.map(appAskTileContext),
|
|
2659
|
+
}));
|
|
2660
|
+
return {
|
|
2661
|
+
version: 1,
|
|
2662
|
+
app: {
|
|
2663
|
+
id: app.id,
|
|
2664
|
+
name: app.name,
|
|
2665
|
+
domain: app.domain,
|
|
2666
|
+
audience: app.audience,
|
|
2667
|
+
lifecycle: app.lifecycle ?? 'draft',
|
|
2668
|
+
visibility: app.visibility ?? 'shared',
|
|
2669
|
+
publicationIntent: app.publicationIntent ?? (app.visibility === 'private' ? 'personal' : 'shared_project'),
|
|
2670
|
+
businessOutcome: app.businessOutcome,
|
|
2671
|
+
caveats: app.caveats ?? [],
|
|
2672
|
+
},
|
|
2673
|
+
dashboards,
|
|
2674
|
+
focus,
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2174
2677
|
function filterBindingsForBlockSource(projectRoot, block) {
|
|
2175
2678
|
const absPath = join(projectRoot, block.path);
|
|
2176
2679
|
if (!existsSync(absPath))
|
|
@@ -2354,6 +2857,35 @@ function promoteSharedDashboardItem(item) {
|
|
|
2354
2857
|
reviewStatus,
|
|
2355
2858
|
};
|
|
2356
2859
|
}
|
|
2860
|
+
export function deleteAppPackage(projectRoot, id) {
|
|
2861
|
+
const cleanId = cleanString(id);
|
|
2862
|
+
if (!cleanId || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(cleanId)) {
|
|
2863
|
+
return { ok: false, status: 400, error: 'A valid App id is required.' };
|
|
2864
|
+
}
|
|
2865
|
+
const loaded = loadAppById(projectRoot, cleanId);
|
|
2866
|
+
if (!loaded)
|
|
2867
|
+
return { ok: false, status: 404, error: `App "${cleanId}" not found.` };
|
|
2868
|
+
const relativeAppDir = relative(projectRoot, loaded.appDir).replaceAll('\\', '/');
|
|
2869
|
+
const isProjectApp = /^apps\/[^/]+$/.test(relativeAppDir);
|
|
2870
|
+
const isDomainApp = /^domains\/(?:[^/]+\/)*apps\/[^/]+$/.test(relativeAppDir);
|
|
2871
|
+
if ((!isProjectApp && !isDomainApp) || relativeAppDir.startsWith('../')) {
|
|
2872
|
+
return { ok: false, status: 400, error: 'DQL refused to delete an App outside this project.' };
|
|
2873
|
+
}
|
|
2874
|
+
const trashRoot = join(projectRoot, '.dql', 'trash', 'apps');
|
|
2875
|
+
mkdirSync(trashRoot, { recursive: true });
|
|
2876
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2877
|
+
let trashDir = join(trashRoot, `${stamp}-${cleanId}`);
|
|
2878
|
+
let suffix = 2;
|
|
2879
|
+
while (existsSync(trashDir))
|
|
2880
|
+
trashDir = join(trashRoot, `${stamp}-${cleanId}-${suffix++}`);
|
|
2881
|
+
renameSync(loaded.appDir, trashDir);
|
|
2882
|
+
return {
|
|
2883
|
+
ok: true,
|
|
2884
|
+
id: cleanId,
|
|
2885
|
+
deletedPath: relativeAppDir,
|
|
2886
|
+
trashPath: relative(projectRoot, trashDir).replaceAll('\\', '/'),
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2357
2889
|
export function createAppPackage(projectRoot, input) {
|
|
2358
2890
|
const name = cleanString(input.name);
|
|
2359
2891
|
const domain = cleanString(input.ownerDomain) || cleanString(input.domain);
|
|
@@ -4474,6 +5006,15 @@ function asRecord(value) {
|
|
|
4474
5006
|
function cleanString(value) {
|
|
4475
5007
|
return typeof value === 'string' ? value.trim() : '';
|
|
4476
5008
|
}
|
|
5009
|
+
function appAiBuildSessionId(requested) {
|
|
5010
|
+
const clean = cleanString(requested);
|
|
5011
|
+
if (clean) {
|
|
5012
|
+
if (!/^app_build_[a-z0-9_:-]+$/i.test(clean))
|
|
5013
|
+
throw new Error('Invalid App build session id.');
|
|
5014
|
+
return clean;
|
|
5015
|
+
}
|
|
5016
|
+
return `app_build_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5017
|
+
}
|
|
4477
5018
|
function stringArray(value) {
|
|
4478
5019
|
if (!Array.isArray(value))
|
|
4479
5020
|
return undefined;
|
|
@@ -4495,6 +5036,43 @@ function slugify(value) {
|
|
|
4495
5036
|
.replace(/[^a-z0-9]+/g, '-')
|
|
4496
5037
|
.replace(/^-+|-+$/g, '');
|
|
4497
5038
|
}
|
|
5039
|
+
function uniqueAppPageId(base, existingIds) {
|
|
5040
|
+
if (!existingIds.has(base))
|
|
5041
|
+
return base;
|
|
5042
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
5043
|
+
const candidate = `${base}-${index}`;
|
|
5044
|
+
if (!existingIds.has(candidate))
|
|
5045
|
+
return candidate;
|
|
5046
|
+
}
|
|
5047
|
+
return `${base}-${Date.now()}`;
|
|
5048
|
+
}
|
|
5049
|
+
/**
|
|
5050
|
+
* API-009: bind an existing-App proposal to every Git-owned byte in the package. This is
|
|
5051
|
+
* deliberately stricter than the metadata snapshot: page, notebook, draft, and
|
|
5052
|
+
* App-document edits all invalidate approval before the atomic directory swap.
|
|
5053
|
+
*/
|
|
5054
|
+
function fingerprintAppDirectory(appDir) {
|
|
5055
|
+
const files = [];
|
|
5056
|
+
const visit = (directory) => {
|
|
5057
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
5058
|
+
const absolutePath = join(directory, entry.name);
|
|
5059
|
+
if (entry.isDirectory())
|
|
5060
|
+
visit(absolutePath);
|
|
5061
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
5062
|
+
files.push({ path: absolutePath, symbolicLink: entry.isSymbolicLink() });
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
};
|
|
5066
|
+
visit(appDir);
|
|
5067
|
+
const hash = createHash('sha256');
|
|
5068
|
+
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
5069
|
+
hash.update(relative(appDir, file.path));
|
|
5070
|
+
hash.update('\0');
|
|
5071
|
+
hash.update(file.symbolicLink ? `link:${readlinkSync(file.path)}` : readFileSync(file.path));
|
|
5072
|
+
hash.update('\0');
|
|
5073
|
+
}
|
|
5074
|
+
return `sha256:${hash.digest('hex')}`;
|
|
5075
|
+
}
|
|
4498
5076
|
function resolveAppPackageDir(projectRoot, id) {
|
|
4499
5077
|
return join(projectRoot, 'apps', id);
|
|
4500
5078
|
}
|
|
@@ -5204,7 +5782,7 @@ function loadDashboardForApp(projectRoot, appId, dashboardId) {
|
|
|
5204
5782
|
for (const d of findDashboardsForApp(appDir)) {
|
|
5205
5783
|
const { document: dd } = loadDashboardDocument(d);
|
|
5206
5784
|
if (dd && dd.id === dashboardId) {
|
|
5207
|
-
return { app: document, dashboard: dd };
|
|
5785
|
+
return { app: document, dashboard: dd, path: d };
|
|
5208
5786
|
}
|
|
5209
5787
|
}
|
|
5210
5788
|
}
|