@mean-weasel/lineage 0.1.11 → 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/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 existsSync7 } from "node:fs";
5
- import { join as join10 } from "node:path";
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 existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
9
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
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 { join as join3 } from "node:path";
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 || join3(packageRoot, ".lineage", "asset-lineage.sqlite");
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
- mkdirSync2(join3(lineageDbPath(), ".."), { recursive: true });
297
- const { DatabaseSync } = require2("node:sqlite");
298
- const database = new DatabaseSync(lineageDbPath());
299
- database.exec("PRAGMA foreign_keys = ON");
300
- database.exec("PRAGMA busy_timeout = 5000");
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,21 +1667,21 @@ function ledgerWorkflowStates(database, project, records, sources) {
1131
1667
 
1132
1668
  // src/server/assetCore.ts
1133
1669
  function isPackageRoot(path) {
1134
- const packageJson = join4(path, "package.json");
1135
- if (!existsSync3(packageJson)) return false;
1670
+ const packageJson = join6(path, "package.json");
1671
+ if (!existsSync6(packageJson)) return false;
1136
1672
  try {
1137
- const packageInfo2 = JSON.parse(readFileSync2(packageJson, "utf8"));
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
1679
  function resolvePackageRoot() {
1144
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
1680
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
1145
1681
  const candidates = [
1146
1682
  process.env.LINEAGE_REPO_ROOT,
1147
- resolve2(moduleDir, ".."),
1148
- resolve2(moduleDir, "../.."),
1683
+ resolve5(moduleDir, ".."),
1684
+ resolve5(moduleDir, "../.."),
1149
1685
  process.cwd()
1150
1686
  ].filter((candidate) => Boolean(candidate));
1151
1687
  const root = candidates.find(isPackageRoot);
@@ -1153,7 +1689,11 @@ function resolvePackageRoot() {
1153
1689
  return root;
1154
1690
  }
1155
1691
  var packageRoot = resolvePackageRoot();
1156
- var repoRoot = resolve2(process.env.LINEAGE_ASSET_ROOT || packageRoot);
1692
+ var repoRoot = resolve5(process.env.LINEAGE_ASSET_ROOT || packageRoot);
1693
+ function setLineageAssetRoot(path) {
1694
+ repoRoot = resolve5(path || packageRoot);
1695
+ return repoRoot;
1696
+ }
1157
1697
  var defaultProject = "demo-project";
1158
1698
  var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
1159
1699
  var publicFallbackBucket = "lineage-demo-assets";
@@ -1179,16 +1719,16 @@ function cleanProject(project = defaultProject) {
1179
1719
  return project;
1180
1720
  }
1181
1721
  function catalogPath(project = defaultProject) {
1182
- return join4(repoRoot, cleanProject(project), "assets", "catalog.json");
1722
+ return join6(repoRoot, cleanProject(project), "assets", "catalog.json");
1183
1723
  }
1184
1724
  function fixtureCatalogPath(project = defaultProject) {
1185
- return join4(packageRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
1725
+ return join6(packageRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
1186
1726
  }
1187
1727
  function resolvedCatalogPath(project = defaultProject) {
1188
1728
  const path = catalogPath(project);
1189
- if (existsSync3(path)) return path;
1729
+ if (existsSync6(path)) return path;
1190
1730
  const clean = cleanProject(project);
1191
- if (clean === defaultProject && existsSync3(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
1731
+ if (clean === defaultProject && existsSync6(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
1192
1732
  return path;
1193
1733
  }
1194
1734
  function normalizeCatalog(catalog, fallbackProject = defaultProject) {
@@ -1305,7 +1845,7 @@ function defaultFallbackCatalog() {
1305
1845
  }, defaultProject);
1306
1846
  }
1307
1847
  function isDefaultFallbackCatalog(catalog) {
1308
- return catalog.project === defaultProject && !existsSync3(catalogPath(defaultProject));
1848
+ return catalog.project === defaultProject && !existsSync6(catalogPath(defaultProject));
1309
1849
  }
1310
1850
  function fallbackPreviewDataUrl(asset) {
1311
1851
  const label = `${asset.title}
@@ -1318,9 +1858,9 @@ function escapeSvgText2(value) {
1318
1858
  }
1319
1859
  function loadCatalog(project = defaultProject) {
1320
1860
  const path = catalogPath(project);
1321
- if (existsSync3(path)) {
1861
+ if (existsSync6(path)) {
1322
1862
  try {
1323
- return normalizeCatalog(JSON.parse(readFileSync2(path, "utf8")), project);
1863
+ return normalizeCatalog(JSON.parse(readFileSync4(path, "utf8")), project);
1324
1864
  } catch (error) {
1325
1865
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
1326
1866
  throw new LineageAssetError(`Missing catalog: ${path}`, 404);
@@ -1331,8 +1871,8 @@ function loadCatalog(project = defaultProject) {
1331
1871
  const clean = cleanProject(project);
1332
1872
  if (clean === defaultProject) {
1333
1873
  const fixturePath = fixtureCatalogPath(clean);
1334
- if (existsSync3(fixturePath)) {
1335
- return normalizeCatalog(JSON.parse(readFileSync2(fixturePath, "utf8")), project);
1874
+ if (existsSync6(fixturePath)) {
1875
+ return normalizeCatalog(JSON.parse(readFileSync4(fixturePath, "utf8")), project);
1336
1876
  }
1337
1877
  return defaultFallbackCatalog();
1338
1878
  }
@@ -1340,8 +1880,8 @@ function loadCatalog(project = defaultProject) {
1340
1880
  }
1341
1881
  function saveCatalog(project, catalog) {
1342
1882
  const normalized = normalizeCatalog(catalog, project);
1343
- mkdirSync3(dirname2(catalogPath(project)), { recursive: true });
1344
- writeFileSync(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
1883
+ mkdirSync5(dirname4(catalogPath(project)), { recursive: true });
1884
+ writeFileSync2(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
1345
1885
  `);
1346
1886
  return normalized;
1347
1887
  }
@@ -1366,7 +1906,7 @@ function runAws(args) {
1366
1906
  return run("aws", args);
1367
1907
  }
1368
1908
  function listProjects() {
1369
- const projects = readdirSync2(repoRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && projectNamePattern.test(entry.name) && existsSync3(catalogPath(entry.name))).flatMap((entry) => {
1909
+ const projects = readdirSync2(repoRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && projectNamePattern.test(entry.name) && existsSync6(catalogPath(entry.name))).flatMap((entry) => {
1370
1910
  try {
1371
1911
  const catalog = loadCatalog(entry.name);
1372
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 }];
@@ -1525,15 +2065,15 @@ function deleteObjectGuarded(project, assetId, confirmation) {
1525
2065
  return storageAdapter.deleteObjectGuarded(project, assetId, confirmation);
1526
2066
  }
1527
2067
  function ensureUploadDir() {
1528
- const dir = join4(repoRoot, ".asset-scratch", "studio-uploads");
1529
- mkdirSync3(dir, { recursive: true });
2068
+ const dir = join6(repoRoot, ".asset-scratch", "studio-uploads");
2069
+ mkdirSync5(dir, { recursive: true });
1530
2070
  return dir;
1531
2071
  }
1532
2072
  function cleanupUploadedTemp(file) {
1533
2073
  if (!file) return;
1534
2074
  const uploadRoot = ensureUploadDir();
1535
- const resolved = resolve2(file);
1536
- if (resolved.startsWith(`${uploadRoot}/`) && existsSync3(resolved)) {
2075
+ const resolved = resolve5(file);
2076
+ if (resolved.startsWith(`${uploadRoot}/`) && existsSync6(resolved)) {
1537
2077
  unlinkSync(resolved);
1538
2078
  }
1539
2079
  }
@@ -1567,7 +2107,7 @@ function lookupAssets(project, assetIds) {
1567
2107
  }
1568
2108
 
1569
2109
  // src/server/assetLineage.ts
1570
- import { join as join5 } from "node:path";
2110
+ import { join as join7 } from "node:path";
1571
2111
 
1572
2112
  // src/server/assetLineageSelection.ts
1573
2113
  var LINEAGE_NEXT_VARIATION_LIMIT = 3;
@@ -1590,7 +2130,7 @@ function normalizeSelectionInput(fields) {
1590
2130
  import { randomBytes as randomBytes2 } from "node:crypto";
1591
2131
 
1592
2132
  // src/server/agentClaims.ts
1593
- import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
2133
+ import { createHash as createHash3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
1594
2134
  var defaultTtlSeconds = 20 * 60;
1595
2135
  var idleAfterSeconds = 5 * 60;
1596
2136
  var staleAfterSeconds = 15 * 60;
@@ -1626,12 +2166,12 @@ function randomId(prefix) {
1626
2166
  return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
1627
2167
  }
1628
2168
  function tokenHash(token) {
1629
- return createHash2("sha256").update(token).digest("hex");
2169
+ return createHash3("sha256").update(token).digest("hex");
1630
2170
  }
1631
2171
  function safeEqual(a, b) {
1632
2172
  const left = Buffer.from(a);
1633
2173
  const right = Buffer.from(b);
1634
- return left.length === right.length && timingSafeEqual(left, right);
2174
+ return left.length === right.length && timingSafeEqual2(left, right);
1635
2175
  }
1636
2176
  function expiresAtFrom(timestamp, ttlSeconds) {
1637
2177
  return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
@@ -1658,6 +2198,12 @@ function derivedState(row, now = /* @__PURE__ */ new Date()) {
1658
2198
  }
1659
2199
  function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1660
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);
1661
2207
  return {
1662
2208
  id: String(row.id),
1663
2209
  project: String(row.project_id),
@@ -1669,7 +2215,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1669
2215
  agent_name: String(row.agent_name),
1670
2216
  agent_kind: String(row.agent_kind),
1671
2217
  thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
1672
- status: String(row.status),
2218
+ status: persistedStatus === "active" && state === "expired" ? "expired" : persistedStatus,
1673
2219
  created_at: String(row.created_at),
1674
2220
  heartbeat_at: heartbeatAt,
1675
2221
  expires_at: String(row.expires_at),
@@ -1679,11 +2225,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1679
2225
  override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
1680
2226
  metadata: parseMetadata(row.metadata_json),
1681
2227
  heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
1682
- derived_state: derivedState({
1683
- expires_at: String(row.expires_at),
1684
- heartbeat_at: heartbeatAt,
1685
- status: String(row.status)
1686
- }, now)
2228
+ derived_state: state
1687
2229
  };
1688
2230
  }
1689
2231
  function eventToRow(row) {
@@ -1825,7 +2367,7 @@ function createAgentClaim(fields) {
1825
2367
  function listAgentClaims(project) {
1826
2368
  const database = lineageDb();
1827
2369
  try {
1828
- expireActiveClaims(database);
2370
+ if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
1829
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();
1830
2372
  return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
1831
2373
  } finally {
@@ -1835,7 +2377,7 @@ function listAgentClaims(project) {
1835
2377
  function inspectAgentClaim(claimId, project) {
1836
2378
  const database = lineageDb();
1837
2379
  try {
1838
- expireActiveClaims(database);
2380
+ if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
1839
2381
  const claim = findClaimById(database, claimId, project);
1840
2382
  if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1841
2383
  const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
@@ -2462,6 +3004,44 @@ function cancelLineageIterateTasksForAssets(project, fields) {
2462
3004
  taskId: task.id
2463
3005
  }));
2464
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
+ }
2465
3045
 
2466
3046
  // src/server/assetLineageWorkspaces.ts
2467
3047
  var actors = /* @__PURE__ */ new Set(["human", "agent", "system"]);
@@ -2536,9 +3116,18 @@ function knownRoots(database, project) {
2536
3116
  and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
2537
3117
  group by parent_asset_id
2538
3118
  `).all(project, project, project, project);
2539
- return rows.map((row) => ({ root_asset_id: row.root_asset_id, selected_at: row.selected_at || void 0 }));
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()];
2540
3128
  }
2541
3129
  function seedLegacyWorkspaces(database, project) {
3130
+ if (process.env.LINEAGE_DB_ACCESS === "read-only") return;
2542
3131
  ensureProject3(database, project);
2543
3132
  const timestamp = nowIso();
2544
3133
  const statement = database.prepare(`
@@ -2562,6 +3151,34 @@ function seedLegacyWorkspaces(database, project) {
2562
3151
  );
2563
3152
  }
2564
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
+ }
2565
3182
  function listRows(database, project) {
2566
3183
  return database.prepare(`
2567
3184
  select * from lineage_workspaces
@@ -2573,23 +3190,15 @@ function listRows(database, project) {
2573
3190
  title
2574
3191
  `).all(project).map(rowToWorkspace);
2575
3192
  }
2576
- function activeWorkspace(database, project) {
2577
- const row = database.prepare(`
2578
- select * from lineage_workspaces
2579
- where project_id = ? and status != 'archived'
2580
- order by active_at desc nulls last, updated_at desc
2581
- limit 1
2582
- `).get(project);
2583
- return row ? rowToWorkspace(row) : null;
2584
- }
2585
3193
  function listLineageWorkspaces(project) {
2586
3194
  const database = lineageDb();
2587
3195
  try {
2588
3196
  seedLegacyWorkspaces(database, project);
3197
+ const workspaces = sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]);
2589
3198
  return {
2590
3199
  project,
2591
- active_workspace: activeWorkspace(database, project),
2592
- workspaces: listRows(database, project),
3200
+ active_workspace: workspaces.find((workspace) => workspace.status !== "archived") || null,
3201
+ workspaces,
2593
3202
  fetchedAt: nowIso()
2594
3203
  };
2595
3204
  } finally {
@@ -2600,7 +3209,7 @@ function inspectLineageWorkspace(project, workspaceId) {
2600
3209
  const database = lineageDb();
2601
3210
  try {
2602
3211
  seedLegacyWorkspaces(database, project);
2603
- 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);
2604
3213
  if (!workspace) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
2605
3214
  return workspace;
2606
3215
  } finally {
@@ -2732,7 +3341,7 @@ function activeLineageWorkspaceRoot(project) {
2732
3341
  const database = lineageDb();
2733
3342
  try {
2734
3343
  seedLegacyWorkspaces(database, project);
2735
- return activeWorkspace(database, project)?.root_asset_id;
3344
+ return sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]).find((workspace) => workspace.status !== "archived")?.root_asset_id;
2736
3345
  } finally {
2737
3346
  database.close();
2738
3347
  }
@@ -2794,7 +3403,7 @@ function upsertProject(database, project) {
2794
3403
  insert into projects (id, product, catalog_path, created_at, updated_at)
2795
3404
  values (?, ?, ?, ?, ?)
2796
3405
  on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
2797
- `).run(project, project, join5(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
3406
+ `).run(project, project, join7(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
2798
3407
  }
2799
3408
  function upsertAsset(database, project, asset) {
2800
3409
  const timestamp = nowIso();
@@ -3013,6 +3622,13 @@ function assertNodeInRoot(database, project, root, node) {
3013
3622
  const nodeRoot = rootFor(database, project, node);
3014
3623
  if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
3015
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
+ }
3016
3632
  function canPreviewLocally(mediaType, localPath) {
3017
3633
  return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
3018
3634
  }
@@ -3387,6 +4003,74 @@ function promoteLineageAttempt(project, fields) {
3387
4003
  database.close();
3388
4004
  }
3389
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
+ }
3390
4074
  function markLineageRerollRequest(project, fields) {
3391
4075
  const database = lineageDb();
3392
4076
  try {
@@ -3583,16 +4267,34 @@ function updateAssetReview(project, fields) {
3583
4267
  return { ok: true, message: `Marked ${fields.assetId} ${fields.reviewState}`, asset_id: fields.assetId, review_state: fields.reviewState };
3584
4268
  }
3585
4269
 
3586
- // src/server/assetLineageHandoff.ts
3587
- var publicPackageCommand = "npx @mean-weasel/lineage";
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
+ }
3588
4284
  function shellQuote(value) {
3589
4285
  return `'${value.replace(/'/g, "'\\''")}'`;
3590
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
3591
4293
  function lineageCommand(command, project, rootAssetId) {
3592
- return `${publicPackageCommand} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --db ${shellQuote(lineageDbPath())} --json`;
4294
+ return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
3593
4295
  }
3594
4296
  function linkChildCommand(project, rootAssetId) {
3595
- return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
4297
+ return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
3596
4298
  }
3597
4299
  function rerollImportGuidance(rootAssetId, targetAssetId) {
3598
4300
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -3634,7 +4336,7 @@ function getLineageBrief(project, rootAssetId) {
3634
4336
  },
3635
4337
  handoff: {
3636
4338
  next_command: lineageCommand("next", project, next.root_asset_id),
3637
- inspect_command: asset ? `${publicPackageCommand} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} --db ${shellQuote(lineageDbPath())} --json` : void 0,
4339
+ inspect_command: asset ? `${lineagePublicPackageCommand()} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} ${lineageRuntimeSelector()} --json` : void 0,
3638
4340
  link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
3639
4341
  },
3640
4342
  fetchedAt: nowIso()
@@ -5020,9 +5722,9 @@ function ensureProject5(database, project) {
5020
5722
  on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
5021
5723
  `).run(project, project, timestamp, timestamp);
5022
5724
  }
5023
- function definitionFor(adapterType, provider) {
5024
- const definition = definitions.find((item) => item.adapter_type === adapterType && item.provider === provider);
5025
- if (!definition) throw new AdapterSettingsError(`Unsupported adapter setting: ${adapterType}/${provider}`, 404);
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);
5026
5728
  return definition;
5027
5729
  }
5028
5730
  function seedDefaults(database, project) {
@@ -5054,22 +5756,22 @@ function parseConfig(value) {
5054
5756
  return {};
5055
5757
  }
5056
5758
  }
5057
- function credentialFor(provider, env) {
5058
- if (provider === "s3") {
5759
+ function credentialFor(provider2, env) {
5760
+ if (provider2 === "s3") {
5059
5761
  return { detected: false, label: "Optional local cloud CLI credential", secret_ref: null };
5060
5762
  }
5061
- if (provider === "buffer") {
5763
+ if (provider2 === "buffer") {
5062
5764
  const detected = Boolean(env.LINEAGE_SCHEDULER_TOKEN && env.LINEAGE_SCHEDULER_ORGANIZATION_ID);
5063
5765
  return { detected, label: "LINEAGE_SCHEDULER_TOKEN + LINEAGE_SCHEDULER_ORGANIZATION_ID", secret_ref: "env:LINEAGE_SCHEDULER_TOKEN" };
5064
5766
  }
5065
5767
  return { detected: true, label: "No external secret required", secret_ref: null };
5066
5768
  }
5067
- function healthStatus(provider, enabled, config, credential) {
5068
- if (provider === "s3") {
5769
+ function healthStatus(provider2, enabled, config, credential) {
5770
+ if (provider2 === "s3") {
5069
5771
  if (!enabled) return "live_disabled";
5070
5772
  return config.bucket && config.region ? "not_tested" : "missing_config";
5071
5773
  }
5072
- if (provider === "buffer") {
5774
+ if (provider2 === "buffer") {
5073
5775
  if (!enabled) return "live_disabled";
5074
5776
  return credential.detected ? "configured" : "dry_run_available";
5075
5777
  }
@@ -5245,8 +5947,8 @@ function getAdapterStatus(project, env = process.env) {
5245
5947
  }
5246
5948
 
5247
5949
  // src/server/adapters/posting/bufferPostingService.ts
5248
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
5249
- import { join as join6 } from "node:path";
5950
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "node:fs";
5951
+ import { join as join9 } from "node:path";
5250
5952
 
5251
5953
  // src/server/contentPostHandoff.ts
5252
5954
  function readinessForPost(post) {
@@ -5742,12 +6444,12 @@ function resolvePost(project, fields) {
5742
6444
  throw new PostingAdapterError("Buffer dry run requires postId or target=selected");
5743
6445
  }
5744
6446
  function payloadPath(project, postId) {
5745
- return join6(repoRoot, ".asset-scratch", "buffer-dry-runs", safeSegment(project), `${safeSegment(postId)}.json`);
6447
+ return join9(repoRoot, ".asset-scratch", "buffer-dry-runs", safeSegment(project), `${safeSegment(postId)}.json`);
5746
6448
  }
5747
6449
  function writePayload(project, postId, payload) {
5748
6450
  const file = payloadPath(project, postId);
5749
- mkdirSync4(join6(file, ".."), { recursive: true });
5750
- writeFileSync2(file, `${JSON.stringify(payload, null, 2)}
6451
+ mkdirSync6(join9(file, ".."), { recursive: true });
6452
+ writeFileSync3(file, `${JSON.stringify(payload, null, 2)}
5751
6453
  `);
5752
6454
  return file;
5753
6455
  }
@@ -5856,11 +6558,11 @@ function registerAdapterRoutes(app2, projectFrom2, asyncRoute2) {
5856
6558
  import { Router } from "express";
5857
6559
 
5858
6560
  // src/server/contentBatchImport.ts
5859
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "node:fs";
5860
- import { basename as basename3, join as join7, relative as relative2 } from "node:path";
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";
5861
6563
  function walk(dir) {
5862
6564
  return readdirSync3(dir, { withFileTypes: true }).flatMap((entry) => {
5863
- const path = join7(dir, entry.name);
6565
+ const path = join10(dir, entry.name);
5864
6566
  if (entry.isDirectory()) return walk(path);
5865
6567
  return entry.isFile() && entry.name.endsWith(".md") ? [path] : [];
5866
6568
  });
@@ -5885,14 +6587,14 @@ function phaseFromStatus(value) {
5885
6587
  return "draft";
5886
6588
  }
5887
6589
  function postIdFor(kind, file) {
5888
- const slug2 = basename3(file, ".md").replace(/^\d{4}-\d{2}-/, "");
6590
+ const slug2 = basename4(file, ".md").replace(/^\d{4}-\d{2}-/, "");
5889
6591
  return `${kind}-${slug2}`;
5890
6592
  }
5891
6593
  function itemFromFile(file) {
5892
6594
  const parts = file.split("/");
5893
6595
  const kind = parts.includes("drafts") ? "draft" : parts.includes("concepts") ? "concept" : null;
5894
6596
  if (!kind) return null;
5895
- const text = readFileSync3(file, "utf8");
6597
+ const text = readFileSync5(file, "utf8");
5896
6598
  const meta = metadata(text);
5897
6599
  const pathChannel = parts[parts.indexOf("channels") + 1] || "";
5898
6600
  const relatedAsset = meta["related asset"] && meta["related asset"] !== "none yet" ? meta["related asset"] : void 0;
@@ -5906,12 +6608,12 @@ function itemFromFile(file) {
5906
6608
  postId: postIdFor(kind, file),
5907
6609
  relatedAsset,
5908
6610
  sourcePath: relative2(repoRoot, file),
5909
- title: text.match(/^#\s+(.+)$/m)?.[1] || basename3(file, ".md")
6611
+ title: text.match(/^#\s+(.+)$/m)?.[1] || basename4(file, ".md")
5910
6612
  };
5911
6613
  }
5912
6614
  function demoMarkdownItems(kind) {
5913
- const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join7(repoRoot, "demo-project", "channels");
5914
- if (!existsSync4(root)) return [];
6615
+ const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join10(repoRoot, "demo-project", "channels");
6616
+ if (!existsSync8(root)) return [];
5915
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));
5916
6618
  }
5917
6619
  function importDemoContentBatch(project, options) {
@@ -6250,6 +6952,11 @@ function contentBatchRouter(projectFrom2) {
6250
6952
  }
6251
6953
 
6252
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";
6253
6960
  var GenerationReceiptError = class extends Error {
6254
6961
  constructor(message, status = 400) {
6255
6962
  super(message);
@@ -6260,10 +6967,44 @@ var GenerationReceiptError = class extends Error {
6260
6967
  function isGenerationReceiptError(error) {
6261
6968
  return error instanceof GenerationReceiptError;
6262
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
+ }
6263
6976
  function parseJson(value, fallback) {
6264
6977
  if (!value) return fallback;
6265
6978
  return JSON.parse(value);
6266
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
+ }
6267
7008
  function receiptFrom(row) {
6268
7009
  return {
6269
7010
  id: String(row.id),
@@ -6327,6 +7068,191 @@ function loadGenerationJob(database, project, id) {
6327
7068
  receipts
6328
7069
  };
6329
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
+ }
6330
7256
 
6331
7257
  // src/server/generationReceiptJobs.ts
6332
7258
  function listImageGenerationJobs(project = defaultProject, fields = {}) {
@@ -6417,15 +7343,15 @@ function registerLineageTaskRoutes(app2, projectFrom2, asyncRoute2) {
6417
7343
  }
6418
7344
 
6419
7345
  // src/server/assetLineageDemo.ts
6420
- import { createHash as createHash3 } from "node:crypto";
6421
- import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
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";
6422
7348
  import { tmpdir } from "node:os";
6423
- import { dirname as dirname3, join as join8, posix } from "node:path";
7349
+ import { dirname as dirname5, join as join11, posix } from "node:path";
6424
7350
  import { gunzipSync } from "node:zlib";
6425
7351
  var demoWorkspaceTitle = "Demo: Content iteration tree";
6426
7352
  var demoWorkspaceNotes = "Repeatable sample lineage for demos and onboarding. Archive it when reviewing real work.";
6427
7353
  var demoBasePath = ["lineage-demo", "2026-06-lineage-demo"];
6428
- var swissifierManifestPath = join8(packageRoot, "fixtures", defaultProject, "lineage", "swissifier-rich-demo.json");
7354
+ var swissifierManifestPath = join11(packageRoot, "fixtures", defaultProject, "lineage", "swissifier-rich-demo.json");
6429
7355
  var demoAssets = [
6430
7356
  { key: "root", channel: "linkedin", file: "demo-root.svg", label: "Initial Demo Concept", fill: "#f6fbfb", stroke: "#0b7f88" },
6431
7357
  { key: "hookA", channel: "tiktok", file: "demo-hook-a-v01.svg", label: "Hook A v01", fill: "#fff8e6", stroke: "#9a6a00" },
@@ -6462,37 +7388,37 @@ var demoPositions = /* @__PURE__ */ new Map([
6462
7388
  ["alt", { x: 1060, y: 80 }]
6463
7389
  ]);
6464
7390
  function demoProjectDir(project) {
6465
- return join8(repoRoot, ".asset-scratch", ...demoBasePath, project);
7391
+ return join11(repoRoot, ".asset-scratch", ...demoBasePath, project);
6466
7392
  }
6467
7393
  function demoRelativePath(project, asset) {
6468
- return join8(...demoBasePath, project, asset.channel, asset.file);
7394
+ return join11(...demoBasePath, project, asset.channel, asset.file);
6469
7395
  }
6470
7396
  function demoFilePath(project, asset) {
6471
- return join8(demoProjectDir(project), asset.channel, asset.file);
7397
+ return join11(demoProjectDir(project), asset.channel, asset.file);
6472
7398
  }
6473
7399
  function swissifierManifest() {
6474
- return JSON.parse(readFileSync4(swissifierManifestPath, "utf8"));
7400
+ return JSON.parse(readFileSync6(swissifierManifestPath, "utf8"));
6475
7401
  }
6476
7402
  function swissifierRelativePath(manifest, asset) {
6477
- return join8(manifest.media.target_dir, asset.file);
7403
+ return join11(manifest.media.target_dir, asset.file);
6478
7404
  }
6479
7405
  function swissifierFilePath(manifest, asset) {
6480
- return join8(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
7406
+ return join11(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
6481
7407
  }
6482
7408
  function swissifierRerollRelativePath(manifest, attempt) {
6483
- return join8(manifest.media.target_dir, "reroll-attempts", attempt.file);
7409
+ return join11(manifest.media.target_dir, "reroll-attempts", attempt.file);
6484
7410
  }
6485
7411
  function swissifierRerollFilePath(manifest, attempt) {
6486
- return join8(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
7412
+ return join11(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
6487
7413
  }
6488
7414
  function swissifierRerollFixturePath(attempt) {
6489
- return join8(dirname3(swissifierManifestPath), "swissifier-rerolls", attempt.file);
7415
+ return join11(dirname5(swissifierManifestPath), "swissifier-rerolls", attempt.file);
6490
7416
  }
6491
7417
  function swissifierSourcePath(manifest, asset, sourceDir) {
6492
- const direct = join8(sourceDir, asset.file);
6493
- if (existsSync5(direct)) return direct;
6494
- const nested = join8(sourceDir, manifest.media.target_dir, asset.file);
6495
- return existsSync5(nested) ? nested : null;
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;
6496
7422
  }
6497
7423
  function swissifierMediaState(manifest) {
6498
7424
  const missing = [];
@@ -6500,7 +7426,7 @@ function swissifierMediaState(manifest) {
6500
7426
  for (const asset of manifest.assets) {
6501
7427
  const relativePath = swissifierRelativePath(manifest, asset);
6502
7428
  const path = swissifierFilePath(manifest, asset);
6503
- if (!existsSync5(path)) {
7429
+ if (!existsSync10(path)) {
6504
7430
  missing.push(relativePath);
6505
7431
  continue;
6506
7432
  }
@@ -6509,7 +7435,7 @@ function swissifierMediaState(manifest) {
6509
7435
  return { invalid, missing };
6510
7436
  }
6511
7437
  function sha256Hex(input) {
6512
- return createHash3("sha256").update(input).digest("hex");
7438
+ return createHash4("sha256").update(input).digest("hex");
6513
7439
  }
6514
7440
  function safeTarEntryName(rawName) {
6515
7441
  const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
@@ -6572,9 +7498,9 @@ function extractSwissifierMediaArchive(archive, manifest, destination) {
6572
7498
  const file = body.subarray(offset, offset + size);
6573
7499
  const actualSha = sha256Hex(file);
6574
7500
  if (actualSha !== asset.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier media archive entry: ${name}`);
6575
- const target = join8(destination, directName);
6576
- mkdirSync5(dirname3(target), { recursive: true });
6577
- writeFileSync3(target, file);
7501
+ const target = join11(destination, directName);
7502
+ mkdirSync7(dirname5(target), { recursive: true });
7503
+ writeFileSync4(target, file);
6578
7504
  extracted.add(directName);
6579
7505
  offset = nextOffset;
6580
7506
  }
@@ -6592,14 +7518,14 @@ async function downloadBuffer(url, maxBytes) {
6592
7518
  return buffer;
6593
7519
  }
6594
7520
  function missingDemoMedia(project) {
6595
- return demoAssets.filter((asset) => !existsSync5(demoFilePath(project, asset))).map((asset) => demoRelativePath(project, asset));
7521
+ return demoAssets.filter((asset) => !existsSync10(demoFilePath(project, asset))).map((asset) => demoRelativePath(project, asset));
6596
7522
  }
6597
7523
  function demoSeedMediaStatus(project = defaultProject) {
6598
7524
  const missing = missingDemoMedia(project);
6599
7525
  const present = demoAssets.length - missing.length;
6600
7526
  return {
6601
7527
  ok: true,
6602
- media_root: join8(repoRoot, ".asset-scratch"),
7528
+ media_root: join11(repoRoot, ".asset-scratch"),
6603
7529
  present,
6604
7530
  total: demoAssets.length,
6605
7531
  missing,
@@ -6616,7 +7542,7 @@ function swissifierRichDemoMediaStatus(project = defaultProject) {
6616
7542
  ok: true,
6617
7543
  demo_id: manifest.id,
6618
7544
  project,
6619
- media_root: join8(repoRoot, ".asset-scratch"),
7545
+ media_root: join11(repoRoot, ".asset-scratch"),
6620
7546
  media_target: manifest.media.target_dir,
6621
7547
  download_available: Boolean(manifest.media.download),
6622
7548
  download_file: manifest.media.download?.file,
@@ -6667,7 +7593,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
6667
7593
  would_restore: manifest.assets.length
6668
7594
  };
6669
7595
  }
6670
- const sourceDir = mkdtempSync(join8(tmpdir(), "lineage-swissifier-media-"));
7596
+ const sourceDir = mkdtempSync(join11(tmpdir(), "lineage-swissifier-media-"));
6671
7597
  try {
6672
7598
  const extracted = extractSwissifierMediaArchive(archive, manifest, sourceDir);
6673
7599
  const restored = restoreSwissifierRichDemoMedia(project, { confirmWrite: true, sourceDir });
@@ -6681,7 +7607,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
6681
7607
  media_status: swissifierRichDemoMediaStatus(project)
6682
7608
  };
6683
7609
  } finally {
6684
- rmSync(sourceDir, { force: true, recursive: true });
7610
+ rmSync2(sourceDir, { force: true, recursive: true });
6685
7611
  }
6686
7612
  }
6687
7613
  function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite: false }) {
@@ -6694,7 +7620,7 @@ function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite:
6694
7620
  return {
6695
7621
  ok: true,
6696
7622
  dryRun: !fields.confirmWrite,
6697
- media_root: join8(repoRoot, ".asset-scratch"),
7623
+ media_root: join11(repoRoot, ".asset-scratch"),
6698
7624
  restored: fields.confirmWrite ? missing.size : 0,
6699
7625
  total: demoAssets.length,
6700
7626
  would_restore: fields.confirmWrite ? 0 : missing.size,
@@ -6711,7 +7637,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
6711
7637
  demo_id: manifest.id,
6712
7638
  project,
6713
7639
  dryRun: !fields.confirmWrite,
6714
- media_root: join8(repoRoot, ".asset-scratch"),
7640
+ media_root: join11(repoRoot, ".asset-scratch"),
6715
7641
  restored: 0,
6716
7642
  total: manifest.assets.length,
6717
7643
  would_restore: 0,
@@ -6739,7 +7665,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
6739
7665
  if (fields.confirmWrite) {
6740
7666
  for (const item of copyable) {
6741
7667
  const target = swissifierFilePath(manifest, item.asset);
6742
- mkdirSync5(dirname3(target), { recursive: true });
7668
+ mkdirSync7(dirname5(target), { recursive: true });
6743
7669
  copyFileSync2(item.source, target);
6744
7670
  }
6745
7671
  }
@@ -6748,7 +7674,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
6748
7674
  demo_id: manifest.id,
6749
7675
  project,
6750
7676
  dryRun: !fields.confirmWrite,
6751
- media_root: join8(repoRoot, ".asset-scratch"),
7677
+ media_root: join11(repoRoot, ".asset-scratch"),
6752
7678
  restored: fields.confirmWrite ? copyable.length : 0,
6753
7679
  total: manifest.assets.length,
6754
7680
  would_restore: fields.confirmWrite ? 0 : copyable.length,
@@ -6770,15 +7696,15 @@ function svg(label, fill, stroke) {
6770
7696
  `;
6771
7697
  }
6772
7698
  function assetIdFor(asset) {
6773
- return `local-${createHash3("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
7699
+ return `local-${createHash4("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
6774
7700
  }
6775
7701
  function demoAssetIds() {
6776
7702
  return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
6777
7703
  }
6778
7704
  function writeDemoFile(project, asset) {
6779
7705
  const path = demoFilePath(project, asset);
6780
- mkdirSync5(dirname3(path), { recursive: true });
6781
- writeFileSync3(path, svg(asset.label, asset.fill, asset.stroke));
7706
+ mkdirSync7(dirname5(path), { recursive: true });
7707
+ writeFileSync4(path, svg(asset.label, asset.fill, asset.stroke));
6782
7708
  }
6783
7709
  function writeDemoFiles(project) {
6784
7710
  const ids = demoAssetIds();
@@ -6787,8 +7713,8 @@ function writeDemoFiles(project) {
6787
7713
  }
6788
7714
  function swissifierRerollAsset(attempt) {
6789
7715
  const source = swissifierRerollFixturePath(attempt);
6790
- if (!existsSync5(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
6791
- const body = readFileSync4(source);
7716
+ if (!existsSync10(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
7717
+ const body = readFileSync6(source);
6792
7718
  if (body.length !== attempt.size_bytes) throw new Error(`Unexpected Swissifier re-roll fixture size for ${attempt.file}`);
6793
7719
  const checksumSha256 = sha256Hex(body);
6794
7720
  if (checksumSha256 !== attempt.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier re-roll fixture: ${attempt.file}`);
@@ -6802,7 +7728,7 @@ function writeSwissifierRerollFiles(manifest) {
6802
7728
  for (const attempt of manifest.reroll_attempts || []) {
6803
7729
  swissifierRerollAsset(attempt);
6804
7730
  const target = swissifierRerollFilePath(manifest, attempt);
6805
- mkdirSync5(dirname3(target), { recursive: true });
7731
+ mkdirSync7(dirname5(target), { recursive: true });
6806
7732
  copyFileSync2(swissifierRerollFixturePath(attempt), target);
6807
7733
  }
6808
7734
  }
@@ -6814,7 +7740,7 @@ function upsertDemoAssets(project, ids) {
6814
7740
  insert into projects (id, product, catalog_path, created_at, updated_at)
6815
7741
  values (?, ?, ?, ?, ?)
6816
7742
  on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
6817
- `).run(project, project, join8(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
7743
+ `).run(project, project, join11(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
6818
7744
  const assetStatement = database.prepare(`
6819
7745
  insert into assets (
6820
7746
  id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
@@ -6837,7 +7763,7 @@ function upsertDemoAssets(project, ids) {
6837
7763
  ids[asset.key],
6838
7764
  project,
6839
7765
  demoRelativePath(project, asset),
6840
- createHash3("sha256").update(body).digest("hex"),
7766
+ createHash4("sha256").update(body).digest("hex"),
6841
7767
  asset.label,
6842
7768
  asset.channel,
6843
7769
  Buffer.byteLength(body),
@@ -6860,7 +7786,7 @@ function upsertSwissifierAssets(project, manifest) {
6860
7786
  insert into projects (id, product, catalog_path, created_at, updated_at)
6861
7787
  values (?, ?, ?, ?, ?)
6862
7788
  on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
6863
- `).run(project, project, join8(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
7789
+ `).run(project, project, join11(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
6864
7790
  const assetStatement = database.prepare(`
6865
7791
  insert into assets (
6866
7792
  id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
@@ -7096,7 +8022,7 @@ function archiveDemoLineageWorkspace(project, confirmWrite) {
7096
8022
  if (!isLineageWorkspaceError(error) || error.status !== 404) throw error;
7097
8023
  archived = { ok: true, message: "No demo lineage workspace exists yet", workspace: null };
7098
8024
  }
7099
- if (confirmWrite) rmSync(demoProjectDir(project), { force: true, recursive: true });
8025
+ if (confirmWrite) rmSync2(demoProjectDir(project), { force: true, recursive: true });
7100
8026
  return {
7101
8027
  ok: true,
7102
8028
  message: confirmWrite ? `Archived ${demoWorkspaceTitle}` : `Would archive ${demoWorkspaceTitle}`,
@@ -7191,13 +8117,13 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
7191
8117
 
7192
8118
  // src/server/runtimeInfo.ts
7193
8119
  import { spawnSync as spawnSync2 } from "node:child_process";
7194
- import { createRequire as createRequire2 } from "node:module";
7195
- import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
7196
- import { join as join9 } from "node:path";
7197
- var require3 = createRequire2(import.meta.url);
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);
7198
8124
  function packageInfo() {
7199
8125
  try {
7200
- const info = JSON.parse(readFileSync5(join9(packageRoot, "package.json"), "utf8"));
8126
+ const info = JSON.parse(readFileSync7(join12(packageRoot, "package.json"), "utf8"));
7201
8127
  return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
7202
8128
  } catch {
7203
8129
  return { name: "@mean-weasel/lineage", version: "0.0.0" };
@@ -7213,32 +8139,47 @@ function normalizeRuntimeChannel(value) {
7213
8139
  function gitSha() {
7214
8140
  const envSha = process.env.LINEAGE_GIT_SHA || process.env.GITHUB_SHA;
7215
8141
  if (envSha) return envSha.slice(0, 40);
7216
- if (!existsSync6(join9(packageRoot, ".git"))) return void 0;
8142
+ if (!existsSync11(join12(packageRoot, ".git"))) return void 0;
7217
8143
  const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: packageRoot, encoding: "utf8" });
7218
8144
  return result.status === 0 ? result.stdout.trim() || void 0 : void 0;
7219
8145
  }
7220
- function tableExists(database, table) {
8146
+ function tableExists2(database, table) {
7221
8147
  return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
7222
8148
  }
7223
8149
  function tableCount(database, table) {
7224
- if (!tableExists(database, table)) return void 0;
8150
+ if (!tableExists2(database, table)) return void 0;
7225
8151
  const row = database.prepare(`select count(*) count from ${table}`).get();
7226
8152
  return typeof row?.count === "number" ? row.count : void 0;
7227
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
+ }
7228
8158
  function getLineageRuntimeInfo(options = {}) {
7229
8159
  const info = packageInfo();
7230
8160
  const dbPath = options.dbPath || lineageDbPath();
7231
- const databaseInfo = { exists: existsSync6(dbPath), path: dbPath };
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: [] };
7232
8164
  if (databaseInfo.exists) {
7233
8165
  try {
7234
- const stat = statSync3(dbPath);
8166
+ const stat = statSync5(dbPath);
7235
8167
  databaseInfo.modified_at = stat.mtime.toISOString();
7236
8168
  databaseInfo.size_bytes = stat.size;
7237
- const { DatabaseSync } = require3("node:sqlite");
8169
+ const { DatabaseSync } = require5("node:sqlite");
7238
8170
  const database = new DatabaseSync(dbPath, { readOnly: true });
7239
8171
  try {
7240
8172
  databaseInfo.projects = tableCount(database, "projects");
7241
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
+ }
7242
8183
  } finally {
7243
8184
  database.close();
7244
8185
  }
@@ -7248,17 +8189,936 @@ function getLineageRuntimeInfo(options = {}) {
7248
8189
  }
7249
8190
  return {
7250
8191
  asset_root: repoRoot,
7251
- channel: normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL),
8192
+ channel,
7252
8193
  database: databaseInfo,
7253
8194
  fetchedAt: nowIso(),
7254
8195
  git_sha: gitSha(),
7255
8196
  node_env: process.env.NODE_ENV,
7256
8197
  package_name: info.name,
8198
+ profile: runtimeProfileIdentity(channel),
8199
+ schema,
7257
8200
  version: info.version
7258
8201
  };
7259
8202
  }
7260
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
+
7261
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
+ }
7262
9122
  var app = express2();
7263
9123
  var port = Number(process.env.PORT || 5173);
7264
9124
  var host = process.env.HOST || "lineage.localhost";
@@ -7266,6 +9126,13 @@ var isProduction = process.env.NODE_ENV === "production";
7266
9126
  var maxUploadBytes = Number(process.env.LINEAGE_MAX_UPLOAD_MB || 200) * 1024 * 1024;
7267
9127
  var upload = multer({ dest: ensureUploadDir(), limits: { fileSize: maxUploadBytes } });
7268
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
+ });
7269
9136
  function projectFrom(input) {
7270
9137
  const candidate = input.body?.project || input.body?.product || input.query?.project || input.query?.product;
7271
9138
  return typeof candidate === "string" ? candidate : defaultProduct;
@@ -7601,16 +9468,16 @@ app.post(
7601
9468
  })
7602
9469
  );
7603
9470
  if (isProduction) {
7604
- const dist = join10(packageRoot, "dist", "web");
7605
- if (existsSync7(dist)) {
9471
+ const dist = join14(packageRoot, "dist", "web");
9472
+ if (existsSync14(dist)) {
7606
9473
  app.use(express2.static(dist));
7607
- app.get("*", (_req, res) => res.sendFile(join10(dist, "index.html")));
9474
+ app.get("*", (_req, res) => res.sendFile(join14(dist, "index.html")));
7608
9475
  }
7609
9476
  } else {
7610
9477
  const { createServer: createViteServer } = await import("vite");
7611
9478
  const e2ePort = process.env.LINEAGE_E2E_PORT ? Number(process.env.LINEAGE_E2E_PORT) : void 0;
7612
9479
  const vite = await createViteServer({
7613
- configFile: join10(packageRoot, "vite.config.ts"),
9480
+ configFile: join14(packageRoot, "vite.config.ts"),
7614
9481
  server: {
7615
9482
  middlewareMode: true,
7616
9483
  ...e2ePort ? { ws: { port: e2ePort + 1e3 } } : {}
@@ -7660,6 +9527,10 @@ app.use((error, _req, res, _next) => {
7660
9527
  res.status(error.status).json({ error: error.message });
7661
9528
  return;
7662
9529
  }
9530
+ if (isManagedWriterRoutingError(error)) {
9531
+ res.status(error.status).json({ error: error.message });
9532
+ return;
9533
+ }
7663
9534
  if (isAgentClaimError(error)) {
7664
9535
  res.status(error.status).json({ error: error.code, message: error.message, conflicts: error.conflicts });
7665
9536
  return;
@@ -7679,7 +9550,23 @@ app.use((error, _req, res, _next) => {
7679
9550
  const message = error instanceof Error ? error.message : String(error);
7680
9551
  res.status(500).json({ error: message });
7681
9552
  });
7682
- app.listen(port, host, () => {
7683
- console.log(`Lineage listening on http://${host}:${port}`);
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);
7684
9571
  });
7685
9572
  //# sourceMappingURL=server.js.map