@mean-weasel/lineage 0.1.6 → 0.1.8
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 +34 -0
- package/dist/cli/lineage-dev.js +1313 -45
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1313 -45
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +1493 -540
- package/dist/server.js.map +4 -4
- package/dist/web/assets/index-BplMjJB9.css +1 -0
- package/dist/web/assets/index-ClSbabxx.js +23 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/dist/web/assets/index-EfT3Ues-.css +0 -1
- package/dist/web/assets/index-NDba9I2v.js +0 -23
package/dist/cli/lineage-dev.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 existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5
5
|
import { homedir, platform } from "node:os";
|
|
6
|
-
import { dirname as dirname3, join as join6, resolve as
|
|
6
|
+
import { dirname as dirname3, join as join6, resolve as resolve5 } from "node:path";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
9
|
|
|
@@ -328,6 +328,13 @@ function catalogPath(project = defaultProject) {
|
|
|
328
328
|
function fixtureCatalogPath(project = defaultProject) {
|
|
329
329
|
return join3(repoRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
|
|
330
330
|
}
|
|
331
|
+
function resolvedCatalogPath(project = defaultProject) {
|
|
332
|
+
const path = catalogPath(project);
|
|
333
|
+
if (existsSync3(path)) return path;
|
|
334
|
+
const clean = cleanProject(project);
|
|
335
|
+
if (clean === defaultProject && existsSync3(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
|
|
336
|
+
return path;
|
|
337
|
+
}
|
|
331
338
|
function normalizeCatalog(catalog, fallbackProject = defaultProject) {
|
|
332
339
|
const project = cleanProject(catalog.project || catalog.product || fallbackProject);
|
|
333
340
|
const product = catalog.product || project;
|
|
@@ -570,6 +577,10 @@ function listAssets(project = defaultProject, options = {}) {
|
|
|
570
577
|
error
|
|
571
578
|
};
|
|
572
579
|
}
|
|
580
|
+
function validateProject(project = defaultProject) {
|
|
581
|
+
const catalog = loadCatalog(project);
|
|
582
|
+
return { project: catalog.project, product: catalog.product, catalogPath: resolvedCatalogPath(project), default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length };
|
|
583
|
+
}
|
|
573
584
|
|
|
574
585
|
// src/server/assetLineageDb.ts
|
|
575
586
|
var require2 = createRequire(import.meta.url);
|
|
@@ -615,6 +626,10 @@ function lineageDb() {
|
|
|
615
626
|
create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
|
|
616
627
|
create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
|
|
617
628
|
create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
|
|
629
|
+
create table if not exists lineage_schema_migrations (
|
|
630
|
+
key text primary key,
|
|
631
|
+
applied_at text not null
|
|
632
|
+
);
|
|
618
633
|
create table if not exists asset_edges (
|
|
619
634
|
id text primary key,
|
|
620
635
|
project_id text not null references projects(id),
|
|
@@ -920,12 +935,49 @@ function lineageDb() {
|
|
|
920
935
|
);
|
|
921
936
|
create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
|
|
922
937
|
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);
|
|
938
|
+
create table if not exists lineage_tasks (
|
|
939
|
+
id text primary key,
|
|
940
|
+
project_id text not null references projects(id),
|
|
941
|
+
root_asset_id text not null references assets(id),
|
|
942
|
+
target_asset_id text not null references assets(id),
|
|
943
|
+
task_type text not null check (task_type in ('iterate', 'reroll')),
|
|
944
|
+
status text not null check (status in ('pending', 'claimed', 'in_progress', 'resolved', 'cancelled')),
|
|
945
|
+
instructions text,
|
|
946
|
+
created_by text not null check (created_by in ('human', 'agent', 'system')),
|
|
947
|
+
created_at text not null,
|
|
948
|
+
updated_at text not null,
|
|
949
|
+
claimed_at text,
|
|
950
|
+
started_at text,
|
|
951
|
+
resolved_at text,
|
|
952
|
+
cancelled_at text,
|
|
953
|
+
resolved_generation_job_id text,
|
|
954
|
+
resolved_asset_id text references assets(id),
|
|
955
|
+
metadata_json text
|
|
956
|
+
);
|
|
957
|
+
create unique index if not exists lineage_tasks_one_open
|
|
958
|
+
on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type)
|
|
959
|
+
where status in ('pending', 'claimed', 'in_progress');
|
|
960
|
+
create index if not exists lineage_tasks_root_status
|
|
961
|
+
on lineage_tasks(project_id, root_asset_id, status, updated_at);
|
|
962
|
+
create index if not exists lineage_tasks_target
|
|
963
|
+
on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type, status);
|
|
964
|
+
create table if not exists lineage_task_events (
|
|
965
|
+
id text primary key,
|
|
966
|
+
task_id text not null references lineage_tasks(id) on delete cascade,
|
|
967
|
+
event_type text not null,
|
|
968
|
+
actor text,
|
|
969
|
+
message text,
|
|
970
|
+
created_at text not null,
|
|
971
|
+
metadata_json text
|
|
972
|
+
);
|
|
973
|
+
create index if not exists lineage_task_events_task_created
|
|
974
|
+
on lineage_task_events(task_id, created_at);
|
|
923
975
|
create table if not exists agent_claims (
|
|
924
976
|
id text primary key,
|
|
925
977
|
token_hash text not null,
|
|
926
978
|
project_id text not null references projects(id),
|
|
927
979
|
channel text,
|
|
928
|
-
scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
980
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
929
981
|
target_id text not null,
|
|
930
982
|
target_title text,
|
|
931
983
|
agent_id text,
|
|
@@ -959,13 +1011,76 @@ function lineageDb() {
|
|
|
959
1011
|
migrateAssetSelections(database);
|
|
960
1012
|
dropLegacyAssetSelectionRootUnique(database);
|
|
961
1013
|
ensureColumn(database, "asset_selections", "notes", "text");
|
|
1014
|
+
backfillLineageTasks(database);
|
|
962
1015
|
ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
|
|
963
1016
|
ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
|
|
964
1017
|
ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
|
|
965
1018
|
ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
|
|
966
1019
|
ensureReviewStateValues(database);
|
|
1020
|
+
ensureAgentClaimScopeValues(database);
|
|
1021
|
+
ensureGenerationReceiptCheckValues(database);
|
|
967
1022
|
return database;
|
|
968
1023
|
}
|
|
1024
|
+
function backfillLineageTasks(database) {
|
|
1025
|
+
database.exec(`
|
|
1026
|
+
create table if not exists lineage_schema_migrations (
|
|
1027
|
+
key text primary key,
|
|
1028
|
+
applied_at text not null
|
|
1029
|
+
)
|
|
1030
|
+
`);
|
|
1031
|
+
const marker = database.prepare("select key from lineage_schema_migrations where key = 'lineage_tasks_backfilled_v1'").get();
|
|
1032
|
+
if (marker) return;
|
|
1033
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1034
|
+
try {
|
|
1035
|
+
database.exec(`
|
|
1036
|
+
insert or ignore into lineage_tasks (
|
|
1037
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1038
|
+
created_by, created_at, updated_at, metadata_json
|
|
1039
|
+
)
|
|
1040
|
+
select
|
|
1041
|
+
s.project_id || ':' || s.root_asset_id || ':lineage-task:iterate:' || s.asset_id,
|
|
1042
|
+
s.project_id,
|
|
1043
|
+
s.root_asset_id,
|
|
1044
|
+
s.asset_id,
|
|
1045
|
+
'iterate',
|
|
1046
|
+
'pending',
|
|
1047
|
+
s.notes,
|
|
1048
|
+
'human',
|
|
1049
|
+
s.selected_at,
|
|
1050
|
+
s.selected_at,
|
|
1051
|
+
null
|
|
1052
|
+
from asset_selections s;
|
|
1053
|
+
|
|
1054
|
+
insert or ignore into lineage_tasks (
|
|
1055
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1056
|
+
created_by, created_at, updated_at, metadata_json
|
|
1057
|
+
)
|
|
1058
|
+
select
|
|
1059
|
+
r.project_id || ':' || r.root_asset_id || ':lineage-task:reroll:' || r.node_asset_id,
|
|
1060
|
+
r.project_id,
|
|
1061
|
+
r.root_asset_id,
|
|
1062
|
+
r.node_asset_id,
|
|
1063
|
+
'reroll',
|
|
1064
|
+
'pending',
|
|
1065
|
+
r.notes,
|
|
1066
|
+
r.requested_by,
|
|
1067
|
+
r.created_at,
|
|
1068
|
+
r.created_at,
|
|
1069
|
+
null
|
|
1070
|
+
from asset_reroll_requests r
|
|
1071
|
+
where r.status = 'pending';
|
|
1072
|
+
`);
|
|
1073
|
+
database.prepare(`
|
|
1074
|
+
insert into lineage_schema_migrations (key, applied_at)
|
|
1075
|
+
values ('lineage_tasks_backfilled_v1', ?)
|
|
1076
|
+
on conflict(key) do nothing
|
|
1077
|
+
`).run(nowIso());
|
|
1078
|
+
database.exec("COMMIT");
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
database.exec("ROLLBACK");
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
969
1084
|
function ensureColumn(database, table, column, definition) {
|
|
970
1085
|
const rows = database.prepare(`pragma table_info(${table})`).all();
|
|
971
1086
|
if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
|
|
@@ -1029,12 +1144,138 @@ function ensureReviewStateValues(database) {
|
|
|
1029
1144
|
drop table asset_reviews_old;
|
|
1030
1145
|
`);
|
|
1031
1146
|
}
|
|
1147
|
+
function ensureAgentClaimScopeValues(database) {
|
|
1148
|
+
const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'agent_claims'").get();
|
|
1149
|
+
if (createSql?.sql?.includes("'lineage_task'")) return;
|
|
1150
|
+
database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
|
|
1151
|
+
try {
|
|
1152
|
+
database.exec(`
|
|
1153
|
+
alter table agent_claims rename to agent_claims_old;
|
|
1154
|
+
create table agent_claims (
|
|
1155
|
+
id text primary key,
|
|
1156
|
+
token_hash text not null,
|
|
1157
|
+
project_id text not null references projects(id),
|
|
1158
|
+
channel text,
|
|
1159
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
1160
|
+
target_id text not null,
|
|
1161
|
+
target_title text,
|
|
1162
|
+
agent_id text,
|
|
1163
|
+
agent_name text not null,
|
|
1164
|
+
agent_kind text not null,
|
|
1165
|
+
thread_id text,
|
|
1166
|
+
status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
|
|
1167
|
+
created_at text not null,
|
|
1168
|
+
heartbeat_at text not null,
|
|
1169
|
+
expires_at text not null,
|
|
1170
|
+
released_at text,
|
|
1171
|
+
revoked_at text,
|
|
1172
|
+
revoked_by text,
|
|
1173
|
+
override_reason text,
|
|
1174
|
+
metadata_json text
|
|
1175
|
+
);
|
|
1176
|
+
insert into agent_claims
|
|
1177
|
+
select * from agent_claims_old;
|
|
1178
|
+
drop table agent_claims_old;
|
|
1179
|
+
create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
|
|
1180
|
+
create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
|
|
1181
|
+
create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
|
|
1182
|
+
`);
|
|
1183
|
+
const violations = database.prepare("pragma foreign_key_check").all();
|
|
1184
|
+
if (violations.length > 0) throw new Error(`Agent claim scope migration failed foreign key check: ${JSON.stringify(violations)}`);
|
|
1185
|
+
database.exec("COMMIT");
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
database.exec("ROLLBACK");
|
|
1188
|
+
throw error;
|
|
1189
|
+
} finally {
|
|
1190
|
+
database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function tableCreateSql(database, table) {
|
|
1194
|
+
const row = database.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
|
|
1195
|
+
return row?.sql || "";
|
|
1196
|
+
}
|
|
1197
|
+
function ensureGenerationReceiptCheckValues(database) {
|
|
1198
|
+
const jobsSql = tableCreateSql(database, "generation_jobs");
|
|
1199
|
+
const inputsSql = tableCreateSql(database, "generation_job_inputs");
|
|
1200
|
+
const needsJobsMigration = Boolean(jobsSql && !jobsSql.includes("'lineage_reroll'"));
|
|
1201
|
+
const needsInputsMigration = Boolean(inputsSql && !inputsSql.includes("'reroll_target'"));
|
|
1202
|
+
if (!needsJobsMigration && !needsInputsMigration) return;
|
|
1203
|
+
database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
|
|
1204
|
+
try {
|
|
1205
|
+
if (needsJobsMigration) migrateGenerationJobsCheck(database);
|
|
1206
|
+
if (needsInputsMigration) migrateGenerationJobInputsCheck(database);
|
|
1207
|
+
const violations = database.prepare("pragma foreign_key_check").all();
|
|
1208
|
+
if (violations.length > 0) throw new Error(`Generation receipt migration failed foreign key check: ${JSON.stringify(violations)}`);
|
|
1209
|
+
database.exec("COMMIT");
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
database.exec("ROLLBACK");
|
|
1212
|
+
throw error;
|
|
1213
|
+
} finally {
|
|
1214
|
+
database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function migrateGenerationJobsCheck(database) {
|
|
1218
|
+
database.exec(`
|
|
1219
|
+
alter table generation_jobs rename to generation_jobs_legacy_check;
|
|
1220
|
+
create table generation_jobs (
|
|
1221
|
+
id text primary key,
|
|
1222
|
+
project_id text not null references projects(id),
|
|
1223
|
+
provider text not null default 'codex-handoff',
|
|
1224
|
+
adapter_version text not null,
|
|
1225
|
+
source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
|
|
1226
|
+
root_asset_id text not null references assets(id),
|
|
1227
|
+
prompt text not null,
|
|
1228
|
+
expected_output_count integer not null check (expected_output_count > 0),
|
|
1229
|
+
status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
|
|
1230
|
+
output_dir text,
|
|
1231
|
+
handoff_json text,
|
|
1232
|
+
created_at text not null,
|
|
1233
|
+
updated_at text not null,
|
|
1234
|
+
imported_at text
|
|
1235
|
+
);
|
|
1236
|
+
insert into generation_jobs (
|
|
1237
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
1238
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
|
|
1239
|
+
)
|
|
1240
|
+
select
|
|
1241
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
1242
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
|
|
1243
|
+
from generation_jobs_legacy_check;
|
|
1244
|
+
drop table generation_jobs_legacy_check;
|
|
1245
|
+
create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
|
|
1246
|
+
`);
|
|
1247
|
+
}
|
|
1248
|
+
function migrateGenerationJobInputsCheck(database) {
|
|
1249
|
+
database.exec(`
|
|
1250
|
+
alter table generation_job_inputs rename to generation_job_inputs_legacy_check;
|
|
1251
|
+
create table generation_job_inputs (
|
|
1252
|
+
id text primary key,
|
|
1253
|
+
job_id text not null references generation_jobs(id) on delete cascade,
|
|
1254
|
+
project_id text not null references projects(id),
|
|
1255
|
+
asset_id text not null references assets(id),
|
|
1256
|
+
root_asset_id text not null references assets(id),
|
|
1257
|
+
role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
|
|
1258
|
+
position integer not null,
|
|
1259
|
+
selection_strategy text not null,
|
|
1260
|
+
selection_snapshot_json text not null,
|
|
1261
|
+
unique(job_id, asset_id, role)
|
|
1262
|
+
);
|
|
1263
|
+
insert into generation_job_inputs (
|
|
1264
|
+
id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
|
|
1265
|
+
)
|
|
1266
|
+
select
|
|
1267
|
+
id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
|
|
1268
|
+
from generation_job_inputs_legacy_check;
|
|
1269
|
+
drop table generation_job_inputs_legacy_check;
|
|
1270
|
+
create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
|
|
1271
|
+
`);
|
|
1272
|
+
}
|
|
1032
1273
|
|
|
1033
1274
|
// src/server/agentClaims.ts
|
|
1034
1275
|
var defaultTtlSeconds = 20 * 60;
|
|
1035
1276
|
var idleAfterSeconds = 5 * 60;
|
|
1036
1277
|
var staleAfterSeconds = 15 * 60;
|
|
1037
|
-
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1278
|
+
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "lineage_task", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1038
1279
|
var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
1039
1280
|
var AgentClaimError = class extends Error {
|
|
1040
1281
|
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
@@ -1339,6 +1580,19 @@ function revokeAgentClaim(project, claimId, fields) {
|
|
|
1339
1580
|
database.close();
|
|
1340
1581
|
}
|
|
1341
1582
|
}
|
|
1583
|
+
function revokeAgentClaimInDatabase(database, project, claimId, fields) {
|
|
1584
|
+
expireActiveClaims(database);
|
|
1585
|
+
const claim = findClaimById(database, claimId, project);
|
|
1586
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1587
|
+
if (claim.status !== "active") return claim;
|
|
1588
|
+
const timestamp = nowIso();
|
|
1589
|
+
database.prepare(`
|
|
1590
|
+
update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1591
|
+
where id = ? and status = 'active'
|
|
1592
|
+
`).run(timestamp, fields.actor || "human", fields.reason || null, claim.id);
|
|
1593
|
+
recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
|
|
1594
|
+
return findClaimById(database, claim.id, project);
|
|
1595
|
+
}
|
|
1342
1596
|
function transferAgentClaim(project, claimId, fields) {
|
|
1343
1597
|
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1344
1598
|
const toAgentName = fields.toAgentName.trim();
|
|
@@ -1356,6 +1610,19 @@ function transferAgentClaim(project, claimId, fields) {
|
|
|
1356
1610
|
database.close();
|
|
1357
1611
|
}
|
|
1358
1612
|
}
|
|
1613
|
+
function recordAgentClaimWriteAllowed(claim, fields) {
|
|
1614
|
+
const database = lineageDb();
|
|
1615
|
+
try {
|
|
1616
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1617
|
+
danger_level: fields.dangerLevel,
|
|
1618
|
+
target_id: fields.targetId,
|
|
1619
|
+
write_kind: fields.writeKind
|
|
1620
|
+
});
|
|
1621
|
+
return { ok: true };
|
|
1622
|
+
} finally {
|
|
1623
|
+
database.close();
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1359
1626
|
function validateAgentClaimForWrite(fields) {
|
|
1360
1627
|
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1361
1628
|
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
@@ -1376,11 +1643,13 @@ function validateAgentClaimForWrite(fields) {
|
|
|
1376
1643
|
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1377
1644
|
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1378
1645
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1646
|
+
if (fields.recordEvent !== false) {
|
|
1647
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1648
|
+
danger_level: fields.dangerLevel,
|
|
1649
|
+
target_id: fields.targetId,
|
|
1650
|
+
write_kind: fields.writeKind
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1384
1653
|
return { ok: true, claim, warnings: [] };
|
|
1385
1654
|
} finally {
|
|
1386
1655
|
database.close();
|
|
@@ -1400,6 +1669,507 @@ function selectedRows(database, project, root) {
|
|
|
1400
1669
|
`).all(project, root);
|
|
1401
1670
|
}
|
|
1402
1671
|
|
|
1672
|
+
// src/server/assetLineageTasks.ts
|
|
1673
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1674
|
+
var LineageTaskError = class extends Error {
|
|
1675
|
+
constructor(message, status = 400) {
|
|
1676
|
+
super(message);
|
|
1677
|
+
this.status = status;
|
|
1678
|
+
}
|
|
1679
|
+
status;
|
|
1680
|
+
};
|
|
1681
|
+
var activeStatuses = ["pending", "claimed", "in_progress"];
|
|
1682
|
+
var taskTypes = /* @__PURE__ */ new Set(["iterate", "reroll"]);
|
|
1683
|
+
var taskStatuses = /* @__PURE__ */ new Set(["pending", "claimed", "in_progress", "resolved", "cancelled"]);
|
|
1684
|
+
function taskIdFor(project, rootAssetId, targetAssetId, taskType) {
|
|
1685
|
+
return `${project}:${rootAssetId}:lineage-task:${taskType}:${targetAssetId}`;
|
|
1686
|
+
}
|
|
1687
|
+
function randomId2(prefix) {
|
|
1688
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
|
|
1689
|
+
}
|
|
1690
|
+
function metadataJson2(metadata) {
|
|
1691
|
+
return metadata ? JSON.stringify(metadata) : null;
|
|
1692
|
+
}
|
|
1693
|
+
function metadataWithoutClaim(metadata) {
|
|
1694
|
+
if (!metadata) return void 0;
|
|
1695
|
+
const next = { ...metadata };
|
|
1696
|
+
delete next.claim_id;
|
|
1697
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
1698
|
+
}
|
|
1699
|
+
function parseMetadata2(value) {
|
|
1700
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
1701
|
+
try {
|
|
1702
|
+
const parsed = JSON.parse(value);
|
|
1703
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1704
|
+
} catch {
|
|
1705
|
+
return void 0;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function normalizeProject(project) {
|
|
1709
|
+
const trimmed = project.trim();
|
|
1710
|
+
if (!trimmed) throw new LineageTaskError("Lineage task requires project");
|
|
1711
|
+
return trimmed;
|
|
1712
|
+
}
|
|
1713
|
+
function normalizeTaskType(taskType) {
|
|
1714
|
+
if (!taskTypes.has(taskType)) throw new LineageTaskError(`Unsupported lineage task type: ${taskType}`);
|
|
1715
|
+
return taskType;
|
|
1716
|
+
}
|
|
1717
|
+
function normalizeStatus(status) {
|
|
1718
|
+
if (!taskStatuses.has(status)) throw new LineageTaskError(`Unsupported lineage task status: ${status}`);
|
|
1719
|
+
return status;
|
|
1720
|
+
}
|
|
1721
|
+
function normalizeActor(actor, label) {
|
|
1722
|
+
const trimmed = actor.trim();
|
|
1723
|
+
if (!trimmed) throw new LineageTaskError(`${label} is required`);
|
|
1724
|
+
return trimmed;
|
|
1725
|
+
}
|
|
1726
|
+
function requireAsset(database, project, assetId) {
|
|
1727
|
+
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
1728
|
+
if (!row) throw new LineageTaskError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1729
|
+
}
|
|
1730
|
+
function taskFromRow(row) {
|
|
1731
|
+
const metadata = parseMetadata2(row.metadata_json);
|
|
1732
|
+
const claimId = typeof metadata?.claim_id === "string" ? metadata.claim_id : void 0;
|
|
1733
|
+
return {
|
|
1734
|
+
id: String(row.id),
|
|
1735
|
+
project_id: String(row.project_id),
|
|
1736
|
+
root_asset_id: String(row.root_asset_id),
|
|
1737
|
+
target_asset_id: String(row.target_asset_id),
|
|
1738
|
+
task_type: String(row.task_type),
|
|
1739
|
+
status: String(row.status),
|
|
1740
|
+
instructions: typeof row.instructions === "string" ? row.instructions : void 0,
|
|
1741
|
+
created_by: String(row.created_by),
|
|
1742
|
+
created_at: String(row.created_at),
|
|
1743
|
+
updated_at: String(row.updated_at),
|
|
1744
|
+
claimed_at: typeof row.claimed_at === "string" ? row.claimed_at : void 0,
|
|
1745
|
+
started_at: typeof row.started_at === "string" ? row.started_at : void 0,
|
|
1746
|
+
resolved_at: typeof row.resolved_at === "string" ? row.resolved_at : void 0,
|
|
1747
|
+
cancelled_at: typeof row.cancelled_at === "string" ? row.cancelled_at : void 0,
|
|
1748
|
+
resolved_generation_job_id: typeof row.resolved_generation_job_id === "string" ? row.resolved_generation_job_id : void 0,
|
|
1749
|
+
resolved_asset_id: typeof row.resolved_asset_id === "string" ? row.resolved_asset_id : void 0,
|
|
1750
|
+
claimed_by_claim_id: claimId,
|
|
1751
|
+
metadata
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function eventFromRow(row) {
|
|
1755
|
+
return {
|
|
1756
|
+
id: String(row.id),
|
|
1757
|
+
task_id: String(row.task_id),
|
|
1758
|
+
event_type: String(row.event_type),
|
|
1759
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
1760
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
1761
|
+
created_at: String(row.created_at),
|
|
1762
|
+
metadata: parseMetadata2(row.metadata_json)
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function findTask(database, project, taskId) {
|
|
1766
|
+
const row = database.prepare("select * from lineage_tasks where project_id = ? and id = ?").get(project, taskId);
|
|
1767
|
+
return row ? taskFromRow(row) : null;
|
|
1768
|
+
}
|
|
1769
|
+
function requireTask(database, project, taskId) {
|
|
1770
|
+
const task = findTask(database, project, taskId);
|
|
1771
|
+
if (!task) throw new LineageTaskError(`Unknown lineage task: ${taskId}`, 404);
|
|
1772
|
+
return task;
|
|
1773
|
+
}
|
|
1774
|
+
function taskEvents(database, taskId) {
|
|
1775
|
+
const rows = database.prepare("select * from lineage_task_events where task_id = ? order by created_at, rowid").all(taskId);
|
|
1776
|
+
return rows.map(eventFromRow);
|
|
1777
|
+
}
|
|
1778
|
+
function recordEvent2(database, taskId, eventType, actor, message, metadata) {
|
|
1779
|
+
database.prepare(`
|
|
1780
|
+
insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
|
|
1781
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
1782
|
+
`).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata));
|
|
1783
|
+
}
|
|
1784
|
+
function taskWithEvents(database, project, taskId) {
|
|
1785
|
+
return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
1786
|
+
}
|
|
1787
|
+
function assertChanged(result, message) {
|
|
1788
|
+
if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
|
|
1789
|
+
}
|
|
1790
|
+
function transaction(database, callback) {
|
|
1791
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1792
|
+
try {
|
|
1793
|
+
const value = callback();
|
|
1794
|
+
database.exec("COMMIT");
|
|
1795
|
+
return value;
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
database.exec("ROLLBACK");
|
|
1798
|
+
throw error;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function taskReadWithEvents(database, project, taskId) {
|
|
1802
|
+
return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
1803
|
+
}
|
|
1804
|
+
function listLineageTasks(project, rootAssetId, statuses = activeStatuses) {
|
|
1805
|
+
const normalizedProject = normalizeProject(project);
|
|
1806
|
+
const normalizedStatuses = statuses.map(normalizeStatus);
|
|
1807
|
+
const database = lineageDb();
|
|
1808
|
+
try {
|
|
1809
|
+
requireAsset(database, normalizedProject, rootAssetId);
|
|
1810
|
+
if (normalizedStatuses.length === 0) {
|
|
1811
|
+
return { project: normalizedProject, root_asset_id: rootAssetId, tasks: [], fetchedAt: nowIso() };
|
|
1812
|
+
}
|
|
1813
|
+
const placeholders = normalizedStatuses.map(() => "?").join(",");
|
|
1814
|
+
const rows = database.prepare(`
|
|
1815
|
+
select * from lineage_tasks
|
|
1816
|
+
where project_id = ? and root_asset_id = ? and status in (${placeholders})
|
|
1817
|
+
order by updated_at desc, created_at desc, id
|
|
1818
|
+
`).all(normalizedProject, rootAssetId, ...normalizedStatuses);
|
|
1819
|
+
return { project: normalizedProject, root_asset_id: rootAssetId, tasks: rows.map(taskFromRow), fetchedAt: nowIso() };
|
|
1820
|
+
} finally {
|
|
1821
|
+
database.close();
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function getLineageTask(project, taskId) {
|
|
1825
|
+
const normalizedProject = normalizeProject(project);
|
|
1826
|
+
const database = lineageDb();
|
|
1827
|
+
try {
|
|
1828
|
+
return { project: normalizedProject, ...taskReadWithEvents(database, normalizedProject, taskId) };
|
|
1829
|
+
} finally {
|
|
1830
|
+
database.close();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function upsertLineageTask(project, fields) {
|
|
1834
|
+
const normalizedProject = normalizeProject(project);
|
|
1835
|
+
const taskType = normalizeTaskType(fields.taskType);
|
|
1836
|
+
const taskId = taskIdFor(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
|
|
1837
|
+
const database = lineageDb();
|
|
1838
|
+
try {
|
|
1839
|
+
requireAsset(database, normalizedProject, fields.rootAssetId);
|
|
1840
|
+
requireAsset(database, normalizedProject, fields.targetAssetId);
|
|
1841
|
+
const existing = database.prepare(`
|
|
1842
|
+
select * from lineage_tasks
|
|
1843
|
+
where project_id = ? and root_asset_id = ? and target_asset_id = ? and task_type = ?
|
|
1844
|
+
and status in ('pending', 'claimed', 'in_progress')
|
|
1845
|
+
order by created_at desc limit 1
|
|
1846
|
+
`).get(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
|
|
1847
|
+
const instructions = fields.instructions?.trim() || void 0;
|
|
1848
|
+
if (existing) {
|
|
1849
|
+
const task = taskFromRow(existing);
|
|
1850
|
+
if (task.status !== "pending" && instructions !== void 0 && instructions !== task.instructions) {
|
|
1851
|
+
throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
|
|
1852
|
+
}
|
|
1853
|
+
if (task.status === "pending" && instructions !== void 0 && instructions !== task.instructions) {
|
|
1854
|
+
const timestamp2 = nowIso();
|
|
1855
|
+
transaction(database, () => {
|
|
1856
|
+
const result = database.prepare(`
|
|
1857
|
+
update lineage_tasks
|
|
1858
|
+
set instructions = ?, updated_at = ?
|
|
1859
|
+
where id = ? and status = 'pending'
|
|
1860
|
+
`).run(instructions, timestamp2, task.id);
|
|
1861
|
+
assertChanged(result, "Only pending lineage tasks can update instructions.");
|
|
1862
|
+
recordEvent2(database, task.id, "instructions_updated", fields.createdBy, "Instructions updated.");
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1866
|
+
}
|
|
1867
|
+
const closedTask = findTask(database, normalizedProject, taskId);
|
|
1868
|
+
if (closedTask) {
|
|
1869
|
+
const timestamp2 = nowIso();
|
|
1870
|
+
transaction(database, () => {
|
|
1871
|
+
const result = database.prepare(`
|
|
1872
|
+
update lineage_tasks
|
|
1873
|
+
set status = 'pending', instructions = ?, created_by = ?, updated_at = ?,
|
|
1874
|
+
claimed_at = null, started_at = null, resolved_at = null, cancelled_at = null,
|
|
1875
|
+
resolved_generation_job_id = null, resolved_asset_id = null, metadata_json = null
|
|
1876
|
+
where id = ? and status not in ('pending', 'claimed', 'in_progress')
|
|
1877
|
+
`).run(instructions || null, fields.createdBy, timestamp2, taskId);
|
|
1878
|
+
assertChanged(result, "Lineage task changed while reopening.");
|
|
1879
|
+
recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
|
|
1880
|
+
});
|
|
1881
|
+
return taskWithEvents(database, normalizedProject, taskId);
|
|
1882
|
+
}
|
|
1883
|
+
const timestamp = nowIso();
|
|
1884
|
+
transaction(database, () => {
|
|
1885
|
+
database.prepare(`
|
|
1886
|
+
insert into lineage_tasks (
|
|
1887
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1888
|
+
created_by, created_at, updated_at, metadata_json
|
|
1889
|
+
) values (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, null)
|
|
1890
|
+
`).run(taskId, normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType, instructions || null, fields.createdBy, timestamp, timestamp);
|
|
1891
|
+
recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
|
|
1892
|
+
});
|
|
1893
|
+
return taskWithEvents(database, normalizedProject, taskId);
|
|
1894
|
+
} finally {
|
|
1895
|
+
database.close();
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function updateLineageTaskInstructions(project, fields) {
|
|
1899
|
+
const normalizedProject = normalizeProject(project);
|
|
1900
|
+
const instructions = fields.instructions.trim();
|
|
1901
|
+
const database = lineageDb();
|
|
1902
|
+
try {
|
|
1903
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1904
|
+
if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
|
|
1905
|
+
const timestamp = nowIso();
|
|
1906
|
+
transaction(database, () => {
|
|
1907
|
+
const result = database.prepare(`
|
|
1908
|
+
update lineage_tasks
|
|
1909
|
+
set instructions = ?, updated_at = ?
|
|
1910
|
+
where id = ? and status = 'pending'
|
|
1911
|
+
`).run(instructions || null, timestamp, task.id);
|
|
1912
|
+
assertChanged(result, "Only pending lineage tasks can update instructions.");
|
|
1913
|
+
recordEvent2(database, task.id, "instructions_updated", "human", "Instructions updated.");
|
|
1914
|
+
});
|
|
1915
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1916
|
+
} finally {
|
|
1917
|
+
database.close();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function addLineageTaskComment(project, fields) {
|
|
1921
|
+
const normalizedProject = normalizeProject(project);
|
|
1922
|
+
const actor = normalizeActor(fields.actor, "Comment actor");
|
|
1923
|
+
const message = fields.message.trim();
|
|
1924
|
+
if (!message) throw new LineageTaskError("Comment message is required");
|
|
1925
|
+
const database = lineageDb();
|
|
1926
|
+
try {
|
|
1927
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1928
|
+
transaction(database, () => {
|
|
1929
|
+
recordEvent2(database, task.id, "comment_added", actor, message);
|
|
1930
|
+
});
|
|
1931
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1932
|
+
} finally {
|
|
1933
|
+
database.close();
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function claimLineageTask(project, fields) {
|
|
1937
|
+
const normalizedProject = normalizeProject(project);
|
|
1938
|
+
const agentName = normalizeActor(fields.agentName, "Agent name");
|
|
1939
|
+
const beforeClaimDb = lineageDb();
|
|
1940
|
+
try {
|
|
1941
|
+
const task = requireTask(beforeClaimDb, normalizedProject, fields.taskId);
|
|
1942
|
+
if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
|
|
1943
|
+
} finally {
|
|
1944
|
+
beforeClaimDb.close();
|
|
1945
|
+
}
|
|
1946
|
+
const claimResult = createAgentClaim({
|
|
1947
|
+
agentName,
|
|
1948
|
+
project: normalizedProject,
|
|
1949
|
+
scopeType: "lineage_task",
|
|
1950
|
+
targetId: fields.taskId,
|
|
1951
|
+
targetTitle: fields.taskId
|
|
1952
|
+
});
|
|
1953
|
+
if (!claimResult.claim) throw new LineageTaskError("Unable to create lineage task claim.", 500);
|
|
1954
|
+
const claim = claimResult.claim;
|
|
1955
|
+
const database = lineageDb();
|
|
1956
|
+
try {
|
|
1957
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1958
|
+
if (task.status !== "pending") {
|
|
1959
|
+
releaseAgentClaim(claimResult.claim_token);
|
|
1960
|
+
throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
|
|
1961
|
+
}
|
|
1962
|
+
const timestamp = nowIso();
|
|
1963
|
+
const metadata = { ...task.metadata || {}, claim_id: claim.id };
|
|
1964
|
+
try {
|
|
1965
|
+
transaction(database, () => {
|
|
1966
|
+
const result = database.prepare(`
|
|
1967
|
+
update lineage_tasks
|
|
1968
|
+
set status = 'claimed', claimed_at = ?, updated_at = ?, metadata_json = ?
|
|
1969
|
+
where id = ? and status = 'pending'
|
|
1970
|
+
`).run(timestamp, timestamp, metadataJson2(metadata), task.id);
|
|
1971
|
+
assertChanged(result, "Only pending lineage tasks can be claimed.");
|
|
1972
|
+
recordEvent2(database, task.id, "claimed", agentName, "Lineage task claimed.", { claim_id: claim.id });
|
|
1973
|
+
});
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
releaseAgentClaim(claimResult.claim_token);
|
|
1976
|
+
throw error;
|
|
1977
|
+
}
|
|
1978
|
+
return {
|
|
1979
|
+
...taskWithEvents(database, normalizedProject, task.id),
|
|
1980
|
+
claim,
|
|
1981
|
+
claim_token: claimResult.claim_token
|
|
1982
|
+
};
|
|
1983
|
+
} finally {
|
|
1984
|
+
database.close();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
function startLineageTask(project, fields) {
|
|
1988
|
+
const normalizedProject = normalizeProject(project);
|
|
1989
|
+
const precheckDatabase = lineageDb();
|
|
1990
|
+
try {
|
|
1991
|
+
const task = requireTask(precheckDatabase, normalizedProject, fields.taskId);
|
|
1992
|
+
if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
|
|
1993
|
+
} finally {
|
|
1994
|
+
precheckDatabase.close();
|
|
1995
|
+
}
|
|
1996
|
+
const validation = validateAgentClaimForWrite({
|
|
1997
|
+
claimToken: fields.claimToken,
|
|
1998
|
+
dangerLevel: "enforce",
|
|
1999
|
+
project: normalizedProject,
|
|
2000
|
+
recordEvent: false,
|
|
2001
|
+
scopeType: "lineage_task",
|
|
2002
|
+
targetId: fields.taskId,
|
|
2003
|
+
writeKind: "lineage_task_start"
|
|
2004
|
+
});
|
|
2005
|
+
if (!validation.ok) throw new LineageTaskError(validation.message, 409);
|
|
2006
|
+
const database = lineageDb();
|
|
2007
|
+
try {
|
|
2008
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2009
|
+
if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
|
|
2010
|
+
if (task.claimed_by_claim_id && task.claimed_by_claim_id !== validation.claim.id) {
|
|
2011
|
+
throw new LineageTaskError("Claim token does not match the task claim.", 409);
|
|
2012
|
+
}
|
|
2013
|
+
const timestamp = nowIso();
|
|
2014
|
+
const metadata = { ...task.metadata || {}, claim_id: validation.claim.id };
|
|
2015
|
+
transaction(database, () => {
|
|
2016
|
+
const result = database.prepare(`
|
|
2017
|
+
update lineage_tasks
|
|
2018
|
+
set status = 'in_progress', started_at = ?, updated_at = ?, metadata_json = ?
|
|
2019
|
+
where id = ? and status = 'claimed'
|
|
2020
|
+
`).run(timestamp, timestamp, metadataJson2(metadata), task.id);
|
|
2021
|
+
assertChanged(result, "Only claimed lineage tasks can be started.");
|
|
2022
|
+
recordEvent2(database, task.id, "started", validation.claim.agent_name, "Lineage task started.", { claim_id: validation.claim.id });
|
|
2023
|
+
});
|
|
2024
|
+
recordAgentClaimWriteAllowed(validation.claim, {
|
|
2025
|
+
dangerLevel: "enforce",
|
|
2026
|
+
targetId: fields.taskId,
|
|
2027
|
+
writeKind: "lineage_task_start"
|
|
2028
|
+
});
|
|
2029
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2030
|
+
} finally {
|
|
2031
|
+
database.close();
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function overrideLineageTask(project, fields) {
|
|
2035
|
+
const normalizedProject = normalizeProject(project);
|
|
2036
|
+
const actor = normalizeActor(fields.actor, "Override actor");
|
|
2037
|
+
const reason = fields.reason.trim();
|
|
2038
|
+
if (!reason) throw new LineageTaskError("Override reason is required");
|
|
2039
|
+
const database = lineageDb();
|
|
2040
|
+
try {
|
|
2041
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2042
|
+
if (!["claimed", "in_progress"].includes(task.status)) {
|
|
2043
|
+
throw new LineageTaskError(`Only claimed or in-progress lineage tasks can be overridden; task is ${task.status}.`, 409);
|
|
2044
|
+
}
|
|
2045
|
+
const timestamp = nowIso();
|
|
2046
|
+
const instructions = fields.instructions === void 0 ? task.instructions : fields.instructions.trim() || void 0;
|
|
2047
|
+
const metadata = metadataWithoutClaim(task.metadata);
|
|
2048
|
+
transaction(database, () => {
|
|
2049
|
+
const result = database.prepare(`
|
|
2050
|
+
update lineage_tasks
|
|
2051
|
+
set status = 'pending', instructions = ?, claimed_at = null, started_at = null,
|
|
2052
|
+
updated_at = ?, metadata_json = ?
|
|
2053
|
+
where project_id = ? and id = ? and status in ('claimed', 'in_progress')
|
|
2054
|
+
`).run(instructions || null, timestamp, metadataJson2(metadata), normalizedProject, task.id);
|
|
2055
|
+
assertChanged(result, `Only active lineage task ${task.id} could be overridden.`);
|
|
2056
|
+
if (task.claimed_by_claim_id) {
|
|
2057
|
+
revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
|
|
2058
|
+
actor,
|
|
2059
|
+
reason
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
recordEvent2(database, task.id, "human_override", actor, reason, {
|
|
2063
|
+
previous_claim_id: task.claimed_by_claim_id,
|
|
2064
|
+
previous_status: task.status
|
|
2065
|
+
});
|
|
2066
|
+
});
|
|
2067
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2068
|
+
} finally {
|
|
2069
|
+
database.close();
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function cancelLineageTask(project, fields) {
|
|
2073
|
+
const normalizedProject = normalizeProject(project);
|
|
2074
|
+
const actor = normalizeActor(fields.actor, "Cancel actor");
|
|
2075
|
+
const database = lineageDb();
|
|
2076
|
+
try {
|
|
2077
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2078
|
+
if (task.status === "cancelled") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
2079
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
2080
|
+
throw new LineageTaskError(`Only open lineage tasks can be cancelled; task is ${task.status}.`, 409);
|
|
2081
|
+
}
|
|
2082
|
+
if (task.status !== "pending" && !fields.override) {
|
|
2083
|
+
throw new LineageTaskError("Cancelling an active lineage task requires override=true.", 409);
|
|
2084
|
+
}
|
|
2085
|
+
const overridingActiveTask = fields.override === true && task.status !== "pending";
|
|
2086
|
+
const metadata = overridingActiveTask ? metadataWithoutClaim(task.metadata) : task.metadata;
|
|
2087
|
+
const timestamp = nowIso();
|
|
2088
|
+
const cancelledTask = {
|
|
2089
|
+
...task,
|
|
2090
|
+
cancelled_at: timestamp,
|
|
2091
|
+
claimed_at: overridingActiveTask ? void 0 : task.claimed_at,
|
|
2092
|
+
claimed_by_claim_id: overridingActiveTask ? void 0 : task.claimed_by_claim_id,
|
|
2093
|
+
started_at: overridingActiveTask ? void 0 : task.started_at,
|
|
2094
|
+
status: "cancelled",
|
|
2095
|
+
updated_at: timestamp,
|
|
2096
|
+
metadata
|
|
2097
|
+
};
|
|
2098
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: cancelledTask, events: taskEvents(database, task.id) };
|
|
2099
|
+
transaction(database, () => {
|
|
2100
|
+
const result = database.prepare(`
|
|
2101
|
+
update lineage_tasks
|
|
2102
|
+
set status = 'cancelled', cancelled_at = ?, claimed_at = ?, started_at = ?,
|
|
2103
|
+
updated_at = ?, metadata_json = ?
|
|
2104
|
+
where id = ? and status = ?
|
|
2105
|
+
`).run(
|
|
2106
|
+
timestamp,
|
|
2107
|
+
overridingActiveTask ? null : task.claimed_at || null,
|
|
2108
|
+
overridingActiveTask ? null : task.started_at || null,
|
|
2109
|
+
timestamp,
|
|
2110
|
+
metadataJson2(metadata),
|
|
2111
|
+
task.id,
|
|
2112
|
+
task.status
|
|
2113
|
+
);
|
|
2114
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be cancelled.`);
|
|
2115
|
+
if (overridingActiveTask) {
|
|
2116
|
+
if (task.claimed_by_claim_id) {
|
|
2117
|
+
revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
|
|
2118
|
+
actor,
|
|
2119
|
+
reason: "Lineage task cancelled by human override."
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
recordEvent2(database, task.id, "human_override", actor, "Lineage task cancelled by human override.", {
|
|
2123
|
+
previous_claim_id: task.claimed_by_claim_id,
|
|
2124
|
+
previous_status: task.status
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
recordEvent2(database, task.id, "cancelled", actor, "Lineage task cancelled.");
|
|
2128
|
+
});
|
|
2129
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2130
|
+
} finally {
|
|
2131
|
+
database.close();
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
function resolveLineageTask(project, fields) {
|
|
2135
|
+
const normalizedProject = normalizeProject(project);
|
|
2136
|
+
const actor = normalizeActor(fields.actor, "Resolve actor");
|
|
2137
|
+
const database = lineageDb();
|
|
2138
|
+
try {
|
|
2139
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2140
|
+
if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
|
|
2141
|
+
if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
2142
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
2143
|
+
throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
|
|
2144
|
+
}
|
|
2145
|
+
const timestamp = nowIso();
|
|
2146
|
+
const resolvedTask = {
|
|
2147
|
+
...task,
|
|
2148
|
+
status: "resolved",
|
|
2149
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
2150
|
+
resolved_at: timestamp,
|
|
2151
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId,
|
|
2152
|
+
updated_at: timestamp
|
|
2153
|
+
};
|
|
2154
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
|
|
2155
|
+
transaction(database, () => {
|
|
2156
|
+
const result = database.prepare(`
|
|
2157
|
+
update lineage_tasks
|
|
2158
|
+
set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
|
|
2159
|
+
where id = ? and status = ?
|
|
2160
|
+
`).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
|
|
2161
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
|
|
2162
|
+
recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
|
|
2163
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
2164
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId
|
|
2165
|
+
});
|
|
2166
|
+
});
|
|
2167
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2168
|
+
} finally {
|
|
2169
|
+
database.close();
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1403
2173
|
// src/server/assetLineageWorkspaces.ts
|
|
1404
2174
|
function lineageWorkspaceId(project, rootAssetId) {
|
|
1405
2175
|
return `${project}:lineage-workspace:${rootAssetId}`;
|
|
@@ -1464,6 +2234,17 @@ function seedLegacyWorkspaces(database, project) {
|
|
|
1464
2234
|
);
|
|
1465
2235
|
}
|
|
1466
2236
|
}
|
|
2237
|
+
function listRows(database, project) {
|
|
2238
|
+
return database.prepare(`
|
|
2239
|
+
select * from lineage_workspaces
|
|
2240
|
+
where project_id = ?
|
|
2241
|
+
order by
|
|
2242
|
+
case status when 'active' then 0 when 'paused' then 1 else 2 end,
|
|
2243
|
+
active_at desc nulls last,
|
|
2244
|
+
updated_at desc,
|
|
2245
|
+
title
|
|
2246
|
+
`).all(project).map(rowToWorkspace);
|
|
2247
|
+
}
|
|
1467
2248
|
function activeWorkspace(database, project) {
|
|
1468
2249
|
const row = database.prepare(`
|
|
1469
2250
|
select * from lineage_workspaces
|
|
@@ -1473,6 +2254,20 @@ function activeWorkspace(database, project) {
|
|
|
1473
2254
|
`).get(project);
|
|
1474
2255
|
return row ? rowToWorkspace(row) : null;
|
|
1475
2256
|
}
|
|
2257
|
+
function listLineageWorkspaces(project) {
|
|
2258
|
+
const database = lineageDb();
|
|
2259
|
+
try {
|
|
2260
|
+
seedLegacyWorkspaces(database, project);
|
|
2261
|
+
return {
|
|
2262
|
+
project,
|
|
2263
|
+
active_workspace: activeWorkspace(database, project),
|
|
2264
|
+
workspaces: listRows(database, project),
|
|
2265
|
+
fetchedAt: nowIso()
|
|
2266
|
+
};
|
|
2267
|
+
} finally {
|
|
2268
|
+
database.close();
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
1476
2271
|
function activeLineageWorkspaceRoot(project) {
|
|
1477
2272
|
const database = lineageDb();
|
|
1478
2273
|
try {
|
|
@@ -1586,7 +2381,7 @@ function indexLineageAssets(project = defaultProject) {
|
|
|
1586
2381
|
database.close();
|
|
1587
2382
|
return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
|
|
1588
2383
|
}
|
|
1589
|
-
function
|
|
2384
|
+
function requireAsset2(database, project, assetId) {
|
|
1590
2385
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
1591
2386
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1592
2387
|
}
|
|
@@ -1606,7 +2401,7 @@ function rootFor(database, project, assetId) {
|
|
|
1606
2401
|
return assetId;
|
|
1607
2402
|
}
|
|
1608
2403
|
function assertCanonicalRoot(database, project, root) {
|
|
1609
|
-
|
|
2404
|
+
requireAsset2(database, project, root);
|
|
1610
2405
|
const canonicalRoot = rootFor(database, project, root);
|
|
1611
2406
|
if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
1612
2407
|
}
|
|
@@ -1643,7 +2438,7 @@ function lineageWriteClaimContext(database, project, assetId) {
|
|
|
1643
2438
|
function getLineageWriteClaimContext(project, assetId) {
|
|
1644
2439
|
const database = lineageDb();
|
|
1645
2440
|
try {
|
|
1646
|
-
|
|
2441
|
+
requireAsset2(database, project, assetId);
|
|
1647
2442
|
return lineageWriteClaimContext(database, project, assetId);
|
|
1648
2443
|
} finally {
|
|
1649
2444
|
database.close();
|
|
@@ -1655,12 +2450,12 @@ function latestSelectedRoot(database, project) {
|
|
|
1655
2450
|
}
|
|
1656
2451
|
function resolveRoot(database, project, rootAssetId) {
|
|
1657
2452
|
if (rootAssetId) {
|
|
1658
|
-
|
|
2453
|
+
requireAsset2(database, project, rootAssetId);
|
|
1659
2454
|
return rootAssetId;
|
|
1660
2455
|
}
|
|
1661
2456
|
const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
|
|
1662
2457
|
if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
|
|
1663
|
-
|
|
2458
|
+
requireAsset2(database, project, root);
|
|
1664
2459
|
return root;
|
|
1665
2460
|
}
|
|
1666
2461
|
function edgeId(project, parent, child) {
|
|
@@ -1685,6 +2480,33 @@ function rerollRequestFrom(row) {
|
|
|
1685
2480
|
resolved_at: rowString(row.resolved_at)
|
|
1686
2481
|
};
|
|
1687
2482
|
}
|
|
2483
|
+
function tasksByTarget(tasks) {
|
|
2484
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
2485
|
+
for (const task of tasks) {
|
|
2486
|
+
if (task.task_type !== "iterate" && task.task_type !== "reroll") continue;
|
|
2487
|
+
const existing = byTarget.get(task.target_asset_id) || {};
|
|
2488
|
+
existing[task.task_type] = task;
|
|
2489
|
+
byTarget.set(task.target_asset_id, existing);
|
|
2490
|
+
}
|
|
2491
|
+
return byTarget;
|
|
2492
|
+
}
|
|
2493
|
+
function withRerollTask(request, task) {
|
|
2494
|
+
return task ? { ...request, task_id: task.id, task } : request;
|
|
2495
|
+
}
|
|
2496
|
+
function taskBackedRerollRequest(project, rootAssetId, task) {
|
|
2497
|
+
return {
|
|
2498
|
+
id: `${task.id}:request`,
|
|
2499
|
+
project_id: project,
|
|
2500
|
+
root_asset_id: rootAssetId,
|
|
2501
|
+
node_asset_id: task.target_asset_id,
|
|
2502
|
+
status: "pending",
|
|
2503
|
+
requested_by: task.created_by,
|
|
2504
|
+
notes: task.instructions,
|
|
2505
|
+
created_at: task.created_at,
|
|
2506
|
+
task_id: task.id,
|
|
2507
|
+
task
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
1688
2510
|
function attemptFrom(row) {
|
|
1689
2511
|
return {
|
|
1690
2512
|
id: String(row.id),
|
|
@@ -1724,7 +2546,7 @@ function withImplicitAttempt(physicalAttempts, row) {
|
|
|
1724
2546
|
}
|
|
1725
2547
|
function assertNodeInRoot(database, project, root, node) {
|
|
1726
2548
|
assertCanonicalRoot(database, project, root);
|
|
1727
|
-
|
|
2549
|
+
requireAsset2(database, project, node);
|
|
1728
2550
|
const nodeRoot = rootFor(database, project, node);
|
|
1729
2551
|
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
1730
2552
|
}
|
|
@@ -1745,8 +2567,8 @@ function localPreviewUrl(project, localPath) {
|
|
|
1745
2567
|
}
|
|
1746
2568
|
function linkLineageAssets(project, fields) {
|
|
1747
2569
|
const database = lineageDb();
|
|
1748
|
-
|
|
1749
|
-
|
|
2570
|
+
requireAsset2(database, project, fields.parentAssetId);
|
|
2571
|
+
requireAsset2(database, project, fields.childAssetId);
|
|
1750
2572
|
if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
|
|
1751
2573
|
const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
|
|
1752
2574
|
try {
|
|
@@ -1797,10 +2619,17 @@ function descendants(database, project, root) {
|
|
|
1797
2619
|
}
|
|
1798
2620
|
function getLineageSnapshot(project, assetId) {
|
|
1799
2621
|
const database = lineageDb();
|
|
1800
|
-
|
|
2622
|
+
requireAsset2(database, project, assetId);
|
|
1801
2623
|
const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
|
|
1802
2624
|
const edges = descendants(database, project, root);
|
|
1803
|
-
const
|
|
2625
|
+
const selected = selectedRows(database, project, root);
|
|
2626
|
+
const tasks = listLineageTasks(project, root).tasks;
|
|
2627
|
+
const ids = [.../* @__PURE__ */ new Set([
|
|
2628
|
+
root,
|
|
2629
|
+
...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id]),
|
|
2630
|
+
...selected.map((row) => row.asset_id),
|
|
2631
|
+
...tasks.map((task) => task.target_asset_id)
|
|
2632
|
+
])];
|
|
1804
2633
|
const placeholders = ids.map(() => "?").join(",");
|
|
1805
2634
|
const rows = database.prepare(`
|
|
1806
2635
|
select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
|
|
@@ -1815,12 +2644,12 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1815
2644
|
for (const attempt of attemptRows.map(attemptFrom)) {
|
|
1816
2645
|
attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
|
|
1817
2646
|
}
|
|
2647
|
+
const lineageTasksByNode = tasksByTarget(tasks);
|
|
1818
2648
|
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) : [];
|
|
1819
2649
|
const rerollsByNode = new Map(rerollRows.map((row) => {
|
|
1820
2650
|
const request = rerollRequestFrom(row);
|
|
1821
|
-
return [request.node_asset_id, request];
|
|
2651
|
+
return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
|
|
1822
2652
|
}));
|
|
1823
|
-
const selected = selectedRows(database, project, root);
|
|
1824
2653
|
const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
|
|
1825
2654
|
const selectedIds = new Set(selected.map((row) => row.asset_id));
|
|
1826
2655
|
const selections = selected.map((row) => ({
|
|
@@ -1837,11 +2666,13 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1837
2666
|
const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
|
|
1838
2667
|
const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
|
|
1839
2668
|
const previewPath = currentAttempt?.file_path || row.local_path;
|
|
2669
|
+
const lineageTasks = lineageTasksByNode.get(row.asset_id);
|
|
1840
2670
|
return {
|
|
1841
2671
|
...node,
|
|
1842
2672
|
attempt_count: attempts.length,
|
|
1843
2673
|
current_attempt: currentAttempt,
|
|
1844
2674
|
is_latest: !childIds.has(row.asset_id),
|
|
2675
|
+
lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
|
|
1845
2676
|
position,
|
|
1846
2677
|
preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
|
|
1847
2678
|
reroll_request: rerollsByNode.get(row.asset_id),
|
|
@@ -1857,6 +2688,7 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1857
2688
|
selected: selections.map((row) => row.asset_id),
|
|
1858
2689
|
selection,
|
|
1859
2690
|
selections,
|
|
2691
|
+
tasks,
|
|
1860
2692
|
latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
|
|
1861
2693
|
nodes,
|
|
1862
2694
|
edges,
|
|
@@ -1868,7 +2700,25 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1868
2700
|
const root = resolveRoot(database, project, rootAssetId);
|
|
1869
2701
|
database.close();
|
|
1870
2702
|
const snapshot = getLineageSnapshot(project, root);
|
|
1871
|
-
const
|
|
2703
|
+
const pendingIterateTasks = (snapshot.tasks || []).filter((task) => task.task_type === "iterate" && task.status === "pending");
|
|
2704
|
+
const taskIdsInSelectionOrder = [
|
|
2705
|
+
...snapshot.selections.map((selection) => selection.asset_id).filter((assetId) => pendingIterateTasks.some((task) => task.target_asset_id === assetId)),
|
|
2706
|
+
...pendingIterateTasks.map((task) => task.target_asset_id).filter((assetId) => !snapshot.selections.some((selection) => selection.asset_id === assetId))
|
|
2707
|
+
];
|
|
2708
|
+
const taskSelectedIds = [...new Set(taskIdsInSelectionOrder)];
|
|
2709
|
+
const selectedIds = taskSelectedIds.length > 0 ? taskSelectedIds : snapshot.selected;
|
|
2710
|
+
const selectedNodes = selectedIds.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
|
|
2711
|
+
const taskSelections = taskSelectedIds.map((assetId, position) => {
|
|
2712
|
+
const task = pendingIterateTasks.find((item) => item.target_asset_id === assetId);
|
|
2713
|
+
return {
|
|
2714
|
+
asset_id: assetId,
|
|
2715
|
+
notes: task?.instructions,
|
|
2716
|
+
position,
|
|
2717
|
+
selected_at: task?.created_at || nowIso()
|
|
2718
|
+
};
|
|
2719
|
+
});
|
|
2720
|
+
const selectedSelection = taskSelections.length > 0 ? taskSelections[0] : snapshot.selection;
|
|
2721
|
+
const selectedSelections = taskSelections.length > 0 ? taskSelections : snapshot.selections;
|
|
1872
2722
|
const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
|
|
1873
2723
|
const warnings = [];
|
|
1874
2724
|
for (const selectedNode of selectedNodes) {
|
|
@@ -1886,9 +2736,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1886
2736
|
next_asset: selectedNodes[0],
|
|
1887
2737
|
next_assets: selectedNodes,
|
|
1888
2738
|
latest: snapshot.latest,
|
|
1889
|
-
selected:
|
|
1890
|
-
selection:
|
|
1891
|
-
selections:
|
|
2739
|
+
selected: selectedIds,
|
|
2740
|
+
selection: selectedSelection,
|
|
2741
|
+
selections: selectedSelections,
|
|
1892
2742
|
candidates: latestNodes,
|
|
1893
2743
|
warnings,
|
|
1894
2744
|
fetchedAt: nowIso()
|
|
@@ -1905,9 +2755,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1905
2755
|
next_asset: latestNodes[0],
|
|
1906
2756
|
next_assets: [latestNodes[0]],
|
|
1907
2757
|
latest: snapshot.latest,
|
|
1908
|
-
selected:
|
|
1909
|
-
selection:
|
|
1910
|
-
selections:
|
|
2758
|
+
selected: selectedIds,
|
|
2759
|
+
selection: selectedSelection,
|
|
2760
|
+
selections: selectedSelections,
|
|
1911
2761
|
candidates: latestNodes,
|
|
1912
2762
|
warnings,
|
|
1913
2763
|
fetchedAt: nowIso()
|
|
@@ -1923,9 +2773,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1923
2773
|
next_asset: null,
|
|
1924
2774
|
next_assets: [],
|
|
1925
2775
|
latest: snapshot.latest,
|
|
1926
|
-
selected:
|
|
1927
|
-
selection:
|
|
1928
|
-
selections:
|
|
2776
|
+
selected: selectedIds,
|
|
2777
|
+
selection: selectedSelection,
|
|
2778
|
+
selections: selectedSelections,
|
|
1929
2779
|
candidates: latestNodes,
|
|
1930
2780
|
warnings,
|
|
1931
2781
|
fetchedAt: nowIso()
|
|
@@ -1935,12 +2785,23 @@ function listLineageRerollRequests(project, rootAssetId) {
|
|
|
1935
2785
|
const database = lineageDb();
|
|
1936
2786
|
try {
|
|
1937
2787
|
assertCanonicalRoot(database, project, rootAssetId);
|
|
2788
|
+
const rerollTasks = listLineageTasks(project, rootAssetId).tasks.filter((task) => task.task_type === "reroll");
|
|
2789
|
+
const pendingRerollTasks = listLineageTasks(project, rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "reroll");
|
|
2790
|
+
const rerollTaskByTarget = new Map(rerollTasks.map((task) => [task.target_asset_id, task]));
|
|
1938
2791
|
const rows = database.prepare(`
|
|
1939
2792
|
select * from asset_reroll_requests
|
|
1940
2793
|
where project_id = ? and root_asset_id = ? and status = 'pending'
|
|
1941
2794
|
order by created_at, id
|
|
1942
2795
|
`).all(project, rootAssetId);
|
|
1943
|
-
|
|
2796
|
+
const requests = rows.map((row) => {
|
|
2797
|
+
const request = rerollRequestFrom(row);
|
|
2798
|
+
return withRerollTask(request, rerollTaskByTarget.get(request.node_asset_id));
|
|
2799
|
+
});
|
|
2800
|
+
const legacyTargets = new Set(requests.map((request) => request.node_asset_id));
|
|
2801
|
+
for (const task of pendingRerollTasks) {
|
|
2802
|
+
if (!legacyTargets.has(task.target_asset_id)) requests.push(taskBackedRerollRequest(project, rootAssetId, task));
|
|
2803
|
+
}
|
|
2804
|
+
return { project, root_asset_id: rootAssetId, requests, fetchedAt: nowIso() };
|
|
1944
2805
|
} finally {
|
|
1945
2806
|
database.close();
|
|
1946
2807
|
}
|
|
@@ -1949,7 +2810,7 @@ function recordLineageRerollAttempt(project, fields) {
|
|
|
1949
2810
|
const database = lineageDb();
|
|
1950
2811
|
try {
|
|
1951
2812
|
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
1952
|
-
|
|
2813
|
+
requireAsset2(database, project, fields.assetId);
|
|
1953
2814
|
assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
|
|
1954
2815
|
const timestamp = nowIso();
|
|
1955
2816
|
const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
|
|
@@ -2038,6 +2899,13 @@ function markLineageRerollRequest(project, fields) {
|
|
|
2038
2899
|
created_at: timestamp
|
|
2039
2900
|
};
|
|
2040
2901
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2902
|
+
const taskResult = upsertLineageTask(project, {
|
|
2903
|
+
createdBy: request.requested_by,
|
|
2904
|
+
instructions: request.notes,
|
|
2905
|
+
rootAssetId: fields.rootAssetId,
|
|
2906
|
+
targetAssetId: fields.nodeAssetId,
|
|
2907
|
+
taskType: "reroll"
|
|
2908
|
+
});
|
|
2041
2909
|
if (existing) {
|
|
2042
2910
|
database.prepare(`
|
|
2043
2911
|
update asset_reroll_requests
|
|
@@ -2057,7 +2925,12 @@ function markLineageRerollRequest(project, fields) {
|
|
|
2057
2925
|
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
2058
2926
|
ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
|
|
2059
2927
|
`).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
|
|
2060
|
-
return {
|
|
2928
|
+
return {
|
|
2929
|
+
ok: true,
|
|
2930
|
+
request: withRerollTask(rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)), taskResult.task),
|
|
2931
|
+
task_id: taskResult.task.id,
|
|
2932
|
+
task: taskResult.task
|
|
2933
|
+
};
|
|
2061
2934
|
} finally {
|
|
2062
2935
|
database.close();
|
|
2063
2936
|
}
|
|
@@ -2075,12 +2948,23 @@ function clearLineageRerollRequest(project, fields) {
|
|
|
2075
2948
|
const timestamp = nowIso();
|
|
2076
2949
|
const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
|
|
2077
2950
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2951
|
+
const task = listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.find((item) => item.task_type === "reroll" && item.target_asset_id === fields.nodeAssetId);
|
|
2952
|
+
const cancelledTask = task ? cancelLineageTask(project, {
|
|
2953
|
+
actor: "human",
|
|
2954
|
+
confirmWrite: true,
|
|
2955
|
+
taskId: task.id
|
|
2956
|
+
}).task : void 0;
|
|
2078
2957
|
database.prepare(`
|
|
2079
2958
|
update asset_reroll_requests
|
|
2080
2959
|
set status = 'cancelled', resolved_at = ?
|
|
2081
2960
|
where id = ?
|
|
2082
2961
|
`).run(timestamp, request.id);
|
|
2083
|
-
return {
|
|
2962
|
+
return {
|
|
2963
|
+
ok: true,
|
|
2964
|
+
request: withRerollTask(request, cancelledTask),
|
|
2965
|
+
task_id: cancelledTask?.id,
|
|
2966
|
+
task: cancelledTask
|
|
2967
|
+
};
|
|
2084
2968
|
} finally {
|
|
2085
2969
|
database.close();
|
|
2086
2970
|
}
|
|
@@ -2097,6 +2981,9 @@ function lineageCommand(command, project, rootAssetId) {
|
|
|
2097
2981
|
function linkChildCommand(project, rootAssetId) {
|
|
2098
2982
|
return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
|
|
2099
2983
|
}
|
|
2984
|
+
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
2985
|
+
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
2986
|
+
}
|
|
2100
2987
|
function getLineageBrief(project, rootAssetId) {
|
|
2101
2988
|
const next = getLineageNextAsset(project, rootAssetId);
|
|
2102
2989
|
const assets = next.next_assets;
|
|
@@ -2143,6 +3030,14 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
2143
3030
|
function linkSelectedLineageChild(project, fields) {
|
|
2144
3031
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
2145
3032
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
3033
|
+
const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
|
|
3034
|
+
const pendingRerollForParent = rerollRequests.find((request) => request.node_asset_id === next.next_asset?.asset_id);
|
|
3035
|
+
if (pendingRerollForParent && fields.confirmWrite) {
|
|
3036
|
+
throw new LineageError(
|
|
3037
|
+
`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.`,
|
|
3038
|
+
409
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
2146
3041
|
if (fields.confirmWrite) {
|
|
2147
3042
|
const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
|
|
2148
3043
|
const validation = validateAgentClaimForWrite({
|
|
@@ -2169,7 +3064,11 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
2169
3064
|
parent_asset_id: next.next_asset.asset_id,
|
|
2170
3065
|
child_asset_id: fields.childAssetId,
|
|
2171
3066
|
reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
|
|
2172
|
-
warning:
|
|
3067
|
+
warning: [
|
|
3068
|
+
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,
|
|
3069
|
+
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,
|
|
3070
|
+
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
|
|
3071
|
+
].filter(Boolean).join(" ") || void 0
|
|
2173
3072
|
};
|
|
2174
3073
|
}
|
|
2175
3074
|
|
|
@@ -2359,7 +3258,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
2359
3258
|
created_at: timestamp
|
|
2360
3259
|
}]
|
|
2361
3260
|
};
|
|
2362
|
-
if (fields.dryRun) return { ok: true, command: "
|
|
3261
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
|
|
2363
3262
|
const database = lineageDb();
|
|
2364
3263
|
try {
|
|
2365
3264
|
database.exec("BEGIN IMMEDIATE");
|
|
@@ -2377,7 +3276,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
2377
3276
|
database.exec("ROLLBACK");
|
|
2378
3277
|
throw error;
|
|
2379
3278
|
}
|
|
2380
|
-
return { ok: true, command: "
|
|
3279
|
+
return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
|
|
2381
3280
|
} finally {
|
|
2382
3281
|
database.close();
|
|
2383
3282
|
}
|
|
@@ -2456,20 +3355,251 @@ function importImageRerollOutput(project = defaultProject, fields) {
|
|
|
2456
3355
|
checksumSha256: resolved.checksum,
|
|
2457
3356
|
confirmWrite: true
|
|
2458
3357
|
});
|
|
3358
|
+
const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
|
|
3359
|
+
if (rerollTask) {
|
|
3360
|
+
resolveLineageTask(project, {
|
|
3361
|
+
actor: "agent",
|
|
3362
|
+
confirmWrite: true,
|
|
3363
|
+
resolvedAssetId: resolved.assetId,
|
|
3364
|
+
resolvedGenerationJobId: fields.jobId,
|
|
3365
|
+
taskId: rerollTask.id
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
2459
3368
|
const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
|
|
2460
|
-
return { ok: true, command: "
|
|
3369
|
+
return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
|
|
2461
3370
|
} finally {
|
|
2462
3371
|
writeDb.close();
|
|
2463
3372
|
}
|
|
2464
3373
|
}
|
|
2465
3374
|
|
|
3375
|
+
// src/server/lineageSelectionPacket.ts
|
|
3376
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3377
|
+
import { existsSync as existsSync5, statSync as statSync4 } from "node:fs";
|
|
3378
|
+
import { isAbsolute, resolve as resolve4 } from "node:path";
|
|
3379
|
+
var LineageSelectionPacketError = class extends Error {
|
|
3380
|
+
constructor(message, warnings = [], errors = []) {
|
|
3381
|
+
super(message);
|
|
3382
|
+
this.warnings = warnings;
|
|
3383
|
+
this.errors = errors;
|
|
3384
|
+
}
|
|
3385
|
+
warnings;
|
|
3386
|
+
errors;
|
|
3387
|
+
};
|
|
3388
|
+
function listAllAssets(project) {
|
|
3389
|
+
const pageSize = 100;
|
|
3390
|
+
const first = listAssets(project, { page: 1, pageSize, source: "all" });
|
|
3391
|
+
const assets = [...first.assets];
|
|
3392
|
+
for (let page = 2; page <= first.pagination.totalPages; page += 1) {
|
|
3393
|
+
assets.push(...listAssets(project, { page, pageSize, source: "all" }).assets);
|
|
3394
|
+
}
|
|
3395
|
+
return assets;
|
|
3396
|
+
}
|
|
3397
|
+
function resolveWorkspace(project, options) {
|
|
3398
|
+
if (options.rootAssetId && options.workspaceId) {
|
|
3399
|
+
throw new LineageSelectionPacketError("Use either --root or --workspace, not both.", [], ["ambiguous_workspace"]);
|
|
3400
|
+
}
|
|
3401
|
+
const snapshot = listLineageWorkspaces(project);
|
|
3402
|
+
const target = options.workspaceId || options.rootAssetId;
|
|
3403
|
+
const workspace = target ? snapshot.workspaces.find((item) => item.id === target || item.root_asset_id === target) : snapshot.active_workspace;
|
|
3404
|
+
if (workspace) return workspace;
|
|
3405
|
+
if (options.rootAssetId) {
|
|
3406
|
+
return {
|
|
3407
|
+
id: lineageWorkspaceId(project, options.rootAssetId),
|
|
3408
|
+
project,
|
|
3409
|
+
root_asset_id: options.rootAssetId,
|
|
3410
|
+
title: `${options.rootAssetId} lineage`,
|
|
3411
|
+
status: "active",
|
|
3412
|
+
created_by: "system",
|
|
3413
|
+
created_at: "",
|
|
3414
|
+
updated_at: ""
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
const message = options.workspaceId ? `Unknown lineage workspace: ${options.workspaceId}` : "No active lineage workspace. Pass --root or activate a workspace first.";
|
|
3418
|
+
throw new LineageSelectionPacketError(message, [], [options.workspaceId ? "unknown_workspace" : "missing_active_workspace"]);
|
|
3419
|
+
}
|
|
3420
|
+
function resolveLocalReference(reference) {
|
|
3421
|
+
if (!reference) return void 0;
|
|
3422
|
+
if (isAbsolute(reference)) return reference;
|
|
3423
|
+
const repoRelative = resolve4(repoRoot, reference);
|
|
3424
|
+
if (existsSync5(repoRelative)) return repoRelative;
|
|
3425
|
+
return resolve4(repoRoot, ".asset-scratch", reference);
|
|
3426
|
+
}
|
|
3427
|
+
function fileSize(path) {
|
|
3428
|
+
if (!path || !existsSync5(path)) return void 0;
|
|
3429
|
+
try {
|
|
3430
|
+
return statSync4(path).size;
|
|
3431
|
+
} catch {
|
|
3432
|
+
return void 0;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
function storageState(hasLocal, hasS3) {
|
|
3436
|
+
if (hasLocal && hasS3) return "local_and_s3";
|
|
3437
|
+
if (hasLocal) return "local_only";
|
|
3438
|
+
if (hasS3) return "s3_backed";
|
|
3439
|
+
return "unresolved";
|
|
3440
|
+
}
|
|
3441
|
+
function currentAttemptFor(node) {
|
|
3442
|
+
if (!node.current_attempt) return void 0;
|
|
3443
|
+
return {
|
|
3444
|
+
asset_id: node.current_attempt.asset_id,
|
|
3445
|
+
checksum_sha256: node.current_attempt.checksum_sha256,
|
|
3446
|
+
file_path: node.current_attempt.file_path,
|
|
3447
|
+
generation_job_id: node.current_attempt.generation_job_id,
|
|
3448
|
+
id: node.current_attempt.id,
|
|
3449
|
+
is_current: node.current_attempt.is_current,
|
|
3450
|
+
source: node.current_attempt.source
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
function assetForNode(node, catalogAsset, warnings) {
|
|
3454
|
+
const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
|
|
3455
|
+
const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
|
|
3456
|
+
const localExists = absolutePath ? existsSync5(absolutePath) : false;
|
|
3457
|
+
const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
|
|
3458
|
+
const s3Key = catalogAsset?.s3?.key || node.s3_key;
|
|
3459
|
+
const hasS3 = Boolean(s3Key);
|
|
3460
|
+
const checksum = catalogAsset?.local?.checksum_sha256 || node.current_attempt?.checksum_sha256 || node.checksum_sha256 || catalogAsset?.s3?.checksum_sha256;
|
|
3461
|
+
const mimeType = catalogAsset?.local?.content_type || catalogAsset?.s3?.content_type;
|
|
3462
|
+
const mediaType = catalogAsset?.content_type || node.media_type;
|
|
3463
|
+
if (hasLocalClaim && !localExists) warnings.push(`Selected asset ${node.asset_id} has a local path but the file is missing: ${absolutePath || node.local_path}`);
|
|
3464
|
+
if (!hasLocalClaim && !hasS3) warnings.push(`Selected asset ${node.asset_id} has neither a local file nor S3 key.`);
|
|
3465
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
3466
|
+
return {
|
|
3467
|
+
asset_id: node.asset_id,
|
|
3468
|
+
campaign: catalogAsset?.campaign || node.campaign,
|
|
3469
|
+
channel: catalogAsset?.channel || node.channel,
|
|
3470
|
+
checksum_sha256: checksum,
|
|
3471
|
+
current_attempt: currentAttemptFor(node),
|
|
3472
|
+
local: {
|
|
3473
|
+
absolute_path: absolutePath,
|
|
3474
|
+
content_type: catalogAsset?.local?.content_type,
|
|
3475
|
+
exists: localExists,
|
|
3476
|
+
relative_path: catalogAsset?.local?.relative_path || node.local_path || node.current_attempt?.file_path,
|
|
3477
|
+
size_bytes: catalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
3478
|
+
},
|
|
3479
|
+
media_type: mediaType,
|
|
3480
|
+
mime_type: mimeType,
|
|
3481
|
+
review_notes: node.review_notes,
|
|
3482
|
+
review_state: node.review_state,
|
|
3483
|
+
s3: {
|
|
3484
|
+
bucket: catalogAsset?.s3?.bucket,
|
|
3485
|
+
checksum_sha256: catalogAsset?.s3?.checksum_sha256,
|
|
3486
|
+
content_type: catalogAsset?.s3?.content_type,
|
|
3487
|
+
etag: catalogAsset?.s3?.etag,
|
|
3488
|
+
key: s3Key,
|
|
3489
|
+
region: catalogAsset?.s3?.region,
|
|
3490
|
+
size_bytes: catalogAsset?.s3?.size_bytes,
|
|
3491
|
+
version_id: catalogAsset?.s3?.version_id
|
|
3492
|
+
},
|
|
3493
|
+
selection_note: node.selection_note,
|
|
3494
|
+
source: catalogAsset?.source || node.source,
|
|
3495
|
+
status: catalogAsset?.status || node.status,
|
|
3496
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
3497
|
+
title: catalogAsset?.title || node.title || node.asset_id
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
function packetId(packetIdentity) {
|
|
3501
|
+
const digest = createHash3("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
|
|
3502
|
+
return `lineage_packet_${digest}`;
|
|
3503
|
+
}
|
|
3504
|
+
function getLineageSelectionPacket(project, options = {}) {
|
|
3505
|
+
const workspace = resolveWorkspace(project, options);
|
|
3506
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
3507
|
+
const catalogSummary = validateProject(project);
|
|
3508
|
+
const catalogAssets = listAllAssets(project);
|
|
3509
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
3510
|
+
const warnings = [];
|
|
3511
|
+
const errors = [];
|
|
3512
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
3513
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
3514
|
+
asset_id: selection.asset_id,
|
|
3515
|
+
position: selection.position,
|
|
3516
|
+
selected_at: selection.selected_at,
|
|
3517
|
+
selection_note: selection.notes
|
|
3518
|
+
}));
|
|
3519
|
+
if (selectedItems.length === 0) warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
3520
|
+
const assets = selectedItems.flatMap((item) => {
|
|
3521
|
+
const node = nodeById.get(item.asset_id);
|
|
3522
|
+
if (!node) {
|
|
3523
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
3524
|
+
warnings.push(message);
|
|
3525
|
+
errors.push("selected_asset_outside_workspace");
|
|
3526
|
+
return [];
|
|
3527
|
+
}
|
|
3528
|
+
return [assetForNode(node, catalogById.get(item.asset_id), warnings)];
|
|
3529
|
+
});
|
|
3530
|
+
if (assets.some((asset) => !asset.dimensions)) warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
3531
|
+
if (options.strict) {
|
|
3532
|
+
const strictErrors = [
|
|
3533
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
3534
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
3535
|
+
...errors
|
|
3536
|
+
];
|
|
3537
|
+
if (strictErrors.length > 0) {
|
|
3538
|
+
throw new LineageSelectionPacketError(`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`, warnings, strictErrors);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
const context = {
|
|
3542
|
+
campaign: options.campaign,
|
|
3543
|
+
channel: options.channel,
|
|
3544
|
+
labels: options.labels || [],
|
|
3545
|
+
notes: options.contextNotes
|
|
3546
|
+
};
|
|
3547
|
+
const identity = {
|
|
3548
|
+
assets: assets.map((asset) => ({
|
|
3549
|
+
asset_id: asset.asset_id,
|
|
3550
|
+
checksum_sha256: asset.checksum_sha256,
|
|
3551
|
+
local_path: asset.local.absolute_path,
|
|
3552
|
+
s3_key: asset.s3.key,
|
|
3553
|
+
storage_state: asset.storage_state
|
|
3554
|
+
})),
|
|
3555
|
+
context,
|
|
3556
|
+
project,
|
|
3557
|
+
root_asset_id: workspace.root_asset_id,
|
|
3558
|
+
schema_version: "lineage.selection_packet.v1",
|
|
3559
|
+
selection: selectedItems,
|
|
3560
|
+
workspace_id: workspace.id
|
|
3561
|
+
};
|
|
3562
|
+
return {
|
|
3563
|
+
assets,
|
|
3564
|
+
context,
|
|
3565
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3566
|
+
errors,
|
|
3567
|
+
kind: "lineage.selection_packet",
|
|
3568
|
+
packet_id: packetId(identity),
|
|
3569
|
+
product: catalogSummary.product,
|
|
3570
|
+
project,
|
|
3571
|
+
schema_version: "lineage.selection_packet.v1",
|
|
3572
|
+
selection: {
|
|
3573
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
3574
|
+
count: selectedItems.length,
|
|
3575
|
+
items: selectedItems,
|
|
3576
|
+
root_asset_id: workspace.root_asset_id
|
|
3577
|
+
},
|
|
3578
|
+
source: {
|
|
3579
|
+
app: "lineage",
|
|
3580
|
+
command: options.command,
|
|
3581
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
3582
|
+
package: options.packageVersion
|
|
3583
|
+
},
|
|
3584
|
+
warnings: [...new Set(warnings)],
|
|
3585
|
+
workspace: {
|
|
3586
|
+
active_at: workspace.active_at,
|
|
3587
|
+
id: workspace.id,
|
|
3588
|
+
notes: workspace.notes,
|
|
3589
|
+
root_asset_id: workspace.root_asset_id,
|
|
3590
|
+
status: workspace.status,
|
|
3591
|
+
title: workspace.title
|
|
3592
|
+
}
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
|
|
2466
3596
|
// src/cli/lineageCli.ts
|
|
2467
3597
|
var signalExitCodes = {
|
|
2468
3598
|
SIGINT: 130,
|
|
2469
3599
|
SIGTERM: 143
|
|
2470
3600
|
};
|
|
2471
3601
|
function packageRoot() {
|
|
2472
|
-
return
|
|
3602
|
+
return resolve5(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
|
|
2473
3603
|
}
|
|
2474
3604
|
function packageVersion() {
|
|
2475
3605
|
try {
|
|
@@ -2493,6 +3623,19 @@ function readOption(args, name) {
|
|
|
2493
3623
|
if (index >= 0) return args[index + 1];
|
|
2494
3624
|
return void 0;
|
|
2495
3625
|
}
|
|
3626
|
+
function readOptions(args, name) {
|
|
3627
|
+
const values = [];
|
|
3628
|
+
const prefix = `${name}=`;
|
|
3629
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3630
|
+
const arg = args[index];
|
|
3631
|
+
if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length));
|
|
3632
|
+
else if (arg === name && args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
3633
|
+
values.push(args[index + 1]);
|
|
3634
|
+
index += 1;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return values;
|
|
3638
|
+
}
|
|
2496
3639
|
function resolveStartOptions(config, args) {
|
|
2497
3640
|
const runtimeDir = dataRoot(config.displayName);
|
|
2498
3641
|
const rawPort = readOption(args, "--port") || process.env.PORT || String(config.defaultPort);
|
|
@@ -2508,20 +3651,29 @@ function resolveStartOptions(config, args) {
|
|
|
2508
3651
|
port
|
|
2509
3652
|
};
|
|
2510
3653
|
}
|
|
2511
|
-
function
|
|
2512
|
-
|
|
3654
|
+
function formatLineageHelp(config) {
|
|
3655
|
+
return `${config.binName} ${packageVersion()}
|
|
2513
3656
|
|
|
2514
3657
|
Usage:
|
|
2515
3658
|
${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
|
|
2516
3659
|
${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
2517
3660
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
2518
3661
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3662
|
+
${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--out <path>] [--strict] [--db <path>] [--json]
|
|
2519
3663
|
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
2520
3664
|
${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
2521
3665
|
${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
2522
3666
|
${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
2523
3667
|
${config.binName} reroll plan --root <asset-id> --target <asset-id> --prompt <text> [--project <project>] [--db <path>] [--json]
|
|
2524
3668
|
${config.binName} reroll import --job-id <job-id> --file <scratch-file> --confirm-write [--project <project>] [--db <path>] [--json]
|
|
3669
|
+
${config.binName} tasks list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3670
|
+
${config.binName} tasks inspect --task <task-id> [--project <project>] [--db <path>] [--json]
|
|
3671
|
+
${config.binName} tasks claim --task <task-id> --agent-name <name> [--project <project>] [--db <path>] [--json]
|
|
3672
|
+
${config.binName} tasks start --task <task-id> --claim-token <claim-id.secret> [--project <project>] [--db <path>] [--json]
|
|
3673
|
+
${config.binName} tasks comment --task <task-id> --message <text> [--project <project>] [--db <path>] [--json]
|
|
3674
|
+
${config.binName} tasks cancel --task <task-id> [--confirm-write] [--override] [--project <project>] [--db <path>] [--json]
|
|
3675
|
+
${config.binName} tasks override --task <task-id> --reason <text> [--instructions <text>] [--project <project>] [--db <path>] [--json]
|
|
3676
|
+
${config.binName} tasks instructions --task <task-id> --instructions <text> [--project <project>] [--db <path>] [--json]
|
|
2525
3677
|
${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
|
|
2526
3678
|
${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
2527
3679
|
${config.binName} agent status [--project <project>] [--json]
|
|
@@ -2533,7 +3685,14 @@ Usage:
|
|
|
2533
3685
|
${config.binName} --help
|
|
2534
3686
|
${config.binName} --version
|
|
2535
3687
|
|
|
2536
|
-
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel
|
|
3688
|
+
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.
|
|
3689
|
+
|
|
3690
|
+
Variation vs re-roll:
|
|
3691
|
+
link-child creates a new visible child variation edge.
|
|
3692
|
+
reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
|
|
3693
|
+
}
|
|
3694
|
+
function printHelp(config) {
|
|
3695
|
+
console.log(formatLineageHelp(config));
|
|
2537
3696
|
}
|
|
2538
3697
|
function openBrowser(url) {
|
|
2539
3698
|
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
@@ -2576,6 +3735,31 @@ function runLineageDataCommand(command, args) {
|
|
|
2576
3735
|
if (!options.assetId) throw new Error("lineage inspect requires --asset-id");
|
|
2577
3736
|
return getLineageSnapshot(options.project, options.assetId);
|
|
2578
3737
|
}
|
|
3738
|
+
if (command === "selection") {
|
|
3739
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
3740
|
+
if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
|
|
3741
|
+
const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
|
|
3742
|
+
const packet = getLineageSelectionPacket(options.project, {
|
|
3743
|
+
campaign: readOption(args, "--campaign"),
|
|
3744
|
+
channel: readOption(args, "--channel"),
|
|
3745
|
+
command: "lineage selection packet",
|
|
3746
|
+
contextNotes: readOption(args, "--context-notes") || readOption(args, "--notes"),
|
|
3747
|
+
dbPath: options.dbPath,
|
|
3748
|
+
labels,
|
|
3749
|
+
packageVersion: packageVersion(),
|
|
3750
|
+
rootAssetId: options.rootAssetId,
|
|
3751
|
+
strict: args.includes("--strict"),
|
|
3752
|
+
workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
|
|
3753
|
+
});
|
|
3754
|
+
const out = readOption(args, "--out");
|
|
3755
|
+
if (out) {
|
|
3756
|
+
const outPath = resolve5(out);
|
|
3757
|
+
mkdirSync4(dirname3(outPath), { recursive: true });
|
|
3758
|
+
writeFileSync2(outPath, `${JSON.stringify(packet, null, 2)}
|
|
3759
|
+
`);
|
|
3760
|
+
}
|
|
3761
|
+
return packet;
|
|
3762
|
+
}
|
|
2579
3763
|
if (command === "link-child") {
|
|
2580
3764
|
if (!options.childAssetId) throw new Error("lineage link-child requires --child");
|
|
2581
3765
|
return linkSelectedLineageChild(options.project, {
|
|
@@ -2636,6 +3820,66 @@ function runLineageDataCommand(command, args) {
|
|
|
2636
3820
|
}
|
|
2637
3821
|
throw new Error(`Unknown reroll command: ${subcommand}`);
|
|
2638
3822
|
}
|
|
3823
|
+
if (command === "tasks") {
|
|
3824
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
3825
|
+
const taskId = readOption(args, "--task");
|
|
3826
|
+
if (subcommand === "list") {
|
|
3827
|
+
if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
|
|
3828
|
+
return listLineageTasks(options.project, options.rootAssetId);
|
|
3829
|
+
}
|
|
3830
|
+
if (subcommand === "inspect") {
|
|
3831
|
+
if (!taskId) throw new Error("lineage tasks inspect requires --task");
|
|
3832
|
+
return getLineageTask(options.project, taskId);
|
|
3833
|
+
}
|
|
3834
|
+
if (subcommand === "claim") {
|
|
3835
|
+
const agentName = readOption(args, "--agent-name");
|
|
3836
|
+
if (!taskId) throw new Error("lineage tasks claim requires --task");
|
|
3837
|
+
if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
|
|
3838
|
+
return claimLineageTask(options.project, { taskId, agentName });
|
|
3839
|
+
}
|
|
3840
|
+
if (subcommand === "start") {
|
|
3841
|
+
if (!taskId) throw new Error("lineage tasks start requires --task");
|
|
3842
|
+
if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
|
|
3843
|
+
return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
|
|
3844
|
+
}
|
|
3845
|
+
if (subcommand === "comment") {
|
|
3846
|
+
const message = readOption(args, "--message");
|
|
3847
|
+
if (!taskId) throw new Error("lineage tasks comment requires --task");
|
|
3848
|
+
if (!message) throw new Error("lineage tasks comment requires --message");
|
|
3849
|
+
return addLineageTaskComment(options.project, {
|
|
3850
|
+
actor: readOption(args, "--actor") || "human",
|
|
3851
|
+
message,
|
|
3852
|
+
taskId
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
if (subcommand === "cancel") {
|
|
3856
|
+
if (!taskId) throw new Error("lineage tasks cancel requires --task");
|
|
3857
|
+
return cancelLineageTask(options.project, {
|
|
3858
|
+
actor: readOption(args, "--actor") || "human",
|
|
3859
|
+
confirmWrite: options.confirmWrite,
|
|
3860
|
+
override: args.includes("--override"),
|
|
3861
|
+
taskId
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
if (subcommand === "override") {
|
|
3865
|
+
const reason = readOption(args, "--reason");
|
|
3866
|
+
if (!taskId) throw new Error("lineage tasks override requires --task");
|
|
3867
|
+
if (!reason) throw new Error("lineage tasks override requires --reason");
|
|
3868
|
+
return overrideLineageTask(options.project, {
|
|
3869
|
+
actor: readOption(args, "--actor") || "human",
|
|
3870
|
+
instructions: readOption(args, "--instructions"),
|
|
3871
|
+
reason,
|
|
3872
|
+
taskId
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
if (subcommand === "instructions") {
|
|
3876
|
+
const instructions = readOption(args, "--instructions");
|
|
3877
|
+
if (!taskId) throw new Error("lineage tasks instructions requires --task");
|
|
3878
|
+
if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
|
|
3879
|
+
return updateLineageTaskInstructions(options.project, { instructions, taskId });
|
|
3880
|
+
}
|
|
3881
|
+
throw new Error(`Unknown tasks command: ${subcommand}`);
|
|
3882
|
+
}
|
|
2639
3883
|
throw new Error(`Unknown command: ${command}`);
|
|
2640
3884
|
}
|
|
2641
3885
|
function rerollRequestedBy(value) {
|
|
@@ -2665,9 +3909,17 @@ function printDataResult(command, result, json) {
|
|
|
2665
3909
|
console.log(`Active: ${snapshot.active_asset_id}`);
|
|
2666
3910
|
return;
|
|
2667
3911
|
}
|
|
3912
|
+
if (command === "selection" && result && typeof result === "object" && "packet_id" in result) {
|
|
3913
|
+
const packet = result;
|
|
3914
|
+
console.log(`${packet.packet_id}: ${packet.selection?.count || packet.assets?.length || 0} selected asset(s)`);
|
|
3915
|
+
if (packet.workspace?.root_asset_id) console.log(`Root: ${packet.workspace.root_asset_id}`);
|
|
3916
|
+
for (const warning of packet.warnings || []) console.log(`Warning: ${warning}`);
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
2668
3919
|
if (command === "link-child" && result && typeof result === "object") {
|
|
2669
3920
|
const link = result;
|
|
2670
3921
|
console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
|
|
3922
|
+
if (link.warning) console.log(`Warning: ${link.warning}`);
|
|
2671
3923
|
return;
|
|
2672
3924
|
}
|
|
2673
3925
|
if (command === "reroll" && result && typeof result === "object") {
|
|
@@ -2688,6 +3940,22 @@ function printDataResult(command, result, json) {
|
|
|
2688
3940
|
return;
|
|
2689
3941
|
}
|
|
2690
3942
|
}
|
|
3943
|
+
if (command === "tasks" && result && typeof result === "object") {
|
|
3944
|
+
if ("tasks" in result) {
|
|
3945
|
+
const listed = result;
|
|
3946
|
+
console.log(`${listed.tasks.length} lineage task(s)`);
|
|
3947
|
+
for (const task of listed.tasks) console.log(`${task.id} ${task.task_type} ${task.status} ${task.target_asset_id}`);
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if ("task" in result) {
|
|
3951
|
+
const mutation = result;
|
|
3952
|
+
const prefix = mutation.dryRun ? "Dry run: " : "";
|
|
3953
|
+
console.log(`${prefix}${mutation.task?.id || "task"} ${mutation.task?.task_type || "task"} ${mutation.task?.status || "unknown"} ${mutation.task?.target_asset_id || ""}`.trim());
|
|
3954
|
+
if ("claim_token" in mutation && typeof mutation.claim_token === "string") console.log(`Token: ${mutation.claim_token}`);
|
|
3955
|
+
if (mutation.events && mutation.events.length > 0) console.log(`Events: ${mutation.events.map((event) => event.event_type).join(", ")}`);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
2691
3959
|
console.log(String(result));
|
|
2692
3960
|
}
|
|
2693
3961
|
function runLineageAgentCommand(command, args) {
|
|
@@ -2817,7 +4085,7 @@ function start(config, args) {
|
|
|
2817
4085
|
process.exit(1);
|
|
2818
4086
|
}
|
|
2819
4087
|
const serverPath = join6(packageRoot(), "dist", "server.js");
|
|
2820
|
-
if (!
|
|
4088
|
+
if (!existsSync6(serverPath)) {
|
|
2821
4089
|
const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
|
|
2822
4090
|
if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
2823
4091
|
else console.error(`${config.binName}: ${message}`);
|
|
@@ -2871,7 +4139,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
2871
4139
|
start(config, normalizedArgs.slice(1));
|
|
2872
4140
|
return;
|
|
2873
4141
|
}
|
|
2874
|
-
if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll") {
|
|
4142
|
+
if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
|
|
2875
4143
|
const commandArgs = normalizedArgs.slice(1);
|
|
2876
4144
|
const json2 = commandArgs.includes("--json");
|
|
2877
4145
|
try {
|