@mean-weasel/lineage 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +198 -33
- package/dist/cli/lineage-channel.js +276 -0
- package/dist/cli/lineage-channel.js.map +7 -0
- package/dist/cli/lineage-dev.js +2008 -141
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage-preview.js +6182 -0
- package/dist/cli/lineage-preview.js.map +7 -0
- package/dist/cli/lineage.js +1991 -136
- package/dist/cli/lineage.js.map +4 -4
- package/dist/cli/managed-service.js +430 -0
- package/dist/cli/managed-service.js.map +7 -0
- package/dist/runtime-build.json +9 -0
- package/dist/server.js +2357 -182
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-CKMlZT43.css → index-3uJbXqPS.css} +1 -1
- package/dist/web/assets/index-BrGIkUus.js +23 -0
- package/dist/web/index.html +2 -2
- package/package.json +22 -9
- package/dist/web/assets/index-ADWhXN3-.js +0 -23
package/dist/server.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
2
|
import express2 from "express";
|
|
3
3
|
import multer from "multer";
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
5
|
+
import { join as join14 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/server/assetCore.ts
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { dirname as
|
|
8
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
9
|
+
import { dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
@@ -282,22 +282,597 @@ 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 } from "node:path";
|
|
288
|
+
|
|
289
|
+
// src/server/profileWriterLease.ts
|
|
290
|
+
import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
291
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
292
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
|
|
293
|
+
|
|
294
|
+
// src/server/lineageProfiles.ts
|
|
295
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
285
296
|
import { createRequire } from "node:module";
|
|
286
|
-
import {
|
|
287
|
-
|
|
297
|
+
import {
|
|
298
|
+
chmodSync,
|
|
299
|
+
constants as fsConstants,
|
|
300
|
+
copyFileSync as copyFileSync2,
|
|
301
|
+
existsSync as existsSync3,
|
|
302
|
+
linkSync,
|
|
303
|
+
mkdirSync as mkdirSync2,
|
|
304
|
+
readFileSync as readFileSync2,
|
|
305
|
+
realpathSync,
|
|
306
|
+
rmSync,
|
|
307
|
+
statSync as statSync3,
|
|
308
|
+
writeFileSync
|
|
309
|
+
} from "node:fs";
|
|
310
|
+
import { homedir, platform } from "node:os";
|
|
311
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative as relative2, resolve as resolve2 } from "node:path";
|
|
312
|
+
|
|
313
|
+
// src/shared/lineageProfileTypes.ts
|
|
314
|
+
var lineageProfileSchemaVersion = "lineage.profile.v1";
|
|
315
|
+
var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
|
|
316
|
+
|
|
317
|
+
// src/server/lineageProfiles.ts
|
|
288
318
|
var require2 = createRequire(import.meta.url);
|
|
319
|
+
var profileIdPattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
320
|
+
function lineageDataRoot() {
|
|
321
|
+
if (process.env.LINEAGE_HOME) return resolve2(process.env.LINEAGE_HOME);
|
|
322
|
+
if (platform() === "darwin") return join3(homedir(), "Library", "Application Support", "Lineage");
|
|
323
|
+
if (platform() === "win32") return join3(process.env.APPDATA || join3(homedir(), "AppData", "Roaming"), "Lineage");
|
|
324
|
+
return join3(process.env.XDG_DATA_HOME || join3(homedir(), ".local", "share"), "lineage");
|
|
325
|
+
}
|
|
326
|
+
function lineageProfileRoot() {
|
|
327
|
+
return resolve2(process.env.LINEAGE_PROFILE_ROOT || join3(lineageDataRoot(), "profiles"));
|
|
328
|
+
}
|
|
329
|
+
function environmentChannel(environment) {
|
|
330
|
+
if (environment === "production") return "stable";
|
|
331
|
+
if (environment === "preview") return "preview";
|
|
332
|
+
return "dev";
|
|
333
|
+
}
|
|
334
|
+
function channelEnvironment(channel) {
|
|
335
|
+
if (channel === "stable") return "production";
|
|
336
|
+
if (channel === "preview") return "preview";
|
|
337
|
+
return "development";
|
|
338
|
+
}
|
|
339
|
+
function profileManifestPath(selector) {
|
|
340
|
+
const value = selector.trim();
|
|
341
|
+
if (!value) throw new Error("Profile selector must not be empty");
|
|
342
|
+
const looksLikePath = isAbsolute(value) || value.includes("/") || value.includes("\\") || value.endsWith(".json");
|
|
343
|
+
if (looksLikePath) return { manifestPath: resolve2(value) };
|
|
344
|
+
if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
|
|
345
|
+
return { manifestPath: join3(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
|
|
346
|
+
}
|
|
347
|
+
function requiredString(record, key) {
|
|
348
|
+
const value = record[key];
|
|
349
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
|
|
350
|
+
return value.trim();
|
|
351
|
+
}
|
|
352
|
+
function optionalString(record, key) {
|
|
353
|
+
const value = record[key];
|
|
354
|
+
if (value === void 0) return void 0;
|
|
355
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
|
|
356
|
+
return value.trim();
|
|
357
|
+
}
|
|
358
|
+
function validateEnvironment(value) {
|
|
359
|
+
if (value === "production" || value === "preview" || value === "development") return value;
|
|
360
|
+
throw new Error(`Invalid profile environment: ${value}`);
|
|
361
|
+
}
|
|
362
|
+
function validateChannel(value) {
|
|
363
|
+
if (value === void 0) return void 0;
|
|
364
|
+
if (value === "stable" || value === "preview" || value === "dev") return value;
|
|
365
|
+
throw new Error(`Invalid expected runtime channel: ${String(value)}`);
|
|
366
|
+
}
|
|
367
|
+
function validateCodeOrigin(value) {
|
|
368
|
+
if (value === void 0) return void 0;
|
|
369
|
+
if (value === "checkout" || value === "package") return value;
|
|
370
|
+
throw new Error(`Invalid expected runtime code_origin: ${String(value)}`);
|
|
371
|
+
}
|
|
372
|
+
function validateFingerprint(value, field) {
|
|
373
|
+
if (value === void 0) return void 0;
|
|
374
|
+
if (!/^[a-f0-9]{64}$/i.test(value)) throw new Error(`Profile ${field} must be a 64-character SHA-256 fingerprint`);
|
|
375
|
+
return value.toLowerCase();
|
|
376
|
+
}
|
|
377
|
+
function resolveManifestPath(manifestPath, value) {
|
|
378
|
+
return resolve2(dirname2(manifestPath), value);
|
|
379
|
+
}
|
|
380
|
+
function validateServiceOrigin(value) {
|
|
381
|
+
let parsed;
|
|
382
|
+
try {
|
|
383
|
+
parsed = new URL(value);
|
|
384
|
+
} catch {
|
|
385
|
+
throw new Error(`Invalid profile service_origin: ${value}`);
|
|
386
|
+
}
|
|
387
|
+
if (parsed.protocol !== "http:") throw new Error("Profile service_origin must use http for the local Lineage service");
|
|
388
|
+
if (!parsed.port) throw new Error("Profile service_origin must include an explicit port");
|
|
389
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
390
|
+
throw new Error("Profile service_origin must contain only scheme, host, and port");
|
|
391
|
+
}
|
|
392
|
+
return parsed.origin;
|
|
393
|
+
}
|
|
394
|
+
function resolveLineageProfile(selector) {
|
|
395
|
+
const { manifestPath, namedProfileId } = profileManifestPath(selector);
|
|
396
|
+
if (!existsSync3(manifestPath)) throw new Error(`Profile manifest does not exist: ${manifestPath}`);
|
|
397
|
+
let raw;
|
|
398
|
+
try {
|
|
399
|
+
raw = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
400
|
+
} catch (error) {
|
|
401
|
+
throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
402
|
+
}
|
|
403
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
|
|
404
|
+
const record = raw;
|
|
405
|
+
const schemaVersion = requiredString(record, "schema_version");
|
|
406
|
+
if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
|
|
407
|
+
const profileId = requiredString(record, "profile_id");
|
|
408
|
+
if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
|
|
409
|
+
if (namedProfileId && namedProfileId !== profileId) {
|
|
410
|
+
throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
|
|
411
|
+
}
|
|
412
|
+
const expectedRaw = record.expected_runtime;
|
|
413
|
+
if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
|
|
414
|
+
throw new Error("Profile expected_runtime must be an object");
|
|
415
|
+
}
|
|
416
|
+
const expected = expectedRaw;
|
|
417
|
+
const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
|
|
418
|
+
const expectedVersion = expected ? optionalString(expected, "version") : void 0;
|
|
419
|
+
const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
|
|
420
|
+
const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
|
|
421
|
+
const migrationsRaw = record.required_schema_migrations;
|
|
422
|
+
if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
423
|
+
throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
|
|
424
|
+
}
|
|
425
|
+
const environment = validateEnvironment(requiredString(record, "environment"));
|
|
426
|
+
const expectedChannel = validateChannel(expected?.channel);
|
|
427
|
+
if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
|
|
428
|
+
throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
|
|
429
|
+
}
|
|
430
|
+
const manifest = {
|
|
431
|
+
asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
|
|
432
|
+
database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
|
|
433
|
+
environment,
|
|
434
|
+
...expected ? {
|
|
435
|
+
expected_runtime: {
|
|
436
|
+
...expectedChannel ? { channel: expectedChannel } : {},
|
|
437
|
+
...expectedCodeFingerprint ? { code_fingerprint: expectedCodeFingerprint } : {},
|
|
438
|
+
...expectedCodeOrigin ? { code_origin: expectedCodeOrigin } : {},
|
|
439
|
+
...expectedGitSha ? { git_sha: expectedGitSha } : {},
|
|
440
|
+
...expectedVersion ? { version: expectedVersion } : {}
|
|
441
|
+
}
|
|
442
|
+
} : {},
|
|
443
|
+
profile_id: profileId,
|
|
444
|
+
...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
|
|
445
|
+
schema_version: lineageProfileSchemaVersion,
|
|
446
|
+
service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
|
|
447
|
+
};
|
|
448
|
+
return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
|
|
449
|
+
}
|
|
450
|
+
function lineageProfileFingerprint(profile) {
|
|
451
|
+
return createHash2("sha256").update(JSON.stringify({
|
|
452
|
+
asset_root: resolve2(profile.asset_root),
|
|
453
|
+
database_path: resolve2(profile.database_path),
|
|
454
|
+
environment: profile.environment,
|
|
455
|
+
profile_id: profile.profile_id,
|
|
456
|
+
schema_version: profile.schema_version,
|
|
457
|
+
service_origin: profile.service_origin
|
|
458
|
+
})).digest("hex");
|
|
459
|
+
}
|
|
460
|
+
function assertProfileRuntimePin(profile, runtime) {
|
|
461
|
+
if (!profile.expected_runtime?.code_fingerprint || !profile.expected_runtime.code_origin) {
|
|
462
|
+
throw new Error(`Profile ${profile.profile_id} must pin expected_runtime.code_fingerprint and expected_runtime.code_origin before binding or writing`);
|
|
463
|
+
}
|
|
464
|
+
if (!runtime?.code?.verified) {
|
|
465
|
+
throw new Error(`Profile ${profile.profile_id} requires a verified runtime code identity before binding or writing`);
|
|
466
|
+
}
|
|
467
|
+
if (runtime.code.fingerprint !== profile.expected_runtime.code_fingerprint) {
|
|
468
|
+
throw new Error(`Profile ${profile.profile_id} expects code fingerprint ${profile.expected_runtime.code_fingerprint}, found ${runtime.code.fingerprint}`);
|
|
469
|
+
}
|
|
470
|
+
if (runtime.code.origin !== profile.expected_runtime.code_origin) {
|
|
471
|
+
throw new Error(`Profile ${profile.profile_id} expects code origin ${profile.expected_runtime.code_origin}, found ${runtime.code.origin}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function assertProfileChannel(profile, channel) {
|
|
475
|
+
const requiredChannel = environmentChannel(profile.environment);
|
|
476
|
+
if (channel === requiredChannel) return;
|
|
477
|
+
if (profile.environment === "production") {
|
|
478
|
+
throw new Error(`Refusing to open production profile ${profile.profile_id} from ${channel} code; use the stable channel`);
|
|
479
|
+
}
|
|
480
|
+
throw new Error(`Profile ${profile.profile_id} requires the ${requiredChannel} channel, not ${channel}`);
|
|
481
|
+
}
|
|
482
|
+
function tableExists(database, table) {
|
|
483
|
+
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
484
|
+
}
|
|
485
|
+
function tableColumns(database, table) {
|
|
486
|
+
if (!tableExists(database, table)) return /* @__PURE__ */ new Set();
|
|
487
|
+
return new Set(database.prepare(`pragma table_info(${table})`).all().map((row) => row.name));
|
|
488
|
+
}
|
|
489
|
+
function inspectDatabase(profile) {
|
|
490
|
+
const result = {
|
|
491
|
+
exists: existsSync3(profile.database_path),
|
|
492
|
+
migration_keys: [],
|
|
493
|
+
path: profile.database_path
|
|
494
|
+
};
|
|
495
|
+
if (!result.exists) return result;
|
|
496
|
+
try {
|
|
497
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
498
|
+
const database = new DatabaseSync(profile.database_path, { readOnly: true });
|
|
499
|
+
try {
|
|
500
|
+
if (tableExists(database, "lineage_profile_identity")) {
|
|
501
|
+
const columns = tableColumns(database, "lineage_profile_identity");
|
|
502
|
+
const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "'' as profile_fingerprint";
|
|
503
|
+
const rows = database.prepare(`select profile_id, environment, bound_at, ${fingerprintExpression} from lineage_profile_identity`).all();
|
|
504
|
+
if (rows.length === 1) {
|
|
505
|
+
const identity = {
|
|
506
|
+
bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
|
|
507
|
+
environment: String(rows[0].environment),
|
|
508
|
+
profile_fingerprint: typeof rows[0].profile_fingerprint === "string" ? rows[0].profile_fingerprint : "",
|
|
509
|
+
profile_id: String(rows[0].profile_id)
|
|
510
|
+
};
|
|
511
|
+
result.identity = identity;
|
|
512
|
+
} else {
|
|
513
|
+
result.error = `Expected exactly one lineage_profile_identity row, found ${rows.length}`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (tableExists(database, "lineage_schema_migrations")) {
|
|
517
|
+
result.migration_keys = database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
518
|
+
}
|
|
519
|
+
} finally {
|
|
520
|
+
database.close();
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
function doctorLineageProfile(selector, runtime) {
|
|
528
|
+
const checks = [];
|
|
529
|
+
const result = {
|
|
530
|
+
checks,
|
|
531
|
+
ok: false,
|
|
532
|
+
runtime: {
|
|
533
|
+
channel: runtime.channel,
|
|
534
|
+
code_fingerprint: runtime.code?.fingerprint,
|
|
535
|
+
code_origin: runtime.code?.origin,
|
|
536
|
+
code_verified: runtime.code?.verified,
|
|
537
|
+
git_sha: runtime.gitSha,
|
|
538
|
+
version: runtime.version
|
|
539
|
+
},
|
|
540
|
+
schema_version: lineageProfileDoctorSchemaVersion
|
|
541
|
+
};
|
|
542
|
+
let profile;
|
|
543
|
+
try {
|
|
544
|
+
profile = resolveLineageProfile(selector);
|
|
545
|
+
result.profile = profile;
|
|
546
|
+
result.manifest_path = profile.manifest_path;
|
|
547
|
+
checks.push({ id: "manifest", message: `Loaded ${profile.profile_id}`, status: "pass" });
|
|
548
|
+
} catch (error) {
|
|
549
|
+
checks.push({ id: "manifest", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
assertProfileChannel(profile, runtime.channel);
|
|
554
|
+
checks.push({ id: "runtime_channel", message: `${runtime.channel} code matches ${profile.environment}`, status: "pass" });
|
|
555
|
+
} catch (error) {
|
|
556
|
+
checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
assertProfileRuntimePin(profile, runtime);
|
|
560
|
+
checks.push({ id: "runtime_code", message: `Verified ${runtime.code.origin} code ${runtime.code.fingerprint}`, status: "pass" });
|
|
561
|
+
} catch (error) {
|
|
562
|
+
checks.push({ id: "runtime_code", message: error instanceof Error ? error.message : String(error), status: "fail" });
|
|
563
|
+
}
|
|
564
|
+
if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
|
|
565
|
+
checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
|
|
566
|
+
} else {
|
|
567
|
+
checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
|
|
568
|
+
}
|
|
569
|
+
if (profile.expected_runtime?.git_sha && profile.expected_runtime.git_sha !== runtime.gitSha) {
|
|
570
|
+
checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
|
|
571
|
+
} else if (profile.expected_runtime?.git_sha) {
|
|
572
|
+
checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
|
|
573
|
+
}
|
|
574
|
+
const assetExists = existsSync3(profile.asset_root);
|
|
575
|
+
const assetIsDirectory = assetExists && statSync3(profile.asset_root).isDirectory();
|
|
576
|
+
result.asset_root = { exists: assetExists, is_directory: assetIsDirectory, path: profile.asset_root };
|
|
577
|
+
checks.push({
|
|
578
|
+
id: "asset_root",
|
|
579
|
+
message: assetIsDirectory ? `Asset root exists: ${profile.asset_root}` : `Asset root is missing or not a directory: ${profile.asset_root}`,
|
|
580
|
+
status: assetIsDirectory ? "pass" : "fail"
|
|
581
|
+
});
|
|
582
|
+
const database = inspectDatabase(profile);
|
|
583
|
+
result.database = database;
|
|
584
|
+
checks.push({
|
|
585
|
+
id: "database_exists",
|
|
586
|
+
message: database.exists ? `Database exists: ${profile.database_path}` : `Database does not exist: ${profile.database_path}`,
|
|
587
|
+
status: database.exists ? "pass" : "fail"
|
|
588
|
+
});
|
|
589
|
+
if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
|
|
590
|
+
if (!database.identity) {
|
|
591
|
+
checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
|
|
592
|
+
} else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment || database.identity.profile_fingerprint !== profile.profile_fingerprint) {
|
|
593
|
+
checks.push({
|
|
594
|
+
id: "database_identity",
|
|
595
|
+
message: `Database identity ${database.identity.profile_id}/${database.identity.environment}/${database.identity.profile_fingerprint || "no-fingerprint"} does not match ${profile.profile_id}/${profile.environment}/${profile.profile_fingerprint}`,
|
|
596
|
+
status: "fail"
|
|
597
|
+
});
|
|
598
|
+
} else {
|
|
599
|
+
checks.push({ id: "database_identity", message: `Database is bound to ${profile.profile_id}`, status: "pass" });
|
|
600
|
+
}
|
|
601
|
+
const missingMigrations = (profile.required_schema_migrations || []).filter((key) => !database.migration_keys.includes(key));
|
|
602
|
+
checks.push({
|
|
603
|
+
id: "database_schema",
|
|
604
|
+
message: missingMigrations.length ? `Missing required schema migrations: ${missingMigrations.join(", ")}` : `${database.migration_keys.length} schema migration marker(s) available`,
|
|
605
|
+
status: missingMigrations.length ? "fail" : "pass"
|
|
606
|
+
});
|
|
607
|
+
result.ok = checks.every((check) => check.status !== "fail");
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
function runtimeProfileIdentity(channel) {
|
|
611
|
+
const selector = process.env.LINEAGE_PROFILE;
|
|
612
|
+
const id = process.env.LINEAGE_PROFILE_ID;
|
|
613
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
614
|
+
if (selector && id && environment) {
|
|
615
|
+
return {
|
|
616
|
+
bound: true,
|
|
617
|
+
environment,
|
|
618
|
+
fingerprint: process.env.LINEAGE_PROFILE_FINGERPRINT,
|
|
619
|
+
id,
|
|
620
|
+
manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
|
|
621
|
+
service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
bound: false,
|
|
626
|
+
environment: channelEnvironment(channel),
|
|
627
|
+
id: "legacy-unbound",
|
|
628
|
+
warning: id || environment || process.env.LINEAGE_PROFILE_FINGERPRINT || 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."
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function assertRuntimeProfileSafety(channel) {
|
|
632
|
+
const hasDerivedIdentity = Boolean(
|
|
633
|
+
process.env.LINEAGE_PROFILE_ID || process.env.LINEAGE_PROFILE_ENVIRONMENT || process.env.LINEAGE_PROFILE_FINGERPRINT || process.env.LINEAGE_PROFILE_MANIFEST || process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
|
|
634
|
+
);
|
|
635
|
+
if (hasDerivedIdentity && !process.env.LINEAGE_PROFILE) {
|
|
636
|
+
throw new Error("Derived Lineage profile identity requires LINEAGE_PROFILE; start through the Lineage CLI with --profile");
|
|
637
|
+
}
|
|
638
|
+
const profile = runtimeProfileIdentity(channel);
|
|
639
|
+
if (profile.bound && profile.environment === "production" && channel !== "stable") {
|
|
640
|
+
throw new Error(`Refusing to open production profile ${profile.id} from ${channel} code; use the stable channel`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function assertUnselectedDatabaseIsUnbound(runtime) {
|
|
644
|
+
if (!runtime.schema.profile_id) return;
|
|
645
|
+
const environment = runtime.schema.profile_environment || "unknown";
|
|
646
|
+
throw new Error(
|
|
647
|
+
`Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
function assertResolvedRuntimeProfileEnvironment(profile) {
|
|
651
|
+
const serviceUrl = new URL(profile.service_origin);
|
|
652
|
+
const expected = /* @__PURE__ */ new Map([
|
|
653
|
+
["LINEAGE_PROFILE_ID", profile.profile_id],
|
|
654
|
+
["LINEAGE_PROFILE_ENVIRONMENT", profile.environment],
|
|
655
|
+
["LINEAGE_PROFILE_FINGERPRINT", profile.profile_fingerprint],
|
|
656
|
+
["LINEAGE_PROFILE_MANIFEST", profile.manifest_path],
|
|
657
|
+
["LINEAGE_PROFILE_SERVICE_ORIGIN", profile.service_origin],
|
|
658
|
+
["LINEAGE_DB", profile.database_path],
|
|
659
|
+
["LINEAGE_ASSET_ROOT", profile.asset_root],
|
|
660
|
+
["HOST", serviceUrl.hostname],
|
|
661
|
+
["PORT", serviceUrl.port]
|
|
662
|
+
]);
|
|
663
|
+
const conflicts = [];
|
|
664
|
+
for (const [key, expectedValue] of expected) {
|
|
665
|
+
const actual = process.env[key];
|
|
666
|
+
const pathValue = key === "LINEAGE_PROFILE_MANIFEST" || key === "LINEAGE_DB" || key === "LINEAGE_ASSET_ROOT";
|
|
667
|
+
const matches = actual && (pathValue ? resolve2(actual) === resolve2(expectedValue) : actual === expectedValue);
|
|
668
|
+
if (!matches) conflicts.push(`${key}=${actual || "(missing)"}`);
|
|
669
|
+
}
|
|
670
|
+
if (conflicts.length > 0) {
|
|
671
|
+
throw new Error(`Resolved profile environment conflicts with ${profile.profile_id}: ${conflicts.join(", ")}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/server/profileWriterLease.ts
|
|
676
|
+
var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
|
|
677
|
+
var ownerFileName = "owner.json";
|
|
678
|
+
var ProfileWriterLeaseConflictError = class extends Error {
|
|
679
|
+
constructor(message, owner) {
|
|
680
|
+
super(message);
|
|
681
|
+
this.owner = owner;
|
|
682
|
+
this.name = "ProfileWriterLeaseConflictError";
|
|
683
|
+
}
|
|
684
|
+
owner;
|
|
685
|
+
};
|
|
686
|
+
function profileWriterLockPath(profile) {
|
|
687
|
+
return join4(dirname3(profile.manifest_path), "writer.lock");
|
|
688
|
+
}
|
|
689
|
+
function ownerPath(lockPath) {
|
|
690
|
+
return join4(lockPath, ownerFileName);
|
|
691
|
+
}
|
|
692
|
+
function errorCode(error) {
|
|
693
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
694
|
+
}
|
|
695
|
+
function readOwner(lockPath) {
|
|
696
|
+
const stat = lstatSync(lockPath);
|
|
697
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
698
|
+
throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
|
|
699
|
+
}
|
|
700
|
+
let raw;
|
|
701
|
+
try {
|
|
702
|
+
raw = JSON.parse(readFileSync3(ownerPath(lockPath), "utf8"));
|
|
703
|
+
} catch (error) {
|
|
704
|
+
throw new Error(`Writer lease metadata is unreadable at ${lockPath}; refusing automatic recovery`, { cause: error });
|
|
705
|
+
}
|
|
706
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
707
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
708
|
+
}
|
|
709
|
+
const owner = raw;
|
|
710
|
+
if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || typeof owner.profile_fingerprint !== "string" || !/^[a-f0-9]{64}$/i.test(owner.profile_fingerprint) || 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) {
|
|
711
|
+
throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
|
|
712
|
+
}
|
|
713
|
+
return owner;
|
|
714
|
+
}
|
|
715
|
+
function processIsAlive(pid) {
|
|
716
|
+
try {
|
|
717
|
+
process.kill(pid, 0);
|
|
718
|
+
return true;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (errorCode(error) === "ESRCH") return false;
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function createLeaseDirectory(lockPath, owner) {
|
|
725
|
+
mkdirSync3(lockPath, { mode: 448 });
|
|
726
|
+
try {
|
|
727
|
+
writeFileSync2(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
|
|
728
|
+
`, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
729
|
+
} catch (error) {
|
|
730
|
+
rmSync2(lockPath, { force: true, recursive: true });
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function reclaimDeadOwner(lockPath, owner) {
|
|
735
|
+
if (processIsAlive(owner.pid)) {
|
|
736
|
+
const { token: _token, ...publicOwner } = owner;
|
|
737
|
+
throw new ProfileWriterLeaseConflictError(
|
|
738
|
+
`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`,
|
|
739
|
+
publicOwner
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
|
|
743
|
+
const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
|
|
744
|
+
try {
|
|
745
|
+
renameSync(lockPath, tombstone);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
if (errorCode(error) === "ENOENT") return;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
rmSync2(tombstone, { force: true, recursive: true });
|
|
751
|
+
}
|
|
752
|
+
function acquireProfileWriterLease(profile, channel, role = "service") {
|
|
753
|
+
assertProfileChannel(profile, channel);
|
|
754
|
+
const lockPath = profileWriterLockPath(profile);
|
|
755
|
+
const owner = {
|
|
756
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
+
environment: profile.environment,
|
|
758
|
+
pid: process.pid,
|
|
759
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
760
|
+
profile_id: profile.profile_id,
|
|
761
|
+
role,
|
|
762
|
+
schema_version: writerLeaseSchemaVersion,
|
|
763
|
+
...role === "service" ? { service_origin: profile.service_origin } : {},
|
|
764
|
+
token: randomUUID2()
|
|
765
|
+
};
|
|
766
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
767
|
+
try {
|
|
768
|
+
createLeaseDirectory(lockPath, owner);
|
|
769
|
+
process.env.LINEAGE_WRITER_LEASE_TOKEN = owner.token;
|
|
770
|
+
process.env.LINEAGE_WRITER_LOCK_PATH = lockPath;
|
|
771
|
+
const { token: _token, ...publicOwner } = owner;
|
|
772
|
+
let released = false;
|
|
773
|
+
return {
|
|
774
|
+
authenticate: (candidate) => {
|
|
775
|
+
if (!candidate) return false;
|
|
776
|
+
const expected = Buffer.from(owner.token);
|
|
777
|
+
const actual = Buffer.from(candidate);
|
|
778
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
779
|
+
},
|
|
780
|
+
lock_path: lockPath,
|
|
781
|
+
owner: publicOwner,
|
|
782
|
+
release: () => {
|
|
783
|
+
if (released) return;
|
|
784
|
+
released = true;
|
|
785
|
+
try {
|
|
786
|
+
const current = readOwner(lockPath);
|
|
787
|
+
if (current.token === owner.token && current.pid === owner.pid) rmSync2(lockPath, { force: true, recursive: true });
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
791
|
+
if (process.env.LINEAGE_WRITER_LOCK_PATH === lockPath) delete process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
} catch (error) {
|
|
795
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
796
|
+
let existing;
|
|
797
|
+
try {
|
|
798
|
+
existing = readOwner(lockPath);
|
|
799
|
+
} catch (readError) {
|
|
800
|
+
if (errorCode(readError) === "ENOENT") continue;
|
|
801
|
+
throw readError;
|
|
802
|
+
}
|
|
803
|
+
if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment || existing.profile_fingerprint !== profile.profile_fingerprint) {
|
|
804
|
+
throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
|
|
805
|
+
}
|
|
806
|
+
reclaimDeadOwner(lockPath, existing);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
|
|
810
|
+
}
|
|
811
|
+
function assertProfileWriterLeaseHeld() {
|
|
812
|
+
if (!process.env.LINEAGE_PROFILE) {
|
|
813
|
+
throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only");
|
|
814
|
+
}
|
|
815
|
+
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
816
|
+
const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
|
|
817
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
818
|
+
const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
|
|
819
|
+
const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
|
|
820
|
+
const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
|
|
821
|
+
if (!profileId || !profileFingerprint || !environment || !manifestPath || !lockPath || !token) {
|
|
822
|
+
throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
|
|
823
|
+
}
|
|
824
|
+
const expectedLockPath = join4(dirname3(resolve3(manifestPath)), "writer.lock");
|
|
825
|
+
if (resolve3(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile manifest");
|
|
826
|
+
const owner = readOwner(lockPath);
|
|
827
|
+
if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.profile_fingerprint !== profileFingerprint || owner.environment !== environment) {
|
|
828
|
+
throw new Error("Current process does not own the selected Lineage profile writer lease");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function assertSelectedProfileDatabaseIdentity(database) {
|
|
832
|
+
const profileId = process.env.LINEAGE_PROFILE_ID;
|
|
833
|
+
const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
|
|
834
|
+
const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
|
|
835
|
+
if (!process.env.LINEAGE_PROFILE || !profileId || !environment || !profileFingerprint) {
|
|
836
|
+
throw new Error("Writable SQLite identity validation requires a fully resolved named Lineage profile");
|
|
837
|
+
}
|
|
838
|
+
const table = database.prepare("select name from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
|
|
839
|
+
if (!table) throw new Error(`Refusing writable open: database is not bound to Lineage profile ${profileId}`);
|
|
840
|
+
const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
|
|
841
|
+
if (!columns.has("profile_id") || !columns.has("environment") || !columns.has("profile_fingerprint")) {
|
|
842
|
+
throw new Error(`Refusing writable open: database identity for ${profileId} is missing required fingerprint fields`);
|
|
843
|
+
}
|
|
844
|
+
const rows = database.prepare("select profile_id, environment, profile_fingerprint from lineage_profile_identity").all();
|
|
845
|
+
if (rows.length !== 1) throw new Error(`Refusing writable open: expected exactly one database profile identity, found ${rows.length}`);
|
|
846
|
+
const identity = rows[0];
|
|
847
|
+
if (identity.profile_id !== profileId || identity.environment !== environment || identity.profile_fingerprint !== profileFingerprint) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`Refusing writable open: database identity ${String(identity.profile_id)}/${String(identity.environment)}/${String(identity.profile_fingerprint)} does not match selected profile ${profileId}/${environment}/${profileFingerprint}`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/server/assetLineageDb.ts
|
|
855
|
+
var require3 = createRequire2(import.meta.url);
|
|
289
856
|
function nowIso() {
|
|
290
857
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
291
858
|
}
|
|
292
859
|
function lineageDbPath() {
|
|
293
|
-
return process.env.LINEAGE_DB ||
|
|
860
|
+
return process.env.LINEAGE_DB || join5(packageRoot, ".lineage", "asset-lineage.sqlite");
|
|
294
861
|
}
|
|
295
862
|
function lineageDb() {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
863
|
+
const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
|
|
864
|
+
if (!readOnly) assertProfileWriterLeaseHeld();
|
|
865
|
+
if (!readOnly && !existsSync5(lineageDbPath())) {
|
|
866
|
+
throw new Error(`Refusing writable open of missing profile database ${lineageDbPath()}; bind an existing database or create a non-production clone first`);
|
|
867
|
+
}
|
|
868
|
+
if (!readOnly) mkdirSync4(join5(lineageDbPath(), ".."), { recursive: true });
|
|
869
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
870
|
+
const database = readOnly ? new DatabaseSync(lineageDbPath(), { readOnly: true }) : new DatabaseSync(lineageDbPath());
|
|
871
|
+
if (process.env.LINEAGE_PROFILE) assertSelectedProfileDatabaseIdentity(database);
|
|
299
872
|
database.exec("PRAGMA foreign_keys = ON");
|
|
300
873
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
874
|
+
if (readOnly) return database;
|
|
875
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
301
876
|
database.exec(`
|
|
302
877
|
create table if not exists projects (
|
|
303
878
|
id text primary key,
|
|
@@ -1131,21 +1706,21 @@ function ledgerWorkflowStates(database, project, records, sources) {
|
|
|
1131
1706
|
|
|
1132
1707
|
// src/server/assetCore.ts
|
|
1133
1708
|
function isPackageRoot(path) {
|
|
1134
|
-
const packageJson =
|
|
1135
|
-
if (!
|
|
1709
|
+
const packageJson = join6(path, "package.json");
|
|
1710
|
+
if (!existsSync6(packageJson)) return false;
|
|
1136
1711
|
try {
|
|
1137
|
-
const packageInfo2 = JSON.parse(
|
|
1712
|
+
const packageInfo2 = JSON.parse(readFileSync4(packageJson, "utf8"));
|
|
1138
1713
|
return packageInfo2.name === "@mean-weasel/lineage";
|
|
1139
1714
|
} catch {
|
|
1140
1715
|
return false;
|
|
1141
1716
|
}
|
|
1142
1717
|
}
|
|
1143
1718
|
function resolvePackageRoot() {
|
|
1144
|
-
const moduleDir =
|
|
1719
|
+
const moduleDir = dirname4(fileURLToPath(import.meta.url));
|
|
1145
1720
|
const candidates = [
|
|
1146
1721
|
process.env.LINEAGE_REPO_ROOT,
|
|
1147
|
-
|
|
1148
|
-
|
|
1722
|
+
resolve4(moduleDir, ".."),
|
|
1723
|
+
resolve4(moduleDir, "../.."),
|
|
1149
1724
|
process.cwd()
|
|
1150
1725
|
].filter((candidate) => Boolean(candidate));
|
|
1151
1726
|
const root = candidates.find(isPackageRoot);
|
|
@@ -1153,7 +1728,11 @@ function resolvePackageRoot() {
|
|
|
1153
1728
|
return root;
|
|
1154
1729
|
}
|
|
1155
1730
|
var packageRoot = resolvePackageRoot();
|
|
1156
|
-
var repoRoot =
|
|
1731
|
+
var repoRoot = resolve4(process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
1732
|
+
function setLineageAssetRoot(path) {
|
|
1733
|
+
repoRoot = resolve4(path || packageRoot);
|
|
1734
|
+
return repoRoot;
|
|
1735
|
+
}
|
|
1157
1736
|
var defaultProject = "demo-project";
|
|
1158
1737
|
var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
|
|
1159
1738
|
var publicFallbackBucket = "lineage-demo-assets";
|
|
@@ -1179,16 +1758,16 @@ function cleanProject(project = defaultProject) {
|
|
|
1179
1758
|
return project;
|
|
1180
1759
|
}
|
|
1181
1760
|
function catalogPath(project = defaultProject) {
|
|
1182
|
-
return
|
|
1761
|
+
return join6(repoRoot, cleanProject(project), "assets", "catalog.json");
|
|
1183
1762
|
}
|
|
1184
1763
|
function fixtureCatalogPath(project = defaultProject) {
|
|
1185
|
-
return
|
|
1764
|
+
return join6(packageRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
|
|
1186
1765
|
}
|
|
1187
1766
|
function resolvedCatalogPath(project = defaultProject) {
|
|
1188
1767
|
const path = catalogPath(project);
|
|
1189
|
-
if (
|
|
1768
|
+
if (existsSync6(path)) return path;
|
|
1190
1769
|
const clean = cleanProject(project);
|
|
1191
|
-
if (clean === defaultProject &&
|
|
1770
|
+
if (clean === defaultProject && existsSync6(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
|
|
1192
1771
|
return path;
|
|
1193
1772
|
}
|
|
1194
1773
|
function normalizeCatalog(catalog, fallbackProject = defaultProject) {
|
|
@@ -1305,7 +1884,7 @@ function defaultFallbackCatalog() {
|
|
|
1305
1884
|
}, defaultProject);
|
|
1306
1885
|
}
|
|
1307
1886
|
function isDefaultFallbackCatalog(catalog) {
|
|
1308
|
-
return catalog.project === defaultProject && !
|
|
1887
|
+
return catalog.project === defaultProject && !existsSync6(catalogPath(defaultProject));
|
|
1309
1888
|
}
|
|
1310
1889
|
function fallbackPreviewDataUrl(asset) {
|
|
1311
1890
|
const label = `${asset.title}
|
|
@@ -1318,9 +1897,9 @@ function escapeSvgText2(value) {
|
|
|
1318
1897
|
}
|
|
1319
1898
|
function loadCatalog(project = defaultProject) {
|
|
1320
1899
|
const path = catalogPath(project);
|
|
1321
|
-
if (
|
|
1900
|
+
if (existsSync6(path)) {
|
|
1322
1901
|
try {
|
|
1323
|
-
return normalizeCatalog(JSON.parse(
|
|
1902
|
+
return normalizeCatalog(JSON.parse(readFileSync4(path, "utf8")), project);
|
|
1324
1903
|
} catch (error) {
|
|
1325
1904
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
1326
1905
|
throw new LineageAssetError(`Missing catalog: ${path}`, 404);
|
|
@@ -1331,8 +1910,8 @@ function loadCatalog(project = defaultProject) {
|
|
|
1331
1910
|
const clean = cleanProject(project);
|
|
1332
1911
|
if (clean === defaultProject) {
|
|
1333
1912
|
const fixturePath = fixtureCatalogPath(clean);
|
|
1334
|
-
if (
|
|
1335
|
-
return normalizeCatalog(JSON.parse(
|
|
1913
|
+
if (existsSync6(fixturePath)) {
|
|
1914
|
+
return normalizeCatalog(JSON.parse(readFileSync4(fixturePath, "utf8")), project);
|
|
1336
1915
|
}
|
|
1337
1916
|
return defaultFallbackCatalog();
|
|
1338
1917
|
}
|
|
@@ -1340,8 +1919,8 @@ function loadCatalog(project = defaultProject) {
|
|
|
1340
1919
|
}
|
|
1341
1920
|
function saveCatalog(project, catalog) {
|
|
1342
1921
|
const normalized = normalizeCatalog(catalog, project);
|
|
1343
|
-
|
|
1344
|
-
|
|
1922
|
+
mkdirSync5(dirname4(catalogPath(project)), { recursive: true });
|
|
1923
|
+
writeFileSync3(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
|
|
1345
1924
|
`);
|
|
1346
1925
|
return normalized;
|
|
1347
1926
|
}
|
|
@@ -1366,7 +1945,7 @@ function runAws(args) {
|
|
|
1366
1945
|
return run("aws", args);
|
|
1367
1946
|
}
|
|
1368
1947
|
function listProjects() {
|
|
1369
|
-
const projects = readdirSync2(repoRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && projectNamePattern.test(entry.name) &&
|
|
1948
|
+
const projects = readdirSync2(repoRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && projectNamePattern.test(entry.name) && existsSync6(catalogPath(entry.name))).flatMap((entry) => {
|
|
1370
1949
|
try {
|
|
1371
1950
|
const catalog = loadCatalog(entry.name);
|
|
1372
1951
|
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 +2104,15 @@ function deleteObjectGuarded(project, assetId, confirmation) {
|
|
|
1525
2104
|
return storageAdapter.deleteObjectGuarded(project, assetId, confirmation);
|
|
1526
2105
|
}
|
|
1527
2106
|
function ensureUploadDir() {
|
|
1528
|
-
const dir =
|
|
1529
|
-
|
|
2107
|
+
const dir = join6(repoRoot, ".asset-scratch", "studio-uploads");
|
|
2108
|
+
mkdirSync5(dir, { recursive: true });
|
|
1530
2109
|
return dir;
|
|
1531
2110
|
}
|
|
1532
2111
|
function cleanupUploadedTemp(file) {
|
|
1533
2112
|
if (!file) return;
|
|
1534
2113
|
const uploadRoot = ensureUploadDir();
|
|
1535
|
-
const resolved =
|
|
1536
|
-
if (resolved.startsWith(`${uploadRoot}/`) &&
|
|
2114
|
+
const resolved = resolve4(file);
|
|
2115
|
+
if (resolved.startsWith(`${uploadRoot}/`) && existsSync6(resolved)) {
|
|
1537
2116
|
unlinkSync(resolved);
|
|
1538
2117
|
}
|
|
1539
2118
|
}
|
|
@@ -1567,7 +2146,7 @@ function lookupAssets(project, assetIds) {
|
|
|
1567
2146
|
}
|
|
1568
2147
|
|
|
1569
2148
|
// src/server/assetLineage.ts
|
|
1570
|
-
import { join as
|
|
2149
|
+
import { join as join7 } from "node:path";
|
|
1571
2150
|
|
|
1572
2151
|
// src/server/assetLineageSelection.ts
|
|
1573
2152
|
var LINEAGE_NEXT_VARIATION_LIMIT = 3;
|
|
@@ -1590,7 +2169,7 @@ function normalizeSelectionInput(fields) {
|
|
|
1590
2169
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1591
2170
|
|
|
1592
2171
|
// src/server/agentClaims.ts
|
|
1593
|
-
import { createHash as
|
|
2172
|
+
import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
1594
2173
|
var defaultTtlSeconds = 20 * 60;
|
|
1595
2174
|
var idleAfterSeconds = 5 * 60;
|
|
1596
2175
|
var staleAfterSeconds = 15 * 60;
|
|
@@ -1626,12 +2205,12 @@ function randomId(prefix) {
|
|
|
1626
2205
|
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1627
2206
|
}
|
|
1628
2207
|
function tokenHash(token) {
|
|
1629
|
-
return
|
|
2208
|
+
return createHash4("sha256").update(token).digest("hex");
|
|
1630
2209
|
}
|
|
1631
2210
|
function safeEqual(a, b) {
|
|
1632
2211
|
const left = Buffer.from(a);
|
|
1633
2212
|
const right = Buffer.from(b);
|
|
1634
|
-
return left.length === right.length &&
|
|
2213
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
1635
2214
|
}
|
|
1636
2215
|
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1637
2216
|
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
@@ -1658,6 +2237,12 @@ function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1658
2237
|
}
|
|
1659
2238
|
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1660
2239
|
const heartbeatAt = String(row.heartbeat_at);
|
|
2240
|
+
const persistedStatus = String(row.status);
|
|
2241
|
+
const state = derivedState({
|
|
2242
|
+
expires_at: String(row.expires_at),
|
|
2243
|
+
heartbeat_at: heartbeatAt,
|
|
2244
|
+
status: persistedStatus
|
|
2245
|
+
}, now);
|
|
1661
2246
|
return {
|
|
1662
2247
|
id: String(row.id),
|
|
1663
2248
|
project: String(row.project_id),
|
|
@@ -1669,7 +2254,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1669
2254
|
agent_name: String(row.agent_name),
|
|
1670
2255
|
agent_kind: String(row.agent_kind),
|
|
1671
2256
|
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1672
|
-
status:
|
|
2257
|
+
status: persistedStatus === "active" && state === "expired" ? "expired" : persistedStatus,
|
|
1673
2258
|
created_at: String(row.created_at),
|
|
1674
2259
|
heartbeat_at: heartbeatAt,
|
|
1675
2260
|
expires_at: String(row.expires_at),
|
|
@@ -1679,11 +2264,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
|
1679
2264
|
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1680
2265
|
metadata: parseMetadata(row.metadata_json),
|
|
1681
2266
|
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1682
|
-
derived_state:
|
|
1683
|
-
expires_at: String(row.expires_at),
|
|
1684
|
-
heartbeat_at: heartbeatAt,
|
|
1685
|
-
status: String(row.status)
|
|
1686
|
-
}, now)
|
|
2267
|
+
derived_state: state
|
|
1687
2268
|
};
|
|
1688
2269
|
}
|
|
1689
2270
|
function eventToRow(row) {
|
|
@@ -1825,7 +2406,7 @@ function createAgentClaim(fields) {
|
|
|
1825
2406
|
function listAgentClaims(project) {
|
|
1826
2407
|
const database = lineageDb();
|
|
1827
2408
|
try {
|
|
1828
|
-
expireActiveClaims(database);
|
|
2409
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1829
2410
|
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
2411
|
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1831
2412
|
} finally {
|
|
@@ -1835,7 +2416,7 @@ function listAgentClaims(project) {
|
|
|
1835
2416
|
function inspectAgentClaim(claimId, project) {
|
|
1836
2417
|
const database = lineageDb();
|
|
1837
2418
|
try {
|
|
1838
|
-
expireActiveClaims(database);
|
|
2419
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
|
|
1839
2420
|
const claim = findClaimById(database, claimId, project);
|
|
1840
2421
|
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1841
2422
|
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
@@ -2462,6 +3043,44 @@ function cancelLineageIterateTasksForAssets(project, fields) {
|
|
|
2462
3043
|
taskId: task.id
|
|
2463
3044
|
}));
|
|
2464
3045
|
}
|
|
3046
|
+
function resolveLineageTask(project, fields) {
|
|
3047
|
+
const normalizedProject = normalizeProject(project);
|
|
3048
|
+
const actor = normalizeActor(fields.actor, "Resolve actor");
|
|
3049
|
+
const database = lineageDb();
|
|
3050
|
+
try {
|
|
3051
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
3052
|
+
if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
|
|
3053
|
+
if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
3054
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
3055
|
+
throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
|
|
3056
|
+
}
|
|
3057
|
+
const timestamp = nowIso();
|
|
3058
|
+
const resolvedTask = {
|
|
3059
|
+
...task,
|
|
3060
|
+
status: "resolved",
|
|
3061
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
3062
|
+
resolved_at: timestamp,
|
|
3063
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId,
|
|
3064
|
+
updated_at: timestamp
|
|
3065
|
+
};
|
|
3066
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
|
|
3067
|
+
transaction(database, () => {
|
|
3068
|
+
const result = database.prepare(`
|
|
3069
|
+
update lineage_tasks
|
|
3070
|
+
set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
|
|
3071
|
+
where id = ? and status = ?
|
|
3072
|
+
`).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
|
|
3073
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
|
|
3074
|
+
recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
|
|
3075
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
3076
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId
|
|
3077
|
+
});
|
|
3078
|
+
});
|
|
3079
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
3080
|
+
} finally {
|
|
3081
|
+
database.close();
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
2465
3084
|
|
|
2466
3085
|
// src/server/assetLineageWorkspaces.ts
|
|
2467
3086
|
var actors = /* @__PURE__ */ new Set(["human", "agent", "system"]);
|
|
@@ -2536,9 +3155,18 @@ function knownRoots(database, project) {
|
|
|
2536
3155
|
and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
|
|
2537
3156
|
group by parent_asset_id
|
|
2538
3157
|
`).all(project, project, project, project);
|
|
2539
|
-
|
|
3158
|
+
const roots = /* @__PURE__ */ new Map();
|
|
3159
|
+
for (const row of rows) {
|
|
3160
|
+
const existing = roots.get(row.root_asset_id);
|
|
3161
|
+
roots.set(row.root_asset_id, {
|
|
3162
|
+
root_asset_id: row.root_asset_id,
|
|
3163
|
+
selected_at: row.selected_at || existing?.selected_at
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
return [...roots.values()];
|
|
2540
3167
|
}
|
|
2541
3168
|
function seedLegacyWorkspaces(database, project) {
|
|
3169
|
+
if (process.env.LINEAGE_DB_ACCESS === "read-only") return;
|
|
2542
3170
|
ensureProject3(database, project);
|
|
2543
3171
|
const timestamp = nowIso();
|
|
2544
3172
|
const statement = database.prepare(`
|
|
@@ -2562,6 +3190,34 @@ function seedLegacyWorkspaces(database, project) {
|
|
|
2562
3190
|
);
|
|
2563
3191
|
}
|
|
2564
3192
|
}
|
|
3193
|
+
function inferredLegacyWorkspaces(database, project) {
|
|
3194
|
+
if (process.env.LINEAGE_DB_ACCESS !== "read-only") return [];
|
|
3195
|
+
const persistedRoots = new Set(
|
|
3196
|
+
database.prepare("select root_asset_id from lineage_workspaces where project_id = ?").all(project).map((row) => row.root_asset_id)
|
|
3197
|
+
);
|
|
3198
|
+
const asset = database.prepare("select id, title from assets where project_id = ? and id = ?");
|
|
3199
|
+
return knownRoots(database, project).flatMap((root) => {
|
|
3200
|
+
if (persistedRoots.has(root.root_asset_id)) return [];
|
|
3201
|
+
const row = asset.get(project, root.root_asset_id);
|
|
3202
|
+
if (!row) return [];
|
|
3203
|
+
const timestamp = root.selected_at || nowIso();
|
|
3204
|
+
return [{
|
|
3205
|
+
active_at: root.selected_at,
|
|
3206
|
+
created_at: timestamp,
|
|
3207
|
+
created_by: "system",
|
|
3208
|
+
id: lineageWorkspaceId(project, root.root_asset_id),
|
|
3209
|
+
project,
|
|
3210
|
+
root_asset_id: root.root_asset_id,
|
|
3211
|
+
status: "active",
|
|
3212
|
+
title: `${row.title} lineage`,
|
|
3213
|
+
updated_at: timestamp
|
|
3214
|
+
}];
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
function sortWorkspaces(workspaces) {
|
|
3218
|
+
const statusRank = { active: 0, paused: 1, archived: 2 };
|
|
3219
|
+
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));
|
|
3220
|
+
}
|
|
2565
3221
|
function listRows(database, project) {
|
|
2566
3222
|
return database.prepare(`
|
|
2567
3223
|
select * from lineage_workspaces
|
|
@@ -2573,23 +3229,15 @@ function listRows(database, project) {
|
|
|
2573
3229
|
title
|
|
2574
3230
|
`).all(project).map(rowToWorkspace);
|
|
2575
3231
|
}
|
|
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
3232
|
function listLineageWorkspaces(project) {
|
|
2586
3233
|
const database = lineageDb();
|
|
2587
3234
|
try {
|
|
2588
3235
|
seedLegacyWorkspaces(database, project);
|
|
3236
|
+
const workspaces = sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]);
|
|
2589
3237
|
return {
|
|
2590
3238
|
project,
|
|
2591
|
-
active_workspace:
|
|
2592
|
-
workspaces
|
|
3239
|
+
active_workspace: workspaces.find((workspace) => workspace.status !== "archived") || null,
|
|
3240
|
+
workspaces,
|
|
2593
3241
|
fetchedAt: nowIso()
|
|
2594
3242
|
};
|
|
2595
3243
|
} finally {
|
|
@@ -2600,7 +3248,7 @@ function inspectLineageWorkspace(project, workspaceId) {
|
|
|
2600
3248
|
const database = lineageDb();
|
|
2601
3249
|
try {
|
|
2602
3250
|
seedLegacyWorkspaces(database, project);
|
|
2603
|
-
const workspace = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
|
|
3251
|
+
const workspace = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId) || inferredLegacyWorkspaces(database, project).find((item) => item.id === workspaceId || item.root_asset_id === workspaceId);
|
|
2604
3252
|
if (!workspace) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
|
|
2605
3253
|
return workspace;
|
|
2606
3254
|
} finally {
|
|
@@ -2732,7 +3380,7 @@ function activeLineageWorkspaceRoot(project) {
|
|
|
2732
3380
|
const database = lineageDb();
|
|
2733
3381
|
try {
|
|
2734
3382
|
seedLegacyWorkspaces(database, project);
|
|
2735
|
-
return
|
|
3383
|
+
return sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]).find((workspace) => workspace.status !== "archived")?.root_asset_id;
|
|
2736
3384
|
} finally {
|
|
2737
3385
|
database.close();
|
|
2738
3386
|
}
|
|
@@ -2794,7 +3442,7 @@ function upsertProject(database, project) {
|
|
|
2794
3442
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
2795
3443
|
values (?, ?, ?, ?, ?)
|
|
2796
3444
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
2797
|
-
`).run(project, project,
|
|
3445
|
+
`).run(project, project, join7(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
2798
3446
|
}
|
|
2799
3447
|
function upsertAsset(database, project, asset) {
|
|
2800
3448
|
const timestamp = nowIso();
|
|
@@ -2865,8 +3513,8 @@ function rootFor(database, project, assetId) {
|
|
|
2865
3513
|
}
|
|
2866
3514
|
function assertCanonicalRoot(database, project, root) {
|
|
2867
3515
|
requireAsset3(database, project, root);
|
|
2868
|
-
const
|
|
2869
|
-
if (
|
|
3516
|
+
const canonicalRoot2 = rootFor(database, project, root);
|
|
3517
|
+
if (canonicalRoot2 !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
2870
3518
|
}
|
|
2871
3519
|
function explicitWorkspaceRoot(database, project, assetId) {
|
|
2872
3520
|
const row = database.prepare(`
|
|
@@ -3013,6 +3661,13 @@ function assertNodeInRoot(database, project, root, node) {
|
|
|
3013
3661
|
const nodeRoot = rootFor(database, project, node);
|
|
3014
3662
|
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
3015
3663
|
}
|
|
3664
|
+
function assertAttemptAssetNotVisibleLineageNode(database, project, root, assetId) {
|
|
3665
|
+
const visibleNodeIds = /* @__PURE__ */ new Set([
|
|
3666
|
+
root,
|
|
3667
|
+
...descendants(database, project, root).flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])
|
|
3668
|
+
]);
|
|
3669
|
+
if (visibleNodeIds.has(assetId)) throw new LineageError(`Attempt asset ${assetId} is already a visible lineage node in ${root}`, 400);
|
|
3670
|
+
}
|
|
3016
3671
|
function canPreviewLocally(mediaType, localPath) {
|
|
3017
3672
|
return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
|
|
3018
3673
|
}
|
|
@@ -3387,6 +4042,74 @@ function promoteLineageAttempt(project, fields) {
|
|
|
3387
4042
|
database.close();
|
|
3388
4043
|
}
|
|
3389
4044
|
}
|
|
4045
|
+
function recordLineageRerollAttempt(project, fields) {
|
|
4046
|
+
const database = lineageDb();
|
|
4047
|
+
try {
|
|
4048
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
4049
|
+
requireAsset3(database, project, fields.assetId);
|
|
4050
|
+
assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
|
|
4051
|
+
const timestamp = nowIso();
|
|
4052
|
+
const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
|
|
4053
|
+
const attemptIndex = Number(maxRow.max_index || 1) + 1;
|
|
4054
|
+
const attempt = {
|
|
4055
|
+
id: `${project}:${fields.nodeAssetId}:attempt:${attemptIndex}`,
|
|
4056
|
+
project_id: project,
|
|
4057
|
+
node_asset_id: fields.nodeAssetId,
|
|
4058
|
+
asset_id: fields.assetId,
|
|
4059
|
+
attempt_index: attemptIndex,
|
|
4060
|
+
source: "reroll",
|
|
4061
|
+
prompt: fields.prompt,
|
|
4062
|
+
generation_job_id: fields.generationJobId,
|
|
4063
|
+
file_path: fields.filePath,
|
|
4064
|
+
checksum_sha256: fields.checksumSha256,
|
|
4065
|
+
created_at: timestamp,
|
|
4066
|
+
promoted_at: timestamp,
|
|
4067
|
+
is_current: true
|
|
4068
|
+
};
|
|
4069
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, attempt };
|
|
4070
|
+
database.exec("BEGIN IMMEDIATE");
|
|
4071
|
+
try {
|
|
4072
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
|
|
4073
|
+
database.prepare(`
|
|
4074
|
+
insert into asset_attempts (
|
|
4075
|
+
id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
|
|
4076
|
+
file_path, checksum_sha256, created_at, promoted_at, is_current
|
|
4077
|
+
) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, 1)
|
|
4078
|
+
`).run(
|
|
4079
|
+
attempt.id,
|
|
4080
|
+
project,
|
|
4081
|
+
fields.nodeAssetId,
|
|
4082
|
+
fields.assetId,
|
|
4083
|
+
attempt.attempt_index,
|
|
4084
|
+
fields.prompt,
|
|
4085
|
+
fields.generationJobId,
|
|
4086
|
+
fields.filePath,
|
|
4087
|
+
fields.checksumSha256,
|
|
4088
|
+
timestamp,
|
|
4089
|
+
timestamp
|
|
4090
|
+
);
|
|
4091
|
+
database.prepare(`
|
|
4092
|
+
update asset_reroll_requests
|
|
4093
|
+
set status = 'resolved', resolved_at = ?
|
|
4094
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
4095
|
+
`).run(timestamp, project, fields.rootAssetId, fields.nodeAssetId);
|
|
4096
|
+
database.prepare(`
|
|
4097
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
4098
|
+
values (?, 'unreviewed', ?, null, null, ?)
|
|
4099
|
+
on conflict(asset_id) do update set
|
|
4100
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
4101
|
+
ignored_at = excluded.ignored_at, updated_at = excluded.updated_at
|
|
4102
|
+
`).run(fields.nodeAssetId, timestamp, timestamp);
|
|
4103
|
+
database.exec("COMMIT");
|
|
4104
|
+
} catch (error) {
|
|
4105
|
+
database.exec("ROLLBACK");
|
|
4106
|
+
throw error;
|
|
4107
|
+
}
|
|
4108
|
+
return { ok: true, attempt };
|
|
4109
|
+
} finally {
|
|
4110
|
+
database.close();
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
3390
4113
|
function markLineageRerollRequest(project, fields) {
|
|
3391
4114
|
const database = lineageDb();
|
|
3392
4115
|
try {
|
|
@@ -3583,16 +4306,34 @@ function updateAssetReview(project, fields) {
|
|
|
3583
4306
|
return { ok: true, message: `Marked ${fields.assetId} ${fields.reviewState}`, asset_id: fields.assetId, review_state: fields.reviewState };
|
|
3584
4307
|
}
|
|
3585
4308
|
|
|
3586
|
-
// src/server/
|
|
3587
|
-
|
|
4309
|
+
// src/server/lineageRuntimeCommand.ts
|
|
4310
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
4311
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
4312
|
+
import { join as join8 } from "node:path";
|
|
4313
|
+
var require4 = createRequire3(import.meta.url);
|
|
4314
|
+
function lineagePublicPackageCommand() {
|
|
4315
|
+
if (process.env.LINEAGE_CHANNEL === "dev") {
|
|
4316
|
+
const sourceCli = join8(packageRoot, "src", "cli", "lineage-dev.ts");
|
|
4317
|
+
const builtCli = join8(packageRoot, "dist", "cli", "lineage-dev.js");
|
|
4318
|
+
return existsSync7(sourceCli) ? `${shellQuote(process.execPath)} --import ${shellQuote(require4.resolve("tsx"))} ${shellQuote(sourceCli)}` : `${shellQuote(process.execPath)} ${shellQuote(builtCli)}`;
|
|
4319
|
+
}
|
|
4320
|
+
if (process.env.LINEAGE_CHANNEL === "preview") return "LINEAGE_CHANNEL=preview npx --package @mean-weasel/lineage@next lineage-dev";
|
|
4321
|
+
return "npx @mean-weasel/lineage";
|
|
4322
|
+
}
|
|
3588
4323
|
function shellQuote(value) {
|
|
3589
4324
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3590
4325
|
}
|
|
4326
|
+
function lineageRuntimeSelector() {
|
|
4327
|
+
const manifest = process.env.LINEAGE_PROFILE_MANIFEST?.trim();
|
|
4328
|
+
return manifest ? `--profile ${shellQuote(manifest)}` : `--db ${shellQuote(lineageDbPath())}`;
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
// src/server/assetLineageHandoff.ts
|
|
3591
4332
|
function lineageCommand(command, project, rootAssetId) {
|
|
3592
|
-
return `${
|
|
4333
|
+
return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
|
|
3593
4334
|
}
|
|
3594
4335
|
function linkChildCommand(project, rootAssetId) {
|
|
3595
|
-
return `${
|
|
4336
|
+
return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
3596
4337
|
}
|
|
3597
4338
|
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
3598
4339
|
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
@@ -3634,7 +4375,7 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
3634
4375
|
},
|
|
3635
4376
|
handoff: {
|
|
3636
4377
|
next_command: lineageCommand("next", project, next.root_asset_id),
|
|
3637
|
-
inspect_command: asset ? `${
|
|
4378
|
+
inspect_command: asset ? `${lineagePublicPackageCommand()} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} ${lineageRuntimeSelector()} --json` : void 0,
|
|
3638
4379
|
link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
|
|
3639
4380
|
},
|
|
3640
4381
|
fetchedAt: nowIso()
|
|
@@ -5020,9 +5761,9 @@ function ensureProject5(database, project) {
|
|
|
5020
5761
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
5021
5762
|
`).run(project, project, timestamp, timestamp);
|
|
5022
5763
|
}
|
|
5023
|
-
function definitionFor(adapterType,
|
|
5024
|
-
const definition = definitions.find((item) => item.adapter_type === adapterType && item.provider ===
|
|
5025
|
-
if (!definition) throw new AdapterSettingsError(`Unsupported adapter setting: ${adapterType}/${
|
|
5764
|
+
function definitionFor(adapterType, provider2) {
|
|
5765
|
+
const definition = definitions.find((item) => item.adapter_type === adapterType && item.provider === provider2);
|
|
5766
|
+
if (!definition) throw new AdapterSettingsError(`Unsupported adapter setting: ${adapterType}/${provider2}`, 404);
|
|
5026
5767
|
return definition;
|
|
5027
5768
|
}
|
|
5028
5769
|
function seedDefaults(database, project) {
|
|
@@ -5054,22 +5795,22 @@ function parseConfig(value) {
|
|
|
5054
5795
|
return {};
|
|
5055
5796
|
}
|
|
5056
5797
|
}
|
|
5057
|
-
function credentialFor(
|
|
5058
|
-
if (
|
|
5798
|
+
function credentialFor(provider2, env) {
|
|
5799
|
+
if (provider2 === "s3") {
|
|
5059
5800
|
return { detected: false, label: "Optional local cloud CLI credential", secret_ref: null };
|
|
5060
5801
|
}
|
|
5061
|
-
if (
|
|
5802
|
+
if (provider2 === "buffer") {
|
|
5062
5803
|
const detected = Boolean(env.LINEAGE_SCHEDULER_TOKEN && env.LINEAGE_SCHEDULER_ORGANIZATION_ID);
|
|
5063
5804
|
return { detected, label: "LINEAGE_SCHEDULER_TOKEN + LINEAGE_SCHEDULER_ORGANIZATION_ID", secret_ref: "env:LINEAGE_SCHEDULER_TOKEN" };
|
|
5064
5805
|
}
|
|
5065
5806
|
return { detected: true, label: "No external secret required", secret_ref: null };
|
|
5066
5807
|
}
|
|
5067
|
-
function healthStatus(
|
|
5068
|
-
if (
|
|
5808
|
+
function healthStatus(provider2, enabled, config, credential) {
|
|
5809
|
+
if (provider2 === "s3") {
|
|
5069
5810
|
if (!enabled) return "live_disabled";
|
|
5070
5811
|
return config.bucket && config.region ? "not_tested" : "missing_config";
|
|
5071
5812
|
}
|
|
5072
|
-
if (
|
|
5813
|
+
if (provider2 === "buffer") {
|
|
5073
5814
|
if (!enabled) return "live_disabled";
|
|
5074
5815
|
return credential.detected ? "configured" : "dry_run_available";
|
|
5075
5816
|
}
|
|
@@ -5245,8 +5986,8 @@ function getAdapterStatus(project, env = process.env) {
|
|
|
5245
5986
|
}
|
|
5246
5987
|
|
|
5247
5988
|
// src/server/adapters/posting/bufferPostingService.ts
|
|
5248
|
-
import { mkdirSync as
|
|
5249
|
-
import { join as
|
|
5989
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5990
|
+
import { join as join9 } from "node:path";
|
|
5250
5991
|
|
|
5251
5992
|
// src/server/contentPostHandoff.ts
|
|
5252
5993
|
function readinessForPost(post) {
|
|
@@ -5742,12 +6483,12 @@ function resolvePost(project, fields) {
|
|
|
5742
6483
|
throw new PostingAdapterError("Buffer dry run requires postId or target=selected");
|
|
5743
6484
|
}
|
|
5744
6485
|
function payloadPath(project, postId) {
|
|
5745
|
-
return
|
|
6486
|
+
return join9(repoRoot, ".asset-scratch", "buffer-dry-runs", safeSegment(project), `${safeSegment(postId)}.json`);
|
|
5746
6487
|
}
|
|
5747
6488
|
function writePayload(project, postId, payload) {
|
|
5748
6489
|
const file = payloadPath(project, postId);
|
|
5749
|
-
|
|
5750
|
-
|
|
6490
|
+
mkdirSync6(join9(file, ".."), { recursive: true });
|
|
6491
|
+
writeFileSync4(file, `${JSON.stringify(payload, null, 2)}
|
|
5751
6492
|
`);
|
|
5752
6493
|
return file;
|
|
5753
6494
|
}
|
|
@@ -5856,11 +6597,11 @@ function registerAdapterRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
5856
6597
|
import { Router } from "express";
|
|
5857
6598
|
|
|
5858
6599
|
// src/server/contentBatchImport.ts
|
|
5859
|
-
import { existsSync as
|
|
5860
|
-
import { basename as basename3, join as
|
|
6600
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
6601
|
+
import { basename as basename3, join as join10, relative as relative3 } from "node:path";
|
|
5861
6602
|
function walk(dir) {
|
|
5862
6603
|
return readdirSync3(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
5863
|
-
const path =
|
|
6604
|
+
const path = join10(dir, entry.name);
|
|
5864
6605
|
if (entry.isDirectory()) return walk(path);
|
|
5865
6606
|
return entry.isFile() && entry.name.endsWith(".md") ? [path] : [];
|
|
5866
6607
|
});
|
|
@@ -5892,7 +6633,7 @@ function itemFromFile(file) {
|
|
|
5892
6633
|
const parts = file.split("/");
|
|
5893
6634
|
const kind = parts.includes("drafts") ? "draft" : parts.includes("concepts") ? "concept" : null;
|
|
5894
6635
|
if (!kind) return null;
|
|
5895
|
-
const text =
|
|
6636
|
+
const text = readFileSync5(file, "utf8");
|
|
5896
6637
|
const meta = metadata(text);
|
|
5897
6638
|
const pathChannel = parts[parts.indexOf("channels") + 1] || "";
|
|
5898
6639
|
const relatedAsset = meta["related asset"] && meta["related asset"] !== "none yet" ? meta["related asset"] : void 0;
|
|
@@ -5905,13 +6646,13 @@ function itemFromFile(file) {
|
|
|
5905
6646
|
phase: phaseFromStatus(meta.status),
|
|
5906
6647
|
postId: postIdFor(kind, file),
|
|
5907
6648
|
relatedAsset,
|
|
5908
|
-
sourcePath:
|
|
6649
|
+
sourcePath: relative3(repoRoot, file),
|
|
5909
6650
|
title: text.match(/^#\s+(.+)$/m)?.[1] || basename3(file, ".md")
|
|
5910
6651
|
};
|
|
5911
6652
|
}
|
|
5912
6653
|
function demoMarkdownItems(kind) {
|
|
5913
|
-
const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT ||
|
|
5914
|
-
if (!
|
|
6654
|
+
const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join10(repoRoot, "demo-project", "channels");
|
|
6655
|
+
if (!existsSync8(root)) return [];
|
|
5915
6656
|
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
6657
|
}
|
|
5917
6658
|
function importDemoContentBatch(project, options) {
|
|
@@ -6250,6 +6991,11 @@ function contentBatchRouter(projectFrom2) {
|
|
|
6250
6991
|
}
|
|
6251
6992
|
|
|
6252
6993
|
// src/server/generationReceipts.ts
|
|
6994
|
+
import { existsSync as existsSync9, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
6995
|
+
import { relative as relative4, resolve as resolve5 } from "node:path";
|
|
6996
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6997
|
+
var adapterVersion = "generation-receipts-v1";
|
|
6998
|
+
var provider = "codex-handoff";
|
|
6253
6999
|
var GenerationReceiptError = class extends Error {
|
|
6254
7000
|
constructor(message, status = 400) {
|
|
6255
7001
|
super(message);
|
|
@@ -6260,10 +7006,44 @@ var GenerationReceiptError = class extends Error {
|
|
|
6260
7006
|
function isGenerationReceiptError(error) {
|
|
6261
7007
|
return error instanceof GenerationReceiptError;
|
|
6262
7008
|
}
|
|
7009
|
+
function jobId() {
|
|
7010
|
+
return `gen-${Date.now().toString(36)}-${randomUUID3().slice(0, 8)}`;
|
|
7011
|
+
}
|
|
7012
|
+
function quote(value) {
|
|
7013
|
+
return JSON.stringify(value);
|
|
7014
|
+
}
|
|
6263
7015
|
function parseJson(value, fallback) {
|
|
6264
7016
|
if (!value) return fallback;
|
|
6265
7017
|
return JSON.parse(value);
|
|
6266
7018
|
}
|
|
7019
|
+
function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
7020
|
+
const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
7021
|
+
return {
|
|
7022
|
+
schema_version: "lineage.generation_handoff.v1",
|
|
7023
|
+
provider,
|
|
7024
|
+
project,
|
|
7025
|
+
job_id: id,
|
|
7026
|
+
prompt,
|
|
7027
|
+
expected_output_count: 1,
|
|
7028
|
+
lineage: {
|
|
7029
|
+
root_asset_id: rootAssetId,
|
|
7030
|
+
parent_asset_id: target.asset_id,
|
|
7031
|
+
selection_strategy: "reroll_request",
|
|
7032
|
+
parent_title: target.title,
|
|
7033
|
+
parent_local_path: target.local_path,
|
|
7034
|
+
parent_s3_key: target.s3_key
|
|
7035
|
+
},
|
|
7036
|
+
instructions: [
|
|
7037
|
+
"Use Codex image generation outside Lineage server code.",
|
|
7038
|
+
"Write the regenerated output file under .asset-scratch before import.",
|
|
7039
|
+
"Do not call live provider APIs from the CLI or server.",
|
|
7040
|
+
"Import exactly one output with reroll import, not link-child or generate image import.",
|
|
7041
|
+
`Resolve re-roll request ${request.id}; do not create a visible lineage child edge.`
|
|
7042
|
+
],
|
|
7043
|
+
import_command: importCommand,
|
|
7044
|
+
guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
|
|
7045
|
+
};
|
|
7046
|
+
}
|
|
6267
7047
|
function receiptFrom(row) {
|
|
6268
7048
|
return {
|
|
6269
7049
|
id: String(row.id),
|
|
@@ -6327,32 +7107,217 @@ function loadGenerationJob(database, project, id) {
|
|
|
6327
7107
|
receipts
|
|
6328
7108
|
};
|
|
6329
7109
|
}
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
if (fields.
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
7110
|
+
function insertReceipt(database, id, type, command, payload) {
|
|
7111
|
+
database.prepare(`
|
|
7112
|
+
insert into generation_job_receipts (id, job_id, receipt_type, status, command, payload_json, created_at)
|
|
7113
|
+
values (?, ?, ?, 'ok', ?, ?, ?)
|
|
7114
|
+
`).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
|
|
7115
|
+
}
|
|
7116
|
+
function planImageReroll(project = defaultProject, fields) {
|
|
7117
|
+
const prompt = fields.prompt.trim();
|
|
7118
|
+
if (!prompt) throw new GenerationReceiptError("Missing --prompt");
|
|
7119
|
+
if (!fields.rootAssetId) throw new GenerationReceiptError("Missing --root");
|
|
7120
|
+
if (!fields.targetAssetId) throw new GenerationReceiptError("Missing --target");
|
|
7121
|
+
const snapshot = getLineageSnapshot(project, fields.rootAssetId);
|
|
7122
|
+
const target = snapshot.nodes.find((node) => node.asset_id === fields.targetAssetId);
|
|
7123
|
+
if (!target) throw new GenerationReceiptError(`Re-roll target is not in lineage: ${fields.targetAssetId}`, 404);
|
|
7124
|
+
const request = listLineageRerollRequests(project, snapshot.root_asset_id).requests.find((item) => item.node_asset_id === fields.targetAssetId);
|
|
7125
|
+
if (!request) throw new GenerationReceiptError(`No pending re-roll request for ${fields.targetAssetId}`);
|
|
7126
|
+
const id = jobId();
|
|
7127
|
+
const timestamp = nowIso();
|
|
7128
|
+
const handoff3 = buildRerollHandoff(project, id, prompt, snapshot.root_asset_id, target, request);
|
|
7129
|
+
const input = {
|
|
7130
|
+
id: `${id}:input:0`,
|
|
7131
|
+
job_id: id,
|
|
7132
|
+
project_id: project,
|
|
7133
|
+
asset_id: target.asset_id,
|
|
7134
|
+
root_asset_id: snapshot.root_asset_id,
|
|
7135
|
+
role: "reroll_target",
|
|
7136
|
+
position: 0,
|
|
7137
|
+
selection_strategy: "reroll_request",
|
|
7138
|
+
selection_snapshot: {
|
|
7139
|
+
project,
|
|
7140
|
+
root_asset_id: snapshot.root_asset_id,
|
|
7141
|
+
strategy: "selected",
|
|
7142
|
+
selection_mode: "single",
|
|
7143
|
+
recommended_action: "evolve_variations",
|
|
7144
|
+
reason: "user_selected",
|
|
7145
|
+
next_asset: target,
|
|
7146
|
+
next_assets: [target],
|
|
7147
|
+
latest: snapshot.latest,
|
|
7148
|
+
selected: [target.asset_id],
|
|
7149
|
+
selection: null,
|
|
7150
|
+
selections: [],
|
|
7151
|
+
candidates: snapshot.nodes,
|
|
7152
|
+
warnings: ["Re-roll target: import output as an attempt, not a lineage child."],
|
|
7153
|
+
fetchedAt: timestamp
|
|
7154
|
+
}
|
|
7155
|
+
};
|
|
7156
|
+
const preview = {
|
|
7157
|
+
id,
|
|
7158
|
+
project_id: project,
|
|
7159
|
+
provider,
|
|
7160
|
+
adapter_version: adapterVersion,
|
|
7161
|
+
source_mode: "lineage_reroll",
|
|
7162
|
+
root_asset_id: snapshot.root_asset_id,
|
|
7163
|
+
prompt,
|
|
7164
|
+
expected_output_count: 1,
|
|
7165
|
+
status: "planned",
|
|
7166
|
+
output_dir: ".asset-scratch",
|
|
7167
|
+
handoff: handoff3,
|
|
7168
|
+
created_at: timestamp,
|
|
7169
|
+
updated_at: timestamp,
|
|
7170
|
+
inputs: [input],
|
|
7171
|
+
outputs: [],
|
|
7172
|
+
receipts: [{
|
|
7173
|
+
id: `${id}:receipt:plan:preview`,
|
|
7174
|
+
job_id: id,
|
|
7175
|
+
receipt_type: "plan",
|
|
7176
|
+
status: "ok",
|
|
7177
|
+
command: "reroll plan",
|
|
7178
|
+
payload: { prompt, expected_output_count: 1, lineage: handoff3.lineage, reroll_request_id: request.id },
|
|
7179
|
+
created_at: timestamp
|
|
7180
|
+
}]
|
|
7181
|
+
};
|
|
7182
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
|
|
6344
7183
|
const database = lineageDb();
|
|
6345
7184
|
try {
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
7185
|
+
database.exec("BEGIN IMMEDIATE");
|
|
7186
|
+
try {
|
|
7187
|
+
database.prepare(`
|
|
7188
|
+
insert into generation_jobs (
|
|
7189
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
7190
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at
|
|
7191
|
+
) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
|
|
7192
|
+
`).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff3), timestamp, timestamp);
|
|
7193
|
+
database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(input.selection_snapshot));
|
|
7194
|
+
insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
|
|
7195
|
+
database.exec("COMMIT");
|
|
7196
|
+
} catch (error) {
|
|
7197
|
+
database.exec("ROLLBACK");
|
|
7198
|
+
throw error;
|
|
7199
|
+
}
|
|
7200
|
+
return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
|
|
7201
|
+
} finally {
|
|
7202
|
+
database.close();
|
|
7203
|
+
}
|
|
7204
|
+
}
|
|
7205
|
+
function isPathInside2(child, parent) {
|
|
7206
|
+
const rel = relative4(parent, child);
|
|
7207
|
+
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
7208
|
+
}
|
|
7209
|
+
function resolveScratchFile(file) {
|
|
7210
|
+
const scratchRoot = resolve5(repoRoot, ".asset-scratch");
|
|
7211
|
+
const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
|
|
7212
|
+
if (!isPathInside2(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
7213
|
+
if (!existsSync9(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
7214
|
+
const realScratchRoot = realpathSync2(scratchRoot);
|
|
7215
|
+
const realCandidate = realpathSync2(candidate);
|
|
7216
|
+
if (!isPathInside2(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
7217
|
+
const stats = statSync4(candidate);
|
|
7218
|
+
if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
|
|
7219
|
+
const checksum = fileSha256(candidate);
|
|
7220
|
+
return {
|
|
7221
|
+
relativePath: relative4(scratchRoot, candidate),
|
|
7222
|
+
checksum,
|
|
7223
|
+
size: stats.size,
|
|
7224
|
+
contentType: contentTypeFor(candidate),
|
|
7225
|
+
assetId: `local-${checksum.slice(0, 12)}`
|
|
7226
|
+
};
|
|
7227
|
+
}
|
|
7228
|
+
function importImageRerollOutput(project = defaultProject, fields) {
|
|
7229
|
+
if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
|
|
7230
|
+
if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
|
|
7231
|
+
const database = lineageDb();
|
|
7232
|
+
let job;
|
|
7233
|
+
try {
|
|
7234
|
+
job = loadGenerationJob(database, project, fields.jobId);
|
|
7235
|
+
} finally {
|
|
7236
|
+
database.close();
|
|
7237
|
+
}
|
|
7238
|
+
if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
|
|
7239
|
+
if (job.source_mode !== "lineage_reroll") throw new GenerationReceiptError(`Generation job is not a re-roll job: ${job.source_mode}`);
|
|
7240
|
+
const target = job.inputs.filter((input) => input.role === "reroll_target");
|
|
7241
|
+
if (target.length !== 1) throw new GenerationReceiptError("Re-roll import requires exactly one reroll_target input");
|
|
7242
|
+
const resolved = resolveScratchFile(fields.file);
|
|
7243
|
+
indexLineageAssets(project);
|
|
7244
|
+
const writeDb = lineageDb();
|
|
7245
|
+
try {
|
|
7246
|
+
const timestamp = nowIso();
|
|
7247
|
+
writeDb.exec("BEGIN IMMEDIATE");
|
|
7248
|
+
try {
|
|
7249
|
+
const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, resolved.assetId);
|
|
7250
|
+
if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${resolved.relativePath}`);
|
|
7251
|
+
const outputId = `${fields.jobId}:output:0`;
|
|
7252
|
+
writeDb.prepare(`insert into generation_job_outputs (
|
|
7253
|
+
id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type, imported_asset_id, parent_asset_id, imported_at
|
|
7254
|
+
) values (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`).run(outputId, fields.jobId, project, resolved.relativePath, resolved.checksum, resolved.size, resolved.contentType, resolved.assetId, target[0].asset_id, timestamp);
|
|
7255
|
+
writeDb.prepare(`
|
|
7256
|
+
update generation_jobs
|
|
7257
|
+
set status = 'imported', imported_at = ?, updated_at = ?
|
|
7258
|
+
where project_id = ? and id = ?
|
|
7259
|
+
`).run(timestamp, timestamp, project, fields.jobId);
|
|
7260
|
+
insertReceipt(writeDb, fields.jobId, "import", "reroll import", {
|
|
7261
|
+
file: { output_index: 0, file_path: resolved.relativePath, imported_asset_id: resolved.assetId, parent_asset_id: target[0].asset_id },
|
|
7262
|
+
reroll: { root_asset_id: job.root_asset_id, node_asset_id: target[0].asset_id }
|
|
7263
|
+
});
|
|
7264
|
+
writeDb.exec("COMMIT");
|
|
7265
|
+
} catch (error) {
|
|
7266
|
+
writeDb.exec("ROLLBACK");
|
|
7267
|
+
throw error;
|
|
7268
|
+
}
|
|
7269
|
+
recordLineageRerollAttempt(project, {
|
|
7270
|
+
rootAssetId: job.root_asset_id,
|
|
7271
|
+
nodeAssetId: target[0].asset_id,
|
|
7272
|
+
assetId: resolved.assetId,
|
|
7273
|
+
prompt: job.prompt,
|
|
7274
|
+
generationJobId: fields.jobId,
|
|
7275
|
+
filePath: resolved.relativePath,
|
|
7276
|
+
checksumSha256: resolved.checksum,
|
|
7277
|
+
confirmWrite: true
|
|
7278
|
+
});
|
|
7279
|
+
const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
|
|
7280
|
+
if (rerollTask) {
|
|
7281
|
+
resolveLineageTask(project, {
|
|
7282
|
+
actor: "agent",
|
|
7283
|
+
confirmWrite: true,
|
|
7284
|
+
resolvedAssetId: resolved.assetId,
|
|
7285
|
+
resolvedGenerationJobId: fields.jobId,
|
|
7286
|
+
taskId: rerollTask.id
|
|
7287
|
+
});
|
|
7288
|
+
}
|
|
7289
|
+
const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
|
|
7290
|
+
return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
|
|
7291
|
+
} finally {
|
|
7292
|
+
writeDb.close();
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
7295
|
+
|
|
7296
|
+
// src/server/generationReceiptJobs.ts
|
|
7297
|
+
function listImageGenerationJobs(project = defaultProject, fields = {}) {
|
|
7298
|
+
const clauses = ["job.project_id = ?"];
|
|
7299
|
+
const params = [project];
|
|
7300
|
+
if (fields.rootAssetId) {
|
|
7301
|
+
clauses.push("job.root_asset_id = ?");
|
|
7302
|
+
params.push(fields.rootAssetId);
|
|
7303
|
+
}
|
|
7304
|
+
if (fields.assetId) {
|
|
7305
|
+
clauses.push("(input.asset_id = ? or output.imported_asset_id = ? or output.parent_asset_id = ?)");
|
|
7306
|
+
params.push(fields.assetId, fields.assetId, fields.assetId);
|
|
7307
|
+
}
|
|
7308
|
+
const limit = Math.max(1, Math.min(50, Number.isInteger(fields.limit) ? Number(fields.limit) : 12));
|
|
7309
|
+
const database = lineageDb();
|
|
7310
|
+
try {
|
|
7311
|
+
const rows = database.prepare(`
|
|
7312
|
+
select distinct job.id
|
|
7313
|
+
from generation_jobs job
|
|
7314
|
+
left join generation_job_inputs input on input.job_id = job.id
|
|
7315
|
+
left join generation_job_outputs output on output.job_id = job.id
|
|
7316
|
+
where ${clauses.join(" and ")}
|
|
7317
|
+
order by job.created_at desc, job.id desc
|
|
7318
|
+
limit ?
|
|
7319
|
+
`).all(...params, limit);
|
|
7320
|
+
return { ok: true, command: "generate image jobs", project, jobs: rows.map((row) => loadGenerationJob(database, project, row.id)), fetchedAt: nowIso() };
|
|
6356
7321
|
} finally {
|
|
6357
7322
|
database.close();
|
|
6358
7323
|
}
|
|
@@ -6417,15 +7382,15 @@ function registerLineageTaskRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
6417
7382
|
}
|
|
6418
7383
|
|
|
6419
7384
|
// src/server/assetLineageDemo.ts
|
|
6420
|
-
import { createHash as
|
|
6421
|
-
import { copyFileSync as
|
|
7385
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
7386
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6422
7387
|
import { tmpdir } from "node:os";
|
|
6423
|
-
import { dirname as
|
|
7388
|
+
import { dirname as dirname5, join as join11, posix } from "node:path";
|
|
6424
7389
|
import { gunzipSync } from "node:zlib";
|
|
6425
7390
|
var demoWorkspaceTitle = "Demo: Content iteration tree";
|
|
6426
7391
|
var demoWorkspaceNotes = "Repeatable sample lineage for demos and onboarding. Archive it when reviewing real work.";
|
|
6427
7392
|
var demoBasePath = ["lineage-demo", "2026-06-lineage-demo"];
|
|
6428
|
-
var swissifierManifestPath =
|
|
7393
|
+
var swissifierManifestPath = join11(packageRoot, "fixtures", defaultProject, "lineage", "swissifier-rich-demo.json");
|
|
6429
7394
|
var demoAssets = [
|
|
6430
7395
|
{ key: "root", channel: "linkedin", file: "demo-root.svg", label: "Initial Demo Concept", fill: "#f6fbfb", stroke: "#0b7f88" },
|
|
6431
7396
|
{ key: "hookA", channel: "tiktok", file: "demo-hook-a-v01.svg", label: "Hook A v01", fill: "#fff8e6", stroke: "#9a6a00" },
|
|
@@ -6462,37 +7427,37 @@ var demoPositions = /* @__PURE__ */ new Map([
|
|
|
6462
7427
|
["alt", { x: 1060, y: 80 }]
|
|
6463
7428
|
]);
|
|
6464
7429
|
function demoProjectDir(project) {
|
|
6465
|
-
return
|
|
7430
|
+
return join11(repoRoot, ".asset-scratch", ...demoBasePath, project);
|
|
6466
7431
|
}
|
|
6467
7432
|
function demoRelativePath(project, asset) {
|
|
6468
|
-
return
|
|
7433
|
+
return join11(...demoBasePath, project, asset.channel, asset.file);
|
|
6469
7434
|
}
|
|
6470
7435
|
function demoFilePath(project, asset) {
|
|
6471
|
-
return
|
|
7436
|
+
return join11(demoProjectDir(project), asset.channel, asset.file);
|
|
6472
7437
|
}
|
|
6473
7438
|
function swissifierManifest() {
|
|
6474
|
-
return JSON.parse(
|
|
7439
|
+
return JSON.parse(readFileSync6(swissifierManifestPath, "utf8"));
|
|
6475
7440
|
}
|
|
6476
7441
|
function swissifierRelativePath(manifest, asset) {
|
|
6477
|
-
return
|
|
7442
|
+
return join11(manifest.media.target_dir, asset.file);
|
|
6478
7443
|
}
|
|
6479
7444
|
function swissifierFilePath(manifest, asset) {
|
|
6480
|
-
return
|
|
7445
|
+
return join11(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
|
|
6481
7446
|
}
|
|
6482
7447
|
function swissifierRerollRelativePath(manifest, attempt) {
|
|
6483
|
-
return
|
|
7448
|
+
return join11(manifest.media.target_dir, "reroll-attempts", attempt.file);
|
|
6484
7449
|
}
|
|
6485
7450
|
function swissifierRerollFilePath(manifest, attempt) {
|
|
6486
|
-
return
|
|
7451
|
+
return join11(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
|
|
6487
7452
|
}
|
|
6488
7453
|
function swissifierRerollFixturePath(attempt) {
|
|
6489
|
-
return
|
|
7454
|
+
return join11(dirname5(swissifierManifestPath), "swissifier-rerolls", attempt.file);
|
|
6490
7455
|
}
|
|
6491
7456
|
function swissifierSourcePath(manifest, asset, sourceDir) {
|
|
6492
|
-
const direct =
|
|
6493
|
-
if (
|
|
6494
|
-
const nested =
|
|
6495
|
-
return
|
|
7457
|
+
const direct = join11(sourceDir, asset.file);
|
|
7458
|
+
if (existsSync10(direct)) return direct;
|
|
7459
|
+
const nested = join11(sourceDir, manifest.media.target_dir, asset.file);
|
|
7460
|
+
return existsSync10(nested) ? nested : null;
|
|
6496
7461
|
}
|
|
6497
7462
|
function swissifierMediaState(manifest) {
|
|
6498
7463
|
const missing = [];
|
|
@@ -6500,7 +7465,7 @@ function swissifierMediaState(manifest) {
|
|
|
6500
7465
|
for (const asset of manifest.assets) {
|
|
6501
7466
|
const relativePath = swissifierRelativePath(manifest, asset);
|
|
6502
7467
|
const path = swissifierFilePath(manifest, asset);
|
|
6503
|
-
if (!
|
|
7468
|
+
if (!existsSync10(path)) {
|
|
6504
7469
|
missing.push(relativePath);
|
|
6505
7470
|
continue;
|
|
6506
7471
|
}
|
|
@@ -6509,7 +7474,7 @@ function swissifierMediaState(manifest) {
|
|
|
6509
7474
|
return { invalid, missing };
|
|
6510
7475
|
}
|
|
6511
7476
|
function sha256Hex(input) {
|
|
6512
|
-
return
|
|
7477
|
+
return createHash5("sha256").update(input).digest("hex");
|
|
6513
7478
|
}
|
|
6514
7479
|
function safeTarEntryName(rawName) {
|
|
6515
7480
|
const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
|
|
@@ -6572,9 +7537,9 @@ function extractSwissifierMediaArchive(archive, manifest, destination) {
|
|
|
6572
7537
|
const file = body.subarray(offset, offset + size);
|
|
6573
7538
|
const actualSha = sha256Hex(file);
|
|
6574
7539
|
if (actualSha !== asset.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier media archive entry: ${name}`);
|
|
6575
|
-
const target =
|
|
6576
|
-
|
|
6577
|
-
|
|
7540
|
+
const target = join11(destination, directName);
|
|
7541
|
+
mkdirSync7(dirname5(target), { recursive: true });
|
|
7542
|
+
writeFileSync5(target, file);
|
|
6578
7543
|
extracted.add(directName);
|
|
6579
7544
|
offset = nextOffset;
|
|
6580
7545
|
}
|
|
@@ -6592,14 +7557,14 @@ async function downloadBuffer(url, maxBytes) {
|
|
|
6592
7557
|
return buffer;
|
|
6593
7558
|
}
|
|
6594
7559
|
function missingDemoMedia(project) {
|
|
6595
|
-
return demoAssets.filter((asset) => !
|
|
7560
|
+
return demoAssets.filter((asset) => !existsSync10(demoFilePath(project, asset))).map((asset) => demoRelativePath(project, asset));
|
|
6596
7561
|
}
|
|
6597
7562
|
function demoSeedMediaStatus(project = defaultProject) {
|
|
6598
7563
|
const missing = missingDemoMedia(project);
|
|
6599
7564
|
const present = demoAssets.length - missing.length;
|
|
6600
7565
|
return {
|
|
6601
7566
|
ok: true,
|
|
6602
|
-
media_root:
|
|
7567
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6603
7568
|
present,
|
|
6604
7569
|
total: demoAssets.length,
|
|
6605
7570
|
missing,
|
|
@@ -6616,7 +7581,7 @@ function swissifierRichDemoMediaStatus(project = defaultProject) {
|
|
|
6616
7581
|
ok: true,
|
|
6617
7582
|
demo_id: manifest.id,
|
|
6618
7583
|
project,
|
|
6619
|
-
media_root:
|
|
7584
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6620
7585
|
media_target: manifest.media.target_dir,
|
|
6621
7586
|
download_available: Boolean(manifest.media.download),
|
|
6622
7587
|
download_file: manifest.media.download?.file,
|
|
@@ -6667,7 +7632,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
|
|
|
6667
7632
|
would_restore: manifest.assets.length
|
|
6668
7633
|
};
|
|
6669
7634
|
}
|
|
6670
|
-
const sourceDir = mkdtempSync(
|
|
7635
|
+
const sourceDir = mkdtempSync(join11(tmpdir(), "lineage-swissifier-media-"));
|
|
6671
7636
|
try {
|
|
6672
7637
|
const extracted = extractSwissifierMediaArchive(archive, manifest, sourceDir);
|
|
6673
7638
|
const restored = restoreSwissifierRichDemoMedia(project, { confirmWrite: true, sourceDir });
|
|
@@ -6681,7 +7646,7 @@ async function downloadSwissifierRichDemoMedia(project = defaultProject, fields
|
|
|
6681
7646
|
media_status: swissifierRichDemoMediaStatus(project)
|
|
6682
7647
|
};
|
|
6683
7648
|
} finally {
|
|
6684
|
-
|
|
7649
|
+
rmSync3(sourceDir, { force: true, recursive: true });
|
|
6685
7650
|
}
|
|
6686
7651
|
}
|
|
6687
7652
|
function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite: false }) {
|
|
@@ -6694,7 +7659,7 @@ function restoreDemoSeedMedia(project = defaultProject, fields = { confirmWrite:
|
|
|
6694
7659
|
return {
|
|
6695
7660
|
ok: true,
|
|
6696
7661
|
dryRun: !fields.confirmWrite,
|
|
6697
|
-
media_root:
|
|
7662
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6698
7663
|
restored: fields.confirmWrite ? missing.size : 0,
|
|
6699
7664
|
total: demoAssets.length,
|
|
6700
7665
|
would_restore: fields.confirmWrite ? 0 : missing.size,
|
|
@@ -6711,7 +7676,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
|
|
|
6711
7676
|
demo_id: manifest.id,
|
|
6712
7677
|
project,
|
|
6713
7678
|
dryRun: !fields.confirmWrite,
|
|
6714
|
-
media_root:
|
|
7679
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6715
7680
|
restored: 0,
|
|
6716
7681
|
total: manifest.assets.length,
|
|
6717
7682
|
would_restore: 0,
|
|
@@ -6739,8 +7704,8 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
|
|
|
6739
7704
|
if (fields.confirmWrite) {
|
|
6740
7705
|
for (const item of copyable) {
|
|
6741
7706
|
const target = swissifierFilePath(manifest, item.asset);
|
|
6742
|
-
|
|
6743
|
-
|
|
7707
|
+
mkdirSync7(dirname5(target), { recursive: true });
|
|
7708
|
+
copyFileSync3(item.source, target);
|
|
6744
7709
|
}
|
|
6745
7710
|
}
|
|
6746
7711
|
return {
|
|
@@ -6748,7 +7713,7 @@ function restoreSwissifierRichDemoMedia(project = defaultProject, fields = { con
|
|
|
6748
7713
|
demo_id: manifest.id,
|
|
6749
7714
|
project,
|
|
6750
7715
|
dryRun: !fields.confirmWrite,
|
|
6751
|
-
media_root:
|
|
7716
|
+
media_root: join11(repoRoot, ".asset-scratch"),
|
|
6752
7717
|
restored: fields.confirmWrite ? copyable.length : 0,
|
|
6753
7718
|
total: manifest.assets.length,
|
|
6754
7719
|
would_restore: fields.confirmWrite ? 0 : copyable.length,
|
|
@@ -6770,15 +7735,15 @@ function svg(label, fill, stroke) {
|
|
|
6770
7735
|
`;
|
|
6771
7736
|
}
|
|
6772
7737
|
function assetIdFor(asset) {
|
|
6773
|
-
return `local-${
|
|
7738
|
+
return `local-${createHash5("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
|
|
6774
7739
|
}
|
|
6775
7740
|
function demoAssetIds() {
|
|
6776
7741
|
return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
|
|
6777
7742
|
}
|
|
6778
7743
|
function writeDemoFile(project, asset) {
|
|
6779
7744
|
const path = demoFilePath(project, asset);
|
|
6780
|
-
|
|
6781
|
-
|
|
7745
|
+
mkdirSync7(dirname5(path), { recursive: true });
|
|
7746
|
+
writeFileSync5(path, svg(asset.label, asset.fill, asset.stroke));
|
|
6782
7747
|
}
|
|
6783
7748
|
function writeDemoFiles(project) {
|
|
6784
7749
|
const ids = demoAssetIds();
|
|
@@ -6787,8 +7752,8 @@ function writeDemoFiles(project) {
|
|
|
6787
7752
|
}
|
|
6788
7753
|
function swissifierRerollAsset(attempt) {
|
|
6789
7754
|
const source = swissifierRerollFixturePath(attempt);
|
|
6790
|
-
if (!
|
|
6791
|
-
const body =
|
|
7755
|
+
if (!existsSync10(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
|
|
7756
|
+
const body = readFileSync6(source);
|
|
6792
7757
|
if (body.length !== attempt.size_bytes) throw new Error(`Unexpected Swissifier re-roll fixture size for ${attempt.file}`);
|
|
6793
7758
|
const checksumSha256 = sha256Hex(body);
|
|
6794
7759
|
if (checksumSha256 !== attempt.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier re-roll fixture: ${attempt.file}`);
|
|
@@ -6802,8 +7767,8 @@ function writeSwissifierRerollFiles(manifest) {
|
|
|
6802
7767
|
for (const attempt of manifest.reroll_attempts || []) {
|
|
6803
7768
|
swissifierRerollAsset(attempt);
|
|
6804
7769
|
const target = swissifierRerollFilePath(manifest, attempt);
|
|
6805
|
-
|
|
6806
|
-
|
|
7770
|
+
mkdirSync7(dirname5(target), { recursive: true });
|
|
7771
|
+
copyFileSync3(swissifierRerollFixturePath(attempt), target);
|
|
6807
7772
|
}
|
|
6808
7773
|
}
|
|
6809
7774
|
function upsertDemoAssets(project, ids) {
|
|
@@ -6814,7 +7779,7 @@ function upsertDemoAssets(project, ids) {
|
|
|
6814
7779
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
6815
7780
|
values (?, ?, ?, ?, ?)
|
|
6816
7781
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
6817
|
-
`).run(project, project,
|
|
7782
|
+
`).run(project, project, join11(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
6818
7783
|
const assetStatement = database.prepare(`
|
|
6819
7784
|
insert into assets (
|
|
6820
7785
|
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
@@ -6837,7 +7802,7 @@ function upsertDemoAssets(project, ids) {
|
|
|
6837
7802
|
ids[asset.key],
|
|
6838
7803
|
project,
|
|
6839
7804
|
demoRelativePath(project, asset),
|
|
6840
|
-
|
|
7805
|
+
createHash5("sha256").update(body).digest("hex"),
|
|
6841
7806
|
asset.label,
|
|
6842
7807
|
asset.channel,
|
|
6843
7808
|
Buffer.byteLength(body),
|
|
@@ -6860,7 +7825,7 @@ function upsertSwissifierAssets(project, manifest) {
|
|
|
6860
7825
|
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
6861
7826
|
values (?, ?, ?, ?, ?)
|
|
6862
7827
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
6863
|
-
`).run(project, project,
|
|
7828
|
+
`).run(project, project, join11(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
6864
7829
|
const assetStatement = database.prepare(`
|
|
6865
7830
|
insert into assets (
|
|
6866
7831
|
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
@@ -7096,7 +8061,7 @@ function archiveDemoLineageWorkspace(project, confirmWrite) {
|
|
|
7096
8061
|
if (!isLineageWorkspaceError(error) || error.status !== 404) throw error;
|
|
7097
8062
|
archived = { ok: true, message: "No demo lineage workspace exists yet", workspace: null };
|
|
7098
8063
|
}
|
|
7099
|
-
if (confirmWrite)
|
|
8064
|
+
if (confirmWrite) rmSync3(demoProjectDir(project), { force: true, recursive: true });
|
|
7100
8065
|
return {
|
|
7101
8066
|
ok: true,
|
|
7102
8067
|
message: confirmWrite ? `Archived ${demoWorkspaceTitle}` : `Would archive ${demoWorkspaceTitle}`,
|
|
@@ -7190,14 +8155,49 @@ function registerLineageWorkspaceRoutes(app2, projectFrom2, asyncRoute2) {
|
|
|
7190
8155
|
}
|
|
7191
8156
|
|
|
7192
8157
|
// src/server/runtimeInfo.ts
|
|
8158
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
7193
8159
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7194
|
-
import { createRequire as
|
|
7195
|
-
import { existsSync as
|
|
7196
|
-
import { join as
|
|
7197
|
-
|
|
7198
|
-
|
|
8160
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
8161
|
+
import { existsSync as existsSync11, lstatSync as lstatSync2, readFileSync as readFileSync7, readdirSync as readdirSync4, readlinkSync, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
|
|
8162
|
+
import { dirname as dirname6, isAbsolute as isAbsolute2, join as join12, resolve as resolve6 } from "node:path";
|
|
8163
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8164
|
+
|
|
8165
|
+
// src/shared/runtimeInfoTypes.ts
|
|
8166
|
+
var lineageRuntimeBuildSchemaVersion = "lineage.runtime_build.v1";
|
|
8167
|
+
var lineageRuntimeInstallSchemaVersion = "lineage.runtime_install.v1";
|
|
8168
|
+
|
|
8169
|
+
// src/server/runtimeInfo.ts
|
|
8170
|
+
var require5 = createRequire4(import.meta.url);
|
|
8171
|
+
var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8172
|
+
function isLineagePackageRoot(root) {
|
|
8173
|
+
try {
|
|
8174
|
+
const info = JSON.parse(readFileSync7(join12(root, "package.json"), "utf8"));
|
|
8175
|
+
return info.name === "@mean-weasel/lineage";
|
|
8176
|
+
} catch {
|
|
8177
|
+
return false;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
function executingCodeRoot() {
|
|
8181
|
+
const moduleDirectory = dirname6(fileURLToPath2(import.meta.url));
|
|
8182
|
+
const candidates = [resolve6(moduleDirectory, "../.."), resolve6(moduleDirectory, "..")];
|
|
8183
|
+
const root = candidates.find(isLineagePackageRoot);
|
|
8184
|
+
if (!root) throw new Error(`Unable to derive Lineage code root from executing module ${fileURLToPath2(import.meta.url)}`);
|
|
8185
|
+
return canonicalRoot(root);
|
|
8186
|
+
}
|
|
8187
|
+
var codeRoot = executingCodeRoot();
|
|
8188
|
+
function sha256(value) {
|
|
8189
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
8190
|
+
}
|
|
8191
|
+
function canonicalRoot(root) {
|
|
8192
|
+
try {
|
|
8193
|
+
return realpathSync3(root);
|
|
8194
|
+
} catch {
|
|
8195
|
+
return resolve6(root);
|
|
8196
|
+
}
|
|
8197
|
+
}
|
|
8198
|
+
function packageInfo(root = codeRoot) {
|
|
7199
8199
|
try {
|
|
7200
|
-
const info = JSON.parse(
|
|
8200
|
+
const info = JSON.parse(readFileSync7(join12(root, "package.json"), "utf8"));
|
|
7201
8201
|
return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
|
|
7202
8202
|
} catch {
|
|
7203
8203
|
return { name: "@mean-weasel/lineage", version: "0.0.0" };
|
|
@@ -7210,35 +8210,240 @@ function normalizeRuntimeChannel(value) {
|
|
|
7210
8210
|
if (value === "development") return "dev";
|
|
7211
8211
|
return process.env.NODE_ENV === "production" ? "stable" : "dev";
|
|
7212
8212
|
}
|
|
7213
|
-
function
|
|
7214
|
-
const
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
8213
|
+
function git(root, args) {
|
|
8214
|
+
const result = spawnSync2("git", args, { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
8215
|
+
return result.status === 0 ? result.stdout : void 0;
|
|
8216
|
+
}
|
|
8217
|
+
function untrackedFingerprint(root, status) {
|
|
8218
|
+
const hash = createHash6("sha256");
|
|
8219
|
+
hash.update(status);
|
|
8220
|
+
const listed = git(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
8221
|
+
if (listed === void 0) return hash.digest("hex");
|
|
8222
|
+
for (const relativePath of listed.split("\0").filter(Boolean).sort()) {
|
|
8223
|
+
hash.update("\0");
|
|
8224
|
+
hash.update(relativePath);
|
|
8225
|
+
const path = join12(root, relativePath);
|
|
8226
|
+
try {
|
|
8227
|
+
const stat = lstatSync2(path);
|
|
8228
|
+
if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
|
|
8229
|
+
else if (stat.isFile()) hash.update(readFileSync7(path));
|
|
8230
|
+
else hash.update(`[${stat.mode}:${stat.size}]`);
|
|
8231
|
+
} catch (error) {
|
|
8232
|
+
hash.update(`[unreadable:${error instanceof Error ? error.message : String(error)}]`);
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
return hash.digest("hex");
|
|
8236
|
+
}
|
|
8237
|
+
function checkoutCodeIdentity(root, channel, version) {
|
|
8238
|
+
const errors = [];
|
|
8239
|
+
const gitRootRaw = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
8240
|
+
const gitSha = git(root, ["rev-parse", "HEAD"])?.trim();
|
|
8241
|
+
const status = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
8242
|
+
const diff = git(root, ["diff", "--binary", "HEAD", "--"]);
|
|
8243
|
+
const canonicalGitRoot = gitRootRaw ? canonicalRoot(gitRootRaw) : void 0;
|
|
8244
|
+
if (!canonicalGitRoot || canonicalGitRoot !== root) errors.push("Code root is not the canonical root of a Git checkout/worktree");
|
|
8245
|
+
if (!gitSha || !/^[a-f0-9]{40}$/i.test(gitSha)) errors.push("Checkout Git revision is unavailable");
|
|
8246
|
+
if (status === void 0 || diff === void 0) errors.push("Checkout dirty state could not be inspected");
|
|
8247
|
+
if (channel !== "dev") errors.push(`Checkout code may run only as dev, not ${channel}`);
|
|
8248
|
+
const dirty = Boolean(status);
|
|
8249
|
+
const sourceFingerprint = sha256(`${gitSha || "unknown"}\0${sha256(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
|
|
8250
|
+
return {
|
|
8251
|
+
channel,
|
|
8252
|
+
dirty,
|
|
8253
|
+
errors,
|
|
8254
|
+
fingerprint: sha256(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
|
|
8255
|
+
git_sha: gitSha,
|
|
8256
|
+
origin: "checkout",
|
|
8257
|
+
package_version: version,
|
|
8258
|
+
root,
|
|
8259
|
+
source_fingerprint: sourceFingerprint,
|
|
8260
|
+
verified: errors.length === 0
|
|
8261
|
+
};
|
|
7219
8262
|
}
|
|
7220
|
-
function
|
|
8263
|
+
function buildFingerprint(build) {
|
|
8264
|
+
return sha256(JSON.stringify({
|
|
8265
|
+
package_name: build.package_name,
|
|
8266
|
+
package_version: build.package_version,
|
|
8267
|
+
schema_version: build.schema_version,
|
|
8268
|
+
source_dirty: build.source_dirty,
|
|
8269
|
+
source_fingerprint: build.source_fingerprint,
|
|
8270
|
+
source_git_sha: build.source_git_sha
|
|
8271
|
+
}));
|
|
8272
|
+
}
|
|
8273
|
+
function readBuildIdentity(root, errors) {
|
|
8274
|
+
const path = join12(root, "dist", "runtime-build.json");
|
|
8275
|
+
let build;
|
|
8276
|
+
try {
|
|
8277
|
+
build = JSON.parse(readFileSync7(path, "utf8"));
|
|
8278
|
+
} catch (error) {
|
|
8279
|
+
errors.push(`Embedded build identity is missing or unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8280
|
+
return void 0;
|
|
8281
|
+
}
|
|
8282
|
+
if (build.schema_version !== lineageRuntimeBuildSchemaVersion) errors.push(`Unsupported build identity schema: ${String(build.schema_version)}`);
|
|
8283
|
+
if (!/^[a-f0-9]{40}$/i.test(build.source_git_sha || "")) errors.push("Embedded build Git revision is invalid");
|
|
8284
|
+
if (!/^[a-f0-9]{64}$/i.test(build.source_fingerprint || "")) errors.push("Embedded source fingerprint is invalid");
|
|
8285
|
+
const expected = buildFingerprint(build);
|
|
8286
|
+
if (build.build_fingerprint !== expected) errors.push("Embedded build fingerprint does not match its contents");
|
|
8287
|
+
return build;
|
|
8288
|
+
}
|
|
8289
|
+
function readInstallReceipt(path, errors) {
|
|
8290
|
+
if (!path) {
|
|
8291
|
+
errors.push("Packaged runtime was not launched through a channel install receipt");
|
|
8292
|
+
return void 0;
|
|
8293
|
+
}
|
|
8294
|
+
let receipt;
|
|
8295
|
+
const receiptPath = resolve6(path);
|
|
8296
|
+
try {
|
|
8297
|
+
receipt = JSON.parse(readFileSync7(receiptPath, "utf8"));
|
|
8298
|
+
} catch (error) {
|
|
8299
|
+
errors.push(`Runtime install receipt is missing or unreadable at ${receiptPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8300
|
+
return void 0;
|
|
8301
|
+
}
|
|
8302
|
+
if (receipt.schema_version !== lineageRuntimeInstallSchemaVersion) errors.push(`Unsupported runtime install receipt schema: ${String(receipt.schema_version)}`);
|
|
8303
|
+
if (!isAbsolute2(receipt.package_root || "")) errors.push("Runtime install receipt package_root must be absolute");
|
|
8304
|
+
if (!receipt.package_integrity?.startsWith("sha512-")) errors.push("Runtime install receipt package integrity is invalid");
|
|
8305
|
+
if (receipt.package_source !== "registry" && receipt.package_source !== "local") errors.push("Runtime install receipt package source is invalid");
|
|
8306
|
+
if (typeof receipt.package_spec !== "string" || !receipt.package_spec) errors.push("Runtime install receipt package spec is invalid");
|
|
8307
|
+
if (!/^[a-f0-9]{64}$/i.test(receipt.package_tree_sha256 || "")) errors.push("Runtime install receipt package tree hash is invalid");
|
|
8308
|
+
if (Number.isNaN(Date.parse(receipt.installed_at))) errors.push("Runtime install receipt timestamp is invalid");
|
|
8309
|
+
return { ...receipt, receipt_path: receiptPath };
|
|
8310
|
+
}
|
|
8311
|
+
function lineagePackageTreeSha256(root) {
|
|
8312
|
+
const hash = createHash6("sha256");
|
|
8313
|
+
const visit = (directory, relativeDirectory = "") => {
|
|
8314
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
8315
|
+
const relativePath = relativeDirectory ? join12(relativeDirectory, entry.name) : entry.name;
|
|
8316
|
+
const path = join12(directory, entry.name);
|
|
8317
|
+
hash.update(relativePath.replaceAll("\\", "/"));
|
|
8318
|
+
hash.update("\0");
|
|
8319
|
+
if (entry.isDirectory()) {
|
|
8320
|
+
hash.update("directory\0");
|
|
8321
|
+
visit(path, relativePath);
|
|
8322
|
+
} else if (entry.isSymbolicLink()) {
|
|
8323
|
+
hash.update("symlink\0");
|
|
8324
|
+
hash.update(readlinkSync(path));
|
|
8325
|
+
} else if (entry.isFile()) {
|
|
8326
|
+
hash.update("file\0");
|
|
8327
|
+
hash.update(readFileSync7(path));
|
|
8328
|
+
} else {
|
|
8329
|
+
hash.update("other\0");
|
|
8330
|
+
}
|
|
8331
|
+
hash.update("\0");
|
|
8332
|
+
}
|
|
8333
|
+
};
|
|
8334
|
+
visit(root);
|
|
8335
|
+
return hash.digest("hex");
|
|
8336
|
+
}
|
|
8337
|
+
function packageCodeIdentity(root, channel, info, receiptPath) {
|
|
8338
|
+
const errors = [];
|
|
8339
|
+
const build = readBuildIdentity(root, errors);
|
|
8340
|
+
const install = readInstallReceipt(receiptPath, errors);
|
|
8341
|
+
if (channel === "dev") errors.push("Published package code cannot run as dev; use a Git checkout/worktree");
|
|
8342
|
+
if (build?.package_name !== info.name || build?.package_version !== info.version) {
|
|
8343
|
+
errors.push("Embedded build package identity does not match package.json");
|
|
8344
|
+
}
|
|
8345
|
+
if (build?.source_dirty) errors.push("Stable and preview package installs require a clean-source build");
|
|
8346
|
+
if (install) {
|
|
8347
|
+
if (install.channel !== channel) errors.push(`Install receipt channel ${install.channel} does not match requested ${channel}`);
|
|
8348
|
+
if (canonicalRoot(install.package_root) !== root) errors.push("Install receipt package root does not match the executing package root");
|
|
8349
|
+
if (install.package_name !== info.name || install.package_version !== info.version) errors.push("Install receipt package identity does not match package.json");
|
|
8350
|
+
if (build && install.build_fingerprint !== build.build_fingerprint) errors.push("Install receipt build fingerprint does not match the embedded build");
|
|
8351
|
+
try {
|
|
8352
|
+
if (lineagePackageTreeSha256(root) !== install.package_tree_sha256) errors.push("Installed package tree does not match the channel install receipt");
|
|
8353
|
+
} catch (error) {
|
|
8354
|
+
errors.push(`Installed package tree could not be verified: ${error instanceof Error ? error.message : String(error)}`);
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
const gitSha = build?.source_git_sha;
|
|
8358
|
+
const sourceFingerprint = build?.source_fingerprint;
|
|
8359
|
+
return {
|
|
8360
|
+
build,
|
|
8361
|
+
channel,
|
|
8362
|
+
dirty: build?.source_dirty,
|
|
8363
|
+
errors,
|
|
8364
|
+
fingerprint: sha256(JSON.stringify({
|
|
8365
|
+
build_fingerprint: build?.build_fingerprint,
|
|
8366
|
+
channel,
|
|
8367
|
+
install_integrity: install?.package_integrity,
|
|
8368
|
+
origin: "package",
|
|
8369
|
+
root,
|
|
8370
|
+
version: info.version
|
|
8371
|
+
})),
|
|
8372
|
+
git_sha: gitSha,
|
|
8373
|
+
install,
|
|
8374
|
+
origin: "package",
|
|
8375
|
+
package_version: info.version,
|
|
8376
|
+
root,
|
|
8377
|
+
source_fingerprint: sourceFingerprint,
|
|
8378
|
+
verified: errors.length === 0
|
|
8379
|
+
};
|
|
8380
|
+
}
|
|
8381
|
+
function getLineageCodeIdentity(channel, options = {}) {
|
|
8382
|
+
const root = canonicalRoot(options.root || codeRoot);
|
|
8383
|
+
const info = packageInfo(root);
|
|
8384
|
+
if (existsSync11(join12(root, ".git"))) return checkoutCodeIdentity(root, channel, info.version);
|
|
8385
|
+
if (existsSync11(join12(root, "package.json"))) {
|
|
8386
|
+
return packageCodeIdentity(root, channel, info, options.receiptPath ?? process.env.LINEAGE_RUNTIME_RECEIPT);
|
|
8387
|
+
}
|
|
8388
|
+
const errors = [`Code root has neither checkout metadata nor package.json: ${root}`];
|
|
8389
|
+
return {
|
|
8390
|
+
channel,
|
|
8391
|
+
errors,
|
|
8392
|
+
fingerprint: sha256(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
|
|
8393
|
+
origin: "unknown",
|
|
8394
|
+
package_version: info.version,
|
|
8395
|
+
root,
|
|
8396
|
+
verified: false
|
|
8397
|
+
};
|
|
8398
|
+
}
|
|
8399
|
+
function assertLineageCodeOrigin(channel) {
|
|
8400
|
+
const identity = getLineageCodeIdentity(channel);
|
|
8401
|
+
if (!identity.verified) {
|
|
8402
|
+
const migration = channel === "dev" ? "Run dev from a Git checkout with npm run lineage:dev -- <command>." : `Install and launch an isolated ${channel} runtime with lineage-channel install ${channel}.`;
|
|
8403
|
+
throw new Error(`Unverified ${channel} code origin: ${identity.errors.join("; ")}. ${migration}`);
|
|
8404
|
+
}
|
|
8405
|
+
return identity;
|
|
8406
|
+
}
|
|
8407
|
+
function tableExists2(database, table) {
|
|
7221
8408
|
return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
|
|
7222
8409
|
}
|
|
7223
8410
|
function tableCount(database, table) {
|
|
7224
|
-
if (!
|
|
8411
|
+
if (!tableExists2(database, table)) return void 0;
|
|
7225
8412
|
const row = database.prepare(`select count(*) count from ${table}`).get();
|
|
7226
8413
|
return typeof row?.count === "number" ? row.count : void 0;
|
|
7227
8414
|
}
|
|
8415
|
+
function migrationKeys(database) {
|
|
8416
|
+
if (!tableExists2(database, "lineage_schema_migrations")) return [];
|
|
8417
|
+
return database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
|
|
8418
|
+
}
|
|
7228
8419
|
function getLineageRuntimeInfo(options = {}) {
|
|
7229
8420
|
const info = packageInfo();
|
|
7230
8421
|
const dbPath = options.dbPath || lineageDbPath();
|
|
7231
|
-
const
|
|
8422
|
+
const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
8423
|
+
const code = options.code || getLineageCodeIdentity(channel);
|
|
8424
|
+
const databaseInfo = { exists: existsSync11(dbPath), path: dbPath };
|
|
8425
|
+
const schema = { migration_keys: [] };
|
|
7232
8426
|
if (databaseInfo.exists) {
|
|
7233
8427
|
try {
|
|
7234
|
-
const stat =
|
|
8428
|
+
const stat = statSync5(dbPath);
|
|
7235
8429
|
databaseInfo.modified_at = stat.mtime.toISOString();
|
|
7236
8430
|
databaseInfo.size_bytes = stat.size;
|
|
7237
|
-
const { DatabaseSync } =
|
|
8431
|
+
const { DatabaseSync } = require5("node:sqlite");
|
|
7238
8432
|
const database = new DatabaseSync(dbPath, { readOnly: true });
|
|
7239
8433
|
try {
|
|
7240
8434
|
databaseInfo.projects = tableCount(database, "projects");
|
|
7241
8435
|
databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
|
|
8436
|
+
schema.migration_keys = migrationKeys(database);
|
|
8437
|
+
if (tableExists2(database, "lineage_profile_identity")) {
|
|
8438
|
+
const columns = new Set(database.prepare("pragma table_info(lineage_profile_identity)").all().map((row) => row.name));
|
|
8439
|
+
const fingerprintExpression = columns.has("profile_fingerprint") ? "profile_fingerprint" : "null as profile_fingerprint";
|
|
8440
|
+
const rows = database.prepare(`select profile_id, environment, ${fingerprintExpression} from lineage_profile_identity`).all();
|
|
8441
|
+
if (rows.length === 1) {
|
|
8442
|
+
schema.profile_id = rows[0].profile_id;
|
|
8443
|
+
schema.profile_environment = rows[0].environment;
|
|
8444
|
+
if (rows[0].profile_fingerprint) schema.profile_fingerprint = rows[0].profile_fingerprint;
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
7242
8447
|
} finally {
|
|
7243
8448
|
database.close();
|
|
7244
8449
|
}
|
|
@@ -7248,24 +8453,974 @@ function getLineageRuntimeInfo(options = {}) {
|
|
|
7248
8453
|
}
|
|
7249
8454
|
return {
|
|
7250
8455
|
asset_root: repoRoot,
|
|
7251
|
-
channel
|
|
8456
|
+
channel,
|
|
8457
|
+
code,
|
|
7252
8458
|
database: databaseInfo,
|
|
7253
8459
|
fetchedAt: nowIso(),
|
|
7254
|
-
git_sha:
|
|
8460
|
+
git_sha: code.git_sha,
|
|
7255
8461
|
node_env: process.env.NODE_ENV,
|
|
7256
8462
|
package_name: info.name,
|
|
8463
|
+
profile: runtimeProfileIdentity(channel),
|
|
8464
|
+
schema,
|
|
8465
|
+
service: {
|
|
8466
|
+
instance_id: process.env.LINEAGE_SERVICE_INSTANCE_ID,
|
|
8467
|
+
launcher_pid: process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0,
|
|
8468
|
+
pid: process.pid,
|
|
8469
|
+
started_at: processStartedAt
|
|
8470
|
+
},
|
|
7257
8471
|
version: info.version
|
|
7258
8472
|
};
|
|
7259
8473
|
}
|
|
7260
8474
|
|
|
8475
|
+
// src/server/managedWriterRouting.ts
|
|
8476
|
+
var managedWriterRequestSchemaVersion = "lineage.managed_writer_request.v1";
|
|
8477
|
+
var managedWriterResponseSchemaVersion = "lineage.managed_writer_response.v1";
|
|
8478
|
+
var managedWriterRoute = "/api/managed-writer/execute";
|
|
8479
|
+
var delegationHeader = "X-Lineage-Writer-Delegation";
|
|
8480
|
+
var ManagedWriterRoutingError = class extends Error {
|
|
8481
|
+
constructor(message, status = 409, options) {
|
|
8482
|
+
super(message, options);
|
|
8483
|
+
this.status = status;
|
|
8484
|
+
this.name = "ManagedWriterRoutingError";
|
|
8485
|
+
}
|
|
8486
|
+
status;
|
|
8487
|
+
};
|
|
8488
|
+
function isManagedWriterRoutingError(error) {
|
|
8489
|
+
return error instanceof ManagedWriterRoutingError;
|
|
8490
|
+
}
|
|
8491
|
+
function requestBody(value) {
|
|
8492
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8493
|
+
throw new ManagedWriterRoutingError("Managed writer request must be a JSON object", 400);
|
|
8494
|
+
}
|
|
8495
|
+
const body = value;
|
|
8496
|
+
if (body.schema_version !== managedWriterRequestSchemaVersion) {
|
|
8497
|
+
throw new ManagedWriterRoutingError(`Unsupported managed writer request schema: ${String(body.schema_version)}`, 400);
|
|
8498
|
+
}
|
|
8499
|
+
if (typeof body.command !== "string" || !body.command || !Array.isArray(body.args) || body.args.some((arg) => typeof arg !== "string")) {
|
|
8500
|
+
throw new ManagedWriterRoutingError("Managed writer request requires a command and string args", 400);
|
|
8501
|
+
}
|
|
8502
|
+
if (typeof body.profile_id !== "string" || typeof body.channel !== "string" || typeof body.environment !== "string" || typeof body.service_origin !== "string") {
|
|
8503
|
+
throw new ManagedWriterRoutingError("Managed writer request requires profile identity", 400);
|
|
8504
|
+
}
|
|
8505
|
+
return body;
|
|
8506
|
+
}
|
|
8507
|
+
function containsProtectedOverride(args) {
|
|
8508
|
+
return args.some((arg) => arg === "--db" || arg.startsWith("--db=") || arg === "--asset-root" || arg.startsWith("--asset-root=") || arg === "--profile" || arg.startsWith("--profile="));
|
|
8509
|
+
}
|
|
8510
|
+
function registerManagedWriterRoute(app2, fields) {
|
|
8511
|
+
app2.post(managedWriterRoute, (req, res, next) => {
|
|
8512
|
+
try {
|
|
8513
|
+
if (!fields.profile || !fields.writerLease) {
|
|
8514
|
+
throw new ManagedWriterRoutingError("Managed writer delegation is unavailable for an unprofiled service", 404);
|
|
8515
|
+
}
|
|
8516
|
+
if (!fields.writerLease.authenticate(req.header(delegationHeader))) {
|
|
8517
|
+
throw new ManagedWriterRoutingError("Managed writer delegation was not authorized", 401);
|
|
8518
|
+
}
|
|
8519
|
+
const body = requestBody(req.body);
|
|
8520
|
+
if (body.profile_id !== fields.profile.profile_id || body.channel !== fields.channel || body.environment !== fields.profile.environment || body.service_origin !== fields.profile.service_origin) {
|
|
8521
|
+
throw new ManagedWriterRoutingError("Managed writer request profile/service identity does not match this service", 409);
|
|
8522
|
+
}
|
|
8523
|
+
if (containsProtectedOverride(body.args)) {
|
|
8524
|
+
throw new ManagedWriterRoutingError("Managed writer requests cannot override profile, database, or asset root", 400);
|
|
8525
|
+
}
|
|
8526
|
+
if (!fields.accepts(body.command, body.args)) {
|
|
8527
|
+
throw new ManagedWriterRoutingError(`Unsupported managed writer command: ${body.command}`, 400);
|
|
8528
|
+
}
|
|
8529
|
+
const result = fields.execute(body.command, body.args);
|
|
8530
|
+
const response = {
|
|
8531
|
+
ok: true,
|
|
8532
|
+
result,
|
|
8533
|
+
schema_version: managedWriterResponseSchemaVersion,
|
|
8534
|
+
service: {
|
|
8535
|
+
channel: fields.channel,
|
|
8536
|
+
environment: fields.profile.environment,
|
|
8537
|
+
profile_id: fields.profile.profile_id,
|
|
8538
|
+
service_origin: fields.profile.service_origin
|
|
8539
|
+
}
|
|
8540
|
+
};
|
|
8541
|
+
res.json(response);
|
|
8542
|
+
} catch (error) {
|
|
8543
|
+
next(error);
|
|
8544
|
+
}
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
8547
|
+
|
|
8548
|
+
// src/cli/lineageCli.ts
|
|
8549
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8550
|
+
import { dirname as dirname7, join as join13, resolve as resolve8 } from "node:path";
|
|
8551
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8552
|
+
|
|
8553
|
+
// src/server/lineageSelectionPacket.ts
|
|
8554
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8555
|
+
import { existsSync as existsSync12, statSync as statSync6 } from "node:fs";
|
|
8556
|
+
import { isAbsolute as isAbsolute3, resolve as resolve7 } from "node:path";
|
|
8557
|
+
var LineageSelectionPacketError = class extends Error {
|
|
8558
|
+
constructor(message, warnings = [], errors = [], diagnostics = []) {
|
|
8559
|
+
super(message);
|
|
8560
|
+
this.warnings = warnings;
|
|
8561
|
+
this.errors = errors;
|
|
8562
|
+
this.diagnostics = diagnostics;
|
|
8563
|
+
}
|
|
8564
|
+
warnings;
|
|
8565
|
+
errors;
|
|
8566
|
+
diagnostics;
|
|
8567
|
+
};
|
|
8568
|
+
function listAllAssets(project) {
|
|
8569
|
+
const pageSize = 100;
|
|
8570
|
+
const first = listAssets(project, { page: 1, pageSize, source: "all" });
|
|
8571
|
+
const assets = [...first.assets];
|
|
8572
|
+
for (let page = 2; page <= first.pagination.totalPages; page += 1) {
|
|
8573
|
+
assets.push(...listAssets(project, { page, pageSize, source: "all" }).assets);
|
|
8574
|
+
}
|
|
8575
|
+
return assets;
|
|
8576
|
+
}
|
|
8577
|
+
function resolveWorkspace(project, options) {
|
|
8578
|
+
if (options.rootAssetId && options.workspaceId) {
|
|
8579
|
+
throw new LineageSelectionPacketError("Use either --root or --workspace, not both.", [], ["ambiguous_workspace"]);
|
|
8580
|
+
}
|
|
8581
|
+
const snapshot = listLineageWorkspaces(project);
|
|
8582
|
+
const target = options.workspaceId || options.rootAssetId;
|
|
8583
|
+
const workspace = target ? snapshot.workspaces.find((item) => item.id === target || item.root_asset_id === target) : snapshot.active_workspace;
|
|
8584
|
+
if (workspace) return workspace;
|
|
8585
|
+
if (options.rootAssetId) {
|
|
8586
|
+
return {
|
|
8587
|
+
id: lineageWorkspaceId(project, options.rootAssetId),
|
|
8588
|
+
project,
|
|
8589
|
+
root_asset_id: options.rootAssetId,
|
|
8590
|
+
title: `${options.rootAssetId} lineage`,
|
|
8591
|
+
status: "active",
|
|
8592
|
+
created_by: "system",
|
|
8593
|
+
created_at: "",
|
|
8594
|
+
updated_at: ""
|
|
8595
|
+
};
|
|
8596
|
+
}
|
|
8597
|
+
const message = options.workspaceId ? `Unknown lineage workspace: ${options.workspaceId}` : "No active lineage workspace. Pass --root or activate a workspace first.";
|
|
8598
|
+
throw new LineageSelectionPacketError(message, [], [options.workspaceId ? "unknown_workspace" : "missing_active_workspace"]);
|
|
8599
|
+
}
|
|
8600
|
+
function resolveLocalReference(reference) {
|
|
8601
|
+
if (!reference) return void 0;
|
|
8602
|
+
if (isAbsolute3(reference)) return reference;
|
|
8603
|
+
const repoRelative = resolve7(repoRoot, reference);
|
|
8604
|
+
if (existsSync12(repoRelative)) return repoRelative;
|
|
8605
|
+
return resolve7(repoRoot, ".asset-scratch", reference);
|
|
8606
|
+
}
|
|
8607
|
+
function fileSize(path) {
|
|
8608
|
+
if (!path || !existsSync12(path)) return void 0;
|
|
8609
|
+
try {
|
|
8610
|
+
return statSync6(path).size;
|
|
8611
|
+
} catch {
|
|
8612
|
+
return void 0;
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
function storageState(hasLocal, hasS3) {
|
|
8616
|
+
if (hasLocal && hasS3) return "local_and_s3";
|
|
8617
|
+
if (hasLocal) return "local_only";
|
|
8618
|
+
if (hasS3) return "s3_backed";
|
|
8619
|
+
return "unresolved";
|
|
8620
|
+
}
|
|
8621
|
+
function currentAttemptFor(node) {
|
|
8622
|
+
if (!node.current_attempt) return void 0;
|
|
8623
|
+
return {
|
|
8624
|
+
asset_id: node.current_attempt.asset_id,
|
|
8625
|
+
checksum_sha256: node.current_attempt.checksum_sha256,
|
|
8626
|
+
file_path: node.current_attempt.file_path,
|
|
8627
|
+
generation_job_id: node.current_attempt.generation_job_id,
|
|
8628
|
+
id: node.current_attempt.id,
|
|
8629
|
+
is_current: node.current_attempt.is_current,
|
|
8630
|
+
source: node.current_attempt.source
|
|
8631
|
+
};
|
|
8632
|
+
}
|
|
8633
|
+
function assetForNode(node, catalogAsset, warnings) {
|
|
8634
|
+
const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
|
|
8635
|
+
const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
|
|
8636
|
+
const localExists = absolutePath ? existsSync12(absolutePath) : false;
|
|
8637
|
+
const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
|
|
8638
|
+
const s3Key = catalogAsset?.s3?.key || node.s3_key;
|
|
8639
|
+
const hasS3 = Boolean(s3Key);
|
|
8640
|
+
const checksum = catalogAsset?.local?.checksum_sha256 || node.current_attempt?.checksum_sha256 || node.checksum_sha256 || catalogAsset?.s3?.checksum_sha256;
|
|
8641
|
+
const mimeType = catalogAsset?.local?.content_type || catalogAsset?.s3?.content_type;
|
|
8642
|
+
const mediaType = catalogAsset?.content_type || node.media_type;
|
|
8643
|
+
if (hasLocalClaim && !localExists) warnings.push(`Selected asset ${node.asset_id} has a local path but the file is missing: ${absolutePath || node.local_path}`);
|
|
8644
|
+
if (!hasLocalClaim && !hasS3) warnings.push(`Selected asset ${node.asset_id} has neither a local file nor S3 key.`);
|
|
8645
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
8646
|
+
return {
|
|
8647
|
+
asset_id: node.asset_id,
|
|
8648
|
+
campaign: catalogAsset?.campaign || node.campaign,
|
|
8649
|
+
channel: catalogAsset?.channel || node.channel,
|
|
8650
|
+
checksum_sha256: checksum,
|
|
8651
|
+
current_attempt: currentAttemptFor(node),
|
|
8652
|
+
local: {
|
|
8653
|
+
absolute_path: absolutePath,
|
|
8654
|
+
content_type: catalogAsset?.local?.content_type,
|
|
8655
|
+
exists: localExists,
|
|
8656
|
+
relative_path: catalogAsset?.local?.relative_path || node.local_path || node.current_attempt?.file_path,
|
|
8657
|
+
size_bytes: catalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
8658
|
+
},
|
|
8659
|
+
media_type: mediaType,
|
|
8660
|
+
mime_type: mimeType,
|
|
8661
|
+
review_notes: node.review_notes,
|
|
8662
|
+
review_state: node.review_state,
|
|
8663
|
+
s3: {
|
|
8664
|
+
bucket: catalogAsset?.s3?.bucket,
|
|
8665
|
+
checksum_sha256: catalogAsset?.s3?.checksum_sha256,
|
|
8666
|
+
content_type: catalogAsset?.s3?.content_type,
|
|
8667
|
+
etag: catalogAsset?.s3?.etag,
|
|
8668
|
+
key: s3Key,
|
|
8669
|
+
region: catalogAsset?.s3?.region,
|
|
8670
|
+
size_bytes: catalogAsset?.s3?.size_bytes,
|
|
8671
|
+
version_id: catalogAsset?.s3?.version_id
|
|
8672
|
+
},
|
|
8673
|
+
selection_note: node.selection_note,
|
|
8674
|
+
source: catalogAsset?.source || node.source,
|
|
8675
|
+
status: catalogAsset?.status || node.status,
|
|
8676
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
8677
|
+
title: catalogAsset?.title || node.title || node.asset_id
|
|
8678
|
+
};
|
|
8679
|
+
}
|
|
8680
|
+
function packetId(packetIdentity) {
|
|
8681
|
+
const digest = createHash7("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
|
|
8682
|
+
return `lineage_packet_${digest}`;
|
|
8683
|
+
}
|
|
8684
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
8685
|
+
function canonicalValue(value) {
|
|
8686
|
+
if (Array.isArray(value)) return value.map((item) => canonicalValue(item));
|
|
8687
|
+
if (value && typeof value === "object") {
|
|
8688
|
+
return Object.fromEntries(
|
|
8689
|
+
Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonicalValue(item)])
|
|
8690
|
+
);
|
|
8691
|
+
}
|
|
8692
|
+
return value;
|
|
8693
|
+
}
|
|
8694
|
+
function canonicalLineageSelectionPacketIdentityJson(projection) {
|
|
8695
|
+
return JSON.stringify(canonicalValue(projection));
|
|
8696
|
+
}
|
|
8697
|
+
function lineageSelectionPacketV2IdentityProjection(packet) {
|
|
8698
|
+
const assetsById = new Map(packet.assets.map((asset) => [asset.asset_id, asset]));
|
|
8699
|
+
const selection = packet.selection.items.map((item) => {
|
|
8700
|
+
const asset = assetsById.get(item.asset_id);
|
|
8701
|
+
if (!asset) {
|
|
8702
|
+
const diagnostics = [{ asset_id: item.asset_id, code: "selected_asset_missing", severity: "error" }];
|
|
8703
|
+
throw new LineageSelectionPacketError(
|
|
8704
|
+
`Selected asset ${item.asset_id} is absent from the v2 packet asset envelope.`,
|
|
8705
|
+
[],
|
|
8706
|
+
["selected_asset_missing"],
|
|
8707
|
+
diagnostics
|
|
8708
|
+
);
|
|
8709
|
+
}
|
|
8710
|
+
return {
|
|
8711
|
+
asset_id: item.asset_id,
|
|
8712
|
+
campaign: asset.campaign,
|
|
8713
|
+
channel: asset.channel,
|
|
8714
|
+
current_attempt: {
|
|
8715
|
+
asset_id: asset.current_attempt.asset_id,
|
|
8716
|
+
attempt_index: asset.current_attempt.attempt_index,
|
|
8717
|
+
checksum_sha256: asset.current_attempt.checksum_sha256,
|
|
8718
|
+
id: asset.current_attempt.id,
|
|
8719
|
+
source: asset.current_attempt.source
|
|
8720
|
+
},
|
|
8721
|
+
media_type: asset.media_type,
|
|
8722
|
+
mime_type: asset.mime_type,
|
|
8723
|
+
position: item.position,
|
|
8724
|
+
selection_note: item.selection_note,
|
|
8725
|
+
title: asset.title
|
|
8726
|
+
};
|
|
8727
|
+
});
|
|
8728
|
+
return {
|
|
8729
|
+
schema_version: "lineage.selection_packet.v2",
|
|
8730
|
+
project: packet.project,
|
|
8731
|
+
product: packet.product,
|
|
8732
|
+
workspace: {
|
|
8733
|
+
id: packet.workspace.id,
|
|
8734
|
+
root_asset_id: packet.workspace.root_asset_id
|
|
8735
|
+
},
|
|
8736
|
+
context: {
|
|
8737
|
+
campaign: packet.context.campaign,
|
|
8738
|
+
channel: packet.context.channel,
|
|
8739
|
+
labels: packet.context.labels,
|
|
8740
|
+
notes: packet.context.notes
|
|
8741
|
+
},
|
|
8742
|
+
selection,
|
|
8743
|
+
diagnostics: packet.diagnostics.map((diagnostic) => ({
|
|
8744
|
+
code: diagnostic.code,
|
|
8745
|
+
severity: diagnostic.severity,
|
|
8746
|
+
...diagnostic.asset_id ? { asset_id: diagnostic.asset_id } : {}
|
|
8747
|
+
}))
|
|
8748
|
+
};
|
|
8749
|
+
}
|
|
8750
|
+
function lineageSelectionPacketV2IdentitySha256(packet) {
|
|
8751
|
+
const projection = lineageSelectionPacketV2IdentityProjection(packet);
|
|
8752
|
+
return createHash7("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
|
|
8753
|
+
}
|
|
8754
|
+
function addDiagnostic(diagnostics, diagnostic) {
|
|
8755
|
+
if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
|
|
8756
|
+
diagnostics.push(diagnostic);
|
|
8757
|
+
}
|
|
8758
|
+
function requireV2CurrentAttempt(node, warnings, diagnostics) {
|
|
8759
|
+
const attempt = node.current_attempt;
|
|
8760
|
+
if (!attempt) {
|
|
8761
|
+
const message = `Selected asset ${node.asset_id} has no current attempt identity.`;
|
|
8762
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_missing", severity: "error" };
|
|
8763
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_missing"], [...diagnostics, diagnostic]);
|
|
8764
|
+
}
|
|
8765
|
+
if (!attempt.id || !attempt.asset_id || !Number.isInteger(attempt.attempt_index) || attempt.attempt_index <= 0 || !attempt.source || attempt.is_current !== true) {
|
|
8766
|
+
const message = `Selected asset ${node.asset_id} has malformed current attempt identity.`;
|
|
8767
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_identity", severity: "error" };
|
|
8768
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_identity"], [...diagnostics, diagnostic]);
|
|
8769
|
+
}
|
|
8770
|
+
if (!attempt.checksum_sha256 || !SHA256_PATTERN.test(attempt.checksum_sha256)) {
|
|
8771
|
+
const message = `Selected asset ${node.asset_id} current attempt does not have a valid lowercase SHA-256 checksum.`;
|
|
8772
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_checksum", severity: "error" };
|
|
8773
|
+
throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_checksum"], [...diagnostics, diagnostic]);
|
|
8774
|
+
}
|
|
8775
|
+
return {
|
|
8776
|
+
asset_id: attempt.asset_id,
|
|
8777
|
+
attempt_index: attempt.attempt_index,
|
|
8778
|
+
checksum_sha256: attempt.checksum_sha256,
|
|
8779
|
+
file_path: attempt.file_path,
|
|
8780
|
+
generation_job_id: attempt.generation_job_id,
|
|
8781
|
+
id: attempt.id,
|
|
8782
|
+
is_current: attempt.is_current,
|
|
8783
|
+
source: attempt.source
|
|
8784
|
+
};
|
|
8785
|
+
}
|
|
8786
|
+
function assetForNodeV2(node, visibleCatalogAsset, catalogById, warnings, errors, diagnostics) {
|
|
8787
|
+
const currentAttempt = requireV2CurrentAttempt(node, warnings, diagnostics);
|
|
8788
|
+
const currentAttemptCatalogAsset = catalogById.get(currentAttempt.asset_id);
|
|
8789
|
+
const isVisibleNodeAttempt = currentAttempt.asset_id === node.asset_id;
|
|
8790
|
+
const mediaCatalogAsset = currentAttemptCatalogAsset || (isVisibleNodeAttempt ? visibleCatalogAsset : void 0);
|
|
8791
|
+
const localReference = currentAttempt.file_path || mediaCatalogAsset?.local?.absolute_path || mediaCatalogAsset?.local?.relative_path || (isVisibleNodeAttempt ? node.local_path : void 0);
|
|
8792
|
+
const absolutePath = resolveLocalReference(localReference);
|
|
8793
|
+
const localExists = absolutePath ? existsSync12(absolutePath) : false;
|
|
8794
|
+
const hasLocalClaim = Boolean(localReference);
|
|
8795
|
+
const s3Key = mediaCatalogAsset?.s3?.key || (isVisibleNodeAttempt ? node.s3_key : void 0);
|
|
8796
|
+
const hasS3 = Boolean(s3Key);
|
|
8797
|
+
const mimeType = mediaCatalogAsset?.local?.content_type || mediaCatalogAsset?.s3?.content_type || visibleCatalogAsset?.local?.content_type || visibleCatalogAsset?.s3?.content_type || (localReference ? contentTypeFor(localReference) : void 0);
|
|
8798
|
+
const mediaType = mediaCatalogAsset?.content_type || visibleCatalogAsset?.content_type || node.media_type;
|
|
8799
|
+
if (localExists && absolutePath && fileSha256(absolutePath) !== currentAttempt.checksum_sha256) {
|
|
8800
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its local file.`;
|
|
8801
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
8802
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
8803
|
+
}
|
|
8804
|
+
const s3Checksum = mediaCatalogAsset?.s3?.checksum_sha256;
|
|
8805
|
+
if (hasS3 && s3Checksum && s3Checksum !== currentAttempt.checksum_sha256) {
|
|
8806
|
+
const message = `Selected asset ${node.asset_id} current attempt checksum does not match its S3 media envelope.`;
|
|
8807
|
+
const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
|
|
8808
|
+
throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
|
|
8809
|
+
}
|
|
8810
|
+
if (hasLocalClaim && !localExists) {
|
|
8811
|
+
warnings.push(`Selected asset ${node.asset_id} current attempt has a local path but the file is missing: ${absolutePath || localReference}`);
|
|
8812
|
+
}
|
|
8813
|
+
if (!hasLocalClaim && !hasS3) {
|
|
8814
|
+
const message = `Selected asset ${node.asset_id} current attempt has neither a local file nor S3 key.`;
|
|
8815
|
+
warnings.push(message);
|
|
8816
|
+
errors.push(message);
|
|
8817
|
+
}
|
|
8818
|
+
if (mediaType && mediaType !== "image" && mediaType !== "gif") {
|
|
8819
|
+
warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
|
|
8820
|
+
addDiagnostic(diagnostics, { asset_id: node.asset_id, code: "unsupported_media_type", severity: "warning" });
|
|
8821
|
+
}
|
|
8822
|
+
const conflictingChecksums = [
|
|
8823
|
+
currentAttemptCatalogAsset?.local?.checksum_sha256,
|
|
8824
|
+
currentAttemptCatalogAsset?.s3?.checksum_sha256,
|
|
8825
|
+
visibleCatalogAsset?.local?.checksum_sha256,
|
|
8826
|
+
visibleCatalogAsset?.s3?.checksum_sha256,
|
|
8827
|
+
node.checksum_sha256
|
|
8828
|
+
].filter((checksum) => Boolean(checksum && checksum !== currentAttempt.checksum_sha256));
|
|
8829
|
+
if (conflictingChecksums.length > 0) {
|
|
8830
|
+
warnings.push(`Selected asset ${node.asset_id} catalog or visible-node checksum differs from its current attempt; current attempt checksum is authoritative.`);
|
|
8831
|
+
}
|
|
8832
|
+
return {
|
|
8833
|
+
asset_id: node.asset_id,
|
|
8834
|
+
campaign: visibleCatalogAsset?.campaign || node.campaign,
|
|
8835
|
+
channel: visibleCatalogAsset?.channel || node.channel,
|
|
8836
|
+
checksum_sha256: currentAttempt.checksum_sha256,
|
|
8837
|
+
current_attempt: currentAttempt,
|
|
8838
|
+
local: {
|
|
8839
|
+
absolute_path: absolutePath,
|
|
8840
|
+
content_type: mediaCatalogAsset?.local?.content_type,
|
|
8841
|
+
exists: localExists,
|
|
8842
|
+
relative_path: mediaCatalogAsset?.local?.relative_path || currentAttempt.file_path || (isVisibleNodeAttempt ? node.local_path : void 0),
|
|
8843
|
+
size_bytes: mediaCatalogAsset?.local?.size_bytes || fileSize(absolutePath)
|
|
8844
|
+
},
|
|
8845
|
+
media_type: mediaType,
|
|
8846
|
+
mime_type: mimeType,
|
|
8847
|
+
review_notes: node.review_notes,
|
|
8848
|
+
review_state: node.review_state,
|
|
8849
|
+
s3: {
|
|
8850
|
+
bucket: mediaCatalogAsset?.s3?.bucket,
|
|
8851
|
+
checksum_sha256: mediaCatalogAsset?.s3?.checksum_sha256,
|
|
8852
|
+
content_type: mediaCatalogAsset?.s3?.content_type,
|
|
8853
|
+
etag: mediaCatalogAsset?.s3?.etag,
|
|
8854
|
+
key: s3Key,
|
|
8855
|
+
region: mediaCatalogAsset?.s3?.region,
|
|
8856
|
+
size_bytes: mediaCatalogAsset?.s3?.size_bytes,
|
|
8857
|
+
version_id: mediaCatalogAsset?.s3?.version_id
|
|
8858
|
+
},
|
|
8859
|
+
selection_note: node.selection_note,
|
|
8860
|
+
source: mediaCatalogAsset?.source || visibleCatalogAsset?.source || node.source,
|
|
8861
|
+
status: visibleCatalogAsset?.status || node.status,
|
|
8862
|
+
storage_state: storageState(hasLocalClaim, hasS3),
|
|
8863
|
+
title: visibleCatalogAsset?.title || node.title || node.asset_id
|
|
8864
|
+
};
|
|
8865
|
+
}
|
|
8866
|
+
function getLineageSelectionPacketV1(project, options = {}) {
|
|
8867
|
+
const workspace = resolveWorkspace(project, options);
|
|
8868
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
8869
|
+
const catalogSummary = validateProject(project);
|
|
8870
|
+
const catalogAssets = listAllAssets(project);
|
|
8871
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
8872
|
+
const warnings = [];
|
|
8873
|
+
const errors = [];
|
|
8874
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
8875
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
8876
|
+
asset_id: selection.asset_id,
|
|
8877
|
+
position: selection.position,
|
|
8878
|
+
selected_at: selection.selected_at,
|
|
8879
|
+
selection_note: selection.notes
|
|
8880
|
+
}));
|
|
8881
|
+
if (selectedItems.length === 0) warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
8882
|
+
const assets = selectedItems.flatMap((item) => {
|
|
8883
|
+
const node = nodeById.get(item.asset_id);
|
|
8884
|
+
if (!node) {
|
|
8885
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
8886
|
+
warnings.push(message);
|
|
8887
|
+
errors.push("selected_asset_outside_workspace");
|
|
8888
|
+
return [];
|
|
8889
|
+
}
|
|
8890
|
+
return [assetForNode(node, catalogById.get(item.asset_id), warnings)];
|
|
8891
|
+
});
|
|
8892
|
+
if (assets.some((asset) => !asset.dimensions)) warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
8893
|
+
if (options.strict) {
|
|
8894
|
+
const strictErrors = [
|
|
8895
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
8896
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
8897
|
+
...errors
|
|
8898
|
+
];
|
|
8899
|
+
if (strictErrors.length > 0) {
|
|
8900
|
+
throw new LineageSelectionPacketError(`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`, warnings, strictErrors);
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8903
|
+
const context = {
|
|
8904
|
+
campaign: options.campaign,
|
|
8905
|
+
channel: options.channel,
|
|
8906
|
+
labels: options.labels || [],
|
|
8907
|
+
notes: options.contextNotes
|
|
8908
|
+
};
|
|
8909
|
+
const identity = {
|
|
8910
|
+
assets: assets.map((asset) => ({
|
|
8911
|
+
asset_id: asset.asset_id,
|
|
8912
|
+
checksum_sha256: asset.checksum_sha256,
|
|
8913
|
+
local_path: asset.local.absolute_path,
|
|
8914
|
+
s3_key: asset.s3.key,
|
|
8915
|
+
storage_state: asset.storage_state
|
|
8916
|
+
})),
|
|
8917
|
+
context,
|
|
8918
|
+
project,
|
|
8919
|
+
root_asset_id: workspace.root_asset_id,
|
|
8920
|
+
schema_version: "lineage.selection_packet.v1",
|
|
8921
|
+
selection: selectedItems,
|
|
8922
|
+
workspace_id: workspace.id
|
|
8923
|
+
};
|
|
8924
|
+
return {
|
|
8925
|
+
assets,
|
|
8926
|
+
context,
|
|
8927
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8928
|
+
errors,
|
|
8929
|
+
kind: "lineage.selection_packet",
|
|
8930
|
+
packet_id: packetId(identity),
|
|
8931
|
+
product: catalogSummary.product,
|
|
8932
|
+
project,
|
|
8933
|
+
schema_version: "lineage.selection_packet.v1",
|
|
8934
|
+
selection: {
|
|
8935
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
8936
|
+
count: selectedItems.length,
|
|
8937
|
+
items: selectedItems,
|
|
8938
|
+
root_asset_id: workspace.root_asset_id
|
|
8939
|
+
},
|
|
8940
|
+
source: {
|
|
8941
|
+
app: "lineage",
|
|
8942
|
+
command: options.command,
|
|
8943
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
8944
|
+
package: options.packageVersion
|
|
8945
|
+
},
|
|
8946
|
+
warnings: [...new Set(warnings)],
|
|
8947
|
+
workspace: {
|
|
8948
|
+
active_at: workspace.active_at,
|
|
8949
|
+
id: workspace.id,
|
|
8950
|
+
notes: workspace.notes,
|
|
8951
|
+
root_asset_id: workspace.root_asset_id,
|
|
8952
|
+
status: workspace.status,
|
|
8953
|
+
title: workspace.title
|
|
8954
|
+
}
|
|
8955
|
+
};
|
|
8956
|
+
}
|
|
8957
|
+
function getLineageSelectionPacketV2(project, options) {
|
|
8958
|
+
const workspace = resolveWorkspace(project, options);
|
|
8959
|
+
const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
|
|
8960
|
+
const catalogSummary = validateProject(project);
|
|
8961
|
+
const catalogAssets = listAllAssets(project);
|
|
8962
|
+
const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
|
|
8963
|
+
const warnings = [];
|
|
8964
|
+
const errors = [];
|
|
8965
|
+
const diagnostics = [];
|
|
8966
|
+
const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
|
|
8967
|
+
const selectedItems = snapshot.selections.map((selection) => ({
|
|
8968
|
+
asset_id: selection.asset_id,
|
|
8969
|
+
position: selection.position,
|
|
8970
|
+
selected_at: selection.selected_at,
|
|
8971
|
+
selection_note: selection.notes
|
|
8972
|
+
}));
|
|
8973
|
+
if (selectedItems.length === 0) {
|
|
8974
|
+
warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
|
|
8975
|
+
addDiagnostic(diagnostics, { code: "empty_selection", severity: "warning" });
|
|
8976
|
+
}
|
|
8977
|
+
const assets = [];
|
|
8978
|
+
for (const item of selectedItems) {
|
|
8979
|
+
const node = nodeById.get(item.asset_id);
|
|
8980
|
+
if (!node) {
|
|
8981
|
+
const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
|
|
8982
|
+
warnings.push(message);
|
|
8983
|
+
errors.push(message);
|
|
8984
|
+
const diagnostic = { asset_id: item.asset_id, code: "selected_asset_outside_workspace", severity: "error" };
|
|
8985
|
+
addDiagnostic(diagnostics, diagnostic);
|
|
8986
|
+
throw new LineageSelectionPacketError(message, warnings, ["selected_asset_outside_workspace"], diagnostics);
|
|
8987
|
+
}
|
|
8988
|
+
assets.push(assetForNodeV2(node, catalogById.get(item.asset_id), catalogById, warnings, errors, diagnostics));
|
|
8989
|
+
}
|
|
8990
|
+
if (assets.some((asset) => !asset.dimensions)) {
|
|
8991
|
+
warnings.push("Image dimensions are unavailable for one or more selected assets.");
|
|
8992
|
+
}
|
|
8993
|
+
if (options.strict) {
|
|
8994
|
+
const strictErrors = [
|
|
8995
|
+
...selectedItems.length === 0 ? ["empty_selection"] : [],
|
|
8996
|
+
...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
|
|
8997
|
+
...assets.filter((asset) => asset.storage_state === "unresolved").map((asset) => `unresolved_current_attempt_media:${asset.asset_id}`),
|
|
8998
|
+
...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => `${diagnostic.code}${diagnostic.asset_id ? `:${diagnostic.asset_id}` : ""}`)
|
|
8999
|
+
];
|
|
9000
|
+
if (strictErrors.length > 0) {
|
|
9001
|
+
throw new LineageSelectionPacketError(
|
|
9002
|
+
`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`,
|
|
9003
|
+
warnings,
|
|
9004
|
+
strictErrors,
|
|
9005
|
+
diagnostics
|
|
9006
|
+
);
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
9009
|
+
const packet = {
|
|
9010
|
+
assets,
|
|
9011
|
+
context: {
|
|
9012
|
+
campaign: options.campaign,
|
|
9013
|
+
channel: options.channel,
|
|
9014
|
+
labels: options.labels || [],
|
|
9015
|
+
notes: options.contextNotes
|
|
9016
|
+
},
|
|
9017
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9018
|
+
diagnostics,
|
|
9019
|
+
errors: [...new Set(errors)],
|
|
9020
|
+
identity_sha256: "",
|
|
9021
|
+
kind: "lineage.selection_packet",
|
|
9022
|
+
packet_id: "",
|
|
9023
|
+
product: catalogSummary.product,
|
|
9024
|
+
project,
|
|
9025
|
+
schema_version: "lineage.selection_packet.v2",
|
|
9026
|
+
selection: {
|
|
9027
|
+
asset_ids: selectedItems.map((item) => item.asset_id),
|
|
9028
|
+
count: selectedItems.length,
|
|
9029
|
+
items: selectedItems,
|
|
9030
|
+
root_asset_id: workspace.root_asset_id
|
|
9031
|
+
},
|
|
9032
|
+
source: {
|
|
9033
|
+
app: "lineage",
|
|
9034
|
+
command: options.command,
|
|
9035
|
+
db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
|
|
9036
|
+
package: options.packageVersion
|
|
9037
|
+
},
|
|
9038
|
+
warnings: [...new Set(warnings)],
|
|
9039
|
+
workspace: {
|
|
9040
|
+
active_at: workspace.active_at,
|
|
9041
|
+
id: workspace.id,
|
|
9042
|
+
notes: workspace.notes,
|
|
9043
|
+
root_asset_id: workspace.root_asset_id,
|
|
9044
|
+
status: workspace.status,
|
|
9045
|
+
title: workspace.title
|
|
9046
|
+
}
|
|
9047
|
+
};
|
|
9048
|
+
packet.identity_sha256 = lineageSelectionPacketV2IdentitySha256(packet);
|
|
9049
|
+
packet.packet_id = `lineage_packet_${packet.identity_sha256.slice(0, 24)}`;
|
|
9050
|
+
return packet;
|
|
9051
|
+
}
|
|
9052
|
+
function getLineageSelectionPacket(project, options = {}) {
|
|
9053
|
+
return options.schema === "v2" ? getLineageSelectionPacketV2(project, options) : getLineageSelectionPacketV1(project, options);
|
|
9054
|
+
}
|
|
9055
|
+
|
|
9056
|
+
// src/cli/lineageCli.ts
|
|
9057
|
+
function packageRoot2() {
|
|
9058
|
+
return resolve8(dirname7(fileURLToPath3(import.meta.url)), "..", "..");
|
|
9059
|
+
}
|
|
9060
|
+
function packageVersion() {
|
|
9061
|
+
try {
|
|
9062
|
+
const packageInfo2 = JSON.parse(readFileSync8(join13(packageRoot2(), "package.json"), "utf8"));
|
|
9063
|
+
return packageInfo2.version || "0.0.0";
|
|
9064
|
+
} catch {
|
|
9065
|
+
return "0.0.0";
|
|
9066
|
+
}
|
|
9067
|
+
}
|
|
9068
|
+
function readOption(args, name) {
|
|
9069
|
+
const prefix = `${name}=`;
|
|
9070
|
+
const inline = args.find((arg) => arg.startsWith(prefix));
|
|
9071
|
+
if (inline) return inline.slice(prefix.length);
|
|
9072
|
+
const index = args.indexOf(name);
|
|
9073
|
+
if (index >= 0) return args[index + 1];
|
|
9074
|
+
return void 0;
|
|
9075
|
+
}
|
|
9076
|
+
function readOptions(args, name) {
|
|
9077
|
+
const values = [];
|
|
9078
|
+
const prefix = `${name}=`;
|
|
9079
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9080
|
+
const arg = args[index];
|
|
9081
|
+
if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length));
|
|
9082
|
+
else if (arg === name && args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
9083
|
+
values.push(args[index + 1]);
|
|
9084
|
+
index += 1;
|
|
9085
|
+
}
|
|
9086
|
+
}
|
|
9087
|
+
return values;
|
|
9088
|
+
}
|
|
9089
|
+
function resolveCliAssetRoot(args) {
|
|
9090
|
+
return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
|
|
9091
|
+
}
|
|
9092
|
+
function configureCliAssetRoot(args) {
|
|
9093
|
+
const assetRoot = resolveCliAssetRoot(args);
|
|
9094
|
+
process.env.LINEAGE_ASSET_ROOT = assetRoot;
|
|
9095
|
+
return setLineageAssetRoot(assetRoot);
|
|
9096
|
+
}
|
|
9097
|
+
function positionalArgs(args) {
|
|
9098
|
+
const values = [];
|
|
9099
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9100
|
+
const arg = args[index];
|
|
9101
|
+
if (arg.startsWith("--")) {
|
|
9102
|
+
if (!arg.includes("=") && args[index + 1] && !args[index + 1].startsWith("--")) index += 1;
|
|
9103
|
+
continue;
|
|
9104
|
+
}
|
|
9105
|
+
values.push(arg);
|
|
9106
|
+
}
|
|
9107
|
+
return values;
|
|
9108
|
+
}
|
|
9109
|
+
function resolveDataCommandOptions(args) {
|
|
9110
|
+
configureCliAssetRoot(args);
|
|
9111
|
+
const positions = positionalArgs(args);
|
|
9112
|
+
const options = {
|
|
9113
|
+
assetId: readOption(args, "--asset-id") || positions[0],
|
|
9114
|
+
childAssetId: readOption(args, "--child"),
|
|
9115
|
+
claimToken: readOption(args, "--claim-token") || process.env.LINEAGE_CLAIM_TOKEN,
|
|
9116
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
9117
|
+
dbPath: readOption(args, "--db"),
|
|
9118
|
+
json: args.includes("--json"),
|
|
9119
|
+
project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
|
|
9120
|
+
rootAssetId: readOption(args, "--root")
|
|
9121
|
+
};
|
|
9122
|
+
if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
|
|
9123
|
+
return options;
|
|
9124
|
+
}
|
|
9125
|
+
function runLineageDataCommand(command, args) {
|
|
9126
|
+
const options = resolveDataCommandOptions(args);
|
|
9127
|
+
if (command === "next") return getLineageNextAsset(options.project, options.rootAssetId || options.assetId);
|
|
9128
|
+
if (command === "brief") return getLineageBrief(options.project, options.rootAssetId || options.assetId);
|
|
9129
|
+
if (command === "inspect") {
|
|
9130
|
+
if (!options.assetId) throw new Error("lineage inspect requires --asset-id");
|
|
9131
|
+
return getLineageSnapshot(options.project, options.assetId);
|
|
9132
|
+
}
|
|
9133
|
+
if (command === "selection") {
|
|
9134
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
9135
|
+
if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
|
|
9136
|
+
const schema = readOption(args, "--schema");
|
|
9137
|
+
if (schema && schema !== "v2") throw new Error(`Unsupported selection packet schema: ${schema}. Omit --schema for v1 or pass --schema v2.`);
|
|
9138
|
+
const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
|
|
9139
|
+
const packet = getLineageSelectionPacket(options.project, {
|
|
9140
|
+
campaign: readOption(args, "--campaign"),
|
|
9141
|
+
channel: readOption(args, "--channel"),
|
|
9142
|
+
command: "lineage selection packet",
|
|
9143
|
+
contextNotes: readOption(args, "--context-notes") || readOption(args, "--notes"),
|
|
9144
|
+
dbPath: options.dbPath,
|
|
9145
|
+
labels,
|
|
9146
|
+
packageVersion: packageVersion(),
|
|
9147
|
+
rootAssetId: options.rootAssetId,
|
|
9148
|
+
schema: schema === "v2" ? "v2" : void 0,
|
|
9149
|
+
strict: args.includes("--strict"),
|
|
9150
|
+
workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
|
|
9151
|
+
});
|
|
9152
|
+
const out = readOption(args, "--out");
|
|
9153
|
+
if (out) {
|
|
9154
|
+
const outPath = resolve8(out);
|
|
9155
|
+
mkdirSync8(dirname7(outPath), { recursive: true });
|
|
9156
|
+
writeFileSync6(outPath, `${JSON.stringify(packet, null, 2)}
|
|
9157
|
+
`);
|
|
9158
|
+
}
|
|
9159
|
+
return packet;
|
|
9160
|
+
}
|
|
9161
|
+
if (command === "link-child") {
|
|
9162
|
+
if (!options.childAssetId) throw new Error("lineage link-child requires --child");
|
|
9163
|
+
return linkSelectedLineageChild(options.project, {
|
|
9164
|
+
childAssetId: options.childAssetId,
|
|
9165
|
+
claimToken: options.claimToken,
|
|
9166
|
+
confirmWrite: options.confirmWrite,
|
|
9167
|
+
rootAssetId: options.rootAssetId || options.assetId
|
|
9168
|
+
});
|
|
9169
|
+
}
|
|
9170
|
+
if (command === "reroll") {
|
|
9171
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
9172
|
+
if (subcommand === "list") {
|
|
9173
|
+
if (!options.rootAssetId) throw new Error("lineage reroll list requires --root");
|
|
9174
|
+
return listLineageRerollRequests(options.project, options.rootAssetId);
|
|
9175
|
+
}
|
|
9176
|
+
if (subcommand === "mark") {
|
|
9177
|
+
const targetAssetId = readOption(args, "--target");
|
|
9178
|
+
const requestedBy = rerollRequestedBy(readOption(args, "--requested-by") || "agent");
|
|
9179
|
+
if (!options.rootAssetId) throw new Error("lineage reroll mark requires --root");
|
|
9180
|
+
if (!targetAssetId) throw new Error("lineage reroll mark requires --target");
|
|
9181
|
+
return markLineageRerollRequest(options.project, {
|
|
9182
|
+
rootAssetId: options.rootAssetId,
|
|
9183
|
+
nodeAssetId: targetAssetId,
|
|
9184
|
+
notes: readOption(args, "--notes"),
|
|
9185
|
+
requestedBy,
|
|
9186
|
+
confirmWrite: options.confirmWrite
|
|
9187
|
+
});
|
|
9188
|
+
}
|
|
9189
|
+
if (subcommand === "cancel") {
|
|
9190
|
+
const targetAssetId = readOption(args, "--target");
|
|
9191
|
+
if (!options.rootAssetId) throw new Error("lineage reroll cancel requires --root");
|
|
9192
|
+
if (!targetAssetId) throw new Error("lineage reroll cancel requires --target");
|
|
9193
|
+
return clearLineageRerollRequest(options.project, {
|
|
9194
|
+
rootAssetId: options.rootAssetId,
|
|
9195
|
+
nodeAssetId: targetAssetId,
|
|
9196
|
+
confirmWrite: options.confirmWrite
|
|
9197
|
+
});
|
|
9198
|
+
}
|
|
9199
|
+
if (subcommand === "plan") {
|
|
9200
|
+
const targetAssetId = readOption(args, "--target");
|
|
9201
|
+
const prompt = readOption(args, "--prompt");
|
|
9202
|
+
if (!options.rootAssetId) throw new Error("lineage reroll plan requires --root");
|
|
9203
|
+
if (!targetAssetId) throw new Error("lineage reroll plan requires --target");
|
|
9204
|
+
if (!prompt) throw new Error("lineage reroll plan requires --prompt");
|
|
9205
|
+
return planImageReroll(options.project, {
|
|
9206
|
+
rootAssetId: options.rootAssetId,
|
|
9207
|
+
targetAssetId,
|
|
9208
|
+
prompt,
|
|
9209
|
+
dryRun: args.includes("--dry-run")
|
|
9210
|
+
});
|
|
9211
|
+
}
|
|
9212
|
+
if (subcommand === "import") {
|
|
9213
|
+
const jobId2 = readOption(args, "--job-id");
|
|
9214
|
+
const file = readOption(args, "--file");
|
|
9215
|
+
if (!jobId2) throw new Error("lineage reroll import requires --job-id");
|
|
9216
|
+
if (!file) throw new Error("lineage reroll import requires --file");
|
|
9217
|
+
return importImageRerollOutput(options.project, { jobId: jobId2, file, confirmWrite: options.confirmWrite });
|
|
9218
|
+
}
|
|
9219
|
+
throw new Error(`Unknown reroll command: ${subcommand}`);
|
|
9220
|
+
}
|
|
9221
|
+
if (command === "tasks") {
|
|
9222
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
9223
|
+
const taskId = readOption(args, "--task");
|
|
9224
|
+
if (subcommand === "list") {
|
|
9225
|
+
if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
|
|
9226
|
+
return listLineageTasks(options.project, options.rootAssetId);
|
|
9227
|
+
}
|
|
9228
|
+
if (subcommand === "inspect") {
|
|
9229
|
+
if (!taskId) throw new Error("lineage tasks inspect requires --task");
|
|
9230
|
+
return getLineageTask(options.project, taskId);
|
|
9231
|
+
}
|
|
9232
|
+
if (subcommand === "claim") {
|
|
9233
|
+
const agentName = readOption(args, "--agent-name");
|
|
9234
|
+
if (!taskId) throw new Error("lineage tasks claim requires --task");
|
|
9235
|
+
if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
|
|
9236
|
+
return claimLineageTask(options.project, { taskId, agentName });
|
|
9237
|
+
}
|
|
9238
|
+
if (subcommand === "start") {
|
|
9239
|
+
if (!taskId) throw new Error("lineage tasks start requires --task");
|
|
9240
|
+
if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
|
|
9241
|
+
return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
|
|
9242
|
+
}
|
|
9243
|
+
if (subcommand === "comment") {
|
|
9244
|
+
const message = readOption(args, "--message");
|
|
9245
|
+
if (!taskId) throw new Error("lineage tasks comment requires --task");
|
|
9246
|
+
if (!message) throw new Error("lineage tasks comment requires --message");
|
|
9247
|
+
return addLineageTaskComment(options.project, {
|
|
9248
|
+
actor: readOption(args, "--actor") || "human",
|
|
9249
|
+
message,
|
|
9250
|
+
taskId
|
|
9251
|
+
});
|
|
9252
|
+
}
|
|
9253
|
+
if (subcommand === "cancel") {
|
|
9254
|
+
if (!taskId) throw new Error("lineage tasks cancel requires --task");
|
|
9255
|
+
return cancelLineageTask(options.project, {
|
|
9256
|
+
actor: readOption(args, "--actor") || "human",
|
|
9257
|
+
confirmWrite: options.confirmWrite,
|
|
9258
|
+
override: args.includes("--override"),
|
|
9259
|
+
taskId
|
|
9260
|
+
});
|
|
9261
|
+
}
|
|
9262
|
+
if (subcommand === "override") {
|
|
9263
|
+
const reason = readOption(args, "--reason");
|
|
9264
|
+
if (!taskId) throw new Error("lineage tasks override requires --task");
|
|
9265
|
+
if (!reason) throw new Error("lineage tasks override requires --reason");
|
|
9266
|
+
return overrideLineageTask(options.project, {
|
|
9267
|
+
actor: readOption(args, "--actor") || "human",
|
|
9268
|
+
instructions: readOption(args, "--instructions"),
|
|
9269
|
+
reason,
|
|
9270
|
+
taskId
|
|
9271
|
+
});
|
|
9272
|
+
}
|
|
9273
|
+
if (subcommand === "instructions") {
|
|
9274
|
+
const instructions = readOption(args, "--instructions");
|
|
9275
|
+
if (!taskId) throw new Error("lineage tasks instructions requires --task");
|
|
9276
|
+
if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
|
|
9277
|
+
return updateLineageTaskInstructions(options.project, { instructions, taskId });
|
|
9278
|
+
}
|
|
9279
|
+
throw new Error(`Unknown tasks command: ${subcommand}`);
|
|
9280
|
+
}
|
|
9281
|
+
throw new Error(`Unknown command: ${command}`);
|
|
9282
|
+
}
|
|
9283
|
+
function rerollRequestedBy(value) {
|
|
9284
|
+
if (value === "agent" || value === "human" || value === "system") return value;
|
|
9285
|
+
throw new Error(`Invalid re-roll requester: ${value}`);
|
|
9286
|
+
}
|
|
9287
|
+
function lineageCliCanDelegateMutation(command, args) {
|
|
9288
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
9289
|
+
if (command === "link-child") return true;
|
|
9290
|
+
if (command === "reroll") return ["mark", "cancel", "plan", "import"].includes(subcommand);
|
|
9291
|
+
if (command === "tasks") return ["claim", "start", "comment", "cancel", "override", "instructions"].includes(subcommand);
|
|
9292
|
+
if (command === "agent") return ["claim", "heartbeat", "release", "revoke", "transfer"].includes(subcommand);
|
|
9293
|
+
return false;
|
|
9294
|
+
}
|
|
9295
|
+
function executeDelegatedLineageMutation(command, args) {
|
|
9296
|
+
if (!lineageCliCanDelegateMutation(command, args)) throw new Error(`Unsupported managed writer command: ${command}`);
|
|
9297
|
+
if (command === "agent") {
|
|
9298
|
+
const agentCommand = args[0] || "";
|
|
9299
|
+
return runLineageAgentCommand(agentCommand, args.slice(1));
|
|
9300
|
+
}
|
|
9301
|
+
return runLineageDataCommand(command, args);
|
|
9302
|
+
}
|
|
9303
|
+
function runLineageAgentCommand(command, args) {
|
|
9304
|
+
configureCliAssetRoot(args);
|
|
9305
|
+
const dbPath = readOption(args, "--db");
|
|
9306
|
+
if (dbPath) process.env.LINEAGE_DB = dbPath;
|
|
9307
|
+
const project = readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct;
|
|
9308
|
+
const claimId = readOption(args, "--claim");
|
|
9309
|
+
const claimToken = readOption(args, "--claim-token") || process.env.LINEAGE_CLAIM_TOKEN;
|
|
9310
|
+
if (command === "claim") {
|
|
9311
|
+
return createAgentClaim({
|
|
9312
|
+
agentId: readOption(args, "--agent-id"),
|
|
9313
|
+
agentKind: readOption(args, "--agent-kind"),
|
|
9314
|
+
agentName: readOption(args, "--agent-name") || "",
|
|
9315
|
+
channel: readOption(args, "--channel"),
|
|
9316
|
+
force: args.includes("--force"),
|
|
9317
|
+
project,
|
|
9318
|
+
reason: readOption(args, "--reason"),
|
|
9319
|
+
scopeType: readOption(args, "--scope") || "",
|
|
9320
|
+
targetId: readOption(args, "--target") || "",
|
|
9321
|
+
targetTitle: readOption(args, "--target-title"),
|
|
9322
|
+
threadId: readOption(args, "--thread-id"),
|
|
9323
|
+
ttlSeconds: parseClaimTtl(readOption(args, "--ttl"))
|
|
9324
|
+
});
|
|
9325
|
+
}
|
|
9326
|
+
if (command === "status") return listAgentClaims(project);
|
|
9327
|
+
if (command === "graph") {
|
|
9328
|
+
const rootAssetId = readOption(args, "--root") || readOption(args, "--asset-id") || positionalArgs(args)[0];
|
|
9329
|
+
if (!rootAssetId) throw new Error("lineage agent graph requires --root");
|
|
9330
|
+
return getLineageSnapshot(project, rootAssetId);
|
|
9331
|
+
}
|
|
9332
|
+
if (command === "inspect") {
|
|
9333
|
+
if (!claimId) throw new Error("lineage agent inspect requires --claim");
|
|
9334
|
+
return inspectAgentClaim(claimId, project);
|
|
9335
|
+
}
|
|
9336
|
+
if (command === "heartbeat") {
|
|
9337
|
+
if (!claimToken) throw new Error("lineage agent heartbeat requires --claim-token");
|
|
9338
|
+
return heartbeatAgentClaim(claimToken, parseClaimTtl(readOption(args, "--ttl")));
|
|
9339
|
+
}
|
|
9340
|
+
if (command === "release") {
|
|
9341
|
+
if (!claimToken) throw new Error("lineage agent release requires --claim-token");
|
|
9342
|
+
return releaseAgentClaim(claimToken);
|
|
9343
|
+
}
|
|
9344
|
+
if (command === "revoke") {
|
|
9345
|
+
if (!claimId) throw new Error("lineage agent revoke requires --claim");
|
|
9346
|
+
return revokeAgentClaim(project, claimId, {
|
|
9347
|
+
actor: readOption(args, "--actor") || "human",
|
|
9348
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
9349
|
+
reason: readOption(args, "--reason")
|
|
9350
|
+
});
|
|
9351
|
+
}
|
|
9352
|
+
if (command === "transfer") {
|
|
9353
|
+
if (!claimId) throw new Error("lineage agent transfer requires --claim");
|
|
9354
|
+
return transferAgentClaim(project, claimId, {
|
|
9355
|
+
actor: readOption(args, "--actor") || "human",
|
|
9356
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
9357
|
+
reason: readOption(args, "--reason"),
|
|
9358
|
+
toAgentName: readOption(args, "--to-agent-name") || ""
|
|
9359
|
+
});
|
|
9360
|
+
}
|
|
9361
|
+
throw new Error(`Unknown agent command: ${command}`);
|
|
9362
|
+
}
|
|
9363
|
+
|
|
7261
9364
|
// src/server.ts
|
|
9365
|
+
var runtimeChannel = normalizeRuntimeChannel(process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
|
|
9366
|
+
var startupCode = assertLineageCodeOrigin(runtimeChannel);
|
|
9367
|
+
assertRuntimeProfileSafety(runtimeChannel);
|
|
9368
|
+
var startupRuntime = getLineageRuntimeInfo({ channel: runtimeChannel, code: startupCode });
|
|
9369
|
+
if (!process.env.LINEAGE_PROFILE) assertUnselectedDatabaseIsUnbound(startupRuntime);
|
|
9370
|
+
var startupProfile;
|
|
9371
|
+
if (process.env.LINEAGE_PROFILE) {
|
|
9372
|
+
if (!process.env.LINEAGE_PROFILE_ID || !process.env.LINEAGE_PROFILE_ENVIRONMENT || !process.env.LINEAGE_PROFILE_MANIFEST) {
|
|
9373
|
+
throw new Error("LINEAGE_PROFILE must be resolved by the Lineage CLI before server startup; use lineage start --profile or lineage-dev start --profile");
|
|
9374
|
+
}
|
|
9375
|
+
const doctor = doctorLineageProfile(process.env.LINEAGE_PROFILE, {
|
|
9376
|
+
channel: runtimeChannel,
|
|
9377
|
+
code: startupRuntime.code,
|
|
9378
|
+
gitSha: startupRuntime.git_sha,
|
|
9379
|
+
version: startupRuntime.version
|
|
9380
|
+
});
|
|
9381
|
+
if (!doctor.ok) {
|
|
9382
|
+
const failures = doctor.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
|
|
9383
|
+
throw new Error(`Configured Lineage profile failed doctor: ${failures}`);
|
|
9384
|
+
}
|
|
9385
|
+
assertResolvedRuntimeProfileEnvironment(doctor.profile);
|
|
9386
|
+
startupProfile = doctor.profile;
|
|
9387
|
+
}
|
|
9388
|
+
var writerLease = startupProfile ? acquireProfileWriterLease(startupProfile, runtimeChannel, "service") : void 0;
|
|
9389
|
+
if (startupProfile) delete process.env.LINEAGE_DB_ACCESS;
|
|
9390
|
+
else process.env.LINEAGE_DB_ACCESS = "read-only";
|
|
9391
|
+
if (writerLease) {
|
|
9392
|
+
try {
|
|
9393
|
+
const startupDatabase = lineageDb();
|
|
9394
|
+
startupDatabase.close();
|
|
9395
|
+
} catch (error) {
|
|
9396
|
+
writerLease.release();
|
|
9397
|
+
throw error;
|
|
9398
|
+
}
|
|
9399
|
+
}
|
|
7262
9400
|
var app = express2();
|
|
7263
9401
|
var port = Number(process.env.PORT || 5173);
|
|
7264
9402
|
var host = process.env.HOST || "lineage.localhost";
|
|
7265
9403
|
var isProduction = process.env.NODE_ENV === "production";
|
|
7266
9404
|
var maxUploadBytes = Number(process.env.LINEAGE_MAX_UPLOAD_MB || 200) * 1024 * 1024;
|
|
7267
|
-
var upload = multer({
|
|
9405
|
+
var upload = multer({
|
|
9406
|
+
limits: { fileSize: maxUploadBytes },
|
|
9407
|
+
storage: startupProfile ? multer.diskStorage({ destination: ensureUploadDir() }) : multer.memoryStorage()
|
|
9408
|
+
});
|
|
7268
9409
|
app.use(express2.json({ limit: "1mb" }));
|
|
9410
|
+
registerManagedWriterRoute(app, {
|
|
9411
|
+
accepts: lineageCliCanDelegateMutation,
|
|
9412
|
+
channel: runtimeChannel,
|
|
9413
|
+
execute: executeDelegatedLineageMutation,
|
|
9414
|
+
profile: startupProfile,
|
|
9415
|
+
writerLease
|
|
9416
|
+
});
|
|
9417
|
+
app.use((req, res, next) => {
|
|
9418
|
+
if (startupProfile || req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS") return next();
|
|
9419
|
+
res.status(409).json({
|
|
9420
|
+
error: "profile_required",
|
|
9421
|
+
message: "Persistent writes require a selected named Lineage profile; legacy-unbound service access is read-only."
|
|
9422
|
+
});
|
|
9423
|
+
});
|
|
7269
9424
|
function projectFrom(input) {
|
|
7270
9425
|
const candidate = input.body?.project || input.body?.product || input.query?.project || input.query?.product;
|
|
7271
9426
|
return typeof candidate === "string" ? candidate : defaultProduct;
|
|
@@ -7279,7 +9434,7 @@ app.get("/api/projects", asyncRoute((_req, res) => {
|
|
|
7279
9434
|
res.json({ projects: listProjects() });
|
|
7280
9435
|
}));
|
|
7281
9436
|
app.get("/api/runtime", asyncRoute((_req, res) => {
|
|
7282
|
-
res.json({ ok: true, runtime: getLineageRuntimeInfo() });
|
|
9437
|
+
res.json({ ok: true, runtime: getLineageRuntimeInfo({ channel: runtimeChannel, code: startupCode }) });
|
|
7283
9438
|
}));
|
|
7284
9439
|
app.get(
|
|
7285
9440
|
"/api/assets",
|
|
@@ -7601,16 +9756,16 @@ app.post(
|
|
|
7601
9756
|
})
|
|
7602
9757
|
);
|
|
7603
9758
|
if (isProduction) {
|
|
7604
|
-
const dist =
|
|
7605
|
-
if (
|
|
9759
|
+
const dist = join14(packageRoot, "dist", "web");
|
|
9760
|
+
if (existsSync14(dist)) {
|
|
7606
9761
|
app.use(express2.static(dist));
|
|
7607
|
-
app.get("*", (_req, res) => res.sendFile(
|
|
9762
|
+
app.get("*", (_req, res) => res.sendFile(join14(dist, "index.html")));
|
|
7608
9763
|
}
|
|
7609
9764
|
} else {
|
|
7610
9765
|
const { createServer: createViteServer } = await import("vite");
|
|
7611
9766
|
const e2ePort = process.env.LINEAGE_E2E_PORT ? Number(process.env.LINEAGE_E2E_PORT) : void 0;
|
|
7612
9767
|
const vite = await createViteServer({
|
|
7613
|
-
configFile:
|
|
9768
|
+
configFile: join14(packageRoot, "vite.config.ts"),
|
|
7614
9769
|
server: {
|
|
7615
9770
|
middlewareMode: true,
|
|
7616
9771
|
...e2ePort ? { ws: { port: e2ePort + 1e3 } } : {}
|
|
@@ -7660,6 +9815,10 @@ app.use((error, _req, res, _next) => {
|
|
|
7660
9815
|
res.status(error.status).json({ error: error.message });
|
|
7661
9816
|
return;
|
|
7662
9817
|
}
|
|
9818
|
+
if (isManagedWriterRoutingError(error)) {
|
|
9819
|
+
res.status(error.status).json({ error: error.message });
|
|
9820
|
+
return;
|
|
9821
|
+
}
|
|
7663
9822
|
if (isAgentClaimError(error)) {
|
|
7664
9823
|
res.status(error.status).json({ error: error.code, message: error.message, conflicts: error.conflicts });
|
|
7665
9824
|
return;
|
|
@@ -7679,7 +9838,23 @@ app.use((error, _req, res, _next) => {
|
|
|
7679
9838
|
const message = error instanceof Error ? error.message : String(error);
|
|
7680
9839
|
res.status(500).json({ error: message });
|
|
7681
9840
|
});
|
|
7682
|
-
|
|
7683
|
-
|
|
9841
|
+
var listenOrigin = `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
9842
|
+
var server = app.listen(port, host, () => {
|
|
9843
|
+
console.log(`Lineage listening on ${listenOrigin}`);
|
|
9844
|
+
});
|
|
9845
|
+
var releaseWriterLease = () => writerLease?.release();
|
|
9846
|
+
process.once("exit", releaseWriterLease);
|
|
9847
|
+
process.once("SIGINT", () => {
|
|
9848
|
+
releaseWriterLease();
|
|
9849
|
+
process.exit(130);
|
|
9850
|
+
});
|
|
9851
|
+
process.once("SIGTERM", () => {
|
|
9852
|
+
releaseWriterLease();
|
|
9853
|
+
process.exit(143);
|
|
9854
|
+
});
|
|
9855
|
+
server.once("error", (error) => {
|
|
9856
|
+
releaseWriterLease();
|
|
9857
|
+
console.error(`Lineage failed to listen on ${listenOrigin}: ${error.message}`);
|
|
9858
|
+
process.exit(1);
|
|
7684
9859
|
});
|
|
7685
9860
|
//# sourceMappingURL=server.js.map
|