@prisma-next/migration-tools 0.11.0-dev.33 → 0.11.0-dev.35

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.
Files changed (50) hide show
  1. package/README.md +4 -4
  2. package/dist/errors-CoEN114u.mjs.map +1 -1
  3. package/dist/exports/aggregate.d.mts +2 -2
  4. package/dist/exports/aggregate.mjs +4 -4
  5. package/dist/exports/enumerate-migration-spaces.mjs +2 -2
  6. package/dist/exports/errors.d.mts +2 -2
  7. package/dist/exports/graph.d.mts +1 -1
  8. package/dist/exports/hash.d.mts +7 -8
  9. package/dist/exports/hash.d.mts.map +1 -1
  10. package/dist/exports/hash.mjs +1 -1
  11. package/dist/exports/io.d.mts.map +1 -1
  12. package/dist/exports/io.mjs +1 -1
  13. package/dist/exports/metadata.d.mts +2 -2
  14. package/dist/exports/migration-graph.d.mts +2 -2
  15. package/dist/exports/migration-graph.mjs +1 -1
  16. package/dist/exports/migration.d.mts +5 -6
  17. package/dist/exports/migration.d.mts.map +1 -1
  18. package/dist/exports/migration.mjs +12 -30
  19. package/dist/exports/migration.mjs.map +1 -1
  20. package/dist/exports/ref-resolution.d.mts +1 -1
  21. package/dist/exports/spaces.mjs +4 -4
  22. package/dist/{graph-C7AJPGV5.d.mts → graph-ZAIvH6p4.d.mts} +1 -2
  23. package/dist/graph-ZAIvH6p4.d.mts.map +1 -0
  24. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  25. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  26. package/dist/{io-gHmDrSjQ.mjs → io-Dc64lvaL.mjs} +2 -8
  27. package/dist/io-Dc64lvaL.mjs.map +1 -0
  28. package/dist/metadata-BCH1rNsk.d.mts +2 -0
  29. package/dist/{migration-graph-C2iNX8dk.mjs → migration-graph-D5JeadSE.mjs} +4 -20
  30. package/dist/migration-graph-D5JeadSE.mjs.map +1 -0
  31. package/dist/{migration-graph-ABYqVsmv.d.mts → migration-graph-D97XMWVH.d.mts} +5 -5
  32. package/dist/{migration-graph-ABYqVsmv.d.mts.map → migration-graph-D97XMWVH.d.mts.map} +1 -1
  33. package/dist/{read-contract-space-contract-CwwlqKl_.mjs → read-contract-space-contract-_EvvV5Gl.mjs} +2 -2
  34. package/dist/{read-contract-space-contract-CwwlqKl_.mjs.map → read-contract-space-contract-_EvvV5Gl.mjs.map} +1 -1
  35. package/dist/{verify-contract-spaces-DxXWxGR0.mjs → verify-contract-spaces-DIdQLGo7.mjs} +2 -2
  36. package/dist/{verify-contract-spaces-DxXWxGR0.mjs.map → verify-contract-spaces-DIdQLGo7.mjs.map} +1 -1
  37. package/package.json +6 -6
  38. package/src/errors.ts +2 -2
  39. package/src/exports/metadata.ts +1 -1
  40. package/src/graph.ts +0 -1
  41. package/src/hash.ts +7 -8
  42. package/src/io.ts +0 -8
  43. package/src/metadata.ts +1 -1
  44. package/src/migration-base.ts +10 -30
  45. package/src/migration-graph.ts +4 -18
  46. package/dist/graph-C7AJPGV5.d.mts.map +0 -1
  47. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  48. package/dist/io-gHmDrSjQ.mjs.map +0 -1
  49. package/dist/metadata-B4Wy6zor.d.mts +0 -2
  50. package/dist/migration-graph-C2iNX8dk.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io-Dc64lvaL.mjs","names":[],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';\nimport type {\n MigrationMetadata,\n MigrationPackage,\n} from '@prisma-next/framework-components/control';\nimport { type } from 'arktype';\nimport { basename, dirname, join, resolve } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n errorProvidedInvariantsMismatch,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport { MigrationOpsSchema } from './op-schema';\nimport type { MigrationOps, OnDiskMigrationPackage } from './package';\n\nexport const MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationMetadataSchema = type({\n '+': 'reject',\n from: 'string > 0 | null',\n to: 'string',\n migrationHash: 'string',\n providedInvariants: 'string[]',\n createdAt: 'string',\n});\n\nexport async function writeMigrationPackage(\n dir: string,\n metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Materialise an in-memory {@link MigrationPackage} to a per-space\n * directory on disk.\n *\n * Writes two files under `<targetDir>/<pkg.dirName>/`:\n *\n * - `migration.json` — the manifest (pretty-printed, matches\n * {@link writeMigrationPackage}'s output for byte-for-byte parity with\n * app-space migrations).\n * - `ops.json` — the operation list (pretty-printed).\n *\n * Distinct verb from the lower-level {@link writeMigrationPackage}\n * (which takes constituent `(metadata, ops)`): callers reading\n * `materialise…` know they are persisting a struct-typed package.\n *\n * Overwrite-idempotent: the per-package directory is cleared before\n * each emit, so re-running against the same `targetDir` produces\n * byte-identical contents and never leaves stale files behind. The\n * lower-level {@link writeMigrationPackage} stays strict because the\n * CLI authoring path (`migration plan` / `migration new`) deliberately\n * refuses to clobber an existing authored migration; this helper is\n * the re-emit path that is supposed to converge on a single canonical\n * on-disk shape.\n *\n * The per-space head contract lives at\n * `<projectMigrationsDir>/<spaceId>/contract.json` (written by\n * {@link import('./emit-contract-space-artefacts').emitContractSpaceArtefacts}),\n * not inside the per-package directory. The runner reads only\n * `migration.json` + `ops.json` from each package.\n */\nexport async function materialiseMigrationPackage(\n targetDir: string,\n pkg: MigrationPackage,\n): Promise<void> {\n const dir = join(targetDir, pkg.dirName);\n await rm(dir, { recursive: true, force: true });\n await writeMigrationPackage(dir, pkg.metadata, pkg.ops);\n}\n\n/**\n * Idempotent variant of {@link materialiseMigrationPackage}: writes the\n * package only if `<targetDir>/<pkg.dirName>/` does not already exist on\n * disk as a directory; returns `{ written: false }` when the package\n * directory is present (no rewrite, no comparison — by-existence skip).\n *\n * Concretely:\n * - existing directory → skip silently, return `{ written: false }`.\n * - missing path → write three files via {@link materialiseMigrationPackage},\n * return `{ written: true }`.\n * - path exists but is not a directory (file/symlink) → treated as\n * missing; {@link materialiseMigrationPackage} will attempt creation\n * and fail with an appropriate OS error.\n * - any other I/O error from `stat` → propagated unchanged.\n *\n * Used by the CLI's `runContractSpaceExtensionMigrationsPass` to\n * materialise extension migration packages into a project's\n * `migrations/<spaceId>/` directory, and by extension-package tests\n * that mirror the same idempotent-rematerialise property locally\n * without taking a CLI dependency.\n */\nexport async function materialiseExtensionMigrationPackageIfMissing(\n targetDir: string,\n pkg: MigrationPackage,\n): Promise<{ readonly written: boolean }> {\n const pkgDir = join(targetDir, pkg.dirName);\n if (await directoryExists(pkgDir)) {\n return { written: false };\n }\n await materialiseMigrationPackage(targetDir, pkg);\n return { written: true };\n}\n\nasync function directoryExists(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isDirectory();\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return false;\n throw error;\n }\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<OnDiskMigrationPackage> {\n const absoluteDir = resolve(dir);\n const manifestPath = join(absoluteDir, MANIFEST_FILE);\n const opsPath = join(absoluteDir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, absoluteDir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, absoluteDir);\n }\n throw error;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n // Re-derive before the hash check so format/duplicate diagnostics\n // fire with their dedicated codes rather than as a generic hash mismatch.\n const derivedInvariants = deriveProvidedInvariants(ops);\n if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {\n throw errorProvidedInvariantsMismatch(\n manifestPath,\n metadata.providedInvariants,\n derivedInvariants,\n );\n }\n\n const pkg: OnDiskMigrationPackage = {\n dirName: basename(absoluteDir),\n dirPath: absoluteDir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(\n absoluteDir,\n verification.storedHash,\n verification.computedHash,\n );\n }\n\n return pkg;\n}\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly OnDiskMigrationPackage[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: OnDiskMigrationPackage[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { MigrationMetadata as MigrationMetadata$1 } from "@prisma-next/framework-components/control";
2
+ export { MigrationMetadata$1 as t };
@@ -122,7 +122,6 @@ function reconstructGraph(packages) {
122
122
  migrationHash: pkg.metadata.migrationHash,
123
123
  dirName: pkg.dirName,
124
124
  createdAt: pkg.metadata.createdAt,
125
- labels: pkg.metadata.labels,
126
125
  invariants: pkg.metadata.providedInvariants
127
126
  };
128
127
  if (migrationByHash.has(migration.migrationHash)) throw errorDuplicateMigrationHash(migration.migrationHash);
@@ -137,22 +136,7 @@ function reconstructGraph(packages) {
137
136
  migrationByHash
138
137
  };
139
138
  }
140
- const LABEL_PRIORITY = {
141
- main: 0,
142
- default: 1,
143
- feature: 2
144
- };
145
- function labelPriority(labels) {
146
- let best = 3;
147
- for (const l of labels) {
148
- const p = LABEL_PRIORITY[l];
149
- if (p !== void 0 && p < best) best = p;
150
- }
151
- return best;
152
- }
153
139
  function compareTieBreak(a, b) {
154
- const lp = labelPriority(a.labels) - labelPriority(b.labels);
155
- if (lp !== 0) return lp;
156
140
  const ca = a.createdAt.localeCompare(b.createdAt);
157
141
  if (ca !== 0) return ca;
158
142
  const tc = a.to.localeCompare(b.to);
@@ -168,7 +152,7 @@ function sortedNeighbors(edges) {
168
152
  * exists. Returns an empty array when `fromHash === toHash` (no-op).
169
153
  *
170
154
  * Neighbor ordering is deterministic via the tie-break sort key:
171
- * label priority → createdAt → to → migrationHash.
155
+ * createdAt → to → migrationHash.
172
156
  */
173
157
  function findPath(graph, fromHash, toHash) {
174
158
  if (fromHash === toHash) return [];
@@ -208,8 +192,8 @@ function findPath(graph, fromHash, toHash) {
208
192
  * control chars at authoring time).
209
193
  *
210
194
  * Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
211
- * invariant come first, with `labelPriority → createdAt → to → migrationHash`
212
- * as the secondary key. The heuristic steers BFS toward the satisfying path;
195
+ * invariant come first, with `createdAt → to → migrationHash` as the
196
+ * secondary key. The heuristic steers BFS toward the satisfying path;
213
197
  * correctness (shortest, deterministic) does not depend on it.
214
198
  */
215
199
  function findPathWithInvariants(graph, fromHash, toHash, required) {
@@ -520,4 +504,4 @@ function detectOrphans(graph) {
520
504
  //#endregion
521
505
  export { findPath as a, findReachableLeaves as c, findLeaf as i, reconstructGraph as l, detectOrphans as n, findPathWithDecision as o, findLatestMigration as r, findPathWithInvariants as s, detectCycles as t };
522
506
 
523
- //# sourceMappingURL=migration-graph-C2iNX8dk.mjs.map
507
+ //# sourceMappingURL=migration-graph-D5JeadSE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-graph-D5JeadSE.mjs","names":[],"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 { OnDiskMigrationPackage } 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 OnDiskMigrationPackage[]): 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 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: createdAt → to → migrationHash.\n// ---------------------------------------------------------------------------\n\nfunction compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {\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 * 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 `createdAt → to → migrationHash` as the\n * 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,5 +1,5 @@
1
1
  import { n as OnDiskMigrationPackage } from "./package-DIttKL7X.mjs";
2
- import { n as MigrationGraph, t as MigrationEdge } from "./graph-C7AJPGV5.mjs";
2
+ import { n as MigrationGraph, t as MigrationEdge } from "./graph-ZAIvH6p4.mjs";
3
3
 
4
4
  //#region src/migration-graph.d.ts
5
5
  declare function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): MigrationGraph;
@@ -9,7 +9,7 @@ declare function reconstructGraph(packages: readonly OnDiskMigrationPackage[]):
9
9
  * exists. Returns an empty array when `fromHash === toHash` (no-op).
10
10
  *
11
11
  * Neighbor ordering is deterministic via the tie-break sort key:
12
- * label priority → createdAt → to → migrationHash.
12
+ * createdAt → to → migrationHash.
13
13
  */
14
14
  declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly MigrationEdge[] | null;
15
15
  /**
@@ -27,8 +27,8 @@ declare function findPath(graph: MigrationGraph, fromHash: string, toHash: strin
27
27
  * control chars at authoring time).
28
28
  *
29
29
  * Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
30
- * invariant come first, with `labelPriority → createdAt → to → migrationHash`
31
- * as the secondary key. The heuristic steers BFS toward the satisfying path;
30
+ * invariant come first, with `createdAt → to → migrationHash` as the
31
+ * secondary key. The heuristic steers BFS toward the satisfying path;
32
32
  * correctness (shortest, deterministic) does not depend on it.
33
33
  */
34
34
  declare function findPathWithInvariants(graph: MigrationGraph, fromHash: string, toHash: string, required: ReadonlySet<string>): readonly MigrationEdge[] | null;
@@ -121,4 +121,4 @@ declare function detectCycles(graph: MigrationGraph): readonly string[][];
121
121
  declare function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[];
122
122
  //#endregion
123
123
  export { findLeaf as a, findPathWithInvariants as c, findLatestMigration as i, findReachableLeaves as l, detectCycles as n, findPath as o, detectOrphans as r, findPathWithDecision as s, PathDecision as t, reconstructGraph as u };
124
- //# sourceMappingURL=migration-graph-ABYqVsmv.d.mts.map
124
+ //# sourceMappingURL=migration-graph-D97XMWVH.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration-graph-ABYqVsmv.d.mts","names":[],"sources":["../src/migration-graph.ts"],"mappings":";;;;iBAsCgB,gBAAA,CAAiB,QAAA,WAAmB,sBAAA,KAA2B,cAAc;;AAA7F;;;;;;;iBAqFgB,QAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,oBACU,aAAa;AAzFoE;AAqF7F;;;;;;;;;;AAIyB;AA4CzB;;;;;;;AArI6F,iBAqI7E,sBAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,UACA,QAAA,EAAU,WAAA,oBACA,aAAA;AAAA,UAsFK,YAAA;EAAA,SACN,YAAA,WAAuB,aAAa;EAAA,SACpC,QAAA;EAAA,SACA,MAAA;EAAA,SACA,gBAAA;EAAA,SACA,eAAA;EAAA,SACA,OAAA;EA5Fc;EAAA,SA8Fd,kBAAA;EARM;;;;;EAAA,SAcN,mBAAA;AAAA;;;;;;;;AAAmB;AAoB9B;;;;;;;;;KAAY,eAAA;EAAA,SACG,IAAA;EAAA,SAAqB,QAAA,EAAU,YAAA;AAAA;EAAA,SAC/B,IAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SACA,cAAA,WAAyB,aAAa;EAAA,SACtC,OAAA;AAAA;;;;AAYkB;AAajC;;;UAfiB,2BAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA,GAAW,WAAW;AAAA;;;;;;;;;;AAkBf;iBALF,oBAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,UACA,OAAA,GAAS,2BAAA,GACR,eAAA;;;;;iBAqLa,mBAAA,CAAoB,KAAA,EAAO,cAAc,EAAE,QAAA;;;AAAgB;AAkB3E;;;;AAA8C;iBAA9B,QAAA,CAAS,KAAqB,EAAd,cAAc;;;;;;iBAwC9B,mBAAA,CAAoB,KAAA,EAAO,cAAA,GAAiB,aAAa;AAAA,iBAQzD,YAAA,CAAa,KAAqB,EAAd,cAAc;AAAA,iBA8DlC,aAAA,CAAc,KAAA,EAAO,cAAA,YAA0B,aAAa"}
1
+ {"version":3,"file":"migration-graph-D97XMWVH.d.mts","names":[],"sources":["../src/migration-graph.ts"],"mappings":";;;;iBAsCgB,gBAAA,CAAiB,QAAA,WAAmB,sBAAA,KAA2B,cAAc;;AAA7F;;;;;;;iBAuEgB,QAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,oBACU,aAAa;AA3EoE;AAuE7F;;;;;;;;;;AAIyB;AA4CzB;;;;;;;AAvH6F,iBAuH7E,sBAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,UACA,QAAA,EAAU,WAAA,oBACA,aAAA;AAAA,UAsFK,YAAA;EAAA,SACN,YAAA,WAAuB,aAAa;EAAA,SACpC,QAAA;EAAA,SACA,MAAA;EAAA,SACA,gBAAA;EAAA,SACA,eAAA;EAAA,SACA,OAAA;EA5Fc;EAAA,SA8Fd,kBAAA;EARM;;;;;EAAA,SAcN,mBAAA;AAAA;;;;;;;;AAAmB;AAoB9B;;;;;;;;;KAAY,eAAA;EAAA,SACG,IAAA;EAAA,SAAqB,QAAA,EAAU,YAAA;AAAA;EAAA,SAC/B,IAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SACA,cAAA,WAAyB,aAAa;EAAA,SACtC,OAAA;AAAA;;;;AAYkB;AAajC;;;UAfiB,2BAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA,GAAW,WAAW;AAAA;;;;;;;;;;AAkBf;iBALF,oBAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,UACA,OAAA,GAAS,2BAAA,GACR,eAAA;;;;;iBAqLa,mBAAA,CAAoB,KAAA,EAAO,cAAc,EAAE,QAAA;;;AAAgB;AAkB3E;;;;AAA8C;iBAA9B,QAAA,CAAS,KAAqB,EAAd,cAAc;;;;;;iBAwC9B,mBAAA,CAAoB,KAAA,EAAO,cAAA,GAAiB,aAAa;AAAA,iBAQzD,YAAA,CAAa,KAAqB,EAAd,cAAc;AAAA,iBA8DlC,aAAA,CAAc,KAAA,EAAO,cAAA,YAA0B,aAAa"}
@@ -1,5 +1,5 @@
1
1
  import { b as errorMissingFile, d as errorInvalidJson, m as errorInvalidRefFile } from "./errors-CoEN114u.mjs";
2
- import { c as spaceMigrationDirectory, l as spaceRefsDirectory, o as assertValidSpaceId } from "./verify-contract-spaces-DxXWxGR0.mjs";
2
+ import { c as spaceMigrationDirectory, l as spaceRefsDirectory, o as assertValidSpaceId } from "./verify-contract-spaces-DIdQLGo7.mjs";
3
3
  import { join } from "pathe";
4
4
  import { readFile } from "node:fs/promises";
5
5
  //#region src/read-contract-space-head-ref.ts
@@ -79,4 +79,4 @@ async function readContractSpaceContract(projectMigrationsDir, spaceId) {
79
79
  //#endregion
80
80
  export { readContractSpaceHeadRef as n, readContractSpaceContract as t };
81
81
 
82
- //# sourceMappingURL=read-contract-space-contract-CwwlqKl_.mjs.map
82
+ //# sourceMappingURL=read-contract-space-contract-_EvvV5Gl.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"read-contract-space-contract-CwwlqKl_.mjs","names":["hasErrnoCode"],"sources":["../src/read-contract-space-head-ref.ts","../src/read-contract-space-contract.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport type { ContractSpaceHeadRef } from '@prisma-next/framework-components/control';\nimport { join } from 'pathe';\nimport { errorInvalidJson, errorInvalidRefFile } from './errors';\nimport { assertValidSpaceId, spaceMigrationDirectory, spaceRefsDirectory } from './space-layout';\n\nexport type { ContractSpaceHeadRef };\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\n/**\n * Read the head ref (`hash` + `invariants`) for a contract space from\n * `<projectMigrationsDir>/<spaceId>/refs/head.json`.\n *\n * Returns `null` when the file does not exist (first emit). Surfaces\n * `MIGRATION.INVALID_JSON` / `MIGRATION.INVALID_REF_FILE` on a corrupt\n * `refs/head.json` so callers can distinguish \"no head ref on disk\"\n * (returns `null`) from \"head ref present but unreadable\" (throws).\n *\n * Validates the space id against `[a-z][a-z0-9_-]{0,63}` for the same\n * filesystem-safety reasons as the rest of the per-space helpers. The\n * helper is uniform across the app and extension spaces.\n */\nexport async function readContractSpaceHeadRef(\n projectMigrationsDir: string,\n spaceId: string,\n): Promise<ContractSpaceHeadRef | null> {\n assertValidSpaceId(spaceId);\n\n const filePath = join(\n spaceRefsDirectory(spaceMigrationDirectory(projectMigrationsDir, spaceId)),\n 'head.json',\n );\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));\n }\n\n if (typeof parsed !== 'object' || parsed === null) {\n throw errorInvalidRefFile(filePath, 'expected an object');\n }\n const obj = parsed as { hash?: unknown; invariants?: unknown };\n if (typeof obj.hash !== 'string') {\n throw errorInvalidRefFile(filePath, 'expected an object with a string `hash` field');\n }\n if (!Array.isArray(obj.invariants) || obj.invariants.some((value) => typeof value !== 'string')) {\n throw errorInvalidRefFile(filePath, 'expected an object with an `invariants` array of strings');\n }\n\n return { hash: obj.hash, invariants: obj.invariants as readonly string[] };\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'pathe';\nimport { errorInvalidJson, errorMissingFile } from './errors';\nimport { assertValidSpaceId } from './space-layout';\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\n/**\n * Read the on-disk contract value for a contract space\n * (`<projectMigrationsDir>/<spaceId>/contract.json`). Returns the parsed\n * JSON value as `unknown` — callers that need a typed contract validate\n * via their family's `deserializeContract` to surface schema issues.\n *\n * Companion to {@link import('./read-contract-space-head-ref').readContractSpaceHeadRef}\n * — same ENOENT-throws / corrupt-file-error semantics. Returns the\n * canonical-JSON value the framework wrote during emit, so re-running\n * this helper across machines / runs yields a byte-identical value.\n */\nexport async function readContractSpaceContract(\n projectMigrationsDir: string,\n spaceId: string,\n): Promise<unknown> {\n assertValidSpaceId(spaceId);\n\n const filePath = join(projectMigrationsDir, spaceId, 'contract.json');\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile('contract.json', join(projectMigrationsDir, spaceId));\n }\n throw error;\n }\n\n try {\n return JSON.parse(raw);\n } catch (e) {\n throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));\n }\n}\n"],"mappings":";;;;;AAQA,SAASA,eAAa,OAAgB,MAAuB;CAC3D,OAAO,iBAAiB,SAAU,MAA4B,SAAS;AACzE;;;;;;;;;;;;;;AAeA,eAAsB,yBACpB,sBACA,SACsC;CACtC,mBAAmB,OAAO;CAE1B,MAAM,WAAW,KACf,mBAAmB,wBAAwB,sBAAsB,OAAO,CAAC,GACzE,WACF;CAEA,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,SAAS,UAAU,OAAO;CACxC,SAAS,OAAO;EACd,IAAIA,eAAa,OAAO,QAAQ,GAC9B,OAAO;EAET,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,SAAS,GAAG;EACV,MAAM,iBAAiB,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;CAC7E;CAEA,IAAI,OAAO,WAAW,YAAY,WAAW,MAC3C,MAAM,oBAAoB,UAAU,oBAAoB;CAE1D,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,SAAS,UACtB,MAAM,oBAAoB,UAAU,+CAA+C;CAErF,IAAI,CAAC,MAAM,QAAQ,IAAI,UAAU,KAAK,IAAI,WAAW,MAAM,UAAU,OAAO,UAAU,QAAQ,GAC5F,MAAM,oBAAoB,UAAU,0DAA0D;CAGhG,OAAO;EAAE,MAAM,IAAI;EAAM,YAAY,IAAI;CAAgC;AAC3E;;;AC5DA,SAAS,aAAa,OAAgB,MAAuB;CAC3D,OAAO,iBAAiB,SAAU,MAA4B,SAAS;AACzE;;;;;;;;;;;;AAaA,eAAsB,0BACpB,sBACA,SACkB;CAClB,mBAAmB,OAAO;CAE1B,MAAM,WAAW,KAAK,sBAAsB,SAAS,eAAe;CAEpE,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,SAAS,UAAU,OAAO;CACxC,SAAS,OAAO;EACd,IAAI,aAAa,OAAO,QAAQ,GAC9B,MAAM,iBAAiB,iBAAiB,KAAK,sBAAsB,OAAO,CAAC;EAE7E,MAAM;CACR;CAEA,IAAI;EACF,OAAO,KAAK,MAAM,GAAG;CACvB,SAAS,GAAG;EACV,MAAM,iBAAiB,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;CAC7E;AACF"}
1
+ {"version":3,"file":"read-contract-space-contract-_EvvV5Gl.mjs","names":["hasErrnoCode"],"sources":["../src/read-contract-space-head-ref.ts","../src/read-contract-space-contract.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport type { ContractSpaceHeadRef } from '@prisma-next/framework-components/control';\nimport { join } from 'pathe';\nimport { errorInvalidJson, errorInvalidRefFile } from './errors';\nimport { assertValidSpaceId, spaceMigrationDirectory, spaceRefsDirectory } from './space-layout';\n\nexport type { ContractSpaceHeadRef };\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\n/**\n * Read the head ref (`hash` + `invariants`) for a contract space from\n * `<projectMigrationsDir>/<spaceId>/refs/head.json`.\n *\n * Returns `null` when the file does not exist (first emit). Surfaces\n * `MIGRATION.INVALID_JSON` / `MIGRATION.INVALID_REF_FILE` on a corrupt\n * `refs/head.json` so callers can distinguish \"no head ref on disk\"\n * (returns `null`) from \"head ref present but unreadable\" (throws).\n *\n * Validates the space id against `[a-z][a-z0-9_-]{0,63}` for the same\n * filesystem-safety reasons as the rest of the per-space helpers. The\n * helper is uniform across the app and extension spaces.\n */\nexport async function readContractSpaceHeadRef(\n projectMigrationsDir: string,\n spaceId: string,\n): Promise<ContractSpaceHeadRef | null> {\n assertValidSpaceId(spaceId);\n\n const filePath = join(\n spaceRefsDirectory(spaceMigrationDirectory(projectMigrationsDir, spaceId)),\n 'head.json',\n );\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));\n }\n\n if (typeof parsed !== 'object' || parsed === null) {\n throw errorInvalidRefFile(filePath, 'expected an object');\n }\n const obj = parsed as { hash?: unknown; invariants?: unknown };\n if (typeof obj.hash !== 'string') {\n throw errorInvalidRefFile(filePath, 'expected an object with a string `hash` field');\n }\n if (!Array.isArray(obj.invariants) || obj.invariants.some((value) => typeof value !== 'string')) {\n throw errorInvalidRefFile(filePath, 'expected an object with an `invariants` array of strings');\n }\n\n return { hash: obj.hash, invariants: obj.invariants as readonly string[] };\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'pathe';\nimport { errorInvalidJson, errorMissingFile } from './errors';\nimport { assertValidSpaceId } from './space-layout';\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\n/**\n * Read the on-disk contract value for a contract space\n * (`<projectMigrationsDir>/<spaceId>/contract.json`). Returns the parsed\n * JSON value as `unknown` — callers that need a typed contract validate\n * via their family's `deserializeContract` to surface schema issues.\n *\n * Companion to {@link import('./read-contract-space-head-ref').readContractSpaceHeadRef}\n * — same ENOENT-throws / corrupt-file-error semantics. Returns the\n * canonical-JSON value the framework wrote during emit, so re-running\n * this helper across machines / runs yields a byte-identical value.\n */\nexport async function readContractSpaceContract(\n projectMigrationsDir: string,\n spaceId: string,\n): Promise<unknown> {\n assertValidSpaceId(spaceId);\n\n const filePath = join(projectMigrationsDir, spaceId, 'contract.json');\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile('contract.json', join(projectMigrationsDir, spaceId));\n }\n throw error;\n }\n\n try {\n return JSON.parse(raw);\n } catch (e) {\n throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));\n }\n}\n"],"mappings":";;;;;AAQA,SAASA,eAAa,OAAgB,MAAuB;CAC3D,OAAO,iBAAiB,SAAU,MAA4B,SAAS;AACzE;;;;;;;;;;;;;;AAeA,eAAsB,yBACpB,sBACA,SACsC;CACtC,mBAAmB,OAAO;CAE1B,MAAM,WAAW,KACf,mBAAmB,wBAAwB,sBAAsB,OAAO,CAAC,GACzE,WACF;CAEA,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,SAAS,UAAU,OAAO;CACxC,SAAS,OAAO;EACd,IAAIA,eAAa,OAAO,QAAQ,GAC9B,OAAO;EAET,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,SAAS,GAAG;EACV,MAAM,iBAAiB,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;CAC7E;CAEA,IAAI,OAAO,WAAW,YAAY,WAAW,MAC3C,MAAM,oBAAoB,UAAU,oBAAoB;CAE1D,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,SAAS,UACtB,MAAM,oBAAoB,UAAU,+CAA+C;CAErF,IAAI,CAAC,MAAM,QAAQ,IAAI,UAAU,KAAK,IAAI,WAAW,MAAM,UAAU,OAAO,UAAU,QAAQ,GAC5F,MAAM,oBAAoB,UAAU,0DAA0D;CAGhG,OAAO;EAAE,MAAM,IAAI;EAAM,YAAY,IAAI;CAAgC;AAC3E;;;AC5DA,SAAS,aAAa,OAAgB,MAAuB;CAC3D,OAAO,iBAAiB,SAAU,MAA4B,SAAS;AACzE;;;;;;;;;;;;AAaA,eAAsB,0BACpB,sBACA,SACkB;CAClB,mBAAmB,OAAO;CAE1B,MAAM,WAAW,KAAK,sBAAsB,SAAS,eAAe;CAEpE,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,SAAS,UAAU,OAAO;CACxC,SAAS,OAAO;EACd,IAAI,aAAa,OAAO,QAAQ,GAC9B,MAAM,iBAAiB,iBAAiB,KAAK,sBAAsB,OAAO,CAAC;EAE7E,MAAM;CACR;CAEA,IAAI;EACF,OAAO,KAAK,MAAM,GAAG;CACvB,SAAS,GAAG;EACV,MAAM,iBAAiB,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;CAC7E;AACF"}
@@ -1,5 +1,5 @@
1
1
  import { v as errorInvalidSpaceId } from "./errors-CoEN114u.mjs";
2
- import { t as MANIFEST_FILE } from "./io-gHmDrSjQ.mjs";
2
+ import { t as MANIFEST_FILE } from "./io-Dc64lvaL.mjs";
3
3
  import { join } from "pathe";
4
4
  import { readdir, stat } from "node:fs/promises";
5
5
  import { APP_SPACE_ID } from "@prisma-next/framework-components/control";
@@ -208,4 +208,4 @@ function verifyContractSpaces(inputs) {
208
208
  //#endregion
209
209
  export { SPACE_REFS_DIRNAME as a, spaceMigrationDirectory as c, RESERVED_SPACE_SUBDIR_NAMES as i, spaceRefsDirectory as l, verifyContractSpaces as n, assertValidSpaceId as o, APP_SPACE_ID as r, isValidSpaceId as s, listContractSpaceDirectories as t };
210
210
 
211
- //# sourceMappingURL=verify-contract-spaces-DxXWxGR0.mjs.map
211
+ //# sourceMappingURL=verify-contract-spaces-DIdQLGo7.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify-contract-spaces-DxXWxGR0.mjs","names":[],"sources":["../src/space-layout.ts","../src/verify-contract-spaces.ts"],"sourcesContent":["import { APP_SPACE_ID } from '@prisma-next/framework-components/control';\nimport { join } from 'pathe';\nimport { errorInvalidSpaceId } from './errors';\n\nexport { APP_SPACE_ID };\n\n/**\n * Branded string carrying a compile-time guarantee that the value has\n * been validated by {@link assertValidSpaceId}. Downstream filesystem\n * helpers (e.g. {@link spaceMigrationDirectory}) accept this type to\n * make \"validated\" tracking visible at the type level rather than\n * relying purely on a runtime check.\n */\nexport type ValidSpaceId = string & { readonly __brand: 'ValidSpaceId' };\n\n/**\n * Pattern a contract-space identifier must match. The constraint is\n * filesystem-friendly: lowercase letters / digits / hyphen / underscore,\n * starts with a letter, max 64 characters.\n */\nconst SPACE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;\n\nexport function isValidSpaceId(spaceId: string): spaceId is ValidSpaceId {\n return SPACE_ID_PATTERN.test(spaceId);\n}\n\nexport function assertValidSpaceId(spaceId: string): asserts spaceId is ValidSpaceId {\n if (!isValidSpaceId(spaceId)) {\n throw errorInvalidSpaceId(spaceId);\n }\n}\n\n/**\n * Resolve the migrations subdirectory for a given contract space.\n *\n * Every contract space — including the app space (default `'app'`) —\n * lands under `<projectMigrationsDir>/<spaceId>/`. The space id is\n * validated against {@link SPACE_ID_PATTERN} because it becomes a\n * filesystem directory name verbatim.\n *\n * `projectMigrationsDir` is the project's top-level `migrations/`\n * directory; the helper does not assume anything about its absolute /\n * relative shape and is symmetric with `pathe.join`.\n */\nexport function spaceMigrationDirectory(projectMigrationsDir: string, spaceId: string): string {\n assertValidSpaceId(spaceId);\n return join(projectMigrationsDir, spaceId);\n}\n\n/**\n * Per-space subdirectory name reserved for the ref store\n * (`migrations/<space>/<SPACE_REFS_DIRNAME>/*.json`). Single source of\n * truth: every helper that composes a per-space refs path imports this\n * constant, and the enumerator uses it (via\n * {@link RESERVED_SPACE_SUBDIR_NAMES}) to exclude reserved names from\n * the contract-space candidate list.\n */\nexport const SPACE_REFS_DIRNAME = 'refs';\n\n/**\n * Names reserved as per-space subdirectories of `migrations/<space>/`.\n * Used by the enumerator to filter contract-space candidates so a\n * reserved name (e.g. a top-level `migrations/refs/` left in the wrong\n * place) is never enumerated as a phantom contract space. Currently\n * holds `SPACE_REFS_DIRNAME`; extend if future per-space layouts add\n * more reserved subdirectories.\n */\nexport const RESERVED_SPACE_SUBDIR_NAMES: ReadonlySet<string> = new Set([SPACE_REFS_DIRNAME]);\n\n/**\n * Resolve the per-space refs directory for `spaceMigrationsDir`\n * (typically the value returned by {@link spaceMigrationDirectory}).\n * Composes the canonical {@link SPACE_REFS_DIRNAME} so callers do not\n * hard-code the literal.\n */\nexport function spaceRefsDirectory(spaceMigrationsDir: string): string {\n return join(spaceMigrationsDir, SPACE_REFS_DIRNAME);\n}\n","import { readdir, stat } from 'node:fs/promises';\nimport { join } from 'pathe';\nimport { MANIFEST_FILE } from './io';\nimport { APP_SPACE_ID } from './space-layout';\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\n/**\n * List the per-space subdirectories under\n * `<projectRoot>/migrations/`. Returns space-id directory names (sorted\n * alphabetically) — i.e. any non-dot-prefixed subdirectory whose root\n * does **not** contain a `migration.json` manifest. The manifest is the\n * structural marker of a user-authored migration directory (see\n * `readMigrationsDir` in `./io`); directory names themselves belong to\n * the user and are not part of the contract.\n *\n * Returns `[]` if the migrations directory does not exist (greenfield\n * project).\n *\n * Reads only the user's repo. **No descriptor import.** The caller\n * (verifier) feeds the result into {@link verifyContractSpaces} alongside\n * the loaded-space set and the marker rows.\n */\nexport async function listContractSpaceDirectories(\n projectMigrationsDir: string,\n): Promise<readonly string[]> {\n let entries: { readonly name: string; readonly isDirectory: boolean }[];\n try {\n const dirents = await readdir(projectMigrationsDir, { withFileTypes: true });\n entries = dirents.map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const namedCandidates = entries\n .filter((e) => e.isDirectory)\n .map((e) => e.name)\n .filter((name) => !name.startsWith('.'))\n .sort();\n\n const manifestChecks = await Promise.all(\n namedCandidates.map(async (name) => {\n try {\n await stat(join(projectMigrationsDir, name, MANIFEST_FILE));\n return { name, isMigrationDir: true };\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return { name, isMigrationDir: false };\n }\n throw error;\n }\n }),\n );\n\n return manifestChecks.filter((c) => !c.isMigrationDir).map((c) => c.name);\n}\n\n/**\n * On-disk head value (`(hash, invariants)`) for one contract space.\n * The verifier compares this against the marker row for the same space\n * to detect drift between the user-emitted artefacts and the live DB\n * marker.\n */\nexport interface ContractSpaceHeadRecord {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\n/**\n * Marker row read from `prisma_contract.marker` (one per `space`).\n * Caller resolves these via the family runtime's marker reader before\n * invoking {@link verifyContractSpaces}.\n */\nexport interface SpaceMarkerRecord {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\nexport interface VerifyContractSpacesInputs {\n /**\n * Set of contract spaces the project declares: `'app'` plus each\n * extension space in `extensionPacks`. The caller's discovery path\n * never reads the extension descriptor module — it walks the\n * `extensionPacks` configuration in `prisma-next.config.ts` for the\n * space ids.\n */\n readonly loadedSpaces: ReadonlySet<string>;\n\n /**\n * Per-space subdirectories observed under\n * `<projectRoot>/migrations/`. Resolved via\n * {@link listContractSpaceDirectories}.\n */\n readonly spaceDirsOnDisk: readonly string[];\n\n /**\n * Head ref per space, keyed by space id. Caller reads\n * `<projectRoot>/migrations/<space-id>/contract.json` and\n * `<projectRoot>/migrations/<space-id>/refs/head.json` to construct\n * this map. Spaces with no contract-space dir on disk simply omit a\n * map entry.\n */\n readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;\n\n /**\n * Marker rows keyed by `space`. Caller reads them from the\n * `prisma_contract.marker` table.\n */\n readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;\n}\n\nexport type SpaceVerifierViolation =\n | {\n readonly kind: 'declaredButUnmigrated';\n readonly spaceId: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'orphanMarker';\n readonly spaceId: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'orphanSpaceDir';\n readonly spaceId: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'hashMismatch';\n readonly spaceId: string;\n readonly priorHeadHash: string;\n readonly markerHash: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'invariantsMismatch';\n readonly spaceId: string;\n readonly onDiskInvariants: readonly string[];\n readonly markerInvariants: readonly string[];\n readonly remediation: string;\n };\n\nexport type VerifyContractSpacesResult =\n | { readonly ok: true }\n | { readonly ok: false; readonly violations: readonly SpaceVerifierViolation[] };\n\n/**\n * Pure structural verifier for the per-space mechanism. Aggregates the\n * three orphan / missing checks plus per-space hash and invariant\n * comparison.\n *\n * Algorithm:\n *\n * - For every extension space declared in `loadedSpaces` (`'app'`\n * excluded — the per-space verifier is scoped to extension members;\n * the app is verified through the aggregate path):\n * - If no contract-space dir on disk → `declaredButUnmigrated`.\n * - Else if `markerRowsBySpace` lacks an entry → no violation here;\n * the live-DB compare done outside this helper is where the\n * absence shows up.\n * - Else compare marker hash / invariants vs. on-disk head hash /\n * invariants → `hashMismatch` / `invariantsMismatch` on drift.\n * - For every contract-space dir on disk that is not in `loadedSpaces` →\n * `orphanSpaceDir`.\n * - For every marker row whose `space` is not in `loadedSpaces` →\n * `orphanMarker`. The app-space marker is always loaded (`'app'` is\n * in `loadedSpaces` by definition).\n *\n * Output is deterministic: violations are sorted first by `kind`\n * (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →\n * `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers\n * passing equivalent inputs see byte-identical violation lists.\n *\n * Synchronous, pure, no I/O. **Does not import the extension descriptor**\n * (the inputs are pre-resolved by the caller); the verifier reads only\n * the user repo, not `node_modules`.\n */\nexport function verifyContractSpaces(\n inputs: VerifyContractSpacesInputs,\n): VerifyContractSpacesResult {\n const violations: SpaceVerifierViolation[] = [];\n\n for (const spaceId of [...inputs.loadedSpaces].sort()) {\n if (spaceId === APP_SPACE_ID) continue;\n\n if (!inputs.spaceDirsOnDisk.includes(spaceId)) {\n violations.push({\n kind: 'declaredButUnmigrated',\n spaceId,\n remediation: `Extension '${spaceId}' is declared in extensionPacks but has not been emitted; run \\`prisma-next migrate\\`.`,\n });\n continue;\n }\n\n const head = inputs.headRefsBySpace.get(spaceId);\n const marker = inputs.markerRowsBySpace.get(spaceId);\n if (!head || !marker) {\n continue;\n }\n\n if (head.hash !== marker.hash) {\n violations.push({\n kind: 'hashMismatch',\n spaceId,\n priorHeadHash: head.hash,\n markerHash: marker.hash,\n remediation: `Marker row for space '${spaceId}' is keyed at ${marker.hash}, but the on-disk ${join('migrations', spaceId, 'contract.json')} resolves to ${head.hash}. Run \\`prisma-next db update\\` to advance the database, or \\`prisma-next migrate\\` if the descriptor was bumped without re-emitting.`,\n });\n continue;\n }\n\n const onDiskInvariants = [...head.invariants].sort();\n const markerInvariants = new Set(marker.invariants);\n const missing = onDiskInvariants.filter((id) => !markerInvariants.has(id));\n if (missing.length > 0) {\n violations.push({\n kind: 'invariantsMismatch',\n spaceId,\n onDiskInvariants,\n markerInvariants: [...marker.invariants].sort(),\n remediation: `Marker row for space '${spaceId}' is missing invariants [${missing.map((s) => JSON.stringify(s)).join(', ')}]. Run \\`prisma-next db update\\` to apply the corresponding data-transform migrations.`,\n });\n }\n }\n\n for (const dir of [...inputs.spaceDirsOnDisk].sort()) {\n if (!inputs.loadedSpaces.has(dir)) {\n violations.push({\n kind: 'orphanSpaceDir',\n spaceId: dir,\n remediation: `Orphan contract-space directory \\`${join('migrations', dir)}/\\` for an extension not in extensionPacks; remove the directory or re-add the extension.`,\n });\n }\n }\n\n for (const space of [...inputs.markerRowsBySpace.keys()].sort()) {\n if (!inputs.loadedSpaces.has(space)) {\n violations.push({\n kind: 'orphanMarker',\n spaceId: space,\n remediation: `Orphan marker row for space '${space}' (no longer in extensionPacks); remediation: manually delete the row from \\`prisma_contract.marker\\`.`,\n });\n }\n }\n\n if (violations.length === 0) {\n return { ok: true };\n }\n\n const kindOrder: Record<SpaceVerifierViolation['kind'], number> = {\n declaredButUnmigrated: 0,\n orphanMarker: 1,\n orphanSpaceDir: 2,\n hashMismatch: 3,\n invariantsMismatch: 4,\n };\n\n violations.sort((a, b) => {\n const k = kindOrder[a.kind] - kindOrder[b.kind];\n if (k !== 0) return k;\n if (a.spaceId < b.spaceId) return -1;\n if (a.spaceId > b.spaceId) return 1;\n return 0;\n });\n\n return { ok: false, violations };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"verify-contract-spaces-DIdQLGo7.mjs","names":[],"sources":["../src/space-layout.ts","../src/verify-contract-spaces.ts"],"sourcesContent":["import { APP_SPACE_ID } from '@prisma-next/framework-components/control';\nimport { join } from 'pathe';\nimport { errorInvalidSpaceId } from './errors';\n\nexport { APP_SPACE_ID };\n\n/**\n * Branded string carrying a compile-time guarantee that the value has\n * been validated by {@link assertValidSpaceId}. Downstream filesystem\n * helpers (e.g. {@link spaceMigrationDirectory}) accept this type to\n * make \"validated\" tracking visible at the type level rather than\n * relying purely on a runtime check.\n */\nexport type ValidSpaceId = string & { readonly __brand: 'ValidSpaceId' };\n\n/**\n * Pattern a contract-space identifier must match. The constraint is\n * filesystem-friendly: lowercase letters / digits / hyphen / underscore,\n * starts with a letter, max 64 characters.\n */\nconst SPACE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;\n\nexport function isValidSpaceId(spaceId: string): spaceId is ValidSpaceId {\n return SPACE_ID_PATTERN.test(spaceId);\n}\n\nexport function assertValidSpaceId(spaceId: string): asserts spaceId is ValidSpaceId {\n if (!isValidSpaceId(spaceId)) {\n throw errorInvalidSpaceId(spaceId);\n }\n}\n\n/**\n * Resolve the migrations subdirectory for a given contract space.\n *\n * Every contract space — including the app space (default `'app'`) —\n * lands under `<projectMigrationsDir>/<spaceId>/`. The space id is\n * validated against {@link SPACE_ID_PATTERN} because it becomes a\n * filesystem directory name verbatim.\n *\n * `projectMigrationsDir` is the project's top-level `migrations/`\n * directory; the helper does not assume anything about its absolute /\n * relative shape and is symmetric with `pathe.join`.\n */\nexport function spaceMigrationDirectory(projectMigrationsDir: string, spaceId: string): string {\n assertValidSpaceId(spaceId);\n return join(projectMigrationsDir, spaceId);\n}\n\n/**\n * Per-space subdirectory name reserved for the ref store\n * (`migrations/<space>/<SPACE_REFS_DIRNAME>/*.json`). Single source of\n * truth: every helper that composes a per-space refs path imports this\n * constant, and the enumerator uses it (via\n * {@link RESERVED_SPACE_SUBDIR_NAMES}) to exclude reserved names from\n * the contract-space candidate list.\n */\nexport const SPACE_REFS_DIRNAME = 'refs';\n\n/**\n * Names reserved as per-space subdirectories of `migrations/<space>/`.\n * Used by the enumerator to filter contract-space candidates so a\n * reserved name (e.g. a top-level `migrations/refs/` left in the wrong\n * place) is never enumerated as a phantom contract space. Currently\n * holds `SPACE_REFS_DIRNAME`; extend if future per-space layouts add\n * more reserved subdirectories.\n */\nexport const RESERVED_SPACE_SUBDIR_NAMES: ReadonlySet<string> = new Set([SPACE_REFS_DIRNAME]);\n\n/**\n * Resolve the per-space refs directory for `spaceMigrationsDir`\n * (typically the value returned by {@link spaceMigrationDirectory}).\n * Composes the canonical {@link SPACE_REFS_DIRNAME} so callers do not\n * hard-code the literal.\n */\nexport function spaceRefsDirectory(spaceMigrationsDir: string): string {\n return join(spaceMigrationsDir, SPACE_REFS_DIRNAME);\n}\n","import { readdir, stat } from 'node:fs/promises';\nimport { join } from 'pathe';\nimport { MANIFEST_FILE } from './io';\nimport { APP_SPACE_ID } from './space-layout';\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\n/**\n * List the per-space subdirectories under\n * `<projectRoot>/migrations/`. Returns space-id directory names (sorted\n * alphabetically) — i.e. any non-dot-prefixed subdirectory whose root\n * does **not** contain a `migration.json` manifest. The manifest is the\n * structural marker of a user-authored migration directory (see\n * `readMigrationsDir` in `./io`); directory names themselves belong to\n * the user and are not part of the contract.\n *\n * Returns `[]` if the migrations directory does not exist (greenfield\n * project).\n *\n * Reads only the user's repo. **No descriptor import.** The caller\n * (verifier) feeds the result into {@link verifyContractSpaces} alongside\n * the loaded-space set and the marker rows.\n */\nexport async function listContractSpaceDirectories(\n projectMigrationsDir: string,\n): Promise<readonly string[]> {\n let entries: { readonly name: string; readonly isDirectory: boolean }[];\n try {\n const dirents = await readdir(projectMigrationsDir, { withFileTypes: true });\n entries = dirents.map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const namedCandidates = entries\n .filter((e) => e.isDirectory)\n .map((e) => e.name)\n .filter((name) => !name.startsWith('.'))\n .sort();\n\n const manifestChecks = await Promise.all(\n namedCandidates.map(async (name) => {\n try {\n await stat(join(projectMigrationsDir, name, MANIFEST_FILE));\n return { name, isMigrationDir: true };\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return { name, isMigrationDir: false };\n }\n throw error;\n }\n }),\n );\n\n return manifestChecks.filter((c) => !c.isMigrationDir).map((c) => c.name);\n}\n\n/**\n * On-disk head value (`(hash, invariants)`) for one contract space.\n * The verifier compares this against the marker row for the same space\n * to detect drift between the user-emitted artefacts and the live DB\n * marker.\n */\nexport interface ContractSpaceHeadRecord {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\n/**\n * Marker row read from `prisma_contract.marker` (one per `space`).\n * Caller resolves these via the family runtime's marker reader before\n * invoking {@link verifyContractSpaces}.\n */\nexport interface SpaceMarkerRecord {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\nexport interface VerifyContractSpacesInputs {\n /**\n * Set of contract spaces the project declares: `'app'` plus each\n * extension space in `extensionPacks`. The caller's discovery path\n * never reads the extension descriptor module — it walks the\n * `extensionPacks` configuration in `prisma-next.config.ts` for the\n * space ids.\n */\n readonly loadedSpaces: ReadonlySet<string>;\n\n /**\n * Per-space subdirectories observed under\n * `<projectRoot>/migrations/`. Resolved via\n * {@link listContractSpaceDirectories}.\n */\n readonly spaceDirsOnDisk: readonly string[];\n\n /**\n * Head ref per space, keyed by space id. Caller reads\n * `<projectRoot>/migrations/<space-id>/contract.json` and\n * `<projectRoot>/migrations/<space-id>/refs/head.json` to construct\n * this map. Spaces with no contract-space dir on disk simply omit a\n * map entry.\n */\n readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;\n\n /**\n * Marker rows keyed by `space`. Caller reads them from the\n * `prisma_contract.marker` table.\n */\n readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;\n}\n\nexport type SpaceVerifierViolation =\n | {\n readonly kind: 'declaredButUnmigrated';\n readonly spaceId: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'orphanMarker';\n readonly spaceId: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'orphanSpaceDir';\n readonly spaceId: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'hashMismatch';\n readonly spaceId: string;\n readonly priorHeadHash: string;\n readonly markerHash: string;\n readonly remediation: string;\n }\n | {\n readonly kind: 'invariantsMismatch';\n readonly spaceId: string;\n readonly onDiskInvariants: readonly string[];\n readonly markerInvariants: readonly string[];\n readonly remediation: string;\n };\n\nexport type VerifyContractSpacesResult =\n | { readonly ok: true }\n | { readonly ok: false; readonly violations: readonly SpaceVerifierViolation[] };\n\n/**\n * Pure structural verifier for the per-space mechanism. Aggregates the\n * three orphan / missing checks plus per-space hash and invariant\n * comparison.\n *\n * Algorithm:\n *\n * - For every extension space declared in `loadedSpaces` (`'app'`\n * excluded — the per-space verifier is scoped to extension members;\n * the app is verified through the aggregate path):\n * - If no contract-space dir on disk → `declaredButUnmigrated`.\n * - Else if `markerRowsBySpace` lacks an entry → no violation here;\n * the live-DB compare done outside this helper is where the\n * absence shows up.\n * - Else compare marker hash / invariants vs. on-disk head hash /\n * invariants → `hashMismatch` / `invariantsMismatch` on drift.\n * - For every contract-space dir on disk that is not in `loadedSpaces` →\n * `orphanSpaceDir`.\n * - For every marker row whose `space` is not in `loadedSpaces` →\n * `orphanMarker`. The app-space marker is always loaded (`'app'` is\n * in `loadedSpaces` by definition).\n *\n * Output is deterministic: violations are sorted first by `kind`\n * (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →\n * `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers\n * passing equivalent inputs see byte-identical violation lists.\n *\n * Synchronous, pure, no I/O. **Does not import the extension descriptor**\n * (the inputs are pre-resolved by the caller); the verifier reads only\n * the user repo, not `node_modules`.\n */\nexport function verifyContractSpaces(\n inputs: VerifyContractSpacesInputs,\n): VerifyContractSpacesResult {\n const violations: SpaceVerifierViolation[] = [];\n\n for (const spaceId of [...inputs.loadedSpaces].sort()) {\n if (spaceId === APP_SPACE_ID) continue;\n\n if (!inputs.spaceDirsOnDisk.includes(spaceId)) {\n violations.push({\n kind: 'declaredButUnmigrated',\n spaceId,\n remediation: `Extension '${spaceId}' is declared in extensionPacks but has not been emitted; run \\`prisma-next migrate\\`.`,\n });\n continue;\n }\n\n const head = inputs.headRefsBySpace.get(spaceId);\n const marker = inputs.markerRowsBySpace.get(spaceId);\n if (!head || !marker) {\n continue;\n }\n\n if (head.hash !== marker.hash) {\n violations.push({\n kind: 'hashMismatch',\n spaceId,\n priorHeadHash: head.hash,\n markerHash: marker.hash,\n remediation: `Marker row for space '${spaceId}' is keyed at ${marker.hash}, but the on-disk ${join('migrations', spaceId, 'contract.json')} resolves to ${head.hash}. Run \\`prisma-next db update\\` to advance the database, or \\`prisma-next migrate\\` if the descriptor was bumped without re-emitting.`,\n });\n continue;\n }\n\n const onDiskInvariants = [...head.invariants].sort();\n const markerInvariants = new Set(marker.invariants);\n const missing = onDiskInvariants.filter((id) => !markerInvariants.has(id));\n if (missing.length > 0) {\n violations.push({\n kind: 'invariantsMismatch',\n spaceId,\n onDiskInvariants,\n markerInvariants: [...marker.invariants].sort(),\n remediation: `Marker row for space '${spaceId}' is missing invariants [${missing.map((s) => JSON.stringify(s)).join(', ')}]. Run \\`prisma-next db update\\` to apply the corresponding data-transform migrations.`,\n });\n }\n }\n\n for (const dir of [...inputs.spaceDirsOnDisk].sort()) {\n if (!inputs.loadedSpaces.has(dir)) {\n violations.push({\n kind: 'orphanSpaceDir',\n spaceId: dir,\n remediation: `Orphan contract-space directory \\`${join('migrations', dir)}/\\` for an extension not in extensionPacks; remove the directory or re-add the extension.`,\n });\n }\n }\n\n for (const space of [...inputs.markerRowsBySpace.keys()].sort()) {\n if (!inputs.loadedSpaces.has(space)) {\n violations.push({\n kind: 'orphanMarker',\n spaceId: space,\n remediation: `Orphan marker row for space '${space}' (no longer in extensionPacks); remediation: manually delete the row from \\`prisma_contract.marker\\`.`,\n });\n }\n }\n\n if (violations.length === 0) {\n return { ok: true };\n }\n\n const kindOrder: Record<SpaceVerifierViolation['kind'], number> = {\n declaredButUnmigrated: 0,\n orphanMarker: 1,\n orphanSpaceDir: 2,\n hashMismatch: 3,\n invariantsMismatch: 4,\n };\n\n violations.sort((a, b) => {\n const k = kindOrder[a.kind] - kindOrder[b.kind];\n if (k !== 0) return k;\n if (a.spaceId < b.spaceId) return -1;\n if (a.spaceId > b.spaceId) return 1;\n return 0;\n });\n\n return { ok: false, violations };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.11.0-dev.33",
3
+ "version": "0.11.0-dev.35",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
8
8
  "dependencies": {
9
- "@prisma-next/contract": "0.11.0-dev.33",
10
- "@prisma-next/framework-components": "0.11.0-dev.33",
11
- "@prisma-next/utils": "0.11.0-dev.33",
9
+ "@prisma-next/contract": "0.11.0-dev.35",
10
+ "@prisma-next/framework-components": "0.11.0-dev.35",
11
+ "@prisma-next/utils": "0.11.0-dev.35",
12
12
  "arktype": "^2.2.0",
13
13
  "pathe": "^2.0.3",
14
14
  "prettier": "^3.8.3"
15
15
  },
16
16
  "devDependencies": {
17
- "@prisma-next/tsconfig": "0.11.0-dev.33",
18
- "@prisma-next/tsdown": "0.11.0-dev.33",
17
+ "@prisma-next/tsconfig": "0.11.0-dev.35",
18
+ "@prisma-next/tsdown": "0.11.0-dev.35",
19
19
  "tsdown": "0.22.0",
20
20
  "typescript": "5.9.3",
21
21
  "vitest": "4.1.6"
package/src/errors.ts CHANGED
@@ -320,8 +320,8 @@ export function errorProvidedInvariantsMismatch(
320
320
  /**
321
321
  * Wire-shape edge surfaced through the JSON envelope's
322
322
  * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —
323
- * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but
324
- * is intentionally dropped here so the envelope stays stable across
323
+ * authoring metadata (`createdAt`) lives on `MigrationEdge` but is
324
+ * intentionally dropped here so the envelope stays stable across
325
325
  * graph-internal refactors.
326
326
  *
327
327
  * Stability: any field added here is part of the public CLI JSON contract.
@@ -1 +1 @@
1
- export type { MigrationHints, MigrationMetadata } from '../metadata';
1
+ export type { MigrationMetadata } from '../metadata';
package/src/graph.ts CHANGED
@@ -8,7 +8,6 @@ export interface MigrationEdge {
8
8
  readonly migrationHash: string;
9
9
  readonly dirName: string;
10
10
  readonly createdAt: string;
11
- readonly labels: readonly string[];
12
11
  /**
13
12
  * Sorted, deduplicated list of `invariantId`s this edge provides.
14
13
  * An empty array means the migration declares no routing-visible
package/src/hash.ts CHANGED
@@ -15,13 +15,12 @@ function sha256Hex(input: string): string {
15
15
  }
16
16
 
17
17
  /**
18
- * Content-addressed migration hash over (metadata envelope sans hints,
19
- * ops). See ADR 199 — Storage-only migration identity for the
20
- * rationale: the storage-hash bookends (`from`, `to`) inside the
21
- * envelope anchor the contract identity by hash, and planner hints are
22
- * advisory and must not affect identity. The full contract IRs are not
23
- * part of the manifest they live in sibling `*-contract.json` files
24
- * authored alongside the migration, never inlined here.
18
+ * Content-addressed migration hash over (metadata envelope, ops). See
19
+ * ADR 199 — Storage-only migration identity for the rationale: the
20
+ * storage-hash bookends (`from`, `to`) inside the envelope anchor the
21
+ * contract identity by hash. The full contract IRs are not part of the
22
+ * manifest they live in sibling `*-contract.json` files authored
23
+ * alongside the migration, never inlined here.
25
24
  *
26
25
  * The integrity check is purely structural, not semantic. The function
27
26
  * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
@@ -46,7 +45,7 @@ export function computeMigrationHash(
46
45
  metadata: Omit<MigrationMetadata, 'migrationHash'> & { readonly migrationHash?: string },
47
46
  ops: MigrationOps,
48
47
  ): string {
49
- const { migrationHash: _migrationHash, hints: _hints, ...strippedMeta } = metadata;
48
+ const { migrationHash: _migrationHash, ...strippedMeta } = metadata;
50
49
 
51
50
  const canonicalMetadata = canonicalizeJson(strippedMeta);
52
51
  const canonicalOps = canonicalizeJson(ops);
package/src/io.ts CHANGED
@@ -28,19 +28,11 @@ function hasErrnoCode(error: unknown, code: string): boolean {
28
28
  return error instanceof Error && (error as { code?: string }).code === code;
29
29
  }
30
30
 
31
- const MigrationHintsSchema = type({
32
- used: 'string[]',
33
- applied: 'string[]',
34
- plannerVersion: 'string',
35
- });
36
-
37
31
  const MigrationMetadataSchema = type({
38
32
  '+': 'reject',
39
33
  from: 'string > 0 | null',
40
34
  to: 'string',
41
35
  migrationHash: 'string',
42
- hints: MigrationHintsSchema,
43
- labels: 'string[]',
44
36
  providedInvariants: 'string[]',
45
37
  createdAt: 'string',
46
38
  });
package/src/metadata.ts CHANGED
@@ -1 +1 @@
1
- export type { MigrationHints, MigrationMetadata } from '@prisma-next/framework-components/control';
1
+ export type { MigrationMetadata } from '@prisma-next/framework-components/control';
@@ -9,14 +9,13 @@ import { type } from 'arktype';
9
9
  import { errorInvalidOperationEntry } from './errors';
10
10
  import { computeMigrationHash } from './hash';
11
11
  import { deriveProvidedInvariants } from './invariants';
12
- import type { MigrationHints, MigrationMetadata } from './metadata';
12
+ import type { MigrationMetadata } from './metadata';
13
13
  import { MigrationOpSchema } from './op-schema';
14
14
  import type { MigrationOps } from './package';
15
15
 
16
16
  export interface MigrationMeta {
17
17
  readonly from: string | null;
18
18
  readonly to: string;
19
- readonly labels?: readonly string[];
20
19
  }
21
20
 
22
21
  // `from` rejects empty strings to mirror `MigrationMetadataSchema` in
@@ -27,7 +26,6 @@ export interface MigrationMeta {
27
26
  const MigrationMetaSchema = type({
28
27
  from: 'string > 0 | null',
29
28
  to: 'string',
30
- 'labels?': type('string').array(),
31
29
  });
32
30
 
33
31
  /**
@@ -127,12 +125,11 @@ export interface MigrationArtifacts {
127
125
  * operations list, and the previously-scaffolded metadata (if any).
128
126
  *
129
127
  * When a `migration.json` already exists for this package (the common
130
- * case: it was scaffolded by `migration plan`), preserve the contract
131
- * bookends, hints, labels, and `createdAt` set there those fields are
132
- * owned by the CLI scaffolder, not the authored class. Only the
133
- * `describe()`-derived fields (`from`, `to`) and the operations
134
- * change as the author iterates. When no metadata exists yet (a bare
135
- * `migration.ts` run from scratch), synthesize a minimal but
128
+ * case: it was scaffolded by `migration plan`), preserve `createdAt`
129
+ * set there that field is owned by the CLI scaffolder, not the authored
130
+ * class. Only the `describe()`-derived fields (`from`, `to`) and the
131
+ * operations change as the author iterates. When no metadata exists yet
132
+ * (a bare `migration.ts` run from scratch), synthesize a minimal but
136
133
  * schema-conformant record so the resulting package can still be read,
137
134
  * verified, and applied.
138
135
  *
@@ -147,39 +144,22 @@ function buildAttestedMetadata(
147
144
  const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
148
145
  from: meta.from,
149
146
  to: meta.to,
150
- labels: meta.labels ?? existing?.labels ?? [],
151
147
  providedInvariants: deriveProvidedInvariants(ops),
152
148
  createdAt: existing?.createdAt ?? new Date().toISOString(),
153
- hints: normalizeHints(existing?.hints),
154
149
  };
155
150
 
156
151
  const migrationHash = computeMigrationHash(baseMetadata, ops);
157
152
  return { ...baseMetadata, migrationHash };
158
153
  }
159
154
 
160
- /**
161
- * Project `existing.hints` down to the known `MigrationHints` shape, dropping
162
- * any legacy keys that may linger in metadata scaffolded by older CLI
163
- * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
164
- * spreading keeps refreshed `migration.json` files schema-clean regardless
165
- * of what was on disk before.
166
- */
167
- function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
168
- return {
169
- used: existing?.used ?? [],
170
- applied: existing?.applied ?? [],
171
- plannerVersion: existing?.plannerVersion ?? '2.0.0',
172
- };
173
- }
174
-
175
155
  /**
176
156
  * Pure conversion from a `Migration` instance (plus the previously
177
157
  * scaffolded metadata, when one exists on disk) to the in-memory
178
158
  * artifacts that downstream tooling persists. Owns metadata validation,
179
- * metadata synthesis/preservation, hint normalization, and the
180
- * content-addressed `migrationHash` computation, but performs no file I/O
181
- * — callers handle reads (to source `existing`) and writes (to persist
182
- * `opsJson` / `metadataJson`).
159
+ * metadata synthesis/preservation, and the content-addressed
160
+ * `migrationHash` computation, but performs no file I/O — callers handle
161
+ * reads (to source `existing`) and writes (to persist `opsJson` /
162
+ * `metadataJson`).
183
163
  */
184
164
  export function buildMigrationArtifacts(
185
165
  instance: Migration,
@@ -66,7 +66,6 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
66
66
  migrationHash: pkg.metadata.migrationHash,
67
67
  dirName: pkg.dirName,
68
68
  createdAt: pkg.metadata.createdAt,
69
- labels: pkg.metadata.labels,
70
69
  invariants: pkg.metadata.providedInvariants,
71
70
  };
72
71
 
@@ -85,23 +84,10 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
85
84
  // ---------------------------------------------------------------------------
86
85
  // Deterministic tie-breaking for BFS neighbour order.
87
86
  // Used by path-finders only; not a general-purpose utility.
88
- // Ordering: label priority → createdAt → to → migrationHash.
87
+ // Ordering: createdAt → to → migrationHash.
89
88
  // ---------------------------------------------------------------------------
90
89
 
91
- const LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };
92
-
93
- function labelPriority(labels: readonly string[]): number {
94
- let best = 3;
95
- for (const l of labels) {
96
- const p = LABEL_PRIORITY[l];
97
- if (p !== undefined && p < best) best = p;
98
- }
99
- return best;
100
- }
101
-
102
90
  function compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {
103
- const lp = labelPriority(a.labels) - labelPriority(b.labels);
104
- if (lp !== 0) return lp;
105
91
  const ca = a.createdAt.localeCompare(b.createdAt);
106
92
  if (ca !== 0) return ca;
107
93
  const tc = a.to.localeCompare(b.to);
@@ -119,7 +105,7 @@ function sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdg
119
105
  * exists. Returns an empty array when `fromHash === toHash` (no-op).
120
106
  *
121
107
  * Neighbor ordering is deterministic via the tie-break sort key:
122
- * label priority → createdAt → to → migrationHash.
108
+ * createdAt → to → migrationHash.
123
109
  */
124
110
  export function findPath(
125
111
  graph: MigrationGraph,
@@ -165,8 +151,8 @@ export function findPath(
165
151
  * control chars at authoring time).
166
152
  *
167
153
  * Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
168
- * invariant come first, with `labelPriority → createdAt → to → migrationHash`
169
- * as the secondary key. The heuristic steers BFS toward the satisfying path;
154
+ * invariant come first, with `createdAt → to → migrationHash` as the
155
+ * secondary key. The heuristic steers BFS toward the satisfying path;
170
156
  * correctness (shortest, deterministic) does not depend on it.
171
157
  */
172
158
  export function findPathWithInvariants(