@prisma-next/migration-tools 0.5.0-dev.36 → 0.5.0-dev.37

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.
@@ -1,3 +1,4 @@
1
+ import { ifDefined } from "@prisma-next/utils/defined";
1
2
  import { basename, dirname, relative } from "pathe";
2
3
 
3
4
  //#region src/errors.ts
@@ -126,9 +127,9 @@ function errorInvalidDestName(destName) {
126
127
  }
127
128
  function errorSameSourceAndTarget(dir, hash) {
128
129
  const dirName = basename(dir);
129
- return new MigrationToolsError("MIGRATION.SAME_SOURCE_AND_TARGET", "Migration has same source and target", {
130
- why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
131
- fix: reemitHint(dir, "or delete the directory if the migration is unwanted and the source TypeScript is gone."),
130
+ return new MigrationToolsError("MIGRATION.SAME_SOURCE_AND_TARGET", "Migration without data-transform operations has same source and target", {
131
+ why: `Migration "${dirName}" has from === to === "${hash}" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform otherwise the migration is a no-op.`,
132
+ fix: reemitHint(dir, "and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted."),
132
133
  details: {
133
134
  dirName,
134
135
  hash
@@ -227,6 +228,33 @@ function errorProvidedInvariantsMismatch(filePath, stored, derived) {
227
228
  }
228
229
  });
229
230
  }
231
+ function errorNoInvariantPath(args) {
232
+ const { refName, required, missing, structuralPath } = args;
233
+ const refClause = refName ? `Ref "${refName}"` : "Target";
234
+ const missingList = missing.map((id) => JSON.stringify(id)).join(", ");
235
+ return new MigrationToolsError("MIGRATION.NO_INVARIANT_PATH", "No path covers the required invariants", {
236
+ why: `${refClause} requires invariants the reachable path doesn't cover. required=[${required.map((id) => JSON.stringify(id)).join(", ")}], missing=[${missingList}].`,
237
+ fix: "Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.",
238
+ details: {
239
+ required,
240
+ missing,
241
+ structuralPath,
242
+ ...ifDefined("refName", refName)
243
+ }
244
+ });
245
+ }
246
+ function errorUnknownInvariant(args) {
247
+ const { refName, unknown, declared } = args;
248
+ return new MigrationToolsError("MIGRATION.UNKNOWN_INVARIANT", "Ref declares invariants no migration in the graph provides", {
249
+ why: `${refName ? `Ref "${refName}" declares` : "Declares"} invariants no migration in the graph provides. unknown=[${unknown.map((id) => JSON.stringify(id)).join(", ")}].`,
250
+ fix: "Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.",
251
+ details: {
252
+ unknown,
253
+ declared,
254
+ ...ifDefined("refName", refName)
255
+ }
256
+ });
257
+ }
230
258
  function errorMigrationHashMismatch(dir, storedHash, computedHash) {
231
259
  return new MigrationToolsError("MIGRATION.HASH_MISMATCH", "Migration package is corrupt", {
232
260
  why: `Stored migrationHash "${storedHash}" does not match the recomputed hash "${computedHash}" for "${relative(process.cwd(), dir)}". The migration.json or ops.json has been edited or partially written since emit.`,
@@ -240,5 +268,5 @@ function errorMigrationHashMismatch(dir, storedHash, computedHash) {
240
268
  }
241
269
 
242
270
  //#endregion
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-Bl3cKiM8.mjs.map
271
+ export { errorUnknownInvariant as C, errorStaleContractBookends as S, errorNoInitialMigration as _, errorDuplicateMigrationHash as a, errorProvidedInvariantsMismatch 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, errorNoInvariantPath as v, errorSameSourceAndTarget as x, errorNoTarget as y };
272
+ //# sourceMappingURL=errors-CfmjBeK0.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-CfmjBeK0.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { ifDefined } from '@prisma-next/utils/defined';\nimport { 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 | null;\n readonly contractHash: string;\n}): MigrationToolsError {\n const { side, metaHash, contractHash } = args;\n // `meta.from` is `string | null` (null = baseline). Render `null` as a\n // human-readable token in the diagnostic so the message stays clear when\n // the mismatch is a baseline-vs-non-baseline disagreement.\n const renderedMetaHash = metaHash === null ? 'null (baseline)' : `\"${metaHash}\"`;\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} = ${renderedMetaHash}. 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 without data-transform operations has same source and target',\n {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform — otherwise the migration is a no-op.`,\n fix: reemitHint(\n dir,\n 'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',\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\n/**\n * Wire-shape edge surfaced through the JSON envelope's\n * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —\n * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but\n * is intentionally dropped here so the envelope stays stable across\n * graph-internal refactors.\n *\n * Stability: any field added here is part of the public CLI JSON contract.\n * Callers (CLI consumers, agents) must be able to treat\n * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.\n */\nexport interface NoInvariantPathStructuralEdge {\n readonly dirName: string;\n readonly migrationHash: string;\n readonly from: string;\n readonly to: string;\n readonly invariants: readonly string[];\n}\n\nexport function errorNoInvariantPath(args: {\n readonly refName?: string;\n readonly required: readonly string[];\n readonly missing: readonly string[];\n readonly structuralPath: readonly NoInvariantPathStructuralEdge[];\n}): MigrationToolsError {\n const { refName, required, missing, structuralPath } = args;\n const refClause = refName ? `Ref \"${refName}\"` : 'Target';\n const missingList = missing.map((id) => JSON.stringify(id)).join(', ');\n const requiredList = required.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.NO_INVARIANT_PATH',\n 'No path covers the required invariants',\n {\n why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,\n fix: 'Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.',\n details: {\n required,\n missing,\n structuralPath,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorUnknownInvariant(args: {\n readonly refName?: string;\n readonly unknown: readonly string[];\n readonly declared: readonly string[];\n}): MigrationToolsError {\n const { refName, unknown, declared } = args;\n const refClause = refName ? `Ref \"${refName}\" declares` : 'Declares';\n const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.UNKNOWN_INVARIANT',\n 'Ref declares invariants no migration in the graph provides',\n {\n why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,\n fix: 'Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.',\n details: {\n unknown,\n declared,\n ...ifDefined('refName', refName),\n },\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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"}
@@ -29,6 +29,35 @@ declare class MigrationToolsError extends Error {
29
29
  static is(error: unknown): error is MigrationToolsError;
30
30
  }
31
31
  declare function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError;
32
+ /**
33
+ * Wire-shape edge surfaced through the JSON envelope's
34
+ * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —
35
+ * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but
36
+ * is intentionally dropped here so the envelope stays stable across
37
+ * graph-internal refactors.
38
+ *
39
+ * Stability: any field added here is part of the public CLI JSON contract.
40
+ * Callers (CLI consumers, agents) must be able to treat
41
+ * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.
42
+ */
43
+ interface NoInvariantPathStructuralEdge {
44
+ readonly dirName: string;
45
+ readonly migrationHash: string;
46
+ readonly from: string;
47
+ readonly to: string;
48
+ readonly invariants: readonly string[];
49
+ }
50
+ declare function errorNoInvariantPath(args: {
51
+ readonly refName?: string;
52
+ readonly required: readonly string[];
53
+ readonly missing: readonly string[];
54
+ readonly structuralPath: readonly NoInvariantPathStructuralEdge[];
55
+ }): MigrationToolsError;
56
+ declare function errorUnknownInvariant(args: {
57
+ readonly refName?: string;
58
+ readonly unknown: readonly string[];
59
+ readonly declared: readonly string[];
60
+ }): MigrationToolsError;
32
61
  //#endregion
33
- export { MigrationToolsError, errorInvalidJson };
62
+ export { MigrationToolsError, type NoInvariantPathStructuralEdge, errorInvalidJson, errorNoInvariantPath, errorUnknownInvariant };
34
63
  //# sourceMappingURL=errors.d.mts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/errors.ts"],"sourcesContent":[],"mappings":";;AAmCA;;;;;;AAkDA;AAgOA;AAQA;AA0BA;;;;;;cApTa,mBAAA,SAA4B,KAAA;;;;;oBAKrB;;;;uBAQK;;sCAWa;;iBA0BtB,gBAAA,wCAAwD;;;;;;;;;;;;UAgOvD,6BAAA;;;;;;;iBAQD,oBAAA;;;;oCAIoB;IAChC;iBAqBY,qBAAA;;;;IAIZ"}
@@ -1,3 +1,3 @@
1
- import { c as errorInvalidJson, t as MigrationToolsError } from "../errors-Bl3cKiM8.mjs";
1
+ import { C as errorUnknownInvariant, c as errorInvalidJson, t as MigrationToolsError, v as errorNoInvariantPath } from "../errors-CfmjBeK0.mjs";
2
2
 
3
- export { MigrationToolsError, errorInvalidJson };
3
+ export { MigrationToolsError, errorInvalidJson, errorNoInvariantPath, errorUnknownInvariant };
@@ -1,4 +1,4 @@
1
- import "../errors-Bl3cKiM8.mjs";
2
- import { n as validateInvariantId, t as deriveProvidedInvariants } from "../invariants-BmrTBQ0A.mjs";
1
+ import "../errors-CfmjBeK0.mjs";
2
+ import { n as validateInvariantId, t as deriveProvidedInvariants } from "../invariants-30VA65sB.mjs";
3
3
 
4
4
  export { deriveProvidedInvariants, validateInvariantId };
@@ -1,6 +1,6 @@
1
- import { c as errorInvalidJson, g as errorMissingFile, h as errorMigrationHashMismatch, l as errorInvalidManifest, m as errorInvalidSlug, o as errorInvalidDestName, r as errorDirectoryExists, y as errorProvidedInvariantsMismatch } from "../errors-Bl3cKiM8.mjs";
1
+ import { b as errorProvidedInvariantsMismatch, c as errorInvalidJson, g as errorMissingFile, h as errorMigrationHashMismatch, l as errorInvalidManifest, m as errorInvalidSlug, o as errorInvalidDestName, r as errorDirectoryExists } from "../errors-CfmjBeK0.mjs";
2
2
  import { n as verifyMigrationHash } from "../hash-BARZdVgW.mjs";
3
- import { t as deriveProvidedInvariants } from "../invariants-BmrTBQ0A.mjs";
3
+ import { t as deriveProvidedInvariants } from "../invariants-30VA65sB.mjs";
4
4
  import { n as MigrationOpsSchema } from "../op-schema-DZKFua46.mjs";
5
5
  import { basename, dirname, join } from "pathe";
6
6
  import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
@@ -39,12 +39,64 @@ interface PathDecision {
39
39
  readonly alternativeCount: number;
40
40
  readonly tieBreakReasons: readonly string[];
41
41
  readonly refName?: string;
42
+ /** The caller-supplied required invariant set, sorted ascending. */
43
+ readonly requiredInvariants: readonly string[];
44
+ /**
45
+ * The subset of `requiredInvariants` actually covered by edges on
46
+ * `selectedPath`. Always a subset of `requiredInvariants` (when the path
47
+ * is satisfying, equal to it); always derived from `selectedPath`.
48
+ */
49
+ readonly satisfiedInvariants: readonly string[];
50
+ }
51
+ /**
52
+ * Outcome of {@link findPathWithDecision}. The pathfinder distinguishes
53
+ * three cases up front so callers don't re-derive structural reachability:
54
+ *
55
+ * - `ok` — a path covering `required` exists; `decision` carries the
56
+ * selection metadata and per-edge invariants.
57
+ * - `unreachable` — `from`→`to` has no structural path. Mapped by callers
58
+ * to the existing no-path / `NO_TARGET` diagnostic.
59
+ * - `unsatisfiable` — `from`→`to` is structurally reachable but no path
60
+ * covers every required invariant. `structuralPath` is the
61
+ * `findPath(graph, from, to)` result, included so callers don't have to
62
+ * recompute it when raising `MIGRATION.NO_INVARIANT_PATH`. `missing` is
63
+ * the subset of `required` that the structural path does *not* cover —
64
+ * correctly accounts for partial coverage when some required invariants
65
+ * are met by the fallback path. Only emitted when `required` is
66
+ * non-empty.
67
+ */
68
+ type FindPathOutcome = {
69
+ readonly kind: 'ok';
70
+ readonly decision: PathDecision;
71
+ } | {
72
+ readonly kind: 'unreachable';
73
+ } | {
74
+ readonly kind: 'unsatisfiable';
75
+ readonly structuralPath: readonly MigrationEdge[];
76
+ readonly missing: readonly string[];
77
+ };
78
+ /**
79
+ * Routing context for {@link findPathWithDecision}. Both fields are optional;
80
+ * `refName` is only used to decorate the resulting `PathDecision` for the
81
+ * JSON envelope, and `required` defaults to an empty set (purely structural
82
+ * routing). They are passed via a single options object so the call sites
83
+ * cannot silently swap two adjacent string parameters.
84
+ */
85
+ interface FindPathWithDecisionOptions {
86
+ readonly refName?: string;
87
+ readonly required?: ReadonlySet<string>;
42
88
  }
43
89
  /**
44
90
  * Find the shortest path from `fromHash` to `toHash` and return structured
45
- * path-decision metadata for machine-readable output.
91
+ * path-decision metadata for machine-readable output. When `required` is
92
+ * non-empty, the returned path is the shortest one whose edges collectively
93
+ * cover every required invariant.
94
+ *
95
+ * The discriminated return type tells the caller *why* a path could not be
96
+ * found, so the CLI can pick the right structured error without re-running
97
+ * a structural BFS.
46
98
  */
47
- declare function findPathWithDecision(graph: MigrationGraph, fromHash: string, toHash: string, refName?: string): PathDecision | null;
99
+ declare function findPathWithDecision(graph: MigrationGraph, fromHash: string, toHash: string, options?: FindPathWithDecisionOptions): FindPathOutcome;
48
100
  /**
49
101
  * Find all branch tips (nodes with no outgoing edges) reachable from
50
102
  * `fromHash` via forward edges.
@@ -1 +1 @@
1
- {"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/migration-graph.ts"],"sourcesContent":[],"mappings":";;;;iBAsCgB,gBAAA,oBAAoC,qBAAqB;;AAAzE;AAkFA;AAgDA;;;;;AA0FiB,iBA1ID,QAAA,CA2IkB,KAAA,EA1IzB,cA0IsC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SAvInC,aAuImC,EAAA,GAAA,IAAA;AAY/C;AA8FA;AAkBA;AAwCA;AAQA;AA8DA;;;;;;;;;;;;;;iBArUgB,sBAAA,QACP,4DAGG,+BACA;UAqFK,YAAA;kCACiB;;;;;;;;;;;iBAYlB,oBAAA,QACP,qEAIN;;;;;iBAyFa,mBAAA,QAA2B;;;;;;;;;iBAkB3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAQ5C,YAAA,QAAoB;iBA8DpB,aAAA,QAAqB,0BAA0B"}
1
+ {"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/migration-graph.ts"],"sourcesContent":[],"mappings":";;;;iBAsCgB,gBAAA,oBAAoC,qBAAqB;;AAAzE;AAqFA;AAgDA;;;;;AA2FiB,iBA3ID,QAAA,CA4IkB,KAAA,EA3IzB,cA2IsC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SAxInC,aAwImC,EAAA,GAAA,IAAA;AAiC/C;AAgBA;AAeA;;;;;AA0LA;AAkBA;AAwCA;AAQA;AA8DA;;;;;;;;iBAtdgB,sBAAA,QACP,4DAGG,+BACA;UAsFK,YAAA;kCACiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAiCtB,eAAA;;qBACkC;;;;;oCAIN;;;;;;;;;;UAWvB,2BAAA;;sBAEK;;;;;;;;;;;;iBAaN,oBAAA,QACP,4DAGE,8BACR;;;;;iBAqLa,mBAAA,QAA2B;;;;;;;;;iBAkB3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAQ5C,YAAA,QAAoB;iBA8DpB,aAAA,QAAqB,0BAA0B"}
@@ -1,4 +1,4 @@
1
- import { _ as errorNoInitialMigration, a as errorDuplicateMigrationHash, b as errorSameSourceAndTarget, n as errorAmbiguousTarget, v as errorNoTarget } from "../errors-Bl3cKiM8.mjs";
1
+ import { _ as errorNoInitialMigration, a as errorDuplicateMigrationHash, n as errorAmbiguousTarget, x as errorSameSourceAndTarget, y as errorNoTarget } from "../errors-CfmjBeK0.mjs";
2
2
  import { t as EMPTY_CONTRACT_HASH } from "../constants-BQEHsaEx.mjs";
3
3
  import { ifDefined } from "@prisma-next/utils/defined";
4
4
 
@@ -114,7 +114,9 @@ function reconstructGraph(packages) {
114
114
  for (const pkg of packages) {
115
115
  const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;
116
116
  const { to } = pkg.metadata;
117
- if (from === to) throw errorSameSourceAndTarget(pkg.dirPath, from);
117
+ if (from === to) {
118
+ if (!pkg.ops.some((op) => op.operationClass === "data")) throw errorSameSourceAndTarget(pkg.dirPath, from);
119
+ }
118
120
  nodes.add(from);
119
121
  nodes.add(to);
120
122
  const migration = {
@@ -215,7 +217,6 @@ function findPath(graph, fromHash, toHash) {
215
217
  */
216
218
  function findPathWithInvariants(graph, fromHash, toHash, required) {
217
219
  if (required.size === 0) return findPath(graph, fromHash, toHash);
218
- if (fromHash === toHash) return null;
219
220
  const stateKey = (s) => {
220
221
  if (s.covered.size === 0) return `${s.node}\0`;
221
222
  return `${s.node}\0${[...s.covered].sort().join("\0")}`;
@@ -283,44 +284,103 @@ function collectNodesReachingTarget(graph, toHash) {
283
284
  }
284
285
  /**
285
286
  * Find the shortest path from `fromHash` to `toHash` and return structured
286
- * path-decision metadata for machine-readable output.
287
+ * path-decision metadata for machine-readable output. When `required` is
288
+ * non-empty, the returned path is the shortest one whose edges collectively
289
+ * cover every required invariant.
290
+ *
291
+ * The discriminated return type tells the caller *why* a path could not be
292
+ * found, so the CLI can pick the right structured error without re-running
293
+ * a structural BFS.
287
294
  */
288
- function findPathWithDecision(graph, fromHash, toHash, refName) {
289
- if (fromHash === toHash) return {
290
- selectedPath: [],
291
- fromHash,
292
- toHash,
293
- alternativeCount: 0,
294
- tieBreakReasons: [],
295
- ...ifDefined("refName", refName)
295
+ function findPathWithDecision(graph, fromHash, toHash, options = {}) {
296
+ const { refName, required = /* @__PURE__ */ new Set() } = options;
297
+ const requiredInvariants = [...required].sort();
298
+ if (fromHash === toHash && required.size === 0) return {
299
+ kind: "ok",
300
+ decision: {
301
+ selectedPath: [],
302
+ fromHash,
303
+ toHash,
304
+ alternativeCount: 0,
305
+ tieBreakReasons: [],
306
+ requiredInvariants,
307
+ satisfiedInvariants: [],
308
+ ...ifDefined("refName", refName)
309
+ }
296
310
  };
297
- const path = findPath(graph, fromHash, toHash);
298
- if (!path) return null;
311
+ const path = findPathWithInvariants(graph, fromHash, toHash, required);
312
+ if (!path) {
313
+ if (required.size === 0) return { kind: "unreachable" };
314
+ const structural = findPath(graph, fromHash, toHash);
315
+ if (structural === null) return { kind: "unreachable" };
316
+ const coveredByStructural = /* @__PURE__ */ new Set();
317
+ for (const edge of structural) for (const inv of edge.invariants) if (required.has(inv)) coveredByStructural.add(inv);
318
+ return {
319
+ kind: "unsatisfiable",
320
+ structuralPath: structural,
321
+ missing: requiredInvariants.filter((id) => !coveredByStructural.has(id))
322
+ };
323
+ }
324
+ const satisfiedInvariants = computeSatisfiedInvariants(required, path);
299
325
  const reachesTarget = collectNodesReachingTarget(graph, toHash);
326
+ const coveragePrefixes = requiredCoveragePrefixes(required, path);
300
327
  const tieBreakReasons = [];
301
328
  let alternativeCount = 0;
302
- for (const edge of path) {
329
+ for (const [i, edge] of path.entries()) {
303
330
  const outgoing = graph.forwardChain.get(edge.from);
304
- if (outgoing && outgoing.length > 1) {
305
- const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
306
- if (reachable.length > 1) {
307
- alternativeCount += reachable.length - 1;
308
- const sorted = sortedNeighbors(reachable);
309
- if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {
310
- if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) tieBreakReasons.push(`at ${edge.from}: ${reachable.length} candidates, selected by tie-break`);
311
- }
312
- }
331
+ if (!outgoing || outgoing.length <= 1) continue;
332
+ const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
333
+ if (reachable.length <= 1) continue;
334
+ let comparisonPool = reachable;
335
+ if (required.size > 0) {
336
+ const prefixSet = coveragePrefixes[i];
337
+ if (prefixSet === void 0) continue;
338
+ comparisonPool = invariantViableAlternativesAtStep(required, prefixSet, reachable);
313
339
  }
340
+ alternativeCount += reachable.length - 1;
341
+ if (sortedNeighbors(reachable)[0]?.migrationHash !== edge.migrationHash) continue;
342
+ if (!reachable.some((e) => e.migrationHash !== edge.migrationHash)) continue;
343
+ const sortedViable = sortedNeighbors(comparisonPool);
344
+ if (sortedViable.length > 1 && sortedViable[0]?.migrationHash === edge.migrationHash && sortedViable.some((e) => e.migrationHash !== edge.migrationHash)) tieBreakReasons.push(`at ${edge.from}: ${comparisonPool.length} candidates, selected by tie-break`);
314
345
  }
315
346
  return {
316
- selectedPath: path,
317
- fromHash,
318
- toHash,
319
- alternativeCount,
320
- tieBreakReasons,
321
- ...ifDefined("refName", refName)
347
+ kind: "ok",
348
+ decision: {
349
+ selectedPath: path,
350
+ fromHash,
351
+ toHash,
352
+ alternativeCount,
353
+ tieBreakReasons,
354
+ requiredInvariants,
355
+ satisfiedInvariants,
356
+ ...ifDefined("refName", refName)
357
+ }
322
358
  };
323
359
  }
360
+ function computeSatisfiedInvariants(required, path) {
361
+ if (required.size === 0) return [];
362
+ const covered = /* @__PURE__ */ new Set();
363
+ for (const edge of path) for (const inv of edge.invariants) if (required.has(inv)) covered.add(inv);
364
+ return [...covered].sort();
365
+ }
366
+ /**
367
+ * For each edge on path, invariant coverage accumulated from earlier edges only —
368
+ * `(required ∩ ∪_{j<i} path[j].invariants)` represented as cumulative set along `required`,
369
+ * keyed as "full set of required ids satisfied before taking path[i]".
370
+ */
371
+ function requiredCoveragePrefixes(required, path) {
372
+ const prefixes = [];
373
+ const acc = /* @__PURE__ */ new Set();
374
+ for (const edge of path) {
375
+ prefixes.push(new Set(acc));
376
+ for (const inv of edge.invariants) if (required.has(inv)) acc.add(inv);
377
+ }
378
+ return prefixes;
379
+ }
380
+ function invariantViableAlternativesAtStep(required, coverageBeforeTakingEdge, outgoing) {
381
+ if (required.size === 0) return [...outgoing];
382
+ return outgoing.filter((e) => [...required].every((id) => coverageBeforeTakingEdge.has(id) || e.invariants.includes(id)));
383
+ }
324
384
  /**
325
385
  * Walk ancestors of each branch tip back to find the last node
326
386
  * that appears on all paths. Returns `fromHash` if no shared ancestor is found.
@@ -1 +1 @@
1
- {"version":3,"file":"migration-graph.mjs","names":["migration: MigrationEdge","LABEL_PRIORITY: Record<string, number>","path: MigrationEdge[]","next: Set<string> | null","cur: string | undefined","tieBreakReasons: string[]","leaves: string[]","cycles: string[][]","stack: Frame[]","cycle: string[]","startNodes: string[]","orphans: MigrationEdge[]"],"sources":["../../src/queue.ts","../../src/graph-ops.ts","../../src/migration-graph.ts"],"sourcesContent":["/**\n * FIFO queue with amortised O(1) push and shift.\n *\n * Uses a head-index cursor over a backing array rather than\n * `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped\n * traversals where the queue is drained in a single pass — it does not\n * reclaim memory for already-shifted items, so it is not suitable for\n * long-lived queues with many push/shift cycles.\n */\nexport class Queue<T> {\n private readonly items: T[];\n private head = 0;\n\n constructor(initial: Iterable<T> = []) {\n this.items = [...initial];\n }\n\n push(item: T): void {\n this.items.push(item);\n }\n\n /**\n * Remove and return the next item. Caller must check `isEmpty` first —\n * shifting an empty queue throws.\n */\n shift(): T {\n if (this.head >= this.items.length) {\n throw new Error('Queue.shift called on empty queue');\n }\n // biome-ignore lint/style/noNonNullAssertion: bounds-checked on the line above\n return this.items[this.head++]!;\n }\n\n get isEmpty(): boolean {\n return this.head >= this.items.length;\n }\n}\n","import { Queue } from './queue';\n\n/**\n * One step of a BFS traversal.\n *\n * `parent` and `incomingEdge` are `null` for start states — they were not\n * reached via any edge. For every other state they record the predecessor\n * state and the edge by which this state was first reached.\n *\n * `state` is the BFS state, most often a string (graph node identifier) but\n * can be a composite object. The string overload keeps the common case\n * ergonomic; the generic overload accepts a caller-supplied `key` function\n * that produces a stable equality key for dedup.\n */\nexport interface BfsStep<S, E> {\n readonly state: S;\n readonly parent: S | null;\n readonly incomingEdge: E | null;\n}\n\n/**\n * Generic breadth-first traversal.\n *\n * Direction (forward/reverse) is expressed by the caller's `neighbours`\n * closure: return `{ next, edge }` pairs where `next` is the state to visit\n * next and `edge` is the edge that connects them. Callers that don't need\n * path reconstruction can ignore the `parent`/`incomingEdge` fields of each\n * yielded step.\n *\n * Ordering — when the result needs to be deterministic (path-finding) the\n * caller is responsible for sorting inside `neighbours`; this generator\n * does not impose an ordering hook of its own. State-dependent orderings\n * have full access to the source state inside the closure.\n *\n * Stops are intrinsic — callers `break` out of the `for..of` loop when\n * they've found what they're looking for.\n */\nexport function bfs<E>(\n starts: Iterable<string>,\n neighbours: (state: string) => Iterable<{ next: string; edge: E }>,\n): Generator<BfsStep<string, E>>;\nexport function bfs<S, E>(\n starts: Iterable<S>,\n neighbours: (state: S) => Iterable<{ next: S; edge: E }>,\n key: (state: S) => string,\n): Generator<BfsStep<S, E>>;\nexport function* bfs<S, E>(\n starts: Iterable<S>,\n neighbours: (state: S) => Iterable<{ next: S; edge: E }>,\n // Identity default for the string overload. TypeScript can't express\n // \"default applies only when S = string\", so this cast bridges the\n // generic implementation signature to the public overloads — which\n // guarantee `key` is omitted only when S = string at the call site.\n key: (state: S) => string = (state) => state as unknown as string,\n): Generator<BfsStep<S, E>> {\n // Queue entries carry the state alongside its key so we don't recompute\n // key() twice per visit (once on dedup, once on parent lookup). Composite\n // keys can be non-trivial to compute; string-overload callers pay nothing\n // since key() is identity there.\n interface Entry {\n readonly state: S;\n readonly key: string;\n }\n const visited = new Set<string>();\n const parentMap = new Map<string, { parent: S; edge: E }>();\n const queue = new Queue<Entry>();\n for (const start of starts) {\n const k = key(start);\n if (!visited.has(k)) {\n visited.add(k);\n queue.push({ state: start, key: k });\n }\n }\n while (!queue.isEmpty) {\n const { state: current, key: curKey } = queue.shift();\n const parentInfo = parentMap.get(curKey);\n yield {\n state: current,\n parent: parentInfo?.parent ?? null,\n incomingEdge: parentInfo?.edge ?? null,\n };\n\n for (const { next, edge } of neighbours(current)) {\n const k = key(next);\n if (!visited.has(k)) {\n visited.add(k);\n parentMap.set(k, { parent: current, edge });\n queue.push({ state: next, key: k });\n }\n }\n }\n}\n","import { ifDefined } from '@prisma-next/utils/defined';\nimport { EMPTY_CONTRACT_HASH } from './constants';\nimport {\n errorAmbiguousTarget,\n errorDuplicateMigrationHash,\n errorNoInitialMigration,\n errorNoTarget,\n errorSameSourceAndTarget,\n} from './errors';\nimport type { MigrationEdge, MigrationGraph } from './graph';\nimport { bfs } from './graph-ops';\nimport type { MigrationPackage } from './package';\n\n/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */\nfunction forwardNeighbours(graph: MigrationGraph, node: string) {\n return (graph.forwardChain.get(node) ?? []).map((edge) => ({ next: edge.to, edge }));\n}\n\n/**\n * Forward-edge neighbours, sorted by the deterministic tie-break.\n * Used by path-finding so the resulting shortest path is stable across runs.\n */\nfunction sortedForwardNeighbours(graph: MigrationGraph, node: string) {\n const edges = graph.forwardChain.get(node) ?? [];\n return [...edges].sort(compareTieBreak).map((edge) => ({ next: edge.to, edge }));\n}\n\n/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */\nfunction reverseNeighbours(graph: MigrationGraph, node: string) {\n return (graph.reverseChain.get(node) ?? []).map((edge) => ({ next: edge.from, edge }));\n}\n\nfunction appendEdge(map: Map<string, MigrationEdge[]>, key: string, entry: MigrationEdge): void {\n const bucket = map.get(key);\n if (bucket) bucket.push(entry);\n else map.set(key, [entry]);\n}\n\nexport function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationEdge[]>();\n const reverseChain = new Map<string, MigrationEdge[]>();\n const migrationByHash = new Map<string, MigrationEdge>();\n\n for (const pkg of packages) {\n // Manifest `from` is `string | null` (null = baseline). The graph layer\n // is the marker/path layer where \"no prior state\" is encoded as the\n // EMPTY_CONTRACT_HASH sentinel; bridge here so pathfinding stays string-\n // keyed.\n const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;\n const { to } = pkg.metadata;\n\n if (from === to) {\n throw errorSameSourceAndTarget(pkg.dirPath, from);\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationEdge = {\n from,\n to,\n migrationHash: pkg.metadata.migrationHash,\n dirName: pkg.dirName,\n createdAt: pkg.metadata.createdAt,\n labels: pkg.metadata.labels,\n invariants: pkg.metadata.providedInvariants,\n };\n\n if (migrationByHash.has(migration.migrationHash)) {\n throw errorDuplicateMigrationHash(migration.migrationHash);\n }\n migrationByHash.set(migration.migrationHash, migration);\n\n appendEdge(forwardChain, from, migration);\n appendEdge(reverseChain, to, migration);\n }\n\n return { nodes, forwardChain, reverseChain, migrationByHash };\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic tie-breaking for BFS neighbour order.\n// Used by path-finders only; not a general-purpose utility.\n// Ordering: label priority → createdAt → to → migrationHash.\n// ---------------------------------------------------------------------------\n\nconst LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };\n\nfunction labelPriority(labels: readonly string[]): number {\n let best = 3;\n for (const l of labels) {\n const p = LABEL_PRIORITY[l];\n if (p !== undefined && p < best) best = p;\n }\n return best;\n}\n\nfunction compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {\n const lp = labelPriority(a.labels) - labelPriority(b.labels);\n if (lp !== 0) return lp;\n const ca = a.createdAt.localeCompare(b.createdAt);\n if (ca !== 0) return ca;\n const tc = a.to.localeCompare(b.to);\n if (tc !== 0) return tc;\n return a.migrationHash.localeCompare(b.migrationHash);\n}\n\nfunction sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdge[] {\n return [...edges].sort(compareTieBreak);\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` using BFS over the\n * contract-hash graph. Returns the ordered list of edges, or null if no path\n * exists. Returns an empty array when `fromHash === toHash` (no-op).\n *\n * Neighbor ordering is deterministic via the tie-break sort key:\n * label priority → createdAt → to → migrationHash.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationEdge[] | null {\n if (fromHash === toHash) return [];\n\n const parents = new Map<string, { parent: string; edge: MigrationEdge }>();\n for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(step.state, { parent: step.parent, edge: step.incomingEdge });\n }\n if (step.state === toHash) {\n const path: MigrationEdge[] = [];\n let cur = toHash;\n let p = parents.get(cur);\n while (p) {\n path.push(p.edge);\n cur = p.parent;\n p = parents.get(cur);\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` whose edges collectively\n * cover every invariant in `required`. Returns `null` when no such path exists\n * (either `fromHash`→`toHash` is structurally unreachable, or every reachable\n * path leaves at least one required invariant uncovered). When `required` is\n * empty, delegates to `findPath` so the result is byte-identical for that case.\n *\n * Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.\n * The covered subset is a `Set<string>` of invariant ids; the state's dedup\n * key is `${node}\\0${[...covered].sort().join('\\0')}`. State keys distinguish\n * distinct `(node, covered)` tuples regardless of node-name length because\n * `\\0` cannot appear in any invariant id (validation rejects whitespace and\n * control chars at authoring time).\n *\n * Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed\n * invariant come first, with `labelPriority → createdAt → to → migrationHash`\n * as the secondary key. The heuristic steers BFS toward the satisfying path;\n * correctness (shortest, deterministic) does not depend on it.\n */\nexport function findPathWithInvariants(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n required: ReadonlySet<string>,\n): readonly MigrationEdge[] | null {\n if (required.size === 0) {\n return findPath(graph, fromHash, toHash);\n }\n if (fromHash === toHash) {\n // Empty path covers no invariants; required is non-empty ⇒ unsatisfiable.\n return null;\n }\n\n interface InvState {\n readonly node: string;\n readonly covered: ReadonlySet<string>;\n }\n const stateKey = (s: InvState): string => {\n if (s.covered.size === 0) return `${s.node}\\0`;\n return `${s.node}\\0${[...s.covered].sort().join('\\0')}`;\n };\n\n const neighbours = (s: InvState): Iterable<{ next: InvState; edge: MigrationEdge }> => {\n const outgoing = graph.forwardChain.get(s.node) ?? [];\n if (outgoing.length === 0) return [];\n return [...outgoing]\n .map((edge) => {\n let useful = false;\n let next: Set<string> | null = null;\n for (const inv of edge.invariants) {\n if (required.has(inv) && !s.covered.has(inv)) {\n if (next === null) next = new Set(s.covered);\n next.add(inv);\n useful = true;\n }\n }\n return { edge, useful, nextCovered: next ?? s.covered };\n })\n .sort((a, b) => {\n if (a.useful !== b.useful) return a.useful ? -1 : 1;\n return compareTieBreak(a.edge, b.edge);\n })\n .map(({ edge, nextCovered }) => ({\n next: { node: edge.to, covered: nextCovered },\n edge,\n }));\n };\n\n // Path reconstruction is consumer-side, keyed on stateKey, same shape as\n // findPath's parents map.\n const parents = new Map<string, { parentKey: string; edge: MigrationEdge }>();\n for (const step of bfs<InvState, MigrationEdge>(\n [{ node: fromHash, covered: new Set() }],\n neighbours,\n stateKey,\n )) {\n const curKey = stateKey(step.state);\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(curKey, { parentKey: stateKey(step.parent), edge: step.incomingEdge });\n }\n if (step.state.node === toHash && step.state.covered.size === required.size) {\n const path: MigrationEdge[] = [];\n let cur: string | undefined = curKey;\n while (cur !== undefined) {\n const p = parents.get(cur);\n if (!p) break;\n path.push(p.edge);\n cur = p.parentKey;\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Reverse-BFS from `toHash` over `reverseChain` to collect every node from\n * which `toHash` is reachable (inclusive of `toHash` itself).\n */\nfunction collectNodesReachingTarget(graph: MigrationGraph, toHash: string): Set<string> {\n const reached = new Set<string>();\n for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) {\n reached.add(step.state);\n }\n return reached;\n}\n\nexport interface PathDecision {\n readonly selectedPath: readonly MigrationEdge[];\n readonly fromHash: string;\n readonly toHash: string;\n readonly alternativeCount: number;\n readonly tieBreakReasons: readonly string[];\n readonly refName?: string;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` and return structured\n * path-decision metadata for machine-readable output.\n */\nexport function findPathWithDecision(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n refName?: string,\n): PathDecision | null {\n if (fromHash === toHash) {\n return {\n selectedPath: [],\n fromHash,\n toHash,\n alternativeCount: 0,\n tieBreakReasons: [],\n ...ifDefined('refName', refName),\n };\n }\n\n const path = findPath(graph, fromHash, toHash);\n if (!path) return null;\n\n // Single reverse BFS marks every node from which `toHash` is reachable.\n // Replaces a per-edge `findPath(e.to, toHash)` call inside the loop below,\n // which made the whole function O(|path| · (V + E)) instead of O(V + E).\n const reachesTarget = collectNodesReachingTarget(graph, toHash);\n\n const tieBreakReasons: string[] = [];\n let alternativeCount = 0;\n\n for (const edge of path) {\n const outgoing = graph.forwardChain.get(edge.from);\n if (outgoing && outgoing.length > 1) {\n const reachable = outgoing.filter((e) => reachesTarget.has(e.to));\n if (reachable.length > 1) {\n alternativeCount += reachable.length - 1;\n const sorted = sortedNeighbors(reachable);\n if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {\n if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) {\n tieBreakReasons.push(\n `at ${edge.from}: ${reachable.length} candidates, selected by tie-break`,\n );\n }\n }\n }\n }\n }\n\n return {\n selectedPath: path,\n fromHash,\n toHash,\n alternativeCount,\n tieBreakReasons,\n ...ifDefined('refName', refName),\n };\n}\n\n/**\n * Walk ancestors of each branch tip back to find the last node\n * that appears on all paths. Returns `fromHash` if no shared ancestor is found.\n */\nfunction findDivergencePoint(\n graph: MigrationGraph,\n fromHash: string,\n leaves: readonly string[],\n): string {\n const ancestorSets = leaves.map((leaf) => {\n const ancestors = new Set<string>();\n for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) {\n ancestors.add(step.state);\n }\n return ancestors;\n });\n\n const commonAncestors = [...(ancestorSets[0] ?? [])].filter((node) =>\n ancestorSets.every((s) => s.has(node)),\n );\n\n let deepest = fromHash;\n let deepestDepth = -1;\n for (const ancestor of commonAncestors) {\n const path = findPath(graph, fromHash, ancestor);\n const depth = path ? path.length : 0;\n if (depth > deepestDepth) {\n deepestDepth = depth;\n deepest = ancestor;\n }\n }\n return deepest;\n}\n\n/**\n * Find all branch tips (nodes with no outgoing edges) reachable from\n * `fromHash` via forward edges.\n */\nexport function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {\n const leaves: string[] = [];\n for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) {\n if (!graph.forwardChain.get(step.state)?.length) {\n leaves.push(step.state);\n }\n }\n return leaves;\n}\n\n/**\n * Find the target contract hash of the migration graph reachable from\n * EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target\n * state (either empty, or containing only the root with no outgoing\n * edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none\n * originate from the empty hash, and AMBIGUOUS_TARGET if multiple\n * branch tips exist.\n */\nexport function findLeaf(graph: MigrationGraph): string | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {\n throw errorNoInitialMigration([...graph.nodes]);\n }\n\n const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);\n\n if (leaves.length === 0) {\n const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);\n if (reachable.length > 0) {\n throw errorNoTarget(reachable);\n }\n return null;\n }\n\n if (leaves.length > 1) {\n const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);\n const branches = leaves.map((tip) => {\n const path = findPath(graph, divergencePoint, tip);\n return {\n tip,\n edges: (path ?? []).map((e) => ({ dirName: e.dirName, from: e.from, to: e.to })),\n };\n });\n throw errorAmbiguousTarget(leaves, { divergencePoint, branches });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: leaves.length is neither 0 nor >1 per the branches above, so exactly one leaf remains\n return leaves[0]!;\n}\n\n/**\n * Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH\n * to the single target. Returns null for an empty graph.\n * Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationEdge | null {\n const leafHash = findLeaf(graph);\n if (leafHash === null) return null;\n\n const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);\n return path?.at(-1) ?? null;\n}\n\nexport function detectCycles(graph: MigrationGraph): readonly string[][] {\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n\n const color = new Map<string, number>();\n const parentMap = new Map<string, string | null>();\n const cycles: string[][] = [];\n\n for (const node of graph.nodes) {\n color.set(node, WHITE);\n }\n\n // Iterative three-color DFS. A frame is (node, outgoing edges, next-index).\n interface Frame {\n node: string;\n outgoing: readonly MigrationEdge[];\n index: number;\n }\n const stack: Frame[] = [];\n\n function pushFrame(u: string): void {\n color.set(u, GRAY);\n stack.push({ node: u, outgoing: graph.forwardChain.get(u) ?? [], index: 0 });\n }\n\n for (const root of graph.nodes) {\n if (color.get(root) !== WHITE) continue;\n parentMap.set(root, null);\n pushFrame(root);\n\n while (stack.length > 0) {\n // biome-ignore lint/style/noNonNullAssertion: stack.length > 0 should guarantee that this cannot be undefined\n const frame = stack[stack.length - 1]!;\n if (frame.index >= frame.outgoing.length) {\n color.set(frame.node, BLACK);\n stack.pop();\n continue;\n }\n // biome-ignore lint/style/noNonNullAssertion: the early-continue above guarantees frame.index < frame.outgoing.length here, so this is defined\n const edge = frame.outgoing[frame.index++]!;\n const v = edge.to;\n const vColor = color.get(v);\n if (vColor === GRAY) {\n const cycle: string[] = [v];\n let cur = frame.node;\n while (cur !== v) {\n cycle.push(cur);\n cur = parentMap.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (vColor === WHITE) {\n parentMap.set(v, frame.node);\n pushFrame(v);\n }\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const startNodes: string[] = [];\n\n if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) {\n startNodes.push(EMPTY_CONTRACT_HASH);\n } else {\n const allTargets = new Set<string>();\n for (const edges of graph.forwardChain.values()) {\n for (const edge of edges) {\n allTargets.add(edge.to);\n }\n }\n for (const node of graph.nodes) {\n if (!allTargets.has(node)) {\n startNodes.push(node);\n }\n }\n }\n\n for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) {\n reachable.add(step.state);\n }\n\n const orphans: MigrationEdge[] = [];\n for (const [from, migrations] of graph.forwardChain) {\n if (!reachable.has(from)) {\n orphans.push(...migrations);\n }\n }\n\n return orphans;\n}\n"],"mappings":";;;;;;;;;;;;;;AASA,IAAa,QAAb,MAAsB;CACpB,AAAiB;CACjB,AAAQ,OAAO;CAEf,YAAY,UAAuB,EAAE,EAAE;AACrC,OAAK,QAAQ,CAAC,GAAG,QAAQ;;CAG3B,KAAK,MAAe;AAClB,OAAK,MAAM,KAAK,KAAK;;;;;;CAOvB,QAAW;AACT,MAAI,KAAK,QAAQ,KAAK,MAAM,OAC1B,OAAM,IAAI,MAAM,oCAAoC;AAGtD,SAAO,KAAK,MAAM,KAAK;;CAGzB,IAAI,UAAmB;AACrB,SAAO,KAAK,QAAQ,KAAK,MAAM;;;;;;ACYnC,UAAiB,IACf,QACA,YAKA,OAA6B,UAAU,OACb;CAS1B,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,4BAAY,IAAI,KAAqC;CAC3D,MAAM,QAAQ,IAAI,OAAc;AAChC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,IAAI,IAAI,MAAM;AACpB,MAAI,CAAC,QAAQ,IAAI,EAAE,EAAE;AACnB,WAAQ,IAAI,EAAE;AACd,SAAM,KAAK;IAAE,OAAO;IAAO,KAAK;IAAG,CAAC;;;AAGxC,QAAO,CAAC,MAAM,SAAS;EACrB,MAAM,EAAE,OAAO,SAAS,KAAK,WAAW,MAAM,OAAO;EACrD,MAAM,aAAa,UAAU,IAAI,OAAO;AACxC,QAAM;GACJ,OAAO;GACP,QAAQ,YAAY,UAAU;GAC9B,cAAc,YAAY,QAAQ;GACnC;AAED,OAAK,MAAM,EAAE,MAAM,UAAU,WAAW,QAAQ,EAAE;GAChD,MAAM,IAAI,IAAI,KAAK;AACnB,OAAI,CAAC,QAAQ,IAAI,EAAE,EAAE;AACnB,YAAQ,IAAI,EAAE;AACd,cAAU,IAAI,GAAG;KAAE,QAAQ;KAAS;KAAM,CAAC;AAC3C,UAAM,KAAK;KAAE,OAAO;KAAM,KAAK;KAAG,CAAC;;;;;;;;;ACzE3C,SAAS,kBAAkB,OAAuB,MAAc;AAC9D,SAAQ,MAAM,aAAa,IAAI,KAAK,IAAI,EAAE,EAAE,KAAK,UAAU;EAAE,MAAM,KAAK;EAAI;EAAM,EAAE;;;;;;AAOtF,SAAS,wBAAwB,OAAuB,MAAc;AAEpE,QAAO,CAAC,GADM,MAAM,aAAa,IAAI,KAAK,IAAI,EAAE,CAC/B,CAAC,KAAK,gBAAgB,CAAC,KAAK,UAAU;EAAE,MAAM,KAAK;EAAI;EAAM,EAAE;;;AAIlF,SAAS,kBAAkB,OAAuB,MAAc;AAC9D,SAAQ,MAAM,aAAa,IAAI,KAAK,IAAI,EAAE,EAAE,KAAK,UAAU;EAAE,MAAM,KAAK;EAAM;EAAM,EAAE;;AAGxF,SAAS,WAAW,KAAmC,KAAa,OAA4B;CAC9F,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,KAAI,OAAQ,QAAO,KAAK,MAAM;KACzB,KAAI,IAAI,KAAK,CAAC,MAAM,CAAC;;AAG5B,SAAgB,iBAAiB,UAAuD;CACtF,MAAM,wBAAQ,IAAI,KAAa;CAC/B,MAAM,+BAAe,IAAI,KAA8B;CACvD,MAAM,+BAAe,IAAI,KAA8B;CACvD,MAAM,kCAAkB,IAAI,KAA4B;AAExD,MAAK,MAAM,OAAO,UAAU;EAK1B,MAAM,OAAO,IAAI,SAAS,QAAQ;EAClC,MAAM,EAAE,OAAO,IAAI;AAEnB,MAAI,SAAS,GACX,OAAM,yBAAyB,IAAI,SAAS,KAAK;AAGnD,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,GAAG;EAEb,MAAMA,YAA2B;GAC/B;GACA;GACA,eAAe,IAAI,SAAS;GAC5B,SAAS,IAAI;GACb,WAAW,IAAI,SAAS;GACxB,QAAQ,IAAI,SAAS;GACrB,YAAY,IAAI,SAAS;GAC1B;AAED,MAAI,gBAAgB,IAAI,UAAU,cAAc,CAC9C,OAAM,4BAA4B,UAAU,cAAc;AAE5D,kBAAgB,IAAI,UAAU,eAAe,UAAU;AAEvD,aAAW,cAAc,MAAM,UAAU;AACzC,aAAW,cAAc,IAAI,UAAU;;AAGzC,QAAO;EAAE;EAAO;EAAc;EAAc;EAAiB;;AAS/D,MAAMC,iBAAyC;CAAE,MAAM;CAAG,SAAS;CAAG,SAAS;CAAG;AAElF,SAAS,cAAc,QAAmC;CACxD,IAAI,OAAO;AACX,MAAK,MAAM,KAAK,QAAQ;EACtB,MAAM,IAAI,eAAe;AACzB,MAAI,MAAM,UAAa,IAAI,KAAM,QAAO;;AAE1C,QAAO;;AAGT,SAAS,gBAAgB,GAAkB,GAA0B;CACnE,MAAM,KAAK,cAAc,EAAE,OAAO,GAAG,cAAc,EAAE,OAAO;AAC5D,KAAI,OAAO,EAAG,QAAO;CACrB,MAAM,KAAK,EAAE,UAAU,cAAc,EAAE,UAAU;AACjD,KAAI,OAAO,EAAG,QAAO;CACrB,MAAM,KAAK,EAAE,GAAG,cAAc,EAAE,GAAG;AACnC,KAAI,OAAO,EAAG,QAAO;AACrB,QAAO,EAAE,cAAc,cAAc,EAAE,cAAc;;AAGvD,SAAS,gBAAgB,OAA2D;AAClF,QAAO,CAAC,GAAG,MAAM,CAAC,KAAK,gBAAgB;;;;;;;;;;AAWzC,SAAgB,SACd,OACA,UACA,QACiC;AACjC,KAAI,aAAa,OAAQ,QAAO,EAAE;CAElC,MAAM,0BAAU,IAAI,KAAsD;AAC1E,MAAK,MAAM,QAAQ,IAAI,CAAC,SAAS,GAAG,MAAM,wBAAwB,OAAO,EAAE,CAAC,EAAE;AAC5E,MAAI,KAAK,WAAW,QAAQ,KAAK,iBAAiB,KAChD,SAAQ,IAAI,KAAK,OAAO;GAAE,QAAQ,KAAK;GAAQ,MAAM,KAAK;GAAc,CAAC;AAE3E,MAAI,KAAK,UAAU,QAAQ;GACzB,MAAMC,OAAwB,EAAE;GAChC,IAAI,MAAM;GACV,IAAI,IAAI,QAAQ,IAAI,IAAI;AACxB,UAAO,GAAG;AACR,SAAK,KAAK,EAAE,KAAK;AACjB,UAAM,EAAE;AACR,QAAI,QAAQ,IAAI,IAAI;;AAEtB,QAAK,SAAS;AACd,UAAO;;;AAIX,QAAO;;;;;;;;;;;;;;;;;;;;;AAsBT,SAAgB,uBACd,OACA,UACA,QACA,UACiC;AACjC,KAAI,SAAS,SAAS,EACpB,QAAO,SAAS,OAAO,UAAU,OAAO;AAE1C,KAAI,aAAa,OAEf,QAAO;CAOT,MAAM,YAAY,MAAwB;AACxC,MAAI,EAAE,QAAQ,SAAS,EAAG,QAAO,GAAG,EAAE,KAAK;AAC3C,SAAO,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,KAAK;;CAGvD,MAAM,cAAc,MAAmE;EACrF,MAAM,WAAW,MAAM,aAAa,IAAI,EAAE,KAAK,IAAI,EAAE;AACrD,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE;AACpC,SAAO,CAAC,GAAG,SAAS,CACjB,KAAK,SAAS;GACb,IAAI,SAAS;GACb,IAAIC,OAA2B;AAC/B,QAAK,MAAM,OAAO,KAAK,WACrB,KAAI,SAAS,IAAI,IAAI,IAAI,CAAC,EAAE,QAAQ,IAAI,IAAI,EAAE;AAC5C,QAAI,SAAS,KAAM,QAAO,IAAI,IAAI,EAAE,QAAQ;AAC5C,SAAK,IAAI,IAAI;AACb,aAAS;;AAGb,UAAO;IAAE;IAAM;IAAQ,aAAa,QAAQ,EAAE;IAAS;IACvD,CACD,MAAM,GAAG,MAAM;AACd,OAAI,EAAE,WAAW,EAAE,OAAQ,QAAO,EAAE,SAAS,KAAK;AAClD,UAAO,gBAAgB,EAAE,MAAM,EAAE,KAAK;IACtC,CACD,KAAK,EAAE,MAAM,mBAAmB;GAC/B,MAAM;IAAE,MAAM,KAAK;IAAI,SAAS;IAAa;GAC7C;GACD,EAAE;;CAKP,MAAM,0BAAU,IAAI,KAAyD;AAC7E,MAAK,MAAM,QAAQ,IACjB,CAAC;EAAE,MAAM;EAAU,yBAAS,IAAI,KAAK;EAAE,CAAC,EACxC,YACA,SACD,EAAE;EACD,MAAM,SAAS,SAAS,KAAK,MAAM;AACnC,MAAI,KAAK,WAAW,QAAQ,KAAK,iBAAiB,KAChD,SAAQ,IAAI,QAAQ;GAAE,WAAW,SAAS,KAAK,OAAO;GAAE,MAAM,KAAK;GAAc,CAAC;AAEpF,MAAI,KAAK,MAAM,SAAS,UAAU,KAAK,MAAM,QAAQ,SAAS,SAAS,MAAM;GAC3E,MAAMD,OAAwB,EAAE;GAChC,IAAIE,MAA0B;AAC9B,UAAO,QAAQ,QAAW;IACxB,MAAM,IAAI,QAAQ,IAAI,IAAI;AAC1B,QAAI,CAAC,EAAG;AACR,SAAK,KAAK,EAAE,KAAK;AACjB,UAAM,EAAE;;AAEV,QAAK,SAAS;AACd,UAAO;;;AAIX,QAAO;;;;;;AAOT,SAAS,2BAA2B,OAAuB,QAA6B;CACtF,MAAM,0BAAU,IAAI,KAAa;AACjC,MAAK,MAAM,QAAQ,IAAI,CAAC,OAAO,GAAG,MAAM,kBAAkB,OAAO,EAAE,CAAC,CAClE,SAAQ,IAAI,KAAK,MAAM;AAEzB,QAAO;;;;;;AAgBT,SAAgB,qBACd,OACA,UACA,QACA,SACqB;AACrB,KAAI,aAAa,OACf,QAAO;EACL,cAAc,EAAE;EAChB;EACA;EACA,kBAAkB;EAClB,iBAAiB,EAAE;EACnB,GAAG,UAAU,WAAW,QAAQ;EACjC;CAGH,MAAM,OAAO,SAAS,OAAO,UAAU,OAAO;AAC9C,KAAI,CAAC,KAAM,QAAO;CAKlB,MAAM,gBAAgB,2BAA2B,OAAO,OAAO;CAE/D,MAAMC,kBAA4B,EAAE;CACpC,IAAI,mBAAmB;AAEvB,MAAK,MAAM,QAAQ,MAAM;EACvB,MAAM,WAAW,MAAM,aAAa,IAAI,KAAK,KAAK;AAClD,MAAI,YAAY,SAAS,SAAS,GAAG;GACnC,MAAM,YAAY,SAAS,QAAQ,MAAM,cAAc,IAAI,EAAE,GAAG,CAAC;AACjE,OAAI,UAAU,SAAS,GAAG;AACxB,wBAAoB,UAAU,SAAS;IACvC,MAAM,SAAS,gBAAgB,UAAU;AACzC,QAAI,OAAO,MAAM,OAAO,GAAG,kBAAkB,KAAK,eAChD;SAAI,UAAU,MAAM,MAAM,EAAE,kBAAkB,KAAK,cAAc,CAC/D,iBAAgB,KACd,MAAM,KAAK,KAAK,IAAI,UAAU,OAAO,oCACtC;;;;;AAOX,QAAO;EACL,cAAc;EACd;EACA;EACA;EACA;EACA,GAAG,UAAU,WAAW,QAAQ;EACjC;;;;;;AAOH,SAAS,oBACP,OACA,UACA,QACQ;CACR,MAAM,eAAe,OAAO,KAAK,SAAS;EACxC,MAAM,4BAAY,IAAI,KAAa;AACnC,OAAK,MAAM,QAAQ,IAAI,CAAC,KAAK,GAAG,MAAM,kBAAkB,OAAO,EAAE,CAAC,CAChE,WAAU,IAAI,KAAK,MAAM;AAE3B,SAAO;GACP;CAEF,MAAM,kBAAkB,CAAC,GAAI,aAAa,MAAM,EAAE,CAAE,CAAC,QAAQ,SAC3D,aAAa,OAAO,MAAM,EAAE,IAAI,KAAK,CAAC,CACvC;CAED,IAAI,UAAU;CACd,IAAI,eAAe;AACnB,MAAK,MAAM,YAAY,iBAAiB;EACtC,MAAM,OAAO,SAAS,OAAO,UAAU,SAAS;EAChD,MAAM,QAAQ,OAAO,KAAK,SAAS;AACnC,MAAI,QAAQ,cAAc;AACxB,kBAAe;AACf,aAAU;;;AAGd,QAAO;;;;;;AAOT,SAAgB,oBAAoB,OAAuB,UAAqC;CAC9F,MAAMC,SAAmB,EAAE;AAC3B,MAAK,MAAM,QAAQ,IAAI,CAAC,SAAS,GAAG,MAAM,kBAAkB,OAAO,EAAE,CAAC,CACpE,KAAI,CAAC,MAAM,aAAa,IAAI,KAAK,MAAM,EAAE,OACvC,QAAO,KAAK,KAAK,MAAM;AAG3B,QAAO;;;;;;;;;;AAWT,SAAgB,SAAS,OAAsC;AAC7D,KAAI,MAAM,MAAM,SAAS,EACvB,QAAO;AAGT,KAAI,CAAC,MAAM,MAAM,IAAI,oBAAoB,CACvC,OAAM,wBAAwB,CAAC,GAAG,MAAM,MAAM,CAAC;CAGjD,MAAM,SAAS,oBAAoB,OAAO,oBAAoB;AAE9D,KAAI,OAAO,WAAW,GAAG;EACvB,MAAM,YAAY,CAAC,GAAG,MAAM,MAAM,CAAC,QAAQ,MAAM,MAAM,oBAAoB;AAC3E,MAAI,UAAU,SAAS,EACrB,OAAM,cAAc,UAAU;AAEhC,SAAO;;AAGT,KAAI,OAAO,SAAS,GAAG;EACrB,MAAM,kBAAkB,oBAAoB,OAAO,qBAAqB,OAAO;AAQ/E,QAAM,qBAAqB,QAAQ;GAAE;GAAiB,UAPrC,OAAO,KAAK,QAAQ;AAEnC,WAAO;KACL;KACA,QAHW,SAAS,OAAO,iBAAiB,IAAI,IAGhC,EAAE,EAAE,KAAK,OAAO;MAAE,SAAS,EAAE;MAAS,MAAM,EAAE;MAAM,IAAI,EAAE;MAAI,EAAE;KACjF;KACD;GAC8D,CAAC;;AAInE,QAAO,OAAO;;;;;;;AAQhB,SAAgB,oBAAoB,OAA6C;CAC/E,MAAM,WAAW,SAAS,MAAM;AAChC,KAAI,aAAa,KAAM,QAAO;AAG9B,QADa,SAAS,OAAO,qBAAqB,SAAS,EAC9C,GAAG,GAAG,IAAI;;AAGzB,SAAgB,aAAa,OAA4C;CACvE,MAAM,QAAQ;CACd,MAAM,OAAO;CACb,MAAM,QAAQ;CAEd,MAAM,wBAAQ,IAAI,KAAqB;CACvC,MAAM,4BAAY,IAAI,KAA4B;CAClD,MAAMC,SAAqB,EAAE;AAE7B,MAAK,MAAM,QAAQ,MAAM,MACvB,OAAM,IAAI,MAAM,MAAM;CASxB,MAAMC,QAAiB,EAAE;CAEzB,SAAS,UAAU,GAAiB;AAClC,QAAM,IAAI,GAAG,KAAK;AAClB,QAAM,KAAK;GAAE,MAAM;GAAG,UAAU,MAAM,aAAa,IAAI,EAAE,IAAI,EAAE;GAAE,OAAO;GAAG,CAAC;;AAG9E,MAAK,MAAM,QAAQ,MAAM,OAAO;AAC9B,MAAI,MAAM,IAAI,KAAK,KAAK,MAAO;AAC/B,YAAU,IAAI,MAAM,KAAK;AACzB,YAAU,KAAK;AAEf,SAAO,MAAM,SAAS,GAAG;GAEvB,MAAM,QAAQ,MAAM,MAAM,SAAS;AACnC,OAAI,MAAM,SAAS,MAAM,SAAS,QAAQ;AACxC,UAAM,IAAI,MAAM,MAAM,MAAM;AAC5B,UAAM,KAAK;AACX;;GAIF,MAAM,IADO,MAAM,SAAS,MAAM,SACnB;GACf,MAAM,SAAS,MAAM,IAAI,EAAE;AAC3B,OAAI,WAAW,MAAM;IACnB,MAAMC,QAAkB,CAAC,EAAE;IAC3B,IAAI,MAAM,MAAM;AAChB,WAAO,QAAQ,GAAG;AAChB,WAAM,KAAK,IAAI;AACf,WAAM,UAAU,IAAI,IAAI,IAAI;;AAE9B,UAAM,SAAS;AACf,WAAO,KAAK,MAAM;cACT,WAAW,OAAO;AAC3B,cAAU,IAAI,GAAG,MAAM,KAAK;AAC5B,cAAU,EAAE;;;;AAKlB,QAAO;;AAGT,SAAgB,cAAc,OAAiD;AAC7E,KAAI,MAAM,MAAM,SAAS,EAAG,QAAO,EAAE;CAErC,MAAM,4BAAY,IAAI,KAAa;CACnC,MAAMC,aAAuB,EAAE;AAE/B,KAAI,MAAM,aAAa,IAAI,oBAAoB,CAC7C,YAAW,KAAK,oBAAoB;MAC/B;EACL,MAAM,6BAAa,IAAI,KAAa;AACpC,OAAK,MAAM,SAAS,MAAM,aAAa,QAAQ,CAC7C,MAAK,MAAM,QAAQ,MACjB,YAAW,IAAI,KAAK,GAAG;AAG3B,OAAK,MAAM,QAAQ,MAAM,MACvB,KAAI,CAAC,WAAW,IAAI,KAAK,CACvB,YAAW,KAAK,KAAK;;AAK3B,MAAK,MAAM,QAAQ,IAAI,aAAa,MAAM,kBAAkB,OAAO,EAAE,CAAC,CACpE,WAAU,IAAI,KAAK,MAAM;CAG3B,MAAMC,UAA2B,EAAE;AACnC,MAAK,MAAM,CAAC,MAAM,eAAe,MAAM,aACrC,KAAI,CAAC,UAAU,IAAI,KAAK,CACtB,SAAQ,KAAK,GAAG,WAAW;AAI/B,QAAO"}
1
+ {"version":3,"file":"migration-graph.mjs","names":["migration: MigrationEdge","LABEL_PRIORITY: Record<string, number>","path: MigrationEdge[]","next: Set<string> | null","cur: string | undefined","tieBreakReasons: string[]","comparisonPool: readonly MigrationEdge[]","prefixes: ReadonlySet<string>[]","leaves: string[]","cycles: string[][]","stack: Frame[]","cycle: string[]","startNodes: string[]","orphans: MigrationEdge[]"],"sources":["../../src/queue.ts","../../src/graph-ops.ts","../../src/migration-graph.ts"],"sourcesContent":["/**\n * FIFO queue with amortised O(1) push and shift.\n *\n * Uses a head-index cursor over a backing array rather than\n * `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped\n * traversals where the queue is drained in a single pass — it does not\n * reclaim memory for already-shifted items, so it is not suitable for\n * long-lived queues with many push/shift cycles.\n */\nexport class Queue<T> {\n private readonly items: T[];\n private head = 0;\n\n constructor(initial: Iterable<T> = []) {\n this.items = [...initial];\n }\n\n push(item: T): void {\n this.items.push(item);\n }\n\n /**\n * Remove and return the next item. Caller must check `isEmpty` first —\n * shifting an empty queue throws.\n */\n shift(): T {\n if (this.head >= this.items.length) {\n throw new Error('Queue.shift called on empty queue');\n }\n // biome-ignore lint/style/noNonNullAssertion: bounds-checked on the line above\n return this.items[this.head++]!;\n }\n\n get isEmpty(): boolean {\n return this.head >= this.items.length;\n }\n}\n","import { Queue } from './queue';\n\n/**\n * One step of a BFS traversal.\n *\n * `parent` and `incomingEdge` are `null` for start states — they were not\n * reached via any edge. For every other state they record the predecessor\n * state and the edge by which this state was first reached.\n *\n * `state` is the BFS state, most often a string (graph node identifier) but\n * can be a composite object. The string overload keeps the common case\n * ergonomic; the generic overload accepts a caller-supplied `key` function\n * that produces a stable equality key for dedup.\n */\nexport interface BfsStep<S, E> {\n readonly state: S;\n readonly parent: S | null;\n readonly incomingEdge: E | null;\n}\n\n/**\n * Generic breadth-first traversal.\n *\n * Direction (forward/reverse) is expressed by the caller's `neighbours`\n * closure: return `{ next, edge }` pairs where `next` is the state to visit\n * next and `edge` is the edge that connects them. Callers that don't need\n * path reconstruction can ignore the `parent`/`incomingEdge` fields of each\n * yielded step.\n *\n * Ordering — when the result needs to be deterministic (path-finding) the\n * caller is responsible for sorting inside `neighbours`; this generator\n * does not impose an ordering hook of its own. State-dependent orderings\n * have full access to the source state inside the closure.\n *\n * Stops are intrinsic — callers `break` out of the `for..of` loop when\n * they've found what they're looking for.\n */\nexport function bfs<E>(\n starts: Iterable<string>,\n neighbours: (state: string) => Iterable<{ next: string; edge: E }>,\n): Generator<BfsStep<string, E>>;\nexport function bfs<S, E>(\n starts: Iterable<S>,\n neighbours: (state: S) => Iterable<{ next: S; edge: E }>,\n key: (state: S) => string,\n): Generator<BfsStep<S, E>>;\nexport function* bfs<S, E>(\n starts: Iterable<S>,\n neighbours: (state: S) => Iterable<{ next: S; edge: E }>,\n // Identity default for the string overload. TypeScript can't express\n // \"default applies only when S = string\", so this cast bridges the\n // generic implementation signature to the public overloads — which\n // guarantee `key` is omitted only when S = string at the call site.\n key: (state: S) => string = (state) => state as unknown as string,\n): Generator<BfsStep<S, E>> {\n // Queue entries carry the state alongside its key so we don't recompute\n // key() twice per visit (once on dedup, once on parent lookup). Composite\n // keys can be non-trivial to compute; string-overload callers pay nothing\n // since key() is identity there.\n interface Entry {\n readonly state: S;\n readonly key: string;\n }\n const visited = new Set<string>();\n const parentMap = new Map<string, { parent: S; edge: E }>();\n const queue = new Queue<Entry>();\n for (const start of starts) {\n const k = key(start);\n if (!visited.has(k)) {\n visited.add(k);\n queue.push({ state: start, key: k });\n }\n }\n while (!queue.isEmpty) {\n const { state: current, key: curKey } = queue.shift();\n const parentInfo = parentMap.get(curKey);\n yield {\n state: current,\n parent: parentInfo?.parent ?? null,\n incomingEdge: parentInfo?.edge ?? null,\n };\n\n for (const { next, edge } of neighbours(current)) {\n const k = key(next);\n if (!visited.has(k)) {\n visited.add(k);\n parentMap.set(k, { parent: current, edge });\n queue.push({ state: next, key: k });\n }\n }\n }\n}\n","import { ifDefined } from '@prisma-next/utils/defined';\nimport { EMPTY_CONTRACT_HASH } from './constants';\nimport {\n errorAmbiguousTarget,\n errorDuplicateMigrationHash,\n errorNoInitialMigration,\n errorNoTarget,\n errorSameSourceAndTarget,\n} from './errors';\nimport type { MigrationEdge, MigrationGraph } from './graph';\nimport { bfs } from './graph-ops';\nimport type { MigrationPackage } from './package';\n\n/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */\nfunction forwardNeighbours(graph: MigrationGraph, node: string) {\n return (graph.forwardChain.get(node) ?? []).map((edge) => ({ next: edge.to, edge }));\n}\n\n/**\n * Forward-edge neighbours, sorted by the deterministic tie-break.\n * Used by path-finding so the resulting shortest path is stable across runs.\n */\nfunction sortedForwardNeighbours(graph: MigrationGraph, node: string) {\n const edges = graph.forwardChain.get(node) ?? [];\n return [...edges].sort(compareTieBreak).map((edge) => ({ next: edge.to, edge }));\n}\n\n/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */\nfunction reverseNeighbours(graph: MigrationGraph, node: string) {\n return (graph.reverseChain.get(node) ?? []).map((edge) => ({ next: edge.from, edge }));\n}\n\nfunction appendEdge(map: Map<string, MigrationEdge[]>, key: string, entry: MigrationEdge): void {\n const bucket = map.get(key);\n if (bucket) bucket.push(entry);\n else map.set(key, [entry]);\n}\n\nexport function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationEdge[]>();\n const reverseChain = new Map<string, MigrationEdge[]>();\n const migrationByHash = new Map<string, MigrationEdge>();\n\n for (const pkg of packages) {\n // Manifest `from` is `string | null` (null = baseline). The graph layer\n // is the marker/path layer where \"no prior state\" is encoded as the\n // EMPTY_CONTRACT_HASH sentinel; bridge here so pathfinding stays string-\n // keyed.\n const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;\n const { to } = pkg.metadata;\n\n if (from === to) {\n const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');\n if (!hasDataOp) {\n throw errorSameSourceAndTarget(pkg.dirPath, from);\n }\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationEdge = {\n from,\n to,\n migrationHash: pkg.metadata.migrationHash,\n dirName: pkg.dirName,\n createdAt: pkg.metadata.createdAt,\n labels: pkg.metadata.labels,\n invariants: pkg.metadata.providedInvariants,\n };\n\n if (migrationByHash.has(migration.migrationHash)) {\n throw errorDuplicateMigrationHash(migration.migrationHash);\n }\n migrationByHash.set(migration.migrationHash, migration);\n\n appendEdge(forwardChain, from, migration);\n appendEdge(reverseChain, to, migration);\n }\n\n return { nodes, forwardChain, reverseChain, migrationByHash };\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic tie-breaking for BFS neighbour order.\n// Used by path-finders only; not a general-purpose utility.\n// Ordering: label priority → createdAt → to → migrationHash.\n// ---------------------------------------------------------------------------\n\nconst LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };\n\nfunction labelPriority(labels: readonly string[]): number {\n let best = 3;\n for (const l of labels) {\n const p = LABEL_PRIORITY[l];\n if (p !== undefined && p < best) best = p;\n }\n return best;\n}\n\nfunction compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {\n const lp = labelPriority(a.labels) - labelPriority(b.labels);\n if (lp !== 0) return lp;\n const ca = a.createdAt.localeCompare(b.createdAt);\n if (ca !== 0) return ca;\n const tc = a.to.localeCompare(b.to);\n if (tc !== 0) return tc;\n return a.migrationHash.localeCompare(b.migrationHash);\n}\n\nfunction sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdge[] {\n return [...edges].sort(compareTieBreak);\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` using BFS over the\n * contract-hash graph. Returns the ordered list of edges, or null if no path\n * exists. Returns an empty array when `fromHash === toHash` (no-op).\n *\n * Neighbor ordering is deterministic via the tie-break sort key:\n * label priority → createdAt → to → migrationHash.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationEdge[] | null {\n if (fromHash === toHash) return [];\n\n const parents = new Map<string, { parent: string; edge: MigrationEdge }>();\n for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(step.state, { parent: step.parent, edge: step.incomingEdge });\n }\n if (step.state === toHash) {\n const path: MigrationEdge[] = [];\n let cur = toHash;\n let p = parents.get(cur);\n while (p) {\n path.push(p.edge);\n cur = p.parent;\n p = parents.get(cur);\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` whose edges collectively\n * cover every invariant in `required`. Returns `null` when no such path exists\n * (either `fromHash`→`toHash` is structurally unreachable, or every reachable\n * path leaves at least one required invariant uncovered). When `required` is\n * empty, delegates to `findPath` so the result is byte-identical for that case.\n *\n * Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.\n * The covered subset is a `Set<string>` of invariant ids; the state's dedup\n * key is `${node}\\0${[...covered].sort().join('\\0')}`. State keys distinguish\n * distinct `(node, covered)` tuples regardless of node-name length because\n * `\\0` cannot appear in any invariant id (validation rejects whitespace and\n * control chars at authoring time).\n *\n * Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed\n * invariant come first, with `labelPriority → createdAt → to → migrationHash`\n * as the secondary key. The heuristic steers BFS toward the satisfying path;\n * correctness (shortest, deterministic) does not depend on it.\n */\nexport function findPathWithInvariants(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n required: ReadonlySet<string>,\n): readonly MigrationEdge[] | null {\n if (required.size === 0) {\n return findPath(graph, fromHash, toHash);\n }\n\n interface InvState {\n readonly node: string;\n readonly covered: ReadonlySet<string>;\n }\n // `\\0` is a safe segment separator: `validateInvariantId` rejects any id\n // containing whitespace or control characters (NUL is U+0000), and node\n // hashes are hex strings. Distinct `(node, covered)` tuples therefore\n // map to distinct strings. If `validateInvariantId` is ever relaxed,\n // re-confirm dedup correctness here.\n const stateKey = (s: InvState): string => {\n if (s.covered.size === 0) return `${s.node}\\0`;\n return `${s.node}\\0${[...s.covered].sort().join('\\0')}`;\n };\n\n const neighbours = (s: InvState): Iterable<{ next: InvState; edge: MigrationEdge }> => {\n const outgoing = graph.forwardChain.get(s.node) ?? [];\n if (outgoing.length === 0) return [];\n return [...outgoing]\n .map((edge) => {\n let useful = false;\n let next: Set<string> | null = null;\n for (const inv of edge.invariants) {\n if (required.has(inv) && !s.covered.has(inv)) {\n if (next === null) next = new Set(s.covered);\n next.add(inv);\n useful = true;\n }\n }\n return { edge, useful, nextCovered: next ?? s.covered };\n })\n .sort((a, b) => {\n if (a.useful !== b.useful) return a.useful ? -1 : 1;\n return compareTieBreak(a.edge, b.edge);\n })\n .map(({ edge, nextCovered }) => ({\n next: { node: edge.to, covered: nextCovered },\n edge,\n }));\n };\n\n // Path reconstruction is consumer-side, keyed on stateKey, same shape as\n // findPath's parents map.\n const parents = new Map<string, { parentKey: string; edge: MigrationEdge }>();\n for (const step of bfs<InvState, MigrationEdge>(\n [{ node: fromHash, covered: new Set() }],\n neighbours,\n stateKey,\n )) {\n const curKey = stateKey(step.state);\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(curKey, { parentKey: stateKey(step.parent), edge: step.incomingEdge });\n }\n if (step.state.node === toHash && step.state.covered.size === required.size) {\n const path: MigrationEdge[] = [];\n let cur: string | undefined = curKey;\n while (cur !== undefined) {\n const p = parents.get(cur);\n if (!p) break;\n path.push(p.edge);\n cur = p.parentKey;\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Reverse-BFS from `toHash` over `reverseChain` to collect every node from\n * which `toHash` is reachable (inclusive of `toHash` itself).\n */\nfunction collectNodesReachingTarget(graph: MigrationGraph, toHash: string): Set<string> {\n const reached = new Set<string>();\n for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) {\n reached.add(step.state);\n }\n return reached;\n}\n\nexport interface PathDecision {\n readonly selectedPath: readonly MigrationEdge[];\n readonly fromHash: string;\n readonly toHash: string;\n readonly alternativeCount: number;\n readonly tieBreakReasons: readonly string[];\n readonly refName?: string;\n /** The caller-supplied required invariant set, sorted ascending. */\n readonly requiredInvariants: readonly string[];\n /**\n * The subset of `requiredInvariants` actually covered by edges on\n * `selectedPath`. Always a subset of `requiredInvariants` (when the path\n * is satisfying, equal to it); always derived from `selectedPath`.\n */\n readonly satisfiedInvariants: readonly string[];\n}\n\n/**\n * Outcome of {@link findPathWithDecision}. The pathfinder distinguishes\n * three cases up front so callers don't re-derive structural reachability:\n *\n * - `ok` — a path covering `required` exists; `decision` carries the\n * selection metadata and per-edge invariants.\n * - `unreachable` — `from`→`to` has no structural path. Mapped by callers\n * to the existing no-path / `NO_TARGET` diagnostic.\n * - `unsatisfiable` — `from`→`to` is structurally reachable but no path\n * covers every required invariant. `structuralPath` is the\n * `findPath(graph, from, to)` result, included so callers don't have to\n * recompute it when raising `MIGRATION.NO_INVARIANT_PATH`. `missing` is\n * the subset of `required` that the structural path does *not* cover —\n * correctly accounts for partial coverage when some required invariants\n * are met by the fallback path. Only emitted when `required` is\n * non-empty.\n */\nexport type FindPathOutcome =\n | { readonly kind: 'ok'; readonly decision: PathDecision }\n | { readonly kind: 'unreachable' }\n | {\n readonly kind: 'unsatisfiable';\n readonly structuralPath: readonly MigrationEdge[];\n readonly missing: readonly string[];\n };\n\n/**\n * Routing context for {@link findPathWithDecision}. Both fields are optional;\n * `refName` is only used to decorate the resulting `PathDecision` for the\n * JSON envelope, and `required` defaults to an empty set (purely structural\n * routing). They are passed via a single options object so the call sites\n * cannot silently swap two adjacent string parameters.\n */\nexport interface FindPathWithDecisionOptions {\n readonly refName?: string;\n readonly required?: ReadonlySet<string>;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` and return structured\n * path-decision metadata for machine-readable output. When `required` is\n * non-empty, the returned path is the shortest one whose edges collectively\n * cover every required invariant.\n *\n * The discriminated return type tells the caller *why* a path could not be\n * found, so the CLI can pick the right structured error without re-running\n * a structural BFS.\n */\nexport function findPathWithDecision(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n options: FindPathWithDecisionOptions = {},\n): FindPathOutcome {\n const { refName, required = new Set<string>() } = options;\n const requiredInvariants = [...required].sort();\n\n if (fromHash === toHash && required.size === 0) {\n return {\n kind: 'ok',\n decision: {\n selectedPath: [],\n fromHash,\n toHash,\n alternativeCount: 0,\n tieBreakReasons: [],\n requiredInvariants,\n satisfiedInvariants: [],\n ...ifDefined('refName', refName),\n },\n };\n }\n\n const path = findPathWithInvariants(graph, fromHash, toHash, required);\n if (!path) {\n if (required.size === 0) {\n return { kind: 'unreachable' };\n }\n const structural = findPath(graph, fromHash, toHash);\n if (structural === null) {\n return { kind: 'unreachable' };\n }\n const coveredByStructural = new Set<string>();\n for (const edge of structural) {\n for (const inv of edge.invariants) {\n if (required.has(inv)) coveredByStructural.add(inv);\n }\n }\n const missing = requiredInvariants.filter((id) => !coveredByStructural.has(id));\n return { kind: 'unsatisfiable', structuralPath: structural, missing };\n }\n\n const satisfiedInvariants = computeSatisfiedInvariants(required, path);\n\n // Single reverse BFS marks every node from which `toHash` is reachable.\n // Replaces a per-edge `findPath(e.to, toHash)` call inside the loop below,\n // which made the whole function O(|path| · (V + E)) instead of O(V + E).\n const reachesTarget = collectNodesReachingTarget(graph, toHash);\n const coveragePrefixes = requiredCoveragePrefixes(required, path);\n\n const tieBreakReasons: string[] = [];\n let alternativeCount = 0;\n\n for (const [i, edge] of path.entries()) {\n const outgoing = graph.forwardChain.get(edge.from);\n if (!outgoing || outgoing.length <= 1) continue;\n const reachable = outgoing.filter((e) => reachesTarget.has(e.to));\n if (reachable.length <= 1) continue;\n\n let comparisonPool: readonly MigrationEdge[] = reachable;\n if (required.size > 0) {\n // coveragePrefixes is built one-per-edge from path, so the index is\n // always in range here; the explicit guard keeps the type narrowed\n // without a non-null assertion.\n const prefixSet = coveragePrefixes[i];\n if (prefixSet === undefined) continue;\n comparisonPool = invariantViableAlternativesAtStep(required, prefixSet, reachable);\n }\n\n alternativeCount += reachable.length - 1;\n const sorted = sortedNeighbors(reachable);\n if (sorted[0]?.migrationHash !== edge.migrationHash) continue;\n if (!reachable.some((e) => e.migrationHash !== edge.migrationHash)) continue;\n\n const sortedViable = sortedNeighbors(comparisonPool);\n if (\n sortedViable.length > 1 &&\n sortedViable[0]?.migrationHash === edge.migrationHash &&\n sortedViable.some((e) => e.migrationHash !== edge.migrationHash)\n ) {\n tieBreakReasons.push(\n `at ${edge.from}: ${comparisonPool.length} candidates, selected by tie-break`,\n );\n }\n }\n\n return {\n kind: 'ok',\n decision: {\n selectedPath: path,\n fromHash,\n toHash,\n alternativeCount,\n tieBreakReasons,\n requiredInvariants,\n satisfiedInvariants,\n ...ifDefined('refName', refName),\n },\n };\n}\n\nfunction computeSatisfiedInvariants(\n required: ReadonlySet<string>,\n path: readonly MigrationEdge[],\n): readonly string[] {\n if (required.size === 0) return [];\n const covered = new Set<string>();\n for (const edge of path) {\n for (const inv of edge.invariants) {\n if (required.has(inv)) covered.add(inv);\n }\n }\n return [...covered].sort();\n}\n\n/**\n * For each edge on path, invariant coverage accumulated from earlier edges only —\n * `(required ∩ ∪_{j<i} path[j].invariants)` represented as cumulative set along `required`,\n * keyed as \"full set of required ids satisfied before taking path[i]\".\n */\nfunction requiredCoveragePrefixes(\n required: ReadonlySet<string>,\n path: readonly MigrationEdge[],\n): readonly ReadonlySet<string>[] {\n const prefixes: ReadonlySet<string>[] = [];\n const acc = new Set<string>();\n for (const edge of path) {\n prefixes.push(new Set(acc));\n for (const inv of edge.invariants) {\n if (required.has(inv)) acc.add(inv);\n }\n }\n return prefixes;\n}\n\nfunction invariantViableAlternativesAtStep(\n required: ReadonlySet<string>,\n coverageBeforeTakingEdge: ReadonlySet<string>,\n outgoing: readonly MigrationEdge[],\n): readonly MigrationEdge[] {\n if (required.size === 0) return [...outgoing];\n return outgoing.filter((e) =>\n [...required].every((id) => coverageBeforeTakingEdge.has(id) || e.invariants.includes(id)),\n );\n}\n\n/**\n * Walk ancestors of each branch tip back to find the last node\n * that appears on all paths. Returns `fromHash` if no shared ancestor is found.\n */\nfunction findDivergencePoint(\n graph: MigrationGraph,\n fromHash: string,\n leaves: readonly string[],\n): string {\n const ancestorSets = leaves.map((leaf) => {\n const ancestors = new Set<string>();\n for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) {\n ancestors.add(step.state);\n }\n return ancestors;\n });\n\n const commonAncestors = [...(ancestorSets[0] ?? [])].filter((node) =>\n ancestorSets.every((s) => s.has(node)),\n );\n\n let deepest = fromHash;\n let deepestDepth = -1;\n for (const ancestor of commonAncestors) {\n const path = findPath(graph, fromHash, ancestor);\n const depth = path ? path.length : 0;\n if (depth > deepestDepth) {\n deepestDepth = depth;\n deepest = ancestor;\n }\n }\n return deepest;\n}\n\n/**\n * Find all branch tips (nodes with no outgoing edges) reachable from\n * `fromHash` via forward edges.\n */\nexport function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {\n const leaves: string[] = [];\n for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) {\n if (!graph.forwardChain.get(step.state)?.length) {\n leaves.push(step.state);\n }\n }\n return leaves;\n}\n\n/**\n * Find the target contract hash of the migration graph reachable from\n * EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target\n * state (either empty, or containing only the root with no outgoing\n * edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none\n * originate from the empty hash, and AMBIGUOUS_TARGET if multiple\n * branch tips exist.\n */\nexport function findLeaf(graph: MigrationGraph): string | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {\n throw errorNoInitialMigration([...graph.nodes]);\n }\n\n const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);\n\n if (leaves.length === 0) {\n const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);\n if (reachable.length > 0) {\n throw errorNoTarget(reachable);\n }\n return null;\n }\n\n if (leaves.length > 1) {\n const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);\n const branches = leaves.map((tip) => {\n const path = findPath(graph, divergencePoint, tip);\n return {\n tip,\n edges: (path ?? []).map((e) => ({ dirName: e.dirName, from: e.from, to: e.to })),\n };\n });\n throw errorAmbiguousTarget(leaves, { divergencePoint, branches });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: leaves.length is neither 0 nor >1 per the branches above, so exactly one leaf remains\n return leaves[0]!;\n}\n\n/**\n * Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH\n * to the single target. Returns null for an empty graph.\n * Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationEdge | null {\n const leafHash = findLeaf(graph);\n if (leafHash === null) return null;\n\n const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);\n return path?.at(-1) ?? null;\n}\n\nexport function detectCycles(graph: MigrationGraph): readonly string[][] {\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n\n const color = new Map<string, number>();\n const parentMap = new Map<string, string | null>();\n const cycles: string[][] = [];\n\n for (const node of graph.nodes) {\n color.set(node, WHITE);\n }\n\n // Iterative three-color DFS. A frame is (node, outgoing edges, next-index).\n interface Frame {\n node: string;\n outgoing: readonly MigrationEdge[];\n index: number;\n }\n const stack: Frame[] = [];\n\n function pushFrame(u: string): void {\n color.set(u, GRAY);\n stack.push({ node: u, outgoing: graph.forwardChain.get(u) ?? [], index: 0 });\n }\n\n for (const root of graph.nodes) {\n if (color.get(root) !== WHITE) continue;\n parentMap.set(root, null);\n pushFrame(root);\n\n while (stack.length > 0) {\n // biome-ignore lint/style/noNonNullAssertion: stack.length > 0 should guarantee that this cannot be undefined\n const frame = stack[stack.length - 1]!;\n if (frame.index >= frame.outgoing.length) {\n color.set(frame.node, BLACK);\n stack.pop();\n continue;\n }\n // biome-ignore lint/style/noNonNullAssertion: the early-continue above guarantees frame.index < frame.outgoing.length here, so this is defined\n const edge = frame.outgoing[frame.index++]!;\n const v = edge.to;\n const vColor = color.get(v);\n if (vColor === GRAY) {\n const cycle: string[] = [v];\n let cur = frame.node;\n while (cur !== v) {\n cycle.push(cur);\n cur = parentMap.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (vColor === WHITE) {\n parentMap.set(v, frame.node);\n pushFrame(v);\n }\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const startNodes: string[] = [];\n\n if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) {\n startNodes.push(EMPTY_CONTRACT_HASH);\n } else {\n const allTargets = new Set<string>();\n for (const edges of graph.forwardChain.values()) {\n for (const edge of edges) {\n allTargets.add(edge.to);\n }\n }\n for (const node of graph.nodes) {\n if (!allTargets.has(node)) {\n startNodes.push(node);\n }\n }\n }\n\n for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) {\n reachable.add(step.state);\n }\n\n const orphans: MigrationEdge[] = [];\n for (const [from, migrations] of graph.forwardChain) {\n if (!reachable.has(from)) {\n orphans.push(...migrations);\n }\n }\n\n return orphans;\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
- import { u as errorInvalidOperationEntry, x as errorStaleContractBookends } from "../errors-Bl3cKiM8.mjs";
1
+ import { S as errorStaleContractBookends, u as errorInvalidOperationEntry } from "../errors-CfmjBeK0.mjs";
2
2
  import { t as computeMigrationHash } from "../hash-BARZdVgW.mjs";
3
- import { t as deriveProvidedInvariants } from "../invariants-BmrTBQ0A.mjs";
3
+ import { t as deriveProvidedInvariants } from "../invariants-30VA65sB.mjs";
4
4
  import { t as MigrationOpSchema } from "../op-schema-DZKFua46.mjs";
5
- import { type } from "arktype";
6
5
  import { ifDefined } from "@prisma-next/utils/defined";
6
+ import { type } from "arktype";
7
7
  import { realpathSync } from "node:fs";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
@@ -1,4 +1,4 @@
1
- import { d as errorInvalidRefFile, f as errorInvalidRefName, p as errorInvalidRefValue, t as MigrationToolsError } from "../errors-Bl3cKiM8.mjs";
1
+ import { d as errorInvalidRefFile, f as errorInvalidRefName, p as errorInvalidRefValue, t as MigrationToolsError } from "../errors-CfmjBeK0.mjs";
2
2
  import { dirname, join, relative } from "pathe";
3
3
  import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
4
4
  import { type } from "arktype";
@@ -1,4 +1,4 @@
1
- import { i as errorDuplicateInvariantInEdge, s as errorInvalidInvariantId } from "./errors-Bl3cKiM8.mjs";
1
+ import { i as errorDuplicateInvariantInEdge, s as errorInvalidInvariantId } from "./errors-CfmjBeK0.mjs";
2
2
 
3
3
  //#region src/invariants.ts
4
4
  /**
@@ -39,4 +39,4 @@ function readInvariantId(op) {
39
39
 
40
40
  //#endregion
41
41
  export { validateInvariantId as n, deriveProvidedInvariants as t };
42
- //# sourceMappingURL=invariants-BmrTBQ0A.mjs.map
42
+ //# sourceMappingURL=invariants-30VA65sB.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"invariants-BmrTBQ0A.mjs","names":[],"sources":["../src/invariants.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { errorDuplicateInvariantInEdge, errorInvalidInvariantId } from './errors';\nimport type { MigrationOps } from './package';\n\n/**\n * Hygiene check for `invariantId`. Rejects empty values plus any\n * whitespace or control character (including Unicode whitespace like\n * NBSP and em space, which are visually identical to ASCII space and\n * routinely sneak in via paste).\n */\nexport function validateInvariantId(invariantId: string): boolean {\n if (invariantId.length === 0) return false;\n return !/[\\p{Cc}\\p{White_Space}]/u.test(invariantId);\n}\n\n/**\n * Walk a migration's operations and produce its `providedInvariants`\n * aggregate: the sorted, deduplicated list of `invariantId`s declared\n * by data-transform ops. Ops without `operationClass === 'data'` are\n * skipped; data ops without an `invariantId` are skipped.\n *\n * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and\n * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.\n */\nexport function deriveProvidedInvariants(ops: MigrationOps): readonly string[] {\n const seen = new Set<string>();\n for (const op of ops) {\n const invariantId = readInvariantId(op);\n if (invariantId === undefined) continue;\n if (!validateInvariantId(invariantId)) {\n throw errorInvalidInvariantId(invariantId);\n }\n if (seen.has(invariantId)) {\n throw errorDuplicateInvariantInEdge(invariantId);\n }\n seen.add(invariantId);\n }\n return [...seen].sort();\n}\n\nfunction readInvariantId(op: MigrationPlanOperation): string | undefined {\n if (op.operationClass !== 'data') return undefined;\n const candidate = (op as { invariantId?: unknown }).invariantId;\n return typeof candidate === 'string' ? candidate : undefined;\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,oBAAoB,aAA8B;AAChE,KAAI,YAAY,WAAW,EAAG,QAAO;AACrC,QAAO,CAAC,2BAA2B,KAAK,YAAY;;;;;;;;;;;AAYtD,SAAgB,yBAAyB,KAAsC;CAC7E,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,cAAc,gBAAgB,GAAG;AACvC,MAAI,gBAAgB,OAAW;AAC/B,MAAI,CAAC,oBAAoB,YAAY,CACnC,OAAM,wBAAwB,YAAY;AAE5C,MAAI,KAAK,IAAI,YAAY,CACvB,OAAM,8BAA8B,YAAY;AAElD,OAAK,IAAI,YAAY;;AAEvB,QAAO,CAAC,GAAG,KAAK,CAAC,MAAM;;AAGzB,SAAS,gBAAgB,IAAgD;AACvE,KAAI,GAAG,mBAAmB,OAAQ,QAAO;CACzC,MAAM,YAAa,GAAiC;AACpD,QAAO,OAAO,cAAc,WAAW,YAAY"}
1
+ {"version":3,"file":"invariants-30VA65sB.mjs","names":[],"sources":["../src/invariants.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { errorDuplicateInvariantInEdge, errorInvalidInvariantId } from './errors';\nimport type { MigrationOps } from './package';\n\n/**\n * Hygiene check for `invariantId`. Rejects empty values plus any\n * whitespace or control character (including Unicode whitespace like\n * NBSP and em space, which are visually identical to ASCII space and\n * routinely sneak in via paste).\n */\nexport function validateInvariantId(invariantId: string): boolean {\n if (invariantId.length === 0) return false;\n return !/[\\p{Cc}\\p{White_Space}]/u.test(invariantId);\n}\n\n/**\n * Walk a migration's operations and produce its `providedInvariants`\n * aggregate: the sorted, deduplicated list of `invariantId`s declared\n * by data-transform ops. Ops without `operationClass === 'data'` are\n * skipped; data ops without an `invariantId` are skipped.\n *\n * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and\n * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.\n */\nexport function deriveProvidedInvariants(ops: MigrationOps): readonly string[] {\n const seen = new Set<string>();\n for (const op of ops) {\n const invariantId = readInvariantId(op);\n if (invariantId === undefined) continue;\n if (!validateInvariantId(invariantId)) {\n throw errorInvalidInvariantId(invariantId);\n }\n if (seen.has(invariantId)) {\n throw errorDuplicateInvariantInEdge(invariantId);\n }\n seen.add(invariantId);\n }\n return [...seen].sort();\n}\n\nfunction readInvariantId(op: MigrationPlanOperation): string | undefined {\n if (op.operationClass !== 'data') return undefined;\n const candidate = (op as { invariantId?: unknown }).invariantId;\n return typeof candidate === 'string' ? candidate : undefined;\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,oBAAoB,aAA8B;AAChE,KAAI,YAAY,WAAW,EAAG,QAAO;AACrC,QAAO,CAAC,2BAA2B,KAAK,YAAY;;;;;;;;;;;AAYtD,SAAgB,yBAAyB,KAAsC;CAC7E,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,cAAc,gBAAgB,GAAG;AACvC,MAAI,gBAAgB,OAAW;AAC/B,MAAI,CAAC,oBAAoB,YAAY,CACnC,OAAM,wBAAwB,YAAY;AAE5C,MAAI,KAAK,IAAI,YAAY,CACvB,OAAM,8BAA8B,YAAY;AAElD,OAAK,IAAI,YAAY;;AAEvB,QAAO,CAAC,GAAG,KAAK,CAAC,MAAM;;AAGzB,SAAS,gBAAgB,IAAgD;AACvE,KAAI,GAAG,mBAAmB,OAAQ,QAAO;CACzC,MAAM,YAAa,GAAiC;AACpD,QAAO,OAAO,cAAc,WAAW,YAAY"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.5.0-dev.36",
3
+ "version": "0.5.0-dev.37",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
@@ -8,9 +8,9 @@
8
8
  "arktype": "^2.1.29",
9
9
  "pathe": "^2.0.3",
10
10
  "prettier": "^3.6.2",
11
- "@prisma-next/contract": "0.5.0-dev.36",
12
- "@prisma-next/utils": "0.5.0-dev.36",
13
- "@prisma-next/framework-components": "0.5.0-dev.36"
11
+ "@prisma-next/contract": "0.5.0-dev.37",
12
+ "@prisma-next/utils": "0.5.0-dev.37",
13
+ "@prisma-next/framework-components": "0.5.0-dev.37"
14
14
  },
15
15
  "devDependencies": {
16
16
  "tsdown": "0.18.4",
package/src/errors.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ifDefined } from '@prisma-next/utils/defined';
1
2
  import { basename, dirname, relative } from 'pathe';
2
3
 
3
4
  /**
@@ -151,12 +152,12 @@ export function errorSameSourceAndTarget(dir: string, hash: string): MigrationTo
151
152
  const dirName = basename(dir);
152
153
  return new MigrationToolsError(
153
154
  'MIGRATION.SAME_SOURCE_AND_TARGET',
154
- 'Migration has same source and target',
155
+ 'Migration without data-transform operations has same source and target',
155
156
  {
156
- why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
157
+ why: `Migration "${dirName}" has from === to === "${hash}" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform otherwise the migration is a no-op.`,
157
158
  fix: reemitHint(
158
159
  dir,
159
- 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',
160
+ 'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',
160
161
  ),
161
162
  details: { dirName, hash },
162
163
  },
@@ -295,6 +296,74 @@ export function errorProvidedInvariantsMismatch(
295
296
  );
296
297
  }
297
298
 
299
+ /**
300
+ * Wire-shape edge surfaced through the JSON envelope's
301
+ * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —
302
+ * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but
303
+ * is intentionally dropped here so the envelope stays stable across
304
+ * graph-internal refactors.
305
+ *
306
+ * Stability: any field added here is part of the public CLI JSON contract.
307
+ * Callers (CLI consumers, agents) must be able to treat
308
+ * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.
309
+ */
310
+ export interface NoInvariantPathStructuralEdge {
311
+ readonly dirName: string;
312
+ readonly migrationHash: string;
313
+ readonly from: string;
314
+ readonly to: string;
315
+ readonly invariants: readonly string[];
316
+ }
317
+
318
+ export function errorNoInvariantPath(args: {
319
+ readonly refName?: string;
320
+ readonly required: readonly string[];
321
+ readonly missing: readonly string[];
322
+ readonly structuralPath: readonly NoInvariantPathStructuralEdge[];
323
+ }): MigrationToolsError {
324
+ const { refName, required, missing, structuralPath } = args;
325
+ const refClause = refName ? `Ref "${refName}"` : 'Target';
326
+ const missingList = missing.map((id) => JSON.stringify(id)).join(', ');
327
+ const requiredList = required.map((id) => JSON.stringify(id)).join(', ');
328
+ return new MigrationToolsError(
329
+ 'MIGRATION.NO_INVARIANT_PATH',
330
+ 'No path covers the required invariants',
331
+ {
332
+ why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,
333
+ fix: 'Add a migration on the path that runs `dataTransform({ invariantId: "<id>", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.',
334
+ details: {
335
+ required,
336
+ missing,
337
+ structuralPath,
338
+ ...ifDefined('refName', refName),
339
+ },
340
+ },
341
+ );
342
+ }
343
+
344
+ export function errorUnknownInvariant(args: {
345
+ readonly refName?: string;
346
+ readonly unknown: readonly string[];
347
+ readonly declared: readonly string[];
348
+ }): MigrationToolsError {
349
+ const { refName, unknown, declared } = args;
350
+ const refClause = refName ? `Ref "${refName}" declares` : 'Declares';
351
+ const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');
352
+ return new MigrationToolsError(
353
+ 'MIGRATION.UNKNOWN_INVARIANT',
354
+ 'Ref declares invariants no migration in the graph provides',
355
+ {
356
+ why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,
357
+ fix: 'Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.',
358
+ details: {
359
+ unknown,
360
+ declared,
361
+ ...ifDefined('refName', refName),
362
+ },
363
+ },
364
+ );
365
+ }
366
+
298
367
  export function errorMigrationHashMismatch(
299
368
  dir: string,
300
369
  storedHash: string,
@@ -1 +1,7 @@
1
- export { errorInvalidJson, MigrationToolsError } from '../errors';
1
+ export {
2
+ errorInvalidJson,
3
+ errorNoInvariantPath,
4
+ errorUnknownInvariant,
5
+ MigrationToolsError,
6
+ type NoInvariantPathStructuralEdge,
7
+ } from '../errors';
@@ -51,7 +51,10 @@ export function reconstructGraph(packages: readonly MigrationPackage[]): Migrati
51
51
  const { to } = pkg.metadata;
52
52
 
53
53
  if (from === to) {
54
- throw errorSameSourceAndTarget(pkg.dirPath, from);
54
+ const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');
55
+ if (!hasDataOp) {
56
+ throw errorSameSourceAndTarget(pkg.dirPath, from);
57
+ }
55
58
  }
56
59
 
57
60
  nodes.add(from);
@@ -175,15 +178,16 @@ export function findPathWithInvariants(
175
178
  if (required.size === 0) {
176
179
  return findPath(graph, fromHash, toHash);
177
180
  }
178
- if (fromHash === toHash) {
179
- // Empty path covers no invariants; required is non-empty ⇒ unsatisfiable.
180
- return null;
181
- }
182
181
 
183
182
  interface InvState {
184
183
  readonly node: string;
185
184
  readonly covered: ReadonlySet<string>;
186
185
  }
186
+ // `\0` is a safe segment separator: `validateInvariantId` rejects any id
187
+ // containing whitespace or control characters (NUL is U+0000), and node
188
+ // hashes are hex strings. Distinct `(node, covered)` tuples therefore
189
+ // map to distinct strings. If `validateInvariantId` is ever relaxed,
190
+ // re-confirm dedup correctness here.
187
191
  const stateKey = (s: InvState): string => {
188
192
  if (s.covered.size === 0) return `${s.node}\0`;
189
193
  return `${s.node}\0${[...s.covered].sort().join('\0')}`;
@@ -263,68 +267,212 @@ export interface PathDecision {
263
267
  readonly alternativeCount: number;
264
268
  readonly tieBreakReasons: readonly string[];
265
269
  readonly refName?: string;
270
+ /** The caller-supplied required invariant set, sorted ascending. */
271
+ readonly requiredInvariants: readonly string[];
272
+ /**
273
+ * The subset of `requiredInvariants` actually covered by edges on
274
+ * `selectedPath`. Always a subset of `requiredInvariants` (when the path
275
+ * is satisfying, equal to it); always derived from `selectedPath`.
276
+ */
277
+ readonly satisfiedInvariants: readonly string[];
278
+ }
279
+
280
+ /**
281
+ * Outcome of {@link findPathWithDecision}. The pathfinder distinguishes
282
+ * three cases up front so callers don't re-derive structural reachability:
283
+ *
284
+ * - `ok` — a path covering `required` exists; `decision` carries the
285
+ * selection metadata and per-edge invariants.
286
+ * - `unreachable` — `from`→`to` has no structural path. Mapped by callers
287
+ * to the existing no-path / `NO_TARGET` diagnostic.
288
+ * - `unsatisfiable` — `from`→`to` is structurally reachable but no path
289
+ * covers every required invariant. `structuralPath` is the
290
+ * `findPath(graph, from, to)` result, included so callers don't have to
291
+ * recompute it when raising `MIGRATION.NO_INVARIANT_PATH`. `missing` is
292
+ * the subset of `required` that the structural path does *not* cover —
293
+ * correctly accounts for partial coverage when some required invariants
294
+ * are met by the fallback path. Only emitted when `required` is
295
+ * non-empty.
296
+ */
297
+ export type FindPathOutcome =
298
+ | { readonly kind: 'ok'; readonly decision: PathDecision }
299
+ | { readonly kind: 'unreachable' }
300
+ | {
301
+ readonly kind: 'unsatisfiable';
302
+ readonly structuralPath: readonly MigrationEdge[];
303
+ readonly missing: readonly string[];
304
+ };
305
+
306
+ /**
307
+ * Routing context for {@link findPathWithDecision}. Both fields are optional;
308
+ * `refName` is only used to decorate the resulting `PathDecision` for the
309
+ * JSON envelope, and `required` defaults to an empty set (purely structural
310
+ * routing). They are passed via a single options object so the call sites
311
+ * cannot silently swap two adjacent string parameters.
312
+ */
313
+ export interface FindPathWithDecisionOptions {
314
+ readonly refName?: string;
315
+ readonly required?: ReadonlySet<string>;
266
316
  }
267
317
 
268
318
  /**
269
319
  * Find the shortest path from `fromHash` to `toHash` and return structured
270
- * path-decision metadata for machine-readable output.
320
+ * path-decision metadata for machine-readable output. When `required` is
321
+ * non-empty, the returned path is the shortest one whose edges collectively
322
+ * cover every required invariant.
323
+ *
324
+ * The discriminated return type tells the caller *why* a path could not be
325
+ * found, so the CLI can pick the right structured error without re-running
326
+ * a structural BFS.
271
327
  */
272
328
  export function findPathWithDecision(
273
329
  graph: MigrationGraph,
274
330
  fromHash: string,
275
331
  toHash: string,
276
- refName?: string,
277
- ): PathDecision | null {
278
- if (fromHash === toHash) {
332
+ options: FindPathWithDecisionOptions = {},
333
+ ): FindPathOutcome {
334
+ const { refName, required = new Set<string>() } = options;
335
+ const requiredInvariants = [...required].sort();
336
+
337
+ if (fromHash === toHash && required.size === 0) {
279
338
  return {
280
- selectedPath: [],
281
- fromHash,
282
- toHash,
283
- alternativeCount: 0,
284
- tieBreakReasons: [],
285
- ...ifDefined('refName', refName),
339
+ kind: 'ok',
340
+ decision: {
341
+ selectedPath: [],
342
+ fromHash,
343
+ toHash,
344
+ alternativeCount: 0,
345
+ tieBreakReasons: [],
346
+ requiredInvariants,
347
+ satisfiedInvariants: [],
348
+ ...ifDefined('refName', refName),
349
+ },
286
350
  };
287
351
  }
288
352
 
289
- const path = findPath(graph, fromHash, toHash);
290
- if (!path) return null;
353
+ const path = findPathWithInvariants(graph, fromHash, toHash, required);
354
+ if (!path) {
355
+ if (required.size === 0) {
356
+ return { kind: 'unreachable' };
357
+ }
358
+ const structural = findPath(graph, fromHash, toHash);
359
+ if (structural === null) {
360
+ return { kind: 'unreachable' };
361
+ }
362
+ const coveredByStructural = new Set<string>();
363
+ for (const edge of structural) {
364
+ for (const inv of edge.invariants) {
365
+ if (required.has(inv)) coveredByStructural.add(inv);
366
+ }
367
+ }
368
+ const missing = requiredInvariants.filter((id) => !coveredByStructural.has(id));
369
+ return { kind: 'unsatisfiable', structuralPath: structural, missing };
370
+ }
371
+
372
+ const satisfiedInvariants = computeSatisfiedInvariants(required, path);
291
373
 
292
374
  // Single reverse BFS marks every node from which `toHash` is reachable.
293
375
  // Replaces a per-edge `findPath(e.to, toHash)` call inside the loop below,
294
376
  // which made the whole function O(|path| · (V + E)) instead of O(V + E).
295
377
  const reachesTarget = collectNodesReachingTarget(graph, toHash);
378
+ const coveragePrefixes = requiredCoveragePrefixes(required, path);
296
379
 
297
380
  const tieBreakReasons: string[] = [];
298
381
  let alternativeCount = 0;
299
382
 
300
- for (const edge of path) {
383
+ for (const [i, edge] of path.entries()) {
301
384
  const outgoing = graph.forwardChain.get(edge.from);
302
- if (outgoing && outgoing.length > 1) {
303
- const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
304
- if (reachable.length > 1) {
305
- alternativeCount += reachable.length - 1;
306
- const sorted = sortedNeighbors(reachable);
307
- if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {
308
- if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) {
309
- tieBreakReasons.push(
310
- `at ${edge.from}: ${reachable.length} candidates, selected by tie-break`,
311
- );
312
- }
313
- }
314
- }
385
+ if (!outgoing || outgoing.length <= 1) continue;
386
+ const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
387
+ if (reachable.length <= 1) continue;
388
+
389
+ let comparisonPool: readonly MigrationEdge[] = reachable;
390
+ if (required.size > 0) {
391
+ // coveragePrefixes is built one-per-edge from path, so the index is
392
+ // always in range here; the explicit guard keeps the type narrowed
393
+ // without a non-null assertion.
394
+ const prefixSet = coveragePrefixes[i];
395
+ if (prefixSet === undefined) continue;
396
+ comparisonPool = invariantViableAlternativesAtStep(required, prefixSet, reachable);
397
+ }
398
+
399
+ alternativeCount += reachable.length - 1;
400
+ const sorted = sortedNeighbors(reachable);
401
+ if (sorted[0]?.migrationHash !== edge.migrationHash) continue;
402
+ if (!reachable.some((e) => e.migrationHash !== edge.migrationHash)) continue;
403
+
404
+ const sortedViable = sortedNeighbors(comparisonPool);
405
+ if (
406
+ sortedViable.length > 1 &&
407
+ sortedViable[0]?.migrationHash === edge.migrationHash &&
408
+ sortedViable.some((e) => e.migrationHash !== edge.migrationHash)
409
+ ) {
410
+ tieBreakReasons.push(
411
+ `at ${edge.from}: ${comparisonPool.length} candidates, selected by tie-break`,
412
+ );
315
413
  }
316
414
  }
317
415
 
318
416
  return {
319
- selectedPath: path,
320
- fromHash,
321
- toHash,
322
- alternativeCount,
323
- tieBreakReasons,
324
- ...ifDefined('refName', refName),
417
+ kind: 'ok',
418
+ decision: {
419
+ selectedPath: path,
420
+ fromHash,
421
+ toHash,
422
+ alternativeCount,
423
+ tieBreakReasons,
424
+ requiredInvariants,
425
+ satisfiedInvariants,
426
+ ...ifDefined('refName', refName),
427
+ },
325
428
  };
326
429
  }
327
430
 
431
+ function computeSatisfiedInvariants(
432
+ required: ReadonlySet<string>,
433
+ path: readonly MigrationEdge[],
434
+ ): readonly string[] {
435
+ if (required.size === 0) return [];
436
+ const covered = new Set<string>();
437
+ for (const edge of path) {
438
+ for (const inv of edge.invariants) {
439
+ if (required.has(inv)) covered.add(inv);
440
+ }
441
+ }
442
+ return [...covered].sort();
443
+ }
444
+
445
+ /**
446
+ * For each edge on path, invariant coverage accumulated from earlier edges only —
447
+ * `(required ∩ ∪_{j<i} path[j].invariants)` represented as cumulative set along `required`,
448
+ * keyed as "full set of required ids satisfied before taking path[i]".
449
+ */
450
+ function requiredCoveragePrefixes(
451
+ required: ReadonlySet<string>,
452
+ path: readonly MigrationEdge[],
453
+ ): readonly ReadonlySet<string>[] {
454
+ const prefixes: ReadonlySet<string>[] = [];
455
+ const acc = new Set<string>();
456
+ for (const edge of path) {
457
+ prefixes.push(new Set(acc));
458
+ for (const inv of edge.invariants) {
459
+ if (required.has(inv)) acc.add(inv);
460
+ }
461
+ }
462
+ return prefixes;
463
+ }
464
+
465
+ function invariantViableAlternativesAtStep(
466
+ required: ReadonlySet<string>,
467
+ coverageBeforeTakingEdge: ReadonlySet<string>,
468
+ outgoing: readonly MigrationEdge[],
469
+ ): readonly MigrationEdge[] {
470
+ if (required.size === 0) return [...outgoing];
471
+ return outgoing.filter((e) =>
472
+ [...required].every((id) => coverageBeforeTakingEdge.has(id) || e.invariants.includes(id)),
473
+ );
474
+ }
475
+
328
476
  /**
329
477
  * Walk ancestors of each branch tip back to find the last node
330
478
  * that appears on all paths. Returns `fromHash` if no shared ancestor is found.
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-Bl3cKiM8.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 | null;\n readonly contractHash: string;\n}): MigrationToolsError {\n const { side, metaHash, contractHash } = args;\n // `meta.from` is `string | null` (null = baseline). Render `null` as a\n // human-readable token in the diagnostic so the message stays clear when\n // the mismatch is a baseline-vs-non-baseline disagreement.\n const renderedMetaHash = metaHash === null ? 'null (baseline)' : `\"${metaHash}\"`;\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} = ${renderedMetaHash}. 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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"}