@mean-weasel/lineage 0.1.14 → 0.1.16
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 +60 -5
- package/dist/cli/lineage-dev.js +889 -55
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage-preview.js +889 -55
- package/dist/cli/lineage-preview.js.map +4 -4
- package/dist/cli/lineage.js +889 -55
- package/dist/cli/lineage.js.map +4 -4
- package/dist/runtime-build.json +4 -4
- package/dist/server.js +854 -88
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-3uJbXqPS.css → app-DwTFac4u.css} +1 -1
- package/dist/web/assets/app-JYTx_Sd4.js +16 -0
- package/dist/web/assets/jsx-runtime-_Rdg6et1.js +8 -0
- package/dist/web/assets/landing-D3Y679_B.css +1 -0
- package/dist/web/assets/landing-ii8AH_DG.js +1 -0
- package/dist/web/index.html +3 -2
- package/dist/web/landing/index.html +19 -0
- package/package.json +2 -1
- package/dist/web/assets/index-DJ5UR_oa.js +0 -23
package/dist/cli/lineage-dev.js
CHANGED
|
@@ -8,6 +8,32 @@ import { dirname as dirname6, join as join10, resolve as resolve8 } from "node:p
|
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
10
10
|
|
|
11
|
+
// src/shared/edgeSummary.ts
|
|
12
|
+
var edgeSummaryWordLimit = 2;
|
|
13
|
+
var EdgeSummaryValidationError = class extends Error {
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "EdgeSummaryValidationError";
|
|
18
|
+
}
|
|
19
|
+
code;
|
|
20
|
+
};
|
|
21
|
+
function normalizeEdgeSummary(value) {
|
|
22
|
+
if (value === void 0 || value === null) return void 0;
|
|
23
|
+
if (typeof value !== "string") throw new EdgeSummaryValidationError("not_text", "Edge summary must be text");
|
|
24
|
+
const words = value.trim().split(/\s+/u).filter(Boolean);
|
|
25
|
+
if (words.length === 0) return void 0;
|
|
26
|
+
if (words.length > edgeSummaryWordLimit) {
|
|
27
|
+
throw new EdgeSummaryValidationError("too_many_words", `Edge summary must contain at most ${edgeSummaryWordLimit} words`);
|
|
28
|
+
}
|
|
29
|
+
return words.join(" ");
|
|
30
|
+
}
|
|
31
|
+
function requireEdgeSummary(value) {
|
|
32
|
+
const summary = normalizeEdgeSummary(value);
|
|
33
|
+
if (!summary) throw new EdgeSummaryValidationError("required", "Edge summary is required");
|
|
34
|
+
return summary;
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
// src/server/agentClaims.ts
|
|
12
38
|
import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
13
39
|
|
|
@@ -17,7 +43,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
|
|
|
17
43
|
import { join as join6 } from "node:path";
|
|
18
44
|
|
|
19
45
|
// src/server/assetCore.ts
|
|
20
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
46
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
21
47
|
import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
22
48
|
import { spawnSync } from "node:child_process";
|
|
23
49
|
import { fileURLToPath } from "node:url";
|
|
@@ -590,21 +616,26 @@ function validateProject(project = defaultProject) {
|
|
|
590
616
|
|
|
591
617
|
// src/server/profileWriterLease.ts
|
|
592
618
|
import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
593
|
-
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
619
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
594
620
|
import { dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
|
|
595
621
|
|
|
596
622
|
// src/server/lineageProfiles.ts
|
|
597
623
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
598
624
|
import { createRequire } from "node:module";
|
|
599
625
|
import {
|
|
600
|
-
chmodSync,
|
|
626
|
+
chmodSync as chmodSync2,
|
|
627
|
+
closeSync,
|
|
601
628
|
constants as fsConstants,
|
|
602
629
|
copyFileSync as copyFileSync2,
|
|
603
630
|
existsSync as existsSync4,
|
|
631
|
+
fsyncSync,
|
|
604
632
|
linkSync,
|
|
633
|
+
lstatSync,
|
|
605
634
|
mkdirSync as mkdirSync3,
|
|
635
|
+
openSync,
|
|
606
636
|
readFileSync as readFileSync3,
|
|
607
637
|
realpathSync,
|
|
638
|
+
renameSync,
|
|
608
639
|
rmSync,
|
|
609
640
|
statSync as statSync3,
|
|
610
641
|
writeFileSync as writeFileSync2
|
|
@@ -617,6 +648,7 @@ var lineageProfileSchemaVersion = "lineage.profile.v1";
|
|
|
617
648
|
var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
|
|
618
649
|
var lineageProfileCloneReceiptSchemaVersion = "lineage.profile_clone_receipt.v1";
|
|
619
650
|
var lineageProfileAssetsCloneReceiptSchemaVersion = "lineage.profile_assets_clone_receipt.v1";
|
|
651
|
+
var lineageProfileRuntimeRepinReceiptSchemaVersion = "lineage.profile_runtime_repin_receipt.v1";
|
|
620
652
|
|
|
621
653
|
// src/server/lineageProfiles.ts
|
|
622
654
|
var require2 = createRequire(import.meta.url);
|
|
@@ -648,13 +680,13 @@ function profileManifestPath(selector) {
|
|
|
648
680
|
if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
|
|
649
681
|
return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
|
|
650
682
|
}
|
|
651
|
-
function requiredString(
|
|
652
|
-
const value =
|
|
683
|
+
function requiredString(record2, key) {
|
|
684
|
+
const value = record2[key];
|
|
653
685
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
|
|
654
686
|
return value.trim();
|
|
655
687
|
}
|
|
656
|
-
function optionalString(
|
|
657
|
-
const value =
|
|
688
|
+
function optionalString(record2, key) {
|
|
689
|
+
const value = record2[key];
|
|
658
690
|
if (value === void 0) return void 0;
|
|
659
691
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
|
|
660
692
|
return value.trim();
|
|
@@ -705,15 +737,15 @@ function resolveLineageProfile(selector) {
|
|
|
705
737
|
throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
706
738
|
}
|
|
707
739
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
|
|
708
|
-
const
|
|
709
|
-
const schemaVersion = requiredString(
|
|
740
|
+
const record2 = raw;
|
|
741
|
+
const schemaVersion = requiredString(record2, "schema_version");
|
|
710
742
|
if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
|
|
711
|
-
const profileId = requiredString(
|
|
743
|
+
const profileId = requiredString(record2, "profile_id");
|
|
712
744
|
if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
|
|
713
745
|
if (namedProfileId && namedProfileId !== profileId) {
|
|
714
746
|
throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
|
|
715
747
|
}
|
|
716
|
-
const expectedRaw =
|
|
748
|
+
const expectedRaw = record2.expected_runtime;
|
|
717
749
|
if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
|
|
718
750
|
throw new Error("Profile expected_runtime must be an object");
|
|
719
751
|
}
|
|
@@ -722,18 +754,18 @@ function resolveLineageProfile(selector) {
|
|
|
722
754
|
const expectedVersion = expected ? optionalString(expected, "version") : void 0;
|
|
723
755
|
const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
|
|
724
756
|
const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
|
|
725
|
-
const migrationsRaw =
|
|
757
|
+
const migrationsRaw = record2.required_schema_migrations;
|
|
726
758
|
if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
727
759
|
throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
|
|
728
760
|
}
|
|
729
|
-
const environment = validateEnvironment(requiredString(
|
|
761
|
+
const environment = validateEnvironment(requiredString(record2, "environment"));
|
|
730
762
|
const expectedChannel = validateChannel(expected?.channel);
|
|
731
763
|
if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
|
|
732
764
|
throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
|
|
733
765
|
}
|
|
734
766
|
const manifest = {
|
|
735
|
-
asset_root: resolveManifestPath(manifestPath, requiredString(
|
|
736
|
-
database_path: resolveManifestPath(manifestPath, requiredString(
|
|
767
|
+
asset_root: resolveManifestPath(manifestPath, requiredString(record2, "asset_root")),
|
|
768
|
+
database_path: resolveManifestPath(manifestPath, requiredString(record2, "database_path")),
|
|
737
769
|
environment,
|
|
738
770
|
...expected ? {
|
|
739
771
|
expected_runtime: {
|
|
@@ -747,7 +779,7 @@ function resolveLineageProfile(selector) {
|
|
|
747
779
|
profile_id: profileId,
|
|
748
780
|
...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
|
|
749
781
|
schema_version: lineageProfileSchemaVersion,
|
|
750
|
-
service_origin: validateServiceOrigin(requiredString(
|
|
782
|
+
service_origin: validateServiceOrigin(requiredString(record2, "service_origin"))
|
|
751
783
|
};
|
|
752
784
|
return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
|
|
753
785
|
}
|
|
@@ -761,6 +793,129 @@ function lineageProfileFingerprint(profile) {
|
|
|
761
793
|
service_origin: profile.service_origin
|
|
762
794
|
})).digest("hex");
|
|
763
795
|
}
|
|
796
|
+
function sha256(value) {
|
|
797
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
798
|
+
}
|
|
799
|
+
function assertOwnerOnlyPath(path, kind) {
|
|
800
|
+
const stats = lstatSync(path);
|
|
801
|
+
const expectedType = kind === "manifest" ? stats.isFile() : stats.isDirectory();
|
|
802
|
+
if (!expectedType || stats.isSymbolicLink()) throw new Error(`Profile ${kind} must be a nonsymlink ${kind === "manifest" ? "regular file" : "directory"}: ${path}`);
|
|
803
|
+
if ((stats.mode & 63) !== 0) throw new Error(`Profile ${kind} must be owner-only: ${path}`);
|
|
804
|
+
if (typeof process.getuid === "function" && stats.uid !== process.getuid()) throw new Error(`Profile ${kind} must be owned by the current user: ${path}`);
|
|
805
|
+
return stats;
|
|
806
|
+
}
|
|
807
|
+
function runtimeRepinInvariant(profile) {
|
|
808
|
+
return JSON.stringify({
|
|
809
|
+
asset_root: profile.asset_root,
|
|
810
|
+
database_path: profile.database_path,
|
|
811
|
+
environment: profile.environment,
|
|
812
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
813
|
+
profile_id: profile.profile_id,
|
|
814
|
+
required_schema_migrations: profile.required_schema_migrations || [],
|
|
815
|
+
schema_version: profile.schema_version,
|
|
816
|
+
service_origin: profile.service_origin
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function sameFileIdentity(left, right) {
|
|
820
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
821
|
+
}
|
|
822
|
+
function repinLineageDevelopmentProfileRuntime(selector, checkoutRoot, runtime, confirmWrite) {
|
|
823
|
+
if (!confirmWrite) throw new Error("Profile runtime repin requires --confirm-write");
|
|
824
|
+
if (runtime.channel !== "dev") throw new Error(`Profile runtime repin requires dev code, not ${runtime.channel}`);
|
|
825
|
+
if (!runtime.code?.verified) throw new Error("Profile runtime repin requires a verified checkout runtime");
|
|
826
|
+
if (runtime.code.origin !== "checkout") throw new Error(`Profile runtime repin requires checkout code, not ${runtime.code.origin}`);
|
|
827
|
+
if (!runtime.code.fingerprint || !/^[a-f0-9]{64}$/i.test(runtime.code.fingerprint)) {
|
|
828
|
+
throw new Error("Profile runtime repin requires a valid executing code fingerprint");
|
|
829
|
+
}
|
|
830
|
+
if (!checkoutRoot.trim()) throw new Error("Profile runtime repin requires --checkout-root");
|
|
831
|
+
const intendedRoot = realpathSync(resolve3(checkoutRoot));
|
|
832
|
+
if (!statSync3(intendedRoot).isDirectory()) throw new Error(`Profile runtime repin checkout root is not a directory: ${intendedRoot}`);
|
|
833
|
+
const executingRoot = realpathSync(runtime.code.root);
|
|
834
|
+
if (intendedRoot !== executingRoot) {
|
|
835
|
+
throw new Error(`Profile runtime repin checkout root ${intendedRoot} does not match executing code root ${executingRoot}`);
|
|
836
|
+
}
|
|
837
|
+
const profile = resolveLineageProfile(selector);
|
|
838
|
+
if (profile.environment !== "development") throw new Error(`Profile runtime repin requires a development profile, not ${profile.environment}`);
|
|
839
|
+
if (profile.expected_runtime?.channel !== "dev") throw new Error("Development profile runtime repin requires an existing dev channel pin");
|
|
840
|
+
if (profile.expected_runtime.code_origin !== "checkout") throw new Error("Development profile runtime repin requires an existing checkout origin pin");
|
|
841
|
+
if (!profile.expected_runtime.code_fingerprint) throw new Error("Development profile runtime repin requires an existing code fingerprint pin");
|
|
842
|
+
const manifestPath = profile.manifest_path;
|
|
843
|
+
assertOwnerOnlyPath(dirname3(manifestPath), "profile directory");
|
|
844
|
+
const beforeStats = assertOwnerOnlyPath(manifestPath, "manifest");
|
|
845
|
+
const beforeText = readFileSync3(manifestPath, "utf8");
|
|
846
|
+
const beforeHash = sha256(beforeText);
|
|
847
|
+
let raw;
|
|
848
|
+
try {
|
|
849
|
+
raw = JSON.parse(beforeText);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
852
|
+
}
|
|
853
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
|
|
854
|
+
const previousFingerprint = profile.expected_runtime.code_fingerprint;
|
|
855
|
+
const nextExpectedRuntime = {
|
|
856
|
+
channel: "dev",
|
|
857
|
+
code_fingerprint: runtime.code.fingerprint,
|
|
858
|
+
code_origin: "checkout",
|
|
859
|
+
...runtime.gitSha ? { git_sha: runtime.gitSha } : {},
|
|
860
|
+
version: runtime.version
|
|
861
|
+
};
|
|
862
|
+
const updated = { ...raw, expected_runtime: nextExpectedRuntime };
|
|
863
|
+
const afterText = `${JSON.stringify(updated, null, 2)}
|
|
864
|
+
`;
|
|
865
|
+
const afterHash = sha256(afterText);
|
|
866
|
+
const result = {
|
|
867
|
+
changed: beforeText !== afterText,
|
|
868
|
+
checkout_root: intendedRoot,
|
|
869
|
+
manifest_after_sha256: afterHash,
|
|
870
|
+
manifest_before_sha256: beforeHash,
|
|
871
|
+
manifest_path: manifestPath,
|
|
872
|
+
new_code_fingerprint: runtime.code.fingerprint,
|
|
873
|
+
previous_code_fingerprint: previousFingerprint,
|
|
874
|
+
profile_fingerprint: profile.profile_fingerprint,
|
|
875
|
+
profile_id: profile.profile_id,
|
|
876
|
+
schema_version: lineageProfileRuntimeRepinReceiptSchemaVersion
|
|
877
|
+
};
|
|
878
|
+
if (!result.changed) return result;
|
|
879
|
+
const temporaryPath = join4(dirname3(manifestPath), `.profile.runtime-repin-${randomUUID()}.tmp`);
|
|
880
|
+
let temporaryExists = false;
|
|
881
|
+
try {
|
|
882
|
+
writeFileSync2(temporaryPath, afterText, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
883
|
+
temporaryExists = true;
|
|
884
|
+
chmodSync2(temporaryPath, 384);
|
|
885
|
+
const temporaryFd = openSync(temporaryPath, "r");
|
|
886
|
+
try {
|
|
887
|
+
fsyncSync(temporaryFd);
|
|
888
|
+
} finally {
|
|
889
|
+
closeSync(temporaryFd);
|
|
890
|
+
}
|
|
891
|
+
const preparedReplacement = resolveLineageProfile(temporaryPath);
|
|
892
|
+
if (runtimeRepinInvariant(preparedReplacement) !== runtimeRepinInvariant(profile)) {
|
|
893
|
+
throw new Error("Profile runtime repin would change immutable profile routing identity");
|
|
894
|
+
}
|
|
895
|
+
const currentStats = assertOwnerOnlyPath(manifestPath, "manifest");
|
|
896
|
+
const currentText = readFileSync3(manifestPath, "utf8");
|
|
897
|
+
if (!sameFileIdentity(beforeStats, currentStats) || sha256(currentText) !== beforeHash) {
|
|
898
|
+
throw new Error("Profile manifest changed while runtime repin was being prepared; refusing replacement");
|
|
899
|
+
}
|
|
900
|
+
renameSync(temporaryPath, manifestPath);
|
|
901
|
+
temporaryExists = false;
|
|
902
|
+
chmodSync2(manifestPath, 384);
|
|
903
|
+
const directoryFd = openSync(dirname3(manifestPath), "r");
|
|
904
|
+
try {
|
|
905
|
+
fsyncSync(directoryFd);
|
|
906
|
+
} finally {
|
|
907
|
+
closeSync(directoryFd);
|
|
908
|
+
}
|
|
909
|
+
const replacement = resolveLineageProfile(manifestPath);
|
|
910
|
+
if (runtimeRepinInvariant(replacement) !== runtimeRepinInvariant(profile)) {
|
|
911
|
+
throw new Error("Profile runtime repin changed immutable profile routing identity");
|
|
912
|
+
}
|
|
913
|
+
if (readFileSync3(manifestPath, "utf8") !== afterText) throw new Error("Profile runtime repin replacement bytes do not match the prepared manifest");
|
|
914
|
+
return result;
|
|
915
|
+
} finally {
|
|
916
|
+
if (temporaryExists) rmSync(temporaryPath, { force: true });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
764
919
|
function assertProfileRuntimePin(profile, runtime) {
|
|
765
920
|
if (!profile.expected_runtime?.code_fingerprint || !profile.expected_runtime.code_origin) {
|
|
766
921
|
throw new Error(`Profile ${profile.profile_id} must pin expected_runtime.code_fingerprint and expected_runtime.code_origin before binding or writing`);
|
|
@@ -1051,6 +1206,7 @@ async function cloneLineageProfileDatabase(sourceDatabasePath, targetSelector, r
|
|
|
1051
1206
|
const source = new DatabaseSync(sourcePath, { readOnly: true });
|
|
1052
1207
|
try {
|
|
1053
1208
|
const pagesCopied = await backup(source, temporaryPath);
|
|
1209
|
+
chmodSync2(temporaryPath, 384);
|
|
1054
1210
|
const cloned = new DatabaseSync(temporaryPath);
|
|
1055
1211
|
let identity2;
|
|
1056
1212
|
try {
|
|
@@ -1188,7 +1344,7 @@ function cloneLineageProfileAssets(sourceAssetRoot, targetSelector, runtime, con
|
|
|
1188
1344
|
const destinationPath = join4(targetRoot, relativePath);
|
|
1189
1345
|
mkdirSync3(dirname3(destinationPath), { recursive: true, mode: 448 });
|
|
1190
1346
|
copyFileSync2(sourcePath, destinationPath, fsConstants.COPYFILE_EXCL);
|
|
1191
|
-
|
|
1347
|
+
chmodSync2(destinationPath, 384);
|
|
1192
1348
|
const sourceHash = fileSha2562(sourcePath);
|
|
1193
1349
|
const destinationHash = fileSha2562(destinationPath);
|
|
1194
1350
|
if (sourceHash !== destinationHash) throw new Error("A source asset changed or failed checksum verification during clone");
|
|
@@ -1198,7 +1354,7 @@ function cloneLineageProfileAssets(sourceAssetRoot, targetSelector, runtime, con
|
|
|
1198
1354
|
treeHash.update(destinationHash);
|
|
1199
1355
|
treeHash.update("\0");
|
|
1200
1356
|
}
|
|
1201
|
-
|
|
1357
|
+
chmodSync2(targetRoot, 448);
|
|
1202
1358
|
const receiptPath = join4(targetRoot, ".lineage-profile-assets.json");
|
|
1203
1359
|
const result = {
|
|
1204
1360
|
asset_root: targetRoot,
|
|
@@ -1251,7 +1407,7 @@ function errorCode(error) {
|
|
|
1251
1407
|
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
1252
1408
|
}
|
|
1253
1409
|
function readOwner(lockPath) {
|
|
1254
|
-
const stat =
|
|
1410
|
+
const stat = lstatSync2(lockPath);
|
|
1255
1411
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
1256
1412
|
throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
|
|
1257
1413
|
}
|
|
@@ -1300,7 +1456,7 @@ function reclaimDeadOwner(lockPath, owner) {
|
|
|
1300
1456
|
const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
|
|
1301
1457
|
const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
|
|
1302
1458
|
try {
|
|
1303
|
-
|
|
1459
|
+
renameSync2(lockPath, tombstone);
|
|
1304
1460
|
} catch (error) {
|
|
1305
1461
|
if (errorCode(error) === "ENOENT") return;
|
|
1306
1462
|
throw error;
|
|
@@ -1492,6 +1648,10 @@ function lineageDb() {
|
|
|
1492
1648
|
child_asset_id text not null references assets(id),
|
|
1493
1649
|
relation_type text not null check (relation_type in ('derived_from')),
|
|
1494
1650
|
created_at text not null,
|
|
1651
|
+
summary text,
|
|
1652
|
+
summary_created_by text check (summary_created_by in ('human', 'agent', 'system')),
|
|
1653
|
+
summary_updated_by text check (summary_updated_by in ('human', 'agent', 'system')),
|
|
1654
|
+
summary_updated_at text,
|
|
1495
1655
|
unique (project_id, parent_asset_id, child_asset_id, relation_type)
|
|
1496
1656
|
);
|
|
1497
1657
|
create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
|
|
@@ -1775,6 +1935,7 @@ function lineageDb() {
|
|
|
1775
1935
|
imported_asset_id text not null references assets(id),
|
|
1776
1936
|
parent_asset_id text not null references assets(id),
|
|
1777
1937
|
imported_at text not null,
|
|
1938
|
+
edge_summary text,
|
|
1778
1939
|
unique(job_id, output_index),
|
|
1779
1940
|
unique(job_id, file_path)
|
|
1780
1941
|
);
|
|
@@ -1863,6 +2024,11 @@ function lineageDb() {
|
|
|
1863
2024
|
);
|
|
1864
2025
|
create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
|
|
1865
2026
|
`);
|
|
2027
|
+
ensureColumn(database, "asset_edges", "summary", "text");
|
|
2028
|
+
ensureColumn(database, "asset_edges", "summary_created_by", "text check (summary_created_by in ('human', 'agent', 'system'))");
|
|
2029
|
+
ensureColumn(database, "asset_edges", "summary_updated_by", "text check (summary_updated_by in ('human', 'agent', 'system'))");
|
|
2030
|
+
ensureColumn(database, "asset_edges", "summary_updated_at", "text");
|
|
2031
|
+
ensureColumn(database, "generation_job_outputs", "edge_summary", "text");
|
|
1866
2032
|
migrateAssetSelections(database);
|
|
1867
2033
|
dropLegacyAssetSelectionRootUnique(database);
|
|
1868
2034
|
ensureColumn(database, "asset_selections", "notes", "text");
|
|
@@ -2988,6 +3154,14 @@ function cancelLineageTask(project, fields) {
|
|
|
2988
3154
|
database.close();
|
|
2989
3155
|
}
|
|
2990
3156
|
}
|
|
3157
|
+
function cancelLineageIterateTasksForAssets(project, fields) {
|
|
3158
|
+
const targetIds = fields.assetIds ? new Set(fields.assetIds) : void 0;
|
|
3159
|
+
return listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "iterate").filter((task) => !targetIds || targetIds.has(task.target_asset_id)).map((task) => cancelLineageTask(project, {
|
|
3160
|
+
actor: fields.actor,
|
|
3161
|
+
confirmWrite: fields.confirmWrite,
|
|
3162
|
+
taskId: task.id
|
|
3163
|
+
}));
|
|
3164
|
+
}
|
|
2991
3165
|
function resolveLineageTask(project, fields) {
|
|
2992
3166
|
const normalizedProject = normalizeProject(project);
|
|
2993
3167
|
const actor = normalizeActor(fields.actor, "Resolve actor");
|
|
@@ -3353,6 +3527,23 @@ function rerollRequestId(project, root, node, timestamp) {
|
|
|
3353
3527
|
function rowString(value) {
|
|
3354
3528
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3355
3529
|
}
|
|
3530
|
+
function lineageEdgeFromRow(row) {
|
|
3531
|
+
const summary = rowString(row.summary);
|
|
3532
|
+
const summaryCreatedBy = rowString(row.summary_created_by);
|
|
3533
|
+
const summaryUpdatedBy = rowString(row.summary_updated_by);
|
|
3534
|
+
const summaryUpdatedAt = rowString(row.summary_updated_at);
|
|
3535
|
+
return {
|
|
3536
|
+
id: String(row.id),
|
|
3537
|
+
parent_asset_id: String(row.parent_asset_id),
|
|
3538
|
+
child_asset_id: String(row.child_asset_id),
|
|
3539
|
+
relation_type: row.relation_type,
|
|
3540
|
+
created_at: String(row.created_at),
|
|
3541
|
+
...summary ? { summary } : {},
|
|
3542
|
+
...summaryCreatedBy ? { summary_created_by: summaryCreatedBy } : {},
|
|
3543
|
+
...summaryUpdatedBy ? { summary_updated_by: summaryUpdatedBy } : {},
|
|
3544
|
+
...summaryUpdatedAt ? { summary_updated_at: summaryUpdatedAt } : {}
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3356
3547
|
function rerollRequestFrom(row) {
|
|
3357
3548
|
return {
|
|
3358
3549
|
id: String(row.id),
|
|
@@ -3452,6 +3643,9 @@ function localPreviewUrl(project, localPath) {
|
|
|
3452
3643
|
return `/api/assets/local-preview?${params.toString()}`;
|
|
3453
3644
|
}
|
|
3454
3645
|
function linkLineageAssets(project, fields) {
|
|
3646
|
+
const summary = normalizeEdgeSummary(fields.summary);
|
|
3647
|
+
if (summary && !fields.summaryActor) throw new LineageError("Lineage edge summary requires an explicit author");
|
|
3648
|
+
if (!summary && fields.summaryActor) throw new LineageError("Lineage edge summary author requires a summary");
|
|
3455
3649
|
const database = lineageDb();
|
|
3456
3650
|
requireAsset2(database, project, fields.parentAssetId);
|
|
3457
3651
|
requireAsset2(database, project, fields.childAssetId);
|
|
@@ -3470,22 +3664,66 @@ function linkLineageAssets(project, fields) {
|
|
|
3470
3664
|
database.close();
|
|
3471
3665
|
throw error;
|
|
3472
3666
|
}
|
|
3667
|
+
const createdAt = nowIso();
|
|
3473
3668
|
const edge = {
|
|
3474
3669
|
id: edgeId(project, fields.parentAssetId, fields.childAssetId),
|
|
3475
3670
|
parent_asset_id: fields.parentAssetId,
|
|
3476
3671
|
child_asset_id: fields.childAssetId,
|
|
3477
3672
|
relation_type: "derived_from",
|
|
3478
|
-
created_at:
|
|
3673
|
+
created_at: createdAt,
|
|
3674
|
+
...summary ? {
|
|
3675
|
+
summary,
|
|
3676
|
+
summary_created_by: fields.summaryActor,
|
|
3677
|
+
summary_updated_by: fields.summaryActor,
|
|
3678
|
+
summary_updated_at: createdAt
|
|
3679
|
+
} : {}
|
|
3479
3680
|
};
|
|
3480
3681
|
if (!fields.confirmWrite) {
|
|
3481
3682
|
database.close();
|
|
3482
3683
|
return { ok: true, dryRun: true, edge };
|
|
3483
3684
|
}
|
|
3484
|
-
database.prepare(`
|
|
3485
|
-
insert into asset_edges (
|
|
3486
|
-
|
|
3685
|
+
const inserted = database.prepare(`
|
|
3686
|
+
insert into asset_edges (
|
|
3687
|
+
id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
|
|
3688
|
+
summary, summary_created_by, summary_updated_by, summary_updated_at
|
|
3689
|
+
)
|
|
3690
|
+
values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
|
|
3487
3691
|
on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
|
|
3488
|
-
`).run(
|
|
3692
|
+
`).run(
|
|
3693
|
+
edge.id,
|
|
3694
|
+
project,
|
|
3695
|
+
edge.parent_asset_id,
|
|
3696
|
+
edge.child_asset_id,
|
|
3697
|
+
edge.created_at,
|
|
3698
|
+
edge.summary || null,
|
|
3699
|
+
edge.summary_created_by || null,
|
|
3700
|
+
edge.summary_updated_by || null,
|
|
3701
|
+
edge.summary_updated_at || null
|
|
3702
|
+
);
|
|
3703
|
+
if (inserted.changes === 0) {
|
|
3704
|
+
const existingRow = database.prepare(`
|
|
3705
|
+
select id, parent_asset_id, child_asset_id, relation_type, created_at,
|
|
3706
|
+
summary, summary_created_by, summary_updated_by, summary_updated_at
|
|
3707
|
+
from asset_edges
|
|
3708
|
+
where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
|
|
3709
|
+
`).get(project, edge.parent_asset_id, edge.child_asset_id);
|
|
3710
|
+
if (!existingRow) {
|
|
3711
|
+
database.close();
|
|
3712
|
+
throw new LineageError("Lineage edge conflict could not be resolved", 409);
|
|
3713
|
+
}
|
|
3714
|
+
const existingEdge = lineageEdgeFromRow(existingRow);
|
|
3715
|
+
if (summary && (existingEdge.summary !== summary || existingEdge.summary_created_by !== fields.summaryActor || existingEdge.summary_updated_by !== fields.summaryActor || !existingEdge.summary_updated_at)) {
|
|
3716
|
+
database.close();
|
|
3717
|
+
throw new LineageError("Lineage edge already exists with a different summary or provenance", 409);
|
|
3718
|
+
}
|
|
3719
|
+
database.close();
|
|
3720
|
+
return {
|
|
3721
|
+
ok: true,
|
|
3722
|
+
idempotent: true,
|
|
3723
|
+
message: `Already linked ${existingEdge.child_asset_id} from ${existingEdge.parent_asset_id}`,
|
|
3724
|
+
edge: existingEdge
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3489
3727
|
database.close();
|
|
3490
3728
|
return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
|
|
3491
3729
|
}
|
|
@@ -3497,7 +3735,13 @@ function descendants(database, project, root) {
|
|
|
3497
3735
|
const parent = queue.shift();
|
|
3498
3736
|
if (seen.has(parent)) continue;
|
|
3499
3737
|
seen.add(parent);
|
|
3500
|
-
const rows = database.prepare(
|
|
3738
|
+
const rows = database.prepare(`
|
|
3739
|
+
select id, parent_asset_id, child_asset_id, relation_type, created_at,
|
|
3740
|
+
summary, summary_created_by, summary_updated_by, summary_updated_at
|
|
3741
|
+
from asset_edges
|
|
3742
|
+
where project_id = ? and parent_asset_id = ?
|
|
3743
|
+
order by created_at
|
|
3744
|
+
`).all(project, parent).map(lineageEdgeFromRow);
|
|
3501
3745
|
edges.push(...rows);
|
|
3502
3746
|
queue.push(...rows.map((row) => row.child_asset_id));
|
|
3503
3747
|
}
|
|
@@ -3883,7 +4127,7 @@ function lineageCommand(command, project, rootAssetId) {
|
|
|
3883
4127
|
return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
|
|
3884
4128
|
}
|
|
3885
4129
|
function linkChildCommand(project, rootAssetId) {
|
|
3886
|
-
return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
4130
|
+
return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --summary "<one-or-two-words>" --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
3887
4131
|
}
|
|
3888
4132
|
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
3889
4133
|
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
@@ -3932,6 +4176,7 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
3932
4176
|
};
|
|
3933
4177
|
}
|
|
3934
4178
|
function linkSelectedLineageChild(project, fields) {
|
|
4179
|
+
const summary = fields.summaryActor ? requireEdgeSummary(fields.summary) : fields.summary;
|
|
3935
4180
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
3936
4181
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
3937
4182
|
const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
|
|
@@ -3960,7 +4205,9 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
3960
4205
|
childAssetId: fields.childAssetId,
|
|
3961
4206
|
confirmWrite: fields.confirmWrite,
|
|
3962
4207
|
claimToken: fields.claimToken,
|
|
3963
|
-
parentAssetId: next.next_asset.asset_id
|
|
4208
|
+
parentAssetId: next.next_asset.asset_id,
|
|
4209
|
+
summary,
|
|
4210
|
+
summaryActor: fields.summaryActor
|
|
3964
4211
|
});
|
|
3965
4212
|
return {
|
|
3966
4213
|
...result,
|
|
@@ -3980,7 +4227,141 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
3980
4227
|
import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
3981
4228
|
import { relative as relative3, resolve as resolve5 } from "node:path";
|
|
3982
4229
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3983
|
-
|
|
4230
|
+
|
|
4231
|
+
// src/shared/generationOutputManifest.ts
|
|
4232
|
+
var generationOutputManifestSchemaVersion = "lineage.generation_output_manifest.v1";
|
|
4233
|
+
var GenerationOutputManifestError = class extends Error {
|
|
4234
|
+
constructor(message) {
|
|
4235
|
+
super(message);
|
|
4236
|
+
this.name = "GenerationOutputManifestError";
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4239
|
+
function record(value, label) {
|
|
4240
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4241
|
+
throw new GenerationOutputManifestError(`${label} must be an object`);
|
|
4242
|
+
}
|
|
4243
|
+
return value;
|
|
4244
|
+
}
|
|
4245
|
+
function exactKeys(value, allowed, label) {
|
|
4246
|
+
const allowedKeys = new Set(allowed);
|
|
4247
|
+
const unsupported = Object.keys(value).filter((key) => !allowedKeys.has(key)).sort();
|
|
4248
|
+
if (unsupported.length > 0) {
|
|
4249
|
+
throw new GenerationOutputManifestError(`${label} contains unsupported field: ${unsupported[0]}`);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
function nonEmptyText(value, label) {
|
|
4253
|
+
if (typeof value !== "string" || !value.trim()) throw new GenerationOutputManifestError(`${label} is required`);
|
|
4254
|
+
return value.trim();
|
|
4255
|
+
}
|
|
4256
|
+
function expectedGenerationOutputParents(job) {
|
|
4257
|
+
if (!Number.isInteger(job.expected_output_count) || job.expected_output_count <= 0) {
|
|
4258
|
+
throw new GenerationOutputManifestError("Generation job expected output count must be a positive integer");
|
|
4259
|
+
}
|
|
4260
|
+
const parents = job.inputs.filter((input) => input.role === "lineage_next_base").sort((left, right) => left.position - right.position);
|
|
4261
|
+
if (parents.length === 0) throw new GenerationOutputManifestError("Generation job has no lineage_next_base input");
|
|
4262
|
+
const parentIds = /* @__PURE__ */ new Set();
|
|
4263
|
+
const parentPositions = /* @__PURE__ */ new Set();
|
|
4264
|
+
for (const parent of parents) {
|
|
4265
|
+
if (typeof parent.asset_id !== "string" || !parent.asset_id || parent.asset_id.trim() !== parent.asset_id) {
|
|
4266
|
+
throw new GenerationOutputManifestError("Generation job has invalid lineage parent asset id");
|
|
4267
|
+
}
|
|
4268
|
+
if (!Number.isInteger(parent.position) || parent.position < 0 || parentPositions.has(parent.position)) {
|
|
4269
|
+
throw new GenerationOutputManifestError("Generation job has invalid lineage parent positions");
|
|
4270
|
+
}
|
|
4271
|
+
if (parentIds.has(parent.asset_id)) throw new GenerationOutputManifestError(`Generation job has duplicate lineage parent: ${parent.asset_id}`);
|
|
4272
|
+
parentIds.add(parent.asset_id);
|
|
4273
|
+
parentPositions.add(parent.position);
|
|
4274
|
+
}
|
|
4275
|
+
if (job.expected_output_count % parents.length !== 0) {
|
|
4276
|
+
throw new GenerationOutputManifestError("Generation job has invalid parent mapping");
|
|
4277
|
+
}
|
|
4278
|
+
const outputsPerParent = job.expected_output_count / parents.length;
|
|
4279
|
+
return Array.from({ length: job.expected_output_count }, (_value, outputIndex) => {
|
|
4280
|
+
return parents[Math.floor(outputIndex / outputsPerParent)].asset_id;
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
function createGenerationOutputManifestDraft(job) {
|
|
4284
|
+
return {
|
|
4285
|
+
schema_version: generationOutputManifestSchemaVersion,
|
|
4286
|
+
job_id: job.id,
|
|
4287
|
+
outputs: expectedGenerationOutputParents(job).map((parentAssetId, outputIndex) => ({
|
|
4288
|
+
output_index: outputIndex,
|
|
4289
|
+
file_path: "",
|
|
4290
|
+
parent_asset_id: parentAssetId,
|
|
4291
|
+
edge_summary: ""
|
|
4292
|
+
}))
|
|
4293
|
+
};
|
|
4294
|
+
}
|
|
4295
|
+
function parseGenerationOutputManifest(value, job, options) {
|
|
4296
|
+
if (!options || typeof options.resolveFilePath !== "function") {
|
|
4297
|
+
throw new GenerationOutputManifestError("Generation output manifest requires a file-path resolver");
|
|
4298
|
+
}
|
|
4299
|
+
const manifest = record(value, "Generation output manifest");
|
|
4300
|
+
exactKeys(manifest, ["schema_version", "job_id", "outputs"], "Generation output manifest");
|
|
4301
|
+
if (manifest.schema_version !== generationOutputManifestSchemaVersion) {
|
|
4302
|
+
throw new GenerationOutputManifestError(`Generation output manifest schema_version must be ${generationOutputManifestSchemaVersion}`);
|
|
4303
|
+
}
|
|
4304
|
+
if (manifest.job_id !== job.id) throw new GenerationOutputManifestError(`Generation output manifest job_id must be ${job.id}`);
|
|
4305
|
+
if (!Array.isArray(manifest.outputs)) throw new GenerationOutputManifestError("Generation output manifest outputs must be an array");
|
|
4306
|
+
const expectedParents = expectedGenerationOutputParents(job);
|
|
4307
|
+
if (manifest.outputs.length !== expectedParents.length) {
|
|
4308
|
+
throw new GenerationOutputManifestError(`Generation output manifest requires ${expectedParents.length} outputs, received ${manifest.outputs.length}`);
|
|
4309
|
+
}
|
|
4310
|
+
const seenIndexes = /* @__PURE__ */ new Set();
|
|
4311
|
+
const outputs = manifest.outputs.map((value2, position) => {
|
|
4312
|
+
const output = record(value2, `Generation output at position ${position}`);
|
|
4313
|
+
exactKeys(output, ["output_index", "file_path", "parent_asset_id", "edge_summary"], `Generation output at position ${position}`);
|
|
4314
|
+
const outputIndex = output.output_index;
|
|
4315
|
+
if (!Number.isInteger(outputIndex) || Number(outputIndex) < 0) {
|
|
4316
|
+
throw new GenerationOutputManifestError(`Generation output at position ${position} requires a non-negative integer output_index`);
|
|
4317
|
+
}
|
|
4318
|
+
const normalizedIndex = Number(outputIndex);
|
|
4319
|
+
if (seenIndexes.has(normalizedIndex)) throw new GenerationOutputManifestError(`Duplicate generation output_index: ${normalizedIndex}`);
|
|
4320
|
+
seenIndexes.add(normalizedIndex);
|
|
4321
|
+
const expectedParent = expectedParents[normalizedIndex];
|
|
4322
|
+
if (!expectedParent) throw new GenerationOutputManifestError(`Unknown generation output_index: ${normalizedIndex}`);
|
|
4323
|
+
const filePath = nonEmptyText(output.file_path, `Generation output ${normalizedIndex} file_path`);
|
|
4324
|
+
const parentAssetId = nonEmptyText(output.parent_asset_id, `Generation output ${normalizedIndex} parent_asset_id`);
|
|
4325
|
+
if (parentAssetId !== expectedParent) {
|
|
4326
|
+
throw new GenerationOutputManifestError(`Generation output ${normalizedIndex} must use parent_asset_id ${expectedParent}`);
|
|
4327
|
+
}
|
|
4328
|
+
let edgeSummary;
|
|
4329
|
+
try {
|
|
4330
|
+
edgeSummary = requireEdgeSummary(output.edge_summary);
|
|
4331
|
+
} catch (error) {
|
|
4332
|
+
if (error instanceof EdgeSummaryValidationError) {
|
|
4333
|
+
throw new GenerationOutputManifestError(`Generation output ${normalizedIndex}: ${error.message}`);
|
|
4334
|
+
}
|
|
4335
|
+
throw error;
|
|
4336
|
+
}
|
|
4337
|
+
return {
|
|
4338
|
+
output_index: normalizedIndex,
|
|
4339
|
+
file_path: filePath,
|
|
4340
|
+
parent_asset_id: parentAssetId,
|
|
4341
|
+
edge_summary: edgeSummary
|
|
4342
|
+
};
|
|
4343
|
+
});
|
|
4344
|
+
for (const outputIndex of expectedParents.keys()) {
|
|
4345
|
+
if (!seenIndexes.has(outputIndex)) throw new GenerationOutputManifestError(`Missing generation output_index: ${outputIndex}`);
|
|
4346
|
+
}
|
|
4347
|
+
outputs.sort((left, right) => left.output_index - right.output_index);
|
|
4348
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
4349
|
+
const resolvedOutputs = outputs.map((output) => {
|
|
4350
|
+
const filePath = nonEmptyText(options.resolveFilePath(output.file_path), `Generation output ${output.output_index} resolved file_path`);
|
|
4351
|
+
if (seenPaths.has(filePath)) throw new GenerationOutputManifestError(`Duplicate generation output file_path: ${filePath}`);
|
|
4352
|
+
seenPaths.add(filePath);
|
|
4353
|
+
return { ...output, file_path: filePath };
|
|
4354
|
+
});
|
|
4355
|
+
return {
|
|
4356
|
+
schema_version: generationOutputManifestSchemaVersion,
|
|
4357
|
+
job_id: job.id,
|
|
4358
|
+
outputs: resolvedOutputs
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
// src/server/generationReceipts.ts
|
|
4363
|
+
var legacyAdapterVersion = "generation-receipts-v1";
|
|
4364
|
+
var manifestAdapterVersion = "generation-receipts-v2";
|
|
3984
4365
|
var provider = "codex-handoff";
|
|
3985
4366
|
var GenerationReceiptError = class extends Error {
|
|
3986
4367
|
constructor(message, status = 400) {
|
|
@@ -3999,6 +4380,70 @@ function parseJson(value, fallback) {
|
|
|
3999
4380
|
if (!value) return fallback;
|
|
4000
4381
|
return JSON.parse(value);
|
|
4001
4382
|
}
|
|
4383
|
+
function positiveInteger(value) {
|
|
4384
|
+
return Number.isInteger(value) && Number(value) > 0;
|
|
4385
|
+
}
|
|
4386
|
+
function resolveLineageSelection(project) {
|
|
4387
|
+
const rootAssetId = activeLineageWorkspaceRoot(project);
|
|
4388
|
+
if (!rootAssetId) throw new GenerationReceiptError("No active lineage workspace for generation planning");
|
|
4389
|
+
const next = getLineageNextAsset(project, rootAssetId);
|
|
4390
|
+
if (!next.next_asset) throw new GenerationReceiptError(`No clear lineage next asset: ${next.reason}`);
|
|
4391
|
+
if (next.strategy !== "selected") throw new GenerationReceiptError("Generation v1 requires an explicit selected lineage next base");
|
|
4392
|
+
if (next.selection_mode !== "multiple" && next.selection?.asset_id !== next.next_asset.asset_id) throw new GenerationReceiptError("Generation v1 requires one explicit selected lineage next base");
|
|
4393
|
+
return next;
|
|
4394
|
+
}
|
|
4395
|
+
function selectedParents(next) {
|
|
4396
|
+
const parents = next.next_assets.length > 0 ? next.next_assets : next.next_asset ? [next.next_asset] : [];
|
|
4397
|
+
if (parents.length === 0) throw new GenerationReceiptError("Missing lineage next base");
|
|
4398
|
+
return parents;
|
|
4399
|
+
}
|
|
4400
|
+
function parentMappings(next, perBaseCount) {
|
|
4401
|
+
return selectedParents(next).map((parent, parentIndex) => ({
|
|
4402
|
+
parent,
|
|
4403
|
+
output_indexes: Array.from({ length: perBaseCount }, (_value, index) => parentIndex * perBaseCount + index)
|
|
4404
|
+
}));
|
|
4405
|
+
}
|
|
4406
|
+
function buildHandoff(project, id, prompt, count, perBaseCount, next, inputs) {
|
|
4407
|
+
const parent = next.next_asset;
|
|
4408
|
+
if (!parent) throw new GenerationReceiptError("Missing lineage next base");
|
|
4409
|
+
const parents = parentMappings(next, perBaseCount);
|
|
4410
|
+
const outputManifest = createGenerationOutputManifestDraft({ id, expected_output_count: count, inputs });
|
|
4411
|
+
const importCommand = `${lineagePublicPackageCommand()} generate image import --project ${quote(project)} --job-id ${quote(id)} --manifest ${quote(".asset-scratch/generation-output-manifest.json")} --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
4412
|
+
return {
|
|
4413
|
+
schema_version: "lineage.generation_handoff.v2",
|
|
4414
|
+
provider,
|
|
4415
|
+
project,
|
|
4416
|
+
job_id: id,
|
|
4417
|
+
prompt,
|
|
4418
|
+
expected_output_count: count,
|
|
4419
|
+
per_base_count: next.selection_mode === "multiple" ? perBaseCount : void 0,
|
|
4420
|
+
lineage: {
|
|
4421
|
+
root_asset_id: next.root_asset_id,
|
|
4422
|
+
parent_asset_id: parent.asset_id,
|
|
4423
|
+
selection_strategy: next.strategy,
|
|
4424
|
+
parent_title: parent.title,
|
|
4425
|
+
parent_local_path: parent.local_path,
|
|
4426
|
+
parent_s3_key: parent.s3_key,
|
|
4427
|
+
parents: parents.length > 1 ? parents.map((mapping) => ({
|
|
4428
|
+
parent_asset_id: mapping.parent.asset_id,
|
|
4429
|
+
parent_title: mapping.parent.title,
|
|
4430
|
+
parent_local_path: mapping.parent.local_path,
|
|
4431
|
+
parent_s3_key: mapping.parent.s3_key,
|
|
4432
|
+
output_indexes: mapping.output_indexes
|
|
4433
|
+
})) : void 0
|
|
4434
|
+
},
|
|
4435
|
+
instructions: [
|
|
4436
|
+
"Use Codex image generation outside Lineage server code.",
|
|
4437
|
+
"Write generated output files under .asset-scratch before import.",
|
|
4438
|
+
"Do not call live provider APIs from the CLI or server.",
|
|
4439
|
+
"Fill every output_manifest entry with its generated file path and a one- or two-word edge summary.",
|
|
4440
|
+
"Import the completed manifest with --confirm-write to persist every lineage child."
|
|
4441
|
+
],
|
|
4442
|
+
import_command: importCommand,
|
|
4443
|
+
output_manifest: outputManifest,
|
|
4444
|
+
guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4002
4447
|
function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
4003
4448
|
const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
|
|
4004
4449
|
return {
|
|
@@ -4027,6 +4472,19 @@ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
|
|
|
4027
4472
|
guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
|
|
4028
4473
|
};
|
|
4029
4474
|
}
|
|
4475
|
+
function inputsFrom(jobIdValue, project, next) {
|
|
4476
|
+
return selectedParents(next).map((parent, position) => ({
|
|
4477
|
+
id: `${jobIdValue}:input:${position}`,
|
|
4478
|
+
job_id: jobIdValue,
|
|
4479
|
+
project_id: project,
|
|
4480
|
+
asset_id: parent.asset_id,
|
|
4481
|
+
root_asset_id: next.root_asset_id,
|
|
4482
|
+
role: "lineage_next_base",
|
|
4483
|
+
position,
|
|
4484
|
+
selection_strategy: next.strategy,
|
|
4485
|
+
selection_snapshot: next
|
|
4486
|
+
}));
|
|
4487
|
+
}
|
|
4030
4488
|
function receiptFrom(row) {
|
|
4031
4489
|
return {
|
|
4032
4490
|
id: String(row.id),
|
|
@@ -4039,6 +4497,7 @@ function receiptFrom(row) {
|
|
|
4039
4497
|
};
|
|
4040
4498
|
}
|
|
4041
4499
|
function outputFrom(row) {
|
|
4500
|
+
const edgeSummary = typeof row.edge_summary === "string" && row.edge_summary.length > 0 ? row.edge_summary : void 0;
|
|
4042
4501
|
return {
|
|
4043
4502
|
id: String(row.id),
|
|
4044
4503
|
job_id: String(row.job_id),
|
|
@@ -4050,7 +4509,8 @@ function outputFrom(row) {
|
|
|
4050
4509
|
content_type: String(row.content_type),
|
|
4051
4510
|
imported_asset_id: String(row.imported_asset_id),
|
|
4052
4511
|
parent_asset_id: String(row.parent_asset_id),
|
|
4053
|
-
imported_at: String(row.imported_at)
|
|
4512
|
+
imported_at: String(row.imported_at),
|
|
4513
|
+
...edgeSummary ? { edge_summary: edgeSummary } : {}
|
|
4054
4514
|
};
|
|
4055
4515
|
}
|
|
4056
4516
|
function loadGenerationJob(database, project, id) {
|
|
@@ -4096,6 +4556,72 @@ function insertReceipt(database, id, type, command, payload) {
|
|
|
4096
4556
|
values (?, ?, ?, 'ok', ?, ?, ?)
|
|
4097
4557
|
`).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
|
|
4098
4558
|
}
|
|
4559
|
+
function planImageGeneration(project = defaultProject, fields) {
|
|
4560
|
+
const prompt = fields.prompt.trim();
|
|
4561
|
+
if (!prompt) throw new GenerationReceiptError("Missing --prompt");
|
|
4562
|
+
if (!fields.fromLineageSelection) throw new GenerationReceiptError("Generation v1 requires --from-lineage-selection");
|
|
4563
|
+
const next = resolveLineageSelection(project);
|
|
4564
|
+
const parentCount = selectedParents(next).length;
|
|
4565
|
+
if (parentCount > 1 && !positiveInteger(fields.perBaseCount)) throw new GenerationReceiptError("Multi-parent generation requires --per-base-count");
|
|
4566
|
+
const perBaseCount = parentCount > 1 ? Number(fields.perBaseCount) : Number(fields.count ?? fields.perBaseCount);
|
|
4567
|
+
const count = parentCount * perBaseCount;
|
|
4568
|
+
if (!positiveInteger(perBaseCount)) throw new GenerationReceiptError("Generation count must be a positive integer");
|
|
4569
|
+
if (fields.count !== void 0 && fields.count !== count) throw new GenerationReceiptError(`Generation count mismatch: expected ${count} from selected bases, received ${fields.count}`);
|
|
4570
|
+
const id = jobId();
|
|
4571
|
+
const inputs = inputsFrom(id, project, next);
|
|
4572
|
+
const handoff = buildHandoff(project, id, prompt, count, perBaseCount, next, inputs);
|
|
4573
|
+
const mappings = parentMappings(next, perBaseCount).map((mapping) => ({ parent_asset_id: mapping.parent.asset_id, output_indexes: mapping.output_indexes }));
|
|
4574
|
+
const timestamp = nowIso();
|
|
4575
|
+
const preview2 = {
|
|
4576
|
+
id,
|
|
4577
|
+
project_id: project,
|
|
4578
|
+
provider,
|
|
4579
|
+
adapter_version: manifestAdapterVersion,
|
|
4580
|
+
source_mode: "lineage_selection",
|
|
4581
|
+
root_asset_id: next.root_asset_id,
|
|
4582
|
+
prompt,
|
|
4583
|
+
expected_output_count: count,
|
|
4584
|
+
status: "planned",
|
|
4585
|
+
output_dir: ".asset-scratch",
|
|
4586
|
+
handoff,
|
|
4587
|
+
created_at: timestamp,
|
|
4588
|
+
updated_at: timestamp,
|
|
4589
|
+
inputs,
|
|
4590
|
+
outputs: [],
|
|
4591
|
+
receipts: [{
|
|
4592
|
+
id: `${id}:receipt:plan:preview`,
|
|
4593
|
+
job_id: id,
|
|
4594
|
+
receipt_type: "plan",
|
|
4595
|
+
status: "ok",
|
|
4596
|
+
command: "generate image plan",
|
|
4597
|
+
payload: { prompt, expected_output_count: count, per_base_count: parentCount > 1 ? perBaseCount : void 0, lineage: handoff.lineage, parent_mappings: mappings },
|
|
4598
|
+
created_at: timestamp
|
|
4599
|
+
}]
|
|
4600
|
+
};
|
|
4601
|
+
if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview2 };
|
|
4602
|
+
const database = lineageDb();
|
|
4603
|
+
try {
|
|
4604
|
+
database.exec("BEGIN IMMEDIATE");
|
|
4605
|
+
try {
|
|
4606
|
+
database.prepare(`
|
|
4607
|
+
insert into generation_jobs (
|
|
4608
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
4609
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at
|
|
4610
|
+
) values (?, ?, ?, ?, 'lineage_selection', ?, ?, ?, 'planned', ?, ?, ?, ?)
|
|
4611
|
+
`).run(id, project, provider, manifestAdapterVersion, next.root_asset_id, prompt, count, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
|
|
4612
|
+
const insertInput = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
4613
|
+
for (const input of inputs) insertInput.run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(next));
|
|
4614
|
+
insertReceipt(database, id, "plan", "generate image plan", preview2.receipts[0].payload);
|
|
4615
|
+
database.exec("COMMIT");
|
|
4616
|
+
} catch (error) {
|
|
4617
|
+
database.exec("ROLLBACK");
|
|
4618
|
+
throw error;
|
|
4619
|
+
}
|
|
4620
|
+
return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
|
|
4621
|
+
} finally {
|
|
4622
|
+
database.close();
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4099
4625
|
function planImageReroll(project = defaultProject, fields) {
|
|
4100
4626
|
const prompt = fields.prompt.trim();
|
|
4101
4627
|
if (!prompt) throw new GenerationReceiptError("Missing --prompt");
|
|
@@ -4140,7 +4666,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
4140
4666
|
id,
|
|
4141
4667
|
project_id: project,
|
|
4142
4668
|
provider,
|
|
4143
|
-
adapter_version:
|
|
4669
|
+
adapter_version: legacyAdapterVersion,
|
|
4144
4670
|
source_mode: "lineage_reroll",
|
|
4145
4671
|
root_asset_id: snapshot.root_asset_id,
|
|
4146
4672
|
prompt,
|
|
@@ -4172,7 +4698,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
4172
4698
|
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
4173
4699
|
expected_output_count, status, output_dir, handoff_json, created_at, updated_at
|
|
4174
4700
|
) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
|
|
4175
|
-
`).run(id, project, provider,
|
|
4701
|
+
`).run(id, project, provider, legacyAdapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
|
|
4176
4702
|
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));
|
|
4177
4703
|
insertReceipt(database, id, "plan", "reroll plan", preview2.receipts[0].payload);
|
|
4178
4704
|
database.exec("COMMIT");
|
|
@@ -4185,14 +4711,66 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
4185
4711
|
database.close();
|
|
4186
4712
|
}
|
|
4187
4713
|
}
|
|
4714
|
+
function parentForOutput(job, outputIndex) {
|
|
4715
|
+
const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
|
|
4716
|
+
if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
|
|
4717
|
+
if (inputs.length === 1) return inputs[0].asset_id;
|
|
4718
|
+
if (job.expected_output_count % inputs.length !== 0) throw new GenerationReceiptError("Generation job has invalid parent mapping");
|
|
4719
|
+
return inputs[Math.floor(outputIndex / (job.expected_output_count / inputs.length))]?.asset_id || inputs[inputs.length - 1].asset_id;
|
|
4720
|
+
}
|
|
4721
|
+
function parentInputs(job) {
|
|
4722
|
+
const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
|
|
4723
|
+
if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
|
|
4724
|
+
return inputs;
|
|
4725
|
+
}
|
|
4726
|
+
function parentFilesFor(job, parentFiles) {
|
|
4727
|
+
const inputs = parentInputs(job);
|
|
4728
|
+
const expectedPerParent = job.expected_output_count / inputs.length;
|
|
4729
|
+
if (!Number.isInteger(expectedPerParent)) throw new GenerationReceiptError("Generation job has invalid parent mapping");
|
|
4730
|
+
const allowedParents = new Set(inputs.map((input) => input.asset_id));
|
|
4731
|
+
const seenParents = /* @__PURE__ */ new Set();
|
|
4732
|
+
const mapped = [];
|
|
4733
|
+
for (const parentAssetId of Object.keys(parentFiles)) {
|
|
4734
|
+
if (!allowedParents.has(parentAssetId)) throw new GenerationReceiptError(`Unknown generation parent mapping: ${parentAssetId}`);
|
|
4735
|
+
if (seenParents.has(parentAssetId)) throw new GenerationReceiptError(`Duplicate generation parent mapping: ${parentAssetId}`);
|
|
4736
|
+
seenParents.add(parentAssetId);
|
|
4737
|
+
}
|
|
4738
|
+
for (const input of inputs) {
|
|
4739
|
+
const files = (parentFiles[input.asset_id] || []).map((file) => file.trim()).filter(Boolean);
|
|
4740
|
+
if (files.length === 0) throw new GenerationReceiptError(`Missing generation parent mapping for ${input.asset_id}`);
|
|
4741
|
+
if (files.length !== expectedPerParent) throw new GenerationReceiptError(`Parent ${input.asset_id} requires ${expectedPerParent} output file${expectedPerParent === 1 ? "" : "s"}, received ${files.length}`);
|
|
4742
|
+
for (const file of files) mapped.push({ file, parentAssetId: input.asset_id });
|
|
4743
|
+
}
|
|
4744
|
+
return mapped;
|
|
4745
|
+
}
|
|
4746
|
+
function orderedFilesFor(job, files) {
|
|
4747
|
+
return files.map((file, index) => ({ file, parentAssetId: parentForOutput(job, index) }));
|
|
4748
|
+
}
|
|
4749
|
+
function inspectImageGeneration(project = defaultProject, jobIdValue) {
|
|
4750
|
+
if (!jobIdValue) throw new GenerationReceiptError("Missing --job-id");
|
|
4751
|
+
const database = lineageDb();
|
|
4752
|
+
try {
|
|
4753
|
+
return { ok: true, command: "generate image inspect", project, job: loadGenerationJob(database, project, jobIdValue) };
|
|
4754
|
+
} finally {
|
|
4755
|
+
database.close();
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4188
4758
|
function isPathInside(child, parent) {
|
|
4189
4759
|
const rel = relative3(parent, child);
|
|
4190
4760
|
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
|
|
4191
4761
|
}
|
|
4192
|
-
function
|
|
4762
|
+
function scratchCandidate(file) {
|
|
4193
4763
|
const scratchRoot = resolve5(repoRoot, ".asset-scratch");
|
|
4194
4764
|
const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
|
|
4195
4765
|
if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
|
|
4766
|
+
return { candidate, scratchRoot };
|
|
4767
|
+
}
|
|
4768
|
+
function resolveScratchManifestPath(file) {
|
|
4769
|
+
const { candidate, scratchRoot } = scratchCandidate(file);
|
|
4770
|
+
return relative3(scratchRoot, candidate);
|
|
4771
|
+
}
|
|
4772
|
+
function resolveScratchFile(file) {
|
|
4773
|
+
const { candidate, scratchRoot } = scratchCandidate(file);
|
|
4196
4774
|
if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
|
|
4197
4775
|
const realScratchRoot = realpathSync2(scratchRoot);
|
|
4198
4776
|
const realCandidate = realpathSync2(candidate);
|
|
@@ -4208,6 +4786,165 @@ function resolveScratchFile(file) {
|
|
|
4208
4786
|
assetId: `local-${checksum.slice(0, 12)}`
|
|
4209
4787
|
};
|
|
4210
4788
|
}
|
|
4789
|
+
function generationManifestConflict() {
|
|
4790
|
+
throw new GenerationReceiptError("Generation import already exists with different output, summary, or provenance", 409);
|
|
4791
|
+
}
|
|
4792
|
+
function confirmManifestRetry(project, job, manifest) {
|
|
4793
|
+
if (job.outputs.length !== manifest.outputs.length) generationManifestConflict();
|
|
4794
|
+
const database = lineageDb();
|
|
4795
|
+
try {
|
|
4796
|
+
for (const expected of manifest.outputs) {
|
|
4797
|
+
const recorded = job.outputs.find((output) => output.output_index === expected.output_index);
|
|
4798
|
+
if (!recorded || recorded.file_path !== expected.file_path || recorded.parent_asset_id !== expected.parent_asset_id || recorded.edge_summary !== expected.edge_summary) generationManifestConflict();
|
|
4799
|
+
const edge = database.prepare(`
|
|
4800
|
+
select summary, summary_created_by, summary_updated_by, summary_updated_at
|
|
4801
|
+
from asset_edges
|
|
4802
|
+
where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
|
|
4803
|
+
`).get(project, recorded.parent_asset_id, recorded.imported_asset_id);
|
|
4804
|
+
if (!edge || edge.summary !== expected.edge_summary || edge.summary_created_by !== "agent" || edge.summary_updated_by !== "agent" || typeof edge.summary_updated_at !== "string" || edge.summary_updated_at.length === 0) generationManifestConflict();
|
|
4805
|
+
}
|
|
4806
|
+
return { ok: true, command: "generate image import", project, job, imported: job.outputs, idempotent: true };
|
|
4807
|
+
} finally {
|
|
4808
|
+
database.close();
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
function importImageGenerationOutputs(project = defaultProject, fields) {
|
|
4812
|
+
if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
|
|
4813
|
+
if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
|
|
4814
|
+
const database = lineageDb();
|
|
4815
|
+
let job;
|
|
4816
|
+
try {
|
|
4817
|
+
job = loadGenerationJob(database, project, fields.jobId);
|
|
4818
|
+
} finally {
|
|
4819
|
+
database.close();
|
|
4820
|
+
}
|
|
4821
|
+
if (job.source_mode !== "lineage_selection") throw new GenerationReceiptError(`Generation job is not an image-selection job: ${job.source_mode}`);
|
|
4822
|
+
const hasManifestInput = fields.manifest !== void 0;
|
|
4823
|
+
const hasParentFilesInput = fields.parentFiles !== void 0;
|
|
4824
|
+
const hasFilesInput = fields.files !== void 0;
|
|
4825
|
+
if (hasManifestInput && (hasParentFilesInput || hasFilesInput)) {
|
|
4826
|
+
throw new GenerationReceiptError("Use --manifest or legacy --files/--parent-files, not both");
|
|
4827
|
+
}
|
|
4828
|
+
if (hasParentFilesInput && hasFilesInput) throw new GenerationReceiptError("Use --files or --parent-files, not both");
|
|
4829
|
+
let manifest;
|
|
4830
|
+
let parentFileRows;
|
|
4831
|
+
let mappingStrategy;
|
|
4832
|
+
if (job.adapter_version === manifestAdapterVersion) {
|
|
4833
|
+
if (!hasManifestInput) throw new GenerationReceiptError("New generation jobs require --manifest");
|
|
4834
|
+
manifest = parseGenerationOutputManifest(fields.manifest, job, { resolveFilePath: resolveScratchManifestPath });
|
|
4835
|
+
if (job.status === "imported") return confirmManifestRetry(project, job, manifest);
|
|
4836
|
+
if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
|
|
4837
|
+
parentFileRows = manifest.outputs.map((output) => ({
|
|
4838
|
+
edgeSummary: output.edge_summary,
|
|
4839
|
+
file: output.file_path,
|
|
4840
|
+
parentAssetId: output.parent_asset_id
|
|
4841
|
+
}));
|
|
4842
|
+
mappingStrategy = "generation_output_manifest_v1";
|
|
4843
|
+
} else if (job.adapter_version === legacyAdapterVersion) {
|
|
4844
|
+
if (hasManifestInput) throw new GenerationReceiptError("Already-planned legacy generation jobs require --files or --parent-files");
|
|
4845
|
+
if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
|
|
4846
|
+
const hasExplicitParentFiles = Boolean(fields.parentFiles && Object.keys(fields.parentFiles).length > 0);
|
|
4847
|
+
parentFileRows = hasExplicitParentFiles ? parentFilesFor(job, fields.parentFiles || {}) : orderedFilesFor(job, (fields.files || []).map((file) => file.trim()).filter(Boolean));
|
|
4848
|
+
mappingStrategy = hasExplicitParentFiles ? "explicit_parent_files" : "ordered_per_base";
|
|
4849
|
+
} else {
|
|
4850
|
+
throw new GenerationReceiptError(`Unsupported generation adapter version: ${job.adapter_version}`);
|
|
4851
|
+
}
|
|
4852
|
+
if (parentFileRows.length === 0) throw new GenerationReceiptError("Generation import requires --files or --parent-files");
|
|
4853
|
+
if (parentFileRows.length !== job.expected_output_count) {
|
|
4854
|
+
throw new GenerationReceiptError(`Output count mismatch: expected ${job.expected_output_count}, received ${parentFileRows.length}`);
|
|
4855
|
+
}
|
|
4856
|
+
const resolved = parentFileRows.map((row) => ({ ...resolveScratchFile(row.file), edgeSummary: row.edgeSummary, parentAssetId: row.parentAssetId }));
|
|
4857
|
+
const uniquePaths = new Set(resolved.map((file) => file.relativePath));
|
|
4858
|
+
if (uniquePaths.size !== resolved.length) throw new GenerationReceiptError("Generation import files must be unique");
|
|
4859
|
+
cancelLineageIterateTasksForAssets(project, {
|
|
4860
|
+
actor: "system",
|
|
4861
|
+
confirmWrite: false,
|
|
4862
|
+
rootAssetId: job.root_asset_id
|
|
4863
|
+
});
|
|
4864
|
+
indexLineageAssets(project);
|
|
4865
|
+
const writeDb = lineageDb();
|
|
4866
|
+
try {
|
|
4867
|
+
const timestamp = nowIso();
|
|
4868
|
+
writeDb.exec("BEGIN IMMEDIATE");
|
|
4869
|
+
try {
|
|
4870
|
+
for (const [index, file] of resolved.entries()) {
|
|
4871
|
+
const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, file.assetId);
|
|
4872
|
+
if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${file.relativePath}`);
|
|
4873
|
+
const outputId = `${fields.jobId}:output:${index}`;
|
|
4874
|
+
writeDb.prepare(`insert into generation_job_outputs (
|
|
4875
|
+
id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type,
|
|
4876
|
+
imported_asset_id, parent_asset_id, imported_at, edge_summary
|
|
4877
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
4878
|
+
outputId,
|
|
4879
|
+
fields.jobId,
|
|
4880
|
+
project,
|
|
4881
|
+
index,
|
|
4882
|
+
file.relativePath,
|
|
4883
|
+
file.checksum,
|
|
4884
|
+
file.size,
|
|
4885
|
+
file.contentType,
|
|
4886
|
+
file.assetId,
|
|
4887
|
+
file.parentAssetId,
|
|
4888
|
+
timestamp,
|
|
4889
|
+
file.edgeSummary || null
|
|
4890
|
+
);
|
|
4891
|
+
const insertedEdge = writeDb.prepare(`insert into asset_edges (
|
|
4892
|
+
id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
|
|
4893
|
+
summary, summary_created_by, summary_updated_by, summary_updated_at
|
|
4894
|
+
) values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
|
|
4895
|
+
on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing`).run(
|
|
4896
|
+
`${project}:${file.parentAssetId}:derived_from:${file.assetId}`,
|
|
4897
|
+
project,
|
|
4898
|
+
file.parentAssetId,
|
|
4899
|
+
file.assetId,
|
|
4900
|
+
timestamp,
|
|
4901
|
+
file.edgeSummary || null,
|
|
4902
|
+
file.edgeSummary ? "agent" : null,
|
|
4903
|
+
file.edgeSummary ? "agent" : null,
|
|
4904
|
+
file.edgeSummary ? timestamp : null
|
|
4905
|
+
);
|
|
4906
|
+
if (file.edgeSummary && insertedEdge.changes === 0) {
|
|
4907
|
+
const edge = writeDb.prepare(`
|
|
4908
|
+
select summary, summary_created_by, summary_updated_by, summary_updated_at
|
|
4909
|
+
from asset_edges
|
|
4910
|
+
where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
|
|
4911
|
+
`).get(project, file.parentAssetId, file.assetId);
|
|
4912
|
+
if (!edge || edge.summary !== file.edgeSummary || edge.summary_created_by !== "agent" || edge.summary_updated_by !== "agent" || typeof edge.summary_updated_at !== "string" || edge.summary_updated_at.length === 0) generationManifestConflict();
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
writeDb.prepare(`
|
|
4916
|
+
update generation_jobs
|
|
4917
|
+
set status = 'imported', imported_at = ?, updated_at = ?
|
|
4918
|
+
where project_id = ? and id = ?
|
|
4919
|
+
`).run(timestamp, timestamp, project, fields.jobId);
|
|
4920
|
+
insertReceipt(writeDb, fields.jobId, "import", "generate image import", {
|
|
4921
|
+
mapping_strategy: mappingStrategy,
|
|
4922
|
+
files: resolved.map((file, index) => ({
|
|
4923
|
+
output_index: index,
|
|
4924
|
+
file_path: file.relativePath,
|
|
4925
|
+
imported_asset_id: file.assetId,
|
|
4926
|
+
parent_asset_id: file.parentAssetId,
|
|
4927
|
+
...file.edgeSummary ? { edge_summary: file.edgeSummary } : {}
|
|
4928
|
+
})),
|
|
4929
|
+
selection_reset: { root_asset_id: job.root_asset_id, cleared: true }
|
|
4930
|
+
});
|
|
4931
|
+
writeDb.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, job.root_asset_id);
|
|
4932
|
+
writeDb.exec("COMMIT");
|
|
4933
|
+
} catch (error) {
|
|
4934
|
+
writeDb.exec("ROLLBACK");
|
|
4935
|
+
throw error;
|
|
4936
|
+
}
|
|
4937
|
+
cancelLineageIterateTasksForAssets(project, {
|
|
4938
|
+
actor: "system",
|
|
4939
|
+
confirmWrite: true,
|
|
4940
|
+
rootAssetId: job.root_asset_id
|
|
4941
|
+
});
|
|
4942
|
+
const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
|
|
4943
|
+
return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
|
|
4944
|
+
} finally {
|
|
4945
|
+
writeDb.close();
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4211
4948
|
function importImageRerollOutput(project = defaultProject, fields) {
|
|
4212
4949
|
if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
|
|
4213
4950
|
if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
|
|
@@ -4783,7 +5520,7 @@ function getLineageSelectionPacket(project, options = {}) {
|
|
|
4783
5520
|
import { createHash as createHash6 } from "node:crypto";
|
|
4784
5521
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
4785
5522
|
import { createRequire as createRequire4 } from "node:module";
|
|
4786
|
-
import { existsSync as existsSync10, lstatSync as
|
|
5523
|
+
import { existsSync as existsSync10, lstatSync as lstatSync3, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync3, statSync as statSync6 } from "node:fs";
|
|
4787
5524
|
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join9, resolve as resolve7 } from "node:path";
|
|
4788
5525
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4789
5526
|
|
|
@@ -4810,7 +5547,7 @@ function executingCodeRoot() {
|
|
|
4810
5547
|
return canonicalRoot(root);
|
|
4811
5548
|
}
|
|
4812
5549
|
var codeRoot = executingCodeRoot();
|
|
4813
|
-
function
|
|
5550
|
+
function sha2562(value) {
|
|
4814
5551
|
return createHash6("sha256").update(value).digest("hex");
|
|
4815
5552
|
}
|
|
4816
5553
|
function canonicalRoot(root) {
|
|
@@ -4849,7 +5586,7 @@ function untrackedFingerprint(root, status) {
|
|
|
4849
5586
|
hash.update(relativePath);
|
|
4850
5587
|
const path = join9(root, relativePath);
|
|
4851
5588
|
try {
|
|
4852
|
-
const stat =
|
|
5589
|
+
const stat = lstatSync3(path);
|
|
4853
5590
|
if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
|
|
4854
5591
|
else if (stat.isFile()) hash.update(readFileSync5(path));
|
|
4855
5592
|
else hash.update(`[${stat.mode}:${stat.size}]`);
|
|
@@ -4871,12 +5608,12 @@ function checkoutCodeIdentity(root, channel, version) {
|
|
|
4871
5608
|
if (status === void 0 || diff === void 0) errors.push("Checkout dirty state could not be inspected");
|
|
4872
5609
|
if (channel !== "dev") errors.push(`Checkout code may run only as dev, not ${channel}`);
|
|
4873
5610
|
const dirty = Boolean(status);
|
|
4874
|
-
const sourceFingerprint =
|
|
5611
|
+
const sourceFingerprint = sha2562(`${gitSha || "unknown"}\0${sha2562(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
|
|
4875
5612
|
return {
|
|
4876
5613
|
channel,
|
|
4877
5614
|
dirty,
|
|
4878
5615
|
errors,
|
|
4879
|
-
fingerprint:
|
|
5616
|
+
fingerprint: sha2562(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
|
|
4880
5617
|
git_sha: gitSha,
|
|
4881
5618
|
origin: "checkout",
|
|
4882
5619
|
package_version: version,
|
|
@@ -4886,7 +5623,7 @@ function checkoutCodeIdentity(root, channel, version) {
|
|
|
4886
5623
|
};
|
|
4887
5624
|
}
|
|
4888
5625
|
function buildFingerprint(build) {
|
|
4889
|
-
return
|
|
5626
|
+
return sha2562(JSON.stringify({
|
|
4890
5627
|
package_name: build.package_name,
|
|
4891
5628
|
package_version: build.package_version,
|
|
4892
5629
|
schema_version: build.schema_version,
|
|
@@ -4986,7 +5723,7 @@ function packageCodeIdentity(root, channel, info, receiptPath) {
|
|
|
4986
5723
|
channel,
|
|
4987
5724
|
dirty: build?.source_dirty,
|
|
4988
5725
|
errors,
|
|
4989
|
-
fingerprint:
|
|
5726
|
+
fingerprint: sha2562(JSON.stringify({
|
|
4990
5727
|
build_fingerprint: build?.build_fingerprint,
|
|
4991
5728
|
channel,
|
|
4992
5729
|
install_integrity: install?.package_integrity,
|
|
@@ -5014,7 +5751,7 @@ function getLineageCodeIdentity(channel, options = {}) {
|
|
|
5014
5751
|
return {
|
|
5015
5752
|
channel,
|
|
5016
5753
|
errors,
|
|
5017
|
-
fingerprint:
|
|
5754
|
+
fingerprint: sha2562(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
|
|
5018
5755
|
origin: "unknown",
|
|
5019
5756
|
package_version: info.version,
|
|
5020
5757
|
root,
|
|
@@ -5111,22 +5848,23 @@ var ManagedWriterRoutingError = class extends Error {
|
|
|
5111
5848
|
status;
|
|
5112
5849
|
};
|
|
5113
5850
|
function managedWriterTimeoutMs(command, args) {
|
|
5114
|
-
const
|
|
5115
|
-
|
|
5851
|
+
const positions = args.filter((arg) => !arg.startsWith("-"));
|
|
5852
|
+
const slowImport = command === "reroll" && positions[0] === "import" || command === "generate" && positions[0] === "image" && positions[1] === "import";
|
|
5853
|
+
return slowImport ? 5 * 6e4 : 3e4;
|
|
5116
5854
|
}
|
|
5117
5855
|
function errorMessage(body, fallback) {
|
|
5118
5856
|
if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
|
|
5119
|
-
const
|
|
5120
|
-
if (typeof
|
|
5121
|
-
if (typeof
|
|
5857
|
+
const record2 = body;
|
|
5858
|
+
if (typeof record2.message === "string") return record2.message;
|
|
5859
|
+
if (typeof record2.error === "string") return record2.error;
|
|
5122
5860
|
return fallback;
|
|
5123
5861
|
}
|
|
5124
5862
|
function agentClaimError(body, status) {
|
|
5125
5863
|
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
5126
|
-
const
|
|
5127
|
-
if (typeof
|
|
5128
|
-
const conflicts = Array.isArray(
|
|
5129
|
-
return new AgentClaimError(
|
|
5864
|
+
const record2 = body;
|
|
5865
|
+
if (typeof record2.message !== "string" || typeof record2.error !== "string") return void 0;
|
|
5866
|
+
const conflicts = Array.isArray(record2.conflicts) ? record2.conflicts : [];
|
|
5867
|
+
return new AgentClaimError(record2.message, status, record2.error, conflicts);
|
|
5130
5868
|
}
|
|
5131
5869
|
function assertResponseIdentity(body, profile, channel) {
|
|
5132
5870
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -5224,6 +5962,26 @@ function readOptions(args, name) {
|
|
|
5224
5962
|
}
|
|
5225
5963
|
return values;
|
|
5226
5964
|
}
|
|
5965
|
+
function readJsonFile(path, label) {
|
|
5966
|
+
try {
|
|
5967
|
+
return JSON.parse(readFileSync6(resolve8(path), "utf8"));
|
|
5968
|
+
} catch (error) {
|
|
5969
|
+
throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
function parentFilesOption(value) {
|
|
5973
|
+
if (value === void 0) return void 0;
|
|
5974
|
+
const mappings = {};
|
|
5975
|
+
for (const rawMapping of value.split(";")) {
|
|
5976
|
+
const separator = rawMapping.indexOf("=");
|
|
5977
|
+
const parent = separator >= 0 ? rawMapping.slice(0, separator).trim() : "";
|
|
5978
|
+
const files = separator >= 0 ? rawMapping.slice(separator + 1).split(",").map((file) => file.trim()).filter(Boolean) : [];
|
|
5979
|
+
if (!parent || files.length === 0) throw new Error("lineage generate image import --parent-files requires parent=file[,file][;parent=file]");
|
|
5980
|
+
if (Object.hasOwn(mappings, parent)) throw new Error(`Duplicate generation parent mapping: ${parent}`);
|
|
5981
|
+
mappings[parent] = files;
|
|
5982
|
+
}
|
|
5983
|
+
return mappings;
|
|
5984
|
+
}
|
|
5227
5985
|
function resolveStartOptions(config, args) {
|
|
5228
5986
|
const profile = prepareCliProfile(config, args);
|
|
5229
5987
|
const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
|
|
@@ -5309,13 +6067,18 @@ Usage:
|
|
|
5309
6067
|
${config.binName} profile bind --profile <id-or-manifest> --confirm-write [--json]
|
|
5310
6068
|
${config.binName} profile clone --source-db <snapshot-source> --target-profile <id-or-manifest> --confirm-write [--json]
|
|
5311
6069
|
${config.binName} profile clone-assets --source-asset-root <path> --target-profile <id-or-manifest> --confirm-write [--json]
|
|
6070
|
+
${config.binName} profile repin-runtime --profile <development-profile> --checkout-root <path> --confirm-write [--json]
|
|
5312
6071
|
${config.binName} runtime info [--json]
|
|
5313
6072
|
${config.binName} runtime doctor [--json]
|
|
5314
6073
|
${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
5315
6074
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
5316
6075
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
5317
6076
|
${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--schema v2] [--out <path>] [--strict] [--db <path>] [--json]
|
|
5318
|
-
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
6077
|
+
${config.binName} link-child --root <asset-id> --child <asset-id> --summary "<one-or-two-words>" [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
6078
|
+
${config.binName} generate image plan --prompt <text> --from-lineage-selection [--count <count>|--per-base-count <count>] [--project <project>] [--dry-run] [--db <path>] [--json]
|
|
6079
|
+
${config.binName} generate image inspect --job-id <job-id> [--project <project>] [--db <path>] [--json]
|
|
6080
|
+
${config.binName} generate image import --job-id <job-id> --manifest <json-file> --confirm-write [--project <project>] [--db <path>] [--json]
|
|
6081
|
+
${config.binName} generate image import --job-id <legacy-job-id> (--files <file,file>|--parent-files <parent=file;parent=file>) --confirm-write [--project <project>] [--db <path>] [--json]
|
|
5319
6082
|
${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
5320
6083
|
${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
5321
6084
|
${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
@@ -5437,7 +6200,8 @@ function resolveDataCommandOptions(args) {
|
|
|
5437
6200
|
dbPath: readOption(args, "--db"),
|
|
5438
6201
|
json: args.includes("--json"),
|
|
5439
6202
|
project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
|
|
5440
|
-
rootAssetId: readOption(args, "--root")
|
|
6203
|
+
rootAssetId: readOption(args, "--root"),
|
|
6204
|
+
summary: readOption(args, "--summary")
|
|
5441
6205
|
};
|
|
5442
6206
|
if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
|
|
5443
6207
|
return options;
|
|
@@ -5480,13 +6244,57 @@ function runLineageDataCommand(command, args) {
|
|
|
5480
6244
|
}
|
|
5481
6245
|
if (command === "link-child") {
|
|
5482
6246
|
if (!options.childAssetId) throw new Error("lineage link-child requires --child");
|
|
6247
|
+
const summary = requireEdgeSummary(options.summary);
|
|
5483
6248
|
return linkSelectedLineageChild(options.project, {
|
|
5484
6249
|
childAssetId: options.childAssetId,
|
|
5485
6250
|
claimToken: options.claimToken,
|
|
5486
6251
|
confirmWrite: options.confirmWrite,
|
|
5487
|
-
rootAssetId: options.rootAssetId || options.assetId
|
|
6252
|
+
rootAssetId: options.rootAssetId || options.assetId,
|
|
6253
|
+
summary,
|
|
6254
|
+
summaryActor: "agent"
|
|
5488
6255
|
});
|
|
5489
6256
|
}
|
|
6257
|
+
if (command === "generate") {
|
|
6258
|
+
const [resource, subcommand] = positionalArgs(args);
|
|
6259
|
+
if (resource !== "image") throw new Error(`Unknown generate command: ${resource}`);
|
|
6260
|
+
if (subcommand === "plan") {
|
|
6261
|
+
const prompt = readOption(args, "--prompt");
|
|
6262
|
+
const rawCount = readOption(args, "--count");
|
|
6263
|
+
const rawPerBaseCount = readOption(args, "--per-base-count");
|
|
6264
|
+
if (!prompt) throw new Error("lineage generate image plan requires --prompt");
|
|
6265
|
+
return planImageGeneration(options.project, {
|
|
6266
|
+
count: rawCount === void 0 ? void 0 : Number(rawCount),
|
|
6267
|
+
dryRun: args.includes("--dry-run"),
|
|
6268
|
+
fromLineageSelection: args.includes("--from-lineage-selection"),
|
|
6269
|
+
perBaseCount: rawPerBaseCount === void 0 ? void 0 : Number(rawPerBaseCount),
|
|
6270
|
+
prompt
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
if (subcommand === "inspect") {
|
|
6274
|
+
const jobId2 = readOption(args, "--job-id");
|
|
6275
|
+
if (!jobId2) throw new Error("lineage generate image inspect requires --job-id");
|
|
6276
|
+
return inspectImageGeneration(options.project, jobId2);
|
|
6277
|
+
}
|
|
6278
|
+
if (subcommand === "import") {
|
|
6279
|
+
const jobId2 = readOption(args, "--job-id");
|
|
6280
|
+
const manifestPath = readOption(args, "--manifest");
|
|
6281
|
+
const filesValue = readOption(args, "--files");
|
|
6282
|
+
const parentFilesValue = readOption(args, "--parent-files");
|
|
6283
|
+
if (!jobId2) throw new Error("lineage generate image import requires --job-id");
|
|
6284
|
+
if (manifestPath !== void 0 && (filesValue !== void 0 || parentFilesValue !== void 0)) {
|
|
6285
|
+
throw new Error("Use --manifest or legacy --files/--parent-files, not both");
|
|
6286
|
+
}
|
|
6287
|
+
if (filesValue !== void 0 && parentFilesValue !== void 0) throw new Error("Use --files or --parent-files, not both");
|
|
6288
|
+
return importImageGenerationOutputs(options.project, {
|
|
6289
|
+
confirmWrite: options.confirmWrite,
|
|
6290
|
+
files: filesValue === void 0 ? void 0 : filesValue.split(",").map((file) => file.trim()).filter(Boolean),
|
|
6291
|
+
jobId: jobId2,
|
|
6292
|
+
manifest: manifestPath === void 0 ? void 0 : readJsonFile(manifestPath, "Generation output manifest"),
|
|
6293
|
+
parentFiles: parentFilesOption(parentFilesValue)
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6296
|
+
throw new Error(`Unknown generate image command: ${subcommand}`);
|
|
6297
|
+
}
|
|
5490
6298
|
if (command === "reroll") {
|
|
5491
6299
|
const subcommand = positionalArgs(args)[0] || "";
|
|
5492
6300
|
if (subcommand === "list") {
|
|
@@ -5640,6 +6448,15 @@ function printDataResult(command, result, json) {
|
|
|
5640
6448
|
if (link.warning) console.log(`Warning: ${link.warning}`);
|
|
5641
6449
|
return;
|
|
5642
6450
|
}
|
|
6451
|
+
if (command === "generate" && result && typeof result === "object" && "job" in result) {
|
|
6452
|
+
const generation = result;
|
|
6453
|
+
if (generation.imported) {
|
|
6454
|
+
console.log(`${generation.idempotent ? "Already imported" : "Imported"} ${generation.imported.length} output(s) for ${generation.job?.id || "job"}`);
|
|
6455
|
+
} else {
|
|
6456
|
+
console.log(`Generation job ${generation.job?.id || "job"} ${generation.job?.status || "unknown"}`);
|
|
6457
|
+
}
|
|
6458
|
+
return;
|
|
6459
|
+
}
|
|
5643
6460
|
if (command === "reroll" && result && typeof result === "object") {
|
|
5644
6461
|
if ("requests" in result) {
|
|
5645
6462
|
const listed = result;
|
|
@@ -5715,6 +6532,18 @@ function runLineageProfileCommand(config, command, args) {
|
|
|
5715
6532
|
const selector = profileSelector(args);
|
|
5716
6533
|
if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
|
|
5717
6534
|
if (command === "doctor") return doctorLineageProfile(selector, runtimeIdentity);
|
|
6535
|
+
if (command === "repin-runtime") {
|
|
6536
|
+
const checkoutRoot = readOption(args, "--checkout-root");
|
|
6537
|
+
if (!checkoutRoot) throw new Error("Profile runtime repin requires --checkout-root");
|
|
6538
|
+
if (!args.includes("--confirm-write")) throw new Error("Profile runtime repin requires --confirm-write");
|
|
6539
|
+
const profile = resolveLineageProfile(selector);
|
|
6540
|
+
const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
|
|
6541
|
+
try {
|
|
6542
|
+
return repinLineageDevelopmentProfileRuntime(selector, checkoutRoot, runtimeIdentity, true);
|
|
6543
|
+
} finally {
|
|
6544
|
+
writerLease.release();
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
5718
6547
|
if (command === "bind") {
|
|
5719
6548
|
if (!args.includes("--confirm-write")) throw new Error("Profile bind requires --confirm-write");
|
|
5720
6549
|
const profile = resolveLineageProfile(selector);
|
|
@@ -5740,6 +6569,9 @@ function printProfileResult(result, json) {
|
|
|
5740
6569
|
} else if (result.schema_version === "lineage.profile_clone_receipt.v1") {
|
|
5741
6570
|
console.log(`Cloned ${result.source_database_path} to ${result.database_path} for ${result.target_identity.profile_id}`);
|
|
5742
6571
|
console.log(`Receipt: ${result.receipt_path}`);
|
|
6572
|
+
} else if (result.schema_version === "lineage.profile_runtime_repin_receipt.v1") {
|
|
6573
|
+
console.log(`${result.changed ? "Repinned" : "Already pinned"} ${result.profile_id} to checkout ${result.checkout_root}`);
|
|
6574
|
+
console.log(`Code fingerprint: ${result.new_code_fingerprint}`);
|
|
5743
6575
|
} else {
|
|
5744
6576
|
console.log(`Cloned ${result.files_copied} referenced asset file(s) into ${result.asset_root}`);
|
|
5745
6577
|
console.log(`Receipt: ${result.receipt_path}`);
|
|
@@ -5770,7 +6602,9 @@ function printRuntimeResult(result, json) {
|
|
|
5770
6602
|
}
|
|
5771
6603
|
function lineageCliRequiresWriterLease(command, args) {
|
|
5772
6604
|
if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
|
|
5773
|
-
const
|
|
6605
|
+
const positions = positionalArgs(args);
|
|
6606
|
+
const subcommand = positions[0] || "";
|
|
6607
|
+
if (command === "generate") return positions[0] !== "image" || positions[1] !== "inspect";
|
|
5774
6608
|
if (command === "reroll") return subcommand !== "list";
|
|
5775
6609
|
if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
|
|
5776
6610
|
if (command === "db") return subcommand !== "info";
|
|
@@ -6110,7 +6944,7 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
6110
6944
|
else console.error(`${config.binName}: ${message2}`);
|
|
6111
6945
|
process.exit(1);
|
|
6112
6946
|
}
|
|
6113
|
-
if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
|
|
6947
|
+
if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "generate" || command === "reroll" || command === "tasks") {
|
|
6114
6948
|
const commandArgs = normalizedArgs.slice(1);
|
|
6115
6949
|
const json2 = commandArgs.includes("--json");
|
|
6116
6950
|
try {
|