@prisma-next/migration-tools 0.3.0-dev.85 → 0.3.0-dev.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{errors-DdSjGRqx.mjs → errors-CqLiJwqA.mjs} +47 -9
- package/dist/errors-CqLiJwqA.mjs.map +1 -0
- package/dist/exports/attestation.d.mts +1 -1
- package/dist/exports/attestation.mjs +1 -1
- package/dist/exports/dag.d.mts +36 -17
- package/dist/exports/dag.d.mts.map +1 -1
- package/dist/exports/dag.mjs +206 -76
- package/dist/exports/dag.mjs.map +1 -1
- package/dist/exports/io.d.mts +3 -3
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +1 -1
- package/dist/exports/refs.d.mts +10 -0
- package/dist/exports/refs.d.mts.map +1 -0
- package/dist/exports/refs.mjs +73 -0
- package/dist/exports/refs.mjs.map +1 -0
- package/dist/exports/types.d.mts +2 -2
- package/dist/exports/types.mjs +13 -2
- package/dist/exports/types.mjs.map +1 -0
- package/dist/{io-Dx98-h0p.mjs → io-afog-e8J.mjs} +2 -3
- package/dist/io-afog-e8J.mjs.map +1 -0
- package/dist/types-9YQfIg6N.d.mts +96 -0
- package/dist/types-9YQfIg6N.d.mts.map +1 -0
- package/package.json +10 -6
- package/src/dag.ts +267 -107
- package/src/errors.ts +58 -7
- package/src/exports/dag.ts +3 -0
- package/src/exports/refs.ts +2 -0
- package/src/exports/types.ts +6 -1
- package/src/io.ts +4 -5
- package/src/refs.ts +102 -0
- package/src/types.ts +54 -7
- package/dist/errors-DdSjGRqx.mjs.map +0 -1
- package/dist/io-Dx98-h0p.mjs.map +0 -1
- package/dist/types-CUnzoaLY.d.mts +0 -56
- package/dist/types-CUnzoaLY.d.mts.map +0 -1
|
@@ -88,28 +88,66 @@ function errorSelfLoop(dirName, hash) {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
function errorAmbiguousLeaf(leaves) {
|
|
91
|
+
function errorAmbiguousLeaf(leaves, context) {
|
|
92
|
+
const divergenceInfo = context ? `\nDivergence point: ${context.divergencePoint}\nBranches:\n${context.branches.map((b) => ` → ${b.leaf} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(" → ") || "direct"})`).join("\n")}` : "";
|
|
92
93
|
return new MigrationToolsError("MIGRATION.AMBIGUOUS_LEAF", "Ambiguous migration graph", {
|
|
93
|
-
why: `Multiple leaf nodes found: ${leaves.join(", ")}. The migration graph has diverged — this typically happens when two developers plan migrations from the same starting point
|
|
94
|
-
fix: "
|
|
95
|
-
details: {
|
|
94
|
+
why: `Multiple leaf nodes found: ${leaves.join(", ")}. The migration graph has diverged — this typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,
|
|
95
|
+
fix: "Use `migration ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.",
|
|
96
|
+
details: {
|
|
97
|
+
leaves,
|
|
98
|
+
...context ? {
|
|
99
|
+
divergencePoint: context.divergencePoint,
|
|
100
|
+
branches: context.branches
|
|
101
|
+
} : {}
|
|
102
|
+
}
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
function errorNoRoot(nodes) {
|
|
99
106
|
return new MigrationToolsError("MIGRATION.NO_ROOT", "Migration graph has no root", {
|
|
100
|
-
why: `No root migration found in the migration graph (nodes: ${nodes.join(", ")}).
|
|
101
|
-
fix: "Inspect the migrations directory for corrupted migration.json files.
|
|
107
|
+
why: `No root migration found in the migration graph (nodes: ${nodes.join(", ")}). No migration starts from the empty contract hash, or all edges form a disconnected subgraph.`,
|
|
108
|
+
fix: "Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.",
|
|
102
109
|
details: { nodes }
|
|
103
110
|
});
|
|
104
111
|
}
|
|
112
|
+
function errorInvalidRefs(refsPath, reason) {
|
|
113
|
+
return new MigrationToolsError("MIGRATION.INVALID_REFS", "Invalid refs.json", {
|
|
114
|
+
why: `refs.json at "${refsPath}" is invalid: ${reason}`,
|
|
115
|
+
fix: "Ensure refs.json is a flat object mapping valid ref names to contract hash strings.",
|
|
116
|
+
details: {
|
|
117
|
+
path: refsPath,
|
|
118
|
+
reason
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function errorInvalidRefName(refName) {
|
|
123
|
+
return new MigrationToolsError("MIGRATION.INVALID_REF_NAME", "Invalid ref name", {
|
|
124
|
+
why: `Ref name "${refName}" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no "." or ".." segments).`,
|
|
125
|
+
fix: `Use a valid ref name (e.g., "staging", "envs/production").`,
|
|
126
|
+
details: { refName }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function errorNoResolvableLeaf(reachableNodes) {
|
|
130
|
+
return new MigrationToolsError("MIGRATION.NO_RESOLVABLE_LEAF", "Migration graph has no resolvable leaf", {
|
|
131
|
+
why: `The migration graph contains cycles and no node has zero outgoing edges (reachable nodes: ${reachableNodes.join(", ")}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,
|
|
132
|
+
fix: "Use --from <hash> to specify the planning origin explicitly.",
|
|
133
|
+
details: { reachableNodes }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function errorInvalidRefValue(value) {
|
|
137
|
+
return new MigrationToolsError("MIGRATION.INVALID_REF_VALUE", "Invalid ref value", {
|
|
138
|
+
why: `Ref value "${value}" is not a valid contract hash. Values must be in the format "sha256:<64 hex chars>" or "sha256:empty".`,
|
|
139
|
+
fix: "Use a valid storage hash from `prisma-next contract emit` output or an existing migration.",
|
|
140
|
+
details: { value }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
105
143
|
function errorDuplicateMigrationId(migrationId) {
|
|
106
144
|
return new MigrationToolsError("MIGRATION.DUPLICATE_MIGRATION_ID", "Duplicate migrationId in migration graph", {
|
|
107
|
-
why: `Multiple migrations share migrationId "${migrationId}".
|
|
145
|
+
why: `Multiple migrations share migrationId "${migrationId}". Each migration must have a unique content-addressed identity.`,
|
|
108
146
|
fix: "Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.",
|
|
109
147
|
details: { migrationId }
|
|
110
148
|
});
|
|
111
149
|
}
|
|
112
150
|
|
|
113
151
|
//#endregion
|
|
114
|
-
export { errorInvalidJson as a,
|
|
115
|
-
//# sourceMappingURL=errors-
|
|
152
|
+
export { errorInvalidJson as a, errorInvalidRefValue as c, errorMissingFile as d, errorNoResolvableLeaf as f, errorDuplicateMigrationId as i, errorInvalidRefs as l, errorSelfLoop as m, errorAmbiguousLeaf as n, errorInvalidManifest as o, errorNoRoot as p, errorDirectoryExists as r, errorInvalidRefName as s, MigrationToolsError as t, errorInvalidSlug as u };
|
|
153
|
+
//# sourceMappingURL=errors-CqLiJwqA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-CqLiJwqA.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,\n * migration-chain reconstruction), distinct from the runtime MIGRATION.* codes for apply-time\n * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: 'Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.',\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: 'Fix the JSON syntax error, or delete the migration directory and re-run migration plan.',\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.',\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorSelfLoop(dirName: string, hash: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.SELF_LOOP', 'Self-loop in migration graph', {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\". A migration must transition between two different contract states.`,\n fix: 'Delete the invalid migration directory and re-run migration plan.',\n details: { dirName, hash },\n });\n}\n\nexport function errorAmbiguousLeaf(\n leaves: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n leaf: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.leaf} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_LEAF', 'Ambiguous migration graph', {\n why: `Multiple leaf nodes found: ${leaves.join(', ')}. The migration graph has diverged — this typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `migration ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.',\n details: {\n leaves,\n ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),\n },\n });\n}\n\nexport function errorNoRoot(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_ROOT', 'Migration graph has no root', {\n why: `No root migration found in the migration graph (nodes: ${nodes.join(', ')}). No migration starts from the empty contract hash, or all edges form a disconnected subgraph.`,\n fix: 'Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.',\n details: { nodes },\n });\n}\n\nexport function errorInvalidRefs(refsPath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REFS', 'Invalid refs.json', {\n why: `refs.json at \"${refsPath}\" is invalid: ${reason}`,\n fix: 'Ensure refs.json is a flat object mapping valid ref names to contract hash strings.',\n details: { path: refsPath, reason },\n });\n}\n\nexport function errorInvalidRefName(refName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_NAME', 'Invalid ref name', {\n why: `Ref name \"${refName}\" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no \".\" or \"..\" segments).`,\n fix: `Use a valid ref name (e.g., \"staging\", \"envs/production\").`,\n details: { refName },\n });\n}\n\nexport function errorNoResolvableLeaf(reachableNodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.NO_RESOLVABLE_LEAF',\n 'Migration graph has no resolvable leaf',\n {\n why: `The migration graph contains cycles and no node has zero outgoing edges (reachable nodes: ${reachableNodes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,\n fix: 'Use --from <hash> to specify the planning origin explicitly.',\n details: { reachableNodes },\n },\n );\n}\n\nexport function errorInvalidRefValue(value: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_VALUE', 'Invalid ref value', {\n why: `Ref value \"${value}\" is not a valid contract hash. Values must be in the format \"sha256:<64 hex chars>\" or \"sha256:empty\".`,\n fix: 'Use a valid storage hash from `prisma-next contract emit` output or an existing migration.',\n details: { value },\n });\n}\n\nexport function errorDuplicateMigrationId(migrationId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_ID',\n 'Duplicate migrationId in migration graph',\n {\n why: `Multiple migrations share migrationId \"${migrationId}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.',\n details: { migrationId },\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,AAAS;CACT,AAAS,WAAW;CACpB,AAAS;CACT,AAAS;CACT,AAAS;CAET,YACE,MACA,SACA,SAKA;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,OAAK,MAAM,QAAQ;AACnB,OAAK,MAAM,QAAQ;AACnB,OAAK,UAAU,QAAQ;;CAGzB,OAAO,GAAG,OAA8C;AACtD,MAAI,EAAE,iBAAiB,OAAQ,QAAO;EACtC,MAAM,YAAY;AAClB,SAAO,UAAU,SAAS,yBAAyB,OAAO,UAAU,SAAS;;;AAIjF,SAAgB,qBAAqB,KAAkC;AACrE,QAAO,IAAI,oBAAoB,wBAAwB,sCAAsC;EAC3F,KAAK,kBAAkB,IAAI;EAC3B,KAAK;EACL,SAAS,EAAE,KAAK;EACjB,CAAC;;AAGJ,SAAgB,iBAAiB,MAAc,KAAkC;AAC/E,QAAO,IAAI,oBAAoB,0BAA0B,WAAW,QAAQ;EAC1E,KAAK,aAAa,KAAK,QAAQ,IAAI;EACnC,KAAK;EACL,SAAS;GAAE;GAAM;GAAK;EACvB,CAAC;;AAGJ,SAAgB,iBAAiB,UAAkB,YAAyC;AAC1F,QAAO,IAAI,oBAAoB,0BAA0B,kCAAkC;EACzF,KAAK,oBAAoB,SAAS,KAAK;EACvC,KAAK;EACL,SAAS;GAAE;GAAU;GAAY;EAClC,CAAC;;AAGJ,SAAgB,qBAAqB,UAAkB,QAAqC;AAC1F,QAAO,IAAI,oBAAoB,8BAA8B,8BAA8B;EACzF,KAAK,gBAAgB,SAAS,gBAAgB;EAC9C,KAAK;EACL,SAAS;GAAE;GAAU;GAAQ;EAC9B,CAAC;;AAGJ,SAAgB,iBAAiB,MAAmC;AAClE,QAAO,IAAI,oBAAoB,0BAA0B,0BAA0B;EACjF,KAAK,aAAa,KAAK;EACvB,KAAK;EACL,SAAS,EAAE,MAAM;EAClB,CAAC;;AAGJ,SAAgB,cAAc,SAAiB,MAAmC;AAChF,QAAO,IAAI,oBAAoB,uBAAuB,gCAAgC;EACpF,KAAK,cAAc,QAAQ,yBAAyB,KAAK;EACzD,KAAK;EACL,SAAS;GAAE;GAAS;GAAM;EAC3B,CAAC;;AAGJ,SAAgB,mBACd,QACA,SAOqB;CACrB,MAAM,iBAAiB,UACnB,uBAAuB,QAAQ,gBAAgB,eAAe,QAAQ,SAAS,KAAK,MAAM,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,OAAO,YAAY,EAAE,MAAM,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,MAAM,IAAI,SAAS,GAAG,CAAC,KAAK,KAAK,KAC3M;AACJ,QAAO,IAAI,oBAAoB,4BAA4B,6BAA6B;EACtF,KAAK,8BAA8B,OAAO,KAAK,KAAK,CAAC,+HAA+H;EACpL,KAAK;EACL,SAAS;GACP;GACA,GAAI,UAAU;IAAE,iBAAiB,QAAQ;IAAiB,UAAU,QAAQ;IAAU,GAAG,EAAE;GAC5F;EACF,CAAC;;AAGJ,SAAgB,YAAY,OAA+C;AACzE,QAAO,IAAI,oBAAoB,qBAAqB,+BAA+B;EACjF,KAAK,0DAA0D,MAAM,KAAK,KAAK,CAAC;EAChF,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAGJ,SAAgB,iBAAiB,UAAkB,QAAqC;AACtF,QAAO,IAAI,oBAAoB,0BAA0B,qBAAqB;EAC5E,KAAK,iBAAiB,SAAS,gBAAgB;EAC/C,KAAK;EACL,SAAS;GAAE,MAAM;GAAU;GAAQ;EACpC,CAAC;;AAGJ,SAAgB,oBAAoB,SAAsC;AACxE,QAAO,IAAI,oBAAoB,8BAA8B,oBAAoB;EAC/E,KAAK,aAAa,QAAQ;EAC1B,KAAK;EACL,SAAS,EAAE,SAAS;EACrB,CAAC;;AAGJ,SAAgB,sBAAsB,gBAAwD;AAC5F,QAAO,IAAI,oBACT,gCACA,0CACA;EACE,KAAK,6FAA6F,eAAe,KAAK,KAAK,CAAC;EAC5H,KAAK;EACL,SAAS,EAAE,gBAAgB;EAC5B,CACF;;AAGH,SAAgB,qBAAqB,OAAoC;AACvE,QAAO,IAAI,oBAAoB,+BAA+B,qBAAqB;EACjF,KAAK,cAAc,MAAM;EACzB,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAGJ,SAAgB,0BAA0B,aAA0C;AAClF,QAAO,IAAI,oBACT,oCACA,4CACA;EACE,KAAK,0CAA0C,YAAY;EAC3D,KAAK;EACL,SAAS,EAAE,aAAa;EACzB,CACF"}
|
package/dist/exports/dag.d.mts
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as MigrationChainEntry, o as MigrationGraph, t as AttestedMigrationBundle } from "../types-9YQfIg6N.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/dag.d.ts
|
|
4
|
-
declare function reconstructGraph(packages: readonly
|
|
4
|
+
declare function reconstructGraph(packages: readonly AttestedMigrationBundle[]): MigrationGraph;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Returns the
|
|
8
|
-
*
|
|
6
|
+
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
7
|
+
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
8
|
+
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
9
|
+
*
|
|
10
|
+
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
11
|
+
* label priority → createdAt → to → migrationId.
|
|
9
12
|
*/
|
|
10
|
-
declare function
|
|
13
|
+
declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly MigrationChainEntry[] | null;
|
|
14
|
+
interface PathDecision {
|
|
15
|
+
readonly selectedPath: readonly MigrationChainEntry[];
|
|
16
|
+
readonly fromHash: string;
|
|
17
|
+
readonly toHash: string;
|
|
18
|
+
readonly alternativeCount: number;
|
|
19
|
+
readonly tieBreakReasons: readonly string[];
|
|
20
|
+
readonly refName?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Find the shortest path from `fromHash` to `toHash` and return structured
|
|
24
|
+
* path-decision metadata for machine-readable output.
|
|
25
|
+
*/
|
|
26
|
+
declare function findPathWithDecision(graph: MigrationGraph, fromHash: string, toHash: string, refName?: string): PathDecision | null;
|
|
11
27
|
/**
|
|
12
|
-
* Find
|
|
13
|
-
*
|
|
28
|
+
* Find all leaf nodes reachable from `fromHash` via forward edges.
|
|
29
|
+
* A leaf is a node with no outgoing edges in the graph.
|
|
30
|
+
*/
|
|
31
|
+
declare function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Find the leaf contract hash of the migration graph reachable from
|
|
34
|
+
* EMPTY_CONTRACT_HASH. Throws NO_ROOT if the graph has nodes but none
|
|
35
|
+
* originate from the empty hash (e.g. root migration was deleted).
|
|
36
|
+
* Throws AMBIGUOUS_LEAF if multiple leaves exist.
|
|
14
37
|
*/
|
|
15
38
|
declare function findLeaf(graph: MigrationGraph): string;
|
|
16
39
|
/**
|
|
17
|
-
* Find the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* This reconstructs the full chain from root to leaf via parent pointers, then
|
|
22
|
-
* extracts the segment between the two hashes. This correctly handles revisited
|
|
23
|
-
* contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.
|
|
40
|
+
* Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
|
|
41
|
+
* to the single leaf. Returns null for an empty graph.
|
|
42
|
+
* Throws AMBIGUOUS_LEAF if the graph has multiple leaves.
|
|
24
43
|
*/
|
|
25
|
-
declare function
|
|
44
|
+
declare function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null;
|
|
26
45
|
declare function detectCycles(graph: MigrationGraph): readonly string[][];
|
|
27
46
|
declare function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[];
|
|
28
47
|
//#endregion
|
|
29
|
-
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, reconstructGraph };
|
|
48
|
+
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
30
49
|
//# sourceMappingURL=dag.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dag.d.mts","names":[],"sources":["../../src/dag.ts"],"sourcesContent":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"dag.d.mts","names":[],"sources":["../../src/dag.ts"],"sourcesContent":[],"mappings":";;;iBAWgB,gBAAA,oBAAoC,4BAA4B;;AAAhF;AAiFA;AA6CA;AAaA;AAqGA;AAgCA;AAwCA;AAkBgB,iBAzPA,QAAA,CAyPoB,KAAA,EAxP3B,cAwPyC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SArPtC,mBAqPsC,EAAA,GAAA,IAAA;AAiDlC,UA7PC,YAAA,CA6PY;kCA5PK;;;;;;;;;;;iBAYlB,oBAAA,QACP,qEAIN;;;;;iBAgGa,mBAAA,QAA2B;;;;;;;iBAgC3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAkB5C,YAAA,QAAoB;iBAiDpB,aAAA,QAAqB,0BAA0B"}
|
package/dist/exports/dag.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { i as errorDuplicateMigrationId,
|
|
1
|
+
import { f as errorNoResolvableLeaf, i as errorDuplicateMigrationId, m as errorSelfLoop, n as errorAmbiguousLeaf, p as errorNoRoot } from "../errors-CqLiJwqA.mjs";
|
|
2
2
|
import { EMPTY_CONTRACT_HASH } from "@prisma-next/core-control-plane/constants";
|
|
3
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
3
4
|
|
|
4
5
|
//#region src/dag.ts
|
|
5
6
|
function reconstructGraph(packages) {
|
|
@@ -7,7 +8,6 @@ function reconstructGraph(packages) {
|
|
|
7
8
|
const forwardChain = /* @__PURE__ */ new Map();
|
|
8
9
|
const reverseChain = /* @__PURE__ */ new Map();
|
|
9
10
|
const migrationById = /* @__PURE__ */ new Map();
|
|
10
|
-
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
11
11
|
for (const pkg of packages) {
|
|
12
12
|
const { from, to } = pkg.manifest;
|
|
13
13
|
if (from === to) throw errorSelfLoop(pkg.dirName, from);
|
|
@@ -17,7 +17,6 @@ function reconstructGraph(packages) {
|
|
|
17
17
|
from,
|
|
18
18
|
to,
|
|
19
19
|
migrationId: pkg.manifest.migrationId,
|
|
20
|
-
parentMigrationId: pkg.manifest.parentMigrationId,
|
|
21
20
|
dirName: pkg.dirName,
|
|
22
21
|
createdAt: pkg.manifest.createdAt,
|
|
23
22
|
labels: pkg.manifest.labels
|
|
@@ -26,10 +25,6 @@ function reconstructGraph(packages) {
|
|
|
26
25
|
if (migrationById.has(migration.migrationId)) throw errorDuplicateMigrationId(migration.migrationId);
|
|
27
26
|
migrationById.set(migration.migrationId, migration);
|
|
28
27
|
}
|
|
29
|
-
const parentId = migration.parentMigrationId;
|
|
30
|
-
const siblings = childrenByParentId.get(parentId);
|
|
31
|
-
if (siblings) siblings.push(migration);
|
|
32
|
-
else childrenByParentId.set(parentId, [migration]);
|
|
33
28
|
const fwd = forwardChain.get(from);
|
|
34
29
|
if (fwd) fwd.push(migration);
|
|
35
30
|
else forwardChain.set(from, [migration]);
|
|
@@ -41,91 +36,221 @@ function reconstructGraph(packages) {
|
|
|
41
36
|
nodes,
|
|
42
37
|
forwardChain,
|
|
43
38
|
reverseChain,
|
|
44
|
-
migrationById
|
|
45
|
-
childrenByParentId
|
|
39
|
+
migrationById
|
|
46
40
|
};
|
|
47
41
|
}
|
|
42
|
+
const LABEL_PRIORITY = {
|
|
43
|
+
main: 0,
|
|
44
|
+
default: 1,
|
|
45
|
+
feature: 2
|
|
46
|
+
};
|
|
47
|
+
function labelPriority(labels) {
|
|
48
|
+
let best = 3;
|
|
49
|
+
for (const l of labels) {
|
|
50
|
+
const p = LABEL_PRIORITY[l];
|
|
51
|
+
if (p !== void 0 && p < best) best = p;
|
|
52
|
+
}
|
|
53
|
+
return best;
|
|
54
|
+
}
|
|
55
|
+
function sortedNeighbors(edges) {
|
|
56
|
+
return [...edges].sort((a, b) => {
|
|
57
|
+
const lp = labelPriority(a.labels) - labelPriority(b.labels);
|
|
58
|
+
if (lp !== 0) return lp;
|
|
59
|
+
const ca = a.createdAt.localeCompare(b.createdAt);
|
|
60
|
+
if (ca !== 0) return ca;
|
|
61
|
+
const tc = a.to.localeCompare(b.to);
|
|
62
|
+
if (tc !== 0) return tc;
|
|
63
|
+
return (a.migrationId ?? "").localeCompare(b.migrationId ?? "");
|
|
64
|
+
});
|
|
65
|
+
}
|
|
48
66
|
/**
|
|
49
|
-
*
|
|
50
|
-
* Returns the
|
|
51
|
-
*
|
|
67
|
+
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
68
|
+
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
69
|
+
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
70
|
+
*
|
|
71
|
+
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
72
|
+
* label priority → createdAt → to → migrationId.
|
|
52
73
|
*/
|
|
53
|
-
function
|
|
54
|
-
if (
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
function findPath(graph, fromHash, toHash) {
|
|
75
|
+
if (fromHash === toHash) return [];
|
|
76
|
+
const visited = /* @__PURE__ */ new Set();
|
|
77
|
+
const parent = /* @__PURE__ */ new Map();
|
|
78
|
+
const queue = [fromHash];
|
|
79
|
+
visited.add(fromHash);
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const current = queue.shift();
|
|
82
|
+
if (current === void 0) break;
|
|
83
|
+
if (current === toHash) {
|
|
84
|
+
const path = [];
|
|
85
|
+
let node = toHash;
|
|
86
|
+
let entry = parent.get(node);
|
|
87
|
+
while (entry) {
|
|
88
|
+
const { node: prev, edge } = entry;
|
|
89
|
+
path.push(edge);
|
|
90
|
+
node = prev;
|
|
91
|
+
entry = parent.get(node);
|
|
92
|
+
}
|
|
93
|
+
path.reverse();
|
|
94
|
+
return path;
|
|
95
|
+
}
|
|
96
|
+
const outgoing = graph.forwardChain.get(current);
|
|
97
|
+
if (!outgoing) continue;
|
|
98
|
+
for (const edge of sortedNeighbors(outgoing)) if (!visited.has(edge.to)) {
|
|
99
|
+
visited.add(edge.to);
|
|
100
|
+
parent.set(edge.to, {
|
|
101
|
+
node: current,
|
|
102
|
+
edge
|
|
103
|
+
});
|
|
104
|
+
queue.push(edge.to);
|
|
105
|
+
}
|
|
65
106
|
}
|
|
66
|
-
|
|
107
|
+
return null;
|
|
67
108
|
}
|
|
68
109
|
/**
|
|
69
|
-
* Find the
|
|
70
|
-
*
|
|
110
|
+
* Find the shortest path from `fromHash` to `toHash` and return structured
|
|
111
|
+
* path-decision metadata for machine-readable output.
|
|
71
112
|
*/
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
function findPathWithDecision(graph, fromHash, toHash, refName) {
|
|
114
|
+
if (fromHash === toHash) return {
|
|
115
|
+
selectedPath: [],
|
|
116
|
+
fromHash,
|
|
117
|
+
toHash,
|
|
118
|
+
alternativeCount: 0,
|
|
119
|
+
tieBreakReasons: [],
|
|
120
|
+
...ifDefined("refName", refName)
|
|
121
|
+
};
|
|
122
|
+
const path = findPath(graph, fromHash, toHash);
|
|
123
|
+
if (!path) return null;
|
|
124
|
+
const tieBreakReasons = [];
|
|
125
|
+
let alternativeCount = 0;
|
|
126
|
+
for (const edge of path) {
|
|
127
|
+
const outgoing = graph.forwardChain.get(edge.from);
|
|
128
|
+
if (outgoing && outgoing.length > 1) {
|
|
129
|
+
const reachable = outgoing.filter((e) => {
|
|
130
|
+
return findPath(graph, e.to, toHash) !== null || e.to === toHash;
|
|
131
|
+
});
|
|
132
|
+
if (reachable.length > 1) {
|
|
133
|
+
alternativeCount += reachable.length - 1;
|
|
134
|
+
const sorted = sortedNeighbors(reachable);
|
|
135
|
+
if (sorted[0] && sorted[0].migrationId === edge.migrationId) {
|
|
136
|
+
if (reachable.some((e) => e.migrationId !== edge.migrationId)) tieBreakReasons.push(`at ${edge.from}: ${reachable.length} candidates, selected by tie-break`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
selectedPath: path,
|
|
143
|
+
fromHash,
|
|
144
|
+
toHash,
|
|
145
|
+
alternativeCount,
|
|
146
|
+
tieBreakReasons,
|
|
147
|
+
...ifDefined("refName", refName)
|
|
148
|
+
};
|
|
75
149
|
}
|
|
76
150
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* goes from `fromHash` to `toHash`.
|
|
80
|
-
*
|
|
81
|
-
* This reconstructs the full chain from root to leaf via parent pointers, then
|
|
82
|
-
* extracts the segment between the two hashes. This correctly handles revisited
|
|
83
|
-
* contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.
|
|
151
|
+
* Walk ancestors of each leaf back from the leaves to find the last node
|
|
152
|
+
* that appears on all paths. Returns `fromHash` if no shared ancestor is found.
|
|
84
153
|
*/
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
154
|
+
function findDivergencePoint(graph, fromHash, leaves) {
|
|
155
|
+
const ancestorSets = leaves.map((leaf) => {
|
|
156
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
157
|
+
const queue = [leaf];
|
|
158
|
+
while (queue.length > 0) {
|
|
159
|
+
const current = queue.shift();
|
|
160
|
+
if (ancestors.has(current)) continue;
|
|
161
|
+
ancestors.add(current);
|
|
162
|
+
const incoming = graph.reverseChain.get(current);
|
|
163
|
+
if (incoming) for (const edge of incoming) queue.push(edge.from);
|
|
164
|
+
}
|
|
165
|
+
return ancestors;
|
|
166
|
+
});
|
|
167
|
+
const commonAncestors = [...ancestorSets[0] ?? []].filter((node) => ancestorSets.every((s) => s.has(node)));
|
|
168
|
+
let deepest = fromHash;
|
|
169
|
+
let deepestDepth = -1;
|
|
170
|
+
for (const ancestor of commonAncestors) {
|
|
171
|
+
const path = findPath(graph, fromHash, ancestor);
|
|
172
|
+
const depth = path ? path.length : 0;
|
|
173
|
+
if (depth > deepestDepth) {
|
|
174
|
+
deepestDepth = depth;
|
|
175
|
+
deepest = ancestor;
|
|
176
|
+
}
|
|
94
177
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
178
|
+
return deepest;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Find all leaf nodes reachable from `fromHash` via forward edges.
|
|
182
|
+
* A leaf is a node with no outgoing edges in the graph.
|
|
183
|
+
*/
|
|
184
|
+
function findReachableLeaves(graph, fromHash) {
|
|
185
|
+
const visited = /* @__PURE__ */ new Set();
|
|
186
|
+
const queue = [fromHash];
|
|
187
|
+
visited.add(fromHash);
|
|
188
|
+
const leaves = [];
|
|
189
|
+
while (queue.length > 0) {
|
|
190
|
+
const current = queue.shift();
|
|
191
|
+
if (current === void 0) break;
|
|
192
|
+
const outgoing = graph.forwardChain.get(current);
|
|
193
|
+
if (!outgoing || outgoing.length === 0) leaves.push(current);
|
|
194
|
+
else for (const edge of outgoing) if (!visited.has(edge.to)) {
|
|
195
|
+
visited.add(edge.to);
|
|
196
|
+
queue.push(edge.to);
|
|
197
|
+
}
|
|
100
198
|
}
|
|
101
|
-
|
|
102
|
-
return chain.slice(startIdx, endIdx);
|
|
199
|
+
return leaves;
|
|
103
200
|
}
|
|
104
201
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* (e.g.
|
|
202
|
+
* Find the leaf contract hash of the migration graph reachable from
|
|
203
|
+
* EMPTY_CONTRACT_HASH. Throws NO_ROOT if the graph has nodes but none
|
|
204
|
+
* originate from the empty hash (e.g. root migration was deleted).
|
|
205
|
+
* Throws AMBIGUOUS_LEAF if multiple leaves exist.
|
|
108
206
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
if (!
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
207
|
+
function findLeaf(graph) {
|
|
208
|
+
if (graph.nodes.size === 0) return EMPTY_CONTRACT_HASH;
|
|
209
|
+
if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) throw errorNoRoot([...graph.nodes]);
|
|
210
|
+
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
|
|
211
|
+
if (leaves.length === 0) {
|
|
212
|
+
const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);
|
|
213
|
+
if (reachable.length > 0) throw errorNoResolvableLeaf(reachable);
|
|
214
|
+
return EMPTY_CONTRACT_HASH;
|
|
215
|
+
}
|
|
216
|
+
if (leaves.length > 1) {
|
|
217
|
+
const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);
|
|
218
|
+
throw errorAmbiguousLeaf(leaves, {
|
|
219
|
+
divergencePoint,
|
|
220
|
+
branches: leaves.map((leaf$1) => {
|
|
221
|
+
return {
|
|
222
|
+
leaf: leaf$1,
|
|
223
|
+
edges: (findPath(graph, divergencePoint, leaf$1) ?? []).map((e) => ({
|
|
224
|
+
dirName: e.dirName,
|
|
225
|
+
from: e.from,
|
|
226
|
+
to: e.to
|
|
227
|
+
}))
|
|
228
|
+
};
|
|
229
|
+
})
|
|
230
|
+
});
|
|
120
231
|
}
|
|
121
|
-
|
|
232
|
+
const leaf = leaves[0];
|
|
233
|
+
return leaf !== void 0 ? leaf : EMPTY_CONTRACT_HASH;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
|
|
237
|
+
* to the single leaf. Returns null for an empty graph.
|
|
238
|
+
* Throws AMBIGUOUS_LEAF if the graph has multiple leaves.
|
|
239
|
+
*/
|
|
240
|
+
function findLatestMigration(graph) {
|
|
241
|
+
if (graph.nodes.size === 0) return null;
|
|
242
|
+
const leafHash = findLeaf(graph);
|
|
243
|
+
if (leafHash === EMPTY_CONTRACT_HASH) return null;
|
|
244
|
+
const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);
|
|
245
|
+
if (!path || path.length === 0) return null;
|
|
246
|
+
return path[path.length - 1] ?? null;
|
|
122
247
|
}
|
|
123
248
|
function detectCycles(graph) {
|
|
124
249
|
const WHITE = 0;
|
|
125
250
|
const GRAY = 1;
|
|
126
251
|
const BLACK = 2;
|
|
127
252
|
const color = /* @__PURE__ */ new Map();
|
|
128
|
-
const
|
|
253
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
129
254
|
const cycles = [];
|
|
130
255
|
for (const node of graph.nodes) color.set(node, WHITE);
|
|
131
256
|
function dfs(u) {
|
|
@@ -138,19 +263,19 @@ function detectCycles(graph) {
|
|
|
138
263
|
let cur = u;
|
|
139
264
|
while (cur !== v) {
|
|
140
265
|
cycle.push(cur);
|
|
141
|
-
cur =
|
|
266
|
+
cur = parentMap.get(cur) ?? v;
|
|
142
267
|
}
|
|
143
268
|
cycle.reverse();
|
|
144
269
|
cycles.push(cycle);
|
|
145
270
|
} else if (color.get(v) === WHITE) {
|
|
146
|
-
|
|
271
|
+
parentMap.set(v, u);
|
|
147
272
|
dfs(v);
|
|
148
273
|
}
|
|
149
274
|
}
|
|
150
275
|
color.set(u, BLACK);
|
|
151
276
|
}
|
|
152
277
|
for (const node of graph.nodes) if (color.get(node) === WHITE) {
|
|
153
|
-
|
|
278
|
+
parentMap.set(node, null);
|
|
154
279
|
dfs(node);
|
|
155
280
|
}
|
|
156
281
|
return cycles;
|
|
@@ -158,9 +283,14 @@ function detectCycles(graph) {
|
|
|
158
283
|
function detectOrphans(graph) {
|
|
159
284
|
if (graph.nodes.size === 0) return [];
|
|
160
285
|
const reachable = /* @__PURE__ */ new Set();
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
286
|
+
const startNodes = [];
|
|
287
|
+
if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) startNodes.push(EMPTY_CONTRACT_HASH);
|
|
288
|
+
else {
|
|
289
|
+
const allTargets = /* @__PURE__ */ new Set();
|
|
290
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) allTargets.add(edge.to);
|
|
291
|
+
for (const node of graph.nodes) if (!allTargets.has(node)) startNodes.push(node);
|
|
292
|
+
}
|
|
293
|
+
const queue = [...startNodes];
|
|
164
294
|
for (const hash of queue) reachable.add(hash);
|
|
165
295
|
while (queue.length > 0) {
|
|
166
296
|
const node = queue.shift();
|
|
@@ -178,5 +308,5 @@ function detectOrphans(graph) {
|
|
|
178
308
|
}
|
|
179
309
|
|
|
180
310
|
//#endregion
|
|
181
|
-
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, reconstructGraph };
|
|
311
|
+
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
182
312
|
//# sourceMappingURL=dag.mjs.map
|
package/dist/exports/dag.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dag.mjs","names":["migration: MigrationChainEntry","children: readonly MigrationChainEntry[] | undefined","chain: MigrationChainEntry[]","current: MigrationChainEntry | undefined","cycles: string[][]","cycle: string[]","queue: string[]","orphans: MigrationChainEntry[]"],"sources":["../../src/dag.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';\nimport {\n errorAmbiguousLeaf,\n errorDuplicateMigrationId,\n errorNoRoot,\n errorSelfLoop,\n} from './errors';\nimport type { MigrationChainEntry, MigrationGraph, MigrationPackage } from './types';\n\nexport function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationChainEntry[]>();\n const reverseChain = new Map<string, MigrationChainEntry[]>();\n const migrationById = new Map<string, MigrationChainEntry>();\n const childrenByParentId = new Map<string | null, MigrationChainEntry[]>();\n\n for (const pkg of packages) {\n const { from, to } = pkg.manifest;\n\n if (from === to) {\n throw errorSelfLoop(pkg.dirName, from);\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationChainEntry = {\n from,\n to,\n migrationId: pkg.manifest.migrationId,\n parentMigrationId: pkg.manifest.parentMigrationId,\n dirName: pkg.dirName,\n createdAt: pkg.manifest.createdAt,\n labels: pkg.manifest.labels,\n };\n\n if (migration.migrationId !== null) {\n if (migrationById.has(migration.migrationId)) {\n throw errorDuplicateMigrationId(migration.migrationId);\n }\n migrationById.set(migration.migrationId, migration);\n }\n\n const parentId = migration.parentMigrationId;\n const siblings = childrenByParentId.get(parentId);\n if (siblings) {\n siblings.push(migration);\n } else {\n childrenByParentId.set(parentId, [migration]);\n }\n\n const fwd = forwardChain.get(from);\n if (fwd) {\n fwd.push(migration);\n } else {\n forwardChain.set(from, [migration]);\n }\n\n const rev = reverseChain.get(to);\n if (rev) {\n rev.push(migration);\n } else {\n reverseChain.set(to, [migration]);\n }\n }\n\n return { nodes, forwardChain, reverseChain, migrationById, childrenByParentId };\n}\n\n/**\n * Walk the parent-migration chain to find the latest migration.\n * Returns the migration with no children, or null for an empty graph.\n * Throws AMBIGUOUS_LEAF if the chain branches.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n const roots = graph.childrenByParentId.get(null);\n if (!roots || roots.length === 0) {\n throw errorNoRoot([...graph.nodes].sort());\n }\n\n if (roots.length > 1) {\n throw errorAmbiguousLeaf(roots.map((e) => e.to));\n }\n\n let current = roots[0];\n if (!current) {\n throw errorNoRoot([...graph.nodes].sort());\n }\n\n for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {\n const children: readonly MigrationChainEntry[] | undefined =\n current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : undefined;\n\n if (!children || children.length === 0) {\n return current;\n }\n\n if (children.length > 1) {\n throw errorAmbiguousLeaf(children.map((e) => e.to));\n }\n\n current = children[0];\n }\n\n throw errorNoRoot([...graph.nodes].sort());\n}\n\n/**\n * Find the leaf contract hash of the migration chain.\n * Convenience wrapper around findLatestMigration.\n */\nexport function findLeaf(graph: MigrationGraph): string {\n const migration = findLatestMigration(graph);\n return migration ? migration.to : EMPTY_CONTRACT_HASH;\n}\n\n/**\n * Find the ordered chain of migrations from `fromHash` to `toHash` by walking the\n * parent-migration chain. Returns the sub-sequence of migrations whose cumulative path\n * goes from `fromHash` to `toHash`.\n *\n * This reconstructs the full chain from root to leaf via parent pointers, then\n * extracts the segment between the two hashes. This correctly handles revisited\n * contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationChainEntry[] | null {\n if (fromHash === toHash) return [];\n\n const chain = buildChain(graph);\n if (!chain) return null;\n\n let startIdx = -1;\n if (chain.length > 0 && chain[0]?.from === fromHash) {\n startIdx = 0;\n } else {\n for (let i = chain.length - 1; i >= 0; i--) {\n if (chain[i]?.to === fromHash) {\n startIdx = i + 1;\n break;\n }\n }\n }\n\n if (startIdx === -1) return null;\n\n let endIdx = -1;\n for (let i = chain.length - 1; i >= startIdx; i--) {\n if (chain[i]?.to === toHash) {\n endIdx = i + 1;\n break;\n }\n }\n\n if (endIdx === -1) return null;\n\n return chain.slice(startIdx, endIdx);\n}\n\n/**\n * Build the full ordered chain of migrations from root to leaf by following\n * parent pointers. Returns null if the chain cannot be reconstructed\n * (e.g. missing root, branches).\n */\nfunction buildChain(graph: MigrationGraph): readonly MigrationChainEntry[] | null {\n const roots = graph.childrenByParentId.get(null);\n if (!roots || roots.length !== 1) return null;\n\n const chain: MigrationChainEntry[] = [];\n let current: MigrationChainEntry | undefined = roots[0];\n\n for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {\n chain.push(current);\n const children =\n current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : undefined;\n if (!children || children.length === 0) break;\n if (children.length > 1) return null;\n current = children[0];\n }\n\n return chain;\n}\n\nexport function detectCycles(graph: MigrationGraph): readonly string[][] {\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n\n const color = new Map<string, number>();\n const parent = new Map<string, string | null>();\n const cycles: string[][] = [];\n\n for (const node of graph.nodes) {\n color.set(node, WHITE);\n }\n\n function dfs(u: string): void {\n color.set(u, GRAY);\n\n const outgoing = graph.forwardChain.get(u);\n if (outgoing) {\n for (const edge of outgoing) {\n const v = edge.to;\n if (color.get(v) === GRAY) {\n // Back edge found — reconstruct cycle\n const cycle: string[] = [v];\n let cur = u;\n while (cur !== v) {\n cycle.push(cur);\n cur = parent.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (color.get(v) === WHITE) {\n parent.set(v, u);\n dfs(v);\n }\n }\n }\n\n color.set(u, BLACK);\n }\n\n for (const node of graph.nodes) {\n if (color.get(node) === WHITE) {\n parent.set(node, null);\n dfs(node);\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const rootMigrations = graph.childrenByParentId.get(null) ?? [];\n const emptyRootExists = rootMigrations.some(\n (migration) => migration.from === EMPTY_CONTRACT_HASH,\n );\n const rootHashes = emptyRootExists\n ? [EMPTY_CONTRACT_HASH]\n : [...new Set(rootMigrations.map((migration) => migration.from))];\n const queue: string[] = rootHashes.length > 0 ? rootHashes : [EMPTY_CONTRACT_HASH];\n\n for (const hash of queue) {\n reachable.add(hash);\n }\n\n while (queue.length > 0) {\n const node = queue.shift();\n if (node === undefined) break;\n const outgoing = graph.forwardChain.get(node);\n if (!outgoing) continue;\n\n for (const migration of outgoing) {\n if (!reachable.has(migration.to)) {\n reachable.add(migration.to);\n queue.push(migration.to);\n }\n }\n }\n\n const orphans: MigrationChainEntry[] = [];\n for (const [from, migrations] of graph.forwardChain) {\n if (!reachable.has(from)) {\n orphans.push(...migrations);\n }\n }\n\n return orphans;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dag.mjs","names":["migration: MigrationChainEntry","LABEL_PRIORITY: Record<string, number>","queue: string[]","path: MigrationChainEntry[]","tieBreakReasons: string[]","leaves: string[]","leaf","cycles: string[][]","cycle: string[]","startNodes: string[]","orphans: MigrationChainEntry[]"],"sources":["../../src/dag.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport {\n errorAmbiguousLeaf,\n errorDuplicateMigrationId,\n errorNoResolvableLeaf,\n errorNoRoot,\n errorSelfLoop,\n} from './errors';\nimport type { AttestedMigrationBundle, MigrationChainEntry, MigrationGraph } from './types';\n\nexport function reconstructGraph(packages: readonly AttestedMigrationBundle[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationChainEntry[]>();\n const reverseChain = new Map<string, MigrationChainEntry[]>();\n const migrationById = new Map<string, MigrationChainEntry>();\n\n for (const pkg of packages) {\n const { from, to } = pkg.manifest;\n\n if (from === to) {\n throw errorSelfLoop(pkg.dirName, from);\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationChainEntry = {\n from,\n to,\n migrationId: pkg.manifest.migrationId,\n dirName: pkg.dirName,\n createdAt: pkg.manifest.createdAt,\n labels: pkg.manifest.labels,\n };\n\n if (migration.migrationId !== null) {\n if (migrationById.has(migration.migrationId)) {\n throw errorDuplicateMigrationId(migration.migrationId);\n }\n migrationById.set(migration.migrationId, migration);\n }\n\n const fwd = forwardChain.get(from);\n if (fwd) {\n fwd.push(migration);\n } else {\n forwardChain.set(from, [migration]);\n }\n\n const rev = reverseChain.get(to);\n if (rev) {\n rev.push(migration);\n } else {\n reverseChain.set(to, [migration]);\n }\n }\n\n return { nodes, forwardChain, reverseChain, migrationById };\n}\n\nconst LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };\n\nfunction labelPriority(labels: readonly string[]): number {\n let best = 3;\n for (const l of labels) {\n const p = LABEL_PRIORITY[l];\n if (p !== undefined && p < best) best = p;\n }\n return best;\n}\n\nfunction sortedNeighbors(edges: readonly MigrationChainEntry[]): readonly MigrationChainEntry[] {\n return [...edges].sort((a, b) => {\n const lp = labelPriority(a.labels) - labelPriority(b.labels);\n if (lp !== 0) return lp;\n const ca = a.createdAt.localeCompare(b.createdAt);\n if (ca !== 0) return ca;\n const tc = a.to.localeCompare(b.to);\n if (tc !== 0) return tc;\n return (a.migrationId ?? '').localeCompare(b.migrationId ?? '');\n });\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` using BFS over the\n * contract-hash graph. Returns the ordered list of edges, or null if no path\n * exists. Returns an empty array when `fromHash === toHash` (no-op).\n *\n * Neighbor ordering is deterministic via the tie-break sort key:\n * label priority → createdAt → to → migrationId.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationChainEntry[] | null {\n if (fromHash === toHash) return [];\n\n const visited = new Set<string>();\n const parent = new Map<string, { node: string; edge: MigrationChainEntry }>();\n const queue: string[] = [fromHash];\n visited.add(fromHash);\n\n while (queue.length > 0) {\n const current = queue.shift();\n if (current === undefined) break;\n\n if (current === toHash) {\n const path: MigrationChainEntry[] = [];\n let node = toHash;\n let entry = parent.get(node);\n while (entry) {\n const { node: prev, edge } = entry;\n path.push(edge);\n node = prev;\n entry = parent.get(node);\n }\n path.reverse();\n return path;\n }\n\n const outgoing = graph.forwardChain.get(current);\n if (!outgoing) continue;\n\n for (const edge of sortedNeighbors(outgoing)) {\n if (!visited.has(edge.to)) {\n visited.add(edge.to);\n parent.set(edge.to, { node: current, edge });\n queue.push(edge.to);\n }\n }\n }\n\n return null;\n}\n\nexport interface PathDecision {\n readonly selectedPath: readonly MigrationChainEntry[];\n readonly fromHash: string;\n readonly toHash: string;\n readonly alternativeCount: number;\n readonly tieBreakReasons: readonly string[];\n readonly refName?: string;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` and return structured\n * path-decision metadata for machine-readable output.\n */\nexport function findPathWithDecision(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n refName?: string,\n): PathDecision | null {\n if (fromHash === toHash) {\n return {\n selectedPath: [],\n fromHash,\n toHash,\n alternativeCount: 0,\n tieBreakReasons: [],\n ...ifDefined('refName', refName),\n };\n }\n\n const path = findPath(graph, fromHash, toHash);\n if (!path) return null;\n\n const tieBreakReasons: string[] = [];\n let alternativeCount = 0;\n\n for (const edge of path) {\n const outgoing = graph.forwardChain.get(edge.from);\n if (outgoing && outgoing.length > 1) {\n const reachable = outgoing.filter((e) => {\n const pathFromE = findPath(graph, e.to, toHash);\n return pathFromE !== null || e.to === toHash;\n });\n if (reachable.length > 1) {\n alternativeCount += reachable.length - 1;\n const sorted = sortedNeighbors(reachable);\n if (sorted[0] && sorted[0].migrationId === edge.migrationId) {\n if (reachable.some((e) => e.migrationId !== edge.migrationId)) {\n tieBreakReasons.push(\n `at ${edge.from}: ${reachable.length} candidates, selected by tie-break`,\n );\n }\n }\n }\n }\n }\n\n return {\n selectedPath: path,\n fromHash,\n toHash,\n alternativeCount,\n tieBreakReasons,\n ...ifDefined('refName', refName),\n };\n}\n\n/**\n * Walk ancestors of each leaf back from the leaves to find the last node\n * that appears on all paths. Returns `fromHash` if no shared ancestor is found.\n */\nfunction findDivergencePoint(\n graph: MigrationGraph,\n fromHash: string,\n leaves: readonly string[],\n): string {\n const ancestorSets = leaves.map((leaf) => {\n const ancestors = new Set<string>();\n const queue = [leaf];\n while (queue.length > 0) {\n const current = queue.shift() as string;\n if (ancestors.has(current)) continue;\n ancestors.add(current);\n const incoming = graph.reverseChain.get(current);\n if (incoming) {\n for (const edge of incoming) {\n queue.push(edge.from);\n }\n }\n }\n return ancestors;\n });\n\n const commonAncestors = [...(ancestorSets[0] ?? [])].filter((node) =>\n ancestorSets.every((s) => s.has(node)),\n );\n\n let deepest = fromHash;\n let deepestDepth = -1;\n for (const ancestor of commonAncestors) {\n const path = findPath(graph, fromHash, ancestor);\n const depth = path ? path.length : 0;\n if (depth > deepestDepth) {\n deepestDepth = depth;\n deepest = ancestor;\n }\n }\n return deepest;\n}\n\n/**\n * Find all leaf nodes reachable from `fromHash` via forward edges.\n * A leaf is a node with no outgoing edges in the graph.\n */\nexport function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {\n const visited = new Set<string>();\n const queue: string[] = [fromHash];\n visited.add(fromHash);\n const leaves: string[] = [];\n\n while (queue.length > 0) {\n const current = queue.shift();\n if (current === undefined) break;\n const outgoing = graph.forwardChain.get(current);\n\n if (!outgoing || outgoing.length === 0) {\n leaves.push(current);\n } else {\n for (const edge of outgoing) {\n if (!visited.has(edge.to)) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n }\n\n return leaves;\n}\n\n/**\n * Find the leaf contract hash of the migration graph reachable from\n * EMPTY_CONTRACT_HASH. Throws NO_ROOT if the graph has nodes but none\n * originate from the empty hash (e.g. root migration was deleted).\n * Throws AMBIGUOUS_LEAF if multiple leaves exist.\n */\nexport function findLeaf(graph: MigrationGraph): string {\n if (graph.nodes.size === 0) {\n return EMPTY_CONTRACT_HASH;\n }\n\n if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {\n throw errorNoRoot([...graph.nodes]);\n }\n\n const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);\n\n if (leaves.length === 0) {\n const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);\n if (reachable.length > 0) {\n throw errorNoResolvableLeaf(reachable);\n }\n return EMPTY_CONTRACT_HASH;\n }\n\n if (leaves.length > 1) {\n const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);\n const branches = leaves.map((leaf) => {\n const path = findPath(graph, divergencePoint, leaf);\n return {\n leaf,\n edges: (path ?? []).map((e) => ({ dirName: e.dirName, from: e.from, to: e.to })),\n };\n });\n throw errorAmbiguousLeaf(leaves, { divergencePoint, branches });\n }\n\n const leaf = leaves[0];\n return leaf !== undefined ? leaf : EMPTY_CONTRACT_HASH;\n}\n\n/**\n * Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH\n * to the single leaf. Returns null for an empty graph.\n * Throws AMBIGUOUS_LEAF if the graph has multiple leaves.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n const leafHash = findLeaf(graph);\n if (leafHash === EMPTY_CONTRACT_HASH) {\n return null;\n }\n\n const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);\n if (!path || path.length === 0) {\n return null;\n }\n\n return path[path.length - 1] ?? null;\n}\n\nexport function detectCycles(graph: MigrationGraph): readonly string[][] {\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n\n const color = new Map<string, number>();\n const parentMap = new Map<string, string | null>();\n const cycles: string[][] = [];\n\n for (const node of graph.nodes) {\n color.set(node, WHITE);\n }\n\n function dfs(u: string): void {\n color.set(u, GRAY);\n\n const outgoing = graph.forwardChain.get(u);\n if (outgoing) {\n for (const edge of outgoing) {\n const v = edge.to;\n if (color.get(v) === GRAY) {\n const cycle: string[] = [v];\n let cur = u;\n while (cur !== v) {\n cycle.push(cur);\n cur = parentMap.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (color.get(v) === WHITE) {\n parentMap.set(v, u);\n dfs(v);\n }\n }\n }\n\n color.set(u, BLACK);\n }\n\n for (const node of graph.nodes) {\n if (color.get(node) === WHITE) {\n parentMap.set(node, null);\n dfs(node);\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const startNodes: string[] = [];\n\n if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) {\n startNodes.push(EMPTY_CONTRACT_HASH);\n } else {\n const allTargets = new Set<string>();\n for (const edges of graph.forwardChain.values()) {\n for (const edge of edges) {\n allTargets.add(edge.to);\n }\n }\n for (const node of graph.nodes) {\n if (!allTargets.has(node)) {\n startNodes.push(node);\n }\n }\n }\n\n const queue = [...startNodes];\n for (const hash of queue) {\n reachable.add(hash);\n }\n\n while (queue.length > 0) {\n const node = queue.shift();\n if (node === undefined) break;\n const outgoing = graph.forwardChain.get(node);\n if (!outgoing) continue;\n\n for (const migration of outgoing) {\n if (!reachable.has(migration.to)) {\n reachable.add(migration.to);\n queue.push(migration.to);\n }\n }\n }\n\n const orphans: MigrationChainEntry[] = [];\n for (const [from, migrations] of graph.forwardChain) {\n if (!reachable.has(from)) {\n orphans.push(...migrations);\n }\n }\n\n return orphans;\n}\n"],"mappings":";;;;;AAWA,SAAgB,iBAAiB,UAA8D;CAC7F,MAAM,wBAAQ,IAAI,KAAa;CAC/B,MAAM,+BAAe,IAAI,KAAoC;CAC7D,MAAM,+BAAe,IAAI,KAAoC;CAC7D,MAAM,gCAAgB,IAAI,KAAkC;AAE5D,MAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,EAAE,MAAM,OAAO,IAAI;AAEzB,MAAI,SAAS,GACX,OAAM,cAAc,IAAI,SAAS,KAAK;AAGxC,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,GAAG;EAEb,MAAMA,YAAiC;GACrC;GACA;GACA,aAAa,IAAI,SAAS;GAC1B,SAAS,IAAI;GACb,WAAW,IAAI,SAAS;GACxB,QAAQ,IAAI,SAAS;GACtB;AAED,MAAI,UAAU,gBAAgB,MAAM;AAClC,OAAI,cAAc,IAAI,UAAU,YAAY,CAC1C,OAAM,0BAA0B,UAAU,YAAY;AAExD,iBAAc,IAAI,UAAU,aAAa,UAAU;;EAGrD,MAAM,MAAM,aAAa,IAAI,KAAK;AAClC,MAAI,IACF,KAAI,KAAK,UAAU;MAEnB,cAAa,IAAI,MAAM,CAAC,UAAU,CAAC;EAGrC,MAAM,MAAM,aAAa,IAAI,GAAG;AAChC,MAAI,IACF,KAAI,KAAK,UAAU;MAEnB,cAAa,IAAI,IAAI,CAAC,UAAU,CAAC;;AAIrC,QAAO;EAAE;EAAO;EAAc;EAAc;EAAe;;AAG7D,MAAMC,iBAAyC;CAAE,MAAM;CAAG,SAAS;CAAG,SAAS;CAAG;AAElF,SAAS,cAAc,QAAmC;CACxD,IAAI,OAAO;AACX,MAAK,MAAM,KAAK,QAAQ;EACtB,MAAM,IAAI,eAAe;AACzB,MAAI,MAAM,UAAa,IAAI,KAAM,QAAO;;AAE1C,QAAO;;AAGT,SAAS,gBAAgB,OAAuE;AAC9F,QAAO,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM;EAC/B,MAAM,KAAK,cAAc,EAAE,OAAO,GAAG,cAAc,EAAE,OAAO;AAC5D,MAAI,OAAO,EAAG,QAAO;EACrB,MAAM,KAAK,EAAE,UAAU,cAAc,EAAE,UAAU;AACjD,MAAI,OAAO,EAAG,QAAO;EACrB,MAAM,KAAK,EAAE,GAAG,cAAc,EAAE,GAAG;AACnC,MAAI,OAAO,EAAG,QAAO;AACrB,UAAQ,EAAE,eAAe,IAAI,cAAc,EAAE,eAAe,GAAG;GAC/D;;;;;;;;;;AAWJ,SAAgB,SACd,OACA,UACA,QACuC;AACvC,KAAI,aAAa,OAAQ,QAAO,EAAE;CAElC,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,yBAAS,IAAI,KAA0D;CAC7E,MAAMC,QAAkB,CAAC,SAAS;AAClC,SAAQ,IAAI,SAAS;AAErB,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,UAAU,MAAM,OAAO;AAC7B,MAAI,YAAY,OAAW;AAE3B,MAAI,YAAY,QAAQ;GACtB,MAAMC,OAA8B,EAAE;GACtC,IAAI,OAAO;GACX,IAAI,QAAQ,OAAO,IAAI,KAAK;AAC5B,UAAO,OAAO;IACZ,MAAM,EAAE,MAAM,MAAM,SAAS;AAC7B,SAAK,KAAK,KAAK;AACf,WAAO;AACP,YAAQ,OAAO,IAAI,KAAK;;AAE1B,QAAK,SAAS;AACd,UAAO;;EAGT,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ;AAChD,MAAI,CAAC,SAAU;AAEf,OAAK,MAAM,QAAQ,gBAAgB,SAAS,CAC1C,KAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,EAAE;AACzB,WAAQ,IAAI,KAAK,GAAG;AACpB,UAAO,IAAI,KAAK,IAAI;IAAE,MAAM;IAAS;IAAM,CAAC;AAC5C,SAAM,KAAK,KAAK,GAAG;;;AAKzB,QAAO;;;;;;AAgBT,SAAgB,qBACd,OACA,UACA,QACA,SACqB;AACrB,KAAI,aAAa,OACf,QAAO;EACL,cAAc,EAAE;EAChB;EACA;EACA,kBAAkB;EAClB,iBAAiB,EAAE;EACnB,GAAG,UAAU,WAAW,QAAQ;EACjC;CAGH,MAAM,OAAO,SAAS,OAAO,UAAU,OAAO;AAC9C,KAAI,CAAC,KAAM,QAAO;CAElB,MAAMC,kBAA4B,EAAE;CACpC,IAAI,mBAAmB;AAEvB,MAAK,MAAM,QAAQ,MAAM;EACvB,MAAM,WAAW,MAAM,aAAa,IAAI,KAAK,KAAK;AAClD,MAAI,YAAY,SAAS,SAAS,GAAG;GACnC,MAAM,YAAY,SAAS,QAAQ,MAAM;AAEvC,WADkB,SAAS,OAAO,EAAE,IAAI,OAAO,KAC1B,QAAQ,EAAE,OAAO;KACtC;AACF,OAAI,UAAU,SAAS,GAAG;AACxB,wBAAoB,UAAU,SAAS;IACvC,MAAM,SAAS,gBAAgB,UAAU;AACzC,QAAI,OAAO,MAAM,OAAO,GAAG,gBAAgB,KAAK,aAC9C;SAAI,UAAU,MAAM,MAAM,EAAE,gBAAgB,KAAK,YAAY,CAC3D,iBAAgB,KACd,MAAM,KAAK,KAAK,IAAI,UAAU,OAAO,oCACtC;;;;;AAOX,QAAO;EACL,cAAc;EACd;EACA;EACA;EACA;EACA,GAAG,UAAU,WAAW,QAAQ;EACjC;;;;;;AAOH,SAAS,oBACP,OACA,UACA,QACQ;CACR,MAAM,eAAe,OAAO,KAAK,SAAS;EACxC,MAAM,4BAAY,IAAI,KAAa;EACnC,MAAM,QAAQ,CAAC,KAAK;AACpB,SAAO,MAAM,SAAS,GAAG;GACvB,MAAM,UAAU,MAAM,OAAO;AAC7B,OAAI,UAAU,IAAI,QAAQ,CAAE;AAC5B,aAAU,IAAI,QAAQ;GACtB,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ;AAChD,OAAI,SACF,MAAK,MAAM,QAAQ,SACjB,OAAM,KAAK,KAAK,KAAK;;AAI3B,SAAO;GACP;CAEF,MAAM,kBAAkB,CAAC,GAAI,aAAa,MAAM,EAAE,CAAE,CAAC,QAAQ,SAC3D,aAAa,OAAO,MAAM,EAAE,IAAI,KAAK,CAAC,CACvC;CAED,IAAI,UAAU;CACd,IAAI,eAAe;AACnB,MAAK,MAAM,YAAY,iBAAiB;EACtC,MAAM,OAAO,SAAS,OAAO,UAAU,SAAS;EAChD,MAAM,QAAQ,OAAO,KAAK,SAAS;AACnC,MAAI,QAAQ,cAAc;AACxB,kBAAe;AACf,aAAU;;;AAGd,QAAO;;;;;;AAOT,SAAgB,oBAAoB,OAAuB,UAAqC;CAC9F,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAMF,QAAkB,CAAC,SAAS;AAClC,SAAQ,IAAI,SAAS;CACrB,MAAMG,SAAmB,EAAE;AAE3B,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,UAAU,MAAM,OAAO;AAC7B,MAAI,YAAY,OAAW;EAC3B,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ;AAEhD,MAAI,CAAC,YAAY,SAAS,WAAW,EACnC,QAAO,KAAK,QAAQ;MAEpB,MAAK,MAAM,QAAQ,SACjB,KAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,EAAE;AACzB,WAAQ,IAAI,KAAK,GAAG;AACpB,SAAM,KAAK,KAAK,GAAG;;;AAM3B,QAAO;;;;;;;;AAST,SAAgB,SAAS,OAA+B;AACtD,KAAI,MAAM,MAAM,SAAS,EACvB,QAAO;AAGT,KAAI,CAAC,MAAM,MAAM,IAAI,oBAAoB,CACvC,OAAM,YAAY,CAAC,GAAG,MAAM,MAAM,CAAC;CAGrC,MAAM,SAAS,oBAAoB,OAAO,oBAAoB;AAE9D,KAAI,OAAO,WAAW,GAAG;EACvB,MAAM,YAAY,CAAC,GAAG,MAAM,MAAM,CAAC,QAAQ,MAAM,MAAM,oBAAoB;AAC3E,MAAI,UAAU,SAAS,EACrB,OAAM,sBAAsB,UAAU;AAExC,SAAO;;AAGT,KAAI,OAAO,SAAS,GAAG;EACrB,MAAM,kBAAkB,oBAAoB,OAAO,qBAAqB,OAAO;AAQ/E,QAAM,mBAAmB,QAAQ;GAAE;GAAiB,UAPnC,OAAO,KAAK,WAAS;AAEpC,WAAO;KACL;KACA,QAHW,SAAS,OAAO,iBAAiBC,OAAK,IAGjC,EAAE,EAAE,KAAK,OAAO;MAAE,SAAS,EAAE;MAAS,MAAM,EAAE;MAAM,IAAI,EAAE;MAAI,EAAE;KACjF;KACD;GAC4D,CAAC;;CAGjE,MAAM,OAAO,OAAO;AACpB,QAAO,SAAS,SAAY,OAAO;;;;;;;AAQrC,SAAgB,oBAAoB,OAAmD;AACrF,KAAI,MAAM,MAAM,SAAS,EACvB,QAAO;CAGT,MAAM,WAAW,SAAS,MAAM;AAChC,KAAI,aAAa,oBACf,QAAO;CAGT,MAAM,OAAO,SAAS,OAAO,qBAAqB,SAAS;AAC3D,KAAI,CAAC,QAAQ,KAAK,WAAW,EAC3B,QAAO;AAGT,QAAO,KAAK,KAAK,SAAS,MAAM;;AAGlC,SAAgB,aAAa,OAA4C;CACvE,MAAM,QAAQ;CACd,MAAM,OAAO;CACb,MAAM,QAAQ;CAEd,MAAM,wBAAQ,IAAI,KAAqB;CACvC,MAAM,4BAAY,IAAI,KAA4B;CAClD,MAAMC,SAAqB,EAAE;AAE7B,MAAK,MAAM,QAAQ,MAAM,MACvB,OAAM,IAAI,MAAM,MAAM;CAGxB,SAAS,IAAI,GAAiB;AAC5B,QAAM,IAAI,GAAG,KAAK;EAElB,MAAM,WAAW,MAAM,aAAa,IAAI,EAAE;AAC1C,MAAI,SACF,MAAK,MAAM,QAAQ,UAAU;GAC3B,MAAM,IAAI,KAAK;AACf,OAAI,MAAM,IAAI,EAAE,KAAK,MAAM;IACzB,MAAMC,QAAkB,CAAC,EAAE;IAC3B,IAAI,MAAM;AACV,WAAO,QAAQ,GAAG;AAChB,WAAM,KAAK,IAAI;AACf,WAAM,UAAU,IAAI,IAAI,IAAI;;AAE9B,UAAM,SAAS;AACf,WAAO,KAAK,MAAM;cACT,MAAM,IAAI,EAAE,KAAK,OAAO;AACjC,cAAU,IAAI,GAAG,EAAE;AACnB,QAAI,EAAE;;;AAKZ,QAAM,IAAI,GAAG,MAAM;;AAGrB,MAAK,MAAM,QAAQ,MAAM,MACvB,KAAI,MAAM,IAAI,KAAK,KAAK,OAAO;AAC7B,YAAU,IAAI,MAAM,KAAK;AACzB,MAAI,KAAK;;AAIb,QAAO;;AAGT,SAAgB,cAAc,OAAuD;AACnF,KAAI,MAAM,MAAM,SAAS,EAAG,QAAO,EAAE;CAErC,MAAM,4BAAY,IAAI,KAAa;CACnC,MAAMC,aAAuB,EAAE;AAE/B,KAAI,MAAM,aAAa,IAAI,oBAAoB,CAC7C,YAAW,KAAK,oBAAoB;MAC/B;EACL,MAAM,6BAAa,IAAI,KAAa;AACpC,OAAK,MAAM,SAAS,MAAM,aAAa,QAAQ,CAC7C,MAAK,MAAM,QAAQ,MACjB,YAAW,IAAI,KAAK,GAAG;AAG3B,OAAK,MAAM,QAAQ,MAAM,MACvB,KAAI,CAAC,WAAW,IAAI,KAAK,CACvB,YAAW,KAAK,KAAK;;CAK3B,MAAM,QAAQ,CAAC,GAAG,WAAW;AAC7B,MAAK,MAAM,QAAQ,MACjB,WAAU,IAAI,KAAK;AAGrB,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,OAAO,MAAM,OAAO;AAC1B,MAAI,SAAS,OAAW;EACxB,MAAM,WAAW,MAAM,aAAa,IAAI,KAAK;AAC7C,MAAI,CAAC,SAAU;AAEf,OAAK,MAAM,aAAa,SACtB,KAAI,CAAC,UAAU,IAAI,UAAU,GAAG,EAAE;AAChC,aAAU,IAAI,UAAU,GAAG;AAC3B,SAAM,KAAK,UAAU,GAAG;;;CAK9B,MAAMC,UAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,MAAM,eAAe,MAAM,aACrC,KAAI,CAAC,UAAU,IAAI,KAAK,CACtB,SAAQ,KAAK,GAAG,WAAW;AAI/B,QAAO"}
|