@mean-weasel/lineage 0.1.5 → 0.1.6

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/server.js CHANGED
@@ -339,6 +339,44 @@ function lineageDb() {
339
339
  );
340
340
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
341
341
  create index if not exists edges_child on asset_edges(project_id, child_asset_id);
342
+ create table if not exists asset_attempts (
343
+ id text primary key,
344
+ project_id text not null references projects(id),
345
+ node_asset_id text not null references assets(id),
346
+ asset_id text not null references assets(id),
347
+ attempt_index integer not null check (attempt_index > 0),
348
+ source text not null check (source in ('initial', 'generated_child', 'reroll')),
349
+ prompt text,
350
+ generation_job_id text,
351
+ file_path text,
352
+ checksum_sha256 text,
353
+ created_at text not null,
354
+ promoted_at text,
355
+ is_current integer not null check (is_current in (0, 1)),
356
+ unique(project_id, node_asset_id, attempt_index),
357
+ unique(project_id, node_asset_id, asset_id, source)
358
+ );
359
+ create unique index if not exists asset_attempts_one_current
360
+ on asset_attempts(project_id, node_asset_id)
361
+ where is_current = 1;
362
+ create index if not exists asset_attempts_node_created
363
+ on asset_attempts(project_id, node_asset_id, created_at);
364
+ create table if not exists asset_reroll_requests (
365
+ id text primary key,
366
+ project_id text not null references projects(id),
367
+ root_asset_id text not null references assets(id),
368
+ node_asset_id text not null references assets(id),
369
+ status text not null check (status in ('pending', 'resolved', 'cancelled')),
370
+ requested_by text not null check (requested_by in ('human', 'agent', 'system')),
371
+ notes text,
372
+ created_at text not null,
373
+ resolved_at text
374
+ );
375
+ create unique index if not exists asset_reroll_requests_one_pending
376
+ on asset_reroll_requests(project_id, root_asset_id, node_asset_id)
377
+ where status = 'pending';
378
+ create index if not exists asset_reroll_requests_root_status
379
+ on asset_reroll_requests(project_id, root_asset_id, status, created_at);
342
380
  create table if not exists asset_reviews (
343
381
  asset_id text primary key references assets(id),
344
382
  review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
@@ -543,7 +581,7 @@ function lineageDb() {
543
581
  project_id text not null references projects(id),
544
582
  provider text not null default 'codex-handoff',
545
583
  adapter_version text not null,
546
- source_mode text not null check (source_mode in ('lineage_selection')),
584
+ source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
547
585
  root_asset_id text not null references assets(id),
548
586
  prompt text not null,
549
587
  expected_output_count integer not null check (expected_output_count > 0),
@@ -561,7 +599,7 @@ function lineageDb() {
561
599
  project_id text not null references projects(id),
562
600
  asset_id text not null references assets(id),
563
601
  root_asset_id text not null references assets(id),
564
- role text not null check (role in ('lineage_next_base', 'reference')),
602
+ role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
565
603
  position integer not null,
566
604
  selection_strategy text not null,
567
605
  selection_snapshot_json text not null,
@@ -2085,6 +2123,11 @@ function rootFor(database, project, assetId) {
2085
2123
  }
2086
2124
  return assetId;
2087
2125
  }
2126
+ function assertCanonicalRoot(database, project, root) {
2127
+ requireAsset2(database, project, root);
2128
+ const canonicalRoot = rootFor(database, project, root);
2129
+ if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
2130
+ }
2088
2131
  function explicitWorkspaceRoot(database, project, assetId) {
2089
2132
  const row = database.prepare(`
2090
2133
  select root_asset_id from lineage_workspaces
@@ -2141,6 +2184,68 @@ function resolveRoot(database, project, rootAssetId) {
2141
2184
  function edgeId(project, parent, child) {
2142
2185
  return `${project}:${parent}:derived_from:${child}`;
2143
2186
  }
2187
+ function rerollRequestId(project, root, node, timestamp) {
2188
+ return `${project}:${root}:reroll:${node}:${Date.parse(timestamp) || Date.now()}`;
2189
+ }
2190
+ function rowString(value) {
2191
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2192
+ }
2193
+ function rerollRequestFrom(row) {
2194
+ return {
2195
+ id: String(row.id),
2196
+ project_id: String(row.project_id),
2197
+ root_asset_id: String(row.root_asset_id),
2198
+ node_asset_id: String(row.node_asset_id),
2199
+ status: row.status,
2200
+ requested_by: row.requested_by,
2201
+ notes: rowString(row.notes),
2202
+ created_at: String(row.created_at),
2203
+ resolved_at: rowString(row.resolved_at)
2204
+ };
2205
+ }
2206
+ function attemptFrom(row) {
2207
+ return {
2208
+ id: String(row.id),
2209
+ project_id: String(row.project_id),
2210
+ node_asset_id: String(row.node_asset_id),
2211
+ asset_id: String(row.asset_id),
2212
+ attempt_index: Number(row.attempt_index),
2213
+ source: row.source,
2214
+ prompt: rowString(row.prompt),
2215
+ generation_job_id: rowString(row.generation_job_id),
2216
+ file_path: rowString(row.file_path),
2217
+ checksum_sha256: rowString(row.checksum_sha256),
2218
+ created_at: String(row.created_at),
2219
+ promoted_at: rowString(row.promoted_at),
2220
+ is_current: Boolean(Number(row.is_current))
2221
+ };
2222
+ }
2223
+ function implicitAttempt(row, isCurrent = true) {
2224
+ const createdAt = row.asset_created_at || nowIso();
2225
+ return {
2226
+ id: `${row.project}:${row.asset_id}:attempt:implicit`,
2227
+ project_id: row.project,
2228
+ node_asset_id: row.asset_id,
2229
+ asset_id: row.asset_id,
2230
+ attempt_index: 1,
2231
+ source: "initial",
2232
+ file_path: row.local_path,
2233
+ checksum_sha256: row.checksum_sha256,
2234
+ created_at: createdAt,
2235
+ promoted_at: createdAt,
2236
+ is_current: isCurrent
2237
+ };
2238
+ }
2239
+ function withImplicitAttempt(physicalAttempts, row) {
2240
+ if (physicalAttempts.some((attempt) => attempt.source === "initial")) return physicalAttempts;
2241
+ return [...physicalAttempts, implicitAttempt(row, !physicalAttempts.some((attempt) => attempt.is_current))];
2242
+ }
2243
+ function assertNodeInRoot(database, project, root, node) {
2244
+ assertCanonicalRoot(database, project, root);
2245
+ requireAsset2(database, project, node);
2246
+ const nodeRoot = rootFor(database, project, node);
2247
+ if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
2248
+ }
2144
2249
  function canPreviewLocally(mediaType, localPath) {
2145
2250
  return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
2146
2251
  }
@@ -2211,11 +2316,21 @@ function getLineageSnapshot(project, assetId) {
2211
2316
  const rows = database.prepare(`
2212
2317
  select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
2213
2318
  a.local_path, a.s3_key, a.checksum_sha256, coalesce(r.review_state, 'unreviewed') review_state,
2214
- r.notes review_notes, l.x layout_x, l.y layout_y
2319
+ r.notes review_notes, a.created_at asset_created_at, l.x layout_x, l.y layout_y
2215
2320
  from assets a left join asset_reviews r on r.asset_id = a.id
2216
2321
  left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
2217
2322
  where a.project_id = ? and a.id in (${placeholders2})
2218
2323
  `).all(root, project, ...ids);
2324
+ const attemptRows = ids.length > 0 ? database.prepare(`select * from asset_attempts where project_id = ? and node_asset_id in (${placeholders2}) order by node_asset_id, attempt_index desc`).all(project, ...ids) : [];
2325
+ const attemptsByNode = /* @__PURE__ */ new Map();
2326
+ for (const attempt of attemptRows.map(attemptFrom)) {
2327
+ attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
2328
+ }
2329
+ const rerollRows = ids.length > 0 ? database.prepare(`select * from asset_reroll_requests where project_id = ? and root_asset_id = ? and status = 'pending' and node_asset_id in (${placeholders2}) order by created_at`).all(project, root, ...ids) : [];
2330
+ const rerollsByNode = new Map(rerollRows.map((row) => {
2331
+ const request = rerollRequestFrom(row);
2332
+ return [request.node_asset_id, request];
2333
+ }));
2219
2334
  const selected = selectedRows(database, project, root);
2220
2335
  const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
2221
2336
  const selectedIds = new Set(selected.map((row) => row.asset_id));
@@ -2228,13 +2343,19 @@ function getLineageSnapshot(project, assetId) {
2228
2343
  const selection = selections[0] || null;
2229
2344
  const nodes = rows.map((row) => {
2230
2345
  const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
2231
- const { layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
2346
+ const { asset_created_at: _assetCreatedAt, layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
2232
2347
  const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
2348
+ const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
2349
+ const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
2350
+ const previewPath = currentAttempt?.file_path || row.local_path;
2233
2351
  return {
2234
2352
  ...node,
2353
+ attempt_count: attempts.length,
2354
+ current_attempt: currentAttempt,
2235
2355
  is_latest: !childIds.has(row.asset_id),
2236
2356
  position,
2237
- preview_url: canPreviewLocally(row.media_type, row.local_path) ? localPreviewUrl(project, row.local_path) : void 0,
2357
+ preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
2358
+ reroll_request: rerollsByNode.get(row.asset_id),
2238
2359
  selection_note: nodeSelection?.notes,
2239
2360
  user_selected: selectedIds.has(row.asset_id)
2240
2361
  };
@@ -2368,6 +2489,170 @@ function getLineageChildren(project, parentAssetId) {
2368
2489
  fetchedAt: nowIso()
2369
2490
  };
2370
2491
  }
2492
+ function listLineageRerollRequests(project, rootAssetId) {
2493
+ const database = lineageDb();
2494
+ try {
2495
+ assertCanonicalRoot(database, project, rootAssetId);
2496
+ const rows = database.prepare(`
2497
+ select * from asset_reroll_requests
2498
+ where project_id = ? and root_asset_id = ? and status = 'pending'
2499
+ order by created_at, id
2500
+ `).all(project, rootAssetId);
2501
+ return { project, root_asset_id: rootAssetId, requests: rows.map(rerollRequestFrom), fetchedAt: nowIso() };
2502
+ } finally {
2503
+ database.close();
2504
+ }
2505
+ }
2506
+ function getLineageAttempts(project, rootAssetId, nodeAssetId) {
2507
+ const database = lineageDb();
2508
+ try {
2509
+ assertNodeInRoot(database, project, rootAssetId, nodeAssetId);
2510
+ const rows = database.prepare(`
2511
+ select * from asset_attempts
2512
+ where project_id = ? and node_asset_id = ?
2513
+ order by attempt_index desc, created_at desc
2514
+ `).all(project, nodeAssetId);
2515
+ const asset = database.prepare("select id asset_id, project_id project, local_path, checksum_sha256, created_at asset_created_at from assets where project_id = ? and id = ?").get(project, nodeAssetId);
2516
+ return { project, root_asset_id: rootAssetId, node_asset_id: nodeAssetId, attempts: withImplicitAttempt(rows.map(attemptFrom), asset), fetchedAt: nowIso() };
2517
+ } finally {
2518
+ database.close();
2519
+ }
2520
+ }
2521
+ function promoteLineageAttempt(project, fields) {
2522
+ const database = lineageDb();
2523
+ try {
2524
+ assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
2525
+ const asset = database.prepare("select id asset_id, project_id project, local_path, checksum_sha256, created_at asset_created_at from assets where project_id = ? and id = ?").get(project, fields.nodeAssetId);
2526
+ const rows = database.prepare(`
2527
+ select * from asset_attempts
2528
+ where project_id = ? and node_asset_id = ?
2529
+ order by attempt_index desc, created_at desc
2530
+ `).all(project, fields.nodeAssetId);
2531
+ const attempts = withImplicitAttempt(rows.map(attemptFrom), asset);
2532
+ const target = attempts.find((attempt2) => attempt2.id === fields.attemptId);
2533
+ if (!target) throw new LineageError(`Attempt ${fields.attemptId} is not in ${fields.nodeAssetId}`, 404);
2534
+ const timestamp = nowIso();
2535
+ const promotedAttempt = { ...target, is_current: true, promoted_at: timestamp };
2536
+ if (!fields.confirmWrite) {
2537
+ return {
2538
+ ok: true,
2539
+ dryRun: true,
2540
+ project,
2541
+ root_asset_id: fields.rootAssetId,
2542
+ node_asset_id: fields.nodeAssetId,
2543
+ attempt: promotedAttempt,
2544
+ attempts: attempts.map((attempt2) => ({ ...attempt2, is_current: attempt2.id === target.id, promoted_at: attempt2.id === target.id ? timestamp : attempt2.promoted_at })),
2545
+ fetchedAt: timestamp
2546
+ };
2547
+ }
2548
+ database.exec("BEGIN IMMEDIATE");
2549
+ try {
2550
+ database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
2551
+ if (target.source !== "initial") {
2552
+ const result = database.prepare(`
2553
+ update asset_attempts
2554
+ set is_current = 1, promoted_at = ?
2555
+ where project_id = ? and node_asset_id = ? and id = ?
2556
+ `).run(timestamp, project, fields.nodeAssetId, target.id);
2557
+ if (result.changes !== 1) throw new LineageError(`Attempt ${fields.attemptId} is not in ${fields.nodeAssetId}`, 404);
2558
+ }
2559
+ database.exec("COMMIT");
2560
+ } catch (error) {
2561
+ database.exec("ROLLBACK");
2562
+ throw error;
2563
+ }
2564
+ const refreshedRows = database.prepare(`
2565
+ select * from asset_attempts
2566
+ where project_id = ? and node_asset_id = ?
2567
+ order by attempt_index desc, created_at desc
2568
+ `).all(project, fields.nodeAssetId);
2569
+ const refreshedAttempts = withImplicitAttempt(refreshedRows.map(attemptFrom), asset);
2570
+ const attempt = refreshedAttempts.find((item) => item.id === target.id) || promotedAttempt;
2571
+ return {
2572
+ ok: true,
2573
+ project,
2574
+ root_asset_id: fields.rootAssetId,
2575
+ node_asset_id: fields.nodeAssetId,
2576
+ attempt,
2577
+ attempts: refreshedAttempts,
2578
+ fetchedAt: nowIso()
2579
+ };
2580
+ } finally {
2581
+ database.close();
2582
+ }
2583
+ }
2584
+ function markLineageRerollRequest(project, fields) {
2585
+ const database = lineageDb();
2586
+ try {
2587
+ assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
2588
+ const existing = database.prepare(`
2589
+ select * from asset_reroll_requests
2590
+ where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
2591
+ order by created_at desc limit 1
2592
+ `).get(project, fields.rootAssetId, fields.nodeAssetId);
2593
+ const timestamp = nowIso();
2594
+ const request = existing ? {
2595
+ ...rerollRequestFrom(existing),
2596
+ notes: fields.notes || rerollRequestFrom(existing).notes,
2597
+ requested_by: fields.requestedBy || rerollRequestFrom(existing).requested_by
2598
+ } : {
2599
+ id: rerollRequestId(project, fields.rootAssetId, fields.nodeAssetId, timestamp),
2600
+ project_id: project,
2601
+ root_asset_id: fields.rootAssetId,
2602
+ node_asset_id: fields.nodeAssetId,
2603
+ status: "pending",
2604
+ requested_by: fields.requestedBy || "human",
2605
+ notes: fields.notes,
2606
+ created_at: timestamp
2607
+ };
2608
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
2609
+ if (existing) {
2610
+ database.prepare(`
2611
+ update asset_reroll_requests
2612
+ set requested_by = ?, notes = ?
2613
+ where id = ?
2614
+ `).run(request.requested_by, request.notes || null, request.id);
2615
+ } else {
2616
+ database.prepare(`
2617
+ insert into asset_reroll_requests (id, project_id, root_asset_id, node_asset_id, status, requested_by, notes, created_at, resolved_at)
2618
+ values (?, ?, ?, ?, 'pending', ?, ?, ?, null)
2619
+ `).run(request.id, project, fields.rootAssetId, fields.nodeAssetId, request.requested_by, request.notes || null, request.created_at);
2620
+ }
2621
+ database.prepare(`
2622
+ insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
2623
+ values (?, 'needs_revision', ?, null, ?, ?)
2624
+ on conflict(asset_id) do update set
2625
+ review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
2626
+ ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
2627
+ `).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
2628
+ return { ok: true, request: rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)) };
2629
+ } finally {
2630
+ database.close();
2631
+ }
2632
+ }
2633
+ function clearLineageRerollRequest(project, fields) {
2634
+ const database = lineageDb();
2635
+ try {
2636
+ assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
2637
+ const existing = database.prepare(`
2638
+ select * from asset_reroll_requests
2639
+ where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
2640
+ order by created_at desc limit 1
2641
+ `).get(project, fields.rootAssetId, fields.nodeAssetId);
2642
+ if (!existing) throw new LineageError(`No pending re-roll request for ${fields.nodeAssetId}`, 404);
2643
+ const timestamp = nowIso();
2644
+ const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
2645
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
2646
+ database.prepare(`
2647
+ update asset_reroll_requests
2648
+ set status = 'cancelled', resolved_at = ?
2649
+ where id = ?
2650
+ `).run(timestamp, request.id);
2651
+ return { ok: true, request };
2652
+ } finally {
2653
+ database.close();
2654
+ }
2655
+ }
2371
2656
  function updateSelectedAsset(project, fields) {
2372
2657
  const database = lineageDb();
2373
2658
  const inputAssetIds = normalizeSelectionInput(fields);
@@ -4734,7 +5019,7 @@ function itemFromFile(file) {
4734
5019
  };
4735
5020
  }
4736
5021
  function demoMarkdownItems(kind) {
4737
- const root = join7(repoRoot, "demo-project", "channels");
5022
+ const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join7(repoRoot, "demo-project", "channels");
4738
5023
  if (!existsSync4(root)) return [];
4739
5024
  return walk(root).map(itemFromFile).filter((item) => Boolean(item)).filter((item) => kind === "all" || `${item.kind}s` === kind).sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
4740
5025
  }
@@ -5136,7 +5421,7 @@ function loadGenerationJob(database, project, id) {
5136
5421
  project_id: String(row.project_id),
5137
5422
  provider: row.provider,
5138
5423
  adapter_version: String(row.adapter_version),
5139
- source_mode: "lineage_selection",
5424
+ source_mode: String(row.source_mode),
5140
5425
  root_asset_id: String(row.root_asset_id),
5141
5426
  prompt: String(row.prompt),
5142
5427
  expected_output_count: Number(row.expected_output_count),
@@ -5245,6 +5530,15 @@ function swissifierRelativePath(manifest, asset) {
5245
5530
  function swissifierFilePath(manifest, asset) {
5246
5531
  return join8(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
5247
5532
  }
5533
+ function swissifierRerollRelativePath(manifest, attempt) {
5534
+ return join8(manifest.media.target_dir, "reroll-attempts", attempt.file);
5535
+ }
5536
+ function swissifierRerollFilePath(manifest, attempt) {
5537
+ return join8(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
5538
+ }
5539
+ function swissifierRerollFixturePath(attempt) {
5540
+ return join8(dirname3(swissifierManifestPath), "swissifier-rerolls", attempt.file);
5541
+ }
5248
5542
  function swissifierSourcePath(manifest, asset, sourceDir) {
5249
5543
  const direct = join8(sourceDir, asset.file);
5250
5544
  if (existsSync5(direct)) return direct;
@@ -5542,6 +5836,27 @@ function writeDemoFiles(project) {
5542
5836
  for (const asset of demoAssets) writeDemoFile(project, asset);
5543
5837
  return ids;
5544
5838
  }
5839
+ function swissifierRerollAsset(attempt) {
5840
+ const source = swissifierRerollFixturePath(attempt);
5841
+ if (!existsSync5(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
5842
+ const body = readFileSync4(source);
5843
+ if (body.length !== attempt.size_bytes) throw new Error(`Unexpected Swissifier re-roll fixture size for ${attempt.file}`);
5844
+ const checksumSha256 = sha256Hex(body);
5845
+ if (checksumSha256 !== attempt.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier re-roll fixture: ${attempt.file}`);
5846
+ return {
5847
+ assetId: `local-${checksumSha256.slice(0, 12)}`,
5848
+ checksumSha256,
5849
+ sizeBytes: body.length
5850
+ };
5851
+ }
5852
+ function writeSwissifierRerollFiles(manifest) {
5853
+ for (const attempt of manifest.reroll_attempts || []) {
5854
+ swissifierRerollAsset(attempt);
5855
+ const target = swissifierRerollFilePath(manifest, attempt);
5856
+ mkdirSync5(dirname3(target), { recursive: true });
5857
+ copyFileSync2(swissifierRerollFixturePath(attempt), target);
5858
+ }
5859
+ }
5545
5860
  function upsertDemoAssets(project, ids) {
5546
5861
  const database = lineageDb();
5547
5862
  const timestamp = nowIso();
@@ -5632,10 +5947,100 @@ function upsertSwissifierAssets(project, manifest) {
5632
5947
  );
5633
5948
  reviewStatement.run(asset.asset_id, timestamp);
5634
5949
  }
5950
+ for (const attempt of manifest.reroll_attempts || []) {
5951
+ const generated = swissifierRerollAsset(attempt);
5952
+ assetStatement.run(
5953
+ generated.assetId,
5954
+ project,
5955
+ swissifierRerollRelativePath(manifest, attempt),
5956
+ generated.checksumSha256,
5957
+ attempt.title,
5958
+ "planned",
5959
+ "local",
5960
+ manifest.campaign,
5961
+ manifest.audience,
5962
+ generated.sizeBytes,
5963
+ attempt.content_type,
5964
+ timestamp,
5965
+ timestamp,
5966
+ timestamp
5967
+ );
5968
+ reviewStatement.run(generated.assetId, timestamp);
5969
+ }
5635
5970
  } finally {
5636
5971
  database.close();
5637
5972
  }
5638
- return { catalog: 0, local: manifest.assets.length, total: manifest.assets.length };
5973
+ const attemptCount = manifest.reroll_attempts?.length || 0;
5974
+ return { catalog: 0, local: manifest.assets.length + attemptCount, total: manifest.assets.length + attemptCount };
5975
+ }
5976
+ function upsertSwissifierRerollAttempts(project, manifest) {
5977
+ const attempts = manifest.reroll_attempts || [];
5978
+ if (attempts.length === 0) return { total: 0 };
5979
+ const database = lineageDb();
5980
+ const timestamp = nowIso();
5981
+ try {
5982
+ database.exec("BEGIN IMMEDIATE");
5983
+ try {
5984
+ const nodes = new Set(attempts.map((attempt) => attempt.node_asset_id));
5985
+ for (const nodeAssetId of nodes) {
5986
+ database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, nodeAssetId);
5987
+ }
5988
+ const statement = database.prepare(`
5989
+ insert into asset_attempts (
5990
+ id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
5991
+ file_path, checksum_sha256, created_at, promoted_at, is_current
5992
+ ) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, ?)
5993
+ on conflict(id) do update set
5994
+ asset_id = excluded.asset_id,
5995
+ source = excluded.source,
5996
+ prompt = excluded.prompt,
5997
+ generation_job_id = excluded.generation_job_id,
5998
+ file_path = excluded.file_path,
5999
+ checksum_sha256 = excluded.checksum_sha256,
6000
+ promoted_at = excluded.promoted_at,
6001
+ is_current = excluded.is_current
6002
+ `);
6003
+ for (const attempt of attempts) {
6004
+ const generated = swissifierRerollAsset(attempt);
6005
+ statement.run(
6006
+ `${project}:${attempt.node_asset_id}:attempt:${attempt.attempt_index}`,
6007
+ project,
6008
+ attempt.node_asset_id,
6009
+ generated.assetId,
6010
+ attempt.attempt_index,
6011
+ attempt.prompt,
6012
+ attempt.generation_job_id,
6013
+ swissifierRerollRelativePath(manifest, attempt),
6014
+ generated.checksumSha256,
6015
+ timestamp,
6016
+ timestamp,
6017
+ attempt.current ? 1 : 0
6018
+ );
6019
+ }
6020
+ for (const nodeAssetId of nodes) {
6021
+ const currentCount = database.prepare("select count(*) count from asset_attempts where project_id = ? and node_asset_id = ? and is_current = 1").get(project, nodeAssetId);
6022
+ if (currentCount.count === 0) {
6023
+ database.prepare(`
6024
+ update asset_attempts
6025
+ set is_current = 1, promoted_at = ?
6026
+ where id = (
6027
+ select id from asset_attempts
6028
+ where project_id = ? and node_asset_id = ?
6029
+ order by attempt_index desc
6030
+ limit 1
6031
+ )
6032
+ `).run(timestamp, project, nodeAssetId);
6033
+ }
6034
+ }
6035
+ database.exec("COMMIT");
6036
+ } catch (error) {
6037
+ database.exec("ROLLBACK");
6038
+ throw error;
6039
+ }
6040
+ } finally {
6041
+ database.close();
6042
+ }
6043
+ return { total: attempts.length };
5639
6044
  }
5640
6045
  function seedDemoLineageWorkspace(project, fields) {
5641
6046
  const ids = fields.confirmWrite ? writeDemoFiles(project) : demoAssetIds();
@@ -5694,10 +6099,12 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
5694
6099
  media_status: swissifierRichDemoMediaStatus(project)
5695
6100
  };
5696
6101
  }
6102
+ writeSwissifierRerollFiles(manifest);
5697
6103
  const summary = upsertSwissifierAssets(project, manifest);
5698
6104
  for (const edge of manifest.edges) {
5699
6105
  linkLineageAssets(project, { parentAssetId: edge.parent, childAssetId: edge.child, confirmWrite: true });
5700
6106
  }
6107
+ const reroll_attempts = upsertSwissifierRerollAttempts(project, manifest);
5701
6108
  updateSelectedAsset(project, {
5702
6109
  assetIds: manifest.selected_asset_ids,
5703
6110
  confirmWrite: true,
@@ -5725,6 +6132,7 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
5725
6132
  media_status: swissifierRichDemoMediaStatus(project),
5726
6133
  root_asset_id: manifest.root_asset_id,
5727
6134
  selected_asset_ids: manifest.selected_asset_ids,
6135
+ reroll_attempts,
5728
6136
  summary,
5729
6137
  workspace
5730
6138
  };
@@ -5931,6 +6339,36 @@ app.get(
5931
6339
  })
5932
6340
  );
5933
6341
  registerLineageWorkspaceRoutes(app, projectFrom, asyncRoute);
6342
+ app.get("/api/lineage/:rootAssetId/rerolls", asyncRoute((req, res) => {
6343
+ res.json(listLineageRerollRequests(projectFrom(req), req.params.rootAssetId));
6344
+ }));
6345
+ app.post("/api/lineage/:rootAssetId/rerolls/:nodeAssetId", asyncRoute((req, res) => {
6346
+ res.json(markLineageRerollRequest(projectFrom(req), {
6347
+ rootAssetId: req.params.rootAssetId,
6348
+ nodeAssetId: req.params.nodeAssetId,
6349
+ notes: typeof req.body.notes === "string" ? req.body.notes : void 0,
6350
+ requestedBy: req.body.requestedBy === "agent" || req.body.requestedBy === "system" ? req.body.requestedBy : "human",
6351
+ confirmWrite: req.body.confirmWrite === true
6352
+ }));
6353
+ }));
6354
+ app.post("/api/lineage/:rootAssetId/rerolls/:nodeAssetId/cancel", asyncRoute((req, res) => {
6355
+ res.json(clearLineageRerollRequest(projectFrom(req), {
6356
+ rootAssetId: req.params.rootAssetId,
6357
+ nodeAssetId: req.params.nodeAssetId,
6358
+ confirmWrite: req.body.confirmWrite === true
6359
+ }));
6360
+ }));
6361
+ app.get("/api/lineage/:rootAssetId/attempts/:nodeAssetId", asyncRoute((req, res) => {
6362
+ res.json(getLineageAttempts(projectFrom(req), req.params.rootAssetId, req.params.nodeAssetId));
6363
+ }));
6364
+ app.post("/api/lineage/:rootAssetId/attempts/:nodeAssetId/promote", asyncRoute((req, res) => {
6365
+ res.json(promoteLineageAttempt(projectFrom(req), {
6366
+ rootAssetId: req.params.rootAssetId,
6367
+ nodeAssetId: req.params.nodeAssetId,
6368
+ attemptId: String(req.body.attemptId || ""),
6369
+ confirmWrite: req.body.confirmWrite === true
6370
+ }));
6371
+ }));
5934
6372
  app.get("/api/lineage/:assetId", asyncRoute((req, res) => {
5935
6373
  res.json(getLineageSnapshot(projectFrom(req), req.params.assetId));
5936
6374
  }));