@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/CHANGELOG.md +7 -0
- package/README.md +15 -2
- package/dist/cli/lineage-dev.js +878 -16
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +878 -16
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +446 -8
- package/dist/server.js.map +3 -3
- package/dist/web/assets/{index-DkLQJqxa.css → index-EfT3Ues-.css} +1 -1
- package/dist/web/assets/index-NDba9I2v.js +23 -0
- package/dist/web/index.html +2 -2
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-drill-mint-diagonal-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v3.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +38 -1
- package/package.json +1 -1
- package/dist/web/assets/index-C54JeMzP.js +0 -22
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,
|
|
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:
|
|
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
|
-
|
|
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
|
}));
|