@mean-weasel/lineage 0.1.15 → 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.
@@ -8,6 +8,32 @@ import { dirname as dirname6, join as join10, resolve as resolve8 } from "node:p
8
8
  import { spawn } from "node:child_process";
9
9
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10
10
 
11
+ // src/shared/edgeSummary.ts
12
+ var edgeSummaryWordLimit = 2;
13
+ var EdgeSummaryValidationError = class extends Error {
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ this.name = "EdgeSummaryValidationError";
18
+ }
19
+ code;
20
+ };
21
+ function normalizeEdgeSummary(value) {
22
+ if (value === void 0 || value === null) return void 0;
23
+ if (typeof value !== "string") throw new EdgeSummaryValidationError("not_text", "Edge summary must be text");
24
+ const words = value.trim().split(/\s+/u).filter(Boolean);
25
+ if (words.length === 0) return void 0;
26
+ if (words.length > edgeSummaryWordLimit) {
27
+ throw new EdgeSummaryValidationError("too_many_words", `Edge summary must contain at most ${edgeSummaryWordLimit} words`);
28
+ }
29
+ return words.join(" ");
30
+ }
31
+ function requireEdgeSummary(value) {
32
+ const summary = normalizeEdgeSummary(value);
33
+ if (!summary) throw new EdgeSummaryValidationError("required", "Edge summary is required");
34
+ return summary;
35
+ }
36
+
11
37
  // src/server/agentClaims.ts
12
38
  import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
13
39
 
@@ -654,13 +680,13 @@ function profileManifestPath(selector) {
654
680
  if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
655
681
  return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
656
682
  }
657
- function requiredString(record, key) {
658
- const value = record[key];
683
+ function requiredString(record2, key) {
684
+ const value = record2[key];
659
685
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
660
686
  return value.trim();
661
687
  }
662
- function optionalString(record, key) {
663
- const value = record[key];
688
+ function optionalString(record2, key) {
689
+ const value = record2[key];
664
690
  if (value === void 0) return void 0;
665
691
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
666
692
  return value.trim();
@@ -711,15 +737,15 @@ function resolveLineageProfile(selector) {
711
737
  throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
712
738
  }
713
739
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
714
- const record = raw;
715
- const schemaVersion = requiredString(record, "schema_version");
740
+ const record2 = raw;
741
+ const schemaVersion = requiredString(record2, "schema_version");
716
742
  if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
717
- const profileId = requiredString(record, "profile_id");
743
+ const profileId = requiredString(record2, "profile_id");
718
744
  if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
719
745
  if (namedProfileId && namedProfileId !== profileId) {
720
746
  throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
721
747
  }
722
- const expectedRaw = record.expected_runtime;
748
+ const expectedRaw = record2.expected_runtime;
723
749
  if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
724
750
  throw new Error("Profile expected_runtime must be an object");
725
751
  }
@@ -728,18 +754,18 @@ function resolveLineageProfile(selector) {
728
754
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
729
755
  const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
730
756
  const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
731
- const migrationsRaw = record.required_schema_migrations;
757
+ const migrationsRaw = record2.required_schema_migrations;
732
758
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
733
759
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
734
760
  }
735
- const environment = validateEnvironment(requiredString(record, "environment"));
761
+ const environment = validateEnvironment(requiredString(record2, "environment"));
736
762
  const expectedChannel = validateChannel(expected?.channel);
737
763
  if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
738
764
  throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
739
765
  }
740
766
  const manifest = {
741
- asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
742
- database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
767
+ asset_root: resolveManifestPath(manifestPath, requiredString(record2, "asset_root")),
768
+ database_path: resolveManifestPath(manifestPath, requiredString(record2, "database_path")),
743
769
  environment,
744
770
  ...expected ? {
745
771
  expected_runtime: {
@@ -753,7 +779,7 @@ function resolveLineageProfile(selector) {
753
779
  profile_id: profileId,
754
780
  ...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
755
781
  schema_version: lineageProfileSchemaVersion,
756
- service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
782
+ service_origin: validateServiceOrigin(requiredString(record2, "service_origin"))
757
783
  };
758
784
  return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
759
785
  }
@@ -1622,6 +1648,10 @@ function lineageDb() {
1622
1648
  child_asset_id text not null references assets(id),
1623
1649
  relation_type text not null check (relation_type in ('derived_from')),
1624
1650
  created_at text not null,
1651
+ summary text,
1652
+ summary_created_by text check (summary_created_by in ('human', 'agent', 'system')),
1653
+ summary_updated_by text check (summary_updated_by in ('human', 'agent', 'system')),
1654
+ summary_updated_at text,
1625
1655
  unique (project_id, parent_asset_id, child_asset_id, relation_type)
1626
1656
  );
1627
1657
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
@@ -1905,6 +1935,7 @@ function lineageDb() {
1905
1935
  imported_asset_id text not null references assets(id),
1906
1936
  parent_asset_id text not null references assets(id),
1907
1937
  imported_at text not null,
1938
+ edge_summary text,
1908
1939
  unique(job_id, output_index),
1909
1940
  unique(job_id, file_path)
1910
1941
  );
@@ -1993,6 +2024,11 @@ function lineageDb() {
1993
2024
  );
1994
2025
  create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
1995
2026
  `);
2027
+ ensureColumn(database, "asset_edges", "summary", "text");
2028
+ ensureColumn(database, "asset_edges", "summary_created_by", "text check (summary_created_by in ('human', 'agent', 'system'))");
2029
+ ensureColumn(database, "asset_edges", "summary_updated_by", "text check (summary_updated_by in ('human', 'agent', 'system'))");
2030
+ ensureColumn(database, "asset_edges", "summary_updated_at", "text");
2031
+ ensureColumn(database, "generation_job_outputs", "edge_summary", "text");
1996
2032
  migrateAssetSelections(database);
1997
2033
  dropLegacyAssetSelectionRootUnique(database);
1998
2034
  ensureColumn(database, "asset_selections", "notes", "text");
@@ -3118,6 +3154,14 @@ function cancelLineageTask(project, fields) {
3118
3154
  database.close();
3119
3155
  }
3120
3156
  }
3157
+ function cancelLineageIterateTasksForAssets(project, fields) {
3158
+ const targetIds = fields.assetIds ? new Set(fields.assetIds) : void 0;
3159
+ return listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "iterate").filter((task) => !targetIds || targetIds.has(task.target_asset_id)).map((task) => cancelLineageTask(project, {
3160
+ actor: fields.actor,
3161
+ confirmWrite: fields.confirmWrite,
3162
+ taskId: task.id
3163
+ }));
3164
+ }
3121
3165
  function resolveLineageTask(project, fields) {
3122
3166
  const normalizedProject = normalizeProject(project);
3123
3167
  const actor = normalizeActor(fields.actor, "Resolve actor");
@@ -3483,6 +3527,23 @@ function rerollRequestId(project, root, node, timestamp) {
3483
3527
  function rowString(value) {
3484
3528
  return typeof value === "string" && value.length > 0 ? value : void 0;
3485
3529
  }
3530
+ function lineageEdgeFromRow(row) {
3531
+ const summary = rowString(row.summary);
3532
+ const summaryCreatedBy = rowString(row.summary_created_by);
3533
+ const summaryUpdatedBy = rowString(row.summary_updated_by);
3534
+ const summaryUpdatedAt = rowString(row.summary_updated_at);
3535
+ return {
3536
+ id: String(row.id),
3537
+ parent_asset_id: String(row.parent_asset_id),
3538
+ child_asset_id: String(row.child_asset_id),
3539
+ relation_type: row.relation_type,
3540
+ created_at: String(row.created_at),
3541
+ ...summary ? { summary } : {},
3542
+ ...summaryCreatedBy ? { summary_created_by: summaryCreatedBy } : {},
3543
+ ...summaryUpdatedBy ? { summary_updated_by: summaryUpdatedBy } : {},
3544
+ ...summaryUpdatedAt ? { summary_updated_at: summaryUpdatedAt } : {}
3545
+ };
3546
+ }
3486
3547
  function rerollRequestFrom(row) {
3487
3548
  return {
3488
3549
  id: String(row.id),
@@ -3582,6 +3643,9 @@ function localPreviewUrl(project, localPath) {
3582
3643
  return `/api/assets/local-preview?${params.toString()}`;
3583
3644
  }
3584
3645
  function linkLineageAssets(project, fields) {
3646
+ const summary = normalizeEdgeSummary(fields.summary);
3647
+ if (summary && !fields.summaryActor) throw new LineageError("Lineage edge summary requires an explicit author");
3648
+ if (!summary && fields.summaryActor) throw new LineageError("Lineage edge summary author requires a summary");
3585
3649
  const database = lineageDb();
3586
3650
  requireAsset2(database, project, fields.parentAssetId);
3587
3651
  requireAsset2(database, project, fields.childAssetId);
@@ -3600,22 +3664,66 @@ function linkLineageAssets(project, fields) {
3600
3664
  database.close();
3601
3665
  throw error;
3602
3666
  }
3667
+ const createdAt = nowIso();
3603
3668
  const edge = {
3604
3669
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
3605
3670
  parent_asset_id: fields.parentAssetId,
3606
3671
  child_asset_id: fields.childAssetId,
3607
3672
  relation_type: "derived_from",
3608
- created_at: nowIso()
3673
+ created_at: createdAt,
3674
+ ...summary ? {
3675
+ summary,
3676
+ summary_created_by: fields.summaryActor,
3677
+ summary_updated_by: fields.summaryActor,
3678
+ summary_updated_at: createdAt
3679
+ } : {}
3609
3680
  };
3610
3681
  if (!fields.confirmWrite) {
3611
3682
  database.close();
3612
3683
  return { ok: true, dryRun: true, edge };
3613
3684
  }
3614
- database.prepare(`
3615
- insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
3616
- values (?, ?, ?, ?, 'derived_from', ?)
3685
+ const inserted = database.prepare(`
3686
+ insert into asset_edges (
3687
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
3688
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3689
+ )
3690
+ values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
3617
3691
  on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
3618
- `).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
3692
+ `).run(
3693
+ edge.id,
3694
+ project,
3695
+ edge.parent_asset_id,
3696
+ edge.child_asset_id,
3697
+ edge.created_at,
3698
+ edge.summary || null,
3699
+ edge.summary_created_by || null,
3700
+ edge.summary_updated_by || null,
3701
+ edge.summary_updated_at || null
3702
+ );
3703
+ if (inserted.changes === 0) {
3704
+ const existingRow = database.prepare(`
3705
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3706
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3707
+ from asset_edges
3708
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
3709
+ `).get(project, edge.parent_asset_id, edge.child_asset_id);
3710
+ if (!existingRow) {
3711
+ database.close();
3712
+ throw new LineageError("Lineage edge conflict could not be resolved", 409);
3713
+ }
3714
+ const existingEdge = lineageEdgeFromRow(existingRow);
3715
+ if (summary && (existingEdge.summary !== summary || existingEdge.summary_created_by !== fields.summaryActor || existingEdge.summary_updated_by !== fields.summaryActor || !existingEdge.summary_updated_at)) {
3716
+ database.close();
3717
+ throw new LineageError("Lineage edge already exists with a different summary or provenance", 409);
3718
+ }
3719
+ database.close();
3720
+ return {
3721
+ ok: true,
3722
+ idempotent: true,
3723
+ message: `Already linked ${existingEdge.child_asset_id} from ${existingEdge.parent_asset_id}`,
3724
+ edge: existingEdge
3725
+ };
3726
+ }
3619
3727
  database.close();
3620
3728
  return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
3621
3729
  }
@@ -3627,7 +3735,13 @@ function descendants(database, project, root) {
3627
3735
  const parent = queue.shift();
3628
3736
  if (seen.has(parent)) continue;
3629
3737
  seen.add(parent);
3630
- 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);
3738
+ const rows = database.prepare(`
3739
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3740
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3741
+ from asset_edges
3742
+ where project_id = ? and parent_asset_id = ?
3743
+ order by created_at
3744
+ `).all(project, parent).map(lineageEdgeFromRow);
3631
3745
  edges.push(...rows);
3632
3746
  queue.push(...rows.map((row) => row.child_asset_id));
3633
3747
  }
@@ -4013,7 +4127,7 @@ function lineageCommand(command, project, rootAssetId) {
4013
4127
  return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
4014
4128
  }
4015
4129
  function linkChildCommand(project, rootAssetId) {
4016
- return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
4130
+ return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --summary "<one-or-two-words>" --confirm-write ${lineageRuntimeSelector()} --json`;
4017
4131
  }
4018
4132
  function rerollImportGuidance(rootAssetId, targetAssetId) {
4019
4133
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -4062,6 +4176,7 @@ function getLineageBrief(project, rootAssetId) {
4062
4176
  };
4063
4177
  }
4064
4178
  function linkSelectedLineageChild(project, fields) {
4179
+ const summary = fields.summaryActor ? requireEdgeSummary(fields.summary) : fields.summary;
4065
4180
  const next = getLineageNextAsset(project, fields.rootAssetId);
4066
4181
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
4067
4182
  const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
@@ -4090,7 +4205,9 @@ function linkSelectedLineageChild(project, fields) {
4090
4205
  childAssetId: fields.childAssetId,
4091
4206
  confirmWrite: fields.confirmWrite,
4092
4207
  claimToken: fields.claimToken,
4093
- parentAssetId: next.next_asset.asset_id
4208
+ parentAssetId: next.next_asset.asset_id,
4209
+ summary,
4210
+ summaryActor: fields.summaryActor
4094
4211
  });
4095
4212
  return {
4096
4213
  ...result,
@@ -4110,7 +4227,141 @@ function linkSelectedLineageChild(project, fields) {
4110
4227
  import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
4111
4228
  import { relative as relative3, resolve as resolve5 } from "node:path";
4112
4229
  import { randomUUID as randomUUID3 } from "node:crypto";
4113
- var adapterVersion = "generation-receipts-v1";
4230
+
4231
+ // src/shared/generationOutputManifest.ts
4232
+ var generationOutputManifestSchemaVersion = "lineage.generation_output_manifest.v1";
4233
+ var GenerationOutputManifestError = class extends Error {
4234
+ constructor(message) {
4235
+ super(message);
4236
+ this.name = "GenerationOutputManifestError";
4237
+ }
4238
+ };
4239
+ function record(value, label) {
4240
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4241
+ throw new GenerationOutputManifestError(`${label} must be an object`);
4242
+ }
4243
+ return value;
4244
+ }
4245
+ function exactKeys(value, allowed, label) {
4246
+ const allowedKeys = new Set(allowed);
4247
+ const unsupported = Object.keys(value).filter((key) => !allowedKeys.has(key)).sort();
4248
+ if (unsupported.length > 0) {
4249
+ throw new GenerationOutputManifestError(`${label} contains unsupported field: ${unsupported[0]}`);
4250
+ }
4251
+ }
4252
+ function nonEmptyText(value, label) {
4253
+ if (typeof value !== "string" || !value.trim()) throw new GenerationOutputManifestError(`${label} is required`);
4254
+ return value.trim();
4255
+ }
4256
+ function expectedGenerationOutputParents(job) {
4257
+ if (!Number.isInteger(job.expected_output_count) || job.expected_output_count <= 0) {
4258
+ throw new GenerationOutputManifestError("Generation job expected output count must be a positive integer");
4259
+ }
4260
+ const parents = job.inputs.filter((input) => input.role === "lineage_next_base").sort((left, right) => left.position - right.position);
4261
+ if (parents.length === 0) throw new GenerationOutputManifestError("Generation job has no lineage_next_base input");
4262
+ const parentIds = /* @__PURE__ */ new Set();
4263
+ const parentPositions = /* @__PURE__ */ new Set();
4264
+ for (const parent of parents) {
4265
+ if (typeof parent.asset_id !== "string" || !parent.asset_id || parent.asset_id.trim() !== parent.asset_id) {
4266
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent asset id");
4267
+ }
4268
+ if (!Number.isInteger(parent.position) || parent.position < 0 || parentPositions.has(parent.position)) {
4269
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent positions");
4270
+ }
4271
+ if (parentIds.has(parent.asset_id)) throw new GenerationOutputManifestError(`Generation job has duplicate lineage parent: ${parent.asset_id}`);
4272
+ parentIds.add(parent.asset_id);
4273
+ parentPositions.add(parent.position);
4274
+ }
4275
+ if (job.expected_output_count % parents.length !== 0) {
4276
+ throw new GenerationOutputManifestError("Generation job has invalid parent mapping");
4277
+ }
4278
+ const outputsPerParent = job.expected_output_count / parents.length;
4279
+ return Array.from({ length: job.expected_output_count }, (_value, outputIndex) => {
4280
+ return parents[Math.floor(outputIndex / outputsPerParent)].asset_id;
4281
+ });
4282
+ }
4283
+ function createGenerationOutputManifestDraft(job) {
4284
+ return {
4285
+ schema_version: generationOutputManifestSchemaVersion,
4286
+ job_id: job.id,
4287
+ outputs: expectedGenerationOutputParents(job).map((parentAssetId, outputIndex) => ({
4288
+ output_index: outputIndex,
4289
+ file_path: "",
4290
+ parent_asset_id: parentAssetId,
4291
+ edge_summary: ""
4292
+ }))
4293
+ };
4294
+ }
4295
+ function parseGenerationOutputManifest(value, job, options) {
4296
+ if (!options || typeof options.resolveFilePath !== "function") {
4297
+ throw new GenerationOutputManifestError("Generation output manifest requires a file-path resolver");
4298
+ }
4299
+ const manifest = record(value, "Generation output manifest");
4300
+ exactKeys(manifest, ["schema_version", "job_id", "outputs"], "Generation output manifest");
4301
+ if (manifest.schema_version !== generationOutputManifestSchemaVersion) {
4302
+ throw new GenerationOutputManifestError(`Generation output manifest schema_version must be ${generationOutputManifestSchemaVersion}`);
4303
+ }
4304
+ if (manifest.job_id !== job.id) throw new GenerationOutputManifestError(`Generation output manifest job_id must be ${job.id}`);
4305
+ if (!Array.isArray(manifest.outputs)) throw new GenerationOutputManifestError("Generation output manifest outputs must be an array");
4306
+ const expectedParents = expectedGenerationOutputParents(job);
4307
+ if (manifest.outputs.length !== expectedParents.length) {
4308
+ throw new GenerationOutputManifestError(`Generation output manifest requires ${expectedParents.length} outputs, received ${manifest.outputs.length}`);
4309
+ }
4310
+ const seenIndexes = /* @__PURE__ */ new Set();
4311
+ const outputs = manifest.outputs.map((value2, position) => {
4312
+ const output = record(value2, `Generation output at position ${position}`);
4313
+ exactKeys(output, ["output_index", "file_path", "parent_asset_id", "edge_summary"], `Generation output at position ${position}`);
4314
+ const outputIndex = output.output_index;
4315
+ if (!Number.isInteger(outputIndex) || Number(outputIndex) < 0) {
4316
+ throw new GenerationOutputManifestError(`Generation output at position ${position} requires a non-negative integer output_index`);
4317
+ }
4318
+ const normalizedIndex = Number(outputIndex);
4319
+ if (seenIndexes.has(normalizedIndex)) throw new GenerationOutputManifestError(`Duplicate generation output_index: ${normalizedIndex}`);
4320
+ seenIndexes.add(normalizedIndex);
4321
+ const expectedParent = expectedParents[normalizedIndex];
4322
+ if (!expectedParent) throw new GenerationOutputManifestError(`Unknown generation output_index: ${normalizedIndex}`);
4323
+ const filePath = nonEmptyText(output.file_path, `Generation output ${normalizedIndex} file_path`);
4324
+ const parentAssetId = nonEmptyText(output.parent_asset_id, `Generation output ${normalizedIndex} parent_asset_id`);
4325
+ if (parentAssetId !== expectedParent) {
4326
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex} must use parent_asset_id ${expectedParent}`);
4327
+ }
4328
+ let edgeSummary;
4329
+ try {
4330
+ edgeSummary = requireEdgeSummary(output.edge_summary);
4331
+ } catch (error) {
4332
+ if (error instanceof EdgeSummaryValidationError) {
4333
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex}: ${error.message}`);
4334
+ }
4335
+ throw error;
4336
+ }
4337
+ return {
4338
+ output_index: normalizedIndex,
4339
+ file_path: filePath,
4340
+ parent_asset_id: parentAssetId,
4341
+ edge_summary: edgeSummary
4342
+ };
4343
+ });
4344
+ for (const outputIndex of expectedParents.keys()) {
4345
+ if (!seenIndexes.has(outputIndex)) throw new GenerationOutputManifestError(`Missing generation output_index: ${outputIndex}`);
4346
+ }
4347
+ outputs.sort((left, right) => left.output_index - right.output_index);
4348
+ const seenPaths = /* @__PURE__ */ new Set();
4349
+ const resolvedOutputs = outputs.map((output) => {
4350
+ const filePath = nonEmptyText(options.resolveFilePath(output.file_path), `Generation output ${output.output_index} resolved file_path`);
4351
+ if (seenPaths.has(filePath)) throw new GenerationOutputManifestError(`Duplicate generation output file_path: ${filePath}`);
4352
+ seenPaths.add(filePath);
4353
+ return { ...output, file_path: filePath };
4354
+ });
4355
+ return {
4356
+ schema_version: generationOutputManifestSchemaVersion,
4357
+ job_id: job.id,
4358
+ outputs: resolvedOutputs
4359
+ };
4360
+ }
4361
+
4362
+ // src/server/generationReceipts.ts
4363
+ var legacyAdapterVersion = "generation-receipts-v1";
4364
+ var manifestAdapterVersion = "generation-receipts-v2";
4114
4365
  var provider = "codex-handoff";
4115
4366
  var GenerationReceiptError = class extends Error {
4116
4367
  constructor(message, status = 400) {
@@ -4129,6 +4380,70 @@ function parseJson(value, fallback) {
4129
4380
  if (!value) return fallback;
4130
4381
  return JSON.parse(value);
4131
4382
  }
4383
+ function positiveInteger(value) {
4384
+ return Number.isInteger(value) && Number(value) > 0;
4385
+ }
4386
+ function resolveLineageSelection(project) {
4387
+ const rootAssetId = activeLineageWorkspaceRoot(project);
4388
+ if (!rootAssetId) throw new GenerationReceiptError("No active lineage workspace for generation planning");
4389
+ const next = getLineageNextAsset(project, rootAssetId);
4390
+ if (!next.next_asset) throw new GenerationReceiptError(`No clear lineage next asset: ${next.reason}`);
4391
+ if (next.strategy !== "selected") throw new GenerationReceiptError("Generation v1 requires an explicit selected lineage next base");
4392
+ 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");
4393
+ return next;
4394
+ }
4395
+ function selectedParents(next) {
4396
+ const parents = next.next_assets.length > 0 ? next.next_assets : next.next_asset ? [next.next_asset] : [];
4397
+ if (parents.length === 0) throw new GenerationReceiptError("Missing lineage next base");
4398
+ return parents;
4399
+ }
4400
+ function parentMappings(next, perBaseCount) {
4401
+ return selectedParents(next).map((parent, parentIndex) => ({
4402
+ parent,
4403
+ output_indexes: Array.from({ length: perBaseCount }, (_value, index) => parentIndex * perBaseCount + index)
4404
+ }));
4405
+ }
4406
+ function buildHandoff(project, id, prompt, count, perBaseCount, next, inputs) {
4407
+ const parent = next.next_asset;
4408
+ if (!parent) throw new GenerationReceiptError("Missing lineage next base");
4409
+ const parents = parentMappings(next, perBaseCount);
4410
+ const outputManifest = createGenerationOutputManifestDraft({ id, expected_output_count: count, inputs });
4411
+ 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`;
4412
+ return {
4413
+ schema_version: "lineage.generation_handoff.v2",
4414
+ provider,
4415
+ project,
4416
+ job_id: id,
4417
+ prompt,
4418
+ expected_output_count: count,
4419
+ per_base_count: next.selection_mode === "multiple" ? perBaseCount : void 0,
4420
+ lineage: {
4421
+ root_asset_id: next.root_asset_id,
4422
+ parent_asset_id: parent.asset_id,
4423
+ selection_strategy: next.strategy,
4424
+ parent_title: parent.title,
4425
+ parent_local_path: parent.local_path,
4426
+ parent_s3_key: parent.s3_key,
4427
+ parents: parents.length > 1 ? parents.map((mapping) => ({
4428
+ parent_asset_id: mapping.parent.asset_id,
4429
+ parent_title: mapping.parent.title,
4430
+ parent_local_path: mapping.parent.local_path,
4431
+ parent_s3_key: mapping.parent.s3_key,
4432
+ output_indexes: mapping.output_indexes
4433
+ })) : void 0
4434
+ },
4435
+ instructions: [
4436
+ "Use Codex image generation outside Lineage server code.",
4437
+ "Write generated output files under .asset-scratch before import.",
4438
+ "Do not call live provider APIs from the CLI or server.",
4439
+ "Fill every output_manifest entry with its generated file path and a one- or two-word edge summary.",
4440
+ "Import the completed manifest with --confirm-write to persist every lineage child."
4441
+ ],
4442
+ import_command: importCommand,
4443
+ output_manifest: outputManifest,
4444
+ guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
4445
+ };
4446
+ }
4132
4447
  function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
4133
4448
  const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
4134
4449
  return {
@@ -4157,6 +4472,19 @@ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
4157
4472
  guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
4158
4473
  };
4159
4474
  }
4475
+ function inputsFrom(jobIdValue, project, next) {
4476
+ return selectedParents(next).map((parent, position) => ({
4477
+ id: `${jobIdValue}:input:${position}`,
4478
+ job_id: jobIdValue,
4479
+ project_id: project,
4480
+ asset_id: parent.asset_id,
4481
+ root_asset_id: next.root_asset_id,
4482
+ role: "lineage_next_base",
4483
+ position,
4484
+ selection_strategy: next.strategy,
4485
+ selection_snapshot: next
4486
+ }));
4487
+ }
4160
4488
  function receiptFrom(row) {
4161
4489
  return {
4162
4490
  id: String(row.id),
@@ -4169,6 +4497,7 @@ function receiptFrom(row) {
4169
4497
  };
4170
4498
  }
4171
4499
  function outputFrom(row) {
4500
+ const edgeSummary = typeof row.edge_summary === "string" && row.edge_summary.length > 0 ? row.edge_summary : void 0;
4172
4501
  return {
4173
4502
  id: String(row.id),
4174
4503
  job_id: String(row.job_id),
@@ -4180,7 +4509,8 @@ function outputFrom(row) {
4180
4509
  content_type: String(row.content_type),
4181
4510
  imported_asset_id: String(row.imported_asset_id),
4182
4511
  parent_asset_id: String(row.parent_asset_id),
4183
- imported_at: String(row.imported_at)
4512
+ imported_at: String(row.imported_at),
4513
+ ...edgeSummary ? { edge_summary: edgeSummary } : {}
4184
4514
  };
4185
4515
  }
4186
4516
  function loadGenerationJob(database, project, id) {
@@ -4226,6 +4556,72 @@ function insertReceipt(database, id, type, command, payload) {
4226
4556
  values (?, ?, ?, 'ok', ?, ?, ?)
4227
4557
  `).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
4228
4558
  }
4559
+ function planImageGeneration(project = defaultProject, fields) {
4560
+ const prompt = fields.prompt.trim();
4561
+ if (!prompt) throw new GenerationReceiptError("Missing --prompt");
4562
+ if (!fields.fromLineageSelection) throw new GenerationReceiptError("Generation v1 requires --from-lineage-selection");
4563
+ const next = resolveLineageSelection(project);
4564
+ const parentCount = selectedParents(next).length;
4565
+ if (parentCount > 1 && !positiveInteger(fields.perBaseCount)) throw new GenerationReceiptError("Multi-parent generation requires --per-base-count");
4566
+ const perBaseCount = parentCount > 1 ? Number(fields.perBaseCount) : Number(fields.count ?? fields.perBaseCount);
4567
+ const count = parentCount * perBaseCount;
4568
+ if (!positiveInteger(perBaseCount)) throw new GenerationReceiptError("Generation count must be a positive integer");
4569
+ if (fields.count !== void 0 && fields.count !== count) throw new GenerationReceiptError(`Generation count mismatch: expected ${count} from selected bases, received ${fields.count}`);
4570
+ const id = jobId();
4571
+ const inputs = inputsFrom(id, project, next);
4572
+ const handoff = buildHandoff(project, id, prompt, count, perBaseCount, next, inputs);
4573
+ const mappings = parentMappings(next, perBaseCount).map((mapping) => ({ parent_asset_id: mapping.parent.asset_id, output_indexes: mapping.output_indexes }));
4574
+ const timestamp = nowIso();
4575
+ const preview2 = {
4576
+ id,
4577
+ project_id: project,
4578
+ provider,
4579
+ adapter_version: manifestAdapterVersion,
4580
+ source_mode: "lineage_selection",
4581
+ root_asset_id: next.root_asset_id,
4582
+ prompt,
4583
+ expected_output_count: count,
4584
+ status: "planned",
4585
+ output_dir: ".asset-scratch",
4586
+ handoff,
4587
+ created_at: timestamp,
4588
+ updated_at: timestamp,
4589
+ inputs,
4590
+ outputs: [],
4591
+ receipts: [{
4592
+ id: `${id}:receipt:plan:preview`,
4593
+ job_id: id,
4594
+ receipt_type: "plan",
4595
+ status: "ok",
4596
+ command: "generate image plan",
4597
+ payload: { prompt, expected_output_count: count, per_base_count: parentCount > 1 ? perBaseCount : void 0, lineage: handoff.lineage, parent_mappings: mappings },
4598
+ created_at: timestamp
4599
+ }]
4600
+ };
4601
+ if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview2 };
4602
+ const database = lineageDb();
4603
+ try {
4604
+ database.exec("BEGIN IMMEDIATE");
4605
+ try {
4606
+ database.prepare(`
4607
+ insert into generation_jobs (
4608
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
4609
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at
4610
+ ) values (?, ?, ?, ?, 'lineage_selection', ?, ?, ?, 'planned', ?, ?, ?, ?)
4611
+ `).run(id, project, provider, manifestAdapterVersion, next.root_asset_id, prompt, count, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4612
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)");
4613
+ 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));
4614
+ insertReceipt(database, id, "plan", "generate image plan", preview2.receipts[0].payload);
4615
+ database.exec("COMMIT");
4616
+ } catch (error) {
4617
+ database.exec("ROLLBACK");
4618
+ throw error;
4619
+ }
4620
+ return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
4621
+ } finally {
4622
+ database.close();
4623
+ }
4624
+ }
4229
4625
  function planImageReroll(project = defaultProject, fields) {
4230
4626
  const prompt = fields.prompt.trim();
4231
4627
  if (!prompt) throw new GenerationReceiptError("Missing --prompt");
@@ -4270,7 +4666,7 @@ function planImageReroll(project = defaultProject, fields) {
4270
4666
  id,
4271
4667
  project_id: project,
4272
4668
  provider,
4273
- adapter_version: adapterVersion,
4669
+ adapter_version: legacyAdapterVersion,
4274
4670
  source_mode: "lineage_reroll",
4275
4671
  root_asset_id: snapshot.root_asset_id,
4276
4672
  prompt,
@@ -4302,7 +4698,7 @@ function planImageReroll(project = defaultProject, fields) {
4302
4698
  id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
4303
4699
  expected_output_count, status, output_dir, handoff_json, created_at, updated_at
4304
4700
  ) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
4305
- `).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4701
+ `).run(id, project, provider, legacyAdapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4306
4702
  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));
4307
4703
  insertReceipt(database, id, "plan", "reroll plan", preview2.receipts[0].payload);
4308
4704
  database.exec("COMMIT");
@@ -4315,14 +4711,66 @@ function planImageReroll(project = defaultProject, fields) {
4315
4711
  database.close();
4316
4712
  }
4317
4713
  }
4714
+ function parentForOutput(job, outputIndex) {
4715
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
4716
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
4717
+ if (inputs.length === 1) return inputs[0].asset_id;
4718
+ if (job.expected_output_count % inputs.length !== 0) throw new GenerationReceiptError("Generation job has invalid parent mapping");
4719
+ return inputs[Math.floor(outputIndex / (job.expected_output_count / inputs.length))]?.asset_id || inputs[inputs.length - 1].asset_id;
4720
+ }
4721
+ function parentInputs(job) {
4722
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
4723
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
4724
+ return inputs;
4725
+ }
4726
+ function parentFilesFor(job, parentFiles) {
4727
+ const inputs = parentInputs(job);
4728
+ const expectedPerParent = job.expected_output_count / inputs.length;
4729
+ if (!Number.isInteger(expectedPerParent)) throw new GenerationReceiptError("Generation job has invalid parent mapping");
4730
+ const allowedParents = new Set(inputs.map((input) => input.asset_id));
4731
+ const seenParents = /* @__PURE__ */ new Set();
4732
+ const mapped = [];
4733
+ for (const parentAssetId of Object.keys(parentFiles)) {
4734
+ if (!allowedParents.has(parentAssetId)) throw new GenerationReceiptError(`Unknown generation parent mapping: ${parentAssetId}`);
4735
+ if (seenParents.has(parentAssetId)) throw new GenerationReceiptError(`Duplicate generation parent mapping: ${parentAssetId}`);
4736
+ seenParents.add(parentAssetId);
4737
+ }
4738
+ for (const input of inputs) {
4739
+ const files = (parentFiles[input.asset_id] || []).map((file) => file.trim()).filter(Boolean);
4740
+ if (files.length === 0) throw new GenerationReceiptError(`Missing generation parent mapping for ${input.asset_id}`);
4741
+ if (files.length !== expectedPerParent) throw new GenerationReceiptError(`Parent ${input.asset_id} requires ${expectedPerParent} output file${expectedPerParent === 1 ? "" : "s"}, received ${files.length}`);
4742
+ for (const file of files) mapped.push({ file, parentAssetId: input.asset_id });
4743
+ }
4744
+ return mapped;
4745
+ }
4746
+ function orderedFilesFor(job, files) {
4747
+ return files.map((file, index) => ({ file, parentAssetId: parentForOutput(job, index) }));
4748
+ }
4749
+ function inspectImageGeneration(project = defaultProject, jobIdValue) {
4750
+ if (!jobIdValue) throw new GenerationReceiptError("Missing --job-id");
4751
+ const database = lineageDb();
4752
+ try {
4753
+ return { ok: true, command: "generate image inspect", project, job: loadGenerationJob(database, project, jobIdValue) };
4754
+ } finally {
4755
+ database.close();
4756
+ }
4757
+ }
4318
4758
  function isPathInside(child, parent) {
4319
4759
  const rel = relative3(parent, child);
4320
4760
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
4321
4761
  }
4322
- function resolveScratchFile(file) {
4762
+ function scratchCandidate(file) {
4323
4763
  const scratchRoot = resolve5(repoRoot, ".asset-scratch");
4324
4764
  const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
4325
4765
  if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
4766
+ return { candidate, scratchRoot };
4767
+ }
4768
+ function resolveScratchManifestPath(file) {
4769
+ const { candidate, scratchRoot } = scratchCandidate(file);
4770
+ return relative3(scratchRoot, candidate);
4771
+ }
4772
+ function resolveScratchFile(file) {
4773
+ const { candidate, scratchRoot } = scratchCandidate(file);
4326
4774
  if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
4327
4775
  const realScratchRoot = realpathSync2(scratchRoot);
4328
4776
  const realCandidate = realpathSync2(candidate);
@@ -4338,6 +4786,165 @@ function resolveScratchFile(file) {
4338
4786
  assetId: `local-${checksum.slice(0, 12)}`
4339
4787
  };
4340
4788
  }
4789
+ function generationManifestConflict() {
4790
+ throw new GenerationReceiptError("Generation import already exists with different output, summary, or provenance", 409);
4791
+ }
4792
+ function confirmManifestRetry(project, job, manifest) {
4793
+ if (job.outputs.length !== manifest.outputs.length) generationManifestConflict();
4794
+ const database = lineageDb();
4795
+ try {
4796
+ for (const expected of manifest.outputs) {
4797
+ const recorded = job.outputs.find((output) => output.output_index === expected.output_index);
4798
+ if (!recorded || recorded.file_path !== expected.file_path || recorded.parent_asset_id !== expected.parent_asset_id || recorded.edge_summary !== expected.edge_summary) generationManifestConflict();
4799
+ const edge = database.prepare(`
4800
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
4801
+ from asset_edges
4802
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
4803
+ `).get(project, recorded.parent_asset_id, recorded.imported_asset_id);
4804
+ 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();
4805
+ }
4806
+ return { ok: true, command: "generate image import", project, job, imported: job.outputs, idempotent: true };
4807
+ } finally {
4808
+ database.close();
4809
+ }
4810
+ }
4811
+ function importImageGenerationOutputs(project = defaultProject, fields) {
4812
+ if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
4813
+ if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
4814
+ const database = lineageDb();
4815
+ let job;
4816
+ try {
4817
+ job = loadGenerationJob(database, project, fields.jobId);
4818
+ } finally {
4819
+ database.close();
4820
+ }
4821
+ if (job.source_mode !== "lineage_selection") throw new GenerationReceiptError(`Generation job is not an image-selection job: ${job.source_mode}`);
4822
+ const hasManifestInput = fields.manifest !== void 0;
4823
+ const hasParentFilesInput = fields.parentFiles !== void 0;
4824
+ const hasFilesInput = fields.files !== void 0;
4825
+ if (hasManifestInput && (hasParentFilesInput || hasFilesInput)) {
4826
+ throw new GenerationReceiptError("Use --manifest or legacy --files/--parent-files, not both");
4827
+ }
4828
+ if (hasParentFilesInput && hasFilesInput) throw new GenerationReceiptError("Use --files or --parent-files, not both");
4829
+ let manifest;
4830
+ let parentFileRows;
4831
+ let mappingStrategy;
4832
+ if (job.adapter_version === manifestAdapterVersion) {
4833
+ if (!hasManifestInput) throw new GenerationReceiptError("New generation jobs require --manifest");
4834
+ manifest = parseGenerationOutputManifest(fields.manifest, job, { resolveFilePath: resolveScratchManifestPath });
4835
+ if (job.status === "imported") return confirmManifestRetry(project, job, manifest);
4836
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
4837
+ parentFileRows = manifest.outputs.map((output) => ({
4838
+ edgeSummary: output.edge_summary,
4839
+ file: output.file_path,
4840
+ parentAssetId: output.parent_asset_id
4841
+ }));
4842
+ mappingStrategy = "generation_output_manifest_v1";
4843
+ } else if (job.adapter_version === legacyAdapterVersion) {
4844
+ if (hasManifestInput) throw new GenerationReceiptError("Already-planned legacy generation jobs require --files or --parent-files");
4845
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
4846
+ const hasExplicitParentFiles = Boolean(fields.parentFiles && Object.keys(fields.parentFiles).length > 0);
4847
+ parentFileRows = hasExplicitParentFiles ? parentFilesFor(job, fields.parentFiles || {}) : orderedFilesFor(job, (fields.files || []).map((file) => file.trim()).filter(Boolean));
4848
+ mappingStrategy = hasExplicitParentFiles ? "explicit_parent_files" : "ordered_per_base";
4849
+ } else {
4850
+ throw new GenerationReceiptError(`Unsupported generation adapter version: ${job.adapter_version}`);
4851
+ }
4852
+ if (parentFileRows.length === 0) throw new GenerationReceiptError("Generation import requires --files or --parent-files");
4853
+ if (parentFileRows.length !== job.expected_output_count) {
4854
+ throw new GenerationReceiptError(`Output count mismatch: expected ${job.expected_output_count}, received ${parentFileRows.length}`);
4855
+ }
4856
+ const resolved = parentFileRows.map((row) => ({ ...resolveScratchFile(row.file), edgeSummary: row.edgeSummary, parentAssetId: row.parentAssetId }));
4857
+ const uniquePaths = new Set(resolved.map((file) => file.relativePath));
4858
+ if (uniquePaths.size !== resolved.length) throw new GenerationReceiptError("Generation import files must be unique");
4859
+ cancelLineageIterateTasksForAssets(project, {
4860
+ actor: "system",
4861
+ confirmWrite: false,
4862
+ rootAssetId: job.root_asset_id
4863
+ });
4864
+ indexLineageAssets(project);
4865
+ const writeDb = lineageDb();
4866
+ try {
4867
+ const timestamp = nowIso();
4868
+ writeDb.exec("BEGIN IMMEDIATE");
4869
+ try {
4870
+ for (const [index, file] of resolved.entries()) {
4871
+ const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, file.assetId);
4872
+ if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${file.relativePath}`);
4873
+ const outputId = `${fields.jobId}:output:${index}`;
4874
+ writeDb.prepare(`insert into generation_job_outputs (
4875
+ id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type,
4876
+ imported_asset_id, parent_asset_id, imported_at, edge_summary
4877
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
4878
+ outputId,
4879
+ fields.jobId,
4880
+ project,
4881
+ index,
4882
+ file.relativePath,
4883
+ file.checksum,
4884
+ file.size,
4885
+ file.contentType,
4886
+ file.assetId,
4887
+ file.parentAssetId,
4888
+ timestamp,
4889
+ file.edgeSummary || null
4890
+ );
4891
+ const insertedEdge = writeDb.prepare(`insert into asset_edges (
4892
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
4893
+ summary, summary_created_by, summary_updated_by, summary_updated_at
4894
+ ) values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
4895
+ on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing`).run(
4896
+ `${project}:${file.parentAssetId}:derived_from:${file.assetId}`,
4897
+ project,
4898
+ file.parentAssetId,
4899
+ file.assetId,
4900
+ timestamp,
4901
+ file.edgeSummary || null,
4902
+ file.edgeSummary ? "agent" : null,
4903
+ file.edgeSummary ? "agent" : null,
4904
+ file.edgeSummary ? timestamp : null
4905
+ );
4906
+ if (file.edgeSummary && insertedEdge.changes === 0) {
4907
+ const edge = writeDb.prepare(`
4908
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
4909
+ from asset_edges
4910
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
4911
+ `).get(project, file.parentAssetId, file.assetId);
4912
+ 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();
4913
+ }
4914
+ }
4915
+ writeDb.prepare(`
4916
+ update generation_jobs
4917
+ set status = 'imported', imported_at = ?, updated_at = ?
4918
+ where project_id = ? and id = ?
4919
+ `).run(timestamp, timestamp, project, fields.jobId);
4920
+ insertReceipt(writeDb, fields.jobId, "import", "generate image import", {
4921
+ mapping_strategy: mappingStrategy,
4922
+ files: resolved.map((file, index) => ({
4923
+ output_index: index,
4924
+ file_path: file.relativePath,
4925
+ imported_asset_id: file.assetId,
4926
+ parent_asset_id: file.parentAssetId,
4927
+ ...file.edgeSummary ? { edge_summary: file.edgeSummary } : {}
4928
+ })),
4929
+ selection_reset: { root_asset_id: job.root_asset_id, cleared: true }
4930
+ });
4931
+ writeDb.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, job.root_asset_id);
4932
+ writeDb.exec("COMMIT");
4933
+ } catch (error) {
4934
+ writeDb.exec("ROLLBACK");
4935
+ throw error;
4936
+ }
4937
+ cancelLineageIterateTasksForAssets(project, {
4938
+ actor: "system",
4939
+ confirmWrite: true,
4940
+ rootAssetId: job.root_asset_id
4941
+ });
4942
+ const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
4943
+ return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
4944
+ } finally {
4945
+ writeDb.close();
4946
+ }
4947
+ }
4341
4948
  function importImageRerollOutput(project = defaultProject, fields) {
4342
4949
  if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
4343
4950
  if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
@@ -5241,22 +5848,23 @@ var ManagedWriterRoutingError = class extends Error {
5241
5848
  status;
5242
5849
  };
5243
5850
  function managedWriterTimeoutMs(command, args) {
5244
- const subcommand = args.find((arg) => !arg.startsWith("-")) || "";
5245
- return command === "reroll" && subcommand === "import" ? 5 * 6e4 : 3e4;
5851
+ const positions = args.filter((arg) => !arg.startsWith("-"));
5852
+ const slowImport = command === "reroll" && positions[0] === "import" || command === "generate" && positions[0] === "image" && positions[1] === "import";
5853
+ return slowImport ? 5 * 6e4 : 3e4;
5246
5854
  }
5247
5855
  function errorMessage(body, fallback) {
5248
5856
  if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
5249
- const record = body;
5250
- if (typeof record.message === "string") return record.message;
5251
- if (typeof record.error === "string") return record.error;
5857
+ const record2 = body;
5858
+ if (typeof record2.message === "string") return record2.message;
5859
+ if (typeof record2.error === "string") return record2.error;
5252
5860
  return fallback;
5253
5861
  }
5254
5862
  function agentClaimError(body, status) {
5255
5863
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
5256
- const record = body;
5257
- if (typeof record.message !== "string" || typeof record.error !== "string") return void 0;
5258
- const conflicts = Array.isArray(record.conflicts) ? record.conflicts : [];
5259
- return new AgentClaimError(record.message, status, record.error, conflicts);
5864
+ const record2 = body;
5865
+ if (typeof record2.message !== "string" || typeof record2.error !== "string") return void 0;
5866
+ const conflicts = Array.isArray(record2.conflicts) ? record2.conflicts : [];
5867
+ return new AgentClaimError(record2.message, status, record2.error, conflicts);
5260
5868
  }
5261
5869
  function assertResponseIdentity(body, profile, channel) {
5262
5870
  if (!body || typeof body !== "object" || Array.isArray(body)) {
@@ -5354,6 +5962,26 @@ function readOptions(args, name) {
5354
5962
  }
5355
5963
  return values;
5356
5964
  }
5965
+ function readJsonFile(path, label) {
5966
+ try {
5967
+ return JSON.parse(readFileSync6(resolve8(path), "utf8"));
5968
+ } catch (error) {
5969
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
5970
+ }
5971
+ }
5972
+ function parentFilesOption(value) {
5973
+ if (value === void 0) return void 0;
5974
+ const mappings = {};
5975
+ for (const rawMapping of value.split(";")) {
5976
+ const separator = rawMapping.indexOf("=");
5977
+ const parent = separator >= 0 ? rawMapping.slice(0, separator).trim() : "";
5978
+ const files = separator >= 0 ? rawMapping.slice(separator + 1).split(",").map((file) => file.trim()).filter(Boolean) : [];
5979
+ if (!parent || files.length === 0) throw new Error("lineage generate image import --parent-files requires parent=file[,file][;parent=file]");
5980
+ if (Object.hasOwn(mappings, parent)) throw new Error(`Duplicate generation parent mapping: ${parent}`);
5981
+ mappings[parent] = files;
5982
+ }
5983
+ return mappings;
5984
+ }
5357
5985
  function resolveStartOptions(config, args) {
5358
5986
  const profile = prepareCliProfile(config, args);
5359
5987
  const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
@@ -5446,7 +6074,11 @@ Usage:
5446
6074
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
5447
6075
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
5448
6076
  ${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--schema v2] [--out <path>] [--strict] [--db <path>] [--json]
5449
- ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
6077
+ ${config.binName} link-child --root <asset-id> --child <asset-id> --summary "<one-or-two-words>" [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
6078
+ ${config.binName} generate image plan --prompt <text> --from-lineage-selection [--count <count>|--per-base-count <count>] [--project <project>] [--dry-run] [--db <path>] [--json]
6079
+ ${config.binName} generate image inspect --job-id <job-id> [--project <project>] [--db <path>] [--json]
6080
+ ${config.binName} generate image import --job-id <job-id> --manifest <json-file> --confirm-write [--project <project>] [--db <path>] [--json]
6081
+ ${config.binName} generate image import --job-id <legacy-job-id> (--files <file,file>|--parent-files <parent=file;parent=file>) --confirm-write [--project <project>] [--db <path>] [--json]
5450
6082
  ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
5451
6083
  ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
5452
6084
  ${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
@@ -5568,7 +6200,8 @@ function resolveDataCommandOptions(args) {
5568
6200
  dbPath: readOption(args, "--db"),
5569
6201
  json: args.includes("--json"),
5570
6202
  project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
5571
- rootAssetId: readOption(args, "--root")
6203
+ rootAssetId: readOption(args, "--root"),
6204
+ summary: readOption(args, "--summary")
5572
6205
  };
5573
6206
  if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
5574
6207
  return options;
@@ -5611,13 +6244,57 @@ function runLineageDataCommand(command, args) {
5611
6244
  }
5612
6245
  if (command === "link-child") {
5613
6246
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
6247
+ const summary = requireEdgeSummary(options.summary);
5614
6248
  return linkSelectedLineageChild(options.project, {
5615
6249
  childAssetId: options.childAssetId,
5616
6250
  claimToken: options.claimToken,
5617
6251
  confirmWrite: options.confirmWrite,
5618
- rootAssetId: options.rootAssetId || options.assetId
6252
+ rootAssetId: options.rootAssetId || options.assetId,
6253
+ summary,
6254
+ summaryActor: "agent"
5619
6255
  });
5620
6256
  }
6257
+ if (command === "generate") {
6258
+ const [resource, subcommand] = positionalArgs(args);
6259
+ if (resource !== "image") throw new Error(`Unknown generate command: ${resource}`);
6260
+ if (subcommand === "plan") {
6261
+ const prompt = readOption(args, "--prompt");
6262
+ const rawCount = readOption(args, "--count");
6263
+ const rawPerBaseCount = readOption(args, "--per-base-count");
6264
+ if (!prompt) throw new Error("lineage generate image plan requires --prompt");
6265
+ return planImageGeneration(options.project, {
6266
+ count: rawCount === void 0 ? void 0 : Number(rawCount),
6267
+ dryRun: args.includes("--dry-run"),
6268
+ fromLineageSelection: args.includes("--from-lineage-selection"),
6269
+ perBaseCount: rawPerBaseCount === void 0 ? void 0 : Number(rawPerBaseCount),
6270
+ prompt
6271
+ });
6272
+ }
6273
+ if (subcommand === "inspect") {
6274
+ const jobId2 = readOption(args, "--job-id");
6275
+ if (!jobId2) throw new Error("lineage generate image inspect requires --job-id");
6276
+ return inspectImageGeneration(options.project, jobId2);
6277
+ }
6278
+ if (subcommand === "import") {
6279
+ const jobId2 = readOption(args, "--job-id");
6280
+ const manifestPath = readOption(args, "--manifest");
6281
+ const filesValue = readOption(args, "--files");
6282
+ const parentFilesValue = readOption(args, "--parent-files");
6283
+ if (!jobId2) throw new Error("lineage generate image import requires --job-id");
6284
+ if (manifestPath !== void 0 && (filesValue !== void 0 || parentFilesValue !== void 0)) {
6285
+ throw new Error("Use --manifest or legacy --files/--parent-files, not both");
6286
+ }
6287
+ if (filesValue !== void 0 && parentFilesValue !== void 0) throw new Error("Use --files or --parent-files, not both");
6288
+ return importImageGenerationOutputs(options.project, {
6289
+ confirmWrite: options.confirmWrite,
6290
+ files: filesValue === void 0 ? void 0 : filesValue.split(",").map((file) => file.trim()).filter(Boolean),
6291
+ jobId: jobId2,
6292
+ manifest: manifestPath === void 0 ? void 0 : readJsonFile(manifestPath, "Generation output manifest"),
6293
+ parentFiles: parentFilesOption(parentFilesValue)
6294
+ });
6295
+ }
6296
+ throw new Error(`Unknown generate image command: ${subcommand}`);
6297
+ }
5621
6298
  if (command === "reroll") {
5622
6299
  const subcommand = positionalArgs(args)[0] || "";
5623
6300
  if (subcommand === "list") {
@@ -5771,6 +6448,15 @@ function printDataResult(command, result, json) {
5771
6448
  if (link.warning) console.log(`Warning: ${link.warning}`);
5772
6449
  return;
5773
6450
  }
6451
+ if (command === "generate" && result && typeof result === "object" && "job" in result) {
6452
+ const generation = result;
6453
+ if (generation.imported) {
6454
+ console.log(`${generation.idempotent ? "Already imported" : "Imported"} ${generation.imported.length} output(s) for ${generation.job?.id || "job"}`);
6455
+ } else {
6456
+ console.log(`Generation job ${generation.job?.id || "job"} ${generation.job?.status || "unknown"}`);
6457
+ }
6458
+ return;
6459
+ }
5774
6460
  if (command === "reroll" && result && typeof result === "object") {
5775
6461
  if ("requests" in result) {
5776
6462
  const listed = result;
@@ -5916,7 +6602,9 @@ function printRuntimeResult(result, json) {
5916
6602
  }
5917
6603
  function lineageCliRequiresWriterLease(command, args) {
5918
6604
  if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
5919
- const subcommand = positionalArgs(args)[0] || "";
6605
+ const positions = positionalArgs(args);
6606
+ const subcommand = positions[0] || "";
6607
+ if (command === "generate") return positions[0] !== "image" || positions[1] !== "inspect";
5920
6608
  if (command === "reroll") return subcommand !== "list";
5921
6609
  if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
5922
6610
  if (command === "db") return subcommand !== "info";
@@ -6256,7 +6944,7 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
6256
6944
  else console.error(`${config.binName}: ${message2}`);
6257
6945
  process.exit(1);
6258
6946
  }
6259
- if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
6947
+ if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "generate" || command === "reroll" || command === "tasks") {
6260
6948
  const commandArgs = normalizedArgs.slice(1);
6261
6949
  const json2 = commandArgs.includes("--json");
6262
6950
  try {