@prisma-next/migration-tools 0.5.0-dev.21 → 0.5.0-dev.23
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/{constants-DOzBI2EP.mjs → constants-BQEHsaEx.mjs} +1 -1
- package/dist/{constants-DOzBI2EP.mjs.map → constants-BQEHsaEx.mjs.map} +1 -1
- package/dist/{errors-BS_Kq8GF.mjs → errors-CRiMISRK.mjs} +24 -2
- package/dist/errors-CRiMISRK.mjs.map +1 -0
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/errors.d.mts +2 -1
- package/dist/exports/errors.d.mts.map +1 -1
- package/dist/exports/errors.mjs +2 -2
- package/dist/exports/graph.d.mts +1 -1
- package/dist/exports/hash.d.mts +2 -2
- package/dist/exports/invariants.d.mts +1 -1
- package/dist/exports/invariants.mjs +2 -2
- package/dist/exports/io.d.mts +2 -2
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +3 -8
- package/dist/exports/io.mjs.map +1 -1
- package/dist/exports/metadata.d.mts +1 -1
- package/dist/exports/migration-graph.d.mts +23 -3
- package/dist/exports/migration-graph.d.mts.map +1 -1
- package/dist/exports/migration-graph.mjs +126 -48
- package/dist/exports/migration-graph.mjs.map +1 -1
- package/dist/exports/migration.d.mts +1 -1
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +43 -2
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +1 -1
- package/dist/exports/refs.mjs +1 -1
- package/dist/{graph-coc0V7k2.d.mts → graph-BHPv-9Gl.d.mts} +1 -1
- package/dist/{graph-coc0V7k2.d.mts.map → graph-BHPv-9Gl.d.mts.map} +1 -1
- package/dist/{invariants-jlMTqh_Q.mjs → invariants-B915SwZg.mjs} +2 -2
- package/dist/{invariants-jlMTqh_Q.mjs.map → invariants-B915SwZg.mjs.map} +1 -1
- package/dist/{metadata-CdSwaQ2k.d.mts → metadata-DuGfxBC3.d.mts} +1 -1
- package/dist/{metadata-CdSwaQ2k.d.mts.map → metadata-DuGfxBC3.d.mts.map} +1 -1
- package/dist/op-schema-DZKFua46.mjs +14 -0
- package/dist/op-schema-DZKFua46.mjs.map +1 -0
- package/dist/{package-DFjGigEm.d.mts → package-DcpcRqpb.d.mts} +2 -2
- package/dist/package-DcpcRqpb.d.mts.map +1 -0
- package/package.json +6 -6
- package/src/errors.ts +29 -0
- package/src/exports/errors.ts +1 -1
- package/src/exports/migration-graph.ts +1 -0
- package/src/graph-ops.ts +57 -30
- package/src/io.ts +1 -10
- package/src/migration-base.ts +54 -0
- package/src/migration-graph.ts +118 -19
- package/src/op-schema.ts +11 -0
- package/dist/errors-BS_Kq8GF.mjs.map +0 -1
- package/dist/package-DFjGigEm.d.mts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants-
|
|
1
|
+
{"version":3,"file":"constants-BQEHsaEx.mjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Sentinel value representing the absence of a contract (empty/new project).\n * This is a human-readable marker, not a real SHA-256 hash.\n */\nexport const EMPTY_CONTRACT_HASH = 'sha256:empty' as const;\n"],"mappings":";;;;;AAIA,MAAa,sBAAsB"}
|
|
@@ -88,6 +88,28 @@ function errorInvalidManifest(filePath, reason) {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
+
function errorInvalidOperationEntry(index, reason) {
|
|
92
|
+
return new MigrationToolsError("MIGRATION.INVALID_OPERATION_ENTRY", "Migration operation entry is malformed", {
|
|
93
|
+
why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,
|
|
94
|
+
fix: "Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').",
|
|
95
|
+
details: {
|
|
96
|
+
index,
|
|
97
|
+
reason
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function errorStaleContractBookends(args) {
|
|
102
|
+
const { side, metaHash, contractHash } = args;
|
|
103
|
+
return new MigrationToolsError("MIGRATION.STALE_CONTRACT_BOOKENDS", "Migration manifest contract bookends disagree with describe()", {
|
|
104
|
+
why: `migration.json stores ${side}Contract.storage.storageHash "${contractHash}", but describe() returned meta.${side} = "${metaHash}". The bookend is stale — most likely the migration's describe() was edited after the package was scaffolded by \`migration plan\`.`,
|
|
105
|
+
fix: "Re-run `migration plan` to regenerate the package with fresh contract bookends, or restore the directory from version control.",
|
|
106
|
+
details: {
|
|
107
|
+
side,
|
|
108
|
+
metaHash,
|
|
109
|
+
contractHash
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
91
113
|
function errorInvalidSlug(slug) {
|
|
92
114
|
return new MigrationToolsError("MIGRATION.INVALID_NAME", "Invalid migration name", {
|
|
93
115
|
why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
|
|
@@ -218,5 +240,5 @@ function errorMigrationHashMismatch(dir, storedHash, computedHash) {
|
|
|
218
240
|
}
|
|
219
241
|
|
|
220
242
|
//#endregion
|
|
221
|
-
export {
|
|
222
|
-
//# sourceMappingURL=errors-
|
|
243
|
+
export { errorNoInitialMigration as _, errorDuplicateMigrationHash as a, errorSameSourceAndTarget as b, errorInvalidJson as c, errorInvalidRefFile as d, errorInvalidRefName as f, errorMissingFile as g, errorMigrationHashMismatch as h, errorDuplicateInvariantInEdge as i, errorInvalidManifest as l, errorInvalidSlug as m, errorAmbiguousTarget as n, errorInvalidDestName as o, errorInvalidRefValue as p, errorDirectoryExists as r, errorInvalidInvariantId as s, MigrationToolsError as t, errorInvalidOperationEntry as u, errorNoTarget as v, errorStaleContractBookends as x, errorProvidedInvariantsMismatch as y };
|
|
244
|
+
//# sourceMappingURL=errors-CRiMISRK.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-CRiMISRK.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { basename, dirname, relative } from 'pathe';\n\n/**\n * Build the canonical \"re-emit this package\" remediation hint.\n *\n * Every on-disk migration package ships its own `migration.ts` author-time\n * file. Running it regenerates `migration.json` and `ops.json` with the\n * correct hash + metadata, so it is the right primitive whenever a single\n * package's on-disk artifacts are missing, malformed, or otherwise corrupt.\n * Pointing users at `migration plan` would emit a *new* package rather than\n * heal the broken one.\n */\nfunction reemitHint(dir: string, fallback?: string): string {\n const relativeDir = relative(process.cwd(), dir);\n const reemit = `Re-emit the package by running \\`node \"${relativeDir}/migration.ts\"\\``;\n return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;\n}\n\n/**\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, hash\n * verification, migration history reconstruction), distinct from the runtime\n * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,\n * 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: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\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: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\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: `Migration manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_OPERATION_ENTRY',\n 'Migration operation entry is malformed',\n {\n why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,\n fix: \"Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').\",\n details: { index, reason },\n },\n );\n}\n\nexport function errorStaleContractBookends(args: {\n readonly side: 'from' | 'to';\n readonly metaHash: string;\n readonly contractHash: string;\n}): MigrationToolsError {\n const { side, metaHash, contractHash } = args;\n return new MigrationToolsError(\n 'MIGRATION.STALE_CONTRACT_BOOKENDS',\n 'Migration manifest contract bookends disagree with describe()',\n {\n why: `migration.json stores ${side}Contract.storage.storageHash \"${contractHash}\", but describe() returned meta.${side} = \"${metaHash}\". The bookend is stale — most likely the migration's describe() was edited after the package was scaffolded by \\`migration plan\\`.`,\n fix: 'Re-run `migration plan` to regenerate the package with fresh contract bookends, or restore the directory from version control.',\n details: { side, metaHash, contractHash },\n },\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(dir: string, hash: string): MigrationToolsError {\n const dirName = basename(dir);\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: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\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 errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_HASH',\n 'Duplicate migrationHash in migration graph',\n {\n why: `Multiple migrations share migrationHash \"${migrationHash}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',\n details: { migrationHash },\n },\n );\n}\n\nexport function errorInvalidInvariantId(invariantId: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {\n why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,\n fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".',\n details: { invariantId },\n });\n}\n\nexport function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',\n 'Duplicate invariantId on a single migration',\n {\n why: `invariantId \"${invariantId}\" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,\n fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',\n details: { invariantId },\n },\n );\n}\n\nexport function errorProvidedInvariantsMismatch(\n filePath: string,\n stored: readonly string[],\n derived: readonly string[],\n): MigrationToolsError {\n const storedSet = new Set(stored);\n const derivedSet = new Set(derived);\n const missing = [...derivedSet].filter((id) => !storedSet.has(id));\n const extra = [...storedSet].filter((id) => !derivedSet.has(id));\n // When sets agree but arrays don't, the only difference is ordering — call\n // it out so the reader doesn't stare at two visually-identical arrays.\n // Canonical providedInvariants is sorted ascending; a manifest with the\n // same ids in a different order is still a mismatch (the hash check would\n // also fail), but the human-readable diagnostic is otherwise unhelpful.\n const orderingOnly = missing.length === 0 && extra.length === 0;\n const why = orderingOnly\n ? `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.`\n : `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`;\n return new MigrationToolsError(\n 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',\n 'providedInvariants on migration.json disagrees with ops.json',\n {\n why,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, stored, derived, difference: { missing, extra } },\n },\n );\n}\n\nexport function errorMigrationHashMismatch(\n dir: string,\n storedHash: string,\n computedHash: string,\n): MigrationToolsError {\n // Render a cwd-relative path in the human-readable diagnostic so users\n // running CLI commands from the project root see a familiar short path.\n // Keep the absolute path in `details.dir` for machine consumers.\n const relativeDir = relative(process.cwd(), dir);\n return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {\n why: `Stored migrationHash \"${storedHash}\" does not match the recomputed hash \"${computedHash}\" for \"${relativeDir}\". The migration.json or ops.json has been edited or partially written since emit.`,\n fix: reemitHint(dir, 'or restore the directory from version control.'),\n details: { dir, storedHash, computedHash },\n });\n}\n"],"mappings":"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"}
|
|
@@ -28,6 +28,7 @@ declare class MigrationToolsError extends Error {
|
|
|
28
28
|
});
|
|
29
29
|
static is(error: unknown): error is MigrationToolsError;
|
|
30
30
|
}
|
|
31
|
+
declare function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError;
|
|
31
32
|
//#endregion
|
|
32
|
-
export { MigrationToolsError };
|
|
33
|
+
export { MigrationToolsError, errorInvalidJson };
|
|
33
34
|
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/errors.ts"],"sourcesContent":[],"mappings":";;AAkCA
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/errors.ts"],"sourcesContent":[],"mappings":";;AAkCA;;;;;;AAkDA;;;;;;;;;cAlDa,mBAAA,SAA4B,KAAA;;;;;oBAKrB;;;;uBAQK;;sCAWa;;iBA0BtB,gBAAA,wCAAwD"}
|
package/dist/exports/errors.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as MigrationToolsError } from "../errors-
|
|
1
|
+
import { c as errorInvalidJson, t as MigrationToolsError } from "../errors-CRiMISRK.mjs";
|
|
2
2
|
|
|
3
|
-
export { MigrationToolsError };
|
|
3
|
+
export { MigrationToolsError, errorInvalidJson };
|
package/dist/exports/graph.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as MigrationGraph, t as MigrationEdge } from "../graph-
|
|
1
|
+
import { n as MigrationGraph, t as MigrationEdge } from "../graph-BHPv-9Gl.mjs";
|
|
2
2
|
export { type MigrationEdge, type MigrationGraph };
|
package/dist/exports/hash.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as MigrationMetadata } from "../metadata-
|
|
2
|
-
import { n as MigrationPackage, t as MigrationOps } from "../package-
|
|
1
|
+
import { n as MigrationMetadata } from "../metadata-DuGfxBC3.mjs";
|
|
2
|
+
import { n as MigrationPackage, t as MigrationOps } from "../package-DcpcRqpb.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/hash.d.ts
|
|
5
5
|
interface VerifyResult {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "../errors-
|
|
2
|
-
import { n as validateInvariantId, t as deriveProvidedInvariants } from "../invariants-
|
|
1
|
+
import "../errors-CRiMISRK.mjs";
|
|
2
|
+
import { n as validateInvariantId, t as deriveProvidedInvariants } from "../invariants-B915SwZg.mjs";
|
|
3
3
|
|
|
4
4
|
export { deriveProvidedInvariants, validateInvariantId };
|
package/dist/exports/io.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as MigrationMetadata } from "../metadata-
|
|
2
|
-
import { n as MigrationPackage, t as MigrationOps } from "../package-
|
|
1
|
+
import { n as MigrationMetadata } from "../metadata-DuGfxBC3.mjs";
|
|
2
|
+
import { n as MigrationPackage, t as MigrationOps } from "../package-DcpcRqpb.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/io.d.ts
|
|
5
5
|
declare function writeMigrationPackage(dir: string, metadata: MigrationMetadata, ops: MigrationOps): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"io.d.mts","names":[],"sources":["../../src/io.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"io.d.mts","names":[],"sources":["../../src/io.ts"],"sourcesContent":[],"mappings":";;;;iBAsDsB,qBAAA,wBAEV,wBACL,eACJ;;AAJH;;;;;AAiCA;AAaA;AAOA;AAIA;AA4FA;AAiCgB,iBArJM,mBAAA,CAqJ4B,OAAI,EAAA,MAAA,EAAA,KAAA,EAAA,SAAA;;;MAlJnD;iBAUmB,sBAAA,wBAEV,oBACT;iBAImB,iBAAA,mBAAoC,eAAe;iBAInD,oBAAA,eAAmC,QAAQ;iBA4F3C,iBAAA,0BAEnB,iBAAiB;iBA+BJ,sBAAA,YAAkC"}
|
package/dist/exports/io.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { c as errorInvalidJson,
|
|
1
|
+
import { c as errorInvalidJson, g as errorMissingFile, h as errorMigrationHashMismatch, l as errorInvalidManifest, m as errorInvalidSlug, o as errorInvalidDestName, r as errorDirectoryExists, y as errorProvidedInvariantsMismatch } from "../errors-CRiMISRK.mjs";
|
|
2
2
|
import { n as verifyMigrationHash } from "../hash-BARZdVgW.mjs";
|
|
3
|
-
import { t as deriveProvidedInvariants } from "../invariants-
|
|
3
|
+
import { t as deriveProvidedInvariants } from "../invariants-B915SwZg.mjs";
|
|
4
|
+
import { n as MigrationOpsSchema } from "../op-schema-DZKFua46.mjs";
|
|
4
5
|
import { basename, dirname, join } from "pathe";
|
|
5
6
|
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import { type } from "arktype";
|
|
@@ -36,12 +37,6 @@ const MigrationMetadataSchema = type({
|
|
|
36
37
|
}).or("null"),
|
|
37
38
|
createdAt: "string"
|
|
38
39
|
});
|
|
39
|
-
const MigrationOpsSchema = type({
|
|
40
|
-
id: "string",
|
|
41
|
-
label: "string",
|
|
42
|
-
operationClass: "'additive' | 'widening' | 'destructive' | 'data'",
|
|
43
|
-
"invariantId?": "string"
|
|
44
|
-
}).array();
|
|
45
40
|
async function writeMigrationPackage(dir, metadata, ops) {
|
|
46
41
|
await mkdir(dirname(dir), { recursive: true });
|
|
47
42
|
try {
|
package/dist/exports/io.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"io.mjs","names":["manifestRaw: string","opsRaw: string","metadata: MigrationMetadata","ops: MigrationOps","pkg: MigrationPackage","entries: string[]","packages: MigrationPackage[]"],"sources":["../../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n errorProvidedInvariantsMismatch,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationMetadata } from './metadata';\nimport type { MigrationOps, MigrationPackage } from './package';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationMetadataSchema = type({\n from: 'string',\n to: 'string',\n migrationHash: 'string',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n providedInvariants: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nconst MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n 'invariantId?': 'string',\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nconst MigrationOpsSchema = MigrationOpSchema.array();\n\nexport async function writeMigrationPackage(\n dir: string,\n metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<MigrationPackage> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n // Re-derive before the hash check so format/duplicate diagnostics\n // fire with their dedicated codes rather than as a generic hash mismatch.\n const derivedInvariants = deriveProvidedInvariants(ops);\n if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {\n throw errorProvidedInvariantsMismatch(\n manifestPath,\n metadata.providedInvariants,\n derivedInvariants,\n );\n }\n\n const pkg: MigrationPackage = {\n dirName: basename(dir),\n dirPath: dir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);\n }\n\n return pkg;\n}\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly MigrationPackage[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: MigrationPackage[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"io.mjs","names":["manifestRaw: string","opsRaw: string","metadata: MigrationMetadata","ops: MigrationOps","pkg: MigrationPackage","entries: string[]","packages: MigrationPackage[]"],"sources":["../../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n errorProvidedInvariantsMismatch,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationMetadata } from './metadata';\nimport { MigrationOpsSchema } from './op-schema';\nimport type { MigrationOps, MigrationPackage } from './package';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationMetadataSchema = type({\n from: 'string',\n to: 'string',\n migrationHash: 'string',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n providedInvariants: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nexport async function writeMigrationPackage(\n dir: string,\n metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<MigrationPackage> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n // Re-derive before the hash check so format/duplicate diagnostics\n // fire with their dedicated codes rather than as a generic hash mismatch.\n const derivedInvariants = deriveProvidedInvariants(ops);\n if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {\n throw errorProvidedInvariantsMismatch(\n manifestPath,\n metadata.providedInvariants,\n derivedInvariants,\n );\n }\n\n const pkg: MigrationPackage = {\n dirName: basename(dir),\n dirPath: dir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);\n }\n\n return pkg;\n}\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly MigrationPackage[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: MigrationPackage[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as MigrationMetadata, t as MigrationHints } from "../metadata-
|
|
1
|
+
import { n as MigrationMetadata, t as MigrationHints } from "../metadata-DuGfxBC3.mjs";
|
|
2
2
|
export { type MigrationHints, type MigrationMetadata };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as MigrationGraph, t as MigrationEdge } from "../graph-
|
|
2
|
-
import { n as MigrationPackage } from "../package-
|
|
1
|
+
import { n as MigrationGraph, t as MigrationEdge } from "../graph-BHPv-9Gl.mjs";
|
|
2
|
+
import { n as MigrationPackage } from "../package-DcpcRqpb.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/migration-graph.d.ts
|
|
5
5
|
declare function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph;
|
|
@@ -12,6 +12,26 @@ declare function reconstructGraph(packages: readonly MigrationPackage[]): Migrat
|
|
|
12
12
|
* label priority → createdAt → to → migrationHash.
|
|
13
13
|
*/
|
|
14
14
|
declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly MigrationEdge[] | null;
|
|
15
|
+
/**
|
|
16
|
+
* Find the shortest path from `fromHash` to `toHash` whose edges collectively
|
|
17
|
+
* cover every invariant in `required`. Returns `null` when no such path exists
|
|
18
|
+
* (either `fromHash`→`toHash` is structurally unreachable, or every reachable
|
|
19
|
+
* path leaves at least one required invariant uncovered). When `required` is
|
|
20
|
+
* empty, delegates to `findPath` so the result is byte-identical for that case.
|
|
21
|
+
*
|
|
22
|
+
* Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.
|
|
23
|
+
* The covered subset is a `Set<string>` of invariant ids; the state's dedup
|
|
24
|
+
* key is `${node}\0${[...covered].sort().join('\0')}`. State keys distinguish
|
|
25
|
+
* distinct `(node, covered)` tuples regardless of node-name length because
|
|
26
|
+
* `\0` cannot appear in any invariant id (validation rejects whitespace and
|
|
27
|
+
* control chars at authoring time).
|
|
28
|
+
*
|
|
29
|
+
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
30
|
+
* invariant come first, with `labelPriority → createdAt → to → migrationHash`
|
|
31
|
+
* as the secondary key. The heuristic steers BFS toward the satisfying path;
|
|
32
|
+
* correctness (shortest, deterministic) does not depend on it.
|
|
33
|
+
*/
|
|
34
|
+
declare function findPathWithInvariants(graph: MigrationGraph, fromHash: string, toHash: string, required: ReadonlySet<string>): readonly MigrationEdge[] | null;
|
|
15
35
|
interface PathDecision {
|
|
16
36
|
readonly selectedPath: readonly MigrationEdge[];
|
|
17
37
|
readonly fromHash: string;
|
|
@@ -48,5 +68,5 @@ declare function findLatestMigration(graph: MigrationGraph): MigrationEdge | nul
|
|
|
48
68
|
declare function detectCycles(graph: MigrationGraph): readonly string[][];
|
|
49
69
|
declare function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[];
|
|
50
70
|
//#endregion
|
|
51
|
-
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
71
|
+
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findPathWithInvariants, findReachableLeaves, reconstructGraph };
|
|
52
72
|
//# sourceMappingURL=migration-graph.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/migration-graph.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/migration-graph.ts"],"sourcesContent":[],"mappings":";;;;iBAsCgB,gBAAA,oBAAoC,qBAAqB;;AAAzE;AA6EA;AAgDA;;;;;AA0FiB,iBA1ID,QAAA,CA2IkB,KAAA,EA1IzB,cA0IsC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SAvInC,aAuImC,EAAA,GAAA,IAAA;AAY/C;AA8FA;AAkBA;AAwCA;AAQA;AA8DA;;;;;;;;;;;;;;iBArUgB,sBAAA,QACP,4DAGG,+BACA;UAqFK,YAAA;kCACiB;;;;;;;;;;;iBAYlB,oBAAA,QACP,qEAIN;;;;;iBAyFa,mBAAA,QAA2B;;;;;;;;;iBAkB3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAQ5C,YAAA,QAAoB;iBA8DpB,aAAA,QAAqB,0BAA0B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
import { t as EMPTY_CONTRACT_HASH } from "../constants-
|
|
1
|
+
import { _ as errorNoInitialMigration, a as errorDuplicateMigrationHash, b as errorSameSourceAndTarget, n as errorAmbiguousTarget, v as errorNoTarget } from "../errors-CRiMISRK.mjs";
|
|
2
|
+
import { t as EMPTY_CONTRACT_HASH } from "../constants-BQEHsaEx.mjs";
|
|
3
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
4
|
|
|
5
5
|
//#region src/queue.ts
|
|
@@ -36,61 +36,65 @@ var Queue = class {
|
|
|
36
36
|
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region src/graph-ops.ts
|
|
39
|
-
|
|
40
|
-
* Generic breadth-first traversal.
|
|
41
|
-
*
|
|
42
|
-
* Direction (forward/reverse) is expressed by the caller's `neighbours`
|
|
43
|
-
* closure: return `{ next, edge }` pairs where `next` is the node to visit
|
|
44
|
-
* next and `edge` is the edge that connects them. Callers that don't need
|
|
45
|
-
* path reconstruction can ignore the `parent`/`incomingEdge` fields of each
|
|
46
|
-
* yielded step.
|
|
47
|
-
*
|
|
48
|
-
* Stops are intrinsic — callers `break` out of the `for..of` loop when
|
|
49
|
-
* they've found what they're looking for.
|
|
50
|
-
*
|
|
51
|
-
* `ordering`, if provided, controls the order in which neighbours of each
|
|
52
|
-
* node are enqueued. Only matters for path-finding: a deterministic ordering
|
|
53
|
-
* makes BFS return a deterministic shortest path when multiple exist.
|
|
54
|
-
*/
|
|
55
|
-
function* bfs(starts, neighbours, ordering) {
|
|
39
|
+
function* bfs(starts, neighbours, key = (state) => state) {
|
|
56
40
|
const visited = /* @__PURE__ */ new Set();
|
|
57
41
|
const parentMap = /* @__PURE__ */ new Map();
|
|
58
42
|
const queue = new Queue();
|
|
59
|
-
for (const start of starts)
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
for (const start of starts) {
|
|
44
|
+
const k = key(start);
|
|
45
|
+
if (!visited.has(k)) {
|
|
46
|
+
visited.add(k);
|
|
47
|
+
queue.push({
|
|
48
|
+
state: start,
|
|
49
|
+
key: k
|
|
50
|
+
});
|
|
51
|
+
}
|
|
62
52
|
}
|
|
63
53
|
while (!queue.isEmpty) {
|
|
64
|
-
const current = queue.shift();
|
|
65
|
-
const parentInfo = parentMap.get(
|
|
54
|
+
const { state: current, key: curKey } = queue.shift();
|
|
55
|
+
const parentInfo = parentMap.get(curKey);
|
|
66
56
|
yield {
|
|
67
|
-
|
|
57
|
+
state: current,
|
|
68
58
|
parent: parentInfo?.parent ?? null,
|
|
69
59
|
incomingEdge: parentInfo?.edge ?? null
|
|
70
60
|
};
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
for (const { next, edge } of neighbours(current)) {
|
|
62
|
+
const k = key(next);
|
|
63
|
+
if (!visited.has(k)) {
|
|
64
|
+
visited.add(k);
|
|
65
|
+
parentMap.set(k, {
|
|
66
|
+
parent: current,
|
|
67
|
+
edge
|
|
68
|
+
});
|
|
69
|
+
queue.push({
|
|
70
|
+
state: next,
|
|
71
|
+
key: k
|
|
72
|
+
});
|
|
73
|
+
}
|
|
80
74
|
}
|
|
81
75
|
}
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
//#endregion
|
|
85
79
|
//#region src/migration-graph.ts
|
|
86
|
-
/** Forward-edge neighbours
|
|
80
|
+
/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */
|
|
87
81
|
function forwardNeighbours(graph, node) {
|
|
88
82
|
return (graph.forwardChain.get(node) ?? []).map((edge) => ({
|
|
89
83
|
next: edge.to,
|
|
90
84
|
edge
|
|
91
85
|
}));
|
|
92
86
|
}
|
|
93
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Forward-edge neighbours, sorted by the deterministic tie-break.
|
|
89
|
+
* Used by path-finding so the resulting shortest path is stable across runs.
|
|
90
|
+
*/
|
|
91
|
+
function sortedForwardNeighbours(graph, node) {
|
|
92
|
+
return [...graph.forwardChain.get(node) ?? []].sort(compareTieBreak).map((edge) => ({
|
|
93
|
+
next: edge.to,
|
|
94
|
+
edge
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */
|
|
94
98
|
function reverseNeighbours(graph, node) {
|
|
95
99
|
return (graph.reverseChain.get(node) ?? []).map((edge) => ({
|
|
96
100
|
next: edge.from,
|
|
@@ -158,10 +162,6 @@ function compareTieBreak(a, b) {
|
|
|
158
162
|
function sortedNeighbors(edges) {
|
|
159
163
|
return [...edges].sort(compareTieBreak);
|
|
160
164
|
}
|
|
161
|
-
/** Ordering adapter for `bfs` — sorts `{next, edge}` pairs by tie-break. */
|
|
162
|
-
function bfsOrdering(items) {
|
|
163
|
-
return items.slice().sort((a, b) => compareTieBreak(a.edge, b.edge));
|
|
164
|
-
}
|
|
165
165
|
/**
|
|
166
166
|
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
167
167
|
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
@@ -173,12 +173,12 @@ function bfsOrdering(items) {
|
|
|
173
173
|
function findPath(graph, fromHash, toHash) {
|
|
174
174
|
if (fromHash === toHash) return [];
|
|
175
175
|
const parents = /* @__PURE__ */ new Map();
|
|
176
|
-
for (const step of bfs([fromHash], (n) =>
|
|
177
|
-
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.
|
|
176
|
+
for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {
|
|
177
|
+
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.state, {
|
|
178
178
|
parent: step.parent,
|
|
179
179
|
edge: step.incomingEdge
|
|
180
180
|
});
|
|
181
|
-
if (step.
|
|
181
|
+
if (step.state === toHash) {
|
|
182
182
|
const path = [];
|
|
183
183
|
let cur = toHash;
|
|
184
184
|
let p = parents.get(cur);
|
|
@@ -194,12 +194,90 @@ function findPath(graph, fromHash, toHash) {
|
|
|
194
194
|
return null;
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
|
+
* Find the shortest path from `fromHash` to `toHash` whose edges collectively
|
|
198
|
+
* cover every invariant in `required`. Returns `null` when no such path exists
|
|
199
|
+
* (either `fromHash`→`toHash` is structurally unreachable, or every reachable
|
|
200
|
+
* path leaves at least one required invariant uncovered). When `required` is
|
|
201
|
+
* empty, delegates to `findPath` so the result is byte-identical for that case.
|
|
202
|
+
*
|
|
203
|
+
* Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.
|
|
204
|
+
* The covered subset is a `Set<string>` of invariant ids; the state's dedup
|
|
205
|
+
* key is `${node}\0${[...covered].sort().join('\0')}`. State keys distinguish
|
|
206
|
+
* distinct `(node, covered)` tuples regardless of node-name length because
|
|
207
|
+
* `\0` cannot appear in any invariant id (validation rejects whitespace and
|
|
208
|
+
* control chars at authoring time).
|
|
209
|
+
*
|
|
210
|
+
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
211
|
+
* invariant come first, with `labelPriority → createdAt → to → migrationHash`
|
|
212
|
+
* as the secondary key. The heuristic steers BFS toward the satisfying path;
|
|
213
|
+
* correctness (shortest, deterministic) does not depend on it.
|
|
214
|
+
*/
|
|
215
|
+
function findPathWithInvariants(graph, fromHash, toHash, required) {
|
|
216
|
+
if (required.size === 0) return findPath(graph, fromHash, toHash);
|
|
217
|
+
if (fromHash === toHash) return null;
|
|
218
|
+
const stateKey = (s) => {
|
|
219
|
+
if (s.covered.size === 0) return `${s.node}\0`;
|
|
220
|
+
return `${s.node}\0${[...s.covered].sort().join("\0")}`;
|
|
221
|
+
};
|
|
222
|
+
const neighbours = (s) => {
|
|
223
|
+
const outgoing = graph.forwardChain.get(s.node) ?? [];
|
|
224
|
+
if (outgoing.length === 0) return [];
|
|
225
|
+
return [...outgoing].map((edge) => {
|
|
226
|
+
let useful = false;
|
|
227
|
+
let next = null;
|
|
228
|
+
for (const inv of edge.invariants) if (required.has(inv) && !s.covered.has(inv)) {
|
|
229
|
+
if (next === null) next = new Set(s.covered);
|
|
230
|
+
next.add(inv);
|
|
231
|
+
useful = true;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
edge,
|
|
235
|
+
useful,
|
|
236
|
+
nextCovered: next ?? s.covered
|
|
237
|
+
};
|
|
238
|
+
}).sort((a, b) => {
|
|
239
|
+
if (a.useful !== b.useful) return a.useful ? -1 : 1;
|
|
240
|
+
return compareTieBreak(a.edge, b.edge);
|
|
241
|
+
}).map(({ edge, nextCovered }) => ({
|
|
242
|
+
next: {
|
|
243
|
+
node: edge.to,
|
|
244
|
+
covered: nextCovered
|
|
245
|
+
},
|
|
246
|
+
edge
|
|
247
|
+
}));
|
|
248
|
+
};
|
|
249
|
+
const parents = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const step of bfs([{
|
|
251
|
+
node: fromHash,
|
|
252
|
+
covered: /* @__PURE__ */ new Set()
|
|
253
|
+
}], neighbours, stateKey)) {
|
|
254
|
+
const curKey = stateKey(step.state);
|
|
255
|
+
if (step.parent !== null && step.incomingEdge !== null) parents.set(curKey, {
|
|
256
|
+
parentKey: stateKey(step.parent),
|
|
257
|
+
edge: step.incomingEdge
|
|
258
|
+
});
|
|
259
|
+
if (step.state.node === toHash && step.state.covered.size === required.size) {
|
|
260
|
+
const path = [];
|
|
261
|
+
let cur = curKey;
|
|
262
|
+
while (cur !== void 0) {
|
|
263
|
+
const p = parents.get(cur);
|
|
264
|
+
if (!p) break;
|
|
265
|
+
path.push(p.edge);
|
|
266
|
+
cur = p.parentKey;
|
|
267
|
+
}
|
|
268
|
+
path.reverse();
|
|
269
|
+
return path;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
197
275
|
* Reverse-BFS from `toHash` over `reverseChain` to collect every node from
|
|
198
276
|
* which `toHash` is reachable (inclusive of `toHash` itself).
|
|
199
277
|
*/
|
|
200
278
|
function collectNodesReachingTarget(graph, toHash) {
|
|
201
279
|
const reached = /* @__PURE__ */ new Set();
|
|
202
|
-
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.
|
|
280
|
+
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.state);
|
|
203
281
|
return reached;
|
|
204
282
|
}
|
|
205
283
|
/**
|
|
@@ -249,7 +327,7 @@ function findPathWithDecision(graph, fromHash, toHash, refName) {
|
|
|
249
327
|
function findDivergencePoint(graph, fromHash, leaves) {
|
|
250
328
|
const ancestorSets = leaves.map((leaf) => {
|
|
251
329
|
const ancestors = /* @__PURE__ */ new Set();
|
|
252
|
-
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.
|
|
330
|
+
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.state);
|
|
253
331
|
return ancestors;
|
|
254
332
|
});
|
|
255
333
|
const commonAncestors = [...ancestorSets[0] ?? []].filter((node) => ancestorSets.every((s) => s.has(node)));
|
|
@@ -271,7 +349,7 @@ function findDivergencePoint(graph, fromHash, leaves) {
|
|
|
271
349
|
*/
|
|
272
350
|
function findReachableLeaves(graph, fromHash) {
|
|
273
351
|
const leaves = [];
|
|
274
|
-
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.
|
|
352
|
+
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.state)?.length) leaves.push(step.state);
|
|
275
353
|
return leaves;
|
|
276
354
|
}
|
|
277
355
|
/**
|
|
@@ -376,12 +454,12 @@ function detectOrphans(graph) {
|
|
|
376
454
|
for (const edges of graph.forwardChain.values()) for (const edge of edges) allTargets.add(edge.to);
|
|
377
455
|
for (const node of graph.nodes) if (!allTargets.has(node)) startNodes.push(node);
|
|
378
456
|
}
|
|
379
|
-
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.
|
|
457
|
+
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.state);
|
|
380
458
|
const orphans = [];
|
|
381
459
|
for (const [from, migrations] of graph.forwardChain) if (!reachable.has(from)) orphans.push(...migrations);
|
|
382
460
|
return orphans;
|
|
383
461
|
}
|
|
384
462
|
|
|
385
463
|
//#endregion
|
|
386
|
-
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
464
|
+
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findPathWithInvariants, findReachableLeaves, reconstructGraph };
|
|
387
465
|
//# sourceMappingURL=migration-graph.mjs.map
|