@mean-weasel/lineage 0.1.5 → 0.1.7
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 +14 -0
- package/README.md +35 -2
- package/dist/cli/lineage-dev.js +1873 -46
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1873 -46
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +1960 -569
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-DkLQJqxa.css → index-38XlcMya.css} +1 -1
- package/dist/web/assets/index-CtsYIUyG.js +23 -0
- package/dist/web/index.html +2 -2
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-drill-mint-diagonal-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v3.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +38 -1
- package/package.json +3 -1
- package/dist/web/assets/index-C54JeMzP.js +0 -22
package/dist/cli/lineage.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/lineageCli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
5
5
|
import { homedir, platform } from "node:os";
|
|
6
|
-
import { dirname as dirname3, join as
|
|
6
|
+
import { dirname as dirname3, join as join6, resolve as resolve4 } from "node:path";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
9
|
|
|
@@ -40,12 +40,92 @@ var mimeByExt = {
|
|
|
40
40
|
".webp": "image/webp"
|
|
41
41
|
};
|
|
42
42
|
var localReviewExts = new Set(Object.keys(mimeByExt));
|
|
43
|
+
var channelFromName = /\b(linkedin|meta|tiktok|youtube|x-twitter|x)\b/i;
|
|
44
|
+
var campaignFromPath = /\b(20\d{2}-\d{2}-[a-z0-9-]+)\b/i;
|
|
45
|
+
function localReviewRoot(repoRoot2) {
|
|
46
|
+
return join(repoRoot2, ".asset-scratch");
|
|
47
|
+
}
|
|
43
48
|
function contentTypeFor(file) {
|
|
44
49
|
return mimeByExt[extname(file).toLowerCase()] || "application/octet-stream";
|
|
45
50
|
}
|
|
46
51
|
function fileSha256(file) {
|
|
47
52
|
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
48
53
|
}
|
|
54
|
+
function walkLocalReviewFiles(dir, files = []) {
|
|
55
|
+
if (!existsSync(dir)) return files;
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === "ENOENT") return files;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "studio-uploads" || entry.name === "playwright-results" || entry.name === "lineage-demo") continue;
|
|
65
|
+
if (process.env.NODE_ENV !== "test" && /^vitest-/.test(entry.name)) continue;
|
|
66
|
+
const path = join(dir, entry.name);
|
|
67
|
+
if (entry.isDirectory()) walkLocalReviewFiles(path, files);
|
|
68
|
+
else if (entry.isFile() && localReviewExts.has(extname(entry.name).toLowerCase())) files.push(path);
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
function inferCampaign(relativePath) {
|
|
73
|
+
return campaignFromPath.exec(relativePath)?.[1] || "local-review";
|
|
74
|
+
}
|
|
75
|
+
function inferChannel(relativePath) {
|
|
76
|
+
const match = channelFromName.exec(relativePath);
|
|
77
|
+
if (!match) return "local";
|
|
78
|
+
return match[1].toLowerCase() === "x" ? "x-twitter" : match[1].toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
function inferContentType(file) {
|
|
81
|
+
const mime = contentTypeFor(file);
|
|
82
|
+
if (mime.startsWith("image/gif")) return "gif";
|
|
83
|
+
if (mime.startsWith("image/")) return "image";
|
|
84
|
+
if (mime.startsWith("video/")) return "video";
|
|
85
|
+
return "other";
|
|
86
|
+
}
|
|
87
|
+
function listLocalReviewAssets(repoRoot2, project, catalog) {
|
|
88
|
+
const catalogChecksums = new Set(catalog.assets.map((asset) => asset.s3?.checksum_sha256).filter(Boolean));
|
|
89
|
+
const root = localReviewRoot(repoRoot2);
|
|
90
|
+
return walkLocalReviewFiles(root).flatMap((file) => {
|
|
91
|
+
try {
|
|
92
|
+
const stats = statSync(file);
|
|
93
|
+
const checksum = fileSha256(file);
|
|
94
|
+
const relativePath = relative(root, file);
|
|
95
|
+
return [{ checksum, file, relativePath, stats }];
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === "ENOENT") return [];
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}).filter((item) => !catalogChecksums.has(item.checksum)).map((item) => {
|
|
101
|
+
const fileSlug = basename(item.file, extname(item.file));
|
|
102
|
+
const channel = inferChannel(item.relativePath);
|
|
103
|
+
return {
|
|
104
|
+
asset_id: `local-${item.checksum.slice(0, 12)}`,
|
|
105
|
+
project,
|
|
106
|
+
product: project,
|
|
107
|
+
source: "local",
|
|
108
|
+
campaign: inferCampaign(item.relativePath),
|
|
109
|
+
channel,
|
|
110
|
+
audience: "local-review",
|
|
111
|
+
status: "planned",
|
|
112
|
+
content_type: inferContentType(item.file),
|
|
113
|
+
title: fileSlug.replace(/[-_]+/g, " "),
|
|
114
|
+
hook: item.relativePath,
|
|
115
|
+
cta: "Review before upload",
|
|
116
|
+
utm_content: fileSlug.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase() || "local_review",
|
|
117
|
+
notes: "Local pre-push asset. Review and refine before uploading to S3.",
|
|
118
|
+
local: {
|
|
119
|
+
relative_path: item.relativePath,
|
|
120
|
+
absolute_path: item.file,
|
|
121
|
+
size_bytes: item.stats.size,
|
|
122
|
+
content_type: contentTypeFor(item.file),
|
|
123
|
+
checksum_sha256: item.checksum,
|
|
124
|
+
updated_at: item.stats.mtime.toISOString()
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
49
129
|
|
|
50
130
|
// src/server/adapters/storage/s3StorageAdapter.ts
|
|
51
131
|
function parseAssetIdFromS3Key(key) {
|
|
@@ -227,6 +307,7 @@ var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
|
|
|
227
307
|
var publicFallbackBucket = "lineage-demo-assets";
|
|
228
308
|
var publicFallbackRegion = "us-east-1";
|
|
229
309
|
var contentTypes = /* @__PURE__ */ new Set(["image", "video", "gif", "audio", "doc", "other"]);
|
|
310
|
+
var baseChannels = ["linkedin", "meta", "tiktok", "x-twitter", "youtube"];
|
|
230
311
|
var projectNamePattern = /^[a-z0-9][a-z0-9-]*$/;
|
|
231
312
|
var LineageAssetError = class extends Error {
|
|
232
313
|
constructor(message, status = 400) {
|
|
@@ -360,6 +441,9 @@ function defaultFallbackCatalog() {
|
|
|
360
441
|
project: defaultProject
|
|
361
442
|
}, defaultProject);
|
|
362
443
|
}
|
|
444
|
+
function isDefaultFallbackCatalog(catalog) {
|
|
445
|
+
return catalog.project === defaultProject && !existsSync3(catalogPath(defaultProject));
|
|
446
|
+
}
|
|
363
447
|
function loadCatalog(project = defaultProject) {
|
|
364
448
|
const path = catalogPath(project);
|
|
365
449
|
if (existsSync3(path)) {
|
|
@@ -425,6 +509,67 @@ var storageAdapter = createS3StorageAdapter({
|
|
|
425
509
|
saveCatalog,
|
|
426
510
|
supportedContentTypes: contentTypes
|
|
427
511
|
});
|
|
512
|
+
function uniqueSorted(values) {
|
|
513
|
+
return Array.from(new Set(values.filter(Boolean))).sort();
|
|
514
|
+
}
|
|
515
|
+
function facetsFor(assets) {
|
|
516
|
+
return {
|
|
517
|
+
audiences: uniqueSorted(assets.map((asset) => asset.audience)),
|
|
518
|
+
campaigns: uniqueSorted(assets.map((asset) => asset.campaign)),
|
|
519
|
+
channels: uniqueSorted([...baseChannels, ...assets.map((asset) => asset.channel)]),
|
|
520
|
+
contentTypes: uniqueSorted(assets.map((asset) => asset.content_type)),
|
|
521
|
+
placementStatuses: uniqueSorted(assets.flatMap((asset) => asset.placements?.map((placement) => placement.status) || [])),
|
|
522
|
+
statuses: uniqueSorted(assets.map((asset) => asset.status)),
|
|
523
|
+
totalSizeBytes: assets.reduce((sum, asset) => sum + (asset.s3?.size_bytes || 0), 0)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function filteredAssets(assets, options) {
|
|
527
|
+
const query = options.query?.trim().toLowerCase();
|
|
528
|
+
return assets.filter((asset) => {
|
|
529
|
+
if (options.status && options.status !== "all" && asset.status !== options.status) return false;
|
|
530
|
+
if (options.channel && options.channel !== "all" && asset.channel !== options.channel) return false;
|
|
531
|
+
if (options.type && options.type !== "all" && asset.content_type !== options.type) return false;
|
|
532
|
+
if (options.placementStatus === "not-posted" && asset.placements?.some((placement) => placement.status === "posted")) return false;
|
|
533
|
+
if (options.placementStatus && !["all", "not-posted"].includes(options.placementStatus) && !asset.placements?.some((placement) => placement.status === options.placementStatus)) return false;
|
|
534
|
+
if (options.campaign && options.campaign !== "all" && asset.campaign !== options.campaign) return false;
|
|
535
|
+
if (options.audience && options.audience !== "all" && asset.audience !== options.audience) return false;
|
|
536
|
+
if (!query) return true;
|
|
537
|
+
return [asset.asset_id, asset.title, asset.campaign, asset.channel, asset.audience, asset.hook, asset.cta].join(" ").toLowerCase().includes(query);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function listAssets(project = defaultProject, options = {}) {
|
|
541
|
+
const catalog = loadCatalog(project);
|
|
542
|
+
const pageSize = Math.min(Math.max(Number(options.pageSize || 10), 1), 100);
|
|
543
|
+
const page = Math.max(Number(options.page || 1), 1);
|
|
544
|
+
const localAssets = listLocalReviewAssets(repoRoot, catalog.project, catalog);
|
|
545
|
+
const source = options.source || "catalog";
|
|
546
|
+
const sourceAssets = source === "local" ? localAssets : source === "all" ? [...localAssets, ...catalog.assets] : catalog.assets;
|
|
547
|
+
const filtered = filteredAssets(sourceAssets, options);
|
|
548
|
+
const totalPages = Math.max(Math.ceil(filtered.length / pageSize), 1);
|
|
549
|
+
const safePage = Math.min(page, totalPages);
|
|
550
|
+
const start2 = (safePage - 1) * pageSize;
|
|
551
|
+
const pageAssets = filtered.slice(start2, start2 + pageSize);
|
|
552
|
+
let liveObjects = [];
|
|
553
|
+
let error;
|
|
554
|
+
if (options.includeLive && !isDefaultFallbackCatalog(catalog)) {
|
|
555
|
+
try {
|
|
556
|
+
liveObjects = storageAdapter.listLiveObjects(catalog);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
error = err instanceof Error ? err.message : String(err);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
catalog: { project: catalog.project, product: catalog.product, default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length },
|
|
563
|
+
assets: pageAssets,
|
|
564
|
+
facets: facetsFor(catalog.assets),
|
|
565
|
+
pagination: { page: safePage, pageSize, total: filtered.length, totalPages },
|
|
566
|
+
liveObjects,
|
|
567
|
+
orphanObjects: liveObjects.filter((object) => !object.cataloged),
|
|
568
|
+
identity: options.includeLive && !isDefaultFallbackCatalog(catalog) ? storageAdapter.getIdentity() : void 0,
|
|
569
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
570
|
+
error
|
|
571
|
+
};
|
|
572
|
+
}
|
|
428
573
|
|
|
429
574
|
// src/server/assetLineageDb.ts
|
|
430
575
|
var require2 = createRequire(import.meta.url);
|
|
@@ -470,6 +615,10 @@ function lineageDb() {
|
|
|
470
615
|
create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
|
|
471
616
|
create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
|
|
472
617
|
create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
|
|
618
|
+
create table if not exists lineage_schema_migrations (
|
|
619
|
+
key text primary key,
|
|
620
|
+
applied_at text not null
|
|
621
|
+
);
|
|
473
622
|
create table if not exists asset_edges (
|
|
474
623
|
id text primary key,
|
|
475
624
|
project_id text not null references projects(id),
|
|
@@ -481,6 +630,44 @@ function lineageDb() {
|
|
|
481
630
|
);
|
|
482
631
|
create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
|
|
483
632
|
create index if not exists edges_child on asset_edges(project_id, child_asset_id);
|
|
633
|
+
create table if not exists asset_attempts (
|
|
634
|
+
id text primary key,
|
|
635
|
+
project_id text not null references projects(id),
|
|
636
|
+
node_asset_id text not null references assets(id),
|
|
637
|
+
asset_id text not null references assets(id),
|
|
638
|
+
attempt_index integer not null check (attempt_index > 0),
|
|
639
|
+
source text not null check (source in ('initial', 'generated_child', 'reroll')),
|
|
640
|
+
prompt text,
|
|
641
|
+
generation_job_id text,
|
|
642
|
+
file_path text,
|
|
643
|
+
checksum_sha256 text,
|
|
644
|
+
created_at text not null,
|
|
645
|
+
promoted_at text,
|
|
646
|
+
is_current integer not null check (is_current in (0, 1)),
|
|
647
|
+
unique(project_id, node_asset_id, attempt_index),
|
|
648
|
+
unique(project_id, node_asset_id, asset_id, source)
|
|
649
|
+
);
|
|
650
|
+
create unique index if not exists asset_attempts_one_current
|
|
651
|
+
on asset_attempts(project_id, node_asset_id)
|
|
652
|
+
where is_current = 1;
|
|
653
|
+
create index if not exists asset_attempts_node_created
|
|
654
|
+
on asset_attempts(project_id, node_asset_id, created_at);
|
|
655
|
+
create table if not exists asset_reroll_requests (
|
|
656
|
+
id text primary key,
|
|
657
|
+
project_id text not null references projects(id),
|
|
658
|
+
root_asset_id text not null references assets(id),
|
|
659
|
+
node_asset_id text not null references assets(id),
|
|
660
|
+
status text not null check (status in ('pending', 'resolved', 'cancelled')),
|
|
661
|
+
requested_by text not null check (requested_by in ('human', 'agent', 'system')),
|
|
662
|
+
notes text,
|
|
663
|
+
created_at text not null,
|
|
664
|
+
resolved_at text
|
|
665
|
+
);
|
|
666
|
+
create unique index if not exists asset_reroll_requests_one_pending
|
|
667
|
+
on asset_reroll_requests(project_id, root_asset_id, node_asset_id)
|
|
668
|
+
where status = 'pending';
|
|
669
|
+
create index if not exists asset_reroll_requests_root_status
|
|
670
|
+
on asset_reroll_requests(project_id, root_asset_id, status, created_at);
|
|
484
671
|
create table if not exists asset_reviews (
|
|
485
672
|
asset_id text primary key references assets(id),
|
|
486
673
|
review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
|
|
@@ -685,7 +872,7 @@ function lineageDb() {
|
|
|
685
872
|
project_id text not null references projects(id),
|
|
686
873
|
provider text not null default 'codex-handoff',
|
|
687
874
|
adapter_version text not null,
|
|
688
|
-
source_mode text not null check (source_mode in ('lineage_selection')),
|
|
875
|
+
source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
|
|
689
876
|
root_asset_id text not null references assets(id),
|
|
690
877
|
prompt text not null,
|
|
691
878
|
expected_output_count integer not null check (expected_output_count > 0),
|
|
@@ -703,7 +890,7 @@ function lineageDb() {
|
|
|
703
890
|
project_id text not null references projects(id),
|
|
704
891
|
asset_id text not null references assets(id),
|
|
705
892
|
root_asset_id text not null references assets(id),
|
|
706
|
-
role text not null check (role in ('lineage_next_base', 'reference')),
|
|
893
|
+
role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
|
|
707
894
|
position integer not null,
|
|
708
895
|
selection_strategy text not null,
|
|
709
896
|
selection_snapshot_json text not null,
|
|
@@ -737,12 +924,49 @@ function lineageDb() {
|
|
|
737
924
|
);
|
|
738
925
|
create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
|
|
739
926
|
create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
|
|
927
|
+
create table if not exists lineage_tasks (
|
|
928
|
+
id text primary key,
|
|
929
|
+
project_id text not null references projects(id),
|
|
930
|
+
root_asset_id text not null references assets(id),
|
|
931
|
+
target_asset_id text not null references assets(id),
|
|
932
|
+
task_type text not null check (task_type in ('iterate', 'reroll')),
|
|
933
|
+
status text not null check (status in ('pending', 'claimed', 'in_progress', 'resolved', 'cancelled')),
|
|
934
|
+
instructions text,
|
|
935
|
+
created_by text not null check (created_by in ('human', 'agent', 'system')),
|
|
936
|
+
created_at text not null,
|
|
937
|
+
updated_at text not null,
|
|
938
|
+
claimed_at text,
|
|
939
|
+
started_at text,
|
|
940
|
+
resolved_at text,
|
|
941
|
+
cancelled_at text,
|
|
942
|
+
resolved_generation_job_id text,
|
|
943
|
+
resolved_asset_id text references assets(id),
|
|
944
|
+
metadata_json text
|
|
945
|
+
);
|
|
946
|
+
create unique index if not exists lineage_tasks_one_open
|
|
947
|
+
on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type)
|
|
948
|
+
where status in ('pending', 'claimed', 'in_progress');
|
|
949
|
+
create index if not exists lineage_tasks_root_status
|
|
950
|
+
on lineage_tasks(project_id, root_asset_id, status, updated_at);
|
|
951
|
+
create index if not exists lineage_tasks_target
|
|
952
|
+
on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type, status);
|
|
953
|
+
create table if not exists lineage_task_events (
|
|
954
|
+
id text primary key,
|
|
955
|
+
task_id text not null references lineage_tasks(id) on delete cascade,
|
|
956
|
+
event_type text not null,
|
|
957
|
+
actor text,
|
|
958
|
+
message text,
|
|
959
|
+
created_at text not null,
|
|
960
|
+
metadata_json text
|
|
961
|
+
);
|
|
962
|
+
create index if not exists lineage_task_events_task_created
|
|
963
|
+
on lineage_task_events(task_id, created_at);
|
|
740
964
|
create table if not exists agent_claims (
|
|
741
965
|
id text primary key,
|
|
742
966
|
token_hash text not null,
|
|
743
967
|
project_id text not null references projects(id),
|
|
744
968
|
channel text,
|
|
745
|
-
scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
969
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
746
970
|
target_id text not null,
|
|
747
971
|
target_title text,
|
|
748
972
|
agent_id text,
|
|
@@ -776,13 +1000,76 @@ function lineageDb() {
|
|
|
776
1000
|
migrateAssetSelections(database);
|
|
777
1001
|
dropLegacyAssetSelectionRootUnique(database);
|
|
778
1002
|
ensureColumn(database, "asset_selections", "notes", "text");
|
|
1003
|
+
backfillLineageTasks(database);
|
|
779
1004
|
ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
|
|
780
1005
|
ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
|
|
781
1006
|
ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
|
|
782
1007
|
ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
|
|
783
1008
|
ensureReviewStateValues(database);
|
|
1009
|
+
ensureAgentClaimScopeValues(database);
|
|
1010
|
+
ensureGenerationReceiptCheckValues(database);
|
|
784
1011
|
return database;
|
|
785
1012
|
}
|
|
1013
|
+
function backfillLineageTasks(database) {
|
|
1014
|
+
database.exec(`
|
|
1015
|
+
create table if not exists lineage_schema_migrations (
|
|
1016
|
+
key text primary key,
|
|
1017
|
+
applied_at text not null
|
|
1018
|
+
)
|
|
1019
|
+
`);
|
|
1020
|
+
const marker = database.prepare("select key from lineage_schema_migrations where key = 'lineage_tasks_backfilled_v1'").get();
|
|
1021
|
+
if (marker) return;
|
|
1022
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1023
|
+
try {
|
|
1024
|
+
database.exec(`
|
|
1025
|
+
insert or ignore into lineage_tasks (
|
|
1026
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1027
|
+
created_by, created_at, updated_at, metadata_json
|
|
1028
|
+
)
|
|
1029
|
+
select
|
|
1030
|
+
s.project_id || ':' || s.root_asset_id || ':lineage-task:iterate:' || s.asset_id,
|
|
1031
|
+
s.project_id,
|
|
1032
|
+
s.root_asset_id,
|
|
1033
|
+
s.asset_id,
|
|
1034
|
+
'iterate',
|
|
1035
|
+
'pending',
|
|
1036
|
+
s.notes,
|
|
1037
|
+
'human',
|
|
1038
|
+
s.selected_at,
|
|
1039
|
+
s.selected_at,
|
|
1040
|
+
null
|
|
1041
|
+
from asset_selections s;
|
|
1042
|
+
|
|
1043
|
+
insert or ignore into lineage_tasks (
|
|
1044
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1045
|
+
created_by, created_at, updated_at, metadata_json
|
|
1046
|
+
)
|
|
1047
|
+
select
|
|
1048
|
+
r.project_id || ':' || r.root_asset_id || ':lineage-task:reroll:' || r.node_asset_id,
|
|
1049
|
+
r.project_id,
|
|
1050
|
+
r.root_asset_id,
|
|
1051
|
+
r.node_asset_id,
|
|
1052
|
+
'reroll',
|
|
1053
|
+
'pending',
|
|
1054
|
+
r.notes,
|
|
1055
|
+
r.requested_by,
|
|
1056
|
+
r.created_at,
|
|
1057
|
+
r.created_at,
|
|
1058
|
+
null
|
|
1059
|
+
from asset_reroll_requests r
|
|
1060
|
+
where r.status = 'pending';
|
|
1061
|
+
`);
|
|
1062
|
+
database.prepare(`
|
|
1063
|
+
insert into lineage_schema_migrations (key, applied_at)
|
|
1064
|
+
values ('lineage_tasks_backfilled_v1', ?)
|
|
1065
|
+
on conflict(key) do nothing
|
|
1066
|
+
`).run(nowIso());
|
|
1067
|
+
database.exec("COMMIT");
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
database.exec("ROLLBACK");
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
786
1073
|
function ensureColumn(database, table, column, definition) {
|
|
787
1074
|
const rows = database.prepare(`pragma table_info(${table})`).all();
|
|
788
1075
|
if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
|
|
@@ -846,12 +1133,138 @@ function ensureReviewStateValues(database) {
|
|
|
846
1133
|
drop table asset_reviews_old;
|
|
847
1134
|
`);
|
|
848
1135
|
}
|
|
1136
|
+
function ensureAgentClaimScopeValues(database) {
|
|
1137
|
+
const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'agent_claims'").get();
|
|
1138
|
+
if (createSql?.sql?.includes("'lineage_task'")) return;
|
|
1139
|
+
database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
|
|
1140
|
+
try {
|
|
1141
|
+
database.exec(`
|
|
1142
|
+
alter table agent_claims rename to agent_claims_old;
|
|
1143
|
+
create table agent_claims (
|
|
1144
|
+
id text primary key,
|
|
1145
|
+
token_hash text not null,
|
|
1146
|
+
project_id text not null references projects(id),
|
|
1147
|
+
channel text,
|
|
1148
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
1149
|
+
target_id text not null,
|
|
1150
|
+
target_title text,
|
|
1151
|
+
agent_id text,
|
|
1152
|
+
agent_name text not null,
|
|
1153
|
+
agent_kind text not null,
|
|
1154
|
+
thread_id text,
|
|
1155
|
+
status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
|
|
1156
|
+
created_at text not null,
|
|
1157
|
+
heartbeat_at text not null,
|
|
1158
|
+
expires_at text not null,
|
|
1159
|
+
released_at text,
|
|
1160
|
+
revoked_at text,
|
|
1161
|
+
revoked_by text,
|
|
1162
|
+
override_reason text,
|
|
1163
|
+
metadata_json text
|
|
1164
|
+
);
|
|
1165
|
+
insert into agent_claims
|
|
1166
|
+
select * from agent_claims_old;
|
|
1167
|
+
drop table agent_claims_old;
|
|
1168
|
+
create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
|
|
1169
|
+
create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
|
|
1170
|
+
create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
|
|
1171
|
+
`);
|
|
1172
|
+
const violations = database.prepare("pragma foreign_key_check").all();
|
|
1173
|
+
if (violations.length > 0) throw new Error(`Agent claim scope migration failed foreign key check: ${JSON.stringify(violations)}`);
|
|
1174
|
+
database.exec("COMMIT");
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
database.exec("ROLLBACK");
|
|
1177
|
+
throw error;
|
|
1178
|
+
} finally {
|
|
1179
|
+
database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function tableCreateSql(database, table) {
|
|
1183
|
+
const row = database.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
|
|
1184
|
+
return row?.sql || "";
|
|
1185
|
+
}
|
|
1186
|
+
function ensureGenerationReceiptCheckValues(database) {
|
|
1187
|
+
const jobsSql = tableCreateSql(database, "generation_jobs");
|
|
1188
|
+
const inputsSql = tableCreateSql(database, "generation_job_inputs");
|
|
1189
|
+
const needsJobsMigration = Boolean(jobsSql && !jobsSql.includes("'lineage_reroll'"));
|
|
1190
|
+
const needsInputsMigration = Boolean(inputsSql && !inputsSql.includes("'reroll_target'"));
|
|
1191
|
+
if (!needsJobsMigration && !needsInputsMigration) return;
|
|
1192
|
+
database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
|
|
1193
|
+
try {
|
|
1194
|
+
if (needsJobsMigration) migrateGenerationJobsCheck(database);
|
|
1195
|
+
if (needsInputsMigration) migrateGenerationJobInputsCheck(database);
|
|
1196
|
+
const violations = database.prepare("pragma foreign_key_check").all();
|
|
1197
|
+
if (violations.length > 0) throw new Error(`Generation receipt migration failed foreign key check: ${JSON.stringify(violations)}`);
|
|
1198
|
+
database.exec("COMMIT");
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
database.exec("ROLLBACK");
|
|
1201
|
+
throw error;
|
|
1202
|
+
} finally {
|
|
1203
|
+
database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function migrateGenerationJobsCheck(database) {
|
|
1207
|
+
database.exec(`
|
|
1208
|
+
alter table generation_jobs rename to generation_jobs_legacy_check;
|
|
1209
|
+
create table generation_jobs (
|
|
1210
|
+
id text primary key,
|
|
1211
|
+
project_id text not null references projects(id),
|
|
1212
|
+
provider text not null default 'codex-handoff',
|
|
1213
|
+
adapter_version text not null,
|
|
1214
|
+
source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
|
|
1215
|
+
root_asset_id text not null references assets(id),
|
|
1216
|
+
prompt text not null,
|
|
1217
|
+
expected_output_count integer not null check (expected_output_count > 0),
|
|
1218
|
+
status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
|
|
1219
|
+
output_dir text,
|
|
1220
|
+
handoff_json text,
|
|
1221
|
+
created_at text not null,
|
|
1222
|
+
updated_at text not null,
|
|
1223
|
+
imported_at text
|
|
1224
|
+
);
|
|
1225
|
+
insert into generation_jobs (
|
|
1226
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
1227
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
|
|
1228
|
+
)
|
|
1229
|
+
select
|
|
1230
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
1231
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
|
|
1232
|
+
from generation_jobs_legacy_check;
|
|
1233
|
+
drop table generation_jobs_legacy_check;
|
|
1234
|
+
create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
|
|
1235
|
+
`);
|
|
1236
|
+
}
|
|
1237
|
+
function migrateGenerationJobInputsCheck(database) {
|
|
1238
|
+
database.exec(`
|
|
1239
|
+
alter table generation_job_inputs rename to generation_job_inputs_legacy_check;
|
|
1240
|
+
create table generation_job_inputs (
|
|
1241
|
+
id text primary key,
|
|
1242
|
+
job_id text not null references generation_jobs(id) on delete cascade,
|
|
1243
|
+
project_id text not null references projects(id),
|
|
1244
|
+
asset_id text not null references assets(id),
|
|
1245
|
+
root_asset_id text not null references assets(id),
|
|
1246
|
+
role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
|
|
1247
|
+
position integer not null,
|
|
1248
|
+
selection_strategy text not null,
|
|
1249
|
+
selection_snapshot_json text not null,
|
|
1250
|
+
unique(job_id, asset_id, role)
|
|
1251
|
+
);
|
|
1252
|
+
insert into generation_job_inputs (
|
|
1253
|
+
id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
|
|
1254
|
+
)
|
|
1255
|
+
select
|
|
1256
|
+
id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
|
|
1257
|
+
from generation_job_inputs_legacy_check;
|
|
1258
|
+
drop table generation_job_inputs_legacy_check;
|
|
1259
|
+
create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
|
|
1260
|
+
`);
|
|
1261
|
+
}
|
|
849
1262
|
|
|
850
1263
|
// src/server/agentClaims.ts
|
|
851
1264
|
var defaultTtlSeconds = 20 * 60;
|
|
852
1265
|
var idleAfterSeconds = 5 * 60;
|
|
853
1266
|
var staleAfterSeconds = 15 * 60;
|
|
854
|
-
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1267
|
+
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "lineage_task", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
855
1268
|
var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
856
1269
|
var AgentClaimError = class extends Error {
|
|
857
1270
|
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
@@ -1156,6 +1569,19 @@ function revokeAgentClaim(project, claimId, fields) {
|
|
|
1156
1569
|
database.close();
|
|
1157
1570
|
}
|
|
1158
1571
|
}
|
|
1572
|
+
function revokeAgentClaimInDatabase(database, project, claimId, fields) {
|
|
1573
|
+
expireActiveClaims(database);
|
|
1574
|
+
const claim = findClaimById(database, claimId, project);
|
|
1575
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1576
|
+
if (claim.status !== "active") return claim;
|
|
1577
|
+
const timestamp = nowIso();
|
|
1578
|
+
database.prepare(`
|
|
1579
|
+
update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1580
|
+
where id = ? and status = 'active'
|
|
1581
|
+
`).run(timestamp, fields.actor || "human", fields.reason || null, claim.id);
|
|
1582
|
+
recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
|
|
1583
|
+
return findClaimById(database, claim.id, project);
|
|
1584
|
+
}
|
|
1159
1585
|
function transferAgentClaim(project, claimId, fields) {
|
|
1160
1586
|
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1161
1587
|
const toAgentName = fields.toAgentName.trim();
|
|
@@ -1173,6 +1599,19 @@ function transferAgentClaim(project, claimId, fields) {
|
|
|
1173
1599
|
database.close();
|
|
1174
1600
|
}
|
|
1175
1601
|
}
|
|
1602
|
+
function recordAgentClaimWriteAllowed(claim, fields) {
|
|
1603
|
+
const database = lineageDb();
|
|
1604
|
+
try {
|
|
1605
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1606
|
+
danger_level: fields.dangerLevel,
|
|
1607
|
+
target_id: fields.targetId,
|
|
1608
|
+
write_kind: fields.writeKind
|
|
1609
|
+
});
|
|
1610
|
+
return { ok: true };
|
|
1611
|
+
} finally {
|
|
1612
|
+
database.close();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1176
1615
|
function validateAgentClaimForWrite(fields) {
|
|
1177
1616
|
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1178
1617
|
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
@@ -1193,17 +1632,22 @@ function validateAgentClaimForWrite(fields) {
|
|
|
1193
1632
|
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1194
1633
|
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1195
1634
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1635
|
+
if (fields.recordEvent !== false) {
|
|
1636
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1637
|
+
danger_level: fields.dangerLevel,
|
|
1638
|
+
target_id: fields.targetId,
|
|
1639
|
+
write_kind: fields.writeKind
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1201
1642
|
return { ok: true, claim, warnings: [] };
|
|
1202
1643
|
} finally {
|
|
1203
1644
|
database.close();
|
|
1204
1645
|
}
|
|
1205
1646
|
}
|
|
1206
1647
|
|
|
1648
|
+
// src/server/assetLineage.ts
|
|
1649
|
+
import { join as join5 } from "node:path";
|
|
1650
|
+
|
|
1207
1651
|
// src/server/assetLineageSelection.ts
|
|
1208
1652
|
function selectedRows(database, project, root) {
|
|
1209
1653
|
return database.prepare(`
|
|
@@ -1214,6 +1658,507 @@ function selectedRows(database, project, root) {
|
|
|
1214
1658
|
`).all(project, root);
|
|
1215
1659
|
}
|
|
1216
1660
|
|
|
1661
|
+
// src/server/assetLineageTasks.ts
|
|
1662
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1663
|
+
var LineageTaskError = class extends Error {
|
|
1664
|
+
constructor(message, status = 400) {
|
|
1665
|
+
super(message);
|
|
1666
|
+
this.status = status;
|
|
1667
|
+
}
|
|
1668
|
+
status;
|
|
1669
|
+
};
|
|
1670
|
+
var activeStatuses = ["pending", "claimed", "in_progress"];
|
|
1671
|
+
var taskTypes = /* @__PURE__ */ new Set(["iterate", "reroll"]);
|
|
1672
|
+
var taskStatuses = /* @__PURE__ */ new Set(["pending", "claimed", "in_progress", "resolved", "cancelled"]);
|
|
1673
|
+
function taskIdFor(project, rootAssetId, targetAssetId, taskType) {
|
|
1674
|
+
return `${project}:${rootAssetId}:lineage-task:${taskType}:${targetAssetId}`;
|
|
1675
|
+
}
|
|
1676
|
+
function randomId2(prefix) {
|
|
1677
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
|
|
1678
|
+
}
|
|
1679
|
+
function metadataJson2(metadata) {
|
|
1680
|
+
return metadata ? JSON.stringify(metadata) : null;
|
|
1681
|
+
}
|
|
1682
|
+
function metadataWithoutClaim(metadata) {
|
|
1683
|
+
if (!metadata) return void 0;
|
|
1684
|
+
const next = { ...metadata };
|
|
1685
|
+
delete next.claim_id;
|
|
1686
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
1687
|
+
}
|
|
1688
|
+
function parseMetadata2(value) {
|
|
1689
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
1690
|
+
try {
|
|
1691
|
+
const parsed = JSON.parse(value);
|
|
1692
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1693
|
+
} catch {
|
|
1694
|
+
return void 0;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function normalizeProject(project) {
|
|
1698
|
+
const trimmed = project.trim();
|
|
1699
|
+
if (!trimmed) throw new LineageTaskError("Lineage task requires project");
|
|
1700
|
+
return trimmed;
|
|
1701
|
+
}
|
|
1702
|
+
function normalizeTaskType(taskType) {
|
|
1703
|
+
if (!taskTypes.has(taskType)) throw new LineageTaskError(`Unsupported lineage task type: ${taskType}`);
|
|
1704
|
+
return taskType;
|
|
1705
|
+
}
|
|
1706
|
+
function normalizeStatus(status) {
|
|
1707
|
+
if (!taskStatuses.has(status)) throw new LineageTaskError(`Unsupported lineage task status: ${status}`);
|
|
1708
|
+
return status;
|
|
1709
|
+
}
|
|
1710
|
+
function normalizeActor(actor, label) {
|
|
1711
|
+
const trimmed = actor.trim();
|
|
1712
|
+
if (!trimmed) throw new LineageTaskError(`${label} is required`);
|
|
1713
|
+
return trimmed;
|
|
1714
|
+
}
|
|
1715
|
+
function requireAsset(database, project, assetId) {
|
|
1716
|
+
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
1717
|
+
if (!row) throw new LineageTaskError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1718
|
+
}
|
|
1719
|
+
function taskFromRow(row) {
|
|
1720
|
+
const metadata = parseMetadata2(row.metadata_json);
|
|
1721
|
+
const claimId = typeof metadata?.claim_id === "string" ? metadata.claim_id : void 0;
|
|
1722
|
+
return {
|
|
1723
|
+
id: String(row.id),
|
|
1724
|
+
project_id: String(row.project_id),
|
|
1725
|
+
root_asset_id: String(row.root_asset_id),
|
|
1726
|
+
target_asset_id: String(row.target_asset_id),
|
|
1727
|
+
task_type: String(row.task_type),
|
|
1728
|
+
status: String(row.status),
|
|
1729
|
+
instructions: typeof row.instructions === "string" ? row.instructions : void 0,
|
|
1730
|
+
created_by: String(row.created_by),
|
|
1731
|
+
created_at: String(row.created_at),
|
|
1732
|
+
updated_at: String(row.updated_at),
|
|
1733
|
+
claimed_at: typeof row.claimed_at === "string" ? row.claimed_at : void 0,
|
|
1734
|
+
started_at: typeof row.started_at === "string" ? row.started_at : void 0,
|
|
1735
|
+
resolved_at: typeof row.resolved_at === "string" ? row.resolved_at : void 0,
|
|
1736
|
+
cancelled_at: typeof row.cancelled_at === "string" ? row.cancelled_at : void 0,
|
|
1737
|
+
resolved_generation_job_id: typeof row.resolved_generation_job_id === "string" ? row.resolved_generation_job_id : void 0,
|
|
1738
|
+
resolved_asset_id: typeof row.resolved_asset_id === "string" ? row.resolved_asset_id : void 0,
|
|
1739
|
+
claimed_by_claim_id: claimId,
|
|
1740
|
+
metadata
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function eventFromRow(row) {
|
|
1744
|
+
return {
|
|
1745
|
+
id: String(row.id),
|
|
1746
|
+
task_id: String(row.task_id),
|
|
1747
|
+
event_type: String(row.event_type),
|
|
1748
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
1749
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
1750
|
+
created_at: String(row.created_at),
|
|
1751
|
+
metadata: parseMetadata2(row.metadata_json)
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function findTask(database, project, taskId) {
|
|
1755
|
+
const row = database.prepare("select * from lineage_tasks where project_id = ? and id = ?").get(project, taskId);
|
|
1756
|
+
return row ? taskFromRow(row) : null;
|
|
1757
|
+
}
|
|
1758
|
+
function requireTask(database, project, taskId) {
|
|
1759
|
+
const task = findTask(database, project, taskId);
|
|
1760
|
+
if (!task) throw new LineageTaskError(`Unknown lineage task: ${taskId}`, 404);
|
|
1761
|
+
return task;
|
|
1762
|
+
}
|
|
1763
|
+
function taskEvents(database, taskId) {
|
|
1764
|
+
const rows = database.prepare("select * from lineage_task_events where task_id = ? order by created_at, rowid").all(taskId);
|
|
1765
|
+
return rows.map(eventFromRow);
|
|
1766
|
+
}
|
|
1767
|
+
function recordEvent2(database, taskId, eventType, actor, message, metadata) {
|
|
1768
|
+
database.prepare(`
|
|
1769
|
+
insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
|
|
1770
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
1771
|
+
`).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata));
|
|
1772
|
+
}
|
|
1773
|
+
function taskWithEvents(database, project, taskId) {
|
|
1774
|
+
return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
1775
|
+
}
|
|
1776
|
+
function assertChanged(result, message) {
|
|
1777
|
+
if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
|
|
1778
|
+
}
|
|
1779
|
+
function transaction(database, callback) {
|
|
1780
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1781
|
+
try {
|
|
1782
|
+
const value = callback();
|
|
1783
|
+
database.exec("COMMIT");
|
|
1784
|
+
return value;
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
database.exec("ROLLBACK");
|
|
1787
|
+
throw error;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function taskReadWithEvents(database, project, taskId) {
|
|
1791
|
+
return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
1792
|
+
}
|
|
1793
|
+
function listLineageTasks(project, rootAssetId, statuses = activeStatuses) {
|
|
1794
|
+
const normalizedProject = normalizeProject(project);
|
|
1795
|
+
const normalizedStatuses = statuses.map(normalizeStatus);
|
|
1796
|
+
const database = lineageDb();
|
|
1797
|
+
try {
|
|
1798
|
+
requireAsset(database, normalizedProject, rootAssetId);
|
|
1799
|
+
if (normalizedStatuses.length === 0) {
|
|
1800
|
+
return { project: normalizedProject, root_asset_id: rootAssetId, tasks: [], fetchedAt: nowIso() };
|
|
1801
|
+
}
|
|
1802
|
+
const placeholders = normalizedStatuses.map(() => "?").join(",");
|
|
1803
|
+
const rows = database.prepare(`
|
|
1804
|
+
select * from lineage_tasks
|
|
1805
|
+
where project_id = ? and root_asset_id = ? and status in (${placeholders})
|
|
1806
|
+
order by updated_at desc, created_at desc, id
|
|
1807
|
+
`).all(normalizedProject, rootAssetId, ...normalizedStatuses);
|
|
1808
|
+
return { project: normalizedProject, root_asset_id: rootAssetId, tasks: rows.map(taskFromRow), fetchedAt: nowIso() };
|
|
1809
|
+
} finally {
|
|
1810
|
+
database.close();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function getLineageTask(project, taskId) {
|
|
1814
|
+
const normalizedProject = normalizeProject(project);
|
|
1815
|
+
const database = lineageDb();
|
|
1816
|
+
try {
|
|
1817
|
+
return { project: normalizedProject, ...taskReadWithEvents(database, normalizedProject, taskId) };
|
|
1818
|
+
} finally {
|
|
1819
|
+
database.close();
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function upsertLineageTask(project, fields) {
|
|
1823
|
+
const normalizedProject = normalizeProject(project);
|
|
1824
|
+
const taskType = normalizeTaskType(fields.taskType);
|
|
1825
|
+
const taskId = taskIdFor(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
|
|
1826
|
+
const database = lineageDb();
|
|
1827
|
+
try {
|
|
1828
|
+
requireAsset(database, normalizedProject, fields.rootAssetId);
|
|
1829
|
+
requireAsset(database, normalizedProject, fields.targetAssetId);
|
|
1830
|
+
const existing = database.prepare(`
|
|
1831
|
+
select * from lineage_tasks
|
|
1832
|
+
where project_id = ? and root_asset_id = ? and target_asset_id = ? and task_type = ?
|
|
1833
|
+
and status in ('pending', 'claimed', 'in_progress')
|
|
1834
|
+
order by created_at desc limit 1
|
|
1835
|
+
`).get(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
|
|
1836
|
+
const instructions = fields.instructions?.trim() || void 0;
|
|
1837
|
+
if (existing) {
|
|
1838
|
+
const task = taskFromRow(existing);
|
|
1839
|
+
if (task.status !== "pending" && instructions !== void 0 && instructions !== task.instructions) {
|
|
1840
|
+
throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
|
|
1841
|
+
}
|
|
1842
|
+
if (task.status === "pending" && instructions !== void 0 && instructions !== task.instructions) {
|
|
1843
|
+
const timestamp2 = nowIso();
|
|
1844
|
+
transaction(database, () => {
|
|
1845
|
+
const result = database.prepare(`
|
|
1846
|
+
update lineage_tasks
|
|
1847
|
+
set instructions = ?, updated_at = ?
|
|
1848
|
+
where id = ? and status = 'pending'
|
|
1849
|
+
`).run(instructions, timestamp2, task.id);
|
|
1850
|
+
assertChanged(result, "Only pending lineage tasks can update instructions.");
|
|
1851
|
+
recordEvent2(database, task.id, "instructions_updated", fields.createdBy, "Instructions updated.");
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1855
|
+
}
|
|
1856
|
+
const closedTask = findTask(database, normalizedProject, taskId);
|
|
1857
|
+
if (closedTask) {
|
|
1858
|
+
const timestamp2 = nowIso();
|
|
1859
|
+
transaction(database, () => {
|
|
1860
|
+
const result = database.prepare(`
|
|
1861
|
+
update lineage_tasks
|
|
1862
|
+
set status = 'pending', instructions = ?, created_by = ?, updated_at = ?,
|
|
1863
|
+
claimed_at = null, started_at = null, resolved_at = null, cancelled_at = null,
|
|
1864
|
+
resolved_generation_job_id = null, resolved_asset_id = null, metadata_json = null
|
|
1865
|
+
where id = ? and status not in ('pending', 'claimed', 'in_progress')
|
|
1866
|
+
`).run(instructions || null, fields.createdBy, timestamp2, taskId);
|
|
1867
|
+
assertChanged(result, "Lineage task changed while reopening.");
|
|
1868
|
+
recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
|
|
1869
|
+
});
|
|
1870
|
+
return taskWithEvents(database, normalizedProject, taskId);
|
|
1871
|
+
}
|
|
1872
|
+
const timestamp = nowIso();
|
|
1873
|
+
transaction(database, () => {
|
|
1874
|
+
database.prepare(`
|
|
1875
|
+
insert into lineage_tasks (
|
|
1876
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1877
|
+
created_by, created_at, updated_at, metadata_json
|
|
1878
|
+
) values (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, null)
|
|
1879
|
+
`).run(taskId, normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType, instructions || null, fields.createdBy, timestamp, timestamp);
|
|
1880
|
+
recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
|
|
1881
|
+
});
|
|
1882
|
+
return taskWithEvents(database, normalizedProject, taskId);
|
|
1883
|
+
} finally {
|
|
1884
|
+
database.close();
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function updateLineageTaskInstructions(project, fields) {
|
|
1888
|
+
const normalizedProject = normalizeProject(project);
|
|
1889
|
+
const instructions = fields.instructions.trim();
|
|
1890
|
+
const database = lineageDb();
|
|
1891
|
+
try {
|
|
1892
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1893
|
+
if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
|
|
1894
|
+
const timestamp = nowIso();
|
|
1895
|
+
transaction(database, () => {
|
|
1896
|
+
const result = database.prepare(`
|
|
1897
|
+
update lineage_tasks
|
|
1898
|
+
set instructions = ?, updated_at = ?
|
|
1899
|
+
where id = ? and status = 'pending'
|
|
1900
|
+
`).run(instructions || null, timestamp, task.id);
|
|
1901
|
+
assertChanged(result, "Only pending lineage tasks can update instructions.");
|
|
1902
|
+
recordEvent2(database, task.id, "instructions_updated", "human", "Instructions updated.");
|
|
1903
|
+
});
|
|
1904
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1905
|
+
} finally {
|
|
1906
|
+
database.close();
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function addLineageTaskComment(project, fields) {
|
|
1910
|
+
const normalizedProject = normalizeProject(project);
|
|
1911
|
+
const actor = normalizeActor(fields.actor, "Comment actor");
|
|
1912
|
+
const message = fields.message.trim();
|
|
1913
|
+
if (!message) throw new LineageTaskError("Comment message is required");
|
|
1914
|
+
const database = lineageDb();
|
|
1915
|
+
try {
|
|
1916
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1917
|
+
transaction(database, () => {
|
|
1918
|
+
recordEvent2(database, task.id, "comment_added", actor, message);
|
|
1919
|
+
});
|
|
1920
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1921
|
+
} finally {
|
|
1922
|
+
database.close();
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function claimLineageTask(project, fields) {
|
|
1926
|
+
const normalizedProject = normalizeProject(project);
|
|
1927
|
+
const agentName = normalizeActor(fields.agentName, "Agent name");
|
|
1928
|
+
const beforeClaimDb = lineageDb();
|
|
1929
|
+
try {
|
|
1930
|
+
const task = requireTask(beforeClaimDb, normalizedProject, fields.taskId);
|
|
1931
|
+
if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
|
|
1932
|
+
} finally {
|
|
1933
|
+
beforeClaimDb.close();
|
|
1934
|
+
}
|
|
1935
|
+
const claimResult = createAgentClaim({
|
|
1936
|
+
agentName,
|
|
1937
|
+
project: normalizedProject,
|
|
1938
|
+
scopeType: "lineage_task",
|
|
1939
|
+
targetId: fields.taskId,
|
|
1940
|
+
targetTitle: fields.taskId
|
|
1941
|
+
});
|
|
1942
|
+
if (!claimResult.claim) throw new LineageTaskError("Unable to create lineage task claim.", 500);
|
|
1943
|
+
const claim = claimResult.claim;
|
|
1944
|
+
const database = lineageDb();
|
|
1945
|
+
try {
|
|
1946
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1947
|
+
if (task.status !== "pending") {
|
|
1948
|
+
releaseAgentClaim(claimResult.claim_token);
|
|
1949
|
+
throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
|
|
1950
|
+
}
|
|
1951
|
+
const timestamp = nowIso();
|
|
1952
|
+
const metadata = { ...task.metadata || {}, claim_id: claim.id };
|
|
1953
|
+
try {
|
|
1954
|
+
transaction(database, () => {
|
|
1955
|
+
const result = database.prepare(`
|
|
1956
|
+
update lineage_tasks
|
|
1957
|
+
set status = 'claimed', claimed_at = ?, updated_at = ?, metadata_json = ?
|
|
1958
|
+
where id = ? and status = 'pending'
|
|
1959
|
+
`).run(timestamp, timestamp, metadataJson2(metadata), task.id);
|
|
1960
|
+
assertChanged(result, "Only pending lineage tasks can be claimed.");
|
|
1961
|
+
recordEvent2(database, task.id, "claimed", agentName, "Lineage task claimed.", { claim_id: claim.id });
|
|
1962
|
+
});
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
releaseAgentClaim(claimResult.claim_token);
|
|
1965
|
+
throw error;
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
...taskWithEvents(database, normalizedProject, task.id),
|
|
1969
|
+
claim,
|
|
1970
|
+
claim_token: claimResult.claim_token
|
|
1971
|
+
};
|
|
1972
|
+
} finally {
|
|
1973
|
+
database.close();
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function startLineageTask(project, fields) {
|
|
1977
|
+
const normalizedProject = normalizeProject(project);
|
|
1978
|
+
const precheckDatabase = lineageDb();
|
|
1979
|
+
try {
|
|
1980
|
+
const task = requireTask(precheckDatabase, normalizedProject, fields.taskId);
|
|
1981
|
+
if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
|
|
1982
|
+
} finally {
|
|
1983
|
+
precheckDatabase.close();
|
|
1984
|
+
}
|
|
1985
|
+
const validation = validateAgentClaimForWrite({
|
|
1986
|
+
claimToken: fields.claimToken,
|
|
1987
|
+
dangerLevel: "enforce",
|
|
1988
|
+
project: normalizedProject,
|
|
1989
|
+
recordEvent: false,
|
|
1990
|
+
scopeType: "lineage_task",
|
|
1991
|
+
targetId: fields.taskId,
|
|
1992
|
+
writeKind: "lineage_task_start"
|
|
1993
|
+
});
|
|
1994
|
+
if (!validation.ok) throw new LineageTaskError(validation.message, 409);
|
|
1995
|
+
const database = lineageDb();
|
|
1996
|
+
try {
|
|
1997
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1998
|
+
if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
|
|
1999
|
+
if (task.claimed_by_claim_id && task.claimed_by_claim_id !== validation.claim.id) {
|
|
2000
|
+
throw new LineageTaskError("Claim token does not match the task claim.", 409);
|
|
2001
|
+
}
|
|
2002
|
+
const timestamp = nowIso();
|
|
2003
|
+
const metadata = { ...task.metadata || {}, claim_id: validation.claim.id };
|
|
2004
|
+
transaction(database, () => {
|
|
2005
|
+
const result = database.prepare(`
|
|
2006
|
+
update lineage_tasks
|
|
2007
|
+
set status = 'in_progress', started_at = ?, updated_at = ?, metadata_json = ?
|
|
2008
|
+
where id = ? and status = 'claimed'
|
|
2009
|
+
`).run(timestamp, timestamp, metadataJson2(metadata), task.id);
|
|
2010
|
+
assertChanged(result, "Only claimed lineage tasks can be started.");
|
|
2011
|
+
recordEvent2(database, task.id, "started", validation.claim.agent_name, "Lineage task started.", { claim_id: validation.claim.id });
|
|
2012
|
+
});
|
|
2013
|
+
recordAgentClaimWriteAllowed(validation.claim, {
|
|
2014
|
+
dangerLevel: "enforce",
|
|
2015
|
+
targetId: fields.taskId,
|
|
2016
|
+
writeKind: "lineage_task_start"
|
|
2017
|
+
});
|
|
2018
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2019
|
+
} finally {
|
|
2020
|
+
database.close();
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function overrideLineageTask(project, fields) {
|
|
2024
|
+
const normalizedProject = normalizeProject(project);
|
|
2025
|
+
const actor = normalizeActor(fields.actor, "Override actor");
|
|
2026
|
+
const reason = fields.reason.trim();
|
|
2027
|
+
if (!reason) throw new LineageTaskError("Override reason is required");
|
|
2028
|
+
const database = lineageDb();
|
|
2029
|
+
try {
|
|
2030
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2031
|
+
if (!["claimed", "in_progress"].includes(task.status)) {
|
|
2032
|
+
throw new LineageTaskError(`Only claimed or in-progress lineage tasks can be overridden; task is ${task.status}.`, 409);
|
|
2033
|
+
}
|
|
2034
|
+
const timestamp = nowIso();
|
|
2035
|
+
const instructions = fields.instructions === void 0 ? task.instructions : fields.instructions.trim() || void 0;
|
|
2036
|
+
const metadata = metadataWithoutClaim(task.metadata);
|
|
2037
|
+
transaction(database, () => {
|
|
2038
|
+
const result = database.prepare(`
|
|
2039
|
+
update lineage_tasks
|
|
2040
|
+
set status = 'pending', instructions = ?, claimed_at = null, started_at = null,
|
|
2041
|
+
updated_at = ?, metadata_json = ?
|
|
2042
|
+
where project_id = ? and id = ? and status in ('claimed', 'in_progress')
|
|
2043
|
+
`).run(instructions || null, timestamp, metadataJson2(metadata), normalizedProject, task.id);
|
|
2044
|
+
assertChanged(result, `Only active lineage task ${task.id} could be overridden.`);
|
|
2045
|
+
if (task.claimed_by_claim_id) {
|
|
2046
|
+
revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
|
|
2047
|
+
actor,
|
|
2048
|
+
reason
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
recordEvent2(database, task.id, "human_override", actor, reason, {
|
|
2052
|
+
previous_claim_id: task.claimed_by_claim_id,
|
|
2053
|
+
previous_status: task.status
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2057
|
+
} finally {
|
|
2058
|
+
database.close();
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function cancelLineageTask(project, fields) {
|
|
2062
|
+
const normalizedProject = normalizeProject(project);
|
|
2063
|
+
const actor = normalizeActor(fields.actor, "Cancel actor");
|
|
2064
|
+
const database = lineageDb();
|
|
2065
|
+
try {
|
|
2066
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2067
|
+
if (task.status === "cancelled") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
2068
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
2069
|
+
throw new LineageTaskError(`Only open lineage tasks can be cancelled; task is ${task.status}.`, 409);
|
|
2070
|
+
}
|
|
2071
|
+
if (task.status !== "pending" && !fields.override) {
|
|
2072
|
+
throw new LineageTaskError("Cancelling an active lineage task requires override=true.", 409);
|
|
2073
|
+
}
|
|
2074
|
+
const overridingActiveTask = fields.override === true && task.status !== "pending";
|
|
2075
|
+
const metadata = overridingActiveTask ? metadataWithoutClaim(task.metadata) : task.metadata;
|
|
2076
|
+
const timestamp = nowIso();
|
|
2077
|
+
const cancelledTask = {
|
|
2078
|
+
...task,
|
|
2079
|
+
cancelled_at: timestamp,
|
|
2080
|
+
claimed_at: overridingActiveTask ? void 0 : task.claimed_at,
|
|
2081
|
+
claimed_by_claim_id: overridingActiveTask ? void 0 : task.claimed_by_claim_id,
|
|
2082
|
+
started_at: overridingActiveTask ? void 0 : task.started_at,
|
|
2083
|
+
status: "cancelled",
|
|
2084
|
+
updated_at: timestamp,
|
|
2085
|
+
metadata
|
|
2086
|
+
};
|
|
2087
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: cancelledTask, events: taskEvents(database, task.id) };
|
|
2088
|
+
transaction(database, () => {
|
|
2089
|
+
const result = database.prepare(`
|
|
2090
|
+
update lineage_tasks
|
|
2091
|
+
set status = 'cancelled', cancelled_at = ?, claimed_at = ?, started_at = ?,
|
|
2092
|
+
updated_at = ?, metadata_json = ?
|
|
2093
|
+
where id = ? and status = ?
|
|
2094
|
+
`).run(
|
|
2095
|
+
timestamp,
|
|
2096
|
+
overridingActiveTask ? null : task.claimed_at || null,
|
|
2097
|
+
overridingActiveTask ? null : task.started_at || null,
|
|
2098
|
+
timestamp,
|
|
2099
|
+
metadataJson2(metadata),
|
|
2100
|
+
task.id,
|
|
2101
|
+
task.status
|
|
2102
|
+
);
|
|
2103
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be cancelled.`);
|
|
2104
|
+
if (overridingActiveTask) {
|
|
2105
|
+
if (task.claimed_by_claim_id) {
|
|
2106
|
+
revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
|
|
2107
|
+
actor,
|
|
2108
|
+
reason: "Lineage task cancelled by human override."
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
recordEvent2(database, task.id, "human_override", actor, "Lineage task cancelled by human override.", {
|
|
2112
|
+
previous_claim_id: task.claimed_by_claim_id,
|
|
2113
|
+
previous_status: task.status
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
recordEvent2(database, task.id, "cancelled", actor, "Lineage task cancelled.");
|
|
2117
|
+
});
|
|
2118
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2119
|
+
} finally {
|
|
2120
|
+
database.close();
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function resolveLineageTask(project, fields) {
|
|
2124
|
+
const normalizedProject = normalizeProject(project);
|
|
2125
|
+
const actor = normalizeActor(fields.actor, "Resolve actor");
|
|
2126
|
+
const database = lineageDb();
|
|
2127
|
+
try {
|
|
2128
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2129
|
+
if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
|
|
2130
|
+
if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
2131
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
2132
|
+
throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
|
|
2133
|
+
}
|
|
2134
|
+
const timestamp = nowIso();
|
|
2135
|
+
const resolvedTask = {
|
|
2136
|
+
...task,
|
|
2137
|
+
status: "resolved",
|
|
2138
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
2139
|
+
resolved_at: timestamp,
|
|
2140
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId,
|
|
2141
|
+
updated_at: timestamp
|
|
2142
|
+
};
|
|
2143
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
|
|
2144
|
+
transaction(database, () => {
|
|
2145
|
+
const result = database.prepare(`
|
|
2146
|
+
update lineage_tasks
|
|
2147
|
+
set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
|
|
2148
|
+
where id = ? and status = ?
|
|
2149
|
+
`).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
|
|
2150
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
|
|
2151
|
+
recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
|
|
2152
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
2153
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId
|
|
2154
|
+
});
|
|
2155
|
+
});
|
|
2156
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2157
|
+
} finally {
|
|
2158
|
+
database.close();
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1217
2162
|
// src/server/assetLineageWorkspaces.ts
|
|
1218
2163
|
function lineageWorkspaceId(project, rootAssetId) {
|
|
1219
2164
|
return `${project}:lineage-workspace:${rootAssetId}`;
|
|
@@ -1336,7 +2281,71 @@ var LineageError = class extends Error {
|
|
|
1336
2281
|
}
|
|
1337
2282
|
status;
|
|
1338
2283
|
};
|
|
1339
|
-
function
|
|
2284
|
+
function collectAssets(project, source) {
|
|
2285
|
+
const first = listAssets(project, { source, page: 1, pageSize: 100 });
|
|
2286
|
+
const assets = [...first.assets];
|
|
2287
|
+
for (let page = 2; page <= first.pagination.totalPages; page += 1) {
|
|
2288
|
+
assets.push(...listAssets(project, { source, page, pageSize: 100 }).assets);
|
|
2289
|
+
}
|
|
2290
|
+
return assets;
|
|
2291
|
+
}
|
|
2292
|
+
function upsertProject(database, project) {
|
|
2293
|
+
const timestamp = nowIso();
|
|
2294
|
+
database.prepare(`
|
|
2295
|
+
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
2296
|
+
values (?, ?, ?, ?, ?)
|
|
2297
|
+
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
2298
|
+
`).run(project, project, join5(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
2299
|
+
}
|
|
2300
|
+
function upsertAsset(database, project, asset) {
|
|
2301
|
+
const timestamp = nowIso();
|
|
2302
|
+
const source = asset.source === "local" ? "local" : "catalog";
|
|
2303
|
+
database.prepare(`
|
|
2304
|
+
insert into assets (
|
|
2305
|
+
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
2306
|
+
channel, campaign, audience, size_bytes, content_type, created_at, updated_at, last_seen_at
|
|
2307
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2308
|
+
on conflict(id) do update set
|
|
2309
|
+
source = excluded.source, local_path = excluded.local_path, s3_key = excluded.s3_key,
|
|
2310
|
+
checksum_sha256 = excluded.checksum_sha256, media_type = excluded.media_type,
|
|
2311
|
+
title = excluded.title, status = excluded.status, channel = excluded.channel,
|
|
2312
|
+
campaign = excluded.campaign, audience = excluded.audience, size_bytes = excluded.size_bytes,
|
|
2313
|
+
content_type = excluded.content_type, updated_at = excluded.updated_at, last_seen_at = excluded.last_seen_at
|
|
2314
|
+
`).run(
|
|
2315
|
+
asset.asset_id,
|
|
2316
|
+
project,
|
|
2317
|
+
source,
|
|
2318
|
+
asset.local?.relative_path || null,
|
|
2319
|
+
asset.s3?.key || null,
|
|
2320
|
+
asset.local?.checksum_sha256 || asset.s3?.checksum_sha256 || null,
|
|
2321
|
+
asset.content_type,
|
|
2322
|
+
asset.title,
|
|
2323
|
+
asset.status,
|
|
2324
|
+
asset.channel || null,
|
|
2325
|
+
asset.campaign || null,
|
|
2326
|
+
asset.audience || null,
|
|
2327
|
+
asset.local?.size_bytes || asset.s3?.size_bytes || null,
|
|
2328
|
+
asset.local?.content_type || asset.s3?.content_type || null,
|
|
2329
|
+
timestamp,
|
|
2330
|
+
timestamp,
|
|
2331
|
+
timestamp
|
|
2332
|
+
);
|
|
2333
|
+
database.prepare(`
|
|
2334
|
+
insert into asset_reviews (asset_id, review_state, updated_at)
|
|
2335
|
+
values (?, 'unreviewed', ?)
|
|
2336
|
+
on conflict(asset_id) do nothing
|
|
2337
|
+
`).run(asset.asset_id, timestamp);
|
|
2338
|
+
}
|
|
2339
|
+
function indexLineageAssets(project = defaultProject) {
|
|
2340
|
+
const database = lineageDb();
|
|
2341
|
+
const catalog = collectAssets(project, "catalog");
|
|
2342
|
+
const local = collectAssets(project, "local");
|
|
2343
|
+
upsertProject(database, project);
|
|
2344
|
+
for (const asset of [...catalog, ...local]) upsertAsset(database, project, asset);
|
|
2345
|
+
database.close();
|
|
2346
|
+
return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
|
|
2347
|
+
}
|
|
2348
|
+
function requireAsset2(database, project, assetId) {
|
|
1340
2349
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
1341
2350
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1342
2351
|
}
|
|
@@ -1355,6 +2364,11 @@ function rootFor(database, project, assetId) {
|
|
|
1355
2364
|
}
|
|
1356
2365
|
return assetId;
|
|
1357
2366
|
}
|
|
2367
|
+
function assertCanonicalRoot(database, project, root) {
|
|
2368
|
+
requireAsset2(database, project, root);
|
|
2369
|
+
const canonicalRoot = rootFor(database, project, root);
|
|
2370
|
+
if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
2371
|
+
}
|
|
1358
2372
|
function explicitWorkspaceRoot(database, project, assetId) {
|
|
1359
2373
|
const row = database.prepare(`
|
|
1360
2374
|
select root_asset_id from lineage_workspaces
|
|
@@ -1388,7 +2402,7 @@ function lineageWriteClaimContext(database, project, assetId) {
|
|
|
1388
2402
|
function getLineageWriteClaimContext(project, assetId) {
|
|
1389
2403
|
const database = lineageDb();
|
|
1390
2404
|
try {
|
|
1391
|
-
|
|
2405
|
+
requireAsset2(database, project, assetId);
|
|
1392
2406
|
return lineageWriteClaimContext(database, project, assetId);
|
|
1393
2407
|
} finally {
|
|
1394
2408
|
database.close();
|
|
@@ -1400,17 +2414,113 @@ function latestSelectedRoot(database, project) {
|
|
|
1400
2414
|
}
|
|
1401
2415
|
function resolveRoot(database, project, rootAssetId) {
|
|
1402
2416
|
if (rootAssetId) {
|
|
1403
|
-
|
|
2417
|
+
requireAsset2(database, project, rootAssetId);
|
|
1404
2418
|
return rootAssetId;
|
|
1405
2419
|
}
|
|
1406
2420
|
const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
|
|
1407
2421
|
if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
|
|
1408
|
-
|
|
2422
|
+
requireAsset2(database, project, root);
|
|
1409
2423
|
return root;
|
|
1410
2424
|
}
|
|
1411
2425
|
function edgeId(project, parent, child) {
|
|
1412
2426
|
return `${project}:${parent}:derived_from:${child}`;
|
|
1413
2427
|
}
|
|
2428
|
+
function rerollRequestId(project, root, node, timestamp) {
|
|
2429
|
+
return `${project}:${root}:reroll:${node}:${Date.parse(timestamp) || Date.now()}`;
|
|
2430
|
+
}
|
|
2431
|
+
function rowString(value) {
|
|
2432
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2433
|
+
}
|
|
2434
|
+
function rerollRequestFrom(row) {
|
|
2435
|
+
return {
|
|
2436
|
+
id: String(row.id),
|
|
2437
|
+
project_id: String(row.project_id),
|
|
2438
|
+
root_asset_id: String(row.root_asset_id),
|
|
2439
|
+
node_asset_id: String(row.node_asset_id),
|
|
2440
|
+
status: row.status,
|
|
2441
|
+
requested_by: row.requested_by,
|
|
2442
|
+
notes: rowString(row.notes),
|
|
2443
|
+
created_at: String(row.created_at),
|
|
2444
|
+
resolved_at: rowString(row.resolved_at)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function tasksByTarget(tasks) {
|
|
2448
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
2449
|
+
for (const task of tasks) {
|
|
2450
|
+
if (task.task_type !== "iterate" && task.task_type !== "reroll") continue;
|
|
2451
|
+
const existing = byTarget.get(task.target_asset_id) || {};
|
|
2452
|
+
existing[task.task_type] = task;
|
|
2453
|
+
byTarget.set(task.target_asset_id, existing);
|
|
2454
|
+
}
|
|
2455
|
+
return byTarget;
|
|
2456
|
+
}
|
|
2457
|
+
function withRerollTask(request, task) {
|
|
2458
|
+
return task ? { ...request, task_id: task.id, task } : request;
|
|
2459
|
+
}
|
|
2460
|
+
function taskBackedRerollRequest(project, rootAssetId, task) {
|
|
2461
|
+
return {
|
|
2462
|
+
id: `${task.id}:request`,
|
|
2463
|
+
project_id: project,
|
|
2464
|
+
root_asset_id: rootAssetId,
|
|
2465
|
+
node_asset_id: task.target_asset_id,
|
|
2466
|
+
status: "pending",
|
|
2467
|
+
requested_by: task.created_by,
|
|
2468
|
+
notes: task.instructions,
|
|
2469
|
+
created_at: task.created_at,
|
|
2470
|
+
task_id: task.id,
|
|
2471
|
+
task
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
function attemptFrom(row) {
|
|
2475
|
+
return {
|
|
2476
|
+
id: String(row.id),
|
|
2477
|
+
project_id: String(row.project_id),
|
|
2478
|
+
node_asset_id: String(row.node_asset_id),
|
|
2479
|
+
asset_id: String(row.asset_id),
|
|
2480
|
+
attempt_index: Number(row.attempt_index),
|
|
2481
|
+
source: row.source,
|
|
2482
|
+
prompt: rowString(row.prompt),
|
|
2483
|
+
generation_job_id: rowString(row.generation_job_id),
|
|
2484
|
+
file_path: rowString(row.file_path),
|
|
2485
|
+
checksum_sha256: rowString(row.checksum_sha256),
|
|
2486
|
+
created_at: String(row.created_at),
|
|
2487
|
+
promoted_at: rowString(row.promoted_at),
|
|
2488
|
+
is_current: Boolean(Number(row.is_current))
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
function implicitAttempt(row, isCurrent = true) {
|
|
2492
|
+
const createdAt = row.asset_created_at || nowIso();
|
|
2493
|
+
return {
|
|
2494
|
+
id: `${row.project}:${row.asset_id}:attempt:implicit`,
|
|
2495
|
+
project_id: row.project,
|
|
2496
|
+
node_asset_id: row.asset_id,
|
|
2497
|
+
asset_id: row.asset_id,
|
|
2498
|
+
attempt_index: 1,
|
|
2499
|
+
source: "initial",
|
|
2500
|
+
file_path: row.local_path,
|
|
2501
|
+
checksum_sha256: row.checksum_sha256,
|
|
2502
|
+
created_at: createdAt,
|
|
2503
|
+
promoted_at: createdAt,
|
|
2504
|
+
is_current: isCurrent
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
function withImplicitAttempt(physicalAttempts, row) {
|
|
2508
|
+
if (physicalAttempts.some((attempt) => attempt.source === "initial")) return physicalAttempts;
|
|
2509
|
+
return [...physicalAttempts, implicitAttempt(row, !physicalAttempts.some((attempt) => attempt.is_current))];
|
|
2510
|
+
}
|
|
2511
|
+
function assertNodeInRoot(database, project, root, node) {
|
|
2512
|
+
assertCanonicalRoot(database, project, root);
|
|
2513
|
+
requireAsset2(database, project, node);
|
|
2514
|
+
const nodeRoot = rootFor(database, project, node);
|
|
2515
|
+
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
2516
|
+
}
|
|
2517
|
+
function assertAttemptAssetNotVisibleLineageNode(database, project, root, assetId) {
|
|
2518
|
+
const visibleNodeIds = /* @__PURE__ */ new Set([
|
|
2519
|
+
root,
|
|
2520
|
+
...descendants(database, project, root).flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])
|
|
2521
|
+
]);
|
|
2522
|
+
if (visibleNodeIds.has(assetId)) throw new LineageError(`Attempt asset ${assetId} is already a visible lineage node in ${root}`, 400);
|
|
2523
|
+
}
|
|
1414
2524
|
function canPreviewLocally(mediaType, localPath) {
|
|
1415
2525
|
return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
|
|
1416
2526
|
}
|
|
@@ -1421,8 +2531,8 @@ function localPreviewUrl(project, localPath) {
|
|
|
1421
2531
|
}
|
|
1422
2532
|
function linkLineageAssets(project, fields) {
|
|
1423
2533
|
const database = lineageDb();
|
|
1424
|
-
|
|
1425
|
-
|
|
2534
|
+
requireAsset2(database, project, fields.parentAssetId);
|
|
2535
|
+
requireAsset2(database, project, fields.childAssetId);
|
|
1426
2536
|
if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
|
|
1427
2537
|
const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
|
|
1428
2538
|
try {
|
|
@@ -1473,20 +2583,37 @@ function descendants(database, project, root) {
|
|
|
1473
2583
|
}
|
|
1474
2584
|
function getLineageSnapshot(project, assetId) {
|
|
1475
2585
|
const database = lineageDb();
|
|
1476
|
-
|
|
2586
|
+
requireAsset2(database, project, assetId);
|
|
1477
2587
|
const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
|
|
1478
2588
|
const edges = descendants(database, project, root);
|
|
1479
|
-
const
|
|
2589
|
+
const selected = selectedRows(database, project, root);
|
|
2590
|
+
const tasks = listLineageTasks(project, root).tasks;
|
|
2591
|
+
const ids = [.../* @__PURE__ */ new Set([
|
|
2592
|
+
root,
|
|
2593
|
+
...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id]),
|
|
2594
|
+
...selected.map((row) => row.asset_id),
|
|
2595
|
+
...tasks.map((task) => task.target_asset_id)
|
|
2596
|
+
])];
|
|
1480
2597
|
const placeholders = ids.map(() => "?").join(",");
|
|
1481
2598
|
const rows = database.prepare(`
|
|
1482
2599
|
select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
|
|
1483
2600
|
a.local_path, a.s3_key, a.checksum_sha256, coalesce(r.review_state, 'unreviewed') review_state,
|
|
1484
|
-
r.notes review_notes, l.x layout_x, l.y layout_y
|
|
2601
|
+
r.notes review_notes, a.created_at asset_created_at, l.x layout_x, l.y layout_y
|
|
1485
2602
|
from assets a left join asset_reviews r on r.asset_id = a.id
|
|
1486
2603
|
left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
|
|
1487
2604
|
where a.project_id = ? and a.id in (${placeholders})
|
|
1488
2605
|
`).all(root, project, ...ids);
|
|
1489
|
-
const
|
|
2606
|
+
const attemptRows = ids.length > 0 ? database.prepare(`select * from asset_attempts where project_id = ? and node_asset_id in (${placeholders}) order by node_asset_id, attempt_index desc`).all(project, ...ids) : [];
|
|
2607
|
+
const attemptsByNode = /* @__PURE__ */ new Map();
|
|
2608
|
+
for (const attempt of attemptRows.map(attemptFrom)) {
|
|
2609
|
+
attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
|
|
2610
|
+
}
|
|
2611
|
+
const lineageTasksByNode = tasksByTarget(tasks);
|
|
2612
|
+
const rerollRows = ids.length > 0 ? database.prepare(`select * from asset_reroll_requests where project_id = ? and root_asset_id = ? and status = 'pending' and node_asset_id in (${placeholders}) order by created_at`).all(project, root, ...ids) : [];
|
|
2613
|
+
const rerollsByNode = new Map(rerollRows.map((row) => {
|
|
2614
|
+
const request = rerollRequestFrom(row);
|
|
2615
|
+
return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
|
|
2616
|
+
}));
|
|
1490
2617
|
const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
|
|
1491
2618
|
const selectedIds = new Set(selected.map((row) => row.asset_id));
|
|
1492
2619
|
const selections = selected.map((row) => ({
|
|
@@ -1498,13 +2625,21 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1498
2625
|
const selection = selections[0] || null;
|
|
1499
2626
|
const nodes = rows.map((row) => {
|
|
1500
2627
|
const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
|
|
1501
|
-
const { layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
|
|
2628
|
+
const { asset_created_at: _assetCreatedAt, layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
|
|
1502
2629
|
const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
|
|
2630
|
+
const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
|
|
2631
|
+
const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
|
|
2632
|
+
const previewPath = currentAttempt?.file_path || row.local_path;
|
|
2633
|
+
const lineageTasks = lineageTasksByNode.get(row.asset_id);
|
|
1503
2634
|
return {
|
|
1504
2635
|
...node,
|
|
2636
|
+
attempt_count: attempts.length,
|
|
2637
|
+
current_attempt: currentAttempt,
|
|
1505
2638
|
is_latest: !childIds.has(row.asset_id),
|
|
2639
|
+
lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
|
|
1506
2640
|
position,
|
|
1507
|
-
preview_url: canPreviewLocally(row.media_type,
|
|
2641
|
+
preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
|
|
2642
|
+
reroll_request: rerollsByNode.get(row.asset_id),
|
|
1508
2643
|
selection_note: nodeSelection?.notes,
|
|
1509
2644
|
user_selected: selectedIds.has(row.asset_id)
|
|
1510
2645
|
};
|
|
@@ -1517,6 +2652,7 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1517
2652
|
selected: selections.map((row) => row.asset_id),
|
|
1518
2653
|
selection,
|
|
1519
2654
|
selections,
|
|
2655
|
+
tasks,
|
|
1520
2656
|
latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
|
|
1521
2657
|
nodes,
|
|
1522
2658
|
edges,
|
|
@@ -1528,7 +2664,25 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1528
2664
|
const root = resolveRoot(database, project, rootAssetId);
|
|
1529
2665
|
database.close();
|
|
1530
2666
|
const snapshot = getLineageSnapshot(project, root);
|
|
1531
|
-
const
|
|
2667
|
+
const pendingIterateTasks = (snapshot.tasks || []).filter((task) => task.task_type === "iterate" && task.status === "pending");
|
|
2668
|
+
const taskIdsInSelectionOrder = [
|
|
2669
|
+
...snapshot.selections.map((selection) => selection.asset_id).filter((assetId) => pendingIterateTasks.some((task) => task.target_asset_id === assetId)),
|
|
2670
|
+
...pendingIterateTasks.map((task) => task.target_asset_id).filter((assetId) => !snapshot.selections.some((selection) => selection.asset_id === assetId))
|
|
2671
|
+
];
|
|
2672
|
+
const taskSelectedIds = [...new Set(taskIdsInSelectionOrder)];
|
|
2673
|
+
const selectedIds = taskSelectedIds.length > 0 ? taskSelectedIds : snapshot.selected;
|
|
2674
|
+
const selectedNodes = selectedIds.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
|
|
2675
|
+
const taskSelections = taskSelectedIds.map((assetId, position) => {
|
|
2676
|
+
const task = pendingIterateTasks.find((item) => item.target_asset_id === assetId);
|
|
2677
|
+
return {
|
|
2678
|
+
asset_id: assetId,
|
|
2679
|
+
notes: task?.instructions,
|
|
2680
|
+
position,
|
|
2681
|
+
selected_at: task?.created_at || nowIso()
|
|
2682
|
+
};
|
|
2683
|
+
});
|
|
2684
|
+
const selectedSelection = taskSelections.length > 0 ? taskSelections[0] : snapshot.selection;
|
|
2685
|
+
const selectedSelections = taskSelections.length > 0 ? taskSelections : snapshot.selections;
|
|
1532
2686
|
const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
|
|
1533
2687
|
const warnings = [];
|
|
1534
2688
|
for (const selectedNode of selectedNodes) {
|
|
@@ -1546,9 +2700,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1546
2700
|
next_asset: selectedNodes[0],
|
|
1547
2701
|
next_assets: selectedNodes,
|
|
1548
2702
|
latest: snapshot.latest,
|
|
1549
|
-
selected:
|
|
1550
|
-
selection:
|
|
1551
|
-
selections:
|
|
2703
|
+
selected: selectedIds,
|
|
2704
|
+
selection: selectedSelection,
|
|
2705
|
+
selections: selectedSelections,
|
|
1552
2706
|
candidates: latestNodes,
|
|
1553
2707
|
warnings,
|
|
1554
2708
|
fetchedAt: nowIso()
|
|
@@ -1565,9 +2719,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1565
2719
|
next_asset: latestNodes[0],
|
|
1566
2720
|
next_assets: [latestNodes[0]],
|
|
1567
2721
|
latest: snapshot.latest,
|
|
1568
|
-
selected:
|
|
1569
|
-
selection:
|
|
1570
|
-
selections:
|
|
2722
|
+
selected: selectedIds,
|
|
2723
|
+
selection: selectedSelection,
|
|
2724
|
+
selections: selectedSelections,
|
|
1571
2725
|
candidates: latestNodes,
|
|
1572
2726
|
warnings,
|
|
1573
2727
|
fetchedAt: nowIso()
|
|
@@ -1583,14 +2737,202 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1583
2737
|
next_asset: null,
|
|
1584
2738
|
next_assets: [],
|
|
1585
2739
|
latest: snapshot.latest,
|
|
1586
|
-
selected:
|
|
1587
|
-
selection:
|
|
1588
|
-
selections:
|
|
2740
|
+
selected: selectedIds,
|
|
2741
|
+
selection: selectedSelection,
|
|
2742
|
+
selections: selectedSelections,
|
|
1589
2743
|
candidates: latestNodes,
|
|
1590
2744
|
warnings,
|
|
1591
2745
|
fetchedAt: nowIso()
|
|
1592
2746
|
};
|
|
1593
2747
|
}
|
|
2748
|
+
function listLineageRerollRequests(project, rootAssetId) {
|
|
2749
|
+
const database = lineageDb();
|
|
2750
|
+
try {
|
|
2751
|
+
assertCanonicalRoot(database, project, rootAssetId);
|
|
2752
|
+
const rerollTasks = listLineageTasks(project, rootAssetId).tasks.filter((task) => task.task_type === "reroll");
|
|
2753
|
+
const pendingRerollTasks = listLineageTasks(project, rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "reroll");
|
|
2754
|
+
const rerollTaskByTarget = new Map(rerollTasks.map((task) => [task.target_asset_id, task]));
|
|
2755
|
+
const rows = database.prepare(`
|
|
2756
|
+
select * from asset_reroll_requests
|
|
2757
|
+
where project_id = ? and root_asset_id = ? and status = 'pending'
|
|
2758
|
+
order by created_at, id
|
|
2759
|
+
`).all(project, rootAssetId);
|
|
2760
|
+
const requests = rows.map((row) => {
|
|
2761
|
+
const request = rerollRequestFrom(row);
|
|
2762
|
+
return withRerollTask(request, rerollTaskByTarget.get(request.node_asset_id));
|
|
2763
|
+
});
|
|
2764
|
+
const legacyTargets = new Set(requests.map((request) => request.node_asset_id));
|
|
2765
|
+
for (const task of pendingRerollTasks) {
|
|
2766
|
+
if (!legacyTargets.has(task.target_asset_id)) requests.push(taskBackedRerollRequest(project, rootAssetId, task));
|
|
2767
|
+
}
|
|
2768
|
+
return { project, root_asset_id: rootAssetId, requests, fetchedAt: nowIso() };
|
|
2769
|
+
} finally {
|
|
2770
|
+
database.close();
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function recordLineageRerollAttempt(project, fields) {
|
|
2774
|
+
const database = lineageDb();
|
|
2775
|
+
try {
|
|
2776
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2777
|
+
requireAsset2(database, project, fields.assetId);
|
|
2778
|
+
assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
|
|
2779
|
+
const timestamp = nowIso();
|
|
2780
|
+
const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
|
|
2781
|
+
const attemptIndex = Number(maxRow.max_index || 1) + 1;
|
|
2782
|
+
const attempt = {
|
|
2783
|
+
id: `${project}:${fields.nodeAssetId}:attempt:${attemptIndex}`,
|
|
2784
|
+
project_id: project,
|
|
2785
|
+
node_asset_id: fields.nodeAssetId,
|
|
2786
|
+
asset_id: fields.assetId,
|
|
2787
|
+
attempt_index: attemptIndex,
|
|
2788
|
+
source: "reroll",
|
|
2789
|
+
prompt: fields.prompt,
|
|
2790
|
+
generation_job_id: fields.generationJobId,
|
|
2791
|
+
file_path: fields.filePath,
|
|
2792
|
+
checksum_sha256: fields.checksumSha256,
|
|
2793
|
+
created_at: timestamp,
|
|
2794
|
+
promoted_at: timestamp,
|
|
2795
|
+
is_current: true
|
|
2796
|
+
};
|
|
2797
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, attempt };
|
|
2798
|
+
database.exec("BEGIN IMMEDIATE");
|
|
2799
|
+
try {
|
|
2800
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
|
|
2801
|
+
database.prepare(`
|
|
2802
|
+
insert into asset_attempts (
|
|
2803
|
+
id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
|
|
2804
|
+
file_path, checksum_sha256, created_at, promoted_at, is_current
|
|
2805
|
+
) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, 1)
|
|
2806
|
+
`).run(
|
|
2807
|
+
attempt.id,
|
|
2808
|
+
project,
|
|
2809
|
+
fields.nodeAssetId,
|
|
2810
|
+
fields.assetId,
|
|
2811
|
+
attempt.attempt_index,
|
|
2812
|
+
fields.prompt,
|
|
2813
|
+
fields.generationJobId,
|
|
2814
|
+
fields.filePath,
|
|
2815
|
+
fields.checksumSha256,
|
|
2816
|
+
timestamp,
|
|
2817
|
+
timestamp
|
|
2818
|
+
);
|
|
2819
|
+
database.prepare(`
|
|
2820
|
+
update asset_reroll_requests
|
|
2821
|
+
set status = 'resolved', resolved_at = ?
|
|
2822
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
2823
|
+
`).run(timestamp, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2824
|
+
database.prepare(`
|
|
2825
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
2826
|
+
values (?, 'unreviewed', ?, null, null, ?)
|
|
2827
|
+
on conflict(asset_id) do update set
|
|
2828
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
2829
|
+
ignored_at = excluded.ignored_at, updated_at = excluded.updated_at
|
|
2830
|
+
`).run(fields.nodeAssetId, timestamp, timestamp);
|
|
2831
|
+
database.exec("COMMIT");
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
database.exec("ROLLBACK");
|
|
2834
|
+
throw error;
|
|
2835
|
+
}
|
|
2836
|
+
return { ok: true, attempt };
|
|
2837
|
+
} finally {
|
|
2838
|
+
database.close();
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
function markLineageRerollRequest(project, fields) {
|
|
2842
|
+
const database = lineageDb();
|
|
2843
|
+
try {
|
|
2844
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2845
|
+
const existing = database.prepare(`
|
|
2846
|
+
select * from asset_reroll_requests
|
|
2847
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
2848
|
+
order by created_at desc limit 1
|
|
2849
|
+
`).get(project, fields.rootAssetId, fields.nodeAssetId);
|
|
2850
|
+
const timestamp = nowIso();
|
|
2851
|
+
const request = existing ? {
|
|
2852
|
+
...rerollRequestFrom(existing),
|
|
2853
|
+
notes: fields.notes || rerollRequestFrom(existing).notes,
|
|
2854
|
+
requested_by: fields.requestedBy || rerollRequestFrom(existing).requested_by
|
|
2855
|
+
} : {
|
|
2856
|
+
id: rerollRequestId(project, fields.rootAssetId, fields.nodeAssetId, timestamp),
|
|
2857
|
+
project_id: project,
|
|
2858
|
+
root_asset_id: fields.rootAssetId,
|
|
2859
|
+
node_asset_id: fields.nodeAssetId,
|
|
2860
|
+
status: "pending",
|
|
2861
|
+
requested_by: fields.requestedBy || "human",
|
|
2862
|
+
notes: fields.notes,
|
|
2863
|
+
created_at: timestamp
|
|
2864
|
+
};
|
|
2865
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2866
|
+
const taskResult = upsertLineageTask(project, {
|
|
2867
|
+
createdBy: request.requested_by,
|
|
2868
|
+
instructions: request.notes,
|
|
2869
|
+
rootAssetId: fields.rootAssetId,
|
|
2870
|
+
targetAssetId: fields.nodeAssetId,
|
|
2871
|
+
taskType: "reroll"
|
|
2872
|
+
});
|
|
2873
|
+
if (existing) {
|
|
2874
|
+
database.prepare(`
|
|
2875
|
+
update asset_reroll_requests
|
|
2876
|
+
set requested_by = ?, notes = ?
|
|
2877
|
+
where id = ?
|
|
2878
|
+
`).run(request.requested_by, request.notes || null, request.id);
|
|
2879
|
+
} else {
|
|
2880
|
+
database.prepare(`
|
|
2881
|
+
insert into asset_reroll_requests (id, project_id, root_asset_id, node_asset_id, status, requested_by, notes, created_at, resolved_at)
|
|
2882
|
+
values (?, ?, ?, ?, 'pending', ?, ?, ?, null)
|
|
2883
|
+
`).run(request.id, project, fields.rootAssetId, fields.nodeAssetId, request.requested_by, request.notes || null, request.created_at);
|
|
2884
|
+
}
|
|
2885
|
+
database.prepare(`
|
|
2886
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
2887
|
+
values (?, 'needs_revision', ?, null, ?, ?)
|
|
2888
|
+
on conflict(asset_id) do update set
|
|
2889
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
2890
|
+
ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
|
|
2891
|
+
`).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
|
|
2892
|
+
return {
|
|
2893
|
+
ok: true,
|
|
2894
|
+
request: withRerollTask(rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)), taskResult.task),
|
|
2895
|
+
task_id: taskResult.task.id,
|
|
2896
|
+
task: taskResult.task
|
|
2897
|
+
};
|
|
2898
|
+
} finally {
|
|
2899
|
+
database.close();
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
function clearLineageRerollRequest(project, fields) {
|
|
2903
|
+
const database = lineageDb();
|
|
2904
|
+
try {
|
|
2905
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2906
|
+
const existing = database.prepare(`
|
|
2907
|
+
select * from asset_reroll_requests
|
|
2908
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
2909
|
+
order by created_at desc limit 1
|
|
2910
|
+
`).get(project, fields.rootAssetId, fields.nodeAssetId);
|
|
2911
|
+
if (!existing) throw new LineageError(`No pending re-roll request for ${fields.nodeAssetId}`, 404);
|
|
2912
|
+
const timestamp = nowIso();
|
|
2913
|
+
const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
|
|
2914
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2915
|
+
const task = listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.find((item) => item.task_type === "reroll" && item.target_asset_id === fields.nodeAssetId);
|
|
2916
|
+
const cancelledTask = task ? cancelLineageTask(project, {
|
|
2917
|
+
actor: "human",
|
|
2918
|
+
confirmWrite: true,
|
|
2919
|
+
taskId: task.id
|
|
2920
|
+
}).task : void 0;
|
|
2921
|
+
database.prepare(`
|
|
2922
|
+
update asset_reroll_requests
|
|
2923
|
+
set status = 'cancelled', resolved_at = ?
|
|
2924
|
+
where id = ?
|
|
2925
|
+
`).run(timestamp, request.id);
|
|
2926
|
+
return {
|
|
2927
|
+
ok: true,
|
|
2928
|
+
request: withRerollTask(request, cancelledTask),
|
|
2929
|
+
task_id: cancelledTask?.id,
|
|
2930
|
+
task: cancelledTask
|
|
2931
|
+
};
|
|
2932
|
+
} finally {
|
|
2933
|
+
database.close();
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
1594
2936
|
|
|
1595
2937
|
// src/server/assetLineageHandoff.ts
|
|
1596
2938
|
var publicPackageCommand = "npx @mean-weasel/lineage";
|
|
@@ -1603,6 +2945,9 @@ function lineageCommand(command, project, rootAssetId) {
|
|
|
1603
2945
|
function linkChildCommand(project, rootAssetId) {
|
|
1604
2946
|
return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
|
|
1605
2947
|
}
|
|
2948
|
+
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
2949
|
+
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
2950
|
+
}
|
|
1606
2951
|
function getLineageBrief(project, rootAssetId) {
|
|
1607
2952
|
const next = getLineageNextAsset(project, rootAssetId);
|
|
1608
2953
|
const assets = next.next_assets;
|
|
@@ -1649,6 +2994,14 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
1649
2994
|
function linkSelectedLineageChild(project, fields) {
|
|
1650
2995
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
1651
2996
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
2997
|
+
const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
|
|
2998
|
+
const pendingRerollForParent = rerollRequests.find((request) => request.node_asset_id === next.next_asset?.asset_id);
|
|
2999
|
+
if (pendingRerollForParent && fields.confirmWrite) {
|
|
3000
|
+
throw new LineageError(
|
|
3001
|
+
`Pending re-roll exists for ${pendingRerollForParent.node_asset_id}. lineage link-child creates a visible child variation edge; it does not re-roll the same node. ${rerollImportGuidance(next.root_asset_id, pendingRerollForParent.node_asset_id)} Cancel the re-roll first if you intentionally want a new child variation.`,
|
|
3002
|
+
409
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
1652
3005
|
if (fields.confirmWrite) {
|
|
1653
3006
|
const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
|
|
1654
3007
|
const validation = validateAgentClaimForWrite({
|
|
@@ -1675,21 +3028,325 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
1675
3028
|
parent_asset_id: next.next_asset.asset_id,
|
|
1676
3029
|
child_asset_id: fields.childAssetId,
|
|
1677
3030
|
reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
|
|
1678
|
-
warning:
|
|
3031
|
+
warning: [
|
|
3032
|
+
pendingRerollForParent ? `Pending re-roll exists for ${pendingRerollForParent.node_asset_id}. link-child would create a visible child variation; use reroll plan/import to update the same node attempt.` : void 0,
|
|
3033
|
+
rerollRequests.length > 0 && !pendingRerollForParent ? `This lineage has ${rerollRequests.length} pending re-roll target(s). link-child is only for new visible child variations, not re-roll attempts.` : void 0,
|
|
3034
|
+
next.next_assets.length > 1 ? "Linked child to the primary selected base; add explicit edges to other selected references if the child derives from them too." : void 0
|
|
3035
|
+
].filter(Boolean).join(" ") || void 0
|
|
1679
3036
|
};
|
|
1680
3037
|
}
|
|
1681
3038
|
|
|
3039
|
+
// src/server/generationReceipts.ts
|
|
3040
|
+
import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "node:fs";
|
|
3041
|
+
import { relative as relative2, resolve as resolve3 } from "node:path";
|
|
3042
|
+
import { randomUUID } from "node:crypto";
|
|
3043
|
+
var adapterVersion = "generation-receipts-v1";
|
|
3044
|
+
var provider = "codex-handoff";
|
|
3045
|
+
var GenerationReceiptError = class extends Error {
|
|
3046
|
+
constructor(message, status = 400) {
|
|
3047
|
+
super(message);
|
|
3048
|
+
this.status = status;
|
|
3049
|
+
}
|
|
3050
|
+
status;
|
|
3051
|
+
};
|
|
3052
|
+
function jobId() {
|
|
3053
|
+
return `gen-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
3054
|
+
}
|
|
3055
|
+
function quote(value) {
|
|
3056
|
+
return JSON.stringify(value);
|
|
3057
|
+
}
|
|
3058
|
+
function parseJson(value, fallback) {
|
|
3059
|
+
if (!value) return fallback;
|
|
3060
|
+
return JSON.parse(value);
|
|
3061
|
+
}
|
|
3062
|
+
function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
3063
|
+
const importCommand = `npx lineage reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write --json`;
|
|
3064
|
+
return {
|
|
3065
|
+
schema_version: "lineage.generation_handoff.v1",
|
|
3066
|
+
provider,
|
|
3067
|
+
project,
|
|
3068
|
+
job_id: id,
|
|
3069
|
+
prompt,
|
|
3070
|
+
expected_output_count: 1,
|
|
3071
|
+
lineage: {
|
|
3072
|
+
root_asset_id: rootAssetId,
|
|
3073
|
+
parent_asset_id: target.asset_id,
|
|
3074
|
+
selection_strategy: "reroll_request",
|
|
3075
|
+
parent_title: target.title,
|
|
3076
|
+
parent_local_path: target.local_path,
|
|
3077
|
+
parent_s3_key: target.s3_key
|
|
3078
|
+
},
|
|
3079
|
+
instructions: [
|
|
3080
|
+
"Use Codex image generation outside Lineage server code.",
|
|
3081
|
+
"Write the regenerated output file under .asset-scratch before import.",
|
|
3082
|
+
"Do not call live provider APIs from the CLI or server.",
|
|
3083
|
+
"Import exactly one output with reroll import, not link-child or generate image import.",
|
|
3084
|
+
`Resolve re-roll request ${request.id}; do not create a visible lineage child edge.`
|
|
3085
|
+
],
|
|
3086
|
+
import_command: importCommand,
|
|
3087
|
+
guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
function receiptFrom(row) {
|
|
3091
|
+
return {
|
|
3092
|
+
id: String(row.id),
|
|
3093
|
+
job_id: String(row.job_id),
|
|
3094
|
+
receipt_type: row.receipt_type,
|
|
3095
|
+
status: row.status,
|
|
3096
|
+
command: String(row.command),
|
|
3097
|
+
payload: parseJson(String(row.payload_json), null),
|
|
3098
|
+
created_at: String(row.created_at)
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
function outputFrom(row) {
|
|
3102
|
+
return {
|
|
3103
|
+
id: String(row.id),
|
|
3104
|
+
job_id: String(row.job_id),
|
|
3105
|
+
project_id: String(row.project_id),
|
|
3106
|
+
output_index: Number(row.output_index),
|
|
3107
|
+
file_path: String(row.file_path),
|
|
3108
|
+
checksum_sha256: String(row.checksum_sha256),
|
|
3109
|
+
size_bytes: Number(row.size_bytes),
|
|
3110
|
+
content_type: String(row.content_type),
|
|
3111
|
+
imported_asset_id: String(row.imported_asset_id),
|
|
3112
|
+
parent_asset_id: String(row.parent_asset_id),
|
|
3113
|
+
imported_at: String(row.imported_at)
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function loadGenerationJob(database, project, id) {
|
|
3117
|
+
const row = database.prepare("select * from generation_jobs where project_id = ? and id = ?").get(project, id);
|
|
3118
|
+
if (!row) throw new GenerationReceiptError(`Unknown generation job: ${id}`, 404);
|
|
3119
|
+
const inputRows = database.prepare("select * from generation_job_inputs where job_id = ? order by position").all(id);
|
|
3120
|
+
const inputs = inputRows.map((input) => ({
|
|
3121
|
+
id: String(input.id),
|
|
3122
|
+
job_id: String(input.job_id),
|
|
3123
|
+
project_id: String(input.project_id),
|
|
3124
|
+
asset_id: String(input.asset_id),
|
|
3125
|
+
root_asset_id: String(input.root_asset_id),
|
|
3126
|
+
role: input.role,
|
|
3127
|
+
position: Number(input.position),
|
|
3128
|
+
selection_strategy: String(input.selection_strategy),
|
|
3129
|
+
selection_snapshot: parseJson(String(input.selection_snapshot_json), {})
|
|
3130
|
+
}));
|
|
3131
|
+
const outputs = database.prepare("select * from generation_job_outputs where job_id = ? order by output_index").all(id).map(outputFrom);
|
|
3132
|
+
const receipts = database.prepare("select * from generation_job_receipts where job_id = ? order by created_at, id").all(id).map(receiptFrom);
|
|
3133
|
+
return {
|
|
3134
|
+
id: String(row.id),
|
|
3135
|
+
project_id: String(row.project_id),
|
|
3136
|
+
provider: row.provider,
|
|
3137
|
+
adapter_version: String(row.adapter_version),
|
|
3138
|
+
source_mode: String(row.source_mode),
|
|
3139
|
+
root_asset_id: String(row.root_asset_id),
|
|
3140
|
+
prompt: String(row.prompt),
|
|
3141
|
+
expected_output_count: Number(row.expected_output_count),
|
|
3142
|
+
status: row.status,
|
|
3143
|
+
output_dir: typeof row.output_dir === "string" ? row.output_dir : void 0,
|
|
3144
|
+
handoff: parseJson(String(row.handoff_json), {}),
|
|
3145
|
+
created_at: String(row.created_at),
|
|
3146
|
+
updated_at: String(row.updated_at),
|
|
3147
|
+
imported_at: typeof row.imported_at === "string" ? row.imported_at : void 0,
|
|
3148
|
+
inputs,
|
|
3149
|
+
outputs,
|
|
3150
|
+
receipts
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
function insertReceipt(database, id, type, command, payload) {
|
|
3154
|
+
database.prepare(`
|
|
3155
|
+
insert into generation_job_receipts (id, job_id, receipt_type, status, command, payload_json, created_at)
|
|
3156
|
+
values (?, ?, ?, 'ok', ?, ?, ?)
|
|
3157
|
+
`).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
|
|
3158
|
+
}
|
|
3159
|
+
function planImageReroll(project = defaultProject, fields) {
|
|
3160
|
+
const prompt = fields.prompt.trim();
|
|
3161
|
+
if (!prompt) throw new GenerationReceiptError("Missing --prompt");
|
|
3162
|
+
if (!fields.rootAssetId) throw new GenerationReceiptError("Missing --root");
|
|
3163
|
+
if (!fields.targetAssetId) throw new GenerationReceiptError("Missing --target");
|
|
3164
|
+
const snapshot = getLineageSnapshot(project, fields.rootAssetId);
|
|
3165
|
+
const target = snapshot.nodes.find((node) => node.asset_id === fields.targetAssetId);
|
|
3166
|
+
if (!target) throw new GenerationReceiptError(`Re-roll target is not in lineage: ${fields.targetAssetId}`, 404);
|
|
3167
|
+
const request = listLineageRerollRequests(project, snapshot.root_asset_id).requests.find((item) => item.node_asset_id === fields.targetAssetId);
|
|
3168
|
+
if (!request) throw new GenerationReceiptError(`No pending re-roll request for ${fields.targetAssetId}`);
|
|
3169
|
+
const id = jobId();
|
|
3170
|
+
const timestamp = nowIso();
|
|
3171
|
+
const handoff = buildRerollHandoff(project, id, prompt, snapshot.root_asset_id, target, request);
|
|
3172
|
+
const input = {
|
|
3173
|
+
id: `${id}:input:0`,
|
|
3174
|
+
job_id: id,
|
|
3175
|
+
project_id: project,
|
|
3176
|
+
asset_id: target.asset_id,
|
|
3177
|
+
root_asset_id: snapshot.root_asset_id,
|
|
3178
|
+
role: "reroll_target",
|
|
3179
|
+
position: 0,
|
|
3180
|
+
selection_strategy: "reroll_request",
|
|
3181
|
+
selection_snapshot: {
|
|
3182
|
+
project,
|
|
3183
|
+
root_asset_id: snapshot.root_asset_id,
|
|
3184
|
+
strategy: "selected",
|
|
3185
|
+
selection_mode: "single",
|
|
3186
|
+
recommended_action: "evolve_variations",
|
|
3187
|
+
reason: "user_selected",
|
|
3188
|
+
next_asset: target,
|
|
3189
|
+
next_assets: [target],
|
|
3190
|
+
latest: snapshot.latest,
|
|
3191
|
+
selected: [target.asset_id],
|
|
3192
|
+
selection: null,
|
|
3193
|
+
selections: [],
|
|
3194
|
+
candidates: snapshot.nodes,
|
|
3195
|
+
warnings: ["Re-roll target: import output as an attempt, not a lineage child."],
|
|
3196
|
+
fetchedAt: timestamp
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
const preview = {
|
|
3200
|
+
id,
|
|
3201
|
+
project_id: project,
|
|
3202
|
+
provider,
|
|
3203
|
+
adapter_version: adapterVersion,
|
|
3204
|
+
source_mode: "lineage_reroll",
|
|
3205
|
+
root_asset_id: snapshot.root_asset_id,
|
|
3206
|
+
prompt,
|
|
3207
|
+
expected_output_count: 1,
|
|
3208
|
+
status: "planned",
|
|
3209
|
+
output_dir: ".asset-scratch",
|
|
3210
|
+
handoff,
|
|
3211
|
+
created_at: timestamp,
|
|
3212
|
+
updated_at: timestamp,
|
|
3213
|
+
inputs: [input],
|
|
3214
|
+
outputs: [],
|
|
3215
|
+
receipts: [{
|
|
3216
|
+
id: `${id}:receipt:plan:preview`,
|
|
3217
|
+
job_id: id,
|
|
3218
|
+
receipt_type: "plan",
|
|
3219
|
+
status: "ok",
|
|
3220
|
+
command: "reroll plan",
|
|
3221
|
+
payload: { prompt, expected_output_count: 1, lineage: handoff.lineage, reroll_request_id: request.id },
|
|
3222
|
+
created_at: timestamp
|
|
3223
|
+
}]
|
|
3224
|
+
};
|
|
3225
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
|
|
3226
|
+
const database = lineageDb();
|
|
3227
|
+
try {
|
|
3228
|
+
database.exec("BEGIN IMMEDIATE");
|
|
3229
|
+
try {
|
|
3230
|
+
database.prepare(`
|
|
3231
|
+
insert into generation_jobs (
|
|
3232
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
3233
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at
|
|
3234
|
+
) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
|
|
3235
|
+
`).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
|
|
3236
|
+
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));
|
|
3237
|
+
insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
|
|
3238
|
+
database.exec("COMMIT");
|
|
3239
|
+
} catch (error) {
|
|
3240
|
+
database.exec("ROLLBACK");
|
|
3241
|
+
throw error;
|
|
3242
|
+
}
|
|
3243
|
+
return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
|
|
3244
|
+
} finally {
|
|
3245
|
+
database.close();
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
function isPathInside(child, parent) {
|
|
3249
|
+
const rel = relative2(parent, child);
|
|
3250
|
+
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
3251
|
+
}
|
|
3252
|
+
function resolveScratchFile(file) {
|
|
3253
|
+
const scratchRoot = resolve3(repoRoot, ".asset-scratch");
|
|
3254
|
+
const candidate = file.startsWith(".asset-scratch/") || resolve3(file).startsWith(scratchRoot) ? resolve3(repoRoot, file) : resolve3(scratchRoot, file);
|
|
3255
|
+
if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3256
|
+
if (!existsSync4(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
3257
|
+
const realScratchRoot = realpathSync(scratchRoot);
|
|
3258
|
+
const realCandidate = realpathSync(candidate);
|
|
3259
|
+
if (!isPathInside(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
3260
|
+
const stats = statSync3(candidate);
|
|
3261
|
+
if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
|
|
3262
|
+
const checksum = fileSha256(candidate);
|
|
3263
|
+
return {
|
|
3264
|
+
relativePath: relative2(scratchRoot, candidate),
|
|
3265
|
+
checksum,
|
|
3266
|
+
size: stats.size,
|
|
3267
|
+
contentType: contentTypeFor(candidate),
|
|
3268
|
+
assetId: `local-${checksum.slice(0, 12)}`
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
function importImageRerollOutput(project = defaultProject, fields) {
|
|
3272
|
+
if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
|
|
3273
|
+
if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
|
|
3274
|
+
const database = lineageDb();
|
|
3275
|
+
let job;
|
|
3276
|
+
try {
|
|
3277
|
+
job = loadGenerationJob(database, project, fields.jobId);
|
|
3278
|
+
} finally {
|
|
3279
|
+
database.close();
|
|
3280
|
+
}
|
|
3281
|
+
if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
|
|
3282
|
+
if (job.source_mode !== "lineage_reroll") throw new GenerationReceiptError(`Generation job is not a re-roll job: ${job.source_mode}`);
|
|
3283
|
+
const target = job.inputs.filter((input) => input.role === "reroll_target");
|
|
3284
|
+
if (target.length !== 1) throw new GenerationReceiptError("Re-roll import requires exactly one reroll_target input");
|
|
3285
|
+
const resolved = resolveScratchFile(fields.file);
|
|
3286
|
+
indexLineageAssets(project);
|
|
3287
|
+
const writeDb = lineageDb();
|
|
3288
|
+
try {
|
|
3289
|
+
const timestamp = nowIso();
|
|
3290
|
+
writeDb.exec("BEGIN IMMEDIATE");
|
|
3291
|
+
try {
|
|
3292
|
+
const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, resolved.assetId);
|
|
3293
|
+
if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${resolved.relativePath}`);
|
|
3294
|
+
const outputId = `${fields.jobId}:output:0`;
|
|
3295
|
+
writeDb.prepare(`insert into generation_job_outputs (
|
|
3296
|
+
id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type, imported_asset_id, parent_asset_id, imported_at
|
|
3297
|
+
) values (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`).run(outputId, fields.jobId, project, resolved.relativePath, resolved.checksum, resolved.size, resolved.contentType, resolved.assetId, target[0].asset_id, timestamp);
|
|
3298
|
+
writeDb.prepare(`
|
|
3299
|
+
update generation_jobs
|
|
3300
|
+
set status = 'imported', imported_at = ?, updated_at = ?
|
|
3301
|
+
where project_id = ? and id = ?
|
|
3302
|
+
`).run(timestamp, timestamp, project, fields.jobId);
|
|
3303
|
+
insertReceipt(writeDb, fields.jobId, "import", "reroll import", {
|
|
3304
|
+
file: { output_index: 0, file_path: resolved.relativePath, imported_asset_id: resolved.assetId, parent_asset_id: target[0].asset_id },
|
|
3305
|
+
reroll: { root_asset_id: job.root_asset_id, node_asset_id: target[0].asset_id }
|
|
3306
|
+
});
|
|
3307
|
+
writeDb.exec("COMMIT");
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
writeDb.exec("ROLLBACK");
|
|
3310
|
+
throw error;
|
|
3311
|
+
}
|
|
3312
|
+
recordLineageRerollAttempt(project, {
|
|
3313
|
+
rootAssetId: job.root_asset_id,
|
|
3314
|
+
nodeAssetId: target[0].asset_id,
|
|
3315
|
+
assetId: resolved.assetId,
|
|
3316
|
+
prompt: job.prompt,
|
|
3317
|
+
generationJobId: fields.jobId,
|
|
3318
|
+
filePath: resolved.relativePath,
|
|
3319
|
+
checksumSha256: resolved.checksum,
|
|
3320
|
+
confirmWrite: true
|
|
3321
|
+
});
|
|
3322
|
+
const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
|
|
3323
|
+
if (rerollTask) {
|
|
3324
|
+
resolveLineageTask(project, {
|
|
3325
|
+
actor: "agent",
|
|
3326
|
+
confirmWrite: true,
|
|
3327
|
+
resolvedAssetId: resolved.assetId,
|
|
3328
|
+
resolvedGenerationJobId: fields.jobId,
|
|
3329
|
+
taskId: rerollTask.id
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
|
|
3333
|
+
return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
|
|
3334
|
+
} finally {
|
|
3335
|
+
writeDb.close();
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
|
|
1682
3339
|
// src/cli/lineageCli.ts
|
|
1683
3340
|
var signalExitCodes = {
|
|
1684
3341
|
SIGINT: 130,
|
|
1685
3342
|
SIGTERM: 143
|
|
1686
3343
|
};
|
|
1687
3344
|
function packageRoot() {
|
|
1688
|
-
return
|
|
3345
|
+
return resolve4(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
|
|
1689
3346
|
}
|
|
1690
3347
|
function packageVersion() {
|
|
1691
3348
|
try {
|
|
1692
|
-
const packageInfo = JSON.parse(readFileSync3(
|
|
3349
|
+
const packageInfo = JSON.parse(readFileSync3(join6(packageRoot(), "package.json"), "utf8"));
|
|
1693
3350
|
return packageInfo.version || "0.0.0";
|
|
1694
3351
|
} catch {
|
|
1695
3352
|
return "0.0.0";
|
|
@@ -1697,9 +3354,9 @@ function packageVersion() {
|
|
|
1697
3354
|
}
|
|
1698
3355
|
function dataRoot(displayName) {
|
|
1699
3356
|
if (process.env.LINEAGE_HOME) return process.env.LINEAGE_HOME;
|
|
1700
|
-
if (platform() === "darwin") return
|
|
1701
|
-
if (platform() === "win32") return
|
|
1702
|
-
return
|
|
3357
|
+
if (platform() === "darwin") return join6(homedir(), "Library", "Application Support", displayName);
|
|
3358
|
+
if (platform() === "win32") return join6(process.env.APPDATA || join6(homedir(), "AppData", "Roaming"), displayName);
|
|
3359
|
+
return join6(process.env.XDG_DATA_HOME || join6(homedir(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
|
|
1703
3360
|
}
|
|
1704
3361
|
function readOption(args, name) {
|
|
1705
3362
|
const prefix = `${name}=`;
|
|
@@ -1717,15 +3374,15 @@ function resolveStartOptions(config, args) {
|
|
|
1717
3374
|
throw new Error(`Invalid port: ${rawPort}`);
|
|
1718
3375
|
}
|
|
1719
3376
|
return {
|
|
1720
|
-
dbPath: readOption(args, "--db") || process.env.LINEAGE_DB ||
|
|
3377
|
+
dbPath: readOption(args, "--db") || process.env.LINEAGE_DB || join6(runtimeDir, `${config.binName}.sqlite`),
|
|
1721
3378
|
host: readOption(args, "--host") || process.env.HOST || config.defaultHost,
|
|
1722
3379
|
json: args.includes("--json"),
|
|
1723
3380
|
open: args.includes("--open"),
|
|
1724
3381
|
port
|
|
1725
3382
|
};
|
|
1726
3383
|
}
|
|
1727
|
-
function
|
|
1728
|
-
|
|
3384
|
+
function formatLineageHelp(config) {
|
|
3385
|
+
return `${config.binName} ${packageVersion()}
|
|
1729
3386
|
|
|
1730
3387
|
Usage:
|
|
1731
3388
|
${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
|
|
@@ -1733,6 +3390,19 @@ Usage:
|
|
|
1733
3390
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
1734
3391
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
1735
3392
|
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
3393
|
+
${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3394
|
+
${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
3395
|
+
${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
3396
|
+
${config.binName} reroll plan --root <asset-id> --target <asset-id> --prompt <text> [--project <project>] [--db <path>] [--json]
|
|
3397
|
+
${config.binName} reroll import --job-id <job-id> --file <scratch-file> --confirm-write [--project <project>] [--db <path>] [--json]
|
|
3398
|
+
${config.binName} tasks list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3399
|
+
${config.binName} tasks inspect --task <task-id> [--project <project>] [--db <path>] [--json]
|
|
3400
|
+
${config.binName} tasks claim --task <task-id> --agent-name <name> [--project <project>] [--db <path>] [--json]
|
|
3401
|
+
${config.binName} tasks start --task <task-id> --claim-token <claim-id.secret> [--project <project>] [--db <path>] [--json]
|
|
3402
|
+
${config.binName} tasks comment --task <task-id> --message <text> [--project <project>] [--db <path>] [--json]
|
|
3403
|
+
${config.binName} tasks cancel --task <task-id> [--confirm-write] [--override] [--project <project>] [--db <path>] [--json]
|
|
3404
|
+
${config.binName} tasks override --task <task-id> --reason <text> [--instructions <text>] [--project <project>] [--db <path>] [--json]
|
|
3405
|
+
${config.binName} tasks instructions --task <task-id> --instructions <text> [--project <project>] [--db <path>] [--json]
|
|
1736
3406
|
${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
|
|
1737
3407
|
${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
1738
3408
|
${config.binName} agent status [--project <project>] [--json]
|
|
@@ -1744,7 +3414,14 @@ Usage:
|
|
|
1744
3414
|
${config.binName} --help
|
|
1745
3415
|
${config.binName} --version
|
|
1746
3416
|
|
|
1747
|
-
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel
|
|
3417
|
+
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.
|
|
3418
|
+
|
|
3419
|
+
Variation vs re-roll:
|
|
3420
|
+
link-child creates a new visible child variation edge.
|
|
3421
|
+
reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
|
|
3422
|
+
}
|
|
3423
|
+
function printHelp(config) {
|
|
3424
|
+
console.log(formatLineageHelp(config));
|
|
1748
3425
|
}
|
|
1749
3426
|
function openBrowser(url) {
|
|
1750
3427
|
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
@@ -1796,8 +3473,123 @@ function runLineageDataCommand(command, args) {
|
|
|
1796
3473
|
rootAssetId: options.rootAssetId || options.assetId
|
|
1797
3474
|
});
|
|
1798
3475
|
}
|
|
3476
|
+
if (command === "reroll") {
|
|
3477
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
3478
|
+
if (subcommand === "list") {
|
|
3479
|
+
if (!options.rootAssetId) throw new Error("lineage reroll list requires --root");
|
|
3480
|
+
return listLineageRerollRequests(options.project, options.rootAssetId);
|
|
3481
|
+
}
|
|
3482
|
+
if (subcommand === "mark") {
|
|
3483
|
+
const targetAssetId = readOption(args, "--target");
|
|
3484
|
+
const requestedBy = rerollRequestedBy(readOption(args, "--requested-by") || "agent");
|
|
3485
|
+
if (!options.rootAssetId) throw new Error("lineage reroll mark requires --root");
|
|
3486
|
+
if (!targetAssetId) throw new Error("lineage reroll mark requires --target");
|
|
3487
|
+
return markLineageRerollRequest(options.project, {
|
|
3488
|
+
rootAssetId: options.rootAssetId,
|
|
3489
|
+
nodeAssetId: targetAssetId,
|
|
3490
|
+
notes: readOption(args, "--notes"),
|
|
3491
|
+
requestedBy,
|
|
3492
|
+
confirmWrite: options.confirmWrite
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
if (subcommand === "cancel") {
|
|
3496
|
+
const targetAssetId = readOption(args, "--target");
|
|
3497
|
+
if (!options.rootAssetId) throw new Error("lineage reroll cancel requires --root");
|
|
3498
|
+
if (!targetAssetId) throw new Error("lineage reroll cancel requires --target");
|
|
3499
|
+
return clearLineageRerollRequest(options.project, {
|
|
3500
|
+
rootAssetId: options.rootAssetId,
|
|
3501
|
+
nodeAssetId: targetAssetId,
|
|
3502
|
+
confirmWrite: options.confirmWrite
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
if (subcommand === "plan") {
|
|
3506
|
+
const targetAssetId = readOption(args, "--target");
|
|
3507
|
+
const prompt = readOption(args, "--prompt");
|
|
3508
|
+
if (!options.rootAssetId) throw new Error("lineage reroll plan requires --root");
|
|
3509
|
+
if (!targetAssetId) throw new Error("lineage reroll plan requires --target");
|
|
3510
|
+
if (!prompt) throw new Error("lineage reroll plan requires --prompt");
|
|
3511
|
+
return planImageReroll(options.project, {
|
|
3512
|
+
rootAssetId: options.rootAssetId,
|
|
3513
|
+
targetAssetId,
|
|
3514
|
+
prompt,
|
|
3515
|
+
dryRun: args.includes("--dry-run")
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
if (subcommand === "import") {
|
|
3519
|
+
const jobId2 = readOption(args, "--job-id");
|
|
3520
|
+
const file = readOption(args, "--file");
|
|
3521
|
+
if (!jobId2) throw new Error("lineage reroll import requires --job-id");
|
|
3522
|
+
if (!file) throw new Error("lineage reroll import requires --file");
|
|
3523
|
+
return importImageRerollOutput(options.project, { jobId: jobId2, file, confirmWrite: options.confirmWrite });
|
|
3524
|
+
}
|
|
3525
|
+
throw new Error(`Unknown reroll command: ${subcommand}`);
|
|
3526
|
+
}
|
|
3527
|
+
if (command === "tasks") {
|
|
3528
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
3529
|
+
const taskId = readOption(args, "--task");
|
|
3530
|
+
if (subcommand === "list") {
|
|
3531
|
+
if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
|
|
3532
|
+
return listLineageTasks(options.project, options.rootAssetId);
|
|
3533
|
+
}
|
|
3534
|
+
if (subcommand === "inspect") {
|
|
3535
|
+
if (!taskId) throw new Error("lineage tasks inspect requires --task");
|
|
3536
|
+
return getLineageTask(options.project, taskId);
|
|
3537
|
+
}
|
|
3538
|
+
if (subcommand === "claim") {
|
|
3539
|
+
const agentName = readOption(args, "--agent-name");
|
|
3540
|
+
if (!taskId) throw new Error("lineage tasks claim requires --task");
|
|
3541
|
+
if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
|
|
3542
|
+
return claimLineageTask(options.project, { taskId, agentName });
|
|
3543
|
+
}
|
|
3544
|
+
if (subcommand === "start") {
|
|
3545
|
+
if (!taskId) throw new Error("lineage tasks start requires --task");
|
|
3546
|
+
if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
|
|
3547
|
+
return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
|
|
3548
|
+
}
|
|
3549
|
+
if (subcommand === "comment") {
|
|
3550
|
+
const message = readOption(args, "--message");
|
|
3551
|
+
if (!taskId) throw new Error("lineage tasks comment requires --task");
|
|
3552
|
+
if (!message) throw new Error("lineage tasks comment requires --message");
|
|
3553
|
+
return addLineageTaskComment(options.project, {
|
|
3554
|
+
actor: readOption(args, "--actor") || "human",
|
|
3555
|
+
message,
|
|
3556
|
+
taskId
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
if (subcommand === "cancel") {
|
|
3560
|
+
if (!taskId) throw new Error("lineage tasks cancel requires --task");
|
|
3561
|
+
return cancelLineageTask(options.project, {
|
|
3562
|
+
actor: readOption(args, "--actor") || "human",
|
|
3563
|
+
confirmWrite: options.confirmWrite,
|
|
3564
|
+
override: args.includes("--override"),
|
|
3565
|
+
taskId
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
if (subcommand === "override") {
|
|
3569
|
+
const reason = readOption(args, "--reason");
|
|
3570
|
+
if (!taskId) throw new Error("lineage tasks override requires --task");
|
|
3571
|
+
if (!reason) throw new Error("lineage tasks override requires --reason");
|
|
3572
|
+
return overrideLineageTask(options.project, {
|
|
3573
|
+
actor: readOption(args, "--actor") || "human",
|
|
3574
|
+
instructions: readOption(args, "--instructions"),
|
|
3575
|
+
reason,
|
|
3576
|
+
taskId
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
if (subcommand === "instructions") {
|
|
3580
|
+
const instructions = readOption(args, "--instructions");
|
|
3581
|
+
if (!taskId) throw new Error("lineage tasks instructions requires --task");
|
|
3582
|
+
if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
|
|
3583
|
+
return updateLineageTaskInstructions(options.project, { instructions, taskId });
|
|
3584
|
+
}
|
|
3585
|
+
throw new Error(`Unknown tasks command: ${subcommand}`);
|
|
3586
|
+
}
|
|
1799
3587
|
throw new Error(`Unknown command: ${command}`);
|
|
1800
3588
|
}
|
|
3589
|
+
function rerollRequestedBy(value) {
|
|
3590
|
+
if (value === "agent" || value === "human" || value === "system") return value;
|
|
3591
|
+
throw new Error(`Invalid re-roll requester: ${value}`);
|
|
3592
|
+
}
|
|
1801
3593
|
function printDataResult(command, result, json) {
|
|
1802
3594
|
if (json) {
|
|
1803
3595
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1824,8 +3616,43 @@ function printDataResult(command, result, json) {
|
|
|
1824
3616
|
if (command === "link-child" && result && typeof result === "object") {
|
|
1825
3617
|
const link = result;
|
|
1826
3618
|
console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
|
|
3619
|
+
if (link.warning) console.log(`Warning: ${link.warning}`);
|
|
1827
3620
|
return;
|
|
1828
3621
|
}
|
|
3622
|
+
if (command === "reroll" && result && typeof result === "object") {
|
|
3623
|
+
if ("requests" in result) {
|
|
3624
|
+
const listed = result;
|
|
3625
|
+
console.log(`${listed.requests.length} pending re-roll target(s)`);
|
|
3626
|
+
for (const request of listed.requests) console.log(`${request.node_asset_id}${request.notes ? `: ${request.notes}` : ""}`);
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
if ("job" in result) {
|
|
3630
|
+
const planned = result;
|
|
3631
|
+
console.log(planned.imported ? `Imported re-roll for ${planned.job?.id || "job"}` : `Planned re-roll ${planned.job?.id || "job"}`);
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
if ("request" in result) {
|
|
3635
|
+
const mutation = result;
|
|
3636
|
+
console.log(`${mutation.dryRun ? "Dry run: " : ""}Re-roll ${mutation.request?.status || "request"} for ${mutation.request?.node_asset_id || "target"}`);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
if (command === "tasks" && result && typeof result === "object") {
|
|
3641
|
+
if ("tasks" in result) {
|
|
3642
|
+
const listed = result;
|
|
3643
|
+
console.log(`${listed.tasks.length} lineage task(s)`);
|
|
3644
|
+
for (const task of listed.tasks) console.log(`${task.id} ${task.task_type} ${task.status} ${task.target_asset_id}`);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if ("task" in result) {
|
|
3648
|
+
const mutation = result;
|
|
3649
|
+
const prefix = mutation.dryRun ? "Dry run: " : "";
|
|
3650
|
+
console.log(`${prefix}${mutation.task?.id || "task"} ${mutation.task?.task_type || "task"} ${mutation.task?.status || "unknown"} ${mutation.task?.target_asset_id || ""}`.trim());
|
|
3651
|
+
if ("claim_token" in mutation && typeof mutation.claim_token === "string") console.log(`Token: ${mutation.claim_token}`);
|
|
3652
|
+
if (mutation.events && mutation.events.length > 0) console.log(`Events: ${mutation.events.map((event) => event.event_type).join(", ")}`);
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
1829
3656
|
console.log(String(result));
|
|
1830
3657
|
}
|
|
1831
3658
|
function runLineageAgentCommand(command, args) {
|
|
@@ -1954,8 +3781,8 @@ function start(config, args) {
|
|
|
1954
3781
|
else console.error(`${config.binName}: ${message}`);
|
|
1955
3782
|
process.exit(1);
|
|
1956
3783
|
}
|
|
1957
|
-
const serverPath =
|
|
1958
|
-
if (!
|
|
3784
|
+
const serverPath = join6(packageRoot(), "dist", "server.js");
|
|
3785
|
+
if (!existsSync5(serverPath)) {
|
|
1959
3786
|
const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
|
|
1960
3787
|
if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
1961
3788
|
else console.error(`${config.binName}: ${message}`);
|
|
@@ -2009,7 +3836,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
2009
3836
|
start(config, normalizedArgs.slice(1));
|
|
2010
3837
|
return;
|
|
2011
3838
|
}
|
|
2012
|
-
if (command === "next" || command === "brief" || command === "inspect" || command === "link-child") {
|
|
3839
|
+
if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll" || command === "tasks") {
|
|
2013
3840
|
const commandArgs = normalizedArgs.slice(1);
|
|
2014
3841
|
const json2 = commandArgs.includes("--json");
|
|
2015
3842
|
try {
|