@duckcodeailabs/dql-cli 1.13.2 → 1.13.3
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 +25 -3
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +1037 -175
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-BlUE3qsX.js → AgentLogPage-B3ugEb3H.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-O88iK6UW.js → AiBuildDialog-BK_TDNcM.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-D-6G4Sh3.js → AiBuildResult-Cob_Lj05.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-MRSGtQDV.js → AiSidePanel-CzIgxmP9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-CcRVO6zN.js → AnalyticsHome-C2vJ2fxY.js} +1 -1
- package/dist/assets/dql-notebook/assets/AppsView-DEWQ0sHT.js +5588 -0
- package/dist/assets/dql-notebook/assets/BlockStudio-CaDNTKMM.js +55 -0
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-itEIUrYy.js → BusinessArtifactView-zwUOtddi.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CbAqOpNc.js → DbtFirstModelingPage-BlnhTMu3.js} +4 -4
- package/dist/assets/dql-notebook/assets/{GitPage-BxOqRYu2.js → GitPage-Coevr9NJ.js} +3 -3
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-Tfkx76CI.js → GlobalAiRail-BTZS11x5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-CeaDbepP.js → GovernedContextPage-CjyyM_ne.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-D1lAryYv.js → HelpDocsPage-C1UeP85v.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-GB6se6xW.js → HomePage-zGPFIvOY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-zCyN-hdg.js → LineageDAG-DbXnQWu3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-Cg0TgG2Y.js → LineageDetailView-Ch1Db01I.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-DwMcenDf.js → LineageDrawer-DLxqJO6D.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-DUZx3ksU.js → LineagePathBreadcrumb-B2tNdkr_.js} +3 -3
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-DT_ycl6s.js → MiniLineageGraph-0gY-ZwCy.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-qdhzeJPF.js → NewBlockModal-DKVkXD20.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-BnaDzs1A.js → NewNotebookModal-BUIByvYT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-CouPFzdo.js → NotebookEditor-D2fIUxjE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-DB_KbH9Y.js → ReadinessPage-Ci4LyNiQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-CamQq_N2.js → SetupOnboarding-BF7ai_0t.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage--mYkOAK2.js → SkillsPage-BgYrgirJ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-ClEYiYeA.js → TrustBadge-Bob1Laiq.js} +1 -1
- package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-DBfsmXes.js → UnifiedAgentRunPanel-IxDUKSG8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-Cg3UyGwh.js → answer-to-notebook-Dk42N0pU.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-DnYPGqlr.js → arrow-left-DKuuTw9l.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-Ct_YGrmU.js → arrow-right-TH8_ufKG.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-DULlwbj6.js → book-open-text-B-dcHHn0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-ByYxoiS9.js → circle-x-BOc2KCZQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-BjYSO53F.js → dagre.esm-CPT_vn5p.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-DprWh9Yf.js → external-link-DtOfpYc6.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-CrEEYkXN.js → grip-vertical-B8joDDH9.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-DJRMz64S.js +635 -0
- package/dist/assets/dql-notebook/assets/{link-2-BkW-rkEC.js → link-2-DMZPHawg.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-BipBLmIE.js → list-tree-Dw0qlxPK.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-BhXsglrO.js → minimize-2-DUiSrPS3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-BoCSilYB.js → panel-right-open-BrrFrRZQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-mdoZawPu.js → play-Ced0HT9g.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-ELiDRUmj.js → rotate-ccw-D_9U9o7X.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-DVNRLfeU.js → semantic-fields-BlDOaonV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-GeUzpmKz.js → sliders-horizontal-BDt6KRGR.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-D63O81Wp.js → star-CxUbF_BW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-BWrCxQVE.js → triangle-alert-COWnsSdh.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-CI-ldXhC.js → upload-DSFC2kJX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-Cv7Yq2hK.js → usePersistedAgentThreadId-DYdx896E.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-Do21aFGo.js → user-round-Bfu7fosn.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-DT7u7F_x.js → wand-sparkles-DSpG0c0j.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-DHh2mMbn.js → workflow-B1_pj9gq.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-7tYr_GKb.js → wrench-CvfM03KZ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-CcYceFMz.js → x-B4coLiOl.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/local-runtime.d.ts +47 -19
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +1015 -566
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AppsView-BTkGC-wl.js +0 -5588
- package/dist/assets/dql-notebook/assets/BlockStudio-BrTNaDpb.js +0 -55
- package/dist/assets/dql-notebook/assets/index-9w93YPF9.js +0 -635
package/dist/apps-api.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
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
|
-
import { loadAppDocument, findAppDocuments, loadDashboardDocument, findDashboardsForApp, parseAppDocument, parseDashboardDocument, normalizeDqlArtifactReference, suggestAppId, createAppBuildDraft, applyAppBuildDraftOperations, } from '@duckcodeailabs/dql-core';
|
|
9
|
+
import { loadAppDocument, findAppDocuments, loadDashboardDocument, findDashboardsForApp, parseAppDocument, parseDashboardDocument, normalizeDqlArtifactReference, suggestAppId, createAppBuildDraft, appBuildDraftHash, applyAppBuildDraftOperations, } from '@duckcodeailabs/dql-core';
|
|
10
10
|
import { defaultPersonaRegistry, defaultLocalAppsDbPath, LocalAppStorage, personaFromMember, } from '@duckcodeailabs/dql-project';
|
|
11
11
|
export async function handleAppsApi(ctx) {
|
|
12
12
|
const { req, res, path, projectRoot } = ctx;
|
|
@@ -40,13 +40,71 @@ export async function handleAppsApi(ctx) {
|
|
|
40
40
|
if (req.method === 'GET' && path === '/api/app-builds') {
|
|
41
41
|
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
42
42
|
try {
|
|
43
|
-
|
|
43
|
+
const drafts = storage.listAppBuildDrafts().flatMap((stored) => {
|
|
44
|
+
const migrated = migrateLegacyAppBuildDraft(projectRoot, stored);
|
|
45
|
+
if (!migrated)
|
|
46
|
+
return [];
|
|
47
|
+
if (stored.version !== migrated.version)
|
|
48
|
+
storage.saveAppBuildDraft(migrated);
|
|
49
|
+
return [migrated];
|
|
50
|
+
});
|
|
51
|
+
sendJson(res, 200, { ok: true, drafts });
|
|
44
52
|
}
|
|
45
53
|
finally {
|
|
46
54
|
storage.close();
|
|
47
55
|
}
|
|
48
56
|
return true;
|
|
49
57
|
}
|
|
58
|
+
const sourceCandidatesMatch = path.match(/^\/api\/app-builds\/([^/]+)\/source-candidates$/);
|
|
59
|
+
if (sourceCandidatesMatch && req.method === 'GET') {
|
|
60
|
+
try {
|
|
61
|
+
const id = decodeURIComponent(sourceCandidatesMatch[1]);
|
|
62
|
+
const current = loadStoredAppBuildDraft(projectRoot, id);
|
|
63
|
+
if (!current) {
|
|
64
|
+
sendJson(res, 404, { ok: false, error: 'App Build Draft not found.' });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const { ensureAgentProjectReady, queryAppSourceCatalog } = await import('@duckcodeailabs/dql-agent');
|
|
68
|
+
await ensureAgentProjectReady(projectRoot);
|
|
69
|
+
const lifecycles = splitQueryValues(ctx.url.searchParams.getAll('lifecycle'));
|
|
70
|
+
const domains = splitQueryValues(ctx.url.searchParams.getAll('domain'));
|
|
71
|
+
const page = queryAppSourceCatalog(projectRoot, {
|
|
72
|
+
query: ctx.url.searchParams.get('q') ?? undefined,
|
|
73
|
+
cursor: ctx.url.searchParams.get('cursor') ?? undefined,
|
|
74
|
+
limit: Number(ctx.url.searchParams.get('limit')) || 50,
|
|
75
|
+
lifecycles: lifecycles.length ? lifecycles : undefined,
|
|
76
|
+
domains: domains.length ? domains : undefined,
|
|
77
|
+
sourcePolicy: current.sourcePolicy,
|
|
78
|
+
includeDeprecated: ctx.url.searchParams.get('includeDeprecated') === 'true',
|
|
79
|
+
});
|
|
80
|
+
sendJson(res, 200, { ok: true, ...page });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const error = err;
|
|
84
|
+
sendJson(res, error.code?.includes('CURSOR') ? 409 : 400, { ok: false, code: error.code, error: error.message });
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (sourceCandidatesMatch && req.method === 'POST') {
|
|
89
|
+
try {
|
|
90
|
+
const id = decodeURIComponent(sourceCandidatesMatch[1]);
|
|
91
|
+
const current = loadStoredAppBuildDraft(projectRoot, id);
|
|
92
|
+
if (!current) {
|
|
93
|
+
sendJson(res, 404, { ok: false, error: 'App Build Draft not found.' });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const body = await readJson(req);
|
|
97
|
+
const { ensureAgentProjectReady, resolveAppSourceCatalogRecords } = await import('@duckcodeailabs/dql-agent');
|
|
98
|
+
await ensureAgentProjectReady(projectRoot);
|
|
99
|
+
const result = resolveAppSourceCatalogRecords(projectRoot, body.sourceIds ?? [], current.sourcePolicy);
|
|
100
|
+
sendJson(res, 200, { ok: true, ...result });
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const error = err;
|
|
104
|
+
sendJson(res, 400, { ok: false, code: error.code, error: error.message });
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
50
108
|
let buildMatch = path.match(/^\/api\/app-builds\/([^/]+)$/);
|
|
51
109
|
if (buildMatch && req.method === 'GET') {
|
|
52
110
|
const draft = loadStoredAppBuildDraft(projectRoot, decodeURIComponent(buildMatch[1]));
|
|
@@ -130,7 +188,103 @@ export async function handleAppsApi(ctx) {
|
|
|
130
188
|
const proposal = await proposeAppBuildDraftOperations(projectRoot, current, {
|
|
131
189
|
prompt: cleanString(body.prompt) || current.frame.goal,
|
|
132
190
|
selectedBlockIds: body.selectedBlockIds,
|
|
133
|
-
}, { generateGovernedAnswer: ctx.generateGovernedAnswer });
|
|
191
|
+
}, { generateGovernedAnswer: ctx.generateGovernedAnswer, planAppBuild: ctx.planAppBuild });
|
|
192
|
+
writeAppBuildDraftAiProposal(projectRoot, proposal);
|
|
193
|
+
sendJson(res, 201, { ok: true, proposal });
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
const composeBuildMatch = path.match(/^\/api\/app-builds\/([^/]+)\/compose$/);
|
|
201
|
+
if (composeBuildMatch && req.method === 'POST') {
|
|
202
|
+
try {
|
|
203
|
+
const id = decodeURIComponent(composeBuildMatch[1]);
|
|
204
|
+
const current = loadStoredAppBuildDraft(projectRoot, id);
|
|
205
|
+
if (!current) {
|
|
206
|
+
sendJson(res, 404, { ok: false, error: 'App Build Draft not found.' });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const body = await readJson(req);
|
|
210
|
+
if (body.expectedRevision !== current.revision || body.expectedProposalHash !== current.proposalHash) {
|
|
211
|
+
sendJson(res, 409, { ok: false, error: 'APP_BUILD_REVISION_CONFLICT: refresh before composing this App.' });
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
const result = await composeStoredAppBuildDraft(projectRoot, current, body);
|
|
215
|
+
sendJson(res, 200, { ok: true, ...result });
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
+
sendJson(res, message.includes('APP_BUILD_') ? 409 : 400, { ok: false, error: message });
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
const reviseProposalMatch = path.match(/^\/api\/app-builds\/([^/]+)\/ai-proposals\/([^/]+)\/revisions$/);
|
|
224
|
+
if (reviseProposalMatch && req.method === 'POST') {
|
|
225
|
+
try {
|
|
226
|
+
const id = decodeURIComponent(reviseProposalMatch[1]);
|
|
227
|
+
const proposalId = decodeURIComponent(reviseProposalMatch[2]);
|
|
228
|
+
const current = loadStoredAppBuildDraft(projectRoot, id);
|
|
229
|
+
const previous = current ? loadAppBuildDraftAiProposal(projectRoot, id, proposalId) : null;
|
|
230
|
+
if (!current || !previous) {
|
|
231
|
+
sendJson(res, 404, { ok: false, error: 'App Build proposal not found.' });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
const body = await readJson(req);
|
|
235
|
+
if (body.expectedRevision !== current.revision || body.expectedProposalHash !== current.proposalHash
|
|
236
|
+
|| previous.baseRevision !== current.revision || previous.baseProposalHash !== current.proposalHash) {
|
|
237
|
+
sendJson(res, 409, { ok: false, error: 'APP_BUILD_PROPOSAL_CONFLICT: refresh before revising this proposal.' });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const answers = body.answers ?? {};
|
|
241
|
+
const answerText = previous.clarifications.flatMap((question) => {
|
|
242
|
+
const answerId = answers[question.id];
|
|
243
|
+
const choice = question.choices.find((candidate) => candidate.id === answerId);
|
|
244
|
+
return choice ? [`${question.question}: ${choice.label}`] : [];
|
|
245
|
+
});
|
|
246
|
+
const selectedSourceIds = unique([
|
|
247
|
+
...(body.selectedSourceIds ?? previous.defaultSelectedSourceIds ?? []),
|
|
248
|
+
...(body.additionalSourceIds ?? []),
|
|
249
|
+
].map(cleanString).filter(Boolean));
|
|
250
|
+
const proposal = await proposeAppBuildDraftOperations(projectRoot, current, {
|
|
251
|
+
prompt: [current.frame.goal, answerText.length ? `Clarification decisions: ${answerText.join('; ')}` : ''].filter(Boolean).join('\n'),
|
|
252
|
+
selectedBlockIds: selectedSourceIds,
|
|
253
|
+
}, { generateGovernedAnswer: ctx.generateGovernedAnswer, planAppBuild: ctx.planAppBuild });
|
|
254
|
+
proposal.defaultSelectedSourceIds = selectedSourceIds.filter((sourceId) => proposal.operations.some((operation) => (operation.type === 'upsert_source' && operation.source.id === sourceId)));
|
|
255
|
+
writeAppBuildDraftAiProposal(projectRoot, proposal);
|
|
256
|
+
sendJson(res, 201, { ok: true, proposal });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
const generateGapMatch = path.match(/^\/api\/app-builds\/([^/]+)\/ai-proposals\/([^/]+)\/gaps$/);
|
|
264
|
+
if (generateGapMatch && req.method === 'POST') {
|
|
265
|
+
try {
|
|
266
|
+
const id = decodeURIComponent(generateGapMatch[1]);
|
|
267
|
+
const proposalId = decodeURIComponent(generateGapMatch[2]);
|
|
268
|
+
const current = loadStoredAppBuildDraft(projectRoot, id);
|
|
269
|
+
const previous = current ? loadAppBuildDraftAiProposal(projectRoot, id, proposalId) : null;
|
|
270
|
+
if (!current || !previous) {
|
|
271
|
+
sendJson(res, 404, { ok: false, error: 'App Build proposal not found.' });
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const body = await readJson(req);
|
|
275
|
+
if (body.expectedRevision !== current.revision || body.expectedProposalHash !== current.proposalHash
|
|
276
|
+
|| previous.baseRevision !== current.revision || previous.baseProposalHash !== current.proposalHash) {
|
|
277
|
+
sendJson(res, 409, { ok: false, error: 'APP_BUILD_PROPOSAL_CONFLICT: refresh before generating a gap.' });
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
if (current.sourcePolicy !== 'include_review_required') {
|
|
281
|
+
sendJson(res, 409, { ok: false, error: 'APP_BUILD_REVIEW_POLICY_REQUIRED: enable review-required sources before generating a gap.' });
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (!ctx.generateGovernedAnswer)
|
|
285
|
+
throw new Error('The configured provider cannot generate an App gap in this runtime.');
|
|
286
|
+
const proposal = await generateExplicitAppBuildGap(projectRoot, current, previous, cleanString(body.requirementId), ctx.generateGovernedAnswer);
|
|
287
|
+
writeAppBuildDraftAiProposal(projectRoot, proposal);
|
|
134
288
|
sendJson(res, 201, { ok: true, proposal });
|
|
135
289
|
}
|
|
136
290
|
catch (err) {
|
|
@@ -216,7 +370,16 @@ export async function handleAppsApi(ctx) {
|
|
|
216
370
|
if (req.method === 'POST' && path === '/api/apps/recommend-blocks') {
|
|
217
371
|
try {
|
|
218
372
|
const body = await readJson(req);
|
|
219
|
-
|
|
373
|
+
const { ensureAgentProjectReady, queryAppSourceCatalog } = await import('@duckcodeailabs/dql-agent');
|
|
374
|
+
await ensureAgentProjectReady(projectRoot);
|
|
375
|
+
const page = queryAppSourceCatalog(projectRoot, {
|
|
376
|
+
query: [body.purpose, body.audience, ...(body.tags ?? [])].filter(Boolean).join(' '),
|
|
377
|
+
domains: body.domain && body.domain !== 'general' ? [body.domain] : undefined,
|
|
378
|
+
lifecycles: body.certifiedOnly ? ['certified'] : undefined,
|
|
379
|
+
sourcePolicy: body.certifiedOnly ? 'governed_only' : 'include_review_required',
|
|
380
|
+
limit: 50,
|
|
381
|
+
});
|
|
382
|
+
sendJson(res, 200, { blocks: page.items.map(appSourceCatalogRecordToLegacyRecommendation) });
|
|
220
383
|
}
|
|
221
384
|
catch (err) {
|
|
222
385
|
sendJson(res, 500, { error: err.message });
|
|
@@ -930,22 +1093,6 @@ export async function handleAppsApi(ctx) {
|
|
|
930
1093
|
}
|
|
931
1094
|
return true;
|
|
932
1095
|
}
|
|
933
|
-
m = path.match(/^\/api\/app-recoveries\/([^/]+)\/restore$/);
|
|
934
|
-
if (m && req.method === 'POST') {
|
|
935
|
-
try {
|
|
936
|
-
const result = restoreAppRecovery(projectRoot, decodeURIComponent(m[1]));
|
|
937
|
-
if (!result.ok) {
|
|
938
|
-
sendJson(res, result.status, result);
|
|
939
|
-
return true;
|
|
940
|
-
}
|
|
941
|
-
ctx.scheduleProjectRefresh?.('app-restored');
|
|
942
|
-
sendJson(res, 200, result);
|
|
943
|
-
}
|
|
944
|
-
catch (err) {
|
|
945
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
946
|
-
}
|
|
947
|
-
return true;
|
|
948
|
-
}
|
|
949
1096
|
if (m && req.method === 'PATCH') {
|
|
950
1097
|
const id = decodeURIComponent(m[1]);
|
|
951
1098
|
try {
|
|
@@ -964,7 +1111,7 @@ export async function handleAppsApi(ctx) {
|
|
|
964
1111
|
return true;
|
|
965
1112
|
}
|
|
966
1113
|
if (m && req.method === 'GET') {
|
|
967
|
-
const id = m[1];
|
|
1114
|
+
const id = decodeURIComponent(m[1]);
|
|
968
1115
|
const result = loadAppById(projectRoot, id);
|
|
969
1116
|
if (!result) {
|
|
970
1117
|
sendJson(res, 404, { error: `App "${id}" not found` });
|
|
@@ -973,6 +1120,22 @@ export async function handleAppsApi(ctx) {
|
|
|
973
1120
|
sendJson(res, 200, result);
|
|
974
1121
|
return true;
|
|
975
1122
|
}
|
|
1123
|
+
m = path.match(/^\/api\/app-recoveries\/([^/]+)\/restore$/);
|
|
1124
|
+
if (m && req.method === 'POST') {
|
|
1125
|
+
try {
|
|
1126
|
+
const result = restoreAppRecovery(projectRoot, decodeURIComponent(m[1]));
|
|
1127
|
+
if (!result.ok) {
|
|
1128
|
+
sendJson(res, result.status, result);
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
ctx.scheduleProjectRefresh?.('app-restored');
|
|
1132
|
+
sendJson(res, 200, result);
|
|
1133
|
+
}
|
|
1134
|
+
catch (err) {
|
|
1135
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
1136
|
+
}
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
976
1139
|
// /api/apps/:id/dashboards
|
|
977
1140
|
m = path.match(/^\/api\/apps\/([^/]+)\/dashboards$/);
|
|
978
1141
|
if (m && req.method === 'GET') {
|
|
@@ -1807,6 +1970,107 @@ export async function proposeAppAiBuild(projectRoot, input, hooks) {
|
|
|
1807
1970
|
kg.close();
|
|
1808
1971
|
}
|
|
1809
1972
|
}
|
|
1973
|
+
function appDraftSourceFromCatalog(source) {
|
|
1974
|
+
const certified = source.lifecycle === 'certified';
|
|
1975
|
+
return {
|
|
1976
|
+
id: source.sourceId,
|
|
1977
|
+
kind: 'block',
|
|
1978
|
+
sourceRef: source.executionRef,
|
|
1979
|
+
qualifiedIdentity: source.qualifiedIdentity,
|
|
1980
|
+
sourcePath: source.sourcePath,
|
|
1981
|
+
executionRef: source.executionRef,
|
|
1982
|
+
snapshotId: source.snapshotId,
|
|
1983
|
+
sourceRevision: source.sourceRevision,
|
|
1984
|
+
sourceFingerprint: source.sourceRevision,
|
|
1985
|
+
lifecycle: source.lifecycle,
|
|
1986
|
+
capabilities: source.capabilities,
|
|
1987
|
+
trustState: certified ? 'certified' : 'review_required',
|
|
1988
|
+
reviewStatus: certified ? 'not_required' : 'required',
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function appSourceCatalogRecordToLegacyRecommendation(source) {
|
|
1992
|
+
return {
|
|
1993
|
+
id: source.sourceId,
|
|
1994
|
+
name: source.name,
|
|
1995
|
+
domain: source.domain ?? 'uncategorized',
|
|
1996
|
+
status: source.lifecycle,
|
|
1997
|
+
owner: source.owner ?? null,
|
|
1998
|
+
tags: source.tags,
|
|
1999
|
+
path: source.executionRef,
|
|
2000
|
+
fingerprint: source.sourceRevision,
|
|
2001
|
+
lastModified: new Date(0).toISOString(),
|
|
2002
|
+
description: source.description ?? '',
|
|
2003
|
+
llmContext: null,
|
|
2004
|
+
chartType: source.capabilities.chartType,
|
|
2005
|
+
filterIds: source.capabilities.filters,
|
|
2006
|
+
dimensionIds: source.capabilities.dimensions,
|
|
2007
|
+
score: source.score ?? 0,
|
|
2008
|
+
reasons: source.reasons,
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
function uniqueAppSourceRecords(sources) {
|
|
2012
|
+
const seen = new Set();
|
|
2013
|
+
return sources.filter((source) => {
|
|
2014
|
+
if (seen.has(source.sourceId))
|
|
2015
|
+
return false;
|
|
2016
|
+
seen.add(source.sourceId);
|
|
2017
|
+
return true;
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
function uniqueTileId(existing, preferred) {
|
|
2021
|
+
const base = slugify(preferred) || 'component';
|
|
2022
|
+
const ids = new Set(existing.map((item) => item.i));
|
|
2023
|
+
if (!ids.has(base))
|
|
2024
|
+
return base;
|
|
2025
|
+
for (let index = 2; index < 10_000; index += 1) {
|
|
2026
|
+
const candidate = `${base}-${index}`;
|
|
2027
|
+
if (!ids.has(candidate))
|
|
2028
|
+
return candidate;
|
|
2029
|
+
}
|
|
2030
|
+
return `${base}-${Date.now()}`;
|
|
2031
|
+
}
|
|
2032
|
+
const APP_REQUIREMENT_STOP_WORDS = new Set([
|
|
2033
|
+
'a', 'an', 'and', 'app', 'application', 'build', 'by', 'create', 'dashboard',
|
|
2034
|
+
'for', 'from', 'in', 'last', 'me', 'of', 'on', 'report', 'show', 'showing',
|
|
2035
|
+
'the', 'to', 'using', 'view', 'with',
|
|
2036
|
+
]);
|
|
2037
|
+
function appRequirementTokens(value) {
|
|
2038
|
+
return new Set(value.toLowerCase().split(/[^a-z0-9]+/)
|
|
2039
|
+
.map((token) => token.length > 3 && token.endsWith('s') ? token.slice(0, -1) : token)
|
|
2040
|
+
.filter((token) => token.length > 1 && !APP_REQUIREMENT_STOP_WORDS.has(token)));
|
|
2041
|
+
}
|
|
2042
|
+
function catalogSourceSatisfiesRequirement(source, requirement) {
|
|
2043
|
+
const normalized = (values) => new Set(values.flatMap((value) => Array.from(appRequirementTokens(value))));
|
|
2044
|
+
const sourceMeasures = normalized(source.capabilities.measures);
|
|
2045
|
+
const sourceDimensions = normalized(source.capabilities.dimensions);
|
|
2046
|
+
const sourceFilters = normalized(source.capabilities.filters);
|
|
2047
|
+
const requires = [
|
|
2048
|
+
[normalized(requirement.measures), sourceMeasures],
|
|
2049
|
+
[normalized(requirement.dimensions), sourceDimensions],
|
|
2050
|
+
[normalized(requirement.filters), sourceFilters],
|
|
2051
|
+
];
|
|
2052
|
+
const declaredCompatible = requires.every(([required, available]) => (required.size === 0 || (available.size > 0 && Array.from(required).every((field) => available.has(field)))));
|
|
2053
|
+
const hasDeclaredRequirement = requires.some(([required]) => required.size > 0);
|
|
2054
|
+
if (declaredCompatible && hasDeclaredRequirement)
|
|
2055
|
+
return true;
|
|
2056
|
+
const requirementTerms = appRequirementTokens(requirement.question);
|
|
2057
|
+
if (requirementTerms.size === 0)
|
|
2058
|
+
return false;
|
|
2059
|
+
const sourceTerms = appRequirementTokens([
|
|
2060
|
+
source.title,
|
|
2061
|
+
source.name,
|
|
2062
|
+
source.description ?? '',
|
|
2063
|
+
source.qualifiedIdentity,
|
|
2064
|
+
...source.tags,
|
|
2065
|
+
...source.capabilities.measures,
|
|
2066
|
+
...source.capabilities.dimensions,
|
|
2067
|
+
].join(' '));
|
|
2068
|
+
const overlap = Array.from(requirementTerms).filter((term) => sourceTerms.has(term)).length;
|
|
2069
|
+
return overlap >= Math.min(2, requirementTerms.size);
|
|
2070
|
+
}
|
|
2071
|
+
function splitQueryValues(values) {
|
|
2072
|
+
return unique(values.flatMap((value) => value.split(',')).map((value) => value.trim()).filter(Boolean));
|
|
2073
|
+
}
|
|
1810
2074
|
function materializeAppBuildProposalReviewAnalyses(projectRoot, draft, plan, proposalId, candidates, inputDashboards) {
|
|
1811
2075
|
const eligible = candidates.filter((candidate) => (candidate.source === 'ai_generated'
|
|
1812
2076
|
&& Boolean(candidate.sql)
|
|
@@ -1956,157 +2220,153 @@ function materializeAppBuildProposalReviewAnalyses(projectRoot, draft, plan, pro
|
|
|
1956
2220
|
* any canonical App source.
|
|
1957
2221
|
*/
|
|
1958
2222
|
export async function proposeAppBuildDraftOperations(projectRoot, draft, input, hooks) {
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
2223
|
+
const prompt = cleanString(input.prompt) || draft.frame.goal;
|
|
2224
|
+
const { ensureAgentProjectReady, planAppBuildBrief, resolveAppSourceCatalogRecords, shortlistAppSources, } = await import('@duckcodeailabs/dql-agent');
|
|
2225
|
+
await ensureAgentProjectReady(projectRoot);
|
|
2226
|
+
let candidates = shortlistAppSources(projectRoot, prompt, {
|
|
2227
|
+
sourcePolicy: draft.sourcePolicy,
|
|
2228
|
+
domains: draft.pages[0]?.metadata.domain && draft.pages[0].metadata.domain !== 'general'
|
|
2229
|
+
? [draft.pages[0].metadata.domain]
|
|
2230
|
+
: undefined,
|
|
2231
|
+
}).items;
|
|
2232
|
+
const requestedIds = unique((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean));
|
|
2233
|
+
if (requestedIds.length) {
|
|
2234
|
+
const exactIds = requestedIds.filter((id) => id.startsWith('app:block:'));
|
|
2235
|
+
const exact = exactIds.length
|
|
2236
|
+
? resolveAppSourceCatalogRecords(projectRoot, exactIds, draft.sourcePolicy).items
|
|
2237
|
+
: [];
|
|
2238
|
+
const requested = new Set(requestedIds);
|
|
2239
|
+
candidates = uniqueAppSourceRecords([
|
|
2240
|
+
...exact,
|
|
2241
|
+
...candidates.filter((candidate) => requested.has(candidate.sourceId)
|
|
2242
|
+
|| requested.has(candidate.name)
|
|
2243
|
+
|| requested.has(candidate.sourcePath)
|
|
2244
|
+
|| requested.has(candidate.qualifiedIdentity)),
|
|
2245
|
+
...candidates,
|
|
2246
|
+
]).slice(0, 12);
|
|
2247
|
+
}
|
|
2248
|
+
const brief = await planAppBuildBrief({
|
|
2249
|
+
prompt,
|
|
2250
|
+
candidates,
|
|
2251
|
+
sourcePolicy: draft.sourcePolicy,
|
|
1962
2252
|
domain: draft.pages[0]?.metadata.domain,
|
|
1963
2253
|
audience: draft.frame.audience,
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
throw new Error(session.error ?? 'The App planner did not produce a proposal.');
|
|
1971
|
-
}
|
|
1972
|
-
const plan = session.plan;
|
|
1973
|
-
const { KGStore, defaultKgPath, materializeAppPlanDocuments } = await import('@duckcodeailabs/dql-agent');
|
|
1974
|
-
const kg = new KGStore(defaultKgPath(projectRoot));
|
|
1975
|
-
let dashboards;
|
|
1976
|
-
try {
|
|
1977
|
-
dashboards = materializeAppPlanDocuments(plan, kg).dashboards;
|
|
1978
|
-
}
|
|
1979
|
-
finally {
|
|
1980
|
-
kg.close();
|
|
1981
|
-
}
|
|
1982
|
-
const proposalTileById = new Map(session.proposal.tiles.map((tile) => [tile.id, tile]));
|
|
1983
|
-
const plannedTiles = plan.pages.flatMap((page) => page.tiles);
|
|
1984
|
-
const sourceOperations = plannedTiles.flatMap((tile) => {
|
|
1985
|
-
const proposalTile = proposalTileById.get(tile.id);
|
|
1986
|
-
if (tile.kind === 'certified_block' && tile.blockId) {
|
|
1987
|
-
return [{
|
|
1988
|
-
type: 'upsert_source',
|
|
1989
|
-
source: {
|
|
1990
|
-
id: `block:${tile.blockId}`,
|
|
1991
|
-
kind: 'certified_block',
|
|
1992
|
-
sourceRef: tile.blockId,
|
|
1993
|
-
sourceFingerprint: proposalTile?.preflight.sourceFingerprint,
|
|
1994
|
-
receiptId: proposalTile?.preflight.receiptId,
|
|
1995
|
-
trustState: 'certified',
|
|
1996
|
-
reviewStatus: 'not_required',
|
|
1997
|
-
},
|
|
1998
|
-
}];
|
|
1999
|
-
}
|
|
2000
|
-
if (tile.kind === 'semantic_query' && tile.semantic) {
|
|
2001
|
-
return [{
|
|
2002
|
-
type: 'upsert_source',
|
|
2003
|
-
source: {
|
|
2004
|
-
id: `semantic:${tile.semantic.id}`,
|
|
2005
|
-
kind: 'governed_semantic',
|
|
2006
|
-
sourceRef: tile.semantic.id,
|
|
2007
|
-
qualifiedIdentity: tile.semantic.qualifiedMetricIds?.join(',') || tile.semantic.id,
|
|
2008
|
-
snapshotId: tile.semantic.snapshotId,
|
|
2009
|
-
sourceFingerprint: tile.semantic.definitionFingerprint,
|
|
2010
|
-
receiptId: proposalTile?.preflight.receiptId,
|
|
2011
|
-
trustState: 'review_required',
|
|
2012
|
-
reviewStatus: 'required',
|
|
2013
|
-
},
|
|
2014
|
-
}];
|
|
2015
|
-
}
|
|
2016
|
-
return [];
|
|
2254
|
+
complete: hooks?.planAppBuild,
|
|
2255
|
+
});
|
|
2256
|
+
const byId = new Map(candidates.map((candidate) => [candidate.sourceId, candidate]));
|
|
2257
|
+
const plannedSources = brief.selectedSourceIds.flatMap((sourceId) => {
|
|
2258
|
+
const source = byId.get(sourceId);
|
|
2259
|
+
return source ? [source] : [];
|
|
2017
2260
|
});
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2261
|
+
const sourceOperations = plannedSources.map((source) => ({
|
|
2262
|
+
type: 'upsert_source',
|
|
2263
|
+
source: appDraftSourceFromCatalog(source),
|
|
2264
|
+
}));
|
|
2265
|
+
const page = draft.pages[0];
|
|
2266
|
+
if (!page)
|
|
2267
|
+
throw new Error('The App draft needs a page before AI can compose it.');
|
|
2268
|
+
const structuralItems = page.layout.items.filter((item) => Boolean(item.text));
|
|
2269
|
+
const baseY = structuralItems.reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
2270
|
+
const reservedItems = [...structuralItems];
|
|
2271
|
+
const componentTileIdById = new Map();
|
|
2272
|
+
const plannedItems = brief.components.flatMap((component, index) => {
|
|
2273
|
+
const source = byId.get(component.sourceId);
|
|
2274
|
+
if (!source)
|
|
2023
2275
|
return [];
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2276
|
+
const certified = source.lifecycle === 'certified';
|
|
2277
|
+
const tileId = uniqueTileId(reservedItems, component.id || `component-${index + 1}`);
|
|
2278
|
+
componentTileIdById.set(component.id, tileId);
|
|
2279
|
+
const tile = {
|
|
2280
|
+
i: tileId,
|
|
2281
|
+
x: (index % 2) * 6,
|
|
2282
|
+
y: baseY + Math.floor(index / 2) * 4,
|
|
2283
|
+
w: component.view === 'kpi' ? 3 : 6,
|
|
2284
|
+
h: component.view === 'kpi' ? 2 : 4,
|
|
2285
|
+
sourceId: source.sourceId,
|
|
2286
|
+
sourceRevision: source.sourceRevision,
|
|
2287
|
+
block: { ref: source.executionRef },
|
|
2288
|
+
viz: { type: component.view },
|
|
2289
|
+
title: component.title,
|
|
2290
|
+
sourceClass: certified ? 'certified_block' : 'exploratory_analysis',
|
|
2291
|
+
filterBindings: source.capabilities.filters.map((filter) => ({
|
|
2292
|
+
filter,
|
|
2293
|
+
binding: filter,
|
|
2294
|
+
mode: 'predicate',
|
|
2295
|
+
capability: 'preflight_required',
|
|
2296
|
+
})),
|
|
2297
|
+
review: { status: certified ? 'not_required' : 'required', sourceFingerprint: source.sourceRevision },
|
|
2298
|
+
trustState: certified ? 'certified' : 'review_required',
|
|
2299
|
+
reviewStatus: certified ? 'certified' : 'review_required',
|
|
2300
|
+
};
|
|
2301
|
+
reservedItems.push(tile);
|
|
2302
|
+
return [tile];
|
|
2303
|
+
});
|
|
2304
|
+
const nextPage = {
|
|
2305
|
+
...page,
|
|
2306
|
+
metadata: { ...page.metadata, description: brief.frame.decision || brief.frame.goal },
|
|
2307
|
+
layout: {
|
|
2308
|
+
...page.layout,
|
|
2309
|
+
items: [...structuralItems, ...plannedItems],
|
|
2310
|
+
responsive: responsiveLayoutsForDashboardItems([...structuralItems, ...plannedItems]),
|
|
2311
|
+
},
|
|
2029
2312
|
};
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
question: clarification.question,
|
|
2033
|
-
choices: clarification.choices.map((label, index) => ({ id: slugify(label) || `choice-${index + 1}`, label })),
|
|
2034
|
-
required: clarification.required,
|
|
2035
|
-
}));
|
|
2036
|
-
const replaceInitialBlank = !draft.baseApp
|
|
2037
|
-
&& draft.pages.length === 1
|
|
2038
|
-
&& draft.pages[0].layout.items.length === 0
|
|
2039
|
-
&& !dashboards.some((page) => page.id === draft.pages[0].id);
|
|
2040
|
-
const coverage = plan.requirementCoverage.map((item) => {
|
|
2041
|
-
const reviewCoverage = reviewAnalyses.coverageByRequirement.get(item.requirementId);
|
|
2042
|
-
if (reviewCoverage) {
|
|
2043
|
-
return {
|
|
2044
|
-
requirementId: item.requirementId,
|
|
2045
|
-
status: 'covered',
|
|
2046
|
-
sourceIds: reviewCoverage.sourceIds,
|
|
2047
|
-
componentIds: reviewCoverage.componentIds,
|
|
2048
|
-
reasons: ['Covered by explicitly selectable review-required App analysis.'],
|
|
2049
|
-
};
|
|
2050
|
-
}
|
|
2313
|
+
const coverage = brief.requirements.map((requirement) => {
|
|
2314
|
+
const components = brief.components.filter((component) => component.requirementIds.includes(requirement.id));
|
|
2051
2315
|
return {
|
|
2052
|
-
requirementId:
|
|
2053
|
-
status:
|
|
2054
|
-
sourceIds:
|
|
2055
|
-
componentIds:
|
|
2056
|
-
reasons:
|
|
2316
|
+
requirementId: requirement.id,
|
|
2317
|
+
status: components.length ? 'covered' : 'gap',
|
|
2318
|
+
sourceIds: Array.from(new Set(components.map((component) => component.sourceId))),
|
|
2319
|
+
componentIds: components.map((component) => componentTileIdById.get(component.id) ?? component.id),
|
|
2320
|
+
reasons: components.length
|
|
2321
|
+
? ['Covered by a source-bound App component from the validated Build Brief.']
|
|
2322
|
+
: ['No supplied source candidate covered this requirement.'],
|
|
2057
2323
|
};
|
|
2058
2324
|
});
|
|
2325
|
+
const clarifications = brief.frame.clarificationQuestions ?? [];
|
|
2326
|
+
const reviewTasks = plannedSources
|
|
2327
|
+
.filter((source) => source.lifecycle !== 'certified')
|
|
2328
|
+
.map((source) => ({
|
|
2329
|
+
type: 'set_review_task',
|
|
2330
|
+
task: {
|
|
2331
|
+
id: `review-source-${createHash('sha256').update(source.sourceId).digest('hex').slice(0, 12)}`,
|
|
2332
|
+
message: `${source.title} is ${source.lifecycle} and must be certified, replaced, or removed before Project publication.`,
|
|
2333
|
+
status: 'open',
|
|
2334
|
+
sourceId: source.sourceId,
|
|
2335
|
+
pageId: page.id,
|
|
2336
|
+
},
|
|
2337
|
+
}));
|
|
2059
2338
|
const operations = [
|
|
2060
|
-
{ type: 'set_name', name: plan.name },
|
|
2061
2339
|
{
|
|
2062
2340
|
type: 'set_frame',
|
|
2063
|
-
frame:
|
|
2064
|
-
goal: session.proposal.buildFrame.goal,
|
|
2065
|
-
decision: plan.businessGoal,
|
|
2066
|
-
audience: session.proposal.buildFrame.audience,
|
|
2067
|
-
metrics: session.proposal.buildFrame.metrics,
|
|
2068
|
-
dimensions: session.proposal.buildFrame.dimensions,
|
|
2069
|
-
grain: session.proposal.buildFrame.grain,
|
|
2070
|
-
timeRange: session.proposal.buildFrame.timeRange,
|
|
2071
|
-
comparison: session.proposal.buildFrame.comparison,
|
|
2072
|
-
filters: session.proposal.buildFrame.filters,
|
|
2073
|
-
desiredOutput: session.proposal.buildFrame.desiredOutput,
|
|
2074
|
-
clarificationQuestions: clarifications,
|
|
2075
|
-
},
|
|
2341
|
+
frame: brief.frame,
|
|
2076
2342
|
},
|
|
2077
2343
|
{
|
|
2078
2344
|
type: 'set_requirements',
|
|
2079
|
-
requirements:
|
|
2080
|
-
id: requirement.id,
|
|
2081
|
-
question: requirement.question,
|
|
2082
|
-
role: requirement.role,
|
|
2083
|
-
required: true,
|
|
2084
|
-
measures: requirement.measures,
|
|
2085
|
-
dimensions: requirement.dimensions,
|
|
2086
|
-
filters: requirement.filters,
|
|
2087
|
-
grain: requirement.grain,
|
|
2088
|
-
})),
|
|
2345
|
+
requirements: brief.requirements,
|
|
2089
2346
|
coverage,
|
|
2090
2347
|
},
|
|
2091
|
-
|
|
2092
|
-
...dashboards.map((page) => ({ type: 'upsert_page', page })),
|
|
2348
|
+
{ type: 'upsert_page', page: nextPage },
|
|
2093
2349
|
...sourceOperations,
|
|
2094
|
-
...
|
|
2350
|
+
...reviewTasks,
|
|
2095
2351
|
];
|
|
2096
2352
|
return {
|
|
2097
|
-
id:
|
|
2353
|
+
id: `proposal_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
2098
2354
|
draftId: draft.id,
|
|
2099
2355
|
baseRevision: draft.revision,
|
|
2100
2356
|
baseProposalHash: draft.proposalHash,
|
|
2101
2357
|
operations,
|
|
2102
|
-
defaultSelectedSourceIds:
|
|
2358
|
+
defaultSelectedSourceIds: plannedSources
|
|
2359
|
+
.filter((source) => source.eligibility.localPreview)
|
|
2360
|
+
.map((source) => source.sourceId),
|
|
2103
2361
|
clarifications,
|
|
2362
|
+
candidateSourceIds: brief.candidateSourceIds,
|
|
2363
|
+
warnings: brief.warnings,
|
|
2104
2364
|
summary: {
|
|
2105
|
-
requirements:
|
|
2106
|
-
covered:
|
|
2107
|
-
gaps:
|
|
2108
|
-
certifiedSources:
|
|
2109
|
-
semanticSources:
|
|
2365
|
+
requirements: brief.requirements.length,
|
|
2366
|
+
covered: coverage.filter((item) => item.status === 'covered').length,
|
|
2367
|
+
gaps: coverage.filter((item) => item.status === 'gap').length,
|
|
2368
|
+
certifiedSources: plannedSources.filter((source) => source.lifecycle === 'certified').length,
|
|
2369
|
+
semanticSources: 0,
|
|
2110
2370
|
},
|
|
2111
2371
|
};
|
|
2112
2372
|
}
|
|
@@ -3264,12 +3524,13 @@ export function approveAppSemanticTiles(projectRoot, appId, dashboardId, input)
|
|
|
3264
3524
|
function appPublicationReadiness(projectRoot, dashboards, options = {}) {
|
|
3265
3525
|
const blockers = [];
|
|
3266
3526
|
let governedTiles = 0;
|
|
3267
|
-
const blocks = collectBlockCandidates(projectRoot);
|
|
3527
|
+
const blocks = collectBlockCandidates(projectRoot, { preserveDuplicates: true });
|
|
3268
3528
|
for (const dashboard of dashboards) {
|
|
3269
3529
|
for (const item of dashboard.layout.items) {
|
|
3270
3530
|
if (item.block) {
|
|
3271
3531
|
governedTiles += 1;
|
|
3272
3532
|
const blockRef = item.block;
|
|
3533
|
+
const boundRevision = item.sourceRevision ?? item.review?.sourceFingerprint;
|
|
3273
3534
|
const block = blocks.find((candidate) => ('blockId' in blockRef
|
|
3274
3535
|
? candidate.id === blockRef.blockId || candidate.name === blockRef.blockId
|
|
3275
3536
|
: candidate.path === blockRef.ref));
|
|
@@ -3281,7 +3542,15 @@ function appPublicationReadiness(projectRoot, dashboards, options = {}) {
|
|
|
3281
3542
|
message: `${dashboard.id}/${item.i} no longer resolves to a certified block.`,
|
|
3282
3543
|
});
|
|
3283
3544
|
}
|
|
3284
|
-
else if (
|
|
3545
|
+
else if (!boundRevision) {
|
|
3546
|
+
blockers.push({
|
|
3547
|
+
dashboardId: dashboard.id,
|
|
3548
|
+
tileId: item.i,
|
|
3549
|
+
code: 'block_source_unversioned',
|
|
3550
|
+
message: `${dashboard.id}/${item.i} has no bound source revision; refresh the certified block before publication.`,
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
else if (boundRevision !== block.fingerprint) {
|
|
3285
3554
|
blockers.push({
|
|
3286
3555
|
dashboardId: dashboard.id,
|
|
3287
3556
|
tileId: item.i,
|
|
@@ -3350,6 +3619,457 @@ function appBuildDraftDir(projectRoot) {
|
|
|
3350
3619
|
// are persisted transactionally in .dql/local/apps.sqlite.
|
|
3351
3620
|
return join(projectRoot, '.dql', 'local', 'app-builds');
|
|
3352
3621
|
}
|
|
3622
|
+
function appBuildProposalDir(projectRoot, draftId) {
|
|
3623
|
+
return join(appBuildDraftDir(projectRoot), draftId, 'proposals');
|
|
3624
|
+
}
|
|
3625
|
+
function writeAppBuildDraftAiProposal(projectRoot, proposal) {
|
|
3626
|
+
if (!/^[a-z0-9_:-]+$/i.test(proposal.draftId) || !/^[a-z0-9_:-]+$/i.test(proposal.id)) {
|
|
3627
|
+
throw new Error('APP_BUILD_PROPOSAL_INVALID: proposal identity is invalid.');
|
|
3628
|
+
}
|
|
3629
|
+
const dir = appBuildProposalDir(projectRoot, proposal.draftId);
|
|
3630
|
+
mkdirSync(dir, { recursive: true });
|
|
3631
|
+
const path = join(dir, `${proposal.id}.json`);
|
|
3632
|
+
const candidate = `${path}.candidate`;
|
|
3633
|
+
writeFileSync(candidate, `${JSON.stringify(proposal, null, 2)}\n`, 'utf8');
|
|
3634
|
+
renameSync(candidate, path);
|
|
3635
|
+
}
|
|
3636
|
+
function loadAppBuildDraftAiProposal(projectRoot, draftId, proposalId) {
|
|
3637
|
+
if (!/^[a-z0-9_:-]+$/i.test(draftId) || !/^[a-z0-9_:-]+$/i.test(proposalId))
|
|
3638
|
+
return null;
|
|
3639
|
+
const path = join(appBuildProposalDir(projectRoot, draftId), `${proposalId}.json`);
|
|
3640
|
+
if (!existsSync(path))
|
|
3641
|
+
return null;
|
|
3642
|
+
try {
|
|
3643
|
+
const proposal = JSON.parse(readFileSync(path, 'utf8'));
|
|
3644
|
+
return proposal.draftId === draftId && proposal.id === proposalId ? proposal : null;
|
|
3645
|
+
}
|
|
3646
|
+
catch {
|
|
3647
|
+
return null;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
async function generateExplicitAppBuildGap(projectRoot, draft, previous, requirementId, generate) {
|
|
3651
|
+
const requirementOperation = previous.operations.find((operation) => operation.type === 'set_requirements');
|
|
3652
|
+
const requirement = requirementOperation?.type === 'set_requirements'
|
|
3653
|
+
? requirementOperation.requirements.find((candidate) => candidate.id === requirementId)
|
|
3654
|
+
: undefined;
|
|
3655
|
+
const coverage = requirementOperation?.type === 'set_requirements'
|
|
3656
|
+
? requirementOperation.coverage?.find((candidate) => candidate.requirementId === requirementId)
|
|
3657
|
+
: undefined;
|
|
3658
|
+
if (!requirement || coverage?.status === 'covered') {
|
|
3659
|
+
throw new Error('APP_BUILD_GAP_NOT_FOUND: choose an uncovered App requirement.');
|
|
3660
|
+
}
|
|
3661
|
+
const answer = await generate(requirement.question);
|
|
3662
|
+
const sql = cleanString(answer.sql);
|
|
3663
|
+
if (!sql)
|
|
3664
|
+
throw new Error('APP_BUILD_GAP_NO_QUERY: the provider did not return executable review DQL.');
|
|
3665
|
+
if (sql.includes('"""'))
|
|
3666
|
+
throw new Error('APP_BUILD_GAP_INVALID_QUERY: generated SQL contains an unsupported triple-quote delimiter.');
|
|
3667
|
+
const proposalId = `proposal_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3668
|
+
const key = slugify(requirement.id) || createHash('sha256').update(requirement.question).digest('hex').slice(0, 12);
|
|
3669
|
+
const sourceRef = `proposals/${proposalId}/drafts/${key}.dql`;
|
|
3670
|
+
const source = [
|
|
3671
|
+
`block "app_${slugify(draft.appId)}_${key}" {`,
|
|
3672
|
+
` domain = "${escapeDqlString(draft.pages[0]?.metadata.domain ?? 'general')}"`,
|
|
3673
|
+
' type = "custom"',
|
|
3674
|
+
' status = "review"',
|
|
3675
|
+
' owner = "local-app-builder"',
|
|
3676
|
+
` description = "${escapeDqlString(answer.text?.slice(0, 240) || requirement.question)}"`,
|
|
3677
|
+
` llmContext = "${escapeDqlString(`Explicitly generated App gap: ${requirement.question}`)}"`,
|
|
3678
|
+
' caveats = ["AI-generated App gap. Validate joins, grain, filters, and interpretation before promotion."]',
|
|
3679
|
+
' tags = ["app-scoped", "ai-generated", "needs-review"]',
|
|
3680
|
+
'',
|
|
3681
|
+
' query = """',
|
|
3682
|
+
sql,
|
|
3683
|
+
' """',
|
|
3684
|
+
'',
|
|
3685
|
+
' visualization {',
|
|
3686
|
+
` chart = "${escapeDqlString(normalizeGeneratedViz(answer.suggestedViz))}"`,
|
|
3687
|
+
' }',
|
|
3688
|
+
'}',
|
|
3689
|
+
'',
|
|
3690
|
+
].join('\n');
|
|
3691
|
+
const fingerprint = `sha256:${createHash('sha256').update(source).digest('hex')}`;
|
|
3692
|
+
const absolute = join(projectRoot, '.dql', 'local', 'app-builds', draft.id, sourceRef);
|
|
3693
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
3694
|
+
writeFileSync(absolute, source, 'utf8');
|
|
3695
|
+
const sourceId = `review-dql:gap:${createHash('sha256').update(`${proposalId}:${requirement.id}`).digest('hex').slice(0, 16)}`;
|
|
3696
|
+
const pageOperation = previous.operations.find((operation) => operation.type === 'upsert_page');
|
|
3697
|
+
if (!pageOperation || pageOperation.type !== 'upsert_page')
|
|
3698
|
+
throw new Error('APP_BUILD_PAGE_REQUIRED: the proposal has no App page.');
|
|
3699
|
+
const tileId = uniqueTileId(pageOperation.page.layout.items, `gap-${requirement.id}`);
|
|
3700
|
+
const y = pageOperation.page.layout.items.reduce((max, tile) => Math.max(max, tile.y + tile.h), 0);
|
|
3701
|
+
const tile = {
|
|
3702
|
+
i: tileId,
|
|
3703
|
+
x: 0,
|
|
3704
|
+
y,
|
|
3705
|
+
w: 6,
|
|
3706
|
+
h: 4,
|
|
3707
|
+
sourceId,
|
|
3708
|
+
sourceRevision: fingerprint,
|
|
3709
|
+
draftAnalysis: { ref: sourceRef, artifactFingerprint: fingerprint },
|
|
3710
|
+
viz: { type: normalizeGeneratedViz(answer.suggestedViz) },
|
|
3711
|
+
title: titleForGapQuestion(requirement.question),
|
|
3712
|
+
sourceClass: 'exploratory_analysis',
|
|
3713
|
+
review: { status: 'required', sourceFingerprint: fingerprint },
|
|
3714
|
+
trustState: 'review_required',
|
|
3715
|
+
reviewStatus: 'review_required',
|
|
3716
|
+
};
|
|
3717
|
+
const operations = previous.operations.map((operation) => {
|
|
3718
|
+
if (operation === pageOperation && operation.type === 'upsert_page') {
|
|
3719
|
+
const items = [...operation.page.layout.items, tile];
|
|
3720
|
+
return { type: 'upsert_page', page: { ...operation.page, layout: { ...operation.page.layout, items, responsive: responsiveLayoutsForDashboardItems(items) } } };
|
|
3721
|
+
}
|
|
3722
|
+
if (operation.type === 'set_requirements') {
|
|
3723
|
+
return {
|
|
3724
|
+
...operation,
|
|
3725
|
+
coverage: (operation.coverage ?? []).map((item) => item.requirementId === requirement.id
|
|
3726
|
+
? { ...item, status: 'covered', sourceIds: [sourceId], componentIds: [tileId], reasons: ['Covered by explicitly generated review-required App DQL.'] }
|
|
3727
|
+
: item),
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
return operation;
|
|
3731
|
+
});
|
|
3732
|
+
operations.push({
|
|
3733
|
+
type: 'upsert_source',
|
|
3734
|
+
source: {
|
|
3735
|
+
id: sourceId,
|
|
3736
|
+
kind: 'review_dql',
|
|
3737
|
+
sourceRef,
|
|
3738
|
+
qualifiedIdentity: tile.title,
|
|
3739
|
+
sourceRevision: fingerprint,
|
|
3740
|
+
sourceFingerprint: fingerprint,
|
|
3741
|
+
lifecycle: 'review',
|
|
3742
|
+
capabilities: { measures: requirement.measures, dimensions: requirement.dimensions, outputs: [], filters: requirement.filters, allowedVisualizations: [tile.viz.type], parameters: [] },
|
|
3743
|
+
trustState: 'review_required',
|
|
3744
|
+
reviewStatus: 'required',
|
|
3745
|
+
},
|
|
3746
|
+
});
|
|
3747
|
+
operations.push({
|
|
3748
|
+
type: 'set_review_task',
|
|
3749
|
+
task: {
|
|
3750
|
+
id: `review-gap-${key}`,
|
|
3751
|
+
message: `Validate the joins, grain, filters, and business interpretation for ${tile.title}.`,
|
|
3752
|
+
status: 'open',
|
|
3753
|
+
sourceId,
|
|
3754
|
+
pageId: pageOperation.page.id,
|
|
3755
|
+
tileId,
|
|
3756
|
+
},
|
|
3757
|
+
});
|
|
3758
|
+
return {
|
|
3759
|
+
...previous,
|
|
3760
|
+
id: proposalId,
|
|
3761
|
+
operations,
|
|
3762
|
+
warnings: Array.from(new Set([...(previous.warnings ?? []), 'Generated gap remains review-required and local-only.'])),
|
|
3763
|
+
summary: {
|
|
3764
|
+
...previous.summary,
|
|
3765
|
+
covered: previous.summary.covered + 1,
|
|
3766
|
+
gaps: Math.max(0, previous.summary.gaps - 1),
|
|
3767
|
+
},
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
async function composeStoredAppBuildDraft(projectRoot, current, request) {
|
|
3771
|
+
if (request.mode === 'ai') {
|
|
3772
|
+
const proposal = loadAppBuildDraftAiProposal(projectRoot, current.id, cleanString(request.proposalId));
|
|
3773
|
+
if (!proposal)
|
|
3774
|
+
throw new Error('APP_BUILD_PROPOSAL_NOT_FOUND: ask AI to rebuild the proposal.');
|
|
3775
|
+
if (proposal.baseRevision !== current.revision || proposal.baseProposalHash !== current.proposalHash) {
|
|
3776
|
+
throw new Error('APP_BUILD_PROPOSAL_CONFLICT: the proposal is based on an older draft revision.');
|
|
3777
|
+
}
|
|
3778
|
+
const selected = new Set(unique((request.selectedSourceIds ?? []).map(cleanString).filter(Boolean)));
|
|
3779
|
+
const available = new Map(proposal.operations.flatMap((operation) => (operation.type === 'upsert_source' ? [[operation.source.id, operation.source]] : [])));
|
|
3780
|
+
const missing = Array.from(selected).filter((sourceId) => !available.has(sourceId));
|
|
3781
|
+
if (missing.length)
|
|
3782
|
+
throw new Error(`APP_BUILD_SOURCE_NOT_IN_PROPOSAL: ${missing.join(', ')}`);
|
|
3783
|
+
for (const sourceId of selected) {
|
|
3784
|
+
const source = available.get(sourceId);
|
|
3785
|
+
if (current.sourcePolicy === 'governed_only' && source.trustState !== 'certified') {
|
|
3786
|
+
throw new Error(`APP_BUILD_REVIEW_POLICY_REQUIRED: enable review-required sources before adding ${source.qualifiedIdentity ?? source.sourceRef}.`);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
const operations = operationsForSelectedAppBuildSources(proposal.operations, selected);
|
|
3790
|
+
const next = applyAppBuildDraftOperations(current, current.revision, operations);
|
|
3791
|
+
writeStoredAppBuildDraft(projectRoot, next, { expectedRevision: current.revision, operations });
|
|
3792
|
+
const pages = operations.flatMap((operation) => operation.type === 'upsert_page' ? [operation.page] : []);
|
|
3793
|
+
return {
|
|
3794
|
+
draft: next,
|
|
3795
|
+
pageIds: pages.map((page) => page.id),
|
|
3796
|
+
tileIds: pages.flatMap((page) => page.layout.items.filter((item) => item.sourceId && selected.has(item.sourceId)).map((item) => item.i)),
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
const selections = request.selections ?? [];
|
|
3800
|
+
if (selections.length === 0)
|
|
3801
|
+
throw new Error('APP_BUILD_SOURCE_REQUIRED: choose at least one source.');
|
|
3802
|
+
if (selections.length > 100)
|
|
3803
|
+
throw new Error('APP_BUILD_SOURCE_LIMIT: at most 100 components can be composed at once.');
|
|
3804
|
+
const { ensureAgentProjectReady, resolveAppSourceCatalogRecords } = await import('@duckcodeailabs/dql-agent');
|
|
3805
|
+
await ensureAgentProjectReady(projectRoot);
|
|
3806
|
+
const resolved = resolveAppSourceCatalogRecords(projectRoot, selections.map((selection) => selection.sourceId), current.sourcePolicy);
|
|
3807
|
+
if (resolved.missingSourceIds.length) {
|
|
3808
|
+
throw new Error(`APP_BUILD_SOURCE_DRIFT: these sources no longer resolve: ${resolved.missingSourceIds.join(', ')}`);
|
|
3809
|
+
}
|
|
3810
|
+
const byId = new Map(resolved.items.map((source) => [source.sourceId, source]));
|
|
3811
|
+
const workingItems = new Map(current.pages.map((page) => [page.id, [...page.layout.items]]));
|
|
3812
|
+
const operations = [];
|
|
3813
|
+
const pageIds = new Set();
|
|
3814
|
+
const tileIds = [];
|
|
3815
|
+
const coverageAdditions = new Map();
|
|
3816
|
+
for (const [index, selection] of selections.entries()) {
|
|
3817
|
+
const source = byId.get(selection.sourceId);
|
|
3818
|
+
if (!source)
|
|
3819
|
+
throw new Error(`APP_BUILD_SOURCE_DRIFT: ${selection.sourceId} no longer resolves.`);
|
|
3820
|
+
if (!source.eligibility.localPreview) {
|
|
3821
|
+
throw new Error(`APP_BUILD_REVIEW_POLICY_REQUIRED: ${source.title} is ${source.lifecycle}; enable review-required sources to add it locally.`);
|
|
3822
|
+
}
|
|
3823
|
+
const page = current.pages.find((candidate) => candidate.id === cleanString(selection.pageId)) ?? current.pages[0];
|
|
3824
|
+
if (!page)
|
|
3825
|
+
throw new Error('APP_BUILD_PAGE_REQUIRED: add a page before adding a source.');
|
|
3826
|
+
const pageItems = workingItems.get(page.id) ?? [...page.layout.items];
|
|
3827
|
+
const tileId = uniqueTileId(pageItems, `${source.name}-${selection.view}`);
|
|
3828
|
+
const y = pageItems.reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
3829
|
+
const certified = source.lifecycle === 'certified';
|
|
3830
|
+
const view = selection.view === 'chart'
|
|
3831
|
+
? normalizeVizType(source.capabilities.chartType ?? source.capabilities.allowedVisualizations?.[0] ?? 'bar')
|
|
3832
|
+
: selection.view === 'kpi' ? 'kpi' : 'table';
|
|
3833
|
+
const tile = {
|
|
3834
|
+
i: tileId,
|
|
3835
|
+
x: (index % 2) * 6,
|
|
3836
|
+
y,
|
|
3837
|
+
w: selection.view === 'kpi' ? 3 : 6,
|
|
3838
|
+
h: selection.view === 'kpi' ? 2 : 4,
|
|
3839
|
+
sourceId: source.sourceId,
|
|
3840
|
+
sourceRevision: source.sourceRevision,
|
|
3841
|
+
block: { ref: source.executionRef },
|
|
3842
|
+
viz: { type: view },
|
|
3843
|
+
title: source.title,
|
|
3844
|
+
sourceClass: certified ? 'certified_block' : 'exploratory_analysis',
|
|
3845
|
+
filterBindings: source.capabilities.filters.map((filter) => ({ filter, binding: filter, mode: 'predicate', capability: 'preflight_required' })),
|
|
3846
|
+
review: { status: certified ? 'not_required' : 'required', sourceFingerprint: source.sourceRevision },
|
|
3847
|
+
trustState: certified ? 'certified' : 'review_required',
|
|
3848
|
+
reviewStatus: certified ? 'certified' : 'review_required',
|
|
3849
|
+
};
|
|
3850
|
+
pageItems.push(tile);
|
|
3851
|
+
workingItems.set(page.id, pageItems);
|
|
3852
|
+
operations.push({ type: 'upsert_source', source: appDraftSourceFromCatalog(source) });
|
|
3853
|
+
operations.push({ type: 'add_tile', pageId: page.id, tile });
|
|
3854
|
+
if (!certified) {
|
|
3855
|
+
operations.push({
|
|
3856
|
+
type: 'set_review_task',
|
|
3857
|
+
task: {
|
|
3858
|
+
id: `review-source-${createHash('sha256').update(source.sourceId).digest('hex').slice(0, 12)}`,
|
|
3859
|
+
message: `${source.title} is ${source.lifecycle} and must be certified, replaced, or removed before Project publication.`,
|
|
3860
|
+
status: 'open',
|
|
3861
|
+
sourceId: source.sourceId,
|
|
3862
|
+
pageId: page.id,
|
|
3863
|
+
tileId,
|
|
3864
|
+
},
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
pageIds.add(page.id);
|
|
3868
|
+
tileIds.push(tileId);
|
|
3869
|
+
for (const requirement of current.requirements) {
|
|
3870
|
+
if (!catalogSourceSatisfiesRequirement(source, requirement))
|
|
3871
|
+
continue;
|
|
3872
|
+
const addition = coverageAdditions.get(requirement.id) ?? { sourceIds: [], componentIds: [] };
|
|
3873
|
+
addition.sourceIds.push(source.sourceId);
|
|
3874
|
+
addition.componentIds.push(tileId);
|
|
3875
|
+
coverageAdditions.set(requirement.id, addition);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
if (current.requirements.length > 0) {
|
|
3879
|
+
const existingByRequirement = new Map(current.coverage.map((item) => [item.requirementId, item]));
|
|
3880
|
+
operations.push({
|
|
3881
|
+
type: 'set_coverage',
|
|
3882
|
+
coverage: current.requirements.map((requirement) => {
|
|
3883
|
+
const existing = existingByRequirement.get(requirement.id);
|
|
3884
|
+
const addition = coverageAdditions.get(requirement.id);
|
|
3885
|
+
const sourceIds = unique([...(existing?.sourceIds ?? []), ...(addition?.sourceIds ?? [])]);
|
|
3886
|
+
const componentIds = unique([...(existing?.componentIds ?? []), ...(addition?.componentIds ?? [])]);
|
|
3887
|
+
const newlyCovered = Boolean(addition?.sourceIds.length && addition.componentIds.length);
|
|
3888
|
+
return {
|
|
3889
|
+
requirementId: requirement.id,
|
|
3890
|
+
status: newlyCovered ? 'covered' : existing?.status ?? 'gap',
|
|
3891
|
+
sourceIds,
|
|
3892
|
+
componentIds,
|
|
3893
|
+
reasons: newlyCovered
|
|
3894
|
+
? unique([...(existing?.reasons ?? []), 'Covered by a server-resolved source added during manual composition.'])
|
|
3895
|
+
: existing?.reasons ?? ['No selected App source covers this requirement.'],
|
|
3896
|
+
};
|
|
3897
|
+
}),
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
const next = applyAppBuildDraftOperations(current, current.revision, operations);
|
|
3901
|
+
writeStoredAppBuildDraft(projectRoot, next, { expectedRevision: current.revision, operations });
|
|
3902
|
+
return { draft: next, pageIds: Array.from(pageIds), tileIds };
|
|
3903
|
+
}
|
|
3904
|
+
function operationsForSelectedAppBuildSources(operations, selected) {
|
|
3905
|
+
const keptComponentIds = new Set(operations.flatMap((operation) => {
|
|
3906
|
+
if (operation.type === 'upsert_page') {
|
|
3907
|
+
return operation.page.layout.items.filter((item) => !item.sourceId || selected.has(item.sourceId)).map((item) => item.i);
|
|
3908
|
+
}
|
|
3909
|
+
if (operation.type === 'add_tile' && (!operation.tile.sourceId || selected.has(operation.tile.sourceId)))
|
|
3910
|
+
return [operation.tile.i];
|
|
3911
|
+
return [];
|
|
3912
|
+
}));
|
|
3913
|
+
return operations.flatMap((operation) => {
|
|
3914
|
+
if (operation.type === 'upsert_source')
|
|
3915
|
+
return selected.has(operation.source.id) ? [operation] : [];
|
|
3916
|
+
if (operation.type === 'set_review_task') {
|
|
3917
|
+
return !operation.task.sourceId || selected.has(operation.task.sourceId) ? [operation] : [];
|
|
3918
|
+
}
|
|
3919
|
+
if (operation.type === 'upsert_page') {
|
|
3920
|
+
const items = operation.page.layout.items.filter((item) => !item.sourceId || selected.has(item.sourceId));
|
|
3921
|
+
return [{
|
|
3922
|
+
...operation,
|
|
3923
|
+
page: {
|
|
3924
|
+
...operation.page,
|
|
3925
|
+
layout: {
|
|
3926
|
+
...operation.page.layout,
|
|
3927
|
+
items,
|
|
3928
|
+
responsive: responsiveLayoutsForDashboardItems(items),
|
|
3929
|
+
},
|
|
3930
|
+
},
|
|
3931
|
+
}];
|
|
3932
|
+
}
|
|
3933
|
+
if (operation.type === 'set_requirements') {
|
|
3934
|
+
const coverage = (operation.coverage ?? []).map((item) => {
|
|
3935
|
+
const sourceIds = item.sourceIds.filter((sourceId) => selected.has(sourceId));
|
|
3936
|
+
const componentIds = item.componentIds.filter((componentId) => keptComponentIds.has(componentId));
|
|
3937
|
+
return {
|
|
3938
|
+
...item,
|
|
3939
|
+
sourceIds,
|
|
3940
|
+
componentIds,
|
|
3941
|
+
status: sourceIds.length && componentIds.length ? item.status : 'gap',
|
|
3942
|
+
};
|
|
3943
|
+
});
|
|
3944
|
+
return [{ ...operation, coverage }];
|
|
3945
|
+
}
|
|
3946
|
+
if (operation.type === 'set_coverage') {
|
|
3947
|
+
return [{
|
|
3948
|
+
...operation,
|
|
3949
|
+
coverage: operation.coverage.map((item) => {
|
|
3950
|
+
const sourceIds = item.sourceIds.filter((sourceId) => selected.has(sourceId));
|
|
3951
|
+
const componentIds = item.componentIds.filter((componentId) => keptComponentIds.has(componentId));
|
|
3952
|
+
return { ...item, sourceIds, componentIds, status: sourceIds.length && componentIds.length ? item.status : 'gap' };
|
|
3953
|
+
}),
|
|
3954
|
+
}];
|
|
3955
|
+
}
|
|
3956
|
+
return [operation];
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Rehydrate the canonical v3 source registry when a Git-owned App is opened in
|
|
3961
|
+
* Studio. Published dashboard documents intentionally keep the immutable
|
|
3962
|
+
* source binding on each tile; the mutable local draft must restore that
|
|
3963
|
+
* registry before core aggregate validation runs. This is metadata-only and
|
|
3964
|
+
* performs no request-time block scan. Current lifecycle/fingerprint checks
|
|
3965
|
+
* remain the responsibility of preview and publication preflight.
|
|
3966
|
+
*/
|
|
3967
|
+
function rehydratePublishedAppSources(pages) {
|
|
3968
|
+
const sources = new Map();
|
|
3969
|
+
const reviewTasks = new Map();
|
|
3970
|
+
let requiresReviewPolicy = false;
|
|
3971
|
+
const normalizedPages = pages.map((page) => ({
|
|
3972
|
+
...page,
|
|
3973
|
+
layout: {
|
|
3974
|
+
...page.layout,
|
|
3975
|
+
items: page.layout.items.map((tile) => {
|
|
3976
|
+
const hasDataSource = Boolean(tile.block || tile.semantic || tile.draftAnalysis || tile.aiPin);
|
|
3977
|
+
if (!hasDataSource)
|
|
3978
|
+
return tile;
|
|
3979
|
+
const blockRef = tile.block ? ('ref' in tile.block ? tile.block.ref : tile.block.blockId) : undefined;
|
|
3980
|
+
const sourceRef = blockRef
|
|
3981
|
+
?? tile.draftAnalysis?.ref
|
|
3982
|
+
?? tile.semantic?.id
|
|
3983
|
+
?? tile.aiPin?.id
|
|
3984
|
+
?? `${page.id}/${tile.i}`;
|
|
3985
|
+
const sourceRevision = tile.sourceRevision
|
|
3986
|
+
?? tile.review?.sourceFingerprint
|
|
3987
|
+
?? tile.draftAnalysis?.artifactFingerprint
|
|
3988
|
+
?? tile.semantic?.definitionFingerprint;
|
|
3989
|
+
const sourceId = tile.sourceId
|
|
3990
|
+
?? `published:${createHash('sha256').update(`${page.id}\0${tile.i}\0${sourceRef}`).digest('hex').slice(0, 20)}`;
|
|
3991
|
+
const certified = Boolean(tile.block
|
|
3992
|
+
&& sourceRevision
|
|
3993
|
+
&& tile.trustState === 'certified'
|
|
3994
|
+
&& tile.review?.status !== 'required');
|
|
3995
|
+
const kind = tile.semantic
|
|
3996
|
+
? 'governed_semantic'
|
|
3997
|
+
: tile.draftAnalysis
|
|
3998
|
+
? 'review_dql'
|
|
3999
|
+
: tile.block
|
|
4000
|
+
? 'block'
|
|
4001
|
+
: 'text';
|
|
4002
|
+
const nextSource = {
|
|
4003
|
+
id: sourceId,
|
|
4004
|
+
kind,
|
|
4005
|
+
sourceRef,
|
|
4006
|
+
qualifiedIdentity: tile.title || sourceRef,
|
|
4007
|
+
...(tile.block ? { sourcePath: sourceRef, executionRef: sourceRef } : {}),
|
|
4008
|
+
...(sourceRevision ? { sourceRevision, sourceFingerprint: sourceRevision } : {}),
|
|
4009
|
+
lifecycle: certified ? 'certified' : tile.draftAnalysis ? 'review' : 'unknown',
|
|
4010
|
+
capabilities: {
|
|
4011
|
+
measures: [],
|
|
4012
|
+
dimensions: [],
|
|
4013
|
+
outputs: [],
|
|
4014
|
+
filters: Array.from(new Set((tile.filterBindings ?? [])
|
|
4015
|
+
.filter((binding) => binding.capability !== 'unsupported')
|
|
4016
|
+
.map((binding) => binding.filter))),
|
|
4017
|
+
allowedVisualizations: Array.from(new Set([
|
|
4018
|
+
tile.viz.type,
|
|
4019
|
+
...(tile.display?.allowedVisualizations ?? []),
|
|
4020
|
+
])),
|
|
4021
|
+
parameters: [],
|
|
4022
|
+
},
|
|
4023
|
+
trustState: certified ? 'certified' : 'review_required',
|
|
4024
|
+
reviewStatus: certified ? 'not_required' : 'required',
|
|
4025
|
+
};
|
|
4026
|
+
const previous = sources.get(sourceId);
|
|
4027
|
+
if (!previous) {
|
|
4028
|
+
sources.set(sourceId, nextSource);
|
|
4029
|
+
}
|
|
4030
|
+
else if (previous.sourceRef !== nextSource.sourceRef
|
|
4031
|
+
|| previous.sourceRevision !== nextSource.sourceRevision
|
|
4032
|
+
|| previous.trustState !== nextSource.trustState) {
|
|
4033
|
+
requiresReviewPolicy = true;
|
|
4034
|
+
sources.set(sourceId, {
|
|
4035
|
+
...previous,
|
|
4036
|
+
lifecycle: 'unknown',
|
|
4037
|
+
trustState: 'review_required',
|
|
4038
|
+
reviewStatus: 'required',
|
|
4039
|
+
});
|
|
4040
|
+
reviewTasks.set(`published-source-${sourceId}`, {
|
|
4041
|
+
id: `published-source-${createHash('sha256').update(sourceId).digest('hex').slice(0, 12)}`,
|
|
4042
|
+
message: `${tile.title || sourceRef} has conflicting published source bindings. Refresh or replace it before publication.`,
|
|
4043
|
+
status: 'open',
|
|
4044
|
+
sourceId,
|
|
4045
|
+
pageId: page.id,
|
|
4046
|
+
tileId: tile.i,
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
if (!certified) {
|
|
4050
|
+
requiresReviewPolicy = true;
|
|
4051
|
+
reviewTasks.set(`published-source-${sourceId}`, {
|
|
4052
|
+
id: `published-source-${createHash('sha256').update(sourceId).digest('hex').slice(0, 12)}`,
|
|
4053
|
+
message: sourceRevision
|
|
4054
|
+
? `${tile.title || sourceRef} must be refreshed against the current project source before publication.`
|
|
4055
|
+
: `${tile.title || sourceRef} has no published source revision. Replace or refresh it before publication.`,
|
|
4056
|
+
status: 'open',
|
|
4057
|
+
sourceId,
|
|
4058
|
+
pageId: page.id,
|
|
4059
|
+
tileId: tile.i,
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
return tile.sourceId === sourceId ? tile : { ...tile, sourceId, ...(sourceRevision ? { sourceRevision } : {}) };
|
|
4063
|
+
}),
|
|
4064
|
+
},
|
|
4065
|
+
}));
|
|
4066
|
+
return {
|
|
4067
|
+
pages: normalizedPages,
|
|
4068
|
+
sources: Array.from(sources.values()),
|
|
4069
|
+
reviewTasks: Array.from(reviewTasks.values()),
|
|
4070
|
+
requiresReviewPolicy,
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
3353
4073
|
export function createStoredAppBuildDraft(projectRoot, input) {
|
|
3354
4074
|
const baseAppId = cleanString(input.baseAppId);
|
|
3355
4075
|
const base = baseAppId ? loadAppById(projectRoot, baseAppId) : null;
|
|
@@ -3386,7 +4106,8 @@ export function createStoredAppBuildDraft(projectRoot, input) {
|
|
|
3386
4106
|
.filter((page) => Boolean(page))
|
|
3387
4107
|
: [];
|
|
3388
4108
|
const template = input.template ?? 'blank';
|
|
3389
|
-
const
|
|
4109
|
+
const published = rehydratePublishedAppSources(basePages);
|
|
4110
|
+
const pages = published.pages.length ? published.pages : [applyAppStudioTemplate(blankPage, template, name)];
|
|
3390
4111
|
const draft = createAppBuildDraft({
|
|
3391
4112
|
id,
|
|
3392
4113
|
appId,
|
|
@@ -3394,23 +4115,19 @@ export function createStoredAppBuildDraft(projectRoot, input) {
|
|
|
3394
4115
|
...(base ? { baseApp: { appId: base.app.id, fingerprint: fingerprintAppDirectory(base.appDir) } } : {}),
|
|
3395
4116
|
authoringMode: input.authoringMode === 'manual' ? 'manual' : 'ai',
|
|
3396
4117
|
template,
|
|
3397
|
-
sourcePolicy: input.sourcePolicy,
|
|
4118
|
+
sourcePolicy: published.requiresReviewPolicy ? 'include_review_required' : input.sourcePolicy,
|
|
3398
4119
|
frame: {
|
|
3399
4120
|
goal: cleanString(input.goal) || `Create ${name}`,
|
|
3400
4121
|
audience: cleanString(input.audience) || base?.app.audience || 'stakeholders',
|
|
3401
4122
|
metrics: [], dimensions: [], filters: [], desiredOutput: 'One responsive analytical page',
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
{ id: 'certified', label: 'Choose a certified block' },
|
|
3407
|
-
{ id: 'semantic', label: 'Choose a semantic metric' },
|
|
3408
|
-
{ id: 'gap', label: 'Keep a visible gap' },
|
|
3409
|
-
],
|
|
3410
|
-
required: true,
|
|
3411
|
-
}],
|
|
4123
|
+
// Clarifications are returned only when the App-specific planner finds a
|
|
4124
|
+
// material ambiguity. A synthetic unanswered question here used to make
|
|
4125
|
+
// every new AI draft enter a UI dead-end before planning even ran.
|
|
4126
|
+
clarificationQuestions: [],
|
|
3412
4127
|
},
|
|
4128
|
+
sources: published.sources,
|
|
3413
4129
|
pages,
|
|
4130
|
+
reviewTasks: published.reviewTasks,
|
|
3414
4131
|
});
|
|
3415
4132
|
writeStoredAppBuildDraft(projectRoot, draft);
|
|
3416
4133
|
return draft;
|
|
@@ -3626,12 +4343,16 @@ function loadStoredAppBuildDraft(projectRoot, id) {
|
|
|
3626
4343
|
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
3627
4344
|
try {
|
|
3628
4345
|
const stored = storage.getAppBuildDraft(clean);
|
|
3629
|
-
if (stored)
|
|
3630
|
-
|
|
4346
|
+
if (stored) {
|
|
4347
|
+
const migrated = migrateLegacyAppBuildDraft(projectRoot, stored);
|
|
4348
|
+
if (migrated && stored.version !== migrated.version)
|
|
4349
|
+
storage.saveAppBuildDraft(migrated);
|
|
4350
|
+
return migrated;
|
|
4351
|
+
}
|
|
3631
4352
|
const legacyPath = join(appBuildDraftDir(projectRoot), `${clean}.json`);
|
|
3632
4353
|
if (!existsSync(legacyPath))
|
|
3633
4354
|
return null;
|
|
3634
|
-
const migrated = migrateLegacyAppBuildDraft(JSON.parse(readFileSync(legacyPath, 'utf-8')));
|
|
4355
|
+
const migrated = migrateLegacyAppBuildDraft(projectRoot, JSON.parse(readFileSync(legacyPath, 'utf-8')));
|
|
3635
4356
|
if (!migrated)
|
|
3636
4357
|
return null;
|
|
3637
4358
|
storage.saveAppBuildDraft(migrated);
|
|
@@ -3653,12 +4374,14 @@ export function writeStoredAppBuildDraft(projectRoot, draft, input = {}) {
|
|
|
3653
4374
|
storage.close();
|
|
3654
4375
|
}
|
|
3655
4376
|
}
|
|
3656
|
-
function migrateLegacyAppBuildDraft(value) {
|
|
4377
|
+
function migrateLegacyAppBuildDraft(projectRoot, value) {
|
|
3657
4378
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3658
4379
|
return null;
|
|
3659
4380
|
const legacy = value;
|
|
3660
|
-
if (legacy.version ===
|
|
4381
|
+
if (legacy.version === 3)
|
|
3661
4382
|
return legacy;
|
|
4383
|
+
if (legacy.version === 2)
|
|
4384
|
+
return migrateV2AppBuildDraft(projectRoot, legacy);
|
|
3662
4385
|
if (legacy.version !== 1 || typeof legacy.id !== 'string' || typeof legacy.appId !== 'string')
|
|
3663
4386
|
return null;
|
|
3664
4387
|
const frame = (legacy.frame && typeof legacy.frame === 'object' ? legacy.frame : {});
|
|
@@ -3704,6 +4427,128 @@ function migrateLegacyAppBuildDraft(value) {
|
|
|
3704
4427
|
updatedAt: typeof legacy.updatedAt === 'string' ? legacy.updatedAt : migrated.updatedAt,
|
|
3705
4428
|
};
|
|
3706
4429
|
}
|
|
4430
|
+
function migrateV2AppBuildDraft(projectRoot, legacy) {
|
|
4431
|
+
if (typeof legacy.id !== 'string' || typeof legacy.appId !== 'string')
|
|
4432
|
+
return null;
|
|
4433
|
+
const declarations = collectBlockCandidates(projectRoot, { preserveDuplicates: true });
|
|
4434
|
+
const oldSources = Array.isArray(legacy.sources) ? legacy.sources : [];
|
|
4435
|
+
const idMap = new Map();
|
|
4436
|
+
const revisionById = new Map();
|
|
4437
|
+
const pathById = new Map();
|
|
4438
|
+
const migrationTasks = [];
|
|
4439
|
+
const sources = oldSources.flatMap((raw, index) => {
|
|
4440
|
+
const oldId = cleanString(raw.id) || `legacy-source-${index + 1}`;
|
|
4441
|
+
const kind = cleanString(raw.kind);
|
|
4442
|
+
if (!['block', 'certified_block', 'review_block'].includes(kind)) {
|
|
4443
|
+
idMap.set(oldId, oldId);
|
|
4444
|
+
return [{ ...raw, id: oldId }];
|
|
4445
|
+
}
|
|
4446
|
+
const sourceRef = cleanString(raw.sourceRef);
|
|
4447
|
+
const fingerprint = cleanString(raw.sourceFingerprint);
|
|
4448
|
+
const matches = declarations.filter((candidate) => (candidate.path === sourceRef
|
|
4449
|
+
|| candidate.id === sourceRef
|
|
4450
|
+
|| candidate.name === sourceRef
|
|
4451
|
+
|| candidate.path.endsWith(`/${sourceRef}`)) && (!fingerprint || candidate.fingerprint === fingerprint));
|
|
4452
|
+
if (matches.length === 1) {
|
|
4453
|
+
const candidate = matches[0];
|
|
4454
|
+
const id = canonicalAppBlockSourceId(candidate);
|
|
4455
|
+
idMap.set(oldId, id);
|
|
4456
|
+
revisionById.set(id, candidate.fingerprint);
|
|
4457
|
+
pathById.set(id, candidate.path);
|
|
4458
|
+
const certified = candidate.status === 'certified';
|
|
4459
|
+
return [{
|
|
4460
|
+
id,
|
|
4461
|
+
kind: 'block',
|
|
4462
|
+
sourceRef: candidate.path,
|
|
4463
|
+
qualifiedIdentity: `${candidate.domain || 'global'}::block::${candidate.name}`,
|
|
4464
|
+
sourcePath: candidate.path,
|
|
4465
|
+
executionRef: candidate.path,
|
|
4466
|
+
sourceRevision: candidate.fingerprint,
|
|
4467
|
+
sourceFingerprint: candidate.fingerprint,
|
|
4468
|
+
lifecycle: certified ? 'certified' : candidate.status === 'review' ? 'review' : 'draft',
|
|
4469
|
+
capabilities: {
|
|
4470
|
+
measures: [],
|
|
4471
|
+
dimensions: candidate.dimensionIds,
|
|
4472
|
+
outputs: [],
|
|
4473
|
+
filters: candidate.filterIds,
|
|
4474
|
+
chartType: candidate.chartType,
|
|
4475
|
+
allowedVisualizations: candidate.chartType ? [candidate.chartType] : [],
|
|
4476
|
+
parameters: [],
|
|
4477
|
+
},
|
|
4478
|
+
trustState: certified ? 'certified' : 'review_required',
|
|
4479
|
+
reviewStatus: certified ? 'not_required' : 'required',
|
|
4480
|
+
}];
|
|
4481
|
+
}
|
|
4482
|
+
idMap.set(oldId, oldId);
|
|
4483
|
+
migrationTasks.push({
|
|
4484
|
+
id: `legacy-source-${createHash('sha256').update(oldId).digest('hex').slice(0, 12)}`,
|
|
4485
|
+
message: matches.length > 1
|
|
4486
|
+
? `Legacy source ${sourceRef || oldId} is ambiguous. Select the exact path and revision before previewing or publishing.`
|
|
4487
|
+
: `Legacy source ${sourceRef || oldId} no longer resolves. Replace or remove it before previewing or publishing.`,
|
|
4488
|
+
status: 'open',
|
|
4489
|
+
sourceId: oldId,
|
|
4490
|
+
});
|
|
4491
|
+
return [{
|
|
4492
|
+
id: oldId,
|
|
4493
|
+
kind: 'block',
|
|
4494
|
+
sourceRef: sourceRef || oldId,
|
|
4495
|
+
qualifiedIdentity: cleanString(raw.qualifiedIdentity) || sourceRef || oldId,
|
|
4496
|
+
sourceRevision: fingerprint || undefined,
|
|
4497
|
+
sourceFingerprint: fingerprint || undefined,
|
|
4498
|
+
lifecycle: 'unknown',
|
|
4499
|
+
capabilities: { measures: [], dimensions: [], outputs: [], filters: [], allowedVisualizations: [], parameters: [] },
|
|
4500
|
+
trustState: 'review_required',
|
|
4501
|
+
reviewStatus: 'required',
|
|
4502
|
+
}];
|
|
4503
|
+
});
|
|
4504
|
+
const pages = (Array.isArray(legacy.pages) ? legacy.pages : []).map((page) => ({
|
|
4505
|
+
...page,
|
|
4506
|
+
layout: {
|
|
4507
|
+
...page.layout,
|
|
4508
|
+
items: page.layout.items.map((item) => {
|
|
4509
|
+
if (item.sourceId)
|
|
4510
|
+
return item;
|
|
4511
|
+
const blockRef = item.block
|
|
4512
|
+
? ('ref' in item.block ? item.block.ref : item.block.blockId)
|
|
4513
|
+
: undefined;
|
|
4514
|
+
if (!blockRef)
|
|
4515
|
+
return item;
|
|
4516
|
+
const oldSource = oldSources.find((source) => {
|
|
4517
|
+
const ref = cleanString(source.sourceRef);
|
|
4518
|
+
return ref === blockRef || cleanString(source.id) === `block:${blockRef}` || cleanString(source.id) === `review-block:${blockRef}`;
|
|
4519
|
+
});
|
|
4520
|
+
const sourceId = oldSource ? idMap.get(cleanString(oldSource.id)) : undefined;
|
|
4521
|
+
if (!sourceId)
|
|
4522
|
+
return item;
|
|
4523
|
+
return {
|
|
4524
|
+
...item,
|
|
4525
|
+
sourceId,
|
|
4526
|
+
sourceRevision: revisionById.get(sourceId) ?? (cleanString(oldSource?.sourceFingerprint) || undefined),
|
|
4527
|
+
...(pathById.get(sourceId) ? { block: { ref: pathById.get(sourceId) } } : {}),
|
|
4528
|
+
};
|
|
4529
|
+
}),
|
|
4530
|
+
},
|
|
4531
|
+
}));
|
|
4532
|
+
const coverage = (Array.isArray(legacy.coverage) ? legacy.coverage : []).map((item) => ({
|
|
4533
|
+
...item,
|
|
4534
|
+
sourceIds: item.sourceIds.map((id) => idMap.get(id) ?? id),
|
|
4535
|
+
}));
|
|
4536
|
+
const migratedWithoutHash = {
|
|
4537
|
+
...legacy,
|
|
4538
|
+
version: 3,
|
|
4539
|
+
sources,
|
|
4540
|
+
pages,
|
|
4541
|
+
coverage,
|
|
4542
|
+
reviewTasks: [
|
|
4543
|
+
...(Array.isArray(legacy.reviewTasks) ? legacy.reviewTasks : []),
|
|
4544
|
+
...migrationTasks,
|
|
4545
|
+
],
|
|
4546
|
+
};
|
|
4547
|
+
return {
|
|
4548
|
+
...migratedWithoutHash,
|
|
4549
|
+
proposalHash: appBuildDraftHash({ ...migratedWithoutHash, proposalHash: undefined }),
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
3707
4552
|
export function deleteStoredAppBuildDraft(projectRoot, draft) {
|
|
3708
4553
|
const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
3709
4554
|
let deleted;
|
|
@@ -3828,22 +4673,33 @@ export function preflightStoredAppBuildDraft(projectRoot, draft) {
|
|
|
3828
4673
|
errors.push(`Run a current settled preview for page ${page.metadata.title || page.id} before Project publication.`);
|
|
3829
4674
|
}
|
|
3830
4675
|
for (const source of draft.sources) {
|
|
3831
|
-
if (source.kind === 'certified_block') {
|
|
4676
|
+
if (source.kind === 'block' || source.kind === 'certified_block') {
|
|
3832
4677
|
const block = blocks.find((candidate) => candidate.id === source.sourceRef || candidate.name === source.sourceRef || candidate.path === source.sourceRef);
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
if (block
|
|
3836
|
-
errors.push(`Source ${source.id}
|
|
4678
|
+
const lifecycle = source.lifecycle ?? (source.kind === 'certified_block' ? 'certified' : block?.status);
|
|
4679
|
+
const boundRevision = source.sourceRevision ?? source.sourceFingerprint;
|
|
4680
|
+
if (!block)
|
|
4681
|
+
errors.push(`Source ${source.id} no longer resolves at ${source.sourceRef}.`);
|
|
4682
|
+
if (block && canonicalAppBlockSourceId(block) !== source.id && source.kind === 'block')
|
|
4683
|
+
errors.push(`Source ${source.id} no longer matches its qualified path identity.`);
|
|
4684
|
+
if (!block || block.status !== 'certified' || lifecycle !== 'certified' || source.trustState !== 'certified') {
|
|
4685
|
+
errors.push(`Source ${source.qualifiedIdentity ?? source.id} is ${lifecycle ?? 'unknown'} and cannot be published to the Project.`);
|
|
4686
|
+
}
|
|
4687
|
+
if (block && !boundRevision) {
|
|
4688
|
+
errors.push(`Source ${source.qualifiedIdentity ?? source.id} has no bound source revision; refresh it before Project publication.`);
|
|
4689
|
+
}
|
|
4690
|
+
else if (block && boundRevision !== block.fingerprint) {
|
|
4691
|
+
errors.push(`Source ${source.qualifiedIdentity ?? source.id} changed after selection.`);
|
|
4692
|
+
}
|
|
3837
4693
|
}
|
|
3838
4694
|
if (source.kind === 'governed_semantic' || source.kind === 'semantic_query') {
|
|
3839
4695
|
if (source.reviewStatus !== 'approved' || !source.snapshotId || !source.receiptId) {
|
|
3840
4696
|
errors.push(`Source ${source.id} requires snapshot-bound semantic approval before Project publication.`);
|
|
3841
4697
|
}
|
|
3842
4698
|
}
|
|
3843
|
-
if (source.kind === 'review_block' || source.kind === 'review_dql' || source.kind === 'exploratory_sql') {
|
|
4699
|
+
if ((source.kind === 'block' && source.trustState !== 'certified') || source.kind === 'review_block' || source.kind === 'review_dql' || source.kind === 'exploratory_sql') {
|
|
3844
4700
|
errors.push(`Source ${source.id} is review-required and cannot be published to the Project.`);
|
|
3845
4701
|
}
|
|
3846
|
-
if ((source.kind === 'review_block' || source.kind === 'review_dql' || source.kind === 'exploratory_sql') && draft.sourcePolicy !== 'include_review_required') {
|
|
4702
|
+
if (((source.kind === 'block' && source.trustState !== 'certified') || source.kind === 'review_block' || source.kind === 'review_dql' || source.kind === 'exploratory_sql') && draft.sourcePolicy !== 'include_review_required') {
|
|
3847
4703
|
errors.push(`Source ${source.id} requires include_review_required policy.`);
|
|
3848
4704
|
}
|
|
3849
4705
|
}
|
|
@@ -3964,7 +4820,7 @@ export function publishStoredAppBuildDraft(projectRoot, draft) {
|
|
|
3964
4820
|
const domain = draft.pages[0]?.metadata.domain || 'general';
|
|
3965
4821
|
const name = draft.name.trim() || draft.appId;
|
|
3966
4822
|
const containsSemantic = draft.sources.some((source) => source.kind === 'governed_semantic' || source.kind === 'semantic_query');
|
|
3967
|
-
const hasCertifiedAnalysis = draft.sources.some((source) => source.kind === 'certified_block');
|
|
4823
|
+
const hasCertifiedAnalysis = draft.sources.some((source) => (source.kind === 'block' || source.kind === 'certified_block') && source.trustState === 'certified');
|
|
3968
4824
|
const lifecycle = hasCertifiedAnalysis && !containsSemantic ? 'certified' : 'review';
|
|
3969
4825
|
const app = {
|
|
3970
4826
|
version: 1,
|
|
@@ -5139,7 +5995,7 @@ function appReadme(app, audience, blocks, dashboardId = 'overview') {
|
|
|
5139
5995
|
'',
|
|
5140
5996
|
].join('\n');
|
|
5141
5997
|
}
|
|
5142
|
-
function collectBlockCandidates(projectRoot) {
|
|
5998
|
+
function collectBlockCandidates(projectRoot, options = {}) {
|
|
5143
5999
|
const blocks = [];
|
|
5144
6000
|
const seen = new Set();
|
|
5145
6001
|
const scanDir = (dir) => {
|
|
@@ -5189,6 +6045,8 @@ function collectBlockCandidates(projectRoot) {
|
|
|
5189
6045
|
};
|
|
5190
6046
|
scanDir(join(projectRoot, 'blocks'));
|
|
5191
6047
|
scanDir(join(projectRoot, 'domains'));
|
|
6048
|
+
if (options.preserveDuplicates)
|
|
6049
|
+
return blocks.sort((left, right) => left.path.localeCompare(right.path));
|
|
5192
6050
|
const byId = new Map();
|
|
5193
6051
|
for (const block of blocks) {
|
|
5194
6052
|
const current = byId.get(block.id);
|
|
@@ -5203,6 +6061,10 @@ function collectBlockCandidates(projectRoot) {
|
|
|
5203
6061
|
}
|
|
5204
6062
|
return Array.from(byId.values());
|
|
5205
6063
|
}
|
|
6064
|
+
function canonicalAppBlockSourceId(block) {
|
|
6065
|
+
const pathIdentity = createHash('sha256').update(`${block.path}\u0000${block.name}`).digest('hex').slice(0, 20);
|
|
6066
|
+
return `app:block:${block.domain || 'global'}:${pathIdentity}`;
|
|
6067
|
+
}
|
|
5206
6068
|
function declaredAppFilterIds(source) {
|
|
5207
6069
|
const filterSection = sectionBody(source, 'filterBindings');
|
|
5208
6070
|
const parameterSection = sectionBody(source, 'parameterPolicy');
|