@prisma-next/migration-tools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @prisma-next/migration-tools
2
+
3
+ On-disk migration persistence, attestation, and chain reconstruction for Prisma Next.
4
+
5
+ ## Responsibilities
6
+
7
+ - **Types**: Define the on-disk migration format (`MigrationManifest`, `MigrationOps`, `MigrationPackage`, `MigrationGraph`)
8
+ - **I/O**: Read and write migration packages to/from disk (`migration.json` + `ops.json`)
9
+ - **Attestation**: Compute and verify content-addressed migration IDs for tamper detection
10
+ - **Chain reconstruction**: Reconstruct and navigate migration history (path finding, latest migration detection, cycle/orphan detection)
11
+
12
+ ## Attestation framing
13
+
14
+ `computeMigrationId` in `attestation.ts` uses explicit framing:
15
+
16
+ 1. Canonicalize migration manifest metadata, ops, and embedded contracts.
17
+ 2. Hash each canonical part independently with SHA-256.
18
+ 3. Hash the canonical JSON tuple of those part hashes.
19
+
20
+ This avoids delimiter-ambiguity and ensures `migrationId` commits to the exact 4-part tuple.
21
+
22
+ ## Ops validation boundary
23
+
24
+ `readMigrationPackage` performs intentionally shallow `ops.json` validation in `io.ts`:
25
+
26
+ - validates envelope fields (`id`, `label`, `operationClass`)
27
+ - does not fully validate operation-specific payload shape
28
+
29
+ Full semantic validation happens in target/family migration planners and runners at execution/planning time.
30
+
31
+ ## Architecture
32
+
33
+ ```mermaid
34
+ graph TD
35
+ CLI["CLI commands<br/>(migration plan, apply, verify, show, status)"] --> IO["io.ts<br/>File I/O"]
36
+ CLI --> ATT["attestation.ts<br/>Migration attestation"]
37
+ CLI --> DAG["dag.ts<br/>Chain operations"]
38
+ IO --> TYPES["types.ts<br/>MigrationManifest, etc."]
39
+ ATT --> IO
40
+ ATT --> CAN["canonicalize-json.ts"]
41
+ ATT --> CP["@prisma-next/core-control-plane<br/>canonicalizeContract"]
42
+ DAG --> TYPES
43
+ DAG --> ABS["@prisma-next/core-control-plane<br/>EMPTY_CONTRACT_HASH"]
44
+ ```
45
+
46
+ ## Dependencies
47
+
48
+ | Package | Why |
49
+ |---|---|
50
+ | `@prisma-next/contract` | `ContractIR` type for embedded contracts in manifests |
51
+ | `@prisma-next/core-control-plane` | `MigrationPlanOperation` types, `EMPTY_CONTRACT_HASH`, `canonicalizeContract` |
52
+ | `arktype` | Runtime shape validation for `migration.json` and `ops.json` |
53
+ | `@prisma-next/utils` | Workspace utility dependency (currently no direct runtime imports in this package) |
54
+ | `pathe` | Cross-platform path manipulation |
55
+
56
+ ### Dependents
57
+
58
+ - `@prisma-next/cli` (M3) — CLI commands consume these functions
59
+
60
+ ## Export Subpaths
61
+
62
+ | Subpath | Contents |
63
+ |---|---|
64
+ | `./types` | `MigrationManifest`, `MigrationOps`, `MigrationPackage`, `MigrationGraph`, `MigrationChainEntry`, `MigrationHints` |
65
+ | `./io` | `writeMigrationPackage`, `readMigrationPackage`, `readMigrationsDir`, `formatMigrationDirName` |
66
+ | `./attestation` | `computeMigrationId`, `attestMigration`, `verifyMigration` |
67
+ | `./dag` | `reconstructGraph`, `findLeaf`, `findPath`, `detectCycles`, `detectOrphans` |
68
+
69
+ ## On-Disk Format
70
+
71
+ Each migration is a directory containing two files:
72
+
73
+ ```
74
+ migrations/
75
+ 20260225T1430_add_users/
76
+ migration.json # MigrationManifest
77
+ ops.json # MigrationPlanOperation[]
78
+ ```
79
+
80
+ See [ADR 028](../../../docs/architecture%20docs/adrs/ADR%20028%20-%20Migration%20Structure%20%26%20Operations.md) and [ADR 001](../../../docs/architecture%20docs/adrs/ADR%20001%20-%20Migrations%20as%20Edges.md) for design rationale.
81
+
82
+ ## Commands
83
+
84
+ ```bash
85
+ pnpm build # Build with tsdown
86
+ pnpm test # Run tests
87
+ pnpm typecheck # Type-check
88
+ ```
@@ -0,0 +1,115 @@
1
+ //#region src/errors.ts
2
+ /**
3
+ * Structured error for migration tooling operations.
4
+ *
5
+ * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
6
+ * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
7
+ * migration-chain reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
8
+ * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
9
+ *
10
+ * Fields:
11
+ * - code: Stable machine-readable code (MIGRATION.SUBCODE)
12
+ * - category: Always 'MIGRATION'
13
+ * - why: Explains the cause in plain language
14
+ * - fix: Actionable remediation step
15
+ * - details: Machine-readable structured data for agents
16
+ */
17
+ var MigrationToolsError = class extends Error {
18
+ code;
19
+ category = "MIGRATION";
20
+ why;
21
+ fix;
22
+ details;
23
+ constructor(code, summary, options) {
24
+ super(summary);
25
+ this.name = "MigrationToolsError";
26
+ this.code = code;
27
+ this.why = options.why;
28
+ this.fix = options.fix;
29
+ this.details = options.details;
30
+ }
31
+ static is(error) {
32
+ if (!(error instanceof Error)) return false;
33
+ const candidate = error;
34
+ return candidate.name === "MigrationToolsError" && typeof candidate.code === "string";
35
+ }
36
+ };
37
+ function errorDirectoryExists(dir) {
38
+ return new MigrationToolsError("MIGRATION.DIR_EXISTS", "Migration directory already exists", {
39
+ why: `The directory "${dir}" already exists. Each migration must have a unique directory.`,
40
+ fix: "Use --name to pick a different name, or delete the existing directory and re-run.",
41
+ details: { dir }
42
+ });
43
+ }
44
+ function errorMissingFile(file, dir) {
45
+ return new MigrationToolsError("MIGRATION.FILE_MISSING", `Missing ${file}`, {
46
+ why: `Expected "${file}" in "${dir}" but the file does not exist.`,
47
+ fix: "Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.",
48
+ details: {
49
+ file,
50
+ dir
51
+ }
52
+ });
53
+ }
54
+ function errorInvalidJson(filePath, parseError) {
55
+ return new MigrationToolsError("MIGRATION.INVALID_JSON", "Invalid JSON in migration file", {
56
+ why: `Failed to parse "${filePath}": ${parseError}`,
57
+ fix: "Fix the JSON syntax error, or delete the migration directory and re-run migration plan.",
58
+ details: {
59
+ filePath,
60
+ parseError
61
+ }
62
+ });
63
+ }
64
+ function errorInvalidManifest(filePath, reason) {
65
+ return new MigrationToolsError("MIGRATION.INVALID_MANIFEST", "Invalid migration manifest", {
66
+ why: `Manifest at "${filePath}" is invalid: ${reason}`,
67
+ fix: "Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.",
68
+ details: {
69
+ filePath,
70
+ reason
71
+ }
72
+ });
73
+ }
74
+ function errorInvalidSlug(slug) {
75
+ return new MigrationToolsError("MIGRATION.INVALID_NAME", "Invalid migration name", {
76
+ why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
77
+ fix: "Provide a name with at least one alphanumeric character, e.g. --name add_users.",
78
+ details: { slug }
79
+ });
80
+ }
81
+ function errorSelfLoop(dirName, hash) {
82
+ return new MigrationToolsError("MIGRATION.SELF_LOOP", "Self-loop in migration graph", {
83
+ why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
84
+ fix: "Delete the invalid migration directory and re-run migration plan.",
85
+ details: {
86
+ dirName,
87
+ hash
88
+ }
89
+ });
90
+ }
91
+ function errorAmbiguousLeaf(leaves) {
92
+ return new MigrationToolsError("MIGRATION.AMBIGUOUS_LEAF", "Ambiguous migration graph", {
93
+ why: `Multiple leaf nodes found: ${leaves.join(", ")}. The migration graph has diverged — this typically happens when two developers plan migrations from the same starting point.`,
94
+ fix: "Delete one of the conflicting migration directories, then re-run `migration plan` to re-plan it from the remaining branch. Or use --from <hash> to explicitly select a starting point.",
95
+ details: { leaves }
96
+ });
97
+ }
98
+ function errorNoRoot(nodes) {
99
+ return new MigrationToolsError("MIGRATION.NO_ROOT", "Migration graph has no root", {
100
+ why: `No root migration found in the migration graph (nodes: ${nodes.join(", ")}). Every migration references a parentMigrationId that does not exist, or the graph contains a cycle in parent pointers.`,
101
+ fix: "Inspect the migrations directory for corrupted migration.json files. Exactly one migration must have parentMigrationId set to null (the first migration).",
102
+ details: { nodes }
103
+ });
104
+ }
105
+ function errorDuplicateMigrationId(migrationId) {
106
+ return new MigrationToolsError("MIGRATION.DUPLICATE_MIGRATION_ID", "Duplicate migrationId in migration graph", {
107
+ why: `Multiple migrations share migrationId "${migrationId}". This makes parent-chain reconstruction ambiguous and unsafe.`,
108
+ fix: "Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.",
109
+ details: { migrationId }
110
+ });
111
+ }
112
+
113
+ //#endregion
114
+ export { errorInvalidJson as a, errorMissingFile as c, errorDuplicateMigrationId as i, errorNoRoot as l, errorAmbiguousLeaf as n, errorInvalidManifest as o, errorDirectoryExists as r, errorInvalidSlug as s, MigrationToolsError as t, errorSelfLoop as u };
115
+ //# sourceMappingURL=errors-DdSjGRqx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-DdSjGRqx.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,\n * migration-chain reconstruction), distinct from the runtime MIGRATION.* codes for apply-time\n * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: 'Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.',\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: 'Fix the JSON syntax error, or delete the migration directory and re-run migration plan.',\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.',\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorSelfLoop(dirName: string, hash: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.SELF_LOOP', 'Self-loop in migration graph', {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\". A migration must transition between two different contract states.`,\n fix: 'Delete the invalid migration directory and re-run migration plan.',\n details: { dirName, hash },\n });\n}\n\nexport function errorAmbiguousLeaf(leaves: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_LEAF', 'Ambiguous migration graph', {\n why: `Multiple leaf nodes found: ${leaves.join(', ')}. The migration graph has diverged — this typically happens when two developers plan migrations from the same starting point.`,\n fix: 'Delete one of the conflicting migration directories, then re-run `migration plan` to re-plan it from the remaining branch. Or use --from <hash> to explicitly select a starting point.',\n details: { leaves },\n });\n}\n\nexport function errorNoRoot(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_ROOT', 'Migration graph has no root', {\n why: `No root migration found in the migration graph (nodes: ${nodes.join(', ')}). Every migration references a parentMigrationId that does not exist, or the graph contains a cycle in parent pointers.`,\n fix: 'Inspect the migrations directory for corrupted migration.json files. Exactly one migration must have parentMigrationId set to null (the first migration).',\n details: { nodes },\n });\n}\n\nexport function errorDuplicateMigrationId(migrationId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_ID',\n 'Duplicate migrationId in migration graph',\n {\n why: `Multiple migrations share migrationId \"${migrationId}\". This makes parent-chain reconstruction ambiguous and unsafe.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.',\n details: { migrationId },\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,AAAS;CACT,AAAS,WAAW;CACpB,AAAS;CACT,AAAS;CACT,AAAS;CAET,YACE,MACA,SACA,SAKA;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,OAAK,MAAM,QAAQ;AACnB,OAAK,MAAM,QAAQ;AACnB,OAAK,UAAU,QAAQ;;CAGzB,OAAO,GAAG,OAA8C;AACtD,MAAI,EAAE,iBAAiB,OAAQ,QAAO;EACtC,MAAM,YAAY;AAClB,SAAO,UAAU,SAAS,yBAAyB,OAAO,UAAU,SAAS;;;AAIjF,SAAgB,qBAAqB,KAAkC;AACrE,QAAO,IAAI,oBAAoB,wBAAwB,sCAAsC;EAC3F,KAAK,kBAAkB,IAAI;EAC3B,KAAK;EACL,SAAS,EAAE,KAAK;EACjB,CAAC;;AAGJ,SAAgB,iBAAiB,MAAc,KAAkC;AAC/E,QAAO,IAAI,oBAAoB,0BAA0B,WAAW,QAAQ;EAC1E,KAAK,aAAa,KAAK,QAAQ,IAAI;EACnC,KAAK;EACL,SAAS;GAAE;GAAM;GAAK;EACvB,CAAC;;AAGJ,SAAgB,iBAAiB,UAAkB,YAAyC;AAC1F,QAAO,IAAI,oBAAoB,0BAA0B,kCAAkC;EACzF,KAAK,oBAAoB,SAAS,KAAK;EACvC,KAAK;EACL,SAAS;GAAE;GAAU;GAAY;EAClC,CAAC;;AAGJ,SAAgB,qBAAqB,UAAkB,QAAqC;AAC1F,QAAO,IAAI,oBAAoB,8BAA8B,8BAA8B;EACzF,KAAK,gBAAgB,SAAS,gBAAgB;EAC9C,KAAK;EACL,SAAS;GAAE;GAAU;GAAQ;EAC9B,CAAC;;AAGJ,SAAgB,iBAAiB,MAAmC;AAClE,QAAO,IAAI,oBAAoB,0BAA0B,0BAA0B;EACjF,KAAK,aAAa,KAAK;EACvB,KAAK;EACL,SAAS,EAAE,MAAM;EAClB,CAAC;;AAGJ,SAAgB,cAAc,SAAiB,MAAmC;AAChF,QAAO,IAAI,oBAAoB,uBAAuB,gCAAgC;EACpF,KAAK,cAAc,QAAQ,yBAAyB,KAAK;EACzD,KAAK;EACL,SAAS;GAAE;GAAS;GAAM;EAC3B,CAAC;;AAGJ,SAAgB,mBAAmB,QAAgD;AACjF,QAAO,IAAI,oBAAoB,4BAA4B,6BAA6B;EACtF,KAAK,8BAA8B,OAAO,KAAK,KAAK,CAAC;EACrD,KAAK;EACL,SAAS,EAAE,QAAQ;EACpB,CAAC;;AAGJ,SAAgB,YAAY,OAA+C;AACzE,QAAO,IAAI,oBAAoB,qBAAqB,+BAA+B;EACjF,KAAK,0DAA0D,MAAM,KAAK,KAAK,CAAC;EAChF,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAGJ,SAAgB,0BAA0B,aAA0C;AAClF,QAAO,IAAI,oBACT,oCACA,4CACA;EACE,KAAK,0CAA0C,YAAY;EAC3D,KAAK;EACL,SAAS,EAAE,aAAa;EACzB,CACF"}
@@ -0,0 +1,15 @@
1
+ import { a as MigrationOps, i as MigrationManifest } from "../types-CUnzoaLY.mjs";
2
+
3
+ //#region src/attestation.d.ts
4
+ interface VerifyResult {
5
+ readonly ok: boolean;
6
+ readonly reason?: 'draft' | 'mismatch';
7
+ readonly storedMigrationId?: string;
8
+ readonly computedMigrationId?: string;
9
+ }
10
+ declare function computeMigrationId(manifest: MigrationManifest, ops: MigrationOps): string;
11
+ declare function attestMigration(dir: string): Promise<string>;
12
+ declare function verifyMigration(dir: string): Promise<VerifyResult>;
13
+ //#endregion
14
+ export { attestMigration, computeMigrationId, verifyMigration };
15
+ //# sourceMappingURL=attestation.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation.d.mts","names":[],"sources":["../../src/attestation.ts"],"sourcesContent":[],"mappings":";;;UAQiB,YAAA;;EAAA,SAAA,MAAY,CAAA,EAAA,OAAA,GAAA,UAAA;EAWb,SAAA,iBAAkB,CAAA,EAAA,MAAW;EA2BvB,SAAA,mBAAe,CAAe,EAAA,MAAO;AAU3D;iBArCgB,kBAAA,WAA6B,wBAAwB;iBA2B/C,eAAA,eAA8B;iBAU9B,eAAA,eAA8B,QAAQ"}
@@ -0,0 +1,65 @@
1
+ import { n as readMigrationPackage } from "../io-Dx98-h0p.mjs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { join } from "pathe";
4
+ import { createHash } from "node:crypto";
5
+ import { canonicalizeContract } from "@prisma-next/core-control-plane/emission";
6
+
7
+ //#region src/canonicalize-json.ts
8
+ function sortKeys(value) {
9
+ if (value === null || typeof value !== "object") return value;
10
+ if (Array.isArray(value)) return value.map(sortKeys);
11
+ const sorted = {};
12
+ for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key]);
13
+ return sorted;
14
+ }
15
+ function canonicalizeJson(value) {
16
+ return JSON.stringify(sortKeys(value));
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/attestation.ts
21
+ function sha256Hex(input) {
22
+ return createHash("sha256").update(input).digest("hex");
23
+ }
24
+ function computeMigrationId(manifest, ops) {
25
+ const { migrationId: _migrationId, signature: _signature, fromContract: _fromContract, toContract: _toContract, ...strippedMeta } = manifest;
26
+ return `sha256:${sha256Hex(canonicalizeJson([
27
+ canonicalizeJson(strippedMeta),
28
+ canonicalizeJson(ops),
29
+ manifest.fromContract !== null ? canonicalizeContract(manifest.fromContract) : "null",
30
+ canonicalizeContract(manifest.toContract)
31
+ ].map(sha256Hex)))}`;
32
+ }
33
+ async function attestMigration(dir) {
34
+ const pkg = await readMigrationPackage(dir);
35
+ const migrationId = computeMigrationId(pkg.manifest, pkg.ops);
36
+ const updated = {
37
+ ...pkg.manifest,
38
+ migrationId
39
+ };
40
+ await writeFile(join(dir, "migration.json"), JSON.stringify(updated, null, 2));
41
+ return migrationId;
42
+ }
43
+ async function verifyMigration(dir) {
44
+ const pkg = await readMigrationPackage(dir);
45
+ if (pkg.manifest.migrationId === null) return {
46
+ ok: false,
47
+ reason: "draft"
48
+ };
49
+ const computed = computeMigrationId(pkg.manifest, pkg.ops);
50
+ if (pkg.manifest.migrationId === computed) return {
51
+ ok: true,
52
+ storedMigrationId: pkg.manifest.migrationId,
53
+ computedMigrationId: computed
54
+ };
55
+ return {
56
+ ok: false,
57
+ reason: "mismatch",
58
+ storedMigrationId: pkg.manifest.migrationId,
59
+ computedMigrationId: computed
60
+ };
61
+ }
62
+
63
+ //#endregion
64
+ export { attestMigration, computeMigrationId, verifyMigration };
65
+ //# sourceMappingURL=attestation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation.mjs","names":["sorted: Record<string, unknown>"],"sources":["../../src/canonicalize-json.ts","../../src/attestation.ts"],"sourcesContent":["function sortKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value).sort()) {\n sorted[key] = sortKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n}\n\nexport function canonicalizeJson(value: unknown): string {\n return JSON.stringify(sortKeys(value));\n}\n","import { createHash } from 'node:crypto';\nimport { writeFile } from 'node:fs/promises';\nimport { canonicalizeContract } from '@prisma-next/core-control-plane/emission';\nimport { join } from 'pathe';\nimport { canonicalizeJson } from './canonicalize-json';\nimport { readMigrationPackage } from './io';\nimport type { MigrationManifest, MigrationOps } from './types';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'draft' | 'mismatch';\n readonly storedMigrationId?: string;\n readonly computedMigrationId?: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\nexport function computeMigrationId(manifest: MigrationManifest, ops: MigrationOps): string {\n const {\n migrationId: _migrationId,\n signature: _signature,\n fromContract: _fromContract,\n toContract: _toContract,\n ...strippedMeta\n } = manifest;\n\n const canonicalManifest = canonicalizeJson(strippedMeta);\n const canonicalOps = canonicalizeJson(ops);\n\n const canonicalFromContract =\n manifest.fromContract !== null ? canonicalizeContract(manifest.fromContract) : 'null';\n const canonicalToContract = canonicalizeContract(manifest.toContract);\n\n const partHashes = [\n canonicalManifest,\n canonicalOps,\n canonicalFromContract,\n canonicalToContract,\n ].map(sha256Hex);\n const hash = sha256Hex(canonicalizeJson(partHashes));\n\n return `sha256:${hash}`;\n}\n\nexport async function attestMigration(dir: string): Promise<string> {\n const pkg = await readMigrationPackage(dir);\n const migrationId = computeMigrationId(pkg.manifest, pkg.ops);\n\n const updated = { ...pkg.manifest, migrationId };\n await writeFile(join(dir, 'migration.json'), JSON.stringify(updated, null, 2));\n\n return migrationId;\n}\n\nexport async function verifyMigration(dir: string): Promise<VerifyResult> {\n const pkg = await readMigrationPackage(dir);\n\n if (pkg.manifest.migrationId === null) {\n return { ok: false, reason: 'draft' };\n }\n\n const computed = computeMigrationId(pkg.manifest, pkg.ops);\n\n if (pkg.manifest.migrationId === computed) {\n return { ok: true, storedMigrationId: pkg.manifest.migrationId, computedMigrationId: computed };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedMigrationId: pkg.manifest.migrationId,\n computedMigrationId: computed,\n };\n}\n"],"mappings":";;;;;;;AAAA,SAAS,SAAS,OAAyB;AACzC,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO;AAET,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,SAAS;CAE5B,MAAMA,SAAkC,EAAE;AAC1C,MAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAAC,MAAM,CACzC,QAAO,OAAO,SAAU,MAAkC,KAAK;AAEjE,QAAO;;AAGT,SAAgB,iBAAiB,OAAwB;AACvD,QAAO,KAAK,UAAU,SAAS,MAAM,CAAC;;;;;ACAxC,SAAS,UAAU,OAAuB;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;AAGzD,SAAgB,mBAAmB,UAA6B,KAA2B;CACzF,MAAM,EACJ,aAAa,cACb,WAAW,YACX,cAAc,eACd,YAAY,aACZ,GAAG,iBACD;AAiBJ,QAAO,UAFM,UAAU,iBANJ;EAPO,iBAAiB,aAAa;EACnC,iBAAiB,IAAI;EAGxC,SAAS,iBAAiB,OAAO,qBAAqB,SAAS,aAAa,GAAG;EACrD,qBAAqB,SAAS,WAAW;EAOpE,CAAC,IAAI,UAAU,CACmC,CAAC;;AAKtD,eAAsB,gBAAgB,KAA8B;CAClE,MAAM,MAAM,MAAM,qBAAqB,IAAI;CAC3C,MAAM,cAAc,mBAAmB,IAAI,UAAU,IAAI,IAAI;CAE7D,MAAM,UAAU;EAAE,GAAG,IAAI;EAAU;EAAa;AAChD,OAAM,UAAU,KAAK,KAAK,iBAAiB,EAAE,KAAK,UAAU,SAAS,MAAM,EAAE,CAAC;AAE9E,QAAO;;AAGT,eAAsB,gBAAgB,KAAoC;CACxE,MAAM,MAAM,MAAM,qBAAqB,IAAI;AAE3C,KAAI,IAAI,SAAS,gBAAgB,KAC/B,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAS;CAGvC,MAAM,WAAW,mBAAmB,IAAI,UAAU,IAAI,IAAI;AAE1D,KAAI,IAAI,SAAS,gBAAgB,SAC/B,QAAO;EAAE,IAAI;EAAM,mBAAmB,IAAI,SAAS;EAAa,qBAAqB;EAAU;AAGjG,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,mBAAmB,IAAI,SAAS;EAChC,qBAAqB;EACtB"}
@@ -0,0 +1,30 @@
1
+ import { n as MigrationGraph, o as MigrationPackage, t as MigrationChainEntry } from "../types-CUnzoaLY.mjs";
2
+
3
+ //#region src/dag.d.ts
4
+ declare function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph;
5
+ /**
6
+ * Walk the parent-migration chain to find the latest migration.
7
+ * Returns the migration with no children, or null for an empty graph.
8
+ * Throws AMBIGUOUS_LEAF if the chain branches.
9
+ */
10
+ declare function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null;
11
+ /**
12
+ * Find the leaf contract hash of the migration chain.
13
+ * Convenience wrapper around findLatestMigration.
14
+ */
15
+ declare function findLeaf(graph: MigrationGraph): string;
16
+ /**
17
+ * Find the ordered chain of migrations from `fromHash` to `toHash` by walking the
18
+ * parent-migration chain. Returns the sub-sequence of migrations whose cumulative path
19
+ * goes from `fromHash` to `toHash`.
20
+ *
21
+ * This reconstructs the full chain from root to leaf via parent pointers, then
22
+ * extracts the segment between the two hashes. This correctly handles revisited
23
+ * contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.
24
+ */
25
+ declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly MigrationChainEntry[] | null;
26
+ declare function detectCycles(graph: MigrationGraph): readonly string[][];
27
+ declare function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[];
28
+ //#endregion
29
+ export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, reconstructGraph };
30
+ //# sourceMappingURL=dag.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dag.d.mts","names":[],"sources":["../../src/dag.ts"],"sourcesContent":[],"mappings":";;;iBASgB,gBAAA,oBAAoC,qBAAqB;;AAAzE;AAiEA;AAyCA;AAcA;AA6DgB,iBApHA,mBAAA,CAoHoB,KAAc,EApHP,cAoHO,CAAA,EApHU,mBAoHV,GAAA,IAAA;AAkDlD;;;;iBA7HgB,QAAA,QAAgB;;;;;;;;;;iBAchB,QAAA,QACP,4DAGG;iBAyDI,YAAA,QAAoB;iBAkDpB,aAAA,QAAqB,0BAA0B"}
@@ -0,0 +1,182 @@
1
+ import { i as errorDuplicateMigrationId, l as errorNoRoot, n as errorAmbiguousLeaf, u as errorSelfLoop } from "../errors-DdSjGRqx.mjs";
2
+ import { EMPTY_CONTRACT_HASH } from "@prisma-next/core-control-plane/constants";
3
+
4
+ //#region src/dag.ts
5
+ function reconstructGraph(packages) {
6
+ const nodes = /* @__PURE__ */ new Set();
7
+ const forwardChain = /* @__PURE__ */ new Map();
8
+ const reverseChain = /* @__PURE__ */ new Map();
9
+ const migrationById = /* @__PURE__ */ new Map();
10
+ const childrenByParentId = /* @__PURE__ */ new Map();
11
+ for (const pkg of packages) {
12
+ const { from, to } = pkg.manifest;
13
+ if (from === to) throw errorSelfLoop(pkg.dirName, from);
14
+ nodes.add(from);
15
+ nodes.add(to);
16
+ const migration = {
17
+ from,
18
+ to,
19
+ migrationId: pkg.manifest.migrationId,
20
+ parentMigrationId: pkg.manifest.parentMigrationId,
21
+ dirName: pkg.dirName,
22
+ createdAt: pkg.manifest.createdAt,
23
+ labels: pkg.manifest.labels
24
+ };
25
+ if (migration.migrationId !== null) {
26
+ if (migrationById.has(migration.migrationId)) throw errorDuplicateMigrationId(migration.migrationId);
27
+ migrationById.set(migration.migrationId, migration);
28
+ }
29
+ const parentId = migration.parentMigrationId;
30
+ const siblings = childrenByParentId.get(parentId);
31
+ if (siblings) siblings.push(migration);
32
+ else childrenByParentId.set(parentId, [migration]);
33
+ const fwd = forwardChain.get(from);
34
+ if (fwd) fwd.push(migration);
35
+ else forwardChain.set(from, [migration]);
36
+ const rev = reverseChain.get(to);
37
+ if (rev) rev.push(migration);
38
+ else reverseChain.set(to, [migration]);
39
+ }
40
+ return {
41
+ nodes,
42
+ forwardChain,
43
+ reverseChain,
44
+ migrationById,
45
+ childrenByParentId
46
+ };
47
+ }
48
+ /**
49
+ * Walk the parent-migration chain to find the latest migration.
50
+ * Returns the migration with no children, or null for an empty graph.
51
+ * Throws AMBIGUOUS_LEAF if the chain branches.
52
+ */
53
+ function findLatestMigration(graph) {
54
+ if (graph.nodes.size === 0) return null;
55
+ const roots = graph.childrenByParentId.get(null);
56
+ if (!roots || roots.length === 0) throw errorNoRoot([...graph.nodes].sort());
57
+ if (roots.length > 1) throw errorAmbiguousLeaf(roots.map((e) => e.to));
58
+ let current = roots[0];
59
+ if (!current) throw errorNoRoot([...graph.nodes].sort());
60
+ for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {
61
+ const children = current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : void 0;
62
+ if (!children || children.length === 0) return current;
63
+ if (children.length > 1) throw errorAmbiguousLeaf(children.map((e) => e.to));
64
+ current = children[0];
65
+ }
66
+ throw errorNoRoot([...graph.nodes].sort());
67
+ }
68
+ /**
69
+ * Find the leaf contract hash of the migration chain.
70
+ * Convenience wrapper around findLatestMigration.
71
+ */
72
+ function findLeaf(graph) {
73
+ const migration = findLatestMigration(graph);
74
+ return migration ? migration.to : EMPTY_CONTRACT_HASH;
75
+ }
76
+ /**
77
+ * Find the ordered chain of migrations from `fromHash` to `toHash` by walking the
78
+ * parent-migration chain. Returns the sub-sequence of migrations whose cumulative path
79
+ * goes from `fromHash` to `toHash`.
80
+ *
81
+ * This reconstructs the full chain from root to leaf via parent pointers, then
82
+ * extracts the segment between the two hashes. This correctly handles revisited
83
+ * contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.
84
+ */
85
+ function findPath(graph, fromHash, toHash) {
86
+ if (fromHash === toHash) return [];
87
+ const chain = buildChain(graph);
88
+ if (!chain) return null;
89
+ let startIdx = -1;
90
+ if (chain.length > 0 && chain[0]?.from === fromHash) startIdx = 0;
91
+ else for (let i = chain.length - 1; i >= 0; i--) if (chain[i]?.to === fromHash) {
92
+ startIdx = i + 1;
93
+ break;
94
+ }
95
+ if (startIdx === -1) return null;
96
+ let endIdx = -1;
97
+ for (let i = chain.length - 1; i >= startIdx; i--) if (chain[i]?.to === toHash) {
98
+ endIdx = i + 1;
99
+ break;
100
+ }
101
+ if (endIdx === -1) return null;
102
+ return chain.slice(startIdx, endIdx);
103
+ }
104
+ /**
105
+ * Build the full ordered chain of migrations from root to leaf by following
106
+ * parent pointers. Returns null if the chain cannot be reconstructed
107
+ * (e.g. missing root, branches).
108
+ */
109
+ function buildChain(graph) {
110
+ const roots = graph.childrenByParentId.get(null);
111
+ if (!roots || roots.length !== 1) return null;
112
+ const chain = [];
113
+ let current = roots[0];
114
+ for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {
115
+ chain.push(current);
116
+ const children = current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : void 0;
117
+ if (!children || children.length === 0) break;
118
+ if (children.length > 1) return null;
119
+ current = children[0];
120
+ }
121
+ return chain;
122
+ }
123
+ function detectCycles(graph) {
124
+ const WHITE = 0;
125
+ const GRAY = 1;
126
+ const BLACK = 2;
127
+ const color = /* @__PURE__ */ new Map();
128
+ const parent = /* @__PURE__ */ new Map();
129
+ const cycles = [];
130
+ for (const node of graph.nodes) color.set(node, WHITE);
131
+ function dfs(u) {
132
+ color.set(u, GRAY);
133
+ const outgoing = graph.forwardChain.get(u);
134
+ if (outgoing) for (const edge of outgoing) {
135
+ const v = edge.to;
136
+ if (color.get(v) === GRAY) {
137
+ const cycle = [v];
138
+ let cur = u;
139
+ while (cur !== v) {
140
+ cycle.push(cur);
141
+ cur = parent.get(cur) ?? v;
142
+ }
143
+ cycle.reverse();
144
+ cycles.push(cycle);
145
+ } else if (color.get(v) === WHITE) {
146
+ parent.set(v, u);
147
+ dfs(v);
148
+ }
149
+ }
150
+ color.set(u, BLACK);
151
+ }
152
+ for (const node of graph.nodes) if (color.get(node) === WHITE) {
153
+ parent.set(node, null);
154
+ dfs(node);
155
+ }
156
+ return cycles;
157
+ }
158
+ function detectOrphans(graph) {
159
+ if (graph.nodes.size === 0) return [];
160
+ const reachable = /* @__PURE__ */ new Set();
161
+ const rootMigrations = graph.childrenByParentId.get(null) ?? [];
162
+ const rootHashes = rootMigrations.some((migration) => migration.from === EMPTY_CONTRACT_HASH) ? [EMPTY_CONTRACT_HASH] : [...new Set(rootMigrations.map((migration) => migration.from))];
163
+ const queue = rootHashes.length > 0 ? rootHashes : [EMPTY_CONTRACT_HASH];
164
+ for (const hash of queue) reachable.add(hash);
165
+ while (queue.length > 0) {
166
+ const node = queue.shift();
167
+ if (node === void 0) break;
168
+ const outgoing = graph.forwardChain.get(node);
169
+ if (!outgoing) continue;
170
+ for (const migration of outgoing) if (!reachable.has(migration.to)) {
171
+ reachable.add(migration.to);
172
+ queue.push(migration.to);
173
+ }
174
+ }
175
+ const orphans = [];
176
+ for (const [from, migrations] of graph.forwardChain) if (!reachable.has(from)) orphans.push(...migrations);
177
+ return orphans;
178
+ }
179
+
180
+ //#endregion
181
+ export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, reconstructGraph };
182
+ //# sourceMappingURL=dag.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dag.mjs","names":["migration: MigrationChainEntry","children: readonly MigrationChainEntry[] | undefined","chain: MigrationChainEntry[]","current: MigrationChainEntry | undefined","cycles: string[][]","cycle: string[]","queue: string[]","orphans: MigrationChainEntry[]"],"sources":["../../src/dag.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';\nimport {\n errorAmbiguousLeaf,\n errorDuplicateMigrationId,\n errorNoRoot,\n errorSelfLoop,\n} from './errors';\nimport type { MigrationChainEntry, MigrationGraph, MigrationPackage } from './types';\n\nexport function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationChainEntry[]>();\n const reverseChain = new Map<string, MigrationChainEntry[]>();\n const migrationById = new Map<string, MigrationChainEntry>();\n const childrenByParentId = new Map<string | null, MigrationChainEntry[]>();\n\n for (const pkg of packages) {\n const { from, to } = pkg.manifest;\n\n if (from === to) {\n throw errorSelfLoop(pkg.dirName, from);\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationChainEntry = {\n from,\n to,\n migrationId: pkg.manifest.migrationId,\n parentMigrationId: pkg.manifest.parentMigrationId,\n dirName: pkg.dirName,\n createdAt: pkg.manifest.createdAt,\n labels: pkg.manifest.labels,\n };\n\n if (migration.migrationId !== null) {\n if (migrationById.has(migration.migrationId)) {\n throw errorDuplicateMigrationId(migration.migrationId);\n }\n migrationById.set(migration.migrationId, migration);\n }\n\n const parentId = migration.parentMigrationId;\n const siblings = childrenByParentId.get(parentId);\n if (siblings) {\n siblings.push(migration);\n } else {\n childrenByParentId.set(parentId, [migration]);\n }\n\n const fwd = forwardChain.get(from);\n if (fwd) {\n fwd.push(migration);\n } else {\n forwardChain.set(from, [migration]);\n }\n\n const rev = reverseChain.get(to);\n if (rev) {\n rev.push(migration);\n } else {\n reverseChain.set(to, [migration]);\n }\n }\n\n return { nodes, forwardChain, reverseChain, migrationById, childrenByParentId };\n}\n\n/**\n * Walk the parent-migration chain to find the latest migration.\n * Returns the migration with no children, or null for an empty graph.\n * Throws AMBIGUOUS_LEAF if the chain branches.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n const roots = graph.childrenByParentId.get(null);\n if (!roots || roots.length === 0) {\n throw errorNoRoot([...graph.nodes].sort());\n }\n\n if (roots.length > 1) {\n throw errorAmbiguousLeaf(roots.map((e) => e.to));\n }\n\n let current = roots[0];\n if (!current) {\n throw errorNoRoot([...graph.nodes].sort());\n }\n\n for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {\n const children: readonly MigrationChainEntry[] | undefined =\n current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : undefined;\n\n if (!children || children.length === 0) {\n return current;\n }\n\n if (children.length > 1) {\n throw errorAmbiguousLeaf(children.map((e) => e.to));\n }\n\n current = children[0];\n }\n\n throw errorNoRoot([...graph.nodes].sort());\n}\n\n/**\n * Find the leaf contract hash of the migration chain.\n * Convenience wrapper around findLatestMigration.\n */\nexport function findLeaf(graph: MigrationGraph): string {\n const migration = findLatestMigration(graph);\n return migration ? migration.to : EMPTY_CONTRACT_HASH;\n}\n\n/**\n * Find the ordered chain of migrations from `fromHash` to `toHash` by walking the\n * parent-migration chain. Returns the sub-sequence of migrations whose cumulative path\n * goes from `fromHash` to `toHash`.\n *\n * This reconstructs the full chain from root to leaf via parent pointers, then\n * extracts the segment between the two hashes. This correctly handles revisited\n * contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationChainEntry[] | null {\n if (fromHash === toHash) return [];\n\n const chain = buildChain(graph);\n if (!chain) return null;\n\n let startIdx = -1;\n if (chain.length > 0 && chain[0]?.from === fromHash) {\n startIdx = 0;\n } else {\n for (let i = chain.length - 1; i >= 0; i--) {\n if (chain[i]?.to === fromHash) {\n startIdx = i + 1;\n break;\n }\n }\n }\n\n if (startIdx === -1) return null;\n\n let endIdx = -1;\n for (let i = chain.length - 1; i >= startIdx; i--) {\n if (chain[i]?.to === toHash) {\n endIdx = i + 1;\n break;\n }\n }\n\n if (endIdx === -1) return null;\n\n return chain.slice(startIdx, endIdx);\n}\n\n/**\n * Build the full ordered chain of migrations from root to leaf by following\n * parent pointers. Returns null if the chain cannot be reconstructed\n * (e.g. missing root, branches).\n */\nfunction buildChain(graph: MigrationGraph): readonly MigrationChainEntry[] | null {\n const roots = graph.childrenByParentId.get(null);\n if (!roots || roots.length !== 1) return null;\n\n const chain: MigrationChainEntry[] = [];\n let current: MigrationChainEntry | undefined = roots[0];\n\n for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {\n chain.push(current);\n const children =\n current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : undefined;\n if (!children || children.length === 0) break;\n if (children.length > 1) return null;\n current = children[0];\n }\n\n return chain;\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 parent = 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 function dfs(u: string): void {\n color.set(u, GRAY);\n\n const outgoing = graph.forwardChain.get(u);\n if (outgoing) {\n for (const edge of outgoing) {\n const v = edge.to;\n if (color.get(v) === GRAY) {\n // Back edge found — reconstruct cycle\n const cycle: string[] = [v];\n let cur = u;\n while (cur !== v) {\n cycle.push(cur);\n cur = parent.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (color.get(v) === WHITE) {\n parent.set(v, u);\n dfs(v);\n }\n }\n }\n\n color.set(u, BLACK);\n }\n\n for (const node of graph.nodes) {\n if (color.get(node) === WHITE) {\n parent.set(node, null);\n dfs(node);\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const rootMigrations = graph.childrenByParentId.get(null) ?? [];\n const emptyRootExists = rootMigrations.some(\n (migration) => migration.from === EMPTY_CONTRACT_HASH,\n );\n const rootHashes = emptyRootExists\n ? [EMPTY_CONTRACT_HASH]\n : [...new Set(rootMigrations.map((migration) => migration.from))];\n const queue: string[] = rootHashes.length > 0 ? rootHashes : [EMPTY_CONTRACT_HASH];\n\n for (const hash of queue) {\n reachable.add(hash);\n }\n\n while (queue.length > 0) {\n const node = queue.shift();\n if (node === undefined) break;\n const outgoing = graph.forwardChain.get(node);\n if (!outgoing) continue;\n\n for (const migration of outgoing) {\n if (!reachable.has(migration.to)) {\n reachable.add(migration.to);\n queue.push(migration.to);\n }\n }\n }\n\n const orphans: MigrationChainEntry[] = [];\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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"}
@@ -0,0 +1,10 @@
1
+ import { a as MigrationOps, i as MigrationManifest, o as MigrationPackage } from "../types-CUnzoaLY.mjs";
2
+
3
+ //#region src/io.d.ts
4
+ declare function writeMigrationPackage(dir: string, manifest: MigrationManifest, ops: MigrationOps): Promise<void>;
5
+ declare function readMigrationPackage(dir: string): Promise<MigrationPackage>;
6
+ declare function readMigrationsDir(migrationsRoot: string): Promise<readonly MigrationPackage[]>;
7
+ declare function formatMigrationDirName(timestamp: Date, slug: string): string;
8
+ //#endregion
9
+ export { formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationPackage };
10
+ //# sourceMappingURL=io.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.d.mts","names":[],"sources":["../../src/io.ts"],"sourcesContent":[],"mappings":";;;iBAyDsB,qBAAA,wBAEV,wBACL,eACJ;iBAgBmB,oBAAA,eAAmC,QAAQ;AApB3C,iBAsFA,iBAAA,CAtFqB,cAAA,EAAA,MAAA,CAAA,EAwFxC,OAxFwC,CAAA,SAwFvB,gBAxFuB,EAAA,CAAA;AAE/B,iBAqHI,sBAAA,CArHJ,SAAA,EAqHsC,IArHtC,EAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAAA"}
@@ -0,0 +1,3 @@
1
+ import { i as writeMigrationPackage, n as readMigrationPackage, r as readMigrationsDir, t as formatMigrationDirName } from "../io-Dx98-h0p.mjs";
2
+
3
+ export { formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationPackage };
@@ -0,0 +1,35 @@
1
+ import { a as MigrationOps, i as MigrationManifest, n as MigrationGraph, o as MigrationPackage, r as MigrationHints, t as MigrationChainEntry } from "../types-CUnzoaLY.mjs";
2
+
3
+ //#region src/errors.d.ts
4
+
5
+ /**
6
+ * Structured error for migration tooling operations.
7
+ *
8
+ * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
9
+ * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
10
+ * migration-chain reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
11
+ * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
12
+ *
13
+ * Fields:
14
+ * - code: Stable machine-readable code (MIGRATION.SUBCODE)
15
+ * - category: Always 'MIGRATION'
16
+ * - why: Explains the cause in plain language
17
+ * - fix: Actionable remediation step
18
+ * - details: Machine-readable structured data for agents
19
+ */
20
+ declare class MigrationToolsError extends Error {
21
+ readonly code: string;
22
+ readonly category: "MIGRATION";
23
+ readonly why: string;
24
+ readonly fix: string;
25
+ readonly details: Record<string, unknown> | undefined;
26
+ constructor(code: string, summary: string, options: {
27
+ readonly why: string;
28
+ readonly fix: string;
29
+ readonly details?: Record<string, unknown>;
30
+ });
31
+ static is(error: unknown): error is MigrationToolsError;
32
+ }
33
+ //#endregion
34
+ export { type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, type MigrationPackage, MigrationToolsError };
35
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/errors.ts"],"sourcesContent":[],"mappings":";;;;;;;AAeA;;;;;;;;;;;;cAAa,mBAAA,SAA4B,KAAA;;;;;oBAKrB;;;;uBAQK;;sCAWa"}
@@ -0,0 +1,3 @@
1
+ import { t as MigrationToolsError } from "../errors-DdSjGRqx.mjs";
2
+
3
+ export { MigrationToolsError };