@duckcodeailabs/dql-cli 1.5.1 → 1.5.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.
@@ -12,6 +12,7 @@ interface Ctx {
12
12
  path: string;
13
13
  projectRoot: string;
14
14
  executeSql?: (sql: string) => Promise<unknown>;
15
+ runNotebook?: (appId: string, notebookPath: string) => Promise<void>;
15
16
  }
16
17
  export declare function handleAppsApi(ctx: Ctx): Promise<boolean>;
17
18
  type AppListEntry = {
@@ -62,6 +63,7 @@ interface AppRecommendationRequest {
62
63
  interface AppCreateRequest {
63
64
  name?: string;
64
65
  domain?: string;
66
+ dashboardTitle?: string;
65
67
  subdomain?: string;
66
68
  groups?: string[];
67
69
  purpose?: string;
@@ -97,5 +99,48 @@ export declare function createAppPackage(projectRoot: string, input: AppCreateRe
97
99
  ok: false;
98
100
  error: string;
99
101
  };
102
+ declare function loadAppById(projectRoot: string, id: string): {
103
+ app: AppDocument;
104
+ dashboards: Array<{
105
+ id: string;
106
+ title: string;
107
+ description?: string;
108
+ itemCount: number;
109
+ }>;
110
+ notebooks: AppListEntry['notebooks'];
111
+ drafts: AppListEntry['drafts'];
112
+ aiPins: unknown[];
113
+ } | null;
114
+ export declare function listNotebookCandidates(projectRoot: string, app: AppDocument, appDir: string): Array<{
115
+ path: string;
116
+ title: string;
117
+ attached: boolean;
118
+ role?: 'source' | 'analysis' | 'supporting';
119
+ visibility?: NonNullable<AppDocument['visibility']>;
120
+ lastModified?: string;
121
+ }>;
122
+ export declare function createNotebookForApp(projectRoot: string, appId: string, input: {
123
+ name?: string;
124
+ title?: string;
125
+ role?: string;
126
+ visibility?: string;
127
+ template?: string;
128
+ }): {
129
+ ok: true;
130
+ path: string;
131
+ app: ReturnType<typeof loadAppById>;
132
+ preview: unknown;
133
+ } | {
134
+ ok: false;
135
+ error: string;
136
+ };
137
+ export declare function previewNotebookForApp(projectRoot: string, appId: string, notebookPath: string): {
138
+ ok: true;
139
+ preview: unknown;
140
+ } | {
141
+ ok: false;
142
+ status: number;
143
+ error: string;
144
+ };
100
145
  export {};
101
146
  //# sourceMappingURL=apps-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apps-api.d.ts","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apps-api.d.ts","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"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"}
package/dist/apps-api.js CHANGED
@@ -83,6 +83,73 @@ export async function handleAppsApi(ctx) {
83
83
  }
84
84
  return true;
85
85
  }
86
+ m = path.match(/^\/api\/apps\/([^/]+)\/notebook-candidates$/);
87
+ if (m && req.method === 'GET') {
88
+ const appId = decodeURIComponent(m[1]);
89
+ const loaded = loadAppById(projectRoot, appId);
90
+ if (!loaded) {
91
+ sendJson(res, 404, { error: `App "${appId}" not found` });
92
+ return true;
93
+ }
94
+ sendJson(res, 200, { notebooks: listNotebookCandidates(projectRoot, loaded.app, join(projectRoot, 'apps', appId)) });
95
+ return true;
96
+ }
97
+ m = path.match(/^\/api\/apps\/([^/]+)\/notebooks\/create$/);
98
+ if (m && req.method === 'POST') {
99
+ const appId = decodeURIComponent(m[1]);
100
+ try {
101
+ const body = await readJson(req);
102
+ const result = createNotebookForApp(projectRoot, appId, body);
103
+ if (!result.ok) {
104
+ sendJson(res, 400, { error: result.error });
105
+ return true;
106
+ }
107
+ sendJson(res, 201, result);
108
+ }
109
+ catch (err) {
110
+ sendJson(res, 500, { error: err.message });
111
+ }
112
+ return true;
113
+ }
114
+ m = path.match(/^\/api\/apps\/([^/]+)\/notebooks\/preview$/);
115
+ if (m && req.method === 'GET') {
116
+ const appId = decodeURIComponent(m[1]);
117
+ const notebookPath = ctx.url.searchParams.get('path') ?? '';
118
+ const result = previewNotebookForApp(projectRoot, appId, notebookPath);
119
+ if (!result.ok) {
120
+ sendJson(res, result.status, { error: result.error });
121
+ return true;
122
+ }
123
+ sendJson(res, 200, result.preview);
124
+ return true;
125
+ }
126
+ m = path.match(/^\/api\/apps\/([^/]+)\/notebooks\/run$/);
127
+ if (m && req.method === 'POST') {
128
+ const appId = decodeURIComponent(m[1]);
129
+ try {
130
+ const body = await readJson(req);
131
+ const notebookPath = cleanString(body.path);
132
+ if (!notebookPath) {
133
+ sendJson(res, 400, { error: 'path is required' });
134
+ return true;
135
+ }
136
+ if (!ctx.runNotebook) {
137
+ sendJson(res, 400, { error: 'Notebook run is unavailable in this host.' });
138
+ return true;
139
+ }
140
+ await ctx.runNotebook(appId, notebookPath);
141
+ const preview = previewNotebookForApp(projectRoot, appId, notebookPath);
142
+ if (!preview.ok) {
143
+ sendJson(res, preview.status, { error: preview.error });
144
+ return true;
145
+ }
146
+ sendJson(res, 200, { ok: true, preview: preview.preview });
147
+ }
148
+ catch (err) {
149
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
150
+ }
151
+ return true;
152
+ }
86
153
  m = path.match(/^\/api\/apps\/([^/]+)\/notebooks$/);
87
154
  if (m && req.method === 'POST') {
88
155
  const appId = decodeURIComponent(m[1]);
@@ -100,6 +167,79 @@ export async function handleAppsApi(ctx) {
100
167
  }
101
168
  return true;
102
169
  }
170
+ m = path.match(/^\/api\/apps\/([^/]+)\/conversations$/);
171
+ if (m) {
172
+ const appId = decodeURIComponent(m[1]);
173
+ if (!loadAppById(projectRoot, appId)) {
174
+ sendJson(res, 404, { error: `App "${appId}" not found` });
175
+ return true;
176
+ }
177
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
178
+ try {
179
+ if (req.method === 'GET') {
180
+ sendJson(res, 200, { conversations: storage.listAppConversations(appId) });
181
+ return true;
182
+ }
183
+ if (req.method === 'POST') {
184
+ const body = await readJson(req);
185
+ const conversation = storage.createAppConversation({
186
+ appId,
187
+ title: body.title,
188
+ dashboardId: body.dashboardId,
189
+ notebookPath: body.notebookPath,
190
+ messages: normalizeConversationMessages(body.messages),
191
+ });
192
+ sendJson(res, 201, { ok: true, conversation });
193
+ return true;
194
+ }
195
+ }
196
+ catch (err) {
197
+ sendJson(res, 500, { error: err.message });
198
+ return true;
199
+ }
200
+ finally {
201
+ storage.close();
202
+ }
203
+ }
204
+ m = path.match(/^\/api\/apps\/([^/]+)\/conversations\/([^/]+)$/);
205
+ if (m) {
206
+ const appId = decodeURIComponent(m[1]);
207
+ const conversationId = decodeURIComponent(m[2]);
208
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
209
+ try {
210
+ const conversation = storage.getAppConversation(conversationId);
211
+ if (!conversation || conversation.appId !== appId) {
212
+ sendJson(res, 404, { error: `Conversation "${conversationId}" not found` });
213
+ return true;
214
+ }
215
+ if (req.method === 'GET') {
216
+ sendJson(res, 200, { conversation });
217
+ return true;
218
+ }
219
+ if (req.method === 'PATCH') {
220
+ const body = await readJson(req);
221
+ const updated = storage.updateAppConversation(conversationId, {
222
+ title: body.title,
223
+ dashboardId: body.dashboardId,
224
+ notebookPath: body.notebookPath,
225
+ messages: body.messages ? normalizeConversationMessages(body.messages) : undefined,
226
+ });
227
+ sendJson(res, 200, { ok: true, conversation: updated });
228
+ return true;
229
+ }
230
+ if (req.method === 'DELETE') {
231
+ sendJson(res, 200, { ok: storage.deleteAppConversation(conversationId) });
232
+ return true;
233
+ }
234
+ }
235
+ catch (err) {
236
+ sendJson(res, 500, { error: err.message });
237
+ return true;
238
+ }
239
+ finally {
240
+ storage.close();
241
+ }
242
+ }
103
243
  m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins$/);
104
244
  if (m) {
105
245
  const appId = decodeURIComponent(m[1]);
@@ -388,6 +528,8 @@ export function createAppPackage(projectRoot, input) {
388
528
  const appDir = join(projectRoot, 'apps', id);
389
529
  if (existsSync(appDir))
390
530
  return { ok: false, error: `App already exists: ${id}` };
531
+ const dashboardTitle = cleanString(input.dashboardTitle) || 'Overview';
532
+ const dashboardId = slugify(dashboardTitle) || 'overview';
391
533
  const owner = cleanString(input.owners?.[0]) || `${process.env.USER ?? 'owner'}@local`;
392
534
  const audience = cleanString(input.audience);
393
535
  const subdomain = cleanString(input.subdomain);
@@ -451,13 +593,13 @@ export function createAppPackage(projectRoot, input) {
451
593
  ],
452
594
  rlsBindings: [],
453
595
  schedules: [],
454
- homepage: { type: 'dashboard', id: 'overview' },
596
+ homepage: { type: 'dashboard', id: dashboardId },
455
597
  };
456
598
  const dashboard = {
457
599
  version: 1,
458
- id: 'overview',
600
+ id: dashboardId,
459
601
  metadata: {
460
- title: `${name} Overview`,
602
+ title: dashboardTitle,
461
603
  description: cleanString(input.purpose) || `Starter dashboard for ${name}`,
462
604
  domain,
463
605
  subdomain: subdomain || undefined,
@@ -477,7 +619,7 @@ export function createAppPackage(projectRoot, input) {
477
619
  const paths = [
478
620
  join(appDir, 'dql.app.json'),
479
621
  join(appDir, 'README.md'),
480
- join(appDir, 'dashboards', 'overview.dqld'),
622
+ join(appDir, 'dashboards', `${dashboardId}.dqld`),
481
623
  join(appDir, 'notebooks'),
482
624
  join(appDir, 'drafts'),
483
625
  ];
@@ -485,8 +627,8 @@ export function createAppPackage(projectRoot, input) {
485
627
  mkdirSync(join(appDir, 'notebooks'), { recursive: true });
486
628
  mkdirSync(join(appDir, 'drafts'), { recursive: true });
487
629
  writeFileSync(join(appDir, 'dql.app.json'), JSON.stringify(app, null, 2) + '\n', 'utf-8');
488
- writeFileSync(join(appDir, 'dashboards', 'overview.dqld'), JSON.stringify(dashboard, null, 2) + '\n', 'utf-8');
489
- writeFileSync(join(appDir, 'README.md'), appReadme(app, audience, selectedBlocks), 'utf-8');
630
+ writeFileSync(join(appDir, 'dashboards', `${dashboardId}.dqld`), JSON.stringify(dashboard, null, 2) + '\n', 'utf-8');
631
+ writeFileSync(join(appDir, 'README.md'), appReadme(app, audience, selectedBlocks, dashboardId), 'utf-8');
490
632
  const created = collectAppsList(projectRoot).find((entry) => entry.id === id);
491
633
  if (!created)
492
634
  return { ok: false, error: `App was written but could not be reloaded: ${id}` };
@@ -494,7 +636,7 @@ export function createAppPackage(projectRoot, input) {
494
636
  ok: true,
495
637
  app: created,
496
638
  paths: paths.map((path) => path.startsWith(projectRoot) ? path.slice(projectRoot.length + 1) : path),
497
- dashboardId: 'overview',
639
+ dashboardId,
498
640
  };
499
641
  }
500
642
  function buildDashboardItems(blocks) {
@@ -584,7 +726,7 @@ function normalizeVizType(chartType) {
584
726
  return 'funnel';
585
727
  return 'table';
586
728
  }
587
- function appReadme(app, audience, blocks) {
729
+ function appReadme(app, audience, blocks, dashboardId = 'overview') {
588
730
  return [
589
731
  `# ${app.name}`,
590
732
  '',
@@ -597,7 +739,7 @@ function appReadme(app, audience, blocks) {
597
739
  `- Visibility: ${app.visibility}`,
598
740
  `- Lifecycle: ${app.lifecycle}`,
599
741
  `- Owners: ${app.owners.join(', ')}`,
600
- `- Starter dashboard: dashboards/overview.dqld`,
742
+ `- Starter dashboard: dashboards/${dashboardId}.dqld`,
601
743
  `- Supporting notebooks: notebooks/`,
602
744
  `- Draft blocks: drafts/`,
603
745
  '',
@@ -684,6 +826,17 @@ function matchArray(source, regex) {
684
826
  .map((item) => item.trim().replace(/^"|"$/g, ''))
685
827
  .filter(Boolean);
686
828
  }
829
+ function normalizeConversationMessages(messages) {
830
+ return (messages ?? [])
831
+ .map((message) => ({
832
+ id: cleanString(message.id) || undefined,
833
+ role: message.role === 'assistant' ? 'assistant' : 'user',
834
+ content: cleanString(message.content),
835
+ events: Array.isArray(message.events) ? message.events : [],
836
+ createdAt: cleanString(message.createdAt) || undefined,
837
+ }))
838
+ .filter((message) => message.content.length > 0);
839
+ }
687
840
  function cleanString(value) {
688
841
  return typeof value === 'string' ? value.trim() : '';
689
842
  }
@@ -726,8 +879,8 @@ function createDashboardForApp(projectRoot, appId, input) {
726
879
  const loaded = loadAppById(projectRoot, appId);
727
880
  if (!loaded)
728
881
  return { ok: false, error: `App "${appId}" not found` };
729
- const title = cleanString(input.title) || 'New tab';
730
- const id = slugify(cleanString(input.id) || title) || `tab-${Date.now()}`;
882
+ const title = cleanString(input.title) || 'New page';
883
+ const id = slugify(cleanString(input.id) || title) || `page-${Date.now()}`;
731
884
  if (!/^[a-z0-9][a-z0-9_-]*$/i.test(id))
732
885
  return { ok: false, error: 'dashboard id must be folder-safe' };
733
886
  const appDir = join(projectRoot, 'apps', appId);
@@ -739,7 +892,7 @@ function createDashboardForApp(projectRoot, appId, input) {
739
892
  id,
740
893
  metadata: {
741
894
  title,
742
- description: cleanString(input.description) || `${title} dashboard tab`,
895
+ description: cleanString(input.description) || `${title} dashboard page`,
743
896
  domain: loaded.app.domain,
744
897
  subdomain: loaded.app.subdomain,
745
898
  groups: loaded.app.groups ?? [],
@@ -925,6 +1078,120 @@ function loadAppById(projectRoot, id) {
925
1078
  }
926
1079
  return null;
927
1080
  }
1081
+ export function listNotebookCandidates(projectRoot, app, appDir) {
1082
+ const attached = new Map(listAppNotebookRefs(projectRoot, app, appDir).map((notebook) => [notebook.path, notebook]));
1083
+ const files = new Map();
1084
+ for (const root of ['notebooks', 'workbooks', 'apps']) {
1085
+ for (const file of scanFiles(join(projectRoot, root), '.dqlnb')) {
1086
+ const rel = relative(projectRoot, file).replaceAll('\\', '/');
1087
+ files.set(rel, file);
1088
+ }
1089
+ }
1090
+ for (const notebook of attached.values()) {
1091
+ const abs = join(projectRoot, notebook.path);
1092
+ if (existsSync(abs))
1093
+ files.set(notebook.path, abs);
1094
+ }
1095
+ return Array.from(files.entries())
1096
+ .map(([path, abs]) => {
1097
+ const ref = attached.get(path);
1098
+ const stat = statSyncSafe(abs);
1099
+ return {
1100
+ path,
1101
+ title: ref?.title ?? notebookTitleFromFile(abs) ?? titleFromPath(path),
1102
+ attached: Boolean(ref),
1103
+ role: ref?.role,
1104
+ visibility: ref?.visibility,
1105
+ lastModified: stat?.mtime.toISOString(),
1106
+ };
1107
+ })
1108
+ .sort((a, b) => Number(b.attached) - Number(a.attached) || a.title.localeCompare(b.title));
1109
+ }
1110
+ export function createNotebookForApp(projectRoot, appId, input) {
1111
+ const loaded = loadAppById(projectRoot, appId);
1112
+ if (!loaded)
1113
+ return { ok: false, error: `App "${appId}" not found` };
1114
+ const title = cleanString(input.title) || cleanString(input.name) || 'App analysis';
1115
+ const slug = slugify(cleanString(input.name) || title) || `notebook-${Date.now()}`;
1116
+ const appDir = join(projectRoot, 'apps', appId);
1117
+ const relPath = `apps/${appId}/notebooks/${slug}.dqlnb`;
1118
+ const absPath = join(projectRoot, relPath);
1119
+ if (existsSync(absPath))
1120
+ return { ok: false, error: `Notebook already exists: ${relPath}` };
1121
+ mkdirSync(dirname(absPath), { recursive: true });
1122
+ writeFileSync(absPath, buildAppNotebookTemplate(title, loaded.app, input.template), 'utf-8');
1123
+ const attached = attachNotebookToApp(projectRoot, appId, {
1124
+ path: relPath,
1125
+ title,
1126
+ role: normalizeNotebookRole(input.role),
1127
+ visibility: input.visibility,
1128
+ });
1129
+ if (!attached.ok)
1130
+ return { ok: false, error: attached.error };
1131
+ const preview = previewNotebookForApp(projectRoot, appId, relPath);
1132
+ return {
1133
+ ok: true,
1134
+ path: relPath,
1135
+ app: loadAppById(projectRoot, appId),
1136
+ preview: preview.ok ? preview.preview : null,
1137
+ };
1138
+ }
1139
+ export function previewNotebookForApp(projectRoot, appId, notebookPath) {
1140
+ if (!loadAppById(projectRoot, appId))
1141
+ return { ok: false, status: 404, error: `App "${appId}" not found` };
1142
+ const rel = cleanString(notebookPath).replaceAll('\\', '/');
1143
+ if (!rel || rel.startsWith('/') || rel.includes('..') || !rel.endsWith('.dqlnb')) {
1144
+ return { ok: false, status: 400, error: 'notebook path must be a project-relative .dqlnb path' };
1145
+ }
1146
+ const abs = join(projectRoot, rel);
1147
+ if (!existsSync(abs))
1148
+ return { ok: false, status: 404, error: `Notebook not found: ${rel}` };
1149
+ try {
1150
+ const raw = readFileSync(abs, 'utf-8');
1151
+ const parsed = JSON.parse(raw);
1152
+ const snapshot = readNotebookRunSnapshot(abs);
1153
+ const snapshotByCell = new Map();
1154
+ for (const entry of snapshot?.cells ?? []) {
1155
+ if (entry && typeof entry === 'object' && typeof entry.cellId === 'string') {
1156
+ snapshotByCell.set(String(entry.cellId), entry);
1157
+ }
1158
+ }
1159
+ const cells = (parsed.cells ?? []).map((cell, index) => {
1160
+ const id = typeof cell.id === 'string' ? cell.id : `cell-${index + 1}`;
1161
+ const snap = snapshotByCell.get(id);
1162
+ return {
1163
+ id,
1164
+ type: typeof cell.type === 'string' ? cell.type : 'sql',
1165
+ name: typeof cell.name === 'string' ? cell.name : typeof cell.title === 'string' ? cell.title : undefined,
1166
+ content: typeof cell.content === 'string' ? cell.content : typeof cell.source === 'string' ? cell.source : '',
1167
+ upstream: typeof cell.upstream === 'string' ? cell.upstream : undefined,
1168
+ chartConfig: cell.chartConfig ?? cell.config,
1169
+ tableConfig: cell.tableConfig,
1170
+ singleValueConfig: cell.singleValueConfig,
1171
+ pivotConfig: cell.pivotConfig,
1172
+ status: snap?.status ?? 'idle',
1173
+ result: snap?.result,
1174
+ error: snap?.error,
1175
+ executionCount: snap?.executionCount,
1176
+ executedAt: snap?.executedAt,
1177
+ };
1178
+ });
1179
+ return {
1180
+ ok: true,
1181
+ preview: {
1182
+ path: rel,
1183
+ title: parsed.title ?? parsed.metadata?.title ?? titleFromPath(rel),
1184
+ metadata: parsed.metadata ?? {},
1185
+ cells,
1186
+ snapshotFound: Boolean(snapshot),
1187
+ capturedAt: typeof snapshot?.capturedAt === 'string' ? snapshot.capturedAt : undefined,
1188
+ },
1189
+ };
1190
+ }
1191
+ catch (err) {
1192
+ return { ok: false, status: 400, error: err instanceof Error ? err.message : String(err) };
1193
+ }
1194
+ }
928
1195
  function attachNotebookToApp(projectRoot, appId, input) {
929
1196
  const notebookPath = cleanString(input.path).replaceAll('\\', '/');
930
1197
  if (!notebookPath)
@@ -985,6 +1252,78 @@ function listAppNotebookRefs(projectRoot, app, appDir) {
985
1252
  }
986
1253
  return Array.from(byPath.values()).sort((a, b) => (a.title ?? a.path).localeCompare(b.title ?? b.path));
987
1254
  }
1255
+ function buildAppNotebookTemplate(title, app, template) {
1256
+ const normalizedTemplate = cleanString(template) || 'blank';
1257
+ const cellId = (base) => `${slugify(base) || 'cell'}_${Math.random().toString(36).slice(2, 8)}`;
1258
+ const intro = [
1259
+ `# ${title}`,
1260
+ '',
1261
+ `App: ${app.name}`,
1262
+ `Domain: ${[app.domain, app.subdomain, ...(app.groups ?? [])].filter(Boolean).join(' / ')}`,
1263
+ '',
1264
+ 'Use this notebook for analysis that supports the App dashboard pages.',
1265
+ ].join('\n');
1266
+ const cells = [
1267
+ {
1268
+ id: cellId('intro'),
1269
+ type: 'markdown',
1270
+ content: intro,
1271
+ },
1272
+ {
1273
+ id: cellId('starter-sql'),
1274
+ type: 'sql',
1275
+ name: 'starter_query',
1276
+ content: '-- Write supporting SQL for this App here\nSELECT 1 AS value;',
1277
+ },
1278
+ ];
1279
+ if (normalizedTemplate === 'summary') {
1280
+ cells.push({
1281
+ id: cellId('summary'),
1282
+ type: 'markdown',
1283
+ content: '## Notes\n\nAdd observations, assumptions, and follow-up questions here.',
1284
+ });
1285
+ }
1286
+ return JSON.stringify({
1287
+ dqlnbVersion: 1,
1288
+ version: 1,
1289
+ title,
1290
+ metadata: {
1291
+ description: `Supporting notebook for ${app.name}`,
1292
+ status: 'draft',
1293
+ categories: [app.domain, app.subdomain, ...(app.groups ?? [])].filter(Boolean),
1294
+ createdAt: new Date().toISOString(),
1295
+ modifiedAt: new Date().toISOString(),
1296
+ },
1297
+ cells,
1298
+ }, null, 2) + '\n';
1299
+ }
1300
+ function notebookTitleFromFile(absPath) {
1301
+ try {
1302
+ const parsed = JSON.parse(readFileSync(absPath, 'utf-8'));
1303
+ if (typeof parsed.title === 'string' && parsed.title.trim())
1304
+ return parsed.title.trim();
1305
+ if (typeof parsed.metadata?.title === 'string' && parsed.metadata.title.trim())
1306
+ return parsed.metadata.title.trim();
1307
+ }
1308
+ catch {
1309
+ // fall back to path-derived title
1310
+ }
1311
+ return null;
1312
+ }
1313
+ function readNotebookRunSnapshot(absNotebookPath) {
1314
+ const snapshotPath = absNotebookPath.replace(/\.dqlnb$/i, '.run.json');
1315
+ if (!existsSync(snapshotPath))
1316
+ return null;
1317
+ try {
1318
+ return JSON.parse(readFileSync(snapshotPath, 'utf-8'));
1319
+ }
1320
+ catch {
1321
+ return null;
1322
+ }
1323
+ }
1324
+ function normalizeNotebookRole(value) {
1325
+ return value === 'source' || value === 'analysis' ? value : 'supporting';
1326
+ }
988
1327
  function listAppDrafts(projectRoot, appDir) {
989
1328
  return scanFiles(join(appDir, 'drafts'), '.dql').map((file) => {
990
1329
  const source = readFileSync(file, 'utf-8');