@prisma-next/migration-tools 0.5.0-dev.9 → 0.5.1
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-DWV9_o2Z.mjs} +2 -2
- package/dist/{constants-BRi0X7B_.mjs.map → constants-DWV9_o2Z.mjs.map} +1 -1
- package/dist/errors-EPL_9p9f.mjs +297 -0
- package/dist/errors-EPL_9p9f.mjs.map +1 -0
- package/dist/exports/aggregate.d.mts +599 -0
- package/dist/exports/aggregate.d.mts.map +1 -0
- package/dist/exports/aggregate.mjs +599 -0
- package/dist/exports/aggregate.mjs.map +1 -0
- package/dist/exports/constants.d.mts.map +1 -1
- package/dist/exports/constants.mjs +2 -3
- package/dist/exports/errors.d.mts +68 -0
- package/dist/exports/errors.d.mts.map +1 -0
- package/dist/exports/errors.mjs +2 -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 +2 -0
- package/dist/exports/invariants.d.mts +39 -0
- package/dist/exports/invariants.d.mts.map +1 -0
- package/dist/exports/invariants.mjs +2 -0
- package/dist/exports/io.d.mts +66 -6
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +2 -3
- package/dist/exports/metadata.d.mts +2 -0
- package/dist/exports/metadata.mjs +1 -0
- package/dist/exports/migration-graph.d.mts +2 -0
- package/dist/exports/migration-graph.mjs +2 -0
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +2 -4
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +15 -14
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +70 -43
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +3 -0
- package/dist/exports/package.mjs +1 -0
- package/dist/exports/refs.d.mts.map +1 -1
- package/dist/exports/refs.mjs +3 -4
- package/dist/exports/refs.mjs.map +1 -1
- package/dist/exports/spaces.d.mts +526 -0
- package/dist/exports/spaces.d.mts.map +1 -0
- package/dist/exports/spaces.mjs +266 -0
- package/dist/exports/spaces.mjs.map +1 -0
- package/dist/graph-HMWAldoR.d.mts +28 -0
- package/dist/graph-HMWAldoR.d.mts.map +1 -0
- package/dist/hash-By50zM_E.mjs +74 -0
- package/dist/hash-By50zM_E.mjs.map +1 -0
- package/dist/invariants-qgQGlsrV.mjs +57 -0
- package/dist/invariants-qgQGlsrV.mjs.map +1 -0
- package/dist/io-D5YYptRO.mjs +239 -0
- package/dist/io-D5YYptRO.mjs.map +1 -0
- package/dist/metadata-CFvm3ayn.d.mts +2 -0
- package/dist/migration-graph-DGNnKDY5.mjs +523 -0
- package/dist/migration-graph-DGNnKDY5.mjs.map +1 -0
- package/dist/migration-graph-DulOITvG.d.mts +124 -0
- package/dist/migration-graph-DulOITvG.d.mts.map +1 -0
- package/dist/op-schema-D5qkXfEf.mjs +13 -0
- package/dist/op-schema-D5qkXfEf.mjs.map +1 -0
- package/dist/package-BjiZ7KDy.d.mts +21 -0
- package/dist/package-BjiZ7KDy.d.mts.map +1 -0
- package/dist/read-contract-space-contract-Cme8KZk_.mjs +259 -0
- package/dist/read-contract-space-contract-Cme8KZk_.mjs.map +1 -0
- package/package.json +42 -17
- package/src/aggregate/loader.ts +379 -0
- package/src/aggregate/marker-types.ts +16 -0
- package/src/aggregate/planner-types.ts +171 -0
- package/src/aggregate/planner.ts +159 -0
- package/src/aggregate/project-schema-to-space.ts +64 -0
- package/src/aggregate/strategies/graph-walk.ts +118 -0
- package/src/aggregate/strategies/synth.ts +122 -0
- package/src/aggregate/types.ts +89 -0
- package/src/aggregate/verifier.ts +230 -0
- package/src/assert-descriptor-self-consistency.ts +70 -0
- package/src/compute-extension-space-apply-path.ts +152 -0
- package/src/concatenate-space-apply-inputs.ts +90 -0
- package/src/contract-space-from-json.ts +63 -0
- package/src/emit-contract-space-artefacts.ts +70 -0
- package/src/errors.ts +251 -17
- package/src/exports/aggregate.ts +42 -0
- package/src/exports/errors.ts +8 -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 +3 -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 +2 -0
- package/src/exports/spaces.ts +45 -0
- package/src/gather-disk-contract-space-state.ts +62 -0
- package/src/graph-ops.ts +57 -30
- package/src/graph.ts +25 -0
- package/src/hash.ts +91 -0
- package/src/invariants.ts +61 -0
- package/src/io.ts +163 -40
- package/src/metadata.ts +1 -0
- package/src/migration-base.ts +97 -56
- package/src/migration-graph.ts +676 -0
- package/src/op-schema.ts +11 -0
- package/src/package.ts +21 -0
- package/src/plan-all-spaces.ts +76 -0
- package/src/read-contract-space-contract.ts +44 -0
- package/src/read-contract-space-head-ref.ts +63 -0
- package/src/space-layout.ts +48 -0
- package/src/verify-contract-spaces.ts +272 -0
- package/dist/attestation-BnzTb0Qp.mjs +0 -65
- package/dist/attestation-BnzTb0Qp.mjs.map +0 -1
- package/dist/errors-BmiSgz1j.mjs +0 -160
- package/dist/errors-BmiSgz1j.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 +0 -51
- package/dist/exports/dag.d.mts.map +0 -1
- package/dist/exports/dag.mjs +0 -386
- package/dist/exports/dag.mjs.map +0 -1
- package/dist/exports/types.d.mts +0 -35
- package/dist/exports/types.d.mts.map +0 -1
- package/dist/exports/types.mjs +0 -3
- package/dist/io-Cd6GLyjK.mjs +0 -153
- package/dist/io-Cd6GLyjK.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/dag.ts +0 -426
- package/src/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
package/dist/errors-BmiSgz1j.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
//#region src/errors.ts
|
|
2
|
-
/**
|
|
3
|
-
* Structured error for migration tooling operations.
|
|
4
|
-
*
|
|
5
|
-
* Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
|
|
6
|
-
* the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
|
|
7
|
-
* migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
|
|
8
|
-
* failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
|
|
9
|
-
*
|
|
10
|
-
* Fields:
|
|
11
|
-
* - code: Stable machine-readable code (MIGRATION.SUBCODE)
|
|
12
|
-
* - category: Always 'MIGRATION'
|
|
13
|
-
* - why: Explains the cause in plain language
|
|
14
|
-
* - fix: Actionable remediation step
|
|
15
|
-
* - details: Machine-readable structured data for agents
|
|
16
|
-
*/
|
|
17
|
-
var MigrationToolsError = class extends Error {
|
|
18
|
-
code;
|
|
19
|
-
category = "MIGRATION";
|
|
20
|
-
why;
|
|
21
|
-
fix;
|
|
22
|
-
details;
|
|
23
|
-
constructor(code, summary, options) {
|
|
24
|
-
super(summary);
|
|
25
|
-
this.name = "MigrationToolsError";
|
|
26
|
-
this.code = code;
|
|
27
|
-
this.why = options.why;
|
|
28
|
-
this.fix = options.fix;
|
|
29
|
-
this.details = options.details;
|
|
30
|
-
}
|
|
31
|
-
static is(error) {
|
|
32
|
-
if (!(error instanceof Error)) return false;
|
|
33
|
-
const candidate = error;
|
|
34
|
-
return candidate.name === "MigrationToolsError" && typeof candidate.code === "string";
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
function errorDirectoryExists(dir) {
|
|
38
|
-
return new MigrationToolsError("MIGRATION.DIR_EXISTS", "Migration directory already exists", {
|
|
39
|
-
why: `The directory "${dir}" already exists. Each migration must have a unique directory.`,
|
|
40
|
-
fix: "Use --name to pick a different name, or delete the existing directory and re-run.",
|
|
41
|
-
details: { dir }
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
function errorMissingFile(file, dir) {
|
|
45
|
-
return new MigrationToolsError("MIGRATION.FILE_MISSING", `Missing ${file}`, {
|
|
46
|
-
why: `Expected "${file}" in "${dir}" but the file does not exist.`,
|
|
47
|
-
fix: "Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.",
|
|
48
|
-
details: {
|
|
49
|
-
file,
|
|
50
|
-
dir
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
function errorInvalidJson(filePath, parseError) {
|
|
55
|
-
return new MigrationToolsError("MIGRATION.INVALID_JSON", "Invalid JSON in migration file", {
|
|
56
|
-
why: `Failed to parse "${filePath}": ${parseError}`,
|
|
57
|
-
fix: "Fix the JSON syntax error, or delete the migration directory and re-run migration plan.",
|
|
58
|
-
details: {
|
|
59
|
-
filePath,
|
|
60
|
-
parseError
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
function errorInvalidManifest(filePath, reason) {
|
|
65
|
-
return new MigrationToolsError("MIGRATION.INVALID_MANIFEST", "Invalid migration manifest", {
|
|
66
|
-
why: `Manifest at "${filePath}" is invalid: ${reason}`,
|
|
67
|
-
fix: "Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.",
|
|
68
|
-
details: {
|
|
69
|
-
filePath,
|
|
70
|
-
reason
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
function errorInvalidSlug(slug) {
|
|
75
|
-
return new MigrationToolsError("MIGRATION.INVALID_NAME", "Invalid migration name", {
|
|
76
|
-
why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
|
|
77
|
-
fix: "Provide a name with at least one alphanumeric character, e.g. --name add_users.",
|
|
78
|
-
details: { slug }
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
function errorInvalidDestName(destName) {
|
|
82
|
-
return new MigrationToolsError("MIGRATION.INVALID_DEST_NAME", "Invalid copy destination name", {
|
|
83
|
-
why: `The destination name "${destName}" must be a single path segment (no ".." or directory separators).`,
|
|
84
|
-
fix: "Use a simple file name such as \"contract.json\" for each destination in the copy list.",
|
|
85
|
-
details: { destName }
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
function errorSameSourceAndTarget(dirName, hash) {
|
|
89
|
-
return new MigrationToolsError("MIGRATION.SAME_SOURCE_AND_TARGET", "Migration has same source and target", {
|
|
90
|
-
why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
|
|
91
|
-
fix: "Delete the invalid migration directory and re-run migration plan.",
|
|
92
|
-
details: {
|
|
93
|
-
dirName,
|
|
94
|
-
hash
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
function errorAmbiguousTarget(branchTips, context) {
|
|
99
|
-
const divergenceInfo = context ? `\nDivergence point: ${context.divergencePoint}\nBranches:\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(" → ") || "direct"})`).join("\n")}` : "";
|
|
100
|
-
return new MigrationToolsError("MIGRATION.AMBIGUOUS_TARGET", "Ambiguous migration target", {
|
|
101
|
-
why: `The migration history has diverged into multiple branches: ${branchTips.join(", ")}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,
|
|
102
|
-
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.",
|
|
103
|
-
details: {
|
|
104
|
-
branchTips,
|
|
105
|
-
...context ? {
|
|
106
|
-
divergencePoint: context.divergencePoint,
|
|
107
|
-
branches: context.branches
|
|
108
|
-
} : {}
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function errorNoInitialMigration(nodes) {
|
|
113
|
-
return new MigrationToolsError("MIGRATION.NO_INITIAL_MIGRATION", "No initial migration found", {
|
|
114
|
-
why: `No migration starts from the empty contract state (known hashes: ${nodes.join(", ")}). At least one migration must originate from the empty state.`,
|
|
115
|
-
fix: "Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.",
|
|
116
|
-
details: { nodes }
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
function errorInvalidRefFile(filePath, reason) {
|
|
120
|
-
return new MigrationToolsError("MIGRATION.INVALID_REF_FILE", "Invalid ref file", {
|
|
121
|
-
why: `Ref file at "${filePath}" is invalid: ${reason}`,
|
|
122
|
-
fix: "Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.",
|
|
123
|
-
details: {
|
|
124
|
-
path: filePath,
|
|
125
|
-
reason
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
function errorInvalidRefName(refName) {
|
|
130
|
-
return new MigrationToolsError("MIGRATION.INVALID_REF_NAME", "Invalid ref name", {
|
|
131
|
-
why: `Ref name "${refName}" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no "." or ".." segments).`,
|
|
132
|
-
fix: `Use a valid ref name (e.g., "staging", "envs/production").`,
|
|
133
|
-
details: { refName }
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
function errorNoTarget(reachableHashes) {
|
|
137
|
-
return new MigrationToolsError("MIGRATION.NO_TARGET", "No migration target could be resolved", {
|
|
138
|
-
why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(", ")}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,
|
|
139
|
-
fix: "Use --from <hash> to specify the planning origin explicitly.",
|
|
140
|
-
details: { reachableHashes }
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
function errorInvalidRefValue(value) {
|
|
144
|
-
return new MigrationToolsError("MIGRATION.INVALID_REF_VALUE", "Invalid ref value", {
|
|
145
|
-
why: `Ref value "${value}" is not a valid contract hash. Values must be in the format "sha256:<64 hex chars>" or "sha256:empty".`,
|
|
146
|
-
fix: "Use a valid storage hash from `prisma-next contract emit` output or an existing migration.",
|
|
147
|
-
details: { value }
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
function errorDuplicateMigrationId(migrationId) {
|
|
151
|
-
return new MigrationToolsError("MIGRATION.DUPLICATE_MIGRATION_ID", "Duplicate migrationId in migration graph", {
|
|
152
|
-
why: `Multiple migrations share migrationId "${migrationId}". Each migration must have a unique content-addressed identity.`,
|
|
153
|
-
fix: "Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.",
|
|
154
|
-
details: { migrationId }
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
//#endregion
|
|
159
|
-
export { errorInvalidDestName as a, errorInvalidRefFile as c, errorInvalidSlug as d, errorMissingFile as f, errorSameSourceAndTarget as h, errorDuplicateMigrationId as i, errorInvalidRefName as l, errorNoTarget as m, errorAmbiguousTarget as n, errorInvalidJson as o, errorNoInitialMigration as p, errorDirectoryExists as r, errorInvalidManifest as s, MigrationToolsError as t, errorInvalidRefValue as u };
|
|
160
|
-
//# sourceMappingURL=errors-BmiSgz1j.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors-BmiSgz1j.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 history 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 errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorSameSourceAndTarget(dirName: string, hash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration has same source and target',\n {\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}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: 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.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. 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 branchTips,\n ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),\n },\n });\n}\n\nexport function errorNoInitialMigration(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_INITIAL_MIGRATION', 'No initial migration found', {\n why: `No migration starts from the empty contract state (known hashes: ${nodes.join(', ')}). At least one migration must originate from the empty state.`,\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 errorInvalidRefFile(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_FILE', 'Invalid ref file', {\n why: `Ref file at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.',\n details: { path: filePath, 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 errorNoTarget(reachableHashes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_TARGET', 'No migration target could be resolved', {\n why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.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: { reachableHashes },\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":"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"}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { a as MigrationManifest, o as MigrationOps, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/attestation.d.ts
|
|
4
|
-
interface VerifyResult {
|
|
5
|
-
readonly ok: boolean;
|
|
6
|
-
readonly reason?: 'mismatch';
|
|
7
|
-
readonly storedMigrationId?: string;
|
|
8
|
-
readonly computedMigrationId?: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Content-addressed migration identity over (manifest envelope sans
|
|
12
|
-
* contracts/hints, ops). See ADR 199 "Storage-only migration identity"
|
|
13
|
-
* for the rationale: contracts are anchored separately by the
|
|
14
|
-
* storage-hash bookends inside the envelope; planner hints are advisory
|
|
15
|
-
* and must not affect identity.
|
|
16
|
-
*
|
|
17
|
-
* The `migrationId` field on the manifest is stripped before hashing so
|
|
18
|
-
* the function can be used both at write time (when no id exists yet)
|
|
19
|
-
* and at verify time (rehashing an already-attested manifest).
|
|
20
|
-
*/
|
|
21
|
-
declare function computeMigrationId(manifest: Omit<MigrationManifest, 'migrationId'> & {
|
|
22
|
-
readonly migrationId?: string;
|
|
23
|
-
}, ops: MigrationOps): string;
|
|
24
|
-
/**
|
|
25
|
-
* Re-hash an on-disk migration bundle and compare against the stored
|
|
26
|
-
* `migrationId`. Returns `{ ok: true }` when the package is internally
|
|
27
|
-
* consistent (manifest + ops still produce the recorded id), or
|
|
28
|
-
* `{ ok: false, reason: 'mismatch', stored, computed }` when they do
|
|
29
|
-
* not — typically a sign of FS corruption, partial writes, or a
|
|
30
|
-
* post-emit hand edit.
|
|
31
|
-
*/
|
|
32
|
-
declare function verifyMigrationBundle(bundle: MigrationBundle): VerifyResult;
|
|
33
|
-
/** Convenience wrapper: read the package from disk then verify it. */
|
|
34
|
-
declare function verifyMigration(dir: string): Promise<VerifyResult>;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { type VerifyResult, computeMigrationId, verifyMigration, verifyMigrationBundle };
|
|
37
|
-
//# sourceMappingURL=attestation.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.d.mts","names":[],"sources":["../../src/attestation.ts"],"sourcesContent":[],"mappings":";;;UAKiB,YAAA;;EAAA,SAAA,MAAY,CAAA,EAAA,UAAA;EAsBb,SAAA,iBAAkB,CAAA,EAAA,MAAA;EACjB,SAAA,mBAAA,CAAA,EAAA,MAAA;;;;AA6BjB;AAoBA;;;;;;;;iBAlDgB,kBAAA,WACJ,KAAK;;QACV;;;;;;;;;iBA4BS,qBAAA,SAA8B,kBAAkB;;iBAoB1C,eAAA,eAA8B,QAAQ"}
|
package/dist/exports/dag.d.mts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { n as MigrationChainEntry, r as MigrationGraph, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/dag.d.ts
|
|
4
|
-
declare function reconstructGraph(packages: readonly MigrationBundle[]): MigrationGraph;
|
|
5
|
-
/**
|
|
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.
|
|
12
|
-
*/
|
|
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;
|
|
27
|
-
/**
|
|
28
|
-
* Find all branch tips (nodes with no outgoing edges) reachable from
|
|
29
|
-
* `fromHash` via forward edges.
|
|
30
|
-
*/
|
|
31
|
-
declare function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[];
|
|
32
|
-
/**
|
|
33
|
-
* Find the target contract hash of the migration graph reachable from
|
|
34
|
-
* EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target
|
|
35
|
-
* state (either empty, or containing only the root with no outgoing
|
|
36
|
-
* edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none
|
|
37
|
-
* originate from the empty hash, and AMBIGUOUS_TARGET if multiple
|
|
38
|
-
* branch tips exist.
|
|
39
|
-
*/
|
|
40
|
-
declare function findLeaf(graph: MigrationGraph): string | null;
|
|
41
|
-
/**
|
|
42
|
-
* Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
|
|
43
|
-
* to the single target. Returns null for an empty graph.
|
|
44
|
-
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
|
|
45
|
-
*/
|
|
46
|
-
declare function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null;
|
|
47
|
-
declare function detectCycles(graph: MigrationGraph): readonly string[][];
|
|
48
|
-
declare function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[];
|
|
49
|
-
//#endregion
|
|
50
|
-
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
51
|
-
//# sourceMappingURL=dag.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dag.d.mts","names":[],"sources":["../../src/dag.ts"],"sourcesContent":[],"mappings":";;;iBAgCgB,gBAAA,oBAAoC,oBAAoB;;AAAxE;AAmFA;AAyCA;AAaA;AA8FA;AAkBA;AAwCA;AAQgB,iBAtNA,QAAA,CAsNoB,KAAA,EArN3B,cAqNyC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SAlNtC,mBAkNsC,EAAA,GAAA,IAAA;AA8DlC,UA3OC,YAAA,CA2OY;kCA1OK;;;;;;;;;;;iBAYlB,oBAAA,QACP,qEAIN;;;;;iBAyFa,mBAAA,QAA2B;;;;;;;;;iBAkB3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAQ5C,YAAA,QAAoB;iBA8DpB,aAAA,QAAqB,0BAA0B"}
|
package/dist/exports/dag.mjs
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { h as errorSameSourceAndTarget, i as errorDuplicateMigrationId, m as errorNoTarget, n as errorAmbiguousTarget, p as errorNoInitialMigration } from "../errors-BmiSgz1j.mjs";
|
|
2
|
-
import { t as EMPTY_CONTRACT_HASH } from "../constants-BRi0X7B_.mjs";
|
|
3
|
-
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
|
-
|
|
5
|
-
//#region src/queue.ts
|
|
6
|
-
/**
|
|
7
|
-
* FIFO queue with amortised O(1) push and shift.
|
|
8
|
-
*
|
|
9
|
-
* Uses a head-index cursor over a backing array rather than
|
|
10
|
-
* `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped
|
|
11
|
-
* traversals where the queue is drained in a single pass — it does not
|
|
12
|
-
* reclaim memory for already-shifted items, so it is not suitable for
|
|
13
|
-
* long-lived queues with many push/shift cycles.
|
|
14
|
-
*/
|
|
15
|
-
var Queue = class {
|
|
16
|
-
items;
|
|
17
|
-
head = 0;
|
|
18
|
-
constructor(initial = []) {
|
|
19
|
-
this.items = [...initial];
|
|
20
|
-
}
|
|
21
|
-
push(item) {
|
|
22
|
-
this.items.push(item);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Remove and return the next item. Caller must check `isEmpty` first —
|
|
26
|
-
* shifting an empty queue throws.
|
|
27
|
-
*/
|
|
28
|
-
shift() {
|
|
29
|
-
if (this.head >= this.items.length) throw new Error("Queue.shift called on empty queue");
|
|
30
|
-
return this.items[this.head++];
|
|
31
|
-
}
|
|
32
|
-
get isEmpty() {
|
|
33
|
-
return this.head >= this.items.length;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
//#endregion
|
|
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) {
|
|
56
|
-
const visited = /* @__PURE__ */ new Set();
|
|
57
|
-
const parentMap = /* @__PURE__ */ new Map();
|
|
58
|
-
const queue = new Queue();
|
|
59
|
-
for (const start of starts) if (!visited.has(start)) {
|
|
60
|
-
visited.add(start);
|
|
61
|
-
queue.push(start);
|
|
62
|
-
}
|
|
63
|
-
while (!queue.isEmpty) {
|
|
64
|
-
const current = queue.shift();
|
|
65
|
-
const parentInfo = parentMap.get(current);
|
|
66
|
-
yield {
|
|
67
|
-
node: current,
|
|
68
|
-
parent: parentInfo?.parent ?? null,
|
|
69
|
-
incomingEdge: parentInfo?.edge ?? null
|
|
70
|
-
};
|
|
71
|
-
const items = neighbours(current);
|
|
72
|
-
const toVisit = ordering ? ordering([...items]) : items;
|
|
73
|
-
for (const { next, edge } of toVisit) if (!visited.has(next)) {
|
|
74
|
-
visited.add(next);
|
|
75
|
-
parentMap.set(next, {
|
|
76
|
-
parent: current,
|
|
77
|
-
edge
|
|
78
|
-
});
|
|
79
|
-
queue.push(next);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
//#endregion
|
|
85
|
-
//#region src/dag.ts
|
|
86
|
-
/** Forward-edge neighbours for BFS: edge `e` from `n` visits `e.to` next. */
|
|
87
|
-
function forwardNeighbours(graph, node) {
|
|
88
|
-
return (graph.forwardChain.get(node) ?? []).map((edge) => ({
|
|
89
|
-
next: edge.to,
|
|
90
|
-
edge
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
93
|
-
/** Reverse-edge neighbours for BFS: edge `e` from `n` visits `e.from` next. */
|
|
94
|
-
function reverseNeighbours(graph, node) {
|
|
95
|
-
return (graph.reverseChain.get(node) ?? []).map((edge) => ({
|
|
96
|
-
next: edge.from,
|
|
97
|
-
edge
|
|
98
|
-
}));
|
|
99
|
-
}
|
|
100
|
-
function appendEdge(map, key, entry) {
|
|
101
|
-
const bucket = map.get(key);
|
|
102
|
-
if (bucket) bucket.push(entry);
|
|
103
|
-
else map.set(key, [entry]);
|
|
104
|
-
}
|
|
105
|
-
function reconstructGraph(packages) {
|
|
106
|
-
const nodes = /* @__PURE__ */ new Set();
|
|
107
|
-
const forwardChain = /* @__PURE__ */ new Map();
|
|
108
|
-
const reverseChain = /* @__PURE__ */ new Map();
|
|
109
|
-
const migrationById = /* @__PURE__ */ new Map();
|
|
110
|
-
for (const pkg of packages) {
|
|
111
|
-
const { from, to } = pkg.manifest;
|
|
112
|
-
if (from === to) throw errorSameSourceAndTarget(pkg.dirName, from);
|
|
113
|
-
nodes.add(from);
|
|
114
|
-
nodes.add(to);
|
|
115
|
-
const migration = {
|
|
116
|
-
from,
|
|
117
|
-
to,
|
|
118
|
-
migrationId: pkg.manifest.migrationId,
|
|
119
|
-
dirName: pkg.dirName,
|
|
120
|
-
createdAt: pkg.manifest.createdAt,
|
|
121
|
-
labels: pkg.manifest.labels
|
|
122
|
-
};
|
|
123
|
-
if (migrationById.has(migration.migrationId)) throw errorDuplicateMigrationId(migration.migrationId);
|
|
124
|
-
migrationById.set(migration.migrationId, migration);
|
|
125
|
-
appendEdge(forwardChain, from, migration);
|
|
126
|
-
appendEdge(reverseChain, to, migration);
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
nodes,
|
|
130
|
-
forwardChain,
|
|
131
|
-
reverseChain,
|
|
132
|
-
migrationById
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
const LABEL_PRIORITY = {
|
|
136
|
-
main: 0,
|
|
137
|
-
default: 1,
|
|
138
|
-
feature: 2
|
|
139
|
-
};
|
|
140
|
-
function labelPriority(labels) {
|
|
141
|
-
let best = 3;
|
|
142
|
-
for (const l of labels) {
|
|
143
|
-
const p = LABEL_PRIORITY[l];
|
|
144
|
-
if (p !== void 0 && p < best) best = p;
|
|
145
|
-
}
|
|
146
|
-
return best;
|
|
147
|
-
}
|
|
148
|
-
function compareTieBreak(a, b) {
|
|
149
|
-
const lp = labelPriority(a.labels) - labelPriority(b.labels);
|
|
150
|
-
if (lp !== 0) return lp;
|
|
151
|
-
const ca = a.createdAt.localeCompare(b.createdAt);
|
|
152
|
-
if (ca !== 0) return ca;
|
|
153
|
-
const tc = a.to.localeCompare(b.to);
|
|
154
|
-
if (tc !== 0) return tc;
|
|
155
|
-
return a.migrationId.localeCompare(b.migrationId);
|
|
156
|
-
}
|
|
157
|
-
function sortedNeighbors(edges) {
|
|
158
|
-
return [...edges].sort(compareTieBreak);
|
|
159
|
-
}
|
|
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
|
-
/**
|
|
165
|
-
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
166
|
-
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
167
|
-
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
168
|
-
*
|
|
169
|
-
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
170
|
-
* label priority → createdAt → to → migrationId.
|
|
171
|
-
*/
|
|
172
|
-
function findPath(graph, fromHash, toHash) {
|
|
173
|
-
if (fromHash === toHash) return [];
|
|
174
|
-
const parents = /* @__PURE__ */ new Map();
|
|
175
|
-
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n), bfsOrdering)) {
|
|
176
|
-
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.node, {
|
|
177
|
-
parent: step.parent,
|
|
178
|
-
edge: step.incomingEdge
|
|
179
|
-
});
|
|
180
|
-
if (step.node === toHash) {
|
|
181
|
-
const path = [];
|
|
182
|
-
let cur = toHash;
|
|
183
|
-
let p = parents.get(cur);
|
|
184
|
-
while (p) {
|
|
185
|
-
path.push(p.edge);
|
|
186
|
-
cur = p.parent;
|
|
187
|
-
p = parents.get(cur);
|
|
188
|
-
}
|
|
189
|
-
path.reverse();
|
|
190
|
-
return path;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Reverse-BFS from `toHash` over `reverseChain` to collect every node from
|
|
197
|
-
* which `toHash` is reachable (inclusive of `toHash` itself).
|
|
198
|
-
*/
|
|
199
|
-
function collectNodesReachingTarget(graph, toHash) {
|
|
200
|
-
const reached = /* @__PURE__ */ new Set();
|
|
201
|
-
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.node);
|
|
202
|
-
return reached;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Find the shortest path from `fromHash` to `toHash` and return structured
|
|
206
|
-
* path-decision metadata for machine-readable output.
|
|
207
|
-
*/
|
|
208
|
-
function findPathWithDecision(graph, fromHash, toHash, refName) {
|
|
209
|
-
if (fromHash === toHash) return {
|
|
210
|
-
selectedPath: [],
|
|
211
|
-
fromHash,
|
|
212
|
-
toHash,
|
|
213
|
-
alternativeCount: 0,
|
|
214
|
-
tieBreakReasons: [],
|
|
215
|
-
...ifDefined("refName", refName)
|
|
216
|
-
};
|
|
217
|
-
const path = findPath(graph, fromHash, toHash);
|
|
218
|
-
if (!path) return null;
|
|
219
|
-
const reachesTarget = collectNodesReachingTarget(graph, toHash);
|
|
220
|
-
const tieBreakReasons = [];
|
|
221
|
-
let alternativeCount = 0;
|
|
222
|
-
for (const edge of path) {
|
|
223
|
-
const outgoing = graph.forwardChain.get(edge.from);
|
|
224
|
-
if (outgoing && outgoing.length > 1) {
|
|
225
|
-
const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
|
|
226
|
-
if (reachable.length > 1) {
|
|
227
|
-
alternativeCount += reachable.length - 1;
|
|
228
|
-
const sorted = sortedNeighbors(reachable);
|
|
229
|
-
if (sorted[0] && sorted[0].migrationId === edge.migrationId) {
|
|
230
|
-
if (reachable.some((e) => e.migrationId !== edge.migrationId)) tieBreakReasons.push(`at ${edge.from}: ${reachable.length} candidates, selected by tie-break`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
selectedPath: path,
|
|
237
|
-
fromHash,
|
|
238
|
-
toHash,
|
|
239
|
-
alternativeCount,
|
|
240
|
-
tieBreakReasons,
|
|
241
|
-
...ifDefined("refName", refName)
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Walk ancestors of each branch tip back to find the last node
|
|
246
|
-
* that appears on all paths. Returns `fromHash` if no shared ancestor is found.
|
|
247
|
-
*/
|
|
248
|
-
function findDivergencePoint(graph, fromHash, leaves) {
|
|
249
|
-
const ancestorSets = leaves.map((leaf) => {
|
|
250
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
251
|
-
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.node);
|
|
252
|
-
return ancestors;
|
|
253
|
-
});
|
|
254
|
-
const commonAncestors = [...ancestorSets[0] ?? []].filter((node) => ancestorSets.every((s) => s.has(node)));
|
|
255
|
-
let deepest = fromHash;
|
|
256
|
-
let deepestDepth = -1;
|
|
257
|
-
for (const ancestor of commonAncestors) {
|
|
258
|
-
const path = findPath(graph, fromHash, ancestor);
|
|
259
|
-
const depth = path ? path.length : 0;
|
|
260
|
-
if (depth > deepestDepth) {
|
|
261
|
-
deepestDepth = depth;
|
|
262
|
-
deepest = ancestor;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return deepest;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Find all branch tips (nodes with no outgoing edges) reachable from
|
|
269
|
-
* `fromHash` via forward edges.
|
|
270
|
-
*/
|
|
271
|
-
function findReachableLeaves(graph, fromHash) {
|
|
272
|
-
const leaves = [];
|
|
273
|
-
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.node)?.length) leaves.push(step.node);
|
|
274
|
-
return leaves;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Find the target contract hash of the migration graph reachable from
|
|
278
|
-
* EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target
|
|
279
|
-
* state (either empty, or containing only the root with no outgoing
|
|
280
|
-
* edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none
|
|
281
|
-
* originate from the empty hash, and AMBIGUOUS_TARGET if multiple
|
|
282
|
-
* branch tips exist.
|
|
283
|
-
*/
|
|
284
|
-
function findLeaf(graph) {
|
|
285
|
-
if (graph.nodes.size === 0) return null;
|
|
286
|
-
if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) throw errorNoInitialMigration([...graph.nodes]);
|
|
287
|
-
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
|
|
288
|
-
if (leaves.length === 0) {
|
|
289
|
-
const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);
|
|
290
|
-
if (reachable.length > 0) throw errorNoTarget(reachable);
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
if (leaves.length > 1) {
|
|
294
|
-
const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);
|
|
295
|
-
throw errorAmbiguousTarget(leaves, {
|
|
296
|
-
divergencePoint,
|
|
297
|
-
branches: leaves.map((tip) => {
|
|
298
|
-
return {
|
|
299
|
-
tip,
|
|
300
|
-
edges: (findPath(graph, divergencePoint, tip) ?? []).map((e) => ({
|
|
301
|
-
dirName: e.dirName,
|
|
302
|
-
from: e.from,
|
|
303
|
-
to: e.to
|
|
304
|
-
}))
|
|
305
|
-
};
|
|
306
|
-
})
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
return leaves[0];
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
|
|
313
|
-
* to the single target. Returns null for an empty graph.
|
|
314
|
-
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
|
|
315
|
-
*/
|
|
316
|
-
function findLatestMigration(graph) {
|
|
317
|
-
const leafHash = findLeaf(graph);
|
|
318
|
-
if (leafHash === null) return null;
|
|
319
|
-
return findPath(graph, EMPTY_CONTRACT_HASH, leafHash)?.at(-1) ?? null;
|
|
320
|
-
}
|
|
321
|
-
function detectCycles(graph) {
|
|
322
|
-
const WHITE = 0;
|
|
323
|
-
const GRAY = 1;
|
|
324
|
-
const BLACK = 2;
|
|
325
|
-
const color = /* @__PURE__ */ new Map();
|
|
326
|
-
const parentMap = /* @__PURE__ */ new Map();
|
|
327
|
-
const cycles = [];
|
|
328
|
-
for (const node of graph.nodes) color.set(node, WHITE);
|
|
329
|
-
const stack = [];
|
|
330
|
-
function pushFrame(u) {
|
|
331
|
-
color.set(u, GRAY);
|
|
332
|
-
stack.push({
|
|
333
|
-
node: u,
|
|
334
|
-
outgoing: graph.forwardChain.get(u) ?? [],
|
|
335
|
-
index: 0
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
for (const root of graph.nodes) {
|
|
339
|
-
if (color.get(root) !== WHITE) continue;
|
|
340
|
-
parentMap.set(root, null);
|
|
341
|
-
pushFrame(root);
|
|
342
|
-
while (stack.length > 0) {
|
|
343
|
-
const frame = stack[stack.length - 1];
|
|
344
|
-
if (frame.index >= frame.outgoing.length) {
|
|
345
|
-
color.set(frame.node, BLACK);
|
|
346
|
-
stack.pop();
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const v = frame.outgoing[frame.index++].to;
|
|
350
|
-
const vColor = color.get(v);
|
|
351
|
-
if (vColor === GRAY) {
|
|
352
|
-
const cycle = [v];
|
|
353
|
-
let cur = frame.node;
|
|
354
|
-
while (cur !== v) {
|
|
355
|
-
cycle.push(cur);
|
|
356
|
-
cur = parentMap.get(cur) ?? v;
|
|
357
|
-
}
|
|
358
|
-
cycle.reverse();
|
|
359
|
-
cycles.push(cycle);
|
|
360
|
-
} else if (vColor === WHITE) {
|
|
361
|
-
parentMap.set(v, frame.node);
|
|
362
|
-
pushFrame(v);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return cycles;
|
|
367
|
-
}
|
|
368
|
-
function detectOrphans(graph) {
|
|
369
|
-
if (graph.nodes.size === 0) return [];
|
|
370
|
-
const reachable = /* @__PURE__ */ new Set();
|
|
371
|
-
const startNodes = [];
|
|
372
|
-
if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) startNodes.push(EMPTY_CONTRACT_HASH);
|
|
373
|
-
else {
|
|
374
|
-
const allTargets = /* @__PURE__ */ new Set();
|
|
375
|
-
for (const edges of graph.forwardChain.values()) for (const edge of edges) allTargets.add(edge.to);
|
|
376
|
-
for (const node of graph.nodes) if (!allTargets.has(node)) startNodes.push(node);
|
|
377
|
-
}
|
|
378
|
-
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.node);
|
|
379
|
-
const orphans = [];
|
|
380
|
-
for (const [from, migrations] of graph.forwardChain) if (!reachable.has(from)) orphans.push(...migrations);
|
|
381
|
-
return orphans;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
//#endregion
|
|
385
|
-
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
386
|
-
//# sourceMappingURL=dag.mjs.map
|