@mean-weasel/lineage 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { existsSync as existsSync14 } from "node:fs";
5
5
  import { join as join14 } from "node:path";
6
6
 
7
7
  // src/server/assetCore.ts
8
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
8
+ import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
9
9
  import { dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
@@ -288,7 +288,7 @@ import { join as join5 } from "node:path";
288
288
 
289
289
  // src/server/profileWriterLease.ts
290
290
  import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
291
- import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
291
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
292
292
  import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
293
293
 
294
294
  // src/server/lineageProfiles.ts
@@ -296,13 +296,18 @@ import { createHash as createHash2, randomUUID } from "node:crypto";
296
296
  import { createRequire } from "node:module";
297
297
  import {
298
298
  chmodSync,
299
+ closeSync,
299
300
  constants as fsConstants,
300
301
  copyFileSync as copyFileSync2,
301
302
  existsSync as existsSync3,
303
+ fsyncSync,
302
304
  linkSync,
305
+ lstatSync,
303
306
  mkdirSync as mkdirSync2,
307
+ openSync,
304
308
  readFileSync as readFileSync2,
305
309
  realpathSync,
310
+ renameSync,
306
311
  rmSync,
307
312
  statSync as statSync3,
308
313
  writeFileSync
@@ -344,13 +349,13 @@ function profileManifestPath(selector) {
344
349
  if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
345
350
  return { manifestPath: join3(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
346
351
  }
347
- function requiredString(record, key) {
348
- const value = record[key];
352
+ function requiredString(record2, key) {
353
+ const value = record2[key];
349
354
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
350
355
  return value.trim();
351
356
  }
352
- function optionalString(record, key) {
353
- const value = record[key];
357
+ function optionalString(record2, key) {
358
+ const value = record2[key];
354
359
  if (value === void 0) return void 0;
355
360
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
356
361
  return value.trim();
@@ -401,15 +406,15 @@ function resolveLineageProfile(selector) {
401
406
  throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
402
407
  }
403
408
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
404
- const record = raw;
405
- const schemaVersion = requiredString(record, "schema_version");
409
+ const record2 = raw;
410
+ const schemaVersion = requiredString(record2, "schema_version");
406
411
  if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
407
- const profileId = requiredString(record, "profile_id");
412
+ const profileId = requiredString(record2, "profile_id");
408
413
  if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
409
414
  if (namedProfileId && namedProfileId !== profileId) {
410
415
  throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
411
416
  }
412
- const expectedRaw = record.expected_runtime;
417
+ const expectedRaw = record2.expected_runtime;
413
418
  if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
414
419
  throw new Error("Profile expected_runtime must be an object");
415
420
  }
@@ -418,18 +423,18 @@ function resolveLineageProfile(selector) {
418
423
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
419
424
  const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
420
425
  const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
421
- const migrationsRaw = record.required_schema_migrations;
426
+ const migrationsRaw = record2.required_schema_migrations;
422
427
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
423
428
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
424
429
  }
425
- const environment = validateEnvironment(requiredString(record, "environment"));
430
+ const environment = validateEnvironment(requiredString(record2, "environment"));
426
431
  const expectedChannel = validateChannel(expected?.channel);
427
432
  if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
428
433
  throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
429
434
  }
430
435
  const manifest = {
431
- asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
432
- database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
436
+ asset_root: resolveManifestPath(manifestPath, requiredString(record2, "asset_root")),
437
+ database_path: resolveManifestPath(manifestPath, requiredString(record2, "database_path")),
433
438
  environment,
434
439
  ...expected ? {
435
440
  expected_runtime: {
@@ -443,7 +448,7 @@ function resolveLineageProfile(selector) {
443
448
  profile_id: profileId,
444
449
  ...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
445
450
  schema_version: lineageProfileSchemaVersion,
446
- service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
451
+ service_origin: validateServiceOrigin(requiredString(record2, "service_origin"))
447
452
  };
448
453
  return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
449
454
  }
@@ -693,7 +698,7 @@ function errorCode(error) {
693
698
  return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
694
699
  }
695
700
  function readOwner(lockPath) {
696
- const stat = lstatSync(lockPath);
701
+ const stat = lstatSync2(lockPath);
697
702
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
698
703
  throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
699
704
  }
@@ -742,7 +747,7 @@ function reclaimDeadOwner(lockPath, owner) {
742
747
  const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
743
748
  const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
744
749
  try {
745
- renameSync(lockPath, tombstone);
750
+ renameSync2(lockPath, tombstone);
746
751
  } catch (error) {
747
752
  if (errorCode(error) === "ENOENT") return;
748
753
  throw error;
@@ -914,6 +919,10 @@ function lineageDb() {
914
919
  child_asset_id text not null references assets(id),
915
920
  relation_type text not null check (relation_type in ('derived_from')),
916
921
  created_at text not null,
922
+ summary text,
923
+ summary_created_by text check (summary_created_by in ('human', 'agent', 'system')),
924
+ summary_updated_by text check (summary_updated_by in ('human', 'agent', 'system')),
925
+ summary_updated_at text,
917
926
  unique (project_id, parent_asset_id, child_asset_id, relation_type)
918
927
  );
919
928
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
@@ -1197,6 +1206,7 @@ function lineageDb() {
1197
1206
  imported_asset_id text not null references assets(id),
1198
1207
  parent_asset_id text not null references assets(id),
1199
1208
  imported_at text not null,
1209
+ edge_summary text,
1200
1210
  unique(job_id, output_index),
1201
1211
  unique(job_id, file_path)
1202
1212
  );
@@ -1285,6 +1295,11 @@ function lineageDb() {
1285
1295
  );
1286
1296
  create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
1287
1297
  `);
1298
+ ensureColumn(database, "asset_edges", "summary", "text");
1299
+ ensureColumn(database, "asset_edges", "summary_created_by", "text check (summary_created_by in ('human', 'agent', 'system'))");
1300
+ ensureColumn(database, "asset_edges", "summary_updated_by", "text check (summary_updated_by in ('human', 'agent', 'system'))");
1301
+ ensureColumn(database, "asset_edges", "summary_updated_at", "text");
1302
+ ensureColumn(database, "generation_job_outputs", "edge_summary", "text");
1288
1303
  migrateAssetSelections(database);
1289
1304
  dropLegacyAssetSelectionRootUnique(database);
1290
1305
  ensureColumn(database, "asset_selections", "notes", "text");
@@ -1638,8 +1653,8 @@ function syncLedgerPlacement(project, assetId, placement) {
1638
1653
  function placeholders(values) {
1639
1654
  return values.map(() => "?").join(",");
1640
1655
  }
1641
- function assetIdsForRecord(record, sources) {
1642
- return [.../* @__PURE__ */ new Set([record.canonical_asset_id, ...sources.map((source) => source.asset_id).filter(Boolean)])];
1656
+ function assetIdsForRecord(record2, sources) {
1657
+ return [.../* @__PURE__ */ new Set([record2.canonical_asset_id, ...sources.map((source) => source.asset_id).filter(Boolean)])];
1643
1658
  }
1644
1659
  function reviewFromRow(row) {
1645
1660
  return {
@@ -1673,7 +1688,7 @@ function selectionFromRow(row) {
1673
1688
  };
1674
1689
  }
1675
1690
  function ledgerWorkflowStates(database, project, records, sources) {
1676
- const recordAssets = new Map(records.map((record) => [record.id, assetIdsForRecord(record, sources.filter((source) => source.record_id === record.id))]));
1691
+ const recordAssets = new Map(records.map((record2) => [record2.id, assetIdsForRecord(record2, sources.filter((source) => source.record_id === record2.id))]));
1677
1692
  const assetIds = [...new Set([...recordAssets.values()].flat())];
1678
1693
  if (assetIds.length === 0) return {};
1679
1694
  const marker = placeholders(assetIds);
@@ -1692,11 +1707,11 @@ function ledgerWorkflowStates(database, project, records, sources) {
1692
1707
  from asset_selections
1693
1708
  where project_id = ? and asset_id in (${marker})
1694
1709
  `).all(project, ...assetIds);
1695
- return Object.fromEntries(records.map((record) => {
1696
- const ids = recordAssets.get(record.id) || [];
1710
+ return Object.fromEntries(records.map((record2) => {
1711
+ const ids = recordAssets.get(record2.id) || [];
1697
1712
  const review = reviews.find((row) => ids.includes(row.asset_id));
1698
1713
  const selection = selections.find((row) => ids.includes(row.asset_id));
1699
- return [record.id, {
1714
+ return [record2.id, {
1700
1715
  review: review ? reviewFromRow(review) : void 0,
1701
1716
  placements: placements.filter((row) => ids.includes(row.asset_id)).map(placementFromRow),
1702
1717
  selection: selection ? selectionFromRow(selection) : void 0
@@ -2105,7 +2120,8 @@ function deleteObjectGuarded(project, assetId, confirmation) {
2105
2120
  }
2106
2121
  function ensureUploadDir() {
2107
2122
  const dir = join6(repoRoot, ".asset-scratch", "studio-uploads");
2108
- mkdirSync5(dir, { recursive: true });
2123
+ mkdirSync5(dir, { recursive: true, mode: 448 });
2124
+ chmodSync2(dir, 448);
2109
2125
  return dir;
2110
2126
  }
2111
2127
  function cleanupUploadedTemp(file) {
@@ -2148,6 +2164,32 @@ function lookupAssets(project, assetIds) {
2148
2164
  // src/server/assetLineage.ts
2149
2165
  import { join as join7 } from "node:path";
2150
2166
 
2167
+ // src/shared/edgeSummary.ts
2168
+ var edgeSummaryWordLimit = 2;
2169
+ var EdgeSummaryValidationError = class extends Error {
2170
+ constructor(code, message) {
2171
+ super(message);
2172
+ this.code = code;
2173
+ this.name = "EdgeSummaryValidationError";
2174
+ }
2175
+ code;
2176
+ };
2177
+ function normalizeEdgeSummary(value) {
2178
+ if (value === void 0 || value === null) return void 0;
2179
+ if (typeof value !== "string") throw new EdgeSummaryValidationError("not_text", "Edge summary must be text");
2180
+ const words = value.trim().split(/\s+/u).filter(Boolean);
2181
+ if (words.length === 0) return void 0;
2182
+ if (words.length > edgeSummaryWordLimit) {
2183
+ throw new EdgeSummaryValidationError("too_many_words", `Edge summary must contain at most ${edgeSummaryWordLimit} words`);
2184
+ }
2185
+ return words.join(" ");
2186
+ }
2187
+ function requireEdgeSummary(value) {
2188
+ const summary = normalizeEdgeSummary(value);
2189
+ if (!summary) throw new EdgeSummaryValidationError("required", "Edge summary is required");
2190
+ return summary;
2191
+ }
2192
+
2151
2193
  // src/server/assetLineageSelection.ts
2152
2194
  var LINEAGE_NEXT_VARIATION_LIMIT = 3;
2153
2195
  function selectionId(project, root, assetId) {
@@ -3578,6 +3620,41 @@ function rerollRequestId(project, root, node, timestamp) {
3578
3620
  function rowString(value) {
3579
3621
  return typeof value === "string" && value.length > 0 ? value : void 0;
3580
3622
  }
3623
+ function lineageEdgeFromRow(row) {
3624
+ const summary = rowString(row.summary);
3625
+ const summaryCreatedBy = rowString(row.summary_created_by);
3626
+ const summaryUpdatedBy = rowString(row.summary_updated_by);
3627
+ const summaryUpdatedAt = rowString(row.summary_updated_at);
3628
+ return {
3629
+ id: String(row.id),
3630
+ parent_asset_id: String(row.parent_asset_id),
3631
+ child_asset_id: String(row.child_asset_id),
3632
+ relation_type: row.relation_type,
3633
+ created_at: String(row.created_at),
3634
+ ...summary ? { summary } : {},
3635
+ ...summaryCreatedBy ? { summary_created_by: summaryCreatedBy } : {},
3636
+ ...summaryUpdatedBy ? { summary_updated_by: summaryUpdatedBy } : {},
3637
+ ...summaryUpdatedAt ? { summary_updated_at: summaryUpdatedAt } : {}
3638
+ };
3639
+ }
3640
+ function nextEdgeSummaryTimestamp(current) {
3641
+ const currentMillis = current ? Date.parse(current) : Number.NaN;
3642
+ const nextMillis = Number.isFinite(currentMillis) ? Math.max(Date.now(), currentMillis + 1) : Date.now();
3643
+ return new Date(nextMillis).toISOString();
3644
+ }
3645
+ function expectedEdgeSummaryTimestamp(value) {
3646
+ if (value === null) return null;
3647
+ if (typeof value === "string" && value.length > 0) return value;
3648
+ throw new LineageError("Edge summary update requires expectedSummaryUpdatedAt as a timestamp or null");
3649
+ }
3650
+ function validatedHumanEdgeSummary(value) {
3651
+ try {
3652
+ return requireEdgeSummary(value);
3653
+ } catch (error) {
3654
+ if (error instanceof EdgeSummaryValidationError) throw new LineageError(error.message);
3655
+ throw error;
3656
+ }
3657
+ }
3581
3658
  function rerollRequestFrom(row) {
3582
3659
  return {
3583
3660
  id: String(row.id),
@@ -3677,6 +3754,9 @@ function localPreviewUrl(project, localPath) {
3677
3754
  return `/api/assets/local-preview?${params.toString()}`;
3678
3755
  }
3679
3756
  function linkLineageAssets(project, fields) {
3757
+ const summary = normalizeEdgeSummary(fields.summary);
3758
+ if (summary && !fields.summaryActor) throw new LineageError("Lineage edge summary requires an explicit author");
3759
+ if (!summary && fields.summaryActor) throw new LineageError("Lineage edge summary author requires a summary");
3680
3760
  const database = lineageDb();
3681
3761
  requireAsset3(database, project, fields.parentAssetId);
3682
3762
  requireAsset3(database, project, fields.childAssetId);
@@ -3695,25 +3775,135 @@ function linkLineageAssets(project, fields) {
3695
3775
  database.close();
3696
3776
  throw error;
3697
3777
  }
3778
+ const createdAt = nowIso();
3698
3779
  const edge = {
3699
3780
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
3700
3781
  parent_asset_id: fields.parentAssetId,
3701
3782
  child_asset_id: fields.childAssetId,
3702
3783
  relation_type: "derived_from",
3703
- created_at: nowIso()
3784
+ created_at: createdAt,
3785
+ ...summary ? {
3786
+ summary,
3787
+ summary_created_by: fields.summaryActor,
3788
+ summary_updated_by: fields.summaryActor,
3789
+ summary_updated_at: createdAt
3790
+ } : {}
3704
3791
  };
3705
3792
  if (!fields.confirmWrite) {
3706
3793
  database.close();
3707
3794
  return { ok: true, dryRun: true, edge };
3708
3795
  }
3709
- database.prepare(`
3710
- insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
3711
- values (?, ?, ?, ?, 'derived_from', ?)
3796
+ const inserted = database.prepare(`
3797
+ insert into asset_edges (
3798
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
3799
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3800
+ )
3801
+ values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
3712
3802
  on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
3713
- `).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
3803
+ `).run(
3804
+ edge.id,
3805
+ project,
3806
+ edge.parent_asset_id,
3807
+ edge.child_asset_id,
3808
+ edge.created_at,
3809
+ edge.summary || null,
3810
+ edge.summary_created_by || null,
3811
+ edge.summary_updated_by || null,
3812
+ edge.summary_updated_at || null
3813
+ );
3814
+ if (inserted.changes === 0) {
3815
+ const existingRow = database.prepare(`
3816
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3817
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3818
+ from asset_edges
3819
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
3820
+ `).get(project, edge.parent_asset_id, edge.child_asset_id);
3821
+ if (!existingRow) {
3822
+ database.close();
3823
+ throw new LineageError("Lineage edge conflict could not be resolved", 409);
3824
+ }
3825
+ const existingEdge = lineageEdgeFromRow(existingRow);
3826
+ if (summary && (existingEdge.summary !== summary || existingEdge.summary_created_by !== fields.summaryActor || existingEdge.summary_updated_by !== fields.summaryActor || !existingEdge.summary_updated_at)) {
3827
+ database.close();
3828
+ throw new LineageError("Lineage edge already exists with a different summary or provenance", 409);
3829
+ }
3830
+ database.close();
3831
+ return {
3832
+ ok: true,
3833
+ idempotent: true,
3834
+ message: `Already linked ${existingEdge.child_asset_id} from ${existingEdge.parent_asset_id}`,
3835
+ edge: existingEdge
3836
+ };
3837
+ }
3714
3838
  database.close();
3715
3839
  return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
3716
3840
  }
3841
+ function updateLineageEdgeSummary(project, fields) {
3842
+ if (!fields.confirmWrite) throw new LineageError("Edge summary updates require confirmWrite=true");
3843
+ if (fields.action !== "set" && fields.action !== "clear") throw new LineageError("Edge summary action must be set or clear");
3844
+ const expectedUpdatedAt = expectedEdgeSummaryTimestamp(fields.expectedSummaryUpdatedAt);
3845
+ const summary = fields.action === "set" ? validatedHumanEdgeSummary(fields.summary) : void 0;
3846
+ if (fields.action === "clear" && fields.summary !== void 0) throw new LineageError("Clear edge summary must not include summary text");
3847
+ const database = lineageDb();
3848
+ try {
3849
+ const row = database.prepare(`
3850
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3851
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3852
+ from asset_edges
3853
+ where project_id = ? and id = ?
3854
+ `).get(project, fields.edgeId);
3855
+ if (!row) throw new LineageError(`Unknown lineage edge: ${fields.edgeId}`, 404);
3856
+ const current = lineageEdgeFromRow(row);
3857
+ const currentUpdatedAt = current.summary_updated_at || null;
3858
+ if (currentUpdatedAt !== expectedUpdatedAt) {
3859
+ throw new LineageError("Edge summary changed since it was opened; refresh and retry", 409);
3860
+ }
3861
+ if (fields.action === "clear" && !current.summary) throw new LineageError("Edge summary is already clear");
3862
+ if (fields.action === "set" && summary === current.summary) throw new LineageError("Edge summary is unchanged");
3863
+ const claimContext = lineageWriteClaimContext(database, project, current.parent_asset_id);
3864
+ requireLineageWorkspaceClaimForWrite({
3865
+ channel: claimContext.channel,
3866
+ claimToken: fields.claimToken,
3867
+ confirmWrite: fields.confirmWrite,
3868
+ project,
3869
+ rootAssetId: claimContext.rootAssetId,
3870
+ writeKind: "lineage_edge_summary"
3871
+ });
3872
+ const updatedAt = nextEdgeSummaryTimestamp(current.summary_updated_at);
3873
+ const createdBy = fields.action === "set" ? current.summary ? current.summary_created_by || "human" : "human" : current.summary_created_by || "human";
3874
+ const result = database.prepare(`
3875
+ update asset_edges
3876
+ set summary = ?, summary_created_by = ?, summary_updated_by = 'human', summary_updated_at = ?
3877
+ where project_id = ? and id = ?
3878
+ and ((summary_updated_at is null and ? is null) or summary_updated_at = ?)
3879
+ `).run(
3880
+ summary || null,
3881
+ createdBy,
3882
+ updatedAt,
3883
+ project,
3884
+ fields.edgeId,
3885
+ expectedUpdatedAt,
3886
+ expectedUpdatedAt
3887
+ );
3888
+ if (Number(result.changes) !== 1) {
3889
+ throw new LineageError("Edge summary changed since it was opened; refresh and retry", 409);
3890
+ }
3891
+ const updatedRow = database.prepare(`
3892
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3893
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3894
+ from asset_edges
3895
+ where project_id = ? and id = ?
3896
+ `).get(project, fields.edgeId);
3897
+ const edge = lineageEdgeFromRow(updatedRow);
3898
+ return {
3899
+ ok: true,
3900
+ edge,
3901
+ message: fields.action === "clear" ? "Cleared edge label" : `Saved edge label \u201C${edge.summary}\u201D`
3902
+ };
3903
+ } finally {
3904
+ database.close();
3905
+ }
3906
+ }
3717
3907
  function descendants(database, project, root) {
3718
3908
  const edges = [];
3719
3909
  const queue = [root];
@@ -3722,7 +3912,13 @@ function descendants(database, project, root) {
3722
3912
  const parent = queue.shift();
3723
3913
  if (seen.has(parent)) continue;
3724
3914
  seen.add(parent);
3725
- 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);
3915
+ const rows = database.prepare(`
3916
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3917
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3918
+ from asset_edges
3919
+ where project_id = ? and parent_asset_id = ?
3920
+ order by created_at
3921
+ `).all(project, parent).map(lineageEdgeFromRow);
3726
3922
  edges.push(...rows);
3727
3923
  queue.push(...rows.map((row) => row.child_asset_id));
3728
3924
  }
@@ -4333,7 +4529,7 @@ function lineageCommand(command, project, rootAssetId) {
4333
4529
  return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
4334
4530
  }
4335
4531
  function linkChildCommand(project, rootAssetId) {
4336
- return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
4532
+ return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --summary "<one-or-two-words>" --confirm-write ${lineageRuntimeSelector()} --json`;
4337
4533
  }
4338
4534
  function rerollImportGuidance(rootAssetId, targetAssetId) {
4339
4535
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -4382,6 +4578,7 @@ function getLineageBrief(project, rootAssetId) {
4382
4578
  };
4383
4579
  }
4384
4580
  function linkSelectedLineageChild(project, fields) {
4581
+ const summary = fields.summaryActor ? requireEdgeSummary(fields.summary) : fields.summary;
4385
4582
  const next = getLineageNextAsset(project, fields.rootAssetId);
4386
4583
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
4387
4584
  const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
@@ -4410,7 +4607,9 @@ function linkSelectedLineageChild(project, fields) {
4410
4607
  childAssetId: fields.childAssetId,
4411
4608
  confirmWrite: fields.confirmWrite,
4412
4609
  claimToken: fields.claimToken,
4413
- parentAssetId: next.next_asset.asset_id
4610
+ parentAssetId: next.next_asset.asset_id,
4611
+ summary,
4612
+ summaryActor: fields.summaryActor
4414
4613
  });
4415
4614
  return {
4416
4615
  ...result,
@@ -4586,8 +4785,8 @@ function recordId(project, asset) {
4586
4785
  const checksum = asset.local?.checksum_sha256 || asset.s3?.checksum_sha256;
4587
4786
  return checksum ? `${project}:sha256:${checksum}` : `${project}:asset:${asset.asset_id}`;
4588
4787
  }
4589
- function sourceId(project, record, sourceType, key) {
4590
- return `${project}:${record}:${sourceType}:${key}`;
4788
+ function sourceId(project, record2, sourceType, key) {
4789
+ return `${project}:${record2}:${sourceType}:${key}`;
4591
4790
  }
4592
4791
  function upsertProject2(database, project) {
4593
4792
  const timestamp = nowIso();
@@ -4642,9 +4841,9 @@ function upsertAssetLedgerAsset(database, project, asset, context = {}) {
4642
4841
  if (asset.s3) upsertSource(database, project, id, "s3", asset, asset.s3.key, context);
4643
4842
  upsertLedgerPlacementsForAsset(database, project, asset);
4644
4843
  }
4645
- function upsertSource(database, project, record, sourceType, asset, key, context = {}) {
4844
+ function upsertSource(database, project, record2, sourceType, asset, key, context = {}) {
4646
4845
  const timestamp = context.seenAt || nowIso();
4647
- const id = sourceId(project, record, sourceType, key);
4846
+ const id = sourceId(project, record2, sourceType, key);
4648
4847
  database.prepare(`
4649
4848
  insert into asset_ledger_sources (
4650
4849
  id, project_id, record_id, source_type, asset_id, local_path, s3_bucket, s3_region,
@@ -4667,7 +4866,7 @@ function upsertSource(database, project, record, sourceType, asset, key, context
4667
4866
  `).run(
4668
4867
  id,
4669
4868
  project,
4670
- record,
4869
+ record2,
4671
4870
  sourceType,
4672
4871
  asset.asset_id,
4673
4872
  sourceType === "local" ? asset.local?.relative_path || null : null,
@@ -4776,13 +4975,13 @@ function getAssetLedgerSnapshot(project = defaultProject) {
4776
4975
  const totals = sourceCounts(database, project);
4777
4976
  const lastIndexRun = latestIndexRun(database, project);
4778
4977
  const sourceRecords = sources.map(toSource);
4779
- const baseRecords = records.map((record) => toRecord(record, sourceRecords.filter((source) => source.record_id === record.id)));
4978
+ const baseRecords = records.map((record2) => toRecord(record2, sourceRecords.filter((source) => source.record_id === record2.id)));
4780
4979
  const workflows = ledgerWorkflowStates(database, project, baseRecords, sourceRecords);
4781
4980
  database.close();
4782
4981
  return {
4783
4982
  project,
4784
4983
  database: lineageDbPath(),
4785
- records: baseRecords.map((record) => ({ ...record, workflow: workflows[record.id] || { placements: [] } })),
4984
+ records: baseRecords.map((record2) => ({ ...record2, workflow: workflows[record2.id] || { placements: [] } })),
4786
4985
  last_index_run: lastIndexRun,
4787
4986
  totals: {
4788
4987
  records: records.length,
@@ -4823,22 +5022,22 @@ function toIndexRun(row) {
4823
5022
  error: row.error || void 0
4824
5023
  };
4825
5024
  }
4826
- function toRecord(record, sources) {
5025
+ function toRecord(record2, sources) {
4827
5026
  return {
4828
- id: record.id,
4829
- project: record.project_id,
4830
- canonical_asset_id: record.canonical_asset_id,
4831
- checksum_sha256: record.checksum_sha256 || void 0,
4832
- media_type: record.media_type,
4833
- title: record.title,
4834
- status: record.status,
4835
- channel: record.channel || void 0,
4836
- campaign: record.campaign || void 0,
4837
- audience: record.audience || void 0,
4838
- updated_at: record.updated_at,
4839
- first_seen_at: record.first_seen_at || void 0,
4840
- last_seen_at: record.last_seen_at,
4841
- indexed_by_run_id: record.indexed_by_run_id || void 0,
5027
+ id: record2.id,
5028
+ project: record2.project_id,
5029
+ canonical_asset_id: record2.canonical_asset_id,
5030
+ checksum_sha256: record2.checksum_sha256 || void 0,
5031
+ media_type: record2.media_type,
5032
+ title: record2.title,
5033
+ status: record2.status,
5034
+ channel: record2.channel || void 0,
5035
+ campaign: record2.campaign || void 0,
5036
+ audience: record2.audience || void 0,
5037
+ updated_at: record2.updated_at,
5038
+ first_seen_at: record2.first_seen_at || void 0,
5039
+ last_seen_at: record2.last_seen_at,
5040
+ indexed_by_run_id: record2.indexed_by_run_id || void 0,
4842
5041
  sources,
4843
5042
  workflow: { placements: [] }
4844
5043
  };
@@ -4872,26 +5071,26 @@ var selectionFilters = /* @__PURE__ */ new Set(["all", "selected", "not-selected
4872
5071
  function normalizeFilter(value, allowed, fallback) {
4873
5072
  return typeof value === "string" && allowed.has(value) ? value : fallback;
4874
5073
  }
4875
- function sourceTypes(record) {
4876
- return new Set(record.sources.map((source) => source.source_type));
5074
+ function sourceTypes(record2) {
5075
+ return new Set(record2.sources.map((source) => source.source_type));
4877
5076
  }
4878
- function matchesStorage(record, storage) {
4879
- const types = sourceTypes(record);
5077
+ function matchesStorage(record2, storage) {
5078
+ const types = sourceTypes(record2);
4880
5079
  if (storage === "local-only") return types.has("local") && !types.has("s3");
4881
5080
  if (storage === "s3-backed") return types.has("s3");
4882
5081
  if (storage === "local-and-s3") return types.has("local") && types.has("s3");
4883
5082
  if (storage === "catalog-only") return types.has("catalog") && !types.has("local") && !types.has("s3");
4884
5083
  return true;
4885
5084
  }
4886
- function matchesPlacement(record, placement) {
4887
- if (placement === "not-scheduled") return record.workflow.placements.length === 0;
4888
- if (placement === "not-posted") return !record.workflow.placements.some((item) => item.status === "posted");
5085
+ function matchesPlacement(record2, placement) {
5086
+ if (placement === "not-scheduled") return record2.workflow.placements.length === 0;
5087
+ if (placement === "not-posted") return !record2.workflow.placements.some((item) => item.status === "posted");
4889
5088
  if (placement === "all") return true;
4890
- return record.workflow.placements.some((item) => item.status === placement);
5089
+ return record2.workflow.placements.some((item) => item.status === placement);
4891
5090
  }
4892
- function matchesSelection(record, selection) {
4893
- if (selection === "selected") return Boolean(record.workflow.selection);
4894
- if (selection === "not-selected") return !record.workflow.selection;
5091
+ function matchesSelection(record2, selection) {
5092
+ if (selection === "selected") return Boolean(record2.workflow.selection);
5093
+ if (selection === "not-selected") return !record2.workflow.selection;
4895
5094
  return true;
4896
5095
  }
4897
5096
  function filterLedgerRecords(records, options = {}) {
@@ -4900,21 +5099,21 @@ function filterLedgerRecords(records, options = {}) {
4900
5099
  const selection = normalizeFilter(options.selection, selectionFilters, "all");
4901
5100
  const storage = normalizeFilter(options.storage, storageFilters, "all");
4902
5101
  const query = options.query?.trim().toLowerCase() || "";
4903
- return records.filter((record) => {
4904
- if (!matchesStorage(record, storage)) return false;
4905
- if (review !== "all" && (record.workflow.review?.review_state || "unreviewed") !== review) return false;
4906
- if (!matchesPlacement(record, placement)) return false;
4907
- if (!matchesSelection(record, selection)) return false;
5102
+ return records.filter((record2) => {
5103
+ if (!matchesStorage(record2, storage)) return false;
5104
+ if (review !== "all" && (record2.workflow.review?.review_state || "unreviewed") !== review) return false;
5105
+ if (!matchesPlacement(record2, placement)) return false;
5106
+ if (!matchesSelection(record2, selection)) return false;
4908
5107
  if (!query) return true;
4909
5108
  return [
4910
- record.canonical_asset_id,
4911
- record.title,
4912
- record.status,
4913
- record.channel,
4914
- record.campaign,
4915
- record.audience,
4916
- record.workflow.review?.review_state,
4917
- ...record.workflow.placements.map((item) => `${item.channel} ${item.status}`)
5109
+ record2.canonical_asset_id,
5110
+ record2.title,
5111
+ record2.status,
5112
+ record2.channel,
5113
+ record2.campaign,
5114
+ record2.audience,
5115
+ record2.workflow.review?.review_state,
5116
+ ...record2.workflow.placements.map((item) => `${item.channel} ${item.status}`)
4918
5117
  ].join(" ").toLowerCase().includes(query);
4919
5118
  });
4920
5119
  }
@@ -6994,7 +7193,141 @@ function contentBatchRouter(projectFrom2) {
6994
7193
  import { existsSync as existsSync9, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
6995
7194
  import { relative as relative4, resolve as resolve5 } from "node:path";
6996
7195
  import { randomUUID as randomUUID3 } from "node:crypto";
6997
- var adapterVersion = "generation-receipts-v1";
7196
+
7197
+ // src/shared/generationOutputManifest.ts
7198
+ var generationOutputManifestSchemaVersion = "lineage.generation_output_manifest.v1";
7199
+ var GenerationOutputManifestError = class extends Error {
7200
+ constructor(message) {
7201
+ super(message);
7202
+ this.name = "GenerationOutputManifestError";
7203
+ }
7204
+ };
7205
+ function record(value, label) {
7206
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7207
+ throw new GenerationOutputManifestError(`${label} must be an object`);
7208
+ }
7209
+ return value;
7210
+ }
7211
+ function exactKeys(value, allowed, label) {
7212
+ const allowedKeys = new Set(allowed);
7213
+ const unsupported = Object.keys(value).filter((key) => !allowedKeys.has(key)).sort();
7214
+ if (unsupported.length > 0) {
7215
+ throw new GenerationOutputManifestError(`${label} contains unsupported field: ${unsupported[0]}`);
7216
+ }
7217
+ }
7218
+ function nonEmptyText(value, label) {
7219
+ if (typeof value !== "string" || !value.trim()) throw new GenerationOutputManifestError(`${label} is required`);
7220
+ return value.trim();
7221
+ }
7222
+ function expectedGenerationOutputParents(job) {
7223
+ if (!Number.isInteger(job.expected_output_count) || job.expected_output_count <= 0) {
7224
+ throw new GenerationOutputManifestError("Generation job expected output count must be a positive integer");
7225
+ }
7226
+ const parents = job.inputs.filter((input) => input.role === "lineage_next_base").sort((left, right) => left.position - right.position);
7227
+ if (parents.length === 0) throw new GenerationOutputManifestError("Generation job has no lineage_next_base input");
7228
+ const parentIds = /* @__PURE__ */ new Set();
7229
+ const parentPositions = /* @__PURE__ */ new Set();
7230
+ for (const parent of parents) {
7231
+ if (typeof parent.asset_id !== "string" || !parent.asset_id || parent.asset_id.trim() !== parent.asset_id) {
7232
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent asset id");
7233
+ }
7234
+ if (!Number.isInteger(parent.position) || parent.position < 0 || parentPositions.has(parent.position)) {
7235
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent positions");
7236
+ }
7237
+ if (parentIds.has(parent.asset_id)) throw new GenerationOutputManifestError(`Generation job has duplicate lineage parent: ${parent.asset_id}`);
7238
+ parentIds.add(parent.asset_id);
7239
+ parentPositions.add(parent.position);
7240
+ }
7241
+ if (job.expected_output_count % parents.length !== 0) {
7242
+ throw new GenerationOutputManifestError("Generation job has invalid parent mapping");
7243
+ }
7244
+ const outputsPerParent = job.expected_output_count / parents.length;
7245
+ return Array.from({ length: job.expected_output_count }, (_value, outputIndex) => {
7246
+ return parents[Math.floor(outputIndex / outputsPerParent)].asset_id;
7247
+ });
7248
+ }
7249
+ function createGenerationOutputManifestDraft(job) {
7250
+ return {
7251
+ schema_version: generationOutputManifestSchemaVersion,
7252
+ job_id: job.id,
7253
+ outputs: expectedGenerationOutputParents(job).map((parentAssetId, outputIndex) => ({
7254
+ output_index: outputIndex,
7255
+ file_path: "",
7256
+ parent_asset_id: parentAssetId,
7257
+ edge_summary: ""
7258
+ }))
7259
+ };
7260
+ }
7261
+ function parseGenerationOutputManifest(value, job, options) {
7262
+ if (!options || typeof options.resolveFilePath !== "function") {
7263
+ throw new GenerationOutputManifestError("Generation output manifest requires a file-path resolver");
7264
+ }
7265
+ const manifest = record(value, "Generation output manifest");
7266
+ exactKeys(manifest, ["schema_version", "job_id", "outputs"], "Generation output manifest");
7267
+ if (manifest.schema_version !== generationOutputManifestSchemaVersion) {
7268
+ throw new GenerationOutputManifestError(`Generation output manifest schema_version must be ${generationOutputManifestSchemaVersion}`);
7269
+ }
7270
+ if (manifest.job_id !== job.id) throw new GenerationOutputManifestError(`Generation output manifest job_id must be ${job.id}`);
7271
+ if (!Array.isArray(manifest.outputs)) throw new GenerationOutputManifestError("Generation output manifest outputs must be an array");
7272
+ const expectedParents = expectedGenerationOutputParents(job);
7273
+ if (manifest.outputs.length !== expectedParents.length) {
7274
+ throw new GenerationOutputManifestError(`Generation output manifest requires ${expectedParents.length} outputs, received ${manifest.outputs.length}`);
7275
+ }
7276
+ const seenIndexes = /* @__PURE__ */ new Set();
7277
+ const outputs = manifest.outputs.map((value2, position) => {
7278
+ const output = record(value2, `Generation output at position ${position}`);
7279
+ exactKeys(output, ["output_index", "file_path", "parent_asset_id", "edge_summary"], `Generation output at position ${position}`);
7280
+ const outputIndex = output.output_index;
7281
+ if (!Number.isInteger(outputIndex) || Number(outputIndex) < 0) {
7282
+ throw new GenerationOutputManifestError(`Generation output at position ${position} requires a non-negative integer output_index`);
7283
+ }
7284
+ const normalizedIndex = Number(outputIndex);
7285
+ if (seenIndexes.has(normalizedIndex)) throw new GenerationOutputManifestError(`Duplicate generation output_index: ${normalizedIndex}`);
7286
+ seenIndexes.add(normalizedIndex);
7287
+ const expectedParent = expectedParents[normalizedIndex];
7288
+ if (!expectedParent) throw new GenerationOutputManifestError(`Unknown generation output_index: ${normalizedIndex}`);
7289
+ const filePath = nonEmptyText(output.file_path, `Generation output ${normalizedIndex} file_path`);
7290
+ const parentAssetId = nonEmptyText(output.parent_asset_id, `Generation output ${normalizedIndex} parent_asset_id`);
7291
+ if (parentAssetId !== expectedParent) {
7292
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex} must use parent_asset_id ${expectedParent}`);
7293
+ }
7294
+ let edgeSummary;
7295
+ try {
7296
+ edgeSummary = requireEdgeSummary(output.edge_summary);
7297
+ } catch (error) {
7298
+ if (error instanceof EdgeSummaryValidationError) {
7299
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex}: ${error.message}`);
7300
+ }
7301
+ throw error;
7302
+ }
7303
+ return {
7304
+ output_index: normalizedIndex,
7305
+ file_path: filePath,
7306
+ parent_asset_id: parentAssetId,
7307
+ edge_summary: edgeSummary
7308
+ };
7309
+ });
7310
+ for (const outputIndex of expectedParents.keys()) {
7311
+ if (!seenIndexes.has(outputIndex)) throw new GenerationOutputManifestError(`Missing generation output_index: ${outputIndex}`);
7312
+ }
7313
+ outputs.sort((left, right) => left.output_index - right.output_index);
7314
+ const seenPaths = /* @__PURE__ */ new Set();
7315
+ const resolvedOutputs = outputs.map((output) => {
7316
+ const filePath = nonEmptyText(options.resolveFilePath(output.file_path), `Generation output ${output.output_index} resolved file_path`);
7317
+ if (seenPaths.has(filePath)) throw new GenerationOutputManifestError(`Duplicate generation output file_path: ${filePath}`);
7318
+ seenPaths.add(filePath);
7319
+ return { ...output, file_path: filePath };
7320
+ });
7321
+ return {
7322
+ schema_version: generationOutputManifestSchemaVersion,
7323
+ job_id: job.id,
7324
+ outputs: resolvedOutputs
7325
+ };
7326
+ }
7327
+
7328
+ // src/server/generationReceipts.ts
7329
+ var legacyAdapterVersion = "generation-receipts-v1";
7330
+ var manifestAdapterVersion = "generation-receipts-v2";
6998
7331
  var provider = "codex-handoff";
6999
7332
  var GenerationReceiptError = class extends Error {
7000
7333
  constructor(message, status = 400) {
@@ -7016,6 +7349,70 @@ function parseJson(value, fallback) {
7016
7349
  if (!value) return fallback;
7017
7350
  return JSON.parse(value);
7018
7351
  }
7352
+ function positiveInteger(value) {
7353
+ return Number.isInteger(value) && Number(value) > 0;
7354
+ }
7355
+ function resolveLineageSelection(project) {
7356
+ const rootAssetId = activeLineageWorkspaceRoot(project);
7357
+ if (!rootAssetId) throw new GenerationReceiptError("No active lineage workspace for generation planning");
7358
+ const next = getLineageNextAsset(project, rootAssetId);
7359
+ if (!next.next_asset) throw new GenerationReceiptError(`No clear lineage next asset: ${next.reason}`);
7360
+ if (next.strategy !== "selected") throw new GenerationReceiptError("Generation v1 requires an explicit selected lineage next base");
7361
+ if (next.selection_mode !== "multiple" && next.selection?.asset_id !== next.next_asset.asset_id) throw new GenerationReceiptError("Generation v1 requires one explicit selected lineage next base");
7362
+ return next;
7363
+ }
7364
+ function selectedParents(next) {
7365
+ const parents = next.next_assets.length > 0 ? next.next_assets : next.next_asset ? [next.next_asset] : [];
7366
+ if (parents.length === 0) throw new GenerationReceiptError("Missing lineage next base");
7367
+ return parents;
7368
+ }
7369
+ function parentMappings(next, perBaseCount) {
7370
+ return selectedParents(next).map((parent, parentIndex) => ({
7371
+ parent,
7372
+ output_indexes: Array.from({ length: perBaseCount }, (_value, index) => parentIndex * perBaseCount + index)
7373
+ }));
7374
+ }
7375
+ function buildHandoff(project, id, prompt, count, perBaseCount, next, inputs) {
7376
+ const parent = next.next_asset;
7377
+ if (!parent) throw new GenerationReceiptError("Missing lineage next base");
7378
+ const parents = parentMappings(next, perBaseCount);
7379
+ const outputManifest = createGenerationOutputManifestDraft({ id, expected_output_count: count, inputs });
7380
+ const importCommand = `${lineagePublicPackageCommand()} generate image import --project ${quote(project)} --job-id ${quote(id)} --manifest ${quote(".asset-scratch/generation-output-manifest.json")} --confirm-write ${lineageRuntimeSelector()} --json`;
7381
+ return {
7382
+ schema_version: "lineage.generation_handoff.v2",
7383
+ provider,
7384
+ project,
7385
+ job_id: id,
7386
+ prompt,
7387
+ expected_output_count: count,
7388
+ per_base_count: next.selection_mode === "multiple" ? perBaseCount : void 0,
7389
+ lineage: {
7390
+ root_asset_id: next.root_asset_id,
7391
+ parent_asset_id: parent.asset_id,
7392
+ selection_strategy: next.strategy,
7393
+ parent_title: parent.title,
7394
+ parent_local_path: parent.local_path,
7395
+ parent_s3_key: parent.s3_key,
7396
+ parents: parents.length > 1 ? parents.map((mapping) => ({
7397
+ parent_asset_id: mapping.parent.asset_id,
7398
+ parent_title: mapping.parent.title,
7399
+ parent_local_path: mapping.parent.local_path,
7400
+ parent_s3_key: mapping.parent.s3_key,
7401
+ output_indexes: mapping.output_indexes
7402
+ })) : void 0
7403
+ },
7404
+ instructions: [
7405
+ "Use Codex image generation outside Lineage server code.",
7406
+ "Write generated output files under .asset-scratch before import.",
7407
+ "Do not call live provider APIs from the CLI or server.",
7408
+ "Fill every output_manifest entry with its generated file path and a one- or two-word edge summary.",
7409
+ "Import the completed manifest with --confirm-write to persist every lineage child."
7410
+ ],
7411
+ import_command: importCommand,
7412
+ output_manifest: outputManifest,
7413
+ guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
7414
+ };
7415
+ }
7019
7416
  function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
7020
7417
  const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
7021
7418
  return {
@@ -7044,6 +7441,19 @@ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
7044
7441
  guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
7045
7442
  };
7046
7443
  }
7444
+ function inputsFrom(jobIdValue, project, next) {
7445
+ return selectedParents(next).map((parent, position) => ({
7446
+ id: `${jobIdValue}:input:${position}`,
7447
+ job_id: jobIdValue,
7448
+ project_id: project,
7449
+ asset_id: parent.asset_id,
7450
+ root_asset_id: next.root_asset_id,
7451
+ role: "lineage_next_base",
7452
+ position,
7453
+ selection_strategy: next.strategy,
7454
+ selection_snapshot: next
7455
+ }));
7456
+ }
7047
7457
  function receiptFrom(row) {
7048
7458
  return {
7049
7459
  id: String(row.id),
@@ -7056,6 +7466,7 @@ function receiptFrom(row) {
7056
7466
  };
7057
7467
  }
7058
7468
  function outputFrom(row) {
7469
+ const edgeSummary = typeof row.edge_summary === "string" && row.edge_summary.length > 0 ? row.edge_summary : void 0;
7059
7470
  return {
7060
7471
  id: String(row.id),
7061
7472
  job_id: String(row.job_id),
@@ -7067,7 +7478,8 @@ function outputFrom(row) {
7067
7478
  content_type: String(row.content_type),
7068
7479
  imported_asset_id: String(row.imported_asset_id),
7069
7480
  parent_asset_id: String(row.parent_asset_id),
7070
- imported_at: String(row.imported_at)
7481
+ imported_at: String(row.imported_at),
7482
+ ...edgeSummary ? { edge_summary: edgeSummary } : {}
7071
7483
  };
7072
7484
  }
7073
7485
  function loadGenerationJob(database, project, id) {
@@ -7113,6 +7525,72 @@ function insertReceipt(database, id, type, command, payload) {
7113
7525
  values (?, ?, ?, 'ok', ?, ?, ?)
7114
7526
  `).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
7115
7527
  }
7528
+ function planImageGeneration(project = defaultProject, fields) {
7529
+ const prompt = fields.prompt.trim();
7530
+ if (!prompt) throw new GenerationReceiptError("Missing --prompt");
7531
+ if (!fields.fromLineageSelection) throw new GenerationReceiptError("Generation v1 requires --from-lineage-selection");
7532
+ const next = resolveLineageSelection(project);
7533
+ const parentCount = selectedParents(next).length;
7534
+ if (parentCount > 1 && !positiveInteger(fields.perBaseCount)) throw new GenerationReceiptError("Multi-parent generation requires --per-base-count");
7535
+ const perBaseCount = parentCount > 1 ? Number(fields.perBaseCount) : Number(fields.count ?? fields.perBaseCount);
7536
+ const count = parentCount * perBaseCount;
7537
+ if (!positiveInteger(perBaseCount)) throw new GenerationReceiptError("Generation count must be a positive integer");
7538
+ if (fields.count !== void 0 && fields.count !== count) throw new GenerationReceiptError(`Generation count mismatch: expected ${count} from selected bases, received ${fields.count}`);
7539
+ const id = jobId();
7540
+ const inputs = inputsFrom(id, project, next);
7541
+ const handoff3 = buildHandoff(project, id, prompt, count, perBaseCount, next, inputs);
7542
+ const mappings = parentMappings(next, perBaseCount).map((mapping) => ({ parent_asset_id: mapping.parent.asset_id, output_indexes: mapping.output_indexes }));
7543
+ const timestamp = nowIso();
7544
+ const preview = {
7545
+ id,
7546
+ project_id: project,
7547
+ provider,
7548
+ adapter_version: manifestAdapterVersion,
7549
+ source_mode: "lineage_selection",
7550
+ root_asset_id: next.root_asset_id,
7551
+ prompt,
7552
+ expected_output_count: count,
7553
+ status: "planned",
7554
+ output_dir: ".asset-scratch",
7555
+ handoff: handoff3,
7556
+ created_at: timestamp,
7557
+ updated_at: timestamp,
7558
+ inputs,
7559
+ outputs: [],
7560
+ receipts: [{
7561
+ id: `${id}:receipt:plan:preview`,
7562
+ job_id: id,
7563
+ receipt_type: "plan",
7564
+ status: "ok",
7565
+ command: "generate image plan",
7566
+ payload: { prompt, expected_output_count: count, per_base_count: parentCount > 1 ? perBaseCount : void 0, lineage: handoff3.lineage, parent_mappings: mappings },
7567
+ created_at: timestamp
7568
+ }]
7569
+ };
7570
+ if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview };
7571
+ const database = lineageDb();
7572
+ try {
7573
+ database.exec("BEGIN IMMEDIATE");
7574
+ try {
7575
+ database.prepare(`
7576
+ insert into generation_jobs (
7577
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
7578
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at
7579
+ ) values (?, ?, ?, ?, 'lineage_selection', ?, ?, ?, 'planned', ?, ?, ?, ?)
7580
+ `).run(id, project, provider, manifestAdapterVersion, next.root_asset_id, prompt, count, ".asset-scratch", JSON.stringify(handoff3), timestamp, timestamp);
7581
+ const insertInput = database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)");
7582
+ for (const input of inputs) insertInput.run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(next));
7583
+ insertReceipt(database, id, "plan", "generate image plan", preview.receipts[0].payload);
7584
+ database.exec("COMMIT");
7585
+ } catch (error) {
7586
+ database.exec("ROLLBACK");
7587
+ throw error;
7588
+ }
7589
+ return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
7590
+ } finally {
7591
+ database.close();
7592
+ }
7593
+ }
7116
7594
  function planImageReroll(project = defaultProject, fields) {
7117
7595
  const prompt = fields.prompt.trim();
7118
7596
  if (!prompt) throw new GenerationReceiptError("Missing --prompt");
@@ -7157,7 +7635,7 @@ function planImageReroll(project = defaultProject, fields) {
7157
7635
  id,
7158
7636
  project_id: project,
7159
7637
  provider,
7160
- adapter_version: adapterVersion,
7638
+ adapter_version: legacyAdapterVersion,
7161
7639
  source_mode: "lineage_reroll",
7162
7640
  root_asset_id: snapshot.root_asset_id,
7163
7641
  prompt,
@@ -7189,7 +7667,7 @@ function planImageReroll(project = defaultProject, fields) {
7189
7667
  id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
7190
7668
  expected_output_count, status, output_dir, handoff_json, created_at, updated_at
7191
7669
  ) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
7192
- `).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff3), timestamp, timestamp);
7670
+ `).run(id, project, provider, legacyAdapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff3), timestamp, timestamp);
7193
7671
  database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(input.selection_snapshot));
7194
7672
  insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
7195
7673
  database.exec("COMMIT");
@@ -7202,14 +7680,66 @@ function planImageReroll(project = defaultProject, fields) {
7202
7680
  database.close();
7203
7681
  }
7204
7682
  }
7683
+ function parentForOutput(job, outputIndex) {
7684
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
7685
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
7686
+ if (inputs.length === 1) return inputs[0].asset_id;
7687
+ if (job.expected_output_count % inputs.length !== 0) throw new GenerationReceiptError("Generation job has invalid parent mapping");
7688
+ return inputs[Math.floor(outputIndex / (job.expected_output_count / inputs.length))]?.asset_id || inputs[inputs.length - 1].asset_id;
7689
+ }
7690
+ function parentInputs(job) {
7691
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
7692
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
7693
+ return inputs;
7694
+ }
7695
+ function parentFilesFor(job, parentFiles) {
7696
+ const inputs = parentInputs(job);
7697
+ const expectedPerParent = job.expected_output_count / inputs.length;
7698
+ if (!Number.isInteger(expectedPerParent)) throw new GenerationReceiptError("Generation job has invalid parent mapping");
7699
+ const allowedParents = new Set(inputs.map((input) => input.asset_id));
7700
+ const seenParents = /* @__PURE__ */ new Set();
7701
+ const mapped = [];
7702
+ for (const parentAssetId of Object.keys(parentFiles)) {
7703
+ if (!allowedParents.has(parentAssetId)) throw new GenerationReceiptError(`Unknown generation parent mapping: ${parentAssetId}`);
7704
+ if (seenParents.has(parentAssetId)) throw new GenerationReceiptError(`Duplicate generation parent mapping: ${parentAssetId}`);
7705
+ seenParents.add(parentAssetId);
7706
+ }
7707
+ for (const input of inputs) {
7708
+ const files = (parentFiles[input.asset_id] || []).map((file) => file.trim()).filter(Boolean);
7709
+ if (files.length === 0) throw new GenerationReceiptError(`Missing generation parent mapping for ${input.asset_id}`);
7710
+ if (files.length !== expectedPerParent) throw new GenerationReceiptError(`Parent ${input.asset_id} requires ${expectedPerParent} output file${expectedPerParent === 1 ? "" : "s"}, received ${files.length}`);
7711
+ for (const file of files) mapped.push({ file, parentAssetId: input.asset_id });
7712
+ }
7713
+ return mapped;
7714
+ }
7715
+ function orderedFilesFor(job, files) {
7716
+ return files.map((file, index) => ({ file, parentAssetId: parentForOutput(job, index) }));
7717
+ }
7718
+ function inspectImageGeneration(project = defaultProject, jobIdValue) {
7719
+ if (!jobIdValue) throw new GenerationReceiptError("Missing --job-id");
7720
+ const database = lineageDb();
7721
+ try {
7722
+ return { ok: true, command: "generate image inspect", project, job: loadGenerationJob(database, project, jobIdValue) };
7723
+ } finally {
7724
+ database.close();
7725
+ }
7726
+ }
7205
7727
  function isPathInside2(child, parent) {
7206
7728
  const rel = relative4(parent, child);
7207
7729
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
7208
7730
  }
7209
- function resolveScratchFile(file) {
7731
+ function scratchCandidate(file) {
7210
7732
  const scratchRoot = resolve5(repoRoot, ".asset-scratch");
7211
7733
  const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
7212
7734
  if (!isPathInside2(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
7735
+ return { candidate, scratchRoot };
7736
+ }
7737
+ function resolveScratchManifestPath(file) {
7738
+ const { candidate, scratchRoot } = scratchCandidate(file);
7739
+ return relative4(scratchRoot, candidate);
7740
+ }
7741
+ function resolveScratchFile(file) {
7742
+ const { candidate, scratchRoot } = scratchCandidate(file);
7213
7743
  if (!existsSync9(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
7214
7744
  const realScratchRoot = realpathSync2(scratchRoot);
7215
7745
  const realCandidate = realpathSync2(candidate);
@@ -7225,6 +7755,165 @@ function resolveScratchFile(file) {
7225
7755
  assetId: `local-${checksum.slice(0, 12)}`
7226
7756
  };
7227
7757
  }
7758
+ function generationManifestConflict() {
7759
+ throw new GenerationReceiptError("Generation import already exists with different output, summary, or provenance", 409);
7760
+ }
7761
+ function confirmManifestRetry(project, job, manifest) {
7762
+ if (job.outputs.length !== manifest.outputs.length) generationManifestConflict();
7763
+ const database = lineageDb();
7764
+ try {
7765
+ for (const expected of manifest.outputs) {
7766
+ const recorded = job.outputs.find((output) => output.output_index === expected.output_index);
7767
+ if (!recorded || recorded.file_path !== expected.file_path || recorded.parent_asset_id !== expected.parent_asset_id || recorded.edge_summary !== expected.edge_summary) generationManifestConflict();
7768
+ const edge = database.prepare(`
7769
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
7770
+ from asset_edges
7771
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
7772
+ `).get(project, recorded.parent_asset_id, recorded.imported_asset_id);
7773
+ if (!edge || edge.summary !== expected.edge_summary || edge.summary_created_by !== "agent" || edge.summary_updated_by !== "agent" || typeof edge.summary_updated_at !== "string" || edge.summary_updated_at.length === 0) generationManifestConflict();
7774
+ }
7775
+ return { ok: true, command: "generate image import", project, job, imported: job.outputs, idempotent: true };
7776
+ } finally {
7777
+ database.close();
7778
+ }
7779
+ }
7780
+ function importImageGenerationOutputs(project = defaultProject, fields) {
7781
+ if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
7782
+ if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
7783
+ const database = lineageDb();
7784
+ let job;
7785
+ try {
7786
+ job = loadGenerationJob(database, project, fields.jobId);
7787
+ } finally {
7788
+ database.close();
7789
+ }
7790
+ if (job.source_mode !== "lineage_selection") throw new GenerationReceiptError(`Generation job is not an image-selection job: ${job.source_mode}`);
7791
+ const hasManifestInput = fields.manifest !== void 0;
7792
+ const hasParentFilesInput = fields.parentFiles !== void 0;
7793
+ const hasFilesInput = fields.files !== void 0;
7794
+ if (hasManifestInput && (hasParentFilesInput || hasFilesInput)) {
7795
+ throw new GenerationReceiptError("Use --manifest or legacy --files/--parent-files, not both");
7796
+ }
7797
+ if (hasParentFilesInput && hasFilesInput) throw new GenerationReceiptError("Use --files or --parent-files, not both");
7798
+ let manifest;
7799
+ let parentFileRows;
7800
+ let mappingStrategy;
7801
+ if (job.adapter_version === manifestAdapterVersion) {
7802
+ if (!hasManifestInput) throw new GenerationReceiptError("New generation jobs require --manifest");
7803
+ manifest = parseGenerationOutputManifest(fields.manifest, job, { resolveFilePath: resolveScratchManifestPath });
7804
+ if (job.status === "imported") return confirmManifestRetry(project, job, manifest);
7805
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
7806
+ parentFileRows = manifest.outputs.map((output) => ({
7807
+ edgeSummary: output.edge_summary,
7808
+ file: output.file_path,
7809
+ parentAssetId: output.parent_asset_id
7810
+ }));
7811
+ mappingStrategy = "generation_output_manifest_v1";
7812
+ } else if (job.adapter_version === legacyAdapterVersion) {
7813
+ if (hasManifestInput) throw new GenerationReceiptError("Already-planned legacy generation jobs require --files or --parent-files");
7814
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
7815
+ const hasExplicitParentFiles = Boolean(fields.parentFiles && Object.keys(fields.parentFiles).length > 0);
7816
+ parentFileRows = hasExplicitParentFiles ? parentFilesFor(job, fields.parentFiles || {}) : orderedFilesFor(job, (fields.files || []).map((file) => file.trim()).filter(Boolean));
7817
+ mappingStrategy = hasExplicitParentFiles ? "explicit_parent_files" : "ordered_per_base";
7818
+ } else {
7819
+ throw new GenerationReceiptError(`Unsupported generation adapter version: ${job.adapter_version}`);
7820
+ }
7821
+ if (parentFileRows.length === 0) throw new GenerationReceiptError("Generation import requires --files or --parent-files");
7822
+ if (parentFileRows.length !== job.expected_output_count) {
7823
+ throw new GenerationReceiptError(`Output count mismatch: expected ${job.expected_output_count}, received ${parentFileRows.length}`);
7824
+ }
7825
+ const resolved = parentFileRows.map((row) => ({ ...resolveScratchFile(row.file), edgeSummary: row.edgeSummary, parentAssetId: row.parentAssetId }));
7826
+ const uniquePaths = new Set(resolved.map((file) => file.relativePath));
7827
+ if (uniquePaths.size !== resolved.length) throw new GenerationReceiptError("Generation import files must be unique");
7828
+ cancelLineageIterateTasksForAssets(project, {
7829
+ actor: "system",
7830
+ confirmWrite: false,
7831
+ rootAssetId: job.root_asset_id
7832
+ });
7833
+ indexLineageAssets(project);
7834
+ const writeDb = lineageDb();
7835
+ try {
7836
+ const timestamp = nowIso();
7837
+ writeDb.exec("BEGIN IMMEDIATE");
7838
+ try {
7839
+ for (const [index, file] of resolved.entries()) {
7840
+ const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, file.assetId);
7841
+ if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${file.relativePath}`);
7842
+ const outputId = `${fields.jobId}:output:${index}`;
7843
+ writeDb.prepare(`insert into generation_job_outputs (
7844
+ id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type,
7845
+ imported_asset_id, parent_asset_id, imported_at, edge_summary
7846
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
7847
+ outputId,
7848
+ fields.jobId,
7849
+ project,
7850
+ index,
7851
+ file.relativePath,
7852
+ file.checksum,
7853
+ file.size,
7854
+ file.contentType,
7855
+ file.assetId,
7856
+ file.parentAssetId,
7857
+ timestamp,
7858
+ file.edgeSummary || null
7859
+ );
7860
+ const insertedEdge = writeDb.prepare(`insert into asset_edges (
7861
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
7862
+ summary, summary_created_by, summary_updated_by, summary_updated_at
7863
+ ) values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
7864
+ on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing`).run(
7865
+ `${project}:${file.parentAssetId}:derived_from:${file.assetId}`,
7866
+ project,
7867
+ file.parentAssetId,
7868
+ file.assetId,
7869
+ timestamp,
7870
+ file.edgeSummary || null,
7871
+ file.edgeSummary ? "agent" : null,
7872
+ file.edgeSummary ? "agent" : null,
7873
+ file.edgeSummary ? timestamp : null
7874
+ );
7875
+ if (file.edgeSummary && insertedEdge.changes === 0) {
7876
+ const edge = writeDb.prepare(`
7877
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
7878
+ from asset_edges
7879
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
7880
+ `).get(project, file.parentAssetId, file.assetId);
7881
+ if (!edge || edge.summary !== file.edgeSummary || edge.summary_created_by !== "agent" || edge.summary_updated_by !== "agent" || typeof edge.summary_updated_at !== "string" || edge.summary_updated_at.length === 0) generationManifestConflict();
7882
+ }
7883
+ }
7884
+ writeDb.prepare(`
7885
+ update generation_jobs
7886
+ set status = 'imported', imported_at = ?, updated_at = ?
7887
+ where project_id = ? and id = ?
7888
+ `).run(timestamp, timestamp, project, fields.jobId);
7889
+ insertReceipt(writeDb, fields.jobId, "import", "generate image import", {
7890
+ mapping_strategy: mappingStrategy,
7891
+ files: resolved.map((file, index) => ({
7892
+ output_index: index,
7893
+ file_path: file.relativePath,
7894
+ imported_asset_id: file.assetId,
7895
+ parent_asset_id: file.parentAssetId,
7896
+ ...file.edgeSummary ? { edge_summary: file.edgeSummary } : {}
7897
+ })),
7898
+ selection_reset: { root_asset_id: job.root_asset_id, cleared: true }
7899
+ });
7900
+ writeDb.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, job.root_asset_id);
7901
+ writeDb.exec("COMMIT");
7902
+ } catch (error) {
7903
+ writeDb.exec("ROLLBACK");
7904
+ throw error;
7905
+ }
7906
+ cancelLineageIterateTasksForAssets(project, {
7907
+ actor: "system",
7908
+ confirmWrite: true,
7909
+ rootAssetId: job.root_asset_id
7910
+ });
7911
+ const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
7912
+ return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
7913
+ } finally {
7914
+ writeDb.close();
7915
+ }
7916
+ }
7228
7917
  function importImageRerollOutput(project = defaultProject, fields) {
7229
7918
  if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
7230
7919
  if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
@@ -8158,7 +8847,7 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
8158
8847
  import { createHash as createHash6 } from "node:crypto";
8159
8848
  import { spawnSync as spawnSync2 } from "node:child_process";
8160
8849
  import { createRequire as createRequire4 } from "node:module";
8161
- import { existsSync as existsSync11, lstatSync as lstatSync2, readFileSync as readFileSync7, readdirSync as readdirSync4, readlinkSync, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
8850
+ import { existsSync as existsSync11, lstatSync as lstatSync3, readFileSync as readFileSync7, readdirSync as readdirSync4, readlinkSync, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
8162
8851
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join12, resolve as resolve6 } from "node:path";
8163
8852
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8164
8853
 
@@ -8224,7 +8913,7 @@ function untrackedFingerprint(root, status) {
8224
8913
  hash.update(relativePath);
8225
8914
  const path = join12(root, relativePath);
8226
8915
  try {
8227
- const stat = lstatSync2(path);
8916
+ const stat = lstatSync3(path);
8228
8917
  if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
8229
8918
  else if (stat.isFile()) hash.update(readFileSync7(path));
8230
8919
  else hash.update(`[${stat.mode}:${stat.size}]`);
@@ -9086,6 +9775,26 @@ function readOptions(args, name) {
9086
9775
  }
9087
9776
  return values;
9088
9777
  }
9778
+ function readJsonFile(path, label) {
9779
+ try {
9780
+ return JSON.parse(readFileSync8(resolve8(path), "utf8"));
9781
+ } catch (error) {
9782
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
9783
+ }
9784
+ }
9785
+ function parentFilesOption(value) {
9786
+ if (value === void 0) return void 0;
9787
+ const mappings = {};
9788
+ for (const rawMapping of value.split(";")) {
9789
+ const separator = rawMapping.indexOf("=");
9790
+ const parent = separator >= 0 ? rawMapping.slice(0, separator).trim() : "";
9791
+ const files = separator >= 0 ? rawMapping.slice(separator + 1).split(",").map((file) => file.trim()).filter(Boolean) : [];
9792
+ if (!parent || files.length === 0) throw new Error("lineage generate image import --parent-files requires parent=file[,file][;parent=file]");
9793
+ if (Object.hasOwn(mappings, parent)) throw new Error(`Duplicate generation parent mapping: ${parent}`);
9794
+ mappings[parent] = files;
9795
+ }
9796
+ return mappings;
9797
+ }
9089
9798
  function resolveCliAssetRoot(args) {
9090
9799
  return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
9091
9800
  }
@@ -9117,7 +9826,8 @@ function resolveDataCommandOptions(args) {
9117
9826
  dbPath: readOption(args, "--db"),
9118
9827
  json: args.includes("--json"),
9119
9828
  project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
9120
- rootAssetId: readOption(args, "--root")
9829
+ rootAssetId: readOption(args, "--root"),
9830
+ summary: readOption(args, "--summary")
9121
9831
  };
9122
9832
  if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
9123
9833
  return options;
@@ -9160,13 +9870,57 @@ function runLineageDataCommand(command, args) {
9160
9870
  }
9161
9871
  if (command === "link-child") {
9162
9872
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
9873
+ const summary = requireEdgeSummary(options.summary);
9163
9874
  return linkSelectedLineageChild(options.project, {
9164
9875
  childAssetId: options.childAssetId,
9165
9876
  claimToken: options.claimToken,
9166
9877
  confirmWrite: options.confirmWrite,
9167
- rootAssetId: options.rootAssetId || options.assetId
9878
+ rootAssetId: options.rootAssetId || options.assetId,
9879
+ summary,
9880
+ summaryActor: "agent"
9168
9881
  });
9169
9882
  }
9883
+ if (command === "generate") {
9884
+ const [resource, subcommand] = positionalArgs(args);
9885
+ if (resource !== "image") throw new Error(`Unknown generate command: ${resource}`);
9886
+ if (subcommand === "plan") {
9887
+ const prompt = readOption(args, "--prompt");
9888
+ const rawCount = readOption(args, "--count");
9889
+ const rawPerBaseCount = readOption(args, "--per-base-count");
9890
+ if (!prompt) throw new Error("lineage generate image plan requires --prompt");
9891
+ return planImageGeneration(options.project, {
9892
+ count: rawCount === void 0 ? void 0 : Number(rawCount),
9893
+ dryRun: args.includes("--dry-run"),
9894
+ fromLineageSelection: args.includes("--from-lineage-selection"),
9895
+ perBaseCount: rawPerBaseCount === void 0 ? void 0 : Number(rawPerBaseCount),
9896
+ prompt
9897
+ });
9898
+ }
9899
+ if (subcommand === "inspect") {
9900
+ const jobId2 = readOption(args, "--job-id");
9901
+ if (!jobId2) throw new Error("lineage generate image inspect requires --job-id");
9902
+ return inspectImageGeneration(options.project, jobId2);
9903
+ }
9904
+ if (subcommand === "import") {
9905
+ const jobId2 = readOption(args, "--job-id");
9906
+ const manifestPath = readOption(args, "--manifest");
9907
+ const filesValue = readOption(args, "--files");
9908
+ const parentFilesValue = readOption(args, "--parent-files");
9909
+ if (!jobId2) throw new Error("lineage generate image import requires --job-id");
9910
+ if (manifestPath !== void 0 && (filesValue !== void 0 || parentFilesValue !== void 0)) {
9911
+ throw new Error("Use --manifest or legacy --files/--parent-files, not both");
9912
+ }
9913
+ if (filesValue !== void 0 && parentFilesValue !== void 0) throw new Error("Use --files or --parent-files, not both");
9914
+ return importImageGenerationOutputs(options.project, {
9915
+ confirmWrite: options.confirmWrite,
9916
+ files: filesValue === void 0 ? void 0 : filesValue.split(",").map((file) => file.trim()).filter(Boolean),
9917
+ jobId: jobId2,
9918
+ manifest: manifestPath === void 0 ? void 0 : readJsonFile(manifestPath, "Generation output manifest"),
9919
+ parentFiles: parentFilesOption(parentFilesValue)
9920
+ });
9921
+ }
9922
+ throw new Error(`Unknown generate image command: ${subcommand}`);
9923
+ }
9170
9924
  if (command === "reroll") {
9171
9925
  const subcommand = positionalArgs(args)[0] || "";
9172
9926
  if (subcommand === "list") {
@@ -9285,8 +10039,10 @@ function rerollRequestedBy(value) {
9285
10039
  throw new Error(`Invalid re-roll requester: ${value}`);
9286
10040
  }
9287
10041
  function lineageCliCanDelegateMutation(command, args) {
9288
- const subcommand = positionalArgs(args)[0] || "";
10042
+ const positions = positionalArgs(args);
10043
+ const subcommand = positions[0] || "";
9289
10044
  if (command === "link-child") return true;
10045
+ if (command === "generate") return positions[0] === "image" && ["plan", "import"].includes(positions[1] || "");
9290
10046
  if (command === "reroll") return ["mark", "cancel", "plan", "import"].includes(subcommand);
9291
10047
  if (command === "tasks") return ["claim", "start", "comment", "cancel", "override", "instructions"].includes(subcommand);
9292
10048
  if (command === "agent") return ["claim", "heartbeat", "release", "revoke", "transfer"].includes(subcommand);
@@ -9562,6 +10318,16 @@ app.post(
9562
10318
  );
9563
10319
  })
9564
10320
  );
10321
+ app.post("/api/lineage/edges/:edgeId/summary", asyncRoute((req, res) => {
10322
+ res.json(updateLineageEdgeSummary(projectFrom(req), {
10323
+ edgeId: req.params.edgeId,
10324
+ action: req.body.action,
10325
+ summary: req.body.summary,
10326
+ expectedSummaryUpdatedAt: req.body.expectedSummaryUpdatedAt,
10327
+ confirmWrite: req.body.confirmWrite === true,
10328
+ claimToken: claimTokenFromRequest(req)
10329
+ }));
10330
+ }));
9565
10331
  app.post("/api/lineage/remove-node", asyncRoute((req, res) => {
9566
10332
  res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true, claimToken: claimTokenFromRequest(req) }));
9567
10333
  }));