@prisma-next/migration-tools 0.5.0-dev.3 → 0.5.0-dev.31
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/README.md +34 -22
- package/dist/{constants-BRi0X7B_.mjs → constants-BQEHsaEx.mjs} +1 -1
- package/dist/{constants-BRi0X7B_.mjs.map → constants-BQEHsaEx.mjs.map} +1 -1
- package/dist/errors-Bl3cKiM8.mjs +244 -0
- package/dist/errors-Bl3cKiM8.mjs.map +1 -0
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/{types.d.mts → errors.d.mts} +7 -8
- package/dist/exports/errors.d.mts.map +1 -0
- package/dist/exports/errors.mjs +3 -0
- package/dist/exports/graph.d.mts +2 -0
- package/dist/exports/graph.mjs +1 -0
- package/dist/exports/hash.d.mts +52 -0
- package/dist/exports/hash.d.mts.map +1 -0
- package/dist/exports/hash.mjs +3 -0
- package/dist/exports/invariants.d.mts +24 -0
- package/dist/exports/invariants.d.mts.map +1 -0
- package/dist/exports/invariants.mjs +4 -0
- package/dist/exports/io.d.mts +7 -6
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +162 -2
- package/dist/exports/io.mjs.map +1 -0
- package/dist/exports/metadata.d.mts +2 -0
- package/dist/exports/metadata.mjs +1 -0
- package/dist/exports/{dag.d.mts → migration-graph.d.mts} +31 -10
- package/dist/exports/migration-graph.d.mts.map +1 -0
- package/dist/exports/{dag.mjs → migration-graph.mjs} +143 -63
- package/dist/exports/migration-graph.mjs.map +1 -0
- package/dist/exports/migration-ts.mjs +1 -1
- package/dist/exports/migration.d.mts +15 -14
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +68 -40
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +2 -0
- package/dist/exports/package.mjs +1 -0
- package/dist/exports/refs.d.mts +11 -5
- package/dist/exports/refs.d.mts.map +1 -1
- package/dist/exports/refs.mjs +106 -30
- package/dist/exports/refs.mjs.map +1 -1
- package/dist/graph-BHPv-9Gl.d.mts +28 -0
- package/dist/graph-BHPv-9Gl.d.mts.map +1 -0
- package/dist/hash-BARZdVgW.mjs +76 -0
- package/dist/hash-BARZdVgW.mjs.map +1 -0
- package/dist/invariants-BmrTBQ0A.mjs +42 -0
- package/dist/invariants-BmrTBQ0A.mjs.map +1 -0
- package/dist/metadata-BP1cmU7Z.d.mts +50 -0
- package/dist/metadata-BP1cmU7Z.d.mts.map +1 -0
- package/dist/op-schema-DZKFua46.mjs +14 -0
- package/dist/op-schema-DZKFua46.mjs.map +1 -0
- package/dist/package-5HCCg0z-.d.mts +21 -0
- package/dist/package-5HCCg0z-.d.mts.map +1 -0
- package/package.json +32 -16
- package/src/errors.ts +139 -15
- package/src/exports/errors.ts +1 -0
- package/src/exports/graph.ts +1 -0
- package/src/exports/hash.ts +2 -0
- package/src/exports/invariants.ts +1 -0
- package/src/exports/io.ts +1 -1
- package/src/exports/metadata.ts +1 -0
- package/src/exports/{dag.ts → migration-graph.ts} +3 -2
- package/src/exports/migration.ts +0 -1
- package/src/exports/package.ts +1 -0
- package/src/exports/refs.ts +10 -2
- package/src/graph-ops.ts +57 -30
- package/src/graph.ts +25 -0
- package/src/hash.ts +91 -0
- package/src/invariants.ts +45 -0
- package/src/io.ts +57 -31
- package/src/metadata.ts +41 -0
- package/src/migration-base.ts +97 -56
- package/src/{dag.ts → migration-graph.ts} +156 -54
- package/src/op-schema.ts +11 -0
- package/src/package.ts +18 -0
- package/src/refs.ts +148 -37
- package/dist/attestation-DtF8tEOM.mjs +0 -65
- package/dist/attestation-DtF8tEOM.mjs.map +0 -1
- package/dist/errors-BKbRGCJM.mjs +0 -160
- package/dist/errors-BKbRGCJM.mjs.map +0 -1
- package/dist/exports/attestation.d.mts +0 -37
- package/dist/exports/attestation.d.mts.map +0 -1
- package/dist/exports/attestation.mjs +0 -4
- package/dist/exports/dag.d.mts.map +0 -1
- package/dist/exports/dag.mjs.map +0 -1
- package/dist/exports/types.d.mts.map +0 -1
- package/dist/exports/types.mjs +0 -3
- package/dist/io-CCnYsUHU.mjs +0 -153
- package/dist/io-CCnYsUHU.mjs.map +0 -1
- package/dist/types-DyGXcWWp.d.mts +0 -71
- package/dist/types-DyGXcWWp.d.mts.map +0 -1
- package/src/attestation.ts +0 -81
- package/src/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
package/dist/exports/io.mjs
CHANGED
|
@@ -1,3 +1,163 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as errorInvalidJson, g as errorMissingFile, h as errorMigrationHashMismatch, l as errorInvalidManifest, m as errorInvalidSlug, o as errorInvalidDestName, r as errorDirectoryExists, y as errorProvidedInvariantsMismatch } from "../errors-Bl3cKiM8.mjs";
|
|
2
|
+
import { n as verifyMigrationHash } from "../hash-BARZdVgW.mjs";
|
|
3
|
+
import { t as deriveProvidedInvariants } from "../invariants-BmrTBQ0A.mjs";
|
|
4
|
+
import { n as MigrationOpsSchema } from "../op-schema-DZKFua46.mjs";
|
|
5
|
+
import { basename, dirname, join } from "pathe";
|
|
6
|
+
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { type } from "arktype";
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
//#region src/io.ts
|
|
10
|
+
const MANIFEST_FILE = "migration.json";
|
|
11
|
+
const OPS_FILE = "ops.json";
|
|
12
|
+
const MAX_SLUG_LENGTH = 64;
|
|
13
|
+
function hasErrnoCode(error, code) {
|
|
14
|
+
return error instanceof Error && error.code === code;
|
|
15
|
+
}
|
|
16
|
+
const MigrationHintsSchema = type({
|
|
17
|
+
used: "string[]",
|
|
18
|
+
applied: "string[]",
|
|
19
|
+
plannerVersion: "string"
|
|
20
|
+
});
|
|
21
|
+
const MigrationMetadataSchema = type({
|
|
22
|
+
"+": "reject",
|
|
23
|
+
from: "string > 0 | null",
|
|
24
|
+
to: "string",
|
|
25
|
+
migrationHash: "string",
|
|
26
|
+
fromContract: "object | null",
|
|
27
|
+
toContract: "object",
|
|
28
|
+
hints: MigrationHintsSchema,
|
|
29
|
+
labels: "string[]",
|
|
30
|
+
providedInvariants: "string[]",
|
|
31
|
+
"authorship?": type({
|
|
32
|
+
"author?": "string",
|
|
33
|
+
"email?": "string"
|
|
34
|
+
}),
|
|
35
|
+
"signature?": type({
|
|
36
|
+
keyId: "string",
|
|
37
|
+
value: "string"
|
|
38
|
+
}).or("null"),
|
|
39
|
+
createdAt: "string"
|
|
40
|
+
});
|
|
41
|
+
async function writeMigrationPackage(dir, metadata, ops) {
|
|
42
|
+
await mkdir(dirname(dir), { recursive: true });
|
|
43
|
+
try {
|
|
44
|
+
await mkdir(dir);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (hasErrnoCode(error, "EEXIST")) throw errorDirectoryExists(dir);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), { flag: "wx" });
|
|
50
|
+
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Copy a list of files into `destDir`, optionally renaming each one.
|
|
54
|
+
*
|
|
55
|
+
* The destination directory is created (with `recursive: true`) if it
|
|
56
|
+
* does not already exist. Each source path is copied byte-for-byte into
|
|
57
|
+
* `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
|
|
58
|
+
* intentionally generic: callers own the list of files (e.g. a contract
|
|
59
|
+
* emitter's emitted output) and the naming convention (e.g. renaming
|
|
60
|
+
* the destination contract to `end-contract.*` and the source contract
|
|
61
|
+
* to `start-contract.*`).
|
|
62
|
+
*/
|
|
63
|
+
async function copyFilesWithRename(destDir, files) {
|
|
64
|
+
await mkdir(destDir, { recursive: true });
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (basename(file.destName) !== file.destName) throw errorInvalidDestName(file.destName);
|
|
67
|
+
await copyFile(file.sourcePath, join(destDir, file.destName));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function writeMigrationMetadata(dir, metadata) {
|
|
71
|
+
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
72
|
+
}
|
|
73
|
+
async function writeMigrationOps(dir, ops) {
|
|
74
|
+
await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
|
|
75
|
+
}
|
|
76
|
+
async function readMigrationPackage(dir) {
|
|
77
|
+
const manifestPath = join(dir, MANIFEST_FILE);
|
|
78
|
+
const opsPath = join(dir, OPS_FILE);
|
|
79
|
+
let manifestRaw;
|
|
80
|
+
try {
|
|
81
|
+
manifestRaw = await readFile(manifestPath, "utf-8");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(MANIFEST_FILE, dir);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
let opsRaw;
|
|
87
|
+
try {
|
|
88
|
+
opsRaw = await readFile(opsPath, "utf-8");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(OPS_FILE, dir);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
let metadata;
|
|
94
|
+
try {
|
|
95
|
+
metadata = JSON.parse(manifestRaw);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
|
|
98
|
+
}
|
|
99
|
+
let ops;
|
|
100
|
+
try {
|
|
101
|
+
ops = JSON.parse(opsRaw);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
|
|
104
|
+
}
|
|
105
|
+
validateMetadata(metadata, manifestPath);
|
|
106
|
+
validateOps(ops, opsPath);
|
|
107
|
+
const derivedInvariants = deriveProvidedInvariants(ops);
|
|
108
|
+
if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) throw errorProvidedInvariantsMismatch(manifestPath, metadata.providedInvariants, derivedInvariants);
|
|
109
|
+
const pkg = {
|
|
110
|
+
dirName: basename(dir),
|
|
111
|
+
dirPath: dir,
|
|
112
|
+
metadata,
|
|
113
|
+
ops
|
|
114
|
+
};
|
|
115
|
+
const verification = verifyMigrationHash(pkg);
|
|
116
|
+
if (!verification.ok) throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);
|
|
117
|
+
return pkg;
|
|
118
|
+
}
|
|
119
|
+
function arraysEqual(a, b) {
|
|
120
|
+
if (a.length !== b.length) return false;
|
|
121
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
function validateMetadata(metadata, filePath) {
|
|
125
|
+
const result = MigrationMetadataSchema(metadata);
|
|
126
|
+
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
|
|
127
|
+
}
|
|
128
|
+
function validateOps(ops, filePath) {
|
|
129
|
+
const result = MigrationOpsSchema(ops);
|
|
130
|
+
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
|
|
131
|
+
}
|
|
132
|
+
async function readMigrationsDir(migrationsRoot) {
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = await readdir(migrationsRoot);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (hasErrnoCode(error, "ENOENT")) return [];
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const packages = [];
|
|
141
|
+
for (const entry of entries.sort()) {
|
|
142
|
+
const entryPath = join(migrationsRoot, entry);
|
|
143
|
+
if (!(await stat(entryPath)).isDirectory()) continue;
|
|
144
|
+
const manifestPath = join(entryPath, MANIFEST_FILE);
|
|
145
|
+
try {
|
|
146
|
+
await stat(manifestPath);
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
packages.push(await readMigrationPackage(entryPath));
|
|
151
|
+
}
|
|
152
|
+
return packages;
|
|
153
|
+
}
|
|
154
|
+
function formatMigrationDirName(timestamp, slug) {
|
|
155
|
+
const sanitized = slug.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
156
|
+
if (sanitized.length === 0) throw errorInvalidSlug(slug);
|
|
157
|
+
const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);
|
|
158
|
+
return `${timestamp.getUTCFullYear()}${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}${String(timestamp.getUTCDate()).padStart(2, "0")}T${String(timestamp.getUTCHours()).padStart(2, "0")}${String(timestamp.getUTCMinutes()).padStart(2, "0")}_${truncated}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
export { copyFilesWithRename, formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationMetadata, writeMigrationOps, writeMigrationPackage };
|
|
163
|
+
//# sourceMappingURL=io.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io.mjs","names":["manifestRaw: string","opsRaw: string","metadata: MigrationMetadata","ops: MigrationOps","pkg: MigrationPackage","entries: string[]","packages: MigrationPackage[]"],"sources":["../../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n errorProvidedInvariantsMismatch,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationMetadata } from './metadata';\nimport { MigrationOpsSchema } from './op-schema';\nimport type { MigrationOps, MigrationPackage } from './package';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationMetadataSchema = type({\n '+': 'reject',\n from: 'string > 0 | null',\n to: 'string',\n migrationHash: 'string',\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n providedInvariants: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nexport async function writeMigrationPackage(\n dir: string,\n metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<MigrationPackage> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n // Re-derive before the hash check so format/duplicate diagnostics\n // fire with their dedicated codes rather than as a generic hash mismatch.\n const derivedInvariants = deriveProvidedInvariants(ops);\n if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {\n throw errorProvidedInvariantsMismatch(\n manifestPath,\n metadata.providedInvariants,\n derivedInvariants,\n );\n }\n\n const pkg: MigrationPackage = {\n dirName: basename(dir),\n dirPath: dir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);\n }\n\n return pkg;\n}\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly MigrationPackage[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: MigrationPackage[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -1,18 +1,39 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { n as MigrationGraph, t as MigrationEdge } from "../graph-BHPv-9Gl.mjs";
|
|
2
|
+
import { n as MigrationPackage } from "../package-5HCCg0z-.mjs";
|
|
2
3
|
|
|
3
|
-
//#region src/
|
|
4
|
-
declare function reconstructGraph(packages: readonly
|
|
4
|
+
//#region src/migration-graph.d.ts
|
|
5
|
+
declare function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph;
|
|
5
6
|
/**
|
|
6
7
|
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
7
8
|
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
8
9
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
9
10
|
*
|
|
10
11
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
11
|
-
* label priority → createdAt → to →
|
|
12
|
+
* label priority → createdAt → to → migrationHash.
|
|
12
13
|
*/
|
|
13
|
-
declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly
|
|
14
|
+
declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly MigrationEdge[] | null;
|
|
15
|
+
/**
|
|
16
|
+
* Find the shortest path from `fromHash` to `toHash` whose edges collectively
|
|
17
|
+
* cover every invariant in `required`. Returns `null` when no such path exists
|
|
18
|
+
* (either `fromHash`→`toHash` is structurally unreachable, or every reachable
|
|
19
|
+
* path leaves at least one required invariant uncovered). When `required` is
|
|
20
|
+
* empty, delegates to `findPath` so the result is byte-identical for that case.
|
|
21
|
+
*
|
|
22
|
+
* Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.
|
|
23
|
+
* The covered subset is a `Set<string>` of invariant ids; the state's dedup
|
|
24
|
+
* key is `${node}\0${[...covered].sort().join('\0')}`. State keys distinguish
|
|
25
|
+
* distinct `(node, covered)` tuples regardless of node-name length because
|
|
26
|
+
* `\0` cannot appear in any invariant id (validation rejects whitespace and
|
|
27
|
+
* control chars at authoring time).
|
|
28
|
+
*
|
|
29
|
+
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
30
|
+
* invariant come first, with `labelPriority → createdAt → to → migrationHash`
|
|
31
|
+
* as the secondary key. The heuristic steers BFS toward the satisfying path;
|
|
32
|
+
* correctness (shortest, deterministic) does not depend on it.
|
|
33
|
+
*/
|
|
34
|
+
declare function findPathWithInvariants(graph: MigrationGraph, fromHash: string, toHash: string, required: ReadonlySet<string>): readonly MigrationEdge[] | null;
|
|
14
35
|
interface PathDecision {
|
|
15
|
-
readonly selectedPath: readonly
|
|
36
|
+
readonly selectedPath: readonly MigrationEdge[];
|
|
16
37
|
readonly fromHash: string;
|
|
17
38
|
readonly toHash: string;
|
|
18
39
|
readonly alternativeCount: number;
|
|
@@ -43,9 +64,9 @@ declare function findLeaf(graph: MigrationGraph): string | null;
|
|
|
43
64
|
* to the single target. Returns null for an empty graph.
|
|
44
65
|
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
|
|
45
66
|
*/
|
|
46
|
-
declare function findLatestMigration(graph: MigrationGraph):
|
|
67
|
+
declare function findLatestMigration(graph: MigrationGraph): MigrationEdge | null;
|
|
47
68
|
declare function detectCycles(graph: MigrationGraph): readonly string[][];
|
|
48
|
-
declare function detectOrphans(graph: MigrationGraph): readonly
|
|
69
|
+
declare function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[];
|
|
49
70
|
//#endregion
|
|
50
|
-
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
51
|
-
//# sourceMappingURL=
|
|
71
|
+
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findPathWithInvariants, findReachableLeaves, reconstructGraph };
|
|
72
|
+
//# sourceMappingURL=migration-graph.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/migration-graph.ts"],"sourcesContent":[],"mappings":";;;;iBAsCgB,gBAAA,oBAAoC,qBAAqB;;AAAzE;AAkFA;AAgDA;;;;;AA0FiB,iBA1ID,QAAA,CA2IkB,KAAA,EA1IzB,cA0IsC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SAvInC,aAuImC,EAAA,GAAA,IAAA;AAY/C;AA8FA;AAkBA;AAwCA;AAQA;AA8DA;;;;;;;;;;;;;;iBArUgB,sBAAA,QACP,4DAGG,+BACA;UAqFK,YAAA;kCACiB;;;;;;;;;;;iBAYlB,oBAAA,QACP,qEAIN;;;;;iBAyFa,mBAAA,QAA2B;;;;;;;;;iBAkB3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAQ5C,YAAA,QAAoB;iBA8DpB,aAAA,QAAqB,0BAA0B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as EMPTY_CONTRACT_HASH } from "../constants-
|
|
1
|
+
import { _ as errorNoInitialMigration, a as errorDuplicateMigrationHash, b as errorSameSourceAndTarget, n as errorAmbiguousTarget, v as errorNoTarget } from "../errors-Bl3cKiM8.mjs";
|
|
2
|
+
import { t as EMPTY_CONTRACT_HASH } from "../constants-BQEHsaEx.mjs";
|
|
3
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
4
|
|
|
5
5
|
//#region src/queue.ts
|
|
@@ -36,61 +36,65 @@ var Queue = class {
|
|
|
36
36
|
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region src/graph-ops.ts
|
|
39
|
-
|
|
40
|
-
* Generic breadth-first traversal.
|
|
41
|
-
*
|
|
42
|
-
* Direction (forward/reverse) is expressed by the caller's `neighbours`
|
|
43
|
-
* closure: return `{ next, edge }` pairs where `next` is the node to visit
|
|
44
|
-
* next and `edge` is the edge that connects them. Callers that don't need
|
|
45
|
-
* path reconstruction can ignore the `parent`/`incomingEdge` fields of each
|
|
46
|
-
* yielded step.
|
|
47
|
-
*
|
|
48
|
-
* Stops are intrinsic — callers `break` out of the `for..of` loop when
|
|
49
|
-
* they've found what they're looking for.
|
|
50
|
-
*
|
|
51
|
-
* `ordering`, if provided, controls the order in which neighbours of each
|
|
52
|
-
* node are enqueued. Only matters for path-finding: a deterministic ordering
|
|
53
|
-
* makes BFS return a deterministic shortest path when multiple exist.
|
|
54
|
-
*/
|
|
55
|
-
function* bfs(starts, neighbours, ordering) {
|
|
39
|
+
function* bfs(starts, neighbours, key = (state) => state) {
|
|
56
40
|
const visited = /* @__PURE__ */ new Set();
|
|
57
41
|
const parentMap = /* @__PURE__ */ new Map();
|
|
58
42
|
const queue = new Queue();
|
|
59
|
-
for (const start of starts)
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
for (const start of starts) {
|
|
44
|
+
const k = key(start);
|
|
45
|
+
if (!visited.has(k)) {
|
|
46
|
+
visited.add(k);
|
|
47
|
+
queue.push({
|
|
48
|
+
state: start,
|
|
49
|
+
key: k
|
|
50
|
+
});
|
|
51
|
+
}
|
|
62
52
|
}
|
|
63
53
|
while (!queue.isEmpty) {
|
|
64
|
-
const current = queue.shift();
|
|
65
|
-
const parentInfo = parentMap.get(
|
|
54
|
+
const { state: current, key: curKey } = queue.shift();
|
|
55
|
+
const parentInfo = parentMap.get(curKey);
|
|
66
56
|
yield {
|
|
67
|
-
|
|
57
|
+
state: current,
|
|
68
58
|
parent: parentInfo?.parent ?? null,
|
|
69
59
|
incomingEdge: parentInfo?.edge ?? null
|
|
70
60
|
};
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
for (const { next, edge } of neighbours(current)) {
|
|
62
|
+
const k = key(next);
|
|
63
|
+
if (!visited.has(k)) {
|
|
64
|
+
visited.add(k);
|
|
65
|
+
parentMap.set(k, {
|
|
66
|
+
parent: current,
|
|
67
|
+
edge
|
|
68
|
+
});
|
|
69
|
+
queue.push({
|
|
70
|
+
state: next,
|
|
71
|
+
key: k
|
|
72
|
+
});
|
|
73
|
+
}
|
|
80
74
|
}
|
|
81
75
|
}
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
//#endregion
|
|
85
|
-
//#region src/
|
|
86
|
-
/** Forward-edge neighbours
|
|
79
|
+
//#region src/migration-graph.ts
|
|
80
|
+
/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */
|
|
87
81
|
function forwardNeighbours(graph, node) {
|
|
88
82
|
return (graph.forwardChain.get(node) ?? []).map((edge) => ({
|
|
89
83
|
next: edge.to,
|
|
90
84
|
edge
|
|
91
85
|
}));
|
|
92
86
|
}
|
|
93
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Forward-edge neighbours, sorted by the deterministic tie-break.
|
|
89
|
+
* Used by path-finding so the resulting shortest path is stable across runs.
|
|
90
|
+
*/
|
|
91
|
+
function sortedForwardNeighbours(graph, node) {
|
|
92
|
+
return [...graph.forwardChain.get(node) ?? []].sort(compareTieBreak).map((edge) => ({
|
|
93
|
+
next: edge.to,
|
|
94
|
+
edge
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */
|
|
94
98
|
function reverseNeighbours(graph, node) {
|
|
95
99
|
return (graph.reverseChain.get(node) ?? []).map((edge) => ({
|
|
96
100
|
next: edge.from,
|
|
@@ -106,22 +110,24 @@ function reconstructGraph(packages) {
|
|
|
106
110
|
const nodes = /* @__PURE__ */ new Set();
|
|
107
111
|
const forwardChain = /* @__PURE__ */ new Map();
|
|
108
112
|
const reverseChain = /* @__PURE__ */ new Map();
|
|
109
|
-
const
|
|
113
|
+
const migrationByHash = /* @__PURE__ */ new Map();
|
|
110
114
|
for (const pkg of packages) {
|
|
111
|
-
const
|
|
112
|
-
|
|
115
|
+
const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;
|
|
116
|
+
const { to } = pkg.metadata;
|
|
117
|
+
if (from === to) throw errorSameSourceAndTarget(pkg.dirPath, from);
|
|
113
118
|
nodes.add(from);
|
|
114
119
|
nodes.add(to);
|
|
115
120
|
const migration = {
|
|
116
121
|
from,
|
|
117
122
|
to,
|
|
118
|
-
|
|
123
|
+
migrationHash: pkg.metadata.migrationHash,
|
|
119
124
|
dirName: pkg.dirName,
|
|
120
|
-
createdAt: pkg.
|
|
121
|
-
labels: pkg.
|
|
125
|
+
createdAt: pkg.metadata.createdAt,
|
|
126
|
+
labels: pkg.metadata.labels,
|
|
127
|
+
invariants: pkg.metadata.providedInvariants
|
|
122
128
|
};
|
|
123
|
-
if (
|
|
124
|
-
|
|
129
|
+
if (migrationByHash.has(migration.migrationHash)) throw errorDuplicateMigrationHash(migration.migrationHash);
|
|
130
|
+
migrationByHash.set(migration.migrationHash, migration);
|
|
125
131
|
appendEdge(forwardChain, from, migration);
|
|
126
132
|
appendEdge(reverseChain, to, migration);
|
|
127
133
|
}
|
|
@@ -129,7 +135,7 @@ function reconstructGraph(packages) {
|
|
|
129
135
|
nodes,
|
|
130
136
|
forwardChain,
|
|
131
137
|
reverseChain,
|
|
132
|
-
|
|
138
|
+
migrationByHash
|
|
133
139
|
};
|
|
134
140
|
}
|
|
135
141
|
const LABEL_PRIORITY = {
|
|
@@ -152,32 +158,28 @@ function compareTieBreak(a, b) {
|
|
|
152
158
|
if (ca !== 0) return ca;
|
|
153
159
|
const tc = a.to.localeCompare(b.to);
|
|
154
160
|
if (tc !== 0) return tc;
|
|
155
|
-
return a.
|
|
161
|
+
return a.migrationHash.localeCompare(b.migrationHash);
|
|
156
162
|
}
|
|
157
163
|
function sortedNeighbors(edges) {
|
|
158
164
|
return [...edges].sort(compareTieBreak);
|
|
159
165
|
}
|
|
160
|
-
/** Ordering adapter for `bfs` — sorts `{next, edge}` pairs by tie-break. */
|
|
161
|
-
function bfsOrdering(items) {
|
|
162
|
-
return items.slice().sort((a, b) => compareTieBreak(a.edge, b.edge));
|
|
163
|
-
}
|
|
164
166
|
/**
|
|
165
167
|
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
166
168
|
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
167
169
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
168
170
|
*
|
|
169
171
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
170
|
-
* label priority → createdAt → to →
|
|
172
|
+
* label priority → createdAt → to → migrationHash.
|
|
171
173
|
*/
|
|
172
174
|
function findPath(graph, fromHash, toHash) {
|
|
173
175
|
if (fromHash === toHash) return [];
|
|
174
176
|
const parents = /* @__PURE__ */ new Map();
|
|
175
|
-
for (const step of bfs([fromHash], (n) =>
|
|
176
|
-
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.
|
|
177
|
+
for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {
|
|
178
|
+
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.state, {
|
|
177
179
|
parent: step.parent,
|
|
178
180
|
edge: step.incomingEdge
|
|
179
181
|
});
|
|
180
|
-
if (step.
|
|
182
|
+
if (step.state === toHash) {
|
|
181
183
|
const path = [];
|
|
182
184
|
let cur = toHash;
|
|
183
185
|
let p = parents.get(cur);
|
|
@@ -193,12 +195,90 @@ function findPath(graph, fromHash, toHash) {
|
|
|
193
195
|
return null;
|
|
194
196
|
}
|
|
195
197
|
/**
|
|
198
|
+
* Find the shortest path from `fromHash` to `toHash` whose edges collectively
|
|
199
|
+
* cover every invariant in `required`. Returns `null` when no such path exists
|
|
200
|
+
* (either `fromHash`→`toHash` is structurally unreachable, or every reachable
|
|
201
|
+
* path leaves at least one required invariant uncovered). When `required` is
|
|
202
|
+
* empty, delegates to `findPath` so the result is byte-identical for that case.
|
|
203
|
+
*
|
|
204
|
+
* Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.
|
|
205
|
+
* The covered subset is a `Set<string>` of invariant ids; the state's dedup
|
|
206
|
+
* key is `${node}\0${[...covered].sort().join('\0')}`. State keys distinguish
|
|
207
|
+
* distinct `(node, covered)` tuples regardless of node-name length because
|
|
208
|
+
* `\0` cannot appear in any invariant id (validation rejects whitespace and
|
|
209
|
+
* control chars at authoring time).
|
|
210
|
+
*
|
|
211
|
+
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
212
|
+
* invariant come first, with `labelPriority → createdAt → to → migrationHash`
|
|
213
|
+
* as the secondary key. The heuristic steers BFS toward the satisfying path;
|
|
214
|
+
* correctness (shortest, deterministic) does not depend on it.
|
|
215
|
+
*/
|
|
216
|
+
function findPathWithInvariants(graph, fromHash, toHash, required) {
|
|
217
|
+
if (required.size === 0) return findPath(graph, fromHash, toHash);
|
|
218
|
+
if (fromHash === toHash) return null;
|
|
219
|
+
const stateKey = (s) => {
|
|
220
|
+
if (s.covered.size === 0) return `${s.node}\0`;
|
|
221
|
+
return `${s.node}\0${[...s.covered].sort().join("\0")}`;
|
|
222
|
+
};
|
|
223
|
+
const neighbours = (s) => {
|
|
224
|
+
const outgoing = graph.forwardChain.get(s.node) ?? [];
|
|
225
|
+
if (outgoing.length === 0) return [];
|
|
226
|
+
return [...outgoing].map((edge) => {
|
|
227
|
+
let useful = false;
|
|
228
|
+
let next = null;
|
|
229
|
+
for (const inv of edge.invariants) if (required.has(inv) && !s.covered.has(inv)) {
|
|
230
|
+
if (next === null) next = new Set(s.covered);
|
|
231
|
+
next.add(inv);
|
|
232
|
+
useful = true;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
edge,
|
|
236
|
+
useful,
|
|
237
|
+
nextCovered: next ?? s.covered
|
|
238
|
+
};
|
|
239
|
+
}).sort((a, b) => {
|
|
240
|
+
if (a.useful !== b.useful) return a.useful ? -1 : 1;
|
|
241
|
+
return compareTieBreak(a.edge, b.edge);
|
|
242
|
+
}).map(({ edge, nextCovered }) => ({
|
|
243
|
+
next: {
|
|
244
|
+
node: edge.to,
|
|
245
|
+
covered: nextCovered
|
|
246
|
+
},
|
|
247
|
+
edge
|
|
248
|
+
}));
|
|
249
|
+
};
|
|
250
|
+
const parents = /* @__PURE__ */ new Map();
|
|
251
|
+
for (const step of bfs([{
|
|
252
|
+
node: fromHash,
|
|
253
|
+
covered: /* @__PURE__ */ new Set()
|
|
254
|
+
}], neighbours, stateKey)) {
|
|
255
|
+
const curKey = stateKey(step.state);
|
|
256
|
+
if (step.parent !== null && step.incomingEdge !== null) parents.set(curKey, {
|
|
257
|
+
parentKey: stateKey(step.parent),
|
|
258
|
+
edge: step.incomingEdge
|
|
259
|
+
});
|
|
260
|
+
if (step.state.node === toHash && step.state.covered.size === required.size) {
|
|
261
|
+
const path = [];
|
|
262
|
+
let cur = curKey;
|
|
263
|
+
while (cur !== void 0) {
|
|
264
|
+
const p = parents.get(cur);
|
|
265
|
+
if (!p) break;
|
|
266
|
+
path.push(p.edge);
|
|
267
|
+
cur = p.parentKey;
|
|
268
|
+
}
|
|
269
|
+
path.reverse();
|
|
270
|
+
return path;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
196
276
|
* Reverse-BFS from `toHash` over `reverseChain` to collect every node from
|
|
197
277
|
* which `toHash` is reachable (inclusive of `toHash` itself).
|
|
198
278
|
*/
|
|
199
279
|
function collectNodesReachingTarget(graph, toHash) {
|
|
200
280
|
const reached = /* @__PURE__ */ new Set();
|
|
201
|
-
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.
|
|
281
|
+
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.state);
|
|
202
282
|
return reached;
|
|
203
283
|
}
|
|
204
284
|
/**
|
|
@@ -226,8 +306,8 @@ function findPathWithDecision(graph, fromHash, toHash, refName) {
|
|
|
226
306
|
if (reachable.length > 1) {
|
|
227
307
|
alternativeCount += reachable.length - 1;
|
|
228
308
|
const sorted = sortedNeighbors(reachable);
|
|
229
|
-
if (sorted[0] && sorted[0].
|
|
230
|
-
if (reachable.some((e) => e.
|
|
309
|
+
if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {
|
|
310
|
+
if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) tieBreakReasons.push(`at ${edge.from}: ${reachable.length} candidates, selected by tie-break`);
|
|
231
311
|
}
|
|
232
312
|
}
|
|
233
313
|
}
|
|
@@ -248,7 +328,7 @@ function findPathWithDecision(graph, fromHash, toHash, refName) {
|
|
|
248
328
|
function findDivergencePoint(graph, fromHash, leaves) {
|
|
249
329
|
const ancestorSets = leaves.map((leaf) => {
|
|
250
330
|
const ancestors = /* @__PURE__ */ new Set();
|
|
251
|
-
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.
|
|
331
|
+
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.state);
|
|
252
332
|
return ancestors;
|
|
253
333
|
});
|
|
254
334
|
const commonAncestors = [...ancestorSets[0] ?? []].filter((node) => ancestorSets.every((s) => s.has(node)));
|
|
@@ -270,7 +350,7 @@ function findDivergencePoint(graph, fromHash, leaves) {
|
|
|
270
350
|
*/
|
|
271
351
|
function findReachableLeaves(graph, fromHash) {
|
|
272
352
|
const leaves = [];
|
|
273
|
-
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.
|
|
353
|
+
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.state)?.length) leaves.push(step.state);
|
|
274
354
|
return leaves;
|
|
275
355
|
}
|
|
276
356
|
/**
|
|
@@ -375,12 +455,12 @@ function detectOrphans(graph) {
|
|
|
375
455
|
for (const edges of graph.forwardChain.values()) for (const edge of edges) allTargets.add(edge.to);
|
|
376
456
|
for (const node of graph.nodes) if (!allTargets.has(node)) startNodes.push(node);
|
|
377
457
|
}
|
|
378
|
-
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.
|
|
458
|
+
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.state);
|
|
379
459
|
const orphans = [];
|
|
380
460
|
for (const [from, migrations] of graph.forwardChain) if (!reachable.has(from)) orphans.push(...migrations);
|
|
381
461
|
return orphans;
|
|
382
462
|
}
|
|
383
463
|
|
|
384
464
|
//#endregion
|
|
385
|
-
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
386
|
-
//# sourceMappingURL=
|
|
465
|
+
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findPathWithInvariants, findReachableLeaves, reconstructGraph };
|
|
466
|
+
//# sourceMappingURL=migration-graph.mjs.map
|