@mean-weasel/lineage 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +82 -4
- package/dist/cli/lineage-dev.js +1325 -114
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1315 -111
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +2039 -150
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-CKMlZT43.css → index-3uJbXqPS.css} +1 -1
- package/dist/web/assets/index-OkymIyPO.js +23 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CICrfv4C.js +0 -23
package/dist/server.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
2
|
import express2 from "express";
|
|
3
3
|
import multer from "multer";
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
5
|
+
import { join as join14 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/server/assetCore.ts
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { dirname as
|
|
8
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9
|
+
import { dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
@@ -282,22 +282,558 @@ function createS3StorageAdapter(deps) {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// src/server/assetLineageDb.ts
|
|
285
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
286
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
|
|
287
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
288
|
+
|
|
289
|
+
// src/server/lineageProfiles.ts
|
|
285
290
|
import { createRequire } from "node:module";
|
|
286
|
-
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
287
|
-
import {
|
|
291
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
|
292
|
+
import { homedir, platform } from "node:os";
|
|
293
|
+
import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve2 } from "node:path";
|
|
294
|
+
|
|
295
|
+
// src/shared/lineageProfileTypes.ts
|
|
296
|
+
var lineageProfileSchemaVersion = "lineage.profile.v1";
|
|
297
|
+
var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
|
|
298
|
+
|
|
299
|
+
// src/server/lineageProfiles.ts
|
|
288
300
|
var require2 = createRequire(import.meta.url);
|
|
301
|
+
var profileIdPattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
302
|
+
function lineageDataRoot() {
|
|
303
|
+
if (process.env.LINEAGE_HOME) return resolve2(process.env.LINEAGE_HOME);
|
|
304
|
+
if (platform() === "darwin") return join3(homedir(), "Library", "Application Support", "Lineage");
|
|
305
|
+
if (platform() === "win32") return join3(process.env.APPDATA || join3(homedir(), "AppData", "Roaming"), "Lineage");
|
|
306
|
+
return join3(process.env.XDG_DATA_HOME || join3(homedir(), ".local", "share"), "lineage");
|
|
307
|
+
}
|
|
308
|
+
function lineageProfileRoot() {
|
|
309
|
+
return resolve2(process.env.LINEAGE_PROFILE_ROOT || join3(lineageDataRoot(), "profiles"));
|
|
310
|
+
}
|
|
311
|
+
function environmentChannel(environment) {
|
|
312
|
+
if (environment === "production") return "stable";
|
|
313
|
+
if (environment === "preview") return "preview";
|
|
314
|
+
return "dev";
|
|
315
|
+
}
|
|
316
|
+
function channelEnvironment(channel) {
|
|
317
|
+
if (channel === "stable") return "production";
|
|
318
|
+
if (channel === "preview") return "preview";
|
|
319
|
+
return "development";
|
|
320
|
+
}
|
|
321
|
+
function profileManifestPath(selector) {
|
|
322
|
+
const value = selector.trim();
|
|
323
|
+
if (!value) throw new Error("Profile selector must not be empty");
|
|
324
|
+
const looksLikePath = isAbsolute(value) || value.includes("/") || value.includes("\\") || value.endsWith(".json");
|
|
325
|
+
if (looksLikePath) return { manifestPath: resolve2(value) };
|
|
326
|
+
if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
|
|
327
|
+
return { manifestPath: join3(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
|
|
328
|
+
}
|
|
329
|
+
function requiredString(record, key) {
|
|
330
|
+
const value = record[key];
|
|
331
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
|
|
332
|
+
return value.trim();
|
|
333
|
+
}
|
|
334
|
+
function optionalString(record, key) {
|
|
335
|
+
const value = record[key];
|
|
336
|
+
if (value === void 0) return void 0;
|
|
337
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
|
|
338
|
+
return value.trim();
|
|
339
|
+
}
|
|
340
|
+
function validateEnvironment(value) {
|
|
341
|
+
if (value === "production" || value === "preview" || value === "development") return value;
|
|
342
|
+
throw new Error(`Invalid profile environment: ${value}`);
|
|
343
|
+
}
|
|
344
|
+
function validateChannel(value) {
|
|
345
|
+
if (value === void 0) return void 0;
|
|
346
|
+
if (value === "stable" || value === "preview" || value === "dev") return value;
|
|
347
|
+
throw new Error(`Invalid expected runtime channel: ${String(value)}`);
|
|
348
|
+
}
|
|
349
|
+
function resolveManifestPath(manifestPath, value) {
|
|
350
|
+
return resolve2(dirname2(manifestPath), value);
|
|
351
|
+
}
|
|
352
|
+
function validateServiceOrigin(value) {
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = new URL(value);
|
|
356
|
+
} catch {
|
|
357
|
+
throw new Error(`Invalid profile service_origin: ${value}`);
|
|
358
|
+
}
|
|
359
|
+
if (parsed.protocol !== "http:") throw new Error("Profile service_origin must use http for the local Lineage service");
|
|
360
|
+
if (!parsed.port) throw new Error("Profile service_origin must include an explicit port");
|
|
361
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
362
|
+
throw new Error("Profile service_origin must contain only scheme, host, and port");
|
|
363
|
+
}
|
|
364
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
365
|
+
const isLocal = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
366
|
+
if (!isLocal) throw new Error("Profile service_origin must use a loopback or localhost host");
|
|
367
|
+
return parsed.origin;
|
|
368
|
+
}
|
|
369
|
+
function resolveLineageProfile(selector) {
|
|
370
|
+
const { manifestPath, namedProfileId } = profileManifestPath(selector);
|
|
371
|
+
if (!existsSync3(manifestPath)) throw new Error(`Profile manifest does not exist: ${manifestPath}`);
|
|
372
|
+
let raw;
|
|
373
|
+
try {
|
|
374
|
+
raw = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
377
|
+
}
|
|
378
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
|
|
379
|
+
const record = raw;
|
|
380
|
+
const schemaVersion = requiredString(record, "schema_version");
|
|
381
|
+
if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
|
|
382
|
+
const profileId = requiredString(record, "profile_id");
|
|
383
|
+
if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
|
|
384
|
+
if (namedProfileId && namedProfileId !== profileId) {
|
|
385
|
+
throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
|
|
386
|
+
}
|
|
387
|
+
const expectedRaw = record.expected_runtime;
|
|
388
|
+
if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
|
|
389
|
+
throw new Error("Profile expected_runtime must be an object");
|
|
390
|
+
}
|
|
391
|
+
const expected = expectedRaw;
|
|
392
|
+
const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
|
|
393
|
+
const expectedVersion = expected ? optionalString(expected, "version") : void 0;
|
|
394
|
+
const migrationsRaw = record.required_schema_migrations;
|
|
395
|
+
if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
396
|
+
throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
|
|
397
|
+
}
|
|
398
|
+
const environment = validateEnvironment(requiredString(record, "environment"));
|
|
399
|
+
const expectedChannel = validateChannel(expected?.channel);
|
|
400
|
+
if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
|
|
401
|
+
throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
|
|
402
|
+
}
|
|
403
|
+
const manifest = {
|
|
404
|
+
asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
|
|
405
|
+
database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
|
|
406
|
+
environment,
|
|
407
|
+
...expected ? {
|
|
408
|
+
expected_runtime: {
|
|
409
|
+
...expectedChannel ? { channel: expectedChannel } : {},
|
|
410
|
+
...expectedGitSha ? { git_sha: expectedGitSha } : {},
|
|
411
|
+
...expectedVersion ? { version: expectedVersion } : {}
|
|
412
|
+
}
|
|
413
|
+
} : {},
|
|
414
|
+
profile_id: profileId,
|
|
415
|
+
...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
|
|
416
|
+
schema_version: lineageProfileSchemaVersion,
|
|
417
|
+
service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
|
|
418
|
+
};
|
|
419
|
+
return { ...manifest, manifest_path: manifestPath };
|
|
420
|
+
}
|
|
421
|
+
function assertProfileChannel(profile, channel) {
|
|
422
|
+
const requiredChannel = environmentChannel(profile.environment);
|
|
423
|
+
if (channel === requiredChannel) return;
|
|
424
|
+
if (profile.environment === "production") {
|
|
425
|
+
throw new Error(`Refusing to open production profile ${profile.profile_id} from ${channel} code; use the stable channel`);
|
|
426
|
+
}
|
|
427
|
+
throw new Error(`Profile ${profile.profile_id} requires the ${requiredChannel} channel, not ${channel}`);
|
|
428
|
+
}
|
|
429
|
+
function tableExists(database, table) {
|
|
430
|
+
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
431
|
+
}
|
|
432
|
+
function gitShasMatch(expected, actual) {
|
|
433
|
+
if (!actual) return false;
|
|
434
|
+
const normalizedExpected = expected.toLowerCase();
|
|
435
|
+
const normalizedActual = actual.toLowerCase();
|
|
436
|
+
const shorterLength = Math.min(normalizedExpected.length, normalizedActual.length);
|
|
437
|
+
return shorterLength >= 12 && normalizedExpected.slice(0, shorterLength) === normalizedActual.slice(0, shorterLength);
|
|
438
|
+
}
|
|
439
|
+
function inspectDatabase(profile) {
|
|
440
|
+
const result = {
|
|
441
|
+
exists: existsSync3(profile.database_path),
|
|
442
|
+
migration_keys: [],
|
|
443
|
+
path: profile.database_path
|
|
444
|
+
};
|
|
445
|
+
if (!result.exists) return result;
|
|
446
|
+
try {
|
|
447
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
448
|
+
const database = new DatabaseSync(profile.database_path, { readOnly: true });
|
|
449
|
+
try {
|
|
450
|
+
if (tableExists(database, "lineage_profile_identity")) {
|
|
451
|
+
const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
|
|
452
|
+
if (rows.length === 1) {
|
|
453
|
+
const identity = {
|
|
454
|
+
bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
|
|
455
|
+
environment: String(rows[0].environment),
|
|
456
|
+
profile_id: String(rows[0].profile_id)
|
|
457
|
+
};
|
|
458
|
+
result.identity = identity;
|
|
459
|
+
} else {
|
|
460
|
+
result.error = `Expected exactly one lineage_profile_identity row, found ${rows.length}`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (tableExists(database, "lineage_schema_migrations")) {
|
|
464
|
+
result.migration_keys = database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
465
|
+
}
|
|
466
|
+
} finally {
|
|
467
|
+
database.close();
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
function doctorLineageProfile(selector, runtime) {
|
|
475
|
+
const checks = [];
|
|
476
|
+
const result = {
|
|
477
|
+
checks,
|
|
478
|
+
ok: false,
|
|
479
|
+
runtime: { channel: runtime.channel, git_sha: runtime.gitSha, version: runtime.version },
|
|
480
|
+
schema_version: lineageProfileDoctorSchemaVersion
|
|
481
|
+
};
|
|
482
|
+
let profile;
|
|
483
|
+
try {
|
|
484
|
+
profile = resolveLineageProfile(selector);
|
|
485
|
+
result.profile = profile;
|
|
486
|
+
result.manifest_path = profile.manifest_path;
|
|
487
|
+
checks.push({ id: "manifest", message: `Loaded ${profile.profile_id}`, status: "pass" });
|
|
488
|
+
} catch (error) {
|
|
489
|
+
checks.push({ id: "manifest", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
assertProfileChannel(profile, runtime.channel);
|
|
494
|
+
checks.push({ id: "runtime_channel", message: `${runtime.channel} code matches ${profile.environment}`, status: "pass" });
|
|
495
|
+
} catch (error) {
|
|
496
|
+
checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
497
|
+
}
|
|
498
|
+
if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
|
|
499
|
+
checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
|
|
500
|
+
} else {
|
|
501
|
+
checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
|
|
502
|
+
}
|
|
503
|
+
if (profile.expected_runtime?.git_sha && !gitShasMatch(profile.expected_runtime.git_sha, runtime.gitSha)) {
|
|
504
|
+
checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
|
|
505
|
+
} else if (profile.expected_runtime?.git_sha) {
|
|
506
|
+
checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
|
|
507
|
+
}
|
|
508
|
+
const assetExists = existsSync3(profile.asset_root);
|
|
509
|
+
const assetIsDirectory = assetExists && statSync3(profile.asset_root).isDirectory();
|
|
510
|
+
result.asset_root = { exists: assetExists, is_directory: assetIsDirectory, path: profile.asset_root };
|
|
511
|
+
checks.push({
|
|
512
|
+
id: "asset_root",
|
|
513
|
+
message: assetIsDirectory ? `Asset root exists: ${profile.asset_root}` : `Asset root is missing or not a directory: ${profile.asset_root}`,
|
|
514
|
+
status: assetIsDirectory ? "pass" : "fail"
|
|
515
|
+
});
|
|
516
|
+
const database = inspectDatabase(profile);
|
|
517
|
+
result.database = database;
|
|
518
|
+
checks.push({
|
|
519
|
+
id: "database_exists",
|
|
520
|
+
message: database.exists ? `Database exists: ${profile.database_path}` : `Database does not exist: ${profile.database_path}`,
|
|
521
|
+
status: database.exists ? "pass" : "fail"
|
|
522
|
+
});
|
|
523
|
+
if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
|
|
524
|
+
if (!database.identity) {
|
|
525
|
+
checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
|
|
526
|
+
} else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment) {
|
|
527
|
+
checks.push({
|
|
528
|
+
id: "database_identity",
|
|
529
|
+
message: `Database identity ${database.identity.profile_id}/${database.identity.environment} does not match ${profile.profile_id}/${profile.environment}`,
|
|
530
|
+
status: "fail"
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
checks.push({ id: "database_identity", message: `Database is bound to ${profile.profile_id}`, status: "pass" });
|
|
534
|
+
}
|
|
535
|
+
const missingMigrations = (profile.required_schema_migrations || []).filter((key) => !database.migration_keys.includes(key));
|
|
536
|
+
checks.push({
|
|
537
|
+
id: "database_schema",
|
|
538
|
+
message: missingMigrations.length ? `Missing required schema migrations: ${missingMigrations.join(", ")}` : `${database.migration_keys.length} schema migration marker(s) available`,
|
|
539
|
+
status: missingMigrations.length ? "fail" : "pass"
|
|
540
|
+
});
|
|
541
|
+
result.ok = checks.every((check) => check.status !== "fail");
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
function runtimeProfileIdentity(channel) {
|
|
545
|
+
const selector = process.env.LINEAGE_PROFILE;
|
|
546
|
+
const id = process.env.LINEAGE_PROFILE_ID;
|
|
547
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
548
|
+
if (selector && id && environment) {
|
|
549
|
+
return {
|
|
550
|
+
bound: true,
|
|
551
|
+
environment,
|
|
552
|
+
id,
|
|
553
|
+
manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
|
|
554
|
+
service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
bound: false,
|
|
559
|
+
environment: channelEnvironment(channel),
|
|
560
|
+
id: "legacy-unbound",
|
|
561
|
+
warning: id || environment || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function assertRuntimeProfileSafety(channel) {
|
|
565
|
+
const hasDerivedIdentity = Boolean(
|
|
566
|
+
process.env.LINEAGE_PROFILE_ID || process.env.LINEAGE_PROFILE_ENVIRONMENT || process.env.LINEAGE_PROFILE_MANIFEST || process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
567
|
+
);
|
|
568
|
+
if (hasDerivedIdentity && !process.env.LINEAGE_PROFILE) {
|
|
569
|
+
throw new Error("Derived Lineage profile identity requires LINEAGE_PROFILE; start through the Lineage CLI with --profile");
|
|
570
|
+
}
|
|
571
|
+
const profile = runtimeProfileIdentity(channel);
|
|
572
|
+
if (profile.bound && profile.environment === "production" && channel !== "stable") {
|
|
573
|
+
throw new Error(`Refusing to open production profile ${profile.id} from ${channel} code; use the stable channel`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function assertUnselectedDatabaseIsUnbound(runtime) {
|
|
577
|
+
if (runtime.database.exists && runtime.database.error) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`Database ${runtime.database.path} identity could not be verified: ${runtime.database.error}; refusing unselected access`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (runtime.schema.profile_identity_rows !== void 0 && runtime.schema.profile_identity_rows !== 1) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`Database ${runtime.database.path} has invalid Lineage profile identity row count ${runtime.schema.profile_identity_rows}; refusing unselected access`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
if (!runtime.schema.profile_id) return;
|
|
588
|
+
const environment = runtime.schema.profile_environment || "unknown";
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
function assertResolvedRuntimeProfileEnvironment(profile) {
|
|
594
|
+
const serviceUrl = new URL(profile.service_origin);
|
|
595
|
+
const serviceHost = serviceUrl.hostname.startsWith("[") && serviceUrl.hostname.endsWith("]") ? serviceUrl.hostname.slice(1, -1) : serviceUrl.hostname;
|
|
596
|
+
const expected = /* @__PURE__ */ new Map([
|
|
597
|
+
["LINEAGE_PROFILE_ID", profile.profile_id],
|
|
598
|
+
["LINEAGE_PROFILE_ENVIRONMENT", profile.environment],
|
|
599
|
+
["LINEAGE_PROFILE_MANIFEST", profile.manifest_path],
|
|
600
|
+
["LINEAGE_PROFILE_SERVICE_ORIGIN", profile.service_origin],
|
|
601
|
+
["LINEAGE_DB", profile.database_path],
|
|
602
|
+
["LINEAGE_ASSET_ROOT", profile.asset_root],
|
|
603
|
+
["HOST", serviceHost],
|
|
604
|
+
["PORT", serviceUrl.port]
|
|
605
|
+
]);
|
|
606
|
+
const conflicts = [];
|
|
607
|
+
for (const [key, expectedValue] of expected) {
|
|
608
|
+
const actual = process.env[key];
|
|
609
|
+
const pathValue = key === "LINEAGE_PROFILE_MANIFEST" || key === "LINEAGE_DB" || key === "LINEAGE_ASSET_ROOT";
|
|
610
|
+
const matches = actual && (pathValue ? resolve2(actual) === resolve2(expectedValue) : actual === expectedValue);
|
|
611
|
+
if (!matches) conflicts.push(`${key}=${actual || "(missing)"}`);
|
|
612
|
+
}
|
|
613
|
+
if (conflicts.length > 0) {
|
|
614
|
+
throw new Error(`Resolved profile environment conflicts with ${profile.profile_id}: ${conflicts.join(", ")}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/server/profileWriterLease.ts
|
|
619
|
+
import { createHash as createHash2, randomUUID, timingSafeEqual } from "node:crypto";
|
|
620
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
621
|
+
import { basename as basename3, dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
|
|
622
|
+
var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
|
|
623
|
+
var ownerFileName = "owner.json";
|
|
624
|
+
var ProfileWriterLeaseConflictError = class extends Error {
|
|
625
|
+
constructor(message, owner) {
|
|
626
|
+
super(message);
|
|
627
|
+
this.owner = owner;
|
|
628
|
+
this.name = "ProfileWriterLeaseConflictError";
|
|
629
|
+
}
|
|
630
|
+
owner;
|
|
631
|
+
};
|
|
632
|
+
function canonicalDatabasePath(databasePath) {
|
|
633
|
+
const resolved = resolve3(databasePath);
|
|
634
|
+
const missingSegments = [];
|
|
635
|
+
let candidate = resolved;
|
|
636
|
+
while (!existsSync4(candidate)) {
|
|
637
|
+
const parent = dirname3(candidate);
|
|
638
|
+
if (parent === candidate) return resolved;
|
|
639
|
+
missingSegments.unshift(basename3(candidate));
|
|
640
|
+
candidate = parent;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
return join4(realpathSync(candidate), ...missingSegments);
|
|
644
|
+
} catch {
|
|
645
|
+
return resolved;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function profileWriterLockPath(profile) {
|
|
649
|
+
return `${canonicalDatabasePath(profile.database_path)}.writer.lock`;
|
|
650
|
+
}
|
|
651
|
+
function ownerPath(lockPath) {
|
|
652
|
+
return join4(lockPath, ownerFileName);
|
|
653
|
+
}
|
|
654
|
+
function errorCode(error) {
|
|
655
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
656
|
+
}
|
|
657
|
+
function readOwner(lockPath) {
|
|
658
|
+
const stat = lstatSync(lockPath);
|
|
659
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
660
|
+
throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
|
|
661
|
+
}
|
|
662
|
+
let raw;
|
|
663
|
+
try {
|
|
664
|
+
raw = JSON.parse(readFileSync3(ownerPath(lockPath), "utf8"));
|
|
665
|
+
} catch (error) {
|
|
666
|
+
throw new Error(`Writer lease metadata is unreadable at ${lockPath}; refusing automatic recovery`, { cause: error });
|
|
667
|
+
}
|
|
668
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
669
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
670
|
+
}
|
|
671
|
+
const owner = raw;
|
|
672
|
+
if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
|
|
673
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
674
|
+
}
|
|
675
|
+
return owner;
|
|
676
|
+
}
|
|
677
|
+
function processIsAlive(pid) {
|
|
678
|
+
try {
|
|
679
|
+
process.kill(pid, 0);
|
|
680
|
+
return true;
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (errorCode(error) === "ESRCH") return false;
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function createLeaseDirectory(lockPath, owner) {
|
|
687
|
+
mkdirSync3(lockPath, { mode: 448 });
|
|
688
|
+
try {
|
|
689
|
+
writeFileSync(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
|
|
690
|
+
`, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
691
|
+
} catch (error) {
|
|
692
|
+
rmSync(lockPath, { force: true, recursive: true });
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function reclaimDeadOwner(lockPath, owner) {
|
|
697
|
+
if (processIsAlive(owner.pid)) {
|
|
698
|
+
const { token: _token, ...publicOwner } = owner;
|
|
699
|
+
throw new ProfileWriterLeaseConflictError(
|
|
700
|
+
`Lineage profile ${owner.profile_id} already has an active ${owner.role} writer (pid ${owner.pid}); use that managed service or stop it before starting another writer`,
|
|
701
|
+
publicOwner
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const tokenFence = createHash2("sha256").update(owner.token).digest("hex").slice(0, 24);
|
|
705
|
+
const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
|
|
706
|
+
try {
|
|
707
|
+
renameSync(lockPath, tombstone);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "EEXIST" || errorCode(error) === "ENOTEMPTY") return;
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
714
|
+
assertProfileChannel(profile, channel);
|
|
715
|
+
const lockPath = profileWriterLockPath(profile);
|
|
716
|
+
const owner = {
|
|
717
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
718
|
+
environment: profile.environment,
|
|
719
|
+
pid: process.pid,
|
|
720
|
+
profile_id: profile.profile_id,
|
|
721
|
+
role,
|
|
722
|
+
schema_version: writerLeaseSchemaVersion,
|
|
723
|
+
...role === "service" ? { service_origin: profile.service_origin } : {},
|
|
724
|
+
token: randomUUID()
|
|
725
|
+
};
|
|
726
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
727
|
+
try {
|
|
728
|
+
createLeaseDirectory(lockPath, owner);
|
|
729
|
+
process.env.LINEAGE_WRITER_LEASE_TOKEN = owner.token;
|
|
730
|
+
process.env.LINEAGE_WRITER_LOCK_PATH = lockPath;
|
|
731
|
+
const { token: _token, ...publicOwner } = owner;
|
|
732
|
+
let released = false;
|
|
733
|
+
return {
|
|
734
|
+
authenticate: (candidate) => {
|
|
735
|
+
if (!candidate) return false;
|
|
736
|
+
const expected = Buffer.from(owner.token);
|
|
737
|
+
const actual = Buffer.from(candidate);
|
|
738
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
739
|
+
},
|
|
740
|
+
lock_path: lockPath,
|
|
741
|
+
owner: publicOwner,
|
|
742
|
+
release: () => {
|
|
743
|
+
if (released) return;
|
|
744
|
+
released = true;
|
|
745
|
+
try {
|
|
746
|
+
const current = readOwner(lockPath);
|
|
747
|
+
if (current.token === owner.token && current.pid === owner.pid) rmSync(lockPath, { force: true, recursive: true });
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
751
|
+
if (process.env.LINEAGE_WRITER_LOCK_PATH === lockPath) delete process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
756
|
+
let existing;
|
|
757
|
+
try {
|
|
758
|
+
existing = readOwner(lockPath);
|
|
759
|
+
} catch (readError) {
|
|
760
|
+
if (errorCode(readError) === "ENOENT") continue;
|
|
761
|
+
throw readError;
|
|
762
|
+
}
|
|
763
|
+
if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment) {
|
|
764
|
+
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
|
|
765
|
+
}
|
|
766
|
+
reclaimDeadOwner(lockPath, existing);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
|
|
770
|
+
}
|
|
771
|
+
function assertProfileWriterLeaseHeld() {
|
|
772
|
+
if (!process.env.LINEAGE_PROFILE) return;
|
|
773
|
+
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
774
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
775
|
+
const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
|
|
776
|
+
const databasePath = process.env.LINEAGE_DB;
|
|
777
|
+
const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
778
|
+
const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
779
|
+
if (!profileId || !environment || !manifestPath || !databasePath || !lockPath || !token) {
|
|
780
|
+
throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
|
|
781
|
+
}
|
|
782
|
+
const expectedLockPath = `${canonicalDatabasePath(databasePath)}.writer.lock`;
|
|
783
|
+
if (resolve3(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile database");
|
|
784
|
+
const owner = readOwner(lockPath);
|
|
785
|
+
if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.environment !== environment) {
|
|
786
|
+
throw new Error("Current process does not own the selected Lineage profile writer lease");
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/server/assetLineageDb.ts
|
|
791
|
+
var require3 = createRequire2(import.meta.url);
|
|
289
792
|
function nowIso() {
|
|
290
793
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
291
794
|
}
|
|
292
795
|
function lineageDbPath() {
|
|
293
|
-
return process.env.LINEAGE_DB ||
|
|
796
|
+
return process.env.LINEAGE_DB || join5(packageRoot, ".lineage", "asset-lineage.sqlite");
|
|
797
|
+
}
|
|
798
|
+
function assertOpenedProfileIdentity(database, databasePath) {
|
|
799
|
+
const selector = process.env.LINEAGE_PROFILE;
|
|
800
|
+
if (!selector) return;
|
|
801
|
+
const profile = resolveLineageProfile(selector);
|
|
802
|
+
if (resolve4(databasePath) !== profile.database_path) {
|
|
803
|
+
throw new Error(`Profile ${profile.profile_id} requires database ${profile.database_path}, not ${databasePath}`);
|
|
804
|
+
}
|
|
805
|
+
const table = database.prepare("select 1 from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
|
|
806
|
+
if (!table) throw new Error(`Profile database ${databasePath} is not bound to a Lineage profile identity`);
|
|
807
|
+
const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
|
|
808
|
+
if (rows.length !== 1) {
|
|
809
|
+
throw new Error(`Profile database ${databasePath} has invalid Lineage profile identity row count ${rows.length}`);
|
|
810
|
+
}
|
|
811
|
+
if (rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`Profile database ${databasePath} is bound to ${rows[0].profile_id}/${rows[0].environment}, not ${profile.profile_id}/${profile.environment}`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
294
816
|
}
|
|
295
817
|
function lineageDb() {
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
818
|
+
const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
|
|
819
|
+
const databasePath = lineageDbPath();
|
|
820
|
+
if (!readOnly) assertProfileWriterLeaseHeld();
|
|
821
|
+
if (process.env.LINEAGE_PROFILE && !existsSync5(databasePath)) {
|
|
822
|
+
throw new Error(`Profile database does not exist: ${databasePath}; bind the profile before opening it`);
|
|
823
|
+
}
|
|
824
|
+
if (!readOnly) mkdirSync4(join5(databasePath, ".."), { recursive: true });
|
|
825
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
826
|
+
const database = readOnly ? new DatabaseSync(databasePath, { readOnly: true }) : new DatabaseSync(databasePath);
|
|
827
|
+
try {
|
|
828
|
+
assertOpenedProfileIdentity(database, databasePath);
|
|
829
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
830
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
831
|
+
if (readOnly) return database;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
database.close();
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
301
837
|
database.exec(`
|
|
302
838
|
create table if not exists projects (
|
|
303
839
|
id text primary key,
|
|
@@ -1131,28 +1667,33 @@ function ledgerWorkflowStates(database, project, records, sources) {
|
|
|
1131
1667
|
|
|
1132
1668
|
// src/server/assetCore.ts
|
|
1133
1669
|
function isPackageRoot(path) {
|
|
1134
|
-
const packageJson =
|
|
1135
|
-
if (!
|
|
1670
|
+
const packageJson = join6(path, "package.json");
|
|
1671
|
+
if (!existsSync6(packageJson)) return false;
|
|
1136
1672
|
try {
|
|
1137
|
-
const packageInfo2 = JSON.parse(
|
|
1673
|
+
const packageInfo2 = JSON.parse(readFileSync4(packageJson, "utf8"));
|
|
1138
1674
|
return packageInfo2.name === "@mean-weasel/lineage";
|
|
1139
1675
|
} catch {
|
|
1140
1676
|
return false;
|
|
1141
1677
|
}
|
|
1142
1678
|
}
|
|
1143
|
-
function
|
|
1144
|
-
const moduleDir =
|
|
1679
|
+
function resolvePackageRoot() {
|
|
1680
|
+
const moduleDir = dirname4(fileURLToPath(import.meta.url));
|
|
1145
1681
|
const candidates = [
|
|
1146
1682
|
process.env.LINEAGE_REPO_ROOT,
|
|
1147
|
-
|
|
1148
|
-
|
|
1683
|
+
resolve5(moduleDir, ".."),
|
|
1684
|
+
resolve5(moduleDir, "../.."),
|
|
1149
1685
|
process.cwd()
|
|
1150
1686
|
].filter((candidate) => Boolean(candidate));
|
|
1151
1687
|
const root = candidates.find(isPackageRoot);
|
|
1152
1688
|
if (!root) throw new Error("Unable to locate Lineage package root");
|
|
1153
1689
|
return root;
|
|
1154
1690
|
}
|
|
1155
|
-
var
|
|
1691
|
+
var packageRoot = resolvePackageRoot();
|
|
1692
|
+
var repoRoot = resolve5(process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
1693
|
+
function setLineageAssetRoot(path) {
|
|
1694
|
+
repoRoot = resolve5(path || packageRoot);
|
|
1695
|
+
return repoRoot;
|
|
1696
|
+
}
|
|
1156
1697
|
var defaultProject = "demo-project";
|
|
1157
1698
|
var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
|
|
1158
1699
|
var publicFallbackBucket = "lineage-demo-assets";
|
|
@@ -1178,16 +1719,16 @@ function cleanProject(project = defaultProject) {
|
|
|
1178
1719
|
return project;
|
|
1179
1720
|
}
|
|
1180
1721
|
function catalogPath(project = defaultProject) {
|
|
1181
|
-
return
|
|
1722
|
+
return join6(repoRoot, cleanProject(project), "assets", "catalog.json");
|
|
1182
1723
|
}
|
|
1183
1724
|
function fixtureCatalogPath(project = defaultProject) {
|
|
1184
|
-
return
|
|
1725
|
+
return join6(packageRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
|
|
1185
1726
|
}
|
|
1186
1727
|
function resolvedCatalogPath(project = defaultProject) {
|
|
1187
1728
|
const path = catalogPath(project);
|
|
1188
|
-
if (
|
|
1729
|
+
if (existsSync6(path)) return path;
|
|
1189
1730
|
const clean = cleanProject(project);
|
|
1190
|
-
if (clean === defaultProject &&
|
|
1731
|
+
if (clean === defaultProject && existsSync6(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
|
|
1191
1732
|
return path;
|
|
1192
1733
|
}
|
|
1193
1734
|
function normalizeCatalog(catalog, fallbackProject = defaultProject) {
|
|
@@ -1304,7 +1845,7 @@ function defaultFallbackCatalog() {
|
|
|
1304
1845
|
}, defaultProject);
|
|
1305
1846
|
}
|
|
1306
1847
|
function isDefaultFallbackCatalog(catalog) {
|
|
1307
|
-
return catalog.project === defaultProject && !
|
|
1848
|
+
return catalog.project === defaultProject && !existsSync6(catalogPath(defaultProject));
|
|
1308
1849
|
}
|
|
1309
1850
|
function fallbackPreviewDataUrl(asset) {
|
|
1310
1851
|
const label = `${asset.title}
|
|
@@ -1317,9 +1858,9 @@ function escapeSvgText2(value) {
|
|
|
1317
1858
|
}
|
|
1318
1859
|
function loadCatalog(project = defaultProject) {
|
|
1319
1860
|
const path = catalogPath(project);
|
|
1320
|
-
if (
|
|
1861
|
+
if (existsSync6(path)) {
|
|
1321
1862
|
try {
|
|
1322
|
-
return normalizeCatalog(JSON.parse(
|
|
1863
|
+
return normalizeCatalog(JSON.parse(readFileSync4(path, "utf8")), project);
|
|
1323
1864
|
} catch (error) {
|
|
1324
1865
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
1325
1866
|
throw new LineageAssetError(`Missing catalog: ${path}`, 404);
|
|
@@ -1330,8 +1871,8 @@ function loadCatalog(project = defaultProject) {
|
|
|
1330
1871
|
const clean = cleanProject(project);
|
|
1331
1872
|
if (clean === defaultProject) {
|
|
1332
1873
|
const fixturePath = fixtureCatalogPath(clean);
|
|
1333
|
-
if (
|
|
1334
|
-
return normalizeCatalog(JSON.parse(
|
|
1874
|
+
if (existsSync6(fixturePath)) {
|
|
1875
|
+
return normalizeCatalog(JSON.parse(readFileSync4(fixturePath, "utf8")), project);
|
|
1335
1876
|
}
|
|
1336
1877
|
return defaultFallbackCatalog();
|
|
1337
1878
|
}
|
|
@@ -1339,8 +1880,8 @@ function loadCatalog(project = defaultProject) {
|
|
|
1339
1880
|
}
|
|
1340
1881
|
function saveCatalog(project, catalog) {
|
|
1341
1882
|
const normalized = normalizeCatalog(catalog, project);
|
|
1342
|
-
|
|
1343
|
-
|
|
1883
|
+
mkdirSync5(dirname4(catalogPath(project)), { recursive: true });
|
|
1884
|
+
writeFileSync2(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
|
|
1344
1885
|
`);
|
|
1345
1886
|
return normalized;
|
|
1346
1887
|
}
|
|
@@ -1365,7 +1906,7 @@ function runAws(args) {
|
|
|
1365
1906
|
return run("aws", args);
|
|
1366
1907
|
}
|
|
1367
1908
|
function listProjects() {
|
|
1368
|
-
const projects = readdirSync2(repoRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && projectNamePattern.test(entry.name) &&
|
|
1909
|
+
const projects = readdirSync2(repoRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && projectNamePattern.test(entry.name) && existsSync6(catalogPath(entry.name))).flatMap((entry) => {
|
|
1369
1910
|
try {
|
|
1370
1911
|
const catalog = loadCatalog(entry.name);
|
|
1371
1912
|
return [{ project: catalog.project, product: catalog.product, catalogPath: resolvedCatalogPath(entry.name), default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length }];
|
|
@@ -1524,15 +2065,15 @@ function deleteObjectGuarded(project, assetId, confirmation) {
|
|
|
1524
2065
|
return storageAdapter.deleteObjectGuarded(project, assetId, confirmation);
|
|
1525
2066
|
}
|
|
1526
2067
|
function ensureUploadDir() {
|
|
1527
|
-
const dir =
|
|
1528
|
-
|
|
2068
|
+
const dir = join6(repoRoot, ".asset-scratch", "studio-uploads");
|
|
2069
|
+
mkdirSync5(dir, { recursive: true });
|
|
1529
2070
|
return dir;
|
|
1530
2071
|
}
|
|
1531
2072
|
function cleanupUploadedTemp(file) {
|
|
1532
2073
|
if (!file) return;
|
|
1533
2074
|
const uploadRoot = ensureUploadDir();
|
|
1534
|
-
const resolved =
|
|
1535
|
-
if (resolved.startsWith(`${uploadRoot}/`) &&
|
|
2075
|
+
const resolved = resolve5(file);
|
|
2076
|
+
if (resolved.startsWith(`${uploadRoot}/`) && existsSync6(resolved)) {
|
|
1536
2077
|
unlinkSync(resolved);
|
|
1537
2078
|
}
|
|
1538
2079
|
}
|
|
@@ -1566,7 +2107,7 @@ function lookupAssets(project, assetIds) {
|
|
|
1566
2107
|
}
|
|
1567
2108
|
|
|
1568
2109
|
// src/server/assetLineage.ts
|
|
1569
|
-
import { join as
|
|
2110
|
+
import { join as join7 } from "node:path";
|
|
1570
2111
|
|
|
1571
2112
|
// src/server/assetLineageSelection.ts
|
|
1572
2113
|
var LINEAGE_NEXT_VARIATION_LIMIT = 3;
|
|
@@ -1589,7 +2130,7 @@ function normalizeSelectionInput(fields) {
|
|
|
1589
2130
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1590
2131
|
|
|
1591
2132
|
// src/server/agentClaims.ts
|
|
1592
|
-
import { createHash as
|
|
2133
|
+
import { createHash as createHash3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
1593
2134
|
var defaultTtlSeconds = 20 * 60;
|
|
1594
2135
|
var idleAfterSeconds = 5 * 60;
|
|
1595
2136
|
var staleAfterSeconds = 15 * 60;
|
|
@@ -1625,12 +2166,12 @@ function randomId(prefix) {
|
|
|
1625
2166
|
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1626
2167
|
}
|
|
1627
2168
|
function tokenHash(token) {
|
|
1628
|
-
return
|
|
2169
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
1629
2170
|
}
|
|
1630
2171
|
function safeEqual(a, b) {
|
|
1631
2172
|
const left = Buffer.from(a);
|
|
1632
2173
|
const right = Buffer.from(b);
|
|
1633
|
-
return left.length === right.length &&
|
|
2174
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
1634
2175
|
}
|
|
1635
2176
|
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1636
2177
|
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
@@ -1657,6 +2198,12 @@ function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1657
2198
|
}
|
|
1658
2199
|
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1659
2200
|
const heartbeatAt = String(row.heartbeat_at);
|
|
2201
|
+
const persistedStatus = String(row.status);
|
|
2202
|
+
const state = derivedState({
|
|
2203
|
+
expires_at: String(row.expires_at),
|
|
2204
|
+
heartbeat_at: heartbeatAt,
|
|
2205
|
+
status: persistedStatus
|
|
2206
|
+
}, now);
|
|
1660
2207
|
return {
|
|
1661
2208
|
id: String(row.id),
|
|
1662
2209
|
project: String(row.project_id),
|
|
@@ -1668,7 +2215,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1668
2215
|
agent_name: String(row.agent_name),
|
|
1669
2216
|
agent_kind: String(row.agent_kind),
|
|
1670
2217
|
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1671
|
-
status:
|
|
2218
|
+
status: persistedStatus === "active" && state === "expired" ? "expired" : persistedStatus,
|
|
1672
2219
|
created_at: String(row.created_at),
|
|
1673
2220
|
heartbeat_at: heartbeatAt,
|
|
1674
2221
|
expires_at: String(row.expires_at),
|
|
@@ -1678,11 +2225,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1678
2225
|
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1679
2226
|
metadata: parseMetadata(row.metadata_json),
|
|
1680
2227
|
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1681
|
-
derived_state:
|
|
1682
|
-
expires_at: String(row.expires_at),
|
|
1683
|
-
heartbeat_at: heartbeatAt,
|
|
1684
|
-
status: String(row.status)
|
|
1685
|
-
}, now)
|
|
2228
|
+
derived_state: state
|
|
1686
2229
|
};
|
|
1687
2230
|
}
|
|
1688
2231
|
function eventToRow(row) {
|
|
@@ -1824,7 +2367,7 @@ function createAgentClaim(fields) {
|
|
|
1824
2367
|
function listAgentClaims(project) {
|
|
1825
2368
|
const database = lineageDb();
|
|
1826
2369
|
try {
|
|
1827
|
-
expireActiveClaims(database);
|
|
2370
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1828
2371
|
const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
|
|
1829
2372
|
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1830
2373
|
} finally {
|
|
@@ -1834,7 +2377,7 @@ function listAgentClaims(project) {
|
|
|
1834
2377
|
function inspectAgentClaim(claimId, project) {
|
|
1835
2378
|
const database = lineageDb();
|
|
1836
2379
|
try {
|
|
1837
|
-
expireActiveClaims(database);
|
|
2380
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1838
2381
|
const claim = findClaimById(database, claimId, project);
|
|
1839
2382
|
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1840
2383
|
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
@@ -2461,6 +3004,44 @@ function cancelLineageIterateTasksForAssets(project, fields) {
|
|
|
2461
3004
|
taskId: task.id
|
|
2462
3005
|
}));
|
|
2463
3006
|
}
|
|
3007
|
+
function resolveLineageTask(project, fields) {
|
|
3008
|
+
const normalizedProject = normalizeProject(project);
|
|
3009
|
+
const actor = normalizeActor(fields.actor, "Resolve actor");
|
|
3010
|
+
const database = lineageDb();
|
|
3011
|
+
try {
|
|
3012
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
3013
|
+
if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
|
|
3014
|
+
if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
3015
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
3016
|
+
throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
|
|
3017
|
+
}
|
|
3018
|
+
const timestamp = nowIso();
|
|
3019
|
+
const resolvedTask = {
|
|
3020
|
+
...task,
|
|
3021
|
+
status: "resolved",
|
|
3022
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
3023
|
+
resolved_at: timestamp,
|
|
3024
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId,
|
|
3025
|
+
updated_at: timestamp
|
|
3026
|
+
};
|
|
3027
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
|
|
3028
|
+
transaction(database, () => {
|
|
3029
|
+
const result = database.prepare(`
|
|
3030
|
+
update lineage_tasks
|
|
3031
|
+
set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
|
|
3032
|
+
where id = ? and status = ?
|
|
3033
|
+
`).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
|
|
3034
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
|
|
3035
|
+
recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
|
|
3036
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
3037
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId
|
|
3038
|
+
});
|
|
3039
|
+
});
|
|
3040
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
3041
|
+
} finally {
|
|
3042
|
+
database.close();
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
2464
3045
|
|
|
2465
3046
|
// src/server/assetLineageWorkspaces.ts
|
|
2466
3047
|
var actors = /* @__PURE__ */ new Set(["human", "agent", "system"]);
|
|
@@ -2535,9 +3116,18 @@ function knownRoots(database, project) {
|
|
|
2535
3116
|
and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
|
|
2536
3117
|
group by parent_asset_id
|
|
2537
3118
|
`).all(project, project, project, project);
|
|
2538
|
-
|
|
3119
|
+
const roots = /* @__PURE__ */ new Map();
|
|
3120
|
+
for (const row of rows) {
|
|
3121
|
+
const existing = roots.get(row.root_asset_id);
|
|
3122
|
+
roots.set(row.root_asset_id, {
|
|
3123
|
+
root_asset_id: row.root_asset_id,
|
|
3124
|
+
selected_at: row.selected_at || existing?.selected_at
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
return [...roots.values()];
|
|
2539
3128
|
}
|
|
2540
3129
|
function seedLegacyWorkspaces(database, project) {
|
|
3130
|
+
if (process.env.LINEAGE_DB_ACCESS === "read-only") return;
|
|
2541
3131
|
ensureProject3(database, project);
|
|
2542
3132
|
const timestamp = nowIso();
|
|
2543
3133
|
const statement = database.prepare(`
|
|
@@ -2561,6 +3151,34 @@ function seedLegacyWorkspaces(database, project) {
|
|
|
2561
3151
|
);
|
|
2562
3152
|
}
|
|
2563
3153
|
}
|
|
3154
|
+
function inferredLegacyWorkspaces(database, project) {
|
|
3155
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") return [];
|
|
3156
|
+
const persistedRoots = new Set(
|
|
3157
|
+
database.prepare("select root_asset_id from lineage_workspaces where project_id = ?").all(project).map((row) => row.root_asset_id)
|
|
3158
|
+
);
|
|
3159
|
+
const asset = database.prepare("select id, title from assets where project_id = ? and id = ?");
|
|
3160
|
+
return knownRoots(database, project).flatMap((root) => {
|
|
3161
|
+
if (persistedRoots.has(root.root_asset_id)) return [];
|
|
3162
|
+
const row = asset.get(project, root.root_asset_id);
|
|
3163
|
+
if (!row) return [];
|
|
3164
|
+
const timestamp = root.selected_at || nowIso();
|
|
3165
|
+
return [{
|
|
3166
|
+
active_at: root.selected_at,
|
|
3167
|
+
created_at: timestamp,
|
|
3168
|
+
created_by: "system",
|
|
3169
|
+
id: lineageWorkspaceId(project, root.root_asset_id),
|
|
3170
|
+
project,
|
|
3171
|
+
root_asset_id: root.root_asset_id,
|
|
3172
|
+
status: "active",
|
|
3173
|
+
title: `${row.title} lineage`,
|
|
3174
|
+
updated_at: timestamp
|
|
3175
|
+
}];
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
function sortWorkspaces(workspaces) {
|
|
3179
|
+
const statusRank = { active: 0, paused: 1, archived: 2 };
|
|
3180
|
+
return workspaces.sort((left, right) => statusRank[left.status] - statusRank[right.status] || (right.active_at || "").localeCompare(left.active_at || "") || right.updated_at.localeCompare(left.updated_at) || left.title.localeCompare(right.title));
|
|
3181
|
+
}
|
|
2564
3182
|
function listRows(database, project) {
|
|
2565
3183
|
return database.prepare(`
|
|
2566
3184
|
select * from lineage_workspaces
|
|
@@ -2572,23 +3190,15 @@ function listRows(database, project) {
|
|
|
2572
3190
|
title
|
|
2573
3191
|
`).all(project).map(rowToWorkspace);
|
|
2574
3192
|
}
|
|
2575
|
-
function activeWorkspace(database, project) {
|
|
2576
|
-
const row = database.prepare(`
|
|
2577
|
-
select * from lineage_workspaces
|
|
2578
|
-
where project_id = ? and status != 'archived'
|
|
2579
|
-
order by active_at desc nulls last, updated_at desc
|
|
2580
|
-
limit 1
|
|
2581
|
-
`).get(project);
|
|
2582
|
-
return row ? rowToWorkspace(row) : null;
|
|
2583
|
-
}
|
|
2584
3193
|
function listLineageWorkspaces(project) {
|
|
2585
3194
|
const database = lineageDb();
|
|
2586
3195
|
try {
|
|
2587
3196
|
seedLegacyWorkspaces(database, project);
|
|
3197
|
+
const workspaces = sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]);
|
|
2588
3198
|
return {
|
|
2589
3199
|
project,
|
|
2590
|
-
active_workspace:
|
|
2591
|
-
workspaces
|
|
3200
|
+
active_workspace: workspaces.find((workspace) => workspace.status !== "archived") || null,
|
|
3201
|
+
workspaces,
|
|
2592
3202
|
fetchedAt: nowIso()
|
|
2593
3203
|
};
|
|
2594
3204
|
} finally {
|
|
@@ -2599,7 +3209,7 @@ function inspectLineageWorkspace(project, workspaceId) {
|
|
|
2599
3209
|
const database = lineageDb();
|
|
2600
3210
|
try {
|
|
2601
3211
|
seedLegacyWorkspaces(database, project);
|
|
2602
|
-
const workspace = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
|
|
3212
|
+
const workspace = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId) || inferredLegacyWorkspaces(database, project).find((item) => item.id === workspaceId || item.root_asset_id === workspaceId);
|
|
2603
3213
|
if (!workspace) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
|
|
2604
3214
|
return workspace;
|
|
2605
3215
|
} finally {
|
|
@@ -2731,7 +3341,7 @@ function activeLineageWorkspaceRoot(project) {
|
|
|
2731
3341
|
const database = lineageDb();
|
|
2732
3342
|
try {
|
|
2733
3343
|
seedLegacyWorkspaces(database, project);
|
|
2734
|
-
return
|
|
3344
|
+
return sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]).find((workspace) => workspace.status !== "archived")?.root_asset_id;
|
|
2735
3345
|
} finally {
|
|
2736
3346
|
database.close();
|
|
2737
3347
|
}
|
|
@@ -2793,7 +3403,7 @@ function upsertProject(database, project) {
|
|
|
2793
3403
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
2794
3404
|
values (?, ?, ?, ?, ?)
|
|
2795
3405
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
2796
|
-
`).run(project, project,
|
|
3406
|
+
`).run(project, project, join7(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
2797
3407
|
}
|
|
2798
3408
|
function upsertAsset(database, project, asset) {
|
|
2799
3409
|
const timestamp = nowIso();
|
|
@@ -3012,6 +3622,13 @@ function assertNodeInRoot(database, project, root, node) {
|
|
|
3012
3622
|
const nodeRoot = rootFor(database, project, node);
|
|
3013
3623
|
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
3014
3624
|
}
|
|
3625
|
+
function assertAttemptAssetNotVisibleLineageNode(database, project, root, assetId) {
|
|
3626
|
+
const visibleNodeIds = /* @__PURE__ */ new Set([
|
|
3627
|
+
root,
|
|
3628
|
+
...descendants(database, project, root).flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])
|
|
3629
|
+
]);
|
|
3630
|
+
if (visibleNodeIds.has(assetId)) throw new LineageError(`Attempt asset ${assetId} is already a visible lineage node in ${root}`, 400);
|
|
3631
|
+
}
|
|
3015
3632
|
function canPreviewLocally(mediaType, localPath) {
|
|
3016
3633
|
return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
|
|
3017
3634
|
}
|
|
@@ -3386,6 +4003,74 @@ function promoteLineageAttempt(project, fields) {
|
|
|
3386
4003
|
database.close();
|
|
3387
4004
|
}
|
|
3388
4005
|
}
|
|
4006
|
+
function recordLineageRerollAttempt(project, fields) {
|
|
4007
|
+
const database = lineageDb();
|
|
4008
|
+
try {
|
|
4009
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
4010
|
+
requireAsset3(database, project, fields.assetId);
|
|
4011
|
+
assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
|
|
4012
|
+
const timestamp = nowIso();
|
|
4013
|
+
const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
|
|
4014
|
+
const attemptIndex = Number(maxRow.max_index || 1) + 1;
|
|
4015
|
+
const attempt = {
|
|
4016
|
+
id: `${project}:${fields.nodeAssetId}:attempt:${attemptIndex}`,
|
|
4017
|
+
project_id: project,
|
|
4018
|
+
node_asset_id: fields.nodeAssetId,
|
|
4019
|
+
asset_id: fields.assetId,
|
|
4020
|
+
attempt_index: attemptIndex,
|
|
4021
|
+
source: "reroll",
|
|
4022
|
+
prompt: fields.prompt,
|
|
4023
|
+
generation_job_id: fields.generationJobId,
|
|
4024
|
+
file_path: fields.filePath,
|
|
4025
|
+
checksum_sha256: fields.checksumSha256,
|
|
4026
|
+
created_at: timestamp,
|
|
4027
|
+
promoted_at: timestamp,
|
|
4028
|
+
is_current: true
|
|
4029
|
+
};
|
|
4030
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, attempt };
|
|
4031
|
+
database.exec("BEGIN IMMEDIATE");
|
|
4032
|
+
try {
|
|
4033
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
|
|
4034
|
+
database.prepare(`
|
|
4035
|
+
insert into asset_attempts (
|
|
4036
|
+
id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
|
|
4037
|
+
file_path, checksum_sha256, created_at, promoted_at, is_current
|
|
4038
|
+
) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, 1)
|
|
4039
|
+
`).run(
|
|
4040
|
+
attempt.id,
|
|
4041
|
+
project,
|
|
4042
|
+
fields.nodeAssetId,
|
|
4043
|
+
fields.assetId,
|
|
4044
|
+
attempt.attempt_index,
|
|
4045
|
+
fields.prompt,
|
|
4046
|
+
fields.generationJobId,
|
|
4047
|
+
fields.filePath,
|
|
4048
|
+
fields.checksumSha256,
|
|
4049
|
+
timestamp,
|
|
4050
|
+
timestamp
|
|
4051
|
+
);
|
|
4052
|
+
database.prepare(`
|
|
4053
|
+
update asset_reroll_requests
|
|
4054
|
+
set status = 'resolved', resolved_at = ?
|
|
4055
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
4056
|
+
`).run(timestamp, project, fields.rootAssetId, fields.nodeAssetId);
|
|
4057
|
+
database.prepare(`
|
|
4058
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
4059
|
+
values (?, 'unreviewed', ?, null, null, ?)
|
|
4060
|
+
on conflict(asset_id) do update set
|
|
4061
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
4062
|
+
ignored_at = excluded.ignored_at, updated_at = excluded.updated_at
|
|
4063
|
+
`).run(fields.nodeAssetId, timestamp, timestamp);
|
|
4064
|
+
database.exec("COMMIT");
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
database.exec("ROLLBACK");
|
|
4067
|
+
throw error;
|
|
4068
|
+
}
|
|
4069
|
+
return { ok: true, attempt };
|
|
4070
|
+
} finally {
|
|
4071
|
+
database.close();
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
3389
4074
|
function markLineageRerollRequest(project, fields) {
|
|
3390
4075
|
const database = lineageDb();
|
|
3391
4076
|
try {
|
|
@@ -3582,16 +4267,34 @@ function updateAssetReview(project, fields) {
|
|
|
3582
4267
|
return { ok: true, message: `Marked ${fields.assetId} ${fields.reviewState}`, asset_id: fields.assetId, review_state: fields.reviewState };
|
|
3583
4268
|
}
|
|
3584
4269
|
|
|
3585
|
-
// src/server/
|
|
3586
|
-
|
|
4270
|
+
// src/server/lineageRuntimeCommand.ts
|
|
4271
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
4272
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
4273
|
+
import { join as join8 } from "node:path";
|
|
4274
|
+
var require4 = createRequire3(import.meta.url);
|
|
4275
|
+
function lineagePublicPackageCommand() {
|
|
4276
|
+
if (process.env.LINEAGE_CHANNEL === "dev") {
|
|
4277
|
+
const sourceCli = join8(packageRoot, "src", "cli", "lineage-dev.ts");
|
|
4278
|
+
const builtCli = join8(packageRoot, "dist", "cli", "lineage-dev.js");
|
|
4279
|
+
return existsSync7(sourceCli) ? `${shellQuote(process.execPath)} --import ${shellQuote(require4.resolve("tsx"))} ${shellQuote(sourceCli)}` : `${shellQuote(process.execPath)} ${shellQuote(builtCli)}`;
|
|
4280
|
+
}
|
|
4281
|
+
if (process.env.LINEAGE_CHANNEL === "preview") return "LINEAGE_CHANNEL=preview npx --package @mean-weasel/lineage@next lineage-dev";
|
|
4282
|
+
return "npx @mean-weasel/lineage";
|
|
4283
|
+
}
|
|
3587
4284
|
function shellQuote(value) {
|
|
3588
4285
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3589
4286
|
}
|
|
4287
|
+
function lineageRuntimeSelector() {
|
|
4288
|
+
const manifest = process.env.LINEAGE_PROFILE_MANIFEST?.trim();
|
|
4289
|
+
return manifest ? `--profile ${shellQuote(manifest)}` : `--db ${shellQuote(lineageDbPath())}`;
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
// src/server/assetLineageHandoff.ts
|
|
3590
4293
|
function lineageCommand(command, project, rootAssetId) {
|
|
3591
|
-
return `${
|
|
4294
|
+
return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
|
|
3592
4295
|
}
|
|
3593
4296
|
function linkChildCommand(project, rootAssetId) {
|
|
3594
|
-
return `${
|
|
4297
|
+
return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
3595
4298
|
}
|
|
3596
4299
|
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
3597
4300
|
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
@@ -3633,7 +4336,7 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
3633
4336
|
},
|
|
3634
4337
|
handoff: {
|
|
3635
4338
|
next_command: lineageCommand("next", project, next.root_asset_id),
|
|
3636
|
-
inspect_command: asset ? `${
|
|
4339
|
+
inspect_command: asset ? `${lineagePublicPackageCommand()} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} ${lineageRuntimeSelector()} --json` : void 0,
|
|
3637
4340
|
link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
|
|
3638
4341
|
},
|
|
3639
4342
|
fetchedAt: nowIso()
|
|
@@ -5019,9 +5722,9 @@ function ensureProject5(database, project) {
|
|
|
5019
5722
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
5020
5723
|
`).run(project, project, timestamp, timestamp);
|
|
5021
5724
|
}
|
|
5022
|
-
function definitionFor(adapterType,
|
|
5023
|
-
const definition = definitions.find((item) => item.adapter_type === adapterType && item.provider ===
|
|
5024
|
-
if (!definition) throw new AdapterSettingsError(`Unsupported adapter setting: ${adapterType}/${
|
|
5725
|
+
function definitionFor(adapterType, provider2) {
|
|
5726
|
+
const definition = definitions.find((item) => item.adapter_type === adapterType && item.provider === provider2);
|
|
5727
|
+
if (!definition) throw new AdapterSettingsError(`Unsupported adapter setting: ${adapterType}/${provider2}`, 404);
|
|
5025
5728
|
return definition;
|
|
5026
5729
|
}
|
|
5027
5730
|
function seedDefaults(database, project) {
|
|
@@ -5053,22 +5756,22 @@ function parseConfig(value) {
|
|
|
5053
5756
|
return {};
|
|
5054
5757
|
}
|
|
5055
5758
|
}
|
|
5056
|
-
function credentialFor(
|
|
5057
|
-
if (
|
|
5759
|
+
function credentialFor(provider2, env) {
|
|
5760
|
+
if (provider2 === "s3") {
|
|
5058
5761
|
return { detected: false, label: "Optional local cloud CLI credential", secret_ref: null };
|
|
5059
5762
|
}
|
|
5060
|
-
if (
|
|
5763
|
+
if (provider2 === "buffer") {
|
|
5061
5764
|
const detected = Boolean(env.LINEAGE_SCHEDULER_TOKEN && env.LINEAGE_SCHEDULER_ORGANIZATION_ID);
|
|
5062
5765
|
return { detected, label: "LINEAGE_SCHEDULER_TOKEN + LINEAGE_SCHEDULER_ORGANIZATION_ID", secret_ref: "env:LINEAGE_SCHEDULER_TOKEN" };
|
|
5063
5766
|
}
|
|
5064
5767
|
return { detected: true, label: "No external secret required", secret_ref: null };
|
|
5065
5768
|
}
|
|
5066
|
-
function healthStatus(
|
|
5067
|
-
if (
|
|
5769
|
+
function healthStatus(provider2, enabled, config, credential) {
|
|
5770
|
+
if (provider2 === "s3") {
|
|
5068
5771
|
if (!enabled) return "live_disabled";
|
|
5069
5772
|
return config.bucket && config.region ? "not_tested" : "missing_config";
|
|
5070
5773
|
}
|
|
5071
|
-
if (
|
|
5774
|
+
if (provider2 === "buffer") {
|
|
5072
5775
|
if (!enabled) return "live_disabled";
|
|
5073
5776
|
return credential.detected ? "configured" : "dry_run_available";
|
|
5074
5777
|
}
|
|
@@ -5244,8 +5947,8 @@ function getAdapterStatus(project, env = process.env) {
|
|
|
5244
5947
|
}
|
|
5245
5948
|
|
|
5246
5949
|
// src/server/adapters/posting/bufferPostingService.ts
|
|
5247
|
-
import { mkdirSync as
|
|
5248
|
-
import { join as
|
|
5950
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5951
|
+
import { join as join9 } from "node:path";
|
|
5249
5952
|
|
|
5250
5953
|
// src/server/contentPostHandoff.ts
|
|
5251
5954
|
function readinessForPost(post) {
|
|
@@ -5741,12 +6444,12 @@ function resolvePost(project, fields) {
|
|
|
5741
6444
|
throw new PostingAdapterError("Buffer dry run requires postId or target=selected");
|
|
5742
6445
|
}
|
|
5743
6446
|
function payloadPath(project, postId) {
|
|
5744
|
-
return
|
|
6447
|
+
return join9(repoRoot, ".asset-scratch", "buffer-dry-runs", safeSegment(project), `${safeSegment(postId)}.json`);
|
|
5745
6448
|
}
|
|
5746
6449
|
function writePayload(project, postId, payload) {
|
|
5747
6450
|
const file = payloadPath(project, postId);
|
|
5748
|
-
|
|
5749
|
-
|
|
6451
|
+
mkdirSync6(join9(file, ".."), { recursive: true });
|
|
6452
|
+
writeFileSync3(file, `${JSON.stringify(payload, null, 2)}
|
|
5750
6453
|
`);
|
|
5751
6454
|
return file;
|
|
5752
6455
|
}
|
|
@@ -5855,11 +6558,11 @@ function registerAdapterRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
5855
6558
|
import { Router } from "express";
|
|
5856
6559
|
|
|
5857
6560
|
// src/server/contentBatchImport.ts
|
|
5858
|
-
import { existsSync as
|
|
5859
|
-
import { basename as
|
|
6561
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
6562
|
+
import { basename as basename4, join as join10, relative as relative2 } from "node:path";
|
|
5860
6563
|
function walk(dir) {
|
|
5861
6564
|
return readdirSync3(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
5862
|
-
const path =
|
|
6565
|
+
const path = join10(dir, entry.name);
|
|
5863
6566
|
if (entry.isDirectory()) return walk(path);
|
|
5864
6567
|
return entry.isFile() && entry.name.endsWith(".md") ? [path] : [];
|
|
5865
6568
|
});
|
|
@@ -5884,14 +6587,14 @@ function phaseFromStatus(value) {
|
|
|
5884
6587
|
return "draft";
|
|
5885
6588
|
}
|
|
5886
6589
|
function postIdFor(kind, file) {
|
|
5887
|
-
const slug2 =
|
|
6590
|
+
const slug2 = basename4(file, ".md").replace(/^\d{4}-\d{2}-/, "");
|
|
5888
6591
|
return `${kind}-${slug2}`;
|
|
5889
6592
|
}
|
|
5890
6593
|
function itemFromFile(file) {
|
|
5891
6594
|
const parts = file.split("/");
|
|
5892
6595
|
const kind = parts.includes("drafts") ? "draft" : parts.includes("concepts") ? "concept" : null;
|
|
5893
6596
|
if (!kind) return null;
|
|
5894
|
-
const text =
|
|
6597
|
+
const text = readFileSync5(file, "utf8");
|
|
5895
6598
|
const meta = metadata(text);
|
|
5896
6599
|
const pathChannel = parts[parts.indexOf("channels") + 1] || "";
|
|
5897
6600
|
const relatedAsset = meta["related asset"] && meta["related asset"] !== "none yet" ? meta["related asset"] : void 0;
|
|
@@ -5905,12 +6608,12 @@ function itemFromFile(file) {
|
|
|
5905
6608
|
postId: postIdFor(kind, file),
|
|
5906
6609
|
relatedAsset,
|
|
5907
6610
|
sourcePath: relative2(repoRoot, file),
|
|
5908
|
-
title: text.match(/^#\s+(.+)$/m)?.[1] ||
|
|
6611
|
+
title: text.match(/^#\s+(.+)$/m)?.[1] || basename4(file, ".md")
|
|
5909
6612
|
};
|
|
5910
6613
|
}
|
|
5911
6614
|
function demoMarkdownItems(kind) {
|
|
5912
|
-
const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT ||
|
|
5913
|
-
if (!
|
|
6615
|
+
const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join10(repoRoot, "demo-project", "channels");
|
|
6616
|
+
if (!existsSync8(root)) return [];
|
|
5914
6617
|
return walk(root).map(itemFromFile).filter((item) => Boolean(item)).filter((item) => kind === "all" || `${item.kind}s` === kind).sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
5915
6618
|
}
|
|
5916
6619
|
function importDemoContentBatch(project, options) {
|
|
@@ -6249,6 +6952,11 @@ function contentBatchRouter(projectFrom2) {
|
|
|
6249
6952
|
}
|
|
6250
6953
|
|
|
6251
6954
|
// src/server/generationReceipts.ts
|
|
6955
|
+
import { existsSync as existsSync9, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
6956
|
+
import { relative as relative3, resolve as resolve6 } from "node:path";
|
|
6957
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
6958
|
+
var adapterVersion = "generation-receipts-v1";
|
|
6959
|
+
var provider = "codex-handoff";
|
|
6252
6960
|
var GenerationReceiptError = class extends Error {
|
|
6253
6961
|
constructor(message, status = 400) {
|
|
6254
6962
|
super(message);
|
|
@@ -6259,10 +6967,44 @@ var GenerationReceiptError = class extends Error {
|
|
|
6259
6967
|
function isGenerationReceiptError(error) {
|
|
6260
6968
|
return error instanceof GenerationReceiptError;
|
|
6261
6969
|
}
|
|
6970
|
+
function jobId() {
|
|
6971
|
+
return `gen-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
|
|
6972
|
+
}
|
|
6973
|
+
function quote(value) {
|
|
6974
|
+
return JSON.stringify(value);
|
|
6975
|
+
}
|
|
6262
6976
|
function parseJson(value, fallback) {
|
|
6263
6977
|
if (!value) return fallback;
|
|
6264
6978
|
return JSON.parse(value);
|
|
6265
6979
|
}
|
|
6980
|
+
function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
6981
|
+
const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
6982
|
+
return {
|
|
6983
|
+
schema_version: "lineage.generation_handoff.v1",
|
|
6984
|
+
provider,
|
|
6985
|
+
project,
|
|
6986
|
+
job_id: id,
|
|
6987
|
+
prompt,
|
|
6988
|
+
expected_output_count: 1,
|
|
6989
|
+
lineage: {
|
|
6990
|
+
root_asset_id: rootAssetId,
|
|
6991
|
+
parent_asset_id: target.asset_id,
|
|
6992
|
+
selection_strategy: "reroll_request",
|
|
6993
|
+
parent_title: target.title,
|
|
6994
|
+
parent_local_path: target.local_path,
|
|
6995
|
+
parent_s3_key: target.s3_key
|
|
6996
|
+
},
|
|
6997
|
+
instructions: [
|
|
6998
|
+
"Use Codex image generation outside Lineage server code.",
|
|
6999
|
+
"Write the regenerated output file under .asset-scratch before import.",
|
|
7000
|
+
"Do not call live provider APIs from the CLI or server.",
|
|
7001
|
+
"Import exactly one output with reroll import, not link-child or generate image import.",
|
|
7002
|
+
`Resolve re-roll request ${request.id}; do not create a visible lineage child edge.`
|
|
7003
|
+
],
|
|
7004
|
+
import_command: importCommand,
|
|
7005
|
+
guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
|
|
7006
|
+
};
|
|
7007
|
+
}
|
|
6266
7008
|
function receiptFrom(row) {
|
|
6267
7009
|
return {
|
|
6268
7010
|
id: String(row.id),
|
|
@@ -6326,6 +7068,191 @@ function loadGenerationJob(database, project, id) {
|
|
|
6326
7068
|
receipts
|
|
6327
7069
|
};
|
|
6328
7070
|
}
|
|
7071
|
+
function insertReceipt(database, id, type, command, payload) {
|
|
7072
|
+
database.prepare(`
|
|
7073
|
+
insert into generation_job_receipts (id, job_id, receipt_type, status, command, payload_json, created_at)
|
|
7074
|
+
values (?, ?, ?, 'ok', ?, ?, ?)
|
|
7075
|
+
`).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
|
|
7076
|
+
}
|
|
7077
|
+
function planImageReroll(project = defaultProject, fields) {
|
|
7078
|
+
const prompt = fields.prompt.trim();
|
|
7079
|
+
if (!prompt) throw new GenerationReceiptError("Missing --prompt");
|
|
7080
|
+
if (!fields.rootAssetId) throw new GenerationReceiptError("Missing --root");
|
|
7081
|
+
if (!fields.targetAssetId) throw new GenerationReceiptError("Missing --target");
|
|
7082
|
+
const snapshot = getLineageSnapshot(project, fields.rootAssetId);
|
|
7083
|
+
const target = snapshot.nodes.find((node) => node.asset_id === fields.targetAssetId);
|
|
7084
|
+
if (!target) throw new GenerationReceiptError(`Re-roll target is not in lineage: ${fields.targetAssetId}`, 404);
|
|
7085
|
+
const request = listLineageRerollRequests(project, snapshot.root_asset_id).requests.find((item) => item.node_asset_id === fields.targetAssetId);
|
|
7086
|
+
if (!request) throw new GenerationReceiptError(`No pending re-roll request for ${fields.targetAssetId}`);
|
|
7087
|
+
const id = jobId();
|
|
7088
|
+
const timestamp = nowIso();
|
|
7089
|
+
const handoff3 = buildRerollHandoff(project, id, prompt, snapshot.root_asset_id, target, request);
|
|
7090
|
+
const input = {
|
|
7091
|
+
id: `${id}:input:0`,
|
|
7092
|
+
job_id: id,
|
|
7093
|
+
project_id: project,
|
|
7094
|
+
asset_id: target.asset_id,
|
|
7095
|
+
root_asset_id: snapshot.root_asset_id,
|
|
7096
|
+
role: "reroll_target",
|
|
7097
|
+
position: 0,
|
|
7098
|
+
selection_strategy: "reroll_request",
|
|
7099
|
+
selection_snapshot: {
|
|
7100
|
+
project,
|
|
7101
|
+
root_asset_id: snapshot.root_asset_id,
|
|
7102
|
+
strategy: "selected",
|
|
7103
|
+
selection_mode: "single",
|
|
7104
|
+
recommended_action: "evolve_variations",
|
|
7105
|
+
reason: "user_selected",
|
|
7106
|
+
next_asset: target,
|
|
7107
|
+
next_assets: [target],
|
|
7108
|
+
latest: snapshot.latest,
|
|
7109
|
+
selected: [target.asset_id],
|
|
7110
|
+
selection: null,
|
|
7111
|
+
selections: [],
|
|
7112
|
+
candidates: snapshot.nodes,
|
|
7113
|
+
warnings: ["Re-roll target: import output as an attempt, not a lineage child."],
|
|
7114
|
+
fetchedAt: timestamp
|
|
7115
|
+
}
|
|
7116
|
+
};
|
|
7117
|
+
const preview = {
|
|
7118
|
+
id,
|
|
7119
|
+
project_id: project,
|
|
7120
|
+
provider,
|
|
7121
|
+
adapter_version: adapterVersion,
|
|
7122
|
+
source_mode: "lineage_reroll",
|
|
7123
|
+
root_asset_id: snapshot.root_asset_id,
|
|
7124
|
+
prompt,
|
|
7125
|
+
expected_output_count: 1,
|
|
7126
|
+
status: "planned",
|
|
7127
|
+
output_dir: ".asset-scratch",
|
|
7128
|
+
handoff: handoff3,
|
|
7129
|
+
created_at: timestamp,
|
|
7130
|
+
updated_at: timestamp,
|
|
7131
|
+
inputs: [input],
|
|
7132
|
+
outputs: [],
|
|
7133
|
+
receipts: [{
|
|
7134
|
+
id: `${id}:receipt:plan:preview`,
|
|
7135
|
+
job_id: id,
|
|
7136
|
+
receipt_type: "plan",
|
|
7137
|
+
status: "ok",
|
|
7138
|
+
command: "reroll plan",
|
|
7139
|
+
payload: { prompt, expected_output_count: 1, lineage: handoff3.lineage, reroll_request_id: request.id },
|
|
7140
|
+
created_at: timestamp
|
|
7141
|
+
}]
|
|
7142
|
+
};
|
|
7143
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
|
|
7144
|
+
const database = lineageDb();
|
|
7145
|
+
try {
|
|
7146
|
+
database.exec("BEGIN IMMEDIATE");
|
|
7147
|
+
try {
|
|
7148
|
+
database.prepare(`
|
|
7149
|
+
insert into generation_jobs (
|
|
7150
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
7151
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at
|
|
7152
|
+
) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
|
|
7153
|
+
`).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff3), timestamp, timestamp);
|
|
7154
|
+
database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(input.selection_snapshot));
|
|
7155
|
+
insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
|
|
7156
|
+
database.exec("COMMIT");
|
|
7157
|
+
} catch (error) {
|
|
7158
|
+
database.exec("ROLLBACK");
|
|
7159
|
+
throw error;
|
|
7160
|
+
}
|
|
7161
|
+
return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
|
|
7162
|
+
} finally {
|
|
7163
|
+
database.close();
|
|
7164
|
+
}
|
|
7165
|
+
}
|
|
7166
|
+
function isPathInside2(child, parent) {
|
|
7167
|
+
const rel = relative3(parent, child);
|
|
7168
|
+
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
7169
|
+
}
|
|
7170
|
+
function resolveScratchFile(file) {
|
|
7171
|
+
const scratchRoot = resolve6(repoRoot, ".asset-scratch");
|
|
7172
|
+
const candidate = file.startsWith(".asset-scratch/") || resolve6(file).startsWith(scratchRoot) ? resolve6(repoRoot, file) : resolve6(scratchRoot, file);
|
|
7173
|
+
if (!isPathInside2(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
7174
|
+
if (!existsSync9(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
7175
|
+
const realScratchRoot = realpathSync2(scratchRoot);
|
|
7176
|
+
const realCandidate = realpathSync2(candidate);
|
|
7177
|
+
if (!isPathInside2(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
7178
|
+
const stats = statSync4(candidate);
|
|
7179
|
+
if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
|
|
7180
|
+
const checksum = fileSha256(candidate);
|
|
7181
|
+
return {
|
|
7182
|
+
relativePath: relative3(scratchRoot, candidate),
|
|
7183
|
+
checksum,
|
|
7184
|
+
size: stats.size,
|
|
7185
|
+
contentType: contentTypeFor(candidate),
|
|
7186
|
+
assetId: `local-${checksum.slice(0, 12)}`
|
|
7187
|
+
};
|
|
7188
|
+
}
|
|
7189
|
+
function importImageRerollOutput(project = defaultProject, fields) {
|
|
7190
|
+
if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
|
|
7191
|
+
if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
|
|
7192
|
+
const database = lineageDb();
|
|
7193
|
+
let job;
|
|
7194
|
+
try {
|
|
7195
|
+
job = loadGenerationJob(database, project, fields.jobId);
|
|
7196
|
+
} finally {
|
|
7197
|
+
database.close();
|
|
7198
|
+
}
|
|
7199
|
+
if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
|
|
7200
|
+
if (job.source_mode !== "lineage_reroll") throw new GenerationReceiptError(`Generation job is not a re-roll job: ${job.source_mode}`);
|
|
7201
|
+
const target = job.inputs.filter((input) => input.role === "reroll_target");
|
|
7202
|
+
if (target.length !== 1) throw new GenerationReceiptError("Re-roll import requires exactly one reroll_target input");
|
|
7203
|
+
const resolved = resolveScratchFile(fields.file);
|
|
7204
|
+
indexLineageAssets(project);
|
|
7205
|
+
const writeDb = lineageDb();
|
|
7206
|
+
try {
|
|
7207
|
+
const timestamp = nowIso();
|
|
7208
|
+
writeDb.exec("BEGIN IMMEDIATE");
|
|
7209
|
+
try {
|
|
7210
|
+
const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, resolved.assetId);
|
|
7211
|
+
if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${resolved.relativePath}`);
|
|
7212
|
+
const outputId = `${fields.jobId}:output:0`;
|
|
7213
|
+
writeDb.prepare(`insert into generation_job_outputs (
|
|
7214
|
+
id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type, imported_asset_id, parent_asset_id, imported_at
|
|
7215
|
+
) values (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`).run(outputId, fields.jobId, project, resolved.relativePath, resolved.checksum, resolved.size, resolved.contentType, resolved.assetId, target[0].asset_id, timestamp);
|
|
7216
|
+
writeDb.prepare(`
|
|
7217
|
+
update generation_jobs
|
|
7218
|
+
set status = 'imported', imported_at = ?, updated_at = ?
|
|
7219
|
+
where project_id = ? and id = ?
|
|
7220
|
+
`).run(timestamp, timestamp, project, fields.jobId);
|
|
7221
|
+
insertReceipt(writeDb, fields.jobId, "import", "reroll import", {
|
|
7222
|
+
file: { output_index: 0, file_path: resolved.relativePath, imported_asset_id: resolved.assetId, parent_asset_id: target[0].asset_id },
|
|
7223
|
+
reroll: { root_asset_id: job.root_asset_id, node_asset_id: target[0].asset_id }
|
|
7224
|
+
});
|
|
7225
|
+
writeDb.exec("COMMIT");
|
|
7226
|
+
} catch (error) {
|
|
7227
|
+
writeDb.exec("ROLLBACK");
|
|
7228
|
+
throw error;
|
|
7229
|
+
}
|
|
7230
|
+
recordLineageRerollAttempt(project, {
|
|
7231
|
+
rootAssetId: job.root_asset_id,
|
|
7232
|
+
nodeAssetId: target[0].asset_id,
|
|
7233
|
+
assetId: resolved.assetId,
|
|
7234
|
+
prompt: job.prompt,
|
|
7235
|
+
generationJobId: fields.jobId,
|
|
7236
|
+
filePath: resolved.relativePath,
|
|
7237
|
+
checksumSha256: resolved.checksum,
|
|
7238
|
+
confirmWrite: true
|
|
7239
|
+
});
|
|
7240
|
+
const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
|
|
7241
|
+
if (rerollTask) {
|
|
7242
|
+
resolveLineageTask(project, {
|
|
7243
|
+
actor: "agent",
|
|
7244
|
+
confirmWrite: true,
|
|
7245
|
+
resolvedAssetId: resolved.assetId,
|
|
7246
|
+
resolvedGenerationJobId: fields.jobId,
|
|
7247
|
+
taskId: rerollTask.id
|
|
7248
|
+
});
|
|
7249
|
+
}
|
|
7250
|
+
const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
|
|
7251
|
+
return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
|
|
7252
|
+
} finally {
|
|
7253
|
+
writeDb.close();
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
6329
7256
|
|
|
6330
7257
|
// src/server/generationReceiptJobs.ts
|
|
6331
7258
|
function listImageGenerationJobs(project = defaultProject, fields = {}) {
|
|
@@ -6416,15 +7343,15 @@ function registerLineageTaskRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
6416
7343
|
}
|
|
6417
7344
|
|
|
6418
7345
|
// src/server/assetLineageDemo.ts
|
|
6419
|
-
import { createHash as
|
|
6420
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
7346
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7347
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6421
7348
|
import { tmpdir } from "node:os";
|
|
6422
|
-
import { dirname as
|
|
7349
|
+
import { dirname as dirname5, join as join11, posix } from "node:path";
|
|
6423
7350
|
import { gunzipSync } from "node:zlib";
|
|
6424
7351
|
var demoWorkspaceTitle = "Demo: Content iteration tree";
|
|
6425
7352
|
var demoWorkspaceNotes = "Repeatable sample lineage for demos and onboarding. Archive it when reviewing real work.";
|
|
6426
7353
|
var demoBasePath = ["lineage-demo", "2026-06-lineage-demo"];
|
|
6427
|
-
var swissifierManifestPath =
|
|
7354
|
+
var swissifierManifestPath = join11(packageRoot, "fixtures", defaultProject, "lineage", "swissifier-rich-demo.json");
|
|
6428
7355
|
var demoAssets = [
|
|
6429
7356
|
{ key: "root", channel: "linkedin", file: "demo-root.svg", label: "Initial Demo Concept", fill: "#f6fbfb", stroke: "#0b7f88" },
|
|
6430
7357
|
{ key: "hookA", channel: "tiktok", file: "demo-hook-a-v01.svg", label: "Hook A v01", fill: "#fff8e6", stroke: "#9a6a00" },
|
|
@@ -6461,37 +7388,37 @@ var demoPositions = /* @__PURE__ */ new Map([
|
|
|
6461
7388
|
["alt", { x: 1060, y: 80 }]
|
|
6462
7389
|
]);
|
|
6463
7390
|
function demoProjectDir(project) {
|
|
6464
|
-
return
|
|
7391
|
+
return join11(repoRoot, ".asset-scratch", ...demoBasePath, project);
|
|
6465
7392
|
}
|
|
6466
7393
|
function demoRelativePath(project, asset) {
|
|
6467
|
-
return
|
|
7394
|
+
return join11(...demoBasePath, project, asset.channel, asset.file);
|
|
6468
7395
|
}
|
|
6469
7396
|
function demoFilePath(project, asset) {
|
|
6470
|
-
return
|
|
7397
|
+
return join11(demoProjectDir(project), asset.channel, asset.file);
|
|
6471
7398
|
}
|
|
6472
7399
|
function swissifierManifest() {
|
|
6473
|
-
return JSON.parse(
|
|
7400
|
+
return JSON.parse(readFileSync6(swissifierManifestPath, "utf8"));
|
|
6474
7401
|
}
|
|
6475
7402
|
function swissifierRelativePath(manifest, asset) {
|
|
6476
|
-
return
|
|
7403
|
+
return join11(manifest.media.target_dir, asset.file);
|
|
6477
7404
|
}
|
|
6478
7405
|
function swissifierFilePath(manifest, asset) {
|
|
6479
|
-
return
|
|
7406
|
+
return join11(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
|
|
6480
7407
|
}
|
|
6481
7408
|
function swissifierRerollRelativePath(manifest, attempt) {
|
|
6482
|
-
return
|
|
7409
|
+
return join11(manifest.media.target_dir, "reroll-attempts", attempt.file);
|
|
6483
7410
|
}
|
|
6484
7411
|
function swissifierRerollFilePath(manifest, attempt) {
|
|
6485
|
-
return
|
|
7412
|
+
return join11(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
|
|
6486
7413
|
}
|
|
6487
7414
|
function swissifierRerollFixturePath(attempt) {
|
|
6488
|
-
return
|
|
7415
|
+
return join11(dirname5(swissifierManifestPath), "swissifier-rerolls", attempt.file);
|
|
6489
7416
|
}
|
|
6490
7417
|
function swissifierSourcePath(manifest, asset, sourceDir) {
|
|
6491
|
-
const direct =
|
|
6492
|
-
if (
|
|
6493
|
-
const nested =
|
|
6494
|
-
return
|
|
7418
|
+
const direct = join11(sourceDir, asset.file);
|
|
7419
|
+
if (existsSync10(direct)) return direct;
|
|
7420
|
+
const nested = join11(sourceDir, manifest.media.target_dir, asset.file);
|
|
7421
|
+
return existsSync10(nested) ? nested : null;
|
|
6495
7422
|
}
|
|
6496
7423
|
function swissifierMediaState(manifest) {
|
|
6497
7424
|
const missing = [];
|
|
@@ -6499,7 +7426,7 @@ function swissifierMediaState(manifest) {
|
|
|
6499
7426
|
for (const asset of manifest.assets) {
|
|
6500
7427
|
const relativePath = swissifierRelativePath(manifest, asset);
|
|
6501
7428
|
const path = swissifierFilePath(manifest, asset);
|
|
6502
|
-
if (!
|
|
7429
|
+
if (!existsSync10(path)) {
|
|
6503
7430
|
missing.push(relativePath);
|
|
6504
7431
|
continue;
|
|
6505
7432
|
}
|
|
@@ -6508,7 +7435,7 @@ function swissifierMediaState(manifest) {
|
|
|
6508
7435
|
return { invalid, missing };
|
|
6509
7436
|
}
|
|
6510
7437
|
function sha256Hex(input) {
|
|
6511
|
-
return
|
|
7438
|
+
return createHash4("sha256").update(input).digest("hex");
|
|
6512
7439
|
}
|
|
6513
7440
|
function safeTarEntryName(rawName) {
|
|
6514
7441
|
const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
|
|
@@ -6571,9 +7498,9 @@ function extractSwissifierMediaArchive(archive, manifest, destination) {
|
|
|
6571
7498
|
const file = body.subarray(offset, offset + size);
|
|
6572
7499
|
const actualSha = sha256Hex(file);
|
|
6573
7500
|
if (actualSha !== asset.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier media archive entry: ${name}`);
|
|
6574
|
-
const target =
|
|
6575
|
-
|
|
6576
|
-
|
|
7501
|
+
const target = join11(destination, directName);
|
|
7502
|
+
mkdirSync7(dirname5(target), { recursive: true });
|
|
7503
|
+
writeFileSync4(target, file);
|
|
6577
7504
|
extracted.add(directName);
|
|
6578
7505
|
offset = nextOffset;
|
|
6579
7506
|
}
|
|
@@ -6591,14 +7518,14 @@ async function downloadBuffer(url, maxBytes) {
|
|
|
6591
7518
|
return buffer;
|
|
6592
7519
|
}
|
|
6593
7520
|
function missingDemoMedia(project) {
|
|
6594
|
-
return demoAssets.filter((asset) => !
|
|
7521
|
+
return demoAssets.filter((asset) => !existsSync10(demoFilePath(project, asset))).map((asset) => demoRelativePath(project, asset));
|
|
6595
7522
|
}
|
|
6596
7523
|
function demoSeedMediaStatus(project = defaultProject) {
|
|
6597
7524
|
const missing = missingDemoMedia(project);
|
|
6598
7525
|
const present = demoAssets.length - missing.length;
|
|
6599
7526
|
return {
|
|
6600
7527
|
ok: true,
|
|
6601
|
-
media_root:
|
|
7528
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6602
7529
|
present,
|
|
6603
7530
|
total: demoAssets.length,
|
|
6604
7531
|
missing,
|
|
@@ -6615,7 +7542,7 @@ function swissifierRichDemoMediaStatus(project = defaultProject) {
|
|
|
6615
7542
|
ok: true,
|
|
6616
7543
|
demo_id: manifest.id,
|
|
6617
7544
|
project,
|
|
6618
|
-
media_root:
|
|
7545
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6619
7546
|
media_target: manifest.media.target_dir,
|
|
6620
7547
|
download_available: Boolean(manifest.media.download),
|
|
6621
7548
|
download_file: manifest.media.download?.file,
|
|
@@ -6666,7 +7593,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
|
|
|
6666
7593
|
would_restore: manifest.assets.length
|
|
6667
7594
|
};
|
|
6668
7595
|
}
|
|
6669
|
-
const sourceDir = mkdtempSync(
|
|
7596
|
+
const sourceDir = mkdtempSync(join11(tmpdir(), "lineage-swissifier-media-"));
|
|
6670
7597
|
try {
|
|
6671
7598
|
const extracted = extractSwissifierMediaArchive(archive, manifest, sourceDir);
|
|
6672
7599
|
const restored = restoreSwissifierRichDemoMedia(project, { confirmWrite: true, sourceDir });
|
|
@@ -6680,7 +7607,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
|
|
|
6680
7607
|
media_status: swissifierRichDemoMediaStatus(project)
|
|
6681
7608
|
};
|
|
6682
7609
|
} finally {
|
|
6683
|
-
|
|
7610
|
+
rmSync2(sourceDir, { force: true, recursive: true });
|
|
6684
7611
|
}
|
|
6685
7612
|
}
|
|
6686
7613
|
function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite: false }) {
|
|
@@ -6693,7 +7620,7 @@ function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite:
|
|
|
6693
7620
|
return {
|
|
6694
7621
|
ok: true,
|
|
6695
7622
|
dryRun: !fields.confirmWrite,
|
|
6696
|
-
media_root:
|
|
7623
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6697
7624
|
restored: fields.confirmWrite ? missing.size : 0,
|
|
6698
7625
|
total: demoAssets.length,
|
|
6699
7626
|
would_restore: fields.confirmWrite ? 0 : missing.size,
|
|
@@ -6710,7 +7637,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
|
|
|
6710
7637
|
demo_id: manifest.id,
|
|
6711
7638
|
project,
|
|
6712
7639
|
dryRun: !fields.confirmWrite,
|
|
6713
|
-
media_root:
|
|
7640
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6714
7641
|
restored: 0,
|
|
6715
7642
|
total: manifest.assets.length,
|
|
6716
7643
|
would_restore: 0,
|
|
@@ -6738,7 +7665,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
|
|
|
6738
7665
|
if (fields.confirmWrite) {
|
|
6739
7666
|
for (const item of copyable) {
|
|
6740
7667
|
const target = swissifierFilePath(manifest, item.asset);
|
|
6741
|
-
|
|
7668
|
+
mkdirSync7(dirname5(target), { recursive: true });
|
|
6742
7669
|
copyFileSync2(item.source, target);
|
|
6743
7670
|
}
|
|
6744
7671
|
}
|
|
@@ -6747,7 +7674,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
|
|
|
6747
7674
|
demo_id: manifest.id,
|
|
6748
7675
|
project,
|
|
6749
7676
|
dryRun: !fields.confirmWrite,
|
|
6750
|
-
media_root:
|
|
7677
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6751
7678
|
restored: fields.confirmWrite ? copyable.length : 0,
|
|
6752
7679
|
total: manifest.assets.length,
|
|
6753
7680
|
would_restore: fields.confirmWrite ? 0 : copyable.length,
|
|
@@ -6769,15 +7696,15 @@ function svg(label, fill, stroke) {
|
|
|
6769
7696
|
`;
|
|
6770
7697
|
}
|
|
6771
7698
|
function assetIdFor(asset) {
|
|
6772
|
-
return `local-${
|
|
7699
|
+
return `local-${createHash4("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
|
|
6773
7700
|
}
|
|
6774
7701
|
function demoAssetIds() {
|
|
6775
7702
|
return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
|
|
6776
7703
|
}
|
|
6777
7704
|
function writeDemoFile(project, asset) {
|
|
6778
7705
|
const path = demoFilePath(project, asset);
|
|
6779
|
-
|
|
6780
|
-
|
|
7706
|
+
mkdirSync7(dirname5(path), { recursive: true });
|
|
7707
|
+
writeFileSync4(path, svg(asset.label, asset.fill, asset.stroke));
|
|
6781
7708
|
}
|
|
6782
7709
|
function writeDemoFiles(project) {
|
|
6783
7710
|
const ids = demoAssetIds();
|
|
@@ -6786,8 +7713,8 @@ function writeDemoFiles(project) {
|
|
|
6786
7713
|
}
|
|
6787
7714
|
function swissifierRerollAsset(attempt) {
|
|
6788
7715
|
const source = swissifierRerollFixturePath(attempt);
|
|
6789
|
-
if (!
|
|
6790
|
-
const body =
|
|
7716
|
+
if (!existsSync10(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
|
|
7717
|
+
const body = readFileSync6(source);
|
|
6791
7718
|
if (body.length !== attempt.size_bytes) throw new Error(`Unexpected Swissifier re-roll fixture size for ${attempt.file}`);
|
|
6792
7719
|
const checksumSha256 = sha256Hex(body);
|
|
6793
7720
|
if (checksumSha256 !== attempt.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier re-roll fixture: ${attempt.file}`);
|
|
@@ -6801,7 +7728,7 @@ function writeSwissifierRerollFiles(manifest) {
|
|
|
6801
7728
|
for (const attempt of manifest.reroll_attempts || []) {
|
|
6802
7729
|
swissifierRerollAsset(attempt);
|
|
6803
7730
|
const target = swissifierRerollFilePath(manifest, attempt);
|
|
6804
|
-
|
|
7731
|
+
mkdirSync7(dirname5(target), { recursive: true });
|
|
6805
7732
|
copyFileSync2(swissifierRerollFixturePath(attempt), target);
|
|
6806
7733
|
}
|
|
6807
7734
|
}
|
|
@@ -6813,7 +7740,7 @@ function upsertDemoAssets(project, ids) {
|
|
|
6813
7740
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
6814
7741
|
values (?, ?, ?, ?, ?)
|
|
6815
7742
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
6816
|
-
`).run(project, project,
|
|
7743
|
+
`).run(project, project, join11(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
6817
7744
|
const assetStatement = database.prepare(`
|
|
6818
7745
|
insert into assets (
|
|
6819
7746
|
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
@@ -6836,7 +7763,7 @@ function upsertDemoAssets(project, ids) {
|
|
|
6836
7763
|
ids[asset.key],
|
|
6837
7764
|
project,
|
|
6838
7765
|
demoRelativePath(project, asset),
|
|
6839
|
-
|
|
7766
|
+
createHash4("sha256").update(body).digest("hex"),
|
|
6840
7767
|
asset.label,
|
|
6841
7768
|
asset.channel,
|
|
6842
7769
|
Buffer.byteLength(body),
|
|
@@ -6859,7 +7786,7 @@ function upsertSwissifierAssets(project, manifest) {
|
|
|
6859
7786
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
6860
7787
|
values (?, ?, ?, ?, ?)
|
|
6861
7788
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
6862
|
-
`).run(project, project,
|
|
7789
|
+
`).run(project, project, join11(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
6863
7790
|
const assetStatement = database.prepare(`
|
|
6864
7791
|
insert into assets (
|
|
6865
7792
|
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
@@ -7095,7 +8022,7 @@ function archiveDemoLineageWorkspace(project, confirmWrite) {
|
|
|
7095
8022
|
if (!isLineageWorkspaceError(error) || error.status !== 404) throw error;
|
|
7096
8023
|
archived = { ok: true, message: "No demo lineage workspace exists yet", workspace: null };
|
|
7097
8024
|
}
|
|
7098
|
-
if (confirmWrite)
|
|
8025
|
+
if (confirmWrite) rmSync2(demoProjectDir(project), { force: true, recursive: true });
|
|
7099
8026
|
return {
|
|
7100
8027
|
ok: true,
|
|
7101
8028
|
message: confirmWrite ? `Archived ${demoWorkspaceTitle}` : `Would archive ${demoWorkspaceTitle}`,
|
|
@@ -7190,13 +8117,13 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
7190
8117
|
|
|
7191
8118
|
// src/server/runtimeInfo.ts
|
|
7192
8119
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7193
|
-
import { createRequire as
|
|
7194
|
-
import { existsSync as
|
|
7195
|
-
import { join as
|
|
7196
|
-
var
|
|
8120
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
8121
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync5 } from "node:fs";
|
|
8122
|
+
import { join as join12 } from "node:path";
|
|
8123
|
+
var require5 = createRequire4(import.meta.url);
|
|
7197
8124
|
function packageInfo() {
|
|
7198
8125
|
try {
|
|
7199
|
-
const info = JSON.parse(
|
|
8126
|
+
const info = JSON.parse(readFileSync7(join12(packageRoot, "package.json"), "utf8"));
|
|
7200
8127
|
return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
|
|
7201
8128
|
} catch {
|
|
7202
8129
|
return { name: "@mean-weasel/lineage", version: "0.0.0" };
|
|
@@ -7212,32 +8139,47 @@ function normalizeRuntimeChannel(value) {
|
|
|
7212
8139
|
function gitSha() {
|
|
7213
8140
|
const envSha = process.env.LINEAGE_GIT_SHA || process.env.GITHUB_SHA;
|
|
7214
8141
|
if (envSha) return envSha.slice(0, 40);
|
|
7215
|
-
if (!
|
|
7216
|
-
const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd:
|
|
8142
|
+
if (!existsSync11(join12(packageRoot, ".git"))) return void 0;
|
|
8143
|
+
const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: packageRoot, encoding: "utf8" });
|
|
7217
8144
|
return result.status === 0 ? result.stdout.trim() || void 0 : void 0;
|
|
7218
8145
|
}
|
|
7219
|
-
function
|
|
8146
|
+
function tableExists2(database, table) {
|
|
7220
8147
|
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
7221
8148
|
}
|
|
7222
8149
|
function tableCount(database, table) {
|
|
7223
|
-
if (!
|
|
8150
|
+
if (!tableExists2(database, table)) return void 0;
|
|
7224
8151
|
const row = database.prepare(`select count(*) count from ${table}`).get();
|
|
7225
8152
|
return typeof row?.count === "number" ? row.count : void 0;
|
|
7226
8153
|
}
|
|
8154
|
+
function migrationKeys(database) {
|
|
8155
|
+
if (!tableExists2(database, "lineage_schema_migrations")) return [];
|
|
8156
|
+
return database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
8157
|
+
}
|
|
7227
8158
|
function getLineageRuntimeInfo(options = {}) {
|
|
7228
8159
|
const info = packageInfo();
|
|
7229
8160
|
const dbPath = options.dbPath || lineageDbPath();
|
|
7230
|
-
const
|
|
8161
|
+
const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
8162
|
+
const databaseInfo = { exists: existsSync11(dbPath), path: dbPath };
|
|
8163
|
+
const schema = { migration_keys: [] };
|
|
7231
8164
|
if (databaseInfo.exists) {
|
|
7232
8165
|
try {
|
|
7233
|
-
const stat =
|
|
8166
|
+
const stat = statSync5(dbPath);
|
|
7234
8167
|
databaseInfo.modified_at = stat.mtime.toISOString();
|
|
7235
8168
|
databaseInfo.size_bytes = stat.size;
|
|
7236
|
-
const { DatabaseSync } =
|
|
8169
|
+
const { DatabaseSync } = require5("node:sqlite");
|
|
7237
8170
|
const database = new DatabaseSync(dbPath, { readOnly: true });
|
|
7238
8171
|
try {
|
|
7239
8172
|
databaseInfo.projects = tableCount(database, "projects");
|
|
7240
8173
|
databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
|
|
8174
|
+
schema.migration_keys = migrationKeys(database);
|
|
8175
|
+
if (tableExists2(database, "lineage_profile_identity")) {
|
|
8176
|
+
const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
|
|
8177
|
+
schema.profile_identity_rows = rows.length;
|
|
8178
|
+
if (rows.length === 1) {
|
|
8179
|
+
schema.profile_id = rows[0].profile_id;
|
|
8180
|
+
schema.profile_environment = rows[0].environment;
|
|
8181
|
+
}
|
|
8182
|
+
}
|
|
7241
8183
|
} finally {
|
|
7242
8184
|
database.close();
|
|
7243
8185
|
}
|
|
@@ -7246,17 +8188,937 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
7246
8188
|
}
|
|
7247
8189
|
}
|
|
7248
8190
|
return {
|
|
7249
|
-
|
|
8191
|
+
asset_root: repoRoot,
|
|
8192
|
+
channel,
|
|
7250
8193
|
database: databaseInfo,
|
|
7251
8194
|
fetchedAt: nowIso(),
|
|
7252
8195
|
git_sha: gitSha(),
|
|
7253
8196
|
node_env: process.env.NODE_ENV,
|
|
7254
8197
|
package_name: info.name,
|
|
8198
|
+
profile: runtimeProfileIdentity(channel),
|
|
8199
|
+
schema,
|
|
7255
8200
|
version: info.version
|
|
7256
8201
|
};
|
|
7257
8202
|
}
|
|
7258
8203
|
|
|
8204
|
+
// src/server/managedWriterRouting.ts
|
|
8205
|
+
var managedWriterRequestSchemaVersion = "lineage.managed_writer_request.v1";
|
|
8206
|
+
var managedWriterResponseSchemaVersion = "lineage.managed_writer_response.v1";
|
|
8207
|
+
var managedWriterRoute = "/api/managed-writer/execute";
|
|
8208
|
+
var delegationHeader = "X-Lineage-Writer-Delegation";
|
|
8209
|
+
var ManagedWriterRoutingError = class extends Error {
|
|
8210
|
+
constructor(message, status = 409, options) {
|
|
8211
|
+
super(message, options);
|
|
8212
|
+
this.status = status;
|
|
8213
|
+
this.name = "ManagedWriterRoutingError";
|
|
8214
|
+
}
|
|
8215
|
+
status;
|
|
8216
|
+
};
|
|
8217
|
+
function isManagedWriterRoutingError(error) {
|
|
8218
|
+
return error instanceof ManagedWriterRoutingError;
|
|
8219
|
+
}
|
|
8220
|
+
function requestBody(value) {
|
|
8221
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8222
|
+
throw new ManagedWriterRoutingError("Managed writer request must be a JSON object", 400);
|
|
8223
|
+
}
|
|
8224
|
+
const body = value;
|
|
8225
|
+
if (body.schema_version !== managedWriterRequestSchemaVersion) {
|
|
8226
|
+
throw new ManagedWriterRoutingError(`Unsupported managed writer request schema: ${String(body.schema_version)}`, 400);
|
|
8227
|
+
}
|
|
8228
|
+
if (typeof body.command !== "string" || !body.command || !Array.isArray(body.args) || body.args.some((arg) => typeof arg !== "string")) {
|
|
8229
|
+
throw new ManagedWriterRoutingError("Managed writer request requires a command and string args", 400);
|
|
8230
|
+
}
|
|
8231
|
+
if (typeof body.profile_id !== "string" || typeof body.channel !== "string" || typeof body.environment !== "string" || typeof body.service_origin !== "string") {
|
|
8232
|
+
throw new ManagedWriterRoutingError("Managed writer request requires profile identity", 400);
|
|
8233
|
+
}
|
|
8234
|
+
return body;
|
|
8235
|
+
}
|
|
8236
|
+
function containsProtectedOverride(args) {
|
|
8237
|
+
return args.some((arg) => arg === "--db" || arg.startsWith("--db=") || arg === "--asset-root" || arg.startsWith("--asset-root=") || arg === "--profile" || arg.startsWith("--profile="));
|
|
8238
|
+
}
|
|
8239
|
+
function registerManagedWriterRoute(app2, fields) {
|
|
8240
|
+
app2.post(managedWriterRoute, (req, res, next) => {
|
|
8241
|
+
try {
|
|
8242
|
+
if (!fields.profile || !fields.writerLease) {
|
|
8243
|
+
throw new ManagedWriterRoutingError("Managed writer delegation is unavailable for an unprofiled service", 404);
|
|
8244
|
+
}
|
|
8245
|
+
if (!fields.writerLease.authenticate(req.header(delegationHeader))) {
|
|
8246
|
+
throw new ManagedWriterRoutingError("Managed writer delegation was not authorized", 401);
|
|
8247
|
+
}
|
|
8248
|
+
const body = requestBody(req.body);
|
|
8249
|
+
if (body.profile_id !== fields.profile.profile_id || body.channel !== fields.channel || body.environment !== fields.profile.environment || body.service_origin !== fields.profile.service_origin) {
|
|
8250
|
+
throw new ManagedWriterRoutingError("Managed writer request profile/service identity does not match this service", 409);
|
|
8251
|
+
}
|
|
8252
|
+
if (containsProtectedOverride(body.args)) {
|
|
8253
|
+
throw new ManagedWriterRoutingError("Managed writer requests cannot override profile, database, or asset root", 400);
|
|
8254
|
+
}
|
|
8255
|
+
if (!fields.accepts(body.command, body.args)) {
|
|
8256
|
+
throw new ManagedWriterRoutingError(`Unsupported managed writer command: ${body.command}`, 400);
|
|
8257
|
+
}
|
|
8258
|
+
const result = fields.execute(body.command, body.args);
|
|
8259
|
+
const response = {
|
|
8260
|
+
ok: true,
|
|
8261
|
+
result,
|
|
8262
|
+
schema_version: managedWriterResponseSchemaVersion,
|
|
8263
|
+
service: {
|
|
8264
|
+
channel: fields.channel,
|
|
8265
|
+
environment: fields.profile.environment,
|
|
8266
|
+
profile_id: fields.profile.profile_id,
|
|
8267
|
+
service_origin: fields.profile.service_origin
|
|
8268
|
+
}
|
|
8269
|
+
};
|
|
8270
|
+
res.json(response);
|
|
8271
|
+
} catch (error) {
|
|
8272
|
+
next(error);
|
|
8273
|
+
}
|
|
8274
|
+
});
|
|
8275
|
+
}
|
|
8276
|
+
|
|
8277
|
+
// src/cli/lineageCli.ts
|
|
8278
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8279
|
+
import { dirname as dirname6, join as join13, resolve as resolve8 } from "node:path";
|
|
8280
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8281
|
+
|
|
8282
|
+
// src/server/lineageSelectionPacket.ts
|
|
8283
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
8284
|
+
import { existsSync as existsSync12, statSync as statSync6 } from "node:fs";
|
|
8285
|
+
import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
|
|
8286
|
+
var LineageSelectionPacketError = class extends Error {
|
|
8287
|
+
constructor(message, warnings = [], errors = [], diagnostics = []) {
|
|
8288
|
+
super(message);
|
|
8289
|
+
this.warnings = warnings;
|
|
8290
|
+
this.errors = errors;
|
|
8291
|
+
this.diagnostics = diagnostics;
|
|
8292
|
+
}
|
|
8293
|
+
warnings;
|
|
8294
|
+
errors;
|
|
8295
|
+
diagnostics;
|
|
8296
|
+
};
|
|
8297
|
+
function listAllAssets(project) {
|
|
8298
|
+
const pageSize = 100;
|
|
8299
|
+
const first = listAssets(project, { page: 1, pageSize, source: "all" });
|
|
8300
|
+
const assets = [...first.assets];
|
|
8301
|
+
for (let page = 2; page <= first.pagination.totalPages; page += 1) {
|
|
8302
|
+
assets.push(...listAssets(project, { page, pageSize, source: "all" }).assets);
|
|
8303
|
+
}
|
|
8304
|
+
return assets;
|
|
8305
|
+
}
|
|
8306
|
+
function resolveWorkspace(project, options) {
|
|
8307
|
+
if (options.rootAssetId && options.workspaceId) {
|
|
8308
|
+
throw new LineageSelectionPacketError("Use either --root or --workspace, not both.", [], ["ambiguous_workspace"]);
|
|
8309
|
+
}
|
|
8310
|
+
const snapshot = listLineageWorkspaces(project);
|
|
8311
|
+
const target = options.workspaceId || options.rootAssetId;
|
|
8312
|
+
const workspace = target ? snapshot.workspaces.find((item) => item.id === target || item.root_asset_id === target) : snapshot.active_workspace;
|
|
8313
|
+
if (workspace) return workspace;
|
|
8314
|
+
if (options.rootAssetId) {
|
|
8315
|
+
return {
|
|
8316
|
+
id: lineageWorkspaceId(project, options.rootAssetId),
|
|
8317
|
+
project,
|
|
8318
|
+
root_asset_id: options.rootAssetId,
|
|
8319
|
+
title: `${options.rootAssetId} lineage`,
|
|
8320
|
+
status: "active",
|
|
8321
|
+
created_by: "system",
|
|
8322
|
+
created_at: "",
|
|
8323
|
+
updated_at: ""
|
|
8324
|
+
};
|
|
8325
|
+
}
|
|
8326
|
+
const message = options.workspaceId ? `Unknown lineage workspace: ${options.workspaceId}` : "No active lineage workspace. Pass --root or activate a workspace first.";
|
|
8327
|
+
throw new LineageSelectionPacketError(message, [], [options.workspaceId ? "unknown_workspace" : "missing_active_workspace"]);
|
|
8328
|
+
}
|
|
8329
|
+
function resolveLocalReference(reference) {
|
|
8330
|
+
if (!reference) return void 0;
|
|
8331
|
+
if (isAbsolute2(reference)) return reference;
|
|
8332
|
+
const repoRelative = resolve7(repoRoot, reference);
|
|
8333
|
+
if (existsSync12(repoRelative)) return repoRelative;
|
|
8334
|
+
return resolve7(repoRoot, ".asset-scratch", reference);
|
|
8335
|
+
}
|
|
8336
|
+
function fileSize(path) {
|
|
8337
|
+
if (!path || !existsSync12(path)) return void 0;
|
|
8338
|
+
try {
|
|
8339
|
+
return statSync6(path).size;
|
|
8340
|
+
} catch {
|
|
8341
|
+
return void 0;
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
function storageState(hasLocal, hasS3) {
|
|
8345
|
+
if (hasLocal && hasS3) return "local_and_s3";
|
|
8346
|
+
if (hasLocal) return "local_only";
|
|
8347
|
+
if (hasS3) return "s3_backed";
|
|
8348
|
+
return "unresolved";
|
|
8349
|
+
}
|
|
8350
|
+
function currentAttemptFor(node) {
|
|
8351
|
+
if (!node.current_attempt) return void 0;
|
|
8352
|
+
return {
|
|
8353
|
+
asset_id: node.current_attempt.asset_id,
|
|
8354
|
+
checksum_sha256: node.current_attempt.checksum_sha256,
|
|
8355
|
+
file_path: node.current_attempt.file_path,
|
|
8356
|
+
generation_job_id: node.current_attempt.generation_job_id,
|
|
8357
|
+
id: node.current_attempt.id,
|
|
8358
|
+
is_current: node.current_attempt.is_current,
|
|
8359
|
+
source: node.current_attempt.source
|
|
8360
|
+
};
|
|
8361
|
+
}
|
|
8362
|
+
function assetForNode(node, catalogAsset, warnings) {
|
|
8363
|
+
const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
|
|
8364
|
+
const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
|
|
8365
|
+
const localExists = absolutePath ? existsSync12(absolutePath) : false;
|
|
8366
|
+
const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
|
|
8367
|
+
const s3Key = catalogAsset?.s3?.key || node.s3_key;
|
|
8368
|
+
const hasS3 = Boolean(s3Key);
|
|
8369
|
+
const checksum = catalogAsset?.local?.checksum_sha256 || node.current_attempt?.checksum_sha256 || node.checksum_sha256 || catalogAsset?.s3?.checksum_sha256;
|
|
8370
|
+
const mimeType = catalogAsset?.local?.content_type || catalogAsset?.s3?.content_type;
|
|
8371
|
+
const mediaType = catalogAsset?.content_type || node.media_type;
|
|
8372
|
+
if (hasLocalClaim && !localExists) warnings.push(`Selected asset ${node.asset_id} has a local path but the file is missing: ${absolutePath || node.local_path}`);
|
|
8373
|
+
if (!hasLocalClaim && !hasS3) warnings.push(`Selected asset ${node.asset_id} has neither a local file nor S3 key.`);
|
|
8374
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
8375
|
+
return {
|
|
8376
|
+
asset_id: node.asset_id,
|
|
8377
|
+
campaign: catalogAsset?.campaign || node.campaign,
|
|
8378
|
+
channel: catalogAsset?.channel || node.channel,
|
|
8379
|
+
checksum_sha256: checksum,
|
|
8380
|
+
current_attempt: currentAttemptFor(node),
|
|
8381
|
+
local: {
|
|
8382
|
+
absolute_path: absolutePath,
|
|
8383
|
+
content_type: catalogAsset?.local?.content_type,
|
|
8384
|
+
exists: localExists,
|
|
8385
|
+
relative_path: catalogAsset?.local?.relative_path || node.local_path || node.current_attempt?.file_path,
|
|
8386
|
+
size_bytes: catalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
8387
|
+
},
|
|
8388
|
+
media_type: mediaType,
|
|
8389
|
+
mime_type: mimeType,
|
|
8390
|
+
review_notes: node.review_notes,
|
|
8391
|
+
review_state: node.review_state,
|
|
8392
|
+
s3: {
|
|
8393
|
+
bucket: catalogAsset?.s3?.bucket,
|
|
8394
|
+
checksum_sha256: catalogAsset?.s3?.checksum_sha256,
|
|
8395
|
+
content_type: catalogAsset?.s3?.content_type,
|
|
8396
|
+
etag: catalogAsset?.s3?.etag,
|
|
8397
|
+
key: s3Key,
|
|
8398
|
+
region: catalogAsset?.s3?.region,
|
|
8399
|
+
size_bytes: catalogAsset?.s3?.size_bytes,
|
|
8400
|
+
version_id: catalogAsset?.s3?.version_id
|
|
8401
|
+
},
|
|
8402
|
+
selection_note: node.selection_note,
|
|
8403
|
+
source: catalogAsset?.source || node.source,
|
|
8404
|
+
status: catalogAsset?.status || node.status,
|
|
8405
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
8406
|
+
title: catalogAsset?.title || node.title || node.asset_id
|
|
8407
|
+
};
|
|
8408
|
+
}
|
|
8409
|
+
function packetId(packetIdentity) {
|
|
8410
|
+
const digest = createHash5("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
|
|
8411
|
+
return `lineage_packet_${digest}`;
|
|
8412
|
+
}
|
|
8413
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
8414
|
+
function canonicalValue(value) {
|
|
8415
|
+
if (Array.isArray(value)) return value.map((item) => canonicalValue(item));
|
|
8416
|
+
if (value && typeof value === "object") {
|
|
8417
|
+
return Object.fromEntries(
|
|
8418
|
+
Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonicalValue(item)])
|
|
8419
|
+
);
|
|
8420
|
+
}
|
|
8421
|
+
return value;
|
|
8422
|
+
}
|
|
8423
|
+
function canonicalLineageSelectionPacketIdentityJson(projection) {
|
|
8424
|
+
return JSON.stringify(canonicalValue(projection));
|
|
8425
|
+
}
|
|
8426
|
+
function lineageSelectionPacketV2IdentityProjection(packet) {
|
|
8427
|
+
const assetsById = new Map(packet.assets.map((asset) => [asset.asset_id, asset]));
|
|
8428
|
+
const selection = packet.selection.items.map((item) => {
|
|
8429
|
+
const asset = assetsById.get(item.asset_id);
|
|
8430
|
+
if (!asset) {
|
|
8431
|
+
const diagnostics = [{ asset_id: item.asset_id, code: "selected_asset_missing", severity: "error" }];
|
|
8432
|
+
throw new LineageSelectionPacketError(
|
|
8433
|
+
`Selected asset ${item.asset_id} is absent from the v2 packet asset envelope.`,
|
|
8434
|
+
[],
|
|
8435
|
+
["selected_asset_missing"],
|
|
8436
|
+
diagnostics
|
|
8437
|
+
);
|
|
8438
|
+
}
|
|
8439
|
+
return {
|
|
8440
|
+
asset_id: item.asset_id,
|
|
8441
|
+
campaign: asset.campaign,
|
|
8442
|
+
channel: asset.channel,
|
|
8443
|
+
current_attempt: {
|
|
8444
|
+
asset_id: asset.current_attempt.asset_id,
|
|
8445
|
+
attempt_index: asset.current_attempt.attempt_index,
|
|
8446
|
+
checksum_sha256: asset.current_attempt.checksum_sha256,
|
|
8447
|
+
id: asset.current_attempt.id,
|
|
8448
|
+
source: asset.current_attempt.source
|
|
8449
|
+
},
|
|
8450
|
+
media_type: asset.media_type,
|
|
8451
|
+
mime_type: asset.mime_type,
|
|
8452
|
+
position: item.position,
|
|
8453
|
+
selection_note: item.selection_note,
|
|
8454
|
+
title: asset.title
|
|
8455
|
+
};
|
|
8456
|
+
});
|
|
8457
|
+
return {
|
|
8458
|
+
schema_version: "lineage.selection_packet.v2",
|
|
8459
|
+
project: packet.project,
|
|
8460
|
+
product: packet.product,
|
|
8461
|
+
workspace: {
|
|
8462
|
+
id: packet.workspace.id,
|
|
8463
|
+
root_asset_id: packet.workspace.root_asset_id
|
|
8464
|
+
},
|
|
8465
|
+
context: {
|
|
8466
|
+
campaign: packet.context.campaign,
|
|
8467
|
+
channel: packet.context.channel,
|
|
8468
|
+
labels: packet.context.labels,
|
|
8469
|
+
notes: packet.context.notes
|
|
8470
|
+
},
|
|
8471
|
+
selection,
|
|
8472
|
+
diagnostics: packet.diagnostics.map((diagnostic) => ({
|
|
8473
|
+
code: diagnostic.code,
|
|
8474
|
+
severity: diagnostic.severity,
|
|
8475
|
+
...diagnostic.asset_id ? { asset_id: diagnostic.asset_id } : {}
|
|
8476
|
+
}))
|
|
8477
|
+
};
|
|
8478
|
+
}
|
|
8479
|
+
function lineageSelectionPacketV2IdentitySha256(packet) {
|
|
8480
|
+
const projection = lineageSelectionPacketV2IdentityProjection(packet);
|
|
8481
|
+
return createHash5("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
|
|
8482
|
+
}
|
|
8483
|
+
function addDiagnostic(diagnostics, diagnostic) {
|
|
8484
|
+
if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
|
|
8485
|
+
diagnostics.push(diagnostic);
|
|
8486
|
+
}
|
|
8487
|
+
function requireV2CurrentAttempt(node, warnings, diagnostics) {
|
|
8488
|
+
const attempt = node.current_attempt;
|
|
8489
|
+
if (!attempt) {
|
|
8490
|
+
const message = `Selected asset ${node.asset_id} has no current attempt identity.`;
|
|
8491
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_missing", severity: "error" };
|
|
8492
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_missing"], [...diagnostics, diagnostic]);
|
|
8493
|
+
}
|
|
8494
|
+
if (!attempt.id || !attempt.asset_id || !Number.isInteger(attempt.attempt_index) || attempt.attempt_index <= 0 || !attempt.source || attempt.is_current !== true) {
|
|
8495
|
+
const message = `Selected asset ${node.asset_id} has malformed current attempt identity.`;
|
|
8496
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_identity", severity: "error" };
|
|
8497
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_identity"], [...diagnostics, diagnostic]);
|
|
8498
|
+
}
|
|
8499
|
+
if (!attempt.checksum_sha256 || !SHA256_PATTERN.test(attempt.checksum_sha256)) {
|
|
8500
|
+
const message = `Selected asset ${node.asset_id} current attempt does not have a valid lowercase SHA-256 checksum.`;
|
|
8501
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_checksum", severity: "error" };
|
|
8502
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_checksum"], [...diagnostics, diagnostic]);
|
|
8503
|
+
}
|
|
8504
|
+
return {
|
|
8505
|
+
asset_id: attempt.asset_id,
|
|
8506
|
+
attempt_index: attempt.attempt_index,
|
|
8507
|
+
checksum_sha256: attempt.checksum_sha256,
|
|
8508
|
+
file_path: attempt.file_path,
|
|
8509
|
+
generation_job_id: attempt.generation_job_id,
|
|
8510
|
+
id: attempt.id,
|
|
8511
|
+
is_current: attempt.is_current,
|
|
8512
|
+
source: attempt.source
|
|
8513
|
+
};
|
|
8514
|
+
}
|
|
8515
|
+
function assetForNodeV2(node, visibleCatalogAsset, catalogById, warnings, errors, diagnostics) {
|
|
8516
|
+
const currentAttempt = requireV2CurrentAttempt(node, warnings, diagnostics);
|
|
8517
|
+
const currentAttemptCatalogAsset = catalogById.get(currentAttempt.asset_id);
|
|
8518
|
+
const isVisibleNodeAttempt = currentAttempt.asset_id === node.asset_id;
|
|
8519
|
+
const mediaCatalogAsset = currentAttemptCatalogAsset || (isVisibleNodeAttempt ? visibleCatalogAsset : void 0);
|
|
8520
|
+
const localReference = currentAttempt.file_path || mediaCatalogAsset?.local?.absolute_path || mediaCatalogAsset?.local?.relative_path || (isVisibleNodeAttempt ? node.local_path : void 0);
|
|
8521
|
+
const absolutePath = resolveLocalReference(localReference);
|
|
8522
|
+
const localExists = absolutePath ? existsSync12(absolutePath) : false;
|
|
8523
|
+
const hasLocalClaim = Boolean(localReference);
|
|
8524
|
+
const s3Key = mediaCatalogAsset?.s3?.key || (isVisibleNodeAttempt ? node.s3_key : void 0);
|
|
8525
|
+
const hasS3 = Boolean(s3Key);
|
|
8526
|
+
const mimeType = mediaCatalogAsset?.local?.content_type || mediaCatalogAsset?.s3?.content_type || visibleCatalogAsset?.local?.content_type || visibleCatalogAsset?.s3?.content_type || (localReference ? contentTypeFor(localReference) : void 0);
|
|
8527
|
+
const mediaType = mediaCatalogAsset?.content_type || visibleCatalogAsset?.content_type || node.media_type;
|
|
8528
|
+
if (localExists && absolutePath && fileSha256(absolutePath) !== currentAttempt.checksum_sha256) {
|
|
8529
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its local file.`;
|
|
8530
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
8531
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
8532
|
+
}
|
|
8533
|
+
const s3Checksum = mediaCatalogAsset?.s3?.checksum_sha256;
|
|
8534
|
+
if (hasS3 && s3Checksum && s3Checksum !== currentAttempt.checksum_sha256) {
|
|
8535
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its S3 media envelope.`;
|
|
8536
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
8537
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
8538
|
+
}
|
|
8539
|
+
if (hasLocalClaim && !localExists) {
|
|
8540
|
+
warnings.push(`Selected asset ${node.asset_id} current attempt has a local path but the file is missing: ${absolutePath || localReference}`);
|
|
8541
|
+
}
|
|
8542
|
+
if (!hasLocalClaim && !hasS3) {
|
|
8543
|
+
const message = `Selected asset ${node.asset_id} current attempt has neither a local file nor S3 key.`;
|
|
8544
|
+
warnings.push(message);
|
|
8545
|
+
errors.push(message);
|
|
8546
|
+
}
|
|
8547
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") {
|
|
8548
|
+
warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
8549
|
+
addDiagnostic(diagnostics, { asset_id: node.asset_id, code: "unsupported_media_type", severity: "warning" });
|
|
8550
|
+
}
|
|
8551
|
+
const conflictingChecksums = [
|
|
8552
|
+
currentAttemptCatalogAsset?.local?.checksum_sha256,
|
|
8553
|
+
currentAttemptCatalogAsset?.s3?.checksum_sha256,
|
|
8554
|
+
visibleCatalogAsset?.local?.checksum_sha256,
|
|
8555
|
+
visibleCatalogAsset?.s3?.checksum_sha256,
|
|
8556
|
+
node.checksum_sha256
|
|
8557
|
+
].filter((checksum) => Boolean(checksum && checksum !== currentAttempt.checksum_sha256));
|
|
8558
|
+
if (conflictingChecksums.length > 0) {
|
|
8559
|
+
warnings.push(`Selected asset ${node.asset_id} catalog or visible-node checksum differs from its current attempt; current attempt checksum is authoritative.`);
|
|
8560
|
+
}
|
|
8561
|
+
return {
|
|
8562
|
+
asset_id: node.asset_id,
|
|
8563
|
+
campaign: visibleCatalogAsset?.campaign || node.campaign,
|
|
8564
|
+
channel: visibleCatalogAsset?.channel || node.channel,
|
|
8565
|
+
checksum_sha256: currentAttempt.checksum_sha256,
|
|
8566
|
+
current_attempt: currentAttempt,
|
|
8567
|
+
local: {
|
|
8568
|
+
absolute_path: absolutePath,
|
|
8569
|
+
content_type: mediaCatalogAsset?.local?.content_type,
|
|
8570
|
+
exists: localExists,
|
|
8571
|
+
relative_path: mediaCatalogAsset?.local?.relative_path || currentAttempt.file_path || (isVisibleNodeAttempt ? node.local_path : void 0),
|
|
8572
|
+
size_bytes: mediaCatalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
8573
|
+
},
|
|
8574
|
+
media_type: mediaType,
|
|
8575
|
+
mime_type: mimeType,
|
|
8576
|
+
review_notes: node.review_notes,
|
|
8577
|
+
review_state: node.review_state,
|
|
8578
|
+
s3: {
|
|
8579
|
+
bucket: mediaCatalogAsset?.s3?.bucket,
|
|
8580
|
+
checksum_sha256: mediaCatalogAsset?.s3?.checksum_sha256,
|
|
8581
|
+
content_type: mediaCatalogAsset?.s3?.content_type,
|
|
8582
|
+
etag: mediaCatalogAsset?.s3?.etag,
|
|
8583
|
+
key: s3Key,
|
|
8584
|
+
region: mediaCatalogAsset?.s3?.region,
|
|
8585
|
+
size_bytes: mediaCatalogAsset?.s3?.size_bytes,
|
|
8586
|
+
version_id: mediaCatalogAsset?.s3?.version_id
|
|
8587
|
+
},
|
|
8588
|
+
selection_note: node.selection_note,
|
|
8589
|
+
source: mediaCatalogAsset?.source || visibleCatalogAsset?.source || node.source,
|
|
8590
|
+
status: visibleCatalogAsset?.status || node.status,
|
|
8591
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
8592
|
+
title: visibleCatalogAsset?.title || node.title || node.asset_id
|
|
8593
|
+
};
|
|
8594
|
+
}
|
|
8595
|
+
function getLineageSelectionPacketV1(project, options = {}) {
|
|
8596
|
+
const workspace = resolveWorkspace(project, options);
|
|
8597
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
8598
|
+
const catalogSummary = validateProject(project);
|
|
8599
|
+
const catalogAssets = listAllAssets(project);
|
|
8600
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
8601
|
+
const warnings = [];
|
|
8602
|
+
const errors = [];
|
|
8603
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
8604
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
8605
|
+
asset_id: selection.asset_id,
|
|
8606
|
+
position: selection.position,
|
|
8607
|
+
selected_at: selection.selected_at,
|
|
8608
|
+
selection_note: selection.notes
|
|
8609
|
+
}));
|
|
8610
|
+
if (selectedItems.length === 0) warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
8611
|
+
const assets = selectedItems.flatMap((item) => {
|
|
8612
|
+
const node = nodeById.get(item.asset_id);
|
|
8613
|
+
if (!node) {
|
|
8614
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
8615
|
+
warnings.push(message);
|
|
8616
|
+
errors.push("selected_asset_outside_workspace");
|
|
8617
|
+
return [];
|
|
8618
|
+
}
|
|
8619
|
+
return [assetForNode(node, catalogById.get(item.asset_id), warnings)];
|
|
8620
|
+
});
|
|
8621
|
+
if (assets.some((asset) => !asset.dimensions)) warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
8622
|
+
if (options.strict) {
|
|
8623
|
+
const strictErrors = [
|
|
8624
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
8625
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
8626
|
+
...errors
|
|
8627
|
+
];
|
|
8628
|
+
if (strictErrors.length > 0) {
|
|
8629
|
+
throw new LineageSelectionPacketError(`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`, warnings, strictErrors);
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
const context = {
|
|
8633
|
+
campaign: options.campaign,
|
|
8634
|
+
channel: options.channel,
|
|
8635
|
+
labels: options.labels || [],
|
|
8636
|
+
notes: options.contextNotes
|
|
8637
|
+
};
|
|
8638
|
+
const identity = {
|
|
8639
|
+
assets: assets.map((asset) => ({
|
|
8640
|
+
asset_id: asset.asset_id,
|
|
8641
|
+
checksum_sha256: asset.checksum_sha256,
|
|
8642
|
+
local_path: asset.local.absolute_path,
|
|
8643
|
+
s3_key: asset.s3.key,
|
|
8644
|
+
storage_state: asset.storage_state
|
|
8645
|
+
})),
|
|
8646
|
+
context,
|
|
8647
|
+
project,
|
|
8648
|
+
root_asset_id: workspace.root_asset_id,
|
|
8649
|
+
schema_version: "lineage.selection_packet.v1",
|
|
8650
|
+
selection: selectedItems,
|
|
8651
|
+
workspace_id: workspace.id
|
|
8652
|
+
};
|
|
8653
|
+
return {
|
|
8654
|
+
assets,
|
|
8655
|
+
context,
|
|
8656
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8657
|
+
errors,
|
|
8658
|
+
kind: "lineage.selection_packet",
|
|
8659
|
+
packet_id: packetId(identity),
|
|
8660
|
+
product: catalogSummary.product,
|
|
8661
|
+
project,
|
|
8662
|
+
schema_version: "lineage.selection_packet.v1",
|
|
8663
|
+
selection: {
|
|
8664
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
8665
|
+
count: selectedItems.length,
|
|
8666
|
+
items: selectedItems,
|
|
8667
|
+
root_asset_id: workspace.root_asset_id
|
|
8668
|
+
},
|
|
8669
|
+
source: {
|
|
8670
|
+
app: "lineage",
|
|
8671
|
+
command: options.command,
|
|
8672
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
8673
|
+
package: options.packageVersion
|
|
8674
|
+
},
|
|
8675
|
+
warnings: [...new Set(warnings)],
|
|
8676
|
+
workspace: {
|
|
8677
|
+
active_at: workspace.active_at,
|
|
8678
|
+
id: workspace.id,
|
|
8679
|
+
notes: workspace.notes,
|
|
8680
|
+
root_asset_id: workspace.root_asset_id,
|
|
8681
|
+
status: workspace.status,
|
|
8682
|
+
title: workspace.title
|
|
8683
|
+
}
|
|
8684
|
+
};
|
|
8685
|
+
}
|
|
8686
|
+
function getLineageSelectionPacketV2(project, options) {
|
|
8687
|
+
const workspace = resolveWorkspace(project, options);
|
|
8688
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
8689
|
+
const catalogSummary = validateProject(project);
|
|
8690
|
+
const catalogAssets = listAllAssets(project);
|
|
8691
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
8692
|
+
const warnings = [];
|
|
8693
|
+
const errors = [];
|
|
8694
|
+
const diagnostics = [];
|
|
8695
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
8696
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
8697
|
+
asset_id: selection.asset_id,
|
|
8698
|
+
position: selection.position,
|
|
8699
|
+
selected_at: selection.selected_at,
|
|
8700
|
+
selection_note: selection.notes
|
|
8701
|
+
}));
|
|
8702
|
+
if (selectedItems.length === 0) {
|
|
8703
|
+
warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
8704
|
+
addDiagnostic(diagnostics, { code: "empty_selection", severity: "warning" });
|
|
8705
|
+
}
|
|
8706
|
+
const assets = [];
|
|
8707
|
+
for (const item of selectedItems) {
|
|
8708
|
+
const node = nodeById.get(item.asset_id);
|
|
8709
|
+
if (!node) {
|
|
8710
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
8711
|
+
warnings.push(message);
|
|
8712
|
+
errors.push(message);
|
|
8713
|
+
const diagnostic = { asset_id: item.asset_id, code: "selected_asset_outside_workspace", severity: "error" };
|
|
8714
|
+
addDiagnostic(diagnostics, diagnostic);
|
|
8715
|
+
throw new LineageSelectionPacketError(message, warnings, ["selected_asset_outside_workspace"], diagnostics);
|
|
8716
|
+
}
|
|
8717
|
+
assets.push(assetForNodeV2(node, catalogById.get(item.asset_id), catalogById, warnings, errors, diagnostics));
|
|
8718
|
+
}
|
|
8719
|
+
if (assets.some((asset) => !asset.dimensions)) {
|
|
8720
|
+
warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
8721
|
+
}
|
|
8722
|
+
if (options.strict) {
|
|
8723
|
+
const strictErrors = [
|
|
8724
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
8725
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
8726
|
+
...assets.filter((asset) => asset.storage_state === "unresolved").map((asset) => `unresolved_current_attempt_media:${asset.asset_id}`),
|
|
8727
|
+
...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => `${diagnostic.code}${diagnostic.asset_id ? `:${diagnostic.asset_id}` : ""}`)
|
|
8728
|
+
];
|
|
8729
|
+
if (strictErrors.length > 0) {
|
|
8730
|
+
throw new LineageSelectionPacketError(
|
|
8731
|
+
`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`,
|
|
8732
|
+
warnings,
|
|
8733
|
+
strictErrors,
|
|
8734
|
+
diagnostics
|
|
8735
|
+
);
|
|
8736
|
+
}
|
|
8737
|
+
}
|
|
8738
|
+
const packet = {
|
|
8739
|
+
assets,
|
|
8740
|
+
context: {
|
|
8741
|
+
campaign: options.campaign,
|
|
8742
|
+
channel: options.channel,
|
|
8743
|
+
labels: options.labels || [],
|
|
8744
|
+
notes: options.contextNotes
|
|
8745
|
+
},
|
|
8746
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8747
|
+
diagnostics,
|
|
8748
|
+
errors: [...new Set(errors)],
|
|
8749
|
+
identity_sha256: "",
|
|
8750
|
+
kind: "lineage.selection_packet",
|
|
8751
|
+
packet_id: "",
|
|
8752
|
+
product: catalogSummary.product,
|
|
8753
|
+
project,
|
|
8754
|
+
schema_version: "lineage.selection_packet.v2",
|
|
8755
|
+
selection: {
|
|
8756
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
8757
|
+
count: selectedItems.length,
|
|
8758
|
+
items: selectedItems,
|
|
8759
|
+
root_asset_id: workspace.root_asset_id
|
|
8760
|
+
},
|
|
8761
|
+
source: {
|
|
8762
|
+
app: "lineage",
|
|
8763
|
+
command: options.command,
|
|
8764
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
8765
|
+
package: options.packageVersion
|
|
8766
|
+
},
|
|
8767
|
+
warnings: [...new Set(warnings)],
|
|
8768
|
+
workspace: {
|
|
8769
|
+
active_at: workspace.active_at,
|
|
8770
|
+
id: workspace.id,
|
|
8771
|
+
notes: workspace.notes,
|
|
8772
|
+
root_asset_id: workspace.root_asset_id,
|
|
8773
|
+
status: workspace.status,
|
|
8774
|
+
title: workspace.title
|
|
8775
|
+
}
|
|
8776
|
+
};
|
|
8777
|
+
packet.identity_sha256 = lineageSelectionPacketV2IdentitySha256(packet);
|
|
8778
|
+
packet.packet_id = `lineage_packet_${packet.identity_sha256.slice(0, 24)}`;
|
|
8779
|
+
return packet;
|
|
8780
|
+
}
|
|
8781
|
+
function getLineageSelectionPacket(project, options = {}) {
|
|
8782
|
+
return options.schema === "v2" ? getLineageSelectionPacketV2(project, options) : getLineageSelectionPacketV1(project, options);
|
|
8783
|
+
}
|
|
8784
|
+
|
|
8785
|
+
// src/cli/lineageCli.ts
|
|
8786
|
+
function packageRoot2() {
|
|
8787
|
+
return resolve8(dirname6(fileURLToPath2(import.meta.url)), "..", "..");
|
|
8788
|
+
}
|
|
8789
|
+
function packageVersion() {
|
|
8790
|
+
try {
|
|
8791
|
+
const packageInfo2 = JSON.parse(readFileSync8(join13(packageRoot2(), "package.json"), "utf8"));
|
|
8792
|
+
return packageInfo2.version || "0.0.0";
|
|
8793
|
+
} catch {
|
|
8794
|
+
return "0.0.0";
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
function readOption(args, name) {
|
|
8798
|
+
const prefix = `${name}=`;
|
|
8799
|
+
const inline = args.find((arg) => arg.startsWith(prefix));
|
|
8800
|
+
if (inline) return inline.slice(prefix.length);
|
|
8801
|
+
const index = args.indexOf(name);
|
|
8802
|
+
if (index >= 0) return args[index + 1];
|
|
8803
|
+
return void 0;
|
|
8804
|
+
}
|
|
8805
|
+
function readOptions(args, name) {
|
|
8806
|
+
const values = [];
|
|
8807
|
+
const prefix = `${name}=`;
|
|
8808
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8809
|
+
const arg = args[index];
|
|
8810
|
+
if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length));
|
|
8811
|
+
else if (arg === name && args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
8812
|
+
values.push(args[index + 1]);
|
|
8813
|
+
index += 1;
|
|
8814
|
+
}
|
|
8815
|
+
}
|
|
8816
|
+
return values;
|
|
8817
|
+
}
|
|
8818
|
+
function resolveCliAssetRoot(args) {
|
|
8819
|
+
return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
8820
|
+
}
|
|
8821
|
+
function configureCliAssetRoot(args) {
|
|
8822
|
+
const assetRoot = resolveCliAssetRoot(args);
|
|
8823
|
+
process.env.LINEAGE_ASSET_ROOT = assetRoot;
|
|
8824
|
+
return setLineageAssetRoot(assetRoot);
|
|
8825
|
+
}
|
|
8826
|
+
function positionalArgs(args) {
|
|
8827
|
+
const values = [];
|
|
8828
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8829
|
+
const arg = args[index];
|
|
8830
|
+
if (arg.startsWith("--")) {
|
|
8831
|
+
if (!arg.includes("=") && args[index + 1] && !args[index + 1].startsWith("--")) index += 1;
|
|
8832
|
+
continue;
|
|
8833
|
+
}
|
|
8834
|
+
values.push(arg);
|
|
8835
|
+
}
|
|
8836
|
+
return values;
|
|
8837
|
+
}
|
|
8838
|
+
function resolveDataCommandOptions(args) {
|
|
8839
|
+
configureCliAssetRoot(args);
|
|
8840
|
+
const positions = positionalArgs(args);
|
|
8841
|
+
const options = {
|
|
8842
|
+
assetId: readOption(args, "--asset-id") || positions[0],
|
|
8843
|
+
childAssetId: readOption(args, "--child"),
|
|
8844
|
+
claimToken: readOption(args, "--claim-token") || process.env.LINEAGE_CLAIM_TOKEN,
|
|
8845
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
8846
|
+
dbPath: readOption(args, "--db"),
|
|
8847
|
+
json: args.includes("--json"),
|
|
8848
|
+
project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
|
|
8849
|
+
rootAssetId: readOption(args, "--root")
|
|
8850
|
+
};
|
|
8851
|
+
if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
|
|
8852
|
+
return options;
|
|
8853
|
+
}
|
|
8854
|
+
function runLineageDataCommand(command, args) {
|
|
8855
|
+
const options = resolveDataCommandOptions(args);
|
|
8856
|
+
if (command === "next") return getLineageNextAsset(options.project, options.rootAssetId || options.assetId);
|
|
8857
|
+
if (command === "brief") return getLineageBrief(options.project, options.rootAssetId || options.assetId);
|
|
8858
|
+
if (command === "inspect") {
|
|
8859
|
+
if (!options.assetId) throw new Error("lineage inspect requires --asset-id");
|
|
8860
|
+
return getLineageSnapshot(options.project, options.assetId);
|
|
8861
|
+
}
|
|
8862
|
+
if (command === "selection") {
|
|
8863
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
8864
|
+
if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
|
|
8865
|
+
const schema = readOption(args, "--schema");
|
|
8866
|
+
if (schema && schema !== "v2") throw new Error(`Unsupported selection packet schema: ${schema}. Omit --schema for v1 or pass --schema v2.`);
|
|
8867
|
+
const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
|
|
8868
|
+
const packet = getLineageSelectionPacket(options.project, {
|
|
8869
|
+
campaign: readOption(args, "--campaign"),
|
|
8870
|
+
channel: readOption(args, "--channel"),
|
|
8871
|
+
command: "lineage selection packet",
|
|
8872
|
+
contextNotes: readOption(args, "--context-notes") || readOption(args, "--notes"),
|
|
8873
|
+
dbPath: options.dbPath,
|
|
8874
|
+
labels,
|
|
8875
|
+
packageVersion: packageVersion(),
|
|
8876
|
+
rootAssetId: options.rootAssetId,
|
|
8877
|
+
schema: schema === "v2" ? "v2" : void 0,
|
|
8878
|
+
strict: args.includes("--strict"),
|
|
8879
|
+
workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
|
|
8880
|
+
});
|
|
8881
|
+
const out = readOption(args, "--out");
|
|
8882
|
+
if (out) {
|
|
8883
|
+
const outPath = resolve8(out);
|
|
8884
|
+
mkdirSync8(dirname6(outPath), { recursive: true });
|
|
8885
|
+
writeFileSync5(outPath, `${JSON.stringify(packet, null, 2)}
|
|
8886
|
+
`);
|
|
8887
|
+
}
|
|
8888
|
+
return packet;
|
|
8889
|
+
}
|
|
8890
|
+
if (command === "link-child") {
|
|
8891
|
+
if (!options.childAssetId) throw new Error("lineage link-child requires --child");
|
|
8892
|
+
return linkSelectedLineageChild(options.project, {
|
|
8893
|
+
childAssetId: options.childAssetId,
|
|
8894
|
+
claimToken: options.claimToken,
|
|
8895
|
+
confirmWrite: options.confirmWrite,
|
|
8896
|
+
rootAssetId: options.rootAssetId || options.assetId
|
|
8897
|
+
});
|
|
8898
|
+
}
|
|
8899
|
+
if (command === "reroll") {
|
|
8900
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
8901
|
+
if (subcommand === "list") {
|
|
8902
|
+
if (!options.rootAssetId) throw new Error("lineage reroll list requires --root");
|
|
8903
|
+
return listLineageRerollRequests(options.project, options.rootAssetId);
|
|
8904
|
+
}
|
|
8905
|
+
if (subcommand === "mark") {
|
|
8906
|
+
const targetAssetId = readOption(args, "--target");
|
|
8907
|
+
const requestedBy = rerollRequestedBy(readOption(args, "--requested-by") || "agent");
|
|
8908
|
+
if (!options.rootAssetId) throw new Error("lineage reroll mark requires --root");
|
|
8909
|
+
if (!targetAssetId) throw new Error("lineage reroll mark requires --target");
|
|
8910
|
+
return markLineageRerollRequest(options.project, {
|
|
8911
|
+
rootAssetId: options.rootAssetId,
|
|
8912
|
+
nodeAssetId: targetAssetId,
|
|
8913
|
+
notes: readOption(args, "--notes"),
|
|
8914
|
+
requestedBy,
|
|
8915
|
+
confirmWrite: options.confirmWrite
|
|
8916
|
+
});
|
|
8917
|
+
}
|
|
8918
|
+
if (subcommand === "cancel") {
|
|
8919
|
+
const targetAssetId = readOption(args, "--target");
|
|
8920
|
+
if (!options.rootAssetId) throw new Error("lineage reroll cancel requires --root");
|
|
8921
|
+
if (!targetAssetId) throw new Error("lineage reroll cancel requires --target");
|
|
8922
|
+
return clearLineageRerollRequest(options.project, {
|
|
8923
|
+
rootAssetId: options.rootAssetId,
|
|
8924
|
+
nodeAssetId: targetAssetId,
|
|
8925
|
+
confirmWrite: options.confirmWrite
|
|
8926
|
+
});
|
|
8927
|
+
}
|
|
8928
|
+
if (subcommand === "plan") {
|
|
8929
|
+
const targetAssetId = readOption(args, "--target");
|
|
8930
|
+
const prompt = readOption(args, "--prompt");
|
|
8931
|
+
if (!options.rootAssetId) throw new Error("lineage reroll plan requires --root");
|
|
8932
|
+
if (!targetAssetId) throw new Error("lineage reroll plan requires --target");
|
|
8933
|
+
if (!prompt) throw new Error("lineage reroll plan requires --prompt");
|
|
8934
|
+
return planImageReroll(options.project, {
|
|
8935
|
+
rootAssetId: options.rootAssetId,
|
|
8936
|
+
targetAssetId,
|
|
8937
|
+
prompt,
|
|
8938
|
+
dryRun: args.includes("--dry-run")
|
|
8939
|
+
});
|
|
8940
|
+
}
|
|
8941
|
+
if (subcommand === "import") {
|
|
8942
|
+
const jobId2 = readOption(args, "--job-id");
|
|
8943
|
+
const file = readOption(args, "--file");
|
|
8944
|
+
if (!jobId2) throw new Error("lineage reroll import requires --job-id");
|
|
8945
|
+
if (!file) throw new Error("lineage reroll import requires --file");
|
|
8946
|
+
return importImageRerollOutput(options.project, { jobId: jobId2, file, confirmWrite: options.confirmWrite });
|
|
8947
|
+
}
|
|
8948
|
+
throw new Error(`Unknown reroll command: ${subcommand}`);
|
|
8949
|
+
}
|
|
8950
|
+
if (command === "tasks") {
|
|
8951
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
8952
|
+
const taskId = readOption(args, "--task");
|
|
8953
|
+
if (subcommand === "list") {
|
|
8954
|
+
if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
|
|
8955
|
+
return listLineageTasks(options.project, options.rootAssetId);
|
|
8956
|
+
}
|
|
8957
|
+
if (subcommand === "inspect") {
|
|
8958
|
+
if (!taskId) throw new Error("lineage tasks inspect requires --task");
|
|
8959
|
+
return getLineageTask(options.project, taskId);
|
|
8960
|
+
}
|
|
8961
|
+
if (subcommand === "claim") {
|
|
8962
|
+
const agentName = readOption(args, "--agent-name");
|
|
8963
|
+
if (!taskId) throw new Error("lineage tasks claim requires --task");
|
|
8964
|
+
if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
|
|
8965
|
+
return claimLineageTask(options.project, { taskId, agentName });
|
|
8966
|
+
}
|
|
8967
|
+
if (subcommand === "start") {
|
|
8968
|
+
if (!taskId) throw new Error("lineage tasks start requires --task");
|
|
8969
|
+
if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
|
|
8970
|
+
return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
|
|
8971
|
+
}
|
|
8972
|
+
if (subcommand === "comment") {
|
|
8973
|
+
const message = readOption(args, "--message");
|
|
8974
|
+
if (!taskId) throw new Error("lineage tasks comment requires --task");
|
|
8975
|
+
if (!message) throw new Error("lineage tasks comment requires --message");
|
|
8976
|
+
return addLineageTaskComment(options.project, {
|
|
8977
|
+
actor: readOption(args, "--actor") || "human",
|
|
8978
|
+
message,
|
|
8979
|
+
taskId
|
|
8980
|
+
});
|
|
8981
|
+
}
|
|
8982
|
+
if (subcommand === "cancel") {
|
|
8983
|
+
if (!taskId) throw new Error("lineage tasks cancel requires --task");
|
|
8984
|
+
return cancelLineageTask(options.project, {
|
|
8985
|
+
actor: readOption(args, "--actor") || "human",
|
|
8986
|
+
confirmWrite: options.confirmWrite,
|
|
8987
|
+
override: args.includes("--override"),
|
|
8988
|
+
taskId
|
|
8989
|
+
});
|
|
8990
|
+
}
|
|
8991
|
+
if (subcommand === "override") {
|
|
8992
|
+
const reason = readOption(args, "--reason");
|
|
8993
|
+
if (!taskId) throw new Error("lineage tasks override requires --task");
|
|
8994
|
+
if (!reason) throw new Error("lineage tasks override requires --reason");
|
|
8995
|
+
return overrideLineageTask(options.project, {
|
|
8996
|
+
actor: readOption(args, "--actor") || "human",
|
|
8997
|
+
instructions: readOption(args, "--instructions"),
|
|
8998
|
+
reason,
|
|
8999
|
+
taskId
|
|
9000
|
+
});
|
|
9001
|
+
}
|
|
9002
|
+
if (subcommand === "instructions") {
|
|
9003
|
+
const instructions = readOption(args, "--instructions");
|
|
9004
|
+
if (!taskId) throw new Error("lineage tasks instructions requires --task");
|
|
9005
|
+
if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
|
|
9006
|
+
return updateLineageTaskInstructions(options.project, { instructions, taskId });
|
|
9007
|
+
}
|
|
9008
|
+
throw new Error(`Unknown tasks command: ${subcommand}`);
|
|
9009
|
+
}
|
|
9010
|
+
throw new Error(`Unknown command: ${command}`);
|
|
9011
|
+
}
|
|
9012
|
+
function rerollRequestedBy(value) {
|
|
9013
|
+
if (value === "agent" || value === "human" || value === "system") return value;
|
|
9014
|
+
throw new Error(`Invalid re-roll requester: ${value}`);
|
|
9015
|
+
}
|
|
9016
|
+
function lineageCliCanDelegateMutation(command, args) {
|
|
9017
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
9018
|
+
if (command === "link-child") return true;
|
|
9019
|
+
if (command === "reroll") return ["mark", "cancel", "plan", "import"].includes(subcommand);
|
|
9020
|
+
if (command === "tasks") return ["claim", "start", "comment", "cancel", "override", "instructions"].includes(subcommand);
|
|
9021
|
+
if (command === "agent") return ["claim", "heartbeat", "release", "revoke", "transfer"].includes(subcommand);
|
|
9022
|
+
return false;
|
|
9023
|
+
}
|
|
9024
|
+
function executeDelegatedLineageMutation(command, args) {
|
|
9025
|
+
if (!lineageCliCanDelegateMutation(command, args)) throw new Error(`Unsupported managed writer command: ${command}`);
|
|
9026
|
+
if (command === "agent") {
|
|
9027
|
+
const agentCommand = args[0] || "";
|
|
9028
|
+
return runLineageAgentCommand(agentCommand, args.slice(1));
|
|
9029
|
+
}
|
|
9030
|
+
return runLineageDataCommand(command, args);
|
|
9031
|
+
}
|
|
9032
|
+
function runLineageAgentCommand(command, args) {
|
|
9033
|
+
configureCliAssetRoot(args);
|
|
9034
|
+
const dbPath = readOption(args, "--db");
|
|
9035
|
+
if (dbPath) process.env.LINEAGE_DB = dbPath;
|
|
9036
|
+
const project = readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct;
|
|
9037
|
+
const claimId = readOption(args, "--claim");
|
|
9038
|
+
const claimToken = readOption(args, "--claim-token") || process.env.LINEAGE_CLAIM_TOKEN;
|
|
9039
|
+
if (command === "claim") {
|
|
9040
|
+
return createAgentClaim({
|
|
9041
|
+
agentId: readOption(args, "--agent-id"),
|
|
9042
|
+
agentKind: readOption(args, "--agent-kind"),
|
|
9043
|
+
agentName: readOption(args, "--agent-name") || "",
|
|
9044
|
+
channel: readOption(args, "--channel"),
|
|
9045
|
+
force: args.includes("--force"),
|
|
9046
|
+
project,
|
|
9047
|
+
reason: readOption(args, "--reason"),
|
|
9048
|
+
scopeType: readOption(args, "--scope") || "",
|
|
9049
|
+
targetId: readOption(args, "--target") || "",
|
|
9050
|
+
targetTitle: readOption(args, "--target-title"),
|
|
9051
|
+
threadId: readOption(args, "--thread-id"),
|
|
9052
|
+
ttlSeconds: parseClaimTtl(readOption(args, "--ttl"))
|
|
9053
|
+
});
|
|
9054
|
+
}
|
|
9055
|
+
if (command === "status") return listAgentClaims(project);
|
|
9056
|
+
if (command === "graph") {
|
|
9057
|
+
const rootAssetId = readOption(args, "--root") || readOption(args, "--asset-id") || positionalArgs(args)[0];
|
|
9058
|
+
if (!rootAssetId) throw new Error("lineage agent graph requires --root");
|
|
9059
|
+
return getLineageSnapshot(project, rootAssetId);
|
|
9060
|
+
}
|
|
9061
|
+
if (command === "inspect") {
|
|
9062
|
+
if (!claimId) throw new Error("lineage agent inspect requires --claim");
|
|
9063
|
+
return inspectAgentClaim(claimId, project);
|
|
9064
|
+
}
|
|
9065
|
+
if (command === "heartbeat") {
|
|
9066
|
+
if (!claimToken) throw new Error("lineage agent heartbeat requires --claim-token");
|
|
9067
|
+
return heartbeatAgentClaim(claimToken, parseClaimTtl(readOption(args, "--ttl")));
|
|
9068
|
+
}
|
|
9069
|
+
if (command === "release") {
|
|
9070
|
+
if (!claimToken) throw new Error("lineage agent release requires --claim-token");
|
|
9071
|
+
return releaseAgentClaim(claimToken);
|
|
9072
|
+
}
|
|
9073
|
+
if (command === "revoke") {
|
|
9074
|
+
if (!claimId) throw new Error("lineage agent revoke requires --claim");
|
|
9075
|
+
return revokeAgentClaim(project, claimId, {
|
|
9076
|
+
actor: readOption(args, "--actor") || "human",
|
|
9077
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
9078
|
+
reason: readOption(args, "--reason")
|
|
9079
|
+
});
|
|
9080
|
+
}
|
|
9081
|
+
if (command === "transfer") {
|
|
9082
|
+
if (!claimId) throw new Error("lineage agent transfer requires --claim");
|
|
9083
|
+
return transferAgentClaim(project, claimId, {
|
|
9084
|
+
actor: readOption(args, "--actor") || "human",
|
|
9085
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
9086
|
+
reason: readOption(args, "--reason"),
|
|
9087
|
+
toAgentName: readOption(args, "--to-agent-name") || ""
|
|
9088
|
+
});
|
|
9089
|
+
}
|
|
9090
|
+
throw new Error(`Unknown agent command: ${command}`);
|
|
9091
|
+
}
|
|
9092
|
+
|
|
7259
9093
|
// src/server.ts
|
|
9094
|
+
var runtimeChannel = normalizeRuntimeChannel(process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
9095
|
+
assertRuntimeProfileSafety(runtimeChannel);
|
|
9096
|
+
var startupRuntime = getLineageRuntimeInfo({ channel: runtimeChannel });
|
|
9097
|
+
if (!process.env.LINEAGE_PROFILE) assertUnselectedDatabaseIsUnbound(startupRuntime);
|
|
9098
|
+
var startupProfile;
|
|
9099
|
+
if (process.env.LINEAGE_PROFILE) {
|
|
9100
|
+
if (!process.env.LINEAGE_PROFILE_ID || !process.env.LINEAGE_PROFILE_ENVIRONMENT || !process.env.LINEAGE_PROFILE_MANIFEST) {
|
|
9101
|
+
throw new Error("LINEAGE_PROFILE must be resolved by the Lineage CLI before server startup; use lineage start --profile or lineage-dev start --profile");
|
|
9102
|
+
}
|
|
9103
|
+
const doctor = doctorLineageProfile(process.env.LINEAGE_PROFILE, { channel: runtimeChannel, gitSha: startupRuntime.git_sha, version: startupRuntime.version });
|
|
9104
|
+
if (!doctor.ok) {
|
|
9105
|
+
const failures = doctor.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
|
|
9106
|
+
throw new Error(`Configured Lineage profile failed doctor: ${failures}`);
|
|
9107
|
+
}
|
|
9108
|
+
assertResolvedRuntimeProfileEnvironment(doctor.profile);
|
|
9109
|
+
startupProfile = doctor.profile;
|
|
9110
|
+
}
|
|
9111
|
+
var writerLease = startupProfile ? acquireProfileWriterLease(startupProfile, runtimeChannel, "service") : void 0;
|
|
9112
|
+
delete process.env.LINEAGE_DB_ACCESS;
|
|
9113
|
+
if (writerLease) {
|
|
9114
|
+
try {
|
|
9115
|
+
const startupDatabase = lineageDb();
|
|
9116
|
+
startupDatabase.close();
|
|
9117
|
+
} catch (error) {
|
|
9118
|
+
writerLease.release();
|
|
9119
|
+
throw error;
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
7260
9122
|
var app = express2();
|
|
7261
9123
|
var port = Number(process.env.PORT || 5173);
|
|
7262
9124
|
var host = process.env.HOST || "lineage.localhost";
|
|
@@ -7264,6 +9126,13 @@ var isProduction = process.env.NODE_ENV === "production";
|
|
|
7264
9126
|
var maxUploadBytes = Number(process.env.LINEAGE_MAX_UPLOAD_MB || 200) * 1024 * 1024;
|
|
7265
9127
|
var upload = multer({ dest: ensureUploadDir(), limits: { fileSize: maxUploadBytes } });
|
|
7266
9128
|
app.use(express2.json({ limit: "1mb" }));
|
|
9129
|
+
registerManagedWriterRoute(app, {
|
|
9130
|
+
accepts: lineageCliCanDelegateMutation,
|
|
9131
|
+
channel: runtimeChannel,
|
|
9132
|
+
execute: executeDelegatedLineageMutation,
|
|
9133
|
+
profile: startupProfile,
|
|
9134
|
+
writerLease
|
|
9135
|
+
});
|
|
7267
9136
|
function projectFrom(input) {
|
|
7268
9137
|
const candidate = input.body?.project || input.body?.product || input.query?.project || input.query?.product;
|
|
7269
9138
|
return typeof candidate === "string" ? candidate : defaultProduct;
|
|
@@ -7599,16 +9468,16 @@ app.post(
|
|
|
7599
9468
|
})
|
|
7600
9469
|
);
|
|
7601
9470
|
if (isProduction) {
|
|
7602
|
-
const dist =
|
|
7603
|
-
if (
|
|
9471
|
+
const dist = join14(packageRoot, "dist", "web");
|
|
9472
|
+
if (existsSync14(dist)) {
|
|
7604
9473
|
app.use(express2.static(dist));
|
|
7605
|
-
app.get("*", (_req, res) => res.sendFile(
|
|
9474
|
+
app.get("*", (_req, res) => res.sendFile(join14(dist, "index.html")));
|
|
7606
9475
|
}
|
|
7607
9476
|
} else {
|
|
7608
9477
|
const { createServer: createViteServer } = await import("vite");
|
|
7609
9478
|
const e2ePort = process.env.LINEAGE_E2E_PORT ? Number(process.env.LINEAGE_E2E_PORT) : void 0;
|
|
7610
9479
|
const vite = await createViteServer({
|
|
7611
|
-
configFile:
|
|
9480
|
+
configFile: join14(packageRoot, "vite.config.ts"),
|
|
7612
9481
|
server: {
|
|
7613
9482
|
middlewareMode: true,
|
|
7614
9483
|
...e2ePort ? { ws: { port: e2ePort + 1e3 } } : {}
|
|
@@ -7658,6 +9527,10 @@ app.use((error, _req, res, _next) => {
|
|
|
7658
9527
|
res.status(error.status).json({ error: error.message });
|
|
7659
9528
|
return;
|
|
7660
9529
|
}
|
|
9530
|
+
if (isManagedWriterRoutingError(error)) {
|
|
9531
|
+
res.status(error.status).json({ error: error.message });
|
|
9532
|
+
return;
|
|
9533
|
+
}
|
|
7661
9534
|
if (isAgentClaimError(error)) {
|
|
7662
9535
|
res.status(error.status).json({ error: error.code, message: error.message, conflicts: error.conflicts });
|
|
7663
9536
|
return;
|
|
@@ -7677,7 +9550,23 @@ app.use((error, _req, res, _next) => {
|
|
|
7677
9550
|
const message = error instanceof Error ? error.message : String(error);
|
|
7678
9551
|
res.status(500).json({ error: message });
|
|
7679
9552
|
});
|
|
7680
|
-
|
|
7681
|
-
|
|
9553
|
+
var listenOrigin = `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
9554
|
+
var server = app.listen(port, host, () => {
|
|
9555
|
+
console.log(`Lineage listening on ${listenOrigin}`);
|
|
9556
|
+
});
|
|
9557
|
+
var releaseWriterLease = () => writerLease?.release();
|
|
9558
|
+
process.once("exit", releaseWriterLease);
|
|
9559
|
+
process.once("SIGINT", () => {
|
|
9560
|
+
releaseWriterLease();
|
|
9561
|
+
process.exit(130);
|
|
9562
|
+
});
|
|
9563
|
+
process.once("SIGTERM", () => {
|
|
9564
|
+
releaseWriterLease();
|
|
9565
|
+
process.exit(143);
|
|
9566
|
+
});
|
|
9567
|
+
server.once("error", (error) => {
|
|
9568
|
+
releaseWriterLease();
|
|
9569
|
+
console.error(`Lineage failed to listen on ${listenOrigin}: ${error.message}`);
|
|
9570
|
+
process.exit(1);
|
|
7682
9571
|
});
|
|
7683
9572
|
//# sourceMappingURL=server.js.map
|