🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@prisma-next/migration-tools

Package Overview
Dependencies
Maintainers
4
Versions
569
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@prisma-next/migration-tools - npm Package Compare versions

Comparing version
0.14.0
to
0.15.0-dev.1
+321
dist/errors-DIS_dcFJ.mjs
import { ifDefined } from "@prisma-next/utils/defined";
import { dirname, relative } from "pathe";
//#region src/errors.ts
/**
* Build the canonical "re-emit this package" remediation hint.
*
* Every on-disk migration package ships its own `migration.ts` author-time
* file. Running it regenerates `migration.json` and `ops.json` with the
* correct hash + metadata, so it is the right primitive whenever a single
* package's on-disk artifacts are missing, malformed, or otherwise corrupt.
* Pointing users at `migration plan` would emit a *new* package rather than
* heal the broken one.
*/
function reemitHint(dir, fallback) {
const reemit = `Re-emit the package by running \`node "${relative(process.cwd(), dir)}/migration.ts"\``;
return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;
}
/**
* Structured error for migration tooling operations.
*
* Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
* the MIGRATION namespace. These are tooling-time errors (file I/O, hash
* verification, migration history reconstruction), distinct from the runtime
* MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,
* etc.).
*
* Fields:
* - code: Stable machine-readable code (MIGRATION.SUBCODE)
* - category: Always 'MIGRATION'
* - why: Explains the cause in plain language
* - fix: Actionable remediation step
* - details: Machine-readable structured data for agents
*/
var MigrationToolsError = class extends Error {
code;
category = "MIGRATION";
why;
fix;
details;
constructor(code, summary, options) {
super(summary);
this.name = "MigrationToolsError";
this.code = code;
this.why = options.why;
this.fix = options.fix;
this.details = options.details;
}
static is(error) {
if (!(error instanceof Error)) return false;
const candidate = error;
return candidate.name === "MigrationToolsError" && typeof candidate.code === "string";
}
};
function errorDirectoryExists(dir) {
return new MigrationToolsError("MIGRATION.DIR_EXISTS", "Migration directory already exists", {
why: `The directory "${dir}" already exists. Each migration must have a unique directory.`,
fix: "Use --name to pick a different name, or delete the existing directory and re-run.",
details: { dir }
});
}
function errorMissingFile(file, dir) {
return new MigrationToolsError("MIGRATION.FILE_MISSING", `Missing ${file}`, {
why: `Expected "${file}" in "${dir}" but the file does not exist.`,
fix: reemitHint(dir, "or delete the directory if the migration is unwanted and the source TypeScript is gone."),
details: {
file,
dir
}
});
}
function errorInvalidJson(filePath, parseError) {
return new MigrationToolsError("MIGRATION.INVALID_JSON", "Invalid JSON in migration file", {
why: `Failed to parse "${filePath}": ${parseError}`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
parseError
}
});
}
function errorInvalidManifest(filePath, reason) {
return new MigrationToolsError("MIGRATION.INVALID_MANIFEST", "Invalid migration manifest", {
why: `Migration manifest at "${filePath}" is invalid: ${reason}`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
reason
}
});
}
function errorInvalidOperationEntry(index, reason) {
return new MigrationToolsError("MIGRATION.INVALID_OPERATION_ENTRY", "Migration operation entry is malformed", {
why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,
fix: "Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').",
details: {
index,
reason
}
});
}
function errorInvalidSlug(slug) {
return new MigrationToolsError("MIGRATION.INVALID_NAME", "Invalid migration name", {
why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
fix: "Provide a name with at least one alphanumeric character, e.g. --name add_users.",
details: { slug }
});
}
function errorInvalidDestName(destName) {
return new MigrationToolsError("MIGRATION.INVALID_DEST_NAME", "Invalid copy destination name", {
why: `The destination name "${destName}" must be a single path segment (no ".." or directory separators).`,
fix: "Use a simple file name such as \"contract.json\" for each destination in the copy list.",
details: { destName }
});
}
function errorInvalidSpaceId(spaceId) {
return new MigrationToolsError("MIGRATION.INVALID_SPACE_ID", "Invalid contract space identifier", {
why: `The space id "${spaceId}" does not match the required pattern /^[a-z][a-z0-9_-]{0,63}$/. Space ids are used as filesystem directory names under \`migrations/\`, so the pattern is conservative on purpose.`,
fix: "Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.",
details: { spaceId }
});
}
function errorDescriptorHeadHashMismatch(args) {
const { extensionId, recomputedHash, headRefHash } = args;
return new MigrationToolsError("MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH", "Extension descriptor's headRef.hash does not match its contractJson", {
why: `Extension "${extensionId}" publishes a \`contractSpace\` whose \`headRef.hash\` (${headRefHash}) does not match the canonical hash recomputed from \`contractSpace.contractJson\` (${recomputedHash}). This means the extension descriptor was published with stale \`headRef.hash\` — typically because the contract was bumped without rerunning the extension's emit pipeline.`,
fix: "Re-run the extension authoring pipeline so `contractJson.storage.storageHash` and `headRef.hash` agree, then republish the extension. If you are the extension author and you intentionally bumped `contractJson`, recompute and update `headRef.hash` (and refresh any on-disk migration metadata that derives from it).",
details: {
extensionId,
recomputedHash,
headRefHash
}
});
}
function errorDuplicateSpaceId(spaceId) {
return new MigrationToolsError("MIGRATION.DUPLICATE_SPACE_ID", "Duplicate contract space identifier", {
why: `The space id "${spaceId}" appears more than once in the per-space planner input. Each space id must be unique across the inputs (the per-space planner emits one output entry per id).`,
fix: "Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.",
details: { spaceId }
});
}
function errorAmbiguousTarget(branchTips, context) {
const divergenceInfo = context ? `\nDivergence point: ${context.divergencePoint}\nBranches:\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(" → ") || "direct"})`).join("\n")}` : "";
return new MigrationToolsError("MIGRATION.AMBIGUOUS_TARGET", "Ambiguous migration target", {
why: `The migration history has diverged into multiple branches: ${branchTips.join(", ")}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,
fix: "Use `ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.",
details: {
branchTips,
...context ? {
divergencePoint: context.divergencePoint,
branches: context.branches
} : {}
}
});
}
function errorNoInitialMigration(nodes) {
return new MigrationToolsError("MIGRATION.NO_INITIAL_MIGRATION", "No initial migration found", {
why: `No migration starts from the empty contract state (known hashes: ${nodes.join(", ")}). At least one migration must originate from the empty state.`,
fix: "Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.",
details: { nodes }
});
}
function errorInvalidRefFile(filePath, reason) {
return new MigrationToolsError("MIGRATION.INVALID_REF_FILE", "Invalid ref file", {
why: `Ref file at "${filePath}" is invalid: ${reason}`,
fix: "Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.",
details: {
path: filePath,
reason
}
});
}
function errorInvalidRefName(refName) {
return new MigrationToolsError("MIGRATION.INVALID_REF_NAME", "Invalid ref name", {
why: `Ref name "${refName}" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no "." or ".." segments).`,
fix: `Use a valid ref name (e.g., "staging", "envs/production").`,
details: { refName }
});
}
function errorNoTarget(reachableHashes) {
return new MigrationToolsError("MIGRATION.NO_TARGET", "No migration target could be resolved", {
why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(", ")}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,
fix: "Use --from <hash> to specify the planning origin explicitly.",
details: { reachableHashes }
});
}
function errorInvalidRefValue(value) {
return new MigrationToolsError("MIGRATION.INVALID_REF_VALUE", "Invalid ref value", {
why: `Ref value "${value}" is not a valid contract hash. Values must be in the format "sha256:<64 hex chars>" or "sha256:empty".`,
fix: "Use a valid storage hash from `prisma-next contract emit` output or an existing migration.",
details: { value }
});
}
function errorInvalidInvariantId(invariantId) {
return new MigrationToolsError("MIGRATION.INVALID_INVARIANT_ID", "Invalid invariantId", {
why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,
fix: "Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".",
details: { invariantId }
});
}
function errorDuplicateInvariantInEdge(invariantId) {
return new MigrationToolsError("MIGRATION.DUPLICATE_INVARIANT_IN_EDGE", "Duplicate invariantId on a single migration", {
why: `invariantId "${invariantId}" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,
fix: "Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.",
details: { invariantId }
});
}
function errorProvidedInvariantsMismatch(filePath, stored, derived) {
const storedSet = new Set(stored);
const derivedSet = new Set(derived);
const missing = [...derivedSet].filter((id) => !storedSet.has(id));
const extra = [...storedSet].filter((id) => !derivedSet.has(id));
return new MigrationToolsError("MIGRATION.PROVIDED_INVARIANTS_MISMATCH", "providedInvariants on migration.json disagrees with ops.json", {
why: missing.length === 0 && extra.length === 0 ? `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.` : `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
stored,
derived,
difference: {
missing,
extra
}
}
});
}
function errorNoInvariantPath(args) {
const { refName, required, missing, structuralPath } = args;
const refClause = refName ? `Ref "${refName}"` : "Target";
const missingList = missing.map((id) => JSON.stringify(id)).join(", ");
return new MigrationToolsError("MIGRATION.NO_INVARIANT_PATH", "No path covers the required invariants", {
why: `${refClause} requires invariants the reachable path doesn't cover. required=[${required.map((id) => JSON.stringify(id)).join(", ")}], missing=[${missingList}].`,
fix: "Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.",
details: {
required,
missing,
structuralPath,
...ifDefined("refName", refName)
}
});
}
function errorUnknownInvariant(args) {
const { refName, unknown, declared } = args;
return new MigrationToolsError("MIGRATION.UNKNOWN_INVARIANT", "Ref declares invariants no migration in the graph provides", {
why: `${refName ? `Ref "${refName}" declares` : "Declares"} invariants no migration in the graph provides. unknown=[${unknown.map((id) => JSON.stringify(id)).join(", ")}].`,
fix: "Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.",
details: {
unknown,
declared,
...ifDefined("refName", refName)
}
});
}
function errorMigrationHashMismatch(dir, storedHash, computedHash) {
return new MigrationToolsError("MIGRATION.HASH_MISMATCH", "Migration package is corrupt", {
why: `Stored migrationHash "${storedHash}" does not match the recomputed hash "${computedHash}" for "${relative(process.cwd(), dir)}". The migration.json or ops.json has been edited or partially written since emit.`,
fix: reemitHint(dir, "or restore the directory from version control."),
details: {
dir,
storedHash,
computedHash
}
});
}
function errorSnapshotMissing(refName) {
return new MigrationToolsError("MIGRATION.SNAPSHOT_MISSING", `Ref "${refName}" has no paired contract snapshot`, {
why: `Ref "${refName}" exists but its paired snapshot files are missing.`,
fix: `Run "prisma-next db update --advance-ref ${refName}" to repopulate the snapshot, or "prisma-next ref delete ${refName}" to clear the orphan pointer.`,
details: {
refName,
identifier: refName,
viaRef: true
}
});
}
function errorBundleNotFoundForGraphNode(hash, explicitLabel) {
return new MigrationToolsError("MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE", explicitLabel ? `No migration bundle found for reference "${explicitLabel}" (resolved hash: ${hash})` : `No migration bundle found for graph node ${hash}`, {
why: `The hash ${hash} is a graph node but no on-disk migration package has an end-contract hash matching it.`,
fix: "Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.",
details: {
hash,
...explicitLabel ? { explicitLabel } : {}
}
});
}
function errorContractDeserializationFailed(filePath, message) {
return new MigrationToolsError("MIGRATION.CONTRACT_DESERIALIZATION_FAILED", "Contract failed to deserialize", {
why: `Contract at "${filePath}" failed to deserialize: ${message}`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
message
}
});
}
function errorHashNotInGraph(hash, graph) {
const reachableHashes = [...graph.nodes].sort();
const reachableList = reachableHashes.length > 0 ? reachableHashes.join(", ") : "(none)";
return new MigrationToolsError("MIGRATION.HASH_NOT_IN_GRAPH", `Hash "${hash}" is not a node in the migration graph`, {
why: `The migration graph contains nodes ${reachableList}; "${hash}" isn't one of them.`,
fix: `Pass a hash that's the from-or-to of an on-disk migration bundle, use --from with a graph-node hash, or run "prisma-next migration plan" to introduce it.`,
details: {
hash,
reachableHashes
}
});
}
function errorMigrationContractViewMissing(className, accessor, jsonField) {
return new MigrationToolsError("MIGRATION.CONTRACT_VIEW_MISSING", `${className}.${accessor} requires ${jsonField}`, {
why: `${className}.${accessor} was read, but this instance has no ${jsonField} to build the view from.`,
fix: `Set ${jsonField} to the migration's committed contract JSON, or avoid reading ${accessor} on a migration that overrides describe() and carries no contract.`,
details: {
className,
accessor,
jsonField
}
});
}
//#endregion
export { errorNoInitialMigration as C, errorSnapshotMissing as D, errorProvidedInvariantsMismatch as E, errorUnknownInvariant as O, errorMissingFile as S, errorNoTarget as T, errorInvalidRefValue as _, errorDescriptorHeadHashMismatch as a, errorMigrationContractViewMissing as b, errorDuplicateSpaceId as c, errorInvalidInvariantId as d, errorInvalidJson as f, errorInvalidRefName as g, errorInvalidRefFile as h, errorContractDeserializationFailed as i, errorHashNotInGraph as l, errorInvalidOperationEntry as m, errorAmbiguousTarget as n, errorDirectoryExists as o, errorInvalidManifest as p, errorBundleNotFoundForGraphNode as r, errorDuplicateInvariantInEdge as s, MigrationToolsError as t, errorInvalidDestName as u, errorInvalidSlug as v, errorNoInvariantPath as w, errorMigrationHashMismatch as x, errorInvalidSpaceId as y };
//# sourceMappingURL=errors-DIS_dcFJ.mjs.map
{"version":3,"file":"errors-DIS_dcFJ.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { ifDefined } from '@prisma-next/utils/defined';\nimport { basename, dirname, relative } from 'pathe';\nimport type { MigrationGraph } from './graph';\n\n/**\n * Build the canonical \"re-emit this package\" remediation hint.\n *\n * Every on-disk migration package ships its own `migration.ts` author-time\n * file. Running it regenerates `migration.json` and `ops.json` with the\n * correct hash + metadata, so it is the right primitive whenever a single\n * package's on-disk artifacts are missing, malformed, or otherwise corrupt.\n * Pointing users at `migration plan` would emit a *new* package rather than\n * heal the broken one.\n */\nfunction reemitHint(dir: string, fallback?: string): string {\n const relativeDir = relative(process.cwd(), dir);\n const reemit = `Re-emit the package by running \\`node \"${relativeDir}/migration.ts\"\\``;\n return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;\n}\n\n/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, hash\n * verification, migration history reconstruction), distinct from the runtime\n * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,\n * etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Migration manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_OPERATION_ENTRY',\n 'Migration operation entry is malformed',\n {\n why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,\n fix: \"Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').\",\n details: { index, reason },\n },\n );\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorInvalidSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_SPACE_ID',\n 'Invalid contract space identifier',\n {\n why: `The space id \"${spaceId}\" does not match the required pattern /^[a-z][a-z0-9_-]{0,63}$/. Space ids are used as filesystem directory names under \\`migrations/\\`, so the pattern is conservative on purpose.`,\n fix: 'Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorDescriptorHeadHashMismatch(args: {\n readonly extensionId: string;\n readonly recomputedHash: string;\n readonly headRefHash: string;\n}): MigrationToolsError {\n const { extensionId, recomputedHash, headRefHash } = args;\n return new MigrationToolsError(\n 'MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH',\n \"Extension descriptor's headRef.hash does not match its contractJson\",\n {\n why: `Extension \"${extensionId}\" publishes a \\`contractSpace\\` whose \\`headRef.hash\\` (${headRefHash}) does not match the canonical hash recomputed from \\`contractSpace.contractJson\\` (${recomputedHash}). This means the extension descriptor was published with stale \\`headRef.hash\\` — typically because the contract was bumped without rerunning the extension's emit pipeline.`,\n fix: 'Re-run the extension authoring pipeline so `contractJson.storage.storageHash` and `headRef.hash` agree, then republish the extension. If you are the extension author and you intentionally bumped `contractJson`, recompute and update `headRef.hash` (and refresh any on-disk migration metadata that derives from it).',\n details: { extensionId, recomputedHash, headRefHash },\n },\n );\n}\n\nexport function errorDuplicateSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_SPACE_ID',\n 'Duplicate contract space identifier',\n {\n why: `The space id \"${spaceId}\" appears more than once in the per-space planner input. Each space id must be unique across the inputs (the per-space planner emits one output entry per id).`,\n fix: 'Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {\n const dirName = basename(dir);\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration without data-transform operations has same source and target',\n {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform — otherwise the migration is a no-op.`,\n fix: reemitHint(\n dir,\n 'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',\n ),\n details: { dirName, hash },\n },\n );\n}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.',\n details: {\n branchTips,\n ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),\n },\n });\n}\n\nexport function errorNoInitialMigration(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_INITIAL_MIGRATION', 'No initial migration found', {\n why: `No migration starts from the empty contract state (known hashes: ${nodes.join(', ')}). At least one migration must originate from the empty state.`,\n fix: 'Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.',\n details: { nodes },\n });\n}\n\nexport function errorInvalidRefs(refsPath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REFS', 'Invalid refs.json', {\n why: `refs.json at \"${refsPath}\" is invalid: ${reason}`,\n fix: 'Ensure refs.json is a flat object mapping valid ref names to contract hash strings.',\n details: { path: refsPath, reason },\n });\n}\n\nexport function errorInvalidRefFile(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_FILE', 'Invalid ref file', {\n why: `Ref file at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.',\n details: { path: filePath, reason },\n });\n}\n\nexport function errorInvalidRefName(refName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_NAME', 'Invalid ref name', {\n why: `Ref name \"${refName}\" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no \".\" or \"..\" segments).`,\n fix: `Use a valid ref name (e.g., \"staging\", \"envs/production\").`,\n details: { refName },\n });\n}\n\nexport function errorNoTarget(reachableHashes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_TARGET', 'No migration target could be resolved', {\n why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,\n fix: 'Use --from <hash> to specify the planning origin explicitly.',\n details: { reachableHashes },\n });\n}\n\nexport function errorInvalidRefValue(value: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_VALUE', 'Invalid ref value', {\n why: `Ref value \"${value}\" is not a valid contract hash. Values must be in the format \"sha256:<64 hex chars>\" or \"sha256:empty\".`,\n fix: 'Use a valid storage hash from `prisma-next contract emit` output or an existing migration.',\n details: { value },\n });\n}\n\nexport function errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_HASH',\n 'Duplicate migrationHash in migration graph',\n {\n why: `Multiple migrations share migrationHash \"${migrationHash}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',\n details: { migrationHash },\n },\n );\n}\n\nexport function errorInvalidInvariantId(invariantId: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {\n why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,\n fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".',\n details: { invariantId },\n });\n}\n\nexport function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',\n 'Duplicate invariantId on a single migration',\n {\n why: `invariantId \"${invariantId}\" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,\n fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',\n details: { invariantId },\n },\n );\n}\n\nexport function errorProvidedInvariantsMismatch(\n filePath: string,\n stored: readonly string[],\n derived: readonly string[],\n): MigrationToolsError {\n const storedSet = new Set(stored);\n const derivedSet = new Set(derived);\n const missing = [...derivedSet].filter((id) => !storedSet.has(id));\n const extra = [...storedSet].filter((id) => !derivedSet.has(id));\n // When sets agree but arrays don't, the only difference is ordering — call\n // it out so the reader doesn't stare at two visually-identical arrays.\n // Canonical providedInvariants is sorted ascending; a manifest with the\n // same ids in a different order is still a mismatch (the hash check would\n // also fail), but the human-readable diagnostic is otherwise unhelpful.\n const orderingOnly = missing.length === 0 && extra.length === 0;\n const why = orderingOnly\n ? `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.`\n : `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`;\n return new MigrationToolsError(\n 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',\n 'providedInvariants on migration.json disagrees with ops.json',\n {\n why,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, stored, derived, difference: { missing, extra } },\n },\n );\n}\n\n/**\n * Wire-shape edge surfaced through the JSON envelope's\n * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —\n * authoring metadata (`createdAt`) lives on `MigrationEdge` but is\n * intentionally dropped here so the envelope stays stable across\n * graph-internal refactors.\n *\n * Stability: any field added here is part of the public CLI JSON contract.\n * Callers (CLI consumers, agents) must be able to treat\n * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.\n */\nexport interface NoInvariantPathStructuralEdge {\n readonly dirName: string;\n readonly migrationHash: string;\n readonly from: string;\n readonly to: string;\n readonly invariants: readonly string[];\n}\n\nexport function errorNoInvariantPath(args: {\n readonly refName?: string;\n readonly required: readonly string[];\n readonly missing: readonly string[];\n readonly structuralPath: readonly NoInvariantPathStructuralEdge[];\n}): MigrationToolsError {\n const { refName, required, missing, structuralPath } = args;\n const refClause = refName ? `Ref \"${refName}\"` : 'Target';\n const missingList = missing.map((id) => JSON.stringify(id)).join(', ');\n const requiredList = required.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.NO_INVARIANT_PATH',\n 'No path covers the required invariants',\n {\n why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,\n fix: 'Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.',\n details: {\n required,\n missing,\n structuralPath,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorUnknownInvariant(args: {\n readonly refName?: string;\n readonly unknown: readonly string[];\n readonly declared: readonly string[];\n}): MigrationToolsError {\n const { refName, unknown, declared } = args;\n const refClause = refName ? `Ref \"${refName}\" declares` : 'Declares';\n const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.UNKNOWN_INVARIANT',\n 'Ref declares invariants no migration in the graph provides',\n {\n why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,\n fix: 'Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.',\n details: {\n unknown,\n declared,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorMigrationHashMismatch(\n dir: string,\n storedHash: string,\n computedHash: string,\n): MigrationToolsError {\n // Render a cwd-relative path in the human-readable diagnostic so users\n // running CLI commands from the project root see a familiar short path.\n // Keep the absolute path in `details.dir` for machine consumers.\n const relativeDir = relative(process.cwd(), dir);\n return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {\n why: `Stored migrationHash \"${storedHash}\" does not match the recomputed hash \"${computedHash}\" for \"${relativeDir}\". The migration.json or ops.json has been edited or partially written since emit.`,\n fix: reemitHint(dir, 'or restore the directory from version control.'),\n details: { dir, storedHash, computedHash },\n });\n}\n\nexport function errorSnapshotMissing(refName: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.SNAPSHOT_MISSING',\n `Ref \"${refName}\" has no paired contract snapshot`,\n {\n why: `Ref \"${refName}\" exists but its paired snapshot files are missing.`,\n fix: `Run \"prisma-next db update --advance-ref ${refName}\" to repopulate the snapshot, or \"prisma-next ref delete ${refName}\" to clear the orphan pointer.`,\n details: { refName, identifier: refName, viaRef: true },\n },\n );\n}\n\nexport function errorBundleNotFoundForGraphNode(\n hash: string,\n explicitLabel?: string,\n): MigrationToolsError {\n const summary = explicitLabel\n ? `No migration bundle found for reference \"${explicitLabel}\" (resolved hash: ${hash})`\n : `No migration bundle found for graph node ${hash}`;\n return new MigrationToolsError('MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE', summary, {\n why: `The hash ${hash} is a graph node but no on-disk migration package has an end-contract hash matching it.`,\n fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',\n details: { hash, ...(explicitLabel ? { explicitLabel } : {}) },\n });\n}\n\nexport function errorContractDeserializationFailed(\n filePath: string,\n message: string,\n): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED',\n 'Contract failed to deserialize',\n {\n why: `Contract at \"${filePath}\" failed to deserialize: ${message}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, message },\n },\n );\n}\n\nexport function errorHashNotInGraph(hash: string, graph: MigrationGraph): MigrationToolsError {\n const reachableHashes = [...graph.nodes].sort();\n const reachableList = reachableHashes.length > 0 ? reachableHashes.join(', ') : '(none)';\n return new MigrationToolsError(\n 'MIGRATION.HASH_NOT_IN_GRAPH',\n `Hash \"${hash}\" is not a node in the migration graph`,\n {\n why: `The migration graph contains nodes ${reachableList}; \"${hash}\" isn't one of them.`,\n fix: `Pass a hash that's the from-or-to of an on-disk migration bundle, use --from with a graph-node hash, or run \"prisma-next migration plan\" to introduce it.`,\n details: { hash, reachableHashes },\n },\n );\n}\n\nexport function errorMigrationContractViewMissing(\n className: string,\n accessor: 'endContract' | 'startContract',\n jsonField: 'endContractJson' | 'startContractJson',\n): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.CONTRACT_VIEW_MISSING',\n `${className}.${accessor} requires ${jsonField}`,\n {\n why: `${className}.${accessor} was read, but this instance has no ${jsonField} to build the view from.`,\n fix: `Set ${jsonField} to the migration's committed contract JSON, or avoid reading ${accessor} on a migration that overrides describe() and carries no contract.`,\n details: { className, accessor, jsonField },\n },\n );\n}\n"],"mappings":"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"}
import { l as errorHashNotInGraph } from "./errors-DIS_dcFJ.mjs";
import "./constants-YnG8_EGO.mjs";
//#region src/graph-membership.ts
function isGraphNode(hash, graph) {
if (hash === "sha256:empty") return true;
return graph.nodes.has(hash);
}
function assertHashIsGraphNode(hash, graph) {
if (isGraphNode(hash, graph)) return;
throw errorHashNotInGraph(hash, graph);
}
//#endregion
export { isGraphNode as n, assertHashIsGraphNode as t };
//# sourceMappingURL=graph-membership-CNneL_Yy.mjs.map
{"version":3,"file":"graph-membership-CNneL_Yy.mjs","names":[],"sources":["../src/graph-membership.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from './constants';\nimport { errorHashNotInGraph } from './errors';\nimport type { MigrationGraph } from './graph';\n\nexport function isGraphNode(hash: string, graph: MigrationGraph): boolean {\n if (hash === EMPTY_CONTRACT_HASH) {\n return true;\n }\n return graph.nodes.has(hash);\n}\n\nexport function assertHashIsGraphNode(hash: string, graph: MigrationGraph): asserts hash is string {\n if (isGraphNode(hash, graph)) {\n return;\n }\n throw errorHashNotInGraph(hash, graph);\n}\n"],"mappings":";;;AAIA,SAAgB,YAAY,MAAc,OAAgC;CACxE,IAAI,SAAA,gBACF,OAAO;CAET,OAAO,MAAM,MAAM,IAAI,IAAI;AAC7B;AAEA,SAAgB,sBAAsB,MAAc,OAA+C;CACjG,IAAI,YAAY,MAAM,KAAK,GACzB;CAEF,MAAM,oBAAoB,MAAM,KAAK;AACvC"}
import { d as errorInvalidInvariantId, s as errorDuplicateInvariantInEdge } from "./errors-DIS_dcFJ.mjs";
//#region src/invariants.ts
/**
* Hygiene check for `invariantId`. Rejects empty values plus any
* whitespace or control character (including Unicode whitespace like
* NBSP and em space, which are visually identical to ASCII space and
* routinely sneak in via paste).
*/
function validateInvariantId(invariantId) {
if (invariantId.length === 0) return false;
return !/[\p{Cc}\p{White_Space}]/u.test(invariantId);
}
/**
* Walk a migration's operations and produce its `providedInvariants`
* aggregate: the sorted, deduplicated list of `invariantId`s declared
* by ops in the migration. Ops without an `invariantId` are skipped.
*
* Both `data`-class ops (data-transforms, e.g. backfills) and
* `additive`-class opaque DDL (e.g. cipherstash's vendored EQL bundle
* via `installEqlBundleOp`) may declare invariantIds: the
* `operationClass` axis classifies *policy gating* (which kinds of ops
* a `db init` / `db update` policy permits), while `invariantId`
* classifies *marker bookkeeping* (which named bundles of work a
* future regeneration knows to skip). The two concerns are
* intentionally orthogonal — an extension can ship additive
* non-IR-derivable DDL (the only way the planner can know the bundle
* is already applied is via the invariantId on the marker) without
* needing to mis-classify it as `data`-class.
*
* Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and
* `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.
*
* @see docs/architecture docs/adrs/ADR 212 - Contract spaces.md
* — extension migrations carry `invariantId`s on additive ops; e.g.
* cipherstash's `installEqlBundle` and structural `create-*` ops are
* additive-class but carry `cipherstash:*` invariantIds.
*/
function deriveProvidedInvariants(ops) {
const seen = /* @__PURE__ */ new Set();
for (const op of ops) {
const invariantId = readInvariantId(op);
if (invariantId === void 0) continue;
if (!validateInvariantId(invariantId)) throw errorInvalidInvariantId(invariantId);
if (seen.has(invariantId)) throw errorDuplicateInvariantInEdge(invariantId);
seen.add(invariantId);
}
return [...seen].sort();
}
function readInvariantId(op) {
if (!Object.hasOwn(op, "invariantId")) return void 0;
const candidate = op.invariantId;
return typeof candidate === "string" ? candidate : void 0;
}
//#endregion
export { validateInvariantId as n, deriveProvidedInvariants as t };
//# sourceMappingURL=invariants-CH8ATaEp.mjs.map
{"version":3,"file":"invariants-CH8ATaEp.mjs","names":[],"sources":["../src/invariants.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { errorDuplicateInvariantInEdge, errorInvalidInvariantId } from './errors';\nimport type { MigrationOps } from './package';\n\n/**\n * Hygiene check for `invariantId`. Rejects empty values plus any\n * whitespace or control character (including Unicode whitespace like\n * NBSP and em space, which are visually identical to ASCII space and\n * routinely sneak in via paste).\n */\nexport function validateInvariantId(invariantId: string): boolean {\n if (invariantId.length === 0) return false;\n return !/[\\p{Cc}\\p{White_Space}]/u.test(invariantId);\n}\n\n/**\n * Walk a migration's operations and produce its `providedInvariants`\n * aggregate: the sorted, deduplicated list of `invariantId`s declared\n * by ops in the migration. Ops without an `invariantId` are skipped.\n *\n * Both `data`-class ops (data-transforms, e.g. backfills) and\n * `additive`-class opaque DDL (e.g. cipherstash's vendored EQL bundle\n * via `installEqlBundleOp`) may declare invariantIds: the\n * `operationClass` axis classifies *policy gating* (which kinds of ops\n * a `db init` / `db update` policy permits), while `invariantId`\n * classifies *marker bookkeeping* (which named bundles of work a\n * future regeneration knows to skip). The two concerns are\n * intentionally orthogonal — an extension can ship additive\n * non-IR-derivable DDL (the only way the planner can know the bundle\n * is already applied is via the invariantId on the marker) without\n * needing to mis-classify it as `data`-class.\n *\n * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and\n * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.\n *\n * @see docs/architecture docs/adrs/ADR 212 - Contract spaces.md\n * — extension migrations carry `invariantId`s on additive ops; e.g.\n * cipherstash's `installEqlBundle` and structural `create-*` ops are\n * additive-class but carry `cipherstash:*` invariantIds.\n */\nexport function deriveProvidedInvariants(ops: MigrationOps): readonly string[] {\n const seen = new Set<string>();\n for (const op of ops) {\n const invariantId = readInvariantId(op);\n if (invariantId === undefined) continue;\n if (!validateInvariantId(invariantId)) {\n throw errorInvalidInvariantId(invariantId);\n }\n if (seen.has(invariantId)) {\n throw errorDuplicateInvariantInEdge(invariantId);\n }\n seen.add(invariantId);\n }\n return [...seen].sort();\n}\n\nfunction readInvariantId(op: MigrationPlanOperation): string | undefined {\n if (!Object.hasOwn(op, 'invariantId')) return undefined;\n const candidate = (op as { invariantId?: unknown }).invariantId;\n return typeof candidate === 'string' ? candidate : undefined;\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,oBAAoB,aAA8B;CAChE,IAAI,YAAY,WAAW,GAAG,OAAO;CACrC,OAAO,CAAC,2BAA2B,KAAK,WAAW;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,yBAAyB,KAAsC;CAC7E,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,cAAc,gBAAgB,EAAE;EACtC,IAAI,gBAAgB,KAAA,GAAW;EAC/B,IAAI,CAAC,oBAAoB,WAAW,GAClC,MAAM,wBAAwB,WAAW;EAE3C,IAAI,KAAK,IAAI,WAAW,GACtB,MAAM,8BAA8B,WAAW;EAEjD,KAAK,IAAI,WAAW;CACtB;CACA,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK;AACxB;AAEA,SAAS,gBAAgB,IAAgD;CACvE,IAAI,CAAC,OAAO,OAAO,IAAI,aAAa,GAAG,OAAO,KAAA;CAC9C,MAAM,YAAa,GAAiC;CACpD,OAAO,OAAO,cAAc,WAAW,YAAY,KAAA;AACrD"}
import { E as errorProvidedInvariantsMismatch, S as errorMissingFile, f as errorInvalidJson, o as errorDirectoryExists, p as errorInvalidManifest, t as MigrationToolsError, u as errorInvalidDestName, v as errorInvalidSlug, x as errorMigrationHashMismatch } from "./errors-DIS_dcFJ.mjs";
import { n as verifyMigrationHash } from "./hash-BNCgfhir.mjs";
import { t as deriveProvidedInvariants } from "./invariants-CH8ATaEp.mjs";
import { n as MigrationOpsSchema } from "./op-schema-BSVHzEQN.mjs";
import { ifDefined } from "@prisma-next/utils/defined";
import { basename, dirname, join, resolve } from "pathe";
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { type } from "arktype";
//#region src/io.ts
const MANIFEST_FILE = "migration.json";
const OPS_FILE = "ops.json";
const END_CONTRACT_FILE = "end-contract.json";
const MAX_SLUG_LENGTH = 64;
function hasErrnoCode(error, code) {
return error instanceof Error && error.code === code;
}
const MigrationMetadataSchema = type({
"+": "reject",
from: "string > 0 | null",
to: "string",
migrationHash: "string",
providedInvariants: "string[]",
createdAt: "string"
});
async function writeMigrationPackage(dir, metadata, ops) {
await mkdir(dirname(dir), { recursive: true });
try {
await mkdir(dir);
} catch (error) {
if (hasErrnoCode(error, "EEXIST")) throw errorDirectoryExists(dir);
throw error;
}
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), { flag: "wx" });
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
}
/**
* Materialise an in-memory {@link MigrationPackage} to a per-space
* directory on disk.
*
* Writes two files under `<targetDir>/<pkg.dirName>/`:
*
* - `migration.json` — the manifest (pretty-printed, matches
* {@link writeMigrationPackage}'s output for byte-for-byte parity with
* app-space migrations).
* - `ops.json` — the operation list (pretty-printed).
*
* Distinct verb from the lower-level {@link writeMigrationPackage}
* (which takes constituent `(metadata, ops)`): callers reading
* `materialise…` know they are persisting a struct-typed package.
*
* Overwrite-idempotent: the per-package directory is cleared before
* each emit, so re-running against the same `targetDir` produces
* byte-identical contents and never leaves stale files behind. The
* lower-level {@link writeMigrationPackage} stays strict because the
* CLI authoring path (`migration plan` / `migration new`) deliberately
* refuses to clobber an existing authored migration; this helper is
* the re-emit path that is supposed to converge on a single canonical
* on-disk shape.
*
* The per-space head contract lives at
* `<projectMigrationsDir>/<spaceId>/contract.json` (written by
* {@link import('./emit-contract-space-artefacts').emitContractSpaceArtefacts}),
* not inside the per-package directory. The runner reads only
* `migration.json` + `ops.json` from each package.
*/
async function materialiseMigrationPackage(targetDir, pkg) {
const dir = join(targetDir, pkg.dirName);
await rm(dir, {
recursive: true,
force: true
});
await writeMigrationPackage(dir, pkg.metadata, pkg.ops);
}
/**
* Idempotent variant of {@link materialiseMigrationPackage}: writes the
* package only if `<targetDir>/<pkg.dirName>/` does not already exist on
* disk as a directory; returns `{ written: false }` when the package
* directory is present (no rewrite, no comparison — by-existence skip).
*
* Concretely:
* - existing directory → skip silently, return `{ written: false }`.
* - missing path → write three files via {@link materialiseMigrationPackage},
* return `{ written: true }`.
* - path exists but is not a directory (file/symlink) → treated as
* missing; {@link materialiseMigrationPackage} will attempt creation
* and fail with an appropriate OS error.
* - any other I/O error from `stat` → propagated unchanged.
*
* Used by the CLI's `runContractSpaceExtensionMigrationsPass` to
* materialise extension migration packages into a project's
* `migrations/<spaceId>/` directory, and by extension-package tests
* that mirror the same idempotent-rematerialise property locally
* without taking a CLI dependency.
*/
async function materialiseExtensionMigrationPackageIfMissing(targetDir, pkg) {
if (await directoryExists(join(targetDir, pkg.dirName))) return { written: false };
await materialiseMigrationPackage(targetDir, pkg);
return { written: true };
}
async function directoryExists(p) {
try {
return (await stat(p)).isDirectory();
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return false;
throw error;
}
}
/**
* Copy a list of files into `destDir`, optionally renaming each one.
*
* The destination directory is created (with `recursive: true`) if it
* does not already exist. Each source path is copied byte-for-byte into
* `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
* intentionally generic: callers own the list of files (e.g. a contract
* emitter's emitted output) and the naming convention (e.g. renaming
* the destination contract to `end-contract.*` and the source contract
* to `start-contract.*`).
*/
async function copyFilesWithRename(destDir, files) {
await mkdir(destDir, { recursive: true });
for (const file of files) {
if (basename(file.destName) !== file.destName) throw errorInvalidDestName(file.destName);
await copyFile(file.sourcePath, join(destDir, file.destName));
}
}
async function writeMigrationMetadata(dir, metadata) {
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
}
async function writeMigrationOps(dir, ops) {
await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
}
/**
* Reads the optional `end-contract.json` snapshot next to a migration
* manifest — the contract IR of the migration's destination state.
* Snapshots are author-time conveniences (ADR 197), never structural
* runner inputs, so a missing or unparseable file is treated as absent
* (`undefined`) — a package holding only `migration.json` + `ops.json`
* must keep loading (pinned regression in this package). A file holding
* the JSON literal `null` is also treated as absent: `undefined` is the
* single "no snapshot" sentinel downstream, and a null contract is not
* a storable state (the contract store's `contract_json` is NOT NULL).
*/
async function readEndContractJson(dir) {
let raw;
try {
raw = await readFile(join(dir, END_CONTRACT_FILE), "utf-8");
} catch {
return;
}
try {
const parsed = JSON.parse(raw);
return parsed === null ? void 0 : parsed;
} catch {
return;
}
}
async function readMigrationPackage(dir) {
const absoluteDir = resolve(dir);
const manifestPath = join(absoluteDir, MANIFEST_FILE);
const opsPath = join(absoluteDir, OPS_FILE);
let manifestRaw;
try {
manifestRaw = await readFile(manifestPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(MANIFEST_FILE, absoluteDir);
throw error;
}
let opsRaw;
try {
opsRaw = await readFile(opsPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(OPS_FILE, absoluteDir);
throw error;
}
let metadata;
try {
metadata = JSON.parse(manifestRaw);
} catch (e) {
throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
}
let ops;
try {
ops = JSON.parse(opsRaw);
} catch (e) {
throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
}
validateMetadata(metadata, manifestPath);
validateOps(ops, opsPath);
const derivedInvariants = deriveProvidedInvariants(ops);
if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) throw errorProvidedInvariantsMismatch(manifestPath, metadata.providedInvariants, derivedInvariants);
const endContractJson = await readEndContractJson(absoluteDir);
const pkg = {
dirName: basename(absoluteDir),
dirPath: absoluteDir,
metadata,
ops,
...ifDefined("endContractJson", endContractJson)
};
const verification = verifyMigrationHash(pkg);
if (!verification.ok) throw errorMigrationHashMismatch(absoluteDir, verification.storedHash, verification.computedHash);
return pkg;
}
/**
* Reads a migration package's manifest and ops without running hash or
* invariants verification. Returns `null` when the files cannot be read or
* parsed (i.e. when the package is genuinely unloadable).
*
* Used by {@link readMigrationsDir} to retain a package whose hash or
* invariants diverge from what is stored on disk — the raw content is still
* useful for display / querying; only integrity is in question.
*/
async function readMigrationPackageRaw(dir) {
const absoluteDir = resolve(dir);
const manifestPath = join(absoluteDir, MANIFEST_FILE);
const opsPath = join(absoluteDir, OPS_FILE);
let manifestRaw;
try {
manifestRaw = await readFile(manifestPath, "utf-8");
} catch {
return null;
}
let opsRaw;
try {
opsRaw = await readFile(opsPath, "utf-8");
} catch {
return null;
}
let metadata;
try {
metadata = JSON.parse(manifestRaw);
} catch {
return null;
}
let ops;
try {
ops = JSON.parse(opsRaw);
} catch {
return null;
}
if (MigrationMetadataSchema(metadata) instanceof type.errors) return null;
if (MigrationOpsSchema(ops) instanceof type.errors) return null;
return {
dirName: basename(absoluteDir),
dirPath: absoluteDir,
metadata,
ops
};
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
function validateMetadata(metadata, filePath) {
const result = MigrationMetadataSchema(metadata);
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
}
function validateOps(ops, filePath) {
const result = MigrationOpsSchema(ops);
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
}
function packageLoadProblemDetailFromError(error) {
if (MigrationToolsError.is(error)) return error.why;
if (error instanceof Error) return error.message;
return String(error);
}
async function readMigrationsDir(migrationsRoot) {
let entries;
try {
entries = await readdir(migrationsRoot);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return {
packages: [],
problems: []
};
throw error;
}
const packages = [];
const problems = [];
for (const entry of entries.sort()) {
const entryPath = join(migrationsRoot, entry);
if (!(await stat(entryPath)).isDirectory()) continue;
const manifestPath = join(entryPath, MANIFEST_FILE);
try {
await stat(manifestPath);
} catch {
continue;
}
let pkg;
try {
pkg = await readMigrationPackage(entryPath);
} catch (error) {
const dirName = entry;
if (MigrationToolsError.is(error)) {
if (error.code === "MIGRATION.HASH_MISMATCH") {
const details = error.details;
const rawPkg = await readMigrationPackageRaw(entryPath);
if (rawPkg !== null) packages.push(rawPkg);
problems.push({
kind: "hashMismatch",
dirName,
stored: typeof details?.["storedHash"] === "string" ? details["storedHash"] : "",
computed: typeof details?.["computedHash"] === "string" ? details["computedHash"] : ""
});
continue;
}
if (error.code === "MIGRATION.PROVIDED_INVARIANTS_MISMATCH") {
const rawPkg = await readMigrationPackageRaw(entryPath);
if (rawPkg !== null) packages.push(rawPkg);
problems.push({
kind: "providedInvariantsMismatch",
dirName
});
continue;
}
}
problems.push({
kind: "packageUnloadable",
dirName,
detail: packageLoadProblemDetailFromError(error)
});
continue;
}
packages.push(pkg);
}
return {
packages,
problems
};
}
function formatMigrationDirName(timestamp, slug) {
const sanitized = slug.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
if (sanitized.length === 0) throw errorInvalidSlug(slug);
const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);
return `${timestamp.getUTCFullYear()}${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}${String(timestamp.getUTCDate()).padStart(2, "0")}T${String(timestamp.getUTCHours()).padStart(2, "0")}${String(timestamp.getUTCMinutes()).padStart(2, "0")}_${truncated}`;
}
//#endregion
export { materialiseMigrationPackage as a, writeMigrationMetadata as c, materialiseExtensionMigrationPackageIfMissing as i, writeMigrationOps as l, copyFilesWithRename as n, readMigrationPackage as o, formatMigrationDirName as r, readMigrationsDir as s, MANIFEST_FILE as t, writeMigrationPackage as u };
//# sourceMappingURL=io-CE79C4uK.mjs.map
{"version":3,"file":"io-CE79C4uK.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 { ifDefined } from '@prisma-next/utils/defined';\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 MigrationToolsError,\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 END_CONTRACT_FILE = 'end-contract.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\n/**\n * Reads the optional `end-contract.json` snapshot next to a migration\n * manifest — the contract IR of the migration's destination state.\n * Snapshots are author-time conveniences (ADR 197), never structural\n * runner inputs, so a missing or unparseable file is treated as absent\n * (`undefined`) — a package holding only `migration.json` + `ops.json`\n * must keep loading (pinned regression in this package). A file holding\n * the JSON literal `null` is also treated as absent: `undefined` is the\n * single \"no snapshot\" sentinel downstream, and a null contract is not\n * a storable state (the contract store's `contract_json` is NOT NULL).\n */\nasync function readEndContractJson(dir: string): Promise<unknown> {\n let raw: string;\n try {\n raw = await readFile(join(dir, END_CONTRACT_FILE), 'utf-8');\n } catch {\n return undefined;\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n return parsed === null ? undefined : parsed;\n } catch {\n return undefined;\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 endContractJson = await readEndContractJson(absoluteDir);\n const pkg: OnDiskMigrationPackage = {\n dirName: basename(absoluteDir),\n dirPath: absoluteDir,\n metadata,\n ops,\n ...ifDefined('endContractJson', endContractJson),\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\n/**\n * Reads a migration package's manifest and ops without running hash or\n * invariants verification. Returns `null` when the files cannot be read or\n * parsed (i.e. when the package is genuinely unloadable).\n *\n * Used by {@link readMigrationsDir} to retain a package whose hash or\n * invariants diverge from what is stored on disk — the raw content is still\n * useful for display / querying; only integrity is in question.\n */\nasync function readMigrationPackageRaw(dir: string): Promise<OnDiskMigrationPackage | null> {\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 {\n return null;\n }\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch {\n return null;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch {\n return null;\n }\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch {\n return null;\n }\n\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) return null;\n\n const opsResult = MigrationOpsSchema(ops);\n if (opsResult instanceof type.errors) return null;\n\n // Deliberately no `endContractJson`: this loader only runs for packages\n // that failed hash / invariants verification, and a snapshot from an\n // unverifiable package must never reach the ledger's contract store.\n return {\n dirName: basename(absoluteDir),\n dirPath: absoluteDir,\n metadata,\n ops,\n };\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\n/**\n * A per-package load-time problem returned by {@link readMigrationsDir}.\n *\n * Three variants, matching the relocated throws from the load path:\n *\n * - `hashMismatch` — stored `migrationHash` differs from the recomputed value.\n * The package is **retained** in the returned `packages` array.\n * - `providedInvariantsMismatch` — `migration.json` declares different\n * `providedInvariants` than `ops.json` implies. The package is **retained**.\n * - `packageUnloadable` — the manifest is missing, unparseable, or schema-\n * invalid. The package is **omitted** from `packages`.\n *\n * Callers that need the `spaceId` context (e.g. the aggregate loader) attach\n * it when converting to {@link import('./integrity-violation').IntegrityViolation}.\n */\nexport type PackageLoadProblem =\n | {\n readonly kind: 'hashMismatch';\n readonly dirName: string;\n readonly stored: string;\n readonly computed: string;\n }\n | { readonly kind: 'providedInvariantsMismatch'; readonly dirName: string }\n | { readonly kind: 'packageUnloadable'; readonly dirName: string; readonly detail: string };\n\n/**\n * Result returned by {@link readMigrationsDir}.\n *\n * - `packages` — every package that could be read; hash-mismatched and\n * invariants-mismatched packages are included here (the problem is\n * represented rather than fatal).\n * - `problems` — one entry per package that had a load-time issue.\n * `packageUnloadable` entries are **not** in `packages`.\n */\nexport interface ReadMigrationsDirResult {\n readonly packages: readonly OnDiskMigrationPackage[];\n readonly problems: readonly PackageLoadProblem[];\n}\n\nfunction packageLoadProblemDetailFromError(error: unknown): string {\n if (MigrationToolsError.is(error)) return error.why;\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\nexport async function readMigrationsDir(migrationsRoot: string): Promise<ReadMigrationsDirResult> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return { packages: [], problems: [] };\n }\n throw error;\n }\n\n const packages: OnDiskMigrationPackage[] = [];\n const problems: PackageLoadProblem[] = [];\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 let pkg: OnDiskMigrationPackage;\n try {\n pkg = await readMigrationPackage(entryPath);\n } catch (error) {\n const dirName = entry;\n if (MigrationToolsError.is(error)) {\n if (error.code === 'MIGRATION.HASH_MISMATCH') {\n const details = error.details;\n const rawPkg = await readMigrationPackageRaw(entryPath);\n if (rawPkg !== null) packages.push(rawPkg);\n problems.push({\n kind: 'hashMismatch',\n dirName,\n stored: typeof details?.['storedHash'] === 'string' ? details['storedHash'] : '',\n computed: typeof details?.['computedHash'] === 'string' ? details['computedHash'] : '',\n });\n continue;\n }\n if (error.code === 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH') {\n const rawPkg = await readMigrationPackageRaw(entryPath);\n if (rawPkg !== null) packages.push(rawPkg);\n problems.push({ kind: 'providedInvariantsMismatch', dirName });\n continue;\n }\n }\n // Any other error (missing file, invalid JSON, invalid manifest schema) →\n // package unloadable; omit from packages.\n problems.push({\n kind: 'packageUnloadable',\n dirName,\n detail: packageLoadProblemDetailFromError(error),\n });\n continue;\n }\n packages.push(pkg);\n }\n\n return { packages, problems };\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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"}
import { C as errorNoInitialMigration, T as errorNoTarget, n as errorAmbiguousTarget } from "./errors-DIS_dcFJ.mjs";
import { t as EMPTY_CONTRACT_HASH } from "./constants-YnG8_EGO.mjs";
import { ifDefined } from "@prisma-next/utils/defined";
//#region src/queue.ts
/**
* FIFO queue with amortised O(1) push and shift.
*
* Uses a head-index cursor over a backing array rather than
* `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped
* traversals where the queue is drained in a single pass — it does not
* reclaim memory for already-shifted items, so it is not suitable for
* long-lived queues with many push/shift cycles.
*/
var Queue = class {
items;
head = 0;
constructor(initial = []) {
this.items = [...initial];
}
push(item) {
this.items.push(item);
}
/**
* Remove and return the next item. Caller must check `isEmpty` first —
* shifting an empty queue throws.
*/
shift() {
if (this.head >= this.items.length) throw new Error("Queue.shift called on empty queue");
return this.items[this.head++];
}
get isEmpty() {
return this.head >= this.items.length;
}
};
//#endregion
//#region src/graph-ops.ts
function* bfs(starts, neighbours, key = (state) => state) {
const visited = /* @__PURE__ */ new Set();
const parentMap = /* @__PURE__ */ new Map();
const queue = new Queue();
for (const start of starts) {
const k = key(start);
if (!visited.has(k)) {
visited.add(k);
queue.push({
state: start,
key: k
});
}
}
while (!queue.isEmpty) {
const { state: current, key: curKey } = queue.shift();
const parentInfo = parentMap.get(curKey);
yield {
state: current,
parent: parentInfo?.parent ?? null,
incomingEdge: parentInfo?.edge ?? null
};
for (const { next, edge } of neighbours(current)) {
const k = key(next);
if (!visited.has(k)) {
visited.add(k);
parentMap.set(k, {
parent: current,
edge
});
queue.push({
state: next,
key: k
});
}
}
}
}
//#endregion
//#region src/migration-graph.ts
/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */
function forwardNeighbours(graph, node) {
return (graph.forwardChain.get(node) ?? []).map((edge) => ({
next: edge.to,
edge
}));
}
/**
* Forward-edge neighbours, sorted by the deterministic tie-break.
* Used by path-finding so the resulting shortest path is stable across runs.
*/
function sortedForwardNeighbours(graph, node) {
return [...graph.forwardChain.get(node) ?? []].sort(compareTieBreak).map((edge) => ({
next: edge.to,
edge
}));
}
/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */
function reverseNeighbours(graph, node) {
return (graph.reverseChain.get(node) ?? []).map((edge) => ({
next: edge.from,
edge
}));
}
function appendEdge(map, key, entry) {
const bucket = map.get(key);
if (bucket) bucket.push(entry);
else map.set(key, [entry]);
}
function reconstructGraph(packages) {
const nodes = /* @__PURE__ */ new Set();
const forwardChain = /* @__PURE__ */ new Map();
const reverseChain = /* @__PURE__ */ new Map();
const migrationByHash = /* @__PURE__ */ new Map();
for (const pkg of packages) {
const from = pkg.metadata.from ?? "sha256:empty";
const { to } = pkg.metadata;
nodes.add(from);
nodes.add(to);
const migration = {
from,
to,
migrationHash: pkg.metadata.migrationHash,
dirName: pkg.dirName,
createdAt: pkg.metadata.createdAt,
invariants: pkg.metadata.providedInvariants
};
if (!migrationByHash.has(migration.migrationHash)) migrationByHash.set(migration.migrationHash, migration);
appendEdge(forwardChain, from, migration);
appendEdge(reverseChain, to, migration);
}
return {
nodes,
forwardChain,
reverseChain,
migrationByHash
};
}
function compareTieBreak(a, b) {
const ca = a.createdAt.localeCompare(b.createdAt);
if (ca !== 0) return ca;
const tc = a.to.localeCompare(b.to);
if (tc !== 0) return tc;
return a.migrationHash.localeCompare(b.migrationHash);
}
function sortedNeighbors(edges) {
return [...edges].sort(compareTieBreak);
}
/**
* Find the shortest path from `fromHash` to `toHash` using BFS over the
* contract-hash graph. Returns the ordered list of edges, or null if no path
* exists. Returns an empty array when `fromHash === toHash` (no-op).
*
* Neighbor ordering is deterministic via the tie-break sort key:
* createdAt → to → migrationHash.
*/
function findPath(graph, fromHash, toHash) {
if (fromHash === toHash) return [];
const parents = /* @__PURE__ */ new Map();
for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.state, {
parent: step.parent,
edge: step.incomingEdge
});
if (step.state === toHash) {
const path = [];
let cur = toHash;
let p = parents.get(cur);
while (p) {
path.push(p.edge);
cur = p.parent;
p = parents.get(cur);
}
path.reverse();
return path;
}
}
return null;
}
/**
* Find the shortest path from `fromHash` to `toHash` whose edges collectively
* cover every invariant in `required`. Returns `null` when no such path exists
* (either `fromHash`→`toHash` is structurally unreachable, or every reachable
* path leaves at least one required invariant uncovered). When `required` is
* empty, delegates to `findPath` so the result is byte-identical for that case.
*
* Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.
* The covered subset is a `Set<string>` of invariant ids; the state's dedup
* key is `${node}\0${[...covered].sort().join('\0')}`. State keys distinguish
* distinct `(node, covered)` tuples regardless of node-name length because
* `\0` cannot appear in any invariant id (validation rejects whitespace and
* control chars at authoring time).
*
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
* invariant come first, with `createdAt → to → migrationHash` as the
* secondary key. The heuristic steers BFS toward the satisfying path;
* correctness (shortest, deterministic) does not depend on it.
*/
function findPathWithInvariants(graph, fromHash, toHash, required) {
if (required.size === 0) return findPath(graph, fromHash, toHash);
const stateKey = (s) => {
if (s.covered.size === 0) return `${s.node}\0`;
return `${s.node}\0${[...s.covered].sort().join("\0")}`;
};
const neighbours = (s) => {
const outgoing = graph.forwardChain.get(s.node) ?? [];
if (outgoing.length === 0) return [];
return [...outgoing].map((edge) => {
let useful = false;
let next = null;
for (const inv of edge.invariants) if (required.has(inv) && !s.covered.has(inv)) {
if (next === null) next = new Set(s.covered);
next.add(inv);
useful = true;
}
return {
edge,
useful,
nextCovered: next ?? s.covered
};
}).sort((a, b) => {
if (a.useful !== b.useful) return a.useful ? -1 : 1;
return compareTieBreak(a.edge, b.edge);
}).map(({ edge, nextCovered }) => ({
next: {
node: edge.to,
covered: nextCovered
},
edge
}));
};
const parents = /* @__PURE__ */ new Map();
for (const step of bfs([{
node: fromHash,
covered: /* @__PURE__ */ new Set()
}], neighbours, stateKey)) {
const curKey = stateKey(step.state);
if (step.parent !== null && step.incomingEdge !== null) parents.set(curKey, {
parentKey: stateKey(step.parent),
edge: step.incomingEdge
});
if (step.state.node === toHash && step.state.covered.size === required.size) {
const path = [];
let cur = curKey;
while (cur !== void 0) {
const p = parents.get(cur);
if (!p) break;
path.push(p.edge);
cur = p.parentKey;
}
path.reverse();
return path;
}
}
return null;
}
/**
* Reverse-BFS from `toHash` over `reverseChain` to collect every node from
* which `toHash` is reachable (inclusive of `toHash` itself).
*/
function collectNodesReachingTarget(graph, toHash) {
const reached = /* @__PURE__ */ new Set();
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.state);
return reached;
}
/**
* Find the shortest path from `fromHash` to `toHash` and return structured
* path-decision metadata for machine-readable output. When `required` is
* non-empty, the returned path is the shortest one whose edges collectively
* cover every required invariant.
*
* The discriminated return type tells the caller *why* a path could not be
* found, so the CLI can pick the right structured error without re-running
* a structural BFS.
*/
function findPathWithDecision(graph, fromHash, toHash, options = {}) {
const { refName, required = /* @__PURE__ */ new Set() } = options;
const requiredInvariants = [...required].sort();
if (fromHash === toHash && required.size === 0) return {
kind: "ok",
decision: {
selectedPath: [],
fromHash,
toHash,
alternativeCount: 0,
tieBreakReasons: [],
requiredInvariants,
satisfiedInvariants: [],
...ifDefined("refName", refName)
}
};
const path = findPathWithInvariants(graph, fromHash, toHash, required);
if (!path) {
if (required.size === 0) return { kind: "unreachable" };
const structural = findPath(graph, fromHash, toHash);
if (structural === null) return { kind: "unreachable" };
const coveredByStructural = /* @__PURE__ */ new Set();
for (const edge of structural) for (const inv of edge.invariants) if (required.has(inv)) coveredByStructural.add(inv);
return {
kind: "unsatisfiable",
structuralPath: structural,
missing: requiredInvariants.filter((id) => !coveredByStructural.has(id))
};
}
const satisfiedInvariants = computeSatisfiedInvariants(required, path);
const reachesTarget = collectNodesReachingTarget(graph, toHash);
const coveragePrefixes = requiredCoveragePrefixes(required, path);
const tieBreakReasons = [];
let alternativeCount = 0;
for (const [i, edge] of path.entries()) {
const outgoing = graph.forwardChain.get(edge.from);
if (!outgoing || outgoing.length <= 1) continue;
const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
if (reachable.length <= 1) continue;
let comparisonPool = reachable;
if (required.size > 0) {
const prefixSet = coveragePrefixes[i];
if (prefixSet === void 0) continue;
comparisonPool = invariantViableAlternativesAtStep(required, prefixSet, reachable);
}
alternativeCount += reachable.length - 1;
if (sortedNeighbors(reachable)[0]?.migrationHash !== edge.migrationHash) continue;
if (!reachable.some((e) => e.migrationHash !== edge.migrationHash)) continue;
const sortedViable = sortedNeighbors(comparisonPool);
if (sortedViable.length > 1 && sortedViable[0]?.migrationHash === edge.migrationHash && sortedViable.some((e) => e.migrationHash !== edge.migrationHash)) tieBreakReasons.push(`at ${edge.from}: ${comparisonPool.length} candidates, selected by tie-break`);
}
return {
kind: "ok",
decision: {
selectedPath: path,
fromHash,
toHash,
alternativeCount,
tieBreakReasons,
requiredInvariants,
satisfiedInvariants,
...ifDefined("refName", refName)
}
};
}
function computeSatisfiedInvariants(required, path) {
if (required.size === 0) return [];
const covered = /* @__PURE__ */ new Set();
for (const edge of path) for (const inv of edge.invariants) if (required.has(inv)) covered.add(inv);
return [...covered].sort();
}
/**
* For each edge on path, invariant coverage accumulated from earlier edges only —
* `(required ∩ ∪_{j<i} path[j].invariants)` represented as cumulative set along `required`,
* keyed as "full set of required ids satisfied before taking path[i]".
*/
function requiredCoveragePrefixes(required, path) {
const prefixes = [];
const acc = /* @__PURE__ */ new Set();
for (const edge of path) {
prefixes.push(new Set(acc));
for (const inv of edge.invariants) if (required.has(inv)) acc.add(inv);
}
return prefixes;
}
function invariantViableAlternativesAtStep(required, coverageBeforeTakingEdge, outgoing) {
if (required.size === 0) return [...outgoing];
return outgoing.filter((e) => [...required].every((id) => coverageBeforeTakingEdge.has(id) || e.invariants.includes(id)));
}
/**
* Walk ancestors of each branch tip back to find the last node
* that appears on all paths. Returns `fromHash` if no shared ancestor is found.
*/
function findDivergencePoint(graph, fromHash, leaves) {
const ancestorSets = leaves.map((leaf) => {
const ancestors = /* @__PURE__ */ new Set();
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.state);
return ancestors;
});
const commonAncestors = [...ancestorSets[0] ?? []].filter((node) => ancestorSets.every((s) => s.has(node)));
let deepest = fromHash;
let deepestDepth = -1;
for (const ancestor of commonAncestors) {
const path = findPath(graph, fromHash, ancestor);
const depth = path ? path.length : 0;
if (depth > deepestDepth) {
deepestDepth = depth;
deepest = ancestor;
}
}
return deepest;
}
/**
* Find all branch tips (nodes with no outgoing edges) reachable from
* `fromHash` via forward edges.
*/
function findReachableLeaves(graph, fromHash) {
const leaves = [];
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.state)?.length) leaves.push(step.state);
return leaves;
}
/**
* Find the target contract hash of the migration graph reachable from
* EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target
* state (either empty, or containing only the root with no outgoing
* edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none
* originate from the empty hash, and AMBIGUOUS_TARGET if multiple
* branch tips exist.
*/
function findLeaf(graph) {
if (graph.nodes.size === 0) return null;
if (!graph.nodes.has("sha256:empty")) throw errorNoInitialMigration([...graph.nodes]);
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
if (leaves.length === 0) {
const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);
if (reachable.length > 0) throw errorNoTarget(reachable);
return null;
}
if (leaves.length > 1) {
const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);
throw errorAmbiguousTarget(leaves, {
divergencePoint,
branches: leaves.map((tip) => {
return {
tip,
edges: (findPath(graph, divergencePoint, tip) ?? []).map((e) => ({
dirName: e.dirName,
from: e.from,
to: e.to
}))
};
})
});
}
return leaves[0];
}
/**
* Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
* to the single target. Returns null for an empty graph.
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
*/
function findLatestMigration(graph) {
const leafHash = findLeaf(graph);
if (leafHash === null) return null;
return findPath(graph, "sha256:empty", leafHash)?.at(-1) ?? null;
}
function detectCycles(graph) {
const WHITE = 0;
const GRAY = 1;
const BLACK = 2;
const color = /* @__PURE__ */ new Map();
const parentMap = /* @__PURE__ */ new Map();
const cycles = [];
for (const node of graph.nodes) color.set(node, WHITE);
const stack = [];
function pushFrame(u) {
color.set(u, GRAY);
stack.push({
node: u,
outgoing: graph.forwardChain.get(u) ?? [],
index: 0
});
}
for (const root of graph.nodes) {
if (color.get(root) !== WHITE) continue;
parentMap.set(root, null);
pushFrame(root);
while (stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame.index >= frame.outgoing.length) {
color.set(frame.node, BLACK);
stack.pop();
continue;
}
const v = frame.outgoing[frame.index++].to;
const vColor = color.get(v);
if (vColor === GRAY) {
const cycle = [v];
let cur = frame.node;
while (cur !== v) {
cycle.push(cur);
cur = parentMap.get(cur) ?? v;
}
cycle.reverse();
cycles.push(cycle);
} else if (vColor === WHITE) {
parentMap.set(v, frame.node);
pushFrame(v);
}
}
}
return cycles;
}
function detectOrphans(graph) {
if (graph.nodes.size === 0) return [];
const reachable = /* @__PURE__ */ new Set();
const startNodes = [];
if (graph.forwardChain.has("sha256:empty")) startNodes.push(EMPTY_CONTRACT_HASH);
else {
const allTargets = /* @__PURE__ */ new Set();
for (const edges of graph.forwardChain.values()) for (const edge of edges) allTargets.add(edge.to);
for (const node of graph.nodes) if (!allTargets.has(node)) startNodes.push(node);
}
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.state);
const orphans = [];
for (const [from, migrations] of graph.forwardChain) if (!reachable.has(from)) orphans.push(...migrations);
return orphans;
}
//#endregion
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 };
//# sourceMappingURL=migration-graph-BXHOgdTg.mjs.map
{"version":3,"file":"migration-graph-BXHOgdTg.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 { errorAmbiguousTarget, errorNoInitialMigration, errorNoTarget } 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 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 migrationByHash.set(migration.migrationHash, migration);\n }\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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"}
import { S as errorMissingFile, f as errorInvalidJson, h as errorInvalidRefFile, y as errorInvalidSpaceId } from "./errors-DIS_dcFJ.mjs";
import { t as MANIFEST_FILE } from "./io-CE79C4uK.mjs";
import { join } from "pathe";
import { readFile, readdir, stat } from "node:fs/promises";
import { APP_SPACE_ID } from "@prisma-next/framework-components/control";
//#region src/space-layout.ts
/**
* Pattern a contract-space identifier must match. The constraint is
* filesystem-friendly: lowercase letters / digits / hyphen / underscore,
* starts with a letter, max 64 characters.
*/
const SPACE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
function isValidSpaceId(spaceId) {
return SPACE_ID_PATTERN.test(spaceId);
}
function assertValidSpaceId(spaceId) {
if (!isValidSpaceId(spaceId)) throw errorInvalidSpaceId(spaceId);
}
/**
* Resolve the migrations subdirectory for a given contract space.
*
* Every contract space — including the app space (default `'app'`) —
* lands under `<projectMigrationsDir>/<spaceId>/`. The space id is
* validated against {@link SPACE_ID_PATTERN} because it becomes a
* filesystem directory name verbatim.
*
* `projectMigrationsDir` is the project's top-level `migrations/`
* directory; the helper does not assume anything about its absolute /
* relative shape and is symmetric with `pathe.join`.
*/
function spaceMigrationDirectory(projectMigrationsDir, spaceId) {
assertValidSpaceId(spaceId);
return join(projectMigrationsDir, spaceId);
}
/**
* Per-space subdirectory name reserved for the ref store
* (`migrations/<space>/<SPACE_REFS_DIRNAME>/*.json`). Single source of
* truth: every helper that composes a per-space refs path imports this
* constant, and the enumerator uses it (via
* {@link RESERVED_SPACE_SUBDIR_NAMES}) to exclude reserved names from
* the contract-space candidate list.
*/
const SPACE_REFS_DIRNAME = "refs";
/**
* Names reserved as per-space subdirectories of `migrations/<space>/`.
* Used by the enumerator to filter contract-space candidates so a
* reserved name (e.g. a top-level `migrations/refs/` left in the wrong
* place) is never enumerated as a phantom contract space. Currently
* holds `SPACE_REFS_DIRNAME`; extend if future per-space layouts add
* more reserved subdirectories.
*/
const RESERVED_SPACE_SUBDIR_NAMES = /* @__PURE__ */ new Set([SPACE_REFS_DIRNAME]);
/**
* Resolve the per-space refs directory for `spaceMigrationsDir`
* (typically the value returned by {@link spaceMigrationDirectory}).
* Composes the canonical {@link SPACE_REFS_DIRNAME} so callers do not
* hard-code the literal.
*/
function spaceRefsDirectory(spaceMigrationsDir) {
return join(spaceMigrationsDir, SPACE_REFS_DIRNAME);
}
//#endregion
//#region src/read-contract-space-head-ref.ts
function hasErrnoCode$2(error, code) {
return error instanceof Error && error.code === code;
}
/**
* Read the head ref (`hash` + `invariants`) for a contract space from
* `<projectMigrationsDir>/<spaceId>/refs/head.json`.
*
* Returns `null` when the file does not exist (first emit). Surfaces
* `MIGRATION.INVALID_JSON` / `MIGRATION.INVALID_REF_FILE` on a corrupt
* `refs/head.json` so callers can distinguish "no head ref on disk"
* (returns `null`) from "head ref present but unreadable" (throws).
*
* Validates the space id against `[a-z][a-z0-9_-]{0,63}` for the same
* filesystem-safety reasons as the rest of the per-space helpers. The
* helper is uniform across the app and extension spaces.
*/
async function readContractSpaceHeadRef(projectMigrationsDir, spaceId) {
assertValidSpaceId(spaceId);
const filePath = join(spaceRefsDirectory(spaceMigrationDirectory(projectMigrationsDir, spaceId)), "head.json");
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
if (hasErrnoCode$2(error, "ENOENT")) return null;
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));
}
if (typeof parsed !== "object" || parsed === null) throw errorInvalidRefFile(filePath, "expected an object");
const obj = parsed;
if (typeof obj.hash !== "string") throw errorInvalidRefFile(filePath, "expected an object with a string `hash` field");
if (!Array.isArray(obj.invariants) || obj.invariants.some((value) => typeof value !== "string")) throw errorInvalidRefFile(filePath, "expected an object with an `invariants` array of strings");
return {
hash: obj.hash,
invariants: obj.invariants
};
}
//#endregion
//#region src/verify-contract-spaces.ts
function hasErrnoCode$1(error, code) {
return error instanceof Error && error.code === code;
}
/**
* List the per-space subdirectories under
* `<projectRoot>/migrations/`. Returns space-id directory names (sorted
* alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
* does **not** contain a `migration.json` manifest. The manifest is the
* structural marker of a user-authored migration directory (see
* `readMigrationsDir` in `./io`); directory names themselves belong to
* the user and are not part of the contract.
*
* Returns `[]` if the migrations directory does not exist (greenfield
* project).
*
* Reads only the user's repo. **No descriptor import.** The caller
* (verifier) feeds the result into {@link verifyContractSpaces} alongside
* the loaded-space set and the marker rows.
*/
async function listContractSpaceDirectories(projectMigrationsDir) {
let entries;
try {
entries = (await readdir(projectMigrationsDir, { withFileTypes: true })).map((d) => ({
name: d.name,
isDirectory: d.isDirectory()
}));
} catch (error) {
if (hasErrnoCode$1(error, "ENOENT")) return [];
throw error;
}
const namedCandidates = entries.filter((e) => e.isDirectory).map((e) => e.name).filter((name) => !name.startsWith(".")).sort();
return (await Promise.all(namedCandidates.map(async (name) => {
try {
await stat(join(projectMigrationsDir, name, MANIFEST_FILE));
return {
name,
isMigrationDir: true
};
} catch (error) {
if (hasErrnoCode$1(error, "ENOENT")) return {
name,
isMigrationDir: false
};
throw error;
}
}))).filter((c) => !c.isMigrationDir).map((c) => c.name);
}
/**
* Pure structural verifier for the per-space mechanism. Aggregates the
* three orphan / missing checks plus per-space hash and invariant
* comparison.
*
* Algorithm:
*
* - For every extension space declared in `loadedSpaces` (`'app'`
* excluded — the per-space verifier is scoped to extension spaces;
* the app is verified through the aggregate path):
* - If no contract-space dir on disk → `declaredButUnmigrated`.
* - Else if `markerRowsBySpace` lacks an entry → no violation here;
* the live-DB compare done outside this helper is where the
* absence shows up.
* - Else compare marker hash / invariants vs. on-disk head hash /
* invariants → `hashMismatch` / `invariantsMismatch` on drift.
* - For every contract-space dir on disk that is not in `loadedSpaces` →
* `orphanSpaceDir`.
* - For every marker row whose `space` is not in `loadedSpaces` →
* `orphanMarker`. The app-space marker is always loaded (`'app'` is
* in `loadedSpaces` by definition).
*
* Output is deterministic: violations are sorted first by `kind`
* (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
* `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
* passing equivalent inputs see byte-identical violation lists.
*
* Synchronous, pure, no I/O. **Does not import the extension descriptor**
* (the inputs are pre-resolved by the caller); the verifier reads only
* the user repo, not `node_modules`.
*/
function verifyContractSpaces(inputs) {
const violations = [];
for (const spaceId of [...inputs.loadedSpaces].sort()) {
if (spaceId === APP_SPACE_ID) continue;
if (!inputs.spaceDirsOnDisk.includes(spaceId)) {
violations.push({
kind: "declaredButUnmigrated",
spaceId,
remediation: `Extension '${spaceId}' is declared in extensionPacks but has not been emitted; run \`prisma-next migrate\`.`
});
continue;
}
const head = inputs.headRefsBySpace.get(spaceId);
const marker = inputs.markerRowsBySpace.get(spaceId);
if (!head || !marker) continue;
if (head.hash !== marker.hash) {
violations.push({
kind: "hashMismatch",
spaceId,
priorHeadHash: head.hash,
markerHash: marker.hash,
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.`
});
continue;
}
const onDiskInvariants = [...head.invariants].sort();
const markerInvariants = new Set(marker.invariants);
const missing = onDiskInvariants.filter((id) => !markerInvariants.has(id));
if (missing.length > 0) violations.push({
kind: "invariantsMismatch",
spaceId,
onDiskInvariants,
markerInvariants: [...marker.invariants].sort(),
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.`
});
}
for (const dir of [...inputs.spaceDirsOnDisk].sort()) if (!inputs.loadedSpaces.has(dir)) violations.push({
kind: "orphanSpaceDir",
spaceId: dir,
remediation: `Orphan contract-space directory \`${join("migrations", dir)}/\` for an extension not in extensionPacks; remove the directory or re-add the extension.`
});
for (const space of [...inputs.markerRowsBySpace.keys()].sort()) if (!inputs.loadedSpaces.has(space)) violations.push({
kind: "orphanMarker",
spaceId: space,
remediation: `Orphan marker row for space '${space}' (no longer in extensionPacks); remediation: manually delete the row from \`prisma_contract.marker\`.`
});
if (violations.length === 0) return { ok: true };
const kindOrder = {
declaredButUnmigrated: 0,
orphanMarker: 1,
orphanSpaceDir: 2,
hashMismatch: 3,
invariantsMismatch: 4
};
violations.sort((a, b) => {
const k = kindOrder[a.kind] - kindOrder[b.kind];
if (k !== 0) return k;
if (a.spaceId < b.spaceId) return -1;
if (a.spaceId > b.spaceId) return 1;
return 0;
});
return {
ok: false,
violations
};
}
//#endregion
//#region src/read-contract-space-contract.ts
function hasErrnoCode(error, code) {
return error instanceof Error && error.code === code;
}
/**
* Read the on-disk contract value for a contract space
* (`<projectMigrationsDir>/<spaceId>/contract.json`). Returns the parsed
* JSON value as `unknown` — callers that need a typed contract validate
* via their family's `deserializeContract` to surface schema issues.
*
* Companion to {@link import('./read-contract-space-head-ref').readContractSpaceHeadRef}
* — same ENOENT-throws / corrupt-file-error semantics. Returns the
* canonical-JSON value the framework wrote during emit, so re-running
* this helper across machines / runs yields a byte-identical value.
*/
async function readContractSpaceContract(projectMigrationsDir, spaceId) {
assertValidSpaceId(spaceId);
const filePath = join(projectMigrationsDir, spaceId, "contract.json");
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile("contract.json", join(projectMigrationsDir, spaceId));
throw error;
}
try {
return JSON.parse(raw);
} catch (e) {
throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));
}
}
//#endregion
export { APP_SPACE_ID as a, assertValidSpaceId as c, spaceRefsDirectory as d, readContractSpaceHeadRef as i, isValidSpaceId as l, listContractSpaceDirectories as n, RESERVED_SPACE_SUBDIR_NAMES as o, verifyContractSpaces as r, SPACE_REFS_DIRNAME as s, readContractSpaceContract as t, spaceMigrationDirectory as u };
//# sourceMappingURL=read-contract-space-contract-DBW4-T5r.mjs.map
{"version":3,"file":"read-contract-space-contract-DBW4-T5r.mjs","names":["hasErrnoCode","hasErrnoCode"],"sources":["../src/space-layout.ts","../src/read-contract-space-head-ref.ts","../src/verify-contract-spaces.ts","../src/read-contract-space-contract.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 { 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 { 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 spaces;\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","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":";;;;;;;;;;;AAoBA,MAAM,mBAAmB;AAEzB,SAAgB,eAAe,SAA0C;CACvE,OAAO,iBAAiB,KAAK,OAAO;AACtC;AAEA,SAAgB,mBAAmB,SAAkD;CACnF,IAAI,CAAC,eAAe,OAAO,GACzB,MAAM,oBAAoB,OAAO;AAErC;;;;;;;;;;;;;AAcA,SAAgB,wBAAwB,sBAA8B,SAAyB;CAC7F,mBAAmB,OAAO;CAC1B,OAAO,KAAK,sBAAsB,OAAO;AAC3C;;;;;;;;;AAUA,MAAa,qBAAqB;;;;;;;;;AAUlC,MAAa,8CAAmD,IAAI,IAAI,CAAC,kBAAkB,CAAC;;;;;;;AAQ5F,SAAgB,mBAAmB,oBAAoC;CACrE,OAAO,KAAK,oBAAoB,kBAAkB;AACpD;;;ACrEA,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,SAASC,eAAa,OAAgB,MAAuB;CAC3D,OAAO,iBAAiB,SAAU,MAA4B,SAAS;AACzE;;;;;;;;;;;;;;;;;AAkBA,eAAsB,6BACpB,sBAC4B;CAC5B,IAAI;CACJ,IAAI;EAEF,WAAU,MADY,QAAQ,sBAAsB,EAAE,eAAe,KAAK,CAAC,EAAA,CACzD,KAAK,OAAO;GAAE,MAAM,EAAE;GAAM,aAAa,EAAE,YAAY;EAAE,EAAE;CAC/E,SAAS,OAAO;EACd,IAAIA,eAAa,OAAO,QAAQ,GAC9B,OAAO,CAAC;EAEV,MAAM;CACR;CAEA,MAAM,kBAAkB,QACrB,QAAQ,MAAM,EAAE,WAAW,CAAC,CAC5B,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,QAAQ,SAAS,CAAC,KAAK,WAAW,GAAG,CAAC,CAAC,CACvC,KAAK;CAgBR,QAAO,MAdsB,QAAQ,IACnC,gBAAgB,IAAI,OAAO,SAAS;EAClC,IAAI;GACF,MAAM,KAAK,KAAK,sBAAsB,MAAM,aAAa,CAAC;GAC1D,OAAO;IAAE;IAAM,gBAAgB;GAAK;EACtC,SAAS,OAAO;GACd,IAAIA,eAAa,OAAO,QAAQ,GAC9B,OAAO;IAAE;IAAM,gBAAgB;GAAM;GAEvC,MAAM;EACR;CACF,CAAC,CACH,EAAA,CAEsB,QAAQ,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;AAC1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0HA,SAAgB,qBACd,QAC4B;CAC5B,MAAM,aAAuC,CAAC;CAE9C,KAAK,MAAM,WAAW,CAAC,GAAG,OAAO,YAAY,CAAC,CAAC,KAAK,GAAG;EACrD,IAAI,YAAY,cAAc;EAE9B,IAAI,CAAC,OAAO,gBAAgB,SAAS,OAAO,GAAG;GAC7C,WAAW,KAAK;IACd,MAAM;IACN;IACA,aAAa,cAAc,QAAQ;GACrC,CAAC;GACD;EACF;EAEA,MAAM,OAAO,OAAO,gBAAgB,IAAI,OAAO;EAC/C,MAAM,SAAS,OAAO,kBAAkB,IAAI,OAAO;EACnD,IAAI,CAAC,QAAQ,CAAC,QACZ;EAGF,IAAI,KAAK,SAAS,OAAO,MAAM;GAC7B,WAAW,KAAK;IACd,MAAM;IACN;IACA,eAAe,KAAK;IACpB,YAAY,OAAO;IACnB,aAAa,yBAAyB,QAAQ,gBAAgB,OAAO,KAAK,oBAAoB,KAAK,cAAc,SAAS,eAAe,EAAE,eAAe,KAAK,KAAK;GACtK,CAAC;GACD;EACF;EAEA,MAAM,mBAAmB,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,KAAK;EACnD,MAAM,mBAAmB,IAAI,IAAI,OAAO,UAAU;EAClD,MAAM,UAAU,iBAAiB,QAAQ,OAAO,CAAC,iBAAiB,IAAI,EAAE,CAAC;EACzE,IAAI,QAAQ,SAAS,GACnB,WAAW,KAAK;GACd,MAAM;GACN;GACA;GACA,kBAAkB,CAAC,GAAG,OAAO,UAAU,CAAC,CAAC,KAAK;GAC9C,aAAa,yBAAyB,QAAQ,2BAA2B,QAAQ,KAAK,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;EAC5H,CAAC;CAEL;CAEA,KAAK,MAAM,OAAO,CAAC,GAAG,OAAO,eAAe,CAAC,CAAC,KAAK,GACjD,IAAI,CAAC,OAAO,aAAa,IAAI,GAAG,GAC9B,WAAW,KAAK;EACd,MAAM;EACN,SAAS;EACT,aAAa,qCAAqC,KAAK,cAAc,GAAG,EAAE;CAC5E,CAAC;CAIL,KAAK,MAAM,SAAS,CAAC,GAAG,OAAO,kBAAkB,KAAK,CAAC,CAAC,CAAC,KAAK,GAC5D,IAAI,CAAC,OAAO,aAAa,IAAI,KAAK,GAChC,WAAW,KAAK;EACd,MAAM;EACN,SAAS;EACT,aAAa,gCAAgC,MAAM;CACrD,CAAC;CAIL,IAAI,WAAW,WAAW,GACxB,OAAO,EAAE,IAAI,KAAK;CAGpB,MAAM,YAA4D;EAChE,uBAAuB;EACvB,cAAc;EACd,gBAAgB;EAChB,cAAc;EACd,oBAAoB;CACtB;CAEA,WAAW,MAAM,GAAG,MAAM;EACxB,MAAM,IAAI,UAAU,EAAE,QAAQ,UAAU,EAAE;EAC1C,IAAI,MAAM,GAAG,OAAO;EACpB,IAAI,EAAE,UAAU,EAAE,SAAS,OAAO;EAClC,IAAI,EAAE,UAAU,EAAE,SAAS,OAAO;EAClC,OAAO;CACT,CAAC;CAED,OAAO;EAAE,IAAI;EAAO;CAAW;AACjC;;;AC1QA,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"}
import { _ as errorInvalidRefValue, g as errorInvalidRefName, h as errorInvalidRefFile, t as MigrationToolsError } from "./errors-DIS_dcFJ.mjs";
import { dirname, join, relative } from "pathe";
import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
import { type } from "arktype";
//#region src/refs.ts
/**
* The system head ref lives at `refs/head.json`. It is read (and its
* corruption judged) through `readContractSpaceHeadRef`, not as a
* user-authored ref, so {@link readRefsTolerant} excludes it.
*/
const HEAD_REF_NAME = "head";
const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
const REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;
function validateRefName(name) {
if (name.length === 0) return false;
if (name.includes("..")) return false;
if (name.includes("//")) return false;
if (name.startsWith(".")) return false;
return REF_NAME_PATTERN.test(name);
}
function validateRefValue(value) {
return REF_VALUE_PATTERN.test(value);
}
const RefEntrySchema = type({
hash: "string",
invariants: "string[]"
}).narrow((entry, ctx) => {
if (!validateRefValue(entry.hash)) return ctx.mustBe(`a valid contract hash (got "${entry.hash}")`);
return true;
});
function refFilePath(refsDir, name) {
return join(refsDir, `${name}.json`);
}
function refNameFromPath(refsDir, filePath) {
return relative(refsDir, filePath).replace(/\.json$/, "");
}
async function readRef(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const filePath = refFilePath(refsDir, name);
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
why: `No ref file found at "${filePath}".`,
fix: `Create the ref with: prisma-next ref set ${name} <hash>`,
details: {
refName: name,
filePath
}
});
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
}
const result = RefEntrySchema(parsed);
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
return result;
}
async function readRefs(refsDir) {
let entries;
try {
entries = await readdir(refsDir, {
recursive: true,
encoding: "utf-8"
});
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") return {};
throw error;
}
const jsonFiles = entries.filter((entry) => entry.endsWith(".json") && !entry.endsWith(".contract.json"));
const refs = {};
for (const jsonFile of jsonFiles) {
const filePath = join(refsDir, jsonFile);
const name = refNameFromPath(refsDir, filePath);
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
const code = error instanceof Error ? error.code : void 0;
if (code === "ENOENT" || code === "EISDIR") continue;
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
}
const result = RefEntrySchema(parsed);
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
refs[name] = result;
}
return refs;
}
/**
* Read a space's user-authored refs without ever throwing on disk
* content. A ref whose JSON is unparseable or whose shape fails
* {@link RefEntrySchema} is omitted from `refs` and reported as a
* {@link RefLoadProblem}; the remaining well-formed refs are still
* returned. A missing `refs/` directory yields no refs and no problems.
*
* `refs/head.json` is deliberately skipped here: the system head ref is
* read through `readContractSpaceHeadRef` (which validates head-ref
* shape, distinct from the strict user-ref hash grammar), so it is judged
* there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,
* …) still propagate — only parse / schema problems are made tolerant.
*/
async function readRefsTolerant(refsDir) {
let entries;
try {
entries = await readdir(refsDir, {
recursive: true,
encoding: "utf-8"
});
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") return {
refs: {},
problems: []
};
throw error;
}
const jsonFiles = entries.filter((entry) => entry.endsWith(".json") && !entry.endsWith(".contract.json") && entry !== `head.json`);
const refs = {};
const problems = [];
for (const jsonFile of jsonFiles) {
const filePath = join(refsDir, jsonFile);
const name = refNameFromPath(refsDir, filePath);
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
const code = error instanceof Error ? error.code : void 0;
if (code === "ENOENT" || code === "EISDIR") continue;
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
problems.push({
refName: name,
detail: e instanceof Error ? e.message : String(e)
});
continue;
}
const result = RefEntrySchema(parsed);
if (result instanceof type.errors) {
problems.push({
refName: name,
detail: result.summary
});
continue;
}
refs[name] = result;
}
return {
refs,
problems
};
}
async function writeRef(refsDir, name, entry) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
if (!validateRefValue(entry.hash)) throw errorInvalidRefValue(entry.hash);
const filePath = refFilePath(refsDir, name);
const dir = dirname(filePath);
await mkdir(dir, { recursive: true });
const tmpPath = join(dir, `.${name.split("/").pop()}.json.${Date.now()}.tmp`);
await writeFile(tmpPath, `${JSON.stringify({
hash: entry.hash,
invariants: [...entry.invariants]
}, null, 2)}\n`);
await rename(tmpPath, filePath);
}
async function deleteRef(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const filePath = refFilePath(refsDir, name);
try {
await unlink(filePath);
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
why: `No ref file found at "${filePath}".`,
fix: "Run `prisma-next ref list` to see available refs.",
details: {
refName: name,
filePath
}
});
throw error;
}
let dir = dirname(filePath);
while (dir !== refsDir && dir.startsWith(refsDir)) try {
await rmdir(dir);
dir = dirname(dir);
} catch (error) {
const code = error instanceof Error ? error.code : void 0;
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") break;
throw error;
}
}
/**
* Index user-authored refs by the contract hash each ref points at.
* Each bucket is sorted lex-asc for deterministic output.
*/
function refsByContractHash(refs) {
const byHash = /* @__PURE__ */ new Map();
for (const [name, entry] of Object.entries(refs)) {
const bucket = byHash.get(entry.hash);
if (bucket) bucket.push(name);
else byHash.set(entry.hash, [name]);
}
for (const bucket of byHash.values()) bucket.sort();
return byHash;
}
/**
* Read `migrations/<space>/refs/*.json` and index by destination hash.
* Returns an empty map when the refs directory does not exist.
*/
async function resolveRefsByContractHash(refsDir) {
return refsByContractHash(await readRefs(refsDir));
}
function resolveRef(refs, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
if (!Object.hasOwn(refs, name)) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
why: `No ref named "${name}" exists.`,
fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: prisma-next ref set ${name} <hash>`,
details: {
refName: name,
availableRefs: Object.keys(refs)
}
});
return refs[name];
}
//#endregion
export { readRefsTolerant as a, resolveRefsByContractHash as c, writeRef as d, readRefs as i, validateRefName as l, deleteRef as n, refsByContractHash as o, readRef as r, resolveRef as s, HEAD_REF_NAME as t, validateRefValue as u };
//# sourceMappingURL=refs-CDIHeNcC.mjs.map
{"version":3,"file":"refs-CDIHeNcC.mjs","names":[],"sources":["../src/refs.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { dirname, join, relative } from 'pathe';\nimport {\n errorInvalidRefFile,\n errorInvalidRefName,\n errorInvalidRefValue,\n MigrationToolsError,\n} from './errors';\n\nexport interface RefEntry {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\nexport type Refs = Readonly<Record<string, RefEntry>>;\n\n/**\n * The system head ref lives at `refs/head.json`. It is read (and its\n * corruption judged) through `readContractSpaceHeadRef`, not as a\n * user-authored ref, so {@link readRefsTolerant} excludes it.\n */\nexport const HEAD_REF_NAME = 'head';\n\n/**\n * A single ref file that exists on disk but cannot be turned into a\n * {@link RefEntry} (unparseable JSON or schema-invalid content). The ref\n * is omitted from the result; the problem is surfaced for the integrity\n * layer to report as `refUnreadable` rather than aborting the load.\n */\nexport interface RefLoadProblem {\n readonly refName: string;\n readonly detail: string;\n}\n\nexport interface TolerantRefsResult {\n readonly refs: Refs;\n readonly problems: readonly RefLoadProblem[];\n}\n\nconst REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;\nconst REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;\n\nexport function validateRefName(name: string): boolean {\n if (name.length === 0) return false;\n if (name.includes('..')) return false;\n if (name.includes('//')) return false;\n if (name.startsWith('.')) return false;\n return REF_NAME_PATTERN.test(name);\n}\n\nexport function validateRefValue(value: string): boolean {\n return REF_VALUE_PATTERN.test(value);\n}\n\nconst RefEntrySchema = type({\n hash: 'string',\n invariants: 'string[]',\n}).narrow((entry, ctx) => {\n if (!validateRefValue(entry.hash))\n return ctx.mustBe(`a valid contract hash (got \"${entry.hash}\")`);\n return true;\n});\n\nfunction refFilePath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.json`);\n}\n\nfunction refNameFromPath(refsDir: string, filePath: string): string {\n const rel = relative(refsDir, filePath);\n return rel.replace(/\\.json$/, '');\n}\n\nexport async function readRef(refsDir: string, name: string): Promise<RefEntry> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const filePath = refFilePath(refsDir, name);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref file found at \"${filePath}\".`,\n fix: `Create the ref with: prisma-next ref set ${name} <hash>`,\n details: { refName: name, filePath },\n });\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function readRefs(refsDir: string): Promise<Refs> {\n let entries: string[];\n try {\n entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return {};\n }\n throw error;\n }\n\n const jsonFiles = entries.filter(\n (entry) => entry.endsWith('.json') && !entry.endsWith('.contract.json'),\n );\n const refs: Record<string, RefEntry> = {};\n\n for (const jsonFile of jsonFiles) {\n const filePath = join(refsDir, jsonFile);\n const name = refNameFromPath(refsDir, filePath);\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT) and\n // benign EISDIR if a directory happens to end in `.json`. Anything else\n // (EACCES, EIO, EMFILE, …) is a real failure and propagates so the CLI\n // surfaces it rather than silently dropping the ref.\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOENT' || code === 'EISDIR') {\n continue;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n refs[name] = result;\n }\n\n return refs;\n}\n\n/**\n * Read a space's user-authored refs without ever throwing on disk\n * content. A ref whose JSON is unparseable or whose shape fails\n * {@link RefEntrySchema} is omitted from `refs` and reported as a\n * {@link RefLoadProblem}; the remaining well-formed refs are still\n * returned. A missing `refs/` directory yields no refs and no problems.\n *\n * `refs/head.json` is deliberately skipped here: the system head ref is\n * read through `readContractSpaceHeadRef` (which validates head-ref\n * shape, distinct from the strict user-ref hash grammar), so it is judged\n * there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,\n * …) still propagate — only parse / schema problems are made tolerant.\n */\nexport async function readRefsTolerant(refsDir: string): Promise<TolerantRefsResult> {\n let entries: string[];\n try {\n entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return { refs: {}, problems: [] };\n }\n throw error;\n }\n\n const jsonFiles = entries.filter(\n (entry) =>\n entry.endsWith('.json') &&\n !entry.endsWith('.contract.json') &&\n entry !== `${HEAD_REF_NAME}.json`,\n );\n const refs: Record<string, RefEntry> = {};\n const problems: RefLoadProblem[] = [];\n\n for (const jsonFile of jsonFiles) {\n const filePath = join(refsDir, jsonFile);\n const name = refNameFromPath(refsDir, filePath);\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT)\n // and benign EISDIR if a directory happens to end in `.json`.\n // Anything else (EACCES, EIO, …) is a real failure and propagates.\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOENT' || code === 'EISDIR') {\n continue;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n problems.push({ refName: name, detail: e instanceof Error ? e.message : String(e) });\n continue;\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n problems.push({ refName: name, detail: result.summary });\n continue;\n }\n\n refs[name] = result;\n }\n\n return { refs, problems };\n}\n\nexport async function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n if (!validateRefValue(entry.hash)) {\n throw errorInvalidRefValue(entry.hash);\n }\n\n const filePath = refFilePath(refsDir, name);\n const dir = dirname(filePath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = join(dir, `.${name.split('/').pop()}.json.${Date.now()}.tmp`);\n await writeFile(\n tmpPath,\n `${JSON.stringify({ hash: entry.hash, invariants: [...entry.invariants] }, null, 2)}\\n`,\n );\n await rename(tmpPath, filePath);\n}\n\nexport async function deleteRef(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const filePath = refFilePath(refsDir, name);\n try {\n await unlink(filePath);\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref file found at \"${filePath}\".`,\n fix: 'Run `prisma-next ref list` to see available refs.',\n details: { refName: name, filePath },\n });\n }\n throw error;\n }\n\n // Clean empty parent directories up to refsDir. Stop walking on the expected\n // \"directory has siblings\" signal (ENOTEMPTY on Linux, EEXIST on some BSDs)\n // and on ENOENT (concurrent removal). Anything else (EACCES, EIO, …) is a\n // real failure and propagates.\n let dir = dirname(filePath);\n while (dir !== refsDir && dir.startsWith(refsDir)) {\n try {\n await rmdir(dir);\n dir = dirname(dir);\n } catch (error) {\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOTEMPTY' || code === 'EEXIST' || code === 'ENOENT') {\n break;\n }\n throw error;\n }\n }\n}\n\n/**\n * Index user-authored refs by the contract hash each ref points at.\n * Each bucket is sorted lex-asc for deterministic output.\n */\nexport function refsByContractHash(refs: Refs): ReadonlyMap<string, readonly string[]> {\n const byHash = new Map<string, string[]>();\n for (const [name, entry] of Object.entries(refs)) {\n const bucket = byHash.get(entry.hash);\n if (bucket) bucket.push(name);\n else byHash.set(entry.hash, [name]);\n }\n for (const bucket of byHash.values()) {\n bucket.sort();\n }\n return byHash;\n}\n\n/**\n * Read `migrations/<space>/refs/*.json` and index by destination hash.\n * Returns an empty map when the refs directory does not exist.\n */\nexport async function resolveRefsByContractHash(\n refsDir: string,\n): Promise<ReadonlyMap<string, readonly string[]>> {\n return refsByContractHash(await readRefs(refsDir));\n}\n\nexport function resolveRef(refs: Refs, name: string): RefEntry {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n // Object.hasOwn gate: plain-object `refs` would otherwise let\n // `refs['constructor']` return Object.prototype.constructor and bypass the\n // UNKNOWN_REF throw. validateRefName accepts `\"constructor\"` as a name shape.\n if (!Object.hasOwn(refs, name)) {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref named \"${name}\" exists.`,\n fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next ref set ${name} <hash>`,\n details: { refName: name, availableRefs: Object.keys(refs) },\n });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: Object.hasOwn gate above guarantees this is defined\n return refs[name]!;\n}\n"],"mappings":"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"}
import { g as errorInvalidRefName, h as errorInvalidRefFile, t as MigrationToolsError } from "./errors-DIS_dcFJ.mjs";
import { d as writeRef, l as validateRefName, n as deleteRef } from "./refs-CDIHeNcC.mjs";
import { basename, dirname, join } from "pathe";
import { access, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { type } from "arktype";
import { randomBytes } from "node:crypto";
import { canonicalizeJson } from "@prisma-next/framework-components/utils";
//#region src/refs/snapshot.ts
const ContractIrSchema = type({
targetFamily: "string",
target: "string",
profileHash: "string",
storage: type({ storageHash: "string" }),
domain: type({ namespaces: "object" })
});
function hasErrnoCode(error, code) {
return error instanceof Error && error.code === code;
}
function snapshotJsonPath(refsDir, name) {
return join(refsDir, `${name}.contract.json`);
}
function snapshotDtsPath(refsDir, name) {
return join(refsDir, `${name}.contract.d.ts`);
}
function tmpPathFor(finalPath) {
return join(dirname(finalPath), `.${basename(finalPath)}.${Date.now()}-${randomBytes(4).toString("hex")}.tmp`);
}
async function atomicWriteFile(finalPath, content) {
await mkdir(dirname(finalPath), { recursive: true });
const tmpPath = tmpPathFor(finalPath);
await writeFile(tmpPath, content);
await rename(tmpPath, finalPath);
}
async function unlinkIfExists(filePath) {
try {
await unlink(filePath);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return;
throw error;
}
}
function parseContractSnapshotJson(filePath, raw) {
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
}
const result = ContractIrSchema(parsed);
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
return result;
}
async function writeRefSnapshot(refsDir, name, snapshot) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const jsonPath = snapshotJsonPath(refsDir, name);
const dtsPath = snapshotDtsPath(refsDir, name);
const jsonContent = `${canonicalizeJson(snapshot.contract)}\n`;
const dtsContent = snapshot.contractDts.endsWith("\n") ? snapshot.contractDts : `${snapshot.contractDts}\n`;
try {
await atomicWriteFile(jsonPath, jsonContent);
} catch (error) {
await unlinkIfExists(jsonPath);
throw error;
}
try {
await atomicWriteFile(dtsPath, dtsContent);
} catch (error) {
await unlinkIfExists(jsonPath);
await unlinkIfExists(dtsPath);
throw error;
}
}
async function readRefSnapshot(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const jsonPath = snapshotJsonPath(refsDir, name);
const dtsPath = snapshotDtsPath(refsDir, name);
let raw;
try {
raw = await readFile(jsonPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return null;
throw error;
}
const contract = parseContractSnapshotJson(jsonPath, raw);
let contractDts;
try {
contractDts = await readFile(dtsPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorInvalidRefFile(dtsPath, "Missing paired contract.d.ts snapshot file");
throw error;
}
return {
contract,
contractDts
};
}
async function deleteRefSnapshot(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
await unlinkIfExists(snapshotJsonPath(refsDir, name));
await unlinkIfExists(snapshotDtsPath(refsDir, name));
}
async function writeRefPaired(refsDir, name, entry, snapshot) {
await writeRefSnapshot(refsDir, name, snapshot);
try {
await writeRef(refsDir, name, entry);
} catch (writeError) {
try {
await deleteRefSnapshot(refsDir, name);
} catch {}
throw writeError;
}
}
function isUnknownRefError(error) {
return MigrationToolsError.is(error) && error.code === "MIGRATION.UNKNOWN_REF";
}
async function snapshotFilesExist(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];
return (await Promise.allSettled(paths.map((filePath) => access(filePath)))).some((result) => result.status === "fulfilled");
}
async function deleteRefPaired(refsDir, name) {
if (await snapshotFilesExist(refsDir, name)) {
try {
await deleteRef(refsDir, name);
} catch (error) {
if (!isUnknownRefError(error)) throw error;
}
await deleteRefSnapshot(refsDir, name);
return;
}
await deleteRef(refsDir, name);
await deleteRefSnapshot(refsDir, name);
}
//#endregion
export { writeRefSnapshot as a, writeRefPaired as i, deleteRefSnapshot as n, readRefSnapshot as r, deleteRefPaired as t };
//# sourceMappingURL=snapshot-DiE5uFFJ.mjs.map
{"version":3,"file":"snapshot-DiE5uFFJ.mjs","names":[],"sources":["../src/refs/snapshot.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport { errorInvalidRefFile, errorInvalidRefName, MigrationToolsError } from '../errors';\nimport { deleteRef, type RefEntry, validateRefName, writeRef } from '../refs';\n\nexport interface ContractIR {\n readonly contract: unknown;\n readonly contractDts: string;\n}\n\nconst ContractIrSchema = type({\n targetFamily: 'string',\n target: 'string',\n profileHash: 'string',\n storage: type({\n storageHash: 'string',\n }),\n domain: type({\n namespaces: 'object',\n }),\n});\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nfunction snapshotJsonPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.json`);\n}\n\nfunction snapshotDtsPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.d.ts`);\n}\n\nfunction tmpPathFor(finalPath: string): string {\n const dir = dirname(finalPath);\n const fileName = basename(finalPath);\n return join(dir, `.${fileName}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`);\n}\n\nasync function atomicWriteFile(finalPath: string, content: string): Promise<void> {\n const dir = dirname(finalPath);\n await mkdir(dir, { recursive: true });\n const tmpPath = tmpPathFor(finalPath);\n await writeFile(tmpPath, content);\n await rename(tmpPath, finalPath);\n}\n\nasync function unlinkIfExists(filePath: string): Promise<void> {\n try {\n await unlink(filePath);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nfunction parseContractSnapshotJson(filePath: string, raw: string): unknown {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = ContractIrSchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function writeRefSnapshot(\n refsDir: string,\n name: string,\n snapshot: ContractIR,\n): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n const jsonContent = `${canonicalizeJson(snapshot.contract)}\\n`;\n const dtsContent = snapshot.contractDts.endsWith('\\n')\n ? snapshot.contractDts\n : `${snapshot.contractDts}\\n`;\n\n try {\n await atomicWriteFile(jsonPath, jsonContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n throw error;\n }\n\n try {\n await atomicWriteFile(dtsPath, dtsContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n await unlinkIfExists(dtsPath);\n throw error;\n }\n}\n\nexport async function readRefSnapshot(refsDir: string, name: string): Promise<ContractIR | null> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n\n let raw: string;\n try {\n raw = await readFile(jsonPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n const contract = parseContractSnapshotJson(jsonPath, raw);\n\n let contractDts: string;\n try {\n contractDts = await readFile(dtsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorInvalidRefFile(dtsPath, 'Missing paired contract.d.ts snapshot file');\n }\n throw error;\n }\n\n return { contract, contractDts };\n}\n\nexport async function deleteRefSnapshot(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n await unlinkIfExists(snapshotJsonPath(refsDir, name));\n await unlinkIfExists(snapshotDtsPath(refsDir, name));\n}\n\nexport async function writeRefPaired(\n refsDir: string,\n name: string,\n entry: RefEntry,\n snapshot: ContractIR,\n): Promise<void> {\n await writeRefSnapshot(refsDir, name, snapshot);\n try {\n await writeRef(refsDir, name, entry);\n } catch (writeError) {\n try {\n await deleteRefSnapshot(refsDir, name);\n } catch {\n // Rollback failure is secondary; preserve the original writeRef error.\n }\n throw writeError;\n }\n}\n\nfunction isUnknownRefError(error: unknown): boolean {\n return MigrationToolsError.is(error) && error.code === 'MIGRATION.UNKNOWN_REF';\n}\n\nasync function snapshotFilesExist(refsDir: string, name: string): Promise<boolean> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];\n const checks = await Promise.allSettled(paths.map((filePath) => access(filePath)));\n return checks.some((result) => result.status === 'fulfilled');\n}\n\nexport async function deleteRefPaired(refsDir: string, name: string): Promise<void> {\n if (await snapshotFilesExist(refsDir, name)) {\n try {\n await deleteRef(refsDir, name);\n } catch (error) {\n if (!isUnknownRefError(error)) {\n throw error;\n }\n }\n await deleteRefSnapshot(refsDir, name);\n return;\n }\n\n await deleteRef(refsDir, name);\n await deleteRefSnapshot(refsDir, name);\n}\n"],"mappings":"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"}
//#region src/verify-contract-spaces.d.ts
/**
* List the per-space subdirectories under
* `<projectRoot>/migrations/`. Returns space-id directory names (sorted
* alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
* does **not** contain a `migration.json` manifest. The manifest is the
* structural marker of a user-authored migration directory (see
* `readMigrationsDir` in `./io`); directory names themselves belong to
* the user and are not part of the contract.
*
* Returns `[]` if the migrations directory does not exist (greenfield
* project).
*
* Reads only the user's repo. **No descriptor import.** The caller
* (verifier) feeds the result into {@link verifyContractSpaces} alongside
* the loaded-space set and the marker rows.
*/
declare function listContractSpaceDirectories(projectMigrationsDir: string): Promise<readonly string[]>;
/**
* On-disk head value (`(hash, invariants)`) for one contract space.
* The verifier compares this against the marker row for the same space
* to detect drift between the user-emitted artefacts and the live DB
* marker.
*/
interface ContractSpaceHeadRecord {
readonly hash: string;
readonly invariants: readonly string[];
}
/**
* Marker row read from `prisma_contract.marker` (one per `space`).
* Caller resolves these via the family runtime's marker reader before
* invoking {@link verifyContractSpaces}.
*/
interface SpaceMarkerRecord {
readonly hash: string;
readonly invariants: readonly string[];
}
interface VerifyContractSpacesInputs {
/**
* Set of contract spaces the project declares: `'app'` plus each
* extension space in `extensionPacks`. The caller's discovery path
* never reads the extension descriptor module — it walks the
* `extensionPacks` configuration in `prisma-next.config.ts` for the
* space ids.
*/
readonly loadedSpaces: ReadonlySet<string>;
/**
* Per-space subdirectories observed under
* `<projectRoot>/migrations/`. Resolved via
* {@link listContractSpaceDirectories}.
*/
readonly spaceDirsOnDisk: readonly string[];
/**
* Head ref per space, keyed by space id. Caller reads
* `<projectRoot>/migrations/<space-id>/contract.json` and
* `<projectRoot>/migrations/<space-id>/refs/head.json` to construct
* this map. Spaces with no contract-space dir on disk simply omit a
* map entry.
*/
readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;
/**
* Marker rows keyed by `space`. Caller reads them from the
* `prisma_contract.marker` table.
*/
readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;
}
type SpaceVerifierViolation = {
readonly kind: 'declaredButUnmigrated';
readonly spaceId: string;
readonly remediation: string;
} | {
readonly kind: 'orphanMarker';
readonly spaceId: string;
readonly remediation: string;
} | {
readonly kind: 'orphanSpaceDir';
readonly spaceId: string;
readonly remediation: string;
} | {
readonly kind: 'hashMismatch';
readonly spaceId: string;
readonly priorHeadHash: string;
readonly markerHash: string;
readonly remediation: string;
} | {
readonly kind: 'invariantsMismatch';
readonly spaceId: string;
readonly onDiskInvariants: readonly string[];
readonly markerInvariants: readonly string[];
readonly remediation: string;
};
type VerifyContractSpacesResult = {
readonly ok: true;
} | {
readonly ok: false;
readonly violations: readonly SpaceVerifierViolation[];
};
/**
* Pure structural verifier for the per-space mechanism. Aggregates the
* three orphan / missing checks plus per-space hash and invariant
* comparison.
*
* Algorithm:
*
* - For every extension space declared in `loadedSpaces` (`'app'`
* excluded — the per-space verifier is scoped to extension spaces;
* the app is verified through the aggregate path):
* - If no contract-space dir on disk → `declaredButUnmigrated`.
* - Else if `markerRowsBySpace` lacks an entry → no violation here;
* the live-DB compare done outside this helper is where the
* absence shows up.
* - Else compare marker hash / invariants vs. on-disk head hash /
* invariants → `hashMismatch` / `invariantsMismatch` on drift.
* - For every contract-space dir on disk that is not in `loadedSpaces` →
* `orphanSpaceDir`.
* - For every marker row whose `space` is not in `loadedSpaces` →
* `orphanMarker`. The app-space marker is always loaded (`'app'` is
* in `loadedSpaces` by definition).
*
* Output is deterministic: violations are sorted first by `kind`
* (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
* `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
* passing equivalent inputs see byte-identical violation lists.
*
* Synchronous, pure, no I/O. **Does not import the extension descriptor**
* (the inputs are pre-resolved by the caller); the verifier reads only
* the user repo, not `node_modules`.
*/
declare function verifyContractSpaces(inputs: VerifyContractSpacesInputs): VerifyContractSpacesResult;
//#endregion
export { VerifyContractSpacesResult as a, VerifyContractSpacesInputs as i, SpaceMarkerRecord as n, listContractSpaceDirectories as o, SpaceVerifierViolation as r, verifyContractSpaces as s, ContractSpaceHeadRecord as t };
//# sourceMappingURL=verify-contract-spaces-zYlAhXOK.d.mts.map
{"version":3,"file":"verify-contract-spaces-zYlAhXOK.d.mts","names":[],"sources":["../src/verify-contract-spaces.ts"],"mappings":";;AAyBA;;;;AAEU;AAyCV;;;;AAEqB;AAQrB;;;;AAEqB;iBAvDC,4BAAA,CACpB,oBAAA,WACC,OAAO;;;;;;;UAyCO,uBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;;;;;;UAQJ,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;AAAA,UAGJ,0BAAA;EAiCL;;;;;;;EAAA,SAzBD,YAAA,EAAc,WAAA;EAiCV;;;;;EAAA,SA1BJ,eAAA;EAoCI;;;;;;;EAAA,SA3BJ,eAAA,EAAiB,WAAA,SAAoB,uBAAA;EAqCjC;;AAAW;AAG1B;EAHe,SA/BJ,iBAAA,EAAmB,WAAA,SAAoB,iBAAA;AAAA;AAAA,KAGtC,sBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,gBAAA;EAAA,SACA,gBAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGH,0BAAA;EAAA,SACG,EAAA;AAAA;EAAA,SACA,EAAA;EAAA,SAAoB,UAAA,WAAqB,sBAAsB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC9D,oBAAA,CACd,MAAA,EAAQ,0BAAA,GACP,0BAA0B"}
import type { AggregateMigrationEdgeRef } from './planner-types';
export function buildFabricatedMigrationEdge(args: {
readonly currentMarkerStorageHash: string | null | undefined;
readonly destinationStorageHash: string;
readonly operationCount: number;
readonly destinationContractJson?: unknown;
}): AggregateMigrationEdgeRef {
return {
dirName: '',
migrationHash: args.destinationStorageHash,
from: args.currentMarkerStorageHash ?? '',
to: args.destinationStorageHash,
operationCount: args.operationCount,
...(args.destinationContractJson !== undefined
? { destinationContractJson: args.destinationContractJson }
: {}),
};
}
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
import type {
ControlAdapterInstance,
ControlFamilyInstance,
MigrationOperationPolicy,
MigrationPlan,
MigrationPlannerConflict,
MigrationPlannerResult,
SchemaOwnership,
TargetMigrationsCapability,
} from '@prisma-next/framework-components/control';
import { buildFabricatedMigrationEdge } from '../fabricated-migration-edge';
import type { ContractMarkerRecordLike } from '../marker-types';
import type { PerSpacePlan } from '../planner-types';
import type { AggregateContractSpace } from '../types';
export interface PlanFromDiffInputs<TFamilyId extends string, TTargetId extends string> {
readonly aggregateTargetId: string;
readonly currentMarker: ContractMarkerRecordLike | null;
readonly space: AggregateContractSpace;
/**
* Ownership oracle over the whole composition — the passive aggregate
* itself. Handed straight through to the family planner, which asks it,
* per live extra node, whether any space owns that entity; a sibling-owned
* node is left untouched, an unowned node is a genuine extra. This strategy
* runs no diff of its own and holds no ownership logic — it forwards the
* aggregate as the oracle.
*/
readonly ownership: SchemaOwnership;
readonly schemaIntrospection: unknown;
readonly adapter: ControlAdapterInstance<TFamilyId, TTargetId>;
readonly migrations: TargetMigrationsCapability<
TFamilyId,
TTargetId,
ControlFamilyInstance<TFamilyId, unknown>
>;
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
readonly operationPolicy: MigrationOperationPolicy;
}
export type PlanFromDiffOutcome =
| { readonly kind: 'ok'; readonly result: PerSpacePlan }
| { readonly kind: 'failure'; readonly conflicts: readonly MigrationPlannerConflict[] };
/**
* The {@link MigrationPlanner.plan} interface is declared as synchronous,
* but historical and test fixture call sites have always invoked it
* with `await` (see prior `db-apply-per-space.ts`). Tolerating a
* Promise here keeps existing test mocks working without changing the
* declared family SPI.
*/
type MaybeAsyncPlannerResult = MigrationPlannerResult | Promise<MigrationPlannerResult>;
/**
* Plan a migration for a single contract space from a diff against the full
* live schema, delegating to the family's `createPlanner(...).plan(...)`.
*
* The planner diffs the whole introspected schema, so it sees other contract
* spaces' tables as "extras"; the orchestration hands the planner the
* aggregate as an ownership oracle so it can ask, per extra, whether any
* space owns it — a sibling-owned table is left untouched, so the planner
* never emits a destructive drop for it, and this strategy holds no ownership
* logic of its own. The schema is never pruned before planning: cross-space
* foreign keys need every sibling table visible to the diff.
*
* The produced plan's `targetId` is set from `aggregateTargetId`
* (the aggregate's ambient target). The family's planner does not
* stamp `targetId` on the produced plan; the aggregate planner is
* the single point that knows the target.
*
* Used by:
*
* - The app space by default (CLI policy
* `ignoreGraphFor: { app.spaceId }`).
* - Any extension space whose `headRef.invariants` is empty and whose
* graph is non-empty (the all-external, zero-migration-package case is
* handled directly by the aggregate planner without calling this).
*/
export async function planFromDiff<TFamilyId extends string, TTargetId extends string>(
input: PlanFromDiffInputs<TFamilyId, TTargetId>,
): Promise<PlanFromDiffOutcome> {
const planner = input.migrations.createPlanner(input.adapter);
const plannerResult: MigrationPlannerResult = await (planner.plan({
contract: input.space.contract(),
schema: input.schemaIntrospection,
policy: input.operationPolicy,
fromContract: null,
frameworkComponents: input.frameworkComponents,
spaceId: input.space.spaceId,
ownership: input.ownership,
}) as MaybeAsyncPlannerResult);
if (plannerResult.kind === 'failure') {
return { kind: 'failure', conflicts: plannerResult.conflicts };
}
const producedPlan = plannerResult.plan;
// The family planner returns a class-instance-shaped plan whose
// `destination` / `operations` are accessors on the prototype, often
// backed by private fields. A naive spread (`{ ...producedPlan }`)
// would lose those accessors and produce a plan with
// `destination: undefined`; rebinding the prototype on a plain
// object would break private-field access. We instead wrap the plan
// in a Proxy that forwards every read except `targetId`, which is
// stamped from the aggregate's ambient target. This preserves the
// planner's class semantics while keeping the aggregate the single
// source of truth for `targetId`.
const plan: MigrationPlan = new Proxy(producedPlan, {
get(target, prop) {
if (prop === 'targetId') return input.aggregateTargetId;
// Forward `this` as the original target so prototype-bound
// private fields (#destination, #operations, …) resolve.
return Reflect.get(target, prop, target);
},
has(target, prop) {
if (prop === 'targetId') return true;
return Reflect.has(target, prop);
},
});
const destinationStorageHash = producedPlan.destination.storageHash;
const producedOps = await Promise.all(producedPlan.operations);
const destinationContract = input.space.contract();
return {
kind: 'ok',
result: {
plan,
displayOps: producedOps,
destinationContract,
strategy: 'plan-from-diff',
...(plannerResult.warnings && plannerResult.warnings.length > 0
? { warnings: plannerResult.warnings }
: {}),
migrationEdges: [
buildFabricatedMigrationEdge({
currentMarkerStorageHash: input.currentMarker?.storageHash,
destinationStorageHash,
operationCount: producedOps.length,
destinationContractJson: destinationContract,
}),
],
},
};
}
import type { MigrationPlan } from '@prisma-next/framework-components/control';
import { EMPTY_CONTRACT_HASH } from '../../constants';
import { findPathWithDecision } from '../../migration-graph';
import type { MigrationOps, OnDiskMigrationPackage } from '../../package';
import { requireHeadRef } from '../aggregate';
import type { ContractMarkerRecordLike } from '../marker-types';
import type { AggregateMigrationEdgeRef, PerSpacePlan } from '../planner-types';
import type { AggregateContractSpace } from '../types';
/**
* Outcome variants for resolving a contract space's recorded migration
* path. Mirrors
* {@link import('../../compute-extension-space-apply-path').ExtensionSpaceApplyPathOutcome}
* but operates against the contract space's lazily-reconstructed `graph()`
* instead of re-reading from disk. The aggregate planner converts
* these into {@link import('../planner-types').PlannerError}
* variants.
*/
export type ResolveRecordedPathOutcome =
| { readonly kind: 'ok'; readonly result: PerSpacePlan }
| { readonly kind: 'unreachable' }
| { readonly kind: 'unsatisfiable'; readonly missing: readonly string[] };
export interface ResolveRecordedPathInputs {
readonly aggregateTargetId: string;
readonly space: AggregateContractSpace;
readonly currentMarker: ContractMarkerRecordLike | null;
/**
* Optional ref name to decorate the resulting `PathDecision`. Used by
* `migrate` to surface the user-supplied `--to <name>` in
* structured-progress events and invariant-path error envelopes. The
* strategy itself does not interpret it.
*/
readonly refName?: string;
}
/**
* Resolve a contract space's hydrated migration graph to the path from the
* live marker to `space.headRef.hash`, covering every required invariant.
*
* Pure synchronous function — no I/O. The aggregate's loader has
* already integrity-checked every package and reconstructed the graph;
* this resolves the path by looking up ops by `migrationHash` and
* assembles a `MigrationPlan` with `targetId` set from the aggregate (no
* placeholder cast).
*
* Required invariants are computed as `headRef.invariants \ marker.invariants`
* — the marker already declares some invariants satisfied; the path
* only needs to provide the remainder. Mirrors today's
* `computeExtensionSpaceApplyPath` semantics.
*/
export function resolveRecordedPath(input: ResolveRecordedPathInputs): ResolveRecordedPathOutcome {
const { aggregateTargetId, space, currentMarker, refName } = input;
const headRef = requireHeadRef(space);
const graph = space.graph();
const packagesByMigrationHash = new Map<string, OnDiskMigrationPackage>(
space.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]),
);
const fromHash = currentMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
const markerInvariants = new Set(currentMarker?.invariants ?? []);
const required = new Set(headRef.invariants.filter((id) => !markerInvariants.has(id)));
const outcome = findPathWithDecision(graph, fromHash, headRef.hash, {
required,
...(refName !== undefined ? { refName } : {}),
});
if (outcome.kind === 'unreachable') {
return { kind: 'unreachable' };
}
if (outcome.kind === 'unsatisfiable') {
return { kind: 'unsatisfiable', missing: outcome.missing };
}
const pathOps: MigrationOps[number][] = [];
const providedInvariantsSet = new Set<string>();
const edgeRefs: AggregateMigrationEdgeRef[] = [];
for (const edge of outcome.decision.selectedPath) {
const pkg = packagesByMigrationHash.get(edge.migrationHash);
if (!pkg) {
throw new Error(
`Migration package missing for edge ${edge.migrationHash} in space "${space.spaceId}". The hydrated migration graph and packagesByMigrationHash map are out of sync — this should be unreachable; report.`,
);
}
for (const op of pkg.ops) pathOps.push(op);
for (const invariant of pkg.metadata.providedInvariants) providedInvariantsSet.add(invariant);
edgeRefs.push({
migrationHash: edge.migrationHash,
dirName: edge.dirName,
from: edge.from,
to: edge.to,
operationCount: pkg.ops.length,
...(pkg.endContractJson !== undefined
? { destinationContractJson: pkg.endContractJson }
: {}),
});
}
const plan: MigrationPlan = {
targetId: aggregateTargetId,
spaceId: space.spaceId,
origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
destination: { storageHash: headRef.hash },
operations: pathOps,
providedInvariants: [...providedInvariantsSet].sort(),
};
return {
kind: 'ok',
result: {
plan,
displayOps: pathOps,
destinationContract: space.contract(),
strategy: 'resolve-recorded-path',
migrationEdges: edgeRefs,
pathDecision: outcome.decision,
},
};
}
import type {
DiffSubjectGranularity,
SchemaDiffIssue,
SchemaEntityCoordinate,
VerifyDatabaseSchemaResult,
} from '@prisma-next/framework-components/control';
import { coordinateKey, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
/**
* Classifies a diff issue's subject granularity on demand — the injected
* capability a contract space's family/target instance provides (via
* `hasSchemaSubjectClassifier`). This module never imports family node
* classes and never reads a classification off the node or the issue;
* absent entirely for families that classify nothing.
*/
export type SchemaSubjectClassifier = (
issue: SchemaDiffIssue,
) => DiffSubjectGranularity | undefined;
/**
* Classifies a diff issue's subject storage `entityKind` on demand — the
* sibling injected capability (`hasSchemaSubjectClassifier`'s
* `classifyEntityKind`) that resolves the same per-family vocabulary the
* contract storage's `entries` dictionary uses. This module never hardcodes
* a family entity kind; absent for families that classify nothing.
*/
export type SchemaEntityKindClassifier = (issue: SchemaDiffIssue) => string | undefined;
/**
* Placeholder `entityKind` for a whole-entity issue detected via the
* classifier-absent path-shape fallback (no family/target injects a
* classifier — e.g. a document family). Not a real family vocabulary term:
* no ownership consumer queries `declaresEntity` for a family in this state
* today.
*/
export const UNCLASSIFIED_ENTITY_KIND = 'unclassified-entity';
function pathIsUnder(path: readonly string[], prefix: readonly string[]): boolean {
if (path.length < prefix.length) return false;
return prefix.every((segment, i) => path[i] === segment);
}
/**
* Whether an issue's subject is a WHOLE top-level entity — as opposed to
* something nested under one (e.g. a field, an index, or a policy). Calls
* the injected `classify` capability, which resolves this from the issue's
* node `nodeKind` (this aggregate never reaches into the node itself).
* Absent `classify` (or a `classify` that returns nothing for this issue —
* a family that doesn't classify) falls back to path shape: a
* top-level entity's path is exactly its own name (one segment), so
* anything deeper is nested.
*/
function isWholeEntityIssue(issue: SchemaDiffIssue, classify?: SchemaSubjectClassifier): boolean {
const granularity = classify?.(issue);
if (granularity !== undefined) return granularity === 'entity';
return issue.path.length === 1;
}
/**
* A contract space's contract-satisfaction view. Strips the top-level entity
* extras — the `not-expected` findings on whole-entity nodes (plus the
* findings the differ's total descent reported under those entities). Those
* belong to the standalone unclaimed-elements list
* ({@link collectExtraElementCoordinates}), never a space's own verdict.
*
* Nested `not-expected` findings (an extra field on the space's own
* declared entity…) and structural findings (an undeclared policy) are
* the space's **own drift** and stay.
*
* The verdict recomputes from the surviving list: the per-space result is
* issue-based (`ok` ⇔ the list is empty), so a space whose only failures
* were top-level extras passes after the strip.
*/
export function stripExtraFindings(
result: VerifyDatabaseSchemaResult,
classify?: SchemaSubjectClassifier,
): VerifyDatabaseSchemaResult {
const droppedTablePaths = result.schema.issues
.filter((issue) => issue.reason === 'not-expected' && isWholeEntityIssue(issue, classify))
.map((issue) => issue.path);
const issues = result.schema.issues.filter((issue) => {
if (issue.reason !== 'not-expected') return true;
if (classify?.(issue) === 'structural') return true;
return !droppedTablePaths.some((prefix) => pathIsUnder(issue.path, prefix));
});
if (issues.length === result.schema.issues.length) return result;
const ok = issues.length === 0;
const { code: staleCode, ...envelope } = result;
void staleCode;
// Warnings are the space's own drift-watch (an observed-policy subject), never
// a sibling's unclaimed extra, so the strip carries them through untouched.
return {
...envelope,
ok,
...(ok ? {} : { code: result.code ?? 'PN-RUN-3010' }),
summary: ok ? 'Database schema satisfies contract' : result.summary,
schema: {
issues,
...(result.schema.warnings !== undefined ? { warnings: result.schema.warnings } : {}),
},
};
}
/**
* The schema-IR entity coordinate a `not-expected` `SchemaDiffIssue`
* addresses, when its subject is a whole top-level entity. A nested leaf
* (a field, an index, a policy on an undeclared entity) has no entity of
* its own to report here.
*
* The whole-entity's name is the last path segment: the differ builds each
* path from its nodes' ids, so at a whole-entity finding the last segment is
* exactly the entity name — for every family, whether or not it stamps a
* granularity. The namespace segment only exists for namespace-qualified
* paths (`['database', namespaceId, entityName]`); single-namespace families
* (a flat `['database', entityName]`, or a bare `[entityName]`) have no
* separate segment, so every entity they declare implicitly shares one
* namespace — the same sentinel the aggregate's own coordinate walk uses
* for those families.
*
* `entityKind` is asked from the injected `classifyEntityKind` capability —
* this module never names a family entity kind itself. Absent a classifier
* (the classifier-absent path-shape fallback, e.g. a document family) the
* coordinate carries {@link UNCLASSIFIED_ENTITY_KIND}: no ownership
* consumer queries `declaresEntity` for a family in that state today, so
* nothing reads the value as true.
*/
function schemaDiffIssueCoordinate(
issue: SchemaDiffIssue,
classify?: SchemaSubjectClassifier,
classifyEntityKind?: SchemaEntityKindClassifier,
): SchemaEntityCoordinate | undefined {
if (!isWholeEntityIssue(issue, classify)) return undefined;
const entityName = issue.path[issue.path.length - 1];
if (entityName === undefined) return undefined;
const namespaceId =
issue.path.length === 3 ? (issue.path[1] ?? UNBOUND_NAMESPACE_ID) : UNBOUND_NAMESPACE_ID;
const entityKind = classifyEntityKind?.(issue) ?? UNCLASSIFIED_ENTITY_KIND;
return { namespaceId, entityKind, entityName };
}
/**
* The schema-IR entity coordinates of every live element this contract
* space's diff reports as `not-expected`, deduplicated. The verifier gathers
* these across all spaces and keeps only the coordinates no contract space
* declares — the standalone unclaimed-elements list, reported once for the
* whole database.
*/
export function collectExtraElementCoordinates(
result: VerifyDatabaseSchemaResult,
classify?: SchemaSubjectClassifier,
classifyEntityKind?: SchemaEntityKindClassifier,
): readonly SchemaEntityCoordinate[] {
const seen = new Map<string, SchemaEntityCoordinate>();
for (const issue of result.schema.issues) {
if (issue.reason !== 'not-expected') continue;
const coordinate = schemaDiffIssueCoordinate(issue, classify, classifyEntityKind);
if (coordinate === undefined) continue;
seen.set(coordinateKey(coordinate), coordinate);
}
return [...seen.values()];
}
+241
-234
import { n as OnDiskMigrationPackage } from "../package-CWicKzNs.mjs";
import { i as Refs, r as RefLoadProblem } from "../refs-BNCUu58Y.mjs";
import { t as ContractSpaceHeadRecord } from "../verify-contract-spaces-CnXNGpBV.mjs";
import { t as ContractSpaceHeadRecord } from "../verify-contract-spaces-zYlAhXOK.mjs";
import { n as MigrationGraph } from "../graph-bDjJ4GL9.mjs";

@@ -8,3 +8,3 @@ import { t as PackageLoadProblem } from "../io-q04IrTcd.mjs";

import { Result } from "@prisma-next/utils/result";
import { ControlAdapterInstance, ControlFamilyInstance, MigrationOperationPolicy, MigrationPlan, MigrationPlanOperation, MigrationPlannerConflict, TargetMigrationsCapability } from "@prisma-next/framework-components/control";
import { ControlAdapterInstance, ControlFamilyInstance, DiffSubjectGranularity, MigrationOperationPolicy, MigrationPlan, MigrationPlanOperation, MigrationPlannerConflict, SchemaDiffIssue, SchemaEntityCoordinate, SchemaOwnership, TargetMigrationsCapability, VerifyDatabaseSchemaResult } from "@prisma-next/framework-components/control";
import { Contract, StorageNamespace } from "@prisma-next/contract/types";

@@ -147,6 +147,6 @@ import { TargetBoundComponentDescriptor } from "@prisma-next/framework-components/components";

/**
* One contract space — app or extension — as a member of a
* {@link ContractSpaceAggregate}. Every member has the same shape.
* One contract space — app or extension — as the aggregate holds it.
* Every space in a {@link ContractSpaceAggregate} has the same shape.
*
* A member is a tolerant snapshot of one space's on-disk state, not a
* A value of this type is a tolerant snapshot of one space's on-disk state, not a
* validated value: `packages` is the raw migration-package list as read

@@ -165,3 +165,3 @@ * from disk (a hash- or invariants-mismatched package is retained here;

* (represented as a `headRefMissing` violation, never fatal). The app
* member's head ref is always synthesised from its live contract's
* space's head ref is always synthesised from its live contract's
* storage hash, so it is never `null`.

@@ -171,3 +171,3 @@ * - `graph()`: the migration graph this space's packages induce —

* `from === to` self-edge is represented, not rejected.
* - `contract()`: the deserialized contract for this member — lazily
* - `contract()`: the deserialized contract for this space — lazily
* produced on first call and memoised. For the app it is the live

@@ -185,3 +185,3 @@ * contract the caller supplied; for an extension it is the on-disk

*/
interface ContractSpaceMember {
interface AggregateContractSpace {
readonly spaceId: string;

@@ -198,3 +198,3 @@ readonly packages: readonly OnDiskMigrationPackage[];

* the app contract space plus every extension contract space, each a
* {@link ContractSpaceMember}.
* {@link AggregateContractSpace}.
*

@@ -206,3 +206,3 @@ * Produced once per CLI invocation by `loadContractSpaceAggregate`.

*
* - `targetId`: the app contract's target; every member is expected to
* - `targetId`: the app contract's target; every space is expected to
* share it (a mismatch surfaces as a `targetMismatch` violation under

@@ -216,2 +216,10 @@ * `checkContracts`).

* lex-ascending.
* - `declaresEntity(coordinate)` / `declaringSpaces(coordinate)`: ownership
* queries — does any contract space declare a storage entity at this
* coordinate (namespace, entity kind, and name), and which spaces do? The verifier's
* unclaimed-elements pass asks these of the diff's extra findings; the
* migration planner asks `declaresEntity` per live extra node to decide
* whether some space owns it (the aggregate satisfies the framework
* {@link SchemaOwnership} oracle). The passive aggregate answers both; it
* runs no diff.
* - `checkIntegrity()`: judges the loaded model and returns every

@@ -222,10 +230,12 @@ * violation (never bailing at the first). Config/contract-dependent

*/
interface ContractSpaceAggregate {
interface ContractSpaceAggregate extends SchemaOwnership {
readonly targetId: string;
readonly app: ContractSpaceMember;
readonly extensions: readonly ContractSpaceMember[];
readonly app: AggregateContractSpace;
readonly extensions: readonly AggregateContractSpace[];
listSpaces(): readonly string[];
hasSpace(id: string): boolean;
space(id: string): ContractSpaceMember | undefined;
spaces(): readonly ContractSpaceMember[];
space(id: string): AggregateContractSpace | undefined;
spaces(): readonly AggregateContractSpace[];
declaresEntity(coordinate: SchemaEntityCoordinate): boolean;
declaringSpaces(coordinate: SchemaEntityCoordinate): readonly string[];
checkIntegrity(opts?: IntegrityQueryOptions): readonly IntegrityViolation[];

@@ -236,12 +246,12 @@ }

/**
* Resolve a member's head ref, asserting it is present. The apply/verify
* Resolve a contract space's head ref, asserting it is present. The apply/verify
* engine only runs after `checkIntegrity` has refused on `headRefMissing`,
* so a member reaching the planner / verifier without a head ref is a
* so a space reaching the planner / verifier without a head ref is a
* programming error (the integrity gate was skipped), not a user-facing
* state. The app member's head ref is always synthesised, so this only
* state. The app space's head ref is always synthesised, so this only
* ever guards an ungated extension space.
*/
declare function requireHeadRef(member: ContractSpaceMember): ContractSpaceHeadRecord;
declare function requireHeadRef(space: AggregateContractSpace): ContractSpaceHeadRecord;
/**
* Build a {@link ContractSpaceMember} with lazily-memoised `graph()`,
* Build a {@link AggregateContractSpace} with lazily-memoised `graph()`,
* `contract()`, and `contractAt()` facets.

@@ -258,3 +268,3 @@ *

*/
declare function createContractSpaceMember(args: {
declare function createAggregateContractSpace(args: {
readonly spaceId: string;

@@ -267,5 +277,5 @@ readonly packages: readonly OnDiskMigrationPackage[];

readonly deserializeContract: (raw: unknown) => Contract;
}): ContractSpaceMember;
}): AggregateContractSpace;
/**
* Collect the union of every namespace declared across all members of an
* Collect the union of every namespace declared across all contract spaces of an
* aggregate (app + extensions) and return a minimal object with the shape

@@ -276,3 +286,3 @@ * `{ storage: { namespaces } }` suitable for passing to

* Callers invoke this after the integrity gate (`buildContractSpaceAggregate`
* with `checkContracts: true`), so every `member.contract()` call is safe —
* with `checkContracts: true`), so every `space.contract()` call is safe —
* no try/catch is needed here.

@@ -286,3 +296,3 @@ */

/**
* Assemble a {@link ContractSpaceAggregate} value from its members and a
* Assemble a {@link ContractSpaceAggregate} value from its contract spaces and a
* `checkIntegrity` implementation. The query methods (`listSpaces` /

@@ -296,4 +306,4 @@ * `hasSpace` / `space` / `spaces`) are derived here so every aggregate —

readonly targetId: string;
readonly app: ContractSpaceMember;
readonly extensions: readonly ContractSpaceMember[];
readonly app: AggregateContractSpace;
readonly extensions: readonly AggregateContractSpace[];
readonly checkIntegrity: (opts?: IntegrityQueryOptions) => readonly IntegrityViolation[];

@@ -305,3 +315,3 @@ }): ContractSpaceAggregate;

* One space's load-time facts that `checkIntegrity` judges: the loaded
* member, the load-time problems `readMigrationsDir` surfaced for it, and
* contract space, the load-time problems `readMigrationsDir` surfaced for it, and
* whether it is the app space (the app head ref is synthesised, so the

@@ -311,3 +321,3 @@ * head-ref checks are skipped for it).

interface IntegritySpaceState {
readonly member: ContractSpaceMember;
readonly space: AggregateContractSpace;
readonly problems: readonly PackageLoadProblem[];

@@ -342,38 +352,2 @@ /** Per-ref problems: a user ref `*.json` that exists but is unparseable. */

//#endregion
//#region src/aggregate/loader.d.ts
/**
* Inputs for {@link loadContractSpaceAggregate}.
*
* Construction reads migration **state** from disk (`migrations/<space>/`
* packages + refs + head refs). The app's *live* contract is not a disk
* artefact — in Prisma Next it is always compiled from the project's
* central contract, so the caller always has it and threads it in as
* `appContract`. `deserializeContract` is held and called lazily only for
* the on-disk extension contracts (`migrations/<ext>/contract.json`).
*/
interface LoadAggregateInput {
readonly migrationsDir: string;
readonly deserializeContract: (raw: unknown) => Contract;
readonly appContract: Contract;
}
/**
* Build a tolerant, queryable {@link ContractSpaceAggregate} from on-disk
* migration state plus the caller's live app contract.
*
* Building **never throws on disk content**: a hash- or
* invariants-mismatched package is retained, an unparseable package is
* omitted, a missing extension head ref leaves `headRef: null`, and an
* unreadable on-disk contract defers its failure to `member.contract()`.
* Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}
* rather than aborting the load. The only rejections are catastrophic I/O
* (a `migrations/` that exists but is unreadable for reasons other than
* absence).
*
* The app space's head ref is synthesised from the live contract's
* storage hash (the app contract is authored independently of the
* migration graph), and `app.contract()` returns the supplied contract.
* Extension spaces read their contract, refs, and head ref from disk.
*/
declare function loadContractSpaceAggregate(input: LoadAggregateInput): Promise<ContractSpaceAggregate>;
//#endregion
//#region src/aggregate/marker-types.d.ts

@@ -402,13 +376,14 @@ /**

*
* - `ignoreGraphFor`: `Set<spaceId>`. For listed members, the planner
* forces the **synth** strategy (synthesise a plan from the contract
* IR via `familyInstance.createPlanner(...).plan(...)`) regardless of
* whether a graph is available. The CLI's daily-driver `db init` /
* `db update` pipelines pass `new Set([aggregate.app.spaceId])` to
* keep today's app-space behaviour: the user's authored
* `migrations/` directory is **not** walked for the app member, the
* plan is synthesised on the fly. Extension members are walked.
* - `ignoreGraphFor`: `Set<spaceId>`. For listed contract spaces, the planner
* forces **`planFromDiff`** (fabricate a plan from a diff against the
* contract IR via `familyInstance.createPlanner(...).plan(...)`)
* regardless of whether a graph is available. The CLI's daily-driver
* `db init` / `db update` pipelines pass `new Set([aggregate.app.spaceId])`
* to keep today's app-space behaviour: the user's authored
* `migrations/` directory is **not** walked for the app space, the
* plan is fabricated on the fly. Extension spaces are walked.
*
* Listing a member here whose `headRef.invariants` is non-empty is
* a `policyConflict` — synth cannot satisfy authored invariants.
* Listing a contract space here whose `headRef.invariants` is non-empty is
* a `policyConflict` — a diff-fabricated plan cannot satisfy authored
* invariants.
*/

@@ -424,7 +399,9 @@ interface CallerPolicy {

* marker yet (greenfield space). The planner treats the marker's
* `storageHash` as the graph-walk's `from` node, falling back to
* {@link import('../constants').EMPTY_CONTRACT_HASH} when absent.
* `storageHash` as the recorded-path resolution's `from` node, falling
* back to {@link import('../constants').EMPTY_CONTRACT_HASH} when absent.
* - `schemaIntrospection`: the family's full live schema IR. Fed into
* the synth strategy after per-space pre-projection via
* {@link import('./project-schema-to-space').projectSchemaToSpace}.
* `planFromDiff` in full; the aggregate itself is handed to the planner as
* an ownership oracle, which the planner asks per live extra node whether
* any space owns it — no schema is pruned up front, and the planner never
* drops a node a sibling space owns.
*

@@ -442,4 +419,4 @@ * Callers (CLI commands) gather this via the family's

*
* The planner is target-agnostic but family-aware: per-member synth
* delegates to the family's `createPlanner(adapter).plan(...)`,
* The planner is target-agnostic but family-aware: per-space
* `planFromDiff` delegates to the family's `createPlanner(adapter).plan(...)`,
* which is why `adapter`, `migrations` (the

@@ -464,3 +441,3 @@ * `TargetMigrationsCapability`), and `frameworkComponents` are all

/**
* Per-member output of the planner. The runner ingests this
* Per-space output of the planner. The runner ingests this
* shape directly via a thin `toRunnerInput` adapter at the CLI.

@@ -473,15 +450,20 @@ *

* - `destinationContract`: the typed contract value the runner uses
* for post-apply verification. For the app member, the user's
* contract; for extension members, the on-disk `contract.json`.
* - `strategy`: which strategy produced this plan (`'graph-walk'` or
* `'synth'`). Surfaced for diagnostics; not consumed by the runner.
* for post-apply verification. For the app space, the user's
* contract; for extension spaces, the on-disk `contract.json`.
* - `strategy`: which operation produced this plan — `'resolve-recorded-path'`
* (walked a recorded migration path), `'plan-from-diff'` (fabricated a
* plan from a state diff), or `'declared-state'` (the space ships no
* migration packages at all; the aggregate declares the no-op directly
* without invoking either). Surfaced for diagnostics; not consumed by
* the runner.
*/
/**
* Per-edge metadata for the chain assembled by the graph-walk
* strategy. Lets `migrate` surface a per-migration `applied[]`
* entry (preserving the `migrationsApplied` count semantics) without
* re-walking the graph.
* Per-edge metadata for the chain assembled by resolving a contract
* space's recorded migration path. Lets `migrate` surface a per-migration
* `applied[]` entry (preserving the `migrationsApplied` count semantics)
* without re-walking the graph.
*
* `synth`-produced plans leave this absent — synthesised plans don't
* have authored edges to surface.
* `plan-from-diff`/`declared-state` plans leave this absent — they don't
* have authored edges to surface (a single fabricated edge is used
* instead, see {@link import('./fabricated-migration-edge').buildFabricatedMigrationEdge}).
*/

@@ -494,2 +476,12 @@ interface AggregateMigrationEdgeRef {

readonly operationCount: number;
/**
* Contract IR JSON of the edge's destination state, threaded from the
* bundle's on-disk `end-contract.json` snapshot (`resolveRecordedPath`)
* or the space's destination contract (`planFromDiff`). Runners persist it as the
* edge's ledger-linked contract row so database tooling can render
* model-level diffs per applied migration; an edge's *before* state is
* the previous row's snapshot by chain construction. Absent when the
* source bundle carries no snapshot.
*/
readonly destinationContractJson?: unknown;
}

@@ -500,16 +492,17 @@ interface PerSpacePlan {

readonly destinationContract: Contract;
readonly strategy: 'graph-walk' | 'synth';
readonly strategy: 'resolve-recorded-path' | 'plan-from-diff' | 'declared-state';
readonly warnings?: readonly MigrationPlannerConflict[];
/**
* Per-edge breakdown of the chain. Graph-walk plans carry one entry per
* authored edge; synth and at-head plans carry a single synthesised edge.
* Per-edge breakdown of the chain. `resolve-recorded-path` plans carry
* one entry per authored edge; `plan-from-diff` and `declared-state`
* plans carry a single fabricated edge.
*/
readonly migrationEdges: readonly AggregateMigrationEdgeRef[];
/**
* Path decision data the strategy used to select the chain
* (alternative count, tie-break reasons, required/satisfied
* invariants, per-edge invariants). Populated by the graph-walk
* strategy; absent for synth-produced plans.
* Path decision data used to select the chain (alternative count,
* tie-break reasons, required/satisfied invariants, per-edge
* invariants). Populated by `resolveRecordedPath`; absent for
* `plan-from-diff`/`declared-state` plans.
*
* `migrate` surfaces this for the app member as
* `migrate` surfaces this for the app space as
* `MigrateSuccess.pathDecision` (back-compat with single-

@@ -534,3 +527,3 @@ * space callers).

* Discriminated failure variants for {@link planMigration}. Each
* variant short-circuits the plan; per-member errors carry the
* variant short-circuits the plan; per-space errors carry the
* `spaceId` so the CLI can surface a precise envelope.

@@ -547,3 +540,3 @@ */

} | {
readonly kind: 'appSynthFailure';
readonly kind: 'planFromDiffFailed';
readonly spaceId: string;

@@ -558,16 +551,67 @@ readonly conflicts: readonly MigrationPlannerConflict[];

//#endregion
//#region src/aggregate/fabricated-migration-edge.d.ts
declare function buildFabricatedMigrationEdge(args: {
readonly currentMarkerStorageHash: string | null | undefined;
readonly destinationStorageHash: string;
readonly operationCount: number;
readonly destinationContractJson?: unknown;
}): AggregateMigrationEdgeRef;
//#endregion
//#region src/aggregate/loader.d.ts
/**
* Inputs for {@link loadContractSpaceAggregate}.
*
* Construction reads migration **state** from disk (`migrations/<space>/`
* packages + refs + head refs). The app's *live* contract is not a disk
* artefact — in Prisma Next it is always compiled from the project's
* central contract, so the caller always has it and threads it in as
* `appContract`. `deserializeContract` is held and called lazily only for
* the on-disk extension contracts (`migrations/<ext>/contract.json`).
*/
interface LoadAggregateInput {
readonly migrationsDir: string;
readonly deserializeContract: (raw: unknown) => Contract;
readonly appContract: Contract;
}
/**
* Build a tolerant, queryable {@link ContractSpaceAggregate} from on-disk
* migration state plus the caller's live app contract.
*
* Building **never throws on disk content**: a hash- or
* invariants-mismatched package is retained, an unparseable package is
* omitted, a missing extension head ref leaves `headRef: null`, and an
* unreadable on-disk contract defers its failure to `space.contract()`.
* Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}
* rather than aborting the load. The only rejections are catastrophic I/O
* (a `migrations/` that exists but is unreadable for reasons other than
* absence).
*
* The app space's head ref is synthesised from the live contract's
* storage hash (the app contract is authored independently of the
* migration graph), and `app.contract()` returns the supplied contract.
* Extension spaces read their contract, refs, and head ref from disk.
*/
declare function loadContractSpaceAggregate(input: LoadAggregateInput): Promise<ContractSpaceAggregate>;
//#endregion
//#region src/aggregate/planner.d.ts
/**
* Plan a migration across every member of a {@link ContractSpaceAggregate}.
* Plan a migration across every contract space of a {@link ContractSpaceAggregate}.
*
* Strategy selection per member, in order; first match wins:
* Per-space operation selection, in order; first match wins:
*
* 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
* - If `member.headRef.invariants` is empty → synth.
* - Else → `policyConflict` (synth cannot satisfy authored invariants).
* 2. Else if `member.graph()` is non-empty AND graph-walk
* succeeds → graph-walk.
* 3. Else if `member.headRef.invariants` is empty → synth.
* 4. Else → graph-walk failure → `extensionPathUnreachable` /
* `extensionPathUnsatisfiable`.
* 1. If `callerPolicy.ignoreGraphFor.has(space.spaceId)`:
* - If `space.headRef.invariants` is empty → `planFromDiff`.
* - Else → `policyConflict` (a diff-fabricated plan cannot satisfy
* authored invariants).
* 2. Else if `space.graph()` is non-empty AND `resolveRecordedPath`
* succeeds → its result.
* 3. Else if `space.graph()` is non-empty but unresolvable →
* `extensionPathUnreachable` / `extensionPathUnsatisfiable`.
* 4. Else (empty graph — the space ships no migration packages at all,
* e.g. an all-external extension space like Supabase's `auth`/`storage`)
* if `space.headRef.invariants` is empty → declare the no-op state
* directly (zero ops, destination = head ref) without invoking the
* family planner.
* 5. Else → `extensionPathUnsatisfiable` (an empty graph cannot satisfy
* non-empty invariants).
*

@@ -584,64 +628,8 @@ * Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`

//#endregion
//#region src/aggregate/project-schema-to-space.d.ts
//#region src/aggregate/strategies/resolve-recorded-path.d.ts
/**
* Project the **introspected live schema** to the slice claimed by a
* single contract-space member.
*
* "Schema" here means the live introspected database state — the
* planner / verifier sees this object as a `MongoSchemaIR` (Mongo) or
* `SqlSchemaIR` (SQL). It is **not** a database schema in the SQL
* `CREATE SCHEMA` sense, nor a contract-space namespace. The
* function's job is to filter that introspected state down to the
* elements claimed by one space, so a per-space verify pass doesn't
* see another space's storage as "extras".
*
* Returns the same `schema` value with every top-level storage element
* (table or collection) claimed by **other** members of the aggregate
* removed. Elements not claimed by any member flow through unchanged —
* the planner / verifier sees them as orphans (extras in strict mode).
*
* Used by:
*
* - The aggregate planner's **synth strategy**: when synthesising a
* plan against a member's contract, the live schema must be projected
* to that member's slice so the planner doesn't treat elements claimed
* by other members as "extras" and emit destructive ops to drop them.
* - The aggregate verifier's **schemaCheck**: projects per member so the
* single-contract verify only sees the slice claimed by the member it
* is checking. Closes the architectural concern that a multi-member
* deployment makes each member's elements look like extras to every
* other member's verify pass.
*
* **Duck-typing semantics**: the helper operates on `unknown` for the
* schema and falls through structurally if the shape doesn't match.
* Two storage shapes are recognised today:
*
* - SQL families expose `storage.tables: Record<string, ...>` on
* contracts and the introspected schema mirrors the same record shape.
* Pruning iterates the record entries.
* - Mongo exposes `storage.collections: Record<string, ...>` on
* contracts; the introspected `MongoSchemaIR` exposes
* `collections: ReadonlyArray<{name: string, ...}>`. Pruning iterates
* the array on the schema side and the record's keys on the
* other-member side.
*
* Schemas of unrecognised shape are returned unchanged. The function
* never imports family classes (`SqlSchemaIR`, `MongoSchemaIR`); the
* projected schema is a plain object — `{...schema, tables: pruned}` or
* `{...schema, collections: pruned}` — that downstream consumers
* duck-type. A future family with a different storage shape gets the
* schema returned unchanged rather than blowing up the aggregate
* planner.
*
* Record-shape detection guards against arrays (`!Array.isArray`) so
* an unrecognised array-shaped value falls through unchanged rather
* than being pruned by numeric keys.
*/
declare function projectSchemaToSpace(schema: unknown, member: ContractSpaceMember, otherMembers: ReadonlyArray<ContractSpaceMember>): unknown;
//#endregion
//#region src/aggregate/strategies/graph-walk.d.ts
/**
* Outcome variants for the graph-walk strategy. Mirrors
* Outcome variants for resolving a contract space's recorded migration
* path. Mirrors
* {@link import('../../compute-extension-space-apply-path').ExtensionSpaceApplyPathOutcome}
* but operates against the member's lazily-reconstructed `graph()`
* but operates against the contract space's lazily-reconstructed `graph()`
* instead of re-reading from disk. The aggregate planner converts

@@ -651,3 +639,3 @@ * these into {@link import('../planner-types').PlannerError}

*/
type GraphWalkOutcome = {
type ResolveRecordedPathOutcome = {
readonly kind: 'ok';

@@ -661,5 +649,5 @@ readonly result: PerSpacePlan;

};
interface GraphWalkStrategyInputs {
interface ResolveRecordedPathInputs {
readonly aggregateTargetId: string;
readonly member: ContractSpaceMember;
readonly space: AggregateContractSpace;
readonly currentMarker: ContractMarkerRecordLike | null;

@@ -675,9 +663,9 @@ /**

/**
* Walk a member's hydrated migration graph from the live marker to
* `member.headRef.hash`, covering every required invariant.
* Resolve a contract space's hydrated migration graph to the path from the
* live marker to `space.headRef.hash`, covering every required invariant.
*
* Pure synchronous function — no I/O. The aggregate's loader has
* already integrity-checked every package and reconstructed the graph;
* this strategy just looks up ops by `migrationHash` and assembles a
* `MigrationPlan` with `targetId` set from the aggregate (no
* this resolves the path by looking up ops by `migrationHash` and
* assembles a `MigrationPlan` with `targetId` set from the aggregate (no
* placeholder cast).

@@ -690,10 +678,21 @@ *

*/
declare function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutcome;
declare function resolveRecordedPath(input: ResolveRecordedPathInputs): ResolveRecordedPathOutcome;
//#endregion
//#region src/aggregate/synth-migration-edge.d.ts
declare function buildSynthMigrationEdge(args: {
readonly currentMarkerStorageHash: string | null | undefined;
readonly destinationStorageHash: string;
readonly operationCount: number;
}): AggregateMigrationEdgeRef;
//#region src/aggregate/unclaimed-elements.d.ts
/**
* Classifies a diff issue's subject granularity on demand — the injected
* capability a contract space's family/target instance provides (via
* `hasSchemaSubjectClassifier`). This module never imports family node
* classes and never reads a classification off the node or the issue;
* absent entirely for families that classify nothing.
*/
type SchemaSubjectClassifier = (issue: SchemaDiffIssue) => DiffSubjectGranularity | undefined;
/**
* Classifies a diff issue's subject storage `entityKind` on demand — the
* sibling injected capability (`hasSchemaSubjectClassifier`'s
* `classifyEntityKind`) that resolves the same per-family vocabulary the
* contract storage's `entries` dictionary uses. This module never hardcodes
* a family entity kind; absent for families that classify nothing.
*/
type SchemaEntityKindClassifier = (issue: SchemaDiffIssue) => string | undefined;
//#endregion

@@ -703,7 +702,7 @@ //#region src/aggregate/verifier.d.ts

* Caller policy for the verifier. Today's only knob is
* `mode`: `strict` treats orphan elements (live tables not claimed by
* any aggregate member) as errors; `lenient` treats them as
* `mode`: `strict` treats unclaimed elements (live tables declared by
* no contract space) as errors; `lenient` treats them as
* informational. Maps directly to `db verify --strict`.
*/
interface VerifierInput<TSchemaResult> {
interface VerifierInput {
readonly aggregate: ContractSpaceAggregate;

@@ -714,17 +713,28 @@ readonly markersBySpaceId: ReadonlyMap<string, ContractMarkerRecordLike | null>;

/**
* Caller-supplied per-space schema verifier. The CLI wires this to
* the family's `verifySqlSchema` (SQL) / equivalent (other
* families). The verifier projects the schema to the
* member's slice via {@link projectSchemaToSpace} before invoking
* the callback, so single-contract semantics are preserved.
*
* Typed structurally with a generic `TSchemaResult` so the
* migration-tools layer doesn't depend on the SQL family's
* `VerifySqlSchemaResult`. CLI callers pass the family's type
* through unchanged.
* Caller-supplied per-space schema verifier. The CLI wires this to the
* family's `verifySchema`, run against the **full** introspected schema. The
* verifier then produces two outputs from the per-space results: each space's
* contract-satisfaction view (extras stripped) and one deduplicated list of
* live elements no contract space declares. It touches no storage shape.
*/
readonly verifySchemaForMember: (projectedSchema: unknown, member: ContractSpaceMember, mode: 'strict' | 'lenient') => TSchemaResult;
readonly verifySchemaForSpace: (schema: unknown, space: AggregateContractSpace, mode: 'strict' | 'lenient') => VerifyDatabaseSchemaResult;
/**
* Classifies a diff issue's subject granularity on demand — the injected
* capability the caller reads off the family instance (via
* `hasSchemaSubjectClassifier`) when it has one. Absent for families that
* classify nothing; the unclaimed-elements sweep falls back to path shape
* in that case. Never stamped onto the issue or the node.
*/
readonly classifySubjectGranularity?: SchemaSubjectClassifier;
/**
* Classifies a diff issue's subject storage `entityKind` on demand — the
* sibling injected capability read off the family instance alongside
* `classifySubjectGranularity`. Absent for families that classify
* nothing; the unclaimed-elements sweep falls back to a placeholder in
* that case. Never stamped onto the issue or the node.
*/
readonly classifyEntityKind?: SchemaEntityKindClassifier;
}
/**
* Marker-check result per member. Mirrors the four cases the
* Marker-check result per contract space. Mirrors the four cases the
* `verifyContractSpaces` primitive surfaces today, plus an `'absent'`

@@ -753,27 +763,22 @@ * case for greenfield spaces (no marker row written yet — `db init`

}
/**
* A live storage element (today: a top-level table) not claimed by any
* member of the aggregate. The verifier always reports these;
* the caller decides what to do — `db verify --strict` treats them as
* errors, the lenient default treats them as informational.
*
* Today only `kind: 'table'` exists. The discriminated shape leaves
* room for orphan columns / indexes / sequences in the future without
* breaking the type contract.
*/
type OrphanElement = {
readonly kind: 'table';
readonly name: string;
};
interface SchemaCheckSection<TSchemaResult> {
readonly perSpace: ReadonlyMap<string, TSchemaResult>;
interface SchemaCheckSection {
/**
* Live elements present in the introspected schema that are not
* claimed by **any** aggregate member. Sorted alphabetically by name.
* Per contract space, its contract-satisfaction view: the space's
* declared nodes only, each pass/fail by whether a missing/mismatch issue
* concerns it. Extras are stripped; the space's verdict is missing/mismatch
* only.
*/
readonly orphanElements: readonly OrphanElement[];
readonly perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>;
/**
* One deduplicated, sorted list of live element names no contract
* space declares (built from the diffs' extra findings, filtered by the
* passive aggregate's ownership query). Reported once for the whole database,
* not per space. Strict callers fail on a non-empty list; lenient callers show
* it informationally.
*/
readonly unclaimed: readonly string[];
}
interface VerifierSuccess<TSchemaResult> {
interface VerifierSuccess {
readonly markerCheck: MarkerCheckSection;
readonly schemaCheck: SchemaCheckSection<TSchemaResult>;
readonly schemaCheck: SchemaCheckSection;
}

@@ -784,3 +789,3 @@ type VerifierError = {

};
type VerifierOutput<TSchemaResult> = Result<VerifierSuccess<TSchemaResult>, VerifierError>;
type VerifierOutput = Result<VerifierSuccess, VerifierError>;
/**

@@ -790,15 +795,17 @@ * Verify a {@link ContractSpaceAggregate} against the live database

*
* - `markerCheck` per member: compare the live marker row against the
* member's `headRef.hash` + `headRef.invariants`. Absence is a
* - `markerCheck` per contract space: compare the live marker row against the
* space's `headRef.hash` + `headRef.invariants`. Absence is a
* distinct kind, not an error (callers — `db verify` strict vs
* `db init` precondition — choose how to interpret it).
* - `schemaCheck` per member: project the live schema to the slice
* the member claims via {@link projectSchemaToSpace}, then delegate
* to the caller-supplied `verifySchemaForMember`. The pre-projection
* means the family's single-contract verifier no longer sees other
* members' tables as `extras`, so a multi-member deployment never
* surfaces cross-member tables as orphaned schema elements.
* - `schemaCheck`: two distinct outputs from the per-space diffs.
* `perSpace` — each space verified against the **full**
* introspected schema, then its extras stripped, leaving the space's
* declared nodes (its contract-satisfaction view; verdict is
* missing/mismatch only). `unclaimed` — the extras gathered
* across every space, deduplicated, and filtered to the names no contract
* space declares (via the passive aggregate's ownership query); reported
* once for the database. No schema is pruned before verifying.
*
* `markerCheck.orphanMarkers` lists every marker row whose `space` is
* not a member of the aggregate. `db verify` callers reject orphans;
* not a contract space of the aggregate. `db verify` callers reject orphans;
* future tooling may not.

@@ -809,5 +816,5 @@ *

*/
declare function verifyMigration<TSchemaResult>(input: VerifierInput<TSchemaResult>): VerifierOutput<TSchemaResult>;
declare function verifyMigration(input: VerifierInput): VerifierOutput;
//#endregion
export { type AggregateCurrentDBState, type AggregateMigrationEdgeRef, type CallerPolicy, type ContractAtOptions, type ContractAtResult, type ContractMarkerRecordLike, type ContractSpaceAggregate, type ContractSpaceMember, type DeclaredExtensionEntry, type GraphWalkOutcome, type GraphWalkStrategyInputs, type IntegrityComputationInput, type IntegrityQueryOptions, type IntegritySpaceState, type IntegrityViolation, type LoadAggregateInput, type MarkerCheckResult, type MarkerCheckSection, type OrphanElement, type PerSpacePlan, type PlannerError, type PlannerInput, type PlannerOutput, type PlannerSuccess, type SchemaCheckSection, type VerifierError, type VerifierInput, type VerifierOutput, type VerifierSuccess, buildSynthMigrationEdge, collectAggregateNamespaces, computeIntegrityViolations, createContractSpaceAggregate, createContractSpaceMember, graphWalkStrategy, loadContractSpaceAggregate, loadProblemToViolation, planMigration, projectSchemaToSpace, requireHeadRef, verifyMigration };
export { type AggregateContractSpace, type AggregateCurrentDBState, type AggregateMigrationEdgeRef, type CallerPolicy, type ContractAtOptions, type ContractAtResult, type ContractMarkerRecordLike, type ContractSpaceAggregate, type DeclaredExtensionEntry, type IntegrityComputationInput, type IntegrityQueryOptions, type IntegritySpaceState, type IntegrityViolation, type LoadAggregateInput, type MarkerCheckResult, type MarkerCheckSection, type PerSpacePlan, type PlannerError, type PlannerInput, type PlannerOutput, type PlannerSuccess, type ResolveRecordedPathInputs, type ResolveRecordedPathOutcome, type SchemaCheckSection, type VerifierError, type VerifierInput, type VerifierOutput, type VerifierSuccess, buildFabricatedMigrationEdge, collectAggregateNamespaces, computeIntegrityViolations, createAggregateContractSpace, createContractSpaceAggregate, loadContractSpaceAggregate, loadProblemToViolation, planMigration, requireHeadRef, resolveRecordedPath, verifyMigration };
//# sourceMappingURL=aggregate.d.mts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"aggregate.d.mts","names":[],"sources":["../../src/integrity-violation.ts","../../src/aggregate/types.ts","../../src/aggregate/aggregate.ts","../../src/aggregate/check-integrity.ts","../../src/aggregate/loader.ts","../../src/aggregate/marker-types.ts","../../src/aggregate/planner-types.ts","../../src/aggregate/planner.ts","../../src/aggregate/project-schema-to-space.ts","../../src/aggregate/strategies/graph-walk.ts","../../src/aggregate/synth-migration-edge.ts","../../src/aggregate/verifier.ts"],"mappings":"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"}
{"version":3,"file":"aggregate.d.mts","names":[],"sources":["../../src/integrity-violation.ts","../../src/aggregate/types.ts","../../src/aggregate/aggregate.ts","../../src/aggregate/check-integrity.ts","../../src/aggregate/marker-types.ts","../../src/aggregate/planner-types.ts","../../src/aggregate/fabricated-migration-edge.ts","../../src/aggregate/loader.ts","../../src/aggregate/planner.ts","../../src/aggregate/strategies/resolve-recorded-path.ts","../../src/aggregate/unclaimed-elements.ts","../../src/aggregate/verifier.ts"],"mappings":"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"}

@@ -1,13 +0,13 @@

import { E as errorSnapshotMissing, f as errorInvalidJson, i as errorContractDeserializationFailed, l as errorHashNotInGraph, r as errorBundleNotFoundForGraphNode, t as MigrationToolsError, x as errorMissingFile } from "../errors-DjTpjyFU.mjs";
import { s as readMigrationsDir } from "../io-DWg_qFDX.mjs";
import { D as errorSnapshotMissing, S as errorMissingFile, f as errorInvalidJson, i as errorContractDeserializationFailed, l as errorHashNotInGraph, r as errorBundleNotFoundForGraphNode, t as MigrationToolsError } from "../errors-DIS_dcFJ.mjs";
import { s as readMigrationsDir } from "../io-CE79C4uK.mjs";
import { t as EMPTY_CONTRACT_HASH } from "../constants-YnG8_EGO.mjs";
import { n as isGraphNode } from "../graph-membership-DEzWfpW0.mjs";
import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-BW4ty9WV.mjs";
import { a as readRefsTolerant, t as HEAD_REF_NAME } from "../refs-kd62Ljkh.mjs";
import { r as readRefSnapshot } from "../snapshot-C-IHpIbr.mjs";
import { a as APP_SPACE_ID, d as spaceRefsDirectory, i as readContractSpaceHeadRef, l as isValidSpaceId, n as listContractSpaceDirectories, o as RESERVED_SPACE_SUBDIR_NAMES, t as readContractSpaceContract, u as spaceMigrationDirectory } from "../read-contract-space-contract-DyLWOWSt.mjs";
import { n as isGraphNode } from "../graph-membership-CNneL_Yy.mjs";
import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-BXHOgdTg.mjs";
import { a as readRefsTolerant, t as HEAD_REF_NAME } from "../refs-CDIHeNcC.mjs";
import { r as readRefSnapshot } from "../snapshot-DiE5uFFJ.mjs";
import { a as APP_SPACE_ID, d as spaceRefsDirectory, i as readContractSpaceHeadRef, l as isValidSpaceId, n as listContractSpaceDirectories, o as RESERVED_SPACE_SUBDIR_NAMES, t as readContractSpaceContract, u as spaceMigrationDirectory } from "../read-contract-space-contract-DBW4-T5r.mjs";
import { join } from "pathe";
import { readFile } from "node:fs/promises";
import { notOk, ok } from "@prisma-next/utils/result";
import { elementCoordinates } from "@prisma-next/framework-components/ir";
import { UNBOUND_NAMESPACE_ID, coordinateKey, elementCoordinates } from "@prisma-next/framework-components/ir";
//#region src/aggregate/aggregate.ts

@@ -103,15 +103,15 @@ function hasErrnoCode(error, code) {

/**
* Resolve a member's head ref, asserting it is present. The apply/verify
* Resolve a contract space's head ref, asserting it is present. The apply/verify
* engine only runs after `checkIntegrity` has refused on `headRefMissing`,
* so a member reaching the planner / verifier without a head ref is a
* so a space reaching the planner / verifier without a head ref is a
* programming error (the integrity gate was skipped), not a user-facing
* state. The app member's head ref is always synthesised, so this only
* state. The app space's head ref is always synthesised, so this only
* ever guards an ungated extension space.
*/
function requireHeadRef(member) {
if (member.headRef === null) throw new Error(`Contract space "${member.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`);
return member.headRef;
function requireHeadRef(space) {
if (space.headRef === null) throw new Error(`Contract space "${space.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`);
return space.headRef;
}
/**
* Build a {@link ContractSpaceMember} with lazily-memoised `graph()`,
* Build a {@link AggregateContractSpace} with lazily-memoised `graph()`,
* `contract()`, and `contractAt()` facets.

@@ -128,3 +128,3 @@ *

*/
function createContractSpaceMember(args) {
function createAggregateContractSpace(args) {
const { spaceId, packages, refs, headRef, refsDir, resolveContract, deserializeContract } = args;

@@ -134,3 +134,3 @@ let graphMemo;

const contractAtMemo = /* @__PURE__ */ new Map();
function memberGraph() {
function spaceGraph() {
graphMemo ??= reconstructGraph(packages);

@@ -144,3 +144,3 @@ return graphMemo;

headRef,
graph: memberGraph,
graph: spaceGraph,
contract() {

@@ -159,3 +159,3 @@ contractMemo ??= resolveContract();

packages,
graph: memberGraph(),
graph: spaceGraph(),
deserializeContract

@@ -169,3 +169,3 @@ });

/**
* Collect the union of every namespace declared across all members of an
* Collect the union of every namespace declared across all contract spaces of an
* aggregate (app + extensions) and return a minimal object with the shape

@@ -176,3 +176,3 @@ * `{ storage: { namespaces } }` suitable for passing to

* Callers invoke this after the integrity gate (`buildContractSpaceAggregate`
* with `checkContracts: true`), so every `member.contract()` call is safe —
* with `checkContracts: true`), so every `space.contract()` call is safe —
* no try/catch is needed here.

@@ -182,7 +182,28 @@ */

const merged = {};
for (const member of aggregate.spaces()) for (const [key, ns] of Object.entries(member.contract().storage.namespaces)) merged[key] = ns;
for (const space of aggregate.spaces()) for (const [key, ns] of Object.entries(space.contract().storage.namespaces)) {
const existing = merged[key];
merged[key] = existing === void 0 ? ns : mergeNamespaceEntries(existing, ns);
}
return { storage: { namespaces: merged } };
}
/**
* Assemble a {@link ContractSpaceAggregate} value from its members and a
* Union two contract spaces' declarations for the same namespace id, per
* entity kind. Two spaces may legitimately share a namespace (e.g. both
* declaring tables in `public`); element-level disjointness is enforced by
* `checkIntegrity`, so the kind maps never collide on a name — replacing the
* whole namespace would silently drop the earlier space's entities.
*/
function mergeNamespaceEntries(a, b) {
const entries = { ...a.entries };
for (const [entityKind, kindMap] of Object.entries(b.entries)) entries[entityKind] = {
...entries[entityKind],
...kindMap
};
return {
...a,
entries
};
}
/**
* Assemble a {@link ContractSpaceAggregate} value from its contract spaces and a
* `checkIntegrity` implementation. The query methods (`listSpaces` /

@@ -198,2 +219,7 @@ * `hasSpace` / `space` / `spaces`) are derived here so every aggregate —

const byId = new Map(ordered.map((m) => [m.spaceId, m]));
const spaceDeclares = (space, coordinate) => {
const key = coordinateKey(coordinate);
for (const coord of elementCoordinates(space.contract().storage)) if (coordinateKey(coord) === key) return true;
return false;
};
return {

@@ -207,2 +233,4 @@ targetId,

spaces: () => ordered,
declaresEntity: (coordinate) => ordered.some((space) => spaceDeclares(space, coordinate)),
declaringSpaces: (coordinate) => ordered.filter((space) => spaceDeclares(space, coordinate)).map((s) => s.spaceId),
checkIntegrity

@@ -222,4 +250,4 @@ };

const violations = [];
for (const { member, problems, refProblems, headRefProblem, isApp } of input.spaces) {
const { spaceId } = member;
for (const { space, problems, refProblems, headRefProblem, isApp } of input.spaces) {
const { spaceId } = space;
for (const problem of problems) violations.push(loadProblemToViolation(spaceId, problem));

@@ -238,3 +266,3 @@ for (const refProblem of refProblems) violations.push({

});
for (const pkg of member.packages) {
for (const pkg of space.packages) {
const from = pkg.metadata.from ?? "sha256:empty";

@@ -250,12 +278,12 @@ const isSelfEdge = from === pkg.metadata.to;

}
violations.push(...duplicateMigrationHashViolations(spaceId, member.packages));
violations.push(...duplicateMigrationHashViolations(spaceId, space.packages));
if (!isApp && headRefProblem === null) {
if (member.headRef === null) violations.push({
if (space.headRef === null) violations.push({
kind: "headRefMissing",
spaceId
});
else if (member.packages.length > 0 && !headRefPresentInGraph(member, member.headRef.hash)) violations.push({
else if (space.packages.length > 0 && !headRefPresentInGraph(space, space.headRef.hash)) violations.push({
kind: "headRefNotInGraph",
spaceId,
hash: member.headRef.hash
hash: space.headRef.hash
});

@@ -311,4 +339,4 @@ }

*/
function headRefPresentInGraph(member, headHash) {
const graph = member.graph();
function headRefPresentInGraph(space, headHash) {
const graph = space.graph();
if (graph.nodes.size === 0) return headHash === EMPTY_CONTRACT_HASH;

@@ -319,3 +347,3 @@ return graph.nodes.has(headHash);

const out = [];
const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.member.spaceId));
const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.space.spaceId));
const declaredIds = new Set(declaredExtensions.map((d) => d.id));

@@ -336,10 +364,10 @@ for (const id of [...extensionSpaceIds].sort()) if (!declaredIds.has(id)) out.push({

const elementLabel = /* @__PURE__ */ new Map();
for (const { member } of input.spaces) {
for (const { space } of input.spaces) {
let contract;
try {
contract = member.contract();
contract = space.contract();
} catch (error) {
out.push({
kind: "contractUnreadable",
spaceId: member.spaceId,
spaceId: space.spaceId,
detail: detailOf$1(error)

@@ -351,13 +379,13 @@ });

kind: "targetMismatch",
spaceId: member.spaceId,
spaceId: space.spaceId,
expected: input.targetId,
actual: contract.target
});
for (const { namespaceId, entityKind, entityName } of elementCoordinates(contract.storage)) {
const key = `${namespaceId}:${entityKind}:${entityName}`;
for (const coordinate of elementCoordinates(contract.storage)) {
const key = coordinateKey(coordinate);
const claimers = elementClaimedBy.get(key);
if (claimers) claimers.push(member.spaceId);
if (claimers) claimers.push(space.spaceId);
else {
elementClaimedBy.set(key, [member.spaceId]);
elementLabel.set(key, `${namespaceId}.${entityName}`);
elementClaimedBy.set(key, [space.spaceId]);
elementLabel.set(key, `${coordinate.namespaceId}.${coordinate.entityName}`);
}

@@ -385,2 +413,14 @@ }

//#endregion
//#region src/aggregate/fabricated-migration-edge.ts
function buildFabricatedMigrationEdge(args) {
return {
dirName: "",
migrationHash: args.destinationStorageHash,
from: args.currentMarkerStorageHash ?? "",
to: args.destinationStorageHash,
operationCount: args.operationCount,
...args.destinationContractJson !== void 0 ? { destinationContractJson: args.destinationContractJson } : {}
};
}
//#endregion
//#region src/aggregate/loader.ts

@@ -394,3 +434,3 @@ /**

* omitted, a missing extension head ref leaves `headRef: null`, and an
* unreadable on-disk contract defers its failure to `member.contract()`.
* unreadable on-disk contract defers its failure to `space.contract()`.
* Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}

@@ -414,4 +454,4 @@ * rather than aborting the load. The only rejections are catastrophic I/O

targetId,
app: appState.member,
extensions: extensionStates.map((state) => state.member),
app: appState.space,
extensions: extensionStates.map((state) => state.space),
checkIntegrity: (opts) => computeIntegrityViolations({

@@ -428,3 +468,3 @@ targetId,

return {
member: createContractSpaceMember({
space: createAggregateContractSpace({
spaceId: APP_SPACE_ID,

@@ -460,3 +500,3 @@ packages,

return {
member: createContractSpaceMember({
space: createAggregateContractSpace({
spaceId,

@@ -528,11 +568,83 @@ packages,

//#endregion
//#region src/aggregate/strategies/graph-walk.ts
//#region src/aggregate/strategies/plan-from-diff.ts
/**
* Walk a member's hydrated migration graph from the live marker to
* `member.headRef.hash`, covering every required invariant.
* Plan a migration for a single contract space from a diff against the full
* live schema, delegating to the family's `createPlanner(...).plan(...)`.
*
* The planner diffs the whole introspected schema, so it sees other contract
* spaces' tables as "extras"; the orchestration hands the planner the
* aggregate as an ownership oracle so it can ask, per extra, whether any
* space owns it — a sibling-owned table is left untouched, so the planner
* never emits a destructive drop for it, and this strategy holds no ownership
* logic of its own. The schema is never pruned before planning: cross-space
* foreign keys need every sibling table visible to the diff.
*
* The produced plan's `targetId` is set from `aggregateTargetId`
* (the aggregate's ambient target). The family's planner does not
* stamp `targetId` on the produced plan; the aggregate planner is
* the single point that knows the target.
*
* Used by:
*
* - The app space by default (CLI policy
* `ignoreGraphFor: { app.spaceId }`).
* - Any extension space whose `headRef.invariants` is empty and whose
* graph is non-empty (the all-external, zero-migration-package case is
* handled directly by the aggregate planner without calling this).
*/
async function planFromDiff(input) {
const plannerResult = await input.migrations.createPlanner(input.adapter).plan({
contract: input.space.contract(),
schema: input.schemaIntrospection,
policy: input.operationPolicy,
fromContract: null,
frameworkComponents: input.frameworkComponents,
spaceId: input.space.spaceId,
ownership: input.ownership
});
if (plannerResult.kind === "failure") return {
kind: "failure",
conflicts: plannerResult.conflicts
};
const producedPlan = plannerResult.plan;
const plan = new Proxy(producedPlan, {
get(target, prop) {
if (prop === "targetId") return input.aggregateTargetId;
return Reflect.get(target, prop, target);
},
has(target, prop) {
if (prop === "targetId") return true;
return Reflect.has(target, prop);
}
});
const destinationStorageHash = producedPlan.destination.storageHash;
const producedOps = await Promise.all(producedPlan.operations);
const destinationContract = input.space.contract();
return {
kind: "ok",
result: {
plan,
displayOps: producedOps,
destinationContract,
strategy: "plan-from-diff",
...plannerResult.warnings && plannerResult.warnings.length > 0 ? { warnings: plannerResult.warnings } : {},
migrationEdges: [buildFabricatedMigrationEdge({
currentMarkerStorageHash: input.currentMarker?.storageHash,
destinationStorageHash,
operationCount: producedOps.length,
destinationContractJson: destinationContract
})]
}
};
}
//#endregion
//#region src/aggregate/strategies/resolve-recorded-path.ts
/**
* Resolve a contract space's hydrated migration graph to the path from the
* live marker to `space.headRef.hash`, covering every required invariant.
*
* Pure synchronous function — no I/O. The aggregate's loader has
* already integrity-checked every package and reconstructed the graph;
* this strategy just looks up ops by `migrationHash` and assembles a
* `MigrationPlan` with `targetId` set from the aggregate (no
* this resolves the path by looking up ops by `migrationHash` and
* assembles a `MigrationPlan` with `targetId` set from the aggregate (no
* placeholder cast).

@@ -545,7 +657,7 @@ *

*/
function graphWalkStrategy(input) {
const { aggregateTargetId, member, currentMarker, refName } = input;
const headRef = requireHeadRef(member);
const graph = member.graph();
const packagesByMigrationHash = new Map(member.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]));
function resolveRecordedPath(input) {
const { aggregateTargetId, space, currentMarker, refName } = input;
const headRef = requireHeadRef(space);
const graph = space.graph();
const packagesByMigrationHash = new Map(space.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]));
const fromHash = currentMarker?.storageHash ?? "sha256:empty";

@@ -568,3 +680,3 @@ const markerInvariants = new Set(currentMarker?.invariants ?? []);

const pkg = packagesByMigrationHash.get(edge.migrationHash);
if (!pkg) throw new Error(`Migration package missing for edge ${edge.migrationHash} in space "${member.spaceId}". The hydrated migration graph and packagesByMigrationHash map are out of sync — this should be unreachable; report.`);
if (!pkg) throw new Error(`Migration package missing for edge ${edge.migrationHash} in space "${space.spaceId}". The hydrated migration graph and packagesByMigrationHash map are out of sync — this should be unreachable; report.`);
for (const op of pkg.ops) pathOps.push(op);

@@ -577,3 +689,4 @@ for (const invariant of pkg.metadata.providedInvariants) providedInvariantsSet.add(invariant);

to: edge.to,
operationCount: pkg.ops.length
operationCount: pkg.ops.length,
...pkg.endContractJson !== void 0 ? { destinationContractJson: pkg.endContractJson } : {}
});

@@ -586,3 +699,3 @@ }

targetId: aggregateTargetId,
spaceId: member.spaceId,
spaceId: space.spaceId,
origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },

@@ -594,4 +707,4 @@ destination: { storageHash: headRef.hash },

displayOps: pathOps,
destinationContract: member.contract(),
strategy: "graph-walk",
destinationContract: space.contract(),
strategy: "resolve-recorded-path",
migrationEdges: edgeRefs,

@@ -603,201 +716,23 @@ pathDecision: outcome.decision

//#endregion
//#region src/aggregate/project-schema-to-space.ts
/**
* Project the **introspected live schema** to the slice claimed by a
* single contract-space member.
*
* "Schema" here means the live introspected database state — the
* planner / verifier sees this object as a `MongoSchemaIR` (Mongo) or
* `SqlSchemaIR` (SQL). It is **not** a database schema in the SQL
* `CREATE SCHEMA` sense, nor a contract-space namespace. The
* function's job is to filter that introspected state down to the
* elements claimed by one space, so a per-space verify pass doesn't
* see another space's storage as "extras".
*
* Returns the same `schema` value with every top-level storage element
* (table or collection) claimed by **other** members of the aggregate
* removed. Elements not claimed by any member flow through unchanged —
* the planner / verifier sees them as orphans (extras in strict mode).
*
* Used by:
*
* - The aggregate planner's **synth strategy**: when synthesising a
* plan against a member's contract, the live schema must be projected
* to that member's slice so the planner doesn't treat elements claimed
* by other members as "extras" and emit destructive ops to drop them.
* - The aggregate verifier's **schemaCheck**: projects per member so the
* single-contract verify only sees the slice claimed by the member it
* is checking. Closes the architectural concern that a multi-member
* deployment makes each member's elements look like extras to every
* other member's verify pass.
*
* **Duck-typing semantics**: the helper operates on `unknown` for the
* schema and falls through structurally if the shape doesn't match.
* Two storage shapes are recognised today:
*
* - SQL families expose `storage.tables: Record<string, ...>` on
* contracts and the introspected schema mirrors the same record shape.
* Pruning iterates the record entries.
* - Mongo exposes `storage.collections: Record<string, ...>` on
* contracts; the introspected `MongoSchemaIR` exposes
* `collections: ReadonlyArray<{name: string, ...}>`. Pruning iterates
* the array on the schema side and the record's keys on the
* other-member side.
*
* Schemas of unrecognised shape are returned unchanged. The function
* never imports family classes (`SqlSchemaIR`, `MongoSchemaIR`); the
* projected schema is a plain object — `{...schema, tables: pruned}` or
* `{...schema, collections: pruned}` — that downstream consumers
* duck-type. A future family with a different storage shape gets the
* schema returned unchanged rather than blowing up the aggregate
* planner.
*
* Record-shape detection guards against arrays (`!Array.isArray`) so
* an unrecognised array-shaped value falls through unchanged rather
* than being pruned by numeric keys.
*/
function projectSchemaToSpace(schema, member, otherMembers) {
if (typeof schema !== "object" || schema === null) return schema;
const ownedByOthers = collectOwnedNames(member, otherMembers);
if (ownedByOthers.size === 0) return schema;
const schemaObj = schema;
if (typeof schemaObj.tables === "object" && schemaObj.tables !== null && !Array.isArray(schemaObj.tables)) return pruneRecord(schemaObj, "tables", ownedByOthers);
if (Array.isArray(schemaObj.collections)) return pruneCollectionsArray(schemaObj, ownedByOthers);
if (typeof schemaObj.collections === "object" && schemaObj.collections !== null && !Array.isArray(schemaObj.collections)) return pruneRecord(schemaObj, "collections", ownedByOthers);
return schema;
}
function collectOwnedNames(member, otherMembers) {
const owned = /* @__PURE__ */ new Set();
for (const other of otherMembers) {
if (other.spaceId === member.spaceId) continue;
for (const { entityName } of elementCoordinates(other.contract().storage)) owned.add(entityName);
}
return owned;
}
function pruneRecord(schemaObj, field, ownedByOthers) {
const source = schemaObj[field];
let removed = false;
const pruned = {};
for (const [name, value] of Object.entries(source)) if (ownedByOthers.has(name)) removed = true;
else pruned[name] = value;
if (!removed) return schemaObj;
return {
...schemaObj,
[field]: pruned
};
}
function pruneCollectionsArray(schemaObj, ownedByOthers) {
const source = schemaObj.collections;
let removed = false;
const pruned = [];
for (const entry of source) {
if (typeof entry === "object" && entry !== null) {
const name = entry.name;
if (typeof name === "string" && ownedByOthers.has(name)) {
removed = true;
continue;
}
}
pruned.push(entry);
}
if (!removed) return schemaObj;
return {
...schemaObj,
collections: pruned
};
}
//#endregion
//#region src/aggregate/synth-migration-edge.ts
function buildSynthMigrationEdge(args) {
return {
dirName: "",
migrationHash: args.destinationStorageHash,
from: args.currentMarkerStorageHash ?? "",
to: args.destinationStorageHash,
operationCount: args.operationCount
};
}
//#endregion
//#region src/aggregate/strategies/synth.ts
/**
* Synthesise a migration plan for a single member by projecting the
* live schema down to that member's claimed slice and delegating to
* the family's `createPlanner(...).plan(...)`.
*
* Pre-projection (via {@link projectSchemaToSpace}) closes the F23
* concern: without it, the family's planner sees other members'
* tables as "extras" and emits destructive ops to drop them. With it,
* the planner only sees the slice this member claims.
*
* The synthesised plan's `targetId` is set from `aggregateTargetId`
* (the aggregate's ambient target). The family's planner does not
* stamp `targetId` on the produced plan; the aggregate planner is
* the single point that knows the target.
*
* Used by:
*
* - The app member by default (CLI policy
* `ignoreGraphFor: { app.spaceId }`).
* - Any extension member whose `headRef.invariants` is empty (the
* strategy selector falls back to synth when graph-walk isn't
* required).
*/
async function synthStrategy(input) {
const projectedSchema = projectSchemaToSpace(input.schemaIntrospection, input.member, input.otherMembers);
const plannerResult = await input.migrations.createPlanner(input.adapter).plan({
contract: input.member.contract(),
schema: projectedSchema,
policy: input.operationPolicy,
fromContract: null,
frameworkComponents: input.frameworkComponents,
spaceId: input.member.spaceId
});
if (plannerResult.kind === "failure") return {
kind: "failure",
conflicts: plannerResult.conflicts
};
const synthedPlan = plannerResult.plan;
const plan = new Proxy(synthedPlan, {
get(target, prop) {
if (prop === "targetId") return input.aggregateTargetId;
return Reflect.get(target, prop, target);
},
has(target, prop) {
if (prop === "targetId") return true;
return Reflect.has(target, prop);
}
});
const destinationStorageHash = synthedPlan.destination.storageHash;
const synthedOps = await Promise.all(synthedPlan.operations);
return {
kind: "ok",
result: {
plan,
displayOps: synthedOps,
destinationContract: input.member.contract(),
strategy: "synth",
...plannerResult.warnings && plannerResult.warnings.length > 0 ? { warnings: plannerResult.warnings } : {},
migrationEdges: [buildSynthMigrationEdge({
currentMarkerStorageHash: input.currentMarker?.storageHash,
destinationStorageHash,
operationCount: synthedOps.length
})]
}
};
}
//#endregion
//#region src/aggregate/planner.ts
/**
* Plan a migration across every member of a {@link ContractSpaceAggregate}.
* Plan a migration across every contract space of a {@link ContractSpaceAggregate}.
*
* Strategy selection per member, in order; first match wins:
* Per-space operation selection, in order; first match wins:
*
* 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
* - If `member.headRef.invariants` is empty → synth.
* - Else → `policyConflict` (synth cannot satisfy authored invariants).
* 2. Else if `member.graph()` is non-empty AND graph-walk
* succeeds → graph-walk.
* 3. Else if `member.headRef.invariants` is empty → synth.
* 4. Else → graph-walk failure → `extensionPathUnreachable` /
* `extensionPathUnsatisfiable`.
* 1. If `callerPolicy.ignoreGraphFor.has(space.spaceId)`:
* - If `space.headRef.invariants` is empty → `planFromDiff`.
* - Else → `policyConflict` (a diff-fabricated plan cannot satisfy
* authored invariants).
* 2. Else if `space.graph()` is non-empty AND `resolveRecordedPath`
* succeeds → its result.
* 3. Else if `space.graph()` is non-empty but unresolvable →
* `extensionPathUnreachable` / `extensionPathUnsatisfiable`.
* 4. Else (empty graph — the space ships no migration packages at all,
* e.g. an all-external extension space like Supabase's `auth`/`storage`)
* if `space.headRef.invariants` is empty → declare the no-op state
* directly (zero ops, destination = head ref) without invoking the
* family planner.
* 5. Else → `extensionPathUnsatisfiable` (an empty graph cannot satisfy
* non-empty invariants).
*

@@ -814,22 +749,20 @@ * Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`

const { aggregate, currentDBState, callerPolicy } = input;
const allMembers = [aggregate.app, ...aggregate.extensions];
const perSpace = /* @__PURE__ */ new Map();
const orderedMembers = [...aggregate.extensions, aggregate.app];
for (const member of orderedMembers) {
const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
const headRef = requireHeadRef(member);
const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
const orderedSpaces = [...aggregate.extensions, aggregate.app];
for (const space of orderedSpaces) {
const currentMarker = currentDBState.markersBySpaceId.get(space.spaceId) ?? null;
const headRef = requireHeadRef(space);
const ignoreGraph = callerPolicy.ignoreGraphFor.has(space.spaceId);
const invariantsRequired = headRef.invariants.length > 0;
if (ignoreGraph && invariantsRequired) return notOk({
kind: "policyConflict",
spaceId: member.spaceId,
detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${member.spaceId}", but the member declares non-empty head-ref invariants (${headRef.invariants.join(", ")}). Synthesising a plan from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${member.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`
spaceId: space.spaceId,
detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${space.spaceId}", but the contract space declares non-empty head-ref invariants (${headRef.invariants.join(", ")}). A plan built directly from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${space.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`
});
if (ignoreGraph) {
const synthOutcome = await synthStrategy({
const diffOutcome = await planFromDiff({
aggregateTargetId: aggregate.targetId,
currentMarker,
member,
otherMembers,
space,
ownership: aggregate,
schemaIntrospection: currentDBState.schemaIntrospection,

@@ -841,23 +774,23 @@ adapter: input.adapter,

});
if (synthOutcome.kind === "failure") return notOk({
kind: "appSynthFailure",
spaceId: member.spaceId,
conflicts: synthOutcome.conflicts
if (diffOutcome.kind === "failure") return notOk({
kind: "planFromDiffFailed",
spaceId: space.spaceId,
conflicts: diffOutcome.conflicts
});
perSpace.set(member.spaceId, synthOutcome.result);
perSpace.set(space.spaceId, diffOutcome.result);
continue;
}
if (member.graph().nodes.size > 0) {
const walked = graphWalkStrategy({
if (space.graph().nodes.size > 0) {
const resolved = resolveRecordedPath({
aggregateTargetId: aggregate.targetId,
member,
space,
currentMarker
});
if (walked.kind === "ok") {
perSpace.set(member.spaceId, walked.result);
if (resolved.kind === "ok") {
perSpace.set(space.spaceId, resolved.result);
continue;
}
if (walked.kind === "unreachable") return notOk({
if (resolved.kind === "unreachable") return notOk({
kind: "extensionPathUnreachable",
spaceId: member.spaceId,
spaceId: space.spaceId,
target: headRef.hash

@@ -867,4 +800,4 @@ });

kind: "extensionPathUnsatisfiable",
spaceId: member.spaceId,
missingInvariants: walked.missing
spaceId: space.spaceId,
missingInvariants: resolved.missing
});

@@ -874,22 +807,22 @@ }

kind: "extensionPathUnsatisfiable",
spaceId: member.spaceId,
spaceId: space.spaceId,
missingInvariants: [...headRef.invariants].sort()
});
const synthOutcome = await synthStrategy({
aggregateTargetId: aggregate.targetId,
currentMarker,
member,
otherMembers,
schemaIntrospection: currentDBState.schemaIntrospection,
adapter: input.adapter,
migrations: input.migrations,
frameworkComponents: input.frameworkComponents,
operationPolicy: input.operationPolicy
perSpace.set(space.spaceId, {
plan: {
targetId: aggregate.targetId,
spaceId: space.spaceId,
origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
destination: { storageHash: headRef.hash },
operations: []
},
displayOps: [],
destinationContract: space.contract(),
strategy: "declared-state",
migrationEdges: [buildFabricatedMigrationEdge({
currentMarkerStorageHash: currentMarker?.storageHash,
destinationStorageHash: headRef.hash,
operationCount: 0
})]
});
if (synthOutcome.kind === "failure") return notOk({
kind: "appSynthFailure",
spaceId: member.spaceId,
conflicts: synthOutcome.conflicts
});
perSpace.set(member.spaceId, synthOutcome.result);
}

@@ -901,2 +834,107 @@ return ok({

}
function pathIsUnder(path, prefix) {
if (path.length < prefix.length) return false;
return prefix.every((segment, i) => path[i] === segment);
}
/**
* Whether an issue's subject is a WHOLE top-level entity — as opposed to
* something nested under one (e.g. a field, an index, or a policy). Calls
* the injected `classify` capability, which resolves this from the issue's
* node `nodeKind` (this aggregate never reaches into the node itself).
* Absent `classify` (or a `classify` that returns nothing for this issue —
* a family that doesn't classify) falls back to path shape: a
* top-level entity's path is exactly its own name (one segment), so
* anything deeper is nested.
*/
function isWholeEntityIssue(issue, classify) {
const granularity = classify?.(issue);
if (granularity !== void 0) return granularity === "entity";
return issue.path.length === 1;
}
/**
* A contract space's contract-satisfaction view. Strips the top-level entity
* extras — the `not-expected` findings on whole-entity nodes (plus the
* findings the differ's total descent reported under those entities). Those
* belong to the standalone unclaimed-elements list
* ({@link collectExtraElementCoordinates}), never a space's own verdict.
*
* Nested `not-expected` findings (an extra field on the space's own
* declared entity…) and structural findings (an undeclared policy) are
* the space's **own drift** and stay.
*
* The verdict recomputes from the surviving list: the per-space result is
* issue-based (`ok` ⇔ the list is empty), so a space whose only failures
* were top-level extras passes after the strip.
*/
function stripExtraFindings(result, classify) {
const droppedTablePaths = result.schema.issues.filter((issue) => issue.reason === "not-expected" && isWholeEntityIssue(issue, classify)).map((issue) => issue.path);
const issues = result.schema.issues.filter((issue) => {
if (issue.reason !== "not-expected") return true;
if (classify?.(issue) === "structural") return true;
return !droppedTablePaths.some((prefix) => pathIsUnder(issue.path, prefix));
});
if (issues.length === result.schema.issues.length) return result;
const ok = issues.length === 0;
const { code: staleCode, ...envelope } = result;
return {
...envelope,
ok,
...ok ? {} : { code: result.code ?? "PN-RUN-3010" },
summary: ok ? "Database schema satisfies contract" : result.summary,
schema: {
issues,
...result.schema.warnings !== void 0 ? { warnings: result.schema.warnings } : {}
}
};
}
/**
* The schema-IR entity coordinate a `not-expected` `SchemaDiffIssue`
* addresses, when its subject is a whole top-level entity. A nested leaf
* (a field, an index, a policy on an undeclared entity) has no entity of
* its own to report here.
*
* The whole-entity's name is the last path segment: the differ builds each
* path from its nodes' ids, so at a whole-entity finding the last segment is
* exactly the entity name — for every family, whether or not it stamps a
* granularity. The namespace segment only exists for namespace-qualified
* paths (`['database', namespaceId, entityName]`); single-namespace families
* (a flat `['database', entityName]`, or a bare `[entityName]`) have no
* separate segment, so every entity they declare implicitly shares one
* namespace — the same sentinel the aggregate's own coordinate walk uses
* for those families.
*
* `entityKind` is asked from the injected `classifyEntityKind` capability —
* this module never names a family entity kind itself. Absent a classifier
* (the classifier-absent path-shape fallback, e.g. a document family) the
* coordinate carries {@link UNCLASSIFIED_ENTITY_KIND}: no ownership
* consumer queries `declaresEntity` for a family in that state today, so
* nothing reads the value as true.
*/
function schemaDiffIssueCoordinate(issue, classify, classifyEntityKind) {
if (!isWholeEntityIssue(issue, classify)) return void 0;
const entityName = issue.path[issue.path.length - 1];
if (entityName === void 0) return void 0;
return {
namespaceId: issue.path.length === 3 ? issue.path[1] ?? UNBOUND_NAMESPACE_ID : UNBOUND_NAMESPACE_ID,
entityKind: classifyEntityKind?.(issue) ?? "unclassified-entity",
entityName
};
}
/**
* The schema-IR entity coordinates of every live element this contract
* space's diff reports as `not-expected`, deduplicated. The verifier gathers
* these across all spaces and keeps only the coordinates no contract space
* declares — the standalone unclaimed-elements list, reported once for the
* whole database.
*/
function collectExtraElementCoordinates(result, classify, classifyEntityKind) {
const seen = /* @__PURE__ */ new Map();
for (const issue of result.schema.issues) {
if (issue.reason !== "not-expected") continue;
const coordinate = schemaDiffIssueCoordinate(issue, classify, classifyEntityKind);
if (coordinate === void 0) continue;
seen.set(coordinateKey(coordinate), coordinate);
}
return [...seen.values()];
}
//#endregion

@@ -908,15 +946,17 @@ //#region src/aggregate/verifier.ts

*
* - `markerCheck` per member: compare the live marker row against the
* member's `headRef.hash` + `headRef.invariants`. Absence is a
* - `markerCheck` per contract space: compare the live marker row against the
* space's `headRef.hash` + `headRef.invariants`. Absence is a
* distinct kind, not an error (callers — `db verify` strict vs
* `db init` precondition — choose how to interpret it).
* - `schemaCheck` per member: project the live schema to the slice
* the member claims via {@link projectSchemaToSpace}, then delegate
* to the caller-supplied `verifySchemaForMember`. The pre-projection
* means the family's single-contract verifier no longer sees other
* members' tables as `extras`, so a multi-member deployment never
* surfaces cross-member tables as orphaned schema elements.
* - `schemaCheck`: two distinct outputs from the per-space diffs.
* `perSpace` — each space verified against the **full**
* introspected schema, then its extras stripped, leaving the space's
* declared nodes (its contract-satisfaction view; verdict is
* missing/mismatch only). `unclaimed` — the extras gathered
* across every space, deduplicated, and filtered to the names no contract
* space declares (via the passive aggregate's ownership query); reported
* once for the database. No schema is pruned before verifying.
*
* `markerCheck.orphanMarkers` lists every marker row whose `space` is
* not a member of the aggregate. `db verify` callers reject orphans;
* not a contract space of the aggregate. `db verify` callers reject orphans;
* future tooling may not.

@@ -938,15 +978,15 @@ *

function runVerifyMigration(input) {
const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForMember } = input;
const allMembers = [aggregate.app, ...aggregate.extensions];
const memberSpaceIds = new Set(allMembers.map((m) => m.spaceId));
const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForSpace, classifySubjectGranularity, classifyEntityKind } = input;
const allSpaces = [aggregate.app, ...aggregate.extensions];
const aggregateSpaceIds = new Set(allSpaces.map((m) => m.spaceId));
const markerPerSpace = /* @__PURE__ */ new Map();
for (const member of allMembers) {
const marker = markersBySpaceId.get(member.spaceId) ?? null;
for (const space of allSpaces) {
const marker = markersBySpaceId.get(space.spaceId) ?? null;
if (marker === null) {
markerPerSpace.set(member.spaceId, { kind: "absent" });
markerPerSpace.set(space.spaceId, { kind: "absent" });
continue;
}
const headRef = requireHeadRef(member);
const headRef = requireHeadRef(space);
if (marker.storageHash !== headRef.hash) {
markerPerSpace.set(member.spaceId, {
markerPerSpace.set(space.spaceId, {
kind: "hashMismatch",

@@ -961,3 +1001,3 @@ markerHash: marker.storageHash,

if (missing.length > 0) {
markerPerSpace.set(member.spaceId, {
markerPerSpace.set(space.spaceId, {
kind: "missingInvariants",

@@ -968,6 +1008,6 @@ missing: [...missing].sort()

}
markerPerSpace.set(member.spaceId, { kind: "ok" });
markerPerSpace.set(space.spaceId, { kind: "ok" });
}
const orphanMarkers = [];
for (const [spaceId, row] of markersBySpaceId) if (row !== null && !memberSpaceIds.has(spaceId)) orphanMarkers.push({
for (const [spaceId, row] of markersBySpaceId) if (row !== null && !aggregateSpaceIds.has(spaceId)) orphanMarkers.push({
spaceId,

@@ -978,6 +1018,9 @@ row

const schemaPerSpace = /* @__PURE__ */ new Map();
for (const member of allMembers) {
const projected = projectSchemaToSpace(schemaIntrospection, member, allMembers.filter((m) => m.spaceId !== member.spaceId));
schemaPerSpace.set(member.spaceId, verifySchemaForMember(projected, member, mode));
const extraCoordinates = /* @__PURE__ */ new Map();
for (const space of allSpaces) {
const result = verifySchemaForSpace(schemaIntrospection, space, mode);
schemaPerSpace.set(space.spaceId, stripExtraFindings(result, classifySubjectGranularity));
for (const coordinate of collectExtraElementCoordinates(result, classifySubjectGranularity, classifyEntityKind)) extraCoordinates.set(coordinateKey(coordinate), coordinate);
}
const unclaimed = [...new Set([...extraCoordinates.values()].filter((coordinate) => !aggregate.declaresEntity(coordinate)).map((coordinate) => coordinate.entityName))].sort((a, b) => a.localeCompare(b));
return ok({

@@ -990,32 +1033,9 @@ markerCheck: {

perSpace: schemaPerSpace,
orphanElements: detectOrphanElements(schemaIntrospection, allMembers)
unclaimed
}
});
}
/**
* Live tables not claimed by any aggregate member. Duck-typed against
* the introspected schema's `tables` map; schemas whose shape doesn't
* match return an empty list (consistent with
* {@link projectSchemaToSpace}'s fall-through).
*/
function detectOrphanElements(schemaIntrospection, members) {
if (typeof schemaIntrospection !== "object" || schemaIntrospection === null) return [];
const liveTables = schemaIntrospection.tables;
if (typeof liveTables !== "object" || liveTables === null) return [];
const claimedTables = /* @__PURE__ */ new Set();
for (const member of members) {
const contract = member.contract();
for (const { entityName } of elementCoordinates(contract.storage)) claimedTables.add(entityName);
}
const orphans = [];
for (const tableName of Object.keys(liveTables)) if (!claimedTables.has(tableName)) orphans.push({
kind: "table",
name: tableName
});
orphans.sort((a, b) => a.name.localeCompare(b.name));
return orphans;
}
//#endregion
export { buildSynthMigrationEdge, collectAggregateNamespaces, computeIntegrityViolations, createContractSpaceAggregate, createContractSpaceMember, graphWalkStrategy, loadContractSpaceAggregate, loadProblemToViolation, planMigration, projectSchemaToSpace, requireHeadRef, verifyMigration };
export { buildFabricatedMigrationEdge, collectAggregateNamespaces, computeIntegrityViolations, createAggregateContractSpace, createContractSpaceAggregate, loadContractSpaceAggregate, loadProblemToViolation, planMigration, requireHeadRef, resolveRecordedPath, verifyMigration };
//# sourceMappingURL=aggregate.mjs.map

@@ -1,2 +0,2 @@

import { C as errorNoInvariantPath, D as errorUnknownInvariant, a as errorDescriptorHeadHashMismatch, f as errorInvalidJson, t as MigrationToolsError } from "../errors-DjTpjyFU.mjs";
import { O as errorUnknownInvariant, a as errorDescriptorHeadHashMismatch, f as errorInvalidJson, t as MigrationToolsError, w as errorNoInvariantPath } from "../errors-DIS_dcFJ.mjs";
export { MigrationToolsError, errorDescriptorHeadHashMismatch, errorInvalidJson, errorNoInvariantPath, errorUnknownInvariant };

@@ -1,2 +0,2 @@

import { n as validateInvariantId, t as deriveProvidedInvariants } from "../invariants-5wBqXSaJ.mjs";
import { n as validateInvariantId, t as deriveProvidedInvariants } from "../invariants-CH8ATaEp.mjs";
export { deriveProvidedInvariants, validateInvariantId };

@@ -1,2 +0,2 @@

import { a as materialiseMigrationPackage, c as writeMigrationMetadata, i as materialiseExtensionMigrationPackageIfMissing, l as writeMigrationOps, n as copyFilesWithRename, o as readMigrationPackage, r as formatMigrationDirName, s as readMigrationsDir, u as writeMigrationPackage } from "../io-DWg_qFDX.mjs";
import { a as materialiseMigrationPackage, c as writeMigrationMetadata, i as materialiseExtensionMigrationPackageIfMissing, l as writeMigrationOps, n as copyFilesWithRename, o as readMigrationPackage, r as formatMigrationDirName, s as readMigrationsDir, u as writeMigrationPackage } from "../io-CE79C4uK.mjs";
export { copyFilesWithRename, formatMigrationDirName, materialiseExtensionMigrationPackageIfMissing, materialiseMigrationPackage, readMigrationPackage, readMigrationsDir, writeMigrationMetadata, writeMigrationOps, writeMigrationPackage };

@@ -1,3 +0,3 @@

import { n as isGraphNode, t as assertHashIsGraphNode } from "../graph-membership-DEzWfpW0.mjs";
import { a as findPath, c as findReachableLeaves, i as findLeaf, l as reconstructGraph, n as detectOrphans, o as findPathWithDecision, r as findLatestMigration, s as findPathWithInvariants, t as detectCycles } from "../migration-graph-BW4ty9WV.mjs";
import { n as isGraphNode, t as assertHashIsGraphNode } from "../graph-membership-CNneL_Yy.mjs";
import { a as findPath, c as findReachableLeaves, i as findLeaf, l as reconstructGraph, n as detectOrphans, o as findPathWithDecision, r as findLatestMigration, s as findPathWithInvariants, t as detectCycles } from "../migration-graph-BXHOgdTg.mjs";
export { assertHashIsGraphNode, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findPathWithInvariants, findReachableLeaves, isGraphNode, reconstructGraph };
import { t as MigrationMetadata$1 } from "../metadata-DtStFLKa.mjs";
import { ControlStack, MigrationPlan, MigrationPlanOperation } from "@prisma-next/framework-components/control";
import { Contract } from "@prisma-next/contract/types";

@@ -14,9 +15,48 @@ //#region src/migration-base.d.ts

* runner can consume it directly via `targetId`, `operations`, `origin`, and
* `destination`. The metadata-shaped inputs come from `describe()`, which
* every migration must implement — `migration.json` is required for a
* migration to be valid.
* `destination`.
*
* The from/to identities come from `describe()`. A migration provides them in
* one of two ways:
* - **Contract-derived (default):** assign the committed `start-contract.json`
* / `end-contract.json` imports to `startContractJson` / `endContractJson`;
* the concrete `describe()` below derives `to`/`from` from their
* `storage.storageHash`. The family bases additionally expose typed view
* getters (`startContract` / `endContract`) over the same JSON.
* - **Override (e.g. extension migrations that carry no contract):** override
* `describe()` directly; the override wins and the JSON fields are unused.
*
* The `Start` / `End` generics carry each migration's precise contract types so
* the family-base view getters resolve to fully-typed views.
*/
declare abstract class Migration<_TOperation extends MigrationPlanOperation = MigrationPlanOperation, TFamilyId extends string = string, TTargetId extends string = string> implements MigrationPlan {
declare abstract class Migration<_TOperation extends MigrationPlanOperation = MigrationPlanOperation, TFamilyId extends string = string, TTargetId extends string = string, _Start extends Contract = Contract, _End extends Contract = Contract> implements MigrationPlan {
abstract readonly targetId: string;
/**
* The migration's end-state contract JSON (the committed `end-contract.json`
* import). When set, the derived `describe()` reads `to` from its
* `storage.storageHash`. Family bases build the typed `endContract` view from
* it. Optional so `describe()`-overriding migrations (no contract) compile.
*
* Typed with a plain `storageHash: string`, not the branded
* `StorageHashBase`, so a raw `contract.json` import — whose `storageHash`
* is an untyped string literal — is assignable without a cast. The full
* `Start`/`End` contract typing is applied downstream in the family bases'
* view getters (via `<Family>ContractView.fromJson<…>`).
*/
readonly endContractJson?: {
readonly storage: {
readonly storageHash: string;
};
};
/**
* The migration's start-state contract JSON (the committed
* `start-contract.json` import). Absent for a baseline migration (`from`
* derives to `null`). Family bases build the typed `startContract` view from
* it.
*/
readonly startContractJson?: {
readonly storage: {
readonly storageHash: string;
};
};
/**
* Assembled `ControlStack` injected by the orchestrator (`runMigration`).

@@ -42,6 +82,10 @@ *

* Metadata inputs used to build `migration.json` and to derive the plan's
* origin/destination identities. Every migration must provide this —
* omitting it would produce an invalid on-disk migration package.
* origin/destination identities.
*
* Default derivation: `to = endContractJson.storage.storageHash`,
* `from = startContractJson?.storage.storageHash ?? null`. A migration that
* carries no contract JSON (e.g. an extension migration) must override this;
* otherwise it throws, since `migration.json` requires a `to` identity.
*/
abstract describe(): MigrationMeta;
describe(): MigrationMeta;
get origin(): {

@@ -55,2 +99,24 @@ readonly storageHash: string;

/**
* Lazy-memoized `endContract` / `startContract` view accessors, one instance
* held per migration. Each target base (`MongoMigration`, `SqliteMigration`,
* `PostgresMigration`) creates one `MigrationContractViews` field from
* `this` — passing its own `<Family>ContractView.fromJson<…>` as `fromJson`
* and a name for error messages — and forwards its
* `endContract`/`startContract` getters to it.
*
* `endContract` throws `MIGRATION.CONTRACT_VIEW_MISSING` when the migration
* has no `endContractJson` (mirrors `describe()`'s own requirement).
* `startContract` returns `null` for a baseline migration (no
* `startContractJson`) instead of throwing.
*/
declare class MigrationContractViews<TView> {
#private;
private readonly migration;
private readonly className;
private readonly fromJson;
constructor(migration: Migration, className: string, fromJson: (json: unknown) => TView);
get endContract(): TView;
get startContract(): TView | null;
}
/**
* Returns true when `import.meta.url` resolves to the same file that was

@@ -90,3 +156,3 @@ * invoked as the node entrypoint (`process.argv[1]`). Used by

//#endregion
export { Migration, type MigrationArtifacts, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint };
export { Migration, type MigrationArtifacts, MigrationContractViews, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint };
//# sourceMappingURL=migration.d.mts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"mappings":";;;;UAeiB,aAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAE;AAAA;;;AAAA;AAsBb;;;;;;uBAAsB,SAAA,qBACA,sBAAA,GAAyB,sBAAA,mFAGlC,aAAA;EAAA,kBAEO,QAAA;EAae;;;;;;;;;EAAA,mBAFd,KAAA,EAAO,YAAA,CAAa,SAAA,EAAW,SAAA;cAEtC,KAAA,GAAQ,YAAA,CAAa,SAAA,EAAW,SAAA;EAlBxB;;;;;;;EAAA,aA6BP,UAAA,cAAwB,sBAAA,GAAyB,OAAA,CAAQ,sBAAA;EAb/B;;;;;EAAA,SAoB9B,QAAA,IAAY,aAAA;EAAA,IAEjB,MAAA;IAAA,SAAqB,WAAA;EAAA;EAAA,IAKrB,WAAA;IAAA,SAA0B,WAAA;EAAA;AAAA;;;;;;AAAW;AAY3C;iBAAgB,kBAAA,CAAmB,aAAqB;;;AAAA;AAsBxD;;;;;;;;UAAiB,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA,EAAU,mBAAiB;EAAA,SAC3B,YAAA;AAAA;;;;;;;;;;iBA4CW,uBAAA,CACpB,QAAA,EAAU,SAAA,EACV,QAAA,EAAU,OAAA,CAAQ,mBAAA,WACjB,OAAA,CAAQ,kBAAA"}
{"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"mappings":";;;;;UAgBiB,aAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAE;AAAA;;;AAAA;AAiCb;;;;;;;;;;;;;;;;;uBAAsB,SAAA,qBACA,sBAAA,GAAyB,sBAAA,uFAG9B,QAAA,GAAW,QAAA,eACb,QAAA,GAAW,QAAA,aACb,aAAA;EAAA,kBAEO,QAAA;EAFP;;;;;;;;;;;;EAAA,SAgBF,eAAA;IAAA,SAA6B,OAAA;MAAA,SAAoB,WAAA;IAAA;EAAA;EAApB;;;;;;EAAA,SAQ7B,iBAAA;IAAA,SAA+B,OAAA;MAAA,SAAoB,WAAA;IAAA;EAAA;EAa3B;;;;;;;;;EAAA,mBAFd,KAAA,EAAO,YAAA,CAAa,SAAA,EAAW,SAAA;cAEtC,KAAA,GAAQ,YAAA,CAAa,SAAA,EAAW,SAAA;EAwCxC;;;AAAqC;AAkB3C;;;EAlBM,aA7BS,UAAA,cAAwB,sBAAA,GAAyB,OAAA,CAAQ,sBAAA;EAsDtB;;;;;;;;;EA3ChD,QAAA,IAAY,aAAA;EAAA,IAaR,MAAA;IAAA,SAAqB,WAAA;EAAA;EAAA,IAKrB,WAAA;IAAA,SAA0B,WAAA;EAAA;AAAA;;;;;;AAuCJ;AAgB5B;;;;AAAwD;AAsBxD;;cA3Da,sBAAA;EAAA;mBAKQ,SAAA;EAAA,iBACA,SAAA;EAAA,iBACA,QAAA;cAFA,SAAA,EAAW,SAAA,EACX,SAAA,UACA,QAAA,GAAW,IAAA,cAAkB,KAAA;EAAA,IAG5C,WAAA,IAAe,KAAA;EAAA,IAWf,aAAA,IAAiB,KAAA;AAAA;;;;;;;;iBAgBP,kBAAA,CAAmB,aAAqB;;;;;;;;;;AAwE3B;;UAlDZ,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA,EAAU,mBAAiB;EAAA,SAC3B,YAAA;AAAA;;;;;;;;;;iBA4CW,uBAAA,CACpB,QAAA,EAAU,SAAA,EACV,QAAA,EAAU,OAAA,CAAQ,mBAAA,WACjB,OAAA,CAAQ,kBAAA"}

@@ -1,4 +0,4 @@

import { m as errorInvalidOperationEntry } from "../errors-DjTpjyFU.mjs";
import { b as errorMigrationContractViewMissing, m as errorInvalidOperationEntry } from "../errors-DIS_dcFJ.mjs";
import { t as computeMigrationHash } from "../hash-BNCgfhir.mjs";
import { t as deriveProvidedInvariants } from "../invariants-5wBqXSaJ.mjs";
import { t as deriveProvidedInvariants } from "../invariants-CH8ATaEp.mjs";
import { t as MigrationOpSchema } from "../op-schema-BSVHzEQN.mjs";

@@ -18,8 +18,39 @@ import { type } from "arktype";

* runner can consume it directly via `targetId`, `operations`, `origin`, and
* `destination`. The metadata-shaped inputs come from `describe()`, which
* every migration must implement — `migration.json` is required for a
* migration to be valid.
* `destination`.
*
* The from/to identities come from `describe()`. A migration provides them in
* one of two ways:
* - **Contract-derived (default):** assign the committed `start-contract.json`
* / `end-contract.json` imports to `startContractJson` / `endContractJson`;
* the concrete `describe()` below derives `to`/`from` from their
* `storage.storageHash`. The family bases additionally expose typed view
* getters (`startContract` / `endContract`) over the same JSON.
* - **Override (e.g. extension migrations that carry no contract):** override
* `describe()` directly; the override wins and the JSON fields are unused.
*
* The `Start` / `End` generics carry each migration's precise contract types so
* the family-base view getters resolve to fully-typed views.
*/
var Migration = class {
/**
* The migration's end-state contract JSON (the committed `end-contract.json`
* import). When set, the derived `describe()` reads `to` from its
* `storage.storageHash`. Family bases build the typed `endContract` view from
* it. Optional so `describe()`-overriding migrations (no contract) compile.
*
* Typed with a plain `storageHash: string`, not the branded
* `StorageHashBase`, so a raw `contract.json` import — whose `storageHash`
* is an untyped string literal — is assignable without a cast. The full
* `Start`/`End` contract typing is applied downstream in the family bases'
* view getters (via `<Family>ContractView.fromJson<…>`).
*/
endContractJson;
/**
* The migration's start-state contract JSON (the committed
* `start-contract.json` import). Absent for a baseline migration (`from`
* derives to `null`). Family bases build the typed `startContract` view from
* it.
*/
startContractJson;
/**
* Assembled `ControlStack` injected by the orchestrator (`runMigration`).

@@ -37,2 +68,19 @@ *

}
/**
* Metadata inputs used to build `migration.json` and to derive the plan's
* origin/destination identities.
*
* Default derivation: `to = endContractJson.storage.storageHash`,
* `from = startContractJson?.storage.storageHash ?? null`. A migration that
* carries no contract JSON (e.g. an extension migration) must override this;
* otherwise it throws, since `migration.json` requires a `to` identity.
*/
describe() {
const end = this.endContractJson;
if (end === void 0) throw new Error("Migration.describe(): provide endContractJson or override describe() — a migration needs a destination contract hash.");
return {
from: this.startContractJson?.storage.storageHash ?? null,
to: end.storage.storageHash
};
}
get origin() {

@@ -47,2 +95,42 @@ const from = this.describe().from;

/**
* Lazy-memoized `endContract` / `startContract` view accessors, one instance
* held per migration. Each target base (`MongoMigration`, `SqliteMigration`,
* `PostgresMigration`) creates one `MigrationContractViews` field from
* `this` — passing its own `<Family>ContractView.fromJson<…>` as `fromJson`
* and a name for error messages — and forwards its
* `endContract`/`startContract` getters to it.
*
* `endContract` throws `MIGRATION.CONTRACT_VIEW_MISSING` when the migration
* has no `endContractJson` (mirrors `describe()`'s own requirement).
* `startContract` returns `null` for a baseline migration (no
* `startContractJson`) instead of throwing.
*/
var MigrationContractViews = class {
migration;
className;
fromJson;
#endView;
#startView;
constructor(migration, className, fromJson) {
this.migration = migration;
this.className = className;
this.fromJson = fromJson;
}
get endContract() {
if (this.#endView === void 0) {
const json = this.migration.endContractJson;
if (json === void 0) throw errorMigrationContractViewMissing(this.className, "endContract", "endContractJson");
this.#endView = this.fromJson(json);
}
return this.#endView;
}
get startContract() {
if (this.#startView === void 0) {
const json = this.migration.startContractJson;
this.#startView = json === void 0 ? null : this.fromJson(json);
}
return this.#startView;
}
};
/**
* Returns true when `import.meta.url` resolves to the same file that was

@@ -110,3 +198,4 @@ * invoked as the node entrypoint (`process.argv[1]`). Used by

}
const parsed = MigrationMetaSchema(instance.describe());
const rawMeta = instance.describe();
const parsed = MigrationMetaSchema(rawMeta);
if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);

@@ -121,4 +210,4 @@ const metadata = buildAttestedMetadata(parsed, ops, existing);

//#endregion
export { Migration, buildMigrationArtifacts, isDirectEntrypoint };
export { Migration, MigrationContractViews, buildMigrationArtifacts, isDirectEntrypoint };
//# sourceMappingURL=migration.mjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"migration.mjs","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { type } from 'arktype';\nimport { errorInvalidOperationEntry } from './errors';\nimport { computeMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationMetadata } from './metadata';\nimport { MigrationOpSchema } from './op-schema';\nimport type { MigrationOps } from './package';\n\nexport interface MigrationMeta {\n readonly from: string | null;\n readonly to: string;\n}\n\n// `from` rejects empty strings to mirror `MigrationMetadataSchema` in\n// `./io.ts`. Without this match, an authored migration could `describe()` with\n// `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have\n// `readMigrationPackage` reject the resulting `migration.json` later — the\n// two validators must agree on the legal value space.\nconst MigrationMetaSchema = type({\n from: 'string > 0 | null',\n to: 'string',\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The metadata-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<\n _TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access. Entries may be Promises\n * when the target requires async codec resolution (e.g. DDL literal defaults).\n */\n abstract get operations(): readonly (MigrationPlanOperation | Promise<MigrationPlanOperation>)[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n return from === null ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` metadata object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n *\n * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical\n * on-disk shape that the arktype loader-schema in `./io` validates.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly metadata: MigrationMetadata;\n readonly metadataJson: string;\n}\n\n/**\n * Build the attested metadata from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded metadata (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve `createdAt`\n * set there — that field is owned by the CLI scaffolder, not the authored\n * class. Only the `describe()`-derived fields (`from`, `to`) and the\n * operations change as the author iterates. When no metadata exists yet\n * (a bare `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant record so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationHash` is recomputed against the current metadata + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedMetadata(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationMetadata> | null,\n): MigrationMetadata {\n const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {\n from: meta.from,\n to: meta.to,\n providedInvariants: deriveProvidedInvariants(ops),\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n };\n\n const migrationHash = computeMigrationHash(baseMetadata, ops);\n return { ...baseMetadata, migrationHash };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded metadata, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * metadata synthesis/preservation, and the content-addressed\n * `migrationHash` computation, but performs no file I/O — callers handle\n * reads (to source `existing`) and writes (to persist `opsJson` /\n * `metadataJson`).\n */\nexport async function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationMetadata> | null,\n): Promise<MigrationArtifacts> {\n const rawOps = instance.operations;\n if (!Array.isArray(rawOps)) {\n throw new Error('operations must be an array');\n }\n const ops = await Promise.all(rawOps);\n\n for (let index = 0; index < ops.length; index++) {\n const result = MigrationOpSchema(ops[index]);\n if (result instanceof type.errors) {\n throw errorInvalidOperationEntry(index, result.summary);\n }\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const metadata = buildAttestedMetadata(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n metadata,\n metadataJson: JSON.stringify(metadata, null, 2),\n };\n}\n"],"mappings":";;;;;;;;AAyBA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;AACN,CAAC;;;;;;;;;;AAWD,IAAsB,YAAtB,MAKA;;;;;;;;;;CAYE;CAEA,YAAY,OAA4C;EACtD,KAAK,QAAQ;CACf;CAkBA,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,SAAS,CAAC,CAAC;EAC7B,OAAO,SAAS,OAAO,OAAO,EAAE,aAAa,KAAK;CACpD;CAEA,IAAI,cAAgD;EAClD,OAAO,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC,GAAG;CAC3C;AACF;;;;;;;;AASA,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,aAAa;CAChD,MAAM,QAAQ,QAAQ,KAAK;CAC3B,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI;EACF,OAAO,aAAa,YAAY,MAAM,aAAa,KAAK;CAC1D,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;AAmCA,SAAS,sBACP,MACA,KACA,UACmB;CACnB,MAAM,eAAyD;EAC7D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,oBAAoB,yBAAyB,GAAG;EAChD,WAAW,UAAU,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;CAC3D;CAEA,MAAM,gBAAgB,qBAAqB,cAAc,GAAG;CAC5D,OAAO;EAAE,GAAG;EAAc;CAAc;AAC1C;;;;;;;;;;AAWA,eAAsB,wBACpB,UACA,UAC6B;CAC7B,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,MAAM,QAAQ,MAAM,GACvB,MAAM,IAAI,MAAM,6BAA6B;CAE/C,MAAM,MAAM,MAAM,QAAQ,IAAI,MAAM;CAEpC,KAAK,IAAI,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS;EAC/C,MAAM,SAAS,kBAAkB,IAAI,MAAM;EAC3C,IAAI,kBAAkB,KAAK,QACzB,MAAM,2BAA2B,OAAO,OAAO,OAAO;CAE1D;CAGA,MAAM,SAAS,oBADU,SAAS,SACO,CAAC;CAC1C,IAAI,kBAAkB,KAAK,QACzB,MAAM,IAAI,MAAM,yCAAyC,OAAO,SAAS;CAG3E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,QAAQ;CAE5D,OAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC;EACpC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,CAAC;CAChD;AACF"}
{"version":3,"file":"migration.mjs","names":["#endView","#startView"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { type } from 'arktype';\nimport { errorInvalidOperationEntry, errorMigrationContractViewMissing } from './errors';\nimport { computeMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationMetadata } from './metadata';\nimport { MigrationOpSchema } from './op-schema';\nimport type { MigrationOps } from './package';\n\nexport interface MigrationMeta {\n readonly from: string | null;\n readonly to: string;\n}\n\n// `from` rejects empty strings to mirror `MigrationMetadataSchema` in\n// `./io.ts`. Without this match, an authored migration could `describe()` with\n// `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have\n// `readMigrationPackage` reject the resulting `migration.json` later — the\n// two validators must agree on the legal value space.\nconst MigrationMetaSchema = type({\n from: 'string > 0 | null',\n to: 'string',\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`.\n *\n * The from/to identities come from `describe()`. A migration provides them in\n * one of two ways:\n * - **Contract-derived (default):** assign the committed `start-contract.json`\n * / `end-contract.json` imports to `startContractJson` / `endContractJson`;\n * the concrete `describe()` below derives `to`/`from` from their\n * `storage.storageHash`. The family bases additionally expose typed view\n * getters (`startContract` / `endContract`) over the same JSON.\n * - **Override (e.g. extension migrations that carry no contract):** override\n * `describe()` directly; the override wins and the JSON fields are unused.\n *\n * The `Start` / `End` generics carry each migration's precise contract types so\n * the family-base view getters resolve to fully-typed views.\n */\nexport abstract class Migration<\n _TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n _Start extends Contract = Contract,\n _End extends Contract = Contract,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * The migration's end-state contract JSON (the committed `end-contract.json`\n * import). When set, the derived `describe()` reads `to` from its\n * `storage.storageHash`. Family bases build the typed `endContract` view from\n * it. Optional so `describe()`-overriding migrations (no contract) compile.\n *\n * Typed with a plain `storageHash: string`, not the branded\n * `StorageHashBase`, so a raw `contract.json` import — whose `storageHash`\n * is an untyped string literal — is assignable without a cast. The full\n * `Start`/`End` contract typing is applied downstream in the family bases'\n * view getters (via `<Family>ContractView.fromJson<…>`).\n */\n readonly endContractJson?: { readonly storage: { readonly storageHash: string } };\n\n /**\n * The migration's start-state contract JSON (the committed\n * `start-contract.json` import). Absent for a baseline migration (`from`\n * derives to `null`). Family bases build the typed `startContract` view from\n * it.\n */\n readonly startContractJson?: { readonly storage: { readonly storageHash: string } };\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access. Entries may be Promises\n * when the target requires async codec resolution (e.g. DDL literal defaults).\n */\n abstract get operations(): readonly (MigrationPlanOperation | Promise<MigrationPlanOperation>)[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities.\n *\n * Default derivation: `to = endContractJson.storage.storageHash`,\n * `from = startContractJson?.storage.storageHash ?? null`. A migration that\n * carries no contract JSON (e.g. an extension migration) must override this;\n * otherwise it throws, since `migration.json` requires a `to` identity.\n */\n describe(): MigrationMeta {\n const end = this.endContractJson;\n if (end === undefined) {\n throw new Error(\n 'Migration.describe(): provide endContractJson or override describe() — a migration needs a destination contract hash.',\n );\n }\n return {\n from: this.startContractJson?.storage.storageHash ?? null,\n to: end.storage.storageHash,\n };\n }\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n return from === null ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Lazy-memoized `endContract` / `startContract` view accessors, one instance\n * held per migration. Each target base (`MongoMigration`, `SqliteMigration`,\n * `PostgresMigration`) creates one `MigrationContractViews` field from\n * `this` — passing its own `<Family>ContractView.fromJson<…>` as `fromJson`\n * and a name for error messages — and forwards its\n * `endContract`/`startContract` getters to it.\n *\n * `endContract` throws `MIGRATION.CONTRACT_VIEW_MISSING` when the migration\n * has no `endContractJson` (mirrors `describe()`'s own requirement).\n * `startContract` returns `null` for a baseline migration (no\n * `startContractJson`) instead of throwing.\n */\nexport class MigrationContractViews<TView> {\n #endView?: TView;\n #startView?: TView | null;\n\n constructor(\n private readonly migration: Migration,\n private readonly className: string,\n private readonly fromJson: (json: unknown) => TView,\n ) {}\n\n get endContract(): TView {\n if (this.#endView === undefined) {\n const json = this.migration.endContractJson;\n if (json === undefined) {\n throw errorMigrationContractViewMissing(this.className, 'endContract', 'endContractJson');\n }\n this.#endView = this.fromJson(json);\n }\n return this.#endView;\n }\n\n get startContract(): TView | null {\n if (this.#startView === undefined) {\n const json = this.migration.startContractJson;\n this.#startView = json === undefined ? null : this.fromJson(json);\n }\n return this.#startView;\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` metadata object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n *\n * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical\n * on-disk shape that the arktype loader-schema in `./io` validates.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly metadata: MigrationMetadata;\n readonly metadataJson: string;\n}\n\n/**\n * Build the attested metadata from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded metadata (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve `createdAt`\n * set there — that field is owned by the CLI scaffolder, not the authored\n * class. Only the `describe()`-derived fields (`from`, `to`) and the\n * operations change as the author iterates. When no metadata exists yet\n * (a bare `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant record so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationHash` is recomputed against the current metadata + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedMetadata(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationMetadata> | null,\n): MigrationMetadata {\n const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {\n from: meta.from,\n to: meta.to,\n providedInvariants: deriveProvidedInvariants(ops),\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n };\n\n const migrationHash = computeMigrationHash(baseMetadata, ops);\n return { ...baseMetadata, migrationHash };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded metadata, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * metadata synthesis/preservation, and the content-addressed\n * `migrationHash` computation, but performs no file I/O — callers handle\n * reads (to source `existing`) and writes (to persist `opsJson` /\n * `metadataJson`).\n */\nexport async function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationMetadata> | null,\n): Promise<MigrationArtifacts> {\n const rawOps = instance.operations;\n if (!Array.isArray(rawOps)) {\n throw new Error('operations must be an array');\n }\n const ops = await Promise.all(rawOps);\n\n for (let index = 0; index < ops.length; index++) {\n const result = MigrationOpSchema(ops[index]);\n if (result instanceof type.errors) {\n throw errorInvalidOperationEntry(index, result.summary);\n }\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const metadata = buildAttestedMetadata(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n metadata,\n metadataJson: JSON.stringify(metadata, null, 2),\n };\n}\n"],"mappings":";;;;;;;;AA0BA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;AACN,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBD,IAAsB,YAAtB,MAOA;;;;;;;;;;;;;CAeE;;;;;;;CAQA;;;;;;;;;;CAWA;CAEA,YAAY,OAA4C;EACtD,KAAK,QAAQ;CACf;;;;;;;;;;CAoBA,WAA0B;EACxB,MAAM,MAAM,KAAK;EACjB,IAAI,QAAQ,KAAA,GACV,MAAM,IAAI,MACR,uHACF;EAEF,OAAO;GACL,MAAM,KAAK,mBAAmB,QAAQ,eAAe;GACrD,IAAI,IAAI,QAAQ;EAClB;CACF;CAEA,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,SAAS,CAAC,CAAC;EAC7B,OAAO,SAAS,OAAO,OAAO,EAAE,aAAa,KAAK;CACpD;CAEA,IAAI,cAAgD;EAClD,OAAO,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC,GAAG;CAC3C;AACF;;;;;;;;;;;;;;AAeA,IAAa,yBAAb,MAA2C;CAKtB;CACA;CACA;CANnB;CACA;CAEA,YACE,WACA,WACA,UACA;EAHiB,KAAA,YAAA;EACA,KAAA,YAAA;EACA,KAAA,WAAA;CAChB;CAEH,IAAI,cAAqB;EACvB,IAAI,KAAKA,aAAa,KAAA,GAAW;GAC/B,MAAM,OAAO,KAAK,UAAU;GAC5B,IAAI,SAAS,KAAA,GACX,MAAM,kCAAkC,KAAK,WAAW,eAAe,iBAAiB;GAE1F,KAAKA,WAAW,KAAK,SAAS,IAAI;EACpC;EACA,OAAO,KAAKA;CACd;CAEA,IAAI,gBAA8B;EAChC,IAAI,KAAKC,eAAe,KAAA,GAAW;GACjC,MAAM,OAAO,KAAK,UAAU;GAC5B,KAAKA,aAAa,SAAS,KAAA,IAAY,OAAO,KAAK,SAAS,IAAI;EAClE;EACA,OAAO,KAAKA;CACd;AACF;;;;;;;;AASA,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,aAAa;CAChD,MAAM,QAAQ,QAAQ,KAAK;CAC3B,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI;EACF,OAAO,aAAa,YAAY,MAAM,aAAa,KAAK;CAC1D,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;AAmCA,SAAS,sBACP,MACA,KACA,UACmB;CACnB,MAAM,eAAyD;EAC7D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,oBAAoB,yBAAyB,GAAG;EAChD,WAAW,UAAU,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;CAC3D;CAEA,MAAM,gBAAgB,qBAAqB,cAAc,GAAG;CAC5D,OAAO;EAAE,GAAG;EAAc;CAAc;AAC1C;;;;;;;;;;AAWA,eAAsB,wBACpB,UACA,UAC6B;CAC7B,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,MAAM,QAAQ,MAAM,GACvB,MAAM,IAAI,MAAM,6BAA6B;CAE/C,MAAM,MAAM,MAAM,QAAQ,IAAI,MAAM;CAEpC,KAAK,IAAI,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS;EAC/C,MAAM,SAAS,kBAAkB,IAAI,MAAM;EAC3C,IAAI,kBAAkB,KAAK,QACzB,MAAM,2BAA2B,OAAO,OAAO,OAAO;CAE1D;CAEA,MAAM,UAAmB,SAAS,SAAS;CAC3C,MAAM,SAAS,oBAAoB,OAAO;CAC1C,IAAI,kBAAkB,KAAK,QACzB,MAAM,IAAI,MAAM,yCAAyC,OAAO,SAAS;CAG3E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,QAAQ;CAE5D,OAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC;EACpC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,CAAC;CAChD;AACF"}

@@ -1,2 +0,2 @@

import { l as validateRefName } from "../refs-kd62Ljkh.mjs";
import { l as validateRefName } from "../refs-CDIHeNcC.mjs";
import { notOk, ok } from "@prisma-next/utils/result";

@@ -3,0 +3,0 @@ //#region src/refs/types.ts

@@ -1,3 +0,3 @@

import { c as resolveRefsByContractHash, d as writeRef, i as readRefs, l as validateRefName, n as deleteRef, o as refsByContractHash, r as readRef, s as resolveRef, t as HEAD_REF_NAME, u as validateRefValue } from "../refs-kd62Ljkh.mjs";
import { a as writeRefSnapshot, i as writeRefPaired, n as deleteRefSnapshot, r as readRefSnapshot, t as deleteRefPaired } from "../snapshot-C-IHpIbr.mjs";
import { c as resolveRefsByContractHash, d as writeRef, i as readRefs, l as validateRefName, n as deleteRef, o as refsByContractHash, r as readRef, s as resolveRef, t as HEAD_REF_NAME, u as validateRefValue } from "../refs-CDIHeNcC.mjs";
import { a as writeRefSnapshot, i as writeRefPaired, n as deleteRefSnapshot, r as readRefSnapshot, t as deleteRefPaired } from "../snapshot-DiE5uFFJ.mjs";
export { HEAD_REF_NAME, deleteRef, deleteRefPaired, deleteRefSnapshot, readRef, readRefSnapshot, readRefs, refsByContractHash, resolveRef, resolveRefsByContractHash, validateRefName, validateRefValue, writeRef, writeRefPaired, writeRefSnapshot };
import { t as MigrationOps } from "../package-CWicKzNs.mjs";
import { a as VerifyContractSpacesResult, i as VerifyContractSpacesInputs, n as SpaceMarkerRecord, o as listContractSpaceDirectories, r as SpaceVerifierViolation, s as verifyContractSpaces, t as ContractSpaceHeadRecord } from "../verify-contract-spaces-CnXNGpBV.mjs";
import { a as VerifyContractSpacesResult, i as VerifyContractSpacesInputs, n as SpaceMarkerRecord, o as listContractSpaceDirectories, r as SpaceVerifierViolation, s as verifyContractSpaces, t as ContractSpaceHeadRecord } from "../verify-contract-spaces-zYlAhXOK.mjs";
import { PreserveEmptyPredicate, StorageSort } from "@prisma-next/contract/hashing";

@@ -301,3 +301,3 @@ import { APP_SPACE_ID, ContractSpace, ContractSpaceHeadRef, ContractSpaceHeadRef as ContractSpaceHeadRef$1 } from "@prisma-next/framework-components/control";

* `extensionPacks` whose descriptor exposes a `contractSpace`. The
* helper reads on-disk head data only for the extension members.
* helper reads on-disk head data only for the extension spaces.
*/

@@ -304,0 +304,0 @@ readonly loadedSpaceIds: ReadonlySet<string>;

@@ -1,6 +0,6 @@

import { a as errorDescriptorHeadHashMismatch, c as errorDuplicateSpaceId } from "../errors-DjTpjyFU.mjs";
import { s as readMigrationsDir } from "../io-DWg_qFDX.mjs";
import { a as errorDescriptorHeadHashMismatch, c as errorDuplicateSpaceId } from "../errors-DIS_dcFJ.mjs";
import { s as readMigrationsDir } from "../io-CE79C4uK.mjs";
import "../constants-YnG8_EGO.mjs";
import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-BW4ty9WV.mjs";
import { a as APP_SPACE_ID, c as assertValidSpaceId, d as spaceRefsDirectory, i as readContractSpaceHeadRef, l as isValidSpaceId, n as listContractSpaceDirectories, o as RESERVED_SPACE_SUBDIR_NAMES, r as verifyContractSpaces, s as SPACE_REFS_DIRNAME, t as readContractSpaceContract, u as spaceMigrationDirectory } from "../read-contract-space-contract-DyLWOWSt.mjs";
import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-BXHOgdTg.mjs";
import { a as APP_SPACE_ID, c as assertValidSpaceId, d as spaceRefsDirectory, i as readContractSpaceHeadRef, l as isValidSpaceId, n as listContractSpaceDirectories, o as RESERVED_SPACE_SUBDIR_NAMES, r as verifyContractSpaces, s as SPACE_REFS_DIRNAME, t as readContractSpaceContract, u as spaceMigrationDirectory } from "../read-contract-space-contract-DBW4-T5r.mjs";
import { ifDefined } from "@prisma-next/utils/defined";

@@ -7,0 +7,0 @@ import { join } from "pathe";

@@ -1,1 +0,1 @@

{"version":3,"file":"spaces.mjs","names":[],"sources":["../../src/assert-descriptor-self-consistency.ts","../../src/compute-extension-space-apply-path.ts","../../src/contract-space-from-json.ts","../../src/emit-contract-space-artefacts.ts","../../src/gather-disk-contract-space-state.ts","../../src/plan-all-spaces.ts"],"sourcesContent":["import type { PreserveEmptyPredicate, StorageSort } from '@prisma-next/contract/hashing';\nimport { computeStorageHash } from '@prisma-next/contract/hashing';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { errorDescriptorHeadHashMismatch } from './errors';\n\n/**\n * Inputs the helper needs to recompute the descriptor's storage hash and\n * compare it to the published `headRef.hash`. Kept structural so the SQL\n * family (and any future target family) can compose the check without\n * coupling to its own descriptor types.\n */\nexport interface DescriptorSelfConsistencyInputs {\n readonly extensionId: string;\n readonly target: string;\n readonly targetFamily: string;\n /**\n * Family-specific storage object. Typed as `unknown` so callers can\n * pass their own narrow storage shape (e.g. `SqlStorage`) without an\n * inline cast — the helper canonicalises through `JSON.stringify`\n * inside {@link computeStorageHash} and only requires a plain\n * record-shaped value at runtime.\n */\n readonly storage: unknown;\n readonly headRefHash: string;\n readonly shouldPreserveEmpty?: PreserveEmptyPredicate;\n readonly sortStorage?: StorageSort;\n}\n\n/**\n * Assert that an extension descriptor is self-consistent: the\n * `headRef.hash` it publishes must match the canonical hash recomputed\n * from its `contractSpace.contractJson`.\n *\n * Recomputes via {@link computeStorageHash} — the same canonical-JSON\n * pipeline the descriptor's own emit pipeline produced the hash with —\n * over `(target, targetFamily, storage)`. Mismatch indicates the\n * extension author bumped `contractJson` without rerunning emit, leaving\n * the descriptor's `headRef.hash` stale; the consumer-side helpers\n * (drift detection, on-disk artefact emission, runner marker writes) all\n * trust `headRef.hash` as the canonical identity, so a stale value would\n * silently corrupt every downstream boundary.\n *\n * Synchronous, pure, no I/O. Throws\n * `MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH` on failure with both the\n * recomputed and published hashes in `details` so callers can surface a\n * clear remediation hint without re-deriving them.\n */\nexport function assertDescriptorSelfConsistency(inputs: DescriptorSelfConsistencyInputs): void {\n // The published `storage.storageHash` is the *output* of the production\n // emit pipeline's `computeStorageHash` call, computed over a storage\n // object that did not yet carry `storageHash`. Recomputing against the\n // published storage as-is would feed the result back into its own input\n // and produce a different digest. Strip `storageHash` before\n // recomputing so the helper sees the same canonical shape the\n // descriptor's authoring pipeline saw.\n // The helper requires only a plain record-shaped storage value at\n // runtime; a single cast here keeps the public input type\n // family-agnostic (`unknown`) while still letting us strip the\n // descriptor-published `storageHash` before re-canonicalising.\n const storageRecord = inputs.storage as Record<string, unknown>;\n const { storageHash: _stripped, ...storageWithoutHash } = storageRecord;\n const recomputed = computeStorageHash({\n target: inputs.target,\n targetFamily: inputs.targetFamily,\n storage: storageWithoutHash,\n ...ifDefined('shouldPreserveEmpty', inputs.shouldPreserveEmpty),\n ...ifDefined('sortStorage', inputs.sortStorage),\n });\n if (recomputed !== inputs.headRefHash) {\n throw errorDescriptorHeadHashMismatch({\n extensionId: inputs.extensionId,\n recomputedHash: recomputed,\n headRefHash: inputs.headRefHash,\n });\n }\n}\n","import { EMPTY_CONTRACT_HASH } from './constants';\nimport { readMigrationsDir } from './io';\nimport { findPathWithDecision, reconstructGraph } from './migration-graph';\nimport type { MigrationOps } from './package';\nimport {\n type ContractSpaceHeadRef,\n readContractSpaceHeadRef,\n} from './read-contract-space-head-ref';\nimport { spaceMigrationDirectory } from './space-layout';\n\n/**\n * Outcome of {@link computeExtensionSpaceApplyPath} — a discriminated union\n * mirroring {@link import('./migration-graph').FindPathOutcome} so callers\n * can map structural / invariant failures to their preferred CLI envelope\n * without re-running pathfinding.\n */\nexport type ExtensionSpaceApplyPathOutcome =\n | {\n readonly kind: 'ok';\n readonly contractSpaceHeadRef: ContractSpaceHeadRef;\n /**\n * Sorted, deduplicated invariant ids covered by the walked path.\n * Mirrors the on-disk `providedInvariants` summed across edges and\n * canonicalised — what the runner stamps on the marker after apply.\n */\n readonly providedInvariants: readonly string[];\n /**\n * Path operations in apply order. Empty when the marker is already\n * at the recorded head (no-op).\n */\n readonly pathOps: MigrationOps;\n /**\n * Migration directory names walked, in order. Mirrors `pathOps`'s\n * structure but at the package granularity — useful for surfacing\n * \"applied N migration(s)\" messages.\n */\n readonly walkedMigrationDirs: readonly string[];\n }\n | { readonly kind: 'unreachable'; readonly contractSpaceHeadRef: ContractSpaceHeadRef }\n | {\n readonly kind: 'unsatisfiable';\n readonly contractSpaceHeadRef: ContractSpaceHeadRef;\n readonly missing: readonly string[];\n readonly structuralPath: readonly { readonly dirName: string; readonly to: string }[];\n }\n | { readonly kind: 'contractSpaceHeadRefMissing' };\n\n/**\n * Inputs to {@link computeExtensionSpaceApplyPath}. The helper is\n * deliberately framework-neutral and consumes only on-disk state:\n *\n * - `projectMigrationsDir` is the project's top-level `migrations/` dir.\n * - `spaceId` selects the per-space subdirectory under it.\n * - `currentMarkerHash` / `currentMarkerInvariants` come from the live\n * marker row keyed by `space = <spaceId>`. `null` hash = no marker yet\n * (the pathfinder treats this as the empty-contract sentinel per ADR\n * 208).\n */\nexport interface ComputeExtensionSpaceApplyPathInputs {\n readonly projectMigrationsDir: string;\n readonly spaceId: string;\n readonly currentMarkerHash: string | null;\n readonly currentMarkerInvariants: readonly string[];\n}\n\n/**\n * Compute the apply path for an extension contract space — the shortest\n * sequence of on-disk migration packages that walks the live marker\n * forward to the on-disk head ref hash, covering every required\n * invariant.\n *\n * Reads only on-disk artefacts (`migrations/<spaceId>/refs/head.json`\n * and the per-space migration packages). **Does not import any\n * extension descriptor module** — `db init` / `db update` must remain\n * runnable without the descriptor source on disk.\n *\n * Behaviour:\n * - Returns `{ kind: 'ok', pathOps: [], … }` when the marker is already\n * at the recorded head and no required invariants are missing.\n * - Returns `{ kind: 'unreachable' }` when the marker hash is not\n * structurally connected to the recorded head in the graph.\n * - Returns `{ kind: 'unsatisfiable', missing, … }` when the marker is\n * reachable but no path covers the required invariants.\n * - Returns `{ kind: 'contractSpaceHeadRefMissing' }` when the per-space\n * `refs/head.json` is absent — the precheck verifier should already\n * have rejected this case, but the helper is defensive so callers can\n * surface a coherent error rather than throw.\n */\nexport async function computeExtensionSpaceApplyPath(\n inputs: ComputeExtensionSpaceApplyPathInputs,\n): Promise<ExtensionSpaceApplyPathOutcome> {\n const { projectMigrationsDir, spaceId, currentMarkerHash, currentMarkerInvariants } = inputs;\n\n const contractSpaceHeadRef = await readContractSpaceHeadRef(projectMigrationsDir, spaceId);\n if (contractSpaceHeadRef === null) {\n return { kind: 'contractSpaceHeadRefMissing' };\n }\n\n const spaceDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);\n const { packages } = await readMigrationsDir(spaceDir);\n const graph = reconstructGraph(packages);\n\n // Live-marker layer encodes \"no prior state\" as EMPTY_CONTRACT_HASH;\n // mirror the `migrate` flow so a fresh-marker initial walk\n // hits the baseline migration whose `from` is EMPTY_CONTRACT_HASH.\n const fromHash = currentMarkerHash ?? EMPTY_CONTRACT_HASH;\n const required = new Set(\n contractSpaceHeadRef.invariants.filter((id) => !currentMarkerInvariants.includes(id)),\n );\n\n const outcome = findPathWithDecision(graph, fromHash, contractSpaceHeadRef.hash, { required });\n\n if (outcome.kind === 'unreachable') {\n return { kind: 'unreachable', contractSpaceHeadRef };\n }\n if (outcome.kind === 'unsatisfiable') {\n return {\n kind: 'unsatisfiable',\n contractSpaceHeadRef,\n missing: outcome.missing,\n structuralPath: outcome.structuralPath.map(({ dirName, to }) => ({ dirName, to })),\n };\n }\n\n const packagesByHash = new Map(packages.map((pkg) => [pkg.metadata.migrationHash, pkg]));\n\n const pathOps: MigrationOps[number][] = [];\n const walkedMigrationDirs: string[] = [];\n const providedInvariantsSet = new Set<string>();\n for (const edge of outcome.decision.selectedPath) {\n const pkg = packagesByHash.get(edge.migrationHash);\n if (!pkg) {\n // Path edges always come from the same `packages` array, so this\n // is only reachable when the graph is internally inconsistent —\n // surface it loudly rather than silently truncating the path.\n throw new Error(\n `Migration package missing for edge ${edge.migrationHash} in space \"${spaceId}\"`,\n );\n }\n walkedMigrationDirs.push(pkg.dirName);\n for (const op of pkg.ops) pathOps.push(op);\n for (const invariant of pkg.metadata.providedInvariants) providedInvariantsSet.add(invariant);\n }\n\n return {\n kind: 'ok',\n contractSpaceHeadRef,\n providedInvariants: [...providedInvariantsSet].sort(),\n pathOps,\n walkedMigrationDirs,\n };\n}\n","import type { Contract } from '@prisma-next/contract/types';\nimport type {\n ContractSpace,\n ContractSpaceHeadRef,\n MigrationPackage,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport type { MigrationMetadata } from './metadata';\n\n/**\n * Materialise a typed {@link ContractSpace} from the JSON artefacts a\n * contract-space extension package emits to disk.\n *\n * Extension descriptors wire `contract.json`, per-migration\n * `migration.json` / `ops.json`, and `refs/head.json` to the framework's\n * typed surfaces. TypeScript widens JSON imports to a structural record\n * that does not preserve readonly modifiers or branded scalars (e.g.\n * `StorageHashBase<'sha256:...'>`), so authoring the descriptor inline\n * forces every wiring site to cast through `unknown`. This helper\n * encapsulates the single narrowing point: descriptor sources stay\n * cast-free, and the (necessary) coercion is colocated with the\n * documentation explaining why it is safe.\n *\n * Safety: the JSON files passed here are produced by the framework's own\n * emit pipeline (`prisma-next contract emit` and `MigrationCLI.run`)\n * and re-validated downstream by the runner / verifier. The descriptor\n * is a pass-through wiring layer — no descriptor consumer treats the\n * narrowed types as a stronger guarantee than \"these came from the\n * canonical emit pipeline\".\n *\n * The helper does not introspect or schema-validate the inputs; runtime\n * validation is the responsibility of `family.deserializeContract`\n * (codec-aware, invoked at control-stack construction) and the\n * per-migration `readMigrationPackage` reader used when loading\n * from disk. JSON-imported packages flow through the descriptor without\n * a disk read, so the equivalent runtime guarantee comes from the emit\n * pipeline that produced the JSON in the first place.\n */\nexport function contractSpaceFromJson<TContract extends Contract = Contract>(inputs: {\n readonly contractJson: unknown;\n readonly migrations: ReadonlyArray<{\n readonly dirName: string;\n readonly metadata: unknown;\n readonly ops: unknown;\n }>;\n readonly headRef: ContractSpaceHeadRef;\n}): ContractSpace<TContract> {\n // The narrowing happens once, here. Casting via `unknown` rather than a\n // direct cast preserves TS's structural soundness checks for the\n // inputs (they must be assignable to `unknown`, which is trivial); the\n // resulting type is the family-specific Contract / MigrationPackage\n // surface descriptors publish.\n const migrations: readonly MigrationPackage[] = inputs.migrations.map((m) => ({\n dirName: m.dirName,\n metadata: m.metadata as MigrationMetadata,\n ops: m.ops as readonly MigrationPlanOperation[],\n }));\n return {\n contractJson: inputs.contractJson as TContract,\n migrations,\n headRef: inputs.headRef,\n };\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { join } from 'pathe';\nimport type { ContractSpaceHeadRef } from './read-contract-space-head-ref';\nimport { assertValidSpaceId, spaceRefsDirectory } from './space-layout';\n\n/**\n * Inputs for {@link emitContractSpaceArtefacts}.\n *\n * - `contract` is the canonical contract value the framework just emitted\n * for the space; it is serialised through {@link canonicalizeJson}, so\n * it must be a JSON-compatible value (objects / arrays / primitives).\n * Typed as `unknown` rather than the SQL-family `Contract<SqlStorage>`\n * to keep `migration-tools` framework-neutral; SQL-family callers pass\n * their typed value through unchanged.\n *\n * - `contractDts` is the pre-rendered `.d.ts` text. Rendering happens in\n * the SQL family (which owns the codec / typemap input the renderer\n * needs), so this helper accepts the text verbatim and writes it out\n * without further transformation.\n *\n * - `headRef` is the head reference for the space.\n * `invariants` are sorted alphabetically before serialisation so two\n * callers passing the same set in different orders produce\n * byte-identical `refs/head.json`.\n */\nexport interface ContractSpaceArtefactInputs {\n readonly contract: unknown;\n readonly contractDts: string;\n readonly headRef: ContractSpaceHeadRef;\n}\n\n/**\n * Emit the per-space artefacts (`contract.json`, `contract.d.ts`,\n * `refs/head.json`) under `<projectMigrationsDir>/<spaceId>/`.\n *\n * Always-overwrite: the framework owns these files; running `migrate`\n * twice with the same inputs is a no-op observably (idempotent), but the\n * helper does not check pre-existing contents — re-emit always wins.\n *\n * Path layout matches the convention in\n * [`spaceMigrationDirectory`](./space-layout.ts). The space id is\n * validated against `[a-z][a-z0-9_-]{0,63}` via\n * {@link assertValidSpaceId} for filesystem-safety reasons; the helper\n * accepts every space uniformly (including the app space, default\n * `'app'`).\n *\n * The migrations directory and space subdirectory are created if they\n * do not yet exist (`mkdir { recursive: true }`).\n */\nexport async function emitContractSpaceArtefacts(\n projectMigrationsDir: string,\n spaceId: string,\n inputs: ContractSpaceArtefactInputs,\n): Promise<void> {\n assertValidSpaceId(spaceId);\n\n const dir = join(projectMigrationsDir, spaceId);\n const refsDir = spaceRefsDirectory(dir);\n await mkdir(refsDir, { recursive: true });\n\n await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(inputs.contract)}\\n`);\n await writeFile(join(dir, 'contract.d.ts'), inputs.contractDts);\n\n const sortedInvariants = [...inputs.headRef.invariants].sort();\n const headJson = canonicalizeJson({\n hash: inputs.headRef.hash,\n invariants: sortedInvariants,\n });\n await writeFile(join(refsDir, 'head.json'), `${headJson}\\n`);\n}\n","import { readContractSpaceHeadRef } from './read-contract-space-head-ref';\nimport { APP_SPACE_ID } from './space-layout';\nimport {\n type ContractSpaceHeadRecord,\n listContractSpaceDirectories,\n} from './verify-contract-spaces';\n\n/**\n * Disk-side inputs to {@link import('./verify-contract-spaces').verifyContractSpaces}\n * — gathered without touching the live database. The caller composes\n * this with the marker rows it reads from the runtime to invoke the\n * verifier.\n */\nexport interface DiskContractSpaceState {\n /** Contract-space directory names observed under `<projectMigrationsDir>/`. */\n readonly spaceDirsOnDisk: readonly string[];\n /** Head-ref `(hash, invariants)` per extension space. */\n readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;\n}\n\n/**\n * Read the on-disk state the per-space verifier needs:\n *\n * - The list of contract-space directories under\n * `<projectMigrationsDir>/` (via\n * {@link import('./verify-contract-spaces').listContractSpaceDirectories}).\n * - The on-disk head ref `(hash, invariants)` for each declared extension space\n * (via {@link readContractSpaceHeadRef}; missing on-disk artefacts are simply\n * omitted — the verifier reports them as `declaredButUnmigrated`).\n *\n * Synchronous in spirit but async due to filesystem reads. Reads only\n * the user's repo. **Does not import any extension descriptor module.**\n *\n * Composition convention: pure target-agnostic primitive in\n * `1-framework`; the SQL family (and any future target family) wires\n * it into its `dbInit` / `verify` flows alongside its own marker-row\n * read before invoking `verifyContractSpaces`.\n */\nexport async function gatherDiskContractSpaceState(args: {\n readonly projectMigrationsDir: string;\n /**\n * Set of space ids the project declares: `'app'` plus each entry in\n * `extensionPacks` whose descriptor exposes a `contractSpace`. The\n * helper reads on-disk head data only for the extension members.\n */\n readonly loadedSpaceIds: ReadonlySet<string>;\n}): Promise<DiskContractSpaceState> {\n const { projectMigrationsDir, loadedSpaceIds } = args;\n\n const spaceDirsOnDisk = await listContractSpaceDirectories(projectMigrationsDir);\n\n const headRefsBySpace = new Map<string, ContractSpaceHeadRecord>();\n for (const spaceId of loadedSpaceIds) {\n if (spaceId === APP_SPACE_ID) continue;\n const head = await readContractSpaceHeadRef(projectMigrationsDir, spaceId);\n if (head !== null) {\n headRefsBySpace.set(spaceId, head);\n }\n }\n\n return { spaceDirsOnDisk, headRefsBySpace };\n}\n","import { errorDuplicateSpaceId } from './errors';\n\n/**\n * Per-space input for {@link planAllSpaces}. One entry per loaded\n * contract space (the application's `'app'` plus each extension that\n * exposes a `contractSpace`).\n *\n * - `priorContract` is `null` for a space that has never been emitted\n * (no `migrations/<space-id>/contract.json` on disk yet); otherwise it\n * is the canonical contract value emitted for that space.\n * - `newContract` is the canonical contract value the planner is about\n * to emit for that space — for app-space, the just-emitted root\n * `contract.json`; for an extension space, the descriptor's\n * `contractSpace.contractJson`.\n */\nexport interface SpacePlanInput<TContract> {\n readonly spaceId: string;\n readonly priorContract: TContract | null;\n readonly newContract: TContract;\n}\n\nexport interface SpacePlanOutput<TPackage> {\n readonly spaceId: string;\n readonly migrationPackages: readonly TPackage[];\n}\n\n/**\n * Iterate the per-space planner across a set of loaded contract spaces\n * and return a deterministic shape regardless of declaration order.\n *\n * Behaviour:\n *\n * - The output is sorted alphabetically by `spaceId`. Two callers\n * passing the same set of inputs in different orders observe\n * byte-identical outputs.\n * - The per-space planner (`planSpace`) is called exactly once per\n * input, in alphabetical-by-spaceId order. Its return value is\n * attached to the corresponding output entry verbatim.\n * - Duplicate `spaceId`s in the input array throw\n * `MIGRATION.DUPLICATE_SPACE_ID` before any `planSpace` call runs,\n * keeping the planner pure when the input is malformed.\n *\n * The signature is generic over `TContract` and `TPackage` because the\n * shape is framework-neutral (SQL family today, Mongo family\n * eventually). Callers wire in whatever contract value and migration\n * package shape their family already speaks.\n *\n * Synchronous: the underlying per-space planner (target's\n * `MigrationPlanner.plan(...)`) is synchronous; callers that need to\n * resolve async I/O (e.g. reading on-disk `contract.json` from disk)\n * resolve it before calling `planAllSpaces` and pass the materialised\n * inputs through.\n */\nexport function planAllSpaces<TContract, TPackage>(\n inputs: readonly SpacePlanInput<TContract>[],\n planSpace: (input: SpacePlanInput<TContract>) => readonly TPackage[],\n): readonly SpacePlanOutput<TPackage>[] {\n const seen = new Set<string>();\n for (const input of inputs) {\n if (seen.has(input.spaceId)) {\n throw errorDuplicateSpaceId(input.spaceId);\n }\n seen.add(input.spaceId);\n }\n\n const sorted = [...inputs].sort((a, b) => {\n if (a.spaceId < b.spaceId) return -1;\n if (a.spaceId > b.spaceId) return 1;\n return 0;\n });\n\n return sorted.map((input) => ({\n spaceId: input.spaceId,\n migrationPackages: planSpace(input),\n }));\n}\n"],"mappings":"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"}
{"version":3,"file":"spaces.mjs","names":[],"sources":["../../src/assert-descriptor-self-consistency.ts","../../src/compute-extension-space-apply-path.ts","../../src/contract-space-from-json.ts","../../src/emit-contract-space-artefacts.ts","../../src/gather-disk-contract-space-state.ts","../../src/plan-all-spaces.ts"],"sourcesContent":["import type { PreserveEmptyPredicate, StorageSort } from '@prisma-next/contract/hashing';\nimport { computeStorageHash } from '@prisma-next/contract/hashing';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { errorDescriptorHeadHashMismatch } from './errors';\n\n/**\n * Inputs the helper needs to recompute the descriptor's storage hash and\n * compare it to the published `headRef.hash`. Kept structural so the SQL\n * family (and any future target family) can compose the check without\n * coupling to its own descriptor types.\n */\nexport interface DescriptorSelfConsistencyInputs {\n readonly extensionId: string;\n readonly target: string;\n readonly targetFamily: string;\n /**\n * Family-specific storage object. Typed as `unknown` so callers can\n * pass their own narrow storage shape (e.g. `SqlStorage`) without an\n * inline cast — the helper canonicalises through `JSON.stringify`\n * inside {@link computeStorageHash} and only requires a plain\n * record-shaped value at runtime.\n */\n readonly storage: unknown;\n readonly headRefHash: string;\n readonly shouldPreserveEmpty?: PreserveEmptyPredicate;\n readonly sortStorage?: StorageSort;\n}\n\n/**\n * Assert that an extension descriptor is self-consistent: the\n * `headRef.hash` it publishes must match the canonical hash recomputed\n * from its `contractSpace.contractJson`.\n *\n * Recomputes via {@link computeStorageHash} — the same canonical-JSON\n * pipeline the descriptor's own emit pipeline produced the hash with —\n * over `(target, targetFamily, storage)`. Mismatch indicates the\n * extension author bumped `contractJson` without rerunning emit, leaving\n * the descriptor's `headRef.hash` stale; the consumer-side helpers\n * (drift detection, on-disk artefact emission, runner marker writes) all\n * trust `headRef.hash` as the canonical identity, so a stale value would\n * silently corrupt every downstream boundary.\n *\n * Synchronous, pure, no I/O. Throws\n * `MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH` on failure with both the\n * recomputed and published hashes in `details` so callers can surface a\n * clear remediation hint without re-deriving them.\n */\nexport function assertDescriptorSelfConsistency(inputs: DescriptorSelfConsistencyInputs): void {\n // The published `storage.storageHash` is the *output* of the production\n // emit pipeline's `computeStorageHash` call, computed over a storage\n // object that did not yet carry `storageHash`. Recomputing against the\n // published storage as-is would feed the result back into its own input\n // and produce a different digest. Strip `storageHash` before\n // recomputing so the helper sees the same canonical shape the\n // descriptor's authoring pipeline saw.\n // The helper requires only a plain record-shaped storage value at\n // runtime; a single cast here keeps the public input type\n // family-agnostic (`unknown`) while still letting us strip the\n // descriptor-published `storageHash` before re-canonicalising.\n const storageRecord = inputs.storage as Record<string, unknown>;\n const { storageHash: _stripped, ...storageWithoutHash } = storageRecord;\n const recomputed = computeStorageHash({\n target: inputs.target,\n targetFamily: inputs.targetFamily,\n storage: storageWithoutHash,\n ...ifDefined('shouldPreserveEmpty', inputs.shouldPreserveEmpty),\n ...ifDefined('sortStorage', inputs.sortStorage),\n });\n if (recomputed !== inputs.headRefHash) {\n throw errorDescriptorHeadHashMismatch({\n extensionId: inputs.extensionId,\n recomputedHash: recomputed,\n headRefHash: inputs.headRefHash,\n });\n }\n}\n","import { EMPTY_CONTRACT_HASH } from './constants';\nimport { readMigrationsDir } from './io';\nimport { findPathWithDecision, reconstructGraph } from './migration-graph';\nimport type { MigrationOps } from './package';\nimport {\n type ContractSpaceHeadRef,\n readContractSpaceHeadRef,\n} from './read-contract-space-head-ref';\nimport { spaceMigrationDirectory } from './space-layout';\n\n/**\n * Outcome of {@link computeExtensionSpaceApplyPath} — a discriminated union\n * mirroring {@link import('./migration-graph').FindPathOutcome} so callers\n * can map structural / invariant failures to their preferred CLI envelope\n * without re-running pathfinding.\n */\nexport type ExtensionSpaceApplyPathOutcome =\n | {\n readonly kind: 'ok';\n readonly contractSpaceHeadRef: ContractSpaceHeadRef;\n /**\n * Sorted, deduplicated invariant ids covered by the walked path.\n * Mirrors the on-disk `providedInvariants` summed across edges and\n * canonicalised — what the runner stamps on the marker after apply.\n */\n readonly providedInvariants: readonly string[];\n /**\n * Path operations in apply order. Empty when the marker is already\n * at the recorded head (no-op).\n */\n readonly pathOps: MigrationOps;\n /**\n * Migration directory names walked, in order. Mirrors `pathOps`'s\n * structure but at the package granularity — useful for surfacing\n * \"applied N migration(s)\" messages.\n */\n readonly walkedMigrationDirs: readonly string[];\n }\n | { readonly kind: 'unreachable'; readonly contractSpaceHeadRef: ContractSpaceHeadRef }\n | {\n readonly kind: 'unsatisfiable';\n readonly contractSpaceHeadRef: ContractSpaceHeadRef;\n readonly missing: readonly string[];\n readonly structuralPath: readonly { readonly dirName: string; readonly to: string }[];\n }\n | { readonly kind: 'contractSpaceHeadRefMissing' };\n\n/**\n * Inputs to {@link computeExtensionSpaceApplyPath}. The helper is\n * deliberately framework-neutral and consumes only on-disk state:\n *\n * - `projectMigrationsDir` is the project's top-level `migrations/` dir.\n * - `spaceId` selects the per-space subdirectory under it.\n * - `currentMarkerHash` / `currentMarkerInvariants` come from the live\n * marker row keyed by `space = <spaceId>`. `null` hash = no marker yet\n * (the pathfinder treats this as the empty-contract sentinel per ADR\n * 208).\n */\nexport interface ComputeExtensionSpaceApplyPathInputs {\n readonly projectMigrationsDir: string;\n readonly spaceId: string;\n readonly currentMarkerHash: string | null;\n readonly currentMarkerInvariants: readonly string[];\n}\n\n/**\n * Compute the apply path for an extension contract space — the shortest\n * sequence of on-disk migration packages that walks the live marker\n * forward to the on-disk head ref hash, covering every required\n * invariant.\n *\n * Reads only on-disk artefacts (`migrations/<spaceId>/refs/head.json`\n * and the per-space migration packages). **Does not import any\n * extension descriptor module** — `db init` / `db update` must remain\n * runnable without the descriptor source on disk.\n *\n * Behaviour:\n * - Returns `{ kind: 'ok', pathOps: [], … }` when the marker is already\n * at the recorded head and no required invariants are missing.\n * - Returns `{ kind: 'unreachable' }` when the marker hash is not\n * structurally connected to the recorded head in the graph.\n * - Returns `{ kind: 'unsatisfiable', missing, … }` when the marker is\n * reachable but no path covers the required invariants.\n * - Returns `{ kind: 'contractSpaceHeadRefMissing' }` when the per-space\n * `refs/head.json` is absent — the precheck verifier should already\n * have rejected this case, but the helper is defensive so callers can\n * surface a coherent error rather than throw.\n */\nexport async function computeExtensionSpaceApplyPath(\n inputs: ComputeExtensionSpaceApplyPathInputs,\n): Promise<ExtensionSpaceApplyPathOutcome> {\n const { projectMigrationsDir, spaceId, currentMarkerHash, currentMarkerInvariants } = inputs;\n\n const contractSpaceHeadRef = await readContractSpaceHeadRef(projectMigrationsDir, spaceId);\n if (contractSpaceHeadRef === null) {\n return { kind: 'contractSpaceHeadRefMissing' };\n }\n\n const spaceDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);\n const { packages } = await readMigrationsDir(spaceDir);\n const graph = reconstructGraph(packages);\n\n // Live-marker layer encodes \"no prior state\" as EMPTY_CONTRACT_HASH;\n // mirror the `migrate` flow so a fresh-marker initial walk\n // hits the baseline migration whose `from` is EMPTY_CONTRACT_HASH.\n const fromHash = currentMarkerHash ?? EMPTY_CONTRACT_HASH;\n const required = new Set(\n contractSpaceHeadRef.invariants.filter((id) => !currentMarkerInvariants.includes(id)),\n );\n\n const outcome = findPathWithDecision(graph, fromHash, contractSpaceHeadRef.hash, { required });\n\n if (outcome.kind === 'unreachable') {\n return { kind: 'unreachable', contractSpaceHeadRef };\n }\n if (outcome.kind === 'unsatisfiable') {\n return {\n kind: 'unsatisfiable',\n contractSpaceHeadRef,\n missing: outcome.missing,\n structuralPath: outcome.structuralPath.map(({ dirName, to }) => ({ dirName, to })),\n };\n }\n\n const packagesByHash = new Map(packages.map((pkg) => [pkg.metadata.migrationHash, pkg]));\n\n const pathOps: MigrationOps[number][] = [];\n const walkedMigrationDirs: string[] = [];\n const providedInvariantsSet = new Set<string>();\n for (const edge of outcome.decision.selectedPath) {\n const pkg = packagesByHash.get(edge.migrationHash);\n if (!pkg) {\n // Path edges always come from the same `packages` array, so this\n // is only reachable when the graph is internally inconsistent —\n // surface it loudly rather than silently truncating the path.\n throw new Error(\n `Migration package missing for edge ${edge.migrationHash} in space \"${spaceId}\"`,\n );\n }\n walkedMigrationDirs.push(pkg.dirName);\n for (const op of pkg.ops) pathOps.push(op);\n for (const invariant of pkg.metadata.providedInvariants) providedInvariantsSet.add(invariant);\n }\n\n return {\n kind: 'ok',\n contractSpaceHeadRef,\n providedInvariants: [...providedInvariantsSet].sort(),\n pathOps,\n walkedMigrationDirs,\n };\n}\n","import type { Contract } from '@prisma-next/contract/types';\nimport type {\n ContractSpace,\n ContractSpaceHeadRef,\n MigrationPackage,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport type { MigrationMetadata } from './metadata';\n\n/**\n * Materialise a typed {@link ContractSpace} from the JSON artefacts a\n * contract-space extension package emits to disk.\n *\n * Extension descriptors wire `contract.json`, per-migration\n * `migration.json` / `ops.json`, and `refs/head.json` to the framework's\n * typed surfaces. TypeScript widens JSON imports to a structural record\n * that does not preserve readonly modifiers or branded scalars (e.g.\n * `StorageHashBase<'sha256:...'>`), so authoring the descriptor inline\n * forces every wiring site to cast through `unknown`. This helper\n * encapsulates the single narrowing point: descriptor sources stay\n * cast-free, and the (necessary) coercion is colocated with the\n * documentation explaining why it is safe.\n *\n * Safety: the JSON files passed here are produced by the framework's own\n * emit pipeline (`prisma-next contract emit` and `MigrationCLI.run`)\n * and re-validated downstream by the runner / verifier. The descriptor\n * is a pass-through wiring layer — no descriptor consumer treats the\n * narrowed types as a stronger guarantee than \"these came from the\n * canonical emit pipeline\".\n *\n * The helper does not introspect or schema-validate the inputs; runtime\n * validation is the responsibility of `family.deserializeContract`\n * (codec-aware, invoked at control-stack construction) and the\n * per-migration `readMigrationPackage` reader used when loading\n * from disk. JSON-imported packages flow through the descriptor without\n * a disk read, so the equivalent runtime guarantee comes from the emit\n * pipeline that produced the JSON in the first place.\n */\nexport function contractSpaceFromJson<TContract extends Contract = Contract>(inputs: {\n readonly contractJson: unknown;\n readonly migrations: ReadonlyArray<{\n readonly dirName: string;\n readonly metadata: unknown;\n readonly ops: unknown;\n }>;\n readonly headRef: ContractSpaceHeadRef;\n}): ContractSpace<TContract> {\n // The narrowing happens once, here. Casting via `unknown` rather than a\n // direct cast preserves TS's structural soundness checks for the\n // inputs (they must be assignable to `unknown`, which is trivial); the\n // resulting type is the family-specific Contract / MigrationPackage\n // surface descriptors publish.\n const migrations: readonly MigrationPackage[] = inputs.migrations.map((m) => ({\n dirName: m.dirName,\n metadata: m.metadata as MigrationMetadata,\n ops: m.ops as readonly MigrationPlanOperation[],\n }));\n return {\n contractJson: inputs.contractJson as TContract,\n migrations,\n headRef: inputs.headRef,\n };\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { join } from 'pathe';\nimport type { ContractSpaceHeadRef } from './read-contract-space-head-ref';\nimport { assertValidSpaceId, spaceRefsDirectory } from './space-layout';\n\n/**\n * Inputs for {@link emitContractSpaceArtefacts}.\n *\n * - `contract` is the canonical contract value the framework just emitted\n * for the space; it is serialised through {@link canonicalizeJson}, so\n * it must be a JSON-compatible value (objects / arrays / primitives).\n * Typed as `unknown` rather than the SQL-family `Contract<SqlStorage>`\n * to keep `migration-tools` framework-neutral; SQL-family callers pass\n * their typed value through unchanged.\n *\n * - `contractDts` is the pre-rendered `.d.ts` text. Rendering happens in\n * the SQL family (which owns the codec / typemap input the renderer\n * needs), so this helper accepts the text verbatim and writes it out\n * without further transformation.\n *\n * - `headRef` is the head reference for the space.\n * `invariants` are sorted alphabetically before serialisation so two\n * callers passing the same set in different orders produce\n * byte-identical `refs/head.json`.\n */\nexport interface ContractSpaceArtefactInputs {\n readonly contract: unknown;\n readonly contractDts: string;\n readonly headRef: ContractSpaceHeadRef;\n}\n\n/**\n * Emit the per-space artefacts (`contract.json`, `contract.d.ts`,\n * `refs/head.json`) under `<projectMigrationsDir>/<spaceId>/`.\n *\n * Always-overwrite: the framework owns these files; running `migrate`\n * twice with the same inputs is a no-op observably (idempotent), but the\n * helper does not check pre-existing contents — re-emit always wins.\n *\n * Path layout matches the convention in\n * [`spaceMigrationDirectory`](./space-layout.ts). The space id is\n * validated against `[a-z][a-z0-9_-]{0,63}` via\n * {@link assertValidSpaceId} for filesystem-safety reasons; the helper\n * accepts every space uniformly (including the app space, default\n * `'app'`).\n *\n * The migrations directory and space subdirectory are created if they\n * do not yet exist (`mkdir { recursive: true }`).\n */\nexport async function emitContractSpaceArtefacts(\n projectMigrationsDir: string,\n spaceId: string,\n inputs: ContractSpaceArtefactInputs,\n): Promise<void> {\n assertValidSpaceId(spaceId);\n\n const dir = join(projectMigrationsDir, spaceId);\n const refsDir = spaceRefsDirectory(dir);\n await mkdir(refsDir, { recursive: true });\n\n await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(inputs.contract)}\\n`);\n await writeFile(join(dir, 'contract.d.ts'), inputs.contractDts);\n\n const sortedInvariants = [...inputs.headRef.invariants].sort();\n const headJson = canonicalizeJson({\n hash: inputs.headRef.hash,\n invariants: sortedInvariants,\n });\n await writeFile(join(refsDir, 'head.json'), `${headJson}\\n`);\n}\n","import { readContractSpaceHeadRef } from './read-contract-space-head-ref';\nimport { APP_SPACE_ID } from './space-layout';\nimport {\n type ContractSpaceHeadRecord,\n listContractSpaceDirectories,\n} from './verify-contract-spaces';\n\n/**\n * Disk-side inputs to {@link import('./verify-contract-spaces').verifyContractSpaces}\n * — gathered without touching the live database. The caller composes\n * this with the marker rows it reads from the runtime to invoke the\n * verifier.\n */\nexport interface DiskContractSpaceState {\n /** Contract-space directory names observed under `<projectMigrationsDir>/`. */\n readonly spaceDirsOnDisk: readonly string[];\n /** Head-ref `(hash, invariants)` per extension space. */\n readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;\n}\n\n/**\n * Read the on-disk state the per-space verifier needs:\n *\n * - The list of contract-space directories under\n * `<projectMigrationsDir>/` (via\n * {@link import('./verify-contract-spaces').listContractSpaceDirectories}).\n * - The on-disk head ref `(hash, invariants)` for each declared extension space\n * (via {@link readContractSpaceHeadRef}; missing on-disk artefacts are simply\n * omitted — the verifier reports them as `declaredButUnmigrated`).\n *\n * Synchronous in spirit but async due to filesystem reads. Reads only\n * the user's repo. **Does not import any extension descriptor module.**\n *\n * Composition convention: pure target-agnostic primitive in\n * `1-framework`; the SQL family (and any future target family) wires\n * it into its `dbInit` / `verify` flows alongside its own marker-row\n * read before invoking `verifyContractSpaces`.\n */\nexport async function gatherDiskContractSpaceState(args: {\n readonly projectMigrationsDir: string;\n /**\n * Set of space ids the project declares: `'app'` plus each entry in\n * `extensionPacks` whose descriptor exposes a `contractSpace`. The\n * helper reads on-disk head data only for the extension spaces.\n */\n readonly loadedSpaceIds: ReadonlySet<string>;\n}): Promise<DiskContractSpaceState> {\n const { projectMigrationsDir, loadedSpaceIds } = args;\n\n const spaceDirsOnDisk = await listContractSpaceDirectories(projectMigrationsDir);\n\n const headRefsBySpace = new Map<string, ContractSpaceHeadRecord>();\n for (const spaceId of loadedSpaceIds) {\n if (spaceId === APP_SPACE_ID) continue;\n const head = await readContractSpaceHeadRef(projectMigrationsDir, spaceId);\n if (head !== null) {\n headRefsBySpace.set(spaceId, head);\n }\n }\n\n return { spaceDirsOnDisk, headRefsBySpace };\n}\n","import { errorDuplicateSpaceId } from './errors';\n\n/**\n * Per-space input for {@link planAllSpaces}. One entry per loaded\n * contract space (the application's `'app'` plus each extension that\n * exposes a `contractSpace`).\n *\n * - `priorContract` is `null` for a space that has never been emitted\n * (no `migrations/<space-id>/contract.json` on disk yet); otherwise it\n * is the canonical contract value emitted for that space.\n * - `newContract` is the canonical contract value the planner is about\n * to emit for that space — for app-space, the just-emitted root\n * `contract.json`; for an extension space, the descriptor's\n * `contractSpace.contractJson`.\n */\nexport interface SpacePlanInput<TContract> {\n readonly spaceId: string;\n readonly priorContract: TContract | null;\n readonly newContract: TContract;\n}\n\nexport interface SpacePlanOutput<TPackage> {\n readonly spaceId: string;\n readonly migrationPackages: readonly TPackage[];\n}\n\n/**\n * Iterate the per-space planner across a set of loaded contract spaces\n * and return a deterministic shape regardless of declaration order.\n *\n * Behaviour:\n *\n * - The output is sorted alphabetically by `spaceId`. Two callers\n * passing the same set of inputs in different orders observe\n * byte-identical outputs.\n * - The per-space planner (`planSpace`) is called exactly once per\n * input, in alphabetical-by-spaceId order. Its return value is\n * attached to the corresponding output entry verbatim.\n * - Duplicate `spaceId`s in the input array throw\n * `MIGRATION.DUPLICATE_SPACE_ID` before any `planSpace` call runs,\n * keeping the planner pure when the input is malformed.\n *\n * The signature is generic over `TContract` and `TPackage` because the\n * shape is framework-neutral (SQL family today, Mongo family\n * eventually). Callers wire in whatever contract value and migration\n * package shape their family already speaks.\n *\n * Synchronous: the underlying per-space planner (target's\n * `MigrationPlanner.plan(...)`) is synchronous; callers that need to\n * resolve async I/O (e.g. reading on-disk `contract.json` from disk)\n * resolve it before calling `planAllSpaces` and pass the materialised\n * inputs through.\n */\nexport function planAllSpaces<TContract, TPackage>(\n inputs: readonly SpacePlanInput<TContract>[],\n planSpace: (input: SpacePlanInput<TContract>) => readonly TPackage[],\n): readonly SpacePlanOutput<TPackage>[] {\n const seen = new Set<string>();\n for (const input of inputs) {\n if (seen.has(input.spaceId)) {\n throw errorDuplicateSpaceId(input.spaceId);\n }\n seen.add(input.spaceId);\n }\n\n const sorted = [...inputs].sort((a, b) => {\n if (a.spaceId < b.spaceId) return -1;\n if (a.spaceId > b.spaceId) return 1;\n return 0;\n });\n\n return sorted.map((input) => ({\n spaceId: input.spaceId,\n migrationPackages: planSpace(input),\n }));\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"io-q04IrTcd.d.mts","names":[],"sources":["../src/io.ts"],"mappings":";;;;iBAwCsB,qBAAA,CACpB,GAAA,UACA,QAAA,EAAU,iBAAA,EACV,GAAA,EAAK,YAAA,GACJ,OAAA;AAJH;;;;;;;;;;;;;;;AAIU;AAgDV;;;;;;;;;AAGU;AA2BV;;;;AAlFA,iBAoDsB,2BAAA,CACpB,SAAA,UACA,GAAA,EAAK,gBAAA,GACJ,OAAO;;;;;;AA8BmB;AA6B7B;;;;;;;;;;AAGU;AAUV;;;;iBA7CsB,6CAAA,CACpB,SAAA,UACA,GAAA,EAAK,gBAAA,GACJ,OAAO;EAAA,SAAY,OAAA;AAAA;;;AA6CZ;AAIV;;;;;;;;iBApBsB,mBAAA,CACpB,OAAA,UACA,KAAA;EAAA,SAA2B,UAAA;EAAA,SAA6B,QAAA;AAAA,MACvD,OAAO;AAAA,iBAUY,sBAAA,CACpB,GAAA,UACA,QAAA,EAAU,iBAAA,GACT,OAAO;AAAA,iBAIY,iBAAA,CAAkB,GAAA,UAAa,GAAA,EAAK,YAAA,GAAe,OAAO;AAAA,iBAI1D,oBAAA,CAAqB,GAAA,WAAc,OAAO,CAAC,sBAAA;;;;AAAsB;AAsKvF;;;;;;;;;;;KAAY,kBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SAA6C,OAAA;AAAA;EAAA,SAC7C,IAAA;EAAA,SAAoC,OAAA;EAAA,SAA0B,MAAA;AAAA;AAa7B;AAShD;;;;;;;;AATgD,UAF/B,uBAAA;EAAA,SACN,QAAA,WAAmB,sBAAA;EAAA,SACnB,QAAA,WAAmB,kBAAkB;AAAA;AAAA,iBAS1B,iBAAA,CAAkB,cAAA,WAAyB,OAAO,CAAC,uBAAA;AAAA,iBAkEzD,sBAAA,CAAuB,SAAA,EAAW,IAAI,EAAE,IAAA"}
{"version":3,"file":"io-q04IrTcd.d.mts","names":[],"sources":["../src/io.ts"],"mappings":";;;;iBA0CsB,qBAAA,CACpB,GAAA,UACA,QAAA,EAAU,iBAAA,EACV,GAAA,EAAK,YAAA,GACJ,OAAA;AAJH;;;;;;;;;;;;;;;AAIU;AAgDV;;;;;;;;;AAGU;AA2BV;;;;AAlFA,iBAoDsB,2BAAA,CACpB,SAAA,UACA,GAAA,EAAK,gBAAA,GACJ,OAAO;;;;;;AA8BmB;AA6B7B;;;;;;;;;;AAGU;AAUV;;;;iBA7CsB,6CAAA,CACpB,SAAA,UACA,GAAA,EAAK,gBAAA,GACJ,OAAO;EAAA,SAAY,OAAA;AAAA;;;AA6CZ;AAIV;;;;;;;;iBApBsB,mBAAA,CACpB,OAAA,UACA,KAAA;EAAA,SAA2B,UAAA;EAAA,SAA6B,QAAA;AAAA,MACvD,OAAO;AAAA,iBAUY,sBAAA,CACpB,GAAA,UACA,QAAA,EAAU,iBAAA,GACT,OAAO;AAAA,iBAIY,iBAAA,CAAkB,GAAA,UAAa,GAAA,EAAK,YAAA,GAAe,OAAO;AAAA,iBA8B1D,oBAAA,CAAqB,GAAA,WAAc,OAAO,CAAC,sBAAA;;;;AAAsB;AA2KvF;;;;;;;;;;;KAAY,kBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SAA6C,OAAA;AAAA;EAAA,SAC7C,IAAA;EAAA,SAAoC,OAAA;EAAA,SAA0B,MAAA;AAAA;AAa7B;AAShD;;;;;;;;AATgD,UAF/B,uBAAA;EAAA,SACN,QAAA,WAAmB,sBAAA;EAAA,SACnB,QAAA,WAAmB,kBAAkB;AAAA;AAAA,iBAS1B,iBAAA,CAAkB,cAAA,WAAyB,OAAO,CAAC,uBAAA;AAAA,iBAkEzD,sBAAA,CAAuB,SAAA,EAAW,IAAI,EAAE,IAAA"}
{
"name": "@prisma-next/migration-tools",
"version": "0.14.0",
"version": "0.15.0-dev.1",
"license": "Apache-2.0",

@@ -9,16 +9,16 @@ "type": "module",

"dependencies": {
"@prisma-next/contract": "0.14.0",
"@prisma-next/framework-components": "0.14.0",
"@prisma-next/utils": "0.14.0",
"arktype": "^2.2.0",
"@prisma-next/contract": "0.15.0-dev.1",
"@prisma-next/framework-components": "0.15.0-dev.1",
"@prisma-next/utils": "0.15.0-dev.1",
"arktype": "^2.2.2",
"pathe": "^2.0.3",
"prettier": "^3.8.3"
"prettier": "^3.9.4"
},
"devDependencies": {
"@prisma-next/test-utils": "0.14.0",
"@prisma-next/tsconfig": "0.14.0",
"@prisma-next/tsdown": "0.14.0",
"tsdown": "0.22.1",
"@prisma-next/test-utils": "0.15.0-dev.1",
"@prisma-next/tsconfig": "0.15.0-dev.1",
"@prisma-next/tsdown": "0.15.0-dev.1",
"tsdown": "0.22.3",
"typescript": "5.9.3",
"vitest": "4.1.8"
"vitest": "4.1.10"
},

@@ -25,0 +25,0 @@ "peerDependencies": {

import { readFile } from 'node:fs/promises';
import type { Contract, StorageNamespace } from '@prisma-next/contract/types';
import type { SchemaEntityCoordinate } from '@prisma-next/framework-components/control';
import { coordinateKey, elementCoordinates } from '@prisma-next/framework-components/ir';
import { join } from 'pathe';

@@ -22,6 +24,6 @@ import {

import type {
AggregateContractSpace,
ContractAtOptions,
ContractAtResult,
ContractSpaceAggregate,
ContractSpaceMember,
} from './types';

@@ -161,20 +163,20 @@

/**
* Resolve a member's head ref, asserting it is present. The apply/verify
* Resolve a contract space's head ref, asserting it is present. The apply/verify
* engine only runs after `checkIntegrity` has refused on `headRefMissing`,
* so a member reaching the planner / verifier without a head ref is a
* so a space reaching the planner / verifier without a head ref is a
* programming error (the integrity gate was skipped), not a user-facing
* state. The app member's head ref is always synthesised, so this only
* state. The app space's head ref is always synthesised, so this only
* ever guards an ungated extension space.
*/
export function requireHeadRef(member: ContractSpaceMember): ContractSpaceHeadRecord {
if (member.headRef === null) {
export function requireHeadRef(space: AggregateContractSpace): ContractSpaceHeadRecord {
if (space.headRef === null) {
throw new Error(
`Contract space "${member.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`,
`Contract space "${space.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`,
);
}
return member.headRef;
return space.headRef;
}
/**
* Build a {@link ContractSpaceMember} with lazily-memoised `graph()`,
* Build a {@link AggregateContractSpace} with lazily-memoised `graph()`,
* `contract()`, and `contractAt()` facets.

@@ -191,3 +193,3 @@ *

*/
export function createContractSpaceMember(args: {
export function createAggregateContractSpace(args: {
readonly spaceId: string;

@@ -200,3 +202,3 @@ readonly packages: readonly OnDiskMigrationPackage[];

readonly deserializeContract: (raw: unknown) => Contract;
}): ContractSpaceMember {
}): AggregateContractSpace {
const { spaceId, packages, refs, headRef, refsDir, resolveContract, deserializeContract } = args;

@@ -207,3 +209,3 @@ let graphMemo: MigrationGraph | undefined;

function memberGraph(): MigrationGraph {
function spaceGraph(): MigrationGraph {
graphMemo ??= reconstructGraph(packages);

@@ -218,3 +220,3 @@ return graphMemo;

headRef,
graph: memberGraph,
graph: spaceGraph,
contract() {

@@ -236,3 +238,3 @@ contractMemo ??= resolveContract();

packages,
graph: memberGraph(),
graph: spaceGraph(),
deserializeContract,

@@ -247,3 +249,3 @@ });

/**
* Collect the union of every namespace declared across all members of an
* Collect the union of every namespace declared across all contract spaces of an
* aggregate (app + extensions) and return a minimal object with the shape

@@ -254,3 +256,3 @@ * `{ storage: { namespaces } }` suitable for passing to

* Callers invoke this after the integrity gate (`buildContractSpaceAggregate`
* with `checkContracts: true`), so every `member.contract()` call is safe —
* with `checkContracts: true`), so every `space.contract()` call is safe —
* no try/catch is needed here.

@@ -262,5 +264,6 @@ */

const merged: Record<string, StorageNamespace> = {};
for (const member of aggregate.spaces()) {
for (const [key, ns] of Object.entries(member.contract().storage.namespaces)) {
merged[key] = ns;
for (const space of aggregate.spaces()) {
for (const [key, ns] of Object.entries(space.contract().storage.namespaces)) {
const existing = merged[key];
merged[key] = existing === undefined ? ns : mergeNamespaceEntries(existing, ns);
}

@@ -272,3 +275,18 @@ }

/**
* Assemble a {@link ContractSpaceAggregate} value from its members and a
* Union two contract spaces' declarations for the same namespace id, per
* entity kind. Two spaces may legitimately share a namespace (e.g. both
* declaring tables in `public`); element-level disjointness is enforced by
* `checkIntegrity`, so the kind maps never collide on a name — replacing the
* whole namespace would silently drop the earlier space's entities.
*/
function mergeNamespaceEntries(a: StorageNamespace, b: StorageNamespace): StorageNamespace {
const entries: Record<string, Readonly<Record<string, unknown>>> = { ...a.entries };
for (const [entityKind, kindMap] of Object.entries(b.entries)) {
entries[entityKind] = { ...entries[entityKind], ...kindMap };
}
return { ...a, entries };
}
/**
* Assemble a {@link ContractSpaceAggregate} value from its contract spaces and a
* `checkIntegrity` implementation. The query methods (`listSpaces` /

@@ -282,9 +300,19 @@ * `hasSpace` / `space` / `spaces`) are derived here so every aggregate —

readonly targetId: string;
readonly app: ContractSpaceMember;
readonly extensions: readonly ContractSpaceMember[];
readonly app: AggregateContractSpace;
readonly extensions: readonly AggregateContractSpace[];
readonly checkIntegrity: (opts?: IntegrityQueryOptions) => readonly IntegrityViolation[];
}): ContractSpaceAggregate {
const { targetId, app, extensions, checkIntegrity } = args;
const ordered: readonly ContractSpaceMember[] = [app, ...extensions];
const ordered: readonly AggregateContractSpace[] = [app, ...extensions];
const byId = new Map(ordered.map((m) => [m.spaceId, m]));
const spaceDeclares = (
space: AggregateContractSpace,
coordinate: SchemaEntityCoordinate,
): boolean => {
const key = coordinateKey(coordinate);
for (const coord of elementCoordinates(space.contract().storage)) {
if (coordinateKey(coord) === key) return true;
}
return false;
};
return {

@@ -298,4 +326,7 @@ targetId,

spaces: () => ordered,
declaresEntity: (coordinate) => ordered.some((space) => spaceDeclares(space, coordinate)),
declaringSpaces: (coordinate) =>
ordered.filter((space) => spaceDeclares(space, coordinate)).map((s) => s.spaceId),
checkIntegrity,
};
}

@@ -1,2 +0,2 @@

import { elementCoordinates } from '@prisma-next/framework-components/ir';
import { coordinateKey, elementCoordinates } from '@prisma-next/framework-components/ir';
import { EMPTY_CONTRACT_HASH } from '../constants';

@@ -12,7 +12,7 @@ import { MigrationToolsError } from '../errors';

import type { RefLoadProblem } from '../refs';
import type { ContractSpaceMember } from './types';
import type { AggregateContractSpace } from './types';
/**
* One space's load-time facts that `checkIntegrity` judges: the loaded
* member, the load-time problems `readMigrationsDir` surfaced for it, and
* contract space, the load-time problems `readMigrationsDir` surfaced for it, and
* whether it is the app space (the app head ref is synthesised, so the

@@ -22,3 +22,3 @@ * head-ref checks are skipped for it).

export interface IntegritySpaceState {
readonly member: ContractSpaceMember;
readonly space: AggregateContractSpace;
readonly problems: readonly PackageLoadProblem[];

@@ -58,4 +58,4 @@ /** Per-ref problems: a user ref `*.json` that exists but is unparseable. */

for (const { member, problems, refProblems, headRefProblem, isApp } of input.spaces) {
const { spaceId } = member;
for (const { space, problems, refProblems, headRefProblem, isApp } of input.spaces) {
const { spaceId } = space;

@@ -83,3 +83,3 @@ for (const problem of problems) {

for (const pkg of member.packages) {
for (const pkg of space.packages) {
const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;

@@ -93,3 +93,3 @@ const isSelfEdge = from === pkg.metadata.to;

violations.push(...duplicateMigrationHashViolations(spaceId, member.packages));
violations.push(...duplicateMigrationHashViolations(spaceId, space.packages));

@@ -105,9 +105,6 @@ // For non-app spaces: a missing head.json is always an authoring error

if (!isApp && headRefProblem === null) {
if (member.headRef === null) {
if (space.headRef === null) {
violations.push({ kind: 'headRefMissing', spaceId });
} else if (
member.packages.length > 0 &&
!headRefPresentInGraph(member, member.headRef.hash)
) {
violations.push({ kind: 'headRefNotInGraph', spaceId, hash: member.headRef.hash });
} else if (space.packages.length > 0 && !headRefPresentInGraph(space, space.headRef.hash)) {
violations.push({ kind: 'headRefNotInGraph', spaceId, hash: space.headRef.hash });
}

@@ -183,4 +180,4 @@ }

*/
function headRefPresentInGraph(member: ContractSpaceMember, headHash: string): boolean {
const graph = member.graph();
function headRefPresentInGraph(space: AggregateContractSpace, headHash: string): boolean {
const graph = space.graph();
if (graph.nodes.size === 0) {

@@ -197,3 +194,3 @@ return headHash === EMPTY_CONTRACT_HASH;

const out: IntegrityViolation[] = [];
const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.member.spaceId));
const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.space.spaceId));
const declaredIds = new Set(declaredExtensions.map((d) => d.id));

@@ -219,8 +216,8 @@

for (const { member } of input.spaces) {
let contract: ReturnType<ContractSpaceMember['contract']>;
for (const { space } of input.spaces) {
let contract: ReturnType<AggregateContractSpace['contract']>;
try {
contract = member.contract();
contract = space.contract();
} catch (error) {
out.push({ kind: 'contractUnreadable', spaceId: member.spaceId, detail: detailOf(error) });
out.push({ kind: 'contractUnreadable', spaceId: space.spaceId, detail: detailOf(error) });
continue;

@@ -232,3 +229,3 @@ }

kind: 'targetMismatch',
spaceId: member.spaceId,
spaceId: space.spaceId,
expected: input.targetId,

@@ -239,9 +236,9 @@ actual: contract.target,

for (const { namespaceId, entityKind, entityName } of elementCoordinates(contract.storage)) {
const key = `${namespaceId}:${entityKind}:${entityName}`;
for (const coordinate of elementCoordinates(contract.storage)) {
const key = coordinateKey(coordinate);
const claimers = elementClaimedBy.get(key);
if (claimers) claimers.push(member.spaceId);
if (claimers) claimers.push(space.spaceId);
else {
elementClaimedBy.set(key, [member.spaceId]);
elementLabel.set(key, `${namespaceId}.${entityName}`);
elementClaimedBy.set(key, [space.spaceId]);
elementLabel.set(key, `${coordinate.namespaceId}.${coordinate.entityName}`);
}

@@ -248,0 +245,0 @@ }

@@ -15,3 +15,3 @@ import type { Contract } from '@prisma-next/contract/types';

import { listContractSpaceDirectories } from '../verify-contract-spaces';
import { createContractSpaceAggregate, createContractSpaceMember } from './aggregate';
import { createAggregateContractSpace, createContractSpaceAggregate } from './aggregate';
import { computeIntegrityViolations, type IntegritySpaceState } from './check-integrity';

@@ -45,3 +45,3 @@ import type { ContractSpaceAggregate } from './types';

* omitted, a missing extension head ref leaves `headRef: null`, and an
* unreadable on-disk contract defers its failure to `member.contract()`.
* unreadable on-disk contract defers its failure to `space.contract()`.
* Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}

@@ -70,4 +70,4 @@ * rather than aborting the load. The only rejections are catastrophic I/O

targetId,
app: appState.member,
extensions: extensionStates.map((state) => state.member),
app: appState.space,
extensions: extensionStates.map((state) => state.space),
checkIntegrity: (opts) => computeIntegrityViolations({ targetId, spaces }, opts),

@@ -86,3 +86,3 @@ });

const member = createContractSpaceMember({
const space = createAggregateContractSpace({
spaceId: APP_SPACE_ID,

@@ -100,3 +100,3 @@ packages,

return {
member,
space,
problems,

@@ -139,3 +139,3 @@ refProblems,

const member = createContractSpaceMember({
const space = createAggregateContractSpace({
spaceId,

@@ -150,3 +150,3 @@ packages,

return { member, problems, refProblems, headRefProblem, isApp: false };
return { space, problems, refProblems, headRefProblem, isApp: false };
}

@@ -153,0 +153,0 @@

@@ -21,13 +21,14 @@ import type { Contract } from '@prisma-next/contract/types';

*
* - `ignoreGraphFor`: `Set<spaceId>`. For listed members, the planner
* forces the **synth** strategy (synthesise a plan from the contract
* IR via `familyInstance.createPlanner(...).plan(...)`) regardless of
* whether a graph is available. The CLI's daily-driver `db init` /
* `db update` pipelines pass `new Set([aggregate.app.spaceId])` to
* keep today's app-space behaviour: the user's authored
* `migrations/` directory is **not** walked for the app member, the
* plan is synthesised on the fly. Extension members are walked.
* - `ignoreGraphFor`: `Set<spaceId>`. For listed contract spaces, the planner
* forces **`planFromDiff`** (fabricate a plan from a diff against the
* contract IR via `familyInstance.createPlanner(...).plan(...)`)
* regardless of whether a graph is available. The CLI's daily-driver
* `db init` / `db update` pipelines pass `new Set([aggregate.app.spaceId])`
* to keep today's app-space behaviour: the user's authored
* `migrations/` directory is **not** walked for the app space, the
* plan is fabricated on the fly. Extension spaces are walked.
*
* Listing a member here whose `headRef.invariants` is non-empty is
* a `policyConflict` — synth cannot satisfy authored invariants.
* Listing a contract space here whose `headRef.invariants` is non-empty is
* a `policyConflict` — a diff-fabricated plan cannot satisfy authored
* invariants.
*/

@@ -44,7 +45,9 @@ export interface CallerPolicy {

* marker yet (greenfield space). The planner treats the marker's
* `storageHash` as the graph-walk's `from` node, falling back to
* {@link import('../constants').EMPTY_CONTRACT_HASH} when absent.
* `storageHash` as the recorded-path resolution's `from` node, falling
* back to {@link import('../constants').EMPTY_CONTRACT_HASH} when absent.
* - `schemaIntrospection`: the family's full live schema IR. Fed into
* the synth strategy after per-space pre-projection via
* {@link import('./project-schema-to-space').projectSchemaToSpace}.
* `planFromDiff` in full; the aggregate itself is handed to the planner as
* an ownership oracle, which the planner asks per live extra node whether
* any space owns it — no schema is pruned up front, and the planner never
* drops a node a sibling space owns.
*

@@ -63,4 +66,4 @@ * Callers (CLI commands) gather this via the family's

*
* The planner is target-agnostic but family-aware: per-member synth
* delegates to the family's `createPlanner(adapter).plan(...)`,
* The planner is target-agnostic but family-aware: per-space
* `planFromDiff` delegates to the family's `createPlanner(adapter).plan(...)`,
* which is why `adapter`, `migrations` (the

@@ -90,3 +93,3 @@ * `TargetMigrationsCapability`), and `frameworkComponents` are all

/**
* Per-member output of the planner. The runner ingests this
* Per-space output of the planner. The runner ingests this
* shape directly via a thin `toRunnerInput` adapter at the CLI.

@@ -99,15 +102,20 @@ *

* - `destinationContract`: the typed contract value the runner uses
* for post-apply verification. For the app member, the user's
* contract; for extension members, the on-disk `contract.json`.
* - `strategy`: which strategy produced this plan (`'graph-walk'` or
* `'synth'`). Surfaced for diagnostics; not consumed by the runner.
* for post-apply verification. For the app space, the user's
* contract; for extension spaces, the on-disk `contract.json`.
* - `strategy`: which operation produced this plan — `'resolve-recorded-path'`
* (walked a recorded migration path), `'plan-from-diff'` (fabricated a
* plan from a state diff), or `'declared-state'` (the space ships no
* migration packages at all; the aggregate declares the no-op directly
* without invoking either). Surfaced for diagnostics; not consumed by
* the runner.
*/
/**
* Per-edge metadata for the chain assembled by the graph-walk
* strategy. Lets `migrate` surface a per-migration `applied[]`
* entry (preserving the `migrationsApplied` count semantics) without
* re-walking the graph.
* Per-edge metadata for the chain assembled by resolving a contract
* space's recorded migration path. Lets `migrate` surface a per-migration
* `applied[]` entry (preserving the `migrationsApplied` count semantics)
* without re-walking the graph.
*
* `synth`-produced plans leave this absent — synthesised plans don't
* have authored edges to surface.
* `plan-from-diff`/`declared-state` plans leave this absent — they don't
* have authored edges to surface (a single fabricated edge is used
* instead, see {@link import('./fabricated-migration-edge').buildFabricatedMigrationEdge}).
*/

@@ -120,2 +128,12 @@ export interface AggregateMigrationEdgeRef {

readonly operationCount: number;
/**
* Contract IR JSON of the edge's destination state, threaded from the
* bundle's on-disk `end-contract.json` snapshot (`resolveRecordedPath`)
* or the space's destination contract (`planFromDiff`). Runners persist it as the
* edge's ledger-linked contract row so database tooling can render
* model-level diffs per applied migration; an edge's *before* state is
* the previous row's snapshot by chain construction. Absent when the
* source bundle carries no snapshot.
*/
readonly destinationContractJson?: unknown;
}

@@ -127,16 +145,17 @@

readonly destinationContract: Contract;
readonly strategy: 'graph-walk' | 'synth';
readonly strategy: 'resolve-recorded-path' | 'plan-from-diff' | 'declared-state';
readonly warnings?: readonly MigrationPlannerConflict[];
/**
* Per-edge breakdown of the chain. Graph-walk plans carry one entry per
* authored edge; synth and at-head plans carry a single synthesised edge.
* Per-edge breakdown of the chain. `resolve-recorded-path` plans carry
* one entry per authored edge; `plan-from-diff` and `declared-state`
* plans carry a single fabricated edge.
*/
readonly migrationEdges: readonly AggregateMigrationEdgeRef[];
/**
* Path decision data the strategy used to select the chain
* (alternative count, tie-break reasons, required/satisfied
* invariants, per-edge invariants). Populated by the graph-walk
* strategy; absent for synth-produced plans.
* Path decision data used to select the chain (alternative count,
* tie-break reasons, required/satisfied invariants, per-edge
* invariants). Populated by `resolveRecordedPath`; absent for
* `plan-from-diff`/`declared-state` plans.
*
* `migrate` surfaces this for the app member as
* `migrate` surfaces this for the app space as
* `MigrateSuccess.pathDecision` (back-compat with single-

@@ -163,3 +182,3 @@ * space callers).

* Discriminated failure variants for {@link planMigration}. Each
* variant short-circuits the plan; per-member errors carry the
* variant short-circuits the plan; per-space errors carry the
* `spaceId` so the CLI can surface a precise envelope.

@@ -175,3 +194,3 @@ */

| {
readonly kind: 'appSynthFailure';
readonly kind: 'planFromDiffFailed';
readonly spaceId: string;

@@ -178,0 +197,0 @@ readonly conflicts: readonly MigrationPlannerConflict[];

import { notOk, ok } from '@prisma-next/utils/result';
import { requireHeadRef } from './aggregate';
import { buildFabricatedMigrationEdge } from './fabricated-migration-edge';
import type { PerSpacePlan, PlannerError, PlannerInput, PlannerOutput } from './planner-types';
import { graphWalkStrategy } from './strategies/graph-walk';
import { synthStrategy } from './strategies/synth';
import type { ContractSpaceMember } from './types';
import { planFromDiff } from './strategies/plan-from-diff';
import { resolveRecordedPath } from './strategies/resolve-recorded-path';
import type { AggregateContractSpace } from './types';

@@ -20,14 +21,21 @@ export type {

/**
* Plan a migration across every member of a {@link ContractSpaceAggregate}.
* Plan a migration across every contract space of a {@link ContractSpaceAggregate}.
*
* Strategy selection per member, in order; first match wins:
* Per-space operation selection, in order; first match wins:
*
* 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
* - If `member.headRef.invariants` is empty → synth.
* - Else → `policyConflict` (synth cannot satisfy authored invariants).
* 2. Else if `member.graph()` is non-empty AND graph-walk
* succeeds → graph-walk.
* 3. Else if `member.headRef.invariants` is empty → synth.
* 4. Else → graph-walk failure → `extensionPathUnreachable` /
* `extensionPathUnsatisfiable`.
* 1. If `callerPolicy.ignoreGraphFor.has(space.spaceId)`:
* - If `space.headRef.invariants` is empty → `planFromDiff`.
* - Else → `policyConflict` (a diff-fabricated plan cannot satisfy
* authored invariants).
* 2. Else if `space.graph()` is non-empty AND `resolveRecordedPath`
* succeeds → its result.
* 3. Else if `space.graph()` is non-empty but unresolvable →
* `extensionPathUnreachable` / `extensionPathUnsatisfiable`.
* 4. Else (empty graph — the space ships no migration packages at all,
* e.g. an all-external extension space like Supabase's `auth`/`storage`)
* if `space.headRef.invariants` is empty → declare the no-op state
* directly (zero ops, destination = head ref) without invoking the
* family planner.
* 5. Else → `extensionPathUnsatisfiable` (an empty graph cannot satisfy
* non-empty invariants).
*

@@ -46,9 +54,8 @@ * Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`

const { aggregate, currentDBState, callerPolicy } = input;
const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
const perSpace = new Map<string, PerSpacePlan>();
// Iterate in apply order so a per-member error short-circuits the
// Iterate in apply order so a per-space error short-circuits the
// walk in the same order the runner would walk inputs.
const orderedMembers: ReadonlyArray<ContractSpaceMember> = [
const orderedSpaces: ReadonlyArray<AggregateContractSpace> = [
...aggregate.extensions,

@@ -58,8 +65,7 @@ aggregate.app,

for (const member of orderedMembers) {
const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
const headRef = requireHeadRef(member);
for (const space of orderedSpaces) {
const currentMarker = currentDBState.markersBySpaceId.get(space.spaceId) ?? null;
const headRef = requireHeadRef(space);
const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
const ignoreGraph = callerPolicy.ignoreGraphFor.has(space.spaceId);
const invariantsRequired = headRef.invariants.length > 0;

@@ -70,4 +76,4 @@

kind: 'policyConflict',
spaceId: member.spaceId,
detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${member.spaceId}", but the member declares non-empty head-ref invariants (${headRef.invariants.join(', ')}). Synthesising a plan from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${member.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`,
spaceId: space.spaceId,
detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${space.spaceId}", but the contract space declares non-empty head-ref invariants (${headRef.invariants.join(', ')}). A plan built directly from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${space.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`,
};

@@ -78,7 +84,7 @@ return notOk(conflict);

if (ignoreGraph) {
const synthOutcome = await synthStrategy({
const diffOutcome = await planFromDiff({
aggregateTargetId: aggregate.targetId,
currentMarker,
member,
otherMembers,
space,
ownership: aggregate,
schemaIntrospection: currentDBState.schemaIntrospection,

@@ -90,29 +96,30 @@ adapter: input.adapter,

});
if (synthOutcome.kind === 'failure') {
if (diffOutcome.kind === 'failure') {
return notOk({
kind: 'appSynthFailure',
spaceId: member.spaceId,
conflicts: synthOutcome.conflicts,
kind: 'planFromDiffFailed',
spaceId: space.spaceId,
conflicts: diffOutcome.conflicts,
});
}
perSpace.set(member.spaceId, synthOutcome.result);
perSpace.set(space.spaceId, diffOutcome.result);
continue;
}
// Try graph-walk first when the graph has nodes; fall back to synth
// when the graph is empty AND no invariants are required.
if (member.graph().nodes.size > 0) {
const walked = graphWalkStrategy({
// Resolve the recorded path first when the graph has nodes; fall back
// to the empty-graph case below when the graph is empty AND no
// invariants are required.
if (space.graph().nodes.size > 0) {
const resolved = resolveRecordedPath({
aggregateTargetId: aggregate.targetId,
member,
space,
currentMarker,
});
if (walked.kind === 'ok') {
perSpace.set(member.spaceId, walked.result);
if (resolved.kind === 'ok') {
perSpace.set(space.spaceId, resolved.result);
continue;
}
if (walked.kind === 'unreachable') {
if (resolved.kind === 'unreachable') {
return notOk({
kind: 'extensionPathUnreachable',
spaceId: member.spaceId,
spaceId: space.spaceId,
target: headRef.hash,

@@ -124,13 +131,15 @@ });

kind: 'extensionPathUnsatisfiable',
spaceId: member.spaceId,
missingInvariants: walked.missing,
spaceId: space.spaceId,
missingInvariants: resolved.missing,
});
}
// Empty graph: synth is the only option, and it can only satisfy
// empty-invariant members.
// Empty graph: the space ships no migration packages at all — every
// real case is an all-external extension space (e.g. Supabase's
// `auth`/`storage`) with nothing for it to manage. It can only ever
// satisfy empty-invariant contract spaces.
if (invariantsRequired) {
return notOk({
kind: 'extensionPathUnsatisfiable',
spaceId: member.spaceId,
spaceId: space.spaceId,
missingInvariants: [...headRef.invariants].sort(),

@@ -140,21 +149,28 @@ });

const synthOutcome = await synthStrategy({
aggregateTargetId: aggregate.targetId,
currentMarker,
member,
otherMembers,
schemaIntrospection: currentDBState.schemaIntrospection,
adapter: input.adapter,
migrations: input.migrations,
frameworkComponents: input.frameworkComponents,
operationPolicy: input.operationPolicy,
// Declare the no-op state directly instead of invoking the family
// planner: a diff-fabricated plan only ever came out empty here
// anyway (control-policy disposition drops everything the space
// doesn't manage), so this constructs that same fixed point without
// depending on the space having nothing managed — honoring
// check-integrity's rule that a space shipping no migrations ships
// no DDL.
perSpace.set(space.spaceId, {
plan: {
targetId: aggregate.targetId,
spaceId: space.spaceId,
origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
destination: { storageHash: headRef.hash },
operations: [],
},
displayOps: [],
destinationContract: space.contract(),
strategy: 'declared-state',
migrationEdges: [
buildFabricatedMigrationEdge({
currentMarkerStorageHash: currentMarker?.storageHash,
destinationStorageHash: headRef.hash,
operationCount: 0,
}),
],
});
if (synthOutcome.kind === 'failure') {
return notOk({
kind: 'appSynthFailure',
spaceId: member.spaceId,
conflicts: synthOutcome.conflicts,
});
}
perSpace.set(member.spaceId, synthOutcome.result);
}

@@ -161,0 +177,0 @@

import type { Contract } from '@prisma-next/contract/types';
import type {
SchemaEntityCoordinate,
SchemaOwnership,
} from '@prisma-next/framework-components/control';
import type { MigrationGraph } from '../graph';

@@ -30,6 +34,6 @@ import type { IntegrityQueryOptions, IntegrityViolation } from '../integrity-violation';

/**
* One contract space — app or extension — as a member of a
* {@link ContractSpaceAggregate}. Every member has the same shape.
* One contract space — app or extension — as the aggregate holds it.
* Every space in a {@link ContractSpaceAggregate} has the same shape.
*
* A member is a tolerant snapshot of one space's on-disk state, not a
* A value of this type is a tolerant snapshot of one space's on-disk state, not a
* validated value: `packages` is the raw migration-package list as read

@@ -48,3 +52,3 @@ * from disk (a hash- or invariants-mismatched package is retained here;

* (represented as a `headRefMissing` violation, never fatal). The app
* member's head ref is always synthesised from its live contract's
* space's head ref is always synthesised from its live contract's
* storage hash, so it is never `null`.

@@ -54,3 +58,3 @@ * - `graph()`: the migration graph this space's packages induce —

* `from === to` self-edge is represented, not rejected.
* - `contract()`: the deserialized contract for this member — lazily
* - `contract()`: the deserialized contract for this space — lazily
* produced on first call and memoised. For the app it is the live

@@ -68,3 +72,3 @@ * contract the caller supplied; for an extension it is the on-disk

*/
export interface ContractSpaceMember {
export interface AggregateContractSpace {
readonly spaceId: string;

@@ -82,3 +86,3 @@ readonly packages: readonly OnDiskMigrationPackage[];

* the app contract space plus every extension contract space, each a
* {@link ContractSpaceMember}.
* {@link AggregateContractSpace}.
*

@@ -90,3 +94,3 @@ * Produced once per CLI invocation by `loadContractSpaceAggregate`.

*
* - `targetId`: the app contract's target; every member is expected to
* - `targetId`: the app contract's target; every space is expected to
* share it (a mismatch surfaces as a `targetMismatch` violation under

@@ -100,2 +104,10 @@ * `checkContracts`).

* lex-ascending.
* - `declaresEntity(coordinate)` / `declaringSpaces(coordinate)`: ownership
* queries — does any contract space declare a storage entity at this
* coordinate (namespace, entity kind, and name), and which spaces do? The verifier's
* unclaimed-elements pass asks these of the diff's extra findings; the
* migration planner asks `declaresEntity` per live extra node to decide
* whether some space owns it (the aggregate satisfies the framework
* {@link SchemaOwnership} oracle). The passive aggregate answers both; it
* runs no diff.
* - `checkIntegrity()`: judges the loaded model and returns every

@@ -106,11 +118,13 @@ * violation (never bailing at the first). Config/contract-dependent

*/
export interface ContractSpaceAggregate {
export interface ContractSpaceAggregate extends SchemaOwnership {
readonly targetId: string;
readonly app: ContractSpaceMember;
readonly extensions: readonly ContractSpaceMember[];
readonly app: AggregateContractSpace;
readonly extensions: readonly AggregateContractSpace[];
listSpaces(): readonly string[];
hasSpace(id: string): boolean;
space(id: string): ContractSpaceMember | undefined;
spaces(): readonly ContractSpaceMember[];
space(id: string): AggregateContractSpace | undefined;
spaces(): readonly AggregateContractSpace[];
declaresEntity(coordinate: SchemaEntityCoordinate): boolean;
declaringSpaces(coordinate: SchemaEntityCoordinate): readonly string[];
checkIntegrity(opts?: IntegrityQueryOptions): readonly IntegrityViolation[];
}

@@ -1,2 +0,6 @@

import { elementCoordinates } from '@prisma-next/framework-components/ir';
import type {
SchemaEntityCoordinate,
VerifyDatabaseSchemaResult,
} from '@prisma-next/framework-components/control';
import { coordinateKey } from '@prisma-next/framework-components/ir';
import type { Result } from '@prisma-next/utils/result';

@@ -6,12 +10,17 @@ import { notOk, ok } from '@prisma-next/utils/result';

import type { ContractMarkerRecordLike } from './marker-types';
import { projectSchemaToSpace } from './project-schema-to-space';
import type { ContractSpaceAggregate, ContractSpaceMember } from './types';
import type { AggregateContractSpace, ContractSpaceAggregate } from './types';
import {
collectExtraElementCoordinates,
type SchemaEntityKindClassifier,
type SchemaSubjectClassifier,
stripExtraFindings,
} from './unclaimed-elements';
/**
* Caller policy for the verifier. Today's only knob is
* `mode`: `strict` treats orphan elements (live tables not claimed by
* any aggregate member) as errors; `lenient` treats them as
* `mode`: `strict` treats unclaimed elements (live tables declared by
* no contract space) as errors; `lenient` treats them as
* informational. Maps directly to `db verify --strict`.
*/
export interface VerifierInput<TSchemaResult> {
export interface VerifierInput {
readonly aggregate: ContractSpaceAggregate;

@@ -22,22 +31,33 @@ readonly markersBySpaceId: ReadonlyMap<string, ContractMarkerRecordLike | null>;

/**
* Caller-supplied per-space schema verifier. The CLI wires this to
* the family's `verifySqlSchema` (SQL) / equivalent (other
* families). The verifier projects the schema to the
* member's slice via {@link projectSchemaToSpace} before invoking
* the callback, so single-contract semantics are preserved.
*
* Typed structurally with a generic `TSchemaResult` so the
* migration-tools layer doesn't depend on the SQL family's
* `VerifySqlSchemaResult`. CLI callers pass the family's type
* through unchanged.
* Caller-supplied per-space schema verifier. The CLI wires this to the
* family's `verifySchema`, run against the **full** introspected schema. The
* verifier then produces two outputs from the per-space results: each space's
* contract-satisfaction view (extras stripped) and one deduplicated list of
* live elements no contract space declares. It touches no storage shape.
*/
readonly verifySchemaForMember: (
projectedSchema: unknown,
member: ContractSpaceMember,
readonly verifySchemaForSpace: (
schema: unknown,
space: AggregateContractSpace,
mode: 'strict' | 'lenient',
) => TSchemaResult;
) => VerifyDatabaseSchemaResult;
/**
* Classifies a diff issue's subject granularity on demand — the injected
* capability the caller reads off the family instance (via
* `hasSchemaSubjectClassifier`) when it has one. Absent for families that
* classify nothing; the unclaimed-elements sweep falls back to path shape
* in that case. Never stamped onto the issue or the node.
*/
readonly classifySubjectGranularity?: SchemaSubjectClassifier;
/**
* Classifies a diff issue's subject storage `entityKind` on demand — the
* sibling injected capability read off the family instance alongside
* `classifySubjectGranularity`. Absent for families that classify
* nothing; the unclaimed-elements sweep falls back to a placeholder in
* that case. Never stamped onto the issue or the node.
*/
readonly classifyEntityKind?: SchemaEntityKindClassifier;
}
/**
* Marker-check result per member. Mirrors the four cases the
* Marker-check result per contract space. Mirrors the four cases the
* `verifyContractSpaces` primitive surfaces today, plus an `'absent'`

@@ -65,26 +85,23 @@ * case for greenfield spaces (no marker row written yet — `db init`

/**
* A live storage element (today: a top-level table) not claimed by any
* member of the aggregate. The verifier always reports these;
* the caller decides what to do — `db verify --strict` treats them as
* errors, the lenient default treats them as informational.
*
* Today only `kind: 'table'` exists. The discriminated shape leaves
* room for orphan columns / indexes / sequences in the future without
* breaking the type contract.
*/
export type OrphanElement = { readonly kind: 'table'; readonly name: string };
export interface SchemaCheckSection<TSchemaResult> {
readonly perSpace: ReadonlyMap<string, TSchemaResult>;
export interface SchemaCheckSection {
/**
* Live elements present in the introspected schema that are not
* claimed by **any** aggregate member. Sorted alphabetically by name.
* Per contract space, its contract-satisfaction view: the space's
* declared nodes only, each pass/fail by whether a missing/mismatch issue
* concerns it. Extras are stripped; the space's verdict is missing/mismatch
* only.
*/
readonly orphanElements: readonly OrphanElement[];
readonly perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>;
/**
* One deduplicated, sorted list of live element names no contract
* space declares (built from the diffs' extra findings, filtered by the
* passive aggregate's ownership query). Reported once for the whole database,
* not per space. Strict callers fail on a non-empty list; lenient callers show
* it informationally.
*/
readonly unclaimed: readonly string[];
}
export interface VerifierSuccess<TSchemaResult> {
export interface VerifierSuccess {
readonly markerCheck: MarkerCheckSection;
readonly schemaCheck: SchemaCheckSection<TSchemaResult>;
readonly schemaCheck: SchemaCheckSection;
}

@@ -97,3 +114,3 @@

export type VerifierOutput<TSchemaResult> = Result<VerifierSuccess<TSchemaResult>, VerifierError>;
export type VerifierOutput = Result<VerifierSuccess, VerifierError>;

@@ -104,15 +121,17 @@ /**

*
* - `markerCheck` per member: compare the live marker row against the
* member's `headRef.hash` + `headRef.invariants`. Absence is a
* - `markerCheck` per contract space: compare the live marker row against the
* space's `headRef.hash` + `headRef.invariants`. Absence is a
* distinct kind, not an error (callers — `db verify` strict vs
* `db init` precondition — choose how to interpret it).
* - `schemaCheck` per member: project the live schema to the slice
* the member claims via {@link projectSchemaToSpace}, then delegate
* to the caller-supplied `verifySchemaForMember`. The pre-projection
* means the family's single-contract verifier no longer sees other
* members' tables as `extras`, so a multi-member deployment never
* surfaces cross-member tables as orphaned schema elements.
* - `schemaCheck`: two distinct outputs from the per-space diffs.
* `perSpace` — each space verified against the **full**
* introspected schema, then its extras stripped, leaving the space's
* declared nodes (its contract-satisfaction view; verdict is
* missing/mismatch only). `unclaimed` — the extras gathered
* across every space, deduplicated, and filtered to the names no contract
* space declares (via the passive aggregate's ownership query); reported
* once for the database. No schema is pruned before verifying.
*
* `markerCheck.orphanMarkers` lists every marker row whose `space` is
* not a member of the aggregate. `db verify` callers reject orphans;
* not a contract space of the aggregate. `db verify` callers reject orphans;
* future tooling may not.

@@ -123,5 +142,3 @@ *

*/
export function verifyMigration<TSchemaResult>(
input: VerifierInput<TSchemaResult>,
): VerifierOutput<TSchemaResult> {
export function verifyMigration(input: VerifierInput): VerifierOutput {
try {

@@ -137,20 +154,26 @@ return runVerifyMigration(input);

function runVerifyMigration<TSchemaResult>(
input: VerifierInput<TSchemaResult>,
): VerifierOutput<TSchemaResult> {
const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForMember } = input;
const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
const memberSpaceIds = new Set(allMembers.map((m) => m.spaceId));
function runVerifyMigration(input: VerifierInput): VerifierOutput {
const {
aggregate,
markersBySpaceId,
schemaIntrospection,
mode,
verifySchemaForSpace,
classifySubjectGranularity,
classifyEntityKind,
} = input;
const allSpaces: ReadonlyArray<AggregateContractSpace> = [aggregate.app, ...aggregate.extensions];
const aggregateSpaceIds = new Set(allSpaces.map((m) => m.spaceId));
// Marker check per member.
// Marker check per contract space.
const markerPerSpace = new Map<string, MarkerCheckResult>();
for (const member of allMembers) {
const marker = markersBySpaceId.get(member.spaceId) ?? null;
for (const space of allSpaces) {
const marker = markersBySpaceId.get(space.spaceId) ?? null;
if (marker === null) {
markerPerSpace.set(member.spaceId, { kind: 'absent' });
markerPerSpace.set(space.spaceId, { kind: 'absent' });
continue;
}
const headRef = requireHeadRef(member);
const headRef = requireHeadRef(space);
if (marker.storageHash !== headRef.hash) {
markerPerSpace.set(member.spaceId, {
markerPerSpace.set(space.spaceId, {
kind: 'hashMismatch',

@@ -165,3 +188,3 @@ markerHash: marker.storageHash,

if (missing.length > 0) {
markerPerSpace.set(member.spaceId, {
markerPerSpace.set(space.spaceId, {
kind: 'missingInvariants',

@@ -172,10 +195,10 @@ missing: [...missing].sort(),

}
markerPerSpace.set(member.spaceId, { kind: 'ok' });
markerPerSpace.set(space.spaceId, { kind: 'ok' });
}
// Orphan markers: entries in markersBySpaceId whose spaceId is not a
// member of the aggregate.
// contract space of the aggregate.
const orphanMarkers: { spaceId: string; row: ContractMarkerRecordLike }[] = [];
for (const [spaceId, row] of markersBySpaceId) {
if (row !== null && !memberSpaceIds.has(spaceId)) {
if (row !== null && !aggregateSpaceIds.has(spaceId)) {
orphanMarkers.push({ spaceId, row });

@@ -186,9 +209,26 @@ }

// Schema check per member (with per-space pre-projection).
const schemaPerSpace = new Map<string, TSchemaResult>();
for (const member of allMembers) {
const others = allMembers.filter((m) => m.spaceId !== member.spaceId);
const projected = projectSchemaToSpace(schemaIntrospection, member, others);
schemaPerSpace.set(member.spaceId, verifySchemaForMember(projected, member, mode));
// Schema check: verify each space against the full schema, then split the
// results in two: each space's contract-satisfaction view (extras
// stripped), and every extra coordinate across all spaces, deduplicated
// and kept only when no contract space declares it at that coordinate.
const schemaPerSpace = new Map<string, VerifyDatabaseSchemaResult>();
const extraCoordinates = new Map<string, SchemaEntityCoordinate>();
for (const space of allSpaces) {
const result = verifySchemaForSpace(schemaIntrospection, space, mode);
schemaPerSpace.set(space.spaceId, stripExtraFindings(result, classifySubjectGranularity));
for (const coordinate of collectExtraElementCoordinates(
result,
classifySubjectGranularity,
classifyEntityKind,
)) {
extraCoordinates.set(coordinateKey(coordinate), coordinate);
}
}
const unclaimed = [
...new Set(
[...extraCoordinates.values()]
.filter((coordinate) => !aggregate.declaresEntity(coordinate))
.map((coordinate) => coordinate.entityName),
),
].sort((a, b) => a.localeCompare(b));

@@ -202,37 +242,5 @@ return ok({

perSpace: schemaPerSpace,
orphanElements: detectOrphanElements(schemaIntrospection, allMembers),
unclaimed,
},
});
}
/**
* Live tables not claimed by any aggregate member. Duck-typed against
* the introspected schema's `tables` map; schemas whose shape doesn't
* match return an empty list (consistent with
* {@link projectSchemaToSpace}'s fall-through).
*/
function detectOrphanElements(
schemaIntrospection: unknown,
members: ReadonlyArray<ContractSpaceMember>,
): readonly OrphanElement[] {
if (typeof schemaIntrospection !== 'object' || schemaIntrospection === null) return [];
const liveTables = (schemaIntrospection as { readonly tables?: unknown }).tables;
if (typeof liveTables !== 'object' || liveTables === null) return [];
const claimedTables = new Set<string>();
for (const member of members) {
const contract = member.contract();
for (const { entityName } of elementCoordinates(contract.storage)) {
claimedTables.add(entityName);
}
}
const orphans: OrphanElement[] = [];
for (const tableName of Object.keys(liveTables as Record<string, unknown>)) {
if (!claimedTables.has(tableName)) {
orphans.push({ kind: 'table', name: tableName });
}
}
orphans.sort((a, b) => a.name.localeCompare(b.name));
return orphans;
}

@@ -458,1 +458,17 @@ import { ifDefined } from '@prisma-next/utils/defined';

}
export function errorMigrationContractViewMissing(
className: string,
accessor: 'endContract' | 'startContract',
jsonField: 'endContractJson' | 'startContractJson',
): MigrationToolsError {
return new MigrationToolsError(
'MIGRATION.CONTRACT_VIEW_MISSING',
`${className}.${accessor} requires ${jsonField}`,
{
why: `${className}.${accessor} was read, but this instance has no ${jsonField} to build the view from.`,
fix: `Set ${jsonField} to the migration's committed contract JSON, or avoid reading ${accessor} on a migration that overrides describe() and carries no contract.`,
details: { className, accessor, jsonField },
},
);
}
export {
collectAggregateNamespaces,
createAggregateContractSpace,
createContractSpaceAggregate,
createContractSpaceMember,
requireHeadRef,

@@ -13,2 +13,3 @@ } from '../aggregate/aggregate';

} from '../aggregate/check-integrity';
export { buildFabricatedMigrationEdge } from '../aggregate/fabricated-migration-edge';
export { type LoadAggregateInput, loadContractSpaceAggregate } from '../aggregate/loader';

@@ -27,14 +28,12 @@ export type { ContractMarkerRecordLike } from '../aggregate/marker-types';

} from '../aggregate/planner';
export { projectSchemaToSpace } from '../aggregate/project-schema-to-space';
export {
type GraphWalkOutcome,
type GraphWalkStrategyInputs,
graphWalkStrategy,
} from '../aggregate/strategies/graph-walk';
export { buildSynthMigrationEdge } from '../aggregate/synth-migration-edge';
type ResolveRecordedPathInputs,
type ResolveRecordedPathOutcome,
resolveRecordedPath,
} from '../aggregate/strategies/resolve-recorded-path';
export type {
AggregateContractSpace,
ContractAtOptions,
ContractAtResult,
ContractSpaceAggregate,
ContractSpaceMember,
} from '../aggregate/types';

@@ -44,3 +43,2 @@ export {

type MarkerCheckSection,
type OrphanElement,
type SchemaCheckSection,

@@ -47,0 +45,0 @@ type VerifierError,

@@ -6,3 +6,4 @@ export {

type MigrationArtifacts,
MigrationContractViews,
type MigrationMeta,
} from '../migration-base';

@@ -44,3 +44,3 @@ import { readContractSpaceHeadRef } from './read-contract-space-head-ref';

* `extensionPacks` whose descriptor exposes a `contractSpace`. The
* helper reads on-disk head data only for the extension members.
* helper reads on-disk head data only for the extension spaces.
*/

@@ -47,0 +47,0 @@ readonly loadedSpaceIds: ReadonlySet<string>;

@@ -68,3 +68,3 @@ /**

| { readonly kind: 'contractUnreadable'; readonly spaceId: string; readonly detail: string }
// genuinely unloadable — package omitted from member.packages
// genuinely unloadable — package omitted from space.packages
| {

@@ -71,0 +71,0 @@ readonly kind: 'packageUnloadable';

@@ -6,2 +6,3 @@ import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';

} from '@prisma-next/framework-components/control';
import { ifDefined } from '@prisma-next/utils/defined';
import { type } from 'arktype';

@@ -27,2 +28,3 @@ import { basename, dirname, join, resolve } from 'pathe';

const OPS_FILE = 'ops.json';
const END_CONTRACT_FILE = 'end-contract.json';
const MAX_SLUG_LENGTH = 64;

@@ -181,2 +183,28 @@

/**
* Reads the optional `end-contract.json` snapshot next to a migration
* manifest — the contract IR of the migration's destination state.
* Snapshots are author-time conveniences (ADR 197), never structural
* runner inputs, so a missing or unparseable file is treated as absent
* (`undefined`) — a package holding only `migration.json` + `ops.json`
* must keep loading (pinned regression in this package). A file holding
* the JSON literal `null` is also treated as absent: `undefined` is the
* single "no snapshot" sentinel downstream, and a null contract is not
* a storable state (the contract store's `contract_json` is NOT NULL).
*/
async function readEndContractJson(dir: string): Promise<unknown> {
let raw: string;
try {
raw = await readFile(join(dir, END_CONTRACT_FILE), 'utf-8');
} catch {
return undefined;
}
try {
const parsed: unknown = JSON.parse(raw);
return parsed === null ? undefined : parsed;
} catch {
return undefined;
}
}
export async function readMigrationPackage(dir: string): Promise<OnDiskMigrationPackage> {

@@ -235,2 +263,3 @@ const absoluteDir = resolve(dir);

const endContractJson = await readEndContractJson(absoluteDir);
const pkg: OnDiskMigrationPackage = {

@@ -241,2 +270,3 @@ dirName: basename(absoluteDir),

ops,
...ifDefined('endContractJson', endContractJson),
};

@@ -302,2 +332,5 @@

// Deliberately no `endContractJson`: this loader only runs for packages
// that failed hash / invariants verification, and a snapshot from an
// unverifiable package must never reach the ledger's contract store.
return {

@@ -304,0 +337,0 @@ dirName: basename(absoluteDir),

import { realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import type { Contract } from '@prisma-next/contract/types';
import type {

@@ -9,3 +10,3 @@ ControlStack,

import { type } from 'arktype';
import { errorInvalidOperationEntry } from './errors';
import { errorInvalidOperationEntry, errorMigrationContractViewMissing } from './errors';
import { computeMigrationHash } from './hash';

@@ -37,5 +38,16 @@ import { deriveProvidedInvariants } from './invariants';

* runner can consume it directly via `targetId`, `operations`, `origin`, and
* `destination`. The metadata-shaped inputs come from `describe()`, which
* every migration must implement — `migration.json` is required for a
* migration to be valid.
* `destination`.
*
* The from/to identities come from `describe()`. A migration provides them in
* one of two ways:
* - **Contract-derived (default):** assign the committed `start-contract.json`
* / `end-contract.json` imports to `startContractJson` / `endContractJson`;
* the concrete `describe()` below derives `to`/`from` from their
* `storage.storageHash`. The family bases additionally expose typed view
* getters (`startContract` / `endContract`) over the same JSON.
* - **Override (e.g. extension migrations that carry no contract):** override
* `describe()` directly; the override wins and the JSON fields are unused.
*
* The `Start` / `End` generics carry each migration's precise contract types so
* the family-base view getters resolve to fully-typed views.
*/

@@ -46,2 +58,4 @@ export abstract class Migration<

TTargetId extends string = string,
_Start extends Contract = Contract,
_End extends Contract = Contract,
> implements MigrationPlan

@@ -52,2 +66,24 @@ {

/**
* The migration's end-state contract JSON (the committed `end-contract.json`
* import). When set, the derived `describe()` reads `to` from its
* `storage.storageHash`. Family bases build the typed `endContract` view from
* it. Optional so `describe()`-overriding migrations (no contract) compile.
*
* Typed with a plain `storageHash: string`, not the branded
* `StorageHashBase`, so a raw `contract.json` import — whose `storageHash`
* is an untyped string literal — is assignable without a cast. The full
* `Start`/`End` contract typing is applied downstream in the family bases'
* view getters (via `<Family>ContractView.fromJson<…>`).
*/
readonly endContractJson?: { readonly storage: { readonly storageHash: string } };
/**
* The migration's start-state contract JSON (the committed
* `start-contract.json` import). Absent for a baseline migration (`from`
* derives to `null`). Family bases build the typed `startContract` view from
* it.
*/
readonly startContractJson?: { readonly storage: { readonly storageHash: string } };
/**
* Assembled `ControlStack` injected by the orchestrator (`runMigration`).

@@ -78,6 +114,21 @@ *

* Metadata inputs used to build `migration.json` and to derive the plan's
* origin/destination identities. Every migration must provide this —
* omitting it would produce an invalid on-disk migration package.
* origin/destination identities.
*
* Default derivation: `to = endContractJson.storage.storageHash`,
* `from = startContractJson?.storage.storageHash ?? null`. A migration that
* carries no contract JSON (e.g. an extension migration) must override this;
* otherwise it throws, since `migration.json` requires a `to` identity.
*/
abstract describe(): MigrationMeta;
describe(): MigrationMeta {
const end = this.endContractJson;
if (end === undefined) {
throw new Error(
'Migration.describe(): provide endContractJson or override describe() — a migration needs a destination contract hash.',
);
}
return {
from: this.startContractJson?.storage.storageHash ?? null,
to: end.storage.storageHash,
};
}

@@ -95,2 +146,45 @@ get origin(): { readonly storageHash: string } | null {

/**
* Lazy-memoized `endContract` / `startContract` view accessors, one instance
* held per migration. Each target base (`MongoMigration`, `SqliteMigration`,
* `PostgresMigration`) creates one `MigrationContractViews` field from
* `this` — passing its own `<Family>ContractView.fromJson<…>` as `fromJson`
* and a name for error messages — and forwards its
* `endContract`/`startContract` getters to it.
*
* `endContract` throws `MIGRATION.CONTRACT_VIEW_MISSING` when the migration
* has no `endContractJson` (mirrors `describe()`'s own requirement).
* `startContract` returns `null` for a baseline migration (no
* `startContractJson`) instead of throwing.
*/
export class MigrationContractViews<TView> {
#endView?: TView;
#startView?: TView | null;
constructor(
private readonly migration: Migration,
private readonly className: string,
private readonly fromJson: (json: unknown) => TView,
) {}
get endContract(): TView {
if (this.#endView === undefined) {
const json = this.migration.endContractJson;
if (json === undefined) {
throw errorMigrationContractViewMissing(this.className, 'endContract', 'endContractJson');
}
this.#endView = this.fromJson(json);
}
return this.#endView;
}
get startContract(): TView | null {
if (this.#startView === undefined) {
const json = this.migration.startContractJson;
this.#startView = json === undefined ? null : this.fromJson(json);
}
return this.#startView;
}
}
/**
* Returns true when `import.meta.url` resolves to the same file that was

@@ -97,0 +191,0 @@ * invoked as the node entrypoint (`process.argv[1]`). Used by

@@ -160,3 +160,3 @@ import { readdir, stat } from 'node:fs/promises';

* - For every extension space declared in `loadedSpaces` (`'app'`
* excluded — the per-space verifier is scoped to extension members;
* excluded — the per-space verifier is scoped to extension spaces;
* the app is verified through the aggregate path):

@@ -163,0 +163,0 @@ * - If no contract-space dir on disk → `declaredButUnmigrated`.

import { ifDefined } from "@prisma-next/utils/defined";
import { dirname, relative } from "pathe";
//#region src/errors.ts
/**
* Build the canonical "re-emit this package" remediation hint.
*
* Every on-disk migration package ships its own `migration.ts` author-time
* file. Running it regenerates `migration.json` and `ops.json` with the
* correct hash + metadata, so it is the right primitive whenever a single
* package's on-disk artifacts are missing, malformed, or otherwise corrupt.
* Pointing users at `migration plan` would emit a *new* package rather than
* heal the broken one.
*/
function reemitHint(dir, fallback) {
const reemit = `Re-emit the package by running \`node "${relative(process.cwd(), dir)}/migration.ts"\``;
return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;
}
/**
* Structured error for migration tooling operations.
*
* Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
* the MIGRATION namespace. These are tooling-time errors (file I/O, hash
* verification, migration history reconstruction), distinct from the runtime
* MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,
* etc.).
*
* Fields:
* - code: Stable machine-readable code (MIGRATION.SUBCODE)
* - category: Always 'MIGRATION'
* - why: Explains the cause in plain language
* - fix: Actionable remediation step
* - details: Machine-readable structured data for agents
*/
var MigrationToolsError = class extends Error {
code;
category = "MIGRATION";
why;
fix;
details;
constructor(code, summary, options) {
super(summary);
this.name = "MigrationToolsError";
this.code = code;
this.why = options.why;
this.fix = options.fix;
this.details = options.details;
}
static is(error) {
if (!(error instanceof Error)) return false;
const candidate = error;
return candidate.name === "MigrationToolsError" && typeof candidate.code === "string";
}
};
function errorDirectoryExists(dir) {
return new MigrationToolsError("MIGRATION.DIR_EXISTS", "Migration directory already exists", {
why: `The directory "${dir}" already exists. Each migration must have a unique directory.`,
fix: "Use --name to pick a different name, or delete the existing directory and re-run.",
details: { dir }
});
}
function errorMissingFile(file, dir) {
return new MigrationToolsError("MIGRATION.FILE_MISSING", `Missing ${file}`, {
why: `Expected "${file}" in "${dir}" but the file does not exist.`,
fix: reemitHint(dir, "or delete the directory if the migration is unwanted and the source TypeScript is gone."),
details: {
file,
dir
}
});
}
function errorInvalidJson(filePath, parseError) {
return new MigrationToolsError("MIGRATION.INVALID_JSON", "Invalid JSON in migration file", {
why: `Failed to parse "${filePath}": ${parseError}`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
parseError
}
});
}
function errorInvalidManifest(filePath, reason) {
return new MigrationToolsError("MIGRATION.INVALID_MANIFEST", "Invalid migration manifest", {
why: `Migration manifest at "${filePath}" is invalid: ${reason}`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
reason
}
});
}
function errorInvalidOperationEntry(index, reason) {
return new MigrationToolsError("MIGRATION.INVALID_OPERATION_ENTRY", "Migration operation entry is malformed", {
why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,
fix: "Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').",
details: {
index,
reason
}
});
}
function errorInvalidSlug(slug) {
return new MigrationToolsError("MIGRATION.INVALID_NAME", "Invalid migration name", {
why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
fix: "Provide a name with at least one alphanumeric character, e.g. --name add_users.",
details: { slug }
});
}
function errorInvalidDestName(destName) {
return new MigrationToolsError("MIGRATION.INVALID_DEST_NAME", "Invalid copy destination name", {
why: `The destination name "${destName}" must be a single path segment (no ".." or directory separators).`,
fix: "Use a simple file name such as \"contract.json\" for each destination in the copy list.",
details: { destName }
});
}
function errorInvalidSpaceId(spaceId) {
return new MigrationToolsError("MIGRATION.INVALID_SPACE_ID", "Invalid contract space identifier", {
why: `The space id "${spaceId}" does not match the required pattern /^[a-z][a-z0-9_-]{0,63}$/. Space ids are used as filesystem directory names under \`migrations/\`, so the pattern is conservative on purpose.`,
fix: "Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.",
details: { spaceId }
});
}
function errorDescriptorHeadHashMismatch(args) {
const { extensionId, recomputedHash, headRefHash } = args;
return new MigrationToolsError("MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH", "Extension descriptor's headRef.hash does not match its contractJson", {
why: `Extension "${extensionId}" publishes a \`contractSpace\` whose \`headRef.hash\` (${headRefHash}) does not match the canonical hash recomputed from \`contractSpace.contractJson\` (${recomputedHash}). This means the extension descriptor was published with stale \`headRef.hash\` — typically because the contract was bumped without rerunning the extension's emit pipeline.`,
fix: "Re-run the extension authoring pipeline so `contractJson.storage.storageHash` and `headRef.hash` agree, then republish the extension. If you are the extension author and you intentionally bumped `contractJson`, recompute and update `headRef.hash` (and refresh any on-disk migration metadata that derives from it).",
details: {
extensionId,
recomputedHash,
headRefHash
}
});
}
function errorDuplicateSpaceId(spaceId) {
return new MigrationToolsError("MIGRATION.DUPLICATE_SPACE_ID", "Duplicate contract space identifier", {
why: `The space id "${spaceId}" appears more than once in the per-space planner input. Each space id must be unique across the inputs (the per-space planner emits one output entry per id).`,
fix: "Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.",
details: { spaceId }
});
}
function errorAmbiguousTarget(branchTips, context) {
const divergenceInfo = context ? `\nDivergence point: ${context.divergencePoint}\nBranches:\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(" → ") || "direct"})`).join("\n")}` : "";
return new MigrationToolsError("MIGRATION.AMBIGUOUS_TARGET", "Ambiguous migration target", {
why: `The migration history has diverged into multiple branches: ${branchTips.join(", ")}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,
fix: "Use `ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.",
details: {
branchTips,
...context ? {
divergencePoint: context.divergencePoint,
branches: context.branches
} : {}
}
});
}
function errorNoInitialMigration(nodes) {
return new MigrationToolsError("MIGRATION.NO_INITIAL_MIGRATION", "No initial migration found", {
why: `No migration starts from the empty contract state (known hashes: ${nodes.join(", ")}). At least one migration must originate from the empty state.`,
fix: "Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.",
details: { nodes }
});
}
function errorInvalidRefFile(filePath, reason) {
return new MigrationToolsError("MIGRATION.INVALID_REF_FILE", "Invalid ref file", {
why: `Ref file at "${filePath}" is invalid: ${reason}`,
fix: "Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.",
details: {
path: filePath,
reason
}
});
}
function errorInvalidRefName(refName) {
return new MigrationToolsError("MIGRATION.INVALID_REF_NAME", "Invalid ref name", {
why: `Ref name "${refName}" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no "." or ".." segments).`,
fix: `Use a valid ref name (e.g., "staging", "envs/production").`,
details: { refName }
});
}
function errorNoTarget(reachableHashes) {
return new MigrationToolsError("MIGRATION.NO_TARGET", "No migration target could be resolved", {
why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(", ")}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,
fix: "Use --from <hash> to specify the planning origin explicitly.",
details: { reachableHashes }
});
}
function errorInvalidRefValue(value) {
return new MigrationToolsError("MIGRATION.INVALID_REF_VALUE", "Invalid ref value", {
why: `Ref value "${value}" is not a valid contract hash. Values must be in the format "sha256:<64 hex chars>" or "sha256:empty".`,
fix: "Use a valid storage hash from `prisma-next contract emit` output or an existing migration.",
details: { value }
});
}
function errorInvalidInvariantId(invariantId) {
return new MigrationToolsError("MIGRATION.INVALID_INVARIANT_ID", "Invalid invariantId", {
why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,
fix: "Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".",
details: { invariantId }
});
}
function errorDuplicateInvariantInEdge(invariantId) {
return new MigrationToolsError("MIGRATION.DUPLICATE_INVARIANT_IN_EDGE", "Duplicate invariantId on a single migration", {
why: `invariantId "${invariantId}" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,
fix: "Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.",
details: { invariantId }
});
}
function errorProvidedInvariantsMismatch(filePath, stored, derived) {
const storedSet = new Set(stored);
const derivedSet = new Set(derived);
const missing = [...derivedSet].filter((id) => !storedSet.has(id));
const extra = [...storedSet].filter((id) => !derivedSet.has(id));
return new MigrationToolsError("MIGRATION.PROVIDED_INVARIANTS_MISMATCH", "providedInvariants on migration.json disagrees with ops.json", {
why: missing.length === 0 && extra.length === 0 ? `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.` : `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
stored,
derived,
difference: {
missing,
extra
}
}
});
}
function errorNoInvariantPath(args) {
const { refName, required, missing, structuralPath } = args;
const refClause = refName ? `Ref "${refName}"` : "Target";
const missingList = missing.map((id) => JSON.stringify(id)).join(", ");
return new MigrationToolsError("MIGRATION.NO_INVARIANT_PATH", "No path covers the required invariants", {
why: `${refClause} requires invariants the reachable path doesn't cover. required=[${required.map((id) => JSON.stringify(id)).join(", ")}], missing=[${missingList}].`,
fix: "Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.",
details: {
required,
missing,
structuralPath,
...ifDefined("refName", refName)
}
});
}
function errorUnknownInvariant(args) {
const { refName, unknown, declared } = args;
return new MigrationToolsError("MIGRATION.UNKNOWN_INVARIANT", "Ref declares invariants no migration in the graph provides", {
why: `${refName ? `Ref "${refName}" declares` : "Declares"} invariants no migration in the graph provides. unknown=[${unknown.map((id) => JSON.stringify(id)).join(", ")}].`,
fix: "Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.",
details: {
unknown,
declared,
...ifDefined("refName", refName)
}
});
}
function errorMigrationHashMismatch(dir, storedHash, computedHash) {
return new MigrationToolsError("MIGRATION.HASH_MISMATCH", "Migration package is corrupt", {
why: `Stored migrationHash "${storedHash}" does not match the recomputed hash "${computedHash}" for "${relative(process.cwd(), dir)}". The migration.json or ops.json has been edited or partially written since emit.`,
fix: reemitHint(dir, "or restore the directory from version control."),
details: {
dir,
storedHash,
computedHash
}
});
}
function errorSnapshotMissing(refName) {
return new MigrationToolsError("MIGRATION.SNAPSHOT_MISSING", `Ref "${refName}" has no paired contract snapshot`, {
why: `Ref "${refName}" exists but its paired snapshot files are missing.`,
fix: `Run "prisma-next db update --advance-ref ${refName}" to repopulate the snapshot, or "prisma-next ref delete ${refName}" to clear the orphan pointer.`,
details: {
refName,
identifier: refName,
viaRef: true
}
});
}
function errorBundleNotFoundForGraphNode(hash, explicitLabel) {
return new MigrationToolsError("MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE", explicitLabel ? `No migration bundle found for reference "${explicitLabel}" (resolved hash: ${hash})` : `No migration bundle found for graph node ${hash}`, {
why: `The hash ${hash} is a graph node but no on-disk migration package has an end-contract hash matching it.`,
fix: "Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.",
details: {
hash,
...explicitLabel ? { explicitLabel } : {}
}
});
}
function errorContractDeserializationFailed(filePath, message) {
return new MigrationToolsError("MIGRATION.CONTRACT_DESERIALIZATION_FAILED", "Contract failed to deserialize", {
why: `Contract at "${filePath}" failed to deserialize: ${message}`,
fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
details: {
filePath,
message
}
});
}
function errorHashNotInGraph(hash, graph) {
const reachableHashes = [...graph.nodes].sort();
const reachableList = reachableHashes.length > 0 ? reachableHashes.join(", ") : "(none)";
return new MigrationToolsError("MIGRATION.HASH_NOT_IN_GRAPH", `Hash "${hash}" is not a node in the migration graph`, {
why: `The migration graph contains nodes ${reachableList}; "${hash}" isn't one of them.`,
fix: `Pass a hash that's the from-or-to of an on-disk migration bundle, use --from with a graph-node hash, or run "prisma-next migration plan" to introduce it.`,
details: {
hash,
reachableHashes
}
});
}
//#endregion
export { errorNoInvariantPath as C, errorUnknownInvariant as D, errorSnapshotMissing as E, errorNoInitialMigration as S, errorProvidedInvariantsMismatch as T, errorInvalidRefValue as _, errorDescriptorHeadHashMismatch as a, errorMigrationHashMismatch as b, errorDuplicateSpaceId as c, errorInvalidInvariantId as d, errorInvalidJson as f, errorInvalidRefName as g, errorInvalidRefFile as h, errorContractDeserializationFailed as i, errorHashNotInGraph as l, errorInvalidOperationEntry as m, errorAmbiguousTarget as n, errorDirectoryExists as o, errorInvalidManifest as p, errorBundleNotFoundForGraphNode as r, errorDuplicateInvariantInEdge as s, MigrationToolsError as t, errorInvalidDestName as u, errorInvalidSlug as v, errorNoTarget as w, errorMissingFile as x, errorInvalidSpaceId as y };
//# sourceMappingURL=errors-DjTpjyFU.mjs.map
{"version":3,"file":"errors-DjTpjyFU.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { ifDefined } from '@prisma-next/utils/defined';\nimport { basename, dirname, relative } from 'pathe';\nimport type { MigrationGraph } from './graph';\n\n/**\n * Build the canonical \"re-emit this package\" remediation hint.\n *\n * Every on-disk migration package ships its own `migration.ts` author-time\n * file. Running it regenerates `migration.json` and `ops.json` with the\n * correct hash + metadata, so it is the right primitive whenever a single\n * package's on-disk artifacts are missing, malformed, or otherwise corrupt.\n * Pointing users at `migration plan` would emit a *new* package rather than\n * heal the broken one.\n */\nfunction reemitHint(dir: string, fallback?: string): string {\n const relativeDir = relative(process.cwd(), dir);\n const reemit = `Re-emit the package by running \\`node \"${relativeDir}/migration.ts\"\\``;\n return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;\n}\n\n/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, hash\n * verification, migration history reconstruction), distinct from the runtime\n * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,\n * etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Migration manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_OPERATION_ENTRY',\n 'Migration operation entry is malformed',\n {\n why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,\n fix: \"Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').\",\n details: { index, reason },\n },\n );\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorInvalidSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_SPACE_ID',\n 'Invalid contract space identifier',\n {\n why: `The space id \"${spaceId}\" does not match the required pattern /^[a-z][a-z0-9_-]{0,63}$/. Space ids are used as filesystem directory names under \\`migrations/\\`, so the pattern is conservative on purpose.`,\n fix: 'Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorDescriptorHeadHashMismatch(args: {\n readonly extensionId: string;\n readonly recomputedHash: string;\n readonly headRefHash: string;\n}): MigrationToolsError {\n const { extensionId, recomputedHash, headRefHash } = args;\n return new MigrationToolsError(\n 'MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH',\n \"Extension descriptor's headRef.hash does not match its contractJson\",\n {\n why: `Extension \"${extensionId}\" publishes a \\`contractSpace\\` whose \\`headRef.hash\\` (${headRefHash}) does not match the canonical hash recomputed from \\`contractSpace.contractJson\\` (${recomputedHash}). This means the extension descriptor was published with stale \\`headRef.hash\\` — typically because the contract was bumped without rerunning the extension's emit pipeline.`,\n fix: 'Re-run the extension authoring pipeline so `contractJson.storage.storageHash` and `headRef.hash` agree, then republish the extension. If you are the extension author and you intentionally bumped `contractJson`, recompute and update `headRef.hash` (and refresh any on-disk migration metadata that derives from it).',\n details: { extensionId, recomputedHash, headRefHash },\n },\n );\n}\n\nexport function errorDuplicateSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_SPACE_ID',\n 'Duplicate contract space identifier',\n {\n why: `The space id \"${spaceId}\" appears more than once in the per-space planner input. Each space id must be unique across the inputs (the per-space planner emits one output entry per id).`,\n fix: 'Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {\n const dirName = basename(dir);\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration without data-transform operations has same source and target',\n {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform — otherwise the migration is a no-op.`,\n fix: reemitHint(\n dir,\n 'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',\n ),\n details: { dirName, hash },\n },\n );\n}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.',\n details: {\n branchTips,\n ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),\n },\n });\n}\n\nexport function errorNoInitialMigration(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_INITIAL_MIGRATION', 'No initial migration found', {\n why: `No migration starts from the empty contract state (known hashes: ${nodes.join(', ')}). At least one migration must originate from the empty state.`,\n fix: 'Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.',\n details: { nodes },\n });\n}\n\nexport function errorInvalidRefs(refsPath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REFS', 'Invalid refs.json', {\n why: `refs.json at \"${refsPath}\" is invalid: ${reason}`,\n fix: 'Ensure refs.json is a flat object mapping valid ref names to contract hash strings.',\n details: { path: refsPath, reason },\n });\n}\n\nexport function errorInvalidRefFile(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_FILE', 'Invalid ref file', {\n why: `Ref file at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.',\n details: { path: filePath, reason },\n });\n}\n\nexport function errorInvalidRefName(refName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_NAME', 'Invalid ref name', {\n why: `Ref name \"${refName}\" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no \".\" or \"..\" segments).`,\n fix: `Use a valid ref name (e.g., \"staging\", \"envs/production\").`,\n details: { refName },\n });\n}\n\nexport function errorNoTarget(reachableHashes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_TARGET', 'No migration target could be resolved', {\n why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,\n fix: 'Use --from <hash> to specify the planning origin explicitly.',\n details: { reachableHashes },\n });\n}\n\nexport function errorInvalidRefValue(value: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_VALUE', 'Invalid ref value', {\n why: `Ref value \"${value}\" is not a valid contract hash. Values must be in the format \"sha256:<64 hex chars>\" or \"sha256:empty\".`,\n fix: 'Use a valid storage hash from `prisma-next contract emit` output or an existing migration.',\n details: { value },\n });\n}\n\nexport function errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_HASH',\n 'Duplicate migrationHash in migration graph',\n {\n why: `Multiple migrations share migrationHash \"${migrationHash}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',\n details: { migrationHash },\n },\n );\n}\n\nexport function errorInvalidInvariantId(invariantId: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {\n why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,\n fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".',\n details: { invariantId },\n });\n}\n\nexport function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',\n 'Duplicate invariantId on a single migration',\n {\n why: `invariantId \"${invariantId}\" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,\n fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',\n details: { invariantId },\n },\n );\n}\n\nexport function errorProvidedInvariantsMismatch(\n filePath: string,\n stored: readonly string[],\n derived: readonly string[],\n): MigrationToolsError {\n const storedSet = new Set(stored);\n const derivedSet = new Set(derived);\n const missing = [...derivedSet].filter((id) => !storedSet.has(id));\n const extra = [...storedSet].filter((id) => !derivedSet.has(id));\n // When sets agree but arrays don't, the only difference is ordering — call\n // it out so the reader doesn't stare at two visually-identical arrays.\n // Canonical providedInvariants is sorted ascending; a manifest with the\n // same ids in a different order is still a mismatch (the hash check would\n // also fail), but the human-readable diagnostic is otherwise unhelpful.\n const orderingOnly = missing.length === 0 && extra.length === 0;\n const why = orderingOnly\n ? `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.`\n : `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`;\n return new MigrationToolsError(\n 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',\n 'providedInvariants on migration.json disagrees with ops.json',\n {\n why,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, stored, derived, difference: { missing, extra } },\n },\n );\n}\n\n/**\n * Wire-shape edge surfaced through the JSON envelope's\n * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —\n * authoring metadata (`createdAt`) lives on `MigrationEdge` but is\n * intentionally dropped here so the envelope stays stable across\n * graph-internal refactors.\n *\n * Stability: any field added here is part of the public CLI JSON contract.\n * Callers (CLI consumers, agents) must be able to treat\n * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.\n */\nexport interface NoInvariantPathStructuralEdge {\n readonly dirName: string;\n readonly migrationHash: string;\n readonly from: string;\n readonly to: string;\n readonly invariants: readonly string[];\n}\n\nexport function errorNoInvariantPath(args: {\n readonly refName?: string;\n readonly required: readonly string[];\n readonly missing: readonly string[];\n readonly structuralPath: readonly NoInvariantPathStructuralEdge[];\n}): MigrationToolsError {\n const { refName, required, missing, structuralPath } = args;\n const refClause = refName ? `Ref \"${refName}\"` : 'Target';\n const missingList = missing.map((id) => JSON.stringify(id)).join(', ');\n const requiredList = required.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.NO_INVARIANT_PATH',\n 'No path covers the required invariants',\n {\n why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,\n fix: 'Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.',\n details: {\n required,\n missing,\n structuralPath,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorUnknownInvariant(args: {\n readonly refName?: string;\n readonly unknown: readonly string[];\n readonly declared: readonly string[];\n}): MigrationToolsError {\n const { refName, unknown, declared } = args;\n const refClause = refName ? `Ref \"${refName}\" declares` : 'Declares';\n const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.UNKNOWN_INVARIANT',\n 'Ref declares invariants no migration in the graph provides',\n {\n why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,\n fix: 'Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.',\n details: {\n unknown,\n declared,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorMigrationHashMismatch(\n dir: string,\n storedHash: string,\n computedHash: string,\n): MigrationToolsError {\n // Render a cwd-relative path in the human-readable diagnostic so users\n // running CLI commands from the project root see a familiar short path.\n // Keep the absolute path in `details.dir` for machine consumers.\n const relativeDir = relative(process.cwd(), dir);\n return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {\n why: `Stored migrationHash \"${storedHash}\" does not match the recomputed hash \"${computedHash}\" for \"${relativeDir}\". The migration.json or ops.json has been edited or partially written since emit.`,\n fix: reemitHint(dir, 'or restore the directory from version control.'),\n details: { dir, storedHash, computedHash },\n });\n}\n\nexport function errorSnapshotMissing(refName: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.SNAPSHOT_MISSING',\n `Ref \"${refName}\" has no paired contract snapshot`,\n {\n why: `Ref \"${refName}\" exists but its paired snapshot files are missing.`,\n fix: `Run \"prisma-next db update --advance-ref ${refName}\" to repopulate the snapshot, or \"prisma-next ref delete ${refName}\" to clear the orphan pointer.`,\n details: { refName, identifier: refName, viaRef: true },\n },\n );\n}\n\nexport function errorBundleNotFoundForGraphNode(\n hash: string,\n explicitLabel?: string,\n): MigrationToolsError {\n const summary = explicitLabel\n ? `No migration bundle found for reference \"${explicitLabel}\" (resolved hash: ${hash})`\n : `No migration bundle found for graph node ${hash}`;\n return new MigrationToolsError('MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE', summary, {\n why: `The hash ${hash} is a graph node but no on-disk migration package has an end-contract hash matching it.`,\n fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',\n details: { hash, ...(explicitLabel ? { explicitLabel } : {}) },\n });\n}\n\nexport function errorContractDeserializationFailed(\n filePath: string,\n message: string,\n): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED',\n 'Contract failed to deserialize',\n {\n why: `Contract at \"${filePath}\" failed to deserialize: ${message}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, message },\n },\n );\n}\n\nexport function errorHashNotInGraph(hash: string, graph: MigrationGraph): MigrationToolsError {\n const reachableHashes = [...graph.nodes].sort();\n const reachableList = reachableHashes.length > 0 ? reachableHashes.join(', ') : '(none)';\n return new MigrationToolsError(\n 'MIGRATION.HASH_NOT_IN_GRAPH',\n `Hash \"${hash}\" is not a node in the migration graph`,\n {\n why: `The migration graph contains nodes ${reachableList}; \"${hash}\" isn't one of them.`,\n fix: `Pass a hash that's the from-or-to of an on-disk migration bundle, use --from with a graph-node hash, or run \"prisma-next migration plan\" to introduce it.`,\n details: { hash, reachableHashes },\n },\n );\n}\n"],"mappings":"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"}
import { l as errorHashNotInGraph } from "./errors-DjTpjyFU.mjs";
import "./constants-YnG8_EGO.mjs";
//#region src/graph-membership.ts
function isGraphNode(hash, graph) {
if (hash === "sha256:empty") return true;
return graph.nodes.has(hash);
}
function assertHashIsGraphNode(hash, graph) {
if (isGraphNode(hash, graph)) return;
throw errorHashNotInGraph(hash, graph);
}
//#endregion
export { isGraphNode as n, assertHashIsGraphNode as t };
//# sourceMappingURL=graph-membership-DEzWfpW0.mjs.map
{"version":3,"file":"graph-membership-DEzWfpW0.mjs","names":[],"sources":["../src/graph-membership.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from './constants';\nimport { errorHashNotInGraph } from './errors';\nimport type { MigrationGraph } from './graph';\n\nexport function isGraphNode(hash: string, graph: MigrationGraph): boolean {\n if (hash === EMPTY_CONTRACT_HASH) {\n return true;\n }\n return graph.nodes.has(hash);\n}\n\nexport function assertHashIsGraphNode(hash: string, graph: MigrationGraph): asserts hash is string {\n if (isGraphNode(hash, graph)) {\n return;\n }\n throw errorHashNotInGraph(hash, graph);\n}\n"],"mappings":";;;AAIA,SAAgB,YAAY,MAAc,OAAgC;CACxE,IAAI,SAAA,gBACF,OAAO;CAET,OAAO,MAAM,MAAM,IAAI,IAAI;AAC7B;AAEA,SAAgB,sBAAsB,MAAc,OAA+C;CACjG,IAAI,YAAY,MAAM,KAAK,GACzB;CAEF,MAAM,oBAAoB,MAAM,KAAK;AACvC"}
import { d as errorInvalidInvariantId, s as errorDuplicateInvariantInEdge } from "./errors-DjTpjyFU.mjs";
//#region src/invariants.ts
/**
* Hygiene check for `invariantId`. Rejects empty values plus any
* whitespace or control character (including Unicode whitespace like
* NBSP and em space, which are visually identical to ASCII space and
* routinely sneak in via paste).
*/
function validateInvariantId(invariantId) {
if (invariantId.length === 0) return false;
return !/[\p{Cc}\p{White_Space}]/u.test(invariantId);
}
/**
* Walk a migration's operations and produce its `providedInvariants`
* aggregate: the sorted, deduplicated list of `invariantId`s declared
* by ops in the migration. Ops without an `invariantId` are skipped.
*
* Both `data`-class ops (data-transforms, e.g. backfills) and
* `additive`-class opaque DDL (e.g. cipherstash's vendored EQL bundle
* via `installEqlBundleOp`) may declare invariantIds: the
* `operationClass` axis classifies *policy gating* (which kinds of ops
* a `db init` / `db update` policy permits), while `invariantId`
* classifies *marker bookkeeping* (which named bundles of work a
* future regeneration knows to skip). The two concerns are
* intentionally orthogonal — an extension can ship additive
* non-IR-derivable DDL (the only way the planner can know the bundle
* is already applied is via the invariantId on the marker) without
* needing to mis-classify it as `data`-class.
*
* Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and
* `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.
*
* @see docs/architecture docs/adrs/ADR 212 - Contract spaces.md
* — extension migrations carry `invariantId`s on additive ops; e.g.
* cipherstash's `installEqlBundle` and structural `create-*` ops are
* additive-class but carry `cipherstash:*` invariantIds.
*/
function deriveProvidedInvariants(ops) {
const seen = /* @__PURE__ */ new Set();
for (const op of ops) {
const invariantId = readInvariantId(op);
if (invariantId === void 0) continue;
if (!validateInvariantId(invariantId)) throw errorInvalidInvariantId(invariantId);
if (seen.has(invariantId)) throw errorDuplicateInvariantInEdge(invariantId);
seen.add(invariantId);
}
return [...seen].sort();
}
function readInvariantId(op) {
if (!Object.hasOwn(op, "invariantId")) return void 0;
const candidate = op.invariantId;
return typeof candidate === "string" ? candidate : void 0;
}
//#endregion
export { validateInvariantId as n, deriveProvidedInvariants as t };
//# sourceMappingURL=invariants-5wBqXSaJ.mjs.map
{"version":3,"file":"invariants-5wBqXSaJ.mjs","names":[],"sources":["../src/invariants.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { errorDuplicateInvariantInEdge, errorInvalidInvariantId } from './errors';\nimport type { MigrationOps } from './package';\n\n/**\n * Hygiene check for `invariantId`. Rejects empty values plus any\n * whitespace or control character (including Unicode whitespace like\n * NBSP and em space, which are visually identical to ASCII space and\n * routinely sneak in via paste).\n */\nexport function validateInvariantId(invariantId: string): boolean {\n if (invariantId.length === 0) return false;\n return !/[\\p{Cc}\\p{White_Space}]/u.test(invariantId);\n}\n\n/**\n * Walk a migration's operations and produce its `providedInvariants`\n * aggregate: the sorted, deduplicated list of `invariantId`s declared\n * by ops in the migration. Ops without an `invariantId` are skipped.\n *\n * Both `data`-class ops (data-transforms, e.g. backfills) and\n * `additive`-class opaque DDL (e.g. cipherstash's vendored EQL bundle\n * via `installEqlBundleOp`) may declare invariantIds: the\n * `operationClass` axis classifies *policy gating* (which kinds of ops\n * a `db init` / `db update` policy permits), while `invariantId`\n * classifies *marker bookkeeping* (which named bundles of work a\n * future regeneration knows to skip). The two concerns are\n * intentionally orthogonal — an extension can ship additive\n * non-IR-derivable DDL (the only way the planner can know the bundle\n * is already applied is via the invariantId on the marker) without\n * needing to mis-classify it as `data`-class.\n *\n * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and\n * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.\n *\n * @see docs/architecture docs/adrs/ADR 212 - Contract spaces.md\n * — extension migrations carry `invariantId`s on additive ops; e.g.\n * cipherstash's `installEqlBundle` and structural `create-*` ops are\n * additive-class but carry `cipherstash:*` invariantIds.\n */\nexport function deriveProvidedInvariants(ops: MigrationOps): readonly string[] {\n const seen = new Set<string>();\n for (const op of ops) {\n const invariantId = readInvariantId(op);\n if (invariantId === undefined) continue;\n if (!validateInvariantId(invariantId)) {\n throw errorInvalidInvariantId(invariantId);\n }\n if (seen.has(invariantId)) {\n throw errorDuplicateInvariantInEdge(invariantId);\n }\n seen.add(invariantId);\n }\n return [...seen].sort();\n}\n\nfunction readInvariantId(op: MigrationPlanOperation): string | undefined {\n if (!Object.hasOwn(op, 'invariantId')) return undefined;\n const candidate = (op as { invariantId?: unknown }).invariantId;\n return typeof candidate === 'string' ? candidate : undefined;\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,oBAAoB,aAA8B;CAChE,IAAI,YAAY,WAAW,GAAG,OAAO;CACrC,OAAO,CAAC,2BAA2B,KAAK,WAAW;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,yBAAyB,KAAsC;CAC7E,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,cAAc,gBAAgB,EAAE;EACtC,IAAI,gBAAgB,KAAA,GAAW;EAC/B,IAAI,CAAC,oBAAoB,WAAW,GAClC,MAAM,wBAAwB,WAAW;EAE3C,IAAI,KAAK,IAAI,WAAW,GACtB,MAAM,8BAA8B,WAAW;EAEjD,KAAK,IAAI,WAAW;CACtB;CACA,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK;AACxB;AAEA,SAAS,gBAAgB,IAAgD;CACvE,IAAI,CAAC,OAAO,OAAO,IAAI,aAAa,GAAG,OAAO,KAAA;CAC9C,MAAM,YAAa,GAAiC;CACpD,OAAO,OAAO,cAAc,WAAW,YAAY,KAAA;AACrD"}
import { T as errorProvidedInvariantsMismatch, b as errorMigrationHashMismatch, f as errorInvalidJson, o as errorDirectoryExists, p as errorInvalidManifest, t as MigrationToolsError, u as errorInvalidDestName, v as errorInvalidSlug, x as errorMissingFile } from "./errors-DjTpjyFU.mjs";
import { n as verifyMigrationHash } from "./hash-BNCgfhir.mjs";
import { t as deriveProvidedInvariants } from "./invariants-5wBqXSaJ.mjs";
import { n as MigrationOpsSchema } from "./op-schema-BSVHzEQN.mjs";
import { basename, dirname, join, resolve } from "pathe";
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { type } from "arktype";
//#region src/io.ts
const MANIFEST_FILE = "migration.json";
const OPS_FILE = "ops.json";
const MAX_SLUG_LENGTH = 64;
function hasErrnoCode(error, code) {
return error instanceof Error && error.code === code;
}
const MigrationMetadataSchema = type({
"+": "reject",
from: "string > 0 | null",
to: "string",
migrationHash: "string",
providedInvariants: "string[]",
createdAt: "string"
});
async function writeMigrationPackage(dir, metadata, ops) {
await mkdir(dirname(dir), { recursive: true });
try {
await mkdir(dir);
} catch (error) {
if (hasErrnoCode(error, "EEXIST")) throw errorDirectoryExists(dir);
throw error;
}
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), { flag: "wx" });
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
}
/**
* Materialise an in-memory {@link MigrationPackage} to a per-space
* directory on disk.
*
* Writes two files under `<targetDir>/<pkg.dirName>/`:
*
* - `migration.json` — the manifest (pretty-printed, matches
* {@link writeMigrationPackage}'s output for byte-for-byte parity with
* app-space migrations).
* - `ops.json` — the operation list (pretty-printed).
*
* Distinct verb from the lower-level {@link writeMigrationPackage}
* (which takes constituent `(metadata, ops)`): callers reading
* `materialise…` know they are persisting a struct-typed package.
*
* Overwrite-idempotent: the per-package directory is cleared before
* each emit, so re-running against the same `targetDir` produces
* byte-identical contents and never leaves stale files behind. The
* lower-level {@link writeMigrationPackage} stays strict because the
* CLI authoring path (`migration plan` / `migration new`) deliberately
* refuses to clobber an existing authored migration; this helper is
* the re-emit path that is supposed to converge on a single canonical
* on-disk shape.
*
* The per-space head contract lives at
* `<projectMigrationsDir>/<spaceId>/contract.json` (written by
* {@link import('./emit-contract-space-artefacts').emitContractSpaceArtefacts}),
* not inside the per-package directory. The runner reads only
* `migration.json` + `ops.json` from each package.
*/
async function materialiseMigrationPackage(targetDir, pkg) {
const dir = join(targetDir, pkg.dirName);
await rm(dir, {
recursive: true,
force: true
});
await writeMigrationPackage(dir, pkg.metadata, pkg.ops);
}
/**
* Idempotent variant of {@link materialiseMigrationPackage}: writes the
* package only if `<targetDir>/<pkg.dirName>/` does not already exist on
* disk as a directory; returns `{ written: false }` when the package
* directory is present (no rewrite, no comparison — by-existence skip).
*
* Concretely:
* - existing directory → skip silently, return `{ written: false }`.
* - missing path → write three files via {@link materialiseMigrationPackage},
* return `{ written: true }`.
* - path exists but is not a directory (file/symlink) → treated as
* missing; {@link materialiseMigrationPackage} will attempt creation
* and fail with an appropriate OS error.
* - any other I/O error from `stat` → propagated unchanged.
*
* Used by the CLI's `runContractSpaceExtensionMigrationsPass` to
* materialise extension migration packages into a project's
* `migrations/<spaceId>/` directory, and by extension-package tests
* that mirror the same idempotent-rematerialise property locally
* without taking a CLI dependency.
*/
async function materialiseExtensionMigrationPackageIfMissing(targetDir, pkg) {
if (await directoryExists(join(targetDir, pkg.dirName))) return { written: false };
await materialiseMigrationPackage(targetDir, pkg);
return { written: true };
}
async function directoryExists(p) {
try {
return (await stat(p)).isDirectory();
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return false;
throw error;
}
}
/**
* Copy a list of files into `destDir`, optionally renaming each one.
*
* The destination directory is created (with `recursive: true`) if it
* does not already exist. Each source path is copied byte-for-byte into
* `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
* intentionally generic: callers own the list of files (e.g. a contract
* emitter's emitted output) and the naming convention (e.g. renaming
* the destination contract to `end-contract.*` and the source contract
* to `start-contract.*`).
*/
async function copyFilesWithRename(destDir, files) {
await mkdir(destDir, { recursive: true });
for (const file of files) {
if (basename(file.destName) !== file.destName) throw errorInvalidDestName(file.destName);
await copyFile(file.sourcePath, join(destDir, file.destName));
}
}
async function writeMigrationMetadata(dir, metadata) {
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
}
async function writeMigrationOps(dir, ops) {
await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
}
async function readMigrationPackage(dir) {
const absoluteDir = resolve(dir);
const manifestPath = join(absoluteDir, MANIFEST_FILE);
const opsPath = join(absoluteDir, OPS_FILE);
let manifestRaw;
try {
manifestRaw = await readFile(manifestPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(MANIFEST_FILE, absoluteDir);
throw error;
}
let opsRaw;
try {
opsRaw = await readFile(opsPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(OPS_FILE, absoluteDir);
throw error;
}
let metadata;
try {
metadata = JSON.parse(manifestRaw);
} catch (e) {
throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
}
let ops;
try {
ops = JSON.parse(opsRaw);
} catch (e) {
throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
}
validateMetadata(metadata, manifestPath);
validateOps(ops, opsPath);
const derivedInvariants = deriveProvidedInvariants(ops);
if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) throw errorProvidedInvariantsMismatch(manifestPath, metadata.providedInvariants, derivedInvariants);
const pkg = {
dirName: basename(absoluteDir),
dirPath: absoluteDir,
metadata,
ops
};
const verification = verifyMigrationHash(pkg);
if (!verification.ok) throw errorMigrationHashMismatch(absoluteDir, verification.storedHash, verification.computedHash);
return pkg;
}
/**
* Reads a migration package's manifest and ops without running hash or
* invariants verification. Returns `null` when the files cannot be read or
* parsed (i.e. when the package is genuinely unloadable).
*
* Used by {@link readMigrationsDir} to retain a package whose hash or
* invariants diverge from what is stored on disk — the raw content is still
* useful for display / querying; only integrity is in question.
*/
async function readMigrationPackageRaw(dir) {
const absoluteDir = resolve(dir);
const manifestPath = join(absoluteDir, MANIFEST_FILE);
const opsPath = join(absoluteDir, OPS_FILE);
let manifestRaw;
try {
manifestRaw = await readFile(manifestPath, "utf-8");
} catch {
return null;
}
let opsRaw;
try {
opsRaw = await readFile(opsPath, "utf-8");
} catch {
return null;
}
let metadata;
try {
metadata = JSON.parse(manifestRaw);
} catch {
return null;
}
let ops;
try {
ops = JSON.parse(opsRaw);
} catch {
return null;
}
if (MigrationMetadataSchema(metadata) instanceof type.errors) return null;
if (MigrationOpsSchema(ops) instanceof type.errors) return null;
return {
dirName: basename(absoluteDir),
dirPath: absoluteDir,
metadata,
ops
};
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
function validateMetadata(metadata, filePath) {
const result = MigrationMetadataSchema(metadata);
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
}
function validateOps(ops, filePath) {
const result = MigrationOpsSchema(ops);
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
}
function packageLoadProblemDetailFromError(error) {
if (MigrationToolsError.is(error)) return error.why;
if (error instanceof Error) return error.message;
return String(error);
}
async function readMigrationsDir(migrationsRoot) {
let entries;
try {
entries = await readdir(migrationsRoot);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return {
packages: [],
problems: []
};
throw error;
}
const packages = [];
const problems = [];
for (const entry of entries.sort()) {
const entryPath = join(migrationsRoot, entry);
if (!(await stat(entryPath)).isDirectory()) continue;
const manifestPath = join(entryPath, MANIFEST_FILE);
try {
await stat(manifestPath);
} catch {
continue;
}
let pkg;
try {
pkg = await readMigrationPackage(entryPath);
} catch (error) {
const dirName = entry;
if (MigrationToolsError.is(error)) {
if (error.code === "MIGRATION.HASH_MISMATCH") {
const details = error.details;
const rawPkg = await readMigrationPackageRaw(entryPath);
if (rawPkg !== null) packages.push(rawPkg);
problems.push({
kind: "hashMismatch",
dirName,
stored: typeof details?.["storedHash"] === "string" ? details["storedHash"] : "",
computed: typeof details?.["computedHash"] === "string" ? details["computedHash"] : ""
});
continue;
}
if (error.code === "MIGRATION.PROVIDED_INVARIANTS_MISMATCH") {
const rawPkg = await readMigrationPackageRaw(entryPath);
if (rawPkg !== null) packages.push(rawPkg);
problems.push({
kind: "providedInvariantsMismatch",
dirName
});
continue;
}
}
problems.push({
kind: "packageUnloadable",
dirName,
detail: packageLoadProblemDetailFromError(error)
});
continue;
}
packages.push(pkg);
}
return {
packages,
problems
};
}
function formatMigrationDirName(timestamp, slug) {
const sanitized = slug.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
if (sanitized.length === 0) throw errorInvalidSlug(slug);
const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);
return `${timestamp.getUTCFullYear()}${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}${String(timestamp.getUTCDate()).padStart(2, "0")}T${String(timestamp.getUTCHours()).padStart(2, "0")}${String(timestamp.getUTCMinutes()).padStart(2, "0")}_${truncated}`;
}
//#endregion
export { materialiseMigrationPackage as a, writeMigrationMetadata as c, materialiseExtensionMigrationPackageIfMissing as i, writeMigrationOps as l, copyFilesWithRename as n, readMigrationPackage as o, formatMigrationDirName as r, readMigrationsDir as s, MANIFEST_FILE as t, writeMigrationPackage as u };
//# sourceMappingURL=io-DWg_qFDX.mjs.map
{"version":3,"file":"io-DWg_qFDX.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 MigrationToolsError,\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\n/**\n * Reads a migration package's manifest and ops without running hash or\n * invariants verification. Returns `null` when the files cannot be read or\n * parsed (i.e. when the package is genuinely unloadable).\n *\n * Used by {@link readMigrationsDir} to retain a package whose hash or\n * invariants diverge from what is stored on disk — the raw content is still\n * useful for display / querying; only integrity is in question.\n */\nasync function readMigrationPackageRaw(dir: string): Promise<OnDiskMigrationPackage | null> {\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 {\n return null;\n }\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch {\n return null;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch {\n return null;\n }\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch {\n return null;\n }\n\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) return null;\n\n const opsResult = MigrationOpsSchema(ops);\n if (opsResult instanceof type.errors) return null;\n\n return {\n dirName: basename(absoluteDir),\n dirPath: absoluteDir,\n metadata,\n ops,\n };\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\n/**\n * A per-package load-time problem returned by {@link readMigrationsDir}.\n *\n * Three variants, matching the relocated throws from the load path:\n *\n * - `hashMismatch` — stored `migrationHash` differs from the recomputed value.\n * The package is **retained** in the returned `packages` array.\n * - `providedInvariantsMismatch` — `migration.json` declares different\n * `providedInvariants` than `ops.json` implies. The package is **retained**.\n * - `packageUnloadable` — the manifest is missing, unparseable, or schema-\n * invalid. The package is **omitted** from `packages`.\n *\n * Callers that need the `spaceId` context (e.g. the aggregate loader) attach\n * it when converting to {@link import('./integrity-violation').IntegrityViolation}.\n */\nexport type PackageLoadProblem =\n | {\n readonly kind: 'hashMismatch';\n readonly dirName: string;\n readonly stored: string;\n readonly computed: string;\n }\n | { readonly kind: 'providedInvariantsMismatch'; readonly dirName: string }\n | { readonly kind: 'packageUnloadable'; readonly dirName: string; readonly detail: string };\n\n/**\n * Result returned by {@link readMigrationsDir}.\n *\n * - `packages` — every package that could be read; hash-mismatched and\n * invariants-mismatched packages are included here (the problem is\n * represented rather than fatal).\n * - `problems` — one entry per package that had a load-time issue.\n * `packageUnloadable` entries are **not** in `packages`.\n */\nexport interface ReadMigrationsDirResult {\n readonly packages: readonly OnDiskMigrationPackage[];\n readonly problems: readonly PackageLoadProblem[];\n}\n\nfunction packageLoadProblemDetailFromError(error: unknown): string {\n if (MigrationToolsError.is(error)) return error.why;\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\nexport async function readMigrationsDir(migrationsRoot: string): Promise<ReadMigrationsDirResult> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return { packages: [], problems: [] };\n }\n throw error;\n }\n\n const packages: OnDiskMigrationPackage[] = [];\n const problems: PackageLoadProblem[] = [];\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 let pkg: OnDiskMigrationPackage;\n try {\n pkg = await readMigrationPackage(entryPath);\n } catch (error) {\n const dirName = entry;\n if (MigrationToolsError.is(error)) {\n if (error.code === 'MIGRATION.HASH_MISMATCH') {\n const details = error.details;\n const rawPkg = await readMigrationPackageRaw(entryPath);\n if (rawPkg !== null) packages.push(rawPkg);\n problems.push({\n kind: 'hashMismatch',\n dirName,\n stored: typeof details?.['storedHash'] === 'string' ? details['storedHash'] : '',\n computed: typeof details?.['computedHash'] === 'string' ? details['computedHash'] : '',\n });\n continue;\n }\n if (error.code === 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH') {\n const rawPkg = await readMigrationPackageRaw(entryPath);\n if (rawPkg !== null) packages.push(rawPkg);\n problems.push({ kind: 'providedInvariantsMismatch', dirName });\n continue;\n }\n }\n // Any other error (missing file, invalid JSON, invalid manifest schema) →\n // package unloadable; omit from packages.\n problems.push({\n kind: 'packageUnloadable',\n dirName,\n detail: packageLoadProblemDetailFromError(error),\n });\n continue;\n }\n packages.push(pkg);\n }\n\n return { packages, problems };\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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"}
import { S as errorNoInitialMigration, n as errorAmbiguousTarget, w as errorNoTarget } from "./errors-DjTpjyFU.mjs";
import { t as EMPTY_CONTRACT_HASH } from "./constants-YnG8_EGO.mjs";
import { ifDefined } from "@prisma-next/utils/defined";
//#region src/queue.ts
/**
* FIFO queue with amortised O(1) push and shift.
*
* Uses a head-index cursor over a backing array rather than
* `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped
* traversals where the queue is drained in a single pass — it does not
* reclaim memory for already-shifted items, so it is not suitable for
* long-lived queues with many push/shift cycles.
*/
var Queue = class {
items;
head = 0;
constructor(initial = []) {
this.items = [...initial];
}
push(item) {
this.items.push(item);
}
/**
* Remove and return the next item. Caller must check `isEmpty` first —
* shifting an empty queue throws.
*/
shift() {
if (this.head >= this.items.length) throw new Error("Queue.shift called on empty queue");
return this.items[this.head++];
}
get isEmpty() {
return this.head >= this.items.length;
}
};
//#endregion
//#region src/graph-ops.ts
function* bfs(starts, neighbours, key = (state) => state) {
const visited = /* @__PURE__ */ new Set();
const parentMap = /* @__PURE__ */ new Map();
const queue = new Queue();
for (const start of starts) {
const k = key(start);
if (!visited.has(k)) {
visited.add(k);
queue.push({
state: start,
key: k
});
}
}
while (!queue.isEmpty) {
const { state: current, key: curKey } = queue.shift();
const parentInfo = parentMap.get(curKey);
yield {
state: current,
parent: parentInfo?.parent ?? null,
incomingEdge: parentInfo?.edge ?? null
};
for (const { next, edge } of neighbours(current)) {
const k = key(next);
if (!visited.has(k)) {
visited.add(k);
parentMap.set(k, {
parent: current,
edge
});
queue.push({
state: next,
key: k
});
}
}
}
}
//#endregion
//#region src/migration-graph.ts
/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */
function forwardNeighbours(graph, node) {
return (graph.forwardChain.get(node) ?? []).map((edge) => ({
next: edge.to,
edge
}));
}
/**
* Forward-edge neighbours, sorted by the deterministic tie-break.
* Used by path-finding so the resulting shortest path is stable across runs.
*/
function sortedForwardNeighbours(graph, node) {
return [...graph.forwardChain.get(node) ?? []].sort(compareTieBreak).map((edge) => ({
next: edge.to,
edge
}));
}
/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */
function reverseNeighbours(graph, node) {
return (graph.reverseChain.get(node) ?? []).map((edge) => ({
next: edge.from,
edge
}));
}
function appendEdge(map, key, entry) {
const bucket = map.get(key);
if (bucket) bucket.push(entry);
else map.set(key, [entry]);
}
function reconstructGraph(packages) {
const nodes = /* @__PURE__ */ new Set();
const forwardChain = /* @__PURE__ */ new Map();
const reverseChain = /* @__PURE__ */ new Map();
const migrationByHash = /* @__PURE__ */ new Map();
for (const pkg of packages) {
const from = pkg.metadata.from ?? "sha256:empty";
const { to } = pkg.metadata;
nodes.add(from);
nodes.add(to);
const migration = {
from,
to,
migrationHash: pkg.metadata.migrationHash,
dirName: pkg.dirName,
createdAt: pkg.metadata.createdAt,
invariants: pkg.metadata.providedInvariants
};
if (!migrationByHash.has(migration.migrationHash)) migrationByHash.set(migration.migrationHash, migration);
appendEdge(forwardChain, from, migration);
appendEdge(reverseChain, to, migration);
}
return {
nodes,
forwardChain,
reverseChain,
migrationByHash
};
}
function compareTieBreak(a, b) {
const ca = a.createdAt.localeCompare(b.createdAt);
if (ca !== 0) return ca;
const tc = a.to.localeCompare(b.to);
if (tc !== 0) return tc;
return a.migrationHash.localeCompare(b.migrationHash);
}
function sortedNeighbors(edges) {
return [...edges].sort(compareTieBreak);
}
/**
* Find the shortest path from `fromHash` to `toHash` using BFS over the
* contract-hash graph. Returns the ordered list of edges, or null if no path
* exists. Returns an empty array when `fromHash === toHash` (no-op).
*
* Neighbor ordering is deterministic via the tie-break sort key:
* createdAt → to → migrationHash.
*/
function findPath(graph, fromHash, toHash) {
if (fromHash === toHash) return [];
const parents = /* @__PURE__ */ new Map();
for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {
if (step.parent !== null && step.incomingEdge !== null) parents.set(step.state, {
parent: step.parent,
edge: step.incomingEdge
});
if (step.state === toHash) {
const path = [];
let cur = toHash;
let p = parents.get(cur);
while (p) {
path.push(p.edge);
cur = p.parent;
p = parents.get(cur);
}
path.reverse();
return path;
}
}
return null;
}
/**
* Find the shortest path from `fromHash` to `toHash` whose edges collectively
* cover every invariant in `required`. Returns `null` when no such path exists
* (either `fromHash`→`toHash` is structurally unreachable, or every reachable
* path leaves at least one required invariant uncovered). When `required` is
* empty, delegates to `findPath` so the result is byte-identical for that case.
*
* Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.
* The covered subset is a `Set<string>` of invariant ids; the state's dedup
* key is `${node}\0${[...covered].sort().join('\0')}`. State keys distinguish
* distinct `(node, covered)` tuples regardless of node-name length because
* `\0` cannot appear in any invariant id (validation rejects whitespace and
* control chars at authoring time).
*
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
* invariant come first, with `createdAt → to → migrationHash` as the
* secondary key. The heuristic steers BFS toward the satisfying path;
* correctness (shortest, deterministic) does not depend on it.
*/
function findPathWithInvariants(graph, fromHash, toHash, required) {
if (required.size === 0) return findPath(graph, fromHash, toHash);
const stateKey = (s) => {
if (s.covered.size === 0) return `${s.node}\0`;
return `${s.node}\0${[...s.covered].sort().join("\0")}`;
};
const neighbours = (s) => {
const outgoing = graph.forwardChain.get(s.node) ?? [];
if (outgoing.length === 0) return [];
return [...outgoing].map((edge) => {
let useful = false;
let next = null;
for (const inv of edge.invariants) if (required.has(inv) && !s.covered.has(inv)) {
if (next === null) next = new Set(s.covered);
next.add(inv);
useful = true;
}
return {
edge,
useful,
nextCovered: next ?? s.covered
};
}).sort((a, b) => {
if (a.useful !== b.useful) return a.useful ? -1 : 1;
return compareTieBreak(a.edge, b.edge);
}).map(({ edge, nextCovered }) => ({
next: {
node: edge.to,
covered: nextCovered
},
edge
}));
};
const parents = /* @__PURE__ */ new Map();
for (const step of bfs([{
node: fromHash,
covered: /* @__PURE__ */ new Set()
}], neighbours, stateKey)) {
const curKey = stateKey(step.state);
if (step.parent !== null && step.incomingEdge !== null) parents.set(curKey, {
parentKey: stateKey(step.parent),
edge: step.incomingEdge
});
if (step.state.node === toHash && step.state.covered.size === required.size) {
const path = [];
let cur = curKey;
while (cur !== void 0) {
const p = parents.get(cur);
if (!p) break;
path.push(p.edge);
cur = p.parentKey;
}
path.reverse();
return path;
}
}
return null;
}
/**
* Reverse-BFS from `toHash` over `reverseChain` to collect every node from
* which `toHash` is reachable (inclusive of `toHash` itself).
*/
function collectNodesReachingTarget(graph, toHash) {
const reached = /* @__PURE__ */ new Set();
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) reached.add(step.state);
return reached;
}
/**
* Find the shortest path from `fromHash` to `toHash` and return structured
* path-decision metadata for machine-readable output. When `required` is
* non-empty, the returned path is the shortest one whose edges collectively
* cover every required invariant.
*
* The discriminated return type tells the caller *why* a path could not be
* found, so the CLI can pick the right structured error without re-running
* a structural BFS.
*/
function findPathWithDecision(graph, fromHash, toHash, options = {}) {
const { refName, required = /* @__PURE__ */ new Set() } = options;
const requiredInvariants = [...required].sort();
if (fromHash === toHash && required.size === 0) return {
kind: "ok",
decision: {
selectedPath: [],
fromHash,
toHash,
alternativeCount: 0,
tieBreakReasons: [],
requiredInvariants,
satisfiedInvariants: [],
...ifDefined("refName", refName)
}
};
const path = findPathWithInvariants(graph, fromHash, toHash, required);
if (!path) {
if (required.size === 0) return { kind: "unreachable" };
const structural = findPath(graph, fromHash, toHash);
if (structural === null) return { kind: "unreachable" };
const coveredByStructural = /* @__PURE__ */ new Set();
for (const edge of structural) for (const inv of edge.invariants) if (required.has(inv)) coveredByStructural.add(inv);
return {
kind: "unsatisfiable",
structuralPath: structural,
missing: requiredInvariants.filter((id) => !coveredByStructural.has(id))
};
}
const satisfiedInvariants = computeSatisfiedInvariants(required, path);
const reachesTarget = collectNodesReachingTarget(graph, toHash);
const coveragePrefixes = requiredCoveragePrefixes(required, path);
const tieBreakReasons = [];
let alternativeCount = 0;
for (const [i, edge] of path.entries()) {
const outgoing = graph.forwardChain.get(edge.from);
if (!outgoing || outgoing.length <= 1) continue;
const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
if (reachable.length <= 1) continue;
let comparisonPool = reachable;
if (required.size > 0) {
const prefixSet = coveragePrefixes[i];
if (prefixSet === void 0) continue;
comparisonPool = invariantViableAlternativesAtStep(required, prefixSet, reachable);
}
alternativeCount += reachable.length - 1;
if (sortedNeighbors(reachable)[0]?.migrationHash !== edge.migrationHash) continue;
if (!reachable.some((e) => e.migrationHash !== edge.migrationHash)) continue;
const sortedViable = sortedNeighbors(comparisonPool);
if (sortedViable.length > 1 && sortedViable[0]?.migrationHash === edge.migrationHash && sortedViable.some((e) => e.migrationHash !== edge.migrationHash)) tieBreakReasons.push(`at ${edge.from}: ${comparisonPool.length} candidates, selected by tie-break`);
}
return {
kind: "ok",
decision: {
selectedPath: path,
fromHash,
toHash,
alternativeCount,
tieBreakReasons,
requiredInvariants,
satisfiedInvariants,
...ifDefined("refName", refName)
}
};
}
function computeSatisfiedInvariants(required, path) {
if (required.size === 0) return [];
const covered = /* @__PURE__ */ new Set();
for (const edge of path) for (const inv of edge.invariants) if (required.has(inv)) covered.add(inv);
return [...covered].sort();
}
/**
* For each edge on path, invariant coverage accumulated from earlier edges only —
* `(required ∩ ∪_{j<i} path[j].invariants)` represented as cumulative set along `required`,
* keyed as "full set of required ids satisfied before taking path[i]".
*/
function requiredCoveragePrefixes(required, path) {
const prefixes = [];
const acc = /* @__PURE__ */ new Set();
for (const edge of path) {
prefixes.push(new Set(acc));
for (const inv of edge.invariants) if (required.has(inv)) acc.add(inv);
}
return prefixes;
}
function invariantViableAlternativesAtStep(required, coverageBeforeTakingEdge, outgoing) {
if (required.size === 0) return [...outgoing];
return outgoing.filter((e) => [...required].every((id) => coverageBeforeTakingEdge.has(id) || e.invariants.includes(id)));
}
/**
* Walk ancestors of each branch tip back to find the last node
* that appears on all paths. Returns `fromHash` if no shared ancestor is found.
*/
function findDivergencePoint(graph, fromHash, leaves) {
const ancestorSets = leaves.map((leaf) => {
const ancestors = /* @__PURE__ */ new Set();
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) ancestors.add(step.state);
return ancestors;
});
const commonAncestors = [...ancestorSets[0] ?? []].filter((node) => ancestorSets.every((s) => s.has(node)));
let deepest = fromHash;
let deepestDepth = -1;
for (const ancestor of commonAncestors) {
const path = findPath(graph, fromHash, ancestor);
const depth = path ? path.length : 0;
if (depth > deepestDepth) {
deepestDepth = depth;
deepest = ancestor;
}
}
return deepest;
}
/**
* Find all branch tips (nodes with no outgoing edges) reachable from
* `fromHash` via forward edges.
*/
function findReachableLeaves(graph, fromHash) {
const leaves = [];
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) if (!graph.forwardChain.get(step.state)?.length) leaves.push(step.state);
return leaves;
}
/**
* Find the target contract hash of the migration graph reachable from
* EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target
* state (either empty, or containing only the root with no outgoing
* edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none
* originate from the empty hash, and AMBIGUOUS_TARGET if multiple
* branch tips exist.
*/
function findLeaf(graph) {
if (graph.nodes.size === 0) return null;
if (!graph.nodes.has("sha256:empty")) throw errorNoInitialMigration([...graph.nodes]);
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
if (leaves.length === 0) {
const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);
if (reachable.length > 0) throw errorNoTarget(reachable);
return null;
}
if (leaves.length > 1) {
const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);
throw errorAmbiguousTarget(leaves, {
divergencePoint,
branches: leaves.map((tip) => {
return {
tip,
edges: (findPath(graph, divergencePoint, tip) ?? []).map((e) => ({
dirName: e.dirName,
from: e.from,
to: e.to
}))
};
})
});
}
return leaves[0];
}
/**
* Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
* to the single target. Returns null for an empty graph.
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
*/
function findLatestMigration(graph) {
const leafHash = findLeaf(graph);
if (leafHash === null) return null;
return findPath(graph, "sha256:empty", leafHash)?.at(-1) ?? null;
}
function detectCycles(graph) {
const WHITE = 0;
const GRAY = 1;
const BLACK = 2;
const color = /* @__PURE__ */ new Map();
const parentMap = /* @__PURE__ */ new Map();
const cycles = [];
for (const node of graph.nodes) color.set(node, WHITE);
const stack = [];
function pushFrame(u) {
color.set(u, GRAY);
stack.push({
node: u,
outgoing: graph.forwardChain.get(u) ?? [],
index: 0
});
}
for (const root of graph.nodes) {
if (color.get(root) !== WHITE) continue;
parentMap.set(root, null);
pushFrame(root);
while (stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame.index >= frame.outgoing.length) {
color.set(frame.node, BLACK);
stack.pop();
continue;
}
const v = frame.outgoing[frame.index++].to;
const vColor = color.get(v);
if (vColor === GRAY) {
const cycle = [v];
let cur = frame.node;
while (cur !== v) {
cycle.push(cur);
cur = parentMap.get(cur) ?? v;
}
cycle.reverse();
cycles.push(cycle);
} else if (vColor === WHITE) {
parentMap.set(v, frame.node);
pushFrame(v);
}
}
}
return cycles;
}
function detectOrphans(graph) {
if (graph.nodes.size === 0) return [];
const reachable = /* @__PURE__ */ new Set();
const startNodes = [];
if (graph.forwardChain.has("sha256:empty")) startNodes.push(EMPTY_CONTRACT_HASH);
else {
const allTargets = /* @__PURE__ */ new Set();
for (const edges of graph.forwardChain.values()) for (const edge of edges) allTargets.add(edge.to);
for (const node of graph.nodes) if (!allTargets.has(node)) startNodes.push(node);
}
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) reachable.add(step.state);
const orphans = [];
for (const [from, migrations] of graph.forwardChain) if (!reachable.has(from)) orphans.push(...migrations);
return orphans;
}
//#endregion
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 };
//# sourceMappingURL=migration-graph-BW4ty9WV.mjs.map
{"version":3,"file":"migration-graph-BW4ty9WV.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 { errorAmbiguousTarget, errorNoInitialMigration, errorNoTarget } 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 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 migrationByHash.set(migration.migrationHash, migration);\n }\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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"}
import { f as errorInvalidJson, h as errorInvalidRefFile, x as errorMissingFile, y as errorInvalidSpaceId } from "./errors-DjTpjyFU.mjs";
import { t as MANIFEST_FILE } from "./io-DWg_qFDX.mjs";
import { join } from "pathe";
import { readFile, readdir, stat } from "node:fs/promises";
import { APP_SPACE_ID } from "@prisma-next/framework-components/control";
//#region src/space-layout.ts
/**
* Pattern a contract-space identifier must match. The constraint is
* filesystem-friendly: lowercase letters / digits / hyphen / underscore,
* starts with a letter, max 64 characters.
*/
const SPACE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
function isValidSpaceId(spaceId) {
return SPACE_ID_PATTERN.test(spaceId);
}
function assertValidSpaceId(spaceId) {
if (!isValidSpaceId(spaceId)) throw errorInvalidSpaceId(spaceId);
}
/**
* Resolve the migrations subdirectory for a given contract space.
*
* Every contract space — including the app space (default `'app'`) —
* lands under `<projectMigrationsDir>/<spaceId>/`. The space id is
* validated against {@link SPACE_ID_PATTERN} because it becomes a
* filesystem directory name verbatim.
*
* `projectMigrationsDir` is the project's top-level `migrations/`
* directory; the helper does not assume anything about its absolute /
* relative shape and is symmetric with `pathe.join`.
*/
function spaceMigrationDirectory(projectMigrationsDir, spaceId) {
assertValidSpaceId(spaceId);
return join(projectMigrationsDir, spaceId);
}
/**
* Per-space subdirectory name reserved for the ref store
* (`migrations/<space>/<SPACE_REFS_DIRNAME>/*.json`). Single source of
* truth: every helper that composes a per-space refs path imports this
* constant, and the enumerator uses it (via
* {@link RESERVED_SPACE_SUBDIR_NAMES}) to exclude reserved names from
* the contract-space candidate list.
*/
const SPACE_REFS_DIRNAME = "refs";
/**
* Names reserved as per-space subdirectories of `migrations/<space>/`.
* Used by the enumerator to filter contract-space candidates so a
* reserved name (e.g. a top-level `migrations/refs/` left in the wrong
* place) is never enumerated as a phantom contract space. Currently
* holds `SPACE_REFS_DIRNAME`; extend if future per-space layouts add
* more reserved subdirectories.
*/
const RESERVED_SPACE_SUBDIR_NAMES = new Set([SPACE_REFS_DIRNAME]);
/**
* Resolve the per-space refs directory for `spaceMigrationsDir`
* (typically the value returned by {@link spaceMigrationDirectory}).
* Composes the canonical {@link SPACE_REFS_DIRNAME} so callers do not
* hard-code the literal.
*/
function spaceRefsDirectory(spaceMigrationsDir) {
return join(spaceMigrationsDir, SPACE_REFS_DIRNAME);
}
//#endregion
//#region src/read-contract-space-head-ref.ts
function hasErrnoCode$2(error, code) {
return error instanceof Error && error.code === code;
}
/**
* Read the head ref (`hash` + `invariants`) for a contract space from
* `<projectMigrationsDir>/<spaceId>/refs/head.json`.
*
* Returns `null` when the file does not exist (first emit). Surfaces
* `MIGRATION.INVALID_JSON` / `MIGRATION.INVALID_REF_FILE` on a corrupt
* `refs/head.json` so callers can distinguish "no head ref on disk"
* (returns `null`) from "head ref present but unreadable" (throws).
*
* Validates the space id against `[a-z][a-z0-9_-]{0,63}` for the same
* filesystem-safety reasons as the rest of the per-space helpers. The
* helper is uniform across the app and extension spaces.
*/
async function readContractSpaceHeadRef(projectMigrationsDir, spaceId) {
assertValidSpaceId(spaceId);
const filePath = join(spaceRefsDirectory(spaceMigrationDirectory(projectMigrationsDir, spaceId)), "head.json");
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
if (hasErrnoCode$2(error, "ENOENT")) return null;
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));
}
if (typeof parsed !== "object" || parsed === null) throw errorInvalidRefFile(filePath, "expected an object");
const obj = parsed;
if (typeof obj.hash !== "string") throw errorInvalidRefFile(filePath, "expected an object with a string `hash` field");
if (!Array.isArray(obj.invariants) || obj.invariants.some((value) => typeof value !== "string")) throw errorInvalidRefFile(filePath, "expected an object with an `invariants` array of strings");
return {
hash: obj.hash,
invariants: obj.invariants
};
}
//#endregion
//#region src/verify-contract-spaces.ts
function hasErrnoCode$1(error, code) {
return error instanceof Error && error.code === code;
}
/**
* List the per-space subdirectories under
* `<projectRoot>/migrations/`. Returns space-id directory names (sorted
* alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
* does **not** contain a `migration.json` manifest. The manifest is the
* structural marker of a user-authored migration directory (see
* `readMigrationsDir` in `./io`); directory names themselves belong to
* the user and are not part of the contract.
*
* Returns `[]` if the migrations directory does not exist (greenfield
* project).
*
* Reads only the user's repo. **No descriptor import.** The caller
* (verifier) feeds the result into {@link verifyContractSpaces} alongside
* the loaded-space set and the marker rows.
*/
async function listContractSpaceDirectories(projectMigrationsDir) {
let entries;
try {
entries = (await readdir(projectMigrationsDir, { withFileTypes: true })).map((d) => ({
name: d.name,
isDirectory: d.isDirectory()
}));
} catch (error) {
if (hasErrnoCode$1(error, "ENOENT")) return [];
throw error;
}
const namedCandidates = entries.filter((e) => e.isDirectory).map((e) => e.name).filter((name) => !name.startsWith(".")).sort();
return (await Promise.all(namedCandidates.map(async (name) => {
try {
await stat(join(projectMigrationsDir, name, MANIFEST_FILE));
return {
name,
isMigrationDir: true
};
} catch (error) {
if (hasErrnoCode$1(error, "ENOENT")) return {
name,
isMigrationDir: false
};
throw error;
}
}))).filter((c) => !c.isMigrationDir).map((c) => c.name);
}
/**
* Pure structural verifier for the per-space mechanism. Aggregates the
* three orphan / missing checks plus per-space hash and invariant
* comparison.
*
* Algorithm:
*
* - For every extension space declared in `loadedSpaces` (`'app'`
* excluded — the per-space verifier is scoped to extension members;
* the app is verified through the aggregate path):
* - If no contract-space dir on disk → `declaredButUnmigrated`.
* - Else if `markerRowsBySpace` lacks an entry → no violation here;
* the live-DB compare done outside this helper is where the
* absence shows up.
* - Else compare marker hash / invariants vs. on-disk head hash /
* invariants → `hashMismatch` / `invariantsMismatch` on drift.
* - For every contract-space dir on disk that is not in `loadedSpaces` →
* `orphanSpaceDir`.
* - For every marker row whose `space` is not in `loadedSpaces` →
* `orphanMarker`. The app-space marker is always loaded (`'app'` is
* in `loadedSpaces` by definition).
*
* Output is deterministic: violations are sorted first by `kind`
* (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
* `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
* passing equivalent inputs see byte-identical violation lists.
*
* Synchronous, pure, no I/O. **Does not import the extension descriptor**
* (the inputs are pre-resolved by the caller); the verifier reads only
* the user repo, not `node_modules`.
*/
function verifyContractSpaces(inputs) {
const violations = [];
for (const spaceId of [...inputs.loadedSpaces].sort()) {
if (spaceId === APP_SPACE_ID) continue;
if (!inputs.spaceDirsOnDisk.includes(spaceId)) {
violations.push({
kind: "declaredButUnmigrated",
spaceId,
remediation: `Extension '${spaceId}' is declared in extensionPacks but has not been emitted; run \`prisma-next migrate\`.`
});
continue;
}
const head = inputs.headRefsBySpace.get(spaceId);
const marker = inputs.markerRowsBySpace.get(spaceId);
if (!head || !marker) continue;
if (head.hash !== marker.hash) {
violations.push({
kind: "hashMismatch",
spaceId,
priorHeadHash: head.hash,
markerHash: marker.hash,
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.`
});
continue;
}
const onDiskInvariants = [...head.invariants].sort();
const markerInvariants = new Set(marker.invariants);
const missing = onDiskInvariants.filter((id) => !markerInvariants.has(id));
if (missing.length > 0) violations.push({
kind: "invariantsMismatch",
spaceId,
onDiskInvariants,
markerInvariants: [...marker.invariants].sort(),
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.`
});
}
for (const dir of [...inputs.spaceDirsOnDisk].sort()) if (!inputs.loadedSpaces.has(dir)) violations.push({
kind: "orphanSpaceDir",
spaceId: dir,
remediation: `Orphan contract-space directory \`${join("migrations", dir)}/\` for an extension not in extensionPacks; remove the directory or re-add the extension.`
});
for (const space of [...inputs.markerRowsBySpace.keys()].sort()) if (!inputs.loadedSpaces.has(space)) violations.push({
kind: "orphanMarker",
spaceId: space,
remediation: `Orphan marker row for space '${space}' (no longer in extensionPacks); remediation: manually delete the row from \`prisma_contract.marker\`.`
});
if (violations.length === 0) return { ok: true };
const kindOrder = {
declaredButUnmigrated: 0,
orphanMarker: 1,
orphanSpaceDir: 2,
hashMismatch: 3,
invariantsMismatch: 4
};
violations.sort((a, b) => {
const k = kindOrder[a.kind] - kindOrder[b.kind];
if (k !== 0) return k;
if (a.spaceId < b.spaceId) return -1;
if (a.spaceId > b.spaceId) return 1;
return 0;
});
return {
ok: false,
violations
};
}
//#endregion
//#region src/read-contract-space-contract.ts
function hasErrnoCode(error, code) {
return error instanceof Error && error.code === code;
}
/**
* Read the on-disk contract value for a contract space
* (`<projectMigrationsDir>/<spaceId>/contract.json`). Returns the parsed
* JSON value as `unknown` — callers that need a typed contract validate
* via their family's `deserializeContract` to surface schema issues.
*
* Companion to {@link import('./read-contract-space-head-ref').readContractSpaceHeadRef}
* — same ENOENT-throws / corrupt-file-error semantics. Returns the
* canonical-JSON value the framework wrote during emit, so re-running
* this helper across machines / runs yields a byte-identical value.
*/
async function readContractSpaceContract(projectMigrationsDir, spaceId) {
assertValidSpaceId(spaceId);
const filePath = join(projectMigrationsDir, spaceId, "contract.json");
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile("contract.json", join(projectMigrationsDir, spaceId));
throw error;
}
try {
return JSON.parse(raw);
} catch (e) {
throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));
}
}
//#endregion
export { APP_SPACE_ID as a, assertValidSpaceId as c, spaceRefsDirectory as d, readContractSpaceHeadRef as i, isValidSpaceId as l, listContractSpaceDirectories as n, RESERVED_SPACE_SUBDIR_NAMES as o, verifyContractSpaces as r, SPACE_REFS_DIRNAME as s, readContractSpaceContract as t, spaceMigrationDirectory as u };
//# sourceMappingURL=read-contract-space-contract-DyLWOWSt.mjs.map
{"version":3,"file":"read-contract-space-contract-DyLWOWSt.mjs","names":["hasErrnoCode","hasErrnoCode"],"sources":["../src/space-layout.ts","../src/read-contract-space-head-ref.ts","../src/verify-contract-spaces.ts","../src/read-contract-space-contract.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 { 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 { 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","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":"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"}
import { _ as errorInvalidRefValue, g as errorInvalidRefName, h as errorInvalidRefFile, t as MigrationToolsError } from "./errors-DjTpjyFU.mjs";
import { dirname, join, relative } from "pathe";
import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
import { type } from "arktype";
//#region src/refs.ts
/**
* The system head ref lives at `refs/head.json`. It is read (and its
* corruption judged) through `readContractSpaceHeadRef`, not as a
* user-authored ref, so {@link readRefsTolerant} excludes it.
*/
const HEAD_REF_NAME = "head";
const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
const REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;
function validateRefName(name) {
if (name.length === 0) return false;
if (name.includes("..")) return false;
if (name.includes("//")) return false;
if (name.startsWith(".")) return false;
return REF_NAME_PATTERN.test(name);
}
function validateRefValue(value) {
return REF_VALUE_PATTERN.test(value);
}
const RefEntrySchema = type({
hash: "string",
invariants: "string[]"
}).narrow((entry, ctx) => {
if (!validateRefValue(entry.hash)) return ctx.mustBe(`a valid contract hash (got "${entry.hash}")`);
return true;
});
function refFilePath(refsDir, name) {
return join(refsDir, `${name}.json`);
}
function refNameFromPath(refsDir, filePath) {
return relative(refsDir, filePath).replace(/\.json$/, "");
}
async function readRef(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const filePath = refFilePath(refsDir, name);
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
why: `No ref file found at "${filePath}".`,
fix: `Create the ref with: prisma-next ref set ${name} <hash>`,
details: {
refName: name,
filePath
}
});
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
}
const result = RefEntrySchema(parsed);
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
return result;
}
async function readRefs(refsDir) {
let entries;
try {
entries = await readdir(refsDir, {
recursive: true,
encoding: "utf-8"
});
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") return {};
throw error;
}
const jsonFiles = entries.filter((entry) => entry.endsWith(".json") && !entry.endsWith(".contract.json"));
const refs = {};
for (const jsonFile of jsonFiles) {
const filePath = join(refsDir, jsonFile);
const name = refNameFromPath(refsDir, filePath);
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
const code = error instanceof Error ? error.code : void 0;
if (code === "ENOENT" || code === "EISDIR") continue;
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
}
const result = RefEntrySchema(parsed);
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
refs[name] = result;
}
return refs;
}
/**
* Read a space's user-authored refs without ever throwing on disk
* content. A ref whose JSON is unparseable or whose shape fails
* {@link RefEntrySchema} is omitted from `refs` and reported as a
* {@link RefLoadProblem}; the remaining well-formed refs are still
* returned. A missing `refs/` directory yields no refs and no problems.
*
* `refs/head.json` is deliberately skipped here: the system head ref is
* read through `readContractSpaceHeadRef` (which validates head-ref
* shape, distinct from the strict user-ref hash grammar), so it is judged
* there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,
* …) still propagate — only parse / schema problems are made tolerant.
*/
async function readRefsTolerant(refsDir) {
let entries;
try {
entries = await readdir(refsDir, {
recursive: true,
encoding: "utf-8"
});
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") return {
refs: {},
problems: []
};
throw error;
}
const jsonFiles = entries.filter((entry) => entry.endsWith(".json") && !entry.endsWith(".contract.json") && entry !== `head.json`);
const refs = {};
const problems = [];
for (const jsonFile of jsonFiles) {
const filePath = join(refsDir, jsonFile);
const name = refNameFromPath(refsDir, filePath);
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (error) {
const code = error instanceof Error ? error.code : void 0;
if (code === "ENOENT" || code === "EISDIR") continue;
throw error;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
problems.push({
refName: name,
detail: e instanceof Error ? e.message : String(e)
});
continue;
}
const result = RefEntrySchema(parsed);
if (result instanceof type.errors) {
problems.push({
refName: name,
detail: result.summary
});
continue;
}
refs[name] = result;
}
return {
refs,
problems
};
}
async function writeRef(refsDir, name, entry) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
if (!validateRefValue(entry.hash)) throw errorInvalidRefValue(entry.hash);
const filePath = refFilePath(refsDir, name);
const dir = dirname(filePath);
await mkdir(dir, { recursive: true });
const tmpPath = join(dir, `.${name.split("/").pop()}.json.${Date.now()}.tmp`);
await writeFile(tmpPath, `${JSON.stringify({
hash: entry.hash,
invariants: [...entry.invariants]
}, null, 2)}\n`);
await rename(tmpPath, filePath);
}
async function deleteRef(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const filePath = refFilePath(refsDir, name);
try {
await unlink(filePath);
} catch (error) {
if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
why: `No ref file found at "${filePath}".`,
fix: "Run `prisma-next ref list` to see available refs.",
details: {
refName: name,
filePath
}
});
throw error;
}
let dir = dirname(filePath);
while (dir !== refsDir && dir.startsWith(refsDir)) try {
await rmdir(dir);
dir = dirname(dir);
} catch (error) {
const code = error instanceof Error ? error.code : void 0;
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") break;
throw error;
}
}
/**
* Index user-authored refs by the contract hash each ref points at.
* Each bucket is sorted lex-asc for deterministic output.
*/
function refsByContractHash(refs) {
const byHash = /* @__PURE__ */ new Map();
for (const [name, entry] of Object.entries(refs)) {
const bucket = byHash.get(entry.hash);
if (bucket) bucket.push(name);
else byHash.set(entry.hash, [name]);
}
for (const bucket of byHash.values()) bucket.sort();
return byHash;
}
/**
* Read `migrations/<space>/refs/*.json` and index by destination hash.
* Returns an empty map when the refs directory does not exist.
*/
async function resolveRefsByContractHash(refsDir) {
return refsByContractHash(await readRefs(refsDir));
}
function resolveRef(refs, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
if (!Object.hasOwn(refs, name)) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
why: `No ref named "${name}" exists.`,
fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: prisma-next ref set ${name} <hash>`,
details: {
refName: name,
availableRefs: Object.keys(refs)
}
});
return refs[name];
}
//#endregion
export { readRefsTolerant as a, resolveRefsByContractHash as c, writeRef as d, readRefs as i, validateRefName as l, deleteRef as n, refsByContractHash as o, readRef as r, resolveRef as s, HEAD_REF_NAME as t, validateRefValue as u };
//# sourceMappingURL=refs-kd62Ljkh.mjs.map
{"version":3,"file":"refs-kd62Ljkh.mjs","names":[],"sources":["../src/refs.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { dirname, join, relative } from 'pathe';\nimport {\n errorInvalidRefFile,\n errorInvalidRefName,\n errorInvalidRefValue,\n MigrationToolsError,\n} from './errors';\n\nexport interface RefEntry {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\nexport type Refs = Readonly<Record<string, RefEntry>>;\n\n/**\n * The system head ref lives at `refs/head.json`. It is read (and its\n * corruption judged) through `readContractSpaceHeadRef`, not as a\n * user-authored ref, so {@link readRefsTolerant} excludes it.\n */\nexport const HEAD_REF_NAME = 'head';\n\n/**\n * A single ref file that exists on disk but cannot be turned into a\n * {@link RefEntry} (unparseable JSON or schema-invalid content). The ref\n * is omitted from the result; the problem is surfaced for the integrity\n * layer to report as `refUnreadable` rather than aborting the load.\n */\nexport interface RefLoadProblem {\n readonly refName: string;\n readonly detail: string;\n}\n\nexport interface TolerantRefsResult {\n readonly refs: Refs;\n readonly problems: readonly RefLoadProblem[];\n}\n\nconst REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;\nconst REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;\n\nexport function validateRefName(name: string): boolean {\n if (name.length === 0) return false;\n if (name.includes('..')) return false;\n if (name.includes('//')) return false;\n if (name.startsWith('.')) return false;\n return REF_NAME_PATTERN.test(name);\n}\n\nexport function validateRefValue(value: string): boolean {\n return REF_VALUE_PATTERN.test(value);\n}\n\nconst RefEntrySchema = type({\n hash: 'string',\n invariants: 'string[]',\n}).narrow((entry, ctx) => {\n if (!validateRefValue(entry.hash))\n return ctx.mustBe(`a valid contract hash (got \"${entry.hash}\")`);\n return true;\n});\n\nfunction refFilePath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.json`);\n}\n\nfunction refNameFromPath(refsDir: string, filePath: string): string {\n const rel = relative(refsDir, filePath);\n return rel.replace(/\\.json$/, '');\n}\n\nexport async function readRef(refsDir: string, name: string): Promise<RefEntry> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const filePath = refFilePath(refsDir, name);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref file found at \"${filePath}\".`,\n fix: `Create the ref with: prisma-next ref set ${name} <hash>`,\n details: { refName: name, filePath },\n });\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function readRefs(refsDir: string): Promise<Refs> {\n let entries: string[];\n try {\n entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return {};\n }\n throw error;\n }\n\n const jsonFiles = entries.filter(\n (entry) => entry.endsWith('.json') && !entry.endsWith('.contract.json'),\n );\n const refs: Record<string, RefEntry> = {};\n\n for (const jsonFile of jsonFiles) {\n const filePath = join(refsDir, jsonFile);\n const name = refNameFromPath(refsDir, filePath);\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT) and\n // benign EISDIR if a directory happens to end in `.json`. Anything else\n // (EACCES, EIO, EMFILE, …) is a real failure and propagates so the CLI\n // surfaces it rather than silently dropping the ref.\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOENT' || code === 'EISDIR') {\n continue;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n refs[name] = result;\n }\n\n return refs;\n}\n\n/**\n * Read a space's user-authored refs without ever throwing on disk\n * content. A ref whose JSON is unparseable or whose shape fails\n * {@link RefEntrySchema} is omitted from `refs` and reported as a\n * {@link RefLoadProblem}; the remaining well-formed refs are still\n * returned. A missing `refs/` directory yields no refs and no problems.\n *\n * `refs/head.json` is deliberately skipped here: the system head ref is\n * read through `readContractSpaceHeadRef` (which validates head-ref\n * shape, distinct from the strict user-ref hash grammar), so it is judged\n * there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,\n * …) still propagate — only parse / schema problems are made tolerant.\n */\nexport async function readRefsTolerant(refsDir: string): Promise<TolerantRefsResult> {\n let entries: string[];\n try {\n entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return { refs: {}, problems: [] };\n }\n throw error;\n }\n\n const jsonFiles = entries.filter(\n (entry) =>\n entry.endsWith('.json') &&\n !entry.endsWith('.contract.json') &&\n entry !== `${HEAD_REF_NAME}.json`,\n );\n const refs: Record<string, RefEntry> = {};\n const problems: RefLoadProblem[] = [];\n\n for (const jsonFile of jsonFiles) {\n const filePath = join(refsDir, jsonFile);\n const name = refNameFromPath(refsDir, filePath);\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT)\n // and benign EISDIR if a directory happens to end in `.json`.\n // Anything else (EACCES, EIO, …) is a real failure and propagates.\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOENT' || code === 'EISDIR') {\n continue;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n problems.push({ refName: name, detail: e instanceof Error ? e.message : String(e) });\n continue;\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n problems.push({ refName: name, detail: result.summary });\n continue;\n }\n\n refs[name] = result;\n }\n\n return { refs, problems };\n}\n\nexport async function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n if (!validateRefValue(entry.hash)) {\n throw errorInvalidRefValue(entry.hash);\n }\n\n const filePath = refFilePath(refsDir, name);\n const dir = dirname(filePath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = join(dir, `.${name.split('/').pop()}.json.${Date.now()}.tmp`);\n await writeFile(\n tmpPath,\n `${JSON.stringify({ hash: entry.hash, invariants: [...entry.invariants] }, null, 2)}\\n`,\n );\n await rename(tmpPath, filePath);\n}\n\nexport async function deleteRef(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const filePath = refFilePath(refsDir, name);\n try {\n await unlink(filePath);\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref file found at \"${filePath}\".`,\n fix: 'Run `prisma-next ref list` to see available refs.',\n details: { refName: name, filePath },\n });\n }\n throw error;\n }\n\n // Clean empty parent directories up to refsDir. Stop walking on the expected\n // \"directory has siblings\" signal (ENOTEMPTY on Linux, EEXIST on some BSDs)\n // and on ENOENT (concurrent removal). Anything else (EACCES, EIO, …) is a\n // real failure and propagates.\n let dir = dirname(filePath);\n while (dir !== refsDir && dir.startsWith(refsDir)) {\n try {\n await rmdir(dir);\n dir = dirname(dir);\n } catch (error) {\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOTEMPTY' || code === 'EEXIST' || code === 'ENOENT') {\n break;\n }\n throw error;\n }\n }\n}\n\n/**\n * Index user-authored refs by the contract hash each ref points at.\n * Each bucket is sorted lex-asc for deterministic output.\n */\nexport function refsByContractHash(refs: Refs): ReadonlyMap<string, readonly string[]> {\n const byHash = new Map<string, string[]>();\n for (const [name, entry] of Object.entries(refs)) {\n const bucket = byHash.get(entry.hash);\n if (bucket) bucket.push(name);\n else byHash.set(entry.hash, [name]);\n }\n for (const bucket of byHash.values()) {\n bucket.sort();\n }\n return byHash;\n}\n\n/**\n * Read `migrations/<space>/refs/*.json` and index by destination hash.\n * Returns an empty map when the refs directory does not exist.\n */\nexport async function resolveRefsByContractHash(\n refsDir: string,\n): Promise<ReadonlyMap<string, readonly string[]>> {\n return refsByContractHash(await readRefs(refsDir));\n}\n\nexport function resolveRef(refs: Refs, name: string): RefEntry {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n // Object.hasOwn gate: plain-object `refs` would otherwise let\n // `refs['constructor']` return Object.prototype.constructor and bypass the\n // UNKNOWN_REF throw. validateRefName accepts `\"constructor\"` as a name shape.\n if (!Object.hasOwn(refs, name)) {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref named \"${name}\" exists.`,\n fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next ref set ${name} <hash>`,\n details: { refName: name, availableRefs: Object.keys(refs) },\n });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: Object.hasOwn gate above guarantees this is defined\n return refs[name]!;\n}\n"],"mappings":"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"}
import { g as errorInvalidRefName, h as errorInvalidRefFile, t as MigrationToolsError } from "./errors-DjTpjyFU.mjs";
import { d as writeRef, l as validateRefName, n as deleteRef } from "./refs-kd62Ljkh.mjs";
import { basename, dirname, join } from "pathe";
import { access, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { type } from "arktype";
import { randomBytes } from "node:crypto";
import { canonicalizeJson } from "@prisma-next/framework-components/utils";
//#region src/refs/snapshot.ts
const ContractIrSchema = type({
targetFamily: "string",
target: "string",
profileHash: "string",
storage: type({ storageHash: "string" }),
domain: type({ namespaces: "object" })
});
function hasErrnoCode(error, code) {
return error instanceof Error && error.code === code;
}
function snapshotJsonPath(refsDir, name) {
return join(refsDir, `${name}.contract.json`);
}
function snapshotDtsPath(refsDir, name) {
return join(refsDir, `${name}.contract.d.ts`);
}
function tmpPathFor(finalPath) {
return join(dirname(finalPath), `.${basename(finalPath)}.${Date.now()}-${randomBytes(4).toString("hex")}.tmp`);
}
async function atomicWriteFile(finalPath, content) {
await mkdir(dirname(finalPath), { recursive: true });
const tmpPath = tmpPathFor(finalPath);
await writeFile(tmpPath, content);
await rename(tmpPath, finalPath);
}
async function unlinkIfExists(filePath) {
try {
await unlink(filePath);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return;
throw error;
}
}
function parseContractSnapshotJson(filePath, raw) {
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
}
const result = ContractIrSchema(parsed);
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
return result;
}
async function writeRefSnapshot(refsDir, name, snapshot) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const jsonPath = snapshotJsonPath(refsDir, name);
const dtsPath = snapshotDtsPath(refsDir, name);
const jsonContent = `${canonicalizeJson(snapshot.contract)}\n`;
const dtsContent = snapshot.contractDts.endsWith("\n") ? snapshot.contractDts : `${snapshot.contractDts}\n`;
try {
await atomicWriteFile(jsonPath, jsonContent);
} catch (error) {
await unlinkIfExists(jsonPath);
throw error;
}
try {
await atomicWriteFile(dtsPath, dtsContent);
} catch (error) {
await unlinkIfExists(jsonPath);
await unlinkIfExists(dtsPath);
throw error;
}
}
async function readRefSnapshot(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const jsonPath = snapshotJsonPath(refsDir, name);
const dtsPath = snapshotDtsPath(refsDir, name);
let raw;
try {
raw = await readFile(jsonPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return null;
throw error;
}
const contract = parseContractSnapshotJson(jsonPath, raw);
let contractDts;
try {
contractDts = await readFile(dtsPath, "utf-8");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw errorInvalidRefFile(dtsPath, "Missing paired contract.d.ts snapshot file");
throw error;
}
return {
contract,
contractDts
};
}
async function deleteRefSnapshot(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
await unlinkIfExists(snapshotJsonPath(refsDir, name));
await unlinkIfExists(snapshotDtsPath(refsDir, name));
}
async function writeRefPaired(refsDir, name, entry, snapshot) {
await writeRefSnapshot(refsDir, name, snapshot);
try {
await writeRef(refsDir, name, entry);
} catch (writeError) {
try {
await deleteRefSnapshot(refsDir, name);
} catch {}
throw writeError;
}
}
function isUnknownRefError(error) {
return MigrationToolsError.is(error) && error.code === "MIGRATION.UNKNOWN_REF";
}
async function snapshotFilesExist(refsDir, name) {
if (!validateRefName(name)) throw errorInvalidRefName(name);
const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];
return (await Promise.allSettled(paths.map((filePath) => access(filePath)))).some((result) => result.status === "fulfilled");
}
async function deleteRefPaired(refsDir, name) {
if (await snapshotFilesExist(refsDir, name)) {
try {
await deleteRef(refsDir, name);
} catch (error) {
if (!isUnknownRefError(error)) throw error;
}
await deleteRefSnapshot(refsDir, name);
return;
}
await deleteRef(refsDir, name);
await deleteRefSnapshot(refsDir, name);
}
//#endregion
export { writeRefSnapshot as a, writeRefPaired as i, deleteRefSnapshot as n, readRefSnapshot as r, deleteRefPaired as t };
//# sourceMappingURL=snapshot-C-IHpIbr.mjs.map
{"version":3,"file":"snapshot-C-IHpIbr.mjs","names":[],"sources":["../src/refs/snapshot.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport { errorInvalidRefFile, errorInvalidRefName, MigrationToolsError } from '../errors';\nimport { deleteRef, type RefEntry, validateRefName, writeRef } from '../refs';\n\nexport interface ContractIR {\n readonly contract: unknown;\n readonly contractDts: string;\n}\n\nconst ContractIrSchema = type({\n targetFamily: 'string',\n target: 'string',\n profileHash: 'string',\n storage: type({\n storageHash: 'string',\n }),\n domain: type({\n namespaces: 'object',\n }),\n});\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nfunction snapshotJsonPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.json`);\n}\n\nfunction snapshotDtsPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.d.ts`);\n}\n\nfunction tmpPathFor(finalPath: string): string {\n const dir = dirname(finalPath);\n const fileName = basename(finalPath);\n return join(dir, `.${fileName}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`);\n}\n\nasync function atomicWriteFile(finalPath: string, content: string): Promise<void> {\n const dir = dirname(finalPath);\n await mkdir(dir, { recursive: true });\n const tmpPath = tmpPathFor(finalPath);\n await writeFile(tmpPath, content);\n await rename(tmpPath, finalPath);\n}\n\nasync function unlinkIfExists(filePath: string): Promise<void> {\n try {\n await unlink(filePath);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nfunction parseContractSnapshotJson(filePath: string, raw: string): unknown {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = ContractIrSchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function writeRefSnapshot(\n refsDir: string,\n name: string,\n snapshot: ContractIR,\n): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n const jsonContent = `${canonicalizeJson(snapshot.contract)}\\n`;\n const dtsContent = snapshot.contractDts.endsWith('\\n')\n ? snapshot.contractDts\n : `${snapshot.contractDts}\\n`;\n\n try {\n await atomicWriteFile(jsonPath, jsonContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n throw error;\n }\n\n try {\n await atomicWriteFile(dtsPath, dtsContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n await unlinkIfExists(dtsPath);\n throw error;\n }\n}\n\nexport async function readRefSnapshot(refsDir: string, name: string): Promise<ContractIR | null> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n\n let raw: string;\n try {\n raw = await readFile(jsonPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n const contract = parseContractSnapshotJson(jsonPath, raw);\n\n let contractDts: string;\n try {\n contractDts = await readFile(dtsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorInvalidRefFile(dtsPath, 'Missing paired contract.d.ts snapshot file');\n }\n throw error;\n }\n\n return { contract, contractDts };\n}\n\nexport async function deleteRefSnapshot(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n await unlinkIfExists(snapshotJsonPath(refsDir, name));\n await unlinkIfExists(snapshotDtsPath(refsDir, name));\n}\n\nexport async function writeRefPaired(\n refsDir: string,\n name: string,\n entry: RefEntry,\n snapshot: ContractIR,\n): Promise<void> {\n await writeRefSnapshot(refsDir, name, snapshot);\n try {\n await writeRef(refsDir, name, entry);\n } catch (writeError) {\n try {\n await deleteRefSnapshot(refsDir, name);\n } catch {\n // Rollback failure is secondary; preserve the original writeRef error.\n }\n throw writeError;\n }\n}\n\nfunction isUnknownRefError(error: unknown): boolean {\n return MigrationToolsError.is(error) && error.code === 'MIGRATION.UNKNOWN_REF';\n}\n\nasync function snapshotFilesExist(refsDir: string, name: string): Promise<boolean> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];\n const checks = await Promise.allSettled(paths.map((filePath) => access(filePath)));\n return checks.some((result) => result.status === 'fulfilled');\n}\n\nexport async function deleteRefPaired(refsDir: string, name: string): Promise<void> {\n if (await snapshotFilesExist(refsDir, name)) {\n try {\n await deleteRef(refsDir, name);\n } catch (error) {\n if (!isUnknownRefError(error)) {\n throw error;\n }\n }\n await deleteRefSnapshot(refsDir, name);\n return;\n }\n\n await deleteRef(refsDir, name);\n await deleteRefSnapshot(refsDir, name);\n}\n"],"mappings":"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"}
//#region src/verify-contract-spaces.d.ts
/**
* List the per-space subdirectories under
* `<projectRoot>/migrations/`. Returns space-id directory names (sorted
* alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
* does **not** contain a `migration.json` manifest. The manifest is the
* structural marker of a user-authored migration directory (see
* `readMigrationsDir` in `./io`); directory names themselves belong to
* the user and are not part of the contract.
*
* Returns `[]` if the migrations directory does not exist (greenfield
* project).
*
* Reads only the user's repo. **No descriptor import.** The caller
* (verifier) feeds the result into {@link verifyContractSpaces} alongside
* the loaded-space set and the marker rows.
*/
declare function listContractSpaceDirectories(projectMigrationsDir: string): Promise<readonly string[]>;
/**
* On-disk head value (`(hash, invariants)`) for one contract space.
* The verifier compares this against the marker row for the same space
* to detect drift between the user-emitted artefacts and the live DB
* marker.
*/
interface ContractSpaceHeadRecord {
readonly hash: string;
readonly invariants: readonly string[];
}
/**
* Marker row read from `prisma_contract.marker` (one per `space`).
* Caller resolves these via the family runtime's marker reader before
* invoking {@link verifyContractSpaces}.
*/
interface SpaceMarkerRecord {
readonly hash: string;
readonly invariants: readonly string[];
}
interface VerifyContractSpacesInputs {
/**
* Set of contract spaces the project declares: `'app'` plus each
* extension space in `extensionPacks`. The caller's discovery path
* never reads the extension descriptor module — it walks the
* `extensionPacks` configuration in `prisma-next.config.ts` for the
* space ids.
*/
readonly loadedSpaces: ReadonlySet<string>;
/**
* Per-space subdirectories observed under
* `<projectRoot>/migrations/`. Resolved via
* {@link listContractSpaceDirectories}.
*/
readonly spaceDirsOnDisk: readonly string[];
/**
* Head ref per space, keyed by space id. Caller reads
* `<projectRoot>/migrations/<space-id>/contract.json` and
* `<projectRoot>/migrations/<space-id>/refs/head.json` to construct
* this map. Spaces with no contract-space dir on disk simply omit a
* map entry.
*/
readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;
/**
* Marker rows keyed by `space`. Caller reads them from the
* `prisma_contract.marker` table.
*/
readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;
}
type SpaceVerifierViolation = {
readonly kind: 'declaredButUnmigrated';
readonly spaceId: string;
readonly remediation: string;
} | {
readonly kind: 'orphanMarker';
readonly spaceId: string;
readonly remediation: string;
} | {
readonly kind: 'orphanSpaceDir';
readonly spaceId: string;
readonly remediation: string;
} | {
readonly kind: 'hashMismatch';
readonly spaceId: string;
readonly priorHeadHash: string;
readonly markerHash: string;
readonly remediation: string;
} | {
readonly kind: 'invariantsMismatch';
readonly spaceId: string;
readonly onDiskInvariants: readonly string[];
readonly markerInvariants: readonly string[];
readonly remediation: string;
};
type VerifyContractSpacesResult = {
readonly ok: true;
} | {
readonly ok: false;
readonly violations: readonly SpaceVerifierViolation[];
};
/**
* Pure structural verifier for the per-space mechanism. Aggregates the
* three orphan / missing checks plus per-space hash and invariant
* comparison.
*
* Algorithm:
*
* - For every extension space declared in `loadedSpaces` (`'app'`
* excluded — the per-space verifier is scoped to extension members;
* the app is verified through the aggregate path):
* - If no contract-space dir on disk → `declaredButUnmigrated`.
* - Else if `markerRowsBySpace` lacks an entry → no violation here;
* the live-DB compare done outside this helper is where the
* absence shows up.
* - Else compare marker hash / invariants vs. on-disk head hash /
* invariants → `hashMismatch` / `invariantsMismatch` on drift.
* - For every contract-space dir on disk that is not in `loadedSpaces` →
* `orphanSpaceDir`.
* - For every marker row whose `space` is not in `loadedSpaces` →
* `orphanMarker`. The app-space marker is always loaded (`'app'` is
* in `loadedSpaces` by definition).
*
* Output is deterministic: violations are sorted first by `kind`
* (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
* `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
* passing equivalent inputs see byte-identical violation lists.
*
* Synchronous, pure, no I/O. **Does not import the extension descriptor**
* (the inputs are pre-resolved by the caller); the verifier reads only
* the user repo, not `node_modules`.
*/
declare function verifyContractSpaces(inputs: VerifyContractSpacesInputs): VerifyContractSpacesResult;
//#endregion
export { VerifyContractSpacesResult as a, VerifyContractSpacesInputs as i, SpaceMarkerRecord as n, listContractSpaceDirectories as o, SpaceVerifierViolation as r, verifyContractSpaces as s, ContractSpaceHeadRecord as t };
//# sourceMappingURL=verify-contract-spaces-CnXNGpBV.d.mts.map
{"version":3,"file":"verify-contract-spaces-CnXNGpBV.d.mts","names":[],"sources":["../src/verify-contract-spaces.ts"],"mappings":";;AAyBA;;;;AAEU;AAyCV;;;;AAEqB;AAQrB;;;;AAEqB;iBAvDC,4BAAA,CACpB,oBAAA,WACC,OAAO;;;;;;;UAyCO,uBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;;;;;;UAQJ,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;AAAA,UAGJ,0BAAA;EAiCL;;;;;;;EAAA,SAzBD,YAAA,EAAc,WAAA;EAiCV;;;;;EAAA,SA1BJ,eAAA;EAoCI;;;;;;;EAAA,SA3BJ,eAAA,EAAiB,WAAA,SAAoB,uBAAA;EAqCjC;;AAAW;AAG1B;EAHe,SA/BJ,iBAAA,EAAmB,WAAA,SAAoB,iBAAA;AAAA;AAAA,KAGtC,sBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,gBAAA;EAAA,SACA,gBAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGH,0BAAA;EAAA,SACG,EAAA;AAAA;EAAA,SACA,EAAA;EAAA,SAAoB,UAAA,WAAqB,sBAAsB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC9D,oBAAA,CACd,MAAA,EAAQ,0BAAA,GACP,0BAA0B"}
import { elementCoordinates } from '@prisma-next/framework-components/ir';
import type { ContractSpaceMember } from './types';
/**
* Project the **introspected live schema** to the slice claimed by a
* single contract-space member.
*
* "Schema" here means the live introspected database state — the
* planner / verifier sees this object as a `MongoSchemaIR` (Mongo) or
* `SqlSchemaIR` (SQL). It is **not** a database schema in the SQL
* `CREATE SCHEMA` sense, nor a contract-space namespace. The
* function's job is to filter that introspected state down to the
* elements claimed by one space, so a per-space verify pass doesn't
* see another space's storage as "extras".
*
* Returns the same `schema` value with every top-level storage element
* (table or collection) claimed by **other** members of the aggregate
* removed. Elements not claimed by any member flow through unchanged —
* the planner / verifier sees them as orphans (extras in strict mode).
*
* Used by:
*
* - The aggregate planner's **synth strategy**: when synthesising a
* plan against a member's contract, the live schema must be projected
* to that member's slice so the planner doesn't treat elements claimed
* by other members as "extras" and emit destructive ops to drop them.
* - The aggregate verifier's **schemaCheck**: projects per member so the
* single-contract verify only sees the slice claimed by the member it
* is checking. Closes the architectural concern that a multi-member
* deployment makes each member's elements look like extras to every
* other member's verify pass.
*
* **Duck-typing semantics**: the helper operates on `unknown` for the
* schema and falls through structurally if the shape doesn't match.
* Two storage shapes are recognised today:
*
* - SQL families expose `storage.tables: Record<string, ...>` on
* contracts and the introspected schema mirrors the same record shape.
* Pruning iterates the record entries.
* - Mongo exposes `storage.collections: Record<string, ...>` on
* contracts; the introspected `MongoSchemaIR` exposes
* `collections: ReadonlyArray<{name: string, ...}>`. Pruning iterates
* the array on the schema side and the record's keys on the
* other-member side.
*
* Schemas of unrecognised shape are returned unchanged. The function
* never imports family classes (`SqlSchemaIR`, `MongoSchemaIR`); the
* projected schema is a plain object — `{...schema, tables: pruned}` or
* `{...schema, collections: pruned}` — that downstream consumers
* duck-type. A future family with a different storage shape gets the
* schema returned unchanged rather than blowing up the aggregate
* planner.
*
* Record-shape detection guards against arrays (`!Array.isArray`) so
* an unrecognised array-shaped value falls through unchanged rather
* than being pruned by numeric keys.
*/
export function projectSchemaToSpace(
schema: unknown,
member: ContractSpaceMember,
otherMembers: ReadonlyArray<ContractSpaceMember>,
): unknown {
if (typeof schema !== 'object' || schema === null) return schema;
const ownedByOthers = collectOwnedNames(member, otherMembers);
if (ownedByOthers.size === 0) return schema;
const schemaObj = schema as { readonly tables?: unknown; readonly collections?: unknown };
if (
typeof schemaObj.tables === 'object' &&
schemaObj.tables !== null &&
!Array.isArray(schemaObj.tables)
) {
return pruneRecord(schemaObj, 'tables', ownedByOthers);
}
if (Array.isArray(schemaObj.collections)) {
return pruneCollectionsArray(schemaObj, ownedByOthers);
}
if (
typeof schemaObj.collections === 'object' &&
schemaObj.collections !== null &&
!Array.isArray(schemaObj.collections)
) {
return pruneRecord(schemaObj, 'collections', ownedByOthers);
}
return schema;
}
function collectOwnedNames(
member: ContractSpaceMember,
otherMembers: ReadonlyArray<ContractSpaceMember>,
): Set<string> {
const owned = new Set<string>();
for (const other of otherMembers) {
if (other.spaceId === member.spaceId) continue;
for (const { entityName } of elementCoordinates(other.contract().storage)) {
owned.add(entityName);
}
}
return owned;
}
function pruneRecord(
schemaObj: { readonly tables?: unknown; readonly collections?: unknown },
field: 'tables' | 'collections',
ownedByOthers: ReadonlySet<string>,
): unknown {
const source = schemaObj[field] as Record<string, unknown>;
let removed = false;
const pruned: Record<string, unknown> = {};
for (const [name, value] of Object.entries(source)) {
if (ownedByOthers.has(name)) {
removed = true;
} else {
pruned[name] = value;
}
}
if (!removed) return schemaObj;
return { ...schemaObj, [field]: pruned };
}
function pruneCollectionsArray(
schemaObj: { readonly collections?: unknown },
ownedByOthers: ReadonlySet<string>,
): unknown {
const source = schemaObj.collections as ReadonlyArray<unknown>;
let removed = false;
const pruned: unknown[] = [];
for (const entry of source) {
if (typeof entry === 'object' && entry !== null) {
const name = (entry as { readonly name?: unknown }).name;
if (typeof name === 'string' && ownedByOthers.has(name)) {
removed = true;
continue;
}
}
pruned.push(entry);
}
if (!removed) return schemaObj;
return { ...schemaObj, collections: pruned };
}
import type { MigrationPlan } from '@prisma-next/framework-components/control';
import { EMPTY_CONTRACT_HASH } from '../../constants';
import { findPathWithDecision } from '../../migration-graph';
import type { MigrationOps, OnDiskMigrationPackage } from '../../package';
import { requireHeadRef } from '../aggregate';
import type { ContractMarkerRecordLike } from '../marker-types';
import type { PerSpacePlan } from '../planner-types';
import type { ContractSpaceMember } from '../types';
/**
* Outcome variants for the graph-walk strategy. Mirrors
* {@link import('../../compute-extension-space-apply-path').ExtensionSpaceApplyPathOutcome}
* but operates against the member's lazily-reconstructed `graph()`
* instead of re-reading from disk. The aggregate planner converts
* these into {@link import('../planner-types').PlannerError}
* variants.
*/
export type GraphWalkOutcome =
| { readonly kind: 'ok'; readonly result: PerSpacePlan }
| { readonly kind: 'unreachable' }
| { readonly kind: 'unsatisfiable'; readonly missing: readonly string[] };
export interface GraphWalkStrategyInputs {
readonly aggregateTargetId: string;
readonly member: ContractSpaceMember;
readonly currentMarker: ContractMarkerRecordLike | null;
/**
* Optional ref name to decorate the resulting `PathDecision`. Used by
* `migrate` to surface the user-supplied `--to <name>` in
* structured-progress events and invariant-path error envelopes. The
* strategy itself does not interpret it.
*/
readonly refName?: string;
}
/**
* Walk a member's hydrated migration graph from the live marker to
* `member.headRef.hash`, covering every required invariant.
*
* Pure synchronous function — no I/O. The aggregate's loader has
* already integrity-checked every package and reconstructed the graph;
* this strategy just looks up ops by `migrationHash` and assembles a
* `MigrationPlan` with `targetId` set from the aggregate (no
* placeholder cast).
*
* Required invariants are computed as `headRef.invariants \ marker.invariants`
* — the marker already declares some invariants satisfied; the path
* only needs to provide the remainder. Mirrors today's
* `computeExtensionSpaceApplyPath` semantics.
*/
export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutcome {
const { aggregateTargetId, member, currentMarker, refName } = input;
const headRef = requireHeadRef(member);
const graph = member.graph();
const packagesByMigrationHash = new Map<string, OnDiskMigrationPackage>(
member.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]),
);
const fromHash = currentMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
const markerInvariants = new Set(currentMarker?.invariants ?? []);
const required = new Set(headRef.invariants.filter((id) => !markerInvariants.has(id)));
const outcome = findPathWithDecision(graph, fromHash, headRef.hash, {
required,
...(refName !== undefined ? { refName } : {}),
});
if (outcome.kind === 'unreachable') {
return { kind: 'unreachable' };
}
if (outcome.kind === 'unsatisfiable') {
return { kind: 'unsatisfiable', missing: outcome.missing };
}
const pathOps: MigrationOps[number][] = [];
const providedInvariantsSet = new Set<string>();
const edgeRefs: Array<{
migrationHash: string;
dirName: string;
from: string;
to: string;
operationCount: number;
}> = [];
for (const edge of outcome.decision.selectedPath) {
const pkg = packagesByMigrationHash.get(edge.migrationHash);
if (!pkg) {
throw new Error(
`Migration package missing for edge ${edge.migrationHash} in space "${member.spaceId}". The hydrated migration graph and packagesByMigrationHash map are out of sync — this should be unreachable; report.`,
);
}
for (const op of pkg.ops) pathOps.push(op);
for (const invariant of pkg.metadata.providedInvariants) providedInvariantsSet.add(invariant);
edgeRefs.push({
migrationHash: edge.migrationHash,
dirName: edge.dirName,
from: edge.from,
to: edge.to,
operationCount: pkg.ops.length,
});
}
const plan: MigrationPlan = {
targetId: aggregateTargetId,
spaceId: member.spaceId,
origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
destination: { storageHash: headRef.hash },
operations: pathOps,
providedInvariants: [...providedInvariantsSet].sort(),
};
return {
kind: 'ok',
result: {
plan,
displayOps: pathOps,
destinationContract: member.contract(),
strategy: 'graph-walk',
migrationEdges: edgeRefs,
pathDecision: outcome.decision,
},
};
}
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
import type {
ControlAdapterInstance,
ControlFamilyInstance,
MigrationOperationPolicy,
MigrationPlan,
MigrationPlannerConflict,
MigrationPlannerResult,
TargetMigrationsCapability,
} from '@prisma-next/framework-components/control';
import type { ContractMarkerRecordLike } from '../marker-types';
import type { PerSpacePlan } from '../planner-types';
import { projectSchemaToSpace } from '../project-schema-to-space';
import { buildSynthMigrationEdge } from '../synth-migration-edge';
import type { ContractSpaceMember } from '../types';
export interface SynthStrategyInputs<TFamilyId extends string, TTargetId extends string> {
readonly aggregateTargetId: string;
readonly currentMarker: ContractMarkerRecordLike | null;
readonly member: ContractSpaceMember;
readonly otherMembers: ReadonlyArray<ContractSpaceMember>;
readonly schemaIntrospection: unknown;
readonly adapter: ControlAdapterInstance<TFamilyId, TTargetId>;
readonly migrations: TargetMigrationsCapability<
TFamilyId,
TTargetId,
ControlFamilyInstance<TFamilyId, unknown>
>;
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
readonly operationPolicy: MigrationOperationPolicy;
}
export type SynthStrategyOutcome =
| { readonly kind: 'ok'; readonly result: PerSpacePlan }
| { readonly kind: 'failure'; readonly conflicts: readonly MigrationPlannerConflict[] };
/**
* The {@link MigrationPlanner.plan} interface is declared as synchronous,
* but historical and test fixture call sites have always invoked it
* with `await` (see prior `db-apply-per-space.ts`). Tolerating a
* Promise here keeps existing test mocks working without changing the
* declared family SPI.
*/
type MaybeAsyncPlannerResult = MigrationPlannerResult | Promise<MigrationPlannerResult>;
/**
* Synthesise a migration plan for a single member by projecting the
* live schema down to that member's claimed slice and delegating to
* the family's `createPlanner(...).plan(...)`.
*
* Pre-projection (via {@link projectSchemaToSpace}) closes the F23
* concern: without it, the family's planner sees other members'
* tables as "extras" and emits destructive ops to drop them. With it,
* the planner only sees the slice this member claims.
*
* The synthesised plan's `targetId` is set from `aggregateTargetId`
* (the aggregate's ambient target). The family's planner does not
* stamp `targetId` on the produced plan; the aggregate planner is
* the single point that knows the target.
*
* Used by:
*
* - The app member by default (CLI policy
* `ignoreGraphFor: { app.spaceId }`).
* - Any extension member whose `headRef.invariants` is empty (the
* strategy selector falls back to synth when graph-walk isn't
* required).
*/
export async function synthStrategy<TFamilyId extends string, TTargetId extends string>(
input: SynthStrategyInputs<TFamilyId, TTargetId>,
): Promise<SynthStrategyOutcome> {
const projectedSchema = projectSchemaToSpace(
input.schemaIntrospection,
input.member,
input.otherMembers,
);
const planner = input.migrations.createPlanner(input.adapter);
const plannerResult: MigrationPlannerResult = await (planner.plan({
contract: input.member.contract(),
schema: projectedSchema,
policy: input.operationPolicy,
fromContract: null,
frameworkComponents: input.frameworkComponents,
spaceId: input.member.spaceId,
}) as MaybeAsyncPlannerResult);
if (plannerResult.kind === 'failure') {
return { kind: 'failure', conflicts: plannerResult.conflicts };
}
const synthedPlan = plannerResult.plan;
// The family planner returns a class-instance-shaped plan whose
// `destination` / `operations` are accessors on the prototype, often
// backed by private fields. A naive spread (`{ ...synthedPlan }`)
// would lose those accessors and produce a plan with
// `destination: undefined`; rebinding the prototype on a plain
// object would break private-field access. We instead wrap the plan
// in a Proxy that forwards every read except `targetId`, which is
// stamped from the aggregate's ambient target. This preserves the
// planner's class semantics while keeping the aggregate the single
// source of truth for `targetId`.
const plan: MigrationPlan = new Proxy(synthedPlan, {
get(target, prop) {
if (prop === 'targetId') return input.aggregateTargetId;
// Forward `this` as the original target so prototype-bound
// private fields (#destination, #operations, …) resolve.
return Reflect.get(target, prop, target);
},
has(target, prop) {
if (prop === 'targetId') return true;
return Reflect.has(target, prop);
},
});
const destinationStorageHash = synthedPlan.destination.storageHash;
const synthedOps = await Promise.all(synthedPlan.operations);
return {
kind: 'ok',
result: {
plan,
displayOps: synthedOps,
destinationContract: input.member.contract(),
strategy: 'synth',
...(plannerResult.warnings && plannerResult.warnings.length > 0
? { warnings: plannerResult.warnings }
: {}),
migrationEdges: [
buildSynthMigrationEdge({
currentMarkerStorageHash: input.currentMarker?.storageHash,
destinationStorageHash,
operationCount: synthedOps.length,
}),
],
},
};
}
import type { AggregateMigrationEdgeRef } from './planner-types';
export function buildSynthMigrationEdge(args: {
readonly currentMarkerStorageHash: string | null | undefined;
readonly destinationStorageHash: string;
readonly operationCount: number;
}): AggregateMigrationEdgeRef {
return {
dirName: '',
migrationHash: args.destinationStorageHash,
from: args.currentMarkerStorageHash ?? '',
to: args.destinationStorageHash,
operationCount: args.operationCount,
};
}

Sorry, the diff of this file is too big to display