@mean-weasel/lineage 0.1.1 → 0.1.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.
@@ -1,21 +1,1199 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/lineageCli.ts
4
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
4
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "node:fs";
5
5
  import { homedir, platform } from "node:os";
6
- import { dirname, join, resolve } from "node:path";
6
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
9
+
10
+ // src/server/assetCore.ts
11
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
12
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
13
+ import { spawnSync } from "node:child_process";
8
14
  import { fileURLToPath } from "node:url";
15
+
16
+ // src/server/adapters/storage/s3StorageAdapter.ts
17
+ import { copyFileSync, existsSync as existsSync2, mkdirSync, statSync as statSync2 } from "node:fs";
18
+ import { basename as basename2, dirname, join as join2 } from "node:path";
19
+
20
+ // src/server/localReview.ts
21
+ import { createHash } from "node:crypto";
22
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
23
+ import { basename, extname, join, relative, resolve } from "node:path";
24
+ var mimeByExt = {
25
+ ".gif": "image/gif",
26
+ ".jpeg": "image/jpeg",
27
+ ".jpg": "image/jpeg",
28
+ ".mov": "video/quicktime",
29
+ ".mp4": "video/mp4",
30
+ ".png": "image/png",
31
+ ".svg": "image/svg+xml",
32
+ ".webp": "image/webp"
33
+ };
34
+ var localReviewExts = new Set(Object.keys(mimeByExt));
35
+ function contentTypeFor(file) {
36
+ return mimeByExt[extname(file).toLowerCase()] || "application/octet-stream";
37
+ }
38
+ function fileSha256(file) {
39
+ return createHash("sha256").update(readFileSync(file)).digest("hex");
40
+ }
41
+
42
+ // src/server/adapters/storage/s3StorageAdapter.ts
43
+ function parseAssetIdFromS3Key(key) {
44
+ const marker = "/assets/";
45
+ const index = key.indexOf(marker);
46
+ if (index === -1) return void 0;
47
+ return key.slice(index + marker.length).split("/")[0];
48
+ }
49
+ function previewDataUrl(asset) {
50
+ const title = asset.title || asset.asset_id;
51
+ const label = `${asset.channel || "catalog"} / ${asset.status || "working"}`;
52
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 1200 675"><rect width="1200" height="675" fill="#f7f5ef"/><rect x="56" y="56" width="1088" height="563" rx="18" fill="#10201c"/><text x="96" y="145" fill="#9fe6c8" font-family="Arial, sans-serif" font-size="34" font-weight="700">Lineage catalog preview</text><text x="96" y="230" fill="#fff8e6" font-family="Arial, sans-serif" font-size="56" font-weight="700">${escapeSvgText(asset.asset_id)}</text><text x="96" y="330" fill="#d9e8df" font-family="Arial, sans-serif" font-size="34">${escapeSvgText(title)}</text><text x="96" y="500" fill="#9fb7ae" font-family="Arial, sans-serif" font-size="26">${escapeSvgText(label)}. No external storage requested.</text></svg>`;
53
+ return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
54
+ }
55
+ function escapeSvgText(value) {
56
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
57
+ }
58
+ function createS3StorageAdapter(deps) {
59
+ return {
60
+ deleteObjectGuarded(project, assetId, confirmation) {
61
+ const enabled = process.env.LINEAGE_ENABLE_CLOUD_DELETE === "true";
62
+ if (!enabled) {
63
+ throw deps.createError("S3 delete is disabled. Use archive unless a human enables LINEAGE_ENABLE_CLOUD_DELETE.", 403);
64
+ }
65
+ if (confirmation !== `delete ${assetId}`) {
66
+ throw deps.createError(`Delete confirmation must exactly equal: delete ${assetId}`);
67
+ }
68
+ const catalog = deps.loadCatalog(project);
69
+ const asset = deps.assetById(catalog, assetId);
70
+ if (!asset.s3) throw deps.createError(`Asset has no S3 object: ${assetId}`);
71
+ deps.runAws(["s3api", "delete-object", "--bucket", asset.s3.bucket, "--key", asset.s3.key, "--region", asset.s3.region]);
72
+ asset.status = "archived";
73
+ deps.saveCatalog(project, catalog);
74
+ return { ok: true, message: `Deleted current S3 object and archived ${assetId}`, catalog };
75
+ },
76
+ getIdentity() {
77
+ try {
78
+ const output = deps.runAws(["sts", "get-caller-identity", "--query", "{Account:Account,Arn:Arn}", "--output", "json"]);
79
+ const parsed = JSON.parse(output.stdout);
80
+ return { account: parsed.Account, arn: parsed.Arn };
81
+ } catch {
82
+ return void 0;
83
+ }
84
+ },
85
+ listLiveObjects(catalog) {
86
+ const bucket = catalog.default_bucket;
87
+ const region = catalog.default_region;
88
+ const prefix = `products/${catalog.product}/`;
89
+ const output = deps.runAws(["s3api", "list-objects-v2", "--bucket", bucket, "--prefix", prefix, "--region", region, "--output", "json"]);
90
+ const parsed = JSON.parse(output.stdout);
91
+ const catalogKeys = new Set(catalog.assets.map((asset) => asset.s3?.key).filter(Boolean));
92
+ return (parsed.Contents || []).map((item) => ({
93
+ key: item.Key,
94
+ size: item.Size,
95
+ lastModified: item.LastModified,
96
+ storageClass: item.StorageClass,
97
+ cataloged: catalogKeys.has(item.Key),
98
+ assetId: parseAssetIdFromS3Key(item.Key)
99
+ }));
100
+ },
101
+ presignAsset(project, assetId, expiresIn = 900) {
102
+ const catalog = deps.loadCatalog(project);
103
+ const asset = deps.assetById(catalog, assetId);
104
+ return { assetId, expiresIn, url: previewDataUrl(asset) };
105
+ },
106
+ promoteAsset(project, assetId, confirmWrite) {
107
+ if (!confirmWrite) throw deps.createError("Promote requires confirmWrite=true");
108
+ const catalog = deps.loadCatalog(project);
109
+ const asset = deps.assetById(catalog, assetId);
110
+ asset.status = "published";
111
+ deps.saveCatalog(project, catalog);
112
+ return {
113
+ ok: true,
114
+ message: `Promoted ${assetId}`,
115
+ catalog
116
+ };
117
+ },
118
+ pullAsset(project, assetId, out = ".asset-scratch") {
119
+ const catalog = deps.loadCatalog(project);
120
+ const asset = deps.assetById(catalog, assetId);
121
+ return {
122
+ ok: true,
123
+ message: `Prepared ${assetId} for local review`,
124
+ output: {
125
+ assetId: asset.asset_id,
126
+ out,
127
+ storage: asset.local ? "local" : asset.s3 ? "catalog-s3-metadata" : "catalog",
128
+ note: "Lineage public package does not pull cloud objects automatically."
129
+ }
130
+ };
131
+ },
132
+ uploadAsset(file, fields) {
133
+ if (!fields.confirmWrite) throw deps.createError("Upload requires confirmWrite=true");
134
+ if (!existsSync2(file)) throw deps.createError(`Upload file missing: ${file}`, 404);
135
+ if (!["working", "published"].includes(fields.status)) throw deps.createError("Upload status must be working or published");
136
+ if (!deps.supportedContentTypes.has(fields.type)) throw deps.createError(`Unsupported asset type: ${fields.type}`);
137
+ const project = deps.cleanProject(fields.project || fields.product || deps.defaultProject);
138
+ const catalog = deps.loadCatalog(project);
139
+ const relativePath = join2("uploads", project, fields.assetId, basename2(file));
140
+ const absolutePath = join2(deps.repoRoot, ".asset-scratch", relativePath);
141
+ mkdirSync(dirname(absolutePath), { recursive: true });
142
+ copyFileSync(file, absolutePath);
143
+ const stats = statSync2(absolutePath);
144
+ const contentType = contentTypeFor(absolutePath);
145
+ const checksumSha256 = fileSha256(absolutePath);
146
+ const now = (/* @__PURE__ */ new Date()).toISOString();
147
+ const nextAsset = {
148
+ asset_id: fields.assetId,
149
+ audience: fields.audience,
150
+ campaign: fields.campaign,
151
+ channel: fields.channel,
152
+ content_type: fields.type,
153
+ cta: fields.cta,
154
+ hook: fields.hook,
155
+ ...fields.format ? { format: fields.format } : {},
156
+ local: {
157
+ absolute_path: absolutePath,
158
+ checksum_sha256: checksumSha256,
159
+ content_type: contentType,
160
+ relative_path: relativePath,
161
+ size_bytes: stats.size,
162
+ updated_at: now
163
+ },
164
+ ...fields.messageFamily ? { message_family: fields.messageFamily } : {},
165
+ ...fields.notes ? { notes: fields.notes } : {},
166
+ product: project,
167
+ project,
168
+ source: "catalog",
169
+ status: fields.status,
170
+ title: fields.title,
171
+ utm_content: fields.utmContent
172
+ };
173
+ const existing = catalog.assets.findIndex((asset) => asset.asset_id === fields.assetId);
174
+ if (existing >= 0) catalog.assets[existing] = { ...catalog.assets[existing], ...nextAsset };
175
+ else catalog.assets.push(nextAsset);
176
+ deps.saveCatalog(project, catalog);
177
+ return {
178
+ ok: true,
179
+ message: `Recorded ${fields.assetId} locally`,
180
+ output: {
181
+ file: basename2(absolutePath),
182
+ relativePath,
183
+ sizeBytes: stats.size,
184
+ contentType,
185
+ checksumSha256
186
+ },
187
+ catalog
188
+ };
189
+ }
190
+ };
191
+ }
192
+
193
+ // src/server/assetLineageDb.ts
194
+ import { createRequire } from "node:module";
195
+ import { mkdirSync as mkdirSync2 } from "node:fs";
196
+ import { join as join3 } from "node:path";
197
+ var require2 = createRequire(import.meta.url);
198
+ function nowIso() {
199
+ return (/* @__PURE__ */ new Date()).toISOString();
200
+ }
201
+ function lineageDbPath() {
202
+ return process.env.LINEAGE_DB || join3(repoRoot, ".lineage", "asset-lineage.sqlite");
203
+ }
204
+ function lineageDb() {
205
+ mkdirSync2(join3(lineageDbPath(), ".."), { recursive: true });
206
+ const { DatabaseSync } = require2("node:sqlite");
207
+ const database = new DatabaseSync(lineageDbPath());
208
+ database.exec("PRAGMA foreign_keys = ON");
209
+ database.exec("PRAGMA busy_timeout = 5000");
210
+ database.exec(`
211
+ create table if not exists projects (
212
+ id text primary key,
213
+ product text not null,
214
+ catalog_path text,
215
+ created_at text not null,
216
+ updated_at text not null
217
+ );
218
+ create table if not exists assets (
219
+ id text primary key,
220
+ project_id text not null references projects(id),
221
+ source text not null check (source in ('local', 'catalog')),
222
+ local_path text,
223
+ s3_key text,
224
+ checksum_sha256 text,
225
+ media_type text not null,
226
+ title text not null,
227
+ status text not null,
228
+ channel text,
229
+ campaign text,
230
+ audience text,
231
+ size_bytes integer,
232
+ content_type text,
233
+ created_at text not null,
234
+ updated_at text not null,
235
+ last_seen_at text not null
236
+ );
237
+ create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
238
+ create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
239
+ create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
240
+ create table if not exists asset_edges (
241
+ id text primary key,
242
+ project_id text not null references projects(id),
243
+ parent_asset_id text not null references assets(id),
244
+ child_asset_id text not null references assets(id),
245
+ relation_type text not null check (relation_type in ('derived_from')),
246
+ created_at text not null,
247
+ unique (project_id, parent_asset_id, child_asset_id, relation_type)
248
+ );
249
+ create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
250
+ create index if not exists edges_child on asset_edges(project_id, child_asset_id);
251
+ create table if not exists asset_reviews (
252
+ asset_id text primary key references assets(id),
253
+ review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
254
+ reviewed_at text,
255
+ ignored_at text,
256
+ notes text,
257
+ updated_at text not null
258
+ );
259
+ create table if not exists asset_selections (
260
+ id text primary key,
261
+ project_id text not null references projects(id),
262
+ root_asset_id text not null references assets(id),
263
+ asset_id text not null references assets(id),
264
+ notes text,
265
+ selected_at text not null,
266
+ unique(project_id, root_asset_id)
267
+ );
268
+ create table if not exists asset_layouts (
269
+ id text primary key,
270
+ project_id text not null references projects(id),
271
+ root_asset_id text not null references assets(id),
272
+ asset_id text not null references assets(id),
273
+ x real not null,
274
+ y real not null,
275
+ updated_at text not null,
276
+ unique(project_id, root_asset_id, asset_id)
277
+ );
278
+ create table if not exists lineage_workspaces (
279
+ id text primary key,
280
+ project_id text not null references projects(id),
281
+ root_asset_id text not null references assets(id),
282
+ title text not null,
283
+ status text not null check (status in ('active', 'paused', 'archived')),
284
+ notes text,
285
+ created_by text not null check (created_by in ('human', 'agent', 'system')),
286
+ active_at text,
287
+ created_at text not null,
288
+ updated_at text not null,
289
+ unique(project_id, root_asset_id)
290
+ );
291
+ create index if not exists lineage_workspaces_project_status on lineage_workspaces(project_id, status, updated_at);
292
+ create index if not exists lineage_workspaces_project_active on lineage_workspaces(project_id, active_at);
293
+ create table if not exists asset_ledger_records (
294
+ id text primary key,
295
+ project_id text not null references projects(id),
296
+ canonical_asset_id text not null,
297
+ checksum_sha256 text,
298
+ media_type text not null,
299
+ title text not null,
300
+ status text not null,
301
+ channel text,
302
+ campaign text,
303
+ audience text,
304
+ created_at text not null,
305
+ updated_at text not null,
306
+ first_seen_at text not null default (datetime('now')),
307
+ last_seen_at text not null
308
+ );
309
+ create index if not exists asset_ledger_records_project_seen on asset_ledger_records(project_id, last_seen_at);
310
+ create index if not exists asset_ledger_records_project_checksum on asset_ledger_records(project_id, checksum_sha256);
311
+ create table if not exists asset_ledger_sources (
312
+ id text primary key,
313
+ project_id text not null references projects(id),
314
+ record_id text not null references asset_ledger_records(id) on delete cascade,
315
+ source_type text not null check (source_type in ('local', 'catalog', 's3')),
316
+ asset_id text,
317
+ local_path text,
318
+ s3_bucket text,
319
+ s3_region text,
320
+ s3_key text,
321
+ s3_version_id text,
322
+ etag text,
323
+ size_bytes integer,
324
+ content_type text,
325
+ updated_at text,
326
+ first_seen_at text not null default (datetime('now')),
327
+ last_seen_at text not null
328
+ );
329
+ create index if not exists asset_ledger_sources_project_type on asset_ledger_sources(project_id, source_type);
330
+ create index if not exists asset_ledger_sources_record on asset_ledger_sources(project_id, record_id);
331
+ create index if not exists asset_ledger_sources_s3_key on asset_ledger_sources(project_id, s3_key);
332
+ create table if not exists asset_ledger_placements (
333
+ id text primary key,
334
+ project_id text not null references projects(id),
335
+ asset_id text not null,
336
+ channel text not null,
337
+ status text not null,
338
+ scheduled_at text,
339
+ posted_at text,
340
+ url text,
341
+ notes text,
342
+ updated_at text not null,
343
+ synced_at text not null,
344
+ unique(project_id, asset_id, channel)
345
+ );
346
+ create index if not exists asset_ledger_placements_project_status on asset_ledger_placements(project_id, status);
347
+ create index if not exists asset_ledger_placements_asset on asset_ledger_placements(project_id, asset_id);
348
+ create table if not exists asset_ledger_index_runs (
349
+ id text primary key,
350
+ project_id text not null references projects(id),
351
+ source_mode text not null check (source_mode in ('all', 'catalog', 'local')),
352
+ include_live_s3 integer not null default 0,
353
+ status text not null check (status in ('running', 'complete', 'failed')),
354
+ started_at text not null,
355
+ completed_at text,
356
+ assets_indexed integer not null default 0,
357
+ records_after integer not null default 0,
358
+ catalog_sources_after integer not null default 0,
359
+ local_sources_after integer not null default 0,
360
+ s3_sources_after integer not null default 0,
361
+ error text
362
+ );
363
+ create index if not exists asset_ledger_index_runs_project_started on asset_ledger_index_runs(project_id, started_at);
364
+ create table if not exists content_batches (
365
+ id text not null,
366
+ project_id text not null references projects(id),
367
+ title text not null,
368
+ campaign text,
369
+ channel text,
370
+ status text not null check (status in ('active', 'archived')),
371
+ notes text,
372
+ created_at text not null,
373
+ updated_at text not null,
374
+ primary key(project_id, id)
375
+ );
376
+ create index if not exists content_batches_project_updated on content_batches(project_id, updated_at);
377
+ create table if not exists content_posts (
378
+ id text not null,
379
+ project_id text not null references projects(id),
380
+ batch_id text not null,
381
+ channel text not null,
382
+ title text not null,
383
+ phase text not null check (phase in ('draft', 'review', 'scheduled', 'posted', 'skipped', 'archived')),
384
+ campaign text,
385
+ body text,
386
+ cta text,
387
+ scheduled_at text,
388
+ posted_at text,
389
+ url text,
390
+ notes text,
391
+ source_path text,
392
+ created_at text not null,
393
+ updated_at text not null,
394
+ primary key(project_id, id),
395
+ foreign key(project_id, batch_id) references content_batches(project_id, id) on delete cascade
396
+ );
397
+ create index if not exists content_posts_project_phase on content_posts(project_id, phase);
398
+ create index if not exists content_posts_batch on content_posts(project_id, batch_id);
399
+ create table if not exists content_post_assets (
400
+ id text primary key,
401
+ project_id text not null references projects(id),
402
+ post_id text not null,
403
+ asset_id text not null,
404
+ role text not null,
405
+ notes text,
406
+ attached_at text not null,
407
+ unique(project_id, post_id, asset_id, role),
408
+ foreign key(project_id, post_id) references content_posts(project_id, id) on delete cascade
409
+ );
410
+ create index if not exists content_post_assets_post on content_post_assets(project_id, post_id);
411
+ create index if not exists content_post_assets_asset on content_post_assets(project_id, asset_id);
412
+ create table if not exists content_targets (
413
+ project_id text primary key references projects(id),
414
+ post_id text not null,
415
+ notes text,
416
+ selected_at text not null,
417
+ updated_at text not null,
418
+ foreign key(project_id, post_id) references content_posts(project_id, id) on delete cascade
419
+ );
420
+ create table if not exists selection_sets (
421
+ id text primary key,
422
+ project_id text not null references projects(id),
423
+ kind text not null check (kind in ('current', 'review')),
424
+ key text not null,
425
+ label text not null,
426
+ status text not null check (status in ('active', 'archived')),
427
+ created_by text not null check (created_by in ('human', 'agent', 'system')),
428
+ created_at text not null,
429
+ updated_at text not null,
430
+ unique(project_id, kind, key)
431
+ );
432
+ create index if not exists selection_sets_project_kind on selection_sets(project_id, kind, updated_at);
433
+ create table if not exists selection_items (
434
+ id text primary key,
435
+ set_id text not null references selection_sets(id) on delete cascade,
436
+ asset_id text not null,
437
+ role text not null check (role in ('primary', 'candidate', 'next_base')),
438
+ variation_label text,
439
+ position integer not null default 0,
440
+ selected_by text check (selected_by in ('human', 'agent', 'system')),
441
+ selected_at text,
442
+ deselected_at text,
443
+ notes text,
444
+ created_at text not null,
445
+ updated_at text not null,
446
+ unique(set_id, asset_id)
447
+ );
448
+ create index if not exists selection_items_set_position on selection_items(set_id, position);
449
+ create unique index if not exists selection_items_set_label on selection_items(set_id, variation_label) where variation_label is not null;
450
+ create table if not exists generation_jobs (
451
+ id text primary key,
452
+ project_id text not null references projects(id),
453
+ provider text not null default 'codex-handoff',
454
+ adapter_version text not null,
455
+ source_mode text not null check (source_mode in ('lineage_selection')),
456
+ root_asset_id text not null references assets(id),
457
+ prompt text not null,
458
+ expected_output_count integer not null check (expected_output_count > 0),
459
+ status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
460
+ output_dir text,
461
+ handoff_json text,
462
+ created_at text not null,
463
+ updated_at text not null,
464
+ imported_at text
465
+ );
466
+ create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
467
+ create table if not exists generation_job_inputs (
468
+ id text primary key,
469
+ job_id text not null references generation_jobs(id) on delete cascade,
470
+ project_id text not null references projects(id),
471
+ asset_id text not null references assets(id),
472
+ root_asset_id text not null references assets(id),
473
+ role text not null check (role in ('lineage_next_base', 'reference')),
474
+ position integer not null,
475
+ selection_strategy text not null,
476
+ selection_snapshot_json text not null,
477
+ unique(job_id, asset_id, role)
478
+ );
479
+ create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
480
+ create table if not exists generation_job_outputs (
481
+ id text primary key,
482
+ job_id text not null references generation_jobs(id) on delete cascade,
483
+ project_id text not null references projects(id),
484
+ output_index integer not null,
485
+ file_path text not null,
486
+ checksum_sha256 text not null,
487
+ size_bytes integer not null,
488
+ content_type text not null,
489
+ imported_asset_id text not null references assets(id),
490
+ parent_asset_id text not null references assets(id),
491
+ imported_at text not null,
492
+ unique(job_id, output_index),
493
+ unique(job_id, file_path)
494
+ );
495
+ create index if not exists generation_job_outputs_job on generation_job_outputs(job_id, output_index);
496
+ create table if not exists generation_job_receipts (
497
+ id text primary key,
498
+ job_id text not null references generation_jobs(id) on delete cascade,
499
+ receipt_type text not null check (receipt_type in ('plan', 'import', 'error')),
500
+ status text not null check (status in ('ok', 'error')),
501
+ command text not null,
502
+ payload_json text not null,
503
+ created_at text not null
504
+ );
505
+ create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
506
+ create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
507
+ `);
508
+ migrateAssetSelections(database);
509
+ dropLegacyAssetSelectionRootUnique(database);
510
+ ensureColumn(database, "asset_selections", "notes", "text");
511
+ ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
512
+ ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
513
+ ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
514
+ ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
515
+ ensureReviewStateValues(database);
516
+ return database;
517
+ }
518
+ function ensureColumn(database, table, column, definition) {
519
+ const rows = database.prepare(`pragma table_info(${table})`).all();
520
+ if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
521
+ }
522
+ function migrateAssetSelections(database) {
523
+ const rows = database.prepare("pragma table_info(asset_selections)").all();
524
+ if (rows.some((row) => row.name === "position")) return;
525
+ const notesSelect = rows.some((row) => row.name === "notes") ? "notes" : "null";
526
+ database.exec(`
527
+ create table if not exists asset_selections_v2 (
528
+ id text primary key,
529
+ project_id text not null references projects(id),
530
+ root_asset_id text not null references assets(id),
531
+ asset_id text not null references assets(id),
532
+ position integer not null default 0,
533
+ notes text,
534
+ selected_at text not null,
535
+ unique(project_id, root_asset_id, asset_id)
536
+ );
537
+ insert or ignore into asset_selections_v2 (id, project_id, root_asset_id, asset_id, position, notes, selected_at)
538
+ select
539
+ project_id || ':' || root_asset_id || ':selected:' || asset_id,
540
+ project_id,
541
+ root_asset_id,
542
+ asset_id,
543
+ 0,
544
+ ${notesSelect},
545
+ selected_at
546
+ from asset_selections;
547
+ drop table asset_selections;
548
+ alter table asset_selections_v2 rename to asset_selections;
549
+ create index if not exists asset_selections_project_root_position
550
+ on asset_selections(project_id, root_asset_id, position, selected_at);
551
+ `);
552
+ }
553
+ function dropLegacyAssetSelectionRootUnique(database) {
554
+ const indexes = database.prepare("pragma index_list(asset_selections)").all();
555
+ for (const index of indexes) {
556
+ if (!index.unique) continue;
557
+ const columns = database.prepare(`pragma index_info(${index.name})`).all();
558
+ const columnNames = columns.map((column) => column.name).join(",");
559
+ if (columnNames === "project_id,root_asset_id") database.exec(`drop index if exists ${index.name}`);
560
+ }
561
+ }
562
+ function ensureReviewStateValues(database) {
563
+ const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'asset_reviews'").get();
564
+ if (createSql?.sql?.includes("needs_revision")) return;
565
+ database.exec(`
566
+ alter table asset_reviews rename to asset_reviews_old;
567
+ create table asset_reviews (
568
+ asset_id text primary key references assets(id),
569
+ review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
570
+ reviewed_at text,
571
+ ignored_at text,
572
+ notes text,
573
+ updated_at text not null
574
+ );
575
+ insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
576
+ select asset_id, review_state, reviewed_at, ignored_at, notes, updated_at
577
+ from asset_reviews_old;
578
+ drop table asset_reviews_old;
579
+ `);
580
+ }
581
+
582
+ // src/server/assetCore.ts
583
+ function isPackageRoot(path) {
584
+ const packageJson = join4(path, "package.json");
585
+ if (!existsSync3(packageJson)) return false;
586
+ try {
587
+ const packageInfo = JSON.parse(readFileSync2(packageJson, "utf8"));
588
+ return packageInfo.name === "@mean-weasel/lineage";
589
+ } catch {
590
+ return false;
591
+ }
592
+ }
593
+ function resolveRepoRoot() {
594
+ const moduleDir = dirname2(fileURLToPath(import.meta.url));
595
+ const candidates = [
596
+ process.env.LINEAGE_REPO_ROOT,
597
+ resolve2(moduleDir, ".."),
598
+ resolve2(moduleDir, "../.."),
599
+ process.cwd()
600
+ ].filter((candidate) => Boolean(candidate));
601
+ const root = candidates.find(isPackageRoot);
602
+ if (!root) throw new Error("Unable to locate Lineage package root");
603
+ return root;
604
+ }
605
+ var repoRoot = resolveRepoRoot();
606
+ var defaultProject = "demo-project";
607
+ var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
608
+ var publicFallbackBucket = "lineage-demo-assets";
609
+ var publicFallbackRegion = "us-east-1";
610
+ var contentTypes = /* @__PURE__ */ new Set(["image", "video", "gif", "audio", "doc", "other"]);
611
+ var projectNamePattern = /^[a-z0-9][a-z0-9-]*$/;
612
+ var LineageAssetError = class extends Error {
613
+ constructor(message, status = 400) {
614
+ super(message);
615
+ this.status = status;
616
+ }
617
+ status;
618
+ };
619
+ function cleanProject(project = defaultProject) {
620
+ if (!projectNamePattern.test(project)) {
621
+ throw new LineageAssetError("Project must be lowercase kebab-case");
622
+ }
623
+ return project;
624
+ }
625
+ function catalogPath(project = defaultProject) {
626
+ return join4(repoRoot, cleanProject(project), "assets", "catalog.json");
627
+ }
628
+ function fixtureCatalogPath(project = defaultProject) {
629
+ return join4(repoRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
630
+ }
631
+ function normalizeCatalog(catalog, fallbackProject = defaultProject) {
632
+ const project = cleanProject(catalog.project || catalog.product || fallbackProject);
633
+ const product = catalog.product || project;
634
+ return {
635
+ ...catalog,
636
+ project,
637
+ product,
638
+ default_bucket: catalog.default_bucket || "",
639
+ default_region: catalog.default_region || "",
640
+ assets: (catalog.assets || []).map((asset) => ({
641
+ ...asset,
642
+ source: asset.source || "catalog",
643
+ project: asset.project || asset.product || project,
644
+ product: asset.product || asset.project || project
645
+ }))
646
+ };
647
+ }
648
+ function fallbackS3(assetId, channel, status = "working") {
649
+ return {
650
+ bucket: publicFallbackBucket,
651
+ checksum_sha256: void 0,
652
+ content_type: "image/png",
653
+ key: `products/${defaultProject}/campaigns/2026-06-public-demo/channels/${channel}/audiences/creators/statuses/${status}/types/image/assets/${assetId}/${assetId}.png`,
654
+ region: publicFallbackRegion,
655
+ size_bytes: 2048,
656
+ updated_at: "2026-06-24T12:00:00.000Z",
657
+ version_id: "public-demo-version"
658
+ };
659
+ }
660
+ function fallbackAsset(fields) {
661
+ return {
662
+ audience: fields.audience || "creators",
663
+ campaign: fields.campaign || "2026-06-public-demo",
664
+ content_type: "image",
665
+ cta: fields.cta || "Save the idea",
666
+ hook: fields.hook || "Public demo creative for local review.",
667
+ product: defaultProject,
668
+ project: defaultProject,
669
+ source: "catalog",
670
+ utm_content: fields.utm_content || fields.asset_id.replace(/-/g, "_"),
671
+ ...fields
672
+ };
673
+ }
674
+ function defaultFallbackCatalog() {
675
+ return normalizeCatalog({
676
+ assets: [
677
+ fallbackAsset({
678
+ asset_id: "demo-meta-short-form-upload-demo-post-static",
679
+ channel: "meta",
680
+ s3: fallbackS3("demo-meta-short-form-upload-demo-post-static", "meta"),
681
+ status: "working",
682
+ title: "Meta short-form demo post static"
683
+ }),
684
+ fallbackAsset({
685
+ asset_id: "demo-linkedin-ledger-catalog-shared",
686
+ channel: "linkedin",
687
+ hook: "Shared ledger creative with catalog metadata.",
688
+ s3: fallbackS3("demo-linkedin-ledger-catalog-shared", "linkedin"),
689
+ status: "working",
690
+ title: "LinkedIn ledger catalog shared"
691
+ }),
692
+ fallbackAsset({
693
+ asset_id: "demo-linkedin-upload-demo-done-static-grounded-v2",
694
+ channel: "linkedin",
695
+ placements: [{
696
+ channel: "linkedin",
697
+ notes: "Synthetic public scheduled placement.",
698
+ scheduled_at: "2026-06-24T16:00:00-07:00",
699
+ status: "scheduled",
700
+ updated_at: "2026-06-24T12:30:00.000Z"
701
+ }],
702
+ s3: fallbackS3("demo-linkedin-upload-demo-done-static-grounded-v2", "linkedin", "approved"),
703
+ status: "approved",
704
+ title: "LinkedIn upload demo scheduled static"
705
+ }),
706
+ fallbackAsset({
707
+ asset_id: "demo-tiktok-upload-demo-export-vertical",
708
+ channel: "tiktok",
709
+ format: "vertical",
710
+ hook: "Fast vertical demo export for content queue tests.",
711
+ s3: fallbackS3("demo-tiktok-upload-demo-export-vertical", "tiktok"),
712
+ status: "working",
713
+ title: "TikTok upload demo export vertical"
714
+ }),
715
+ fallbackAsset({
716
+ asset_id: "demo-youtube-short-demo-posted-cut",
717
+ channel: "youtube",
718
+ placements: [{
719
+ channel: "youtube",
720
+ notes: "Synthetic public posted placement.",
721
+ posted_at: "2026-06-25T16:00:00-07:00",
722
+ status: "posted",
723
+ updated_at: "2026-06-25T17:00:00.000Z"
724
+ }],
725
+ s3: fallbackS3("demo-youtube-short-demo-posted-cut", "youtube", "published"),
726
+ status: "published",
727
+ title: "YouTube short demo posted cut"
728
+ }),
729
+ fallbackAsset({
730
+ asset_id: "demo-x-twitter-carousel-demo-working-static",
731
+ channel: "x-twitter",
732
+ format: "static",
733
+ s3: fallbackS3("demo-x-twitter-carousel-demo-working-static", "x-twitter"),
734
+ status: "working",
735
+ title: "X Twitter carousel demo working static"
736
+ })
737
+ ],
738
+ default_bucket: "",
739
+ default_region: "",
740
+ product: defaultProject,
741
+ project: defaultProject
742
+ }, defaultProject);
743
+ }
744
+ function loadCatalog(project = defaultProject) {
745
+ const path = catalogPath(project);
746
+ if (existsSync3(path)) {
747
+ try {
748
+ return normalizeCatalog(JSON.parse(readFileSync2(path, "utf8")), project);
749
+ } catch (error) {
750
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
751
+ throw new LineageAssetError(`Missing catalog: ${path}`, 404);
752
+ }
753
+ throw error;
754
+ }
755
+ }
756
+ const clean = cleanProject(project);
757
+ if (clean === defaultProject) {
758
+ const fixturePath = fixtureCatalogPath(clean);
759
+ if (existsSync3(fixturePath)) {
760
+ return normalizeCatalog(JSON.parse(readFileSync2(fixturePath, "utf8")), project);
761
+ }
762
+ return defaultFallbackCatalog();
763
+ }
764
+ throw new LineageAssetError(`Missing catalog: ${path}`, 404);
765
+ }
766
+ function saveCatalog(project, catalog) {
767
+ const normalized = normalizeCatalog(catalog, project);
768
+ mkdirSync3(dirname2(catalogPath(project)), { recursive: true });
769
+ writeFileSync(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
770
+ `);
771
+ return normalized;
772
+ }
773
+ function run(command, args) {
774
+ const result = spawnSync(command, args, {
775
+ cwd: repoRoot,
776
+ encoding: "utf8",
777
+ stdio: ["ignore", "pipe", "pipe"],
778
+ env: {
779
+ ...process.env,
780
+ AWS_REGION: process.env.AWS_REGION || "us-east-1",
781
+ AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION || "us-east-1"
782
+ }
783
+ });
784
+ if (result.status !== 0) {
785
+ const detail = result.stderr.trim() || result.stdout.trim();
786
+ throw new LineageAssetError(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`, 502);
787
+ }
788
+ return { stdout: result.stdout, stderr: result.stderr };
789
+ }
790
+ function runAws(args) {
791
+ return run("aws", args);
792
+ }
793
+ function assetById(catalog, assetId) {
794
+ const asset = catalog.assets.find((item) => item.asset_id === assetId);
795
+ if (!asset) throw new LineageAssetError(`Unknown asset: ${assetId}`, 404);
796
+ return asset;
797
+ }
798
+ var storageAdapter = createS3StorageAdapter({
799
+ assetById,
800
+ cleanProject,
801
+ createError: (message, status) => new LineageAssetError(message, status),
802
+ defaultProject,
803
+ loadCatalog,
804
+ runAws,
805
+ repoRoot,
806
+ saveCatalog,
807
+ supportedContentTypes: contentTypes
808
+ });
809
+
810
+ // src/server/assetLineageSelection.ts
811
+ function selectedRows(database, project, root) {
812
+ return database.prepare(`
813
+ select asset_id, notes, position, selected_at
814
+ from asset_selections
815
+ where project_id = ? and root_asset_id = ?
816
+ order by position, selected_at, asset_id
817
+ `).all(project, root);
818
+ }
819
+
820
+ // src/server/assetLineageWorkspaces.ts
821
+ function lineageWorkspaceId(project, rootAssetId) {
822
+ return `${project}:lineage-workspace:${rootAssetId}`;
823
+ }
824
+ function ensureProject(database, project) {
825
+ const timestamp = nowIso();
826
+ database.prepare(`
827
+ insert into projects (id, product, created_at, updated_at)
828
+ values (?, ?, ?, ?)
829
+ on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
830
+ `).run(project, project, timestamp, timestamp);
831
+ }
832
+ function rowToWorkspace(row) {
833
+ return {
834
+ id: String(row.id),
835
+ project: String(row.project_id),
836
+ root_asset_id: String(row.root_asset_id),
837
+ title: String(row.title),
838
+ status: String(row.status),
839
+ notes: typeof row.notes === "string" ? row.notes : void 0,
840
+ created_by: String(row.created_by),
841
+ active_at: typeof row.active_at === "string" ? row.active_at : void 0,
842
+ created_at: String(row.created_at),
843
+ updated_at: String(row.updated_at)
844
+ };
845
+ }
846
+ function knownRoots(database, project) {
847
+ const rows = database.prepare(`
848
+ select root_asset_id, max(selected_at) selected_at from asset_selections where project_id = ? group by root_asset_id
849
+ union
850
+ select root_asset_id, null selected_at from asset_layouts where project_id = ? group by root_asset_id
851
+ union
852
+ select parent_asset_id root_asset_id, null selected_at
853
+ from asset_edges
854
+ where project_id = ?
855
+ and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
856
+ group by parent_asset_id
857
+ `).all(project, project, project, project);
858
+ return rows.map((row) => ({ root_asset_id: row.root_asset_id, selected_at: row.selected_at || void 0 }));
859
+ }
860
+ function seedLegacyWorkspaces(database, project) {
861
+ ensureProject(database, project);
862
+ const timestamp = nowIso();
863
+ const statement = database.prepare(`
864
+ insert into lineage_workspaces (
865
+ id, project_id, root_asset_id, title, status, notes, created_by, active_at, created_at, updated_at
866
+ )
867
+ select ?, ?, a.id, a.title || ' lineage', 'active', null, 'system', ?, ?, ?
868
+ from assets a
869
+ where a.project_id = ? and a.id = ?
870
+ on conflict(project_id, root_asset_id) do nothing
871
+ `);
872
+ for (const root of knownRoots(database, project)) {
873
+ statement.run(
874
+ lineageWorkspaceId(project, root.root_asset_id),
875
+ project,
876
+ root.selected_at || null,
877
+ timestamp,
878
+ timestamp,
879
+ project,
880
+ root.root_asset_id
881
+ );
882
+ }
883
+ }
884
+ function activeWorkspace(database, project) {
885
+ const row = database.prepare(`
886
+ select * from lineage_workspaces
887
+ where project_id = ? and status != 'archived'
888
+ order by active_at desc nulls last, updated_at desc
889
+ limit 1
890
+ `).get(project);
891
+ return row ? rowToWorkspace(row) : null;
892
+ }
893
+ function activeLineageWorkspaceRoot(project) {
894
+ const database = lineageDb();
895
+ try {
896
+ seedLegacyWorkspaces(database, project);
897
+ return activeWorkspace(database, project)?.root_asset_id;
898
+ } finally {
899
+ database.close();
900
+ }
901
+ }
902
+
903
+ // src/server/assetLineage.ts
904
+ var LineageError = class extends Error {
905
+ constructor(message, status = 400) {
906
+ super(message);
907
+ this.status = status;
908
+ }
909
+ status;
910
+ };
911
+ function requireAsset(database, project, assetId) {
912
+ const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
913
+ if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
914
+ }
915
+ function parentOf(database, project, assetId) {
916
+ const row = database.prepare("select parent_asset_id from asset_edges where project_id = ? and child_asset_id = ? order by created_at limit 1").get(project, assetId);
917
+ return row?.parent_asset_id;
918
+ }
919
+ function rootFor(database, project, assetId) {
920
+ let root = assetId;
921
+ const seen = /* @__PURE__ */ new Set();
922
+ while (!seen.has(root)) {
923
+ seen.add(root);
924
+ const parent = parentOf(database, project, root);
925
+ if (!parent) return root;
926
+ root = parent;
927
+ }
928
+ return assetId;
929
+ }
930
+ function latestSelectedRoot(database, project) {
931
+ const row = database.prepare("select root_asset_id from asset_selections where project_id = ? order by selected_at desc limit 1").get(project);
932
+ return row?.root_asset_id;
933
+ }
934
+ function resolveRoot(database, project, rootAssetId) {
935
+ if (rootAssetId) {
936
+ requireAsset(database, project, rootAssetId);
937
+ return rootAssetId;
938
+ }
939
+ const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
940
+ if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
941
+ requireAsset(database, project, root);
942
+ return root;
943
+ }
944
+ function edgeId(project, parent, child) {
945
+ return `${project}:${parent}:derived_from:${child}`;
946
+ }
947
+ function canPreviewLocally(mediaType, localPath) {
948
+ return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
949
+ }
950
+ function localPreviewUrl(project, localPath) {
951
+ if (!localPath) return void 0;
952
+ const params = new URLSearchParams({ project, path: localPath });
953
+ return `/api/assets/local-preview?${params.toString()}`;
954
+ }
955
+ function linkLineageAssets(project, fields) {
956
+ const database = lineageDb();
957
+ requireAsset(database, project, fields.parentAssetId);
958
+ requireAsset(database, project, fields.childAssetId);
959
+ if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
960
+ const edge = {
961
+ id: edgeId(project, fields.parentAssetId, fields.childAssetId),
962
+ parent_asset_id: fields.parentAssetId,
963
+ child_asset_id: fields.childAssetId,
964
+ relation_type: "derived_from",
965
+ created_at: nowIso()
966
+ };
967
+ if (!fields.confirmWrite) {
968
+ database.close();
969
+ return { ok: true, dryRun: true, edge };
970
+ }
971
+ database.prepare(`
972
+ insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
973
+ values (?, ?, ?, ?, 'derived_from', ?)
974
+ on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
975
+ `).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
976
+ database.close();
977
+ return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
978
+ }
979
+ function descendants(database, project, root) {
980
+ const edges = [];
981
+ const queue = [root];
982
+ const seen = /* @__PURE__ */ new Set();
983
+ while (queue.length > 0) {
984
+ const parent = queue.shift();
985
+ if (seen.has(parent)) continue;
986
+ seen.add(parent);
987
+ const rows = database.prepare("select id, parent_asset_id, child_asset_id, relation_type, created_at from asset_edges where project_id = ? and parent_asset_id = ? order by created_at").all(project, parent);
988
+ edges.push(...rows);
989
+ queue.push(...rows.map((row) => row.child_asset_id));
990
+ }
991
+ return edges;
992
+ }
993
+ function getLineageSnapshot(project, assetId) {
994
+ const database = lineageDb();
995
+ requireAsset(database, project, assetId);
996
+ const root = rootFor(database, project, assetId);
997
+ const edges = descendants(database, project, root);
998
+ const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
999
+ const placeholders = ids.map(() => "?").join(",");
1000
+ const rows = database.prepare(`
1001
+ select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
1002
+ a.local_path, a.s3_key, a.checksum_sha256, coalesce(r.review_state, 'unreviewed') review_state,
1003
+ r.notes review_notes, l.x layout_x, l.y layout_y
1004
+ from assets a left join asset_reviews r on r.asset_id = a.id
1005
+ left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
1006
+ where a.project_id = ? and a.id in (${placeholders})
1007
+ `).all(root, project, ...ids);
1008
+ const selected = selectedRows(database, project, root);
1009
+ const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
1010
+ const selectedIds = new Set(selected.map((row) => row.asset_id));
1011
+ const selections = selected.map((row) => ({
1012
+ asset_id: row.asset_id,
1013
+ notes: row.notes || void 0,
1014
+ position: Number(row.position || 0),
1015
+ selected_at: row.selected_at
1016
+ }));
1017
+ const selection = selections[0] || null;
1018
+ const nodes = rows.map((row) => {
1019
+ const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
1020
+ const { layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
1021
+ const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
1022
+ return {
1023
+ ...node,
1024
+ is_latest: !childIds.has(row.asset_id),
1025
+ position,
1026
+ preview_url: canPreviewLocally(row.media_type, row.local_path) ? localPreviewUrl(project, row.local_path) : void 0,
1027
+ selection_note: nodeSelection?.notes,
1028
+ user_selected: selectedIds.has(row.asset_id)
1029
+ };
1030
+ });
1031
+ database.close();
1032
+ return {
1033
+ project,
1034
+ root_asset_id: root,
1035
+ active_asset_id: assetId,
1036
+ selected: selections.map((row) => row.asset_id),
1037
+ selection,
1038
+ selections,
1039
+ latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
1040
+ nodes,
1041
+ edges,
1042
+ fetchedAt: nowIso()
1043
+ };
1044
+ }
1045
+ function getLineageNextAsset(project, rootAssetId) {
1046
+ const database = lineageDb();
1047
+ const root = resolveRoot(database, project, rootAssetId);
1048
+ database.close();
1049
+ const snapshot = getLineageSnapshot(project, root);
1050
+ const selectedNodes = snapshot.selected.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
1051
+ const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
1052
+ const warnings = [];
1053
+ for (const selectedNode of selectedNodes) {
1054
+ if (selectedNode.is_latest) continue;
1055
+ warnings.push("Selected asset is not a latest leaf; agents should treat this as an intentional branch choice.");
1056
+ }
1057
+ if (selectedNodes.length > 0) {
1058
+ return {
1059
+ project,
1060
+ root_asset_id: snapshot.root_asset_id,
1061
+ strategy: "selected",
1062
+ selection_mode: selectedNodes.length > 1 ? "multiple" : "single",
1063
+ recommended_action: "evolve_variations",
1064
+ reason: "user_selected",
1065
+ next_asset: selectedNodes[0],
1066
+ next_assets: selectedNodes,
1067
+ latest: snapshot.latest,
1068
+ selected: snapshot.selected,
1069
+ selection: snapshot.selection,
1070
+ selections: snapshot.selections,
1071
+ candidates: latestNodes,
1072
+ warnings,
1073
+ fetchedAt: nowIso()
1074
+ };
1075
+ }
1076
+ if (latestNodes.length === 1) {
1077
+ return {
1078
+ project,
1079
+ root_asset_id: snapshot.root_asset_id,
1080
+ strategy: "single_latest",
1081
+ selection_mode: "fallback",
1082
+ recommended_action: "evolve_variations",
1083
+ reason: "single_latest_fallback",
1084
+ next_asset: latestNodes[0],
1085
+ next_assets: [latestNodes[0]],
1086
+ latest: snapshot.latest,
1087
+ selected: snapshot.selected,
1088
+ selection: snapshot.selection,
1089
+ selections: snapshot.selections,
1090
+ candidates: latestNodes,
1091
+ warnings,
1092
+ fetchedAt: nowIso()
1093
+ };
1094
+ }
1095
+ return {
1096
+ project,
1097
+ root_asset_id: snapshot.root_asset_id,
1098
+ strategy: latestNodes.length > 1 ? "ambiguous_latest" : "empty",
1099
+ selection_mode: "none",
1100
+ recommended_action: latestNodes.length > 1 ? "choose_next_base" : "none",
1101
+ reason: latestNodes.length > 1 ? "multiple_latest_no_selection" : "no_lineage_candidates",
1102
+ next_asset: null,
1103
+ next_assets: [],
1104
+ latest: snapshot.latest,
1105
+ selected: snapshot.selected,
1106
+ selection: snapshot.selection,
1107
+ selections: snapshot.selections,
1108
+ candidates: latestNodes,
1109
+ warnings,
1110
+ fetchedAt: nowIso()
1111
+ };
1112
+ }
1113
+
1114
+ // src/server/assetLineageHandoff.ts
1115
+ var publicPackageCommand = "npx @mean-weasel/lineage";
1116
+ function shellQuote(value) {
1117
+ return `'${value.replace(/'/g, "'\\''")}'`;
1118
+ }
1119
+ function lineageCommand(command, project, rootAssetId) {
1120
+ return `${publicPackageCommand} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --db ${shellQuote(lineageDbPath())} --json`;
1121
+ }
1122
+ function linkChildCommand(project, rootAssetId) {
1123
+ return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
1124
+ }
1125
+ function getLineageBrief(project, rootAssetId) {
1126
+ const next = getLineageNextAsset(project, rootAssetId);
1127
+ const assets = next.next_assets;
1128
+ const asset = next.next_asset;
1129
+ const referenceAssetIds = assets.map((item) => item.asset_id);
1130
+ const rationale = next.selections.map((selection) => selection.notes).find(Boolean) || asset?.selection_note || next.selection?.notes;
1131
+ const channels = [...new Set(assets.map((item) => item.channel || "unknown"))];
1132
+ const campaigns = [...new Set(assets.map((item) => item.campaign || "unknown"))];
1133
+ const prompt = assets.length > 0 ? [
1134
+ assets.length === 1 ? `Create 3-4 variations from asset ${assets[0].asset_id} (${assets[0].title}).` : `Create 3-4 variations using these ${assets.length} selected references: ${referenceAssetIds.join(", ")}.`,
1135
+ rationale ? `Preserve this selection rationale: ${rationale}` : "Preserve the strongest visible ideas while exploring distinct alternatives.",
1136
+ `Keep project=${project}, root=${next.root_asset_id}, channels=${channels.join(",")}, campaigns=${campaigns.join(",")}.`,
1137
+ "After generation, index outputs and link chosen children with lineage link-child."
1138
+ ].join(" ") : "Select one to three latest lineage candidates before generating variations.";
1139
+ return {
1140
+ project,
1141
+ root_asset_id: next.root_asset_id,
1142
+ strategy: next.strategy,
1143
+ selection_mode: next.selection_mode,
1144
+ recommended_action: next.recommended_action,
1145
+ reason: next.reason,
1146
+ next_asset: asset,
1147
+ next_assets: assets,
1148
+ selection: next.selection,
1149
+ selections: next.selections,
1150
+ latest: next.latest,
1151
+ warnings: next.warnings,
1152
+ brief: {
1153
+ title: asset ? `Evolve ${assets.length > 1 ? `${assets.length} selected bases` : asset.title}` : "Choose next lineage base",
1154
+ objective: asset ? "Generate the next branch of visual variations from the selected lineage base or bases." : "Resolve the next base before generation.",
1155
+ prompt,
1156
+ reference_asset_id: asset?.asset_id,
1157
+ reference_asset_ids: referenceAssetIds,
1158
+ rationale
1159
+ },
1160
+ handoff: {
1161
+ next_command: lineageCommand("next", project, next.root_asset_id),
1162
+ inspect_command: asset ? `${publicPackageCommand} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} --db ${shellQuote(lineageDbPath())} --json` : void 0,
1163
+ link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
1164
+ },
1165
+ fetchedAt: nowIso()
1166
+ };
1167
+ }
1168
+ function linkSelectedLineageChild(project, fields) {
1169
+ const next = getLineageNextAsset(project, fields.rootAssetId);
1170
+ if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
1171
+ const result = linkLineageAssets(project, {
1172
+ childAssetId: fields.childAssetId,
1173
+ confirmWrite: fields.confirmWrite,
1174
+ parentAssetId: next.next_asset.asset_id
1175
+ });
1176
+ return {
1177
+ ...result,
1178
+ root_asset_id: next.root_asset_id,
1179
+ parent_asset_id: next.next_asset.asset_id,
1180
+ child_asset_id: fields.childAssetId,
1181
+ reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
1182
+ warning: next.next_assets.length > 1 ? "Linked child to the primary selected base; add explicit edges to other selected references if the child derives from them too." : void 0
1183
+ };
1184
+ }
1185
+
1186
+ // src/cli/lineageCli.ts
9
1187
  var signalExitCodes = {
10
1188
  SIGINT: 130,
11
1189
  SIGTERM: 143
12
1190
  };
13
1191
  function packageRoot() {
14
- return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
1192
+ return resolve3(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
15
1193
  }
16
1194
  function packageVersion() {
17
1195
  try {
18
- const packageInfo = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf8"));
1196
+ const packageInfo = JSON.parse(readFileSync3(join5(packageRoot(), "package.json"), "utf8"));
19
1197
  return packageInfo.version || "0.0.0";
20
1198
  } catch {
21
1199
  return "0.0.0";
@@ -23,9 +1201,9 @@ function packageVersion() {
23
1201
  }
24
1202
  function dataRoot(displayName) {
25
1203
  if (process.env.LINEAGE_HOME) return process.env.LINEAGE_HOME;
26
- if (platform() === "darwin") return join(homedir(), "Library", "Application Support", displayName);
27
- if (platform() === "win32") return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), displayName);
28
- return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
1204
+ if (platform() === "darwin") return join5(homedir(), "Library", "Application Support", displayName);
1205
+ if (platform() === "win32") return join5(process.env.APPDATA || join5(homedir(), "AppData", "Roaming"), displayName);
1206
+ return join5(process.env.XDG_DATA_HOME || join5(homedir(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
29
1207
  }
30
1208
  function readOption(args, name) {
31
1209
  const prefix = `${name}=`;
@@ -43,8 +1221,8 @@ function resolveStartOptions(config, args) {
43
1221
  throw new Error(`Invalid port: ${rawPort}`);
44
1222
  }
45
1223
  return {
46
- dbPath: readOption(args, "--db") || process.env.LINEAGE_DB || join(runtimeDir, `${config.binName}.sqlite`),
47
- host: readOption(args, "--host") || process.env.HOST || "127.0.0.1",
1224
+ dbPath: readOption(args, "--db") || process.env.LINEAGE_DB || join5(runtimeDir, `${config.binName}.sqlite`),
1225
+ host: readOption(args, "--host") || process.env.HOST || config.defaultHost,
48
1226
  json: args.includes("--json"),
49
1227
  open: args.includes("--open"),
50
1228
  port
@@ -55,6 +1233,10 @@ function printHelp(config) {
55
1233
 
56
1234
  Usage:
57
1235
  ${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
1236
+ ${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
1237
+ ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
1238
+ ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
1239
+ ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
58
1240
  ${config.binName} --help
59
1241
  ${config.binName} --version
60
1242
 
@@ -66,6 +1248,80 @@ function openBrowser(url) {
66
1248
  const opener = spawn(command, args, { detached: true, stdio: "ignore" });
67
1249
  opener.unref();
68
1250
  }
1251
+ function positionalArgs(args) {
1252
+ const values = [];
1253
+ for (let index = 0; index < args.length; index += 1) {
1254
+ const arg = args[index];
1255
+ if (arg.startsWith("--")) {
1256
+ if (!arg.includes("=") && args[index + 1] && !args[index + 1].startsWith("--")) index += 1;
1257
+ continue;
1258
+ }
1259
+ values.push(arg);
1260
+ }
1261
+ return values;
1262
+ }
1263
+ function resolveDataCommandOptions(args) {
1264
+ const positions = positionalArgs(args);
1265
+ const options = {
1266
+ assetId: readOption(args, "--asset-id") || positions[0],
1267
+ childAssetId: readOption(args, "--child"),
1268
+ confirmWrite: args.includes("--confirm-write"),
1269
+ dbPath: readOption(args, "--db"),
1270
+ json: args.includes("--json"),
1271
+ project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
1272
+ rootAssetId: readOption(args, "--root")
1273
+ };
1274
+ if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
1275
+ return options;
1276
+ }
1277
+ function runLineageDataCommand(command, args) {
1278
+ const options = resolveDataCommandOptions(args);
1279
+ if (command === "next") return getLineageNextAsset(options.project, options.rootAssetId || options.assetId);
1280
+ if (command === "brief") return getLineageBrief(options.project, options.rootAssetId || options.assetId);
1281
+ if (command === "inspect") {
1282
+ if (!options.assetId) throw new Error("lineage inspect requires --asset-id");
1283
+ return getLineageSnapshot(options.project, options.assetId);
1284
+ }
1285
+ if (command === "link-child") {
1286
+ if (!options.childAssetId) throw new Error("lineage link-child requires --child");
1287
+ return linkSelectedLineageChild(options.project, {
1288
+ childAssetId: options.childAssetId,
1289
+ confirmWrite: options.confirmWrite,
1290
+ rootAssetId: options.rootAssetId || options.assetId
1291
+ });
1292
+ }
1293
+ throw new Error(`Unknown command: ${command}`);
1294
+ }
1295
+ function printDataResult(command, result, json) {
1296
+ if (json) {
1297
+ console.log(JSON.stringify(result, null, 2));
1298
+ return;
1299
+ }
1300
+ if (command === "next" && result && typeof result === "object" && "reason" in result) {
1301
+ const next = result;
1302
+ console.log(next.next_asset ? `${next.next_asset.asset_id}: ${next.next_asset.title}` : `No next asset: ${next.reason}`);
1303
+ console.log(`Root: ${next.root_asset_id}`);
1304
+ return;
1305
+ }
1306
+ if (command === "brief" && result && typeof result === "object" && "brief" in result) {
1307
+ const brief = result;
1308
+ console.log(brief.brief.title);
1309
+ console.log(brief.brief.prompt);
1310
+ return;
1311
+ }
1312
+ if (command === "inspect" && result && typeof result === "object" && "nodes" in result) {
1313
+ const snapshot = result;
1314
+ console.log(`${snapshot.root_asset_id}: ${snapshot.nodes.length} node(s), ${snapshot.edges.length} edge(s)`);
1315
+ console.log(`Active: ${snapshot.active_asset_id}`);
1316
+ return;
1317
+ }
1318
+ if (command === "link-child" && result && typeof result === "object") {
1319
+ const link = result;
1320
+ console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
1321
+ return;
1322
+ }
1323
+ console.log(String(result));
1324
+ }
69
1325
  function start(config, args) {
70
1326
  let options;
71
1327
  const json = args.includes("--json");
@@ -77,14 +1333,14 @@ function start(config, args) {
77
1333
  else console.error(`${config.binName}: ${message}`);
78
1334
  process.exit(1);
79
1335
  }
80
- const serverPath = join(packageRoot(), "dist", "server.js");
81
- if (!existsSync(serverPath)) {
1336
+ const serverPath = join5(packageRoot(), "dist", "server.js");
1337
+ if (!existsSync4(serverPath)) {
82
1338
  const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
83
1339
  if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
84
1340
  else console.error(`${config.binName}: ${message}`);
85
1341
  process.exit(1);
86
1342
  }
87
- mkdirSync(dirname(options.dbPath), { recursive: true });
1343
+ mkdirSync4(dirname3(options.dbPath), { recursive: true });
88
1344
  const url = `http://${options.host}:${options.port}`;
89
1345
  if (options.json) {
90
1346
  console.log(JSON.stringify({ channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, status: "starting", url }, null, 2));
@@ -126,11 +1382,25 @@ function runLineageCli(config, args = process.argv.slice(2)) {
126
1382
  console.log(packageVersion());
127
1383
  process.exit(0);
128
1384
  }
129
- const [command] = args;
1385
+ const normalizedArgs = args[0] === "lineage" ? args.slice(1) : args;
1386
+ const [command] = normalizedArgs;
130
1387
  if (command === "start") {
131
- start(config, args.slice(1));
1388
+ start(config, normalizedArgs.slice(1));
132
1389
  return;
133
1390
  }
1391
+ if (command === "next" || command === "brief" || command === "inspect" || command === "link-child") {
1392
+ const commandArgs = normalizedArgs.slice(1);
1393
+ const json2 = commandArgs.includes("--json");
1394
+ try {
1395
+ printDataResult(command, runLineageDataCommand(command, commandArgs), json2);
1396
+ } catch (error) {
1397
+ const message2 = error instanceof Error ? error.message : String(error);
1398
+ if (json2) console.error(JSON.stringify({ ok: false, command, error: message2 }, null, 2));
1399
+ else console.error(`${config.binName}: ${message2}`);
1400
+ process.exit(1);
1401
+ }
1402
+ process.exit(0);
1403
+ }
134
1404
  const json = args.includes("--json");
135
1405
  const message = `Unknown command: ${command}`;
136
1406
  if (json) console.error(JSON.stringify({ ok: false, command, error: message }, null, 2));
@@ -139,5 +1409,5 @@ function runLineageCli(config, args = process.argv.slice(2)) {
139
1409
  }
140
1410
 
141
1411
  // src/cli/lineage-dev.ts
142
- runLineageCli({ binName: "lineage-dev", channel: "development", defaultPort: 5198, displayName: "Lineage Dev" });
1412
+ runLineageCli({ binName: "lineage-dev", channel: "development", defaultHost: "lineage-dev.localhost", defaultPort: 5198, displayName: "Lineage Dev" });
143
1413
  //# sourceMappingURL=lineage-dev.js.map