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