@mean-weasel/lineage 0.1.15 → 0.1.17

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +249 -19
  3. package/dist/cli/lineage-channel.js +25 -5
  4. package/dist/cli/lineage-channel.js.map +2 -2
  5. package/dist/cli/lineage-dev.js +891 -52
  6. package/dist/cli/lineage-dev.js.map +4 -4
  7. package/dist/cli/lineage-preview.js +891 -52
  8. package/dist/cli/lineage-preview.js.map +4 -4
  9. package/dist/cli/lineage.js +891 -52
  10. package/dist/cli/lineage.js.map +4 -4
  11. package/dist/cli/managed-service.js +19 -5
  12. package/dist/cli/managed-service.js.map +2 -2
  13. package/dist/runtime-build.json +4 -4
  14. package/dist/server.js +883 -89
  15. package/dist/server.js.map +4 -4
  16. package/dist/web/assets/agent-shared-state-DOfpTLiw.webp +0 -0
  17. package/dist/web/assets/agent-to-canvas-DtIu_cPq.mp4 +0 -0
  18. package/dist/web/assets/{app-CDxSvoD3.css → app-B-fAyNsU.css} +1 -1
  19. package/dist/web/assets/app-DMogkUoS.js +16 -0
  20. package/dist/web/assets/attempt-history-D6wdic6n.webp +0 -0
  21. package/dist/web/assets/branching-tree-B8I5_S6D.png +0 -0
  22. package/dist/web/assets/canvas-cli-DHBacq7r.png +0 -0
  23. package/dist/web/assets/hero-agent-sync-CSyh0tHy.mp4 +0 -0
  24. package/dist/web/assets/hero-board-DG3ED4AW.webp +0 -0
  25. package/dist/web/assets/hero-lineage-growth-Bsl9tiq-.mp4 +0 -0
  26. package/dist/web/assets/hero-trace-connections-Bb52ABBc.mp4 +0 -0
  27. package/dist/web/assets/human-selection-BdDBDE-I.webp +0 -0
  28. package/dist/web/assets/human-to-agent-D1z92ZO9.mp4 +0 -0
  29. package/dist/web/assets/{jsx-runtime-_Rdg6et1.js → jsx-runtime-DAFSxiwi.js} +1 -1
  30. package/dist/web/assets/landing-Bn4qKbIO.css +1 -0
  31. package/dist/web/assets/landing-CoyGmxBo.js +1 -0
  32. package/dist/web/assets/reroll-history-DQcEH42R.mp4 +0 -0
  33. package/dist/web/index.html +3 -3
  34. package/dist/web/landing/index.html +3 -3
  35. package/fixtures/demo-project/lineage/swissifier-rich-demo.json +13 -13
  36. package/package.json +4 -3
  37. package/dist/web/assets/app-CYUgf6rX.js +0 -16
  38. package/dist/web/assets/landing-D3Y679_B.css +0 -1
  39. package/dist/web/assets/landing-ii8AH_DG.js +0 -1
package/dist/server.js CHANGED
@@ -349,13 +349,13 @@ function profileManifestPath(selector) {
349
349
  if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
350
350
  return { manifestPath: join3(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
351
351
  }
352
- function requiredString(record, key) {
353
- const value = record[key];
352
+ function requiredString(record2, key) {
353
+ const value = record2[key];
354
354
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
355
355
  return value.trim();
356
356
  }
357
- function optionalString(record, key) {
358
- const value = record[key];
357
+ function optionalString(record2, key) {
358
+ const value = record2[key];
359
359
  if (value === void 0) return void 0;
360
360
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
361
361
  return value.trim();
@@ -406,15 +406,15 @@ function resolveLineageProfile(selector) {
406
406
  throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
407
407
  }
408
408
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
409
- const record = raw;
410
- const schemaVersion = requiredString(record, "schema_version");
409
+ const record2 = raw;
410
+ const schemaVersion = requiredString(record2, "schema_version");
411
411
  if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
412
- const profileId = requiredString(record, "profile_id");
412
+ const profileId = requiredString(record2, "profile_id");
413
413
  if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
414
414
  if (namedProfileId && namedProfileId !== profileId) {
415
415
  throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
416
416
  }
417
- const expectedRaw = record.expected_runtime;
417
+ const expectedRaw = record2.expected_runtime;
418
418
  if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
419
419
  throw new Error("Profile expected_runtime must be an object");
420
420
  }
@@ -423,18 +423,18 @@ function resolveLineageProfile(selector) {
423
423
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
424
424
  const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
425
425
  const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
426
- const migrationsRaw = record.required_schema_migrations;
426
+ const migrationsRaw = record2.required_schema_migrations;
427
427
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
428
428
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
429
429
  }
430
- const environment = validateEnvironment(requiredString(record, "environment"));
430
+ const environment = validateEnvironment(requiredString(record2, "environment"));
431
431
  const expectedChannel = validateChannel(expected?.channel);
432
432
  if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
433
433
  throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
434
434
  }
435
435
  const manifest = {
436
- asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
437
- 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")),
438
438
  environment,
439
439
  ...expected ? {
440
440
  expected_runtime: {
@@ -448,7 +448,7 @@ function resolveLineageProfile(selector) {
448
448
  profile_id: profileId,
449
449
  ...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
450
450
  schema_version: lineageProfileSchemaVersion,
451
- service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
451
+ service_origin: validateServiceOrigin(requiredString(record2, "service_origin"))
452
452
  };
453
453
  return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
454
454
  }
@@ -815,7 +815,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
815
815
  }
816
816
  function assertProfileWriterLeaseHeld() {
817
817
  if (!process.env.LINEAGE_PROFILE) {
818
- throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only");
818
+ throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only. Create a fresh profile with `profile init --profile <id> --confirm-write`, then pass that profile to the command");
819
819
  }
820
820
  const profileId = process.env.LINEAGE_PROFILE_ID;
821
821
  const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
@@ -919,6 +919,10 @@ function lineageDb() {
919
919
  child_asset_id text not null references assets(id),
920
920
  relation_type text not null check (relation_type in ('derived_from')),
921
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,
922
926
  unique (project_id, parent_asset_id, child_asset_id, relation_type)
923
927
  );
924
928
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
@@ -1202,6 +1206,7 @@ function lineageDb() {
1202
1206
  imported_asset_id text not null references assets(id),
1203
1207
  parent_asset_id text not null references assets(id),
1204
1208
  imported_at text not null,
1209
+ edge_summary text,
1205
1210
  unique(job_id, output_index),
1206
1211
  unique(job_id, file_path)
1207
1212
  );
@@ -1290,6 +1295,11 @@ function lineageDb() {
1290
1295
  );
1291
1296
  create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
1292
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");
1293
1303
  migrateAssetSelections(database);
1294
1304
  dropLegacyAssetSelectionRootUnique(database);
1295
1305
  ensureColumn(database, "asset_selections", "notes", "text");
@@ -1643,8 +1653,8 @@ function syncLedgerPlacement(project, assetId, placement) {
1643
1653
  function placeholders(values) {
1644
1654
  return values.map(() => "?").join(",");
1645
1655
  }
1646
- function assetIdsForRecord(record, sources) {
1647
- 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)])];
1648
1658
  }
1649
1659
  function reviewFromRow(row) {
1650
1660
  return {
@@ -1678,7 +1688,7 @@ function selectionFromRow(row) {
1678
1688
  };
1679
1689
  }
1680
1690
  function ledgerWorkflowStates(database, project, records, sources) {
1681
- 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))]));
1682
1692
  const assetIds = [...new Set([...recordAssets.values()].flat())];
1683
1693
  if (assetIds.length === 0) return {};
1684
1694
  const marker = placeholders(assetIds);
@@ -1697,11 +1707,11 @@ function ledgerWorkflowStates(database, project, records, sources) {
1697
1707
  from asset_selections
1698
1708
  where project_id = ? and asset_id in (${marker})
1699
1709
  `).all(project, ...assetIds);
1700
- return Object.fromEntries(records.map((record) => {
1701
- const ids = recordAssets.get(record.id) || [];
1710
+ return Object.fromEntries(records.map((record2) => {
1711
+ const ids = recordAssets.get(record2.id) || [];
1702
1712
  const review = reviews.find((row) => ids.includes(row.asset_id));
1703
1713
  const selection = selections.find((row) => ids.includes(row.asset_id));
1704
- return [record.id, {
1714
+ return [record2.id, {
1705
1715
  review: review ? reviewFromRow(review) : void 0,
1706
1716
  placements: placements.filter((row) => ids.includes(row.asset_id)).map(placementFromRow),
1707
1717
  selection: selection ? selectionFromRow(selection) : void 0
@@ -2154,6 +2164,32 @@ function lookupAssets(project, assetIds) {
2154
2164
  // src/server/assetLineage.ts
2155
2165
  import { join as join7 } from "node:path";
2156
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
+
2157
2193
  // src/server/assetLineageSelection.ts
2158
2194
  var LINEAGE_NEXT_VARIATION_LIMIT = 3;
2159
2195
  function selectionId(project, root, assetId) {
@@ -3584,6 +3620,41 @@ function rerollRequestId(project, root, node, timestamp) {
3584
3620
  function rowString(value) {
3585
3621
  return typeof value === "string" && value.length > 0 ? value : void 0;
3586
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
+ }
3587
3658
  function rerollRequestFrom(row) {
3588
3659
  return {
3589
3660
  id: String(row.id),
@@ -3683,6 +3754,9 @@ function localPreviewUrl(project, localPath) {
3683
3754
  return `/api/assets/local-preview?${params.toString()}`;
3684
3755
  }
3685
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");
3686
3760
  const database = lineageDb();
3687
3761
  requireAsset3(database, project, fields.parentAssetId);
3688
3762
  requireAsset3(database, project, fields.childAssetId);
@@ -3701,25 +3775,135 @@ function linkLineageAssets(project, fields) {
3701
3775
  database.close();
3702
3776
  throw error;
3703
3777
  }
3778
+ const createdAt = nowIso();
3704
3779
  const edge = {
3705
3780
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
3706
3781
  parent_asset_id: fields.parentAssetId,
3707
3782
  child_asset_id: fields.childAssetId,
3708
3783
  relation_type: "derived_from",
3709
- 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
+ } : {}
3710
3791
  };
3711
3792
  if (!fields.confirmWrite) {
3712
3793
  database.close();
3713
3794
  return { ok: true, dryRun: true, edge };
3714
3795
  }
3715
- database.prepare(`
3716
- insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
3717
- 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', ?, ?, ?, ?, ?)
3718
3802
  on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
3719
- `).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
+ }
3720
3838
  database.close();
3721
3839
  return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
3722
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
+ }
3723
3907
  function descendants(database, project, root) {
3724
3908
  const edges = [];
3725
3909
  const queue = [root];
@@ -3728,7 +3912,13 @@ function descendants(database, project, root) {
3728
3912
  const parent = queue.shift();
3729
3913
  if (seen.has(parent)) continue;
3730
3914
  seen.add(parent);
3731
- 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);
3732
3922
  edges.push(...rows);
3733
3923
  queue.push(...rows.map((row) => row.child_asset_id));
3734
3924
  }
@@ -4339,7 +4529,7 @@ function lineageCommand(command, project, rootAssetId) {
4339
4529
  return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
4340
4530
  }
4341
4531
  function linkChildCommand(project, rootAssetId) {
4342
- 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`;
4343
4533
  }
4344
4534
  function rerollImportGuidance(rootAssetId, targetAssetId) {
4345
4535
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -4388,6 +4578,7 @@ function getLineageBrief(project, rootAssetId) {
4388
4578
  };
4389
4579
  }
4390
4580
  function linkSelectedLineageChild(project, fields) {
4581
+ const summary = fields.summaryActor ? requireEdgeSummary(fields.summary) : fields.summary;
4391
4582
  const next = getLineageNextAsset(project, fields.rootAssetId);
4392
4583
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
4393
4584
  const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
@@ -4416,7 +4607,9 @@ function linkSelectedLineageChild(project, fields) {
4416
4607
  childAssetId: fields.childAssetId,
4417
4608
  confirmWrite: fields.confirmWrite,
4418
4609
  claimToken: fields.claimToken,
4419
- parentAssetId: next.next_asset.asset_id
4610
+ parentAssetId: next.next_asset.asset_id,
4611
+ summary,
4612
+ summaryActor: fields.summaryActor
4420
4613
  });
4421
4614
  return {
4422
4615
  ...result,
@@ -4592,8 +4785,8 @@ function recordId(project, asset) {
4592
4785
  const checksum = asset.local?.checksum_sha256 || asset.s3?.checksum_sha256;
4593
4786
  return checksum ? `${project}:sha256:${checksum}` : `${project}:asset:${asset.asset_id}`;
4594
4787
  }
4595
- function sourceId(project, record, sourceType, key) {
4596
- return `${project}:${record}:${sourceType}:${key}`;
4788
+ function sourceId(project, record2, sourceType, key) {
4789
+ return `${project}:${record2}:${sourceType}:${key}`;
4597
4790
  }
4598
4791
  function upsertProject2(database, project) {
4599
4792
  const timestamp = nowIso();
@@ -4648,9 +4841,9 @@ function upsertAssetLedgerAsset(database, project, asset, context = {}) {
4648
4841
  if (asset.s3) upsertSource(database, project, id, "s3", asset, asset.s3.key, context);
4649
4842
  upsertLedgerPlacementsForAsset(database, project, asset);
4650
4843
  }
4651
- function upsertSource(database, project, record, sourceType, asset, key, context = {}) {
4844
+ function upsertSource(database, project, record2, sourceType, asset, key, context = {}) {
4652
4845
  const timestamp = context.seenAt || nowIso();
4653
- const id = sourceId(project, record, sourceType, key);
4846
+ const id = sourceId(project, record2, sourceType, key);
4654
4847
  database.prepare(`
4655
4848
  insert into asset_ledger_sources (
4656
4849
  id, project_id, record_id, source_type, asset_id, local_path, s3_bucket, s3_region,
@@ -4673,7 +4866,7 @@ function upsertSource(database, project, record, sourceType, asset, key, context
4673
4866
  `).run(
4674
4867
  id,
4675
4868
  project,
4676
- record,
4869
+ record2,
4677
4870
  sourceType,
4678
4871
  asset.asset_id,
4679
4872
  sourceType === "local" ? asset.local?.relative_path || null : null,
@@ -4782,13 +4975,13 @@ function getAssetLedgerSnapshot(project = defaultProject) {
4782
4975
  const totals = sourceCounts(database, project);
4783
4976
  const lastIndexRun = latestIndexRun(database, project);
4784
4977
  const sourceRecords = sources.map(toSource);
4785
- 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)));
4786
4979
  const workflows = ledgerWorkflowStates(database, project, baseRecords, sourceRecords);
4787
4980
  database.close();
4788
4981
  return {
4789
4982
  project,
4790
4983
  database: lineageDbPath(),
4791
- records: baseRecords.map((record) => ({ ...record, workflow: workflows[record.id] || { placements: [] } })),
4984
+ records: baseRecords.map((record2) => ({ ...record2, workflow: workflows[record2.id] || { placements: [] } })),
4792
4985
  last_index_run: lastIndexRun,
4793
4986
  totals: {
4794
4987
  records: records.length,
@@ -4829,22 +5022,22 @@ function toIndexRun(row) {
4829
5022
  error: row.error || void 0
4830
5023
  };
4831
5024
  }
4832
- function toRecord(record, sources) {
5025
+ function toRecord(record2, sources) {
4833
5026
  return {
4834
- id: record.id,
4835
- project: record.project_id,
4836
- canonical_asset_id: record.canonical_asset_id,
4837
- checksum_sha256: record.checksum_sha256 || void 0,
4838
- media_type: record.media_type,
4839
- title: record.title,
4840
- status: record.status,
4841
- channel: record.channel || void 0,
4842
- campaign: record.campaign || void 0,
4843
- audience: record.audience || void 0,
4844
- updated_at: record.updated_at,
4845
- first_seen_at: record.first_seen_at || void 0,
4846
- last_seen_at: record.last_seen_at,
4847
- 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,
4848
5041
  sources,
4849
5042
  workflow: { placements: [] }
4850
5043
  };
@@ -4878,26 +5071,26 @@ var selectionFilters = /* @__PURE__ */ new Set(["all", "selected", "not-selected
4878
5071
  function normalizeFilter(value, allowed, fallback) {
4879
5072
  return typeof value === "string" && allowed.has(value) ? value : fallback;
4880
5073
  }
4881
- function sourceTypes(record) {
4882
- 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));
4883
5076
  }
4884
- function matchesStorage(record, storage) {
4885
- const types = sourceTypes(record);
5077
+ function matchesStorage(record2, storage) {
5078
+ const types = sourceTypes(record2);
4886
5079
  if (storage === "local-only") return types.has("local") && !types.has("s3");
4887
5080
  if (storage === "s3-backed") return types.has("s3");
4888
5081
  if (storage === "local-and-s3") return types.has("local") && types.has("s3");
4889
5082
  if (storage === "catalog-only") return types.has("catalog") && !types.has("local") && !types.has("s3");
4890
5083
  return true;
4891
5084
  }
4892
- function matchesPlacement(record, placement) {
4893
- if (placement === "not-scheduled") return record.workflow.placements.length === 0;
4894
- 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");
4895
5088
  if (placement === "all") return true;
4896
- return record.workflow.placements.some((item) => item.status === placement);
5089
+ return record2.workflow.placements.some((item) => item.status === placement);
4897
5090
  }
4898
- function matchesSelection(record, selection) {
4899
- if (selection === "selected") return Boolean(record.workflow.selection);
4900
- 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;
4901
5094
  return true;
4902
5095
  }
4903
5096
  function filterLedgerRecords(records, options = {}) {
@@ -4906,21 +5099,21 @@ function filterLedgerRecords(records, options = {}) {
4906
5099
  const selection = normalizeFilter(options.selection, selectionFilters, "all");
4907
5100
  const storage = normalizeFilter(options.storage, storageFilters, "all");
4908
5101
  const query = options.query?.trim().toLowerCase() || "";
4909
- return records.filter((record) => {
4910
- if (!matchesStorage(record, storage)) return false;
4911
- if (review !== "all" && (record.workflow.review?.review_state || "unreviewed") !== review) return false;
4912
- if (!matchesPlacement(record, placement)) return false;
4913
- 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;
4914
5107
  if (!query) return true;
4915
5108
  return [
4916
- record.canonical_asset_id,
4917
- record.title,
4918
- record.status,
4919
- record.channel,
4920
- record.campaign,
4921
- record.audience,
4922
- record.workflow.review?.review_state,
4923
- ...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}`)
4924
5117
  ].join(" ").toLowerCase().includes(query);
4925
5118
  });
4926
5119
  }
@@ -7000,7 +7193,141 @@ function contentBatchRouter(projectFrom2) {
7000
7193
  import { existsSync as existsSync9, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
7001
7194
  import { relative as relative4, resolve as resolve5 } from "node:path";
7002
7195
  import { randomUUID as randomUUID3 } from "node:crypto";
7003
- 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";
7004
7331
  var provider = "codex-handoff";
7005
7332
  var GenerationReceiptError = class extends Error {
7006
7333
  constructor(message, status = 400) {
@@ -7022,6 +7349,70 @@ function parseJson(value, fallback) {
7022
7349
  if (!value) return fallback;
7023
7350
  return JSON.parse(value);
7024
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
+ }
7025
7416
  function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
7026
7417
  const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
7027
7418
  return {
@@ -7050,6 +7441,19 @@ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
7050
7441
  guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
7051
7442
  };
7052
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
+ }
7053
7457
  function receiptFrom(row) {
7054
7458
  return {
7055
7459
  id: String(row.id),
@@ -7062,6 +7466,7 @@ function receiptFrom(row) {
7062
7466
  };
7063
7467
  }
7064
7468
  function outputFrom(row) {
7469
+ const edgeSummary = typeof row.edge_summary === "string" && row.edge_summary.length > 0 ? row.edge_summary : void 0;
7065
7470
  return {
7066
7471
  id: String(row.id),
7067
7472
  job_id: String(row.job_id),
@@ -7073,7 +7478,8 @@ function outputFrom(row) {
7073
7478
  content_type: String(row.content_type),
7074
7479
  imported_asset_id: String(row.imported_asset_id),
7075
7480
  parent_asset_id: String(row.parent_asset_id),
7076
- imported_at: String(row.imported_at)
7481
+ imported_at: String(row.imported_at),
7482
+ ...edgeSummary ? { edge_summary: edgeSummary } : {}
7077
7483
  };
7078
7484
  }
7079
7485
  function loadGenerationJob(database, project, id) {
@@ -7119,6 +7525,72 @@ function insertReceipt(database, id, type, command, payload) {
7119
7525
  values (?, ?, ?, 'ok', ?, ?, ?)
7120
7526
  `).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
7121
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
+ }
7122
7594
  function planImageReroll(project = defaultProject, fields) {
7123
7595
  const prompt = fields.prompt.trim();
7124
7596
  if (!prompt) throw new GenerationReceiptError("Missing --prompt");
@@ -7163,7 +7635,7 @@ function planImageReroll(project = defaultProject, fields) {
7163
7635
  id,
7164
7636
  project_id: project,
7165
7637
  provider,
7166
- adapter_version: adapterVersion,
7638
+ adapter_version: legacyAdapterVersion,
7167
7639
  source_mode: "lineage_reroll",
7168
7640
  root_asset_id: snapshot.root_asset_id,
7169
7641
  prompt,
@@ -7195,7 +7667,7 @@ function planImageReroll(project = defaultProject, fields) {
7195
7667
  id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
7196
7668
  expected_output_count, status, output_dir, handoff_json, created_at, updated_at
7197
7669
  ) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
7198
- `).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);
7199
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));
7200
7672
  insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
7201
7673
  database.exec("COMMIT");
@@ -7208,14 +7680,66 @@ function planImageReroll(project = defaultProject, fields) {
7208
7680
  database.close();
7209
7681
  }
7210
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
+ }
7211
7727
  function isPathInside2(child, parent) {
7212
7728
  const rel = relative4(parent, child);
7213
7729
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
7214
7730
  }
7215
- function resolveScratchFile(file) {
7731
+ function scratchCandidate(file) {
7216
7732
  const scratchRoot = resolve5(repoRoot, ".asset-scratch");
7217
7733
  const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
7218
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);
7219
7743
  if (!existsSync9(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
7220
7744
  const realScratchRoot = realpathSync2(scratchRoot);
7221
7745
  const realCandidate = realpathSync2(candidate);
@@ -7231,6 +7755,165 @@ function resolveScratchFile(file) {
7231
7755
  assetId: `local-${checksum.slice(0, 12)}`
7232
7756
  };
7233
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
+ }
7234
7917
  function importImageRerollOutput(project = defaultProject, fields) {
7235
7918
  if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
7236
7919
  if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
@@ -7962,6 +8645,23 @@ function upsertSwissifierRerollAttempts(project, manifest) {
7962
8645
  }
7963
8646
  return { total: attempts.length };
7964
8647
  }
8648
+ function backfillSwissifierEdgeSummaries(project, manifest) {
8649
+ const database = lineageDb();
8650
+ const timestamp = nowIso();
8651
+ try {
8652
+ const statement = database.prepare(`
8653
+ update asset_edges
8654
+ set summary = ?, summary_created_by = 'system', summary_updated_by = 'system', summary_updated_at = ?
8655
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
8656
+ and summary is null and summary_created_by is null and summary_updated_by is null and summary_updated_at is null
8657
+ `);
8658
+ for (const edge of manifest.edges) {
8659
+ statement.run(requireEdgeSummary(edge.summary), timestamp, project, edge.parent, edge.child);
8660
+ }
8661
+ } finally {
8662
+ database.close();
8663
+ }
8664
+ }
7965
8665
  function seedDemoLineageWorkspace(project, fields) {
7966
8666
  const ids = fields.confirmWrite ? writeDemoFiles(project) : demoAssetIds();
7967
8667
  const rootAssetId = ids.root;
@@ -8024,6 +8724,7 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
8024
8724
  for (const edge of manifest.edges) {
8025
8725
  linkLineageAssets(project, { parentAssetId: edge.parent, childAssetId: edge.child, confirmWrite: true });
8026
8726
  }
8727
+ backfillSwissifierEdgeSummaries(project, manifest);
8027
8728
  const reroll_attempts = upsertSwissifierRerollAttempts(project, manifest);
8028
8729
  updateSelectedAsset(project, {
8029
8730
  assetIds: manifest.selected_asset_ids,
@@ -8429,6 +9130,9 @@ function getLineageRuntimeInfo(options = {}) {
8429
9130
  const code = options.code || getLineageCodeIdentity(channel);
8430
9131
  const databaseInfo = { exists: existsSync11(dbPath), path: dbPath };
8431
9132
  const schema = { migration_keys: [] };
9133
+ const serviceInstanceId = process.env.LINEAGE_SERVICE_INSTANCE_ID;
9134
+ const launcherPid = process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0;
9135
+ const isServiceProcess = Boolean(serviceInstanceId && Number.isInteger(launcherPid) && launcherPid > 0);
8432
9136
  if (databaseInfo.exists) {
8433
9137
  try {
8434
9138
  const stat = statSync5(dbPath);
@@ -8466,14 +9170,22 @@ function getLineageRuntimeInfo(options = {}) {
8466
9170
  git_sha: code.git_sha,
8467
9171
  node_env: process.env.NODE_ENV,
8468
9172
  package_name: info.name,
8469
- profile: runtimeProfileIdentity(channel),
8470
- schema,
8471
- service: {
8472
- instance_id: process.env.LINEAGE_SERVICE_INSTANCE_ID,
8473
- launcher_pid: process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0,
9173
+ process: {
8474
9174
  pid: process.pid,
9175
+ role: isServiceProcess ? "service" : "command",
8475
9176
  started_at: processStartedAt
8476
9177
  },
9178
+ profile: runtimeProfileIdentity(channel),
9179
+ schema,
9180
+ ...isServiceProcess ? {
9181
+ service: {
9182
+ instance_id: serviceInstanceId,
9183
+ launcher_pid: launcherPid,
9184
+ mode: process.env.LINEAGE_SERVICE_MODE === "managed" ? "managed" : "foreground",
9185
+ pid: process.pid,
9186
+ started_at: processStartedAt
9187
+ }
9188
+ } : {},
8477
9189
  version: info.version
8478
9190
  };
8479
9191
  }
@@ -9092,6 +9804,26 @@ function readOptions(args, name) {
9092
9804
  }
9093
9805
  return values;
9094
9806
  }
9807
+ function readJsonFile(path, label) {
9808
+ try {
9809
+ return JSON.parse(readFileSync8(resolve8(path), "utf8"));
9810
+ } catch (error) {
9811
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
9812
+ }
9813
+ }
9814
+ function parentFilesOption(value) {
9815
+ if (value === void 0) return void 0;
9816
+ const mappings = {};
9817
+ for (const rawMapping of value.split(";")) {
9818
+ const separator = rawMapping.indexOf("=");
9819
+ const parent = separator >= 0 ? rawMapping.slice(0, separator).trim() : "";
9820
+ const files = separator >= 0 ? rawMapping.slice(separator + 1).split(",").map((file) => file.trim()).filter(Boolean) : [];
9821
+ if (!parent || files.length === 0) throw new Error("lineage generate image import --parent-files requires parent=file[,file][;parent=file]");
9822
+ if (Object.hasOwn(mappings, parent)) throw new Error(`Duplicate generation parent mapping: ${parent}`);
9823
+ mappings[parent] = files;
9824
+ }
9825
+ return mappings;
9826
+ }
9095
9827
  function resolveCliAssetRoot(args) {
9096
9828
  return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
9097
9829
  }
@@ -9123,7 +9855,8 @@ function resolveDataCommandOptions(args) {
9123
9855
  dbPath: readOption(args, "--db"),
9124
9856
  json: args.includes("--json"),
9125
9857
  project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
9126
- rootAssetId: readOption(args, "--root")
9858
+ rootAssetId: readOption(args, "--root"),
9859
+ summary: readOption(args, "--summary")
9127
9860
  };
9128
9861
  if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
9129
9862
  return options;
@@ -9166,13 +9899,57 @@ function runLineageDataCommand(command, args) {
9166
9899
  }
9167
9900
  if (command === "link-child") {
9168
9901
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
9902
+ const summary = requireEdgeSummary(options.summary);
9169
9903
  return linkSelectedLineageChild(options.project, {
9170
9904
  childAssetId: options.childAssetId,
9171
9905
  claimToken: options.claimToken,
9172
9906
  confirmWrite: options.confirmWrite,
9173
- rootAssetId: options.rootAssetId || options.assetId
9907
+ rootAssetId: options.rootAssetId || options.assetId,
9908
+ summary,
9909
+ summaryActor: "agent"
9174
9910
  });
9175
9911
  }
9912
+ if (command === "generate") {
9913
+ const [resource, subcommand] = positionalArgs(args);
9914
+ if (resource !== "image") throw new Error(`Unknown generate command: ${resource}`);
9915
+ if (subcommand === "plan") {
9916
+ const prompt = readOption(args, "--prompt");
9917
+ const rawCount = readOption(args, "--count");
9918
+ const rawPerBaseCount = readOption(args, "--per-base-count");
9919
+ if (!prompt) throw new Error("lineage generate image plan requires --prompt");
9920
+ return planImageGeneration(options.project, {
9921
+ count: rawCount === void 0 ? void 0 : Number(rawCount),
9922
+ dryRun: args.includes("--dry-run"),
9923
+ fromLineageSelection: args.includes("--from-lineage-selection"),
9924
+ perBaseCount: rawPerBaseCount === void 0 ? void 0 : Number(rawPerBaseCount),
9925
+ prompt
9926
+ });
9927
+ }
9928
+ if (subcommand === "inspect") {
9929
+ const jobId2 = readOption(args, "--job-id");
9930
+ if (!jobId2) throw new Error("lineage generate image inspect requires --job-id");
9931
+ return inspectImageGeneration(options.project, jobId2);
9932
+ }
9933
+ if (subcommand === "import") {
9934
+ const jobId2 = readOption(args, "--job-id");
9935
+ const manifestPath = readOption(args, "--manifest");
9936
+ const filesValue = readOption(args, "--files");
9937
+ const parentFilesValue = readOption(args, "--parent-files");
9938
+ if (!jobId2) throw new Error("lineage generate image import requires --job-id");
9939
+ if (manifestPath !== void 0 && (filesValue !== void 0 || parentFilesValue !== void 0)) {
9940
+ throw new Error("Use --manifest or legacy --files/--parent-files, not both");
9941
+ }
9942
+ if (filesValue !== void 0 && parentFilesValue !== void 0) throw new Error("Use --files or --parent-files, not both");
9943
+ return importImageGenerationOutputs(options.project, {
9944
+ confirmWrite: options.confirmWrite,
9945
+ files: filesValue === void 0 ? void 0 : filesValue.split(",").map((file) => file.trim()).filter(Boolean),
9946
+ jobId: jobId2,
9947
+ manifest: manifestPath === void 0 ? void 0 : readJsonFile(manifestPath, "Generation output manifest"),
9948
+ parentFiles: parentFilesOption(parentFilesValue)
9949
+ });
9950
+ }
9951
+ throw new Error(`Unknown generate image command: ${subcommand}`);
9952
+ }
9176
9953
  if (command === "reroll") {
9177
9954
  const subcommand = positionalArgs(args)[0] || "";
9178
9955
  if (subcommand === "list") {
@@ -9291,8 +10068,10 @@ function rerollRequestedBy(value) {
9291
10068
  throw new Error(`Invalid re-roll requester: ${value}`);
9292
10069
  }
9293
10070
  function lineageCliCanDelegateMutation(command, args) {
9294
- const subcommand = positionalArgs(args)[0] || "";
10071
+ const positions = positionalArgs(args);
10072
+ const subcommand = positions[0] || "";
9295
10073
  if (command === "link-child") return true;
10074
+ if (command === "generate") return positions[0] === "image" && ["plan", "import"].includes(positions[1] || "");
9296
10075
  if (command === "reroll") return ["mark", "cancel", "plan", "import"].includes(subcommand);
9297
10076
  if (command === "tasks") return ["claim", "start", "comment", "cancel", "override", "instructions"].includes(subcommand);
9298
10077
  if (command === "agent") return ["claim", "heartbeat", "release", "revoke", "transfer"].includes(subcommand);
@@ -9372,7 +10151,12 @@ var runtimeChannel = normalizeRuntimeChannel(process.env.LINEAGE_CHANNEL || proc
9372
10151
  var startupCode = assertLineageCodeOrigin(runtimeChannel);
9373
10152
  assertRuntimeProfileSafety(runtimeChannel);
9374
10153
  var startupRuntime = getLineageRuntimeInfo({ channel: runtimeChannel, code: startupCode });
9375
- if (!process.env.LINEAGE_PROFILE) assertUnselectedDatabaseIsUnbound(startupRuntime);
10154
+ if (!process.env.LINEAGE_PROFILE) {
10155
+ if (process.env.LINEAGE_ALLOW_UNBOUND_DIAGNOSTIC !== "1") {
10156
+ throw new Error("Lineage server startup requires a named profile. Use a channel launcher with start --profile <id>, or use `npm run dev -- --profile <id>` for hot reload. Set LINEAGE_ALLOW_UNBOUND_DIAGNOSTIC=1 only for an explicit read-only diagnostic server.");
10157
+ }
10158
+ assertUnselectedDatabaseIsUnbound(startupRuntime);
10159
+ }
9376
10160
  var startupProfile;
9377
10161
  if (process.env.LINEAGE_PROFILE) {
9378
10162
  if (!process.env.LINEAGE_PROFILE_ID || !process.env.LINEAGE_PROFILE_ENVIRONMENT || !process.env.LINEAGE_PROFILE_MANIFEST) {
@@ -9424,7 +10208,7 @@ app.use((req, res, next) => {
9424
10208
  if (startupProfile || req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS") return next();
9425
10209
  res.status(409).json({
9426
10210
  error: "profile_required",
9427
- message: "Persistent writes require a selected named Lineage profile; legacy-unbound service access is read-only."
10211
+ message: "Persistent writes require a selected named Lineage profile; legacy-unbound service access is read-only. Create a fresh profile with `profile init --profile <id> --confirm-write`, then start with that profile."
9428
10212
  });
9429
10213
  });
9430
10214
  function projectFrom(input) {
@@ -9568,6 +10352,16 @@ app.post(
9568
10352
  );
9569
10353
  })
9570
10354
  );
10355
+ app.post("/api/lineage/edges/:edgeId/summary", asyncRoute((req, res) => {
10356
+ res.json(updateLineageEdgeSummary(projectFrom(req), {
10357
+ edgeId: req.params.edgeId,
10358
+ action: req.body.action,
10359
+ summary: req.body.summary,
10360
+ expectedSummaryUpdatedAt: req.body.expectedSummaryUpdatedAt,
10361
+ confirmWrite: req.body.confirmWrite === true,
10362
+ claimToken: claimTokenFromRequest(req)
10363
+ }));
10364
+ }));
9571
10365
  app.post("/api/lineage/remove-node", asyncRoute((req, res) => {
9572
10366
  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) }));
9573
10367
  }));