@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.
- package/CHANGELOG.md +13 -0
- package/README.md +249 -19
- package/dist/cli/lineage-channel.js +25 -5
- package/dist/cli/lineage-channel.js.map +2 -2
- package/dist/cli/lineage-dev.js +891 -52
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage-preview.js +891 -52
- package/dist/cli/lineage-preview.js.map +4 -4
- package/dist/cli/lineage.js +891 -52
- package/dist/cli/lineage.js.map +4 -4
- package/dist/cli/managed-service.js +19 -5
- package/dist/cli/managed-service.js.map +2 -2
- package/dist/runtime-build.json +4 -4
- package/dist/server.js +883 -89
- package/dist/server.js.map +4 -4
- package/dist/web/assets/agent-shared-state-DOfpTLiw.webp +0 -0
- package/dist/web/assets/agent-to-canvas-DtIu_cPq.mp4 +0 -0
- package/dist/web/assets/{app-CDxSvoD3.css → app-B-fAyNsU.css} +1 -1
- package/dist/web/assets/app-DMogkUoS.js +16 -0
- package/dist/web/assets/attempt-history-D6wdic6n.webp +0 -0
- package/dist/web/assets/branching-tree-B8I5_S6D.png +0 -0
- package/dist/web/assets/canvas-cli-DHBacq7r.png +0 -0
- package/dist/web/assets/hero-agent-sync-CSyh0tHy.mp4 +0 -0
- package/dist/web/assets/hero-board-DG3ED4AW.webp +0 -0
- package/dist/web/assets/hero-lineage-growth-Bsl9tiq-.mp4 +0 -0
- package/dist/web/assets/hero-trace-connections-Bb52ABBc.mp4 +0 -0
- package/dist/web/assets/human-selection-BdDBDE-I.webp +0 -0
- package/dist/web/assets/human-to-agent-D1z92ZO9.mp4 +0 -0
- package/dist/web/assets/{jsx-runtime-_Rdg6et1.js → jsx-runtime-DAFSxiwi.js} +1 -1
- package/dist/web/assets/landing-Bn4qKbIO.css +1 -0
- package/dist/web/assets/landing-CoyGmxBo.js +1 -0
- package/dist/web/assets/reroll-history-DQcEH42R.mp4 +0 -0
- package/dist/web/index.html +3 -3
- package/dist/web/landing/index.html +3 -3
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +13 -13
- package/package.json +4 -3
- package/dist/web/assets/app-CYUgf6rX.js +0 -16
- package/dist/web/assets/landing-D3Y679_B.css +0 -1
- 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(
|
|
353
|
-
const value =
|
|
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(
|
|
358
|
-
const value =
|
|
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
|
|
410
|
-
const schemaVersion = requiredString(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
437
|
-
database_path: resolveManifestPath(manifestPath, requiredString(
|
|
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(
|
|
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(
|
|
1647
|
-
return [.../* @__PURE__ */ new Set([
|
|
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((
|
|
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((
|
|
1701
|
-
const ids = recordAssets.get(
|
|
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 [
|
|
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:
|
|
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 (
|
|
3717
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
4596
|
-
return `${project}:${
|
|
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,
|
|
4844
|
+
function upsertSource(database, project, record2, sourceType, asset, key, context = {}) {
|
|
4652
4845
|
const timestamp = context.seenAt || nowIso();
|
|
4653
|
-
const id = sourceId(project,
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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(
|
|
5025
|
+
function toRecord(record2, sources) {
|
|
4833
5026
|
return {
|
|
4834
|
-
id:
|
|
4835
|
-
project:
|
|
4836
|
-
canonical_asset_id:
|
|
4837
|
-
checksum_sha256:
|
|
4838
|
-
media_type:
|
|
4839
|
-
title:
|
|
4840
|
-
status:
|
|
4841
|
-
channel:
|
|
4842
|
-
campaign:
|
|
4843
|
-
audience:
|
|
4844
|
-
updated_at:
|
|
4845
|
-
first_seen_at:
|
|
4846
|
-
last_seen_at:
|
|
4847
|
-
indexed_by_run_id:
|
|
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(
|
|
4882
|
-
return new Set(
|
|
5074
|
+
function sourceTypes(record2) {
|
|
5075
|
+
return new Set(record2.sources.map((source) => source.source_type));
|
|
4883
5076
|
}
|
|
4884
|
-
function matchesStorage(
|
|
4885
|
-
const types = sourceTypes(
|
|
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(
|
|
4893
|
-
if (placement === "not-scheduled") return
|
|
4894
|
-
if (placement === "not-posted") return !
|
|
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
|
|
5089
|
+
return record2.workflow.placements.some((item) => item.status === placement);
|
|
4897
5090
|
}
|
|
4898
|
-
function matchesSelection(
|
|
4899
|
-
if (selection === "selected") return Boolean(
|
|
4900
|
-
if (selection === "not-selected") return !
|
|
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((
|
|
4910
|
-
if (!matchesStorage(
|
|
4911
|
-
if (review !== "all" && (
|
|
4912
|
-
if (!matchesPlacement(
|
|
4913
|
-
if (!matchesSelection(
|
|
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
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
...
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
}));
|